21 KiB
21 KiB
SKILL.md - vala-order-amortization-stat 订单均摊结算统计技能
技能描述
用于统计指定账期内的订单均摊收入、退费冲销,按天计算均摊金额,支持部分退费场景的退后订单金额和周期自动计算。
触发场景
当用户提到以下关键词组合时激活本技能:
- 订单均摊、按天均摊、收入均摊
- 账期结算、月度结算、季度结算
- 退费冲销、部分退款均摊
前置依赖
- 已连接线上PostgreSQL数据库 vala_bi 库
- 依赖表:
- bi_vala_order:订单主表
- bi_refund_order:退费订单表
- bi_vala_seasonal_ticket:季卡周期表
- bi_vala_app_account:用户账户表(用于剔除测试账号)
- 表关联规则:
- bi_vala_order.trade_no ↔ bi_refund_order.trade_no 关联
- bi_vala_order.out_trade_no ↔ bi_refund_order.out_trade_no 关联
执行方式
本技能已预置为可直接执行的脚本,无需每次创建临时脚本。
快速执行
python3 ~/.openclaw/workspace/skills/vala-order-amortization-stat/run.py --start {账期起始日} --end {账期结束日}
示例
# 2026年3月账期
python3 ~/.openclaw/workspace/skills/vala-order-amortization-stat/run.py --start 2026-03-01 --end 2026-03-31
# 也支持位置参数
python3 ~/.openclaw/workspace/skills/vala-order-amortization-stat/run.py 2026-03-01 2026-03-31
输出
- Excel报表保存到:
output/订单均摊结算报表_{start}_{end}.xlsx(两个Sheet:汇总表、订单明细) - 控制台输出JSON格式的汇总结果,可直接解析
目录结构
vala-order-amortization-stat/
├── SKILL.md # 技能说明(本文件)
├── run.py # 执行脚本(传入账期参数即可)
└── sql/
├── summary.sql # 汇总查询SQL模板
└── detail.sql # 订单明细查询SQL模板
操作流程
步骤1:确认核心参数
执行前必须向用户确认以下参数:
- 账期起始日期(格式:YYYY-MM-DD)
- 账期结束日期(格式:YYYY-MM-DD)
- 默认输出维度:账期整体汇总,无需按天/周/月拆分,无需分渠道统计
步骤2:数据过滤规则
- 订单范围:
- 2025-09-01 至 账期结束日 内创建的所有订单
- bi_vala_order.status IN (3,4)(已完成、已退款订单)
- 订单实际支付金额≥10元:bi_vala_order.pay_amount_int ≥ 1000(单位:分)
- 关联bi_vala_app_account表(不限制status,不剔除测试账号)
- 退费范围:
- 退费完成时间落在账期内的所有退款成功记录,判定条件:bi_refund_order.updated_at(退款处理完成时间)在账期起止时间范围内
- bi_refund_order.status = 3(退款处理完成)
- 账期前退费订单处理规则:
- 退费完成时间在账期起始日之前的订单,不会纳入本账期的冲销逻辑:
- 全额退费订单:冲销动作已在退费发生的对应账期执行完毕,本账期完全不统计该订单的任何数据
- 部分退费订单:冲销动作、退后订单金额/剩余周期调整已在退费发生的对应账期完成,本账期仅按调整后的退后订单金额计算均摊,不产生额外冲销金额
- 退费完成时间在账期起始日之前的订单,不会纳入本账期的冲销逻辑:
步骤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 退费场景计算
所有退费冲销金额均为税后金额,税费同步对应冲销
- 所有退费订单(全额/部分)通用计算:
- 历史退费订单冲销均摊金额:从2026年9月1日起至账期起始日之前,该订单已产生的全部税后均摊金额(统一显示为负数,用于冲销)
- 历史退费订单冲销税费金额:上述历史均摊金额对应的税费(统一显示为负数,用于冲销)
- 全额退费(bi_refund_order.refund_type = 2):无需额外计算补充均摊,仅执行上述通用冲销逻辑
- 部分退费(bi_refund_order.refund_type = 3):在通用冲销逻辑基础上,额外计算:
- 历史部分退费订单补充均摊金额:部分退费后的退后订单待均摊税后金额,在本账期内产生的均摊收入
- 历史部分退费订单补充税费金额:上述补充均摊金额对应的税费
3.3 账期内收入计算
账期内总收入 = 账期内所有正常订单的日均摊金额总和 + 账期内所有退费冲销金额总和
核心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
),
-- 步骤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,
-- ===== 正常均摊金额计算(未退费 或 账期前部分退费的订单)=====
-- 均摊有效起始日(取转正日和账期起始日的较大值)
GREATEST(owc.amortization_start_date, '${账期起始日}'::date) AS eff_start,
-- 均摊有效结束日(取均摊结束日和账期结束日的较小值)
LEAST(owc.amortization_end_date, '${账期结束日}'::date) AS eff_end
FROM order_with_cycle owc
WHERE owc.total_cycle_days > 0 -- 排除无均摊周期的异常订单
),
-- 步骤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,
-- 该订单从转正日到账期结束日的总已均摊天数(用于计算历史累计)
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;
输出格式
- 优先输出Excel报表,存放于output/目录下,命名格式:
订单均摊结算报表_${账期起始日}_${账期结束日}.xlsx - 报表包含以下Sheet:
- 汇总表:订单数、正式订单数、试用订单数、冲销前税费、冲销税费、补充税费、冲销后税费、冲销前均摊金额、冲销均摊金额、补充均摊金额、冲销后均摊金额
- 订单明细:订单号、订单金额、税率、税额、税后金额、总均摊周期、已均摊天数、历史均摊金额、账期均摊金额、未确认收入、剩余周期
- 文字回复核心指标:
📊 账期${账期起始日}至${账期结束日}均摊结算结果: 总订单数:XXX单 正式订单数:XXX单 试用订单数:XXX单 冲销后税费:XXX元 冲销后均摊金额(税后净收入):XXX元
汇总指标计算逻辑(所有均摊金额均为税后金额)
- 订单数:账期内(下单日落在账期起止时间范围内)新增的所有符合条件的订单总数量
- 正式订单数:账期内新增的订单中,截止账期结束日已过7天试用期(下单日+7天 ≤ 账期结束日)的订单数量,仅正式订单开始参与均摊计算
- 试用订单数:账期内新增的订单中,截止账期结束日仍处于7天试用期内(下单日+7天 > 账期结束日)的订单数量,试用订单未开始均摊,不参与金额计算
- 冲销前税费:账期内所有正式订单的含税订单总金额 × 各订单对应税率 的总和
- 冲销税费:本账期内发生退费的所有订单的含税订单总金额 × 各订单对应税率 的总和(正数展示,计算时扣除)
- 补充税费:本账期内发生部分退费的订单,退后订单含税金额 × 各订单对应税率 的总和,其中:部分退费订单退后含税金额 = 原订单含税金额 - 退费含税金额
- 冲销后税费:账期内最终确认的总税费,计算公式:
冲销后税费 = 冲销前税费 - 冲销税费 + 补充税费 - 冲销前均摊金额(税后):历史未退费订单 + 账期内正式订单 在本账期内产生的税后均摊收入总和
- 冲销均摊金额(税后):本账期内发生退费的所有订单,需要冲销的历史均摊金额总和(正数展示,计算时扣除)
- 补充均摊金额(税后):本账期内发生部分退费的订单,从转正日起至账期最后一日产生的税后均摊金额总和
- 冲销后均摊金额(税后):账期内最终确认的税后净收入,计算公式:
冲销后均摊金额 = 冲销前均摊金额 - 冲销均摊金额 + 补充均摊金额
注意事项
- 所有金额保留2位小数,百分比保留1位小数
- 部分退费场景需确保bi_vala_seasonal_ticket中对应退费部分的status已更新为-1
- 自动剔除测试账号,无需额外过滤
- 冲销金额统一显示为负数