# 租户访客默认楼层策略 — 业务逻辑设计 **日期**:2026-05-05 **状态**:设计稿(待审核) **基线版本**:v2.0.20 --- ## 1. 现有问题 `PersonRuleServiceImpl.addVisitor` 中策略查询被包裹在 `if (!callerProvidedFloors)` 条件内,导致调用方一旦传了 `floorIds`(UC-02),策略被完全跳过。 **修正要求**:任何时候都应当查询策略,有策略且生效则以策略 `allow_zone_ids` **替代** 候选楼层。 --- ## 2. 完整时序图 ### 2.1 UC-01:调用方未传 floorIds(组织默认楼层) ``` 调用方 电梯应用 组织服务 tenant_visitor_floor_policy │ │ │ │ │ POST /add/visitor │ │ │ │ (无floorIds) │ │ │ │ ────────────────────→ │ │ │ │ │ │ │ │ │ ── Phase1: 查组织 ── │ │ │ │ POST /component/person/detail(personId) │ │ │ ─────────────────────→ │ │ │ │ ←───────────────────── │ │ │ │ PersonResult { │ │ │ │ floorList: [A,B,C], │ │ │ │ organizationIds:[...] │ │ │ │ } │ │ │ │ │ │ │ │ ── Phase2: 候选楼层 ── │ │ │ │ candidate = floorList │ │ │ │ = [A,B,C] │ │ │ │ │ │ │ │ ── Phase3: ALWAYS 查策略 ── │ │ │ SELECT * WHERE org_id=? AND enabled=1 │ │ │ ──────────────────────────────────────────→ │ │ │ ←────────────────────────────────────────── │ │ │ │ │ │ │ ┌─ policy存在? ─┐ │ │ │ │ │ │ │ │ │ │ YES NO │ │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ │ effective= effective │ │ │ │ allow_zone_ids =candidate │ │ │ │ =[6F] =[A,B,C] │ │ │ │ │ │ │ │ ── Phase4: 空集校验 ── │ │ │ │ effective=[6F] │ │ │ │ │ │ │ │ ── Phase5: 写规则绑图库 ── │ │ ←──────────────────── │ │ │ │ success(6F) │ │ │ ``` ### 2.2 UC-02:调用方传了 floorIds ``` 调用方 电梯应用 组织服务 tenant_visitor_floor_policy │ │ │ │ │ POST /add/visitor │ │ │ │ floorIds=[7F,8F] │ │ │ │ ────────────────────→ │ │ │ │ │ │ │ │ │ ── Phase1: 查组织 ── │ │ │ │ POST /component/person/detail(personId) │ │ │ ─────────────────────→ │ │ │ │ ←───────────────────── │ │ │ │ PersonResult { │ │ │ │ organizationIds │ │ │ │ } ← 仅取orgIds,不用floorList │ │ │ │ │ │ │ ── Phase2: 候选楼层 ── │ │ │ │ candidate = param.floorIds │ │ │ = [7F,8F] │ │ │ │ │ │ │ │ ── Phase3: ALWAYS 查策略 ── │ │ │ SELECT * WHERE org_id=? AND enabled=1 │ │ │ ──────────────────────────────────────────→ │ │ │ ←────────────────────────────────────────── │ │ │ │ │ │ │ ┌─ policy存在? ─┐ │ │ │ │ │ │ │ │ │ │ YES NO │ │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ │ effective= effective │ │ │ │ allow_zone_ids =candidate │ │ │ │ =[6F] =[7F,8F] │ │ │ │ │ │ │ │ ── Phase4: 空集校验 ── │ │ │ │ effective=[6F] │ │ │ │ │ │ │ │ ── Phase5: 写规则绑图库 ── │ │ ←──────────────────── │ │ │ │ success(6F) │ │ │ ``` --- ## 3. 完整控制流 ``` addVisitor(param, context): ┌────────────────────────────────────────────────────────┐ │ Phase1: 查被访人组织(ALWAYS,用于策略获取) │ │ detail = personService.detail(personId, businessId) │ │ if detail fail → return detail.code/msg │ │ personResult = detail.getData() │ │ orgIds = personResult.getOrganizationIds() │ └────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────┐ │ Phase2: 确定候选楼层 candidate │ │ if (param.floorIds 非空): ← UC-02 │ │ candidate = param.floorIds │ │ else: ← UC-01 │ │ candidate = personResult.getFloorList() │ │ if candidate 为空 → return 76260531 │ └────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────┐ │ Phase3: ALWAYS 查策略,有策略则替代 │ │ policy = DAO.selectEnabledByOrgId(orgId) │ │ if (policy 存在 && enabled=1): │ │ effective = parseAllowZoneIds(policy.allow_zone_ids)│ │ ← 策略的 allow 直接替代,非求交 │ │ else: │ │ effective = candidate ← 无策略约束,原值 │ └────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────┐ │ Phase4: 空集校验 │ │ if effective 为空 → return 76260531 │ │ param.setFloorIds(effective) │ └────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────┐ │ Phase5: 写规则 + 绑图库(不变) │ │ zoneService.page → image_rule_ref → batchBind → ... │ └────────────────────────────────────────────────────────┘ ``` --- ## 4. 决策矩阵 | 场景 | caller传floorIds | 有生效策略 | 行为 | 最终楼层 | |------|----------------|-----------|------|---------| | **A** | 空 | 无 | 取组织 floorList | `floorList` | | **B** | 空 | 有,allow=[6F] | 策略替代 | `[6F]` | | **C** | `[7F]` | 无 | 用调用方传入值 | `[7F]` | | **D** | `[7F]` | 有,allow=[6F] | 策略替代(忽略调用方) | `[6F]` | | **E** | `[6F,7F]` | 有,allow=[6F] | 策略替代 | `[6F]` | | **F** | 空 | 有,allow=[](无效或空) | 等同无策略 | `floorList` | ### 关键差异 | 场景 | 旧行为(v2.0.18) | 新行为(v2.0.20) | |------|-----------------|-----------------| | **D**:传 7F,策略 allow=6F | 开通 7F(绕过策略 ❌) | 开通 6F(策略替代 ✅) | | **E**:传 6F+7F,策略 allow=6F | 开通 6F+7F(绕过策略 ❌) | 仅开通 6F(策略替代 ✅) | --- ## 5. 错误码 | 错误码 | 场景 | 说明 | |--------|------|------| | 76260531 | 无可用楼层 | Phase4 中 `effective` 为空(含被访人无楼层、策略 allow 为空等) | | 76260533 | 策略配置错误 | `allow_zone_ids` JSON 解析失败(已有 `parseAllowZoneIds` 兜底返回空) | --- ## 6. 日志设计 | 路径 | 级别 | 内容 | |------|------|------| | 请求入口 | INFO | businessId, personId, visitorId, requestFloorSize | | Phase1 detail 失败 | WARN | personId, code, msg | | Phase2 UC-01 | INFO | candidate = floorList | | Phase2 UC-02 | INFO | candidate = param.floorIds | | Phase2 空 floorList | WARN | personId | | Phase3 查策略 | INFO | orgIds | | Phase3 策略存在 | INFO | policyId, orgId, allow_zone_ids | | Phase3 策略不存在 | INFO | 使用候选楼层原值 | | Phase4 空集 | WARN | businessId, personId, visitorId, candidate | --- ## 7. 初始化流程分析(登记页楼层展示) ### 7.1 调用路径 基于 `cwos-component-organization-service` 反编译分析,初始化时的 `/component/person/detail` 调用链: ``` 登记页初始化 │ ├─ GET/POST /component/person/detail ───→ 组织服务 (ninca-common-component-organization) │ │ │ PersonController.java:132 │ │ → imgStorePersonService.detail() │ │ → ImgPersonServiceImpl.java │ │ line 639-650 │ │ → elevatorFeignClient │ │ .listByImageId(...) │ ← 调电梯查询人员授权楼层 │ → image_rule_ref 表查询 │ │ ← floorList (List) │ │ │ │ ← PersonResult { │ │ floorList: ["zoneId1","zoneId2",...], │ ← 被访人当前授权楼层 │ organizationIds: [...], │ ← 用于 addVisitor 策略查询 │ defaultFloor, chooseFloor, ... │ │ } │ │ │ └─ 展示 floorList → 用户选择 → POST /add/visitor → 策略替代(如有) ``` ### 7.2 floorList 的数据来源 `ImgPersonServiceImpl.java` 中 `floorList` 的组装逻辑: ```java // line 639-650 (ImgPersonServiceImpl.java) ArrayList floorList = new ArrayList<>(); // 调电梯 Feign 接口获取该人员在 image_rule_ref 中的授权区域 CloudwalkResult> images = this.elevatorFeignClient.listByImageId(acsPassRuleImageForm); for (int i = 0; i < acsPassRuleImageResultDtoList.size(); i++) { floorList.add(((AcsPassRuleImageResultDto)acsPassRuleImageResultDtoList.get(i)).getZoneId()); } personResult.setFloorList(floorList); ``` **结论**:`floorList` 是被访人在电梯 `image_rule_ref` 中**已有授权的楼层集合**,不是全量楼层。所以初始化页展示的本身就是被访人有权限的楼层。 ### 7.3 初始化和提交的联动关系 | 阶段 | 调用接口 | 返回数据 | 是否经过策略 | |------|---------|---------|------------| | 初始化 | `组织服务 /component/person/detail` | `floorList`(授权楼层)| ❌ 策略在组织服务中不存在 | | 提交 | `电梯应用 /add/visitor` | 开通结果 | ✅ v2.0.20 已修复 | ### 7.4 潜在问题 由于初始化时不经过策略,存在展示与开通不一致的可能: | 场景 | 初始化展示 | 用户选择 | addVisitor 实际开通 | 用户体验 | |------|-----------|---------|-------------------|---------| | 被访人授权多楼层,策略 allow=[6F] | floorList=[6F,7F,8F] | 选 7F | **6F(策略替代)** | 用户困惑 | | 被访人授权多楼层,策略 allow=[6F] | floorList=[6F,7F,8F] | 不选(UC-01) | **6F(策略替代)** | 与预期一致 | | 无策略 | floorList | 任意 | 与选择一致 | 正常 | **解决方案建议**(后续阶段): 在电梯应用新增 `/elevator/person/effective-floors` 预览接口,供初始化时展示策略约束后的有效楼层。当前阶段可先通过前端交互说明,或由第三方 BFF 自行做策略感知。 --- ## 8. 与现有文档的差异 | 文档 | 旧逻辑 | 新逻辑 | |------|--------|--------| | 数据库阶段技术设计 §5.2 | UC-02 不读策略表 | UC-02 读策略表,替代 | | 数据库阶段技术设计 §5.3 | UC-02 以请求为准 | UC-02 以策略为准 | | 当前代码 `if(!callerProvidedFloors)` | 策略仅在 UC-01 查 | 策略 ALWAYS 查 | | `resolveEffectiveFrozens` 方法 | 历史:∩ + 校验 hostFloors | **废止**;规范语义为组织侧 **替代**,电梯透传 `floorList` |