chore: 文档与 IDE 配置整理

- .kiro/specs/ → docs/specs/(41 个历史需求 spec 迁移,移除 .config.kiro)
- CLAUDE.md 三层拆分:根文件精简 + apps/backend/CLAUDE.md + .claude/commands/
- 新增 /spec-close、/pre-change 两个工作流命令
- DDL 基线刷新(从测试库重新导出 11 个文件,dws 35→38 表,biz 18→21 表)
- BD_Manual → BD_manual 命名统一(48 个文件)
- 修复 3 处文档与数据库不一致(auth.users.status 默认值、scheduled_tasks 字段、RLS 视图数)
- 新增 BD_manual_public_rbac_tables.md(public schema 8 张 RBAC/工作流表)
- 合并 biz.trigger_jobs 文档(10→12 字段,归档独立文档)
- docs/database/README.md 索引更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Neo
2026-04-06 00:02:37 +08:00
parent 8228b3fa37
commit 70324d8542
185 changed files with 13595 additions and 1219 deletions

52
apps/backend/CLAUDE.md Normal file
View File

@@ -0,0 +1,52 @@
# CLAUDE.md — Backend (FastAPI)
进入本目录时自动加载。
## 架构模式
### 全局响应包装
`ResponseWrapperMiddleware` 把所有 2xx 响应包为 `{ "code": 0, "data": <payload> }`
非 2xx 响应保持原样。前端统一通过 `response.data` 解包。
### 序列化
`CamelModel` 基类snake_case → camelCase 自动转换(小程序 API 用)。
后端代码始终用 snake_caseJSON 输出自动转驼峰。
### JWT 双认证
| 认证方式 | 用途 | 表 | JWT aud |
|---------|------|-----|---------|
| 用户名+密码 | admin-web 登录 | `auth.admin_users` | `admin` |
| 微信 code | 小程序登录 | `auth.users` | `miniapp` |
| 用户名+密码 | tenant-admin 登录 | `auth.tenant_admins` | `tenant-admin` |
待审核用户有 limited token仅可访问审核状态接口
### AI 集成
8 个千问应用通过 DashScope SDK
chat / finance / clue / analysis / tactics / note / customer / consolidate
特性:熔断(连续失败自动断路)、限流(每分钟/每日)、预算追踪、对话缓存。
### 后台服务lifespan
- `TaskQueue`:按 site_id 消费FIFO 队列
- `Scheduler`:读 `meta.scheduled_tasks` 自动入队
- 4 个触发器:日结/月结/工资/关系指数
### 数据库访问
- 业务库通过 `APP_DB_DSN` 直连 `zqyy_app`
- ETL 数据通过 FDW 映射的 `app.v_*` RLS 视图访问
- 查询前必须 `SET LOCAL app.current_site_id = :site_id`
## 测试
```bash
cd apps/backend && pytest tests/ -v
```
使用测试库(`TEST_APP_DB_DSN`),禁止连正式库。

View File

