Files
starRiverProperty/docs/superpowers/walkthroughs/2026-05-09-visitor-floor-policy-analysis.md
T
hpd840321 42c4a9fd6b docs: mark elevator-side tenant policy SQL as deprecated, add guangfa visitor floor design
- Deprecate elevator-side tenant_visitor_floor_policy SQL files
  (V2 queries only component-organization library)
- Add guangfa 28F visitor floor design spec (table-driven approach A)
- Add complete database ER diagram (14 DBs, 537 tables)
- Add implementation plan for guangfa visitor floor policy
- Code walkthrough docs for visitor floor policy analysis
2026-05-09 23:56:12 +08:00

22 KiB

访客楼层策略完整走查 & 广发基金 20 层扩展评估

日期: 2026-05-09 状态: 走查评估完成 作者: OpenCode Agent


目录

  1. 架构总览
  2. 星河湾 40F/6F 硬编码逻辑走查
  3. 租户访客策略完整链路走查
  4. 广发基金当前 28F 策略分析
  5. 广发基金扩展至 20 层评估
  6. 变更点清单与风险矩阵
  7. 测试验证策略
  8. 附录: 关键文件索引

1. 架构总览

1.1 服务拓扑

访客登记页 (BFF/第三方)
    │
    │ POST /elevator/person/add/visitor
    ▼
┌──────────────────────────────┐
│ cw-elevator-application      │  PersonRuleServiceImpl#addVisitor()
│ (电梯应用)                    │  ┌─ 阶段1: personService.detail()
│                               │  ├─ 阶段2: UC-01/UC-02 分支
│                               │  ├─ 阶段3: 空集校验
│                               │  └─ 阶段4: 写入 image_rule_ref
├──────────────────────────────┤
│ PersonService.detail()       │  ──Feign──▶
└──────────────────────────────┘
                │
                ▼
┌──────────────────────────────┐
│ ninca-common-component-org   │  ImgPersonServiceImpl#detail()
│ (组织组件)                    │  ┌─ listByImageId() → 电梯 Feign
│                               │  ├─ ★ 策略替代 (replacementZoneIds)
│                               │  └─ 返回 PersonResult.floorList
│                               │
│   TenantVisitorFloorPolicyService
│   TenantVisitorFloorPolicyMapper
│   tenant_visitor_floor_policy 表
└──────────────────────────────┘

1.2 核心原则(已确立)

原则 说明
策略只在组织组件 tenant_visitor_floor_policy 表及策略逻辑仅在 ninca-common-component-organization
电梯侧只透传 PersonRuleServiceImpl#addVisitor() 不再读策略表,不做求交
替代(Replacement)语义 策略命中时 allow_zone_ids 整表替换 floorList,禁止与原始楼层求交
对外接口不变 PersonService.detailPersonResult.floorList 契约保持兼容
隔离键 = org_id 策略按组织节点 (org_id) 隔离,非 business_id

1.3 楼层数据流

detail() 路径 (UC-01 权威来源):
  ImgPersonServiceImpl#detail()
    → elevatorFeignClient.listByImageId()        # 获取被访人所有楼层
    → tenantVisitorFloorPolicyService.replacementZoneIds()  # 策略替代
    → PersonResult.floorList = allow_zone_ids     # 返回替代后的楼层

listByPage 路径 (列表展示):
  ImgPersonServiceImpl#listByPage(isVisitor)
    → P1: 策略命中 → 使用 allow_zone_ids (跳过 XHW 块)
    → P2: 无策略 → listByImageId + XHW 40F/6F 硬编码逻辑

addVisitor() 路径 (电梯派梯):
  PersonRuleServiceImpl#addVisitor()
    → UC-01 (无 floorIds): effective = personResult.floorList
    → UC-02 (有 floorIds): effective = param.floorIds
    → 写入 image_rule_ref (每条 floorId 一行)

2. 星河湾 40F/6F 硬编码逻辑走查

2.1 配置来源

文件: backend/ninca-common-component-organization/cwos-component-organization-starter/deploy/run-verify/application.properties

