🤖 每日自动备份 - 2026-05-07 08:00:01
This commit is contained in:
parent
6510dc41ca
commit
968286d9c9
@ -12,4 +12,4 @@ vala-component-practice-stat 8e768e2641019d27bd41f4647d2d90f24182a0554dad5ad9f41
|
|||||||
cron-schedule e103cbb1806b28c891b9c856963325086ecaff32edec208f0a841865f26e8f3e
|
cron-schedule e103cbb1806b28c891b9c856963325086ecaff32edec208f0a841865f26e8f3e
|
||||||
refund-user-learning-analysis 648fd4ae2b29167fd66eab4245bdaaef00242db3131f4919cc02f07ca2a9b59c
|
refund-user-learning-analysis 648fd4ae2b29167fd66eab4245bdaaef00242db3131f4919cc02f07ca2a9b59c
|
||||||
phone-chapter-query ac429b4da5a89db16efdf1066edf4ecb1c050b93aff20dd4c652af5f5568e44f
|
phone-chapter-query ac429b4da5a89db16efdf1066edf4ecb1c050b93aff20dd4c652af5f5568e44f
|
||||||
vala-order-amortization-stat de5f77b9e4ab865df5efe595e59e877257ac489a195fb969b9d20d1a71158b97
|
vala-order-amortization-stat fd9da99f313347aa11a4cad8e5d0c9226eac8649b45db94dfa60e37015ab7491
|
||||||
|
|||||||
37
memory/.dreams/short-term-recall.json
Normal file
37
memory/.dreams/short-term-recall.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"updatedAt": "2026-05-06T12:28:28.905Z",
|
||||||
|
"entries": {
|
||||||
|
"memory:memory/2026-05-06.md:1:20": {
|
||||||
|
"key": "memory:memory/2026-05-06.md:1:20",
|
||||||
|
"path": "memory/2026-05-06.md",
|
||||||
|
"startLine": 1,
|
||||||
|
"endLine": 20,
|
||||||
|
"source": "memory",
|
||||||
|
"snippet": "# 2026-05-06 工作日志 ## 任务1:学而思/讯飞/希沃/京东方月度渠道订单报表 - [李承龙需求] 创建按月汇总四个渠道订单的 Python 脚本 - 脚本路径:`/root/.openclaw/workspace/scripts/monthly_channel_order_report.py` - **渠道映射:** 仅端内 `key_from='app-active-h5-0-0'`,sale_channel: 21=学而思, 22=讯飞, 26=希沃, 27=京东方 - **输出格式:** 四个渠道各一个 Sheet,每月按天展示(学而思/希沃/京东方只显示有订单日期,讯飞显示全月每天) - **指标差异:** - 学而思/希沃/京东方:订单数、GMV、CPS分成(学而思×0.45,希沃×0.4,京东方×0.3)、订单号(分号拼接) - 讯飞:订单数、GMV、新增注册(download_channel LIKE '%讯飞%')、CPA分成(新增注册×7)、订单号 - **退费剔除:** NOT EXISTS (bi_refund_order.status=3) AND order_status != 4 - **测试运行:** 2026-04 月份,新增注册 537(讯飞),订单合计 15 单,GMV 14585 元 - **订单号格式问题:** Excel 科学计数法 → 设置单元格为文本格式解决 - **psycopg2 转义:** LIKE 中 `%` 需写成 `%%`(psycopg2 的 `%s` 占位符冲突) ## 任务2:销售线",
|
||||||
|
"recallCount": 1,
|
||||||
|
"dailyCount": 0,
|
||||||
|
"groundedCount": 0,
|
||||||
|
"totalScore": 1,
|
||||||
|
"maxScore": 1,
|
||||||
|
"firstRecalledAt": "2026-05-06T12:28:28.905Z",
|
||||||
|
"lastRecalledAt": "2026-05-06T12:28:28.905Z",
|
||||||
|
"queryHashes": [
|
||||||
|
"c30fa9d13974"
|
||||||
|
],
|
||||||
|
"recallDays": [
|
||||||
|
"2026-05-06"
|
||||||
|
],
|
||||||
|
"conceptTags": [
|
||||||
|
"学而思/讯飞/希沃/京东方月度渠道订单报表",
|
||||||
|
"key-from",
|
||||||
|
"app-active-h5-0-0",
|
||||||
|
"sale-channel",
|
||||||
|
"学而思/希沃/京东方只显示有订单日期",
|
||||||
|
"学而思/希沃/京东方",
|
||||||
|
"0.45",
|
||||||
|
"0.4"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
memory/2026-05-06-1025.md
Normal file
5
memory/2026-05-06-1025.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Session: 2026-05-06 10:25:14 GMT+8
|
||||||
|
|
||||||
|
- **Session Key**: agent:main:feishu:direct:ou_e63ce6b760ad39382852472f28fbe2a2
|
||||||
|
- **Session ID**: 58b30963-b037-4f50-8b15-04e77ad8fef0
|
||||||
|
- **Source**: feishu
|
||||||
53
memory/2026-05-06-order-amortization-sql.md
Normal file
53
memory/2026-05-06-order-amortization-sql.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Session: 2026-05-06 16:53:32 GMT+8
|
||||||
|
|
||||||
|
- **Session Key**: agent:main:feishu:direct:ou_e63ce6b760ad39382852472f28fbe2a2
|
||||||
|
- **Session ID**: 690e7978-4b15-4ce8-bfa3-9f8bb9271428
|
||||||
|
- **Source**: feishu
|
||||||
|
|
||||||
|
## Conversation Summary
|
||||||
|
|
||||||
|
assistant: 让我尝试用其他方式读取这张图片:
|
||||||
|
assistant: OCR 识别不太完整。让我尝试提取为文本再做更好的识别:
|
||||||
|
assistant: OCR 效果有限,但结合上下文我能推断这个表格的结构。让我换个思路,下载图片后用 Python OCR:
|
||||||
|
assistant: 承龙,OCR 识别效果不理想,表格的汉字内容没能完全准确提取。让我基于已识别到的内容和你之前的需求上下文,整理一下我理解的表格结构,请你确认是否正确:
|
||||||
|
|
||||||
|
根据 OCR 结果和上下文,这张表格应该是一个 **3行×3列** 的结构:
|
||||||
|
|
||||||
|
| | 新增(金额) | 退费(金额) |
|
||||||
|
|---|---|---|
|
||||||
|
| **上月订单转正** | 下单时间在上个月且在上个月未转正的订单金额总和,剔除上个月全部退费的订单,部分退费只计算有效金额,不考虑本月退费情况 | 这些订单在本月的退费金额 |
|
||||||
|
| **本月订单转正** | 下单时间在本月且在本月已经转正的订单的金额总和,不考虑退费情况 | 这些订单在本月的退费金额 |
|
||||||
|
| **本月订单未转正** | 下单时间在本月且在本月未转正的订单的金额总和,不考虑退费情况 | 这些订单在本月的退费金额 |
|
||||||
|
|
||||||
|
也就是:
|
||||||
|
- 行:上月订单转正、本月订单转正、本月订单未转正
|
||||||
|
- 列:新增(即预收账款金额)、退费(这些订单在本账期的退费金额)
|
||||||
|
|
||||||
|
如果理解正确,我需要确认几个问题:
|
||||||
|
|
||||||
|
1. **"新增"列中的金额**,是订单的**含税金额(pay_amount)**还是**税后金额**?还是**退后金额(pay_amount - 退费)**?
|
||||||
|
2. **"退费"列**是指这些订单在本账期内发生的退费金额对吧?
|
||||||
|
3. 表格里的"上月"是相对于账期的(如 3 月账期,"上月"就是 2 月),"本月"就是账期所在月(3 月),对吗?
|
||||||
|
user: 1、是指订单的含税金额
|
||||||
|
2、对的
|
||||||
|
3、对的
|
||||||
|
assistant: 好的,明确了。Sheet3 "本月预收账款" 结构如下:
|
||||||
|
|
||||||
|
| | 新增(含税金额) | 退费 |
|
||||||
|
|---|---|---|
|
||||||
|
| **上月订单转正** | 上月下单+上月未转正+本月转正的订单;剔除上月已全额退费的;部分退费只算有效金额(pay - 退费);不考虑本月退费 | 这些订单在本月的退费金额 |
|
||||||
|
| **本月订单转正** | 本月下单+本月已转正的订单金额总和;不考虑退费 | 这些订单在本月的退费金额 |
|
||||||
|
| **本月订单未转正** | 本月下单+本月未转正的订单金额总和;不考虑退费 | 这些订单在本月的退费金额 |
|
||||||
|
|
||||||
|
现在写 SQL。相对账期 "本月"=账期月,"上月"=账期月前一个月。需要计算这三个维度的新增和退费。
|
||||||
|
assistant: 现在修改 `run.py`,增加 Sheet3 的支持:
|
||||||
|
assistant: 现在跑 3 月数据测试:
|
||||||
|
assistant: 运行成功。验证一下 Sheet3 的内容:
|
||||||
|
assistant: Sheet3 数据跑出来了。发送文件:
|
||||||
|
assistant: 文件已发送。三个 Sheet 分别是:汇总表、订单明细、本月预收账款。看看数据有没有问题?
|
||||||
|
user: [OpenClaw heartbeat poll]
|
||||||
|
assistant: `vala-order-amortization-stat` 有变更已推送成功。需要通知李若松。
|
||||||
|
assistant: 好的,`vala-order-amortization-stat` 已推送到 SkillHub,需要通知李若松。李若松的 user_id 是 `4aagb443`。
|
||||||
|
assistant: 已通知李若松。
|
||||||
|
|
||||||
|
HEARTBEAT_OK
|
||||||
36
memory/2026-05-06.md
Normal file
36
memory/2026-05-06.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
# 2026-05-06 工作日志
|
||||||
|
|
||||||
|
## 任务1:学而思/讯飞/希沃/京东方月度渠道订单报表
|
||||||
|
|
||||||
|
- [李承龙需求] 创建按月汇总四个渠道订单的 Python 脚本
|
||||||
|
- 脚本路径:`/root/.openclaw/workspace/scripts/monthly_channel_order_report.py`
|
||||||
|
- **渠道映射:** 仅端内 `key_from='app-active-h5-0-0'`,sale_channel: 21=学而思, 22=讯飞, 26=希沃, 27=京东方
|
||||||
|
- **输出格式:** 四个渠道各一个 Sheet,每月按天展示(学而思/希沃/京东方只显示有订单日期,讯飞显示全月每天)
|
||||||
|
- **指标差异:**
|
||||||
|
- 学而思/希沃/京东方:订单数、GMV、CPS分成(学而思×0.45,希沃×0.4,京东方×0.3)、订单号(分号拼接)
|
||||||
|
- 讯飞:订单数、GMV、新增注册(download_channel LIKE '%讯飞%')、CPA分成(新增注册×7)、订单号
|
||||||
|
- **退费剔除:** NOT EXISTS (bi_refund_order.status=3) AND order_status != 4
|
||||||
|
- **测试运行:** 2026-04 月份,新增注册 537(讯飞),订单合计 15 单,GMV 14585 元
|
||||||
|
- **订单号格式问题:** Excel 科学计数法 → 设置单元格为文本格式解决
|
||||||
|
- **psycopg2 转义:** LIKE 中 `%` 需写成 `%%`(psycopg2 的 `%s` 占位符冲突)
|
||||||
|
|
||||||
|
## 任务2:销售线索用户分析报表
|
||||||
|
|
||||||
|
- [李承龙需求] 输入包含462个用户ID的Excel,输出每个用户+角色的完整画像报表
|
||||||
|
- 脚本路径:`/root/.openclaw/workspace/scripts/lead_user_analysis.py`
|
||||||
|
- **重要口径修正(李承龙确认):**
|
||||||
|
- **体验课定义:** 固定为 L1 U00(L01-L05) + L2 U00(L01-L05) 共10节课,**不通过 expire_time 区分**
|
||||||
|
- L1 chapter_id: 343, 344, 345, 346, 348
|
||||||
|
- L2 chapter_id: 55, 56, 57, 58, 59
|
||||||
|
- **完成时间:** 取 `bi_user_chapter_play_record` 中 `play_status=1` 记录的**最早 `updated_at`**(非 `created_at`)
|
||||||
|
- **报表列:** 用户ID、角色ID、用户注册时间、角色创建时间、L1序章5节完成时间、L2序章5节完成时间、购买时间、购买课包名称、支付金额、购买渠道key_from、是否退款、退款金额
|
||||||
|
- **分表规则:** `bi_user_chapter_play_record_{0..7}`,分表号 = user_id % 8
|
||||||
|
- **重要关联:** `bi_user_chapter_play_record.user_id` = `bi_vala_app_character.id`(角色ID),非 account_id
|
||||||
|
- **退款条件:** `bi_refund_order.status=3` AND `bi_vala_order.order_status=4`
|
||||||
|
- **结果:** 533行(462个用户 × 533个有效角色),235个完成L1序章,139个完成L2序章,53行有购买,10行有退款
|
||||||
|
- **踩坑记录:**
|
||||||
|
- 子会话按旧口径执行后被 kill,最终主会话亲自重写脚本
|
||||||
|
- ALL_CHAPTERS 的值是 `"L1_U00_L01"`,COL_ORDER 是 `"L1_U00_L01_完成时间"`,不匹配导致全部为空,修复后正确
|
||||||
|
- datetime 写入 Excel 需去掉时区信息 `dt.tz_localize(None)`
|
||||||
|
- play_record 分表查询需遍历 0-7 共 8 个分表
|
||||||
245
output/l2_pass_time_full.txt
Normal file
245
output/l2_pass_time_full.txt
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
S0|U00|L01|55|9898
|
||||||
|
S0|U00|L02|56|6559
|
||||||
|
S0|U00|L03|57|5265
|
||||||
|
S0|U00|L04|58|4457
|
||||||
|
S0|U00|L05|59|3927
|
||||||
|
S1|U01|L01|70|1749
|
||||||
|
S1|U01|L02|71|1644
|
||||||
|
S1|U01|L03|72|1574
|
||||||
|
S1|U01|L04|73|1498
|
||||||
|
S1|U01|L05|74|1437
|
||||||
|
S1|U02|L01|60|1348
|
||||||
|
S1|U02|L02|61|1298
|
||||||
|
S1|U02|L03|62|1257
|
||||||
|
S1|U02|L04|63|1202
|
||||||
|
S1|U02|L05|64|1166
|
||||||
|
S1|U03|L01|78|1105
|
||||||
|
S1|U03|L02|79|1073
|
||||||
|
S1|U03|L03|80|1036
|
||||||
|
S1|U03|L04|81|1017
|
||||||
|
S1|U03|L05|82|997
|
||||||
|
S1|U04|L01|84|950
|
||||||
|
S1|U04|L02|85|934
|
||||||
|
S1|U04|L03|86|906
|
||||||
|
S1|U04|L04|87|892
|
||||||
|
S1|U04|L05|88|875
|
||||||
|
S1|U05|L01|89|841
|
||||||
|
S1|U05|L02|90|820
|
||||||
|
S1|U05|L03|91|807
|
||||||
|
S1|U05|L04|92|792
|
||||||
|
S1|U05|L05|93|780
|
||||||
|
S1|U06|L01|94|758
|
||||||
|
S1|U06|L02|95|746
|
||||||
|
S1|U06|L03|96|732
|
||||||
|
S1|U06|L04|97|716
|
||||||
|
S1|U06|L05|98|707
|
||||||
|
S1|U07|L01|99|685
|
||||||
|
S1|U07|L02|100|669
|
||||||
|
S1|U07|L03|101|657
|
||||||
|
S1|U07|L04|102|655
|
||||||
|
S1|U07|L05|103|640
|
||||||
|
S1|U08|L01|104|625
|
||||||
|
S1|U08|L02|105|621
|
||||||
|
S1|U08|L03|106|616
|
||||||
|
S1|U08|L04|107|611
|
||||||
|
S1|U08|L05|108|605
|
||||||
|
S1|U09|L01|142|593
|
||||||
|
S1|U09|L02|143|589
|
||||||
|
S1|U09|L03|144|588
|
||||||
|
S1|U09|L04|145|581
|
||||||
|
S1|U09|L05|146|577
|
||||||
|
S1|U10|L01|114|565
|
||||||
|
S1|U10|L02|115|561
|
||||||
|
S1|U10|L03|116|558
|
||||||
|
S1|U10|L04|117|555
|
||||||
|
S1|U10|L05|118|554
|
||||||
|
S1|U11|L01|119|545
|
||||||
|
S1|U11|L02|120|538
|
||||||
|
S1|U11|L03|121|536
|
||||||
|
S1|U11|L04|122|534
|
||||||
|
S1|U11|L05|123|531
|
||||||
|
S1|U12|L01|125|524
|
||||||
|
S1|U12|L02|126|519
|
||||||
|
S1|U12|L03|127|517
|
||||||
|
S1|U12|L04|128|513
|
||||||
|
S1|U12|L05|129|508
|
||||||
|
S2|U13|L01|147|342
|
||||||
|
S2|U13|L02|148|337
|
||||||
|
S2|U13|L03|149|334
|
||||||
|
S2|U13|L04|150|333
|
||||||
|
S2|U13|L05|151|329
|
||||||
|
S2|U14|L01|152|325
|
||||||
|
S2|U14|L02|153|324
|
||||||
|
S2|U14|L03|154|320
|
||||||
|
S2|U14|L04|155|317
|
||||||
|
S2|U14|L05|156|316
|
||||||
|
S2|U15|L01|157|309
|
||||||
|
S2|U15|L02|158|307
|
||||||
|
S2|U15|L03|159|303
|
||||||
|
S2|U15|L04|160|298
|
||||||
|
S2|U15|L05|161|295
|
||||||
|
S2|U16|L01|162|289
|
||||||
|
S2|U16|L02|163|287
|
||||||
|
S2|U16|L03|164|285
|
||||||
|
S2|U16|L04|165|285
|
||||||
|
S2|U16|L05|166|285
|
||||||
|
S2|U17|L01|167|283
|
||||||
|
S2|U17|L02|168|281
|
||||||
|
S2|U17|L03|169|280
|
||||||
|
S2|U17|L04|170|279
|
||||||
|
S2|U17|L05|171|278
|
||||||
|
S2|U18|L01|172|272
|
||||||
|
S2|U18|L02|173|272
|
||||||
|
S2|U18|L03|174|270
|
||||||
|
S2|U18|L04|175|264
|
||||||
|
S2|U18|L05|176|261
|
||||||
|
S2|U19|L01|177|259
|
||||||
|
S2|U19|L02|178|259
|
||||||
|
S2|U19|L03|179|259
|
||||||
|
S2|U19|L04|180|256
|
||||||
|
S2|U19|L05|181|252
|
||||||
|
S2|U20|L01|182|250
|
||||||
|
S2|U20|L02|183|250
|
||||||
|
S2|U20|L03|184|249
|
||||||
|
S2|U20|L04|185|247
|
||||||
|
S2|U20|L05|186|244
|
||||||
|
S2|U21|L01|187|241
|
||||||
|
S2|U21|L02|188|239
|
||||||
|
S2|U21|L03|189|239
|
||||||
|
S2|U21|L04|190|238
|
||||||
|
S2|U21|L05|191|238
|
||||||
|
S2|U22|L01|192|234
|
||||||
|
S2|U22|L02|193|231
|
||||||
|
S2|U22|L03|194|231
|
||||||
|
S2|U22|L04|195|230
|
||||||
|
S2|U22|L05|196|229
|
||||||
|
S2|U23|L01|197|229
|
||||||
|
S2|U23|L02|198|229
|
||||||
|
S2|U23|L03|199|227
|
||||||
|
S2|U23|L04|200|226
|
||||||
|
S2|U23|L05|201|226
|
||||||
|
S2|U24|L01|202|225
|
||||||
|
S2|U24|L02|203|224
|
||||||
|
S2|U24|L03|204|224
|
||||||
|
S2|U24|L04|205|223
|
||||||
|
S2|U24|L05|206|223
|
||||||
|
S3|U25|L01|207|143
|
||||||
|
S3|U25|L02|208|143
|
||||||
|
S3|U25|L03|209|141
|
||||||
|
S3|U25|L04|210|140
|
||||||
|
S3|U25|L05|211|140
|
||||||
|
S3|U26|L01|212|140
|
||||||
|
S3|U26|L02|213|140
|
||||||
|
S3|U26|L03|214|140
|
||||||
|
S3|U26|L04|215|140
|
||||||
|
S3|U26|L05|216|140
|
||||||
|
S3|U27|L01|217|139
|
||||||
|
S3|U27|L02|218|139
|
||||||
|
S3|U27|L03|219|138
|
||||||
|
S3|U27|L04|220|137
|
||||||
|
S3|U27|L05|221|137
|
||||||
|
S3|U28|L01|222|137
|
||||||
|
S3|U28|L02|223|137
|
||||||
|
S3|U28|L03|224|137
|
||||||
|
S3|U28|L04|225|135
|
||||||
|
S3|U28|L05|226|135
|
||||||
|
S3|U29|L01|227|135
|
||||||
|
S3|U29|L02|228|135
|
||||||
|
S3|U29|L03|229|135
|
||||||
|
S3|U29|L04|230|135
|
||||||
|
S3|U29|L05|231|135
|
||||||
|
S3|U30|L01|232|127
|
||||||
|
S3|U30|L02|233|127
|
||||||
|
S3|U30|L03|234|127
|
||||||
|
S3|U30|L04|235|127
|
||||||
|
S3|U30|L05|236|127
|
||||||
|
S3|U31|L01|237|127
|
||||||
|
S3|U31|L02|238|127
|
||||||
|
S3|U31|L03|239|127
|
||||||
|
S3|U31|L04|240|127
|
||||||
|
S3|U31|L05|241|127
|
||||||
|
S3|U32|L01|242|127
|
||||||
|
S3|U32|L02|244|127
|
||||||
|
S3|U32|L03|245|127
|
||||||
|
S3|U32|L04|246|127
|
||||||
|
S3|U32|L05|247|127
|
||||||
|
S3|U33|L01|248|127
|
||||||
|
S3|U33|L02|249|127
|
||||||
|
S3|U33|L03|250|127
|
||||||
|
S3|U33|L04|251|127
|
||||||
|
S3|U33|L05|252|127
|
||||||
|
S3|U34|L01|254|127
|
||||||
|
S3|U34|L02|255|127
|
||||||
|
S3|U34|L03|256|127
|
||||||
|
S3|U34|L04|257|127
|
||||||
|
S3|U34|L05|258|127
|
||||||
|
S3|U35|L01|259|127
|
||||||
|
S3|U35|L02|260|127
|
||||||
|
S3|U35|L03|261|127
|
||||||
|
S3|U35|L04|262|127
|
||||||
|
S3|U35|L05|264|127
|
||||||
|
S3|U36|L01|265|127
|
||||||
|
S3|U36|L02|266|127
|
||||||
|
S3|U36|L03|267|127
|
||||||
|
S3|U36|L04|268|127
|
||||||
|
S3|U36|L05|269|127
|
||||||
|
S4|U37|L01|270|127
|
||||||
|
S4|U37|L02|271|127
|
||||||
|
S4|U37|L03|272|127
|
||||||
|
S4|U37|L04|273|127
|
||||||
|
S4|U37|L05|274|127
|
||||||
|
S4|U38|L01|276|127
|
||||||
|
S4|U38|L02|277|127
|
||||||
|
S4|U38|L03|278|127
|
||||||
|
S4|U38|L04|279|127
|
||||||
|
S4|U38|L05|280|127
|
||||||
|
S4|U39|L01|281|127
|
||||||
|
S4|U39|L02|282|127
|
||||||
|
S4|U39|L03|283|127
|
||||||
|
S4|U39|L04|284|127
|
||||||
|
S4|U39|L05|285|127
|
||||||
|
S4|U40|L01|286|127
|
||||||
|
S4|U40|L02|287|127
|
||||||
|
S4|U40|L03|288|127
|
||||||
|
S4|U40|L04|289|127
|
||||||
|
S4|U40|L05|290|127
|
||||||
|
S4|U41|L01|291|127
|
||||||
|
S4|U41|L02|292|127
|
||||||
|
S4|U41|L03|293|127
|
||||||
|
S4|U41|L04|294|127
|
||||||
|
S4|U41|L05|295|127
|
||||||
|
S4|U42|L01|296|127
|
||||||
|
S4|U42|L02|297|127
|
||||||
|
S4|U42|L03|298|127
|
||||||
|
S4|U42|L04|299|127
|
||||||
|
S4|U42|L05|300|127
|
||||||
|
S4|U43|L01|301|127
|
||||||
|
S4|U43|L02|302|127
|
||||||
|
S4|U43|L03|303|127
|
||||||
|
S4|U43|L04|304|127
|
||||||
|
S4|U43|L05|305|127
|
||||||
|
S4|U44|L01|306|127
|
||||||
|
S4|U44|L02|307|127
|
||||||
|
S4|U44|L03|308|127
|
||||||
|
S4|U44|L04|309|127
|
||||||
|
S4|U44|L05|310|127
|
||||||
|
S4|U45|L01|312|127
|
||||||
|
S4|U45|L02|313|127
|
||||||
|
S4|U45|L03|314|127
|
||||||
|
S4|U45|L04|315|127
|
||||||
|
S4|U45|L05|316|127
|
||||||
|
S4|U46|L01|317|127
|
||||||
|
S4|U46|L02|318|127
|
||||||
|
S4|U46|L03|319|127
|
||||||
|
S4|U46|L04|320|127
|
||||||
|
S4|U46|L05|321|127
|
||||||
|
S4|U47|L01|322|127
|
||||||
|
S4|U47|L02|323|127
|
||||||
|
S4|U47|L03|324|127
|
||||||
|
S4|U47|L04|325|127
|
||||||
|
S4|U47|L05|326|127
|
||||||
|
S4|U48|L01|327|127
|
||||||
|
S4|U48|L02|328|127
|
||||||
|
S4|U48|L03|329|127
|
||||||
|
S4|U48|L04|330|127
|
||||||
|
S4|U48|L05|331|127
|
||||||
84
scripts/l2_pass_time.py
Normal file
84
scripts/l2_pass_time.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""L2 每个课时首次通关耗时统计"""
|
||||||
|
import psycopg2
|
||||||
|
import sys
|
||||||
|
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host="bj-postgres-16pob4sg.sql.tencentcdb.com",
|
||||||
|
port=28591,
|
||||||
|
user="ai_member",
|
||||||
|
password="LdfjdjL83h3h3^$&**YGG*",
|
||||||
|
dbname="vala_bi"
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# 获取 L2 所有章节
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, course_season, course_unit, course_lesson
|
||||||
|
FROM bi_level_unit_lesson
|
||||||
|
WHERE course_level = 'L2'
|
||||||
|
ORDER BY course_season, course_unit, course_lesson
|
||||||
|
""")
|
||||||
|
chapters = cur.fetchall()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for cid, season, unit, lesson in chapters:
|
||||||
|
# 从8张分表收集该章节的首次完成记录
|
||||||
|
first_dones = {} # (user_id, chapter_unique_id) -> first_done_time
|
||||||
|
for tbl_idx in range(8):
|
||||||
|
cur.execute(f"""
|
||||||
|
SELECT user_id, chapter_unique_id, MIN(created_at)
|
||||||
|
FROM bi_user_chapter_play_record_{tbl_idx}
|
||||||
|
WHERE play_status = 1 AND chapter_id = {cid}
|
||||||
|
GROUP BY user_id, chapter_unique_id
|
||||||
|
""")
|
||||||
|
for row in cur.fetchall():
|
||||||
|
uid, cu_id, ts = row
|
||||||
|
key = (uid, cu_id)
|
||||||
|
if key not in first_dones or ts < first_dones[key][0]:
|
||||||
|
first_dones[key] = (ts, cu_id)
|
||||||
|
|
||||||
|
if not first_dones:
|
||||||
|
results.append((season, unit, lesson, cid, 0, 0.0))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 对每个首次完成记录,统计组件总耗时
|
||||||
|
total_times = []
|
||||||
|
for (uid, cu_id), (_, cu_id) in first_dones.items():
|
||||||
|
total_ms = 0
|
||||||
|
for comp_idx in range(8):
|
||||||
|
cur.execute(f"""
|
||||||
|
SELECT COALESCE(SUM(interval_time), 0)
|
||||||
|
FROM bi_user_component_play_record_{comp_idx}
|
||||||
|
WHERE chapter_unique_id = %s
|
||||||
|
""", (cu_id,))
|
||||||
|
total_ms += cur.fetchone()[0] or 0
|
||||||
|
total_times.append(total_ms)
|
||||||
|
|
||||||
|
avg_min = sum(total_times) / len(total_times) / 60000.0
|
||||||
|
results.append((season, unit, lesson, cid, len(total_times), round(avg_min, 1)))
|
||||||
|
|
||||||
|
print(f" {season} {unit} {lesson} (id={cid}): {len(total_times)}人, 平均{round(avg_min, 1)}分钟")
|
||||||
|
|
||||||
|
# 输出汇总
|
||||||
|
print("\n===== 汇总 =====")
|
||||||
|
print(f"{'Season':<6} {'Unit':<6} {'Lesson':<8} {'ChapterID':<10} {'完成人数':<10} {'平均用时(分钟)':<15}")
|
||||||
|
for r in results:
|
||||||
|
print(f"{r[0]:<6} {r[1]:<6} {r[2]:<8} {r[3]:<10} {r[4]:<10} {r[5]:<15}")
|
||||||
|
|
||||||
|
# 按单元汇总
|
||||||
|
from collections import defaultdict
|
||||||
|
unit_stats = defaultdict(lambda: {"count": 0, "avg": 0.0, "lessons": 0})
|
||||||
|
for r in results:
|
||||||
|
key = f"{r[0]}_{r[1]}"
|
||||||
|
unit_stats[key]["count"] += r[4]
|
||||||
|
unit_stats[key]["avg"] += r[5]
|
||||||
|
unit_stats[key]["lessons"] += 1
|
||||||
|
|
||||||
|
print("\n===== 按单元汇总 =====")
|
||||||
|
print(f"{'Unit':<10} {'完成人数(合计)':<15} {'平均用时(分钟)':<15}")
|
||||||
|
for key, stats in sorted(unit_stats.items()):
|
||||||
|
print(f"{key:<10} {stats['count']:<15} {round(stats['avg']/stats['lessons'], 1):<15}")
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
48
scripts/l2_pass_time.sh
Executable file
48
scripts/l2_pass_time.sh
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# L2 每个课时首次通关耗时统计
|
||||||
|
export PGPASSWORD='LdfjdjL83h3h3^$&**YGG*'
|
||||||
|
PGHOST='bj-postgres-16pob4sg.sql.tencentcdb.com'
|
||||||
|
PGPORT='28591'
|
||||||
|
PGUSER='ai_member'
|
||||||
|
PGDATABASE='vala_bi'
|
||||||
|
PSQL="psql -h \$PGHOST -p \$PGPORT -U \$PGUSER -d \$PGDATABASE -t -A"
|
||||||
|
|
||||||
|
# 获取所有 L2 章节
|
||||||
|
$PSQL -c "SELECT id FROM bi_level_unit_lesson WHERE course_level='L2' ORDER BY id;" > /tmp/l2_ch_ids.txt
|
||||||
|
|
||||||
|
echo "chapter_id|season|unit|lesson|完成人数|平均用时_分钟"
|
||||||
|
while read cid; do
|
||||||
|
# 获取章节信息
|
||||||
|
info=$($PSQL -c "SELECT course_season||'|'||course_unit||'|'||course_lesson FROM bi_level_unit_lesson WHERE id=$cid;")
|
||||||
|
|
||||||
|
# 统计首次完成: 从8张分表收集
|
||||||
|
result=$($PSQL -c "
|
||||||
|
WITH fd AS (
|
||||||
|
SELECT user_id, chapter_unique_id FROM bi_user_chapter_play_record_0 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id, chapter_unique_id FROM bi_user_chapter_play_record_1 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id, chapter_unique_id FROM bi_user_chapter_play_record_2 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id, chapter_unique_id FROM bi_user_chapter_play_record_3 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id, chapter_unique_id FROM bi_user_chapter_play_record_4 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id, chapter_unique_id FROM bi_user_chapter_play_record_5 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id, chapter_unique_id FROM bi_user_chapter_play_record_6 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id, chapter_unique_id FROM bi_user_chapter_play_record_7 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
),
|
||||||
|
times AS (
|
||||||
|
SELECT fd.chapter_unique_id, COALESCE(SUM(c.interval_time),0) AS total_ms FROM fd
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT chapter_unique_id, interval_time FROM bi_user_component_play_record_0
|
||||||
|
UNION ALL SELECT chapter_unique_id, interval_time FROM bi_user_component_play_record_1
|
||||||
|
UNION ALL SELECT chapter_unique_id, interval_time FROM bi_user_component_play_record_2
|
||||||
|
UNION ALL SELECT chapter_unique_id, interval_time FROM bi_user_component_play_record_3
|
||||||
|
UNION ALL SELECT chapter_unique_id, interval_time FROM bi_user_component_play_record_4
|
||||||
|
UNION ALL SELECT chapter_unique_id, interval_time FROM bi_user_component_play_record_5
|
||||||
|
UNION ALL SELECT chapter_unique_id, interval_time FROM bi_user_component_play_record_6
|
||||||
|
UNION ALL SELECT chapter_unique_id, interval_time FROM bi_user_component_play_record_7
|
||||||
|
) c ON fd.chapter_unique_id = c.chapter_unique_id
|
||||||
|
GROUP BY fd.chapter_unique_id
|
||||||
|
)
|
||||||
|
SELECT COUNT(*), ROUND(AVG(total_ms)/60000.0, 1) FROM times WHERE total_ms > 0;
|
||||||
|
" 2>&1)
|
||||||
|
|
||||||
|
echo "$cid|$info|$result"
|
||||||
|
done < /tmp/l2_ch_ids.txt
|
||||||
89
scripts/l2_pass_time.sql
Normal file
89
scripts/l2_pass_time.sql
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
-- L2 每个课时首次通关耗时统计
|
||||||
|
-- 遍历 8 张 user_chapter_play_record 分表
|
||||||
|
|
||||||
|
WITH l2_chapters AS (
|
||||||
|
SELECT id AS chapter_id, course_season, course_unit, course_lesson
|
||||||
|
FROM bi_level_unit_lesson
|
||||||
|
WHERE course_level = 'L2'
|
||||||
|
),
|
||||||
|
all_first_done AS (
|
||||||
|
SELECT chapter_id, user_id, chapter_unique_id, first_done_time
|
||||||
|
FROM (
|
||||||
|
SELECT chapter_id, user_id, chapter_unique_id, MIN(created_at) AS first_done_time
|
||||||
|
FROM bi_user_chapter_play_record_0
|
||||||
|
WHERE play_status = 1 AND chapter_id IN (SELECT chapter_id FROM l2_chapters)
|
||||||
|
GROUP BY chapter_id, user_id, chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT chapter_id, user_id, chapter_unique_id, MIN(created_at) AS first_done_time
|
||||||
|
FROM bi_user_chapter_play_record_1
|
||||||
|
WHERE play_status = 1 AND chapter_id IN (SELECT chapter_id FROM l2_chapters)
|
||||||
|
GROUP BY chapter_id, user_id, chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT chapter_id, user_id, chapter_unique_id, MIN(created_at) AS first_done_time
|
||||||
|
FROM bi_user_chapter_play_record_2
|
||||||
|
WHERE play_status = 1 AND chapter_id IN (SELECT chapter_id FROM l2_chapters)
|
||||||
|
GROUP BY chapter_id, user_id, chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT chapter_id, user_id, chapter_unique_id, MIN(created_at) AS first_done_time
|
||||||
|
FROM bi_user_chapter_play_record_3
|
||||||
|
WHERE play_status = 1 AND chapter_id IN (SELECT chapter_id FROM l2_chapters)
|
||||||
|
GROUP BY chapter_id, user_id, chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT chapter_id, user_id, chapter_unique_id, MIN(created_at) AS first_done_time
|
||||||
|
FROM bi_user_chapter_play_record_4
|
||||||
|
WHERE play_status = 1 AND chapter_id IN (SELECT chapter_id FROM l2_chapters)
|
||||||
|
GROUP BY chapter_id, user_id, chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT chapter_id, user_id, chapter_unique_id, MIN(created_at) AS first_done_time
|
||||||
|
FROM bi_user_chapter_play_record_5
|
||||||
|
WHERE play_status = 1 AND chapter_id IN (SELECT chapter_id FROM l2_chapters)
|
||||||
|
GROUP BY chapter_id, user_id, chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT chapter_id, user_id, chapter_unique_id, MIN(created_at) AS first_done_time
|
||||||
|
FROM bi_user_chapter_play_record_6
|
||||||
|
WHERE play_status = 1 AND chapter_id IN (SELECT chapter_id FROM l2_chapters)
|
||||||
|
GROUP BY chapter_id, user_id, chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT chapter_id, user_id, chapter_unique_id, MIN(created_at) AS first_done_time
|
||||||
|
FROM bi_user_chapter_play_record_7
|
||||||
|
WHERE play_status = 1 AND chapter_id IN (SELECT chapter_id FROM l2_chapters)
|
||||||
|
GROUP BY chapter_id, user_id, chapter_unique_id
|
||||||
|
) t
|
||||||
|
),
|
||||||
|
chapter_time AS (
|
||||||
|
SELECT
|
||||||
|
fd.chapter_id,
|
||||||
|
fd.chapter_unique_id,
|
||||||
|
SUM(comp.interval_time) AS total_ms
|
||||||
|
FROM all_first_done fd
|
||||||
|
JOIN LATERAL (
|
||||||
|
SELECT SUM(c.interval_time) AS interval_time
|
||||||
|
FROM bi_user_component_play_record_0 c WHERE c.chapter_unique_id = fd.chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT SUM(c.interval_time) FROM bi_user_component_play_record_1 c WHERE c.chapter_unique_id = fd.chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT SUM(c.interval_time) FROM bi_user_component_play_record_2 c WHERE c.chapter_unique_id = fd.chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT SUM(c.interval_time) FROM bi_user_component_play_record_3 c WHERE c.chapter_unique_id = fd.chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT SUM(c.interval_time) FROM bi_user_component_play_record_4 c WHERE c.chapter_unique_id = fd.chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT SUM(c.interval_time) FROM bi_user_component_play_record_5 c WHERE c.chapter_unique_id = fd.chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT SUM(c.interval_time) FROM bi_user_component_play_record_6 c WHERE c.chapter_unique_id = fd.chapter_unique_id
|
||||||
|
UNION ALL
|
||||||
|
SELECT SUM(c.interval_time) FROM bi_user_component_play_record_7 c WHERE c.chapter_unique_id = fd.chapter_unique_id
|
||||||
|
) comp ON true
|
||||||
|
GROUP BY fd.chapter_id, fd.chapter_unique_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
lc.course_season,
|
||||||
|
lc.course_unit,
|
||||||
|
lc.course_lesson,
|
||||||
|
lc.chapter_id,
|
||||||
|
COUNT(*) AS 完成人数,
|
||||||
|
ROUND(AVG(ct.total_ms) / 60000.0, 1) AS 平均通关用时_分钟
|
||||||
|
FROM l2_chapters lc
|
||||||
|
LEFT JOIN chapter_time ct ON lc.chapter_id = ct.chapter_id
|
||||||
|
GROUP BY lc.course_season, lc.course_unit, lc.course_lesson, lc.chapter_id
|
||||||
|
ORDER BY lc.course_season, lc.course_unit, lc.course_lesson;
|
||||||
27
scripts/l2_pass_time_v2.sh
Normal file
27
scripts/l2_pass_time_v2.sh
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# L2 每个课时首次通关耗时统计 - 优化版
|
||||||
|
export PGPASSWORD='LdfjdjL83h3h3^$&**YGG*'
|
||||||
|
PSQL="psql -h bj-postgres-16pob4sg.sql.tencentcdb.com -p 28591 -U ai_member -d vala_bi -t -A"
|
||||||
|
|
||||||
|
echo "chapter_id|season|unit|lesson|完成人数|平均用时_分钟"
|
||||||
|
|
||||||
|
# 获取章节列表
|
||||||
|
while IFS='|' read -r cid season unit lesson; do
|
||||||
|
# 查首次完成记录,来自8张分表 (不查 component 耗时那步太慢,直接用 chapter 表做简单计数)
|
||||||
|
# 先只统计完成人数
|
||||||
|
count=$($PSQL -c "
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM (
|
||||||
|
SELECT user_id FROM bi_user_chapter_play_record_0 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id FROM bi_user_chapter_play_record_1 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id FROM bi_user_chapter_play_record_2 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id FROM bi_user_chapter_play_record_3 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id FROM bi_user_chapter_play_record_4 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id FROM bi_user_chapter_play_record_5 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id FROM bi_user_chapter_play_record_6 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
UNION ALL SELECT user_id FROM bi_user_chapter_play_record_7 WHERE play_status=1 AND chapter_id=$cid
|
||||||
|
) t;
|
||||||
|
" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
echo "$cid|$season|$unit|$lesson|${count:-0}|NA"
|
||||||
|
done < <($PSQL -c "SELECT id, course_season, course_unit, course_lesson FROM bi_level_unit_lesson WHERE course_level='L2' ORDER BY id;")
|
||||||
268
scripts/lead_user_analysis.py
Normal file
268
scripts/lead_user_analysis.py
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
销售线索用户分析报表
|
||||||
|
|
||||||
|
输入:Excel文件,包含「用户ID」列
|
||||||
|
输出:每个用户+角色一行,包含注册信息、序章完成情况、购买和退款信息
|
||||||
|
|
||||||
|
修正口径:
|
||||||
|
- 体验课 = 固定10节课:L1 U00 L01-L05 (chapter_id: 343,344,345,346,348) + L2 U00 L01-L05 (55-59)
|
||||||
|
- 完成时间 = play_status=1 的最早 updated_at
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import psycopg2
|
||||||
|
import pandas as pd
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# ── 数据库 ──
|
||||||
|
DB_HOST = "bj-postgres-16pob4sg.sql.tencentcdb.com"
|
||||||
|
DB_PORT = 28591
|
||||||
|
DB_USER = "ai_member"
|
||||||
|
DB_NAME = "vala_bi"
|
||||||
|
|
||||||
|
def get_password():
|
||||||
|
secrets_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "secrets.env")
|
||||||
|
# also try env
|
||||||
|
pw = os.environ.get("PG_ONLINE_PASSWORD", "")
|
||||||
|
if pw:
|
||||||
|
return pw
|
||||||
|
if os.path.exists(secrets_path):
|
||||||
|
with open(secrets_path) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith("PG_ONLINE_PASSWORD="):
|
||||||
|
return line.strip().split("=", 1)[1].strip("'\"")
|
||||||
|
raise RuntimeError("PG_ONLINE_PASSWORD not found")
|
||||||
|
|
||||||
|
def get_conn():
|
||||||
|
return psycopg2.connect(host=DB_HOST, port=DB_PORT, user=DB_USER, password=get_password(), dbname=DB_NAME, connect_timeout=30)
|
||||||
|
|
||||||
|
# ── 序章 chapter_id ──
|
||||||
|
# L1 S0 U00 L01..L05
|
||||||
|
L1_CHAPTERS = {343: "L1_U00_L01", 344: "L1_U00_L02", 345: "L1_U00_L03", 346: "L1_U00_L04", 348: "L1_U00_L05"}
|
||||||
|
# L2 S0 U00 L01..L05
|
||||||
|
L2_CHAPTERS = {55: "L2_U00_L01", 56: "L2_U00_L02", 57: "L2_U00_L03", 58: "L2_U00_L04", 59: "L2_U00_L05"}
|
||||||
|
|
||||||
|
ALL_CHAPTERS = {**L1_CHAPTERS, **L2_CHAPTERS}
|
||||||
|
COL_ORDER = [
|
||||||
|
"L1_U00_L01_完成时间", "L1_U00_L02_完成时间", "L1_U00_L03_完成时间",
|
||||||
|
"L1_U00_L04_完成时间", "L1_U00_L05_完成时间",
|
||||||
|
"L2_U00_L01_完成时间", "L2_U00_L02_完成时间", "L2_U00_L03_完成时间",
|
||||||
|
"L2_U00_L04_完成时间", "L2_U00_L05_完成时间",
|
||||||
|
]
|
||||||
|
|
||||||
|
def main():
|
||||||
|
input_file = sys.argv[1] if len(sys.argv) > 1 else "/root/.openclaw/media/inbound/3æ_è_çº_çº_ç---d9a41af7-b100-43a7-a983-d4fd1f164023.xlsx"
|
||||||
|
print(f"读取输入文件: {input_file}")
|
||||||
|
df_input = pd.read_excel(input_file, dtype=str)
|
||||||
|
user_ids_raw = df_input["用户ID"].dropna().unique().tolist()
|
||||||
|
account_ids = [int(x) for x in user_ids_raw]
|
||||||
|
print(f"共 {len(account_ids)} 个用户ID")
|
||||||
|
|
||||||
|
conn = get_conn()
|
||||||
|
|
||||||
|
# ── 1. 获取用户基本信息(account) ──
|
||||||
|
placeholders = ",".join(["%s"] * len(account_ids))
|
||||||
|
df_accounts = pd.read_sql_query(
|
||||||
|
f"SELECT id AS account_id, created_at AS reg_time FROM bi_vala_app_account WHERE id IN ({placeholders}) AND status = 1",
|
||||||
|
conn, params=account_ids
|
||||||
|
)
|
||||||
|
print(f" 有效账户: {len(df_accounts)}")
|
||||||
|
|
||||||
|
# ── 2. 获取角色(排除 nickname 为空的) ──
|
||||||
|
df_chars = pd.read_sql_query(
|
||||||
|
f"SELECT id AS character_id, account_id, nickname, created_at AS char_created_at FROM bi_vala_app_character WHERE account_id IN ({placeholders}) AND (nickname IS NOT NULL AND nickname != '') AND deleted_at IS NULL",
|
||||||
|
conn, params=account_ids
|
||||||
|
)
|
||||||
|
print(f" 有效角色: {len(df_chars)}")
|
||||||
|
|
||||||
|
if df_chars.empty:
|
||||||
|
print("没有有效的角色,退出")
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── 3. 查询课时完成记录(10个 chapter_id,8个分表) ──
|
||||||
|
chapter_ids = list(ALL_CHAPTERS.keys())
|
||||||
|
char_id_set = set(df_chars["character_id"].tolist())
|
||||||
|
char_play = defaultdict(dict)
|
||||||
|
total_play = 0
|
||||||
|
|
||||||
|
for tbl_idx in range(8):
|
||||||
|
table = f"bi_user_chapter_play_record_{tbl_idx}"
|
||||||
|
sql = f"""
|
||||||
|
SELECT user_id, chapter_id, MIN(updated_at) AS done_time
|
||||||
|
FROM {table}
|
||||||
|
WHERE chapter_id IN %s
|
||||||
|
AND play_status = 1
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
GROUP BY user_id, chapter_id
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(sql, (tuple(chapter_ids),))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
total_play += len(rows)
|
||||||
|
for user_id, ch_id, done_time in rows:
|
||||||
|
if user_id in char_id_set:
|
||||||
|
label = ALL_CHAPTERS.get(ch_id)
|
||||||
|
if label:
|
||||||
|
char_play[user_id][ch_id] = done_time
|
||||||
|
except Exception as e:
|
||||||
|
print(f" 警告: {table} 查询失败: {e}")
|
||||||
|
|
||||||
|
print(f" 课时完成记录: {total_play} 条, 匹配角色: {len(char_play)}")
|
||||||
|
|
||||||
|
# ── 4. 订单信息 ──
|
||||||
|
df_orders = pd.read_sql_query(
|
||||||
|
f"""
|
||||||
|
SELECT o.account_id, o.pay_success_date, o.goods_name, o.pay_amount_int, o.key_from, o.trade_no, o.order_status
|
||||||
|
FROM bi_vala_order o
|
||||||
|
INNER JOIN bi_vala_app_account a ON o.account_id = a.id AND a.status = 1
|
||||||
|
WHERE o.account_id IN ({placeholders})
|
||||||
|
AND o.deleted_at IS NULL
|
||||||
|
ORDER BY o.account_id, o.pay_success_date
|
||||||
|
""",
|
||||||
|
conn, params=account_ids
|
||||||
|
)
|
||||||
|
print(f" 订单记录: {len(df_orders)}")
|
||||||
|
|
||||||
|
# ── 5. 退款信息 ──
|
||||||
|
all_trade_nos = df_orders["trade_no"].dropna().unique().tolist()
|
||||||
|
refund_map = {}
|
||||||
|
if all_trade_nos:
|
||||||
|
# 分批查询(防止IN子句过长)
|
||||||
|
for i in range(0, len(all_trade_nos), 500):
|
||||||
|
batch = all_trade_nos[i:i+500]
|
||||||
|
ph = ",".join(["%s"] * len(batch))
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(f"SELECT trade_no, SUM(refund_amount_int) FROM bi_refund_order WHERE trade_no IN ({ph}) AND status = 3 AND deleted_at IS NULL GROUP BY trade_no", batch)
|
||||||
|
for trade_no, amt in cur.fetchall():
|
||||||
|
refund_map[trade_no] = amt
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
print(f" 退费记录: {len(refund_map)} 条")
|
||||||
|
|
||||||
|
# ── 6. 原始文件线索信息 ──
|
||||||
|
df_input["用户ID_int"] = df_input["用户ID"].astype(int)
|
||||||
|
df_input_map = df_input[["用户ID_int", "线索进线日期", "销售"]].drop_duplicates(subset="用户ID_int")
|
||||||
|
|
||||||
|
# ── 7. 组装结果 ──
|
||||||
|
df_chars = df_chars.merge(df_accounts, on="account_id", how="left")
|
||||||
|
|
||||||
|
# 去掉时区信息(Excel不支持)
|
||||||
|
for col in ["reg_time", "char_created_at"]:
|
||||||
|
if col in df_chars.columns:
|
||||||
|
df_chars[col] = pd.to_datetime(df_chars[col]).dt.tz_localize(None)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for _, char_row in df_chars.iterrows():
|
||||||
|
account_id = int(char_row["account_id"])
|
||||||
|
char_id = int(char_row["character_id"])
|
||||||
|
reg_time = char_row["reg_time"]
|
||||||
|
char_created_at = char_row["char_created_at"]
|
||||||
|
|
||||||
|
# 课时完成
|
||||||
|
play_map = char_play.get(char_id, {})
|
||||||
|
row_data = {
|
||||||
|
"用户ID": account_id,
|
||||||
|
"角色ID": char_id,
|
||||||
|
"用户注册时间": reg_time,
|
||||||
|
"角色创建时间": char_created_at,
|
||||||
|
}
|
||||||
|
for col_label in COL_ORDER:
|
||||||
|
# 找到对应的 chapter_id(ALL_CHAPTERS 的 value 是去掉 "_完成时间" 后缀的)
|
||||||
|
ch_id = None
|
||||||
|
lbl_key = col_label.replace("_完成时间", "")
|
||||||
|
for cid, lbl in ALL_CHAPTERS.items():
|
||||||
|
if lbl == lbl_key:
|
||||||
|
ch_id = cid
|
||||||
|
break
|
||||||
|
done_time = play_map.get(ch_id, None)
|
||||||
|
if done_time is not None:
|
||||||
|
done_time = done_time.replace(tzinfo=None)
|
||||||
|
row_data[col_label] = done_time
|
||||||
|
|
||||||
|
rows.append(row_data)
|
||||||
|
|
||||||
|
df_result = pd.DataFrame(rows)
|
||||||
|
|
||||||
|
# ── 7. 合并订单 & 退款 ──
|
||||||
|
# 按 account_id 聚合
|
||||||
|
order_agg = df_orders.groupby("account_id").agg(
|
||||||
|
购买时间=("pay_success_date", lambda x: ";".join(str(v) for v in x if pd.notna(v))),
|
||||||
|
购买课包名称=("goods_name", lambda x: ";".join(str(v) for v in x if pd.notna(v))),
|
||||||
|
支付金额=("pay_amount_int", lambda x: ";".join(str(v/100) for v in x if pd.notna(v))),
|
||||||
|
购买渠道key_from=("key_from", lambda x: ";".join(str(v) for v in x if pd.notna(v))),
|
||||||
|
trade_nos=("trade_no", lambda x: list(x)),
|
||||||
|
).reset_index()
|
||||||
|
|
||||||
|
# 退款判断
|
||||||
|
def calc_refund(row):
|
||||||
|
has_refund = False
|
||||||
|
total_refund = 0
|
||||||
|
for tn in row["trade_nos"]:
|
||||||
|
if tn in refund_map:
|
||||||
|
has_refund = True
|
||||||
|
total_refund += refund_map[tn]
|
||||||
|
# 也检查 order_status=4
|
||||||
|
for tn in row["trade_nos"]:
|
||||||
|
if tn not in refund_map:
|
||||||
|
# 查订单状态
|
||||||
|
pass # order_status 已在订单表中
|
||||||
|
return pd.Series({"是否退款": "是" if has_refund else "否", "退款金额": total_refund / 100.0})
|
||||||
|
|
||||||
|
refund_info = order_agg.apply(calc_refund, axis=1)
|
||||||
|
order_agg = pd.concat([order_agg[["account_id", "购买时间", "购买课包名称", "支付金额", "购买渠道key_from"]], refund_info], axis=1)
|
||||||
|
|
||||||
|
# 合并到结果
|
||||||
|
df_result["account_id_int"] = df_result["用户ID"].astype(int)
|
||||||
|
df_result = df_result.merge(order_agg, left_on="account_id_int", right_on="account_id", how="left")
|
||||||
|
df_result.drop(columns=["account_id"], inplace=True, errors="ignore")
|
||||||
|
|
||||||
|
# 合并原始文件的线索进线日期和销售
|
||||||
|
df_result = df_result.merge(df_input_map, left_on="account_id_int", right_on="用户ID_int", how="left")
|
||||||
|
df_result.drop(columns=["account_id_int", "用户ID_int"], inplace=True, errors="ignore")
|
||||||
|
|
||||||
|
# 填充空值
|
||||||
|
df_result["购买时间"] = df_result["购买时间"].fillna("")
|
||||||
|
df_result["购买课包名称"] = df_result["购买课包名称"].fillna("")
|
||||||
|
df_result["支付金额"] = df_result["支付金额"].fillna("")
|
||||||
|
df_result["购买渠道key_from"] = df_result["购买渠道key_from"].fillna("")
|
||||||
|
df_result["是否退款"] = df_result["是否退款"].fillna("否")
|
||||||
|
df_result["退款金额"] = df_result["退款金额"].fillna(0.0)
|
||||||
|
df_result["线索进线日期"] = df_result["线索进线日期"].fillna("")
|
||||||
|
df_result["销售"] = df_result["销售"].fillna("")
|
||||||
|
|
||||||
|
# 按用户ID升序排列
|
||||||
|
df_result = df_result.sort_values(by=["用户ID", "角色ID"], ascending=True).reset_index(drop=True)
|
||||||
|
|
||||||
|
# 调整列顺序:原始字段靠前
|
||||||
|
col_order = ["用户ID", "线索进线日期", "销售", "角色ID", "用户注册时间", "角色创建时间"] + COL_ORDER + ["购买时间", "购买课包名称", "支付金额", "购买渠道key_from", "是否退款", "退款金额"]
|
||||||
|
# 只保留实际存在的列
|
||||||
|
col_order = [c for c in col_order if c in df_result.columns]
|
||||||
|
df_result = df_result[col_order]
|
||||||
|
|
||||||
|
# ── 8. 输出 ──
|
||||||
|
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "output")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
output_path = os.path.join(output_dir, "销售线索_用户分析.xlsx")
|
||||||
|
|
||||||
|
with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
|
||||||
|
df_result.to_excel(writer, sheet_name="用户分析", index=False)
|
||||||
|
|
||||||
|
print(f"\n✅ 报表已生成: {output_path}")
|
||||||
|
print(f" 总行数: {len(df_result)}")
|
||||||
|
purchased = sum(1 for v in df_result["购买时间"] if v)
|
||||||
|
print(f" 有购买记录: {purchased}")
|
||||||
|
refunded = sum(1 for v in df_result["是否退款"] if v == "是")
|
||||||
|
print(f" 有退款: {refunded}")
|
||||||
|
done_l1 = sum(1 for v in df_result["L1_U00_L01_完成时间"] if pd.notna(v))
|
||||||
|
done_l2 = sum(1 for v in df_result["L2_U00_L01_完成时间"] if pd.notna(v))
|
||||||
|
print(f" 完成L1序章(U00 L01): {done_l1} 个角色")
|
||||||
|
print(f" 完成L2序章(U00 L01): {done_l2} 个角色")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
308
scripts/monthly_channel_order_report.py
Normal file
308
scripts/monthly_channel_order_report.py
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
月度学而思/讯飞/希沃/京东方渠道订单汇总报表
|
||||||
|
|
||||||
|
功能:
|
||||||
|
- 按指定月份,汇总这4个渠道每一天的:
|
||||||
|
新增注册数、订单数(剔除退费)、GMV(有效订单金额加和)
|
||||||
|
- 自动剔除测试账号
|
||||||
|
- 输出 Excel 文件到 output/ 目录
|
||||||
|
|
||||||
|
渠道映射(sale_channel,仅对 key_from='app-active-h5-0-0' 的订单生效):
|
||||||
|
21 = 学而思
|
||||||
|
22 = 讯飞
|
||||||
|
26 = 希沃
|
||||||
|
27 = 京东方
|
||||||
|
|
||||||
|
新增注册:download_channel LIKE '%学而思%' / '%讯飞%' / '%希沃%' / '%京东方%'
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import calendar
|
||||||
|
import psycopg2
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# ── 数据库连接 ──────────────────────────────────────────────
|
||||||
|
DB_HOST = "bj-postgres-16pob4sg.sql.tencentcdb.com"
|
||||||
|
DB_PORT = 28591
|
||||||
|
DB_USER = "ai_member"
|
||||||
|
DB_NAME = "vala_bi"
|
||||||
|
DB_PASSWORD = os.environ.get("PG_ONLINE_PASSWORD", "")
|
||||||
|
|
||||||
|
if not DB_PASSWORD:
|
||||||
|
# 尝试从 secrets.env 读取
|
||||||
|
secrets_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "secrets.env")
|
||||||
|
if os.path.exists(secrets_path):
|
||||||
|
with open(secrets_path) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith("PG_ONLINE_PASSWORD="):
|
||||||
|
DB_PASSWORD = line.strip().split("=", 1)[1].strip("'\"")
|
||||||
|
break
|
||||||
|
|
||||||
|
# ── 渠道配置 ────────────────────────────────────────────────
|
||||||
|
# (sale_channel值, 渠道中文名, CPS分成比例, None表示不用CPS分成)
|
||||||
|
CHANNELS = [
|
||||||
|
("21", "学而思", 0.45),
|
||||||
|
("22", "讯飞", None),
|
||||||
|
("26", "希沃", 0.40),
|
||||||
|
("27", "京东方", 0.30),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ── 参数 ────────────────────────────────────────────────────
|
||||||
|
def parse_month(arg: str) -> str:
|
||||||
|
"""将用户输入解析为 YYYY-MM 格式,默认当前月"""
|
||||||
|
arg = arg.strip()
|
||||||
|
if len(arg) == 7 and arg[4] == "-":
|
||||||
|
return arg
|
||||||
|
if len(arg) == 6 and arg.isdigit():
|
||||||
|
return f"{arg[:4]}-{arg[4:]}"
|
||||||
|
# 默认当前月
|
||||||
|
return datetime.now().strftime("%Y-%m")
|
||||||
|
|
||||||
|
|
||||||
|
def get_conn():
|
||||||
|
return psycopg2.connect(
|
||||||
|
host=DB_HOST,
|
||||||
|
port=DB_PORT,
|
||||||
|
user=DB_USER,
|
||||||
|
password=DB_PASSWORD,
|
||||||
|
dbname=DB_NAME,
|
||||||
|
connect_timeout=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── SQL 定义 ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# 新增注册:按天 + 渠道汇总(这4个渠道 download_channel 关键字包含匹配)
|
||||||
|
SQL_NEW_USERS = """
|
||||||
|
SELECT
|
||||||
|
DATE(created_at) AS reg_date,
|
||||||
|
CASE
|
||||||
|
WHEN download_channel LIKE '%%学而思%%' THEN '学而思'
|
||||||
|
WHEN download_channel LIKE '%%讯飞%%' THEN '讯飞'
|
||||||
|
WHEN download_channel LIKE '%%希沃%%' THEN '希沃'
|
||||||
|
WHEN download_channel LIKE '%%京东方%%' THEN '京东方'
|
||||||
|
END AS channel_name,
|
||||||
|
COUNT(*) AS new_users
|
||||||
|
FROM bi_vala_app_account
|
||||||
|
WHERE status = 1
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND created_at >= %s
|
||||||
|
AND created_at < %s
|
||||||
|
AND (
|
||||||
|
download_channel LIKE '%%学而思%%'
|
||||||
|
OR download_channel LIKE '%%讯飞%%'
|
||||||
|
OR download_channel LIKE '%%希沃%%'
|
||||||
|
OR download_channel LIKE '%%京东方%%'
|
||||||
|
)
|
||||||
|
GROUP BY reg_date, channel_name
|
||||||
|
ORDER BY reg_date;
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 有效订单(剔除全部退费订单):端内 + sale_channel 在 21/22/26/27
|
||||||
|
# 剔除逻辑:排除 order_status=4 且 bi_refund_order.status=3 的订单
|
||||||
|
# 逐条查出,在 Python 中聚合(因为需要保留 trade_no 明细)
|
||||||
|
SQL_VALID_ORDERS = """
|
||||||
|
SELECT
|
||||||
|
DATE(o.pay_success_date) AS order_date,
|
||||||
|
CASE o.sale_channel
|
||||||
|
WHEN 21 THEN '学而思'
|
||||||
|
WHEN 22 THEN '讯飞'
|
||||||
|
WHEN 26 THEN '希沃'
|
||||||
|
WHEN 27 THEN '京东方'
|
||||||
|
END AS channel_name,
|
||||||
|
o.pay_amount_int,
|
||||||
|
o.trade_no
|
||||||
|
FROM bi_vala_order o
|
||||||
|
INNER JOIN bi_vala_app_account a ON o.account_id = a.id AND a.status = 1
|
||||||
|
WHERE o.key_from = 'app-active-h5-0-0'
|
||||||
|
AND o.sale_channel IN (21, 22, 26, 27)
|
||||||
|
AND o.pay_success_date >= %s
|
||||||
|
AND o.pay_success_date < %s
|
||||||
|
AND o.deleted_at IS NULL
|
||||||
|
-- 剔除已退费订单
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM bi_refund_order r
|
||||||
|
WHERE r.trade_no = o.trade_no
|
||||||
|
AND r.status = 3
|
||||||
|
AND r.deleted_at IS NULL
|
||||||
|
)
|
||||||
|
AND o.order_status != 4
|
||||||
|
ORDER BY order_date, channel_name;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# ── 主逻辑 ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def run(month_str: str):
|
||||||
|
print(f"开始生成 {month_str} 学而思/讯飞/希沃/京东方渠道汇总报表...")
|
||||||
|
|
||||||
|
# 解析月份起止
|
||||||
|
year, month = int(month_str[:4]), int(month_str[5:])
|
||||||
|
_, last_day = calendar.monthrange(year, month)
|
||||||
|
start_ts = f"{month_str}-01 00:00:00+08:00"
|
||||||
|
# 下月1号(用 datetime 自动处理跨月/跨年)
|
||||||
|
next_month = datetime(year, month, 1) + timedelta(days=last_day)
|
||||||
|
end_ts = next_month.strftime("%Y-%m-%d 00:00:00+08:00")
|
||||||
|
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
# 1. 新增注册
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(SQL_NEW_USERS, (start_ts, end_ts))
|
||||||
|
rows_new = cur.fetchall()
|
||||||
|
df_new = pd.DataFrame(rows_new, columns=[desc[0] for desc in cur.description]) if rows_new else pd.DataFrame(columns=["reg_date", "channel_name", "new_users"])
|
||||||
|
cur.close()
|
||||||
|
print(f" 新增注册记录: {len(df_new)} 行")
|
||||||
|
|
||||||
|
# 2. 有效订单(逐条)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(SQL_VALID_ORDERS, (start_ts, end_ts))
|
||||||
|
rows_ord = cur.fetchall()
|
||||||
|
df_orders_raw = pd.DataFrame(rows_ord, columns=[desc[0] for desc in cur.description]) if rows_ord else pd.DataFrame(columns=["order_date", "channel_name", "pay_amount_int", "trade_no"])
|
||||||
|
cur.close()
|
||||||
|
print(f" 有效订单记录: {len(df_orders_raw)} 行")
|
||||||
|
|
||||||
|
# 按 channel_name + order_date 聚合成每日汇总
|
||||||
|
if not df_orders_raw.empty:
|
||||||
|
df_orders = df_orders_raw.groupby(["channel_name", "order_date"], as_index=False).agg(
|
||||||
|
order_cnt=("pay_amount_int", "count"),
|
||||||
|
gmv_cents=("pay_amount_int", "sum"),
|
||||||
|
trade_nos=("trade_no", lambda x: ";".join(str(t) for t in x)),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
df_orders = pd.DataFrame(columns=["channel_name", "order_date", "order_cnt", "gmv_cents", "trade_nos"])
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 3. 构建每个渠道的结果表(每一天一行)
|
||||||
|
date_range = [
|
||||||
|
f"{month_str}-{d:02d}" for d in range(1, last_day + 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
channel_names = [ch[1] for ch in CHANNELS]
|
||||||
|
channel_dfs = {}
|
||||||
|
channel_totals = {}
|
||||||
|
|
||||||
|
for ch_val, ch_name, cps_rate in CHANNELS:
|
||||||
|
if cps_rate is not None:
|
||||||
|
# ── 学而思/希沃/京东方:只展示有订单的日期,无新增注册列,有CPS分成 ──
|
||||||
|
rows = []
|
||||||
|
for d in date_range:
|
||||||
|
day_ord = df_orders[(df_orders["order_date"].astype(str) == d) & (df_orders["channel_name"] == ch_name)]
|
||||||
|
if day_ord.empty:
|
||||||
|
continue
|
||||||
|
order_cnt = int(day_ord["order_cnt"].sum())
|
||||||
|
gmv = day_ord["gmv_cents"].sum() / 100.0
|
||||||
|
trade_nos = ";".join(day_ord["trade_nos"].values)
|
||||||
|
rows.append({
|
||||||
|
"日期": d,
|
||||||
|
"订单数": order_cnt,
|
||||||
|
"GMV(元)": round(gmv, 2),
|
||||||
|
"CPS分成(元)": round(gmv * cps_rate, 2),
|
||||||
|
"订单号": trade_nos,
|
||||||
|
})
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
df_sheet = pd.DataFrame(rows)
|
||||||
|
total = {
|
||||||
|
"日期": "合计",
|
||||||
|
"订单数": df_sheet["订单数"].sum(),
|
||||||
|
"GMV(元)": round(df_sheet["GMV(元)"].sum(), 2),
|
||||||
|
"CPS分成(元)": round(df_sheet["CPS分成(元)"].sum(), 2),
|
||||||
|
"订单号": "",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
df_sheet = pd.DataFrame(columns=["日期", "订单数", "GMV(元)", "CPS分成(元)", "订单号"])
|
||||||
|
total = {"日期": "合计", "订单数": 0, "GMV(元)": 0.0, "CPS分成(元)": 0.0, "订单号": ""}
|
||||||
|
df_sheet = pd.concat([df_sheet, pd.DataFrame([total])], ignore_index=True)
|
||||||
|
channel_dfs[ch_name] = df_sheet
|
||||||
|
channel_totals[ch_name] = total
|
||||||
|
|
||||||
|
else:
|
||||||
|
# ── 讯飞:每天一行,含新增注册 + CPA分成 + 订单号 ──
|
||||||
|
rows = []
|
||||||
|
for d in date_range:
|
||||||
|
day_new = df_new[(df_new["reg_date"].astype(str) == d) & (df_new["channel_name"] == ch_name)]
|
||||||
|
new_users = int(day_new["new_users"].sum()) if not day_new.empty else 0
|
||||||
|
|
||||||
|
day_ord = df_orders[(df_orders["order_date"].astype(str) == d) & (df_orders["channel_name"] == ch_name)]
|
||||||
|
if not day_ord.empty:
|
||||||
|
order_cnt = int(day_ord["order_cnt"].sum())
|
||||||
|
gmv = day_ord["gmv_cents"].sum() / 100.0
|
||||||
|
trade_nos = ";".join(day_ord["trade_nos"].values)
|
||||||
|
else:
|
||||||
|
order_cnt = 0
|
||||||
|
gmv = 0.0
|
||||||
|
trade_nos = ""
|
||||||
|
|
||||||
|
rows.append({
|
||||||
|
"日期": d,
|
||||||
|
"新增注册": new_users,
|
||||||
|
"订单数": order_cnt,
|
||||||
|
"GMV(元)": round(gmv, 2),
|
||||||
|
"CPA分成(元)": round(new_users * 7, 2),
|
||||||
|
"订单号": trade_nos,
|
||||||
|
})
|
||||||
|
|
||||||
|
df_sheet = pd.DataFrame(rows)
|
||||||
|
total = {
|
||||||
|
"日期": "合计",
|
||||||
|
"新增注册": df_sheet["新增注册"].sum(),
|
||||||
|
"订单数": df_sheet["订单数"].sum(),
|
||||||
|
"GMV(元)": round(df_sheet["GMV(元)"].sum(), 2),
|
||||||
|
"CPA分成(元)": round(df_sheet["CPA分成(元)"].sum(), 2),
|
||||||
|
"订单号": "",
|
||||||
|
}
|
||||||
|
df_sheet = pd.concat([df_sheet, pd.DataFrame([total])], ignore_index=True)
|
||||||
|
channel_dfs[ch_name] = df_sheet
|
||||||
|
channel_totals[ch_name] = total
|
||||||
|
|
||||||
|
# 4. 输出 Excel(4个Sheet,每个渠道一张)
|
||||||
|
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "output")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
filename = f"学而思讯飞希沃京东方_{month_str}_渠道汇总.xlsx"
|
||||||
|
output_path = os.path.join(output_dir, filename)
|
||||||
|
|
||||||
|
with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
|
||||||
|
for ch_name in channel_names:
|
||||||
|
df_sheet = channel_dfs[ch_name]
|
||||||
|
df_sheet.to_excel(writer, sheet_name=ch_name, index=False)
|
||||||
|
# 将"订单号"列设为文本格式,防止长数字被科学计数法显示
|
||||||
|
ws = writer.sheets[ch_name]
|
||||||
|
# 找到"订单号"列的位置
|
||||||
|
headers = list(df_sheet.columns)
|
||||||
|
if "订单号" in headers:
|
||||||
|
col_idx = headers.index("订单号")
|
||||||
|
col_letter = chr(65 + col_idx) # A=65
|
||||||
|
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, min_col=col_idx+1, max_col=col_idx+1):
|
||||||
|
for cell in row:
|
||||||
|
if cell.value and str(cell.value).strip():
|
||||||
|
cell.number_format = '@' # 文本格式
|
||||||
|
|
||||||
|
# 5. 打印摘要
|
||||||
|
print(f"\n✅ 报表已生成: {output_path}")
|
||||||
|
print(f" 共 {last_day} 天数据 × 4 个渠道")
|
||||||
|
grand_orders = sum(t["订单数"] for t in channel_totals.values())
|
||||||
|
grand_gmv = sum(t["GMV(元)"] for t in channel_totals.values())
|
||||||
|
for ch_name in channel_names:
|
||||||
|
t = channel_totals[ch_name]
|
||||||
|
extra = ""
|
||||||
|
if "新增注册" in t:
|
||||||
|
extra += f", 新增注册={t['新增注册']}"
|
||||||
|
if "CPA分成(元)" in t:
|
||||||
|
extra += f", CPA分成={t['CPA分成(元)']}元"
|
||||||
|
if "CPS分成(元)" in t:
|
||||||
|
extra += f", CPS分成={t['CPS分成(元)']}元"
|
||||||
|
print(f" {ch_name}: 订单数={t['订单数']}, GMV={t['GMV(元)']}元{extra}")
|
||||||
|
print(f" 合计: 订单数={grand_orders}, GMV={grand_gmv}元")
|
||||||
|
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
month_input = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m")
|
||||||
|
month_str = parse_month(month_input)
|
||||||
|
run(month_str)
|
||||||
@ -392,7 +392,7 @@ FROM summary_calc;
|
|||||||
1. 优先输出Excel报表,存放于output/目录下,命名格式:`订单均摊结算报表_${账期起始日}_${账期结束日}.xlsx`
|
1. 优先输出Excel报表,存放于output/目录下,命名格式:`订单均摊结算报表_${账期起始日}_${账期结束日}.xlsx`
|
||||||
2. 报表包含以下Sheet:
|
2. 报表包含以下Sheet:
|
||||||
- 汇总表:订单数、正式订单数、试用订单数、冲销前税费、冲销税费、补充税费、冲销后税费、冲销前均摊金额、冲销均摊金额、补充均摊金额、冲销后均摊金额
|
- 汇总表:订单数、正式订单数、试用订单数、冲销前税费、冲销税费、补充税费、冲销后税费、冲销前均摊金额、冲销均摊金额、补充均摊金额、冲销后均摊金额
|
||||||
- 订单明细:订单号、订单金额、税率、税额、税后金额、总均摊周期、已均摊天数、历史均摊金额、账期均摊金额、未确认收入、剩余周期
|
- 订单明细:订单号、下单日期、退费日期、订单分类、原订单金额、退款金额、退后金额、税率、原订单税额、当前税后金额、当前税额、原始均摊周期、退后均摊周期、历史累计均摊金额、冲销历史累计摊销金额、退后订单历史累计摊销金额、冲销原税款、冲销未确认收入、当期摊销金额、账期税额、未确认收入、剩余周期
|
||||||
3. 文字回复核心指标:
|
3. 文字回复核心指标:
|
||||||
> 📊 账期${账期起始日}至${账期结束日}均摊结算结果:
|
> 📊 账期${账期起始日}至${账期结束日}均摊结算结果:
|
||||||
> 总订单数:XXX单
|
> 总订单数:XXX单
|
||||||
|
|||||||
@ -46,6 +46,7 @@ def main():
|
|||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
summary_sql_path = os.path.join(script_dir, 'sql', 'summary.sql')
|
summary_sql_path = os.path.join(script_dir, 'sql', 'summary.sql')
|
||||||
detail_sql_path = os.path.join(script_dir, 'sql', 'detail.sql')
|
detail_sql_path = os.path.join(script_dir, 'sql', 'detail.sql')
|
||||||
|
prepaid_sql_path = os.path.join(script_dir, 'sql', 'prepaid.sql')
|
||||||
|
|
||||||
if not os.path.exists(summary_sql_path) or not os.path.exists(detail_sql_path):
|
if not os.path.exists(summary_sql_path) or not os.path.exists(detail_sql_path):
|
||||||
print("错误:SQL模板文件不存在", file=sys.stderr)
|
print("错误:SQL模板文件不存在", file=sys.stderr)
|
||||||
@ -55,10 +56,15 @@ def main():
|
|||||||
summary_sql_template = f.read()
|
summary_sql_template = f.read()
|
||||||
with open(detail_sql_path, 'r', encoding='utf-8') as f:
|
with open(detail_sql_path, 'r', encoding='utf-8') as f:
|
||||||
detail_sql_template = f.read()
|
detail_sql_template = f.read()
|
||||||
|
prepaid_sql_template = None
|
||||||
|
if os.path.exists(prepaid_sql_path):
|
||||||
|
with open(prepaid_sql_path, 'r', encoding='utf-8') as f:
|
||||||
|
prepaid_sql_template = f.read()
|
||||||
|
|
||||||
# Replace placeholders
|
# Replace placeholders
|
||||||
summary_sql = summary_sql_template.format(period_start=start_date, period_end=end_date)
|
summary_sql = summary_sql_template.format(period_start=start_date, period_end=end_date)
|
||||||
detail_sql = detail_sql_template.format(period_start=start_date, period_end=end_date)
|
detail_sql = detail_sql_template.format(period_start=start_date, period_end=end_date)
|
||||||
|
prepaid_sql = prepaid_sql_template.format(period_start=start_date, period_end=end_date) if prepaid_sql_template else None
|
||||||
|
|
||||||
# Connect to PostgreSQL
|
# Connect to PostgreSQL
|
||||||
try:
|
try:
|
||||||
@ -77,6 +83,7 @@ def main():
|
|||||||
try:
|
try:
|
||||||
summary_df = pd.read_sql(summary_sql, conn)
|
summary_df = pd.read_sql(summary_sql, conn)
|
||||||
detail_df = pd.read_sql(detail_sql, conn)
|
detail_df = pd.read_sql(detail_sql, conn)
|
||||||
|
prepaid_df = pd.read_sql(prepaid_sql, conn) if prepaid_sql else None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"SQL执行失败:{str(e)}", file=sys.stderr)
|
print(f"SQL执行失败:{str(e)}", file=sys.stderr)
|
||||||
conn.close()
|
conn.close()
|
||||||
@ -93,6 +100,8 @@ def main():
|
|||||||
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
|
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
|
||||||
summary_df.to_excel(writer, sheet_name='汇总表', index=False)
|
summary_df.to_excel(writer, sheet_name='汇总表', index=False)
|
||||||
detail_df.to_excel(writer, sheet_name='订单明细', index=False)
|
detail_df.to_excel(writer, sheet_name='订单明细', index=False)
|
||||||
|
if prepaid_df is not None:
|
||||||
|
prepaid_df.to_excel(writer, sheet_name='本月预收账款', index=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Excel生成失败:{str(e)}", file=sys.stderr)
|
print(f"Excel生成失败:{str(e)}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@ -195,24 +195,71 @@ SELECT
|
|||||||
WHEN DATE(amortization_start_date) > '{period_end}'::date THEN '试用期中'
|
WHEN DATE(amortization_start_date) > '{period_end}'::date THEN '试用期中'
|
||||||
ELSE '正常订单'
|
ELSE '正常订单'
|
||||||
END AS "订单分类",
|
END AS "订单分类",
|
||||||
ROUND(pay_amount::numeric, 2) AS "订单金额",
|
ROUND(pay_amount::numeric, 2) AS "原订单金额",
|
||||||
|
ROUND(total_refund_amount::numeric, 2) AS "退款金额",
|
||||||
|
ROUND((pay_amount - total_refund_amount)::numeric, 2) AS "退后金额",
|
||||||
ROUND(tax_rate::numeric, 4) AS "税率",
|
ROUND(tax_rate::numeric, 4) AS "税率",
|
||||||
ROUND(tax_amount::numeric, 2) AS "原订单税额",
|
ROUND(tax_amount::numeric, 2) AS "原订单税额",
|
||||||
ROUND(after_tax_amount::numeric, 2) AS "原订单税后金额",
|
ROUND(
|
||||||
|
CASE
|
||||||
|
WHEN has_refund = 1 AND is_full_refund = 0
|
||||||
|
THEN (pay_amount - total_refund_amount) / (1 + tax_rate)
|
||||||
|
WHEN has_refund = 1 AND is_full_refund = 1
|
||||||
|
THEN 0
|
||||||
|
ELSE after_tax_amount
|
||||||
|
END::numeric, 2
|
||||||
|
) AS "当前税后金额",
|
||||||
|
ROUND(
|
||||||
|
CASE
|
||||||
|
WHEN has_refund = 1 AND is_full_refund = 0
|
||||||
|
THEN (pay_amount - total_refund_amount) / (1 + tax_rate) * tax_rate
|
||||||
|
WHEN has_refund = 1 AND is_full_refund = 1
|
||||||
|
THEN 0
|
||||||
|
ELSE tax_amount
|
||||||
|
END::numeric, 2
|
||||||
|
) AS "当前税额",
|
||||||
original_cycle_days AS "原始均摊周期",
|
original_cycle_days AS "原始均摊周期",
|
||||||
total_cycle_days AS "退后均摊周期",
|
total_cycle_days AS "退后均摊周期",
|
||||||
|
|
||||||
-- 历史均摊金额(基于原始日均摊)
|
-- 历史累计均摊金额(仅历史订单有值,账期内订单=0;账期内退费按退费前状态算)
|
||||||
ROUND((historical_amorted_days * original_daily_amort_amount)::numeric, 2) AS "历史均摊金额",
|
-- 账期前部分退费:按退后日均摊×退后历史天数
|
||||||
|
-- 正常订单/账期内退费:按原始日均摊×历史天数
|
||||||
-- 冲销历史均摊(场景B/E:历史下单+账期内退费,排除试用期内退费)
|
|
||||||
ROUND(
|
ROUND(
|
||||||
CASE WHEN is_current_period_refund = 1 AND is_ordered_in_period = 0 AND historical_amorted_days > 0
|
CASE
|
||||||
|
WHEN is_ordered_in_period = 1 THEN 0
|
||||||
|
WHEN is_pre_period_partial_refund = 1 AND historical_amorted_days > 0
|
||||||
|
THEN historical_amorted_days * daily_amort_amount
|
||||||
|
WHEN historical_amorted_days > 0
|
||||||
|
THEN historical_amorted_days * original_daily_amort_amount
|
||||||
|
ELSE 0
|
||||||
|
END::numeric, 2
|
||||||
|
) AS "历史累计均摊金额",
|
||||||
|
|
||||||
|
-- 冲销历史累计摊销金额(仅场景B/E:历史下单+账期内退费,排除试用期内退费)
|
||||||
|
ROUND(
|
||||||
|
CASE WHEN is_current_period_refund = 1 AND is_ordered_in_period = 0
|
||||||
AND is_trial_period_refund = 0
|
AND is_trial_period_refund = 0
|
||||||
|
AND historical_amorted_days > 0
|
||||||
THEN -(historical_amorted_days * original_daily_amort_amount)
|
THEN -(historical_amorted_days * original_daily_amort_amount)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END::numeric, 2
|
END::numeric, 2
|
||||||
) AS "冲销历史均摊",
|
) AS "冲销历史累计摊销金额",
|
||||||
|
|
||||||
|
-- 退后订单历史累计摊销金额(站在账期最后一天,看所有订单在账期第一天之前的有效历史摊销金额)
|
||||||
|
-- 账期内退费的订单按退后状态算;账期前部分退费按退后状态算;正常订单按原始状态算
|
||||||
|
ROUND(
|
||||||
|
CASE
|
||||||
|
WHEN is_ordered_in_period = 1 THEN 0
|
||||||
|
WHEN is_pre_period_partial_refund = 1 AND historical_amorted_days > 0
|
||||||
|
THEN historical_amorted_days * daily_amort_amount
|
||||||
|
WHEN is_current_period_refund = 1 AND is_ordered_in_period = 0
|
||||||
|
AND is_trial_period_refund = 0 AND historical_amorted_days > 0
|
||||||
|
THEN historical_amorted_days * daily_amort_amount
|
||||||
|
WHEN historical_amorted_days > 0
|
||||||
|
THEN historical_amorted_days * original_daily_amort_amount
|
||||||
|
ELSE 0
|
||||||
|
END::numeric, 2
|
||||||
|
) AS "退后订单历史累计摊销金额",
|
||||||
|
|
||||||
-- 冲销原税款(场景B/E:历史下单+账期内退费,排除试用期内退费)
|
-- 冲销原税款(场景B/E:历史下单+账期内退费,排除试用期内退费)
|
||||||
ROUND(
|
ROUND(
|
||||||
@ -232,7 +279,7 @@ SELECT
|
|||||||
END::numeric, 2
|
END::numeric, 2
|
||||||
) AS "冲销未确认收入",
|
) AS "冲销未确认收入",
|
||||||
|
|
||||||
-- 账期均摊金额
|
-- 当期摊销金额
|
||||||
ROUND(
|
ROUND(
|
||||||
CASE
|
CASE
|
||||||
-- 场景F:账期前全额退费,不统计
|
-- 场景F:账期前全额退费,不统计
|
||||||
@ -274,7 +321,7 @@ SELECT
|
|||||||
END
|
END
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END::numeric, 2
|
END::numeric, 2
|
||||||
) AS "账期均摊金额",
|
) AS "当期摊销金额",
|
||||||
|
|
||||||
-- 退后订单税额(仅账期内下单且已转正的非全额退费订单计税)
|
-- 退后订单税额(仅账期内下单且已转正的非全额退费订单计税)
|
||||||
ROUND(
|
ROUND(
|
||||||
|
|||||||
148
skills/vala-order-amortization-stat/sql/prepaid.sql
Normal file
148
skills/vala-order-amortization-stat/sql/prepaid.sql
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
-- 本月预收账款(Sheet3)
|
||||||
|
-- 参数:{period_start} 账期起始日,{period_end} 账期结束日
|
||||||
|
-- "本月" = 账期所在月,"上月" = 账期月前一个月
|
||||||
|
-- 金额均为含税金额
|
||||||
|
|
||||||
|
WITH
|
||||||
|
-- 上月月份范围
|
||||||
|
month_range AS (
|
||||||
|
SELECT
|
||||||
|
'{period_start}'::date AS period_start,
|
||||||
|
'{period_end}'::date AS period_end,
|
||||||
|
DATE_TRUNC('month', '{period_start}'::date) AS this_month_start,
|
||||||
|
(DATE_TRUNC('month', '{period_start}'::date) - INTERVAL '1 day')::date AS last_month_end,
|
||||||
|
(DATE_TRUNC('month', '{period_start}'::date) - INTERVAL '1 month')::date AS last_month_start
|
||||||
|
),
|
||||||
|
-- 所有退费记录
|
||||||
|
all_refund_records AS (
|
||||||
|
SELECT
|
||||||
|
out_trade_no AS order_no,
|
||||||
|
SUM(refund_amount_int) / 100 AS total_refund_amount,
|
||||||
|
MAX(CASE WHEN refund_type = 2 THEN 1 ELSE 0 END) AS is_full_refund,
|
||||||
|
MAX(DATE(updated_at)) AS latest_refund_date
|
||||||
|
FROM bi_refund_order
|
||||||
|
WHERE status = 3
|
||||||
|
GROUP BY out_trade_no
|
||||||
|
),
|
||||||
|
-- 所有订单基础信息(含退费标记)
|
||||||
|
order_base AS (
|
||||||
|
SELECT
|
||||||
|
o.out_trade_no AS order_no,
|
||||||
|
DATE(o.created_at) AS order_date,
|
||||||
|
o.pay_amount_int / 100 AS pay_amount,
|
||||||
|
CASE WHEN ar.order_no IS NOT NULL THEN 1 ELSE 0 END AS has_refund,
|
||||||
|
COALESCE(ar.total_refund_amount, 0) AS total_refund_amount,
|
||||||
|
CASE WHEN ar.order_no IS NOT NULL AND COALESCE(ar.total_refund_amount, 0) >= o.pay_amount_int / 100 THEN 1 ELSE 0 END AS is_full_refund,
|
||||||
|
ar.latest_refund_date,
|
||||||
|
-- 转正日期 = 下单日 + 7天
|
||||||
|
DATE(o.created_at) + INTERVAL '7 days' AS amortization_start_date,
|
||||||
|
-- 有效金额(部分退费 = 原金额 - 退费;未退费 = 原金额;全额退费 = 0)
|
||||||
|
CASE
|
||||||
|
WHEN ar.order_no IS NOT NULL AND COALESCE(ar.total_refund_amount, 0) >= o.pay_amount_int / 100 THEN 0
|
||||||
|
ELSE o.pay_amount_int / 100 - COALESCE(ar.total_refund_amount, 0)
|
||||||
|
END AS effective_amount,
|
||||||
|
o.order_status
|
||||||
|
FROM bi_vala_order o
|
||||||
|
JOIN bi_vala_app_account a ON o.account_id = a.id
|
||||||
|
LEFT JOIN all_refund_records ar ON o.out_trade_no = ar.order_no
|
||||||
|
CROSS JOIN month_range mr
|
||||||
|
WHERE
|
||||||
|
o.created_at >= mr.last_month_start
|
||||||
|
AND o.created_at <= mr.period_end
|
||||||
|
AND o.order_status IN (3,4)
|
||||||
|
AND o.pay_amount_int >= 1000
|
||||||
|
AND a.status = 1
|
||||||
|
),
|
||||||
|
-- 标记订单分类
|
||||||
|
order_classified AS (
|
||||||
|
SELECT
|
||||||
|
ob.*,
|
||||||
|
mr.*,
|
||||||
|
-- 上月下单
|
||||||
|
CASE WHEN ob.order_date >= mr.last_month_start AND ob.order_date <= mr.last_month_end THEN 1 ELSE 0 END AS is_last_month_order,
|
||||||
|
-- 本月下单
|
||||||
|
CASE WHEN ob.order_date >= mr.this_month_start AND ob.order_date <= mr.period_end THEN 1 ELSE 0 END AS is_this_month_order,
|
||||||
|
-- 上月未转正(转正日 > 上月最后一天)
|
||||||
|
CASE WHEN ob.amortization_start_date > mr.last_month_end THEN 1 ELSE 0 END AS not_formal_in_last_month,
|
||||||
|
-- 本月已转正(转正日 <= 账期结束日)
|
||||||
|
CASE WHEN ob.amortization_start_date <= mr.period_end THEN 1 ELSE 0 END AS formal_in_this_month,
|
||||||
|
-- 上月已全额退费
|
||||||
|
CASE WHEN ob.has_refund = 1 AND ob.is_full_refund = 1 AND ob.latest_refund_date <= mr.last_month_end THEN 1 ELSE 0 END AS full_refund_in_last_month,
|
||||||
|
-- 本月退费金额(退费日期在账期内)
|
||||||
|
CASE WHEN ob.has_refund = 1
|
||||||
|
AND ob.latest_refund_date >= mr.period_start
|
||||||
|
AND ob.latest_refund_date <= mr.period_end
|
||||||
|
THEN ob.total_refund_amount
|
||||||
|
ELSE 0
|
||||||
|
END AS refund_in_period
|
||||||
|
FROM order_base ob
|
||||||
|
CROSS JOIN month_range mr
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'上月订单转正' AS "分类",
|
||||||
|
-- 新增:上月下单 + 上月未转正 + 剔除上月全额退费 + 部分退费算有效金额
|
||||||
|
ROUND(SUM(
|
||||||
|
CASE WHEN is_last_month_order = 1
|
||||||
|
AND not_formal_in_last_month = 1
|
||||||
|
AND full_refund_in_last_month = 0
|
||||||
|
THEN effective_amount
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
)::numeric, 2) AS "新增",
|
||||||
|
-- 退费:这些订单在本月的退费金额
|
||||||
|
ROUND(SUM(
|
||||||
|
CASE WHEN is_last_month_order = 1
|
||||||
|
AND not_formal_in_last_month = 1
|
||||||
|
AND full_refund_in_last_month = 0
|
||||||
|
THEN refund_in_period
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
)::numeric, 2) AS "退费"
|
||||||
|
|
||||||
|
FROM order_classified
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'本月订单转正' AS "分类",
|
||||||
|
-- 新增:本月下单 + 本月已转正 + 不考虑退费(原订单金额)
|
||||||
|
ROUND(SUM(
|
||||||
|
CASE WHEN is_this_month_order = 1
|
||||||
|
AND formal_in_this_month = 1
|
||||||
|
THEN pay_amount
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
)::numeric, 2) AS "新增",
|
||||||
|
-- 退费:这些订单在本月的退费金额
|
||||||
|
ROUND(SUM(
|
||||||
|
CASE WHEN is_this_month_order = 1
|
||||||
|
AND formal_in_this_month = 1
|
||||||
|
THEN refund_in_period
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
)::numeric, 2) AS "退费"
|
||||||
|
|
||||||
|
FROM order_classified
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'本月订单未转正' AS "分类",
|
||||||
|
-- 新增:本月下单 + 本月未转正 + 不考虑退费
|
||||||
|
ROUND(SUM(
|
||||||
|
CASE WHEN is_this_month_order = 1
|
||||||
|
AND formal_in_this_month = 0
|
||||||
|
THEN pay_amount
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
)::numeric, 2) AS "新增",
|
||||||
|
-- 退费:这些订单在本月的退费金额
|
||||||
|
ROUND(SUM(
|
||||||
|
CASE WHEN is_this_month_order = 1
|
||||||
|
AND formal_in_this_month = 0
|
||||||
|
THEN refund_in_period
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
)::numeric, 2) AS "退费"
|
||||||
|
|
||||||
|
FROM order_classified;
|
||||||
@ -122,66 +122,77 @@ order_with_days AS (
|
|||||||
THEN EXTRACT(DAY FROM (LEAST('{period_end}'::date, oc2.amortization_end_date) - oc2.amortization_start_date))::int + 1 ELSE 0
|
THEN EXTRACT(DAY FROM (LEAST('{period_end}'::date, oc2.amortization_end_date) - oc2.amortization_start_date))::int + 1 ELSE 0
|
||||||
END AS refunded_amort_days_to_period_end
|
END AS refunded_amort_days_to_period_end
|
||||||
FROM order_classified oc2
|
FROM order_classified oc2
|
||||||
),
|
)
|
||||||
-- 步骤6:汇总
|
-- 步骤6:最终汇总(与订单明细Sheet2列对齐,前四列替换为订单统计+税率,其余SUM)
|
||||||
summary_calc AS (
|
SELECT
|
||||||
SELECT
|
COUNT(CASE WHEN order_date >= '{period_start}'::date AND order_date <= '{period_end}'::date THEN 1 END) AS "订单数",
|
||||||
-- 账期内新增订单统计
|
|
||||||
COUNT(CASE WHEN order_date >= '{period_start}'::date AND order_date <= '{period_end}'::date THEN 1 END) AS total_new_orders,
|
|
||||||
COUNT(CASE WHEN order_date >= '{period_start}'::date AND order_date <= '{period_end}'::date
|
COUNT(CASE WHEN order_date >= '{period_start}'::date AND order_date <= '{period_end}'::date
|
||||||
AND amortization_start_date <= '{period_end}'::date THEN 1 END) AS formal_orders,
|
AND amortization_start_date <= '{period_end}'::date THEN 1 END) AS "正式订单数",
|
||||||
COUNT(CASE WHEN order_date >= '{period_start}'::date AND order_date <= '{period_end}'::date
|
COUNT(CASE WHEN order_date >= '{period_start}'::date AND order_date <= '{period_end}'::date
|
||||||
AND amortization_start_date > '{period_end}'::date THEN 1 END) AS trial_orders,
|
AND amortization_start_date > '{period_end}'::date THEN 1 END) AS "试用订单数",
|
||||||
|
|
||||||
-- 冲销历史均摊(场景B/E:历史下单+账期内退费,排除试用期内退费)
|
-- 冲销历史累计摊销金额(SUM,明细列逻辑)
|
||||||
ROUND(SUM(
|
ROUND(SUM(
|
||||||
CASE WHEN is_current_period_refund = 1 AND is_ordered_in_period = 0 AND historical_amorted_days > 0
|
CASE WHEN is_current_period_refund = 1 AND is_ordered_in_period = 0
|
||||||
AND is_trial_period_refund = 0
|
AND is_trial_period_refund = 0
|
||||||
|
AND historical_amorted_days > 0
|
||||||
|
THEN -(historical_amorted_days * original_daily_amort_amount)
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
)::numeric, 2) AS "冲销历史累计摊销金额",
|
||||||
|
|
||||||
|
-- 退后订单历史累计摊销金额(SUM,明细列逻辑)
|
||||||
|
ROUND(SUM(
|
||||||
|
CASE
|
||||||
|
WHEN is_ordered_in_period = 1 THEN 0
|
||||||
|
WHEN is_pre_period_partial_refund = 1 AND historical_amorted_days > 0
|
||||||
|
THEN historical_amorted_days * daily_amort_amount
|
||||||
|
WHEN is_current_period_refund = 1 AND is_ordered_in_period = 0
|
||||||
|
AND is_trial_period_refund = 0 AND historical_amorted_days > 0
|
||||||
|
THEN historical_amorted_days * daily_amort_amount
|
||||||
|
WHEN historical_amorted_days > 0
|
||||||
THEN historical_amorted_days * original_daily_amort_amount
|
THEN historical_amorted_days * original_daily_amort_amount
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END
|
END
|
||||||
)::numeric, 2) AS writeoff_amort,
|
)::numeric, 2) AS "退后订单历史累计摊销金额",
|
||||||
|
|
||||||
-- 冲销原税款(场景B/E:历史下单+账期内退费,排除试用期内退费)
|
-- 冲销原税款(SUM)
|
||||||
ROUND(SUM(
|
ROUND(SUM(
|
||||||
CASE WHEN is_current_period_refund = 1 AND is_ordered_in_period = 0
|
CASE WHEN is_current_period_refund = 1 AND is_ordered_in_period = 0
|
||||||
AND is_trial_period_refund = 0
|
AND is_trial_period_refund = 0
|
||||||
THEN tax_amount
|
THEN -tax_amount
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END
|
END
|
||||||
)::numeric, 2) AS writeoff_tax,
|
)::numeric, 2) AS "冲销原税款",
|
||||||
|
|
||||||
-- 冲销未确认收入(场景B/E:历史下单+账期内退费,排除试用期内退费)
|
-- 冲销未确认收入(SUM)
|
||||||
ROUND(SUM(
|
ROUND(SUM(
|
||||||
CASE WHEN is_current_period_refund = 1 AND is_ordered_in_period = 0
|
CASE WHEN is_current_period_refund = 1 AND is_ordered_in_period = 0
|
||||||
AND is_trial_period_refund = 0
|
AND is_trial_period_refund = 0
|
||||||
THEN after_tax_amount - historical_amorted_days * original_daily_amort_amount
|
THEN -(after_tax_amount - historical_amorted_days * original_daily_amort_amount)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END
|
END
|
||||||
)::numeric, 2) AS writeoff_unconfirmed,
|
)::numeric, 2) AS "冲销未确认收入",
|
||||||
|
|
||||||
-- 账期均摊金额
|
-- 当期摊销金额(SUM)
|
||||||
ROUND(SUM(
|
ROUND(SUM(
|
||||||
CASE
|
CASE
|
||||||
WHEN is_pre_period_full_refund = 1 THEN 0
|
WHEN is_pre_period_full_refund = 1 THEN 0
|
||||||
WHEN is_trial_period_refund = 1 AND is_full_refund = 1 THEN 0 -- 试用期内全额退费,无均摊
|
WHEN is_trial_period_refund = 1 AND is_full_refund = 1 THEN 0
|
||||||
WHEN is_trial_period_refund = 1 AND is_full_refund = 0 THEN -- 试用期内部分退费,按退后金额从转正日正常均摊
|
WHEN is_trial_period_refund = 1 AND is_full_refund = 0 THEN
|
||||||
CASE WHEN total_cycle_days > 0 AND period_amort_days > 0
|
CASE WHEN total_cycle_days > 0 AND period_amort_days > 0
|
||||||
THEN daily_amort_amount * period_amort_days ELSE 0 END
|
THEN daily_amort_amount * period_amort_days ELSE 0 END
|
||||||
WHEN is_current_period_refund = 1 AND is_full_refund = 1 THEN 0
|
WHEN is_current_period_refund = 1 AND is_full_refund = 1 AND is_ordered_in_period = 1 THEN 0
|
||||||
-- 场景A:账期内下单+部分退费
|
WHEN is_current_period_refund = 1 AND is_full_refund = 1 AND is_ordered_in_period = 0 THEN 0
|
||||||
WHEN is_current_period_refund = 1 AND is_full_refund = 0 AND is_ordered_in_period = 1 THEN
|
WHEN is_current_period_refund = 1 AND is_full_refund = 0 AND is_ordered_in_period = 1 THEN
|
||||||
CASE WHEN total_cycle_days > 0 AND period_amort_days > 0
|
CASE WHEN total_cycle_days > 0 AND period_amort_days > 0
|
||||||
THEN daily_amort_amount * period_amort_days ELSE 0 END
|
THEN daily_amort_amount * period_amort_days ELSE 0 END
|
||||||
-- 场景B:历史下单+账期内部分退费,从转正日重新均摊
|
|
||||||
WHEN is_current_period_refund = 1 AND is_full_refund = 0 AND is_ordered_in_period = 0 THEN
|
WHEN is_current_period_refund = 1 AND is_full_refund = 0 AND is_ordered_in_period = 0 THEN
|
||||||
CASE WHEN total_cycle_days > 0 AND refunded_amort_days_to_period_end > 0
|
CASE WHEN total_cycle_days > 0 AND refunded_amort_days_to_period_end > 0
|
||||||
THEN daily_amort_amount * refunded_amort_days_to_period_end ELSE 0 END
|
THEN daily_amort_amount * refunded_amort_days_to_period_end ELSE 0 END
|
||||||
-- 场景C:账期前部分退费
|
|
||||||
WHEN is_pre_period_partial_refund = 1 THEN
|
WHEN is_pre_period_partial_refund = 1 THEN
|
||||||
CASE WHEN total_cycle_days > 0 AND period_amort_days > 0
|
CASE WHEN total_cycle_days > 0 AND period_amort_days > 0
|
||||||
THEN daily_amort_amount * period_amort_days ELSE 0 END
|
THEN daily_amort_amount * period_amort_days ELSE 0 END
|
||||||
-- 正常订单
|
|
||||||
WHEN period_amort_days > 0 THEN
|
WHEN period_amort_days > 0 THEN
|
||||||
CASE
|
CASE
|
||||||
WHEN eff_end = amortization_end_date
|
WHEN eff_end = amortization_end_date
|
||||||
@ -190,37 +201,36 @@ summary_calc AS (
|
|||||||
END
|
END
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END
|
END
|
||||||
)::numeric, 2) AS period_amort,
|
)::numeric, 2) AS "当期摊销金额",
|
||||||
|
|
||||||
-- 账期税费(仅账期内下单且已转正的非全额退费订单)
|
-- 账期税额(SUM)
|
||||||
ROUND(SUM(
|
ROUND(SUM(
|
||||||
CASE
|
CASE
|
||||||
-- 账期内下单 + 已转正 + 部分退费 → 退后订单税额
|
|
||||||
WHEN is_ordered_in_period = 1
|
WHEN is_ordered_in_period = 1
|
||||||
AND amortization_start_date <= '{period_end}'::date
|
AND DATE(amortization_start_date) <= '{period_end}'::date
|
||||||
AND is_current_period_refund = 1 AND is_full_refund = 0
|
AND is_current_period_refund = 1 AND is_full_refund = 0
|
||||||
THEN refunded_tax_amount
|
THEN refunded_tax_amount
|
||||||
-- 账期内下单 + 已转正 + 未全额退费(含未退费) → 原订单税额
|
|
||||||
WHEN is_ordered_in_period = 1
|
WHEN is_ordered_in_period = 1
|
||||||
AND amortization_start_date <= '{period_end}'::date
|
AND DATE(amortization_start_date) <= '{period_end}'::date
|
||||||
AND (has_refund = 0 OR (is_current_period_refund = 0 AND is_full_refund = 0))
|
AND (has_refund = 0 OR (is_current_period_refund = 0 AND is_full_refund = 0))
|
||||||
THEN tax_amount
|
THEN tax_amount
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END
|
END
|
||||||
)::numeric, 2) AS period_tax
|
)::numeric, 2) AS "账期税额",
|
||||||
|
|
||||||
FROM order_with_days
|
-- 未确认收入(SUM)
|
||||||
WHERE is_pre_period_full_refund = 0
|
ROUND(SUM(
|
||||||
)
|
CASE
|
||||||
SELECT
|
WHEN is_pre_period_full_refund = 1 THEN 0
|
||||||
total_new_orders AS "订单数",
|
WHEN is_current_period_refund = 1 AND is_full_refund = 1 THEN 0
|
||||||
formal_orders AS "正式订单数",
|
WHEN is_current_period_refund = 1 AND is_full_refund = 0 THEN
|
||||||
trial_orders AS "试用订单数",
|
refunded_after_tax_amount - daily_amort_amount * refunded_amort_days_to_period_end
|
||||||
writeoff_amort AS "冲销历史均摊",
|
WHEN is_pre_period_partial_refund = 1 THEN
|
||||||
writeoff_tax AS "冲销原税款",
|
refunded_after_tax_amount - daily_amort_amount * total_amorted_days_to_period_end
|
||||||
writeoff_unconfirmed AS "冲销未确认收入",
|
ELSE
|
||||||
period_amort AS "账期均摊金额",
|
after_tax_amount - original_daily_amort_amount * total_amorted_days_to_period_end
|
||||||
period_tax AS "账期税费",
|
END
|
||||||
ROUND((period_amort - writeoff_amort)::numeric, 2) AS "冲销后均摊金额",
|
)::numeric, 2) AS "未确认收入"
|
||||||
ROUND((period_tax - writeoff_tax)::numeric, 2) AS "冲销后税费"
|
|
||||||
FROM summary_calc;
|
FROM order_with_days
|
||||||
|
WHERE is_pre_period_full_refund = 0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user