@@ -0,0 +1,60 @@
# cfg_skill_type 课程类型配置表
> 生成时间2026-03-24
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | dws |
| 表名 | cfg_skill_type |
| 主键 | skill_type_id自增 |
| 唯一键 | skill_id |
| 数据来源 | 手工维护,对应飞球系统的课程/技能类型 |
| 更新频率 | 按需(新增课程类型时) |
| 说明 | 将飞球系统的 skill_id 映射到业务课程分类BASE/BONUS供 ETL 到店判定、工资计算、绩效统计使用 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | skill_type_id | SERIAL | NO | 自增主键 |
| 2 | skill_id | BIGINT | NO | 飞球系统技能 ID唯一 |
| 3 | skill_name | VARCHAR | YES | 技能名称(来自飞球系统) |
| 4 | course_type_code | VARCHAR | NO | 课程分类代码BASE基础课/ BONUS附加课/超休/激励课) |
| 5 | course_type_name | VARCHAR | NO | 课程分类中文名 |
| 6 | is_active | BOOLEAN | NO | 是否启用(默认 true |
| 7 | description | TEXT | YES | 备注说明 |
| 8 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 9 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 当前数据(截至 2026-03-24
| skill_id | skill_name | course_type_code | 来源说明 |
|----------|-----------|-----------------|---------|
| 2790683529513797 | 基础课 | BASE | 飞球系统原始课程类型2026-03-24 补录) |
| 2790683529513798 | 附加课 | BONUS | 飞球系统原始课程类型2026-03-24 补录) |
| 2791903611396869 | 台球基础陪打 | BASE | 初始种子数据 |
| 2807440316432197 | 台球超休服务 | BONUS | 初始种子数据 |
| 2807440316432198 | 包厢服务 | BASE | 初始种子数据 |
| 3039912271463941 | 包厢课 | BASE | 飞球系统原始课程类型2026-03-24 补录) |
## 业务口径
- `course_type_code = 'BONUS'` 用于 WBI/NCI 到店判定settle_type=3 的商城订单,仅当关联了 BONUS 类型的助教服务记录时才算"到店"
- `course_type_code = 'BASE'` 用于基础课工资计算(按助教等级计价)
- `course_type_code = 'BONUS'` 用于附加课工资计算(固定 190 元/小时)
## 下游依赖
| 消费方 | 用途 |
|--------|------|
| `member_index_base._build_visit_condition_sql()` | WBI/NCI 到店判定 |
| `index_verifier.visit_members` CTE | 指数验证器到店范围 |
| 助教工资计算任务 | 区分基础课/附加课计价 |
| 助教绩效统计 | 按课程类型分类统计服务时长 |
## 维护注意事项
- 飞球系统新增课程类型时,必须同步在此表补录,否则相关订单会被 WBI 到店判定漏掉
- 2026-03-24 发现 3 条缺失记录导致 113 名会员、3766 条服务记录的到店判定失效

View File

@@ -1,6 +1,6 @@
# cfg_area_category 台区分类映射表
> 生成时间2026-02-03 | 更新时间2026-03-09
> 生成时间2026-02-03 | 更新时间2026-03-20
## 表信息
@@ -30,6 +30,7 @@
| 11 | description | TEXT | YES | | 说明 |
| 12 | created_at | TIMESTAMPTZ | NO | | 创建时间 |
| 13 | updated_at | TIMESTAMPTZ | NO | | 更新时间 |
| 14 | sort_order | INTEGER | NO | | 前端筛选器显示排序值越小越靠前DEFAULT 100 |
## 变更说明2026-03-09
@@ -108,6 +109,7 @@
| 2026-02-03 | 初始创建,区域级精确 + LIKE 模糊匹配 |
| 2026-03-07 | 新增 source_table_name 支持台桌级细分;废弃 BILLIARD_VIP |
| 2026-03-09 | 改为纯台桌级精确映射,删除所有 LIKE 和区域级映射 |
| 2026-03-20 | 新增 sort_order 字段,控制前端筛选器分类显示排序 |
## 验证 SQL

View File

