新增 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)
|