feat: gift card breakdown - migrations, fdw queries, finance recharge task, board-finance page updates

This commit is contained in:
Neo
2026-03-20 03:24:17 +08:00
parent 8f8abad4c9
commit 675301f38b
15 changed files with 473 additions and 185 deletions

View File

@@ -1906,8 +1906,9 @@ def get_finance_recharge(
actual_income → recharge_cash, first_charge → first_recharge_cash,
renew_charge → renewal_cash, consumed → 不存在(暂返回 0,
card_balance → cash_card_balance, all_card_balance → total_card_balance。
赠送卡 3×4 矩阵细分列不存在(无酒水卡/台费卡/抵用券拆分),
只有 gift_card_balance 总额,矩阵全部返回 0。
CHANGE 2026-07-22 | Prompt: gift-card-breakdown Task 7.1 | 直接原因: SQL 新增 6 个赠送卡细分字段 SUM 聚合
新增: gift_liquor_balance, gift_table_fee_balance, gift_voucher_balance,
gift_liquor_recharge, gift_table_fee_recharge, gift_voucher_recharge
"""
with _fdw_context(conn, site_id) as cur:
cur.execute(
@@ -1917,7 +1918,13 @@ def get_finance_recharge(
SUM(renewal_cash) AS renew_charge,
SUM(cash_card_balance) AS card_balance,
SUM(total_card_balance) AS all_card_balance,
SUM(gift_card_balance) AS gift_balance_total
SUM(gift_card_balance) AS gift_balance_total,
SUM(gift_liquor_balance) AS gift_liquor_balance,
SUM(gift_table_fee_balance) AS gift_table_fee_balance,
SUM(gift_voucher_balance) AS gift_voucher_balance,
SUM(gift_liquor_recharge) AS gift_liquor_recharge,
SUM(gift_table_fee_recharge) AS gift_table_fee_recharge,
SUM(gift_voucher_recharge) AS gift_voucher_recharge
FROM app.v_dws_finance_recharge_summary
WHERE stat_date >= %s::date AND stat_date <= %s::date
""",
@@ -1932,6 +1939,14 @@ def get_finance_recharge(
return _empty_recharge_data()
gift_balance = _f(row[5])
# CHANGE 2026-07-22 | Prompt: gift-card-breakdown Task 7.2 | 直接原因: gift_rows 填充真实细分数据
gift_liquor_balance = _f(row[6])
gift_table_fee_balance = _f(row[7])
gift_voucher_balance = _f(row[8])
gift_liquor_recharge = _f(row[9])
gift_table_fee_recharge = _f(row[10])
gift_voucher_recharge = _f(row[11])
# CHANGE 2026-03-20 | gift_rows 每个 cell 必须是 GiftCell dict{"value": float}
# 不能是裸 float否则 Pydantic ResponseValidationError
_gc = lambda v: {"value": v}
@@ -1942,10 +1957,24 @@ def get_finance_recharge(
"consumed": 0.0, # 视图无消耗列,暂返回 0
"card_balance": _f(row[3]),
"gift_rows": [
# 赠送卡矩阵:视图无细分列(酒水卡/台费卡/抵用券),全部返回 0
{"label": "新增", "total": _gc(0.0), "liquor": _gc(0.0), "table_fee": _gc(0.0), "voucher": _gc(0.0)},
{"label": "消费", "total": _gc(0.0), "liquor": _gc(0.0), "table_fee": _gc(0.0), "voucher": _gc(0.0)},
{"label": "余额", "total": _gc(gift_balance), "liquor": _gc(0.0), "table_fee": _gc(0.0), "voucher": _gc(0.0)},
# 新增行total = 三个细分之和(保证 total = liquor + table_fee + voucher 恒等)
{"label": "新增",
"total": _gc(gift_liquor_recharge + gift_table_fee_recharge + gift_voucher_recharge),
"liquor": _gc(gift_liquor_recharge),
"table_fee": _gc(gift_table_fee_recharge),
"voucher": _gc(gift_voucher_recharge)},
# 消费行:上游 API 仅提供消费总额,无法按卡类型拆分,细分列保持 0
{"label": "消费",
"total": _gc(0.0),
"liquor": _gc(0.0),
"table_fee": _gc(0.0),
"voucher": _gc(0.0)},
# 余额行:填充对应细分余额
{"label": "余额",
"total": _gc(gift_balance),
"liquor": _gc(gift_liquor_balance),
"table_fee": _gc(gift_table_fee_balance),
"voucher": _gc(gift_voucher_balance)},
],
"all_card_balance": _f(row[4]),
}

View File

@@ -38,9 +38,15 @@
| 18 | new_member_count | INTEGER | NO | 新增会员数 |
| 19 | total_card_balance | NUMERIC(14,2) | NO | 全部会员卡余额(当日末) |
| 20 | cash_card_balance | NUMERIC(14,2) | NO | 储值卡余额 |
| 21 | gift_card_balance | NUMERIC(14,2) | NO | 赠送卡余额 |
| 22 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 23 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
| 21 | gift_card_balance | NUMERIC(14,2) | NO | 赠送卡余额(合计) |
| 22 | gift_liquor_balance | NUMERIC(14,2) | NO | 酒水卡余额当日末快照DEFAULT 0 |
| 23 | gift_table_fee_balance | NUMERIC(14,2) | NO | 台费卡余额当日末快照DEFAULT 0 |
| 24 | gift_voucher_balance | NUMERIC(14,2) | NO | 抵用券余额当日末快照DEFAULT 0 |
| 25 | gift_liquor_recharge | NUMERIC(14,2) | NO | 酒水卡新增充值赠送部分DEFAULT 0 |
| 26 | gift_table_fee_recharge | NUMERIC(14,2) | NO | 台费卡新增充值赠送部分DEFAULT 0 |
| 27 | gift_voucher_recharge | NUMERIC(14,2) | NO | 抵用券新增充值赠送部分DEFAULT 0 |
| 28 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 29 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 数据来源
@@ -70,11 +76,46 @@ GROUP BY DATE(pay_time);
SELECT
SUM(balance) AS total_card_balance,
SUM(CASE WHEN card_type_id = 2793249295533893 THEN balance ELSE 0 END) AS cash_card_balance,
SUM(CASE WHEN card_type_id != 2793249295533893 THEN balance ELSE 0 END) AS gift_card_balance
SUM(CASE WHEN card_type_id != 2793249295533893 THEN balance ELSE 0 END) AS gift_card_balance,
-- 赠送卡按卡类型拆分余额
SUM(CASE WHEN card_type_id = 2794699703437125 THEN balance ELSE 0 END) AS gift_liquor_balance,
SUM(CASE WHEN card_type_id = 2791990152417157 THEN balance ELSE 0 END) AS gift_table_fee_balance,
SUM(CASE WHEN card_type_id = 2793266846533445 THEN balance ELSE 0 END) AS gift_voucher_balance
FROM dwd.dim_member_card_account
WHERE scd2_is_current = 1;
WHERE scd2_is_current = 1
AND COALESCE(is_delete, 0) = 0;
```
### 赠送卡新增充值拆分dwd_recharge_order JOIN dim_member_card_account
```sql
SELECT
dca.card_type_id,
SUM(ro.point_amount) AS gift_recharge
FROM dwd.dwd_recharge_order ro
JOIN dwd.dim_member_card_account dca
ON ro.tenant_member_card_id = dca.tenant_member_id
WHERE ro.site_id = :site_id
AND DATE(ro.pay_time) >= :start_date
AND DATE(ro.pay_time) <= :end_date
AND dca.card_type_id IN (2794699703437125, 2791990152417157, 2793266846533445)
AND dca.scd2_is_current = 1
AND COALESCE(dca.is_delete, 0) = 0
GROUP BY dca.card_type_id;
-- 结果映射:
-- 2794699703437125 → gift_liquor_recharge酒水卡
-- 2791990152417157 → gift_table_fee_recharge台费卡
-- 2793266846533445 → gift_voucher_recharge抵用券
```
## 恒等式约束
| 约束 | 表达式 | 说明 |
|------|--------|------|
| 余额恒等 | `gift_card_balance = gift_liquor_balance + gift_table_fee_balance + gift_voucher_balance` | 赠送卡总余额 = 三种卡类型余额之和 |
| 新增恒等 | `recharge_gift = gift_liquor_recharge + gift_table_fee_recharge + gift_voucher_recharge` | 赠送总额 = 三种卡类型新增之和 |
> ETL 不做运行时校验,由数据质量检查任务事后验证。验证 SQL 见 `scripts/ops/verify_gift_breakdown.sql`。
## 使用说明
**首充判断**
@@ -84,9 +125,9 @@ WHERE scd2_is_current = 1;
**储值卡ID**
- 储值卡 card_type_id = 2793249295533893
**赠送卡ID**(归入 `gift_card_balance`
- 台费卡 2791990152417157
- 酒水卡 2794699703437125
- 活动抵用券 2793266846533445
- 台费卡 2791990152417157`gift_table_fee_balance` / `gift_table_fee_recharge`
- 酒水卡 2794699703437125`gift_liquor_balance` / `gift_liquor_recharge`
- 活动抵用券 2793266846533445`gift_voucher_balance` / `gift_voucher_recharge`
**其他卡类型**(当前未计入 `cash_card_balance``gift_card_balance`
- 年卡 2791987095408517
@@ -94,16 +135,47 @@ WHERE scd2_is_current = 1;
> ⚠️ 年卡和月卡的余额目前未被 `_extract_card_balances()` 统计,不包含在 `total_card_balance` 中。详见 P12 PRD`docs/prd/specs/P12-gift-card-breakdown.md`)。
## 数据流向
```
DWD 层
├── dim_member_card_account (card_type_id + balance)
│ ├── → gift_liquor_balance (card_type_id=2794699703437125)
│ ├── → gift_table_fee_balance (card_type_id=2791990152417157)
│ └── → gift_voucher_balance (card_type_id=2793266846533445)
├── dwd_recharge_order JOIN dim_member_card_account (point_amount)
│ ├── → gift_liquor_recharge (card_type_id=2794699703437125)
│ ├── → gift_table_fee_recharge (card_type_id=2791990152417157)
│ └── → gift_voucher_recharge (card_type_id=2793266846533445)
└── dwd_settlement_head (gift_card_amount)
└── → 消费行仅提供总额,无法按卡类型拆分
ETL (FinanceRechargeTask)
├── _extract_card_balances() → 余额拆分
├── _extract_gift_recharge_breakdown() → 新增充值拆分
└── transform() → 合并 6 字段写入 DWS
DWS → RLS 视图 → FDW 外部表 → 后端 API → 小程序
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯 |
| 数据范围 | 2025-07-21 ~ 至今 |
| 依赖表 | dwd_recharge_order, dim_member_card_account |
| 依赖表 | dwd_recharge_order, dim_member_card_account(余额快照 + 新增充值 JOIN |
<!--
AI_CHANGELOG:
- 日期: 2026-03-21
- Prompt: gift-card-breakdown spec Task 11.1 — BD 手册更新
- 直接原因: DWS 层新增 6 个赠送卡细分字段3 种卡类型 × 余额+新增),需同步 BD 手册
- 变更摘要: 字段说明表新增 6 字段gift_liquor/table_fee/voucher_balance/recharge数据来源新增余额拆分 SQL 和新增充值 JOIN SQL新增恒等式约束章节新增数据流向章节赠送卡 ID 映射补充字段对应关系;依赖表说明更新
- 风险与验证: 纯文档变更,无运行时影响
- 日期: 2026-03-19
- Prompt: card_type_id 年卡/月卡映射同步
- 直接原因: 用户确认 2791987095408517=年卡、2793306611533637=月卡,同步到所有涉及 card_type_id 的文档

View File

@@ -77,6 +77,8 @@ class FinanceRechargeTask(FinanceBaseTask):
def transform(self, extracted: Dict[str, Any], context: TaskContext) -> List[Dict[str, Any]]:
recharge_summary = extracted['recharge_summary']
card_balances = extracted['card_balances']
# CHANGE 2026-07-18 | task 4.2: 提取赠送卡新增充值拆分数据
gift_recharge_breakdown = extracted.get('gift_recharge_breakdown', {})
site_id = extracted['site_id']
results = []
@@ -107,6 +109,14 @@ class FinanceRechargeTask(FinanceBaseTask):
'total_card_balance': self.safe_decimal(balance.get('total_balance', 0)),
'cash_card_balance': self.safe_decimal(balance.get('cash_balance', 0)),
'gift_card_balance': self.safe_decimal(balance.get('gift_balance', 0)),
# CHANGE 2026-07-18 | task 4.2: 赠送卡细分余额(来自 card_balances
'gift_liquor_balance': self.safe_decimal(balance.get('gift_liquor_balance', 0)),
'gift_table_fee_balance': self.safe_decimal(balance.get('gift_table_fee_balance', 0)),
'gift_voucher_balance': self.safe_decimal(balance.get('gift_voucher_balance', 0)),
# CHANGE 2026-07-18 | task 4.2: 赠送卡细分新增充值(来自 gift_recharge_breakdown
'gift_liquor_recharge': self.safe_decimal(gift_recharge_breakdown.get('gift_liquor_recharge', 0)),
'gift_table_fee_recharge': self.safe_decimal(gift_recharge_breakdown.get('gift_table_fee_recharge', 0)),
'gift_voucher_recharge': self.safe_decimal(gift_recharge_breakdown.get('gift_voucher_recharge', 0)),
}
results.append(record)

View File

@@ -1,7 +1,9 @@
// 财务看板页 — 忠于 H5 原型结构,内联 mock 数据
// TODO: 联调时替换 mock 数据为真实 API 调用
// 财务看板页 — 忠于 H5 原型结构
// CHANGE 2026-07-22 | Prompt: gift-card-breakdown Task 9.1 | 直接原因: 赠送卡矩阵 giftRows 从 mock 替换为真实 API 数据
import { getRandomAiColor } from '../../utils/ai-color'
import { fetchBoardFinance } from '../../services/api'
import { formatMoney } from '../../utils/money'
/** 目录板块定义 */
interface TocItem {
@@ -154,26 +156,12 @@ Page({
consumedCompare: '5.2%',
cardBalance: '¥642,600',
cardBalanceCompare: '11.4%',
giftRows: [
{
label: '新增', total: '¥108,600', totalCompare: '9.8%',
wine: '¥43,200', wineCompare: '11.2%',
table: '¥54,100', tableCompare: '8.5%',
coupon: '¥11,300', couponCompare: '6.3%',
},
{
label: '消费', total: '¥75,800', totalCompare: '7.2%',
wine: '¥32,100', wineCompare: '8.1%',
table: '¥32,800', tableCompare: '6.5%',
coupon: '¥10,900', couponCompare: '5.8%',
},
{
label: '余额', total: '¥243,900', totalCompare: '4.5%',
wine: '¥118,500', wineCompare: '5.2%',
table: '¥109,200', tableCompare: '3.8%',
coupon: '¥16,200', couponCompare: '2.5%',
},
],
giftRows: [] as Array<{
label: string; total: string; totalCompare: string;
wine: string; wineCompare: string;
table: string; tableCompare: string;
coupon: string; couponCompare: string;
}>,
allCardBalance: '¥586,500',
allCardBalanceCompare: '6.2%',
},
@@ -299,7 +287,8 @@ Page({
// 同步 custom-tab-bar 选中态
const tabBar = this.getTabBar?.()
if (tabBar) tabBar.setData({ active: 'board' })
// TODO: 联调时在此刷新看板数据
// 加载赠送卡矩阵真实数据
this._loadGiftRows()
},
onReady() {
@@ -395,7 +384,42 @@ Page({
})
},
/**
* 从 Finance_Board_API 加载赠送卡矩阵数据
* 字段映射liquor→wine、tableFee→table、voucher→coupon
*/
async _loadGiftRows() {
try {
const data = await fetchBoardFinance({
time: this.data.selectedTime,
area: this.data.selectedArea,
compare: this.data.compareEnabled ? 1 : 0,
})
// API 返回 camelCasegiftRows[].liquor/tableFee/voucher每个是 GiftCell {value, compare?, down?, flat?}
const rechargePanel = data.rechargePanel || data
const apiRows = rechargePanel.giftRows || []
const giftRows = apiRows.map((row: any) => ({
label: row.label || '',
total: formatMoney(row.total?.value),
totalCompare: row.total?.compare || '',
wine: formatMoney(row.liquor?.value),
wineCompare: row.liquor?.compare || '',
table: formatMoney(row.tableFee?.value),
tableCompare: row.tableFee?.compare || '',
coupon: formatMoney(row.voucher?.value),
couponCompare: row.voucher?.compare || '',
}))
this.setData({ 'recharge.giftRows': giftRows })
} catch (err) {
console.error('[board-finance] 赠送卡数据加载失败', err)
// 加载失败时清空 giftRows不显示 mock 数据
this.setData({ 'recharge.giftRows': [] })
wx.showToast({ title: '赠送卡数据加载失败', icon: 'none', duration: 2000 })
}
},
onPullDownRefresh() {
this._loadGiftRows()
setTimeout(() => wx.stopPullDownRefresh(), 500)
},
@@ -489,5 +513,6 @@ Page({
/** P4: 错误态重试 */
onRetry() {
this.setData({ pageState: 'normal' })
this._loadGiftRows()
},
})