Files
starRiverProperty/docs/superpowers/specs/2026-05-10-floor-policy-trigger-optimization.md

352 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 楼层策略触发优化:仅访客邀约场景执行策略
**日期**: 2026-05-10
**状态**: 方案评估(未实施)
**关联**: [component-org 日志分析](./2026-05-10-component-org-log-analysis.md)
---
## 一、问题定义
### 1.1 现象
`TenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive()` 被嵌入 `ImgPersonServiceImpl.detail()` L670-672,导致**所有**调用 `POST /component/person/detail` 的请求都触发策略 DB 查询,而非仅访客邀约场景。
### 1.2 根因
`detail()` 是通用人员详情 API,被 7 个不同调用方消费,策略代码无条件嵌入其中,无法区分调用方意图:
```
POST /component/person/detail ← 唯一入口,无法区分调用方
├─ 访客邀约页 ✅ 需要策略 → 当前正确
├─ 人员管理详情 ❌ 不需要 → 多余 DB 查询
├─ addVisitor UC-01 ✅ 需要策略 → 当前正确
├─ addVisitor UC-02 ❌ 不需要 → 多余 DB 查询(不消费 floorList
├─ CRK 访客注册 ✅ 需要策略 → 当前正确
├─ CRK 门禁识别 ❌ 不需要 → 多余 DB 查询
└─ 电梯设备同步 ❌ 不需要 → 多余 DB 查询
```
### 1.3 影响评估
| 指标 | 当前值 | 评估 |
|------|--------|------|
| 调用频率 | ~1 次/分钟 | 低,无性能风险 |
| 策略查询耗时 | < 5ms(索引查询) | 可忽略 |
| 多余调用占比 | ~60%(7 个调用方中 4 个不需要) | 架构不精确 |
**结论**:当前频率下无性能问题,但架构语义不精确,需要优化。
---
## 二、方案评估
### 2.1 涉及的关键代码路径
```
┌─ interface 层 ───────────────────────────────────────────────────┐
│ │
│ DetailImgPersonParam (component-organization-interface) │
│ public class DetailImgPersonParam { │
│ private String id; │
│ private String businessId; │
│ } │
│ │
│ PersonDetailParam (intelligent-cwoscomponent-interface) │
│ public class PersonDetailParam implements Serializable { │
│ private String id; │
│ private String businessId; │
│ } │
└───────────────────────────────────────────────────────────────────┘
┌─ 服务层 ─────────────────────────────────────────────────────────┐
│ │
│ ImgPersonServiceImpl.detail() L670-672: │
│ Optional<List<String>> replacementFloors = │
│ this.tenantVisitorFloorPolicyService │
│ .replacementZoneIdsIfPolicyActive( │
│ result.getOrganizationIds()); │
│ │
│ PersonRuleServiceImpl.addVisitor() L172-175: │
│ PersonDetailParam detailParam = new PersonDetailParam(); │
│ detailParam.setId(param.getPersonId()); │
│ detailParam.setBusinessId(context.getCompany().getCompanyId()); │
│ CloudwalkResult<PersonResult> detailResult = │
│ this.personService.detail(detailParam, context); │
└───────────────────────────────────────────────────────────────────┘
```
---
### 方案 A:新增 `skipFloorPolicy` 字段(⭐ 推荐)
#### 改动内容
**Step 1**: `DetailImgPersonParam` 新增字段
```java
// cwos-component-organization-interface/.../DetailImgPersonParam.java
public class DetailImgPersonParam {
private String id;
private String businessId;
private Boolean skipFloorPolicy; // ★ 新增: true=跳过策略查询
// getter / setter
public Boolean getSkipFloorPolicy() {
return skipFloorPolicy;
}
public void setSkipFloorPolicy(Boolean skipFloorPolicy) {
this.skipFloorPolicy = skipFloorPolicy;
}
}
```
**Step 2**: `PersonDetailParam` 新增字段
```java
// intelligent-cwoscomponent-interface/.../PersonDetailParam.java
public class PersonDetailParam implements Serializable {
private String id;
private String businessId;
private Boolean skipFloorPolicy; // ★ 新增
// getter / setter
}
```
**Step 3**: `ImgPersonServiceImpl.detail()` 加条件判断
```java
// L670 修改前:
Optional<List<String>> replacementFloors =
this.tenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive(
result.getOrganizationIds());
// L670 修改后:
Optional<List<String>> replacementFloors = Optional.empty();
if (!Boolean.TRUE.equals(param.getSkipFloorPolicy())) {
replacementFloors = this.tenantVisitorFloorPolicyService
.replacementZoneIdsIfPolicyActive(result.getOrganizationIds());
}
if (replacementFloors.isPresent()) {
// ... 现有替换逻辑不变
}
```
**Step 4**: elevator 侧 addVisitor UC-02 显式跳过
```java
// PersonRuleServiceImpl.addVisitor() L172 之后加:
PersonDetailParam detailParam = new PersonDetailParam();
detailParam.setId(param.getPersonId());
detailParam.setBusinessId(context.getCompany().getCompanyId());
// ★ UC-02: 调用方已显式指定楼层,无需策略查询
if (!CollectionUtils.isEmpty(param.getFloorIds())) {
detailParam.setSkipFloorPolicy(true);
}
CloudwalkResult<PersonResult> detailResult =
this.personService.detail(detailParam, context);
```
#### 改动总结
| 文件 | 改动 | 行数 |
|------|------|------|
| `DetailImgPersonParam.java` | 新增 `skipFloorPolicy` 字段 | +5 |
| `PersonDetailParam.java` | 新增 `skipFloorPolicy` 字段 | +5 |
| `ImgPersonServiceImpl.java` | L670 条件判断包裹 | +3 |
| `PersonRuleServiceImpl.java` | addVisitor UC-02 传参 | +3 |
| **合计** | **4 文件,~16 行** | |
#### 评估
| 维度 | 评价 |
|------|------|
| 改动量 | ★★★★★ 极小(4 文件,~16 行) |
| 向后兼容 | ✅ `Boolean` 默认 null/false = 策略执行,与当前行为一致 |
| 语义精确性 | ✅ 调用方显式声明意图,类型安全 |
| 可测试性 | ✅ 可写单测验证 `skipFloorPolicy=true` 时不走策略 |
| 渐进式 | ✅ 不传参的调用方行为不变,后续按需传 `true` |
| Feign 兼容 | ✅ Feign 序列化自动支持 Boolean 字段 |
| 风险 | 低:interface 层加字段不破坏契约 |
---
### 方案 B:利用 CloudwalkCallContext.notes 隐式传参(不推荐)
```java
// elevator 侧:
context.getNotes().put("skipFloorPolicy", "true");
// ImgPersonServiceImpl 侧:
Map<String, String> notes = context.getNotes();
boolean skip = "true".equalsIgnoreCase(notes.get("skipFloorPolicy"));
```
| 维度 | 评价 |
|------|------|
| 改动量 | ★★★★★ 仅改 service 层 |
| 语义精确性 | ❌ 隐式语义,字符串 key,无类型安全 |
| 可发现性 | ❌ 调用方无法从 API 签名得知此能力 |
| 可维护性 | ❌ key 拼写错误 → 静默失败,难以排查 |
| 风险 | 中:notes 是 Map<String,String>,无编译期检查 |
---
### 方案 C:拆分为独立端点(不可行)
```
POST /component/person/detail → 不执行策略
POST /component/person/detail/visitor → 执行策略
```
| 维度 | 评价 |
|------|------|
| 语义精确性 | ✅ URL 级区分 |
| 改动量 | ❌ 大:新增 Controller + Service 方法 |
| 约束冲突 | ❌ 违反"对外接口不可扩展"原则 |
| 代码重复 | ❌ detail() 体量大(100+ 行),拆分造成重复 |
---
### 方案 D:保持现状
| 维度 | 评价 |
|------|------|
| 改动量 | 零 |
| 性能影响 | 策略查询 < 5ms,当前频率 ~1次/分钟,可忽略 |
| 设计意图 | 设计文档规定 detail() 是 floorList 唯一权威来源 |
---
## 三、推荐结论
### 采用方案 A:新增 `skipFloorPolicy` 字段
**理由**
1. **改动最小**:4 文件 ~16 行,不破坏任何现有契约
2. **语义明确**:字段名自解释,调用方显式声明意图
3. **向后兼容**:默认 null = 策略执行,不传参的调用方行为不变
4. **渐进优化**:后续 CRK、前端等调用方可按需传入 `skipFloorPolicy=true`
5. **符合约束**:不新增接口,不改变语义,仅扩展参数
### 优化效果预估
| 调用方 | 当前行为 | 优化后 | 策略查询减少 |
|--------|---------|--------|-------------|
| 前端访客邀约 | ✅ 执行策略 | 不变 | — |
| 前端人员管理 | ❌ 多余查询 | skipFloorPolicy=true | ✅ 消除 |
| addVisitor UC-01 | ✅ 执行策略 | 不变 | — |
| addVisitor UC-02 | ❌ 多余查询 | skipFloorPolicy=true | ✅ 消除 |
| CRK 访客注册 | ✅ 执行策略 | 不变 | — |
| CRK 门禁识别 | ❌ 多余查询 | skipFloorPolicy=true(后续) | ✅ 消除 |
| 电梯设备同步 | ❌ 多余查询 | skipFloorPolicy=true(后续) | ✅ 消除 |
**预估**elevator addVisitor UC-02 优化立即生效,后续逐步覆盖其他调用方,最终减少约 60% 的无意义策略查询。
### 实施顺序
```
Phase 1(核心优化):
1. DetailImgPersonParam + skipFloorPolicy 字段
2. PersonDetailParam + skipFloorPolicy 字段
3. ImgPersonServiceImpl.detail() 加条件判断
4. PersonRuleServiceImpl.addVisitor() UC-02 传参
Phase 2(渐进覆盖):
5. 前端人员管理 → skipFloorPolicy=true
6. CRK 门禁识别 → skipFloorPolicy=true
7. 电梯设备同步 → skipFloorPolicy=true
```
---
## 四、附录:完整改动预览
### 4.1 DetailImgPersonParam.java
```java
public class DetailImgPersonParam {
private String id;
private String businessId;
private Boolean skipFloorPolicy;
// ... 原有 getter/setter ...
public Boolean getSkipFloorPolicy() {
return skipFloorPolicy;
}
public void setSkipFloorPolicy(Boolean skipFloorPolicy) {
this.skipFloorPolicy = skipFloorPolicy;
}
}
```
### 4.2 PersonDetailParam.java
```java
public class PersonDetailParam implements Serializable {
private static final long serialVersionUID = 7666677873099106082L;
private String id;
private String businessId;
private Boolean skipFloorPolicy;
// ... 原有 getter/setter ...
public Boolean getSkipFloorPolicy() {
return skipFloorPolicy;
}
public void setSkipFloorPolicy(Boolean skipFloorPolicy) {
this.skipFloorPolicy = skipFloorPolicy;
}
}
```
### 4.3 ImgPersonServiceImpl.java L667-685
```java
int rawFloorCount = floorList.size();
this.logger.info("[DETAIL] personId={} listByImageId returned {} zones: {}",
param.getId(), rawFloorCount, floorList);
// ★ 仅当未显式跳过策略时才执行策略查询
Optional<List<String>> replacementFloors = Optional.empty();
if (!Boolean.TRUE.equals(param.getSkipFloorPolicy())) {
replacementFloors = this.tenantVisitorFloorPolicyService
.replacementZoneIdsIfPolicyActive(result.getOrganizationIds());
}
if (replacementFloors.isPresent()) {
List<String> beforeReplacement = new ArrayList<>(floorList);
floorList = new ArrayList<>(replacementFloors.get());
zoneNames = buildCommaSeparatedFloorNames(businessId, floorList);
this.logger.info("[DETAIL-POLICY] personId={} orgIds={} floorList REPLACED: {}→{}",
param.getId(), result.getOrganizationIds(), beforeReplacement, floorList);
} else {
this.logger.debug("[DETAIL-POLICY] personId={} orgIds={} {}",
param.getId(), result.getOrganizationIds(),
Boolean.TRUE.equals(param.getSkipFloorPolicy())
? "policy skipped by caller" : "no policy matched, using raw floorList");
}
```
### 4.4 PersonRuleServiceImpl.java L172-175
```java
PersonDetailParam detailParam = new PersonDetailParam();
detailParam.setId(param.getPersonId());
detailParam.setBusinessId(context.getCompany().getCompanyId());
// UC-02: 调用方已显式指定 floorIds,此时 floorList 不被消费,跳过策略查询
if (!CollectionUtils.isEmpty(param.getFloorIds())) {
detailParam.setSkipFloorPolicy(true);
}
CloudwalkResult<PersonResult> detailResult =
this.personService.detail(detailParam, context);
```