From 88fe4c99460cf4e225ec1d626c77c12180b953f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=BA=AA?= Date: Thu, 23 Apr 2026 08:00:02 +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-04-23=2008:00:02?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vala_skill_hashes | 1 + MEMORY.md | 3 + scripts/check_seasonal_ticket_fields.py | 29 ++ scripts/check_tables.py | 30 ++ scripts/find_202602_order_202603_refund.py | 47 +++ scripts/find_history_order_202603_refund.py | 47 +++ scripts/query_order_all_refund.py | 51 +++ scripts/query_order_refund_time.py | 53 +++ .../vala_order_amortization_stat_202603.py | 371 ++++++++++++++++++ skills/vala-order-amortization-stat/SKILL.md | 143 +++++++ 10 files changed, 775 insertions(+) create mode 100644 scripts/check_seasonal_ticket_fields.py create mode 100644 scripts/check_tables.py create mode 100644 scripts/find_202602_order_202603_refund.py create mode 100644 scripts/find_history_order_202603_refund.py create mode 100644 scripts/query_order_all_refund.py create mode 100644 scripts/query_order_refund_time.py create mode 100644 scripts/vala_order_amortization_stat_202603.py create mode 100644 skills/vala-order-amortization-stat/SKILL.md diff --git a/.vala_skill_hashes b/.vala_skill_hashes index af59d76..b0f3160 100644 --- a/.vala_skill_hashes +++ b/.vala_skill_hashes @@ -12,3 +12,4 @@ vala-component-practice-stat 8e768e2641019d27bd41f4647d2d90f24182a0554dad5ad9f41 cron-schedule e103cbb1806b28c891b9c856963325086ecaff32edec208f0a841865f26e8f3e refund-user-learning-analysis 648fd4ae2b29167fd66eab4245bdaaef00242db3131f4919cc02f07ca2a9b59c phone-chapter-query ac429b4da5a89db16efdf1066edf4ecb1c050b93aff20dd4c652af5f5568e44f +vala-order-amortization-stat 9a07c4466a98a55e62d0b0c4948ec709fda7a8e607c9497a0cfba32b77046c50 diff --git a/MEMORY.md b/MEMORY.md index 2edb881..5ec238d 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -64,6 +64,9 @@ - `course_lesson`:课时,如 L01、L02、L03、L04、L05(每单元固定5节课) - 示例:L1 S0 U00 L01 → id=343,L2 S0 U00 L01 → id=55 - [李承龙确认] 以后匹配课程统一使用此表,不再从 MySQL 配置表手动查找 + - 订单表与退费表关联规则: + - `bi_vala_order.trade_no` ↔ `bi_refund_order.trade_no` 关联 + - `bi_vala_order.out_trade_no` ↔ `bi_refund_order.out_trade_no` 关联 ## 业务知识库 - **已收集13个常用SQL查询模板** diff --git a/scripts/check_seasonal_ticket_fields.py b/scripts/check_seasonal_ticket_fields.py new file mode 100644 index 0000000..3214b66 --- /dev/null +++ b/scripts/check_seasonal_ticket_fields.py @@ -0,0 +1,29 @@ +import psycopg2 + +# 读取密码 +DB_PASSWORD = "" +with open('/root/.openclaw/workspace/secrets.env', 'r') as f: + for line in f: + if line.startswith('PG_ONLINE_PASSWORD='): + DB_PASSWORD = line.strip().split('=', 1)[1].strip('"\'') + break + +# 数据库连接 +conn = psycopg2.connect( + host='bj-postgres-16pob4sg.sql.tencentcdb.com', + port=28591, + user='ai_member', + password=DB_PASSWORD, + database='vala_bi' +) + +cur = conn.cursor() +# 查询表结构 +cur.execute("SELECT column_name FROM information_schema.columns WHERE table_name = 'bi_vala_seasonal_ticket'") +columns = cur.fetchall() +print("bi_vala_seasonal_ticket表字段:") +for col in columns: + print(col[0]) + +cur.close() +conn.close() diff --git a/scripts/check_tables.py b/scripts/check_tables.py new file mode 100644 index 0000000..47417e4 --- /dev/null +++ b/scripts/check_tables.py @@ -0,0 +1,30 @@ +import psycopg2 + +# 读取密码 +DB_PASSWORD = "" +with open('/root/.openclaw/workspace/secrets.env', 'r') as f: + for line in f: + if line.startswith('PG_ONLINE_PASSWORD='): + DB_PASSWORD = line.strip().split('=', 1)[1].strip('"\'') + break + +# 数据库连接 +conn = psycopg2.connect( + host='bj-postgres-16pob4sg.sql.tencentcdb.com', + port=28591, + user='ai_member', + password=DB_PASSWORD, + database='vala_bi' +) + +cur = conn.cursor() +# 查询所有表 +cur.execute("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'") +tables = cur.fetchall() +print("vala_bi库public模式下的表:") +for table in tables: + if 'seasonal' in table[0] or 'ticket' in table[0]: + print(f"→ {table[0]}") + +cur.close() +conn.close() diff --git a/scripts/find_202602_order_202603_refund.py b/scripts/find_202602_order_202603_refund.py new file mode 100644 index 0000000..a01b6ac --- /dev/null +++ b/scripts/find_202602_order_202603_refund.py @@ -0,0 +1,47 @@ +import psycopg2 +import pandas as pd + +# 读取密码 +DB_PASSWORD = "" +with open('/root/.openclaw/workspace/secrets.env', 'r') as f: + for line in f: + if line.startswith('PG_ONLINE_PASSWORD='): + DB_PASSWORD = line.strip().split('=', 1)[1].strip('"\'') + break + +# 数据库连接 +conn = psycopg2.connect( + host='bj-postgres-16pob4sg.sql.tencentcdb.com', + port=28591, + user='ai_member', + password=DB_PASSWORD, + database='vala_bi' +) + +# 查询2月下单3月退费的订单 +sql = """ +SELECT + o.out_trade_no AS order_no, + o.created_at AS order_create_time, + o.pay_amount_int / 100 AS pay_amount, + o.order_status, + r.updated_at AS refund_time, + CAST(r.refund_amount AS NUMERIC) / 100 AS refund_amount, + CASE r.refund_type WHEN 1 THEN '仅退款' WHEN 2 THEN '全额退货退款' WHEN 3 THEN '部分退货退款' END AS refund_type +FROM bi_vala_order o +JOIN bi_refund_order r ON o.out_trade_no = r.trade_no +WHERE + o.created_at BETWEEN '2026-02-01 00:00:00' AND '2026-02-28 23:59:59' + AND r.updated_at BETWEEN '2026-03-01 00:00:00' AND '2026-03-31 23:59:59' + AND r.status = 3 +""" + +df = pd.read_sql(sql, conn) +conn.close() + +print("🔍 2026年2月下单、2026年3月退费的订单列表:") +print(f"共找到{len(df)}条符合条件的记录:") +if not df.empty: + print(df.to_string(index=False)) +else: + print("无符合条件的记录") diff --git a/scripts/find_history_order_202603_refund.py b/scripts/find_history_order_202603_refund.py new file mode 100644 index 0000000..4d2cd6b --- /dev/null +++ b/scripts/find_history_order_202603_refund.py @@ -0,0 +1,47 @@ +import psycopg2 +import pandas as pd + +# 读取密码 +DB_PASSWORD = "" +with open('/root/.openclaw/workspace/secrets.env', 'r') as f: + for line in f: + if line.startswith('PG_ONLINE_PASSWORD='): + DB_PASSWORD = line.strip().split('=', 1)[1].strip('"\'') + break + +# 数据库连接 +conn = psycopg2.connect( + host='bj-postgres-16pob4sg.sql.tencentcdb.com', + port=28591, + user='ai_member', + password=DB_PASSWORD, + database='vala_bi' +) + +# 查询2025年9月到2026年2月下单3月退费的订单 +sql = """ +SELECT + o.out_trade_no AS order_no, + DATE(o.created_at) AS order_create_date, + o.pay_amount_int / 100 AS pay_amount, + o.order_status, + DATE(r.updated_at) AS refund_date, + CAST(r.refund_amount AS NUMERIC) / 100 AS refund_amount, + CASE r.refund_type WHEN 1 THEN '仅退款' WHEN 2 THEN '全额退货退款' WHEN 3 THEN '部分退货退款' END AS refund_type +FROM bi_vala_order o +JOIN bi_refund_order r ON o.out_trade_no = r.trade_no +WHERE + o.created_at BETWEEN '2025-09-01 00:00:00' AND '2026-02-28 23:59:59' + AND r.updated_at BETWEEN '2026-03-01 00:00:00' AND '2026-03-31 23:59:59' + AND r.status = 3 +""" + +df = pd.read_sql(sql, conn) +conn.close() + +print("🔍 2025年9月-2026年2月下单、2026年3月退费的订单列表:") +print(f"共找到{len(df)}条符合条件的记录:") +if not df.empty: + print(df.to_string(index=False)) +else: + print("无符合条件的记录") diff --git a/scripts/query_order_all_refund.py b/scripts/query_order_all_refund.py new file mode 100644 index 0000000..2fa4969 --- /dev/null +++ b/scripts/query_order_all_refund.py @@ -0,0 +1,51 @@ +import psycopg2 +import pandas as pd + +# 读取密码 +DB_PASSWORD = "" +with open('/root/.openclaw/workspace/secrets.env', 'r') as f: + for line in f: + if line.startswith('PG_ONLINE_PASSWORD='): + DB_PASSWORD = line.strip().split('=', 1)[1].strip('"\'') + break + +# 数据库连接 +conn = psycopg2.connect( + host='bj-postgres-16pob4sg.sql.tencentcdb.com', + port=28591, + user='ai_member', + password=DB_PASSWORD, + database='vala_bi' +) + +# 查询指定订单的所有退费记录(不限制状态) +order_no = "dd202602111013471770776027390252" +sql = """ +SELECT + r.id AS refund_id, + r.trade_no AS order_no, + r.created_at AS refund_apply_time, + r.updated_at AS refund_update_time, + CAST(r.refund_amount AS NUMERIC) / 100 AS refund_amount, + r.status AS refund_status_code, + CASE r.status + WHEN 0 THEN '待审核' + WHEN 1 THEN '审核通过' + WHEN 2 THEN '审核拒绝' + WHEN 3 THEN '退款成功' + WHEN 4 THEN '退款失败' + ELSE '其他状态' + END AS refund_status_name, + CASE r.refund_type WHEN 1 THEN '仅退款' WHEN 2 THEN '全额退货退款' WHEN 3 THEN '部分退货退款' END AS refund_type +FROM bi_refund_order r +WHERE r.trade_no = %s +""" + +df = pd.read_sql(sql, conn, params=(order_no,)) +conn.close() + +if df.empty: + print(f"订单号 {order_no} 无任何退费申请记录") +else: + print(f"🔍 订单 {order_no} 的所有退费记录:") + print(df.to_string(index=False)) diff --git a/scripts/query_order_refund_time.py b/scripts/query_order_refund_time.py new file mode 100644 index 0000000..91ca244 --- /dev/null +++ b/scripts/query_order_refund_time.py @@ -0,0 +1,53 @@ +import psycopg2 +import pandas as pd + +# 读取密码 +DB_PASSWORD = "" +with open('/root/.openclaw/workspace/secrets.env', 'r') as f: + for line in f: + if line.startswith('PG_ONLINE_PASSWORD='): + DB_PASSWORD = line.strip().split('=', 1)[1].strip('"\'') + break + +# 数据库连接 +conn = psycopg2.connect( + host='bj-postgres-16pob4sg.sql.tencentcdb.com', + port=28591, + user='ai_member', + password=DB_PASSWORD, + database='vala_bi' +) + +# 查询指定订单 +order_no = "dd202602111013471770776027390252" +sql = """ +SELECT + o.out_trade_no AS order_no, + o.created_at AS order_create_time, + o.pay_amount_int / 100 AS pay_amount, + o.order_status, + r.updated_at AS refund_finish_time, + CAST(r.refund_amount AS NUMERIC) / 100 AS refund_amount, + CASE r.refund_type WHEN 1 THEN '仅退款' WHEN 2 THEN '全额退货退款' WHEN 3 THEN '部分退货退款' END AS refund_type +FROM bi_vala_order o +LEFT JOIN bi_refund_order r ON o.out_trade_no = r.trade_no AND r.status = 3 +WHERE o.out_trade_no = %s +""" + +df = pd.read_sql(sql, conn, params=(order_no,)) +conn.close() + +if df.empty: + print(f"未找到订单号为 {order_no} 的记录") +else: + row = df.iloc[0] + print(f"订单号:{row['order_no']}") + print(f"下单时间:{row['order_create_time'].strftime('%Y-%m-%d %H:%M:%S') if pd.notna(row['order_create_time']) else '无'}") + print(f"订单金额:{row['pay_amount']}元") + print(f"订单状态:{row['order_status']}") + if pd.notna(row['refund_finish_time']): + print(f"退款完成时间:{row['refund_finish_time'].strftime('%Y-%m-%d %H:%M:%S')}") + print(f"退款金额:{row['refund_amount']}元") + print(f"退款类型:{row['refund_type']}") + else: + print("该订单无退费记录") diff --git a/scripts/vala_order_amortization_stat_202603.py b/scripts/vala_order_amortization_stat_202603.py new file mode 100644 index 0000000..baa7cde --- /dev/null +++ b/scripts/vala_order_amortization_stat_202603.py @@ -0,0 +1,371 @@ +import os +import pandas as pd +import psycopg2 +from datetime import datetime, timedelta + +# 手动读取secrets.env获取数据库密码 +DB_PASSWORD = "" +with open('/root/.openclaw/workspace/secrets.env', 'r') as f: + for line in f: + if line.startswith('PG_ONLINE_PASSWORD='): + DB_PASSWORD = line.strip().split('=', 1)[1].strip('"\'') + break + +# 数据库连接配置 +DB_CONFIG = { + 'host': 'bj-postgres-16pob4sg.sql.tencentcdb.com', + 'port': 28591, + 'user': 'ai_member', + 'password': DB_PASSWORD, + 'database': 'vala_bi' +} + +# 账期参数 +ACCOUNT_START = datetime(2026, 3, 1).date() +ACCOUNT_END = datetime(2026, 3, 31).date() + +def get_db_connection(): + """获取数据库连接""" + return psycopg2.connect(**DB_CONFIG) + +def query_order_base(): + """查询符合条件的订单基础信息""" + sql = """ + SELECT + o.id AS order_id, + o.out_trade_no AS order_no, + o.pay_amount_int / 100 AS pay_amount, + o.created_at, + DATE(o.created_at) + INTERVAL '7 day' AS amortization_start_date, + o.order_status, + 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 + FROM bi_vala_order o + JOIN bi_vala_app_account a ON o.account_id = a.id + WHERE + o.created_at >= '2025-09-01' + AND o.order_status IN (3,4) + AND o.pay_amount_int >= 1000 + AND a.status = 1 + """ + conn = get_db_connection() + df = pd.read_sql(sql, conn) + conn.close() + + # 处理日期格式 + df['created_at'] = pd.to_datetime(df['created_at']).dt.date + df['amortization_start_date'] = pd.to_datetime(df['amortization_start_date']).dt.date + + # 标记订单是否为账期内新增 + df['is_new_in_account'] = df['created_at'].between(ACCOUNT_START, ACCOUNT_END) + + # 标记是否为正式订单(截止账期结束日已过试用期) + df['is_formal'] = df['amortization_start_date'] <= ACCOUNT_END + + return df + +def query_order_cycle(order_nos): + """查询订单总均摊周期""" + if not order_nos: + return pd.DataFrame(columns=['order_no', 'total_cycle_days']) + + order_no_str = "', '".join(order_nos) + sql = f""" + SELECT + out_trade_no AS order_no, + COUNT(DISTINCT id) * 90 AS total_cycle_days + FROM bi_vala_seasonal_ticket + WHERE status != -1 AND out_trade_no IN ('{order_no_str}') + GROUP BY out_trade_no + """ + conn = get_db_connection() + df = pd.read_sql(sql, conn) + conn.close() + return df + +def query_refund_records(): + """查询账期内的退费记录""" + sql = """ + SELECT + r.out_trade_no AS order_no, + CAST(r.refund_amount AS NUMERIC) / 100.0 AS refund_amount, + r.refund_type, + DATE(r.updated_at) AS refund_date + FROM bi_refund_order r + WHERE + r.status = 3 + AND r.updated_at BETWEEN %s AND %s + """ + conn = get_db_connection() + df = pd.read_sql(sql, conn, params=(ACCOUNT_START, ACCOUNT_END)) + conn.close() + return df + +def calculate_amortization_days(amort_start, total_cycle, period_start, period_end): + """计算账期内的均摊天数""" + if pd.isna(amort_start) or pd.isna(total_cycle) or total_cycle <= 0: + return 0 + amort_end = amort_start + timedelta(days=total_cycle - 1) + # 计算实际均摊区间和账期的交集 + start = max(amort_start, period_start) + end = min(amort_end, period_end) + if start > end: + return 0 + return (end - start).days + 1 + +def main(): + print("开始计算2026年3月订单均摊数据...") + + # 1. 获取订单基础数据 + order_base_df = query_order_base() + if order_base_df.empty: + print("无符合条件的订单数据") + return + + # 2. 获取退费记录 + refund_df = query_refund_records() + # 处理重复订单退费记录,保留最新的一条 + if not refund_df.empty: + refund_df = refund_df.sort_values('refund_date', ascending=False).drop_duplicates('order_no', keep='first') + # 转成字典方便查询 + refund_dict = refund_df.set_index('order_no').to_dict('index') + + # 新增规则:过滤账期内下单且账期内全额退费的订单,直接排除 + # 账期内下单且账期内部分退费的订单,直接计算剩余金额,不需要冲销 + exclude_order_nos = [] + for _, order_row in order_base_df.iterrows(): + order_no = order_row['order_no'] + is_new_in_account = order_row['is_new_in_account'] + if order_no in refund_dict and is_new_in_account: + refund_type = refund_dict[order_no]['refund_type'] + if refund_type == 2: + # 账期内下单+账期内全额退费:直接排除 + exclude_order_nos.append(order_no) + elif refund_type == 3: + # 账期内下单+账期内部分退费:直接更新税后金额为剩余金额 + refund_amount = refund_dict[order_no]['refund_amount'] + order_base_df.loc[order_base_df['order_no'] == order_no, 'after_tax_amount'] = (order_row['pay_amount'] - refund_amount) * (1 - order_row['tax_rate']) + order_base_df.loc[order_base_df['order_no'] == order_no, 'tax_amount'] = (order_row['pay_amount'] - refund_amount) * order_row['tax_rate'] + + # 剔除全额退费的订单 + order_base_df = order_base_df[~order_base_df['order_no'].isin(exclude_order_nos)].reset_index(drop=True) + + # 3. 获取订单均摊周期 + order_cycle_df = query_order_cycle(order_base_df['order_no'].tolist()) + order_df = pd.merge(order_base_df, order_cycle_df, on='order_no', how='left') + order_df['total_cycle_days'] = order_df['total_cycle_days'].fillna(0) + + # 4. 计算基础指标 + # 订单类指标 + total_orders = order_df[order_df['is_new_in_account']].shape[0] + formal_orders = order_df[(order_df['is_new_in_account']) & (order_df['is_formal'])].shape[0] + trial_orders = total_orders - formal_orders + + # 计算冲销前税费 + pre_writeoff_tax = order_df[(order_df['is_new_in_account']) & (order_df['is_formal'])]['tax_amount'].sum() + + # 计算冲销税费 + writeoff_tax = 0 + # 计算冲销均摊金额 + writeoff_amort = 0 + # 计算补充税费和补充均摊金额 + supplement_tax = 0 + supplement_amort = 0 + + # 处理退费订单:仅处理历史订单(非账期内下单的订单),账期内订单已提前处理 + for _, refund_row in refund_df.iterrows(): + order_no = refund_row['order_no'] + refund_type = refund_row['refund_type'] + refund_amount = refund_row['refund_amount'] + order_info = order_df[order_df['order_no'] == order_no] + if order_info.empty: + continue + order_info = order_info.iloc[0] + # 跳过账期内下单的订单,已提前处理 + if order_info['is_new_in_account']: + continue + + # 累加冲销税费 + writeoff_tax += order_info['pay_amount'] * order_info['tax_rate'] + + # 计算该订单历史均摊金额(2025-09-01至2026-02-28的均摊) + amort_start = order_info['amortization_start_date'] + total_cycle = order_info['total_cycle_days'] + history_amort_days = calculate_amortization_days(amort_start, total_cycle, datetime(2025,9,1).date(), datetime(2026,2,28).date()) + daily_amort = order_info['after_tax_amount'] / total_cycle if total_cycle > 0 else 0 + history_amort_amount = history_amort_days * daily_amort + writeoff_amort += history_amort_amount + + # 部分退费处理 + if refund_type == 3: + remaining_amount = order_info['pay_amount'] - refund_amount + remaining_after_tax = remaining_amount * (1 - order_info['tax_rate']) + # 补充税费 + supplement_tax += remaining_amount * order_info['tax_rate'] + # 补充均摊:从转正日到账期最后一日的均摊,应用最后一天补差规则 + days = calculate_amortization_days(amort_start, total_cycle, amort_start, ACCOUNT_END) + daily_amort_partial = remaining_after_tax / total_cycle if total_cycle > 0 else 0 + if total_cycle > 0 and days >= total_cycle: + supplement_amort += round(remaining_after_tax, 2) + else: + supplement_amort += round(days * daily_amort_partial, 2) + + # 计算冲销后税费 + after_writeoff_tax = pre_writeoff_tax - writeoff_tax + supplement_tax + + # 计算冲销前均摊金额 + pre_writeoff_amort = 0 + # 历史未退费订单的账期均摊(排除历史退费订单) + refund_order_nos = list(refund_dict.keys()) + for _, order_row in order_df[~order_df['order_no'].isin(refund_order_nos)].iterrows(): + if not order_row['is_formal'] or order_row['total_cycle_days'] <= 0: + continue + amort_start = order_row['amortization_start_date'] + days = calculate_amortization_days(amort_start, order_row['total_cycle_days'], ACCOUNT_START, ACCOUNT_END) + daily_amort = order_row['after_tax_amount'] / order_row['total_cycle_days'] + pre_writeoff_amort += days * daily_amort + # 账期内正式订单的账期均摊 + for _, order_row in order_df[order_df['is_new_in_account'] & order_df['is_formal']].iterrows(): + if order_row['total_cycle_days'] <= 0: + continue + amort_start = order_row['amortization_start_date'] + days = calculate_amortization_days(amort_start, order_row['total_cycle_days'], ACCOUNT_START, ACCOUNT_END) + daily_amort = order_row['after_tax_amount'] / order_row['total_cycle_days'] + pre_writeoff_amort += days * daily_amort + + # 计算冲销后均摊金额 + after_writeoff_amort = pre_writeoff_amort - writeoff_amort + supplement_amort + + # 5. 生成汇总表 + summary_df = pd.DataFrame([{ + '指标': '订单数', + '数值': total_orders + }, { + '指标': '正式订单数', + '数值': formal_orders + }, { + '指标': '试用订单数', + '数值': trial_orders + }, { + '指标': '冲销前税费(元)', + '数值': round(pre_writeoff_tax, 2) + }, { + '指标': '冲销税费(元)', + '数值': round(writeoff_tax, 2) + }, { + '指标': '补充税费(元)', + '数值': round(supplement_tax, 2) + }, { + '指标': '冲销后税费(元)', + '数值': round(after_writeoff_tax, 2) + }, { + '指标': '冲销前均摊金额(税后,元)', + '数值': round(pre_writeoff_amort, 2) + }, { + '指标': '冲销均摊金额(税后,元)', + '数值': round(writeoff_amort, 2) + }, { + '指标': '补充均摊金额(税后,元)', + '数值': round(supplement_amort, 2) + }, { + '指标': '冲销后均摊金额(税后净收入,元)', + '数值': round(after_writeoff_amort, 2) + }]) + + # 6. 生成订单明细表 + order_detail_list = [] + for _, order_row in order_df.iterrows(): + # 计算历史均摊金额(2025-09-01至2026-02-28) + amort_start = order_row['amortization_start_date'] + total_cycle = order_row['total_cycle_days'] + history_days = calculate_amortization_days(amort_start, total_cycle, datetime(2025,9,1).date(), datetime(2026,2,28).date()) + daily_amort = order_row['after_tax_amount'] / total_cycle if total_cycle > 0 else 0 + # 周期最后一天补差规则 + if total_cycle > 0 and history_days >= total_cycle: + history_amort = round(order_row['after_tax_amount'], 2) + else: + history_amort = round(history_days * daily_amort, 2) + + # 计算账期均摊金额 + account_days = calculate_amortization_days(amort_start, total_cycle, ACCOUNT_START, ACCOUNT_END) + total_used_days = history_days + account_days + # 周期最后一天补差规则 + if total_cycle > 0 and total_used_days >= total_cycle: + # 如果累计天数超过总周期,账期均摊=总金额-历史均摊 + account_amort = round(order_row['after_tax_amount'] - history_amort, 2) + if account_amort < 0: + account_amort = 0 + else: + account_amort = round(account_days * daily_amort, 2) + + # 未确认收入 + total_amort = history_amort + account_amort + unconfirmed_income = round(order_row['after_tax_amount'] - total_amort, 2) + if unconfirmed_income < 0: + unconfirmed_income = 0 + + # 剩余周期 + used_days = history_days + account_days + remaining_cycle = max(0, total_cycle - used_days) + + # 新增字段:下单时间 + create_time = order_row['created_at'].strftime('%Y-%m-%d') + + # 新增字段:退费时间 + refund_time = "" + refund_type = None + if order_row['order_no'] in refund_dict: + refund_time = refund_dict[order_row['order_no']]['refund_date'].strftime('%Y-%m-%d') + refund_type = refund_dict[order_row['order_no']]['refund_type'] + + # 新增字段:订单分类 + order_category = "" + if refund_type is not None: + if refund_type == 2: + order_category = "历史新增全额退费订单" + elif refund_type == 3: + order_category = "历史新增部分退费订单" + else: + if order_row['is_new_in_account']: + if order_row['is_formal']: + order_category = "账期新增正式订单" + else: + order_category = "账期新增试用订单" + else: + order_category = "历史正式订单" + + order_detail_list.append({ + '订单号': order_row['order_no'], + '下单时间': create_time, + '退费时间': refund_time, + '订单分类': order_category, + '订单金额(元)': round(order_row['pay_amount'], 2), + '税率': f"{order_row['tax_rate'] * 100}%", + '税额(元)': round(order_row['tax_amount'], 2), + '税后金额(元)': round(order_row['after_tax_amount'], 2), + '总均摊周期(天)': int(total_cycle), + '已均摊天数(天)': used_days, + '历史均摊金额(元)': history_amort, + '账期均摊金额(元)': account_amort, + '未确认收入(元)': unconfirmed_income, + '剩余周期(天)': remaining_cycle + }) + detail_df = pd.DataFrame(order_detail_list) + + # 7. 写入Excel + output_path = f"/root/.openclaw/workspace/output/订单均摊结算报表_{ACCOUNT_START}_{ACCOUNT_END}.xlsx" + with pd.ExcelWriter(output_path) as writer: + summary_df.to_excel(writer, sheet_name='汇总表', index=False) + detail_df.to_excel(writer, sheet_name='订单明细', index=False) + + print(f"报表生成完成,保存路径:{output_path}") + print("\n📊 账期2026-03-01至2026-03-31均摊结算结果:") + print(f"总订单数:{total_orders}单") + print(f"正式订单数:{formal_orders}单") + print(f"试用订单数:{trial_orders}单") + print(f"冲销后税费:{round(after_writeoff_tax, 2)}元") + print(f"冲销后均摊金额(税后净收入):{round(after_writeoff_amort, 2)}元") + +if __name__ == "__main__": + main() diff --git a/skills/vala-order-amortization-stat/SKILL.md b/skills/vala-order-amortization-stat/SKILL.md new file mode 100644 index 0000000..0872c52 --- /dev/null +++ b/skills/vala-order-amortization-stat/SKILL.md @@ -0,0 +1,143 @@ +# 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:用户账户表(用于剔除测试账号) +3. 表关联规则: + - bi_vala_order.trade_no ↔ bi_refund_order.trade_no 关联 + - bi_vala_order.out_trade_no ↔ bi_refund_order.out_trade_no 关联 +## 操作流程 +### 步骤1:确认核心参数 +执行前必须向用户确认以下参数: +- 账期起始日期(格式:YYYY-MM-DD) +- 账期结束日期(格式:YYYY-MM-DD) +- 默认输出维度:账期整体汇总,无需按天/周/月拆分,无需分渠道统计 +### 步骤2:数据过滤规则 +1. 订单范围: + - 2025-09-01 至 账期结束日 内创建的所有订单 + - bi_vala_order.status IN (3,4)(已完成、已退款订单) + - 订单实际支付金额≥10元:bi_vala_order.pay_amount_int ≥ 1000(单位:分) + - 关联bi_vala_app_account表,仅保留bi_vala_app_account.status = 1的非测试账号订单 +2. 退费范围: + - 退费完成时间落在账期内的所有退款成功记录,判定条件:bi_refund_order.updated_at(退款处理完成时间)在账期起止时间范围内 + - bi_refund_order.status = 3(退款处理完成) +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)天累计已均摊金额 + 避免浮点精度导致的金额尾差 +#### 3.2 退费场景计算 +所有退费冲销金额均为税后金额,税费同步对应冲销 +1. **所有退费订单(全额/部分)通用计算**: + - 历史退费订单冲销均摊金额:从2026年9月1日起至账期起始日之前,该订单已产生的全部税后均摊金额(统一显示为负数,用于冲销) + - 历史退费订单冲销税费金额:上述历史均摊金额对应的税费(统一显示为负数,用于冲销) +2. **全额退费(bi_refund_order.refund_type = 2)**:无需额外计算补充均摊,仅执行上述通用冲销逻辑 +3. **部分退费(bi_refund_order.refund_type = 3)**:在通用冲销逻辑基础上,额外计算: + - 历史部分退费订单补充均摊金额:部分退费后剩余的待均摊税后金额,在本账期内产生的均摊收入 + - 历史部分退费订单补充税费金额:上述补充均摊金额对应的税费 +#### 3.3 账期内收入计算 +账期内总收入 = 账期内所有正常订单的日均摊金额总和 + 账期内所有退费冲销金额总和 +## 核心SQL模板 +```sql +-- 步骤1:获取所有符合条件的订单基础信息 +WITH order_base AS ( + SELECT + o.id AS order_id, + o.order_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.status AS order_status, + a.id AS account_id, + o.key_from, + o.sale_channel + FROM bi_vala_order o + JOIN bi_vala_app_account a ON o.account_id = a.id + WHERE + o.created_at >= '2025-09-01' + AND o.status IN (3,4) + 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, + CAST(r.refund_amount AS NUMERIC) / 100 AS refund_amount, + r.refund_type, + DATE(r.updated_at) AS refund_date + FROM bi_refund_order r + JOIN order_base o ON r.out_trade_no = o.order_no + WHERE + r.status = 3 + AND r.updated_at BETWEEN '${账期起始日}' AND '${账期结束日}' +) +-- 后续按需求聚合维度计算日均摊金额和冲销金额 +``` +## 输出格式 +1. 优先输出Excel报表,存放于output/目录下,命名格式:`订单均摊结算报表_${账期起始日}_${账期结束日}.xlsx` +2. 报表包含以下Sheet: + - 汇总表:订单数、正式订单数、试用订单数、冲销前税费、冲销税费、补充税费、冲销后税费、冲销前均摊金额、冲销均摊金额、补充均摊金额、冲销后均摊金额 + - 订单明细:订单号、订单金额、税率、税额、税后金额、总均摊周期、已均摊天数、历史均摊金额、账期均摊金额、未确认收入、剩余周期 +3. 文字回复核心指标: + > 📊 账期${账期起始日}至${账期结束日}均摊结算结果: + > 总订单数:XXX单 + > 正式订单数:XXX单 + > 试用订单数:XXX单 + > 冲销后税费:XXX元 + > 冲销后均摊金额(税后净收入):XXX元 +## 汇总指标计算逻辑(所有均摊金额均为税后金额) +1. **订单数**:账期内(下单日落在账期起止时间范围内)新增的所有符合条件的订单总数量 +2. **正式订单数**:账期内新增的订单中,截止账期结束日已过7天试用期(下单日+7天 ≤ 账期结束日)的订单数量,仅正式订单开始参与均摊计算 +3. **试用订单数**:账期内新增的订单中,截止账期结束日仍处于7天试用期内(下单日+7天 > 账期结束日)的订单数量,试用订单未开始均摊,不参与金额计算 +4. **冲销前税费**:账期内所有正式订单的含税订单总金额 × 各订单对应税率 的总和 +5. **冲销税费**:本账期内发生退费的所有订单的含税订单总金额 × 各订单对应税率 的总和(正数展示,计算时扣除) +6. **补充税费**:本账期内发生部分退费的订单,退费后剩余的含税订单金额 × 各订单对应税率 的总和,其中:部分退费订单剩余含税金额 = 原订单含税金额 - 退费含税金额 +7. **冲销后税费**:账期内最终确认的总税费,计算公式:`冲销后税费 = 冲销前税费 - 冲销税费 + 补充税费` +8. **冲销前均摊金额(税后)**:历史未退费订单 + 账期内正式订单 在本账期内产生的税后均摊收入总和 +9. **冲销均摊金额(税后)**:本账期内发生退费的所有订单,需要冲销的历史均摊金额总和(正数展示,计算时扣除) +10. **补充均摊金额(税后)**:本账期内发生部分退费的订单,从转正日起至账期最后一日产生的税后均摊金额总和 +11. **冲销后均摊金额(税后)**:账期内最终确认的税后净收入,计算公式:`冲销后均摊金额 = 冲销前均摊金额 - 冲销均摊金额 + 补充均摊金额` +## 注意事项 +1. 所有金额保留2位小数,百分比保留1位小数 +2. 部分退费场景需确保bi_vala_seasonal_ticket中对应退费部分的status已更新为-1 +3. 自动剔除测试账号,无需额外过滤 +4. 冲销金额统一显示为负数 \ No newline at end of file