🤖 每日自动备份 - 2026-05-07 08:00:01

This commit is contained in:
小溪 2026-05-07 08:00:01 +08:00
parent 6510dc41ca
commit 968286d9c9
17 changed files with 1526 additions and 112 deletions

View File

@ -12,4 +12,4 @@ vala-component-practice-stat 8e768e2641019d27bd41f4647d2d90f24182a0554dad5ad9f41
cron-schedule e103cbb1806b28c891b9c856963325086ecaff32edec208f0a841865f26e8f3e
refund-user-learning-analysis 648fd4ae2b29167fd66eab4245bdaaef00242db3131f4919cc02f07ca2a9b59c
phone-chapter-query ac429b4da5a89db16efdf1066edf4ecb1c050b93aff20dd4c652af5f5568e44f
vala-order-amortization-stat de5f77b9e4ab865df5efe595e59e877257ac489a195fb969b9d20d1a71158b97
vala-order-amortization-stat fd9da99f313347aa11a4cad8e5d0c9226eac8649b45db94dfa60e37015ab7491

View 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"
]
}
}
}

View 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

View 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
View 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 个分表

View 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
View 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
View 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
View 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;

View 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;")

View 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_id8个分表 ──
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_idALL_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()

View 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. 输出 Excel4个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)

View File

@ -392,7 +392,7 @@ FROM summary_calc;
1. 优先输出Excel报表存放于output/目录下,命名格式:`订单均摊结算报表_${账期起始日}_${账期结束日}.xlsx`
2. 报表包含以下Sheet
- 汇总表:订单数、正式订单数、试用订单数、冲销前税费、冲销税费、补充税费、冲销后税费、冲销前均摊金额、冲销均摊金额、补充均摊金额、冲销后均摊金额
- 订单明细:订单号、订单金额、税率、税额、税后金额、总均摊周期、已均摊天数、历史均摊金额、账期均摊金额、未确认收入、剩余周期
- 订单明细:订单号、下单日期、退费日期、订单分类、原订单金额、退款金额、退后金额、税率、原订单税额、当前税后金额、当前税额、原始均摊周期、退后均摊周期、历史累计均摊金额、冲销历史累计摊销金额、退后订单历史累计摊销金额、冲销原税款、冲销未确认收入、当期摊销金额、账期税额、未确认收入、剩余周期
3. 文字回复核心指标:
> 📊 账期${账期起始日}至${账期结束日}均摊结算结果:
> 总订单数XXX单

View File

