Files
Django/merchant_ops/services/action_log.py
T
XingQueandCursor 9c029288ba feat(merchant): 订单操作流水表与按真实操作人统计
新增 merchant_order_action_log,在结算/撤单/退款/罚单/派单/换打手成功路径写入真实操作人;提供 action-stats / action-operators 接口供经营数据页筛选统计。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 23:42:39 +08:00

158 lines
5.1 KiB
Python

"""商家订单操作流水:真实操作人落库(支持同一订单多次操作)。"""
import logging
from decimal import Decimal, InvalidOperation
from merchant_ops.constants import (
ACTOR_MERCHANT,
ACTOR_STAFF,
ORDER_ACTION_LABELS,
)
from merchant_ops.models import MerchantOrderActionLog
logger = logging.getLogger(__name__)
def _to_decimal(amount):
if amount is None or amount == '':
return None
try:
return Decimal(str(amount))
except (InvalidOperation, TypeError, ValueError):
return None
def _client_ip(request):
if request is None:
return ''
xff = request.META.get('HTTP_X_FORWARDED_FOR')
if xff:
return xff.split(',')[0].strip()
return request.META.get('REMOTE_ADDR', '') or ''
def resolve_operator_from_request(request, merchant_id):
"""
优先 request._staff_member(子客服代理);否则记为商家本人。
返回 dict: operator_user_id/operator_type/staff_member_id/role_code/role_name
"""
mid = str(merchant_id or '').strip()
staff_member = getattr(request, '_staff_member', None) if request is not None else None
if staff_member is not None:
role = getattr(staff_member, 'role', None)
return {
'operator_user_id': str(getattr(staff_member, 'staff_user_id', '') or ''),
'operator_type': ACTOR_STAFF,
'staff_member_id': getattr(staff_member, 'id', None),
'role_code': getattr(role, 'role_code', '') or '',
'role_name': getattr(role, 'role_name', '') or '',
}
# 商家本人:代理场景下 request.user 已是商家
uid = ''
if request is not None and getattr(request, 'user', None) is not None:
uid = str(getattr(request.user, 'UserUID', '') or '')
if not uid:
uid = mid
return {
'operator_user_id': uid,
'operator_type': ACTOR_MERCHANT,
'staff_member_id': None,
'role_code': 'OWNER',
'role_name': '商家本人',
}
def record_order_action(
*,
merchant_id,
order_id,
action_code,
operator_user_id,
operator_type,
staff_member_id=None,
role_code='',
role_name='',
amount=None,
leixing_id=None,
remark='',
club_id='xq',
request=None,
):
"""写入一条操作流水。失败只打日志,不影响主业务。"""
try:
oid = str(order_id or '').strip()
mid = str(merchant_id or '').strip()
if not oid or not mid or not operator_user_id:
logger.warning(
'操作流水跳过:参数不完整 merchant=%s order=%s action=%s operator=%s',
mid, oid, action_code, operator_user_id,
)
return None
code = int(action_code)
row = MerchantOrderActionLog.objects.create(
club_id=(club_id or 'xq')[:16],
merchant_id=mid[:7],
order_id=oid[:32],
action_code=code,
operator_user_id=str(operator_user_id)[:7],
operator_type=(operator_type or ACTOR_MERCHANT)[:16],
staff_member_id=staff_member_id,
role_code=(role_code or '')[:32],
role_name=(role_name or '')[:64],
amount=_to_decimal(amount),
leixing_id=int(leixing_id) if leixing_id not in (None, '') else None,
remark=(remark or '')[:500],
)
logger.info(
'【商家操作流水】写入成功 id=%s merchant=%s order=%s action=%s(%s) '
'operator=%s type=%s role=%s amount=%s leixing=%s ip=%s',
row.id, mid, oid, code, ORDER_ACTION_LABELS.get(code, '?'),
operator_user_id, operator_type, role_code or role_name,
amount, leixing_id, _client_ip(request),
)
return row
except Exception:
logger.error(
'【商家操作流水】写入失败 merchant=%s order=%s action=%s',
merchant_id, order_id, action_code, exc_info=True,
)
return None
def record_order_action_from_request(
request,
action_code,
order_id,
*,
merchant_id=None,
amount=None,
leixing_id=None,
remark='',
club_id='xq',
):
"""从 request(含 _staff_member)自动识别操作人并落库。"""
mid = str(merchant_id or '').strip()
if not mid and request is not None and getattr(request, 'user', None) is not None:
mid = str(getattr(request.user, 'UserUID', '') or '')
op = resolve_operator_from_request(request, mid)
return record_order_action(
merchant_id=mid,
order_id=order_id,
action_code=action_code,
operator_user_id=op['operator_user_id'],
operator_type=op['operator_type'],
staff_member_id=op.get('staff_member_id'),
role_code=op.get('role_code', ''),
role_name=op.get('role_name', ''),
amount=amount,
leixing_id=leixing_id,
remark=remark,
club_id=club_id,
request=request,
)
def leixing_id_from_order(order):
if order is None:
return None
return getattr(order, 'ProductTypeID', None) or getattr(order, 'leixing_id', None)