mirror of
https://github.com/hpd840321/starRiverProperty.git
synced 2026-06-09 08:20:31 +08:00
docs: mark elevator-side tenant policy SQL as deprecated, add guangfa visitor floor design
- Deprecate elevator-side tenant_visitor_floor_policy SQL files (V2 queries only component-organization library) - Add guangfa 28F visitor floor design spec (table-driven approach A) - Add complete database ER diagram (14 DBs, 537 tables) - Add implementation plan for guangfa visitor floor policy - Code walkthrough docs for visitor floor policy analysis
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
# 广发基金访客默认28F — 方案A实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 将广发基金访客默认楼层从 28F (单层) 扩展为目标楼层列表, 通过 UPDATE `tenant_visitor_floor_policy.allow_zone_ids` 实现, 零代码变更。
|
||||
|
||||
**Architecture:** 纯数据变更。利用 V2 已有的 `tenant_visitor_floor_policy` 表驱动策略架构, 在 `ImgPersonServiceImpl.detail()` L643-648 的策略替代块中自动生效。仅修改 component-organization 库, 电梯侧不参与。
|
||||
|
||||
**Tech Stack:** MySQL (component-organization 库), Shell (验证)
|
||||
|
||||
---
|
||||
|
||||
## 前置确认
|
||||
|
||||
实施前需确认目标楼层列表 (zone_id)。当前仅28F:
|
||||
|
||||
```
|
||||
zone_id = 605560545117995008 (28F, code=0x1C)
|
||||
```
|
||||
|
||||
若仅为演示 28F 单层方案, 无需修改数据 (当前已是 28F)。若需扩展到多楼层, 需先确认 zone_id 列表。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 确认目标 zone_id 列表 (调研)
|
||||
|
||||
**Files:**
|
||||
- (不修改文件)
|
||||
|
||||
- [ ] **Step 1: 查询当前 20 层 zone_id**
|
||||
|
||||
```sql
|
||||
-- 连接: mysql -h 192.168.3.12 -P 3307 -u root -p'123456' cw-elevator-application
|
||||
|
||||
SELECT zone_id, zone_name, code, parent_id
|
||||
FROM code_elevator_area
|
||||
WHERE parent_id = '605560539791228928'
|
||||
AND zone_name NOT LIKE '%B%'
|
||||
ORDER BY CAST(zone_name AS UNSIGNED) ASC;
|
||||
```
|
||||
|
||||
预期: 返回所有可用楼层, 确认目标 20 层的 zone_id 列表。
|
||||
|
||||
- [ ] **Step 2: 记录目标楼层列表**
|
||||
|
||||
将 zone_id 列表整理为 JSON 数组格式:
|
||||
```json
|
||||
["605560545117995008", "zone_id_2", ..., "zone_id_20"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 更新策略数据 (生产库)
|
||||
|
||||
**Files:**
|
||||
- (不修改文件)
|
||||
|
||||
- [ ] **Step 1: 备份当前策略**
|
||||
|
||||
```sql
|
||||
-- 连接: component-organization 库
|
||||
CREATE TABLE tenant_visitor_floor_policy_bak_20260509 AS
|
||||
SELECT * FROM tenant_visitor_floor_policy
|
||||
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
|
||||
```
|
||||
|
||||
预期: `Query OK, 1 row affected`
|
||||
|
||||
- [ ] **Step 2: 验证当前策略状态**
|
||||
|
||||
```sql
|
||||
SELECT id, org_id, allow_zone_ids, enabled, policy_version
|
||||
FROM tenant_visitor_floor_policy
|
||||
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
|
||||
```
|
||||
|
||||
预期: `enabled=1, allow_zone_ids='["605560545117995008"]'`
|
||||
|
||||
- [ ] **Step 3: 更新 allow_zone_ids**
|
||||
|
||||
```sql
|
||||
-- 以28F单层为例 (若为20层则替换为Task 1确认的列表)
|
||||
UPDATE tenant_visitor_floor_policy
|
||||
SET allow_zone_ids = '["605560545117995008"]',
|
||||
policy_version = policy_version + 1,
|
||||
remark = '广发基金:访客默认28F (2026-05-09 方案A实施)',
|
||||
updated_at = UNIX_TIMESTAMP(NOW()) * 1000
|
||||
WHERE id = 'gf_vstr_policy_guangfa_fund_001x'
|
||||
AND enabled = 1;
|
||||
```
|
||||
|
||||
预期: `Query OK, 1 row affected`
|
||||
|
||||
- [ ] **Step 4: 验证更新结果**
|
||||
|
||||
```sql
|
||||
SELECT id, org_id, allow_zone_ids, enabled, policy_version, updated_at
|
||||
FROM tenant_visitor_floor_policy
|
||||
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
|
||||
```
|
||||
|
||||
预期: `policy_version` 自增 1, `allow_zone_ids` 为新列表
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 更新种子 SQL
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/sql/organization_tenant_visitor_floor_policy_init_tenants.sql`
|
||||
|
||||
- [ ] **Step 1: 更新广发基金策略种子数据**
|
||||
|
||||
找到文件中 `gf_vstr_policy_guangfa_fund_001x` 的 INSERT 语句, 更新 `allow_zone_ids`:
|
||||
|
||||
```sql
|
||||
-- 原内容 (~第 33 行):
|
||||
'["605560545117995008"]',
|
||||
|
||||
-- 改为:
|
||||
'["605560545117995008"]',
|
||||
```
|
||||
|
||||
同步更新 `remark` 字段:
|
||||
|
||||
```sql
|
||||
-- 原内容:
|
||||
'广发基金:访客楼层策略(组织库);默认 28F。',
|
||||
|
||||
-- 改为:
|
||||
'广发基金:访客楼层策略(组织库);默认 28F (方案A-20260509)。',
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证种子文件一致性**
|
||||
|
||||
确认 `org_id = '488b8ad049bb43408a6fbcc50bcb89ac'` 与生产库一致, `policy_type = 'INTERSECT_ALLOWLIST'` 不变。
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 标记电梯侧 SQL 为废弃
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/sql/tenant_visitor_floor_policy_init_guangfa_fund.sql`
|
||||
|
||||
- [ ] **Step 1: 添加废弃注释**
|
||||
|
||||
在文件头部添加:
|
||||
|
||||
```sql
|
||||
-- @deprecated 2026-05-09: V2 架构中策略仅在 component-organization 库维护。
|
||||
-- cw-elevator-application 不再查询此表 (PersonRuleServiceImpl.addVisitor()
|
||||
-- 已移除策略代码)。此文件保留仅供历史参考。
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 同样标记物业策略文件**
|
||||
|
||||
修改 `docs/sql/tenant_visitor_floor_policy_init_property_mgmt_6f.sql`:
|
||||
|
||||
```sql
|
||||
-- @deprecated 2026-05-09: 同上。物业策略由 organization_tenant_visitor_floor_policy_init_tenants.sql 维护。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 功能验证
|
||||
|
||||
**Files:**
|
||||
- (不修改文件)
|
||||
|
||||
- [ ] **Step 1: 验证 detail() floorList 策略生效**
|
||||
|
||||
```bash
|
||||
# 广发基金被访人 1072908835884208128
|
||||
curl -s -X POST http://127.0.0.1:18081/elevator/person/add/visitor \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"personId": "1072908835884208128",
|
||||
"businessId": "2524639890ba4f2cba9ba1a4eeaa4015",
|
||||
"visitorName": "test_28f_verify",
|
||||
"begVisitorTime": "2026-05-09 00:00:00",
|
||||
"endVisitorTime": "2026-12-31 23:59:59"
|
||||
}' | python3 -m json.tool
|
||||
```
|
||||
|
||||
预期: 返回 `success: true`, 不返回 `76260531` (无楼层错误)。
|
||||
|
||||
- [ ] **Step 2: 验证 image_rule_ref 写入**
|
||||
|
||||
```sql
|
||||
-- 使用上一步返回的 visitorId
|
||||
SELECT zone_id, zone_name, person_id, parent_rule
|
||||
FROM image_rule_ref
|
||||
WHERE person_id = '<visitor_id_from_step1>';
|
||||
```
|
||||
|
||||
预期: 写入行数等于 `allow_zone_ids` 中 zone 数量 (当前为 1 行, zone_name=28F)。
|
||||
|
||||
- [ ] **Step 3: 验证物业公司不受影响**
|
||||
|
||||
```bash
|
||||
# 物业公司被访人 (使用不同的 personId)
|
||||
curl -s -X POST http://127.0.0.1:18081/elevator/person/add/visitor \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"personId": "<物业员工personId>",
|
||||
"businessId": "2524639890ba4f2cba9ba1a4eeaa4015",
|
||||
"visitorName": "test_pm_verify",
|
||||
"begVisitorTime": "2026-05-09 00:00:00",
|
||||
"endVisitorTime": "2026-12-31 23:59:59"
|
||||
}' | python3 -m json.tool
|
||||
```
|
||||
|
||||
预期: 返回 `success: true`, 物业访客仍获 40F 或 6F (取决于 org 归属)。
|
||||
|
||||
- [ ] **Step 4: 日志验证 — 确认 [GF-28F] 策略命中**
|
||||
|
||||
```bash
|
||||
# 检查 component-org 日志 (detail 中的策略替代)
|
||||
grep "POLICY\|replacementZoneIds\|allow_zone_ids" <component-org-log> | tail -5
|
||||
```
|
||||
|
||||
预期: 日志中出现 `[POLICY] entry orgIds=[488b8ad...]` 和 allow_zone_ids 解析结果。
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Commit
|
||||
|
||||
- [ ] **Step 1: 提交种子 SQL 变更**
|
||||
|
||||
```bash
|
||||
git add docs/sql/organization_tenant_visitor_floor_policy_init_tenants.sql
|
||||
git add docs/sql/tenant_visitor_floor_policy_init_guangfa_fund.sql
|
||||
git add docs/sql/tenant_visitor_floor_policy_init_property_mgmt_6f.sql
|
||||
git commit -m "docs: mark elevator-side tenant policy SQL as deprecated (V2 only queries component-org)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 回滚方案
|
||||
|
||||
```sql
|
||||
-- 恢复到 28F 单层
|
||||
UPDATE tenant_visitor_floor_policy
|
||||
SET allow_zone_ids = '["605560545117995008"]',
|
||||
policy_version = policy_version + 1,
|
||||
remark = '广发基金:访客默认28F(回滚)',
|
||||
updated_at = UNIX_TIMESTAMP(NOW()) * 1000
|
||||
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
|
||||
-- 即刻生效, 无需重启
|
||||
```
|
||||
@@ -0,0 +1,715 @@
|
||||
# 星河湾星中星平台 — 完整数据库架构与 ER 关系文档
|
||||
|
||||
**日期**: 2026-05-09
|
||||
**MySQL**: 192.168.3.12:3307 (root/123456)
|
||||
**状态**: 完整梳理
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [数据库总览](#1-数据库总览)
|
||||
2. [核心库 1: component-organization](#2-component-organization-组织人员库)
|
||||
3. [核心库 2: cw-elevator-application](#3-cw-elevator-application-电梯应用库)
|
||||
4. [核心库 3: ninca_common](#4-ninca_common-公共组件库)
|
||||
5. [辅助库: cwos_portal](#5-cwos_portal-门户管理库)
|
||||
6. [辅助库: cwos_manager](#6-cwos_manager-运维管理库)
|
||||
7. [辅助库: alarm_deploy](#7-alarm_deploy-报警服务库)
|
||||
8. [其他库汇总](#8-其他数据库)
|
||||
9. [跨库关系图 (ER)](#9-跨库关系图)
|
||||
10. [访客邀约+派梯完整数据流](#10-访客邀约派梯完整数据流)
|
||||
|
||||
---
|
||||
|
||||
## 1. 数据库总览
|
||||
|
||||
| # | 数据库 | 表数 | 用途 | 所属服务 |
|
||||
|---|--------|------|------|---------|
|
||||
| 1 | `component-organization` | 40 | 组织/人员/标签/策略 | ninca-common-component-organization |
|
||||
| 2 | `cw-elevator-application` | 33 | 电梯规则/识别/记录 | cw-elevator-application |
|
||||
| 3 | `ninca_common` | 38 | 区域/设备/消息/文件 | ninca-common (共享服务) |
|
||||
| 4 | `cwos_portal` | 155 | 设备/图库/鉴权/账号 | cwos-portal, cwos-system-api |
|
||||
| 5 | `cwos_manager` | 33 | 运维/部署/配置 | cwos-manager |
|
||||
| 6 | `alarm_deploy` | 49 | 报警/布控/识别记录 | ninca-qk-alarm |
|
||||
| 7 | `person_file` | 56 | 人像档案 | ninca-person-file |
|
||||
| 8 | `snap_deploy` | 1 | 抓拍部署配置 | snap-app |
|
||||
| 9 | `cloudwalk_device_thirdparty` | 5 | 第三方设备网关 | device-auth |
|
||||
| 10 | `g` | 1 | (shard) | — |
|
||||
| 11 | `ods` | 59 | 运营数据仓库 | — |
|
||||
| 12 | `p` | 4 | (shard) | — |
|
||||
| 13 | `xqconfig` | 59 | CRK 引擎配置 (Hash) | ninca-crk-std |
|
||||
| 14 | `xqfacerecog` | 4 | CRK 人脸识别结果 | ninca-crk-std |
|
||||
|
||||
**访客邀约+派梯核心链路涉及**: `component-organization` ↔ `cw-elevator-application` ↔ `ninca_common`
|
||||
|
||||
---
|
||||
|
||||
## 2. component-organization (组织人员库)
|
||||
|
||||
**服务**: `ninca-common-component-organization`
|
||||
**连接**: `jdbc:mysql://192.168.3.12:3307/component-organization`
|
||||
|
||||
### 2.1 表清单 (40 张)
|
||||
|
||||
| 表名 | 用途 | 关键外键 |
|
||||
|------|------|---------|
|
||||
| `cw_is_person` | 人员主表 | — |
|
||||
| `cw_is_person_label_ref` | 人员↔标签关联 | PERSON_ID → cw_is_person.ID, LABEL_ID → cw_is_label.ID |
|
||||
| `cw_is_label` | 标签主表 | — |
|
||||
| `cw_is_organization` | 组织架构主表 | PARENT_ID → cw_is_organization.ID |
|
||||
| `cw_is_person_organization_ref` | 人员↔组织关联 | PERSON_ID → cw_is_person.ID, ORG_ID → cw_is_organization.ID |
|
||||
| `org_floor` | 组织↔楼层关联 | org_id → cw_is_organization.ID |
|
||||
| `tenant_visitor_floor_policy` | 租户访客楼层策略 | org_id → cw_is_organization.ID |
|
||||
| `cw_is_group_person_ref` | 图库↔人员关联 | PERSON_ID → cw_is_person.ID, IMAGE_STORE_ID → cw_is_device_image_store.ID |
|
||||
| `cw_is_device_image_store` | 图库主表 | — |
|
||||
| `cw_is_image_store_associated_ref` | 图库↔组织/标签关联 | IMAGE_STORE_ID → cw_is_device_image_store.ID |
|
||||
| `cw_is_device_person` | 设备人员同步 | — |
|
||||
| `cw_is_device_person_sync_log` | 同步日志 | — |
|
||||
| `cw_is_person_properties` | 人员属性定义 | — |
|
||||
| `cw_is_person_properties_switch` | 属性开关 | — |
|
||||
| `cw_is_person_audit` | 人员审核记录 | — |
|
||||
| `cw_is_person_registry` | 人员注册 | — |
|
||||
| `cw_is_person_registry_device` | 注册设备 | — |
|
||||
| `cw_is_person_registry_properties` | 注册属性 | — |
|
||||
| `cw_is_person_batch_import` | 批量导入主表 | — |
|
||||
| `cw_is_person_batch_detail` | 批量导入明细 | — |
|
||||
| `cw_is_organization_extend` | 组织扩展 | — |
|
||||
| `cw_is_organization_extend_detail` | 组织扩展明细 | — |
|
||||
| `cw_is_organization_type` | 组织类型 | — |
|
||||
| `cw_is_organization_type_properties` | 组织类型属性 | — |
|
||||
| `cw_is_organization_area_ref` | 组织↔区域关联 | — |
|
||||
| `cw_is_organization_image_store` | 组织图库 | — |
|
||||
| `cw_operation_log` | 操作日志 | — |
|
||||
| `cw_task_job_everytime_details` | 定时任务 | — |
|
||||
| `QRTZ_*` (11 张) | Quartz 调度表 | — |
|
||||
|
||||
### 2.2 核心表 DDL
|
||||
|
||||
#### cw_is_person (人员主表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `cw_is_person` (
|
||||
`ID` varchar(32) NOT NULL COMMENT '主键ID',
|
||||
`BUSINESS_ID` varchar(32) NOT NULL COMMENT '企业ID (tenant)',
|
||||
`PERSON_CODE` varchar(64) DEFAULT NULL COMMENT '人员CODE',
|
||||
`NAME` varchar(64) DEFAULT NULL COMMENT '姓名',
|
||||
`USER_NAME` varchar(255) DEFAULT NULL COMMENT '用户名',
|
||||
`PHONE` varchar(64) DEFAULT NULL COMMENT '联系电话',
|
||||
`EMAIL` varchar(64) DEFAULT NULL COMMENT '邮箱',
|
||||
`STATUS` smallint(2) DEFAULT NULL COMMENT '人员状态(0有效 1无效)',
|
||||
`EXPIRY_BEGIN_DATE` decimal(20,0) DEFAULT NULL COMMENT '有效期开始(时间戳)',
|
||||
`EXPIRY_END_DATE` decimal(20,0) DEFAULT NULL COMMENT '有效期结束(时间戳)',
|
||||
`SHOW_PICTURE` varchar(255) DEFAULT NULL COMMENT '展示照',
|
||||
`COMPARE_PICTURE` text COMMENT '比对照',
|
||||
`IMAGE_ID` varchar(255) DEFAULT NULL COMMENT '识别照',
|
||||
`IS_DEL` smallint(2) DEFAULT NULL COMMENT '0未删除 1已删除',
|
||||
`CREATE_TIME` decimal(20,0) DEFAULT NULL,
|
||||
`CREATE_USER_ID` varchar(32) DEFAULT NULL,
|
||||
`LAST_UPDATE_TIME` decimal(20,0) DEFAULT NULL,
|
||||
`LAST_UPDATE_USER_ID` varchar(32) DEFAULT NULL,
|
||||
-- EXT1~EXT40 (40个扩展字段)
|
||||
`SOURCE` smallint(1) NOT NULL DEFAULT '1' COMMENT '1页面管理 2抓拍',
|
||||
`IC_CARD_NO` varchar(255) DEFAULT NULL COMMENT 'IC卡号',
|
||||
`IC_CARD_TYPE` varchar(255) DEFAULT NULL COMMENT 'IC卡类型',
|
||||
`DEFAULT_FLOOR` varchar(255) DEFAULT NULL COMMENT '★默认楼层zone_id',
|
||||
`CHOOSE_FLOOR` text COMMENT '★选中的楼层(被访人可访问楼层)',
|
||||
PRIMARY KEY (`ID`),
|
||||
KEY `IDX_IP_PAGE` (`IS_DEL`,`BUSINESS_ID`,`LAST_UPDATE_TIME`,`ID`,`SOURCE`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人员信息表';
|
||||
```
|
||||
|
||||
#### cw_is_label (标签主表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `cw_is_label` (
|
||||
`ID` varchar(32) NOT NULL COMMENT '唯一标识',
|
||||
`NAME` varchar(64) DEFAULT NULL COMMENT '标签名(如"访客")',
|
||||
`CODE` varchar(64) NOT NULL COMMENT '编码(访客标签CODE="1")',
|
||||
`BUSINESS_ID` varchar(32) DEFAULT NULL,
|
||||
`IS_DEL` smallint(2) DEFAULT NULL COMMENT '0未删 1已删',
|
||||
`ADD_TYPE` smallint(2) DEFAULT NULL COMMENT '0手动 1应用初始 2应用创建',
|
||||
PRIMARY KEY (`ID`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
```
|
||||
|
||||
#### cw_is_person_label_ref (人员↔标签关联)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `cw_is_person_label_ref` (
|
||||
`ID` varchar(32) NOT NULL,
|
||||
`PERSON_ID` varchar(32) NOT NULL COMMENT '→ cw_is_person.ID',
|
||||
`LABEL_ID` varchar(32) NOT NULL COMMENT '→ cw_is_label.ID',
|
||||
PRIMARY KEY (`ID`),
|
||||
KEY `IDX_PERSON` (`PERSON_ID`),
|
||||
KEY `IDX_LABEL` (`LABEL_ID`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人员标签关联表';
|
||||
```
|
||||
|
||||
#### cw_is_organization (组织架构主表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `cw_is_organization` (
|
||||
`ID` varchar(32) NOT NULL,
|
||||
`NAME` varchar(60) DEFAULT NULL COMMENT '如"[28-38F]广发基金管理有限公司"',
|
||||
`PARENT_ID` varchar(32) DEFAULT NULL COMMENT '→ 自身.ID (树形)',
|
||||
`BUSINESS_ID` varchar(32) DEFAULT NULL,
|
||||
`TYPE_ID` varchar(32) DEFAULT NULL,
|
||||
`ORDER_BY` int(2) DEFAULT NULL,
|
||||
`IS_DEL` smallint(2) DEFAULT NULL,
|
||||
`IS_VALID` int(2) DEFAULT NULL COMMENT '0禁用 1启用',
|
||||
PRIMARY KEY (`ID`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
```
|
||||
|
||||
#### cw_is_person_organization_ref (人员↔组织关联)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `cw_is_person_organization_ref` (
|
||||
`ID` varchar(32) NOT NULL,
|
||||
`PERSON_ID` varchar(32) NOT NULL COMMENT '→ cw_is_person.ID',
|
||||
`ORG_ID` varchar(32) NOT NULL COMMENT '→ cw_is_organization.ID',
|
||||
PRIMARY KEY (`ID`),
|
||||
KEY `IDX_PERSON` (`PERSON_ID`),
|
||||
KEY `IDX_ORG` (`ORG_ID`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人员组织关联表';
|
||||
```
|
||||
|
||||
#### org_floor (组织↔楼层映射)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `org_floor` (
|
||||
`org_id` varchar(64) NOT NULL COMMENT '→ cw_is_organization.ID',
|
||||
`zone_id` varchar(64) DEFAULT NULL COMMENT '楼层zone_id',
|
||||
`is_all` smallint(2) NOT NULL COMMENT '0-全部 1-非全部',
|
||||
`zone_name` varchar(100) DEFAULT NULL COMMENT '楼层名'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='机构楼层对应表';
|
||||
```
|
||||
|
||||
#### tenant_visitor_floor_policy (访客楼层策略)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `tenant_visitor_floor_policy` (
|
||||
`id` varchar(32) NOT NULL,
|
||||
`business_id` varchar(64) DEFAULT NULL COMMENT 'DEPRECATED: 历史字段',
|
||||
`org_id` varchar(32) DEFAULT NULL COMMENT '→ cw_is_organization.ID (★隔离键)',
|
||||
`policy_type` varchar(32) NOT NULL DEFAULT 'INTERSECT_ALLOWLIST' COMMENT '策略类型(历史命名;语义为替代)',
|
||||
`allow_zone_ids` text COMMENT '★ JSON数组 zoneId列表',
|
||||
`building_id` varchar(64) DEFAULT NULL,
|
||||
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1启用 0停用',
|
||||
`policy_version` bigint(20) NOT NULL DEFAULT '1',
|
||||
`remark` varchar(256) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_org_building` (`org_id`,`building_id`),
|
||||
KEY `idx_org_enabled` (`org_id`,`enabled`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户访客楼层策略(组织库;detail/访客列表替代floorList)';
|
||||
```
|
||||
|
||||
**当前数据 (广发基金)**:
|
||||
- `id` = `gf_vstr_policy_guangfa_fund_001x`
|
||||
- `org_id` = `488b8ad049bb43408a6fbcc50bcb89ac`
|
||||
- `allow_zone_ids` = `["605560545117995008"]` (仅 28F)
|
||||
- `enabled` = 1
|
||||
|
||||
#### cw_is_group_person_ref (图库↔人员)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `cw_is_group_person_ref` (
|
||||
`ID` varchar(32) NOT NULL,
|
||||
`IMAGE_STORE_ID` varchar(32) DEFAULT NULL COMMENT '→ 图库',
|
||||
`PERSON_ID` varchar(32) DEFAULT NULL COMMENT '→ cw_is_person.ID',
|
||||
`EXPIRY_BEGIN_DATE` decimal(20,0) DEFAULT NULL,
|
||||
`EXPIRY_END_DATE` decimal(20,0) DEFAULT NULL,
|
||||
`STATUS` smallint(2) DEFAULT '0' COMMENT '-1删除 0正常 1失效',
|
||||
`GROUP_STATUS` smallint(1) DEFAULT NULL COMMENT '0未建模 1建模中 2完成 3失败',
|
||||
PRIMARY KEY (`ID`),
|
||||
KEY `IDX_IGPR_PERSON` (`IMAGE_STORE_ID`,`PERSON_ID`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
```
|
||||
|
||||
### 2.3 组织库 ER 关系图
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ component-organization │
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌───────────────────┐ │
|
||||
│ │ cw_is_person │ │ cw_is_label │ │
|
||||
│ │ ID (PK) │ │ ID (PK) │ │
|
||||
│ │ BUSINESS_ID │ │ NAME ("访客") │ │
|
||||
│ │ NAME │ │ CODE ("1") │ │
|
||||
│ │ DEFAULT_FLOOR │ └────────┬──────────┘ │
|
||||
│ │ CHOOSE_FLOOR │ │ │
|
||||
│ └──┬──────┬───────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ │ ┌───────────────────┴──────────┐ │
|
||||
│ │ │ │ cw_is_person_label_ref │ │
|
||||
│ │ │ │ PERSON_ID → cw_is_person │ │
|
||||
│ │ │ │ LABEL_ID → cw_is_label │ │
|
||||
│ │ │ └──────────────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ └────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────────────────────────┴──────────┐ │
|
||||
│ │ │ cw_is_person_organization_ref │ │
|
||||
│ │ │ PERSON_ID → cw_is_person │ │
|
||||
│ │ │ ORG_ID → cw_is_organization │ │
|
||||
│ │ └────────────┬───────────────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────┴────────────────┐ │
|
||||
│ │ │ cw_is_organization │ │
|
||||
│ │ │ ID (PK) │ │
|
||||
│ │ │ NAME │ │
|
||||
│ │ │ PARENT_ID → 自身 (树形) │ │
|
||||
│ │ └──┬─────────────┬────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ │ │ ┌─────────┴──────────────┐ │
|
||||
│ │ │ │ org_floor │ │
|
||||
│ │ │ │ org_id → organization │ │
|
||||
│ │ │ │ zone_id │ │
|
||||
│ │ │ └────────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ │ ┌──────────────────────────────┐ │
|
||||
│ │ │ │ tenant_visitor_floor_policy │ │
|
||||
│ │ │ │ org_id → organization (★) │ │
|
||||
│ │ │ │ allow_zone_ids (JSON) │ │
|
||||
│ │ │ │ enabled (0/1) │ │
|
||||
│ │ │ └──────────────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ ┌──┴──────────────────────────────┐ │
|
||||
│ │ │ cw_is_group_person_ref │ │
|
||||
│ └──│ PERSON_ID → cw_is_person │ │
|
||||
│ │ IMAGE_STORE_ID → 图库 │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**关键关联链 (访客邀约)**:
|
||||
```
|
||||
PersonService.detail(personId)
|
||||
→ cw_is_person + cw_is_person_organization_ref → 获取 organizationIds
|
||||
→ cw_is_person_label_ref → 获取 labelIds
|
||||
→ tenant_visitor_floor_policy (按 org_id) → allow_zone_ids (替代 floorList)
|
||||
→ 返回 PersonResult { floorList, labelIds, organizationIds }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. cw-elevator-application (电梯应用库)
|
||||
|
||||
**服务**: `cw-elevator-application`
|
||||
**连接**: ShardingSphere → `jdbc:mysql://mysql_01:3306/cw-elevator-application`
|
||||
**特征**: 识别/电梯记录表按**年份分表** (2020-2030)
|
||||
|
||||
### 3.1 表清单 (33 张)
|
||||
|
||||
| 表名 | 用途 | 分区 |
|
||||
|------|------|------|
|
||||
| `image_rule_ref` | ★通行规则(核心) — 人员/标签/组织↔楼层 | — |
|
||||
| `code_elevator_area` | 电梯区号与编码对照 | — |
|
||||
| `it_acs_pass_rule` | 通行规则主表 | — |
|
||||
| `it_acs_recog_record` | 识别记录 | 按年: `_2020` ~ `_2030` |
|
||||
| `it_acs_elevator_record` | 电梯乘坐记录 | 按年: `_2020` ~ `_2030` |
|
||||
| `elevator_device` | 电梯设备 | — |
|
||||
| `device_image_store` | 设备图库关联 | — |
|
||||
| `it_acs_device_task` | 设备任务 | — |
|
||||
| `send_record_time` | 发送记录时间 | — |
|
||||
| `send_record_timebak` | 发送记录时间备份 | — |
|
||||
| `tenant_visitor_floor_policy` | ⚠️ V1历史遗留 (V2不查询) | — |
|
||||
|
||||
### 3.2 核心表 DDL
|
||||
|
||||
#### image_rule_ref (★通行规则 — 最核心)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `image_rule_ref` (
|
||||
`id` varchar(32) NOT NULL,
|
||||
`zone_id` varchar(64) NOT NULL COMMENT '★楼层zone_id → code_elevator_area.zone_id',
|
||||
`zone_name` varchar(64) DEFAULT NULL COMMENT '楼层名(如28F)',
|
||||
`name` varchar(64) DEFAULT NULL COMMENT '规则名',
|
||||
`person_id` varchar(64) DEFAULT NULL COMMENT '★人员id → cw_is_person.ID',
|
||||
`include_labels` varchar(64) DEFAULT NULL COMMENT '★包含标签ID(单值) → cw_is_label.ID',
|
||||
`include_organizations` varchar(64) DEFAULT NULL COMMENT '★包含组织ID(单值) → cw_is_organization.ID',
|
||||
`exclude_labels` varchar(64) DEFAULT NULL COMMENT '排除标签ID',
|
||||
`is_default` tinyint(1) DEFAULT '0' COMMENT '是否默认规则',
|
||||
`start_time` bigint(20) DEFAULT NULL,
|
||||
`end_time` bigint(20) DEFAULT NULL,
|
||||
`create_time` bigint(20) DEFAULT NULL,
|
||||
`last_update_time` bigint(20) DEFAULT NULL,
|
||||
`business_id` varchar(64) DEFAULT NULL COMMENT '企业id',
|
||||
`parent_rule` varchar(64) DEFAULT NULL COMMENT '归属规则id → it_acs_pass_rule.ID',
|
||||
`person_delete` tinyint(1) DEFAULT '0' COMMENT '人员是否已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `image_rule_ref_include_labels_IDX` (`include_labels`),
|
||||
KEY `image_rule_ref_include_organizations_IDX` (`include_organizations`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通行规则信息表(★访客权限核心)';
|
||||
```
|
||||
|
||||
**关键查询** (`listByPersonInfo` — 决定楼层访问):
|
||||
```sql
|
||||
SELECT DISTINCT zone_id, zone_name
|
||||
FROM image_rule_ref
|
||||
WHERE person_id = #{personId} AND person_delete = 0
|
||||
OR include_labels IN (...)
|
||||
OR include_organizations IN (...)
|
||||
ORDER BY CAST(zone_name as signed) ASC
|
||||
```
|
||||
|
||||
#### code_elevator_area (电梯区号编码)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `code_elevator_area` (
|
||||
`zone_id` varchar(64) NOT NULL COMMENT '★电梯编码(如 605560545117995008)',
|
||||
`code` varchar(64) NOT NULL COMMENT '地区编码(如 0x1C=28F)',
|
||||
`create_time` bigint(13) DEFAULT NULL,
|
||||
`last_update_time` bigint(13) DEFAULT NULL,
|
||||
`is_first` tinyint(4) DEFAULT NULL COMMENT '是否首层',
|
||||
`parent_id` varchar(64) DEFAULT NULL COMMENT '父级id(楼栋)',
|
||||
PRIMARY KEY (`zone_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='电梯编码';
|
||||
```
|
||||
|
||||
**已知 zone_id**:
|
||||
| zone_id | zone_name | code | parent_id (楼栋) |
|
||||
|---------|-----------|------|-------------------|
|
||||
| 605560547135455232 | 40F | — | 605560539791228928 |
|
||||
| 605560541473144832 | 6F | — | 605560539791228928 |
|
||||
| 605560545117995008 | 28F | 0x1C | 605560539791228928 |
|
||||
| 605560539791228928 | (楼栋) | — | — |
|
||||
|
||||
#### it_acs_pass_rule (通行规则主表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `it_acs_pass_rule` (
|
||||
`ID` varchar(32) NOT NULL,
|
||||
`BUSINESS_ID` varchar(32) NOT NULL,
|
||||
`ZONE_ID` varchar(32) NOT NULL COMMENT '楼层ID',
|
||||
`ZONE_NAME` varchar(100) DEFAULT NULL,
|
||||
`NAME` varchar(200) NOT NULL COMMENT '通行规则名称',
|
||||
`IMAGE_STORE_ID` varchar(32) NOT NULL COMMENT '图库id',
|
||||
`IS_DEFAULT` tinyint(1) DEFAULT '0',
|
||||
PRIMARY KEY (`ID`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通行规则表';
|
||||
```
|
||||
|
||||
#### it_acs_recog_record (识别记录 — 按年分表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `it_acs_recog_record` (
|
||||
`ID` varchar(32) NOT NULL,
|
||||
`PERSON_ID` varchar(64) DEFAULT NULL,
|
||||
`PERSON_NAME` varchar(64) DEFAULT NULL,
|
||||
`BUSINESS_ID` varchar(32) NOT NULL,
|
||||
`DEVICE_ID` varchar(64) DEFAULT NULL,
|
||||
`DEVICE_CODE` varchar(64) NOT NULL,
|
||||
`RECOGNITION_RESULT` tinyint(1) DEFAULT NULL COMMENT '1成功 2失败',
|
||||
`RECOGNITION_TIME` bigint(255) NOT NULL,
|
||||
`PERSON_LABEL_IDS` text COMMENT '★人员标签ID列表(用于MQTT访客判断)',
|
||||
`SCORE` decimal(10,6) NOT NULL COMMENT '识别分数',
|
||||
PRIMARY KEY (`ID`),
|
||||
KEY `it_acs_recog_record_PERSON_ID_IDX` (`PERSON_ID`,`DEVICE_ID`,`RECOGNITION_TIME`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='识别记录表';
|
||||
```
|
||||
|
||||
**分表策略**: `it_acs_recog_record_2020` ~ `it_acs_recog_record_2030` (11 张)
|
||||
|
||||
#### elevator_device (电梯设备)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `elevator_device` (
|
||||
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`device_id` varchar(64) DEFAULT NULL,
|
||||
`device_name` varchar(64) NOT NULL COMMENT '设备名称',
|
||||
`current_floor_id` varchar(64) DEFAULT NULL COMMENT '当前楼层zone_id',
|
||||
`current_floor` varchar(64) DEFAULT NULL COMMENT '当前楼层名',
|
||||
`elevator_floor_list` varchar(255) DEFAULT NULL COMMENT '★派梯楼层(逗号分隔)',
|
||||
`elevator_floor_id_list` varchar(1000) DEFAULT NULL COMMENT '★派梯楼层ID',
|
||||
`business_id` varchar(32) NOT NULL COMMENT '租户id',
|
||||
PRIMARY KEY (`ID`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='派梯设备';
|
||||
```
|
||||
|
||||
### 3.3 电梯库 ER 关系图
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ cw-elevator-application │
|
||||
│ │
|
||||
│ ┌───────────────────────┐ ┌─────────────────────┐ │
|
||||
│ │ it_acs_pass_rule │ │ code_elevator_area │ │
|
||||
│ │ ID (PK) │ │ zone_id (PK) │ │
|
||||
│ │ ZONE_ID │──┐ │ code │ │
|
||||
│ │ ZONE_NAME │ │ │ parent_id(楼栋) │ │
|
||||
│ │ IMAGE_STORE_ID │ │ └──────────┬──────────┘ │
|
||||
│ └───────────┬───────────┘ │ │ │
|
||||
│ │ │ │ │
|
||||
│ ┌───────────┴──────────────┴───────────────┴───────┐ │
|
||||
│ │ image_rule_ref (★核心) │ │
|
||||
│ │ zone_id ← code_elevator_area.zone_id │ │
|
||||
│ │ person_id ← cw_is_person.ID │ │
|
||||
│ │ include_labels ← cw_is_label.ID (单值) │ │
|
||||
│ │ include_organizations ← cw_is_organization.ID │ │
|
||||
│ │ parent_rule ← it_acs_pass_rule.ID │ │
|
||||
│ │ is_default (0/1) │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌───────────────────────┐ ┌─────────────────────┐ │
|
||||
│ │ elevator_device │ │ it_acs_recog_record │ │
|
||||
│ │ device_id │ │ PERSON_LABEL_IDS │ │
|
||||
│ │ elevator_floor_list │ │ RECOGNITION_TIME │ │
|
||||
│ │ elevator_floor_id_list│ │ (按年分表 2020-2030)│ │
|
||||
│ └───────────────────────┘ └─────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**image_rule_ref 三种访问模式**:
|
||||
```
|
||||
1. person_id 直连: WHERE person_id = X AND person_delete = 0
|
||||
2. 标签关联: WHERE include_labels = X (人员标签匹配此标签)
|
||||
3. 组织关联: WHERE include_organizations = X (人员组织匹配此组织)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. ninca_common (公共组件库)
|
||||
|
||||
**服务**: `ninca-common` (共享服务, 通过 Feign 调用)
|
||||
**连接**: `jdbc:mysql://mysql_01:3306/ninca_common` (推测)
|
||||
|
||||
### 4.1 关键表
|
||||
|
||||
| 表名 | 用途 |
|
||||
|------|------|
|
||||
| `cw_qz_zone` | ★区域/楼层定义表 — zone_id 的主数据源 |
|
||||
| `cw_qz_zone_unit_ref` | 区域↔单元关联 |
|
||||
| `cw_qz_zone_type` | 区域类型 |
|
||||
| `cw_qz_device_area` | 设备区域 |
|
||||
| `cw_qz_district_map` | 区域地图 |
|
||||
| `cw_qz_message_*` (7 张) | 消息/通知系统 |
|
||||
| `cw_qz_file_*` (4 张) | 文件管理 |
|
||||
| `QRTZ_*` (11 张) | Quartz 调度 |
|
||||
|
||||
#### cw_qz_zone (区域/楼层主数据)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `cw_qz_zone` (
|
||||
`ID` varchar(32) DEFAULT NULL COMMENT 'zone_id',
|
||||
`CODE` varchar(32) DEFAULT NULL,
|
||||
`LEVEL` int(11) DEFAULT NULL,
|
||||
`NAME` varchar(64) DEFAULT NULL COMMENT '区域名(如"28F")',
|
||||
`PARENT_ID` varchar(32) DEFAULT NULL COMMENT '父级(楼栋)',
|
||||
`BUSINESS_ID` varchar(32) DEFAULT NULL,
|
||||
`TYPE_ID` varchar(32) DEFAULT NULL,
|
||||
PRIMARY KEY ... (无显式PK定义)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='区域表';
|
||||
```
|
||||
|
||||
**Feign 接口**: `ZoneFeignClient.findZonelist()` → `POST /sysetting/zone/list` (component-org 通过此接口解析 zone_id → zone_name)
|
||||
|
||||
---
|
||||
|
||||
## 5. cwos_portal (门户管理库)
|
||||
|
||||
**155 张表** — 平台最大数据库,涵盖: 设备管理、图库管理、鉴权认证、账号系统、识别引擎配置
|
||||
|
||||
### 5.1 核心模块表
|
||||
|
||||
| 模块 | 关键表 | 用途 |
|
||||
|------|--------|------|
|
||||
| 账号 | `cw_ac_account`, `cw_ac_business` | ★租户/企业账号 |
|
||||
| 鉴权 | `cw_auth_secret`, `cw_auth_secret_application` | API Key 管理 |
|
||||
| 图库 | `cw_ag_image_store`, `cw_ag_image`, `cw_ag_image_store_image` | 人脸底库 |
|
||||
| 设备 | `cw_ge_device`, `cw_ge_device_camera`, `cw_ge_area` | 设备/摄像头/区域 |
|
||||
| 算法引擎 | `cw_de_group`, `cw_de_service`, `cw_de_sub_group` | CRK 引擎分组 |
|
||||
| 模型 | `cw_de_model_upgrade_task` | 模型升级 |
|
||||
|
||||
### 5.2 关键关联: 图库系统
|
||||
|
||||
```
|
||||
cw_ag_image_store (图库)
|
||||
→ cw_ag_application_image_store (应用↔图库)
|
||||
→ cw_ag_device_image_store (设备↔图库)
|
||||
→ cw_ag_image_store_image (图库内的图片)
|
||||
→ cw_ag_image (图片)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. cwos_manager (运维管理库)
|
||||
|
||||
**33 张表** — 部署/运维管理: 应用、组件、节点、配置、操作日志
|
||||
|
||||
---
|
||||
|
||||
## 7. alarm_deploy (报警服务库)
|
||||
|
||||
**49 张表** — 人脸/车辆/行为报警系统
|
||||
|
||||
### 7.1 核心表
|
||||
|
||||
| 表名 | 用途 |
|
||||
|------|------|
|
||||
| `cw_alarm_control_task` | 布控任务 |
|
||||
| `cw_alarm_control_task_device` | 布控↔设备关联 |
|
||||
| `cw_alarm_control_task_group` | 布控↔底库关联 |
|
||||
| `cw_alarm_blacklist_record` | ★黑名单报警记录 |
|
||||
| `cw_alarm_stranger_record` | ★陌生人报警 |
|
||||
| `cw_alarm_face_properties_record` | 人脸属性报警 |
|
||||
| `cw_alarm_plate_record` | 车牌报警 |
|
||||
| `three_level_control_record` | ★三级布控记录(含 grab_floor) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 其他数据库
|
||||
|
||||
| 数据库 | 表数 | 核心表 | 用途 |
|
||||
|--------|------|--------|------|
|
||||
| `person_file` | 56 | 人像档案表 | 抓拍人像存档 |
|
||||
| `snap_deploy` | 1 | 抓拍配置 | snap-app 部署 |
|
||||
| `cloudwalk_device_thirdparty` | 5 | `gw_thirdparty_device_*` | 第三方设备网关 |
|
||||
| `g` | 1 | shard | — |
|
||||
| `ods` | 59 | 运营数据 | ODS 数据仓库 |
|
||||
| `p` | 4 | shard | — |
|
||||
| `xqconfig` | 59 | `hash*` (CRK 引擎配置) | ninca-crk-std |
|
||||
| `xqfacerecog` | 4 | `recogdbforface*` | CRK 识别结果分片 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 跨库关系图 (ER)
|
||||
|
||||
### 9.1 平台级 ER 总图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 星河湾星中星 平台 ER │
|
||||
│ │
|
||||
│ ┌─ component-organization ───────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ cw_is_person ──┬── cw_is_person_label_ref ── cw_is_label │ │
|
||||
│ │ │ │ │
|
||||
│ │ ├── cw_is_person_organization_ref ── cw_is_organization ││
|
||||
│ │ │ ├── org_floor │ │
|
||||
│ │ │ └── tenant_visitor_floor_policy (★策略) │ │
|
||||
│ │ │ │ │
|
||||
│ │ └── cw_is_group_person_ref ── IMAGE_STORE_ID │ │
|
||||
│ └──────────────────────┬───────────────────────────────────────────┘ │
|
||||
│ │ zone_id (via Feign) │
|
||||
│ ┌─ cw-elevator-application ──────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ code_elevator_area (zone_id, code, parent_id) │ │
|
||||
│ │ ▲ │ │
|
||||
│ │ │ zone_id │ │
|
||||
│ │ image_rule_ref ── person_id ──→ cw_is_person.ID │ │
|
||||
│ │ ├── include_labels ──→ cw_is_label.ID │ │
|
||||
│ │ ├── include_organizations ──→ cw_is_organization.ID │ │
|
||||
│ │ └── parent_rule ──→ it_acs_pass_rule.ID │ │
|
||||
│ │ │ │
|
||||
│ │ it_acs_recog_record ── PERSON_ID ──→ cw_is_person.ID │ │
|
||||
│ │ ── PERSON_LABEL_IDS (★访客判断: "1"=访客) │ │
|
||||
│ └──────────────────────┬───────────────────────────────────────────┘ │
|
||||
│ │ zone_id (via Feign ZoneFeignClient) │
|
||||
│ ┌─ ninca_common ──────────────────────────────────────────────────┐ │
|
||||
│ │ cw_qz_zone (★zone主数据: ID, NAME, PARENT_ID) │ │
|
||||
│ │ cw_qz_zone_unit_ref │ │
|
||||
│ │ cw_qz_device_area │ │
|
||||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ cwos_portal ───────────────────────────────────────────────────┐ │
|
||||
│ │ cw_ac_business (★BUSINESS_ID主数据) ── cw_ac_account │ │
|
||||
│ │ cw_ag_image_store (图库) ── cw_ag_image (图片) │ │
|
||||
│ │ cw_ge_device (设备) ── cw_ge_device_camera (摄像头) │ │
|
||||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 9.2 关键跨库关联
|
||||
|
||||
| 关联 | 从库 | 从表.字段 | 到库 | 到表.字段 | 类型 |
|
||||
|------|------|----------|------|----------|------|
|
||||
| 人员↔楼层规则 | cw-elevator-app | `image_rule_ref.person_id` | component-org | `cw_is_person.ID` | 逻辑外键 |
|
||||
| 标签↔楼层规则 | cw-elevator-app | `image_rule_ref.include_labels` | component-org | `cw_is_label.ID` | 逻辑外键 |
|
||||
| 组织↔楼层规则 | cw-elevator-app | `image_rule_ref.include_organizations` | component-org | `cw_is_organization.ID` | 逻辑外键 |
|
||||
| 楼层编码↔主数据 | cw-elevator-app | `code_elevator_area.zone_id` | ninca_common | `cw_qz_zone.ID` | 逻辑外键 |
|
||||
| 策略↔组织 | component-org | `tenant_visitor_floor_policy.org_id` | component-org | `cw_is_organization.ID` | ⚠️ 同库 |
|
||||
| 访客标签判断 | cw-elevator-app | `it_acs_recog_record.PERSON_LABEL_IDS` | component-org | `cw_is_label.CODE = "1"` | 业务规则 |
|
||||
| 企业ID | component-org | `cw_is_person.BUSINESS_ID` | cwos_portal | `cw_ac_business.ID` | 逻辑外键 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 访客邀约+派梯完整数据流
|
||||
|
||||
### 10.1 邀约页加载
|
||||
|
||||
```
|
||||
Step 1: 前端 → POST /component/person/detail
|
||||
├─ component-org: cw_is_person WHERE ID = personId
|
||||
├─ cw_is_person_label_ref JOIN cw_is_label → labelIds, labelNames
|
||||
├─ cw_is_person_organization_ref JOIN cw_is_organization → organizationIds
|
||||
│
|
||||
├─ ★ 策略检查: tenant_visitor_floor_policy
|
||||
│ WHERE org_id = ? AND enabled = 1
|
||||
│ → allow_zone_ids = ["605560545117995008"] (28F)
|
||||
│
|
||||
├─ 未命中策略 → Feign: elevator-app POST /elevator/passRule/image
|
||||
│ → image_rule_ref: SELECT DISTINCT zone_id, zone_name
|
||||
│ WHERE person_id = X AND person_delete = 0
|
||||
│ OR include_labels IN (...)
|
||||
│ OR include_organizations IN (...)
|
||||
│ → code_elevator_area: 获取 zone_id 对应的 code
|
||||
│
|
||||
└─ 返回 PersonResult { floorList: [28F_zone_id], labelIds, organizationIds }
|
||||
```
|
||||
|
||||
### 10.2 派梯执行
|
||||
|
||||
```
|
||||
Step 2: 前端 → POST /elevator/person/add/visitor
|
||||
├─ elevator-app: PersonRuleServiceImpl.addVisitor()
|
||||
│
|
||||
├─ Feign: component-org POST /component/person/detail (回查)
|
||||
│ → 返回 floorList = [28F_zone_id] (策略已替代)
|
||||
│
|
||||
├─ UC-02: floorIds 非空 → effective = param.floorIds = [28F_zone_id]
|
||||
│ UC-01: floorIds 为空 → effective = detail.floorList = [28F_zone_id]
|
||||
│
|
||||
├─ ★ 无校验: floorIds 是否是 host floorList 的子集 (V2 不做交集)
|
||||
│
|
||||
├─ elevator-app: image_rule_ref.getDefaultByZoneId(28F_zone_id)
|
||||
│ → SELECT * FROM image_rule_ref WHERE zone_id=? AND is_default=1
|
||||
│
|
||||
├─ elevator-app: INSERT INTO image_rule_ref
|
||||
│ (person_id = visitorId, zone_id = 28F, parent_rule = defaultRule.id)
|
||||
│
|
||||
├─ Feign: component-org POST /component/imagestore/person/batchBind
|
||||
│ → cw_is_group_person_ref: INSERT (person_id = visitorId, image_store_id = ?)
|
||||
│
|
||||
└─ Feign: updateGroupPersonRef → 更新组人员引用
|
||||
```
|
||||
|
||||
### 10.3 识别派梯 (MQTT 实时)
|
||||
|
||||
```
|
||||
Step 3: 人脸识别 → 电梯派梯 (实时)
|
||||
├─ it_acs_recog_record: INSERT 识别记录
|
||||
│ PERSON_LABEL_IDS = "1" → 判定 isVisitor = true
|
||||
│
|
||||
├─ MqttServiceImpl: 检查 PERSON_LABEL_IDS.contains("1")
|
||||
│ → isVisitor = TRUE → 按访客逻辑派梯
|
||||
│
|
||||
├─ image_rule_ref: 查询访客的规则行
|
||||
│ WHERE person_id = visitorId
|
||||
│ → zone_id = 28F
|
||||
│
|
||||
└─ elevator_device: 派梯到对应楼层
|
||||
elevator_floor_list / elevator_floor_id_list
|
||||
```
|
||||
|
||||
### 10.4 关键业务规则
|
||||
|
||||
| 规则 | 位置 | 说明 |
|
||||
|------|------|------|
|
||||
| 访客标签CODE="1" | `cw_is_label.CODE` + `it_acs_recog_record.PERSON_LABEL_IDS` | MQTT 中判断 `isVisitor` |
|
||||
| 访客列表排除"访客"标签的人 | `ImgPersonServiceImpl.listByPage` L374-377 | `labelNames.contains("访客") → continue` |
|
||||
| 策略替代 floorList | `tenant_visitor_floor_policy` → `ImgPersonServiceImpl.detail()` L643-648 | org_id 命中 → allow_zone_ids 替换 |
|
||||
| 40F/6F 硬编码默认 | `ImgPersonServiceImpl.listByPage` L354-370 | xhwId→40F, else→6F |
|
||||
| addVisitor 无交集校验 | `PersonRuleServiceImpl.addVisitor()` | UC-02 floorIds 任意值均可 |
|
||||
| addVisitor 无 defaultRule 检查 | `PersonRuleServiceImpl.addVisitor()` L222 | `getDefaultByZoneId` 返回 null → NPE |
|
||||
@@ -0,0 +1,524 @@
|
||||
# 广发基金访客默认28F — 架构设计文档
|
||||
|
||||
**日期**: 2026-05-09
|
||||
**状态**: 评审通过 (方案A: 表驱动, 单库维护)
|
||||
**读者**: 架构师 / 技术评审
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [架构决策摘要](#1-架构决策摘要)
|
||||
2. [数据结构关系 — 完整 ER 图](#2-数据结构关系--完整-er-图)
|
||||
3. [业务数据流 — 访客邀约+派梯全链路](#3-业务数据流--访客邀约派梯全链路)
|
||||
4. [方案A实现详解](#4-方案a实现详解)
|
||||
5. [方案合理性论证](#5-方案合理性论证)
|
||||
6. [附录: 关键表DDL](#6-附录-关键表ddl)
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构决策摘要
|
||||
|
||||
| 决策项 | 选择 | 理由 |
|
||||
|--------|------|------|
|
||||
| 策略存储 | `tenant_visitor_floor_policy` 表 (component-organization 库) | V2 已实现,代码就绪 |
|
||||
| 策略语义 | 替代 (Replacement),非求交 (Intersection) | 产品需求: 策略启用时整表替换 floorList |
|
||||
| 隔离键 | `org_id` (组织节点) | 多租户共享同一 `business_id`, 需组织级隔离 |
|
||||
| 策略生效点 | `ImgPersonServiceImpl.detail()` L643-648 | 邀约页初始化 + UC-01 派梯兜底 |
|
||||
| 数据库维护 | 仅 component-organization | V2 电梯侧不查询此表 |
|
||||
| 代码变更 | **0 行** | UPDATE 1 条 SQL |
|
||||
|
||||
### 本质认知
|
||||
|
||||
广发基金 28F 策略**不是一个新需求** — 它是当前 `tenant_visitor_floor_policy` 表驱动架构的**已有实例**。当前生产库中已存在该策略行(`enabled=1`),且 `ImgPersonServiceImpl.detail()` 的 L643-648 已经在执行策略替代逻辑。扩展至 20 层是**纯数据变更**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据结构关系 — 完整 ER 图
|
||||
|
||||
### 2.1 涉及三库的核心表
|
||||
|
||||
```
|
||||
┌── component-organization ───────────────────────────────────────────┐
|
||||
│ │
|
||||
│ cw_is_person cw_is_label │
|
||||
│ ├─ ID (PK) ├─ ID (PK) │
|
||||
│ ├─ BUSINESS_ID (FK→portal) ├─ NAME ("访客") │
|
||||
│ ├─ NAME ├─ CODE ("1") │
|
||||
│ ├─ DEFAULT_FLOOR (zone_id) └──────────────────┐ │
|
||||
│ └─ CHOOSE_FLOOR (zone_id list) │ │
|
||||
│ │ │ │
|
||||
│ ├── cw_is_person_label_ref ───────────────────┘ │
|
||||
│ │ ├─ PERSON_ID (FK→person) │
|
||||
│ │ └─ LABEL_ID (FK→label) │
|
||||
│ │ │
|
||||
│ ├── cw_is_person_organization_ref │
|
||||
│ │ ├─ PERSON_ID (FK→person) │
|
||||
│ │ └─ ORG_ID (FK→organization) ──────────┐ │
|
||||
│ │ │ │
|
||||
│ cw_is_organization │ │
|
||||
│ ├─ ID (PK) ◄────────────────────────────────────────┘ │
|
||||
│ ├─ NAME ("[28-38F]广发基金管理有限公司") │
|
||||
│ ├─ PARENT_ID (FK→自身, 树形) │
|
||||
│ ├─ BUSINESS_ID (FK→portal) │
|
||||
│ └─ IS_VALID │
|
||||
│ │ │
|
||||
│ ├── org_floor │
|
||||
│ │ ├─ org_id (FK→organization) │
|
||||
│ │ └─ zone_id (逻辑FK→code_elevator_area) │
|
||||
│ │ │
|
||||
│ └── tenant_visitor_floor_policy ★ 策略表 │
|
||||
│ ├─ id = 'gf_vstr_policy_guangfa_fund_001x' │
|
||||
│ ├─ org_id (FK→organization, UNIQUE) │
|
||||
│ ├─ allow_zone_ids (JSON数组 zone_id列表) │
|
||||
│ ├─ enabled (0/1) │
|
||||
│ └─ policy_version (自增) │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ zone_id (逻辑外键, 非数据库约束)
|
||||
▼
|
||||
┌── cw-elevator-application ───────────────────────────────────────────┐
|
||||
│ │
|
||||
│ code_elevator_area │
|
||||
│ ├─ zone_id (PK) ◄──── 所有 zone_id 的权威来源 │
|
||||
│ ├─ code (如 0x1C=28F) │
|
||||
│ ├─ parent_id (楼栋 building_id) │
|
||||
│ └─ is_first (是否首层) │
|
||||
│ │
|
||||
│ image_rule_ref ★ 通行规则 │
|
||||
│ ├─ person_id (逻辑FK→cw_is_person) │
|
||||
│ ├─ include_labels (逻辑FK→cw_is_label, 单值) │
|
||||
│ ├─ include_organizations (逻辑FK→cw_is_organization, 单值) │
|
||||
│ ├─ zone_id (FK→code_elevator_area) │
|
||||
│ ├─ zone_name │
|
||||
│ ├─ parent_rule (FK→it_acs_pass_rule) │
|
||||
│ └─ is_default (0/1) │
|
||||
│ │
|
||||
│ it_acs_pass_rule │
|
||||
│ ├─ ID (PK) ◄─── image_rule_ref.parent_rule │
|
||||
│ ├─ ZONE_ID (FK→code_elevator_area) │
|
||||
│ └─ IMAGE_STORE_ID (FK→cwos_portal.cw_ag_image_store) │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ zone_id (逻辑外键, Feign查询)
|
||||
▼
|
||||
┌── ninca_common ──────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ cw_qz_zone ★ zone 主数据 │
|
||||
│ ├─ ID (zone_id) ◄──── Feign: ZoneFeignClient.findZonelist() │
|
||||
│ ├─ NAME ("28F") │
|
||||
│ ├─ PARENT_ID (楼栋/上级区域) │
|
||||
│ ├─ BUSINESS_ID │
|
||||
│ └─ LEVEL │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 关键实体关系
|
||||
|
||||
| 关系 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| Person → Organization | N:N | `cw_is_person_organization_ref` 关联表 |
|
||||
| Person → Label | N:N | `cw_is_person_label_ref` 关联表 |
|
||||
| Organization → Floor (org_floor) | 1:N | 组织可关联多个楼层 |
|
||||
| Organization → Policy (tenant_visitor_floor_policy) | 1:1 | `uk_org_building` 唯一约束 |
|
||||
| Policy → Floor (allow_zone_ids) | 1:N | JSON 数组存储 |
|
||||
| image_rule_ref → Person/Zone | N:1 | 每条规则绑定一个人员+楼层 |
|
||||
| image_rule_ref → PassRule | N:1 | `parent_rule` 指向默认规则模板 |
|
||||
|
||||
### 2.3 广发基金当前数据快照
|
||||
|
||||
```sql
|
||||
-- cw_is_organization (广发基金组织节点)
|
||||
ID = '488b8ad049bb43408a6fbcc50bcb89ac'
|
||||
NAME = '[28-38F]广发基金管理有限公司'
|
||||
BUSINESS_ID = '2524639890ba4f2cba9ba1a4eeaa4015' -- 星河湾中心
|
||||
|
||||
-- tenant_visitor_floor_policy (当前策略)
|
||||
id = 'gf_vstr_policy_guangfa_fund_001x'
|
||||
org_id = '488b8ad049bb43408a6fbcc50bcb89ac'
|
||||
allow_zone_ids = '["605560545117995008"]' -- 仅 28F
|
||||
enabled = 1
|
||||
policy_version = 1
|
||||
|
||||
-- code_elevator_area (28F zone)
|
||||
zone_id = '605560545117995008'
|
||||
code = 0x1C
|
||||
parent_id = '605560539791228928' -- 星河湾中心楼栋
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 业务数据流 — 访客邀约+派梯全链路
|
||||
|
||||
### 3.1 核心业务前提
|
||||
|
||||
**一个关键事实**: 广发基金和物业公司共享同一个 `BUSINESS_ID = 2524639890ba4f2cba9ba1a4eeaa4015` (星河湾中心)。这意味着:
|
||||
- 不能使用 `business_id` 做策略隔离
|
||||
- 必须使用 `org_id` 做细粒度隔离
|
||||
- `tenant_visitor_floor_policy.uk_org_building(org_id, building_id)` 保证了每个组织节点唯一一条策略
|
||||
|
||||
### 3.2 完整数据流 (6 步骤)
|
||||
|
||||
```
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
Step 1: 前端打开访客邀约页 — 选择被访人(广发员工)
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
前端 → POST /component/person/detail { id: "广发员工personId" }
|
||||
│
|
||||
├─ DB: cw_is_person WHERE ID = personId
|
||||
│ → NAME, PHONE, DEFAULT_FLOOR, CHOOSE_FLOOR
|
||||
│
|
||||
├─ DB: cw_is_person_organization_ref WHERE PERSON_ID = personId
|
||||
│ → orgIds = ['488b8ad...'] (广发基金)
|
||||
│
|
||||
├─ DB: cw_is_person_label_ref WHERE PERSON_ID = personId
|
||||
│ → labelIds = ['someLabelId', ...]
|
||||
│
|
||||
├─ Feign → elevator: POST /elevator/passRule/image
|
||||
│ { personId, includeLabels, includeOrganizations }
|
||||
│ │
|
||||
│ ├─ DB: image_rule_ref
|
||||
│ │ SELECT DISTINCT zone_id,zone_name
|
||||
│ │ WHERE person_id=X AND person_delete=0
|
||||
│ │ OR include_labels IN (...)
|
||||
│ │ OR include_organizations IN (...)
|
||||
│ │ → 返回被访人所有可访问楼层 (如: 28F,...,38F 共11层)
|
||||
│ │
|
||||
│ └─ 返回: [{zone_id:"28F",zone_name:"28F"}, ...]
|
||||
│
|
||||
├─ ★ 策略替代 (ImgPersonServiceImpl.detail() L643-648):
|
||||
│ DB: tenant_visitor_floor_policy
|
||||
│ SELECT * WHERE org_id='488b8ad...' AND enabled=1
|
||||
│ → allow_zone_ids = ["28F_zone_id"]
|
||||
│
|
||||
│ floorList = ["28F_zone_id"] ← 替换! 不再是11层
|
||||
│ zoneNames = "28F"
|
||||
│
|
||||
└─ 返回: PersonResult { floorList: ["28F_zone_id"], floorNames: "28F" }
|
||||
|
||||
前端收到 floorList = [28F] → 渲染: ☑ 28F (唯一可选楼层)
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
Step 2: 用户填写访客信息 + 确认楼层 → 提交
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
前端 → POST /elevator/person/add/visitor
|
||||
{ personId: "广发员工", visitorId: "新访客",
|
||||
floorIds: ["28F_zone_id"], begVisitorTime, endVisitorTime }
|
||||
│
|
||||
├─ Feign → component-org: POST /component/person/detail (回查)
|
||||
│ → floorList = ["28F_zone_id"] (策略已生效)
|
||||
│
|
||||
├─ UC-02 判定: param.floorIds 非空
|
||||
│ effective = ["28F_zone_id"]
|
||||
│
|
||||
├─ DB: image_rule_ref
|
||||
│ SELECT * WHERE zone_id='28F' AND is_default=1
|
||||
│ → 获取默认规则模板 (parent_rule, name, zoneName)
|
||||
│
|
||||
├─ DB: image_rule_ref INSERT
|
||||
│ (person_id=visitorId, zone_id=28F, parent_rule=defaultRule.id)
|
||||
│
|
||||
├─ Feign → component-org: POST /component/imagestore/person/batchBind
|
||||
│ (personId=visitorId, imageStoreId, expiry dates)
|
||||
│ → DB: cw_is_group_person_ref INSERT
|
||||
│
|
||||
└─ 返回: success
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
Step 3: 访客到达 — 人脸识别 → 自动派梯
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
设备识别 → DB: it_acs_recog_record INSERT
|
||||
{ PERSON_ID: visitorId, PERSON_LABEL_IDS: "1", ... }
|
||||
│
|
||||
├─ MQTT 判定: PERSON_LABEL_IDS.contains("1") → isVisitor = true
|
||||
│
|
||||
├─ DB: image_rule_ref WHERE person_id = visitorId
|
||||
│ → zone_id = 28F
|
||||
│
|
||||
└─ 派梯到 28F
|
||||
```
|
||||
|
||||
### 3.3 策略替代的精确代码位置
|
||||
|
||||
```java
|
||||
// ImgPersonServiceImpl.detail()
|
||||
// Line 620-626: 调用 elevator 获取被访人原始楼层
|
||||
AcsPassRuleImageForm acsPassRuleImageForm = new AcsPassRuleImageForm();
|
||||
acsPassRuleImageForm.setPersonId(param.getId());
|
||||
acsPassRuleImageForm.setIncludeOrganizations(result.getOrganizationIds());
|
||||
acsPassRuleImageForm.setIncludeLabels(result.getLabelIds());
|
||||
CloudwalkResult<List<AcsPassRuleImageResultDto>> images =
|
||||
this.elevatorFeignClient.listByImageId(acsPassRuleImageForm);
|
||||
// → 返回被访人所有楼层 (标签+组织+个人关联的 zone)
|
||||
|
||||
// Line 632-641: 组装原始 floorList (全量)
|
||||
for (AcsPassRuleImageResultDto dto : images.getData()) {
|
||||
floorList.add(dto.getZoneId());
|
||||
}
|
||||
|
||||
// ★ Line 643-648: 策略替代 — 就是这个位置
|
||||
Optional<List<String>> replacementFloors =
|
||||
this.tenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive(
|
||||
result.getOrganizationIds());
|
||||
if (replacementFloors.isPresent()) {
|
||||
floorList = new ArrayList<>(replacementFloors.get()); // 完全替换!
|
||||
zoneNames = buildCommaSeparatedFloorNames(businessId, floorList);
|
||||
}
|
||||
|
||||
// Line 650-651: 写入 PersonResult
|
||||
result.setFloorList(floorList);
|
||||
```
|
||||
|
||||
### 3.4 策略查询链
|
||||
|
||||
```
|
||||
TenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive(orgIds)
|
||||
│
|
||||
├─ 遍历 orgIds, 对每个 orgId:
|
||||
│
|
||||
└─ TenantVisitorFloorPolicyMapper.selectEnabledByOrgId(orgId)
|
||||
│
|
||||
└─ SQL:
|
||||
SELECT * FROM tenant_visitor_floor_policy
|
||||
WHERE org_id = #{orgId}
|
||||
AND enabled = 1
|
||||
AND (building_id IS NULL OR building_id = '')
|
||||
LIMIT 1
|
||||
│
|
||||
└─ 返回第一行命中 → 解析 allow_zone_ids JSON → 返回 zone_id 列表
|
||||
|
||||
↓ 如果任意 orgId 命中策略 → 返回 Optional.of(allow_zone_ids)
|
||||
↓ 如果所有 orgId 都未命中 → 返回 Optional.empty()
|
||||
→ ImgPersonServiceImpl.detail() 保留 listByImageId 原始结果
|
||||
```
|
||||
|
||||
### 3.5 listByPage (访客列表) 的并行路径
|
||||
|
||||
```
|
||||
listByPage(isVisitor=true) 的策略处理 (L326-333):
|
||||
|
||||
P1: tenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive(orgIds)
|
||||
├─ 命中 → 直接使用 allow_zone_ids 构建 floorInfoList
|
||||
│ 设置 defaultChooseFloor = allow_zone_ids[0]
|
||||
│ 设置 isAcrossDay = 0
|
||||
│ 跳过 P2 (包括跳过 40F/6F 硬编码块)
|
||||
│
|
||||
└─ 未命中 → P2: listByImageId + XHW硬编码块 (L336-371)
|
||||
├─ orgIds.contains(xhwId) → 40F
|
||||
└─ else → 6F
|
||||
```
|
||||
|
||||
**40F/6F 硬编码与策略表的关系**: 策略命中时,硬编码逻辑被**完全跳过**。两者互斥,不冲突。
|
||||
|
||||
---
|
||||
|
||||
## 4. 方案A实现详解
|
||||
|
||||
### 4.1 实现步骤
|
||||
|
||||
| # | 步骤 | 操作 | 数据库 |
|
||||
|---|------|------|--------|
|
||||
| 1 | 确认 zone_id 列表 | `SELECT zone_id,zone_name,code FROM cw-elevator-app.code_elevator_area WHERE parent_id='605560539791228928' AND zone_id BETWEEN ? AND ?` | elevator |
|
||||
| 2 | 更新策略 | `UPDATE component-org.tenant_visitor_floor_policy SET allow_zone_ids='["z1",...,"z20"]' WHERE id='gf_vstr_policy_guangfa_fund_001x'` | component-org |
|
||||
| 3 | 更新种子SQL | 修改 `organization_tenant_visitor_floor_policy_init_tenants.sql` | 代码仓库 |
|
||||
| 4 | 验证 | curl addVisitor + 检查 image_rule_ref 写入行数 | — |
|
||||
|
||||
### 4.2 为什么只需要修改 component-organization 库
|
||||
|
||||
```
|
||||
V2 架构中的策略查询链:
|
||||
|
||||
elevator-app
|
||||
PersonRuleServiceImpl.addVisitor()
|
||||
→ personService.detail() [Feign调用]
|
||||
→ component-org: ImgPersonServiceImpl.detail()
|
||||
→ tenantVisitorFloorPolicyService [本地调用]
|
||||
→ TenantVisitorFloorPolicyMapper [本地调用]
|
||||
→ SQL: SELECT FROM tenant_visitor_floor_policy ← component-org 库
|
||||
|
||||
电梯侧完全不参与策略查询。V1 时代的电梯侧 tenant_visitor_floor_policy 表
|
||||
已成为死数据。确认:
|
||||
- V2 电梯 Java 源码中无 TenantVisitorFloorPolicy* 引用
|
||||
- V2 电梯 MyBatis Mapper 中无该表查询
|
||||
- PersonRuleServiceImpl.addVisitor() 注释明确: "电梯侧不再读策略表"
|
||||
```
|
||||
|
||||
### 4.3 数据一致性保证
|
||||
|
||||
| 保证机制 | 说明 |
|
||||
|----------|------|
|
||||
| 唯一约束 | `uk_org_building(org_id, building_id)` — 确保每个组织只有一条策略 |
|
||||
| 版本追踪 | `policy_version` 字段自增 — 每次变更可追溯 |
|
||||
| 回滚即时 | UPDATE enabled=0 或 UPDATE allow_zone_ids — 下个 detail() 调用即刻生效 |
|
||||
| 幂等变更 | 使用固定 `id` 的 INSERT ON DUPLICATE KEY UPDATE — 重复执行安全 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 方案合理性论证
|
||||
|
||||
### 5.1 为什么是表驱动而非硬编码
|
||||
|
||||
| 论证维度 | 表驱动 (A) | 硬编码 (B/C) |
|
||||
|----------|-----------|-------------|
|
||||
| **当前代码就绪** | L643-648 已存在策略替代块 | 需要新增 @Value + if/else |
|
||||
| **现有实例** | 广发 28F 已作为表行存在 (enabled=1) | 需要新增代码路径 |
|
||||
| **扩展性** | INSERT 一行 = 新租户策略 | 每次 + @Value + 代码分支 |
|
||||
| **运行时变更** | UPDATE SQL, 即时生效 | 改配置 + 重启服务 |
|
||||
| **多 zone 支持** | JSON 数组原生支持 | 长字符串, 难以维护 |
|
||||
| **组织级隔离** | `org_id` 精确定位 | if/else 链, 易出错 |
|
||||
| **与 40F/6F 关系** | 互斥 (策略先于硬编码) | 冲突 (两套逻辑并排) |
|
||||
| **数据库一致性** | 单库 (component-org) | 无数据库参与 |
|
||||
|
||||
### 5.2 为什么不需要修改电梯侧
|
||||
|
||||
V2 架构将策略职责完全收敛到 component-organization:
|
||||
|
||||
```
|
||||
V1 (旧) V2 (新)
|
||||
┌──────────┐ ┌──────────┐
|
||||
策略存储 │ 双库维护 │ → │ 单库维护 │
|
||||
├──────────┤ ├──────────┤
|
||||
策略查询 │ elevator │ → │ org组件 │
|
||||
├──────────┤ ├──────────┤
|
||||
addVisitor语义 │ 求交(∩) │ → │ 替代(=) │
|
||||
├──────────┤ ├──────────┤
|
||||
listByPage语义 │ 无策略 │ → │ 策略覆盖 │
|
||||
└──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
V2 的设计决策已在代码中体现 (L643-648 替代语义, addVisitor "此处不做 ∩" 注释)。方案A是对这个既有设计的延续,而非引入新的设计模式。
|
||||
|
||||
### 5.3 为什么 org_id 是合适的隔离键
|
||||
|
||||
```
|
||||
问题: BUSINESS_ID=2524639890ba4f2cba9ba1a4eeaa4015 被多个租户共享
|
||||
├─ 广发基金 (28-38F)
|
||||
├─ 星河湾物业管理 (全楼)
|
||||
├─ 康怡健
|
||||
├─ 大石
|
||||
└─ ... (642 个组织节点)
|
||||
|
||||
如果按 business_id 隔离 → 一个策略影响所有租户 → 不可行
|
||||
如果按 org_id 隔离 → 每个组织独立策略 → 精确控制
|
||||
```
|
||||
|
||||
`tenant_visitor_floor_policy` 的 `uk_org_building(org_id, building_id)` 唯一约束正是为此设计。
|
||||
|
||||
### 5.4 为什么 20 层扩展只需 1 条 SQL
|
||||
|
||||
```
|
||||
扩展前 (当前生产):
|
||||
allow_zone_ids = '["605560545117995008"]' ← 1 个 zone (28F)
|
||||
→ detail() floorList = [28F]
|
||||
→ addVisitor UC-01 effective = [28F]
|
||||
→ image_rule_ref INSERT 1 行
|
||||
|
||||
扩展后:
|
||||
allow_zone_ids = '["28F_id","29F_id",...,"47F_id"]' ← 20 个 zone
|
||||
→ detail() floorList = [28F,...,47F]
|
||||
→ addVisitor UC-01 effective = [28F,...,47F]
|
||||
→ image_rule_ref INSERT 20 行 (循环, 单次批量操作)
|
||||
|
||||
引擎层面无任何变化: parseAllowZoneIds() 已支持任意数量,
|
||||
replacementZoneIdsIfPolicyActive() 已遍历 orgIds 并返回列表,
|
||||
detail() L647 已用 new ArrayList<>(replacementFloors.get()) 接收。
|
||||
```
|
||||
|
||||
### 5.5 当前架构的已知缺陷 (与方案A无关)
|
||||
|
||||
| 缺陷 | 位置 | 影响 | 是否本方案引入 |
|
||||
|------|------|------|-------------|
|
||||
| addVisitor 无 floorId 校验 | PersonRuleServiceImpl L222 | 调用方可传任意 floorId | ❌ 已有 |
|
||||
| UC-02 不做子集检查 | PersonRuleServiceImpl L187-191 | 绕过策略限制 | ❌ 已有 |
|
||||
| getDefaultByZoneId 无 null 检查 | PersonRuleServiceImpl L222-227 | NPE → 76260530 | ❌ 已有 |
|
||||
| 40F/6F 仍为硬编码 | ImgPersonServiceImpl L354-370 | 技术债 | ❌ 已有 |
|
||||
|
||||
以上缺陷在方案A、B、C 中均存在,非本方案引入。
|
||||
|
||||
---
|
||||
|
||||
## 6. 附录: 关键表DDL
|
||||
|
||||
### 6.1 tenant_visitor_floor_policy (策略表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `tenant_visitor_floor_policy` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键',
|
||||
`business_id` varchar(64) DEFAULT NULL COMMENT 'DEPRECATED',
|
||||
`org_id` varchar(32) DEFAULT NULL COMMENT '组织节点ID (隔离键)',
|
||||
`policy_type` varchar(32) NOT NULL DEFAULT 'INTERSECT_ALLOWLIST',
|
||||
`allow_zone_ids` text COMMENT 'JSON数组 zoneId列表',
|
||||
`building_id` varchar(64) DEFAULT NULL,
|
||||
`enabled` tinyint(1) NOT NULL DEFAULT '1',
|
||||
`policy_version` bigint(20) NOT NULL DEFAULT '1',
|
||||
`remark` varchar(256) DEFAULT NULL,
|
||||
`created_at` bigint(20) DEFAULT NULL,
|
||||
`updated_at` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_org_building` (`org_id`,`building_id`),
|
||||
KEY `idx_org_enabled` (`org_id`,`enabled`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
### 6.2 image_rule_ref (通行规则 — 访客权限落地表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `image_rule_ref` (
|
||||
`id` varchar(32) NOT NULL,
|
||||
`zone_id` varchar(64) NOT NULL COMMENT '楼层zone_id',
|
||||
`zone_name` varchar(64) DEFAULT NULL,
|
||||
`name` varchar(64) DEFAULT NULL COMMENT '规则名',
|
||||
`person_id` varchar(64) DEFAULT NULL COMMENT '人员id',
|
||||
`include_labels` varchar(64) DEFAULT NULL COMMENT '标签ID(单值)',
|
||||
`include_organizations` varchar(64) DEFAULT NULL COMMENT '组织ID(单值)',
|
||||
`is_default` tinyint(1) DEFAULT '0' COMMENT '默认规则标记',
|
||||
`parent_rule` varchar(64) DEFAULT NULL COMMENT '归属规则id',
|
||||
`person_delete` tinyint(1) DEFAULT '0',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `image_rule_ref_include_labels_IDX` (`include_labels`),
|
||||
KEY `image_rule_ref_include_organizations_IDX` (`include_organizations`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
### 6.3 cw_is_organization (组织架构)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `cw_is_organization` (
|
||||
`ID` varchar(32) NOT NULL,
|
||||
`NAME` varchar(60) DEFAULT NULL COMMENT '如"[28-38F]广发基金管理有限公司"',
|
||||
`PARENT_ID` varchar(32) DEFAULT NULL COMMENT '树形结构',
|
||||
`BUSINESS_ID` varchar(32) DEFAULT NULL,
|
||||
`IS_DEL` smallint(2) DEFAULT NULL,
|
||||
`IS_VALID` int(2) DEFAULT NULL COMMENT '0禁用 1启用',
|
||||
PRIMARY KEY (`ID`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
```
|
||||
|
||||
### 6.4 code_elevator_area (zone 编码)
|
||||
|
||||
```sql
|
||||
CREATE TABLE `code_elevator_area` (
|
||||
`zone_id` varchar(64) NOT NULL COMMENT '电梯编码',
|
||||
`code` varchar(64) NOT NULL COMMENT '地区编码(如0x1C=28F)',
|
||||
`parent_id` varchar(64) DEFAULT NULL COMMENT '楼栋id',
|
||||
`is_first` tinyint(4) DEFAULT NULL,
|
||||
PRIMARY KEY (`zone_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
广发基金访客默认 28F (及后续 20 层扩展) 的合理实现方案是:
|
||||
|
||||
1. **利用已有的** `tenant_visitor_floor_policy` 表驱动架构
|
||||
2. **更新** `allow_zone_ids` 字段为目标 zone 列表
|
||||
3. **不改** 任何 Java 代码 (L643-648 策略替代块已就绪)
|
||||
4. **只维护** component-organization 库 (V2 电梯侧不参与)
|
||||
5. **org_id** 做隔离键 (多租户共享同一 business_id)
|
||||
|
||||
架构评审关键点: 这不是引入新模式, 而是对 V2 既有表驱动设计的**延续使用**。
|
||||
@@ -0,0 +1,296 @@
|
||||
# 广发基金 28F 硬编码方案 — 组件/模块/位置细化设计
|
||||
|
||||
**日期**: 2026-05-09
|
||||
**分支**: `feature/guangfa-28f-hardcoded`
|
||||
**状态**: 方案细化完成,待实施
|
||||
|
||||
---
|
||||
|
||||
## 1. 涉及组件与模块总览
|
||||
|
||||
```
|
||||
source/backend/
|
||||
├── ninca-common-component-organization/ ★ 唯一变更组件
|
||||
│ ├── cwos-component-organization-service/ ★ 代码变更
|
||||
│ │ └── src/main/java/cn/cloudwalk/service/organization/service/
|
||||
│ │ └── ImgPersonServiceImpl.java ★ 唯一变更 Java 文件
|
||||
│ │
|
||||
│ ├── cwos-component-organization-starter/ ★ 配置变更
|
||||
│ │ └── deploy/run-verify/
|
||||
│ │ └── application.properties ★ 唯一变更配置文件
|
||||
│ │
|
||||
│ └── (其余模块不变: data, interface, web)
|
||||
│
|
||||
├── cw-elevator-application/ ☆ 仅增强日志
|
||||
│ └── cw-elevator-application-service/
|
||||
│ └── .../impl/PersonRuleServiceImpl.java ☆ +2 行日志
|
||||
│
|
||||
└── intelligent-cwoscomponent/ 不变 (仅 Feign 接口定义)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 变更点详细定位
|
||||
|
||||
### 2.1 模块: `cwos-component-organization-service`
|
||||
|
||||
**文件**: `backend/ninca-common-component-organization/cwos-component-organization-service/src/main/java/cn/cloudwalk/service/organization/service/ImgPersonServiceImpl.java`
|
||||
|
||||
**包**: `cn.cloudwalk.service.organization.service`
|
||||
**类**: `ImgPersonServiceImpl` (1414 行)
|
||||
|
||||
#### 位置 A — 依赖注入区 (第 157-158 行后,新增 2 行)
|
||||
|
||||
**原代码**:
|
||||
```java
|
||||
157: @Value("${xhwSixFloorId}")
|
||||
158: private String xhwSixFloorId;
|
||||
159:
|
||||
160: private static final String imageBase64 =
|
||||
```
|
||||
|
||||
**改为**:
|
||||
```java
|
||||
157: @Value("${xhwSixFloorId}")
|
||||
158: private String xhwSixFloorId;
|
||||
159:
|
||||
160: @Value("${gfOrgId}")
|
||||
161: private String gfOrgId;
|
||||
162:
|
||||
163: @Value("${gfDefaultFloorId}")
|
||||
164: private String gfDefaultFloorId;
|
||||
165:
|
||||
166: private static final String imageBase64 =
|
||||
```
|
||||
|
||||
#### 位置 B — `listByPage()` 访客列表分支 (第 354-370 行,插入 else-if)
|
||||
|
||||
**当前代码** (精确):
|
||||
```java
|
||||
354: if (imgStorePersonResult.getOrganizationIds().contains(this.xhwId)) {
|
||||
355: imgStorePersonResult.setDefaultChooseFloor(this.xhwDefaultFloorId);
|
||||
356: List<AcsPassRuleImageResultDto> floorInfoList = new ArrayList<>();
|
||||
357: AcsPassRuleImageResultDto resultDto = new AcsPassRuleImageResultDto();
|
||||
358: resultDto.setZoneId(this.xhwDefaultFloorId);
|
||||
359: resultDto.setZoneName("40F");
|
||||
360: floorInfoList.add(resultDto);
|
||||
361: imgStorePersonResult.setFloorInfoList(floorInfoList);
|
||||
362: } else {
|
||||
363: imgStorePersonResult.setDefaultChooseFloor(this.xhwSixFloorId);
|
||||
364: List<AcsPassRuleImageResultDto> floorInfoList = new ArrayList<>();
|
||||
365: AcsPassRuleImageResultDto resultDto = new AcsPassRuleImageResultDto();
|
||||
366: resultDto.setZoneId(this.xhwSixFloorId);
|
||||
367: resultDto.setZoneName("6F");
|
||||
368: floorInfoList.add(resultDto);
|
||||
369: imgStorePersonResult.setFloorInfoList(floorInfoList);
|
||||
370: }
|
||||
```
|
||||
|
||||
**改为**:
|
||||
```java
|
||||
354: if (imgStorePersonResult.getOrganizationIds().contains(this.xhwId)) {
|
||||
355: imgStorePersonResult.setDefaultChooseFloor(this.xhwDefaultFloorId);
|
||||
356: List<AcsPassRuleImageResultDto> floorInfoList = new ArrayList<>();
|
||||
357: AcsPassRuleImageResultDto resultDto = new AcsPassRuleImageResultDto();
|
||||
358: resultDto.setZoneId(this.xhwDefaultFloorId);
|
||||
359: resultDto.setZoneName("40F");
|
||||
360: floorInfoList.add(resultDto);
|
||||
361: imgStorePersonResult.setFloorInfoList(floorInfoList);
|
||||
362: } else if (imgStorePersonResult.getOrganizationIds().contains(this.gfOrgId)) {
|
||||
363: imgStorePersonResult.setDefaultChooseFloor(this.gfDefaultFloorId);
|
||||
364: List<AcsPassRuleImageResultDto> floorInfoList = new ArrayList<>();
|
||||
365: AcsPassRuleImageResultDto resultDto = new AcsPassRuleImageResultDto();
|
||||
366: resultDto.setZoneId(this.gfDefaultFloorId);
|
||||
367: resultDto.setZoneName("28F");
|
||||
368: floorInfoList.add(resultDto);
|
||||
369: imgStorePersonResult.setFloorInfoList(floorInfoList);
|
||||
370: imgStorePersonResult.setIsAcrossDay(Integer.valueOf(0));
|
||||
371: this.logger.info("[GF-28F] listByPage MATCH orgId={} in orgIds={} → default 28F",
|
||||
372: this.gfOrgId, imgStorePersonResult.getOrganizationIds());
|
||||
373: } else {
|
||||
374: imgStorePersonResult.setDefaultChooseFloor(this.xhwSixFloorId);
|
||||
375: List<AcsPassRuleImageResultDto> floorInfoList = new ArrayList<>();
|
||||
376: AcsPassRuleImageResultDto resultDto = new AcsPassRuleImageResultDto();
|
||||
377: resultDto.setZoneId(this.xhwSixFloorId);
|
||||
378: resultDto.setZoneName("6F");
|
||||
379: floorInfoList.add(resultDto);
|
||||
380: imgStorePersonResult.setFloorInfoList(floorInfoList);
|
||||
381: }
|
||||
```
|
||||
|
||||
**注意**: 行号偏移 (+5 行,因上方注入区新增)。实际编辑以内容匹配为准。
|
||||
|
||||
#### 位置 C — `detail()` 邀约+派梯核心路径 (第 643-651 行,在策略替代块后插入)
|
||||
|
||||
**当前代码** (精确):
|
||||
```java
|
||||
643: Optional<List<String>> replacementFloors =
|
||||
644: this.tenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive(
|
||||
645: result.getOrganizationIds());
|
||||
646: if (replacementFloors.isPresent()) {
|
||||
647: floorList = new ArrayList<>(replacementFloors.get());
|
||||
648: zoneNames = buildCommaSeparatedFloorNames(businessId, floorList);
|
||||
649: }
|
||||
650: result.setFloorNames(zoneNames);
|
||||
651: result.setFloorList(floorList);
|
||||
```
|
||||
|
||||
**改为**:
|
||||
```java
|
||||
643: Optional<List<String>> replacementFloors =
|
||||
644: this.tenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive(
|
||||
645: result.getOrganizationIds());
|
||||
646: if (replacementFloors.isPresent()) {
|
||||
647: floorList = new ArrayList<>(replacementFloors.get());
|
||||
648: zoneNames = buildCommaSeparatedFloorNames(businessId, floorList);
|
||||
649: }
|
||||
650: // 广发基金: 邀约 + UC-01 派梯 floorList 限制为 28F
|
||||
651: if (!CollectionUtils.isEmpty(result.getOrganizationIds())
|
||||
652: && result.getOrganizationIds().contains(this.gfOrgId)) {
|
||||
653: List<String> originalFloors = new ArrayList<>(floorList);
|
||||
654: floorList = Collections.singletonList(this.gfDefaultFloorId);
|
||||
655: zoneNames = "28F";
|
||||
656: this.logger.info("[GF-28F] detail MATCH orgId={} in orgIds={} → floor restricted: {}→[28F]",
|
||||
657: this.gfOrgId, result.getOrganizationIds(), originalFloors);
|
||||
658: } else {
|
||||
659: this.logger.debug("[GF-28F] detail NO-MATCH orgId={} not in orgIds={}",
|
||||
660: this.gfOrgId, result.getOrganizationIds());
|
||||
661: }
|
||||
662: result.setFloorNames(zoneNames);
|
||||
663: result.setFloorList(floorList);
|
||||
```
|
||||
|
||||
**注意**: `Collections.singletonList` 需要 `import java.util.Collections;` — 检查是否已存在(当前代码 `listByPage` 中使用了 `Collections.singletonList`,import 应已存在)。
|
||||
|
||||
#### 位置 D — `detail()` 入口日志 (在第 600 行附近,`detail()` 方法体内)
|
||||
|
||||
在 `detail()` 方法体的早期(如第 570-600 行区间,根据实际方法体定位),新增:
|
||||
|
||||
```java
|
||||
this.logger.info("[GF-DETAIL] entry personId={} businessId={} orgIds={}",
|
||||
param.getId(), businessId, result.getOrganizationIds());
|
||||
```
|
||||
|
||||
#### 位置 E — `detail()` listByImageId 返回日志 (在第 630 行附近)
|
||||
|
||||
在 `listByImageId` 调用成功后、循环组装 `floorList` 前:
|
||||
|
||||
```java
|
||||
this.logger.info("[GF-DETAIL] listByImageId returned {} zones: {}",
|
||||
acsPassRuleImageResultDtoList.size(),
|
||||
acsPassRuleImageResultDtoList.stream()
|
||||
.map(AcsPassRuleImageResultDto::getZoneId).collect(Collectors.toList()));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 模块: `cw-elevator-application-service`
|
||||
|
||||
**文件**: `backend/cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/person/impl/PersonRuleServiceImpl.java`
|
||||
|
||||
**包**: `cn.cloudwalk.elevator.person.impl`
|
||||
**类**: `PersonRuleServiceImpl`
|
||||
|
||||
#### 位置 F — addVisitor() 日志增强 (第 187-194 行附近)
|
||||
|
||||
**当前代码** (精确):
|
||||
```java
|
||||
187: boolean callerProvidedFloors = !CollectionUtils.isEmpty(param.getFloorIds());
|
||||
188: if (callerProvidedFloors) {
|
||||
189: effective = param.getFloorIds();
|
||||
190: this.logger.info("UC-02:调用方显式楼层 effective={}", effective);
|
||||
191: } else {
|
||||
192: effective = personResult.getFloorList();
|
||||
193: if (CollectionUtils.isEmpty(effective)) {
|
||||
194: this.logger.warn("UC-01:被访人 detail.floorList 为空 personId={}", param.getPersonId());
|
||||
```
|
||||
|
||||
**改为**:
|
||||
```java
|
||||
187: boolean callerProvidedFloors = !CollectionUtils.isEmpty(param.getFloorIds());
|
||||
188: if (callerProvidedFloors) {
|
||||
189: effective = param.getFloorIds();
|
||||
190: this.logger.info("[GF-ADDV] UC-02 effective={}", effective);
|
||||
191: } else {
|
||||
192: effective = personResult.getFloorList();
|
||||
193: if (CollectionUtils.isEmpty(effective)) {
|
||||
194: this.logger.warn("[GF-ADDV] UC-01 floorList empty personId={}", param.getPersonId());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 模块: `cwos-component-organization-starter` (配置)
|
||||
|
||||
**文件**: `backend/ninca-common-component-organization/cwos-component-organization-starter/deploy/run-verify/application.properties`
|
||||
|
||||
**位置**: 第 172 行后(`xhwSixFloorId` 之后),新增 2 行:
|
||||
|
||||
```properties
|
||||
# 第 170-172 行 (现有):
|
||||
xhwId=21474e012cd14e26bc62771873b22562
|
||||
xhwDefaultFloorId=605560547135455232
|
||||
xhwSixFloorId=605560541473144832
|
||||
|
||||
# 第 173-174 行 (新增):
|
||||
gfOrgId=488b8ad049bb43408a6fbcc50bcb89ac
|
||||
gfDefaultFloorId=605560545117995008
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.4 数据层: 禁用原表驱动策略
|
||||
|
||||
**数据库**: `component-organization` (组织库)
|
||||
**表**: `tenant_visitor_floor_policy`
|
||||
|
||||
```sql
|
||||
UPDATE tenant_visitor_floor_policy
|
||||
SET enabled = 0,
|
||||
remark = CONCAT(remark, ' [DISABLED 2026-05-09: migrated to hardcoded 28F in ImgPersonServiceImpl]'),
|
||||
updated_at = UNIX_TIMESTAMP(NOW()) * 1000
|
||||
WHERE id = 'gf_vstr_policy_guangfa_fund_001x'
|
||||
AND enabled = 1;
|
||||
```
|
||||
|
||||
**数据库**: `cw-elevator-application` (电梯库,如果存在同步表)
|
||||
```sql
|
||||
-- 仅当电梯库也有 tenant_visitor_floor_policy 表时执行
|
||||
UPDATE tenant_visitor_floor_policy
|
||||
SET enabled = 0
|
||||
WHERE id = 'gf_vstr_policy_guangfa_fund_001x'
|
||||
AND enabled = 1;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 不变更清单
|
||||
|
||||
| 组件 | 模块 | 文件 | 原因 |
|
||||
|------|------|------|------|
|
||||
| intelligent-cwoscomponent | interface | PersonService.java | Feign 接口定义,不变 |
|
||||
| intelligent-cwoscomponent | rest | PersonFeignClient.java | HTTP 路径映射,不变 |
|
||||
| ninca-common-component-organization | data | TenantVisitorFloorPolicyMapper.java | 仍服务物业策略,不删 |
|
||||
| ninca-common-component-organization | service | TenantVisitorFloorPolicyService.java | 仍服务物业策略,不删 |
|
||||
| ninca-common-component-organization | web | PersonController.java | REST 入口,不变 |
|
||||
| ninca-common-component-organization | starter | bootstrap.properties | 启动配置,不变 |
|
||||
| cw-elevator-application | web | AcsPersonController.java | REST 入口,不变 |
|
||||
| cw-elevator-application | data | ImageRuleRefDao.java | DAO 层,不变 |
|
||||
| scripts/test-env | config | component-org.properties | 测试模板(最小配置),不变 |
|
||||
| ninca-common-component-organization | releases/ | 所有 `releases/` 下文件 | 历史发布快照,不变 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 汇总
|
||||
|
||||
| # | 组件 | Maven 模块 | 文件 | 变更类型 | 行数 |
|
||||
|---|------|-----------|------|---------|------|
|
||||
| A | component-org | cwos-component-organization-**service** | `ImgPersonServiceImpl.java` | 注入 `@Value` ×2 | +4 |
|
||||
| B | component-org | cwos-component-organization-**service** | `ImgPersonServiceImpl.java` | `listByPage` else-if 分支 | +11 |
|
||||
| C | component-org | cwos-component-organization-**service** | `ImgPersonServiceImpl.java` | `detail()` floorList 截断 | +13 |
|
||||
| D | component-org | cwos-component-organization-**service** | `ImgPersonServiceImpl.java` | `detail()` 入口日志 | +2 |
|
||||
| E | component-org | cwos-component-organization-**service** | `ImgPersonServiceImpl.java` | listByImageId 返回日志 | +3 |
|
||||
| F | elevator-app | cw-elevator-application-**service** | `PersonRuleServiceImpl.java` | addVisitor 日志前缀 | ~0 (替换) |
|
||||
| — | component-org | cwos-component-organization-**starter** | `application.properties` | 配置 ×2 | +2 |
|
||||
| — | (数据库) | — | `tenant_visitor_floor_policy` | UPDATE enabled=0 | 1 行 |
|
||||
|
||||
**总计: 2 个组件, 3 个模块, 3 个文件, ~35 行新增**
|
||||
@@ -0,0 +1,243 @@
|
||||
# 广发基金 28F 策略 — 参照 40F/6F 硬编码模式重实现可行性评估
|
||||
|
||||
**日期**: 2026-05-09
|
||||
**状态**: 评估完成 — **不可行 (推荐保留表驱动方案)**
|
||||
|
||||
---
|
||||
|
||||
## 1. 两种模式的本质差异
|
||||
|
||||
### 1.1 40F/6F 硬编码模式
|
||||
|
||||
```
|
||||
┌─ @Value("${xhwId}") 注入配置
|
||||
├─ @Value("${xhwDefaultFloorId}") 注入配置
|
||||
├─ @Value("${xhwSixFloorId}") 注入配置
|
||||
│
|
||||
└─ ImgPersonServiceImpl#listByPage (仅此一处)
|
||||
└─ if (orgIds.contains(xhwId))
|
||||
└─ defaultChooseFloor = xhwDefaultFloorId, zoneName = "40F"
|
||||
else
|
||||
└─ defaultChooseFloor = xhwSixFloorId, zoneName = "6F"
|
||||
```
|
||||
|
||||
**核心特征**: 只影响 `listByPage` 访客列表展示层, **不涉及 `detail()` / `addVisitor()`**。
|
||||
|
||||
### 1.2 当前广发基金表驱动模式
|
||||
|
||||
```
|
||||
┌─ tenant_visitor_floor_policy 表 数据源
|
||||
├─ TenantVisitorFloorPolicyMapper DAO
|
||||
├─ TenantVisitorFloorPolicyService 策略引擎
|
||||
│
|
||||
├─ ImgPersonServiceImpl#listByPage P1 分支 (列表展示)
|
||||
└─ ImgPersonServiceImpl#detail() 核心路径 (UC-01 派梯)
|
||||
└─ replacementZoneIdsIfPolicyActive → floorList 替代
|
||||
└─ PersonRuleServiceImpl#addVisitor() → 写入 image_rule_ref
|
||||
```
|
||||
|
||||
**核心特征**: 同时影响 `listByPage` (列表) 和 `detail()` (派梯)。
|
||||
|
||||
### 1.3 关键差异对比
|
||||
|
||||
| 维度 | 40F/6F 硬编码 | 广发表驱动 | 差异 |
|
||||
|------|-------------|-----------|------|
|
||||
| 数据存储 | `application.properties` | `tenant_visitor_floor_policy` 表 | 文件 vs DB |
|
||||
| 变更方式 | 改配置文件 + 重启 | UPDATE SQL (立即生效) | 重启 vs 实时 |
|
||||
| 生效范围 | `listByPage` 仅列表 | `listByPage` + `detail()` | 单路径 vs 双路径 |
|
||||
| 多 zone 支持 | 否 (单 zone) | 是 (JSON 数组) | 1 vs N |
|
||||
| zone 名称 | 硬编码字符串 `"40F"` | 动态解析 (zone 表) | 硬编码 vs 动态 |
|
||||
| 组织匹配 | contains(xhwId) | 按 org_id 精确匹配 | 包含 vs 精确 |
|
||||
| 多组织策略 | 无概念 | 按 orgIds 顺序依次命中 | N/A |
|
||||
| 启用/禁用 | 无开关 | `enabled` 字段 | 需删配置 vs 一行 SQL |
|
||||
| 策略版本 | 无 | `policy_version` 自增 | 无追踪 vs 有追踪 |
|
||||
| addVisitor 影响 | ❌ 无 (不走 detail) | ✅ 核心路径 | **致命差异** |
|
||||
|
||||
---
|
||||
|
||||
## 2. 致命问题: `detail()` 路径缺失
|
||||
|
||||
### 2.1 `addVisitor()` 的楼层来源
|
||||
|
||||
```java
|
||||
// PersonRuleServiceImpl#addVisitor() 第 187-194 行
|
||||
boolean callerProvidedFloors = !CollectionUtils.isEmpty(param.getFloorIds());
|
||||
if (callerProvidedFloors) {
|
||||
effective = param.getFloorIds(); // UC-02
|
||||
} else {
|
||||
effective = personResult.getFloorList(); // UC-01 ← 来自 detail()
|
||||
}
|
||||
```
|
||||
|
||||
`detail()` 返回的 `floorList` 是 UC-01 派梯的**唯一数据源**。
|
||||
|
||||
### 2.2 `detail()` 的当前楼层组装流程
|
||||
|
||||
```java
|
||||
// ImgPersonServiceImpl#detail() 第 630-651 行
|
||||
// Step A: listByImageId 获取被访人全部楼层
|
||||
CloudwalkResult<List<AcsPassRuleImageResultDto>> images = elevatorFeignClient.listByImageId(...);
|
||||
|
||||
// Step B: 遍历组装 floorList (原始全量)
|
||||
for (AcsPassRuleImageResultDto dto : acsPassRuleImageResultDtoList) {
|
||||
floorList.add(dto.getZoneId());
|
||||
}
|
||||
|
||||
// Step C: ★ 策略替代 — 这是 40F/6F 模式完全没有的
|
||||
Optional<List<String>> replacementFloors =
|
||||
tenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive(organizationIds);
|
||||
if (replacementFloors.isPresent()) {
|
||||
floorList = new ArrayList<>(replacementFloors.get()); // ← 替代
|
||||
}
|
||||
|
||||
// Step D: 写入 PersonResult
|
||||
result.setFloorList(floorList);
|
||||
```
|
||||
|
||||
### 2.3 如果搬到 40F/6F 模式会发生什么
|
||||
|
||||
广发基金访客 `addVisitor` 的 UC-01 路径:
|
||||
|
||||
```
|
||||
// 改造后 detail() 不再查策略表
|
||||
detail() → listByImageId() → floorList = [28F, 29F, ..., 38F] 全部楼层
|
||||
→ 无策略替代 → floorList = 11 个 zone (被访人的全部楼层)
|
||||
→ addVisitor UC-01: effective = 11 个 zone
|
||||
→ 写入 11 行 image_rule_ref
|
||||
→ ❌ 访客获得了不应该有的 29-38F 权限
|
||||
```
|
||||
|
||||
**结论**: 40F/6F 模式的核心缺陷是**不接入 `detail()` 路径**。广发基金策略如果只套用 `listByPage` 硬编码, 将导致 `addVisitor` 派梯**完全绕过策略限制**, 访客获得被访人的全部楼层权限而非仅限 20 层。
|
||||
|
||||
---
|
||||
|
||||
## 3. 如果强行修补 (在 detail() 中也加硬编码)
|
||||
|
||||
### 3.1 代码膨胀预估
|
||||
|
||||
```java
|
||||
// 需要在 ImgPersonServiceImpl 中新增:
|
||||
@Value("${gfOrgId}") private String gfOrgId;
|
||||
@Value("${gfDefaultFloorList}") private String gfDefaultFloorList; // 逗号分隔 20 个 zoneId
|
||||
|
||||
// detail() 中新增 (第 646 行附近):
|
||||
if (result.getOrganizationIds().contains(this.gfOrgId)) {
|
||||
floorList = Arrays.asList(gfDefaultFloorList.split(","));
|
||||
zoneNames = buildCommaSeparatedFloorNames(businessId, floorList);
|
||||
}
|
||||
|
||||
// listByPage 中新增 (第 354 行附近):
|
||||
if (imgStorePersonResult.getOrganizationIds().contains(this.gfOrgId)) {
|
||||
List<String> floorIds = Arrays.asList(gfDefaultFloorList.split(","));
|
||||
buildFloorInfoListFromOrderedZoneIds(businessId, floorIds);
|
||||
imgStorePersonResult.setDefaultChooseFloor(floorIds.get(0));
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 问题清单
|
||||
|
||||
| # | 问题 | 严重度 |
|
||||
|---|------|--------|
|
||||
| 1 | 每新增一个租户需要加 2 个 `@Value` + 2 处 if/else | 🔴 |
|
||||
| 2 | `application.properties` 中 `gfDefaultFloorList` 需要维护 20 个 zone_id (长字符串) | 🔴 |
|
||||
| 3 | 20 个 zone_id 在配置文件中不可读,容易出错 | 🟡 |
|
||||
| 4 | 修改策略需改配置文件 + 重启服务 | 🟡 |
|
||||
| 5 | 无法动态启用/禁用 (需注释掉 @Value 或删配置) | 🟡 |
|
||||
| 6 | 无策略版本追踪 | 🟢 |
|
||||
| 7 | 与现有 `tenant_visitor_floor_policy` 表形成**双重逻辑源** | 🔴 |
|
||||
| 8 | `detail()` 中硬编码逻辑与表驱动逻辑并存,维护混乱 | 🔴 |
|
||||
|
||||
### 3.3 架构退化示意
|
||||
|
||||
```
|
||||
改造前 (表驱动, 单一逻辑源):
|
||||
detail() ─→ TenantVisitorFloorPolicyService ─→ DB
|
||||
listByPage ─→ TenantVisitorFloorPolicyService ─→ DB
|
||||
|
||||
改造后 (混合模式, 双逻辑源):
|
||||
detail() ─┬→ TenantVisitorFloorPolicyService ─→ DB (其他租户)
|
||||
└→ if (contains(gfOrgId)) { split(config) } (广发特例)
|
||||
listByPage ─┬→ TenantVisitorFloorPolicyService ─→ DB (其他租户)
|
||||
├→ if (contains(xhwId)) { "40F" } (物业)
|
||||
├→ else { "6F" } (非物业)
|
||||
└→ if (contains(gfOrgId)) { split(config) } (广发特例 ← 新增)
|
||||
```
|
||||
|
||||
**每增加一个租户策略, 代码分支呈线性增长**, 最终成为无法维护的 if/else 链。
|
||||
|
||||
---
|
||||
|
||||
## 4. 对比结论
|
||||
|
||||
| 评估维度 | 40F/6F 模式 | 表驱动模式 | 胜出 |
|
||||
|----------|-----------|-----------|------|
|
||||
| 代码简洁性 | 简单 (3 个 @Value + 1 个 if/else) | 中等 (5 个类) | 40F |
|
||||
| 扩展性 (新增租户) | 差 (每次加代码) | 优 (INSERT 一行) | 表驱动 |
|
||||
| 多 zone 支持 | 无 (单 zone) | 优 (JSON 数组) | 表驱动 |
|
||||
| 变更生效 | 需重启 | 实时 | 表驱动 |
|
||||
| 启用/禁用 | 改配置重启 | UPDATE SQL | 表驱动 |
|
||||
| 版本追踪 | 无 | policy_version | 表驱动 |
|
||||
| **addVisitor 覆盖** | **❌ 不覆盖** | **✅ 覆盖** | **表驱动** |
|
||||
| 双重逻辑源风险 | 无 (仅 listByPage) | 无 (单一策略引擎) | 平 |
|
||||
| 安全审计 | 无 | DB 行可审计 | 表驱动 |
|
||||
|
||||
### 4.1 40F/6F 模式为什么可以存在
|
||||
|
||||
40F/6F 逻辑是**星河湾物业管理的历史遗留**, 且:
|
||||
1. 其职责仅限于 `listByPage` 访客列表的**展示层默认值**
|
||||
2. 不影响 `detail()` / `addVisitor()` 的派梯权限
|
||||
3. 是"全物业" vs "非物业"的二分类, 不需要细粒度组织匹配
|
||||
4. 只有 2 个 zone, 不需要多 zone 支持
|
||||
|
||||
因此 40F/6F 的正确理解是: **这不是一个"楼层策略", 而是列表展示的 fallback 默认值**。
|
||||
|
||||
### 4.2 广发基金不能套用的根本原因
|
||||
|
||||
广发基金 20 层是**真正的访问控制策略** — 它必须限制 `addVisitor` 写入的 `image_rule_ref` 行。40F/6F 模式完全不接入这条路径, 强行接入会导致代码膨胀和双重逻辑源。
|
||||
|
||||
---
|
||||
|
||||
## 5. 推荐方案
|
||||
|
||||
### 方案 A: 保持表驱动不改 (★★★★★ 强烈推荐)
|
||||
|
||||
```
|
||||
当前架构已经正确:
|
||||
tenant_visitor_floor_policy 表 ← 数据
|
||||
TenantVisitorFloorPolicyService ← 引擎
|
||||
ImgPersonServiceImpl#detail() ← 替代 floorList
|
||||
ImgPersonServiceImpl#listByPage ← P1 展示
|
||||
PersonRuleServiceImpl#addVisitor ← UC-01 透传
|
||||
```
|
||||
|
||||
**唯一需要的变更**: UPDATE `allow_zone_ids` 从 1 个 zone 到 20 个 zone。
|
||||
|
||||
### 方案 B: 重构 40F/6F 到表驱动 (★★★ 锦上添花, 非必须)
|
||||
|
||||
将 40F/6F 的硬编码逻辑也迁移到 `tenant_visitor_floor_policy` 表:
|
||||
|
||||
```sql
|
||||
INSERT INTO tenant_visitor_floor_policy (...) VALUES (
|
||||
'pm_40f_hardcoded_migration',
|
||||
'21474e012cd14e26bc62771873b22562', -- xhwId
|
||||
'2524639890ba4f2cba9ba1a4eeaa4015',
|
||||
'INTERSECT_ALLOWLIST',
|
||||
'["605560547135455232"]', -- 40F
|
||||
NULL, 1, 1,
|
||||
'物业公司:列表默认展示 40F (迁移自硬编码)'
|
||||
);
|
||||
```
|
||||
|
||||
然后在 `listByPage` 中删除 40F/6F 硬编码分支, 统一走策略引擎。此项非当前必须, 可作为技术债务清理。
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
| 问题 | 答案 |
|
||||
|------|------|
|
||||
| 能否参照 40F/6F 模式重实现广发 28F? | **技术上可以, 产出上不可行** |
|
||||
| 根本原因 | 40F/6F 模式不接入 `detail()`, 导致 `addVisitor` 绕过策略 |
|
||||
| 如果强行修补 | 双逻辑源、代码膨胀、维护灾难 |
|
||||
| 正确做法 | 保持表驱动, 扩展 `allow_zone_ids` 即可 (零代码变更) |
|
||||
| 额外收益 | 后续可将 40F/6F 也迁移到表驱动 (统一架构) |
|
||||
@@ -0,0 +1,590 @@
|
||||
# 访客楼层策略完整走查 & 广发基金 20 层扩展评估
|
||||
|
||||
**日期**: 2026-05-09
|
||||
**状态**: 走查评估完成
|
||||
**作者**: OpenCode Agent
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [架构总览](#1-架构总览)
|
||||
2. [星河湾 40F/6F 硬编码逻辑走查](#2-星河湾-40f6f-硬编码逻辑走查)
|
||||
3. [租户访客策略完整链路走查](#3-租户访客策略完整链路走查)
|
||||
4. [广发基金当前 28F 策略分析](#4-广发基金当前-28f-策略分析)
|
||||
5. [广发基金扩展至 20 层评估](#5-广发基金扩展至-20-层评估)
|
||||
6. [变更点清单与风险矩阵](#6-变更点清单与风险矩阵)
|
||||
7. [测试验证策略](#7-测试验证策略)
|
||||
8. [附录: 关键文件索引](#8-附录-关键文件索引)
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构总览
|
||||
|
||||
### 1.1 服务拓扑
|
||||
|
||||
```
|
||||
访客登记页 (BFF/第三方)
|
||||
│
|
||||
│ POST /elevator/person/add/visitor
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ cw-elevator-application │ PersonRuleServiceImpl#addVisitor()
|
||||
│ (电梯应用) │ ┌─ 阶段1: personService.detail()
|
||||
│ │ ├─ 阶段2: UC-01/UC-02 分支
|
||||
│ │ ├─ 阶段3: 空集校验
|
||||
│ │ └─ 阶段4: 写入 image_rule_ref
|
||||
├──────────────────────────────┤
|
||||
│ PersonService.detail() │ ──Feign──▶
|
||||
└──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ ninca-common-component-org │ ImgPersonServiceImpl#detail()
|
||||
│ (组织组件) │ ┌─ listByImageId() → 电梯 Feign
|
||||
│ │ ├─ ★ 策略替代 (replacementZoneIds)
|
||||
│ │ └─ 返回 PersonResult.floorList
|
||||
│ │
|
||||
│ TenantVisitorFloorPolicyService
|
||||
│ TenantVisitorFloorPolicyMapper
|
||||
│ tenant_visitor_floor_policy 表
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 核心原则(已确立)
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **策略只在组织组件** | `tenant_visitor_floor_policy` 表及策略逻辑仅在 `ninca-common-component-organization` |
|
||||
| **电梯侧只透传** | `PersonRuleServiceImpl#addVisitor()` 不再读策略表,不做求交 |
|
||||
| **替代(Replacement)语义** | 策略命中时 `allow_zone_ids` **整表替换** `floorList`,禁止与原始楼层求交 |
|
||||
| **对外接口不变** | `PersonService.detail` → `PersonResult.floorList` 契约保持兼容 |
|
||||
| **隔离键 = org_id** | 策略按组织节点 (`org_id`) 隔离,非 `business_id` |
|
||||
|
||||
### 1.3 楼层数据流
|
||||
|
||||
```
|
||||
detail() 路径 (UC-01 权威来源):
|
||||
ImgPersonServiceImpl#detail()
|
||||
→ elevatorFeignClient.listByImageId() # 获取被访人所有楼层
|
||||
→ tenantVisitorFloorPolicyService.replacementZoneIds() # 策略替代
|
||||
→ PersonResult.floorList = allow_zone_ids # 返回替代后的楼层
|
||||
|
||||
listByPage 路径 (列表展示):
|
||||
ImgPersonServiceImpl#listByPage(isVisitor)
|
||||
→ P1: 策略命中 → 使用 allow_zone_ids (跳过 XHW 块)
|
||||
→ P2: 无策略 → listByImageId + XHW 40F/6F 硬编码逻辑
|
||||
|
||||
addVisitor() 路径 (电梯派梯):
|
||||
PersonRuleServiceImpl#addVisitor()
|
||||
→ UC-01 (无 floorIds): effective = personResult.floorList
|
||||
→ UC-02 (有 floorIds): effective = param.floorIds
|
||||
→ 写入 image_rule_ref (每条 floorId 一行)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 星河湾 40F/6F 硬编码逻辑走查
|
||||
|
||||
### 2.1 配置来源
|
||||
|
||||
**文件**: `backend/ninca-common-component-organization/cwos-component-organization-starter/deploy/run-verify/application.properties`
|
||||
|
||||
```properties
|
||||
# 第 170-172 行
|
||||
xhwId=21474e012cd14e26bc62771873b22562
|
||||
xhwDefaultFloorId=605560547135455232 # = 40F
|
||||
xhwSixFloorId=605560541473144832 # = 6F
|
||||
```
|
||||
|
||||
### 2.2 Java 实现
|
||||
|
||||
**文件**: `backend/ninca-common-component-organization/cwos-component-organization-service/src/main/java/cn/cloudwalk/service/organization/service/ImgPersonServiceImpl.java`
|
||||
|
||||
注入点 (第 154-158 行):
|
||||
```java
|
||||
@Value("${xhwId}") private String xhwId;
|
||||
@Value("${xhwDefaultFloorId}") private String xhwDefaultFloorId;
|
||||
@Value("${xhwSixFloorId}") private String xhwSixFloorId;
|
||||
```
|
||||
|
||||
### 2.3 生效路径: `listByPage` 访客列表分支
|
||||
|
||||
**关键事实**: 40F/6F 硬编码逻辑 **只存在于 `listByPage`**,不存在于 `detail()`。这意味着:
|
||||
|
||||
- **访客邀约页 / UC-01 派梯**: 走 `detail()` → 不会触发 40F/6F 逻辑
|
||||
- **访客列表页**: 走 `listByPage(isVisitor)` → 无策略命中时触发 40F/6F
|
||||
|
||||
```java
|
||||
// listByPage 第 326-370 行
|
||||
if (policyZones.isPresent()) {
|
||||
// P1: 策略命中 → 使用 allow_zone_ids (跳过 XHW 块)
|
||||
imgStorePersonResult.setFloorInfoList(policyFloorInfo);
|
||||
imgStorePersonResult.setDefaultChooseFloor(policyZones.get().get(0));
|
||||
} else {
|
||||
// P2: 无策略 → 走到 XHW 40F/6F
|
||||
List<OrgFloor> orgFloorList = orgFloorMapper.listByOrgIds(organizationIds);
|
||||
if (CollectionUtils.isEmpty(orgFloorList)) {
|
||||
imgStorePersonResult.setIsAcrossDay(1); // 跨天
|
||||
} else {
|
||||
if (imgStorePersonResult.getOrganizationIds().contains(this.xhwId)) {
|
||||
// 物业公司 → 默认 40F
|
||||
imgStorePersonResult.setDefaultChooseFloor(this.xhwDefaultFloorId);
|
||||
// zoneName = "40F"
|
||||
} else {
|
||||
// 其他组织 → 默认 6F
|
||||
imgStorePersonResult.setDefaultChooseFloor(this.xhwSixFloorId);
|
||||
// zoneName = "6F"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 40F/6F 判定逻辑总结
|
||||
|
||||
```
|
||||
是否命中租户策略?
|
||||
├── YES → 策略 allow_zone_ids 替代 (跳过 XHW 块)
|
||||
└── NO → 查询 OrgFloor 表
|
||||
├── 无 OrgFloor 记录 → isAcrossDay=1 (跨天通行)
|
||||
└── 有 OrgFloor 记录
|
||||
├── 组织 ID 含 xhwId → 默认 40F
|
||||
└── 其他组织 → 默认 6F
|
||||
```
|
||||
|
||||
### 2.5 影响范围
|
||||
|
||||
| 接口 | 是否涉及 40F/6F | 说明 |
|
||||
|------|----------------|------|
|
||||
| `PersonService.detail` | ❌ 不涉及 | detail 只做 listByImageId + 策略替代 |
|
||||
| `listByPage(isVisitor)` | ✅ 涉及 | 无策略命中时展示 40F/6F |
|
||||
| `addVisitor` | ❌ 不涉及 | 只透传 floorList/floorIds |
|
||||
|
||||
---
|
||||
|
||||
## 3. 租户访客策略完整链路走查
|
||||
|
||||
### 3.1 数据库表结构
|
||||
|
||||
**表**: `tenant_visitor_floor_policy` (组织库 `component-organization`)
|
||||
|
||||
```sql
|
||||
-- DDL 定义于: docs/sql/tenant_visitor_floor_policy_v2.sql
|
||||
id VARCHAR(64) PRIMARY KEY
|
||||
org_id VARCHAR(64) -- 组织节点 ID (隔离键)
|
||||
business_id VARCHAR(64) -- 业务 ID
|
||||
policy_type VARCHAR(64) -- 'INTERSECT_ALLOWLIST'
|
||||
allow_zone_ids TEXT -- JSON 数组: ["zoneId1","zoneId2"]
|
||||
building_id VARCHAR(64) -- NULL (全局)
|
||||
enabled TINYINT(1) -- 0/1
|
||||
policy_version INT -- 版本号 (ON UPDATE 自增)
|
||||
remark VARCHAR(512)
|
||||
created_at BIGINT
|
||||
updated_at BIGINT
|
||||
|
||||
UNIQUE KEY: uk_org_building (org_id, building_id)
|
||||
```
|
||||
|
||||
### 3.2 策略查询链路
|
||||
|
||||
```
|
||||
TenantVisitorFloorPolicyMapper.selectEnabledByOrgId(orgId)
|
||||
↓ @Select
|
||||
SELECT * FROM tenant_visitor_floor_policy
|
||||
WHERE org_id = #{orgId}
|
||||
AND enabled = 1
|
||||
AND (building_id IS NULL OR building_id = '')
|
||||
LIMIT 1
|
||||
|
||||
TenantVisitorFloorPolicyService.findEnabledPolicyByOrgId(orgId)
|
||||
↓ 返回 Optional<TenantVisitorFloorPolicy>
|
||||
|
||||
TenantVisitorFloorPolicyService.replacementZoneIdsIfPolicyActive(orgIds)
|
||||
↓ 遍历 orgIds, 找到第一个命中策略的
|
||||
↓ 解析 allow_zone_ids JSON → List<String>
|
||||
↓ 返回 Optional<List<String>> (非空列表)
|
||||
|
||||
ImgPersonServiceImpl#detail()
|
||||
↓ replacementFloors.isPresent() ?
|
||||
↓ YES: floorList = replacementFloors.get() // 替代
|
||||
↓ NO: 保留 listByImageId 原始结果
|
||||
```
|
||||
|
||||
### 3.3 多组织匹配优先级
|
||||
|
||||
`replacementZoneIdsIfPolicyActive(List<String> orgIds)` 按 `orgIds` 顺序依次尝试:
|
||||
|
||||
```java
|
||||
// TenantVisitorFloorPolicyService.java 第 70-80 行
|
||||
for (String id : orgIds) {
|
||||
Optional<List<String>> zones = replacementZoneIdsIfPolicyActive(id);
|
||||
if (zones.isPresent()) {
|
||||
return zones; // 第一个命中的策略作为结果
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
意味着: **人员所属的第一个有策略的组织决定最终楼层**。
|
||||
|
||||
### 3.4 电梯侧 addVisitor() 流程
|
||||
|
||||
```java
|
||||
// PersonRuleServiceImpl.java 第 165-270 行
|
||||
addVisitor() {
|
||||
// 阶段 1: 获取被访人 detail
|
||||
PersonResult personResult = personService.detail(detailParam);
|
||||
|
||||
// 阶段 2: 确定生效楼层
|
||||
if (callerProvidedFloors) {
|
||||
effective = param.getFloorIds(); // UC-02: 调用方指定
|
||||
} else {
|
||||
effective = personResult.getFloorList(); // UC-01: 使用 detail 返回
|
||||
}
|
||||
|
||||
// 阶段 3: 空集校验 → 76260531
|
||||
if (CollectionUtils.isEmpty(effective)) {
|
||||
return fail("76260531");
|
||||
}
|
||||
|
||||
// 阶段 4: 写入 image_rule_ref
|
||||
for (String floorId : effective) {
|
||||
ImageRuleRefResultDto defaultRule = imageRuleRefDao.getDefaultByZoneId(floorId);
|
||||
// 插入 image_rule_ref 行
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 错误码
|
||||
|
||||
| 错误码 | 含义 | 触发点 |
|
||||
|--------|------|--------|
|
||||
| `76260530` | addVisitor 未捕获异常 | PersonRuleServiceImpl:263 |
|
||||
| `76260531` | 无可用楼层 | PersonRuleServiceImpl:184, 197, 208 |
|
||||
| `76260532` | 历史: 求交为空 (已废弃) | V1 解编译版:207 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 广发基金当前 28F 策略分析
|
||||
|
||||
### 4.1 当前策略配置
|
||||
|
||||
**组织库种子数据** (`docs/sql/organization_tenant_visitor_floor_policy_init_tenants.sql`):
|
||||
|
||||
```sql
|
||||
INSERT INTO tenant_visitor_floor_policy (...) VALUES (
|
||||
'gf_vstr_policy_guangfa_fund_001x', -- id
|
||||
'488b8ad049bb43408a6fbcc50bcb89ac', -- org_id (广发基金组织节点)
|
||||
'2524639890ba4f2cba9ba1a4eeaa4015', -- business_id
|
||||
'INTERSECT_ALLOWLIST', -- policy_type
|
||||
'["605560545117995008"]', -- allow_zone_ids: 仅 28F
|
||||
NULL, 1, 1, -- building_id, enabled, version
|
||||
'广发基金:访客楼层策略(组织库);默认 28F。'
|
||||
);
|
||||
```
|
||||
|
||||
**允许楼层**: 1 个 zone — `605560545117995008` (28F)
|
||||
|
||||
### 4.2 当前生效流程
|
||||
|
||||
```
|
||||
广发基金访客邀约:
|
||||
1. 前端/POST /elevator/person/add/visitor
|
||||
2. addVisitor() → personService.detail(personId)
|
||||
3. ImgPersonServiceImpl#detail()
|
||||
→ listByImageId() 返回被访人全部楼层 (可能多 zone)
|
||||
→ replacementZoneIds(["488b8ad..."]) 命中策略
|
||||
→ floorList = ["605560545117995008"] # 仅 28F
|
||||
4. addVisitor() UC-01: effective = ["605560545117995008"]
|
||||
5. 写入 image_rule_ref (仅 28F 一条)
|
||||
```
|
||||
|
||||
### 4.3 当前配置的局限
|
||||
|
||||
| 局限 | 描述 |
|
||||
|------|------|
|
||||
| 仅 1 层 | 广发基金租用 28-38F (11层),但策略仅开放 28F 给访客 |
|
||||
| 组织粒度单一 | `org_id` 绑定到 `[28-38F]广发基金管理有限公司` 一个节点 |
|
||||
| 无部门区分 | 广发基金内部所有部门的访客均只能到达 28F |
|
||||
|
||||
### 4.4 相关文件清单
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `docs/sql/tenant_visitor_floor_policy_init_guangfa_fund.sql` | 广发 28F 种子 (电梯库侧) |
|
||||
| `docs/sql/organization_tenant_visitor_floor_policy_init_tenants.sql` | 广发 28F + 物业 28F+6F 种子 (组织库侧) |
|
||||
| `docs/testing/广发基金访客被访楼层接口生产验证.md` | 广发生产验证文档 |
|
||||
| `docs/testing/tenant-visitor-default-floor-isolation.md` | 租户隔离边界说明 |
|
||||
| `scripts/test-env/stub-person-service.py` | 测试桩 (organizationNames: ["广发基金"]) |
|
||||
| `backend/cw-elevator-application/tools/visitor_floor_verification/...` | 策略验证脚本 |
|
||||
| `docs/testing/release-visitor-noauth-verify-v20260430/...` | 批量验证套件 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 广发基金扩展至 20 层评估
|
||||
|
||||
### 5.1 需求理解
|
||||
|
||||
将广发基金访客可到达楼层从 **1 层 (28F)** 扩展至 **20 层** (推测: 28-38F 共 11 层 + 扩展至 20 层范围 = 可能包含更广范围的楼层如 19-38F 或 28-47F 等)。
|
||||
|
||||
**待确认**: 具体哪 20 层的 zone_id 列表需与业务方确认。
|
||||
|
||||
### 5.2 变更方案
|
||||
|
||||
#### 方案 A: 纯数据变更 (推荐 ★★★★★)
|
||||
|
||||
**操作**: 仅更新 `tenant_visitor_floor_policy` 表的 `allow_zone_ids` 字段, 从 `["605560545117995008"]` 扩展为 20 个 zone_id 的 JSON 数组。
|
||||
|
||||
**变更范围**:
|
||||
|
||||
| 层级 | 变更内容 |
|
||||
|------|---------|
|
||||
| 数据库 | UPDATE `tenant_visitor_floor_policy` SET `allow_zone_ids` = '["zone1",...,"zone20"]' WHERE `id` = 'gf_vstr_policy_guangfa_fund_001x' |
|
||||
| Java 代码 | **无需修改** — `parseAllowZoneIds()` 已支持任意数量 zone |
|
||||
| SQL 种子 | 更新 `organization_tenant_visitor_floor_policy_init_tenants.sql` 和 `tenant_visitor_floor_policy_init_guangfa_fund.sql` |
|
||||
| 接口契约 | **不变** — `PersonResult.floorList` 自动包含 20 个 zone |
|
||||
|
||||
**优点**:
|
||||
- 零代码变更
|
||||
- 现有策略引擎自动支持多 zone 替代
|
||||
- `detail()` 返回 `floorList` 自动包含 20 个 zone
|
||||
- `addVisitor()` 透传, 为每个 zone 写一条 `image_rule_ref`
|
||||
- 立即生效 (无需重启, 下次 `detail()` 调用即可)
|
||||
|
||||
**风险**: 无 — `parseAllowZoneIds()` 和 `buildFloorInfoListFromOrderedZoneIds()` 都已正确处理多 zone 列表。
|
||||
|
||||
**验证**:
|
||||
```sql
|
||||
-- 执行变更后验证
|
||||
SELECT id, org_id, allow_zone_ids, enabled, policy_version
|
||||
FROM tenant_visitor_floor_policy
|
||||
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
|
||||
|
||||
-- 预期: allow_zone_ids 包含 20 个 zone_id 的 JSON 数组
|
||||
```
|
||||
|
||||
#### 方案 B: 多组织拆分策略
|
||||
|
||||
**思路**: 为广发基金的不同部门 / 组织节点配置不同的楼层范围。
|
||||
|
||||
**变更范围**:
|
||||
- 新增多个 `tenant_visitor_floor_policy` 行, 每条绑定不同的 `org_id`
|
||||
- 例如: `[28-30F]广发基金IT部` → allow 28-30F, `[31-33F]广发基金财务部` → allow 31-33F
|
||||
|
||||
**适用场景**: 广发基金内部需要按部门细粒度控制访客楼层时启用。当前需求为整体 20 层开放, 暂不需要此方案。
|
||||
|
||||
#### 方案 C: 前端增强 (可选)
|
||||
|
||||
**思路**: 前端访客邀约页根据 `floorList` (20 个 zone) 展示多选列表, 允许 BFF 调用方选择特定楼层子集传入 `addVisitor`。
|
||||
|
||||
**注意**: 当前 `addVisitor` 已支持 UC-02 调用方传 `floorIds` 子集, 无需后端变更。
|
||||
|
||||
### 5.3 推荐实施步骤
|
||||
|
||||
```
|
||||
Step 1 ─ 确认 20 层 zone_id 列表
|
||||
查询 code_elevator_area 表获取对应 zone_id
|
||||
SELECT zone_id, zone_name, code FROM code_elevator_area
|
||||
WHERE building_id = '605560539791228928' -- floor.building.id
|
||||
|
||||
Step 2 ─ 更新生产库策略数据
|
||||
UPDATE tenant_visitor_floor_policy
|
||||
SET allow_zone_ids = '["zone1","zone2",...,"zone20"]',
|
||||
policy_version = policy_version + 1,
|
||||
remark = '广发基金:访客默认 20 层(扩展自 v1 28F 单层)',
|
||||
updated_at = UNIX_TIMESTAMP(NOW()) * 1000
|
||||
WHERE id = 'gf_vstr_policy_guangfa_fund_001x'
|
||||
|
||||
Step 3 ─ 更新组织库种子 SQL
|
||||
修改 organization_tenant_visitor_floor_policy_init_tenants.sql
|
||||
|
||||
Step 4 ─ 验证
|
||||
curl POST /elevator/person/add/visitor (广发基金被访人)
|
||||
检查 PersonResult.floorList 包含 20 个 zone
|
||||
检查 image_rule_ref 写入 20 行
|
||||
```
|
||||
|
||||
### 5.4 性能影响
|
||||
|
||||
| 影响项 | 评估 |
|
||||
|--------|------|
|
||||
| `detail()` 响应 | 增加 19 个 zone_id, 响应体微增 (~1-2KB) |
|
||||
| `addVisitor()` 写入 | 从 1 行 image_rule_ref 变为 20 行, 批量插入一次性完成 |
|
||||
| `listByPage` 列表 | `buildFloorInfoListFromOrderedZoneIds()` 构建 20 条而不是 1 条 |
|
||||
| zone 查询 | 结果列表变大但无额外 DB 查询 |
|
||||
|
||||
**结论**: 性能影响可忽略。核心开销在 `addVisitor` 的 `image_rule_ref` 批量插入 (20 行), 仍为单次 DB 操作。
|
||||
|
||||
### 5.5 何时需要代码变更
|
||||
|
||||
当前架构中, **以下场景需要代码变更**:
|
||||
|
||||
| 场景 | 是否需要代码变更 |
|
||||
|------|-----------------|
|
||||
| 修改 allow_zone_ids (数量变化) | ❌ 不需要 |
|
||||
| 新增策略行 (新租户) | ❌ 不需要 |
|
||||
| 修改策略类型 (policy_type) | ⚠️ 可能需要 — 当前仅实现 INTERSECT_ALLOWLIST |
|
||||
| 新增策略字段 (如按 building_id 隔离) | ✅ 需要 — Mapper/TDO 需添加字段 |
|
||||
| 前端默认选中逻辑需要映射 zone_name | ❌ 不需要 — `buildFloorInfoListFromOrderedZoneIds` 已处理 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 变更点清单与风险矩阵
|
||||
|
||||
### 6.1 变更范围最小化方案
|
||||
|
||||
| # | 步骤 | 类型 | 风险 |
|
||||
|---|------|------|------|
|
||||
| 1 | 确认 20 层 zone_id 列表 | 调研 | 🟢 |
|
||||
| 2 | UPDATE tenant_visitor_floor_policy (组织库) | DML | 🟢 |
|
||||
| 3 | UPDATE tenant_visitor_floor_policy (电梯库, 如存在) | DML | 🟢 |
|
||||
| 4 | 更新种子 SQL 文件 | 文档 | 🟢 |
|
||||
| 5 | 功能验证 (见 §7) | 测试 | 🟢 |
|
||||
|
||||
### 6.2 关键检查点
|
||||
|
||||
| 检查点 | 预期结果 |
|
||||
|--------|---------|
|
||||
| 策略查询 | `selectEnabledByOrgId('488b8ad...')` 返回 `enabled=1` 行 |
|
||||
| allow_zone_ids 解析 | `parseAllowZoneIds()` 返回 20 个 zone_id |
|
||||
| detail() floorList | `PersonResult.floorList` 含 20 个 zone_id |
|
||||
| addVisitor UC-01 | `effective` = 20 个 zone_id, 写入 20 行 image_rule_ref |
|
||||
| addVisitor UC-02 | 调用方传 5 个 floorIds → 只写 5 行 (策略不覆盖 UC-02) |
|
||||
| listByPage 访客列表 | 策略命中 → 展示 20 层, defaultChooseFloor = 第一个 zone |
|
||||
| 其他租户不受影响 | 非广发基金组织 → detail() 走 listByImageId 原路径 |
|
||||
|
||||
### 6.3 回滚方案
|
||||
|
||||
```sql
|
||||
-- 回滚到 28F 单层
|
||||
UPDATE tenant_visitor_floor_policy
|
||||
SET allow_zone_ids = '["605560545117995008"]',
|
||||
policy_version = policy_version + 1,
|
||||
remark = '广发基金:访客默认 28F(回滚)',
|
||||
updated_at = UNIX_TIMESTAMP(NOW()) * 1000
|
||||
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
|
||||
```
|
||||
|
||||
回滚立即生效, 无需重启服务。
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试验证策略
|
||||
|
||||
### 7.1 快速验证 (curl)
|
||||
|
||||
```bash
|
||||
# 广发基金被访人访客邀约
|
||||
curl -X POST http://127.0.0.1:18081/elevator/person/add/visitor \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"personId": "1072908835884208128",
|
||||
"businessId": "2524639890ba4f2cba9ba1a4eeaa4015",
|
||||
"visitorName": "test_20f",
|
||||
"begVisitorTime": "2026-05-09 00:00:00",
|
||||
"endVisitorTime": "2026-12-31 23:59:59"
|
||||
}'
|
||||
|
||||
# 预期: 返回成功, image_rule_ref 写入 20 行
|
||||
```
|
||||
|
||||
### 7.2 数据库验证
|
||||
|
||||
```sql
|
||||
-- 1. 验证策略配置
|
||||
SELECT id, org_id, allow_zone_ids, enabled, policy_version
|
||||
FROM tenant_visitor_floor_policy
|
||||
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
|
||||
|
||||
-- 2. 验证写入的规则行数
|
||||
SELECT COUNT(*) AS rule_count
|
||||
FROM image_rule_ref
|
||||
WHERE person_id = '<visitor_id>';
|
||||
|
||||
-- 预期: rule_count = 20
|
||||
|
||||
-- 3. 验证 zone 覆盖
|
||||
SELECT DISTINCT zone_id, zone_name
|
||||
FROM image_rule_ref
|
||||
WHERE person_id = '<visitor_id>'
|
||||
ORDER BY zone_name;
|
||||
```
|
||||
|
||||
### 7.3 验证脚本 (现有工具)
|
||||
|
||||
```bash
|
||||
# 使用现有组织策略验证脚本
|
||||
cd backend/cw-elevator-application/tools/visitor_floor_verification
|
||||
python3 scripts/verify_org_policy_fix.py
|
||||
|
||||
# 期望: T1 (有策略→allow 替代) 返回 20 个 zone
|
||||
# T2-T7 不受影响
|
||||
```
|
||||
|
||||
### 7.4 回归检查点
|
||||
|
||||
| 测试项 | 期望 |
|
||||
|--------|------|
|
||||
| 广发基金访客获 20 层权限 | ✅ |
|
||||
| 物业公司访客仍为 28F+6F | ✅ |
|
||||
| 其他租户访客不受影响 | ✅ |
|
||||
| UC-02 调用方指定 3 层 → 只写 3 行 | ✅ |
|
||||
| listByPage 访客列表策略命中展示 | ✅ |
|
||||
| 策略禁用 (enabled=0) → 回退到原逻辑 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 8. 附录: 关键文件索引
|
||||
|
||||
### 8.1 Java 源码
|
||||
|
||||
| 文件 | 行数参考 | 功能 |
|
||||
|------|---------|------|
|
||||
| `backend/ninca-common-component-organization/.../policy/TenantVisitorFloorPolicyService.java` | 全文 103 行 | 策略服务: 查询、解析、替代 |
|
||||
| `backend/ninca-common-component-organization/.../service/ImgPersonServiceImpl.java` | 154-158 (注入), 326-370 (listByPage), 643-648 (detail) | 策略应用点 |
|
||||
| `backend/cw-elevator-application/.../impl/PersonRuleServiceImpl.java` | 165-270 (addVisitor) | 电梯侧透传 |
|
||||
| `backend/ninca-common-component-organization/.../entity/TenantVisitorFloorPolicy.java` | 全文 | 策略实体 |
|
||||
| `backend/ninca-common-component-organization/.../mapper/TenantVisitorFloorPolicyMapper.java` | 18-26 (查询) | 策略 DAO |
|
||||
|
||||
### 8.2 SQL 脚本
|
||||
|
||||
| 文件 | 功能 |
|
||||
|------|------|
|
||||
| `docs/sql/tenant_visitor_floor_policy.sql` | 表 DDL |
|
||||
| `docs/sql/tenant_visitor_floor_policy_v2.sql` | 增加 org_id 迁移 |
|
||||
| `docs/sql/tenant_visitor_floor_policy_init_guangfa_fund.sql` | 广发基金种子 (电梯库) |
|
||||
| `docs/sql/organization_tenant_visitor_floor_policy_init_tenants.sql` | 广发 + 物业种子 (组织库) |
|
||||
| `docs/sql/tenant_visitor_floor_policy_migrate_org_id.sql` | org_id 数据迁移 |
|
||||
|
||||
### 8.3 设计文档
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `docs/superpowers/specs/2026-05-06-tenant-visitor-policy-organization-implementation.md` | 策略迁入组织组件规范 (559 行) |
|
||||
| `docs/superpowers/specs/2026-05-01-org-id-policy-fix-design.md` | org_id 粒度修复设计 |
|
||||
| `docs/business/租户访客默认楼层技术产品方案.md` | 产品方案 (427 行) |
|
||||
| `docs/testing/tenant-visitor-default-floor-isolation.md` | 租户隔离边界 (105 行) |
|
||||
| `docs/testing/visitor-registration-floor-validation.md` | 测试方案 (151 行) |
|
||||
| `docs/testing/广发基金访客被访楼层接口生产验证.md` | 广发生产验证 |
|
||||
|
||||
### 8.4 测试与验证
|
||||
|
||||
| 文件 | 功能 |
|
||||
|------|------|
|
||||
| `scripts/test-env/verify-functional.sh` | 功能验证 (F3/F4 租户策略测试) |
|
||||
| `scripts/test-env/stub-person-service.py` | 广发基金测试桩 |
|
||||
| `backend/cw-elevator-application/tools/visitor_floor_verification/...` | 策略验证脚本集 |
|
||||
| `docs/testing/release-visitor-noauth-verify-v20260430/...` | 批量验证套件 |
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
1. **40F/6F 逻辑**是星河湾物业管理的历史遗留硬编码, 仅影响 `listByPage` 访客列表展示, 不影响 `detail()` 和 `addVisitor()`。
|
||||
|
||||
2. **租户策略 (tenant_visitor_floor_policy)** 走 **替代** 语义: 命中后 `allow_zone_ids` 完全替代 `floorList`, 不做求交。
|
||||
|
||||
3. **广发基金当前仅开放 28F** (1 层), 通过 `tenant_visitor_floor_policy` 单行配置实现。
|
||||
|
||||
4. **扩展至 20 层是纯数据变更**: 只需 UPDATE `allow_zone_ids` 字段为 20 个 zone_id 的 JSON 数组。**零代码变更, 零接口变更, 零重启**。
|
||||
|
||||
5. **next step**: 确认 20 层 zone_id 列表 → UPDATE 生产库 → 更新种子 SQL → 功能验证。
|
||||
Reference in New Issue
Block a user