Initial commit: reorganized source tree

- backend/: 13 Maven modules (cw-elevator-application, cloudwalk-cloud, intelligent-cwoscomponent, ninca-crk, etc.)
- frontend/: 4 Vue projects (elevator-front, cwos-portal, alarm-front, front_acs) + decompiled + scripts
- scripts/: build, test-env, tools (Docker Compose, service templates, API parity)
- docs/: AGENTS.md, superpowers specs, architecture docs
- .gitignore: standard Java/Maven exclusions

Moved from legacy maven-*/ root layout to backend/ organized structure.
This commit is contained in:
hpd840321
2026-05-09 09:00:12 +08:00
commit 7b2bd307f1
7260 changed files with 612980 additions and 0 deletions
@@ -0,0 +1,115 @@
# 租户访客楼层策略 — 代码重构实施指南
> **目标**:在**不破坏对外 HTTP / Feign 契约**的前提下,实现规范中的 **「组织 `detail` 内以 `allow_zone_ids` 替代 `PersonResult.floorList`」**,使**邀约页、UC-01 派梯**与**租户策略**一致。
> **依据**:[访客邀约与派梯楼层一致性梳理](访客邀约与派梯楼层一致性梳理.md)、[迁入组织规格](../superpowers/specs/2026-05-06-tenant-visitor-policy-organization-implementation.md)(以下简称 **《组织规格》**)。
> **当前状态**:电梯 **`PersonRuleServiceImpl#addVisitor`** 已**不**读策略表、**不**做 ∩。组织侧已落地 **`TenantVisitorFloorPolicyService`** + **`ImgPersonServiceImpl#detail` / `page(isVisitor)`** 的 **`allow_zone_ids` 替代**;部署组织库需执行 **`docs/sql/organization_tenant_visitor_floor_policy.sql`**。电梯工程内已无 **`TenantVisitorFloorPolicyDao`**(死代码已删)。
---
## 1. 重构原则(必须遵守)
| 原则 | 说明 |
|------|------|
| **策略语义** | 仅 **「替代」**:命中策略时 **`floorList` = 配置中的 `allow_zone_ids` 解析结果****不要**与 `listByImageId` 结果做 **∩** 作为规范主路径。 |
| **唯一对外楼层权威** | 消费「被访人可派梯/可邀约楼层」**必须**走 **`PersonService.detail``floorList`**(经 Intelligent → 组织)。 |
| **电梯侧** | **不**新增「再算一遍策略」;**不**恢复 `TenantVisitorFloorPolicyDao` 参与 `addVisitor` 有效楼层计算。 |
| **接口兼容** | 《组织规格》约定:`cwos-component-organization-interface` **不**为策略新增公开 DTO/方法时,策略仅作为**服务层内部实现**;对外仍只通过现有 `detail` 返回里的 `floorList` / `floorNames` 体现。 |
| **UC-02** | 显式 `floorIds` 仍由调用方负责与业务单一致;若需 **⊆ 策略** 的硬约束,在 **BFF** 实现,或单独立项改电梯**且**经评审。 |
---
## 2. 分阶段实施任务
### 阶段 A — 数据与组织工程骨架
1. **策略表落库位置**(二选一,推荐 **组织库** 为唯一主库)
- **推荐**:在 **组织服务使用的 MySQL 库** 中创建与《组织规格》一致的表结构(字段含 `business_id``policy_type``allow_zone_ids` JSON、`enabled``building_id` 可空等)。
- **迁移期**:若生产数据仍在**电梯库** `tenant_visitor_floor_policy`,做 **一次性迁移脚本** + 运维切换窗口;迁移完成后电梯侧 Dao **仅删除调用**,不必双写。
2. **组织模块新增(均在 `maven-ninca-common-component-organization`**
- Entity / Mapper / XML`tenant_visitor_floor_policy` CRUD 或至少 **按 `business_id` + enabled** 查询租户默认行(`building_id IS NULL`)。
- **`TenantVisitorFloorPolicyService`**(命名可依项目惯例):
- `boolean isPolicyActive(String businessId)``Optional<PolicyRow> findTenantDefault(String businessId)`
- `List<String> parseAllowZoneIds(String json)`(健壮解析,非法 JSON → 视为未启用或记录告警)。
- **不做**:在 interface 模块暴露新 REST(除非产品明确要求管理 API)。
### 阶段 B — 在 `ImgPersonServiceImpl#detail` 接入替代逻辑(核心)
**文件**`cwos-component-organization-service/.../ImgPersonServiceImpl.java`,方法 **`detail`**。
**插入点**:在 **`elevatorFeignClient.listByImageId` 成功**、已根据 `images` 遍历得到 **`floorList` / `floorNames`(原始)** 之后;在 **`result.setFloorList` / `setFloorNames`** 之前增加分支:
```
伪代码:
原始列表 = 当前遍历 listByImageId 得到的 floorList, floorNames
若 TenantVisitorFloorPolicyService 对 businessId(及必要时 organizationIds)判定「启用且 allow 非空」:
floorList := allow_zone_ids 解析后的 zoneId 列表(顺序:与 JSON 数组顺序一致)
floorNames := 需与 floorList 对齐展示
选项 1:配置侧同时存 id→name 映射(扩展列或独立字典表)
选项 2:对 allow 中每个 zoneId 调现有 Zone Feign 批量查名称(注意批量与超时)
选项 3:若产品允许仅展示 zoneId,则 floorNames 可与 zoneId 同步占位(不推荐体验)
否则:
保持现有原始 listByImageId 语义
result.setFloorList(...)
result.setFloorNames(...)
```
**注意**
- **`defaultFloor` / `floorName`(单人默认层展示)** 与 **`floorList`(通行可达层)** 语义不同,勿混写;参见 [08 文档](../../maven-cw-elevator-application/cw-elevator-application-service/docs/08-visitor-registration-and-elevator-auth.md)。
- **启用判定键**:与《组织规格》一致,以 **`business_id`**Header `businessid` / `companyId`)为主;若后续要 **按机构细分**,再扩展查询条件,不在本阶段默认实现。
### 阶段 C — 访客列表 `page(isVisitor)` 对齐(强烈建议)
**文件**:同一 `ImgPersonServiceImpl`,方法 **`page`**,分支 **`param.getIsVisitor()` 非空**。
- 现状:该分支内有 **星河湾 40F/6F** 等与 **`detail`** 不一致的展示逻辑。
- **建议**:在策略**命中**时 **优先应用与 `detail` 相同的替代结果**(或抽 **私有方法** `applyTenantVisitorFloorPolicy(resultRow, businessId, …)``detail``page` 共用),并 **跳过**与租户策略冲突的硬编码默认层块(参见《组织规格》§4.3)。
- 避免:邀约走 `detail`、列表走 `page` 时出现两套楼层。
### 阶段 D — 电梯侧清理(收尾)
1. **确认** `PersonRuleServiceImpl` **无** `TenantVisitorFloorPolicyDao` 注入与 ∩ 逻辑(当前梳理版本已符合则仅做静态检查)。
2. **删除或标注废弃**`cw-elevator-application-data`**`TenantVisitorFloorPolicyDao` / Mapper / XML** 若已不再被任何 Bean 引用 — **删除**可减少歧义;若迁移脚本仍需参考表结构,可保留 DDL 文档到 `docs/sql`,代码删除前 grep 全仓引用。
3. **电梯库表**:迁移完成后由 DBA **删表或归档**(按运维规范)。
### 阶段 E — UC-02 与集成(可选增强)
- **BFF**:派梯前读取邀约单 `floorIds`,调用 **`detail`** 得到 **`floorList`**(已含替代),校验 **`floorIds ⊆ floorList`**(集合意义),失败则拒绝派梯并返回明确错误码。
- **幂等**:邀约单号 + 派梯状态机由业务系统保证,电梯接口本身不变。
### 阶段 F — 测试与验收
| 场景 | 期望 |
|------|------|
| 未配置策略 | `detail.floorList` 与改造前 **listByImageId** 一致;UC-01 行为不变。 |
| 配置启用且 allow 非空 | `detail.floorList` **等于** allow 列表(替代);UC-01 开通层与邀约页一致。 |
| 关闭策略 / JSON 无效 | 回退原始语义;无启动报错。 |
| `page(isVisitor)` | 与 `detail` 策略表现一致(验收抽样)。 |
---
## 3. 风险与缓解
| 风险 | 缓解 |
|------|------|
| **`floorNames` 与 id 不对齐** | 替代后必须统一用 Zone 服务补全名称或扩展配置。 |
| **性能**detail 每次多一次 DB + 可选批量 Zone | 策略行缓存(短 TTL)或本地缓存按 `business_id`。 |
| **跨楼栋首层**(§4.8 | 产品确认单次 `effective` 是否允许多楼栋;否则约束邀约/UC-02 单层栋或拆分调用。 |
---
## 4. 推荐提交顺序(便于 Code Review
1. 组织库 DDL + Mapper + **PolicyService**(单测解析 JSON)。
2. **`detail` 接入替代** + 单元/集成测试(可 Mock Feign)。
3. **`page` 分支对齐**(若有)。
4. 电梯侧删除死代码 + 文档更新(本文 + 《一致性梳理》§6)。
5. 迁移脚本在生产前演练。
---
**文档版本**:与仓库实施同步更新;重大契约变更需同步修改《组织规格》并评审。
@@ -0,0 +1,293 @@
# 租户访客默认楼层:数据库配置阶段 — 详细技术设计
> **文档性质**:实施级技术设计(历史草案)。**现行规范(2026-05-06**:租户策略 **`allow_zone_ids` 仅在组织 `PersonService.detail` 以「**替代**」写入 `floorList`,电梯 **`addVisitor` 不透传策略表、不做 ∩**。下文若仍出现「求交 / ∩」,视为历史表述;以 [迁入组织组件规格](../superpowers/specs/2026-05-06-tenant-visitor-policy-organization-implementation.md) 为准。
> **产品依据**:[租户访客默认楼层技术产品方案](租户访客默认楼层技术产品方案.md)(策略类型、安全底线、AC 验收)。
> **流程依据**:[访客注册与派梯楼层业务流程走查](访客注册与派梯楼层业务流程走查.md)UC-01 / UC-02、`PersonRuleServiceImpl.addVisitor`)。
| 项目 | 内容 |
|------|------|
| 版本 | v0.1 草案(数据库阶段) |
| 适用工程 | `maven-cw-elevator-application`(电梯应用库;落库表位于**电梯库**或与电梯同数据源的业务库,以现网数据源划分为准) |
| 读者 | 后端开发、DBA、集成测试、运维 |
| **模型索引** | [租户组织人员访客-数据模型与用例](../architecture/租户组织人员访客-数据模型与用例.md)(仓库级 ER、`business_id` 与组织主键辨析) |
---
## 1. 背景与阶段目标
### 1.1 问题复述
多租户场景下,部分机构要求:在调用方**不传** `floorIds`(走 **UC-01**)时,访客派梯生效楼层**不得**简单等同于被访人组织侧 `floorList` 全集,而应**收敛**为机构允许的若干 `zoneId`(如固定接待层)。未做特殊要求的租户须与现网行为**完全一致**。
### 1.2 本阶段目标(范围边界)
| 目标 | 说明 |
|------|------|
| **配置方式** | 通过 **数据库表** 维护「哪些租户启用访客楼层策略及允许列表」;由实施/DBA/SQL 脚本录入,**不提供** 物业/客服可视化管理界面(后续阶段再做)。 |
| **行为(规范)** | 存在有效策略时,组织 **`detail`** 将 **`floorList` 替换为 `allow_zone_ids`(替代)**;电梯侧 **不再**对候选楼层与 allow 做 ∩。 |
| **兼容** | 表中**无**该租户配置、或 `enabled=0`、或 `allow_zone_ids` 为空:与现网 **UC-01** 一致(仍使用组织返回的 `floorList` 全集)。 |
| **显式楼层** | 请求体已带**非空** `floorIds`**UC-02**):电梯 **`addVisitor`** 以请求为准;策略约束应在 BFF/组织侧预先体现,**禁止**在规范文档中将 UC-02 描述为与 allow **求交**。 |
| **访客业务系统 / 登记页** | 本阶段**不要求**改第三方 BFF;但若登记页仍只拉组织 `floorList` 展示,则**展示可能与电梯最终开通楼层不一致** —— 见 **§8 风险与后续工作**。 |
### 1.3 非目标(明确排除)
- 物业管理端 CRUD 页面、审计日志界面、策略版本与登记单快照联动(产品方案 §2.7、§3.4 完整能力)。
- 组织侧收窄 `floorList`、Nacos 配置中心等其它路径(参见产品方案 §4.1 方案族)。
- 按楼栋 `building_id` 的多套策略并行(表结构可预留字段,本阶段查询规则见 **§4.3**)。
---
## 2. 设计原则与安全约束
1. **权限上界(不变量)**
租户策略仅表达**允许集合**;规范路径下 **`PersonService.detail` 输出的 `floorList` 在策略命中时即为 `allow_zone_ids`(替代后的权威列表)**,电梯 **`addVisitor` UC-01** 仅透传该列表。**禁止**再以「与被访人原始 `listByImageId` 结果求交」作为规范语义。
2. **替代后为空 / 配置无效**
若组织侧替代结果无法产生有效 `floorList`(或策略 JSON 无效),应在 **detail****addVisitor** 短路失败(如 **`76260531`**),禁止继续 `zone/page` 首元素等(避免 **UC-04** 类 NPE)。
3. **被访人无楼层**
组织 `detail` 返回的 `floorList`**null 或空列表**:应用层**短路失败**。
4. **数据源**
策略表以 **`businessId`(机构 ID** 与调用上下文 `CloudwalkCallContext.company.companyId` 对齐;所有查询必须带 `business_id` 条件,避免串租户。
---
## 3. 总体架构(本阶段)
```mermaid
flowchart LR
subgraph callers [调用方不变]
T[第三方 / intelligent]
end
subgraph elev [cw-elevator-application]
API["POST /elevator/person/add/visitor"]
SVC["PersonRuleServiceImpl.addVisitor"]
ORG["Feign PersonService.detail"]
DB[("tenant_visitor_floor_policy")]
end
subgraph orgsvc [组织服务]
DETAIL["POST /component/person/detail"]
end
T --> API --> SVC
SVC -->|floorIds 为空| ORG --> DETAIL
SVC -->|读策略| DB
SVC -->|后续现有逻辑| Z["zoneService.page 等"]
```
**职责划分**
- **策略存储与读取**:电梯应用访问本表(与现网 `image_rule_ref` 等表同一数据源即可,减少分布式事务)。
- **「访客系统只获取对应楼层」**:若「访客系统」指**登记页/第三方 BFF**,本阶段**不强制**其改代码;电梯侧已保证**开通结果**收敛。展示一致需后续 **预览接口或 BFF 同源计算**(产品方案 §2.3、§2.5)。
---
## 4. 数据模型设计
### 4.1 逻辑模型
与产品方案 §4.3 对齐,本阶段至少使用下列语义字段:
| 字段 | 类型(建议) | 必填 | 说明 |
|------|----------------|------|------|
| `id` | `VARCHAR(32)` PK | 是 | 主键,UUID |
| `business_id` | `VARCHAR(64)` | 是 | 机构/租户 ID,与 `businessId` 一致 |
| `policy_type` | `VARCHAR(32)` | 是 | 本阶段仅使用 **`INTERSECT_ALLOWLIST`**;预留 `HOST_FLOOR_LIST` 等枚举便于扩展 |
| `allow_zone_ids` | `TEXT` | 条件 | **JSON 数组**,元素为 `zoneId` 字符串;策略生效时须非空(见 §4.4) |
| `building_id` | `VARCHAR(64)` NULL | 否 | **租户级默认**:本阶段固定 **`NULL`** 表示全机构默认一条;非 NULL 预留给「按楼栋」扩展 |
| `enabled` | `TINYINT(1)` | 是 | `1` 启用,`0` 停用(等价于未配置) |
| `policy_version` | `BIGINT` | 是 | 每次配置变更递增;本阶段**可不**接入登记快照,但建议表结构一次到位 |
| `remark` | `VARCHAR(256)` | 否 | 实施备注(如「广发基金接待层」) |
| `created_by` / `updated_by` | `VARCHAR(64)` | 否 | 本阶段手工 SQL 可填运维账号 |
| `created_at` / `updated_at` | `BIGINT` | 否 | Unix 毫秒时间戳,与项目内其它表风格一致 |
**唯一约束建议**`UNIQUE KEY uk_biz_building (business_id, building_id)`
MySQL 中 `building_id` 为 NULL 时多行 `(biz, NULL)` 在部分版本下**可能**被唯一索引允许多条 —— **实施约束**:应用层查询 `WHERE business_id = ? AND building_id IS NULL ... LIMIT 1`,DBA 规范**每个租户仅一行**租户级策略。
### 4.2 DDL 草案(MySQL
```sql
CREATE TABLE tenant_visitor_floor_policy (
id VARCHAR(32) NOT NULL COMMENT '主键',
business_id VARCHAR(64) NOT NULL COMMENT '机构/租户 ID',
policy_type VARCHAR(32) NOT NULL DEFAULT 'INTERSECT_ALLOWLIST' COMMENT '策略类型',
allow_zone_ids TEXT NULL COMMENT 'JSON 数组,zoneId 列表',
building_id VARCHAR(64) NULL COMMENT '预留:楼栋维度;租户默认填 NULL',
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1 启用 0 停用',
policy_version BIGINT NOT NULL DEFAULT 1 COMMENT '配置版本号',
remark VARCHAR(256) NULL,
created_by VARCHAR(64) NULL,
created_at BIGINT NULL,
updated_by VARCHAR(64) NULL,
updated_at BIGINT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_biz_building (business_id, building_id),
KEY idx_business_enabled (business_id, enabled)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户访客默认楼层策略(与组织 floorList 求交)';
```
脚本落地路径:**`docs/sql/tenant_visitor_floor_policy.sql`**(本仓库已提供);亦可复制到各环境 Flyway/Liquibase 目录(以现网规范为准)。
**实现分支**`feature/tenant-visitor-floor-policy-db`(电梯 `PersonRuleServiceImpl.addVisitor` + `TenantVisitorFloorPolicyDao`)。
### 4.3 查询语义(应用层)
单条加载(租户级、启用、默认楼栋):
```text
SELECT ... FROM tenant_visitor_floor_policy
WHERE business_id = ?
AND enabled = 1
AND policy_type = 'INTERSECT_ALLOWLIST'
AND (building_id IS NULL OR building_id = '')
ORDER BY updated_at DESC, policy_version DESC
LIMIT 1;
```
若无行 → 不应用策略。
`allow_zone_ids` 为 NULL、空串、或 JSON 解析为空数组 → **本设计建议**:视为**未配置有效允许列表**,行为与「无策略」一致(避免误配导致全员无法开通);**可选变体**(需产品签字):解析为空则判为配置错误并拒绝 UC-01 —— 实施前二选一写死。
### 4.4 `allow_zone_ids` JSON 约定
- 格式:`["zoneId1","zoneId2"]`UTF-8,无 BOM。
- 元素与组织 `floorList`、空间服务 `zoneId` **同一套 ID**
- 实施前在**空间服务**或台账中核对「28 楼」等业务语言与 `zoneId` 映射(产品方案 §4.6)。
### 4.5 配置示例(实施 SQL
```sql
-- 示例:某租户仅允许访客到达 zone A、B(需替换为真实 business_id / zoneId
INSERT INTO tenant_visitor_floor_policy
(id, business_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, remark, created_at, updated_at)
VALUES
(REPLACE(UUID(),'-',''), 'REPLACE_WITH_BUSINESS_ID', 'INTERSECT_ALLOWLIST',
'["REPLACE_ZONE_A","REPLACE_ZONE_B"]', NULL, 1, 1, '实施录入:接待层', UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000);
```
停用某租户策略:`UPDATE ... SET enabled = 0, policy_version = policy_version + 1, updated_at = ...`
---
## 5. 应用层设计(电梯服务)
### 5.1 改造锚点
| 组件 | 路径 | 说明 |
|------|------|------|
| HTTP | `AcsPersonController#addVisitor` | 入参不变 |
| 服务 | `PersonRuleServiceImpl#addVisitor` | 在 **仅当** `floorIds` 为空分支内,于 `personService.detail` 取回 `floorList` 之后,插入策略加载与求交 |
| 数据访问 | 新建 `TenantVisitorFloorPolicyDao` + MyBatis `Mapper` | 仅 `select`;本阶段无写接口 |
### 5.2 控制流(伪代码)
```text
函数 addVisitor(param, context):
callerProvidedFloors = (param.floorIds 非空)
若 NOT callerProvidedFloors:
调用组织 detail(personId, businessId)
若 detail 失败: 返回 detail 的 code/message
hostFloors = PersonResult.floorList;若 null 则 []
若 hostFloors 为空:
返回业务错误「被访人无派梯楼层」
policy = DAO.selectEnabledTenantDefault(businessId) // building_id IS NULL
若 policy 存在且 allow_zone_ids 解析为非空列表 allow:
effective = [ z for z in hostFloors if z in allowSet ] // 保持 hostFloors 顺序
若 effective 为空:
返回业务错误「租户访客楼层与被访人授权无交集」
param.floorIds = effective
否则:
param.floorIds = hostFloors
// callerProvidedFloors: 不读策略表,param.floorIds 保持调用方原值
若 param.floorIds 为空:
返回业务错误「无可用派梯楼层」
后续沿用现有: zoneService.page、image_rule_ref、batchBind ...
```
### 5.3 与 UC 对照矩阵
| 场景 | `floorIds` 请求 | 表中是否有启用策略 | 期望生效列表 |
|------|-----------------|---------------------|--------------|
| UC-01 现网 | 空 | 否 | `floorList`(与现网一致) |
| UC-01 + 策略 | 空 | 是且 allow 非空 | `allow ∩ floorList` |
| UC-01 + 策略无交集 | 空 | 是且 allow 非空 | **失败**,明确错误码 |
| UC-01 被访人无层 | 空 | 任意 | **失败**(无 floorList |
| UC-02 | 非空 | 任意 | **以请求为准**(本阶段不读表) |
### 5.4 错误码与文案(建议)
| 错误码 | 场景 | 用户/集成方可见文案(中文示例) |
|--------|------|----------------------------------|
| 沿用组织失败码 | `detail` 失败 | 透传组织返回 |
| **新增** `76260531`(示例) | `floorList` 为空或求交后仍无可用楼层(可归并细分) | 无法为访客开通派梯:被访人无授权楼层或无可生效楼层 |
| **新增** `76260532`(示例) | 租户允许列表与被访人 `floorList` 无交集 | 无法为访客开通派梯:租户访客楼层策略与被访人授权楼层不一致,请联系管理员 |
> 注:具体码段需与现网 `762605xx` 资源文件及网关错误码规范对齐;实施时在 `messages` 或统一错误表中登记。
### 5.5 异常与日志
- `addVisitor` 外层 `catch (Exception)` 若吞掉 `ServiceException`,会导致业务错误码丢失;**须**对 `ServiceException` 单独 `rethrow` 或改为**提前返回** `CloudwalkResult.fail`,避免一律映射为 `76260530`
- 日志:在求交前后打 **INFO**`businessId``personId``visitorId`、策略 `id``policy_version`、**脱敏后的** `effective` 楼层数量或 zoneId 列表 —— 按合规要求决定是否打全量 ID)。
### 5.6 事务与性能
- 策略查询为只读,可与现有 `addVisitor` 事务边界一致;**无**跨服务写。
- QPS 不高场景可不加缓存;若加缓存:key=`businessId`+`building_id`,失效条件为策略 `UPDATE`(或 TTL + 版本号)。本阶段可省略。
---
## 6. 测试设计
### 6.1 单元测试
| 用例 ID | 输入 | 期望 |
|---------|------|------|
| T-U01 | 无策略,`floorList`=[a,b],不传 `floorIds` | 生效 [a,b] |
| T-U02 | 策略 allow=[b,c]`floorList`=[a,b] | 生效 [b] |
| T-U03 | 策略 allow=[x]`floorList`=[a,b] | 失败,码 76260532 类 |
| T-U04 | `floorList`=[] | 失败 |
| T-U05 | 调用方传 `floorIds`=[x] | 不读表,生效 [x] |
| T-U06 | `enabled=0` | 等同无策略 |
| T-U07 | `allow_zone_ids` 空或解析为空 | 等同无策略(若采用 §4.3 建议) |
### 6.2 集成 / 回归
- 未插表租户:全量回归 **UC-01 / UC-02**
- 插表租户:组织造数 `floorList` 与策略 allow 多种组合,验证 **AC-1AC-3**(产品方案 §3.5)。
---
## 7. 发布与运维
1. **发布顺序**:先 **DDL 上线**(表空不影响行为)→ 再发 **应用包**(读表逻辑)。
2. **配置变更**:仅 `INSERT/UPDATE`;重大变更递增 `policy_version` 并记录 `remark`
3. **回滚**:应用回滚后行为恢复现网;表数据可保留。
4. **监控**:对新增错误码计数;对「求交为空」单独告警便于实施核对 zoneId。
---
## 8. 风险、依赖与后续阶段
| 项 | 说明 |
|----|------|
| **登记页与开通不一致** | 第三方若仍只展示 `floorList` 全集,而电梯已求交,易产生客诉。缓解:**阶段 2** 提供管理 API + 预览接口,或 BFF 读**同一策略源**(若表在电梯库,可通过只读副本或同步配置解决跨库)。 |
| **直连电梯绕过 BFF** | 本阶段在电梯侧兜底,**直连仍受策略约束**(产品方案 §6.2 中「B+C」思路的部分收益)。 |
| **多楼栋** | 预留 `building_id`;待产品定义「按楼栋策略」后再扩展查询与 UI。 |
| **阶段 2(产品已规划)** | 物业管理端页面:策略维护、空间树多选、`policy_version` 审计、可选 **preview-floors** 与登记快照对齐(产品方案 §2.5、§4.4)。 |
---
## 9. 文档维护
| 项目 | 内容 |
|------|------|
| 路径 | `docs/business/租户访客默认楼层-数据库配置阶段技术设计.md` |
| **变更记录** | [租户访客默认楼层-数据库阶段变更记录](租户访客默认楼层-数据库阶段变更记录.md)(发版/评审用) |
| 修订触发 | `add/visitor` 契约变更;策略表字段变更;决定 UC-02 是否参与求交 |
| 关联 PR | 实现类、`Mapper.xml`、DDL 脚本与错误码资源文件 |
---
*本文档为数据库阶段详细设计;实施以现网分支、代码评审与安全评审结论为准。*
@@ -0,0 +1,133 @@
# 租户访客默认楼层(数据库配置阶段)— 变更记录
> **用途**:记录本仓库内已落地的代码与脚本变更,便于评审、发版说明与运维回溯。
> **设计依据**:[租户访客默认楼层-数据库配置阶段技术设计](租户访客默认楼层-数据库配置阶段技术设计.md)
> **产品依据**:[租户访客默认楼层技术产品方案](租户访客默认楼层技术产品方案.md)
> **流程依据**:[访客注册与派梯楼层业务流程走查](访客注册与派梯楼层业务流程走查.md)
---
## 1. 版本信息
| 项目 | 内容 |
|------|------|
| **Git 分支** | `feature/tenant-visitor-floor-policy-db` |
| **主提交** | `25cff4d``feat(elevator): 租户访客默认楼层策略表与 UC-01 求交` |
| **涉及工程** | `maven-cw-elevator-application``cw-elevator-application-data``cw-elevator-application-service` |
| **对外接口** | 无新增 HTTP;行为变更集中在 **`POST /elevator/person/add/visitor`**`PersonRuleServiceImpl.addVisitor` |
---
## 2. 变更摘要
1. **新增数据库表** `tenant_visitor_floor_policy`:按租户(`business_id`)配置访客允许 `zoneId` 列表(JSON),本阶段仅支持 **`INTERSECT_ALLOWLIST`** 与 **租户级**`building_id` 为空)策略行。
2. **电梯服务 `addVisitor`**:当请求 **未携带非空 `floorIds`**(走查 **UC-01**)时,在取得组织侧被访人 **`floorList`** 后,若存在启用策略且 `allow_zone_ids` 解析为非空数组,则 **`effective = floorList ∩ allow`****保持 `floorList` 原有顺序**);无交集则失败;无表/停用/允许列表为空或 JSON 无效则 **与现网一致** 使用全量 `floorList`
3. **当请求已携带非空 `floorIds`****UC-02**):**不读策略表**、不对入参求交,行为与变更前一致。
4. **健壮性**:组织 `detail` 失败透传错误码;`PersonResult` / `floorList` 为空返回 **`76260531`**;策略求交为空返回 **`76260532`**;最终 `floorIds` 仍为空时返回 **`76260531`**`addVisitor`**`ServiceException` 不再被泛化 `catch` 吞掉**,避免一律映射为 `76260530`
---
## 3. 文件清单
### 3.1 新增
| 路径 | 说明 |
|------|------|
| `docs/sql/tenant_visitor_floor_policy.sql` | 建表 DDL`CREATE TABLE IF NOT EXISTS`)及注释示例 |
| `maven-cw-elevator-application/cw-elevator-application-data/.../person/dto/TenantVisitorFloorPolicyDto.java` | 策略行 DTO |
| `maven-cw-elevator-application/cw-elevator-application-data/.../person/mapper/TenantVisitorFloorPolicyMapper.java` | MyBatis Mapper 接口 |
| `maven-cw-elevator-application/cw-elevator-application-data/.../person/mapper/TenantVisitorFloorPolicyMapper.xml` | `selectEnabledTenantDefault` SQL |
| `maven-cw-elevator-application/cw-elevator-application-data/.../person/dao/TenantVisitorFloorPolicyDao.java` | DAO 接口 |
| `maven-cw-elevator-application/cw-elevator-application-data/.../person/impl/TenantVisitorFloorPolicyDaoImpl.java` | DAO 实现 |
| `docs/business/租户访客默认楼层-数据库配置阶段技术设计.md` | 技术设计(含实现分支与 DDL 路径说明) |
### 3.2 修改
| 路径 | 说明 |
|------|------|
| `maven-cw-elevator-application/cw-elevator-application-service/.../person/impl/PersonRuleServiceImpl.java` | `addVisitor` 主流程;注入 `TenantVisitorFloorPolicyDao`;新增 `parseAllowZoneIds``intersectPreserveHostOrder` |
---
## 4. 行为对照表(验收速查)
| 场景 | `floorIds` 请求体 | 库中策略 | 期望 |
|------|-------------------|----------|------|
| UC-01 基线 | 空 / 未传 | 无行或 `enabled=0``allow_zone_ids` 空/无效 JSON | 与变更前一致:使用组织 **`floorList` 全集** |
| UC-01 + 策略 | 空 / 未传 | 有启用行且 allow 非空 | **`floorList ∩ allow`**(保序) |
| 策略无交集 | 空 / 未传 | allow 与 `floorList` 无共同元素 | **`CloudwalkResult.fail("76260532", ...)`** |
| 被访人无楼层 | 空 / 未传 | 任意 | **`76260531`**`floorList` 空或 null |
| 组织 detail 失败 | 空 / 未传 | 任意 | **透传** `detail` 的 code/message |
| UC-02 | **非空** `floorIds` | 任意 | **不读表**,按请求列表后续处理(与变更前一致) |
---
## 5. 数据库与配置
### 5.1 执行顺序
1. 在电梯应用(或与电梯同数据源)库执行 **`docs/sql/tenant_visitor_floor_policy.sql`**。
2. 对目标租户 **`INSERT`** 策略行(`business_id``allow_zone_ids` JSON、`enabled=1``policy_type='INTERSECT_ALLOWLIST'``building_id`**NULL**)。
3. 发布包含本分支的 **`cw-elevator-application`** 制品。
### 5.2 `allow_zone_ids` 格式
- JSON 数组字符串,例如:`["zoneId1","zoneId2"]`
- 解析失败或解析结果为空:**按无有效策略处理**(不打断 UC-01,避免误配导致全租户不可用);并打 **WARN** 日志。
### 5.3 查询规则(与 Mapper 一致)
- `business_id = ?`
- `enabled = 1`
- `policy_type = 'INTERSECT_ALLOWLIST'`
- `building_id IS NULL OR building_id = ''`
- `ORDER BY updated_at DESC, policy_version DESC LIMIT 1`
**运维约定**:同一租户、租户级策略(`building_id` 为空)**仅维护一行**,避免多行时仅命中最新一条。
---
## 6. 错误码与文案(待资源文件补全)
| 错误码 | 触发条件 | 建议中文文案(需在统一 messages 中配置) |
|--------|----------|------------------------------------------|
| `76260531` | 被访人无 `floorList`;或经处理后仍无可用楼层 | 无法为访客开通派梯:被访人无授权楼层或无可生效楼层 |
| `76260532` | 租户允许列表与被访人 `floorList` 交集为空 | 无法为访客开通派梯:租户访客楼层策略与被访人授权楼层不一致,请联系管理员 |
若未配置文案,`getMessage` 可能回退为码或占位,**发版前请在现网 i18n 资源中登记**。
---
## 7. 日志
- 当发生 **策略求交且结果非空** 时,打 **INFO**`businessId``personId``visitorId``policyId``policyVersion`、**生效楼层数量** `effectiveSize`(不落全量 zoneId,减少日志敏感面;若排障需要可临时调高或改为 DEBUG)。
---
## 8. 集成与运行依赖
| 项 | 说明 |
|----|------|
| **MyBatis** | 启动类需能扫描到 **`cn.cloudwalk.elevator.person.mapper`**(或与现有 `cn.cloudwalk.elevator` 下 Mapper 扫描策略一致);`Mapper.xml` 位于 `cw-elevator-application-data``src/main/java/.../mapper/*.xml`(与现有模块一致)。 |
| **表不存在** | 未执行 DDL 时访问表会抛 SQL 异常,通常落入 `76260530` 包装;**须先 DDL 再发应用**。 |
| **本地编译** | 聚合工程要求 **JDK 8** 跑 Mavenenforcer);若 `cw-elevator-application-common` 等模块因私服依赖未解析失败,与本次新增类无直接关系,需在完整依赖环境编译。 |
---
## 9. 明确未包含(后续迭代)
- 物业管理端页面、策略 CRUD REST、审计与 `policy_version` 写入登记快照。
- 登记页 / BFF **预览接口**与电梯开通结果的 **展示同源**(见技术设计 §8)。
-**`building_id`** 的多套策略(表字段已预留,查询与产品未定前不启用)。
---
## 10. 修订历史
| 日期 | 修订内容 |
|------|----------|
| 2026-04-24 | 初稿:对应分支 `feature/tenant-visitor-floor-policy-db`、提交 `25cff4d` |
---
*本文档随该功能后续提交持续更新;合并主干后建议在提交说明中引用本路径。*
@@ -0,0 +1,427 @@
# 租户访客默认楼层:技术产品方案
> **文档类型**:产品与工程技术方案(含**登记页数据项与接口溯源**、端到端闭环、数据模型与实施路线)。
> **前置阅读**:[访客注册与派梯楼层业务流程走查](访客注册与派梯楼层业务流程走查.md)(UC-01UC-06、对外 HTTP/Feign 与代码位置)。
> **关联工程**[Maven 聚合工程说明](../architecture/Maven聚合工程说明.md)`maven-cw-elevator-application`、`maven-intelligent-cwoscomponent` 等边界)。
> **排期(2026-04)**:凡依赖**物业管理端、登记页、第三方前端**改动的阶段,在取得可维护前端工程前**暂时跳过**;见 [docs/README 当前排期与范围](../README.md#当前排期与范围2026-04)。本文中**纯后端/接口/数据**部分仍可独立推进。
> **术语对齐(2026-05-06,强制)**:租户访客楼层策略的工程语义 **只能是「替代」**——在 **`PersonService.detail`** 将 **`allow_zone_ids` 写入 `PersonResult.floorList`**;电梯 **`addVisitor`** **不透传策略表、不与 allow 求交(∩)**。下文表格中出现的 **INTERSECT / ∩ / 求交** 多为历史方案或 **BFF 侧自选收窄**,**不等价**于现行后端规范;以 [迁入组织组件规格](../superpowers/specs/2026-05-06-tenant-visitor-policy-organization-implementation.md) 为准。
---
## 1. 背景与问题陈述
### 1.1 业务背景
多租户部署下,不同机构对**访客可达楼层**的策略不一致:
- **默认诉求**:访客开通派梯时,若业务侧**不显式传楼层列表**(与 [UC-01](访客注册与派梯楼层业务流程走查.md) 一致),系统仍应开通派梯,但**不应简单等同于被访人组织侧 `floorList` 全集**。
- **典型例子(产品化名)**:**「广发基金」类租户**要求:访客**默认仅可派梯至 28 楼**(或某固定接待层),而被访人本人在组织中可能仍具备多层办公权限。
### 1.2 `floorIds` 是谁的字段、空与非空分别代表什么
#### 1.2.1 字段归属(哪个主体的属性)
`floorIds` **不是**组织人员主数据里「被访人」的持久化字段,也**不是**访客人员档案在组织库里的固定属性;在现有实现中,它是 **电梯应用对外接口** 的一次请求参数。
| 项目 | 说明 |
|------|------|
| **载体** | HTTP **`POST /elevator/person/add/visitor`** 的请求体(`AcsPersonAddVisitorForm` / 服务层 `AcsPersonAddVisitorParam`,见 `maven-cw-elevator-application`)。 |
| **语义** | **本次「给访客开通派梯」操作**中,调用方希望电梯在哪些 **空间分区(`zoneId`)** 上为 `visitorId` 写入默认通行规则并绑图库。 |
| **主体** | 归属主体是 **「派梯开通请求」**(一次 API 调用),由 **调用方**(第三方 BFF、访客业务后台、或 `intelligent-cwoscomponent-rest` 等)填入;电梯服务**读取**后执行落库与远程绑图库。 |
因此:在集成文档中应写清——**`floorIds` 列出的是「本次开通涉及的楼层 zoneId 列表」,由调用方在请求时提供;缺省(空)时电梯才按约定去组织侧拉被访人的 `floorList` 补全。**
#### 1.2.2 `floorIds` 非空时:业务含义与电梯行为
当调用方传入 **非空** `floorIds` 时(走查文档中的 **UC-02:访客派梯-第三方指定楼层**):
| 项目 | 说明 |
|------|------|
| **业务含义** | 调用方已明确「本次访客只(或优先)开通这些楼层」的意图;常见来源包括:登记/审批流程里已算好的**生效楼层**、访客在允许范围内**勾选的多层**、或业务规则直接指定的接待层列表。 |
| **电梯侧行为** | **不再**为补楼层去调 **`POST /component/person/detail`**;直接使用请求中的 `floorIds` 作为待写入规则的楼层列表,后续步骤(`zone/page` 取楼栋、`getDefaultByZoneId`、写 `image_rule_ref``batchBind` 等)与「列表来自组织补全」时相同,**仅楼层列表来源不同**。 |
| **责任边界** | **正确性、合法性、与租户策略及被访人授权的一致性**由调用方负责(或通过 BFF 统一校验);电梯按列表执行;若需「仍不得超过被访人 `floorList`」的硬约束,须在 **BFF、intelligent 或电梯增强**中择一层实现(见 §4、§5)。 |
#### 1.2.3 为什么调用方会选择「非空」(不依赖空列表)
| 动机 | 说明 |
|------|------|
| **与 UI/审批结果一致** | 登记页或审批单已确定可访楼层,开通时**显式写入**同一列表,避免电梯侧再解释「默认」。 |
| **多租户策略在域外已算完** | 推荐做法(§4):在第三方 BFF 做 `tenantAllow ∩ hostFloors`,把结果作为 `floorIds` 传入,电梯无需理解租户表。 |
| **避免 UC-01 与产品语义冲突** | 不传 `floorIds` 时电梯等价于「被访人 `floorList` 全集」;若产品要「小于全集」,**应传非空** `floorIds`,否则现网电梯逻辑无法满足。 |
| **集成契约清晰** | 显式列表便于审计、重放、幂等比对(可与登记快照 `effective_preview_json` 逐字一致)。 |
#### 1.2.4 `floorIds` 为空时:电梯侧补全(与上文对照)
当前电梯侧实现(`PersonRuleServiceImpl.addVisitor`)在 **`floorIds` 为空或缺省** 时:
1. 调用组织 **`/component/person/detail`**`personId` + `businessId`);
2. 将返回的 **`PersonResult.getFloorList()`** 赋给本次执行使用的楼层列表;
3. **不使用** `defaultFloor` 单字段作为补全依据。
因此:**仅在「调用方不传 `floorIds`」这一模式下**,现网电梯无法表达租户级收敛;多租户默认接待层需在 **调用方传非空 `floorIds`****电梯/ BFF 增强兜底** 中实现(见全文 §4、§6)。
### 1.3 文档目标
| 维度 | 目标 |
|------|------|
| **产品** | 定义租户策略、角色权限、配置流程、登记/邀约 **UX 与开通结果一致**、验收标准。 |
| **闭环** | 从**第三方系统在注册/邀约流程中初始化访客登记页**开始,明确**每个数据项从哪个系统、哪类接口获取**;经提交、审批(若有)到 **`add/visitor`** 生效,并给出**一致性校验**与回读建议。 |
| **技术** | 可选架构、推荐组合、数据模型与接口草案、改造边界、安全求交、**多租户默认访客楼层的实现路径综合评估**。 |
---
## 2. 端到端闭环:从第三方登记页初始化到派梯生效
本章回答:**登记页打开时要拉哪些数据、按什么顺序调接口、算出什么给 UI 存什么草稿、审批后如何与电梯开通对齐**,从而形成可审计、可测试的闭环。
### 2.1 闭环目标
| 目标 | 说明 |
|------|------|
| **同源计算** | 「页面上展示的预计可派梯楼层」与「最终调用 `add/visitor``floorIds`(或电梯兜底前的逻辑输入)」由**同一套规则函数**得出,避免两套逻辑漂移。 |
| **接口可追溯** | 每个 UI 字段可映射到**具体外部接口或本地配置**;本仓库已明确的接口见走查文档 §3~§4。 |
| **租户可配置** | `businessId`(机构 ID)贯穿上下文;租户策略变更后,**进行中的登记单**行为需在产品中定义(见 §2.7)。 |
### 2.2 阶段划分(第三方视角)
| 阶段 | 名称 | 参与系统 | 产出 |
|------|------|----------|------|
| **P0** | 登记页/邀约页 **初始化** | 第三方 + 组织 + 空间 +(可选)租户策略服务 | 表单默认值、`effectiveFloorsPreview`、可选楼层展示文案 |
| **P1** | 用户填写与 **校验** | 第三方前端 + 后端 | 通过校验的草稿 DTO |
| **P2** | **提交**登记/发起邀约 | 第三方 DB、工作流引擎 | 业务主键、状态机(待审批/已登记) |
| **P3** | **审批**(若存在) | 第三方审批流 | 通过/驳回;通过则触发 P4 |
| **P4** | **组织侧** 写入访客人员、拿到 `visitorId` | 组织/访客模块(**不在本仓库**,以实际 API 为准) | `visitorId` |
| **P5** | **派梯开通** | 第三方或 intelligent → **电梯** `POST /elevator/person/add/visitor` | 规则写入、图库绑定 |
| **P6** | **回读与告知** | 第三方查询电梯/通行结果或异步任务状态 | 与 P0 展示一致的「已开通楼层」说明 |
### 2.3 访客登记/邀约页:数据项定义与数据来源
下表面向**第三方业务系统**实现同学:列的是**逻辑字段**;物理表名由各自系统定义。接口路径以走查文档为准;**组织人员搜索**(选被访人)在走查中未展开,行内标注为**待对接清单**。
| 逻辑数据项 | 典型 UI 表现 | 数据来源 | 接口或服务(已知/占位) | 关键入参 / 出参 | 与租户楼层关系 |
|-------------|--------------|----------|---------------------------|-----------------|----------------|
| **租户/机构 ID** | 隐藏或顶部切换 | 登录态 / 网关注入 | 上下文 `businessId`,与 `CloudwalkCallContext.company` 一致 | `businessId` | 策略表主键维度 |
| **被访人 `personId`** | 选人组件 | **组织人员检索**(具体路径以 `ninca-common-component-organization` 或网关文档为准) | 占位:`POST .../person/page` 或等价检索 API | 选中行 → `personId` | 后续 `detail` 入参 |
| **被访人姓名/部门等展示** | 选人后回显 | 同上检索结果或二次 `detail` | 检索结果字段 **或** `POST /component/person/detail` | `PersonResult` 展示字段 | 非楼层核心 |
| **被访人授权楼层 ID 列表 `hostFloors`** | 一般不直接展示 ID | **组织**(与电梯 UC-01 同源) | **`POST /component/person/detail`** | 请求:`id`=被访人 `personId``businessId`;响应:`PersonResult.floorList` | 求交上界 |
| **`defaultFloor` / `floorInfoList` 等** | 若产品要展示「默认办公层」文案 | **组织** `detail` 同响应 | 同上 | `PersonResult.defaultFloor``floorInfoList` | **电梯补全不用 `defaultFloor`**;若 UI 用其展示,须与 `floorList` 语义对齐(走查 §8 风险 1) |
| **空间树/楼层名称** | 楼层中文名、楼栋 | **空间服务** `ninca-common` | **`POST /sysetting/zone/tree`**、**`POST /sysetting/zone/page`** | 用 `hostFloors``zoneId` **反查** `zoneName`、楼栋 | 仅展示增强 |
| **租户访客楼层策略** | 管理员配置页;登记页可不直接展示 | **租户策略存储**(自建表或配置中心) | 草案 §4.3 管理 API **或** 第三方本地表 + 管理端 | `policy_type``allow_zone_ids`、版本号 | **与 `hostFloors` 求交**得预览 |
| **`effectiveFloorsPreview`** | 「本次访问预计可开通派梯的楼层」 | **计算字段**(禁止手写死) | `computeEffective(policy, hostFloors, userOverride?)` | 输出 `List<zoneId>`,可附 `displayNames` | **必须与 P5 输入一致** |
| **访客可选楼层(若产品允许访客改)** | 多选框,默认选中 `effectiveFloorsPreview` | `effectiveFloorsPreview` 的子集 | 前端约束:用户勾选 ⊆ 预览集 | 提交 `userSelectedZoneIds` | 高敏租户可关闭多选 |
| **访客姓名、证件、事由、到访时间等** | 表单 | 用户输入 + 机构规则 | 第三方本地 | — | 与楼层正交 |
| **人脸/照片** | 采集/上传 | 用户 + 第三方存储 | 组织图库/人像接口以实际为准 | — | 走查未展开 |
| **策略版本号 `policyVersion`** | 可不展示 | 租户策略表 | 读取策略时返回 | 写入登记草稿 | 审计与回放 |
| **登记单草稿持久化** | — | 第三方 DB | 本地表建议字段:`draft_id, business_id, host_person_id, host_floors_snapshot_json, policy_snapshot_json, effective_preview_json, policy_version, status` | P5 时读出 `effective_preview` 或重算 | 支持审批前后回放 |
**不在本仓库的接口**(须在集成清单中单独列出):访客人员创建、`visitorId` 发放、审批流回调 URL、人像入库等——闭环在 P4 前依赖这些能力,本文仅要求:**P5 的 `floorIds` 与 P0 预览同源**。
### 2.4 接口编排顺序(推荐:登记页初始化一次请求链)
第三方**后端聚合接口**(推荐由自有 **BFF** 实现,避免前端直连多枚令牌)建议内部按序调用:
```
1) 校验 businessId、操作者权限
2) GET 租户访客楼层策略(若无则视为 HOST_FLOOR_LIST
3) POST /component/person/detailid=被访人 personId, businessId
→ 得 hostFloors = floorList(可能 null/空)
4) computeEffective(policy, hostFloors, requestedFromClient=null)
→ 若空:返回明确错误码,登记页展示「无法为该被访人开通访客派梯,请联系管理员」
5) 可选:POST /sysetting/zone/page 或 tree 子集调用,将 zoneId 映射为 zoneName / 楼栋
6) 返回给前端:{ hostPerson, effectiveFloorsPreview, displayNames, policyVersion, warnings }
```
前端**仅展示**第 6 步结果,**不在前端重复实现求交**(防篡改可签短期 token 或仅信服务端字段)。
### 2.5 可选:统一「预览」能力(降低集成方重复劳动)
可在 **第三方 BFF****intelligent** 增加只读接口(路径示例):
| 方法 | 路径(示例) | 入参 | 出参 |
|------|----------------|------|------|
| `POST` | `/api/visitor/register/preview-floors` | `businessId`, `hostPersonId`, 可选 `userSelectedZoneIds` | `hostFloors`, `tenantPolicy`, `effectiveZoneIds`, `displayFloors[]`, `policyVersion`, `canSubmit` |
**契约**`effectiveZoneIds` 的计算规则须与 P5 调用 `add/visitor` 时组装的 `floorIds` **完全一致**(同一服务端模块单元测试)。
### 2.6 P5 派梯开通:与登记页的衔接
| 路径 | 做法 |
|------|------|
| **推荐(方案 B** | 审批通过后,从登记单读出 **快照**或 **用相同函数重算**(若策略允许重算):组装 **`floorIds` = `effectiveZoneIds`**,调用 **`POST /elevator/person/add/visitor`**(或 `ElevatorPersonFeignClient.addVisitor`),走 **UC-02**。 |
| **兜底(方案 C/D** | 若历史集成方坚持不传 `floorIds`,则由电梯或 intelligent 在服务端求交(见 §4);登记页仍应按 §2.3 展示**与兜底后一致**的预览(通过同一预览 API 或文档约定算法)。 |
**时间参数**`begVisitorTime` / `endVisitorTime` 与走查一致,须与登记单有效期对齐。
### 2.7 闭环一致性:策略中途变更、重试与幂等
| 场景 | 产品建议 |
|------|----------|
| 管理员在**登记草稿未提交**时修改策略 | 下次打开登记页重新走 P0,**以新策略为准**。 |
| 草稿已提交、**待审批**期间策略变更 | 二选一并在需求中写死:**(a)** 审批节点展示「策略已变更请重审」并阻塞直到人工确认;**(b)** 以提交时刻 `policyVersion` **冻结**计算结果,审批通过仍按旧结果开通(需评估合规)。 |
| **审批通过瞬间**策略变更 | P5 使用**出队消息中的快照** `effective_preview_json`,避免 TOCTOU。 |
| 重复点击开通 | `visitorId` + 时间窗幂等;与电梯侧是否已存在规则引用需对齐(可查电梯侧人员规则再调,属增强项)。 |
### 2.8 端到端泳道图(闭环)
```mermaid
sequenceDiagram
participant U as 访客用户
participant FE as 第三方前端
participant BFF as 第三方BFF
participant Pol as 租户策略存储
participant Org as 组织 person/detail
participant Zone as 空间 zone/tree|page
participant Reg as 第三方登记库
participant Apv as 审批流
participant OrgW as 组织写访客
participant Elv as 电梯 add/visitor
U->>FE: 打开登记页
FE->>BFF: 初始化(被访人personId)
BFF->>Pol: 读策略(businessId)
BFF->>Org: detail(personId)
Org-->>BFF: floorList
BFF->>BFF: computeEffective
opt 展示楼层名
BFF->>Zone: page/tree(zoneIds)
Zone-->>BFF: names
end
BFF-->>FE: 预览+policyVersion
U->>FE: 填写并提交
FE->>BFF: 提交草稿
BFF->>Reg: 持久化快照
Reg-->>FE: draftId
opt 需审批
BFF->>Apv: 发起
Apv-->>BFF: 通过+token
end
BFF->>OrgW: 创建访客→visitorId
BFF->>Elv: add/visitor(visitorId,personId,floorIds=effective,...)
Elv-->>BFF: 成功/失败
BFF-->>FE: 结果与楼层说明
```
---
## 3. 产品方案
### 3.1 目标用户与角色
| 角色 | 职责 |
|------|------|
| **平台/租户管理员** | 在本机构内配置「访客默认楼层策略」、维护允许的 `zoneId` 列表或与楼栋的映射、查看审计日志。 |
| **被访人员工** | 日常办公权限仍在组织侧维护;不强制要求其理解访客策略。 |
| **访客** | 通过邀约/登记获得限时通行;仅应看到**实际可派梯楼层**(与开通结果一致)。 |
| **运维/实施** | 将「28 楼」等业务语言映射为空间服务中的 **`zoneId`**,并录入策略或导入模板。 |
### 3.2 用户故事(示例)
1. **作为租户管理员**,我希望为本机构配置「访客默认仅开放指定楼层集合」,以便满足物业/合规对访客活动范围的要求。
2. **作为租户管理员**,我希望可选策略为「与员工授权楼层求交」或「固定楼层但必须仍在员工授权内」,以便在**不放大访客权限**的前提下简化配置。
3. **作为访客业务系统**,在调用电梯开通接口时,我可以在不传 `floorIds` 的情况下仍得到符合租户策略的派梯结果(若采用电梯或 BFF 兜底策略)。
4. **作为访客**,我在登记完成前看到的「可访问楼层」说明,应与最终开通的楼层**一致**,避免纠纷。
### 3.3 策略类型(产品枚举建议)
| 策略代码(建议) | 产品名称 | 行为摘要 | 适用场景 |
|------------------|----------|----------|----------|
| `HOST_FLOOR_LIST` | 与现网 UC-01 一致 | 不传 `floorIds` 时,等价于被访人 **`floorList` 全集** | 默认租户、未配置策略时的回退 |
| `INTERSECT_ALLOWLIST` | 租户允许列表 ∩ 被访人楼层 | 最终楼层 = **`allowZoneIds``floorList`**(交集为空则失败并提示) | **广发基金类**:员工多层、访客只允许接待层等 |
| `FIXED_ZONES` | 固定楼层(仍 ∩ 被访人) | 最终楼层 = **`fixedZoneIds``floorList`** | 强固定少量楼层且必须与员工授权一致 |
| `EXPLICIT_ONLY` | 仅显式开通 | 业务侧**必须**传 `floorIds`(等价 UC-02);租户策略关闭「空列表自动补全」 | 金融高敏楼栋 |
**产品原则(建议写进需求)**:凡涉及「租户默认/固定楼层」的,**最终生效楼层必须 ⊆ 被访人 `floorList`**(安全底线,见第 5 章)。
### 3.4 管理端功能清单(MVP → 完整)
**MVP(最小可用)**
- 租户级开关:是否启用「访客楼层策略」。
- 策略类型选择 + **允许楼层多选**(选项数据来自空间树 `/sysetting/zone/tree` 或分页接口,与走查文档一致)。
- 保存后**审计**:操作人、时间、前后 JSON 快照。
**增强**
- 按**楼栋**分别配置(多栋园区)。
- 按**被访部门**或**职级**附加规则(优先级:部门规则 > 租户默认)。
- 导入/导出 CSV`businessId, zoneName, zoneId`)。
- 与「访客类型」联动(VIP 可走多层等,需单独权限模型)。
### 3.5 验收标准(补充闭环相关)
| 编号 | 场景 | 期望 |
|------|------|------|
| AC-1 | 租户配置「允许 {28F}」,被访人 `floorList` 含 28F 与其它层 | 不传 `floorIds` 时(若走兜底),访客**仅**开通 28F |
| AC-2 | 同上,被访人 `floorList` **不含** 28F | **开通失败**,返回明确错误(禁止静默落到其它层) |
| AC-3 | 租户未配置策略 | 行为与现网 **UC-01** 一致 |
| AC-4 | 业务显式传 `floorIds`UC-02 | **以传入为准**(或启用「显式也求交」须在集成合同写明) |
| AC-5 | 注册前 UI 展示楼层 | 与 `add/visitor` **最终生效楼层**一致(含策略与求交) |
| **AC-6** | 登记页初始化与 P5 开通相隔任意时间(策略不变) | **两次 `computeEffective` 结果一致**(或均使用快照) |
| **AC-7** | 仅改展示文案、不改服务端 | **禁止**;展示必须以服务端预览为准 |
---
## 4. 技术方案
### 4.1 方案族对比
| 方案 | 实现位置 | 优点 | 缺点 | 新租户扩展 |
|------|----------|------|------|------------|
| **A. 组织侧收窄 `floorList`** | `ninca-common-component-organization` | 不改电梯;与 UC-01 完全一致 | 员工本人权限与「访客可继承列表」易混 | 每租户数据治理 |
| **B. 业务/访客层显式传 `floorIds`** | 第三方 BFF:登记页与 P5 **同源计算** | 不改电梯;闭环最清晰(UC-02) | 多调用入口需统一 | 租户表在**业务库** |
| **C. 电梯侧策略引擎** | `PersonRuleServiceImpl.addVisitor` | 直连电梯的旧集成也能兜底 | 电梯存策略、发版耦合 | `businessId` 维表 |
| **D. intelligent BFF** | `intelligent-cwoscomponent-rest` | 不直接改电梯包 | 绕过 intelligent 的直连须约束 | 表可在 intelligent 库 |
| **E. 配置中心** | Nacos 等 | 变更快 | 审计弱 | 适合灰度,长期建议落库 |
**推荐组合(工程上较稳)**
1. **首选****方案 B** — 第三方 **BFF****P0 与 P5 共用** `computeEffective``add/visitor` **始终传 `floorIds`**
2. **可选兜底****方案 C 或 D** 仅在 `floorIds` 为空时求交;登记页预览仍应调用**同一预览逻辑**(可部署在 BFF 或 D),避免「兜底路径」与「预览路径」算法分叉。
### 4.2 核心算法(推荐伪代码)
设:
- `hostFloors` = 组织 `detail` 返回的 `floorList`(可能为空)。
- `tenantAllow` = 租户配置的允许访客楼层 `zoneId` 集合。
- `requested` = 调用方传入的 `floorIds`(可能为空)。
```
if requested 非空:
effective = requested // 默认信任;高敏合同可改为 requested ∩ tenantAllow ∩ hostFloors
else:
effective = tenantAllow ∩ hostFloors // 或 policy=HOST 时 tenantAllow 视为全集
if effective 为空:
返回业务错误(勿继续 zone page / insert
```
**电梯 `addVisitor` 当前风险点**:补全后未校验空列表即使用 `floorIds.get(0)`。无论采用何种产品方案,**建议在生效列表计算完成后统一做空集校验**,与 [UC-04](访客注册与派梯楼层业务流程走查.md) 治理合并。
### 4.3 数据模型草案(租户策略落库)
```sql
CREATE TABLE tenant_visitor_floor_policy (
id VARCHAR(32) PRIMARY KEY,
business_id VARCHAR(64) NOT NULL COMMENT '机构/租户 ID',
policy_type VARCHAR(32) NOT NULL,
allow_zone_ids TEXT COMMENT 'JSON 数组 zoneId',
building_id VARCHAR(64) COMMENT '可选',
enabled TINYINT(1) NOT NULL DEFAULT 1,
policy_version BIGINT NOT NULL DEFAULT 1 COMMENT '每次更新+1,供登记快照引用',
remark VARCHAR(256),
created_by VARCHAR(64),
created_at BIGINT,
updated_by VARCHAR(64),
updated_at BIGINT,
UNIQUE KEY uk_biz_building (business_id, building_id)
);
```
**缓存**:按 `businessId` 缓存;更新策略时递增 `policy_version` 并使缓存失效。
### 4.4 管理 API 草案(REST
| 方法 | 路径 | 说明 |
|------|------|------|
| `GET` | `/admin/tenant/{businessId}/visitor-floor-policy` | 查询当前策略 |
| `PUT` | `/admin/tenant/{businessId}/visitor-floor-policy` | 全量更新;**递增** `policy_version` |
| `POST` | `/admin/tenant/{businessId}/visitor-floor-policy/preview` | body`hostPersonId`;返回与登记页同源结构 |
### 4.5 电梯服务改造要点(若采用方案 C)
见前文;补充:**登记页预览若不走电梯**,则电梯内求交逻辑须与 BFF **算法对齐**(共享库或 OpenAPI 生成的客户端 stub 中携带版本号)。
### 4.6 「28 楼」与 zoneId 映射
- 空间:**`/sysetting/zone/tree`**、**`/sysetting/zone/page`**(走查 §3.2)。
- 台账:`businessId``buildingId``display_name``zoneId`、生效日期。
### 4.7 兼容性与集成契约
| 项目 | 建议 |
|------|------|
| **与 UC-02** | 集成合同写明:`floorIds` 非空是否二次求交。 |
| **幂等** | 审批消息带 `draft_id` + `effective_hash`P5 去重。 |
| **版本** | 请求带 `policyVersion`,与快照比对可告警。 |
---
## 5. 安全与合规
1. **权限上界**:访客生效楼层 ⊆ 被访人 **`floorList`**。
2. **禁止静默降级**:交集为空则失败并记录。
3. **审计**:策略变更、预览请求、开通请求(可采样)。
4. **数据隔离**:所有查询带 `businessId`
---
## 6. 多租户「默认访客楼层」实现路径综合评估
在已具备 **§2 闭环**(登记页初始化 → 快照 → 审批 → `add/visitor`)的前提下,对各实现路径按维度打分(**高 / 中 / 低** 为实施与维护成本或风险的主观分级,供选型会使用)。
### 6.1 评估维度说明
| 维度 | 含义 |
|------|------|
| **登记页契合** | 能否自然地在 **P0** 提供准确预览、少调接口。 |
| **闭环一致性** | 单源计算、快照、防 TOCTOU 是否易实现。 |
| **新租户成本** | 新开租户时配置、实施、回归工作量。 |
| **入侵性** | 对电梯/组织/第三方存量改动的范围。 |
| **绕过风险** | 存在不经过 BFF 直连电梯时是否仍安全。 |
### 6.2 综合对比表
| 路径 | 登记页契合 | 闭环一致性 | 新租户成本 | 入侵性 | 绕过风险 | 结论 |
|------|------------|------------|------------|--------|----------|------|
| **B:第三方 BFF + 显式 `floorIds`** | **高**BFF 聚合 detail+策略+zone | **高**(同函数+快照) | **低**(插策略行 + 空间台账) | **低**(电梯可不改) | **中**(依赖网关禁止直连) | **首推** |
| **Dintelligent 预览 + 转发** | **高** | **高**(预览与转发同模块) | **中**(需发布 intelligent | **中** | **中** | 适合已统一走 intelligent 的客户 |
| **C:仅电梯兜底** | **低**(预览仍需别处算,否则与 UI 不一致) | **中**(易双算法) | **低** | **高** | **低** | 作**兜底**,不宜单独承担预览 |
| **A:仅组织改 `floorList`** | **中** | **中**(与员工权限耦合) | **高**(组织数据治理难) | **高** | **低** | 仅当组织域已区分访客继承列时考虑 |
| **B + C** | **高** | **高** | **低** | **中** | **低** | **大型项目推荐**B 主路径 + C 防直连漏传 |
### 6.3 新租户接入检查清单(SOP)
1. 在空间服务确认 **`zoneId`** 与接待层名称。
2. 在租户策略表插入 **`business_id` + `INTERSECT_ALLOWLIST` + allow_zone_ids**(或走管理端保存)。
3. 第三方 BFF 配置 **Nexus/网关** 指向组织与空间。
4. 跑通 **P0 预览****P5 add/visitor** 自动化用例(覆盖 AC-1~AC-7)。
5. 培训租户管理员:**改策略会影响后续新单**;进行中单据策略见 §2.7。
---
## 7. 实施路线(建议阶段)
| 阶段 | 内容 | 产出 |
|------|------|------|
| **0** | 盘点所有 `add/visitor` 入口;登记页是否已有 `detail` 调用 | 调用方与数据项缺口表 |
| **1** | 定稿:§2.3 数据项表 + §2.7 策略变更策略 + AC-6/7 | PRD、接口契约 |
| **2** | 实现 BFF 聚合初始化 + 登记快照 + P5 显式 `floorIds` | 广发基金类租户上线 |
| **3** | 可选:电梯空列表校验 + C/D 兜底 + 监控 | 零 NPE、直连防护 |
---
## 8. 测试用例矩阵(摘录)
| ID | 阶段 | 条件 | 期望 |
|----|------|------|------|
| T0 | P0 | 被访人无 `floorList` | 预览失败,禁止提交 |
| T1 | P0→P5 | INTERSECT,有交集 | 预览与开通均为交集 |
| T6 | P0→P5 | 审批期间策略变更 + 冻结快照 | 开通结果与提交时预览一致 |
| T7 | P0 | 仅前端改楼层展示 | 服务端拒绝或覆盖(AC-7) |
(原 T1~T5 矩阵仍适用于纯开通层逻辑,可与上表合并维护。)
---
## 9. 文档版本与维护
| 项目 | 内容 |
|------|------|
| 输出路径 | `docs/business/租户访客默认楼层技术产品方案.md` |
| 依据代码 | `maven-cw-elevator-application``maven-intelligent-cwoscomponent`;闭环 P4 依赖组织访客 API(仓外) |
| 修订触发 | 登记页字段增减;`person/detail` 字段变更;`add/visitor` 契约变更 |
---
*本文档为方案级输出;组织人员检索、访客建档等接口以实际部署与网关路由为准。表结构与路径在实施前需经安全评审。*
@@ -0,0 +1,323 @@
# 访客注册与派梯楼层:完整业务流程走查
> **范围说明**:本文基于本仓库内 Maven 工程源码(`maven-cw-elevator-application`、`maven-intelligent-cwoscomponent` 等)梳理**接口调用链、业务逻辑与用例**。
> **访客业务后台**(如「轻舟 / intelligent/three」访客注册、审批流)若不在本仓库,文中以「第三方业务系统」统称,并标明本仓库可见的**被调接口**与**出站 Feign/HTTP**。
---
## 1. 术语与角色
| 术语 | 含义 |
|------|------|
| 第三方业务系统 | 访客注册、邀约、审批等上层应用;可经 intelligent 组件或直接调电梯服务 |
| 组织人员服务 | `ninca-common-component-organization`(可配置),提供人员详情、图库人员绑定等 |
| 空间/区域服务 | `ninca-common``/sysetting/zone`,提供楼栋-楼层树、分区 page |
| 电梯应用 | `cw-elevator-application`,维护派梯规则 `image_rule_ref`、设备图库绑定等 |
| 被访人 | `personId`,组织侧人员主键 |
| 访客 | `visitorId`,组织侧访客人员主键 |
| 楼层 | 以 **分区/空间 ID**`zoneId` / `floorId`)表示,与 `PersonResult.floorList` 元素一致 |
---
## 2. 业务总览(从「默认楼层」到「指定楼层」)
```mermaid
flowchart TB
subgraph third [第三方业务系统]
A[访客注册/邀约完成]
B{是否已知具体楼层 zoneId 列表?}
C[调组织服务拉人员详情]
D[组装 floorIds 显式列表]
end
subgraph org [组织人员服务 ninca-common-component-organization]
P["POST /component/person/detail"]
I["POST /component/imagestore/person/batchBind 等"]
end
subgraph intel [intelligent-cwoscomponent-rest 可选]
E["ElevatorPersonService.addVisitor → Feign"]
end
subgraph elev [电梯 cw-elevator-application]
F["POST /elevator/person/add/visitor"]
G["PersonRuleServiceImpl.addVisitor"]
end
A --> B
B -->|否 需默认| C
C --> P
P -->|PersonResult.floorList| third
B -->|是 或 已补全| D
D --> E
C --> E
E --> F
F --> G
G -->|缺 floorIds 时内部再调 P 取 floorList| P
G --> I
```
**要点**
- 「**默认有哪些楼层**」在本仓库电梯侧的实现是:**未传 `floorIds` 时**,调用 **`PersonService.detail`**Feign → **`POST /component/person/detail`**),取 **`PersonResult.getFloorList()`****不是** `PersonResult.defaultFloor` 单字段。
- 「**第三方设定具体楼层**」:**在请求体中携带非空的 `floorIds`** 调用 **`POST /elevator/person/add/visitor`**(或经 `ElevatorPersonService` 转发),电梯侧**不再**调人员详情补楼层。
---
## 3. 流程 A:第三方如何获知「默认」楼层(可走的接口)
第三方若**自行**在注册前展示「被访人默认可达楼层」,应在**组织侧**完成数据拉取;本仓库可见的**权威数据源**为人员详情返回的 **`floorList`**(及可能同时返回的 `defaultFloor``floorInfoList` 等,但电梯访客派梯**仅用** `floorList` 补全逻辑)。
### 3.1 推荐:人员详情(与电梯补全逻辑一致)
| 项目 | 内容 |
|------|------|
| 调用方 | 任意有权限的服务;intelligent 中为 `RestPersonServiceImpl``PersonFeignClient` |
| 服务名配置 | `${feign.component-organization.name:ninca-common-component-organization}` |
| HTTP | **`POST /component/person/detail`** |
| 请求体 | `PersonDetailParam``id` = 被访人 `personId``businessId` = 机构 |
| 响应 | `CloudwalkResult<PersonResult>`,关注 **`floorList`**`List<String>` 楼层/分区 ID |
代码位置(Feign 声明):
```19:32:maven-intelligent-cwoscomponent/intelligent-cwoscomponent-rest/src/main/java/cn/cloudwalk/rest/cwoscomponent/intelligent/person/feign/PersonFeignClient.java
@FeignClient(name = "${feign.component-organization.name:ninca-common-component-organization}",
path = "/component/person", fallback = PersonFeignClientFallback.class)
public interface PersonFeignClient {
...
@RequestMapping(value = {"/detail"}, method = {RequestMethod.POST})
CloudwalkResult<PersonResult> detail(@RequestBody PersonDetailParam paramPersonDetailParam);
```
`PersonResult` 中与楼层相关的字段(电梯 `addVisitor` **补全时只用 `floorList`**):
```27:34:maven-intelligent-cwoscomponent/intelligent-cwoscomponent-interface/src/main/java/cn/cloudwalk/client/cwoscomponent/intelligent/person/result/PersonResult.java
private String defaultFloor;
private String chooseFloor;
private List<String> floorList;
private List<AcsPassRuleImageResultDto> floorInfoList;
...
private String defaultChooseFloor;
```
### 3.2 辅助:空间树 / 楼层分页(电梯网关或管理端常用)
| 项目 | 内容 |
|------|------|
| Feign | `ZoneFeignClient` → `${feign.ninca-common.name:ninca-common}` |
| HTTP | **`POST /sysetting/zone/tree`**、**`POST /sysetting/zone/page`** |
| 用途 | 按楼栋展示树、按条件查分区;**不替代**人员已授权楼层列表 |
```16:23:maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/zone/client/ZoneFeignClient.java
@FeignClient(name = "${feign.ninca-common.name:ninca-common}", path = "/sysetting/zone",
fallback = ZoneFeignClientFallback.class)
public interface ZoneFeignClient {
@RequestMapping(value = {"/tree"}, method = {RequestMethod.POST})
...
@RequestMapping(value = {"/page"}, method = {RequestMethod.POST})
CloudwalkResult<CloudwalkPageAble<ZoneResult>> page(ZoneQueryParam paramZoneQueryParam) throws ServiceException;
}
```
### 3.3 辅助:通行规则-楼层列表(管理派梯规则维度)
| 项目 | 内容 |
|------|------|
| Controller | `AcsPassRuleController` |
| HTTP | **`POST /elevator/passRule/floor`** 等 |
| 用途 | 规则/图库与楼层关系维护与查询;与「人员 floorList」不同维度 |
```45:55:maven-cw-elevator-application/cw-elevator-application-web/src/main/java/cn/cloudwalk/elevator/passrule/controller/AcsPassRuleController.java
@RequestMapping({"/floor"})
public CloudwalkResult<CloudwalkPageAble<AcsPassRuleFloorResult>>
listFloor(@RequestBody AcsPassRuleFloorForm form) {
...
return this.imageRuleRefService.listFloor(param, getCloudwalkContext());
```
### 3.4 访客记录查询(识别访客身份,非楼层来源)
电梯乘梯记录中通过 **RestTemplate** 调 **`intelligent/three/visitor/record/query`**,用于判断是否访客及被访人,**不参与** `addVisitor` 楼层列表计算:
```262:274:maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/record/impl/AcsElevatorRecordServiceImpl.java
URI uri =
combineAuthClientURI("intelligent/three/visitor/record/query", (MultiValueMap<String, String>)null);
VisitorRecordQueryParam form = new VisitorRecordQueryParam();
form.setVisitorId(addDTO.getRecognitionFaceId());
...
```
另有 Feign **`VisitorFeignClient`**`ninca-crk-std` 的 **`/intelligent/visitor/record/query`**,与上为不同网关路径,同属访客记录查询能力。
---
## 4. 流程 B:第三方设定「具体楼层」并开通访客派梯
### 4.1 对外 HTTP(电梯应用)
| 项目 | 内容 |
|------|------|
| Method / Path | **`POST /elevator/person/add/visitor`** |
| Controller | `AcsPersonController#addVisitor` |
| Body | `AcsPersonAddVisitorForm` → `AcsPersonAddVisitorParam` |
```53:62:maven-cw-elevator-application/cw-elevator-application-web/src/main/java/cn/cloudwalk/elevator/person/controller/AcsPersonController.java
@RequestMapping({"/add/visitor"})
public CloudwalkResult<Boolean> addVisitor(@RequestBody AcsPersonAddVisitorForm form) {
AcsPersonAddVisitorParam param =
(AcsPersonAddVisitorParam)BeanCopyUtils.copyProperties(form, AcsPersonAddVisitorParam.class);
try {
return this.personRuleService.addVisitor(param, getCloudwalkContext());
```
### 4.2 经 intelligent 的 Feign(业务方 SDK 式调用)
| 项目 | 内容 |
|------|------|
| 接口 | `ElevatorPersonService.addVisitor` |
| 实现 | `RestElevatorPersonServiceImpl` |
| Feign | `ElevatorPersonFeignClient` |
| 目标 | `${feign.elevator.name:elevator-app}` **`POST /elevator/person/add/visitor`** |
```11:15:maven-intelligent-cwoscomponent/intelligent-cwoscomponent-rest/src/main/java/cn/cloudwalk/rest/cwoscomponent/intelligent/elevator/feign/ElevatorPersonFeignClient.java
@FeignClient(name = "${feign.elevator.name:elevator-app}", path = "/elevator/person",
fallback = ElevatorPersonFeignClientFallback.class)
public interface ElevatorPersonFeignClient {
@RequestMapping(value = {"/add/visitor"}, method = {RequestMethod.POST})
CloudwalkResult<Boolean> addVisitor(@RequestBody AcsPersonAddVisitorParam paramAcsPersonAddVisitorParam);
```
### 4.3 请求字段语义(用例输入)
| 字段 | 必填性 | 说明 |
|------|--------|------|
| `visitorId` | 是 | 访客在组织侧人员 ID |
| `personId` | 是 | 被访人 ID;**补全楼层时**用于 `detail` |
| `begVisitorTime` / `endVisitorTime` | 视图库接口 | 传入图库绑定有效期 |
| `floorIds` | 否 | **非空**:第三方显式指定可派梯楼层;**空/缺省**:由电梯侧按被访人 **`floorList`** 补全 |
---
## 5. 电梯侧核心业务逻辑:`PersonRuleServiceImpl.addVisitor`
实现类:`PersonRuleServiceImpl``maven-cw-elevator-application/.../PersonRuleServiceImpl.java`)。
### 5.1 步骤分解
| 步骤 | 逻辑 | 外部依赖 |
|------|------|----------|
| 1 | 若 `floorIds` 为空 → `PersonDetailParam(personId, businessId)` → **`personService.detail`** | Feign → **`POST /component/person/detail`**,取 **`getFloorList()`** 赋给 `floorIds` |
| 2 | 用 **`floorIds.get(0)`** 构造 `ZoneQueryParam`**`zoneService.page`** 查分区页,取首条 `ZoneResult` 得 **`parentId`(楼栋)** | Feign → **`POST /sysetting/zone/page`** |
| 3 | **`deviceImageStoreDao.getByBuildingId(parentId)`** 得图库 `imageStoreId` | 本地 DAO |
| 4 | 对 **每个 `floorId`**`imageRuleRefDao.getDefaultByZoneId(floorId)` 取默认通行规则,组装 **`ImageRuleRefAddDto`**(访客 `visitorId` 挂父规则) | 本地 DAO |
| 5 | **`imageRuleRefDao.insertList`** 批量写入派梯规则引用 | 本地 DAO |
| 6 | 组装 **`ImageStorePersonBindParam`**(图库、访客 ID、有效期),**`imageStorePersonService.batchBind`** | Feign → **`POST /component/imagestore/person/batchBind`** |
| 7 | **`imageStorePersonService.updateGroupPersonRef`** 更新组与人员引用 | Feign → **`POST /component/imagestore/person/updateGroupPersonRef`** |
关键代码(补全楼层 + 循环写规则 + 绑图库):
```170:223:maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/person/impl/PersonRuleServiceImpl.java
if (CollectionUtils.isEmpty(param.getFloorIds())) {
PersonDetailParam detailParam = new PersonDetailParam();
detailParam.setId(param.getPersonId());
detailParam.setBusinessId(context.getCompany().getCompanyId());
CloudwalkResult<PersonResult> detail = this.personService.detail(detailParam, context);
param.setFloorIds(((PersonResult)detail.getData()).getFloorList());
}
ZoneQueryParam zoneQueryParam = new ZoneQueryParam();
zoneQueryParam.setId(param.getFloorIds().get(0));
...
for (String floorId : param.getFloorIds()) {
ImageRuleRefResultDto defaultRule = this.imageRuleRefDao.getDefaultByZoneId(floorId);
...
addDto.setPersonId(param.getVisitorId());
...
}
...
CloudwalkResult<ImgStoreBatchBindPersonResult> bindResult =
this.imageStorePersonService.batchBind(imageStorePersonBindParam, context);
...
this.imageStorePersonService.updateGroupPersonRef(refParam, context);
```
图库 Feign(与上表一致):
```19:41:maven-intelligent-cwoscomponent/intelligent-cwoscomponent-rest/src/main/java/cn/cloudwalk/rest/cwoscomponent/intelligent/imagestore/feign/ImageStorePersonFeignClient.java
@FeignClient(name = "${feign.component-organization.name:ninca-common-component-organization}",
path = "/component/imagestore/person", fallback = ImageStorePersonFeignClientFallback.class)
public interface ImageStorePersonFeignClient {
...
@RequestMapping(value = {"/batchBind"}, method = {RequestMethod.POST})
CloudwalkResult<ImgStoreBatchBindPersonResult>
batchBind(@RequestBody ImageStorePersonBindParam paramImageStorePersonBindParam);
@RequestMapping(value = {"/updateGroupPersonRef"}, method = {RequestMethod.POST})
CloudwalkResult<Boolean>
updateGroupPersonRef(@RequestBody UpdateGroupPersonRefParam paramUpdateGroupPersonRefParam);
```
### 5.2 与「内部员工」派梯开通的对比(非访客)
**`POST /elevator/person/add`** → `PersonRuleServiceImpl.add`:按**单个** `zoneId`(楼层)与 `parentId`(楼栋)给多名 **`personIds`** 写规则并绑图库;**不**调人员 `detail` 取 `floorList`。用于从已有人员批量加通行权限的另一条业务线。
```79:136:maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/person/impl/PersonRuleServiceImpl.java
public CloudwalkResult<Boolean> add(AcsPersonAddParam param, CloudwalkCallContext context) throws ServiceException {
...
ImageRuleRefResultDto defaultRule = this.imageRuleRefDao.getDefaultByZoneId(param.getZoneId());
...
for (String personId : param.getPersonIds()) {
...
addDto.setZoneId(param.getZoneId());
```
---
## 6. 接口调用流转汇总表
| 序号 | 调用方向 | 协议与路径 | 典型触发 |
|------|----------|------------|----------|
| 1 | 第三方 / intelligent → 电梯 | `POST /elevator/person/add/visitor` | 访客开通派梯 |
| 2 | 电梯 → 组织 | `POST /component/person/detail` | `floorIds` 为空时补全 |
| 3 | 电梯 → 空间 | `POST /sysetting/zone/page` | 取楼层所属楼栋 |
| 4 | 电梯 → 组织 | `POST /component/imagestore/person/batchBind` | 访客绑图库 |
| 5 | 电梯 → 组织 | `POST /component/imagestore/person/updateGroupPersonRef` | 更新组人关系 |
| 6 | 第三方自行(可选) | `POST /component/person/detail` | 注册前展示默认可达楼层 |
| 7 | 管理端(可选) | `POST /elevator/passRule/floor` 等 | 规则/楼层维护 |
| 8 | 电梯 → 网关 HTTP | `POST intelligent/three/visitor/record/query` | 乘梯记录识别访客 |
---
## 7. 用例(UC)说明
| 用例 ID | 名称 | 前置条件 | 主流程 | 期望结果 |
|---------|------|----------|--------|----------|
| UC-01 | 访客派梯-使用被访人默认楼层列表 | 被访人 `personId` 在组织侧 `detail` 返回**非空** `floorList`;访客已注册 | 调 `add/visitor` 且 **不传 `floorIds`** | 电梯取 `floorList` 为访客写入各层默认规则并绑图库 |
| UC-02 | 访客派梯-第三方指定楼层 | `floorIds` 为合法 zoneId 列表 | 调 `add/visitor` 且 **传入 `floorIds`** | **不再**调 `person/detail` 补楼层;按列表写规则 |
| UC-03 | 注册前展示「可访楼层」 | 需与被访人授权一致 | 第三方先调 **`/component/person/detail`**,展示 `floorList`(或产品定义的 `defaultFloor` 映射) | UI 与电梯补全逻辑对齐(若仅用 `defaultFloor` 需与产品确认是否与 `floorList` 一致) |
| UC-04 | `floorList` 为空 | 被访人无派梯楼层 | `add/visitor` 不传 `floorIds` | `floorIds` 仍为空 → 后续 `get(0)` 等**可能异常**;第三方应校验或传显式楼层 |
| UC-05 | 内部人员加单层派梯 | 已知 `zoneId`、楼栋 `parentId` | `POST /elevator/person/add` | 单层规则 + 图库绑定 |
| UC-06 | 乘梯记录标记访客 | 识别到人脸 | 记录服务调 **`visitor/record/query`** | 回填 `isVisitor`、被访人 |
**扩展(租户访客默认楼层)**:若需「在 UC-01 不传 `floorIds` 的前提下,按租户策略收敛访客楼层(如默认仅开放某接待层)」的产品与技术设计,以及**从第三方登记页初始化数据项、接口编排到 `add/visitor` 的端到端闭环**,见 [租户访客默认楼层技术产品方案](租户访客默认楼层技术产品方案.md)(文中 **§2**)。
---
## 8. 风险与待确认项(走查结论)
1. **`PersonResult.defaultFloor` 与 `floorList`**:电梯 `addVisitor` **仅使用 `floorList`**。若产品「默认访问楼层」对应 `defaultFloor` 标量,需核对组织服务是否在 `detail` 中把二者对齐,否则存在**语义偏差**。
2. **`floorIds.get(0)`**:补全后若列表为空,会在取首元素时失败;第三方或组织数据需保证一致性。
3. **`cwos-sdk-event` 等**与本文无关的依赖问题不影响本业务链结论。
4. **访客注册主流程**(表单、审批、写组织人员表)若在三方工程,需在对应仓库继续搜 **`ElevatorPersonService`**、**`addVisitor`**、**`/elevator/person/add/visitor`** 的引用以闭合「从注册到派梯」的端到端文档。
---
## 9. 文档版本信息
| 项目 | 内容 |
|------|------|
| 输出路径 | `docs/business/访客注册与派梯楼层业务流程走查.md` |
| 依据代码根目录 | `maven-cw-elevator-application`、`maven-intelligent-cwoscomponent` |
| 说明 | 外部服务行为以接口契约为准,组织服务内部如何组装 `floorList` 需在 **ninca-common-component-organization** 源码中二次走查 |
---
*本文档由代码走查自动生成梳理,若接口路径随部署配置变化,请以运行环境 `application*.yml` 中 `feign.*` 为准。*
@@ -0,0 +1,357 @@
# 访客邀约与派梯楼层一致性梳理
> **文档用途**:把「邀约页展示的楼层」与「电梯开通权限实际生效的楼层」如何在现有代码中对齐,用大白话 + 源码位置一并说明,便于产品、集成与排障。
> **关联文档**:[访客与电梯业务完整说明](../../maven-cw-elevator-application/cw-elevator-application-service/docs/08-visitor-registration-and-elevator-auth.md)、[租户访客默认楼层技术产品方案](租户访客默认楼层技术产品方案.md)、[租户访客策略迁入组织规格](../superpowers/specs/2026-05-06-tenant-visitor-policy-organization-implementation.md)、[**租户访客楼层策略 — 代码重构实施指南**](租户访客楼层策略-代码重构实施指南.md)(落地步骤与阶段划分)。
---
## 1. 一句话原则
**所有环节只信同一串楼层 ID**:能去哪几层,应在**拉被访人详情**时由组织侧算清(含租户默认楼层策略时写入 `floorList`);邀约登记把用户选定写进自己的业务单;**派梯时把这份楼层列表放进请求里的 `floorIds`**,电梯就按这一串执行。**不要用「人员列表分页」等接口顶替详情里的楼层主数据。**
---
## 2. 大白话说明
| 诉求 | 做法 |
|------|------|
| 邀约和派梯不要两套楼层 | 选人后楼层候选以 **`PersonService.detail`(组织 `/component/person/detail`)→ `floorList`** 为准;租户「默认只开放某几层」应在组织 **`detail` 链路内完成策略替代后再返回(规范见 §5,落地状态见 §6)。 |
| 邀约单已经定了访问楼层 | 后续 **`POST /elevator/person/add/visitor` 必须把邀约单里保存的那串楼层放进 `floorIds`**;电梯**不会**根据邀约单号去数据库查记录——**只认本次 HTTP 参数**。 |
| 派梯请求不传楼层 | 电梯会用 **被访人详情里的 `floorList`** 作为生效楼层,**不会**自动对齐你们库里存的邀约楼层——若要与邀约一致,**集成侧应始终传 `floorIds`**。 |
---
## 3. 时序图(实现归属:组织侧 / 电梯侧 / 集成侧)
**图例**
| 标注 | 含义 |
|------|------|
| **组织侧** | `maven-ninca-common-component-organization``PersonController``ImgPersonServiceImpl#detail`、组织库、(规范中的)租户访客策略服务 |
| **电梯侧** | `maven-cw-elevator-application``AcsPersonController``PersonRuleServiceImpl#addVisitor`、电梯库 `image_rule_ref` / `zone` / 楼栋图库映射等 |
| **Intelligent** | `maven-intelligent-cwoscomponent``PersonService.detail` → Feign 调组织 `/component/person/detail` |
| **集成侧** | 第三方 / BFF / 访客业务库:邀约单持久化、派梯前组装 `floorIds`(电梯代码中无邀约表) |
### 3.0 端到端:访客建档在前、派梯在后(推荐集成顺序)
典型闭环:**先**完成 **访客人员建档** 并得到 **`visitorId`**(平台人员主键),**再**调用 **`POST /elevator/person/add/visitor`**。邀约页拉 **`detail`**、保存 **`floorIds`** 可与建档分步编排;但 **`addVisitor` 必须在建档成功之后调用**(电梯侧不写访客主数据,见 §4.4)。
```mermaid
sequenceDiagram
autonumber
participant U as 用户 / 前台
participant BFF as BFF / 集成侧
participant PS as Intelligent<br/>PersonService.detail
participant OC as 组织侧<br/>ImgPersonServiceImpl#detail
participant ORG as 组织侧<br/>访客建档(示意接口)
participant AC as 电梯侧<br/>AcsPersonController addVisitor
rect rgb(245,248,250)
Note over U,OC: ① 邀约:选被访人 + 楼层候选(与 §3.1 同源)
U->>BFF: 选被访人 hostPersonId
BFF->>PS: detail(hostPersonId)
PS->>OC: POST /component/person/detail
OC-->>BFF: PersonResult.floorList 等
U->>BFF: 勾选楼层并提交邀约单
BFF->>BFF: 持久化邀约记录(hostPersonId、floorIds、访期等)
end
rect rgb(248,252,255)
Note over BFF,ORG: ② 访客建档(须先于派梯;具体路径以现场组织/访客模块为准)
BFF->>ORG: 创建访客人员 / 人像入库(示意)
Note over ORG: 组织侧或其它访客服务:人员落库,得到 visitorId
ORG-->>BFF: visitorId(平台 personId
end
rect rgb(255,248,245)
Note over BFF,AC: ③ 派梯开通(visitorId 已存在)
BFF->>AC: POST /elevator/person/add/visitor<br/>personId=hostPersonId, visitorId, floorIds=邀约单楼层…
Note over AC: 电梯侧:§3.3 UC-02;不传 floorIds 则 §3.2 UC-01
AC-->>BFF: CloudwalkResult
end
```
| 区块 | 实现归属 | 说明 |
|------|-----------|------|
| ① 邀约与楼层候选 | **集成侧** + **Intelligent → 组织** `detail` | 楼层权威路径与 **§3.1** 一致;邀约单仅存 **集成侧** 业务库 |
| ② 访客建档 | **组织侧为主(示意)** | 本仓库 **`addVisitor`** **不包含**建档;返回的 **visitorId** 即后续规则与图库绑定的主体 |
| ③ 派梯 | **电梯侧** `addVisitor` + 组织 Feign 绑图库 | 详见 **§4.5**;请求体**无邀约单号****floorIds** 须由集成侧按单据填入(对齐 **§3.3** |
---
### 3.1 邀约页初始化 — 拉被访人「可访问楼层」主路径(与 UC-01 同源)
组织侧组装 `floorList`;其中 **`listByImageId`** 通过 Feign **回调电梯 HTTP** 读取通行规则。
```mermaid
sequenceDiagram
autonumber
participant FE as 前端 / BFF<br/>集成侧
participant PS as Intelligent<br/>PersonService.detail
participant OC as 组织侧<br/>PersonController / ImgPersonServiceImpl#detail
participant EF as 电梯 HTTP<br/>passRule/imagelistByImageId
participant TP as 租户策略(规范)<br/>组织侧 DB / Service
FE->>PS: detail(personId, businessId)
Note over PS: Intelligent:路由实现
PS->>OC: POST /component/person/detail
Note over OC: 组织侧:查人员、组装机构标签等
OC->>EF: Feign listByImageId(通行楼层原始列表)
Note over EF: 电梯侧:凭规则返回 zoneId 列表
EF-->>OC: AcsPassRuleImageResultDto 列表
OC->>TP: (可选)命中租户访客策略则替代 floorList
Note over TP: 组织侧:策略语义见 §5<br/>未落地时仍为 listByImageId 结果(§6
TP-->>OC: allow_zone_ids / 未启用
OC-->>PS: PersonResult(含 floorList
PS-->>FE: 展示可选楼层 / 默认勾选依据
```
| 步骤 | 实现位置 |
|------|-----------|
| 对外 detail 聚合入口 | **Intelligent** → 组织 **`PersonController`** |
| `floorList` 原始数据来源(通行规则) | **电梯侧** HTTP,由组织 **`ElevatorFeignClient.listByImageId`** 调用 |
| 租户默认楼层「替代」写入 `floorList` | **组织侧**(规范:`ImgPersonServiceImpl#detail` 内;见 §5~§6 |
| 邀约单保存用户勾选 | **集成侧**业务库,不在本仓库电梯模块 |
---
### 3.2 派梯 UC-01 — 请求 **未带** `floorIds`(电梯用组织 detail 的 `floorList`
电梯侧编排;**被访人详情**仍在 **组织侧** 计算;写规则、绑图库在 **电梯侧 + 组织 Feign**
```mermaid
sequenceDiagram
autonumber
participant BFF as BFF / 调用方<br/>集成侧
participant AC as 电梯侧<br/>AcsPersonController
participant PR as 电梯侧<br/>PersonRuleServiceImpl#addVisitor
participant PS as Intelligent<br/>PersonService.detail
participant OC as 组织侧<br/>ImgPersonServiceImpl#detail
participant DB as 电梯侧 DB<br/>image_rule_ref 等
participant IS as 组织侧<br/>ImageStorePersonService.batchBindFeign
BFF->>AC: POST /elevator/person/add/visitor<br/>floorIds 为空
AC->>PR: addVisitor(param)
PR->>PS: detail(personId)
PS->>OC: /component/person/detail
Note over OC: 组织侧:返回 floorList(§3.1 同源)
OC-->>PS: PersonResult
PS-->>PR: floorList
PR->>PR: effective = personResult.floorListUC-01
PR->>DB: 按楼层写规则、取楼栋 imageStoreId
Note over DB: 电梯侧:PersonRuleServiceImpl 本地 Dao
PR->>IS: batchBind(visitorId, 访期…)
Note over IS: 组织侧:图库绑定
IS-->>PR: 成功 / 失败
PR-->>AC: CloudwalkResult
AC-->>BFF: 结果
```
| 步骤 | 实现位置 |
|------|-----------|
| HTTP 入口 | **电梯侧** `AcsPersonController` |
| UC-01 取楼层 | **电梯侧** `PersonRuleServiceImpl` 使用 **组织 detail** 返回的 `floorList` |
| `PersonService.detail` 实现 | **组织侧** `ImgPersonServiceImpl#detail` |
| 写 `image_rule_ref`、选首层换楼栋 | **电梯侧** `PersonRuleServiceImpl` |
| 访客绑图库 | **组织侧** 服务,电梯 **Feign** 调用 |
---
### 3.3 派梯 UC-02 — 请求 **携带** `floorIds`(与邀约单保存的楼层一致)
与 UC-01 共用同一入口;**生效楼层**取请求体,**不再**用 `detail.floorList` 替换,但仍会调 **detail** 做被访人存在性与前置校验。
```mermaid
sequenceDiagram
autonumber
participant BFF as BFF / 调用方<br/>集成侧
participant AC as 电梯侧<br/>AcsPersonController
participant PR as 电梯侧<br/>PersonRuleServiceImpl#addVisitor
participant PS as Intelligent<br/>PersonService.detail
participant OC as 组织侧<br/>detail(仅校验被访人)
participant DB as 电梯侧 DB
participant IS as 组织侧<br/>batchBindFeign
BFF->>AC: POST /elevator/person/add/visitor<br/>floorIds = 邀约单持久化的列表
Note over BFF: 集成侧:从邀约记录读出楼层写入 body
AC->>PR: addVisitor(param)
PR->>PS: detail(personId)
PS->>OC: /component/person/detail
OC-->>PR: PersonResultfloorList 本轮可不用于生效集)
PR->>PR: effective = param.getFloorIds()UC-02
Note over PR: 电梯侧:不做 floorIds ⊆ detail.floorList 校验(信任调用方)
PR->>DB: 按 effective 写规则…
PR->>IS: batchBind…
IS-->>PR: ok
PR-->>BFF: 结果
```
| 步骤 | 实现位置 |
|------|-----------|
| 从邀约单带出 `floorIds` | **集成侧** |
| UC-02 分支(`effective = param.floorIds` | **电梯侧** `PersonRuleServiceImpl` |
| 子集校验(可选) | **集成侧 BFF** 或后续扩展;**当前电梯侧未实现** |
---
## 4. 派梯接口代码走查(电梯应用)
### 4.1 HTTP 入口
- **路径**`POST /elevator/person/add/visitor`
- **类**`maven-cw-elevator-application/cw-elevator-application-web/.../AcsPersonController.java`
- **请求体**`AcsPersonAddVisitorForm` → 拷贝为 `AcsPersonAddVisitorParam``PersonRuleService.addVisitor`
### 4.2 请求体字段(与邀约记录的关系)
| 字段 | 含义 |
|------|------|
| `personId` | 被访人在组织侧的人员 ID |
| `visitorId` | 访客人员 ID(平台 personId |
| `begVisitorTime` / `endVisitorTime` | 访期(绑图库 et al. |
| `floorIds` | **本次开通涉及的楼层 zoneId 列表**;由调用方填入。**无邀约单号字段**——电梯侧不读访客邀约业务表 |
定义见:`cw-elevator-application-web/.../form/AcsPersonAddVisitorForm.java`
### 4.3 生效楼层如何决定(`PersonRuleServiceImpl#addVisitor`
实现类:`cw-elevator-application-service/.../PersonRuleServiceImpl.java`
1. **始终先调** `personService.detail`(被访人存在性 / 组织侧详情)。失败则直接返回(如 `76260531`)。
2. **决定 `effective`(最终开通的楼层列表)**
- **`floorIds` 非空**(实现里称 **UC-02**):**`effective = 请求里的 `floorIds`**,原样使用。
- **`floorIds` 为空****UC-01**):**`effective = personResult.getFloorList()`**(来自组织 `detail`)。
3. **空列表**:返回失败(`76260531`)。
4. 后续:按 `effective` 每层写 `image_rule_ref`、取楼栋图库 `imageStoreId`、对 **`visitorId`** 调组织侧 `batchBind`、更新组人员引用等。
**重要**:显式传入 `floorIds` 时,当前实现**不会**再与 `personResult.getFloorList()` 做「子集校验」——楼层是否合规依赖**调用方 / BFF**;电梯信任请求体。
### 4.4 业务目标与范围(访客派梯做什么、不做什么)
| 维度 | 说明 |
|------|------|
| **目标** | 在已有 **访客人员 ID**`visitorId`)前提下,为本租户本次访问开通 **电梯通行规则**(电梯库 `image_rule_ref`),并把访客绑定到 **对应楼栋的人脸图库**(组织侧 `batchBind`),使闸机/电梯侧能按楼层放行。 |
| **不做** | **不在此接口创建访客档案**;访客姓名、证件、人像建档应在组织/访客业务前置完成并得到 **`visitorId`**。 |
| **租户上下文** | `businessId` 来自调用上下文(与 `CloudwalkCallContext.company` 一致),与被访人 `personId`、访客 `visitorId` 同属该租户数据范围。 |
---
### 4.5 分阶段业务逻辑(与 `PersonRuleServiceImpl#addVisitor` 对齐)
```mermaid
flowchart LR
S1[1 被访人 detail] --> S2[2 确定 effective]
S2 --> S3[3 首层 zone → 楼栋 → imageStoreId]
S3 --> S4[4 每层写 image_rule_ref<br/>personId=visitorId]
S4 --> S5[5 batchBind 访期 + updateGroupPersonRef]
```
下列阶段均在 **`maven-cw-elevator-application`** · **`PersonRuleServiceImpl.addVisitor`** 中顺序执行。
#### 阶段 1 — 校验被访人可查(组织侧)
- 构造 `PersonDetailParam``id = personId``businessId = context 租户`),调用 **`personService.detail`**(经 Intelligent → 组织 **`ImgPersonServiceImpl#detail`**)。
- **成功**:得到 **`PersonResult`**,后续 UC-01 需要其中的 **`floorList`**。
- **失败**:透传组织返回的 **code / message**;若结果为 null 或不成功且无语义,使用 **`76260531`**(无可用被访人信息或详情不可用)。
- **数据为空**`personResult == null`**`76260531`**。
#### 阶段 2 — 确定本次生效楼层列表 `effective`
- 见 §4.3**UC-02** 用请求 **`floorIds`****UC-01** 用 **`personResult.getFloorList()`**。
- **`effective` 为空**(含 UC-01 时 `floorList` 为空、或 UC-02 传空列表):**`76260531`**。
#### 阶段 3 — 解析「楼栋」并确定组织侧图库 `imageStoreId`(电梯侧 + 空间服务)
-**`effective` 的第一个元素**作为 **`zoneQueryParam.id`**,调用 **`zoneService.page`**(空间服务)解析 **楼层节点**
- 用返回的 **`ZoneResult.get(0).getParentId()`** 作为 **楼栋 ID**,再 **`deviceImageStoreDao.getByBuildingId(parentId)`** 得到 **`imageStoreId`**。
- **业务含义**:多楼层同一次开通时,**以列表首层所在楼栋**作为本次绑定的图库归属;各 **`floorId` 仍分别落在对应分区规则上,但 **人脸图库绑定指向同一 `imageStoreId`**(由首层楼栋决定)。
#### 阶段 4 — 按楼层写入访客通行规则(电梯侧库)
-**`effective` 中每个 `floorId`**
**`imageRuleRefDao.getDefaultByZoneId(floorId)`** 取该分区默认规则模板 → 组装 **`ImageRuleRefAddDto`****`personId` 填的是 `visitorId`(访客)**,写入 **`image_rule_ref`**(批量 **`insertList`**)。
- **语义**:访客在每个选定楼层上各有一条「挂默认父规则」的通行引用,与被访人自有规则分离。
#### 阶段 5 — 访期内人脸图库绑定与组同步(组织侧 Feign)
- **`ImageStorePersonBindParam`**`imageStoreId`(阶段 3)、**`personIds = [visitorId]`**、**`nullDateIsLongTerm = true`**、**`expiryBeginDate` / `expiryEndDate`** 取自请求的 **`begVisitorTime` / `endVisitorTime`**。
- 调用 **`imageStorePersonService.batchBind`**(失败则返回 **组织侧错误码**,电梯透传)。
- 成功后 **`updateGroupPersonRef`**:同一 **`visitorId`** + **`imageStoreId`**,用于组人员引用同步(具体语义见组织组件实现)。
#### 阶段 6 — 异常兜底
- 未捕获的 **`ServiceException`**:向上抛出。
- 其他 **`Exception`**:包装为 **`76260530`**(通用失败)。
---
### 4.6 访期参数(`begVisitorTime` / `endVisitorTime`
| 项 | 说明 |
|------|------|
| **用途** | 传入组织 **`batchBind`**,作为访客在该图库上的 **有效期起止**(与通行规则配合使用,以现场组织/图库策略为准)。 |
| **与楼层关系** | 访期 **不改变** `effective` 楼层集合;仅影响绑定是否在时间上有效。 |
| **`nullDateIsLongTerm=true`** | 代码固定传入;具体与空起止如何组合以组织 **`batchBind`** 实现为准,集成时建议在测试环境验证边界。 |
---
### 4.7 错误码与典型原因(电梯侧可见)
| 错误码 | 典型场景 |
|--------|-----------|
| **`76260531`** | `detail` 失败;被访人 `PersonResult` 为空;UC-01 时 **`floorList` 为空**;或 **`effective` 最终仍为空**。 |
| **`76260530`** | `addVisitor` 内部未预期的 **`Exception`**(如 DB、空指针、空间分页无数据等未单独映射时)。 |
| **组织 / Feign 返回码** | **`batchBind`** 失败时 **透传**对方 **code / message**,成功后再执行 **`updateGroupPersonRef`**;若 bind 失败则 **不会**继续组引用更新。 |
| **`76260521`** | Controller 层捕获 **`ServiceException`** 时映射(多见于其它接口;`addVisitor` 内多为 fail 码直接返回)。 |
排障时建议按日志顺序核对:**detail → effective → zone 首层 → imageStoreId → 每层 getDefaultByZoneId → batchBind**。
---
### 4.8 业务边界与集成注意
1. **首层决定楼栋图库**:若 `effective` 中楼层分属不同楼栋,当前实现**仅以第一项**定 `imageStoreId`;产品若要求「多楼栋多图库」,需拆分多次调用或扩展实现(现状未支持单次多楼栋)。
2. **每层必须有默认规则**:某 **`floorId`** 下 **`getDefaultByZoneId` 若为空**,可能在后续组装/插入时异常并落入 **`76260530`**,需在数据配置层保证各访客可达层已配置默认规则。
3. **幂等与重复开通**:同一访客重复调用可能产生多条规则引用或组织侧重复绑定行为,**以组织与电梯 DAO 约束为准**;重要场景建议业务层幂等(例如按邀约单号去重)。
4. **部分失败**:规则已 **`insertList`** 后若 **`batchBind`** 失败,当前流程 **不会自动回滚**已插入的规则行(需运维或补偿策略关注)。
---
## 5. 与「租户访客默认楼层」策略的关系
- **规范方向**(详见 `docs/superpowers/specs/2026-05-06-tenant-visitor-policy-organization-implementation.md`):租户允许楼层在组织 **`ImgPersonServiceImpl#detail`** 内以 **`allow_zone_ids` 替代**写入返回的 **`floorList`**;电梯 **UC-01** 仅透传该 `floorList`,不在电梯库再与策略表求交。
- **邀约初始化**:应通过 **`detail``floorList`** 展示可选楼层,与 **UC-01** 同源。
- **邀约单已保存楼层后的派梯**:把单据里的楼层写入 **`floorIds`**(UC-02),与单据一致;若需防止超范围,应在 **BFF** 侧校验 `floorIds ⊆ detail.floorList`(策略替代后)。
---
## 6. 实现状态提示(避免误判)
组织侧已实现:**`TenantVisitorFloorPolicyService`** 读取组织库 **`tenant_visitor_floor_policy`**,在 **`ImgPersonServiceImpl#detail`**(及 **`page(isVisitor)`**)对 **`floorList` / 楼层展示** 做 **`allow_zone_ids` 替代**。部署前须在组织库执行 **`docs/sql/organization_tenant_visitor_floor_policy.sql`**;未建表时策略查询失败会自动回退为 **`listByImageId` 原始结果**。详见 [租户访客楼层策略-代码重构实施指南](租户访客楼层策略-代码重构实施指南.md)。
---
## 7. 集成 checklist
- [ ] 邀约页初始化楼层:**走 `PersonService.detail`**,不要用人员分页/导出接口当作 `floorList` 主来源。
- [ ] 邀约保存:持久化用户选定或允许的 **楼层 zoneId 列表**(与展示同源)。
- [ ] 派梯:**从邀约单读出楼层 → 填入 `floorIds`** 调用 `/elevator/person/add/visitor`;若业务允许「不传楼层」,需知悉此时等价 UC-01,**与邀约单无自动对齐**。
- [ ] 安全(可选):BFF 校验 `floorIds``detail.floorList` 的子集(或业务规则允许的超集策略)。
- [ ] 日志:`AcsPersonController` 已打 `requestFloorSize`,便于核对是否传了楼层。
---
## 8. 源码索引(电梯)
| 环节 | 路径 |
|------|------|
| Controller | `maven-cw-elevator-application/cw-elevator-application-web/.../AcsPersonController.java``addVisitor` |
| 表单 | `.../form/AcsPersonAddVisitorForm.java` |
| 参数 | `cw-elevator-application-service/.../param/AcsPersonAddVisitorParam.java` |
| 核心逻辑(§4.4~§4.8) | `cw-elevator-application-service/.../PersonRuleServiceImpl.java``addVisitor` |
| 空间分页(首层 → 楼栋) | 同模块内 **`ZoneService`**`PersonRuleServiceImpl` 注入调用) |
| 图库绑定 / 组引用 | Feign → 组织 **`ImageStorePersonService`**`batchBind``updateGroupPersonRef` |
---
**文档版本**:与仓库梳理同步;若 `addVisitor` 行为变更,请同步更新 **§4**(含 §4.4~§4.8 业务逻辑与错误码)。