@@ -0,0 +1,159 @@
# dws_finance_area_daily 区域日粒度财务原子层表
> 生成时间2026-03-28
> 关联 SPECboard-finance-dws-area-refactor
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | dws |
| 表名 | dws_finance_area_daily |
| 主键 | id |
| 唯一键 | (site_id, stat_date, area_code) |
| 数据来源 | dwd_settlement_head + dim_table + dws_finance_daily_summary |
| 更新频率 | 每小时更新当日数据 |
| 幂等策略 | delete-before-insert按 site_id + stat_date 删除后插入 9 行) |
| 说明 | 按 (site_id, stat_date, area_code) 粒度存储 9 个区域的收入/优惠/现金流预计算数据 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | tenant_id | BIGINT | NO | 租户ID |
| 4 | stat_date | DATE | NO | 统计日期(营业日,按 BUSINESS_DAY_START_HOUR=8 切点) |
| 5 | area_code | VARCHAR(20) | NO | 区域编码all/hall/hallA/hallB/hallC/vip/snooker/mahjong/ktv |
| 6 | table_fee_amount | NUMERIC(14,2) | NO | 台费正价 |
| 7 | goods_amount | NUMERIC(14,2) | NO | 商品正价 |
| 8 | assistant_pd_amount | NUMERIC(14,2) | NO | 助教基础课正价(陪打) |
| 9 | assistant_cx_amount | NUMERIC(14,2) | NO | 助教激励课正价(超休) |
| 10 | gross_amount | NUMERIC(14,2) | NO | 毛收入 = 四项之和 |
| 11 | discount_groupbuy | NUMERIC(14,2) | NO | 团购优惠 |
| 12 | discount_vip | NUMERIC(14,2) | NO | 会员折扣 |
| 13 | discount_manual | NUMERIC(14,2) | NO | 手动调整adjust_amount |
| 14 | discount_gift_card | NUMERIC(14,2) | NO | 赠送卡消费金额口径 |
| 15 | discount_rounding | NUMERIC(14,2) | NO | 抹零 |
| 16 | discount_other | NUMERIC(14,2) | NO | 其他优惠 |
| 17 | discount_total | NUMERIC(14,2) | NO | 优惠合计 = 六项之和 |
| 18 | confirmed_income | NUMERIC(14,2) | NO | 确认收入 = gross_amount - discount_total |
| 19 | cash_pay_amount | NUMERIC(14,2) | NO | 收银实付(仅 all 行有效) |
| 20 | cash_paper_amount | NUMERIC(14,2) | NO | 纸币支付(仅 all 行有效) |
| 21 | scan_pay_amount | NUMERIC(14,2) | NO | 扫码支付(仅 all 行有效) |
| 22 | groupbuy_pay_amount | NUMERIC(14,2) | NO | 团购支付金额(仅 all 行有效) |
| 23 | recharge_cash_inflow | NUMERIC(14,2) | NO | 充值现金流入(仅 all 行有效) |
| 24 | cash_inflow_total | NUMERIC(14,2) | NO | 现金流入合计(仅 all 行有效) |
| 25 | cash_outflow_total | NUMERIC(14,2) | NO | 现金流出合计(仅 all 行有效) |
| 26 | cash_balance_change | NUMERIC(14,2) | NO | 现金余额变动(仅 all 行有效) |
| 27 | card_consume_total | NUMERIC(14,2) | NO | 卡消费合计(仅 all 行有效) |
| 28 | recharge_card_consume | NUMERIC(14,2) | NO | 充值卡消费(仅 all 行有效) |
| 29 | gift_card_consume | NUMERIC(14,2) | NO | 赠送卡消费(仅 all 行有效) |
| 30 | recharge_cash | NUMERIC(14,2) | NO | 充值现金(仅 all 行有效) |
| 31 | first_recharge_cash | NUMERIC(14,2) | NO | 首充现金(仅 all 行有效) |
| 32 | renewal_cash | NUMERIC(14,2) | NO | 续充现金(仅 all 行有效) |
| 33 | order_count | INTEGER | NO | 结账单数 |
| 34 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 35 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 约束与索引
| 约束/索引 | 类型 | 列 |
|-----------|------|-----|
| dws_finance_area_daily_pkey | PRIMARY KEY | id |
| dws_finance_area_daily_site_id_stat_date_area_code_key | UNIQUE | (site_id, stat_date, area_code) |
## RLS 视图
```sql
CREATE OR REPLACE VIEW dws.v_dws_finance_area_daily AS
SELECT * FROM dws.dws_finance_area_daily
WHERE site_id = (current_setting('app.current_site_id'::text))::bigint;
```
## 数学恒等式
```
gross_amount = table_fee_amount + goods_amount + assistant_pd_amount + assistant_cx_amount
discount_total = discount_groupbuy + discount_vip + discount_manual + discount_gift_card + discount_rounding + discount_other
confirmed_income = gross_amount - discount_total
```
- `area_code ≠ 'all'` 时:现金流/卡消费/充值字段 = 0
- `all` 行收入/优惠 = hallA~ktv 各行对应字段之和
- `hall` 行 = hallA~ktv 各行之和(历史兼容)
- 每个 (site_id, stat_date) 恰好 9 行
## 变更原因
解决财务看板在 `area≠all` 时优惠数据从全局 DWS 表取数导致区域级优惠占比严重失真的 bug如 B区优惠占比 417.9%)。
## 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL 任务 | 新增 DWS_FINANCE_AREA_DAILY 任务,依赖 DWD_LOAD_FROM_ODS |
| 后端 API | board_service.py 改为从本表查询 overview/revenue 板块数据 |
| 小程序 | 无直接影响API 签名不变) |
| dws_finance_daily_summary | 不改动,本表的 all 行现金流/充值/卡消费复用其数据 |
## 回滚策略
```sql
DROP VIEW IF EXISTS dws.v_dws_finance_area_daily;
DROP TABLE IF EXISTS dws.dws_finance_area_daily;
```
回滚后后端需恢复到从 `dws_finance_daily_summary` 取数的旧逻辑。
## 验证 SQL
```sql
-- 1. 验证表存在且有数据
SELECT COUNT(*), COUNT(DISTINCT area_code), MIN(stat_date), MAX(stat_date)
FROM dws.dws_finance_area_daily
WHERE site_id = 1;
-- 2. 验证每天恰好 9 行
SELECT stat_date, COUNT(*) AS row_count
FROM dws.dws_finance_area_daily
WHERE site_id = 1
GROUP BY stat_date
HAVING COUNT(*) != 9
ORDER BY stat_date;
-- 3. 验证收入恒等式
SELECT stat_date, area_code,
gross_amount,
(table_fee_amount + goods_amount + assistant_pd_amount + assistant_cx_amount) AS calc_gross,
gross_amount - (table_fee_amount + goods_amount + assistant_pd_amount + assistant_cx_amount) AS diff
FROM dws.dws_finance_area_daily
WHERE site_id = 1
AND gross_amount != (table_fee_amount + goods_amount + assistant_pd_amount + assistant_cx_amount);
-- 4. 验证优惠恒等式
SELECT stat_date, area_code,
discount_total,
(discount_groupbuy + discount_vip + discount_manual + discount_gift_card + discount_rounding + discount_other) AS calc_disc,
discount_total - (discount_groupbuy + discount_vip + discount_manual + discount_gift_card + discount_rounding + discount_other) AS diff
FROM dws.dws_finance_area_daily
WHERE site_id = 1
AND discount_total != (discount_groupbuy + discount_vip + discount_manual + discount_gift_card + discount_rounding + discount_other);
-- 5. 验证非 all 行现金流为零
SELECT stat_date, area_code, cash_inflow_total, cash_outflow_total
FROM dws.dws_finance_area_daily
WHERE site_id = 1
AND area_code != 'all'
AND (cash_inflow_total != 0 OR cash_outflow_total != 0);
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯delete-before-insert |
| 数据范围 | 2025-07-16 ~ 至今 |
| 依赖表 | dwd_settlement_head, dim_table, dws_finance_daily_summary |
| 回填脚本 | `scripts/ops/backfill_finance_area_daily.py` |

View File

@@ -0,0 +1,121 @@
# dws_finance_board_cache 看板缓存层表
> 生成时间2026-03-28
> 关联 SPECboard-finance-dws-area-refactor
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | dws |
| 表名 | dws_finance_board_cache |
| 主键 | id |
| 唯一键 | (site_id, time_range, area_code) |
| 数据来源 | dws_finance_area_daily日粒度原子层 |
| 更新频率 | 每天一次(营业日切点后) |
| 幂等策略 | ON CONFLICT (site_id, time_range, area_code) DO UPDATE |
| 说明 | 缓存已完成周期的 overview 聚合结果,避免重复 SUM 计算 |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | id | BIGSERIAL | NO | 自增主键 |
| 2 | site_id | BIGINT | NO | 门店ID |
| 3 | time_range | VARCHAR(20) | NO | 时间范围lastMonth/lastWeek/lastQuarter/quarter3/half6 |
| 4 | area_code | VARCHAR(20) | NO | 区域编码all/hall/hallA/hallB/hallC/vip/snooker/mahjong/ktv |
| 5 | start_date | DATE | NO | 当期起始日期 |
| 6 | end_date | DATE | NO | 当期结束日期 |
| 7 | prev_start_date | DATE | YES | 上期起始日期(环比用) |
| 8 | prev_end_date | DATE | YES | 上期结束日期(环比用) |
| 9 | occurrence | NUMERIC(14,2) | NO | 发生额gross_amount 周期汇总) |
| 10 | discount | NUMERIC(14,2) | NO | 优惠合计discount_total 周期汇总) |
| 11 | discount_rate | NUMERIC(8,4) | NO | 优惠占比 = discount / occurrence |
| 12 | confirmed_revenue | NUMERIC(14,2) | NO | 确认收入 = occurrence - discount |
| 13 | cash_in | NUMERIC(14,2) | NO | 现金流入合计(仅 area_code=all 有效) |
| 14 | cash_out | NUMERIC(14,2) | NO | 现金流出合计(仅 area_code=all 有效) |
| 15 | cash_balance | NUMERIC(14,2) | NO | 现金余额变动 = cash_in - cash_out |
| 16 | balance_rate | NUMERIC(8,4) | NO | 余额变动率 = cash_balance / cash_in |
| 17 | data_fingerprint | VARCHAR(64) | YES | 源数据 MD5 指纹,用于检测补录导致的数据变化 |
| 18 | computed_at | TIMESTAMPTZ | NO | 缓存计算时间 |
| 19 | created_at | TIMESTAMPTZ | NO | 创建时间 |
| 20 | updated_at | TIMESTAMPTZ | NO | 更新时间 |
## 约束与索引
| 约束/索引 | 类型 | 列 |
|-----------|------|-----|
| dws_finance_board_cache_pkey | PRIMARY KEY | id |
| dws_finance_board_cache_site_id_time_range_area_code_key | UNIQUE | (site_id, time_range, area_code) |
## RLS 视图
```sql
CREATE OR REPLACE VIEW dws.v_dws_finance_board_cache AS
SELECT * FROM dws.dws_finance_board_cache
WHERE site_id = (current_setting('app.current_site_id'::text))::bigint;
```
## 缓存策略
- 已完成周期缓存lastMonth, lastWeek, lastQuarter, quarter3, half6
- 当期周期不缓存month, week, quarter
- 失效条件:`data_fingerprint` 变化(补录导致源数据变化)
- 指纹算法:`MD5(sorted [(stat_date, gross_amount, discount_total), ...])`
## 变更原因
为已完成周期的聚合结果提供缓存,避免每次查询都从日粒度表 SUM 计算。通过数据指纹机制自动检测补录导致的数据变化并重算。
## 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL 任务 | 新增 DWS_FINANCE_BOARD_CACHE 任务,依赖 DWS_FINANCE_AREA_DAILY |
| 后端 API | board_service.py 已完成周期先查缓存,未命中从日粒度表 SUM |
| 小程序 | 无直接影响API 签名不变) |
## 回滚策略
```sql
DROP VIEW IF EXISTS dws.v_dws_finance_board_cache;
DROP TABLE IF EXISTS dws.dws_finance_board_cache;
```
回滚后后端需移除缓存查询逻辑,改为每次从日粒度表 SUM。
## 验证 SQL
```sql
-- 1. 验证表存在且有数据
SELECT COUNT(*), COUNT(DISTINCT time_range), COUNT(DISTINCT area_code)
FROM dws.dws_finance_board_cache
WHERE site_id = 1;
-- 2. 验证唯一约束生效(应返回 0 行)
SELECT site_id, time_range, area_code, COUNT(*)
FROM dws.dws_finance_board_cache
GROUP BY site_id, time_range, area_code
HAVING COUNT(*) > 1;
-- 3. 验证 confirmed_revenue = occurrence - discount
SELECT time_range, area_code,
occurrence, discount, confirmed_revenue,
(occurrence - discount) AS expected
FROM dws.dws_finance_board_cache
WHERE site_id = 1
AND ABS(confirmed_revenue - (occurrence - discount)) > 0.01;
-- 4. 验证当期周期不在缓存中
SELECT * FROM dws.dws_finance_board_cache
WHERE time_range IN ('month', 'week', 'quarter');
```
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯upsert 幂等) |
| 数据范围 | 取决于 dws_finance_area_daily 的数据范围 |
| 依赖表 | dws_finance_area_daily |
| 回填脚本 | `scripts/ops/backfill_finance_area_daily.py`(阶段 2 自动重算缓存) |