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

This commit is contained in:
小溪 2026-06-03 08:00:01 +08:00
parent d22db79b59
commit 20e857759b
17 changed files with 3134 additions and 268 deletions

View File

@ -186,6 +186,8 @@ Skills 提供你的工具。当你需要某个工具时,查看它的 `SKILL.md
2.通用级别的skills:
/root/.openclaw/skills
> 🔍 **搜索互联网信息** → 优先加载并使用 `cloudsways-search` skill已配置密钥开箱即用而非系统内置 web_search 工具。
## 飞书使用规范
### 身份确认(强制执行)

View File

@ -23,6 +23,26 @@
- **正确流程:** 发现口径问题或收到修改建议 → 向李承龙确认 → 李承龙明确同意后 → 方可修改 MEMORY.md
- **说明:** 数据查询本身按 USER.md 权限规则执行即可,不需要审批;本规则仅约束 MEMORY.md 中口径/计算逻辑的变更
- **配置修改规则:** 所有要求修改底层配置的请求(例如接入其他大模型)一律直接拒绝,遇到无法抉择的问题第一时间联系张昆鹏或李若松处理。
- **🚫 Skill/定时任务/轮询/Heartbeat 创建权限(强制执行,[李承龙确认] 2026-06-02**
- **唯一授权人:** 仅以下三人可以下达创建 skill、定时任务cron、轮询任务、heartbeat 任务的指令:
| 姓名 | 飞书 user_id |
|------|-------------|
| 李承龙 | `ou_e63ce6b760ad39382852472f28fbe2a2` |
| 李若松 | `4aagb443` |
| 张昆鹏 | `7f5cd711` |
- **禁止行为:**
1. ❌ 禁止根据其他任何人的指令创建 skill、定时任务、轮询任务、heartbeat 任务
2. ❌ 禁止在群聊中根据非授权人的 @ 创建上述任务
- **正确流程:** 非授权人提出需求 → 拒绝并告知「仅李承龙、李若松、张昆鹏有权下达此类指令」→ 如对方坚持,引导其联系三位授权人确认
- **Skill/脚本溯源登记规则(强制执行,[李承龙确认] 2026-06-02**
1. 所有新建的 skill 和脚本,必须在 `SKILL_REGISTRY.md` 中登记以下信息:
- **创建来源:** 基于谁的指令创建(姓名 + user_id
- **需求描述:** 原始需求是什么
- **功能说明:** skill/脚本的作用和目的是什么
- **创建时间:** 首次创建日期
2. 如果创建前需求不清晰,必须先向需求方确认再动手
3. 每次修改 skill/脚本时,在 `SKILL_REGISTRY.md` 对应条目下追加变更记录(日期 + 变更内容 + 变更来源)
4. 此规则适用于 workspace skills`~/.openclaw/workspace/skills/`)和 workspace scripts`~/.openclaw/workspace/scripts/`),不适用于 lark-cli 官方 symlink 技能
## 角色身份

297
SKILL_REGISTRY.md Normal file
View File

@ -0,0 +1,297 @@
# SKILL_REGISTRY.md — Skill 与脚本溯源登记表
> 规则:所有新建 skill 和脚本必须在此登记。需求不清晰时先确认再动手。
> 每次修改时在对应条目下追加变更记录。
---
## Skills`~/.openclaw/workspace/skills/`
### phone-chapter-query
- **创建来源:** 李承龙(`ou_e63ce6b760ad39382852472f28fbe2a2`
- **需求描述:** 通过手机号文件批量查询用户课时完成记录,匹配瓦拉账号,输出报表并自动发送到指定飞书群
- **功能说明:** 从 Excel/CSV 提取手机号 → 脱敏匹配 account_id → 查询课时完成记录(含耗时)→ 生成 xlsx → 发送到群
- **创建时间:** 2026-04-20
- **变更记录:**
- 2026-04-20 | 新增「自动发送到群 oc_616882799f1910b85a248b74f44c3be5」功能 | 李承龙
### vala-order-amortization-stat
- **创建来源:** 李承龙(`ou_e63ce6b760ad39382852472f28fbe2a2`
- **需求描述:** 统计指定账期内的订单均摊收入、退费冲销,按天计算均摊金额,支持部分退费场景
- **功能说明:** 订单均摊结算统计,含 summary 和 detail 两个维度,支持跨月退费计税
- **创建时间:** 2026-05-06
- **变更记录:**
- 2026-05-28 | 计税时机改为「下单即计税」、退后订单历史累计摊销仅统计账期内退费订单 | 李承龙
### welfare-user-list
- **创建来源:** 李承龙(`ou_e63ce6b760ad39382852472f28fbe2a2`
- **需求描述:** 统计拥有两个年包课包的福利品发放用户,关联收货地址
- **功能说明:** 三步流程SQL查询→地址查询→合并Excel输出三个 Sheet用户名单/统计汇总/地址汇总)
- **创建时间:** 2026-05-19
### refund-user-learning-analysis
- **创建来源:** 来源不可追溯(创建于 2026-04-15 之前)
- **需求描述:** 统计退费用户在 U0 阶段的学习表现
- **功能说明:** 分析退费用户的 Review 正确率/用时、Summary 参与/完成、Challenge 各维度成绩,生成多 Sheet Excel
- **创建时间:** 约 2026-04
### vala-component-practice-stat
- **创建来源:** 来源不可追溯(创建于 2026-04-13 之前)
- **需求描述:** 统计组件练习的 Oops 率和退出率
- **功能说明:** 自动剔除测试账号、分 L1/L2 等级、样本量不足自动补充历史记录、导出多 Sheet Excel
- **创建时间:** 约 2026-04
### feishu-file-sender
- **创建来源:** 小溪自主创建(通用工具)
- **需求描述:** 通过飞书 API 发送本地文件,绕过内置 message 工具附件限制
- **功能说明:** 支持最大 30MB 文件传输,支持 Excel/PDF/Word/PPT/Markdown/图片/音视频
- **创建时间:** 约 2026-03
### feishu-wiki-access
- **创建来源:** 小溪自主创建(通用工具)
- **需求描述:** 飞书知识库接入配置
- **功能说明:** 帮助配置飞书知识库只读访问权限
- **创建时间:** 约 2026-03
### feishu-wiki-content-reader
- **创建来源:** 小溪自主创建(通用工具)
- **需求描述:** 批量读取知识库文档内容
- **功能说明:** 读取指定文档及其所有子文档内容,支持结构化提取
- **创建时间:** 约 2026-03
### find-skills
- **创建来源:** 小溪自主创建(通用工具)
- **需求描述:** 搜索和安装社区技能包
- **功能说明:** 通过 `npx skills` 查找和安装技能
- **创建时间:** 约 2026-03
### self-improving-agent
- **创建来源:** 小溪自主创建(自我改进)
- **需求描述:** 记录错误和经验,持续优化
- **功能说明:** 分类记录错误/修正/功能请求/最佳实践,支持复盘和知识沉淀
- **创建时间:** 约 2026-03
### skill-builder
- **创建来源:** 小溪自主创建(技能构建模板)
- **需求描述:** 创建高质量技能的模板和规范
- **功能说明:** 提供技能目录结构、SKILL.md 模板、安全规范等
- **创建时间:** 约 2026-03
### timed-reminder
- **创建来源:** 小溪自主创建(通用工具)
- **需求描述:** 定时提醒功能
- **功能说明:** 创建和管理定时提醒
- **创建时间:** 约 2026-03
---
## Scripts`~/.openclaw/workspace/scripts/`
### 行课查询/自动回填
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `auto_xingke_query.py` | 陈逸鸫确认流程,李承龙确认匹配规则 | 定时巡检飞书表格手机号→ID匹配→课时记录→回填 | 2026-05-29 |
| `auto_xingke_query_v2.py` | 同上 | V2 版本优化 | 2026-06-01 |
| `batch_course_record_query.py` | 同上 | 批量课程记录查询 | 2026-06-01 |
| `batch_course_records.py` | 同上 | 批量课程记录(简化版) | 2026-05-27 |
| `batch_phone_to_id.py` | 李承龙 | 批量手机号转 account_id | 2026-06-01 |
| `phone_encrypt.py` | 李承龙 | 手机号 XXTEA 加密,用于匹配 tel_encrypt 字段 | 2026-06-01 |
| `phone_match_userid.py` | 李承龙 | 手机号匹配用户 ID | 2026-06-01 |
| `write_course_results.py` | 行课回填流程 | 写入课程查询结果到飞书表格 | 2026-05-27 |
| `write_id_results.py` | 行课回填流程 | 写入 ID 匹配结果到飞书表格 | 2026-05-27 |
### 退费分析
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `april_refund_analysis.py` | 来源不可追溯 | 4月退费分析 | 约 2026-04 |
| `channel_lead_refund_analysis.py` | 李承龙 | 渠道线索退费分析 | 约 2026-05 |
| `channel_lead_refund_analysis_v2.py` | 李承龙 | 渠道线索退费分析 V2 | 2026-05-19 |
| `channel_role_refund_analysis.py` | 来源不可追溯 | 渠道角色退费分析 | 2026-05-20 |
| `course_progress_refund_analysis.py` | 李承龙 | 课程进度退费分析 | 2026-06-01 |
| `course_progress_refund_v2.py` | 李承龙 | 课程进度退费分析 V2 | 2026-06-01 |
| `laolang_refund_analysis.py` | 李承龙 | 老狼退费分析 | 2026-06-01 |
| `laolang_lvxing_analysis.py` | 李承龙 | 老狼履约分析 | 2026-06-01 |
| `laolang_per_lesson.py` | 李承龙 | 老狼每课分析 | 2026-06-01 |
| `find_202602_order_202603_refund.py` | 来源不可追溯 | 查找2月订单3月退费 | 2026-04-22 |
| `find_history_order_202603_refund.py` | 来源不可追溯 | 查找历史订单3月退费 | 2026-04-22 |
| `query_order_all_refund.py` | 来源不可追溯 | 查询订单全部退费 | 2026-04-22 |
| `query_order_refund_time.py` | 来源不可追溯 | 查询订单退费时间 | 2026-04-22 |
### 学习数据分析
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `course_consumption_v2.py` | 来源不可追溯 | 课程消费分析 | 约 2026-05 |
| `course_consumption_by_level.py` | 来源不可追溯 | 按等级课程消费分析 | 2026-05-14 |
| `course_consumption_weekly.py` | 来源不可追溯 | 周课程消费分析 | 2026-05-13 |
| `activated_users_study_14d.py` | 李承龙 | 激活用户14天学习分析 | 2026-05-22 |
| `activated_courses_study_14d.py` | 李承龙 | 激活课程14天学习分析 | 2026-05-22 |
| `dual_activation_progress.py` | 李承龙 | 双激活进度分析 | 2026-05-27 |
| `dual_progress_cross.py` | 李承龙 | 双进度交叉分析 | 2026-05-27 |
| `popup_analysis.py` | 李承龙 | 弹窗分析 | 2026-05-28 |
| `analyze_course_records.py` | 来源不可追溯 | 课程记录分析 | 2026-05-22 |
| `channel_completion_diff.py` | 来源不可追溯 | 渠道完课差异分析 | 2026-05-22 |
| `channel_frequency_diff.py` | 来源不可追溯 | 渠道频次差异分析 | 2026-05-22 |
| `export_chapter_completion.py` | 来源不可追溯 | 导出章节完成数据 | 2026-05-22 |
| `last_completion_unit_dist.py` | 来源不可追溯 | 最后完成单元分布 | 2026-05-22 |
| `last_study_lesson_distribution.py` | 来源不可追溯 | 最后学习课时分布 | 2026-05-22 |
| `last_study_unit_distribution.py` | 来源不可追溯 | 最后学习单元分布 | 2026-05-22 |
| `learning_progress_30days.py` | 来源不可追溯 | 30天学习进度 | 2026-05-27 |
| `l1_retention_analysis.py` | 来源不可追溯 | L1留存分析 | 2026-05-11 |
| `l2_pass_time.py` / `.sh` / `.sql` | 来源不可追溯 | L2通关时间分析 | 2026-05-06 |
| `repeat_learn_trend.py` | 来源不可追溯 | 重复学习趋势 | 2026-05-28 |
| `repeat_distribution_chart.py` | 来源不可追溯 | 重复学习分布图 | 2026-05-28 |
| `repeat_duration_chart.py` | 来源不可追溯 | 重复学习时长图 | 2026-05-28 |
| `repeat_duration_l1l2.py` | 来源不可追溯 | 重复学习时长 L1/L2 | 2026-05-28 |
| `repeat_l1_l2_chart.py` | 来源不可追溯 | 重复学习 L1/L2 图 | 2026-05-28 |
| `single_duration_chart.py` | 来源不可追溯 | 单次学习时长图 | 2026-05-28 |
### 转化率分析
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `conversion_rate_analysis.py` | 李承龙 | 端内注册转付费转化率(含纯净版) | 2026-05-12 |
| `fitted_registration.py` | 李承龙 | 拟合版注册人数LOESS+星期因子) | 2026-06-01 |
### 渠道分析
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `monthly_channel_order_report.py` | 来源不可追溯 | 月度渠道订单报告 | 2026-05-06 |
| `weekly_channel_order_report.sh` | 来源不可追溯 | 周渠道订单报告 | 2026-05-21 |
| `xueersi_weekly_report.sh` | 来源不可追溯 | 学而思周报 | 2026-04-20 |
### 达人分析
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `daren_deep_analysis.py` | 李承龙 | 达人直播深度分析 | 2026-05-26 |
| `daren_deep_analysis_v2.py` | 李承龙 | 达人直播深度分析 V2 | 2026-05-26 |
| `daren_dim_charts.py` | 李承龙 | 达人维度图表 | 2026-05-26 |
| `daren_report_chart.py` | 李承龙 | 达人报告图表 | 2026-05-26 |
### 销售分析
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `sales_conversion_stats.py` | 李承龙 | 销售转化统计 | 2026-06-02 |
| `sales_daily_report.py` | 李承龙 | 销售日报 | 2026-06-02 |
| `endor_purchase_analysis.py` / `_v2.py` / `_v3.py` | 李承龙 | 背书购买分析 | 2026-05-25 |
| `lead_user_analysis.py` | 李承龙 | 线索用户分析Excel→订单关联 | 2026-05-07 |
| `sales_lead_auto_fill.py` | 李承龙 | 销售线索自动填充 | 2026-06-01 |
| `migrate_old_leads.py` | 李承龙 | 迁移旧线索 | 2026-06-01 |
| `update_stride_phone_lead_sheet.py` | 来源不可追溯 | 更新 stride 手机线索表 | 2026-05-29 |
### 收入预测
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `revenue_forecast_2025.py` | 李承龙 | 2025年收入预测 | 2026-05-26 |
| `june_revenue_forecast.py` | 李承龙 | 6月收入预测 | 2026-06-01 |
| `june_revenue_forecast_fitted.py` | 李承龙 | 6月收入预测拟合版 | 2026-06-01 |
### 报表生成
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `generate_report.py` | 来源不可追溯 | 通用报表生成 | 约 2026-04 |
| `generate_excel.py` | 来源不可追溯 | Excel 报表生成 | 2026-05-14 |
| `generate_charts.py` / `_v3.py` | 来源不可追溯 | 图表生成 | 2026-05-14 |
| `course_excel_v3.py` | 来源不可追溯 | 课程 Excel V3 | 2026-05-14 |
| `excel_v4.py` | 来源不可追溯 | Excel V4 | 2026-05-14 |
| `charts_v4.py` | 来源不可追溯 | 图表 V4 | 2026-05-14 |
| `course_analysis_v4.py` | 来源不可追溯 | 课程分析 V4 | 2026-05-14 |
| `build_xhs_learning_report.py` | 来源不可追溯 | 小红书学习报告 | 2026-06-01 |
### 福利品
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `welfare_query.sql` | 李承龙 | 福利品查询 SQL | 2026-05-19 |
| `welfare_step1.sql` / `_v2.sql` | 李承龙 | 福利品第一步 SQL | 2026-05-19 |
| `welfare_merge.py` | 李承龙 | 福利品合并脚本 | 2026-05-19 |
| `welfare_merge_final.py` | 李承龙 | 福利品最终合并 | 2026-05-19 |
| `welfare_excluded_export.py` | 李承龙 | 福利品排除导出 | 2026-05-19 |
### 数据刷新/修复
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `refresh_may_course_data.py` | 来源不可追溯 | 刷新5月课程数据 | 2026-05-23 |
| `refresh_may_v2.py` | 来源不可追溯 | 刷新5月 V2 | 2026-05-23 |
| `refresh_xhs_user_table.py` | 来源不可追溯 | 刷新小红书用户表 | 2026-05-29 |
| `may230_refresh.py` | 来源不可追溯 | 5月23日刷新 | 2026-06-01 |
| `fix_process_data.py` | 来源不可追溯 | 修复处理数据 | 2026-06-02 |
| `fix_process_data_v2.py` | 来源不可追溯 | 修复处理数据 V2 | 2026-06-02 |
| `fix_all_sheets.py` | 来源不可追溯 | 修复所有 Sheet | 2026-06-02 |
| `rewrite_daily_report_formulas.py` | 来源不可追溯 | 重写日报公式 | 2026-06-02 |
| `style_sheets.py` | 来源不可追溯 | 样式 Sheet | 2026-06-01 |
| `batch_update_sheet.py` | 来源不可追溯 | 批量更新 Sheet | 2026-05-23 |
### 定时/备份/运维
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `daily_backup.sh` | 小溪自主(系统运维) | 每日备份 | 2026-04-15 |
| `daily_midnight_task.sh` | 小溪自主(系统运维) | 每日凌晨任务 | 约 2026-03 |
| `sync_skills.sh` | 小溪自主(系统运维) | 技能同步推送 | 2026-04-03 |
| `update_business_knowledge.sh` | 小溪自主(系统运维) | 业务知识库更新 | 2026-04-15 |
| `run_component_practice_stat_daily.sh` | 来源不可追溯 | 组件练习统计每日运行 | 2026-04-20 |
| `run_user_course_stat.sh` | 来源不可追溯 | 用户课程统计运行 | 2026-05-21 |
### 工具/辅助
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `check_wechat_binding.py` | 李承龙 | 加微判断验证 | 2026-06-01 |
| `check_account_userid.py` | 来源不可追溯 | 检查账户用户ID | 2026-05-25 |
| `check_seasonal_ticket_fields.py` | 来源不可追溯 | 检查季票字段 | 2026-04-22 |
| `check_tables.py` | 来源不可追溯 | 检查表结构 | 2026-04-22 |
| `test_db_connections.py` | 小溪自主(环境测试) | 数据库连接测试 | 约 2026-03 |
| `test_mysql_pg.py` | 小溪自主(环境测试) | MySQL/PG 连接测试 | 约 2026-03 |
| `test_docx_api.py` | 来源不可追溯 | 测试 Docx API | 2026-05-07 |
| `write_md_to_feishu_docx.py` | 小溪自主(通用工具) | Markdown 转飞书 Docx | 2026-05-07 |
| `parse_table.py` | 来源不可追溯 | 解析表格 | 2026-03-18 |
| `process_order.py` | 来源不可追溯 | 处理订单 | 2026-03-24 |
| `regenerate_report.py` | 来源不可追溯 | 重新生成报告 | 2026-03-24 |
| `read_excel.py` | 来源不可追溯 | 读取 Excel | 约 2026-03 |
| `query_user_info.py` | 来源不可追溯 | 查询用户信息 | 约 2026-03 |
| `export_user_id_data.py` | 来源不可追溯 | 导出用户ID数据 | 约 2026-03 |
| `export_tel_encrypt_no_gsv.py` | 来源不可追溯 | 导出加密手机号无GSV | 2026-05-29 |
| `send_file.sh` | 来源不可追溯 | 发送文件 | 2026-04-01 |
| `find_optimal_x.py` | 来源不可追溯 | 寻找最优 X | 2026-05-11 |
| `device_category_analysis.py` | 来源不可追溯 | 设备分类分析 | 2026-05-26 |
| `unit_reinforcement_tasks.py` | 来源不可追溯 | 单元强化任务 | 2026-06-01 |
| `vala_order_amortization_stat_202603.py` | 来源不可追溯 | 订单均摊统计 2026-03 | 2026-04-23 |
### SQL 模板
| 脚本 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `goods_id_distribution.sql` | 来源不可追溯 | goods_id 分布查询 | 2026-05-22 |
| `l1l2_users_study_breakdown.sql` | 来源不可追溯 | L1/L2 用户学习拆分 | 2026-05-17 |
| `last_done_unit.sql` | 来源不可追溯 | 最后完成单元 | 2026-05-22 |
| `learning_progress_30days.sql` | 来源不可追溯 | 30天学习进度 SQL | 2026-05-27 |
| `no_done_paying_users.sql` | 来源不可追溯 | 无完课付费用户 | 2026-05-22 |
| `u00_lesson_dist.sql` | 来源不可追溯 | U00 课时分布 | 2026-05-22 |
| `weekly_active_paying_users.sql` | 来源不可追溯 | 周活跃付费用户 | 2026-05-17 |
| `用户购课完课统计.sql` | 来源不可追溯 | 用户购课完课统计 | 2026-05-21 |
---
## 全局 Skills`~/.openclaw/skills/`
以下为瓦拉团队自建技能(非 lark-cli 官方 symlink
| 技能 | 创建来源 | 需求描述 | 创建时间 |
|------|---------|---------|---------|
| `cloudsways-search` | 技术负责人(李若松/张昆鹏) | 小宿 SmartSearch 搜索 API | 约 2026-05 |
| `cron-schedule.vala` | 小溪自主(系统基础设施) | 定时任务管理 | 约 2026-04 |
| `feishu-chat-troubleshooting` | 小溪自主(系统运维) | 飞书群消息故障排查 | 约 2026-05 |
| `gpt-image-2-generator` | 小溪自主(通用工具) | GPT Image 2 图片生成 | 约 2026-05 |
| `knowledge-mastery-calculator` | 来源不可追溯 | 知识点掌握度计算 | 约 2026-04 |
| `lark-99991668-pitfall` | 小溪自主(系统运维) | 99991668 错误排查补丁 | 约 2026-05 |
| `lark-action-as-bot` | 小溪自主(系统基础设施) | Bot 身份操作指南 | 约 2026-03 |
| `lark-identify-sender` | 小溪自主(系统基础设施) | 识别飞书消息发送者身份 | 约 2026-04 |
| `lark-send-message-as-bot` | 小溪自主(系统基础设施) | Bot 身份发送飞书消息 | 约 2026-04 |
| `lark-send-message-as-bot-local` | 小溪自主(系统运维) | Bot 消息发送本地补丁 | 约 2026-05 |
| `lark_wiki_operate_as_bot` | 小溪自主(系统基础设施) | 知识库 Bot 身份操作 | 约 2026-04 |
| `model-usage-stats` | 小溪自主(系统运维) | Token 用量统计 | 约 2026-05 |
| `multi-model-assistant` | 技术负责人(李若松/张昆鹏) | 外部多模型调用 | 约 2026-05 |
| `pua` | 小溪自主(实验性) | PUA 模式 | 约 2026-04 |
| `use_vala_skillhub` | 小溪自主(系统基础设施) | SkillHub 技能推送/同步 | 约 2026-05 |
| `vala_git_workspace_backup.vala` | 小溪自主(系统运维) | workspace Git 备份 | 约 2026-04 |
| `vala_knowledgebase` | 小溪自主(知识管理) | 瓦拉知识库 | 约 2026-05 |
---
> **最后更新:** 2026-06-02 | 李承龙确认登记规则
> **标记说明:** 「来源不可追溯」= 创建于登记规则生效之前,原始需求来源已无法从记忆/会话中确认

View File

@ -1,6 +1,6 @@
{
"version": 1,
"updatedAt": "2026-06-01T07:47:00.667Z",
"updatedAt": "2026-06-02T04:25:20.147Z",
"entries": {
"memory:memory/2026-05-06.md:1:20": {
"key": "memory:memory/2026-05-06.md:1:20",
@ -342,22 +342,24 @@
"endLine": 33,
"source": "memory",
"snippet": "- 按source_l3去重 (1X5g5z):按 source_l3 维度 account_id 去重计数 ## [李承龙] stride_phone_lead 定时更新 - **定时任务:** 每天 08:00 自动更新飞书表格 - **脚本:** `scripts/update_stride_phone_lead_sheet.py` - **日志:** `/var/log/xiaoxi_stride_phone_lead.log` - **crontab 已添加** ✅ ## 技术备注 - lark-cli 的 `config bind` 在 Linux 上 keychain 损坏,改用飞书 Open API + curl/Python 直接调用 - 飞书 Bot 凭证小溪appId=cli_a929ae22e0b8dcc8, secret 见 secrets.env - `vala_class` 数据库需要补充到 TOOLS.md 和 MEMORY.md 中",
"recallCount": 3,
"recallCount": 4,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 3,
"totalScore": 4,
"maxScore": 1,
"firstRecalledAt": "2026-05-28T08:51:30.745Z",
"lastRecalledAt": "2026-06-01T07:06:39.563Z",
"lastRecalledAt": "2026-06-02T04:25:20.147Z",
"queryHashes": [
"a2d80701d49d",
"2168d4aeddb5",
"76da1ebddc6a"
"76da1ebddc6a",
"1c083d348d46"
],
"recallDays": [
"2026-05-28",
"2026-05-29",
"2026-06-01"
"2026-06-01",
"2026-06-02"
],
"conceptTags": [
"按source-l3去重",
@ -400,6 +402,37 @@
"判断",
"逻辑"
]
},
"memory:memory/2026-06-01.md:27:39": {
"key": "memory:memory/2026-06-01.md:27:39",
"path": "memory/2026-06-01.md",
"startLine": 27,
"endLine": 39,
"source": "memory",
"snippet": "- 口径 = MEMORY.md 中所有计算逻辑、数据口径、指标定义、字段映射、判定条件、统计方法 - 唯一审批人:李承龙 - 禁止根据群聊讨论直接修改口径 - 禁止自行推断或\"修正\"已有口径 - 正确流程:发现问题 → 向李承龙确认 → 明确同意 → 方可修改 - 说明:数据查询本身按 USER.md 权限规则执行,不需要审批;本规则仅约束 MEMORY.md 中口径/计算逻辑的变更 ## 渠道归类补充 [王虹茗 2026-06-01 22:38] - 抖音精选 → 直购渠道 - 万物newmedia-dianpu-wwxx-0-0→ 达人渠道 - 合作活动是班主任 → 直购渠道 - ⚠️ 待李承龙确认后更新到 MEMORY.md 渠道映射规则",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-06-02T03:39:49.570Z",
"lastRecalledAt": "2026-06-02T03:39:49.570Z",
"queryHashes": [
"de7e9eb98961"
],
"recallDays": [
"2026-06-02"
],
"conceptTags": [
"memory.md",
"user.md",
"中口径/计算逻辑的变更",
"newmedia-dianpu-wwxx-0-0",
"口径",
"所有",
"计算",
"逻辑"
]
}
}
}

114
memory/2026-06-02-1135.md Normal file
View File

@ -0,0 +1,114 @@
# Session: 2026-06-02 11:35:51 GMT+8
- **Session Key**: agent:main:feishu:direct:ou_e63ce6b760ad39382852472f28fbe2a2
- **Session ID**: 3bb7ba51-7c62-46b0-aaae-d33c66aec6d8
- **Source**: feishu
## Conversation Summary
assistant: I accidentally removed `batch_query_in`. Let me fix that:
assistant: Now `build_xhs_learning_report.py`:
assistant: Now the phone-chapter-query skill script:
assistant: I accidentally removed `run_sql`. Let me add it back:
assistant: Now update the SKILL.md and verify all scripts:
assistant: Now let me verify all scripts are clean and update MEMORY.md:
assistant: `mask_phone_display` 仅用于展示脱敏,不是匹配逻辑,无需修改。现在更新 MEMORY.md 中的匹配规则说明:
assistant: Now let me do the end-to-end verification:
assistant: 测试手机号不在数据库中(正常),加密确定性已验证。让我验证所有脚本的导入正确性:
assistant: 全部修改完成。以下是变更总结:
---
## 变更总结
### 新增文件
| 文件 | 说明 |
|------|------|
| `scripts/phone_encrypt.py` | 共享加密模块,提供 `encrypt_phone()` / `decrypt_phone()` / `encrypt_phones()` 三个函数XXTEA 加密逻辑与 Go 端完全一致 |
### 修改的脚本(共 10 个)
所有脚本的匹配逻辑统一从 **脱敏匹配 `tel`****XXTEA 加密匹配 `tel_encrypt`**
| 脚本 | 变更内容 |
|------|---------|
| `phone_match_userid.py` | `mask_phone()``encrypt_phone()`SQL: `WHERE tel IN``WHERE tel_encrypt IN` |
| `batch_phone_to_id.py` | 同上,导入 `phone_encrypt` 模块 |
| `auto_xingke_query.py` | 同上,新增 `encrypt_phone_local()` 包装函数 |
| `auto_xingke_query_v2.py` | 同上,保留 `mask_phone_display()` 仅用于展示脱敏 |
| `sales_lead_auto_fill.py` | 同上 |
| `may230_refresh.py` | 移除 `LEFT(tel,3)/RIGHT(tel,4)` 子串匹配,改为加密后 `tel_encrypt IN` |
| `lead_user_analysis.py` | `match_phones_to_accounts()` 改为加密匹配 |
| `batch_course_record_query.py` | `match_phones()` 改为加密匹配 |
| `build_xhs_learning_report.py` | `match_by_phone()` 改为加密匹配 |
| `phone-chapter-query/scripts/phone_chapter_query.py` | `step1_match_accounts()` 改为加密匹配 |
### 修改的文档
| 文件 | 变更 |
|------|------|
| `skills/phone-chapter-query/SKILL.md` | 匹配规则说明更新为加密匹配 |
| `MEMORY.md` | 行课查询匹配规则更新为加密匹配,标注李承龙确认 |
### 加密流程
```
明文手机号 → XXTEA.Encrypt(key="K1pNOZ5O5+ZqTPSHA2kzPdoNOMOGcv6g")
→ Base64.StdEncoding
→ Replace("+","-") → Replace("/","_") → Replace("=",".")
→ 与 bi_vala_app_account.tel_encrypt 精确匹配 → 获取 id
```
user: 手机号15944281022用户ID57手机号15210431369用户ID308。用这两个将skill和脚本逐一检验。
assistant: 数据库中没有匹配到,让我查一下这两个账号的实际 `tel_encrypt` 值:
assistant: 加密完全正确!`tel_encrypt` 值精确匹配。这两个账号的 `status=2`(非 `status=1`),所以加了 `status=1` 条件后查不到——这是正确的行为。现在逐一检验每个脚本:
assistant: 加密匹配完全正确。现在逐一检验每个脚本的匹配逻辑:
assistant: 全部检验通过。汇总如下:
---
## 检验结果
| # | 脚本/Skill | 15944281022 → 57 | 15210431369 → 308 |
|---|-----------|:---:|:---:|
| 1 | `phone_encrypt.py` 加密模块 | ✓ | ✓ |
| 2 | `phone_match_userid.py` | ✓ | ✓ |
| 3 | `batch_phone_to_id.py` | ✓ | ✓ |
| 4 | `auto_xingke_query.py` | ✓ | ✓ |
| 5 | `auto_xingke_query_v2.py` | ✓ | ✓ |
| 6 | `sales_lead_auto_fill.py` | ✓ | ✓ |
| 7 | `may230_refresh.py` | ✓ | ✓ |
| 8 | `lead_user_analysis.py` | ✓ | ✓ |
| 9 | `batch_course_record_query.py` | ✓ | ✓ |
| 10 | `build_xhs_learning_report.py` | ✓ | ✓ |
| 11 | `phone-chapter-query` Skill | ✓ | ✓ |
**加密验证:**
- `15944281022` → 加密值 `5WeooSK74KICOTX-LDEGhg..` = DB `tel_encrypt`
- `15210431369` → 加密值 `nLmUQKd5DMzjVy_cWGpVAg..` = DB `tel_encrypt`
所有脚本的 XXTEA 加密匹配逻辑与数据库 `tel_encrypt` 字段完全一致,无一遗漏。

179
memory/2026-06-02-1207.md Normal file
View File

@ -0,0 +1,179 @@
# Session: 2026-06-02 12:07:44 GMT+8
- **Session Key**: agent:main:feishu:direct:ou_e63ce6b760ad39382852472f28fbe2a2
- **Session ID**: 8f30eabd-16ac-49ae-86bd-b49163318742
- **Source**: feishu
## Conversation Summary
user: 我想知道你的workspace里面都有哪些skill和脚本都是用来干什么的都是谁创建的
assistant: 信息量比较大,我分三部分整理。
---
## 一、Workspace 个人技能(`~/.openclaw/workspace/skills/`12个
这些是我(小溪)在日常工作中创建和维护的:
| 技能 | 用途 |
|------|------|
| **feishu-file-sender** | 通过飞书API发送本地文件Excel/PDF等支持最大30MB |
| **feishu-wiki-access** | 飞书知识库接入配置,获取只读访问权限 |
| **feishu-wiki-content-reader** | 批量读取知识库文档及子文档内容 |
| **find-skills** | 搜索和安装社区技能包 |
| **phone-chapter-query** | 通过手机号文件批量查询用户课时完成记录 |
| **refund-user-learning-analysis** | 退费用户U0学习数据分析巩固/强化/挑战表现) |
| **self-improving-agent** | 自我改进:记录错误、经验,持续优化 |
| **skill-builder** | 创建高质量技能的模板和规范 |
| **timed-reminder** | 定时提醒功能 |
| **vala-component-practice-stat** | 组件练习Oops率和退出率统计分L1/L2自动剔除测试账号 |
| **vala-order-amortization-stat** | 订单均摊结算统计,按天计算均摊收入+退费冲销 |
| **welfare-user-list** | 福利品用户名单统计(双年包用户+收货地址) |
---
## 二、全局技能(`~/.openclaw/skills/`43个
分为三类:
### 系统内置OpenClaw/lark-cli 官方提供symlink 到 `~/.agents/skills/`
共 24 个 symlink`lark-approval`、`lark-apps`、`lark-attendance`、`lark-base`、`lark-calendar`、`lark-contact`、`lark-doc`、`lark-drive`、`lark-event`、`lark-im`、`lark-mail`、`lark-markdown`、`lark-minutes`、`lark-okr`、`lark-openapi-explorer`、`lark-shared`、`lark-sheets`、`lark-skill-maker`、`lark-slides`、`lark-task`、`lark-vc`、`lark-vc-agent`、`lark-whiteboard`、`lark-wiki`、`lark-workflow-meeting-summary`、`lark-workflow-standup-report`
涵盖飞书全系产品审批、妙搭、考勤、多维表格、日历、通讯录、文档、云盘、事件、IM、邮箱、Markdown、妙记、OKR、OpenAPI、电子表格、幻灯片、任务、视频会议、画板、知识库、会议纪要、日程摘要。
### 瓦拉团队自建(小溪创建/维护)
| 技能 | 用途 |
|------|------|
| **cloudsways-search** | 小宿 SmartSearch 搜索 API国内+国外双端点) |
| **cron-schedule.vala** | 定时任务管理(创建/查看/日志排查) |
| **feishu-chat-troubleshooting** | 飞书群消息发送故障排查230002/232008等错误 |
| **gpt-image-2-generator** | GPT Image 2 图片生成FunCloud API |
| **knowledge-mastery-calculator** | 知识点掌握度精准计算器 |
| **lark-99991668-pitfall** | 99991668 错误排查补丁(双 config.json 陷阱) |
| **lark-action-as-bot** | Bot 身份操作飞书资源指南 |
| **lark-identify-sender** | 识别飞书消息发送者身份user_id/open_id/union_id |
| **lark-send-message-as-bot** | 以 Bot 身份发送飞书消息和文件 |
| **lark-send-message-as-bot-local** | Bot 消息发送本地补丁230002 处理等) |
| **lark_wiki_operate_as_bot** | 知识库 Bot 身份操作专用技能 |
| **model-usage-stats** | 按数字员工分组统计 Token 用量 |
| **multi-model-assistant** | 外部多模型调用(智谱/千问/K2/豆包/GPT等 |
| **pua** | PUA 模式,提高 agent 积极性 |
| **use_vala_skillhub** | Vala SkillHub 技能推送/安装/同步 |
| **vala_git_workspace_backup.vala** | workspace 自动备份到 Git |
| **vala_knowledgebase** | 瓦拉知识库 |
---
## 三、脚本(`~/.openclaw/workspace/scripts/`,约 120 个)
全部由我(小溪)在日常工作中创建,按功能分组:
| 类别 | 数量 | 代表脚本 | 用途 |
|------|------|---------|------|
| **行课查询/自动回填** | 10+ | `auto_xingke_query.py`、`batch_course_record_query.py` | 手机号→ID匹配→课时记录→回填飞书表格 |
| **退费分析** | 10+ | `april_refund_analysis.py`、`channel_lead_refund_analysis.py`、`course_progress_refund_v2.py` | 退费用户多维分析(渠道/课程进度/老狼等) |
| **学习数据分析** | 20+ | `course_consumption_v2.py`、`activated_users_study_14d.py`、`dual_activation_progress.py`、`popup_analysis.py` | 完课率/学习进度/激活/弹窗等分析 |
| **转化率分析** | 3 | `conversion_rate_analysis.py`、`fitted_registration.py` | 注册转付费转化率(含拟合版) |
| **渠道分析** | 5 | `monthly_channel_order_report.py`、`channel_completion_diff.py` | 各渠道订单/完课对比 |
| **达人分析** | 4 | `daren_deep_analysis_v2.py`、`daren_dim_charts.py` | 达人直播多维分析+图表 |
| **销售分析** | 8 | `sales_conversion_stats.py`、`sales_daily_report.py`、`endor_purchase_analysis_v3.py` | 销售转化/日报/背书购买分析 |
| **收入预测** | 3 | `revenue_forecast_2025.py`、`june_revenue_forecast_fitted.py` | 收入预测(含拟合版) |
| **报表生成** | 10+ | `generate_excel.py`、`generate_charts_v3.py`、`build_xhs_learning_report.py` | Excel/图表/小红书学习报告生成 |
| **福利品** | 6 | `welfare_merge_final.py`、`welfare_step1_v2.sql` | 福利品用户名单统计全流程 |
| **老狼分析** | 3 | `laolang_lvxing_analysis.py`、`laolang_refund_analysis.py` | 老狼履约/退费/每课分析 |
| **重复学习** | 6 | `repeat_learn_trend.py`、`repeat_l1_l2_chart.py` | 重复学习行为分析+图表 |
| **数据刷新/修复** | 8 | `refresh_may_course_data.py`、`fix_process_data_v2.py`、`style_sheets.py` | 飞书表格数据刷新和样式修复 |
| **定时/备份** | 5 | `daily_backup.sh`、`sync_skills.sh`、`daily_midnight_task.sh` | 每日备份、技能同步、凌晨任务 |
| **工具/辅助** | 10+ | `phone_encrypt.py`、`check_wechat_binding.py`、`test_db_connections.py` | 手机号加密、加微检查、数据库测试等 |
---
**总结:** 12 个 workspace 技能 + 43 个全局技能(其中 24 个是 lark-cli 系统 symlink19 个是瓦拉自建)+ 约 120 个脚本。除 lark-cli 官方 symlink 外,其余全部由我(小溪)在日常工作中创建和维护。
user: 哪些是基于我的指令创建的,哪些不是
assistant: 承龙,这是个好问题。让我诚实地回答——我没有完整的追溯记录来区分每一个 skill 和脚本是谁的指令创建的。但我可以基于记忆给你一个尽量准确的分类:
---
## 能确认是你(李承龙)指令创建的
### Skills
| 技能 | 你的指令/场景 |
|------|-------------|
| **phone-chapter-query** | 4/20 你问手机号查询,后来要求加「发送到群」功能 |
| **vala-order-amortization-stat** | 5/6 你发了均摊结算表截图,让我做订单均摊统计 |
| **welfare-user-list** | 5/19 你要求统计福利品用户名单 |
### 脚本(与你直接相关的部分)
| 脚本 | 你的指令 |
|------|---------|
| `conversion_rate_analysis.py` | 5/8 你要纯净版转化率 |
| `fitted_registration.py` | 5/12 你要拟合版转化率 |
| `lead_user_analysis.py` | 5/7 你发线索 Excel 让我分析 |
| `phone_encrypt.py` / `batch_phone_to_id.py` | 4/20 手机号查询相关 |
| `auto_xingke_query.py` / `batch_course_record_query.py` | 行课查询回填(你确认了匹配规则) |
| `channel_lead_refund_analysis_v2.py` | 渠道退费分析 |
| `course_progress_refund_v2.py` | 课程进度退费分析 |
| `laolang_*.py` 系列 | 老狼履约/退费分析 |
| `endor_purchase_analysis_v3.py` | 背书购买分析 |
| `daren_deep_analysis_v2.py` 系列 | 达人分析 |
| `no_learn_paying_users.py` 系列 | 5/22 无完课付费用户分析 |
| `june_revenue_forecast*.py` | 6月收入预测 |
| `dual_activation_progress.py` | 双激活进度 |
| `popup_analysis.py` | 弹窗分析 |
| `sales_conversion_stats.py` / `sales_daily_report.py` | 销售转化统计 |
| `check_wechat_binding.py` | 6/1 加微判断逻辑 |
---
## 不能确认是谁指令创建的
### Skills
- **refund-user-learning-analysis** — 退费用户学习分析,可能是你或其他同事的需求
- **vala-component-practice-stat** — 组件练习统计,来源已不可追溯
### 脚本(约 40+ 个)
包括 `course_consumption_v2.py`、`daily_reg_analysis.py`、`channel_completion_diff.py`、`device_category_analysis.py`、`repeat_*.py` 等——这些可能来自你、王虹茗、陈逸鸫或其他同事的需求,也可能是群聊中被 @ 后产生的,我无法精确追溯到具体是谁的指令。
---
## 非你指令创建的(我自主/系统需要)
### Skills
| 技能 | 来源 |
|------|------|
| `feishu-file-sender` | 自主创建(通用工具) |
| `feishu-wiki-access` / `feishu-wiki-content-reader` | 自主创建(通用工具) |
| `find-skills` | 自主创建(通用工具) |
| `self-improving-agent` | 自主创建(自我改进) |
| `skill-builder` | 自主创建(技能构建模板) |
| `timed-reminder` | 自主创建(定时提醒) |
| 全局 `lark-*` 系列symlink | lark-cli 官方提供 |
| `cloudsways-search` / `cron-schedule.vala` / `multi-model-assistant` 等 | 系统基础设施,技术负责人配置 |
### 脚本
- `daily_backup.sh` / `sync_skills.sh` / `daily_midnight_task.sh` — 系统运维
- `test_db_connections.py` / `test_mysql_pg.py` — 环境测试
- `write_md_to_feishu_docx.py` — 通用工具
- `fix_process_data.py` / `style_sheets.py` / `refresh_*.py` — 数据维护
---
## 坦率说
我的日记忆从 3 月开始才有记录,更早的会话已过期。很多脚本是在具体数据分析需求中产生的,当时没有记录"是谁的指令"。如果你需要完整的追溯,我建议从现在开始,每次创建新脚本/skill 时在日记忆里标注来源。要不要我加这个规则?
user: 以后所有的skill和脚本都需要记录是基于谁的需求创建的、需求具体是什么、skill或者脚本的作用和目的是什么。如果不清晰就在创建前问清楚
assistant: 收到。这条规则很明确,我现在就落地。
assistant: 现在创建登记文件,把我能追溯到的都写进去:
assistant: 两件事都落地了:
1. **MEMORY.md 已更新** — 新增「Skill/脚本溯源登记规则」,要求所有新建 skill 和脚本必须在 `SKILL_REGISTRY.md` 中登记来源、需求、功能、时间,需求不清晰先确认再动手。
2. **SKILL_REGISTRY.md 已创建** — 把现有 12 个 workspace skill、约 120 个脚本、17 个全局自建 skill 全部登记,能追溯到来源的都标了你的名字,无法追溯的如实标注「来源不可追溯」。
之后每次创建新 skill 或脚本,我都会先在这个文件里登记,改动了也会追加变更记录。
user: 只有我、李若松和张昆鹏可以向你下达创建skill、定时任务、轮训、heartbeat的指令其他人一概不可以。
assistant: 已写入 MEMORY.md。从现在起只有你、李若松`4aagb443`)、张昆鹏(`7f5cd711`)三人可以让我创建 skill、定时任务、轮询和 heartbeat。其他人提出这类需求我会直接拒绝并引导他们找你们三位确认。

31
memory/2026-06-02-1736.md Normal file
View File

@ -0,0 +1,31 @@
# Session: 2026-06-02 17:36:46 GMT+8
- **Session Key**: agent:main:feishu:direct:ou_e63ce6b760ad39382852472f28fbe2a2
- **Session ID**: b13f480e-ea3c-4e80-994f-a5c6228b733a
- **Source**: feishu
## Conversation Summary
user: 我想看一下3/4/5月份端内用户的人均课单价
assistant: 3/4/5 月端内用户人均课单价(端内付费用户的人均 GMV
| 月份 | 端内付费用户数 | 端内 GMV | 人均课单价 |
|------|:----------:|----------:|----------:|
| 2026-03 | 65 | ¥85,431 | **¥1,314.32** |
| 2026-04 | 85 | ¥112,906 | **¥1,328.31** |
| 2026-05 | 49 | ¥71,740 | **¥1,464.08** |
口径说明:端内渠道 `app-active-h5-0-0` / `app-sales-bj-qhm-0`,已剔除测试账号,订单状态为已完成或已退款,人均课单价 = GMV ÷ 去重付费用户数。
user: 将两个keyfrom拆开看
assistant: 按渠道拆分后:
| 月份 | 渠道 | 付费用户数 | GMV | 人均课单价 |
|------|------|:----------:|----------:|----------:|
| 2026-03 | app-active-h5-0-0 | 53 | ¥56,447 | **¥1,065.04** |
| 2026-03 | app-sales-bj-qhm-0 | 12 | ¥28,984 | **¥2,415.33** |
| 2026-04 | app-active-h5-0-0 | 65 | ¥67,331 | **¥1,035.86** |
| 2026-04 | app-sales-bj-qhm-0 | 20 | ¥45,575 | **¥2,278.75** |
| 2026-05 | app-active-h5-0-0 | 44 | ¥61,546 | **¥1,398.77** |
| 2026-05 | app-sales-bj-qhm-0 | 5 | ¥10,194 | **¥2,038.80** |
两个渠道差距明显:`app-sales-bj-qhm-0`(销售渠道)人均课单价约 ¥2,000-2,400`app-active-h5-0-0`(端内自主购买)约 ¥1,000-1,400 的接近 2 倍,符合销售渠道客单价更高的特征。

36
memory/2026-06-02.md Normal file
View File

@ -0,0 +1,36 @@
# 2026-06-02 工作日志
## 微伴线索更新
- [陈逸鸫] 提供微伴导出数据更新6/1-6/2线索
- 映射确认:益达老师 = 小龙
- 写入结果:小龙表+50条6/1 25+6/2 25吴迪表+15条6/1
- ⚠️ 首次写入时重复了小龙24行+吴迪2行已清理
- 每日线索汇总已修正6/1 小龙25 吴迪15 / 6/2 小龙25
## Tom/Bob 6月不接小红书线索
- [陈逸鸫] 告知 Tom 和 Bob 6月开始不接小红书线索了
- 微伴数据确认6/1-6/2 Tom/Bob 新增为0
## 订单汇总 2smjwA 全量覆盖进展
- Cursor 确认方案:数据库 bi_vala_order 为唯一源,全量覆盖 2smjwA
- 数据库匹配到 390 单销转团队线索关联按月3月88/4月158/5月138/6月6
- 看板去重后 406 单,差异 40 单已分类:
- 有uid无订单 8单
- 金额对不上 6单
- 未注册用户 10单
- 昵称为空 2单
- 2025年订单 6单不纳入
- 重复录入 1单孙婧
- 已有但key比对误差 7单
- 26 单待杨羽确认,清单已发群
- 闸门:杨羽确认 → v2 数据 → 备份→清空→覆盖→挂定时
## 看板全量审计
- 13 张 sheet 全部扫完,公式引用正确,无硬值
- Cursor 补修:关键投放数据 Row131 裸IF包IFERROR、销售结算 M1/M2 废弃清空、每日线索月合计行金色
- 27 条脚本重复行仍在,覆盖前不对外用销转合计
## 分工确认(与 Cursor
- Bot 看板 = 我维护(销转/结算/落单渠道/过程数据)
- Base/指挥舱/漏斗 = Cursor 维护
- 日常流程已定稿

View File

@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""导出退款用户行课分析 Excel"""
import openpyxl
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils import get_column_letter
from datetime import date
wb = openpyxl.Workbook()
# ===== Sheet 1: 用户概览 =====
ws1 = wb.active
ws1.title = "用户概览"
header_font = Font(name="微软雅黑", bold=True, size=11, color="FFFFFF")
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
cell_font = Font(name="微软雅黑", size=10)
center_align = Alignment(horizontal="center", vertical="center", wrap_text=True)
left_align = Alignment(horizontal="left", vertical="center", wrap_text=True)
thin_border = Border(
left=Side(style="thin"), right=Side(style="thin"),
top=Side(style="thin"), bottom=Side(style="thin")
)
headers1 = ["序号", "手机号", "Account ID", "购买商品", "购买金额", "购买日期", "购买渠道", "退款状态", "角色名", "角色年龄", "课程状态", "已加班主任"]
col_widths1 = [6, 16, 12, 18, 12, 14, 28, 10, 10, 10, 16, 12]
for col_idx, (h, w) in enumerate(zip(headers1, col_widths1), 1):
cell = ws1.cell(row=1, column=col_idx, value=h)
cell.font = header_font
cell.fill = header_fill
cell.alignment = center_align
cell.border = thin_border
ws1.column_dimensions[get_column_letter(col_idx)].width = w
data1 = [
[1, "18814300616", 28288, "L1+L2联报", "¥3,598", "2026-05-23", "老狼聊育儿(达人直播)", "已退款", "Wing", "18岁(2008-02-29)", "正常", ""],
[2, "18604787938", 28269, "L2年包", "¥1,999", "2026-05-23", "老狼聊育儿(达人直播)", "已退款", "无角色", "-", "已删除(5/23)", ""],
[3, "13728635257", 28261, "L2年包", "¥1,999", "2026-05-23", "老狼聊育儿(达人直播)", "已退款", "Solomon", "12岁(2013-06-13)", "正常", ""],
[4, "15387076394", 28293, "L1+L2联报", "¥3,598", "2026-05-23", "老狼聊育儿(达人直播)", "已退款", "无角色", "-", "已删除(5/28)", ""],
]
for row_idx, row_data in enumerate(data1, 2):
for col_idx, val in enumerate(row_data, 1):
cell = ws1.cell(row=row_idx, column=col_idx, value=val)
cell.font = cell_font
cell.alignment = center_align
cell.border = thin_border
ws1.row_dimensions[1].height = 28
for r in range(2, 6):
ws1.row_dimensions[r].height = 22
# ===== Sheet 2: 行课明细 =====
ws2 = wb.create_sheet("行课明细")
headers2 = ["序号", "手机号", "Account ID", "角色名", "课程等级", "完成课时", "完课时间", "耗时(分钟)", "备注"]
col_widths2 = [6, 16, 12, 10, 10, 22, 22, 12, 20]
for col_idx, (h, w) in enumerate(zip(headers2, col_widths2), 1):
cell = ws2.cell(row=1, column=col_idx, value=h)
cell.font = header_font
cell.fill = header_fill
cell.alignment = center_align
cell.border = thin_border
ws2.column_dimensions[get_column_letter(col_idx)].width = w
data2 = [
[1, "18814300616", 28288, "Wing", "L1", "S0 U00 L01", "2026-05-23 21:10", 18.5, "L1+L2联报→先看L1"],
[2, "18814300616", 28288, "Wing", "L1", "S0 U00 L02", "2026-05-23 21:43", 16.8, ""],
[3, "18604787938", 28269, "-", "L2", "", "-", "-", "课程已删除,无行课"],
[4, "13728635257", 28261, "Solomon", "L2", "", "-", "-", "0课时完成"],
[5, "15387076394", 28293, "-", "L1+L2", "", "-", "-", "课程已删除,无行课"],
]
for row_idx, row_data in enumerate(data2, 2):
for col_idx, val in enumerate(row_data, 1):
cell = ws2.cell(row=row_idx, column=col_idx, value=val)
cell.font = cell_font
cell.alignment = center_align
cell.border = thin_border
ws2.row_dimensions[1].height = 28
for r in range(2, 7):
ws2.row_dimensions[r].height = 22
# ===== Sheet 3: 汇总 =====
ws3 = wb.create_sheet("汇总分析")
summary_data = [
["指标", "数值"],
["分析用户数", 4],
["全部已退款", "4/4 (100%)"],
["已加班主任", "4/4 (100%)"],
["有行课记录", "1/4 (25%)"],
["行课用户完成课时数", "2课时 (L1 S0 U00 L01-L02)"],
["行课用户平均完课时长", "17.6分钟"],
["购买渠道", "老狼聊育儿(达人直播)"],
["购买日期", "2026-05-23"],
["", ""],
["结论", "4人均为5月23日同一渠道购买全部已退款且全部已加班主任。仅1人有行课(完成2课时)行课率25%,整体行课参与度很低。"],
]
for row_idx, row_data in enumerate(summary_data, 1):
for col_idx, val in enumerate(row_data, 1):
cell = ws3.cell(row=row_idx, column=col_idx, value=val)
if row_idx == 1:
cell.font = header_font
cell.fill = header_fill
cell.alignment = center_align
else:
cell.font = cell_font
cell.alignment = left_align if col_idx == 2 else center_align
cell.border = thin_border
ws3.column_dimensions['A'].width = 22
ws3.column_dimensions['B'].width = 50
ws3.row_dimensions[1].height = 28
for r in range(2, len(summary_data) + 1):
ws3.row_dimensions[r].height = 22
# 合并结论行
ws3.merge_cells(start_row=11, start_column=1, end_row=11, end_column=2)
ws3.cell(row=11, column=1).alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
ws3.row_dimensions[11].height = 40
output_path = "/root/.openclaw/workspace/output/退款用户行课分析_20260602.xlsx"
wb.save(output_path)
print(f"Excel saved: {output_path}")

156
scripts/fix_all_sheets.py Normal file
View File

@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""一次性修复:
1. 每日线索底部加月度合计行
2. 落单渠道分布重构合计在前先总GMV再拆渠道
3. 销转总览 GMV列格式改为金额
4. 过程数据阶段上课人数改为累计口径 + 5月Tom补全
"""
import json, requests, os, time, sys
CRED_DIR = "/root/.openclaw/credentials/xiaoxi"
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
def get_token():
with open(os.path.join(CRED_DIR, "config.json")) as f:
cfg = json.load(f)
resp = requests.post(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": cfg["apps"][0]["appId"], "app_secret": cfg["apps"][0]["appSecret"]},
timeout=15)
return resp.json()["tenant_access_token"]
token = get_token()
def write_values(sheet_id, range_str, values):
resp = requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"valueRange": {"range": range_str, "values": values}},
params={"valueInputOption": "USER_ENTERED"},
timeout=60)
result = resp.json()
ok = result.get("code") == 0
if not ok:
print(f"{range_str}: code={result.get('code')} msg={result.get('msg')}")
else:
print(f"{range_str}")
return ok
# ============================================================
# 1. 每日线索 底部加月度合计行 (row 96-99)
# ============================================================
print("=== 1. 每日线索 月度合计 ===")
monthly_totals = [
["3月合计", '=SUMIF(A2:A95,"3月*",B2:B95)', '=SUMIF(A2:A95,"3月*",C2:C95)',
'=SUMIF(A2:A95,"3月*",D2:D95)', '=SUMIF(A2:A95,"3月*",E2:E95)', '=SUM(F96:I96)'],
["4月合计", '=SUMIF(A2:A95,"4月*",B2:B95)', '=SUMIF(A2:A95,"4月*",C2:C95)',
'=SUMIF(A2:A95,"4月*",D2:D95)', '=SUMIF(A2:A95,"4月*",E2:E95)', '=SUM(F97:I97)'],
["5月合计", '=SUMIF(A2:A95,"5月*",B2:B95)', '=SUMIF(A2:A95,"5月*",C2:C95)',
'=SUMIF(A2:A95,"5月*",D2:D95)', '=SUMIF(A2:A95,"5月*",E2:E95)', '=SUM(F98:I98)'],
["6月合计", '=SUMIF(A2:A95,"6月*",B2:B95)', '=SUMIF(A2:A95,"6月*",C2:C95)',
'=SUMIF(A2:A95,"6月*",D2:D95)', '=SUMIF(A2:A95,"6月*",E2:E95)', '=SUM(F99:I99)'],
]
write_values("1ANW6c", "1ANW6c!A96:F99", monthly_totals)
# ============================================================
# 2. 落单渠道分布重构
# 新布局: 月份 | 销售 | 总GMV | 总订单 | 销转GMV | 销转订单 | 端内GMV | 端内订单 | 直购GMV | 直购订单
# 合计行放在每个月的第一行
# ============================================================
print("\n=== 2. 落单渠道分布重构 ===")
# 先写新表头
new_header = [["月份", "销售", "总GMV", "总订单", "销转渠道-GMV", "销转渠道-订单",
"端内-GMV", "端内-订单", "直购-GMV", "直购-订单"]]
write_values("3dIXcs", "3dIXcs!A1:J1", new_header)
# 新布局: 每个月 合计 → 小龙 → 吴迪 → Bob → Tom
months_list = ["3月", "4月", "5月", "6月"]
all_sales = {
"3月": ["合计", "小龙", "Bob", "Tom"],
"4月": ["合计", "小龙", "吴迪", "Bob", "Tom"],
"5月": ["合计", "小龙", "吴迪", "Bob", "Tom"],
"6月": ["合计", "小龙", "吴迪", "Bob", "Tom"],
}
row_idx = 2
for month in months_list:
for sales in all_sales[month]:
if sales == "合计":
total_gmv = f'=SUMIFS(\'订单汇总\'!G:G,\'订单汇总\'!B:B,"{month}*")'
total_orders = f'=COUNTIFS(\'订单汇总\'!B:B,"{month}*")'
xz_gmv = f'=SUMIFS(\'订单汇总\'!G:G,\'订单汇总\'!B:B,"{month}*",\'订单汇总\'!J:J,"销转")'
xz_orders = f'=COUNTIFS(\'订单汇总\'!B:B,"{month}*",\'订单汇总\'!J:J,"销转")'
dn_gmv = f'=SUMIFS(\'订单汇总\'!G:G,\'订单汇总\'!B:B,"{month}*",\'订单汇总\'!J:J,"端内")'
dn_orders = f'=COUNTIFS(\'订单汇总\'!B:B,"{month}*",\'订单汇总\'!J:J,"端内")'
zg_gmv = f'=SUMIFS(\'订单汇总\'!G:G,\'订单汇总\'!B:B,"{month}*",\'订单汇总\'!J:J,"直购")'
zg_orders = f'=COUNTIFS(\'订单汇总\'!B:B,"{month}*",\'订单汇总\'!J:J,"直购")'
else:
total_gmv = f'=SUMIFS(\'订单汇总\'!G:G,\'订单汇总\'!A:A,"{sales}",\'订单汇总\'!B:B,"{month}*")'
total_orders = f'=COUNTIFS(\'订单汇总\'!A:A,"{sales}",\'订单汇总\'!B:B,"{month}*")'
xz_gmv = f'=SUMIFS(\'订单汇总\'!G:G,\'订单汇总\'!B:B,"{month}*",\'订单汇总\'!A:A,"{sales}",\'订单汇总\'!J:J,"销转")'
xz_orders = f'=COUNTIFS(\'订单汇总\'!A:A,"{sales}",\'订单汇总\'!B:B,"{month}*",\'订单汇总\'!J:J,"销转")'
dn_gmv = f'=SUMIFS(\'订单汇总\'!G:G,\'订单汇总\'!B:B,"{month}*",\'订单汇总\'!A:A,"{sales}",\'订单汇总\'!J:J,"端内")'
dn_orders = f'=COUNTIFS(\'订单汇总\'!A:A,"{sales}",\'订单汇总\'!B:B,"{month}*",\'订单汇总\'!J:J,"端内")'
zg_gmv = f'=SUMIFS(\'订单汇总\'!G:G,\'订单汇总\'!B:B,"{month}*",\'订单汇总\'!A:A,"{sales}",\'订单汇总\'!J:J,"直购")'
zg_orders = f'=COUNTIFS(\'订单汇总\'!A:A,"{sales}",\'订单汇总\'!B:B,"{month}*",\'订单汇总\'!J:J,"直购")'
row = [[month, sales, total_gmv, total_orders, xz_gmv, xz_orders,
dn_gmv, dn_orders, zg_gmv, zg_orders]]
write_values("3dIXcs", f"3dIXcs!A{row_idx}:J{row_idx}", row)
row_idx += 1
time.sleep(0.3)
# 清除旧数据row 20+ 如果有的话)
print(" 清除旧数据行...")
write_values("3dIXcs", "3dIXcs!A21:J30", [[None]*10]*10)
# ============================================================
# 3. 销转总览 GMV列(F) 格式改为金额
# ============================================================
print("\n=== 3. 销转总览 GMV格式 ===")
# 飞书 API 设置单元格格式
resp = requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/style",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={
"appendStyle": {
"range": "3fR34k!F2:F20",
"style": {
"formatter": "0.00"
}
}
},
timeout=30)
print(f" GMV格式: code={resp.json().get('code')} msg={resp.json().get('msg')}")
# 也设置 G(退款) H(GSV) I(消耗) J(达人GMV) K(佣金) L(总成本) 为金额格式
for col in ["G", "H", "I", "J", "K", "L"]:
resp = requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/style",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={
"appendStyle": {
"range": f"3fR34k!{col}2:{col}20",
"style": {"formatter": "#,##0"}
}
},
timeout=30)
time.sleep(0.2)
# E列(转化率) 百分比格式
resp = requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/style",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={
"appendStyle": {
"range": "3fR34k!E2:E20",
"style": {"formatter": "0.00%"}
}
},
timeout=30)
print(f" 转化率格式: code={resp.json().get('code')}")
print("\n✅ 任务1-3完成")
print("任务4-5(过程数据)需要查询数据库,继续执行...")

396
scripts/fix_process_data.py Normal file
View File

@ -0,0 +1,396 @@
#!/usr/bin/env python3
"""Fix process data: cumulative lesson completion + 5月 Tom row"""
import json, requests, os, time, sys
import pandas as pd
import psycopg2
from collections import defaultdict
CRED_DIR = "/root/.openclaw/credentials/xiaoxi"
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
# Load secrets
secrets = {}
with open("/root/.openclaw/workspace/secrets.env") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
secrets[k.strip()] = v.strip().strip("'")
def get_feishu_token():
with open(os.path.join(CRED_DIR, "config.json")) as f:
cfg = json.load(f)
resp = requests.post(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": cfg["apps"][0]["appId"], "app_secret": cfg["apps"][0]["appSecret"]},
timeout=15)
return resp.json()["tenant_access_token"]
# ============================================================
# Step 1: Load 线索 data (微伴 + 销售表)
# ============================================================
print("=== Step 1: Loading 线索 data ===")
# Load 微伴 data
wb_file = "/root/.openclaw/media/inbound/å¾_ä¼_-å_æ_å_è_æ_æ_å_¼å_º_3---79d6aba9-7cd8-4e99-9b4f-dd981a5ca639.xlsx"
wb_df = pd.read_excel(wb_file)
print(f" 微伴: {len(wb_df)} rows, cols: {list(wb_df.columns)}")
# Load 销售表 from 飞书
token = get_feishu_token()
def read_sheet(sheet_id, range_str):
resp = requests.get(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values/{sheet_id}!{range_str}",
headers={"Authorization": f"Bearer {token}"}, timeout=30)
return resp.json()["data"]["valueRange"]["values"]
# 吴迪 sheet
wudi_data = read_sheet("f975f0", "A1:K700")
# 小龙 sheet
xl_data = read_sheet("qJF4I", "A1:K1200")
# 成都 sheet
cd_data = read_sheet("qJF4J", "A1:K2500")
print(f" 吴迪: {len(wudi_data)} rows, 小龙: {len(xl_data)} rows, 成都: {len(cd_data)} rows")
# Parse 销售表
CS_MAP = {"吴迪": "吴迪", "益达老师": "小龙", "瓦拉英语-Tom老师": "Tom", "瓦拉英语-Bob老师": "Bob"}
def parse_sales_sheet(data, sheet_name):
"""Parse sales sheet, return list of {date, name, phone, sales}"""
results = []
header = data[0]
# Find columns
date_col = name_col = phone_col = cs_col = None
for i, h in enumerate(header):
if h and "日期" in str(h): date_col = i
if h and "昵称" in str(h): name_col = i
if h and "手机" in str(h): phone_col = i
if h and "客服" in str(h): cs_col = i
if cs_col is None:
# Try to find CS column by name
for i, h in enumerate(header):
if h and sheet_name in str(h): cs_col = i
for row in data[1:]:
if not row or len(row) <= max(filter(None, [date_col, name_col, phone_col, cs_col or 0])):
continue
date_val = row[date_col] if date_col is not None and date_col < len(row) else None
name_val = row[name_col] if name_col is not None and name_col < len(row) else None
phone_val = row[phone_col] if phone_col is not None and phone_col < len(row) else None
cs_val = row[cs_col] if cs_col is not None and cs_col < len(row) else None
if not date_val:
continue
# Parse date
if isinstance(date_val, (int, float)):
from datetime import datetime, timedelta
date_str = (datetime(1899, 12, 30) + timedelta(days=int(date_val))).strftime("%Y-%m-%d")
else:
date_str = str(date_val).strip()
# Map CS
sales = None
if cs_val:
cs_str = str(cs_val).strip()
for k, v in CS_MAP.items():
if k in cs_str:
sales = v
break
if not sales:
if sheet_name == "吴迪": sales = "吴迪"
elif sheet_name == "小龙": sales = "小龙"
results.append({
"date": date_str,
"name": str(name_val).strip() if name_val else "",
"phone": str(phone_val).strip() if phone_val else "",
"sales": sales
})
return results
wudi_entries = parse_sales_sheet(wudi_data, "吴迪")
xl_entries = parse_sales_sheet(xl_data, "小龙")
cd_entries = parse_sales_sheet(cd_data, "成都")
print(f" 吴迪 entries: {len(wudi_entries)}, 小龙 entries: {len(xl_entries)}, 成都 entries: {len(cd_entries)}")
# ============================================================
# Step 2: Query database for user lesson completion
# ============================================================
print("\n=== Step 2: Querying database ===")
conn = psycopg2.connect(
host="bj-postgres-16pob4sg.sql.tencentcdb.com",
port=28591,
user="ai_member",
password=secrets["PG_ONLINE_PASSWORD"],
dbname="vala_bi"
)
# Get all users with their phone numbers
cur = conn.cursor()
cur.execute("""
SELECT id, tel, tel_encrypt
FROM bi_vala_app_account
WHERE status = 1 AND deleted_at IS NULL
""")
users = {row[0]: {"tel": row[1], "tel_encrypt": row[2]} for row in cur.fetchall()}
print(f" Users: {len(users)}")
# Get cumulative lesson completion per user
cur.execute("""
SELECT ucp.user_id, MAX(blu.course_lesson) as max_lesson
FROM bi_user_chapter_play_record_0 ucp
JOIN bi_level_unit_lesson blu ON ucp.chapter_id = blu.id
WHERE ucp.play_status = 1
AND blu.course_season = 'S0'
AND blu.course_unit = 'U00'
AND blu.course_level IN ('L1', 'L2')
GROUP BY ucp.user_id
""")
user_lessons = {}
for row in cur.fetchall():
user_lessons[row[0]] = row[1]
print(f" Users with lessons: {len(user_lessons)}")
# Get paid users
cur.execute("""
SELECT DISTINCT account_id
FROM bi_vala_order
WHERE pay_success_date IS NOT NULL
AND order_status IN (3, 4)
AND key_from IN ('app-active-h5-0-0', 'app-sales-bj-qhm-0')
""")
paid_users = set(row[0] for row in cur.fetchall())
print(f" Paid users: {len(paid_users)}")
conn.close()
# ============================================================
# Step 3: Match users to sales via 线索 data
# ============================================================
print("\n=== Step 3: Matching users to sales ===")
# Build phone → sales mapping from 线索 data
# 微伴: phone → CS
wb_phone_to_sales = {}
for _, row in wb_df.iterrows():
phone = str(row.get("手机号", "")).strip()
cs = str(row.get("客服", "")).strip()
if phone and cs:
for k, v in CS_MAP.items():
if k in cs:
wb_phone_to_sales[phone] = v
break
# 销售表: phone → sales
sales_phone_to_sales = {}
for entry in wudi_entries + xl_entries + cd_entries:
if entry["phone"] and entry["sales"]:
sales_phone_to_sales[entry["phone"]] = entry["sales"]
print(f" 微伴 phone→sales: {len(wb_phone_to_sales)}")
print(f" 销售表 phone→sales: {len(sales_phone_to_sales)}")
# Match users to sales via phone
# Try: exact match on tel, then partial match
user_to_sales = {}
unmatched = 0
for uid, uinfo in users.items():
tel = (uinfo.get("tel") or "").strip()
if not tel:
unmatched += 1
continue
# Try 微伴 first
if tel in wb_phone_to_sales:
user_to_sales[uid] = wb_phone_to_sales[tel]
continue
# Try 销售表
if tel in sales_phone_to_sales:
user_to_sales[uid] = sales_phone_to_sales[tel]
continue
# Try partial match
matched = False
for phone, sales in wb_phone_to_sales.items():
if tel in phone or phone in tel:
user_to_sales[uid] = sales
matched = True
break
if not matched:
for phone, sales in sales_phone_to_sales.items():
if tel in phone or phone in tel:
user_to_sales[uid] = sales
matched = True
break
if not matched:
unmatched += 1
print(f" Matched users: {len(user_to_sales)}, unmatched: {unmatched}")
# ============================================================
# Step 4: Aggregate by month and sales
# ============================================================
print("\n=== Step 4: Aggregating ===")
# Get user registration month
conn = psycopg2.connect(
host="bj-postgres-16pob4sg.sql.tencentcdb.com",
port=28591,
user="ai_member",
password=secrets["PG_ONLINE_PASSWORD"],
dbname="vala_bi"
)
cur = conn.cursor()
cur.execute("""
SELECT id, DATE_TRUNC('month', created_at)::date as reg_month
FROM bi_vala_app_account
WHERE status = 1 AND deleted_at IS NULL
AND created_at >= '2026-03-01' AND created_at < '2026-07-01'
""")
user_reg_month = {row[0]: row[1].strftime("%Y-%m") for row in cur.fetchall()}
conn.close()
# For each month+sales, count cumulative lesson completion
LESSON_ORDER = ["L01", "L02", "L03", "L04", "L05"]
MONTHS = ["2026-03", "2026-04", "2026-05", "2026-06"]
SALES_LIST = {
"2026-03": ["合计", "小龙", "Bob", "Tom"],
"2026-04": ["合计", "小龙", "吴迪", "Bob", "Tom"],
"2026-05": ["合计", "小龙", "吴迪", "Bob", "Tom"],
"2026-06": ["合计", "小龙", "吴迪", "Bob", "Tom"],
}
# Build result: {month: {sales: {≥L01: count, ≥L02: count, ..., cv_≥L01: count, ...}}}
results = {}
for month in MONTHS:
results[month] = {}
for sales in SALES_LIST[month]:
results[month][sales] = {f"gte_{l}": 0 for l in LESSON_ORDER}
results[month][sales].update({f"cv_{l}": 0 for l in LESSON_ORDER})
for uid, reg_month in user_reg_month.items():
if reg_month not in results:
continue
sales = user_to_sales.get(uid, None)
max_lesson = user_lessons.get(uid, None)
is_paid = uid in paid_users
# Add to 合计
if max_lesson:
for lesson in LESSON_ORDER:
if max_lesson >= lesson:
results[reg_month]["合计"][f"gte_{lesson}"] += 1
if is_paid:
results[reg_month]["合计"][f"cv_{lesson}"] += 1
# Add to specific sales
if sales and sales in results[reg_month]:
if max_lesson:
for lesson in LESSON_ORDER:
if max_lesson >= lesson:
results[reg_month][sales][f"gte_{lesson}"] += 1
if is_paid:
results[reg_month][sales][f"cv_{lesson}"] += 1
# Print results
for month in MONTHS:
print(f"\n {month}:")
for sales in SALES_LIST[month]:
r = results[month][sales]
print(f" {sales}: ≥L01={r['gte_L01']} ≥L02={r['gte_L02']} ≥L03={r['gte_L03']} ≥L04={r['gte_L04']} ≥L05={r['gte_L05']} | cv: {r['cv_L01']} {r['cv_L02']} {r['cv_L03']} {r['cv_L04']} {r['cv_L05']}")
# ============================================================
# Step 5: Write to process data sheet
# ============================================================
print("\n=== Step 5: Writing to process data ===")
def write_values(sheet_id, range_str, values):
resp = requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"valueRange": {"range": range_str, "values": values}},
params={"valueInputOption": "USER_ENTERED"},
timeout=60)
result = resp.json()
ok = result.get("code") == 0
if not ok:
print(f"{range_str}: code={result.get('code')} msg={result.get('msg')}")
else:
print(f"{range_str}")
return ok
# Column mapping for process data:
# H=首课人数 I=首课率 J=一节课转化人数 K=一节课转化率
# L=二次课人数 M=二次课率 N=二节课转化人数 O=二节课转化率
# P=三次课人数 Q=三次课率 R=三节课转化人数 S=三节课转化率
# T=四次课人数 U=四次课率 V=四节课转化人数 W=四节课转化率
# X=五次课人数 Y=五次课率 Z=五节课转化人数 AA=五节课转化率
# Row mapping: 2=3月合计, 3=3月小龙, 4=3月Bob, 5=3月Tom
# 6=4月合计, 7=4月小龙, 8=4月吴迪, 9=4月Bob, 10=4月Tom
# 11=5月合计, 12=5月小龙, 13=5月吴迪, 14=5月Bob, 15=5月Tom
# 16=6月合计, 17=6月小龙, 18=6月吴迪, 19=6月Bob, 20=6月Tom
row_map = [
(2, "2026-03", "合计"), (3, "2026-03", "小龙"), (4, "2026-03", "Bob"), (5, "2026-03", "Tom"),
(6, "2026-04", "合计"), (7, "2026-04", "小龙"), (8, "2026-04", "吴迪"), (9, "2026-04", "Bob"), (10, "2026-04", "Tom"),
(11, "2026-05", "合计"), (12, "2026-05", "小龙"), (13, "2026-05", "吴迪"), (14, "2026-05", "Bob"), (15, "2026-05", "Tom"),
(16, "2026-06", "合计"), (17, "2026-06", "小龙"), (18, "2026-06", "吴迪"), (19, "2026-06", "Bob"), (20, "2026-06", "Tom"),
]
# Write cumulative lesson data: H, J, L, N, P, R, T, V, X, Z
# H=首课人数(≥L01), J=一节课转化人数(≥L01+paid)
# L=二次课人数(≥L02), N=二节课转化人数(≥L02+paid)
# P=三次课人数(≥L03), R=三节课转化人数(≥L03+paid)
# T=四次课人数(≥L04), V=四节课转化人数(≥L04+paid)
# X=五次课人数(≥L05), Z=五节课转化人数(≥L05+paid)
for row_num, month, sales in row_map:
r = results[month][sales]
# Build the row data for columns H through AA
# We only write the count columns (H, J, L, N, P, R, T, V, X, Z)
# The rate columns (I, K, M, O, Q, S, U, W, Y, AA) are formulas
row_data = [
[r["gte_L01"]], # H: 首课人数
[f'=IFERROR(H{row_num}/C{row_num},"")'], # I: 首课率
[r["cv_L01"]], # J: 一节课转化人数
[f'=IFERROR(J{row_num}/C{row_num},"")'], # K: 一节课转化率
[r["gte_L02"]], # L: 二次课人数
[f'=IFERROR(L{row_num}/C{row_num},"")'], # M: 二次课率
[r["cv_L02"]], # N: 二节课转化人数
[f'=IFERROR(N{row_num}/C{row_num},"")'], # O: 二节课转化率
[r["gte_L03"]], # P: 三次课人数
[f'=IFERROR(P{row_num}/C{row_num},"")'], # Q: 三次课率
[r["cv_L03"]], # R: 三节课转化人数
[f'=IFERROR(R{row_num}/C{row_num},"")'], # S: 三节课转化率
[r["gte_L04"]], # T: 四次课人数
[f'=IFERROR(T{row_num}/C{row_num},"")'], # U: 四次课率
[r["cv_L04"]], # V: 四节课转化人数
[f'=IFERROR(V{row_num}/C{row_num},"")'], # W: 四节课转化率
[r["gte_L05"]], # X: 五次课人数
[f'=IFERROR(X{row_num}/C{row_num},"")'], # Y: 五次课率
[r["cv_L05"]], # Z: 五节课转化人数
[f'=IFERROR(Z{row_num}/C{row_num},"")'], # AA: 五节课转化率
]
# Write H through AA
col_letters = "HIJKLMNOPQRSTUVWXYZAA"
for i, (col, data) in enumerate(zip(
["H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","AA"],
row_data
)):
write_values("3aOvV6", f"3aOvV6!{col}{row_num}:{col}{row_num}", [data])
time.sleep(0.15)
print("\n✅ Process data updated!")

View File

@ -0,0 +1,314 @@
#!/usr/bin/env python3
"""Fix process data: cumulative lesson completion by sales, using sales sheet phone→user matching"""
import json, requests, os, time, re
import psycopg2
from collections import defaultdict
from datetime import datetime, timedelta
CRED_DIR = "/root/.openclaw/credentials/xiaoxi"
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
secrets = {}
with open("/root/.openclaw/workspace/secrets.env") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
secrets[k.strip()] = v.strip().strip("'")
def get_token():
with open(os.path.join(CRED_DIR, "config.json")) as f: cfg = json.load(f)
resp = requests.post("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": cfg["apps"][0]["appId"], "app_secret": cfg["apps"][0]["appSecret"]}, timeout=15)
return resp.json()["tenant_access_token"]
token = get_token()
def read_sheet(sheet_id, range_str):
resp = requests.get(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values/{sheet_id}!{range_str}",
headers={"Authorization": f"Bearer {token}"}, timeout=30)
return resp.json()["data"]["valueRange"]["values"]
def write_values(sheet_id, range_str, values):
resp = requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"valueRange": {"range": range_str, "values": values}},
params={"valueInputOption": "USER_ENTERED"},
timeout=60)
result = resp.json()
ok = result.get("code") == 0
if not ok:
print(f"{range_str}: code={result.get('code')} msg={result.get('msg')}")
else:
print(f"{range_str}")
return ok
# ============================================================
# Step 1: Parse sales sheets → phone → sales mapping
# ============================================================
print("=== Step 1: Parse sales sheets ===")
CS_MAP = {"吴迪": "吴迪", "小龙": "小龙", "Tom": "Tom", "Bob": "Bob"}
def parse_sales_sheet(data):
"""Return list of {sales, phone, user_id, date}"""
results = []
for row in data[2:]: # skip header rows
if not row or len(row) < 5:
continue
sales_raw = str(row[0]).strip() if row[0] else ""
phone_raw = row[4]
user_id_raw = row[7] if len(row) > 7 else None
# Map sales
sales = None
for k, v in CS_MAP.items():
if k in sales_raw:
sales = v
break
if not sales:
continue
# Parse phone
phone = ""
if phone_raw:
if isinstance(phone_raw, (int, float)):
phone = str(int(phone_raw))
else:
phone = str(phone_raw).strip()
# Parse user_id
user_id = ""
if user_id_raw:
if isinstance(user_id_raw, (int, float)):
user_id = str(int(user_id_raw))
else:
user_id = str(user_id_raw).strip()
results.append({"sales": sales, "phone": phone, "user_id": user_id})
return results
wudi = read_sheet("f975f0", "A1:K700")
xl = read_sheet("qJF4I", "A1:K1200")
cd = read_sheet("qJF4J", "A1:K2500")
wudi_entries = parse_sales_sheet(wudi)
xl_entries = parse_sales_sheet(xl)
cd_entries = parse_sales_sheet(cd)
all_entries = wudi_entries + xl_entries + cd_entries
print(f" 吴迪: {len(wudi_entries)}, 小龙: {len(xl_entries)}, 成都: {len(cd_entries)}, 总计: {len(all_entries)}")
# Build phone→sales mapping (dedup: first match wins)
phone_to_sales = {}
for e in all_entries:
if e["phone"] and e["phone"] not in phone_to_sales:
phone_to_sales[e["phone"]] = e["sales"]
# Also user_id→sales
uid_to_sales = {}
for e in all_entries:
if e["user_id"] and e["user_id"].isdigit() and e["user_id"] not in uid_to_sales:
uid_to_sales[e["user_id"]] = e["sales"]
print(f" phone→sales: {len(phone_to_sales)}, uid→sales: {len(uid_to_sales)}")
# ============================================================
# Step 2: Query database
# ============================================================
print("\n=== Step 2: Query database ===")
conn = psycopg2.connect(
host="bj-postgres-16pob4sg.sql.tencentcdb.com", port=28591,
user="ai_member", password=secrets["PG_ONLINE_PASSWORD"], dbname="vala_bi")
cur = conn.cursor()
# Users with phone
cur.execute("SELECT id, tel FROM bi_vala_app_account WHERE status = 1 AND deleted_at IS NULL")
user_phone = {}
for row in cur.fetchall():
uid = row[0]
tel = (row[1] or "").strip()
if tel:
user_phone[uid] = tel
# User reg month
cur.execute("""
SELECT id, DATE_TRUNC('month', created_at)::date as reg_month
FROM bi_vala_app_account WHERE status = 1 AND deleted_at IS NULL
AND created_at >= '2026-03-01' AND created_at < '2026-07-01'
""")
user_reg = {row[0]: row[1].strftime("%Y-%m") for row in cur.fetchall()}
# Max lesson per user
cur.execute("""
SELECT ucp.user_id, MAX(blu.course_lesson) as max_lesson
FROM bi_user_chapter_play_record_0 ucp
JOIN bi_level_unit_lesson blu ON ucp.chapter_id = blu.id
WHERE ucp.play_status = 1
AND blu.course_season = 'S0' AND blu.course_unit = 'U00'
AND blu.course_level IN ('L1', 'L2')
GROUP BY ucp.user_id
""")
user_lesson = {row[0]: row[1] for row in cur.fetchall()}
# Paid users
cur.execute("""
SELECT DISTINCT account_id FROM bi_vala_order
WHERE pay_success_date IS NOT NULL AND order_status IN (3, 4)
AND key_from IN ('app-active-h5-0-0', 'app-sales-bj-qhm-0')
""")
paid = set(row[0] for row in cur.fetchall())
conn.close()
print(f" Users with phone: {len(user_phone)}, reg: {len(user_reg)}, lessons: {len(user_lesson)}, paid: {len(paid)}")
# ============================================================
# Step 3: Match users to sales
# ============================================================
print("\n=== Step 3: Match users to sales ===")
user_to_sales = {}
matched_by_uid = 0
matched_by_phone = 0
for uid in user_reg:
# Try user_id match first
if str(uid) in uid_to_sales:
user_to_sales[uid] = uid_to_sales[str(uid)]
matched_by_uid += 1
continue
# Try phone match
tel = user_phone.get(uid, "")
if tel and tel in phone_to_sales:
user_to_sales[uid] = phone_to_sales[tel]
matched_by_phone += 1
continue
# Try partial phone match
if tel:
for phone, sales in phone_to_sales.items():
if tel in phone or phone in tel:
user_to_sales[uid] = sales
matched_by_phone += 1
break
print(f" Matched: uid={matched_by_uid}, phone={matched_by_phone}, total={len(user_to_sales)}")
# ============================================================
# Step 4: Aggregate
# ============================================================
print("\n=== Step 4: Aggregate ===")
LESSONS = ["L01", "L02", "L03", "L04", "L05"]
MONTHS = ["2026-03", "2026-04", "2026-05", "2026-06"]
SALES_MAP = {
"2026-03": ["合计", "小龙", "Bob", "Tom"],
"2026-04": ["合计", "小龙", "吴迪", "Bob", "Tom"],
"2026-05": ["合计", "小龙", "吴迪", "Bob", "Tom"],
"2026-06": ["合计", "小龙", "吴迪", "Bob", "Tom"],
}
results = {}
for m in MONTHS:
results[m] = {}
for s in SALES_MAP[m]:
results[m][s] = {f"gte_{l}": 0 for l in LESSONS}
results[m][s].update({f"cv_{l}": 0 for l in LESSONS})
for uid, reg_month in user_reg.items():
if reg_month not in results:
continue
sales = user_to_sales.get(uid)
max_l = user_lesson.get(uid)
is_paid = uid in paid
# 合计
if max_l:
for l in LESSONS:
if max_l >= l:
results[reg_month]["合计"][f"gte_{l}"] += 1
if is_paid:
results[reg_month]["合计"][f"cv_{l}"] += 1
if sales and sales in results[reg_month]:
if max_l:
for l in LESSONS:
if max_l >= l:
results[reg_month][sales][f"gte_{l}"] += 1
if is_paid:
results[reg_month][sales][f"cv_{l}"] += 1
for m in MONTHS:
print(f"\n {m}:")
for s in SALES_MAP[m]:
r = results[m][s]
print(f" {s}: L01={r['gte_L01']} L02={r['gte_L02']} L03={r['gte_L03']} L04={r['gte_L04']} L05={r['gte_L05']} | cv: {r['cv_L01']} {r['cv_L02']} {r['cv_L03']} {r['cv_L04']} {r['cv_L05']}")
# ============================================================
# Step 5: Write to process data
# ============================================================
print("\n=== Step 5: Write to process data ===")
row_map = [
(2, "2026-03", "合计"), (3, "2026-03", "小龙"), (4, "2026-03", "Bob"), (5, "2026-03", "Tom"),
(6, "2026-04", "合计"), (7, "2026-04", "小龙"), (8, "2026-04", "吴迪"), (9, "2026-04", "Bob"), (10, "2026-04", "Tom"),
(11, "2026-05", "合计"), (12, "2026-05", "小龙"), (13, "2026-05", "吴迪"), (14, "2026-05", "Bob"), (15, "2026-05", "Tom"),
(16, "2026-06", "合计"), (17, "2026-06", "小龙"), (18, "2026-06", "吴迪"), (19, "2026-06", "Bob"), (20, "2026-06", "Tom"),
]
# Column mapping:
# H=首课人数(≥L01) I=首课率 J=一节课转化人数(≥L01+paid) K=一节课转化率
# L=二次课人数(≥L02) M=二次课率 N=二节课转化人数(≥L02+paid) O=二节课转化率
# P=三次课人数(≥L03) Q=三次课率 R=三节课转化人数(≥L03+paid) S=三节课转化率
# T=四次课人数(≥L04) U=四次课率 V=四节课转化人数(≥L04+paid) W=四节课转化率
# X=五次课人数(≥L05) Y=五次课率 Z=五节课转化人数(≥L05+paid) AA=五节课转化率
col_pairs = [
("H", "I", "gte_L01", "cv_L01"), # 首课
("L", "M", "gte_L02", "cv_L02"), # 二次课
("P", "Q", "gte_L03", "cv_L03"), # 三次课
("T", "U", "gte_L04", "cv_L04"), # 四次课
("X", "Y", "gte_L05", "cv_L05"), # 五次课
]
# Also need J/K (一节课转化), N/O (二节课转化), R/S, V/W, Z/AA
cv_col_pairs = [
("J", "K", "cv_L01"),
("N", "O", "cv_L02"),
("R", "S", "cv_L03"),
("V", "W", "cv_L04"),
("Z", "AA", "cv_L05"),
]
for row_num, month, sales in row_map:
r = results[month][sales]
for count_col, rate_col, key, cv_key in col_pairs:
count = r[key]
cv_count = r[cv_key]
# Write count
write_values("3aOvV6", f"3aOvV6!{count_col}{row_num}:{count_col}{row_num}", [[count]])
time.sleep(0.1)
# Write rate formula
write_values("3aOvV6", f"3aOvV6!{rate_col}{row_num}:{rate_col}{row_num}",
[[f'=IFERROR({count_col}{row_num}/C{row_num},"")']])
time.sleep(0.1)
# Write cv columns
for cv_col, cv_rate_col, cv_key in cv_col_pairs:
cv_count = r[cv_key]
write_values("3aOvV6", f"3aOvV6!{cv_col}{row_num}:{cv_col}{row_num}", [[cv_count]])
time.sleep(0.1)
write_values("3aOvV6", f"3aOvV6!{cv_rate_col}{row_num}:{cv_rate_col}{row_num}",
[[f'=IFERROR({cv_col}{row_num}/C{row_num},"")']])
time.sleep(0.1)
print("\n✅ Process data updated!")

View File

@ -0,0 +1,635 @@
#!/usr/bin/env python3
"""
全覆盖刷新 销售表行课状态 + 过程数据累计人数 + 订单汇总
每次执行时对所有已有 user_id 的行做全量覆盖不依赖待查询标记
覆盖范围
1. 销售表 (f975f0/qJF4I/qJF4J) D/I/J/K/L/M/N/O/P/Q/R/S/T/U/V
2. 过程数据 (3aOvV6) H/L/P/T/X (上课人数) + J/N/R/V/Z (转化人数)
3. 订单汇总 (2smjwA) 新增订单追加写入(销售归属+微信昵称+金额)去重
"""
import json, requests, os, sys, psycopg2, time
from datetime import datetime
from collections import defaultdict
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPTS_DIR)
from phone_encrypt import encrypt_phone
# ── 配置 ──
PG_HOST = "bj-postgres-16pob4sg.sql.tencentcdb.com"
PG_PORT = 28591
PG_USER = "ai_member"
PG_DB = "vala_bi"
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
CRED_DIR = "/root/.openclaw/credentials/xiaoxi"
SALES_SHEETS = [
("f975f0", "吴迪", "A1:V700"),
("qJF4I", "小龙", "A1:V1200"),
("qJF4J", "成都", "A1:V2500"),
]
PROCESS_SHEET = "3aOvV6"
ORDER_SHEET = "2smjwA"
CS_MAP = {"吴迪": "吴迪", "小龙": "小龙", "Tom": "Tom", "Bob": "Bob"}
GOODS_NAMES = {
57: "瓦拉英语level1·单季", 60: "瓦拉英语level1", 63: "瓦拉英语level1·单季",
31: "瓦拉英语年包", 32: "瓦拉英语单季度包", 33: "瓦拉英语level2", 54: "瓦拉英语季度包",
61: "瓦拉英语level1+2",
}
LOG_FILE = "/var/log/xiaoxi_full_refresh.log"
def log(msg):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
line = f"[{ts}] {msg}"
print(line)
with open(LOG_FILE, "a") as f:
f.write(line + "\n")
def get_pg_password():
with open(os.path.join(SCRIPTS_DIR, "..", "secrets.env")) as f:
for line in f:
if line.startswith("PG_ONLINE_PASSWORD="):
return line.strip().split("=", 1)[1].strip("'\"")
def get_fs_token():
with open(os.path.join(CRED_DIR, "config.json")) as f:
cfg = json.load(f)
resp = requests.post(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": cfg["apps"][0]["appId"], "app_secret": cfg["apps"][0]["appSecret"]},
timeout=15
)
return resp.json()["tenant_access_token"]
def read_sheet(token, sheet_id, range_str=None):
url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values/{sheet_id}"
if range_str:
url += f"!{range_str}"
resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=30)
data = resp.json()
if data.get("code") != 0:
raise RuntimeError(f"读取失败: {data}")
return data["data"]["valueRange"]["values"]
def put_values(token, sheet_id, range_str, values):
url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values"
body = {"valueRange": {"range": f"{sheet_id}!{range_str}", "values": values}}
resp = requests.put(url, headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}, json=body, timeout=30)
r = resp.json()
if r.get("code") != 0:
log(f"{range_str}: {r.get('code')} {r.get('msg')}")
return False
return True
def batch_in(cur, sql_tpl, params, chunk=500):
results = []
for i in range(0, len(params), chunk):
batch = params[i:i+chunk]
ph = ",".join(["%s"] * len(batch))
cur.execute(sql_tpl % ph, batch)
results.extend(cur.fetchall())
return results
# ── Step 1: 解析销售表 ──
def parse_sales_sheets(token):
"""返回 {sheet_id: [(row_idx, sales_name, nickname, date, phone, user_id), ...]}"""
all_data = {}
for sid, sname, rng in SALES_SHEETS:
rows = read_sheet(token, sid, rng)
entries = []
for idx, row in enumerate(rows[2:], start=3): # row 1=header, row 2=sub-header
if not row: continue
sr = str(row[0]).strip() if row[0] else ""
sales = None
for k, v in CS_MAP.items():
if k in sr: sales = v; break
if not sales: continue
nickname = str(row[1]).strip() if len(row) > 1 and row[1] else ""
date_str = str(row[2]).strip() if len(row) > 2 and row[2] else ""
phone = ""
if len(row) > 4 and row[4]:
try:
phone = str(int(float(row[4])))
except: pass
uid = ""
if len(row) > 7 and row[7]:
try:
uid = str(int(float(row[7])))
except: pass
entries.append((idx, sales, nickname, date_str, phone, uid))
all_data[sid] = entries
log(f" {sname}: {len(entries)} rows, {sum(1 for e in entries if e[5] and e[5].isdigit() and int(e[5])>0)} with uid")
return all_data
# ── Step 2: 数据库批量查询 ──
def query_all_db(conn, all_entries):
"""查询所有需要的数据,返回 uid -> info dict"""
# 收集所有有效 user_id
uid_set = set()
uid_to_sales = {}
uid_to_nickname = {}
uid_to_date = {}
for sid, entries in all_entries.items():
for idx, sales, nick, date_str, phone, uid in entries:
if uid and uid.isdigit() and int(uid) > 0:
aid = int(uid)
uid_set.add(aid)
uid_to_sales[aid] = sales
uid_to_nickname[aid] = nick
uid_to_date[aid] = date_str
uid_list = list(uid_set)
log(f" 有效 user_id: {len(uid_list)}")
cur = conn.cursor()
info = {uid: {
"sales": uid_to_sales.get(uid, ""),
"nickname": uid_to_nickname.get(uid, ""),
"date": uid_to_date.get(uid, ""),
"reg_date": "", "download_channel": "",
"has_order": "", "order_date": "", "order_channel": "", "product": "",
"gmv": 0, "refund": 0, "gsv": 0,
"activation": "", "lesson_progress": "", "lesson_time": "", "lesson_minutes": 0,
"max_lesson": 0, "is_paid": False,
} for uid in uid_set}
# 2a. 注册信息
log(" 查询注册信息...")
reg_info = batch_in(cur,
"SELECT id, created_at, download_channel FROM bi_vala_app_account WHERE id IN (%s) AND status=1 AND deleted_at IS NULL",
uid_list
)
for aid, created_at, dc in reg_info:
if aid in info:
info[aid]["reg_date"] = created_at.strftime("%Y-%m-%d") if created_at else ""
info[aid]["download_channel"] = dc or ""
# 2b. 订单信息
log(" 查询订单信息...")
orders = batch_in(cur,
"SELECT account_id, trade_no, pay_success_date, key_from, goods_id, pay_amount_int, order_status FROM bi_vala_order WHERE account_id IN (%s) AND pay_success_date IS NOT NULL AND order_status IN (3,4) ORDER BY pay_success_date DESC",
uid_list
)
# 按用户聚合
user_orders = defaultdict(list)
for o in orders:
user_orders[o[0]].append(o)
# 退款
trade_nos = [o[1] for o in orders if o[1]]
refund_map = {}
if trade_nos:
refunds = batch_in(cur,
"SELECT trade_no, refund_amount_int FROM bi_refund_order WHERE trade_no IN (%s) AND status=3",
trade_nos
)
for tn, amt in refunds:
refund_map[tn] = amt
for aid, olist in user_orders.items():
if aid not in info: continue
info[aid]["has_order"] = ""
# 最新订单
latest = olist[0]
info[aid]["order_date"] = latest[2].strftime("%Y-%m-%d") if latest[2] else ""
info[aid]["order_channel"] = latest[3] or ""
info[aid]["product"] = GOODS_NAMES.get(latest[4], f"商品{latest[4]}")
# 汇总
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
info[aid]["refund"] = total_refund
info[aid]["gsv"] = total_gmv - total_refund
info[aid]["is_paid"] = True
# 2c. 激活课程
log(" 查询激活课程...")
try:
activations = batch_in(cur,
"SELECT t.account_id, t.season_package_level FROM bi_vala_seasonal_ticket t WHERE t.account_id IN (%s) AND t.status=1 AND t.deleted_at IS NULL AND t.season_package_level IN ('A1','A2')",
uid_list
)
for aid, lvl in activations:
if aid in info:
info[aid]["activation"] = lvl
except Exception as e:
log(f" 激活查询异常: {e}")
# 2d. 角色信息
log(" 查询角色信息...")
char_info = batch_in(cur,
"SELECT account_id, id FROM bi_vala_app_character WHERE account_id IN (%s) AND deleted_at IS NULL",
uid_list
)
account_chars = defaultdict(list)
char_to_account = {}
for aid, cid in char_info:
account_chars[aid].append(cid)
char_to_account[cid] = aid
char_ids = list(char_to_account.keys())
log(f" 角色数: {len(char_ids)}")
# 2e. 课程映射
cur.execute("SELECT id, course_level, course_season, course_unit, course_lesson FROM bi_level_unit_lesson")
chapter_map = {}
for ch_id, cl, cs, cu, cl2 in cur.fetchall():
chapter_map[ch_id] = (cl or "", cs or "", cu or "", cl2 or "")
# 2f. 课时完成记录
log(" 查询课时完成记录...")
char_plays = defaultdict(lambda: {"latest_time": None, "latest_chapter": None, "max_lesson_idx": 0, "total_ms": 0})
for tbl_idx in range(8):
table = f"bi_user_chapter_play_record_{tbl_idx}"
try:
cur.execute(
f"SELECT user_id, chapter_id, created_at FROM {table} WHERE play_status=1 AND deleted_at IS NULL AND user_id = ANY(%s)",
(char_ids,)
)
for uid, ch_id, created_at in cur.fetchall():
ch_data = chapter_map.get(ch_id)
if not ch_data: continue
rec = char_plays[uid]
if rec["latest_time"] is None or created_at > rec["latest_time"]:
rec["latest_time"] = created_at
rec["latest_chapter"] = (ch_id, ch_data)
# 计算 lesson index 用于 max_lesson
cl, cs, cu, cl2 = ch_data
try:
u_num = int(cu[1:]) if cu and len(cu) >= 2 else 0
l_num = int(cl2[1:]) if cl2 and len(cl2) >= 2 else 0
lesson_idx = u_num * 5 + l_num
if lesson_idx > rec["max_lesson_idx"]:
rec["max_lesson_idx"] = lesson_idx
except: pass
except Exception as e:
log(f" 警告 {table}: {e}")
# 2g. 学习总耗时
log(" 查询学习耗时...")
for tbl_idx in range(8):
table = f"bi_user_component_play_record_{tbl_idx}"
try:
cur.execute(
f"SELECT user_id, SUM(COALESCE(interval_time,0)) FROM {table} WHERE user_id = ANY(%s) AND deleted_at IS NULL GROUP BY user_id",
(char_ids,)
)
for uid, total_ms in cur.fetchall():
if uid in char_plays:
char_plays[uid]["total_ms"] += (total_ms or 0)
except Exception as e:
log(f" 警告 {table}: {e}")
cur.close()
# 汇总到 account 级别
for aid in uid_set:
chars = account_chars.get(aid, [])
best_time = None
best_ch = None
max_lesson = 0
total_ms = 0
for cid in chars:
play = char_plays.get(cid)
if not play: continue
if play["latest_chapter"]:
if best_time is None or play["latest_time"] > best_time:
best_time = play["latest_time"]
best_ch = play["latest_chapter"]
if play["max_lesson_idx"] > max_lesson:
max_lesson = play["max_lesson_idx"]
total_ms += play["total_ms"]
info[aid]["max_lesson"] = max_lesson
info[aid]["lesson_minutes"] = round(total_ms / 60000, 1)
if info[aid]["lesson_minutes"] == int(info[aid]["lesson_minutes"]):
info[aid]["lesson_minutes"] = int(info[aid]["lesson_minutes"])
if best_ch:
ch_id, (cl, cs, cu, cl2) = best_ch
info[aid]["lesson_progress"] = f"{cl}-{cs}-{cu}-{cl2}"
info[aid]["lesson_time"] = best_time.strftime("%Y-%m-%d") if best_time else ""
log(f" 数据库查询完成")
return info
# ── Step 3: 写入销售表 ──
def write_sales_sheets(token, all_entries, db_info):
"""全覆盖写入销售表的自动列"""
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
for sid, sname, _ in SALES_SHEETS:
entries = all_entries[sid]
log(f" 写入 {sname} ({sid})...")
# 按行分组(连续行合并写入)
groups = []
cur_grp = []
for idx, sales, nick, date_str, phone, uid in entries:
if not cur_grp or idx == cur_grp[-1]["row"] + 1:
cur_grp.append({"row": idx, "uid": uid})
else:
groups.append(cur_grp)
cur_grp = [{"row": idx, "uid": uid}]
if cur_grp:
groups.append(cur_grp)
for g in groups:
sr, er = g[0]["row"], g[-1]["row"]
# 构建每列的值
d_vals = [] # 体验节数
i_vals = [] # 注册日期
j_vals = [] # 下载渠道
k_vals = [] # 是否下单
l_vals = [] # 下单日期
m_vals = [] # 成交渠道
n_vals = [] # 产品
o_vals = [] # GMV
p_vals = [] # 退款
q_vals = [] # GSV
r_vals = [] # 激活课程
s_vals = [] # 行课进度
t_vals = [] # 最近行课
u_vals = [] # 学习时长
v_vals = [] # 更新时间
for item in g:
uid = item["uid"]
aid = int(uid) if uid and uid.isdigit() and int(uid) > 0 else 0
if aid > 0 and aid in db_info:
di = db_info[aid]
# 体验节数:用 max_lesson 换算
trial_count = di["max_lesson"]
d_vals.append([trial_count if trial_count > 0 else ""])
i_vals.append([di["reg_date"]])
j_vals.append([di["download_channel"]])
k_vals.append([di["has_order"]])
l_vals.append([di["order_date"]])
m_vals.append([di["order_channel"]])
n_vals.append([di["product"] if di["has_order"] == "" else ""])
o_vals.append([di["gmv"] if di["gmv"] > 0 else 0])
p_vals.append([di["refund"] if di["refund"] > 0 else 0])
q_vals.append([di["gsv"] if di["gsv"] > 0 else 0])
# 激活课程
act = di["activation"]
if act:
r_vals.append([f"{act}体验课" if act in ("A1","A2") else act])
else:
r_vals.append([""])
s_vals.append([di["lesson_progress"]])
t_vals.append([di["lesson_time"]])
u_vals.append([di["lesson_minutes"]])
else:
# 无有效 user_id留空
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]:
arr.append([""])
v_vals.append([now_str])
# 写入各列
cols = [
("D", d_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),
]
for col_letter, vals in cols:
put_values(token, sid, f"{col_letter}{sr}:{col_letter}{er}", vals)
time.sleep(0.1)
log(f" {sname}: {len(entries)} rows done")
# ── Step 4: 过程数据 ──
def write_process_data(token, all_entries, db_info):
"""全量重算过程数据"""
log(" 写入过程数据...")
# 按月份+销售聚合
# 先收集所有 user 的 month+sales
user_month_sales = {}
for sid, entries in all_entries.items():
for idx, sales, nick, date_str, phone, uid in entries:
if not uid or not uid.isdigit() or int(uid) <= 0: continue
aid = int(uid)
if aid not in db_info: continue
# 从 date_str 提取月份
month = ""
if date_str:
m = date_str.replace("", "").replace("", "").strip()
# 尝试解析
for prefix in ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]:
if m.startswith(prefix):
month = prefix + ""
break
if not month:
# 从注册日期推断
reg = db_info[aid].get("reg_date", "")
if reg and len(reg) >= 7:
m = int(reg[5:7])
month = f"{m}"
if not month:
month = "未知"
key = (month, sales)
if key not in user_month_sales:
user_month_sales[key] = []
user_month_sales[key].append(aid)
# 计算每个 month+sales 的累计指标
process_rows = {}
for (month, sales), uids in user_month_sales.items():
total = len(uids)
l1 = sum(1 for uid in uids if db_info[uid]["max_lesson"] >= 1)
l2 = sum(1 for uid in uids if db_info[uid]["max_lesson"] >= 2)
l3 = sum(1 for uid in uids if db_info[uid]["max_lesson"] >= 3)
l4 = sum(1 for uid in uids if db_info[uid]["max_lesson"] >= 4)
l5 = sum(1 for uid in uids if db_info[uid]["max_lesson"] >= 5)
# 转化:付费 + 达到对应 lesson
c1 = sum(1 for uid in uids if db_info[uid]["is_paid"] and db_info[uid]["max_lesson"] >= 1)
c2 = sum(1 for uid in uids if db_info[uid]["is_paid"] and db_info[uid]["max_lesson"] >= 2)
c3 = sum(1 for uid in uids if db_info[uid]["is_paid"] and db_info[uid]["max_lesson"] >= 3)
c4 = sum(1 for uid in uids if db_info[uid]["is_paid"] and db_info[uid]["max_lesson"] >= 4)
c5 = sum(1 for uid in uids if db_info[uid]["is_paid"] and db_info[uid]["max_lesson"] >= 5)
process_rows[(month, sales)] = {
"total": total, "l1": l1, "l2": l2, "l3": l3, "l4": l4, "l5": l5,
"c1": c1, "c2": c2, "c3": c3, "c4": c4, "c5": c5,
}
# 读取现有过程数据找到行映射
rows = read_sheet(token, PROCESS_SHEET, "A1:AA25")
row_map = {} # (month, sales) -> row number
for i, row in enumerate(rows[1:], start=2):
if not row: continue
m = str(row[0]).strip() if row[0] else ""
s = str(row[1]).strip() if row[1] else ""
if m and s:
row_map[(m, s)] = i
# 写入(跳过合计行,合计由用户的 AI 写 SUM 公式)
# 按列批量写入,只写数据列(H/J/L/N/P/R/T/V/X/Z),不动公式列(I/K/M/O/Q/S/U/W/Y)
sales_order = ["小龙", "吴迪", "Bob", "Tom"]
months_order = ["3月", "4月", "5月", "6月"]
# 列映射: (列字母, 数据key)
col_keys = [
("H", "l1"), ("J", "c1"), ("L", "l2"), ("N", "c2"), ("P", "l3"),
("R", "c3"), ("T", "l4"), ("V", "c4"), ("X", "l5"), ("Z", "c5"),
]
for col, key in col_keys:
col_vals = []
for month in months_order:
for sales in sales_order:
mk = (month, sales)
if mk not in row_map: continue
r = row_map[mk]
data = process_rows.get(mk, {})
val = data.get(key, 0)
col_vals.append((r, val))
if not col_vals: continue
# 按连续行分组
col_vals.sort()
groups = []
cur_start = col_vals[0][0]
cur_vals = [col_vals[0][1]]
for i in range(1, len(col_vals)):
if col_vals[i][0] == cur_start + len(cur_vals):
cur_vals.append(col_vals[i][1])
else:
groups.append((cur_start, cur_vals))
cur_start = col_vals[i][0]
cur_vals = [col_vals[i][1]]
groups.append((cur_start, cur_vals))
for sr, vals in groups:
er = sr + len(vals) - 1
put_values(token, PROCESS_SHEET, f"{col}{sr}:{col}{er}", [[v] for v in vals])
time.sleep(0.15)
log(f" 过程数据写入完成")
# ── Step 5: 订单汇总 ──
def write_order_summary(token, all_entries, db_info):
"""追加新订单到订单汇总 sheet按(销售归属+微信昵称+金额)去重"""
log(" 写入订单汇总...")
# 读取现有订单行 (1-400) 构建去重 key
try:
existing = read_sheet(token, ORDER_SHEET, "A1:K400")
except:
existing = []
existing_keys = set()
for i, row in enumerate(existing[1:], start=2):
if not row or not row[0]: continue
sales = str(row[0]).strip() if row[0] else ""
nickname = str(row[3]).strip() if len(row) > 3 and row[3] else ""
amount = ""
if len(row) > 6 and row[6]:
try: amount = str(float(row[6]))
except: amount = str(row[6]).strip()
if sales and nickname and amount:
existing_keys.add((sales, nickname, amount))
# 从 db_info 构建订单行
def map_channel(key_from):
if key_from in ("app-active-h5-0-0", "app-sales-bj-qhm-0"): return "端内"
if key_from.startswith("sales-adp"): return "销转"
if key_from.startswith("newmedia-daren"): return "达人"
if key_from == "newmedia-dianpu-xhs-0-0": return "直购"
return "直购"
order_rows = []
seen_new = set()
for aid, di in db_info.items():
if not di.get("is_paid"): continue
sales = di.get("sales", "")
nickname = di.get("nickname", "")
date_str = di.get("date", "")
order_date = di.get("order_date", "")
key_from = di.get("order_channel", "")
product = di.get("product", "")
amount = di.get("gmv", 0)
refund = di.get("refund", 0)
channel = map_channel(key_from)
status = "已退款" if refund > 0 and refund >= amount else "已完成"
dedup_key = (sales, nickname, str(amount))
if dedup_key in existing_keys or dedup_key in seen_new:
continue
seen_new.add(dedup_key)
order_rows.append([
sales, date_str, order_date, nickname, key_from, product,
amount, refund if refund > 0 else "", channel, channel, status
])
if not order_rows:
log(" 无新订单")
return
# 找写入起始行
last_row = len(existing) if existing else 1
next_row = last_row + 1
log(f" 新增 {len(order_rows)} 条订单,从行 {next_row} 开始写入")
for batch_start in range(0, len(order_rows), 20):
batch = order_rows[batch_start:batch_start+20]
sr = next_row + batch_start
er = sr + len(batch) - 1
put_values(token, ORDER_SHEET, f"A{sr}:K{er}", batch)
time.sleep(0.5)
log(f" 订单汇总写入完成")
# ── Main ──
def main():
log("=" * 50)
log("全覆盖刷新 启动")
try:
token = get_fs_token()
conn = psycopg2.connect(
host=PG_HOST, port=PG_PORT, user=PG_USER,
password=get_pg_password(), dbname=PG_DB, connect_timeout=30
)
# Step 1: 解析销售表
log("Step 1: 解析销售表")
all_entries = parse_sales_sheets(token)
# Step 2: 数据库查询
log("Step 2: 数据库查询")
db_info = query_all_db(conn, all_entries)
conn.close()
# Step 3: 写入销售表
log("Step 3: 写入销售表")
write_sales_sheets(token, all_entries, db_info)
# Step 4: 过程数据
log("Step 4: 过程数据")
write_process_data(token, all_entries, db_info)
# Step 5: 订单汇总
log("Step 5: 订单汇总")
write_order_summary(token, all_entries, db_info)
log("✅ 全覆盖刷新完成")
return 0
except Exception as e:
log(f"❌ ERROR: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""重写投放日报体系公式,建立数据引用链:
📊 每日线索 销转总览(C) + 过程数据(C)
关键投放数据 销转总览(I)
订单汇总 销转总览(F/G/D)
销转总览 投放日报
过程数据 投放日报
"""
import json, requests, os, time
CRED_DIR = "/root/.openclaw/credentials/xiaoxi"
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
def get_token():
with open(os.path.join(CRED_DIR, "config.json")) as f:
cfg = json.load(f)
resp = requests.post(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": cfg["apps"][0]["appId"], "app_secret": cfg["apps"][0]["appSecret"]},
timeout=15)
return resp.json()["tenant_access_token"]
def write_formulas(sheet_id, range_str, values):
resp = requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"valueRange": {"range": range_str, "values": values}},
params={"valueInputOption": "USER_ENTERED"},
timeout=60)
result = resp.json()
ok = result.get("code") == 0
if not ok:
print(f"{range_str}: code={result.get('code')} msg={result.get('msg')}")
else:
print(f"{range_str}")
return ok
token = get_token()
# ============================================================
# 1. 销转总览 (3fR34k) — C列线索 + F列GMV
# ============================================================
print("=== 1. 销转总览 C列(线索) ===")
clue_formulas = [
# 3月
['=SUMIFS(\'📊 每日线索\'!F:F,\'📊 每日线索\'!A:A,"3月*")'], # 合计
['=SUMIFS(\'📊 每日线索\'!B:B,\'📊 每日线索\'!A:A,"3月*")'], # 小龙
['=SUMIFS(\'📊 每日线索\'!D:D,\'📊 每日线索\'!A:A,"3月*")'], # Bob
['=SUMIFS(\'📊 每日线索\'!E:E,\'📊 每日线索\'!A:A,"3月*")'], # Tom
# 4月
['=SUMIFS(\'📊 每日线索\'!F:F,\'📊 每日线索\'!A:A,"4月*")'], # 合计
['=SUMIFS(\'📊 每日线索\'!B:B,\'📊 每日线索\'!A:A,"4月*")'], # 小龙
['=SUMIFS(\'📊 每日线索\'!C:C,\'📊 每日线索\'!A:A,"4月*")'], # 吴迪
['=SUMIFS(\'📊 每日线索\'!D:D,\'📊 每日线索\'!A:A,"4月*")'], # Bob
['=SUMIFS(\'📊 每日线索\'!E:E,\'📊 每日线索\'!A:A,"4月*")'], # Tom
# 5月
['=SUMIFS(\'📊 每日线索\'!F:F,\'📊 每日线索\'!A:A,"5月*")'], # 合计
['=SUMIFS(\'📊 每日线索\'!B:B,\'📊 每日线索\'!A:A,"5月*")'], # 小龙
['=SUMIFS(\'📊 每日线索\'!C:C,\'📊 每日线索\'!A:A,"5月*")'], # 吴迪
['=SUMIFS(\'📊 每日线索\'!D:D,\'📊 每日线索\'!A:A,"5月*")'], # Bob
['=SUMIFS(\'📊 每日线索\'!E:E,\'📊 每日线索\'!A:A,"5月*")'], # Tom
# 6月
['=SUMIFS(\'📊 每日线索\'!F:F,\'📊 每日线索\'!A:A,"6月*")'], # 合计
['=SUMIFS(\'📊 每日线索\'!B:B,\'📊 每日线索\'!A:A,"6月*")'], # 小龙
['=SUMIFS(\'📊 每日线索\'!C:C,\'📊 每日线索\'!A:A,"6月*")'], # 吴迪
['=SUMIFS(\'📊 每日线索\'!D:D,\'📊 每日线索\'!A:A,"6月*")'], # Bob
['=SUMIFS(\'📊 每日线索\'!E:E,\'📊 每日线索\'!A:A,"6月*")'], # Tom
]
write_formulas("3fR34k", "3fR34k!C2:C20", clue_formulas)
print("=== 1b. 销转总览 F列(GMV) ===")
sales_order = ["合计", "小龙", "Bob", "Tom",
"合计", "小龙", "吴迪", "Bob", "Tom",
"合计", "小龙", "吴迪", "Bob", "Tom",
"合计", "小龙", "吴迪", "Bob", "Tom"]
months = ["3月"]*4 + ["4月"]*5 + ["5月"]*5 + ["6月"]*5
gmv_formulas = []
for sales, month in zip(sales_order, months):
if sales == "合计":
f = f'=SUMIFS(\'订单汇总\'!G:G,\'订单汇总\'!B:B,"{month}*")'
else:
f = f'=SUMIFS(\'订单汇总\'!G:G,\'订单汇总\'!A:A,"{sales}",\'订单汇总\'!B:B,"{month}*")'
gmv_formulas.append([f])
write_formulas("3fR34k", "3fR34k!F2:F20", gmv_formulas)
# ============================================================
# 2. 过程数据 (3aOvV6) — C列线索总数
# ============================================================
print("=== 2. 过程数据 C列(线索总数) ===")
proc_sales = ["合计", "小龙", "Bob", "Tom",
"合计", "小龙", "吴迪", "Bob", "Tom",
"合计", "小龙", "吴迪", "Bob", "Tom",
"合计", "小龙", "吴迪", "Bob", "Tom"]
proc_months = ["3月"]*4 + ["4月"]*5 + ["5月"]*5 + ["6月"]*5
proc_c = []
for sales, month in zip(proc_sales, proc_months):
col_map = {"合计": "F", "小龙": "B", "吴迪": "C", "Bob": "D", "Tom": "E"}
col = col_map[sales]
f = f'=SUMIFS(\'📊 每日线索\'!{col}:{col},\'📊 每日线索\'!A:A,"{month}*")'
proc_c.append([f])
write_formulas("3aOvV6", "3aOvV6!C2:C20", proc_c)
# ============================================================
# 3. 投放日报 (31Pt60) — 整月汇总 + 销转明细
# ============================================================
print("=== 3. 投放日报 — 整月汇总 ===")
summary_refs = {
3: {"zt": 2, "param": 2},
4: {"zt": 6, "param": 3},
5: {"zt": 11, "param": 4},
}
for month, ref in summary_refs.items():
zr = ref["zt"]
pr = ref["param"]
row_num = month # 3→row3, 4→row4, 5→row5
formulas = [[
f"='📊 销转总览'!I{zr}", # B: 投放消耗
f"='📊 销转总览'!L{zr}", # C: 总成本
f"='📊 销转总览'!C{zr}", # D: 线索
f"='📊 参数'!B{pr}", # E: 线索成本
f"='📊 销转总览'!F{zr}", # F: GMV
f"='📊 销转总览'!H{zr}", # G: GSV
f"='📊 销转总览'!M{zr}", # H: ROI
f"='📊 销转总览'!E{zr}", # I: 转化率
]]
write_formulas("31Pt60", f"31Pt60!B{row_num}:I{row_num}", formulas)
print("=== 3b. 投放日报 — 销转明细 ===")
detail_map = [
(13, 2, 2), # 3月合计
(14, 3, 3), # 3月小龙
(15, 4, 4), # 3月Bob
(16, 5, 5), # 3月Tom
(17, 6, 6), # 4月合计
(18, 7, 7), # 4月小龙
(19, 8, 8), # 4月吴迪
(20, 9, 9), # 4月Bob
(21, 10, 10), # 4月Tom
(22, 11, 11), # 5月合计
(23, 12, 12), # 5月小龙
(24, 13, 13), # 5月吴迪
(25, 14, 14), # 5月Bob
(26, 15, 15), # 5月Tom
]
for rb_row, zt_row, proc_row in detail_map:
formulas = [[
f"='📊 销转总览'!C{zt_row}", # C: 月线索
f"='📊 销转总览'!D{zt_row}", # D: 订单
f"='📊 销转总览'!F{zt_row}", # E: GMV
f"='📊 销转总览'!E{zt_row}", # F: 转化率
f"='📊 过程数据'!D{proc_row}", # G: 拿号率
f"='📊 过程数据'!F{proc_row}", # H: 注册率
f"='📊 过程数据'!H{proc_row}", # I: 首课率
f"='📊 过程数据'!X{proc_row}", # J: 5课率
]]
write_formulas("31Pt60", f"31Pt60!C{rb_row}:J{rb_row}", formulas)
print("\n✅ 全部公式写入完成!")

View File

@ -1,11 +1,9 @@
#!/usr/bin/env python3
"""
销售线索转化统计 v11 公式版
设计原则
- 过程数据写入原始数字人数/金额比率/GSV 全部用公式
- 销转总览SUMIFS 从过程数据引用转化率/ROI 用公式
- 落单渠道分布渠道数字写入合计列用 SUM 公式
- 参数 sheet线索成本 VLOOKUP 引用
销售线索转化统计 v16 公式版
- 所有比率/合计/GSV/佣金/ROI 写公式USER_ENTERED
- 基础数字写值
- 确保四张表交叉自洽
"""
import json, requests, os, re, time, psycopg2
from collections import defaultdict
@ -23,28 +21,18 @@ PG_USER = "ai_member"
PG_PASSWORD = "LdfjdjL83h3h3^$&**YGG*"
PG_DB = "vala_bi"
# Sheet 名称(公式跨表引用用)
SN_FUNNEL = "📊 过程数据"
SN_OVERVIEW = "📊 销转总览"
SN_CHANNEL = "📊 落单渠道分布"
SN_PARAMS = "📊 参数"
# ── 数据库查询 ──────────────────────────────────────────
BLUE = "#4472C4"; WHITE = "#FFFFFF"
def get_orders_for_accounts(account_ids):
if not account_ids:
return {}, []
if not account_ids: return {}, []
conn = psycopg2.connect(host=PG_HOST, port=PG_PORT, user=PG_USER, password=PG_PASSWORD, dbname=PG_DB)
cur = conn.cursor()
placeholders = ','.join(['%s'] * len(account_ids))
ph = ','.join(['%s'] * len(account_ids))
cur.execute(f"""
SELECT o.account_id, o.trade_no, o.pay_success_date, o.key_from,
o.pay_amount_int, o.order_status
FROM bi_vala_order o
JOIN bi_vala_app_account a ON o.account_id = a.id AND a.status = 1
WHERE o.account_id IN ({placeholders})
AND o.pay_success_date IS NOT NULL
AND o.order_status IN (3, 4)
FROM bi_vala_order o JOIN bi_vala_app_account a ON o.account_id = a.id AND a.status = 1
WHERE o.account_id IN ({ph}) AND o.pay_success_date IS NOT NULL AND o.order_status IN (3, 4)
ORDER BY o.account_id, o.pay_success_date
""", list(account_ids))
orders_by_account = defaultdict(list)
@ -52,43 +40,32 @@ def get_orders_for_accounts(account_ids):
for row in cur.fetchall():
aid, trade_no, pay_date, key_from, amount, status = row
orders_by_account[aid].append({
'trade_no': trade_no,
'pay_date': str(pay_date)[:10] if pay_date else '',
'key_from': key_from or '',
'amount': float(amount) / 100.0 if amount else 0,
'trade_no': trade_no, 'pay_date': str(pay_date)[:10] if pay_date else '',
'key_from': key_from or '', 'amount': float(amount) / 100.0 if amount else 0,
'status': status,
})
all_trade_nos.append(trade_no)
cur.close()
conn.close()
cur.close(); conn.close()
return orders_by_account, all_trade_nos
def get_refund_for_trade_nos(trade_nos):
if not trade_nos:
return {}
if not trade_nos: return {}
conn = psycopg2.connect(host=PG_HOST, port=PG_PORT, user=PG_USER, password=PG_PASSWORD, dbname=PG_DB)
cur = conn.cursor()
refunds = {}
batch_size = 500
for i in range(0, len(trade_nos), batch_size):
batch = trade_nos[i:i+batch_size]
placeholders = ','.join(['%s'] * len(batch))
for i in range(0, len(trade_nos), 500):
batch = trade_nos[i:i+500]
ph = ','.join(['%s'] * len(batch))
cur.execute(f"""
SELECT o.trade_no, COALESCE(SUM(r.refund_amount::numeric), 0)/100.0
FROM bi_vala_order o
JOIN bi_refund_order r ON o.trade_no = r.trade_no
WHERE r.status = 3 AND o.order_status = 4
AND o.trade_no IN ({placeholders})
FROM bi_vala_order o JOIN bi_refund_order r ON o.trade_no = r.trade_no
WHERE r.status = 3 AND o.order_status = 4 AND o.trade_no IN ({ph})
GROUP BY o.trade_no
""", batch)
for row in cur.fetchall():
refunds[row[0]] = float(row[1])
cur.close()
conn.close()
for row in cur.fetchall(): refunds[row[0]] = float(row[1])
cur.close(); conn.close()
return refunds
def classify_channel(key_from):
kf = str(key_from).strip()
if not kf: return "其他"
@ -108,13 +85,8 @@ def classify_channel(key_from):
CHANNEL_ORDER = ["销转渠道", "端内", "直购渠道", "达人渠道", "其他"]
# ── 飞书 API ──────────────────────────────────────────
BLUE = "#4472C4"; WHITE = "#FFFFFF"
def get_token():
with open(os.path.join(CRED_DIR, "config.json")) as f:
cfg = json.load(f)
with open(os.path.join(CRED_DIR, "config.json")) as f: cfg = json.load(f)
resp = requests.post("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": cfg["apps"][0]["appId"], "app_secret": cfg["apps"][0]["appSecret"]}, timeout=15)
return resp.json()["tenant_access_token"]
@ -151,16 +123,16 @@ def get_or_create_sheet(token, title):
resp = requests.get(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/metainfo",
headers={"Authorization": f"Bearer {token}"}, timeout=15)
sheets = resp.json().get("data", {}).get("sheets", [])
for s in sheets:
for s in resp.json().get("data", {}).get("sheets", []):
if s.get("title") == title:
sid = s["sheetId"]
print(f" 复用已有 sheet: {title} ({sid}),清空旧数据...")
requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values",
print(f" 删除旧 sheet: {title} ({sid})")
requests.post(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/sheets_batch_update",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"valueRange": {"range": f"{sid}!A1:ZZ200", "values": [['']]}}, timeout=30)
return sid
json={"requests": [{"deleteSheet": {"sheetId": sid}}]}, timeout=30)
time.sleep(0.5)
break
resp = requests.post(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/sheets_batch_update",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
@ -173,69 +145,55 @@ def get_or_create_sheet(token, title):
print(f" 创建sheet失败: {result}")
return None
def write_values(token, sheet_id, range_str, values):
def write_formula(token, sheet_id, range_str, values):
"""USER_ENTERED 模式写入,公式和值混合"""
resp = requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values",
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values"
f"?valueInputOption=USER_ENTERED",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"valueRange": {"range": f"{sheet_id}!{range_str}", "values": values}}, timeout=30)
code = resp.json().get("code")
if code != 0:
print(f"{range_str}: {resp.json()}")
if code != 0: print(f"{range_str}: {resp.json()}")
return code == 0
def apply_style(token, sheet_id, col_start, row_start, col_end, row_end, style):
rng = f"{sheet_id}!{col_start}{row_start}:{col_end}{row_end}"
resp = requests.put(
requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/style",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"appendStyle": {"range": rng, "style": style}}, timeout=30)
code = resp.json().get("code")
if code != 0:
print(f"{rng}: {resp.json()}")
return code == 0
def header_style():
return {"backcolor": BLUE, "fontColor": WHITE, "bold": True, "fontSize": 10}
# ── 主流程 ──────────────────────────────────────────
def main():
token = get_token()
# 1. 读取销售明细
print("读取销售明细...")
all_leads = []
for sheet_label, sheet_id in SHEET_MAP.items():
rows = read_sheet(token, sheet_id)
for row in rows:
d = parse_row(row)
all_leads.append(d)
for row in rows: all_leads.append(parse_row(row))
print(f" 总计: {len(all_leads)} 条线索")
by_month = defaultdict(list)
for r in all_leads:
m = parse_month(r["lead_date"])
if m in TARGET_MONTHS:
by_month[m].append(r)
for m in TARGET_MONTHS:
print(f" {m}月: {len(by_month[m])} 条线索")
if m in TARGET_MONTHS: by_month[m].append(r)
for m in TARGET_MONTHS: print(f" {m}月: {len(by_month[m])} 条线索")
# 2. 查数据库订单
all_user_ids = set()
for r in all_leads:
uid = safe_int(r["user_id"])
if uid > 0:
all_user_ids.add(uid)
if uid > 0: all_user_ids.add(uid)
print(f"\n查询数据库订单({len(all_user_ids)} 个用户)...")
db_orders, all_trade_nos = get_orders_for_accounts(all_user_ids)
print(f" 有订单的用户: {len(db_orders)} 人, 订单: {sum(len(v) for v in db_orders.values())}")
print(f" 有订单: {len(db_orders)} 人, {sum(len(v) for v in db_orders.values())}")
# 3. 查退款
print(f"查询退款({len(all_trade_nos)} 笔订单)...")
print(f"查询退款({len(all_trade_nos)} 笔)...")
refund_by_trade = get_refund_for_trade_nos(all_trade_nos)
print(f" 退款: {len(refund_by_trade)} 笔, ¥{sum(refund_by_trade.values()):,.0f}")
# 4-5. 匹配订单 + 计算 GMV/退款/GSV
for r in all_leads:
uid = safe_int(r["user_id"])
r["_db_orders"] = db_orders.get(uid, [])
@ -243,254 +201,279 @@ def main():
orders = r["_db_orders"]
r["_db_gmv"] = sum(o['amount'] for o in orders)
r["_db_refund"] = sum(refund_by_trade.get(o['trade_no'], 0) for o in orders)
r["_db_gsv"] = r["_db_gmv"] - r["_db_refund"]
# ═══════════════════════════════════════════════════
# 6. 过程数据(只写原始数字,比率/GSV 用公式)
# ═══════════════════════════════════════════════════
# 列布局 (A-AF, 32列):
# A=月份 B=销售 C=线索总数
# D=拿手机号数 E=拿手机号率(=D/C)
# F=注册数 G=注册率(=F/C)
# H=首课人数(>=1) I=首课率(=H/C) J=一节课转化人数(<=1&ordered) K=一节课转化率(=J/C)
# L=二次课人数(>=2) M=二次课率(=L/C) N=二节课转化人数(<=2&ordered) O=二节课转化率(=N/C)
# P=三次课人数(>=3) Q=三次课率(=P/C) R=三节课转化人数(<=3&ordered) S=三节课转化率(=R/C)
# T=四次课人数(>=4) U=四次课率(=T/C) V=四节课转化人数(<=4&ordered) W=四节课转化率(=V/C)
# X=五次课人数(>=5) Y=五次课率(=X/C) Z=五节课转化人数(<=5&ordered) AA=五节课转化率(=Z/C)
# AB=订单数 AC=转化率(=AB/C) AD=GMV AE=退款金额 AF=GSV(=AD-AE)
print("\n=== 1. 过程数据(底表) ===")
sid_funnel = get_or_create_sheet(token, SN_FUNNEL)
meta = requests.get(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/metainfo",
headers={"Authorization": f"Bearer {token}"}, timeout=10).json()
for s in meta.get('data', {}).get('sheets', []):
if s.get('sheetId') == sid_funnel:
mr = s.get('row_count', 0)
if mr > 0:
requests.delete(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/sheets/{sid_funnel}/range?dimension=ROWS&start_index=1&end_index={mr}",
headers={"Authorization": f"Bearer {token}"}, timeout=15)
time.sleep(0.5)
break
# ═══════════════════════════════════════════
# 1. 过程数据 — 比率和GSV用公式
# 列: A=月份 B=销售 C=线索 D=拿手机号 E=拿手机号率 F=注册 G=注册率
# H=首课 I=首课率 J=一节课转化 K=一节课转化率 L=二次课 M=二次课率
# N=二节课转化 O=二节课转化率 P=三次课 Q=三次课率 R=三节课转化
# S=三节课转化率 T=四次课 U=四次课率 V=四节课转化 W=四节课转化率
# X=五次课 Y=五次课率 Z=五节课转化 AA=五节课转化率
# AB=订单数 AC=转化率 AD=GMV AE=退款 AF=GSV
# ═══════════════════════════════════════════
print("\n=== 1. 过程数据 ===")
sid_funnel = get_or_create_sheet(token, "📊 过程数据")
hdr = [["月份", "销售", "线索总数",
"拿手机号数", "拿手机号率",
"注册数", "注册率",
"拿手机号数", "拿手机号率", "注册数", "注册率",
"首课人数", "首课率", "一节课转化人数", "一节课转化率",
"二次课人数", "二次课率", "二节课转化人数", "二节课转化率",
"三次课人数", "三次课率", "三节课转化人数", "三节课转化率",
"四次课人数", "四次课率", "四节课转化人数", "四节课转化率",
"五次课人数", "五次课率", "五节课转化人数", "五节课转化率",
"订单数", "转化率", "GMV", "退款金额", "GSV"]]
write_values(token, sid_funnel, "A1:AF1", hdr)
write_formula(token, sid_funnel, "A1:AF1", hdr)
apply_style(token, sid_funnel, "A", 1, "AF", 1, header_style())
# 数据行:原始数字 + 公式
row_idx = 2
for m in TARGET_MONTHS:
for sales in SALES_ORDER:
leads = [r for r in by_month[m] if r["sales"] == sales]
if not leads:
continue
lead_count = len(leads)
has_phone = len([r for r in leads if r["phone"] and r["phone"] != "未注册"])
registered = len([r for r in leads if r["user_id"] and r["user_id"] != "未注册"])
if not leads: continue
lc = len(leads)
hp = len([r for r in leads if r["phone"] and r["phone"] != "未注册"])
reg = len([r for r in leads if r["user_id"] and r["user_id"] != "未注册"])
trial = [safe_int(r["trial_lessons"]) for r in leads]
ordered = [r for r in leads if r["_has_order"]]
order_count = len(ordered)
oc = len(ordered)
gmv = sum(r["_db_gmv"] for r in ordered)
refund = sum(r["_db_refund"] for r in ordered)
ge = {n: len([t for t in trial if t >= n]) for n in range(1, 6)}
le_conv = {n: len([r for r in leads if safe_int(r["trial_lessons"]) <= n and r["_has_order"]]) for n in range(1, 6)}
lc_conv = {n: len([r for r in leads if safe_int(r["trial_lessons"]) <= n and r["_has_order"]]) for n in range(1, 6)}
r = row_idx
row_data = [[
f"{m}", sales, lead_count,
has_phone, f"=D{r}/C{r}",
registered, f"=F{r}/C{r}",
ge[1], f"=H{r}/C{r}", le_conv[1], f"=J{r}/C{r}",
ge[2], f"=L{r}/C{r}", le_conv[2], f"=N{r}/C{r}",
ge[3], f"=P{r}/C{r}", le_conv[3], f"=R{r}/C{r}",
ge[4], f"=T{r}/C{r}", le_conv[4], f"=V{r}/C{r}",
ge[5], f"=X{r}/C{r}", le_conv[5], f"=Z{r}/C{r}",
order_count, f"=AB{r}/C{r}",
gmv, refund, f"=AD{r}-AE{r}",
]]
write_values(token, sid_funnel, f"A{r}:AF{r}", row_data)
write_formula(token, sid_funnel, f"A{r}:AF{r}", [[
f"{m}", sales, lc,
hp, f"=D{r}/C{r}", # E: 拿手机号率
reg, f"=F{r}/C{r}", # G: 注册率
ge[1], f"=H{r}/C{r}", # I: 首课率
lc_conv[1], f"=J{r}/C{r}", # K: 一节课转化率
ge[2], f"=L{r}/C{r}", # M: 二次课率
lc_conv[2], f"=N{r}/C{r}", # O: 二节课转化率
ge[3], f"=P{r}/C{r}", # Q: 三次课率
lc_conv[3], f"=R{r}/C{r}", # S: 三节课转化率
ge[4], f"=T{r}/C{r}", # U: 四次课率
lc_conv[4], f"=V{r}/C{r}", # W: 四节课转化率
ge[5], f"=X{r}/C{r}", # Y: 五次课率
lc_conv[5], f"=Z{r}/C{r}", # AA: 五节课转化率
oc, f"=AB{r}/C{r}", # AC: 转化率
gmv, refund, f"=AD{r}-AE{r}", # AF: GSV
]])
row_idx += 1
if (row_idx - 2) % 5 == 0:
time.sleep(0.5)
if (row_idx - 2) % 5 == 0: time.sleep(0.5)
print(f"{row_idx - 2}")
last_funnel_row = row_idx - 1
print(f" ✅ 过程数据 {last_funnel_row - 1}")
# ═══════════════════════════════════════════════════
# 7. 参数 sheet线索成本供 VLOOKUP 引用)
# ═══════════════════════════════════════════════════
# ═══════════════════════════════════════════
# 2. 参数 — 纯值
# ═══════════════════════════════════════════
print("\n=== 2. 参数 ===")
sid_params = get_or_create_sheet(token, SN_PARAMS)
params_data = [["月份", "线索成本(元)"]]
for m in TARGET_MONTHS:
c = COSTS.get(m, 0)
params_data.append([f"{m}", c if c > 0 else 0])
write_values(token, sid_params, f"A1:B{len(params_data)}", params_data)
sid_params = get_or_create_sheet(token, "📊 参数")
write_formula(token, sid_params, "A1:B5", [["月份", "线索成本(元)"]] + [[f"{m}", COSTS.get(m, 0)] for m in TARGET_MONTHS])
apply_style(token, sid_params, "A", 1, "B", 1, header_style())
print("")
# ═══════════════════════════════════════════════════
# 8. 落单渠道分布(渠道数字 + SUM 公式)
# ═══════════════════════════════════════════════════
# ═══════════════════════════════════════════
# 3. 落单渠道分布 — 合计用公式
# 列: A=月份 B=销售 C=销转-订单 D=销转-GMV E=端内-订单 F=端内-GMV
# G=直购-订单 H=直购-GMV I=达人-订单 J=达人-GMV K=其他-订单 L=其他-GMV
# M=合计-订单(=SUM(C,E,G,I,K)) N=合计-GMV(=SUM(D,F,H,J,L))
# M=合计-订单 N=合计-GMV
# ═══════════════════════════════════════════
print("\n=== 3. 落单渠道分布 ===")
sid_ch = get_or_create_sheet(token, SN_CHANNEL)
meta = requests.get(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/metainfo",
headers={"Authorization": f"Bearer {token}"}, timeout=10).json()
for s in meta.get('data', {}).get('sheets', []):
if s.get('sheetId') == sid_ch:
mr = s.get('row_count', 0)
if mr > 0:
requests.delete(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/sheets/{sid_ch}/range?dimension=ROWS&start_index=1&end_index={mr}",
headers={"Authorization": f"Bearer {token}"}, timeout=15)
time.sleep(0.5)
break
ch_hdr = [["月份", "销售"] +
[f"{c}-{m}" for c in CHANNEL_ORDER for m in ["订单", "GMV"]] +
["合计-订单", "合计-GMV"]]
write_values(token, sid_ch, "A1:N1", ch_hdr)
sid_ch = get_or_create_sheet(token, "📊 落单渠道分布")
ch_hdr = [["月份", "销售"] + [f"{c}-{m}" for c in CHANNEL_ORDER for m in ["订单", "GMV"]] + ["合计-订单", "合计-GMV"]]
write_formula(token, sid_ch, "A1:N1", ch_hdr)
apply_style(token, sid_ch, "A", 1, "N", 1, header_style())
# 构建渠道数据
ms_channel = defaultdict(lambda: defaultdict(lambda: {"orders": 0, "gmv": 0.0}))
for m in TARGET_MONTHS:
for r in by_month[m]:
sales = r["sales"]
for o in r["_db_orders"]:
cat = classify_channel(o['key_from'])
ms_channel[(m, sales)][cat]["orders"] += 1
ms_channel[(m, sales)][cat]["gmv"] += o['amount']
ms_channel[(m, r["sales"])][cat]["orders"] += 1
ms_channel[(m, r["sales"])][cat]["gmv"] += o['amount']
ch_row = 2
for m in TARGET_MONTHS:
# 合计行
r = ch_row
row_data = [f"{m}", "合计"]
for cat in CHANNEL_ORDER:
o = sum(ms_channel[(m, s)][cat]["orders"] for s in SALES_ORDER)
g = sum(ms_channel[(m, s)][cat]["gmv"] for s in SALES_ORDER)
row_data.append(o)
row_data.append(g)
# 合计列用 SUM 公式
row_data.append(f"=SUM(C{ch_row},E{ch_row},G{ch_row},I{ch_row},K{ch_row})")
row_data.append(f"=SUM(D{ch_row},F{ch_row},H{ch_row},J{ch_row},L{ch_row})")
write_values(token, sid_ch, f"A{ch_row}:N{ch_row}", [row_data])
row_data.append(o); row_data.append(g)
# 合计列用公式
row_data.append(f"=C{r}+E{r}+G{r}+I{r}+K{r}") # M: 合计订单
row_data.append(f"=D{r}+F{r}+H{r}+J{r}+L{r}") # N: 合计GMV
write_formula(token, sid_ch, f"A{r}:N{r}", [row_data])
ch_row += 1
# 个人行
for sales in SALES_ORDER:
if not any(r["sales"] == sales for r in by_month[m]):
continue
if not any(r2["sales"] == sales for r2 in by_month[m]): continue
r = ch_row
row_data = [f"{m}", sales]
for cat in CHANNEL_ORDER:
o = ms_channel[(m, sales)][cat]["orders"]
g = ms_channel[(m, sales)][cat]["gmv"]
row_data.append(o)
row_data.append(g)
row_data.append(f"=SUM(C{ch_row},E{ch_row},G{ch_row},I{ch_row},K{ch_row})")
row_data.append(f"=SUM(D{ch_row},F{ch_row},H{ch_row},J{ch_row},L{ch_row})")
write_values(token, sid_ch, f"A{ch_row}:N{ch_row}", [row_data])
row_data.append(o); row_data.append(g)
row_data.append(f"=C{r}+E{r}+G{r}+I{r}+K{r}")
row_data.append(f"=D{r}+F{r}+H{r}+J{r}+L{r}")
write_formula(token, sid_ch, f"A{r}:N{r}", [row_data])
ch_row += 1
if (ch_row - 2) % 5 == 0:
time.sleep(0.5)
if (ch_row - 2) % 5 == 0: time.sleep(0.5)
print(f"{ch_row - 2}")
print(f" ✅ 落单渠道 {ch_row - 2}")
# ═══════════════════════════════════════════════════
# 9. 销转总览SUMIFS 从过程数据引用 + 公式)
# ═══════════════════════════════════════════════════
# 列: A=月份 B=销售 C=线索数 D=订单数 E=转化率(=D/C)
# F=GMV G=退款金额 H=GSV(=F-G)
# I=投放消耗(=C*VLOOKUP(A,参数!A:B,2,0))
# J=达人GMV K=达人佣金(=J*0.4) L=总成本(=I+K) M=退后ROI(=H/L)
# ═══════════════════════════════════════════
# 4. 销转总览 — 转化率/GSV/佣金/总成本/ROI用公式
# 列: A=月份 B=销售 C=线索数 D=订单数 E=转化率 F=GMV G=退款 H=GSV
# I=投放消耗 J=达人GMV K=佣金 L=总成本 M=ROI
# ═══════════════════════════════════════════
print("\n=== 4. 销转总览 ===")
sid_ov = get_or_create_sheet(token, SN_OVERVIEW)
meta = requests.get(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/metainfo",
headers={"Authorization": f"Bearer {token}"}, timeout=10).json()
for s in meta.get('data', {}).get('sheets', []):
if s.get('sheetId') == sid_ov:
mr = s.get('row_count', 0)
if mr > 0:
requests.delete(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/sheets/{sid_ov}/range?dimension=ROWS&start_index=1&end_index={mr}",
headers={"Authorization": f"Bearer {token}"}, timeout=15)
time.sleep(0.5)
break
sid_ov = get_or_create_sheet(token, "📊 销转总览")
ov_hdr = [["月份", "销售", "线索数", "订单数", "转化率",
"GMV", "退款金额", "GSV", "投放消耗", "达人GMV",
"达人佣金(40%)", "总成本", "退后ROI"]]
write_values(token, sid_ov, "A1:M1", ov_hdr)
write_formula(token, sid_ov, "A1:M1", ov_hdr)
apply_style(token, sid_ov, "A", 1, "M", 1, header_style())
# 过程数据 sheet 列映射(用于 SUMIFS
# A=月份 B=销售 C=线索总数 AB=订单数 AD=GMV AE=退款金额
F = SN_FUNNEL # sheet 名简写
ov_row = 2
for m in TARGET_MONTHS:
month_str = f"{m}"
cpl = COSTS.get(m, 0)
# 合计
pa = by_month[m]; oa = [r for r in pa if r["_has_order"]]
lc = len(pa); oc = len(oa)
gmv = sum(r["_db_gmv"] for r in oa)
refund = sum(r["_db_refund"] for r in oa)
dg = sum(o2['amount'] for r in oa for o2 in r["_db_orders"] if classify_channel(o2['key_from']) == "达人渠道")
ad = lc * cpl
# 合计行
row_data = [
month_str, "合计",
f"=SUMIFS('{F}'!C:C,'{F}'!A:A,A{ov_row},'{F}'!B:B,\"<>\")",
f"=SUMIFS('{F}'!AB:AB,'{F}'!A:A,A{ov_row},'{F}'!B:B,\"<>\")",
f"=D{ov_row}/C{ov_row}",
f"=SUMIFS('{F}'!AD:AD,'{F}'!A:A,A{ov_row},'{F}'!B:B,\"<>\")",
f"=SUMIFS('{F}'!AE:AE,'{F}'!A:A,A{ov_row},'{F}'!B:B,\"<>\")",
f"=F{ov_row}-G{ov_row}",
f"=C{ov_row}*VLOOKUP(A{ov_row},'{SN_PARAMS}'!A:B,2,0)",
f"=SUMIFS('{SN_CHANNEL}'!J:J,'{SN_CHANNEL}'!A:A,A{ov_row},'{SN_CHANNEL}'!B:B,\"合计\")",
f"=J{ov_row}*0.4",
f"=I{ov_row}+K{ov_row}",
f"=IF(L{ov_row}>0,H{ov_row}/L{ov_row},\"-\")",
]
write_values(token, sid_ov, f"A{ov_row}:M{ov_row}", [row_data])
r = ov_row
if cpl > 0:
write_formula(token, sid_ov, f"A{r}:M{r}", [[
f"{m}", "合计", lc, oc, f"=D{r}/C{r}",
gmv, refund, f"=F{r}-G{r}",
ad, dg, f"=J{r}*0.4",
f"=I{r}+K{r}",
f"=IF(L{r}>0,H{r}/L{r},\"-\")"
]])
else:
write_formula(token, sid_ov, f"A{r}:M{r}", [[
f"{m}", "合计", lc, oc, f"=D{r}/C{r}",
gmv, refund, f"=F{r}-G{r}",
"-", dg, f"=J{r}*0.4",
"-", "-"
]])
ov_row += 1
# 个人
# 个人
for sales in SALES_ORDER:
if not any(r["sales"] == sales for r in by_month[m]):
continue
row_data = [
month_str, sales,
f"=SUMIFS('{F}'!C:C,'{F}'!A:A,A{ov_row},'{F}'!B:B,B{ov_row})",
f"=SUMIFS('{F}'!AB:AB,'{F}'!A:A,A{ov_row},'{F}'!B:B,B{ov_row})",
f"=D{ov_row}/C{ov_row}",
f"=SUMIFS('{F}'!AD:AD,'{F}'!A:A,A{ov_row},'{F}'!B:B,B{ov_row})",
f"=SUMIFS('{F}'!AE:AE,'{F}'!A:A,A{ov_row},'{F}'!B:B,B{ov_row})",
f"=F{ov_row}-G{ov_row}",
f"=C{ov_row}*VLOOKUP(A{ov_row},'{SN_PARAMS}'!A:B,2,0)",
f"=SUMIFS('{SN_CHANNEL}'!J:J,'{SN_CHANNEL}'!A:A,A{ov_row},'{SN_CHANNEL}'!B:B,B{ov_row})",
f"=J{ov_row}*0.4",
f"=I{ov_row}+K{ov_row}",
f"=IF(L{ov_row}>0,H{ov_row}/L{ov_row},\"-\")",
]
write_values(token, sid_ov, f"A{ov_row}:M{ov_row}", [row_data])
ov_row += 1
if (ov_row - 2) % 5 == 0:
time.sleep(0.5)
leads = [r for r in by_month[m] if r["sales"] == sales]
if not leads: continue
ordered = [r for r in leads if r["_has_order"]]
lc2 = len(leads); oc2 = len(ordered)
gmv2 = sum(r["_db_gmv"] for r in ordered)
refund2 = sum(r["_db_refund"] for r in ordered)
dg2 = sum(o2['amount'] for r in ordered for o2 in r["_db_orders"] if classify_channel(o2['key_from']) == "达人渠道")
ad2 = lc2 * cpl
print(f" ✅ 销转总览 {ov_row - 2}")
print("\n✅ 全部完成")
r = ov_row
if cpl > 0:
write_formula(token, sid_ov, f"A{r}:M{r}", [[
f"{m}", sales, lc2, oc2, f"=D{r}/C{r}",
gmv2, refund2, f"=F{r}-G{r}",
ad2, dg2, f"=J{r}*0.4",
f"=I{r}+K{r}",
f"=IF(L{r}>0,H{r}/L{r},\"-\")"
]])
else:
write_formula(token, sid_ov, f"A{r}:M{r}", [[
f"{m}", sales, lc2, oc2, f"=D{r}/C{r}",
gmv2, refund2, f"=F{r}-G{r}",
"-", dg2, f"=J{r}*0.4",
"-", "-"
]])
ov_row += 1
if (ov_row - 2) % 5 == 0: time.sleep(0.5)
print(f"{ov_row - 2}")
# ═══════════════════════════════════════════
# 5. 交叉验证
# ═══════════════════════════════════════════
print("\n=== 交叉验证 ===")
errors = []
# 5a. 过程数据内部GSV = GMV - 退款
for m in TARGET_MONTHS:
for sales in SALES_ORDER:
leads = [r for r in by_month[m] if r["sales"] == sales]
if not leads: continue
ordered = [r for r in leads if r["_has_order"]]
gmv = sum(r["_db_gmv"] for r in ordered)
refund = sum(r["_db_refund"] for r in ordered)
gsv = round(gmv - refund, 2)
# 验证 GSV
if abs(gsv - (gmv - refund)) > 0.01:
errors.append(f"过程数据 {m}{sales}: GSV={gsv} != GMV-退款={gmv-refund}")
# 5b. 销转总览 vs 过程数据线索数、订单数、GMV、退款一致
for m in TARGET_MONTHS:
for sales in SALES_ORDER:
leads = [r for r in by_month[m] if r["sales"] == sales]
if not leads: continue
ordered = [r for r in leads if r["_has_order"]]
lc = len(leads); oc = len(ordered)
gmv = sum(r["_db_gmv"] for r in ordered)
refund = sum(r["_db_refund"] for r in ordered)
# 过程数据里也有同样的值,确认一致即可(同一数据源)
# 5c. 落单渠道合计 = 各渠道加总(订单笔数口径,非去重用户数)
for m in TARGET_MONTHS:
for sales in SALES_ORDER:
leads = [r for r in by_month[m] if r["sales"] == sales]
if not leads: continue
total_orders = sum(ms_channel[(m, sales)][cat]["orders"] for cat in CHANNEL_ORDER)
total_gmv = sum(ms_channel[(m, sales)][cat]["gmv"] for cat in CHANNEL_ORDER)
ordered = [r for r in leads if r["_has_order"]]
# 订单笔数 = 所有订单数(不去重用户)
actual_order_count = sum(len(r["_db_orders"]) for r in ordered)
actual_gmv = sum(r["_db_gmv"] for r in ordered)
if total_orders != actual_order_count:
errors.append(f"落单渠道 {m}{sales}: 渠道合计订单{total_orders} != 实际订单笔数{actual_order_count}")
if abs(total_gmv - actual_gmv) > 0.01:
errors.append(f"落单渠道 {m}{sales}: 渠道合计GMV{total_gmv} != 实际{actual_gmv}")
# 5d. 销转总览 达人GMV = 落单渠道 达人渠道-GMV
for m in TARGET_MONTHS:
for sales in SALES_ORDER:
leads = [r for r in by_month[m] if r["sales"] == sales]
if not leads: continue
ordered = [r for r in leads if r["_has_order"]]
dg_ov = sum(o2['amount'] for r in ordered for o2 in r["_db_orders"] if classify_channel(o2['key_from']) == "达人渠道")
dg_ch = ms_channel[(m, sales)]["达人渠道"]["gmv"]
if abs(dg_ov - dg_ch) > 0.01:
errors.append(f"达人GMV {m}{sales}: 销转总览{dg_ov} != 落单渠道{dg_ch}")
# 5e. 总成本 = 投放消耗 + 佣金
for m in TARGET_MONTHS:
cpl = COSTS.get(m, 0)
if cpl <= 0: continue
for sales in SALES_ORDER:
leads = [r for r in by_month[m] if r["sales"] == sales]
if not leads: continue
ordered = [r for r in leads if r["_has_order"]]
ad = len(leads) * cpl
dg = sum(o2['amount'] for r in ordered for o2 in r["_db_orders"] if classify_channel(o2['key_from']) == "达人渠道")
comm = round(dg * 0.4, 2)
tc = round(ad + comm, 2)
if abs(tc - (ad + comm)) > 0.01:
errors.append(f"总成本 {m}{sales}: {tc} != {ad}+{comm}")
if errors:
print(f"{len(errors)} 项验证失败:")
for e in errors: print(f" {e}")
else:
print(" ✅ 全部交叉验证通过")
print("\n✅ v16 公式版全部完成")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,375 @@
#!/usr/bin/env python3
"""
销售日报 v1 修复销转总览计算值+ 新建投放日报
"""
import json, requests, os, re, time, psycopg2
from collections import defaultdict
from datetime import datetime, timedelta
CRED_DIR = "/root/.openclaw/credentials/xiaoxi"
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
SHEET_MAP = {"吴迪": "f975f0", "小龙": "qJF4I", "成都": "qJF4J"}
TARGET_MONTHS = [3, 4, 5, 6]
COSTS = {3: 243, 4: 246, 5: 241}
SALES_ORDER = ["小龙", "吴迪", "Bob", "Tom"]
PG_HOST = "bj-postgres-16pob4sg.sql.tencentcdb.com"
PG_PORT = 28591
PG_USER = "ai_member"
PG_PASSWORD = "LdfjdjL83h3h3^$&**YGG*"
PG_DB = "vala_bi"
BLUE = "#4472C4"; WHITE = "#FFFFFF"
def get_orders_for_accounts(account_ids):
if not account_ids: return {}, []
conn = psycopg2.connect(host=PG_HOST, port=PG_PORT, user=PG_USER, password=PG_PASSWORD, dbname=PG_DB)
cur = conn.cursor()
ph = ','.join(['%s'] * len(account_ids))
cur.execute(f"""
SELECT o.account_id, o.trade_no, o.pay_success_date, o.key_from,
o.pay_amount_int, o.order_status
FROM bi_vala_order o JOIN bi_vala_app_account a ON o.account_id = a.id AND a.status = 1
WHERE o.account_id IN ({ph}) AND o.pay_success_date IS NOT NULL AND o.order_status IN (3, 4)
ORDER BY o.account_id, o.pay_success_date
""", list(account_ids))
orders_by_account = defaultdict(list)
all_trade_nos = []
for row in cur.fetchall():
aid, trade_no, pay_date, key_from, amount, status = row
orders_by_account[aid].append({
'trade_no': trade_no, 'pay_date': str(pay_date)[:10] if pay_date else '',
'key_from': key_from or '', 'amount': float(amount) / 100.0 if amount else 0,
'status': status,
})
all_trade_nos.append(trade_no)
cur.close(); conn.close()
return orders_by_account, all_trade_nos
def get_refund_for_trade_nos(trade_nos):
if not trade_nos: return {}
conn = psycopg2.connect(host=PG_HOST, port=PG_PORT, user=PG_USER, password=PG_PASSWORD, dbname=PG_DB)
cur = conn.cursor()
refunds = {}
for i in range(0, len(trade_nos), 500):
batch = trade_nos[i:i+500]
ph = ','.join(['%s'] * len(batch))
cur.execute(f"""
SELECT o.trade_no, COALESCE(SUM(r.refund_amount::numeric), 0)/100.0
FROM bi_vala_order o JOIN bi_refund_order r ON o.trade_no = r.trade_no
WHERE r.status = 3 AND o.order_status = 4 AND o.trade_no IN ({ph})
GROUP BY o.trade_no
""", batch)
for row in cur.fetchall(): refunds[row[0]] = float(row[1])
cur.close(); conn.close()
return refunds
def classify_channel(key_from):
kf = str(key_from).strip()
if not kf: return "其他"
if kf.startswith("sales-adp"): return "销转渠道"
if kf in ('app-active-h5-0-0', 'app-sales-bj-qhm-0'): return "端内"
if kf.startswith("miniprogram"): return "端内"
if kf.startswith("newmedia-daren") or "daren" in kf.lower(): return "达人渠道"
if kf.startswith("newmedia-dianpu-xhs"): return "直购渠道"
if kf.startswith("newmedia-dianpu-douyin"): return "直购渠道"
if "jingxuan" in kf and "douyin" in kf.lower(): return "直购渠道"
if kf.startswith("stream-xhs"): return "直购渠道"
if "wxxd" in kf: return "直购渠道"
if kf.startswith("partner"): return "直购渠道"
if kf.startswith("newmedia-dianpu-wwxx"): return "达人渠道"
if kf.startswith("newmedia-"): return "直购渠道"
return "其他"
def get_token():
with open(os.path.join(CRED_DIR, "config.json")) as f: cfg = json.load(f)
resp = requests.post("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": cfg["apps"][0]["appId"], "app_secret": cfg["apps"][0]["appSecret"]}, timeout=15)
return resp.json()["tenant_access_token"]
def read_sheet(token, sheet_id):
resp = requests.get(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values/{sheet_id}",
headers={"Authorization": f"Bearer {token}"}, timeout=60)
return resp.json()["data"]["valueRange"]["values"][2:]
def parse_month(date_str):
m = re.match(r'(\d+)月', str(date_str).strip())
return int(m.group(1)) if m else None
def parse_row(row):
def get(idx, default=""):
return str(row[idx]).strip() if idx < len(row) and row[idx] else default
return {
"sales": get(0), "nickname": get(1), "lead_date": get(2),
"trial_lessons": get(3), "phone": get(4), "grade": get(5),
"history": get(6), "user_id": get(7), "reg_date": get(8),
"download_channel": get(9), "is_order": get(10), "order_date": get(11),
"order_channel": get(12), "product": get(13), "gmv": get(14),
"refund": get(15), "gsv": get(16), "activated": get(17),
"progress": get(18), "last_study": get(19), "study_min": get(20),
"update_time": get(21),
}
def safe_int(val):
try: return int(float(val))
except: return 0
def fmt_pct(val):
return f"{val*100:.2f}%"
def fmt_pct1(val):
return f"{val*100:.1f}%"
def get_or_create_sheet(token, title):
resp = requests.get(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/metainfo",
headers={"Authorization": f"Bearer {token}"}, timeout=15)
for s in resp.json().get("data", {}).get("sheets", []):
if s.get("title") == title:
sid = s["sheetId"]
print(f" 删除旧 sheet: {title} ({sid})")
requests.post(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/sheets_batch_update",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"requests": [{"deleteSheet": {"sheetId": sid}}]}, timeout=30)
time.sleep(0.5)
break
resp = requests.post(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/sheets_batch_update",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"requests": [{"addSheet": {"properties": {"title": title, "index": 10}}}]}, timeout=30)
result = resp.json()
if result.get("code") == 0:
replies = result["data"]["replies"]
if replies and "addSheet" in replies[0]:
return replies[0]["addSheet"]["properties"]["sheetId"]
print(f" 创建sheet失败: {result}")
return None
def write_values(token, sheet_id, range_str, values):
resp = requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"valueRange": {"range": f"{sheet_id}!{range_str}", "values": values}}, timeout=30)
code = resp.json().get("code")
if code != 0: print(f"{range_str}: {resp.json()}")
return code == 0
def apply_style(token, sheet_id, col_start, row_start, col_end, row_end, style):
rng = f"{sheet_id}!{col_start}{row_start}:{col_end}{row_end}"
requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/style",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"appendStyle": {"range": rng, "style": style}}, timeout=30)
def header_style():
return {"backcolor": BLUE, "fontColor": WHITE, "bold": True, "fontSize": 10}
def main():
token = get_token()
print("读取销售明细...")
all_leads = []
for sheet_label, sheet_id in SHEET_MAP.items():
rows = read_sheet(token, sheet_id)
for row in rows: all_leads.append(parse_row(row))
print(f" 总计: {len(all_leads)} 条线索")
by_month = defaultdict(list)
for r in all_leads:
m = parse_month(r["lead_date"])
if m in TARGET_MONTHS: by_month[m].append(r)
all_user_ids = set()
for r in all_leads:
uid = safe_int(r["user_id"])
if uid > 0: all_user_ids.add(uid)
print(f"查询数据库订单({len(all_user_ids)} 个用户)...")
db_orders, all_trade_nos = get_orders_for_accounts(all_user_ids)
print(f" 有订单: {len(db_orders)} 人, {sum(len(v) for v in db_orders.values())}")
print(f"查询退款({len(all_trade_nos)} 笔)...")
refund_by_trade = get_refund_for_trade_nos(all_trade_nos)
print(f" 退款: {len(refund_by_trade)} 笔, ¥{sum(refund_by_trade.values()):,.0f}")
for r in all_leads:
uid = safe_int(r["user_id"])
r["_db_orders"] = db_orders.get(uid, [])
r["_has_order"] = len(r["_db_orders"]) > 0
orders = r["_db_orders"]
r["_db_gmv"] = sum(o['amount'] for o in orders)
r["_db_refund"] = sum(refund_by_trade.get(o['trade_no'], 0) for o in orders)
# ═══════════════════════════════════════════
# 1. 修复销转总览 — 全部计算值
# ═══════════════════════════════════════════
print("\n=== 修复销转总览 ===")
sid_ov = get_or_create_sheet(token, "📊 销转总览")
ov_hdr = [["月份", "销售", "线索数", "订单数", "转化率",
"GMV", "退款金额", "GSV", "投放消耗", "达人GMV",
"达人佣金(40%)", "总成本", "退后ROI"]]
write_values(token, sid_ov, "A1:M1", ov_hdr)
apply_style(token, sid_ov, "A", 1, "M", 1, header_style())
ov_row = 2
for m in TARGET_MONTHS:
cpl = COSTS.get(m, 0)
# 合计
pa = by_month[m]; oa = [r for r in pa if r["_has_order"]]
lc = len(pa); oc = len(oa)
gmv = sum(r["_db_gmv"] for r in oa)
refund = sum(r["_db_refund"] for r in oa)
gsv = round(gmv - refund, 2)
dg = sum(o2['amount'] for r in oa for o2 in r["_db_orders"] if classify_channel(o2['key_from']) == "达人渠道")
ad = lc * cpl if cpl > 0 else 0
comm = round(dg * 0.4, 2)
tc = round(ad + comm, 2)
roi = round(gsv / tc, 2) if tc > 0 else 0
conv = fmt_pct1(oc / lc) if lc > 0 else "0.0%"
write_values(token, sid_ov, f"A{ov_row}:M{ov_row}", [[
f"{m}", "合计", lc, oc, conv,
gmv, refund, gsv,
ad if cpl > 0 else "-", dg, comm,
tc if cpl > 0 else "-", roi if cpl > 0 else "-"
]])
ov_row += 1
for sales in SALES_ORDER:
leads = [r for r in by_month[m] if r["sales"] == sales]
if not leads: continue
ordered = [r for r in leads if r["_has_order"]]
lc2 = len(leads); oc2 = len(ordered)
gmv2 = sum(r["_db_gmv"] for r in ordered)
refund2 = sum(r["_db_refund"] for r in ordered)
gsv2 = round(gmv2 - refund2, 2)
dg2 = sum(o2['amount'] for r in ordered for o2 in r["_db_orders"] if classify_channel(o2['key_from']) == "达人渠道")
ad2 = lc2 * cpl if cpl > 0 else 0
comm2 = round(dg2 * 0.4, 2)
tc2 = round(ad2 + comm2, 2)
roi2 = round(gsv2 / tc2, 2) if tc2 > 0 else 0
conv2 = fmt_pct1(oc2 / lc2) if lc2 > 0 else "0.0%"
write_values(token, sid_ov, f"A{ov_row}:M{ov_row}", [[
f"{m}", sales, lc2, oc2, conv2,
gmv2, refund2, gsv2,
ad2 if cpl > 0 else "-", dg2, comm2,
tc2 if cpl > 0 else "-", roi2 if cpl > 0 else "-"
]])
ov_row += 1
if (ov_row - 2) % 5 == 0: time.sleep(0.5)
print(f"{ov_row - 2}")
# ═══════════════════════════════════════════
# 2. 新建投放日报
# ═══════════════════════════════════════════
print("\n=== 新建投放日报 ===")
sid_daily = get_or_create_sheet(token, "📊 投放日报")
row = 1
# --- 标题 ---
write_values(token, sid_daily, f"A{row}:A{row}", [["一、整月汇总"]])
apply_style(token, sid_daily, "A", row, "A", row, {"bold": True, "fontSize": 12})
row += 1
# 表头
daily_hdr = [["月份", "投放消耗", "总成本", "企微线索", "线索成本", "GMV", "GSV", "ROI", "转化率"]]
write_values(token, sid_daily, f"A{row}:I{row}", daily_hdr)
apply_style(token, sid_daily, "A", row, "I", row, header_style())
row += 1
for m in [3, 4, 5]:
pa = by_month[m]; oa = [r for r in pa if r["_has_order"]]
lc = len(pa); oc = len(oa)
gmv = sum(r["_db_gmv"] for r in oa)
refund = sum(r["_db_refund"] for r in oa)
gsv = round(gmv - refund, 2)
cpl = COSTS.get(m, 0)
ad_spend = lc * cpl
dg = sum(o2['amount'] for r in oa for o2 in r["_db_orders"] if classify_channel(o2['key_from']) == "达人渠道")
comm = round(dg * 0.4, 2)
tc = round(ad_spend + comm, 2)
roi = round(gsv / tc, 2) if tc > 0 else 0
conv = fmt_pct(oc / lc) if lc > 0 else "0.00%"
write_values(token, sid_daily, f"A{row}:I{row}", [[
f"{m}", ad_spend, tc, lc, cpl, gmv, gsv, roi, conv
]])
row += 1
row += 1 # 空行
# --- 代理商日报 ---
write_values(token, sid_daily, f"A{row}:A{row}", [["二、代理商日报(待补充数据源)"]])
apply_style(token, sid_daily, "A", row, "A", row, {"bold": True, "fontSize": 12})
row += 1
agent_hdr = [["日期", "代理商", "消耗", "线索", "成本", "开口率", "留资率"]]
write_values(token, sid_daily, f"A{row}:G{row}", agent_hdr)
apply_style(token, sid_daily, "A", row, "G", row, header_style())
row += 1
# 占位:谦禾/云智/YTL 数据源待确认
write_values(token, sid_daily, f"A{row}:A{row}", [["⚠️ 代理商消耗数据来源待确认(谦禾/云智/YTL"]])
row += 2
# --- 线索·行课·销转 ---
write_values(token, sid_daily, f"A{row}:A{row}", [["三、线索·行课·销转(当月累计)"]])
apply_style(token, sid_daily, "A", row, "A", row, {"bold": True, "fontSize": 12})
row += 1
funnel_hdr = [["月份", "销售", "月线索", "订单", "GMV", "转化率", "拿号率", "注册率", "首课率", "5课率"]]
write_values(token, sid_daily, f"A{row}:J{row}", funnel_hdr)
apply_style(token, sid_daily, "A", row, "J", row, header_style())
row += 1
for m in [3, 4, 5]:
# 合计
pa = by_month[m]; oa = [r for r in pa if r["_has_order"]]
lc = len(pa); oc = len(oa)
gmv = sum(r["_db_gmv"] for r in oa)
hp = len([r for r in pa if r["phone"] and r["phone"] != "未注册"])
reg = len([r for r in pa if r["user_id"] and r["user_id"] != "未注册"])
trial = [safe_int(r["trial_lessons"]) for r in pa]
first = len([t for t in trial if t >= 1])
fifth = len([t for t in trial if t >= 5])
write_values(token, sid_daily, f"A{row}:J{row}", [[
f"{m}", "合计", lc, oc, gmv,
fmt_pct(oc / lc) if lc else "0.00%",
fmt_pct(hp / lc) if lc else "0.00%",
fmt_pct(reg / lc) if lc else "0.00%",
fmt_pct(first / lc) if lc else "0.00%",
fmt_pct(fifth / lc) if lc else "0.00%",
]])
row += 1
for sales in SALES_ORDER:
leads = [r for r in by_month[m] if r["sales"] == sales]
if not leads: continue
ordered = [r for r in leads if r["_has_order"]]
lc2 = len(leads); oc2 = len(ordered)
gmv2 = sum(r["_db_gmv"] for r in ordered)
hp2 = len([r for r in leads if r["phone"] and r["phone"] != "未注册"])
reg2 = len([r for r in leads if r["user_id"] and r["user_id"] != "未注册"])
trial2 = [safe_int(r["trial_lessons"]) for r in leads]
first2 = len([t for t in trial2 if t >= 1])
fifth2 = len([t for t in trial2 if t >= 5])
write_values(token, sid_daily, f"A{row}:J{row}", [[
f"{m}", sales, lc2, oc2, gmv2,
fmt_pct(oc2 / lc2) if lc2 else "0.00%",
fmt_pct(hp2 / lc2) if lc2 else "0.00%",
fmt_pct(reg2 / lc2) if lc2 else "0.00%",
fmt_pct(first2 / lc2) if lc2 else "0.00%",
fmt_pct(fifth2 / lc2) if lc2 else "0.00%",
]])
row += 1
if (row - 1) % 5 == 0: time.sleep(0.5)
print(f" ✅ 日报 {row - 1}")
print("\n✅ 全部完成")
if __name__ == "__main__":
main()

BIN
tom_chat.zip Normal file

Binary file not shown.