Files
Django/merchant_ops/services/action_log.py
XingQue 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
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""商家订单操作流水:真实操作人落库(支持同一订单多次操作)。"""
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)