# 第 170-172 行
xhwId=21474e012cd14e26bc62771873b22562
xhwDefaultFloorId=605560547135455232    # = 40F
xhwSixFloorId=605560541473144832        # = 6F

2.2 Java 实现

文件: backend/ninca-common-component-organization/cwos-component-organization-service/src/main/java/cn/cloudwalk/service/organization/service/ImgPersonServiceImpl.java

注入点 (第 154-158 行):

@Value("${xhwId}")                  private String xhwId;
@Value("${xhwDefaultFloorId}")      private String xhwDefaultFloorId;
@Value("${xhwSixFloorId}")          private String xhwSixFloorId;

2.3 生效路径: listByPage 访客列表分支

关键事实: 40F/6F 硬编码逻辑 只存在于 listByPage,不存在于 detail()。这意味着:

  • 访客邀约页 / UC-01 派梯: 走 detail() → 不会触发 40F/6F 逻辑
  • 访客列表页: 走 listByPage(isVisitor) → 无策略命中时触发 40F/6F
// listByPage 第 326-370 行
if (policyZones.isPresent()) {
    // P1: 策略命中 → 使用 allow_zone_ids (跳过 XHW 块)
    imgStorePersonResult.setFloorInfoList(policyFloorInfo);
    imgStorePersonResult.setDefaultChooseFloor(policyZones.get().get(0));
} else {
    // P2: 无策略 → 走到 XHW 40F/6F
    List<OrgFloor> orgFloorList = orgFloorMapper.listByOrgIds(organizationIds);
    if (CollectionUtils.isEmpty(orgFloorList)) {
        imgStorePersonResult.setIsAcrossDay(1);   // 跨天
    } else {
        if (imgStorePersonResult.getOrganizationIds().contains(this.xhwId)) {
            // 物业公司 → 默认 40F
            imgStorePersonResult.setDefaultChooseFloor(this.xhwDefaultFloorId);
            // zoneName = "40F"
        } else {
            // 其他组织 → 默认 6F
            imgStorePersonResult.setDefaultChooseFloor(this.xhwSixFloorId);
            // zoneName = "6F"
        }
    }
}

2.4 40F/6F 判定逻辑总结

是否命中租户策略?
    ├── YES → 策略 allow_zone_ids 替代 (跳过 XHW 块)
    └── NO  → 查询 OrgFloor 表
                  ├── 无 OrgFloor 记录 → isAcrossDay=1 (跨天通行)
                  └── 有 OrgFloor 记录
                        ├── 组织 ID 含 xhwId → 默认 40F
                        └── 其他组织 → 默认 6F

2.5 影响范围

接口 是否涉及 40F/6F 说明
PersonService.detail 不涉及 detail 只做 listByImageId + 策略替代
listByPage(isVisitor) 涉及 无策略命中时展示 40F/6F
addVisitor 不涉及 只透传 floorList/floorIds

3. 租户访客策略完整链路走查

3.1 数据库表结构

: tenant_visitor_floor_policy (组织库 component-organization)

-- DDL 定义于: docs/sql/tenant_visitor_floor_policy_v2.sql
id              VARCHAR(64)   PRIMARY KEY
org_id          VARCHAR(64)   -- 组织节点 ID (隔离键)
business_id     VARCHAR(64)   -- 业务 ID
policy_type     VARCHAR(64)   -- 'INTERSECT_ALLOWLIST'
allow_zone_ids  TEXT          -- JSON 数组: ["zoneId1","zoneId2"]
building_id     VARCHAR(64)   -- NULL (全局)
enabled         TINYINT(1)    -- 0/1
policy_version  INT           -- 版本号 (ON UPDATE 自增)
remark          VARCHAR(512)
created_at      BIGINT
updated_at      BIGINT

UNIQUE KEY: uk_org_building (org_id, building_id)

3.2 策略查询链路

TenantVisitorFloorPolicyMapper.selectEnabledByOrgId(orgId)
    ↓ @Select
    SELECT * FROM tenant_visitor_floor_policy
    WHERE org_id = #{orgId}
      AND enabled = 1
      AND (building_id IS NULL OR building_id = '')
    LIMIT 1

