200 Commits

Author SHA1 Message Date
XingQue
5be696db9d fix: repair jituan/urls.py IndentationError on identity_tag import 2026-06-28 00:35:50 +08:00
XingQue
3a1aaa9b61 feat(jituan): 后台按俱乐部添加考核官 khggl-add 2026-06-28 00:32:52 +08:00
XingQue
54f8e23488 fix: separate penalty czjilu from merchant recharge and correct finance stats 2026-06-27 18:50:32 +08:00
XingQue
65477592af fix: 罚单/积分处罚仅归属申请人俱乐部,查询不再按被罚人/订单混入 2026-06-27 18:26:22 +08:00
XingQue
a6d32f848e fix: filter_penalty_qs 合并 Q 条件切片错误 2026-06-27 18:22:07 +08:00
XingQue
ee7867b36f fix: 罚单/积分处罚归属申请人俱乐部;申请锁定分红缴纳按锁定值入账 2026-06-27 18:15:42 +08:00
XingQue
374dd26855 fix: 罚款微信支付使用俱乐部openid并安全解析微信响应 2026-06-27 17:59:53 +08:00
XingQue
d19b6250ae fix: 罚单申请时锁定分红利率与应得金额,缴纳按锁定值入账 2026-06-27 17:46:39 +08:00
XingQue
e2e568bcda fix: 子客服派单未设置佣金时 final_yongjin 未初始化 2026-06-27 11:19:37 +08:00
XingQue
318e3e3216 fix: 商家撤销罚单处理人身份为商家;处罚待处理含申诉中 2026-06-27 08:04:35 +08:00
XingQue
93bf2193d2 fix: 商家分红利率预览回退全局配置;撤销罚单记录处理人;防重复分红 2026-06-27 07:55:06 +08:00
XingQue
fdecf2fe3c feat: 商家订单准确统计、客服备注与按客服筛选 2026-06-27 05:17:36 +08:00
XingQue
2445b39f3b feat: 商家罚单列表统计与证据图管理接口 2026-06-27 04:51:24 +08:00
XingQue
8f21491c72 修复商家订单时间段筛选在USE_TZ=False下MySQL 500 2026-06-26 20:38:04 +08:00
XingQue
c8b67a2fc8 商家订单列表支持时间段筛选并修正分页has_more 2026-06-26 20:28:34 +08:00
XingQue
3083096cd7 修复商家改昵称接口 UpdateTime 字段不存在 2026-06-26 17:38:37 +08:00
XingQue
cf0882575e 商家首页图标key分离、商家改昵称接口、抢单列表sj_avatar 2026-06-26 17:08:50 +08:00
XingQue
fc63836cc7 fix: 抢单池返回商家头像 sj_avatar 2026-06-26 16:03:49 +08:00
XingQue
992f4f4f18 feat: 分俱乐部推广海报背景与二维码生成 2026-06-26 15:52:17 +08:00
XingQue
aaf14264d8 fix: 考试会员判断与打手会员信息同步 2026-06-26 15:16:43 +08:00
XingQue
0b202722e6 fix: 考试bundle错误提示区分未上架 2026-06-26 14:41:03 +08:00
XingQue
1aa8c2dc87 fix: bundle 未开启考试时拒绝拉题 2026-06-26 06:16:37 +08:00
XingQue
85531da9eb feat: 考试权限dsks666、页面内切俱乐部、商品板块仅集团可改 2026-06-26 06:07:52 +08:00
XingQue
34e79aa753 feat: 考试集团一览与商品集团权限边界 2026-06-26 05:57:39 +08:00
XingQue
65c5e6572e feat: 打手接单考试(按俱乐部题库、配置与通过状态) 2026-06-26 05:48:53 +08:00
XingQue
ff5b13e717 fix(jituan): 俱乐部商品类型/考核标签按视图隔离 2026-06-26 05:38:32 +08:00
XingQue
2b9ea6ccec feat(jituan): 俱乐部商品类型/考核标签上架配置与抢单池过滤 2026-06-26 05:15:50 +08:00
XingQue
41fa98e8b7 移除未使用的商家充值图标配置项,优化打手横幅图标说明 2026-06-26 04:37:50 +08:00
XingQue
8884159edc fix: 菜单访问时自动补全 miniapp.assets 种子 2026-06-26 04:20:07 +08:00
XingQue
1e5de2061a fix: 图标仅在上传后下发;注册图标与频道菜单 2026-06-26 04:15:46 +08:00
XingQue
095a8e89e4 fix: 注册 miniapp.assets 菜单权限,修复图标页被重定向到板块配置 2026-06-26 04:07:38 +08:00
XingQue
24949b1cb2 feat: 小程序图标与频道配置 API(按俱乐部) 2026-06-26 04:03:52 +08:00
XingQue
141f17859a fix: 身份标签 color 合并进 texiao_json 2026-06-26 03:26:34 +08:00
XingQue
01d73c5fae fix: 抢单列表身份标签 club 回退与 dashou 称号 2026-06-26 03:17:19 +08:00
XingQue
78c5442381 fix: 抢单列表 dashou_biaoqian 按 dashou 称号类型过滤 2026-06-26 03:09:58 +08:00
XingQue
550061435e fix: xzjphbhqsj 与 phbhqsj 同用集团整体数据
去掉 club_id 过滤,仅保留邀请人数/派单流水等排序差异
2026-06-26 03:04:45 +08:00
XingQue
5ebcb56b65 feat: 星之界排行榜新接口 xzjphbhqsj
管事/组长按邀请人数排序;商家按派单流水;旧 phbhqsj 不变
2026-06-26 03:00:09 +08:00
XingQue
af2578573c 体验会员拦截审核提现与收款;修复分红0元仍走永久分红 2026-06-26 02:36:24 +08:00
XingQue
f6b99f96a4 fix: 会员永久分红生效,管事/组长分红配置按俱乐部读取 2026-06-26 02:22:44 +08:00
XingQue
d25c2dd862 fix: 会员余额购按俱乐部分天数履约,后台展示俱乐部销量 2026-06-26 02:12:49 +08:00
XingQue
a7e51aac3f fix: lbhqltqx 使用 BossID/Order__Status 修复 FieldError 2026-06-26 01:55:26 +08:00
XingQue
f38f1216ea fix: 微信支付元转分改用 Decimal,修复 2.01 元被收 2 元 2026-06-26 01:53:28 +08:00
XingQue
eb1fc6d2ae fix: 仅体验会员禁止余额抵扣,正式会员恢复抵扣 2026-06-26 01:47:15 +08:00
XingQue
2f0838e552 feat: 会员体验版、俱乐部分离上架与分红计数加固
扩展 club_huiyuan_price/huiyuangoumai/czjilu/gsfenhong 字段;微信购会员强制写 czjilu+收支入账;formal_cishu 仅统计正式已支付单;关闭余额购会员;俱乐部上架/下架 API。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 01:32:33 +08:00
XingQue
deb1688d0c fix: 换打手继承拼多多订单号;优化后台添加会员提示 2026-06-25 22:47:19 +08:00
XingQue
93e68fe20f feat: 链接派单H5提交必填拼多多订单号并写入订单
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 17:29:23 +08:00
XingQue
815f344d19 feat: 商家订单列表返回标签/头像/优质商家,与抢单池字段对齐
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 17:16:05 +08:00
XingQue
a69c008f96 fix: FluentQuery.filter 污染 base_qs 导致积分处罚列表为空 2026-06-25 16:28:45 +08:00
XingQue
8ee7814a45 fix: 处罚/罚款列表俱乐部过滤与全部Tab;回填fadan club_id 2026-06-25 16:21:05 +08:00
XingQue
2bd45f8654 fix: 积分处罚俱乐部过滤与 create_time;菜单增加提现/处罚详情 2026-06-25 16:11:58 +08:00
XingQue
59f5a695d8 fix: 公告按页、身份标签权限与菜单种子 2026-06-25 15:50:54 +08:00
XingQue
9575edcfc4 feat: 公告按页、身份标签、拼多多订单号、派单与抢单改造 2026-06-25 15:35:51 +08:00
XingQue
d1dc65e13b feat: 客服后台菜单权限与 menu-access 接口 2026-06-25 04:10:17 +08:00
XingQue
ff1e1a73dd feat: 客服后台菜单按权限展示侧栏 2026-06-25 04:05:15 +08:00
XingQue
7a2723fef7 feat: 俱乐部杂项图片/关注快手;角色客服按club隔离 2026-06-25 02:43:40 +08:00
XingQue
0c4b28ea45 fix: 俱乐部ID统一解析;会员支付/分红/展示按club隔离 2026-06-25 01:37:58 +08:00
XingQue
cca53bbc96 fix: 手机号认证按俱乐部 wx_appid 换 token,修复星之界 40013 2026-06-25 01:11:32 +08:00
XingQue
4714e2259b 俱乐部隔离:展示配置/会员可售/手机认证阈值;修复小程序轮播 2026-06-24 23:49:25 +08:00
XingQue
d56c85bca9 修复任职停用;星之界种子邀请链 seed_xzj_invite_data 2026-06-24 23:33:25 +08:00
XingQue
49482830f9 seed_admin_assignments 禁止默认批量;任职角色仅作标签说明 2026-06-24 23:13:03 +08:00
XingQue
77e1d3f293 修复 seed_admin_assignments 用 KefuProfile 筛选客服账号 2026-06-24 23:05:06 +08:00
XingQue
65613af195 完成后台多俱乐部数据隔离:订单/会员/财务/板块,并添加星之界初始化命令 2026-06-24 22:59:08 +08:00
XingQue
8154b2639d 今日收入笔数只读daily_income_stat.total_count 2026-06-24 22:27:31 +08:00
XingQue
7f2f0caaa1 财务接口按俱乐部过滤,日收支只读daily_income_stat与daily_payout_stat 2026-06-24 22:22:12 +08:00
XingQue
8e5db24bad 修复财务接口:统一date字段查询,板子全平台合计 2026-06-24 22:08:40 +08:00
XingQue
79f2d4f020 财务恢复:只读日统计表,废弃szjilu,新增restore_finance_stats 2026-06-24 21:50:40 +08:00
XingQue
8c271f639a 财务每日收入改从真实支付实时汇总,与统计表解耦 2026-06-24 21:45:08 +08:00
XingQue
b1fbc3cd8f fix: 财务接口+恢复今日收入 2026-06-24 21:35:55 +08:00
XingQue
84665e0c5d fix: undo_repair_wechat_income 反向撤销重复记账 2026-06-24 21:29:19 +08:00
XingQue
3b7c539a56 fix: 收入恢复命令修正 2026-06-24 21:21:54 +08:00
XingQue
600188a99a fix: 恢复 repair 重复记账 + 禁用错误补账命令 2026-06-24 21:18:57 +08:00
XingQue
a6068f7e2b fix: 微信支付入账补记 + 财务按俱乐部分组 2026-06-24 21:10:55 +08:00
XingQue
0827da594c fix: 支付入账降级 + 财务全平台汇总 2026-06-24 20:58:28 +08:00
XingQue
a693e35177 fix: 多俱乐部下单 openid;集团超管可切俱乐部 2026-06-24 20:45:40 +08:00
XingQue
08fc8f50f3 fix: 多俱乐部下单按 user_wx_openid 取 openid;集团超管始终可切俱乐部 2026-06-24 20:40:30 +08:00
XingQue
8e5662c56f fix: admin_audit_log 操作人ID支持手机号解析 2026-06-24 18:25:21 +08:00
XingQue
45123d5b6f fix: 充值履约幂等加固(会员/押金/积分),后台加会员club_id 2026-06-24 18:16:11 +08:00
XingQue
9cd7b6920a fix: 会员充值先开通再分红防回滚,补开通与注册时间字段 2026-06-24 18:08:11 +08:00
XingQue
83519ac67b fix: 修复微信入账重复统计,入账幂等防双记 2026-06-24 17:46:37 +08:00
XingQue
04c54d0733 feat: 超管任职分配、俱乐部创建与bootstrap、角色update_role 2026-06-24 16:56:10 +08:00
XingQue
0ac56c5b92 fix: 会员分红按购买次序与duoci_fenhong计,严格俱乐部配置 2026-06-24 16:47:28 +08:00
XingQue
48bf50c340 fix: 订单列表 FluentQuery 状态统计污染导致列表为空 2026-06-24 06:50:29 +08:00
XingQue
467bb71736 fix: 修复 SzxxView 重复 values 导致 500,财务累计统计去重 2026-06-24 06:44:44 +08:00
XingQue
bfc38c2d43 fix: 支付 request.user、jituan import、财务日统计汇总 2026-06-24 06:34:35 +08:00
XingQue
5ae01a9ff2 fix: 订单列表、超管1383714110、财务szjilu累计、俱乐部配置API 2026-06-24 06:27:01 +08:00
XingQue
b33bd3eb18 fix: 订单分页缓存、统计与列表一致、admin-assignments 解析 2026-06-24 06:18:47 +08:00
XingQue
c42f979082 feat(jituan): 订单列表修复、财务 club 过滤、数据范围 API 2026-06-24 06:07:41 +08:00
XingQue
32486a6e77 fix: backfill_penalty_club_id 改用 FluentQuery.to_list() 2026-06-24 05:31:57 +08:00
XingQue
5c4663c8ec fix: config.0006 半路失败恢复命令 recover_config_0006 2026-06-24 05:27:45 +08:00
XingQue
a8b6e46f61 fix: migration 加唯一约束前自动去重,补充部署清理脚本 2026-06-24 05:20:44 +08:00
XingQue
574fe5e860 fix: Bankuai 从 rank.models 导入,修复 migrate 启动失败 2026-06-24 05:15:30 +08:00
XingQue
dcc4936428 feat(jituan): 集团多俱乐部改造 — club 隔离、财务/提现/分红/展示配置 2026-06-24 05:02:18 +08:00
XingQue
ce9b09f096 fix: 考核记录拒绝次数统计兼容 gvsdsdk ValuesResult 2026-06-24 00:09:56 +08:00
XingQue
98daa93abd fix: 考核记录列表统计不污染查询条件 2026-06-24 00:07:19 +08:00
XingQue
8dae04c32c feat: 后台考核记录列表统计与退回待审核操作 2026-06-23 22:34:18 +08:00
XingQue
3f4712bb44 revert: 链接页聊天恢复纯文本,移除发送者追踪 2026-06-23 21:23:23 +08:00
XingQue
6e37477e4f feat: 链接页发消息记录发送者IP与设备指纹(不读客服JWT) 2026-06-23 21:14:10 +08:00
XingQue
bdebbb9348 fix: 手机号认证规则补子客服与打手历史订单
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 16:50:08 +08:00
XingQue
45af7e1649 fix: 订单时间字段别名与子客服鉴权/读权限统一
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 16:22:47 +08:00
XingQue
a07bd3d388 fix: 调整手机号强制认证判定规则(身份触发) 2026-06-22 15:26:05 +08:00
XingQue
1ef7c62ab4 feat: 更换邀请人、操作日志、组长海报token修复
复用 xiugaijilu 补全后台操作日志;新增更换打手/管事邀请人;修复组长海报 access_token 失效;修复组长列表 BossProfile 搜索 500。
2026-06-21 12:30:23 +08:00
XingQue
a1d5a2c3a3 feat: 更换邀请人、操作日志与组长列表搜索修复
复用 xiugaijilu 为后台关键改动补全详细日志,新增更换打手/管事邀请人接口与操作日志页面,并修复组长列表 BossProfile 关联名导致的搜索 500。
2026-06-21 12:12:08 +08:00
XingQue
24cd2ccea3 feat: 后台管事升级组长接口 htgssjzz,详情返回 is_zuzhang 2026-06-21 11:28:23 +08:00
XingQue
d19bcb844a fix: 商家订单类型接口增加商家/客服身份校验 2026-06-21 05:26:17 +08:00
XingQue
86b4641558 fix: 罚款处理接口兼容前端 fadan_id 参数 2026-06-21 03:10:05 +08:00
XingQue
da4bb47ee5 fix: 下单微信支付参数名 Amount/Remark 修正为 jine/beizhu 2026-06-21 00:30:39 +08:00
XingQue
4dd3c1bbf0 fix: 下单收益计算函数参数名修正 2026-06-21 00:27:47 +08:00
XingQue
2ce2aad956 fix: 后台添加角色按客服账号定位;下单OpenID字段名修正 2026-06-21 00:23:19 +08:00
XingQue
4f6c06a488 fix: 修复财务接口字段名及后台角色权限/用户列表返回格式 2026-06-20 18:28:29 +08:00
XingQue
7890d0992a 后端改了一处(views_manage.py) 2026-06-20 14:00:01 +08:00
XingQue
a30bd69bf2 子客服管理接口禁止对自己划收额度、换角色、封禁;成员列表返回 isSelf;具备 staff_manage 的总管可读角色列表用于给他人换角色。 2026-06-20 13:39:13 +08:00
XingQue
42af45f92c 在 merchant_ops/services/wallet.py 的 import 里加上了 WALLET_REVOKE。 2026-06-20 05:07:26 +08:00
XingQue
d68a1da2c2 第二轮:客服额度资金闭环与收回额度
- 划额度扣商家余额,客服派单/链接只扣个人额度
- 新增 wallet/revoke 收回未使用额度,权限 wallet_revoke
- 客服额度单撤销/退款只释放额度,不再误退商家余额
- 链接待填单(13)可撤销;总管/财务可划收额度
2026-06-20 04:59:29 +08:00
XingQue
b2520726dc 派单统计时机:生成链接不计派单;客户填单成功才计;退款/结单在真正完成时统计。
客服日统计联动:update_shangjia_daily(..., order_id=...) 会同步更新对应派单客服的日统计。
客服 /me 接口:返回今日/累计派单、结单、退款等 stats。
角色权限修复:预置角色(总管/财务/派单员)每次访问时强制同步权限,换角色后权限才生效。
展示增强:订单详情带派单员头像;客服列表带金额字段;管理员同意退款补商家+客服统计。
2026-06-20 04:34:41 +08:00
XingQue
c70b2ee7be 修复了客服日统计时机,展示更加全面的数据 2026-06-20 04:23:31 +08:00
XingQue
993c0c54b0 修复了客服数据统计,强化了商家数据获取接口获取更多数据 2026-06-20 04:12:57 +08:00
XingQue
4b704d48fa 修复了商家客服接口添加客服操作专属接口,其次商家看日志接口,修复导入 2026-06-20 03:56:18 +08:00
XingQue
470546ae82 修复了商家客服接口添加客服操作专属接口,其次商家看日志接口 2026-06-20 03:54:14 +08:00
XingQue
1d4d054c75 修复了商家客服登录身份标识问题 2026-06-20 03:36:58 +08:00
XingQue
92704a86ca 修复了商家客服模块模型导入 2026-06-20 03:27:42 +08:00
XingQue
1ac05494d6 修复了商家客服接口 2026-06-20 03:25:24 +08:00
XingQue
6fa0e1638d fix: 模板创建变量名; 客服模板/角色/权限 API 2026-06-20 03:17:01 +08:00
XingQue
89632228cc 新表迁移备选方案 2026-06-20 02:34:52 +08:00
XingQue
7dc3064a88 fix: MerchantOrderDispatch 从 merchant_ops.models 导入 2026-06-20 02:22:38 +08:00
XingQue
0cf5341d73 feat: 商家子客服 merchant_ops 模块与专用 API 2026-06-20 02:19:39 +08:00
XingQue
e233ee4bea fix: 罚款/考核支付回调 Czjilu 字段名兼容 2026-06-20 02:00:36 +08:00
XingQue
3fecb9390f fix: 订单列表兼容 leixing_id 与 ProductTypeID 商品类型筛选 2026-06-20 01:17:59 +08:00
XingQue
4ded4ea50a fix: 管理员换打手跳过客服统计字段 2026-06-20 00:44:34 +08:00
XingQue
157ac05632 1. 更换打手 /houtai/zxkfghds
问题:H5 传 dingdan_id,后端只读 OrderID → 400「参数不完整」
修复:同时接受 OrderID / dingdan_id;成功响应增加 new_dingdan_id(与 new_OrderID 一致)
2. 生成罚单 /houtai/htfksc、/houtai/kffkdssq
问题:前端传 beichufa_id、chufaliyou、fakuanjine、yingxiang_qiangdan,后端只认 PenalizedUserID、Reason、FineAmount 等
修复:在 backend/utils.py 增加 pick_penalty_create_params(),两套字段名都支持
3. 后台订单相关接口(orders/views.py)
剩余 10 处 只读 OrderID 的接口(换打手、退款、结单、罚单等)已统一兼容 dingdan_id
2026-06-19 23:23:02 +08:00
XingQue
1ce023552c 已改 3 处(django/orders/views.py),均兼容 OrderID / dingdan_id:
接口	用途
/dingdan/dsscpz
打手获取 COS 上传凭证
/dingdan/dsscpz(商家侧同类)
商家处罚图片凭证
/dingdan/dstijiao
打手提交交付(避免凭证通过后提交再 400)
2026-06-19 23:02:34 +08:00
XingQue
e656760441 已修复
已在 django/orders/views.py 里,把商家订单详情页相关接口都改成同时兼容 OrderID 和 dingdan_id(与 sjddxq 一致):

接口	用途
/dingdan/sjtkshenqing
申请退款(你这次报错的)
/dingdan/sjchexiao
撤销订单
/dingdan/sjjiesuan
结算
/dingdan/sjfksq
罚款申请
/dingdan/zxsjghds
更换打手
2026-06-19 22:44:36 +08:00
XingQue
f851dbdefd 累计改动汇总(相对远程,全在 orders/views.py)
处罚扣积分:dashou.jifen(不再误用 DeductedPoints)
商家/打手详情:兼容 dingdan_id 与 OrderID
抢单等:Dingdan.objects → Order.query
2026-06-19 22:36:24 +08:00
XingQue
a055131ec7 实际改了什么(只在 orders/views.py)
商家处罚扣积分:dashou.DeductedPoints → dashou.jifen(模型字段本来就是 jifen)
商家订单详情 /dingdan/sjddxq:同时接受 OrderID 和 dingdan_id
打手订单详情 /dingdan/dsddxq:同上
属于星阙业务接口的字段兼容,不是 SDK 改造。
2026-06-19 22:30:12 +08:00
f890369684 修正了字段名不同的问题(补充) 2026-06-19 22:17:36 +08:00
a7f43a4e74 修正字段名不同的问题 2026-06-19 22:15:46 +08:00
a06202d297 修正了前后端名称不一致问题 2026-06-19 21:38:11 +08:00
0b3beca147 修复了字段迁移问题 2026-06-19 21:27:38 +08:00
f212e7203e 修复了已知的字段问题 2026-06-19 21:25:21 +08:00
212840d4ab 修正了字段名错误 2026-06-19 21:03:45 +08:00
52bd2dfd85 修正了字段问题 2026-06-19 20:54:07 +08:00
c1118fc335 增加了缺失的分页字段 2026-06-19 20:44:41 +08:00
54ee08b3c5 开启生产白名单 2026-06-19 20:14:41 +08:00
ef922b2b90 修复了键 ORM 查询 2026-06-19 19:52:15 +08:00
479fd8bbd8 修复了已知问题 2026-06-19 19:49:58 +08:00
7f5e76551c 补充修复 2026-06-19 19:43:52 +08:00
33eda43664 键名修改 2026-06-19 19:24:32 +08:00
e64697b64a 彻底解决了 create_time 的问题,将所有字段都改为了 CreateTime 2026-06-19 19:19:22 +08:00
90e0bde7f1 修正 2026-06-19 19:09:15 +08:00
1a8348cf4b 修复了单个 create_time 问题,但很可能仍然存在问题 2026-06-19 19:05:44 +08:00
ee42d80d93 修正了 SDK,修复了 aggregate 不能传入 args 的问题 2026-06-19 18:57:50 +08:00
fcd2a5e8a9 修正 2026-06-19 17:50:18 +08:00
8aa36e3764 修正过早的键更新 2026-06-19 17:33:31 +08:00
0ff82d1bac 修复了一些问题 2026-06-19 16:41:01 +08:00
15381c3051 修正了 错误字段定义 2026-06-19 15:23:10 +08:00
e717fc1763 修改白名单 2026-06-19 15:14:00 +08:00
df927e141d 修复了属性取值问题 2026-06-19 15:13:00 +08:00
a699037826 修复了部分问题 2026-06-19 14:26:55 +08:00
c3819ccd65 修复了一些字段名错误的问题 2026-06-19 13:54:35 +08:00
0cd20850e0 修复了 管理员 无 客服扩展表 视图问题 2026-06-19 13:42:01 +08:00
4963b06e21 修正管理员权限判断 2026-06-19 13:08:12 +08:00
ce0fb79b39 添加权限调试日志 2026-06-19 12:46:16 +08:00
46e5a5eeee 修正 2026-06-19 12:30:12 +08:00
67872f823c 修正了 超级管理员 的权限 2026-06-19 12:27:31 +08:00
c9bc811bc1 修正了小写手机键导致的问题 2026-06-19 12:11:00 +08:00
7df8a1a1d2 试点修复了 config 模型的拼音字段 2026-06-19 11:41:18 +08:00
6f6af7553a 维护 2026-06-19 02:05:40 +08:00
854767a2c2 自动赋予 000001 所有权限 2026-06-19 02:01:41 +08:00
d71a81f279 修复了管理员无法进后台的问题 2026-06-19 01:58:34 +08:00
5b9436b994 修复了 User 表请求的问题 2026-06-19 00:08:08 +08:00
d3903c913b 修改了鉴权模型,使用 GVSDSDK 来修正 2026-06-18 23:51:33 +08:00
38fcfd730c 开发者电脑 IP 切换,增加白名单 2026-06-18 23:11:09 +08:00
e672ac8d3f 修复了堆栈不可见错误,以及 ORM 错误查询 2026-06-18 22:54:07 +08:00
b7f987f2c8 增加了 IP 黑白名单功能(中间件) 2026-06-18 22:25:21 +08:00
f169b6569a 修正 2026-06-18 22:17:46 +08:00
d93d3b3c6d 修复了一些问题 2026-06-18 20:59:46 +08:00
05498904cf 修复 JWT 验证 401 问题 2026-06-18 20:29:37 +08:00
5c8b82df15 修复了token认证错误 2026-06-18 19:17:26 +08:00
d769dec2ec 修复了 UserPassword 不存在的问题,修复了明文密码的问题 2026-06-18 19:02:31 +08:00
5ac9c785f9 修复了残留的旧字段名 2026-06-18 18:47:08 +08:00
bb73a492cc 修复了存在的一些问题,比如 orders 的名称改变导致的字段不存在问题 2026-06-18 18:45:20 +08:00
5ebded322f 修复了 User Password 验权 2026-06-18 18:33:40 +08:00
c093fa46c1 补充修复 2026-06-18 18:22:08 +08:00
c368b6cb73 补充修改 2026-06-18 18:18:00 +08:00
53049d1588 修复了一些 2026-06-18 18:02:45 +08:00
XingQue
e67bc17fa2 fix: KefuLoginView 客服登录改用 Phone 字段查询
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 11:22:31 +08:00
XingQue
16845d0ce2 只改了 django/config/views.py 里的 PopupConfigView:
authentication_classes = []
去掉 user.id 登录检查
2026-06-18 10:57:11 +08:00
XingQue
177d082d80 客服登录接口 KefuLoginView(/yonghu/kefujinru)已修复。 2026-06-18 09:47:23 +08:00
XingQue
d96381fd4e GVSDSDK 的 model_base.py 在 Python 3.10 下不能从 typing 导入 Self,加了兼容:3.11 用标准库,3.10 用 typing_extensions.Self。只动这一处,业务逻辑不变。 2026-06-18 09:37:55 +08:00
854c832c07 进行了 backend 的重构,实验中 2026-06-18 04:15:23 +08:00
54cc658f0b 修复了主键 \x00 的问题 2026-06-18 03:01:24 +08:00
f80ce51859 修正了 property 直接取值的问题 2026-06-18 02:49:04 +08:00
7ce545fedb 补充修改 2026-06-18 02:30:42 +08:00
f9da920e6b 补充 2026-06-17 23:02:35 +08:00
ca3a109c41 显示堆栈 2026-06-17 22:13:08 +08:00
4aca437d2b 修复了已知的生产环境问题 2026-06-17 21:59:37 +08:00
d00f0d08e5 进行了 GVSDSDK 迁移,可能存在诸多问题 2026-06-17 20:37:47 +08:00
72770ad73b 回滚到了最近一个可行的生产版本(分支 2) 2026-06-17 13:51:51 +08:00
61ddf81fb0 回退了更多阶段的修改 2026-06-17 04:13:14 +08:00
004012b59f 本地回退了操作 2026-06-17 04:10:52 +08:00
461643079b 做了疯狂的架构迁移,正在进行测试,不稳定 2026-06-17 02:02:41 +08:00
317 changed files with 213931 additions and 36661 deletions

View File

@@ -14,6 +14,8 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = app_secrets.SECRET_KEY
DEBUG = False
# 详细错误日志开关True=在 except 块中输出完整堆栈到日志False=仅记录错误摘要
DEBUG_DETAIL_LOG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'www.abas.asia']
# ==================== 应用配置 ====================
@@ -38,10 +40,15 @@ INSTALLED_APPS = [
'utils',
'shop',
'rank',
'merchant_ops',
'jituan',
]
MIDDLEWARE = [
'utils.ip_filter_middleware.IPFilterMiddleware',
'utils.debug_middleware.DebugExceptionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'jituan.middleware.ClubContextMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@@ -51,6 +58,45 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# 日志配置 — 500 错误堆栈输出到 console受 DEBUG_DETAIL_LOG 开关控制
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '[%(asctime)s] %(levelname)s %(name)s: %(message)s'
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'INFO',
},
# 业务模块日志 — DEBUG_DETAIL_LOG=True 时输出 DEBUG 级别(含完整堆栈)
'users': {
'handlers': ['console'],
'level': 'DEBUG' if DEBUG_DETAIL_LOG else 'INFO',
'propagate': False,
},
'orders': {
'handlers': ['console'],
'level': 'DEBUG' if DEBUG_DETAIL_LOG else 'INFO',
'propagate': False,
},
'utils.debug_middleware': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': False,
},
},
}
ROOT_URLCONF = 'a_long_dianjing.urls'
TEMPLATES = [
@@ -119,8 +165,8 @@ SIMPLE_JWT = {
'LEEWAY': 0,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'yonghuid',
'USER_ID_CLAIM': 'yonghuid',
'USER_ID_FIELD': 'UserUID',
'USER_ID_CLAIM': 'UserUID',
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
@@ -132,12 +178,13 @@ AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
]
AUTH_USER_MODEL = 'users.UserMain'
AUTH_USER_MODEL = 'users.User'
# ==================== DRF 配置 ====================
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
'gvsdsdk.auth.jwt_auth.CompatibleJWTAuthentication',
'gvsdsdk.auth.elt_auth.ELTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
@@ -177,8 +224,28 @@ CORS_ALLOW_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
CORS_ALLOW_HEADERS = [
"accept", "accept-encoding", "authorization", "content-type",
"dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with",
"x-club-id", "x-club-scope",
]
# ==================== IP 访问控制 ====================
# 访问模式:
# production — 生产模式(默认):放行所有 IP
# whitelist — 白名单模式:仅允许白名单内 IP 访问,其余返回 403
# blacklist — 黑名单模式:禁止黑名单内 IP 访问,其余放行
IP_ACCESS_MODE = 'production'
# 白名单whitelist 模式下生效)— 支持单 IP 和 CIDR 网段
IP_WHITELIST = [
'111.32.64.171',
'43.142.0.208',
'223.104.7.77',
'112.7.200.17'
]
# 黑名单blacklist 模式下生效)— 支持单 IP 和 CIDR 网段
IP_BLACKLIST = []
# 免过滤路径(任何模式下都放行,如管理后台、健康检查)
IP_FILTER_EXEMPT_PATHS = []
# ==================== 腾讯云 COS ====================
COS_SECRET_ID = app_secrets.COS_SECRET_ID
COS_SECRET_KEY = app_secrets.COS_SECRET_KEY

View File

@@ -24,6 +24,7 @@ urlpatterns = [
path('houtai/', include('backend.urls')),
path('shangdian/', include('shop.urls')),
path('dengji/', include('rank.urls')),
path('jituan/', include('jituan.urls')),
]
# 重要说明:

View File

@@ -1,296 +1,303 @@
# Generated by Django 4.2.27 on 2026-04-06 23:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="AbnormalUserLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"account_id",
models.CharField(
db_index=True, max_length=20, verbose_name="账号IDphone"
),
),
(
"ip_address",
models.GenericIPAddressField(
blank=True, null=True, verbose_name="IP地址"
),
),
(
"real_ip",
models.GenericIPAddressField(
blank=True, null=True, verbose_name="真实IP穿透代理后"
),
),
(
"x_forwarded_for",
models.CharField(
blank=True,
default="",
max_length=255,
verbose_name="X-Forwarded-For",
),
),
(
"user_agent",
models.CharField(
blank=True,
default="",
max_length=500,
verbose_name="User-Agent",
),
),
(
"request_method",
models.CharField(
blank=True, default="", max_length=10, verbose_name="请求方法"
),
),
(
"request_path",
models.CharField(
blank=True, default="", max_length=500, verbose_name="请求路径"
),
),
(
"request_params",
models.TextField(
blank=True, default="", verbose_name="请求参数GET/POST"
),
),
(
"request_body",
models.TextField(
blank=True, default="", verbose_name="请求体(原始)"
),
),
(
"request_headers",
models.TextField(
blank=True, default="", verbose_name="请求头JSON格式"
),
),
(
"attempt_type",
models.CharField(
blank=True, default="", max_length=50, verbose_name="攻击类型"
),
),
(
"detail",
models.TextField(blank=True, default="", verbose_name="详细描述"),
),
(
"create_time",
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
),
],
options={
"verbose_name": "异常用户日志",
"verbose_name_plural": "异常用户日志",
"db_table": "abnormal_user_log",
},
),
migrations.CreateModel(
name="Permission",
fields=[
(
"perm_id",
models.AutoField(
primary_key=True, serialize=False, verbose_name="权限ID"
),
),
(
"perm_code",
models.CharField(
db_index=True,
max_length=100,
unique=True,
verbose_name="权限编码",
),
),
(
"perm_name",
models.CharField(max_length=100, verbose_name="权限名称"),
),
(
"description",
models.CharField(
blank=True, default="", max_length=200, verbose_name="权限描述"
),
),
(
"create_time",
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
),
(
"update_time",
models.DateTimeField(auto_now=True, verbose_name="更新时间"),
),
],
options={
"verbose_name": "权限",
"verbose_name_plural": "权限",
"db_table": "permission",
},
),
migrations.CreateModel(
name="Role",
fields=[
(
"role_id",
models.AutoField(
primary_key=True, serialize=False, verbose_name="角色ID"
),
),
(
"role_code",
models.CharField(
db_index=True,
max_length=50,
unique=True,
verbose_name="角色编码",
),
),
("role_name", models.CharField(max_length=50, verbose_name="角色名称")),
(
"description",
models.CharField(
blank=True, default="", max_length=200, verbose_name="角色描述"
),
),
(
"create_time",
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
),
(
"update_time",
models.DateTimeField(auto_now=True, verbose_name="更新时间"),
),
],
options={
"verbose_name": "角色",
"verbose_name_plural": "角色",
"db_table": "role",
},
),
migrations.CreateModel(
name="UserRole",
fields=[
("id", models.AutoField(primary_key=True, serialize=False)),
(
"account_id",
models.CharField(
db_index=True, max_length=11, verbose_name="账号ID"
),
),
(
"create_time",
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
),
(
"update_time",
models.DateTimeField(auto_now=True, verbose_name="更新时间"),
),
(
"role",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="backend.role",
verbose_name="角色",
),
),
],
options={
"verbose_name": "用户角色关联",
"verbose_name_plural": "用户角色关联",
"db_table": "user_role",
},
),
migrations.CreateModel(
name="RolePermission",
fields=[
("id", models.AutoField(primary_key=True, serialize=False)),
(
"create_time",
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
),
(
"permission",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="backend.permission",
verbose_name="权限",
),
),
(
"role",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="backend.role",
verbose_name="角色",
),
),
],
options={
"verbose_name": "角色权限关联",
"verbose_name_plural": "角色权限关联",
"db_table": "role_permission",
},
),
migrations.AddIndex(
model_name="role",
index=models.Index(fields=["role_code"], name="role_role_co_b9f497_idx"),
),
migrations.AddIndex(
model_name="permission",
index=models.Index(
fields=["perm_code"], name="permission_perm_co_56cbe3_idx"
),
),
migrations.AddIndex(
model_name="abnormaluserlog",
index=models.Index(
fields=["account_id"], name="abnormal_us_account_425899_idx"
),
),
migrations.AddIndex(
model_name="abnormaluserlog",
index=models.Index(
fields=["ip_address"], name="abnormal_us_ip_addr_3d612b_idx"
),
),
migrations.AddIndex(
model_name="abnormaluserlog",
index=models.Index(
fields=["create_time"], name="abnormal_us_create__ee16da_idx"
),
),
migrations.AlterUniqueTogether(
name="userrole",
unique_together={("account_id", "role")},
),
migrations.AlterUniqueTogether(
name="rolepermission",
unique_together={("role", "permission")},
),
]
# Generated by Django 4.2.27 on 2026-06-18 03:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AbnormalUserLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('account_id', models.CharField(db_index=True, max_length=20, verbose_name='账号IDphone')),
('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='IP地址')),
('real_ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='真实IP穿透代理后')),
('x_forwarded_for', models.CharField(blank=True, default='', max_length=255, verbose_name='X-Forwarded-For')),
('user_agent', models.CharField(blank=True, default='', max_length=500, verbose_name='User-Agent')),
('request_method', models.CharField(blank=True, default='', max_length=10, verbose_name='请求方法')),
('request_path', models.CharField(blank=True, default='', max_length=500, verbose_name='请求路径')),
('request_params', models.TextField(blank=True, default='', verbose_name='请求参数GET/POST')),
('request_body', models.TextField(blank=True, default='', verbose_name='请求体(原始)')),
('request_headers', models.TextField(blank=True, default='', verbose_name='请求头JSON格式')),
('attempt_type', models.CharField(blank=True, default='', max_length=50, verbose_name='攻击类型')),
('detail', models.TextField(blank=True, default='', verbose_name='详细描述')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
],
options={
'verbose_name': '异常用户日志',
'verbose_name_plural': '异常用户日志',
'db_table': 'abnormal_user_log',
},
),
migrations.CreateModel(
name='LeaderDailyStats',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('LeaderID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='组长用户ID')),
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
('InvitedManagerCount', models.IntegerField(db_column='yaoqing_guanshi_shu', default=0, verbose_name='邀请管事总数')),
('TotalIncome', models.DecimalField(db_column='shouru_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='收入总额(分佣)')),
('CommissionAmount', models.DecimalField(db_column='fenyong_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='分佣金额')),
],
options={
'verbose_name': '组长每日统计',
'db_table': 'zuzhang_ri_tongji',
},
),
migrations.CreateModel(
name='ManagerDailyStats',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ManagerID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='管事用户ID')),
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
('InvitedPlayerCount', models.IntegerField(db_column='yaoqing_dashou_shu', default=0, verbose_name='邀请打手总数')),
('RechargedPlayerCount', models.IntegerField(db_column='chongzhi_dashou_shu', default=0, verbose_name='充值打手数量')),
('TotalIncome', models.DecimalField(db_column='shouru_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='收入总额')),
],
options={
'verbose_name': '管事每日统计',
'db_table': 'guanshi_ri_tongji',
},
),
migrations.CreateModel(
name='ManagerRenewalDailyStats',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ManagerID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='管事用户ID')),
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
('RenewalTotal', models.IntegerField(db_column='xufei_zongshu', default=0, verbose_name='今日续费总数')),
('RenewalRevenue', models.DecimalField(db_column='xufei_shouyi', decimal_places=2, default=0.0, max_digits=12, verbose_name='续费收益金额')),
],
options={
'verbose_name': '管事续费每日统计',
'db_table': 'guanshi_xufei_ri_tongji',
},
),
migrations.CreateModel(
name='MerchantDailyStats',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('MerchantID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='商家用户ID')),
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
('AssignedOrderCount', models.IntegerField(db_column='paifa_dingdan_shu', default=0, verbose_name='派发订单总数')),
('AssignedAmount', models.DecimalField(db_column='paifa_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='派发订单总金额')),
('SettledOrderCount', models.IntegerField(db_column='jiesuan_dingdan_shu', default=0, verbose_name='结算订单总数')),
('SettledAmount', models.DecimalField(db_column='jiesuan_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='结算总金额')),
('RefundOrderCount', models.IntegerField(db_column='tuikuan_dingdan_shu', default=0, verbose_name='退款订单总数')),
('RefundAmount', models.DecimalField(db_column='tuikuan_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总金额')),
],
options={
'verbose_name': '商家每日统计',
'db_table': 'shangjia_ri_tongji',
},
),
migrations.CreateModel(
name='Permission',
fields=[
('perm_id', models.AutoField(primary_key=True, serialize=False, verbose_name='权限ID')),
('perm_code', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='权限编码')),
('perm_name', models.CharField(max_length=100, verbose_name='权限名称')),
('description', models.CharField(blank=True, default='', max_length=200, verbose_name='权限描述')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '权限',
'verbose_name_plural': '权限',
'db_table': 'permission',
'managed': True,
},
),
migrations.CreateModel(
name='PlayerDailyStats',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('PlayerID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='打手用户ID')),
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
('AcceptedOrderTotal', models.IntegerField(db_column='jiedan_zongliang', default=0, verbose_name='接单总量')),
('CompletedOrderTotal', models.IntegerField(db_column='chengjiao_zongliang', default=0, verbose_name='成交单总量')),
('AcceptedAmount', models.DecimalField(db_column='jiedan_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='接单总金额')),
('CompletedAmount', models.DecimalField(db_column='chengjiao_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='成交总金额')),
('RefundCount', models.IntegerField(db_column='tuikuan_liang', default=0, verbose_name='退款订单量')),
('RefundAmount', models.DecimalField(db_column='tuikuan_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总金额')),
],
options={
'verbose_name': '打手每日统计',
'db_table': 'dashou_ri_tongji',
},
),
migrations.CreateModel(
name='Role',
fields=[
('role_id', models.AutoField(primary_key=True, serialize=False, verbose_name='角色ID')),
('role_code', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='角色编码')),
('role_name', models.CharField(max_length=50, verbose_name='角色名称')),
('description', models.CharField(blank=True, default='', max_length=200, verbose_name='角色描述')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '角色',
'verbose_name_plural': '角色',
'db_table': 'role',
'managed': True,
},
),
migrations.CreateModel(
name='WithdrawalDailyStats',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
('WithdrawalType', models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], db_column='leixing', verbose_name='用户类型')),
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='当天提现总额(元)')),
],
options={
'verbose_name': '每日提现统计',
'db_table': 'tixian_ri_tongji',
'indexes': [models.Index(fields=['Date', 'WithdrawalType'], name='tixian_ri_t_riqi_c18bb2_idx')],
'unique_together': {('Date', 'WithdrawalType')},
},
),
migrations.CreateModel(
name='UserRole',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色')),
],
options={
'verbose_name': '用户角色关联',
'verbose_name_plural': '用户角色关联',
'db_table': 'user_role',
'managed': True,
},
),
migrations.CreateModel(
name='RolePermission',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.permission', verbose_name='权限')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色')),
],
options={
'verbose_name': '角色权限关联',
'verbose_name_plural': '角色权限关联',
'db_table': 'role_permission',
'managed': True,
},
),
migrations.AddIndex(
model_name='role',
index=models.Index(fields=['role_code'], name='role_role_co_b9f497_idx'),
),
migrations.AddIndex(
model_name='playerdailystats',
index=models.Index(fields=['Date', '-AcceptedOrderTotal'], name='dashou_ri_t_riqi_95a785_idx'),
),
migrations.AddIndex(
model_name='playerdailystats',
index=models.Index(fields=['Date', '-CompletedOrderTotal'], name='dashou_ri_t_riqi_ad9c1a_idx'),
),
migrations.AddIndex(
model_name='playerdailystats',
index=models.Index(fields=['Date', '-AcceptedAmount'], name='dashou_ri_t_riqi_5da703_idx'),
),
migrations.AddIndex(
model_name='playerdailystats',
index=models.Index(fields=['Date', '-CompletedAmount'], name='dashou_ri_t_riqi_0a3156_idx'),
),
migrations.AlterUniqueTogether(
name='playerdailystats',
unique_together={('PlayerID', 'Date')},
),
migrations.AddIndex(
model_name='permission',
index=models.Index(fields=['perm_code'], name='permission_perm_co_56cbe3_idx'),
),
migrations.AddIndex(
model_name='merchantdailystats',
index=models.Index(fields=['Date', '-AssignedAmount'], name='shangjia_ri_riqi_d00f7f_idx'),
),
migrations.AddIndex(
model_name='merchantdailystats',
index=models.Index(fields=['Date', '-SettledAmount'], name='shangjia_ri_riqi_25772a_idx'),
),
migrations.AddIndex(
model_name='merchantdailystats',
index=models.Index(fields=['Date', '-AssignedOrderCount'], name='shangjia_ri_riqi_e05c54_idx'),
),
migrations.AlterUniqueTogether(
name='merchantdailystats',
unique_together={('MerchantID', 'Date')},
),
migrations.AddIndex(
model_name='managerrenewaldailystats',
index=models.Index(fields=['Date', '-RenewalTotal'], name='guanshi_xuf_riqi_7c128f_idx'),
),
migrations.AddIndex(
model_name='managerrenewaldailystats',
index=models.Index(fields=['Date', '-RenewalRevenue'], name='guanshi_xuf_riqi_8f09b5_idx'),
),
migrations.AlterUniqueTogether(
name='managerrenewaldailystats',
unique_together={('ManagerID', 'Date')},
),
migrations.AddIndex(
model_name='managerdailystats',
index=models.Index(fields=['Date', '-InvitedPlayerCount'], name='guanshi_ri__riqi_075e1a_idx'),
),
migrations.AddIndex(
model_name='managerdailystats',
index=models.Index(fields=['Date', '-RechargedPlayerCount'], name='guanshi_ri__riqi_78a79d_idx'),
),
migrations.AddIndex(
model_name='managerdailystats',
index=models.Index(fields=['Date', '-TotalIncome'], name='guanshi_ri__riqi_334f52_idx'),
),
migrations.AlterUniqueTogether(
name='managerdailystats',
unique_together={('ManagerID', 'Date')},
),
migrations.AddIndex(
model_name='leaderdailystats',
index=models.Index(fields=['Date', '-InvitedManagerCount'], name='zuzhang_ri__riqi_87169e_idx'),
),
migrations.AddIndex(
model_name='leaderdailystats',
index=models.Index(fields=['Date', '-TotalIncome'], name='zuzhang_ri__riqi_1d7e51_idx'),
),
migrations.AlterUniqueTogether(
name='leaderdailystats',
unique_together={('LeaderID', 'Date')},
),
migrations.AddIndex(
model_name='abnormaluserlog',
index=models.Index(fields=['account_id'], name='abnormal_us_account_425899_idx'),
),
migrations.AddIndex(
model_name='abnormaluserlog',
index=models.Index(fields=['ip_address'], name='abnormal_us_ip_addr_3d612b_idx'),
),
migrations.AddIndex(
model_name='abnormaluserlog',
index=models.Index(fields=['CreateTime'], name='abnormal_us_create__ee16da_idx'),
),
migrations.AlterUniqueTogether(
name='userrole',
unique_together={('account_id', 'role')},
),
migrations.AlterUniqueTogether(
name='rolepermission',
unique_together={('role', 'permission')},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.27 on 2026-06-19 19:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('backend', '0001_initial'),
]
operations = [
migrations.RenameIndex(
model_name='abnormaluserlog',
new_name='abnormal_us_CreateT_38f96c_idx',
old_name='abnormal_us_create__ee16da_idx',
),
]

View File

@@ -1,339 +0,0 @@
# Generated by Django 4.2.27 on 2026-04-09 14:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("backend", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="ZuzhangRiTongji",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"yonghuid",
models.CharField(
db_index=True, max_length=7, verbose_name="组长用户ID"
),
),
("riqi", models.DateField(db_index=True, verbose_name="日期")),
(
"yaoqing_guanshi_shu",
models.IntegerField(default=0, verbose_name="邀请管事总数"),
),
(
"shouru_zonge",
models.DecimalField(
decimal_places=2,
default=0.0,
max_digits=12,
verbose_name="收入总额(分佣)",
),
),
(
"fenyong_jine",
models.DecimalField(
decimal_places=2,
default=0.0,
max_digits=12,
verbose_name="分佣金额",
),
),
],
options={
"verbose_name": "组长每日统计",
"db_table": "zuzhang_ri_tongji",
"indexes": [
models.Index(
fields=["riqi", "-yaoqing_guanshi_shu"],
name="zuzhang_ri__riqi_87169e_idx",
),
models.Index(
fields=["riqi", "-shouru_zonge"],
name="zuzhang_ri__riqi_1d7e51_idx",
),
],
"unique_together": {("yonghuid", "riqi")},
},
),
migrations.CreateModel(
name="ShangjiaRiTongji",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"yonghuid",
models.CharField(
db_index=True, max_length=7, verbose_name="商家用户ID"
),
),
("riqi", models.DateField(db_index=True, verbose_name="日期")),
(
"paifa_dingdan_shu",
models.IntegerField(default=0, verbose_name="派发订单总数"),
),
(
"paifa_jine",
models.DecimalField(
decimal_places=2,
default=0.0,
max_digits=12,
verbose_name="派发订单总金额",
),
),
(
"jiesuan_dingdan_shu",
models.IntegerField(default=0, verbose_name="结算订单总数"),
),
(
"jiesuan_jine",
models.DecimalField(
decimal_places=2,
default=0.0,
max_digits=12,
verbose_name="结算总金额",
),
),
(
"tuikuan_dingdan_shu",
models.IntegerField(default=0, verbose_name="退款订单总数"),
),
(
"tuikuan_jine",
models.DecimalField(
decimal_places=2,
default=0.0,
max_digits=12,
verbose_name="退款总金额",
),
),
],
options={
"verbose_name": "商家每日统计",
"db_table": "shangjia_ri_tongji",
"indexes": [
models.Index(
fields=["riqi", "-paifa_jine"],
name="shangjia_ri_riqi_d00f7f_idx",
),
models.Index(
fields=["riqi", "-jiesuan_jine"],
name="shangjia_ri_riqi_25772a_idx",
),
models.Index(
fields=["riqi", "-paifa_dingdan_shu"],
name="shangjia_ri_riqi_e05c54_idx",
),
],
"unique_together": {("yonghuid", "riqi")},
},
),
migrations.CreateModel(
name="GuanshiXufeiRiTongji",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"yonghuid",
models.CharField(
db_index=True, max_length=7, verbose_name="管事用户ID"
),
),
("riqi", models.DateField(db_index=True, verbose_name="日期")),
(
"xufei_zongshu",
models.IntegerField(default=0, verbose_name="今日续费总数"),
),
(
"xufei_shouyi",
models.DecimalField(
decimal_places=2,
default=0.0,
max_digits=12,
verbose_name="续费收益金额",
),
),
],
options={
"verbose_name": "管事续费每日统计",
"db_table": "guanshi_xufei_ri_tongji",
"indexes": [
models.Index(
fields=["riqi", "-xufei_zongshu"],
name="guanshi_xuf_riqi_7c128f_idx",
),
models.Index(
fields=["riqi", "-xufei_shouyi"],
name="guanshi_xuf_riqi_8f09b5_idx",
),
],
"unique_together": {("yonghuid", "riqi")},
},
),
migrations.CreateModel(
name="GuanshiRiTongji",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"yonghuid",
models.CharField(
db_index=True, max_length=7, verbose_name="管事用户ID"
),
),
("riqi", models.DateField(db_index=True, verbose_name="日期")),
(
"yaoqing_dashou_shu",
models.IntegerField(default=0, verbose_name="邀请打手总数"),
),
(
"chongzhi_dashou_shu",
models.IntegerField(default=0, verbose_name="充值打手数量"),
),
(
"shouru_zonge",
models.DecimalField(
decimal_places=2,
default=0.0,
max_digits=12,
verbose_name="收入总额",
),
),
],
options={
"verbose_name": "管事每日统计",
"db_table": "guanshi_ri_tongji",
"indexes": [
models.Index(
fields=["riqi", "-yaoqing_dashou_shu"],
name="guanshi_ri__riqi_075e1a_idx",
),
models.Index(
fields=["riqi", "-chongzhi_dashou_shu"],
name="guanshi_ri__riqi_78a79d_idx",
),
models.Index(
fields=["riqi", "-shouru_zonge"],
name="guanshi_ri__riqi_334f52_idx",
),
],
"unique_together": {("yonghuid", "riqi")},
},
),
migrations.CreateModel(
name="DashouRiTongji",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"yonghuid",
models.CharField(
db_index=True, max_length=7, verbose_name="打手用户ID"
),
),
("riqi", models.DateField(db_index=True, verbose_name="日期")),
(
"jiedan_zongliang",
models.IntegerField(default=0, verbose_name="接单总量"),
),
(
"chengjiao_zongliang",
models.IntegerField(default=0, verbose_name="成交单总量"),
),
(
"jiedan_zonge",
models.DecimalField(
decimal_places=2,
default=0.0,
max_digits=12,
verbose_name="接单总金额",
),
),
(
"chengjiao_zonge",
models.DecimalField(
decimal_places=2,
default=0.0,
max_digits=12,
verbose_name="成交总金额",
),
),
(
"tuikuan_liang",
models.IntegerField(default=0, verbose_name="退款订单量"),
),
(
"tuikuan_jine",
models.DecimalField(
decimal_places=2,
default=0.0,
max_digits=12,
verbose_name="退款总金额",
),
),
],
options={
"verbose_name": "打手每日统计",
"db_table": "dashou_ri_tongji",
"indexes": [
models.Index(
fields=["riqi", "-jiedan_zongliang"],
name="dashou_ri_t_riqi_95a785_idx",
),
models.Index(
fields=["riqi", "-chengjiao_zongliang"],
name="dashou_ri_t_riqi_ad9c1a_idx",
),
models.Index(
fields=["riqi", "-jiedan_zonge"],
name="dashou_ri_t_riqi_5da703_idx",
),
models.Index(
fields=["riqi", "-chengjiao_zonge"],
name="dashou_ri_t_riqi_0a3156_idx",
),
],
"unique_together": {("yonghuid", "riqi")},
},
),
]

View File

@@ -1,54 +0,0 @@
# Generated by Django 4.2.27 on 2026-04-10 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("backend", "0002_zuzhangritongji_shangjiaritongji_and_more"),
]
operations = [
migrations.CreateModel(
name="TixianRiTongji",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("riqi", models.DateField(db_index=True, verbose_name="日期")),
(
"leixing",
models.IntegerField(
choices=[(1, "打手"), (2, "管事"), (3, "组长")],
verbose_name="用户类型",
),
),
(
"total_amount",
models.DecimalField(
decimal_places=2,
default=0.0,
max_digits=12,
verbose_name="当天提现总额(元)",
),
),
],
options={
"verbose_name": "每日提现统计",
"db_table": "tixian_ri_tongji",
"indexes": [
models.Index(
fields=["riqi", "leixing"], name="tixian_ri_t_riqi_c18bb2_idx"
)
],
"unique_together": {("riqi", "leixing")},
},
),
]

View File

@@ -0,0 +1,40 @@
from django.db import migrations, models
from django.db.models import Max
def _dedupe_withdrawal_daily(apps, schema_editor):
Model = apps.get_model('backend', 'WithdrawalDailyStats')
for row in Model.objects.values('club_id', 'Date', 'WithdrawalType').annotate(max_id=Max('id')):
Model.objects.filter(
club_id=row['club_id'],
Date=row['Date'],
WithdrawalType=row['WithdrawalType'],
).exclude(id=row['max_id']).delete()
class Migration(migrations.Migration):
dependencies = [
('backend', '0002_rename_abnormal_us_create__ee16da_idx_abnormal_us_createt_38f96c_idx'),
]
operations = [
migrations.AddField(
model_name='withdrawaldailystats',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AlterUniqueTogether(
name='withdrawaldailystats',
unique_together=set(),
),
migrations.RunPython(_dedupe_withdrawal_daily, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='withdrawaldailystats',
unique_together={('club_id', 'Date', 'WithdrawalType')},
),
migrations.AddIndex(
model_name='withdrawaldailystats',
index=models.Index(fields=['club_id', 'Date', 'WithdrawalType'], name='tixian_ri_club_date_type_idx'),
),
]

View File

@@ -1,34 +0,0 @@
# Generated by Django 4.2.27 on 2026-06-15 01:42
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('backend', '0003_tixianritongji'),
]
operations = [
migrations.AlterField(
model_name='rolepermission',
name='permission',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.permission', verbose_name='权限'),
),
migrations.AlterField(
model_name='rolepermission',
name='role',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色'),
),
migrations.AlterField(
model_name='tixianritongji',
name='leixing',
field=models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], verbose_name='用户类型'),
),
migrations.AlterField(
model_name='userrole',
name='role',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色'),
),
]

View File

@@ -0,0 +1,91 @@
from django.db import migrations, models
from django.db.models import Max
def _dedupe_role_daily(apps, schema_editor, model_name, user_field):
Model = apps.get_model('backend', model_name)
for row in Model.objects.values('club_id', user_field, 'Date').annotate(max_id=Max('id')):
Model.objects.filter(
club_id=row['club_id'],
**{user_field: row[user_field], 'Date': row['Date']},
).exclude(id=row['max_id']).delete()
def _dedupe_merchant(apps, schema_editor):
_dedupe_role_daily(apps, schema_editor, 'MerchantDailyStats', 'MerchantID')
def _dedupe_player(apps, schema_editor):
_dedupe_role_daily(apps, schema_editor, 'PlayerDailyStats', 'PlayerID')
def _dedupe_leader(apps, schema_editor):
_dedupe_role_daily(apps, schema_editor, 'LeaderDailyStats', 'LeaderID')
def _dedupe_manager(apps, schema_editor):
_dedupe_role_daily(apps, schema_editor, 'ManagerDailyStats', 'ManagerID')
def _dedupe_manager_renewal(apps, schema_editor):
_dedupe_role_daily(apps, schema_editor, 'ManagerRenewalDailyStats', 'ManagerID')
class Migration(migrations.Migration):
dependencies = [
('backend', '0003_withdrawaldailystats_club_id'),
]
operations = [
migrations.AddField(
model_name='merchantdailystats',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='playerdailystats',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='leaderdailystats',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='managerdailystats',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='managerrenewaldailystats',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.RunPython(_dedupe_merchant, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='merchantdailystats',
unique_together={('club_id', 'MerchantID', 'Date')},
),
migrations.RunPython(_dedupe_player, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='playerdailystats',
unique_together={('club_id', 'PlayerID', 'Date')},
),
migrations.RunPython(_dedupe_leader, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='leaderdailystats',
unique_together={('club_id', 'LeaderID', 'Date')},
),
migrations.RunPython(_dedupe_manager, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='managerdailystats',
unique_together={('club_id', 'ManagerID', 'Date')},
),
migrations.RunPython(_dedupe_manager_renewal, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='managerrenewaldailystats',
unique_together={('club_id', 'ManagerID', 'Date')},
),
]

View File

@@ -1,271 +1,333 @@
from django.db import models
from gvsdsdk.model_base import QModel
# ==================== 1. 角色表 ====================
class Role(QModel):
"""角色表"""
role_id = models.AutoField(primary_key=True, verbose_name='角色ID')
role_code = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='角色编码')
role_name = models.CharField(max_length=50, verbose_name='角色名称')
description = models.CharField(max_length=200, blank=True, default='', verbose_name='角色描述')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'role'
verbose_name = '角色'
verbose_name_plural = '角色'
indexes = [
models.Index(fields=['role_code']),
]
def __str__(self):
return self.role_name
# ==================== 2. 权限表 ====================
class Permission(QModel):
"""权限表"""
perm_id = models.AutoField(primary_key=True, verbose_name='权限ID')
perm_code = models.CharField(max_length=100, unique=True, db_index=True, verbose_name='权限编码')
perm_name = models.CharField(max_length=100, verbose_name='权限名称')
description = models.CharField(max_length=200, blank=True, default='', verbose_name='权限描述')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'permission'
verbose_name = '权限'
verbose_name_plural = '权限'
indexes = [
models.Index(fields=['perm_code']),
]
def __str__(self):
return f"{self.perm_name} ({self.perm_code})"
# ==================== 3. 角色-权限关联表 ====================
class RolePermission(QModel):
"""角色权限关联表"""
id = models.AutoField(primary_key=True)
role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色')
permission = models.ForeignKey(Permission, on_delete=models.CASCADE, db_index=True, verbose_name='权限')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
db_table = 'role_permission'
verbose_name = '角色权限关联'
verbose_name_plural = '角色权限关联'
unique_together = [['role', 'permission']] # 防止重复授权
def __str__(self):
return f"{self.role.role_name} -> {self.permission.perm_name}"
# ==================== 4. 用户-角色关联表 ====================
class UserRole(QModel):
"""用户角色关联表"""
id = models.AutoField(primary_key=True)
account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID') # 对应 UserMain 的 phone 字段
role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'user_role'
verbose_name = '用户角色关联'
verbose_name_plural = '用户角色关联'
unique_together = [['account_id', 'role']] # 同一用户同一角色只能有一条
def __str__(self):
return f"{self.account_id} -> {self.role.role_name}"
class AbnormalUserLog(QModel):
"""记录恶意尝试越权操作的用户信息"""
# 账号信息
account_id = models.CharField(max_length=20, db_index=True, verbose_name='账号IDphone')
# 网络信息
ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP地址')
real_ip = models.GenericIPAddressField(null=True, blank=True, verbose_name='真实IP穿透代理后')
x_forwarded_for = models.CharField(max_length=255, blank=True, default='', verbose_name='X-Forwarded-For')
user_agent = models.CharField(max_length=500, blank=True, default='', verbose_name='User-Agent')
# 地理位置信息需借助第三方API这里只存储请求时的IP后续可离线解析
# 但我们可以存储更多请求细节
request_method = models.CharField(max_length=10, blank=True, default='', verbose_name='请求方法')
request_path = models.CharField(max_length=500, blank=True, default='', verbose_name='请求路径')
request_params = models.TextField(blank=True, default='', verbose_name='请求参数GET/POST')
request_body = models.TextField(blank=True, default='', verbose_name='请求体(原始')
request_headers = models.TextField(blank=True, default='', verbose_name='请求头JSON格式')
# 攻击特征
attempt_type = models.CharField(max_length=50, blank=True, default='', verbose_name='攻击类型')
detail = models.TextField(blank=True, default='', verbose_name='详细描述')
# 时间戳
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
db_table = 'abnormal_user_log'
verbose_name = '异常用户日志'
verbose_name_plural = verbose_name
indexes = [
models.Index(fields=['account_id']),
models.Index(fields=['ip_address']),
models.Index(fields=['create_time']),
]
def __str__(self):
return f"{self.account_id} @ {self.ip_address} - {self.create_time}"
class ShangjiaRiTongji(QModel):
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='商家用户ID')
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
# 派发统计
paifa_dingdan_shu = models.IntegerField(default=0, verbose_name='派发订单总数')
paifa_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='派发订单总金额')
# 结算统计(成交)
jiesuan_dingdan_shu = models.IntegerField(default=0, verbose_name='结算订单总数')
jiesuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='结算总金额')
# 退款统计(可选)
tuikuan_dingdan_shu = models.IntegerField(default=0, verbose_name='退款订单总数')
tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额')
class Meta:
db_table = 'shangjia_ri_tongji'
verbose_name = '商家每日统计'
unique_together = [['yonghuid', 'riqi']] # 每人每天一条记录
indexes = [
models.Index(fields=['riqi', '-paifa_jine']),
models.Index(fields=['riqi', '-jiesuan_jine']),
models.Index(fields=['riqi', '-paifa_dingdan_shu']),
]
class DashouRiTongji(QModel):
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='打手用户ID')
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
# 接单统计
jiedan_zongliang = models.IntegerField(default=0, verbose_name='接单总量')
chengjiao_zongliang = models.IntegerField(default=0, verbose_name='成交单总量')
# 金额统计
jiedan_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='接单总金额')
chengjiao_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='成交总金额')
# 退款统计
tuikuan_liang = models.IntegerField(default=0, verbose_name='退款订单量')
tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额')
class Meta:
db_table = 'dashou_ri_tongji'
verbose_name = '打手每日统计'
unique_together = [['yonghuid', 'riqi']]
indexes = [
models.Index(fields=['riqi', '-jiedan_zongliang']),
models.Index(fields=['riqi', '-chengjiao_zongliang']),
models.Index(fields=['riqi', '-jiedan_zonge']),
models.Index(fields=['riqi', '-chengjiao_zonge']),
]
class ZuzhangRiTongji(QModel):
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='组长用户ID')
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
yaoqing_guanshi_shu = models.IntegerField(default=0, verbose_name='邀请管事总数')
shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额(分佣)')
# 可选明细
fenyong_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='分佣金额')
class Meta:
db_table = 'zuzhang_ri_tongji'
verbose_name = '组长每日统计'
unique_together = [['yonghuid', 'riqi']]
indexes = [
models.Index(fields=['riqi', '-yaoqing_guanshi_shu']),
models.Index(fields=['riqi', '-shouru_zonge']),
]
class GuanshiRiTongji(QModel):
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID')
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
yaoqing_dashou_shu = models.IntegerField(default=0, verbose_name='邀请打手总数')
chongzhi_dashou_shu = models.IntegerField(default=0, verbose_name='充值打手数量')
shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额')
class Meta:
db_table = 'guanshi_ri_tongji'
verbose_name = '管事每日统计'
unique_together = [['yonghuid', 'riqi']]
indexes = [
models.Index(fields=['riqi', '-yaoqing_dashou_shu']),
models.Index(fields=['riqi', '-chongzhi_dashou_shu']),
models.Index(fields=['riqi', '-shouru_zonge']),
]
class GuanshiXufeiRiTongji(QModel):
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID')
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
xufei_zongshu = models.IntegerField(default=0, verbose_name='今日续费总数') # 续费订单数(或续费打手数)
xufei_shouyi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='续费收益金额')
class Meta:
db_table = 'guanshi_xufei_ri_tongji'
verbose_name = '管事续费每日统计'
unique_together = [['yonghuid', 'riqi']]
indexes = [
models.Index(fields=['riqi', '-xufei_zongshu']),
models.Index(fields=['riqi', '-xufei_shouyi']),
]
# models.py 中添加
class TixianRiTongji(QModel):
"""
每日提现统计表(按身份统计当天提现总额)
"""
riqi = models.DateField(db_index=True, verbose_name='日期')
leixing = models.IntegerField(
choices=[(1, '打手'), (2, '管事'), (3, '组长'),
(4, '考核官'), (5, '打手押金'), (6, '商家余额')],
verbose_name='用户类型'
)
total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='当天提现总额(元)')
class Meta:
db_table = 'tixian_ri_tongji'
verbose_name = '每日提现统计'
unique_together = [['riqi', 'leixing']] # 每天每个身份一条记录
indexes = [models.Index(fields=['riqi', 'leixing'])]
def __str__(self):
return f"{self.riqi} - {self.get_leixing_display()} 提现总额: {self.total_amount}"
from django.db import models
from gvsdsdk.model_base import QModel
# ==================== 1. 角色表 ====================
class Role(QModel):
"""角色表"""
role_id = models.AutoField(primary_key=True, verbose_name='角色ID')
role_code = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='角色编码')
role_name = models.CharField(max_length=50, verbose_name='角色名称')
description = models.CharField(max_length=200, blank=True, default='', verbose_name='角色描述')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'role'
verbose_name = '角色'
verbose_name_plural = '角色'
managed = True
indexes = [
models.Index(fields=['role_code']),
]
def __str__(self):
return self.role_name
# ==================== 2. 权限表 ====================
class Permission(QModel):
"""权限表"""
perm_id = models.AutoField(primary_key=True, verbose_name='权限ID')
perm_code = models.CharField(max_length=100, unique=True, db_index=True, verbose_name='权限编码')
perm_name = models.CharField(max_length=100, verbose_name='权限名称')
description = models.CharField(max_length=200, blank=True, default='', verbose_name='权限描述')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'permission'
verbose_name = '权限'
verbose_name_plural = '权限'
managed = True
indexes = [
models.Index(fields=['perm_code']),
]
def __str__(self):
return f"{self.perm_name} ({self.perm_code})"
# ==================== 3. 角色-权限关联表 ====================
class RolePermission(QModel):
"""角色权限关联表"""
id = models.AutoField(primary_key=True)
role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色')
permission = models.ForeignKey(Permission, on_delete=models.CASCADE, db_index=True, verbose_name='权限')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
db_table = 'role_permission'
verbose_name = '角色权限关联'
verbose_name_plural = '角色权限关联'
managed = True
unique_together = [['role', 'permission']]
def __str__(self):
return f"{self.role.role_name} -> {self.permission.perm_name}"
# ==================== 4. 用户-角色关联表 ====================
class UserRole(QModel):
"""用户角色关联表"""
id = models.AutoField(primary_key=True)
account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID')
role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'user_role'
verbose_name = '用户角色关联'
verbose_name_plural = '用户角色关联'
managed = True
unique_together = [['account_id', 'role']]
def __str__(self):
return f"{self.account_id} -> {self.role.role_name}"
# ==================== 5. 异常用户日志 ====================
class AbnormalUserLog(QModel):
"""记录恶意尝试越权操作的用户信息"""
account_id = models.CharField(max_length=20, db_index=True, verbose_name='账号IDphone')
ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP地址')
real_ip = models.GenericIPAddressField(null=True, blank=True, verbose_name='真实IP穿透代理后')
x_forwarded_for = models.CharField(max_length=255, blank=True, default='', verbose_name='X-Forwarded-For')
user_agent = models.CharField(max_length=500, blank=True, default='', verbose_name='User-Agent')
request_method = models.CharField(max_length=10, blank=True, default='', verbose_name='请求方法')
request_path = models.CharField(max_length=500, blank=True, default='', verbose_name='请求路径')
request_params = models.TextField(blank=True, default='', verbose_name='请求参数GET/POST')
request_body = models.TextField(blank=True, default='', verbose_name='请求体(原始')
request_headers = models.TextField(blank=True, default='', verbose_name='请求JSON格式')
attempt_type = models.CharField(max_length=50, blank=True, default='', verbose_name='攻击类型')
detail = models.TextField(blank=True, default='', verbose_name='详细描述')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
db_table = 'abnormal_user_log'
verbose_name = '异常用户日志'
verbose_name_plural = verbose_name
indexes = [
models.Index(fields=['account_id']),
models.Index(fields=['ip_address']),
models.Index(fields=['CreateTime']),
]
def __str__(self):
return f"{self.account_id} @ {self.ip_address} - {self.CreateTime}"
# ==================== 6. 商家每日统计 ====================
class MerchantDailyStats(QModel):
"""商家每日统计表(原 ShangjiaRiTongji"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
MerchantID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='商家用户ID')
Date = models.DateField(
db_index=True, db_column='riqi', verbose_name='日期')
# 派发统计
AssignedOrderCount = models.IntegerField(
default=0, db_column='paifa_dingdan_shu', verbose_name='派发订单总数')
AssignedAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='paifa_jine', verbose_name='派发订单总金额')
# 结算统计
SettledOrderCount = models.IntegerField(
default=0, db_column='jiesuan_dingdan_shu', verbose_name='结算订单总数')
SettledAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='jiesuan_jine', verbose_name='结算总金额')
# 退款统计
RefundOrderCount = models.IntegerField(
default=0, db_column='tuikuan_dingdan_shu', verbose_name='退款订单总数')
RefundAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='tuikuan_jine', verbose_name='退款总金额')
class Meta:
db_table = 'shangjia_ri_tongji'
verbose_name = '商家每日统计'
unique_together = [['club_id', 'MerchantID', 'Date']]
indexes = [
models.Index(fields=['Date', '-AssignedAmount']),
models.Index(fields=['Date', '-SettledAmount']),
models.Index(fields=['Date', '-AssignedOrderCount']),
]
def __str__(self):
return f"商家{self.MerchantID} {self.Date}"
# ==================== 7. 打手每日统计 ====================
class PlayerDailyStats(QModel):
"""打手每日统计表(原 DashouRiTongji"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
PlayerID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='打手用户ID')
Date = models.DateField(
db_index=True, db_column='riqi', verbose_name='日期')
# 接单统计
AcceptedOrderTotal = models.IntegerField(
default=0, db_column='jiedan_zongliang', verbose_name='接单总量')
CompletedOrderTotal = models.IntegerField(
default=0, db_column='chengjiao_zongliang', verbose_name='成交单总量')
# 金额统计
AcceptedAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='jiedan_zonge', verbose_name='接单总金额')
CompletedAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='chengjiao_zonge', verbose_name='成交总金额')
# 退款统计
RefundCount = models.IntegerField(
default=0, db_column='tuikuan_liang', verbose_name='退款订单量')
RefundAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='tuikuan_jine', verbose_name='退款总金额')
class Meta:
db_table = 'dashou_ri_tongji'
verbose_name = '打手每日统计'
unique_together = [['club_id', 'PlayerID', 'Date']]
indexes = [
models.Index(fields=['Date', '-AcceptedOrderTotal']),
models.Index(fields=['Date', '-CompletedOrderTotal']),
models.Index(fields=['Date', '-AcceptedAmount']),
models.Index(fields=['Date', '-CompletedAmount']),
]
def __str__(self):
return f"打手{self.PlayerID} {self.Date}"
# ==================== 8. 组长每日统计 ====================
class LeaderDailyStats(QModel):
"""组长每日统计表(原 ZuzhangRiTongji"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
LeaderID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='组长用户ID')
Date = models.DateField(
db_index=True, db_column='riqi', verbose_name='日期')
InvitedManagerCount = models.IntegerField(
default=0, db_column='yaoqing_guanshi_shu', verbose_name='邀请管事总数')
TotalIncome = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='shouru_zonge', verbose_name='收入总额(分佣)')
CommissionAmount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='fenyong_jine', verbose_name='分佣金额')
class Meta:
db_table = 'zuzhang_ri_tongji'
verbose_name = '组长每日统计'
unique_together = [['club_id', 'LeaderID', 'Date']]
indexes = [
models.Index(fields=['Date', '-InvitedManagerCount']),
models.Index(fields=['Date', '-TotalIncome']),
]
def __str__(self):
return f"组长{self.LeaderID} {self.Date}"
# ==================== 9. 管事每日统计 ====================
class ManagerDailyStats(QModel):
"""管事每日统计表(原 GuanshiRiTongji"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
ManagerID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='管事用户ID')
Date = models.DateField(
db_index=True, db_column='riqi', verbose_name='日期')
InvitedPlayerCount = models.IntegerField(
default=0, db_column='yaoqing_dashou_shu', verbose_name='邀请打手总数')
RechargedPlayerCount = models.IntegerField(
default=0, db_column='chongzhi_dashou_shu', verbose_name='充值打手数量')
TotalIncome = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='shouru_zonge', verbose_name='收入总额')
class Meta:
db_table = 'guanshi_ri_tongji'
verbose_name = '管事每日统计'
unique_together = [['club_id', 'ManagerID', 'Date']]
indexes = [
models.Index(fields=['Date', '-InvitedPlayerCount']),
models.Index(fields=['Date', '-RechargedPlayerCount']),
models.Index(fields=['Date', '-TotalIncome']),
]
def __str__(self):
return f"管事{self.ManagerID} {self.Date}"
# ==================== 10. 管事续费每日统计 ====================
class ManagerRenewalDailyStats(QModel):
"""管事续费每日统计表(原 GuanshiXufeiRiTongji"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
ManagerID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='管事用户ID')
Date = models.DateField(
db_index=True, db_column='riqi', verbose_name='日期')
RenewalTotal = models.IntegerField(
default=0, db_column='xufei_zongshu', verbose_name='今日续费总数')
RenewalRevenue = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00,
db_column='xufei_shouyi', verbose_name='续费收益金额')
class Meta:
db_table = 'guanshi_xufei_ri_tongji'
verbose_name = '管事续费每日统计'
unique_together = [['club_id', 'ManagerID', 'Date']]
indexes = [
models.Index(fields=['Date', '-RenewalTotal']),
models.Index(fields=['Date', '-RenewalRevenue']),
]
def __str__(self):
return f"管事续费{self.ManagerID} {self.Date}"
# ==================== 11. 提现每日统计 ====================
class WithdrawalDailyStats(QModel):
"""每日提现统计表(原 TixianRiTongji按俱乐部+身份统计当天提现到账总额"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
Date = models.DateField(
db_index=True, db_column='riqi', verbose_name='日期')
WithdrawalType = models.IntegerField(
choices=[(1, '打手'), (2, '管事'), (3, '组长'),
(4, '考核官'), (5, '打手押金'), (6, '商家余额')],
db_column='leixing', verbose_name='用户类型')
total_amount = models.DecimalField(
max_digits=12, decimal_places=2, default=0.00, verbose_name='当天提现总额(元)')
class Meta:
db_table = 'tixian_ri_tongji'
verbose_name = '每日提现统计'
unique_together = [['club_id', 'Date', 'WithdrawalType']]
indexes = [
models.Index(fields=['club_id', 'Date', 'WithdrawalType']),
models.Index(fields=['Date', 'WithdrawalType']),
]
def __str__(self):
return f"{self.Date} - {self.get_WithdrawalType_display()} 提现总额: {self.total_amount}"
# ==================== 兼容别名(供逐步迁移) ====================
ShangjiaRiTongji = MerchantDailyStats
DashouRiTongji = PlayerDailyStats
ZuzhangRiTongji = LeaderDailyStats
GuanshiRiTongji = ManagerDailyStats
GuanshiXufeiRiTongji = ManagerRenewalDailyStats
TixianRiTongji = WithdrawalDailyStats

View File

@@ -32,7 +32,7 @@ class PopupPageSerializer(serializers.ModelSerializer):
class Meta:
model = PopupPage
fields = [
'id', 'page_key', 'name', 'description',
'id', 'club_id', 'page_key', 'name', 'description',
'is_active', 'ignore_user_mute',
'created_at', 'updated_at', 'popups'
]

View File

@@ -6,15 +6,15 @@ from .view import (GetRolePermissionView,
ModifyAdminUserView, KefuGetDashouListView, KefuGetDashouDetailView, KefuUpdateDashouView, KefuGetShangjiaListView,
KefuGetShangjiaDetailView, KefuUpdateShangjiaView, GetProductBaseDataView, GetProductListView, SaveProductOrderView,
AddProductView, DeleteProductView, GetProductDetailView, UpdateProductView, UpdateMemberView, GetMemberListView,
AddMemberView, GetGuanliListView, GetGuanliDetailView, UpdateGuanliView, GetWithdrawSettingsView,
UpdateWithdrawSettingsView, GetZuzhangListView, GetZuzhangDetailView, UpdateZuzhangView, GetProductTypeZoneView,
AddMemberView, GetGuanliListView, GetGuanliDetailView, PromoteGuanshiToZuzhangView, ChangeInviterView, UpdateGuanliView, GetWithdrawSettingsView,
UpdateWithdrawSettingsView, GetZuzhangListView, GetZuzhangDetailView, UpdateZuzhangView, GetOperationLogListView, GetProductTypeZoneView,
ModifyProductTypeZoneView, GetRateView, ModifyRateView, PopupNoticeListAPIView, PopupNoticeModifyAPIView,ShopListView,
ShopModifyView, ShopPublicTypeAndAuditView, ShopListForProductView, ShopProductModifyView,
ShopProductTypeMappingView, ProductListView, ProductModifyView, KefuChatPermissionsView,
FineApplyView, PunishDashouView,
FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView,
HqbkxxView,BkxgView,CaiwuView,CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView,
CwqtczhqView, KhpzhqView, ChzsgcView,KhgglView, ShgxgsjView, ZxkfghdsView)
CwqtczhqView, KhpzhqView, ChzsgcView,KhgglView, ShgxgsjView, KhjlglView, KhjlczView, ZxkfghdsView)
urlpatterns = [
path('gljshq', GetRolePermissionView.as_view(), name='获取就角色信息'),
@@ -46,7 +46,10 @@ urlpatterns = [
path('hthqgslb', GetGuanliListView.as_view(), name='后台获取管事列表'),
path('hthqgsxq', GetGuanliDetailView.as_view(), name='后台获取管事详情信息'),
path('htgssjzz', PromoteGuanshiToZuzhangView.as_view(), name='后台管事升级组长'),
path('htghyqr', ChangeInviterView.as_view(), name='后台更换邀请人'),
path('htxggsxx', UpdateGuanliView.as_view(), name='后台更新管事数据'),
path('hqczrz', GetOperationLogListView.as_view(), name='后台操作日志列表'),
path('hthqtxpz', GetWithdrawSettingsView.as_view(), name='后台获取提现配置'),
path('htxgtxsz', UpdateWithdrawSettingsView.as_view(), name='后台修改提现配置'),
path('hthqzzlb', GetZuzhangListView.as_view(), name='后台获取组长列表'),
@@ -100,6 +103,8 @@ urlpatterns = [
path('khggl', KhgglView.as_view(), name='获取考核管信息'),
path('shgxgsj', ShgxgsjView.as_view(), name='修改考核管数据'),
path('khjlgl', KhjlglView.as_view(), name='考核记录列表与统计'),
path('khjlcz', KhjlczView.as_view(), name='考核记录操作'),
path('zxkfghds', ZxkfghdsView.as_view(), name='链接换打手或重新下单'),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

27
check_prod_uid.py Normal file
View File

@@ -0,0 +1,27 @@
"""检查生产库中 UserUID 为空的用户"""
import pymysql
conn = pymysql.connect(
host='gvsds.com', port=50030,
user='root', password='sajksh.sdfGH3YUge.wjkd+',
database='xaio_cheng_xu', charset='utf8mb4'
)
cur = conn.cursor()
# UserUID 为 NULL 的用户数
cur.execute("SELECT COUNT(*) FROM user WHERE UserUID IS NULL OR UserUID = ''")
print(f'UserUID为空的用户数: {cur.fetchone()[0]}')
# 总用户数
cur.execute("SELECT COUNT(*) FROM user")
print(f'总用户数: {cur.fetchone()[0]}')
# 抽样 UserUID 为 NULL 的用户
cur.execute("SELECT UserUUID, UserName, OpenID, Phone FROM user WHERE UserUID IS NULL OR UserUID = '' LIMIT 5")
rows = cur.fetchall()
if rows:
print('\nUserUID为空的用户示例:')
for r in rows:
print(f' UserName={r[1]}, OpenID={r[2]}, Phone={r[3]}')
conn.close()

View File

@@ -0,0 +1,114 @@
"""
修复 repair_wechat_income 搞乱的收入200 显示成 600+)。
两种模式(先预览,再加 --yes
A. 默认:重扫本月真实微信支付,删掉脏数据后重建
python manage.py fix_income_month
python manage.py fix_income_month --yes
B. 若日志金额已对、只是日统计表脏了:
python manage.py fix_income_month --logs-only --yes
"""
from calendar import monthrange
from datetime import date
from django.core.management.base import BaseCommand
from django.db.models import Sum
from config.models import DailyIncomeStat, PlatformIncomeLog
from jituan.services.szjilu_accounting import (
align_daily_income_from_logs_only,
dedupe_platform_income_logs,
fix_income_period,
)
class Command(BaseCommand):
help = '修复本月收入统计错乱repair 重复记账)'
def add_arguments(self, parser):
parser.add_argument('--year', type=int, default=0, help='年,默认今年')
parser.add_argument('--month', type=int, default=0, help='月,默认本月')
parser.add_argument('--today-only', action='store_true', help='只修今天')
parser.add_argument('--logs-only', action='store_true', help='只按现有日志重写日统计,不重扫订单')
parser.add_argument('--yes', action='store_true', help='确认执行')
def handle(self, *args, **options):
today = date.today()
year = options['year'] or today.year
month = options['month'] or today.month
dry_run = not options['yes']
if options['today_only']:
start = end = today
label = f'今日 {today}'
else:
_, last = monthrange(year, month)
start = date(year, month, 1)
end = date(year, month, last)
label = f'{year}-{month:02d} 全月'
if options['logs_only']:
self._run_logs_only(dry_run, label)
return
self.stdout.write(self.style.WARNING(f'{label}】收入修复\n'))
if dry_run:
self.stdout.write('预览模式(未改库)\n')
r = fix_income_period(start, end, dry_run=dry_run)
self.stdout.write(f' 错乱日统计合计: {r["before_daily_amount"]:.2f} 元 / {r["before_daily_count"]}')
self.stdout.write(f' 错乱日志合计: {r["before_log_amount"]:.2f} 元 / {r["before_log_count"]}')
self.stdout.write(self.style.SUCCESS(
f' 重建后(严格): {r["after_amount"]:.2f} 元 / {r["after_count"]}'
))
if r['items']:
self.stdout.write(' 明细:')
for row in r['items']:
if len(row) == 5:
d, ref, cid, amt, kind = row
self.stdout.write(f' {d} {ref} {amt}元 club={cid} ({kind})')
else:
ref, cid, amt, kind = row
self.stdout.write(f' {ref} {amt}元 club={cid} ({kind})')
if not dry_run:
self._print_today_total(today)
else:
self.stdout.write(self.style.WARNING(
'\n重建后金额对 → python manage.py fix_income_month --yes\n'
'只修今天 → python manage.py fix_income_month --today-only --yes\n'
'日志已对、日统计脏 → python manage.py fix_income_month --logs-only --yes'
))
def _run_logs_only(self, dry_run, label):
try:
log_sum = PlatformIncomeLog.objects.aggregate(t=Sum('amount'))['t'] or 0
log_cnt = PlatformIncomeLog.objects.count()
daily_sum = DailyIncomeStat.objects.aggregate(t=Sum('total_amount'))['t'] or 0
except Exception as exc:
self.stderr.write(f'读取失败: {exc}')
return
self.stdout.write(f'{label}】仅按日志对齐日统计\n')
self.stdout.write(f' platform_income_log 合计: {float(log_sum):.2f} 元 / {log_cnt}')
self.stdout.write(f' daily_income_stat 现合计: {float(daily_sum):.2f}')
if dry_run:
self.stdout.write(self.style.WARNING('\n预览。确认后: python manage.py fix_income_month --logs-only --yes'))
return
n = align_daily_income_from_logs_only()
self.stdout.write(self.style.SUCCESS(f'已重写 daily_income_stat {n} 行,并重算 szjilu'))
self._print_today_total(date.today())
def _print_today_total(self, today):
agg = DailyIncomeStat.objects.filter(date=today).aggregate(
amt=Sum('total_amount'), cnt=Sum('total_count'),
)
self.stdout.write(self.style.SUCCESS(
f'\n数据库今日日统计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)}'
))
self.stdout.write('请刷新后台财务页核对。')

View File

@@ -0,0 +1,29 @@
"""只修正今日 daily_income_stat.total_count不动金额、不动其它天"""
from datetime import date
from django.core.management.base import BaseCommand
from jituan.services.szjilu_accounting import fix_income_stat_count_for_date
class Command(BaseCommand):
help = '修正今日收入笔数repair 会把 total_count 虚增,本命令只改笔数列)'
def add_arguments(self, parser):
parser.add_argument('--date', type=str, default='', help='YYYY-MM-DD默认今天')
parser.add_argument('--yes', action='store_true')
def handle(self, *args, **options):
stat_date = date.fromisoformat(options['date']) if options['date'] else date.today()
dry_run = not options['yes']
r = fix_income_stat_count_for_date(stat_date, dry_run=dry_run)
self.stdout.write(f'日期: {r["date"]}')
self.stdout.write(f' 表内笔数合计(错): {r["before_count"]}')
self.stdout.write(self.style.SUCCESS(f' 修正后笔数: {r["after_count"]}'))
if r['clubs']:
for cid, cnt in r['clubs'].items():
self.stdout.write(f' club={cid}: {cnt}')
if dry_run:
self.stdout.write(self.style.WARNING('\n确认: python manage.py fix_today_income_count --yes'))
else:
self.stdout.write(self.style.SUCCESS('\n已写入 daily_income_stat.total_count请刷新财务页。'))

View File

@@ -0,0 +1,70 @@
"""
【终极恢复】清空全部收支脏数据,按真实微信支付逐笔重建。
只执行这一条,不要再用 repair / undo / fix
预览python manage.py rebuild_income_stats
执行python manage.py rebuild_income_stats --yes
"""
from datetime import date
from django.core.management.base import BaseCommand
from django.db.models import Sum
from config.models import DailyIncomeStat
from jituan.services.szjilu_accounting import rebuild_income_stats_from_scratch
class Command(BaseCommand):
help = '清空收支脏数据,按真实微信支付逐笔重建(终极恢复)'
def add_arguments(self, parser):
parser.add_argument('--since-year', type=int, default=0, help='从哪一年扫,默认去年')
parser.add_argument('--until-year', type=int, default=0, help='到哪一年,默认今年')
parser.add_argument('--yes', action='store_true', help='确认执行')
def handle(self, *args, **options):
until_year = options['until_year'] or date.today().year
since_year = options['since_year'] or (until_year - 1)
dry_run = not options['yes']
if dry_run:
self.stdout.write(self.style.WARNING('【预览】未改数据库\n'))
else:
self.stdout.write(self.style.ERROR(
'【执行】删除全部收入日志和日统计,按真实支付重建...\n'
))
r = rebuild_income_stats_from_scratch(
since_year=since_year, until_year=until_year, dry_run=dry_run,
)
self.stdout.write(f'=== {since_year} ~ {until_year} ===')
self.stdout.write(f' 错乱前 daily 合计: {r["before_daily_total"]:.2f}')
self.stdout.write(f' 错乱前 log: {r["before_log_total"]:.2f} 元 / {r["before_log_count"]}')
self.stdout.write(self.style.SUCCESS(
f' 重建后合计: {r["after_total"]:.2f} 元 / {r["after_count"]}'
))
self.stdout.write(self.style.SUCCESS(
f' 重建后本月: {r["month_amount"]:.2f}'
))
self.stdout.write(self.style.SUCCESS(
f' 重建后今日: {r["today_amount"]:.2f} 元 / {r["today_count"]} 笔 ← 看这条'
))
if not dry_run:
self.stdout.write(self.style.SUCCESS(
f'\ndaily 表合计: {r.get("after_daily_total", 0):.2f}'
))
agg = DailyIncomeStat.objects.filter(date=date.today()).aggregate(
amt=Sum('total_amount'), cnt=Sum('total_count'),
)
self.stdout.write(
f'今日 daily 行合计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)}'
)
self.stdout.write(self.style.SUCCESS('\n完成。刷新后台财务页。'))
else:
self.stdout.write(self.style.WARNING(
f'\n今日 {r["today_amount"]:.2f} 元对的话:\n'
f' python manage.py rebuild_income_stats --yes'
))

View File

@@ -0,0 +1,104 @@
"""修复 config.0006 半路失败:去重 + 补唯一约束/索引,然后 fake 0006。"""
from django.core.management.base import BaseCommand
from django.db import connection
from django.db.models import Max
from config.models import PopupPage, Tupianpeizhi
def _table_indexes(table):
with connection.cursor() as cursor:
cursor.execute(f'SHOW INDEX FROM `{table}`')
rows = cursor.fetchall()
# Key_name, Column_name, Non_unique
by_key = {}
for row in rows:
key_name = row[2]
col = row[4]
non_unique = row[1]
by_key.setdefault(key_name, {'cols': [], 'unique': non_unique == 0})
by_key[key_name]['cols'].append(col)
return by_key
def _has_unique_on(table, columns):
cols = list(columns)
for info in _table_indexes(table).values():
if info['unique'] and info['cols'] == cols:
return True
return False
def _drop_index(table, key_name):
with connection.cursor() as cursor:
cursor.execute(f'ALTER TABLE `{table}` DROP INDEX `{key_name}`')
def _add_unique(table, name, columns):
cols = ', '.join(f'`{c}`' for c in columns)
with connection.cursor() as cursor:
cursor.execute(f'ALTER TABLE `{table}` ADD UNIQUE `{name}` ({cols})')
def _add_index(table, name, columns):
cols = ', '.join(f'`{c}`' for c in columns)
with connection.cursor() as cursor:
cursor.execute(f'CREATE INDEX `{name}` ON `{table}` ({cols})')
class Command(BaseCommand):
help = 'config.0006 半路失败时:清重复数据、补约束,再执行 migrate config 0006 --fake'
def handle(self, *args, **options):
self.stdout.write('1) 清理重复数据…')
for row in Tupianpeizhi.objects.values('club_id', 'ImageType').annotate(max_id=Max('id')):
n = Tupianpeizhi.objects.filter(
club_id=row['club_id'],
ImageType=row['ImageType'],
).exclude(id=row['max_id']).delete()[0]
if n:
self.stdout.write(f' tupianpeizhi 删除 {n} 条重复')
for row in PopupPage.objects.values('club_id', 'page_key').annotate(max_id=Max('id')):
n = PopupPage.objects.filter(
club_id=row['club_id'],
page_key=row['page_key'],
).exclude(id=row['max_id']).delete()[0]
if n:
self.stdout.write(f' popup_page 删除 {n} 条重复')
self.stdout.write('2) 检查并补约束/索引…')
# popup_page去掉 page_key 单独唯一,改为 (club_id, page_key)
popup_indexes = _table_indexes('popup_page')
for key_name, info in popup_indexes.items():
if info['unique'] and info['cols'] == ['page_key']:
self.stdout.write(f' 删除 popup_page 旧唯一索引 {key_name}')
_drop_index('popup_page', key_name)
if not _has_unique_on('popup_page', ['club_id', 'page_key']):
self.stdout.write(' 添加 popup_page UNIQUE (club_id, page_key)')
_add_unique('popup_page', 'popup_page_club_page_key_uniq', ['club_id', 'page_key'])
else:
self.stdout.write(' popup_page UNIQUE (club_id, page_key) 已存在')
if not _has_unique_on('tupianpeizhi', ['club_id', 'ImageType']):
self.stdout.write(' 添加 tupianpeizhi UNIQUE (club_id, ImageType)')
_add_unique('tupianpeizhi', 'tupianpeizhi_club_id_ImageType_uniq', ['club_id', 'ImageType'])
else:
self.stdout.write(' tupianpeizhi UNIQUE (club_id, ImageType) 已存在')
lunbo_indexes = _table_indexes('lunbo')
if 'lunbo_club_page_type_idx' not in lunbo_indexes:
self.stdout.write(' 添加 lunbo 索引 lunbo_club_page_type_idx')
_add_index('lunbo', 'lunbo_club_page_type_idx', ['club_id', 'page_key', 'ImageType'])
else:
self.stdout.write(' lunbo 索引 lunbo_club_page_type_idx 已存在')
self.stdout.write(self.style.SUCCESS(
'\n完成。请执行:\n'
' python manage.py migrate config 0006 --fake\n'
' python manage.py migrate config\n'
' python manage.py migrate\n'
))

View File

@@ -0,0 +1,92 @@
"""
恢复被 repair_wechat_income 搞乱的收入统计。
repair 会对已入账订单重复加钱200 变 900
本命令:删当日错误日志 → 按真实微信支付重建 → 扣回 szjilu 多记部分。
用法:
1. 先预览python manage.py recover_wechat_income_stats
2. 看「重建后」金额是否对(应接近真实收入,如 200 左右)
3. 确认后python manage.py recover_wechat_income_stats --yes
4. 若多日错乱python manage.py recover_wechat_income_stats --days 7 --yes
5. 最后全量对齐日统计表python manage.py recover_wechat_income_stats --yes --rebuild-all-daily
"""
from datetime import date, timedelta
from django.core.management.base import BaseCommand
from django.db.models import Sum
from config.models import DailyIncomeStat
from jituan.services.szjilu_accounting import (
rebuild_all_daily_income_from_logs,
recover_income_stats_for_date,
)
class Command(BaseCommand):
help = '恢复 repair_wechat_income 重复记账(先预览,--yes 执行)'
def add_arguments(self, parser):
parser.add_argument('--date', type=str, default='', help='日期 YYYY-MM-DD默认今天')
parser.add_argument('--days', type=int, default=1, help='向前恢复几天')
parser.add_argument('--yes', action='store_true', help='确认执行')
parser.add_argument(
'--rebuild-all-daily',
action='store_true',
help='恢复后按 platform_income_log 全量重写 daily_income_stat纠正累计总收入',
)
def handle(self, *args, **options):
if options['date']:
end_day = date.fromisoformat(options['date'])
else:
end_day = date.today()
days = max(1, int(options['days'] or 1))
start_day = end_day - timedelta(days=days - 1)
dry_run = not options['yes']
if dry_run:
self.stdout.write(self.style.WARNING(
'【预览】未改数据库。下面「重建后」应是正确金额。\n'
))
else:
self.stdout.write(self.style.WARNING('【执行】正在恢复...\n'))
for i in range(days):
d = start_day + timedelta(days=i)
r = recover_income_stats_for_date(d, dry_run=dry_run)
self.stdout.write(f'=== {r["date"]} ===')
self.stdout.write(
f' 错乱前日统计: {r["before_daily_amount"]:.2f} 元 / {r["before_daily_count"]}'
)
self.stdout.write(
f' 当日幂等日志: {r["before_log_amount"]:.2f} 元 / {r["before_log_count"]}'
)
self.stdout.write(self.style.SUCCESS(
f' 重建后(正确值): {r["after_amount"]:.2f} 元 / {r["after_count"]}'
))
if r['items']:
self.stdout.write(' 明细:')
for ref, cid, amt, kind in r['items']:
self.stdout.write(f' {ref} {amt}元 club={cid} ({kind})')
if not dry_run and options['rebuild_all_daily']:
n = rebuild_all_daily_income_from_logs()
self.stdout.write(self.style.SUCCESS(f'已按日志全量重写 daily_income_stat: {n}'))
if not dry_run:
agg = DailyIncomeStat.objects.filter(date=end_day).aggregate(
amt=Sum('total_amount'),
cnt=Sum('total_count'),
)
self.stdout.write(self.style.SUCCESS(
f'\n数据库今日日统计现为: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)}'
))
self.stdout.write(self.style.SUCCESS('恢复完成,请刷新后台财务页。'))
else:
self.stdout.write(self.style.WARNING(
'\n预览完成。重建后金额对的话执行:\n'
' python manage.py recover_wechat_income_stats --yes\n'
'累计总收入仍不对再加:\n'
' python manage.py recover_wechat_income_stats --yes --rebuild-all-daily'
))

View File

@@ -0,0 +1,15 @@
"""【已废弃】请用 sync_daily_income_from_log本命令曾导致重复记账。"""
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = '已废弃:勿使用。请改用 python manage.py sync_daily_income_from_log --reset-day'
def handle(self, *args, **options):
self.stderr.write(self.style.ERROR(
'repair_wechat_income 已禁用(会对已入账订单重复加钱)。\n'
'纠正今日虚高请执行:\n'
' python manage.py sync_daily_income_from_log --reset-day\n'
'可选同时重算累计:\n'
' python manage.py sync_daily_income_from_log --reset-day --sync-szjilu'
))

View File

@@ -0,0 +1,48 @@
"""只恢复今日收入(修复后台四个板子显示 0"""
from datetime import date
from django.core.management.base import BaseCommand
from django.db.models import Sum
from config.models import DailyIncomeStat
from jituan.services.szjilu_accounting import repopulate_today_income_stats
class Command(BaseCommand):
help = '重建今日 daily_income_stat修复财务页今日收入为 0'
def add_arguments(self, parser):
parser.add_argument('--date', type=str, default='', help='日期 YYYY-MM-DD默认今天')
parser.add_argument('--yes', action='store_true', help='确认执行')
def handle(self, *args, **options):
stat_date = date.fromisoformat(options['date']) if options['date'] else date.today()
dry_run = not options['yes']
if dry_run:
self.stdout.write(self.style.WARNING('【预览】\n'))
else:
self.stdout.write(self.style.WARNING('【执行】重建今日收入...\n'))
r = repopulate_today_income_stats(stat_date, dry_run=dry_run)
self.stdout.write(f'日期: {r["date"]}')
self.stdout.write(f' 当前: {r["before_amount"]:.2f} 元 / {r["before_count"]}')
self.stdout.write(self.style.SUCCESS(
f' 重建后: {r["after_amount"]:.2f} 元 / {r["after_count"]}'
))
if r['items']:
for ref, cid, amt, kind in r['items']:
self.stdout.write(f' {ref} {amt}元 club={cid} ({kind})')
if not dry_run:
agg = DailyIncomeStat.objects.filter(date=stat_date).aggregate(
amt=Sum('total_amount'), cnt=Sum('total_count'),
)
self.stdout.write(self.style.SUCCESS(
f'\n完成。今日日统计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)}'
))
self.stdout.write('请刷新后台财务页。')
else:
self.stdout.write(self.style.WARNING(
'\n确认后: python manage.py repopulate_today_income --yes'
))

View File

@@ -0,0 +1,47 @@
"""
从订单/充值表恢复 daily_income_stat不删订单只重算统计表
repair/rebuild 搞乱后用这个,不要用 repair_wechat_income。
预览python manage.py restore_daily_income_from_orders
执行python manage.py restore_daily_income_from_orders --yes
"""
from datetime import date
from django.core.management.base import BaseCommand
from jituan.services.szjilu_accounting import restore_daily_income_from_orders
class Command(BaseCommand):
help = '从订单/充值表恢复日收入统计(订单不动)'
def add_arguments(self, parser):
parser.add_argument('--since-year', type=int, default=0)
parser.add_argument('--until-year', type=int, default=0)
parser.add_argument('--yes', action='store_true')
def handle(self, *args, **options):
until_year = options['until_year'] or date.today().year
since_year = options['since_year'] or (until_year - 1)
dry_run = not options['yes']
if dry_run:
self.stdout.write(self.style.WARNING('【预览】订单数据不会动\n'))
else:
self.stdout.write(self.style.WARNING('【执行】按订单/充值重算日统计...\n'))
r = restore_daily_income_from_orders(since_year, until_year, dry_run=dry_run)
self.stdout.write(f'范围: {r["since_year"]} ~ {r["until_year"]}')
self.stdout.write(f' 支付笔数(订单表): {r["payment_rows"]}')
self.stdout.write(f' 日统计行数: {r["daily_rows"]}')
self.stdout.write(f' 当前 daily 合计: {r["before_daily_total"]:.2f}')
self.stdout.write(self.style.SUCCESS(
f' 恢复后 daily 合计: {r["after_daily_total"]:.2f}'
))
if dry_run:
self.stdout.write(self.style.WARNING(
'\n确认: python manage.py restore_daily_income_from_orders --yes'
))
else:
self.stdout.write(self.style.SUCCESS('\n完成。推 kefu 后刷新财务页。'))

View File

@@ -0,0 +1,68 @@
"""
恢复 daily_income_stat每日收入统计表全表数据。
repair_wechat_income 对该表的破坏:
- 把扫描到的每笔订单/充值重复写入 daily_income_stat
- 金额记到「执行命令那天」,不是真实支付日
- 含退款单(Status=5)也被当成收入加进去
本命令按 platform_income_log + 无日志的已支付单,重建整张 daily_income_stat。
不碰 daily_payout_stat不碰 szjilu不删订单。
预览python manage.py restore_finance_stats
执行python manage.py restore_finance_stats --yes
"""
from datetime import date
from django.core.management.base import BaseCommand
from django.db.models import Sum
from config.models import DailyIncomeStat, DailyPayoutStat
from jituan.services.szjilu_accounting import rebuild_daily_income_stat_table
class Command(BaseCommand):
help = '恢复 daily_income_stat 全表(财务四个板子读这张表)'
def add_arguments(self, parser):
parser.add_argument('--yes', action='store_true', help='确认写入数据库')
def handle(self, *args, **options):
dry_run = not options['yes']
if dry_run:
self.stdout.write(self.style.WARNING('【预览】未改数据库\n'))
else:
self.stdout.write(self.style.WARNING('【执行】重建 daily_income_stat...\n'))
r = rebuild_daily_income_stat_table(dry_run=dry_run)
self.stdout.write(f' 支付笔数(去重): {r["payment_rows"]}')
self.stdout.write(f' 其中日志: {r["from_log"]}')
self.stdout.write(f' 补漏订单: {r["gap_orders"]}')
self.stdout.write(f' 补漏充值: {r["gap_cz"]}')
self.stdout.write(f' 日统计行数: {r["daily_rows"]}')
self.stdout.write(f' 恢复前 daily 合计: {r["before_daily_total"]:.2f}')
self.stdout.write(self.style.SUCCESS(
f' 恢复后 daily 合计: {r["after_daily_total"]:.2f}'
))
self.stdout.write(f' 其中今日收入: {r["today_amount"]:.2f} 元 / {r["today_count"]}')
payout_today = DailyPayoutStat.objects.filter(date=date.today()).aggregate(
t=Sum('total_amount'),
)['t'] or 0
self.stdout.write(
f' (未动)今日支出表: {float(payout_today):.2f}'
)
if dry_run:
self.stdout.write(self.style.WARNING(
'\n预览数字合理后执行:\n'
' python manage.py restore_finance_stats --yes\n'
'然后 push 代码并刷新财务页。'
))
else:
check = DailyIncomeStat.objects.aggregate(t=Sum('total_amount'))['t'] or 0
self.stdout.write(self.style.SUCCESS(
f'\n完成。数据库 daily_income_stat 合计: {float(check):.2f}'
))

View File

@@ -0,0 +1,68 @@
"""
用 platform_income_log 重写 daily_income_stat纠正重复记账后的虚高数字。
权威数据源是 platform_income_log每笔支付一条biz_ref 唯一),不是扫订单重算。
"""
from datetime import date, timedelta
from django.core.management.base import BaseCommand
from jituan.services.szjilu_accounting import (
sync_daily_income_stat_for_date,
sync_szjilu_income_from_logs,
)
class Command(BaseCommand):
help = (
'按 platform_income_log 同步 daily_income_stat纠正今日收入虚高/笔数不准)。'
'不要用 repair_wechat_income那个会重复加钱。'
)
def add_arguments(self, parser):
parser.add_argument(
'--date',
type=str,
default='',
help='同步日期 YYYY-MM-DD默认今天',
)
parser.add_argument(
'--days',
type=int,
default=1,
help='从指定日期起向前同步几天,默认 1',
)
parser.add_argument(
'--reset-day',
action='store_true',
help='同步前先删除该日全部日统计行,再按日志重建(纠正重复记账必选)',
)
parser.add_argument(
'--sync-szjilu',
action='store_true',
help='同时按日志全量重算 szjilu 累计收入/今日流水',
)
def handle(self, *args, **options):
if options['date']:
end_day = date.fromisoformat(options['date'])
else:
end_day = date.today()
days = max(1, int(options['days'] or 1))
start_day = end_day - timedelta(days=days - 1)
reset_day = bool(options['reset_day'])
total = 0
for i in range(days):
d = start_day + timedelta(days=i)
n = sync_daily_income_stat_for_date(d, reset_day=reset_day)
total += n
self.stdout.write(f'{d}: 已同步 {n} 个俱乐部日统计行')
if options['sync_szjilu']:
sz = sync_szjilu_income_from_logs()
self.stdout.write(f'szjilu: 已重算 {sz} 个俱乐部累计收入')
self.stdout.write(self.style.SUCCESS(
f'完成。今日收入/笔数现与 platform_income_log 一致。'
f'若仍有漏记支付,请确认 migrate config 后等新支付自动入账。'
))

View File

@@ -0,0 +1,133 @@
"""
【反向撤销】repair_wechat_income 造成的重复记账。
你当时执行的指令是:
python manage.py repair_wechat_income
或:
python manage.py repair_wechat_income --date 2026-06-24 --days 7
repair 逻辑:按「当日创建的已支付订单/充值单」逐条 +金额、+笔数、写 platform_income_log、加 szjilu。
本命令用完全相同扫描列表,逐条反向 -金额、-笔数、删日志、减 szjilu。
用法:
1. 预览(不改库):
python manage.py undo_repair_wechat_income
2. 确认后撤销(加多少减多少):
python manage.py undo_repair_wechat_income --yes
3. 若当时带了参数,必须一致:
python manage.py undo_repair_wechat_income --date 2026-06-01 --days 30 --run-date 2026-06-24 --yes
--run-date = 你执行 repair 那天,默认今天)
4. 全部清零(恢复到 0
python manage.py undo_repair_wechat_income --zero-all --yes
"""
from datetime import date, timedelta
from django.core.management.base import BaseCommand
from django.db.models import Sum
from config.models import DailyIncomeStat
from jituan.services.szjilu_accounting import (
undo_repair_wechat_income_period,
zero_all_income_statistics,
)
class Command(BaseCommand):
help = '反向撤销 repair_wechat_income 造成的收支重复记账'
def add_arguments(self, parser):
parser.add_argument(
'--date', type=str, default='',
help='与 repair 相同的 --date扫描区间结束日默认今天',
)
parser.add_argument(
'--days', type=int, default=1,
help='与 repair 相同的 --days默认 1',
)
parser.add_argument(
'--run-date', type=str, default='',
help='你执行 repair 的日期(金额被记到哪天的 daily默认今天',
)
parser.add_argument('--yes', action='store_true', help='确认执行')
parser.add_argument(
'--zero-all', action='store_true',
help='收支统计全部清零日收入、日志、szjilu 收入流水归零)',
)
def handle(self, *args, **options):
if options['zero_all']:
self._zero_all(not options['yes'])
return
if options['date']:
end_day = date.fromisoformat(options['date'])
else:
end_day = date.today()
days = max(1, int(options['days'] or 1))
start_day = end_day - timedelta(days=days - 1)
if options['run_date']:
inflation_date = date.fromisoformat(options['run_date'])
else:
inflation_date = date.today()
dry_run = not options['yes']
self.stdout.write(self.style.WARNING(
'========== 反向撤销 repair_wechat_income ==========\n'
f'原 repair 扫描区间: {start_day} ~ {end_day} --days {days}\n'
f'金额被记到日统计日期: {inflation_date} --run-date\n'
))
if dry_run:
self.stdout.write('【预览】未改数据库\n')
else:
self.stdout.write('【执行】正在反向扣减...\n')
r = undo_repair_wechat_income_period(
start_day, end_day, inflation_date, dry_run=dry_run,
)
self.stdout.write(f' 将撤销条数: {r["undo_count"]}')
self.stdout.write(f' 将扣减金额: {r["undo_amount"]:.2f}')
self.stdout.write(
f' 执行前 {inflation_date} 日统计: {r["before_daily_on_inflation_date"]:.2f}'
)
if r['items']:
self.stdout.write(' 明细:')
for ref, cid, amt, kind in r['items']:
self.stdout.write(f' - {ref} {amt}元 club={cid} ({kind})')
if not dry_run:
self.stdout.write(self.style.SUCCESS(
f'\n执行后 {inflation_date} 日统计: {r.get("after_daily_on_inflation_date", 0):.2f}'
))
self.stdout.write(self.style.SUCCESS('反向撤销完成,请刷新后台财务页。'))
else:
self.stdout.write(self.style.WARNING(
'\n预览完成。确认后执行(参数与 repair 保持一致):\n'
f' python manage.py undo_repair_wechat_income --date {end_day} --days {days} '
f'--run-date {inflation_date} --yes\n'
'全部清零:\n'
' python manage.py undo_repair_wechat_income --zero-all --yes'
))
def _zero_all(self, dry_run):
agg = DailyIncomeStat.objects.aggregate(amt=Sum('total_amount'), cnt=Sum('total_count'))
self.stdout.write(self.style.ERROR(
'========== 收支统计全部清零 ==========\n'
f'当前 daily_income_stat 合计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)}\n'
'将删除: platform_income_log 全部、daily_income_stat 全部\n'
'将归零: szjilu.TotalIncome / TotalFlow / DailyFlow\n'
))
if dry_run:
self.stdout.write(self.style.WARNING(
'预览。确认清零:\n'
' python manage.py undo_repair_wechat_income --zero-all --yes'
))
return
zero_all_income_statistics()
self.stdout.write(self.style.SUCCESS('已全部清零。请刷新后台。'))

View File

@@ -1,92 +1,296 @@
# Generated by Django 5.2.10 on 2026-01-14 19:59
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Gonggao',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('leixing', models.IntegerField(blank=True, null=True, verbose_name='公告类型')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
('jieshao', models.TextField(blank=True, null=True, verbose_name='公告介绍')),
],
options={
'verbose_name': '公告',
'verbose_name_plural': '公告',
'db_table': 'gonggao',
},
),
migrations.CreateModel(
name='Lunbo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tupian_url', models.CharField(blank=True, max_length=500, null=True, verbose_name='轮播图片URL')),
('leixing', models.IntegerField(blank=True, null=True, verbose_name='图片类型:1轮播2个人中心背景')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
],
options={
'verbose_name': '轮播',
'verbose_name_plural': '轮播',
'db_table': 'lunbo',
},
),
migrations.CreateModel(
name='Qunpeizhi',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('peizhiid', models.IntegerField(blank=True, null=True, verbose_name='群类型:1.打手 2.管事')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
('neirong', models.CharField(blank=True, max_length=200, null=True, verbose_name='群介绍')),
('qunid', models.CharField(blank=True, max_length=100, null=True, verbose_name='群id')),
('jieshao', models.CharField(blank=True, max_length=200, null=True, verbose_name='介绍')),
],
options={
'db_table': 'qunpeizhi',
},
),
migrations.CreateModel(
name='Szjilu',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='记录ID')),
('zongsy', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总收益')),
('zongls', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总流水')),
('zongzc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总支出')),
('jrzc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日支出')),
('jrls', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日流水')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
],
options={
'verbose_name': '收支记录',
'verbose_name_plural': '收支记录',
'db_table': 'szjilu',
},
),
migrations.CreateModel(
name='Tupianpeizhi',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tupian', models.CharField(blank=True, max_length=500, null=True, verbose_name='配置图片URL')),
('leixing', models.PositiveIntegerField(blank=True, null=True, verbose_name='提现类型1:打手规则图片2默认头像图片')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
],
options={
'verbose_name': '配置图片',
'verbose_name_plural': '配置图片',
'db_table': 'tupianpeizhi',
},
),
]
# Generated by Django 4.2.27 on 2026-06-17 21:18
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Gonggao',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('leixing', models.IntegerField(blank=True, null=True, verbose_name='公告类型')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
('jieshao', models.TextField(blank=True, null=True, verbose_name='公告介绍')),
],
options={
'verbose_name': '公告',
'verbose_name_plural': '公告',
'db_table': 'gonggao',
},
),
migrations.CreateModel(
name='Lunbo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tupian_url', models.CharField(blank=True, max_length=500, null=True, verbose_name='轮播图片URL')),
('leixing', models.IntegerField(blank=True, null=True, verbose_name='图片类型:1轮播2个人中心背景')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
],
options={
'verbose_name': '轮播',
'verbose_name_plural': '轮播',
'db_table': 'lunbo',
},
),
migrations.CreateModel(
name='PopupConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('popup_id', models.CharField(max_length=50, verbose_name='弹窗唯一ID')),
('title', models.CharField(blank=True, max_length=200, verbose_name='标题(后台用)')),
('description', models.TextField(blank=True, verbose_name='描述(后台用)')),
('strategy_type', models.CharField(choices=[('once', '仅一次'), ('daily', '每日'), ('weekly', '每周'), ('monthly', '每月'), ('yearly', '每年'), ('always', '始终'), ('count', '周期内次数限制')], default='once', max_length=20)),
('max_count', models.PositiveIntegerField(default=1, verbose_name='周期内最大展示次数')),
('reset_interval', models.CharField(blank=True, choices=[('day', ''), ('week', ''), ('month', ''), ('year', '')], max_length=20, verbose_name='重置周期count类型时使用')),
('duration', models.PositiveIntegerField(default=0, verbose_name='强制阅读秒数0=无强制)')),
('force_even_muted', models.BooleanField(default=False, verbose_name='强制弹出(忽略用户今日屏蔽)')),
('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')),
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
('start_time', models.DateTimeField(blank=True, null=True, verbose_name='生效开始时间')),
('end_time', models.DateTimeField(blank=True, null=True, verbose_name='生效结束时间')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': '弹窗配置',
'verbose_name_plural': '弹窗配置',
'db_table': 'popup_config',
'ordering': ['sort_order', 'id'],
},
),
migrations.CreateModel(
name='PopupPage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('page_key', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='页面标识')),
('name', models.CharField(max_length=100, verbose_name='页面名称')),
('description', models.TextField(blank=True, verbose_name='描述')),
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('ignore_user_mute', models.BooleanField(default=False, verbose_name='忽略用户今日屏蔽')),
],
options={
'verbose_name': '弹窗页面',
'verbose_name_plural': '弹窗页面',
'db_table': 'popup_page',
},
),
migrations.CreateModel(
name='Qunpeizhi',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('peizhiid', models.IntegerField(blank=True, null=True, verbose_name='群类型:1.打手 2.管事')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
('neirong', models.CharField(blank=True, max_length=200, null=True, verbose_name='群介绍')),
('qunid', models.CharField(blank=True, max_length=100, null=True, verbose_name='群id')),
('jieshao', models.CharField(blank=True, max_length=200, null=True, verbose_name='介绍')),
],
options={
'db_table': 'qunpeizhi',
},
),
migrations.CreateModel(
name='Szjilu',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='记录ID')),
('zongsy', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总收益')),
('zongls', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总流水')),
('zongzc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总支出')),
('jrzc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日支出')),
('jrls', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日流水')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
],
options={
'verbose_name': '收支记录',
'verbose_name_plural': '收支记录',
'db_table': 'szjilu',
},
),
migrations.CreateModel(
name='TixianQuotaDefault',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('leixing', models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], unique=True, verbose_name='用户类型')),
('default_quota', models.DecimalField(decimal_places=2, default=20.0, max_digits=10, verbose_name='默认每日提现限额(元)')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '默认提现限额',
'verbose_name_plural': '默认提现限额',
'db_table': 'tixian_quota_default',
},
),
migrations.CreateModel(
name='Tupianpeizhi',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tupian', models.CharField(blank=True, max_length=500, null=True, verbose_name='配置图片URL')),
('leixing', models.PositiveIntegerField(blank=True, null=True, verbose_name='提现类型1:打手规则图片2默认头像图片')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
('fangwen_cishu', models.PositiveIntegerField(default=0, verbose_name='访问次数')),
],
options={
'verbose_name': '配置图片',
'verbose_name_plural': '配置图片',
'db_table': 'tupianpeizhi',
},
),
migrations.CreateModel(
name='WithdrawConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('mode', models.IntegerField(choices=[(1, '自动提现'), (2, '手动提现')], default=2, verbose_name='提现模式')),
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '提现模式配置',
'verbose_name_plural': '提现模式配置',
'db_table': 'withdraw_config',
},
),
migrations.CreateModel(
name='ShangjiaMoban',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='模板ID')),
('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')),
('moban_jieshao', models.CharField(max_length=500, verbose_name='模板介绍')),
('jiage', models.DecimalField(decimal_places=2, help_text='单位:元', max_digits=10, verbose_name='模板价格')),
('fabu_shuliang', models.PositiveIntegerField(default=0, verbose_name='发布数量')),
('yongjin_qiangdan', models.BooleanField(default=False, verbose_name='是否开启佣金抢单')),
('yongjin_jiage', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='佣金价格')),
('chenghao_id', models.IntegerField(blank=True, null=True, verbose_name='关联称号ID')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '商家订单模板',
'verbose_name_plural': '商家订单模板',
'db_table': 'shangjia_moban',
'indexes': [models.Index(fields=['yonghu_id', 'leixing_id'], name='shangjia_mo_yonghu__bb287d_idx'), models.Index(fields=['yonghu_id', 'leixing_id', 'id'], name='shangjia_mo_yonghu__4240d0_idx')],
'unique_together': {('yonghu_id', 'leixing_id', 'moban_jieshao')},
},
),
migrations.CreateModel(
name='ShangjiaLianjie',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='链接ID')),
('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')),
('moban_id', models.PositiveIntegerField(db_index=True, verbose_name='模板ID')),
('lianjie', models.CharField(max_length=500, unique=True, verbose_name='链接地址')),
('dingdan_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='订单ID')),
('is_used', models.BooleanField(db_index=True, default=False, verbose_name='是否已使用')),
('shiyongzhe_ip', models.CharField(blank=True, max_length=50, null=True, verbose_name='使用者IP')),
('youxiao_shijian', models.DateTimeField(blank=True, null=True, verbose_name='有效截止时间')),
('lianjie_token', models.CharField(blank=True, db_index=True, max_length=32, null=True, unique=True, verbose_name='链接Token')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('previous_lianjie_id', models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='上一个链接ID')),
('previous_dingdan_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='上一个订单ID')),
],
options={
'verbose_name': '商家链接',
'verbose_name_plural': '商家链接',
'db_table': 'shangjia_lianjie',
'indexes': [models.Index(fields=['yonghu_id', 'moban_id', 'leixing_id'], name='shangjia_li_yonghu__9f2b25_idx'), models.Index(fields=['lianjie_token'], name='shangjia_li_lianjie_99014c_idx'), models.Index(fields=['dingdan_id'], name='shangjia_li_dingdan_95b682_idx')],
},
),
migrations.CreateModel(
name='PopupImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image_url', models.CharField(max_length=500, verbose_name='图片相对路径')),
('text', models.TextField(blank=True, verbose_name='图片上方文字')),
('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')),
('created_at', models.DateTimeField(auto_now_add=True)),
('popup_config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='config.popupconfig')),
],
options={
'verbose_name': '弹窗图片',
'verbose_name_plural': '弹窗图片',
'db_table': 'popup_image',
'ordering': ['sort_order', 'id'],
},
),
migrations.AddField(
model_name='popupconfig',
name='page',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='popups', to='config.popuppage'),
),
migrations.CreateModel(
name='DailyPayoutStat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')),
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='出款总额')),
('total_count', models.PositiveIntegerField(default=0, verbose_name='出款总笔数')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '每日出款统计',
'verbose_name_plural': '每日出款统计',
'db_table': 'daily_payout_stat',
'indexes': [models.Index(fields=['date'], name='daily_payou_date_b534ec_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_payou_year_6376ca_idx')],
},
),
migrations.CreateModel(
name='DailyIncomeStat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')),
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='总收入金额')),
('total_count', models.PositiveIntegerField(default=0, verbose_name='总收入笔数')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '每日收入统计',
'verbose_name_plural': '每日收入统计',
'db_table': 'daily_income_stat',
'indexes': [models.Index(fields=['date'], name='daily_incom_date_75e6df_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_incom_year_d85d10_idx')],
},
),
migrations.CreateModel(
name='AccountPermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')),
('permission_code', models.IntegerField(verbose_name='权限代码')),
('is_enabled', models.BooleanField(default=False, verbose_name='是否开启')),
],
options={
'verbose_name': '账号权限',
'verbose_name_plural': '账号权限',
'db_table': 'account_permission',
'unique_together': {('account_id', 'permission_code')},
},
),
migrations.AlterUniqueTogether(
name='popupconfig',
unique_together={('page', 'popup_id')},
),
]

View File

@@ -0,0 +1,64 @@
# Generated manually to rename pinyin fields to PascalCase
# db_column is preserved, so these are no-ops at the database level
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('config', '0001_initial'),
]
operations = [
# Lunbo
migrations.RenameField(model_name='lunbo', old_name='tupian_url', new_name='ImageURL'),
migrations.RenameField(model_name='lunbo', old_name='leixing', new_name='ImageType'),
# Gonggao
migrations.RenameField(model_name='gonggao', old_name='leixing', new_name='NoticeType'),
migrations.RenameField(model_name='gonggao', old_name='jieshao', new_name='Content'),
# Qunpeizhi
migrations.RenameField(model_name='qunpeizhi', old_name='peizhiid', new_name='GroupType'),
migrations.RenameField(model_name='qunpeizhi', old_name='neirong', new_name='GroupContent'),
migrations.RenameField(model_name='qunpeizhi', old_name='qunid', new_name='GroupID'),
migrations.RenameField(model_name='qunpeizhi', old_name='jieshao', new_name='Description'),
# Tupianpeizhi
migrations.RenameField(model_name='tupianpeizhi', old_name='tupian', new_name='ImageURL'),
migrations.RenameField(model_name='tupianpeizhi', old_name='leixing', new_name='ImageType'),
migrations.RenameField(model_name='tupianpeizhi', old_name='fangwen_cishu', new_name='AccessCount'),
# Szjilu
migrations.RenameField(model_name='szjilu', old_name='zongsy', new_name='TotalIncome'),
migrations.RenameField(model_name='szjilu', old_name='zongls', new_name='TotalFlow'),
migrations.RenameField(model_name='szjilu', old_name='zongzc', new_name='TotalExpense'),
migrations.RenameField(model_name='szjilu', old_name='jrzc', new_name='DailyExpense'),
migrations.RenameField(model_name='szjilu', old_name='jrls', new_name='DailyFlow'),
# ShangjiaMoban
migrations.RenameField(model_name='shangjiamoban', old_name='yonghu_id', new_name='UserID'),
migrations.RenameField(model_name='shangjiamoban', old_name='leixing_id', new_name='ProductTypeID'),
migrations.RenameField(model_name='shangjiamoban', old_name='moban_jieshao', new_name='TemplateDesc'),
migrations.RenameField(model_name='shangjiamoban', old_name='jiage', new_name='Price'),
migrations.RenameField(model_name='shangjiamoban', old_name='fabu_shuliang', new_name='PublishedCount'),
migrations.RenameField(model_name='shangjiamoban', old_name='yongjin_qiangdan', new_name='CommissionGrabEnabled'),
migrations.RenameField(model_name='shangjiamoban', old_name='yongjin_jiage', new_name='CommissionPrice'),
migrations.RenameField(model_name='shangjiamoban', old_name='chenghao_id', new_name='TitleID'),
# ShangjiaLianjie
migrations.RenameField(model_name='shangjialianjie', old_name='yonghu_id', new_name='UserID'),
migrations.RenameField(model_name='shangjialianjie', old_name='leixing_id', new_name='ProductTypeID'),
migrations.RenameField(model_name='shangjialianjie', old_name='moban_id', new_name='TemplateID'),
migrations.RenameField(model_name='shangjialianjie', old_name='lianjie', new_name='LinkURL'),
migrations.RenameField(model_name='shangjialianjie', old_name='dingdan_id', new_name='OrderID'),
migrations.RenameField(model_name='shangjialianjie', old_name='shiyongzhe_ip', new_name='UserIP'),
migrations.RenameField(model_name='shangjialianjie', old_name='youxiao_shijian', new_name='ExpireTime'),
migrations.RenameField(model_name='shangjialianjie', old_name='lianjie_token', new_name='LinkToken'),
migrations.RenameField(model_name='shangjialianjie', old_name='previous_lianjie_id', new_name='PreviousLinkID'),
migrations.RenameField(model_name='shangjialianjie', old_name='previous_dingdan_id', new_name='PreviousOrderID'),
# TixianQuotaDefault
migrations.RenameField(model_name='tixianquotadefault', old_name='leixing', new_name='UserType'),
]

View File

@@ -1,56 +0,0 @@
# Generated by Django 4.2.27 on 2026-01-27 12:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ShangjiaMoban',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='模板ID')),
('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')),
('moban_jieshao', models.CharField(max_length=500, verbose_name='模板介绍')),
('jiage', models.DecimalField(decimal_places=2, help_text='单位:元', max_digits=10, verbose_name='模板价格')),
('fabu_shuliang', models.PositiveIntegerField(default=0, verbose_name='发布数量')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '商家订单模板',
'verbose_name_plural': '商家订单模板',
'db_table': 'shangjia_moban',
'indexes': [models.Index(fields=['yonghu_id', 'leixing_id'], name='shangjia_mo_yonghu__bb287d_idx'), models.Index(fields=['yonghu_id', 'leixing_id', 'id'], name='shangjia_mo_yonghu__4240d0_idx')],
'unique_together': {('yonghu_id', 'leixing_id', 'moban_jieshao')},
},
),
migrations.CreateModel(
name='ShangjiaLianjie',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='链接ID')),
('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')),
('moban_id', models.PositiveIntegerField(db_index=True, verbose_name='模板ID')),
('lianjie', models.CharField(max_length=500, unique=True, verbose_name='链接地址')),
('dingdan_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='订单ID')),
('is_used', models.BooleanField(db_index=True, default=False, verbose_name='是否已使用')),
('shiyongzhe_ip', models.CharField(blank=True, max_length=50, null=True, verbose_name='使用者IP')),
('youxiao_shijian', models.DateTimeField(blank=True, null=True, verbose_name='有效截止时间')),
('lianjie_token', models.CharField(blank=True, db_index=True, max_length=32, null=True, unique=True, verbose_name='链接Token')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '商家链接',
'verbose_name_plural': '商家链接',
'db_table': 'shangjia_lianjie',
'indexes': [models.Index(fields=['yonghu_id', 'moban_id', 'leixing_id'], name='shangjia_li_yonghu__9f2b25_idx'), models.Index(fields=['lianjie_token'], name='shangjia_li_lianjie_99014c_idx'), models.Index(fields=['dingdan_id'], name='shangjia_li_dingdan_95b682_idx')],
},
),
]

View File

@@ -0,0 +1,76 @@
# Generated by Django 4.2.27 on 2026-06-19 11:36
# Modified to use SeparateDatabaseAndState for RemoveIndex to bypass
# MySQL backend's _create_missing_fk_index check that fails after RenameField
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0002_rename_pinyin_to_pascal'),
]
operations = [
# RemoveIndex operations wrapped in SeparateDatabaseAndState
# to bypass MySQL backend's FK index check that references old field names
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.RemoveIndex(
model_name='shangjialianjie',
name='shangjia_li_yonghu__9f2b25_idx',
),
migrations.RemoveIndex(
model_name='shangjialianjie',
name='shangjia_li_lianjie_99014c_idx',
),
migrations.RemoveIndex(
model_name='shangjialianjie',
name='shangjia_li_dingdan_95b682_idx',
),
migrations.RemoveIndex(
model_name='shangjiamoban',
name='shangjia_mo_yonghu__bb287d_idx',
),
migrations.RemoveIndex(
model_name='shangjiamoban',
name='shangjia_mo_yonghu__4240d0_idx',
),
],
database_operations=[
migrations.RunSQL(
sql=[
'DROP INDEX `shangjia_li_yonghu__9f2b25_idx` ON `shangjia_lianjie`',
'DROP INDEX `shangjia_li_lianjie_99014c_idx` ON `shangjia_lianjie`',
'DROP INDEX `shangjia_li_dingdan_95b682_idx` ON `shangjia_lianjie`',
'DROP INDEX `shangjia_mo_yonghu__bb287d_idx` ON `shangjia_moban`',
'DROP INDEX `shangjia_mo_yonghu__4240d0_idx` ON `shangjia_moban`',
],
reverse_sql=[
# No reverse needed - indexes will be recreated by AddIndex
],
),
],
),
# AddIndex operations - these should work fine since fields exist with new names
migrations.AddIndex(
model_name='shangjialianjie',
index=models.Index(fields=['UserID', 'TemplateID', 'ProductTypeID'], name='shangjia_li_UserID_e59d94_idx'),
),
migrations.AddIndex(
model_name='shangjialianjie',
index=models.Index(fields=['LinkToken'], name='shangjia_li_LinkTok_6b32bb_idx'),
),
migrations.AddIndex(
model_name='shangjialianjie',
index=models.Index(fields=['OrderID'], name='shangjia_li_OrderID_e7ddf9_idx'),
),
migrations.AddIndex(
model_name='shangjiamoban',
index=models.Index(fields=['UserID', 'ProductTypeID'], name='shangjia_mo_UserID_abd699_idx'),
),
migrations.AddIndex(
model_name='shangjiamoban',
index=models.Index(fields=['UserID', 'ProductTypeID', 'id'], name='shangjia_mo_UserID_2dcb54_idx'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.2.27 on 2026-03-16 17:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0002_shangjiamoban_shangjialianjie'),
]
operations = [
migrations.AddField(
model_name='tupianpeizhi',
name='fangwen_cishu',
field=models.PositiveIntegerField(default=0, verbose_name='访问次数'),
),
]

View File

@@ -0,0 +1,47 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0003_remove_shangjialianjie_shangjia_li_yonghu__9f2b25_idx_and_more'),
]
operations = [
migrations.AddField(
model_name='dailyincomestat',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部'),
),
migrations.AddField(
model_name='dailypayoutstat',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部'),
),
migrations.AlterField(
model_name='dailyincomestat',
name='date',
field=models.DateField(db_index=True, verbose_name='统计日期'),
),
migrations.AlterField(
model_name='dailypayoutstat',
name='date',
field=models.DateField(db_index=True, verbose_name='统计日期'),
),
migrations.AddIndex(
model_name='dailyincomestat',
index=models.Index(fields=['club_id', 'date'], name='idx_income_club_date'),
),
migrations.AddIndex(
model_name='dailypayoutstat',
index=models.Index(fields=['club_id', 'date'], name='idx_payout_club_date'),
),
migrations.AddConstraint(
model_name='dailyincomestat',
constraint=models.UniqueConstraint(fields=('date', 'club_id'), name='uniq_daily_income_club_date'),
),
migrations.AddConstraint(
model_name='dailypayoutstat',
constraint=models.UniqueConstraint(fields=('date', 'club_id'), name='uniq_daily_payout_club_date'),
),
]

View File

@@ -1,28 +0,0 @@
# Generated by Django 4.2.27 on 2026-03-20 15:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0003_tupianpeizhi_fangwen_cishu'),
]
operations = [
migrations.CreateModel(
name='TixianQuotaDefault',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('leixing', models.IntegerField(choices=[(1, '打手'), (2, '管事')], unique=True, verbose_name='用户类型')),
('default_quota', models.DecimalField(decimal_places=2, default=20.0, max_digits=10, verbose_name='默认每日提现限额(元)')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '默认提现限额',
'verbose_name_plural': '默认提现限额',
'db_table': 'tixian_quota_default',
},
),
]

View File

@@ -1,76 +0,0 @@
# Generated by Django 4.2.27 on 2026-03-24 17:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0004_tixianquotadefault'),
]
operations = [
migrations.CreateModel(
name='OrderTypeMapping',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('partner_club_id', models.CharField(db_index=True, max_length=6, verbose_name='对方俱乐部ID')),
('partner_type_id', models.PositiveIntegerField(verbose_name='对方订单类型ID')),
('partner_type_name', models.CharField(max_length=100, verbose_name='对方订单类型名称')),
('our_type_id', models.PositiveIntegerField(verbose_name='我方订单类型ID')),
('our_type_name', models.CharField(max_length=100, verbose_name='我方订单类型名称')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '订单类型映射',
'verbose_name_plural': '订单类型映射',
'db_table': 'order_type_mapping',
'unique_together': {('partner_club_id', 'partner_type_id')},
},
),
migrations.CreateModel(
name='DailyDispatchStat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(db_index=True, verbose_name='统计日期')),
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
('direction', models.PositiveSmallIntegerField(choices=[(1, '我方派单'), (2, '对方派单')], verbose_name='派单方向')),
('partner_club_id', models.CharField(db_index=True, max_length=6, verbose_name='对方俱乐部ID')),
('dispatch_count', models.PositiveIntegerField(default=0, verbose_name='派单数')),
('dispatch_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='派单总额')),
('success_count', models.PositiveIntegerField(default=0, verbose_name='成交订单数')),
('success_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='成交总额')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '每日派单统计',
'verbose_name_plural': '每日派单统计',
'db_table': 'daily_dispatch_stat',
'indexes': [models.Index(fields=['year', 'month', 'day', 'direction', 'partner_club_id'], name='daily_dispa_year_f98f91_idx'), models.Index(fields=['date'], name='daily_dispa_date_9ac7fe_idx')],
},
),
migrations.CreateModel(
name='Club',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=6, verbose_name='本俱乐部ID')),
('partner_club_id', models.CharField(max_length=6, unique=True, verbose_name='对方俱乐部ID')),
('is_interop_enabled', models.BooleanField(default=False, verbose_name='是否开启平台互通')),
('is_receive_enabled', models.BooleanField(default=False, verbose_name='是否同意接收对方订单')),
('dashou_rate', models.DecimalField(decimal_places=4, default=1.0, max_digits=5, verbose_name='打手分红费率')),
('partner_domain', models.URLField(blank=True, verbose_name='对方服务器域名')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '俱乐部',
'verbose_name_plural': '俱乐部',
'db_table': 'club',
'indexes': [models.Index(fields=['club_id'], name='club_club_id_e75636_idx'), models.Index(fields=['partner_club_id'], name='club_partner_b76039_idx')],
},
),
]

View File

@@ -0,0 +1,37 @@
from django.db import migrations, models
def backfill_szjilu_club_id(apps, schema_editor):
Szjilu = apps.get_model('config', 'Szjilu')
for row in Szjilu.objects.all():
if not getattr(row, 'club_id', None):
row.club_id = 'xq'
row.save(update_fields=['club_id'])
class Migration(migrations.Migration):
dependencies = [
('config', '0004_daily_stat_club_id'),
]
operations = [
migrations.AddField(
model_name='szjilu',
name='club_id',
field=models.CharField(
db_index=True,
default='xq',
max_length=16,
verbose_name='俱乐部ID',
),
),
migrations.RunPython(backfill_szjilu_club_id, migrations.RunPython.noop),
migrations.AddConstraint(
model_name='szjilu',
constraint=models.UniqueConstraint(
fields=('club_id',),
name='uniq_szjilu_club_id',
),
),
]

View File

@@ -1,52 +0,0 @@
# Generated by Django 4.2.27 on 2026-04-01 09:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0005_ordertypemapping_dailydispatchstat_club'),
]
operations = [
migrations.CreateModel(
name='ClubConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=6, verbose_name='俱乐部ID')),
('club_nickname', models.CharField(blank=True, default='', max_length=50, verbose_name='俱乐部昵称')),
('club_avatar', models.CharField(blank=True, default='', max_length=500, verbose_name='俱乐部头像地址')),
('backend_domain', models.CharField(blank=True, default='', max_length=200, verbose_name='后台域名')),
('chat_api_key', models.CharField(blank=True, default='', max_length=200, verbose_name='聊天API密钥')),
('chat_api_id', models.CharField(blank=True, default='', max_length=200, verbose_name='聊天API ID')),
('customer_service_api_url', models.CharField(blank=True, default='', max_length=200, verbose_name='客服API URL')),
('customer_service_corp_id', models.CharField(blank=True, default='', max_length=100, verbose_name='客服企业ID')),
('storage_bucket_domain', models.CharField(blank=True, default='', max_length=500, verbose_name='存储桶域名')),
('is_self', models.SmallIntegerField(choices=[(0, '对方'), (1, '我方')], db_index=True, default=0, verbose_name='是否我方俱乐部')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '俱乐部配置',
'verbose_name_plural': '俱乐部配置',
'db_table': 'club_config',
'unique_together': {('club_id',)},
},
),
migrations.CreateModel(
name='AccountPermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')),
('permission_code', models.IntegerField(verbose_name='权限代码')),
('is_enabled', models.BooleanField(default=False, verbose_name='是否开启')),
],
options={
'verbose_name': '账号权限',
'verbose_name_plural': '账号权限',
'db_table': 'account_permission',
'unique_together': {('account_id', 'permission_code')},
},
),
]

View File

@@ -0,0 +1,81 @@
from django.db import migrations, models
from django.db.models import Max
def _dedupe_popuppage(apps, schema_editor):
PopupPage = apps.get_model('config', 'PopupPage')
for row in PopupPage.objects.values('club_id', 'page_key').annotate(max_id=Max('id')):
PopupPage.objects.filter(
club_id=row['club_id'],
page_key=row['page_key'],
).exclude(id=row['max_id']).delete()
def _dedupe_tupianpeizhi(apps, schema_editor):
Tupianpeizhi = apps.get_model('config', 'Tupianpeizhi')
for row in Tupianpeizhi.objects.values('club_id', 'ImageType').annotate(max_id=Max('id')):
Tupianpeizhi.objects.filter(
club_id=row['club_id'],
ImageType=row['ImageType'],
).exclude(id=row['max_id']).delete()
class Migration(migrations.Migration):
dependencies = [
('config', '0005_szjilu_club_id'),
]
operations = [
migrations.AddField(
model_name='gonggao',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='lunbo',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='lunbo',
name='page_key',
field=models.CharField(
db_index=True, default='order_pool', max_length=32,
verbose_name='页面标识(order_pool/accept_order/merchant_home/dashou_center)',
),
),
migrations.AddField(
model_name='popuppage',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='tupianpeizhi',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AlterUniqueTogether(
name='popuppage',
unique_together=set(),
),
migrations.AlterField(
model_name='popuppage',
name='page_key',
field=models.CharField(db_index=True, max_length=50, verbose_name='页面标识'),
),
migrations.RunPython(_dedupe_popuppage, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='popuppage',
unique_together={('club_id', 'page_key')},
),
migrations.RunPython(_dedupe_tupianpeizhi, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='tupianpeizhi',
unique_together={('club_id', 'ImageType')},
),
migrations.AddIndex(
model_name='lunbo',
index=models.Index(fields=['club_id', 'page_key', 'ImageType'], name='lunbo_club_page_type_idx'),
),
]

View File

@@ -1,28 +0,0 @@
# Generated by Django 4.2.27 on 2026-04-02 13:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0006_clubconfig_accountpermission'),
]
operations = [
migrations.AddField(
model_name='club',
name='receive_rate',
field=models.DecimalField(decimal_places=4, default=1.0, max_digits=5, verbose_name='接收打手分红费率'),
),
migrations.AddField(
model_name='dailydispatchstat',
name='claimed_amount',
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='抢单总额'),
),
migrations.AddField(
model_name='dailydispatchstat',
name='claimed_count',
field=models.PositiveIntegerField(default=0, verbose_name='抢单数'),
),
]

View File

@@ -0,0 +1,26 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0006_display_club_id'),
]
operations = [
migrations.CreateModel(
name='PlatformIncomeLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('biz_ref', models.CharField(db_index=True, max_length=64, unique=True, verbose_name='业务单号')),
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='入账金额')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
],
options={
'verbose_name': '平台入账幂等日志',
'verbose_name_plural': '平台入账幂等日志',
'db_table': 'platform_income_log',
},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.2.27 on 2026-04-04 18:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more'),
]
operations = [
migrations.AddField(
model_name='clubconfig',
name='storage_bucket_name',
field=models.CharField(blank=True, default='', max_length=100, verbose_name='存储桶名称'),
),
]

View File

@@ -0,0 +1,34 @@
from django.db import migrations, models
def backfill_gonggao_page_key(apps, schema_editor):
Gonggao = apps.get_model('config', 'Gonggao')
for row in Gonggao.objects.filter(page_key='').only('id'):
row.page_key = 'order_pool'
row.save(update_fields=['page_key'])
for row in Gonggao.objects.filter(page_key__isnull=True).only('id'):
row.page_key = 'order_pool'
row.save(update_fields=['page_key'])
class Migration(migrations.Migration):
dependencies = [
('config', '0007_platform_income_log'),
]
operations = [
migrations.AddField(
model_name='gonggao',
name='page_key',
field=models.CharField(
db_index=True, default='order_pool', max_length=32,
verbose_name='页面标识(order_pool/accept_order/merchant_home)',
),
),
migrations.RunPython(backfill_gonggao_page_key, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='gonggao',
unique_together={('club_id', 'page_key')},
),
]

View File

@@ -1,53 +0,0 @@
# Generated by Django 4.2.27 on 2026-04-05 13:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0008_clubconfig_storage_bucket_name'),
]
operations = [
migrations.CreateModel(
name='DailyPayoutStat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')),
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='出款总额')),
('total_count', models.PositiveIntegerField(default=0, verbose_name='出款总笔数')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '每日出款统计',
'verbose_name_plural': '每日出款统计',
'db_table': 'daily_payout_stat',
'indexes': [models.Index(fields=['date'], name='daily_payou_date_b534ec_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_payou_year_6376ca_idx')],
},
),
migrations.CreateModel(
name='DailyIncomeStat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')),
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='总收入金额')),
('total_count', models.PositiveIntegerField(default=0, verbose_name='总收入笔数')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '每日收入统计',
'verbose_name_plural': '每日收入统计',
'db_table': 'daily_income_stat',
'indexes': [models.Index(fields=['date'], name='daily_incom_date_75e6df_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_incom_year_d85d10_idx')],
},
),
]

View File

@@ -0,0 +1,27 @@
from django.db import migrations
def copy_gonggao_to_other_pages(apps, schema_editor):
"""将 order_pool 公告复制到 accept_order / merchant_home仅当目标页尚无记录"""
Gonggao = apps.get_model('config', 'Gonggao')
for pk in ('accept_order', 'merchant_home'):
for row in Gonggao.objects.filter(page_key='order_pool'):
if Gonggao.objects.filter(club_id=row.club_id, page_key=pk).exists():
continue
Gonggao.objects.create(
club_id=row.club_id,
page_key=pk,
NoticeType=row.NoticeType,
Content=row.Content or '',
)
class Migration(migrations.Migration):
dependencies = [
('config', '0008_gonggao_page_key'),
]
operations = [
migrations.RunPython(copy_gonggao_to_other_pages, migrations.RunPython.noop),
]

View File

@@ -0,0 +1,67 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('config', '0009_gonggao_copy_pages'),
]
operations = [
migrations.CreateModel(
name='ClubMiniappIcon',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
('icon_key', models.CharField(db_index=True, max_length=64, verbose_name='图标键')),
('label', models.CharField(blank=True, default='', max_length=100, verbose_name='展示名称')),
('image_path', models.CharField(blank=True, default='', max_length=500, verbose_name='图片相对路径')),
('description', models.CharField(blank=True, default='', max_length=300, verbose_name='说明')),
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '小程序图标',
'verbose_name_plural': '小程序图标',
'db_table': 'club_miniapp_icon',
'ordering': ['icon_key'],
'unique_together': {('club_id', 'icon_key')},
},
),
migrations.CreateModel(
name='ClubPindao',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(default='xq', max_length=16, unique=True, verbose_name='俱乐部ID')),
('channel_no', models.CharField(blank=True, default='', max_length=100, verbose_name='频道号')),
('title', models.CharField(blank=True, default='频道', max_length=100, verbose_name='标题')),
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '俱乐部频道',
'verbose_name_plural': '俱乐部频道',
'db_table': 'club_pindao',
},
),
migrations.CreateModel(
name='ClubPindaoImage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
('image_path', models.CharField(max_length=500, verbose_name='图片相对路径')),
('sort_order', models.PositiveIntegerField(default=0, verbose_name='排序')),
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('pindao', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='images', to='config.clubpindao')),
],
options={
'verbose_name': '频道图片',
'verbose_name_plural': '频道图片',
'db_table': 'club_pindao_image',
'ordering': ['sort_order', 'id'],
},
),
]

View File

@@ -1,84 +0,0 @@
# Generated by Django 4.2.27 on 2026-04-22 11:24
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('config', '0009_dailypayoutstat_dailyincomestat'),
]
operations = [
migrations.CreateModel(
name='PopupConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('popup_id', models.CharField(max_length=50, verbose_name='弹窗唯一ID')),
('title', models.CharField(blank=True, max_length=200, verbose_name='标题(后台用)')),
('description', models.TextField(blank=True, verbose_name='描述(后台用)')),
('strategy_type', models.CharField(choices=[('once', '仅一次'), ('daily', '每日'), ('weekly', '每周'), ('monthly', '每月'), ('yearly', '每年'), ('always', '始终'), ('count', '周期内次数限制')], default='once', max_length=20)),
('max_count', models.PositiveIntegerField(default=1, verbose_name='周期内最大展示次数')),
('reset_interval', models.CharField(blank=True, choices=[('day', ''), ('week', ''), ('month', ''), ('year', '')], max_length=20, verbose_name='重置周期count类型时使用')),
('duration', models.PositiveIntegerField(default=0, verbose_name='强制阅读秒数0=无强制)')),
('force_even_muted', models.BooleanField(default=False, verbose_name='强制弹出(忽略用户今日屏蔽)')),
('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')),
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
('start_time', models.DateTimeField(blank=True, null=True, verbose_name='生效开始时间')),
('end_time', models.DateTimeField(blank=True, null=True, verbose_name='生效结束时间')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': '弹窗配置',
'verbose_name_plural': '弹窗配置',
'db_table': 'popup_config',
'ordering': ['sort_order', 'id'],
},
),
migrations.CreateModel(
name='PopupPage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('page_key', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='页面标识')),
('name', models.CharField(max_length=100, verbose_name='页面名称')),
('description', models.TextField(blank=True, verbose_name='描述')),
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('ignore_user_mute', models.BooleanField(default=False, verbose_name='忽略用户今日屏蔽')),
],
options={
'verbose_name': '弹窗页面',
'verbose_name_plural': '弹窗页面',
'db_table': 'popup_page',
},
),
migrations.CreateModel(
name='PopupImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image_url', models.CharField(max_length=500, verbose_name='图片相对路径')),
('text', models.TextField(blank=True, verbose_name='图片上方文字')),
('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')),
('created_at', models.DateTimeField(auto_now_add=True)),
('popup_config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='config.popupconfig')),
],
options={
'verbose_name': '弹窗图片',
'verbose_name_plural': '弹窗图片',
'db_table': 'popup_image',
'ordering': ['sort_order', 'id'],
},
),
migrations.AddField(
model_name='popupconfig',
name='page',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='popups', to='config.popuppage'),
),
migrations.AlterUniqueTogether(
name='popupconfig',
unique_together={('page', 'popup_id')},
),
]

View File

@@ -1,18 +1,16 @@
# Generated by Django 4.2.27 on 2026-05-18 02:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0012_usershenheguan'),
('config', '0010_club_miniapp_icon_pindao'),
]
operations = [
migrations.AddField(
model_name='usershenheguan',
name='is_renzheng',
field=models.BooleanField(default=False, verbose_name='是否认证'),
model_name='clubminiappicon',
name='is_uploaded',
field=models.BooleanField(default=False, verbose_name='是否已在后台上传'),
),
]

View File

@@ -1,27 +0,0 @@
# Generated by Django 4.2.27 on 2026-04-25 12:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more'),
]
operations = [
migrations.CreateModel(
name='WithdrawConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('mode', models.IntegerField(choices=[(1, '自动提现'), (2, '手动提现')], default=2, verbose_name='提现模式')),
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '提现模式配置',
'verbose_name_plural': '提现模式配置',
'db_table': 'withdraw_config',
},
),
]

View File

@@ -1,28 +0,0 @@
# Generated by Django 4.2.27 on 2026-05-17 18:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0011_withdrawconfig'),
]
operations = [
migrations.AddField(
model_name='shangjiamoban',
name='chenghao_id',
field=models.IntegerField(blank=True, null=True, verbose_name='关联称号ID'),
),
migrations.AddField(
model_name='shangjiamoban',
name='yongjin_jiage',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='佣金价格'),
),
migrations.AddField(
model_name='shangjiamoban',
name='yongjin_qiangdan',
field=models.BooleanField(default=False, verbose_name='是否开启佣金抢单'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 4.2.27 on 2026-05-26 03:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0012_shangjiamoban_chenghao_id_and_more'),
]
operations = [
migrations.AddField(
model_name='shangjialianjie',
name='previous_dingdan_id',
field=models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='上一个订单ID'),
),
migrations.AddField(
model_name='shangjialianjie',
name='previous_lianjie_id',
field=models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='上一个链接ID'),
),
]

View File

@@ -1,30 +0,0 @@
# Generated by Django 4.2.27 on 2026-06-15 01:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0013_shangjialianjie_previous_dingdan_id_and_more'),
]
operations = [
migrations.DeleteModel(
name='Club',
),
migrations.DeleteModel(
name='ClubConfig',
),
migrations.DeleteModel(
name='DailyDispatchStat',
),
migrations.DeleteModel(
name='OrderTypeMapping',
),
migrations.AlterField(
model_name='tixianquotadefault',
name='leixing',
field=models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], unique=True, verbose_name='用户类型'),
),
]

View File

@@ -1,434 +1,522 @@
from django.db import models
from gvsdsdk.model_base import QModel
class Lunbo(QModel):
tupian_url = models.CharField(max_length=500, null=True, blank=True, verbose_name='轮播图片URL')
leixing = models.IntegerField(null=True, blank=True, verbose_name='图片类型:1轮播2个人中心背景')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间')
class Meta:
db_table = 'lunbo'
verbose_name = '轮播'
verbose_name_plural = '轮播'
# 不要索引!数据量太小
class Gonggao(QModel):
leixing = models.IntegerField(null=True, blank=True, verbose_name='公告类型')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间')
jieshao = models.TextField(null=True, blank=True, verbose_name='公告介绍')
class Meta:
db_table = 'gonggao'
verbose_name = '公告'
verbose_name_plural = '公告'
# 不要索引!数据量太小
class Qunpeizhi(QModel):
peizhiid = models.IntegerField(null=True, blank=True, verbose_name='群类型:1.打手 2.管事')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间')
neirong = models.CharField(max_length=200, null=True, blank=True, verbose_name='群介绍')
qunid = models.CharField(max_length=100, null=True, blank=True, verbose_name='群id')
jieshao = models.CharField(max_length=200, null=True, blank=True, verbose_name='介绍')
class Meta:
db_table = 'qunpeizhi'
class Tupianpeizhi(QModel):
tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='配置图片URL')
leixing = models.PositiveIntegerField(null=True, blank=True, verbose_name='提现类型1:打手规则图片2默认头像图片')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间')
fangwen_cishu = models.PositiveIntegerField(default=0, verbose_name='访问次数') # 新增字段
class Meta:
db_table = 'tupianpeizhi'
verbose_name = '配置图片'
verbose_name_plural = '配置图片'
class Szjilu(QModel):
id = models.AutoField(primary_key=True, verbose_name='记录ID')
zongsy = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总收益')
zongls = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总流水')
zongzc = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总支出')
jrzc = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日支出')
jrls = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日流水')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间')
class Meta:
db_table = 'szjilu'
verbose_name = '收支记录'
verbose_name_plural = '收支记录'
class ShangjiaMoban(QModel):
"""商家订单模板表 - 存储商家创建的订单模板"""
# 主键ID自动递增
id = models.AutoField(primary_key=True, verbose_name='模板ID')
# 关联字段
yonghu_id = models.CharField(max_length=7, db_index=True, verbose_name='用户ID')
leixing_id = models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')
# 模板内容
moban_jieshao = models.CharField(max_length=500, verbose_name='模板介绍')
jiage = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name='模板价格',
help_text='单位:元'
)
# 统计字段
fabu_shuliang = models.PositiveIntegerField(default=0, verbose_name='发布数量')
yongjin_qiangdan = models.BooleanField(default=False, verbose_name='是否开启佣金抢单')
yongjin_jiage = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='佣金价格')
chenghao_id = models.IntegerField(null=True, blank=True, verbose_name='关联称号ID')
# 时间字段
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'shangjia_moban'
verbose_name = '商家订单模板'
verbose_name_plural = verbose_name
indexes = [
# 高频查询组合索引
models.Index(fields=['yonghu_id', 'leixing_id']),
models.Index(fields=['yonghu_id', 'leixing_id','id']),
]
# 同一个商家同一类型下模板介绍不能重复(允许修改)
unique_together = ['yonghu_id', 'leixing_id', 'moban_jieshao']
def __str__(self):
return f"模板{self.id}-{self.yonghu_id}"
class ShangjiaLianjie(QModel):
"""商家链接存储表 - 存储生成的派单链接"""
# 主键ID自动递增
id = models.AutoField(primary_key=True, verbose_name='链接ID')
# 关联字段
yonghu_id = models.CharField(max_length=7, db_index=True, verbose_name='用户ID')
leixing_id = models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')
moban_id = models.PositiveIntegerField(db_index=True, verbose_name='模板ID')
# 链接核心数据
lianjie = models.CharField(max_length=500, unique=True, verbose_name='链接地址')
#lianjie_token = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='链接Token')
# 订单关联(生成订单后更新)
dingdan_id = models.CharField(max_length=32, null=True, blank=True, db_index=True, verbose_name='订单ID')
# 使用状态
is_used = models.BooleanField(default=False, verbose_name='是否已使用', db_index=True)
#shiyong_time = models.DateTimeField(null=True, blank=True, verbose_name='使用时间')
# 使用者信息
shiyongzhe_ip = models.CharField(max_length=50, null=True, blank=True, verbose_name='使用者IP')
#shiyongzhe_info = models.TextField(null=True, blank=True, verbose_name='使用者信息')
# 链接控制
#shiyong_cishu = models.PositiveIntegerField(default=1, verbose_name='可使用次数')
#yixiang_cishu = models.PositiveIntegerField(default=0, verbose_name='已使用次数')
youxiao_shijian = models.DateTimeField(null=True, blank=True, verbose_name='有效截止时间')
# 安全字段
#anquan_ma = models.CharField(max_length=100, null=True, blank=True, verbose_name='安全码')
# 新增 Token 字段
lianjie_token = models.CharField(
max_length=32,
unique=True,
db_index=True,
verbose_name='链接Token',
null=True,
blank=True # 先设置为可选,后面再设为必填
)
# 时间字段
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
# 在 ShangjiaLianjie 类中添加
# 上一个链接的ID可选记录订单被更换前的链接
previous_lianjie_id = models.PositiveIntegerField(
null=True,
blank=True,
db_index=True,
verbose_name='上一个链接ID'
)
# 上一个订单的ID可选记录被更换的旧订单ID
previous_dingdan_id = models.CharField(
max_length=32,
null=True,
blank=True,
db_index=True,
verbose_name='上一个订单ID'
)
class Meta:
db_table = 'shangjia_lianjie'
verbose_name = '商家链接'
verbose_name_plural = verbose_name
indexes = [
# 高频查询组合索引
models.Index(fields=['yonghu_id', 'moban_id','leixing_id']),
models.Index(fields=['lianjie_token']),
#models.Index(fields=['lianjie_token']),
models.Index(fields=['dingdan_id']),
#models.Index(fields=['moban_id']),
#models.Index(fields=['create_time']),
#models.Index(fields=['youxiao_shijian', 'is_used']),
]
#ordering = ['-create_time']
def __str__(self):
return f"链接{self.id}-{self.yonghu_id}"
class TixianQuotaDefault(QModel):
"""
默认每日提现限额表
"""
id = models.AutoField(primary_key=True)
leixing = models.IntegerField(
choices=[(1, '打手'), (2, '管事'), (3, '组长'),
(4, '考核官'), (5, '打手押金'), (6, '商家余额')],
unique=True,
verbose_name='用户类型'
)
default_quota = models.DecimalField(
max_digits=10,
decimal_places=2,
default=20.00,
verbose_name='默认每日提现限额(元)'
)
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'tixian_quota_default'
verbose_name = '默认提现限额'
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.get_leixing_display()} 默认限额: {self.default_quota}"
class WithdrawConfig(QModel):
"""
提现模式配置表
- mode = 1: 自动提现(前端展示收款码模式)
- mode = 2: 手动提现(前端展示微信零钱模式)
默认值为 2手动打款没有记录时前端也按默认 2 处理
"""
MODE_CHOICES = (
(1, '自动提现'), # 对应前端模式一
(2, '手动提现'), # 对应前端模式二
)
mode = models.IntegerField(
choices=MODE_CHOICES,
default=2,
verbose_name='提现模式'
)
created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'withdraw_config'
verbose_name = '提现模式配置'
verbose_name_plural = verbose_name
def __str__(self):
return f'提现模式: {"自动" if self.mode == 1 else "手动"}'
class AccountPermission(QModel):
"""账号权限表:记录每个账号拥有的权限及开关状态"""
account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID')
permission_code = models.IntegerField(verbose_name='权限代码')
is_enabled = models.BooleanField(default=False, verbose_name='是否开启')
class Meta:
db_table = 'account_permission'
verbose_name = '账号权限'
verbose_name_plural = '账号权限'
unique_together = [['account_id', 'permission_code']] # 一个账号的同一权限只能有一条记录
def __str__(self):
return f"{self.account_id} 权限{self.permission_code}: {'开启' if self.is_enabled else '关闭'}"
# apps/dingdan/models.py (新增模型)
class DailyIncomeStat(QModel):
"""
每日收入统计表
记录平台每天通过微信支付获得的总收入和总笔数
"""
# 日期相关字段(便于快速筛选和分组)
date = models.DateField(verbose_name='统计日期', db_index=True, unique=True)
year = models.PositiveSmallIntegerField(verbose_name='年份')
month = models.PositiveSmallIntegerField(verbose_name='月份')
day = models.PositiveSmallIntegerField(verbose_name='日期')
# 收入统计
total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='总收入金额')
total_count = models.PositiveIntegerField(default=0, verbose_name='总收入笔数')
# 时间戳
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'daily_income_stat'
verbose_name = '每日收入统计'
verbose_name_plural = verbose_name
indexes = [
models.Index(fields=['date']),
models.Index(fields=['year', 'month', 'day']),
]
def __str__(self):
return f"{self.date} - 收入{self.total_amount}元 - {self.total_count}"
# apps/dingdan/models.py
class DailyPayoutStat(QModel):
"""
每日出款统计表
记录平台每天通过提现/结算等方式的出款总额和总笔数
"""
date = models.DateField(verbose_name='统计日期', db_index=True, unique=True)
year = models.PositiveSmallIntegerField(verbose_name='年份')
month = models.PositiveSmallIntegerField(verbose_name='月份')
day = models.PositiveSmallIntegerField(verbose_name='日期')
total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='出款总额')
total_count = models.PositiveIntegerField(default=0, verbose_name='出款总笔数')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'daily_payout_stat'
verbose_name = '每日出款统计'
verbose_name_plural = verbose_name
indexes = [
models.Index(fields=['date']),
models.Index(fields=['year', 'month', 'day']),
]
def __str__(self):
return f"{self.date} - 出款{self.total_amount}元 - {self.total_count}"
class PopupPage(QModel):
page_key = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='页面标识')
name = models.CharField(max_length=100, verbose_name='页面名称')
description = models.TextField(blank=True, verbose_name='描述')
is_active = models.BooleanField(default=True, verbose_name='是否启用')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
ignore_user_mute = models.BooleanField(default=False, verbose_name='忽略用户今日屏蔽')
class Meta:
db_table = 'popup_page'
verbose_name = '弹窗页面'
verbose_name_plural = '弹窗页面'
def __str__(self):
return f'{self.name} ({self.page_key})'
class PopupConfig(QModel):
STRATEGY_CHOICES = (
('once', '仅一次'),
('daily', '每日'),
('weekly', '每周'),
('monthly', '每月'),
('yearly', '每年'),
('always', '始终'),
('count', '周期内次数限制'),
)
page = models.ForeignKey(PopupPage, on_delete=models.CASCADE, related_name='popups')
popup_id = models.CharField(max_length=50, verbose_name='弹窗唯一ID')
title = models.CharField(max_length=200, blank=True, verbose_name='标题(后台用)')
description = models.TextField(blank=True, verbose_name='描述(后台用)')
strategy_type = models.CharField(max_length=20, choices=STRATEGY_CHOICES, default='once')
max_count = models.PositiveIntegerField(default=1, verbose_name='周期内最大展示次数')
reset_interval = models.CharField(max_length=20, blank=True,
choices=(('day',''),('week',''),('month',''),('year','')),
verbose_name='重置周期count类型时使用')
duration = models.PositiveIntegerField(default=0, verbose_name='强制阅读秒数0=无强制)')
force_even_muted = models.BooleanField(default=False, verbose_name='强制弹出(忽略用户今日屏蔽)')
sort_order = models.PositiveIntegerField(default=0, verbose_name='展示顺序')
is_active = models.BooleanField(default=True, verbose_name='是否启用')
start_time = models.DateTimeField(null=True, blank=True, verbose_name='生效开始时间')
end_time = models.DateTimeField(null=True, blank=True, verbose_name='生效结束时间')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'popup_config'
verbose_name = '弹窗配置'
verbose_name_plural = '弹窗配置'
unique_together = ('page', 'popup_id')
ordering = ['sort_order', 'id']
def __str__(self):
return f'{self.page.name} - {self.popup_id}'
class PopupImage(QModel):
popup_config = models.ForeignKey(PopupConfig, on_delete=models.CASCADE, related_name='images')
image_url = models.CharField(max_length=500, verbose_name='图片相对路径')
text = models.TextField(blank=True, verbose_name='图片上方文字')
sort_order = models.PositiveIntegerField(default=0, verbose_name='展示顺序')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'popup_image'
verbose_name = '弹窗图片'
verbose_name_plural = '弹窗图片'
ordering = ['sort_order', 'id']
def __str__(self):
return f'{self.popup_config.popup_id} - 图片{self.sort_order}'
from django.db import models
from gvsdsdk.model_base import QModel
class Lunbo(QModel):
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
page_key = models.CharField(
max_length=32, default='order_pool', db_index=True,
verbose_name='页面标识(order_pool/accept_order/merchant_home/dashou_center)',
)
ImageURL = models.CharField(max_length=500, null=True, blank=True, verbose_name='轮播图片URL')
ImageType = models.IntegerField(null=True, blank=True, verbose_name='图片类型:1轮播2个人中心背景')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更改时间')
class Meta:
db_table = 'lunbo'
verbose_name = '轮播'
verbose_name_plural = '轮播'
indexes = [
models.Index(fields=['club_id', 'page_key', 'ImageType']),
]
class Gonggao(QModel):
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
page_key = models.CharField(
max_length=32, default='order_pool', db_index=True,
verbose_name='页面标识(order_pool/accept_order/merchant_home)',
)
NoticeType = models.IntegerField(null=True, blank=True, verbose_name='公告类型')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更改时间')
Content = models.TextField(null=True, blank=True, verbose_name='公告介绍')
class Meta:
db_table = 'gonggao'
verbose_name = '公告'
verbose_name_plural = '公告'
unique_together = [['club_id', 'page_key']]
class Qunpeizhi(QModel):
GroupType = models.IntegerField(null=True, blank=True, verbose_name='群类型:1.打手 2.管事')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更改时间')
GroupContent = models.CharField(max_length=200, null=True, blank=True, verbose_name='群介绍')
GroupID = models.CharField(max_length=100, null=True, blank=True, verbose_name='群id')
Description = models.CharField(max_length=200, null=True, blank=True, verbose_name='介绍')
class Meta:
db_table = 'qunpeizhi'
class Tupianpeizhi(QModel):
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
ImageURL = models.CharField(max_length=500, null=True, blank=True, verbose_name='配置图片URL')
ImageType = models.PositiveIntegerField(null=True, blank=True, verbose_name='提现类型1:打手规则图片2默认头像图片')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更改时间')
AccessCount = models.PositiveIntegerField(default=0, verbose_name='访问次数')
class Meta:
db_table = 'tupianpeizhi'
verbose_name = '配置图片'
verbose_name_plural = '配置图片'
# ImageType 1/2 每俱乐部唯一3(关注快手) 允许多张
class Szjilu(QModel):
"""
【已废弃 · 禁止使用】
本表与财务看板「每日收入 / 每日支出」无关。
财务四个板子只读 daily_income_stat、daily_payout_stat不得读写本表。
repair_wechat_income 曾误写本表导致数据膨胀,后续入账也不再更新本表。
"""
id = models.AutoField(primary_key=True, verbose_name='记录ID')
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
TotalIncome = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总收益')
TotalFlow = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总流水')
TotalExpense = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总支出')
DailyExpense = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日支出')
DailyFlow = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今日流水')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更改时间')
class Meta:
db_table = 'szjilu'
verbose_name = '收支记录'
verbose_name_plural = '收支记录'
constraints = [
models.UniqueConstraint(fields=['club_id'], name='uniq_szjilu_club_id'),
]
class ShangjiaMoban(QModel):
"""商家订单模板表 - 存储商家创建的订单模板"""
# 主键ID自动递增
id = models.AutoField(primary_key=True, verbose_name='模板ID')
# 关联字段
UserID = models.CharField(max_length=7, db_index=True, verbose_name='用户ID')
ProductTypeID = models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')
# 模板内容
TemplateDesc = models.CharField(max_length=500, verbose_name='模板介绍')
Price = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name='模板价格',
help_text='单位:元'
)
# 统计字段
PublishedCount = models.PositiveIntegerField(default=0, verbose_name='发布数量')
CommissionGrabEnabled = models.BooleanField(default=False, verbose_name='是否开启佣金抢单')
CommissionPrice = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='佣金价格')
TitleID = models.IntegerField(null=True, blank=True, verbose_name='关联称号ID')
# 时间字段
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'shangjia_moban'
verbose_name = '商家订单模板'
verbose_name_plural = verbose_name
indexes = [
# 高频查询组合索引
models.Index(fields=['UserID', 'ProductTypeID']),
models.Index(fields=['UserID', 'ProductTypeID','id']),
]
# 同一个商家同一类型下模板介绍不能重复(允许修改)
unique_together = ['UserID', 'ProductTypeID', 'TemplateDesc']
def __str__(self):
return f"模板{self.id}-{self.UserID}"
class ShangjiaLianjie(QModel):
"""商家链接存储表 - 存储生成的派单链接"""
# 主键ID自动递增
id = models.AutoField(primary_key=True, verbose_name='链接ID')
# 关联字段
UserID = models.CharField(max_length=7, db_index=True, verbose_name='用户ID')
ProductTypeID = models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')
TemplateID = models.PositiveIntegerField(db_index=True, verbose_name='模板ID')
# 链接核心数据
LinkURL = models.CharField(max_length=500, unique=True, verbose_name='链接地址')
# 订单关联(生成订单后更新)
OrderID = models.CharField(max_length=32, null=True, blank=True, db_index=True, verbose_name='订单ID')
# 使用状态
is_used = models.BooleanField(default=False, verbose_name='是否已使用', db_index=True)
# 使用者信息
UserIP = models.CharField(max_length=50, null=True, blank=True, verbose_name='使用者IP')
# 链接控制
ExpireTime = models.DateTimeField(null=True, blank=True, verbose_name='有效截止时间')
# 安全字段
LinkToken = models.CharField(
max_length=32,
unique=True,
db_index=True,
verbose_name='链接Token',
null=True,
blank=True
)
# 时间字段
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
# 上一个链接的ID可选记录订单被更换前的链接
PreviousLinkID = models.PositiveIntegerField(
null=True,
blank=True,
db_index=True,
verbose_name='上一个链接ID'
)
# 上一个订单的ID可选记录被更换的旧订单ID
PreviousOrderID = models.CharField(
max_length=32,
null=True,
blank=True,
db_index=True,
verbose_name='上一个订单ID'
)
class Meta:
db_table = 'shangjia_lianjie'
verbose_name = '商家链接'
verbose_name_plural = verbose_name
indexes = [
# 高频查询组合索引
models.Index(fields=['UserID', 'TemplateID','ProductTypeID']),
models.Index(fields=['LinkToken']),
models.Index(fields=['OrderID']),
]
def __str__(self):
return f"链接{self.id}-{self.UserID}"
class TixianQuotaDefault(QModel):
"""
默认每日提现限额表
"""
id = models.AutoField(primary_key=True)
UserType = models.IntegerField(
choices=[(1, '打手'), (2, '管事'), (3, '组长'),
(4, '考核官'), (5, '打手押金'), (6, '商家余额')],
unique=True,
verbose_name='用户类型'
)
default_quota = models.DecimalField(
max_digits=10,
decimal_places=2,
default=20.00,
verbose_name='默认每日提现限额(元)'
)
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'tixian_quota_default'
verbose_name = '默认提现限额'
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.get_UserType_display()} 默认限额: {self.default_quota}"
class WithdrawConfig(QModel):
"""
提现模式配置表
- mode = 1: 自动提现(前端展示收款码模式)
- mode = 2: 手动提现(前端展示微信零钱模式)
默认值为 2手动打款没有记录时前端也按默认 2 处理
"""
MODE_CHOICES = (
(1, '自动提现'), # 对应前端模式一
(2, '手动提现'), # 对应前端模式二
)
mode = models.IntegerField(
choices=MODE_CHOICES,
default=2,
verbose_name='提现模式'
)
created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'withdraw_config'
verbose_name = '提现模式配置'
verbose_name_plural = verbose_name
def __str__(self):
return f'提现模式: {"自动" if self.mode == 1 else "手动"}'
class AccountPermission(QModel):
"""账号权限表:记录每个账号拥有的权限及开关状态"""
account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID')
permission_code = models.IntegerField(verbose_name='权限代码')
is_enabled = models.BooleanField(default=False, verbose_name='是否开启')
class Meta:
db_table = 'account_permission'
verbose_name = '账号权限'
verbose_name_plural = '账号权限'
unique_together = [['account_id', 'permission_code']] # 一个账号的同一权限只能有一条记录
def __str__(self):
return f"{self.account_id} 权限{self.permission_code}: {'开启' if self.is_enabled else '关闭'}"
# apps/dingdan/models.py (新增模型)
class DailyIncomeStat(QModel):
"""
每日收入统计表 —— 财务看板「今日收入 / 累计收入 / 每日明细」唯一数据源。
微信支付成功时由 update_daily_income 写入;勿读 szjilu 表。
"""
# 日期相关字段(便于快速筛选和分组)
date = models.DateField(verbose_name='统计日期', db_index=True)
club_id = models.CharField(
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
)
year = models.PositiveSmallIntegerField(verbose_name='年份')
month = models.PositiveSmallIntegerField(verbose_name='月份')
day = models.PositiveSmallIntegerField(verbose_name='日期')
# 收入统计
total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='总收入金额')
total_count = models.PositiveIntegerField(default=0, verbose_name='总收入笔数')
# 时间戳
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'daily_income_stat'
verbose_name = '每日收入统计'
verbose_name_plural = verbose_name
indexes = [
models.Index(fields=['date']),
models.Index(fields=['year', 'month', 'day']),
models.Index(fields=['club_id', 'date']),
]
constraints = [
models.UniqueConstraint(fields=['date', 'club_id'], name='uniq_daily_income_club_date'),
]
def __str__(self):
return f"{self.date} - 收入{self.total_amount}元 - {self.total_count}"
# apps/dingdan/models.py
class DailyPayoutStat(QModel):
"""
每日出款统计表 —— 财务看板「今日支出 / 累计支出」唯一数据源。
"""
date = models.DateField(verbose_name='统计日期', db_index=True)
club_id = models.CharField(
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
)
year = models.PositiveSmallIntegerField(verbose_name='年份')
month = models.PositiveSmallIntegerField(verbose_name='月份')
day = models.PositiveSmallIntegerField(verbose_name='日期')
total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='出款总额')
total_count = models.PositiveIntegerField(default=0, verbose_name='出款总笔数')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'daily_payout_stat'
verbose_name = '每日出款统计'
verbose_name_plural = verbose_name
indexes = [
models.Index(fields=['date']),
models.Index(fields=['year', 'month', 'day']),
models.Index(fields=['club_id', 'date']),
]
constraints = [
models.UniqueConstraint(fields=['date', 'club_id'], name='uniq_daily_payout_club_date'),
]
def __str__(self):
return f"{self.date} - 出款{self.total_amount}元 - {self.total_count}"
class PlatformIncomeLog(QModel):
"""微信入账幂等日志:同一 biz_ref 只记一次 daily_income_stat。"""
biz_ref = models.CharField(max_length=64, unique=True, db_index=True, verbose_name='业务单号')
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
amount = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='入账金额')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
db_table = 'platform_income_log'
verbose_name = '平台入账幂等日志'
verbose_name_plural = verbose_name
class PopupPage(QModel):
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
page_key = models.CharField(max_length=50, db_index=True, verbose_name='页面标识')
name = models.CharField(max_length=100, verbose_name='页面名称')
description = models.TextField(blank=True, verbose_name='描述')
is_active = models.BooleanField(default=True, verbose_name='是否启用')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
ignore_user_mute = models.BooleanField(default=False, verbose_name='忽略用户今日屏蔽')
class Meta:
db_table = 'popup_page'
verbose_name = '弹窗页面'
verbose_name_plural = '弹窗页面'
unique_together = [['club_id', 'page_key']]
def __str__(self):
return f'{self.name} ({self.page_key})'
class PopupConfig(QModel):
STRATEGY_CHOICES = (
('once', '仅一次'),
('daily', '每日'),
('weekly', '每周'),
('monthly', '每月'),
('yearly', '每年'),
('always', '始终'),
('count', '周期内次数限制'),
)
page = models.ForeignKey(PopupPage, on_delete=models.CASCADE, related_name='popups')
popup_id = models.CharField(max_length=50, verbose_name='弹窗唯一ID')
title = models.CharField(max_length=200, blank=True, verbose_name='标题(后台用)')
description = models.TextField(blank=True, verbose_name='描述(后台用)')
strategy_type = models.CharField(max_length=20, choices=STRATEGY_CHOICES, default='once')
max_count = models.PositiveIntegerField(default=1, verbose_name='周期内最大展示次数')
reset_interval = models.CharField(max_length=20, blank=True,
choices=(('day',''),('week',''),('month',''),('year','')),
verbose_name='重置周期count类型时使用')
duration = models.PositiveIntegerField(default=0, verbose_name='强制阅读秒数0=无强制)')
force_even_muted = models.BooleanField(default=False, verbose_name='强制弹出(忽略用户今日屏蔽)')
sort_order = models.PositiveIntegerField(default=0, verbose_name='展示顺序')
is_active = models.BooleanField(default=True, verbose_name='是否启用')
start_time = models.DateTimeField(null=True, blank=True, verbose_name='生效开始时间')
end_time = models.DateTimeField(null=True, blank=True, verbose_name='生效结束时间')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'popup_config'
verbose_name = '弹窗配置'
verbose_name_plural = '弹窗配置'
unique_together = ('page', 'popup_id')
ordering = ['sort_order', 'id']
def __str__(self):
return f'{self.page.name} - {self.popup_id}'
class PopupImage(QModel):
popup_config = models.ForeignKey(PopupConfig, on_delete=models.CASCADE, related_name='images')
image_url = models.CharField(max_length=500, verbose_name='图片相对路径')
text = models.TextField(blank=True, verbose_name='图片上方文字')
sort_order = models.PositiveIntegerField(default=0, verbose_name='展示顺序')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'popup_image'
verbose_name = '弹窗图片'
verbose_name_plural = '弹窗图片'
ordering = ['sort_order', 'id']
def __str__(self):
return f'{self.popup_config.popup_id} - 图片{self.sort_order}'
class ClubMiniappIcon(QModel):
"""小程序功能图标(按俱乐部,固定 icon_key + 存储桶相对路径)。"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
icon_key = models.CharField(max_length=64, db_index=True, verbose_name='图标键')
label = models.CharField(max_length=100, blank=True, default='', verbose_name='展示名称')
image_path = models.CharField(max_length=500, blank=True, default='', verbose_name='图片相对路径')
description = models.CharField(max_length=300, blank=True, default='', verbose_name='说明')
is_active = models.BooleanField(default=True, verbose_name='是否启用')
is_uploaded = models.BooleanField(default=False, verbose_name='是否已在后台上传')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'club_miniapp_icon'
verbose_name = '小程序图标'
verbose_name_plural = verbose_name
unique_together = [['club_id', 'icon_key']]
ordering = ['icon_key']
def __str__(self):
return f'{self.club_id}:{self.icon_key}'
class ClubPindao(QModel):
"""俱乐部频道配置(每俱乐部一条)。"""
club_id = models.CharField(max_length=16, default='xq', unique=True, verbose_name='俱乐部ID')
channel_no = models.CharField(max_length=100, blank=True, default='', verbose_name='频道号')
title = models.CharField(max_length=100, blank=True, default='频道', verbose_name='标题')
is_active = models.BooleanField(default=True, verbose_name='是否启用')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'club_pindao'
verbose_name = '俱乐部频道'
verbose_name_plural = verbose_name
def __str__(self):
return f'{self.club_id} 频道'
class ClubPindaoImage(QModel):
"""频道展示图片(多张)。"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
pindao = models.ForeignKey(
ClubPindao, on_delete=models.CASCADE, related_name='images', null=True, blank=True,
)
image_path = models.CharField(max_length=500, verbose_name='图片相对路径')
sort_order = models.PositiveIntegerField(default=0, verbose_name='排序')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
db_table = 'club_pindao_image'
verbose_name = '频道图片'
verbose_name_plural = verbose_name
ordering = ['sort_order', 'id']
def __str__(self):
return f'{self.club_id} 频道图{self.sort_order}'

View File

@@ -1,85 +1,43 @@
"""
阿龙电竞 - 收支记录定时任务
处理收支记录表的每日清零
固定使用ID=1的记录
"""
from celery import shared_task
from django.db import transaction
from django.utils import timezone
from datetime import datetime
from decimal import Decimal
import logging
# 导入模型
from config.models import Szjilu
from utils.celery_utils import log_task_execution, rollback_on_failure
logger = logging.getLogger(__name__)
@shared_task
@rollback_on_failure("收支记录每日清零")
def daily_sz_reset_task():
"""
收支记录每日清零 - 每天凌晨0点5分执行
清零收支记录表的今日相关字段固定使用ID=1的记录
"""
try:
with transaction.atomic():
# 获取今天的日期
today = datetime.now().strftime('%Y-%m-%d')
logger.info(f"开始执行收支记录清零任务,日期: {today}")
# 查询ID=1的收支记录如果不存在则创建
try:
szjilu = Szjilu.objects.get(id=1)
logger.info(f"找到ID=1的收支记录")
except Szjilu.DoesNotExist:
# 创建ID=1的初始记录
szjilu = Szjilu.objects.create(
id=1,
zongsy=Decimal('0.00'),
zongls=Decimal('0.00'),
zongzc=Decimal('0.00'),
jrzc=Decimal('0.00'),
jrls=Decimal('0.00')
)
logger.info(f"创建ID=1的收支记录")
# 记录清零前的值(用于日志)
old_jrzc = szjilu.jrzc
old_jrls = szjilu.jrls
# 清零今日字段
szjilu.jrzc = Decimal('0.00')
szjilu.jrls = Decimal('0.00')
szjilu.save(update_fields=['jrzc', 'jrls', 'update_time'])
logger.info(f"收支记录清零完成,今日支出原值: {old_jrzc},今日流水原值: {old_jrls}")
result_msg = f"收支记录清零完成 - 日期: {today}, 记录ID: {szjilu.id}"
log_task_execution("收支记录每日清零", True, result_msg)
return {
"success": True,
"date": today,
"record_id": szjilu.id,
"jrzc_cleared": True,
"jrls_cleared": True,
"old_jrzc": str(old_jrzc),
"old_jrls": str(old_jrls),
"message": result_msg
}
except Exception as e:
error_msg = f"收支记录每日清零失败: {str(e)}"
logger.error(error_msg)
log_task_execution("收支记录每日清零", False, error_msg)
return {
"success": False,
"error": str(e),
"date": datetime.now().strftime('%Y-%m-%d'),
"message": error_msg
"""
收支记录定时任务:每日清零各俱乐部今日流水/今日支出。
"""
from celery import shared_task
from datetime import datetime
import logging
from jituan.services.szjilu_accounting import reset_all_daily_szjilu
from utils.celery_utils import log_task_execution, rollback_on_failure
logger = logging.getLogger(__name__)
@shared_task
@rollback_on_failure("收支记录每日清零")
def daily_sz_reset_task():
"""收支记录每日清零 - 每天凌晨0点5分执行。"""
try:
today = datetime.now().strftime('%Y-%m-%d')
logger.info(f"开始执行收支记录清零任务,日期: {today}")
updated = reset_all_daily_szjilu()
result_msg = f"收支记录清零完成 - 日期: {today}, 俱乐部记录数: {updated}"
log_task_execution("收支记录每日清零", True, result_msg)
return {
"success": True,
"date": today,
"clubs_updated": updated,
"message": result_msg,
}
except Exception as e:
error_msg = f"收支记录每日清零失败: {str(e)}"
logger.error(error_msg)
log_task_execution("收支记录每日清零", False, error_msg)
return {
"success": False,
"error": str(e),
"date": datetime.now().strftime('%Y-%m-%d'),
"message": error_msg,
}

View File

@@ -3,8 +3,8 @@ from django.urls import path
from .views import ShangpinGonggaoView, AdminUpdateConfigView, AdminUploadImageView, AdminConfigQueryView, \
DashouPaihangView,GetAdminZhanghaoView, GetXiugaiJiluView,ShangjiaTianjiaMobanView, ShangjiaMobanListView, \
ShangjiaGengxinMobanView, ShangjiaShanchuMobanView, ShangjiaGenerateLinkView,KehuGetDingdanLianjieView, \
KehuTianxieDingdanView,GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\
GetDynamicConfigView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\
KehuTianxieDingdanView, GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\
GetDynamicConfigView, HaibaoPeizhiView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\
ShangjiaLianjieListView
@@ -32,6 +32,7 @@ urlpatterns = [
path('guanshiewm', GuanshiQRCodeView.as_view(), name='管事生成二维码'),
path('zuzhanghb', ZuzhangHaibaoView.as_view(), name='组长二维码'),
path('qdpzhq', GetDynamicConfigView.as_view(), name='小程序获取配置'),
path('haibaopz', HaibaoPeizhiView.as_view(), name='推广海报配置'),
path('tanchuang', PopupConfigView.as_view(), name='微信小程序获取弹窗数据'),
path('hqtxzsym', GetWithdrawModeView.as_view(), name='小程序获取提现方式'),

File diff suppressed because it is too large Load Diff

82
create_rbac_tables.sql Normal file
View File

@@ -0,0 +1,82 @@
-- ============================================================
-- RBAC 表创建脚本
-- 仅创建生产数据库中缺失的表,已存在的表会被跳过
-- 执行方式: mysql -u root -p xaio_cheng_xu < create_rbac_tables.sql
-- 注意: 索引和外键单独处理,避免重复创建报错
-- ============================================================
-- ============================================================
-- Part 1: backend RBAC 表小写表名verify_kefu_permission 使用)
-- ============================================================
CREATE TABLE IF NOT EXISTS `role` (
`role_id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`role_code` varchar(50) NOT NULL UNIQUE,
`role_name` varchar(50) NOT NULL,
`description` varchar(200) NOT NULL,
`create_time` datetime(6) NOT NULL,
`update_time` datetime(6) NOT NULL
);
CREATE TABLE IF NOT EXISTS `permission` (
`perm_id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`perm_code` varchar(100) NOT NULL UNIQUE,
`perm_name` varchar(100) NOT NULL,
`description` varchar(200) NOT NULL,
`create_time` datetime(6) NOT NULL,
`update_time` datetime(6) NOT NULL
);
CREATE TABLE IF NOT EXISTS `user_role` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`account_id` varchar(11) NOT NULL,
`create_time` datetime(6) NOT NULL,
`update_time` datetime(6) NOT NULL,
`role_id` integer NOT NULL,
UNIQUE KEY `user_role_account_id_role_id_uniq` (`account_id`, `role_id`)
);
CREATE TABLE IF NOT EXISTS `role_permission` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`create_time` datetime(6) NOT NULL,
`permission_id` integer NOT NULL,
`role_id` integer NOT NULL,
UNIQUE KEY `role_permission_role_id_permission_id_uniq` (`role_id`, `permission_id`)
);
-- backend 表索引(忽略已存在的错误)
CREATE INDEX `user_role_account_id_idx` ON `user_role` (`account_id`);
-- backend 外键约束
ALTER TABLE `user_role` ADD CONSTRAINT `user_role_role_fk` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`);
ALTER TABLE `role_permission` ADD CONSTRAINT `role_permission_permission_fk` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`perm_id`);
ALTER TABLE `role_permission` ADD CONSTRAINT `role_permission_role_fk` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`);
-- ============================================================
-- Part 2: gvsdsdk RBAC 表大写表名UUID 主键)
-- ============================================================
CREATE TABLE IF NOT EXISTS `AccountActivation` (`AccountActivationCode` BINARY(6) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `EphemeralToken` (`ELTUUID` BINARY(16) NOT NULL PRIMARY KEY, `ELTUserUUID` BINARY(16) NOT NULL, `ELTCreateTime` datetime(6) NOT NULL, `ELTEndTime` datetime(6) NOT NULL, `ELTType` smallint NOT NULL, `ELTAllowIP` json NOT NULL, `ELTStatus` smallint NOT NULL);
CREATE TABLE IF NOT EXISTS `Password` (`UserUUID` BINARY(16) NOT NULL PRIMARY KEY, `Password` varchar(1024) NOT NULL);
CREATE TABLE IF NOT EXISTS `Permission` (`PermUUID` BINARY(16) NOT NULL PRIMARY KEY, `TenantUUID` BINARY(16) NOT NULL, `PermName` varchar(128) NOT NULL, `PermCode` varchar(128) NOT NULL, `PermCategory` varchar(64) NOT NULL, `PermStatus` smallint NOT NULL, `PermDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `Position` (`PositionUUID` BINARY(16) NOT NULL PRIMARY KEY, `TenantUUID` BINARY(16) NOT NULL, `PositionName` varchar(128) NOT NULL, `PositionCode` varchar(64) NOT NULL, `PositionLevel` smallint NOT NULL, `PositionStatus` smallint NOT NULL, `PositionDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL, `UpdateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `PositionTemplate` (`PositionTemplateUUID` BINARY(16) NOT NULL PRIMARY KEY, `TenantUUID` BINARY(16) NOT NULL, `TemplateName` varchar(128) NOT NULL, `TemplateCode` varchar(64) NOT NULL, `TemplateStatus` smallint NOT NULL, `TemplateDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL, `UpdateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `PositionTemplatePermission` (`PTPermUUID` BINARY(16) NOT NULL PRIMARY KEY, `PositionTemplateUUID` BINARY(16) NOT NULL, `PermUUID` BINARY(16) NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `Role` (`RoleUUID` BINARY(16) NOT NULL PRIMARY KEY, `TenantUUID` BINARY(16) NOT NULL, `RoleName` varchar(128) NOT NULL, `RoleType` smallint NOT NULL, `RoleStatus` smallint NOT NULL, `AssignScope` smallint NOT NULL, `AssignScopeUUID` BINARY(16) NULL, `RoleDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `RolePermission` (`RolePermUUID` BINARY(16) NOT NULL PRIMARY KEY, `RoleUUID` BINARY(16) NOT NULL, `PermUUID` BINARY(16) NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `RoleShop` (`RoleShopUUID` BINARY(16) NOT NULL PRIMARY KEY, `RoleUUID` BINARY(16) NOT NULL, `ShopUUID` BINARY(16) NOT NULL, `IsMainShop` smallint NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `Shop` (`ShopUUID` BINARY(16) NOT NULL PRIMARY KEY, `CompanyUUID` BINARY(16) NOT NULL, `ShopName` varchar(128) NOT NULL, `ShopCode` varchar(64) NOT NULL, `ShopType` smallint NOT NULL, `ShopStatus` smallint NOT NULL, `ShopAddress` varchar(512) NOT NULL, `ShopContact` varchar(128) NOT NULL, `ShopPhone` varchar(32) NOT NULL, `ShopDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL, `UpdateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `User2FA` (`User2FAUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `SecretKey` varchar(64) NOT NULL, `IsEnabled` smallint NOT NULL, `Verified` smallint NOT NULL, `LastUsedAt` datetime(6) NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `UserABAC` (`UserABACUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `HierarchicalPermissionIdentifiers` varchar(256) NOT NULL);
CREATE TABLE IF NOT EXISTS `UserAvatar` (`UserUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserAvatar` longblob NOT NULL, `AvatarUploadTime` datetime(6) NOT NULL, `AvatarFileType` varchar(16) NOT NULL);
CREATE TABLE IF NOT EXISTS `UserDept` (`UserDeptUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `DeptUUID` BINARY(16) NOT NULL, `PositionUUID` BINARY(16) NULL, `IsMainDept` smallint NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `UserLoginLog` (`LogUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `LoginIP` varchar(39) NOT NULL, `LoginPlatform` varchar(32) NOT NULL, `LoginStatus` smallint NOT NULL, `LoginTime` datetime(6) NOT NULL, `UserAgent` varchar(512) NOT NULL);
CREATE TABLE IF NOT EXISTS `UserPending2FA` (`PendingUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL UNIQUE, `SecretKey` varchar(64) NOT NULL, `ExpireAt` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `UserRole` (`UserRoleUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `RoleUUID` BINARY(16) NOT NULL, `CompanyUUID` BINARY(16) NOT NULL, `CreateTime` datetime(6) NOT NULL);
-- gvsdsdk 索引
CREATE INDEX `EphemELTUserUUID_idx` ON `EphemeralToken` (`ELTUserUUID`);
CREATE INDEX `EphemELTStatus_idx` ON `EphemeralToken` (`ELTStatus`);
CREATE INDEX `User2FA_UserUUID_idx` ON `User2FA` (`UserUUID`);
CREATE INDEX `UserLoginLog_UserUUID_idx` ON `UserLoginLog` (`UserUUID`);

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,73 @@
-- 部署 jituan 前若 migrate 报 Duplicate entry 唯一约束冲突时使用。
-- 不要用 mysql 命令行1Panel MySQL 不在本机 socket请用
-- python manage.py dbshell < deploy/pre_migrate_dedupe.sql
-- 或半路失败的 config.0006
-- python manage.py recover_config_0006
-- python manage.py migrate config 0006 --fake
-- python manage.py migrate
-- config.0006 tupianpeizhi
DELETE FROM tupianpeizhi
WHERE id NOT IN (
SELECT keep_id FROM (
SELECT MAX(id) AS keep_id FROM tupianpeizhi GROUP BY club_id, ImageType
) AS t
);
-- config.0006 popup_page
DELETE FROM popup_page
WHERE id NOT IN (
SELECT keep_id FROM (
SELECT MAX(id) AS keep_id FROM popup_page GROUP BY club_id, page_key
) AS t
);
-- products.0005 duoci_fenhong
DELETE FROM duoci_fenhong
WHERE id NOT IN (
SELECT keep_id FROM (
SELECT MAX(id) AS keep_id FROM duoci_fenhong GROUP BY club_id, huiyuan, yonghuid, cishu
) AS t
);
-- backend.0003 提现日统计
DELETE FROM tixian_ri_tongji
WHERE id NOT IN (
SELECT keep_id FROM (
SELECT MAX(id) AS keep_id FROM tixian_ri_tongji GROUP BY club_id, Date, WithdrawalType
) AS t
);
-- backend.0004 角色日统计
DELETE FROM shangjia_ri_tongji
WHERE id NOT IN (
SELECT keep_id FROM (
SELECT MAX(id) AS keep_id FROM shangjia_ri_tongji GROUP BY club_id, MerchantID, Date
) AS t
);
DELETE FROM dashou_ri_tongji
WHERE id NOT IN (
SELECT keep_id FROM (
SELECT MAX(id) AS keep_id FROM dashou_ri_tongji GROUP BY club_id, PlayerID, Date
) AS t
);
DELETE FROM zuzhang_ri_tongji
WHERE id NOT IN (
SELECT keep_id FROM (
SELECT MAX(id) AS keep_id FROM zuzhang_ri_tongji GROUP BY club_id, LeaderID, Date
) AS t
);
DELETE FROM guanshi_ri_tongji
WHERE id NOT IN (
SELECT keep_id FROM (
SELECT MAX(id) AS keep_id FROM guanshi_ri_tongji GROUP BY club_id, ManagerID, Date
) AS t
);
DELETE FROM guanshi_xufei_ri_tongji
WHERE id NOT IN (
SELECT keep_id FROM (
SELECT MAX(id) AS keep_id FROM guanshi_xufei_ri_tongji GROUP BY club_id, ManagerID, Date
) AS t
);
-- szjilu 应只有 1 行;多行时勿自动删,需人工核对 TotalIncome 后处理

163
find.py
View File

@@ -1,163 +0,0 @@
"""
REPL 模式文件内容搜索工具
支持正则或纯文本搜索,默认搜索当前目录
"""
import os
import re
import sys
# 硬编码排除目录
EXCLUDE_DIRS = {
'.git', '.vs', 'node_modules', '__pycache__', '.idea',
'miniprogram_npm', '_backup_pre_redesign',
}
# 排除的文件扩展名
EXCLUDE_EXTS = {
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.bmp',
'.mp3', '.mp4', '.wav', '.avi',
'.zip', '.rar', '.7z', '.tar', '.gz',
'.exe', '.dll', '.so', '.dylib',
'.pyc', '.pyo', '.class', '.o', '.obj',
'.db', '.sqlite', '.vsidx',
}
# 默认搜索根目录
DEFAULT_ROOT = os.path.dirname(os.path.abspath(__file__))
def should_skip(dirpath):
"""判断目录是否应跳过"""
parts = dirpath.replace('\\', '/').split('/')
return any(p in EXCLUDE_DIRS for p in parts)
def search(pattern, root=DEFAULT_ROOT, use_regex=True, max_results=50):
"""搜索文件内容"""
try:
if use_regex:
compiled = re.compile(pattern, re.IGNORECASE)
except re.error as e:
print(f' 正则语法错误: {e}')
return
count = 0
for dirpath, dirnames, filenames in os.walk(root):
# 就地修改 dirnames 以跳过排除目录
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
for fname in filenames:
ext = os.path.splitext(fname)[1].lower()
if ext in EXCLUDE_EXTS:
continue
fpath = os.path.join(dirpath, fname)
relpath = os.path.relpath(fpath, root)
try:
with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
for lineno, line in enumerate(f, 1):
line_stripped = line.rstrip('\n\r')
if use_regex:
match = compiled.search(line_stripped)
if match:
highlight = line_stripped[:match.start()] + \
f'>>> {match.group()} <<<' + \
line_stripped[match.end():]
print(f' {relpath}:{lineno} {highlight}')
count += 1
else:
if pattern.lower() in line_stripped.lower():
print(f' {relpath}:{lineno} {line_stripped}')
count += 1
if count >= max_results:
print(f'\n ... 已达上限 {max_results} 条,停止')
return
except (PermissionError, OSError):
continue
if count == 0:
print(' 未找到匹配内容')
else:
print(f'\n{count} 条匹配')
def main():
print('=== 文件内容搜索工具 ===')
print(f'搜索根目录: {DEFAULT_ROOT}')
print(f'排除目录: {EXCLUDE_DIRS}')
print()
print('命令:')
print(' /regex <pattern> 正则搜索')
print(' /text <text> 纯文本搜索')
print(' /max <n> 设置结果上限(默认50)')
print(' /dir <path> 切换搜索目录')
print(' /help 显示帮助')
print(' /quit 退出')
print()
print('直接输入内容默认使用正则搜索')
print()
root = DEFAULT_ROOT
use_regex = True
max_results = 50
while True:
try:
raw = input('find> ').strip()
except (EOFError, KeyboardInterrupt):
print('\n再见')
break
if not raw:
continue
if raw == '/quit':
print('再见')
break
if raw == '/help':
print(' /regex <pattern> 正则搜索')
print(' /text <text> 纯文本搜索')
print(' /max <n> 设置结果上限')
print(' /dir <path> 切换搜索目录')
print(' /help 显示帮助')
print(' /quit 退出')
print(' 直接输入内容默认使用正则搜索')
continue
if raw.startswith('/regex '):
pattern = raw[7:].strip()
if pattern:
search(pattern, root, use_regex=True, max_results=max_results)
continue
if raw.startswith('/text '):
pattern = raw[6:].strip()
if pattern:
search(pattern, root, use_regex=False, max_results=max_results)
continue
if raw.startswith('/max '):
try:
max_results = int(raw[5:].strip())
print(f' 结果上限设为 {max_results}')
except ValueError:
print(' 请输入数字')
continue
if raw.startswith('/dir '):
new_dir = raw[5:].strip()
if os.path.isdir(new_dir):
root = os.path.abspath(new_dir)
print(f' 搜索目录切换为: {root}')
else:
print(f' 目录不存在: {new_dir}')
continue
# 默认正则搜索
search(raw, root, use_regex=use_regex, max_results=max_results)
if __name__ == '__main__':
main()

View File

@@ -1,35 +0,0 @@
"""
修复 django_migrations 表中的旧 app 名称引用。
在服务器上执行: python manage.py shell < fix_migration_history.py
或: python manage.py shell
然后粘贴此脚本内容
"""
from django.db import connection
APP_NAME_MAPPINGS = {
'houtai': 'backend',
'yonghu': 'users',
'dingdan': 'orders',
'peizhi': 'config',
'shangpin': 'products',
'shangdian': 'shop',
'dengji': 'rank',
}
with connection.cursor() as cursor:
for old_name, new_name in APP_NAME_MAPPINGS.items():
cursor.execute(
"SELECT COUNT(*) FROM django_migrations WHERE app = %s",
[old_name]
)
count = cursor.fetchone()[0]
if count > 0:
cursor.execute(
"UPDATE django_migrations SET app = %s WHERE app = %s",
[new_name, old_name]
)
print(f"已更新 {count} 条记录: {old_name} -> {new_name}")
else:
print(f"无需更新: {old_name} (无记录)")
print("\n修复完成!请运行 python manage.py migrate")

View File

@@ -13,7 +13,8 @@ class ELTAuthentication:
except (ValueError, AttributeError):
return None
from gvsdsdk.models import User, EphemeralToken
from gvsdsdk.models import EphemeralToken
from gvsds.apps.core.models import User
import datetime
try:

54
gvsdsdk/auth/jwt_auth.py Normal file
View File

@@ -0,0 +1,54 @@
"""自定义 JWT 认证 — 向后兼容旧 token 的 user_id claim"""
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import InvalidToken, AuthenticationFailed
from rest_framework_simplejwt.settings import api_settings
from django.utils.translation import gettext_lazy as _
class CompatibleJWTAuthentication(JWTAuthentication):
"""
兼容旧 token 的 JWT 认证类。
旧 tokenUSER_ID_CLAIM='user_id' 时生成payload 中使用 'user_id' claim
值为 user.idUUID 字符串)。
新 tokenUSER_ID_CLAIM='UserUID' 时生成payload 中使用 'UserUID' claim
值为 user.UserUID7位业务ID
此类先尝试新 claim找不到时回退到旧 claim并用对应字段查库。
"""
def get_user(self, validated_token):
# 1. 先尝试当前配置的 USER_ID_CLAIM
user_id = validated_token.get(api_settings.USER_ID_CLAIM)
lookup_field = api_settings.USER_ID_FIELD
if user_id is None:
# 2. 回退:尝试旧 token 中的 'user_id' claim
user_id = validated_token.get('user_id')
if user_id is None:
raise InvalidToken(
{'detail': _('令牌未包含用户标识符'), 'code': 'token_not_valid'}
)
# 旧 token 的 user_id 来自 user.id即 UserUUID
# 需要用 UserUUID 字段查库
lookup_field = 'UserUUID'
try:
user = self.user_model.objects.get(**{lookup_field: user_id})
except self.user_model.DoesNotExist:
raise AuthenticationFailed(_('用户不存在'), code='user_not_found')
if not user.is_active:
raise AuthenticationFailed(_('用户已被禁用'), code='user_inactive')
if api_settings.CHECK_REVOKE_TOKEN:
from rest_framework_simplejwt.utils import get_md5_hash_password
if validated_token.get(
api_settings.REVOKE_TOKEN_CLAIM
) != get_md5_hash_password(user.password):
raise AuthenticationFailed(
_('用户密码已更改'), code='password_changed'
)
return user

View File

@@ -87,32 +87,39 @@ class RoleManager:
@staticmethod
def GetUsersForRole(RoleUUID):
"""获取拥有指定角色的所有用户"""
from gvsdsdk.models import User
from gvsds.apps.core.models import User
user_uuids = UserRole.objects.filter(RoleUUID=RoleUUID).values_list('UserUUID', flat=True)
return User.objects.filter(UserUUID__in=user_uuids)
@staticmethod
def AssignPermission(RoleUUID, PermUUID):
"""为角色分配权限"""
def AssignPermission(RoleUUID, PermUUID, CompanyUUID):
"""为角色分配权限(指定公司)"""
rp = RolePermission(
RolePermUUID=uuid.uuid4().bytes,
RoleUUID=RoleUUID,
PermUUID=PermUUID,
CompanyUUID=CompanyUUID,
CreateTime=datetime.datetime.utcnow(),
)
rp.save()
return rp
@staticmethod
def RemovePermission(RoleUUID, PermUUID):
def RemovePermission(RoleUUID, PermUUID, CompanyUUID=None):
"""移除角色的指定权限"""
deleted, _ = RolePermission.objects.filter(RoleUUID=RoleUUID, PermUUID=PermUUID).delete()
qs = RolePermission.objects.filter(RoleUUID=RoleUUID, PermUUID=PermUUID)
if CompanyUUID is not None:
qs = qs.filter(CompanyUUID=CompanyUUID)
deleted, _ = qs.delete()
return deleted > 0
@staticmethod
def GetPermissionsForRole(RoleUUID):
"""获取角色的所有权限"""
perm_uuids = RolePermission.objects.filter(RoleUUID=RoleUUID).values_list('PermUUID', flat=True)
def GetPermissionsForRole(RoleUUID, CompanyUUID=None):
"""获取角色的所有权限(可按公司过滤)"""
qs = RolePermission.objects.filter(RoleUUID=RoleUUID)
if CompanyUUID is not None:
qs = qs.filter(CompanyUUID=CompanyUUID)
perm_uuids = qs.values_list('PermUUID', flat=True)
return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1)

View File

@@ -1,4 +1,5 @@
from gvsdsdk.models import User, UserRole, Role, RolePermission, Permission, UserABAC
from gvsds.apps.core.models import User
from gvsdsdk.models import UserRole, Role, RolePermission, Permission, UserABAC
from rest_framework.permissions import BasePermission
@@ -48,7 +49,7 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
"""权限验证器——混合 ABAC + RBAC 检查
RBAC 仅检查当前角色CurrentRoleUUID Cookie 指定的角色)的权限,
通过 RolePermission → Permission 链路查询。
通过 RolePermission → Permission 链路查询,按当前公司过滤
超级管理员/员工直接放行。
"""
if not permission:
@@ -63,6 +64,7 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
return True
current_role = getattr(request, 'current_role', None) if request else None
current_company = getattr(request, 'current_company_uuid', None) if request else None
for attr in attributes:
# ABAC 检查
@@ -73,13 +75,16 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
if abac_match:
continue
# RBAC 检查:仅检查当前角色
# RBAC 检查:仅检查当前角色,按公司过滤权限
if current_role and isinstance(current_role, Role):
if current_role.RoleStatus != 1:
return False
perm_uuids = RolePermission.objects.filter(
rp_qs = RolePermission.objects.filter(
RoleUUID=current_role.RoleUUID
).values_list('PermUUID', flat=True)
)
if current_company:
rp_qs = rp_qs.filter(CompanyUUID=current_company)
perm_uuids = rp_qs.values_list('PermUUID', flat=True)
rbac_result = Permission.objects.filter(
PermUUID__in=perm_uuids,
PermCode=attr,
@@ -94,9 +99,12 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
RoleUUID__in=user_role_uuids,
RoleStatus=1,
).values_list('RoleUUID', flat=True)
perm_uuids = RolePermission.objects.filter(
rp_qs = RolePermission.objects.filter(
RoleUUID__in=active_role_uuids
).values_list('PermUUID', flat=True)
)
if current_company:
rp_qs = rp_qs.filter(CompanyUUID=current_company)
perm_uuids = rp_qs.values_list('PermUUID', flat=True)
rbac_result = Permission.objects.filter(
PermUUID__in=perm_uuids,
PermCode=attr,

View File

@@ -1,352 +1,352 @@
"""模块:分佣 SAAS 数据模型
SAAS 级分佣引擎——事件驱动、规则外置、参与方可配置。
子服务只管说"发生了什么"SAAS 负责"怎么分""""
import uuid
import datetime
from django.db import models
from gvsdsdk.model_base import QModel
from gvsdsdk.models import UUIDField
class CommissionEvent(QModel):
"""分佣事件表——子服务触发的分佣事件
子服务在业务发生时(订单完成、会员购买、充值到账等)
调用 CommissionEngine.Dispatch() 写入此表。
"""
EVT_ORDER_COMPLETE = 'order_complete'
EVT_MEMBER_PURCHASE = 'member_purchase'
EVT_RECHARGE = 'recharge'
EVT_PENALTY = 'penalty'
EVT_REGISTER = 'register'
EVT_CUSTOM = 'custom'
EVT_TYPE_CHOICES = [
(EVT_ORDER_COMPLETE, '订单完成'),
(EVT_MEMBER_PURCHASE, '会员购买'),
(EVT_RECHARGE, '充值到账'),
(EVT_PENALTY, '罚款缴纳'),
(EVT_REGISTER, '用户注册'),
(EVT_CUSTOM, '自定义事件'),
]
EVT_PENDING = 0
EVT_PROCESSING = 1
EVT_SETTLED = 2
EVT_FAILED = 3
EVT_STATUS_CHOICES = [
(EVT_PENDING, '待处理'),
(EVT_PROCESSING, '处理中'),
(EVT_SETTLED, '已结算'),
(EVT_FAILED, '结算失败'),
]
EventUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
EventType = models.CharField(max_length=32, choices=EVT_TYPE_CHOICES)
EventKey = models.CharField(max_length=128)
TotalAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
SourceUserUUID = UUIDField(null=True, blank=True)
EventPayload = models.JSONField(default=dict)
Participants = models.JSONField(default=dict)
EventStatus = models.SmallIntegerField(default=EVT_PENDING, choices=EVT_STATUS_CHOICES)
EventTime = models.DateTimeField(default=datetime.datetime.utcnow)
ProcessTime = models.DateTimeField(null=True, blank=True)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
class Meta:
db_table = 'CommissionEvent'
app_label = 'gvsdsdk'
verbose_name = '分佣事件'
verbose_name_plural = '分佣事件'
unique_together = [('TenantUUID', 'EventKey')]
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'EventType', 'EventStatus'], name='idx_cmevt_tenant_type_status'),
models.Index(fields=['EventKey'], name='idx_cmevt_event_key'),
models.Index(fields=['CreateTime'], name='idx_cmevt_create_time'),
]
def __str__(self):
return f'{self.EventType}:{self.EventKey}'
@property
def IsSettled(self):
return self.EventStatus == self.EVT_SETTLED
def MarkProcessing(self):
self.EventStatus = self.EVT_PROCESSING
self.save(update_fields=['EventStatus'])
def MarkSettled(self):
self.EventStatus = self.EVT_SETTLED
self.ProcessTime = datetime.datetime.utcnow()
self.save(update_fields=['EventStatus', 'ProcessTime'])
def MarkFailed(self):
self.EventStatus = self.EVT_FAILED
self.ProcessTime = datetime.datetime.utcnow()
self.save(update_fields=['EventStatus', 'ProcessTime'])
class RateTable(QModel):
"""费率表——可复用的费率配置
每个租户可创建多张费率表,不同事件类型绑定不同的表。
例如:打手分成费率表、提现手续费率表、罚款分红费率表。
"""
RateTableUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
TableCode = models.CharField(max_length=64)
TableName = models.CharField(max_length=128)
TableDescription = models.CharField(max_length=512, default='')
TableStatus = models.SmallIntegerField(default=1)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
UpdateTime = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'CommissionRateTable'
app_label = 'gvsdsdk'
verbose_name = '费率表'
verbose_name_plural = '费率表'
unique_together = [('TenantUUID', 'TableCode')]
managed = False
indexes = [
models.Index(fields=['TenantUUID'], name='idx_cmrate_tenant'),
]
def __str__(self):
return f'{self.TableCode}:{self.TableName}'
def GetRate(self, key):
try:
entry = self.Entries.get(RateKey=key, EntryStatus=1)
return entry.RateValue
except RateEntry.DoesNotExist:
return None
def GetRates(self):
return {
e.RateKey: e.RateValue
for e in self.Entries.filter(EntryStatus=1)
}
class RateEntry(QModel):
"""费率条目——费率表中的一行"""
EntryUUID = UUIDField(primary_key=True)
RateTable = models.ForeignKey(
RateTable, on_delete=models.CASCADE,
related_name='Entries',
)
RateKey = models.CharField(max_length=64)
RateLabel = models.CharField(max_length=128, default='')
RateValue = models.DecimalField(max_digits=10, decimal_places=6)
EntryStatus = models.SmallIntegerField(default=1)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
class Meta:
db_table = 'CommissionRateEntry'
app_label = 'gvsdsdk'
verbose_name = '费率条目'
verbose_name_plural = '费率条目'
unique_together = [('RateTable', 'RateKey')]
managed = False
indexes = [
models.Index(fields=['RateTable', 'RateKey'], name='idx_cmrentry_table_key'),
]
def __str__(self):
return f'{self.RateKey}={self.RateValue}'
class CommissionRule(QModel):
"""分佣规则表——定义"怎么分"
每条规则绑定一种事件类型,定义参与者如何分账。
支持比例分账、固定金额、阶梯分账、剩余归平台四种 SplitType。
"""
SPLT_PERCENTAGE = 'percentage'
SPLT_FIXED = 'fixed'
SPLT_TIERED = 'tiered'
SPLT_REMAINDER = 'remainder'
SPLT_TYPE_CHOICES = [
(SPLT_PERCENTAGE, '比例分账'),
(SPLT_FIXED, '固定金额'),
(SPLT_TIERED, '阶梯分账'),
(SPLT_REMAINDER, '剩余归平台'),
]
RuleUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
EventType = models.CharField(max_length=32, choices=CommissionEvent.EVT_TYPE_CHOICES)
RuleName = models.CharField(max_length=128)
RuleDescription = models.CharField(max_length=512, default='')
ParticipantRules = models.JSONField(default=list)
RateTable = models.ForeignKey(
RateTable, on_delete=models.SET_NULL,
null=True, blank=True, related_name='rules',
)
MaxInviteLevel = models.SmallIntegerField(default=2)
RulePriority = models.IntegerField(default=0)
RuleStatus = models.SmallIntegerField(default=1)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
UpdateTime = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'CommissionRule'
app_label = 'gvsdsdk'
verbose_name = '分佣规则'
verbose_name_plural = '分佣规则'
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'EventType', 'RuleStatus'], name='idx_cmrule_tenant_type_status'),
models.Index(fields=['RulePriority'], name='idx_cmrule_priority'),
]
def __str__(self):
return f'{self.RuleName}({self.EventType})'
def FindParticipantRule(self, role):
for pr in self.ParticipantRules:
if pr.get('role') == role:
return pr
return None
class CommissionRecord(QModel):
"""分佣记录表——不可变分账流水
每条记录代表一次分账结果:谁、从哪个事件、按什么规则、分了多少。
生成后即冻结,规则变更不影响历史记录。
"""
REC_PENDING = 0
REC_SETTLED = 1
REC_WITHDRAWN = 2
REC_CANCELLED = 3
REC_STATUS_CHOICES = [
(REC_PENDING, '待结算'),
(REC_SETTLED, '已结算'),
(REC_WITHDRAWN, '已提现'),
(REC_CANCELLED, '已取消'),
]
RecordUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
Event = models.ForeignKey(
CommissionEvent, on_delete=models.CASCADE,
related_name='records',
)
Rule = models.ForeignKey(
CommissionRule, on_delete=models.SET_NULL,
null=True, related_name='records',
)
ParticipantID = models.CharField(max_length=128)
ParticipantRole = models.CharField(max_length=64)
SplitType = models.CharField(max_length=16)
Amount = models.DecimalField(max_digits=18, decimal_places=4)
RuleSnapshot = models.JSONField(default=dict)
Status = models.SmallIntegerField(default=REC_PENDING, choices=REC_STATUS_CHOICES)
TierIndex = models.IntegerField(default=0)
SettleTime = models.DateTimeField(null=True, blank=True)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
class Meta:
db_table = 'CommissionRecord'
app_label = 'gvsdsdk'
verbose_name = '分佣记录'
verbose_name_plural = '分佣记录'
ordering = ['-CreateTime']
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'ParticipantID', 'Status'], name='idx_cmrec_tenant_part_status'),
models.Index(fields=['Event'], name='idx_cmrec_event'),
models.Index(fields=['CreateTime'], name='idx_cmrec_create_time'),
]
def __str__(self):
return f'{self.ParticipantRole}:{self.ParticipantID}={self.Amount}'
@property
def IsSettled(self):
return self.Status >= self.REC_SETTLED
def Settle(self):
self.Status = self.REC_SETTLED
self.SettleTime = datetime.datetime.utcnow()
self.save(update_fields=['Status', 'SettleTime'])
def Cancel(self):
self.Status = self.REC_CANCELLED
self.save(update_fields=['Status'])
class PreSettlement(QModel):
"""预结算表——跨平台订单待确认款项
当订单涉及外部平台时,打手分成先挂预结算,
对方确认后转为正式 CommissionRecord。
"""
PST_PENDING = 0
PST_CONFIRMED = 1
PST_REJECTED = 2
PST_STATUS_CHOICES = [
(PST_PENDING, '待确认'),
(PST_CONFIRMED, '已确认'),
(PST_REJECTED, '已拒绝'),
]
SettlementUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
EventKey = models.CharField(max_length=128)
ExternalPlatform = models.CharField(max_length=64, default='')
ExternalOrderID = models.CharField(max_length=128, default='')
ParticipantID = models.CharField(max_length=128)
ParticipantRole = models.CharField(max_length=64, default='worker')
Amount = models.DecimalField(max_digits=18, decimal_places=4)
OrderAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
Status = models.SmallIntegerField(default=PST_PENDING, choices=PST_STATUS_CHOICES)
ConfirmedBy = models.CharField(max_length=128, default='')
ConfirmDeadline = models.DateTimeField(null=True, blank=True)
ConfirmedAt = models.DateTimeField(null=True, blank=True)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
UpdateTime = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'CommissionPreSettlement'
app_label = 'gvsdsdk'
verbose_name = '预结算'
verbose_name_plural = '预结算'
unique_together = [('EventKey', 'ParticipantID')]
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'Status'], name='idx_cmpst_tenant_status'),
models.Index(fields=['EventKey'], name='idx_cmpst_event_key'),
models.Index(fields=['ExternalOrderID'], name='idx_cmpst_ext_order'),
]
def __str__(self):
return f'预结算:{self.EventKey}->{self.ParticipantID}={self.Amount}'
def Confirm(self, confirmed_by=''):
self.Status = self.PST_CONFIRMED
self.ConfirmedBy = confirmed_by
self.ConfirmedAt = datetime.datetime.utcnow()
self.save(update_fields=['Status', 'ConfirmedBy', 'ConfirmedAt'])
def Reject(self, confirmed_by=''):
self.Status = self.PST_REJECTED
self.ConfirmedBy = confirmed_by
self.ConfirmedAt = datetime.datetime.utcnow()
self.save(update_fields=['Status', 'ConfirmedBy', 'ConfirmedAt'])
"""模块:分佣 SAAS 数据模型
SAAS 级分佣引擎——事件驱动、规则外置、参与方可配置。
子服务只管说"发生了什么"SAAS 负责"怎么分""""
import uuid
import datetime
from django.db import models
from gvsdsdk.model_base import QModel
from gvsdsdk.models import UUIDField
class CommissionEvent(QModel):
"""分佣事件表——子服务触发的分佣事件
子服务在业务发生时(订单完成、会员购买、充值到账等)
调用 CommissionEngine.Dispatch() 写入此表。
"""
EVT_ORDER_COMPLETE = 'order_complete'
EVT_MEMBER_PURCHASE = 'member_purchase'
EVT_RECHARGE = 'recharge'
EVT_PENALTY = 'penalty'
EVT_REGISTER = 'register'
EVT_CUSTOM = 'custom'
EVT_TYPE_CHOICES = [
(EVT_ORDER_COMPLETE, '订单完成'),
(EVT_MEMBER_PURCHASE, '会员购买'),
(EVT_RECHARGE, '充值到账'),
(EVT_PENALTY, '罚款缴纳'),
(EVT_REGISTER, '用户注册'),
(EVT_CUSTOM, '自定义事件'),
]
EVT_PENDING = 0
EVT_PROCESSING = 1
EVT_SETTLED = 2
EVT_FAILED = 3
EVT_STATUS_CHOICES = [
(EVT_PENDING, '待处理'),
(EVT_PROCESSING, '处理中'),
(EVT_SETTLED, '已结算'),
(EVT_FAILED, '结算失败'),
]
EventUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
EventType = models.CharField(max_length=32, choices=EVT_TYPE_CHOICES)
EventKey = models.CharField(max_length=128)
TotalAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
SourceUserUUID = UUIDField(null=True, blank=True)
EventPayload = models.JSONField(default=dict)
Participants = models.JSONField(default=dict)
EventStatus = models.SmallIntegerField(default=EVT_PENDING, choices=EVT_STATUS_CHOICES)
EventTime = models.DateTimeField(default=datetime.datetime.utcnow)
ProcessTime = models.DateTimeField(null=True, blank=True)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
class Meta:
db_table = 'CommissionEvent'
app_label = 'gvsdsdk'
verbose_name = '分佣事件'
verbose_name_plural = '分佣事件'
unique_together = [('TenantUUID', 'EventKey')]
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'EventType', 'EventStatus'], name='idx_cmevt_tenant_type_status'),
models.Index(fields=['EventKey'], name='idx_cmevt_event_key'),
models.Index(fields=['CreateTime'], name='idx_cmevt_CreateTime'),
]
def __str__(self):
return f'{self.EventType}:{self.EventKey}'
@property
def IsSettled(self):
return self.EventStatus == self.EVT_SETTLED
def MarkProcessing(self):
self.EventStatus = self.EVT_PROCESSING
self.save(update_fields=['EventStatus'])
def MarkSettled(self):
self.EventStatus = self.EVT_SETTLED
self.ProcessTime = datetime.datetime.utcnow()
self.save(update_fields=['EventStatus', 'ProcessTime'])
def MarkFailed(self):
self.EventStatus = self.EVT_FAILED
self.ProcessTime = datetime.datetime.utcnow()
self.save(update_fields=['EventStatus', 'ProcessTime'])
class RateTable(QModel):
"""费率表——可复用的费率配置
每个租户可创建多张费率表,不同事件类型绑定不同的表。
例如:打手分成费率表、提现手续费率表、罚款分红费率表。
"""
RateTableUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
TableCode = models.CharField(max_length=64)
TableName = models.CharField(max_length=128)
TableDescription = models.CharField(max_length=512, default='')
TableStatus = models.SmallIntegerField(default=1)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
UpdateTime = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'CommissionRateTable'
app_label = 'gvsdsdk'
verbose_name = '费率表'
verbose_name_plural = '费率表'
unique_together = [('TenantUUID', 'TableCode')]
managed = False
indexes = [
models.Index(fields=['TenantUUID'], name='idx_cmrate_tenant'),
]
def __str__(self):
return f'{self.TableCode}:{self.TableName}'
def GetRate(self, key):
try:
entry = self.Entries.get(RateKey=key, EntryStatus=1)
return entry.RateValue
except RateEntry.DoesNotExist:
return None
def GetRates(self):
return {
e.RateKey: e.RateValue
for e in self.Entries.filter(EntryStatus=1)
}
class RateEntry(QModel):
"""费率条目——费率表中的一行"""
EntryUUID = UUIDField(primary_key=True)
RateTable = models.ForeignKey(
RateTable, on_delete=models.CASCADE,
related_name='Entries',
)
RateKey = models.CharField(max_length=64)
RateLabel = models.CharField(max_length=128, default='')
RateValue = models.DecimalField(max_digits=10, decimal_places=6)
EntryStatus = models.SmallIntegerField(default=1)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
class Meta:
db_table = 'CommissionRateEntry'
app_label = 'gvsdsdk'
verbose_name = '费率条目'
verbose_name_plural = '费率条目'
unique_together = [('RateTable', 'RateKey')]
managed = False
indexes = [
models.Index(fields=['RateTable', 'RateKey'], name='idx_cmrentry_table_key'),
]
def __str__(self):
return f'{self.RateKey}={self.RateValue}'
class CommissionRule(QModel):
"""分佣规则表——定义"怎么分"
每条规则绑定一种事件类型,定义参与者如何分账。
支持比例分账、固定金额、阶梯分账、剩余归平台四种 SplitType。
"""
SPLT_PERCENTAGE = 'percentage'
SPLT_FIXED = 'fixed'
SPLT_TIERED = 'tiered'
SPLT_REMAINDER = 'remainder'
SPLT_TYPE_CHOICES = [
(SPLT_PERCENTAGE, '比例分账'),
(SPLT_FIXED, '固定金额'),
(SPLT_TIERED, '阶梯分账'),
(SPLT_REMAINDER, '剩余归平台'),
]
RuleUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
EventType = models.CharField(max_length=32, choices=CommissionEvent.EVT_TYPE_CHOICES)
RuleName = models.CharField(max_length=128)
RuleDescription = models.CharField(max_length=512, default='')
ParticipantRules = models.JSONField(default=list)
RateTable = models.ForeignKey(
RateTable, on_delete=models.SET_NULL,
null=True, blank=True, related_name='rules',
)
MaxInviteLevel = models.SmallIntegerField(default=2)
RulePriority = models.IntegerField(default=0)
RuleStatus = models.SmallIntegerField(default=1)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
UpdateTime = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'CommissionRule'
app_label = 'gvsdsdk'
verbose_name = '分佣规则'
verbose_name_plural = '分佣规则'
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'EventType', 'RuleStatus'], name='idx_cmrule_tenant_type_status'),
models.Index(fields=['RulePriority'], name='idx_cmrule_priority'),
]
def __str__(self):
return f'{self.RuleName}({self.EventType})'
def FindParticipantRule(self, role):
for pr in self.ParticipantRules:
if pr.get('role') == role:
return pr
return None
class CommissionRecord(QModel):
"""分佣记录表——不可变分账流水
每条记录代表一次分账结果:谁、从哪个事件、按什么规则、分了多少。
生成后即冻结,规则变更不影响历史记录。
"""
REC_PENDING = 0
REC_SETTLED = 1
REC_WITHDRAWN = 2
REC_CANCELLED = 3
REC_STATUS_CHOICES = [
(REC_PENDING, '待结算'),
(REC_SETTLED, '已结算'),
(REC_WITHDRAWN, '已提现'),
(REC_CANCELLED, '已取消'),
]
RecordUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
Event = models.ForeignKey(
CommissionEvent, on_delete=models.CASCADE,
related_name='records',
)
Rule = models.ForeignKey(
CommissionRule, on_delete=models.SET_NULL,
null=True, related_name='records',
)
ParticipantID = models.CharField(max_length=128)
ParticipantRole = models.CharField(max_length=64)
SplitType = models.CharField(max_length=16)
Amount = models.DecimalField(max_digits=18, decimal_places=4)
RuleSnapshot = models.JSONField(default=dict)
Status = models.SmallIntegerField(default=REC_PENDING, choices=REC_STATUS_CHOICES)
TierIndex = models.IntegerField(default=0)
SettleTime = models.DateTimeField(null=True, blank=True)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
class Meta:
db_table = 'CommissionRecord'
app_label = 'gvsdsdk'
verbose_name = '分佣记录'
verbose_name_plural = '分佣记录'
ordering = ['-CreateTime']
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'ParticipantID', 'Status'], name='idx_cmrec_tenant_part_status'),
models.Index(fields=['Event'], name='idx_cmrec_event'),
models.Index(fields=['CreateTime'], name='idx_cmrec_CreateTime'),
]
def __str__(self):
return f'{self.ParticipantRole}:{self.ParticipantID}={self.Amount}'
@property
def IsSettled(self):
return self.Status >= self.REC_SETTLED
def Settle(self):
self.Status = self.REC_SETTLED
self.SettleTime = datetime.datetime.utcnow()
self.save(update_fields=['Status', 'SettleTime'])
def Cancel(self):
self.Status = self.REC_CANCELLED
self.save(update_fields=['Status'])
class PreSettlement(QModel):
"""预结算表——跨平台订单待确认款项
当订单涉及外部平台时,打手分成先挂预结算,
对方确认后转为正式 CommissionRecord。
"""
PST_PENDING = 0
PST_CONFIRMED = 1
PST_REJECTED = 2
PST_STATUS_CHOICES = [
(PST_PENDING, '待确认'),
(PST_CONFIRMED, '已确认'),
(PST_REJECTED, '已拒绝'),
]
SettlementUUID = UUIDField(primary_key=True)
TenantUUID = UUIDField()
EventKey = models.CharField(max_length=128)
ExternalPlatform = models.CharField(max_length=64, default='')
ExternalOrderID = models.CharField(max_length=128, default='')
ParticipantID = models.CharField(max_length=128)
ParticipantRole = models.CharField(max_length=64, default='worker')
Amount = models.DecimalField(max_digits=18, decimal_places=4)
OrderAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
Status = models.SmallIntegerField(default=PST_PENDING, choices=PST_STATUS_CHOICES)
ConfirmedBy = models.CharField(max_length=128, default='')
ConfirmDeadline = models.DateTimeField(null=True, blank=True)
ConfirmedAt = models.DateTimeField(null=True, blank=True)
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
UpdateTime = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'CommissionPreSettlement'
app_label = 'gvsdsdk'
verbose_name = '预结算'
verbose_name_plural = '预结算'
unique_together = [('EventKey', 'ParticipantID')]
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'Status'], name='idx_cmpst_tenant_status'),
models.Index(fields=['EventKey'], name='idx_cmpst_event_key'),
models.Index(fields=['ExternalOrderID'], name='idx_cmpst_ext_order'),
]
def __str__(self):
return f'预结算:{self.EventKey}->{self.ParticipantID}={self.Amount}'
def Confirm(self, confirmed_by=''):
self.Status = self.PST_CONFIRMED
self.ConfirmedBy = confirmed_by
self.ConfirmedAt = datetime.datetime.utcnow()
self.save(update_fields=['Status', 'ConfirmedBy', 'ConfirmedAt'])
def Reject(self, confirmed_by=''):
self.Status = self.PST_REJECTED
self.ConfirmedBy = confirmed_by
self.ConfirmedAt = datetime.datetime.utcnow()
self.save(update_fields=['Status', 'ConfirmedBy', 'ConfirmedAt'])

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
"""将 User 表中明文密码迁移为 bcrypt 哈希"""
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
User = get_user_model()
class Command(BaseCommand):
help = '将 User 表中所有明文密码迁移为 bcrypt 哈希'
def handle(self, *args, **options):
# 筛选非空且非 bcrypt 哈希的密码
users = User.objects.exclude(UserPassword='')
updated = 0
skipped = 0
for user in users.iterator():
stored = user.UserPassword or ''
# 已是 bcrypt 哈希则跳过
if stored.startswith(('$2b$', '$2a$')) and len(stored) == 60:
skipped += 1
continue
# 明文密码 → bcrypt 哈希
user.SetPassword(stored)
user.save(update_fields=['UserPassword'])
updated += 1
self.stdout.write(self.style.SUCCESS(
f'完成:{updated} 条明文密码已转为 bcrypt{skipped} 条已是哈希跳过'
))

View File

@@ -1,8 +1,8 @@
# Generated by Django 6.0.5 on 2026-06-13 11:43
# Generated by Django 4.2.27 on 2026-06-19 18:42
import datetime
import gvsdsdk.models
from django.db import migrations, models
import gvsdsdk.models
class Migration(migrations.Migration):
@@ -16,9 +16,9 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='AccountActivation',
fields=[
('AccountActivationCode', gvsdsdk.models.FixedBinaryField(binary_length=6, db_column='AccountActivationCode', max_length=6, primary_key=True, serialize=False)),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')),
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
('AccountActivationCode', gvsdsdk.models.FixedBinaryField(binary_length=6, db_column='AccountActivationCode', max_length=6, primary_key=True, serialize=False, verbose_name='激活码')),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
],
options={
'verbose_name': '账户激活',
@@ -30,13 +30,13 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='EphemeralToken',
fields=[
('ELTUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUUID', primary_key=True, serialize=False)),
('ELTUserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUserUUID', db_index=True)),
('ELTCreateTime', models.DateTimeField(db_column='ELTCreateTime', default=datetime.datetime.utcnow)),
('ELTEndTime', models.DateTimeField(db_column='ELTEndTime', default=gvsdsdk.models._elt_end_time_default)),
('ELTType', models.SmallIntegerField(db_column='ELTType', default=0)),
('ELTAllowIP', models.JSONField(db_column='ELTAllowIP', default=list)),
('ELTStatus', models.SmallIntegerField(db_column='ELTStatus', db_index=True, default=1)),
('ELTUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUUID', primary_key=True, serialize=False, verbose_name='令牌UUID')),
('ELTUserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUserUUID', db_index=True, verbose_name='用户UUID')),
('ELTCreateTime', models.DateTimeField(db_column='ELTCreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
('ELTEndTime', models.DateTimeField(db_column='ELTEndTime', default=gvsdsdk.models._EltEndTimeDefault, verbose_name='过期时间')),
('ELTType', models.SmallIntegerField(db_column='ELTType', default=0, verbose_name='令牌类型')),
('ELTAllowIP', models.JSONField(db_column='ELTAllowIP', default=list, verbose_name='允许IP')),
('ELTStatus', models.SmallIntegerField(db_column='ELTStatus', db_index=True, default=1, verbose_name='状态')),
],
options={
'verbose_name': '临时令牌',
@@ -48,8 +48,8 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Password',
fields=[
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)),
('Password', models.CharField(db_column='Password', max_length=1024)),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False, verbose_name='用户UUID')),
('Password', models.CharField(db_column='Password', max_length=1024, verbose_name='密码哈希')),
],
options={
'verbose_name': '密码',
@@ -61,12 +61,14 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Permission',
fields=[
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', primary_key=True, serialize=False)),
('PermName', models.CharField(db_column='PermName', max_length=128)),
('PermCode', models.CharField(db_column='PermCode', max_length=128)),
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
('PermStatus', models.SmallIntegerField(db_column='PermStatus', default=1)),
('PermDesc', models.CharField(db_column='PermDesc', default='', max_length=512)),
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', primary_key=True, serialize=False, verbose_name='权限UUID')),
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
('PermName', models.CharField(db_column='PermName', max_length=128, verbose_name='权限名称')),
('PermCode', models.CharField(db_column='PermCode', max_length=128, verbose_name='权限键')),
('PermCategory', models.CharField(db_column='PermCategory', default='', max_length=64, verbose_name='权限分类')),
('PermStatus', models.SmallIntegerField(db_column='PermStatus', default=1, verbose_name='是否启用')),
('PermDesc', models.CharField(db_column='PermDesc', default='', max_length=512, verbose_name='描述')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
],
options={
'verbose_name': '权限',
@@ -75,16 +77,72 @@ class Migration(migrations.Migration):
'managed': True,
},
),
migrations.CreateModel(
name='Position',
fields=[
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID', primary_key=True, serialize=False, verbose_name='职务UUID')),
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
('PositionName', models.CharField(db_column='PositionName', max_length=128, verbose_name='职务名称')),
('PositionCode', models.CharField(db_column='PositionCode', default='', max_length=64, verbose_name='职务编码')),
('PositionLevel', models.SmallIntegerField(db_column='PositionLevel', default=0, verbose_name='职务级别')),
('PositionStatus', models.SmallIntegerField(db_column='PositionStatus', default=1, verbose_name='状态')),
('PositionDesc', models.CharField(db_column='PositionDesc', default='', max_length=512, verbose_name='描述')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='更新时间')),
],
options={
'verbose_name': '人事职务标签',
'verbose_name_plural': '人事职务标签',
'db_table': 'Position',
'managed': True,
},
),
migrations.CreateModel(
name='PositionTemplate',
fields=[
('PositionTemplateUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionTemplateUUID', primary_key=True, serialize=False, verbose_name='岗位模板UUID')),
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
('TemplateName', models.CharField(db_column='TemplateName', max_length=128, verbose_name='模板名称')),
('TemplateCode', models.CharField(db_column='TemplateCode', default='', max_length=64, verbose_name='模板编码')),
('TemplateStatus', models.SmallIntegerField(db_column='TemplateStatus', default=1, verbose_name='模板状态')),
('TemplateDesc', models.CharField(db_column='TemplateDesc', default='', max_length=512, verbose_name='模板描述')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='更新时间')),
],
options={
'verbose_name': '岗位模板',
'verbose_name_plural': '岗位模板',
'db_table': 'PositionTemplate',
'managed': True,
},
),
migrations.CreateModel(
name='PositionTemplatePermission',
fields=[
('PTPermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PTPermUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
('PositionTemplateUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionTemplateUUID', verbose_name='岗位模板UUID')),
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', verbose_name='权限UUID')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
],
options={
'verbose_name': '岗位模板权限关联',
'verbose_name_plural': '岗位模板权限关联',
'db_table': 'PositionTemplatePermission',
'managed': True,
},
),
migrations.CreateModel(
name='Role',
fields=[
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', primary_key=True, serialize=False)),
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')),
('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='DeptUUID', null=True)),
('RoleName', models.CharField(db_column='RoleName', max_length=128)),
('RoleType', models.SmallIntegerField(db_column='RoleType', default=2)),
('RoleStatus', models.SmallIntegerField(db_column='RoleStatus', default=1)),
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', primary_key=True, serialize=False, verbose_name='角色UUID')),
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
('RoleName', models.CharField(db_column='RoleName', max_length=128, verbose_name='角色名称')),
('RoleType', models.SmallIntegerField(db_column='RoleType', default=2, verbose_name='角色类型')),
('RoleStatus', models.SmallIntegerField(db_column='RoleStatus', default=1, verbose_name='角色状态')),
('AssignScope', models.SmallIntegerField(db_column='AssignScope', default=0, verbose_name='分配可见范围')),
('AssignScopeUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='AssignScopeUUID', null=True, verbose_name='分配范围UUID')),
('RoleDesc', models.CharField(db_column='RoleDesc', default='', max_length=512, verbose_name='角色描述')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
],
options={
'verbose_name': '角色',
@@ -94,55 +152,69 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='RoleDept',
name='RolePermission',
fields=[
('RoleDeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleDeptUUID', primary_key=True, serialize=False)),
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='DeptUUID')),
('IsMainDept', models.SmallIntegerField(db_column='IsMainDept', default=1)),
('RolePermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RolePermUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')),
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', verbose_name='权限UUID')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
],
options={
'verbose_name': '角色部门关联',
'verbose_name_plural': '角色部门关联',
'db_table': 'RoleDept',
'verbose_name': '角色权限关联',
'verbose_name_plural': '角色权限关联',
'db_table': 'RolePermission',
'managed': True,
},
),
migrations.CreateModel(
name='User',
name='RoleShop',
fields=[
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)),
('UserName', models.CharField(db_column='UserName', max_length=256, unique=True)),
('UserPassword', models.CharField(db_column='UserPassword', default='', max_length=60)),
('UserPositionStatus', models.SmallIntegerField(db_column='UserPositionStatus', default=0)),
('UserPersonalSignature', models.CharField(db_column='UserPersonalSignature', default='', max_length=1024)),
('UserEmail', models.CharField(db_column='UserEmail', default='', max_length=256)),
('UserEmailVerify', models.SmallIntegerField(db_column='UserEmailVerify', default=0)),
('UserAccountLicense', models.SmallIntegerField(db_column='UserAccountLicense', default=1)),
('UserGender', models.SmallIntegerField(db_column='UserGender', default=-1)),
('UserTele', models.CharField(blank=True, db_column='UserTele', max_length=16, null=True)),
('UserCreateTime', models.DateTimeField(db_column='UserCreateTime', default=datetime.datetime.utcnow)),
('UserLastLoginDate', models.DateTimeField(db_column='UserLastLoginDate', default=datetime.datetime.utcnow)),
('IsStaff', models.BooleanField(db_column='IsStaff', default=False)),
('IsSuperuser', models.BooleanField(db_column='IsSuperuser', default=False)),
('RoleShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleShopUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')),
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', verbose_name='商铺UUID')),
('IsMainShop', models.SmallIntegerField(db_column='IsMainShop', default=0, verbose_name='主商铺')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
],
options={
'verbose_name': '用户',
'verbose_name_plural': '用户',
'db_table': 'User',
'verbose_name': '角色商铺关联',
'verbose_name_plural': '角色商铺关联',
'db_table': 'RoleShop',
'managed': True,
},
),
migrations.CreateModel(
name='Shop',
fields=[
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', primary_key=True, serialize=False, verbose_name='商铺UUID')),
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID', verbose_name='公司UUID')),
('ShopName', models.CharField(db_column='ShopName', max_length=128, verbose_name='商铺名称')),
('ShopCode', models.CharField(db_column='ShopCode', default='', max_length=64, verbose_name='商铺编码')),
('ShopType', models.SmallIntegerField(db_column='ShopType', default=0, verbose_name='商铺类型')),
('ShopStatus', models.SmallIntegerField(db_column='ShopStatus', default=1, verbose_name='商铺状态')),
('ShopAddress', models.CharField(db_column='ShopAddress', default='', max_length=512, verbose_name='商铺地址')),
('ShopContact', models.CharField(db_column='ShopContact', default='', max_length=128, verbose_name='联系人')),
('ShopPhone', models.CharField(db_column='ShopPhone', default='', max_length=32, verbose_name='联系电话')),
('ShopDesc', models.CharField(db_column='ShopDesc', default='', max_length=512, verbose_name='描述')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
('UpdateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='更新时间')),
],
options={
'verbose_name': '商铺',
'verbose_name_plural': '商铺',
'db_table': 'Shop',
'managed': True,
},
),
migrations.CreateModel(
name='User2FA',
fields=[
('User2FAUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='User2FAUUID', primary_key=True, serialize=False)),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True)),
('SecretKey', models.CharField(db_column='SecretKey', max_length=64)),
('IsEnabled', models.SmallIntegerField(db_column='IsEnabled', default=0)),
('Verified', models.SmallIntegerField(db_column='Verified', default=0)),
('LastUsedAt', models.DateTimeField(blank=True, db_column='LastUsedAt', null=True)),
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
('User2FAUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='User2FAUUID', primary_key=True, serialize=False, verbose_name='2FA UUID')),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True, verbose_name='用户UUID')),
('SecretKey', models.CharField(db_column='SecretKey', max_length=64, verbose_name='密钥')),
('IsEnabled', models.SmallIntegerField(db_column='IsEnabled', default=0, verbose_name='是否启用')),
('Verified', models.SmallIntegerField(db_column='Verified', default=0, verbose_name='已验证')),
('LastUsedAt', models.DateTimeField(blank=True, db_column='LastUsedAt', null=True, verbose_name='最后使用')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
],
options={
'verbose_name': '双因素认证',
@@ -154,9 +226,9 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='UserABAC',
fields=[
('UserABACUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserABACUUID', primary_key=True, serialize=False)),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')),
('HierarchicalPermissionIdentifiers', models.CharField(db_column='HierarchicalPermissionIdentifiers', max_length=256)),
('UserABACUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserABACUUID', primary_key=True, serialize=False, verbose_name='ABAC UUID')),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
('HierarchicalPermissionIdentifiers', models.CharField(db_column='HierarchicalPermissionIdentifiers', max_length=256, verbose_name='层级权限标识')),
],
options={
'verbose_name': '用户ABAC',
@@ -168,10 +240,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='UserAvatar',
fields=[
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)),
('UserAvatar', models.BinaryField(db_column='UserAvatar', editable=True)),
('AvatarUploadTime', models.DateTimeField(db_column='AvatarUploadTime', default=datetime.datetime.utcnow)),
('AvatarFileType', models.CharField(db_column='AvatarFileType', default='png', max_length=16)),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False, verbose_name='用户UUID')),
('UserAvatar', models.BinaryField(db_column='UserAvatar', editable=True, verbose_name='头像数据')),
('AvatarUploadTime', models.DateTimeField(db_column='AvatarUploadTime', default=datetime.datetime.utcnow, verbose_name='上传时间')),
('AvatarFileType', models.CharField(db_column='AvatarFileType', default='png', max_length=16, verbose_name='文件类型')),
],
options={
'verbose_name': '用户头像',
@@ -180,16 +252,33 @@ class Migration(migrations.Migration):
'managed': True,
},
),
migrations.CreateModel(
name='UserDept',
fields=[
('UserDeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserDeptUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='DeptUUID', verbose_name='部门UUID')),
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='PositionUUID', null=True, verbose_name='职务标签UUID')),
('IsMainDept', models.SmallIntegerField(db_column='IsMainDept', default=1, verbose_name='是否主部门')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
],
options={
'verbose_name': '员工部门关联',
'verbose_name_plural': '员工部门关联',
'db_table': 'UserDept',
'managed': True,
},
),
migrations.CreateModel(
name='UserLoginLog',
fields=[
('LogUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='LogUUID', primary_key=True, serialize=False)),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True)),
('LoginIP', models.CharField(db_column='LoginIP', max_length=39)),
('LoginPlatform', models.CharField(db_column='LoginPlatform', max_length=32)),
('LoginStatus', models.SmallIntegerField(db_column='LoginStatus')),
('LoginTime', models.DateTimeField(db_column='LoginTime', default=datetime.datetime.utcnow)),
('UserAgent', models.CharField(db_column='UserAgent', default='', max_length=512)),
('LogUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='LogUUID', primary_key=True, serialize=False, verbose_name='日志UUID')),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True, verbose_name='用户UUID')),
('LoginIP', models.CharField(db_column='LoginIP', max_length=39, verbose_name='登录IP')),
('LoginPlatform', models.CharField(db_column='LoginPlatform', max_length=32, verbose_name='登录平台')),
('LoginStatus', models.SmallIntegerField(db_column='LoginStatus', verbose_name='登录状态')),
('LoginTime', models.DateTimeField(db_column='LoginTime', default=datetime.datetime.utcnow, verbose_name='登录时间')),
('UserAgent', models.CharField(db_column='UserAgent', default='', max_length=512, verbose_name='用户代理')),
],
options={
'verbose_name': '登录日志',
@@ -201,10 +290,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='UserPending2FA',
fields=[
('PendingUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PendingUUID', primary_key=True, serialize=False)),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', unique=True)),
('SecretKey', models.CharField(db_column='SecretKey', max_length=64)),
('ExpireAt', models.DateTimeField(db_column='ExpireAt')),
('PendingUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PendingUUID', primary_key=True, serialize=False, verbose_name='待确认UUID')),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', unique=True, verbose_name='用户UUID')),
('SecretKey', models.CharField(db_column='SecretKey', max_length=64, verbose_name='密钥')),
('ExpireAt', models.DateTimeField(db_column='ExpireAt', verbose_name='过期时间')),
],
options={
'verbose_name': '2FA待确认',
@@ -216,10 +305,11 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='UserRole',
fields=[
('UserRoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserRoleUUID', primary_key=True, serialize=False)),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')),
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
('UserRoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserRoleUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')),
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID', verbose_name='公司UUID')),
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
],
options={
'verbose_name': '用户角色关联',

View File

@@ -1,90 +0,0 @@
# Generated by Django 6.0.5 on 2026-06-13 12:29
import datetime
import gvsdsdk.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gvsdsdk', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Position',
fields=[
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID', primary_key=True, serialize=False)),
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')),
('PositionName', models.CharField(db_column='PositionName', max_length=128)),
('PositionCode', models.CharField(db_column='PositionCode', default='', max_length=64)),
('PositionLevel', models.SmallIntegerField(db_column='PositionLevel', default=0)),
('PositionStatus', models.SmallIntegerField(db_column='PositionStatus', default=1)),
('PositionDesc', models.CharField(db_column='PositionDesc', default='', max_length=512)),
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow)),
],
options={
'verbose_name': '职务',
'verbose_name_plural': '职务',
'db_table': 'Position',
'managed': True,
},
),
migrations.CreateModel(
name='RolePosition',
fields=[
('RolePositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RolePositionUUID', primary_key=True, serialize=False)),
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID')),
('IsMainPosition', models.SmallIntegerField(db_column='IsMainPosition', default=0)),
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
],
options={
'verbose_name': '角色职务关联',
'verbose_name_plural': '角色职务关联',
'db_table': 'RolePosition',
'managed': True,
},
),
migrations.CreateModel(
name='RoleShop',
fields=[
('RoleShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleShopUUID', primary_key=True, serialize=False)),
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID')),
('IsMainShop', models.SmallIntegerField(db_column='IsMainShop', default=0)),
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
],
options={
'verbose_name': '角色商铺关联',
'verbose_name_plural': '角色商铺关联',
'db_table': 'RoleShop',
'managed': True,
},
),
migrations.CreateModel(
name='Shop',
fields=[
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', primary_key=True, serialize=False)),
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')),
('ShopName', models.CharField(db_column='ShopName', max_length=128)),
('ShopCode', models.CharField(db_column='ShopCode', default='', max_length=64)),
('ShopType', models.SmallIntegerField(db_column='ShopType', default=0)),
('ShopStatus', models.SmallIntegerField(db_column='ShopStatus', default=1)),
('ShopAddress', models.CharField(db_column='ShopAddress', default='', max_length=512)),
('ShopContact', models.CharField(db_column='ShopContact', default='', max_length=128)),
('ShopPhone', models.CharField(db_column='ShopPhone', default='', max_length=32)),
('ShopDesc', models.CharField(db_column='ShopDesc', default='', max_length=512)),
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow)),
],
options={
'verbose_name': '商铺',
'verbose_name_plural': '商铺',
'db_table': 'Shop',
'managed': True,
},
),
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,532 @@
"""MSYNC ORM 中转处理器
子服务器端注册 Socket.IO 事件处理器,接收主服务器发来的 ORM 查询请求,
在本地数据库执行 SQL 并返回结果。
用法(子服务器端):
from gvsdsdk.msync.orm_handler import ORMRelayHandler
handler = ORMRelayHandler()
handler.Register(sio_client) # 注册到 MSyncSocketClient 实例
"""
import datetime
import logging
import uuid
logger = logging.getLogger(__name__)
class ORMRelayHandler:
"""ORM 中转处理器——在子服务器端执行本地 SQL 查询
通过 MSYNC Socket.IO 事件协议,主服务器将 ORM 请求转发给子服务器,
子服务器在本地数据库执行查询后返回结果。
"""
def Register(self, sio_client):
"""将 ORM 事件处理器注册到 MSyncSocketClient 实例
Args:
sio_client: MSyncSocketClient 实例,需要有 _sio 属性
"""
sio = sio_client._sio
@sio.on('msync:orm:tables', namespace='/msync')
def on_orm_tables(data):
return self.HandleTables(data)
@sio.on('msync:orm:columns', namespace='/msync')
def on_orm_columns(data):
return self.HandleColumns(data)
@sio.on('msync:orm:list', namespace='/msync')
def on_orm_list(data):
return self.HandleList(data)
@sio.on('msync:orm:add', namespace='/msync')
def on_orm_add(data):
return self.HandleAdd(data)
@sio.on('msync:orm:modify', namespace='/msync')
def on_orm_modify(data):
return self.HandleModify(data)
@sio.on('msync:orm:delete', namespace='/msync')
def on_orm_delete(data):
return self.HandleDelete(data)
logger.info('[ORM] ORM 中转处理器已注册')
def _GetConnection(self):
"""获取本地数据库连接Django default"""
from django.db import connections
conn = connections['default']
conn.ensure_connection()
return conn
# ─── 事件处理 ──────────────────────────────────────────────
def HandleTables(self, data):
"""列出本地数据库中所有用户表"""
try:
conn = self._GetConnection()
except Exception as e:
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
try:
with conn.cursor() as cursor:
tables = self._ListTables(cursor)
return {'code': 0, 'msg': 'success', 'data': tables}
except Exception as e:
logger.error(f'[ORM] HandleTables 异常: {e}')
return {'code': 1, 'msg': f'查询失败: {e}'}
def HandleColumns(self, data):
"""列出指定表的列信息"""
table_name = (data or {}).get('Table', '')
if not table_name:
return {'code': 1, 'msg': '缺少 Table 参数'}
try:
conn = self._GetConnection()
except Exception as e:
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
try:
with conn.cursor() as cursor:
columns = self._ListColumns(cursor, table_name)
return {'code': 0, 'msg': 'success', 'data': columns}
except Exception as e:
logger.error(f'[ORM] HandleColumns 异常: {e}')
return {'code': 1, 'msg': f'查询失败: {e}'}
def HandleList(self, data):
"""分页查询表数据"""
table_name = (data or {}).get('Table', '')
if not table_name:
return {'code': 1, 'msg': '缺少 Table 参数'}
try:
conn = self._GetConnection()
except Exception as e:
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
try:
with conn.cursor() as cursor:
result = self._QueryTable(cursor, table_name, data or {})
if result.get('error'):
return {'code': 1, 'msg': result['error'], 'data': None}
return {'code': 0, 'msg': 'success', 'data': result}
except Exception as e:
logger.error(f'[ORM] HandleList 异常: {e}')
return {'code': 1, 'msg': f'查询失败: {e}'}
def HandleAdd(self, data):
"""新增记录"""
table_name = (data or {}).get('Table', '')
record = (data or {}).get('Record', {})
if not table_name:
return {'code': 1, 'msg': '缺少 Table 参数'}
try:
conn = self._GetConnection()
except Exception as e:
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
try:
with conn.cursor() as cursor:
result = self._AddRecord(cursor, table_name, record)
return result
except Exception as e:
logger.error(f'[ORM] HandleAdd 异常: {e}')
return {'code': 1, 'msg': f'新增失败: {e}'}
def HandleModify(self, data):
"""修改记录"""
table_name = (data or {}).get('Table', '')
record = (data or {}).get('Record', {})
if not table_name:
return {'code': 1, 'msg': '缺少 Table 参数'}
try:
conn = self._GetConnection()
except Exception as e:
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
try:
with conn.cursor() as cursor:
result = self._ModifyRecord(cursor, table_name, record)
return result
except Exception as e:
logger.error(f'[ORM] HandleModify 异常: {e}')
return {'code': 1, 'msg': f'修改失败: {e}'}
def HandleDelete(self, data):
"""删除记录"""
table_name = (data or {}).get('Table', '')
record = (data or {}).get('Record', {})
if not table_name:
return {'code': 1, 'msg': '缺少 Table 参数'}
try:
conn = self._GetConnection()
except Exception as e:
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
try:
with conn.cursor() as cursor:
result = self._DeleteRecord(cursor, table_name, record)
return result
except Exception as e:
logger.error(f'[ORM] HandleDelete 异常: {e}')
return {'code': 1, 'msg': f'删除失败: {e}'}
# ─── SQL 执行 ──────────────────────────────────────────────
def _ListTables(self, cursor):
"""列出所有用户表,含 i18n 标题"""
cursor.execute(
"SELECT TABLE_NAME, TABLE_ROWS, TABLE_COMMENT FROM information_schema.TABLES "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE' "
"ORDER BY TABLE_NAME"
)
from django.apps import apps
# 构建表名→中文标题映射
model_verbose = {}
for model in apps.get_models():
meta = model._meta
# 优先使用 table_title自定义 Meta 属性),其次 verbose_name
table_title = getattr(meta, 'table_title', None)
title = str(table_title) if table_title else (str(meta.verbose_name) if meta.verbose_name else '')
if title:
model_verbose[meta.object_name] = title
model_verbose[meta.db_table] = title
result = []
for row in cursor.fetchall():
table_name, table_rows, table_comment = row
# 优先级TABLE_COMMENT > table_title/verbose_name > 表名
title = table_comment or model_verbose.get(table_name) or table_name
result.append({'name': table_name, 'title': title, 'count': table_rows or 0})
return result
def _ListColumns(self, cursor, table_name):
"""列出指定表的列信息"""
cursor.execute(
"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_KEY, EXTRA, COLUMN_TYPE, COLUMN_COMMENT "
"FROM information_schema.COLUMNS "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s "
"ORDER BY ORDINAL_POSITION",
[table_name]
)
model_verbose = self._FindModelVerboseNames(table_name)
logger.info(f'[ORM] _ListColumns table={table_name} model_verbose={model_verbose}')
columns = []
for row in cursor.fetchall():
col_name, data_type, is_nullable, col_key, extra, col_type, col_comment = row
is_pk = col_key == 'PRI'
is_auto = 'auto_increment' in (extra or '').lower()
if data_type in ('int', 'bigint', 'tinyint', 'smallint', 'mediumint'):
ftype = 'number'
elif data_type in ('float', 'double', 'decimal'):
ftype = 'number'
elif data_type in ('datetime', 'timestamp'):
ftype = 'datetime'
elif data_type in ('date',):
ftype = 'date'
elif data_type in ('time',):
ftype = 'time'
elif data_type in ('json',):
ftype = 'textarea'
elif data_type in ('text', 'mediumtext', 'longtext', 'tinytext'):
ftype = 'textarea'
elif data_type in ('binary', 'varbinary', 'blob'):
ftype = 'uuid'
else:
ftype = 'text'
title = col_comment or model_verbose.get(col_name) or col_name
columns.append({
'field': col_name,
'title': title,
'type': ftype,
'editable': not is_pk and not is_auto,
'sortable': True,
'filterable': True,
'primaryKey': is_pk,
'hideInForm': is_pk,
'required': is_nullable == 'NO' and not is_auto,
'dbType': col_type,
})
return columns
def _QueryTable(self, cursor, table_name, params):
"""分页查询表数据"""
cursor.execute(
"SELECT COUNT(*) FROM information_schema.TABLES "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s",
[table_name]
)
if cursor.fetchone()[0] == 0:
return {'records': [], 'total': 0, 'error': f'表不存在: {table_name}'}
col_info = self._GetTableColumns(cursor, table_name)
pk_cols = {k: v for k, v in col_info.items() if v['is_pk']}
where_parts = []
where_values = []
for pk_name in pk_cols:
pk_val = params.get(pk_name, '')
if pk_val:
where_parts.append(f'`{pk_name}` = %s')
where_values.append(self._ConvertValueForDB(pk_val, pk_cols[pk_name]))
keyword = params.get('keyword', '')
if not where_parts and keyword:
cursor.execute(
"SELECT COLUMN_NAME FROM information_schema.COLUMNS "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s "
"AND DATA_TYPE IN ('varchar', 'char', 'text', 'mediumtext', 'longtext')",
[table_name]
)
text_cols = [row[0] for row in cursor.fetchall()]
if text_cols:
conditions = [f'`{c}` LIKE %s' for c in text_cols]
or_clause = ' OR '.join(conditions)
where_parts.append(f'({or_clause})')
where_values.extend([f'%{keyword}%'] * len(text_cols))
where_clause = (' WHERE ' + ' AND '.join(where_parts)) if where_parts else ''
if where_clause:
cursor.execute(f'SELECT COUNT(*) FROM `{table_name}`{where_clause}', where_values)
total = cursor.fetchone()[0]
else:
cursor.execute(f'SELECT COUNT(*) FROM `{table_name}`')
total = cursor.fetchone()[0]
try:
page = max(int(params.get('PageIndex', 1)), 1)
except (ValueError, TypeError):
page = 1
try:
page_size = min(max(int(params.get('PageSize', 20)), 1), 200)
except (ValueError, TypeError):
page_size = 20
offset = (page - 1) * page_size
cursor.execute(
f'SELECT * FROM `{table_name}`{where_clause} ORDER BY 1 LIMIT %s OFFSET %s',
where_values + [page_size, offset]
)
desc = [d[0] for d in cursor.description]
records = []
for row in cursor.fetchall():
record = {}
for i, val in enumerate(row):
val = self._SerializeValue(val)
record[desc[i]] = val
records.append(record)
return {'records': records, 'total': total, 'page': page, 'PageSize': page_size}
def _AddRecord(self, cursor, table_name, record):
"""新增记录"""
if not self._ValidateTableName(cursor, table_name):
return {'code': 1, 'msg': f'{table_name} 不存在'}
col_info = self._GetTableColumns(cursor, table_name)
fields = []
values = []
for key, val in record.items():
if key not in col_info:
continue
if col_info[key]['is_auto']:
continue
fields.append(f'`{key}`')
values.append(self._ConvertValueForDB(val, col_info[key]))
if not fields:
return {'code': 1, 'msg': '没有可插入的字段'}
placeholders = ', '.join(['%s'] * len(fields))
sql = f'INSERT INTO `{table_name}` ({", ".join(fields)}) VALUES ({placeholders})'
cursor.execute(sql, values)
return {'code': 0, 'msg': '新增成功'}
def _ModifyRecord(self, cursor, table_name, record):
"""修改记录"""
if not self._ValidateTableName(cursor, table_name):
return {'code': 1, 'msg': f'{table_name} 不存在'}
col_info = self._GetTableColumns(cursor, table_name)
pk_cols = {k: v for k, v in col_info.items() if v['is_pk']}
if not pk_cols:
return {'code': 1, 'msg': '表没有主键,无法定位记录'}
set_parts = []
set_values = []
for key, val in record.items():
if key not in col_info or key in pk_cols:
continue
set_parts.append(f'`{key}` = %s')
set_values.append(self._ConvertValueForDB(val, col_info[key]))
if not set_parts:
return {'code': 0, 'msg': '没有需要更新的字段'}
where_parts = []
where_values = []
for pk_name in pk_cols:
if pk_name not in record:
return {'code': 1, 'msg': f'缺少主键字段: {pk_name}'}
where_parts.append(f'`{pk_name}` = %s')
where_values.append(self._ConvertValueForDB(record[pk_name], pk_cols[pk_name]))
sql = f'UPDATE `{table_name}` SET {", ".join(set_parts)} WHERE {" AND ".join(where_parts)}'
cursor.execute(sql, set_values + where_values)
if cursor.rowcount == 0:
return {'code': 1, 'msg': '记录不存在或未变更'}
return {'code': 0, 'msg': '修改成功'}
def _DeleteRecord(self, cursor, table_name, record):
"""删除记录"""
if not self._ValidateTableName(cursor, table_name):
return {'code': 1, 'msg': f'{table_name} 不存在'}
col_info = self._GetTableColumns(cursor, table_name)
pk_cols = {k: v for k, v in col_info.items() if v['is_pk']}
if not pk_cols:
return {'code': 1, 'msg': '表没有主键,无法定位记录'}
where_parts = []
where_values = []
for pk_name in pk_cols:
if pk_name not in record:
return {'code': 1, 'msg': f'缺少主键字段: {pk_name}'}
where_parts.append(f'`{pk_name}` = %s')
where_values.append(self._ConvertValueForDB(record[pk_name], pk_cols[pk_name]))
sql = f'DELETE FROM `{table_name}` WHERE {" AND ".join(where_parts)}'
cursor.execute(sql, where_values)
if cursor.rowcount == 0:
return {'code': 1, 'msg': '记录不存在'}
return {'code': 0, 'msg': '删除成功'}
# ─── 工具方法 ──────────────────────────────────────────────
def _ValidateTableName(self, cursor, table_name):
"""验证表名存在"""
cursor.execute(
"SELECT COUNT(*) FROM information_schema.TABLES "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s",
[table_name]
)
return cursor.fetchone()[0] > 0
def _GetTableColumns(self, cursor, table_name):
"""获取表的列信息"""
cursor.execute(
"SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, COLUMN_KEY, EXTRA, IS_NULLABLE "
"FROM information_schema.COLUMNS "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s "
"ORDER BY ORDINAL_POSITION",
[table_name]
)
cols = {}
for row in cursor.fetchall():
col_name, data_type, column_type, col_key, extra, is_nullable = row
cols[col_name] = {
'type': data_type,
'column_type': column_type or '',
'is_pk': col_key == 'PRI',
'is_auto': 'auto_increment' in (extra or '').lower(),
'is_nullable': is_nullable == 'YES',
}
return cols
def _ConvertValueForDB(self, value, col_info):
"""将前端传来的值转换为数据库可接受的类型"""
if value is None or value == '':
if col_info['is_nullable']:
return None
return ''
data_type = col_info['type']
if data_type in ('int', 'bigint', 'tinyint', 'smallint', 'mediumint'):
return int(value)
elif data_type in ('float', 'double', 'decimal'):
return float(value)
elif data_type in ('binary', 'varbinary'):
import re
hex_str = re.sub(r'[^0-9a-fA-F]', '', str(value))
if len(hex_str) == 32:
return uuid.UUID(hex_str).bytes
return value
return value
def _SerializeValue(self, val):
"""将数据库值序列化为 JSON 安全格式"""
if isinstance(val, bytes):
if len(val) == 16:
try:
return str(uuid.UUID(bytes=val))
except ValueError:
return val.hex()
return val.hex()
elif isinstance(val, datetime.datetime):
return val.strftime('%Y-%m-%d %H:%M:%S')
elif isinstance(val, datetime.date):
return val.isoformat()
elif isinstance(val, datetime.time):
return val.strftime('%H:%M:%S')
elif isinstance(val, (bytearray, memoryview)):
return bytes(val).hex()
return val
def _FindModelVerboseNames(self, table_name):
"""从 Django 模型中获取字段的 verbose_name"""
from django.apps import apps
verbose_map = {}
try:
for model in apps.get_models():
if model._meta.db_table == table_name:
logger.info(f'[ORM] _FindModelVerboseNames matched model={model.__name__} db_table={model._meta.db_table}')
for f in model._meta.get_fields():
# 获取数据库列名
col_name = None
if hasattr(f, 'column') and f.column:
col_name = f.column
elif hasattr(f, 'db_column') and f.db_column:
col_name = f.db_column
elif hasattr(f, 'attname'):
col_name = f.attname
if not col_name:
continue
vn = getattr(f, 'verbose_name', None)
if vn and str(vn) != col_name:
verbose_map[col_name] = str(vn)
logger.debug(f'[ORM] {col_name} -> {str(vn)}')
break
else:
# 没有匹配的模型
available = [m._meta.db_table for m in apps.get_models()]
logger.warning(f'[ORM] _FindModelVerboseNames: 未找到 db_table={table_name} 的模型, 可用表: {available[:20]}...')
except Exception as e:
logger.error(f'[ORM] _FindModelVerboseNames 异常: {e}')
return verbose_map

7
jituan/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class JituanConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'jituan'
verbose_name = '集团多俱乐部'

30
jituan/constants.py Normal file
View File

@@ -0,0 +1,30 @@
"""集团多俱乐部常量。"""
CLUB_ID_DEFAULT = 'xq'
CLUB_HEADER = 'X-Club-Id'
CLUB_SCOPE_HEADER = 'X-Club-Scope'
DATA_SCOPE_ALL = 'ALL_CLUBS'
DATA_SCOPE_SINGLE = 'SINGLE_CLUB'
# 管理任职角色码(数据范围 / 集团 vs 子公司)
ADMIN_ROLE_LABELS = {
'GROUP_OWNER': '集团总负责人',
'GROUP_SUPER_ADMIN': '集团超管',
'GROUP_AFTER_SALES': '集团售后',
'GROUP_FINANCE': '集团财务',
'GROUP_CONFIG': '集团配置',
'CLUB_OWNER': '俱乐部总负责人',
'CLUB_ADMIN': '俱乐部管理员',
'CLUB_AFTER_SALES': '俱乐部售后',
'CLUB_FINANCE': '俱乐部财务',
'CLUB_OPERATOR': '俱乐部运营',
}
# 功能权限仍走 gvsdsdk Permission.perm_code如 caiwu、dingdan、000001
ADMIN_SCOPE_GROUP = 'GROUP'
ADMIN_SCOPE_CLUB = 'CLUB'
# 集团最高权限账号(写死超级管理员,拥有全部功能权限与集团视图)
SUPER_ADMIN_PHONES = frozenset({'1383714110'})

View File

View File

View File

@@ -0,0 +1,47 @@
"""回填 lunbo / gonggao / popup_page / tupianpeizhi 的 club_id 与 lunbo.page_key。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from config.models import Gonggao, Lunbo, PopupPage, Tupianpeizhi
class Command(BaseCommand):
help = '回填展示配置表的 club_id并为 lunbo 补 page_key'
def handle(self, *args, **options):
updated = {'lunbo_club': 0, 'lunbo_page': 0, 'gonggao': 0, 'popup': 0, 'tupian': 0}
with transaction.atomic():
for row in Lunbo.query.all().only('id', 'club_id', 'page_key', 'ImageType'):
fields = []
if not row.club_id:
row.club_id = CLUB_ID_DEFAULT
fields.append('club_id')
updated['lunbo_club'] += 1
if not row.page_key:
if row.ImageType == 2:
row.page_key = 'dashou_center'
else:
row.page_key = 'order_pool'
fields.append('page_key')
updated['lunbo_page'] += 1
if fields:
row.save(update_fields=fields)
for row in Gonggao.query.filter(club_id='').only('id', 'club_id'):
Gonggao.query.filter(id=row.id).update(club_id=CLUB_ID_DEFAULT)
updated['gonggao'] += 1
for row in PopupPage.query.filter(club_id='').only('id', 'club_id'):
PopupPage.query.filter(id=row.id).update(club_id=CLUB_ID_DEFAULT)
updated['popup'] += 1
for row in Tupianpeizhi.query.filter(club_id='').only('id', 'club_id'):
Tupianpeizhi.query.filter(id=row.id).update(club_id=CLUB_ID_DEFAULT)
updated['tupian'] += 1
self.stdout.write(self.style.SUCCESS(
f'回填完成: lunbo_club={updated["lunbo_club"]}, lunbo_page={updated["lunbo_page"]}, '
f'gonggao={updated["gonggao"]}, popup={updated["popup"]}, tupian={updated["tupian"]}'
))

View File

@@ -0,0 +1,23 @@
"""按充值订单 / 订单 / 用户回填 gsfenhong.club_id。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_penalty import resolve_gsfenhong_club_id
from products.models import Gsfenhong
class Command(BaseCommand):
help = '回填 gsfenhong.club_id'
def handle(self, *args, **options):
updated = 0
with transaction.atomic():
for row in Gsfenhong.query.all().only('dingdan_id', 'dashouid', 'club_id'):
cid = resolve_gsfenhong_club_id(dingdan_id=row.dingdan_id, dashouid=row.dashouid)
if not cid:
cid = CLUB_ID_DEFAULT
if row.club_id != cid:
Gsfenhong.query.filter(dingdan_id=row.dingdan_id).update(club_id=cid)
updated += 1
self.stdout.write(self.style.SUCCESS(f'gsfenhong club_id 更新 {updated}'))

View File

@@ -0,0 +1,41 @@
"""回填订单 ClubID空值设为 xq"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from orders.models import Order
class Command(BaseCommand):
help = '回填 Order.ClubID空或缺省设为 xq'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='仅统计不落库',
)
def handle(self, *args, **options):
dry_run = options.get('dry_run', False)
updated = 0
skipped = 0
for order in Order.query.all().only('id', 'OrderID', 'ClubID'):
current = getattr(order, 'ClubID', None) or ''
if current:
skipped += 1
continue
if dry_run:
self.stdout.write(f'[dry-run] {order.OrderID} -> ClubID={CLUB_ID_DEFAULT}')
updated += 1
continue
with transaction.atomic():
order.ClubID = CLUB_ID_DEFAULT
order.save(update_fields=['ClubID'])
updated += 1
suffix = 'dry-run' if dry_run else ''
self.stdout.write(self.style.SUCCESS(f'完成{suffix}:更新 {updated},跳过 {skipped}'))

View File

@@ -0,0 +1,42 @@
"""回填罚单 club_id从关联订单或被罚用户"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_penalty import resolve_penalty_club_id
from orders.models import Order, Penalty
from users.business_models import User
class Command(BaseCommand):
help = '按关联订单/被罚用户回填 fadan.club_id默认仅处理仍为 xq 的记录)'
def add_arguments(self, parser):
parser.add_argument('--all', action='store_true', help='重算全部罚单(慎用)')
parser.add_argument('--dry-run', action='store_true')
def handle(self, *args, **options):
dry = options['dry_run']
qs = Penalty.query.all()
if not options['all']:
qs = qs.filter(ClubID=CLUB_ID_DEFAULT)
updated = 0
with transaction.atomic():
for p in qs.to_list():
order = None
if p.RelatedOrderID:
order = Order.query.filter(OrderID=p.RelatedOrderID).first()
cid = resolve_penalty_club_id(
order=order,
penalized_user_id=p.PenalizedUserID,
)
if cid and cid != p.ClubID:
if dry:
self.stdout.write(f'would update penalty {p.id}: {p.ClubID} -> {cid}')
else:
p.ClubID = cid
p.save(update_fields=['ClubID'])
updated += 1
self.stdout.write(self.style.SUCCESS(f'罚单 club_id 更新 {updated}'))

View File

@@ -0,0 +1,53 @@
"""回填 chufajilu / fadan / duoci_fenhong club_id。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_penalty import (
resolve_gsfenhong_club_id,
resolve_penalty_club_id,
resolve_penalty_record_club_id,
)
from orders.models import Penalty, PenaltyRecord
from products.models import DuociFenhong
class Command(BaseCommand):
help = '回填 PenaltyRecord / Penalty / DuociFenhong 的 club_id'
def handle(self, *args, **options):
pr_updated = 0
fd_updated = 0
df_updated = 0
with transaction.atomic():
for row in PenaltyRecord.query.all().only('id', 'OrderID', 'PlayerID', 'ClubID'):
cid = resolve_penalty_record_club_id(order_id=row.OrderID, player_id=row.PlayerID)
if row.ClubID != cid:
PenaltyRecord.query.filter(id=row.id).update(ClubID=cid)
pr_updated += 1
for row in Penalty.query.all().only('id', 'RelatedOrderID', 'PenalizedUserID', 'ClubID'):
cid = resolve_penalty_club_id(
order=None,
penalized_user_id=row.PenalizedUserID,
)
if row.RelatedOrderID:
from orders.models import Order
o = Order.query.filter(OrderID=row.RelatedOrderID).first()
if o:
cid = resolve_penalty_club_id(order=o, penalized_user_id=row.PenalizedUserID)
if row.ClubID != cid:
Penalty.query.filter(id=row.id).update(ClubID=cid)
fd_updated += 1
for row in DuociFenhong.query.all().only('huiyuan', 'yonghuid', 'cishu', 'club_id'):
cid = resolve_gsfenhong_club_id(dashouid=row.yonghuid) or CLUB_ID_DEFAULT
if row.club_id != cid:
DuociFenhong.query.filter(
club_id=row.club_id,
huiyuan=row.huiyuan,
yonghuid=row.yonghuid,
cishu=row.cishu,
).update(club_id=cid)
df_updated += 1
self.stdout.write(self.style.SUCCESS(
f'PenaltyRecord={pr_updated}, Penalty={fd_updated}, DuociFenhong={df_updated}'
))

View File

@@ -0,0 +1,36 @@
"""回填角色日统计 club_id按用户归属俱乐部"""
from django.core.management.base import BaseCommand
from django.db import transaction
from backend.models import (
LeaderDailyStats,
ManagerDailyStats,
ManagerRenewalDailyStats,
MerchantDailyStats,
PlayerDailyStats,
)
from backend.utils import _club_id_for_yonghuid
MODELS = [
(MerchantDailyStats, 'MerchantID'),
(PlayerDailyStats, 'PlayerID'),
(LeaderDailyStats, 'LeaderID'),
(ManagerDailyStats, 'ManagerID'),
(ManagerRenewalDailyStats, 'ManagerID'),
]
class Command(BaseCommand):
help = '回填 5 张角色日统计表的 club_id'
def handle(self, *args, **options):
updated = 0
with transaction.atomic():
for model, uid_field in MODELS:
for row in model.query.all().only('id', uid_field, 'club_id'):
cid = _club_id_for_yonghuid(getattr(row, uid_field))
if row.club_id != cid:
model.query.filter(id=row.id).update(club_id=cid)
updated += 1
self.stdout.write(self.style.SUCCESS(f'角色日统计 club_id 更新 {updated}'))

View File

@@ -0,0 +1,42 @@
"""按用户 ClubID 回填提现流水 club_id。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_user import get_user_club_id
from users.business_models import User
from users.models import TixianAutoRecord, Tixianjilu, TixianShenheJilu
class Command(BaseCommand):
help = '回填 tixianjilu / tixian_shenhe_jilu / tixian_auto_record 的 club_id'
def handle(self, *args, **options):
uid_club = {}
for u in User.query.all().only('UserUID', 'ClubID'):
uid_club[u.UserUID] = get_user_club_id(u)
updated = {'jilu': 0, 'shenhe': 0, 'auto': 0}
with transaction.atomic():
for row in Tixianjilu.query.all().only('id', 'yonghuid', 'club_id'):
cid = uid_club.get(row.yonghuid) or CLUB_ID_DEFAULT
if row.club_id != cid:
Tixianjilu.query.filter(id=row.id).update(club_id=cid)
updated['jilu'] += 1
for row in TixianShenheJilu.query.all().only('id', 'yonghuid', 'club_id'):
cid = uid_club.get(row.yonghuid) or CLUB_ID_DEFAULT
if row.club_id != cid:
TixianShenheJilu.query.filter(id=row.id).update(club_id=cid)
updated['shenhe'] += 1
for row in TixianAutoRecord.query.all().only('id', 'yonghuid', 'club_id'):
cid = uid_club.get(row.yonghuid) or CLUB_ID_DEFAULT
if row.club_id != cid:
TixianAutoRecord.query.filter(id=row.id).update(club_id=cid)
updated['auto'] += 1
self.stdout.write(self.style.SUCCESS(
f'回填完成: jilu={updated["jilu"]}, shenhe={updated["shenhe"]}, auto={updated["auto"]}'
))

View File

@@ -0,0 +1,53 @@
"""回填用户 ClubID缺省或空时设为 xq 或 user_wx_openid 绑定)。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import UserWxOpenid
from users.business_models import User
class Command(BaseCommand):
help = '回填 User.ClubIDOpenID 绑定 → user_wx_openid否则默认 xq'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='仅统计不落库',
)
def handle(self, *args, **options):
dry_run = options.get('dry_run', False)
updated = 0
skipped = 0
users = User.query.all()
for user in users:
current = getattr(user, 'ClubID', None) or ''
if current and current != CLUB_ID_DEFAULT:
skipped += 1
continue
new_club = CLUB_ID_DEFAULT
if user.OpenID:
binding = UserWxOpenid.query.filter(openid=user.OpenID).order_by('id').first()
if binding:
new_club = binding.club_id
if current == new_club:
skipped += 1
continue
if dry_run:
self.stdout.write(f'[dry-run] {user.UserUID} -> ClubID={new_club}')
updated += 1
continue
with transaction.atomic():
user.ClubID = new_club
user.save(update_fields=['ClubID'])
updated += 1
suffix = 'dry-run' if dry_run else ''
self.stdout.write(self.style.SUCCESS(f'完成{suffix}:更新 {updated},跳过 {skipped}'))

View File

@@ -0,0 +1,75 @@
"""清理重复任职 + 可选为星之界创建组长邀请链。"""
import time
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import Club, ClubHuiyuanPrice
from jituan.services.admin_assignments import dedupe_active_assignments
from jituan.services.club_user import ensure_user_club_id
from users.business_models import User
from users.models import UserGuanshi, UserZuzhang
class Command(BaseCommand):
help = '清理重复 admin_assignment可选为 xzj 用户创建组长并输出邀请码'
def add_arguments(self, parser):
parser.add_argument('--dedupe-only', action='store_true', help='只清理重复有效任职')
parser.add_argument('--club-id', default='xzj', help='目标俱乐部')
parser.add_argument('--phone', type=str, default='', help='用户手机号(须已微信登录过)')
parser.add_argument('--make-zuzhang', action='store_true', help='将该用户设为 xzj 组长并生成邀请码')
def handle(self, *args, **options):
removed = dedupe_active_assignments()
self.stdout.write(self.style.SUCCESS(f'已停用重复任职 {removed}'))
if options.get('dedupe_only'):
return
club_id = (options.get('club_id') or 'xzj').strip()
if not Club.query.filter(club_id=club_id).exists():
raise CommandError(f'俱乐部 {club_id} 不存在')
price_cnt = ClubHuiyuanPrice.query.filter(club_id=club_id).count()
self.stdout.write(f'club_huiyuan_price({club_id}) 共 {price_cnt} 条(创建俱乐部时从 xq 复制,价相同需后台单独改)')
phone = (options.get('phone') or '').strip()
if options.get('make_zuzhang'):
if not phone:
raise CommandError('请指定 --phone')
user = User.query.filter(Phone=phone).first()
if not user:
raise CommandError(f'未找到手机号 {phone},请先在星之界小程序登录一次')
ensure_user_club_id(user, club_id)
user.ClubID = club_id
user.save(update_fields=['ClubID'])
if hasattr(user, 'ZuzhangProfile'):
z = user.ZuzhangProfile
self.stdout.write(self.style.SUCCESS(
f'已是组长,邀请码: {z.yaoqingma}(管事注册用组长码)'
))
return
yaoqingma = f'ZZ{int(time.time())}{str(user.UserUID)[-4:]}'
with transaction.atomic():
UserZuzhang.query.create(
user=user,
yaoqingma=yaoqingma,
zhuangtai=1,
)
self.stdout.write(self.style.SUCCESS(
f'已创建 xzj 组长 UserUID={user.UserUID},组长邀请码: {yaoqingma}\n'
f'管事注册: 小程序内 guanshizhuceinviteCode={yaoqingma}'
))
else:
self.stdout.write(
'查邀请码 SQL表名 User 大写):\n'
f"SELECT u.UserUID,u.Phone,z.yaoqingma FROM `User` u "
f"JOIN user_zuzhang z ON z.UserUUID=u.UserUUID WHERE u.ClubID='{club_id}';\n"
f"SELECT u.UserUID,u.Phone,g.yaoqingma FROM `User` u "
f"JOIN user_guanshi g ON g.UserUUID=u.UserUUID WHERE u.ClubID='{club_id}';\n"
'创建组长: python manage.py bootstrap_xzj_invite --phone 手机号 --make-zuzhang'
)

View File

@@ -0,0 +1,110 @@
"""从 xq 模板复制配置,创建新子公司俱乐部。"""
from decimal import Decimal
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import Club, ClubHuiyuanPrice, ClubLilubiao, ClubTixianQuota, ClubWithdrawConfig
from jituan.services.szjilu_accounting import normalize_szjilu_club_id
from jituan.services.display_config import copy_display_config
from config.models import Szjilu
class Command(BaseCommand):
help = '从 xq 复制支付/会员价/利率配置,创建新俱乐部'
def add_arguments(self, parser):
parser.add_argument('club_id', type=str, help='新俱乐部 ID如 xy')
parser.add_argument('--name', type=str, required=True, help='展示名称')
parser.add_argument('--wx-appid', type=str, default='', help='小程序 appid')
parser.add_argument('--from-club', type=str, default=CLUB_ID_DEFAULT, help='模板俱乐部')
def handle(self, *args, **options):
new_id = (options['club_id'] or '').strip()
name = (options['name'] or '').strip()
template_id = normalize_szjilu_club_id(options.get('from_club'))
if not new_id or len(new_id) > 16:
raise CommandError('club_id 无效')
if new_id == template_id:
raise CommandError('新 club_id 不能与模板相同')
try:
template = Club.query.get(club_id=template_id)
except Club.DoesNotExist:
raise CommandError(f'模板俱乐部 {template_id} 不存在,请先 seed_club_xq')
with transaction.atomic():
if Club.query.filter(club_id=new_id).exists():
raise CommandError(f'俱乐部 {new_id} 已存在')
Club.query.create(
club_id=new_id,
name=name,
status=1,
wx_appid=options.get('wx_appid') or template.wx_appid,
wx_secret=template.wx_secret,
mch_id=template.mch_id,
pay_app_id=template.pay_app_id or options.get('wx_appid') or template.wx_appid,
api_v3_key=template.api_v3_key,
cert_serial_no=template.cert_serial_no,
private_key_path=template.private_key_path,
platform_cert_dir=template.platform_cert_dir,
official_appid=template.official_appid,
official_secret=template.official_secret,
official_token=template.official_token,
encoding_aes_key=template.encoding_aes_key,
template_id=template.template_id,
template_max_per_minute=template.template_max_per_minute,
h5_domain=template.h5_domain,
oss_prefix=template.oss_prefix or new_id,
goeasy_appkey=template.goeasy_appkey,
goeasy_secret=template.goeasy_secret,
config_json=dict(template.config_json or {}),
sort_order=template.sort_order + 1,
)
for row in ClubHuiyuanPrice.query.filter(club_id=template_id):
ClubHuiyuanPrice.query.create(
club_id=new_id,
huiyuan_id=row.huiyuan_id,
jiage=row.jiage or Decimal('0'),
guanshifc=row.guanshifc or Decimal('0'),
zuzhangfc=row.zuzhangfc or Decimal('0'),
formal_days=row.formal_days or 30,
trial_enabled=bool(row.trial_enabled),
trial_price=row.trial_price or Decimal('0'),
trial_days=row.trial_days or 0,
trial_guanshifc=row.trial_guanshifc or Decimal('0'),
trial_zuzhangfc=row.trial_zuzhangfc or Decimal('0'),
is_enabled=row.is_enabled,
bankuai_id=row.bankuai_id,
)
for row in ClubLilubiao.query.filter(club_id=template_id):
ClubLilubiao.query.create(
club_id=new_id,
config_type=row.config_type,
lilv=row.lilv or Decimal('0'),
remark=row.remark or '',
)
try:
tpl_wd = ClubWithdrawConfig.query.get(club_id=template_id)
ClubWithdrawConfig.query.create(club_id=new_id, mode=tpl_wd.mode)
except ClubWithdrawConfig.DoesNotExist:
ClubWithdrawConfig.query.create(club_id=new_id, mode=1)
for row in ClubTixianQuota.query.filter(club_id=template_id):
ClubTixianQuota.query.create(
club_id=new_id,
leixing=row.leixing,
daily_limit=row.daily_limit or Decimal('0'),
)
Szjilu.objects.get_or_create(club_id=new_id)
copy_display_config(template_id, new_id)
self.stdout.write(self.style.SUCCESS(f'俱乐部 {new_id} ({name}) 已创建,配置自 {template_id} 复制'))

View File

@@ -0,0 +1,89 @@
"""为后台客服账号初始化 admin_assignment数据范围非功能权限"""
import logging
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_SINGLE, ADMIN_ROLE_LABELS
from jituan.models import AdminAssignment
from users.business_models import User
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '为 kefu 账号创建默认子公司任职club=xq, CLUB_ADMIN超管可不建 assignment'
def add_arguments(self, parser):
parser.add_argument(
'--club-id', default=CLUB_ID_DEFAULT, help='目标俱乐部 ID默认 xq',
)
parser.add_argument(
'--role-code', default='CLUB_ADMIN', help='任职角色码(仅展示标签,不控制菜单功能权限)',
)
parser.add_argument(
'--phone', type=str, default='', help='仅给该手机号客服添加任职(推荐)',
)
parser.add_argument(
'--all-kefu',
action='store_true',
help='给全部客服账号批量添加(慎用,会扫所有有 KefuProfile 的账号)',
)
parser.add_argument(
'--dry-run', action='store_true', help='只打印不写入',
)
def handle(self, *args, **options):
club_id = options['club_id']
role_code = options['role_code']
dry_run = options['dry_run']
target_phone = (options.get('phone') or '').strip()
all_kefu = options.get('all_kefu')
if not target_phone and not all_kefu:
raise CommandError(
'请指定 --phone 13800138000只配一个人或 --all-kefu批量全部客服'
'默认不会自动给所有人加任职。'
)
if target_phone:
target = User.query.filter(Phone=target_phone).first()
if not target:
raise CommandError(f'未找到手机号 {target_phone}')
kefu_users = [target]
else:
kefu_users = list(
User.query.filter(KefuProfile__isnull=False).select_related('KefuProfile')
)
created = 0
skipped = 0
with transaction.atomic():
for u in kefu_users:
if u.IsSuperuser:
skipped += 1
continue
exists = AdminAssignment.query.filter(
yonghuid=u.UserUID, club_id=club_id, role_code=role_code,
).exists()
if exists:
skipped += 1
continue
if dry_run:
self.stdout.write(f'would assign {u.UserUID} -> {club_id} {role_code}')
created += 1
continue
AdminAssignment.query.create(
yonghuid=u.UserUID,
club_id=club_id,
role_code=role_code,
data_scope=DATA_SCOPE_SINGLE,
is_primary=True,
status=1,
)
created += 1
label = ADMIN_ROLE_LABELS.get(role_code, role_code)
self.stdout.write(self.style.SUCCESS(
f'完成:新建 {created} 条,跳过 {skipped}club={club_id}, role={label}'
))

View File

@@ -0,0 +1,183 @@
"""从现网配置初始化 xq 俱乐部及覆盖表。"""
import logging
from decimal import Decimal
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import (
Club,
ClubHuiyuanPrice,
ClubLilubiao,
ClubWithdrawConfig,
UserWxOpenid,
)
from users.business_models import User
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '初始化星阙(xq)俱乐部主数据,并从 huiyuan/lilubiao 复制覆盖配置'
def handle(self, *args, **options):
with transaction.atomic():
club, created = self._ensure_club_xq()
self.stdout.write(self.style.SUCCESS(
f'club xq: {"新建" if created else "已存在"}'
))
self._copy_huiyuan_prices()
self._copy_lilubiao()
self._ensure_withdraw_config()
migrated = self._migrate_openid_bindings()
self.stdout.write(self.style.SUCCESS(f'user_wx_openid 迁移绑定: {migrated}'))
def _ensure_club_xq(self):
wx_v3 = dict(getattr(settings, 'WECHAT_PAY_V3_CONFIG', {}) or {})
defaults = {
'name': '星阙电竞',
'status': 1,
'wx_appid': getattr(settings, 'WEIXIN_APPID', ''),
'wx_secret': getattr(settings, 'WEIXIN_SECRET', ''),
'mch_id': getattr(settings, 'WEIXIN_MCHID', '') or wx_v3.get('MCHID', ''),
'pay_app_id': wx_v3.get('APPID', '') or getattr(settings, 'WEIXIN_APPID', ''),
'api_v3_key': wx_v3.get('API_V3_KEY', ''),
'cert_serial_no': wx_v3.get('CERT_SERIAL_NO', ''),
'private_key_path': wx_v3.get('PRIVATE_KEY_PATH', ''),
'platform_cert_dir': wx_v3.get('PLATFORM_CERT_DIR', ''),
'config_json': {
'mch_key': getattr(settings, 'WEIXIN_SHANGHUMIYAO', ''),
'transfer_scene_id': wx_v3.get('TRANSFER_SCENE_ID', '1005'),
},
'official_appid': getattr(settings, 'WEIXIN_OFFICIAL_APPID', ''),
'official_secret': getattr(settings, 'WEIXIN_OFFICIAL_SECRET', ''),
'official_token': getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', ''),
'encoding_aes_key': getattr(settings, 'WEIXIN_OFFICIAL_ENCODING_AES_KEY', ''),
'template_id': getattr(settings, 'WEIXIN_TEMPLATE_ID', ''),
'template_max_per_minute': getattr(settings, 'WEIXIN_TEMPLATE_MAX_PER_MINUTE', 950),
'h5_domain': getattr(settings, 'H5_DOMAIN', ''),
'goeasy_appkey': getattr(settings, 'GOEASY_APPKEY', ''),
'goeasy_secret': getattr(settings, 'GOEASY_SECRET', ''),
'sort_order': 0,
}
try:
club = Club.query.get(club_id=CLUB_ID_DEFAULT)
for k, v in defaults.items():
if k == 'config_json':
merged = dict(club.config_json or {})
for ck, cv in v.items():
if cv and not merged.get(ck):
merged[ck] = cv
club.config_json = merged
continue
if v and not getattr(club, k, None):
setattr(club, k, v)
# V3 字段允许用 settings 补全(证书路径等)
for field in ('api_v3_key', 'cert_serial_no', 'private_key_path', 'platform_cert_dir', 'pay_app_id'):
val = defaults.get(field)
if val and not getattr(club, field, None):
setattr(club, field, val)
club.save()
return club, False
except Club.DoesNotExist:
club = Club.query.create(club_id=CLUB_ID_DEFAULT, **defaults)
return club, True
def _copy_huiyuan_prices(self):
from products.models import Huiyuan
count = 0
for h in Huiyuan.query.all():
bankuai_id = getattr(h, 'bankuai_id', None)
existing = ClubHuiyuanPrice.query.filter(
club_id=CLUB_ID_DEFAULT, huiyuan_id=h.huiyuan_id
).first()
if existing:
continue
ClubHuiyuanPrice.query.create(
club_id=CLUB_ID_DEFAULT,
huiyuan_id=h.huiyuan_id,
jiage=h.jiage or Decimal('0'),
guanshifc=h.guanshifc or Decimal('0'),
zuzhangfc=h.zuzhangfc or Decimal('0'),
is_enabled=True,
bankuai_id=bankuai_id,
)
count += 1
self.stdout.write(f'club_huiyuan_price 新增 {count}')
def _copy_lilubiao(self):
from orders.models import CommissionRate
count = 0
for row in CommissionRate.query.all():
config_type = int(row.Platform or 0)
existing = ClubLilubiao.query.filter(
club_id=CLUB_ID_DEFAULT, config_type=config_type
).first()
if existing:
continue
ClubLilubiao.query.create(
club_id=CLUB_ID_DEFAULT,
config_type=config_type,
lilv=row.Rate or Decimal('0'),
remark='',
)
count += 1
self.stdout.write(f'club_lilubiao 新增 {count}')
def _ensure_withdraw_config(self):
from config.models import WithdrawConfig, TixianQuotaDefault
mode = 1
try:
wc = WithdrawConfig.query.first()
if wc:
mode = wc.mode
except Exception:
pass
try:
cfg = ClubWithdrawConfig.query.get(club_id=CLUB_ID_DEFAULT)
cfg.mode = mode
cfg.save(update_fields=['mode'])
except ClubWithdrawConfig.DoesNotExist:
ClubWithdrawConfig.query.create(club_id=CLUB_ID_DEFAULT, mode=mode)
self._copy_tixian_quota()
def _copy_tixian_quota(self):
from config.models import TixianQuotaDefault
from jituan.models import ClubTixianQuota
count = 0
for row in TixianQuotaDefault.query.all():
existing = ClubTixianQuota.query.filter(
club_id=CLUB_ID_DEFAULT, leixing=row.UserType,
).first()
if existing:
continue
ClubTixianQuota.query.create(
club_id=CLUB_ID_DEFAULT,
leixing=row.UserType,
daily_limit=row.default_quota or Decimal('0'),
)
count += 1
self.stdout.write(f'club_tixian_quota 新增 {count}')
def _migrate_openid_bindings(self):
migrated = 0
users = User.query.filter(OpenID__isnull=False).exclude(OpenID='')
for u in users:
exists = UserWxOpenid.query.filter(
club_id=CLUB_ID_DEFAULT, openid=u.OpenID
).exists()
if exists:
continue
UserWxOpenid.query.create(
club_id=CLUB_ID_DEFAULT,
openid=u.OpenID,
yonghuid=u.UserUID,
unionid=u.UnionID,
)
if hasattr(u, 'ClubID') and (not u.ClubID or u.ClubID != CLUB_ID_DEFAULT):
u.ClubID = CLUB_ID_DEFAULT
u.save(update_fields=['ClubID'])
migrated += 1
return migrated

View File

@@ -0,0 +1,61 @@
"""一键初始化星之界(xzj)俱乐部(默认不批量给所有客服加任职)。"""
from django.core.management import call_command
from django.core.management.base import BaseCommand
from jituan.models import Club
class Command(BaseCommand):
help = '创建星之界电竞俱乐部(xzj);任职请单独 seed_admin_assignments --phone ...'
def add_arguments(self, parser):
parser.add_argument(
'--wx-appid', default='wxdefa454152e78a03', help='星之界小程序 appid',
)
parser.add_argument(
'--assignments',
action='store_true',
help='同时给 --phone 指定客服加 xzj 任职(需配合 --phone',
)
parser.add_argument(
'--phone', type=str, default='', help='与 --assignments 联用:客服手机号',
)
def handle(self, *args, **options):
wx_appid = (options.get('wx_appid') or '').strip()
club_id = 'xzj'
name = '星之界电竞'
if Club.query.filter(club_id=club_id).exists():
self.stdout.write(self.style.WARNING(f'俱乐部 {club_id} 已存在,跳过 create_club'))
else:
self.stdout.write(f'创建俱乐部 {club_id} ...')
call_command(
'create_club',
club_id,
name=name,
wx_appid=wx_appid,
)
if options.get('assignments'):
phone = (options.get('phone') or '').strip()
if not phone:
self.stdout.write(self.style.WARNING(
'未指定 --phone跳过任职。示例'
'python manage.py seed_club_xzj --assignments --phone 你的手机号'
))
else:
call_command(
'seed_admin_assignments',
club_id=club_id,
phone=phone,
)
else:
self.stdout.write(
'未加任职。需要时在后台「数据范围」配置,或:'
'python manage.py seed_admin_assignments --phone 手机号 --club-id xzj'
)
self.stdout.write(self.style.SUCCESS(
f'星之界 {club_id} 就绪。请在后台「俱乐部配置」检查支付密钥。'
))

View File

@@ -0,0 +1,49 @@
"""补齐 gvsdsdk 权限码(可重复执行)。"""
import uuid
from django.core.management.base import BaseCommand
from django.utils import timezone
from gvsdsdk.models import Permission
# (PermCode, PermName, PermDesc, PermCategory)
EXTRA_PERMISSIONS = [
(
'dsks666',
'打手接单考试管理',
'配置各俱乐部考试开关、抽题规则与题库(小程序配置-打手接单考试)',
'小程序配置',
),
(
'sfbq666',
'身份装饰标签管理',
'管理小程序身份装饰标签(与用户管理-身份装饰标签菜单对应)',
'用户管理',
),
]
class Command(BaseCommand):
help = '写入 gvsdsdk Permission 表缺失的权限码(如 sfbq666'
def handle(self, *args, **options):
sample = Permission.objects.filter(PermStatus=1).first()
tenant_uuid = sample.TenantUUID if sample else uuid.UUID('00000000-0000-0000-0000-000000000001').bytes
created = 0
for code, name, desc, category in EXTRA_PERMISSIONS:
if Permission.objects.filter(PermCode=code).exists():
self.stdout.write(f' 已存在: {code}')
continue
Permission.objects.create(
PermUUID=uuid.uuid4().bytes,
TenantUUID=tenant_uuid,
PermCode=code,
PermName=name,
PermDesc=desc,
PermCategory=category,
PermStatus=1,
CreateTime=timezone.now(),
)
created += 1
self.stdout.write(self.style.SUCCESS(f' 新增权限: {code} - {name}'))
self.stdout.write(self.style.SUCCESS(f'完成,新增 {created} 条权限'))

View File

@@ -0,0 +1,152 @@
"""一次性写入客服后台菜单与权限映射。用法: python manage.py seed_kefu_menu"""
from django.core.management.base import BaseCommand
from jituan.models import KefuMenuPage
# 固定 page_id与前端 Layout 一致
MENU_ROWS = [
# 板块与财务
{'page_id': 'finance', 'name': '板块与财务', 'path': '', 'parent_id': '', 'sort_order': 10},
{'page_id': 'finance.bankuai', 'name': '板块配置', 'path': '/finance/bankuai', 'parent_id': 'finance', 'sort_order': 11,
'perm_codes': ['bankuai']},
{'page_id': 'finance.data', 'name': '财务数据', 'path': '/finance/data', 'parent_id': 'finance', 'sort_order': 12,
'perm_codes': ['caiwu']},
# 考核
{'page_id': 'assessment', 'name': '考核管理', 'path': '', 'parent_id': '', 'sort_order': 20},
{'page_id': 'assessment.examiner', 'name': '考核官管理', 'path': '/assessment/examiner', 'parent_id': 'assessment', 'sort_order': 21,
'perm_codes': ['kaohepeizhi']},
{'page_id': 'assessment.config', 'name': '考核配置', 'path': '/assessment/config', 'parent_id': 'assessment', 'sort_order': 22,
'perm_codes': ['kaohepeizhi']},
{'page_id': 'assessment.record', 'name': '考核记录', 'path': '/assessment/record', 'parent_id': 'assessment', 'sort_order': 23,
'perm_codes': ['kaohepeizhi']},
# 订单
{'page_id': 'order', 'name': '订单管理', 'path': '', 'parent_id': '', 'sort_order': 30},
{'page_id': 'order.platform', 'name': '平台订单', 'path': '/order/platform', 'parent_id': 'order', 'sort_order': 31,
'perm_codes': ['002ab']},
{'page_id': 'order.merchant', 'name': '商家订单', 'path': '/order/merchant', 'parent_id': 'order', 'sort_order': 32,
'perm_codes': ['002ac']},
# 跨平台
{'page_id': 'cross-platform', 'name': '跨平台管理', 'path': '', 'parent_id': '', 'sort_order': 40},
{'page_id': 'cross-platform.order', 'name': '跨平台订单', 'path': '/cross-order', 'parent_id': 'cross-platform', 'sort_order': 41,
'perm_codes': ['003aa']},
{'page_id': 'cross-platform.settings', 'name': '跨平台设置', 'path': '/cross-settings', 'parent_id': 'cross-platform', 'sort_order': 42,
'perm_codes': ['003aa']},
{'page_id': 'cross-platform.data', 'name': '跨平台数据', 'path': '/cross-data', 'parent_id': 'cross-platform', 'sort_order': 43,
'perm_codes': ['003aa']},
{'page_id': 'cross-platform.pre', 'name': '预结算管理', 'path': '/cross-pre', 'parent_id': 'cross-platform', 'sort_order': 44,
'perm_codes': ['003aa']},
# 用户
{'page_id': 'user', 'name': '用户管理', 'path': '', 'parent_id': '', 'sort_order': 50},
{'page_id': 'user.player', 'name': '打手管理', 'path': '/user/player', 'parent_id': 'user', 'sort_order': 51,
'perm_codes': ['001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg']},
{'page_id': 'user.guanli', 'name': '管事管理', 'path': '/user/guanli', 'parent_id': 'user', 'sort_order': 52,
'perm_codes': ['4400a', '4400b', '4400c', '4400d', '4400e', '4400f']},
{'page_id': 'user.shangjia', 'name': '商家管理', 'path': '/user/shangjia', 'parent_id': 'user', 'sort_order': 53,
'perm_codes': ['1100a', '1100b']},
{'page_id': 'user.zuzhang', 'name': '组长管理', 'path': '/user/zuzhang', 'parent_id': 'user', 'sort_order': 54,
'perm_codes': ['6600a', '6600b', '6600c', '6600d', '6600e']},
{'page_id': 'user.identity-tag', 'name': '身份装饰标签', 'path': '/user/identity-tag', 'parent_id': 'user', 'sort_order': 55,
'perm_codes': ['8080a', 'sfbq666']},
# 提现
{'page_id': 'withdraw', 'name': '提现管理', 'path': '', 'parent_id': '', 'sort_order': 60},
{'page_id': 'withdraw.audit', 'name': '提现审核', 'path': '/withdraw/audit', 'parent_id': 'withdraw', 'sort_order': 61,
'perm_codes': ['005bb']},
{'page_id': 'withdraw.detail', 'name': '提现详情', 'path': '/withdraw/detail', 'parent_id': 'withdraw', 'sort_order': 62,
'perm_codes': ['005bb']},
{'page_id': 'withdraw.data', 'name': '提现数据', 'path': '/withdraw/data', 'parent_id': 'withdraw', 'sort_order': 63,
'perm_codes': ['005bb']},
{'page_id': 'withdraw.settings', 'name': '提现设置', 'path': '/withdraw/settings', 'parent_id': 'withdraw', 'sort_order': 64,
'perm_codes': ['5500a', '5500b', '5500c']},
# 管理员
{'page_id': 'admin', 'name': '管理员管理', 'path': '', 'parent_id': '', 'sort_order': 70},
{'page_id': 'admin.role', 'name': '角色管理', 'path': '/admin/role', 'parent_id': 'admin', 'sort_order': 71,
'require_super': True},
{'page_id': 'admin.data-scope', 'name': '数据范围配置', 'path': '/admin/data-scope', 'parent_id': 'admin', 'sort_order': 72,
'require_group_manage': True},
{'page_id': 'admin.club-config', 'name': '俱乐部密钥配置', 'path': '/admin/club-config', 'parent_id': 'admin', 'sort_order': 73,
'require_super': True},
{'page_id': 'admin.user', 'name': '后台用户', 'path': '/admin/user', 'parent_id': 'admin', 'sort_order': 74,
'require_super': True},
{'page_id': 'admin.operation-log', 'name': '操作日志', 'path': '/admin/operation-log', 'parent_id': 'admin', 'sort_order': 75,
'perm_codes': ['czrz666']},
# 处罚
{'page_id': 'punishment', 'name': '处罚管理', 'path': '/punishment', 'parent_id': '', 'sort_order': 80,
'perm_codes': ['005aa', '66693a', '66693b', '66693c', '66694c', '99933abs']},
{'page_id': 'punishment.detail', 'name': '积分处罚详情', 'path': '/punishment/detail', 'parent_id': 'punishment', 'sort_order': 81,
'perm_codes': ['005aa', '66693a', '66693b', '66693c', '66694c', '99933abs']},
# 商品
{'page_id': 'product', 'name': '商品管理', 'path': '', 'parent_id': '', 'sort_order': 90},
{'page_id': 'product.list', 'name': '商品列表', 'path': '/product/list', 'parent_id': 'product', 'sort_order': 91,
'perm_codes': ['2200a']},
{'page_id': 'product.type-zone', 'name': '商品类型专区管理', 'path': '/product/type-zone', 'parent_id': 'product', 'sort_order': 92,
'perm_codes': ['7007a']},
{'page_id': 'product.data', 'name': '商品数据分析', 'path': '/product/data', 'parent_id': 'product', 'sort_order': 93,
'perm_codes': ['2200a']},
# 会员
{'page_id': 'member', 'name': '会员管理', 'path': '', 'parent_id': '', 'sort_order': 100},
{'page_id': 'member.list', 'name': '会员管理', 'path': '/member/list', 'parent_id': 'member', 'sort_order': 101,
'perm_codes': ['3300a']},
{'page_id': 'member.data', 'name': '会员数据分析', 'path': '/member/data', 'parent_id': 'member', 'sort_order': 102,
'perm_codes': ['huiyuancaiwu']},
# 小程序
{'page_id': 'miniapp', 'name': '小程序配置', 'path': '', 'parent_id': '', 'sort_order': 110},
{'page_id': 'miniapp.carousel', 'name': '轮播图与公告', 'path': '/miniapp/carousel', 'parent_id': 'miniapp', 'sort_order': 111,
'perm_codes': ['8080a']},
{'page_id': 'miniapp.assets', 'name': '图标与频道', 'path': '/miniapp/assets', 'parent_id': 'miniapp', 'sort_order': 112,
'perm_codes': ['8080a']},
{'page_id': 'miniapp.popup', 'name': '弹窗公告管理', 'path': '/miniapp/popup-notice', 'parent_id': 'miniapp', 'sort_order': 113,
'perm_codes': ['8080a']},
{'page_id': 'miniapp.withdraw-settings', 'name': '提现与收款设置', 'path': '/withdraw/settings', 'parent_id': 'miniapp', 'sort_order': 114,
'perm_codes': ['5500a', '5500b', '5500c']},
{'page_id': 'miniapp.leixing', 'name': '抢单商品类型', 'path': '/miniapp/leixing', 'parent_id': 'miniapp', 'sort_order': 115,
'perm_codes': ['8080a']},
{'page_id': 'miniapp.kaohe-tags', 'name': '抢单考核标签', 'path': '/miniapp/kaohe-tags', 'parent_id': 'miniapp', 'sort_order': 116,
'perm_codes': ['8080a']},
{'page_id': 'miniapp.dashou-exam', 'name': '打手接单考试', 'path': '/miniapp/dashou-exam', 'parent_id': 'miniapp', 'sort_order': 117,
'perm_codes': ['dsks666', '8080a']},
# 店铺
{'page_id': 'shop', 'name': '店铺管理', 'path': '', 'parent_id': '', 'sort_order': 120},
{'page_id': 'shop.list', 'name': '店铺列表', 'path': '/shop/list', 'parent_id': 'shop', 'sort_order': 121,
'perm_codes': ['999a', '999b', '999c', '999d', '999e']},
{'page_id': 'shop.withdraw', 'name': '店铺提现申请', 'path': '/shop/withdraw-apply', 'parent_id': 'shop', 'sort_order': 122,
'perm_codes': ['999a', '999b', '999c', '999d', '999e']},
{'page_id': 'shop.products', 'name': '店铺商品管理', 'path': '/shop/products', 'parent_id': 'shop', 'sort_order': 123,
'perm_codes': ['999a', '999b', '999c', '999d', '999e']},
# 会话
{'page_id': 'chat.agent', 'name': '会话管理', 'path': '/chat/agent', 'parent_id': '', 'sort_order': 130,
'perm_codes': ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4']},
]
class Command(BaseCommand):
help = '写入客服后台菜单页与权限映射(可重复执行,幂等更新)'
def handle(self, *args, **options):
created, updated = 0, 0
for row in MENU_ROWS:
defaults = {
'name': row['name'],
'path': row.get('path', ''),
'parent_id': row.get('parent_id', ''),
'sort_order': row.get('sort_order', 0),
'perm_codes': row.get('perm_codes', []),
'require_super': row.get('require_super', False),
'require_group_manage': row.get('require_group_manage', False),
}
existing = KefuMenuPage.query.filter(page_id=row['page_id']).first()
if existing:
for key, val in defaults.items():
setattr(existing, key, val)
existing.save()
updated += 1
else:
KefuMenuPage(page_id=row['page_id'], **defaults).save()
created += 1
self.stdout.write(self.style.SUCCESS(
f'kefu_menu_page 种子完成:新增 {created},更新 {updated},共 {len(MENU_ROWS)}'
))
try:
from django.core.management import call_command
call_command('seed_gvsdsdk_permissions')
except Exception as e:
self.stdout.write(self.style.WARNING(f'seed_gvsdsdk_permissions 跳过: {e}'))

View File

@@ -0,0 +1,55 @@
"""将指定手机号设为超级管理员(功能权限 + 集团视图)。"""
import uuid
from django.core.management.base import BaseCommand
from django.db import transaction
from django.utils import timezone
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
from jituan.constants import SUPER_ADMIN_PHONES
from users.business_models import User
class Command(BaseCommand):
help = '将 SUPER_ADMIN_PHONES 设为 IsSuperuser并绑定「管理员」角色含 000001'
def handle(self, *args, **options):
admin_role = Role.objects.filter(RoleName='管理员', RoleStatus=1).first()
super_perm = Permission.objects.filter(PermCode='000001', PermStatus=1).first()
if admin_role and super_perm:
if not RolePermission.objects.filter(
RoleUUID=admin_role.RoleUUID, PermUUID=super_perm.PermUUID,
).exists():
RolePermission.objects.create(
RolePermUUID=uuid.uuid4().bytes,
RoleUUID=admin_role.RoleUUID,
PermUUID=super_perm.PermUUID,
CreateTime=timezone.now(),
)
self.stdout.write(self.style.SUCCESS('已为「管理员」角色绑定 000001 权限'))
updated = 0
for phone in SUPER_ADMIN_PHONES:
users = list(User.query.filter(Phone=phone))
if not users:
self.stdout.write(self.style.WARNING(f'未找到手机号 {phone}'))
continue
for user in users:
with transaction.atomic():
user.IsSuperuser = True
user.IsStaff = True
user.save(update_fields=['IsSuperuser', 'IsStaff'])
if admin_role and not UserRole.objects.filter(
UserUUID=user.UserUUID, RoleUUID=admin_role.RoleUUID,
).exists():
UserRole.objects.create(
UserRoleUUID=uuid.uuid4().bytes,
UserUUID=user.UserUUID,
RoleUUID=admin_role.RoleUUID,
CreateTime=timezone.now(),
)
updated += 1
self.stdout.write(self.style.SUCCESS(
f'已设置超管: {phone} (UserUID={user.UserUID})'
))
self.stdout.write(self.style.SUCCESS(f'完成,更新 {updated} 个账号'))

View File

@@ -0,0 +1,40 @@
"""为每个启用俱乐部初始化 szjilu 收支流水行。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from config.models import Szjilu
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import Club
from jituan.services.szjilu_accounting import normalize_szjilu_club_id
class Command(BaseCommand):
help = '为每个俱乐部创建 szjilu 收支流水行(已有则跳过)'
def add_arguments(self, parser):
parser.add_argument(
'--club-id',
type=str,
default='',
help='仅处理指定 club_id默认处理全部启用俱乐部',
)
def handle(self, *args, **options):
club_id = (options.get('club_id') or '').strip()
if club_id:
club_ids = [normalize_szjilu_club_id(club_id)]
else:
club_ids = list(
Club.query.filter(status=1).values_list('club_id', flat=True)
)
if CLUB_ID_DEFAULT not in club_ids:
club_ids.append(CLUB_ID_DEFAULT)
created = 0
with transaction.atomic():
for cid in club_ids:
_, was_created = Szjilu.objects.get_or_create(club_id=cid)
if was_created:
created += 1
self.stdout.write(f'创建 szjilu: club_id={cid}')
self.stdout.write(self.style.SUCCESS(f'完成,新建 {created} 条 szjilu 记录'))

View File

@@ -0,0 +1,133 @@
"""星之界(xzj)邀请链种子数据:自动插入组长+管事(含邀请码),无需先登录小程序。"""
from django.core.management import call_command
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.models import Club, ClubHuiyuanPrice
from users.business_models import User
from users.models import UserBoss, UserGuanshi, UserZuzhang
from utils.invitationcode_utils import CreateInvitationCode
CLUB_ID = 'xzj'
WX_APPID = 'wxdefa454152e78a03'
# 固定种子账号(可重复执行,幂等)
SEED_ZUZHANG = {
'UserUID': '9900001',
'OpenID': 'xzj_seed_openid_zuzhang_v1',
'Phone': '19900000001',
'UserName': 'xzj_seed_zuzhang',
'nickname': '星之界种子组长',
}
SEED_GUANSHI = {
'UserUID': '9900002',
'OpenID': 'xzj_seed_openid_guanshi_v1',
'Phone': '19900000002',
'UserName': 'xzj_seed_guanshi',
'nickname': '星之界种子管事',
}
def _ensure_club():
if Club.query.filter(club_id=CLUB_ID).exists():
return
call_command(
'create_club',
CLUB_ID,
name='星之界电竞',
wx_appid=WX_APPID,
)
def _ensure_user(meta):
user = User.query.filter(UserUID=meta['UserUID']).first()
if not user:
user = User.query.filter(OpenID=meta['OpenID']).first()
if user:
if user.ClubID != CLUB_ID:
user.ClubID = CLUB_ID
user.save(update_fields=['ClubID'])
return user, False
user = User.query.create(
UserUID=meta['UserUID'],
UserName=meta['UserName'],
OpenID=meta['OpenID'],
Phone=meta['Phone'],
ClubID=CLUB_ID,
)
UserBoss.query.create(user=user, nickname=meta['nickname'])
return user, True
def _ensure_zuzhang(user):
if hasattr(user, 'ZuzhangProfile'):
return user.ZuzhangProfile, False
yaoqingma = CreateInvitationCode(str(user.UserUID))
z = UserZuzhang.query.create(
user=user,
yaoqingma=yaoqingma,
zhuangtai=1,
)
return z, True
def _ensure_guanshi(user, zuzhang_uid):
if hasattr(user, 'GuanshiProfile'):
return user.GuanshiProfile, False
yaoqingma = CreateInvitationCode(str(user.UserUID))
g = UserGuanshi.query.create(
user=user,
yaoqingma=yaoqingma,
yaoqingren=zuzhang_uid,
zhuangtai=1,
)
return g, True
class Command(BaseCommand):
help = '插入星之界种子组长+管事(含邀请码),一条命令即可邀请注册'
def add_arguments(self, parser):
parser.add_argument(
'--force-new-codes',
action='store_true',
help='已存在时重新生成邀请码(慎用)',
)
def handle(self, *args, **options):
with transaction.atomic():
_ensure_club()
zz_user, zz_new = _ensure_user(SEED_ZUZHANG)
zz_profile, zz_profile_new = _ensure_zuzhang(zz_user)
if options.get('force_new_codes') and not zz_profile_new:
zz_profile.yaoqingma = CreateInvitationCode(str(zz_user.UserUID))
zz_profile.save(update_fields=['yaoqingma'])
gs_user, gs_new = _ensure_user(SEED_GUANSHI)
gs_profile, gs_profile_new = _ensure_guanshi(gs_user, zz_user.UserUID)
if options.get('force_new_codes') and not gs_profile_new:
gs_profile.yaoqingma = CreateInvitationCode(str(gs_user.UserUID))
gs_profile.save(update_fields=['yaoqingma'])
price_cnt = ClubHuiyuanPrice.query.filter(club_id=CLUB_ID).count()
self.stdout.write(self.style.SUCCESS('=' * 60))
self.stdout.write(self.style.SUCCESS('星之界 xzj 邀请链种子数据已就绪'))
self.stdout.write(self.style.SUCCESS('=' * 60))
self.stdout.write(f'club_huiyuan_price 条数: {price_cnt}(与星阙相同则需后台 xzj 视图下单独改价)')
self.stdout.write('')
self.stdout.write('【组长】管事注册用这个邀请码guanshizhuce / zuzhangyqmzc:')
self.stdout.write(f' UserUID={zz_user.UserUID} Phone={SEED_ZUZHANG["Phone"]}')
self.stdout.write(f' 组长邀请码 inviteCode = {zz_profile.yaoqingma}')
self.stdout.write('')
self.stdout.write('【管事】打手注册用这个邀请码dashouzhuce:')
self.stdout.write(f' UserUID={gs_user.UserUID} Phone={SEED_GUANSHI["Phone"]}')
self.stdout.write(f' 管事邀请码 inviteCode = {gs_profile.yaoqingma}')
self.stdout.write('')
self.stdout.write('新建: 组长=%s 管事=%s' % (
'' if zz_new or zz_profile_new else '否(已存在)',
'' if gs_new or gs_profile_new else '否(已存在)',
))
self.stdout.write(self.style.SUCCESS('=' * 60))

20
jituan/middleware.py Normal file
View File

@@ -0,0 +1,20 @@
"""请求级俱乐部上下文中间件。"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
class ClubContextMiddleware:
"""
为每个请求注入 club_id / club_scope。
旧客户端不传 Header 时 club_id 恒为 xq不影响现网。
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.club_id = resolve_club_id_from_request(request)
request.club_scope = resolve_club_scope(request)
if not request.club_id:
request.club_id = CLUB_ID_DEFAULT
return self.get_response(request)

View File

@@ -0,0 +1,183 @@
# Generated for jituan multi-club platform
import gvsdsdk.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='Club',
fields=[
('club_id', models.CharField(max_length=16, primary_key=True, serialize=False, verbose_name='俱乐部ID')),
('name', models.CharField(max_length=64, verbose_name='展示名')),
('status', models.IntegerField(default=1, verbose_name='1启用0停用')),
('wx_appid', models.CharField(blank=True, default='', max_length=32)),
('wx_secret', models.CharField(blank=True, default='', max_length=128)),
('mch_id', models.CharField(blank=True, default='', max_length=32)),
('pay_app_id', models.CharField(blank=True, default='', max_length=32)),
('api_v3_key', models.CharField(blank=True, default='', max_length=128)),
('cert_serial_no', models.CharField(blank=True, default='', max_length=64)),
('private_key_path', models.CharField(blank=True, default='', max_length=255)),
('platform_cert_dir', models.CharField(blank=True, default='', max_length=255)),
('official_appid', models.CharField(blank=True, default='', max_length=32)),
('official_secret', models.CharField(blank=True, default='', max_length=128)),
('official_token', models.CharField(blank=True, default='', max_length=64)),
('encoding_aes_key', models.CharField(blank=True, default='', max_length=64)),
('template_id', models.CharField(blank=True, default='', max_length=64)),
('template_max_per_minute', models.IntegerField(default=950)),
('h5_domain', models.CharField(blank=True, default='', max_length=128)),
('oss_prefix', models.CharField(blank=True, default='', max_length=128)),
('goeasy_appkey', models.CharField(blank=True, default='', max_length=64)),
('goeasy_secret', models.CharField(blank=True, default='', max_length=128)),
('config_json', models.JSONField(blank=True, default=dict)),
('company_uuid', models.BinaryField(blank=True, null=True)),
('sort_order', models.IntegerField(default=0)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': '俱乐部',
'db_table': 'club',
},
),
migrations.CreateModel(
name='UserWxOpenid',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=16)),
('openid', models.CharField(db_index=True, max_length=64)),
('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
('unionid', models.CharField(blank=True, max_length=64, null=True)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'user_wx_openid',
'unique_together': {('club_id', 'openid')},
},
),
migrations.CreateModel(
name='AdminAssignment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('yonghuid', models.CharField(db_index=True, max_length=7)),
('club_id', models.CharField(blank=True, db_index=True, max_length=16, null=True, verbose_name='NULL=集团任职')),
('role_code', models.CharField(default='CLUB_ADMIN', max_length=50)),
('data_scope', models.CharField(default='SINGLE_CLUB', max_length=20)),
('is_primary', models.BooleanField(default=False)),
('granted_by', models.CharField(blank=True, default='', max_length=7)),
('status', models.IntegerField(default=1, verbose_name='1有效0停用')),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'admin_assignment',
'unique_together': {('yonghuid', 'club_id', 'role_code')},
},
),
migrations.CreateModel(
name='AdminAuditLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(blank=True, db_index=True, max_length=16, null=True)),
('operator_yonghuid', models.CharField(db_index=True, max_length=7)),
('operator_role_code', models.CharField(blank=True, default='', max_length=50)),
('target_yonghuid', models.CharField(blank=True, max_length=7, null=True)),
('action', models.CharField(db_index=True, max_length=64)),
('resource_type', models.CharField(blank=True, default='', max_length=32)),
('resource_id', models.CharField(blank=True, default='', max_length=64)),
('field_name', models.CharField(blank=True, default='', max_length=64)),
('value_before', models.TextField(blank=True, null=True)),
('value_after', models.TextField(blank=True, null=True)),
('request_ip', models.CharField(blank=True, default='', max_length=64)),
('remark', models.CharField(blank=True, default='', max_length=500)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'admin_audit_log',
'indexes': [models.Index(fields=['club_id', '-CreateTime'], name='admin_audit_club_time_idx')],
},
),
migrations.CreateModel(
name='ClubHuiyuanPrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=16)),
('huiyuan_id', models.CharField(db_index=True, max_length=6)),
('jiage', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('guanshifc', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('zuzhangfc', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('is_enabled', models.BooleanField(default=True)),
('bankuai_id', models.IntegerField(blank=True, null=True)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'club_huiyuan_price',
'unique_together': {('club_id', 'huiyuan_id')},
},
),
migrations.CreateModel(
name='ClubLilubiao',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=16)),
('config_type', models.IntegerField(db_index=True, verbose_name='同 lilubiao.fadanpingtai')),
('lilv', models.DecimalField(decimal_places=4, default=0, max_digits=10)),
('remark', models.CharField(blank=True, default='', max_length=255)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'club_lilubiao',
'unique_together': {('club_id', 'config_type')},
},
),
migrations.CreateModel(
name='ClubWithdrawConfig',
fields=[
('club_id', models.CharField(max_length=16, primary_key=True, serialize=False)),
('mode', models.IntegerField(default=1, verbose_name='1自动2手动')),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'club_withdraw_config',
},
),
migrations.CreateModel(
name='ClubTixianQuota',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=16)),
('leixing', models.IntegerField(db_index=True)),
('daily_limit', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'club_tixian_quota',
'unique_together': {('club_id', 'leixing')},
},
),
migrations.CreateModel(
name='ClubFadanFenhongLilv',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=16)),
('shenfen', models.IntegerField(db_index=True)),
('lilv', models.DecimalField(decimal_places=4, default=0, max_digits=10)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'club_fadan_fenhong_lilv',
'unique_together': {('club_id', 'shenfen')},
},
),
]

View File

@@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('jituan', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='adminauditlog',
name='operator_yonghuid',
field=models.CharField(db_index=True, max_length=16),
),
]

View File

@@ -0,0 +1,40 @@
# Generated manually — Role.club_id + 放宽 tupianpeizhi 唯一约束(关注快手可多图)
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('jituan', '0002_admin_audit_operator_widen'),
('config', '0006_display_club_id'),
]
operations = [
migrations.RunSQL(
sql="""
ALTER TABLE Role ADD COLUMN club_id VARCHAR(16) NOT NULL DEFAULT '';
CREATE INDEX role_club_id_idx ON Role (club_id);
UPDATE Role SET club_id = '' WHERE RoleName = '管理员';
UPDATE Role SET club_id = 'xq' WHERE club_id = '' AND RoleName <> '管理员';
""",
reverse_sql="ALTER TABLE Role DROP COLUMN club_id;",
),
migrations.RunSQL(
sql="""
SET @idx_exists := (
SELECT COUNT(1) FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'tupianpeizhi'
AND index_name = 'tupianpeizhi_club_id_ImageType_uniq'
);
SET @sql := IF(@idx_exists > 0,
'ALTER TABLE tupianpeizhi DROP INDEX tupianpeizhi_club_id_ImageType_uniq',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
""",
reverse_sql=migrations.RunSQL.noop,
),
]

View File

@@ -0,0 +1,32 @@
# Generated manually for kefu menu pages
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('jituan', '0003_role_club_id'),
]
operations = [
migrations.CreateModel(
name='KefuMenuPage',
fields=[
('page_id', models.CharField(max_length=64, primary_key=True, serialize=False, verbose_name='页面ID')),
('name', models.CharField(max_length=64, verbose_name='展示名')),
('path', models.CharField(blank=True, default='', max_length=128, verbose_name='路由路径,分组为空')),
('parent_id', models.CharField(blank=True, default='', db_index=True, max_length=64)),
('sort_order', models.IntegerField(default=0)),
('perm_codes', models.JSONField(blank=True, default=list, verbose_name='拥有任一权限即可见')),
('require_super', models.BooleanField(default=False, verbose_name='仅超级管理000001')),
('require_group_manage', models.BooleanField(default=False, verbose_name='仅集团超管任职')),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'kefu_menu_page',
'ordering': ['sort_order', 'page_id'],
},
),
]

View File

@@ -0,0 +1,56 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('jituan', '0004_kefu_menu_page'),
]
operations = [
migrations.CreateModel(
name='IdentityTag',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=16)),
('name', models.CharField(max_length=64, verbose_name='标签名')),
('color', models.CharField(default='#000000', max_length=32, verbose_name='主色')),
('flash_enabled', models.BooleanField(default=False, verbose_name='闪光特效')),
('texiao_json', models.TextField(blank=True, default='', verbose_name='特效JSON')),
('sort_order', models.IntegerField(default=0)),
('status', models.IntegerField(default=1, verbose_name='1启用0停用')),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'identity_tag',
'ordering': ['sort_order', 'id'],
},
),
migrations.CreateModel(
name='IdentityTagBind',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=16)),
('yonghuid', models.CharField(db_index=True, max_length=7)),
('shenfen', models.IntegerField(verbose_name='1打手2管事3商家4组长')),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('tag', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='binds', to='jituan.identitytag',
)),
],
options={
'db_table': 'identity_tag_bind',
},
),
migrations.AddIndex(
model_name='identitytagbind',
index=models.Index(fields=['club_id', 'yonghuid', 'shenfen'], name='identity_tag_bind_lookup'),
),
migrations.AlterUniqueTogether(
name='identitytagbind',
unique_together={('club_id', 'yonghuid', 'shenfen', 'tag')},
),
]

Some files were not shown because too many files have changed in this diff Show More