chore: v1 整理 — 清理历史文件、DDL 合并、文档归档

- 清理 1155 个已删除的历史文件(废弃 prompt_logs、tmp、旧 ops 脚本)
- export/ 数据文件从 git 移除(已在 .gitignore)
- demo-miniprogram 从 tmp/ 移入 apps/,添加 CLAUDE.md 注解
- DDL 合并:完整 schema 定义填充到 db/*/schemas/(从 docs/database/ddl/ 复制)
- 39 个 v1 迁移脚本归档到 db/_archived/migrations_v1_merged/
- 4 个迁移变更类 BD_Manual 文档归档到 docs/database/_archived/
- .gitignore 补充 .vite/ 和 apps/*.zip
- settings.json 添加 effortLevel 默认配置
- scripts/ops/ 新增运维脚本入库

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Neo
2026-04-06 00:39:27 +08:00
parent 6f8f12314f
commit 779b2f6d52
1340 changed files with 9124 additions and 132087 deletions

View File

@@ -0,0 +1,53 @@
# BD 手册auth.users.avatar_url 字段
## 概述
`auth.users` 表新增 `avatar_url` 字段,存储用户头像的相对路径。
## 字段定义
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `avatar_url` | `VARCHAR(500)` | `NULL` | 头像相对路径,格式 `avatars/{user_id}.jpg` |
## 数据流
1. 小程序端通过 `<button open-type="chooseAvatar">` 获取微信头像临时路径
2. 通过 `wx.uploadFile` 上传到 `POST /api/xcx/avatar/upload`
3. 后端保存文件到 `AVATAR_EXPORT_PATH/{user_id}.jpg`(覆盖式,幂等)
4. 数据库 `avatar_url` 更新为 `avatars/{user_id}.jpg`(相对路径)
5. 小程序通过 `GET /api/xcx/avatar/{user_id}` 获取头像文件
## 关联接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/xcx/avatar/upload` | POST | 上传头像,更新 avatar_url |
| `/api/xcx/avatar/{user_id}` | GET | 获取头像文件FileResponse |
| `/api/xcx/me` | GET | 返回 avatar_url 字段 |
## 设计决策
- 审核表 `auth.user_applications` 不冗余 `avatar_url`,通过 JOIN `auth.users` 获取(头像可能更新)
- 文件命名按 `user_id`,覆盖式保存(幂等,无历史版本)
- 文件大小限制 2MB空文件拒绝
## 迁移脚本
`db/zqyy_app/migrations/20260324_add_avatar_url_to_users.sql`
## 环境变量
`AVATAR_EXPORT_PATH` — 头像文件存储目录,缺失时后端报 500 错误
## 回滚
```sql
ALTER TABLE auth.users DROP COLUMN IF EXISTS avatar_url;
```
## 验证
```sql
SELECT id, avatar_url FROM auth.users WHERE avatar_url IS NOT NULL LIMIT 5;
```

View File

@@ -0,0 +1,83 @@
# BD_ManualFDW 财务区域查询映射fdw_finance_area
> 目标库:后端通过 `get_etl_readonly_connection(site_id)` 直连 ETL 库
> 关联 SPECboard-finance-dws-area-refactor
> 日期2026-03-28
---
## 1. 变更说明
### 新增 RLS 视图ETL 库 app Schema 自动导出)
本次新增的两张 DWS 表各有一个 RLS 视图,后端通过直连 ETL 库的 `app.v_*` 视图访问(与 RNS1.2 以来的模式一致,不使用 `fdw_etl.*` 外部表)。
| 视图 | 源表 | 用途 |
|------|------|------|
| `dws.v_dws_finance_area_daily` | `dws.dws_finance_area_daily` | 区域日粒度财务数据overview/revenue 板块) |
| `dws.v_dws_finance_board_cache` | `dws.dws_finance_board_cache` | 已完成周期缓存overview 8 项指标) |
### 后端查询函数
| 函数 | 视图 | 用途 |
|------|------|------|
| `get_finance_overview_area()` | `v_dws_finance_area_daily` | 按 area_code 聚合 overview 8 项指标 |
| `get_finance_revenue_area()` | `v_dws_finance_area_daily` | 按 area_code 聚合 revenue 板块数据 |
| `get_finance_board_cache()` | `v_dws_finance_board_cache` | 查询缓存 |
| `set_finance_board_cache()` | `dws.dws_finance_board_cache` | 写入/更新缓存 |
### RLS 隔离
所有查询通过 `SET LOCAL app.current_site_id = :site_id` 设置门店隔离参数RLS 视图的 `WHERE site_id = current_setting('app.current_site_id')::bigint` 自动过滤。
---
## 2. 兼容性影响
| 组件 | 影响 |
|------|------|
| ETL 任务 | 无影响。FDW 配置不修改 ETL 库 |
| 后端 API | `fdw_queries.py` 新增 4 个函数,`board_service.py` 改为调用新函数 |
| 小程序 | 无直接影响API 签名不变) |
| 现有 FDW 外部表 | 无影响。新视图通过直连 ETL 库访问,不经过 `fdw_etl` Schema |
---
## 3. 回滚策略
后端回滚:
1. 恢复 `board_service.py` 到旧逻辑(从 `dws_finance_daily_summary` 取数)
2. 移除 `fdw_queries.py` 中的 4 个新函数
数据库回滚:
```sql
DROP VIEW IF EXISTS dws.v_dws_finance_board_cache;
DROP TABLE IF EXISTS dws.dws_finance_board_cache;
DROP VIEW IF EXISTS dws.v_dws_finance_area_daily;
DROP TABLE IF EXISTS dws.dws_finance_area_daily;
```
---
## 4. 验证 SQL
```sql
-- 1. 验证 RLS 视图可访问(需先设置 site_id
SET LOCAL app.current_site_id = '1';
SELECT COUNT(*) FROM dws.v_dws_finance_area_daily;
-- 2. 验证缓存视图可访问
SET LOCAL app.current_site_id = '1';
SELECT COUNT(*) FROM dws.v_dws_finance_board_cache;
-- 3. 验证 app_reader 角色有 SELECT 权限
SELECT has_table_privilege('app_reader', 'dws.v_dws_finance_area_daily', 'SELECT') AS daily_ok,
has_table_privilege('app_reader', 'dws.v_dws_finance_board_cache', 'SELECT') AS cache_ok;
-- 4. 验证区域日粒度数据完整性(每天 9 行)
SET LOCAL app.current_site_id = '1';
SELECT stat_date, COUNT(*) AS cnt
FROM dws.v_dws_finance_area_daily
GROUP BY stat_date
HAVING COUNT(*) != 9;
```

View File

@@ -0,0 +1,74 @@
# BD 手册idx_coach_tasks_rb_unique_active
| 字段 | 值 |
|------|-----|
| 数据库 | zqyy_app |
| Schema | biz |
| 表 | coach_tasks |
| 索引名 | idx_coach_tasks_rb_unique_active |
| 类型 | UNIQUE (partial) |
| 创建日期 | 2026-03-25 |
| 迁移脚本 | `db/zqyy_app/migrations/2026-03-25__relationship_building_baseline.sql` |
## 索引定义
```sql
CREATE UNIQUE INDEX idx_coach_tasks_rb_unique_active
ON biz.coach_tasks (site_id, assistant_id, member_id)
WHERE task_type = 'relationship_building' AND status = 'active';
```
## 用途
保证每个 `(site_id, assistant_id, member_id)` 组合最多存在 1 条 `status = 'active'``relationship_building` 任务。
支持 `_generate_baseline_relationship_tasks()` 中的 upsert 操作:
```sql
INSERT INTO biz.coach_tasks (...)
VALUES (...)
ON CONFLICT (site_id, assistant_id, member_id)
WHERE task_type = 'relationship_building' AND status = 'active'
DO NOTHING
```
## 业务背景
保底 relationship_building 任务:对每个助教,所有确切发生过服务关系(`session_count > 0`)的客户都生成一条 relationship_building 任务。partial unique index 确保幂等性——重复运行不会产生重复任务。
## 影响范围
- 写入方:`task_generator._generate_baseline_relationship_tasks()` — upsert 依赖此索引
- 读取方:`task_manager.get_task_list_v2()` — 查询不直接使用此索引,但受益于去重保证
- 不影响其他 task_type 的任务partial index 仅覆盖 `relationship_building` + `active`
## 回滚
```sql
DROP INDEX IF EXISTS biz.idx_coach_tasks_rb_unique_active;
```
回滚后 `_generate_baseline_relationship_tasks()``ON CONFLICT` 子句会报错(无匹配索引),需同步回滚代码。
## 验证 SQL
```sql
-- 1. 确认索引存在
SELECT indexname, indexdef FROM pg_indexes
WHERE indexname = 'idx_coach_tasks_rb_unique_active';
-- 2. 确认无重复
SELECT site_id, assistant_id, member_id, COUNT(*)
FROM biz.coach_tasks
WHERE task_type = 'relationship_building' AND status = 'active'
GROUP BY site_id, assistant_id, member_id
HAVING COUNT(*) > 1;
-- 应返回 0 行
-- 3. 测试 upsert 幂等性dry run
EXPLAIN INSERT INTO biz.coach_tasks
(site_id, assistant_id, member_id, task_type, status, priority_score)
VALUES (1, 1, 1, 'relationship_building', 'active', 0)
ON CONFLICT (site_id, assistant_id, member_id)
WHERE task_type = 'relationship_building' AND status = 'active'
DO NOTHING;
```

View File

@@ -0,0 +1,105 @@
# BD 手册user_site_roles / user_assistant_binding 软删除
## 变更日期
2026-03-24
## 变更说明
`auth.user_site_roles``auth.user_assistant_binding` 两张表上新增软删除字段,替代原有的物理删除(`DELETE`)。
### 新增字段
| 表 | 字段 | 类型 | 默认值 | 说明 |
|----|------|------|--------|------|
| `auth.user_site_roles` | `is_removed` | `boolean` | `false` | 软删除标记 |
| `auth.user_site_roles` | `removed_at` | `timestamptz` | `NULL` | 移除时间戳 |
| `auth.user_assistant_binding` | `is_removed` | `boolean` | `false` | 软删除标记 |
| `auth.user_assistant_binding` | `removed_at` | `timestamptz` | `NULL` | 移除时间戳 |
### 新增索引
| 索引名 | 表 | 类型 | 说明 |
|--------|-----|------|------|
| `ix_user_site_roles_active` | `user_site_roles` | 部分索引 `WHERE is_removed = false` | 加速活跃记录查询 |
| `ix_user_assistant_binding_active` | `user_assistant_binding` | 部分索引 `WHERE is_removed = false` | 加速活跃记录查询 |
## 兼容性影响
### 后端 API已同步修改
所有查询 `user_site_roles``user_assistant_binding` 的位置均已添加 `AND is_removed = false` 过滤:
- `xcx_auth.py`登录、me 接口、切换门店、刷新令牌、获取门店列表、dev 调试接口
- `tenant_users.py`:用户列表、编辑用户、更新绑定、移除用户
- `role.py`:权限查询、门店列表、角色检查
- `task_manager.py`:获取助教 ID
- `task_generator.py`:门店助教规模检查、入驻时间保护
- `performance_service.py`:助教信息查询
### ETL
无直接影响。ETL 不写入这两张表。
### 小程序
无代码改动。被移除的用户在小程序端会因角色查询返回空而进入已有的无权限路由。
### 租户管理后台
`remove_user` 操作从 `DELETE` 改为 `UPDATE SET is_removed = true, removed_at = now()`
## 查询规则(强制)
所有读取 `user_site_roles``user_assistant_binding` 的 SELECT 查询,必须包含 `AND is_removed = false`(或等效的 JOIN 条件 `AND xxx.is_removed = false`)。
例外:
- 管理后台需要查看已移除记录的场景(如审计日志)
- dev 调试接口中的物理删除操作(仅开发模式)
## 回滚策略
```sql
-- 1. 恢复所有被软删除的记录
UPDATE auth.user_site_roles SET is_removed = false, removed_at = NULL WHERE is_removed = true;
UPDATE auth.user_assistant_binding SET is_removed = false, removed_at = NULL WHERE is_removed = true;
-- 2. 删除索引
DROP INDEX IF EXISTS auth.ix_user_site_roles_active;
DROP INDEX IF EXISTS auth.ix_user_assistant_binding_active;
-- 3. 删除字段
ALTER TABLE auth.user_site_roles DROP COLUMN IF EXISTS removed_at;
ALTER TABLE auth.user_site_roles DROP COLUMN IF EXISTS is_removed;
ALTER TABLE auth.user_assistant_binding DROP COLUMN IF EXISTS removed_at;
ALTER TABLE auth.user_assistant_binding DROP COLUMN IF EXISTS is_removed;
```
注意:回滚后需同步还原后端代码中所有 `AND is_removed = false` 过滤条件,并将 `remove_user` 恢复为 `DELETE`
## 验证 SQL
```sql
-- 1. 确认字段存在且默认值正确
SELECT column_name, data_type, column_default
FROM information_schema.columns
WHERE table_schema = 'auth'
AND table_name IN ('user_site_roles', 'user_assistant_binding')
AND column_name IN ('is_removed', 'removed_at')
ORDER BY table_name, column_name;
-- 2. 确认部分索引存在
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'auth'
AND indexname IN ('ix_user_site_roles_active', 'ix_user_assistant_binding_active');
-- 3. 确认现有数据未被误标记(所有记录 is_removed 应为 false
SELECT 'user_site_roles' AS tbl, COUNT(*) AS total,
COUNT(*) FILTER (WHERE is_removed = true) AS removed
FROM auth.user_site_roles
UNION ALL
SELECT 'user_assistant_binding', COUNT(*),
COUNT(*) FILTER (WHERE is_removed = true)
FROM auth.user_assistant_binding;
-- 4. 确认活跃记录查询走部分索引EXPLAIN 检查)
EXPLAIN SELECT * FROM auth.user_site_roles WHERE user_id = 1 AND site_id = 1 AND is_removed = false;
```
## 迁移脚本
`db/zqyy_app/migrations/20260324_soft_delete_user_site_roles_and_binding.sql`