docs: add initialization flow analysis to policy design doc

- Add §7 initialization flow: /component/person/detail call chain
  from decompiled component-organization source
- Document floorList assembly in ImgPersonServiceImpl (via
  elevatorFeignClient.listByImageId)
- Analyze init vs submit consistency: gap when policy exists
This commit is contained in:
反编译工作区
2026-05-05 19:57:01 +08:00
parent d52babe2c9
commit 1cac12d940
572 changed files with 64161 additions and 61 deletions
@@ -1,79 +1,279 @@
# 租户访客默认楼层策略 — 逻辑修正设计
# 租户访客默认楼层策略 — 业务逻辑设计
**日期**2026-05-05
**状态**待实施
**基线版本**v2.0.18 → v2.0.19
**状态**设计稿(待审核)
**基线版本**v2.0.20
---
## 1. 问题
## 1. 现有问题
现有实现在 `PersonRuleServiceImpl.addVisitor`策略查询包裹在 `if (!callerProvidedFloors)` 条件内,导致
`PersonRuleServiceImpl.addVisitor` 中策略查询包裹在 `if (!callerProvidedFloors)` 条件内,导致调用方一旦传了 `floorIds`UC-02),策略被完全跳过。
- **UC-02**(调用方传了 `floorIds`):策略被完全跳过,即使租户配置了生效策略也不会被执行
- 违反用户预期:**任何时候都应当查询策略**,有策略且生效就走策略路径
**修正要求**:任何时候都应当查询策略,有策略且生效则以策略 `allow_zone_ids` **替代** 候选楼层。
## 2. 修正后控制流
---
## 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):
// 阶段1: 确定候选楼层列表 candidate
if (param.floorIds 非空): ← UC-02
candidate = param.floorIds
else: ← UC-01
candidate = personService.detail() → floorList
if candidate 为空: 返回 76260531
// 阶段2: 查询策略(ALWAYS
policy = DAO.selectEnabledByOrgId(orgId) ← enabled=1
// 阶段3: 策略决定最终楼层
if (policy 存在):
effective = candidate policy.allow_zone_ids
if effective 为空: 返回 76260532(策略约束下无可用楼层)
else:
effective = candidate ← 无策略约束,原样使用
param.floorIds = effective
// 继续 zoneService.page → image_rule_ref → batchBind ...
┌────────────────────────────────────────────────────────┐
│ 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 → ... │
└────────────────────────────────────────────────────────┘
```
## 3. 核心变更
---
| 变更点 | 改前 | 改后 |
|--------|------|------|
| 策略查询条件 | 仅在 `!callerProvidedFloors` 时查 | **ALWAYS 查** |
| UC-02 策略行为 | 完全跳过策略 | 策略生效时对 `floorIds` 求交 |
| UC-01 策略行为 | 查组织 `floorList` → 查策略 | 查组织 floorList → 查策略(不变) |
## 4. 决策矩阵
## 4. UC 对照矩阵
| 场景 | 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` |
| 场景 | floorIds | 策略 | 改前结果 | 改后结果 |
|------|----------|------|---------|---------|
| UC-01 无策略 | 空 | 无 | `floorList` 全集 | `floorList` 全集 ✅ |
| UC-01 + 策略 | 空 | 有且生效 | `allow ∩ floorList` | `allow ∩ floorList`|
| UC-01 无交集 | 空 | allow 与 floorList 无交集 | 失败 76260532 | 失败 76260532 ✅ |
| UC-02 无策略 | 非空 | 无 | 按请求楼层 | 按请求楼层 ✅ |
| UC-02 + 策略 | 非空 | 有且生效 | **按请求楼层(绕过策略)❌** | **`请求楼层 ∩ allow` ✅** |
| UC-02 策略不包含 | 非空 | allow 不含请求楼层 | 成功开通(绕过)❌ | **失败 76260532 ✅** |
### 关键差异
| 场景 | 旧行为(v2.0.18 | 新行为(v2.0.20 |
|------|-----------------|-----------------|
| **D**:传 7F,策略 allow=6F | 开通 7F(绕过策略 ❌) | 开通 6F(策略替代 |
| **E**:传 6F+7F,策略 allow=6F | 开通 6F+7F(绕过策略 ❌) | 仅开通 6F(策略替代 |
---
## 5. 错误码
| 错误码 | 场景 | 说明 |
|--------|------|------|
| 76260531 | 无可用楼层 | `floorList` 为空 或 求交后为空 |
| 76260532 | 策略交集为空 | `candidate ∩ allow` 无交集 |
| 76260533 | 策略配置错误 | allow 包含被访人无权访问的 zoneId |
| 76260531 | 无可用楼层 | Phase4 中 `effective` 为空(含被访人无楼层、策略 allow 为空等) |
| 76260533 | 策略配置错误 | `allow_zone_ids` JSON 解析失败(已有 `parseAllowZoneIds` 兜底返回空) |
## 6. 日志完善
---
| 关键路径 | 日志级别 | 内容 |
|---------|---------|------|
| UC-02 分支 | INFO | 调用方已指定楼层,候选楼层为 xxx |
| UC-01 分支 | INFO | 未传楼层,查组织 floorList 得到 xxx |
| 策略查询 | INFO | 查询 orgId=xxx 的策略 |
| 策略存在 | INFO | 找到启用策略 policyId=xxx allow=xxx |
| 无策略 | INFO | 未找到启用策略,使用候选楼层原值 |
| 求交成功 | INFO | 策略生效,最终楼层为 xxx |
| 求交为空 | WARN | 候选楼层与策略无交集,返回 76260532 |
| 空楼层 | WARN | 无可用楼层,返回 76260531 |
## 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<zoneId>) │
│ │
│ ← 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<String> floorList = new ArrayList<>();
// 调电梯 Feign 接口获取该人员在 image_rule_ref 中的授权区域
CloudwalkResult<List<AcsPassRuleImageResultDto>> 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 | 不再使用,直接 `parseAllowZoneIds` |
@@ -0,0 +1,184 @@
# 租户访客默认楼层策略 — 业务逻辑重设计
**日期**2026-05-05
**状态**:待审核
**设计依据**:产品方案 [租户访客默认楼层技术产品方案](../../business/租户访客默认楼层技术产品方案.md)
**涉及代码**`PersonRuleServiceImpl.addVisitor`
---
## 1. 业务规则(核心不变量)
| 规则 | 说明 |
|------|------|
| **策略全时生效** | 无论调用方是否传入 `floorIds`,**始终查询**该机构是否有启用策略 |
| **策略即安全边界** | 策略的 `allow_zone_ids` 定义访客可达楼层的**上限集合**,任何路径都不能超出此集合 |
| **无策略不禁锢** | 机构未配置策略或策略未启用时,行为与无策略版本完全一致 |
| **交集为空必须拒绝** | `candidate ∩ allow` 为空时禁止继续开通,返回明确错误码 |
---
## 2. 总流程
### 2.1 UC-01:调用方未传 floorIds
```mermaid
sequenceDiagram
participant Caller as 调用方/BFF
participant Elevator as 电梯应用<br/>cw-elevator-application
participant Org as 组织服务<br/>ninca-common-component-organization
participant PolicyDB as 策略表<br/>tenant_visitor_floor_policy
Caller->>Elevator: POST /elevator/person/add/visitor<br/>{personId, visitorId}<br/>(不传 floorIds)
Note over Elevator: 阶段1:查被访人信息
Elevator->>Org: POST /component/person/detail<br/>{personId, businessId}
Org-->>Elevator: PersonResult<br/>{floorList, organizationIds}
Note over Elevator: 阶段2:候选楼层 = floorList
Note over Elevator: floorList 来自组织服务
Note over Elevator: 阶段3:查策略
Elevator->>PolicyDB: SELECT * FROM tenant_visitor_floor_policy<br/>WHERE org_id IN (organizationIds) AND enabled=1
PolicyDB-->>Elevator: policy 行 / 空
alt 策略存在且生效
Note over Elevator: 最终楼层 = floorList ∩ allow_zone_ids
alt 交集为空
Elevator-->>Caller: 失败 76260532<br/>(租户策略与被访人授权无交集)
else 交集非空
Note over Elevator: 继续开通流程
Elevator-->>Caller: 成功,仅开通交集内楼层
end
else 无策略或未启用
Note over Elevator: 最终楼层 = floorList(原值)
Elevator-->>Caller: 成功
end
```
### 2.2 UC-02:调用方传入 floorIds
```mermaid
sequenceDiagram
participant Caller as 调用方/BFF
participant Elevator as 电梯应用<br/>cw-elevator-application
participant Org as 组织服务<br/>ninca-common-component-organization
participant PolicyDB as 策略表<br/>tenant_visitor_floor_policy
Caller->>Elevator: POST /elevator/person/add/visitor<br/>{personId, visitorId, floorIds:[...]}
Note over Elevator: 阶段1:查被访人信息(仅取 organizationIds
Elevator->>Org: POST /component/person/detail<br/>{personId, businessId}
Org-->>Elevator: PersonResult<br/>{organizationIds}
Note over Elevator: 阶段2:候选楼层 = 调用方传入的 floorIds
Note over Elevator: 阶段3:查策略(ALWAYS
Elevator->>PolicyDB: SELECT * FROM tenant_visitor_floor_policy<br/>WHERE org_id IN (organizationIds) AND enabled=1
PolicyDB-->>Elevator: policy 行 / 空
alt 策略存在且生效
Note over Elevator: 最终楼层 = callerFloorIds ∩ allow_zone_ids
alt 交集为空
Elevator-->>Caller: 失败 76260532<br/>(请求楼层不在策略允许范围内)
else 交集非空
Elevator-->>Caller: 成功,仅开通交集内楼层
end
else 无策略或未启用
Note over Elevator: 最终楼层 = callerFloorIds(原值)
Elevator-->>Caller: 成功
end
```
---
## 3. 控制流伪代码
```
addVisitor(param, context):
// === 阶段1:查被访人组织信息(ALWAYS)===
detailResult = personService.detail(personId, businessId)
if failed: return detailResult.error
person = detailResult.data
// === 阶段2:确定候选楼层 ===
if param.floorIds 非空: ← UC-02
candidate = param.floorIds
else: ← UC-01
candidate = person.floorList
if candidate 为空: return 76260531
// === 阶段3ALWAYS 查策略 ===
policy = findEnabledPolicy(person.organizationIds)
if policy != null:
effective = intersect(candidate, policy.allow_zone_ids)
if effective 为空: return 76260532
else:
effective = candidate
// === 阶段4:空集校验 ===
if effective 为空: return 76260531
param.floorIds = effective
// === 阶段5:开通流程(不变)===
zoneService.page → image_rule_ref → batchBind → ...
```
---
## 4. 场景对照矩阵
| 场景 | 调用方 floorIds | 策略状态 | 候选楼层 | 最终结果 |
|------|----------------|---------|---------|---------|
| UC-01 无策略 | 空 | 无策略行 | `floorList` | `floorList` ✅ |
| UC-01 + 策略通过 | 空 | 有且生效 | `floorList` | `floorList ∩ allow` ✅ |
| UC-01 + 策略无交集 | 空 | allow 与 floorList 无交集 | `floorList` | 失败 76260532 ✅ |
| UC-01 被访人无楼层 | 空 | 任意 | `floorList`=空 | 失败 76260531 ✅ |
| UC-02 无策略 | [A,B] | 无策略行 | [A,B] | [A,B] ✅ |
| UC-02 + 策略包含 | [A,B] | allow=[A,C] | [A,B] | [A] ✅ |
| UC-02 + 策略不包含 | [A,B] | allow=[C,D] | [A,B] | 失败 76260532 ✅ |
### 与当前实现的差异
| 场景 | 当前实现 | 重设计后 |
|------|---------|---------|
| UC-02 + 策略存在 | 绕过策略,按请求楼层开通 ❌ | 策略求交 ✅ |
| UC-02 + 策略不包含请求楼层 | 成功开通(本应拒绝)❌ | 失败 76260532 ✅ |
---
## 5. 错误码
| 错误码 | 触发条件 | 说明 |
|--------|---------|------|
| 76260531 | 候选楼层为空 | 被访人 `floorList` 为空,或有效楼层为空 |
| 76260532 | `candidate ∩ allow` 无交集 | 策略约束了可访楼层,但候选楼层全部不在允许范围内 |
| 76260533 | 策略配置错误 | `allow_zone_ids` 包含被访人无权限的 zoneId |
---
## 6. 日志规范
| 关键路径 | 日志内容 |
|---------|---------|
| 入口 | `businessId, personId, visitorId, requestFloorSize` |
| UC-01 | `调用方未传楼层,取被访人默认楼层 floorList=xxx` |
| UC-02 | `调用方已指定楼层,候选楼层=candidate` |
| 策略查询 | `查询组织 orgIds=xxx` |
| 策略命中 | `找到启用策略 policyId=xxx allow=xxx` |
| 策略未命中 | `未找到启用策略,使用候选楼层原值` |
| 求交成功 | `策略生效,最终楼层=effective` |
| 求交为空 | `候选楼层与策略无交集,返回 76260532` |
| 空楼层 | `无可用楼层,返回 76260531` |
---
## 7. 实施范围
| 变更点 | 影响 | 风险 |
|--------|------|------|
| UC-02 增加 `personService.detail()` 调用 | 多一次 RPC(数百微秒) | 低 |
| UC-02 增加策略求交逻辑 | 对传入楼层做过滤 | 中——调用方可能不符合预期 |
| 删除 `callerProvidedFloors` 分支 | 简化代码结构 | 低 |
> **兼容性提醒**:UC-02 行为改变意味着:之前能开通的请求(传入策略允许范围外的楼层)现在会失败。需通知集成方。