Files
Neo-ZQYY/.kiro/specs/01-miniapp-db-foundation/design.md
Neo b25308c3f4 feat: P1-P3 全栈集成 — 数据库基础 + DWS 扩展 + 小程序鉴权 + 工程化体系
## P1 数据库基础
- zqyy_app: 创建 auth/biz schema、FDW 连接 etl_feiqiu
- etl_feiqiu: 创建 app schema RLS 视图、商品库存预警表
- 清理 assistant_abolish 残留数据

## P2 ETL/DWS 扩展
- 新增 DWS 助教订单贡献度表 (dws.assistant_order_contribution)
- 新增 assistant_order_contribution_task 任务及 RLS 视图
- member_consumption 增加充值字段、assistant_daily 增加处罚字段
- 更新 ODS/DWD/DWS 任务文档及业务规则文档
- 更新 consistency_checker、flow_runner、task_registry 等核心模块

## P3 小程序鉴权系统
- 新增 xcx_auth 路由/schema(微信登录 + JWT)
- 新增 wechat/role/matching/application 服务层
- zqyy_app 鉴权表迁移 + 角色权限种子数据
- auth/dependencies.py 支持小程序 JWT 鉴权

## 文档与审计
- 新增 DOCUMENTATION-MAP 文档导航
- 新增 7 份 BD_Manual 数据库变更文档
- 更新 DDL 基线快照(etl_feiqiu 6 schema + zqyy_app auth)
- 新增全栈集成审计记录、部署检查清单更新
- 新增 BACKLOG 路线图、FDW→Core 迁移计划

## Kiro 工程化
- 新增 5 个 Spec(P1/P2/P3/全栈集成/核心业务)
- 新增审计自动化脚本(agent_on_stop/build_audit_context/compliance_prescan)
- 新增 6 个 Hook(合规检查/会话日志/提交审计等)
- 新增 doc-map steering 文件

## 运维与测试
- 新增 ops 脚本:迁移验证/API 健康检查/ETL 监控/集成报告
- 新增属性测试:test_dws_contribution / test_auth_system
- 清理过期 export 报告文件
- 更新 .gitignore 排除规则
2026-02-26 08:03:53 +08:00

