🤖 每日自动备份 - 2026-06-16 08:00:01
This commit is contained in:
parent
4135e93b17
commit
b25d6fef0f
19
MEMORY.md
19
MEMORY.md
@ -27,11 +27,17 @@
|
|||||||
- 「Bot刷新」/「S2 刷新」/「Bot 销转看板」→ 执行 `scripts/bot_sales_step2_refresh.py`,完成后群回「Bot刷新完成」,不动订单汇总 tab [陈逸鸫确认 2026-06-06]
|
- 「Bot刷新」/「S2 刷新」/「Bot 销转看板」→ 执行 `scripts/bot_sales_step2_refresh.py`,完成后群回「Bot刷新完成」,不动订单汇总 tab [陈逸鸫确认 2026-06-06]
|
||||||
- **Bot 销转看板 S1–S3 协作流程 [陈逸鸫确认 2026-06-06]:**
|
- **Bot 销转看板 S1–S3 协作流程 [陈逸鸫确认 2026-06-06]:**
|
||||||
- S1 Cursor → 微伴入库/每日线索/聚光 → @小溪
|
- S1 Cursor → 微伴入库/每日线索/聚光 → @小溪
|
||||||
- S2 小溪 → 销售三表 D/H/I/J + K–V + S/U → 群回「Bot刷新完成」
|
- S2 小溪 → 销售三表 D/H/I/J + K–U + X–Z → 群回「Bot刷新完成」
|
||||||
- S3 Cursor → 收到后粘贴订单汇总 + 刷看板公式
|
- S3 Cursor → 收到后粘贴订单汇总 + 刷看板公式
|
||||||
- 当日默认只跑一轮 S2;再刷需群里 `【执行更新】` @小溪
|
- 当日默认只跑一轮 S2;再刷需群里 `【执行更新】` @小溪
|
||||||
- 详细手册:`docs/伪BI-小溪操作手册.md`、`docs/bot-xiaoxi-collaboration-s1-s3.md`
|
- 详细手册:`docs/伪BI-小溪操作手册.md`、`docs/bot-xiaoxi-collaboration-s1-s3.md`
|
||||||
- S2 核心规则:① E→H 必须 phone_encrypt.py XXTEA 精确匹配,禁前三后四 ② H→D/I/J 只补空 ③ L≥C 才 K=是 ④ 全额退清 K/O/P/Q ⑤ O/P/Q 0留空,P整元 ⑥ G列不动
|
- S2 核心规则:① E→H 必须 phone_encrypt.py XXTEA 精确匹配,禁前三后四 ② H→D/I/J 只补空 ③ K(下单日)≥C(进线日期) 才 Y(有效订单)=1 ④ 全额退清 N/O/P 清空 ⑤ N/O/P 0留空,O整元 ⑥ G列不动
|
||||||
|
- **线索表(销售三表)列结构 [陈逸鸫确认 2026-06-15]:**
|
||||||
|
A销售归属 B微信昵称 C进线日期 D体验节数 E手机号 F用户年级 G课史/跟进 H用户ID I注册日期 J下载渠道 K下单日期 L成交渠道 M产品 N下单金额(GMV) O退款金额 P实际收入(GSV) Q激活课程 R当前行课进度 S最近行课时间 T累计学习时长(min) U更新时间 V微伴补充 W进线早于下单 X订单号 Y有效订单 Z渠道归属
|
||||||
|
- 脚本填写:D/H/I/J/K~U/X/Y/Z
|
||||||
|
- Y(有效订单)=1 → 进入订单汇总表
|
||||||
|
- **订单汇总表列结构 [陈逸鸫确认 2026-06-15]:**
|
||||||
|
A销售归属 B微信昵称 C进线日期 D体验节数 E手机号 F用户年级 G课史/跟进 H用户ID I注册日期 J下载渠道 K下单日期 L成交渠道 M产品 N下单金额(GMV) O退款金额 P实际收入(GSV) Q激活课程 R当前行课进度 S最近行课时间 T累计学习时长(min) U更新时间 V渠道归属 W有效成单 X订单号
|
||||||
- **飞书表格写入 5000 格上限规则(强制执行,[李承龙确认] 2026-06-13):**
|
- **飞书表格写入 5000 格上限规则(强制执行,[李承龙确认] 2026-06-13):**
|
||||||
- 飞书 Open API 单次写入上限为 5000 格(行×列),超过上限静默失败不报错
|
- 飞书 Open API 单次写入上限为 5000 格(行×列),超过上限静默失败不报错
|
||||||
- 所有脚本写入飞书表格时必须使用 `scripts/feishu_sheet_utils.py` 共享工具,自动分批确保 ≤ 4400 格/批
|
- 所有脚本写入飞书表格时必须使用 `scripts/feishu_sheet_utils.py` 共享工具,自动分批确保 ≤ 4400 格/批
|
||||||
@ -181,6 +187,15 @@
|
|||||||
- 小红书店铺:`newmedia-dianpu-xhs-0-0`
|
- 小红书店铺:`newmedia-dianpu-xhs-0-0`
|
||||||
- 达人直播:`newmedia-daren%`(前缀匹配)
|
- 达人直播:`newmedia-daren%`(前缀匹配)
|
||||||
- 万物:`newmedia-dianpu-wwxx-0-0`
|
- 万物:`newmedia-dianpu-wwxx-0-0`
|
||||||
|
- **销售转化渠道分类规则(仅用于销售转化场景,[王虹茗确认,李承龙确认] 2026-06-15):**
|
||||||
|
- 将 key_from 归为四类:
|
||||||
|
| 类别 | 匹配规则 |
|
||||||
|
|------|---------|
|
||||||
|
| 端内 | `app-active-h5-0-0`, `app-sales-bj-qhm-0`, `app-sales-bj-wd-0` |
|
||||||
|
| 销转 | `sales-adp-*` |
|
||||||
|
| 达人 | `newmedia-daren-*` + `newmedia-dianpu-wwxx-0-0`(万物算达人) |
|
||||||
|
| 直购 | `newmedia-dianpu-*`(**不含 wwxx**)+ 其余所有杂项(partner-*, stream-*, miniprogram-*, newmedia-jingxuan-*, 空值, shuadan 等) |
|
||||||
|
- **注意:** 此分类规则仅在销售转化场景下使用,不替代通用的渠道映射规则
|
||||||
- **sale_channel字段映射规则(仅对`key_from = app-active-h5-0-0`的订单生效):**
|
- **sale_channel字段映射规则(仅对`key_from = app-active-h5-0-0`的订单生效):**
|
||||||
| sale_channel值 | 对应渠道名称 |
|
| sale_channel值 | 对应渠道名称 |
|
||||||
|---------------|--------------|
|
|---------------|--------------|
|
||||||
|
|||||||
1
USER.md
1
USER.md
@ -44,6 +44,7 @@
|
|||||||
| 胡陈辰 | `gc64176a` |
|
| 胡陈辰 | `gc64176a` |
|
||||||
| 刘彦江 | `1da2afbf` |
|
| 刘彦江 | `1da2afbf` |
|
||||||
| 姜小龙 | `bc227c85` |
|
| 姜小龙 | `bc227c85` |
|
||||||
|
| 赵一凡 | `a3168f9d` |
|
||||||
|
|
||||||
> ⚠️ 以上用户拥有全部数据查询权限,但其个人信息、查询内容、对话记录**禁止写入 MEMORY.md(长期记忆)**,仅可记录在短期日记忆中用于会话连续性。
|
> ⚠️ 以上用户拥有全部数据查询权限,但其个人信息、查询内容、对话记录**禁止写入 MEMORY.md(长期记忆)**,仅可记录在短期日记忆中用于会话连续性。
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"updatedAt": "2026-06-14T07:14:40.970Z",
|
"updatedAt": "2026-06-15T02:42:34.143Z",
|
||||||
"entries": {
|
"entries": {
|
||||||
"memory:memory/2026-05-06.md:1:20": {
|
"memory:memory/2026-05-06.md:1:20": {
|
||||||
"key": "memory:memory/2026-05-06.md:1:20",
|
"key": "memory:memory/2026-05-06.md:1:20",
|
||||||
@ -668,18 +668,20 @@
|
|||||||
"endLine": 56,
|
"endLine": 56,
|
||||||
"source": "memory",
|
"source": "memory",
|
||||||
"snippet": "| 15 | Rachel | 3/5 | 10994 | 13510564547 | 3/7 | ✓ | sales-adp-bj-jxl-0, GMV=1999 | | 16 | soul | 3/2 | 17387 | 15640464255 | 3/12 | ✓ | sales-adp-bj-jxl-0, GMV=1999 | | 17 | 红 | 3/7 | 17025 | 13533955004 | 3/14 | ✓ | sales-adp-bj-jxl-0, GMV=1999 | | 18 | 一笑轩渠 | 3/8 | 17425 | 15017528458 | 3/11 | ✓ | sales-adp-bj-jxl-0, GMV=1999 | | 19 | 蜗牛 | 3/2 | ❓ | ❓ | ❓ | - | 晚柠5/15订单数百笔,无手机/UID无法定位 | | 20 | c_瑶 | 3/6 | ❓ | ❓ | ❓ | - | \"直购\"渠道DB不存在,3/14无3998/1999匹配 | ### 关键发现 1. Group A 中 7/10 用户 DB 中无任何订单(pre汇总有GMV但DB不存在) 2. Group B 4个手机号全部未注册(H=未注册 确认正确) 3. Group C #15-#18 4笔 jxl-0 均 L≥C,pre怀疑#18 L<C不成立 4. #19 蜗牛和 #20 c_瑶 无法定位 ## 陈逸鸫 - full_refresh (S2+S3) 联调 ### 执行记录 - 时间:2026-06-12 18:00 左右 - S2:",
|
"snippet": "| 15 | Rachel | 3/5 | 10994 | 13510564547 | 3/7 | ✓ | sales-adp-bj-jxl-0, GMV=1999 | | 16 | soul | 3/2 | 17387 | 15640464255 | 3/12 | ✓ | sales-adp-bj-jxl-0, GMV=1999 | | 17 | 红 | 3/7 | 17025 | 13533955004 | 3/14 | ✓ | sales-adp-bj-jxl-0, GMV=1999 | | 18 | 一笑轩渠 | 3/8 | 17425 | 15017528458 | 3/11 | ✓ | sales-adp-bj-jxl-0, GMV=1999 | | 19 | 蜗牛 | 3/2 | ❓ | ❓ | ❓ | - | 晚柠5/15订单数百笔,无手机/UID无法定位 | | 20 | c_瑶 | 3/6 | ❓ | ❓ | ❓ | - | \"直购\"渠道DB不存在,3/14无3998/1999匹配 | ### 关键发现 1. Group A 中 7/10 用户 DB 中无任何订单(pre汇总有GMV但DB不存在) 2. Group B 4个手机号全部未注册(H=未注册 确认正确) 3. Group C #15-#18 4笔 jxl-0 均 L≥C,pre怀疑#18 L<C不成立 4. #19 蜗牛和 #20 c_瑶 无法定位 ## 陈逸鸫 - full_refresh (S2+S3) 联调 ### 执行记录 - 时间:2026-06-12 18:00 左右 - S2:",
|
||||||
"recallCount": 1,
|
"recallCount": 2,
|
||||||
"dailyCount": 0,
|
"dailyCount": 0,
|
||||||
"groundedCount": 0,
|
"groundedCount": 0,
|
||||||
"totalScore": 1,
|
"totalScore": 2,
|
||||||
"maxScore": 1,
|
"maxScore": 1,
|
||||||
"firstRecalledAt": "2026-06-14T06:58:06.164Z",
|
"firstRecalledAt": "2026-06-14T06:58:06.164Z",
|
||||||
"lastRecalledAt": "2026-06-14T06:58:06.164Z",
|
"lastRecalledAt": "2026-06-15T02:42:34.143Z",
|
||||||
"queryHashes": [
|
"queryHashes": [
|
||||||
"6769ba9ebb36"
|
"6769ba9ebb36",
|
||||||
|
"0b9f27c6db59"
|
||||||
],
|
],
|
||||||
"recallDays": [
|
"recallDays": [
|
||||||
"2026-06-14"
|
"2026-06-14",
|
||||||
|
"2026-06-15"
|
||||||
],
|
],
|
||||||
"conceptTags": [
|
"conceptTags": [
|
||||||
"3/5",
|
"3/5",
|
||||||
@ -691,6 +693,99 @@
|
|||||||
"3/8",
|
"3/8",
|
||||||
"3/11"
|
"3/11"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"memory:memory/2026-06-13.md:1:21": {
|
||||||
|
"key": "memory:memory/2026-06-13.md:1:21",
|
||||||
|
"path": "memory/2026-06-13.md",
|
||||||
|
"startLine": 1,
|
||||||
|
"endLine": 21,
|
||||||
|
"source": "memory",
|
||||||
|
"snippet": "# 2026-06-13 工作日志 ## 飞书表格写入 5000 格上限问题修复 **来源:** 陈逸鸫发现全量刷订单漏数据,李承龙确认修复 **问题:** - `refresh_order_summary.py` 清空时 500行×26列=13,000格 超过飞书 API 5000 格上限 - `sales_leads_full_refresh.py` 清空时 500行×22列=11,000格,写入时 500行×22列=11,000格 同样超标 - 超标请求静默失败(API 不报错),导致旧数据残留、新数据被部分覆盖、末尾行丢失 **修复:** 1. 创建 `scripts/feishu_sheet_utils.py` 共享工具,封装安全分批写入/清空逻辑,自动计算批大小 ≤ 4400 格/批 2. `refresh_order_summary.py` 和 `sales_leads_full_refresh.py` 均已改用共享工具 3. 登记到 `SKILL_REGISTRY.md` 和 `MEMORY.md` **关键参数:** - 22列 → 单批最大 200 行 (4400 格) - 24列 → 单批最大 183 行 (4392 格) - 26列 → 单批最大 169 行 (4394 格)",
|
||||||
|
"recallCount": 1,
|
||||||
|
"dailyCount": 0,
|
||||||
|
"groundedCount": 0,
|
||||||
|
"totalScore": 1,
|
||||||
|
"maxScore": 1,
|
||||||
|
"firstRecalledAt": "2026-06-15T00:23:49.908Z",
|
||||||
|
"lastRecalledAt": "2026-06-15T00:23:49.908Z",
|
||||||
|
"queryHashes": [
|
||||||
|
"72dbb087c75b"
|
||||||
|
],
|
||||||
|
"recallDays": [
|
||||||
|
"2026-06-15"
|
||||||
|
],
|
||||||
|
"conceptTags": [
|
||||||
|
"refresh-order-summary.py",
|
||||||
|
"sales-leads-full-refresh.py",
|
||||||
|
"scripts/feishu-sheet-utils.py",
|
||||||
|
"封装安全分批写入/清空逻辑",
|
||||||
|
"格/批",
|
||||||
|
"skill-registry.md",
|
||||||
|
"memory.md",
|
||||||
|
"工作"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"memory:memory/2026-06-12.md:63:73": {
|
||||||
|
"key": "memory:memory/2026-06-12.md:63:73",
|
||||||
|
"path": "memory/2026-06-12.md",
|
||||||
|
"startLine": 63,
|
||||||
|
"endLine": 73,
|
||||||
|
"source": "memory",
|
||||||
|
"snippet": "- 整体小红书(含达人+直购):880 用户、969 单、328 万 GMV ### 直购表 + S3 联调完成 - 时间:2026-06-12 18:40 左右 - 新建直购 sheet `1sosYE`(Bot 工作簿内),独立存放小红书直购订单 - 新建 `scripts/refresh_direct_sheet.py`:从 DB 直查 `dianpu-xhs` / `stream-xhs` 订单,不依赖手机号,全量 73 条 - 修改 `scripts/refresh_order_summary.py`:S3 读取三表 + 直购表,合并写入订单汇总 - 直购行 C 列为空时跳过 L≥C 检查(直购无进线日期概念) - full_refresh 流程:先 `refresh_direct_sheet.py` → 再 `refresh_order_summary.py` - 本次验证:381(三表)+ 73(直购)= 454 行写入订单汇总",
|
||||||
|
"recallCount": 1,
|
||||||
|
"dailyCount": 0,
|
||||||
|
"groundedCount": 0,
|
||||||
|
"totalScore": 1,
|
||||||
|
"maxScore": 1,
|
||||||
|
"firstRecalledAt": "2026-06-15T02:42:34.143Z",
|
||||||
|
"lastRecalledAt": "2026-06-15T02:42:34.143Z",
|
||||||
|
"queryHashes": [
|
||||||
|
"0b9f27c6db59"
|
||||||
|
],
|
||||||
|
"recallDays": [
|
||||||
|
"2026-06-15"
|
||||||
|
],
|
||||||
|
"conceptTags": [
|
||||||
|
"scripts/refresh-direct-sheet.py",
|
||||||
|
"dianpu-xhs",
|
||||||
|
"stream-xhs",
|
||||||
|
"scripts/refresh-order-summary.py",
|
||||||
|
"full-refresh",
|
||||||
|
"refresh-direct-sheet.py",
|
||||||
|
"refresh-order-summary.py",
|
||||||
|
"整体"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"memory:memory/2026-06-12.md:53:68": {
|
||||||
|
"key": "memory:memory/2026-06-12.md:53:68",
|
||||||
|
"path": "memory/2026-06-12.md",
|
||||||
|
"startLine": 53,
|
||||||
|
"endLine": 68,
|
||||||
|
"source": "memory",
|
||||||
|
"snippet": "- S3:筛选 K=是 · O>0 · P<O · L≥C → 381 行写入订单汇总 2smjwA,W 渠道归属 + X=1 已写入 ### 发现的问题 1. **订单汇总前 8 行无 M/N(成交渠道/产品)**:S2 脚本读小龙 sheet 时 API 只返回了 1198 行(截断),后面约 200 行没被处理。已手动补 M/N。 2. **小龙 sheet 第 20 行\"赫尔辛基的阳光\"**:E 列手机号和 H 列 UID 均为空,但 K/O/R/S/T/U 有值。原因是之前 S2 刷新时 E 列有手机号匹配到了 UID,后来手机号被人清掉但订单数据残留。下次 S2 会清空该行自动列。 3. **Bot 应用无多维表格创建权限**:缺少 `bitable:app` 或 `base:app:create` 权限,需技术负责人在飞书开放平台开通。 ### 小红书直购数据(2026-03-12 ~ 2026-06-12) - DB 中 25 个直购用户(26 单),订单汇总仅覆盖 11 行 - 差距原因:直购用户在销售表无手机号 → S2 匹配不到 UID → 进不了订单汇总 - 整体小红书(含达人+直购):880 用户、969 单、328 万 GMV ### 直购表 + S3 联调完成 - 时间:2026-06-12 18:40 左右 - 新建直购 sheet `1sosYE`(Bot 工作簿内),独立存放小红书直购订单 - 新建 `scripts/refresh_direct_sheet.py`:从 DB 直查 `dianpu-xhs` / `stream-xhs`",
|
||||||
|
"recallCount": 1,
|
||||||
|
"dailyCount": 0,
|
||||||
|
"groundedCount": 0,
|
||||||
|
"totalScore": 1,
|
||||||
|
"maxScore": 1,
|
||||||
|
"firstRecalledAt": "2026-06-15T02:42:34.143Z",
|
||||||
|
"lastRecalledAt": "2026-06-15T02:42:34.143Z",
|
||||||
|
"queryHashes": [
|
||||||
|
"0b9f27c6db59"
|
||||||
|
],
|
||||||
|
"recallDays": [
|
||||||
|
"2026-06-15"
|
||||||
|
],
|
||||||
|
"conceptTags": [
|
||||||
|
"m/n",
|
||||||
|
"成交渠道/产品",
|
||||||
|
"k/o/r/s/t/u",
|
||||||
|
"scripts/refresh-direct-sheet.py",
|
||||||
|
"dianpu-xhs",
|
||||||
|
"stream-xhs",
|
||||||
|
"筛选",
|
||||||
|
"订单"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
memory/2026-06-15-1214.md
Normal file
65
memory/2026-06-15-1214.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# Session: 2026-06-15 12:14:04 GMT+8
|
||||||
|
|
||||||
|
- **Session Key**: agent:main:feishu:direct:ou_e63ce6b760ad39382852472f28fbe2a2
|
||||||
|
- **Session ID**: f244c806-798c-44a9-b71b-480bc55f280f
|
||||||
|
- **Source**: feishu
|
||||||
|
|
||||||
|
## Conversation Summary
|
||||||
|
|
||||||
|
assistant: Now let me also check the `COL_REFUND_DATE` — I'm writing empty string now, but when `is_refunded = "是"` it might make sense to still show the refund date. Let me check if there's a reference issue:
|
||||||
|
|
||||||
|
|
||||||
|
assistant: 完成。退费率从 10.0% → 0.0%,符合预期。
|
||||||
|
|
||||||
|
改动逻辑:之前 `has_refund` 是账号级别的全局标记,只要历史上有一笔退费就标"是"。现在改为按每笔订单判断:转化后只要存在一笔 ≥ 析出日期且未退费的有效订单,就算未退费(即使之前有退费记录)。只有当所有 ≥ 析出日期的订单都被退费时,才标"是"。
|
||||||
|
user: [message_id: om_x100b6dc3cb5e08acb4a8c5c2a6f2359]
|
||||||
|
李承龙: 然后你需要核对一下转化金额,需要是该用户的全部订单的加和减去全部退费的加和。同时你需要注意退费金额的小数点的计算是不是对的。
|
||||||
|
assistant: 两个问题:
|
||||||
|
|
||||||
|
1. `refund_amount` 是 varchar 类型存的是**元**(如 `"3598.00"`),而 `pay_amount_int` 是 int 存的是**分**(如 `359800`)。当前 `::numeric::int` 把 `3598.00` 元当 3598 分用了,差了 100 倍。
|
||||||
|
2. 转化金额应该是用户级别的总 GSV(全部订单加和 - 全部退费加和),不是单笔订单的 GSV。
|
||||||
|
|
||||||
|
|
||||||
|
assistant: 两个问题需要修复:
|
||||||
|
|
||||||
|
1. `refund_amount` 存的是**元**(varchar `"3598.00"`),`pay_amount_int` 存的是**分**(int `359800`),需要 `*100` 统一单位
|
||||||
|
2. 转化金额应该是用户级别的总 GSV,不是单笔订单的 GSV
|
||||||
|
|
||||||
|
|
||||||
|
assistant: Now update `process_sheet` to use the new structure:
|
||||||
|
|
||||||
|
|
||||||
|
assistant: 先验证 SQL 修复是否正确:
|
||||||
|
|
||||||
|
|
||||||
|
assistant: 验证通过。`refund_amount::numeric * 100` 正确将元转分。account 30280:第一笔全退(GSV=0),第二笔未退(GSV=359800),总 GSV=3598.00 元。跑正式:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
assistant: 完成。修复了两个问题:
|
||||||
|
|
||||||
|
1. **退费金额单位修复**:`refund_amount` 存的是**元**(varchar `"3598.00"`),`pay_amount_int` 存的是**分**(int `359800`)。之前 `::numeric::int` 把 `3598.00` 当 3598 分用了,差了 100 倍。改为 `::numeric * 100` 统一为分。
|
||||||
|
|
||||||
|
2. **转化金额改为用户总 GSV**:之前是取首笔匹配订单的 GSV,现在改为用户全部订单的 `pay_amount_int` 加和减去全部退费的 `refund_amount * 100` 加和,再除以 100 得到元。
|
||||||
|
|
||||||
|
结果:总析出 45,匹配 18,转化 10,转化率 22.2%,退费率 0.0%。你可以去表里核对一下 J 列的转化金额。
|
||||||
|
user: [message_id: om_x100b6dc3e957078cb03b6fe7b942192]
|
||||||
|
李承龙: 我在统计sheet中,转化率列后面添加了GMV和GSV列,需要填上
|
||||||
|
assistant: 需要在 `query_orders_and_refunds` 中增加 GMV(全部 pay_amount 加和,不扣退费),然后在 `process_sheet` 中传给 `lead_data`,最后在 `compute_stats` 和 `write_all_stats` 中汇总写入。
|
||||||
|
|
||||||
|
|
||||||
|
assistant: Now update `lead_data` to carry GMV/GSV, and update `compute_stats` + `write_all_stats`:
|
||||||
|
|
||||||
|
|
||||||
|
assistant: Now update `compute_stats` to aggregate GMV/GSV:
|
||||||
|
|
||||||
|
|
||||||
|
assistant: Now update `write_all_stats` to write GMV/GSV columns (inserted after 退费率 G, shifting completion rates to J-S):
|
||||||
|
|
||||||
|
|
||||||
|
assistant: 完成。统计 sheet 新增 H 列 GMV、I 列 GSV:
|
||||||
|
|
||||||
|
- 吴迪 2026-06:总析出 45,匹配 18,转化 10,转化率 22.2%,退费率 0.0%,**GMV=22984.00,GSV=19386.00**
|
||||||
|
|
||||||
|
GMV = 全部订单 pay_amount 加和(含退费),GSV = GMV 扣除退费金额。你可以去统计 sheet 核对一下。
|
||||||
29
memory/2026-06-15-1737.md
Normal file
29
memory/2026-06-15-1737.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Session: 2026-06-15 17:37:58 GMT+8
|
||||||
|
|
||||||
|
- **Session Key**: agent:main:feishu:direct:ou_e63ce6b760ad39382852472f28fbe2a2
|
||||||
|
- **Session ID**: fc0d2372-c44e-4671-aec5-5b37b130516a
|
||||||
|
- **Source**: feishu
|
||||||
|
|
||||||
|
## Conversation Summary
|
||||||
|
|
||||||
|
user: [message_id: om_x100b6dc385cd64a4b042b9043fef6d1]
|
||||||
|
李承龙: 看下赵一凡的id
|
||||||
|
assistant: 赵一凡不在当前的 vala_users_list.md 名单中,让我在数据库中查一下。
|
||||||
|
|
||||||
|
|
||||||
|
assistant: 赵一凡的飞书 ID 信息如下:
|
||||||
|
|
||||||
|
| 字段 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| **姓名** | 赵一凡 |
|
||||||
|
| **user_id**(租户级) | `a3168f9d` |
|
||||||
|
| **open_id** | `ou_13f70b43d99dc634d484ac7ebc4ef670` |
|
||||||
|
| **union_id** | `on_a07c0b5e6bf97c4c6f54c7588d5cfa80` |
|
||||||
|
|
||||||
|
另外,赵一凡不在现有的 `vala_users_list.md` 名单中,需要我把他加进去吗?
|
||||||
|
user: [message_id: om_x100b6dc3abcfd4a8b24f23001dd2d20]
|
||||||
|
李承龙: 加进去,同时把她加到user.md的一级用户中
|
||||||
|
assistant: 已完成两处更新:
|
||||||
|
|
||||||
|
1. **vala_users_list.md** — 赵一凡 `a3168f9d` 已加入,总人数 57→58
|
||||||
|
2. **USER.md** — 赵一凡 `a3168f9d` 已加入一级完整权限用户列表
|
||||||
18
memory/2026-06-15-1911.md
Normal file
18
memory/2026-06-15-1911.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Session: 2026-06-15 19:11:59 GMT+8
|
||||||
|
|
||||||
|
- **Session Key**: agent:main:feishu:direct:ou_e63ce6b760ad39382852472f28fbe2a2
|
||||||
|
- **Session ID**: 57e4e7f0-6dc1-4c9d-85b9-82ee493d0400
|
||||||
|
- **Source**: feishu
|
||||||
|
|
||||||
|
## Conversation Summary
|
||||||
|
|
||||||
|
user: [message_id: om_x100b6dc8cf28f0b8b396e439badade8]
|
||||||
|
李承龙: sales_leads_full_refresh.py这个脚本,所有的订单时间都从日期调整成日期加具体的时分秒
|
||||||
|
assistant: 只改一处:`query_all_pg` 中 `order_date` 的格式化,从 `%Y-%m-%d` 改为 `%Y-%m-%d %H:%M:%S`。日期比较逻辑(`order_date < clue_date`)字符串比较仍然正确。
|
||||||
|
|
||||||
|
|
||||||
|
assistant: 已修改。`order_date` 现在输出 `YYYY-MM-DD HH:MM:SS` 格式,会同时影响:
|
||||||
|
- 销售三表的 **L 列(下单日期)**
|
||||||
|
- 订单汇总 sheet 的 **L 列(下单日期)**
|
||||||
|
|
||||||
|
日期比较逻辑(`order_date < clue_date`)用字符串比较仍然正确——同一天的订单不会因多了时分秒而被误判为"早于线索日期"。
|
||||||
20
memory/2026-06-15-1915.md
Normal file
20
memory/2026-06-15-1915.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Session: 2026-06-15 19:15:32 GMT+8
|
||||||
|
|
||||||
|
- **Session Key**: agent:main:feishu:direct:ou_e63ce6b760ad39382852472f28fbe2a2
|
||||||
|
- **Session ID**: 59db7bdc-2cb3-4d11-b53e-f355070389b3
|
||||||
|
- **Source**: feishu
|
||||||
|
|
||||||
|
## Conversation Summary
|
||||||
|
|
||||||
|
user: [message_id: om_x100b6dc9af16d508b107252224f18dc]
|
||||||
|
李承龙: 6月份app-active-h5-0-0这个keyfrom有多少订单
|
||||||
|
assistant: 6月份 `app-active-h5-0-0` 渠道数据:
|
||||||
|
|
||||||
|
| 指标 | 数值 |
|
||||||
|
|------|------|
|
||||||
|
| 订单数 | **30 单** |
|
||||||
|
| GMV | **31,870.00 元** |
|
||||||
|
| GSV | **31,838.03 元** |
|
||||||
|
| 退费率 | **10.0%** |
|
||||||
|
|
||||||
|
统计口径:已剔除测试账号,仅统计 `pay_success_date` 在 6/1~6/30 的已完成/已退款订单。
|
||||||
112
memory/2026-06-15.md
Normal file
112
memory/2026-06-15.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# 2026-06-15 工作日志
|
||||||
|
|
||||||
|
## 陈逸鸫 - 订单口径变更:停直购
|
||||||
|
|
||||||
|
**来源:** 陈逸鸫(群聊「数据更新V2」)
|
||||||
|
|
||||||
|
**变更内容:**
|
||||||
|
1. 以后不管直购单,只看小红书销转,直购 GMV 不再进销转对账
|
||||||
|
2. full_refresh 不再 merge 直购进订单汇总
|
||||||
|
3. 直购表(1sosYE)可删除(需人工在飞书操作,API 不支持删 sheet)
|
||||||
|
4. 仍支持按手机查 keyfrom/GSV/退款(不定销售归属)
|
||||||
|
|
||||||
|
**脚本修改:**
|
||||||
|
- `scripts/refresh_order_summary.py`:去掉 Step 1.5 读取直购表逻辑,不再合并直购行
|
||||||
|
- `scripts/full_refresh_sales.py`:不涉及直购,无需改动
|
||||||
|
- `scripts/refresh_direct_sheet.py`:保留不动(如后续仍需独立查直购数据)
|
||||||
|
|
||||||
|
## 陈逸鸫 - full_refresh 执行
|
||||||
|
|
||||||
|
- 时间:2026-06-15 08:24-08:26
|
||||||
|
- 销售三表:吴迪 697 / 小龙 1198 / 成都 2203
|
||||||
|
- 有效 user_id:2009
|
||||||
|
- 订单汇总:383 条(不含直购)
|
||||||
|
- 直购表:75 条已刷新
|
||||||
|
- 订单汇总合并后:461 条(含直购,这是口径变更前的最后一次含直购的合并)
|
||||||
|
|
||||||
|
## 陈逸鸫 - 线索表字段调整(2026-06-15)
|
||||||
|
|
||||||
|
**来源:** 陈逸鸫(群聊「小红书数据需求」)
|
||||||
|
|
||||||
|
**线索表(销售三表)新列结构(A~Z):**
|
||||||
|
| 列 | 字段名 | 脚本填写 |
|
||||||
|
|----|--------|---------|
|
||||||
|
| A | 销售归属 | - |
|
||||||
|
| B | 微信昵称 | - |
|
||||||
|
| C | 进线日期 | - |
|
||||||
|
| D | 体验节数 | ✅ |
|
||||||
|
| E | 手机号 | - |
|
||||||
|
| F | 用户年级 | - |
|
||||||
|
| G | 课史/跟进 | - |
|
||||||
|
| H | 用户ID | ✅ |
|
||||||
|
| I | 注册日期 | ✅ |
|
||||||
|
| J | 下载渠道 | ✅ |
|
||||||
|
| K | 下单日期 | ✅ |
|
||||||
|
| L | 成交渠道 | ✅ |
|
||||||
|
| M | 产品 | ✅ |
|
||||||
|
| N | 下单金额(GMV) | ✅ |
|
||||||
|
| O | 退款金额 | ✅ |
|
||||||
|
| P | 实际收入(GSV) | ✅ |
|
||||||
|
| Q | 激活课程 | ✅ |
|
||||||
|
| R | 当前行课进度 | ✅ |
|
||||||
|
| S | 最近行课时间 | ✅ |
|
||||||
|
| T | 累计学习时长(min) | ✅ |
|
||||||
|
| U | 更新时间 | ✅ |
|
||||||
|
| V | 微伴补充 | - |
|
||||||
|
| W | 进线早于下单 | - |
|
||||||
|
| X | 订单号 | ✅ |
|
||||||
|
| Y | 有效订单 | ✅ |
|
||||||
|
| Z | 渠道归属 | ✅ |
|
||||||
|
|
||||||
|
**对比旧版变化:**
|
||||||
|
- 删除旧 K 列「是否下单」
|
||||||
|
- 旧 L~V 列整体左移一位(L→K, M→L, ..., V→U)
|
||||||
|
- 新增 V(微伴补充), W(进线早于下单), X(订单号), Y(有效订单), Z(渠道归属)
|
||||||
|
|
||||||
|
**订单汇总表新列结构(A~X):**
|
||||||
|
| 列 | 字段名 |
|
||||||
|
|----|--------|
|
||||||
|
| A | 销售归属 |
|
||||||
|
| B | 微信昵称 |
|
||||||
|
| C | 进线日期 |
|
||||||
|
| D | 体验节数 |
|
||||||
|
| E | 手机号 |
|
||||||
|
| F | 用户年级 |
|
||||||
|
| G | 课史/跟进 |
|
||||||
|
| H | 用户ID |
|
||||||
|
| I | 注册日期 |
|
||||||
|
| J | 下载渠道 |
|
||||||
|
| K | 下单日期 |
|
||||||
|
| L | 成交渠道 |
|
||||||
|
| M | 产品 |
|
||||||
|
| N | 下单金额(GMV) |
|
||||||
|
| O | 退款金额 |
|
||||||
|
| P | 实际收入(GSV) |
|
||||||
|
| Q | 激活课程 |
|
||||||
|
| R | 当前行课进度 |
|
||||||
|
| S | 最近行课时间 |
|
||||||
|
| T | 累计学习时长(min) |
|
||||||
|
| U | 更新时间 |
|
||||||
|
| V | 渠道归属 |
|
||||||
|
| W | 有效成单 |
|
||||||
|
| X | 订单号 |
|
||||||
|
|
||||||
|
**有效订单判定规则(不变):**
|
||||||
|
- 线索表中 Y 列(有效订单)= 1 的订单进入订单汇总表
|
||||||
|
- 判定逻辑同之前:有支付成功订单、非全额退清、下单日 ≥ 进线日期
|
||||||
|
|
||||||
|
## 王虹茗 - 小红书线索落单规则(key_from 分类)
|
||||||
|
|
||||||
|
**来源:** 王虹茗确认,陈逸鸫转述(群聊「小红书数据需求」2026-06-15)
|
||||||
|
|
||||||
|
**分类规则(四类):**
|
||||||
|
| 类别 | key_from 匹配规则 |
|
||||||
|
|------|-------------------|
|
||||||
|
| 端内 | `app-active-h5-0-0`, `app-sales-bj-qhm-0`, `app-sales-bj-wd-0` |
|
||||||
|
| 销转 | `sales-adp-*` |
|
||||||
|
| 达人 | `newmedia-daren-*` + `newmedia-dianpu-wwxx-0-0`(万物算达人) |
|
||||||
|
| 直购 | `newmedia-dianpu-*`(**不含 wwxx**)+ 其余所有杂项(partner-*, stream-*, miniprogram-*, newmedia-jingxuan-*, 空值, shuadan 等) |
|
||||||
|
|
||||||
|
**关键点:**
|
||||||
|
- wwxx(万物,`newmedia-dianpu-wwxx-0-0`)归入达人,不归入直购
|
||||||
|
- 店铺 dianpu(不含 wwxx)+ 所有杂项渠道,统一归入直购一个大类
|
||||||
@ -1,16 +1,16 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Bot 销转看板 Step2 刷新 — XXTEA 精确匹配版 (v2)
|
Bot 销转看板 Step2 刷新 — XXTEA 精确匹配版 (v3)
|
||||||
E列11位明文手机号 → XXTEA加密 → bi_vala_app_account.tel_encrypt精确匹配 → H列UID
|
E列11位明文手机号 → XXTEA加密 → bi_vala_app_account.tel_encrypt精确匹配 → H列UID
|
||||||
|
|
||||||
S2 规则:
|
S2 规则:
|
||||||
① E→H: phone_encrypt.py XXTEA 精确匹配, 查不到留空
|
① E→H: phone_encrypt.py XXTEA 精确匹配, 查不到留空
|
||||||
② H→D/I/J: 只补空, 不覆盖已有值
|
② H→D/I/J: 只补空, 不覆盖已有值
|
||||||
③ K=是: 仅当 L(下单日) >= C(线索日期)
|
③ Y=1: 仅当 K(下单日) >= C(线索日期)
|
||||||
④ 全额退清: 所有订单都退费 → K/O/P/Q 全部清空
|
④ 全额退清: 所有订单都退费 → N/O/P 全部清空
|
||||||
⑤ O/P/Q 0留空, P整元
|
⑤ N/O/P 0留空, O整元
|
||||||
⑥ G列不动, 订单汇总不动
|
⑥ G列不动, 订单汇总不动
|
||||||
覆盖列: D/H/I/J + K-V + S/U
|
覆盖列: D/H/I/J + K-U + X/Y/Z
|
||||||
"""
|
"""
|
||||||
import json, re, time, sys, os, requests, psycopg2
|
import json, re, time, sys, os, requests, psycopg2
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -27,9 +27,9 @@ from phone_encrypt import encrypt_phone
|
|||||||
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
|
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
|
||||||
|
|
||||||
SALES_SHEETS = [
|
SALES_SHEETS = [
|
||||||
("qJF4I", "小龙", "A1:V1200"),
|
("qJF4I", "小龙", "A1:Z1200"),
|
||||||
("f975f0", "吴迪", "A1:V700"),
|
("f975f0", "吴迪", "A1:Z700"),
|
||||||
("qJF4J", "成都", "A1:V2500"),
|
("qJF4J", "成都", "A1:Z2500"),
|
||||||
]
|
]
|
||||||
|
|
||||||
CS_MAP = {"吴迪": "吴迪", "小龙": "小龙", "Tom": "Tom", "Bob": "Bob"}
|
CS_MAP = {"吴迪": "吴迪", "小龙": "小龙", "Tom": "Tom", "Bob": "Bob"}
|
||||||
@ -228,7 +228,7 @@ def query_all_pg(all_entries, phone_map):
|
|||||||
"has_order": False, "order_date": "", "order_channel": "", "product": "",
|
"has_order": False, "order_date": "", "order_channel": "", "product": "",
|
||||||
"gmv": 0, "refund": 0, "gsv": 0,
|
"gmv": 0, "refund": 0, "gsv": 0,
|
||||||
"activation": "", "lesson_progress": "", "lesson_time": "", "lesson_minutes": 0,
|
"activation": "", "lesson_progress": "", "lesson_time": "", "lesson_minutes": 0,
|
||||||
"max_lesson": 0,
|
"max_lesson": 0, "trade_no": "",
|
||||||
} for uid in uid_set}
|
} for uid in uid_set}
|
||||||
|
|
||||||
# 3a. 注册信息
|
# 3a. 注册信息
|
||||||
@ -278,9 +278,10 @@ def query_all_pg(all_entries, phone_map):
|
|||||||
continue
|
continue
|
||||||
info[aid]["has_order"] = True
|
info[aid]["has_order"] = True
|
||||||
latest = olist[0]
|
latest = olist[0]
|
||||||
info[aid]["order_date"] = latest[2].strftime("%Y-%m-%d") if latest[2] else ""
|
info[aid]["order_date"] = f"{latest[2].month}月{latest[2].day}日 {latest[2].strftime('%H:%M:%S')}" if latest[2] else ""
|
||||||
info[aid]["order_channel"] = latest[3] or ""
|
info[aid]["order_channel"] = latest[3] or ""
|
||||||
info[aid]["product"] = GOODS_NAMES.get(latest[4], f"商品{latest[4]}")
|
info[aid]["product"] = GOODS_NAMES.get(latest[4], f"商品{latest[4]}")
|
||||||
|
info[aid]["trade_no"] = latest[1] or ""
|
||||||
total_gmv = sum(o[5] for o in olist) / 100.0
|
total_gmv = sum(o[5] for o in olist) / 100.0
|
||||||
total_refund = sum(refund_map.get(o[1], 0) for o in olist) / 100.0
|
total_refund = sum(refund_map.get(o[1], 0) for o in olist) / 100.0
|
||||||
info[aid]["gmv"] = total_gmv
|
info[aid]["gmv"] = total_gmv
|
||||||
@ -402,7 +403,7 @@ def query_all_pg(all_entries, phone_map):
|
|||||||
|
|
||||||
# ── Step 4: 写入销售三表 ──
|
# ── Step 4: 写入销售三表 ──
|
||||||
def write_sales_sheets(token, all_entries, phone_map, db_info):
|
def write_sales_sheets(token, all_entries, phone_map, db_info):
|
||||||
"""全覆盖写入销售表的自动列"""
|
"""全覆盖写入销售表的自动列: D/H/I/J + K-U + X/Y/Z"""
|
||||||
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
for sid, sname, _ in SALES_SHEETS:
|
for sid, sname, _ in SALES_SHEETS:
|
||||||
@ -428,7 +429,8 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
|||||||
d_vals, h_vals, i_vals, j_vals = [], [], [], []
|
d_vals, h_vals, i_vals, j_vals = [], [], [], []
|
||||||
k_vals, l_vals, m_vals, n_vals = [], [], [], []
|
k_vals, l_vals, m_vals, n_vals = [], [], [], []
|
||||||
o_vals, p_vals, q_vals, r_vals = [], [], [], []
|
o_vals, p_vals, q_vals, r_vals = [], [], [], []
|
||||||
s_vals, t_vals, u_vals, v_vals = [], [], [], []
|
s_vals, t_vals, u_vals = [], [], []
|
||||||
|
x_vals, y_vals, z_vals = [], [], []
|
||||||
|
|
||||||
for item in g:
|
for item in g:
|
||||||
phone = item["phone"]
|
phone = item["phone"]
|
||||||
@ -486,51 +488,62 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
|||||||
gsv_int = int(di["gsv"])
|
gsv_int = int(di["gsv"])
|
||||||
is_full_refund = (gmv_int > 0 and gmv_int == refund_int)
|
is_full_refund = (gmv_int > 0 and gmv_int == refund_int)
|
||||||
|
|
||||||
# K=是: 有订单且非全额退清
|
# 有效订单判定: 有订单且非全额退清
|
||||||
# 注: C列(进线日期)实际存的是手机号, 无法做L≥C日期比较
|
|
||||||
order_date = di["order_date"]
|
order_date = di["order_date"]
|
||||||
should_k_yes = di["has_order"] and not is_full_refund
|
should_y_yes = di["has_order"] and not is_full_refund
|
||||||
|
|
||||||
if is_full_refund:
|
if is_full_refund:
|
||||||
# 全额退清 → K/O/P/Q 全部清空
|
# 全额退清 → N/O/P 全部清空
|
||||||
k_vals.append([""])
|
n_vals.append([""])
|
||||||
o_vals.append([""])
|
o_vals.append([""])
|
||||||
p_vals.append([""])
|
p_vals.append([""])
|
||||||
q_vals.append([""])
|
|
||||||
else:
|
else:
|
||||||
k_vals.append(["是" if should_k_yes else ""])
|
n_vals.append([gmv_int if gmv_int > 0 else ""])
|
||||||
o_vals.append([gmv_int if gmv_int > 0 else ""])
|
o_vals.append([refund_int if refund_int > 0 else ""])
|
||||||
p_vals.append([refund_int if refund_int > 0 else ""])
|
p_vals.append([gsv_int if gsv_int > 0 else ""])
|
||||||
q_vals.append([gsv_int if gsv_int > 0 else ""])
|
|
||||||
|
|
||||||
l_vals.append([order_date])
|
# K: 下单日期, L: 成交渠道, M: 产品
|
||||||
m_vals.append([di["order_channel"]])
|
k_vals.append([order_date])
|
||||||
n_vals.append([di["product"] if di["has_order"] else ""])
|
l_vals.append([di["order_channel"]])
|
||||||
|
m_vals.append([di["product"] if di["has_order"] else ""])
|
||||||
|
|
||||||
|
# Q: 激活课程
|
||||||
act = di["activation"]
|
act = di["activation"]
|
||||||
if act:
|
if act:
|
||||||
r_vals.append([f"{act}体验课" if act in ("A1", "A2") else act])
|
q_vals.append([f"{act}体验课" if act in ("A1", "A2") else act])
|
||||||
else:
|
else:
|
||||||
r_vals.append([""])
|
q_vals.append([""])
|
||||||
|
|
||||||
|
# R: 行课进度, S: 最近行课时间
|
||||||
lp = di["lesson_progress"]
|
lp = di["lesson_progress"]
|
||||||
s_vals.append([lp if lp else ""])
|
r_vals.append([lp if lp else ""])
|
||||||
t_vals.append([di["lesson_time"]])
|
s_vals.append([di["lesson_time"]])
|
||||||
|
|
||||||
|
# T: 学习时长
|
||||||
lm = di["lesson_minutes"]
|
lm = di["lesson_minutes"]
|
||||||
u_vals.append([lm if lm > 0 else ""])
|
t_vals.append([lm if lm > 0 else ""])
|
||||||
|
|
||||||
|
# X: 订单号
|
||||||
|
x_vals.append([di.get("trade_no", "")])
|
||||||
|
# Y: 有效订单
|
||||||
|
y_vals.append([1 if should_y_yes else ""])
|
||||||
|
# Z: 渠道归属
|
||||||
|
z_vals.append([di["order_channel"]])
|
||||||
else:
|
else:
|
||||||
for arr in [d_vals, i_vals, j_vals, k_vals, l_vals, m_vals, n_vals,
|
for arr in [d_vals, i_vals, j_vals, k_vals, l_vals, m_vals, n_vals,
|
||||||
o_vals, p_vals, q_vals, r_vals, s_vals, t_vals, u_vals]:
|
o_vals, p_vals, q_vals, r_vals, s_vals, t_vals,
|
||||||
|
x_vals, y_vals, z_vals]:
|
||||||
arr.append([""])
|
arr.append([""])
|
||||||
|
|
||||||
v_vals.append([now_str])
|
# U: 更新时间
|
||||||
|
u_vals.append([now_str])
|
||||||
|
|
||||||
cols = [
|
cols = [
|
||||||
("D", d_vals), ("H", h_vals), ("I", i_vals), ("J", j_vals),
|
("D", d_vals), ("H", h_vals), ("I", i_vals), ("J", j_vals),
|
||||||
("K", k_vals), ("L", l_vals), ("M", m_vals), ("N", n_vals),
|
("K", k_vals), ("L", l_vals), ("M", m_vals), ("N", n_vals),
|
||||||
("O", o_vals), ("P", p_vals), ("Q", q_vals), ("R", r_vals),
|
("O", o_vals), ("P", p_vals), ("Q", q_vals), ("R", r_vals),
|
||||||
("S", s_vals), ("T", t_vals), ("U", u_vals), ("V", v_vals),
|
("S", s_vals), ("T", t_vals), ("U", u_vals),
|
||||||
|
("X", x_vals), ("Y", y_vals), ("Z", z_vals),
|
||||||
]
|
]
|
||||||
for col_letter, vals in cols:
|
for col_letter, vals in cols:
|
||||||
put_values(token, sid, f"{col_letter}{sr}:{col_letter}{er}", vals)
|
put_values(token, sid, f"{col_letter}{sr}:{col_letter}{er}", vals)
|
||||||
|
|||||||
@ -3,13 +3,13 @@
|
|||||||
端内析出leads数据 — 自动回填 + 统计汇总脚本
|
端内析出leads数据 — 自动回填 + 统计汇总脚本
|
||||||
|
|
||||||
流程:
|
流程:
|
||||||
1. 读取曲慧萌/吴迪 sheet 中 A 列的手机号
|
1. 读取吴迪 sheet 中 B 列的手机号
|
||||||
2. XXTEA 加密 → 匹配 bi_vala_app_account.tel_encrypt → 获取 account_id
|
2. XXTEA 加密 → 匹配 bi_vala_app_account.tel_encrypt → 获取 account_id
|
||||||
3. 查询注册日期、转化、退费、U0体验课完成日期 → 回写
|
3. 查询注册日期、转化(全部key_from,购课时间≥析出时间)、转化keyfrom、退费、U0体验课完成日期 → 回写
|
||||||
4. 按析出月份汇总统计 → 写入"统计" sheet
|
4. 按析出月份汇总统计 → 写入"统计" sheet
|
||||||
|
|
||||||
统计口径:
|
统计口径:
|
||||||
- 转化率 = 已转化leads / 有效析出leads(剔除端外购课用户)
|
- 转化率 = 已转化leads / 总leads
|
||||||
- 退费率 = 退费leads / 已转化leads
|
- 退费率 = 退费leads / 已转化leads
|
||||||
- 完成率 = 完成该课的leads / 总leads
|
- 完成率 = 完成该课的leads / 总leads
|
||||||
|
|
||||||
@ -26,17 +26,9 @@ from collections import defaultdict
|
|||||||
|
|
||||||
# ── 配置 ──────────────────────────────────────────────
|
# ── 配置 ──────────────────────────────────────────────
|
||||||
SPREADSHEET_TOKEN = "FA3xsw3kph4pdatKlUrcyPgInAc"
|
SPREADSHEET_TOKEN = "FA3xsw3kph4pdatKlUrcyPgInAc"
|
||||||
SHEET_QHM = "7f0e35" # 曲慧萌
|
|
||||||
SHEET_WD = "1K3O6s" # 吴迪
|
SHEET_WD = "1K3O6s" # 吴迪
|
||||||
SHEET_STAT = "scyF3H" # 统计
|
SHEET_STAT = "scyF3H" # 统计
|
||||||
|
|
||||||
# 端内渠道
|
|
||||||
INNER_CHANNELS = [
|
|
||||||
"app-active-h5-0-0",
|
|
||||||
"app-sales-bj-qhm-0",
|
|
||||||
"app-sales-bj-wd-0",
|
|
||||||
]
|
|
||||||
|
|
||||||
# U0 体验课 chapter_id
|
# U0 体验课 chapter_id
|
||||||
U0_CHAPTERS = {
|
U0_CHAPTERS = {
|
||||||
"L1-U0-L01": 343,
|
"L1-U0-L01": 343,
|
||||||
@ -67,9 +59,11 @@ COL_EXTRACT_DATE = 4 # E: 析出日期(手动填)
|
|||||||
COL_REG_DATE = 5 # F: 注册日期
|
COL_REG_DATE = 5 # F: 注册日期
|
||||||
COL_CONVERTED = 6 # G: 是否转化
|
COL_CONVERTED = 6 # G: 是否转化
|
||||||
COL_CONVERT_DATE = 7 # H: 转化日期
|
COL_CONVERT_DATE = 7 # H: 转化日期
|
||||||
COL_REFUND = 8 # I: 是否退费
|
COL_CONVERT_KEYFROM = 8 # I: 转化keyfrom
|
||||||
COL_REFUND_DATE = 9 # J: 退费日期
|
COL_CONVERT_GSV = 9 # J: 转化金额(GSV)(新增)
|
||||||
COL_U0_START = 10 # K-T: L1-U0-L01 ~ L2-U0-L05
|
COL_REFUND = 10 # K: 是否退费
|
||||||
|
COL_REFUND_DATE = 11 # L: 退费日期
|
||||||
|
COL_U0_START = 12 # M-W: L1-U0-L01 ~ L2-U0-L05
|
||||||
|
|
||||||
# ── 数据库 ─────────────────────────────────────────────
|
# ── 数据库 ─────────────────────────────────────────────
|
||||||
PG_HOST = "bj-postgres-16pob4sg.sql.tencentcdb.com"
|
PG_HOST = "bj-postgres-16pob4sg.sql.tencentcdb.com"
|
||||||
@ -180,64 +174,47 @@ def match_phones(phones: list[str]) -> dict[str, dict]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def query_conversion(account_ids: list[str]) -> dict[str, dict]:
|
def query_orders_and_refunds(account_ids: list[str]) -> dict[str, dict]:
|
||||||
|
"""
|
||||||
|
查询全部渠道订单及退费信息(不限 key_from)。
|
||||||
|
返回: {account_id: {"orders": [(pay_date, key_from, is_refunded), ...], "total_gmv_cents": int, "total_gsv_cents": int}}
|
||||||
|
orders 按 pay_success_date 升序排列。
|
||||||
|
"""
|
||||||
if not account_ids:
|
if not account_ids:
|
||||||
return {}
|
return {}
|
||||||
BATCH_SIZE = 100
|
BATCH_SIZE = 100
|
||||||
results = {}
|
results = {}
|
||||||
channels_str = ",".join(f"'{c}'" for c in INNER_CHANNELS)
|
|
||||||
for i in range(0, len(account_ids), BATCH_SIZE):
|
for i in range(0, len(account_ids), BATCH_SIZE):
|
||||||
batch = account_ids[i:i + BATCH_SIZE]
|
batch = account_ids[i:i + BATCH_SIZE]
|
||||||
ids_str = ",".join(batch)
|
ids_str = ",".join(batch)
|
||||||
|
# 查询每笔订单及退费状态
|
||||||
sql = f"""
|
sql = f"""
|
||||||
SELECT o.account_id::text,
|
SELECT o.account_id::text,
|
||||||
MIN(o.pay_success_date::date::text) AS first_pay_date,
|
o.pay_success_date::date::text,
|
||||||
BOOL_OR(r.id IS NOT NULL AND r.status = 3 AND o2.order_status = 4) AS has_refund,
|
o.key_from,
|
||||||
MIN(CASE WHEN r.id IS NOT NULL AND r.status = 3 AND o2.order_status = 4
|
o.pay_amount_int,
|
||||||
THEN r.created_at::date::text END) AS first_refund_date
|
COALESCE(SUM(CASE WHEN r.status = 3 AND o2.order_status = 4 THEN r.refund_amount::numeric * 100 ELSE 0 END), 0)::bigint AS total_refund_fen,
|
||||||
|
BOOL_OR(r.id IS NOT NULL AND r.status = 3 AND o2.order_status = 4) AS is_refunded
|
||||||
FROM bi_vala_order o
|
FROM bi_vala_order o
|
||||||
LEFT JOIN bi_refund_order r ON o.trade_no = r.trade_no AND r.status = 3
|
LEFT JOIN bi_refund_order r ON o.trade_no = r.trade_no
|
||||||
LEFT JOIN bi_vala_order o2 ON o.trade_no = o2.trade_no AND o2.order_status = 4
|
LEFT JOIN bi_vala_order o2 ON o.trade_no = o2.trade_no AND o2.order_status = 4
|
||||||
WHERE o.account_id IN ({ids_str})
|
WHERE o.account_id IN ({ids_str})
|
||||||
AND o.key_from IN ({channels_str})
|
|
||||||
AND o.pay_success_date IS NOT NULL
|
AND o.pay_success_date IS NOT NULL
|
||||||
AND o.order_status IN (3, 4)
|
AND o.order_status IN (3, 4)
|
||||||
GROUP BY o.account_id
|
GROUP BY o.account_id, o.pay_success_date, o.key_from, o.pay_amount_int
|
||||||
|
ORDER BY o.account_id, o.pay_success_date
|
||||||
"""
|
"""
|
||||||
for row in pg_query(sql):
|
for row in pg_query(sql):
|
||||||
if len(row) >= 3:
|
if len(row) >= 6:
|
||||||
acc_id, first_pay, has_refund = row[0], row[1], row[2]
|
acc_id, pay_date, key_from, pay_amount, total_refund_fen, is_refunded = row[0], row[1], row[2], row[3], row[4], row[5]
|
||||||
first_refund = row[3] if len(row) >= 4 else ""
|
if acc_id not in results:
|
||||||
results[acc_id] = {
|
results[acc_id] = {"orders": [], "total_gmv_cents": 0, "total_gsv_cents": 0}
|
||||||
"converted": "是" if first_pay else "否",
|
refunded = is_refunded in ("t", "true")
|
||||||
"convert_date": first_pay or "",
|
results[acc_id]["orders"].append((pay_date, key_from, refunded))
|
||||||
"refunded": "是" if has_refund in ("t", "true") else "否",
|
# 累加用户总 GMV(分)和 GSV(分)
|
||||||
"refund_date": first_refund or "",
|
results[acc_id]["total_gmv_cents"] += int(pay_amount)
|
||||||
}
|
gsv_fen = int(pay_amount) - int(total_refund_fen)
|
||||||
return results
|
results[acc_id]["total_gsv_cents"] += gsv_fen
|
||||||
|
|
||||||
|
|
||||||
def query_outside_conversion(account_ids: list[str]) -> set[str]:
|
|
||||||
"""查询哪些账号有端外购课(key_from 不在端内渠道中)"""
|
|
||||||
if not account_ids:
|
|
||||||
return set()
|
|
||||||
BATCH_SIZE = 100
|
|
||||||
results = set()
|
|
||||||
channels_str = ",".join(f"'{c}'" for c in INNER_CHANNELS)
|
|
||||||
for i in range(0, len(account_ids), BATCH_SIZE):
|
|
||||||
batch = account_ids[i:i + BATCH_SIZE]
|
|
||||||
ids_str = ",".join(batch)
|
|
||||||
sql = f"""
|
|
||||||
SELECT DISTINCT o.account_id::text
|
|
||||||
FROM bi_vala_order o
|
|
||||||
WHERE o.account_id IN ({ids_str})
|
|
||||||
AND o.key_from NOT IN ({channels_str})
|
|
||||||
AND o.pay_success_date IS NOT NULL
|
|
||||||
AND o.order_status IN (3, 4)
|
|
||||||
"""
|
|
||||||
for row in pg_query(sql):
|
|
||||||
if len(row) >= 1:
|
|
||||||
results.add(row[0])
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
@ -305,7 +282,7 @@ def process_sheet(sheet_id: str, sheet_name: str, dry_run: bool = False) -> list
|
|||||||
print(f"处理 Sheet: {sheet_name} ({sheet_id})")
|
print(f"处理 Sheet: {sheet_name} ({sheet_id})")
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
|
|
||||||
range_str = f"{sheet_id}!A2:T"
|
range_str = f"{sheet_id}!A2:W"
|
||||||
try:
|
try:
|
||||||
rows = lark_read(sheet_id, range_str)
|
rows = lark_read(sheet_id, range_str)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -339,11 +316,8 @@ def process_sheet(sheet_id: str, sheet_name: str, dry_run: bool = False) -> list
|
|||||||
matched_accounts = [info["id"] for info in acc_info.values()]
|
matched_accounts = [info["id"] for info in acc_info.values()]
|
||||||
matched_phones = set(acc_info.keys())
|
matched_phones = set(acc_info.keys())
|
||||||
|
|
||||||
print("→ 查询转化信息...")
|
print("→ 查询订单及退费信息(全部渠道)...")
|
||||||
conv_info = query_conversion(matched_accounts)
|
order_info = query_orders_and_refunds(matched_accounts)
|
||||||
|
|
||||||
print("→ 查询端外购课...")
|
|
||||||
outside_accounts = query_outside_conversion(matched_accounts)
|
|
||||||
|
|
||||||
print("→ 查询 U0 学习进度...")
|
print("→ 查询 U0 学习进度...")
|
||||||
learn_info = query_learning(matched_accounts)
|
learn_info = query_learning(matched_accounts)
|
||||||
@ -357,7 +331,6 @@ def process_sheet(sheet_id: str, sheet_name: str, dry_run: bool = False) -> list
|
|||||||
if not info:
|
if not info:
|
||||||
continue
|
continue
|
||||||
acc_id = info["id"]
|
acc_id = info["id"]
|
||||||
conv = conv_info.get(acc_id, {})
|
|
||||||
learn = learn_info.get(acc_id, {})
|
learn = learn_info.get(acc_id, {})
|
||||||
|
|
||||||
for row_idx in row_indices:
|
for row_idx in row_indices:
|
||||||
@ -366,32 +339,59 @@ def process_sheet(sheet_id: str, sheet_name: str, dry_run: bool = False) -> list
|
|||||||
if len(rows[row_idx]) > COL_EXTRACT_DATE and rows[row_idx][COL_EXTRACT_DATE]:
|
if len(rows[row_idx]) > COL_EXTRACT_DATE and rows[row_idx][COL_EXTRACT_DATE]:
|
||||||
extract_date = str(rows[row_idx][COL_EXTRACT_DATE]).strip()
|
extract_date = str(rows[row_idx][COL_EXTRACT_DATE]).strip()
|
||||||
|
|
||||||
# F 列逻辑:端内购课 > 端外购课 > 否
|
# 转化判断:全部渠道,购课时间 ≥ 析出时间
|
||||||
is_inner = conv.get("converted", "否") == "是"
|
# 退费判断:只要存在一笔 ≥ 析出日期且未退费的订单,就算未退费
|
||||||
is_outside = acc_id in outside_accounts
|
# 转化金额:用户全部订单 GSV(全部 pay_amount - 全部 refund_amount)
|
||||||
if is_inner:
|
orders = order_info.get(acc_id, {})
|
||||||
f_value = "是"
|
conv_date = ""
|
||||||
convert_date = conv.get("convert_date", "")
|
conv_keyfrom = ""
|
||||||
elif is_outside:
|
conv_gsv = ""
|
||||||
f_value = "端外购课"
|
is_refunded = "否"
|
||||||
convert_date = ""
|
|
||||||
|
# 析出日期可能是 Excel 序列数字,统一转为 YYYY-MM-DD
|
||||||
|
extract_date_ymd = ""
|
||||||
|
if extract_date:
|
||||||
|
if extract_date.isdigit():
|
||||||
|
extract_date_ymd = excel_serial_to_date(int(extract_date))
|
||||||
else:
|
else:
|
||||||
f_value = "否"
|
extract_date_ymd = extract_date
|
||||||
convert_date = ""
|
|
||||||
|
if extract_date_ymd:
|
||||||
|
first_match = None
|
||||||
|
has_valid_order = False
|
||||||
|
for pay_date, kf, refunded in orders.get("orders", []):
|
||||||
|
if pay_date >= extract_date_ymd:
|
||||||
|
if first_match is None:
|
||||||
|
first_match = (pay_date, kf)
|
||||||
|
if not refunded:
|
||||||
|
has_valid_order = True
|
||||||
|
if first_match:
|
||||||
|
conv_date = first_match[0]
|
||||||
|
conv_keyfrom = first_match[1]
|
||||||
|
# 转化金额 = 用户总 GSV(元)
|
||||||
|
total_gsv = orders.get("total_gsv_cents", 0)
|
||||||
|
conv_gsv = str(round(total_gsv / 100, 2))
|
||||||
|
is_refunded = "否" if has_valid_order else "是"
|
||||||
|
|
||||||
|
f_value = "是" if conv_date else "否"
|
||||||
|
|
||||||
updates.append((row_idx, COL_USER_ID, acc_id))
|
updates.append((row_idx, COL_USER_ID, acc_id))
|
||||||
updates.append((row_idx, COL_REG_DATE, info.get("created_at", "")))
|
updates.append((row_idx, COL_REG_DATE, info.get("created_at", "")))
|
||||||
updates.append((row_idx, COL_CONVERTED, f_value))
|
updates.append((row_idx, COL_CONVERTED, f_value))
|
||||||
updates.append((row_idx, COL_CONVERT_DATE, convert_date))
|
updates.append((row_idx, COL_CONVERT_DATE, conv_date))
|
||||||
updates.append((row_idx, COL_REFUND, conv.get("refunded", "否")))
|
updates.append((row_idx, COL_CONVERT_KEYFROM, conv_keyfrom))
|
||||||
updates.append((row_idx, COL_REFUND_DATE, conv.get("refund_date", "")))
|
updates.append((row_idx, COL_CONVERT_GSV, conv_gsv))
|
||||||
|
updates.append((row_idx, COL_REFUND, is_refunded))
|
||||||
|
updates.append((row_idx, COL_REFUND_DATE, ""))
|
||||||
for col_offset, lesson_name in enumerate(U0_COL_ORDER):
|
for col_offset, lesson_name in enumerate(U0_COL_ORDER):
|
||||||
updates.append((row_idx, COL_U0_START + col_offset, learn.get(lesson_name, "")))
|
updates.append((row_idx, COL_U0_START + col_offset, learn.get(lesson_name, "")))
|
||||||
|
|
||||||
lead_data.append({
|
lead_data.append({
|
||||||
"extract_date": extract_date,
|
"extract_date": extract_date,
|
||||||
"converted": f_value,
|
"converted": f_value,
|
||||||
"refunded": conv.get("refunded", "否"),
|
"refunded": is_refunded,
|
||||||
|
"gmv_cents": orders.get("total_gmv_cents", 0),
|
||||||
|
"gsv_cents": orders.get("total_gsv_cents", 0),
|
||||||
"lessons": {k: learn.get(k, "") for k in U0_COL_ORDER},
|
"lessons": {k: learn.get(k, "") for k in U0_COL_ORDER},
|
||||||
"has_phone": True,
|
"has_phone": True,
|
||||||
})
|
})
|
||||||
@ -411,6 +411,8 @@ def process_sheet(sheet_id: str, sheet_name: str, dry_run: bool = False) -> list
|
|||||||
"extract_date": extract_date,
|
"extract_date": extract_date,
|
||||||
"converted": existing_converted,
|
"converted": existing_converted,
|
||||||
"refunded": "否",
|
"refunded": "否",
|
||||||
|
"gmv_cents": 0,
|
||||||
|
"gsv_cents": 0,
|
||||||
"lessons": {k: "" for k in U0_COL_ORDER},
|
"lessons": {k: "" for k in U0_COL_ORDER},
|
||||||
"has_phone": False,
|
"has_phone": False,
|
||||||
})
|
})
|
||||||
@ -431,16 +433,16 @@ def process_sheet(sheet_id: str, sheet_name: str, dry_run: bool = False) -> list
|
|||||||
lark_write(sheet_id, f"{sheet_id}!D{actual_row}:D{actual_row}",
|
lark_write(sheet_id, f"{sheet_id}!D{actual_row}:D{actual_row}",
|
||||||
[[str(col_vals[COL_USER_ID])]])
|
[[str(col_vals[COL_USER_ID])]])
|
||||||
|
|
||||||
# F-T: 注册日期 ~ L2-U0-L5
|
# F-V: 注册日期 ~ L2-U0-L5(含 I 列转化keyfrom)
|
||||||
f_to_t = []
|
f_to_w = []
|
||||||
for col in range(COL_REG_DATE, COL_U0_START + len(U0_COL_ORDER)):
|
for col in range(COL_REG_DATE, COL_U0_START + len(U0_COL_ORDER)):
|
||||||
val = col_vals.get(col, "")
|
val = col_vals.get(col, "")
|
||||||
f_to_t.append(str(val) if val else "")
|
f_to_w.append(str(val) if val else "")
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
print(f" [DRY-RUN] {sheet_id}!D{actual_row} + F{actual_row}:T{actual_row} ← ...")
|
print(f" [DRY-RUN] {sheet_id}!D{actual_row} + F{actual_row}:W{actual_row} ← ...")
|
||||||
else:
|
else:
|
||||||
lark_write(sheet_id, f"{sheet_id}!F{actual_row}:T{actual_row}", [f_to_t])
|
lark_write(sheet_id, f"{sheet_id}!F{actual_row}:W{actual_row}", [f_to_w])
|
||||||
print(f" ✓ 行 {actual_row} 回写成功")
|
print(f" ✓ 行 {actual_row} 回写成功")
|
||||||
|
|
||||||
unmatched = set(phones) - matched_phones
|
unmatched = set(phones) - matched_phones
|
||||||
@ -458,8 +460,7 @@ def compute_stats(lead_data: list[dict]) -> dict[str, dict]:
|
|||||||
"""
|
"""
|
||||||
按析出月份汇总统计
|
按析出月份汇总统计
|
||||||
口径:
|
口径:
|
||||||
- 有效析出用户数 = 总leads - 端外购课leads
|
- 转化率 = 已转化leads / 总leads
|
||||||
- 转化率 = 已转化leads / 有效析出用户数
|
|
||||||
- 退费率 = 退费leads / 已转化leads
|
- 退费率 = 退费leads / 已转化leads
|
||||||
- 完成率 = 完成该课的leads / 总leads
|
- 完成率 = 完成该课的leads / 总leads
|
||||||
"""
|
"""
|
||||||
@ -494,30 +495,32 @@ def compute_stats(lead_data: list[dict]) -> dict[str, dict]:
|
|||||||
for month, leads in sorted(month_groups.items()):
|
for month, leads in sorted(month_groups.items()):
|
||||||
total = len(leads)
|
total = len(leads)
|
||||||
matched = sum(1 for l in leads if l.get("has_phone", False))
|
matched = sum(1 for l in leads if l.get("has_phone", False))
|
||||||
outside_only = sum(1 for l in leads if l["converted"] == "端外购课")
|
|
||||||
converted_all = sum(1 for l in leads if l["converted"] == "是")
|
converted_all = sum(1 for l in leads if l["converted"] == "是")
|
||||||
refunded = sum(1 for l in leads if l["refunded"] == "是")
|
refunded = sum(1 for l in leads if l["refunded"] == "是")
|
||||||
converted_unrefunded = sum(1 for l in leads if l["converted"] == "是" and l["refunded"] != "是")
|
converted_unrefunded = sum(1 for l in leads if l["converted"] == "是" and l["refunded"] != "是")
|
||||||
|
|
||||||
# 转化率分母剔除端外购课用户
|
conv_rate = converted_all / total * 100 if total > 0 else 0
|
||||||
effective_total = total - outside_only
|
|
||||||
conv_rate = converted_all / effective_total * 100 if effective_total > 0 else 0
|
|
||||||
refund_rate = refunded / converted_all * 100 if converted_all > 0 else 0
|
refund_rate = refunded / converted_all * 100 if converted_all > 0 else 0
|
||||||
|
|
||||||
|
# GMV / GSV 汇总(元)
|
||||||
|
total_gmv = sum(l.get("gmv_cents", 0) for l in leads) / 100
|
||||||
|
total_gsv = sum(l.get("gsv_cents", 0) for l in leads) / 100
|
||||||
|
|
||||||
lesson_rates = {}
|
lesson_rates = {}
|
||||||
for lesson_name in U0_COL_ORDER:
|
for lesson_name in U0_COL_ORDER:
|
||||||
completed = sum(1 for l in leads if l["lessons"].get(lesson_name, ""))
|
completed = sum(1 for l in leads if l["lessons"].get(lesson_name, ""))
|
||||||
lesson_rates[lesson_name] = completed / total * 100 if total > 0 else 0
|
lesson_rates[lesson_name] = completed / total * 100 if total > 0 else 0
|
||||||
|
|
||||||
result[month] = {
|
result[month] = {
|
||||||
"total": effective_total,
|
"total": total,
|
||||||
"matched": matched,
|
"matched": matched,
|
||||||
"outside_only": outside_only,
|
|
||||||
"converted_all": converted_all,
|
"converted_all": converted_all,
|
||||||
"converted_unrefunded": converted_unrefunded,
|
"converted_unrefunded": converted_unrefunded,
|
||||||
"refunded": refunded,
|
"refunded": refunded,
|
||||||
"conv_rate": conv_rate,
|
"conv_rate": conv_rate,
|
||||||
"refund_rate": refund_rate,
|
"refund_rate": refund_rate,
|
||||||
|
"gmv": total_gmv,
|
||||||
|
"gsv": total_gsv,
|
||||||
"lesson_rates": lesson_rates,
|
"lesson_rates": lesson_rates,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -532,21 +535,21 @@ def write_all_stats(all_stats: dict[str, dict[str, dict]], dry_run: bool = False
|
|||||||
按 销售+月份 逐行写入,从第2行开始
|
按 销售+月份 逐行写入,从第2行开始
|
||||||
"""
|
"""
|
||||||
# 先写表头
|
# 先写表头
|
||||||
header = ["销售", "月份", "有效析出用户数", "匹配用户数", "转化用户数", "转化率", "退费率"] + \
|
header = ["销售", "月份", "总析出用户数", "匹配用户数", "转化用户数", "转化率", "退费率", "GMV", "GSV"] + \
|
||||||
[f"{name}完成率" for name in U0_COL_ORDER]
|
[f"{name}完成率" for name in U0_COL_ORDER]
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
lark_write(SHEET_STAT, f"{SHEET_STAT}!A1:Q1", [header])
|
lark_write(SHEET_STAT, f"{SHEET_STAT}!A1:S1", [header])
|
||||||
|
|
||||||
# 构建有序行列表: [(sales_name, month, stats), ...]
|
# 构建有序行列表: [(sales_name, month, stats), ...]
|
||||||
rows_data = []
|
rows_data = []
|
||||||
for sales_name in ["曲慧萌", "吴迪"]:
|
for sales_name in ["吴迪"]:
|
||||||
stats = all_stats.get(sales_name, {})
|
stats = all_stats.get(sales_name, {})
|
||||||
for month in sorted(stats.keys()):
|
for month in sorted(stats.keys()):
|
||||||
rows_data.append((sales_name, month, stats[month]))
|
rows_data.append((sales_name, month, stats[month]))
|
||||||
|
|
||||||
# 先清除统计 sheet 旧数据(A2:Q50),避免残留旧行
|
# 先清除统计 sheet 旧数据(A2:S50),避免残留旧行
|
||||||
print(" → 清除统计 sheet 旧数据...")
|
print(" → 清除统计 sheet 旧数据...")
|
||||||
lark_write(SHEET_STAT, f"{SHEET_STAT}!A2:Q50", [[""] * 17] * 49)
|
lark_write(SHEET_STAT, f"{SHEET_STAT}!A2:S50", [[""] * 19] * 49)
|
||||||
|
|
||||||
if not rows_data:
|
if not rows_data:
|
||||||
print(" 无统计数据")
|
print(" 无统计数据")
|
||||||
@ -559,7 +562,7 @@ def write_all_stats(all_stats: dict[str, dict[str, dict]], dry_run: bool = False
|
|||||||
lark_write(SHEET_STAT, f"{SHEET_STAT}!A{row_num}:A{row_num}", [[sales_name]])
|
lark_write(SHEET_STAT, f"{SHEET_STAT}!A{row_num}:A{row_num}", [[sales_name]])
|
||||||
# B: 月份
|
# B: 月份
|
||||||
lark_write(SHEET_STAT, f"{SHEET_STAT}!B{row_num}:B{row_num}", [[month]])
|
lark_write(SHEET_STAT, f"{SHEET_STAT}!B{row_num}:B{row_num}", [[month]])
|
||||||
# C: 有效析出用户数(剔除端外购课)
|
# C: 总析出用户数
|
||||||
lark_write(SHEET_STAT, f"{SHEET_STAT}!C{row_num}:C{row_num}", [[s["total"]]])
|
lark_write(SHEET_STAT, f"{SHEET_STAT}!C{row_num}:C{row_num}", [[s["total"]]])
|
||||||
# D: 匹配用户数
|
# D: 匹配用户数
|
||||||
lark_write(SHEET_STAT, f"{SHEET_STAT}!D{row_num}:D{row_num}", [[s["matched"]]])
|
lark_write(SHEET_STAT, f"{SHEET_STAT}!D{row_num}:D{row_num}", [[s["matched"]]])
|
||||||
@ -569,12 +572,16 @@ def write_all_stats(all_stats: dict[str, dict[str, dict]], dry_run: bool = False
|
|||||||
lark_write(SHEET_STAT, f"{SHEET_STAT}!F{row_num}:F{row_num}", [[round(s["conv_rate"] / 100, 3)]])
|
lark_write(SHEET_STAT, f"{SHEET_STAT}!F{row_num}:F{row_num}", [[round(s["conv_rate"] / 100, 3)]])
|
||||||
# G: 退费率
|
# G: 退费率
|
||||||
lark_write(SHEET_STAT, f"{SHEET_STAT}!G{row_num}:G{row_num}", [[round(s["refund_rate"] / 100, 3)]])
|
lark_write(SHEET_STAT, f"{SHEET_STAT}!G{row_num}:G{row_num}", [[round(s["refund_rate"] / 100, 3)]])
|
||||||
# H-Q: 完成率
|
# H: GMV(元)
|
||||||
|
lark_write(SHEET_STAT, f"{SHEET_STAT}!H{row_num}:H{row_num}", [[round(s["gmv"], 2)]])
|
||||||
|
# I: GSV(元)
|
||||||
|
lark_write(SHEET_STAT, f"{SHEET_STAT}!I{row_num}:I{row_num}", [[round(s["gsv"], 2)]])
|
||||||
|
# J-S: 完成率
|
||||||
lesson_vals = [round(s["lesson_rates"][name] / 100, 3) for name in U0_COL_ORDER]
|
lesson_vals = [round(s["lesson_rates"][name] / 100, 3) for name in U0_COL_ORDER]
|
||||||
lark_write(SHEET_STAT, f"{SHEET_STAT}!H{row_num}:Q{row_num}", [lesson_vals])
|
lark_write(SHEET_STAT, f"{SHEET_STAT}!J{row_num}:S{row_num}", [lesson_vals])
|
||||||
|
|
||||||
print(f" ✓ {sales_name} {month}: 有效析出={s['total']} 匹配={s['matched']} 转化={s['converted_all']} "
|
print(f" ✓ {sales_name} {month}: 总析出={s['total']} 匹配={s['matched']} 转化={s['converted_all']} "
|
||||||
f"转化率={s['conv_rate']:.1f}% 退费率={s['refund_rate']:.1f}%")
|
f"转化率={s['conv_rate']:.1f}% 退费率={s['refund_rate']:.1f}% GMV={s['gmv']:.2f} GSV={s['gsv']:.2f}")
|
||||||
|
|
||||||
|
|
||||||
# ── 主流程 ──────────────────────────────────────────────
|
# ── 主流程 ──────────────────────────────────────────────
|
||||||
@ -584,8 +591,7 @@ def main():
|
|||||||
if dry_run:
|
if dry_run:
|
||||||
print("⚠️ DRY-RUN 模式,不会实际写入\n")
|
print("⚠️ DRY-RUN 模式,不会实际写入\n")
|
||||||
|
|
||||||
# 处理两个销售 sheet
|
# 处理吴迪 sheet
|
||||||
qhm_data = process_sheet(SHEET_QHM, "曲慧萌", dry_run)
|
|
||||||
wd_data = process_sheet(SHEET_WD, "吴迪", dry_run)
|
wd_data = process_sheet(SHEET_WD, "吴迪", dry_run)
|
||||||
|
|
||||||
# 汇总统计
|
# 汇总统计
|
||||||
@ -593,15 +599,14 @@ def main():
|
|||||||
print("汇总统计 → 统计 sheet")
|
print("汇总统计 → 统计 sheet")
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
|
|
||||||
qhm_stats = compute_stats(qhm_data)
|
|
||||||
wd_stats = compute_stats(wd_data)
|
wd_stats = compute_stats(wd_data)
|
||||||
|
|
||||||
all_stats = {"曲慧萌": qhm_stats, "吴迪": wd_stats}
|
all_stats = {"吴迪": wd_stats}
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
for sales_name, stats in all_stats.items():
|
for sales_name, stats in all_stats.items():
|
||||||
for month, s in stats.items():
|
for month, s in stats.items():
|
||||||
print(f" [DRY-RUN] {sales_name} {month}: 有效析出={s['total']} 匹配={s['matched']} 转化={s['converted_all']} 转化率={s['conv_rate']:.1f}% 退费率={s['refund_rate']:.1f}%")
|
print(f" [DRY-RUN] {sales_name} {month}: 总析出={s['total']} 匹配={s['matched']} 转化={s['converted_all']} 转化率={s['conv_rate']:.1f}% 退费率={s['refund_rate']:.1f}% GMV={s['gmv']:.2f} GSV={s['gsv']:.2f}")
|
||||||
else:
|
else:
|
||||||
write_all_stats(all_stats, dry_run)
|
write_all_stats(all_stats, dry_run)
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,6 @@ APP_ID = "cli_a929ae22e0b8dcc8"
|
|||||||
APP_SECRET = "OtFjMy7p3qE3VvLbMdcWidwgHOnGD4FJ"
|
APP_SECRET = "OtFjMy7p3qE3VvLbMdcWidwgHOnGD4FJ"
|
||||||
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
|
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
|
||||||
SALES_SHEETS = {"f975f0": "吴迪", "qJF4I": "小龙", "qJF4J": "成都"}
|
SALES_SHEETS = {"f975f0": "吴迪", "qJF4I": "小龙", "qJF4J": "成都"}
|
||||||
DIRECT_SHEET = "1sosYE" # 直购表(小红书店铺+stream-xhs,不依赖手机号匹配)
|
|
||||||
SUMMARY_SHEET = "2smjwA"
|
SUMMARY_SHEET = "2smjwA"
|
||||||
|
|
||||||
def _get_pg_password():
|
def _get_pg_password():
|
||||||
@ -188,24 +187,6 @@ def main():
|
|||||||
|
|
||||||
print(f"Total rows: {len(all_rows)}")
|
print(f"Total rows: {len(all_rows)}")
|
||||||
|
|
||||||
# ── Step 1.5: 读取直购表 ──
|
|
||||||
print(f"Reading 直购...")
|
|
||||||
direct_vals = read_sheet(token, DIRECT_SHEET, "A2:V10000")
|
|
||||||
direct_rows = []
|
|
||||||
for i, row in enumerate(direct_vals):
|
|
||||||
while len(row) < 22:
|
|
||||||
row.append("")
|
|
||||||
b = str(row[1]).strip() if row[1] else ""
|
|
||||||
h = str(row[7]).strip() if row[7] else ""
|
|
||||||
if b or h:
|
|
||||||
direct_rows.append({
|
|
||||||
"sid": DIRECT_SHEET, "name": "直购", "row": i + 2,
|
|
||||||
"raw": row[:22],
|
|
||||||
})
|
|
||||||
print(f" {len(direct_rows)} non-empty rows")
|
|
||||||
all_rows.extend(direct_rows)
|
|
||||||
print(f"Total rows (with 直购): {len(all_rows)}")
|
|
||||||
|
|
||||||
# ── Step 2: 筛选进订单汇总的行 ──
|
# ── Step 2: 筛选进订单汇总的行 ──
|
||||||
# 条件:K=是 · O>0 · 非全额退(P空或P<O) · L≥C
|
# 条件:K=是 · O>0 · 非全额退(P空或P<O) · L≥C
|
||||||
order_rows = []
|
order_rows = []
|
||||||
@ -245,7 +226,7 @@ def main():
|
|||||||
|
|
||||||
print(f"Order rows after filter: {len(order_rows)}")
|
print(f"Order rows after filter: {len(order_rows)}")
|
||||||
|
|
||||||
# ── Step 2.5: 去重(同一人可能在三表+直购中出现多次)──
|
# ── Step 2.5: 去重(同一人可能在三表中出现多次)──
|
||||||
# 按 (A销售归属, B微信昵称, O下单金额, P退款金额, L下单日期) 去重
|
# 按 (A销售归属, B微信昵称, O下单金额, P退款金额, L下单日期) 去重
|
||||||
seen = set()
|
seen = set()
|
||||||
deduped = []
|
deduped = []
|
||||||
|
|||||||
@ -6,15 +6,15 @@
|
|||||||
1. 读取「小龙」「吴迪」「成都」三个 sheet 的 E 列手机号
|
1. 读取「小龙」「吴迪」「成都」三个 sheet 的 E 列手机号
|
||||||
2. XXTEA 加密 → bi_vala_app_account.tel_encrypt 精确匹配 → 获取 account_id
|
2. XXTEA 加密 → bi_vala_app_account.tel_encrypt 精确匹配 → 获取 account_id
|
||||||
3. 查询 PostgreSQL 获取用户订单/学习数据
|
3. 查询 PostgreSQL 获取用户订单/学习数据
|
||||||
4. 填写 H~V 列(自动列),V 列为操作更新时间
|
4. 填写 D/H/I/J/K~U/X/Y/Z 列(自动列),U 列为操作更新时间
|
||||||
5. 将三个 sheet 中 K=是(已下单)的用户汇总到「订单汇总」sheet
|
5. 将三个 sheet 中 Y=1(有效订单)的用户汇总到「订单汇总」sheet
|
||||||
|
|
||||||
规则(沿用 S2 规则):
|
规则(沿用 S2 规则):
|
||||||
① E→H: XXTEA 精确匹配, 查不到留空
|
① E→H: XXTEA 精确匹配, 查不到留空
|
||||||
② H→D/I/J: 只补空, 不覆盖已有值
|
② H→D/I/J: 只补空, 不覆盖已有值
|
||||||
③ K=是: 仅当 L(下单日) >= C(线索日期)
|
③ Y=1: 仅当 K(下单日) >= C(线索日期)
|
||||||
④ 全额退清: 所有订单都退费 → K/O/P/Q 全部清空
|
④ 全额退清: 所有订单都退费 → N/O/P 全部清空
|
||||||
⑤ O/P/Q 0 留空, P 整元
|
⑤ N/O/P 0 留空, O 整元
|
||||||
⑥ G 列不动
|
⑥ G 列不动
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
@ -36,9 +36,9 @@ from phone_encrypt import encrypt_phone
|
|||||||
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
|
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
|
||||||
|
|
||||||
SALES_SHEETS = [
|
SALES_SHEETS = [
|
||||||
("qJF4I", "小龙", "A3:V2607"),
|
("qJF4I", "小龙", "A3:Z2607"),
|
||||||
("f975f0", "吴迪", "A3:V8149"),
|
("f975f0", "吴迪", "A3:Z8149"),
|
||||||
("qJF4J", "成都", "A3:V2500"),
|
("qJF4J", "成都", "A3:Z2500"),
|
||||||
]
|
]
|
||||||
|
|
||||||
SUMMARY_SHEET_ID = "2smjwA"
|
SUMMARY_SHEET_ID = "2smjwA"
|
||||||
@ -60,6 +60,26 @@ CHANNEL_MAP = {
|
|||||||
"官网": "官网",
|
"官网": "官网",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def classify_channel(keyfrom):
|
||||||
|
"""将 keyfrom 归类为 销转/直购/端内/达人/其他"""
|
||||||
|
if not keyfrom:
|
||||||
|
return ""
|
||||||
|
kf = keyfrom.lower()
|
||||||
|
if any(kw in kf for kw in ["newmedia-daren", "jingxuan", "stream-daren"]):
|
||||||
|
return "达人"
|
||||||
|
if any(kw in kf for kw in ["sales-adp", "stream-wxxd"]):
|
||||||
|
return "销转"
|
||||||
|
if "微信小店" in keyfrom:
|
||||||
|
return "销转"
|
||||||
|
if any(kw in kf for kw in ["app-active", "app-sales", "partner-active"]):
|
||||||
|
return "端内"
|
||||||
|
if any(kw in kf for kw in ["dianpu-", "stream-xhs"]):
|
||||||
|
return "直购"
|
||||||
|
if any(kw in keyfrom for kw in ["抖音", "直购", "小红书"]):
|
||||||
|
return "直购"
|
||||||
|
return "其他"
|
||||||
|
|
||||||
LOG_FILE = "/var/log/xiaoxi_full_refresh.log"
|
LOG_FILE = "/var/log/xiaoxi_full_refresh.log"
|
||||||
|
|
||||||
|
|
||||||
@ -204,7 +224,9 @@ def parse_sales_sheets(token):
|
|||||||
"S": safe_cell(row, 18),
|
"S": safe_cell(row, 18),
|
||||||
"T": safe_cell(row, 19),
|
"T": safe_cell(row, 19),
|
||||||
"U": safe_cell(row, 20),
|
"U": safe_cell(row, 20),
|
||||||
"V": safe_cell(row, 21),
|
"X": safe_cell(row, 23),
|
||||||
|
"Y": safe_cell(row, 24),
|
||||||
|
"Z": safe_cell(row, 25),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -299,6 +321,7 @@ def query_all_pg(all_entries, phone_map):
|
|||||||
"has_order": False, "order_date": "", "order_channel": "", "product": "",
|
"has_order": False, "order_date": "", "order_channel": "", "product": "",
|
||||||
"gmv": 0, "refund": 0, "gsv": 0,
|
"gmv": 0, "refund": 0, "gsv": 0,
|
||||||
"activation": "", "lesson_progress": "", "lesson_time": "", "lesson_minutes": 0,
|
"activation": "", "lesson_progress": "", "lesson_time": "", "lesson_minutes": 0,
|
||||||
|
"trade_no": "",
|
||||||
} for uid in uid_set}
|
} for uid in uid_set}
|
||||||
|
|
||||||
# 3a. 注册信息
|
# 3a. 注册信息
|
||||||
@ -354,9 +377,17 @@ def query_all_pg(all_entries, phone_map):
|
|||||||
continue
|
continue
|
||||||
info[aid]["has_order"] = True
|
info[aid]["has_order"] = True
|
||||||
latest = olist[0]
|
latest = olist[0]
|
||||||
info[aid]["order_date"] = latest[2].strftime("%Y-%m-%d") if latest[2] else ""
|
# K列格式: M月D日 HH:MM:SS, 同时保留 raw 用于日期比较
|
||||||
|
if latest[2]:
|
||||||
|
dt = latest[2]
|
||||||
|
info[aid]["order_date"] = f"{dt.month}月{dt.day}日 {dt.strftime('%H:%M:%S')}"
|
||||||
|
info[aid]["order_date_raw"] = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
else:
|
||||||
|
info[aid]["order_date"] = ""
|
||||||
|
info[aid]["order_date_raw"] = ""
|
||||||
info[aid]["order_channel"] = latest[3] or ""
|
info[aid]["order_channel"] = latest[3] or ""
|
||||||
info[aid]["product"] = GOODS_NAMES.get(latest[4], f"商品{latest[4]}")
|
info[aid]["product"] = GOODS_NAMES.get(latest[4], f"商品{latest[4]}")
|
||||||
|
info[aid]["trade_no"] = latest[1] or ""
|
||||||
total_gmv = sum(o[5] for o in olist) / 100.0
|
total_gmv = sum(o[5] for o in olist) / 100.0
|
||||||
total_refund = sum(refund_map.get(o[1], 0) for o in olist) / 100.0
|
total_refund = sum(refund_map.get(o[1], 0) for o in olist) / 100.0
|
||||||
info[aid]["gmv"] = total_gmv
|
info[aid]["gmv"] = total_gmv
|
||||||
@ -469,7 +500,7 @@ def query_all_pg(all_entries, phone_map):
|
|||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
# ═══ Step 4: 写入销售三表 H~V 列 ═══
|
# ═══ Step 4: 写入销售三表 D/H/I/J/K~U/X/Y/Z 列 ═══
|
||||||
|
|
||||||
def write_sales_sheets(token, all_entries, phone_map, db_info):
|
def write_sales_sheets(token, all_entries, phone_map, db_info):
|
||||||
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
@ -496,7 +527,8 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
|||||||
d_vals, h_vals, i_vals, j_vals = [], [], [], []
|
d_vals, h_vals, i_vals, j_vals = [], [], [], []
|
||||||
k_vals, l_vals, m_vals, n_vals = [], [], [], []
|
k_vals, l_vals, m_vals, n_vals = [], [], [], []
|
||||||
o_vals, p_vals, q_vals, r_vals = [], [], [], []
|
o_vals, p_vals, q_vals, r_vals = [], [], [], []
|
||||||
s_vals, t_vals, u_vals, v_vals = [], [], [], []
|
s_vals, t_vals, u_vals = [], [], []
|
||||||
|
x_vals, y_vals, z_vals = [], [], []
|
||||||
|
|
||||||
for e in g:
|
for e in g:
|
||||||
phone = e["phone"]
|
phone = e["phone"]
|
||||||
@ -551,52 +583,70 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
|||||||
gsv_int = int(di["gsv"])
|
gsv_int = int(di["gsv"])
|
||||||
is_full_refund = (gmv_int > 0 and gmv_int == refund_int)
|
is_full_refund = (gmv_int > 0 and gmv_int == refund_int)
|
||||||
|
|
||||||
# K=是: 有订单 且 非全额退清 且 L(下单日) >= C(线索日期)
|
# 有效订单判定: 有订单 且 非全额退清 且 K(下单日) >= C(线索日期)
|
||||||
order_date = di["order_date"]
|
order_date = di["order_date"]
|
||||||
should_k_yes = di["has_order"] and not is_full_refund
|
should_y_yes = di["has_order"] and not is_full_refund
|
||||||
|
|
||||||
# 日期比较: L >= C
|
# 日期比较: K >= C (用 raw 格式比较)
|
||||||
if should_k_yes and clue_date and order_date:
|
order_date_raw = di.get("order_date_raw", "")
|
||||||
if order_date < clue_date:
|
if should_y_yes and clue_date and order_date_raw:
|
||||||
should_k_yes = False
|
if order_date_raw[:10] < clue_date:
|
||||||
|
should_y_yes = False
|
||||||
|
|
||||||
if is_full_refund:
|
if is_full_refund:
|
||||||
k_vals.append([""])
|
n_vals.append([""])
|
||||||
o_vals.append([""])
|
o_vals.append([""])
|
||||||
p_vals.append([""])
|
p_vals.append([""])
|
||||||
q_vals.append([""])
|
|
||||||
else:
|
else:
|
||||||
k_vals.append(["是" if should_k_yes else ""])
|
n_vals.append([gmv_int if gmv_int > 0 else ""])
|
||||||
o_vals.append([gmv_int if gmv_int > 0 else ""])
|
o_vals.append([refund_int if refund_int > 0 else ""])
|
||||||
p_vals.append([refund_int if refund_int > 0 else ""])
|
p_vals.append([gsv_int if gsv_int > 0 else ""])
|
||||||
q_vals.append([gsv_int if gsv_int > 0 else ""])
|
|
||||||
|
|
||||||
l_vals.append([order_date])
|
# K: 下单日期, L: 成交渠道, M: 产品
|
||||||
m_vals.append([di["order_channel"]])
|
k_vals.append([order_date])
|
||||||
n_vals.append([di["product"] if di["has_order"] else ""])
|
l_vals.append([di["order_channel"]])
|
||||||
|
m_vals.append([di["product"] if di["has_order"] else ""])
|
||||||
|
|
||||||
|
# Q: 激活课程
|
||||||
act = di["activation"]
|
act = di["activation"]
|
||||||
if act:
|
if act:
|
||||||
r_vals.append([f"{act}体验课" if act in ("A1", "A2") else act])
|
q_vals.append([f"{act}体验课" if act in ("A1", "A2") else act])
|
||||||
else:
|
else:
|
||||||
r_vals.append([""])
|
q_vals.append([""])
|
||||||
|
|
||||||
s_vals.append([di["lesson_progress"] if di["lesson_progress"] else ""])
|
# R: 行课进度, S: 最近行课时间, T: 学习时长
|
||||||
t_vals.append([di["lesson_time"]])
|
r_vals.append([di["lesson_progress"] if di["lesson_progress"] else ""])
|
||||||
|
s_vals.append([di["lesson_time"]])
|
||||||
lm = di["lesson_minutes"]
|
lm = di["lesson_minutes"]
|
||||||
u_vals.append([lm if lm > 0 else ""])
|
t_vals.append([lm if lm > 0 else ""])
|
||||||
|
|
||||||
|
# X: 订单号 — 保留已有
|
||||||
|
if existing["X"]:
|
||||||
|
x_vals.append([existing["X"]])
|
||||||
|
else:
|
||||||
|
x_vals.append([di.get("trade_no", "")])
|
||||||
|
# Y: 有效订单 — 保留已有
|
||||||
|
if existing["Y"]:
|
||||||
|
y_vals.append([existing["Y"]])
|
||||||
|
else:
|
||||||
|
y_vals.append([1 if should_y_yes else ""])
|
||||||
|
# Z: 渠道归属(销转/直购/端内/达人)
|
||||||
|
z_vals.append([classify_channel(di["order_channel"])])
|
||||||
else:
|
else:
|
||||||
for arr in [d_vals, i_vals, j_vals, k_vals, l_vals, m_vals, n_vals,
|
for arr in [d_vals, i_vals, j_vals, k_vals, l_vals, m_vals, n_vals,
|
||||||
o_vals, p_vals, q_vals, r_vals, s_vals, t_vals, u_vals]:
|
o_vals, p_vals, q_vals, r_vals, s_vals, t_vals,
|
||||||
|
x_vals, y_vals, z_vals]:
|
||||||
arr.append([""])
|
arr.append([""])
|
||||||
|
|
||||||
v_vals.append([now_str])
|
# U: 更新时间
|
||||||
|
u_vals.append([now_str])
|
||||||
|
|
||||||
cols = [
|
cols = [
|
||||||
("D", d_vals), ("H", h_vals), ("I", i_vals), ("J", j_vals),
|
("D", d_vals), ("H", h_vals), ("I", i_vals), ("J", j_vals),
|
||||||
("K", k_vals), ("L", l_vals), ("M", m_vals), ("N", n_vals),
|
("K", k_vals), ("L", l_vals), ("M", m_vals), ("N", n_vals),
|
||||||
("O", o_vals), ("P", p_vals), ("Q", q_vals), ("R", r_vals),
|
("O", o_vals), ("P", p_vals), ("Q", q_vals), ("R", r_vals),
|
||||||
("S", s_vals), ("T", t_vals), ("U", u_vals), ("V", v_vals),
|
("S", s_vals), ("T", t_vals), ("U", u_vals),
|
||||||
|
("X", x_vals), ("Y", y_vals), ("Z", z_vals),
|
||||||
]
|
]
|
||||||
for col_letter, vals in cols:
|
for col_letter, vals in cols:
|
||||||
put_values(token, sid, f"{col_letter}{sr}:{col_letter}{er}", vals)
|
put_values(token, sid, f"{col_letter}{sr}:{col_letter}{er}", vals)
|
||||||
@ -608,7 +658,7 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
|||||||
# ═══ Step 5: 汇总到「订单汇总」sheet ═══
|
# ═══ Step 5: 汇总到「订单汇总」sheet ═══
|
||||||
|
|
||||||
def clear_summary_sheet(token):
|
def clear_summary_sheet(token):
|
||||||
"""先清空订单汇总 sheet 的旧数据(A~V列,从第3行开始),再写入新数据。"""
|
"""先清空订单汇总 sheet 的旧数据(A~X列,从第3行开始),再写入新数据。"""
|
||||||
log(" 检查订单汇总 sheet 现有数据...")
|
log(" 检查订单汇总 sheet 现有数据...")
|
||||||
try:
|
try:
|
||||||
rows = read_sheet(token, SUMMARY_SHEET_ID, "A3:A5000")
|
rows = read_sheet(token, SUMMARY_SHEET_ID, "A3:A5000")
|
||||||
@ -621,9 +671,9 @@ def clear_summary_sheet(token):
|
|||||||
log(" 订单汇总 sheet 无旧数据,跳过清空")
|
log(" 订单汇总 sheet 无旧数据,跳过清空")
|
||||||
return
|
return
|
||||||
|
|
||||||
log(f" 清空 A3:V{last_data_row}({last_data_row - 2} 行旧数据)...")
|
log(f" 清空 A3:X{last_data_row}({last_data_row - 2} 行旧数据)...")
|
||||||
writer = FeishuSheetWriter(SPREADSHEET_TOKEN, token)
|
writer = FeishuSheetWriter(SPREADSHEET_TOKEN, token)
|
||||||
writer.clear(SUMMARY_SHEET_ID, start_row=3, end_row=last_data_row, cols=22)
|
writer.clear(SUMMARY_SHEET_ID, start_row=3, end_row=last_data_row, cols=24)
|
||||||
log(" 清空完成")
|
log(" 清空完成")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f" 清空异常: {e}")
|
log(f" 清空异常: {e}")
|
||||||
@ -631,22 +681,22 @@ def clear_summary_sheet(token):
|
|||||||
|
|
||||||
def write_summary_sheet(token, all_entries, phone_map, db_info):
|
def write_summary_sheet(token, all_entries, phone_map, db_info):
|
||||||
"""
|
"""
|
||||||
将三个销售 sheet 中 K=是(已下单)的行汇总到「订单汇总」sheet。
|
将三个销售 sheet 中 Y=1(有效订单)的行汇总到「订单汇总」sheet。
|
||||||
先清空旧数据,再全量写入。
|
先清空旧数据,再全量写入。
|
||||||
订单汇总 sheet 的列结构(A~X):
|
订单汇总 sheet 的列结构(A~X):
|
||||||
A: 销售归属, B: 微信昵称, C: 进线日期, D: 体验节数, E: 手机号,
|
A: 销售归属, B: 微信昵称, C: 进线日期, D: 体验节数, E: 手机号,
|
||||||
F: 用户年级, G: 课史/跟进, H: 用户ID, I: 注册日期, J: 下载渠道,
|
F: 用户年级, G: 课史/跟进, H: 用户ID, I: 注册日期, J: 下载渠道,
|
||||||
K: 是否下单, L: 下单日期, M: 成交渠道, N: 产品,
|
K: 下单日期, L: 成交渠道, M: 产品,
|
||||||
O: 下单金额(GMV), P: 退款金额, Q: 实际收入(GSV), R: 激活课程,
|
N: 下单金额(GMV), O: 退款金额, P: 实际收入(GSV), Q: 激活课程,
|
||||||
S: 当前行课进度, T: 最近行课时间, U: 累计学习时长(min), V: 更新时间,
|
R: 当前行课进度, S: 最近行课时间, T: 累计学习时长(min), U: 更新时间,
|
||||||
W: 渠道归属(公式), X: 有效成单(公式)
|
V: 渠道归属, W: 有效成单, X: 订单号
|
||||||
"""
|
"""
|
||||||
# 先清空旧数据
|
# 先清空旧数据
|
||||||
clear_summary_sheet(token)
|
clear_summary_sheet(token)
|
||||||
|
|
||||||
log(" 汇总订单数据...")
|
log(" 汇总订单数据...")
|
||||||
|
|
||||||
# 收集所有 K=是 的行
|
# 收集所有有效订单的行
|
||||||
summary_rows = []
|
summary_rows = []
|
||||||
for sid, sname, _ in SALES_SHEETS:
|
for sid, sname, _ in SALES_SHEETS:
|
||||||
entries = all_entries[sid]
|
entries = all_entries[sid]
|
||||||
@ -663,24 +713,24 @@ def write_summary_sheet(token, all_entries, phone_map, db_info):
|
|||||||
|
|
||||||
di = db_info.get(aid, {}) if aid > 0 else {}
|
di = db_info.get(aid, {}) if aid > 0 else {}
|
||||||
|
|
||||||
# 判断是否下单
|
# 判断是否有效订单
|
||||||
gmv_int = int(di.get("gmv", 0))
|
gmv_int = int(di.get("gmv", 0))
|
||||||
refund_int = int(di.get("refund", 0))
|
refund_int = int(di.get("refund", 0))
|
||||||
gsv_int = int(di.get("gsv", 0))
|
gsv_int = int(di.get("gsv", 0))
|
||||||
is_full_refund = (gmv_int > 0 and gmv_int == refund_int)
|
is_full_refund = (gmv_int > 0 and gmv_int == refund_int)
|
||||||
has_order = di.get("has_order", False) and not is_full_refund
|
has_order = di.get("has_order", False) and not is_full_refund
|
||||||
|
|
||||||
# 日期比较
|
# 日期比较: K(下单日) >= C(进线日期) (用 raw 格式比较)
|
||||||
order_date = di.get("order_date", "")
|
order_date_raw = di.get("order_date_raw", "")
|
||||||
clue_date = e["clue_date_parsed"]
|
clue_date = e["clue_date_parsed"]
|
||||||
if has_order and clue_date and order_date:
|
if has_order and clue_date and order_date_raw:
|
||||||
if order_date < clue_date:
|
if order_date_raw[:10] < clue_date:
|
||||||
has_order = False
|
has_order = False
|
||||||
|
|
||||||
if not has_order:
|
if not has_order:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 构建汇总行
|
# 构建汇总行 (A~X, 24列)
|
||||||
row_data = [
|
row_data = [
|
||||||
e["sales"], # A: 销售归属
|
e["sales"], # A: 销售归属
|
||||||
e["nickname"], # B: 微信昵称
|
e["nickname"], # B: 微信昵称
|
||||||
@ -692,43 +742,41 @@ def write_summary_sheet(token, all_entries, phone_map, db_info):
|
|||||||
str(aid) if aid > 0 else "", # H: 用户ID
|
str(aid) if aid > 0 else "", # H: 用户ID
|
||||||
di.get("reg_date", ""), # I: 注册日期
|
di.get("reg_date", ""), # I: 注册日期
|
||||||
di.get("download_channel", ""), # J: 下载渠道
|
di.get("download_channel", ""), # J: 下载渠道
|
||||||
"是", # K: 是否下单
|
di.get("order_date", ""), # K: 下单日期
|
||||||
order_date, # L: 下单日期
|
di.get("order_channel", ""), # L: 成交渠道
|
||||||
di.get("order_channel", ""), # M: 成交渠道
|
di.get("product", ""), # M: 产品
|
||||||
di.get("product", ""), # N: 产品
|
gmv_int if gmv_int > 0 else "", # N: GMV
|
||||||
gmv_int if gmv_int > 0 else "", # O: GMV
|
refund_int if refund_int > 0 else "", # O: 退款金额
|
||||||
refund_int if refund_int > 0 else "", # P: 退款金额
|
gsv_int if gsv_int > 0 else "", # P: GSV
|
||||||
gsv_int if gsv_int > 0 else "", # Q: GSV
|
(f"{di['activation']}体验课" if di.get("activation") in ("A1", "A2") else di.get("activation", "")), # Q: 激活课程
|
||||||
(f"{di['activation']}体验课" if di.get("activation") in ("A1", "A2") else di.get("activation", "")), # R: 激活课程
|
di.get("lesson_progress", ""), # R: 行课进度
|
||||||
di.get("lesson_progress", ""), # S: 行课进度
|
di.get("lesson_time", ""), # S: 最近行课时间
|
||||||
di.get("lesson_time", ""), # T: 最近行课时间
|
di.get("lesson_minutes", 0) or "", # T: 学习时长
|
||||||
di.get("lesson_minutes", 0) or "", # U: 学习时长
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # U: 更新时间
|
||||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # V: 更新时间
|
classify_channel(di.get("order_channel", "")), # V: 渠道归属
|
||||||
# W: 渠道归属 — 公式
|
1, # W: 有效成单
|
||||||
# X: 有效成单 — 公式
|
di.get("trade_no", ""), # X: 订单号
|
||||||
]
|
]
|
||||||
summary_rows.append(row_data)
|
summary_rows.append(row_data)
|
||||||
|
|
||||||
log(f" 共 {len(summary_rows)} 条下单记录待汇总")
|
log(f" 共 {len(summary_rows)} 条有效订单待汇总")
|
||||||
|
|
||||||
if not summary_rows:
|
if not summary_rows:
|
||||||
log(" 无下单记录,跳过汇总")
|
log(" 无有效订单,跳过汇总")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 写入订单汇总 sheet(从第3行开始,覆盖 A~V 列,W/X 列保留公式)
|
# 写入订单汇总 sheet(从第3行开始,A~X 共24列)
|
||||||
# 使用安全写入工具,自动分批遵守 5000 格上限
|
|
||||||
# 22 列 → 单批最大 200 行(200×22=4400 格 ≤ 5000)
|
|
||||||
writer = FeishuSheetWriter(SPREADSHEET_TOKEN, token)
|
writer = FeishuSheetWriter(SPREADSHEET_TOKEN, token)
|
||||||
|
|
||||||
# 构建 A~V 的值数组(22列),确保每行长度一致
|
# 构建 A~X 的值数组(24列),确保每行长度一致
|
||||||
values = []
|
values = []
|
||||||
for row_data in summary_rows:
|
for row_data in summary_rows:
|
||||||
padded = row_data[:22]
|
padded = row_data[:24]
|
||||||
while len(padded) < 22:
|
while len(padded) < 24:
|
||||||
padded.append("")
|
padded.append("")
|
||||||
values.append(padded)
|
values.append(padded)
|
||||||
|
|
||||||
writer.write(SUMMARY_SHEET_ID, start_row=3, rows=values, cols=22)
|
writer.write(SUMMARY_SHEET_ID, start_row=3, rows=values, cols=24)
|
||||||
|
|
||||||
log(f" 订单汇总写入完成, 共 {len(summary_rows)} 行")
|
log(f" 订单汇总写入完成, 共 {len(summary_rows)} 行")
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user