diff --git a/app/admin/repositories/cps.py b/app/admin/repositories/cps.py index 4790f93..3271a74 100644 --- a/app/admin/repositories/cps.py +++ b/app/admin/repositories/cps.py @@ -428,3 +428,45 @@ def group_click_timeseries( for h in range(24): points.append(_point(f"{h:02d}:00", agg.get(h))) return points + + +def group_order_daily( + db: Session, *, sid: str, start: datetime, end: datetime, +) -> dict[str, dict]: + """美团群订单按天(北京时区,key=YYYY-MM-DD)聚合。口径与 group_stats._order_agg 一致: + 取消(4)/风控(5)不计有效;结算只数 status=6。退款佣金取当天全部单的 refund_profit_cents 之和。 + 返回 {date: {order_count, canceled_count, gmv_cents, est_commission_cents, + refund_profit_cents, settled_commission_cents}};无单的天不在 dict 里(端点侧补零)。 + """ + rows = ( + db.execute( + select(CpsOrder) + .where(CpsOrder.sid == sid) + .where(CpsOrder.pay_time >= _as_utc(start)) + .where(CpsOrder.pay_time <= _as_utc(end)) + ) + .scalars() + .all() + ) + by_day: dict[str, list] = {} + for o in rows: + if not o.pay_time: + continue + pt = o.pay_time if o.pay_time.tzinfo else o.pay_time.replace(tzinfo=timezone.utc) + d = pt.astimezone(_BJ_TZ).strftime("%Y-%m-%d") + by_day.setdefault(d, []).append(o) + + out: dict[str, dict] = {} + for d, items in by_day.items(): + valid = [o for o in items if o.mt_status not in _INVALID_STATUS] + settled = [o for o in items if o.mt_status == _SETTLED_STATUS] + canceled = [o for o in items if o.mt_status in _INVALID_STATUS] + out[d] = { + "order_count": len(valid), + "canceled_count": len(canceled), + "gmv_cents": sum(o.pay_price_cents or 0 for o in valid), + "est_commission_cents": sum(o.commission_cents or 0 for o in valid), + "refund_profit_cents": sum(o.refund_profit_cents or 0 for o in items), + "settled_commission_cents": sum(o.commission_cents or 0 for o in settled), + } + return out diff --git a/app/admin/routers/cps.py b/app/admin/routers/cps.py index c6cd9b3..6f892dd 100644 --- a/app/admin/routers/cps.py +++ b/app/admin/routers/cps.py @@ -433,3 +433,58 @@ def group_timeseries( "granularity": granularity, "points": points, } + + +@router.get("/groups/{group_id}/daily", summary="群每天明细大表格(点击+订单按天合并)") +def group_daily( + group_id: int, + db: AdminDb, + days: Annotated[int, Query(ge=1, le=90)] = 7, +) -> dict: + group = cps_repo.get_group(db, group_id) + if group is None: + raise HTTPException(status_code=404, detail="群不存在") + bj = timezone(timedelta(hours=8)) + now_bj = datetime.now(bj) + start = (now_bj - timedelta(days=days - 1)).replace(hour=0, minute=0, second=0, microsecond=0) + end = now_bj + # 点击侧:连续 N 天补零,顺序 = start..end(与下面 while 同序,按 index 对齐避免跨年 MM-DD 碰撞) + click_points = cps_repo.group_click_timeseries( + db, group_id=group_id, granularity="day", start=start, end=end + ) + # 订单侧:仅美团群(有 sid)有对账;淘宝/京东订单列返回 None(前端显示 "-") + is_meituan = "meituan" in (group.platforms or []) and bool(group.sid) + order_daily = ( + cps_repo.group_order_daily(db, sid=group.sid, start=start, end=end) if is_meituan else {} + ) + _ORDER_KEYS = ( + "order_count", "canceled_count", "gmv_cents", + "est_commission_cents", "refund_profit_cents", "settled_commission_cents", + ) + rows = [] + cur = start.astimezone(bj).date() + last = end.astimezone(bj).date() + idx = 0 + while cur <= last: + cp = click_points[idx] if idx < len(click_points) else None + row = { + "date": cur.strftime("%m-%d"), + "click_pv": cp["click_pv"] if cp else 0, + "click_uv": cp["click_uv"] if cp else 0, + "copy_pv": cp["copy_pv"] if cp else 0, + } + if is_meituan: + od = order_daily.get(cur.strftime("%Y-%m-%d")) + row.update({k: (od[k] if od else 0) for k in _ORDER_KEYS}) + else: + row.update({k: None for k in _ORDER_KEYS}) + rows.append(row) + idx += 1 + cur += timedelta(days=1) + return { + "group_id": group.id, + "group_name": group.name, + "is_meituan": is_meituan, + "days": days, + "rows": rows, + }