diff --git a/backend/urls.py b/backend/urls.py index e389b5d..d4566eb 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -6,8 +6,8 @@ from .view import (GetRolePermissionView, ModifyAdminUserView, KefuGetDashouListView, KefuGetDashouDetailView, KefuUpdateDashouView, KefuGetShangjiaListView, KefuGetShangjiaDetailView, KefuUpdateShangjiaView, GetProductBaseDataView, GetProductListView, SaveProductOrderView, AddProductView, DeleteProductView, GetProductDetailView, UpdateProductView, UpdateMemberView, GetMemberListView, - AddMemberView, GetGuanliListView, GetGuanliDetailView, PromoteGuanshiToZuzhangView, 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, @@ -47,7 +47,9 @@ urlpatterns = [ 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='后台获取组长列表'), diff --git a/backend/utils.py b/backend/utils.py index 93679c5..0b6e31b 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -531,4 +531,39 @@ def pick_penalty_create_params(data): affects = data.get('AffectsGrabbing') if affects is None: affects = data.get('yingxiang_qiangdan', 1) - return penalized_id, reason, fine_amount, affects \ No newline at end of file + return penalized_id, reason, fine_amount, affects + + +# ---------- 修改记录(Xiugaijilu) ---------- +XIUGAI_LEIXING_DASHOU = 2 +XIUGAI_LEIXING_GUANSHI = 3 +XIUGAI_LEIXING_SHANGJIA = 4 +XIUGAI_LEIXING_ZUZHANG = 5 + +XIUGAI_LEIXING_LABEL = { + XIUGAI_LEIXING_DASHOU: '打手', + XIUGAI_LEIXING_GUANSHI: '管事', + XIUGAI_LEIXING_SHANGJIA: '商家', + XIUGAI_LEIXING_ZUZHANG: '组长', +} + + +def write_xiugai_log(*, yonghuid, xiugaiid, leixing, qitashuoming, **extra_fields): + """ + 统一写入 xiugaijilu 修改记录。 + qitashuoming 应详细描述本次操作(尤其是金额、身份、上下级变更等)。 + extra_fields 可传入 xiugaitijiao、shangjiayue、guanshiyue 等模型字段。 + """ + from users.models import Xiugaijilu + + if not qitashuoming: + label = XIUGAI_LEIXING_LABEL.get(leixing, f'类型{leixing}') + qitashuoming = f'【后台】修改{label}信息,操作人{xiugaiid}' + + Xiugaijilu.query.create( + yonghuid=str(yonghuid), + xiugaiid=str(xiugaiid), + leixing=leixing, + qitashuoming=qitashuoming, + **extra_fields, + ) \ No newline at end of file diff --git a/backend/view.py b/backend/view.py index bea34ce..f439479 100644 --- a/backend/view.py +++ b/backend/view.py @@ -50,6 +50,12 @@ from .utils import ( 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 集中导入 @@ -1004,12 +1010,16 @@ class KefuUpdateDashouView(APIView): dashou_profile.yue -= amount dashou_profile.save() after = dashou_profile.yue - Xiugaijilu.query.create( + write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, - leixing=2, + leixing=XIUGAI_LEIXING_DASHOU, xiugaitijiao=after, xiugaitijiaoq=before['yue'], + qitashuoming=( + f'【后台】减少打手可提现余额:{before["yue"]}元 → {after}元,' + f'减少{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' + ), ) return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after)}}) @@ -1020,12 +1030,16 @@ class KefuUpdateDashouView(APIView): dashou_profile.yue += amount dashou_profile.save() after = dashou_profile.yue - Xiugaijilu.query.create( + write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, - leixing=2, + leixing=XIUGAI_LEIXING_DASHOU, xiugaitijiao=after, xiugaitijiaoq=before['yue'], + qitashuoming=( + f'【后台】增加打手可提现余额:{before["yue"]}元 → {after}元,' + f'增加{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' + ), ) return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after)}}) @@ -1039,12 +1053,16 @@ class KefuUpdateDashouView(APIView): dashou_profile.yajin -= amount dashou_profile.save() after = dashou_profile.yajin - Xiugaijilu.query.create( + write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, - leixing=2, + leixing=XIUGAI_LEIXING_DASHOU, yajin=after, yyajin=before['yajin'], + qitashuoming=( + f'【后台】减少打手押金:{before["yajin"]}元 → {after}元,' + f'减少{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' + ), ) return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after)}}) @@ -1055,12 +1073,16 @@ class KefuUpdateDashouView(APIView): dashou_profile.yajin += amount dashou_profile.save() after = dashou_profile.yajin - Xiugaijilu.query.create( + write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, - leixing=2, + leixing=XIUGAI_LEIXING_DASHOU, yajin=after, yyajin=before['yajin'], + qitashuoming=( + f'【后台】增加打手押金:{before["yajin"]}元 → {after}元,' + f'增加{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' + ), ) return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after)}}) @@ -1074,12 +1096,16 @@ class KefuUpdateDashouView(APIView): dashou_profile.jifen -= jifen dashou_profile.save() after = dashou_profile.jifen - Xiugaijilu.query.create( + write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, - leixing=2, + leixing=XIUGAI_LEIXING_DASHOU, jifen=after, yjifen=before['jifen'], + qitashuoming=( + f'【后台】减少打手积分:{before["jifen"]} → {after},' + f'减少{jifen}分,打手ID={dashou_id},操作人={kefu.user.Phone}' + ), ) return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after}}) @@ -1090,12 +1116,16 @@ class KefuUpdateDashouView(APIView): dashou_profile.jifen += jifen dashou_profile.save() after = dashou_profile.jifen - Xiugaijilu.query.create( + write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, - leixing=2, + leixing=XIUGAI_LEIXING_DASHOU, jifen=after, yjifen=before['jifen'], + qitashuoming=( + f'【后台】增加打手积分:{before["jifen"]} → {after},' + f'增加{jifen}分,打手ID={dashou_id},操作人={kefu.user.Phone}' + ), ) return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after}}) @@ -1108,11 +1138,16 @@ class KefuUpdateDashouView(APIView): return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) dashou_profile.zhanghaozhuangtai = new_status dashou_profile.save() - Xiugaijilu.query.create( + status_label = '正常' if new_status == 1 else '封禁' + old_label = '正常' if before['zhanghaozhuangtai'] == 1 else '封禁' + write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, - leixing=2, - qitashuoming=f'修改账号状态为{new_status}', + leixing=XIUGAI_LEIXING_DASHOU, + qitashuoming=( + f'【后台】修改打手账号状态:{old_label} → {status_label},' + f'打手ID={dashou_id},操作人={kefu.user.Phone}' + ), ) return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}}) @@ -1124,11 +1159,16 @@ class KefuUpdateDashouView(APIView): return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) dashou_profile.zaixianzhuangtai = new_status dashou_profile.save() - Xiugaijilu.query.create( + status_label = '在线' if new_status == 1 else '离线' + old_label = '在线' if before['zaixianzhuangtai'] == 1 else '离线' + write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, - leixing=2, - qitashuoming=f'修改在线状态为{new_status}', + leixing=XIUGAI_LEIXING_DASHOU, + qitashuoming=( + f'【后台】修改打手在线状态:{old_label} → {status_label},' + f'打手ID={dashou_id},操作人={kefu.user.Phone}' + ), ) return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}}) @@ -1140,11 +1180,16 @@ class KefuUpdateDashouView(APIView): return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) dashou_profile.zhuangtai = new_status dashou_profile.save() - Xiugaijilu.query.create( + status_label = '空闲' if new_status == 1 else '忙碌' + old_label = '空闲' if before['zhuangtai'] == 1 else '忙碌' + write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, - leixing=2, - qitashuoming=f'修改接单状态为{new_status}', + leixing=XIUGAI_LEIXING_DASHOU, + qitashuoming=( + f'【后台】修改打手接单状态:{old_label} → {status_label},' + f'打手ID={dashou_id},操作人={kefu.user.Phone}' + ), ) return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}}) @@ -1173,13 +1218,17 @@ class KefuUpdateDashouView(APIView): record.daoqi_time = new_daoqi record.huiyuan_zhuangtai = 1 record.save() - Xiugaijilu.query.create( + write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, - leixing=2, + leixing=XIUGAI_LEIXING_DASHOU, huiyuants=days, huiyuan_id=huiyuan_id, - qitashuoming=f'加会员 {huiyuan_id},增加{days}天', + qitashuoming=( + f'【后台】给打手添加会员:会员ID={huiyuan_id}({huiyuan.jieshao}),' + f'增加{days}天,到期时间={record.daoqi_time.strftime("%Y-%m-%d %H:%M:%S")},' + f'打手ID={dashou_id},操作人={kefu.user.Phone}' + ), ) return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}}) @@ -1193,12 +1242,15 @@ class KefuUpdateDashouView(APIView): except Huiyuangoumai.DoesNotExist: return Response({'code': 404, 'msg': '该打手未购买此会员'}) record.delete() - Xiugaijilu.query.create( + write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, - leixing=2, + leixing=XIUGAI_LEIXING_DASHOU, huiyuan_id=huiyuan_id, - qitashuoming=f'移除会员 {huiyuan_id}', + qitashuoming=( + f'【后台】移除打手会员:会员ID={huiyuan_id},' + f'打手ID={dashou_id},操作人={kefu.user.Phone}' + ), ) return Response({'code': 0, 'msg': '减会员成功'}) @@ -1212,11 +1264,14 @@ class KefuUpdateDashouView(APIView): # 可限制合法值,这里只记录日志 dashou_profile.chenghao = new_chenghao dashou_profile.save() - Xiugaijilu.query.create( + write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, - leixing=2, - qitashuoming=f'修改称号:{before["chenghao"] or "无"} → {new_chenghao}', + leixing=XIUGAI_LEIXING_DASHOU, + qitashuoming=( + f'【后台】修改打手称号:{before["chenghao"] or "无"} → {new_chenghao},' + f'打手ID={dashou_id},操作人={kefu.user.Phone}' + ), ) return Response({'code': 0, 'msg': '修改称号成功', 'data': {'new_chenghao': new_chenghao}}) @@ -1490,11 +1545,23 @@ class KefuUpdateShangjiaView(APIView): return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) except: return Response({'code': 400, 'msg': '金额格式错误'}) + old_yue = shangjia.yue new_yue = shangjia.yue + amount if new_yue > 999999999.99: return Response({'code': 400, 'msg': '增加后余额超出上限'}) shangjia.yue = new_yue shangjia.save(update_fields=['yue']) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_SHANGJIA, + shangjiayue=new_yue, + shangjiayueq=old_yue, + qitashuoming=( + f'【后台】增加商家余额:{old_yue}元 → {new_yue}元,增加{amount}元,' + f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' + ), + ) return Response({'code': 0, 'msg': '余额增加成功', 'data': {'new_balance': str(shangjia.yue)}}) # 减少余额 @@ -1508,8 +1575,20 @@ class KefuUpdateShangjiaView(APIView): return Response({'code': 400, 'msg': '金额格式错误'}) if shangjia.yue < amount: return Response({'code': 400, 'msg': '余额不足'}) + old_yue = shangjia.yue shangjia.yue = shangjia.yue - amount shangjia.save(update_fields=['yue']) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_SHANGJIA, + shangjiayue=shangjia.yue, + shangjiayueq=old_yue, + qitashuoming=( + f'【后台】减少商家余额:{old_yue}元 → {shangjia.yue}元,减少{amount}元,' + f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' + ), + ) return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}}) # 封禁 @@ -1518,6 +1597,15 @@ class KefuUpdateShangjiaView(APIView): return Response({'code': 400, 'msg': '商家已是封禁状态'}) shangjia.zhuangtai = 2 shangjia.save(update_fields=['zhuangtai']) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_SHANGJIA, + qitashuoming=( + f'【后台】封禁商家账号:正常 → 禁用,' + f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' + ), + ) return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}}) # 解封 @@ -1526,6 +1614,15 @@ class KefuUpdateShangjiaView(APIView): return Response({'code': 400, 'msg': '商家已是正常状态'}) shangjia.zhuangtai = 1 shangjia.save(update_fields=['zhuangtai']) + write_xiugai_log( + yonghuid=yonghuid, + xiugaiid=kefu.user.Phone, + leixing=XIUGAI_LEIXING_SHANGJIA, + qitashuoming=( + f'【后台】解封商家账号:禁用 → 正常,' + f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' + ), + ) return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}}) @@ -3059,6 +3156,16 @@ class PromoteGuanshiToZuzhangView(APIView): 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, @@ -3119,6 +3226,7 @@ class UpdateGuanliView(APIView): 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') @@ -3130,6 +3238,11 @@ class UpdateGuanliView(APIView): 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: @@ -3137,22 +3250,42 @@ class UpdateGuanliView(APIView): 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. 余额修改(加:4400c;减:4400b)---------- new_yue = request.data.get('yue') @@ -3168,11 +3301,27 @@ class UpdateGuanliView(APIView): if new_yue > old_yue: if '4400c' not in permissions: return Response({'code': 403, 'msg': '无权限增加余额(需要4400c)'}) + action_desc = f'增加{new_yue - old_yue}元' elif new_yue < old_yue: if '4400b' not in permissions: return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'}) - guanshi.yue = new_yue - guanshi.save() + 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') @@ -3180,14 +3329,31 @@ class UpdateGuanliView(APIView): 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') @@ -3277,6 +3443,25 @@ class UpdateGuanliView(APIView): if (record.cishu, record.huiyuan) not in keep_set: record.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': '修改成功'}) @@ -3559,7 +3744,7 @@ class GetZuzhangListView(APIView): if keyword: queryset = queryset.filter( Q(user__UserUID__icontains=keyword) | - Q(user__boss_profile__nickname__icontains=keyword) + Q(user__BossProfile__nickname__icontains=keyword) ) if status is not None and status != '': @@ -3812,12 +3997,16 @@ class UpdateZuzhangView(APIView): # 开始事务 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: @@ -3826,6 +4015,9 @@ class UpdateZuzhangView(APIView): 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: @@ -3834,9 +4026,23 @@ class UpdateZuzhangView(APIView): 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或忽略 # 这里不处理 @@ -3852,11 +4058,27 @@ class UpdateZuzhangView(APIView): if new_val > old_ketixian_jine: if '6600a' not in permissions: return Response({'code': 403, 'msg': '无权限增加可提现金额(需要6600a)'}) + 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)'}) - zuzhang.ketixian_jine = new_val - zuzhang.save(update_fields=['ketixian_jine']) + 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') @@ -3864,14 +4086,31 @@ class UpdateZuzhangView(APIView): 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') @@ -3957,11 +4196,231 @@ class UpdateZuzhangView(APIView): if (record.cishu, record.huiyuan) not in keep_set: record.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}, + }) + + +class GetOperationLogListView(APIView): + """ + 后台操作日志列表(xiugaijilu) + POST /houtai/hqczrz + 权限:000001 超级权限 或 czrz666 + """ + 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 '000001' not in permissions and 'czrz666' not in permissions: + return Response({'code': 403, 'msg': '无权限查看操作日志(需要000001或czrz666)'}) + + yonghuid = request.data.get('yonghuid', '').strip() + xiugaiid = request.data.get('xiugaiid', '').strip() + leixing = request.data.get('leixing') + keyword = request.data.get('keyword', '').strip() + try: + page = max(1, int(request.data.get('page', 1))) + except (ValueError, TypeError): + page = 1 + try: + page_size = min(100, max(1, int(request.data.get('page_size', 20)))) + except (ValueError, TypeError): + page_size = 20 + + qs = Xiugaijilu.query.all().order_by('-CreateTime') + if yonghuid: + qs = qs.filter(yonghuid=yonghuid) + if xiugaiid: + qs = qs.filter(xiugaiid__icontains=xiugaiid) + if leixing not in (None, ''): + try: + qs = qs.filter(leixing=int(leixing)) + except (ValueError, TypeError): + pass + if keyword: + qs = qs.filter(qitashuoming__icontains=keyword) + + total = qs.count() + start = (page - 1) * page_size + records = qs[start:start + page_size] + + def _fmt_decimal(val): + if val is None: + return None + return str(val) + + data_list = [] + for r in records: + data_list.append({ + 'id': r.id, + 'yonghuid': r.yonghuid, + 'xiugaiid': r.xiugaiid, + 'leixing': r.leixing, + 'leixing_label': XIUGAI_LEIXING_LABEL.get(r.leixing, f'类型{r.leixing}'), + 'xiugaitijiao': _fmt_decimal(r.xiugaitijiao), + 'xiugaitijiaoq': _fmt_decimal(r.xiugaitijiaoq), + 'jifen': r.jifen, + 'yjifen': r.yjifen, + 'yajin': _fmt_decimal(r.yajin), + 'yyajin': _fmt_decimal(r.yyajin), + 'shangjiayue': _fmt_decimal(r.shangjiayue), + 'shangjiayueq': _fmt_decimal(r.shangjiayueq), + 'guanshiyue': _fmt_decimal(r.guanshiyue), + 'guanshiyueq': _fmt_decimal(r.guanshiyueq), + 'huiyuants': r.huiyuants, + 'huiyuan_id': r.huiyuan_id, + 'qitashuoming': r.qitashuoming or '', + 'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '', + }) + + return Response({ + 'code': 0, + 'data': { + 'list': data_list, + 'total': total, + 'page': page, + 'page_size': page_size, + }, + }) class GetProductTypeZoneView(APIView):