统一记录后台敏感操作到 xiugaijilu,并提供管理员操作日志查询接口。

覆盖商家/管事/组长资金与封禁、冻结解冻、后台账号变更;列表接口权限 000001。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-17 01:41:35 +08:00
parent 83ccdff0f5
commit d3f6ff34bb
7 changed files with 545 additions and 10 deletions

View File

@@ -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(),
}
})

View File

@@ -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'])

View File

@@ -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='客服获取打手数据'),

View File

@@ -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': '修改成功'})

View File

@@ -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'),
),
]

View File

@@ -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}"

102
yonghu/xiugai_log.py Normal file
View File

@@ -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)