feat: 订单列表/详情返回自动结算倒计时字段,并加小程序结算率接口
仅 eligible 结算中订单下发预计时间;支持 auto_settle_pending 筛选;新增 /dingdan/wo-de-deal-stat。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -287,3 +287,48 @@ def serialize_dashou_stat(row) -> dict:
|
|||||||
'deal_rate': round(c / n, 6) if n else 0,
|
'deal_rate': round(c / n, 6) if n else 0,
|
||||||
'fined_rate': round(f / n, 6) if n else 0,
|
'fined_rate': round(f / n, 6) if n else 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def miniapp_merchant_deal_payload(row) -> dict | None:
|
||||||
|
"""小程序商家订单列表顶:有样本才返回;文案字段供前端标「结算率」。"""
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
n = int(row.order_count or 0)
|
||||||
|
if n <= 0:
|
||||||
|
return None
|
||||||
|
full = serialize_merchant_stat(row)
|
||||||
|
out = {
|
||||||
|
'role': 'merchant',
|
||||||
|
'order_count': full['order_count'],
|
||||||
|
'deal_rate': full['deal_rate'],
|
||||||
|
'fine_rate': full['fine_rate'],
|
||||||
|
'deal_rate_text': f"{round(full['deal_rate'] * 100, 1)}%",
|
||||||
|
'fine_rate_text': f"{round(full['fine_rate'] * 100, 1)}%",
|
||||||
|
}
|
||||||
|
sc = int(full.get('settle_duration_sample_count') or 0)
|
||||||
|
if sc > 0 and full.get('avg_deal_hours'):
|
||||||
|
h = float(full['avg_deal_hours'])
|
||||||
|
out['avg_deal_hours'] = h
|
||||||
|
if h < 1:
|
||||||
|
out['avg_deal_hours_text'] = f'{max(1, int(round(h * 60)))}分钟'
|
||||||
|
else:
|
||||||
|
out['avg_deal_hours_text'] = f'{round(h, 1)}小时'
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def miniapp_dashou_deal_payload(row) -> dict | None:
|
||||||
|
"""小程序打手订单列表顶:有样本才返回。"""
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
n = int(row.order_count or 0)
|
||||||
|
if n <= 0:
|
||||||
|
return None
|
||||||
|
full = serialize_dashou_stat(row)
|
||||||
|
return {
|
||||||
|
'role': 'dashou',
|
||||||
|
'order_count': full['order_count'],
|
||||||
|
'deal_rate': full['deal_rate'],
|
||||||
|
'fined_rate': full['fined_rate'],
|
||||||
|
'deal_rate_text': f"{round(full['deal_rate'] * 100, 1)}%",
|
||||||
|
'fined_rate_text': f"{round(full['fined_rate'] * 100, 1)}%",
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,41 @@ def get_or_create_deal_config(club_id: str):
|
|||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def order_auto_settle_payload(order) -> dict:
|
||||||
|
"""
|
||||||
|
小程序列表/详情展示用。
|
||||||
|
仅 Status=8 且 AutoSettleEligible 且有 AutoExpireAt 时返回字段;否则空 dict。
|
||||||
|
前端有字段才展示,避免关开关/老单误报。
|
||||||
|
"""
|
||||||
|
from backend.utils import fmt_datetime
|
||||||
|
|
||||||
|
if not order:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
status = int(getattr(order, 'Status', 0) or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
status = 0
|
||||||
|
if status != 8:
|
||||||
|
return {}
|
||||||
|
if not bool(getattr(order, 'AutoSettleEligible', False)):
|
||||||
|
return {}
|
||||||
|
expire_at = getattr(order, 'AutoExpireAt', None)
|
||||||
|
if not expire_at:
|
||||||
|
return {}
|
||||||
|
now = timezone.now()
|
||||||
|
# 与项目 USE_TZ 对齐,避免剩余秒数偏差
|
||||||
|
if timezone.is_aware(expire_at) and timezone.is_naive(now):
|
||||||
|
now = timezone.make_aware(now, timezone.get_current_timezone())
|
||||||
|
elif timezone.is_naive(expire_at) and timezone.is_aware(now):
|
||||||
|
expire_at = timezone.make_aware(expire_at, timezone.get_current_timezone())
|
||||||
|
remain = int((expire_at - now).total_seconds())
|
||||||
|
return {
|
||||||
|
'auto_settle_eligible': True,
|
||||||
|
'auto_expire_at': fmt_datetime(expire_at),
|
||||||
|
'auto_settle_remain_seconds': max(0, remain),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def config_to_dict(row) -> dict:
|
def config_to_dict(row) -> dict:
|
||||||
if not row:
|
if not row:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -114,3 +114,54 @@ class DashouDealStatView(APIView):
|
|||||||
'msg': 'ok',
|
'msg': 'ok',
|
||||||
'data': {'list': [stats_svc.serialize_dashou_stat(r) for r in qs]},
|
'data': {'list': [stats_svc.serialize_dashou_stat(r) for r in qs]},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class MiniappMyDealStatView(APIView):
|
||||||
|
"""
|
||||||
|
POST /dingdan/wo-de-deal-stat
|
||||||
|
小程序订单列表顶:结算率/罚款率。
|
||||||
|
body.role: merchant | dashou(默认按身份推断)
|
||||||
|
商家客服看所属商家指标;无样本返回 data=null。
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
club_id = (resolve_club_id_from_request(request) or '').strip()
|
||||||
|
if not club_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少俱乐部', 'data': None})
|
||||||
|
user = request.user
|
||||||
|
uid = getattr(user, 'UserUID', '') or ''
|
||||||
|
role = (request.data.get('role') or '').strip().lower()
|
||||||
|
|
||||||
|
if role not in ('merchant', 'dashou'):
|
||||||
|
# 推断:活跃子客服 → merchant;否则看商家认证 / 打手
|
||||||
|
from merchant_ops.services.authz import get_active_staff_member, is_merchant_owner
|
||||||
|
if get_active_staff_member(user) or is_merchant_owner(user):
|
||||||
|
role = 'merchant'
|
||||||
|
else:
|
||||||
|
role = 'dashou'
|
||||||
|
|
||||||
|
if role == 'merchant':
|
||||||
|
merchant_uid = uid
|
||||||
|
from merchant_ops.services.authz import get_active_staff_member, is_merchant_owner, member_has_perm
|
||||||
|
member = get_active_staff_member(user)
|
||||||
|
if member and not is_merchant_owner(user):
|
||||||
|
if not member_has_perm(member, 'order_view_all') and not member_has_perm(member, 'order_view_self'):
|
||||||
|
return Response({'code': 403, 'msg': '无订单查看权限', 'data': None})
|
||||||
|
merchant_uid = member.merchant_id
|
||||||
|
from jituan.models import MerchantDealStat
|
||||||
|
row = MerchantDealStat.query.filter(club_id=club_id, merchant_uid=merchant_uid).first()
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'ok',
|
||||||
|
'data': stats_svc.miniapp_merchant_deal_payload(row),
|
||||||
|
})
|
||||||
|
|
||||||
|
from jituan.models import DashouDealStat
|
||||||
|
row = DashouDealStat.query.filter(club_id=club_id, dashou_uid=uid).first()
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'ok',
|
||||||
|
'data': stats_svc.miniapp_dashou_deal_payload(row),
|
||||||
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from rank.models import DingdanBiaoqian, YonghuChenghao
|
|||||||
from users.business_models import User
|
from users.business_models import User
|
||||||
from users.models import UserShangjia
|
from users.models import UserShangjia
|
||||||
from backend.utils import fmt_datetime
|
from backend.utils import fmt_datetime
|
||||||
|
from jituan.services.order_deal import order_auto_settle_payload
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -126,7 +127,7 @@ def enrich_merchant_order_list_items(ext_list, club_id=None):
|
|||||||
zid = o.AssignedID or ''
|
zid = o.AssignedID or ''
|
||||||
zinfo = zhiding_info.get(zid, {})
|
zinfo = zhiding_info.get(zid, {})
|
||||||
|
|
||||||
result.append({
|
item = {
|
||||||
'dingdan_id': oid,
|
'dingdan_id': oid,
|
||||||
'zhuangtai': o.Status,
|
'zhuangtai': o.Status,
|
||||||
'jine': float(o.Amount) if o.Amount else 0.0,
|
'jine': float(o.Amount) if o.Amount else 0.0,
|
||||||
@@ -149,6 +150,8 @@ def enrich_merchant_order_list_items(ext_list, club_id=None):
|
|||||||
'zhiding_uid': zid,
|
'zhiding_uid': zid,
|
||||||
'zhiding_avatar': zinfo.get('avatar', ''),
|
'zhiding_avatar': zinfo.get('avatar', ''),
|
||||||
'zhiding_nicheng': zinfo.get('nicheng', ''),
|
'zhiding_nicheng': zinfo.get('nicheng', ''),
|
||||||
})
|
}
|
||||||
|
item.update(order_auto_settle_payload(o))
|
||||||
|
result.append(item)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -47,6 +47,38 @@ def parse_order_list_bound(value, is_end=False):
|
|||||||
return _db_dt(dt)
|
return _db_dt(dt)
|
||||||
|
|
||||||
|
|
||||||
|
def wants_auto_settle_pending(data) -> bool:
|
||||||
|
"""前端筛「待自动结算」:auto_settle_pending = 1/true。"""
|
||||||
|
if not data:
|
||||||
|
return False
|
||||||
|
v = data.get('auto_settle_pending')
|
||||||
|
if v is True or v == 1:
|
||||||
|
return True
|
||||||
|
if isinstance(v, str) and v.strip().lower() in ('1', 'true', 'yes'):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def apply_auto_settle_pending_filter(qs, data, *, through_order=True):
|
||||||
|
"""
|
||||||
|
仅 Status=8 且 AutoSettleEligible 且有到期时间。
|
||||||
|
through_order=True:MerchantOrderExt(Order__*);False:Order 本表。
|
||||||
|
"""
|
||||||
|
if not wants_auto_settle_pending(data):
|
||||||
|
return qs
|
||||||
|
if through_order:
|
||||||
|
return qs.filter(
|
||||||
|
Order__Status=8,
|
||||||
|
Order__AutoSettleEligible=True,
|
||||||
|
Order__AutoExpireAt__isnull=False,
|
||||||
|
)
|
||||||
|
return qs.filter(
|
||||||
|
Status=8,
|
||||||
|
AutoSettleEligible=True,
|
||||||
|
AutoExpireAt__isnull=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def apply_merchant_order_list_filters(qs, data):
|
def apply_merchant_order_list_filters(qs, data):
|
||||||
"""在 MerchantOrderExt 查询集上应用类型/关键字/时间段筛选。"""
|
"""在 MerchantOrderExt 查询集上应用类型/关键字/时间段筛选。"""
|
||||||
leixing_id = pick_leixing_id(data)
|
leixing_id = pick_leixing_id(data)
|
||||||
@@ -95,4 +127,5 @@ def apply_merchant_order_list_filters(qs, data):
|
|||||||
return qs.none()
|
return qs.none()
|
||||||
qs = qs.filter(Order__OrderID__in=order_ids)
|
qs = qs.filter(Order__OrderID__in=order_ids)
|
||||||
|
|
||||||
|
qs = apply_auto_settle_pending_filter(qs, data, through_order=True)
|
||||||
return qs
|
return qs
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from .views import CreateOrderView, WechatPayNotifyView, AlipayPayNotifyView, al
|
|||||||
LianTongPeiDuiDingDanLieBiaoView, LianTongDingDanZhuangTaiView, LianTongDingDanZhuangTaiPiLiangView, \
|
LianTongPeiDuiDingDanLieBiaoView, LianTongDingDanZhuangTaiView, LianTongDingDanZhuangTaiPiLiangView, \
|
||||||
ZhiDingChaxunView, ZhiDingHuiFuView
|
ZhiDingChaxunView, ZhiDingHuiFuView
|
||||||
from jituan.services.fubei_notify import FubeiPayNotifyView
|
from jituan.services.fubei_notify import FubeiPayNotifyView
|
||||||
|
from jituan.views_order_deal import MiniappMyDealStatView
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -39,6 +40,7 @@ urlpatterns = [
|
|||||||
path('sjchufa', ShangjiaChufaShenqingView.as_view(), name='商家处罚打手申请'),
|
path('sjchufa', ShangjiaChufaShenqingView.as_view(), name='商家处罚打手申请'),
|
||||||
path('ddlxhq', ShangpinLeixingHuoquView.as_view(), name='打手抢单页商品类型获取'),
|
path('ddlxhq', ShangpinLeixingHuoquView.as_view(), name='打手抢单页商品类型获取'),
|
||||||
path('dshqdingdan', DashouDingdanHuoquView1.as_view(), name='打手订单列表页面获取'),
|
path('dshqdingdan', DashouDingdanHuoquView1.as_view(), name='打手订单列表页面获取'),
|
||||||
|
path('wo-de-deal-stat', MiniappMyDealStatView.as_view(), name='小程序我的结算率罚款率'),
|
||||||
|
|
||||||
# 获取订单列表
|
# 获取订单列表
|
||||||
path('ddhq', DashouDingdanHuoquView.as_view(), name='打手抢单页获取订单'),
|
path('ddhq', DashouDingdanHuoquView.as_view(), name='打手抢单页获取订单'),
|
||||||
|
|||||||
@@ -679,6 +679,11 @@ class DashouDingdanHuoquView1(APIView):
|
|||||||
keyword_query = Q(OrderID__icontains=keyword) | Q(Description__icontains=keyword)
|
keyword_query = Q(OrderID__icontains=keyword) | Q(Description__icontains=keyword)
|
||||||
query &= keyword_query
|
query &= keyword_query
|
||||||
|
|
||||||
|
from orders.services.merchant_order_list_filters import wants_auto_settle_pending
|
||||||
|
# 待自动结算筛:强制落在结算中且 eligible
|
||||||
|
if wants_auto_settle_pending(data):
|
||||||
|
query &= Q(Status=8, AutoSettleEligible=True, AutoExpireAt__isnull=False)
|
||||||
|
|
||||||
# 5. 执行查询,按创建时间倒序
|
# 5. 执行查询,按创建时间倒序
|
||||||
dingdan_query = Order.query.filter(query).order_by('-CreateTime')
|
dingdan_query = Order.query.filter(query).order_by('-CreateTime')
|
||||||
|
|
||||||
@@ -690,7 +695,9 @@ class DashouDingdanHuoquView1(APIView):
|
|||||||
'Description',
|
'Description',
|
||||||
'ImageURL',
|
'ImageURL',
|
||||||
'Nickname',
|
'Nickname',
|
||||||
'CreateTime'
|
'CreateTime',
|
||||||
|
'AutoSettleEligible',
|
||||||
|
'AutoExpireAt',
|
||||||
)
|
)
|
||||||
|
|
||||||
total_count = dingdan_query.count()
|
total_count = dingdan_query.count()
|
||||||
@@ -702,6 +709,7 @@ class DashouDingdanHuoquView1(APIView):
|
|||||||
current_page_count = len(current_page_orders)
|
current_page_count = len(current_page_orders)
|
||||||
|
|
||||||
# 7. 构建返回数据列表
|
# 7. 构建返回数据列表
|
||||||
|
from jituan.services.order_deal import order_auto_settle_payload
|
||||||
order_list = []
|
order_list = []
|
||||||
for order in current_page_orders:
|
for order in current_page_orders:
|
||||||
order_data = {
|
order_data = {
|
||||||
@@ -713,6 +721,7 @@ class DashouDingdanHuoquView1(APIView):
|
|||||||
'nicheng': order.Nickname or '',
|
'nicheng': order.Nickname or '',
|
||||||
**datetime_aliases(order.CreateTime),
|
**datetime_aliases(order.CreateTime),
|
||||||
}
|
}
|
||||||
|
order_data.update(order_auto_settle_payload(order))
|
||||||
order_list.append(order_data)
|
order_list.append(order_data)
|
||||||
|
|
||||||
# 8. 判断是否还有更多数据
|
# 8. 判断是否还有更多数据
|
||||||
@@ -1278,6 +1287,11 @@ class DashouDingdanXiangqingView(APIView):
|
|||||||
'fuwudashou_id': dingdan.PlayerID or '',
|
'fuwudashou_id': dingdan.PlayerID or '',
|
||||||
'fadanpingtai': dingdan.Platform or ''
|
'fadanpingtai': dingdan.Platform or ''
|
||||||
}
|
}
|
||||||
|
try:
|
||||||
|
from jituan.services.order_deal import order_auto_settle_payload
|
||||||
|
dingdan_data.update(order_auto_settle_payload(dingdan))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 6. 查询打手提交的图片
|
# 6. 查询打手提交的图片
|
||||||
dashou_tupian_list = PlayerDeliveryImage.query.filter(
|
dashou_tupian_list = PlayerDeliveryImage.query.filter(
|
||||||
|
|||||||
@@ -754,6 +754,12 @@ class ShangjiaDingdanXiangqingView(APIView):
|
|||||||
'laoban_shouji': getattr(shangjia_kuozhan, 'BossPhone', '') or '',
|
'laoban_shouji': getattr(shangjia_kuozhan, 'BossPhone', '') or '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
from jituan.services.order_deal import order_auto_settle_payload
|
||||||
|
response_data.update(order_auto_settle_payload(order))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning('自动结算展示字段附加失败: %s', e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from merchant_ops.services.order_enrich import enrich_order_detail_data
|
from merchant_ops.services.order_enrich import enrich_order_detail_data
|
||||||
enrich_order_detail_data(response_data, dingdan_id)
|
enrich_order_detail_data(response_data, dingdan_id)
|
||||||
|
|||||||
Reference in New Issue
Block a user