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
This commit is contained in:
hpd840321
2026-05-09 23:56:12 +08:00
parent 721e88dd89
commit 42c4a9fd6b
9 changed files with 2628 additions and 1 deletions
@@ -0,0 +1,296 @@
# 广发基金 28F 硬编码方案 — 组件/模块/位置细化设计
**日期**: 2026-05-09
**分支**: `feature/guangfa-28f-hardcoded`
**状态**: 方案细化完成,待实施
---
## 1. 涉及组件与模块总览
```
source/backend/
├── ninca-common-component-organization/ ★ 唯一变更组件
│ ├── cwos-component-organization-service/ ★ 代码变更
│ │ └── src/main/java/cn/cloudwalk/service/organization/service/
│ │ └── ImgPersonServiceImpl.java ★ 唯一变更 Java 文件
│ │
│ ├── cwos-component-organization-starter/ ★ 配置变更
│ │ └── deploy/run-verify/
│ │ └── application.properties ★ 唯一变更配置文件
│ │
│ └── (其余模块不变: data, interface, web)
├── cw-elevator-application/ ☆ 仅增强日志
│ └── cw-elevator-application-service/
│ └── .../impl/PersonRuleServiceImpl.java ☆ +2 行日志
└── intelligent-cwoscomponent/ 不变 (仅 Feign 接口定义)
```
---
## 2. 变更点详细定位
### 2.1 模块: `cwos-component-organization-service`
**文件**: `backend/ninca-common-component-organization/cwos-component-organization-service/src/main/java/cn/cloudwalk/service/organization/service/ImgPersonServiceImpl.java`
**包**: `cn.cloudwalk.service.organization.service`
**类**: `ImgPersonServiceImpl` (1414 行)
#### 位置 A — 依赖注入区 (第 157-158 行后,新增 2 行)
**原代码**:
```java
157: @Value("${xhwSixFloorId}")
158: private String xhwSixFloorId;
159:
160: private static final String imageBase64 =
```
**改为**:
```java
157: @Value("${xhwSixFloorId}")
158: private String xhwSixFloorId;
159:
160: @Value("${gfOrgId}")
161: private String gfOrgId;
162:
163: @Value("${gfDefaultFloorId}")
164: private String gfDefaultFloorId;
165:
166: private static final String imageBase64 =
```
#### 位置 B — `listByPage()` 访客列表分支 (第 354-370 行,插入 else-if)
**当前代码** (精确):
```java
354: if (imgStorePersonResult.getOrganizationIds().contains(this.xhwId)) {
355: imgStorePersonResult.setDefaultChooseFloor(this.xhwDefaultFloorId);
356: List<AcsPassRuleImageResultDto> floorInfoList = new ArrayList<>();
357: AcsPassRuleImageResultDto resultDto = new AcsPassRuleImageResultDto();
358: resultDto.setZoneId(this.xhwDefaultFloorId);
359: resultDto.setZoneName("40F");
360: floorInfoList.add(resultDto);
361: imgStorePersonResult.setFloorInfoList(floorInfoList);
362: } else {
363: imgStorePersonResult.setDefaultChooseFloor(this.xhwSixFloorId);
364: List<AcsPassRuleImageResultDto> floorInfoList = new ArrayList<>();
365: AcsPassRuleImageResultDto resultDto = new AcsPassRuleImageResultDto();
366: resultDto.setZoneId(this.xhwSixFloorId);
367: resultDto.setZoneName("6F");
368: floorInfoList.add(resultDto);
369: imgStorePersonResult.setFloorInfoList(floorInfoList);
370: }
```
**改为**:
```java
354: if (imgStorePersonResult.getOrganizationIds().contains(this.xhwId)) {
355: imgStorePersonResult.setDefaultChooseFloor(this.xhwDefaultFloorId);
356: List<AcsPassRuleImageResultDto> floorInfoList = new ArrayList<>();
357: AcsPassRuleImageResultDto resultDto = new AcsPassRuleImageResultDto();
358: resultDto.setZoneId(this.xhwDefaultFloorId);
359: resultDto.setZoneName("40F");
360: floorInfoList.add(resultDto);
361: imgStorePersonResult.setFloorInfoList(floorInfoList);
362: } else if (imgStorePersonResult.getOrganizationIds().contains(this.gfOrgId)) {
363: imgStorePersonResult.setDefaultChooseFloor(this.gfDefaultFloorId);
364: List<AcsPassRuleImageResultDto> floorInfoList = new ArrayList<>();
365: AcsPassRuleImageResultDto resultDto = new AcsPassRuleImageResultDto();
366: resultDto.setZoneId(this.gfDefaultFloorId);
367: resultDto.setZoneName("28F");
368: floorInfoList.add(resultDto);
369: imgStorePersonResult.setFloorInfoList(floorInfoList);
370: imgStorePersonResult.setIsAcrossDay(Integer.valueOf(0));
371: this.logger.info("[GF-28F] listByPage MATCH orgId={} in orgIds={} → default 28F",
372: this.gfOrgId, imgStorePersonResult.getOrganizationIds());
373: } else {
374: imgStorePersonResult.setDefaultChooseFloor(this.xhwSixFloorId);
375: List<AcsPassRuleImageResultDto> floorInfoList = new ArrayList<>();
376: AcsPassRuleImageResultDto resultDto = new AcsPassRuleImageResultDto();
377: resultDto.setZoneId(this.xhwSixFloorId);
378: resultDto.setZoneName("6F");
379: floorInfoList.add(resultDto);
380: imgStorePersonResult.setFloorInfoList(floorInfoList);
381: }
```
**注意**: 行号偏移 (+5 行,因上方注入区新增)。实际编辑以内容匹配为准。
#### 位置 C — `detail()` 邀约+派梯核心路径 (第 643-651 行,在策略替代块后插入)
**当前代码** (精确):
```java
643: Optional<List<String>> replacementFloors =
644: this.tenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive(
645: result.getOrganizationIds());
646: if (replacementFloors.isPresent()) {
647: floorList = new ArrayList<>(replacementFloors.get());
648: zoneNames = buildCommaSeparatedFloorNames(businessId, floorList);
649: }
650: result.setFloorNames(zoneNames);
651: result.setFloorList(floorList);
```
**改为**:
```java
643: Optional<List<String>> replacementFloors =
644: this.tenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive(
645: result.getOrganizationIds());
646: if (replacementFloors.isPresent()) {
647: floorList = new ArrayList<>(replacementFloors.get());
648: zoneNames = buildCommaSeparatedFloorNames(businessId, floorList);
649: }
650: // 广发基金: 邀约 + UC-01 派梯 floorList 限制为 28F
651: if (!CollectionUtils.isEmpty(result.getOrganizationIds())
652: && result.getOrganizationIds().contains(this.gfOrgId)) {
653: List<String> originalFloors = new ArrayList<>(floorList);
654: floorList = Collections.singletonList(this.gfDefaultFloorId);
655: zoneNames = "28F";
656: this.logger.info("[GF-28F] detail MATCH orgId={} in orgIds={} → floor restricted: {}→[28F]",
657: this.gfOrgId, result.getOrganizationIds(), originalFloors);
658: } else {
659: this.logger.debug("[GF-28F] detail NO-MATCH orgId={} not in orgIds={}",
660: this.gfOrgId, result.getOrganizationIds());
661: }
662: result.setFloorNames(zoneNames);
663: result.setFloorList(floorList);
```
**注意**: `Collections.singletonList` 需要 `import java.util.Collections;` — 检查是否已存在(当前代码 `listByPage` 中使用了 `Collections.singletonList`import 应已存在)。
#### 位置 D — `detail()` 入口日志 (在第 600 行附近,`detail()` 方法体内)
`detail()` 方法体的早期(如第 570-600 行区间,根据实际方法体定位),新增:
```java
this.logger.info("[GF-DETAIL] entry personId={} businessId={} orgIds={}",
param.getId(), businessId, result.getOrganizationIds());
```
#### 位置 E — `detail()` listByImageId 返回日志 (在第 630 行附近)
`listByImageId` 调用成功后、循环组装 `floorList` 前:
```java
this.logger.info("[GF-DETAIL] listByImageId returned {} zones: {}",
acsPassRuleImageResultDtoList.size(),
acsPassRuleImageResultDtoList.stream()
.map(AcsPassRuleImageResultDto::getZoneId).collect(Collectors.toList()));
```
---
### 2.2 模块: `cw-elevator-application-service`
**文件**: `backend/cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/person/impl/PersonRuleServiceImpl.java`
**包**: `cn.cloudwalk.elevator.person.impl`
**类**: `PersonRuleServiceImpl`
#### 位置 F — addVisitor() 日志增强 (第 187-194 行附近)
**当前代码** (精确):
```java
187: boolean callerProvidedFloors = !CollectionUtils.isEmpty(param.getFloorIds());
188: if (callerProvidedFloors) {
189: effective = param.getFloorIds();
190: this.logger.info("UC-02:调用方显式楼层 effective={}", effective);
191: } else {
192: effective = personResult.getFloorList();
193: if (CollectionUtils.isEmpty(effective)) {
194: this.logger.warn("UC-01:被访人 detail.floorList 为空 personId={}", param.getPersonId());
```
**改为**:
```java
187: boolean callerProvidedFloors = !CollectionUtils.isEmpty(param.getFloorIds());
188: if (callerProvidedFloors) {
189: effective = param.getFloorIds();
190: this.logger.info("[GF-ADDV] UC-02 effective={}", effective);
191: } else {
192: effective = personResult.getFloorList();
193: if (CollectionUtils.isEmpty(effective)) {
194: this.logger.warn("[GF-ADDV] UC-01 floorList empty personId={}", param.getPersonId());
```
---
### 2.3 模块: `cwos-component-organization-starter` (配置)
**文件**: `backend/ninca-common-component-organization/cwos-component-organization-starter/deploy/run-verify/application.properties`
**位置**: 第 172 行后(`xhwSixFloorId` 之后),新增 2 行:
```properties
# 第 170-172 行 (现有):
xhwId=21474e012cd14e26bc62771873b22562
xhwDefaultFloorId=605560547135455232
xhwSixFloorId=605560541473144832
# 第 173-174 行 (新增):
gfOrgId=488b8ad049bb43408a6fbcc50bcb89ac
gfDefaultFloorId=605560545117995008
```
---
### 2.4 数据层: 禁用原表驱动策略
**数据库**: `component-organization` (组织库)
**表**: `tenant_visitor_floor_policy`
```sql
UPDATE tenant_visitor_floor_policy
SET enabled = 0,
remark = CONCAT(remark, ' [DISABLED 2026-05-09: migrated to hardcoded 28F in ImgPersonServiceImpl]'),
updated_at = UNIX_TIMESTAMP(NOW()) * 1000
WHERE id = 'gf_vstr_policy_guangfa_fund_001x'
AND enabled = 1;
```
**数据库**: `cw-elevator-application` (电梯库,如果存在同步表)
```sql
-- 仅当电梯库也有 tenant_visitor_floor_policy 表时执行
UPDATE tenant_visitor_floor_policy
SET enabled = 0
WHERE id = 'gf_vstr_policy_guangfa_fund_001x'
AND enabled = 1;
```
---
## 3. 不变更清单
| 组件 | 模块 | 文件 | 原因 |
|------|------|------|------|
| intelligent-cwoscomponent | interface | PersonService.java | Feign 接口定义,不变 |
| intelligent-cwoscomponent | rest | PersonFeignClient.java | HTTP 路径映射,不变 |
| ninca-common-component-organization | data | TenantVisitorFloorPolicyMapper.java | 仍服务物业策略,不删 |
| ninca-common-component-organization | service | TenantVisitorFloorPolicyService.java | 仍服务物业策略,不删 |
| ninca-common-component-organization | web | PersonController.java | REST 入口,不变 |
| ninca-common-component-organization | starter | bootstrap.properties | 启动配置,不变 |
| cw-elevator-application | web | AcsPersonController.java | REST 入口,不变 |
| cw-elevator-application | data | ImageRuleRefDao.java | DAO 层,不变 |
| scripts/test-env | config | component-org.properties | 测试模板(最小配置),不变 |
| ninca-common-component-organization | releases/ | 所有 `releases/` 下文件 | 历史发布快照,不变 |
---
## 4. 汇总
| # | 组件 | Maven 模块 | 文件 | 变更类型 | 行数 |
|---|------|-----------|------|---------|------|
| A | component-org | cwos-component-organization-**service** | `ImgPersonServiceImpl.java` | 注入 `@Value` ×2 | +4 |
| B | component-org | cwos-component-organization-**service** | `ImgPersonServiceImpl.java` | `listByPage` else-if 分支 | +11 |
| C | component-org | cwos-component-organization-**service** | `ImgPersonServiceImpl.java` | `detail()` floorList 截断 | +13 |
| D | component-org | cwos-component-organization-**service** | `ImgPersonServiceImpl.java` | `detail()` 入口日志 | +2 |
| E | component-org | cwos-component-organization-**service** | `ImgPersonServiceImpl.java` | listByImageId 返回日志 | +3 |
| F | elevator-app | cw-elevator-application-**service** | `PersonRuleServiceImpl.java` | addVisitor 日志前缀 | ~0 (替换) |
| — | component-org | cwos-component-organization-**starter** | `application.properties` | 配置 ×2 | +2 |
| — | (数据库) | — | `tenant_visitor_floor_policy` | UPDATE enabled=0 | 1 行 |
**总计: 2 个组件, 3 个模块, 3 个文件, ~35 行新增**
@@ -0,0 +1,243 @@
# 广发基金 28F 策略 — 参照 40F/6F 硬编码模式重实现可行性评估
**日期**: 2026-05-09
**状态**: 评估完成 — **不可行 (推荐保留表驱动方案)**
---
## 1. 两种模式的本质差异
### 1.1 40F/6F 硬编码模式
```
┌─ @Value("${xhwId}") 注入配置
├─ @Value("${xhwDefaultFloorId}") 注入配置
├─ @Value("${xhwSixFloorId}") 注入配置
└─ ImgPersonServiceImpl#listByPage (仅此一处)
└─ if (orgIds.contains(xhwId))
└─ defaultChooseFloor = xhwDefaultFloorId, zoneName = "40F"
else
└─ defaultChooseFloor = xhwSixFloorId, zoneName = "6F"
```
**核心特征**: 只影响 `listByPage` 访客列表展示层, **不涉及 `detail()` / `addVisitor()`**
### 1.2 当前广发基金表驱动模式
```
┌─ tenant_visitor_floor_policy 表 数据源
├─ TenantVisitorFloorPolicyMapper DAO
├─ TenantVisitorFloorPolicyService 策略引擎
├─ ImgPersonServiceImpl#listByPage P1 分支 (列表展示)
└─ ImgPersonServiceImpl#detail() 核心路径 (UC-01 派梯)
└─ replacementZoneIdsIfPolicyActive → floorList 替代
└─ PersonRuleServiceImpl#addVisitor() → 写入 image_rule_ref
```
**核心特征**: 同时影响 `listByPage` (列表) 和 `detail()` (派梯)。
### 1.3 关键差异对比
| 维度 | 40F/6F 硬编码 | 广发表驱动 | 差异 |
|------|-------------|-----------|------|
| 数据存储 | `application.properties` | `tenant_visitor_floor_policy` 表 | 文件 vs DB |
| 变更方式 | 改配置文件 + 重启 | UPDATE SQL (立即生效) | 重启 vs 实时 |
| 生效范围 | `listByPage` 仅列表 | `listByPage` + `detail()` | 单路径 vs 双路径 |
| 多 zone 支持 | 否 (单 zone) | 是 (JSON 数组) | 1 vs N |
| zone 名称 | 硬编码字符串 `"40F"` | 动态解析 (zone 表) | 硬编码 vs 动态 |
| 组织匹配 | contains(xhwId) | 按 org_id 精确匹配 | 包含 vs 精确 |
| 多组织策略 | 无概念 | 按 orgIds 顺序依次命中 | N/A |
| 启用/禁用 | 无开关 | `enabled` 字段 | 需删配置 vs 一行 SQL |
| 策略版本 | 无 | `policy_version` 自增 | 无追踪 vs 有追踪 |
| addVisitor 影响 | ❌ 无 (不走 detail) | ✅ 核心路径 | **致命差异** |
---
## 2. 致命问题: `detail()` 路径缺失
### 2.1 `addVisitor()` 的楼层来源
```java
// PersonRuleServiceImpl#addVisitor() 第 187-194 行
boolean callerProvidedFloors = !CollectionUtils.isEmpty(param.getFloorIds());
if (callerProvidedFloors) {
effective = param.getFloorIds(); // UC-02
} else {
effective = personResult.getFloorList(); // UC-01 ← 来自 detail()
}
```
`detail()` 返回的 `floorList` 是 UC-01 派梯的**唯一数据源**。
### 2.2 `detail()` 的当前楼层组装流程
```java
// ImgPersonServiceImpl#detail() 第 630-651 行
// Step A: listByImageId 获取被访人全部楼层
CloudwalkResult<List<AcsPassRuleImageResultDto>> images = elevatorFeignClient.listByImageId(...);
// Step B: 遍历组装 floorList (原始全量)
for (AcsPassRuleImageResultDto dto : acsPassRuleImageResultDtoList) {
floorList.add(dto.getZoneId());
}
// Step C: ★ 策略替代 — 这是 40F/6F 模式完全没有的
Optional<List<String>> replacementFloors =
tenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive(organizationIds);
if (replacementFloors.isPresent()) {
floorList = new ArrayList<>(replacementFloors.get()); // ← 替代
}
// Step D: 写入 PersonResult
result.setFloorList(floorList);
```
### 2.3 如果搬到 40F/6F 模式会发生什么
广发基金访客 `addVisitor` 的 UC-01 路径:
```
// 改造后 detail() 不再查策略表
detail() → listByImageId() → floorList = [28F, 29F, ..., 38F] 全部楼层
→ 无策略替代 → floorList = 11 个 zone (被访人的全部楼层)
→ addVisitor UC-01: effective = 11 个 zone
→ 写入 11 行 image_rule_ref
→ ❌ 访客获得了不应该有的 29-38F 权限
```
**结论**: 40F/6F 模式的核心缺陷是**不接入 `detail()` 路径**。广发基金策略如果只套用 `listByPage` 硬编码, 将导致 `addVisitor` 派梯**完全绕过策略限制**, 访客获得被访人的全部楼层权限而非仅限 20 层。
---
## 3. 如果强行修补 (在 detail() 中也加硬编码)
### 3.1 代码膨胀预估
```java
// 需要在 ImgPersonServiceImpl 中新增:
@Value("${gfOrgId}") private String gfOrgId;
@Value("${gfDefaultFloorList}") private String gfDefaultFloorList; // 逗号分隔 20 个 zoneId
// detail() 中新增 (第 646 行附近):
if (result.getOrganizationIds().contains(this.gfOrgId)) {
floorList = Arrays.asList(gfDefaultFloorList.split(","));
zoneNames = buildCommaSeparatedFloorNames(businessId, floorList);
}
// listByPage 中新增 (第 354 行附近):
if (imgStorePersonResult.getOrganizationIds().contains(this.gfOrgId)) {
List<String> floorIds = Arrays.asList(gfDefaultFloorList.split(","));
buildFloorInfoListFromOrderedZoneIds(businessId, floorIds);
imgStorePersonResult.setDefaultChooseFloor(floorIds.get(0));
}
```
### 3.2 问题清单
| # | 问题 | 严重度 |
|---|------|--------|
| 1 | 每新增一个租户需要加 2 个 `@Value` + 2 处 if/else | 🔴 |
| 2 | `application.properties``gfDefaultFloorList` 需要维护 20 个 zone_id (长字符串) | 🔴 |
| 3 | 20 个 zone_id 在配置文件中不可读,容易出错 | 🟡 |
| 4 | 修改策略需改配置文件 + 重启服务 | 🟡 |
| 5 | 无法动态启用/禁用 (需注释掉 @Value 或删配置) | 🟡 |
| 6 | 无策略版本追踪 | 🟢 |
| 7 | 与现有 `tenant_visitor_floor_policy` 表形成**双重逻辑源** | 🔴 |
| 8 | `detail()` 中硬编码逻辑与表驱动逻辑并存,维护混乱 | 🔴 |
### 3.3 架构退化示意
```
改造前 (表驱动, 单一逻辑源):
detail() ─→ TenantVisitorFloorPolicyService ─→ DB
listByPage ─→ TenantVisitorFloorPolicyService ─→ DB
改造后 (混合模式, 双逻辑源):
detail() ─┬→ TenantVisitorFloorPolicyService ─→ DB (其他租户)
└→ if (contains(gfOrgId)) { split(config) } (广发特例)
listByPage ─┬→ TenantVisitorFloorPolicyService ─→ DB (其他租户)
├→ if (contains(xhwId)) { "40F" } (物业)
├→ else { "6F" } (非物业)
└→ if (contains(gfOrgId)) { split(config) } (广发特例 ← 新增)
```
**每增加一个租户策略, 代码分支呈线性增长**, 最终成为无法维护的 if/else 链。
---
## 4. 对比结论
| 评估维度 | 40F/6F 模式 | 表驱动模式 | 胜出 |
|----------|-----------|-----------|------|
| 代码简洁性 | 简单 (3 个 @Value + 1 个 if/else) | 中等 (5 个类) | 40F |
| 扩展性 (新增租户) | 差 (每次加代码) | 优 (INSERT 一行) | 表驱动 |
| 多 zone 支持 | 无 (单 zone) | 优 (JSON 数组) | 表驱动 |
| 变更生效 | 需重启 | 实时 | 表驱动 |
| 启用/禁用 | 改配置重启 | UPDATE SQL | 表驱动 |
| 版本追踪 | 无 | policy_version | 表驱动 |
| **addVisitor 覆盖** | **❌ 不覆盖** | **✅ 覆盖** | **表驱动** |
| 双重逻辑源风险 | 无 (仅 listByPage) | 无 (单一策略引擎) | 平 |
| 安全审计 | 无 | DB 行可审计 | 表驱动 |
### 4.1 40F/6F 模式为什么可以存在
40F/6F 逻辑是**星河湾物业管理的历史遗留**, 且:
1. 其职责仅限于 `listByPage` 访客列表的**展示层默认值**
2. 不影响 `detail()` / `addVisitor()` 的派梯权限
3. 是"全物业" vs "非物业"的二分类, 不需要细粒度组织匹配
4. 只有 2 个 zone, 不需要多 zone 支持
因此 40F/6F 的正确理解是: **这不是一个"楼层策略", 而是列表展示的 fallback 默认值**
### 4.2 广发基金不能套用的根本原因
广发基金 20 层是**真正的访问控制策略** — 它必须限制 `addVisitor` 写入的 `image_rule_ref` 行。40F/6F 模式完全不接入这条路径, 强行接入会导致代码膨胀和双重逻辑源。
---
## 5. 推荐方案
### 方案 A: 保持表驱动不改 (★★★★★ 强烈推荐)
```
当前架构已经正确:
tenant_visitor_floor_policy 表 ← 数据
TenantVisitorFloorPolicyService ← 引擎
ImgPersonServiceImpl#detail() ← 替代 floorList
ImgPersonServiceImpl#listByPage ← P1 展示
PersonRuleServiceImpl#addVisitor ← UC-01 透传
```
**唯一需要的变更**: UPDATE `allow_zone_ids` 从 1 个 zone 到 20 个 zone。
### 方案 B: 重构 40F/6F 到表驱动 (★★★ 锦上添花, 非必须)
将 40F/6F 的硬编码逻辑也迁移到 `tenant_visitor_floor_policy` 表:
```sql
INSERT INTO tenant_visitor_floor_policy (...) VALUES (
'pm_40f_hardcoded_migration',
'21474e012cd14e26bc62771873b22562', -- xhwId
'2524639890ba4f2cba9ba1a4eeaa4015',
'INTERSECT_ALLOWLIST',
'["605560547135455232"]', -- 40F
NULL, 1, 1,
'物业公司:列表默认展示 40F (迁移自硬编码)'
);
```
然后在 `listByPage` 中删除 40F/6F 硬编码分支, 统一走策略引擎。此项非当前必须, 可作为技术债务清理。
---
## 6. 总结
| 问题 | 答案 |
|------|------|
| 能否参照 40F/6F 模式重实现广发 28F? | **技术上可以, 产出上不可行** |
| 根本原因 | 40F/6F 模式不接入 `detail()`, 导致 `addVisitor` 绕过策略 |
| 如果强行修补 | 双逻辑源、代码膨胀、维护灾难 |
| 正确做法 | 保持表驱动, 扩展 `allow_zone_ids` 即可 (零代码变更) |
| 额外收益 | 后续可将 40F/6F 也迁移到表驱动 (统一架构) |
@@ -0,0 +1,590 @@
# 访客楼层策略完整走查 & 广发基金 20 层扩展评估
**日期**: 2026-05-09
**状态**: 走查评估完成
**作者**: OpenCode Agent
---
## 目录
1. [架构总览](#1-架构总览)
2. [星河湾 40F/6F 硬编码逻辑走查](#2-星河湾-40f6f-硬编码逻辑走查)
3. [租户访客策略完整链路走查](#3-租户访客策略完整链路走查)
4. [广发基金当前 28F 策略分析](#4-广发基金当前-28f-策略分析)
5. [广发基金扩展至 20 层评估](#5-广发基金扩展至-20-层评估)
6. [变更点清单与风险矩阵](#6-变更点清单与风险矩阵)
7. [测试验证策略](#7-测试验证策略)
8. [附录: 关键文件索引](#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.detail``PersonResult.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`
```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 行):
```java
@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
```java
// 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`)
```sql
-- 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` 顺序依次尝试:
```java
// TenantVisitorFloorPolicyService.java 第 70-80 行
for (String id : orgIds) {
Optional<List<String>> zones = replacementZoneIdsIfPolicyActive(id);
if (zones.isPresent()) {
return zones; // 第一个命中的策略作为结果
}
}
```
意味着: **人员所属的第一个有策略的组织决定最终楼层**
### 3.4 电梯侧 addVisitor() 流程
```java
// 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`):
```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.sql``tenant_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 列表。
**验证**:
```sql
-- 执行变更后验证
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 查询 |
**结论**: 性能影响可忽略。核心开销在 `addVisitor``image_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 回滚方案
```sql
-- 回滚到 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)
```bash
# 广发基金被访人访客邀约
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 数据库验证
```sql
-- 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 验证脚本 (现有工具)
```bash
# 使用现有组织策略验证脚本
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 → 功能验证。