feat: add service config templates and extraction script

Former-commit-id: 1de24b7eb79676d1aba9d799a58c5a753290cf52
This commit is contained in:
反编译工作区
2026-05-01 19:38:01 +08:00
parent 3175b7074b
commit 8b15445328
2433 changed files with 8322164 additions and 1604 deletions
@@ -0,0 +1,396 @@
# 数据库表结构参考手册 — 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 走查全部 5 个数据库(2 库直连 + 3 库代码推导),输出带 Mermaid ER 图 + 脱敏样本数据的 Markdown 参考手册。
**Architecture:** 分三层执行 — 数据库查询层(直连 INFORMATION_SCHEMA + SELECT 样本)、代码扫描层(并行读取全部 MyBatis Mapper XML 提取表/列/JOIN)、文档生成层(交叉验证 + 组装 Markdown)。直连库与代码扫描完全并行。
**Tech Stack:** MySQL CLI (`mysql`), Bash, Python 3JSON 缓存), Mermaid erDiagram
**Spec:** `docs/superpowers/specs/2026-05-01-database-schema-reference-design.md`
---
## 前置检查
- [ ] **Step 0: 确认数据库可达**
```bash
mysql -h 192.168.3.12 -P 3307 -u root -p123456 -e "SELECT VERSION(); SHOW DATABASES LIKE '%component%'; SHOW DATABASES LIKE '%elevator%';"
```
期望输出: MySQL 版本号 + `component-organization``cw-elevator-application` 两个库存在。
> 若失败:全量降级为代码推导模式(见 Task 4 备选路径)。
---
### Task 1: 采集组件组织库 schema + 样本
**Files:**
- Create: `docs/superpowers/data/component-organization/schema_raw.json`
- Create: `docs/superpowers/data/component-organization/tables.json`
- Create: `docs/superpowers/data/component-organization/samples/`
- [ ] **Step 1: 导出表清单**
```bash
mkdir -p docs/superpowers/data/component-organization/samples
mysql -h 192.168.3.12 -P 3307 -u root -p123456 component-organization -N -e \
"SELECT TABLE_NAME, IFNULL(TABLE_ROWS,0), ENGINE, IFNULL(TABLE_COMMENT,'') \
FROM INFORMATION_SCHEMA.TABLES \
WHERE TABLE_SCHEMA='component-organization' AND TABLE_TYPE='BASE TABLE' \
ORDER BY TABLE_NAME;" > docs/superpowers/data/component-organization/tables.tsv
```
- [ ] **Step 2: 导出全部列定义**
```bash
mysql -h 192.168.3.12 -P 3307 -u root -p123456 component-organization -N -e \
"SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE, IFNULL(COLUMN_DEFAULT,'NULL'), COLUMN_KEY, EXTRA, IFNULL(COLUMN_COMMENT,'') \
FROM INFORMATION_SCHEMA.COLUMNS \
WHERE TABLE_SCHEMA='component-organization' AND TABLE_NAME NOT LIKE 'QRTZ_%' AND TABLE_NAME NOT LIKE 'quartz_%' \
ORDER BY TABLE_NAME, ORDINAL_POSITION;" > docs/superpowers/data/component-organization/columns.tsv
```
- [ ] **Step 3: 导出索引**
```bash
mysql -h 192.168.3.12 -P 3307 -u root -p123456 component-organization -N -e \
"SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, NON_UNIQUE, SEQ_IN_INDEX \
FROM INFORMATION_SCHEMA.STATISTICS \
WHERE TABLE_SCHEMA='component-organization' \
ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX;" > docs/superpowers/data/component-organization/indexes.tsv
```
- [ ] **Step 4: 逐表采集样本(每表 3 行)**
```bash
# 读取表清单,排除系统表,对每张表执行 SELECT * LIMIT 3
while IFS=$'\t' read -r table_name rest; do
if [[ "$table_name" != QRTZ_* ]] && [[ "$table_name" != quartz_* ]]; then
echo "--- Extracting: $table_name ---"
mysql -h 192.168.3.12 -P 3307 -u root -p123456 component-organization -t -e \
"SELECT * FROM \`$table_name\` ORDER BY 1 DESC LIMIT 3;" \
> "docs/superpowers/data/component-organization/samples/${table_name}.txt" 2>&1
fi
done < <(cut -f1 docs/superpowers/data/component-organization/tables.tsv)
```
- [ ] **Step 5: 提交中间产物**
```bash
git add docs/superpowers/data/component-organization/
git commit -m "data: add component-organization schema raw dump and samples"
```
---
### Task 2: 采集电梯应用库 schema + 样本
**Files:**
- Create: `docs/superpowers/data/cw-elevator-application/schema_raw.json`
- Create: `docs/superpowers/data/cw-elevator-application/tables.json`
- Create: `docs/superpowers/data/cw-elevator-application/samples/`
- [ ] **Step 1: 导出表清单**
```bash
mkdir -p docs/superpowers/data/cw-elevator-application/samples
mysql -h 192.168.3.12 -P 3307 -u root -p123456 cw-elevator-application -N -e \
"SELECT TABLE_NAME, IFNULL(TABLE_ROWS,0), ENGINE, IFNULL(TABLE_COMMENT,'') \
FROM INFORMATION_SCHEMA.TABLES \
WHERE TABLE_SCHEMA='cw-elevator-application' AND TABLE_TYPE='BASE TABLE' \
ORDER BY TABLE_NAME;" > docs/superpowers/data/cw-elevator-application/tables.tsv
```
- [ ] **Step 2: 导出全部列定义**
```bash
mysql -h 192.168.3.12 -P 3307 -u root -p123456 cw-elevator-application -N -e \
"SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE, IFNULL(COLUMN_DEFAULT,'NULL'), COLUMN_KEY, EXTRA, IFNULL(COLUMN_COMMENT,'') \
FROM INFORMATION_SCHEMA.COLUMNS \
WHERE TABLE_SCHEMA='cw-elevator-application' AND TABLE_NAME NOT LIKE 'QRTZ_%' AND TABLE_NAME NOT LIKE 'quartz_%' \
ORDER BY TABLE_NAME, ORDINAL_POSITION;" > docs/superpowers/data/cw-elevator-application/columns.tsv
```
- [ ] **Step 3: 导出索引**
```bash
mysql -h 192.168.3.12 -P 3307 -u root -p123456 cw-elevator-application -N -e \
"SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, NON_UNIQUE, SEQ_IN_INDEX \
FROM INFORMATION_SCHEMA.STATISTICS \
WHERE TABLE_SCHEMA='cw-elevator-application' \
ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX;" > docs/superpowers/data/cw-elevator-application/indexes.tsv
```
- [ ] **Step 4: 逐表采集样本(每表 3 行)**
```bash
while IFS=$'\t' read -r table_name rest; do
if [[ "$table_name" != QRTZ_* ]] && [[ "$table_name" != quartz_* ]]; then
echo "--- Extracting: $table_name ---"
mysql -h 192.168.3.12 -P 3307 -u root -p123456 cw-elevator-application -t -e \
"SELECT * FROM \`$table_name\` ORDER BY 1 DESC LIMIT 3;" \
> "docs/superpowers/data/cw-elevator-application/samples/${table_name}.txt" 2>&1
fi
done < <(cut -f1 docs/superpowers/data/cw-elevator-application/tables.tsv)
```
- [ ] **Step 5: 提交中间产物**
```bash
git add docs/superpowers/data/cw-elevator-application/
git commit -m "data: add cw-elevator-application schema raw dump and samples"
```
---
### Task 3: 扫描所有 MyBatis Mapper XML 提取表/列/JOIN
**Files:**
- Create: `docs/superpowers/data/mapper_tables.tsv`
- [ ] **Step 1: 枚举所有 Mapper XML 文件**
```bash
find ./maven-cw-elevator-application ./maven-cwos-resource ./maven-ninca-crk ./maven-ninca-qk-alarm \
-name "*Mapper.xml" -path "*/src/main/*" 2>/dev/null | sort \
> docs/superpowers/data/mapper_files.txt
wc -l docs/superpowers/data/mapper_files.txt
```
期望输出: ~80+ 个 Mapper XML 文件。
- [ ] **Step 2: 从 Mapper XML 提取 INSERT/UPDATE/FROM 表名**
```bash
# 提取 INSERT INTO / UPDATE / FROM / JOIN 后的表名
> docs/superpowers/data/mapper_tables.tsv
while IFS= read -r xmlfile; do
module=$(echo "$xmlfile" | cut -d'/' -f2)
tablenames=$(grep -oP '(INSERT\s+INTO\s+|UPDATE\s+|FROM\s+|JOIN\s+)\s*\`?\K[a-z_][a-z0-9_]*' "$xmlfile" 2>/dev/null | sort -u | tr '\n' ',')
echo -e "$module\t$xmlfile\t$tablenames"
done < docs/superpowers/data/mapper_files.txt > docs/superpowers/data/mapper_tables.tsv
```
- [ ] **Step 3: 提取 resultMap 列映射**
对每个 Mapper XML,读取 `<resultMap>` 中的 `<result column="..." property="...">` 提取列名。用下面脚本:
```bash
> docs/superpowers/data/mapper_columns.tsv
while IFS= read -r xmlfile; do
cols=$(grep -oP '<result\s+column="\K[^"]+' "$xmlfile" 2>/dev/null | sort -u | tr '\n' ',')
ids=$(grep -oP '<id\s+column="\K[^"]+' "$xmlfile" 2>/dev/null | sort -u | tr '\n' ',')
echo -e "${xmlfile}\t${ids}|${cols}"
done < docs/superpowers/data/mapper_files.txt > docs/superpowers/data/mapper_columns.tsv
```
- [ ] **Step 4: 提取 JOIN 关系**
```bash
> docs/superpowers/data/mapper_joins.tsv
while IFS= read -r xmlfile; do
joins=$(grep -oP '(LEFT\s+|RIGHT\s+|INNER\s+)?JOIN\s+\`?\K[a-z_][a-z0-9_]*\s+ON\s+\K[^;]+' "$xmlfile" 2>/dev/null | tr '\n' '|')
if [ -n "$joins" ]; then
echo -e "${xmlfile}\t${joins}"
fi
done < docs/superpowers/data/mapper_files.txt > docs/superpowers/data/mapper_joins.tsv
```
- [ ] **Step 5: 扫描 ShardingSphere 分表配置**
```bash
grep -r "actual-data-nodes\|sharding-column\|table-strategy" \
--include="*.properties" --include="*.yml" --include="*.yaml" \
./maven-cw-elevator-application/ ./maven-ninca-crk/ 2>/dev/null \
> docs/superpowers/data/sharding_config.txt
```
- [ ] **Step 6: 提交**
```bash
git add docs/superpowers/data/mapper_*.tsv docs/superpowers/data/mapper_files.txt docs/superpowers/data/sharding_config.txt
git commit -m "data: add MyBatis mapper table/column/join extraction"
```
---
### Task 4: 代码推导不可达库的表结构
**Files:**
- Create: `docs/superpowers/data/ninca-crk-std/mapper_tables.tsv`
- Create: `docs/superpowers/data/alarm-deploy/mapper_tables.tsv`
- Create: `docs/superpowers/data/cwos-resource/mapper_tables.tsv`
**ninca_crk_std****alarm_deploy** 的 MyBatis mapper 路径在 `application.properties` 中声明:
- ninca-crk: `classpath*:cn/cloudwalk/data/**/mysql/*.xml,classpath*:cn/cloudwalk/task/data/**/mysql/*.xml`
- alarm: `classpath:cn/cloudwalk/data/**/mysql/*.xml`
- cwos-resource: Mapper XML 已在 Task 3 中扫描
- [ ] **Step 1: 从 ninca-crk 的 Mapper XML 提取表信息**
```bash
# ninca-crk 的 Mapper XML 位于 src/main/java 下(非 resources/mapper
find ./maven-ninca-crk -name "*.xml" -path "*/mysql/*" 2>/dev/null | sort > docs/superpowers/data/ninca-crk-std/mapper_files.txt
while IFS= read -r xmlfile; do
tablenames=$(grep -oP '(INSERT\s+INTO\s+|UPDATE\s+|FROM\s+|JOIN\s+)\s*\`?\K[a-z_][a-z0-9_]*' "$xmlfile" 2>/dev/null | sort -u | tr '\n' ',')
echo -e "ninca-crk-std\t$xmlfile\t$tablenames"
done < docs/superpowers/data/ninca-crk-std/mapper_files.txt > docs/superpowers/data/ninca-crk-std/mapper_tables.tsv
```
- [ ] **Step 2: 从 alarm 的 Mapper XML 提取表信息**
```bash
find ./maven-ninca-qk-alarm -name "*.xml" -path "*/mysql/*" 2>/dev/null | sort > docs/superpowers/data/alarm-deploy/mapper_files.txt
while IFS= read -r xmlfile; do
tablenames=$(grep -oP '(INSERT\s+INTO\s+|UPDATE\s+|FROM\s+|JOIN\s+)\s*\`?\K[a-z_][a-z0-9_]*' "$xmlfile" 2>/dev/null | sort -u | tr '\n' ',')
echo -e "alarm-deploy\t$xmlfile\t$tablenames"
done < docs/superpowers/data/alarm-deploy/mapper_files.txt > docs/superpowers/data/alarm-deploy/mapper_tables.tsv
```
- [ ] **Step 3: 从 cwos-resource 的 Mapper XML 提取表信息**
```bash
# cwos-resource 有 db2/mysql/oracle 三个方言目录,以 mysql/ 为准
find ./maven-cwos-resource -name "*.xml" -path "*/mysql/*" 2>/dev/null | sort > docs/superpowers/data/cwos-resource/mapper_files.txt
while IFS= read -r xmlfile; do
tablenames=$(grep -oP '(INSERT\s+INTO\s+|UPDATE\s+|FROM\s+|JOIN\s+)\s*\`?\K[a-z_][a-z0-9_]*' "$xmlfile" 2>/dev/null | sort -u | tr '\n' ',')
echo -e "cwos-resource\t$xmlfile\t$tablenames"
done < docs/superpowers/data/cwos-resource/mapper_files.txt > docs/superpowers/data/cwos-resource/mapper_tables.tsv
```
- [ ] **Step 4: 提交**
```bash
git add docs/superpowers/data/ninca-crk-std/ docs/superpowers/data/alarm-deploy/ docs/superpowers/data/cwos-resource/
git commit -m "data: add code-derived table info for unreachable databases"
```
---
### Task 5: 交叉验证与生成最终 Markdown 文档
**Files:**
- Create: `docs/superpowers/specs/2026-05-01-database-schema-reference.md`(最终产物)
此任务通过 subagent 执行——将 Task 1-4 采集的原始数据 + 现有架构文档 + Mapper 扫描结果汇总为最终手册。
- [ ] **Step 1: 交付 subagent 生成文档**
委托 `deep` agent,传入以下 prompt
```
TASK: 根据以下原始数据生成数据库表结构参考手册 Markdown 文件。
EXPECTED OUTCOME:
- 文件写入 docs/superpowers/specs/2026-05-01-database-schema-reference.md
- 包含 9 个章节(见设计说明 §2)
- 每库有 Mermaid erDiagram ER 图
- 每表有列清单(列名、类型、可空、键、注释)
- 可连库的表有脱敏样本数据(Markdown 表格,≤3 行)
- 跨库关系总图
- 代码-表映射索引
REQUIRED TOOLS: Read, Write, Bash
MUST DO:
1. 读取设计说明: docs/superpowers/specs/2026-05-01-database-schema-reference-design.md
2. 读取现有架构文档: docs/architecture/租户组织人员访客-数据模型与用例.md
3. 读取 Task 1-2 的 schema 原始数据: docs/superpowers/data/component-organization/ 和 cw-elevator-application/ 下的 tables.tsv, columns.tsv, indexes.tsv
4. 读取 Task 1-2 的样本数据: samples/ 目录下各 .txt 文件
5. 读取 Task 3-4 的 Mapper 扫描结果: docs/superpowers/data/mapper_tables.tsv, mapper_columns.tsv, mapper_joins.tsv
6. 对每张表应用脱敏规则(设计说明 §5.3):姓名截断、手机号截断、IP替换、密码列跳过、时间戳转换
7. 按设计说明 §4 规范绘制 Mermaid ER 图:业务关键列、三种关系线(约束/JOIN/跨库)、subgraph 分包
8. 交叉验证:列出「库中有但代码无 Mapper」的表(标注为运维/外部表),对比 DDL 与实际列
9. 对不可达库(ninca-crk-std, alarm-deploy, cwos-resource)显式标注「⚠️ 未连接生产库,从代码推导」
10. Mermaid 语法必须可渲染(erDiagram 关键字、正确的 {} 语法、||--o{ 关系线)
11. 文档中不出现 "TBD"、"TODO"、"待补充"
MUST NOT DO:
- 不要编造未在原始数据中出现的表名或列名
- 不要对不可达库声称有样本数据
- 不要包含系统表(QRTZ_*, quartz_*
- 不要包含未脱敏的姓名/手机号/IP
CONTEXT:
- 仓库路径: /media/zebra/9e8fa357-7db6-4d70-88ed-d5de5a059a663/星河湾星中星/源码
- 数据库环境: 192.168.3.12:3307 (MySQL), user=root, password=123456
- 数据库: component-organization, cw-elevator-application (直连); ninca_crk_std, alarm_deploy, cwos_resource (代码推导)
- 现有架构文档已有 organization 库 5 表 + elevator 库 2 表的 ER 模型,以此为起点扩展
- MyBatis Mapper XML 路径已在 mapper_files.txt 中列出
```
- [ ] **Step 2: 验证文档完整性**
```bash
# 检查文档存在且非空
wc -l docs/superpowers/specs/2026-05-01-database-schema-reference.md
# 检查 Mermaid 块数量(应有 ≥5 个 erDiagram 块)
grep -c 'erDiagram' docs/superpowers/specs/2026-05-01-database-schema-reference.md
# 检查无占位符
! grep -n 'TBD\|TODO\|待补充' docs/superpowers/specs/2026-05-01-database-schema-reference.md
echo "Exit: $?"
```
期望: 文件 ≥ 500 行,≥ 5 个 erDiagram 块,无占位符匹配(exit 0)。
- [ ] **Step 3: 提交**
```bash
git add docs/superpowers/specs/2026-05-01-database-schema-reference.md
git commit -m "docs: add full database schema reference manual with ER diagrams and sample data"
```
---
### Task 6: 收尾清理
- [ ] **Step 1: 取消中间数据跟踪(可选)**
若中间数据不应入库,更新 `.gitignore`
```bash
# 如需排除原始数据
echo "docs/superpowers/data/" >> .gitignore
```
- [ ] **Step 2: 最终提交**
```bash
git add -A
git status
git commit -m "chore: finalize database schema reference delivery"
```
---
## 备选路径
### 若 192.168.3.12 不可达
跳过 Task 1 和 Task 2。所有表结构从代码 Mapper XML 推导,标注「⚠️ 数据库不可达,全部信息从代码推导」。在 Task 5 的 subagent prompt 中移除「读取样本数据」步骤。
### 若某个库的 Mapper XML 为空
对应章节仅输出「该模块未发现 MyBatis Mapper XML,无法从代码推导表结构」,不生成 ER 图。
---
## 完成检查清单
- [ ] 产物文件 `docs/superpowers/specs/2026-05-01-database-schema-reference.md` 存在且 ≥ 500 行
- [ ] 5 个数据库各有独立章节 + ER 图
- [ ] 跨库关系总图存在
- [ ] 代码-表映射索引覆盖全部 Mapper XML
- [ ] 可连库的每张表有 1-3 行脱敏样本
- [ ]`TBD` / `TODO` / `待补充`
- [ ] 所有 Mermaid 块语法正确可渲染
- [ ] 脱敏规则已应用(姓名截断、手机号截断等)
@@ -0,0 +1,647 @@
# 租户访客楼层策略 org_id 粒度修复 — 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:**`tenant_visitor_floor_policy` 的策略键从 `business_id` 改为 `org_id`,实现二选一语义(有策略用 allow,无策略用 floorList),修复 F1/F2/W2 问题。
**Architecture:** DDL 先上线(加列+改约束,不影响行为)→ 代码切换(Mapper/DAO/Service 三层的 business_id → org_id + 二选一逻辑)→ 数据迁移(运维 SQL 填 org_id)。整体改动控制在 7 个文件内,最小风险。
**Tech Stack:** Java 8, Spring Boot, MyBatis, MySQL 5.7
**Spec:** `docs/superpowers/specs/2026-05-01-org-id-policy-fix-design.md`
---
## 前置条件
- [ ] **Step 0: 确认分支与编译环境**
```bash
git checkout -b fix/org-id-policy-granularity
cd maven-cw-elevator-application && mvn formatter:validate -Dformatter-maven-plugin.version=2.16.0
```
期望: formatter 校验通过。
---
### Task 1: DDL — 策略表结构变更
**Files:**
- Create: `docs/sql/tenant_visitor_floor_policy_v2.sql`
- [ ] **Step 1: 编写 DDL 脚本**
```sql
-- 租户访客楼层策略:org_id 粒度修复
-- 执行顺序:先 DDL → 数据迁移(Task 5)→ 发应用包
-- 回滚:DROP INDEX uk_org_building, DROP COLUMN org_id, ADD UNIQUE KEY uk_biz_building (business_id, building_id)
USE `cw-elevator-application`;
-- 1. 新增 org_id 列
ALTER TABLE tenant_visitor_floor_policy
ADD COLUMN org_id VARCHAR(32) NULL COMMENT '组织节点ID(cw_is_organization.ID)'
AFTER business_id;
-- 2. 替换唯一约束(business_id → org_id
ALTER TABLE tenant_visitor_floor_policy
DROP INDEX uk_biz_building,
ADD UNIQUE KEY uk_org_building (org_id, building_id);
-- 3. 标记 business_id 为废弃
ALTER TABLE tenant_visitor_floor_policy
MODIFY COLUMN business_id VARCHAR(64) NULL COMMENT 'DEPRECATED: 已废弃,以 org_id 为准';
-- 验证
SELECT COLUMN_NAME, COLUMN_KEY, COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'cw-elevator-application'
AND TABLE_NAME = 'tenant_visitor_floor_policy'
ORDER BY ORDINAL_POSITION;
```
- [ ] **Step 2: 在开发库执行 DDL**
```bash
mysql -h 192.168.3.12 -P 3307 -u root -p123456 cw-elevator-application < docs/sql/tenant_visitor_floor_policy_v2.sql
```
期望: 无错误,`org_id` 列存在,`uk_org_building` 索引存在,`uk_biz_building` 已删除。
- [ ] **Step 3: 提交**
```bash
git add docs/sql/tenant_visitor_floor_policy_v2.sql
git commit -m "feat: add org_id column and uk_org_building constraint to tenant_visitor_floor_policy"
```
---
### Task 2: DTO — 新增 orgId 字段
**Files:**
- Modify: `maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/dto/TenantVisitorFloorPolicyDto.java`
- [ ] **Step 1: 添加 orgId 字段 + getter/setter**
`businessId` 的 setter 之后插入:
```java
// 新增字段
private String orgId;
public String getOrgId() {
return orgId;
}
public void setOrgId(String orgId) {
this.orgId = orgId;
}
```
> 注意:`businessId` 字段保留不删,兼容旧序列化。
- [ ] **Step 2: 验证编译**
```bash
cd maven-cw-elevator-application && mvn compile -pl cw-elevator-application-data -am -DskipTests
```
期望: BUILD SUCCESS。
- [ ] **Step 3: 提交**
```bash
git add maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/dto/TenantVisitorFloorPolicyDto.java
git commit -m "feat: add orgId field to TenantVisitorFloorPolicyDto"
```
---
### Task 3: Mapper — SQL 切换 business_id → org_id
**Files:**
- Modify: `maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/mapper/TenantVisitorFloorPolicyMapper.xml`
- Modify: `maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/mapper/TenantVisitorFloorPolicyMapper.java`
- [ ] **Step 1: 修改 Mapper XML — WHERE 条件 + 映射**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.cloudwalk.elevator.person.mapper.TenantVisitorFloorPolicyMapper">
<select id="selectEnabledByOrgId" resultType="cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto">
SELECT id,
org_id AS orgId,
policy_type AS policyType,
allow_zone_ids AS allowZoneIds,
building_id AS buildingId,
enabled AS enabled,
policy_version AS policyVersion
FROM tenant_visitor_floor_policy
WHERE org_id = #{orgId,jdbcType=VARCHAR}
AND enabled = 1
AND policy_type = 'INTERSECT_ALLOWLIST'
AND (building_id IS NULL OR building_id = '')
ORDER BY updated_at DESC, policy_version DESC
LIMIT 1
</select>
<!-- 旧方法保留作历史参考(可选删除)
<select id="selectEnabledTenantDefault" resultType="...">
... business_id ...
</select>
-->
</mapper>
```
- [ ] **Step 2: 修改 Mapper 接口**
```java
package cn.cloudwalk.elevator.person.mapper;
import cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto;
import org.apache.ibatis.annotations.Param;
public interface TenantVisitorFloorPolicyMapper {
/**
* 按组织节点 ID 查询启用中的 INTERSECT_ALLOWLIST 策略(building_id 为空)。
*/
TenantVisitorFloorPolicyDto selectEnabledByOrgId(@Param("orgId") String orgId);
// 旧方法(废弃,保留以兼容编译)
// TenantVisitorFloorPolicyDto selectEnabledTenantDefault(@Param("businessId") String businessId);
}
```
- [ ] **Step 3: 验证编译**
```bash
cd maven-cw-elevator-application && mvn compile -pl cw-elevator-application-data -am -DskipTests
```
期望: BUILD SUCCESS。
- [ ] **Step 4: 提交**
```bash
git add maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/mapper/TenantVisitorFloorPolicyMapper.xml
git add maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/mapper/TenantVisitorFloorPolicyMapper.java
git commit -m "feat: change policy query from business_id to org_id in TenantVisitorFloorPolicyMapper"
```
---
### Task 4: DAO — 接口与实现切换
**Files:**
- Modify: `maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/dao/TenantVisitorFloorPolicyDao.java`
- Modify: `maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/impl/TenantVisitorFloorPolicyDaoImpl.java`
- [ ] **Step 1: 修改 DAO 接口**
```java
package cn.cloudwalk.elevator.person.dao;
import cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto;
public interface TenantVisitorFloorPolicyDao {
/**
* 按组织节点 ID 查询启用中的 INTERSECT_ALLOWLIST 策略(building_id 为空)。
*
* @param orgId 组织节点 IDcw_is_organization.ID
* @return 无配置时 null
*/
TenantVisitorFloorPolicyDto selectEnabledByOrgId(String orgId);
// 旧方法(废弃)
// TenantVisitorFloorPolicyDto selectEnabledTenantDefault(String businessId);
}
```
- [ ] **Step 2: 修改 DAO 实现**
```java
package cn.cloudwalk.elevator.person.impl;
import cn.cloudwalk.elevator.person.dao.TenantVisitorFloorPolicyDao;
import cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto;
import cn.cloudwalk.elevator.person.mapper.TenantVisitorFloorPolicyMapper;
import javax.annotation.Resource;
import org.springframework.stereotype.Repository;
@Repository
public class TenantVisitorFloorPolicyDaoImpl implements TenantVisitorFloorPolicyDao {
@Resource
private TenantVisitorFloorPolicyMapper tenantVisitorFloorPolicyMapper;
@Override
public TenantVisitorFloorPolicyDto selectEnabledByOrgId(String orgId) {
return this.tenantVisitorFloorPolicyMapper.selectEnabledByOrgId(orgId);
}
}
```
- [ ] **Step 3: 验证编译**
```bash
cd maven-cw-elevator-application && mvn compile -pl cw-elevator-application-data -am -DskipTests
```
期望: BUILD SUCCESS。
- [ ] **Step 4: 提交**
```bash
git add maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/dao/TenantVisitorFloorPolicyDao.java
git add maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/impl/TenantVisitorFloorPolicyDaoImpl.java
git commit -m "feat: update DAO interface and impl to use org_id query"
```
---
### Task 5: Service — addVisitor 核心逻辑重写
**Files:**
- Modify: `maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/person/impl/PersonRuleServiceImpl.java`
这是改动最大的文件。分 3 个子步骤。
- [ ] **Step 1: 重写 addVisitor 方法(第 174-275 行)**
完整替换:
```java
@CloudwalkParamsValidate
public CloudwalkResult<Boolean> addVisitor(AcsPersonAddVisitorParam param, CloudwalkCallContext context)
throws ServiceException {
this.logger.info("根据被访人添加访客派梯权限开始,AcsPersonAddVisitorParam=[{}], CloudwalkCallContext=[{}]",
JSONObject.toJSONString(param), JSONObject.toJSONString(context));
try {
// ===== Step 1: 获取被访人信息(UC-01/02 都需要) =====
PersonDetailParam detailParam = new PersonDetailParam();
detailParam.setId(param.getPersonId());
detailParam.setBusinessId(context.getCompany().getCompanyId());
CloudwalkResult<PersonResult> detail = this.personService.detail(detailParam, context);
if (detail == null || !detail.isSuccess()) {
String code = detail != null ? detail.getCode() : "76260531";
String msg = detail != null ? detail.getMessage() : getMessage("76260531");
return CloudwalkResult.fail(code, msg);
}
PersonResult personResult = (PersonResult) detail.getData();
if (personResult == null) {
return CloudwalkResult.fail("76260531", getMessage("76260531"));
}
List<String> hostFloors = personResult.getFloorList();
if (CollectionUtils.isEmpty(hostFloors)) {
return CloudwalkResult.fail("76260531", getMessage("76260531"));
}
// ===== Step 2: 按 org_id 查找策略 =====
TenantVisitorFloorPolicyDto policy = findPolicyByOrgIds(personResult.getOrganizationIds());
// ===== Step 3: 确定生效楼层(二选一,不求交) =====
List<String> effectiveFloors;
boolean callerProvidedFloors = !CollectionUtils.isEmpty(param.getFloorIds());
if (policy != null) {
// 有策略:直接用 allow,忽略调用方 floorIds
effectiveFloors = resolveEffectiveFloors(
callerProvidedFloors ? param.getFloorIds() : hostFloors,
hostFloors, policy, param.getPersonId());
} else {
// 无策略:用调用方 floorIds 或 hostFloors
effectiveFloors = callerProvidedFloors ? param.getFloorIds() : hostFloors;
if (callerProvidedFloors) {
// UC-02 软校验:记录不在 hostFloors 中的楼层
Set<String> hostSet = new HashSet<>(hostFloors);
List<String> outliers = param.getFloorIds().stream()
.filter(f -> !hostSet.contains(f))
.collect(Collectors.toList());
if (!outliers.isEmpty()) {
this.logger.warn("UC-02 传入非被访人授权楼层 businessId={} personId={} outliers={}",
context.getCompany().getCompanyId(), param.getPersonId(), outliers);
}
}
}
if (CollectionUtils.isEmpty(effectiveFloors)) {
return CloudwalkResult.fail("76260531", getMessage("76260531"));
}
param.setFloorIds(effectiveFloors);
// ===== Step 4: 落库(不变) =====
ZoneQueryParam zoneQueryParam = new ZoneQueryParam();
zoneQueryParam.setId(param.getFloorIds().get(0));
zoneQueryParam.setRowsOfPage(10);
zoneQueryParam.setCurrentPage(1);
CloudwalkResult<CloudwalkPageAble<ZoneResult>> zonePage = this.zoneService.page(zoneQueryParam, context);
List<ZoneResult> zoneResults = (List<ZoneResult>) ((CloudwalkPageAble) zonePage.getData()).getDatas();
String imageStoreId =
this.deviceImageStoreDao.getByBuildingId(((ZoneResult) zoneResults.get(0)).getParentId());
List<ImageRuleRefAddDto> insertList = new ArrayList<>();
for (String floorId : param.getFloorIds()) {
ImageRuleRefResultDto defaultRule = this.imageRuleRefDao.getDefaultByZoneId(floorId);
ImageRuleRefAddDto addDto = new ImageRuleRefAddDto();
addDto.setId(genUUID());
addDto.setBusinessId(context.getCompany().getCompanyId());
addDto.setPersonId(param.getVisitorId());
addDto.setParentRule(defaultRule.getId());
addDto.setName(defaultRule.getName());
addDto.setZoneId(defaultRule.getZoneId());
addDto.setZoneName(defaultRule.getZoneName());
addDto.setCreateTime(Long.valueOf(System.currentTimeMillis()));
addDto.setLastUpdateTime(Long.valueOf(System.currentTimeMillis()));
addDto.setPersonDelete(Integer.valueOf(0));
insertList.add(addDto);
}
this.logger.info("访客添加派梯权限开始,数据为=[{}]", JSONObject.toJSONString(insertList));
if (!CollectionUtils.isEmpty(insertList)) {
this.imageRuleRefDao.insertList(insertList);
}
ImageStorePersonBindParam imageStorePersonBindParam = new ImageStorePersonBindParam();
imageStorePersonBindParam.setImageStoreId(imageStoreId);
imageStorePersonBindParam.setPersonIds(Collections.singletonList(param.getVisitorId()));
imageStorePersonBindParam.setNullDateIsLongTerm(Boolean.valueOf(true));
imageStorePersonBindParam.setExpiryBeginDate(param.getBegVisitorTime());
imageStorePersonBindParam.setExpiryEndDate(param.getEndVisitorTime());
this.logger.info("远程调用绑定人员图库开始,imageStorePersonBindParam=[{}], CloudwalkCallContext=[{}]",
JSONObject.toJSONString(imageStorePersonBindParam), JSONObject.toJSONString(context));
CloudwalkResult<ImgStoreBatchBindPersonResult> bindResult =
this.imageStorePersonService.batchBind(imageStorePersonBindParam, context);
if (!bindResult.isSuccess()) {
this.logger.error("远程调用绑定人员图库异常,原因:[{}],失败人员id:[{}]", bindResult.getMessage(), param.getVisitorId());
return CloudwalkResult.fail(bindResult.getCode(), bindResult.getMessage());
}
UpdateGroupPersonRefParam refParam = new UpdateGroupPersonRefParam();
refParam.setBusinessId(context.getCompany().getCompanyId());
refParam.setPersonIds(Collections.singletonList(param.getVisitorId()));
refParam.setImageStoreId(imageStoreId);
this.imageStorePersonService.updateGroupPersonRef(refParam, context);
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
this.logger.error("根据被访人添加访客派梯权限失败,原因:[{}]", e);
throw new ServiceException("76260530", getMessage("76260530"));
}
return CloudwalkResult.success(Boolean.valueOf(true));
}
```
- [ ] **Step 2: 添加两个新辅助方法 + 修改 W2(JSON 日志升级)**
`addVisitor` 方法之后插入:
```java
/**
* 按 org_id 查找策略,遍历 organizationIds 取第一个命中。
*/
private TenantVisitorFloorPolicyDto findPolicyByOrgIds(List<String> orgIds) {
if (CollectionUtils.isEmpty(orgIds)) return null;
for (String orgId : orgIds) {
TenantVisitorFloorPolicyDto p = this.tenantVisitorFloorPolicyDao.selectEnabledByOrgId(orgId);
if (p != null && p.getEnabled() != null && p.getEnabled().intValue() == 1) {
List<String> allow = parseAllowZoneIds(p.getAllowZoneIds());
if (!CollectionUtils.isEmpty(allow)) return p;
}
}
return null;
}
/**
* 二选一:用 allow 替换 fallbackFloors。
* 约束:allow 必须是 hostFloors 的子集,否则拒绝(76260533)。
*/
private List<String> resolveEffectiveFloors(
List<String> fallbackFloorsUnused, List<String> hostFloors,
TenantVisitorFloorPolicyDto policy, String personId) {
List<String> allow = parseAllowZoneIds(policy.getAllowZoneIds());
if (CollectionUtils.isEmpty(allow)) return fallbackFloorsUnused;
// 安全校验:allow 中每个值必须在 hostFloors 中存在
Set<String> hostSet = new HashSet<>(hostFloors);
List<String> unknownAllow = allow.stream()
.filter(a -> !hostSet.contains(a))
.collect(Collectors.toList());
if (!unknownAllow.isEmpty()) {
this.logger.error("策略配置错误:allow 包含不在被访人 floorList 中的 zoneId"
+ "orgId={} policyId={} personId={} unknownAllow={} hostFloors={}",
policy.getOrgId(), policy.getId(), personId, unknownAllow, hostFloors);
throw new ServiceException("76260533",
"策略配置了被访人无权访问的楼层,请联系管理员");
}
this.logger.info("策略生效 orgId={} policyId={} v={} allowSize={} hostSize={}",
policy.getOrgId(), policy.getId(), policy.getPolicyVersion(),
allow.size(), hostFloors.size());
return allow;
}
```
同时修改 `parseAllowZoneIds` 的 catch 块(W2 修复):
```java
// 旧代码:
// this.logger.warn("allow_zone_ids JSON 无效,按无策略处理: {}", e.getMessage());
// 新代码:
this.logger.error("allow_zone_ids JSON 无效,策略失效!policyId={} raw={}",
"policy.id", json, e); // 注意:此处无法获取 policy.id,改用实际可用字段
```
> 实际实现时,`parseAllowZoneIds` 不持有 `policyId`,可以在 `resolveEffectiveFloors` 中调用 `parseAllowZoneIds` 之前先做 null 检查,将 ERROR 日志放在调用处:
```java
private List<String> resolveEffectiveFloors(...) {
String rawJson = policy.getAllowZoneIds();
List<String> allow = parseAllowZoneIds(rawJson);
if (CollectionUtils.isEmpty(allow)) {
if (!StringUtils.isBlank(rawJson)) {
this.logger.error("allow_zone_ids JSON 无效或为空,策略失效!orgId={} policyId={} raw={}",
policy.getOrgId(), policy.getId(), rawJson);
}
return fallbackFloorsUnused;
}
// ... 后续校验
}
```
- [ ] **Step 3: 删除旧辅助方法 `intersectPreserveHostOrder`(不再需要)**
该方法已被 `resolveEffectiveFloors` 替代,可删除或保留(无调用方即可)。
- [ ] **Step 4: 验证编译**
```bash
cd maven-cw-elevator-application && mvn compile -DskipTests
```
期望: BUILD SUCCESS。
- [ ] **Step 5: 提交**
```bash
git add maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/person/impl/PersonRuleServiceImpl.java
git commit -m "feat: rewrite addVisitor with org_id policy lookup and either-or semantics
- Replace business_id policy key with org_id from PersonResult.getOrganizationIds()
- Change from intersection (floorList ∩ allow) to either-or (policy? allow : floorList)
- Add resolveEffectiveFloors with allow ⊆ floorList safety check (76260533)
- UC-02 now also checks policy (policy takes precedence over caller floorIds)
- Upgrade JSON parse failure log from WARN to ERROR
- Remove unused intersectPreserveHostOrder method"
```
---
### Task 6: 错误码注册(76260533
**Files:**
- Check: `maven-cw-elevator-application/cw-elevator-application-starter/src/main/resources/access-control.properties`(或对应的 messages 资源文件)
- [ ] **Step 1: 查找错误码资源文件**
```bash
grep -rn "76260531\|76260532" --include="*.properties" --include="*.xml" maven-cw-elevator-application/
```
- [ ] **Step 2: 在对应的 messages 文件中新增**
```properties
76260533=策略配置了被访人无权访问的楼层,请联系管理员
```
- [ ] **Step 3: 提交**
```bash
git add <错误码资源文件路径>
git commit -m "feat: add error code 76260533 for policy-host floor mismatch"
```
---
### Task 7: 数据迁移 SQL
**Files:**
- Create: `docs/sql/tenant_visitor_floor_policy_migrate_org_id.sql`
- [ ] **Step 1: 编写迁移脚本**
```sql
-- 租户访客楼层策略:business_id → org_id 数据迁移
-- 前提:DDLTask 1)已执行
-- 执行方式:人工确认 org_id 对应关系后逐行执行
USE cw-elevator-application;
-- 1. 列出所有公司级组织节点(供确认)
-- 在 component-organization 库执行:
-- SELECT o.ID, o.NAME, o.PARENT_ID
-- FROM `component-organization`.cw_is_organization o
-- WHERE o.BUSINESS_ID = '2524639890ba4f2cba9ba1a4eeaa4015'
-- AND o.IS_DEL = 0
-- ORDER BY o.NAME;
-- 2. 为现有策略行填入 org_id(示例:广发基金)
-- 请先确认 NAME 匹配正确
UPDATE tenant_visitor_floor_policy
SET org_id = '<广发基金的 org_id>',
business_id = NULL -- 可选:标记 business_id 已废弃
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
-- 3. 为其他公司新增策略行(模板)
-- INSERT INTO tenant_visitor_floor_policy
-- (id, org_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, remark, created_at, updated_at)
-- VALUES
-- (REPLACE(UUID(),'-',''), '<公司 org_id>', 'INTERSECT_ALLOWLIST',
-- '["<zone_id>"]', NULL, 1, 1, '', UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000);
-- 4. 验证迁移结果
SELECT id, org_id, business_id, policy_type, allow_zone_ids, enabled
FROM tenant_visitor_floor_policy
ORDER BY org_id;
```
- [ ] **Step 2: 提交**
```bash
git add docs/sql/tenant_visitor_floor_policy_migrate_org_id.sql
git commit -m "docs: add org_id data migration SQL for tenant_visitor_floor_policy"
```
---
### Task 8: 构建验证 + 发布准备
- [ ] **Step 1: 全量构建**
```bash
cd maven-cw-elevator-application && mvn clean install -DskipTests
```
期望: BUILD SUCCESS,无编译错误。
- [ ] **Step 2: formatter 校验**
```bash
cd maven-cw-elevator-application && mvn formatter:validate -Dformatter-maven-plugin.version=2.16.0
```
期望: 无格式化违规。
- [ ] **Step 3: 生成发布包**
```bash
bash scripts/release-cw-elevator-application.sh 2.0.10
```
- [ ] **Step 4: 提交发布包**
```bash
git add releases/
git commit -m "release: cw-elevator-application v2.0.10 with org_id policy fix"
```
---
## 回滚方案
| 步骤 | 操作 |
|------|------|
| 1. 回滚应用包 | 部署旧版本 JAR(用 `business_id` 查询的代码) |
| 2. 回滚 DDL(可选) | `DROP INDEX uk_org_building; ALTER TABLE ... DROP COLUMN org_id; ADD UNIQUE KEY uk_biz_building (business_id, building_id);` |
| 3. 恢复数据(可选) | `UPDATE tenant_visitor_floor_policy SET business_id = '252463...' WHERE org_id IS NOT NULL;` |
> DDL 回滚不影响旧代码行为(旧代码不查 `org_id` 列)。
---
## 发布顺序(生产环境)
```
1. DDL 上线(Task 1) → 表结构变更,不影响线上行为
2. 数据迁移(Task 7) → 运维手工填 org_id
3. 发应用包(Task 8) → 代码切换到 org_id 查询
4. 验证(Task 8 后) → 抽样确认策略生效
```
---
## 完成检查清单
- [ ] DDL 在开发库执行成功
- [ ] `TenantVisitorFloorPolicyDto``orgId` 字段
- [ ] Mapper XML/Java 使用 `org_id` 查询
- [ ] DAO 接口/实现已切换
- [ ] `addVisitor` 使用 `findPolicyByOrgIds` + `resolveEffectiveFloors`
- [ ] W2 修复:JSON 解析失败打 ERROR 而非 WARN
- [ ] 错误码 76260533 已在资源文件注册
- [ ] 数据迁移 SQL 已编写
- [ ] `mvn clean install` 通过
- [ ] `mvn formatter:validate` 通过
@@ -0,0 +1,544 @@
# org_id 策略修复验证脚本 — 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 编写 Python 无鉴权验证脚本 `verify_org_policy_fix.py`,自动准备测试数据、执行 7 个用例、清理数据、输出 JSON 报告。
**Architecture:** 单脚本 4 个 PhasePhase0 健康检查 → Phase1 MySQL 准备数据 → Phase2 HTTP 逐用例调用 add/visitor + passRule/image → Phase3 MySQL 清理 → Phase4 输出 JSON。复用现有 `quick_verify_visitor_floor_policy.py` 的 noauth 调用模式。
**Tech Stack:** Python 3.8+, `requests`, `pymysql`, JSON
**Spec:** `docs/superpowers/specs/2026-05-01-org-policy-verify-design.md`
---
### Task 1: 脚本骨架与配置
**Files:**
- Create: `maven-cw-elevator-application/tools/visitor_floor_verification/scripts/verify_org_policy_fix.py`
- [ ] **Step 1: 写入脚本骨架**
```python
#!/usr/bin/env python3
"""org_id 策略修复 — 无鉴权验证脚本"""
import argparse
import json
import sys
import time
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
import pymysql
import requests
# ===== 配置常量 =====
DB_CONFIG = {
"host": "192.168.3.12",
"port": 3307,
"user": "root",
"password": "123456",
"db_org": "component-organization",
"db_elevator": "cw-elevator-application",
}
BUSINESS_ID = "2524639890ba4f2cba9ba1a4eeaa4015"
# 测试用组织节点
ORG_1403 = "72fb65ec5de94201b909a98b8bae1892"
ORG_1405 = "2095de3d541f44eba686c78fda68336f"
ORG_GUANGFA = "488b8ad049bb43408a6fbcc50bcb89ac"
# 被访人
HOST_CHEN = "1060601019894960128" # 陈国辉 (1403+星中心)
HOST_WANG = "1090779433129840640" # 王姣 (1405)
HOST_QIN = "1072908835884208128" # 秦夏 (广发基金)
# 访客(测试专用号段)
VISITOR_IDS = [
"9199000100000000001", "9199000100000000002", "9199000100000000003",
"9199000100000000004", "9199000100000000005", "9199000100000000006",
"9199000100000000007",
]
ZONE_28F = "605560545117995008"
ZONE_99F = "605560540000000000" # 不存在,用于 T3
OK_CODES = {"0", "200"}
TEST_CASES = [
{
"id": "T1", "name": "有策略→allow替换floorList",
"host_id": HOST_CHEN, "visitor_id": VISITOR_IDS[0],
"policy_id": "policy_t1_1403", "expected_pass": True,
"expected_floors": [ZONE_28F],
},
{
"id": "T2", "name": "无策略→floorList",
"host_id": HOST_WANG, "visitor_id": VISITOR_IDS[1],
"policy_id": None, "expected_pass": True,
"expected_floors": None, # 不做楼层精确比对,只验证成功
},
{
"id": "T3", "name": "allow含无效zone→拒绝",
"host_id": HOST_CHEN, "visitor_id": VISITOR_IDS[2],
"policy_id": "policy_t3_invalid", "expected_pass": False,
"expected_code": "76260533",
},
{
"id": "T4", "name": "多组织命中第一个策略",
"host_id": HOST_CHEN, "visitor_id": VISITOR_IDS[3],
"policy_id": "policy_t1_1403", "expected_pass": True,
"expected_floors": [ZONE_28F],
},
{
"id": "T5", "name": "enabled=0等同无策略",
"host_id": HOST_CHEN, "visitor_id": VISITOR_IDS[4],
"policy_id": "policy_t5_disabled", "expected_pass": True,
"expected_floors": None,
},
{
"id": "T6", "name": "UC-02策略优先",
"host_id": HOST_CHEN, "visitor_id": VISITOR_IDS[5],
"policy_id": "policy_t1_1403", "expected_pass": True,
"expected_floors": [ZONE_28F],
"floor_ids_override": ["605560541473144832"], # 传6F,策略应覆盖为28F
},
{
"id": "T7", "name": "广发基金迁移验证",
"host_id": HOST_QIN, "visitor_id": VISITOR_IDS[6],
"policy_id": "gf_vstr_policy_guangfa_fund_001x", "expected_pass": True,
"expected_floors": [ZONE_28F],
},
]
def parse_args():
p = argparse.ArgumentParser(description="org_id 策略修复验证")
p.add_argument("--elevator-base-url", default="http://127.0.0.1:18081")
p.add_argument("--skip-db", action="store_true", help="跳过数据库准备/清理")
return p.parse_args()
if __name__ == "__main__":
args = parse_args()
print(f"elevator: {args.elevator_base_url}")
print(f"skip-db: {args.skip_db}")
print(f"cases: {len(TEST_CASES)}")
```
- [ ] **Step 2: 验证导入可用**
```bash
cd maven-cw-elevator-application/tools/visitor_floor_verification
python3 -c "import requests; import pymysql; print('OK')"
```
期望: `OK`
- [ ] **Step 3: 提交**
```bash
git add maven-cw-elevator-application/tools/visitor_floor_verification/scripts/verify_org_policy_fix.py
git commit -m "test: scaffold verify_org_policy_fix.py with config and test cases"
```
---
### Task 2: Phase 0 — 健康检查 + Phase 1 — 数据准备/清理
**Files:**
- Modify: `verify_org_policy_fix.py` (追加函数)
- [ ] **Step 1: 添加健康检查函数**
```python
def health_check(base_url: str) -> bool:
"""GET /actuator/health"""
try:
r = requests.get(f"{base_url}/actuator/health", timeout=10)
ok = r.status_code == 200
print(f"[HEALTH] {base_url} -> {r.status_code} {'OK' if ok else 'FAIL'}")
return ok
except Exception as e:
print(f"[HEALTH] {base_url} -> ERROR: {e}")
return False
```
- [ ] **Step 2: 添加数据库连接函数**
```python
def get_db_conn():
return pymysql.connect(
host=DB_CONFIG["host"], port=DB_CONFIG["port"],
user=DB_CONFIG["user"], password=DB_CONFIG["password"],
database=DB_CONFIG["db_elevator"],
charset="utf8mb4", autocommit=True,
)
def execute_sql(sql: str, params=None):
conn = get_db_conn()
try:
with conn.cursor() as cur:
cur.execute(sql, params)
finally:
conn.close()
```
- [ ] **Step 3: 添加数据准备函数**
```python
def prepare_test_data():
"""INSERT 测试策略 + UPDATE 广发基金 org_id"""
policies = [
("policy_t1_1403", ORG_1403, f'["{ZONE_28F}"]', 1),
("policy_t3_invalid", ORG_1403, f'["{ZONE_28F}","{ZONE_99F}"]', 1),
("policy_t5_disabled", ORG_1403, f'["{ZONE_28F}"]', 0),
]
for pid, oid, zones_json, enabled in policies:
execute_sql("DELETE FROM tenant_visitor_floor_policy WHERE id=%s", (pid,))
execute_sql(
"INSERT INTO tenant_visitor_floor_policy "
"(id, org_id, business_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, created_at, updated_at) "
"VALUES (%s, %s, NULL, 'INTERSECT_ALLOWLIST', %s, NULL, %s, 1, UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000)",
(pid, oid, zones_json, enabled),
)
print(f" INSERT policy {pid} org={oid} enabled={enabled}")
# 广发基金迁移
execute_sql(
"UPDATE tenant_visitor_floor_policy SET org_id=%s WHERE id='gf_vstr_policy_guangfa_fund_001x'",
(ORG_GUANGFA,),
)
print(f" UPDATE 广发基金 org_id={ORG_GUANGFA}")
```
- [ ] **Step 4: 添加数据清理函数**
```python
def cleanup_test_data():
"""DELETE 测试策略 + 回滚广发基金 org_id"""
for pid in ["policy_t1_1403", "policy_t3_invalid", "policy_t5_disabled"]:
execute_sql("DELETE FROM tenant_visitor_floor_policy WHERE id=%s", (pid,))
print(f" DELETE {pid}")
execute_sql(
"UPDATE tenant_visitor_floor_policy SET org_id=NULL WHERE id='gf_vstr_policy_guangfa_fund_001x'"
)
print(" UPDATE 广发基金 org_id=NULL (回滚)")
```
- [ ] **Step 5: 测试 DB 函数**
```bash
python3 -c "
import verify_org_policy_fix as v
v.prepare_test_data()
print('prepared')
v.cleanup_test_data()
print('cleaned')
"
```
期望: 看到 INSERT/DELETE 输出,无异常。
- [ ] **Step 6: 提交**
```bash
git add verify_org_policy_fix.py
git commit -m "test: add Phase 0-1: health check + DB prepare/cleanup"
```
---
### Task 3: Phase 2 — noauth HTTP 调用 + 回读验证
**Files:**
- Modify: `verify_org_policy_fix.py` (追加函数)
- [ ] **Step 1: 添加 noauth 请求头构建**
```python
def build_noauth_headers() -> Dict[str, str]:
return {
"Content-Type": "application/json",
"businessid": BUSINESS_ID,
}
def now_ms() -> int:
return int(time.time() * 1000)
def tomorrow_ms() -> int:
return int((time.time() + 86400) * 1000)
```
- [ ] **Step 2: 添加 add_visitor 调用函数**
```python
def call_add_visitor(base_url: str, person_id: str, visitor_id: str,
floor_ids: Optional[List[str]] = None) -> Dict[str, Any]:
"""POST /elevator/person/add/visitor"""
body = {
"personId": person_id,
"visitorId": visitor_id,
"floorIds": floor_ids if floor_ids is not None else [],
"begVisitorTime": now_ms(),
"endVisitorTime": tomorrow_ms(),
}
try:
r = requests.post(
f"{base_url}/elevator/person/add/visitor",
json=body, headers=build_noauth_headers(), timeout=30,
)
return {
"http_status": r.status_code,
"body": r.json() if r.headers.get("content-type", "").startswith("application/json") else r.text,
}
except Exception as e:
return {"http_status": 0, "error": str(e)}
```
- [ ] **Step 3: 添加 passRule/image 回读函数**
```python
def call_passrule_image(base_url: str, visitor_id: str) -> Dict[str, Any]:
"""POST /elevator/passRule/image"""
body = {"personId": visitor_id}
try:
r = requests.post(
f"{base_url}/elevator/passRule/image",
json=body, headers=build_noauth_headers(), timeout=30,
)
return {
"http_status": r.status_code,
"body": r.json() if r.headers.get("content-type", "").startswith("application/json") else r.text,
}
except Exception as e:
return {"http_status": 0, "error": str(e)}
def extract_zone_ids(passrule_response: Dict) -> List[str]:
"""从 passRule/image 响应中提取 zoneId 列表"""
try:
datas = passrule_response["body"]["data"]["datas"]
return [d["zoneId"] for d in datas if "zoneId" in d]
except (KeyError, TypeError):
return []
```
- [ ] **Step 4: 测试 HTTP 调用(需 V2 运行中)**
```bash
python3 -c "
import verify_org_policy_fix as v
r = v.call_add_visitor('http://127.0.0.1:18081', '1060601019894960128', '9199000100000000001')
print(json.dumps(r, indent=2, ensure_ascii=False))
"
```
期望: HTTP 200,响应中包含 `success` 字段。
- [ ] **Step 5: 提交**
```bash
git add verify_org_policy_fix.py
git commit -m "test: add Phase 2: noauth HTTP calls + passRule/image extraction"
```
---
### Task 4: Phase 2 — 用例执行器 + Phase 3-4 — 报告
**Files:**
- Modify: `verify_org_policy_fix.py` (追加函数 + main)
- [ ] **Step 1: 添加用例执行函数**
```python
def run_case(base_url: str, case: Dict[str, Any]) -> Dict[str, Any]:
"""执行单个用例,返回结果 dict"""
cid = case["id"]
print(f"\n[{cid}] {case['name']}")
floor_ids = case.get("floor_ids_override")
# Step A: 确保正确的策略行生效
pid = case.get("policy_id")
if pid:
# T3: 需要切换到 policy_t3_invalid(先停用 policy_t1_1403
if cid == "T3":
execute_sql("DELETE FROM tenant_visitor_floor_policy WHERE id='policy_t1_1403'")
print(f" [DB] 临时删除 policy_t1_1403 以启用 T3 策略")
result = {"id": cid, "name": case["name"]}
# Step B: add/visitor
r = call_add_visitor(base_url, case["host_id"], case["visitor_id"], floor_ids)
result["add_visitor"] = {
"http_status": r.get("http_status"),
"success": r.get("body", {}).get("success") if isinstance(r.get("body"), dict) else None,
"code": r.get("body", {}).get("code") if isinstance(r.get("body"), dict) else None,
"message": r.get("body", {}).get("message") if isinstance(r.get("body"), dict) else None,
"error": r.get("error"),
}
av = result["add_visitor"]
business_ok = av["http_status"] == 200 and str(av.get("code", "")) in OK_CODES
# Step C: 判定
if case["expected_pass"]:
if business_ok:
# 回读楼层
pr = call_passrule_image(base_url, case["visitor_id"])
actual_zones = extract_zone_ids(pr)
result["passrule_image"] = {"zones": actual_zones}
expected = case.get("expected_floors")
if expected is not None:
match = set(actual_zones) == set(expected)
result["floor_match"] = match
result["passed"] = match
print(f" add/visitor OK, floors: actual={actual_zones} expected={expected} match={match}")
else:
result["passed"] = True
print(f" add/visitor OK, floors={actual_zones} (no strict check)")
else:
result["passed"] = False
print(f" expected success but got code={av.get('code')} msg={av.get('message')}")
else:
# 期望失败
expected_code = case.get("expected_code")
actual_code = str(av.get("code", ""))
result["passed"] = (not business_ok) and (actual_code == expected_code)
print(f" expected fail code={expected_code} actual={actual_code} passed={result['passed']}")
# Step D: 恢复策略(T3 执行后)
if cid == "T3":
execute_sql(
"INSERT INTO tenant_visitor_floor_policy "
"(id, org_id, business_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, created_at, updated_at) "
"VALUES ('policy_t1_1403', %s, NULL, 'INTERSECT_ALLOWLIST', %s, NULL, 1, 1, UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000)",
(ORG_1403, f'["{ZONE_28F}"]'),
)
print(f" [DB] 恢复 policy_t1_1403")
return result
```
- [ ] **Step 2: 添加报告生成函数**
```python
def generate_report(results: List[Dict], base_url: str) -> Dict:
passed = sum(1 for r in results if r.get("passed"))
failed = len(results) - passed
return {
"test": "org_id policy fix verification",
"timestamp": datetime.now().isoformat(),
"elevator_url": base_url,
"mode": "noauth-probe",
"business_id": BUSINESS_ID,
"summary": {"total": len(results), "passed": passed, "failed": failed},
"results": results,
}
```
- [ ] **Step 3: 完善 main 函数**
```python
if __name__ == "__main__":
args = parse_args()
base = args.elevator_base_url.rstrip("/")
# Phase 0
if not health_check(base):
print("FATAL: elevator service not reachable")
sys.exit(1)
# Phase 1
if not args.skip_db:
print("\n=== Phase 1: prepare test data ===")
prepare_test_data()
# Phase 2
print(f"\n=== Phase 2: run {len(TEST_CASES)} cases ===")
results = []
for case in TEST_CASES:
r = run_case(base, case)
results.append(r)
# Phase 3
if not args.skip_db:
print("\n=== Phase 3: cleanup ===")
cleanup_test_data()
# Phase 4
report = generate_report(results, base)
report_path = f"report/org-policy-fix-verify-{datetime.now().strftime('%Y%m%d-%H%M%S')}.json"
import os
os.makedirs("report", exist_ok=True)
with open(report_path, "w", encoding="utf-8") as f:
json.dump(report, f, indent=2, ensure_ascii=False)
print(f"\n=== Report: {report_path} ===")
print(f"Passed: {report['summary']['passed']}/{report['summary']['total']}")
for r in results:
status = "" if r.get("passed") else ""
print(f" {status} [{r['id']}] {r['name']}")
sys.exit(0 if report["summary"]["failed"] == 0 else 1)
```
- [ ] **Step 4: 提交**
```bash
git add verify_org_policy_fix.py
git commit -m "test: add Phase 2-4: case runner + report generation + main entry"
```
---
### Task 5: 端到端运行验证
- [ ] **Step 1: 确保 V2 运行中**
```bash
curl -s http://127.0.0.1:18081/actuator/health
```
期望: `{"status":"UP"}`
- [ ] **Step 2: 执行全量验证**
```bash
cd maven-cw-elevator-application/tools/visitor_floor_verification
python3 scripts/verify_org_policy_fix.py --elevator-base-url http://127.0.0.1:18081
```
- [ ] **Step 3: 检查报告**
```bash
ls -la report/org-policy-fix-verify-*.json | tail -1
python3 -c "import json; r=json.load(open('$(ls -t report/org-policy-fix-verify-*.json | head -1)')); print(f\"{r['summary']['passed']}/{r['summary']['total']} passed\")"
```
期望: `7/7 passed`
- [ ] **Step 4: 提交报告(可选,不提交 JSON 到 git)**
```bash
git status
```
---
## 完成检查清单
- [ ] `verify_org_policy_fix.py` 存在且可导入
- [ ] Phase 0: `health_check()` 返回 True
- [ ] Phase 1: `prepare_test_data()` 无异常
- [ ] Phase 2: 7 个用例全部执行
- [ ] Phase 3: `cleanup_test_data()` 无异常
- [ ] Phase 4: JSON 报告生成,7/7 passed
- [ ] 无脱敏泄露(报告中不出现真实姓名/手机号)
File diff suppressed because it is too large Load Diff