@ -46,6 +46,7 @@ def main():
script_dir = os.path.dirname(os.path.abspath(__file__))
summary_sql_path = os.path.join(script_dir, 'sql', 'summary.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):
print("错误SQL模板文件不存在", file=sys.stderr)
@ -55,10 +56,15 @@ def main():
summary_sql_template = f.read()
with open(detail_sql_path, 'r', encoding='utf-8') as f:
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
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)
prepaid_sql = prepaid_sql_template.format(period_start=start_date, period_end=end_date) if prepaid_sql_template else None
# Connect to PostgreSQL
try:
@ -77,6 +83,7 @@ def main():
try:
summary_df = pd.read_sql(summary_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:
print(f"SQL执行失败{str(e)}", file=sys.stderr)
conn.close()
@ -93,6 +100,8 @@ def main():
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
summary_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:
print(f"Excel生成失败{str(e)}", file=sys.stderr)
sys.exit(1)

View File

@ -195,24 +195,71 @@ SELECT
WHEN DATE(amortization_start_date) > '{period_end}'::date THEN '试用期中'
ELSE '正常订单'
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_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 "原始均摊周期",
total_cycle_days AS "退后均摊周期",
-- 历史均摊金额(基于原始日均摊)
ROUND((historical_amorted_days * original_daily_amort_amount)::numeric, 2) AS "历史均摊金额",
-- 冲销历史均摊场景B/E历史下单+账期内退费,排除试用期内退费)
-- 历史累计均摊金额(仅历史订单有值,账期内订单=0账期内退费按退费前状态算
-- 账期前部分退费:按退后日均摊×退后历史天数
-- 正常订单/账期内退费:按原始日均摊×历史天数
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 historical_amorted_days > 0
THEN -(historical_amorted_days * original_daily_amort_amount)
ELSE 0
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历史下单+账期内退费,排除试用期内退费)
ROUND(
@ -232,7 +279,7 @@ SELECT
END::numeric, 2
) AS "冲销未确认收入",
-- 账期均摊金额
-- 当期摊销金额
ROUND(
CASE
-- 场景F账期前全额退费不统计
@ -274,7 +321,7 @@ SELECT
END
ELSE 0
END::numeric, 2
) AS "账期均摊金额",
) AS "当期摊销金额",
-- 退后订单税额(仅账期内下单且已转正的非全额退费订单计税)
ROUND(

View 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;

View File

@ -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
END AS refunded_amort_days_to_period_end
FROM order_classified oc2
),
-- 步骤6汇总
summary_calc AS (
SELECT
-- 账期内新增订单统计
COUNT(CASE WHEN order_date >= '{period_start}'::date AND order_date <= '{period_end}'::date THEN 1 END) AS total_new_orders,
)
-- 步骤6最终汇总与订单明细Sheet2列对齐前四列替换为订单统计+税率其余SUM
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
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
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(
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 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
ELSE 0
END
)::numeric, 2) AS writeoff_amort,
)::numeric, 2) AS "退后订单历史累计摊销金额",
-- 冲销原税款场景B/E历史下单+账期内退费,排除试用期内退费)
-- 冲销原税款SUM
ROUND(SUM(
CASE WHEN is_current_period_refund = 1 AND is_ordered_in_period = 0
AND is_trial_period_refund = 0
THEN tax_amount
THEN -tax_amount
ELSE 0
END
)::numeric, 2) AS writeoff_tax,
)::numeric, 2) AS "冲销原税款",
-- 冲销未确认收入场景B/E历史下单+账期内退费,排除试用期内退费)
-- 冲销未确认收入SUM
ROUND(SUM(
CASE WHEN is_current_period_refund = 1 AND is_ordered_in_period = 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
END
)::numeric, 2) AS writeoff_unconfirmed,
)::numeric, 2) AS "冲销未确认收入",
-- 账期均摊金额
-- 当期摊销金额SUM
ROUND(SUM(
CASE
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 = 0 THEN -- 试用期内部分退费,按退后金额从转正日正常均摊
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
CASE WHEN total_cycle_days > 0 AND period_amort_days > 0
THEN daily_amort_amount * period_amort_days ELSE 0 END
WHEN is_current_period_refund = 1 AND is_full_refund = 1 THEN 0
-- 场景A账期内下单+部分退费
WHEN is_current_period_refund = 1 AND is_full_refund = 1 AND is_ordered_in_period = 1 THEN 0
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
CASE WHEN total_cycle_days > 0 AND period_amort_days > 0
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
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
-- 场景C账期前部分退费
WHEN is_pre_period_partial_refund = 1 THEN
CASE WHEN total_cycle_days > 0 AND period_amort_days > 0
THEN daily_amort_amount * period_amort_days ELSE 0 END
-- 正常订单
WHEN period_amort_days > 0 THEN
CASE
WHEN eff_end = amortization_end_date
@ -190,37 +201,36 @@ summary_calc AS (
END
ELSE 0
END
)::numeric, 2) AS period_amort,
)::numeric, 2) AS "当期摊销金额",
-- 账期税费(仅账期内下单且已转正的非全额退费订单
-- 账期税额SUM
ROUND(SUM(
CASE
-- 账期内下单 + 已转正 + 部分退费 → 退后订单税额
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
THEN refunded_tax_amount
-- 账期内下单 + 已转正 + 未全额退费(含未退费) → 原订单税额
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))
THEN tax_amount
ELSE 0
END
)::numeric, 2) AS period_tax
)::numeric, 2) AS "账期税额",
FROM order_with_days
WHERE is_pre_period_full_refund = 0
)
SELECT
total_new_orders AS "订单数",
formal_orders AS "正式订单数",
trial_orders AS "试用订单数",
writeoff_amort AS "冲销历史均摊",
writeoff_tax AS "冲销原税款",
writeoff_unconfirmed AS "冲销未确认收入",
period_amort AS "账期均摊金额",
period_tax AS "账期税费",
ROUND((period_amort - writeoff_amort)::numeric, 2) AS "冲销后均摊金额",
ROUND((period_tax - writeoff_tax)::numeric, 2) AS "冲销后税费"
FROM summary_calc;
-- 未确认收入SUM
ROUND(SUM(
CASE
WHEN is_pre_period_full_refund = 1 THEN 0
WHEN is_current_period_refund = 1 AND is_full_refund = 1 THEN 0
WHEN is_current_period_refund = 1 AND is_full_refund = 0 THEN
refunded_after_tax_amount - daily_amort_amount * refunded_amort_days_to_period_end
WHEN is_pre_period_partial_refund = 1 THEN
refunded_after_tax_amount - daily_amort_amount * total_amorted_days_to_period_end
ELSE
after_tax_amount - original_daily_amort_amount * total_amorted_days_to_period_end
END
)::numeric, 2) AS "未确认收入"
FROM order_with_days
WHERE is_pre_period_full_refund = 0;