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:
hpd840321
2026-05-09 23:56:12 +08:00
parent 721e88dd89
commit 42c4a9fd6b
9 changed files with 2628 additions and 1 deletions
@@ -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 既有表驱动设计的**延续使用**。