🤖 每日自动备份 - 2026-06-16 08:00:01

This commit is contained in:
小溪 2026-06-16 08:00:01 +08:00
parent 4135e93b17
commit b25d6fef0f
12 changed files with 651 additions and 249 deletions

View File

@ -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 销转看板 S1S3 协作流程 [陈逸鸫确认 2026-06-06]** - **Bot 销转看板 S1S3 协作流程 [陈逸鸫确认 2026-06-06]**
- S1 Cursor → 微伴入库/每日线索/聚光 → @小溪 - S1 Cursor → 微伴入库/每日线索/聚光 → @小溪
- S2 小溪 → 销售三表 D/H/I/J + KV + S/U → 群回「Bot刷新完成」 - S2 小溪 → 销售三表 D/H/I/J + KU + XZ → 群回「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值 | 对应渠道名称 |
|---------------|--------------| |---------------|--------------|

View File

@ -44,6 +44,7 @@
| 胡陈辰 | `gc64176a` | | 胡陈辰 | `gc64176a` |
| 刘彦江 | `1da2afbf` | | 刘彦江 | `1da2afbf` |
| 姜小龙 | `bc227c85` | | 姜小龙 | `bc227c85` |
| 赵一凡 | `a3168f9d` |
> ⚠️ 以上用户拥有全部数据查询权限,但其个人信息、查询内容、对话记录**禁止写入 MEMORY.md长期记忆**,仅可记录在短期日记忆中用于会话连续性。 > ⚠️ 以上用户拥有全部数据查询权限,但其个人信息、查询内容、对话记录**禁止写入 MEMORY.md长期记忆**,仅可记录在短期日记忆中用于会话连续性。

View File

@ -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≥Cpre怀疑#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≥Cpre怀疑#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 行写入订单汇总 2smjwAW 渠道归属 + 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
View 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.00GSV=19386.00**
GMV = 全部订单 pay_amount 加和含退费GSV = GMV 扣除退费金额。你可以去统计 sheet 核对一下。

29
memory/2026-06-15-1737.md Normal file
View 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
View 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
View 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
View 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_id2009
- 订单汇总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+ 所有杂项渠道,统一归入直购一个大类

View File

@ -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 规则:
EH: phone_encrypt.py XXTEA 精确匹配, 查不到留空 EH: phone_encrypt.py XXTEA 精确匹配, 查不到留空
HD/I/J: 只补空, 不覆盖已有值 HD/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)

View File

@ -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)

View File

@ -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 = []

View File

@ -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 规则:
EH: XXTEA 精确匹配, 查不到留空 EH: XXTEA 精确匹配, 查不到留空
HD/I/J: 只补空, 不覆盖已有值 HD/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)}")