feat: 中文化 django-admin 字段标签 (T-603)

This commit is contained in:
QiuSW
2026-07-04 16:27:45 +08:00
parent 4aceb3ae25
commit fb1037933d
14 changed files with 714 additions and 139 deletions
+11 -11
View File
@@ -8,10 +8,10 @@ from .security import AiKeyEncryptionError, encrypt_api_key
class AiModelAdminForm(forms.ModelForm):
api_key = forms.CharField(
label="API key",
label="API Key",
required=False,
widget=forms.PasswordInput(render_value=False),
help_text="Leave blank to keep the existing encrypted key.",
help_text="留空表示保留已有加密密钥。",
)
class Meta:
@@ -33,7 +33,7 @@ class AiModelAdminForm(forms.ModelForm):
cleaned_data = super().clean()
api_key = cleaned_data.get("api_key")
if not self.instance.pk and not api_key:
raise forms.ValidationError("API key is required when creating an AI model.")
raise forms.ValidationError("创建 AI 模型时必须填写 API Key。")
if api_key:
try:
self._api_key_encrypted = encrypt_api_key(api_key)
@@ -71,14 +71,14 @@ class AiModelAdmin(admin.ModelAdmin):
fieldsets = (
(None, {"fields": ("name", "url", "model", "api_type", "is_active")}),
(
"Credentials",
"凭证",
{
"fields": ("api_key", "api_key_status"),
"description": "The stored key is encrypted and never displayed.",
"description": "已保存的密钥会加密存储,后台不会回显明文。",
},
),
(
"Capabilities and request defaults",
"能力与请求默认值",
{
"fields": (
"capabilities",
@@ -88,16 +88,16 @@ class AiModelAdmin(admin.ModelAdmin):
)
},
),
("Timestamps", {"fields": ("created_at", "updated_at")}),
("时间信息", {"fields": ("created_at", "updated_at")}),
)
@admin.display(description="capabilities")
@admin.display(description="能力")
def capabilities_display(self, obj):
return ", ".join(sorted(obj.capabilities_set()))
@admin.display(description="API key")
@admin.display(description="API Key")
def api_key_status(self, obj):
return obj.api_key_masked or "not set"
return obj.api_key_masked or "未设置"
def save_model(self, request, obj, form, change):
before = _snapshot_existing(obj) if change else None
@@ -241,7 +241,7 @@ class AiConfigAuditLogAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=None):
return False
@admin.display(description="changed fields")
@admin.display(description="变更字段")
def changed_fields_display(self, obj):
return ", ".join(obj.changed_fields)
@@ -0,0 +1,151 @@
# Generated by Django 5.2.15 on 2026-07-04 08:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ai', '0003_alter_aiconfigauditlog_options_alter_aimodel_options_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='aiconfigauditlog',
name='action',
field=models.CharField(choices=[('create', '创建'), ('update', '更新'), ('delete', '删除')], max_length=16, verbose_name='动作'),
),
migrations.AlterField(
model_name='aiconfigauditlog',
name='actor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='操作人'),
),
migrations.AlterField(
model_name='aiconfigauditlog',
name='changed_fields',
field=models.JSONField(blank=True, default=list, verbose_name='变更字段'),
),
migrations.AlterField(
model_name='aiconfigauditlog',
name='changes',
field=models.JSONField(blank=True, default=dict, verbose_name='变更内容'),
),
migrations.AlterField(
model_name='aiconfigauditlog',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='aiconfigauditlog',
name='target_id',
field=models.PositiveBigIntegerField(blank=True, null=True, verbose_name='目标 ID'),
),
migrations.AlterField(
model_name='aiconfigauditlog',
name='target_repr',
field=models.CharField(max_length=255, verbose_name='目标描述'),
),
migrations.AlterField(
model_name='aiconfigauditlog',
name='target_type',
field=models.CharField(choices=[('ai_model', 'AI 模型'), ('model_alias', '能力别名')], max_length=32, verbose_name='目标类型'),
),
migrations.AlterField(
model_name='aimodel',
name='api_key_encrypted',
field=models.TextField(blank=True, editable=False, verbose_name='加密 API Key'),
),
migrations.AlterField(
model_name='aimodel',
name='api_type',
field=models.CharField(choices=[('auto', '自动'), ('chat', '聊天补全'), ('gemini', 'Gemini'), ('images', '图片生成'), ('images_edits', '图片编辑')], default='auto', max_length=32, verbose_name='API 类型'),
),
migrations.AlterField(
model_name='aimodel',
name='capabilities',
field=models.JSONField(blank=True, default=list, verbose_name='能力'),
),
migrations.AlterField(
model_name='aimodel',
name='connect_timeout_seconds',
field=models.PositiveIntegerField(default=30, verbose_name='连接超时秒数'),
),
migrations.AlterField(
model_name='aimodel',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='aimodel',
name='extra_body',
field=models.JSONField(blank=True, default=dict, verbose_name='额外请求体'),
),
migrations.AlterField(
model_name='aimodel',
name='is_active',
field=models.BooleanField(default=True, verbose_name='启用'),
),
migrations.AlterField(
model_name='aimodel',
name='model',
field=models.CharField(max_length=128, verbose_name='模型标识'),
),
migrations.AlterField(
model_name='aimodel',
name='name',
field=models.CharField(max_length=128, unique=True, verbose_name='名称'),
),
migrations.AlterField(
model_name='aimodel',
name='timeout_seconds',
field=models.PositiveIntegerField(default=0, help_text='0 表示使用 provider 默认值。', verbose_name='请求超时秒数'),
),
migrations.AlterField(
model_name='aimodel',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
),
migrations.AlterField(
model_name='aimodel',
name='url',
field=models.URLField(max_length=500, verbose_name='接口 URL'),
),
migrations.AlterField(
model_name='modelalias',
name='ai_model',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='aliases', to='ai.aimodel', verbose_name='AI 模型'),
),
migrations.AlterField(
model_name='modelalias',
name='alias',
field=models.SlugField(max_length=64, verbose_name='能力别名'),
),
migrations.AlterField(
model_name='modelalias',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='modelalias',
name='is_active',
field=models.BooleanField(default=True, verbose_name='启用'),
),
migrations.AlterField(
model_name='modelalias',
name='is_default',
field=models.BooleanField(default=False, verbose_name='默认别名'),
),
migrations.AlterField(
model_name='modelalias',
name='operation_type',
field=models.CharField(choices=[('title', '生成标题'), ('image', '生成图片')], max_length=32, verbose_name='操作类型'),
),
migrations.AlterField(
model_name='modelalias',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
),
]
+39 -35
View File
@@ -11,31 +11,33 @@ from .security import decrypt_api_key, encrypt_api_key, mask_secret
class AiModel(models.Model):
class ApiType(models.TextChoices):
AUTO = "auto", "Auto"
CHAT = "chat", "Chat completions"
AUTO = "auto", "自动"
CHAT = "chat", "聊天补全"
GEMINI = "gemini", "Gemini"
IMAGES = "images", "Images generations"
IMAGES_EDITS = "images_edits", "Images edits"
IMAGES = "images", "图片生成"
IMAGES_EDITS = "images_edits", "图片编辑"
name = models.CharField(max_length=128, unique=True)
url = models.URLField(max_length=500)
model = models.CharField(max_length=128)
name = models.CharField("名称", max_length=128, unique=True)
url = models.URLField("接口 URL", max_length=500)
model = models.CharField("模型标识", max_length=128)
api_type = models.CharField(
"API 类型",
max_length=32,
choices=ApiType.choices,
default=ApiType.AUTO,
)
api_key_encrypted = models.TextField(blank=True, editable=False)
capabilities = models.JSONField(default=list, blank=True)
api_key_encrypted = models.TextField("加密 API Key", blank=True, editable=False)
capabilities = models.JSONField("能力", default=list, blank=True)
timeout_seconds = models.PositiveIntegerField(
"请求超时秒数",
default=0,
help_text="0 means use provider resolution defaults.",
help_text="0 表示使用 provider 默认值。",
)
connect_timeout_seconds = models.PositiveIntegerField(default=30)
extra_body = models.JSONField(default=dict, blank=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
connect_timeout_seconds = models.PositiveIntegerField("连接超时秒数", default=30)
extra_body = models.JSONField("额外请求体", default=dict, blank=True)
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 = "ai_model"
@@ -88,20 +90,21 @@ class AiModel(models.Model):
class ModelAlias(models.Model):
class OperationType(models.TextChoices):
TITLE = "title", "Generate title"
IMAGE = "image", "Generate image"
TITLE = "title", "生成标题"
IMAGE = "image", "生成图片"
alias = models.SlugField(max_length=64)
operation_type = models.CharField(max_length=32, choices=OperationType.choices)
alias = models.SlugField("能力别名", max_length=64)
operation_type = models.CharField("操作类型", max_length=32, choices=OperationType.choices)
ai_model = models.ForeignKey(
AiModel,
verbose_name="AI 模型",
on_delete=models.PROTECT,
related_name="aliases",
)
is_default = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_default = models.BooleanField("默认别名", default=False)
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 = "model_alias"
@@ -142,28 +145,29 @@ class ModelAlias(models.Model):
class AiConfigAuditLog(models.Model):
class TargetType(models.TextChoices):
AI_MODEL = "ai_model", "AI model"
MODEL_ALIAS = "model_alias", "Model alias"
AI_MODEL = "ai_model", "AI 模型"
MODEL_ALIAS = "model_alias", "能力别名"
class Action(models.TextChoices):
CREATE = "create", "Create"
UPDATE = "update", "Update"
DELETE = "delete", "Delete"
CREATE = "create", "创建"
UPDATE = "update", "更新"
DELETE = "delete", "删除"
actor = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name="操作人",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
action = models.CharField(max_length=16, choices=Action.choices)
target_type = models.CharField(max_length=32, choices=TargetType.choices)
target_id = models.PositiveBigIntegerField(null=True, blank=True)
target_repr = models.CharField(max_length=255)
changed_fields = models.JSONField(default=list, blank=True)
changes = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
action = models.CharField("动作", max_length=16, choices=Action.choices)
target_type = models.CharField("目标类型", max_length=32, choices=TargetType.choices)
target_id = models.PositiveBigIntegerField("目标 ID", null=True, blank=True)
target_repr = models.CharField("目标描述", max_length=255)
changed_fields = models.JSONField("变更字段", default=list, blank=True)
changes = models.JSONField("变更内容", default=dict, blank=True)
created_at = models.DateTimeField("创建时间", auto_now_add=True)
class Meta:
db_table = "ai_config_audit_log"
+1 -1
View File
@@ -51,7 +51,7 @@ class PricingRuleAdmin(admin.ModelAdmin):
readonly_fields = ("created_at", "updated_at")
list_editable = ("points_cost", "is_active")
@admin.display(description="resolution")
@admin.display(description="分辨率")
def resolution_display(self, obj):
return obj.resolution or "*"
@@ -0,0 +1,272 @@
# Generated by Django 5.2.15 on 2026-07-04 08:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0005_alter_callrecord_options_alter_exchangerate_options_and_more'),
('users', '0005_alter_apikey_created_at_alter_apikey_key_hash_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='callrecord',
name='alias',
field=models.CharField(blank=True, max_length=64, verbose_name='能力别名'),
),
migrations.AlterField(
model_name='callrecord',
name='api_key',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='call_records', to='users.apikey', verbose_name='API 密钥'),
),
migrations.AlterField(
model_name='callrecord',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='callrecord',
name='error_message',
field=models.TextField(blank=True, verbose_name='错误信息'),
),
migrations.AlterField(
model_name='callrecord',
name='model_used',
field=models.CharField(blank=True, max_length=128, verbose_name='实际模型'),
),
migrations.AlterField(
model_name='callrecord',
name='operation_type',
field=models.CharField(choices=[('title', '生成标题'), ('image', '生成图片')], max_length=32, verbose_name='操作类型'),
),
migrations.AlterField(
model_name='callrecord',
name='points_cost',
field=models.BigIntegerField(default=0, verbose_name='扣费点数'),
),
migrations.AlterField(
model_name='callrecord',
name='prompt',
field=models.TextField(blank=True, verbose_name='提示词'),
),
migrations.AlterField(
model_name='callrecord',
name='resolution',
field=models.CharField(blank=True, max_length=32, verbose_name='分辨率'),
),
migrations.AlterField(
model_name='callrecord',
name='result_ref',
field=models.TextField(blank=True, verbose_name='结果引用'),
),
migrations.AlterField(
model_name='callrecord',
name='result_summary',
field=models.TextField(blank=True, verbose_name='结果摘要'),
),
migrations.AlterField(
model_name='callrecord',
name='status',
field=models.CharField(choices=[('pending', '待处理'), ('success', '成功'), ('failed', '失败')], default='pending', max_length=20, verbose_name='状态'),
),
migrations.AlterField(
model_name='callrecord',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
),
migrations.AlterField(
model_name='callrecord',
name='upstream_latency_ms',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='上游耗时毫秒'),
),
migrations.AlterField(
model_name='callrecord',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='call_records', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
),
migrations.AlterField(
model_name='exchangerate',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='exchangerate',
name='currency',
field=models.CharField(default='CNY', max_length=10, verbose_name='币种'),
),
migrations.AlterField(
model_name='exchangerate',
name='effective_from',
field=models.DateTimeField(verbose_name='生效时间'),
),
migrations.AlterField(
model_name='exchangerate',
name='is_active',
field=models.BooleanField(default=True, verbose_name='启用'),
),
migrations.AlterField(
model_name='exchangerate',
name='note',
field=models.CharField(blank=True, max_length=255, verbose_name='备注'),
),
migrations.AlterField(
model_name='exchangerate',
name='points_per_unit',
field=models.DecimalField(decimal_places=4, max_digits=12, verbose_name='每单位金额点数'),
),
migrations.AlterField(
model_name='exchangerate',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
),
migrations.AlterField(
model_name='pointsledger',
name='balance_after',
field=models.BigIntegerField(verbose_name='变动后余额'),
),
migrations.AlterField(
model_name='pointsledger',
name='change_type',
field=models.CharField(choices=[('recharge', '充值'), ('consume', '消费'), ('adjust', '调整'), ('refund', '退款')], max_length=20, verbose_name='变动类型'),
),
migrations.AlterField(
model_name='pointsledger',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='pointsledger',
name='points_delta',
field=models.BigIntegerField(verbose_name='点数变动'),
),
migrations.AlterField(
model_name='pointsledger',
name='reason',
field=models.TextField(blank=True, verbose_name='原因'),
),
migrations.AlterField(
model_name='pointsledger',
name='ref_call',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ledger_entries', to='billing.callrecord', verbose_name='关联调用'),
),
migrations.AlterField(
model_name='pointsledger',
name='ref_order_id',
field=models.PositiveBigIntegerField(blank=True, null=True, verbose_name='关联订单 ID'),
),
migrations.AlterField(
model_name='pointsledger',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='points_ledger_entries', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
),
migrations.AlterField(
model_name='pricingrule',
name='alias',
field=models.CharField(max_length=64, verbose_name='能力别名'),
),
migrations.AlterField(
model_name='pricingrule',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='pricingrule',
name='is_active',
field=models.BooleanField(default=True, verbose_name='启用'),
),
migrations.AlterField(
model_name='pricingrule',
name='operation_type',
field=models.CharField(choices=[('title', '生成标题'), ('image', '生成图片')], max_length=32, verbose_name='操作类型'),
),
migrations.AlterField(
model_name='pricingrule',
name='points_cost',
field=models.BigIntegerField(verbose_name='点数单价'),
),
migrations.AlterField(
model_name='pricingrule',
name='resolution',
field=models.CharField(blank=True, default='', help_text='留空表示该别名所有分辨率通用。', max_length=32, verbose_name='分辨率'),
),
migrations.AlterField(
model_name='pricingrule',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
),
migrations.AlterField(
model_name='rechargeorder',
name='amount_money',
field=models.DecimalField(decimal_places=2, max_digits=12, verbose_name='金额'),
),
migrations.AlterField(
model_name='rechargeorder',
name='code_url',
field=models.TextField(blank=True, verbose_name='二维码票据'),
),
migrations.AlterField(
model_name='rechargeorder',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='rechargeorder',
name='currency',
field=models.CharField(default='CNY', max_length=10, verbose_name='币种'),
),
migrations.AlterField(
model_name='rechargeorder',
name='exchange_rate',
field=models.DecimalField(decimal_places=4, max_digits=12, verbose_name='锁定汇率'),
),
migrations.AlterField(
model_name='rechargeorder',
name='expires_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='过期时间'),
),
migrations.AlterField(
model_name='rechargeorder',
name='order_no',
field=models.CharField(max_length=64, unique=True, verbose_name='订单号'),
),
migrations.AlterField(
model_name='rechargeorder',
name='paid_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='支付时间'),
),
migrations.AlterField(
model_name='rechargeorder',
name='pay_method',
field=models.CharField(choices=[('weixin', '微信'), ('alipay', '支付宝')], max_length=20, verbose_name='支付方式'),
),
migrations.AlterField(
model_name='rechargeorder',
name='payment_txn_no',
field=models.CharField(blank=True, max_length=128, verbose_name='支付交易号'),
),
migrations.AlterField(
model_name='rechargeorder',
name='points_granted',
field=models.BigIntegerField(verbose_name='到账点数'),
),
migrations.AlterField(
model_name='rechargeorder',
name='status',
field=models.CharField(choices=[('pending', '待支付'), ('paid', '已支付'), ('failed', '失败'), ('expired', '已过期')], default='pending', max_length=20, verbose_name='状态'),
),
migrations.AlterField(
model_name='rechargeorder',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
),
migrations.AlterField(
model_name='rechargeorder',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='recharge_orders', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
),
]
+67 -59
View File
@@ -12,43 +12,46 @@ def normalize_resolution(value: str | None) -> str:
class CallRecord(models.Model):
class OperationType(models.TextChoices):
TITLE = "title", "Generate title"
IMAGE = "image", "Generate image"
TITLE = "title", "生成标题"
IMAGE = "image", "生成图片"
class Status(models.TextChoices):
PENDING = "pending", "Pending"
SUCCESS = "success", "Success"
FAILED = "failed", "Failed"
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)
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)
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"
@@ -73,18 +76,19 @@ class CallRecord(models.Model):
class PricingRule(models.Model):
operation_type = models.CharField(max_length=32, choices=CallRecord.OperationType.choices)
alias = models.CharField(max_length=64)
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="Leave blank to apply to every resolution for this alias.",
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)
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"
@@ -123,13 +127,13 @@ class PricingRule(models.Model):
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)
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"
@@ -162,37 +166,39 @@ class ExchangeRate(models.Model):
class RechargeOrder(models.Model):
class PayMethod(models.TextChoices):
WEIXIN = "weixin", "Weixin"
ALIPAY = "alipay", "Alipay"
WEIXIN = "weixin", "微信"
ALIPAY = "alipay", "支付宝"
class Status(models.TextChoices):
PENDING = "pending", "Pending"
PAID = "paid", "Paid"
FAILED = "failed", "Failed"
EXPIRED = "expired", "Expired"
PENDING = "pending", "待支付"
PAID = "paid", "已支付"
FAILED = "failed", "失败"
EXPIRED = "expired", "已过期"
order_no = models.CharField(max_length=64, unique=True)
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()
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)
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"
@@ -238,29 +244,31 @@ class RechargeOrder(models.Model):
class PointsLedger(models.Model):
class ChangeType(models.TextChoices):
RECHARGE = "recharge", "Recharge"
CONSUME = "consume", "Consume"
ADJUST = "adjust", "Adjust"
REFUND = "refund", "Refund"
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(null=True, blank=True)
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)
reason = models.TextField("原因", blank=True)
created_at = models.DateTimeField("创建时间", auto_now_add=True)
class Meta:
db_table = "points_ledger"
+3 -3
View File
@@ -40,7 +40,7 @@ class WalletAdjustmentForm(forms.Form):
@admin.register(User)
class UserAdmin(DjangoUserAdmin):
fieldsets = DjangoUserAdmin.fieldsets + (
("Cmhub", {"fields": ("payment_user_id", "status", "created_at")}),
("业务信息", {"fields": ("payment_user_id", "status", "created_at")}),
)
readonly_fields = ("created_at",)
list_display = (
@@ -58,7 +58,7 @@ class UserAdmin(DjangoUserAdmin):
def get_queryset(self, request):
return super().get_queryset(request).select_related("wallet")
@admin.display(description="points")
@admin.display(description="点数余额")
def wallet_points_balance(self, obj):
wallet = getattr(obj, "wallet", None)
return wallet.points_balance if wallet else 0
@@ -191,7 +191,7 @@ class ApiKeyAdmin(admin.ModelAdmin):
)
ordering = ("-created_at", "-id")
@admin.display(description="key hash")
@admin.display(description="Key 哈希")
def key_hash_masked(self, obj):
if not obj.key_hash:
return ""
@@ -0,0 +1,95 @@
# Generated by Django 5.2.15 on 2026-07-04 08:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0004_alter_apikey_options_alter_user_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='apikey',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='apikey',
name='key_hash',
field=models.CharField(editable=False, max_length=64, unique=True, verbose_name='API Key 哈希'),
),
migrations.AlterField(
model_name='apikey',
name='key_prefix',
field=models.CharField(editable=False, max_length=32, verbose_name='Key 前缀'),
),
migrations.AlterField(
model_name='apikey',
name='last_used_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='最后使用时间'),
),
migrations.AlterField(
model_name='apikey',
name='name',
field=models.CharField(blank=True, max_length=80, verbose_name='名称'),
),
migrations.AlterField(
model_name='apikey',
name='status',
field=models.CharField(choices=[('active', '有效'), ('revoked', '已吊销')], default='active', max_length=20, verbose_name='状态'),
),
migrations.AlterField(
model_name='apikey',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
),
migrations.AlterField(
model_name='apikey',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='api_keys', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
),
migrations.AlterField(
model_name='user',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(max_length=254, unique=True, verbose_name='邮箱'),
),
migrations.AlterField(
model_name='user',
name='payment_user_id',
field=models.CharField(blank=True, help_text='外部支付系统用户标识,可为空。', max_length=128, verbose_name='支付系统用户 ID'),
),
migrations.AlterField(
model_name='user',
name='status',
field=models.CharField(choices=[('active', '正常'), ('disabled', '禁用')], default='active', max_length=20, verbose_name='状态'),
),
migrations.AlterField(
model_name='userwallet',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='创建时间'),
),
migrations.AlterField(
model_name='userwallet',
name='points_balance',
field=models.BigIntegerField(default=0, verbose_name='点数余额'),
),
migrations.AlterField(
model_name='userwallet',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
),
migrations.AlterField(
model_name='userwallet',
name='user',
field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='wallet', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
),
]
+21 -16
View File
@@ -12,21 +12,23 @@ from django.db.models import Q
class User(AbstractUser):
class Status(models.TextChoices):
ACTIVE = "active", "Active"
DISABLED = "disabled", "Disabled"
ACTIVE = "active", "正常"
DISABLED = "disabled", "禁用"
email = models.EmailField("email address", unique=True)
email = models.EmailField("邮箱", unique=True)
payment_user_id = models.CharField(
"支付系统用户 ID",
max_length=128,
blank=True,
help_text="External payment system user identifier, if available.",
help_text="外部支付系统用户标识,可为空。",
)
status = models.CharField(
"状态",
max_length=20,
choices=Status.choices,
default=Status.ACTIVE,
)
created_at = models.DateTimeField(auto_now_add=True)
created_at = models.DateTimeField("创建时间", auto_now_add=True)
@property
def is_business_active(self) -> bool:
@@ -41,12 +43,13 @@ class User(AbstractUser):
class UserWallet(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
verbose_name="用户",
on_delete=models.PROTECT,
related_name="wallet",
)
points_balance = models.BigIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
points_balance = models.BigIntegerField("点数余额", default=0)
created_at = models.DateTimeField("创建时间", auto_now_add=True)
updated_at = models.DateTimeField("更新时间", auto_now=True)
class Meta:
db_table = "user_wallet"
@@ -68,25 +71,27 @@ class ApiKey(models.Model):
KEY_PREFIX_LENGTH = 16
class Status(models.TextChoices):
ACTIVE = "active", "Active"
REVOKED = "revoked", "Revoked"
ACTIVE = "active", "有效"
REVOKED = "revoked", "已吊销"
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name="用户",
on_delete=models.PROTECT,
related_name="api_keys",
)
name = models.CharField(max_length=80, blank=True)
key_hash = models.CharField(max_length=64, unique=True, editable=False)
key_prefix = models.CharField(max_length=32, editable=False)
name = models.CharField("名称", max_length=80, blank=True)
key_hash = models.CharField("API Key 哈希", max_length=64, unique=True, editable=False)
key_prefix = models.CharField("Key 前缀", max_length=32, editable=False)
status = models.CharField(
"状态",
max_length=20,
choices=Status.choices,
default=Status.ACTIVE,
)
last_used_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
last_used_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 = "api_key"