TenantVisitorFloorPolicyService.findEnabledPolicyByOrgId(orgId)
    ↓ 返回 Optional<TenantVisitorFloorPolicy>

TenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive(orgIds)
    ↓ 遍历 orgIds, 找到第一个命中策略的
    ↓ 解析 allow_zone_ids JSON → List<String>
    ↓ 返回 Optional<List<String>> (非空列表)

ImgPersonServiceImpl#detail()
    ↓ replacementFloors.isPresent() ?
    ↓ YES: floorList = replacementFloors.get()  // 替代
    ↓ NO:  保留 listByImageId 原始结果

3.3 多组织匹配优先级

replacementZoneIdsIfPolicyActive(List<String> orgIds)orgIds 顺序依次尝试:

// TenantVisitorFloorPolicyService.java 第 70-80 行
for (String id : orgIds) {
    Optional<List<String>> zones = replacementZoneIdsIfPolicyActive(id);
    if (zones.isPresent()) {
        return zones;  // 第一个命中的策略作为结果
    }
}

意味着: 人员所属的第一个有策略的组织决定最终楼层

3.4 电梯侧 addVisitor() 流程

// PersonRuleServiceImpl.java 第 165-270 行
addVisitor() {
    // 阶段 1: 获取被访人 detail
    PersonResult personResult = personService.detail(detailParam);
    
    // 阶段 2: 确定生效楼层
    if (callerProvidedFloors) {
        effective = param.getFloorIds();          // UC-02: 调用方指定
    } else {
        effective = personResult.getFloorList();  // UC-01: 使用 detail 返回
    }
    
    // 阶段 3: 空集校验 → 76260531
    if (CollectionUtils.isEmpty(effective)) {
        return fail("76260531");
    }
    
    // 阶段 4: 写入 image_rule_ref
    for (String floorId : effective) {
        ImageRuleRefResultDto defaultRule = imageRuleRefDao.getDefaultByZoneId(floorId);
        // 插入 image_rule_ref 行
    }
}

3.5 错误码

错误码 含义 触发点
76260530 addVisitor 未捕获异常 PersonRuleServiceImpl:263
76260531 无可用楼层 PersonRuleServiceImpl:184, 197, 208
76260532 历史: 求交为空 (已废弃) V1 解编译版:207

4. 广发基金当前 28F 策略分析

4.1 当前策略配置

组织库种子数据 (docs/sql/organization_tenant_visitor_floor_policy_init_tenants.sql):

INSERT INTO tenant_visitor_floor_policy (...) VALUES (
  'gf_vstr_policy_guangfa_fund_001x',           -- id
  '488b8ad049bb43408a6fbcc50bcb89ac',           -- org_id (广发基金组织节点)
  '2524639890ba4f2cba9ba1a4eeaa4015',           -- business_id
  'INTERSECT_ALLOWLIST',                         -- policy_type
  '["605560545117995008"]',                      -- allow_zone_ids: 仅 28F
  NULL, 1, 1,                                    -- building_id, enabled, version
  '广发基金:访客楼层策略(组织库);默认 28F。'
);

允许楼层: 1 个 zone — 605560545117995008 (28F)

4.2 当前生效流程

广发基金访客邀约:
  1. 前端/POST /elevator/person/add/visitor
  2. addVisitor() → personService.detail(personId)
  3. ImgPersonServiceImpl#detail()
     → listByImageId() 返回被访人全部楼层 (可能多 zone)
     → replacementZoneIds(["488b8ad..."]) 命中策略
     → floorList = ["605560545117995008"]  # 仅 28F
  4. addVisitor() UC-01: effective = ["605560545117995008"]
  5. 写入 image_rule_ref (仅 28F 一条)

4.3 当前配置的局限

局限 描述
仅 1 层 广发基金租用 28-38F (11层),但策略仅开放 28F 给访客
组织粒度单一 org_id 绑定到 [28-38F]广发基金管理有限公司 一个节点
无部门区分 广发基金内部所有部门的访客均只能到达 28F

