新增 merchant_order_action_log,在结算/撤单/退款/罚单/派单/换打手成功路径写入真实操作人;提供 action-stats / action-operators 接口供经营数据页筛选统计。 Co-authored-by: Cursor <cursoragent@cursor.com>
158 lines
5.1 KiB
Python
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)
|