348 Commits

Author SHA1 Message Date
XingQue
2a99e2ea10 fix: 会员首次充值积分加满10分,后续充值不再加分
体验会员原仅加5分且正式会员会重复加分;改为首次充值会员(任意类型)补满至10分,此后任意会员充值不再增加积分。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 01:45:50 +08:00
XingQue
390202b9e4 fix: 资料页仅改头像不再误判为手机号认证
手机号认证流需同时带头像与 shoujihao_code 且无昵称;普通编辑只上传头像可正常保存。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 00:12:34 +08:00
XingQue
65dae4f3ab fix: 店铺二维码下载拒绝异常大文件
超过2MB的OSS对象视为路径错误,提示重新生成而非原样返回。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 00:04:21 +08:00
XingQue
df49631401 fix: 店铺二维码生成与下载接口
使用 stable access_token 并重试;规范化 OSS Key;下载接口返回 JSON 错误便于前端识别。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 23:51:00 +08:00
XingQue
90e187b65c chore: 补齐店铺管理权限码种子与SQL脚本
新增999a~999e与1199ab~1199abe权限定义,并提供可重复执行的查询/插入SQL。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 23:09:37 +08:00
XingQue
a92352f2b0 fix: 店铺管理接口超级管理员权限校验失效
改用 has_perm_code 替代 set(permissions) 交集判断,避免 _AllPermissions 被转成 set 后丢失超管放行能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 23:06:34 +08:00
XingQue
214b52badd feat: 新增后台复制店铺商品目录接口
支持按用户ID复制商品类型、专区与商品,OSS图片独立拷贝,并提供覆盖/补充两种模式。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 22:58:56 +08:00
581295443a 给所有支付接口加上了支付宝 2026-07-09 22:06:46 +08:00
XingQue
914e8c70f2 修复手机号认证完成后仍反复弹窗的问题
认证提交接口在仅上传头像时错误返回成功,检查接口也未兼容已有手机号的老数据。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 21:57:23 +08:00
7443fef6de 修复了认证上传失效问题 2026-07-09 21:09:39 +08:00
d734ce1dcc 测试 2026-07-09 21:05:09 +08:00
6c0a461a2b 修复了订单状态的验签问题 2026-07-09 20:49:26 +08:00
XingQue
f2295717ee Merge branch 'main' of https://git.gvsds.com/XingQue/Django 2026-07-09 20:42:11 +08:00
XingQue
fd7bcde9d9 fix: 店铺订单接口 PlatformOrderExt 查询字段修正
将 dianpu_id 改为模型字段 ShopID,修复 ddltj/ddlbhq 的 FieldError;统计聚合改用 id__in 匹配 Order 主键。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 20:39:05 +08:00
fb8d905fb0 修复了订单状态更新 BUG 2026-07-09 20:32:40 +08:00
f702c13422 修复了支付宝 fucha 逻辑 2026-07-09 20:28:12 +08:00
bf1088d0c5 修复二维码当面付问题 2026-07-09 20:06:01 +08:00
51edcf9fce 使用二维码进行支付宝交易 2026-07-09 19:50:40 +08:00
0826b0749d 修复了验签机制 2026-07-09 19:22:48 +08:00
aa3aa932fe 修复签名生成机制 2026-07-09 19:14:57 +08:00
5755730093 使用生产模式密钥 2026-07-09 18:06:24 +08:00
de3ff211a6 fixed: fucha接口硬编码了微信查询 2026-07-09 03:08:14 +08:00
e2b95edeb8 修复 URL 过长的问题 2026-07-09 02:53:52 +08:00
92abc0d9e7 修复导入配置 2026-07-09 02:06:52 +08:00
d7d38d9b08 修复了沙箱私钥解析问题 2026-07-09 01:58:38 +08:00
9c11698660 测试中,尝试接入支付宝(沙箱) 2026-07-09 01:51:18 +08:00
XingQue
d64042fe46 fix: 微信openid绑定重复时不污染外层事务,登录可正常回落
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 01:18:42 +08:00
XingQue
5754f6a509 fix: 微信登录并发注册撞唯一键时正确回落已有用户
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 01:12:46 +08:00
XingQue
d5fbc08364 fix: 自动提现按俱乐部解析微信openid,修复已登录仍提示微信登录 2026-07-09 01:06:08 +08:00
XingQue
b2a2da0f97 fix: 自动提现不再要求手机号/收款码,仅校验微信OpenID;手动提现保持原校验收款信息
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 22:16:26 +08:00
XingQue
c2db2ad583 fix: 总会员只读展开子会员类型,修复club_id默认xq查错配置;dddhq不再写库补会员
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 22:13:12 +08:00
XingQue
2248a61936 fix: 添加后台用户不再硬编码绑定客服角色,按所选俱乐部角色分配
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 21:47:50 +08:00
XingQue
39dfe3d52e fix: 统一微信用户定位,修复wechatlogin等UserName(openid)重复1062
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 21:28:23 +08:00
XingQue
1ffd92758d fix: 押金提现不再封号,统一加强打手/收款校验
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 21:06:28 +08:00
XingQue
ad33ee6c86 fix: User.ClubID默认xq导致星之界新用户误判,创建时置空并以wx绑定为准
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 18:34:45 +08:00
XingQue
d025287785 fix: 星之界扫码允许老xq账号通过本俱乐部管事码注册
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 18:31:14 +08:00
XingQue
e0c48073f1 fix: 老小程序不改端,后端兼容扫码注册(dashouzhuce/wdlyhdl)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 18:20:45 +08:00
XingQue
7fe49775b9 fix: dashouzhuce返回dashoustatus供前端识别注册成功
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 18:06:54 +08:00
XingQue
2225722ab5 fix: 邀请校验管事无ClubID时以当前小程序为准,注册失败打日志
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 18:06:22 +08:00
XingQue
aeaa54583a fix: 扫码注册统一用户查找与俱乐部校验,修复wdlyhdl与自动注册失败
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 18:00:29 +08:00
XingQue
f23c8a3360 fix: 俱乐部微信登录兼容旧用户,避免UserName重复
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 17:43:39 +08:00
XingQue
a47d4ee0a6 fix: 扫码注册按俱乐部微信配置换openid,Referer识别小程序
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 17:37:51 +08:00
XingQue
95d7e8d13d fix: 体验会员抢单池与正式同等展示,修复总会员子类型与club_id
clumber 传递 club_id 展开总会员子类型;充值单路径同步;不暴露 is_trial。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 16:51:42 +08:00
XingQue
f87a40fbd6 fix: 抢单池 clumber 只读合并有效期内充值单,体验会员按正式展示
不写库、不 sync;修复体验单已付但 huiyuangoumai 未落库时老端显示未开通。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 16:41:09 +08:00
XingQue
627eeeb29b fix: 修复 rollback --since 日期字符串解析报错
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 16:26:19 +08:00
XingQue
c464c31140 fix: 加固会员回滚命令,有未过期充值单绝不收回
增加 repair 指纹校验;减会员后重新付费不再误收。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 16:20:33 +08:00
XingQue
7c3f04e7f3 fix: 禁止过期充值单补开通 + 管理命令收回错误恢复会员
rollback_erroneous_member_restore:后台已减仍有效、充值单已过期仍显示有效。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:56:11 +08:00
XingQue
1c6e908f28 fix: 补 import _czjilu_entitlement_revoked
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:49:38 +08:00
XingQue
cec7d69199 fix: 停止每次拉单补回已删会员;后台移除标记充值单撤销
ddhq 不再全量 sync 充值单;jian_huiyuan 标记权益已撤销防复活;clumber 只读 huiyuangoumai。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:49:07 +08:00
XingQue
38fc52241f fix: dddhq 不再把 yaoqiuleixing 改成 0,原样返回 1+huiyuan_id
体验会员与正式同走 clumber 匹配;仅提现校验正式会员。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:41:25 +08:00
XingQue
15cef26d81 fix: 修复 dddhq resolve_effective_club_id 未导入导致 500
函数内局部 import 导致 UnboundLocalError,将会员刷新 import 提前到使用处。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:37:26 +08:00
XingQue
aaa745baf4 fix: dddhq与dashouxinxi每次请求同步刷新会员clumber
抢单池拉单前先 sync 权益;老端依赖 yaoqiuleixing=0 与 clumber 即时更新。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:34:10 +08:00
XingQue
12f9100b17 fix: 适配老小程序抢单池展示,登录clumber含体验会员
老端仅认 yaoqiuleixing=0 或 clumber.huiyuanid 严格匹配;统一 build_auth_clumber 与 dddhq 同源含 czjilu 兜底。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:27:58 +08:00
XingQue
bcfa30d5de fix: 抢单池展示只校验对应会员未过期,体验与正式同等
ddhq 不区分体验/正式;qiangdan 校验会员+过期;提现仍禁体验会员。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:21:01 +08:00
XingQue
7edb8f22cb fix: 抢单池体验会员按正式展示,修复 czjilu 权益被总会员逻辑短路
ddhq 仅展示层变通;qiangdan 仍按会员类型校验,提现仍禁体验会员。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:14:02 +08:00
XingQue
4eb74ae968 fix: 修复抢单池 clumber 合成时 naive/aware 时间比较报错
统一 czjilu 到期时间与 now 的时区类型,修复 ddhq 500。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:09:48 +08:00
XingQue
7819f1a897 fix: 体验会员本单有权限时 yaoqiuleixing 返回 0 仅适配老端展示
逐单判断会员类型:有权限的单显示可抢,无权限/过期仍显示未开通;抢单与提现后端校验不变。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:05:48 +08:00
XingQue
3ec821ae6a fix: 抢单池按订单会员类型逐单校验,取消任意会员放开全部单
会员单不再统一 yaoqiuleixing=0;clumber 裁剪为仅有权限的会员;ddhq 返回 hasRequiredMember。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:59:00 +08:00
XingQue
b54904a9f2 fix: 严格校验体验会员不可提现,抢单按会员类型匹配
展示层 clumber 与提现/抢单权限分离:佣金提现须正式会员并双重拦截体验期;抢单走 resolve_order_membership_ids + 总会员包含判断。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:26:16 +08:00
XingQue
abfe497ddc fix: 抢单池有体验/已付会员一律按已开通返回
/ddhq 有任意有效会员时 yaoqiuleixing 强制为 0;czjilu 已付单可合成 clumber;抢单校验支持充值单兜底。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:18:36 +08:00
XingQue
f46e36b539 fix: 老端抢单池有体验/正式会员时 yaoqiuleixing 返回 0
/ddhq 在用户有有效会员(含体验)时将 yaoqiuleixing 置 0,老小程序不再显示未开通会员;抢单仍由服务端校验。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:13:24 +08:00
XingQue
eedd5fbd46 fix: 体验会员按正式会员返回抢单池数据 + 修复 MerchantStaffMember
clumber/dashouxinxi 有效期内体验会员与正式会员同格式返回;ddhq 保持原 yaoqiuleixing/huiyuan_id 供老端匹配;补 merchant-staff 缺 import。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:09:12 +08:00
XingQue
86e7a60642 fix: 纯后端适配老小程序抢单池会员展示
/ddhq 有权限时返回 yaoqiuleixing=0;clumber 统一 huiyuanid+daoqi 字符串并补齐 ID 变体;读取前自动补写已付未落库的体验/正式会员。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:05:22 +08:00
XingQue
dfd4bed221 fix: 抢单池会员展示加强校验与 hasRequiredMember 直返
会员 ID 忽略前导零等价匹配;订单缺 huiyuan_id 时回退商品类型;接口直返 hasRequiredMember 并将已授权订单 yaoqiuleixing 置 0。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 13:56:54 +08:00
XingQue
3a2b8a2bba fix: 体验/总会员抢单池显示与接单权限
统一 clumber 返回 huiyuanid 并展开总会员子类型;抢单池接口对已具备会员权限的用户将 yaoqiuleixing 置 0,使小程序无需发版即可正常显示分佣并抢单。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 13:47:05 +08:00
XingQue
0062d495c9 fix: 回填命令兼容 FluentQuery(不用 iterator)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 00:14:29 +08:00
XingQue
7dc78acb73 fix: 会员充值链路加固(体验资格、积分分红、czjilu审计、余额抵扣)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 00:07:54 +08:00
XingQue
1e3d65e0e4 fix: 排行榜昵称与管事/组长统计字段对齐
管事/组长不再展示 openid,改读业务昵称;俱乐部榜补充无效/有效邀请与收益字段;管事按有效人数、组长按收益总额排序。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 21:49:36 +08:00
XingQue
bfd5a63cb4 fix: 订单类型接口与 menu-access 对齐鉴权,去掉业务 401
kfhqptddlx 不再校验 phone/UserType,仅 JWT + is_kefu_backend_account,避免菜单能进但类型接口 401 踢登录。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 19:35:23 +08:00
XingQue
6e2e1944b6 fix: 角色UUID bytes比对修复权限全空,legacy权限合并兜底 2026-07-06 19:28:35 +08:00
XingQue
2edca09859 fix: 俱乐部角色ORM过滤+商家订单权限002ab/003aa+legacy兜底+perm-debug 2026-07-06 19:25:15 +08:00
XingQue
7bb891187b fix: 按俱乐部隔离权限,menu-access 返回任职无角色提示 2026-07-06 19:21:38 +08:00
XingQue
a14cba63e6 fix: 集团可给各俱乐部分配不同功能角色,按俱乐部隔离权限校验 2026-07-06 19:14:00 +08:00
XingQue
ebcedbdea1 fix: 商家订单权限与菜单一致,含本俱乐部+全局角色
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 19:03:23 +08:00
XingQue
45a4dde034 fix: 切换俱乐部仅算本俱乐部角色权限,统一权限解析
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 19:00:18 +08:00
XingQue
553d2d5707 fix: 俱乐部+全局角色权限并集,修复002ac校验
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 18:44:11 +08:00
XingQue
1e54d8b871 fix: 多俱乐部任职即可切换,权限按当前俱乐部计算
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 18:40:42 +08:00
XingQue
df6c6912d9 fix: 恢复全局角色权限校验,修复商家订单403
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 18:36:39 +08:00
XingQue
64ddb7d0b8 fix: 俱乐部000001不再展开全部菜单权限
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 18:32:01 +08:00
XingQue
7c763ad5b4 fix: 顶栏仅可切换有功能角色的俱乐部
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 18:28:24 +08:00
XingQue
1c6607fa6e fix: 角色分配按俱乐部超管权限隔离
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 18:23:29 +08:00
XingQue
2bbe166d63 fix: 角色操作按俱乐部解析避免同名Role冲突
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 18:17:37 +08:00
XingQue
2573db1bbf fix: 俱乐部分配任职仅集团高层可操作
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 18:06:37 +08:00
XingQue
1dfb46c38c feat: 多俱乐部后台权限与管理员用户俱乐部分配
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 17:59:59 +08:00
f1c8633345 perf: P1 性能优化批量修复 + kefu 视图拆分
P1 性能优化(避免循环内 N+1 / N 次 DB 查询):
- shop_order_views._batch_images: 移除 ThreadPoolExecutor 掩盖的 N 次查询,改单次批量 + Python 侧分组
- product_query.DashouHuiyuanList: 5N 查询 -> 3 次批量预取 + 本地闭包判断
- roles.GetRolePermissionView / 用户列表: 循环内 RolePermission/Permission/UserRole/Role -> 批量 __in 预取
- guanli / zuzhang 杜次分红 4.2/4.3: 循环内 update_or_create + 单条 delete -> bulk_create + bulk_update + 批量 delete
- admin_config QQ 群配置: 循环内 exists/first/save/create -> 批量预取 + bulk_create + bulk_update
- jituan 服务层多处合并统计查询、批量预取 map
- rank 多处循环 N+1 改批量预取
- backend 多处循环内 count/create 改批量
- config/orders/merchant_ops 多处循环 N+1 改批量预取

其他改动:
- users/views/kefu.py 拆分为 kefu_base/kefu_dashou/kefu_orders/kefu_punishment/kefu_withdraw 5 个文件
- 删除遗留脚本 check_prod_uid.py / create_rbac_tables.sql
2026-07-05 23:17:38 +08:00
XingQue
76ea5f8255 fix: 假单管理补全 DATA_SCOPE_ALL 导入
修复 fake-grab-order-list 500 NameError。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 22:35:30 +08:00
XingQue
59a6deef73 fix: 菜单种子自动补全假单管理页
检测 product.fake-orders 缺失时自动执行 seed_kefu_menu。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 22:32:38 +08:00
XingQue
ddfd3d3ac7 feat: 假单后台管理接口(按俱乐部+商品类型 CRUD)
新增 fake-grab-order-list/manage/avatar 接口,权限 2200a;支持增删改价格介绍头像等;假单展示使用自定义头像。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 22:28:13 +08:00
XingQue
2a350a2ec8 feat: 会员充值明细接口与俱乐部一键创建总会员
新增 hy-recharge-records 查询 czjilu 会员单并关联 gsfenhong 分红;hy-bundle-create 支持俱乐部一步创建总会员;会员数据分析页权限改为 3300a。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 18:27:48 +08:00
XingQue
37bc8f960a feat: 总会员(包含式)模型、迁移、抢单校验与后台配置
- Huiyuan.is_bundle + club_huiyuan_bundle_include 表
- 购买/到期逻辑不变,抢单时总会员覆盖子会员类型
- dshyhq/clumber 返回 is_bundle 与 included_huiyuan_ids

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 18:12:57 +08:00
XingQue
5754a69e0b feat: 打手中心全量可配置图标下发与会员卡片图上传接口
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 17:36:31 +08:00
XingQue
69c7ffd962 fix: dashou 订单列表补全 ZHIDING_HF_TEXT 常量
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 17:06:32 +08:00
XingQue
9dbca25b4d fix: dshyhq 返回会员卡片图 card_image/card_bg
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 16:42:14 +08:00
XingQue
aaf0c95925 feat: 会员充值页卡片图与 miniapp 背景图配置 2026-07-05 16:25:42 +08:00
XingQue
3b91fb09ea merge: 合并 origin/main 视图拆分 (c5ae305) 2026-07-05 16:17:51 +08:00
c5ae30507b 进行了完整的视图拆分,同步了 merchant_ops 的修改 2026-07-05 10:01:09 +08:00
XingQue
26dcbc1dc9 fix: 商家老板可绑定为其他店子客服
允许已是商家老板的用户担任其他商家子客服,禁止绑自己的店。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 20:48:12 +08:00
9d98ac9e83 修复了常量归类 2026-07-04 19:58:19 +08:00
7608faf396 拆分了 config 2026-07-04 19:29:53 +08:00
63cb8e9ee9 尝试将 backend 做了视图拆分,来保证可维护性,若此版本存在生产环境问题应该回退 2026-07-04 18:52:00 +08:00
XingQue
0afeff8227 fix: 链接页接单打手ID与指定打手分离,手机号改为选填 2026-07-04 00:04:22 +08:00
XingQue
432387982e feat: 链接填单增加拼多多订单号/手机号校验并隐藏H5价格 2026-07-03 23:32:33 +08:00
XingQue
c7adec20db fix: 修复添加后台用户 RoleUUID 转换错误并严格校验角色绑定 2026-07-02 16:03:23 +08:00
XingQue
725a1386c3 fix: 完全回滚公告轮播身份校验代码,恢复改前行为
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 14:17:26 +08:00
XingQue
0fc9c85503 fix: 回滚公告轮播身份校验,避免前端整页无数据 2026-07-02 14:11:09 +08:00
XingQue
593ed1ab09 fix: 公告轮播page_key逻辑与商家充值轮询yonghuid字段 2026-07-02 14:05:38 +08:00
XingQue
2fb1411b78 fix: 客服订单详情罚款状态使用统一标签,支持平台审核中 2026-07-02 13:12:22 +08:00
XingQue
d8ac8521d4 公告轮播按用户身份限制 page_key,无身份仅可访问点单相关 2026-07-02 13:05:38 +08:00
XingQue
2dcd5cc9a6 fix: 平台审核仅zhuangtai=5,不再误纳入旧商家待缴纳 2026-07-02 02:54:49 +08:00
XingQue
0c40b326ea fix: 平台审核队列兼容旧商家罚款+列表筛选 2026-07-02 02:38:21 +08:00
XingQue
5d20d1a25e fix: 修复换打手权限、后台用户重复创建及角色绑定 2026-07-02 02:20:29 +08:00
XingQue
c21168ad2d 商家罚款增加平台审核:新增状态5、证据图、审核API;审核通过前不影响抢单,提现仍拦截未结案罚单 2026-07-02 01:53:25 +08:00
XingQue
9fe03727c5 feat(rank): 排行榜奖励活动期、领取审计与校验;修复邀请二维码路径 2026-07-02 00:40:50 +08:00
XingQue
9f4d57a677 fix: 组长邀请二维码改用 fighter 新路径,不再走 manager 2026-07-01 23:46:21 +08:00
XingQue
9b636f1711 fix: 管事/组长邀请二维码改用小程序新页面路径
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 23:43:45 +08:00
XingQue
7f7859c2a4 fix: 排行榜返回完整统计字段并跟随后台排序方案 2026-07-01 19:32:29 +08:00
XingQue
3394fef79b fix: 排行榜按方案档位展示各名次预计奖金 2026-07-01 19:17:43 +08:00
XingQue
2504997c53 fix: 集团总榜不展示俱乐部分奖行内金额
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 17:41:17 +08:00
XingQue
5ef4ff7c91 feat: 排行榜奖励按俱乐部分奖与手动领取
- 新增奖励方案/档位/结算/领取四表与懒结算服务
- 小程序 phbjlxx/phbjllq/phbjlphb 新接口,phbhqsj 零改动
- 后台 phbjhq/phbjbc/phbjjl 与 phbj666 权限菜单

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 17:40:56 +08:00
XingQue
cb03898986 chore: 排行榜奖励功能开发前备份标记
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 17:30:22 +08:00
XingQue
e533424de5 feat: 客服后台话术配置接口与会话管理菜单 2026-07-01 12:28:22 +08:00
XingQue
b2fcff303b feat: 小程序客服话术表与自动回复接口
- 新增 MiniappScriptScene / MiniappScriptAutoReply 模型与迁移
- 新增 /peizhi/huashuhq、/peizhi/huashu_match 接口及 Admin 配置

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 11:59:21 +08:00
XingQue
c7270bcff7 chore: 客服话术接口与扫码兼容改造前备份标记
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 11:55:56 +08:00
XingQue
3452b0fb69 fix: 小程序扫码二维码路径改为 fighter 页,修复打手/管事邀请注册 2026-06-30 13:53:43 +08:00
XingQue
94fd40618e fix: 恢复 update_shangjia_daily 导入,修复链接派单 500
误删 backend.utils 导入导致 /peizhi/kehutx 提交订单 NameError。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 06:58:08 +08:00
XingQue
634796b012 feat: H5链接配对群ID与客服订单详情扩展(指定响应/罚款) 2026-06-29 06:51:49 +08:00
XingQue
31247b7915 feat: 指定单打手响应查询与提交接口
新增 zdcx/zdhf 接口及订单表指定响应字段,供抢单页想接/不想接/等会接。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 05:51:25 +08:00
XingQue
0de3c164fb 聊天:配对群ID、历史订单与状态接口、抢单建群对齐 2026-06-29 05:23:27 +08:00
XingQue
645b075f89 feat: add dashou deposit increase requires group executive
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 01:51:31 +08:00
XingQue
fb495197b8 fix: zuzhang rank today query uses UserZuzhang not UserBoss
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 01:44:31 +08:00
XingQue
dc71c4f1cb feat: gate withdraw audit and add-balance to group executives only
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 01:30:56 +08:00
XingQue
dd9e4afa49 revert: restore gvsdsdk role/permission APIs; keep KefuProfile login fix
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 00:02:40 +08:00
XingQue
42cb117a8a fix: kefu permissions use legacy permission table instead of gvsdsdk
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 23:59:32 +08:00
XingQue
c336a893a0 fix: merge legacy user_role permissions with gvsdsdk for kefu login 2026-06-28 23:27:24 +08:00
XingQue
188792328e fix: menu access fallback and bind kefu role by yonghuid 2026-06-28 23:24:26 +08:00
XingQue
218001399c fix: bind kefu role by yonghuid when phone duplicated 2026-06-28 23:18:14 +08:00
XingQue
de91089743 fix: clearer no-perm message with bound roles 2026-06-28 23:11:50 +08:00
XingQue
a45bc150e8 fix: kefu menu builtin fallback when DB empty, fix menu load 2026-06-28 23:03:41 +08:00
XingQue
d7c09e230f fix: menu_access 返回 permission_codes 便于排查无菜单权限 2026-06-28 22:47:57 +08:00
XingQue
3d2fb61df5 fix: 假单打乱时间比较改用时间戳,避免 naive/aware 相减报错 2026-06-28 18:41:42 +08:00
XingQue
b152f95ef8 fix: 假单打乱时间比较统一为 timezone-aware
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 18:37:14 +08:00
XingQue
22f1781ea2 fix: 假单打乱顺序持久化到OrderSequence,2分钟内全用户一致
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 18:33:36 +08:00
XingQue
872dbedb6e fix: 假单2分钟窗口内固定顺序,避免刷新即变序
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 18:23:25 +08:00
XingQue
5c7bb82a7b feat: 假单列表全局2分钟打乱一次,全用户顺序一致
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 18:22:57 +08:00
XingQue
48bb43d9a8 fix: 假单头像beijing路径;JD前缀假单统一拒绝抢单
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 18:07:38 +08:00
XingQue
7c9db3f360 feat: 假单种子脚本、商家名称字段、返回随机顺序与时间
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 17:41:32 +08:00
XingQue
b050a6895b feat: 抢单大厅假单池,仅改造 ddhq 接口
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 17:20:53 +08:00
XingQue
2003ee01a5 fix: 俱乐部000001不再展示集团级后台菜单与俱乐部密钥配置
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 16:52:25 +08:00
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
419 changed files with 90234 additions and 56208 deletions

View File

@@ -1,2 +0,0 @@
# 3. 在项目的 `__init__.py` 文件与manage.py同级添加两行代码
# 文件路径a_long_dianjing/__init__.py

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
@@ -317,3 +384,9 @@ WECHAT_PAY_V3_CONFIG = {
'NOTIFY_URL': 'https://www.abas.asia/hqhd/yonghu/tixiancallback',
'TRANSFER_SCENE_ID': '1005',
}
# ==================== 支付宝支付 ====================
ALIPAY_SANDBOX = app_secrets.ALIPAY_SANDBOX
ALIPAY_PROD = app_secrets.ALIPAY_PROD
ALIPAY_SANDBOX_CFG = app_secrets.ALIPAY_SANDBOX_CFG

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

@@ -0,0 +1,259 @@
"""
店铺商品目录复制服务:复制店铺商品类型、专区、商品(含 OSS 图片独立拷贝)。
"""
import logging
import time
import uuid
from django.db import transaction
from products.models import Shangpin, ShangpinZhuanqu
from shop.models import Dianpu, ShangpinLeixingDianpu
from utils.oss_utils import copy_in_oss, delete_from_oss, _url_to_oss_key
logger = logging.getLogger('houtai')
COPY_MODE_REPLACE = 'replace'
COPY_MODE_APPEND = 'append'
class ShopCopyError(Exception):
def __init__(self, message, code=400):
super().__init__(message)
self.message = message
self.code = code
def resolve_dianpu_by_user_uid(user_uid):
"""通过小程序用户 IDYonghuPingzheng.yonghu全局唯一定位店铺"""
uid = str(user_uid or '').strip()
if not uid:
raise ShopCopyError('用户ID不能为空')
try:
return Dianpu.objects.select_related('pingzheng').get(pingzheng__yonghu=uid)
except Dianpu.DoesNotExist:
raise ShopCopyError(f'未找到用户ID {uid} 对应的店铺', 404)
def _gen_dest_key(target_dianpu_id, category, src_url):
src_key = _url_to_oss_key(src_url)
ext = 'png'
if src_key and '.' in src_key:
ext = src_key.rsplit('.', 1)[-1].lower() or 'png'
ts = int(time.time() * 1000)
rand = uuid.uuid4().hex[:8]
return f"dianpu/shangpin/{category}/{target_dianpu_id}_{ts}_{rand}.{ext}"
def _copy_image_url(src_url, target_dianpu_id, category):
"""复制 OSS 图片到目标店铺独立路径,返回相对 URL"""
if not src_url:
return None
dest_key = _gen_dest_key(target_dianpu_id, category, src_url)
if not copy_in_oss(src_url, dest_key):
raise ShopCopyError(f'图片复制失败: {src_url}')
return dest_key
def _clear_target_catalog_db_only(target_dianpu):
"""全部覆盖模式:仅删除目标店铺商品目录 DB 记录,返回待清理的 OSS URL"""
dianpu_id = target_dianpu.id
oss_to_delete = []
products = list(Shangpin.query.filter(dianpu_id=dianpu_id))
for p in products:
if p.tupian_url:
oss_to_delete.append(p.tupian_url)
if p.guize_tupian:
oss_to_delete.append(p.guize_tupian)
if products:
Shangpin.query.filter(dianpu_id=dianpu_id).delete()
ShangpinZhuanqu.query.filter(dianpu_id=dianpu_id).delete()
types = list(ShangpinLeixingDianpu.query.filter(dianpu_id=dianpu_id))
for t in types:
if t.tupian_url:
oss_to_delete.append(t.tupian_url)
if types:
ShangpinLeixingDianpu.query.filter(dianpu_id=dianpu_id).delete()
return oss_to_delete
def _delete_oss_urls(urls):
for url in urls:
if url:
delete_from_oss(url)
def _pre_copy_source_images(source_dianpu, target_dianpu):
"""
预复制源店铺所有图片到目标店铺 OSS 路径。
返回结构化的预复制数据,供事务内写库使用。
"""
src_id = source_dianpu.id
tgt_id = target_dianpu.id
source_types = list(
ShangpinLeixingDianpu.query.filter(dianpu_id=src_id).order_by('paixu', 'id')
)
type_payloads = []
for st in source_types:
type_payloads.append({
'source': st,
'tupian_url': _copy_image_url(st.tupian_url, tgt_id, 'shangpinleixing'),
})
source_zones = list(
ShangpinZhuanqu.query.filter(dianpu_id=src_id).order_by('paixu', 'id')
)
zone_payloads = []
for sz in source_zones:
zone_payloads.append({'source': sz})
source_products = list(
Shangpin.query.filter(dianpu_id=src_id).order_by('paixu', 'id')
)
product_payloads = []
for sp in source_products:
product_payloads.append({
'source': sp,
'tupian_url': _copy_image_url(sp.tupian_url, tgt_id, 'shangpin'),
'guize_tupian': _copy_image_url(sp.guize_tupian, tgt_id, 'shangpinguize') if sp.guize_tupian else None,
})
return type_payloads, zone_payloads, product_payloads
def _insert_copied_catalog(target_dianpu, type_payloads, zone_payloads, product_payloads):
"""在目标店铺插入复制后的类型、专区、商品,维护 ID 映射"""
type_id_map = {}
zone_id_map = {}
for item in type_payloads:
st = item['source']
new_type = ShangpinLeixingDianpu.query.create(
dianpu_id=target_dianpu.id,
gonggong_leixing_id=st.gonggong_leixing_id,
jieshao=st.jieshao,
tupian_url=item['tupian_url'],
paixu=st.paixu,
shangjia_zhuangtai=st.shangjia_zhuangtai,
fengjin_zhuangtai=st.fengjin_zhuangtai,
shenhe_zhuangtai=st.shenhe_zhuangtai,
)
type_id_map[st.id] = new_type.id
for item in zone_payloads:
sz = item['source']
new_dianpu_leixing_id = type_id_map.get(sz.dianpu_leixing_id)
if not new_dianpu_leixing_id:
raise ShopCopyError(f'专区「{sz.mingzi}」关联的商品类型在复制映射中缺失')
new_zone = ShangpinZhuanqu.query.create(
dianpu_id=target_dianpu.id,
mingzi=sz.mingzi,
dianpu_leixing_id=new_dianpu_leixing_id,
leixing_id=sz.leixing_id,
paixu=sz.paixu,
shangjia_zhuangtai=sz.shangjia_zhuangtai,
fengjin_zhuangtai=sz.fengjin_zhuangtai,
shenhezhuangtai=sz.shenhezhuangtai,
)
zone_id_map[sz.id] = new_zone.id
copied_products = 0
for item in product_payloads:
sp = item['source']
new_dianpu_leixing_id = type_id_map.get(sp.dianpu_leixing_id)
new_zhuanqu_id = zone_id_map.get(sp.zhuanqu_id) if sp.zhuanqu_id else None
if sp.dianpu_leixing_id and not new_dianpu_leixing_id:
raise ShopCopyError(f'商品「{sp.biaoqian}」关联的商品类型在复制映射中缺失')
if sp.zhuanqu_id and not new_zhuanqu_id:
raise ShopCopyError(f'商品「{sp.biaoqian}」关联的专区在复制映射中缺失')
Shangpin.query.create(
biaoqian=sp.biaoqian,
jiage=sp.jiage,
kucun=sp.kucun,
leixing_id=sp.leixing_id,
zhuanqu_id=new_zhuanqu_id,
zhenshi_xiaoliang=0,
duiwai_xiaoliang=0,
jieshao=sp.jieshao,
xiadan_xuzhi=sp.xiadan_xuzhi,
guize_tupian=item['guize_tupian'],
yaoqiuleixing=sp.yaoqiuleixing,
huiyuan_id=sp.huiyuan_id,
yongjin=sp.yongjin,
kaioi_ewai_dashou_fencheng=sp.kaioi_ewai_dashou_fencheng,
ewai_dashou_fencheng=sp.ewai_dashou_fencheng,
tupian_url=item['tupian_url'],
paixu=sp.paixu,
shenhezhuangtai=sp.shenhezhuangtai,
dianpu_id=target_dianpu.id,
dianpu_leixing_id=new_dianpu_leixing_id,
shangjia_zhuangtai=sp.shangjia_zhuangtai,
fengjin_zhuangtai=sp.fengjin_zhuangtai,
shenhe_zhuangtai=sp.shenhe_zhuangtai,
)
copied_products += 1
return {
'types': len(type_payloads),
'zones': len(zone_payloads),
'products': copied_products,
}
def copy_shop_catalog(source_user_uid, target_user_uid, copy_mode=COPY_MODE_APPEND):
"""
复制店铺商品目录。
:param source_user_uid: 源店铺小程序用户 IDYonghuPingzheng.yonghu唯一
:param target_user_uid: 目标店铺小程序用户 ID
:param copy_mode: 'replace' 全部覆盖 | 'append' 补充追加
"""
if copy_mode not in (COPY_MODE_REPLACE, COPY_MODE_APPEND):
raise ShopCopyError('copy_mode 无效,仅支持 replace 或 append')
source_dianpu = resolve_dianpu_by_user_uid(source_user_uid)
target_dianpu = resolve_dianpu_by_user_uid(target_user_uid)
if source_dianpu.id == target_dianpu.id:
raise ShopCopyError('源店铺与目标店铺不能相同')
# 1. 预复制 OSS 图片DB 变更前完成,失败则目标店不受影响)
type_payloads, zone_payloads, product_payloads = _pre_copy_source_images(
source_dianpu, target_dianpu
)
oss_cleanup = []
# 2. 数据库事务:锁定目标店 → 可选清空 → 写入
with transaction.atomic():
Dianpu.objects.select_for_update().get(id=target_dianpu.id)
if copy_mode == COPY_MODE_REPLACE:
oss_cleanup = _clear_target_catalog_db_only(target_dianpu)
stats = _insert_copied_catalog(target_dianpu, type_payloads, zone_payloads, product_payloads)
if oss_cleanup:
_delete_oss_urls(oss_cleanup)
logger.info(
'店铺商品复制完成: source=%s(%s) -> target=%s(%s) mode=%s stats=%s',
source_dianpu.id, source_user_uid,
target_dianpu.id, target_user_uid,
copy_mode, stats,
)
return {
'source_dianpu_id': source_dianpu.id,
'source_dianpu_name': source_dianpu.dianpu_mingcheng,
'target_dianpu_id': target_dianpu.id,
'target_dianpu_name': target_dianpu.dianpu_mingcheng,
'copy_mode': copy_mode,
'copied_types': stats['types'],
'copied_zones': stats['zones'],
'copied_products': stats['products'],
}

View File

@@ -6,15 +6,17 @@ 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,
ShopProductTypeMappingView, ProductListView, ProductModifyView, ShopCopyCatalogView, KefuChatPermissionsView,
FineApplyView, PunishDashouView,
FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView,
FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView, FaKuanPingTaiShenHeView,
HqbkxxView,BkxgView,CaiwuView,CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView,
CwqtczhqView, KhpzhqView, ChzsgcView,KhgglView, ShgxgsjView, ZxkfghdsView)
CwqtczhqView, KhpzhqView, ChzsgcView,KhgglView, ShgxgsjView, KhjlglView, KhjlczView, ZxkfghdsView)
from config.views import MiniappScriptHoutaiListView, MiniappScriptHoutaiModifyView
from rank.reward_houtai_views import RankRewardHoutaiListView, RankRewardHoutaiSaveView, RankRewardHoutaiClaimsView
urlpatterns = [
path('gljshq', GetRolePermissionView.as_view(), name='获取就角色信息'),
@@ -46,7 +48,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='后台获取组长列表'),
@@ -60,6 +65,11 @@ urlpatterns = [
path('htxgfl', ModifyRateView.as_view(), name='后台修改费率'),
path('hthqtcxx', PopupNoticeListAPIView.as_view(), name='后台获取弹窗配置'),
path('htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='后台更新创建弹窗配置'),
path('hthqhs', MiniappScriptHoutaiListView.as_view(), name='后台获取话术配置'),
path('htxghs', MiniappScriptHoutaiModifyView.as_view(), name='后台更新话术配置'),
path('phbjhq', RankRewardHoutaiListView.as_view(), name='后台排行榜奖励配置'),
path('phbjbc', RankRewardHoutaiSaveView.as_view(), name='后台保存排行榜奖励'),
path('phbjjl', RankRewardHoutaiClaimsView.as_view(), name='后台排行榜领取明细'),
path('hqdplbsj', ShopListView.as_view(), name='获取店铺列表数据'),
path('xgdpxx', ShopModifyView.as_view(), name='修改或添加店铺数据'),
@@ -72,6 +82,7 @@ urlpatterns = [
path('hqhtdpsplx', ProductListView.as_view(), name='后台获取店铺商品数据'),
path('htxgdpspsj', ProductModifyView.as_view(), name='后台修改店铺商品数据'),
path('htfzdpspsj', ShopCopyCatalogView.as_view(), name='后台复制店铺商品目录'),
path('kfhqltqx', KefuChatPermissionsView.as_view(), name='后台获取客服聊天配置'),
@@ -79,6 +90,7 @@ urlpatterns = [
path('htglyhqcfsltj', FaKuanTongJiView.as_view(),name='后台罚单统计图片'), # 罚款统计
path('hthqfklb', FaKuanLieBiaoView.as_view(),name='后台罚单列表'), # 罚款列表
path('glyclfk', FaKuanChuLiView.as_view(),name='后台罚单操作'), # 处理罚款
path('glyptshfk', FaKuanPingTaiShenHeView.as_view(), name='后台罚款平台审核'),
path('htfksc', FaKuanChuangJianView.as_view(),name='后台生成罚单'), # 创建罚款
path('kffkdssq', FineApplyView.as_view(), name='客服罚款申请'), # 客服罚款申请
path('kpcf', PunishDashouView.as_view(), name='客服处罚打手'), # 客服处罚打手
@@ -100,6 +112,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

View File

754
backend/views/exams.py Normal file
View File

@@ -0,0 +1,754 @@
import hmac
import threading
import traceback
import uuid
import hashlib
import xmltodict
import time
import logging
import requests
import os
import secrets
import random
import string
from calendar import monthrange
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
# 工具类导入
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.catalog_scope import block_non_group_catalog_scope
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
from ..utils import (
verify_kefu_permission,
PERMISSION_TO_SHENFEN,
SHENFEN_PROFILE_MAP,
has_fadan_view_permission,
has_merchant_order_permission,
check_fadan_permission,
pick_order_id,
pick_penalty_create_params,
write_xiugai_log,
XIUGAI_LEIXING_DASHOU,
XIUGAI_LEIXING_GUANSHI,
XIUGAI_LEIXING_SHANGJIA,
XIUGAI_LEIXING_ZUZHANG,
XIUGAI_LEIXING_LABEL,
)
# models 集中导入
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
## backend
from backend.models import WithdrawalDailyStats
## users
from users.models import (
UserGuanshi, UserBoss, UserZuzhang,
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
)
from users.business_models import User
## orders
from orders.models import (
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
OrderPlayerHistory, Order, RefundRecord,
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
)
## shop
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
## products
from products.models import (
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
)
## config
from config.models import (
WithdrawConfig, ShangjiaLianjie, AccountPermission,
TixianQuotaDefault,
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
)
## rank
from rank.models import (
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
)
# 序列化器
from ..serializers import PopupPageSerializer
# 全局常量、日志对象
logger = logging.getLogger('houtai')
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
# 审核记录状态映射(原位于 zxkf.py按功能组归类到考核模块
SHENHE_ZHUANGTAI_MAP = {
0: '考核中',
1: '已通过',
2: '未通过',
3: '待审核',
}
class KhgglView(APIView):
"""
考核官管理数据接口(列表、筛选、分页)
POST /houtai/khggl
Body: {
"phone": "管理员手机号",
"page": 1, // 可选默认1
"page_size": 15, // 可选默认15
"yonghuid": "1234567", // 按用户ID筛选可选
"zhuangtai": 1, // 账号状态 0=禁用,1=正常,可选
"shenhe_zhuangtai": 0, // 审核官状态 0=空闲,1=审核中,可选
"bankuai_name": "王者" // 板块名称模糊筛选,可选
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'kaohepeizhi' not in permissions:
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
# 分页参数
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 15))
if page < 1: page = 1
if page_size < 1: page_size = 15
# 筛选条件
filter_yonghuid = request.data.get('yonghuid', '').strip()
filter_zhuangtai = request.data.get('zhuangtai')
filter_shenhe_zhuangtai = request.data.get('shenhe_zhuangtai')
filter_bankuai_name = request.data.get('bankuai_name', '').strip()
# 基础查询:所有审核官,预加载用户及打手扩展表
queryset = filter_user_related_by_club(
UserShenheguan.query.select_related('user__DashouProfile'),
request,
)
if filter_yonghuid:
queryset = queryset.filter(user__UserUID=filter_yonghuid)
if filter_zhuangtai is not None:
queryset = queryset.filter(zhuangtai=filter_zhuangtai)
if filter_shenhe_zhuangtai is not None:
queryset = queryset.filter(shenhe_zhuangtai=filter_shenhe_zhuangtai)
# 如果填写了板块名称,筛选出关联该板块的审核官
if filter_bankuai_name:
bankuai_ids = Bankuai.query.filter(
mingcheng__icontains=filter_bankuai_name
).values_list('bankuai_id', flat=True)
shenheguan_ids = KaoheguanBankuai.query.filter(
bankuai_id__in=bankuai_ids
).values_list('kaoheguan_id', flat=True)
queryset = queryset.filter(user__UserUID__in=shenheguan_ids)
total = queryset.count()
start = (page - 1) * page_size
end = start + page_size
shenheguan_list = queryset.order_by('-CreateTime')[start:end]
from collections import defaultdict
from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request
# ---------- 优化板块打手数量1 次 annotate 替代 2N 次循环查询)----------
# 原逻辑:每板块查 dashou 标签 ID再统计关联的不同用户数要求有 DashouProfile
bankuai_dashou_qs = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(
chenghao__leixing='dashou',
yonghu__DashouProfile__isnull=False,
),
request,
)
bankuai_dashou_count_map = {
item['chenghao__bankuai_id']: item['user_count']
for item in bankuai_dashou_qs.values('chenghao__bankuai_id').annotate(
user_count=Count('yonghu', distinct=True)
)
}
# ---------- 优化板块标签统计1 次 annotate 替代 N*M 次循环查询)----------
# 原逻辑:每板块每标签分别统计关联的不同用户数(不要求 DashouProfile
tag_counts_qs = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(chenghao__leixing='dashou'),
request,
).values(
'chenghao_id', 'chenghao__bankuai_id', 'chenghao__mingcheng'
).annotate(user_count=Count('yonghu', distinct=True))
tags_by_bankuai = defaultdict(list)
for item in tag_counts_qs:
tags_by_bankuai[item['chenghao__bankuai_id']].append({
'id': item['chenghao_id'],
'name': item['chenghao__mingcheng'],
'user_count': item['user_count'],
})
all_banquai = Bankuai.query.all()
bankuai_tags_data = [
{
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'tags': tags_by_bankuai.get(bk.bankuai_id, []),
}
for bk in all_banquai
]
# ---------- 优化审核官板块关联1 次批量查询替代 P 次循环查询)----------
shenheguan_user_uids = [sg.user.UserUID for sg in shenheguan_list]
all_kgs = KaoheguanBankuai.query.filter(
kaoheguan_id__in=shenheguan_user_uids
).select_related('bankuai')
kg_map = defaultdict(list)
for kg in all_kgs:
kg_map[kg.kaoheguan_id].append(kg)
# 构造返回数据(审核官列表)
data_list = []
for sg in shenheguan_list:
user = sg.user
bankuai_info = [
{
'bankuai_id': kg.bankuai.bankuai_id,
'mingcheng': kg.bankuai.mingcheng,
'dashou_count': bankuai_dashou_count_map.get(kg.bankuai.bankuai_id, 0),
}
for kg in kg_map.get(user.UserUID, [])
]
dashou_nick = user.DashouProfile.nicheng if hasattr(user, 'DashouProfile') and user.DashouProfile else ''
data_list.append({
'yonghuid': user.UserUID,
'avatar': user.Avatar or '',
'phone': user.Phone or '',
'zhuangtai': sg.zhuangtai,
'shenhe_zhuangtai': sg.shenhe_zhuangtai,
'shenhe_zongshu': sg.shenhe_zongshu,
'tongguo_zongshu': sg.tongguo_zongshu,
'yue': float(sg.yue),
'zonge': float(sg.zonge),
'CreateTime': sg.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.CreateTime else '',
'UpdateTime': sg.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.UpdateTime else '',
'bankuai': bankuai_info,
'nicheng': dashou_nick,
'is_renzheng': sg.is_renzheng,
})
all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
return Response({
'code': 0,
'msg': 'ok',
'data': {
'total': total,
'page': page,
'page_size': page_size,
'list': data_list,
'all_bankuai': all_bankuai,
'bankuai_tags': bankuai_tags_data, # 新增板块标签统计
}
})
except Exception as e:
logger.exception("KhgglView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
class ShgxgsjView(APIView):
"""
审核官信息修改(余额、状态、板块关联)
POST /houtai/shgxgsj
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'kaohepeizhi' not in permissions:
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
yonghuid = request.data.get('yonghuid')
if not yonghuid:
return Response({'code': 1, 'msg': '缺少用户ID'}, status=400)
try:
shenheguan = UserShenheguan.query.select_related('user').get(user__UserUID=yonghuid)
except UserShenheguan.DoesNotExist:
return Response({'code': 1, 'msg': '审核官不存在'}, status=404)
from jituan.services.shenhe_club import user_belongs_to_request_club
if not user_belongs_to_request_club(shenheguan.user, request):
return Response({'code': 403, 'msg': '该考核官不属于当前俱乐部'}, status=403)
action = request.data.get('action')
if action == 'update_info':
if 'yue' in request.data:
shenheguan.yue = float(request.data.get('yue', 0))
if 'zhuangtai' in request.data:
zt = int(request.data.get('zhuangtai'))
if zt not in [0, 1]:
return Response({'code': 1, 'msg': '无效的账号状态'}, status=400)
shenheguan.zhuangtai = zt
if 'shenhe_zhuangtai' in request.data:
szt = int(request.data.get('shenhe_zhuangtai'))
if szt not in [0, 1]:
return Response({'code': 1, 'msg': '无效的审核官状态'}, status=400)
shenheguan.shenhe_zhuangtai = szt
# 新增认证状态编辑
if 'is_renzheng' in request.data:
shenheguan.is_renzheng = bool(request.data.get('is_renzheng'))
shenheguan.save()
return Response({'code': 0, 'msg': '修改成功'})
elif action == 'add_bankuai':
bankuai_id = request.data.get('bankuai_id')
if not bankuai_id:
return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
if not Bankuai.query.filter(bankuai_id=bankuai_id).exists():
return Response({'code': 1, 'msg': '板块不存在'}, status=400)
if KaoheguanBankuai.query.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists():
return Response({'code': 1, 'msg': '已关联该板块'}, status=400)
KaoheguanBankuai.query.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id)
return Response({'code': 0, 'msg': '板块关联成功'})
elif action == 'remove_bankuai':
bankuai_id = request.data.get('bankuai_id')
if not bankuai_id:
return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
deleted, _ = KaoheguanBankuai.query.filter(
kaoheguan_id=yonghuid, bankuai_id=bankuai_id
).delete()
if not deleted:
return Response({'code': 1, 'msg': '未找到该板块关联'}, status=400)
return Response({'code': 0, 'msg': '板块移除成功'})
else:
return Response({'code': 1, 'msg': '未知操作'}, status=400)
except Exception as e:
logger.exception("ShgxgsjView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
def _batch_user_profile_map(user_uids):
"""批量获取用户头像、昵称、手机号"""
uids = list({u for u in user_uids if u})
if not uids:
return {}
users = User.query.filter(UserUID__in=uids)
dashou_nicks = {
d.user.UserUID: d.nicheng
for d in UserDashou.query.filter(user__UserUID__in=uids).select_related('user')
}
result = {}
for u in users:
result[u.UserUID] = {
'yonghuid': u.UserUID,
'avatar': u.Avatar or '',
'phone': u.Phone or '',
'nicheng': dashou_nicks.get(u.UserUID) or u.UserName or u.Phone or u.UserUID,
}
return result
def _build_shenhe_jilu_filters(request_data):
"""构造 ShenheJilu 查询条件(列表与统计共用)"""
qs = ShenheJilu.query.select_related('bankuai', 'chenghao')
jilu_id = (request_data.get('jilu_id') or '').strip()
if jilu_id:
qs = qs.filter(jilu_id=jilu_id)
zhuangtai = request_data.get('zhuangtai')
if zhuangtai is not None and zhuangtai != '':
qs = qs.filter(zhuangtai=int(zhuangtai))
assessed = request_data.get('assessed')
if assessed == 'done':
qs = qs.filter(zhuangtai__in=[1, 2])
elif assessed == 'undone':
qs = qs.filter(zhuangtai__in=[0, 3])
bankuai_id = request_data.get('bankuai_id')
if bankuai_id not in (None, ''):
qs = qs.filter(bankuai_id=int(bankuai_id))
chenghao_id = request_data.get('chenghao_id')
if chenghao_id not in (None, ''):
qs = qs.filter(chenghao_id=int(chenghao_id))
cishu = request_data.get('cishu')
if cishu not in (None, ''):
qs = qs.filter(cishu=int(cishu))
dashou_kw = (request_data.get('dashou_keyword') or '').strip()
if dashou_kw:
dashou_uids = list(
User.query.filter(
Q(UserUID=dashou_kw) |
Q(UserName__icontains=dashou_kw) |
Q(Phone__icontains=dashou_kw) |
Q(DashouProfile__nicheng__icontains=dashou_kw)
).values_list('UserUID', flat=True)
)
if dashou_uids:
qs = qs.filter(shenqingren_id__in=dashou_uids)
else:
qs = qs.none()
kg_kw = (request_data.get('kaoheguan_keyword') or '').strip()
if kg_kw:
kg_uids = list(
User.query.filter(
Q(UserUID=kg_kw) |
Q(UserName__icontains=kg_kw) |
Q(Phone__icontains=kg_kw) |
Q(DashouProfile__nicheng__icontains=kg_kw)
).values_list('UserUID', flat=True)
)
if kg_uids:
qs = qs.filter(shenheguan_id__in=kg_uids)
else:
qs = qs.none()
start_time = (request_data.get('start_time') or '').strip()
end_time = (request_data.get('end_time') or '').strip()
if start_time:
qs = qs.filter(CreateTime__gte=start_time)
if end_time:
qs = qs.filter(CreateTime__lte=end_time)
has_kaoheguan = request_data.get('has_kaoheguan')
if has_kaoheguan in (True, 'true', '1', 1):
qs = qs.exclude(shenheguan_id__isnull=True).exclude(shenheguan_id='')
elif has_kaoheguan in (False, 'false', '0', 0):
qs = qs.filter(Q(shenheguan_id__isnull=True) | Q(shenheguan_id=''))
return qs
def _serialize_shenhe_jilu_row(jl, profile_map, fee_map=None, reject_count_map=None):
dashou = profile_map.get(jl.shenqingren_id, {
'yonghuid': jl.shenqingren_id,
'avatar': '',
'phone': '',
'nicheng': jl.shenqingren_id,
})
kaoheguan = None
if jl.shenheguan_id:
kaoheguan = profile_map.get(jl.shenheguan_id, {
'yonghuid': jl.shenheguan_id,
'avatar': '',
'phone': '',
'nicheng': jl.shenheguan_id,
})
kaoheguan['game_id'] = jl.shenheguan_youxi_id or ''
current_fee = 0.0
if fee_map is not None:
current_fee = fee_map.get((jl.jilu_id, jl.cishu), 0.0)
else:
fd = KaoheJiluFeiyong.query.filter(jilu_id=jl.jilu_id, cishu=jl.cishu).first()
if fd:
current_fee = float(fd.jine)
reject_count = 0
if reject_count_map is not None:
reject_count = reject_count_map.get(jl.jilu_id, 0)
return {
'jilu_id': jl.jilu_id,
'zhuangtai': jl.zhuangtai,
'zhuangtai_text': SHENHE_ZHUANGTAI_MAP.get(jl.zhuangtai, str(jl.zhuangtai)),
'cishu': jl.cishu,
'jiaofei_jine': float(jl.jiaofei_jine or 0),
'current_fee': current_fee,
'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None,
'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '',
'chenghao_id': jl.chenghao.id if jl.chenghao else None,
'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '',
'chenghao_icon': jl.chenghao.tubiao_url if jl.chenghao else '',
'guize_neirong': jl.guize_neirong or '',
'dashou': {
**dashou,
'game_id': jl.dashou_youxi_id or '',
'beizhu': jl.dashou_beizhu or '',
},
'kaoheguan': kaoheguan,
'reject_count': reject_count,
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '',
'UpdateTime': jl.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.UpdateTime else '',
}
class KhjlglView(APIView):
"""
考核记录列表 + 统计
POST /houtai/khjlgl
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'kaohepeizhi' not in permissions:
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
if page < 1:
page = 1
if page_size < 1:
page_size = 20
if page_size > 100:
page_size = 100
from jituan.services.shenhe_club import filter_shenhe_jilu_by_request
filter_data = request.data
qs = filter_shenhe_jilu_by_request(
_build_shenhe_jilu_filters(filter_data),
request,
)
# FluentQuery.filter() 会原地修改同一对象;统计与列表必须各自独立查询
stats_base = filter_shenhe_jilu_by_request(
_build_shenhe_jilu_filters(filter_data),
request,
)
agg = stats_base.aggregate(total_fee=Sum('jiaofei_jine'))
tag_rows = filter_shenhe_jilu_by_request(
_build_shenhe_jilu_filters(filter_data),
request,
).values(
'chenghao_id', 'chenghao__mingcheng'
).annotate(
total=Count('jilu_id'),
passed=Count('jilu_id', filter=Q(zhuangtai=1)),
failed=Count('jilu_id', filter=Q(zhuangtai=2)),
in_progress=Count('jilu_id', filter=Q(zhuangtai=0)),
pending=Count('jilu_id', filter=Q(zhuangtai=3)),
).order_by('-total')
_stats_agg = stats_base.aggregate(
total=Count('id'),
passed=Count('id', filter=Q(zhuangtai=1)),
failed=Count('id', filter=Q(zhuangtai=2)),
in_progress=Count('id', filter=Q(zhuangtai=0)),
pending=Count('id', filter=Q(zhuangtai=3)),
)
stats = {
'total': _stats_agg['total'],
'passed': _stats_agg['passed'],
'failed': _stats_agg['failed'],
'in_progress': _stats_agg['in_progress'],
'pending': _stats_agg['pending'],
'total_fee': float(agg['total_fee'] or 0),
'tag_stats': [
{
'chenghao_id': r['chenghao_id'],
'chenghao_name': r['chenghao__mingcheng'] or '未知标签',
'total': r['total'],
'passed': r['passed'],
'failed': r['failed'],
'in_progress': r['in_progress'],
'pending': r['pending'],
}
for r in tag_rows if r['chenghao_id']
],
}
total = qs.count()
start = (page - 1) * page_size
rows = qs.order_by('-CreateTime')[start:start + page_size].to_list()
uids = []
jilu_ids = []
for jl in rows:
uids.append(jl.shenqingren_id)
if jl.shenheguan_id:
uids.append(jl.shenheguan_id)
jilu_ids.append(jl.jilu_id)
profile_map = _batch_user_profile_map(uids)
fee_pairs = [(jl.jilu_id, jl.cishu) for jl in rows]
fee_map = {}
if fee_pairs:
jids = [p[0] for p in fee_pairs]
for fd in KaoheJiluFeiyong.query.filter(jilu_id__in=jids):
fee_map[(fd.jilu_id, fd.cishu)] = float(fd.jine)
reject_counts = {
r['jilu_id']: r['cnt']
for r in KaoheJujueJilu.query.filter(jilu_id__in=jilu_ids)
.values('jilu_id')
.annotate(cnt=Count('id'))
.all()
} if jilu_ids else {}
data_list = [
_serialize_shenhe_jilu_row(jl, profile_map, fee_map, reject_counts)
for jl in rows
]
all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
all_tags = list(
Chenghao.query.filter(leixing='dashou')
.values('id', 'mingcheng', 'bankuai_id')
.order_by('mingcheng')
)
return Response({
'code': 0,
'msg': 'ok',
'data': {
'total': total,
'page': page,
'page_size': page_size,
'list': data_list,
'stats': stats,
'all_bankuai': all_bankuai,
'all_tags': all_tags,
},
})
except Exception as e:
logger.exception('KhjlglView 接口错误')
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
class KhjlczView(APIView):
"""
考核记录操作 / 详情
POST /houtai/khjlcz
action:
- get_detail: 获取单条详情(含缴费明细、拒绝历史)
- to_pending: 考核中 → 待审核(取消指定考核官,与小程序 transfer 一致)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'kaohepeizhi' not in permissions:
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
action = request.data.get('action')
jilu_id = (request.data.get('jilu_id') or '').strip()
if not jilu_id:
return Response({'code': 1, 'msg': '缺少记录ID'}, status=400)
if action == 'get_detail':
try:
jl = ShenheJilu.query.select_related('bankuai', 'chenghao').get(jilu_id=jilu_id)
except ShenheJilu.DoesNotExist:
return Response({'code': 1, 'msg': '记录不存在'}, status=404)
uids = [jl.shenqingren_id]
if jl.shenheguan_id:
uids.append(jl.shenheguan_id)
profile_map = _batch_user_profile_map(uids)
fee_list = [
{
'cishu': f.cishu,
'jine': float(f.jine),
}
for f in KaoheJiluFeiyong.query.filter(jilu_id=jilu_id).order_by('cishu')
]
reject_list = []
_reject_records = list(KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'))
_kg_ids = [r.shenheguan_id for r in _reject_records if r.shenheguan_id]
_kg_prof_map = _batch_user_profile_map(_kg_ids) if _kg_ids else {}
for r in _reject_records:
kg_prof = _kg_prof_map.get(r.shenheguan_id, {})
reject_list.append({
'cishu': r.cishu,
'shenheguan_id': r.shenheguan_id,
'shenheguan_nicheng': kg_prof.get('nicheng', r.shenheguan_id),
'yuanyin': r.yuanyin,
'CreateTime': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
})
record = _serialize_shenhe_jilu_row(jl, profile_map)
record['fee_list'] = fee_list
record['reject_list'] = reject_list
return Response({'code': 0, 'msg': 'ok', 'data': record})
if action == 'to_pending':
with transaction.atomic():
jl = ShenheJilu.objects.select_for_update().filter(jilu_id=jilu_id).first()
if not jl:
return Response({'code': 1, 'msg': '记录不存在'}, status=404)
if jl.zhuangtai != 0:
return Response({'code': 1, 'msg': '仅「考核中」的记录可退回待审核'}, status=400)
old_kg_id = jl.shenheguan_id
jl.shenheguan_id = None
jl.shenheguan_youxi_id = None
jl.zhuangtai = 3
jl.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id', 'zhuangtai', 'UpdateTime'])
if old_kg_id:
UserShenheguan.query.filter(user__UserUID=old_kg_id).update(shenhe_zhuangtai=0)
return Response({'code': 0, 'msg': '已退回待审核,考核官指定已取消'})
return Response({'code': 1, 'msg': '无效的操作类型'}, status=400)
except Exception as e:
logger.exception('KhjlczView 接口错误')
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)

983
backend/views/finance.py Normal file
View File

@@ -0,0 +1,983 @@
import hmac
import threading
import traceback
import uuid
import hashlib
import xmltodict
import time
import logging
import requests
import os
import secrets
import random
import string
from calendar import monthrange
from collections import defaultdict
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
# 工具类导入
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.catalog_scope import block_non_group_catalog_scope
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
from ..utils import (
verify_kefu_permission,
PERMISSION_TO_SHENFEN,
SHENFEN_PROFILE_MAP,
has_fadan_view_permission,
has_merchant_order_permission,
check_fadan_permission,
pick_order_id,
pick_penalty_create_params,
write_xiugai_log,
XIUGAI_LEIXING_DASHOU,
XIUGAI_LEIXING_GUANSHI,
XIUGAI_LEIXING_SHANGJIA,
XIUGAI_LEIXING_ZUZHANG,
XIUGAI_LEIXING_LABEL,
)
# models 集中导入
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
## backend
from backend.models import WithdrawalDailyStats
## users
from users.models import (
UserGuanshi, UserBoss, UserZuzhang,
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
)
from users.business_models import User
## orders
from orders.models import (
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
OrderPlayerHistory, Order, RefundRecord,
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
)
## shop
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
## products
from products.models import (
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
)
## config
from config.models import (
WithdrawConfig, ShangjiaLianjie, AccountPermission,
TixianQuotaDefault,
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
)
## rank
from rank.models import (
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
)
# 序列化器
from ..serializers import PopupPageSerializer
# 全局常量、日志对象
logger = logging.getLogger('houtai')
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
class CaiwuView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'caiwu' not in permissions:
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
from jituan.services.caiwu_stats import build_caiwu_payload
try:
data = build_caiwu_payload(request)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('CaiwuView 异常')
return Response({'code': 99, 'msg': '统计失败'}, status=500)
class CwhybkhqView(APIView):
"""
获取所有板块及对应会员列表
POST /houtai/cwhybkhq
Body: { "phone": "管理员手机号" }
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'huiyuancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
all_bankuai = list(Bankuai.query.all())
bankuai_ids = [bk.bankuai_id for bk in all_bankuai]
all_members = Huiyuan.query.filter(bankuai_id__in=bankuai_ids).values('huiyuan_id', 'jieshao', 'bankuai_id')
members_by_bankuai = defaultdict(list)
for m in all_members:
members_by_bankuai[m['bankuai_id']].append(dict(m))
bankuai_list = []
for bk in all_bankuai:
bankuai_list.append({
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'members': members_by_bankuai.get(bk.bankuai_id, [])
})
return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list})
except Exception as e:
logger.exception("CwhybkhqView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
class HybkjtsjView(APIView):
"""
会员详细统计数据
POST /houtai/hybkjtsj
Body: {
"phone": "管理员手机号",
"huiyuan_id": "会员ID",
"granularity": "day" | "month" | "year",
"year": 2026,
"month": 5,
"include_guanshi": true,
"include_zuzhang": true,
"summary_date": "2026-05-16"
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'huiyuancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
huiyuan_id = request.data.get('huiyuan_id')
granularity = request.data.get('granularity', 'day')
year = request.data.get('year')
month = request.data.get('month')
include_guanshi = request.data.get('include_guanshi', False)
include_zuzhang = request.data.get('include_zuzhang', False)
summary_date = request.data.get('summary_date')
if not huiyuan_id:
return Response({'code': 1, 'msg': '缺少会员ID'}, status=400)
# 🔧 直接使用 date.today() 避免模块名冲突
now = date.today()
# ---------- 构造时间范围 ----------
if granularity == 'day':
if not year or not month:
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
start_date = date(year, month, 1)
if month == 12:
end_date = date(year + 1, 1, 1)
else:
end_date = date(year, month + 1, 1)
trunc_func = TruncDate('CreateTime')
if not summary_date:
summary_date = now.strftime('%Y-%m-%d')
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
elif granularity == 'month':
if not year:
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
start_date = date(year, 1, 1)
end_date = date(year + 1, 1, 1)
trunc_func = TruncMonth('CreateTime')
if not summary_date:
summary_date = now.strftime('%Y-%m')
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
elif granularity == 'year':
year = year or now.year
start_date = date(year, 1, 1)
end_date = date(year + 1, 1, 1)
trunc_func = TruncYear('CreateTime')
if not summary_date:
summary_date = str(year)
summary_q = date(year, 1, 1)
else:
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
# 生成 naive datetime避免 USE_TZ=False 报错)
start_dt = datetime.combine(start_date, datetime.min.time())
end_dt = datetime.combine(end_date, datetime.min.time())
# ---------- 充值过滤 ----------
chongzhi_filter = {
'huiyuan_id': huiyuan_id, # 会员ID筛选
'leixing': 1,
'zhuangtai': 3,
'CreateTime__gte': start_dt,
'CreateTime__lt': end_dt
}
# ---------- 分红过滤 ----------
fenhong_filter = {
'huiyuan_id': huiyuan_id, # 分红表会员ID筛选
'CreateTime__gte': start_dt,
'CreateTime__lt': end_dt
}
# 充值分组查询
chongzhi_qs = filter_queryset_by_club(
Czjilu.query.filter(**chongzhi_filter), request,
) \
.annotate(period=trunc_func) \
.values('period') \
.annotate(
chongzhi_count=Count('id'),
chongzhi_amount=Sum('jine')
).order_by('period')
# 分红分组查询
fenhong_qs = None
if include_guanshi or include_zuzhang:
fenhong_agg = {}
if include_guanshi:
fenhong_agg['guanshi_amount'] = Sum('fenhong')
if include_zuzhang:
fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong')
fenhong_qs = filter_gsfenhong_qs(
Gsfenhong.query.filter(**fenhong_filter), request,
) \
.annotate(period=trunc_func) \
.values('period') \
.annotate(**fenhong_agg) \
.order_by('period')
# 合并数据
data_map = {}
for item in chongzhi_qs:
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
item['period'].strftime('%Y-%m') if granularity == 'month' else \
item['period'].strftime('%Y')
data_map[period_str] = {
'chongzhi_count': item['chongzhi_count'],
'chongzhi_amount': float(item['chongzhi_amount'] or 0)
}
if fenhong_qs:
for item in fenhong_qs:
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
item['period'].strftime('%Y-%m') if granularity == 'month' else \
item['period'].strftime('%Y')
if period_str not in data_map:
data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0}
if include_guanshi:
data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0)
if include_zuzhang:
data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0)
time_series = []
for period_str, vals in data_map.items():
guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0
zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0
vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2)
vals['date'] = period_str
time_series.append(vals)
time_series.sort(key=lambda x: x['date'])
# ---------- 汇总数据 ----------
summary_chongzhi_filter = chongzhi_filter.copy()
if granularity == 'day':
summary_chongzhi_filter['CreateTime__date'] = summary_q
elif granularity == 'month':
summary_chongzhi_filter['CreateTime__year'] = summary_q.year
summary_chongzhi_filter['CreateTime__month'] = summary_q.month
else:
summary_chongzhi_filter['CreateTime__year'] = summary_q.year
summary_chongzhi = Czjilu.query.filter(**summary_chongzhi_filter).aggregate(
total_count=Count('id'),
total_amount=Sum('jine')
)
total_chongzhi_count = summary_chongzhi['total_count'] or 0
total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0)
total_guanshi = 0.0
total_zuzhang = 0.0
if include_guanshi or include_zuzhang:
summary_fenhong_filter = fenhong_filter.copy()
if granularity == 'day':
summary_fenhong_filter['CreateTime__date'] = summary_q
elif granularity == 'month':
summary_fenhong_filter['CreateTime__year'] = summary_q.year
summary_fenhong_filter['CreateTime__month'] = summary_q.month
else:
summary_fenhong_filter['CreateTime__year'] = summary_q.year
fenhong_agg = {}
if include_guanshi:
fenhong_agg['guanshi_sum'] = Sum('fenhong')
if include_zuzhang:
fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong')
summary_fenhong = Gsfenhong.query.filter(**summary_fenhong_filter).aggregate(**fenhong_agg)
total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0)
total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0)
total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2)
return Response({
'code': 0,
'msg': 'ok',
'data': {
'time_series': time_series,
'summary': {
'total_chongzhi_count': total_chongzhi_count,
'total_chongzhi_amount': total_chongzhi_amount,
'total_guanshi_fenhong': total_guanshi,
'total_zuzhang_fenhong': total_zuzhang,
'total_shouyi': total_shouyi
}
}
})
except Exception as e:
logger.exception("HybkjtsjView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
class SzxxView(APIView):
"""
每日收支详细统计数据
POST /houtai/szxx
只读 daily_income_stat / daily_payout_stat按 date 字段汇总。
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
from jituan.services.szxx_stats import build_szxx_payload
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'caiwu' not in permissions:
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
granularity = request.data.get('granularity', 'day')
year = request.data.get('year')
month = request.data.get('month')
summary_date = request.data.get('summary_date')
data, err = build_szxx_payload(request, granularity, year, month, summary_date)
if err:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception as e:
logger.exception("SzxxView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
class CwddhqlxView(APIView):
"""
获取订单类型(商品类型)用于筛选
POST /houtai/cwddhqlx
Body: { "phone": "管理员手机号" }
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'dingdancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
# 获取非审核状态shenhezhuangtai=1的商品类型只返回ID和名称
types = ShangpinLeixing.query.filter(shenhezhuangtai=1).values('id', 'jieshao')
return Response({
'code': 0,
'msg': 'ok',
'data': list(types)
})
except Exception as e:
logger.exception("CwddhqlxView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
class CwhqjtddsjView(APIView):
"""
订单详细统计数据(修正版)
POST /houtai/cwhqjtddsj
Body: {
"phone": "管理员手机号",
"granularity": "day" | "month" | "year",
"year": 2026,
"month": 5,
"type_id": 1, # 商品类型ID0表示全部
"order_source": 1, # 1=平台单2=商家单0=全部
"summary_date": "2026-05-16"
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'dingdancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
granularity = request.data.get('granularity', 'day')
year = request.data.get('year')
month = request.data.get('month')
type_id = request.data.get('type_id', 0)
order_source = request.data.get('order_source', 0)
summary_date = request.data.get('summary_date')
now = date.today()
# ---------- 时间范围 ----------
if granularity == 'day':
if not year or not month:
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
start_date = date(year, month, 1)
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
trunc_func = TruncDate('CreateTime')
if not summary_date:
summary_date = now.strftime('%Y-%m-%d')
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
elif granularity == 'month':
if not year:
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
start_date = date(year, 1, 1)
end_date = date(year + 1, 1, 1)
trunc_func = TruncMonth('CreateTime')
if not summary_date:
summary_date = now.strftime('%Y-%m')
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
elif granularity == 'year':
year = year or now.year
start_date = date(year, 1, 1)
end_date = date(year + 1, 1, 1)
trunc_func = TruncYear('CreateTime')
if not summary_date:
summary_date = str(year)
summary_q = date(year, 1, 1)
else:
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
start_dt = datetime.combine(start_date, datetime.min.time())
end_dt = datetime.combine(end_date, datetime.min.time())
# ---------- 基础过滤 ----------
filters = {
'CreateTime__gte': start_dt,
'CreateTime__lt': end_dt,
'Status__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态
}
if int(type_id) != 0:
filters['ProductTypeID'] = type_id
if int(order_source) in [1, 2]:
filters['Platform'] = order_source
# ---------- 时间序列分组查询 ----------
order_base = filter_queryset_by_club(
Order.query.filter(**filters), request, club_field='ClubID',
)
qs = order_base \
.annotate(period=trunc_func) \
.values('period') \
.annotate(
# 订单总量(所有符合条件的状态)
order_count=Count('id'),
# 订单总额
order_amount=Sum('Amount'),
# 成交单量 (Status=3)
success_count=Count('id', filter=Q(Status=3)),
# 成交订单总额
success_amount=Sum('Amount', filter=Q(Status=3)),
# 退款单量 (Status=5)
refund_count=Count('id', filter=Q(Status=5)),
# 打手分成总额(仅成交订单)
PlayerCommission_sum=Sum('PlayerCommission', filter=Q(Status=3)),
# 平台成交订单的店铺分红
dianpu_fenhong_sum=Sum('pingtai_kuozhan__ShopIncome',
filter=Q(Status=3, Platform=1)),
# 成交订单中:平台单金额、商家单金额、各自打手分成
platform_success_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
merchant_success_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
platform_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
merchant_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
).order_by('period')
time_series = []
for item in qs:
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
item['period'].strftime('%Y-%m') if granularity == 'month' else \
item['period'].strftime('%Y')
# 平台收益计算
platform_profit = (item['platform_success_amount'] or 0) \
- (item['platform_dashou'] or 0) \
- (item['dianpu_fenhong_sum'] or 0)
merchant_profit = (item['merchant_success_amount'] or 0) \
- (item['merchant_dashou'] or 0)
total_profit = platform_profit + merchant_profit
time_series.append({
'date': period_str,
'order_count': item['order_count'],
'order_amount': float(item['order_amount'] or 0),
'success_count': item['success_count'],
'success_amount': float(item['success_amount'] or 0),
'refund_count': item['refund_count'],
'dashou_fencheng': float(item['PlayerCommission_sum'] or 0),
'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0),
'profit': round(total_profit, 2)
})
# ---------- 汇总数据 ----------
summary_qs = filter_queryset_by_club(
Order.query.filter(**filters), request, club_field='ClubID',
)
agg = summary_qs.aggregate(
total_order_count=Count('id'),
total_order_amount=Sum('Amount'),
total_success_count=Count('id', filter=Q(Status=3)),
total_success_amount=Sum('Amount', filter=Q(Status=3)),
total_refund_count=Count('id', filter=Q(Status=5)),
total_dashou=Sum('PlayerCommission', filter=Q(Status=3)),
total_dianpu_fenhong=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)),
platform_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
merchant_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
platform_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
merchant_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
)
total_order_count = agg['total_order_count'] or 0
total_order_amount = agg['total_order_amount'] or 0
total_success_count = agg['total_success_count'] or 0
total_success_amount = agg['total_success_amount'] or 0
total_refund_count = agg['total_refund_count'] or 0
total_dashou = agg['total_dashou'] or 0
total_dianpu_fenhong = agg['total_dianpu_fenhong'] or 0
platform_amount = agg['platform_amount'] or 0
merchant_amount = agg['merchant_amount'] or 0
platform_dashou_sum = agg['platform_dashou_sum'] or 0
merchant_dashou_sum = agg['merchant_dashou_sum'] or 0
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
profit_merchant = merchant_amount - merchant_dashou_sum
total_profit = profit_platform + profit_merchant
summary = {
'total_order_count': total_order_count,
'total_order_amount': round(float(total_order_amount), 2),
'total_success_count': total_success_count,
'total_success_amount': round(float(total_success_amount), 2),
'total_refund_count': total_refund_count,
'total_PlayerCommission': round(float(total_dashou), 2),
'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2),
'total_profit': round(total_profit, 2)
}
return Response({
'code': 0,
'msg': 'ok',
'data': {
'time_series': time_series,
'summary': summary
}
})
except Exception as e:
logger.exception("CwhqjtddsjView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
class CwqtczhqView(APIView):
"""
其他充值/罚款数据统计(非会员充值)
POST /houtai/cwqtczhq
Body: {
"phone": "管理员手机号",
"granularity": "day" | "month" | "year",
"year": 2026,
"month": 5, // day时必填
"types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
"summary_date": "2026-05-17" // 汇总日期格式随颗粒度day:YYYY-MM-DD, month:YYYY-MM, year:YYYY
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'qtczcaiwu' not in permissions:
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
from jituan.services.finance_detail_stats import build_chongzhi_finance_payload
granularity = request.data.get('granularity', 'day')
year = request.data.get('year')
month = request.data.get('month')
types = request.data.get('types', [2, 3, 4, 5, 6])
summary_date = request.data.get('summary_date')
if isinstance(types, list) and len(types) == 0:
types = [2, 3, 4, 5, 6]
data, err = build_chongzhi_finance_payload(
request, granularity, year, month, types, summary_date,
)
if err:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception as e:
logger.exception("CwqtczhqView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
'''class CwqtczhqView_LEGACY(APIView):
"""
其他充值/罚款数据统计(非会员充值)
POST /houtai/cwqtczhq
Body: {
"phone": "管理员手机号",
"granularity": "day" | "month" | "year",
"year": 2026,
"month": 5, // day时必填
"types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
"summary_date": "2026-05-17" // 汇总日期格式随颗粒度day:YYYY-MM-DD, month:YYYY-MM, year:YYYY
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'qtczcaiwu' not in permissions:
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
granularity = request.data.get('granularity', 'day')
year = request.data.get('year')
month = request.data.get('month')
types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部
summary_date = request.data.get('summary_date')
if isinstance(types, list) and len(types) == 0:
types = [2, 3, 4, 5, 6]
now = date.today()
# ---------- 时间范围(用于曲线图) ----------
if granularity == 'day':
if not year or not month:
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
start_date = date(year, month, 1)
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
trunc_func = TruncDate('CreateTime')
# 默认汇总日期
if not summary_date:
summary_date = now.strftime('%Y-%m-%d')
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
elif granularity == 'month':
if not year:
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
start_date = date(year, 1, 1)
end_date = date(year + 1, 1, 1)
trunc_func = TruncMonth('CreateTime')
if not summary_date:
summary_date = now.strftime('%Y-%m')
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
elif granularity == 'year':
year = year or now.year
start_date = date(year, 1, 1)
end_date = date(year + 1, 1, 1)
trunc_func = TruncYear('CreateTime')
if not summary_date:
summary_date = str(year)
summary_q = date(year, 1, 1)
else:
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
start_dt = datetime.combine(start_date, datetime.min.time())
end_dt = datetime.combine(end_date, datetime.min.time())
# ---------- 构建时间序列 ----------
time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} }
def ensure_date(ds):
if ds not in time_series_map:
time_series_map[ds] = {}
return time_series_map[ds]
# 1. 处理充值记录Czjilu
cz_types = [t for t in types if t in [2, 3, 4, 5]]
if cz_types:
cz_qs = Czjilu.query.filter(
CreateTime__gte=start_dt,
CreateTime__lt=end_dt,
leixing__in=cz_types,
zhuangtai=3 # 只统计支付成功的
).annotate(
period=trunc_func
).values('period', 'leixing').annotate(
total_count=Count('id'),
total_amount=Sum('jine')
).order_by('period', 'leixing')
for item in cz_qs:
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
item['period'].strftime('%Y-%m') if granularity == 'month' else \
item['period'].strftime('%Y')
lx = item['leixing']
amount = float(item['total_amount'] or 0)
count = item['total_count']
# 收益:积分(3)收益为金额本身其他为0
profit = amount if lx == 3 else 0.0
day_data = ensure_date(period_str)
day_data[str(lx)] = {
'count': count,
'amount': amount,
'profit': profit,
'fenhong_count': 0, # 充值类型无分红
'fenhong_amount': 0.0
}
# 2. 处理罚款Penalty—— 包含分红
if 6 in types:
# 罚款记录
penalty_qs = Penalty.query.filter(
CreateTime__gte=start_dt,
CreateTime__lt=end_dt,
Status=2 # 已缴纳
).annotate(
period=trunc_func
).values('period').annotate(
penalty_count=Count('id'),
penalty_amount=Sum('FineAmount')
).order_by('period')
# 分红记录
fenhong_qs = PenaltyBonus.query.filter(
CreateTime__gte=start_dt,
CreateTime__lt=end_dt
).annotate(
period=trunc_func
).values('period').annotate(
fh_count=Count('id'),
fh_amount=Sum('WithdrawableAmount')
).order_by('period')
# 合并
for item in penalty_qs:
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
item['period'].strftime('%Y-%m') if granularity == 'month' else \
item['period'].strftime('%Y')
day_data = ensure_date(period_str)
day_data['6'] = {
'count': item['penalty_count'],
'amount': float(item['penalty_amount'] or 0),
'profit': 0.0, # 先设为0后续计算
'fenhong_count': 0,
'fenhong_amount': 0.0
}
for item in fenhong_qs:
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
item['period'].strftime('%Y-%m') if granularity == 'month' else \
item['period'].strftime('%Y')
day_data = ensure_date(period_str)
if '6' not in day_data:
day_data['6'] = {'count': 0, 'amount': 0.0, 'profit': 0.0, 'fenhong_count': 0, 'fenhong_amount': 0.0}
day_data['6']['fenhong_count'] += item['fh_count']
day_data['6']['fenhong_amount'] += float(item['fh_amount'] or 0)
# 计算罚款收益 = 罚款金额 - 分红金额
for period_str, day_data in time_series_map.items():
if '6' in day_data:
penalty = day_data['6']
penalty['profit'] = round(penalty['amount'] - penalty['fenhong_amount'], 2)
# 构造有序时间序列列表
time_series = []
for period_str in sorted(time_series_map.keys()):
entry = {'date': period_str, 'types': {}}
for type_code in sorted(time_series_map[period_str].keys()):
entry['types'][type_code] = time_series_map[period_str][type_code]
time_series.append(entry)
# ---------- 汇总指定日期 ----------
summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
# 汇总充值类型
for lx in cz_types:
filter_kwargs = {
'leixing': lx,
'zhuangtai': 3,
}
if granularity == 'day':
filter_kwargs['CreateTime__date'] = summary_q
elif granularity == 'month':
filter_kwargs['CreateTime__year'] = summary_q.year
filter_kwargs['CreateTime__month'] = summary_q.month
else:
filter_kwargs['CreateTime__year'] = summary_q.year
agg = filter_queryset_by_club(
Czjilu.query.filter(**filter_kwargs), request,
).aggregate(count=Count('id'), amount=Sum('jine'))
count = agg['count'] or 0
amount = float(agg['amount'] or 0)
profit = amount if lx == 3 else 0.0
summary_result['by_type'][str(lx)] = {
'count': count,
'amount': round(amount, 2),
'profit': round(profit, 2),
'fenhong_count': 0,
'fenhong_amount': 0.0
}
summary_result['total_count'] += count
summary_result['total_amount'] += amount
summary_result['total_profit'] += profit
# 汇总罚款
if 6 in types:
penalty_filter = {
'Status': 2,
}
fenhong_filter = {}
if granularity == 'day':
penalty_filter['CreateTime__date'] = summary_q
fenhong_filter['CreateTime__date'] = summary_q
elif granularity == 'month':
penalty_filter['CreateTime__year'] = summary_q.year
penalty_filter['CreateTime__month'] = summary_q.month
fenhong_filter['CreateTime__year'] = summary_q.year
fenhong_filter['CreateTime__month'] = summary_q.month
else:
penalty_filter['CreateTime__year'] = summary_q.year
fenhong_filter['CreateTime__year'] = summary_q.year
penalty_agg = Penalty.query.filter(**penalty_filter).aggregate(
count=Count('id'), amount=Sum('FineAmount'))
penalty_count = penalty_agg['count'] or 0
penalty_amount = float(penalty_agg['amount'] or 0)
fenhong_agg = PenaltyBonus.query.filter(**fenhong_filter).aggregate(
fh_count=Count('id'), fh_amount=Sum('WithdrawableAmount'))
fh_count = fenhong_agg['fh_count'] or 0
fh_amount = float(fenhong_agg['fh_amount'] or 0)
profit = round(penalty_amount - fh_amount, 2)
summary_result['by_type']['6'] = {
'count': penalty_count,
'amount': round(penalty_amount, 2),
'profit': profit,
'fenhong_count': fh_count,
'fenhong_amount': round(fh_amount, 2)
}
summary_result['total_count'] += penalty_count
summary_result['total_amount'] += penalty_amount
summary_result['total_profit'] += profit
summary_result['total_amount'] = round(summary_result['total_amount'], 2)
summary_result['total_profit'] = round(summary_result['total_profit'], 2)
return Response({
'code': 0,
'msg': 'ok',
'data': {
'time_series': time_series,
'summary': summary_result
}
})
except Exception as e:
logger.exception("CwqtczhqView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)'''
# ==================== 后端接口 ====================
# 文件views.py (添加以下两个视图类)

860
backend/views/guanli.py Normal file
View File

@@ -0,0 +1,860 @@
import hmac
import threading
import traceback
import uuid
import hashlib
import xmltodict
import time
import logging
import requests
import os
import secrets
import random
import string
from calendar import monthrange
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
# 工具类导入
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.catalog_scope import block_non_group_catalog_scope
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
from ..utils import (
verify_kefu_permission,
PERMISSION_TO_SHENFEN,
SHENFEN_PROFILE_MAP,
has_fadan_view_permission,
has_merchant_order_permission,
check_fadan_permission,
pick_order_id,
pick_penalty_create_params,
write_xiugai_log,
XIUGAI_LEIXING_DASHOU,
XIUGAI_LEIXING_GUANSHI,
XIUGAI_LEIXING_SHANGJIA,
XIUGAI_LEIXING_ZUZHANG,
XIUGAI_LEIXING_LABEL,
)
# models 集中导入
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
## backend
from backend.models import WithdrawalDailyStats
## users
from users.models import (
UserGuanshi, UserBoss, UserZuzhang,
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
)
from users.business_models import User
## orders
from orders.models import (
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
OrderPlayerHistory, Order, RefundRecord,
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
)
## shop
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
## products
from products.models import (
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
)
## config
from config.models import (
WithdrawConfig, ShangjiaLianjie, AccountPermission,
TixianQuotaDefault,
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
)
## rank
from rank.models import (
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
)
# 序列化器
from ..serializers import PopupPageSerializer
# 全局常量、日志对象
logger = logging.getLogger('houtai')
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
class GetGuanliListView(APIView):
"""
获取管事列表
权限4400a,4400b,4400c,4400d,4400e,4400f 任一
POST /houtai/hthqgslb
参数:
username, keyword(可选), balance_op/balance_value, commission_op/commission_value,
invite_count_op/invite_count_value, status, page, page_size
返回:
code:0, data: { list: [...], total }
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
# 权限校验(需要任一权限)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
if not any(p in permissions for p in required_perms):
return Response({'code': 403, 'msg': '您没有权限查看管事列表'})
# 分页参数
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
if page < 1 or page_size < 1:
raise ValueError
except:
return Response({'code': 400, 'msg': '分页参数错误'})
# 筛选参数
keyword = request.data.get('keyword', '').strip()
balance_op = request.data.get('balance_op', '').strip().lower()
balance_value = request.data.get('balance_value')
commission_op = request.data.get('commission_op', '').strip().lower()
commission_value = request.data.get('commission_value')
invite_count_op = request.data.get('invite_count_op', '').strip().lower()
invite_count_value = request.data.get('invite_count_value')
status = request.data.get('status')
# 构建查询:从 User 开始,关联 UserGuanshi 和 UserBoss左连接获取昵称
qs = filter_user_qs_by_club(
User.query.filter(
GuanshiProfile__isnull=False # 确保是管事
).select_related('GuanshiProfile').select_related('BossProfile'),
request,
)
# 关键词搜索用户ID 或 昵称来自 boss_profile.nickname
if keyword:
qs = qs.filter(
Q(UserUID__icontains=keyword) |
Q(BossProfile__nickname__icontains=keyword)
)
# 余额筛选(使用 guanshi_profile.yue
if balance_op and balance_value is not None and balance_value != '':
try:
balance_val = Decimal(str(balance_value))
if balance_op == 'gte':
qs = qs.filter(GuanshiProfile__yue__gte=balance_val)
elif balance_op == 'lte':
qs = qs.filter(GuanshiProfile__yue__lte=balance_val)
else:
return Response({'code': 400, 'msg': '余额比较符无效'})
except:
return Response({'code': 400, 'msg': '余额数值格式错误'})
# 分佣总额筛选guanshi_profile.chongzhifenrun
if commission_op and commission_value is not None and commission_value != '':
try:
commission_val = Decimal(str(commission_value))
if commission_op == 'gte':
qs = qs.filter(GuanshiProfile__chongzhifenrun__gte=commission_val)
elif commission_op == 'lte':
qs = qs.filter(GuanshiProfile__chongzhifenrun__lte=commission_val)
else:
return Response({'code': 400, 'msg': '分佣比较符无效'})
except:
return Response({'code': 400, 'msg': '分佣数值格式错误'})
# 邀请打手总数筛选guanshi_profile.yaogingshuliang
if invite_count_op and invite_count_value is not None and invite_count_value != '':
try:
invite_val = int(invite_count_value)
if invite_count_op == 'gte':
qs = qs.filter(GuanshiProfile__yaogingshuliang__gte=invite_val)
elif invite_count_op == 'lte':
qs = qs.filter(GuanshiProfile__yaogingshuliang__lte=invite_val)
else:
return Response({'code': 400, 'msg': '邀请数量比较符无效'})
except:
return Response({'code': 400, 'msg': '邀请数量格式错误'})
# 状态筛选
if status is not None and status != '':
try:
status_val = int(status)
if status_val not in (0, 1):
raise ValueError
qs = qs.filter(GuanshiProfile__zhuangtai=status_val)
except:
return Response({'code': 400, 'msg': '状态值无效'})
# 计算总数
total = qs.count()
# 分页
offset = (page - 1) * page_size
users = qs.order_by('-UserCreateTime')[offset:offset + page_size]
# 构建返回数据
result = []
for user in users:
guanshi = user.GuanshiProfile
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
result.append({
'yonghuid': user.UserUID,
'avatar': user.Avatar or '',
'nickname': boss.nickname if boss else '',
'zhuangtai': guanshi.zhuangtai,
'yaogingshuliang': guanshi.yaogingshuliang,
'yue': str(guanshi.yue),
'chongzhifenrun': str(guanshi.chongzhifenrun),
})
return Response({
'code': 0,
**list_response_meta(request),
'data': {'list': result, 'total': total},
})
class GetGuanliDetailView(APIView):
"""
获取管事详情
权限4400a,4400b,4400c,4400d,4400e,4400f 任一
POST /houtai/hthqgsxq
参数username, yonghuid
返回:
code:0, data: {
base: {...},
balance: {...},
permanent_enabled, permanent_amount,
custom_first_dividend: {},
extra_dividend_map: {},
all_members: []
}
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
yonghuid = request.data.get('yonghuid', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not yonghuid:
return Response({'code': 400, 'msg': '缺少用户ID'})
# 权限校验(需要任一权限)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
if not any(p in permissions for p in required_perms):
return Response({'code': 403, 'msg': '您没有权限查看管事详情'})
# 查询用户主表和管事扩展表
try:
user = User.query.select_related('GuanshiProfile', 'BossProfile').get(UserUID=yonghuid)
guanshi = user.GuanshiProfile
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
except User.DoesNotExist:
return Response({'code': 404, 'msg': '管事不存在'})
except UserGuanshi.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是管事'})
denied = forbid_if_user_out_of_scope(request, user)
if denied:
return denied
# 基础信息
is_zuzhang = UserZuzhang.query.filter(user=user).exists()
base_data = {
'yonghuid': user.UserUID,
'nickname': boss.nickname if boss else '',
'avatar': user.Avatar or '',
'dianhua': guanshi.dianhua or '',
'wechat': guanshi.wechat or '',
'zhuangtai': guanshi.zhuangtai,
'yaoqingma': guanshi.yaoqingma,
'yaoqingren': guanshi.yaoqingren,
'invite_qrcode_url': guanshi.invite_qrcode_url or '',
'yaogingshuliang': guanshi.yaogingshuliang,
'jinrichongzhi': guanshi.jinrichongzhi,
'jinyuechongzhi': guanshi.jinyuechongzhi,
'chongzhifenrun': str(guanshi.chongzhifenrun),
'create_time': guanshi.CreateTime.isoformat() if guanshi.CreateTime else None,
'CreateTime': guanshi.CreateTime.isoformat() if guanshi.CreateTime else None,
}
# 余额提现相关
balance_data = {
'yue': str(guanshi.yue),
'jinritixian_jine': str(guanshi.jinritixian_jine),
'last_tixian_date': guanshi.last_tixian_date.isoformat() if guanshi.last_tixian_date else None,
'kaioi_ewai_xiane': guanshi.kaioi_ewai_xiane,
'ewai_xiane': str(guanshi.ewai_xiane),
}
# 永久分红(二次分红)
permanent_enabled = guanshi.fenghong_erci_enabled
permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0'
# 获取所有会员(用于前端选择)
all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc'))
for m in all_members:
m['jiage'] = str(m['jiage'])
m['guanshifc'] = str(m['guanshifc'])
# 获取多次分红配置(按次数和会员组织,按俱乐部隔离)
from jituan.services.club_penalty import resolve_gsfenhong_club_id
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
duoci_records = DuociFenhong.query.filter(
yonghuid=yonghuid, club_id=guanshi_club,
).order_by('cishu')
custom_first = {} # 首次分红定制cishu=1
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } }
# 批量预取会员信息
_huiyuan_ids = [r.huiyuan for r in duoci_records]
_huiyuan_map = {
h.huiyuan_id: h
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao', 'jiage')
}
for record in duoci_records:
# 获取会员信息
member = _huiyuan_map.get(record.huiyuan)
if not member:
continue
member_info = {
'jieshao': member.jieshao,
'jiage': str(member.jiage),
}
if record.cishu == 1:
custom_first[record.huiyuan] = float(record.guanshi_fenhong)
else:
times = record.cishu
if times not in extra_map:
extra_map[times] = {}
extra_map[times][record.huiyuan] = {
'guanshi_fenhong': float(record.guanshi_fenhong),
**member_info
}
# 注意:首次分红定制只存储有定制记录的会员,没有定制的会员使用默认值(前端通过会员表展示)
# 额外次数分红按次数返回
return Response({
'code': 0,
'data': {
'base': base_data,
'balance': balance_data,
'permanent_enabled': permanent_enabled,
'permanent_amount': permanent_amount,
'custom_first_dividend': custom_first,
'extra_dividend_map': extra_map,
'all_members': all_members,
'is_zuzhang': is_zuzhang,
}
})
def _generate_unique_zuzhang_yaoqingma(yonghuid):
"""生成唯一组长邀请码(与小程序组长注册逻辑一致)"""
timestamp = str(int(time.time()))[-6:]
uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0')
rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2))
base = f"Z{timestamp}{uid_part}{rand_str}"
for _ in range(5):
if not UserZuzhang.query.filter(yaoqingma=base).exists():
return base
base = base + random.choice(string.digits)
return base + str(int(time.time() * 1000))[-4:]
def _create_zuzhang_profile(user):
"""为已有用户创建组长扩展记录(默认字段与小程序注册一致)"""
invite_guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
yaoqingma = _generate_unique_zuzhang_yaoqingma(user.UserUID)
return UserZuzhang.query.create(
user=user,
yaoqingma=yaoqingma,
fenyong_zonge=Decimal('0.00'),
ketixian_jine=Decimal('0.00'),
yaoqing_zongshu=invite_guanshi_count,
jinri_fenyong=Decimal('0.00'),
jinyue_fenyong=Decimal('0.00'),
zhuangtai=1,
jinri_tixian=Decimal('0.00'),
ewai_tixian_xiane=Decimal('0.00'),
kaioi_ewai_tixian=False,
kaioi_ewai_fenhong=False,
ewai_fenhong_jine=Decimal('0.00'),
)
class PromoteGuanshiToZuzhangView(APIView):
"""
后台将管事用户升级为组长(写入 user_zuzhang 扩展表)
POST /houtai/htgssjzz
权限sjzz666
参数username, yonghuid
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
yonghuid = request.data.get('yonghuid', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not yonghuid:
return Response({'code': 400, 'msg': '缺少用户ID'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if 'sjzz666' not in permissions:
return Response({'code': 403, 'msg': '无权限升级组长(需要 sjzz666'})
try:
user = User.query.select_related('GuanshiProfile').get(UserUID=yonghuid)
guanshi = user.GuanshiProfile
except User.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
except UserGuanshi.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是管事,无法升级组长'})
if UserZuzhang.query.filter(user=user).exists():
zuzhang = user.ZuzhangProfile
return Response({
'code': 400,
'msg': '该用户已是组长',
'data': {
'yonghuid': user.UserUID,
'yaoqingma': zuzhang.yaoqingma,
},
})
if guanshi.zhuangtai != 1:
return Response({'code': 400, 'msg': '管事账号已禁用,无法升级组长'})
try:
with transaction.atomic():
zuzhang = _create_zuzhang_profile(user)
except IntegrityError:
return Response({'code': 400, 'msg': '升级失败,请稍后重试'})
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=kefu.user.Phone,
leixing=XIUGAI_LEIXING_GUANSHI,
qitashuoming=(
f'【后台】管事升级组长用户ID={yonghuid}'
f'生成组长邀请码={zuzhang.yaoqingma},操作人={kefu.user.Phone}'
),
)
logger.info(f"管事升级组长成功: yonghuid={yonghuid}, yaoqingma={zuzhang.yaoqingma}")
return Response({
'code': 0,
'msg': '已升级为组长',
'data': {
'yonghuid': user.UserUID,
'nickname': getattr(user.BossProfile, 'nickname', '') if hasattr(user, 'BossProfile') else '',
'yaoqingma': zuzhang.yaoqingma,
'yaoqing_zongshu': zuzhang.yaoqing_zongshu,
'ketixian_jine': str(zuzhang.ketixian_jine),
},
})
class UpdateGuanliView(APIView):
"""
修改管事信息(基础信息、余额、提现限额、分红策略)
权限细分:
- 修改状态(封禁/解封)及昵称/电话/微信需要4400a
- 增加余额需要4400c
- 减少余额需要4400b
- 修改提现限额kaioi_ewai_xiane/ewai_xiane需要4400e
- 修改分红相关永久分红、定制首次、额外次数需要4400f
POST /houtai/htxggsxx
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
yonghuid = request.data.get('yonghuid', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not yonghuid:
return Response({'code': 400, 'msg': '缺少用户ID'})
# 权限校验(先获取权限列表)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 查询管事对象(不在事务外加锁,事务内加锁)
try:
user = User.query.get(UserUID=yonghuid)
guanshi = user.GuanshiProfile
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
except User.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
except UserGuanshi.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是管事'})
# 开始事务,内部使用 select_for_update
with transaction.atomic():
# 重新获取加锁对象
user = User.objects.select_for_update().get(UserUID=yonghuid)
guanshi = user.GuanshiProfile
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
operator = kefu.user.Phone
# ---------- 1. 基础信息修改需要4400a----------
nickname = request.data.get('nickname')
dianhua = request.data.get('dianhua')
wechat = request.data.get('wechat')
zhuangtai = request.data.get('zhuangtai')
avatar = request.data.get('avatar')
if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]):
if '4400a' not in permissions:
return Response({'code': 403, 'msg': '无权限修改管事基础信息需要4400a'})
base_changes = []
old_nickname = boss.nickname if boss else ''
old_dianhua = guanshi.dianhua
old_wechat = guanshi.wechat
old_zhuangtai = guanshi.zhuangtai
# 处理昵称(在老板扩展表中)
if nickname is not None:
if not boss:
boss = UserBoss.query.create(user=user, nickname=nickname)
else:
boss.nickname = nickname
boss.save()
if str(nickname) != str(old_nickname):
base_changes.append(f'昵称:{old_nickname or ""}{nickname}')
# 处理其他字段
if dianhua is not None:
guanshi.dianhua = dianhua
if str(dianhua) != str(old_dianhua):
base_changes.append(f'电话:{old_dianhua or ""}{dianhua}')
if wechat is not None:
guanshi.wechat = wechat
if str(wechat) != str(old_wechat):
base_changes.append(f'微信:{old_wechat or ""}{wechat}')
if zhuangtai is not None:
try:
zt = int(zhuangtai)
if zt in (0, 1):
guanshi.zhuangtai = zt
if zt != old_zhuangtai:
zt_map = {0: '禁用', 1: '正常'}
base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}{zt_map.get(zt, zt)}')
except:
pass
if avatar is not None:
user.Avatar = avatar
user.save()
base_changes.append('更新了头像')
guanshi.save()
if base_changes:
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_GUANSHI,
qitashuoming=(
f'【后台】修改管事基础信息:{"".join(base_changes)}'
f'管事ID={yonghuid},操作人={operator}'
),
)
# ---------- 2. 余额修改4400c4400b----------
new_yue = request.data.get('yue')
if new_yue is not None:
try:
new_yue = Decimal(str(new_yue))
if new_yue < 0:
return Response({'code': 400, 'msg': '余额不能为负数'})
except:
return Response({'code': 400, 'msg': '余额格式错误'})
old_yue = guanshi.yue
if new_yue > old_yue:
if '4400c' not in permissions:
return Response({'code': 403, 'msg': '无权限增加余额需要4400c'})
from jituan.services.group_finance_gate import deny_group_finance_exec
deny = deny_group_finance_exec(request.user, request)
if deny:
return deny
action_desc = f'增加{new_yue - old_yue}'
elif new_yue < old_yue:
if '4400b' not in permissions:
return Response({'code': 403, 'msg': '无权限减少余额需要4400b'})
action_desc = f'减少{old_yue - new_yue}'
else:
action_desc = '无变动'
if new_yue != old_yue:
guanshi.yue = new_yue
guanshi.save()
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_GUANSHI,
guanshiyue=new_yue,
guanshiyueq=old_yue,
qitashuoming=(
f'【后台】修改管事可提现余额:{old_yue}元 → {new_yue}元({action_desc}'
f'管事ID={yonghuid},操作人={operator}'
),
)
# ---------- 3. 提现限额修改需要4400e----------
kaioi_ewai = request.data.get('kaioi_ewai_xiane')
ewai_val = request.data.get('ewai_xiane')
if kaioi_ewai is not None or ewai_val is not None:
if '4400e' not in permissions:
return Response({'code': 403, 'msg': '无权限修改提现限额需要4400e'})
limit_changes = []
old_kaioi = guanshi.kaioi_ewai_xiane
old_ewai = guanshi.ewai_xiane
if kaioi_ewai is not None:
guanshi.kaioi_ewai_xiane = bool(kaioi_ewai)
if bool(kaioi_ewai) != old_kaioi:
limit_changes.append(f'额外限额开关:{"" if old_kaioi else ""}{"" if kaioi_ewai else ""}')
if ewai_val is not None:
try:
guanshi.ewai_xiane = Decimal(str(ewai_val))
if guanshi.ewai_xiane != old_ewai:
limit_changes.append(f'额外限额金额:{old_ewai}{guanshi.ewai_xiane}')
except:
return Response({'code': 400, 'msg': '额外限额金额格式错误'})
guanshi.save()
if limit_changes:
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_GUANSHI,
qitashuoming=(
f'【后台】修改管事提现限额:{"".join(limit_changes)}'
f'管事ID={yonghuid},操作人={operator}'
),
)
# ---------- 4. 分红相关修改需要4400f----------
perm_enabled = request.data.get('permanent_enabled')
perm_amount = request.data.get('permanent_amount')
custom_first = request.data.get('custom_first_dividend')
extra_map = request.data.get('extra_dividend_map')
if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
if '4400f' not in permissions:
return Response({'code': 403, 'msg': '无权限修改分红策略需要4400f'})
# 4.1 永久分红
if perm_enabled is not None:
guanshi.fenghong_erci_enabled = bool(perm_enabled)
if perm_amount is not None:
try:
guanshi.fenghong_erci = Decimal(str(perm_amount))
except:
return Response({'code': 400, 'msg': '永久分红金额格式错误'})
guanshi.save()
# 4.2 首次定制分红cishu=1
if custom_first is not None and isinstance(custom_first, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
# 批量预取当前所有首次定制记录(避免循环内 N+1
existing_first_list = list(
DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, cishu=1,
)
)
_existing_map = {r.huiyuan: r for r in existing_first_list}
_to_create = []
_to_update = []
_to_delete_ids = []
# 前端传来的定制字典 {会员ID: 金额}
for huiyuan_id, amount in custom_first.items():
try:
amount = Decimal(str(amount))
except:
continue
if amount <= 0:
rec = _existing_map.get(huiyuan_id)
if rec is not None:
_to_delete_ids.append(rec.id)
else:
rec = _existing_map.get(huiyuan_id)
if rec is not None:
rec.guanshi_fenhong = amount
rec.zuzhang_fenhong = Decimal('0')
_to_update.append(rec)
else:
_to_create.append(DuociFenhong(
club_id=guanshi_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=1,
guanshi_fenhong=amount,
zuzhang_fenhong=Decimal('0'),
))
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
for rec in existing_first_list:
if rec.huiyuan not in custom_first and rec.id not in _to_delete_ids:
_to_delete_ids.append(rec.id)
if _to_create:
DuociFenhong.objects.bulk_create(_to_create)
if _to_update:
DuociFenhong.objects.bulk_update(_to_update, ['guanshi_fenhong', 'zuzhang_fenhong'])
if _to_delete_ids:
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
# 4.3 额外次数分红cishu >= 2
if extra_map is not None and isinstance(extra_map, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
# 收集需要保留的记录
keep_set = set()
# 解析前端数据,先汇总 keep_set 与目标金额
_extra_targets = [] # [(cishu, huiyuan_id, amount)]
for times_str, members in extra_map.items():
try:
cishu = int(times_str)
if cishu < 2:
continue
except:
continue
if not isinstance(members, dict):
continue
for huiyuan_id, data in members.items():
if not isinstance(data, dict):
continue
amount = data.get('guanshi_fenhong')
if amount is None:
continue
try:
amount = Decimal(str(amount))
except:
continue
keep_set.add((cishu, huiyuan_id))
_extra_targets.append((cishu, huiyuan_id, amount))
# 批量预取已有额外次数记录
existing_extra_list = list(
DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2,
)
)
_extra_map = {(r.cishu, r.huiyuan): r for r in existing_extra_list}
_to_create = []
_to_update = []
_to_delete_ids = []
for cishu, huiyuan_id, amount in _extra_targets:
rec = _extra_map.get((cishu, huiyuan_id))
if rec is not None:
rec.guanshi_fenhong = amount
rec.zuzhang_fenhong = Decimal('0')
_to_update.append(rec)
else:
_to_create.append(DuociFenhong(
club_id=guanshi_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=cishu,
guanshi_fenhong=amount,
zuzhang_fenhong=Decimal('0'),
))
# 删除不在 keep_set 中的额外次数记录
for rec in existing_extra_list:
if (rec.cishu, rec.huiyuan) not in keep_set:
_to_delete_ids.append(rec.id)
if _to_create:
DuociFenhong.objects.bulk_create(_to_create)
if _to_update:
DuociFenhong.objects.bulk_update(_to_update, ['guanshi_fenhong', 'zuzhang_fenhong'])
if _to_delete_ids:
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
dividend_parts = []
if perm_enabled is not None:
dividend_parts.append(f'永久分红开关→{"" if perm_enabled else ""}')
if perm_amount is not None:
dividend_parts.append(f'永久分红金额→{perm_amount}')
if custom_first is not None:
dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)')
if extra_map is not None:
dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)')
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_GUANSHI,
qitashuoming=(
f'【后台】修改管事分红策略:{"".join(dividend_parts) or "配置已更新"}'
f'管事ID={yonghuid},操作人={operator}'
),
)
return Response({'code': 0, 'msg': '修改成功'})

1155
backend/views/kefu.py Normal file

File diff suppressed because it is too large Load Diff

457
backend/views/members.py Normal file
View File

@@ -0,0 +1,457 @@
import hmac
import threading
import traceback
import uuid
import hashlib
import xmltodict
import time
import logging
import requests
import os
import secrets
import random
import string
from calendar import monthrange
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
# 工具类导入
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.catalog_scope import block_non_group_catalog_scope
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
from ..utils import (
verify_kefu_permission,
PERMISSION_TO_SHENFEN,
SHENFEN_PROFILE_MAP,
has_fadan_view_permission,
has_merchant_order_permission,
check_fadan_permission,
pick_order_id,
pick_penalty_create_params,
write_xiugai_log,
XIUGAI_LEIXING_DASHOU,
XIUGAI_LEIXING_GUANSHI,
XIUGAI_LEIXING_SHANGJIA,
XIUGAI_LEIXING_ZUZHANG,
XIUGAI_LEIXING_LABEL,
)
# models 集中导入
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
## backend
from backend.models import WithdrawalDailyStats
## users
from users.models import (
UserGuanshi, UserBoss, UserZuzhang,
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
)
from users.business_models import User
## orders
from orders.models import (
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
OrderPlayerHistory, Order, RefundRecord,
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
)
## shop
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
## products
from products.models import (
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
)
## config
from config.models import (
WithdrawConfig, ShangjiaLianjie, AccountPermission,
TixianQuotaDefault,
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
)
## rank
from rank.models import (
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
)
# 序列化器
from ..serializers import PopupPageSerializer
# 全局常量、日志对象
logger = logging.getLogger('houtai')
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
class GetMemberListView(APIView):
"""
获取会员列表
权限3300a
POST /houtai/hthqhylb
参数username
返回code:0, data: { list: [...] }
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限查看会员列表'})
from jituan.services.club_member_admin import build_member_list_payload
payload = build_member_list_payload(request)
return Response({
'code': 0,
'data': {'list': payload['list']},
'club_id': payload.get('club_id'),
'scope': payload.get('scope'),
'price_note': payload.get('price_note'),
})
class UpdateMemberView(APIView):
"""
修改会员信息
权限3300a
POST /houtai/htxghyxx
参数:
username, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
返回code:0 成功
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
huiyuan_id = request.data.get('huiyuan_id', '').strip()
jieshao = request.data.get('jieshao', '').strip()
jiage = request.data.get('jiage')
guanshifc = request.data.get('guanshifc')
zuzhangfc = request.data.get('zuzhangfc')
jtjieshao = request.data.get('jtjieshao', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not huiyuan_id:
return Response({'code': 400, 'msg': '缺少会员ID'})
if not jieshao:
return Response({'code': 400, 'msg': '会员名称不能为空'})
try:
jiage = Decimal(str(jiage))
guanshifc = Decimal(str(guanshifc))
zuzhangfc = Decimal(str(zuzhangfc))
if jiage < 0 or guanshifc < 0 or zuzhangfc < 0:
raise ValueError
except:
return Response({'code': 400, 'msg': '金额格式错误'})
if guanshifc + zuzhangfc > jiage:
return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
# 权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限修改会员'})
from jituan.services.club_member_admin import update_member_for_club
extra = {
'formal_days': request.data.get('formal_days'),
'trial_enabled': request.data.get('trial_enabled'),
'trial_price': request.data.get('trial_price'),
'trial_days': request.data.get('trial_days'),
'trial_guanshifc': request.data.get('trial_guanshifc'),
'trial_zuzhangfc': request.data.get('trial_zuzhangfc'),
'card_image': request.data.get('card_image'),
'card_bg': request.data.get('card_bg'),
'included_huiyuan_ids': request.data.get('included_huiyuan_ids'),
}
data, err = update_member_for_club(
request, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, extra=extra,
)
if err:
return Response({'code': 400, 'msg': err})
return Response({'code': 0, 'msg': '修改成功'})
class AddMemberView(APIView):
"""
添加会员
权限3300a
POST /houtai/httjhy
参数:
username, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
返回code:0, data: { huiyuan_id }
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
jieshao = request.data.get('jieshao', '').strip()
jiage = request.data.get('jiage')
guanshifc = request.data.get('guanshifc')
zuzhangfc = request.data.get('zuzhangfc')
jtjieshao = request.data.get('jtjieshao', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not jieshao:
return Response({'code': 400, 'msg': '会员名称不能为空'})
try:
jiage = Decimal(str(jiage))
guanshifc = Decimal(str(guanshifc))
zuzhangfc = Decimal(str(zuzhangfc))
if jiage <= 0 or guanshifc < 0 or zuzhangfc < 0:
raise ValueError
except:
return Response({'code': 400, 'msg': '金额格式错误'})
if guanshifc + zuzhangfc > jiage:
return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
# 权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限添加会员'})
from jituan.services.club_member_admin import add_global_member
from jituan.services.club_context import resolve_club_scope
from jituan.constants import DATA_SCOPE_ALL
if resolve_club_scope(request) != DATA_SCOPE_ALL and '000001' not in permissions:
return Response({
'code': 403,
'msg': '仅集团汇总视图可新建全局会员类型;俱乐部请从会员 catalog 上架',
})
data, err = add_global_member(
jieshao, jiage, guanshifc, zuzhangfc, jtjieshao,
is_bundle=bool(request.data.get('is_bundle')),
)
if err:
return Response({'code': 400, 'msg': err})
return Response({'code': 0, 'msg': '添加成功', 'data': data})
class ClubMemberCatalogView(APIView):
"""俱乐部可上架的集团会员 catalog。POST /houtai/hthycatalog"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限'})
from jituan.services.club_member_admin import build_member_catalog_payload
return Response({'code': 0, 'data': build_member_catalog_payload(request)})
class ClubMemberEnableView(APIView):
"""俱乐部上架会员。POST /houtai/hthysj"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
huiyuan_id = request.data.get('huiyuan_id', '').strip()
if not username or not huiyuan_id:
return Response({'code': 400, 'msg': '参数不完整'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限'})
try:
jiage = Decimal(str(request.data.get('jiage')))
guanshifc = Decimal(str(request.data.get('guanshifc')))
zuzhangfc = Decimal(str(request.data.get('zuzhangfc')))
except Exception:
return Response({'code': 400, 'msg': '金额格式错误'})
from jituan.services.club_member_admin import enable_member_for_club
extra = request.data
data, err = enable_member_for_club(
request, huiyuan_id, jiage, guanshifc, zuzhangfc, extra=extra,
)
if err:
return Response({'code': 400, 'msg': err})
return Response({'code': 0, 'msg': '上架成功', 'data': data})
class ClubMemberDisableView(APIView):
"""俱乐部下架会员。POST /houtai/hthyxj"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
huiyuan_id = request.data.get('huiyuan_id', '').strip()
if not username or not huiyuan_id:
return Response({'code': 400, 'msg': '参数不完整'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限'})
from jituan.services.club_member_admin import disable_member_for_club
data, err = disable_member_for_club(request, huiyuan_id)
if err:
return Response({'code': 400, 'msg': err})
return Response({'code': 0, 'msg': '已下架', 'data': data})
class MemberCardImageUploadView(APIView):
"""POST /jituan/houtai/hy-card-upload 上传会员充值卡片图"""
permission_classes = []
parser_classes = [MultiPartParser, FormParser, JSONParser]
def post(self, request):
username = (request.data.get('username') or request.data.get('phone') or '').strip()
huiyuan_id = (request.data.get('huiyuan_id') or '').strip()
field = (request.data.get('field') or 'card_image').strip()
file_obj = request.FILES.get('file')
if not username or not huiyuan_id or not file_obj:
return Response({'code': 400, 'msg': '参数不完整'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限'})
from jituan.services.club_member_admin import upload_member_card_image
data, err = upload_member_card_image(request, huiyuan_id, field, file_obj)
if err:
return Response({'code': 400, 'msg': err})
return Response({'code': 0, 'msg': '上传成功', 'data': data})
class MemberRechargeRecordsView(APIView):
"""POST /jituan/houtai/hy-recharge-records — 会员充值记录明细 + 分红"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = (request.data.get('username') or request.data.get('phone') or '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限查看会员充值记录'})
from jituan.services.member_recharge_records import build_member_recharge_records_payload
filters = request.data.get('filters') or {}
for key in (
'dingdan_id', 'yonghuid', 'buyer_keyword', 'huiyuan_id', 'is_trial',
'zhuangtai', 'date_from', 'date_to', 'jine_min', 'jine_max',
'guanshi_id', 'zuzhang_id', 'keyword', 'is_bundle', 'has_fenhong', 'club_id',
):
if request.data.get(key) not in (None, ''):
filters[key] = request.data.get(key)
payload = build_member_recharge_records_payload(
request,
filters=filters,
page=request.data.get('page', 1),
page_size=request.data.get('page_size', 20),
)
return Response({'code': 0, 'data': payload})
class MemberBundleCreateView(APIView):
"""POST /jituan/houtai/hy-bundle-create — 俱乐部一键创建总会员并上架"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = (request.data.get('username') or request.data.get('phone') or '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限'})
jieshao = (request.data.get('jieshao') or '').strip()
if not jieshao:
return Response({'code': 400, 'msg': '会员名称不能为空'})
try:
jiage = Decimal(str(request.data.get('jiage')))
guanshifc = Decimal(str(request.data.get('guanshifc')))
zuzhangfc = Decimal(str(request.data.get('zuzhangfc')))
except Exception:
return Response({'code': 400, 'msg': '金额格式错误'})
from jituan.services.club_member_admin import create_bundle_member_for_club
data, err = create_bundle_member_for_club(
request,
jieshao=jieshao,
jiage=jiage,
guanshifc=guanshifc,
zuzhangfc=zuzhangfc,
jtjieshao=(request.data.get('jtjieshao') or '').strip(),
formal_days=int(request.data.get('formal_days') or 30),
included_huiyuan_ids=request.data.get('included_huiyuan_ids') or [],
)
if err:
return Response({'code': 400, 'msg': err})
return Response({'code': 0, 'msg': '总会员创建并上架成功', 'data': data})

735
backend/views/penalties.py Normal file
View File

@@ -0,0 +1,735 @@
import hmac
import threading
import traceback
import uuid
import hashlib
import xmltodict
import time
import logging
import requests
import os
import secrets
import random
import string
from calendar import monthrange
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
# 工具类导入
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.catalog_scope import block_non_group_catalog_scope
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
from ..utils import (
verify_kefu_permission,
PERMISSION_TO_SHENFEN,
SHENFEN_PROFILE_MAP,
has_fadan_view_permission,
has_merchant_order_permission,
check_fadan_permission,
pick_order_id,
pick_penalty_create_params,
write_xiugai_log,
XIUGAI_LEIXING_DASHOU,
XIUGAI_LEIXING_GUANSHI,
XIUGAI_LEIXING_SHANGJIA,
XIUGAI_LEIXING_ZUZHANG,
XIUGAI_LEIXING_LABEL,
)
# models 集中导入
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
## backend
from backend.models import WithdrawalDailyStats
## users
from users.models import (
UserGuanshi, UserBoss, UserZuzhang,
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
)
from users.business_models import User
## orders
from orders.models import (
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
OrderPlayerHistory, Order, RefundRecord,
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
)
## shop
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
## products
from products.models import (
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
)
## config
from config.models import (
WithdrawConfig, ShangjiaLianjie, AccountPermission,
TixianQuotaDefault,
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
)
## rank
from rank.models import (
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
)
# 序列化器
from ..serializers import PopupPageSerializer
# 全局常量、日志对象
logger = logging.getLogger('houtai')
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
class FaKuanTongJiView(APIView):
"""
罚款统计接口
POST /houtai/htglyhqcfsltj
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '参数不完整'})
try:
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if not has_fadan_view_permission(permissions, username):
return Response({'code': 403, 'msg': '无权限查看罚款统计'})
from utils.penalty_status import penalty_platform_audit_pending_q
pending_q = penalty_platform_audit_pending_q()
stats = filter_penalty_qs(Penalty.query.all(), request).aggregate(
total=Count('id'),
daijiaona=Count('id', filter=Q(Status=1)),
shensuzhong=Count('id', filter=Q(Status=3)),
yijiaona=Count('id', filter=Q(Status=2)),
yibohui=Count('id', filter=Q(Status=4)),
pingtai_shenhe=Count('id', filter=pending_q),
)
data = {k: v or 0 for k, v in stats.items()}
from jituan.services.club_user_access import list_response_meta
return Response({'code': 0, 'msg': '成功', 'data': {**data, **list_response_meta(request)}})
except Exception as e:
logger.error(f"罚款统计异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
class FaKuanLieBiaoView(APIView):
"""
罚款列表接口
POST /houtai/hthqfklb
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '参数不完整'})
try:
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if not has_fadan_view_permission(permissions, username):
return Response({'code': 403, 'msg': '无权限查看罚款列表'})
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
if page_size > 100:
page_size = 100
qs = filter_penalty_qs(Penalty.query.all(), request)
# 状态筛选5=平台审核中,仅 zhuangtai=5
from utils.penalty_status import penalty_platform_audit_pending_q
zhuangtai_raw = request.data.get('zhuangtai')
if zhuangtai_raw is not None and str(zhuangtai_raw).strip() != '':
try:
zt = int(zhuangtai_raw)
if zt == 5:
qs = qs.filter(penalty_platform_audit_pending_q())
elif zt in (1, 2, 3, 4):
qs = qs.filter(Status=zt)
except (ValueError, TypeError):
pass
# 被处罚人ID
PenalizedUserID = request.data.get('PenalizedUserID', '').strip()
if PenalizedUserID:
qs = qs.filter(PenalizedUserID=PenalizedUserID)
# 被处罚人身份
beichufa_shenfen = request.data.get('beichufa_shenfen')
if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]:
qs = qs.filter(Identity=int(beichufa_shenfen))
# 模糊搜索订单ID、被罚人ID、申请人ID
sousuo = request.data.get('sousuo', '').strip()
if sousuo:
qs = qs.filter(
Q(RelatedOrderID__icontains=sousuo) |
Q(PenalizedUserID__icontains=sousuo) |
Q(ApplicantID__icontains=sousuo)
)
qs = qs.order_by('-CreateTime')
paginator = Paginator(qs, page_size)
try:
page_obj = paginator.page(page)
except Exception:
return Response({
'code': 0, 'msg': '成功',
'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size}
})
records = list(page_obj)
penalty_ids = [r.id for r in records]
# 批量获取图片并分组
zhengju_map = {}
shensu_map = {}
if penalty_ids:
tupians = PenaltyAppealImage.query.filter(
Penalty_id__in=penalty_ids
).values('Penalty_id', 'ImageURL', 'Purpose')
for t in tupians:
fid = t['Penalty_id']
url = t['ImageURL']
if t['Purpose'] == 1:
zhengju_map.setdefault(fid, []).append(url)
else:
shensu_map.setdefault(fid, []).append(url)
from utils.penalty_status import penalty_display_status, is_penalty_platform_audit_pending
# 组装列表数据
results = []
for r in records:
results.append({
'id': r.id,
'beichufa_id': r.PenalizedUserID,
'shenfen': r.Identity,
'shenfen_text': dict(Penalty._meta.get_field('Identity').choices).get(r.Identity, ''),
'shenqing_chufa': r.ApplicantID or '',
'shenqingren_shenfen': r.ApplicantIdentity,
'chufaliyou': r.Reason or '',
'fakuanjine': str(r.FineAmount),
'guanliandingdan_id': r.RelatedOrderID or '',
'zhuangtai': r.Status,
'zhuangtai_text': penalty_display_status(r),
'platform_audit_pending': is_penalty_platform_audit_pending(r),
'yingxiang_qiangdan': r.AffectsGrabbing,
'shensuliyou': r.AppealReason or '',
'bohuiliyou': r.RejectReason or '',
'chulizhe': r.ProcessorID or '',
'chulizhe_shenfen': r.ProcessorIdentity,
'zhengju_tupian': zhengju_map.get(r.id, []),
'shensu_tupian': shensu_map.get(r.id, []),
'creat_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '',
})
from jituan.services.club_user_access import list_response_meta
return Response({
'code': 0, 'msg': '成功',
'data': {
'list': results,
'total': paginator.count,
'page': page,
'page_size': page_size,
**list_response_meta(request),
}
})
except Exception as e:
logger.error(f"罚款列表异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
class FaKuanChuLiView(APIView):
"""
处理罚款申诉(同意/拒绝)
POST /houtai/glyclfk
"""
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser, JSONParser]
def post(self, request):
try:
username = request.data.get('username', '').strip()
penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id')
action = request.data.get('action', '').strip()
chuli_liyou = request.data.get('chuli_liyou', '').strip()
if not all([username, penalty_id, action]):
return Response({'code': 400, 'msg': '参数不完整'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
with transaction.atomic():
try:
penalty = Penalty.objects.select_for_update().get(id=penalty_id)
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在'})
deny = forbid_penalty_out_of_scope(request, penalty)
if deny:
return deny
if penalty.Status != 3:
return Response({'code': 400, 'msg': '当前状态不可处理'})
if not check_fadan_permission(permissions, penalty.Identity):
return Response({'code': 403, 'msg': '无权限处理该类罚单'})
# 处理上传的图片
uploaded_urls = []
files = request.FILES.getlist('tupian') or []
if not files:
single = request.FILES.get('tupian')
if single:
files = [single]
for file in files:
valid, msg = validate_image(file)
if not valid:
return Response({'code': 400, 'msg': msg})
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
file_name = f"chufa_{penalty_id}_{int(time.time() * 1000)}.{ext}"
file_path = f"chufatupian/kechufatupian/{file_name}"
url = upload_to_oss(file, file_path)
if not url:
return Response({'code': 500, 'msg': '图片上传失败'})
uploaded_urls.append(file_path)
# 更新状态
if action == 'agree': # 同意申诉 → 驳回处罚
penalty.Status = 4
penalty.RejectReason = chuli_liyou
elif action == 'reject': # 拒绝申诉 → 回到待缴纳
penalty.Status = 1
penalty.RejectReason = chuli_liyou
else:
return Response({'code': 400, 'msg': '无效的操作类型'})
# 记录处理者(使用 request.user 的 phone
penalty.ProcessorID = request.user.Phone
penalty.ProcessorIdentity = 2 # 这里假设处理者是售后身份,可根据实际业务获取
penalty.save()
# 保存图片证据图片用途1
for relative_url in uploaded_urls:
PenaltyAppealImage.query.create(
Penalty_id=penalty_id,
PenalizedUserID=penalty.PenalizedUserID,
ImageURL=relative_url,
Purpose=1
)
logger.info(f"罚款处理成功: {penalty_id}, 操作: {action}")
return Response({'code': 0, 'msg': '处理成功'})
except Exception as e:
logger.error(f"罚款处理异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
class FaKuanPingTaiShenHeView(APIView):
"""
平台审核商家提交的罚款(同意/驳回)
POST /houtai/glyptshfk
action: platform_approve | platform_reject
"""
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser, JSONParser]
def post(self, request):
try:
from utils.penalty_status import (
PENALTY_PLATFORM_AUDIT, PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_REJECTED,
)
username = request.data.get('username', '').strip()
penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id')
action = request.data.get('action', '').strip()
chuli_liyou = request.data.get('chuli_liyou', '').strip()
if not all([username, penalty_id, action]):
return Response({'code': 400, 'msg': '参数不完整'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
with transaction.atomic():
try:
penalty = Penalty.objects.select_for_update().get(id=penalty_id)
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在'})
deny = forbid_penalty_out_of_scope(request, penalty)
if deny:
return deny
if penalty.Status != PENALTY_PLATFORM_AUDIT:
return Response({'code': 400, 'msg': '当前状态不可平台审核'})
if not check_fadan_permission(permissions, penalty.Identity):
return Response({'code': 403, 'msg': '无权限处理该类罚单'})
if action == 'platform_approve':
if penalty.AppealReason:
penalty.Status = PENALTY_APPEALING
else:
penalty.Status = PENALTY_PENDING_PAY
penalty.ApproveReason = chuli_liyou or penalty.ApproveReason or ''
elif action == 'platform_reject':
penalty.Status = PENALTY_REJECTED
penalty.RejectReason = chuli_liyou or '平台审核驳回'
else:
return Response({'code': 400, 'msg': '无效的操作类型'})
penalty.ProcessorID = request.user.Phone
penalty.ProcessorIdentity = 3
penalty.save()
logger.info(f"罚款平台审核成功: {penalty_id}, 操作: {action}")
return Response({'code': 0, 'msg': '平台审核完成'})
except Exception as e:
logger.error(f"罚款平台审核异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
class FaKuanChuangJianView(APIView):
"""
管理员创建罚款
POST /houtai/htfksc
"""
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser, JSONParser]
def post(self, request):
try:
username = request.data.get('username', '').strip()
PenalizedUserID, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data)
shenfen = request.data.get('shenfen')
if not all([username, PenalizedUserID, shenfen, Reason, FineAmount is not None]):
return Response({'code': 400, 'msg': '参数不完整'})
try:
shenfen = int(shenfen)
FineAmount = float(FineAmount)
except (ValueError, TypeError):
return Response({'code': 400, 'msg': '参数格式错误'})
if shenfen not in [1, 2, 3, 4]:
return Response({'code': 400, 'msg': '无效的被罚人身份'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if not check_fadan_permission(permissions, shenfen):
return Response({'code': 403, 'msg': '无权限创建该类罚单'})
# 验证用户是否存在对应的扩展表
try:
user = User.query.get(UserUID=PenalizedUserID)
profile_attr = SHENFEN_PROFILE_MAP.get(shenfen)
if not profile_attr or not hasattr(user, profile_attr):
return Response({'code': 400, 'msg': '该用户不是所选身份'})
except User.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
# 处理图片上传
uploaded_urls = []
files = request.FILES.getlist('tupian') or []
if not files:
single = request.FILES.get('tupian')
if single:
files = [single]
for file in files:
valid, msg = validate_image(file)
if not valid:
return Response({'code': 400, 'msg': msg})
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
file_path = f"chufatupian/kechufatupian/{file_name}"
url = upload_to_oss(file, file_path)
if not url:
return Response({'code': 500, 'msg': '图片上传失败'})
uploaded_urls.append(file_path)
with transaction.atomic():
penalty = Penalty.query.create(
PenalizedUserID=PenalizedUserID,
Identity=shenfen,
ApplicantID=request.user.Phone, # 申请人(客服)手机号
ApplicantIdentity=2, # 售后身份
Reason=Reason,
FineAmount=FineAmount,
AffectsGrabbing=AffectsGrabbing,
Status=1, # 待缴纳
ClubID=resolve_penalty_club_id(
penalized_user_id=PenalizedUserID,
request=request,
user=request.user,
),
)
PenaltyAppealImage.objects.bulk_create([
PenaltyAppealImage(
Penalty=penalty,
PenalizedUserID=PenalizedUserID,
ImageURL=relative_url,
Purpose=1,
)
for relative_url in uploaded_urls
])
from users.fadan_fenhong_utils import lock_penalty_bonus
lock_penalty_bonus(penalty)
logger.info(f"罚款创建成功: {penalty.id}")
return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': penalty.id}})
except Exception as e:
logger.error(f"创建罚款异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
class FineApplyView(APIView):
"""
客服罚款申请接口(基于订单对打手发起罚款)
POST /houtai/kffkdssq
权限: JWT认证 + 客服权限验证
请求参数:
username (必填) : 客服用户名(手机号)
OrderID (必填) : 订单ID
Reason (必填) : 罚款原因
FineAmount (必填) : 罚款金额(元)
AffectsGrabbing (可选) : 是否影响抢单1=是0=否默认1
"""
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser, JSONParser]
def post(self, request):
try:
username = request.data.get('username', '').strip()
OrderID = pick_order_id(request.data)
_, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data)
if not all([username, OrderID, Reason, FineAmount is not None]):
return Response({'code': 400, 'msg': '参数不完整'})
try:
FineAmount = float(FineAmount)
except (ValueError, TypeError):
return Response({'code': 400, 'msg': '罚款金额格式错误'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 验证订单存在
try:
order = Order.query.get(OrderID=OrderID)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
# 获取打手ID
dashou_id = order.PlayerID
if not dashou_id:
return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'})
# 重复罚款检查
if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=OrderID).exists():
return Response({'code': 400, 'msg': '该打手已被罚款过'})
# 检查权限(打手身份 shenfen=1
if not check_fadan_permission(permissions, 1):
return Response({'code': 403, 'msg': '无权限处罚打手'})
# 处理图片上传
uploaded_urls = []
files = request.FILES.getlist('tupian') or []
if not files:
single = request.FILES.get('tupian')
if single:
files = [single]
for file in files:
valid, msg = validate_image(file)
if not valid:
return Response({'code': 400, 'msg': msg})
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
file_path = f"chufatupian/kechufatupian/{file_name}"
url = upload_to_oss(file, file_path)
if not url:
return Response({'code': 500, 'msg': '图片上传失败'})
uploaded_urls.append(file_path)
# 创建罚单
with transaction.atomic():
penalty = Penalty.query.create(
PenalizedUserID=dashou_id,
ApplicantID=request.user.Phone,
Identity=1, # 被处罚人身份1 打手
Reason=Reason,
FineAmount=FineAmount,
RelatedOrderID=OrderID,
Status=1, # 待缴纳
AffectsGrabbing=AffectsGrabbing,
ApplicantIdentity=1, # 申请人身份1 客服
ClubID=resolve_penalty_club_id(
order=order,
request=request,
user=request.user,
applicant_id=getattr(request.user, 'UserUID', None),
),
)
for relative_url in uploaded_urls:
PenaltyAppealImage.query.create(
Penalty=penalty,
PenalizedUserID=dashou_id,
ImageURL=relative_url,
Purpose=1
)
from users.fadan_fenhong_utils import lock_penalty_bonus
lock_penalty_bonus(penalty)
logger.info(f"客服罚款申请成功: 订单 {OrderID}, 打手 {dashou_id}, 金额 {FineAmount}")
return Response({'code': 0, 'msg': '罚款已生成', 'data': {'id': penalty.id}})
except Exception as e:
logger.error(f"客服罚款申请异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
class PunishDashouView(APIView):
"""
客服处罚打手接口(扣除积分)
POST /houtai/kpcf
权限: JWT认证 + 客服权限验证
请求参数:
username (必填) : 客服用户名(手机号)
OrderID (必填) : 订单ID
reason (可选) : 处罚原因
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
try:
username = request.data.get('username', '').strip()
OrderID = request.data.get('OrderID', '').strip()
reason = request.data.get('reason', '').strip()
if not all([username, OrderID]):
return Response({'code': 400, 'msg': '参数不完整'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 检查权限(打手身份 shenfen=1
if not check_fadan_permission(permissions, 1):
return Response({'code': 403, 'msg': '无权限处罚打手'})
# 查询订单
try:
order = Order.query.get(OrderID=OrderID)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
# 获取打手ID
dashou_id = order.PlayerID
if not dashou_id:
return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'})
# 查询打手
try:
dashou_user = User.query.get(UserUID=dashou_id)
dashou = dashou_user.DashouProfile
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手用户不存在'})
except ObjectDoesNotExist:
return Response({'code': 400, 'msg': '该用户不是打手'})
# 创建处罚记录扣除积分5分
jifen = 5
with transaction.atomic():
penalty_record = PenaltyRecord.query.create(
PlayerID=dashou_id,
ApplicantID=username,
PenaltyReason=reason or '',
ApplyStatus=0,
DeductedPoints=jifen,
OrderID=OrderID,
ClubID=resolve_penalty_record_club_id(
order_id=OrderID,
player_id=dashou_id,
request=request,
user=request.user,
applicant_id=getattr(request.user, 'UserUID', None) or username,
),
)
# 扣除打手积分
dashou.jifen = F('jifen') - jifen
dashou.save(update_fields=['jifen'])
logger.info(f"客服处罚打手成功: 打手 {dashou_id}, 订单 {OrderID}, 扣除积分 {jifen}")
return Response({'code': 0, 'msg': '处罚成功', 'data': {'penalty_record_id': penalty_record.id}})
except Exception as e:
logger.error(f"客服处罚打手异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})

1151
backend/views/products.py Normal file

File diff suppressed because it is too large Load Diff

960
backend/views/roles.py Normal file
View File

@@ -0,0 +1,960 @@
import hmac
import threading
import traceback
import uuid
import hashlib
import xmltodict
import time
import logging
import requests
import os
import secrets
import random
import string
from calendar import monthrange
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
# 工具类导入
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.catalog_scope import block_non_group_catalog_scope
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
from ..utils import (
verify_kefu_permission,
PERMISSION_TO_SHENFEN,
SHENFEN_PROFILE_MAP,
has_fadan_view_permission,
has_merchant_order_permission,
check_fadan_permission,
pick_order_id,
pick_penalty_create_params,
write_xiugai_log,
XIUGAI_LEIXING_DASHOU,
XIUGAI_LEIXING_GUANSHI,
XIUGAI_LEIXING_SHANGJIA,
XIUGAI_LEIXING_ZUZHANG,
XIUGAI_LEIXING_LABEL,
)
# models 集中导入
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
## backend
from backend.models import WithdrawalDailyStats
## users
from users.models import (
UserGuanshi, UserBoss, UserZuzhang,
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
)
from users.business_models import User
## orders
from orders.models import (
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
OrderPlayerHistory, Order, RefundRecord,
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
)
## shop
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
## products
from products.models import (
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
)
## config
from config.models import (
WithdrawConfig, ShangjiaLianjie, AccountPermission,
TixianQuotaDefault,
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
)
## rank
from rank.models import (
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
)
# 序列化器
from ..serializers import PopupPageSerializer
# 全局常量、日志对象
logger = logging.getLogger('houtai')
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
def _serialize_permission_row(perm_row):
"""gvsdsdk Permission → 后台 Vue 约定的小写字段"""
if isinstance(perm_row, dict):
code = perm_row.get('PermCode') or perm_row.get('perm_code') or ''
name = perm_row.get('PermName') or perm_row.get('perm_name') or code
desc = perm_row.get('PermDesc') or perm_row.get('perm_desc') or ''
else:
code = perm_row.PermCode or ''
name = perm_row.PermName or code
desc = perm_row.PermDesc or ''
return {'perm_code': code, 'perm_name': name, 'perm_desc': desc}
class GetRolePermissionView(APIView):
"""
获取所有角色、权限以及角色关联的用户数
权限要求:用户必须拥有 000001 权限
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
# 1. 权限校验(验证客服身份,并获取权限列表)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions # 直接返回错误响应
from jituan.services.admin_context import can_access_admin_user_management
if not can_access_admin_user_management(request.user, permissions):
return Response({'code': 403, 'msg': '您无权访问此页面'})
# 3. 获取所有权限
all_perms = Permission.objects.filter(PermStatus=1).values('PermCode', 'PermName', 'PermDesc')
permissions_list = [_serialize_permission_row(p) for p in all_perms]
from jituan.services.club_rbac import count_role_users_in_scope, filter_roles_for_request
kefu_user_uuids = list(
User.objects.filter(KefuProfile__isnull=False).values_list('UserUUID', flat=True)
)
roles = list(filter_roles_for_request(request))
# 批量预取 RolePermission + Permission避免循环内 N+1
_role_uuids = [r.RoleUUID for r in roles]
_rp_pairs = list(
RolePermission.objects.filter(RoleUUID__in=_role_uuids)
.values_list('RoleUUID', 'PermUUID')
) if _role_uuids else []
_perm_uuids = list({p for _, p in _rp_pairs})
_perm_rows_by_uuid = {
p['PermUUID']: p
for p in Permission.objects.filter(PermUUID__in=_perm_uuids)
.values('PermUUID', 'PermCode', 'PermName', 'PermDesc')
} if _perm_uuids else {}
_perms_by_role = {}
for role_uuid, perm_uuid in _rp_pairs:
perm_row = _perm_rows_by_uuid.get(perm_uuid)
if perm_row is not None:
_perms_by_role.setdefault(role_uuid, []).append(_serialize_permission_row(perm_row))
roles_data = []
for role in roles:
user_count = count_role_users_in_scope(role, request, kefu_user_uuids)
perms_of_role = _perms_by_role.get(role.RoleUUID, [])
roles_data.append({
'role_code': role.RoleName,
'role_name': role.RoleName,
'description': role.RoleDesc or '',
'club_id': getattr(role, 'ClubID', '') or '',
'user_count': user_count,
'permissions': perms_of_role
})
return Response({
'code': 0,
'data': {
'roles': roles_data,
'permissions': permissions_list
}
})
class ModifyRolePermissionView(APIView):
"""
通用接口:支持删除角色、为角色添加权限、移除角色权限
操作类型通过 action 字段区分
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
action = request.data.get('action') # delete_role, add_permission, remove_permission
if not username or not action:
return Response({'code': 400, 'msg': '缺少必要参数'})
# 权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '000001' not in permissions:
return Response({'code': 403, 'msg': '您无权进行此操作'})
# 获取角色(按当前俱乐部解析,避免多俱乐部同名角色冲突)
role_code = request.data.get('role_code')
if not role_code:
return Response({'code': 400, 'msg': '缺少角色编码'})
from jituan.services.club_rbac import resolve_role_by_name, resolve_role_club_id
explicit_club = (request.data.get('target_club_id') or request.data.get('club_id') or '').strip() or None
bind_club_id = resolve_role_club_id(request, explicit_club)
role = resolve_role_by_name(role_code, bind_club_id)
if not role:
return Response({'code': 404, 'msg': '角色不存在或与当前俱乐部不匹配'})
# 超级管理员角色的特殊限制
if role.RoleName == '管理员':
if action in ['delete_role']:
return Response({'code': 403, 'msg': '超级管理员角色不能删除'})
# 根据 action 处理
if action == 'delete_role':
# 删除角色:同时删除角色权限关联、用户角色关联
with transaction.atomic():
RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete()
UserRole.objects.filter(RoleUUID=role.RoleUUID).delete()
role.delete()
return Response({'code': 0, 'msg': '角色删除成功'})
elif action == 'add_permission':
perm_code = request.data.get('perm_code')
if not perm_code:
return Response({'code': 400, 'msg': '缺少权限编码'})
if role.RoleName == '管理员' and perm_code == '000001':
return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'})
try:
perm = Permission.objects.get(PermCode=perm_code)
except Permission.DoesNotExist:
return Response({'code': 404, 'msg': '权限不存在'})
# 检查是否已存在
if RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).exists():
return Response({'code': 400, 'msg': '该角色已拥有此权限'})
RolePermission.objects.create(
RolePermUUID=uuid.uuid4().bytes,
RoleUUID=role.RoleUUID,
PermUUID=perm.PermUUID,
CreateTime=timezone.now(),
)
return Response({'code': 0, 'msg': '权限添加成功'})
elif action == 'remove_permission':
perm_code = request.data.get('perm_code')
if not perm_code:
return Response({'code': 400, 'msg': '缺少权限编码'})
if role.RoleName == '管理员' and perm_code == '000001':
return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'})
try:
perm = Permission.objects.get(PermCode=perm_code)
except Permission.DoesNotExist:
return Response({'code': 404, 'msg': '权限不存在'})
deleted, _ = RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).delete()
if deleted == 0:
return Response({'code': 400, 'msg': '该角色未拥有此权限'})
return Response({'code': 0, 'msg': '权限移除成功'})
elif action == 'update_role':
role_name = (request.data.get('role_name') or '').strip()
description = (request.data.get('description') or '').strip()
perm_codes = request.data.get('perm_codes') or []
if role.RoleName == '管理员':
role_name = role.RoleName
elif role_name and role_name != role.RoleName:
role_club_id = getattr(role, 'ClubID', '') or ''
if Role.objects.filter(
RoleName=role_name, ClubID=role_club_id,
).exclude(RoleUUID=role.RoleUUID).exists():
return Response({'code': 400, 'msg': '该俱乐部下角色名称已存在'})
with transaction.atomic():
if role.RoleName != '管理员':
if role_name:
role.RoleName = role_name
role.RoleDesc = description
role.save(update_fields=['RoleName', 'RoleDesc'])
RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete()
for pc in perm_codes:
if pc == '000001' and role.RoleName != '管理员':
continue
try:
perm = Permission.objects.get(PermCode=pc, PermStatus=1)
except Permission.DoesNotExist:
continue
RolePermission.objects.create(
RolePermUUID=uuid.uuid4().bytes,
RoleUUID=role.RoleUUID,
PermUUID=perm.PermUUID,
CreateTime=timezone.now(),
)
if role.RoleName == '管理员':
try:
super_perm = Permission.objects.get(PermCode='000001', PermStatus=1)
if not RolePermission.objects.filter(
RoleUUID=role.RoleUUID, PermUUID=super_perm.PermUUID,
).exists():
RolePermission.objects.create(
RolePermUUID=uuid.uuid4().bytes,
RoleUUID=role.RoleUUID,
PermUUID=super_perm.PermUUID,
CreateTime=timezone.now(),
)
except Permission.DoesNotExist:
pass
return Response({'code': 0, 'msg': '角色保存成功'})
else:
return Response({'code': 400, 'msg': '无效的 action'})
class AddRoleView(APIView):
"""
添加角色:自动生成角色编码,前端提供角色名称、描述、选择的权限列表
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
role_name = request.data.get('role_name', '').strip()
description = request.data.get('description', '').strip()
perm_codes = request.data.get('perm_codes', []) # 列表
if not username or not role_name:
return Response({'code': 400, 'msg': '缺少必要参数'})
# 权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '000001' not in permissions:
return Response({'code': 403, 'msg': '您无权进行此操作'})
# 检查角色名称是否已存在(同俱乐部内唯一)
from jituan.services.club_rbac import resolve_role_club_id, role_name_exists
target_club_id = resolve_role_club_id(
request, (request.data.get('target_club_id') or '').strip() or None,
)
if role_name_exists(role_name, target_club_id):
return Response({'code': 400, 'msg': '该俱乐部下角色名称已存在'})
# 生成唯一角色编码格式ROLE_ + 8位随机字母数字
role_code = f"ROLE_{''.join(random.choices(string.ascii_uppercase + string.digits, k=8))}"
# 创建角色
with transaction.atomic():
role = Role.objects.create(
RoleUUID=uuid.uuid4().bytes,
TenantUUID=b'\x00' * 16,
RoleName=role_name,
RoleType=2,
RoleStatus=1,
AssignScope=0,
RoleDesc=description,
ClubID=target_club_id,
CreateTime=timezone.now(),
)
# 绑定权限(过滤掉不存在的权限编码)
for pc in perm_codes:
# 不允许添加 000001 权限(此权限仅供超级管理员)
if pc == '000001':
continue
try:
perm = Permission.objects.get(PermCode=pc)
RolePermission.objects.create(
RolePermUUID=uuid.uuid4().bytes,
RoleUUID=role.RoleUUID,
PermUUID=perm.PermUUID,
CreateTime=timezone.now(),
)
except Permission.DoesNotExist:
logger.warning(f"添加角色时忽略不存在的权限编码: {pc}")
return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}})
class GetAdminUserListView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
from jituan.services.admin_context import can_access_admin_user_management
if not can_access_admin_user_management(request.user, permissions):
return Response({'code': 403, 'msg': '您无权访问此页面'})
phone = request.data.get('phone', '').strip()
nicheng = request.data.get('nicheng', '').strip()
role_code = request.data.get('roleCode', '').strip()
page = int(request.data.get('page', 1))
page_size = int(request.data.get('pageSize', 20))
# UserType 是 @property 非数据库字段,不能用 filter
# KefuProfile__isnull=False 已确保只查到客服用户
users = User.query.filter(
KefuProfile__isnull=False
).select_related('KefuProfile')
from jituan.models import AdminAssignment, Club
from jituan.constants import ADMIN_ROLE_LABELS
from jituan.services.club_rbac import filter_kefu_users_by_admin_scope
users = filter_kefu_users_by_admin_scope(users, request)
if phone:
users = users.filter(Phone__icontains=phone)
if nicheng:
users = users.filter(KefuProfile__nicheng__icontains=nicheng)
# 关键修正:先求值为列表,再传入 phone__in
if role_code:
from jituan.services.club_rbac import resolve_role_by_name, resolve_role_club_id
filter_club_id = resolve_role_club_id(request)
target_role = resolve_role_by_name(role_code, filter_club_id)
if target_role:
user_uuids = list(
UserRole.objects.filter(RoleUUID=target_role.RoleUUID)
.values_list('UserUUID', flat=True)
)
users = users.filter(UserUUID__in=user_uuids)
else:
users = users.none()
users = users.order_by('-UserCreateTime')
paginator = Paginator(users, page_size)
page_obj = paginator.get_page(page)
# 批量预取本页所有用户的 UserRole + Role避免循环内 N+1
_page_users = list(page_obj)
_page_user_uuids = [u.UserUUID for u in _page_users]
_ur_pairs = list(
UserRole.objects.filter(UserUUID__in=_page_user_uuids)
.values_list('UserUUID', 'RoleUUID')
) if _page_user_uuids else []
_role_uuids_in_page = list({r for _, r in _ur_pairs})
_role_info_by_uuid = {
r.RoleUUID: {'role_name': r.RoleName, 'club_id': getattr(r, 'ClubID', '') or ''}
for r in Role.objects.filter(RoleUUID__in=_role_uuids_in_page)
} if _role_uuids_in_page else {}
_roles_by_user = {}
for user_uuid, role_uuid in _ur_pairs:
info = _role_info_by_uuid.get(role_uuid)
if info is not None:
_roles_by_user.setdefault(user_uuid, []).append({
'role_code': info['role_name'],
'role_name': info['role_name'],
'club_id': info['club_id'],
})
_page_yonghuids = [u.UserUID for u in _page_users]
_club_name_map = {c.club_id: c.name for c in Club.query.filter(status=1)}
_assignments_by_user = {}
if _page_yonghuids:
for a in AdminAssignment.query.filter(yonghuid__in=_page_yonghuids, status=1):
if a.club_id:
club_label = _club_name_map.get(a.club_id, a.club_id)
else:
club_label = '集团(全部子公司)'
_assignments_by_user.setdefault(a.yonghuid, []).append({
'id': a.id,
'club_id': a.club_id,
'club_label': club_label,
'role_code': a.role_code,
'role_name': ADMIN_ROLE_LABELS.get(a.role_code, a.role_code),
'is_primary': a.is_primary,
})
data_list = []
for user in _page_users:
roles_data = _roles_by_user.get(user.UserUUID, [])
data_list.append({
'phone': user.Phone or '',
'yonghuid': user.UserUID or '',
'nicheng': user.KefuProfile.nicheng,
'club_id': getattr(user, 'ClubID', '') or '',
'roles': roles_data,
'assignments': _assignments_by_user.get(user.UserUID, []),
'status': user.KefuProfile.zhuangtai,
'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
'update_time': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '',
'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
'UpdateTime': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '',
})
from jituan.services.admin_context import can_manage_admin_assignments, get_manageable_club_ids
return Response({
'code': 0,
'data': {
'list': data_list,
'total': paginator.count,
'page': page,
'pageSize': page_size,
'can_manage_club_assignments': can_manage_admin_assignments(request.user, permissions),
'manageable_club_ids': sorted(get_manageable_club_ids(request.user, permissions)),
}
})
except Exception as e:
logger.error(f"GetAdminUserListView 异常: {e}\n{traceback.format_exc()}")
return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'})
class GetAdminRolesView(APIView):
"""获取所有角色(用于筛选和分配)"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
from jituan.services.admin_context import can_access_admin_user_management
if not can_access_admin_user_management(request.user, permissions):
return Response({'code': 403, 'msg': '您无权访问此页面'})
from jituan.services.admin_context import can_manage_admin_assignments, get_manageable_club_ids
from jituan.services.club_rbac import filter_roles_for_request
all_clubs = request.data.get('all_clubs') in (True, 1, '1', 'true', 'yes')
if all_clubs and can_manage_admin_assignments(request.user, permissions):
roles = Role.objects.filter(RoleStatus=1).order_by('ClubID', 'RoleName').values('RoleName', 'ClubID')
else:
manageable = get_manageable_club_ids(request.user, permissions)
if manageable:
roles = Role.objects.filter(
RoleStatus=1, ClubID__in=manageable,
).order_by('ClubID', 'RoleName').values('RoleName', 'ClubID')
else:
roles = filter_roles_for_request(request).values('RoleName', 'ClubID')
return Response({
'code': 0,
'data': {
'roles': [
{
'role_code': r['RoleName'],
'role_name': r['RoleName'],
'club_id': r.get('ClubID') or '',
}
for r in roles
]
}
})
'''class GetAdminUserListView(APIView):
"""获取后台用户列表(支持分页、筛选)"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
from jituan.services.admin_context import can_access_admin_user_management
if not can_access_admin_user_management(request.user, permissions):
return Response({'code': 403, 'msg': '您无权访问此页面'})
# 筛选条件
phone = request.data.get('phone', '').strip()
nicheng = request.data.get('nicheng', '').strip()
role_code = request.data.get('roleCode', '').strip()
page = int(request.data.get('page', 1))
page_size = int(request.data.get('pageSize', 20))
# 基础查询:存在客服扩展表
users = User.query.filter(KefuProfile__isnull=False).select_related('KefuProfile')
if phone:
users = users.filter(Phone__icontains=phone)
if nicheng:
users = users.filter(KefuProfile__nicheng__icontains=nicheng)
if role_code:
# 筛选拥有该角色的用户
user_ids_with_role = UserRole.query.filter(role__role_code=role_code).values_list('account_id', flat=True)
users = users.filter(phone__in=user_ids_with_role)
# 排序
users = users.order_by('-CreateTime')
# 分页
paginator = Paginator(users, page_size)
page_obj = paginator.get_page(page)
# 构建返回数据
data_list = []
for user in page_obj:
# 获取用户的所有角色
user_roles = UserRole.query.filter(account_id=user.Phone).select_related('role')
roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles]
data_list.append({
'phone': user.Phone,
'nicheng': user.KefuProfile.nicheng,
'roles': roles_data,
'status': user.KefuProfile.zhuangtai,
'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S'),
'UpdateTime': user.UpdateTime.strftime('%Y-%m-%d %H:%M:%S'),
})
return Response({
'code': 0,
'data': {
'list': data_list,
'total': paginator.count,
'page': page,
'pageSize': page_size
}
})'''
class ModifyAdminUserView(APIView):
"""修改后台用户信息(昵称、密码、二级密码、状态、角色)"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
action = request.data.get('action')
if not username or not action:
return Response({'code': 400, 'msg': '缺少参数'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
from jituan.services.admin_context import (
can_manage_admin_assignments,
can_manage_roles_for_club,
can_access_admin_user_management,
)
club_assign_actions = {'add_club_assignment', 'remove_club_assignment'}
role_manage_actions = {'add_user_roles', 'remove_user_role'}
if action in club_assign_actions:
if not can_manage_admin_assignments(request.user, permissions):
return Response({'code': 403, 'msg': '无权分配俱乐部任职'})
elif action in role_manage_actions:
if not can_access_admin_user_management(request.user, permissions):
return Response({'code': 403, 'msg': '您无权管理角色分配'})
elif not can_manage_admin_assignments(request.user, permissions) and '000001' not in permissions:
return Response({'code': 403, 'msg': '您无权进行此操作'})
target_yonghuid = request.data.get('yonghuid', '').strip()
target_phone = request.data.get('phone', '').strip()
if not target_yonghuid and not target_phone:
return Response({'code': 400, 'msg': '缺少目标用户yonghuid 或 phone'})
from jituan.services.club_rbac import resolve_kefu_admin_user
target_user = resolve_kefu_admin_user(yonghuid=target_yonghuid, phone=target_phone)
if not target_user:
return Response({'code': 404, 'msg': '用户不存在或非后台账号'})
try:
target_kefu = target_user.KefuProfile
except UserKefu.DoesNotExist:
return Response({'code': 404, 'msg': '用户扩展信息缺失'})
with transaction.atomic():
if action == 'update_user_info':
# 修改昵称、状态、密码、二级密码
nicheng = request.data.get('nicheng')
status = request.data.get('status')
password = request.data.get('password')
erjimima = request.data.get('erjimima')
if nicheng is not None:
target_kefu.nicheng = nicheng
if status is not None:
target_kefu.zhuangtai = int(status)
if password:
if len(password) < 6:
return Response({'code': 400, 'msg': '密码至少6位'})
target_user.SetPassword(password)
if erjimima:
if len(erjimima) < 6:
return Response({'code': 400, 'msg': '二级密码至少6位'})
import bcrypt as _bcrypt
target_kefu.erjimima = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
target_user.save()
target_kefu.save()
return Response({'code': 0, 'msg': '修改成功'})
elif action == 'add_user_roles':
role_codes = request.data.get('role_codes', [])
if not role_codes:
return Response({'code': 400, 'msg': '请选择角色'})
from jituan.services.club_rbac import resolve_role_by_name
from jituan.services.club_context import resolve_club_id_from_request
bind_club_id = (request.data.get('target_club_id') or '').strip()
if not bind_club_id:
bind_club_id = resolve_club_id_from_request(request)
if not can_manage_roles_for_club(request.user, permissions, bind_club_id):
return Response({'code': 403, 'msg': '您无权管理该俱乐部的角色分配'})
added, missing = [], []
for rc in role_codes:
role = resolve_role_by_name(rc, bind_club_id)
if not role:
missing.append(rc)
continue
role_club = (getattr(role, 'ClubID', None) or '').strip()
if role_club and role_club != bind_club_id:
missing.append(rc)
continue
_, created = UserRole.objects.get_or_create(
UserUUID=target_user.UserUUID,
RoleUUID=role.RoleUUID,
defaults={
'UserRoleUUID': uuid.uuid4().bytes,
'CompanyUUID': uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
},
)
if created:
added.append(role.RoleName)
elif role.RoleName not in added:
added.append(role.RoleName)
if not added:
return Response({
'code': 400,
'msg': f'未找到可绑定的角色:{", ".join(missing or role_codes)}。请确认顶栏俱乐部与角色所属俱乐部一致',
})
if missing:
return Response({
'code': 0,
'msg': f'已绑定:{", ".join(added)};未找到:{", ".join(missing)}',
})
return Response({'code': 0, 'msg': '角色添加成功'})
elif action == 'remove_user_role':
role_code = request.data.get('role_code')
if not role_code:
return Response({'code': 400, 'msg': '缺少角色编码'})
from jituan.services.club_rbac import resolve_role_by_name
from jituan.services.club_context import resolve_club_id_from_request
bind_club_id = (request.data.get('target_club_id') or '').strip()
if not bind_club_id:
bind_club_id = resolve_club_id_from_request(request)
if not can_manage_roles_for_club(request.user, permissions, bind_club_id):
return Response({'code': 403, 'msg': '您无权管理该俱乐部的角色分配'})
role = resolve_role_by_name(role_code, bind_club_id)
if not role:
return Response({'code': 404, 'msg': '角色不存在或与所选俱乐部不匹配'})
UserRole.objects.filter(UserUUID=target_user.UserUUID, RoleUUID=role.RoleUUID).delete()
return Response({'code': 0, 'msg': '角色移除成功'})
elif action == 'add_club_assignment':
club_id = (request.data.get('club_id') or '').strip()
if not club_id:
return Response({'code': 400, 'msg': '请选择俱乐部'})
from jituan.services.admin_assignments import create_or_reactivate_assignment
from jituan.constants import DATA_SCOPE_SINGLE
role_code = (request.data.get('role_code') or 'CLUB_ADMIN').strip()
is_primary = bool(request.data.get('is_primary', False))
_, err = create_or_reactivate_assignment(
yonghuid=target_user.UserUID,
club_id=club_id,
role_code=role_code,
data_scope=DATA_SCOPE_SINGLE,
is_primary=is_primary,
granted_by=request.user.UserUID,
)
if err:
return Response({'code': 400, 'msg': err})
return Response({'code': 0, 'msg': '俱乐部任职已添加'})
elif action == 'remove_club_assignment':
assignment_id = request.data.get('assignment_id')
if not assignment_id:
return Response({'code': 400, 'msg': '缺少任职 id'})
from jituan.services.admin_assignments import deactivate_assignment
from jituan.models import AdminAssignment
row = AdminAssignment.query.filter(
id=assignment_id, yonghuid=target_user.UserUID, status=1,
).first()
if not row:
return Response({'code': 404, 'msg': '任职记录不存在'})
ok, err = deactivate_assignment(assignment_id)
if not ok:
return Response({'code': 404, 'msg': err or '停用失败'})
return Response({'code': 0, 'msg': '俱乐部任职已移除'})
else:
return Response({'code': 400, 'msg': '无效的action'})
class AddAdminUserView(APIView):
"""添加后台用户(客服账号)"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
from jituan.services.admin_context import (
can_manage_admin_assignments,
can_manage_roles_for_club,
)
from jituan.services.club_context import resolve_club_id_from_request
target_club_id = resolve_club_id_from_request(request)
if not (
can_manage_admin_assignments(request.user, permissions)
or can_manage_roles_for_club(request.user, permissions, target_club_id)
):
return Response({'code': 403, 'msg': '您无权在该俱乐部添加用户'})
phone = request.data.get('phone', '').strip()
password = request.data.get('password', '').strip()
erjimima = request.data.get('erjimima', '').strip()
nicheng = request.data.get('nicheng', '').strip()
role_codes = request.data.get('role_codes', [])
if not all([phone, password, erjimima, nicheng]):
return Response({'code': 400, 'msg': '请填写完整信息'})
if len(password) < 6 or len(erjimima) < 6:
return Response({'code': 400, 'msg': '密码和二级密码至少6位'})
from jituan.services.club_rbac import resolve_roles_for_binding
# 校验手机号是否已被后台客服占用(同俱乐部优先)
existing_kefu = User.query.filter(
Phone=phone, KefuProfile__isnull=False
).order_by('-UserCreateTime').first()
if existing_kefu:
return Response({'code': 400, 'msg': '该账号已存在'})
if not isinstance(role_codes, list):
return Response({'code': 400, 'msg': 'role_codes 格式错误'})
if not role_codes:
return Response({'code': 400, 'msg': '请至少选择一个角色'})
# 仅绑定前端所选角色(各俱乐部角色名不同,勿硬编码「客服」)
role_names_to_bind = []
seen_names = set()
for name in role_codes:
n = (name or '').strip()
if n and n not in seen_names:
seen_names.add(n)
role_names_to_bind.append(n)
roles_to_bind, missing_roles = resolve_roles_for_binding(role_names_to_bind, target_club_id)
if missing_roles:
return Response({
'code': 400,
'msg': f'角色不存在或与当前俱乐部不匹配:{", ".join(missing_roles)}',
})
if not roles_to_bind:
return Response({'code': 400, 'msg': '请至少选择一个有效角色'})
with transaction.atomic():
# 事务内二次校验,防止并发重复创建
if User.query.filter(Phone=phone, KefuProfile__isnull=False).exists():
return Response({'code': 400, 'msg': '该账号已存在'})
# 生成唯一的 yonghuid7位数字
while True:
yonghuid = str(random.randint(1000000, 9999999))
if not User.query.filter(UserUID=yonghuid).exists():
break
# 生成唯一的 openid格式admin_{时间戳}_{随机6位}
timestamp = str(int(time.time() * 1000))
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
openid = f"admin_{timestamp}_{random_suffix}"
while User.query.filter(OpenID=openid).exists():
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
openid = f"admin_{timestamp}_{random_suffix}"
user_main = User.query.create(
UserUID=yonghuid,
UserName=openid,
OpenID=openid,
Phone=phone,
ClubID=target_club_id,
)
user_main.SetPassword(password)
user_main.save(update_fields=['UserPassword', 'ClubID'])
# 创建客服扩展表(后台用户扩展)
import bcrypt as _bcrypt
erjimima_hashed = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
UserKefu.query.create(
user=user_main,
nicheng=nicheng,
erjimima=erjimima_hashed,
zhuangtai=1
)
for role in roles_to_bind:
UserRole.objects.get_or_create(
UserUUID=user_main.UserUUID,
RoleUUID=role.RoleUUID,
defaults={
'UserRoleUUID': uuid.uuid4().bytes,
'CompanyUUID': uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
},
)
from jituan.services.admin_assignments import create_or_reactivate_assignment
from jituan.constants import DATA_SCOPE_SINGLE
create_or_reactivate_assignment(
yonghuid=yonghuid,
club_id=target_club_id,
role_code='CLUB_ADMIN',
data_scope=DATA_SCOPE_SINGLE,
is_primary=True,
granted_by=request.user.UserUID,
)
return Response({'code': 0, 'msg': '添加成功'})

362
backend/views/sections.py Normal file
View File

@@ -0,0 +1,362 @@
import hmac
import threading
import traceback
import uuid
import hashlib
import xmltodict
import time
import logging
import requests
import os
import secrets
import random
import string
from calendar import monthrange
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
# 工具类导入
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.catalog_scope import block_non_group_catalog_scope
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
from ..utils import (
verify_kefu_permission,
PERMISSION_TO_SHENFEN,
SHENFEN_PROFILE_MAP,
has_fadan_view_permission,
has_merchant_order_permission,
check_fadan_permission,
pick_order_id,
pick_penalty_create_params,
write_xiugai_log,
XIUGAI_LEIXING_DASHOU,
XIUGAI_LEIXING_GUANSHI,
XIUGAI_LEIXING_SHANGJIA,
XIUGAI_LEIXING_ZUZHANG,
XIUGAI_LEIXING_LABEL,
)
# models 集中导入
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
## backend
from backend.models import WithdrawalDailyStats
## users
from users.models import (
UserGuanshi, UserBoss, UserZuzhang,
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
)
from users.business_models import User
## orders
from orders.models import (
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
OrderPlayerHistory, Order, RefundRecord,
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
)
## shop
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
## products
from products.models import (
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
)
## config
from config.models import (
WithdrawConfig, ShangjiaLianjie, AccountPermission,
TixianQuotaDefault,
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
)
## rank
from rank.models import (
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
)
# 序列化器
from ..serializers import PopupPageSerializer
# 全局常量、日志对象
logger = logging.getLogger('houtai')
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
class HqbkxxView(APIView):
"""
获取板块配置所需全部数据
POST /houtai/hqbkxx
Body: { "phone": "管理员手机号" }
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 从前端获取传递的账号,用于防越权校验
username_from_frontend = request.data.get('phone', None)
# 2. 调用公共验证方法
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
# 3. 如果第一个返回值为 None说明验证失败直接返回第二个值
if kefu_obj is None:
return permissions
# ---------- 验证通过 ----------
# 4. 检查是否有板块管理权限
if 'bankuai' not in permissions:
return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
scope_err = block_non_group_catalog_scope(request)
if scope_err:
return Response({'code': 400, 'msg': scope_err})
# 5. 查询所有板块
bankuai_list = []
for bk in Bankuai.query.all():
bankuai_list.append({
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'CreateTime': bk.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if bk.CreateTime else ''
})
# 6. 查询所有商品类型
shangpin_leixing_list = []
for sp in ShangpinLeixing.query.all():
shangpin_leixing_list.append({
'id': sp.id,
'jieshao': sp.jieshao,
'tupian_url': sp.tupian_url or '',
'bankuai_id': sp.bankuai_id
})
# 7. 查询会员(附带当前俱乐部售价)
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL
from jituan.models import ClubHuiyuanPrice
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
scope = resolve_club_scope(request)
club_id = resolve_club_id_from_request(request)
price_map = {}
if scope != DATA_SCOPE_ALL:
for row in ClubHuiyuanPrice.query.filter(club_id=club_id):
price_map[row.huiyuan_id] = row
huiyuan_list = []
# 在受限 scope 下预过滤,避免 Huiyuan 全表遍历
# 注:当 scope != DATA_SCOPE_ALL 且 club_id != CLUB_ID_DEFAULT 时,
# 只有 price_map 中存在记录的会员才可能被保留(无记录的会被跳过)
huiyuan_qs = Huiyuan.query.all()
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT:
huiyuan_qs = huiyuan_qs.filter(huiyuan_id__in=list(price_map.keys()))
# 仅查询所需字段减少内存占用dict 比 model 实例更轻)
huiyuan_qs = huiyuan_qs.values('huiyuan_id', 'jieshao', 'bankuai_id', 'jiage', 'guanshifc', 'zuzhangfc')
for hy in huiyuan_qs:
row = price_map.get(hy['huiyuan_id'])
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT and row is None:
continue
if row is not None and not row.is_enabled:
continue
item = {
'huiyuan_id': hy['huiyuan_id'],
'jieshao': hy['jieshao'],
'bankuai_id': hy['bankuai_id'],
'jiage': str(row.jiage) if row else str(hy['jiage']),
'guanshifc': str(row.guanshifc) if row else str(hy['guanshifc']),
'zuzhangfc': str(row.zuzhangfc) if row else str(hy['zuzhangfc']),
}
huiyuan_list.append(item)
return Response({
'code': 0,
'msg': 'ok',
'data': {
'bankuai': bankuai_list,
'shangpin_leixing': shangpin_leixing_list,
'huiyuan': huiyuan_list,
'club_id': club_id,
'scope': scope,
}
})
class BkxgView(APIView):
"""
板块增删改统一接口
POST /houtai/bkxg
Body: {
"phone": "管理员手机号",
"action": "create | update | delete | add_items | remove_items",
"bankuai_id": int,
"bankuai_name": str,
"item_type": "shangpin_leixing | huiyuan",
"item_ids": [1,2,3]
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 从前端获取传递的账号
username_from_frontend = request.data.get('phone', None)
# 2. 调用公共验证方法
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
# 3. 验证失败直接返回
if kefu_obj is None:
return permissions
# 4. 检查板块管理权限
if 'bankuai' not in permissions:
return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
scope_err = block_non_group_catalog_scope(request)
if scope_err:
return Response({'code': 400, 'msg': scope_err})
# 5. 获取操作类型
action = request.data.get('action')
if not action:
return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
# ==================== 创建板块 ====================
if action == 'create':
bankuai_name = request.data.get('bankuai_name', '').strip()
if not bankuai_name:
return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
if Bankuai.query.filter(mingcheng=bankuai_name).exists():
return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
shangpin_ids = request.data.get('shangpin_ids', [])
huiyuan_ids = request.data.get('huiyuan_ids', [])
with transaction.atomic():
new_bk = Bankuai.query.create(mingcheng=bankuai_name)
if shangpin_ids:
ShangpinLeixing.query.filter(
id__in=shangpin_ids,
bankuai__isnull=True
).update(bankuai=new_bk)
if huiyuan_ids:
Huiyuan.query.filter(
huiyuan_id__in=huiyuan_ids,
bankuai__isnull=True
).update(bankuai=new_bk)
return Response({
'code': 0,
'msg': '创建成功',
'data': {'bankuai_id': new_bk.bankuai_id}
})
# ==================== 以下操作需要 bankuai_id ====================
bankuai_id = request.data.get('bankuai_id')
if not bankuai_id:
return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400)
try:
bk = Bankuai.query.get(bankuai_id=bankuai_id)
except Bankuai.DoesNotExist:
return Response({'code': 1, 'msg': '板块不存在'}, status=404)
# ==================== 修改板块名称 ====================
if action == 'update':
new_name = request.data.get('bankuai_name', '').strip()
if not new_name:
return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
if Bankuai.query.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists():
return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
bk.mingcheng = new_name
bk.save()
return Response({'code': 0, 'msg': '修改成功'})
# ==================== 删除板块 ====================
elif action == 'delete':
with transaction.atomic():
ShangpinLeixing.query.filter(bankuai=bk).update(bankuai=None)
Huiyuan.query.filter(bankuai=bk).update(bankuai=None)
bk.delete()
return Response({'code': 0, 'msg': '删除成功'})
# ==================== 添加绑定 ====================
elif action == 'add_items':
item_type = request.data.get('item_type')
item_ids = request.data.get('item_ids', [])
if not item_type or not item_ids:
return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
if item_type == 'shangpin_leixing':
ShangpinLeixing.query.filter(
id__in=item_ids,
bankuai__isnull=True
).update(bankuai=bk)
elif item_type == 'huiyuan':
Huiyuan.query.filter(
huiyuan_id__in=item_ids,
bankuai__isnull=True
).update(bankuai=bk)
else:
return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
return Response({'code': 0, 'msg': '添加成功'})
# ==================== 移除绑定 ====================
elif action == 'remove_items':
item_type = request.data.get('item_type')
item_ids = request.data.get('item_ids', [])
if not item_type or not item_ids:
return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
if item_type == 'shangpin_leixing':
ShangpinLeixing.query.filter(
id__in=item_ids,
bankuai=bk
).update(bankuai=None)
elif item_type == 'huiyuan':
Huiyuan.query.filter(
huiyuan_id__in=item_ids,
bankuai=bk
).update(bankuai=None)
else:
return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
return Response({'code': 0, 'msg': '移除成功'})
else:
return Response({'code': 1, 'msg': '未知操作'}, status=400)

973
backend/views/shops.py Normal file
View File

@@ -0,0 +1,973 @@
import hmac
import threading
import traceback
import uuid
import hashlib
import xmltodict
import time
import logging
import requests
import os
import secrets
import random
import string
from calendar import monthrange
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
# 工具类导入
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.catalog_scope import block_non_group_catalog_scope
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
from ..utils import (
verify_kefu_permission,
has_perm_code,
PERMISSION_TO_SHENFEN,
SHENFEN_PROFILE_MAP,
has_fadan_view_permission,
has_merchant_order_permission,
check_fadan_permission,
pick_order_id,
pick_penalty_create_params,
write_xiugai_log,
XIUGAI_LEIXING_DASHOU,
XIUGAI_LEIXING_GUANSHI,
XIUGAI_LEIXING_SHANGJIA,
XIUGAI_LEIXING_ZUZHANG,
XIUGAI_LEIXING_LABEL,
)
# models 集中导入
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
## backend
from backend.models import WithdrawalDailyStats
## users
from users.models import (
UserGuanshi, UserBoss, UserZuzhang,
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
)
from users.business_models import User
## orders
from orders.models import (
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
OrderPlayerHistory, Order, RefundRecord,
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
)
## shop
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
## products
from products.models import (
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
)
## config
from config.models import (
WithdrawConfig, ShangjiaLianjie, AccountPermission,
TixianQuotaDefault,
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
)
## rank
from rank.models import (
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
)
# 序列化器
from ..serializers import PopupPageSerializer
# 全局常量、日志对象
logger = logging.getLogger('houtai')
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
SHOP_MANAGE_PERMS = ('999a', '999b', '999c', '999d', '999e')
class ShopListView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 调用公共权限验证(与您给的示例完全一致)
username_frontend = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username_frontend)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
# 2. 检查是否拥有所需权限999a~999e 任意一个)
if not has_perm_code(permissions, *SHOP_MANAGE_PERMS):
return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'})
# 3. 解析请求参数
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip()
zhuangtai = request.data.get('zhuangtai')
lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip()
user_id = request.data.get('user_id', '').strip()
ketixian_yue_min = request.data.get('ketixian_yue_min')
ketixian_yue_max = request.data.get('ketixian_yue_max')
zhengqu_zonge_min = request.data.get('zhengqu_zonge_min')
zhengqu_zonge_max = request.data.get('zhengqu_zonge_max')
# 4. 构建查询集(先查店铺,预加载关联凭证)
queryset = Dianpu.query.select_related('pingzheng').all()
if dianpu_mingcheng:
queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng)
if zhuangtai is not None:
queryset = queryset.filter(zhuangtai=zhuangtai)
if lianxi_dianhua:
queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua)
if user_id:
queryset = queryset.filter(pingzheng__yonghu=user_id)
# 金额范围过滤
if ketixian_yue_min is not None:
queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min)
if ketixian_yue_max is not None:
queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max)
if zhengqu_zonge_min is not None:
queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min)
if zhengqu_zonge_max is not None:
queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max)
# 5. 分页
paginator = Paginator(queryset, page_size)
page_obj = paginator.get_page(page)
# 6. 构造返回数据
shop_list = []
for dianpu in page_obj:
pingzheng = dianpu.pingzheng
shop_list.append({
'id': dianpu.id,
'dianpu_mingcheng': dianpu.dianpu_mingcheng,
'dianpu_touxiang': dianpu.dianpu_touxiang or '',
'lianxi_dianhua': dianpu.lianxi_dianhua or '',
'weixinhao': dianpu.weixinhao or '',
'zhuangtai': dianpu.zhuangtai,
'bangding_yonghushu': dianpu.bangding_yonghushu,
'erweima_url': dianpu.erweima_url or '',
'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng,
'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0,
'ketixian_yue': dianpu.ketixian_yue,
'zhengqu_zonge': dianpu.zhengqu_zonge,
'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0,
'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane,
'pingzheng__yonghu': pingzheng.yonghu, # 用户ID
'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号
})
# 获取默认配置ID=1不存在则创建
default_config, _ = DianpuMorenPeizhi.query.get_or_create(id=1, defaults={
'moren_choucheng_feilv': 0.1,
'moren_tixian_xiane': 1000.00
})
return Response({
'code': 0,
'msg': 'success',
'data': {
'list': shop_list,
'total': paginator.count,
'default_config': {
'moren_choucheng_feilv': default_config.moren_choucheng_feilv,
'moren_tixian_xiane': default_config.moren_tixian_xiane,
}
}
})
class ShopModifyView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 身份验证
username_frontend = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username_frontend)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
action = request.data.get('action', '')
# 2. 根据 action 分派任务
if action == 'update_dianpu':
return self._update_dianpu(request, permissions)
elif action == 'add_dianpu':
return self._add_dianpu(request, permissions)
elif action == 'update_default':
return self._update_default(request, permissions)
else:
return Response({'code': 400, 'msg': '未知操作类型'})
def _update_dianpu(self, request, permissions):
"""修改店铺信息,精确权限控制"""
dianpu_id = request.data.get('dianpu_id')
if not dianpu_id:
return Response({'code': 400, 'msg': '缺少店铺ID'})
try:
with transaction.atomic():
dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id)
# 基础信息修改 (999e)
if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']):
if not has_perm_code(permissions, '999e'):
return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'})
if 'dianpu_mingcheng' in request.data:
dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng']
if 'lianxi_dianhua' in request.data:
dianpu.lianxi_dianhua = request.data['lianxi_dianhua']
if 'weixinhao' in request.data:
dianpu.weixinhao = request.data['weixinhao']
# 店铺状态 (999a)
if 'zhuangtai' in request.data:
if not has_perm_code(permissions, '999a'):
return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'})
new_status = int(request.data['zhuangtai'])
if new_status not in (0, 1):
return Response({'code': 400, 'msg': '店铺状态无效'})
dianpu.zhuangtai = new_status
# 可提现余额 (999b)
if 'ketixian_yue' in request.data:
if not has_perm_code(permissions, '999b'):
return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'})
dianpu.ketixian_yue = request.data['ketixian_yue']
# 提现限额相关 (999c)
if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data:
if not has_perm_code(permissions, '999c'):
return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'})
if 'kaiqi_meiri_xiane' in request.data:
dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane']
if 'meiri_tixian_xiane' in request.data:
dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane']
# 抽成相关 (999d)
if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data:
if not has_perm_code(permissions, '999d'):
return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'})
if 'kaiqi_shouyi_choucheng' in request.data:
dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng']
if 'shouyi_choucheng_feilv' in request.data:
dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv']
dianpu.save() # Django 会自动判断更新哪些字段
return Response({'code': 0, 'msg': '店铺信息更新成功'})
except Dianpu.DoesNotExist:
return Response({'code': 404, 'msg': '店铺不存在'})
except Exception as e:
logger.exception("修改店铺失败")
return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
def _add_dianpu(self, request, permissions):
"""添加店铺,需要权限 999b"""
if not has_perm_code(permissions, '999b'):
return Response({'code': 403, 'msg': '无权限添加店铺(999b)'})
yonghu_id = request.data.get('yonghu_id')
zhanghao = request.data.get('zhanghao')
mima = request.data.get('mima')
dianpu_mingcheng = request.data.get('dianpu_mingcheng')
if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]):
return Response({'code': 400, 'msg': '必填字段缺失用户ID、账号、密码、店铺名称'})
# 检查用户是否存在
if not User.query.filter(UserUID=yonghu_id).exists():
return Response({'code': 404, 'msg': '用户不存在'})
# 检查凭证是否已存在
if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists():
return Response({'code': 400, 'msg': '登录账号已存在'})
if YonghuPingzheng.query.filter(yonghu=yonghu_id).exists():
return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'})
try:
with transaction.atomic():
pingzheng = YonghuPingzheng.query.create(
yonghu=yonghu_id,
zhanghao=zhanghao,
mima=mima,
is_active=True
)
dianpu = Dianpu.query.create(
pingzheng=pingzheng,
dianpu_mingcheng=dianpu_mingcheng,
lianxi_dianhua=request.data.get('lianxi_dianhua', ''),
weixinhao=request.data.get('weixinhao', ''),
kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False),
shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0),
kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False),
meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0),
)
return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id})
except IntegrityError as e:
logger.error("创建店铺违反唯一性约束: %s", e)
return Response({'code': 400, 'msg': '数据冲突请检查账号或用户ID'})
except Exception as e:
logger.exception("创建店铺失败")
return Response({'code': 500, 'msg': '创建失败,请稍后重试'})
def _update_default(self, request, permissions):
"""修改默认配置,按修改内容校验权限"""
feilv = request.data.get('moren_choucheng_feilv')
xiane = request.data.get('moren_tixian_xiane')
modify_commission = feilv is not None
modify_withdraw = xiane is not None
if modify_commission and not has_perm_code(permissions, '999d'):
return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'})
if modify_withdraw and not has_perm_code(permissions, '999c'):
return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'})
try:
config, _ = DianpuMorenPeizhi.query.get_or_create(id=1)
if modify_commission:
config.moren_choucheng_feilv = feilv
if modify_withdraw:
config.moren_tixian_xiane = xiane
config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) +
(['moren_tixian_xiane'] if modify_withdraw else []))
return Response({'code': 0, 'msg': '默认配置更新成功'})
except Exception as e:
logger.exception("更新默认配置失败")
return Response({'code': 500, 'msg': '更新失败'})
# ==================== 1. 公共商品类型 + 审核模式 ====================
class ShopPublicTypeAndAuditView(APIView):
"""
返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关
前端请求路径:/houtai/htdphqsplx
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# ---------- 身份与权限校验 ----------
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
# 拥有 1199ab/1199abc/1199abd 任一个即可访问
if not has_perm_code(permissions, *SHOP_PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'})
# ---------- 查询公共商品类型(仅正常审核状态的) ----------
try:
# shenhezhuangtai = 1 表示正常 (根据模型默认值)
types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values(
'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin'
)
type_list = []
for t in types_qs:
type_list.append({
'id': t['id'],
'jieshao': t['jieshao'] or '',
'tupian_url': t['tupian_url'] or '',
'yaoqiuleixing': t['yaoqiuleixing'],
'huiyuan_id': t['huiyuan_id'],
'yongjin': t['yongjin'],
})
except Exception as e:
logger.exception("查询公共商品类型失败")
return Response({'code': 500, 'msg': '服务器错误,请稍后重试'})
# ---------- 获取审核模式配置 ----------
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False
return Response({
'code': 0,
'data': {
'public_types': type_list,
'kaiqi_shenhe': kaiqi_shenhe
}
})
# ==================== 2. 店铺列表(支持搜索、筛选、分页) ====================
class ShopListForProductView(APIView):
"""
返回店铺列表,用于商品管理页面选择店铺。
支持关键词搜索店铺ID/名称/用户ID、店铺状态筛选、分页。
前端请求路径:/houtai/htdphqdpys
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
if not has_perm_code(permissions, *SHOP_PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无权访问店铺列表'})
# ---------- 安全解析分页参数 ----------
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '分页参数格式错误'})
# 搜索关键词字符串ORM 自动防注入)
keyword = str(request.data.get('keyword', '')).strip()
# 店铺状态筛选1正常0封禁不传则全部
zhuangtai = request.data.get('zhuangtai')
# ---------- 构建查询集关联凭证表获取用户ID ----------
shops = Dianpu.query.select_related('pingzheng').all()
# 状态筛选:只接受 0 或 1
if zhuangtai is not None:
try:
zhuangtai = int(zhuangtai)
if zhuangtai in [0, 1]:
shops = shops.filter(zhuangtai=zhuangtai)
else:
return Response({'code': 400, 'msg': '状态参数只能为0或1'})
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '状态参数必须为整数'})
# 关键词搜索店铺ID/名称/用户ID
if keyword:
shops = shops.filter(
models.Q(id__icontains=keyword) |
models.Q(dianpu_mingcheng__icontains=keyword) |
models.Q(pingzheng__yonghu__icontains=keyword)
)
# ---------- 分页 ----------
paginator = Paginator(shops, page_size)
page_obj = paginator.get_page(page)
shop_list = []
for shop in page_obj:
shop_list.append({
'id': shop.id,
'dianpu_mingcheng': shop.dianpu_mingcheng,
'zhuangtai': shop.zhuangtai,
'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '',
'lianxi_dianhua': shop.lianxi_dianhua or '',
'weixinhao': shop.weixinhao or '',
})
return Response({
'code': 0,
'data': {
'list': shop_list,
'total': paginator.count
}
})
# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ====================
class ShopProductTypeMappingView(APIView):
"""
返回店铺商品类型映射ShangpinLeixingDianpu列表。
支持店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。
每次请求同时返回“待审核总数”shenhe_zhuangtai=False 的记录数)。
前端请求路径:/houtai/htdphqyssplx
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
if not has_perm_code(permissions, *SHOP_PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无权访问商品类型映射'})
# ---------- 安全解析参数 ----------
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '分页参数格式错误'})
keyword = str(request.data.get('keyword', '')).strip()
shop_id = request.data.get('shop_id')
public_type_id = request.data.get('public_type_id')
# 布尔型筛选字段处理None 表示不筛选,否则转为布尔值
def parse_bool(val):
if val is None:
return None
return bool(val)
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
# ---------- 查询(连表店铺和公共类型) ----------
mappings = ShangpinLeixingDianpu.query.select_related('dianpu', 'gonggong_leixing').all()
# 筛选条件:均为参数化,完全防注入
if shop_id is not None:
mappings = mappings.filter(dianpu_id=int(shop_id))
if public_type_id is not None:
mappings = mappings.filter(gonggong_leixing_id=int(public_type_id))
if shangjia is not None:
mappings = mappings.filter(shangjia_zhuangtai=shangjia)
if fengjin is not None:
mappings = mappings.filter(fengjin_zhuangtai=fengjin)
if shenhe is not None:
mappings = mappings.filter(shenhe_zhuangtai=shenhe)
# 关键字搜索介绍、ID、店铺名、公共类型名
if keyword:
mappings = mappings.filter(
models.Q(jieshao__icontains=keyword) |
models.Q(id__icontains=keyword) |
models.Q(dianpu__dianpu_mingcheng__icontains=keyword) |
models.Q(gonggong_leixing__jieshao__icontains=keyword)
)
# ---------- 待审核总数(不受分页影响) ----------
pending_total = mappings.filter(shenhe_zhuangtai=False).count()
# ---------- 分页 ----------
paginator = Paginator(mappings, page_size)
page_obj = paginator.get_page(page)
mapping_list = []
for m in page_obj:
mapping_list.append({
'id': m.id,
'dianpu_id': m.dianpu_id,
'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '',
'gonggong_leixing_id': m.gonggong_leixing_id,
'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '',
'jieshao': m.jieshao,
'tupian_url': m.tupian_url or '',
'paixu': m.paixu,
'shangjia_zhuangtai': m.shangjia_zhuangtai,
'fengjin_zhuangtai': m.fengjin_zhuangtai,
'shenhe_zhuangtai': m.shenhe_zhuangtai,
})
return Response({
'code': 0,
'data': {
'list': mapping_list,
'total': paginator.count,
'pending_total': pending_total
}
})
# ==================== 统一修改接口(已修正事务) ====================
class ShopProductModifyView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
user_perms = permissions
action = request.data.get('action')
if action == 'update_audit_mode':
return self._update_audit_mode(request, user_perms)
elif action == 'update_mapping_status':
return self._update_mapping_status(request, user_perms)
else:
return Response({'code': 400, 'msg': '未知操作'})
@transaction.atomic # ✅ 事务修复点
def _update_audit_mode(self, request, user_perms):
if not has_perm_code(permissions, '1199ab'):
return Response({'code': 403, 'msg': '无权限修改审核模式'})
kaiqi = request.data.get('kaiqi_shenhe')
if kaiqi is None:
return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'})
try:
config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1)
config.kaiqi_shenhe = bool(kaiqi)
config.save(update_fields=['kaiqi_shenhe'])
return Response({'code': 0, 'msg': '审核模式已更新'})
except Exception as e:
logger.exception("更新审核模式失败")
return Response({'code': 500, 'msg': '服务器错误'})
@transaction.atomic
def _update_mapping_status(self, request, user_perms):
mapping_id = request.data.get('mapping_id')
if not mapping_id:
return Response({'code': 400, 'msg': '缺少 mapping_id'})
try:
mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id))
except (ShangpinLeixingDianpu.DoesNotExist, ValueError):
return Response({'code': 404, 'msg': '映射不存在'})
fengjin = request.data.get('fengjin_zhuangtai')
shangjia = request.data.get('shangjia_zhuangtai')
shenhe = request.data.get('shenhe_zhuangtai')
# 权限判断
if fengjin is not None:
if fengjin:
if not has_perm_code(user_perms, '1199abc'):
return Response({'code': 403, 'msg': '无权限封禁'})
else:
if not has_perm_code(user_perms, '1199abd'):
return Response({'code': 403, 'msg': '无权限解封'})
mapping.fengjin_zhuangtai = bool(fengjin)
if shangjia is not None:
if not shangjia:
if not has_perm_code(user_perms, '1199abc'):
return Response({'code': 403, 'msg': '无权限下架'})
else:
if not has_perm_code(user_perms, '1199abd'):
return Response({'code': 403, 'msg': '无权限上架'})
mapping.shangjia_zhuangtai = bool(shangjia)
if shenhe is not None:
if shenhe:
if not has_perm_code(user_perms, '1199abd'):
return Response({'code': 403, 'msg': '无权限审核通过'})
mapping.shenhe_zhuangtai = bool(shenhe)
mapping.save()
return Response({'code': 0, 'msg': '修改成功'})
# 商品管理相关权限组(拥有任意一个即可查看列表)
PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe']
class ProductListView(APIView):
"""
商品列表接口,支持多条件筛选与分页。
请求路径: /houtai/hqhtdpsplx
权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# ----- 身份与权限验证 -----
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
if not has_perm_code(permissions, *PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无商品管理权限'})
# ----- 安全解析分页与筛选参数 -----
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 15))
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '分页参数格式错误'})
keyword = str(request.data.get('keyword', '')).strip()
shop_id = request.data.get('shop_id') # 店铺ID (可选)
shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选)
public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选)
# 布尔型状态筛选
def parse_bool(val):
if val is None:
return None
return bool(val)
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
# ----- 构建查询(预加载店铺和店铺商品类型) -----
products = Shangpin.query.select_related('dianpu', 'dianpu_leixing').all()
# 按店铺筛选
if shop_id is not None:
try:
products = products.filter(dianpu_id=int(shop_id))
except (ValueError, TypeError):
return Response({'code': 400, 'msg': 'shop_id 格式错误'})
# 按店铺商品类型映射筛选
if shop_product_type_id is not None:
try:
products = products.filter(dianpu_leixing_id=int(shop_product_type_id))
except (ValueError, TypeError):
return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'})
# 按公共商品类型筛选leixing_id 存储的是公共类型的ID
if public_type_id is not None:
try:
products = products.filter(leixing_id=int(public_type_id))
except (ValueError, TypeError):
return Response({'code': 400, 'msg': 'public_type_id 格式错误'})
# 平台审核状态筛选 (BooleanField)
if shenhe is not None:
products = products.filter(shenhe_zhuangtai=shenhe)
# 上架状态筛选
if shangjia is not None:
products = products.filter(shangjia_zhuangtai=shangjia)
# 封禁状态筛选
if fengjin is not None:
products = products.filter(fengjin_zhuangtai=fengjin)
# 关键词搜索商品ID或商品标题搜索具有模糊性使用icontains
if keyword:
# 尝试纯数字可能是商品ID使用精确匹配与标题模糊搜索
products = products.filter(
models.Q(id__icontains=keyword) |
models.Q(biaoqian__icontains=keyword)
)
# ----- 获取待审核总数(平台审核未通过的数量) -----
pending_total = products.filter(shenhe_zhuangtai=False).count()
# ----- 分页 -----
paginator = Paginator(products, page_size)
page_obj = paginator.get_page(page)
# ----- 构造返回数据批量获取公共类型名称避免N+1 -----
# 提取所有公共商品类型ID
type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None}
public_type_map = {}
if type_ids:
public_types = ShangpinLeixing.query.filter(id__in=type_ids).values('id', 'jieshao')
public_type_map = {t['id']: t['jieshao'] for t in public_types}
product_list = []
for p in page_obj:
product_list.append({
'id': p.id,
'biaoqian': p.biaoqian or '',
'jiage': p.jiage,
'kucun': p.kucun,
'leixing_id': p.leixing_id,
'public_type_name': public_type_map.get(p.leixing_id, ''),
'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
'duiwai_xiaoliang': p.duiwai_xiaoliang,
'jieshao': p.jieshao or '',
'xiadan_xuzhi': p.xiadan_xuzhi or '',
'guize_tupian': p.guize_tupian or '',
'yaoqiuleixing': p.yaoqiuleixing,
'huiyuan_id': p.huiyuan_id or '',
'yongjin': p.yongjin,
'kaioi_ewai_PlayerCommission': p.kaioi_ewai_dashou_fencheng,
'ewai_PlayerCommission': p.ewai_dashou_fencheng,
'CreateTime': p.CreateTime.isoformat() if p.CreateTime else '',
'UpdateTime': p.UpdateTime.isoformat() if p.UpdateTime else '',
'paixu': p.paixu,
'dianpu_id': p.dianpu_id,
'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '',
'dianpu_leixing_id': p.dianpu_leixing_id,
'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '',
'shangjia_zhuangtai': p.shangjia_zhuangtai,
'fengjin_zhuangtai': p.fengjin_zhuangtai,
'shenhe_zhuangtai': p.shenhe_zhuangtai,
})
return Response({
'code': 0,
'data': {
'list': product_list,
'total': paginator.count,
'pending_total': pending_total, # 待审核商品总数
}
})
class ProductModifyView(APIView):
"""
商品修改接口,根据修改内容细粒度鉴权。
请求路径: /houtai/htxgdpspsj
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
action = request.data.get('action', '')
if action != 'update_product':
return Response({'code': 400, 'msg': '未知操作'})
return self._update_product(request, permissions)
@transaction.atomic
def _update_product(self, request, permissions):
"""执行商品修改,严格校验权限"""
product_id = request.data.get('product_id')
if not product_id:
return Response({'code': 400, 'msg': '缺少商品ID'})
try:
# 行级锁防止并发修改
product = Shangpin.objects.select_for_update().get(id=int(product_id))
except (Shangpin.DoesNotExist, ValueError):
return Response({'code': 404, 'msg': '商品不存在'})
# 需要修改的字段
fields_to_update = {}
# 将要修改的状态标志用于后续权限判断
status_changes = {
'shangjia': request.data.get('shangjia_zhuangtai'),
'fengjin': request.data.get('fengjin_zhuangtai'),
'shenhe': request.data.get('shenhe_zhuangtai'),
}
# ---------- 权限分类检查 ----------
# 1. 封禁/下架操作 (消极操作) -> 1199abc
if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']):
if not has_perm_code(permissions, '1199abc'):
return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'})
if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True:
if not has_perm_code(permissions, '1199abc'):
return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'})
# 2. 解封/上架/过审 (积极操作) -> 1199abd
if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']):
if not has_perm_code(permissions, '1199abd'):
return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'})
if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False:
if not has_perm_code(permissions, '1199abd'):
return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'})
# 3. 修改其他数据(非状态字段)-> 1199abe
# 除了上架、封禁、审核之外的字段都属于“其他数据”
other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username',
'shangjia_zhuangtai', 'fengjin_zhuangtai',
'shenhe_zhuangtai']]
if other_fields and not has_perm_code(permissions, '1199abe'):
return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'})
# ---------- 应用修改 ----------
# 允许修改的字段白名单(防止注入)
# 前端参数名 → 模型字段名映射
field_name_map = {
'kaioi_ewai_PlayerCommission': 'kaioi_ewai_dashou_fencheng',
'ewai_PlayerCommission': 'ewai_dashou_fencheng',
}
allowed_fields = [
'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang',
'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin',
'kaioi_ewai_PlayerCommission', 'ewai_PlayerCommission',
'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai'
]
update_fields = []
for field in allowed_fields:
value = request.data.get(field)
if value is not None:
model_field = field_name_map.get(field, field)
# 简单类型校验(根据字段类型)
if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_PlayerCommission', 'paixu']:
try:
value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value)
except (ValueError, TypeError):
return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'})
setattr(product, model_field, value)
update_fields.append(model_field)
if update_fields:
product.save(update_fields=update_fields)
return Response({'code': 0, 'msg': '商品修改成功'})
class ShopCopyCatalogView(APIView):
"""
复制店铺商品目录(类型 + 专区 + 商品,含 OSS 图片独立拷贝)。
请求路径: /houtai/htfzdpspsj
权限: 店铺管理权限 999a~999e 任一
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
if not has_perm_code(permissions, *SHOP_MANAGE_PERMS):
return Response({'code': 403, 'msg': '您没有权限执行店铺商品复制操作'})
source_user_uid = str(request.data.get('source_user_uid', '')).strip()
target_user_uid = str(request.data.get('target_user_uid', '')).strip()
copy_mode = str(request.data.get('copy_mode', 'append')).strip().lower()
if not source_user_uid or not target_user_uid:
return Response({'code': 400, 'msg': '请填写源店铺与目标店铺的用户ID'})
from backend.services.shop_copy_service import copy_shop_catalog, ShopCopyError
try:
result = copy_shop_catalog(source_user_uid, target_user_uid, copy_mode)
return Response({
'code': 0,
'msg': (
f'复制成功:商品类型 {result["copied_types"]} 个,'
f'专区 {result["copied_zones"]} 个,商品 {result["copied_products"]}'
),
'data': result,
})
except ShopCopyError as e:
return Response({'code': e.code, 'msg': e.message})
except Exception:
logger.exception('复制店铺商品失败')
return Response({'code': 500, 'msg': '复制失败,请稍后重试'})

1166
backend/views/system.py Normal file

File diff suppressed because it is too large Load Diff

350
backend/views/titles.py Normal file
View File

@@ -0,0 +1,350 @@
import hmac
import threading
import traceback
import uuid
import hashlib
import xmltodict
import time
import logging
import requests
import os
import secrets
import random
import string
from calendar import monthrange
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef, Prefetch
from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
# 工具类导入
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.catalog_scope import block_non_group_catalog_scope
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
from ..utils import (
verify_kefu_permission,
PERMISSION_TO_SHENFEN,
SHENFEN_PROFILE_MAP,
has_fadan_view_permission,
has_merchant_order_permission,
check_fadan_permission,
pick_order_id,
pick_penalty_create_params,
write_xiugai_log,
XIUGAI_LEIXING_DASHOU,
XIUGAI_LEIXING_GUANSHI,
XIUGAI_LEIXING_SHANGJIA,
XIUGAI_LEIXING_ZUZHANG,
XIUGAI_LEIXING_LABEL,
)
# models 集中导入
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
## backend
from backend.models import WithdrawalDailyStats
## users
from users.models import (
UserGuanshi, UserBoss, UserZuzhang,
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
)
from users.business_models import User
## orders
from orders.models import (
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
OrderPlayerHistory, Order, RefundRecord,
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
)
## shop
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
## products
from products.models import (
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
)
## config
from config.models import (
WithdrawConfig, ShangjiaLianjie, AccountPermission,
TixianQuotaDefault,
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
)
## rank
from rank.models import (
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
)
# 序列化器
from ..serializers import PopupPageSerializer
# 全局常量、日志对象
logger = logging.getLogger('houtai')
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
class KhpzhqView(APIView):
"""
获取所有考核配置数据(称号列表、板块、费用)
POST /houtai/khpzhq
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'kaohepeizhi' not in permissions:
return Response({'code': 403, 'msg': '无考核配置权限'}, status=403)
bankuai_list = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
chenghao_qs = Chenghao.query.prefetch_related(
Prefetch('kaohecishufeiyong_set', queryset=KaoheCishuFeiyong.objects.order_by('cishu'))
).all()
chenghao_list = []
for ch in chenghao_qs:
# 获取费用:按次数排序,转为 {cishu: feiyong} 字典
# prefetch_related 已用 Prefetch 排序,直接遍历不会绕过缓存
feiyong_dict = {
item.cishu: float(item.feiyong)
for item in ch.kaohecishufeiyong_set.all()
}
max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0
chenghao_list.append({
'id': ch.id,
'mingcheng': ch.mingcheng,
'leixing': ch.leixing,
'leixing_name': ch.get_leixing_display(),
'bankuai_id': ch.bankuai_id,
'tubiao_url': ch.tubiao_url or '',
'texiao_miaoshu': ch.texiao_miaoshu or '',
'CreateTime': ch.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if ch.CreateTime else '',
'kaohe_guize': ch.kaohe_guize or '',
'kaioi_jinpai': ch.kaioi_jinpai,
'feiyong': feiyong_dict, # {1:100.0, 2:200.0}
'max_cishu': max_cishu
})
return Response({
'code': 0,
'msg': 'ok',
'data': {
'bankuai': bankuai_list,
'chenghao': chenghao_list
}
})
except Exception as e:
logger.exception("KhpzhqView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
class ChzsgcView(APIView):
"""
称号(标签)增删改查统一接口
POST /houtai/chzsgc
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username_from_frontend = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
if kefu_obj is None:
return permissions
if 'kaohepeizhi' not in permissions:
return Response({'code': 403, 'msg': '无考核配置权限'}, status=403)
action = request.data.get('action')
if not action:
return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
# ========== 创建称号 ==========
if action == 'create':
mingcheng = request.data.get('mingcheng', '').strip()
leixing = request.data.get('leixing', 'dashou')
bankuai_id = request.data.get('bankuai_id')
texiao_miaoshu = request.data.get('texiao_miaoshu', '')
kaohe_guize = request.data.get('kaohe_guize', '')
kaioi_jinpai = request.data.get('kaioi_jinpai', False)
feiyong_list = request.data.get('feiyong_list', [])
if not mingcheng:
return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
if Chenghao.query.filter(mingcheng=mingcheng).exists():
return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
with transaction.atomic():
ch = Chenghao.query.create(
mingcheng=mingcheng,
leixing=leixing,
bankuai_id=bankuai_id,
texiao_miaoshu=texiao_miaoshu,
kaohe_guize=kaohe_guize,
kaioi_jinpai=kaioi_jinpai
)
# 处理费用:前端传来 [{cishu:1, feiyong:100}, ...]
if feiyong_list:
for item in feiyong_list:
cishu = int(item.get('cishu', 1))
feiyong = float(item.get('feiyong', 0))
KaoheCishuFeiyong.query.update_or_create(
chenghao=ch, cishu=cishu,
defaults={'feiyong': feiyong}
)
else:
# 默认添加第一次免费
KaoheCishuFeiyong.query.create(chenghao=ch, cishu=1, feiyong=0)
return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}})
# ========== 修改称号 ==========
elif action == 'update':
ch_id = request.data.get('chenghao_id')
if not ch_id:
return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
try:
ch = Chenghao.query.get(id=ch_id)
except Chenghao.DoesNotExist:
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
mingcheng = request.data.get('mingcheng', ch.mingcheng).strip()
leixing = request.data.get('leixing', ch.leixing)
bankuai_id = request.data.get('bankuai_id', ch.bankuai_id)
texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu)
kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize)
kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai)
if not mingcheng:
return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
if Chenghao.query.filter(mingcheng=mingcheng).exclude(id=ch_id).exists():
return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
ch.mingcheng = mingcheng
ch.leixing = leixing
ch.bankuai_id = bankuai_id
ch.texiao_miaoshu = texiao_miaoshu
ch.kaohe_guize = kaohe_guize
ch.kaioi_jinpai = kaioi_jinpai
ch.save()
# 如果前端传递了 feiyong_list则全量替换费用
feiyong_list = request.data.get('feiyong_list')
if feiyong_list is not None:
with transaction.atomic():
ch.kaohecishufeiyong_set.all().delete()
for item in feiyong_list:
KaoheCishuFeiyong.query.create(
chenghao=ch,
cishu=int(item.get('cishu', 1)),
feiyong=float(item.get('feiyong', 0))
)
return Response({'code': 0, 'msg': '修改成功'})
# ========== 删除称号 ==========
elif action == 'delete':
ch_id = request.data.get('chenghao_id')
if not ch_id:
return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
try:
ch = Chenghao.query.get(id=ch_id)
except Chenghao.DoesNotExist:
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
ch.delete()
return Response({'code': 0, 'msg': '删除成功'})
# ========== 添加/修改某次费用 ==========
elif action == 'add_feiyong':
ch_id = request.data.get('chenghao_id')
cishu = request.data.get('cishu')
feiyong = request.data.get('feiyong', 0)
if not ch_id or not cishu:
return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
try:
ch = Chenghao.query.get(id=ch_id)
except Chenghao.DoesNotExist:
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
if int(cishu) < 1:
return Response({'code': 1, 'msg': '次数必须>=1'}, status=400)
KaoheCishuFeiyong.query.update_or_create(
chenghao=ch, cishu=cishu,
defaults={'feiyong': feiyong}
)
return Response({'code': 0, 'msg': '添加/修改费用成功'})
# ========== 删除某次费用 ==========
elif action == 'remove_feiyong':
ch_id = request.data.get('chenghao_id')
cishu = request.data.get('cishu')
if not ch_id or not cishu:
return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
if int(cishu) == 1:
return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400)
try:
ch = Chenghao.query.get(id=ch_id)
except Chenghao.DoesNotExist:
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
KaoheCishuFeiyong.query.filter(chenghao=ch, cishu=cishu).delete()
return Response({'code': 0, 'msg': '删除成功'})
# ========== 修改某次费用等价于add ==========
elif action == 'update_feiyong':
return self.add_feiyong(request) # 复用逻辑
else:
return Response({'code': 1, 'msg': '未知操作'}, status=400)
except Exception as e:
logger.exception("ChzsgcView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
# ==================== 后端 views.py ====================
# 请确保已有以下导入,如果没有请在文件头部添加

View File

@@ -0,0 +1,170 @@
import hmac
import threading
import traceback
import uuid
import hashlib
import xmltodict
import time
import logging
import requests
import os
import secrets
import random
import string
from calendar import monthrange
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
# 工具类导入
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.catalog_scope import block_non_group_catalog_scope
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
from ..utils import (
verify_kefu_permission,
PERMISSION_TO_SHENFEN,
SHENFEN_PROFILE_MAP,
has_fadan_view_permission,
has_merchant_order_permission,
check_fadan_permission,
pick_order_id,
pick_penalty_create_params,
write_xiugai_log,
XIUGAI_LEIXING_DASHOU,
XIUGAI_LEIXING_GUANSHI,
XIUGAI_LEIXING_SHANGJIA,
XIUGAI_LEIXING_ZUZHANG,
XIUGAI_LEIXING_LABEL,
)
# models 集中导入
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
## backend
from backend.models import WithdrawalDailyStats
## users
from users.models import (
UserGuanshi, UserBoss, UserZuzhang,
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
)
from users.business_models import User
## orders
from orders.models import (
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
OrderPlayerHistory, Order, RefundRecord,
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
)
## shop
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
## products
from products.models import (
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
)
## config
from config.models import (
WithdrawConfig, ShangjiaLianjie, AccountPermission,
TixianQuotaDefault,
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
)
## rank
from rank.models import (
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
)
# 序列化器
from ..serializers import PopupPageSerializer
# 全局常量、日志对象
logger = logging.getLogger('houtai')
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
class GetWithdrawSettingsView(APIView):
"""
获取提现设置(手续费利率、每日限额、当日已提现总额)
权限:需要 5500a 或 5500b 或 5500c 任一
POST /houtai/hthqtxpz
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 需要5500a/b/c任一
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': '无权限查看提现设置'})
from jituan.services.withdraw_config import build_withdraw_settings_payload
return Response({'code': 0, 'data': build_withdraw_settings_payload(request)})
class UpdateWithdrawSettingsView(APIView):
"""
修改提现设置(权限细分):
- 修改打手相关利率、个人限额、总限额需要5500a
- 修改管事相关需要5500b
- 修改组长相关需要5500c
注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算),
后端不直接修改 WithdrawalDailyStats该表由提现流程自动累加。
POST /houtai/htxgtxsz
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 权限映射:要修改的角色需要对应的权限
# 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3
from jituan.services.withdraw_config import apply_withdraw_settings_update
return apply_withdraw_settings_update(request, permissions)

835
backend/views/zuzhang.py Normal file
View File

@@ -0,0 +1,835 @@
import hmac
import threading
import traceback
import uuid
import hashlib
import xmltodict
import time
import logging
import requests
import os
import secrets
import random
import string
from calendar import monthrange
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
# 工具类导入
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.catalog_scope import block_non_group_catalog_scope
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
from ..utils import (
verify_kefu_permission,
PERMISSION_TO_SHENFEN,
SHENFEN_PROFILE_MAP,
has_fadan_view_permission,
has_merchant_order_permission,
check_fadan_permission,
pick_order_id,
pick_penalty_create_params,
write_xiugai_log,
XIUGAI_LEIXING_DASHOU,
XIUGAI_LEIXING_GUANSHI,
XIUGAI_LEIXING_SHANGJIA,
XIUGAI_LEIXING_ZUZHANG,
XIUGAI_LEIXING_LABEL,
)
# models 集中导入
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
## backend
from backend.models import WithdrawalDailyStats
## users
from users.models import (
UserGuanshi, UserBoss, UserZuzhang,
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
)
from users.business_models import User
## orders
from orders.models import (
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
OrderPlayerHistory, Order, RefundRecord,
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
)
## shop
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
## products
from products.models import (
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
)
## config
from config.models import (
WithdrawConfig, ShangjiaLianjie, AccountPermission,
TixianQuotaDefault,
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
)
## rank
from rank.models import (
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
)
# 序列化器
from ..serializers import PopupPageSerializer
# 全局常量、日志对象
logger = logging.getLogger('houtai')
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
class GetZuzhangListView(APIView):
"""
组长列表接口(分页 + 多条件筛选)
路由POST /houtai/hthqzzlb
权限:需要登录,且拥有 6600a~6600e 任一权限
"""
permission_classes = [IsAuthenticated]
@transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处)
def post(self, request):
# 1. 获取前端传递的 username用于防越权
frontend_username = request.data.get('username', '')
# 2. 调用公共校验方法,验证身份并获取权限列表
result = verify_kefu_permission(request, username_from_frontend=frontend_username)
# 重要verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response)
# 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response
if isinstance(result, tuple) and len(result) == 2:
user_obj, second = result
if isinstance(second, Response):
return second # 校验失败,直接返回错误响应
else:
permissions = second # 正常情况permissions 是列表
else:
# 防止意外返回格式,视为失败
return Response({'code': 500, 'msg': '权限校验模块异常'})
# 3. 检查是否拥有组长管理所需权限6600a ~ 6600e 任一)
required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e']
if not any(perm in permissions for perm in required_perms):
logger.warning(f"用户 {request.user.Phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}")
return Response({'code': 403, 'msg': '您无权访问组长管理页面'})
# 4. 提取请求参数(分页 + 筛选)
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
except ValueError:
page, page_size = 1, 20
keyword = request.data.get('keyword', '').strip()
status = request.data.get('status') # 1=正常, 0=禁用
invite_count_op = request.data.get('invite_count_op') # gte / lte
invite_count_value = request.data.get('invite_count_value')
amount_op = request.data.get('amount_op')
amount_value = request.data.get('amount_value')
commission_op = request.data.get('commission_op')
commission_value = request.data.get('commission_value')
# 5. 构建基础 QuerySet预加载关联表
queryset = filter_user_related_by_club(
UserZuzhang.query.select_related('user', 'user__BossProfile').all(),
request,
)
# 6. 应用筛选条件(使用 ORM 安全过滤)
if keyword:
queryset = queryset.filter(
Q(user__UserUID__icontains=keyword) |
Q(user__BossProfile__nickname__icontains=keyword)
)
if status is not None and status != '':
try:
queryset = queryset.filter(zhuangtai=int(status))
except ValueError:
pass
# 邀请总数筛选
if invite_count_value not in (None, ''):
try:
val = float(invite_count_value)
op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte'
queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val})
except (ValueError, TypeError):
pass
# 可提现金额筛选
if amount_value not in (None, ''):
try:
val = float(amount_value)
op = amount_op if amount_op in ('gte', 'lte') else 'gte'
queryset = queryset.filter(**{f'ketixian_jine__{op}': val})
except (ValueError, TypeError):
pass
# 分佣总额筛选
if commission_value not in (None, ''):
try:
val = float(commission_value)
op = commission_op if commission_op in ('gte', 'lte') else 'gte'
queryset = queryset.filter(**{f'fenyong_zonge__{op}': val})
except (ValueError, TypeError):
pass
# 7. 总数统计
total = queryset.count()
# 8. 分页
offset = (page - 1) * page_size
paged_queryset = queryset[offset:offset + page_size]
# 9. 构造返回数据
data_list = []
for zu in paged_queryset:
user = zu.user
nickname = ''
if hasattr(user, 'BossProfile') and user.BossProfile:
nickname = user.BossProfile.nickname or ''
data_list.append({
'yonghuid': user.UserUID,
'avatar': user.Avatar or '',
'nickname': nickname,
'fenyong_zonge': float(zu.fenyong_zonge),
'ketixian_jine': float(zu.ketixian_jine),
'yaoqing_zongshu': zu.yaoqing_zongshu,
'zhuangtai': zu.zhuangtai,
})
return Response({
'code': 0,
'msg': 'success',
**list_response_meta(request),
'data': {
'list': data_list,
'total': total,
}
})
class GetZuzhangDetailView(APIView):
"""
获取组长详情
权限6600a,6600b,6600c,6600d,6600e 任一
POST /houtai/hthqzzxq
参数username, yonghuid
返回:
code:0, data: {
base: {...},
balance: {...},
permanent_enabled, permanent_amount,
custom_first_dividend: {},
extra_dividend_map: {},
all_members: []
}
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
yonghuid = request.data.get('yonghuid', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not yonghuid:
return Response({'code': 400, 'msg': '缺少用户ID'})
# 权限校验(需要任一权限)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
required_perms = {'6600a', '6600b', '6600c', '6600d', '6600e'}
if not any(p in permissions for p in required_perms):
return Response({'code': 403, 'msg': '您没有权限查看组长详情'})
# 查询组长主表、扩展表、老板扩展表(昵称)
try:
user = User.query.select_related('ZuzhangProfile', 'BossProfile').get(UserUID=yonghuid)
zuzhang = user.ZuzhangProfile
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
except User.DoesNotExist:
return Response({'code': 404, 'msg': '组长不存在'})
except UserZuzhang.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是组长'})
denied = forbid_if_user_out_of_scope(request, user)
if denied:
return denied
# 基础信息
base_data = {
'yonghuid': user.UserUID,
'nickname': boss.nickname if boss else '',
'avatar': user.Avatar or '',
'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信但前端可能需要可从User关联实际上组长表无电话字段这里留空或从其他表获取
'wechat': '',
'zhuangtai': zuzhang.zhuangtai,
'yaoqingma': zuzhang.yaoqingma,
'yaoqingren': '', # 组长表没有邀请人字段
'haibao_url': zuzhang.haibao_url or '',
'yaoqing_zongshu': zuzhang.yaoqing_zongshu,
'jinri_fenyong': str(zuzhang.jinri_fenyong),
'jinyue_fenyong': str(zuzhang.jinyue_fenyong),
'fenyong_zonge': str(zuzhang.fenyong_zonge),
'CreateTime': zuzhang.CreateTime.isoformat() if zuzhang.CreateTime else None,
'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
}
# 余额提现相关
balance_data = {
'ketixian_jine': str(zuzhang.ketixian_jine),
'jinri_tixian': str(zuzhang.jinri_tixian),
'last_tixian_time': zuzhang.last_tixian_time.isoformat() if zuzhang.last_tixian_time else None,
'kaioi_ewai_tixian': zuzhang.kaioi_ewai_tixian,
'ewai_tixian_xiane': str(zuzhang.ewai_tixian_xiane),
}
# 永久分红(额外分红开关和金额)
permanent_enabled = zuzhang.kaioi_ewai_fenhong
permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0'
# 获取所有会员(用于前端选择)
all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc'))
for m in all_members:
m['jiage'] = str(m['jiage'])
m['zuzhangfc'] = str(m['zuzhangfc'])
# 获取多次分红配置(按次数和会员组织,按俱乐部隔离)
from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
duoci_records = DuociFenhong.query.filter(
yonghuid=yonghuid, club_id=zuzhang_club,
).order_by('cishu')
custom_first = {} # 首次分红定制cishu=1
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } }
# 批量预取会员信息
_huiyuan_ids = [r.huiyuan for r in duoci_records]
_huiyuan_map = {
h.huiyuan_id: h
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao', 'jiage')
}
for record in duoci_records:
# 获取会员信息
member = _huiyuan_map.get(record.huiyuan)
if not member:
continue
member_info = {
'jieshao': member.jieshao,
'jiage': str(member.jiage),
}
if record.cishu == 1:
custom_first[record.huiyuan] = float(record.zuzhang_fenhong)
else:
times = record.cishu
if times not in extra_map:
extra_map[times] = {}
extra_map[times][record.huiyuan] = {
'zuzhang_fenhong': float(record.zuzhang_fenhong),
**member_info
}
return Response({
'code': 0,
'data': {
'base': base_data,
'balance': balance_data,
'permanent_enabled': permanent_enabled,
'permanent_amount': permanent_amount,
'custom_first_dividend': custom_first,
'extra_dividend_map': extra_map,
'all_members': all_members,
}
})
class UpdateZuzhangView(APIView):
"""
修改组长信息(基础信息、余额、提现限额、分红策略)
权限细分:
- 增加可提现金额6600a
- 减少可提现金额6600b
- 修改状态(封禁/解封6600c
- 修改提现限额kaioi_ewai_tixian/ewai_tixian_xiane6600d
- 修改分红相关永久分红、定制首次、额外次数6600e
POST /houtai/htxgzzxx
参数:
username, yonghuid,
nickname, avatar, dianhua, wechat, zhuangtai, # 基础
ketixian_jine, kaioi_ewai_tixian, ewai_tixian_xiane, # 余额提现
permanent_enabled, permanent_amount, # 永久分红
custom_first_dividend: {}, # 首次定制分红 {会员ID: 金额}
extra_dividend_map: {} # 额外次数分红 {次数: {会员ID: {zuzhang_fenhong}}}
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
yonghuid = request.data.get('yonghuid', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not yonghuid:
return Response({'code': 400, 'msg': '缺少用户ID'})
# 权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 查询组长对象
try:
user = User.query.get(UserUID=yonghuid)
zuzhang = user.ZuzhangProfile
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
except User.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
except UserZuzhang.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是组长'})
# 获取原始数据(用于对比金额变化)
old_ketixian_jine = zuzhang.ketixian_jine
# 开始事务
with transaction.atomic():
operator = kefu.user.Phone
old_nickname = boss.nickname if boss else ''
old_zhuangtai = zuzhang.zhuangtai
# ---------- 1. 基础信息修改需要6600c按需求基础信息修改通常用6600c但为了安全暂不校验如有需要可加----------
nickname = request.data.get('nickname', '').strip()
avatar = request.data.get('avatar', '').strip()
dianhua = request.data.get('dianhua', '').strip()
wechat = request.data.get('wechat', '').strip()
zhuangtai = request.data.get('zhuangtai')
base_changes = []
# 状态修改需要6600c
if zhuangtai is not None:
if '6600c' not in permissions:
return Response({'code': 403, 'msg': '无权限修改组长状态需要6600c'})
try:
zt = int(zhuangtai)
if zt in (0, 1):
zuzhang.zhuangtai = zt
if zt != old_zhuangtai:
zt_map = {0: '禁用', 1: '正常'}
base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}{zt_map.get(zt, zt)}')
except:
pass
if nickname:
if not boss:
boss = UserBoss.query.create(user=user, nickname=nickname)
else:
boss.nickname = nickname
boss.save()
if nickname != old_nickname:
base_changes.append(f'昵称:{old_nickname or ""}{nickname}')
if avatar:
user.Avatar = avatar
user.save()
base_changes.append('更新了头像')
if base_changes:
zuzhang.save()
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_ZUZHANG,
qitashuoming=(
f'【后台】修改组长基础信息:{"".join(base_changes)}'
f'组长ID={yonghuid},操作人={operator}'
),
)
# 组长表没有电话/微信字段如需存储可暂存到User或忽略
# 这里不处理
# ---------- 2. 可提现金额修改6600a6600b----------
new_ketixian_jine = request.data.get('ketixian_jine')
if new_ketixian_jine is not None:
try:
new_val = Decimal(str(new_ketixian_jine))
if new_val < 0:
return Response({'code': 400, 'msg': '可提现金额不能为负数'})
except:
return Response({'code': 400, 'msg': '金额格式错误'})
if new_val > old_ketixian_jine:
if '6600a' not in permissions:
return Response({'code': 403, 'msg': '无权限增加可提现金额需要6600a'})
from jituan.services.group_finance_gate import deny_group_finance_exec
deny = deny_group_finance_exec(request.user, request)
if deny:
return deny
action_desc = f'增加{new_val - old_ketixian_jine}'
elif new_val < old_ketixian_jine:
if '6600b' not in permissions:
return Response({'code': 403, 'msg': '无权限减少可提现金额需要6600b'})
action_desc = f'减少{old_ketixian_jine - new_val}'
else:
action_desc = '无变动'
if new_val != old_ketixian_jine:
zuzhang.ketixian_jine = new_val
zuzhang.save(update_fields=['ketixian_jine'])
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_ZUZHANG,
xiugaitijiao=new_val,
xiugaitijiaoq=old_ketixian_jine,
qitashuoming=(
f'【后台】修改组长可提现金额:{old_ketixian_jine}元 → {new_val}元({action_desc}'
f'组长ID={yonghuid},操作人={operator}'
),
)
# ---------- 3. 提现限额修改需要6600d----------
kaioi_ewai = request.data.get('kaioi_ewai_tixian')
ewai_val = request.data.get('ewai_tixian_xiane')
if kaioi_ewai is not None or ewai_val is not None:
if '6600d' not in permissions:
return Response({'code': 403, 'msg': '无权限修改提现限额需要6600d'})
limit_changes = []
old_kaioi = zuzhang.kaioi_ewai_tixian
old_ewai = zuzhang.ewai_tixian_xiane
if kaioi_ewai is not None:
zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai)
if bool(kaioi_ewai) != old_kaioi:
limit_changes.append(f'额外限额开关:{"" if old_kaioi else ""}{"" if kaioi_ewai else ""}')
if ewai_val is not None:
try:
zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val))
if zuzhang.ewai_tixian_xiane != old_ewai:
limit_changes.append(f'额外限额金额:{old_ewai}{zuzhang.ewai_tixian_xiane}')
except:
return Response({'code': 400, 'msg': '额外限额金额格式错误'})
zuzhang.save()
if limit_changes:
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_ZUZHANG,
qitashuoming=(
f'【后台】修改组长提现限额:{"".join(limit_changes)}'
f'组长ID={yonghuid},操作人={operator}'
),
)
# ---------- 4. 分红相关修改需要6600e----------
perm_enabled = request.data.get('permanent_enabled')
perm_amount = request.data.get('permanent_amount')
custom_first = request.data.get('custom_first_dividend')
extra_map = request.data.get('extra_dividend_map')
if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
if '6600e' not in permissions:
return Response({'code': 403, 'msg': '无权限修改分红策略需要6600e'})
# 4.1 永久分红(额外分红开关和金额)
if perm_enabled is not None:
zuzhang.kaioi_ewai_fenhong = bool(perm_enabled)
if perm_amount is not None:
try:
zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount))
except:
return Response({'code': 400, 'msg': '永久分红金额格式错误'})
zuzhang.save()
# 4.2 首次定制分红cishu=1
if custom_first is not None and isinstance(custom_first, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
# 批量预取当前所有首次定制记录(避免循环内 N+1
existing_first_list = list(
DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, cishu=1,
)
)
_existing_map = {r.huiyuan: r for r in existing_first_list}
_to_create = []
_to_update = []
_to_delete_ids = []
for huiyuan_id, amount in custom_first.items():
try:
amount = Decimal(str(amount))
except:
continue
if amount <= 0:
# 删除定制(恢复默认)
rec = _existing_map.get(huiyuan_id)
if rec is not None:
_to_delete_ids.append(rec.id)
else:
rec = _existing_map.get(huiyuan_id)
if rec is not None:
rec.zuzhang_fenhong = amount
rec.guanshi_fenhong = Decimal('0') # 组长表只关心组长分红
_to_update.append(rec)
else:
_to_create.append(DuociFenhong(
club_id=zuzhang_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=1,
zuzhang_fenhong=amount,
guanshi_fenhong=Decimal('0'),
))
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
for rec in existing_first_list:
if rec.huiyuan not in custom_first and rec.id not in _to_delete_ids:
_to_delete_ids.append(rec.id)
if _to_create:
DuociFenhong.objects.bulk_create(_to_create)
if _to_update:
DuociFenhong.objects.bulk_update(_to_update, ['zuzhang_fenhong', 'guanshi_fenhong'])
if _to_delete_ids:
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
# 4.3 额外次数分红cishu >= 2
if extra_map is not None and isinstance(extra_map, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
keep_set = set()
_extra_targets = [] # [(cishu, huiyuan_id, amount)]
for times_str, members in extra_map.items():
try:
cishu = int(times_str)
if cishu < 2:
continue
except:
continue
if not isinstance(members, dict):
continue
for huiyuan_id, data in members.items():
if not isinstance(data, dict):
continue
amount = data.get('zuzhang_fenhong')
if amount is None:
continue
try:
amount = Decimal(str(amount))
except:
continue
keep_set.add((cishu, huiyuan_id))
_extra_targets.append((cishu, huiyuan_id, amount))
# 批量预取已有额外次数记录
existing_extra_list = list(
DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2,
)
)
_extra_map = {(r.cishu, r.huiyuan): r for r in existing_extra_list}
_to_create = []
_to_update = []
_to_delete_ids = []
for cishu, huiyuan_id, amount in _extra_targets:
rec = _extra_map.get((cishu, huiyuan_id))
if rec is not None:
rec.zuzhang_fenhong = amount
rec.guanshi_fenhong = Decimal('0')
_to_update.append(rec)
else:
_to_create.append(DuociFenhong(
club_id=zuzhang_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=cishu,
zuzhang_fenhong=amount,
guanshi_fenhong=Decimal('0'),
))
# 删除不在 keep_set 中的额外次数记录
for rec in existing_extra_list:
if (rec.cishu, rec.huiyuan) not in keep_set:
_to_delete_ids.append(rec.id)
if _to_create:
DuociFenhong.objects.bulk_create(_to_create)
if _to_update:
DuociFenhong.objects.bulk_update(_to_update, ['zuzhang_fenhong', 'guanshi_fenhong'])
if _to_delete_ids:
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
dividend_parts = []
if perm_enabled is not None:
dividend_parts.append(f'永久分红开关→{"" if perm_enabled else ""}')
if perm_amount is not None:
dividend_parts.append(f'永久分红金额→{perm_amount}')
if custom_first is not None:
dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)')
if extra_map is not None:
dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)')
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_ZUZHANG,
qitashuoming=(
f'【后台】修改组长分红策略:{"".join(dividend_parts) or "配置已更新"}'
f'组长ID={yonghuid},操作人={operator}'
),
)
# 保存组长其他可能修改的字段(例如电话、微信没有,忽略)
return Response({'code': 0, 'msg': '修改成功'})
class ChangeInviterView(APIView):
"""
后台更换邀请人/上级
POST /houtai/htghyqr
参数:
username: 操作人账号
yonghuid: 被修改用户ID
target_type: dashou打手| guanshi管事
new_yaoqingren: 新邀请人用户ID须在 User 主表存在)
权限:
dashou → 001ee
guanshi → 4400a
校验:
打手新上级须为管事;管事新上级须为组长;不可设为自己
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
yonghuid = request.data.get('yonghuid', '').strip()
target_type = request.data.get('target_type', '').strip().lower()
new_yaoqingren = request.data.get('new_yaoqingren', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not yonghuid:
return Response({'code': 400, 'msg': '缺少用户ID'})
if target_type not in ('dashou', 'guanshi'):
return Response({'code': 400, 'msg': 'target_type 须为 dashou 或 guanshi'})
if not new_yaoqingren:
return Response({'code': 400, 'msg': '缺少新邀请人ID'})
if new_yaoqingren == yonghuid:
return Response({'code': 400, 'msg': '不能将自己设为邀请人'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if target_type == 'dashou':
if '001ee' not in permissions:
return Response({'code': 403, 'msg': '无权限更换打手邀请人需要001ee'})
else:
if '4400a' not in permissions:
return Response({'code': 403, 'msg': '无权限更换管事邀请人需要4400a'})
try:
target_user = User.query.get(UserUID=yonghuid)
except User.DoesNotExist:
return Response({'code': 404, 'msg': '被修改用户不存在'})
try:
inviter_user = User.query.get(UserUID=new_yaoqingren)
except User.DoesNotExist:
return Response({'code': 404, 'msg': '新邀请人用户ID不存在'})
operator = kefu.user.Phone
with transaction.atomic():
if target_type == 'dashou':
try:
dashou = target_user.DashouProfile
except UserDashou.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是打手'})
try:
guanshi_profile = inviter_user.GuanshiProfile
except UserGuanshi.DoesNotExist:
return Response({'code': 400, 'msg': '新邀请人不是管事,无法作为打手上级'})
if guanshi_profile.zhuangtai != 1:
return Response({'code': 400, 'msg': '新邀请人管事账号已禁用'})
old_inviter = dashou.yaoqingren or ''
dashou.yaoqingren = new_yaoqingren
dashou.save(update_fields=['yaoqingren'])
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_DASHOU,
qitashuoming=(
f'【后台】更换打手邀请人:{old_inviter}{new_yaoqingren}'
f'打手ID={yonghuid},新上级须为管事(已校验),操作人={operator}'
),
)
else:
try:
guanshi = target_user.GuanshiProfile
except UserGuanshi.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是管事'})
try:
zuzhang_profile = inviter_user.ZuzhangProfile
except UserZuzhang.DoesNotExist:
return Response({'code': 400, 'msg': '新邀请人不是组长,无法作为管事上级'})
if zuzhang_profile.zhuangtai != 1:
return Response({'code': 400, 'msg': '新邀请人组长账号已禁用'})
old_inviter = guanshi.yaoqingren or ''
guanshi.yaoqingren = new_yaoqingren
guanshi.save(update_fields=['yaoqingren'])
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_GUANSHI,
qitashuoming=(
f'【后台】更换管事邀请人:{old_inviter}{new_yaoqingren}'
f'管事ID={yonghuid},新上级须为组长(已校验),操作人={operator}'
),
)
return Response({
'code': 0,
'msg': '邀请人更换成功',
'data': {'yonghuid': yonghuid, 'new_yaoqingren': new_yaoqingren},
})

347
backend/views/zxkf.py Normal file
View File

@@ -0,0 +1,347 @@
import hmac
import threading
import traceback
import uuid
import hashlib
import xmltodict
import time
import logging
import requests
import os
import secrets
import random
import string
from calendar import monthrange
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
# 工具类导入
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.catalog_scope import block_non_group_catalog_scope
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
from ..utils import (
verify_kefu_permission,
PERMISSION_TO_SHENFEN,
SHENFEN_PROFILE_MAP,
has_fadan_view_permission,
has_merchant_order_permission,
check_fadan_permission,
pick_order_id,
pick_penalty_create_params,
write_xiugai_log,
XIUGAI_LEIXING_DASHOU,
XIUGAI_LEIXING_GUANSHI,
XIUGAI_LEIXING_SHANGJIA,
XIUGAI_LEIXING_ZUZHANG,
XIUGAI_LEIXING_LABEL,
)
# models 集中导入
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
## backend
from backend.models import WithdrawalDailyStats
## users
from users.models import (
UserGuanshi, UserBoss, UserZuzhang,
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
)
from users.business_models import User
## orders
from orders.models import (
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
OrderPlayerHistory, Order, RefundRecord,
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
)
## shop
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
## products
from products.models import (
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
)
## config
from config.models import (
WithdrawConfig, ShangjiaLianjie, AccountPermission,
TixianQuotaDefault,
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
)
## rank
from rank.models import (
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
)
# 序列化器
from ..serializers import PopupPageSerializer
# 全局常量、日志对象
logger = logging.getLogger('houtai')
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
class ZxkfghdsView(APIView):
"""
客服更换打手接口
路径POST /houtai/zxkfghds
功能:
1. 将当前订单状态改为已退款5释放接单打手若有
2. 如需跨平台通知,则调用对方平台退款通知接口
3. 基于原订单信息生成全新订单(状态直接为 1已替老板填写
4. 生成新的派单链接,并标记为已使用
5. 新链接记录上一个链接ID和上一个订单ID
6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款
权限002ac商家订单管理/ 002ab平台订单管理/ 003aa跨平台管理
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# ========== 1. 参数提取 ==========
username = request.data.get('username', '').strip() or request.data.get('phone', '').strip()
OrderID = pick_order_id(request.data)
if not username or not OrderID:
logger.warning("更换打手参数不完整")
return Response({'code': 400, 'msg': '参数不完整'})
# ========== 2. 权限校验(商家链接页客服需 002ac非仅跨平台 003aa ==========
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions # 返回 error response
if not has_merchant_order_permission(permissions):
return Response({'code': 403, 'msg': '您无权处理订单'})
current_user = request.user
# ========== 3. 查询原订单及关联信息 ==========
try:
old_order = Order.query.select_related('shangjia_kuozhan').get(OrderID=OrderID)
except Order.DoesNotExist:
logger.warning(f"订单不存在: {OrderID}")
return Response({'code': 404, 'msg': '订单不存在'})
# 仅允许状态为 2进行中或 8结算中更换打手
if old_order.Status not in [1,7,2, 8]:
return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'})
# 本接口仅处理商家发单Platform=2
if old_order.Platform != 2:
return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'})
# 获取原链接用于模板ID等
try:
old_lianjie = ShangjiaLianjie.query.get(OrderID=OrderID)
except ShangjiaLianjie.DoesNotExist:
logger.error(f"原链接记录不存在,订单: {OrderID}")
return Response({'code': 500, 'msg': '链接数据异常'})
# 商家信息(仅用于记录关联,不操作余额)
try:
shangjia_ext = old_order.shangjia_kuozhan
shangjia_id = shangjia_ext.MerchantID
# 获取商家昵称用于新订单扩展表
shangjia_user = User.query.select_related('ShopProfile').get(UserUID=shangjia_id)
shangjia_nicheng = shangjia_user.ShopProfile.nicheng or f"商家{shangjia_id}"
except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
logger.error(f"商家信息缺失,订单: {OrderID}")
return Response({'code': 500, 'msg': '商家数据异常'})
# ========== 4. 退款处理(仅打手侧,不碰商家) ==========
with transaction.atomic():
# 5.1 订单状态改为已退款
old_order.Status = 5
old_order.AssignedCS = current_user.Phone
old_order.save()
# 5.2 处理接单打手(若有)
dashou_id = old_order.PlayerID
if dashou_id:
try:
dashou_user = User.query.get(UserUID=dashou_id)
dashou_profile = dashou_user.DashouProfile
dashou_profile.tuikuanliang += 1 # 退款次数+1
dashou_profile.zhuangtai = 1 # 设为空闲
dashou_profile.save()
except (User.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"打手 {dashou_id} 不存在,跳过更新")
# 5.3 更新退款记录表
RefundRecord.query.update_or_create(
OrderID=OrderID,
defaults={
'PlayerID': dashou_id or '',
'ApplicantID': '',
'ProcessorID': current_user.Phone,
'Reason': '客服更换打手退款',
'ApplyStatus': 1,
'Amount': old_order.Amount or 0,
'PlayerCommission': old_order.PlayerCommission or 0,
'Description': old_order.Description or '',
'Remark': '更换打手自动退款',
'Nickname': old_order.Nickname or '',
}
)
# 5.4 打手每日统计(退款)
if dashou_id:
try:
update_dashou_daily_by_action(
yonghuid=dashou_id,
amount=old_order.PlayerCommission,
action=3 # 3=退款
)
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# 5.5 客服处理统计(管理员无 KefuProfile跳过
if hasattr(kefu, 'jinrichuli'):
kefu.jinrichuli += 1
kefu.jinyuechuli += 1
kefu.zongchuli += 1
kefu.save()
# 至此,原订单已退款,事务提交
# ========== 6. 生成新订单和链接(不扣商家余额) ==========
try:
with transaction.atomic():
# ---- 6.1 生成新的订单ID和Token ----
def generate_OrderID():
timestamp = int(time.time() * 1000)
process_id = os.getpid() % 1000
random_num = random.randint(1000, 9999)
return f"SJ{timestamp}{process_id:03d}{random_num}"
def generate_secure_token(dd_id, uid):
timestamp = int(time.time() * 1_000_000)
salt = secrets.token_hex(8)
raw = f"{timestamp}:{dd_id}:{uid}:{salt}"
token = hashlib.sha256(raw.encode()).hexdigest()[:24]
if ShangjiaLianjie.query.filter(LinkToken=token).exists():
return generate_secure_token(dd_id, uid)
return token
def generate_link_url(token):
base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/')
return f"{base}/order/{token}"
new_OrderID = generate_OrderID()
secure_token = generate_secure_token(new_OrderID, shangjia_id)
# ---- 6.2 创建新订单复制核心数据状态直接为1 ----
new_order = Order.query.create(
OrderID=new_OrderID,
Status=1, # 下单中
Platform=2,
Amount=old_order.Amount,
PlayerCommission=old_order.PlayerCommission,
ProductTypeID=old_order.ProductTypeID,
GrabRequirement=old_order.GrabRequirement,
MembershipID=old_order.MembershipID,
CommissionReq=old_order.CommissionReq,
Description=old_order.Description,
Nickname=old_order.Nickname, # 继承游戏昵称
Remark=old_order.Remark,
User1ID=old_order.User1ID, # 商家标识
ImageURL=old_order.ImageURL,
ExternalOrderID=old_order.ExternalOrderID,
ClubID=getattr(old_order, 'ClubID', None) or 'xq',
# 注意:不继承 AssignedID、PlayerID
)
# ---- 6.3 创建商家扩展表 ----
MerchantOrderExt.query.create(
Order=new_order,
MerchantID=shangjia_id,
MerchantNickname=shangjia_nicheng,
)
# ---- 6.4 生成新链接并关联旧链接信息 ----
link_url = generate_link_url(secure_token)
new_lianjie = ShangjiaLianjie.query.create(
UserID=shangjia_id,
ProductTypeID=old_lianjie.ProductTypeID,
TemplateID=old_lianjie.TemplateID,
LinkURL=link_url,
LinkToken=secure_token,
OrderID=new_OrderID,
is_used=True, # 直接标记已使用
ExpireTime=timezone.now() + timezone.timedelta(days=1),
PreviousLinkID=old_lianjie.id, # 记录旧链接ID
PreviousOrderID=old_order.OrderID, # 记录旧订单ID
)
# ---- 6.5 异步通知(广播新订单、跨平台同步) ----
try:
order_info = {
'OrderID': new_OrderID,
'game_type': self._get_game_type_name(new_order.ProductTypeID),
'amount': str(new_order.Amount),
'order_desc': (new_order.Description or '')[:50],
}
dingdan_guangbo.delay(order_info)
except Exception as e:
logger.error(f"广播通知失败: {e}")
# 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单
logger.info(f"更换打手成功:旧订单{OrderID}→退款,新订单{new_OrderID}已生成")
return Response({
'code': 0,
'msg': '更换成功',
'data': {
'new_OrderID': new_OrderID,
'new_dingdan_id': new_OrderID,
'new_lianjie': link_url,
}
})
except Exception as e:
logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '生成新订单失败'})
def _get_game_type_name(self, ProductTypeID):
"""根据类型ID获取游戏类型名称"""
try:
gt = ShangpinLeixing.query.filter(id=ProductTypeID).first()
return gt.jieshao if gt else '游戏订单'
except Exception:
return '游戏订单'

View File

@@ -1 +1,16 @@
from django.contrib import admin
from .models import MiniappScriptScene, MiniappScriptAutoReply
@admin.register(MiniappScriptScene)
class MiniappScriptSceneAdmin(admin.ModelAdmin):
list_display = ('club_id', 'scene_key', 'title', 'enabled', 'UpdateTime')
list_filter = ('club_id', 'enabled')
search_fields = ('scene_key', 'title', 'content')
@admin.register(MiniappScriptAutoReply)
class MiniappScriptAutoReplyAdmin(admin.ModelAdmin):
list_display = ('club_id', 'keyword', 'reply_type', 'sort_order', 'enabled', 'UpdateTime')
list_filter = ('club_id', 'reply_type', 'enabled')
search_fields = ('keyword', 'reply_content')

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

@@ -0,0 +1,52 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0011_clubminiappicon_is_uploaded'),
]
operations = [
migrations.CreateModel(
name='MiniappScriptScene',
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')),
('scene_key', models.CharField(db_index=True, max_length=64, verbose_name='场景键')),
('title', models.CharField(blank=True, default='', max_length=128, verbose_name='标题')),
('content', models.TextField(blank=True, default='', verbose_name='正文')),
('confirm_text', models.CharField(blank=True, default='', max_length=16, verbose_name='确认按钮')),
('cancel_text', models.CharField(blank=True, default='', max_length=16, verbose_name='取消按钮')),
('enabled', 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': 'miniapp_script_scene',
'unique_together': {('club_id', 'scene_key')},
},
),
migrations.CreateModel(
name='MiniappScriptAutoReply',
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')),
('keyword', models.CharField(max_length=200, verbose_name='关键词')),
('reply_type', models.CharField(choices=[('text', '文本'), ('image', '图片')], default='text', max_length=16, verbose_name='回复类型')),
('reply_content', models.TextField(verbose_name='回复内容(文本或图片URL)')),
('sort_order', models.IntegerField(default=0, verbose_name='排序(大优先)')),
('enabled', 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': 'miniapp_script_auto_reply',
'ordering': ['-sort_order', 'id'],
},
),
]

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='用户类型'),
),
]

File diff suppressed because it is too large Load Diff

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,9 +3,9 @@ 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,\
ShangjiaLianjieListView
KehuTianxieDingdanView, GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\
GetDynamicConfigView, HaibaoPeizhiView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\
ShangjiaLianjieListView, HuashuHqView, HuashuMatchView
urlpatterns = [
@@ -32,10 +32,13 @@ 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='小程序获取提现方式'),
path('yhbdsjh', CheckPhoneAuthView.as_view(), name='判断是否需要手机号绑定'),
path('hqsjlslj', ShangjiaLianjieListView.as_view(), name='商家获取生成链接'),
path('huashuhq', HuashuHqView.as_view(), name='小程序场景话术获取'),
path('huashu_match', HuashuMatchView.as_view(), name='客服关键词自动回复'),
]

File diff suppressed because it is too large Load Diff

74
config/views/__init__.py Normal file
View File

@@ -0,0 +1,74 @@
"""
配置应用视图聚合模块config/views/__init__.py
本模块原为 config/views.py3718 行单文件),已按功能组拆分到 config/views/ 子包。
本文件作为聚合 re-export 入口,重新导出所有视图类与公共符号。
外部引用config/urls.py、backend/urls.py、merchant_ops/views_templates.py保持不变。
功能组拆分:
- miniapp_config 小程序动态配置(动态配置/海报/弹窗/提现方式/手机号认证)
- admin_config 管理员配置(商品公告/配置查询/图片上传/群与利率更新)
- admin_stats 管理员统计/记录(打手排行/账号查询/修改记录)
- merchant_templates 商家订单模板
- merchant_links 商家链接/客户订单(含 IPUtils、OrderLinkThrottle
- qrcode_poster 二维码/海报(关注/管事二维码/组长海报)
- scripts 话术配置(含辅助函数)
"""
import logging
# 各功能组视图
from .miniapp_config import (
GetDynamicConfigView, HaibaoPeizhiView, PopupConfigView,
GetWithdrawModeView, CheckPhoneAuthView,
)
from .admin_config import (
ShangpinGonggaoView, AdminConfigQueryView, AdminUploadImageView,
AdminUpdateConfigView,
)
from .admin_stats import (
DashouPaihangView, GetAdminZhanghaoView, GetXiugaiJiluView,
)
from .merchant_templates import (
ShangjiaMobanListView, ShangjiaTianjiaMobanView,
ShangjiaShanchuMobanView, ShangjiaGengxinMobanView,
)
from .merchant_links import (
ShangjiaGenerateLinkView, ShangjiaLianjieListView,
KehuGetDingdanLianjieView, KehuTianxieDingdanView,
IPUtils, OrderLinkThrottle,
)
from .qrcode_poster import (
GuanZhuAListView, GuanshiQRCodeView, ZuzhangHaibaoView,
)
from .scripts import (
HuashuHqView, HuashuMatchView,
MiniappScriptHoutaiListView, MiniappScriptHoutaiModifyView,
)
# 模块级符号(保持向后兼容)
logger = logging.getLogger(__name__)
__all__ = [
# miniapp_config
'GetDynamicConfigView', 'HaibaoPeizhiView', 'PopupConfigView',
'GetWithdrawModeView', 'CheckPhoneAuthView',
# admin_config
'ShangpinGonggaoView', 'AdminConfigQueryView', 'AdminUploadImageView',
'AdminUpdateConfigView',
# admin_stats
'DashouPaihangView', 'GetAdminZhanghaoView', 'GetXiugaiJiluView',
# merchant_templates
'ShangjiaMobanListView', 'ShangjiaTianjiaMobanView',
'ShangjiaShanchuMobanView', 'ShangjiaGengxinMobanView',
# merchant_links
'ShangjiaGenerateLinkView', 'ShangjiaLianjieListView',
'KehuGetDingdanLianjieView', 'KehuTianxieDingdanView',
'IPUtils', 'OrderLinkThrottle',
# qrcode_poster
'GuanZhuAListView', 'GuanshiQRCodeView', 'ZuzhangHaibaoView',
# scripts
'HuashuHqView', 'HuashuMatchView',
'MiniappScriptHoutaiListView', 'MiniappScriptHoutaiModifyView',
# 公共符号
'logger',
]

View File

@@ -0,0 +1,625 @@
import io
import os
import re
import time
import random
import secrets
import hashlib
import threading
import urllib.parse
from decimal import Decimal, InvalidOperation
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q, F, Max, Prefetch
from gvsdsdk.fluent import db, func, FQ
from django.core.cache import cache
from django.core.paginator import Paginator
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
# from utils.ip_security import *
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from backend.utils import update_shangjia_daily
from utils.chat_utils import subscribe_merchant_link_chat
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
from ..models import (
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
MiniappScriptScene, MiniappScriptAutoReply,
)
from users.models import (
AdminProfile, UserDashou, UserBoss,
UserShangjia, UserGuanshi, UserZuzhang
)
from users.business_models import User
from orders.models import CommissionRate, Order, MerchantOrderExt
from orders.notice_tasks import dingdan_guangbo
from rank.models import Chenghao, DingdanBiaoqian
from products.models import ShangpinLeixing, Huiyuangoumai
from ..serializers import PopupConfigSerializer
import traceback
import requests
import logging
logger = logging.getLogger(__name__)
class ShangpinGonggaoView(APIView):
"""
商品公告和轮播图接口
获取类型为1的公告和轮播图
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
authentication_classes = []
def post(self, request):
"""
处理POST请求返回公告和轮播图数据
请求: POST /api/peizhi/shangpingonggao/
返回: {
"shangpingonggao": "公告内容字符串",
"shangpinlunbo": ["轮播图URL1", "轮播图URL2"]
}
"""
try:
logger.info("收到商品公告和轮播图请求")
from jituan.services.display_config import get_gonggao_content, get_lunbo_urls, normalize_page_key
page_key = normalize_page_key(request.data.get('page_key'), image_type=1)
gonggao_content = get_gonggao_content(request, notice_type=1, page_key=page_key)
lunbo_urls = get_lunbo_urls(request, page_key=page_key, image_type=1)
response_data = {
"shangpingonggao": gonggao_content,
"shangpinlunbo": lunbo_urls,
"page_key": page_key,
}
logger.info(f"返回数据:公告长度{len(gonggao_content)},轮播图数量{len(lunbo_urls)} page_key={page_key}")
# 4. 返回响应
return Response(response_data, status=status.HTTP_200_OK)
except Exception as e:
# 记录异常
logger.error(f"获取商品公告和轮播图时发生错误:{str(e)}")
# 返回错误响应
error_data = {
"error": "服务器内部错误",
"detail": "获取数据失败,请稍后重试"
}
return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class AdminConfigQueryView(APIView):
"""
管理员获取系统配置接口
URL: /peizhi/adpzhq
方法: POST
权限: JWT Token认证 + 管理员权限
参数: zhanghao (管理员账号)
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
try:
# 获取前端传递的账号
zhanghao = request.data.get('zhanghao')
if not zhanghao:
return Response({
'code': 1,
'msg': '账号不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 获取当前用户User实例
user_main = request.user
# 验证账号比较user_main的phone与前端传递的zhanghao
if user_main.Phone != zhanghao:
return Response({
'code': 1,
'msg': '账号不匹配',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 验证用户类型是否为管理员
if user_main.UserType != 'admin':
return Response({
'code': 1,
'msg': '无权限',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 验证管理员扩展表是否存在
try:
admin_profile = user_main.AdminProfile
except AdminProfile.DoesNotExist:
return Response({
'code': 1,
'msg': '无权限',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 查询图片配置
rule_image = Tupianpeizhi.query.filter(ImageType=1).first()
default_avatar = Tupianpeizhi.query.filter(ImageType=2).first()
# 查询群配置
qun_configs = Qunpeizhi.query.all()
qq_groups = []
for config in qun_configs:
qq_groups.append({
'peizhiid': config.GroupType,
'neirong': config.GroupContent if config.GroupContent else '',
'qunid': config.GroupID if config.GroupID else '',
'jieshao': config.Description if config.Description else ''
})
# 查询利率配置
platform_rate_obj = CommissionRate.query.filter(Platform='1').first()
shop_rate_obj = CommissionRate.query.filter(Platform='3').first()
# 🔴【新增】打手提现抽成利率字符5
dashou_withdraw_obj = CommissionRate.query.filter(Platform='5').first()
# 🔴【新增】管事提现抽成利率字符6
guanshi_withdraw_obj = CommissionRate.query.filter(Platform='6').first()
# 构建返回数据
data = {
'images': {
'rule_image': rule_image.ImageURL if rule_image and rule_image.ImageURL else '',
'default_avatar': default_avatar.ImageURL if default_avatar and default_avatar.ImageURL else ''
},
'qq_groups': qq_groups,
'rates': {
'platform_rate': float(platform_rate_obj.Rate) if platform_rate_obj and platform_rate_obj.Rate else 0.0,
'shop_rate': float(shop_rate_obj.Rate) if shop_rate_obj and shop_rate_obj.Rate else 0.0,
# 🔴【新增】打手提现抽成利率
'dashou_withdraw_rate': float(
dashou_withdraw_obj.Rate) if dashou_withdraw_obj and dashou_withdraw_obj.Rate else 0.0,
# 🔴【新增】管事提现抽成利率
'guanshi_withdraw_rate': float(
guanshi_withdraw_obj.Rate) if guanshi_withdraw_obj and guanshi_withdraw_obj.Rate else 0.0
}
}
return Response({
'code': 0,
'msg': 'success',
'data': data
})
except Exception as e:
return Response({
'code': 99,
'msg': f'系统错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class AdminUploadImageView(APIView):
"""
管理员上传配置图片接口
URL: /peizhi/adxgtp
方法: POST
权限: JWT Token认证 + 管理员权限
参数: zhanghao, leixing (1:打手规则图片, 2:默认头像图片), image (文件)
"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = (MultiPartParser, FormParser)
def post(self, request):
try:
zhanghao = request.data.get('zhanghao')
leixing = request.data.get('leixing')
image_file = request.FILES.get('image')
if not zhanghao:
return Response({
'code': 1,
'msg': '账号不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if not leixing:
return Response({
'code': 2,
'msg': '图片类型不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if not image_file:
return Response({
'code': 3,
'msg': '请选择图片文件',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 验证用户权限
user_main = request.user
if user_main.Phone != zhanghao:
return Response({
'code': 1,
'msg': '账号不匹配',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
if user_main.UserType != 'admin':
return Response({
'code': 1,
'msg': '无权限',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
try:
admin_profile = user_main.AdminProfile
except AdminProfile.DoesNotExist:
return Response({
'code': 1,
'msg': '无权限',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 验证图片类型
if leixing not in ['1', '2']:
return Response({
'code': 4,
'msg': '图片类型错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 验证图片文件
is_valid, error_msg = validate_image(image_file)
if not is_valid:
return Response({
'code': 5,
'msg': f'图片验证失败: {error_msg}',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 根据类型确定固定相对URL和文件名
if leixing == '1':
# 打手规则图片
fixed_relative_url = 'a_long/dashouguize.jpg'
oss_file_name = 'dashouguize.jpg'
else:
# 默认头像图片
fixed_relative_url = 'a_long/morentouxiang.jpg'
oss_file_name = 'morentouxiang.jpg'
# 构建OSS文件路径
oss_file_path = fixed_relative_url # a_long/dashouguize.jpg 或 a_long/morentouxiang.jpg
# 上传到OSS覆盖原有文件
image_file.seek(0) # 重置文件指针
full_url = upload_to_oss(image_file, oss_file_path)
if not full_url:
return Response({
'code': 6,
'msg': '图片上传到存储桶失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 更新数据库
with transaction.atomic():
# 获取或创建图片配置记录
tupian_peizhi, created = Tupianpeizhi.query.get_or_create(ImageType=int(leixing))
tupian_peizhi.ImageURL = fixed_relative_url
tupian_peizhi.save()
return Response({
'code': 0,
'msg': '图片上传成功',
'data': {
'image_url': fixed_relative_url
}
})
except Exception as e:
return Response({
'code': 99,
'msg': f'系统错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class AdminUpdateConfigView(APIView):
"""
管理员更新系统配置接口QQ群、利率- 已修复
URL: /peizhi/adupdate
方法: POST
权限: JWT Token认证 + 管理员权限
参数:
- zhanghao: 管理员账号
- qq_groups: [{"peizhiid": 1, "neirong": "", "qunid": "", "jieshao": ""}, ...] (可选)
- rates: {"platform_rate": 0.1, "shop_rate": 0.2} (可选)
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
try:
zhanghao = request.data.get('zhanghao')
if not zhanghao:
return Response({
'code': 1,
'msg': '账号不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 验证用户权限
user_main = request.user
if user_main.Phone != zhanghao:
return Response({
'code': 1,
'msg': '账号不匹配',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
if user_main.UserType != 'admin':
return Response({
'code': 1,
'msg': '无权限',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
try:
admin_profile = user_main.AdminProfile
except AdminProfile.DoesNotExist:
return Response({
'code': 1,
'msg': '无权限',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
response_data = {}
# 🔴 修复更新QQ群配置 - 确保根据peizhiid正确更新对应的记录
qq_groups = request.data.get('qq_groups')
if qq_groups and isinstance(qq_groups, list):
# 收集所有 peizhiid批量预取已有记录避免循环内 N+1
_peizhiids = [g.get('peizhiid') for g in qq_groups if g.get('peizhiid')]
_existing_map = {}
if _peizhiids:
for q in Qunpeizhi.query.filter(GroupType__in=_peizhiids):
# 如果存在多个相同 peizhiid 的记录,取第一个(应该只有一个)
if q.GroupType not in _existing_map:
_existing_map[q.GroupType] = q
_to_create = []
_to_update = []
updated_groups = []
for group_data in qq_groups:
peizhiid = group_data.get('peizhiid')
if not peizhiid:
continue
neirong = group_data.get('neirong')
qunid = group_data.get('qunid')
jieshao = group_data.get('jieshao')
qun_config = _existing_map.get(peizhiid)
if qun_config is not None:
# 更新已有记录
if neirong is not None:
qun_config.GroupContent = neirong or ''
if qunid is not None:
qun_config.GroupID = qunid or ''
if jieshao is not None:
qun_config.Description = jieshao or ''
_to_update.append(qun_config)
else:
# 不存在则创建新的
qun_config = Qunpeizhi(
GroupType=peizhiid,
GroupContent=neirong or '',
GroupID=qunid or '',
Description=jieshao or '',
)
_to_create.append(qun_config)
updated_groups.append({
'peizhiid': qun_config.GroupType,
'neirong': qun_config.GroupContent,
'qunid': qun_config.GroupID,
'jieshao': qun_config.Description
})
if _to_create:
Qunpeizhi.objects.bulk_create(_to_create)
if _to_update:
Qunpeizhi.objects.bulk_update(
_to_update, ['GroupContent', 'GroupID', 'Description']
)
if updated_groups:
response_data['qq_groups'] = updated_groups
# 🔴 修复:更新利率配置 - 确保更新现有记录而不是创建新记录
rates = request.data.get('rates')
if rates and isinstance(rates, dict):
updated_rates = {}
# 平台发单利率类型1fadanpingtai='1'
platform_rate = rates.get('platform_rate')
if platform_rate is not None:
try:
platform_rate_float = float(platform_rate)
if 0 <= platform_rate_float <= 1:
# 🔴 修复:查询现有记录,如果不存在则创建
platform_rate_obj = CommissionRate.query.filter(Platform='1').first()
if platform_rate_obj:
# 更新现有记录
platform_rate_obj.Rate = platform_rate_float
platform_rate_obj.save()
else:
# 创建新记录
platform_rate_obj = CommissionRate.query.create(
Rate=platform_rate_float,
Platform='1'
)
updated_rates['platform_rate'] = platform_rate_float
else:
return Response({
'code': 3,
'msg': '平台发单利率必须在0-1之间',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except ValueError:
return Response({
'code': 4,
'msg': '平台发单利率格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 商家发单利率类型2fadanpingtai='2'
shop_rate = rates.get('shop_rate')
if shop_rate is not None:
try:
shop_rate_float = float(shop_rate)
if 0 <= shop_rate_float <= 1:
# 🔴 修复:查询现有记录,如果不存在则创建
shop_rate_obj = CommissionRate.query.filter(Platform='3').first()
if shop_rate_obj:
# 更新现有记录
shop_rate_obj.Rate = shop_rate_float
shop_rate_obj.save()
else:
# 创建新记录
shop_rate_obj = CommissionRate.query.create(
Rate=shop_rate_float,
Platform='3'
)
updated_rates['shop_rate'] = shop_rate_float
else:
return Response({
'code': 5,
'msg': '商家发单利率必须在0-1之间',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except ValueError:
return Response({
'code': 6,
'msg': '商家发单利率格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 🔴【新增】打手提现抽成利率类型5fadanpingtai='5'
dashou_withdraw_rate = rates.get('dashou_withdraw_rate')
if dashou_withdraw_rate is not None:
try:
dashou_withdraw_rate_float = float(dashou_withdraw_rate)
if 0 <= dashou_withdraw_rate_float <= 1:
# 查询现有记录,如果不存在则创建
dashou_withdraw_obj = CommissionRate.query.filter(Platform='5').first()
if dashou_withdraw_obj:
# 更新现有记录
dashou_withdraw_obj.Rate = dashou_withdraw_rate_float
dashou_withdraw_obj.save()
else:
# 创建新记录
dashou_withdraw_obj = CommissionRate.query.create(
Rate=dashou_withdraw_rate_float,
Platform='5'
)
updated_rates['dashou_withdraw_rate'] = dashou_withdraw_rate_float
else:
return Response({
'code': 7, # 注意:这里需要调整错误码,不要重复
'msg': '打手提现抽成利率必须在0-1之间',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except ValueError:
return Response({
'code': 8,
'msg': '打手提现抽成利率格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 🔴【新增】管事提现抽成利率类型6fadanpingtai='6'
guanshi_withdraw_rate = rates.get('guanshi_withdraw_rate')
if guanshi_withdraw_rate is not None:
try:
guanshi_withdraw_rate_float = float(guanshi_withdraw_rate)
if 0 <= guanshi_withdraw_rate_float <= 1:
# 查询现有记录,如果不存在则创建
guanshi_withdraw_obj = CommissionRate.query.filter(Platform='6').first()
if guanshi_withdraw_obj:
# 更新现有记录
guanshi_withdraw_obj.Rate = guanshi_withdraw_rate_float
guanshi_withdraw_obj.save()
else:
# 创建新记录
guanshi_withdraw_obj = CommissionRate.query.create(
Rate=guanshi_withdraw_rate_float,
Platform='6'
)
updated_rates['guanshi_withdraw_rate'] = guanshi_withdraw_rate_float
else:
return Response({
'code': 9,
'msg': '管事提现抽成利率必须在0-1之间',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except ValueError:
return Response({
'code': 10,
'msg': '管事提现抽成利率格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if updated_rates:
response_data['rates'] = updated_rates
# 如果没有任何更新
if not response_data:
return Response({
'code': 7,
'msg': '没有需要更新的数据',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
return Response({
'code': 0,
'msg': '更新成功',
'data': response_data
})
except Exception as e:
return Response({
'code': 99,
'msg': f'系统错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

730
config/views/admin_stats.py Normal file
View File

@@ -0,0 +1,730 @@
import io
import os
import re
import time
import random
import secrets
import hashlib
import threading
import urllib.parse
from decimal import Decimal, InvalidOperation
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q, F, Max, Prefetch
from gvsdsdk.fluent import db, func, FQ
from django.core.cache import cache
from django.core.paginator import Paginator
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
# from utils.ip_security import *
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from backend.utils import update_shangjia_daily
from utils.chat_utils import subscribe_merchant_link_chat
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
from ..models import (
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
MiniappScriptScene, MiniappScriptAutoReply,
)
from users.models import (
AdminProfile, UserDashou, UserBoss,
UserShangjia, UserGuanshi, UserZuzhang
)
from users.business_models import User
from orders.models import CommissionRate, Order, MerchantOrderExt
from orders.notice_tasks import dingdan_guangbo
from rank.models import Chenghao, DingdanBiaoqian
from products.models import ShangpinLeixing, Huiyuangoumai
from ..serializers import PopupConfigSerializer
import traceback
import requests
import logging
logger = logging.getLogger(__name__)
class DashouPaihangView(APIView):
"""
排行榜接口
路径:/peizhi/dsph
方法POST
认证JWT Token认证
请求参数:
- leixing: 1=今日收入2=今月收入
- yonghu_leixing: 1=打手2=管事3=商家
返回数据:
- code: 0=成功,其他=失败
- msg: 提示信息
- data: {
xianshi: True/False, # 是否展示真实数据
paihang_list: [ # 排行榜列表
{
uid: '用户ID',
touxiang: '头像相对URL',
nicheng: '用户昵称',
jine: 收入金额,
mingci: 排名
},
...
]
}
"""
# =============== 可配置参数 ===============
# 每次查询返回的最大记录数(可修改)
ZUI_DAO_CHA_XUN_SHU_LIANG = 50
# 最少需要多少人才能展示(可修改)
ZUI_SHAO_REN_SHU_YAO_QIU = 1
# 第一名最低金额要求(单位:元)(可修改)
DI_YI_MING_ZUI_DI_JIN_E = Decimal('10.00')
# 管事第一名最低数量要求(可修改)
DI_YI_MING_ZUI_DI_SHU_LIANG = 1
# =============== 可配置参数结束 ===============
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 获取请求参数 - 修复参数类型问题
leixing_str = request.data.get('leixing', '1')
yonghu_leixing_str = request.data.get('yonghu_leixing', '1')
# 转换为整数
try:
leixing = int(leixing_str)
except (ValueError, TypeError):
leixing = 1
try:
yonghu_leixing = int(yonghu_leixing_str)
except (ValueError, TypeError):
yonghu_leixing = 1
# 参数验证
if leixing not in [1, 2]:
return Response({
'code': 400,
'msg': '参数错误leixing必须为1(今日)或2(今月)',
'data': {'xianshi': False}
})
if yonghu_leixing not in [1, 2, 3]:
return Response({
'code': 400,
'msg': '参数错误yonghu_leixing必须为1(打手)、2(管事)或3(商家)',
'data': {'xianshi': False}
})
# 根据用户类型调用不同的查询方法
if yonghu_leixing == 1:
# 打手排行榜
paihang_list, xianshi_flag = self.chaxun_dashou_paihang(leixing)
elif yonghu_leixing == 2:
# 管事排行榜
paihang_list, xianshi_flag = self.chaxun_guanshi_paihang(leixing)
else:
# 商家排行榜
paihang_list, xianshi_flag = self.chaxun_shangjia_paihang(leixing)
# 返回结果
return Response({
'code': 0,
'msg': '获取成功',
'data': {
'xianshi': xianshi_flag,
'paihang_list': paihang_list
}
})
except Exception as e:
logger.error(f"排行榜查询失败: {str(e)}", exc_info=True)
return Response({
'code': 500,
'msg': '服务器内部错误',
'data': {'xianshi': False}
})
def chaxun_dashou_paihang(self, leixing):
"""
查询打手排行榜
:param leixing: 1=今日收入2=今月收入
:return: (排行榜列表, 是否展示标志)
"""
# 确定查询的字段
if leixing == 1:
# 今日收入
shouyi_ziduan = 'jinrishouyi'
else:
# 今月收入
shouyi_ziduan = 'jinyueshouyi'
# 查询条件收入大于0打手账号状态正常1=正常)
# 注意打手表中的zhanghaozhuangtai字段
chaxun_tiaojian = Q(**{f'{shouyi_ziduan}__gt': 0}) & Q(zhanghaozhuangtai=1)
# 查询打手扩展表,并关联用户主表
dashou_list = UserDashou.query.select_related('user').filter(
chaxun_tiaojian
).order_by(f'-{shouyi_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
# 检查是否满足展示条件
if len(dashou_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
# 人数不足,不展示
return [], False
# 检查第一名金额是否达到最低要求
if leixing == 1:
di_yi_ming_jine = dashou_list[0].jinrishouyi
else:
di_yi_ming_jine = dashou_list[0].jinyueshouyi
if di_yi_ming_jine < self.DI_YI_MING_ZUI_DI_JIN_E:
# 第一名金额不足,不展示
return [], False
# 构建返回数据
paihang_list = []
for index, dashou in enumerate(dashou_list):
# 获取用户主表信息
user_main = dashou.user
# 打手的昵称直接使用打手扩展表的nicheng字段
dashou_nicheng = dashou.nicheng or '未设置昵称'
# 获取头像相对URL
touxiang_url = user_main.Avatar or ''
# 获取收入金额
if leixing == 1:
shouyi_jine = dashou.jinrishouyi
else:
shouyi_jine = dashou.jinyueshouyi
paihang_list.append({
'uid': user_main.UserUID,
'touxiang': touxiang_url,
'nicheng': dashou_nicheng,
'jine': float(shouyi_jine), # 转换为float方便前端处理
'mingci': index + 1
})
return paihang_list, True
def chaxun_guanshi_paihang(self, leixing):
"""
查询管事排行榜
:param leixing: 1=今日2=今月
:return: (排行榜列表, 是否展示标志)
"""
# 确定查询的字段
if leixing == 1:
# 今日充值打手数量
chaxun_ziduan = 'jinrichongzhi'
else:
# 今月充值打手数量
chaxun_ziduan = 'jinyuechongzhi'
# 查询条件数量大于0管事状态正常状态1为正常
# 注意管手表中的zhuangtai字段
chaxun_tiaojian = Q(**{f'{chaxun_ziduan}__gt': 0}) & Q(zhuangtai=1)
# 查询管事扩展表,并关联用户主表
guanshi_list = UserGuanshi.query.select_related('user').filter(
chaxun_tiaojian
).order_by(f'-{chaxun_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
# 检查是否满足展示条件
if len(guanshi_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
# 人数不足,不展示
return [], False
# 检查第一名数量是否达到最低要求
if leixing == 1:
di_yi_ming_shuliang = guanshi_list[0].jinrichongzhi
else:
di_yi_ming_shuliang = guanshi_list[0].jinyuechongzhi
if di_yi_ming_shuliang < self.DI_YI_MING_ZUI_DI_SHU_LIANG:
# 第一名数量不足,不展示
return [], False
# 批量预取老板扩展表昵称
_boss_user_ids = [g.user_id for g in guanshi_list]
_boss_nicheng_map = {
b.user_id: b.nickname
for b in UserBoss.query.filter(user_id__in=_boss_user_ids).only('user_id', 'nickname')
}
# 构建返回数据
paihang_list = []
for index, guanshi in enumerate(guanshi_list):
# 获取用户主表信息
user_main = guanshi.user
# 管事的昵称需要查询老板扩展表
guanshi_nicheng = _boss_nicheng_map.get(guanshi.user_id, '未设置昵称')
# 获取头像相对URL
touxiang_url = user_main.Avatar or ''
# 获取数量
if leixing == 1:
chongzhi_shuliang = guanshi.jinrichongzhi
else:
chongzhi_shuliang = guanshi.jinyuechongzhi
paihang_list.append({
'uid': user_main.UserUID,
'touxiang': touxiang_url,
'nicheng': guanshi_nicheng,
'jine': float(chongzhi_shuliang), # 这里jine字段实际存储的是数量
'mingci': index + 1
})
return paihang_list, True
def chaxun_shangjia_paihang(self, leixing):
"""
查询商家排行榜
:param leixing: 1=今日流水2=今月流水
:return: (排行榜列表, 是否展示标志)
"""
# 确定查询的字段
if leixing == 1:
# 今日流水
liushui_ziduan = 'jinriliushui'
else:
# 今月流水
liushui_ziduan = 'jinyueliushui'
# 查询条件流水大于0商家状态正常状态1为正常
# 注意商家表中的zhuangtai字段
chaxun_tiaojian = Q(**{f'{liushui_ziduan}__gt': 0}) & Q(zhuangtai=1)
# 查询商家扩展表,并关联用户主表
shangjia_list = UserShangjia.query.select_related('user').filter(
chaxun_tiaojian
).order_by(f'-{liushui_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
# 检查是否满足展示条件
if len(shangjia_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
# 人数不足,不展示
return [], False
# 检查第一名金额是否达到最低要求
if leixing == 1:
di_yi_ming_liushui = shangjia_list[0].jinriliushui
else:
di_yi_ming_liushui = shangjia_list[0].jinyueliushui
if di_yi_ming_liushui < self.DI_YI_MING_ZUI_DI_JIN_E:
# 第一名流水不足,不展示
return [], False
# 批量预取老板扩展表昵称
_boss_user_ids = [s.user_id for s in shangjia_list]
_boss_nicheng_map = {
b.user_id: b.nickname
for b in UserBoss.query.filter(user_id__in=_boss_user_ids).only('user_id', 'nickname')
}
# 构建返回数据
paihang_list = []
for index, shangjia in enumerate(shangjia_list):
# 获取用户主表信息
user_main = shangjia.user
# 商家的昵称需要查询老板扩展表
shangjia_nicheng = _boss_nicheng_map.get(shangjia.user_id, '未设置昵称')
# 获取头像相对URL
touxiang_url = user_main.Avatar or ''
# 获取流水金额
if leixing == 1:
liushui_jine = shangjia.jinriliushui
else:
liushui_jine = shangjia.jinyueliushui
paihang_list.append({
'uid': user_main.UserUID,
'touxiang': touxiang_url,
'nicheng': shangjia_nicheng,
'jine': float(liushui_jine),
'mingci': index + 1
})
return paihang_list, True
class GetAdminZhanghaoView(APIView):
"""
获取管理员账号列表接口
路径:/peizhi/adzhhq
方法POST
权限JWT Token + 管理员验证
参数:
- zhanghao: 当前登录的管理员账号(字符串)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 🔥 1. 身份验证100%按表模型)
user = request.user
# 获取当前用户的手机号CharField字符串类型
current_phone = getattr(user, 'Phone', '')
if not current_phone:
return Response({
'code': 401,
'message': '用户手机号信息不完整',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 检查前端传递的账号是否匹配(都是字符串)
frontend_zhanghao = request.data.get('zhanghao', '').strip()
if frontend_zhanghao != current_phone:
return Response({
'code': 403,
'message': '账号不匹配,无权限访问',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 🔥 2. 验证是否为管理员user_type是CharField值是'admin'字符串)
user_type = getattr(user, 'user_type', '')
if user_type != 'admin':
# 检查是否有管理员扩展表
if not hasattr(user, 'AdminProfile'):
return Response({
'code': 403,
'message': '无管理员权限',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 🔥 3. 查询管理员账号列表(全部按字符串查询)
with connection.cursor() as cursor:
# 🔥 查询所有管理员账号user_type='admin' 字符串)
# 🔥 phone和yonghuid都是CharFieldxiugaiid也是CharField
sql = """
SELECT
um.id,
um.phone,
um.yonghuid,
um.UserType,
COALESCE(COUNT(xjl.id), 0) as jilushuliang
FROM user_main um
LEFT JOIN xiugaijilu xjl ON um.phone = xjl.xiugaiid
WHERE um.UserType = 'admin'
GROUP BY um.id, um.phone, um.yonghuid, um.UserType
ORDER BY um.CreateTime DESC
"""
cursor.execute(sql)
results = cursor.fetchall()
# 🔥 4. 处理查询结果(全部保持字符串类型)
zhanghao_list = []
for row in results:
zhanghao_list.append({
#'id': row[0],
'id': str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
'zhanghao':str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
'yonghuid': str(row[2]) if row[2] else '', # 🔥 保持字符串
'user_type': str(row[3]) if row[3] else '', # 🔥 保持字符串
'jilushuliang': int(row[4]) # 只有这个是数字
})
return Response({
'code': 0,
'message': '获取成功',
'data': zhanghao_list
}, status=status.HTTP_200_OK)
except Exception as e:
print(f"获取管理员账号列表失败: {str(e)}")
traceback.print_exc()
return Response({
'code': 500,
'message': '系统错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class GetXiugaiJiluView(APIView):
"""
获取修改记录列表接口
路径:/peizhi/adhqxg
方法POST
权限JWT Token + 管理员验证
参数:
- zhanghao: 当前登录的管理员账号(字符串)
- xiugaizhe_zhanghaoid: 选中的管理员账号ID字符串对应phone或yonghuid
- page: 页码(整数)
- pagesize: 每页条数(整数)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 🔥 1. 身份验证100%按表模型)
user = request.user
# 获取当前用户的手机号CharField字符串
current_phone = getattr(user, 'Phone', '')
if not current_phone:
return Response({
'code': 401,
'message': '用户手机号信息不完整',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 检查前端传递的账号是否匹配(字符串比较)
frontend_zhanghao = request.data.get('zhanghao', '')
if not isinstance(frontend_zhanghao, str):
frontend_zhanghao = str(frontend_zhanghao)
frontend_zhanghao = frontend_zhanghao.strip()
if frontend_zhanghao != current_phone:
return Response({
'code': 403,
'message': '账号不匹配,无权限访问',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 🔥 2. 验证是否为管理员user_type='admin'字符串)
user_type = getattr(user, 'user_type', '')
if user_type != 'admin':
if not hasattr(user, 'AdminProfile'):
return Response({
'code': 403,
'message': '无管理员权限',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 🔥 3. 获取参数100%按实际类型)
# 🔥 xiugaizhe_zhanghaoid 是字符串对应xiugaiid字段CharField
xiugaizhe_zhanghaoid_param = request.data.get('xiugaizhe_zhanghaoid', '')
# 🔥 强制转为字符串因为数据库是CharField
if xiugaizhe_zhanghaoid_param is None:
xiugaizhe_zhanghaoid = ''
else:
# 无论如何都转为字符串
xiugaizhe_zhanghaoid = str(xiugaizhe_zhanghaoid_param).strip()
# 🔥 分页参数转为整数
try:
page = int(request.data.get('page', 1))
except:
page = 1
try:
pagesize = int(request.data.get('pagesize', 10))
except:
pagesize = 10
# 🔥 4. 参数验证
if not xiugaizhe_zhanghaoid:
return Response({
'code': 400,
'message': '修改者账号ID不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if page < 1:
page = 1
if pagesize < 1 or pagesize > 50:
pagesize = 10
# 🔥 5. 高效查询100%按表字段类型)
with connection.cursor() as cursor:
# 🔥 5.1 查询总记录数xiugaiid是CharField用字符串查询
count_sql = """
SELECT COUNT(*)
FROM xiugaijilu
WHERE xiugaiid = %s
"""
cursor.execute(count_sql, [xiugaizhe_zhanghaoid])
total_count = cursor.fetchone()[0]
# 🔥 5.2 计算分页
offset = (page - 1) * pagesize
# 🔥 5.3 查询当前页数据使用参数化查询防止SQL注入
data_sql = """
SELECT
xjl.id,
xjl.yonghuid, -- CharField
xjl.xiugaiid, -- CharField
xjl.leixing, -- IntegerField
xjl.xiugaitijiao, -- DecimalField
xjl.xiugaitijiaoq, -- DecimalField
xjl.jifen, -- IntegerField
xjl.yjifen, -- IntegerField
xjl.yajin, -- DecimalField
xjl.yyajin, -- DecimalField
xjl.shangjiayue, -- DecimalField
xjl.shangjiayueq, -- DecimalField
xjl.guanshiyue, -- DecimalField
xjl.guanshiyueq, -- DecimalField
xjl.huiyuants, -- IntegerField
xjl.huiyuan_id, -- CharField
xjl.qitashuoming, -- TextField
xjl.CreateTime, -- DateTimeField
um.avatar -- CharField
FROM xiugaijilu xjl
LEFT JOIN user_main um ON xjl.yonghuid = um.yonghuid
WHERE xjl.xiugaiid = %s
ORDER BY xjl.CreateTime DESC
LIMIT %s OFFSET %s
"""
cursor.execute(data_sql, [xiugaizhe_zhanghaoid, pagesize, offset])
records = cursor.fetchall()
# 🔥 6. 处理查询结果(严格按照字段类型处理)
# 预查询修改者账号信息(循环外执行一次,避免循环内重复查询同一 SQL
xiugaizhe_zhanghao = ''
with connection.cursor() as cursor2:
cursor2.execute(
"SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1",
[xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid]
)
xiugaizhe_info = cursor2.fetchone()
if xiugaizhe_info:
xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \
xiugaizhe_info[1] else xiugaizhe_zhanghaoid
jilu_list = []
for record in records:
# 修改者账号信息已预查询,直接使用
# 获取会员名称(如果有)
huiyuan_mingzi = ''
if record[15]: # huiyuan_id是CharField
with connection.cursor() as cursor3:
cursor3.execute(
"SELECT jieshao FROM huiyuan WHERE huiyuan_id = %s LIMIT 1",
[str(record[15])] # 🔥 转为字符串查询
)
huiyuan_info = cursor3.fetchone()
if huiyuan_info:
huiyuan_mingzi = str(huiyuan_info[0]) if huiyuan_info[0] else ''
# 🔥 严格按照前端字段名映射,保持正确类型
jilu_data = {
# 基础字段(字符串)
'id': record[0],
'beixiugai_yonghuid': str(record[1]) if record[1] else '', # yonghuid
'xiugaizhe_id': str(record[2]) if record[2] else '', # xiugaiid
'xiugaizhe_zhanghao': xiugaizhe_zhanghao, # 字符串
'beixiugai_yonghuleixing': record[3], # leixing是整数
# 打手字段Decimal转floatInteger转int
'xiugaihou_dashoutixianjine': float(record[4]) if record[4] is not None else 0.00,
'xiugaiqian_dashoutixianjine': float(record[5]) if record[5] is not None else 0.00,
'xiugaihou_dashoujifen': int(record[6]) if record[6] is not None else 0,
'xiugaiqian_dashoujifen': int(record[7]) if record[7] is not None else 0,
'xiugaihou_dashouyajin': float(record[8]) if record[8] is not None else 0.00,
'xiugaiqian_dashouyajin': float(record[9]) if record[9] is not None else 0.00,
# 商家字段
'xiugaihou_shangjiayue': float(record[10]) if record[10] is not None else 0.00,
'xiugaiqian_shangjiayue': float(record[11]) if record[11] is not None else 0.00,
# 管事字段
'xiugaihou_guanshiyue': float(record[12]) if record[12] is not None else 0.00,
'xiugaiqian_guanshiyue': float(record[13]) if record[13] is not None else 0.00,
# 会员字段
'tianjia_huiyuantianshu': int(record[14]) if record[14] is not None else 0,
'huiyuan_id': str(record[15]) if record[15] else '', # 字符串
'huiyuan_mingzi': huiyuan_mingzi, # 字符串
# 其他字段
'qita_shuoming': str(record[16]) if record[16] else '', # 字符串
'xiugai_time': record[17].strftime('%Y-%m-%d %H:%M:%S') if record[17] else None,
'touxiang': str(record[18]) if record[18] else '' # avatar是字符串
}
jilu_list.append(jilu_data)
# 🔥 7. 分页控制
queried_records = len(jilu_list)
has_more = False
if queried_records == pagesize:
total_queried = (page - 1) * pagesize + queried_records
has_more = total_queried < total_count
return Response({
'code': 0,
'message': '获取成功',
'data': {
'list': jilu_list,
'zongshuliang': total_count,
'dangqian_page': page,
'page_size': pagesize,
'has_more': has_more,
'shijichaxundediaoshuliang': queried_records
}
}, status=status.HTTP_200_OK)
except ValueError as e:
print(f"参数错误: {str(e)}")
return Response({
'code': 400,
'message': '参数错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
print(f"获取修改记录失败: {str(e)}")
traceback.print_exc()
return Response({
'code': 500,
'message': '系统错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,490 @@
import io
import os
import re
import time
import random
import secrets
import hashlib
import threading
import urllib.parse
from decimal import Decimal, InvalidOperation
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q, F, Max, Prefetch
from gvsdsdk.fluent import db, func, FQ
from django.core.cache import cache
from django.core.paginator import Paginator
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
# from utils.ip_security import *
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from backend.utils import update_shangjia_daily
from utils.chat_utils import subscribe_merchant_link_chat
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
from ..models import (
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
MiniappScriptScene, MiniappScriptAutoReply,
)
from users.models import (
AdminProfile, UserDashou, UserBoss,
UserShangjia, UserGuanshi, UserZuzhang
)
from users.business_models import User
from orders.models import CommissionRate, Order, MerchantOrderExt
from orders.notice_tasks import dingdan_guangbo
from rank.models import Chenghao, DingdanBiaoqian
from products.models import ShangpinLeixing, Huiyuangoumai
from ..serializers import PopupConfigSerializer
import traceback
import requests
import logging
logger = logging.getLogger(__name__)
class ShangjiaMobanListView(APIView):
"""
商家订单模板列表接口
POST /peizhi/sjddmblb
权限: JWT认证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
yonghu_id = user.UserUID
leixing_id = request.data.get('shangpinTypeId')
if not leixing_id:
return Response({'code': 400, 'msg': '商品类型ID不能为空', 'data': {}}, status=400)
get_type = int(request.data.get('getType', 2))
page = int(request.data.get('page', 1))
page_size = int(request.data.get('pageSize', 50))
keyword = request.data.get('keyword', '')
min_price = request.data.get('minPrice', '')
label_id = request.data.get('labelId', None)
if label_id is not None:
try:
label_id = int(label_id)
except (ValueError, TypeError):
label_id = None
query = Q(UserID=yonghu_id, ProductTypeID=leixing_id)
if get_type == 1: # 搜索模式
if keyword:
keyword = str(keyword).strip()
try:
mid = int(keyword)
query &= Q(id=mid) | Q(TemplateDesc__icontains=keyword)
except ValueError:
query &= Q(TemplateDesc__icontains=keyword)
if min_price:
try:
query &= Q(Price__gte=Decimal(str(min_price)))
except:
pass
if label_id:
query &= Q(TitleID=label_id)
mobans = ShangjiaMoban.query.filter(query).order_by('-id')
formatted = []
for moban in mobans:
texiao_json = ''
label_name = ''
if moban.TitleID:
try:
ch = Chenghao.query.get(id=moban.TitleID)
label_name = ch.mingcheng
texiao_json = ch.texiao_miaoshu or ''
except Chenghao.DoesNotExist:
pass
formatted.append({
'mobanId': moban.id,
'shangpinTypeId': moban.ProductTypeID,
'jieshao': moban.TemplateDesc,
'jiage': str(moban.Price),
'fabushuliang': moban.PublishedCount,
'commissionEnabled': moban.CommissionGrabEnabled,
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
'labelId': moban.TitleID,
'labelName': label_name,
'texiaoJson': texiao_json,
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if moban.CreateTime else ''
})
return Response({'code': 200, 'msg': '搜索成功', 'data': {'list': formatted, 'total': len(formatted), 'hasMore': False}})
# 分页模式
queryset = ShangjiaMoban.query.filter(query).order_by('-id')
paginator = Paginator(queryset, page_size)
if page < 1 or page > paginator.num_pages:
return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
current_page = paginator.page(page)
# 批量预取所有称号,避免循环内 N+1
_mobans_list = list(current_page.object_list)
_title_ids = [m.TitleID for m in _mobans_list if m.TitleID]
_ch_map = {
ch.id: ch
for ch in Chenghao.query.filter(id__in=_title_ids).only('id', 'mingcheng', 'texiao_miaoshu')
} if _title_ids else {}
formatted = []
for moban in _mobans_list:
texiao_json = ''
label_name = ''
ch = _ch_map.get(moban.TitleID)
if ch:
label_name = ch.mingcheng
texiao_json = ch.texiao_miaoshu or ''
formatted.append({
'mobanId': moban.id,
'shangpinTypeId': moban.ProductTypeID,
'jieshao': moban.TemplateDesc,
'jiage': str(moban.Price),
'fabushuliang': moban.PublishedCount,
'commissionEnabled': moban.CommissionGrabEnabled,
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
'labelId': moban.TitleID,
'labelName': label_name,
'texiaoJson': texiao_json,
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if moban.CreateTime else ''
})
has_more = current_page.has_next()
if len(formatted) < page_size:
has_more = False
return Response({
'code': 200,
'msg': '获取成功',
'data': {
'list': formatted,
'total': paginator.count,
'hasMore': has_more,
'currentPage': page,
'pageSize': page_size,
'totalPages': paginator.num_pages
}
})
except Exception as e:
logger.error(f"获取模板列表失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
class ShangjiaTianjiaMobanView(APIView):
"""
商家添加订单模板接口
POST /peizhi/sjtjddmb
权限: JWT认证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取当前商家用户ID
user = request.user
yonghu_id = user.UserUID
# 2. 接收并清洗参数,避免类型错误
leixing_id = request.data.get('shangpinTypeId')
jieshao = request.data.get('jieshao', '').strip() if request.data.get('jieshao') else ''
jiage = request.data.get('jiage', '')
# 佣金相关
commission_enabled = request.data.get('commissionEnabled', False)
commission_value = request.data.get('commissionValue', None)
# 标签ID称号ID
label_id = request.data.get('labelId', None)
# 3. 必填验证
if not leixing_id or not jieshao or not jiage:
logger.warning(f"参数缺失 - leixing_id:{leixing_id}, jieshao:{jieshao}, jiage:{jiage}")
return Response({
'code': 400,
'msg': '缺少必填字段(订单类型、介绍、价格)',
'data': {}
}, status=400)
# 4. 价格格式验证
try:
jiage_decimal = Decimal(str(jiage))
if jiage_decimal <= 0:
return Response({'code': 400, 'msg': '价格必须大于0', 'data': {}}, status=400)
if jiage_decimal > 10000:
return Response({'code': 400, 'msg': '价格不能超过10000元', 'data': {}}, status=400)
except (InvalidOperation, ValueError, TypeError):
return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
# 5. 介绍长度验证
if len(jieshao) > 100:
return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
# 6. 佣金验证(若启用)
CommissionGrabEnabled = False
CommissionPrice = None
if commission_enabled:
CommissionGrabEnabled = True
if not commission_value:
return Response({'code': 400, 'msg': '开启佣金时,佣金金额不能为空', 'data': {}}, status=400)
try:
comm_decimal = Decimal(str(commission_value))
if comm_decimal <= 0:
return Response({'code': 400, 'msg': '佣金必须大于0', 'data': {}}, status=400)
CommissionPrice = comm_decimal
except (InvalidOperation, ValueError, TypeError):
return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
# 7. 标签ID清洗前端可能传数字或字符串
cleaned_label_id = None
if label_id is not None and str(label_id).strip():
try:
cleaned_label_id = int(label_id)
except (ValueError, TypeError):
return Response({'code': 400, 'msg': '标签ID格式错误', 'data': {}}, status=400)
# 8. 验证商品类型是否存在且可用
try:
shangpin_obj = ShangpinLeixing.query.get(
id=leixing_id,
shenhezhuangtai=1 # 审核通过
)
except ShangpinLeixing.DoesNotExist:
return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404)
# 9. 检查同一用户、同一类型下是否已存在相同介绍的模板
if ShangjiaMoban.query.filter(
UserID=yonghu_id,
ProductTypeID=leixing_id,
TemplateDesc=jieshao
).exists():
return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
# 10. 执行数据库创建(事务)
with transaction.atomic():
moban = ShangjiaMoban.query.create(
UserID=yonghu_id,
ProductTypeID=leixing_id,
TemplateDesc=jieshao,
Price=jiage_decimal,
PublishedCount=0,
CommissionGrabEnabled=CommissionGrabEnabled,
CommissionPrice=CommissionPrice,
TitleID=cleaned_label_id
)
# 11. 返回成功数据(字段与前端对齐)
return Response({
'code': 200,
'msg': '模板创建成功',
'data': {
'mobanId': moban.id,
'shangpinTypeId': leixing_id,
'jieshao': moban.TemplateDesc,
'jiage': str(moban.Price),
'fabushuliang': moban.PublishedCount,
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S'),
'commissionEnabled': moban.CommissionGrabEnabled,
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
'labelId': moban.TitleID
}
})
except Exception as e:
# 打印完整错误堆栈到日志
logger.error(f"添加模板失败: {str(e)}", exc_info=True)
return Response({
'code': 500,
'msg': '服务器内部错误,添加模板失败,请联系管理员',
'data': {}
}, status=500)
class ShangjiaShanchuMobanView(APIView):
"""
删除模板
POST /peizhi/sjddmbsc
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
yonghu_id = user.UserUID
moban_id = request.data.get('mobanId')
leixing_id = request.data.get('shangpinTypeId')
if not all([moban_id, leixing_id]):
return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400)
try:
moban = ShangjiaMoban.query.get(id=moban_id, UserID=yonghu_id, ProductTypeID=leixing_id)
except ShangjiaMoban.DoesNotExist:
return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404)
# 强制硬删除
moban.delete()
return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}})
except Exception as e:
logger.error(f"删除模板失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
class ShangjiaGengxinMobanView(APIView):
"""
商家更新订单模板接口
POST /peizhi/sjddmbgx
权限: JWT认证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
yonghu_id = user.UserUID
moban_id = request.data.get('mobanId')
leixing_id = request.data.get('shangpinTypeId')
new_jieshao = request.data.get('jieshao', '').strip()
new_jiage = request.data.get('jiage', '').strip()
# 新增字段
commission_enabled = request.data.get('commissionEnabled')
commission_value = request.data.get('commissionValue')
label_id = request.data.get('labelId')
if not all([moban_id, leixing_id]):
return Response({'code': 400, 'msg': '模板ID和商品类型ID不能为空', 'data': {}}, status=400)
# 查询模板
try:
moban = ShangjiaMoban.query.get(
id=moban_id,
UserID=yonghu_id,
ProductTypeID=leixing_id
)
except ShangjiaMoban.DoesNotExist:
return Response({'code': 404, 'msg': '模板不存在或无权修改', 'data': {}}, status=404)
update_fields = []
# 修改介绍
if new_jieshao:
if len(new_jieshao) > 100:
return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
if new_jieshao != moban.TemplateDesc:
moban.TemplateDesc = new_jieshao
update_fields.append('TemplateDesc')
# 修改价格
if new_jiage:
try:
jiage_decimal = Decimal(new_jiage)
if jiage_decimal <= 0 or jiage_decimal > 10000:
return Response({'code': 400, 'msg': '价格必须在0.1-10000元之间', 'data': {}}, status=400)
if jiage_decimal != moban.Price:
moban.Price = jiage_decimal
update_fields.append('Price')
except (InvalidOperation, ValueError, TypeError):
return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
# 修改标签
if label_id is not None: # 允许传空字符串或不传传0表示清空
new_label = int(label_id) if label_id else None
if new_label != moban.TitleID:
moban.TitleID = new_label
update_fields.append('TitleID')
# 修改佣金设置
if commission_enabled is not None:
enabled = bool(commission_enabled)
if enabled:
if not commission_value:
return Response({'code': 400, 'msg': '佣金价格不能为空', 'data': {}}, status=400)
try:
comm_decimal = Decimal(commission_value)
if comm_decimal < 0.1 or comm_decimal > 50:
return Response({'code': 400, 'msg': '佣金需在0.1-50元之间', 'data': {}}, status=400)
if moban.CommissionGrabEnabled != True or moban.CommissionPrice != comm_decimal:
moban.CommissionGrabEnabled = True
moban.CommissionPrice = comm_decimal
update_fields.extend(['CommissionGrabEnabled', 'CommissionPrice'])
except (InvalidOperation, ValueError, TypeError):
return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
else:
if moban.CommissionGrabEnabled != False or moban.CommissionPrice is not None:
moban.CommissionGrabEnabled = False
moban.CommissionPrice = None
update_fields.extend(['CommissionGrabEnabled', 'CommissionPrice'])
if not update_fields:
return Response({'code': 400, 'msg': '没有修改内容', 'data': {}}, status=400)
# 检查介绍唯一性
if 'TemplateDesc' in update_fields:
if ShangjiaMoban.query.filter(
UserID=yonghu_id,
ProductTypeID=leixing_id,
TemplateDesc=moban.TemplateDesc
).exclude(id=moban_id).exists():
return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
with transaction.atomic():
moban.save(update_fields=update_fields)
# 返回最新数据
return Response({
'code': 200,
'msg': '模板修改成功',
'data': {
'mobanId': moban.id,
'shangpinTypeId': moban.ProductTypeID,
'jieshao': moban.TemplateDesc,
'jiage': str(moban.Price),
'fabushuliang': moban.PublishedCount,
'commissionEnabled': moban.CommissionGrabEnabled,
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice else None,
'labelId': moban.TitleID,
'updateTime': moban.UpdateTime.strftime('%Y-%m-%d %H:%M:%S')
}
})
except Exception as e:
logger = logging.getLogger(__name__)
logger.error(f"修改商家模板失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)

View File

@@ -0,0 +1,334 @@
import io
import os
import re
import time
import random
import secrets
import hashlib
import threading
import urllib.parse
from decimal import Decimal, InvalidOperation
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q, F, Max, Prefetch
from gvsdsdk.fluent import db, func, FQ
from django.core.cache import cache
from django.core.paginator import Paginator
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
# from utils.ip_security import *
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from backend.utils import update_shangjia_daily
from utils.chat_utils import subscribe_merchant_link_chat
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
from ..models import (
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
MiniappScriptScene, MiniappScriptAutoReply,
)
from users.models import (
AdminProfile, UserDashou, UserBoss,
UserShangjia, UserGuanshi, UserZuzhang
)
from users.business_models import User
from orders.models import CommissionRate, Order, MerchantOrderExt
from orders.notice_tasks import dingdan_guangbo
from rank.models import Chenghao, DingdanBiaoqian
from products.models import ShangpinLeixing, Huiyuangoumai
from ..serializers import PopupConfigSerializer
import traceback
import requests
import logging
logger = logging.getLogger(__name__)
class GetDynamicConfigView(APIView):
permission_classes = [AllowAny]
parser_classes = [JSONParser]
def post(self, request):
try:
from jituan.services.display_config import get_tupianpeizhi_url
from jituan.services.miniapp_assets import build_miniapp_assets_payload
cos_oss_url = getattr(settings, 'COS_DOMAIN', '')
if cos_oss_url and not cos_oss_url.endswith('/'):
cos_oss_url += '/'
cos_bucket = getattr(settings, 'COS_BUCKET', '')
cos_region = getattr(settings, 'COS_REGION', 'ap-shanghai')
goeasy_host = 'hangzhou.goeasy.io'
goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '')
kefu_link = getattr(settings, 'KEFU_LINK', '')
kefu_enterprise_id = getattr(settings, 'KEFU_ENTERPRISE_ID', '')
rule_img = get_tupianpeizhi_url(request, 1)
avatar_img = get_tupianpeizhi_url(request, 2)
morentouxiang = avatar_img or 'beijing/morentouxiang.jpg'
dashouguize = rule_img or 'a_long/dashouguize.jpg'
# 构造返回数据
data = {
"code": 0,
"data": {
"cos": {
"bucket": cos_bucket,
"region": cos_region,
"ossImageUrl": cos_oss_url,
"uploadPathPrefix": "order/"
},
"goEasy": {
"host": goeasy_host,
"appkey": goeasy_appkey
},
"otherConfig": {
"morentouxiang": morentouxiang,
"dashouguize": dashouguize
},
"kefu": {
"link": kefu_link,
"enterpriseId": kefu_enterprise_id
},
"miniappAssets": build_miniapp_assets_payload(request),
}
}
return Response(data)
except Exception as e:
logger.error(f"获取动态配置失败: {str(e)}", exc_info=True)
return Response({"code": 500, "msg": "服务器内部错误"}, status=500)
class HaibaoPeizhiView(APIView):
"""小程序推广海报背景配置(按俱乐部)。"""
permission_classes = [AllowAny]
parser_classes = [JSONParser]
def post(self, request):
try:
from jituan.services.display_config import _display_club_id
from jituan.services.miniapp_assets import get_poster_payload
club_id = _display_club_id(request)
poster = get_poster_payload(club_id)
return Response({
'code': 0,
'msg': 'success',
'data': {
'club_id': club_id,
'poster': poster,
},
})
except Exception as e:
logger.error('获取海报配置失败: %s', e, exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
class PopupConfigView(APIView):
"""
获取指定页面的弹窗配置
请求格式POST { "pageKey": "dashouzhongxin" }
返回格式:{ "code": 0, "data": { "serverTime": "...", "popups": [...] } }
"""
authentication_classes = []
permission_classes = [AllowAny]
def post(self, request):
# 获取参数 pageKey
page_key = request.data.get('pageKey')
if not page_key:
return Response({
'code': 400,
'msg': '缺少参数 pageKey'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 查询页面配置(按俱乐部)
from jituan.services.display_config import get_gonggao_content
from jituan.services.club_context import resolve_club_id_from_request
club_id = resolve_club_id_from_request(request)
try:
popup_page = PopupPage.query.get(club_id=club_id, page_key=page_key, is_active=True)
except PopupPage.DoesNotExist:
# 该页面没有配置弹窗,返回空
return Response({
'code': 0,
'data': {
'serverTime': timezone.now().isoformat(),
'popups': []
}
})
# 4. 查询该页面下所有启用的弹窗(且在有效时间范围内)
now = timezone.now()
popups = PopupConfig.query.filter(
page=popup_page,
is_active=True,
start_time__lte=now,
end_time__gte=now
).prefetch_related('images') # 预加载图片避免N+1查询
# 按 sort_order 排序model Meta 已定义)
# 5. 序列化
serializer = PopupConfigSerializer(popups, many=True)
# 6. 返回数据
return Response({
'code': 0,
'data': {
'serverTime': now.isoformat(),
'ignore_user_mute': popup_page.ignore_user_mute, # 新增
'popups': serializer.data
}
})
class GetWithdrawModeView(APIView):
"""
获取提现模式配置
前端请求 /peizhi/hqtxzsym
返回格式: { "code": 0, "data": { "mode": 1 or 2 } }
"""
permission_classes = [IsAuthenticated] # JWT 认证
def post(self, request, *args, **kwargs):
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_user_club_id
from jituan.services.withdraw_config import get_withdraw_mode
user = getattr(request, 'user', None)
club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None
if not club_id:
club_id = resolve_club_id_from_request(request)
mode = get_withdraw_mode(club_id)
return Response({
'code': 0,
'data': {
'mode': mode,
'club_id': club_id,
}
})
class CheckPhoneAuthView(APIView):
"""
判断当前登录用户是否需要强制手机号认证
规则:
- 已认证过手机号PhoneVerified=True → 不需要
- 未认证时,满足以下任一条件 → 需要认证:
打手:有会员购买记录,或押金 > 0
商家:已是商家(存在商家扩展表)
管事:已是管事(存在管事扩展表)
组长:已是组长(存在组长扩展表)
老板有下单记录alldingdan > 0
子客服已是有效商家客服merchant_staff_member 活跃)
"""
permission_classes = [IsAuthenticated]
@staticmethod
def _phone_auth_completed(user):
"""是否已完成手机号认证(兼容 PhoneVerified 未写入的老数据)。"""
if bool(getattr(user, 'PhoneVerified', False)):
return True
phone = (getattr(user, 'Phone', None) or '').strip()
return len(phone) >= 11
def post(self, request):
uid = getattr(request.user, 'UserUID', None)
user = request.user
if uid:
try:
user = User.query.get(UserUID=uid)
except User.DoesNotExist:
user = request.user
if self._phone_auth_completed(user):
return Response({'code': 0, 'need_auth': False})
if self._dashou_meets_threshold(user):
return Response({'code': 0, 'need_auth': True})
if self._shangjia_meets_threshold(user):
return Response({'code': 0, 'need_auth': True})
if self._guanshi_meets_threshold(user):
return Response({'code': 0, 'need_auth': True})
if self._zuzhang_meets_threshold(user):
return Response({'code': 0, 'need_auth': True})
if self._boss_meets_threshold(user):
return Response({'code': 0, 'need_auth': True})
if self._staff_meets_threshold(user):
return Response({'code': 0, 'need_auth': True})
return Response({'code': 0, 'need_auth': False})
def _staff_meets_threshold(self, user):
"""子客服:已是有效商家客服"""
try:
from merchant_ops.services.authz import get_active_staff_member
return get_active_staff_member(user) is not None
except Exception:
return False
def _dashou_meets_threshold(self, user):
"""打手:有会员记录、押金 > 0、或有过接单/成交订单"""
try:
dashou = UserDashou.query.get(user=user)
except UserDashou.DoesNotExist:
return False
if dashou.yajin and dashou.yajin > 0:
return True
from jituan.services.club_context import resolve_effective_club_id
club_id = resolve_effective_club_id(self.request, user)
if Huiyuangoumai.query.filter(yonghu_id=user.UserUID, club_id=club_id).exists():
return True
if (dashou.jiedanzongliang or 0) > 0 or (dashou.chengjiaozongliang or 0) > 0:
return True
return Order.query.filter(PlayerID=user.UserUID).exists()
def _shangjia_meets_threshold(self, user):
"""商家:已是商家身份"""
return UserShangjia.query.filter(user=user).exists()
def _guanshi_meets_threshold(self, user):
"""管事:已是管事身份"""
return UserGuanshi.query.filter(user=user).exists()
def _zuzhang_meets_threshold(self, user):
"""组长:已是组长身份"""
return UserZuzhang.query.filter(user=user).exists()
def _boss_meets_threshold(self, user):
"""老板:有下单记录"""
try:
boss = UserBoss.query.get(user=user)
except UserBoss.DoesNotExist:
return False
return (boss.alldingdan or 0) > 0

View File

@@ -0,0 +1,553 @@
import io
import os
import re
import time
import random
import secrets
import hashlib
import threading
import urllib.parse
from decimal import Decimal, InvalidOperation
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q, F, Max, Prefetch
from gvsdsdk.fluent import db, func, FQ
from django.core.cache import cache
from django.core.paginator import Paginator
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
# from utils.ip_security import *
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from backend.utils import update_shangjia_daily
from utils.chat_utils import subscribe_merchant_link_chat
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
from ..models import (
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
MiniappScriptScene, MiniappScriptAutoReply,
)
from users.models import (
AdminProfile, UserDashou, UserBoss,
UserShangjia, UserGuanshi, UserZuzhang
)
from users.business_models import User
from orders.models import CommissionRate, Order, MerchantOrderExt
from orders.notice_tasks import dingdan_guangbo
from rank.models import Chenghao, DingdanBiaoqian
from products.models import ShangpinLeixing, Huiyuangoumai
from ..serializers import PopupConfigSerializer
import traceback
import requests
import logging
logger = logging.getLogger(__name__)
class GuanZhuAListView(APIView):
"""
获取关注阿龙页面图片列表
POST /peizhi/gzal
需要JWT认证
返回:
{
"code": 0,
"msg": "success",
"data": {
"images": ["相对URL1", "相对URL2", ...],
"last_update": "2025-03-16 12:34:56",
"visit_count": 123
}
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
from jituan.services.club_context import resolve_effective_club_id
club_id = resolve_effective_club_id(request, request.user)
qs = Tupianpeizhi.query.filter(ImageType=3, club_id=club_id)
if not qs.exists():
return Response({
'code': 0,
'msg': 'success',
'data': {
'images': [],
'last_update': None,
'visit_count': 0
}
}, status=status.HTTP_200_OK)
# 获取所有图片的相对URL
images = list(qs.values_list('ImageURL', flat=True))
# 获取最新更新时间取所有记录的UpdateTime最大值
last_update = qs.aggregate(UpdateTime__max=Max('UpdateTime'))['UpdateTime__max']
last_update_str = last_update.strftime('%Y-%m-%d %H:%M:%S') if last_update else None
# 原子性增加访问次数
# 先更新所有记录,再获取更新后的最大次数
qs.update(AccessCount=F('AccessCount') + 1)
visit_count = qs.aggregate(access_count__max=Max('AccessCount'))['access_count__max'] or 0
return Response({
'code': 0,
'msg': 'success',
'data': {
'images': images,
'last_update': last_update_str,
'visit_count': visit_count
}
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"获取关注阿龙图片列表失败: {e}", exc_info=True)
return Response({
'code': 1,
'msg': '服务器内部错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
#不用缓存一天2000次
class GuanshiQRCodeView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
logger.info("===== 开始生成管事二维码 =====")
current_user = request.user
logger.info(f"当前用户ID: {current_user.UserUID}")
try:
from jituan.services.club_context import resolve_effective_club_id
from utils.weixin_token import get_club_mini_access_token, is_weixin_token_invalid
club_id = resolve_effective_club_id(request, current_user)
try:
guanshi = current_user.GuanshiProfile
logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}")
except UserGuanshi.DoesNotExist:
logger.warning(f"用户 {current_user.UserUID} 不是管事身份")
return Response(
{'code': 403, 'message': '当前用户不是管事身份', 'data': None},
status=status.HTTP_403_FORBIDDEN
)
if guanshi.zhuangtai != 1:
logger.warning(f"用户 {current_user.UserUID} 管事状态异常: {guanshi.zhuangtai}")
return Response(
{'code': 403, 'message': '管事账号状态异常', 'data': None},
status=status.HTTP_403_FORBIDDEN
)
invite_code = guanshi.yaoqingma
if not invite_code:
logger.info(f"用户 {current_user.UserUID} 无邀请码,开始生成")
invite_code = CreateInvitationCode(str(current_user.UserUID))
if not VerifyInvitationCode(invite_code):
logger.error(f"邀请码唯一性校验失败,用户: {current_user.UserUID}")
return Response(
{'code': 500, 'message': '邀请码生成失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
guanshi.yaoqingma = invite_code
guanshi.save(update_fields=['yaoqingma'])
logger.info(f"邀请码生成成功: {invite_code}")
else:
logger.info(f"使用已有邀请码: {invite_code}")
# 按俱乐部使用对应小程序 appid 的 access_token
access_token = get_club_mini_access_token(club_id)
if not access_token:
logger.error("获取 access_token 失败 club_id=%s", club_id)
return Response(
{'code': 500, 'message': '获取微信access_token失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
logger.info(f"获取到 access_token (前10位): {access_token[:10]}... club={club_id}")
encoded_invite = urllib.parse.quote(invite_code, safe='')
# 新小程序:打手端 Tab「我的」旧码 dashouduan 由小程序兼容页转发
page_path = f'pages/fighter/fighter?inviteCode={encoded_invite}'
wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
post_data = {
'path': page_path,
'width': 430,
'auto_color': False,
'line_color': {'r': 0, 'g': 0, 'b': 0}
}
logger.info(f"调用微信接口生成小程序码path: {page_path}")
def _request_qr(token):
try:
return requests.post(
f'https://api.weixin.qq.com/wxa/getwxacode?access_token={token}',
json=post_data, timeout=10,
)
except Exception as e:
logger.error(f"请求微信接口异常: {str(e)}", exc_info=True)
return None
wx_resp = _request_qr(access_token)
if wx_resp is None:
return Response(
{'code': 500, 'message': '调用微信服务失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if wx_resp.headers.get('content-type', '').startswith('application/json'):
try:
err = wx_resp.json()
if is_weixin_token_invalid(err.get('errcode'), err.get('errmsg', '')):
access_token = get_club_mini_access_token(club_id, force_refresh=True)
if access_token:
wx_resp = _request_qr(access_token)
except Exception:
pass
logger.info(f"微信接口响应状态码: {wx_resp.status_code}")
if wx_resp.status_code != 200:
error_info = {}
if wx_resp.headers.get('content-type') == 'application/json':
try:
error_info = wx_resp.json()
except:
pass
errmsg = error_info.get('errmsg', '未知错误')
logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}")
if 'errcode' in error_info:
logger.error(f"微信错误码: {error_info.get('errcode')}")
return Response(
{'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
content_type = wx_resp.headers.get('content-type', '')
if 'image' not in content_type:
try:
error_info = wx_resp.json()
errmsg = error_info.get('errmsg', '未知错误')
logger.error(f"微信接口返回非图片内容: {errmsg}")
return Response(
{'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
except:
logger.error("微信接口返回未知内容无法解析为JSON")
return Response(
{'code': 500, 'message': '生成二维码失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
timestamp = int(time.time())
filename = f"{current_user.UserUID}_{timestamp}.png"
oss_path = f"erweima/guanshi/{club_id}/{filename}"
file_obj = io.BytesIO(wx_resp.content)
logger.info(f"开始上传图片到COS: {oss_path}")
try:
full_url = upload_to_oss(file_obj, oss_path)
logger.info(f"上传成功完整URL: {full_url}")
except Exception as e:
logger.error(f"上传到COS异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '图片上传失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if not full_url:
logger.error("upload_to_oss 返回了空 URL")
return Response(
{'code': 500, 'message': '图片上传失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
cos_domain = getattr(settings, 'COS_DOMAIN', '')
if cos_domain and full_url.startswith(cos_domain):
relative_path = full_url[len(cos_domain):].lstrip('/')
else:
relative_path = oss_path
logger.info(f"相对路径: {relative_path}")
old_url = guanshi.invite_qrcode_url
if old_url:
logger.info(f"删除旧二维码: {old_url}")
try:
delete_from_oss(old_url)
except Exception as e:
logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}")
with transaction.atomic():
guanshi.invite_qrcode_url = relative_path
guanshi.save(update_fields=['invite_qrcode_url'])
logger.info("数据库更新成功")
return Response({
'code': 0,
'message': 'success',
'data': {'url': relative_path}
})
except Exception as e:
logger.error(f"生成二维码过程中发生未捕获异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def get_fresh_access_token(self):
"""
直接请求微信接口获取新的 access_token不缓存保证最新。
"""
appid = getattr(settings, 'WEIXIN_APPID', '')
secret = getattr(settings, 'WEIXIN_SECRET', '')
if not appid or not secret:
logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置")
return None
url = 'https://api.weixin.qq.com/cgi-bin/token'
params = {
'grant_type': 'client_credential',
'appid': appid,
'secret': secret
}
try:
logger.info("正在请求微信接口获取最新 access_token...")
resp = requests.get(url, params=params, timeout=5)
data = resp.json()
new_token = data.get('access_token')
if new_token:
logger.info("成功获取最新 access_token")
return new_token
else:
errcode = data.get('errcode', '未知')
errmsg = data.get('errmsg', '未知错误')
logger.error(f"获取 access_token 失败errcode={errcode}, errmsg={errmsg}")
return None
except Exception as e:
logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True)
return None
class ZuzhangHaibaoView(APIView):
"""
组长专属海报二维码生成接口
路径:/peizhi/zuzhanghb
方法POST
权限JWT认证仅限组长身份
返回:
{
"code": 0,
"message": "success",
"data": {
"url": "erweima/zuzhang/1000001_1234567890.png",
"yaoqingma": "ABC123XYZ"
}
}
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
current_user = request.user
from jituan.services.club_context import resolve_effective_club_id
from utils.weixin_token import get_club_mini_access_token
club_id = resolve_effective_club_id(request, current_user)
# 1. 验证组长身份
try:
zuzhang = current_user.ZuzhangProfile
except UserZuzhang.DoesNotExist:
return Response({
'code': 403,
'message': '当前用户不是组长身份',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 2. 验证组长账号状态
if zuzhang.zhuangtai != 1:
return Response({
'code': 403,
'message': '组长账号状态异常,无法生成海报',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 3. 获取或生成邀请码
invite_code = zuzhang.yaoqingma
if not invite_code:
invite_code = CreateInvitationCode(str(current_user.UserUID))
if not VerifyInvitationCode(invite_code):
logger.error(f"组长邀请码生成失败,用户: {current_user.UserUID}")
return Response({
'code': 500,
'message': '邀请码生成失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
zuzhang.yaoqingma = invite_code
zuzhang.save(update_fields=['yaoqingma'])
# 4. 获取微信 access_token按俱乐部对应小程序
access_token = get_club_mini_access_token(club_id, force_refresh=False)
if not access_token:
return Response({
'code': 500,
'message': '获取微信access_token失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# ===== 修改点:使用 A 接口getwxacode =====
encoded_invite = urllib.parse.quote(invite_code, safe='')
# 新小程序:打手端 Tab「我的」管事注册旧码 guanshiduan 由小程序兼容页转发
page_path = f'pages/fighter/fighter?registerType=guanshi&inviteCode={encoded_invite}'
wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token)
if wx_error and is_weixin_token_invalid(wx_error.get('errcode'), wx_error.get('errmsg', '')):
logger.warning('组长海报二维码 token 失效,强制刷新后重试 club=%s', club_id)
access_token = get_club_mini_access_token(club_id, force_refresh=True)
if not access_token:
return Response({
'code': 500,
'message': '获取微信access_token失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token)
if wx_error:
errmsg = wx_error.get('errmsg', '未知错误')
logger.error(f"微信接口返回错误: {errmsg}")
return Response({
'code': 500,
'message': f'生成二维码失败: {errmsg}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if wx_resp is None:
return Response({
'code': 500,
'message': '调用微信服务失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 5. 上传到腾讯云 COS
timestamp = int(time.time())
filename = f"{current_user.UserUID}_{timestamp}.png"
oss_path = f"erweima/zuzhang/{club_id}/{filename}"
file_obj = io.BytesIO(wx_resp.content)
try:
full_url = upload_to_oss(file_obj, oss_path)
except Exception as e:
logger.error(f"上传到COS失败: {e}")
return Response({
'code': 500,
'message': '图片上传失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if not full_url:
return Response({
'code': 500,
'message': '图片上传失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 6. 获取相对路径
cos_domain = getattr(settings, 'COS_DOMAIN', '')
if cos_domain and full_url.startswith(cos_domain):
relative_path = full_url[len(cos_domain):].lstrip('/')
else:
relative_path = oss_path
# 7. 删除旧二维码文件
old_url = zuzhang.haibao_url
if old_url:
delete_from_oss(old_url)
# 8. 更新数据库中的 haibao_url 字段
with transaction.atomic():
zuzhang.haibao_url = relative_path
zuzhang.save(update_fields=['haibao_url'])
# 9. 返回邀请码和二维码相对路径
return Response({
'code': 0,
'message': 'success',
'data': {
'url': relative_path,
'yaoqingma': invite_code
}
})
def _request_wx_qrcode(self, page_path, access_token):
"""
调用 getwxacode 生成小程序码。
返回 (response, error_dict);成功时 error_dict 为 None。
"""
wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
post_data = {
'path': page_path,
'width': 430,
'auto_color': False,
'line_color': {'r': 0, 'g': 0, 'b': 0},
}
try:
wx_resp = requests.post(wx_url, json=post_data, timeout=10)
except Exception as e:
logger.error(f'请求微信接口失败: {e}')
return None, {'errmsg': '调用微信服务失败'}
if wx_resp.status_code != 200:
error_info = {}
if wx_resp.headers.get('content-type', '').startswith('application/json'):
try:
error_info = wx_resp.json()
except Exception:
pass
return wx_resp, {
'errcode': error_info.get('errcode'),
'errmsg': error_info.get('errmsg', f'HTTP {wx_resp.status_code}'),
}
content_type = wx_resp.headers.get('content-type', '')
if 'image' not in content_type:
try:
error_info = wx_resp.json()
return wx_resp, {
'errcode': error_info.get('errcode'),
'errmsg': error_info.get('errmsg', '未知错误'),
}
except Exception:
return wx_resp, {'errmsg': '微信接口返回未知内容'}
return wx_resp, None

365
config/views/scripts.py Normal file
View File

@@ -0,0 +1,365 @@
import io
import os
import re
import time
import random
import secrets
import hashlib
import threading
import urllib.parse
from decimal import Decimal, InvalidOperation
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q, F, Max, Prefetch
from gvsdsdk.fluent import db, func, FQ
from django.core.cache import cache
from django.core.paginator import Paginator
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
# from utils.ip_security import *
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from backend.utils import update_shangjia_daily
from utils.chat_utils import subscribe_merchant_link_chat
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
from ..models import (
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
MiniappScriptScene, MiniappScriptAutoReply,
)
from users.models import (
AdminProfile, UserDashou, UserBoss,
UserShangjia, UserGuanshi, UserZuzhang
)
from users.business_models import User
from orders.models import CommissionRate, Order, MerchantOrderExt
from orders.notice_tasks import dingdan_guangbo
from rank.models import Chenghao, DingdanBiaoqian
from products.models import ShangpinLeixing, Huiyuangoumai
from ..serializers import PopupConfigSerializer
import traceback
import requests
import logging
logger = logging.getLogger(__name__)
# 小程序客服话术 / 自动回复默认值(原位于 miniapp_config.py按功能组归类到话术模块
MINIAPP_SCRIPT_DEFAULTS = {
'cs_welcome': {
'title': '',
'content': '你好,请问有什么可以帮到您的?',
'confirm_text': '',
'cancel_text': '',
},
'chat_image_confirm': {
'title': '温馨提示',
'content': '平台禁止任何违法违规行为,聊天内容仅限于商家与接单员正常业务对接。严禁赌博、诈骗及其他非法行为,违者后果自负。',
'confirm_text': '确认',
'cancel_text': '取消',
},
'merchant_dispatch_confirm': {
'title': '确认派单',
'content': '确认向该打手派发此订单?',
'confirm_text': '确认',
'cancel_text': '取消',
},
}
def _resolve_script_club_id(request):
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_user_club_id
user = getattr(request, 'user', None)
club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None
if not club_id:
club_id = resolve_club_id_from_request(request)
return club_id or 'xq'
def _merge_script_scene(scene_key, row):
base = dict(MINIAPP_SCRIPT_DEFAULTS.get(scene_key) or {})
if not row:
return base
if row.title:
base['title'] = row.title
if row.content:
base['content'] = row.content
if row.confirm_text:
base['confirm_text'] = row.confirm_text
if row.cancel_text:
base['cancel_text'] = row.cancel_text
return base
class HuashuHqView(APIView):
"""POST /peizhi/huashuhq 批量获取场景话术"""
permission_classes = [IsAuthenticated]
def post(self, request):
scene_keys = request.data.get('scene_keys') or []
if not isinstance(scene_keys, list) or not scene_keys:
return Response({'code': 400, 'msg': 'scene_keys 不能为空', 'data': None}, status=400)
club_id = _resolve_script_club_id(request)
rows = MiniappScriptScene.query.filter(
club_id=club_id, scene_key__in=scene_keys, enabled=True,
)
row_map = {r.scene_key: r for r in rows}
data = {}
for key in scene_keys:
if not isinstance(key, str) or not key.strip():
continue
data[key.strip()] = _merge_script_scene(key.strip(), row_map.get(key.strip()))
return Response({'code': 200, 'msg': 'success', 'data': data})
class HuashuMatchView(APIView):
"""POST /peizhi/huashu_match 客服关键词自动回复匹配"""
permission_classes = [IsAuthenticated]
def post(self, request):
scene_key = (request.data.get('scene_key') or '').strip()
user_text = (request.data.get('user_text') or '').strip()
if scene_key != 'cs_auto_reply':
return Response({'code': 400, 'msg': '不支持的 scene_key', 'data': {'matched': False, 'content': ''}})
if not user_text:
return Response({'code': 200, 'msg': 'success', 'data': {'matched': False, 'content': ''}})
club_id = _resolve_script_club_id(request)
rules = MiniappScriptAutoReply.query.filter(
club_id=club_id, enabled=True,
).order_by('-sort_order', 'id')
text_lower = user_text.lower()
for rule in rules:
kw_raw = (rule.keyword or '').strip()
if not kw_raw:
continue
keywords = [k.strip() for k in kw_raw.split(',') if k.strip()]
for kw in keywords:
if kw.lower() in text_lower or kw in user_text:
reply_type = rule.reply_type or MiniappScriptAutoReply.REPLY_TEXT
return Response({
'code': 200,
'msg': 'success',
'data': {
'matched': True,
'content': rule.reply_content or '',
'reply_type': reply_type,
},
})
return Response({'code': 200, 'msg': 'success', 'data': {'matched': False, 'content': ''}})
# ---------- 后台:客服话术配置(/houtai/hthqhs、/houtai/htxghs ----------
SCRIPT_CONFIG_PERMS = ('kfhs666', '8080a')
SCRIPT_SCENE_KEYS = ('chat_image_confirm', 'cs_welcome', 'merchant_dispatch_confirm')
def _has_script_config_perm(permissions):
return any(code in permissions for code in SCRIPT_CONFIG_PERMS)
def _script_scene_item_type(scene_key):
return 'text_display' if scene_key == 'cs_welcome' else 'confirm_modal'
def _serialize_script_scene(row):
return {
'id': row.id,
'scene_key': row.scene_key,
'item_type': _script_scene_item_type(row.scene_key),
'title': row.title or '',
'content': row.content or '',
'confirm_text': row.confirm_text or '',
'cancel_text': row.cancel_text or '',
'is_active': bool(row.enabled),
}
def _serialize_script_auto_reply(row):
keywords = [k.strip() for k in (row.keyword or '').split(',') if k.strip()]
return {
'id': row.id,
'scene_key': 'cs_auto_reply',
'item_type': 'auto_reply',
'title': '',
'keywords': keywords,
'match_mode': 'contains',
'priority': row.sort_order or 0,
'content': row.reply_content or '',
'is_active': bool(row.enabled),
}
class MiniappScriptHoutaiListView(APIView):
"""POST /houtai/hthqhs 后台获取话术与自动回复配置"""
permission_classes = [IsAuthenticated]
def post(self, request):
from backend.utils import verify_kefu_permission
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
if not _has_script_config_perm(permissions):
return Response({'code': 403, 'msg': '您没有权限访问客服聊天配置'})
club_id = _resolve_script_club_id(request)
scenes = MiniappScriptScene.query.filter(
club_id=club_id, scene_key__in=SCRIPT_SCENE_KEYS,
).order_by('scene_key')
auto_rules = MiniappScriptAutoReply.query.filter(club_id=club_id).order_by('-sort_order', 'id')
items = [_serialize_script_scene(r) for r in scenes]
items.extend(_serialize_script_auto_reply(r) for r in auto_rules)
return Response({'code': 0, 'data': {'items': items, 'club_id': club_id}})
class MiniappScriptHoutaiModifyView(APIView):
"""POST /houtai/htxghs 后台增删改话术与自动回复"""
permission_classes = [IsAuthenticated]
def post(self, request):
from backend.utils import verify_kefu_permission
from jituan.services.display_config import forbid_display_write_in_all_scope
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
if not _has_script_config_perm(permissions):
return Response({'code': 403, 'msg': '您没有权限操作客服聊天配置'})
action = (request.data.get('action') or '').strip()
if not action:
return Response({'code': 400, 'msg': '缺少 action 参数'})
club_id = _resolve_script_club_id(request)
if action == 'delete':
return self._delete_item(request, club_id)
deny = forbid_display_write_in_all_scope(request)
if deny:
return deny
if action in ('create', 'update'):
item_type = (request.data.get('item_type') or '').strip()
if item_type == 'auto_reply':
return self._save_auto_reply(request, club_id, action)
return self._save_scene(request, club_id, action)
return Response({'code': 400, 'msg': f'未知的 action: {action}'})
def _save_scene(self, request, club_id, action):
scene_key = (request.data.get('scene_key') or '').strip()
if scene_key not in SCRIPT_SCENE_KEYS:
return Response({'code': 400, 'msg': '不支持的 scene_key'})
content = (request.data.get('content') or '').strip()
if not content:
return Response({'code': 400, 'msg': '请填写正文内容'})
fields = {
'title': (request.data.get('title') or '').strip(),
'content': content,
'confirm_text': (request.data.get('confirm_text') or '').strip(),
'cancel_text': (request.data.get('cancel_text') or '').strip(),
'enabled': bool(request.data.get('is_active', True)),
}
if action == 'update':
row_id = request.data.get('id')
if not row_id:
return Response({'code': 400, 'msg': '缺少 id'})
row = MiniappScriptScene.query.filter(id=row_id, club_id=club_id).first()
if not row:
return Response({'code': 404, 'msg': '配置不存在'})
for k, v in fields.items():
setattr(row, k, v)
row.save()
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_scene(row)})
existing = MiniappScriptScene.query.filter(club_id=club_id, scene_key=scene_key).first()
if existing:
for k, v in fields.items():
setattr(existing, k, v)
existing.save()
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_scene(existing)})
row = MiniappScriptScene.query.create(club_id=club_id, scene_key=scene_key, **fields)
return Response({'code': 0, 'msg': '创建成功', 'data': _serialize_script_scene(row)})
def _save_auto_reply(self, request, club_id, action):
keywords = request.data.get('keywords') or []
if not isinstance(keywords, list) or not keywords:
return Response({'code': 400, 'msg': '请至少添加一个触发关键词'})
kw_list = [str(k).strip() for k in keywords if str(k).strip()]
if not kw_list:
return Response({'code': 400, 'msg': '请至少添加一个触发关键词'})
content = (request.data.get('content') or '').strip()
if not content:
return Response({'code': 400, 'msg': '请填写回复内容'})
fields = {
'keyword': ','.join(kw_list),
'reply_content': content,
'sort_order': int(request.data.get('priority') or 0),
'enabled': bool(request.data.get('is_active', True)),
'reply_type': MiniappScriptAutoReply.REPLY_TEXT,
}
if action == 'update':
row_id = request.data.get('id')
if not row_id:
return Response({'code': 400, 'msg': '缺少 id'})
row = MiniappScriptAutoReply.query.filter(id=row_id, club_id=club_id).first()
if not row:
return Response({'code': 404, 'msg': '规则不存在'})
for k, v in fields.items():
setattr(row, k, v)
row.save()
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_auto_reply(row)})
row = MiniappScriptAutoReply.query.create(club_id=club_id, **fields)
return Response({'code': 0, 'msg': '创建成功', 'data': _serialize_script_auto_reply(row)})
def _delete_item(self, request, club_id):
from jituan.services.display_config import forbid_display_write_in_all_scope
deny = forbid_display_write_in_all_scope(request)
if deny:
return deny
row_id = request.data.get('id')
if not row_id:
return Response({'code': 400, 'msg': '缺少 id'})
row = MiniappScriptAutoReply.query.filter(id=row_id, club_id=club_id).first()
if row:
row.delete()
return Response({'code': 0, 'msg': '删除成功'})
row = MiniappScriptScene.query.filter(id=row_id, club_id=club_id).first()
if row:
return Response({'code': 400, 'msg': '场景话术不支持删除,请改为禁用'})
return Response({'code': 404, 'msg': '记录不存在'})

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 后处理

View File

@@ -0,0 +1,111 @@
-- ============================================================
-- 店铺相关权限:查询 + 缺失则插入
-- 主权限表Permissiongvsdsdk RBAC角色管理页读取此表
-- 兼容旧表permissionlegacy部分账号仍走 user_role.account_id=手机号)
-- ============================================================
-- ---------- 1. 查询 gvsdsdk 主权限表 ----------
SELECT PermCode, PermName, PermDesc, PermCategory, PermStatus
FROM `Permission`
WHERE PermCode IN (
'999a', '999b', '999c', '999d', '999e',
'1199ab', '1199abc', '1199abd', '1199abe'
)
ORDER BY PermCode;
-- ---------- 2. 查询 legacy 权限表(如仍在使用) ----------
SELECT perm_id, perm_code, perm_name, description
FROM `permission`
WHERE perm_code IN (
'999a', '999b', '999c', '999d', '999e',
'1199ab', '1199abc', '1199abd', '1199abe'
)
ORDER BY perm_code;
-- ---------- 3. 缺失则写入 Permission主表推荐 ----------
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺状态管理', '999a', '店铺管理', 1, '修改店铺封禁/正常状态', NOW()
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '999a');
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺余额与创建', '999b', '店铺管理', 1, '修改可提现余额、添加新店铺', NOW()
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '999b');
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺提现限额', '999c', '店铺管理', 1, '修改每日提现限额及开关', NOW()
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '999c');
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺抽成配置', '999d', '店铺管理', 1, '修改店铺订单收益抽成及默认抽成费率', NOW()
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '999d');
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺基本信息', '999e', '店铺管理', 1, '修改店铺名称、电话、微信号', NOW()
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '999e');
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺商品审核模式', '1199ab', '店铺管理', 1, '查看/修改店铺商品全局审核开关', NOW()
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '1199ab');
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺商品封禁下架', '1199abc', '店铺管理', 1, '封禁、下架、驳回店铺商品/类型审核', NOW()
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '1199abc');
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺商品上架解封', '1199abd', '店铺管理', 1, '上架、解封、审核通过店铺商品/类型', NOW()
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '1199abd');
INSERT INTO `Permission` (`PermUUID`, `TenantUUID`, `PermName`, `PermCode`, `PermCategory`, `PermStatus`, `PermDesc`, `CreateTime`)
SELECT UUID_TO_BIN(UUID()), t.TenantUUID, '店铺商品信息修改', '1199abe', '店铺管理', 1, '修改店铺商品标题、价格、库存等基本信息', NOW()
FROM (SELECT TenantUUID FROM `Permission` WHERE PermStatus = 1 LIMIT 1) t
WHERE NOT EXISTS (SELECT 1 FROM `Permission` p WHERE p.PermCode = '1199abe');
-- ---------- 4. 可选legacy permission 表(若你们仍在用旧角色表) ----------
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
SELECT '999a', '店铺状态管理', '修改店铺封禁/正常状态', NOW(), NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '999a');
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
SELECT '999b', '店铺余额与创建', '修改可提现余额、添加新店铺', NOW(), NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '999b');
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
SELECT '999c', '店铺提现限额', '修改每日提现限额及开关', NOW(), NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '999c');
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
SELECT '999d', '店铺抽成配置', '修改店铺订单收益抽成及默认抽成费率', NOW(), NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '999d');
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
SELECT '999e', '店铺基本信息', '修改店铺名称、电话、微信号', NOW(), NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '999e');
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
SELECT '1199ab', '店铺商品审核模式', '查看/修改店铺商品全局审核开关', NOW(), NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '1199ab');
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
SELECT '1199abc', '店铺商品封禁下架', '封禁、下架、驳回店铺商品/类型审核', NOW(), NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '1199abc');
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
SELECT '1199abd', '店铺商品上架解封', '上架、解封、审核通过店铺商品/类型', NOW(), NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '1199abd');
INSERT INTO `permission` (`perm_code`, `perm_name`, `description`, `CreateTime`, `UpdateTime`)
SELECT '1199abe', '店铺商品信息修改', '修改店铺商品标题、价格、库存等基本信息', NOW(), NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE perm_code = '1199abe');
-- ---------- 5. 插入后再查一遍确认 ----------
SELECT PermCode, PermName, PermStatus FROM `Permission`
WHERE PermCode IN ('999a','999b','999c','999d','999e','1199ab','1199abc','1199abd','1199abe')
ORDER BY PermCode;

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

0
jituan/__init__.py Normal file
View File

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