4.4 相关文件清单

文件 内容
docs/sql/tenant_visitor_floor_policy_init_guangfa_fund.sql 广发 28F 种子 (电梯库侧)
docs/sql/organization_tenant_visitor_floor_policy_init_tenants.sql 广发 28F + 物业 28F+6F 种子 (组织库侧)
docs/testing/广发基金访客被访楼层接口生产验证.md 广发生产验证文档
docs/testing/tenant-visitor-default-floor-isolation.md 租户隔离边界说明
scripts/test-env/stub-person-service.py 测试桩 (organizationNames: ["广发基金"])
backend/cw-elevator-application/tools/visitor_floor_verification/... 策略验证脚本
docs/testing/release-visitor-noauth-verify-v20260430/... 批量验证套件

5. 广发基金扩展至 20 层评估

5.1 需求理解

将广发基金访客可到达楼层从 1 层 (28F) 扩展至 20 层 (推测: 28-38F 共 11 层 + 扩展至 20 层范围 = 可能包含更广范围的楼层如 19-38F 或 28-47F 等)。

待确认: 具体哪 20 层的 zone_id 列表需与业务方确认。

5.2 变更方案

方案 A: 纯数据变更 (推荐 ★★★★★)

操作: 仅更新 tenant_visitor_floor_policy 表的 allow_zone_ids 字段, 从 ["605560545117995008"] 扩展为 20 个 zone_id 的 JSON 数组。

变更范围:

层级 变更内容
数据库 UPDATE tenant_visitor_floor_policy SET allow_zone_ids = '["zone1",...,"zone20"]' WHERE id = 'gf_vstr_policy_guangfa_fund_001x'
Java 代码 无需修改parseAllowZoneIds() 已支持任意数量 zone
SQL 种子 更新 organization_tenant_visitor_floor_policy_init_tenants.sqltenant_visitor_floor_policy_init_guangfa_fund.sql
接口契约 不变PersonResult.floorList 自动包含 20 个 zone

优点:

  • 零代码变更
  • 现有策略引擎自动支持多 zone 替代
  • detail() 返回 floorList 自动包含 20 个 zone
  • addVisitor() 透传, 为每个 zone 写一条 image_rule_ref
  • 立即生效 (无需重启, 下次 detail() 调用即可)

风险: 无 — parseAllowZoneIds()buildFloorInfoListFromOrderedZoneIds() 都已正确处理多 zone 列表。

验证:

-- 执行变更后验证
SELECT id, org_id, allow_zone_ids, enabled, policy_version
FROM tenant_visitor_floor_policy
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';

-- 预期: allow_zone_ids 包含 20 个 zone_id 的 JSON 数组

方案 B: 多组织拆分策略

思路: 为广发基金的不同部门 / 组织节点配置不同的楼层范围。

变更范围:

  • 新增多个 tenant_visitor_floor_policy 行, 每条绑定不同的 org_id
  • 例如: [28-30F]广发基金IT部 → allow 28-30F, [31-33F]广发基金财务部 → allow 31-33F

适用场景: 广发基金内部需要按部门细粒度控制访客楼层时启用。当前需求为整体 20 层开放, 暂不需要此方案。

方案 C: 前端增强 (可选)

思路: 前端访客邀约页根据 floorList (20 个 zone) 展示多选列表, 允许 BFF 调用方选择特定楼层子集传入 addVisitor

注意: 当前 addVisitor 已支持 UC-02 调用方传 floorIds 子集, 无需后端变更。

5.3 推荐实施步骤

Step 1 ─ 确认 20 层 zone_id 列表
         查询 code_elevator_area 表获取对应 zone_id
         SELECT zone_id, zone_name, code FROM code_elevator_area
         WHERE building_id = '605560539791228928'  -- floor.building.id

Step 2 ─ 更新生产库策略数据
         UPDATE tenant_visitor_floor_policy
         SET allow_zone_ids = '["zone1","zone2",...,"zone20"]',
             policy_version = policy_version + 1,
             remark = '广发基金:访客默认 20 层(扩展自 v1 28F 单层)',
             updated_at = UNIX_TIMESTAMP(NOW()) * 1000
         WHERE id = 'gf_vstr_policy_guangfa_fund_001x'

