from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from django.db.models import Q from apps.users.models import ApiKey def normalize_resolution(value: str | None) -> str: return "" if value is None else str(value).strip().upper() class CallRecord(models.Model): class OperationType(models.TextChoices): TITLE = "title", "生成标题" IMAGE = "image", "生成图片" class Status(models.TextChoices): PENDING = "pending", "待处理" SUCCESS = "success", "成功" FAILED = "failed", "失败" user = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name="用户", on_delete=models.PROTECT, related_name="call_records", ) api_key = models.ForeignKey( ApiKey, verbose_name="API 密钥", null=True, blank=True, on_delete=models.SET_NULL, related_name="call_records", ) operation_type = models.CharField("操作类型", max_length=32, choices=OperationType.choices) alias = models.CharField("能力别名", max_length=64, blank=True) model_used = models.CharField("实际模型", max_length=128, blank=True) resolution = models.CharField("分辨率", max_length=32, blank=True) prompt = models.TextField("提示词", blank=True) points_cost = models.BigIntegerField("扣费点数", default=0) status = models.CharField( "状态", max_length=20, choices=Status.choices, default=Status.PENDING, ) upstream_latency_ms = models.PositiveIntegerField("上游耗时毫秒", null=True, blank=True) error_message = models.TextField("错误信息", blank=True) result_ref = models.TextField("结果引用", blank=True) result_summary = models.TextField("结果摘要", blank=True) created_at = models.DateTimeField("创建时间", auto_now_add=True) updated_at = models.DateTimeField("更新时间", auto_now=True) class Meta: db_table = "call_record" verbose_name = "调用记录" verbose_name_plural = "调用记录" ordering = ("-created_at", "-id") constraints = [ models.CheckConstraint( condition=Q(points_cost__gte=0), name="call_record_points_cost_non_negative", ), ] indexes = [ models.Index(fields=("user", "created_at")), models.Index(fields=("api_key", "created_at")), models.Index(fields=("operation_type", "status")), models.Index(fields=("alias",)), ] def __str__(self) -> str: return f"{self.operation_type}:{self.alias or ''} {self.status}" class PricingRule(models.Model): operation_type = models.CharField("操作类型", max_length=32, choices=CallRecord.OperationType.choices) alias = models.CharField("能力别名", max_length=64) resolution = models.CharField( "分辨率", max_length=32, blank=True, default="", help_text="留空表示该别名所有分辨率通用。", ) points_cost = models.BigIntegerField("点数单价") is_active = models.BooleanField("启用", default=True) created_at = models.DateTimeField("创建时间", auto_now_add=True) updated_at = models.DateTimeField("更新时间", auto_now=True) class Meta: db_table = "pricing_rule" verbose_name = "计费规则" verbose_name_plural = "计费规则" ordering = ("operation_type", "alias", "resolution") constraints = [ models.UniqueConstraint( fields=("operation_type", "alias", "resolution"), name="unique_pricing_rule_per_alias_resolution", ), models.CheckConstraint( condition=Q(points_cost__gt=0), name="pricing_rule_points_cost_positive", ), ] indexes = [ models.Index(fields=("operation_type", "alias", "is_active")), models.Index(fields=("operation_type", "alias", "resolution", "is_active")), ] def __str__(self) -> str: resolution = self.resolution or "*" return f"{self.operation_type}:{self.alias}:{resolution} = {self.points_cost}" def clean(self) -> None: super().clean() self.alias = str(self.alias or "").strip() self.resolution = normalize_resolution(self.resolution) if not self.alias: raise ValidationError({"alias": "Alias is required."}) def save(self, *args, **kwargs) -> None: self.full_clean() super().save(*args, **kwargs) class ExchangeRate(models.Model): currency = models.CharField("币种", max_length=10, default="CNY") points_per_unit = models.DecimalField("每单位金额点数", max_digits=12, decimal_places=4) is_active = models.BooleanField("启用", default=True) effective_from = models.DateTimeField("生效时间") note = models.CharField("备注", max_length=255, blank=True) created_at = models.DateTimeField("创建时间", auto_now_add=True) updated_at = models.DateTimeField("更新时间", auto_now=True) class Meta: db_table = "exchange_rate" verbose_name = "汇率" verbose_name_plural = "汇率" ordering = ("-effective_from", "-id") constraints = [ models.CheckConstraint( condition=Q(points_per_unit__gt=0), name="exchange_rate_points_per_unit_positive", ), ] indexes = [ models.Index(fields=("currency", "is_active", "effective_from")), ] def __str__(self) -> str: return f"{self.currency} 1 = {self.points_per_unit} points" def clean(self) -> None: super().clean() self.currency = str(self.currency or "").strip().upper() if not self.currency: raise ValidationError({"currency": "Currency is required."}) def save(self, *args, **kwargs) -> None: self.full_clean() super().save(*args, **kwargs) class RechargeOrder(models.Model): class PayMethod(models.TextChoices): WEIXIN = "weixin", "微信" ALIPAY = "alipay", "支付宝" class Status(models.TextChoices): PENDING = "pending", "待支付" PAID = "paid", "已支付" FAILED = "failed", "失败" EXPIRED = "expired", "已过期" order_no = models.CharField("订单号", max_length=64, unique=True) user = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name="用户", on_delete=models.PROTECT, related_name="recharge_orders", ) amount_money = models.DecimalField("金额", max_digits=12, decimal_places=2) currency = models.CharField("币种", max_length=10, default="CNY") pay_method = models.CharField("支付方式", max_length=20, choices=PayMethod.choices) exchange_rate = models.DecimalField("锁定汇率", max_digits=12, decimal_places=4) points_granted = models.BigIntegerField("到账点数") status = models.CharField( "状态", max_length=20, choices=Status.choices, default=Status.PENDING, ) code_url = models.TextField("二维码票据", blank=True) expires_at = models.DateTimeField("过期时间", null=True, blank=True) payment_txn_no = models.CharField("支付交易号", max_length=128, blank=True) paid_at = models.DateTimeField("支付时间", null=True, blank=True) created_at = models.DateTimeField("创建时间", auto_now_add=True) updated_at = models.DateTimeField("更新时间", auto_now=True) class Meta: db_table = "recharge_order" verbose_name = "充值订单" verbose_name_plural = "充值订单" ordering = ("-created_at", "-id") constraints = [ models.CheckConstraint( condition=Q(amount_money__gt=0), name="recharge_order_amount_money_positive", ), models.CheckConstraint( condition=Q(exchange_rate__gt=0), name="recharge_order_exchange_rate_positive", ), models.CheckConstraint( condition=Q(points_granted__gt=0), name="recharge_order_points_granted_positive", ), ] indexes = [ models.Index(fields=("user", "created_at")), models.Index(fields=("status", "created_at")), models.Index(fields=("pay_method", "status")), ] def __str__(self) -> str: return f"{self.order_no} {self.status}" def clean(self) -> None: super().clean() self.order_no = str(self.order_no or "").strip() self.currency = str(self.currency or "").strip().upper() if not self.order_no: raise ValidationError({"order_no": "Order number is required."}) if not self.currency: raise ValidationError({"currency": "Currency is required."}) def save(self, *args, **kwargs) -> None: self.full_clean() super().save(*args, **kwargs) class PointsLedger(models.Model): class ChangeType(models.TextChoices): RECHARGE = "recharge", "充值" CONSUME = "consume", "消费" ADJUST = "adjust", "调整" REFUND = "refund", "退款" user = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name="用户", on_delete=models.PROTECT, related_name="points_ledger_entries", ) change_type = models.CharField("变动类型", max_length=20, choices=ChangeType.choices) points_delta = models.BigIntegerField("点数变动") balance_after = models.BigIntegerField("变动后余额") ref_order_id = models.PositiveBigIntegerField("关联订单 ID", null=True, blank=True) ref_call = models.ForeignKey( CallRecord, verbose_name="关联调用", null=True, blank=True, on_delete=models.PROTECT, related_name="ledger_entries", ) reason = models.TextField("原因", blank=True) created_at = models.DateTimeField("创建时间", auto_now_add=True) class Meta: db_table = "points_ledger" verbose_name = "点数流水" verbose_name_plural = "点数流水" ordering = ("-created_at", "-id") constraints = [ models.CheckConstraint( condition=Q(balance_after__gte=0), name="points_ledger_balance_after_non_negative", ), models.CheckConstraint( condition=~Q(points_delta=0), name="points_ledger_points_delta_non_zero", ), models.UniqueConstraint( fields=("ref_call", "change_type"), name="unique_ledger_change_type_per_call", ), models.UniqueConstraint( fields=("ref_order_id", "change_type"), name="unique_ledger_change_type_per_order", ), ] indexes = [ models.Index(fields=("user", "created_at")), models.Index(fields=("change_type", "created_at")), models.Index(fields=("ref_order_id",)), models.Index(fields=("ref_call",)), ] def __str__(self) -> str: return f"{self.user} {self.change_type} {self.points_delta}" def clean(self) -> None: super().clean() if self.change_type == self.ChangeType.ADJUST and not (self.reason or "").strip(): raise ValidationError({"reason": "Adjust ledger entries require a reason."})