From e13f0f76585e3def29ca93e6e834a866a32773c4 Mon Sep 17 00:00:00 2001 From: marco Date: Tue, 23 Jun 2026 16:36:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(cps):=20=E7=BE=A4=E5=86=85=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E7=94=A8=E6=88=B7=E6=8C=89"=E6=9C=AC=E7=BE=A4?= =?UTF-8?q?=E7=82=B9=E5=87=BB"=E7=AE=97,=E4=B8=8D=E7=94=A8=20first=5Fgroup?= =?UTF-8?q?=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit first_group_id 只记首次授权群;同一用户(cookie 已存)再去别的群不再走授权回调, 会被漏算(实测:用户首次在群47授权,再去群52点击,群52详情显示0)。 改为:在本群有带 openid 点击的 distinct openid 即本群用户,关联 cps_wx_user 画像。 Co-Authored-By: Claude Opus 4.8 --- app/admin/repositories/cps.py | 59 ++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/app/admin/repositories/cps.py b/app/admin/repositories/cps.py index 4b1c7c3..fcb0565 100644 --- a/app/admin/repositories/cps.py +++ b/app/admin/repositories/cps.py @@ -474,44 +474,53 @@ def group_order_daily( def group_wx_users(db: Session, *, group_id: int, limit: int = 200) -> list[dict]: - """群内微信用户(首次来源 first_group_id=group_id)+ 每人在该群的 visit/copy 次数(领券画像)。 + """该群来过的微信用户:在本群有带 openid 点击的所有用户 + 本群 visit/copy 次数 + 画像。 - 按首次进入倒序。copy 次数 = 该用户在该群点了几次「复制口令」(领券意向),visit = 进落地页次数。 + 用「本群点击」判定归属,而非 cps_wx_user.first_group_id —— 同一用户会在多个群活动, + first_group_id 只记首次授权群,会漏掉"先在别处授权、cookie 已存、又来本群"的用户。 + copy=本群点「复制口令」次数(领券意向),visit=进落地页次数。按本群首次点击倒序。 + 昵称/头像来自授权画像;美团/京东群只 302 跳转、无 userinfo 触发点,故多为空。 """ - users = ( - db.execute( - select(CpsWxUser) - .where(CpsWxUser.first_group_id == group_id) - .order_by(desc(CpsWxUser.first_seen)) - .limit(limit) - ) - .scalars() - .all() - ) - if not users: - return [] - openids = [u.openid for u in users] - stat: dict[str, dict] = {} rows = db.execute( select(CpsClick.openid, CpsClick.event_type, func.count()) .where(CpsClick.group_id == group_id) - .where(CpsClick.openid.in_(openids)) + .where(CpsClick.openid.is_not(None)) .group_by(CpsClick.openid, CpsClick.event_type) ).all() + if not rows: + return [] + stat: dict[str, dict] = {} for openid, event_type, cnt in rows: s = stat.setdefault(openid, {"visit": 0, "copy": 0}) if event_type == "copy": s["copy"] = cnt else: s["visit"] = cnt - return [ + openids = list(stat.keys()) + # 本群每个 openid 的首次点击时间(排序 + 展示"首次进入") + first_seen = dict( + db.execute( + select(CpsClick.openid, func.min(CpsClick.clicked_at)) + .where(CpsClick.group_id == group_id) + .where(CpsClick.openid.is_not(None)) + .group_by(CpsClick.openid) + ).all() + ) + # 关联画像(未授权 userinfo 的昵称/头像为空) + users = { + u.openid: u + for u in db.execute(select(CpsWxUser).where(CpsWxUser.openid.in_(openids))).scalars().all() + } + result = [ { - "openid": u.openid, - "nickname": u.nickname, - "headimgurl": u.headimgurl, - "first_seen": u.first_seen, - "visit_count": stat.get(u.openid, {}).get("visit", 0), - "copy_count": stat.get(u.openid, {}).get("copy", 0), + "openid": openid, + "nickname": users[openid].nickname if openid in users else None, + "headimgurl": users[openid].headimgurl if openid in users else None, + "first_seen": first_seen.get(openid), + "visit_count": stat[openid]["visit"], + "copy_count": stat[openid]["copy"], } - for u in users + for openid in openids ] + result.sort(key=lambda x: x["first_seen"], reverse=True) + return result[:limit]