Step 3 ─ 更新组织库种子 SQL
         修改 organization_tenant_visitor_floor_policy_init_tenants.sql

Step 4 ─ 验证
         curl POST /elevator/person/add/visitor (广发基金被访人)
         检查 PersonResult.floorList 包含 20 个 zone
         检查 image_rule_ref 写入 20 行

5.4 性能影响

影响项 评估
detail() 响应 增加 19 个 zone_id, 响应体微增 (~1-2KB)
addVisitor() 写入 从 1 行 image_rule_ref 变为 20 行, 批量插入一次性完成
listByPage 列表 buildFloorInfoListFromOrderedZoneIds() 构建 20 条而不是 1 条
zone 查询 结果列表变大但无额外 DB 查询

结论: 性能影响可忽略。核心开销在 addVisitorimage_rule_ref 批量插入 (20 行), 仍为单次 DB 操作。

5.5 何时需要代码变更

当前架构中, 以下场景需要代码变更:

场景 是否需要代码变更
修改 allow_zone_ids (数量变化) 不需要
新增策略行 (新租户) 不需要
修改策略类型 (policy_type) ⚠️ 可能需要 — 当前仅实现 INTERSECT_ALLOWLIST
新增策略字段 (如按 building_id 隔离) 需要 — Mapper/TDO 需添加字段
前端默认选中逻辑需要映射 zone_name 不需要 — buildFloorInfoListFromOrderedZoneIds 已处理

6. 变更点清单与风险矩阵

6.1 变更范围最小化方案

# 步骤 类型 风险
1 确认 20 层 zone_id 列表 调研 🟢
2 UPDATE tenant_visitor_floor_policy (组织库) DML 🟢
3 UPDATE tenant_visitor_floor_policy (电梯库, 如存在) DML 🟢
4 更新种子 SQL 文件 文档 🟢
5 功能验证 (见 §7) 测试 🟢

6.2 关键检查点

检查点 预期结果
策略查询 selectEnabledByOrgId('488b8ad...') 返回 enabled=1
allow_zone_ids 解析 parseAllowZoneIds() 返回 20 个 zone_id
detail() floorList PersonResult.floorList 含 20 个 zone_id
addVisitor UC-01 effective = 20 个 zone_id, 写入 20 行 image_rule_ref
addVisitor UC-02 调用方传 5 个 floorIds → 只写 5 行 (策略不覆盖 UC-02)
listByPage 访客列表 策略命中 → 展示 20 层, defaultChooseFloor = 第一个 zone
其他租户不受影响 非广发基金组织 → detail() 走 listByImageId 原路径

6.3 回滚方案

-- 回滚到 28F 单层
UPDATE tenant_visitor_floor_policy
SET allow_zone_ids = '["605560545117995008"]',
    policy_version = policy_version + 1,
    remark = '广发基金:访客默认 28F(回滚)',
    updated_at = UNIX_TIMESTAMP(NOW()) * 1000
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';

回滚立即生效, 无需重启服务。


7. 测试验证策略

7.1 快速验证 (curl)

# 广发基金被访人访客邀约
curl -X POST http://127.0.0.1:18081/elevator/person/add/visitor \
  -H 'Content-Type: application/json' \
  -d '{
    "personId": "1072908835884208128",
    "businessId": "2524639890ba4f2cba9ba1a4eeaa4015",
    "visitorName": "test_20f",
    "begVisitorTime": "2026-05-09 00:00:00",
    "endVisitorTime": "2026-12-31 23:59:59"
  }'

# 预期: 返回成功, image_rule_ref 写入 20 行

7.2 数据库验证

-- 1. 验证策略配置
SELECT id, org_id, allow_zone_ids, enabled, policy_version
FROM tenant_visitor_floor_policy
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';

