🤖 每日自动备份 - 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 销转看板 S1–S3 协作流程 [陈逸鸫确认 2026-06-06]:**
|
||||
- S1 Cursor → 微伴入库/每日线索/聚光 → @小溪
|
||||
- S2 小溪 → 销售三表 D/H/I/J + K–V + S/U → 群回「Bot刷新完成」
|
||||
- S2 小溪 → 销售三表 D/H/I/J + K–U + X–Z → 群回「Bot刷新完成」
|
||||
- S3 Cursor → 收到后粘贴订单汇总 + 刷看板公式
|
||||
- 当日默认只跑一轮 S2;再刷需群里 `【执行更新】` @小溪
|
||||
- 详细手册:`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):**
|
||||
- 飞书 Open API 单次写入上限为 5000 格(行×列),超过上限静默失败不报错
|
||||
- 所有脚本写入飞书表格时必须使用 `scripts/feishu_sheet_utils.py` 共享工具,自动分批确保 ≤ 4400 格/批
|
||||
@ -181,6 +187,15 @@
|
||||
- 小红书店铺:`newmedia-dianpu-xhs-0-0`
|
||||
- 达人直播:`newmedia-daren%`(前缀匹配)
|
||||
- 万物:`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值 | 对应渠道名称 |
|
||||
|---------------|--------------|
|
||||
|
||||
1
USER.md
1
USER.md
@ -44,6 +44,7 @@
|
||||
| 胡陈辰 | `gc64176a` |
|
||||
| 刘彦江 | `1da2afbf` |
|
||||
| 姜小龙 | `bc227c85` |
|
||||
| 赵一凡 | `a3168f9d` |
|
||||
|
||||
> ⚠️ 以上用户拥有全部数据查询权限,但其个人信息、查询内容、对话记录**禁止写入 MEMORY.md(长期记忆)**,仅可记录在短期日记忆中用于会话连续性。
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-14T07:14:40.970Z",
|
||||
"updatedAt": "2026-06-15T02:42:34.143Z",
|
||||
"entries": {
|
||||
"memory:memory/2026-05-06.md:1:20": {
|
||||
"key": "memory:memory/2026-05-06.md:1:20",
|
||||
@ -668,18 +668,20 @@
|
||||
"endLine": 56,
|
||||
"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:",
|
||||
"recallCount": 1,
|
||||
"recallCount": 2,
|
||||
"dailyCount": 0,
|
||||
"groundedCount": 0,
|
||||
"totalScore": 1,
|
||||
"totalScore": 2,
|
||||
"maxScore": 1,
|
||||
"firstRecalledAt": "2026-06-14T06:58:06.164Z",
|
||||
"lastRecalledAt": "2026-06-14T06:58:06.164Z",
|
||||
"lastRecalledAt": "2026-06-15T02:42:34.143Z",
|
||||
"queryHashes": [
|
||||
"6769ba9ebb36"
|
||||
"6769ba9ebb36",
|
||||
"0b9f27c6db59"
|
||||
],
|
||||
"recallDays": [
|
||||
"2026-06-14"
|
||||
"2026-06-14",
|
||||
"2026-06-15"
|
||||
],
|
||||
"conceptTags": [
|
||||
"3/5",
|
||||
@ -691,6 +693,99 @@
|
||||
"3/8",
|
||||
"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
|
||||
"""
|
||||
Bot 销转看板 Step2 刷新 — XXTEA 精确匹配版 (v2)
|
||||
Bot 销转看板 Step2 刷新 — XXTEA 精确匹配版 (v3)
|
||||
E列11位明文手机号 → XXTEA加密 → bi_vala_app_account.tel_encrypt精确匹配 → H列UID
|
||||
|
||||
S2 规则:
|
||||
① E→H: phone_encrypt.py XXTEA 精确匹配, 查不到留空
|
||||
② H→D/I/J: 只补空, 不覆盖已有值
|
||||
③ K=是: 仅当 L(下单日) >= C(线索日期)
|
||||
④ 全额退清: 所有订单都退费 → K/O/P/Q 全部清空
|
||||
⑤ O/P/Q 0留空, P整元
|
||||
③ Y=1: 仅当 K(下单日) >= C(线索日期)
|
||||
④ 全额退清: 所有订单都退费 → N/O/P 全部清空
|
||||
⑤ N/O/P 0留空, O整元
|
||||
⑥ 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
|
||||
from datetime import datetime
|
||||
@ -27,9 +27,9 @@ from phone_encrypt import encrypt_phone
|
||||
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
|
||||
|
||||
SALES_SHEETS = [
|
||||
("qJF4I", "小龙", "A1:V1200"),
|
||||
("f975f0", "吴迪", "A1:V700"),
|
||||
("qJF4J", "成都", "A1:V2500"),
|
||||
("qJF4I", "小龙", "A1:Z1200"),
|
||||
("f975f0", "吴迪", "A1:Z700"),
|
||||
("qJF4J", "成都", "A1:Z2500"),
|
||||
]
|
||||
|
||||
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": "",
|
||||
"gmv": 0, "refund": 0, "gsv": 0,
|
||||
"activation": "", "lesson_progress": "", "lesson_time": "", "lesson_minutes": 0,
|
||||
"max_lesson": 0,
|
||||
"max_lesson": 0, "trade_no": "",
|
||||
} for uid in uid_set}
|
||||
|
||||
# 3a. 注册信息
|
||||
@ -278,9 +278,10 @@ def query_all_pg(all_entries, phone_map):
|
||||
continue
|
||||
info[aid]["has_order"] = True
|
||||
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]["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_refund = sum(refund_map.get(o[1], 0) for o in olist) / 100.0
|
||||
info[aid]["gmv"] = total_gmv
|
||||
@ -402,7 +403,7 @@ def query_all_pg(all_entries, phone_map):
|
||||
|
||||
# ── Step 4: 写入销售三表 ──
|
||||
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")
|
||||
|
||||
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 = [], [], [], []
|
||||
k_vals, l_vals, m_vals, n_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:
|
||||
phone = item["phone"]
|
||||
@ -486,51 +488,62 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
||||
gsv_int = int(di["gsv"])
|
||||
is_full_refund = (gmv_int > 0 and gmv_int == refund_int)
|
||||
|
||||
# K=是: 有订单且非全额退清
|
||||
# 注: C列(进线日期)实际存的是手机号, 无法做L≥C日期比较
|
||||
# 有效订单判定: 有订单且非全额退清
|
||||
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:
|
||||
# 全额退清 → K/O/P/Q 全部清空
|
||||
k_vals.append([""])
|
||||
# 全额退清 → N/O/P 全部清空
|
||||
n_vals.append([""])
|
||||
o_vals.append([""])
|
||||
p_vals.append([""])
|
||||
q_vals.append([""])
|
||||
else:
|
||||
k_vals.append(["是" if should_k_yes else ""])
|
||||
o_vals.append([gmv_int if gmv_int > 0 else ""])
|
||||
p_vals.append([refund_int if refund_int > 0 else ""])
|
||||
q_vals.append([gsv_int if gsv_int > 0 else ""])
|
||||
n_vals.append([gmv_int if gmv_int > 0 else ""])
|
||||
o_vals.append([refund_int if refund_int > 0 else ""])
|
||||
p_vals.append([gsv_int if gsv_int > 0 else ""])
|
||||
|
||||
l_vals.append([order_date])
|
||||
m_vals.append([di["order_channel"]])
|
||||
n_vals.append([di["product"] if di["has_order"] else ""])
|
||||
# K: 下单日期, L: 成交渠道, M: 产品
|
||||
k_vals.append([order_date])
|
||||
l_vals.append([di["order_channel"]])
|
||||
m_vals.append([di["product"] if di["has_order"] else ""])
|
||||
|
||||
# Q: 激活课程
|
||||
act = di["activation"]
|
||||
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:
|
||||
r_vals.append([""])
|
||||
q_vals.append([""])
|
||||
|
||||
# R: 行课进度, S: 最近行课时间
|
||||
lp = di["lesson_progress"]
|
||||
s_vals.append([lp if lp else ""])
|
||||
t_vals.append([di["lesson_time"]])
|
||||
r_vals.append([lp if lp else ""])
|
||||
s_vals.append([di["lesson_time"]])
|
||||
|
||||
# T: 学习时长
|
||||
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:
|
||||
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([""])
|
||||
|
||||
v_vals.append([now_str])
|
||||
# U: 更新时间
|
||||
u_vals.append([now_str])
|
||||
|
||||
cols = [
|
||||
("D", d_vals), ("H", h_vals), ("I", i_vals), ("J", j_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),
|
||||
("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:
|
||||
put_values(token, sid, f"{col_letter}{sr}:{col_letter}{er}", vals)
|
||||
|
||||
@ -3,13 +3,13 @@
|
||||
端内析出leads数据 — 自动回填 + 统计汇总脚本
|
||||
|
||||
流程:
|
||||
1. 读取曲慧萌/吴迪 sheet 中 A 列的手机号
|
||||
1. 读取吴迪 sheet 中 B 列的手机号
|
||||
2. XXTEA 加密 → 匹配 bi_vala_app_account.tel_encrypt → 获取 account_id
|
||||
3. 查询注册日期、转化、退费、U0体验课完成日期 → 回写
|
||||
3. 查询注册日期、转化(全部key_from,购课时间≥析出时间)、转化keyfrom、退费、U0体验课完成日期 → 回写
|
||||
4. 按析出月份汇总统计 → 写入"统计" sheet
|
||||
|
||||
统计口径:
|
||||
- 转化率 = 已转化leads / 有效析出leads(剔除端外购课用户)
|
||||
- 转化率 = 已转化leads / 总leads
|
||||
- 退费率 = 退费leads / 已转化leads
|
||||
- 完成率 = 完成该课的leads / 总leads
|
||||
|
||||
@ -26,17 +26,9 @@ from collections import defaultdict
|
||||
|
||||
# ── 配置 ──────────────────────────────────────────────
|
||||
SPREADSHEET_TOKEN = "FA3xsw3kph4pdatKlUrcyPgInAc"
|
||||
SHEET_QHM = "7f0e35" # 曲慧萌
|
||||
SHEET_WD = "1K3O6s" # 吴迪
|
||||
SHEET_STAT = "scyF3H" # 统计
|
||||
|
||||
# 端内渠道
|
||||
INNER_CHANNELS = [
|
||||
"app-active-h5-0-0",
|
||||
"app-sales-bj-qhm-0",
|
||||
"app-sales-bj-wd-0",
|
||||
]
|
||||
|
||||
# U0 体验课 chapter_id
|
||||
U0_CHAPTERS = {
|
||||
"L1-U0-L01": 343,
|
||||
@ -59,17 +51,19 @@ U0_COL_ORDER = [
|
||||
|
||||
# 列映射(0-based)
|
||||
# 注意:A列「序号」和C列「微信昵称」由销售手动填写,脚本不读写
|
||||
COL_SEQ = 0 # A: 序号(手动填,脚本跳过)
|
||||
COL_PHONE = 1 # B: 用户手机号
|
||||
COL_NICKNAME = 2 # C: 微信昵称(销售手动填写,脚本跳过)
|
||||
COL_USER_ID = 3 # D: 用户ID
|
||||
COL_EXTRACT_DATE = 4 # E: 析出日期(手动填)
|
||||
COL_REG_DATE = 5 # F: 注册日期
|
||||
COL_CONVERTED = 6 # G: 是否转化
|
||||
COL_CONVERT_DATE = 7 # H: 转化日期
|
||||
COL_REFUND = 8 # I: 是否退费
|
||||
COL_REFUND_DATE = 9 # J: 退费日期
|
||||
COL_U0_START = 10 # K-T: L1-U0-L01 ~ L2-U0-L05
|
||||
COL_SEQ = 0 # A: 序号(手动填,脚本跳过)
|
||||
COL_PHONE = 1 # B: 用户手机号
|
||||
COL_NICKNAME = 2 # C: 微信昵称(销售手动填写,脚本跳过)
|
||||
COL_USER_ID = 3 # D: 用户ID
|
||||
COL_EXTRACT_DATE = 4 # E: 析出日期(手动填)
|
||||
COL_REG_DATE = 5 # F: 注册日期
|
||||
COL_CONVERTED = 6 # G: 是否转化
|
||||
COL_CONVERT_DATE = 7 # H: 转化日期
|
||||
COL_CONVERT_KEYFROM = 8 # I: 转化keyfrom
|
||||
COL_CONVERT_GSV = 9 # J: 转化金额(GSV)(新增)
|
||||
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"
|
||||
@ -180,64 +174,47 @@ def match_phones(phones: list[str]) -> dict[str, dict]:
|
||||
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:
|
||||
return {}
|
||||
BATCH_SIZE = 100
|
||||
results = {}
|
||||
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 o.account_id::text,
|
||||
MIN(o.pay_success_date::date::text) AS first_pay_date,
|
||||
BOOL_OR(r.id IS NOT NULL AND r.status = 3 AND o2.order_status = 4) AS has_refund,
|
||||
MIN(CASE WHEN r.id IS NOT NULL AND r.status = 3 AND o2.order_status = 4
|
||||
THEN r.created_at::date::text END) AS first_refund_date
|
||||
o.pay_success_date::date::text,
|
||||
o.key_from,
|
||||
o.pay_amount_int,
|
||||
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
|
||||
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
|
||||
WHERE o.account_id IN ({ids_str})
|
||||
AND o.key_from IN ({channels_str})
|
||||
AND o.pay_success_date IS NOT NULL
|
||||
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):
|
||||
if len(row) >= 3:
|
||||
acc_id, first_pay, has_refund = row[0], row[1], row[2]
|
||||
first_refund = row[3] if len(row) >= 4 else ""
|
||||
results[acc_id] = {
|
||||
"converted": "是" if first_pay else "否",
|
||||
"convert_date": first_pay or "",
|
||||
"refunded": "是" if has_refund in ("t", "true") else "否",
|
||||
"refund_date": first_refund or "",
|
||||
}
|
||||
return results
|
||||
|
||||
|
||||
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])
|
||||
if len(row) >= 6:
|
||||
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]
|
||||
if acc_id not in results:
|
||||
results[acc_id] = {"orders": [], "total_gmv_cents": 0, "total_gsv_cents": 0}
|
||||
refunded = is_refunded in ("t", "true")
|
||||
results[acc_id]["orders"].append((pay_date, key_from, refunded))
|
||||
# 累加用户总 GMV(分)和 GSV(分)
|
||||
results[acc_id]["total_gmv_cents"] += int(pay_amount)
|
||||
gsv_fen = int(pay_amount) - int(total_refund_fen)
|
||||
results[acc_id]["total_gsv_cents"] += gsv_fen
|
||||
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"{'='*60}")
|
||||
|
||||
range_str = f"{sheet_id}!A2:T"
|
||||
range_str = f"{sheet_id}!A2:W"
|
||||
try:
|
||||
rows = lark_read(sheet_id, range_str)
|
||||
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_phones = set(acc_info.keys())
|
||||
|
||||
print("→ 查询转化信息...")
|
||||
conv_info = query_conversion(matched_accounts)
|
||||
|
||||
print("→ 查询端外购课...")
|
||||
outside_accounts = query_outside_conversion(matched_accounts)
|
||||
print("→ 查询订单及退费信息(全部渠道)...")
|
||||
order_info = query_orders_and_refunds(matched_accounts)
|
||||
|
||||
print("→ 查询 U0 学习进度...")
|
||||
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:
|
||||
continue
|
||||
acc_id = info["id"]
|
||||
conv = conv_info.get(acc_id, {})
|
||||
learn = learn_info.get(acc_id, {})
|
||||
|
||||
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]:
|
||||
extract_date = str(rows[row_idx][COL_EXTRACT_DATE]).strip()
|
||||
|
||||
# F 列逻辑:端内购课 > 端外购课 > 否
|
||||
is_inner = conv.get("converted", "否") == "是"
|
||||
is_outside = acc_id in outside_accounts
|
||||
if is_inner:
|
||||
f_value = "是"
|
||||
convert_date = conv.get("convert_date", "")
|
||||
elif is_outside:
|
||||
f_value = "端外购课"
|
||||
convert_date = ""
|
||||
else:
|
||||
f_value = "否"
|
||||
convert_date = ""
|
||||
# 转化判断:全部渠道,购课时间 ≥ 析出时间
|
||||
# 退费判断:只要存在一笔 ≥ 析出日期且未退费的订单,就算未退费
|
||||
# 转化金额:用户全部订单 GSV(全部 pay_amount - 全部 refund_amount)
|
||||
orders = order_info.get(acc_id, {})
|
||||
conv_date = ""
|
||||
conv_keyfrom = ""
|
||||
conv_gsv = ""
|
||||
is_refunded = "否"
|
||||
|
||||
# 析出日期可能是 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:
|
||||
extract_date_ymd = extract_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_REG_DATE, info.get("created_at", "")))
|
||||
updates.append((row_idx, COL_CONVERTED, f_value))
|
||||
updates.append((row_idx, COL_CONVERT_DATE, convert_date))
|
||||
updates.append((row_idx, COL_REFUND, conv.get("refunded", "否")))
|
||||
updates.append((row_idx, COL_REFUND_DATE, conv.get("refund_date", "")))
|
||||
updates.append((row_idx, COL_CONVERT_DATE, conv_date))
|
||||
updates.append((row_idx, COL_CONVERT_KEYFROM, conv_keyfrom))
|
||||
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):
|
||||
updates.append((row_idx, COL_U0_START + col_offset, learn.get(lesson_name, "")))
|
||||
|
||||
lead_data.append({
|
||||
"extract_date": extract_date,
|
||||
"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},
|
||||
"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,
|
||||
"converted": existing_converted,
|
||||
"refunded": "否",
|
||||
"gmv_cents": 0,
|
||||
"gsv_cents": 0,
|
||||
"lessons": {k: "" for k in U0_COL_ORDER},
|
||||
"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}",
|
||||
[[str(col_vals[COL_USER_ID])]])
|
||||
|
||||
# F-T: 注册日期 ~ L2-U0-L5
|
||||
f_to_t = []
|
||||
# F-V: 注册日期 ~ L2-U0-L5(含 I 列转化keyfrom)
|
||||
f_to_w = []
|
||||
for col in range(COL_REG_DATE, COL_U0_START + len(U0_COL_ORDER)):
|
||||
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:
|
||||
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:
|
||||
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} 回写成功")
|
||||
|
||||
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
|
||||
"""
|
||||
@ -494,30 +495,32 @@ def compute_stats(lead_data: list[dict]) -> dict[str, dict]:
|
||||
for month, leads in sorted(month_groups.items()):
|
||||
total = len(leads)
|
||||
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"] == "是")
|
||||
refunded = sum(1 for l in leads if l["refunded"] == "是")
|
||||
converted_unrefunded = sum(1 for l in leads if l["converted"] == "是" and l["refunded"] != "是")
|
||||
|
||||
# 转化率分母剔除端外购课用户
|
||||
effective_total = total - outside_only
|
||||
conv_rate = converted_all / effective_total * 100 if effective_total > 0 else 0
|
||||
conv_rate = converted_all / total * 100 if total > 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 = {}
|
||||
for lesson_name in U0_COL_ORDER:
|
||||
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
|
||||
|
||||
result[month] = {
|
||||
"total": effective_total,
|
||||
"total": total,
|
||||
"matched": matched,
|
||||
"outside_only": outside_only,
|
||||
"converted_all": converted_all,
|
||||
"converted_unrefunded": converted_unrefunded,
|
||||
"refunded": refunded,
|
||||
"conv_rate": conv_rate,
|
||||
"refund_rate": refund_rate,
|
||||
"gmv": total_gmv,
|
||||
"gsv": total_gsv,
|
||||
"lesson_rates": lesson_rates,
|
||||
}
|
||||
|
||||
@ -532,21 +535,21 @@ def write_all_stats(all_stats: dict[str, dict[str, dict]], dry_run: bool = False
|
||||
按 销售+月份 逐行写入,从第2行开始
|
||||
"""
|
||||
# 先写表头
|
||||
header = ["销售", "月份", "有效析出用户数", "匹配用户数", "转化用户数", "转化率", "退费率"] + \
|
||||
header = ["销售", "月份", "总析出用户数", "匹配用户数", "转化用户数", "转化率", "退费率", "GMV", "GSV"] + \
|
||||
[f"{name}完成率" for name in U0_COL_ORDER]
|
||||
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), ...]
|
||||
rows_data = []
|
||||
for sales_name in ["曲慧萌", "吴迪"]:
|
||||
for sales_name in ["吴迪"]:
|
||||
stats = all_stats.get(sales_name, {})
|
||||
for month in sorted(stats.keys()):
|
||||
rows_data.append((sales_name, month, stats[month]))
|
||||
|
||||
# 先清除统计 sheet 旧数据(A2:Q50),避免残留旧行
|
||||
# 先清除统计 sheet 旧数据(A2:S50),避免残留旧行
|
||||
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:
|
||||
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]])
|
||||
# B: 月份
|
||||
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"]]])
|
||||
# D: 匹配用户数
|
||||
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)]])
|
||||
# G: 退费率
|
||||
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]
|
||||
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']} "
|
||||
f"转化率={s['conv_rate']:.1f}% 退费率={s['refund_rate']:.1f}%")
|
||||
print(f" ✓ {sales_name} {month}: 总析出={s['total']} 匹配={s['matched']} 转化={s['converted_all']} "
|
||||
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:
|
||||
print("⚠️ DRY-RUN 模式,不会实际写入\n")
|
||||
|
||||
# 处理两个销售 sheet
|
||||
qhm_data = process_sheet(SHEET_QHM, "曲慧萌", dry_run)
|
||||
# 处理吴迪 sheet
|
||||
wd_data = process_sheet(SHEET_WD, "吴迪", dry_run)
|
||||
|
||||
# 汇总统计
|
||||
@ -593,15 +599,14 @@ def main():
|
||||
print("汇总统计 → 统计 sheet")
|
||||
print(f"{'='*60}")
|
||||
|
||||
qhm_stats = compute_stats(qhm_data)
|
||||
wd_stats = compute_stats(wd_data)
|
||||
|
||||
all_stats = {"曲慧萌": qhm_stats, "吴迪": wd_stats}
|
||||
all_stats = {"吴迪": wd_stats}
|
||||
|
||||
if dry_run:
|
||||
for sales_name, stats in all_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:
|
||||
write_all_stats(all_stats, dry_run)
|
||||
|
||||
|
||||
@ -19,7 +19,6 @@ APP_ID = "cli_a929ae22e0b8dcc8"
|
||||
APP_SECRET = "OtFjMy7p3qE3VvLbMdcWidwgHOnGD4FJ"
|
||||
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
|
||||
SALES_SHEETS = {"f975f0": "吴迪", "qJF4I": "小龙", "qJF4J": "成都"}
|
||||
DIRECT_SHEET = "1sosYE" # 直购表(小红书店铺+stream-xhs,不依赖手机号匹配)
|
||||
SUMMARY_SHEET = "2smjwA"
|
||||
|
||||
def _get_pg_password():
|
||||
@ -188,24 +187,6 @@ def main():
|
||||
|
||||
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: 筛选进订单汇总的行 ──
|
||||
# 条件:K=是 · O>0 · 非全额退(P空或P<O) · L≥C
|
||||
order_rows = []
|
||||
@ -245,7 +226,7 @@ def main():
|
||||
|
||||
print(f"Order rows after filter: {len(order_rows)}")
|
||||
|
||||
# ── Step 2.5: 去重(同一人可能在三表+直购中出现多次)──
|
||||
# ── Step 2.5: 去重(同一人可能在三表中出现多次)──
|
||||
# 按 (A销售归属, B微信昵称, O下单金额, P退款金额, L下单日期) 去重
|
||||
seen = set()
|
||||
deduped = []
|
||||
|
||||
@ -6,15 +6,15 @@
|
||||
1. 读取「小龙」「吴迪」「成都」三个 sheet 的 E 列手机号
|
||||
2. XXTEA 加密 → bi_vala_app_account.tel_encrypt 精确匹配 → 获取 account_id
|
||||
3. 查询 PostgreSQL 获取用户订单/学习数据
|
||||
4. 填写 H~V 列(自动列),V 列为操作更新时间
|
||||
5. 将三个 sheet 中 K=是(已下单)的用户汇总到「订单汇总」sheet
|
||||
4. 填写 D/H/I/J/K~U/X/Y/Z 列(自动列),U 列为操作更新时间
|
||||
5. 将三个 sheet 中 Y=1(有效订单)的用户汇总到「订单汇总」sheet
|
||||
|
||||
规则(沿用 S2 规则):
|
||||
① E→H: XXTEA 精确匹配, 查不到留空
|
||||
② H→D/I/J: 只补空, 不覆盖已有值
|
||||
③ K=是: 仅当 L(下单日) >= C(线索日期)
|
||||
④ 全额退清: 所有订单都退费 → K/O/P/Q 全部清空
|
||||
⑤ O/P/Q 0 留空, P 整元
|
||||
③ Y=1: 仅当 K(下单日) >= C(线索日期)
|
||||
④ 全额退清: 所有订单都退费 → N/O/P 全部清空
|
||||
⑤ N/O/P 0 留空, O 整元
|
||||
⑥ G 列不动
|
||||
|
||||
用法:
|
||||
@ -36,9 +36,9 @@ from phone_encrypt import encrypt_phone
|
||||
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
|
||||
|
||||
SALES_SHEETS = [
|
||||
("qJF4I", "小龙", "A3:V2607"),
|
||||
("f975f0", "吴迪", "A3:V8149"),
|
||||
("qJF4J", "成都", "A3:V2500"),
|
||||
("qJF4I", "小龙", "A3:Z2607"),
|
||||
("f975f0", "吴迪", "A3:Z8149"),
|
||||
("qJF4J", "成都", "A3:Z2500"),
|
||||
]
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@ -204,7 +224,9 @@ def parse_sales_sheets(token):
|
||||
"S": safe_cell(row, 18),
|
||||
"T": safe_cell(row, 19),
|
||||
"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": "",
|
||||
"gmv": 0, "refund": 0, "gsv": 0,
|
||||
"activation": "", "lesson_progress": "", "lesson_time": "", "lesson_minutes": 0,
|
||||
"trade_no": "",
|
||||
} for uid in uid_set}
|
||||
|
||||
# 3a. 注册信息
|
||||
@ -354,9 +377,17 @@ def query_all_pg(all_entries, phone_map):
|
||||
continue
|
||||
info[aid]["has_order"] = True
|
||||
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]["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_refund = sum(refund_map.get(o[1], 0) for o in olist) / 100.0
|
||||
info[aid]["gmv"] = total_gmv
|
||||
@ -469,7 +500,7 @@ def query_all_pg(all_entries, phone_map):
|
||||
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):
|
||||
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 = [], [], [], []
|
||||
k_vals, l_vals, m_vals, n_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:
|
||||
phone = e["phone"]
|
||||
@ -551,52 +583,70 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
||||
gsv_int = int(di["gsv"])
|
||||
is_full_refund = (gmv_int > 0 and gmv_int == refund_int)
|
||||
|
||||
# K=是: 有订单 且 非全额退清 且 L(下单日) >= C(线索日期)
|
||||
# 有效订单判定: 有订单 且 非全额退清 且 K(下单日) >= C(线索日期)
|
||||
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
|
||||
if should_k_yes and clue_date and order_date:
|
||||
if order_date < clue_date:
|
||||
should_k_yes = False
|
||||
# 日期比较: K >= C (用 raw 格式比较)
|
||||
order_date_raw = di.get("order_date_raw", "")
|
||||
if should_y_yes and clue_date and order_date_raw:
|
||||
if order_date_raw[:10] < clue_date:
|
||||
should_y_yes = False
|
||||
|
||||
if is_full_refund:
|
||||
k_vals.append([""])
|
||||
n_vals.append([""])
|
||||
o_vals.append([""])
|
||||
p_vals.append([""])
|
||||
q_vals.append([""])
|
||||
else:
|
||||
k_vals.append(["是" if should_k_yes else ""])
|
||||
o_vals.append([gmv_int if gmv_int > 0 else ""])
|
||||
p_vals.append([refund_int if refund_int > 0 else ""])
|
||||
q_vals.append([gsv_int if gsv_int > 0 else ""])
|
||||
n_vals.append([gmv_int if gmv_int > 0 else ""])
|
||||
o_vals.append([refund_int if refund_int > 0 else ""])
|
||||
p_vals.append([gsv_int if gsv_int > 0 else ""])
|
||||
|
||||
l_vals.append([order_date])
|
||||
m_vals.append([di["order_channel"]])
|
||||
n_vals.append([di["product"] if di["has_order"] else ""])
|
||||
# K: 下单日期, L: 成交渠道, M: 产品
|
||||
k_vals.append([order_date])
|
||||
l_vals.append([di["order_channel"]])
|
||||
m_vals.append([di["product"] if di["has_order"] else ""])
|
||||
|
||||
# Q: 激活课程
|
||||
act = di["activation"]
|
||||
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:
|
||||
r_vals.append([""])
|
||||
q_vals.append([""])
|
||||
|
||||
s_vals.append([di["lesson_progress"] if di["lesson_progress"] else ""])
|
||||
t_vals.append([di["lesson_time"]])
|
||||
# R: 行课进度, S: 最近行课时间, T: 学习时长
|
||||
r_vals.append([di["lesson_progress"] if di["lesson_progress"] else ""])
|
||||
s_vals.append([di["lesson_time"]])
|
||||
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:
|
||||
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([""])
|
||||
|
||||
v_vals.append([now_str])
|
||||
# U: 更新时间
|
||||
u_vals.append([now_str])
|
||||
|
||||
cols = [
|
||||
("D", d_vals), ("H", h_vals), ("I", i_vals), ("J", j_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),
|
||||
("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:
|
||||
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 ═══
|
||||
|
||||
def clear_summary_sheet(token):
|
||||
"""先清空订单汇总 sheet 的旧数据(A~V列,从第3行开始),再写入新数据。"""
|
||||
"""先清空订单汇总 sheet 的旧数据(A~X列,从第3行开始),再写入新数据。"""
|
||||
log(" 检查订单汇总 sheet 现有数据...")
|
||||
try:
|
||||
rows = read_sheet(token, SUMMARY_SHEET_ID, "A3:A5000")
|
||||
@ -621,9 +671,9 @@ def clear_summary_sheet(token):
|
||||
log(" 订单汇总 sheet 无旧数据,跳过清空")
|
||||
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.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(" 清空完成")
|
||||
except Exception as e:
|
||||
log(f" 清空异常: {e}")
|
||||
@ -631,22 +681,22 @@ def clear_summary_sheet(token):
|
||||
|
||||
def write_summary_sheet(token, all_entries, phone_map, db_info):
|
||||
"""
|
||||
将三个销售 sheet 中 K=是(已下单)的行汇总到「订单汇总」sheet。
|
||||
将三个销售 sheet 中 Y=1(有效订单)的行汇总到「订单汇总」sheet。
|
||||
先清空旧数据,再全量写入。
|
||||
订单汇总 sheet 的列结构(A~X):
|
||||
A: 销售归属, B: 微信昵称, C: 进线日期, D: 体验节数, E: 手机号,
|
||||
F: 用户年级, G: 课史/跟进, H: 用户ID, I: 注册日期, J: 下载渠道,
|
||||
K: 是否下单, L: 下单日期, M: 成交渠道, N: 产品,
|
||||
O: 下单金额(GMV), P: 退款金额, Q: 实际收入(GSV), R: 激活课程,
|
||||
S: 当前行课进度, T: 最近行课时间, U: 累计学习时长(min), V: 更新时间,
|
||||
W: 渠道归属(公式), X: 有效成单(公式)
|
||||
K: 下单日期, L: 成交渠道, M: 产品,
|
||||
N: 下单金额(GMV), O: 退款金额, P: 实际收入(GSV), Q: 激活课程,
|
||||
R: 当前行课进度, S: 最近行课时间, T: 累计学习时长(min), U: 更新时间,
|
||||
V: 渠道归属, W: 有效成单, X: 订单号
|
||||
"""
|
||||
# 先清空旧数据
|
||||
clear_summary_sheet(token)
|
||||
|
||||
log(" 汇总订单数据...")
|
||||
|
||||
# 收集所有 K=是 的行
|
||||
# 收集所有有效订单的行
|
||||
summary_rows = []
|
||||
for sid, sname, _ in SALES_SHEETS:
|
||||
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 {}
|
||||
|
||||
# 判断是否下单
|
||||
# 判断是否有效订单
|
||||
gmv_int = int(di.get("gmv", 0))
|
||||
refund_int = int(di.get("refund", 0))
|
||||
gsv_int = int(di.get("gsv", 0))
|
||||
is_full_refund = (gmv_int > 0 and gmv_int == refund_int)
|
||||
has_order = di.get("has_order", False) and not is_full_refund
|
||||
|
||||
# 日期比较
|
||||
order_date = di.get("order_date", "")
|
||||
# 日期比较: K(下单日) >= C(进线日期) (用 raw 格式比较)
|
||||
order_date_raw = di.get("order_date_raw", "")
|
||||
clue_date = e["clue_date_parsed"]
|
||||
if has_order and clue_date and order_date:
|
||||
if order_date < clue_date:
|
||||
if has_order and clue_date and order_date_raw:
|
||||
if order_date_raw[:10] < clue_date:
|
||||
has_order = False
|
||||
|
||||
if not has_order:
|
||||
continue
|
||||
|
||||
# 构建汇总行
|
||||
# 构建汇总行 (A~X, 24列)
|
||||
row_data = [
|
||||
e["sales"], # A: 销售归属
|
||||
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
|
||||
di.get("reg_date", ""), # I: 注册日期
|
||||
di.get("download_channel", ""), # J: 下载渠道
|
||||
"是", # K: 是否下单
|
||||
order_date, # L: 下单日期
|
||||
di.get("order_channel", ""), # M: 成交渠道
|
||||
di.get("product", ""), # N: 产品
|
||||
gmv_int if gmv_int > 0 else "", # O: GMV
|
||||
refund_int if refund_int > 0 else "", # P: 退款金额
|
||||
gsv_int if gsv_int > 0 else "", # Q: GSV
|
||||
(f"{di['activation']}体验课" if di.get("activation") in ("A1", "A2") else di.get("activation", "")), # R: 激活课程
|
||||
di.get("lesson_progress", ""), # S: 行课进度
|
||||
di.get("lesson_time", ""), # T: 最近行课时间
|
||||
di.get("lesson_minutes", 0) or "", # U: 学习时长
|
||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # V: 更新时间
|
||||
# W: 渠道归属 — 公式
|
||||
# X: 有效成单 — 公式
|
||||
di.get("order_date", ""), # K: 下单日期
|
||||
di.get("order_channel", ""), # L: 成交渠道
|
||||
di.get("product", ""), # M: 产品
|
||||
gmv_int if gmv_int > 0 else "", # N: GMV
|
||||
refund_int if refund_int > 0 else "", # O: 退款金额
|
||||
gsv_int if gsv_int > 0 else "", # P: GSV
|
||||
(f"{di['activation']}体验课" if di.get("activation") in ("A1", "A2") else di.get("activation", "")), # Q: 激活课程
|
||||
di.get("lesson_progress", ""), # R: 行课进度
|
||||
di.get("lesson_time", ""), # S: 最近行课时间
|
||||
di.get("lesson_minutes", 0) or "", # T: 学习时长
|
||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # U: 更新时间
|
||||
classify_channel(di.get("order_channel", "")), # V: 渠道归属
|
||||
1, # W: 有效成单
|
||||
di.get("trade_no", ""), # X: 订单号
|
||||
]
|
||||
summary_rows.append(row_data)
|
||||
|
||||
log(f" 共 {len(summary_rows)} 条下单记录待汇总")
|
||||
log(f" 共 {len(summary_rows)} 条有效订单待汇总")
|
||||
|
||||
if not summary_rows:
|
||||
log(" 无下单记录,跳过汇总")
|
||||
log(" 无有效订单,跳过汇总")
|
||||
return
|
||||
|
||||
# 写入订单汇总 sheet(从第3行开始,覆盖 A~V 列,W/X 列保留公式)
|
||||
# 使用安全写入工具,自动分批遵守 5000 格上限
|
||||
# 22 列 → 单批最大 200 行(200×22=4400 格 ≤ 5000)
|
||||
# 写入订单汇总 sheet(从第3行开始,A~X 共24列)
|
||||
writer = FeishuSheetWriter(SPREADSHEET_TOKEN, token)
|
||||
|
||||
# 构建 A~V 的值数组(22列),确保每行长度一致
|
||||
# 构建 A~X 的值数组(24列),确保每行长度一致
|
||||
values = []
|
||||
for row_data in summary_rows:
|
||||
padded = row_data[:22]
|
||||
while len(padded) < 22:
|
||||
padded = row_data[:24]
|
||||
while len(padded) < 24:
|
||||
padded.append("")
|
||||
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)} 行")
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user