diff --git a/houtai/caozuo_rizhi_views.py b/houtai/caozuo_rizhi_views.py new file mode 100644 index 0000000..cf5bc6b --- /dev/null +++ b/houtai/caozuo_rizhi_views.py @@ -0,0 +1,127 @@ +""" +后台操作日志查询(管理员管理页,权限 000001) +""" +from django.core.paginator import Paginator +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from houtai.utils import verify_kefu_permission +from yonghu.models import Xiugaijilu, UserMain +from yonghu.xiugai_log import LEIXING_NAME + + +def _fmt_dt(dt): + if not dt: + return '' + return dt.strftime('%Y-%m-%d %H:%M:%S') + + +def _fmt_dec(v): + if v is None: + return '0.00' + return f'{v:.2f}' + + +def serialize_xiugai_row(row: Xiugaijilu) -> dict: + return { + 'id': row.id, + 'beixiugai_yonghuid': row.yonghuid or '', + 'xiugaizhe_zhanghao': row.xiugaiid or '', + 'leixing': row.leixing, + 'leixing_name': LEIXING_NAME.get(row.leixing, f'类型{row.leixing}'), + 'dashou_yue_qian': _fmt_dec(row.xiugaitijiaoq), + 'dashou_yue_hou': _fmt_dec(row.xiugaitijiao), + 'dashou_jifen_qian': int(row.yjifen or 0), + 'dashou_jifen_hou': int(row.jifen or 0), + 'dashou_yajin_qian': _fmt_dec(row.yyajin), + 'dashou_yajin_hou': _fmt_dec(row.yajin), + 'shangjia_yue_qian': _fmt_dec(row.shangjiayueq), + 'shangjia_yue_hou': _fmt_dec(row.shangjiayue), + 'guanshi_yue_qian': _fmt_dec(row.guanshiyueq), + 'guanshi_yue_hou': _fmt_dec(row.guanshiyue), + 'zuzhang_yue_qian': _fmt_dec(row.zuzhangyueq), + 'zuzhang_yue_hou': _fmt_dec(row.zuzhangyue), + 'huiyuants': int(row.huiyuants or 0), + 'huiyuan_id': row.huiyuan_id or '', + 'qitashuoming': row.qitashuoming or '', + 'create_time': _fmt_dt(row.create_time), + } + + +class GetCaozuoRizhiView(APIView): + """ + 查询某后台账号的操作日志 + POST /houtai/hqczrz + 权限:000001 + 参数: + username: 当前登录客服账号 + phone: 要查看的操作人账号(对应 xiugaijilu.xiugaiid) + page / pageSize + yonghuid: 可选,按被操作用户筛选 + leixing: 可选,按用户类型筛选(2打手/3管事/4商家/5组长/9后台账号/0系统) + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + username = (request.data.get('username') or '').strip() + if not username: + return Response({'code': 400, 'msg': '缺少username'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + if '000001' not in permissions: + return Response({'code': 403, 'msg': '您无权查看操作日志'}) + + phone = (request.data.get('phone') or '').strip() + if not phone: + return Response({'code': 400, 'msg': '缺少要查看的账号 phone'}) + + try: + page = max(1, int(request.data.get('page', 1))) + except (TypeError, ValueError): + page = 1 + try: + page_size = int(request.data.get('pageSize', request.data.get('pagesize', 20))) + except (TypeError, ValueError): + page_size = 20 + page_size = min(max(page_size, 1), 100) + + qs = Xiugaijilu.objects.filter(xiugaiid=phone).order_by('-create_time', '-id') + + yonghuid = (request.data.get('yonghuid') or '').strip() + if yonghuid: + qs = qs.filter(yonghuid=yonghuid) + + leixing_raw = request.data.get('leixing') + if leixing_raw is not None and str(leixing_raw).strip() != '': + try: + qs = qs.filter(leixing=int(leixing_raw)) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': 'leixing 无效'}) + + paginator = Paginator(qs, page_size) + page_obj = paginator.get_page(page) + data_list = [serialize_xiugai_row(row) for row in page_obj] + + # 附带被操作用户头像(可选) + yids = {d['beixiugai_yonghuid'] for d in data_list if d['beixiugai_yonghuid']} + avatar_map = {} + if yids: + for um in UserMain.objects.filter(yonghuid__in=yids).only('yonghuid', 'avatar'): + avatar_map[um.yonghuid] = um.avatar or '' + for d in data_list: + d['touxiang'] = avatar_map.get(d['beixiugai_yonghuid'], '') + + return Response({ + 'code': 0, + 'msg': '获取成功', + 'data': { + 'list': data_list, + 'total': paginator.count, + 'page': page, + 'pageSize': page_size, + 'has_more': page_obj.has_next(), + } + }) diff --git a/houtai/dongjie_views.py b/houtai/dongjie_views.py index 0ed189a..f0d1083 100644 --- a/houtai/dongjie_views.py +++ b/houtai/dongjie_views.py @@ -66,6 +66,22 @@ class DongjieUserConfigView(APIView): try: with transaction.atomic(): data = save_user_freeze_settings(user_main, leixing, request.data) + from yonghu.xiugai_log import ( + write_xiugai_log, FREEZE_TO_XIUGAI_LEIXING, FREEZE_LEIXING_NAME, + ) + role_name = FREEZE_LEIXING_NAME.get(leixing, str(leixing)) + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=kefu.user.phone, + leixing=FREEZE_TO_XIUGAI_LEIXING.get(leixing, 0), + qitashuoming=( + f'保存用户冻结配置[{role_name}]:' + f'单独开关={data.get("dandu_dongjie_enabled")},' + f'模式={data.get("dongjie_mode")},' + f'比例={data.get("dongjie_bili")},' + f'固定额={data.get("dongjie_guding_jine")}' + ), + ) return Response({'code': 0, 'msg': '保存成功', 'data': data}) except ValueError as e: return Response({'code': 400, 'msg': str(e)}) @@ -81,6 +97,35 @@ class DongjieUserConfigView(APIView): user_main, leixing, jine, operator_id=username, beizhu=beizhu, ) + from yonghu.xiugai_log import ( + write_xiugai_log, FREEZE_TO_XIUGAI_LEIXING, FREEZE_LEIXING_NAME, + ) + role_name = FREEZE_LEIXING_NAME.get(leixing, str(leixing)) + xiugai_leixing = FREEZE_TO_XIUGAI_LEIXING.get(leixing, 0) + log_kwargs = { + 'yonghuid': yonghuid, + 'operator_phone': kefu.user.phone, + 'leixing': xiugai_leixing, + 'qitashuoming': ( + f'管理员解冻[{role_name}] {result["unfreeze_jine"]},' + f'余额 {result["balance_before"]}→{result["balance_after"]},' + f'冻结池 {result["dongjie_chi_before"]}→{result["dongjie_chi_after"]}' + + (f';备注:{beizhu}' if beizhu else '') + ), + } + if leixing == 1: + log_kwargs['dashou_yue_before'] = result['balance_before'] + log_kwargs['dashou_yue_after'] = result['balance_after'] + elif leixing == 5: + log_kwargs['yajin_before'] = result['balance_before'] + log_kwargs['yajin_after'] = result['balance_after'] + elif leixing == 2: + log_kwargs['guanshi_yue_before'] = result['balance_before'] + log_kwargs['guanshi_yue_after'] = result['balance_after'] + elif leixing == 6: + log_kwargs['shangjia_yue_before'] = result['balance_before'] + log_kwargs['shangjia_yue_after'] = result['balance_after'] + write_xiugai_log(**log_kwargs) info = get_user_freeze_info(user_main, leixing) info['balance'] = str(result['balance_after']) info['dongjie_chi'] = str(result['dongjie_chi_after']) diff --git a/houtai/urls.py b/houtai/urls.py index 15c0a30..ff5bca2 100644 --- a/houtai/urls.py +++ b/houtai/urls.py @@ -19,6 +19,7 @@ from .view import GetClubConfigView, GetCrossOrderListView, PartnerGetOrderDataV KhgglView, ShgxgsjView, ZxkfghdsView, KptxwztjbView from .dongjie_views import DongjieUserConfigView from .tixian_profit_views import TixianChouchengProfitView +from .caozuo_rizhi_views import GetCaozuoRizhiView from .huashu_views import HuashuListAPIView, HuashuModifyAPIView from .xieyi_views import ( XieyiListAPIView, XieyiModifyAPIView, XieyiImageUploadAPIView, @@ -56,6 +57,7 @@ urlpatterns = [ path('hqyhjsgl', GetAdminRolesView.as_view(), name='管理用户页面获取用户角色'), path('hqjsyhsj', GetAdminUserListView.as_view(), name='管理用户页面获取用户数据'), + path('hqczrz', GetCaozuoRizhiView.as_view(), name='管理用户页面获取操作日志'), path('xghtyhsj', ModifyAdminUserView.as_view(), name='管理用户页面修改用户数据'), path('tjyhjs', AddAdminUserView.as_view(), name='管理用户页面添加用户'), path('kefuhqdslb', KefuGetDashouListView.as_view(), name='客服获取打手数据'), diff --git a/houtai/view.py b/houtai/view.py index f3c063a..3981552 100644 --- a/houtai/view.py +++ b/houtai/view.py @@ -4048,6 +4048,13 @@ class GetAdminUserListView(APIView): # 构建返回数据 data_list = [] + phones = [user.phone for user in page_obj] + log_counts = { + row['xiugaiid']: row['c'] + for row in Xiugaijilu.objects.filter(xiugaiid__in=phones) + .values('xiugaiid') + .annotate(c=Count('id')) + } for user in page_obj: # 获取用户的所有角色 user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role') @@ -4057,6 +4064,7 @@ class GetAdminUserListView(APIView): 'nicheng': user.kefu_profile.nicheng, 'roles': roles_data, 'status': user.kefu_profile.zhuangtai, + 'jilushuliang': int(log_counts.get(user.phone, 0)), 'create_time': user.create_time.strftime('%Y-%m-%d %H:%M:%S'), 'update_time': user.update_time.strftime('%Y-%m-%d %H:%M:%S'), }) @@ -4109,32 +4117,60 @@ class ModifyAdminUserView(APIView): status = request.data.get('status') password = request.data.get('password') erjimima = request.data.get('erjimima') - if nicheng is not None: + old_nicheng = target_kefu.nicheng + old_status = target_kefu.zhuangtai + changed = [] + if nicheng is not None and nicheng != old_nicheng: target_kefu.nicheng = nicheng + changed.append(f'昵称 {old_nicheng}→{nicheng}') if status is not None: - target_kefu.zhuangtai = int(status) + new_status = int(status) + if new_status != old_status: + target_kefu.zhuangtai = new_status + changed.append(f'状态 {old_status}→{new_status}({"正常" if new_status == 1 else "禁用"})') if password: if len(password) < 6: return Response({'code': 400, 'msg': '密码至少6位'}) target_user.password = password + changed.append('修改登录密码') if erjimima: if len(erjimima) < 6: return Response({'code': 400, 'msg': '二级密码至少6位'}) target_kefu.erjimima = erjimima # 假设二级密码也需要加密 + changed.append('修改二级密码') target_user.save() target_kefu.save() + if changed: + from yonghu.xiugai_log import write_xiugai_log, LEIXING_KEFU + write_xiugai_log( + yonghuid=target_user.yonghuid, + operator_phone=kefu.user.phone, + leixing=LEIXING_KEFU, + qitashuoming=f'后台账号[{target_phone}]:' + ';'.join(changed), + ) 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': '请选择角色'}) + added = [] for rc in role_codes: try: role = Role.objects.get(role_code=rc) except Role.DoesNotExist: continue - UserRole.objects.get_or_create(account_id=target_phone, role=role) + _, created = UserRole.objects.get_or_create(account_id=target_phone, role=role) + if created: + added.append(f'{role.role_name}({rc})') + if added: + from yonghu.xiugai_log import write_xiugai_log, LEIXING_KEFU + write_xiugai_log( + yonghuid=target_user.yonghuid, + operator_phone=kefu.user.phone, + leixing=LEIXING_KEFU, + qitashuoming=f'后台账号[{target_phone}]添加角色:{",".join(added)}', + ) return Response({'code': 0, 'msg': '角色添加成功'}) elif action == 'remove_user_role': @@ -4145,7 +4181,15 @@ class ModifyAdminUserView(APIView): role = Role.objects.get(role_code=role_code) except Role.DoesNotExist: return Response({'code': 404, 'msg': '角色不存在'}) - UserRole.objects.filter(account_id=target_phone, role=role).delete() + deleted, _ = UserRole.objects.filter(account_id=target_phone, role=role).delete() + if deleted: + from yonghu.xiugai_log import write_xiugai_log, LEIXING_KEFU + write_xiugai_log( + yonghuid=target_user.yonghuid, + operator_phone=kefu.user.phone, + leixing=LEIXING_KEFU, + qitashuoming=f'后台账号[{target_phone}]移除角色:{role.role_name}({role_code})', + ) return Response({'code': 0, 'msg': '角色移除成功'}) else: @@ -4178,6 +4222,9 @@ class AddAdminUserView(APIView): if not all([phone, password, erjimima, nicheng]): return Response({'code': 400, 'msg': '请填写完整信息'}) + # UserMain.phone / UserRole.account_id 均为 max_length=11 + if len(phone) > 11: + return Response({'code': 400, 'msg': '账号最长11位,请使用手机号'}) if len(password) < 6 or len(erjimima) < 6: return Response({'code': 400, 'msg': '密码和二级密码至少6位'}) @@ -4222,6 +4269,14 @@ class AddAdminUserView(APIView): except Role.DoesNotExist: pass + from yonghu.xiugai_log import write_xiugai_log, LEIXING_KEFU + write_xiugai_log( + yonghuid=user_main.yonghuid, + operator_phone=kefu.user.phone, + leixing=LEIXING_KEFU, + qitashuoming=f'新增后台账号[{phone}]昵称={nicheng},角色={role_codes}', + ) + return Response({'code': 0, 'msg': '添加成功'}) @@ -4706,6 +4761,7 @@ class KefuUpdateDashouView(APIView): leixing=2, xiugaitijiao=after, xiugaitijiaoq=before['yue'], + qitashuoming=f'打手减余额 {amount},{before["yue"]} → {after}', ) return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after)}}) @@ -4722,6 +4778,7 @@ class KefuUpdateDashouView(APIView): leixing=2, xiugaitijiao=after, xiugaitijiaoq=before['yue'], + qitashuoming=f'打手加余额 {amount},{before["yue"]} → {after}', ) return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after)}}) @@ -4741,6 +4798,7 @@ class KefuUpdateDashouView(APIView): leixing=2, yajin=after, yyajin=before['yajin'], + qitashuoming=f'打手减押金 {amount},{before["yajin"]} → {after}', ) return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after)}}) @@ -4757,6 +4815,7 @@ class KefuUpdateDashouView(APIView): leixing=2, yajin=after, yyajin=before['yajin'], + qitashuoming=f'打手加押金 {amount},{before["yajin"]} → {after}', ) return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after)}}) @@ -4776,6 +4835,7 @@ class KefuUpdateDashouView(APIView): leixing=2, jifen=after, yjifen=before['jifen'], + qitashuoming=f'打手减积分 {jifen},{before["jifen"]} → {after}', ) return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after}}) @@ -4792,6 +4852,7 @@ class KefuUpdateDashouView(APIView): leixing=2, jifen=after, yjifen=before['jifen'], + qitashuoming=f'打手加积分 {jifen},{before["jifen"]} → {after}', ) return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after}}) @@ -4808,7 +4869,7 @@ class KefuUpdateDashouView(APIView): yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, - qitashuoming=f'修改账号状态为{new_status}', + qitashuoming=f'打手账号{"解封" if new_status == 1 else "封禁"},状态→{new_status}', ) return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}}) @@ -5186,11 +5247,21 @@ class KefuUpdateShangjiaView(APIView): return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) except: return Response({'code': 400, 'msg': '金额格式错误'}) - new_yue = shangjia.yue + amount + old_yue = shangjia.yue + new_yue = old_yue + amount if new_yue > 999999999.99: return Response({'code': 400, 'msg': '增加后余额超出上限'}) shangjia.yue = new_yue shangjia.save(update_fields=['yue']) + from yonghu.xiugai_log import write_xiugai_log, LEIXING_SHANGJIA + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=kefu.user.phone, + leixing=LEIXING_SHANGJIA, + shangjia_yue_before=old_yue, + shangjia_yue_after=new_yue, + qitashuoming=f'商家加余额 {amount},{old_yue} → {new_yue}', + ) return Response({'code': 0, 'msg': '余额增加成功', 'data': {'new_balance': str(shangjia.yue)}}) # 减少余额 @@ -5202,26 +5273,53 @@ class KefuUpdateShangjiaView(APIView): return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) except: return Response({'code': 400, 'msg': '金额格式错误'}) - if shangjia.yue < amount: + old_yue = shangjia.yue + if old_yue < amount: return Response({'code': 400, 'msg': '余额不足'}) - shangjia.yue = shangjia.yue - amount + new_yue = old_yue - amount + shangjia.yue = new_yue shangjia.save(update_fields=['yue']) + from yonghu.xiugai_log import write_xiugai_log, LEIXING_SHANGJIA + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=kefu.user.phone, + leixing=LEIXING_SHANGJIA, + shangjia_yue_before=old_yue, + shangjia_yue_after=new_yue, + qitashuoming=f'商家减余额 {amount},{old_yue} → {new_yue}', + ) return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}}) # 封禁 if action == 'ban': if shangjia.zhuangtai == 2: return Response({'code': 400, 'msg': '商家已是封禁状态'}) + old_zt = shangjia.zhuangtai shangjia.zhuangtai = 2 shangjia.save(update_fields=['zhuangtai']) + from yonghu.xiugai_log import write_xiugai_log, LEIXING_SHANGJIA + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=kefu.user.phone, + leixing=LEIXING_SHANGJIA, + qitashuoming=f'商家封禁,状态 {old_zt} → 2', + ) return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}}) # 解封 if action == 'unban': if shangjia.zhuangtai == 1: return Response({'code': 400, 'msg': '商家已是正常状态'}) + old_zt = shangjia.zhuangtai shangjia.zhuangtai = 1 shangjia.save(update_fields=['zhuangtai']) + from yonghu.xiugai_log import write_xiugai_log, LEIXING_SHANGJIA + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=kefu.user.phone, + leixing=LEIXING_SHANGJIA, + qitashuoming=f'商家解封,状态 {old_zt} → 1', + ) return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}}) @@ -6506,6 +6604,11 @@ class UpdateGuanliView(APIView): guanshi = user.guanshi_profile boss = user.boss_profile if hasattr(user, 'boss_profile') else None + old_yue = guanshi.yue + old_zhuangtai = guanshi.zhuangtai + old_kaioi = guanshi.kaioi_ewai_xiane + old_ewai = guanshi.ewai_xiane + # ---------- 1. 基础信息修改(需要4400a)---------- nickname = request.data.get('nickname') dianhua = request.data.get('dianhua') @@ -6550,7 +6653,6 @@ class UpdateGuanliView(APIView): 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)'}) @@ -6580,10 +6682,12 @@ class UpdateGuanliView(APIView): perm_amount = request.data.get('permanent_amount') custom_first = request.data.get('custom_first_dividend') extra_map = request.data.get('extra_dividend_map') + fenhong_changed = False 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)'}) + fenhong_changed = True # 4.1 永久分红 if perm_enabled is not None: @@ -6663,6 +6767,48 @@ class UpdateGuanliView(APIView): if (record.cishu, record.huiyuan) not in keep_set: record.delete() + # ---------- 敏感操作写日志 ---------- + from yonghu.xiugai_log import write_xiugai_log, LEIXING_GUANSHI + op_phone = kefu.user.phone + if guanshi.yue != old_yue: + delta = guanshi.yue - old_yue + act = '加余额' if delta > 0 else '减余额' + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=op_phone, + leixing=LEIXING_GUANSHI, + guanshi_yue_before=old_yue, + guanshi_yue_after=guanshi.yue, + qitashuoming=f'管事{act} {abs(delta)},{old_yue} → {guanshi.yue}', + ) + if guanshi.zhuangtai != old_zhuangtai: + zt_txt = '解封' if guanshi.zhuangtai == 1 else '封禁' + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=op_phone, + leixing=LEIXING_GUANSHI, + qitashuoming=f'管事{zt_txt},状态 {old_zhuangtai} → {guanshi.zhuangtai}', + ) + if (kaioi_ewai is not None or ewai_val is not None) and ( + guanshi.kaioi_ewai_xiane != old_kaioi or guanshi.ewai_xiane != old_ewai + ): + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=op_phone, + leixing=LEIXING_GUANSHI, + qitashuoming=( + f'管事提现限额变更:开关 {old_kaioi}→{guanshi.kaioi_ewai_xiane},' + f'额外限额 {old_ewai}→{guanshi.ewai_xiane}' + ), + ) + if fenhong_changed: + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=op_phone, + leixing=LEIXING_GUANSHI, + qitashuoming='管事分红策略变更(永久/首次定制/额外次数)', + ) + return Response({'code': 0, 'msg': '修改成功'}) @@ -6900,6 +7046,25 @@ class UpdateWithdrawSettingsView(APIView): bool(request.data.get('dongjie_quanju_enabled', False)), request.data.get('dongjie_roles', []), ) + from yonghu.xiugai_log import write_xiugai_log, LEIXING_SYSTEM + write_xiugai_log( + yonghuid='0000000', + operator_phone=kefu.user.phone, + leixing=LEIXING_SYSTEM, + qitashuoming=( + f'修改全局冻结配置:开关={request.data.get("dongjie_quanju_enabled")},' + f'角色配置={request.data.get("dongjie_roles")}' + ), + ) + + if withdraw_mode is not None: + from yonghu.xiugai_log import write_xiugai_log, LEIXING_SYSTEM + write_xiugai_log( + yonghuid='0000000', + operator_phone=kefu.user.phone, + leixing=LEIXING_SYSTEM, + qitashuoming=f'修改提现模式为 {withdraw_mode}(1自动/2手动)', + ) # 注意:TixianRiTongji 表由提现流程自动更新,此处不直接修改 return Response({'code': 0, 'msg': '设置修改成功'}) @@ -7215,6 +7380,9 @@ class UpdateZuzhangView(APIView): # 获取原始数据(用于对比金额变化) old_ketixian_jine = zuzhang.ketixian_jine + old_zhuangtai = zuzhang.zhuangtai + old_kaioi = zuzhang.kaioi_ewai_tixian + old_ewai = zuzhang.ewai_tixian_xiane # 开始事务 with transaction.atomic(): @@ -7284,10 +7452,12 @@ class UpdateZuzhangView(APIView): perm_amount = request.data.get('permanent_amount') custom_first = request.data.get('custom_first_dividend') extra_map = request.data.get('extra_dividend_map') + fenhong_changed = False 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)'}) + fenhong_changed = True # 4.1 永久分红(额外分红开关和金额) if perm_enabled is not None: @@ -7363,7 +7533,51 @@ class UpdateZuzhangView(APIView): if (record.cishu, record.huiyuan) not in keep_set: record.delete() - # 保存组长其他可能修改的字段(例如电话、微信没有,忽略) + # 保存状态字段 + if zhuangtai is not None: + zuzhang.save(update_fields=['zhuangtai']) + + # ---------- 敏感操作写日志 ---------- + from yonghu.xiugai_log import write_xiugai_log, LEIXING_ZUZHANG + op_phone = kefu.user.phone + if zuzhang.ketixian_jine != old_ketixian_jine: + delta = zuzhang.ketixian_jine - old_ketixian_jine + act = '加可提现' if delta > 0 else '减可提现' + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=op_phone, + leixing=LEIXING_ZUZHANG, + zuzhang_yue_before=old_ketixian_jine, + zuzhang_yue_after=zuzhang.ketixian_jine, + qitashuoming=f'组长{act} {abs(delta)},{old_ketixian_jine} → {zuzhang.ketixian_jine}', + ) + if zuzhang.zhuangtai != old_zhuangtai: + zt_txt = '解封' if zuzhang.zhuangtai == 1 else '封禁' + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=op_phone, + leixing=LEIXING_ZUZHANG, + qitashuoming=f'组长{zt_txt},状态 {old_zhuangtai} → {zuzhang.zhuangtai}', + ) + if (kaioi_ewai is not None or ewai_val is not None) and ( + zuzhang.kaioi_ewai_tixian != old_kaioi or zuzhang.ewai_tixian_xiane != old_ewai + ): + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=op_phone, + leixing=LEIXING_ZUZHANG, + qitashuoming=( + f'组长提现限额变更:开关 {old_kaioi}→{zuzhang.kaioi_ewai_tixian},' + f'额外限额 {old_ewai}→{zuzhang.ewai_tixian_xiane}' + ), + ) + if fenhong_changed: + write_xiugai_log( + yonghuid=yonghuid, + operator_phone=op_phone, + leixing=LEIXING_ZUZHANG, + qitashuoming='组长分红策略变更(永久/首次定制/额外次数)', + ) return Response({'code': 0, 'msg': '修改成功'}) diff --git a/yonghu/migrations/0026_xiugaijilu_zuzhang_and_indexes.py b/yonghu/migrations/0026_xiugaijilu_zuzhang_and_indexes.py new file mode 100644 index 0000000..283e392 --- /dev/null +++ b/yonghu/migrations/0026_xiugaijilu_zuzhang_and_indexes.py @@ -0,0 +1,37 @@ +# Generated manually for Xiugaijilu zuzhang fields + indexes + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yonghu', '0025_tixianshenhejilu_dakuan_mch_id'), + ] + + operations = [ + migrations.AddField( + model_name='xiugaijilu', + name='zuzhangyue', + field=models.DecimalField( + decimal_places=2, default=0.00, max_digits=12, + verbose_name='修改后的组长可提现金额', + ), + ), + migrations.AddField( + model_name='xiugaijilu', + name='zuzhangyueq', + field=models.DecimalField( + decimal_places=2, default=0.00, max_digits=12, + verbose_name='修改前的组长可提现金额', + ), + ), + migrations.AddIndex( + model_name='xiugaijilu', + index=models.Index(fields=['xiugaiid', '-create_time'], name='idx_xiugai_op_time'), + ), + migrations.AddIndex( + model_name='xiugaijilu', + index=models.Index(fields=['yonghuid', '-create_time'], name='idx_xiugai_target_time'), + ), + ] diff --git a/yonghu/models.py b/yonghu/models.py index a9d428c..02da517 100644 --- a/yonghu/models.py +++ b/yonghu/models.py @@ -623,6 +623,10 @@ class Xiugaijilu(models.Model): guanshiyue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='修改后的管事余额') guanshiyueq = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='修改前的管事余额') + # 组长可提现余额 + zuzhangyue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='修改后的组长可提现金额') + zuzhangyueq = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='修改前的组长可提现金额') + # 会员相关 huiyuants = models.IntegerField(default=0, verbose_name='添加会员天数') huiyuan_id = models.CharField(max_length=6, null=True, blank=True, verbose_name='会员ID') @@ -636,6 +640,10 @@ class Xiugaijilu(models.Model): db_table = 'xiugaijilu' verbose_name = '修改记录表' verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['xiugaiid', '-create_time'], name='idx_xiugai_op_time'), + models.Index(fields=['yonghuid', '-create_time'], name='idx_xiugai_target_time'), + ] def __str__(self): return f"修改记录-用户{self.yonghuid}" diff --git a/yonghu/xiugai_log.py b/yonghu/xiugai_log.py new file mode 100644 index 0000000..4fc8c44 --- /dev/null +++ b/yonghu/xiugai_log.py @@ -0,0 +1,102 @@ +""" +后台敏感操作统一写入 xiugaijilu(修改/操作日志)。 + +leixing 约定: + 0=系统配置, 2=打手, 3=管事, 4=商家, 5=组长, 9=后台账号 +""" +from __future__ import annotations + +from decimal import Decimal +from typing import Any, Optional + +from .models import Xiugaijilu + +LEIXING_SYSTEM = 0 +LEIXING_DASHOU = 2 +LEIXING_GUANSHI = 3 +LEIXING_SHANGJIA = 4 +LEIXING_ZUZHANG = 5 +LEIXING_KEFU = 9 + +LEIXING_NAME = { + LEIXING_SYSTEM: '系统配置', + LEIXING_DASHOU: '打手', + LEIXING_GUANSHI: '管事', + LEIXING_SHANGJIA: '商家', + LEIXING_ZUZHANG: '组长', + LEIXING_KEFU: '后台账号', +} + +# 冻结提现类型 → 操作日志用户类型 +FREEZE_TO_XIUGAI_LEIXING = { + 1: LEIXING_DASHOU, # 打手佣金 + 2: LEIXING_GUANSHI, # 管事 + 5: LEIXING_DASHOU, # 打手押金 + 6: LEIXING_SHANGJIA, # 商家 +} + +FREEZE_LEIXING_NAME = { + 1: '打手佣金', + 2: '管事分红', + 5: '打手押金', + 6: '商家余额', +} + + +def _d(v: Any) -> Decimal: + if v is None: + return Decimal('0.00') + if isinstance(v, Decimal): + return v + return Decimal(str(v)) + + +def write_xiugai_log( + *, + yonghuid: str, + operator_phone: str, + leixing: int, + qitashuoming: str = '', + dashou_yue_before=None, + dashou_yue_after=None, + jifen_before=None, + jifen_after=None, + yajin_before=None, + yajin_after=None, + shangjia_yue_before=None, + shangjia_yue_after=None, + guanshi_yue_before=None, + guanshi_yue_after=None, + zuzhang_yue_before=None, + zuzhang_yue_after=None, + huiyuants: int = 0, + huiyuan_id: Optional[str] = None, +) -> Xiugaijilu: + """写入一条操作日志;金额类字段仅在传入时覆盖默认 0。""" + kwargs = { + 'yonghuid': str(yonghuid or '')[:7], + 'xiugaiid': str(operator_phone or '')[:30], + 'leixing': int(leixing), + 'qitashuoming': (qitashuoming or '')[:2000] or None, + 'huiyuants': int(huiyuants or 0), + 'huiyuan_id': (str(huiyuan_id)[:6] if huiyuan_id else None), + } + if dashou_yue_before is not None or dashou_yue_after is not None: + kwargs['xiugaitijiaoq'] = _d(dashou_yue_before) + kwargs['xiugaitijiao'] = _d(dashou_yue_after) + if jifen_before is not None or jifen_after is not None: + kwargs['yjifen'] = int(jifen_before or 0) + kwargs['jifen'] = int(jifen_after or 0) + if yajin_before is not None or yajin_after is not None: + kwargs['yyajin'] = _d(yajin_before) + kwargs['yajin'] = _d(yajin_after) + if shangjia_yue_before is not None or shangjia_yue_after is not None: + kwargs['shangjiayueq'] = _d(shangjia_yue_before) + kwargs['shangjiayue'] = _d(shangjia_yue_after) + if guanshi_yue_before is not None or guanshi_yue_after is not None: + kwargs['guanshiyueq'] = _d(guanshi_yue_before) + kwargs['guanshiyue'] = _d(guanshi_yue_after) + if zuzhang_yue_before is not None or zuzhang_yue_after is not None: + kwargs['zuzhangyueq'] = _d(zuzhang_yue_before) + kwargs['zuzhangyue'] = _d(zuzhang_yue_after) + return Xiugaijilu.objects.create(**kwargs)