-- 2. 验证写入的规则行数
SELECT COUNT(*) AS rule_count
FROM image_rule_ref
WHERE person_id = '<visitor_id>';

-- 预期: rule_count = 20

-- 3. 验证 zone 覆盖
SELECT DISTINCT zone_id, zone_name
FROM image_rule_ref
WHERE person_id = '<visitor_id>'
ORDER BY zone_name;

7.3 验证脚本 (现有工具)

# 使用现有组织策略验证脚本
cd backend/cw-elevator-application/tools/visitor_floor_verification
python3 scripts/verify_org_policy_fix.py

# 期望: T1 (有策略→allow 替代) 返回 20 个 zone
#       T2-T7 不受影响

7.4 回归检查点

测试项 期望
广发基金访客获 20 层权限
物业公司访客仍为 28F+6F
其他租户访客不受影响
UC-02 调用方指定 3 层 → 只写 3 行
listByPage 访客列表策略命中展示
策略禁用 (enabled=0) → 回退到原逻辑

8. 附录: 关键文件索引

8.1 Java 源码

文件 行数参考 功能
backend/ninca-common-component-organization/.../policy/TenantVisitorFloorPolicyService.java 全文 103 行 策略服务: 查询、解析、替代
backend/ninca-common-component-organization/.../service/ImgPersonServiceImpl.java 154-158 (注入), 326-370 (listByPage), 643-648 (detail) 策略应用点
backend/cw-elevator-application/.../impl/PersonRuleServiceImpl.java 165-270 (addVisitor) 电梯侧透传
backend/ninca-common-component-organization/.../entity/TenantVisitorFloorPolicy.java 全文 策略实体
backend/ninca-common-component-organization/.../mapper/TenantVisitorFloorPolicyMapper.java 18-26 (查询) 策略 DAO

8.2 SQL 脚本

文件 功能
docs/sql/tenant_visitor_floor_policy.sql 表 DDL
docs/sql/tenant_visitor_floor_policy_v2.sql 增加 org_id 迁移
docs/sql/tenant_visitor_floor_policy_init_guangfa_fund.sql 广发基金种子 (电梯库)
docs/sql/organization_tenant_visitor_floor_policy_init_tenants.sql 广发 + 物业种子 (组织库)
docs/sql/tenant_visitor_floor_policy_migrate_org_id.sql org_id 数据迁移

8.3 设计文档

文件 内容
docs/superpowers/specs/2026-05-06-tenant-visitor-policy-organization-implementation.md 策略迁入组织组件规范 (559 行)
docs/superpowers/specs/2026-05-01-org-id-policy-fix-design.md org_id 粒度修复设计
docs/business/租户访客默认楼层技术产品方案.md 产品方案 (427 行)
docs/testing/tenant-visitor-default-floor-isolation.md 租户隔离边界 (105 行)
docs/testing/visitor-registration-floor-validation.md 测试方案 (151 行)
docs/testing/广发基金访客被访楼层接口生产验证.md 广发生产验证

8.4 测试与验证

文件 功能
scripts/test-env/verify-functional.sh 功能验证 (F3/F4 租户策略测试)
scripts/test-env/stub-person-service.py 广发基金测试桩
backend/cw-elevator-application/tools/visitor_floor_verification/... 策略验证脚本集
docs/testing/release-visitor-noauth-verify-v20260430/... 批量验证套件

总结

  1. 40F/6F 逻辑是星河湾物业管理的历史遗留硬编码, 仅影响 listByPage 访客列表展示, 不影响 detail()addVisitor()

  2. 租户策略 (tenant_visitor_floor_policy)替代 语义: 命中后 allow_zone_ids 完全替代 floorList, 不做求交。

  3. 广发基金当前仅开放 28F (1 层), 通过 tenant_visitor_floor_policy 单行配置实现。

  4. 扩展至 20 层是纯数据变更: 只需 UPDATE allow_zone_ids 字段为 20 个 zone_id 的 JSON 数组。零代码变更, 零接口变更, 零重启

  5. next step: 确认 20 层 zone_id 列表 → UPDATE 生产库 → 更新种子 SQL → 功能验证。