fix(merchant): 操作统计按订单去重并返回职位备注

同一行为对同一订单只计1单金额取Max;补充 total_order_count/action_count;人员候选展示职位与商家备注。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-16 00:00:07 +08:00
parent 477129de46
commit dac6193d5b

View File

@@ -1,8 +1,10 @@
"""按真实操作人聚合商家订单操作流水统计。"""
from collections import defaultdict
from datetime import datetime
from decimal import Decimal
from django.db.models import Count, Sum, Q
from django.conf import settings
from django.db.models import Count, Max, Sum
from django.utils import timezone
from merchant_ops.constants import (
@@ -34,9 +36,7 @@ def _parse_dt(value, end_of_day=False):
if dt is None:
return None
from django.conf import settings
use_tz = bool(getattr(settings, 'USE_TZ', False))
if use_tz:
if timezone.is_naive(dt):
dt = timezone.make_aware(dt, timezone.get_current_timezone())
@@ -52,6 +52,15 @@ def _money(val):
return f'{Decimal(str(val)):.2f}'
def _dec(val):
if val is None:
return Decimal('0')
try:
return Decimal(str(val))
except Exception:
return Decimal('0')
def build_action_stats_queryset(
merchant_id,
*,
@@ -93,47 +102,92 @@ def build_action_stats_queryset(
return qs
def _aggregate_unique_orders(qs, group_fields):
"""
同一分组下:同一订单同一行为只计 1 单、金额取该订单该行为记录的 Max(amount)。
返回: {group_tuple: {'order_count', 'action_count', 'amount'}}
"""
# 操作次数(含重复)
action_cnt_map = {}
for row in qs.values(*group_fields).annotate(cnt=Count('id')):
key = tuple(row[f] for f in group_fields)
action_cnt_map[key] = int(row['cnt'] or 0)
# 订单去重:每个 (group..., order_id) 只留一笔金额
unique_rows = qs.values(*group_fields, 'order_id').annotate(one_amt=Max('amount'))
result = defaultdict(lambda: {
'order_count': 0,
'action_count': 0,
'amount': Decimal('0'),
})
for row in unique_rows:
key = tuple(row[f] for f in group_fields)
result[key]['order_count'] += 1
result[key]['amount'] += _dec(row['one_amt'])
for key, meta in result.items():
meta['action_count'] = action_cnt_map.get(key, 0)
# 只有重复操作、但 unique 漏掉的分组也补上(理论上 unique 覆盖全部)
for key, cnt in action_cnt_map.items():
if key not in result:
result[key] = {
'order_count': 0,
'action_count': cnt,
'amount': Decimal('0'),
}
return result
def summarize_action_logs(qs):
total = qs.aggregate(cnt=Count('id'), amt=Sum('amount'))
by_action_rows = (
qs.values('action_code')
.annotate(cnt=Count('id'), amt=Sum('amount'))
.order_by('action_code')
)
action_total = qs.aggregate(cnt=Count('id'))
order_total = qs.values('order_id').distinct().count()
# 总额:先按 (action_code, order_id) 去重取 Max(amount),再按订单取各行为中金额的?
# 用户关心订单量去重;总额对「全部」按 (action, order) 去重后的金额求和(不同行为各自算一笔金额)
unique_amt_rows = qs.values('action_code', 'order_id').annotate(one_amt=Max('amount'))
total_amount = sum((_dec(r['one_amt']) for r in unique_amt_rows), Decimal('0'))
by_action_map = _aggregate_unique_orders(qs, ['action_code'])
by_action = []
for row in by_action_rows:
code = int(row['action_code'])
for key in sorted(by_action_map.keys(), key=lambda k: k[0] if k[0] is not None else 0):
code = int(key[0])
meta = by_action_map[key]
by_action.append({
'action_code': code,
'action_label': ORDER_ACTION_LABELS.get(code, f'未知({code})'),
'count': int(row['cnt'] or 0),
'amount': _money(row['amt']),
'order_count': meta['order_count'],
'count': meta['order_count'], # 兼容旧字段:对外主展示订单量
'action_count': meta['action_count'],
'amount': _money(meta['amount']),
})
by_leixing_rows = (
qs.exclude(leixing_id__isnull=True)
.values('leixing_id')
.annotate(cnt=Count('id'), amt=Sum('amount'))
.order_by('-cnt')
by_leixing_map = _aggregate_unique_orders(
qs.exclude(leixing_id__isnull=True),
['leixing_id'],
)
leixing_ids = [r['leixing_id'] for r in by_leixing_rows if r['leixing_id'] is not None]
leixing_ids = [k[0] for k in by_leixing_map.keys() if k[0] is not None]
name_map = {}
if leixing_ids:
for lx in ShangpinLeixing.query.filter(id__in=leixing_ids).only('id', 'jieshao'):
name_map[lx.id] = getattr(lx, 'jieshao', '') or str(lx.id)
by_leixing = []
for row in by_leixing_rows:
lid = row['leixing_id']
for key, meta in sorted(by_leixing_map.items(), key=lambda x: -x[1]['order_count']):
lid = key[0]
by_leixing.append({
'leixing_id': lid,
'leixing_name': name_map.get(lid, str(lid)),
'count': int(row['cnt'] or 0),
'amount': _money(row['amt']),
'order_count': meta['order_count'],
'count': meta['order_count'],
'action_count': meta['action_count'],
'amount': _money(meta['amount']),
})
return {
'total_count': int(total['cnt'] or 0),
'total_amount': _money(total['amt']),
'total_order_count': int(order_total or 0),
'total_count': int(order_total or 0), # 主数字=去重订单量
'total_action_count': int(action_total['cnt'] or 0),
'total_amount': _money(total_amount),
'by_action': by_action,
'by_leixing': by_leixing,
}
@@ -143,22 +197,44 @@ def build_operator_action_stats(merchant_id, filters):
qs = build_action_stats_queryset(merchant_id, **filters)
summary = summarize_action_logs(qs)
people_rows = (
qs.values('operator_user_id', 'operator_type', 'role_code', 'role_name')
.annotate(cnt=Count('id'), amt=Sum('amount'))
.order_by('-cnt')
people_map = _aggregate_unique_orders(
qs,
['operator_user_id', 'operator_type', 'role_code', 'role_name'],
)
# 补职位备注
staff_uids = [
k[0] for k in people_map.keys()
if k[1] == ACTOR_STAFF
]
remark_map = {}
name_map = {}
if staff_uids:
for m in MerchantStaffMember.objects.filter(
merchant_id=str(merchant_id),
staff_user_id__in=staff_uids,
status=1,
).select_related('role'):
remark_map[m.staff_user_id] = m.merchant_remark or ''
name_map[m.staff_user_id] = m.display_name or ''
people = []
for row in people_rows:
for key, meta in sorted(people_map.items(), key=lambda x: -x[1]['order_count']):
uid, op_type, role_code, role_name = key
role_name = role_name or ('商家本人' if op_type == ACTOR_MERCHANT else '')
remark = remark_map.get(uid, '') if op_type == ACTOR_STAFF else ''
display = name_map.get(uid, '') if op_type == ACTOR_STAFF else '商家本人'
people.append({
'operator_user_id': row['operator_user_id'],
'operator_type': row['operator_type'],
'role_code': row['role_code'] or '',
'role_name': row['role_name'] or (
'商家本人' if row['operator_type'] == ACTOR_MERCHANT else ''
),
'count': int(row['cnt'] or 0),
'amount': _money(row['amt']),
'operator_user_id': uid,
'operator_type': op_type,
'role_code': role_code or '',
'role_name': role_name,
'merchant_remark': remark,
'display_name': display,
'order_count': meta['order_count'],
'count': meta['order_count'],
'action_count': meta['action_count'],
'amount': _money(meta['amount']),
})
return {
@@ -172,12 +248,13 @@ def build_operator_action_stats(merchant_id, filters):
def list_operator_candidates(merchant_id):
"""经营数据页可选身份:商家本人 + 子客服列表"""
"""经营数据页可选身份:商家本人 + 子客服(展示职位+备注)"""
items = [{
'operator_user_id': str(merchant_id),
'operator_type': ACTOR_MERCHANT,
'role_code': 'OWNER',
'role_name': '商家本人',
'merchant_remark': '',
'display_name': '商家本人',
'staff_member_id': None,
}]
@@ -188,12 +265,22 @@ def list_operator_candidates(merchant_id):
.order_by('id')
)
for m in members:
role_name = m.role.role_name if m.role else '子客服'
remark = (m.merchant_remark or '').strip()
nick = (m.display_name or '').strip()
parts = [role_name]
if remark:
parts.append(f'备注:{remark}')
elif nick:
parts.append(nick)
parts.append(m.staff_user_id)
items.append({
'operator_user_id': m.staff_user_id,
'operator_type': ACTOR_STAFF,
'role_code': m.role.role_code if m.role else '',
'role_name': m.role.role_name if m.role else '',
'display_name': m.display_name or m.merchant_remark or m.staff_user_id,
'role_name': role_name,
'merchant_remark': remark,
'display_name': ' · '.join(parts),
'staff_member_id': m.id,
})
return items