feat: 跨俱乐部资产迁移(计划/令牌/流水 + 用户与后台 API)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-29 00:15:27 +08:00
parent 568dcb720e
commit 0ebe5b4872
9 changed files with 1658 additions and 1 deletions

View File

@@ -740,3 +740,156 @@ class DealStatEvent(QModel):
class Meta:
db_table = 'deal_stat_event'
verbose_name = '成交指标事件'
# ==================== 跨俱乐部资产迁移(独立模块,默认关闭不影响现网)====================
class ClubMigratePlan(QModel):
"""迁移计划from_club → to_club未启用时小程序完全无感。"""
plan_id = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='计划ID')
name = models.CharField(max_length=64, blank=True, default='', verbose_name='计划名称')
from_club_id = models.CharField(max_length=16, db_index=True, verbose_name='源俱乐部')
to_club_id = models.CharField(max_length=16, db_index=True, verbose_name='目标俱乐部')
enabled = models.BooleanField(default=False, db_index=True, verbose_name='是否启用')
# null=永久;有值则必须在区间内
start_at = models.DateTimeField(null=True, blank=True, verbose_name='开始时间')
end_at = models.DateTimeField(null=True, blank=True, verbose_name='结束时间')
# ["dashou.yue","dashou.yajin","dashou.jifen","dashou.dongjie_yue","dashou.huiyuan", ...]
asset_whitelist = models.JSONField(default=list, blank=True, verbose_name='资产白名单')
# {"require_no_unpaid_penalty":true,"require_orders_closed":false,"require_not_banned":true}
condition_json = models.JSONField(default=dict, blank=True, verbose_name='门槛条件')
force_intercept = models.BooleanField(default=True, verbose_name='源店强制迁移页')
# ["dashou"] 首期默认打手
force_roles = models.JSONField(default=list, blank=True, verbose_name='强制角色列表')
default_once_per_user = models.BooleanField(default=True, verbose_name='默认每人成功一次')
remark = models.CharField(max_length=255, blank=True, default='')
updated_by = models.CharField(max_length=32, blank=True, default='')
CreateTime = models.DateTimeField(auto_now_add=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'club_migrate_plan'
verbose_name = '跨店资产迁移计划'
indexes = [
models.Index(fields=['from_club_id', 'enabled'], name='idx_cmp_from_en'),
models.Index(fields=['to_club_id', 'enabled'], name='idx_cmp_to_en'),
]
class ClubMigrateToken(QModel):
"""一次性迁移令牌(设密后签发,确认后作废)。"""
STATUS_PENDING_PASSWORD = 0
STATUS_ISSUED = 1
STATUS_CONSUMED = 2
STATUS_EXPIRED = 3
STATUS_CANCELLED = 4
token = models.CharField(max_length=64, unique=True, db_index=True, verbose_name='令牌')
plan_id = models.CharField(max_length=32, db_index=True)
from_club_id = models.CharField(max_length=16, db_index=True)
to_club_id = models.CharField(max_length=16, db_index=True)
from_uid = models.CharField(max_length=16, db_index=True, verbose_name='源UserUID')
password_hash = models.CharField(max_length=128, blank=True, default='', verbose_name='转移密码哈希')
password_salt = models.CharField(max_length=32, blank=True, default='')
status = models.SmallIntegerField(default=0, db_index=True)
fail_count = models.PositiveIntegerField(default=0, verbose_name='验密失败次数')
asset_snapshot = models.JSONField(default=dict, blank=True, verbose_name='发码时资产快照')
qrcode_url = models.CharField(max_length=512, blank=True, default='', verbose_name='目标店小程序码URL')
expire_at = models.DateTimeField(null=True, blank=True, db_index=True)
consumed_at = models.DateTimeField(null=True, blank=True)
CreateTime = models.DateTimeField(auto_now_add=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'club_migrate_token'
verbose_name = '跨店迁移令牌'
indexes = [
models.Index(fields=['plan_id', 'from_uid', 'status'], name='idx_cmt_plan_uid_st'),
]
class ClubMigrateConsumption(QModel):
"""计划×源用户成功占用(防并发双迁;默认一次)。"""
plan_id = models.CharField(max_length=32, db_index=True)
from_uid = models.CharField(max_length=16, db_index=True)
success_count = models.PositiveIntegerField(default=0)
last_ledger_id = models.BigIntegerField(null=True, blank=True)
CreateTime = models.DateTimeField(auto_now_add=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'club_migrate_consumption'
verbose_name = '跨店迁移占用'
unique_together = [['plan_id', 'from_uid']]
class ClubMigrateUserQuota(QModel):
"""例外:指定源用户在某计划下可额外成功次数。"""
plan_id = models.CharField(max_length=32, db_index=True)
from_uid = models.CharField(max_length=16, db_index=True)
max_extra_times = models.PositiveIntegerField(default=1, verbose_name='额外成功次数')
reason = models.CharField(max_length=255, blank=True, default='')
updated_by = models.CharField(max_length=32, blank=True, default='')
CreateTime = models.DateTimeField(auto_now_add=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'club_migrate_user_quota'
verbose_name = '跨店迁移二次白名单'
unique_together = [['plan_id', 'from_uid']]
class ClubMigrateLedger(QModel):
"""一次成功过户主单P0 溯源)。"""
STATUS_SUCCESS = 1
STATUS_REVERSED = 2
plan_id = models.CharField(max_length=32, db_index=True)
token = models.CharField(max_length=64, blank=True, default='', db_index=True)
from_club_id = models.CharField(max_length=16, db_index=True)
to_club_id = models.CharField(max_length=16, db_index=True)
from_uid = models.CharField(max_length=16, db_index=True)
to_uid = models.CharField(max_length=16, db_index=True)
migrate_seq = models.PositiveIntegerField(default=1, verbose_name='本计划第几次')
status = models.SmallIntegerField(default=1, db_index=True)
summary_json = models.JSONField(default=dict, blank=True, verbose_name='摘要')
client_meta = models.JSONField(default=dict, blank=True)
CreateTime = models.DateTimeField(auto_now_add=True, db_index=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'club_migrate_ledger'
verbose_name = '跨店迁移流水主单'
indexes = [
models.Index(fields=['from_uid', 'plan_id'], name='idx_cml_from_plan'),
models.Index(fields=['to_uid', 'plan_id'], name='idx_cml_to_plan'),
models.Index(fields=['from_club_id', 'to_club_id', 'CreateTime'], name='idx_cml_clubs_t'),
]
class ClubMigrateLedgerItem(QModel):
"""流水明细:每个角色字段一行(金额前后值必填,便于对账纠错)。"""
ITEM_BALANCE = 'balance'
ITEM_HUIYUAN = 'huiyuan'
ledger_id = models.BigIntegerField(db_index=True)
plan_id = models.CharField(max_length=32, db_index=True)
item_type = models.CharField(max_length=16, default='balance', db_index=True)
role = models.CharField(max_length=16, db_index=True)
field = models.CharField(max_length=32, blank=True, default='', verbose_name='字段名')
amount = models.DecimalField(max_digits=14, decimal_places=2, default=Decimal('0'))
before_src = models.CharField(max_length=64, blank=True, default='')
after_src = models.CharField(max_length=64, blank=True, default='')
before_dst = models.CharField(max_length=64, blank=True, default='')
after_dst = models.CharField(max_length=64, blank=True, default='')
# 会员专用
huiyuan_id = models.CharField(max_length=16, blank=True, default='', db_index=True)
meta_json = models.JSONField(default=dict, blank=True)
CreateTime = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'club_migrate_ledger_item'
verbose_name = '跨店迁移流水明细'
indexes = [
models.Index(fields=['ledger_id', 'role'], name='idx_cmli_led_role'),
]