mirror of
https://github.com/hpd840321/starRiverProperty.git
synced 2026-06-09 08:20:31 +08:00
feat(elevator): 租户访客默认楼层策略表与 UC-01 求交
- 新增 tenant_visitor_floor_policy DDL(docs/sql) - MyBatis:TenantVisitorFloorPolicyMapper/Dao 按 businessId 读启用策略 - PersonRuleServiceImpl.addVisitor:未传 floorIds 时组织 floorList 与 allow_zone_ids 求交;无交集 76260532;无楼层 76260531;显式 floorIds 不读表;ServiceException 原样抛出 Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,291 @@
|
|||||||
|
# 租户访客默认楼层:数据库配置阶段 — 详细技术设计
|
||||||
|
|
||||||
|
> **文档性质**:实施级技术设计(TDD 范围:本阶段仅 **数据库表 + 电梯服务侧读表求交**;**不包含** 物业管理端前端页面与开放管理 API)。
|
||||||
|
> **产品依据**:[租户访客默认楼层技术产品方案](租户访客默认楼层技术产品方案.md)(策略类型、安全底线、AC 验收)。
|
||||||
|
> **流程依据**:[访客注册与派梯楼层业务流程走查](访客注册与派梯楼层业务流程走查.md)(UC-01 / UC-02、`PersonRuleServiceImpl.addVisitor`)。
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 版本 | v0.1 草案(数据库阶段) |
|
||||||
|
| 适用工程 | `maven-cw-elevator-application`(电梯应用库;落库表位于**电梯库**或与电梯同数据源的业务库,以现网数据源划分为准) |
|
||||||
|
| 读者 | 后端开发、DBA、集成测试、运维 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与阶段目标
|
||||||
|
|
||||||
|
### 1.1 问题复述
|
||||||
|
|
||||||
|
多租户场景下,部分机构要求:在调用方**不传** `floorIds`(走 **UC-01**)时,访客派梯生效楼层**不得**简单等同于被访人组织侧 `floorList` 全集,而应**收敛**为机构允许的若干 `zoneId`(如固定接待层)。未做特殊要求的租户须与现网行为**完全一致**。
|
||||||
|
|
||||||
|
### 1.2 本阶段目标(范围边界)
|
||||||
|
|
||||||
|
| 目标 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **配置方式** | 通过 **数据库表** 维护「哪些租户启用访客楼层策略及允许列表」;由实施/DBA/SQL 脚本录入,**不提供** 物业/客服可视化管理界面(后续阶段再做)。 |
|
||||||
|
| **行为** | 仅对**存在有效策略行**的 `businessId`:在 **UC-01** 路径上,在取得被访人 `floorList` 后执行 **`allow_zone_ids ∩ floorList`**(顺序建议保持 **`floorList` 顺序** 过滤)。 |
|
||||||
|
| **兼容** | 表中**无**该租户配置、或 `enabled=0`、或 `allow_zone_ids` 为空:与现网 **UC-01** 一致(仍使用组织返回的 `floorList` 全集)。 |
|
||||||
|
| **显式楼层** | 请求体已带**非空** `floorIds`(**UC-02**):本阶段 **不改变** 现网语义,**不**读策略表、**不**对传入列表做求交(与产品方案 **AC-4** 一致;若未来合同要求「显式也求交」须另立变更)。 |
|
||||||
|
| **访客业务系统 / 登记页** | 本阶段**不要求**改第三方 BFF;但若登记页仍只拉组织 `floorList` 展示,则**展示可能与电梯最终开通楼层不一致** —— 见 **§8 风险与后续工作**。 |
|
||||||
|
|
||||||
|
### 1.3 非目标(明确排除)
|
||||||
|
|
||||||
|
- 物业管理端 CRUD 页面、审计日志界面、策略版本与登记单快照联动(产品方案 §2.7、§3.4 完整能力)。
|
||||||
|
- 组织侧收窄 `floorList`、Nacos 配置中心等其它路径(参见产品方案 §4.1 方案族)。
|
||||||
|
- 按楼栋 `building_id` 的多套策略并行(表结构可预留字段,本阶段查询规则见 **§4.3**)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 设计原则与安全约束
|
||||||
|
|
||||||
|
1. **权限上界(不变量)**
|
||||||
|
租户策略仅表达**允许集合**;在 **UC-01** 路径下,最终写入电梯规则的 `zoneId` 列表必须是 **`allow_zone_ids` 与 `PersonResult.floorList` 的交集** 的子集(实现上等价于交集本身)。**禁止**在交集为空时静默落到其它楼层。
|
||||||
|
|
||||||
|
2. **交集为空**
|
||||||
|
租户已配置非空允许列表,但与被访人 `floorList` **无交集**:返回**明确业务错误**(禁止继续 `zone/page`、`get(0)` 等,避免走查文档 **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-1~AC-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` |
|
||||||
|
| 修订触发 | `add/visitor` 契约变更;策略表字段变更;决定 UC-02 是否参与求交 |
|
||||||
|
| 关联 PR | 实现类、`Mapper.xml`、DDL 脚本与错误码资源文件 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文档为数据库阶段详细设计;实施以现网分支、代码评审与安全评审结论为准。*
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- 租户访客默认楼层策略(电梯应用库)
|
||||||
|
-- 设计说明:docs/business/租户访客默认楼层-数据库配置阶段技术设计.md
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS 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 求交)';
|
||||||
|
|
||||||
|
-- 示例(实施时替换占位符后执行)
|
||||||
|
-- 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);
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package cn.cloudwalk.elevator.person.dao;
|
||||||
|
|
||||||
|
import cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto;
|
||||||
|
|
||||||
|
public interface TenantVisitorFloorPolicyDao {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询租户级启用中的 INTERSECT_ALLOWLIST 策略(building_id 为空)。
|
||||||
|
*
|
||||||
|
* @param businessId 机构 ID
|
||||||
|
* @return 无配置时 null
|
||||||
|
*/
|
||||||
|
TenantVisitorFloorPolicyDto selectEnabledTenantDefault(String businessId);
|
||||||
|
}
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
package cn.cloudwalk.elevator.person.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户访客楼层策略(表 tenant_visitor_floor_policy 行映射)。
|
||||||
|
*/
|
||||||
|
public class TenantVisitorFloorPolicyDto {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private String businessId;
|
||||||
|
private String policyType;
|
||||||
|
private String allowZoneIds;
|
||||||
|
private String buildingId;
|
||||||
|
private Integer enabled;
|
||||||
|
private Long policyVersion;
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBusinessId() {
|
||||||
|
return businessId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBusinessId(String businessId) {
|
||||||
|
this.businessId = businessId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPolicyType() {
|
||||||
|
return policyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPolicyType(String policyType) {
|
||||||
|
this.policyType = policyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAllowZoneIds() {
|
||||||
|
return allowZoneIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowZoneIds(String allowZoneIds) {
|
||||||
|
this.allowZoneIds = allowZoneIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBuildingId() {
|
||||||
|
return buildingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBuildingId(String buildingId) {
|
||||||
|
this.buildingId = buildingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(Integer enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getPolicyVersion() {
|
||||||
|
return policyVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPolicyVersion(Long policyVersion) {
|
||||||
|
this.policyVersion = policyVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
package cn.cloudwalk.elevator.person.impl;
|
||||||
|
|
||||||
|
import cn.cloudwalk.elevator.person.dao.TenantVisitorFloorPolicyDao;
|
||||||
|
import cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto;
|
||||||
|
import cn.cloudwalk.elevator.person.mapper.TenantVisitorFloorPolicyMapper;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class TenantVisitorFloorPolicyDaoImpl implements TenantVisitorFloorPolicyDao {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TenantVisitorFloorPolicyMapper tenantVisitorFloorPolicyMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TenantVisitorFloorPolicyDto selectEnabledTenantDefault(String businessId) {
|
||||||
|
return this.tenantVisitorFloorPolicyMapper.selectEnabledTenantDefault(businessId);
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package cn.cloudwalk.elevator.person.mapper;
|
||||||
|
|
||||||
|
import cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
public interface TenantVisitorFloorPolicyMapper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户级默认策略:building_id 为空,启用,INTERSECT_ALLOWLIST。
|
||||||
|
*/
|
||||||
|
TenantVisitorFloorPolicyDto selectEnabledTenantDefault(@Param("businessId") String businessId);
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<mapper namespace="cn.cloudwalk.elevator.person.mapper.TenantVisitorFloorPolicyMapper">
|
||||||
|
|
||||||
|
<select id="selectEnabledTenantDefault" resultType="cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto">
|
||||||
|
SELECT id,
|
||||||
|
business_id AS businessId,
|
||||||
|
policy_type AS policyType,
|
||||||
|
allow_zone_ids AS allowZoneIds,
|
||||||
|
building_id AS buildingId,
|
||||||
|
enabled AS enabled,
|
||||||
|
policy_version AS policyVersion
|
||||||
|
FROM tenant_visitor_floor_policy
|
||||||
|
WHERE business_id = #{businessId,jdbcType=VARCHAR}
|
||||||
|
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
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
+70
-2
@@ -29,6 +29,8 @@ import cn.cloudwalk.elevator.passrule.dto.ImageRuleRefAddDto;
|
|||||||
import cn.cloudwalk.elevator.passrule.dto.ImageRuleRefResultDto;
|
import cn.cloudwalk.elevator.passrule.dto.ImageRuleRefResultDto;
|
||||||
import cn.cloudwalk.elevator.passrule.impl.AbstractAcsPassService;
|
import cn.cloudwalk.elevator.passrule.impl.AbstractAcsPassService;
|
||||||
import cn.cloudwalk.elevator.passrule.result.AcsPassRuleResult;
|
import cn.cloudwalk.elevator.passrule.result.AcsPassRuleResult;
|
||||||
|
import cn.cloudwalk.elevator.person.dao.TenantVisitorFloorPolicyDao;
|
||||||
|
import cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto;
|
||||||
import cn.cloudwalk.elevator.person.param.AcsPersonAddParam;
|
import cn.cloudwalk.elevator.person.param.AcsPersonAddParam;
|
||||||
import cn.cloudwalk.elevator.person.param.AcsPersonAddVisitorParam;
|
import cn.cloudwalk.elevator.person.param.AcsPersonAddVisitorParam;
|
||||||
import cn.cloudwalk.elevator.person.param.AcsPersonDeleteParam;
|
import cn.cloudwalk.elevator.person.param.AcsPersonDeleteParam;
|
||||||
@@ -45,12 +47,16 @@ import cn.cloudwalk.elevator.util.StringUtils;
|
|||||||
import cn.cloudwalk.elevator.zone.param.ZoneQueryParam;
|
import cn.cloudwalk.elevator.zone.param.ZoneQueryParam;
|
||||||
import cn.cloudwalk.elevator.zone.result.ZoneResult;
|
import cn.cloudwalk.elevator.zone.result.ZoneResult;
|
||||||
import cn.cloudwalk.elevator.zone.service.ZoneService;
|
import cn.cloudwalk.elevator.zone.service.ZoneService;
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -73,6 +79,8 @@ public class PersonRuleServiceImpl extends AbstractAcsPassService implements Per
|
|||||||
private AcsElevatorDeviceDao acsElevatorDeviceDao;
|
private AcsElevatorDeviceDao acsElevatorDeviceDao;
|
||||||
@Resource
|
@Resource
|
||||||
private ZoneService zoneService;
|
private ZoneService zoneService;
|
||||||
|
@Resource
|
||||||
|
private TenantVisitorFloorPolicyDao tenantVisitorFloorPolicyDao;
|
||||||
|
|
||||||
@CloudwalkParamsValidate
|
@CloudwalkParamsValidate
|
||||||
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
|
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
|
||||||
@@ -167,12 +175,47 @@ public class PersonRuleServiceImpl extends AbstractAcsPassService implements Per
|
|||||||
this.logger.info("根据被访人添加访客派梯权限开始,AcsPersonAddVisitorParam=[{}], CloudwalkCallContext=[{}]",
|
this.logger.info("根据被访人添加访客派梯权限开始,AcsPersonAddVisitorParam=[{}], CloudwalkCallContext=[{}]",
|
||||||
JSONObject.toJSONString(param), JSONObject.toJSONString(context));
|
JSONObject.toJSONString(param), JSONObject.toJSONString(context));
|
||||||
try {
|
try {
|
||||||
if (CollectionUtils.isEmpty(param.getFloorIds())) {
|
boolean callerProvidedFloors = !CollectionUtils.isEmpty(param.getFloorIds());
|
||||||
|
if (!callerProvidedFloors) {
|
||||||
PersonDetailParam detailParam = new PersonDetailParam();
|
PersonDetailParam detailParam = new PersonDetailParam();
|
||||||
detailParam.setId(param.getPersonId());
|
detailParam.setId(param.getPersonId());
|
||||||
detailParam.setBusinessId(context.getCompany().getCompanyId());
|
detailParam.setBusinessId(context.getCompany().getCompanyId());
|
||||||
CloudwalkResult<PersonResult> detail = this.personService.detail(detailParam, context);
|
CloudwalkResult<PersonResult> detail = this.personService.detail(detailParam, context);
|
||||||
param.setFloorIds(((PersonResult)detail.getData()).getFloorList());
|
if (detail == null || !detail.isSuccess()) {
|
||||||
|
String code = detail != null ? detail.getCode() : "76260531";
|
||||||
|
String msg = detail != null ? detail.getMessage() : getMessage("76260531");
|
||||||
|
return CloudwalkResult.fail(code, msg);
|
||||||
|
}
|
||||||
|
PersonResult personResult = (PersonResult)detail.getData();
|
||||||
|
if (personResult == null) {
|
||||||
|
return CloudwalkResult.fail("76260531", getMessage("76260531"));
|
||||||
|
}
|
||||||
|
List<String> hostFloors = personResult.getFloorList();
|
||||||
|
if (CollectionUtils.isEmpty(hostFloors)) {
|
||||||
|
return CloudwalkResult.fail("76260531", getMessage("76260531"));
|
||||||
|
}
|
||||||
|
List<String> effectiveFloors = hostFloors;
|
||||||
|
TenantVisitorFloorPolicyDto policy =
|
||||||
|
this.tenantVisitorFloorPolicyDao.selectEnabledTenantDefault(context.getCompany().getCompanyId());
|
||||||
|
if (policy != null && policy.getEnabled() != null && policy.getEnabled().intValue() == 1) {
|
||||||
|
List<String> allow = parseAllowZoneIds(policy.getAllowZoneIds());
|
||||||
|
if (!CollectionUtils.isEmpty(allow)) {
|
||||||
|
Set<String> allowSet = new HashSet<>(allow);
|
||||||
|
List<String> intersected = intersectPreserveHostOrder(hostFloors, allowSet);
|
||||||
|
if (intersected.isEmpty()) {
|
||||||
|
return CloudwalkResult.fail("76260532", getMessage("76260532"));
|
||||||
|
}
|
||||||
|
effectiveFloors = intersected;
|
||||||
|
this.logger.info(
|
||||||
|
"租户访客楼层策略求交 businessId={} personId={} visitorId={} policyId={} policyVersion={} effectiveSize={}",
|
||||||
|
context.getCompany().getCompanyId(), param.getPersonId(), param.getVisitorId(),
|
||||||
|
policy.getId(), policy.getPolicyVersion(), Integer.valueOf(intersected.size()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
param.setFloorIds(effectiveFloors);
|
||||||
|
}
|
||||||
|
if (CollectionUtils.isEmpty(param.getFloorIds())) {
|
||||||
|
return CloudwalkResult.fail("76260531", getMessage("76260531"));
|
||||||
}
|
}
|
||||||
ZoneQueryParam zoneQueryParam = new ZoneQueryParam();
|
ZoneQueryParam zoneQueryParam = new ZoneQueryParam();
|
||||||
zoneQueryParam.setId(param.getFloorIds().get(0));
|
zoneQueryParam.setId(param.getFloorIds().get(0));
|
||||||
@@ -221,6 +264,8 @@ public class PersonRuleServiceImpl extends AbstractAcsPassService implements Per
|
|||||||
refParam.setPersonIds(Collections.singletonList(param.getVisitorId()));
|
refParam.setPersonIds(Collections.singletonList(param.getVisitorId()));
|
||||||
refParam.setImageStoreId(imageStoreId);
|
refParam.setImageStoreId(imageStoreId);
|
||||||
this.imageStorePersonService.updateGroupPersonRef(refParam, context);
|
this.imageStorePersonService.updateGroupPersonRef(refParam, context);
|
||||||
|
} catch (ServiceException e) {
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
this.logger.error("根据被访人添加访客派梯权限失败,原因:[{}]", e);
|
this.logger.error("根据被访人添加访客派梯权限失败,原因:[{}]", e);
|
||||||
throw new ServiceException("76260530", getMessage("76260530"));
|
throw new ServiceException("76260530", getMessage("76260530"));
|
||||||
@@ -228,6 +273,29 @@ public class PersonRuleServiceImpl extends AbstractAcsPassService implements Per
|
|||||||
return CloudwalkResult.success(Boolean.valueOf(true));
|
return CloudwalkResult.success(Boolean.valueOf(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 allow_zone_ids JSON;无效或空则返回空列表(等同未配置有效策略)。
|
||||||
|
*/
|
||||||
|
private List<String> parseAllowZoneIds(String json) {
|
||||||
|
if (StringUtils.isBlank(json)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<String> list = JSON.parseArray(json, String.class);
|
||||||
|
if (list == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return list.stream().filter(Objects::nonNull).filter(s -> !s.isEmpty()).collect(Collectors.toList());
|
||||||
|
} catch (Exception e) {
|
||||||
|
this.logger.warn("allow_zone_ids JSON 无效,按无策略处理: {}", e.getMessage());
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> intersectPreserveHostOrder(List<String> hostFloors, Set<String> allowSet) {
|
||||||
|
return hostFloors.stream().filter(allowSet::contains).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
public CloudwalkResult<Boolean> edit(AcsPersonEditParam param, CloudwalkCallContext context)
|
public CloudwalkResult<Boolean> edit(AcsPersonEditParam param, CloudwalkCallContext context)
|
||||||
throws ServiceException {
|
throws ServiceException {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user