432 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 设计文档小程序数据库基础设施层miniapp-db-foundation
## 概述
本设计实现 P1 基础设施层的三大核心能力:
1. **业务库 Schema 划分**:在 `test_zqyy_app` 中创建 `auth`(认证)和 `biz`(业务)两个 Schema配合权限管理
2. **ETL 库 RLS 视图层**:在 `test_etl_feiqiu.app` Schema 中为 35 张 DWD/DWS 表创建行级安全视图,通过 `site_id` 隔离多门店数据
3. **FDW 跨库映射**:通过 `postgres_fdw` 将 ETL 库的 RLS 视图映射为业务库的只读外部表
**环境变量驱动**:所有数据库名称通过 `.env` 环境变量引用,不硬编码。迁移脚本中使用占位符,验证脚本从 `PG_DSN` / `APP_DB_DSN` 解析连接信息。
| 环境变量 | 用途 | 示例值 |
|---------|------|--------|
| `PG_DSN` | ETL 库连接字符串 | `postgresql://user:pass@host:5432/test_etl_feiqiu` |
| `APP_DB_DSN` | 业务库连接字符串 | `postgresql://user:pass@host:5432/test_zqyy_app` |
整体数据流向:
```
ETL 库PG_DSN 业务库APP_DB_DSN
┌─────────────────────┐ ┌─────────────────────┐
│ dwd.dim_member │ │ auth (用户认证) │
│ dwd.dim_assistant │ │ biz (业务数据) │
│ dws.dws_* │ │ public (系统管理) │
│ dws.cfg_* │ │ │
│ │ │ │ fdw_etl │
│ ▼ │ postgres_fdw │ ├ v_dim_member │
│ app.v_dim_member │ ◄──────────────► │ ├ v_dim_assistant │
│ app.v_dws_* │ IMPORT SCHEMA │ └ v_dws_* │
│ (RLS: site_id 过滤) │ │ (外部表,只读) │
└─────────────────────┘ └─────────────────────┘
```
## 架构
### 分层架构
```mermaid
graph TB
subgraph "业务库APP_DB_DSN"
AUTH["auth Schema<br/>用户认证、权限、映射"]
BIZ["biz Schema<br/>业务数据"]
PUBLIC["public Schema<br/>系统管理表(保留)"]
FDW["fdw_etl Schema<br/>FDW 外部表(只读)"]
end
subgraph "ETL 库PG_DSN"
APP["app Schema<br/>RLS 视图层"]
DWD["dwd Schema<br/>明细层11 张表)"]
DWS["dws Schema<br/>汇总层24 张表)"]
end
FDW -->|"postgres_fdw<br/>IMPORT FOREIGN SCHEMA"| APP
APP -->|"WHERE site_id = current_setting(...)"| DWD
APP -->|"WHERE site_id = current_setting(...)"| DWS
```
### 执行顺序
迁移脚本必须按以下顺序执行:
1. **ETL 库**(通过 `PG_DSN` 连接):创建 `app` Schema → 创建 `app_reader` 角色 → 创建 RLS 视图 → 授权
2. **业务库**(通过 `APP_DB_DSN` 连接):创建 `auth`/`biz` Schema → 安装 `postgres_fdw` → 创建外部服务器 → 用户映射 → 导入外部表 → 授权
## 组件与接口
### 组件 1Schema 管理(业务库)
**职责**:在业务库(`APP_DB_DSN`)中创建 `auth``biz` Schema配置权限。
**SQL 接口**
```sql
-- 创建 Schema
CREATE SCHEMA IF NOT EXISTS auth;
CREATE SCHEMA IF NOT EXISTS biz;
-- 授权 app_user
GRANT USAGE ON SCHEMA auth TO app_user;
GRANT USAGE ON SCHEMA biz TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA auth TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA biz TO app_user;
-- 未来新表自动授权
ALTER DEFAULT PRIVILEGES IN SCHEMA auth
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA biz
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
```
### 组件 2RLS 视图层ETL 库)
**职责**:在 ETL 库(`PG_DSN`)的 `app` Schema 中为每张源表创建带 `site_id` 过滤的视图。
**视图命名规则**`app.v_<源表名>`,例如 `app.v_dim_member``app.v_dws_member_consumption_summary`
**视图模板**
```sql
CREATE OR REPLACE VIEW app.v_<> AS
SELECT * FROM <schema>.<>
WHERE site_id = current_setting('app.current_site_id')::bigint;
```
**DWD 层视图清单11 张)**
| 视图名 | 源表 |
|--------|------|
| `app.v_dim_member` | `dwd.dim_member` |
| `app.v_dim_assistant` | `dwd.dim_assistant` |
| `app.v_dim_member_card_account` | `dwd.dim_member_card_account` |
| `app.v_dim_table` | `dwd.dim_table` |
| `app.v_dwd_settlement_head` | `dwd.dwd_settlement_head` |
| `app.v_dwd_table_fee_log` | `dwd.dwd_table_fee_log` |
| `app.v_dwd_assistant_service_log` | `dwd.dwd_assistant_service_log` |
| `app.v_dwd_recharge_order` | `dwd.dwd_recharge_order` |
| `app.v_dwd_store_goods_sale` | `dwd.dwd_store_goods_sale` |
| `app.v_dim_staff` | `dwd.dim_staff` |
| `app.v_dim_staff_ex` | `dwd.dim_staff_ex` |
**DWS 层视图清单24 张)**
| 视图名 | 源表 |
|--------|------|
| `app.v_dws_member_consumption_summary` | `dws.dws_member_consumption_summary` |
| `app.v_dws_member_visit_detail` | `dws.dws_member_visit_detail` |
| `app.v_dws_member_winback_index` | `dws.dws_member_winback_index` |
| `app.v_dws_member_newconv_index` | `dws.dws_member_newconv_index` |
| `app.v_dws_member_recall_index` | `dws.dws_member_recall_index` |
| `app.v_dws_member_assistant_relation_index` | `dws.dws_member_assistant_relation_index` |
| `app.v_dws_member_assistant_intimacy` | `dws.dws_member_assistant_intimacy` |
| `app.v_dws_assistant_daily_detail` | `dws.dws_assistant_daily_detail` |
| `app.v_dws_assistant_monthly_summary` | `dws.dws_assistant_monthly_summary` |
| `app.v_dws_assistant_salary_calc` | `dws.dws_assistant_salary_calc` |
| `app.v_dws_assistant_customer_stats` | `dws.dws_assistant_customer_stats` |
| `app.v_dws_assistant_finance_analysis` | `dws.dws_assistant_finance_analysis` |
| `app.v_dws_finance_daily_summary` | `dws.dws_finance_daily_summary` |
| `app.v_dws_finance_income_structure` | `dws.dws_finance_income_structure` |
| `app.v_dws_finance_recharge_summary` | `dws.dws_finance_recharge_summary` |
| `app.v_dws_finance_discount_detail` | `dws.dws_finance_discount_detail` |
| `app.v_dws_finance_expense_summary` | `dws.dws_finance_expense_summary` |
| `app.v_dws_platform_settlement` | `dws.dws_platform_settlement` |
| `app.v_dws_assistant_recharge_commission` | `dws.dws_assistant_recharge_commission` |
| `app.v_cfg_performance_tier` | `dws.cfg_performance_tier` |
| `app.v_cfg_assistant_level_price` | `dws.cfg_assistant_level_price` |
| `app.v_cfg_bonus_rules` | `dws.cfg_bonus_rules` |
| `app.v_cfg_index_parameters` | `dws.cfg_index_parameters` |
| `app.v_dws_order_summary` | `dws.dws_order_summary` |
**P2 预留(注释标记,暂不创建)**
- `dws.dws_member_spending_power_index` → 待 P2 完成后补充
- `dws.dws_assistant_order_contribution` → 待 P2 完成后补充
**`cfg_*` 表特殊处理**:配置表(`cfg_performance_tier``cfg_assistant_level_price``cfg_bonus_rules``cfg_index_parameters`)可能不含 `site_id` 列。对于不含 `site_id` 的配置表,视图直接 `SELECT *` 不加过滤条件。
**权限配置**
```sql
-- 创建只读角色(如不存在)
DO $$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'app_reader') THEN
CREATE ROLE app_reader LOGIN;
END IF;
END $$;
GRANT USAGE ON SCHEMA app TO app_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA app GRANT SELECT ON TABLES TO app_reader;
```
### 组件 3FDW 跨库映射(业务库)
**职责**:通过 `postgres_fdw` 将 ETL 库 `app` Schema 的视图映射为业务库 `fdw_etl` Schema 的外部表。
**实现方式**:使用 `IMPORT FOREIGN SCHEMA` 批量导入,而非逐表定义外部表。这与现有 `db/fdw/setup_fdw_test.sql` 的模式一致。
**环境感知**:迁移脚本中的 `host``dbname``port``password` 等连接参数使用占位符 `'***'`,部署时根据环境替换。项目已有 `db/fdw/setup_fdw_test.sql`(测试环境)和 `db/fdw/setup_fdw.sql`(生产环境)的分环境模式,本次迁移脚本遵循相同模式——提供测试环境和生产环境两个版本。
```sql
-- 安装扩展
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
-- 创建外部服务器
-- host / dbname / port 按环境替换,从 PG_DSN 解析 ETL 库连接信息
CREATE SERVER IF NOT EXISTS etl_feiqiu_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '***', dbname '***', port '***');
-- 用户映射(密码按环境替换)
CREATE USER MAPPING IF NOT EXISTS FOR app_user
SERVER etl_feiqiu_server
OPTIONS (user 'app_reader', password '***');
-- 创建目标 Schema
CREATE SCHEMA IF NOT EXISTS fdw_etl;
-- 批量导入
IMPORT FOREIGN SCHEMA app
FROM SERVER etl_feiqiu_server
INTO fdw_etl;
-- 授权
GRANT USAGE ON SCHEMA fdw_etl TO app_user;
GRANT SELECT ON ALL TABLES IN SCHEMA fdw_etl TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_etl GRANT SELECT ON TABLES TO app_user;
```
**设计决策**
1. 使用 `IMPORT FOREIGN SCHEMA` 而非逐表 `CREATE FOREIGN TABLE`——自动匹配列定义,避免手动维护列类型不一致的风险
2. 新增 RLS 视图后只需重新执行 `IMPORT` 即可同步
3. 与现有 `db/fdw/setup_fdw_test.sql` 保持一致
4. 服务器名使用通用名 `etl_feiqiu_server`(不含环境前缀),通过连接参数区分环境
### 组件 4验证脚本
**职责**:自动化检查所有数据库对象是否正确创建。
**文件位置**`scripts/ops/validate_p1_db_foundation.py`
**接口**
```python
def validate_p1_db_foundation() -> dict:
"""
返回验证结果字典:
{
"schemas": {"auth": bool, "biz": bool, "app": bool, "fdw_etl": bool},
"rls_views": {"app.v_dim_member": bool, ...},
"fdw_tables": {"fdw_etl.v_dim_member": bool, ...},
"rls_filtering": bool,
"permissions": {"app_user": bool, "app_reader": bool},
"errors": [str, ...]
}
"""
```
**环境变量依赖**(强制从 `.env` 加载,缺失时 `RuntimeError` 终止):
- `PG_DSN`ETL 库连接字符串(从中解析 host、port、dbname
- `APP_DB_DSN`:业务库连接字符串(从中解析 host、port、dbname
- 脚本通过 `load_dotenv()` 加载根 `.env`,禁止硬编码任何数据库名称或连接参数
## 数据模型
### Schema 拓扑
```mermaid
erDiagram
test_zqyy_app_auth {
string schema_name "auth"
string purpose "用户认证、权限、映射"
}
test_zqyy_app_biz {
string schema_name "biz"
string purpose "业务数据任务、备注、AI、Excel"
}
test_zqyy_app_fdw_etl {
string schema_name "fdw_etl"
string purpose "FDW 外部表(只读)"
}
test_zqyy_app_public {
string schema_name "public"
string purpose "系统管理表(保留)"
}
test_etl_feiqiu_app {
string schema_name "app"
string purpose "RLS 视图层"
}
test_etl_feiqiu_dwd {
string schema_name "dwd"
string purpose "明细层11 张表)"
}
test_etl_feiqiu_dws {
string schema_name "dws"
string purpose "汇总层24 张表)"
}
test_etl_feiqiu_app ||--o{ test_etl_feiqiu_dwd : "视图引用"
test_etl_feiqiu_app ||--o{ test_etl_feiqiu_dws : "视图引用"
test_zqyy_app_fdw_etl ||--|| test_etl_feiqiu_app : "postgres_fdw 映射"
```
### RLS 视图数据流
对于含 `site_id` 的表:
```
源表数据(全量)→ RLS 视图site_id 过滤)→ FDW 外部表(只读访问)
```
对于不含 `site_id` 的配置表(`cfg_*`
```
源表数据(全量)→ 直通视图(无过滤)→ FDW 外部表(只读访问)
```
### 迁移脚本清单
| 序号 | 目标库 | 文件名 | 内容 |
|------|--------|--------|------|
| 1 | ETL 库(`PG_DSN` | `YYYY-MM-DD__p1_create_app_schema_rls_views.sql` | 创建 app Schema + 全部 RLS 视图 + app_reader 权限 |
| 2 | 业务库(`APP_DB_DSN` | `YYYY-MM-DD__p1_create_auth_biz_schemas.sql` | 创建 auth/biz Schema + app_user 权限 |
| 3 | 业务库(`APP_DB_DSN` | `YYYY-MM-DD__p1_setup_fdw_etl.sql` | FDW 扩展 + 外部服务器 + 用户映射 + 导入外部表 |
## 正确性属性Correctness Properties
*属性是系统在所有有效执行中都应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1默认权限自动授予
*For any*`auth``biz` Schema 中新创建的表,`app_user` 角色都应自动获得 SELECT、INSERT、UPDATE、DELETE 权限,无需额外手动授权。
**Validates: Requirements 1.5**
### Property 2public Schema 不变量
*For any* 迁移脚本执行前后,`test_zqyy_app.public` Schema 中的表集合应保持不变——迁移不应删除、重命名或修改 `public` 中的现有表。
**Validates: Requirements 1.6**
### Property 3RLS 视图定义包含 site_id 过滤
*For any* `test_etl_feiqiu.app` Schema 中的 RLS 视图(含 `site_id` 列的源表对应的视图),其视图定义 SQL 中都应包含 `current_setting('app.current_site_id')` 过滤条件。
**Validates: Requirements 2.4**
### Property 4RLS 过滤正确性
*For any*`site_id` 列的 RLS 视图和任意有效的 `site_id` 值,设置 `app.current_site_id` 后查询该视图,返回结果中所有行的 `site_id` 都应等于设置的值。
**Validates: Requirements 2.5**
### Property 5未设置 site_id 时 RLS 视图拒绝访问
*For any*`site_id` 过滤的 RLS 视图,在未设置 `app.current_site_id` 的会话中执行查询,应抛出错误而非返回数据。
**Validates: Requirements 2.6**
### Property 6FDW 外部表完整性与数据一致性
*For any* `test_etl_feiqiu.app` Schema 中的视图,`test_zqyy_app.fdw_etl` Schema 中都应存在对应的可查询外部表,且在相同 `site_id` 条件下,外部表返回的数据与 RLS 视图返回的数据一致。
**Validates: Requirements 3.4, 3.5, 3.6**
### Property 7迁移脚本结构合规性
*For any* `db/etl_feiqiu/migrations/``db/zqyy_app/migrations/` 中本次新增的迁移脚本文件,文件名应匹配 `YYYY-MM-DD__*.sql` 模式,且文件内容中应包含回滚语句(以注释形式)。
**Validates: Requirements 4.3, 4.4**
### Property 8迁移脚本幂等性
*For any* 本次新增的迁移脚本,连续执行两次的结果应与执行一次相同——第二次执行不应产生错误。
**Validates: Requirements 4.5**
### Property 9环境变量缺失时验证脚本报错
*For any* 必需环境变量(`PG_DSN``APP_DB_DSN`)的缺失组合,验证脚本应立即抛出错误终止,而非静默使用默认值或空字符串。
**Validates: Requirements 5.8**
## 错误处理
### 迁移脚本错误处理
| 场景 | 处理方式 |
|------|---------|
| Schema 已存在 | `CREATE SCHEMA IF NOT EXISTS` 幂等跳过 |
| 视图已存在 | `CREATE OR REPLACE VIEW` 覆盖更新 |
| 角色不存在 | `DO $$ ... IF NOT EXISTS ... END $$` 条件创建 |
| 源表不存在P2 待建表) | 以注释形式预留,不创建视图 |
| FDW 服务器已存在 | `CREATE SERVER IF NOT EXISTS` 幂等跳过 |
| 用户映射已存在 | `CREATE USER MAPPING IF NOT EXISTS` 幂等跳过 |
| `IMPORT FOREIGN SCHEMA` 表已存在 | 先 `DROP SCHEMA fdw_etl CASCADE` 再重新导入(脚本中提供选项) |
### 验证脚本错误处理
| 场景 | 处理方式 |
|------|---------|
| 环境变量缺失 | `RuntimeError` 立即终止,输出缺失变量名 |
| 数据库连接失败 | 捕获 `psycopg2.OperationalError`,输出连接参数(脱敏)和错误信息 |
| Schema/视图/外部表不存在 | 记录为失败项,继续检查其余项目 |
| RLS 过滤验证无数据 | 标记为 SKIP无法验证不标记为失败 |
| 权限查询失败 | 记录具体错误,继续检查 |
### `current_setting` 未设置时的行为
PostgreSQL 中 `current_setting('app.current_site_id')` 在未设置时会抛出 `ERROR: unrecognized configuration parameter "app.current_site_id"`。这是期望行为(需求 2.6),确保不会意外返回全量数据。
如果需要更友好的错误信息,可以使用 `current_setting('app.current_site_id', true)` 返回 NULL然后在视图中用 `CASE` 处理。但当前设计选择让 PostgreSQL 原生报错,因为:
1. 更安全——不可能绕过
2. 后端代码必须显式设置 `SET app.current_site_id = ...`,这是一个强制约束
## 测试策略
### 属性测试Property-Based Testing
使用 Python `hypothesis` 框架,测试目录:`tests/`Monorepo 级属性测试目录)。
每个属性测试至少运行 100 次迭代。每个测试用注释标注对应的设计属性编号。
标注格式:`# Feature: miniapp-db-foundation, Property N: <属性标题>`
**属性测试清单**
| 属性 | 测试方法 | 生成器 |
|------|---------|--------|
| P1 默认权限 | 生成随机表名,在 auth/biz 中创建表,验证 app_user 权限 | `hypothesis.strategies.text` 生成合法 SQL 标识符 |
| P3 视图定义过滤 | 遍历所有 app schema 视图,检查定义 SQL | 无需生成器,遍历所有视图 |
| P4 RLS 过滤正确性 | 生成随机 site_id设置后查询视图验证结果 | `hypothesis.strategies.integers` 生成 site_id |
| P5 未设置 site_id 报错 | 遍历所有 RLS 视图,在新会话中查询 | 无需生成器,遍历所有视图 |
| P7 脚本结构合规 | 遍历所有新增迁移脚本,验证命名和内容 | 无需生成器,遍历文件 |
| P8 幂等性 | 对每个迁移脚本执行两次,验证无错误 | 无需生成器 |
| P9 环境变量缺失 | 生成环境变量缺失组合,验证报错 | `hypothesis.strategies.sampled_from` 生成缺失组合 |
**注意**P2public schema 不变量)和 P6FDW 数据一致性)需要真实数据库环境,作为集成测试在验证脚本中实现,而非 hypothesis 属性测试。
### 单元测试
单元测试聚焦于验证脚本的逻辑正确性:
- 验证脚本在 Schema 缺失时正确报告失败
- 验证脚本在权限不足时正确报告
- 验证脚本的输出格式正确JSON 结构)
- 环境变量缺失时的错误消息包含变量名
### 集成测试
集成测试通过验证脚本 `scripts/ops/validate_p1_db_foundation.py` 实现,覆盖:
- 全部 Schema 存在性检查
- 全部 RLS 视图存在性和过滤正确性
- 全部 FDW 外部表存在性和可查询性
- 权限配置完整性
- FDW 数据与 RLS 视图数据一致性