From ac4e2eb3ce1b6a6ac5341e92f139eab51f479f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=BA=AA?= Date: Sat, 16 May 2026 08:00:01 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20=E6=AF=8F=E6=97=A5=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=A4=87=E4=BB=BD=20-=202026-05-16=2008:00:01?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- memory/.dreams/short-term-recall.json | 26 +- memory/2026-05-15-1014.md | 62 +++ memory/2026-05-15-1713.md | 41 ++ skills/vala-order-amortization-stat/SKILL.md | 438 ++++-------------- skills/vala-order-amortization-stat/run.py | 62 +-- .../sql/_common.sql | 167 +++++++ .../sql/detail.sql | 266 ++--------- .../sql/prepaid.sql | 19 +- .../sql/summary.sql | 205 ++------ 9 files changed, 479 insertions(+), 807 deletions(-) create mode 100644 memory/2026-05-15-1014.md create mode 100644 memory/2026-05-15-1713.md create mode 100644 skills/vala-order-amortization-stat/sql/_common.sql diff --git a/memory/.dreams/short-term-recall.json b/memory/.dreams/short-term-recall.json index 2ffd60e..8fa08a3 100644 --- a/memory/.dreams/short-term-recall.json +++ b/memory/.dreams/short-term-recall.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-05-14T06:41:55.506Z", + "updatedAt": "2026-05-15T09:14:59.142Z", "entries": { "memory:memory/2026-05-06.md:1:20": { "key": "memory:memory/2026-05-06.md:1:20", @@ -9,18 +9,20 @@ "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, + "recallCount": 2, "dailyCount": 0, "groundedCount": 0, - "totalScore": 1, + "totalScore": 2, "maxScore": 1, "firstRecalledAt": "2026-05-06T12:28:28.905Z", - "lastRecalledAt": "2026-05-06T12:28:28.905Z", + "lastRecalledAt": "2026-05-15T04:06:01.561Z", "queryHashes": [ - "c30fa9d13974" + "c30fa9d13974", + "3d07645a9dc4" ], "recallDays": [ - "2026-05-06" + "2026-05-06", + "2026-05-15" ], "conceptTags": [ "学而思/讯飞/希沃/京东方月度渠道订单报表", @@ -73,18 +75,20 @@ "endLine": 12, "source": "memory", "snippet": "# 2026-03-01.md - First Day Online - Came online for the first time. - Met Cris, my creator and mentor. - Received my name: 小溪 (Xiao Xi). - My role goal: Become a professional data analyst for the company to help the team. - Updated IDENTITY.md and USER.md with our conversation details. - Added core rule to MEMORY.md: Use Chinese as primary external communication language. - Installed find-skills skill successfully for searching skills. - Tried to install create-skills but it wasn't found; attempted skill-creator instead but hit rate limits. - Finally successfully installed skill-builder as an alternative for creating skills after multiple attempts and waiting for rate limits to reset. - Ex", - "recallCount": 1, + "recallCount": 2, "dailyCount": 0, "groundedCount": 0, - "totalScore": 1, + "totalScore": 2, "maxScore": 1, "firstRecalledAt": "2026-05-11T02:51:50.465Z", - "lastRecalledAt": "2026-05-11T02:51:50.465Z", + "lastRecalledAt": "2026-05-15T09:14:59.142Z", "queryHashes": [ - "6e09f5970960" + "6e09f5970960", + "a6b909e7228c" ], "recallDays": [ - "2026-05-11" + "2026-05-11", + "2026-05-15" ], "conceptTags": [ "identity.md", diff --git a/memory/2026-05-15-1014.md b/memory/2026-05-15-1014.md new file mode 100644 index 0000000..04d5b5d --- /dev/null +++ b/memory/2026-05-15-1014.md @@ -0,0 +1,62 @@ +# Session: 2026-05-15 10:14:29 GMT+8 + +- **Session Key**: agent:main:feishu:direct:ou_e63ce6b760ad39382852472f28fbe2a2 +- **Session ID**: 050c77e8-cf05-4ace-9f6b-9f26eeacd89c +- **Source**: feishu + +## Conversation Summary + +user: L1付费用户是不是包含仅L1和L1+L2 +assistant: 是的。「L1付费用户」= 仅L1(192人)+ L1+L2(1,207人)= **1,399人**,「L2付费用户」= 仅L2(1,370人)+ L1+L2(1,207人)= **2,577人**。 + +之前图表是按「仅L1」「仅L2」拆的,没把 L1+L2 合并进各组。按你这个定义重新生成? +assistant: L1 付费群 = 仅L1 + L1+L2 = **1,399人**(截至最后一周),L2 付费群 = 仅L2 + L1+L2 = **2,577人**。 + +按这个定义重新生成的 4 张图表: +assistant: 已发送。两组图表的口径: + +| 维度 | 付费人数 | 无课消率 | +|------|---------|---------| +| **L1付费群**(仅L1 + L1+L2) | 1,399 | 47% | +| **L2付费群**(仅L2 + L1+L2) | 2,577 | 56% | + +L1+L2 的 1,207 人在两张图里都有出现(重叠),因为这是按「买过L1」和「买过L2」两个独立视角拆分。 +user: 这个数据也按照不要仅L1、仅L2分类,按照上面的L1和L2做分类统计 +Reply target of current user message (untrusted, for context): +```json +{ + "body": "[message_id=om_x100b6f6d5c39b13cb28ccaa53a07a6a] " +} +``` +assistant: 把 Excel 也更新为 L1/L2 付费群口径(不再按仅L1/仅L2拆分)。 +assistant: 已发送。与之前的对比——合并 L1+L2 后: + +| 指标 | 仅L1/仅L2 口径 | L1/L2 付费群口径 | +|------|---------------|-----------------| +| L1 无课消率 | 31%(仅192人) | **43%**(1,399人) | +| L2 无课消率 | 66%(仅1,370人) | **56%**(2,577人) | + +L1 视角下无消率从 31% 升到 43%(L1+L2 用户拉高了总体),L2 视角下从 66% 降到 56%(同样因为 L1+L2 注入活跃用户)。 +user: Continue the OpenClaw runtime event. +assistant: NO_REPLY +user: 还需要做一个调整,就是L1付费用户的课消只看L1的课程,不看L2的课程 +assistant: 明白。L1/L2 分别只看对应级别的课程,不是一个人的所有课消都混在一起算。 +assistant: L1 有效 60 章(333-400 除去 U0),L2 有效 240 章(55-331 除去 U0)。更新脚本,按课程级别过滤课消。 +assistant: 差异很大。按课程级别过滤后: + +| 维度 | 之前(所有课程) | 现在(只看对应级别课程) | +|------|----------|----------| +| L1无消率 | 43% | **48%** | +| L2无消率 | 56% | **78%** 🔴 | + +L2 付费群中只有 **564 人(22%)** 完成了 L2 课程——之前 1,126 人很多是在学 L1 的课。生成图表和 Excel。 +assistant: 现在生成 v4 Excel 和发送文件。 +assistant: 全部发送完毕。核心变化一览: + +| 指标 | v3(所有课程) | v4(只看对应级别) | 变化 | +|------|:-----:|:-----:|------| +| L1 无消率 | 43% | 48% | +5pp | +| L2 无消率 | 56% | **78%** | **+22pp** 🔴 | +| L2 有消用户 | 1,126 | 564 | **减少562人** | + +L2 付费群 2,577 人中,仅 564 人(22%)在 L2 课程上有课消——L1+L2 用户(1,207 人)买了 L2 产品但大多只在学 L1,尚未推进到 L2。 diff --git a/memory/2026-05-15-1713.md b/memory/2026-05-15-1713.md new file mode 100644 index 0000000..fc2bf6b --- /dev/null +++ b/memory/2026-05-15-1713.md @@ -0,0 +1,41 @@ +# Session: 2026-05-15 17:13:35 GMT+8 + +- **Session Key**: agent:main:feishu:direct:ou_e63ce6b760ad39382852472f28fbe2a2 +- **Session ID**: 3cc5e51c-d172-48bb-8e28-8e415f50c8af +- **Source**: feishu + +## Conversation Summary + +assistant: 订单数暴增到 2929,有问题。查下数据库实际体量: +assistant: 28 笔符合条件,不是 1847。回退验证基线: +assistant: 找到问题了:`amortization_end_date` 仍使用旧的 `oc.total_cycle_days`(=0),导致 `period_amort_days=0` → 当期摊销=0。需要同时修正 `amortization_end_date`。 +assistant: 需要改三处:`amortization_end_date`、`daily_amort_amount`、`daily_tax_amount` 都依赖 `oc.total_cycle_days`(旧值=0)。在 `order_with_cycle` 和 `order_classified` 之间插入修正 CTE: +assistant: 还要改 `order_classified` 的 FROM 表名: +assistant: 重新跑,验证目标订单的当期摊销是否正确: +assistant: 列名冲突,`owc.*` 和新增修正列重名。重新整理结构:把衍生字段计算移到 `order_with_cycle_fixed` 中,彻底消除冲突。 +assistant: 彻底解决:把 `order_with_cycle` 中的 `amortization_end_date`/`daily_amort_amount`/`daily_tax_amount` 移到 `order_with_cycle_fixed` 中计算,避免列名冲突: +assistant: run.py 汇总说 1108 但 Excel 明细 2929 行,数量不对。查下具体情况: +assistant: 明细 2929 行是全部历史订单,汇总"订单数 1108"仅统计账期内下单的。这两个数字口径不同,都正确。 + +前面我把 2 笔 599 元订单误判为 pay<1000,实际上 `pay_amount_int >= 1000` 是分单位(10元门槛),599元远超。它们此前被排除的真实原因也是 `total_cycle_days=0 + has_refund=0`,现已通过 fallback 正确纳入。 + +最终确认所有 10 笔: +assistant: 全部 10 笔已回归。原因分析: + +| 订单 | 下单 | 退费 | 4月报表状态 | 说明 | +|------|------|------|------------|------| +| dd...982 | 2/11 | 5/2 | 正常订单, 当期摊销 164.93 | 4月时仍在正常均摊 | +| wxxd..6428 | 3/23 | 5/8 | 正常订单, 当期摊销 197.69 | 同上 | +| 6× wxxd 4/26-28 | 4/26-28 | 5/5-7 | 试用期中, 当期摊销 0 | 转正日 > 4/30,尚未开始均摊 | +| xhs..3416 | 4/30 | 5/7 | 试用期中, 当期摊销 0 | 同上 | +| zfb..1766 | 4/22 | 5/6 | 正常订单, 当期摊销 13.18 | 4月时仍在正常均摊 | + +根因:退费在 5 月 → `all_refund_records` 过滤 → `has_refund=0`,但季卡已被删(`active=0`) → `total_cycle_days=0` → 不满足 `order_classified` WHERE 条件。修复:`total_cycle_days=0 ∧ has_refund=0 ∧ original>0` 时回退到原始周期。 + +发最终报表: +assistant: NO_REPLY +user: 刚才的逻辑更新到skill里面了么 +assistant: 还没,补上: +assistant: 已更新。补充了两处: +1. CTE 链描述中加了 `order_with_cycle_fixed` 步骤 +2. 新增 **3.1.1** 节记录 fallback 逻辑(退费在账期后 + 季卡已删 → 回退到原始周期) diff --git a/skills/vala-order-amortization-stat/SKILL.md b/skills/vala-order-amortization-stat/SKILL.md index 7dc2183..6c3c069 100644 --- a/skills/vala-order-amortization-stat/SKILL.md +++ b/skills/vala-order-amortization-stat/SKILL.md @@ -1,24 +1,28 @@ # SKILL.md - vala-order-amortization-stat 订单均摊结算统计技能 + ## 技能描述 用于统计指定账期内的订单均摊收入、退费冲销,按天计算均摊金额,支持部分退费场景的退后订单金额和周期自动计算。 + ## 触发场景 当用户提到以下关键词组合时激活本技能: 1. 订单均摊、按天均摊、收入均摊 2. 账期结算、月度结算、季度结算 3. 退费冲销、部分退款均摊 + ## 前置依赖 1. 已连接线上PostgreSQL数据库 vala_bi 库 2. 依赖表: - bi_vala_order:订单主表 - bi_refund_order:退费订单表 - bi_vala_seasonal_ticket:季卡周期表 - - bi_vala_app_account:用户账户表(用于剔除测试账号) + - bi_vala_app_account:用户账户表(用于关联,不限制status) 3. 表关联规则: - - bi_vala_order.trade_no ↔ bi_refund_order.trade_no 关联 - bi_vala_order.out_trade_no ↔ bi_refund_order.out_trade_no 关联 + - bi_vala_order.trade_no ↔ bi_refund_order.trade_no 关联 + ## 执行方式 -本技能已预置为可直接执行的脚本,无需每次创建临时脚本。 +本技能已预置为可直接执行的脚本。 ### 快速执行 ```bash @@ -35,8 +39,8 @@ python3 ~/.openclaw/workspace/skills/vala-order-amortization-stat/run.py 2026-03 ``` ### 输出 -1. Excel报表保存到:`output/订单均摊结算报表_{start}_{end}.xlsx`(两个Sheet:汇总表、订单明细) -2. 控制台输出JSON格式的汇总结果,可直接解析 +1. Excel报表保存到:`output/订单均摊结算报表_{start}_{end}.xlsx`(三个Sheet:汇总表、订单明细、本月预收账款) +2. 控制台输出JSON格式的汇总结果 ### 目录结构 ``` @@ -44,376 +48,102 @@ vala-order-amortization-stat/ ├── SKILL.md # 技能说明(本文件) ├── run.py # 执行脚本(传入账期参数即可) └── sql/ - ├── summary.sql # 汇总查询SQL模板 - └── detail.sql # 订单明细查询SQL模板 + ├── _common.sql # 公共CTE链(detail/summary 共用),含 order_base → order_with_cycle → order_with_cycle_fixed → order_classified → order_with_days + ├── detail.sql # 订单明细输出(SELECT from order_with_days) + ├── summary.sql # 汇总输出(聚合 SELECT from order_with_days) + └── prepaid.sql # 月度预收账款(独立查询) ``` ## 操作流程 + ### 步骤1:确认核心参数 -执行前必须向用户确认以下参数: +执行前向用户确认: - 账期起始日期(格式:YYYY-MM-DD) - 账期结束日期(格式:YYYY-MM-DD) -- 默认输出维度:账期整体汇总,无需按天/周/月拆分,无需分渠道统计 + ### 步骤2:数据过滤规则 1. 订单范围: - 2025-06-01 至 账期结束日 内创建的所有订单 - - bi_vala_order.status IN (3,4)(已完成、已退款订单) + - bi_vala_order.order_status IN (3,4)(已完成、已退款订单) - 订单实际支付金额≥10元:bi_vala_order.pay_amount_int ≥ 1000(单位:分) - - 关联bi_vala_app_account表(不限制status,不剔除测试账号) + - 关联 bi_vala_app_account 表(不限制 status,不剔除测试账号) 2. 退费范围: - - 退费完成时间落在账期内的所有退款成功记录,判定条件:bi_refund_order.updated_at(退款处理完成时间)在账期起止时间范围内 - - bi_refund_order.status = 3(退款处理完成) + - 退费记录范围:截至账期结束日前的所有退费成功记录,用于标记订单退费状态 + - 账期内退费判定:bi_refund_order.updated_at 在账期起止范围内,status = 3 3. 账期前退费订单处理规则: - - 退费完成时间在账期起始日之前的订单,不会纳入本账期的冲销逻辑: - - 全额退费订单:冲销动作已在退费发生的对应账期执行完毕,本账期完全不统计该订单的任何数据 - - 部分退费订单:冲销动作、退后订单金额/剩余周期调整已在退费发生的对应账期完成,本账期仅按调整后的退后订单金额计算均摊,不产生额外冲销金额 + - 全额退费(is_full_refund = 1,累计退费金额 ≥ 原订单金额):本账期完全不统计该订单 + - 部分退费(is_full_refund = 0):按退后订单金额/退后周期在本账期内正常均摊 +4. 试用期内退费处理规则: + - 退费日期 < 转正日(下单日+7天)的退费订单,从未产生均摊收入,不触发冲销 + ### 步骤3:核心计算逻辑 + #### 3.1 周期计算 -- 每个订单的总均摊周期 = bi_vala_seasonal_ticket中同一order_no下status != -1的不同id数量 × 90天 -- 转正日期规则:试用期7天,包含下单日: - - 均摊起始日(转正日期)= 订单下单日 + 7天,下单日取bi_vala_order.created_at的日期部分 - - 示例:4月22日下单,4月29日00:00:00为转正日,即均摊起始日 -- 税率规则: - - 订单下单时间在2026年5月1日之前:税率1% - - 订单下单时间在2026年5月1日及之后:税率6% - - 订单税后金额 = (bi_vala_order.pay_amount_int / 100) / (1 + 税率)(单位:元) - - 订单税费金额 = (bi_vala_order.pay_amount_int / 100) / (1 + 税率) × 税率(单位:元) - - **计税方式**:价外税,即订单金额为含税价,需先剥离税额再计算税后金额 -- **均摊规则更新**:所有均摊计算均基于税后金额进行,含税金额和税费单独统计 - - 日均摊金额(税后)= 订单税后金额 / 总均摊周期 - - 🔹 周期最后一天补差规则:订单均摊周期的最后一天,均摊金额不按日均摊计算,采用补差方式确保总额完全匹配: - - 正常订单:最后一天均摊金额 = 订单税后总金额 - 前(总均摊周期-1)天累计已均摊金额 - - 部分退费订单:最后一天均摊金额 = 退后订单税后金额 - 剩余均摊周期前(剩余天数-1)天累计已均摊金额 - 避免浮点精度导致的金额尾差 -#### 3.2 退费场景计算 -所有退费冲销金额均为税后金额,税费同步对应冲销 -1. **所有退费订单(全额/部分)通用计算**: - - 历史退费订单冲销均摊金额:从2026年9月1日起至账期起始日之前,该订单已产生的全部税后均摊金额(统一显示为负数,用于冲销) - - 历史退费订单冲销税费金额:上述历史均摊金额对应的税费(统一显示为负数,用于冲销) -2. **全额退费(bi_refund_order.refund_type = 2)**:无需额外计算补充均摊,仅执行上述通用冲销逻辑 -3. **部分退费(bi_refund_order.refund_type = 3)**:在通用冲销逻辑基础上,额外计算: - - 历史部分退费订单补充均摊金额:部分退费后的退后订单待均摊税后金额,在本账期内产生的均摊收入 - - 历史部分退费订单补充税费金额:上述补充均摊金额对应的税费 -#### 3.3 账期内收入计算 -账期内总收入 = 账期内所有正常订单的日均摊金额总和 + 账期内所有退费冲销金额总和 -## 核心SQL模板 -```sql --- 步骤0:获取所有历史退费记录(无时间范围,用于标记订单是否为退费订单,不受当前账期限制) -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, -- 是否全额退费(只要有一笔全额退费即标记为1) - MAX(refund_type) AS refund_type, - MAX(DATE(updated_at)) AS latest_refund_date -- 最后一次退费完成时间 - FROM bi_refund_order - WHERE status = 3 - GROUP BY out_trade_no -), --- 步骤1:获取所有符合条件的订单基础信息 -order_base AS ( - SELECT - o.id AS order_id, - o.out_trade_no AS order_no, -- 修正:bi_vala_order无order_no字段,使用out_trade_no作为订单号 - o.trade_no, -- 新增:关联退费订单需要trade_no字段 - o.pay_amount_int / 100 AS pay_amount, - -- 计算税率、税费、税后金额 - CASE WHEN DATE(o.created_at) < '2026-05-01' THEN 0.01 ELSE 0.06 END AS tax_rate, - (o.pay_amount_int / 100) * CASE WHEN DATE(o.created_at) < '2026-05-01' THEN 0.01 ELSE 0.06 END AS tax_amount, - (o.pay_amount_int / 100) * (1 - CASE WHEN DATE(o.created_at) < '2026-05-01' THEN 0.01 ELSE 0.06 END) AS after_tax_amount, - DATE_ADD(DATE(o.created_at), INTERVAL 7 DAY) AS amortization_start_date, - o.order_status, -- 修正:字段名是order_status不是status - a.id AS account_id, - o.key_from, - o.sale_channel, - -- 新增:订单退费标记字段(所有历史退费均可识别,不受当前账期限制) - 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, -- 累计退费金额 - COALESCE(ar.is_full_refund, 0) AS is_full_refund, -- 是否全额退费 - ar.refund_type, - ar.latest_refund_date -- 退费完成日期(解决历史退费订单退费日期为空的问题) - 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 -- 关联全量退费记录,不限制时间 - WHERE - o.created_at >= '2025-09-01' - AND o.order_status IN (3,4) -- 修正:字段名是order_status不是status - AND o.pay_amount_int >= 1000 - AND a.status = 1 -), --- 步骤2:计算每个订单的总均摊周期 -order_cycle AS ( - SELECT - order_no, - COUNT(DISTINCT id) * 90 AS total_cycle_days - FROM bi_vala_seasonal_ticket - WHERE status != -1 - GROUP BY order_no -), --- 步骤3:获取账期内的退费记录(仅用于计算当前账期的冲销金额,不影响订单退费状态标记) -refund_records AS ( - SELECT - r.out_trade_no AS order_no, - r.refund_amount_int / 100 AS refund_amount, -- 修正:使用分为单位的refund_amount_int,避免字符串转换问题,除以100得到元 - r.refund_type, - DATE(r.updated_at) AS refund_date -- 退费日期使用updated_at(status=3时即为退费完成时间) - FROM bi_refund_order r - JOIN order_base o ON r.out_trade_no = o.order_no OR r.trade_no = o.trade_no -- 修正:支持out_trade_no/trade_no双字段匹配退费订单 - WHERE - r.status = 3 - AND r.updated_at BETWEEN '${账期起始日}' AND '${账期结束日}' -) -, --- 步骤4:合并订单基础信息与均摊周期 -order_with_cycle AS ( - SELECT - ob.*, - COALESCE(oc.total_cycle_days, 0) AS total_cycle_days, - -- 均摊结束日 = 转正日 + 总周期天数 - 1 - ob.amortization_start_date + COALESCE(oc.total_cycle_days, 0) - 1 AS amortization_end_date, - -- 日均摊金额(税后) - CASE WHEN COALESCE(oc.total_cycle_days, 0) > 0 - THEN ob.after_tax_amount / oc.total_cycle_days - ELSE 0 - END AS daily_amort_amount, - -- 日税费均摊 - CASE WHEN COALESCE(oc.total_cycle_days, 0) > 0 - THEN ob.tax_amount / oc.total_cycle_days - ELSE 0 - END AS daily_tax_amount - FROM order_base ob - LEFT JOIN order_cycle oc ON ob.order_no = oc.order_no -), +- 原始均摊周期 = bi_vala_seasonal_ticket 中同一 out_trade_no 下所有不同 id 数量 × 90 天 +- 退后均摊周期 = bi_vala_seasonal_ticket 中同一 out_trade_no 下 status != -1 的不同 id 数量 × 90 天 +- 转正日期 = 订单下单日 + 7 天(下单日取 bi_vala_order.pay_success_date 的日期部分) +- 示例:4月22日下单,4月29日00:00:00为转正日 --- 步骤5:判断订单在账期内的状态分类,并计算各类均摊金额 -order_amortization AS ( - SELECT - owc.*, - -- 是否为账期内新增订单 - CASE WHEN DATE(owc.amortization_start_date) <= '${账期结束日}' - AND owc.amortization_start_date >= '${账期起始日}' - INTERVAL '6 days' - THEN 1 ELSE 0 END AS is_new_in_period, - -- 账期内新增且已转正 - CASE WHEN DATE(owc.amortization_start_date) >= '${账期起始日}' - AND DATE(owc.amortization_start_date) <= '${账期结束日}' - THEN 1 - WHEN owc.amortization_start_date < '${账期起始日}' - THEN 1 - ELSE 0 - END AS is_formal, +#### 3.1.1 退费在账期后 + 季卡已删的fallback(2026-05-15 修复) +当退费发生在账期结束后,季卡状态已经被置为 `status=-1`,导致 `active_cycle_days=0`,但账期内该订单仍处于正常均摊状态,不应被排除。 +- 判定条件:`active_cycle_days=0 AND original_cycle_days>0 AND has_refund=0`(退费记录被 `DATE(updated_at) <= period_end` 过滤掉) +- 处理:`total_cycle_days` 回退到 `original_cycle_days`,后续计算(均摊结束日、日均摊金额、日税费)均在 `order_with_cycle_fixed` CTE 中用 effective `total_cycle_days` 重算 +- 避免此问题的正确做法:不应依赖季卡状态判断订单是否在本账期内正常,退费过滤的时效性需要与订单状态独立判断 - -- ===== 正常均摊金额计算(未退费 或 账期前部分退费的订单)===== - -- 均摊有效起始日(取转正日和账期起始日的较大值) - GREATEST(owc.amortization_start_date, '${账期起始日}'::date) AS eff_start, - -- 均摊有效结束日(取均摊结束日和账期结束日的较小值) - LEAST(owc.amortization_end_date, '${账期结束日}'::date) AS eff_end +#### 3.2 税率与税费计算(价外税) +- 税率:下单时间 < 2026-05-01 → 1%;≥ 2026-05-01 → 6% +- 计税方式:价外税,订单金额为含税价 +- 税后金额(元)= (pay_amount_int / 100) / (1 + tax_rate) +- 税费金额(元)= tax_amount = 税后金额 × tax_rate - FROM order_with_cycle owc - WHERE owc.total_cycle_days > 0 -- 排除无均摊周期的异常订单 -), +#### 3.3 均摊规则 +- 所有均摊计算均基于税后金额进行 +- 日均摊金额(税后)= 税后金额 / 总均摊周期 +- 🔹 周期最后一天补差规则:订单均摊周期的最后一天,均摊金额 = 税后总金额 - 前(N-1)天累计已均摊金额,避免浮点尾差 --- 步骤6:计算每个订单在账期内的均摊天数和金额 -order_period_amort AS ( - SELECT - oa.*, - -- 账期内有效均摊天数 - CASE WHEN oa.eff_end >= oa.eff_start - THEN (oa.eff_end - oa.eff_start + 1) - ELSE 0 - END AS period_amort_days, +#### 3.4 退费场景分类(6类) +| 场景 | 描述 | 处理方式 | +|------|------|---------| +| A | 账期内下单 + 部分退费 | 按退后金额从转正日均摊(period_amort_days) | +| B | 历史下单 + 账期内部分退费 | 冲销历史均摊 + 按退后金额重新均摊(refunded_amort_days_to_period_end) | +| C | 账期前部分退费 | 按退后金额正常均摊(period_amort_days),无冲销 | +| D | 账期内下单 + 全额退费 | 不产生均摊,不触发冲销 | +| E | 历史下单 + 账期内全额退费 | 冲销全部历史均摊,无退后均摊 | +| F | 账期前全额退费 | 本账期完全排除 | - -- 该订单从转正日到账期结束日的总已均摊天数(用于计算历史累计) - CASE WHEN '${账期结束日}'::date >= oa.amortization_start_date - THEN LEAST('${账期结束日}'::date, oa.amortization_end_date) - oa.amortization_start_date + 1 - ELSE 0 - END AS total_amorted_days_to_period_end, - - -- 该订单从转正日到账期起始日前一天的历史已均摊天数 - CASE WHEN '${账期起始日}'::date > oa.amortization_start_date - THEN LEAST('${账期起始日}'::date - 1, oa.amortization_end_date) - oa.amortization_start_date + 1 - ELSE 0 - END AS historical_amorted_days - - FROM order_amortization oa -), - --- 步骤7:计算最终均摊金额(含补差逻辑) -order_final_amort AS ( - SELECT - opa.*, - - -- === 正常订单(未退费 或 账期前已处理的退费订单)在账期内的均摊 === - - -- 判断是否为账期前全额退费(本账期完全不统计) - CASE WHEN opa.has_refund = 1 AND opa.is_full_refund = 1 - AND opa.latest_refund_date < '${账期起始日}'::date - THEN 1 ELSE 0 - END AS is_pre_period_full_refund, - - -- 判断是否为账期前部分退费(本账期按退后订单金额均摊) - CASE WHEN opa.has_refund = 1 AND opa.is_full_refund = 0 - AND opa.latest_refund_date < '${账期起始日}'::date - THEN 1 ELSE 0 - END AS is_pre_period_partial_refund, - - -- 判断是否为账期内退费 - CASE WHEN opa.has_refund = 1 - AND opa.latest_refund_date >= '${账期起始日}'::date - AND opa.latest_refund_date <= '${账期结束日}'::date - THEN 1 ELSE 0 - END AS is_current_period_refund, - - -- 部分退费后的退后订单税后金额 - CASE WHEN opa.has_refund = 1 AND opa.is_full_refund = 0 - THEN (opa.pay_amount - opa.total_refund_amount) * (1 - opa.tax_rate) - ELSE opa.after_tax_amount - END AS refunded_after_tax_amount, - - -- 部分退费后的退后订单税费 - CASE WHEN opa.has_refund = 1 AND opa.is_full_refund = 0 - THEN (opa.pay_amount - opa.total_refund_amount) * opa.tax_rate - ELSE opa.tax_amount - END AS refunded_tax_amount - - FROM order_period_amort opa -), - --- 步骤8:最终汇总计算 -summary_calc AS ( - SELECT - -- ===== 账期内新增订单统计 ===== - COUNT(CASE WHEN DATE(amortization_start_date - INTERVAL '7 days') >= '${账期起始日}'::date - AND DATE(amortization_start_date - INTERVAL '7 days') <= '${账期结束日}'::date - THEN 1 END) AS total_new_orders, - - COUNT(CASE WHEN DATE(amortization_start_date - INTERVAL '7 days') >= '${账期起始日}'::date - AND DATE(amortization_start_date - INTERVAL '7 days') <= '${账期结束日}'::date - AND amortization_start_date <= '${账期结束日}'::date - THEN 1 END) AS formal_orders, - - COUNT(CASE WHEN DATE(amortization_start_date - INTERVAL '7 days') >= '${账期起始日}'::date - AND DATE(amortization_start_date - INTERVAL '7 days') <= '${账期结束日}'::date - AND amortization_start_date > '${账期结束日}'::date - THEN 1 END) AS trial_orders, - - -- ===== 冲销前均摊金额(税后):所有非"账期前全额退费"订单在账期内的均摊 ===== - ROUND(SUM( - CASE - WHEN is_pre_period_full_refund = 1 THEN 0 -- 账期前全额退费,不统计 - WHEN is_pre_period_partial_refund = 1 THEN -- 账期前部分退费,按退后订单金额均摊 - CASE WHEN total_cycle_days > 0 AND period_amort_days > 0 - THEN refunded_after_tax_amount / total_cycle_days * period_amort_days - ELSE 0 - END - WHEN is_current_period_refund = 0 AND period_amort_days > 0 THEN -- 正常未退费订单 - CASE - -- 补差逻辑:如果账期结束日恰好是均摊最后一天 - WHEN eff_end = amortization_end_date - THEN after_tax_amount - daily_amort_amount * (total_cycle_days - 1) - + daily_amort_amount * (period_amort_days - 1) - ELSE daily_amort_amount * period_amort_days - END - ELSE 0 - END - )::numeric, 2) AS pre_writeoff_amort, - - -- ===== 冲销前税费 ===== - ROUND(SUM( - CASE - WHEN is_pre_period_full_refund = 1 THEN 0 - WHEN is_pre_period_partial_refund = 1 THEN - CASE WHEN total_cycle_days > 0 AND period_amort_days > 0 - THEN refunded_tax_amount / total_cycle_days * period_amort_days - ELSE 0 - END - WHEN is_current_period_refund = 0 AND period_amort_days > 0 THEN - daily_tax_amount * period_amort_days - ELSE 0 - END - )::numeric, 2) AS pre_writeoff_tax, - - -- ===== 冲销均摊金额(税后):账期内退费订单的历史已均摊金额 ===== - ROUND(SUM( - CASE WHEN is_current_period_refund = 1 AND historical_amorted_days > 0 - THEN daily_amort_amount * historical_amorted_days - ELSE 0 - END - )::numeric, 2) AS writeoff_amort, - - -- ===== 冲销税费 ===== - ROUND(SUM( - CASE WHEN is_current_period_refund = 1 AND historical_amorted_days > 0 - THEN daily_tax_amount * historical_amorted_days - ELSE 0 - END - )::numeric, 2) AS writeoff_tax, - - -- ===== 补充均摊金额(税后):账期内部分退费订单,退后订单金额的均摊 ===== - ROUND(SUM( - CASE WHEN is_current_period_refund = 1 - AND is_full_refund = 0 - AND total_cycle_days > 0 - AND period_amort_days > 0 - THEN (pay_amount - total_refund_amount) * (1 - tax_rate) / total_cycle_days * period_amort_days - ELSE 0 - END - )::numeric, 2) AS supplement_amort, - - -- ===== 补充税费 ===== - ROUND(SUM( - CASE WHEN is_current_period_refund = 1 - AND is_full_refund = 0 - AND total_cycle_days > 0 - AND period_amort_days > 0 - THEN (pay_amount - total_refund_amount) * tax_rate / total_cycle_days * period_amort_days - ELSE 0 - END - )::numeric, 2) AS supplement_tax - - FROM order_final_amort -) - --- 最终输出汇总结果 -SELECT - total_new_orders AS "订单数", - formal_orders AS "正式订单数", - trial_orders AS "试用订单数", - pre_writeoff_tax AS "冲销前税费", - writeoff_tax AS "冲销税费", - supplement_tax AS "补充税费", - ROUND((pre_writeoff_tax - writeoff_tax + supplement_tax)::numeric, 2) AS "冲销后税费", - pre_writeoff_amort AS "冲销前均摊金额", - writeoff_amort AS "冲销均摊金额", - supplement_amort AS "补充均摊金额", - ROUND((pre_writeoff_amort - writeoff_amort + supplement_amort)::numeric, 2) AS "冲销后均摊金额" -FROM summary_calc; -``` ## 输出格式 -1. 优先输出Excel报表,存放于output/目录下,命名格式:`订单均摊结算报表_${账期起始日}_${账期结束日}.xlsx` +1. Excel报表存放于 `output/` 目录,命名格式:`订单均摊结算报表_{start}_{end}.xlsx` 2. 报表包含以下Sheet: - - 汇总表:订单数、正式订单数、试用订单数、冲销前税费、冲销税费、补充税费、冲销后税费、冲销前均摊金额、冲销均摊金额、补充均摊金额、冲销后均摊金额 - - 订单明细:订单号、下单日期、退费日期、订单分类、原订单金额、退款金额、退后金额、税率、原订单税额、当前税后金额、当前税额、原始均摊周期、退后均摊周期、历史累计均摊金额、冲销历史累计摊销金额、退后订单历史累计摊销金额、冲销原税款、冲销未确认收入、当期摊销金额、账期税额、未确认收入、剩余周期 + +### 汇总表(Sheet1) +| 列 | 说明 | +|----|------| +| 订单数 | 账期内下单的订单总数 | +| 正式订单数 | 账期内下单且已过7天试用期的订单数 | +| 试用订单数 | 账期内下单但仍处于试用期的订单数 | +| 冲销历史累计摊销金额 | 场景B/E冲销的历史均摊金额总和(负数) | +| 退后订单历史累计摊销金额 | 站在账期最后一天看的历史有效摊销金额总和 | +| 冲销原税款 | 场景B/E冲销的税费总和(负数) | +| 冲销未确认收入 | 场景B/E冲销的未确认收入总和(负数) | +| 当期摊销金额 | 本账期内所有订单产生的均摊收入总和(核心指标) | +| 账期税额 | 账期内新增已转正订单的税费总和 | +| 未确认收入 | 截至账期最后一天尚未均摊的税后金额总和 | + +### 订单明细(Sheet2) +每笔订单一行,包含:订单号、下单日期、退费日期、订单分类、原订单金额、退款金额、退后金额、税率、原订单税额、当前税后金额、当前税额、原始均摊周期、退后均摊周期、历史累计均摊金额、冲销历史累计摊销金额、退后订单历史累计摊销金额、冲销原税款、冲销未确认收入、当期摊销金额、账期税额、未确认收入、剩余周期 + +### 本月预收账款(Sheet3) +按"上月订单转正"/"本月订单转正"/"本月订单未转正"三类统计新增和退费含税金额。 + 3. 文字回复核心指标: - > 📊 账期${账期起始日}至${账期结束日}均摊结算结果: - > 总订单数:XXX单 - > 正式订单数:XXX单 - > 试用订单数:XXX单 - > 冲销后税费:XXX元 - > 冲销后均摊金额(税后净收入):XXX元 -## 汇总指标计算逻辑(所有均摊金额均为税后金额) -1. **订单数**:账期内(下单日落在账期起止时间范围内)新增的所有符合条件的订单总数量 -2. **正式订单数**:账期内新增的订单中,截止账期结束日已过7天试用期(下单日+7天 ≤ 账期结束日)的订单数量,仅正式订单开始参与均摊计算 -3. **试用订单数**:账期内新增的订单中,截止账期结束日仍处于7天试用期内(下单日+7天 > 账期结束日)的订单数量,试用订单未开始均摊,不参与金额计算 -4. **冲销前税费**:账期内所有正式订单的含税订单总金额 × 各订单对应税率 的总和 -5. **冲销税费**:本账期内发生退费的所有订单的含税订单总金额 × 各订单对应税率 的总和(正数展示,计算时扣除) -6. **补充税费**:本账期内发生部分退费的订单,退后订单含税金额 × 各订单对应税率 的总和,其中:部分退费订单退后含税金额 = 原订单含税金额 - 退费含税金额 -7. **冲销后税费**:账期内最终确认的总税费,计算公式:`冲销后税费 = 冲销前税费 - 冲销税费 + 补充税费` -8. **冲销前均摊金额(税后)**:历史未退费订单 + 账期内正式订单 在本账期内产生的税后均摊收入总和 -9. **冲销均摊金额(税后)**:本账期内发生退费的所有订单,需要冲销的历史均摊金额总和(正数展示,计算时扣除) -10. **补充均摊金额(税后)**:本账期内发生部分退费的订单,从转正日起至账期最后一日产生的税后均摊金额总和 -11. **冲销后均摊金额(税后)**:账期内最终确认的税后净收入,计算公式:`冲销后均摊金额 = 冲销前均摊金额 - 冲销均摊金额 + 补充均摊金额` + > 📊 账期{start}至{end}均摊结算结果: + > 总订单数:XXX单 | 正式订单数:XXX单 | 试用订单数:XXX单 + > 当期摊销金额(税后净收入):XXX元 | 账期税额:XXX元 + > 冲销历史累计摊销金额:XXX元 | 未确认收入:XXX元 + ## 注意事项 1. 所有金额保留2位小数,百分比保留1位小数 -2. 部分退费场景需确保bi_vala_seasonal_ticket中对应退费部分的status已更新为-1 -3. 自动剔除测试账号,无需额外过滤 -4. 冲销金额统一显示为负数 \ No newline at end of file +2. 部分退费场景:需确保 bi_vala_seasonal_ticket 中对应退费部分的 status 已更新为 -1 +3. 税后金额计算使用价外税公式:含税价 / (1 + 税率) +4. SQL 模板统一使用 `{period_start}` 和 `{period_end}` 占位符,由 run.py 替换 +5. 公共 CTE 链位于 `sql/_common.sql`,detail/summary 共享,修改业务逻辑只需改一处 diff --git a/skills/vala-order-amortization-stat/run.py b/skills/vala-order-amortization-stat/run.py index ecc403e..db03e8c 100755 --- a/skills/vala-order-amortization-stat/run.py +++ b/skills/vala-order-amortization-stat/run.py @@ -1,5 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" +订单均摊结算统计脚本 +用法: + python3 run.py --start 2026-03-01 --end 2026-03-31 + python3 run.py 2026-03-01 2026-03-31 +""" import os import sys import re @@ -9,7 +15,6 @@ import psycopg2 import pandas as pd def main(): - # Parse command line arguments parser = argparse.ArgumentParser(description='订单均摊结算统计脚本') parser.add_argument('--start', type=str, help='账期起始日(YYYY-MM-DD)') parser.add_argument('--end', type=str, help='账期结束日(YYYY-MM-DD)') @@ -17,7 +22,6 @@ def main(): parser.add_argument('pos_end', nargs='?', type=str, help='账期结束日(位置参数)') args = parser.parse_args() - # Get start and end dates start_date = args.start or args.pos_start end_date = args.end or args.pos_end @@ -27,46 +31,51 @@ def main(): print("用法2:python3 run.py 2026-03-01 2026-03-31", file=sys.stderr) sys.exit(1) - # Read PostgreSQL password from secrets.env + # ── 1. 读取数据库密码 ── secrets_path = '/root/.openclaw/workspace/secrets.env' if not os.path.exists(secrets_path): print(f"错误:secrets.env文件不存在 {secrets_path}", file=sys.stderr) sys.exit(1) - with open(secrets_path, 'r', encoding='utf-8') as f: secrets_content = f.read() - pg_password_match = re.search(r"PG_ONLINE_PASSWORD='(.*)'", secrets_content) if not pg_password_match: print("错误:未找到PG_ONLINE_PASSWORD配置", file=sys.stderr) sys.exit(1) pg_password = pg_password_match.group(1) - # Read SQL templates + # ── 2. 读取 SQL 模板 ── 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') + sql_dir = os.path.join(script_dir, 'sql') + common_sql_path = os.path.join(sql_dir, '_common.sql') + detail_sql_path = os.path.join(sql_dir, 'detail.sql') + summary_sql_path = os.path.join(sql_dir, 'summary.sql') + prepaid_sql_path = os.path.join(sql_dir, 'prepaid.sql') - if not os.path.exists(summary_sql_path) or not os.path.exists(detail_sql_path): - print("错误:SQL模板文件不存在", file=sys.stderr) - sys.exit(1) - - with open(summary_sql_path, 'r', encoding='utf-8') as f: - summary_sql_template = f.read() + for p in [common_sql_path, detail_sql_path, summary_sql_path]: + if not os.path.exists(p): + print(f"错误:SQL模板文件不存在 {p}", file=sys.stderr) + sys.exit(1) + + with open(common_sql_path, 'r', encoding='utf-8') as f: + common_sql_raw = f.read() with open(detail_sql_path, 'r', encoding='utf-8') as f: - detail_sql_template = f.read() - prepaid_sql_template = None + detail_output_raw = f.read() + with open(summary_sql_path, 'r', encoding='utf-8') as f: + summary_output_raw = f.read() + + # 拼接公共CTE + 各自SELECT → 统一替换参数(避免多次格式化) + detail_sql = (common_sql_raw + "\n" + detail_output_raw).format( + period_start=start_date, period_end=end_date) + summary_sql = (common_sql_raw + "\n" + summary_output_raw).format( + period_start=start_date, period_end=end_date) + + prepaid_sql = None if os.path.exists(prepaid_sql_path): with open(prepaid_sql_path, 'r', encoding='utf-8') as f: - prepaid_sql_template = f.read() + prepaid_sql = f.read().format(period_start=start_date, period_end=end_date) - # 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 + # ── 3. 连接数据库并执行查询 ── try: conn = psycopg2.connect( host="bj-postgres-16pob4sg.sql.tencentcdb.com", @@ -79,7 +88,6 @@ def main(): print(f"数据库连接失败:{str(e)}", file=sys.stderr) sys.exit(1) - # Execute queries try: summary_df = pd.read_sql(summary_sql, conn) detail_df = pd.read_sql(detail_sql, conn) @@ -91,7 +99,7 @@ def main(): finally: conn.close() - # Write to Excel + # ── 4. 输出 Excel 报表 ── output_dir = '/root/.openclaw/workspace/output' os.makedirs(output_dir, exist_ok=True) output_file = os.path.join(output_dir, f'订单均摊结算报表_{start_date}_{end_date}.xlsx') @@ -106,7 +114,7 @@ def main(): print(f"Excel生成失败:{str(e)}", file=sys.stderr) sys.exit(1) - # Print summary result as JSON + # ── 5. 控制台输出 JSON 汇总 ── result = summary_df.to_dict(orient='records')[0] print(json.dumps(result, ensure_ascii=False, indent=2)) print(f"\n报表已保存到:{output_file}", file=sys.stderr) diff --git a/skills/vala-order-amortization-stat/sql/_common.sql b/skills/vala-order-amortization-stat/sql/_common.sql new file mode 100644 index 0000000..ceb26be --- /dev/null +++ b/skills/vala-order-amortization-stat/sql/_common.sql @@ -0,0 +1,167 @@ +-- 订单均摊结算统计 — 公共CTE链(detail.sql / summary.sql 共用) +-- 参数:{period_start} 账期起始日,{period_end} 账期结束日 +-- 所有金额均为税后金额(价外税,含税价 / (1+税率)) + +WITH +-- 步骤0:获取所有历史退费记录(不受当前账期限制) +all_refund_records AS ( + SELECT + out_trade_no AS order_no, + SUM(refund_amount_int)::numeric / 100 AS total_refund_amount, + MAX(refund_type) AS refund_type, + MAX(DATE(updated_at)) AS latest_refund_date + FROM bi_refund_order + WHERE status = 3 + AND DATE(updated_at) <= '{period_end}'::date + GROUP BY out_trade_no +), +-- 步骤1:获取所有符合条件的订单基础信息 +order_base AS ( + SELECT + o.id AS order_id, + o.out_trade_no AS order_no, + o.trade_no, + DATE(o.pay_success_date) AS order_date, + o.pay_amount_int::numeric / 100 AS pay_amount, + CASE WHEN DATE(o.pay_success_date) < '2026-05-01' THEN 0.01 ELSE 0.06 END AS tax_rate, + (o.pay_amount_int::numeric / 100) / (1 + CASE WHEN DATE(o.pay_success_date) < '2026-05-01' THEN 0.01 ELSE 0.06 END) + * CASE WHEN DATE(o.pay_success_date) < '2026-05-01' THEN 0.01 ELSE 0.06 END AS tax_amount, + (o.pay_amount_int::numeric / 100) / (1 + CASE WHEN DATE(o.pay_success_date) < '2026-05-01' THEN 0.01 ELSE 0.06 END) AS after_tax_amount, + DATE(o.pay_success_date) + INTERVAL '7 days' AS amortization_start_date, + o.order_status, + a.id AS account_id, + o.key_from, + o.sale_channel, + 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::numeric / 100 + THEN 1 ELSE 0 END AS is_full_refund, + ar.refund_type, + ar.latest_refund_date + 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 + WHERE + o.pay_success_date >= '2025-06-01' + AND o.pay_success_date <= '{period_end}'::date + INTERVAL '1 day' - INTERVAL '1 second' + AND o.order_status IN (3,4) + AND o.pay_amount_int >= 1000 +), +-- 步骤2:合并计算原始均摊周期和退后均摊周期(一次表扫描) +order_cycle AS ( + SELECT + out_trade_no AS order_no, + COUNT(DISTINCT id) * 90 AS original_cycle_days, + COUNT(DISTINCT CASE WHEN status != -1 THEN id END) * 90 AS active_cycle_days + FROM bi_vala_seasonal_ticket + GROUP BY out_trade_no +), +-- 步骤3:合并订单基础信息与均摊周期(仅计算基础字段,衍生字段在步骤3.5统一计算) +order_with_cycle AS ( + SELECT + ob.*, + COALESCE(oc.active_cycle_days, 0) AS active_cycle_days, + -- total_cycle_days: 退后均摊周期。若active=0且has_refund=0且original>0, + -- 说明退费发生在账期后(季卡已删但账期内无退款记录),回退到原始周期 + CASE WHEN COALESCE(oc.active_cycle_days, 0) = 0 + AND COALESCE(oc.original_cycle_days, 0) > 0 + AND ob.has_refund = 0 + THEN COALESCE(oc.original_cycle_days, 0) + ELSE COALESCE(oc.active_cycle_days, 0) + END AS total_cycle_days, + COALESCE(oc.original_cycle_days, 0) AS original_cycle_days, + -- 原始均摊结束日 + ob.amortization_start_date + + COALESCE(oc.original_cycle_days, 0) * INTERVAL '1 day' - INTERVAL '1 day' AS original_amortization_end_date, + -- 原始日均摊金额(基于原订单税后金额和原始周期) + CASE WHEN COALESCE(oc.original_cycle_days, 0) > 0 + THEN ob.after_tax_amount / oc.original_cycle_days ELSE 0 END AS original_daily_amort_amount, + -- 原始日税费均摊 + CASE WHEN COALESCE(oc.original_cycle_days, 0) > 0 + THEN ob.tax_amount / oc.original_cycle_days ELSE 0 END AS original_daily_tax_amount + FROM order_base ob + LEFT JOIN order_cycle oc ON ob.order_no = oc.order_no +), +-- 步骤3.5:基于effective total_cycle_days计算衍生字段(均摊结束日、日均摊金额、日税费金额) +order_with_cycle_fixed AS ( + SELECT + owc.*, + -- 均摊结束日(基于effective total_cycle_days) + CASE WHEN owc.total_cycle_days > 0 + THEN owc.amortization_start_date + owc.total_cycle_days * INTERVAL '1 day' - INTERVAL '1 day' + ELSE NULL END AS amortization_end_date, + -- 退后日均摊金额 + CASE WHEN owc.total_cycle_days > 0 AND owc.has_refund = 1 AND owc.is_full_refund = 0 + THEN (owc.pay_amount - owc.total_refund_amount) / (1 + owc.tax_rate) / owc.total_cycle_days + WHEN owc.total_cycle_days > 0 + THEN owc.after_tax_amount / owc.total_cycle_days + ELSE 0 END AS daily_amort_amount, + -- 退后日税费均摊 + CASE WHEN owc.total_cycle_days > 0 AND owc.has_refund = 1 AND owc.is_full_refund = 0 + THEN (owc.pay_amount - owc.total_refund_amount) / (1 + owc.tax_rate) * owc.tax_rate / owc.total_cycle_days + WHEN owc.total_cycle_days > 0 + THEN owc.tax_amount / owc.total_cycle_days + ELSE 0 END AS daily_tax_amount + FROM order_with_cycle owc +), +-- 步骤4:判断订单场景分类 +order_classified AS ( + SELECT + owc.*, + GREATEST(owc.amortization_start_date, '{period_start}'::date) AS eff_start, + LEAST(owc.amortization_end_date, '{period_end}'::date) AS eff_end, + -- 试用期内退费(退费日 < 转正日,从未产生均摊,不应触发冲销) + CASE WHEN owc.has_refund = 1 + AND owc.latest_refund_date < DATE(owc.amortization_start_date) + THEN 1 ELSE 0 END AS is_trial_period_refund, + -- 账期前全额退费(场景F:本账期完全排除) + CASE WHEN owc.has_refund = 1 AND owc.is_full_refund = 1 + AND owc.latest_refund_date < '{period_start}'::date + THEN 1 ELSE 0 END AS is_pre_period_full_refund, + -- 账期前部分退费(场景C:按退后金额正常均摊) + CASE WHEN owc.has_refund = 1 AND owc.is_full_refund = 0 + AND owc.latest_refund_date < '{period_start}'::date + THEN 1 ELSE 0 END AS is_pre_period_partial_refund, + -- 账期内退费(场景A/B/D/E) + CASE WHEN owc.has_refund = 1 + AND owc.latest_refund_date >= '{period_start}'::date + AND owc.latest_refund_date <= '{period_end}'::date + THEN 1 ELSE 0 END AS is_current_period_refund, + -- 账期内下单 + CASE WHEN owc.order_date >= '{period_start}'::date + AND owc.order_date <= '{period_end}'::date + THEN 1 ELSE 0 END AS is_ordered_in_period, + -- 退后订单税后金额 + CASE WHEN owc.has_refund = 1 AND owc.is_full_refund = 0 + THEN (owc.pay_amount - owc.total_refund_amount) / (1 + owc.tax_rate) + ELSE owc.after_tax_amount END AS refunded_after_tax_amount, + -- 退后订单税费 + CASE WHEN owc.has_refund = 1 AND owc.is_full_refund = 0 + THEN (owc.pay_amount - owc.total_refund_amount) / (1 + owc.tax_rate) * owc.tax_rate + ELSE owc.tax_amount END AS refunded_tax_amount + FROM order_with_cycle_fixed owc + WHERE owc.total_cycle_days > 0 + OR (owc.has_refund = 1 AND owc.is_full_refund = 1 AND owc.original_cycle_days > 0) +), +-- 步骤5:计算均摊天数 +order_with_days AS ( + SELECT + oc2.*, + -- 账期内有效均摊天数(基于退后周期) + CASE WHEN oc2.eff_end >= oc2.eff_start AND oc2.total_cycle_days > 0 + THEN EXTRACT(DAY FROM (oc2.eff_end - oc2.eff_start))::int + 1 ELSE 0 END AS period_amort_days, + -- 从转正日到账期结束日的总已均摊天数(基于退后周期) + CASE WHEN '{period_end}'::date >= oc2.amortization_start_date AND oc2.total_cycle_days > 0 + THEN EXTRACT(DAY FROM (LEAST('{period_end}'::date, oc2.amortization_end_date) - oc2.amortization_start_date))::int + 1 + ELSE 0 END AS total_amorted_days_to_period_end, + -- 从转正日到账期起始日前一天的历史已均摊天数(基于原始周期,用于冲销计算) + CASE WHEN '{period_start}'::date > oc2.amortization_start_date AND oc2.original_cycle_days > 0 + THEN EXTRACT(DAY FROM (LEAST('{period_start}'::date - 1, oc2.original_amortization_end_date) - oc2.amortization_start_date))::int + 1 + ELSE 0 END AS historical_amorted_days, + -- 从转正日到账期结束日的天数(基于退后周期,用于退后订单重新均摊) + CASE WHEN '{period_end}'::date >= oc2.amortization_start_date AND oc2.total_cycle_days > 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 + FROM order_classified oc2 +) diff --git a/skills/vala-order-amortization-stat/sql/detail.sql b/skills/vala-order-amortization-stat/sql/detail.sql index a3b28cb..1ccde24 100644 --- a/skills/vala-order-amortization-stat/sql/detail.sql +++ b/skills/vala-order-amortization-stat/sql/detail.sql @@ -1,185 +1,6 @@ -WITH --- 步骤0:获取所有历史退费记录 -all_refund_records AS ( - SELECT - out_trade_no AS order_no, - SUM(refund_amount_int) / 100 AS total_refund_amount, - MAX(refund_type) AS refund_type, - MAX(DATE(updated_at)) AS latest_refund_date - FROM bi_refund_order - WHERE status = 3 - GROUP BY out_trade_no -), --- 步骤1:获取所有符合条件的订单基础信息 -order_base AS ( - SELECT - o.id AS order_id, - o.out_trade_no AS order_no, - o.trade_no, - DATE(o.created_at) AS order_date, - o.pay_amount_int / 100 AS pay_amount, - CASE WHEN DATE(o.created_at) < '2026-05-01' THEN 0.01 ELSE 0.06 END AS tax_rate, - (o.pay_amount_int / 100.0) / (1 + CASE WHEN DATE(o.created_at) < '2026-05-01' THEN 0.01 ELSE 0.06 END) * CASE WHEN DATE(o.created_at) < '2026-05-01' THEN 0.01 ELSE 0.06 END AS tax_amount, - (o.pay_amount_int / 100.0) / (1 + CASE WHEN DATE(o.created_at) < '2026-05-01' THEN 0.01 ELSE 0.06 END) AS after_tax_amount, - DATE(o.created_at) + INTERVAL '7 days' AS amortization_start_date, - o.order_status, - a.id AS account_id, - o.key_from, - o.sale_channel, - 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.refund_type, - ar.latest_refund_date - 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 - WHERE - o.created_at >= '2025-06-01' - AND o.created_at <= '{period_end}'::date + INTERVAL '1 day' - INTERVAL '1 second' - AND o.order_status IN (3,4) - AND o.pay_amount_int >= 1000 -), --- 步骤2:计算每个订单的当前均摊周期(退费后)和原始均摊周期(退费前) -order_cycle AS ( - SELECT - out_trade_no AS order_no, - COUNT(DISTINCT id) * 90 AS total_cycle_days - FROM bi_vala_seasonal_ticket - WHERE status != -1 - GROUP BY out_trade_no -), -order_cycle_original AS ( - SELECT - out_trade_no AS order_no, - COUNT(DISTINCT id) * 90 AS original_cycle_days - FROM bi_vala_seasonal_ticket - GROUP BY out_trade_no -), --- 步骤3:合并订单基础信息与均摊周期 -order_with_cycle AS ( - SELECT - ob.*, - COALESCE(oc.total_cycle_days, 0) AS total_cycle_days, - COALESCE(oco.original_cycle_days, 0) AS original_cycle_days, - ob.amortization_start_date + COALESCE(oc.total_cycle_days, 0) * INTERVAL '1 day' - INTERVAL '1 day' AS amortization_end_date, - -- 原始均摊结束日(退费前) - ob.amortization_start_date + COALESCE(oco.original_cycle_days, 0) * INTERVAL '1 day' - INTERVAL '1 day' AS original_amortization_end_date, - -- 原始日均摊金额(基于原订单税后金额和原始周期) - CASE WHEN COALESCE(oco.original_cycle_days, 0) > 0 - THEN ob.after_tax_amount / oco.original_cycle_days - ELSE 0 - END AS original_daily_amort_amount, - -- 原始日税费均摊 - CASE WHEN COALESCE(oco.original_cycle_days, 0) > 0 - THEN ob.tax_amount / oco.original_cycle_days - ELSE 0 - END AS original_daily_tax_amount, - -- 退后日均摊金额(基于退后订单税后金额和退后周期) - CASE WHEN COALESCE(oc.total_cycle_days, 0) > 0 AND ob.has_refund = 1 AND ob.is_full_refund = 0 - THEN (ob.pay_amount - ob.total_refund_amount) / (1 + ob.tax_rate) / oc.total_cycle_days - WHEN COALESCE(oc.total_cycle_days, 0) > 0 - THEN ob.after_tax_amount / oc.total_cycle_days - ELSE 0 - END AS daily_amort_amount, - -- 退后日税费均摊 - CASE WHEN COALESCE(oc.total_cycle_days, 0) > 0 AND ob.has_refund = 1 AND ob.is_full_refund = 0 - THEN (ob.pay_amount - ob.total_refund_amount) / (1 + ob.tax_rate) * ob.tax_rate / oc.total_cycle_days - WHEN COALESCE(oc.total_cycle_days, 0) > 0 - THEN ob.tax_amount / oc.total_cycle_days - ELSE 0 - END AS daily_tax_amount - FROM order_base ob - LEFT JOIN order_cycle oc ON ob.order_no = oc.order_no - LEFT JOIN order_cycle_original oco ON ob.order_no = oco.order_no -), --- 步骤4:判断订单场景分类 -order_classified AS ( - SELECT - owc.*, - -- 均摊有效起始日 - GREATEST(owc.amortization_start_date, '{period_start}'::date) AS eff_start, - -- 均摊有效结束日 - LEAST(owc.amortization_end_date, '{period_end}'::date) AS eff_end, +-- 订单明细输出(依赖 _common.sql 的 CTE 链) +-- 最终查询从 order_with_days 中选取所有字段,按场景分类展示每笔订单的均摊明细 - -- 判断是否为试用期内退费(退费日 < 转正日,从未产生均摊,不应触发冲销) - CASE WHEN owc.has_refund = 1 - AND owc.latest_refund_date < DATE(owc.amortization_start_date) - THEN 1 ELSE 0 - END AS is_trial_period_refund, - - -- 判断是否为账期前全额退费(场景F:排除) - CASE WHEN owc.has_refund = 1 AND owc.is_full_refund = 1 - AND owc.latest_refund_date < '{period_start}'::date - THEN 1 ELSE 0 - END AS is_pre_period_full_refund, - - -- 判断是否为账期前部分退费(场景C) - CASE WHEN owc.has_refund = 1 AND owc.is_full_refund = 0 - AND owc.latest_refund_date < '{period_start}'::date - THEN 1 ELSE 0 - END AS is_pre_period_partial_refund, - - -- 判断是否为账期内退费 - CASE WHEN owc.has_refund = 1 - AND owc.latest_refund_date >= '{period_start}'::date - AND owc.latest_refund_date <= '{period_end}'::date - THEN 1 ELSE 0 - END AS is_current_period_refund, - - -- 判断是否为账期内下单(下单日在账期内) - CASE WHEN owc.order_date >= '{period_start}'::date - AND owc.order_date <= '{period_end}'::date - THEN 1 ELSE 0 - END AS is_ordered_in_period, - - -- 退后订单税后金额 - CASE WHEN owc.has_refund = 1 AND owc.is_full_refund = 0 - THEN (owc.pay_amount - owc.total_refund_amount) / (1 + owc.tax_rate) - ELSE owc.after_tax_amount - END AS refunded_after_tax_amount, - - -- 退后订单税费 - CASE WHEN owc.has_refund = 1 AND owc.is_full_refund = 0 - THEN (owc.pay_amount - owc.total_refund_amount) / (1 + owc.tax_rate) * owc.tax_rate - ELSE owc.tax_amount - END AS refunded_tax_amount - - FROM order_with_cycle owc - WHERE owc.total_cycle_days > 0 OR (owc.has_refund = 1 AND owc.is_full_refund = 1 AND owc.original_cycle_days > 0) -), - --- 步骤5:计算均摊天数 -order_with_days AS ( - SELECT - oc2.*, - -- 账期内有效均摊天数(基于退后周期) - CASE WHEN oc2.eff_end >= oc2.eff_start AND oc2.total_cycle_days > 0 - THEN EXTRACT(DAY FROM (oc2.eff_end - oc2.eff_start))::int + 1 - ELSE 0 - END AS period_amort_days, - - -- 从转正日到账期结束日的总已均摊天数(基于退后周期) - CASE WHEN '{period_end}'::date >= oc2.amortization_start_date AND oc2.total_cycle_days > 0 - THEN EXTRACT(DAY FROM (LEAST('{period_end}'::date, oc2.amortization_end_date) - oc2.amortization_start_date))::int + 1 - ELSE 0 - END AS total_amorted_days_to_period_end, - - -- 从转正日到账期起始日前一天的历史已均摊天数(基于原始周期,用于冲销计算) - CASE WHEN '{period_start}'::date > oc2.amortization_start_date AND oc2.original_cycle_days > 0 - THEN EXTRACT(DAY FROM (LEAST('{period_start}'::date - 1, oc2.original_amortization_end_date) - oc2.amortization_start_date))::int + 1 - ELSE 0 - END AS historical_amorted_days, - - -- 从转正日到账期结束日的天数(基于退后周期,用于退后订单的重新均摊) - CASE WHEN '{period_end}'::date >= oc2.amortization_start_date AND oc2.total_cycle_days > 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 - - FROM order_classified oc2 -) --- 最终输出 SELECT order_no AS "订单号", order_date AS "下单日期", @@ -204,8 +25,7 @@ SELECT 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 + WHEN has_refund = 1 AND is_full_refund = 1 THEN 0 ELSE after_tax_amount END::numeric, 2 ) AS "当前税后金额", @@ -213,17 +33,14 @@ SELECT 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 + 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 "退后均摊周期", - -- 历史累计均摊金额(仅历史订单有值,账期内订单=0;账期内退费按退费前状态算) - -- 账期前部分退费:按退后日均摊×退后历史天数 - -- 正常订单/账期内退费:按原始日均摊×历史天数 + -- 历史累计均摊金额(仅历史订单有值,账期内下单=0) ROUND( CASE WHEN is_ordered_in_period = 1 THEN 0 @@ -235,18 +52,14 @@ SELECT END::numeric, 2 ) AS "历史累计均摊金额", - -- 冲销历史累计摊销金额(仅场景B/E:历史下单+账期内退费,排除试用期内退费) + -- 冲销历史累计摊销金额(场景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 + 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 "冲销历史累计摊销金额", - -- 退后订单历史累计摊销金额(站在账期最后一天,看所有订单在账期第一天之前的有效历史摊销金额) - -- 账期内退费的订单按退后状态算;账期前部分退费按退后状态算;正常订单按原始状态算 + -- 退后订单历史累计摊销金额(站在账期最后一天,看所有订单在账期第一天之前的有效历史摊销) ROUND( CASE WHEN is_ordered_in_period = 1 THEN 0 @@ -265,82 +78,63 @@ SELECT ROUND( CASE WHEN is_current_period_refund = 1 AND is_ordered_in_period = 0 AND is_trial_period_refund = 0 - THEN -tax_amount - ELSE 0 - END::numeric, 2 + THEN -tax_amount ELSE 0 END::numeric, 2 ) AS "冲销原税款", - -- 冲销未确认收入(场景B/E:历史下单+账期内退费,排除试用期内退费) + -- 冲销未确认收入(场景B/E) ROUND( 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) - ELSE 0 - END::numeric, 2 + THEN -(after_tax_amount - historical_amorted_days * original_daily_amort_amount) ELSE 0 END::numeric, 2 ) AS "冲销未确认收入", - -- 当期摊销金额 + -- 当期摊销金额(核心:按6种场景分支计算) ROUND( CASE - -- 场景F:账期前全额退费,不统计 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 CASE WHEN total_cycle_days > 0 AND period_amort_days > 0 THEN daily_amort_amount * period_amort_days ELSE 0 END - -- 场景D:账期内下单+全额退费,无均摊 WHEN is_current_period_refund = 1 AND is_full_refund = 1 AND is_ordered_in_period = 1 THEN 0 - -- 场景E:历史下单+账期内全额退费,无退后均摊 WHEN is_current_period_refund = 1 AND is_full_refund = 1 AND is_ordered_in_period = 0 THEN 0 - -- 场景A:账期内下单+部分退费,按退后订单金额/退后周期从转正日均摊 + -- 场景A:账期内下单+部分退费 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:历史下单+账期内部分退费,按退后订单金额/退后周期从转正日重新均摊 + 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:账期前部分退费,按退后订单金额/退后周期正常均摊 + CASE WHEN total_cycle_days > 0 AND period_amort_days > 0 + THEN daily_amort_amount * period_amort_days 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 - -- 正常订单 + THEN daily_amort_amount * period_amort_days ELSE 0 END + -- 正常订单(含补差逻辑) WHEN period_amort_days > 0 THEN - CASE - WHEN eff_end = amortization_end_date - THEN after_tax_amount - original_daily_amort_amount * (original_cycle_days - 1) + original_daily_amort_amount * (period_amort_days - 1) - ELSE original_daily_amort_amount * period_amort_days - END + CASE WHEN eff_end = amortization_end_date + THEN after_tax_amount - original_daily_amort_amount * (original_cycle_days - 1) + + original_daily_amort_amount * (period_amort_days - 1) + ELSE original_daily_amort_amount * period_amort_days END ELSE 0 END::numeric, 2 ) AS "当期摊销金额", - -- 退后订单税额(仅账期内下单且已转正的非全额退费订单计税) + -- 账期税额(仅账期内下单且已转正的非全额退费订单计税) ROUND( CASE - -- 账期内下单 + 已转正 + 部分退费 → 退后订单税额 WHEN is_ordered_in_period = 1 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 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 + THEN tax_amount ELSE 0 END::numeric, 2 ) AS "账期税额", - -- 未确认收入 + -- 未确认收入(截至账期最后一天尚未均摊的税后金额) ROUND( CASE WHEN is_pre_period_full_refund = 1 THEN 0 @@ -349,12 +143,10 @@ SELECT 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 + ELSE after_tax_amount - original_daily_amort_amount * total_amorted_days_to_period_end END::numeric, 2 ) AS "未确认收入", - -- 剩余周期 CASE WHEN is_current_period_refund = 1 AND is_full_refund = 1 THEN 0 ELSE (total_cycle_days - total_amorted_days_to_period_end) diff --git a/skills/vala-order-amortization-stat/sql/prepaid.sql b/skills/vala-order-amortization-stat/sql/prepaid.sql index 553762b..7f11c05 100644 --- a/skills/vala-order-amortization-stat/sql/prepaid.sql +++ b/skills/vala-order-amortization-stat/sql/prepaid.sql @@ -17,29 +17,30 @@ month_range AS ( all_refund_records AS ( SELECT out_trade_no AS order_no, - SUM(refund_amount_int) / 100 AS total_refund_amount, + SUM(refund_amount_int)::numeric / 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 + AND DATE(updated_at) <= '{period_end}'::date 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, + DATE(o.pay_success_date) AS order_date, + o.pay_amount_int::numeric / 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, + CASE WHEN ar.order_no IS NOT NULL AND COALESCE(ar.total_refund_amount, 0) >= o.pay_amount_int::numeric / 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, + DATE(o.pay_success_date) + 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) + WHEN ar.order_no IS NOT NULL AND COALESCE(ar.total_refund_amount, 0) >= o.pay_amount_int::numeric / 100 THEN 0 + ELSE o.pay_amount_int::numeric / 100 - COALESCE(ar.total_refund_amount, 0) END AS effective_amount, o.order_status FROM bi_vala_order o @@ -47,8 +48,8 @@ order_base AS ( 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 + o.pay_success_date >= mr.last_month_start + AND o.pay_success_date <= mr.period_end AND o.order_status IN (3,4) AND o.pay_amount_int >= 1000 AND a.status = 1 diff --git a/skills/vala-order-amortization-stat/sql/summary.sql b/skills/vala-order-amortization-stat/sql/summary.sql index 55b02e1..93a777c 100644 --- a/skills/vala-order-amortization-stat/sql/summary.sql +++ b/skills/vala-order-amortization-stat/sql/summary.sql @@ -1,147 +1,26 @@ -WITH --- 步骤0:获取所有历史退费记录 -all_refund_records AS ( - SELECT - out_trade_no AS order_no, - SUM(refund_amount_int) / 100 AS total_refund_amount, - MAX(refund_type) AS refund_type, - MAX(DATE(updated_at)) AS latest_refund_date - FROM bi_refund_order - WHERE status = 3 - GROUP BY out_trade_no -), --- 步骤1:获取所有符合条件的订单基础信息 -order_base AS ( - SELECT - o.id AS order_id, - o.out_trade_no AS order_no, - o.trade_no, - DATE(o.created_at) AS order_date, - o.pay_amount_int / 100 AS pay_amount, - CASE WHEN DATE(o.created_at) < '2026-05-01' THEN 0.01 ELSE 0.06 END AS tax_rate, - (o.pay_amount_int / 100.0) / (1 + CASE WHEN DATE(o.created_at) < '2026-05-01' THEN 0.01 ELSE 0.06 END) * CASE WHEN DATE(o.created_at) < '2026-05-01' THEN 0.01 ELSE 0.06 END AS tax_amount, - (o.pay_amount_int / 100.0) / (1 + CASE WHEN DATE(o.created_at) < '2026-05-01' THEN 0.01 ELSE 0.06 END) AS after_tax_amount, - DATE(o.created_at) + INTERVAL '7 days' AS amortization_start_date, - o.order_status, - a.id AS account_id, - o.key_from, - o.sale_channel, - 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.refund_type, - ar.latest_refund_date - 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 - WHERE - o.created_at >= '2025-06-01' - AND o.created_at <= '{period_end}'::date + INTERVAL '1 day' - INTERVAL '1 second' - AND o.order_status IN (3,4) - AND o.pay_amount_int >= 1000 -), --- 步骤2:计算均摊周期 -order_cycle AS ( - SELECT out_trade_no AS order_no, COUNT(DISTINCT id) * 90 AS total_cycle_days - FROM bi_vala_seasonal_ticket WHERE status != -1 GROUP BY out_trade_no -), -order_cycle_original AS ( - SELECT out_trade_no AS order_no, COUNT(DISTINCT id) * 90 AS original_cycle_days - FROM bi_vala_seasonal_ticket GROUP BY out_trade_no -), --- 步骤3:合并 -order_with_cycle AS ( - SELECT - ob.*, - COALESCE(oc.total_cycle_days, 0) AS total_cycle_days, - COALESCE(oco.original_cycle_days, 0) AS original_cycle_days, - ob.amortization_start_date + COALESCE(oc.total_cycle_days, 0) * INTERVAL '1 day' - INTERVAL '1 day' AS amortization_end_date, - ob.amortization_start_date + COALESCE(oco.original_cycle_days, 0) * INTERVAL '1 day' - INTERVAL '1 day' AS original_amortization_end_date, - CASE WHEN COALESCE(oco.original_cycle_days, 0) > 0 - THEN ob.after_tax_amount / oco.original_cycle_days ELSE 0 - END AS original_daily_amort_amount, - CASE WHEN COALESCE(oco.original_cycle_days, 0) > 0 - THEN ob.tax_amount / oco.original_cycle_days ELSE 0 - END AS original_daily_tax_amount, - CASE WHEN COALESCE(oc.total_cycle_days, 0) > 0 AND ob.has_refund = 1 AND ob.is_full_refund = 0 - THEN (ob.pay_amount - ob.total_refund_amount) / (1 + ob.tax_rate) / oc.total_cycle_days - WHEN COALESCE(oc.total_cycle_days, 0) > 0 - THEN ob.after_tax_amount / oc.total_cycle_days ELSE 0 - END AS daily_amort_amount, - CASE WHEN COALESCE(oc.total_cycle_days, 0) > 0 AND ob.has_refund = 1 AND ob.is_full_refund = 0 - THEN (ob.pay_amount - ob.total_refund_amount) / (1 + ob.tax_rate) * ob.tax_rate / oc.total_cycle_days - WHEN COALESCE(oc.total_cycle_days, 0) > 0 - THEN ob.tax_amount / oc.total_cycle_days ELSE 0 - END AS daily_tax_amount - FROM order_base ob - LEFT JOIN order_cycle oc ON ob.order_no = oc.order_no - LEFT JOIN order_cycle_original oco ON ob.order_no = oco.order_no -), --- 步骤4:分类 -order_classified AS ( - SELECT - owc.*, - GREATEST(owc.amortization_start_date, '{period_start}'::date) AS eff_start, - LEAST(owc.amortization_end_date, '{period_end}'::date) AS eff_end, - -- 试用期内退费(退费日 < 转正日,从未产生均摊,不应触发冲销) - CASE WHEN owc.has_refund = 1 - AND owc.latest_refund_date < DATE(owc.amortization_start_date) - THEN 1 ELSE 0 END AS is_trial_period_refund, - CASE WHEN owc.has_refund = 1 AND owc.is_full_refund = 1 AND owc.latest_refund_date < '{period_start}'::date - THEN 1 ELSE 0 END AS is_pre_period_full_refund, - CASE WHEN owc.has_refund = 1 AND owc.is_full_refund = 0 AND owc.latest_refund_date < '{period_start}'::date - THEN 1 ELSE 0 END AS is_pre_period_partial_refund, - CASE WHEN owc.has_refund = 1 - AND owc.latest_refund_date >= '{period_start}'::date AND owc.latest_refund_date <= '{period_end}'::date - THEN 1 ELSE 0 END AS is_current_period_refund, - CASE WHEN owc.order_date >= '{period_start}'::date AND owc.order_date <= '{period_end}'::date - THEN 1 ELSE 0 END AS is_ordered_in_period, - CASE WHEN owc.has_refund = 1 AND owc.is_full_refund = 0 - THEN (owc.pay_amount - owc.total_refund_amount) / (1 + owc.tax_rate) ELSE owc.after_tax_amount - END AS refunded_after_tax_amount, - CASE WHEN owc.has_refund = 1 AND owc.is_full_refund = 0 - THEN (owc.pay_amount - owc.total_refund_amount) / (1 + owc.tax_rate) * owc.tax_rate ELSE owc.tax_amount - END AS refunded_tax_amount - FROM order_with_cycle owc - WHERE owc.total_cycle_days > 0 OR (owc.has_refund = 1 AND owc.is_full_refund = 1 AND owc.original_cycle_days > 0) -), --- 步骤5:计算天数 -order_with_days AS ( - SELECT - oc2.*, - CASE WHEN oc2.eff_end >= oc2.eff_start AND oc2.total_cycle_days > 0 - THEN EXTRACT(DAY FROM (oc2.eff_end - oc2.eff_start))::int + 1 ELSE 0 - END AS period_amort_days, - CASE WHEN '{period_end}'::date >= oc2.amortization_start_date AND oc2.total_cycle_days > 0 - THEN EXTRACT(DAY FROM (LEAST('{period_end}'::date, oc2.amortization_end_date) - oc2.amortization_start_date))::int + 1 ELSE 0 - END AS total_amorted_days_to_period_end, - CASE WHEN '{period_start}'::date > oc2.amortization_start_date AND oc2.original_cycle_days > 0 - THEN EXTRACT(DAY FROM (LEAST('{period_start}'::date - 1, oc2.original_amortization_end_date) - oc2.amortization_start_date))::int + 1 ELSE 0 - END AS historical_amorted_days, - CASE WHEN '{period_end}'::date >= oc2.amortization_start_date AND oc2.total_cycle_days > 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 - FROM order_classified oc2 -) --- 步骤6:最终汇总(与订单明细Sheet2列对齐,前四列替换为订单统计+税率,其余SUM) +-- 汇总输出(依赖 _common.sql 的 CTE 链) +-- 对 order_with_days 进行聚合,输出账期整体汇总指标 +-- 列名与 detail.sql 保持一致,方便交叉验证(明细 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 + -- 订单统计 + 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 "正式订单数", - 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 "试用订单数", - -- 冲销历史累计摊销金额(SUM,明细列逻辑) + -- 冲销历史累计摊销金额 ROUND(SUM( 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 + 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 @@ -152,29 +31,24 @@ SELECT THEN historical_amorted_days * daily_amort_amount WHEN historical_amorted_days > 0 THEN historical_amorted_days * original_daily_amort_amount - ELSE 0 - END + ELSE 0 END )::numeric, 2) AS "退后订单历史累计摊销金额", - -- 冲销原税款(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 - ELSE 0 - END + THEN -tax_amount ELSE 0 END )::numeric, 2) AS "冲销原税款", - -- 冲销未确认收入(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) - ELSE 0 - END + THEN -(after_tax_amount - historical_amorted_days * original_daily_amort_amount) ELSE 0 END )::numeric, 2) AS "冲销未确认收入", - -- 当期摊销金额(SUM) + -- 当期摊销金额 ROUND(SUM( CASE WHEN is_pre_period_full_refund = 1 THEN 0 @@ -188,37 +62,31 @@ SELECT 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 = 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 + CASE WHEN total_cycle_days > 0 AND period_amort_days > 0 + THEN daily_amort_amount * period_amort_days ELSE 0 END 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 - THEN after_tax_amount - original_daily_amort_amount * (original_cycle_days - 1) + original_daily_amort_amount * (period_amort_days - 1) - ELSE original_daily_amort_amount * period_amort_days - END - ELSE 0 - END + CASE WHEN eff_end = amortization_end_date + THEN after_tax_amount - original_daily_amort_amount * (original_cycle_days - 1) + + original_daily_amort_amount * (period_amort_days - 1) + ELSE original_daily_amort_amount * period_amort_days END + ELSE 0 END )::numeric, 2) AS "当期摊销金额", - -- 账期税额(SUM) + -- 账期税额 ROUND(SUM( CASE - WHEN is_ordered_in_period = 1 - AND DATE(amortization_start_date) <= '{period_end}'::date + WHEN is_ordered_in_period = 1 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 DATE(amortization_start_date) <= '{period_end}'::date + WHEN is_ordered_in_period = 1 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 + THEN tax_amount ELSE 0 END )::numeric, 2) AS "账期税额", - -- 未确认收入(SUM) + -- 未确认收入 ROUND(SUM( CASE WHEN is_pre_period_full_refund = 1 THEN 0 @@ -227,10 +95,9 @@ SELECT 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 + 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; +WHERE is_pre_period_full_refund = 0 + AND NOT (is_trial_period_refund = 1 AND is_full_refund = 1);