mirror of
https://github.com/hpd840321/starRiverProperty.git
synced 2026-06-09 08:20:31 +08:00
7b2bd307f1
- backend/: 13 Maven modules (cw-elevator-application, cloudwalk-cloud, intelligent-cwoscomponent, ninca-crk, etc.) - frontend/: 4 Vue projects (elevator-front, cwos-portal, alarm-front, front_acs) + decompiled + scripts - scripts/: build, test-env, tools (Docker Compose, service templates, API parity) - docs/: AGENTS.md, superpowers specs, architecture docs - .gitignore: standard Java/Maven exclusions Moved from legacy maven-*/ root layout to backend/ organized structure.
648 lines
24 KiB
Markdown
648 lines
24 KiB
Markdown
# 租户访客楼层策略 org_id 粒度修复 — 实施计划
|
||
|
||
> **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:** 将 `tenant_visitor_floor_policy` 的策略键从 `business_id` 改为 `org_id`,实现二选一语义(有策略用 allow,无策略用 floorList),修复 F1/F2/W2 问题。
|
||
|
||
**Architecture:** DDL 先上线(加列+改约束,不影响行为)→ 代码切换(Mapper/DAO/Service 三层的 business_id → org_id + 二选一逻辑)→ 数据迁移(运维 SQL 填 org_id)。整体改动控制在 7 个文件内,最小风险。
|
||
|
||
**Tech Stack:** Java 8, Spring Boot, MyBatis, MySQL 5.7
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-05-01-org-id-policy-fix-design.md`
|
||
|
||
---
|
||
|
||
## 前置条件
|
||
|
||
- [ ] **Step 0: 确认分支与编译环境**
|
||
|
||
```bash
|
||
git checkout -b fix/org-id-policy-granularity
|
||
cd maven-cw-elevator-application && mvn formatter:validate -Dformatter-maven-plugin.version=2.16.0
|
||
```
|
||
|
||
期望: formatter 校验通过。
|
||
|
||
---
|
||
|
||
### Task 1: DDL — 策略表结构变更
|
||
|
||
**Files:**
|
||
- Create: `docs/sql/tenant_visitor_floor_policy_v2.sql`
|
||
|
||
- [ ] **Step 1: 编写 DDL 脚本**
|
||
|
||
```sql
|
||
-- 租户访客楼层策略:org_id 粒度修复
|
||
-- 执行顺序:先 DDL → 数据迁移(Task 5)→ 发应用包
|
||
-- 回滚:DROP INDEX uk_org_building, DROP COLUMN org_id, ADD UNIQUE KEY uk_biz_building (business_id, building_id)
|
||
|
||
USE `cw-elevator-application`;
|
||
|
||
-- 1. 新增 org_id 列
|
||
ALTER TABLE tenant_visitor_floor_policy
|
||
ADD COLUMN org_id VARCHAR(32) NULL COMMENT '组织节点ID(cw_is_organization.ID)'
|
||
AFTER business_id;
|
||
|
||
-- 2. 替换唯一约束(business_id → org_id)
|
||
ALTER TABLE tenant_visitor_floor_policy
|
||
DROP INDEX uk_biz_building,
|
||
ADD UNIQUE KEY uk_org_building (org_id, building_id);
|
||
|
||
-- 3. 标记 business_id 为废弃
|
||
ALTER TABLE tenant_visitor_floor_policy
|
||
MODIFY COLUMN business_id VARCHAR(64) NULL COMMENT 'DEPRECATED: 已废弃,以 org_id 为准';
|
||
|
||
-- 验证
|
||
SELECT COLUMN_NAME, COLUMN_KEY, COLUMN_COMMENT
|
||
FROM INFORMATION_SCHEMA.COLUMNS
|
||
WHERE TABLE_SCHEMA = 'cw-elevator-application'
|
||
AND TABLE_NAME = 'tenant_visitor_floor_policy'
|
||
ORDER BY ORDINAL_POSITION;
|
||
```
|
||
|
||
- [ ] **Step 2: 在开发库执行 DDL**
|
||
|
||
```bash
|
||
mysql -h 192.168.3.12 -P 3307 -u root -p123456 cw-elevator-application < docs/sql/tenant_visitor_floor_policy_v2.sql
|
||
```
|
||
|
||
期望: 无错误,`org_id` 列存在,`uk_org_building` 索引存在,`uk_biz_building` 已删除。
|
||
|
||
- [ ] **Step 3: 提交**
|
||
|
||
```bash
|
||
git add docs/sql/tenant_visitor_floor_policy_v2.sql
|
||
git commit -m "feat: add org_id column and uk_org_building constraint to tenant_visitor_floor_policy"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: DTO — 新增 orgId 字段
|
||
|
||
**Files:**
|
||
- Modify: `maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/dto/TenantVisitorFloorPolicyDto.java`
|
||
|
||
- [ ] **Step 1: 添加 orgId 字段 + getter/setter**
|
||
|
||
在 `businessId` 的 setter 之后插入:
|
||
|
||
```java
|
||
// 新增字段
|
||
private String orgId;
|
||
|
||
public String getOrgId() {
|
||
return orgId;
|
||
}
|
||
|
||
public void setOrgId(String orgId) {
|
||
this.orgId = orgId;
|
||
}
|
||
```
|
||
|
||
> 注意:`businessId` 字段保留不删,兼容旧序列化。
|
||
|
||
- [ ] **Step 2: 验证编译**
|
||
|
||
```bash
|
||
cd maven-cw-elevator-application && mvn compile -pl cw-elevator-application-data -am -DskipTests
|
||
```
|
||
|
||
期望: BUILD SUCCESS。
|
||
|
||
- [ ] **Step 3: 提交**
|
||
|
||
```bash
|
||
git add maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/dto/TenantVisitorFloorPolicyDto.java
|
||
git commit -m "feat: add orgId field to TenantVisitorFloorPolicyDto"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Mapper — SQL 切换 business_id → org_id
|
||
|
||
**Files:**
|
||
- Modify: `maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/mapper/TenantVisitorFloorPolicyMapper.xml`
|
||
- Modify: `maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/mapper/TenantVisitorFloorPolicyMapper.java`
|
||
|
||
- [ ] **Step 1: 修改 Mapper XML — WHERE 条件 + 映射**
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||
|
||
<mapper namespace="cn.cloudwalk.elevator.person.mapper.TenantVisitorFloorPolicyMapper">
|
||
|
||
<select id="selectEnabledByOrgId" resultType="cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto">
|
||
SELECT id,
|
||
org_id AS orgId,
|
||
policy_type AS policyType,
|
||
allow_zone_ids AS allowZoneIds,
|
||
building_id AS buildingId,
|
||
enabled AS enabled,
|
||
policy_version AS policyVersion
|
||
FROM tenant_visitor_floor_policy
|
||
WHERE org_id = #{orgId,jdbcType=VARCHAR}
|
||
AND enabled = 1
|
||
AND policy_type = 'INTERSECT_ALLOWLIST'
|
||
AND (building_id IS NULL OR building_id = '')
|
||
ORDER BY updated_at DESC, policy_version DESC
|
||
LIMIT 1
|
||
</select>
|
||
|
||
<!-- 旧方法保留作历史参考(可选删除)
|
||
<select id="selectEnabledTenantDefault" resultType="...">
|
||
... business_id ...
|
||
</select>
|
||
-->
|
||
</mapper>
|
||
```
|
||
|
||
- [ ] **Step 2: 修改 Mapper 接口**
|
||
|
||
```java
|
||
package cn.cloudwalk.elevator.person.mapper;
|
||
|
||
import cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto;
|
||
import org.apache.ibatis.annotations.Param;
|
||
|
||
public interface TenantVisitorFloorPolicyMapper {
|
||
|
||
/**
|
||
* 按组织节点 ID 查询启用中的 INTERSECT_ALLOWLIST 策略(building_id 为空)。
|
||
*/
|
||
TenantVisitorFloorPolicyDto selectEnabledByOrgId(@Param("orgId") String orgId);
|
||
|
||
// 旧方法(废弃,保留以兼容编译)
|
||
// TenantVisitorFloorPolicyDto selectEnabledTenantDefault(@Param("businessId") String businessId);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 验证编译**
|
||
|
||
```bash
|
||
cd maven-cw-elevator-application && mvn compile -pl cw-elevator-application-data -am -DskipTests
|
||
```
|
||
|
||
期望: BUILD SUCCESS。
|
||
|
||
- [ ] **Step 4: 提交**
|
||
|
||
```bash
|
||
git add maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/mapper/TenantVisitorFloorPolicyMapper.xml
|
||
git add maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/mapper/TenantVisitorFloorPolicyMapper.java
|
||
git commit -m "feat: change policy query from business_id to org_id in TenantVisitorFloorPolicyMapper"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: DAO — 接口与实现切换
|
||
|
||
**Files:**
|
||
- Modify: `maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/dao/TenantVisitorFloorPolicyDao.java`
|
||
- Modify: `maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/impl/TenantVisitorFloorPolicyDaoImpl.java`
|
||
|
||
- [ ] **Step 1: 修改 DAO 接口**
|
||
|
||
```java
|
||
package cn.cloudwalk.elevator.person.dao;
|
||
|
||
import cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto;
|
||
|
||
public interface TenantVisitorFloorPolicyDao {
|
||
|
||
/**
|
||
* 按组织节点 ID 查询启用中的 INTERSECT_ALLOWLIST 策略(building_id 为空)。
|
||
*
|
||
* @param orgId 组织节点 ID(cw_is_organization.ID)
|
||
* @return 无配置时 null
|
||
*/
|
||
TenantVisitorFloorPolicyDto selectEnabledByOrgId(String orgId);
|
||
|
||
// 旧方法(废弃)
|
||
// TenantVisitorFloorPolicyDto selectEnabledTenantDefault(String businessId);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 修改 DAO 实现**
|
||
|
||
```java
|
||
package cn.cloudwalk.elevator.person.impl;
|
||
|
||
import cn.cloudwalk.elevator.person.dao.TenantVisitorFloorPolicyDao;
|
||
import cn.cloudwalk.elevator.person.dto.TenantVisitorFloorPolicyDto;
|
||
import cn.cloudwalk.elevator.person.mapper.TenantVisitorFloorPolicyMapper;
|
||
import javax.annotation.Resource;
|
||
import org.springframework.stereotype.Repository;
|
||
|
||
@Repository
|
||
public class TenantVisitorFloorPolicyDaoImpl implements TenantVisitorFloorPolicyDao {
|
||
|
||
@Resource
|
||
private TenantVisitorFloorPolicyMapper tenantVisitorFloorPolicyMapper;
|
||
|
||
@Override
|
||
public TenantVisitorFloorPolicyDto selectEnabledByOrgId(String orgId) {
|
||
return this.tenantVisitorFloorPolicyMapper.selectEnabledByOrgId(orgId);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 验证编译**
|
||
|
||
```bash
|
||
cd maven-cw-elevator-application && mvn compile -pl cw-elevator-application-data -am -DskipTests
|
||
```
|
||
|
||
期望: BUILD SUCCESS。
|
||
|
||
- [ ] **Step 4: 提交**
|
||
|
||
```bash
|
||
git add maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/dao/TenantVisitorFloorPolicyDao.java
|
||
git add maven-cw-elevator-application/cw-elevator-application-data/src/main/java/cn/cloudwalk/elevator/person/impl/TenantVisitorFloorPolicyDaoImpl.java
|
||
git commit -m "feat: update DAO interface and impl to use org_id query"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: Service — addVisitor 核心逻辑重写
|
||
|
||
**Files:**
|
||
- Modify: `maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/person/impl/PersonRuleServiceImpl.java`
|
||
|
||
这是改动最大的文件。分 3 个子步骤。
|
||
|
||
- [ ] **Step 1: 重写 addVisitor 方法(第 174-275 行)**
|
||
|
||
完整替换:
|
||
|
||
```java
|
||
@CloudwalkParamsValidate
|
||
public CloudwalkResult<Boolean> addVisitor(AcsPersonAddVisitorParam param, CloudwalkCallContext context)
|
||
throws ServiceException {
|
||
this.logger.info("根据被访人添加访客派梯权限开始,AcsPersonAddVisitorParam=[{}], CloudwalkCallContext=[{}]",
|
||
JSONObject.toJSONString(param), JSONObject.toJSONString(context));
|
||
try {
|
||
// ===== Step 1: 获取被访人信息(UC-01/02 都需要) =====
|
||
PersonDetailParam detailParam = new PersonDetailParam();
|
||
detailParam.setId(param.getPersonId());
|
||
detailParam.setBusinessId(context.getCompany().getCompanyId());
|
||
CloudwalkResult<PersonResult> detail = this.personService.detail(detailParam, context);
|
||
if (detail == null || !detail.isSuccess()) {
|
||
String code = detail != null ? detail.getCode() : "76260531";
|
||
String msg = detail != null ? detail.getMessage() : getMessage("76260531");
|
||
return CloudwalkResult.fail(code, msg);
|
||
}
|
||
PersonResult personResult = (PersonResult) detail.getData();
|
||
if (personResult == null) {
|
||
return CloudwalkResult.fail("76260531", getMessage("76260531"));
|
||
}
|
||
List<String> hostFloors = personResult.getFloorList();
|
||
if (CollectionUtils.isEmpty(hostFloors)) {
|
||
return CloudwalkResult.fail("76260531", getMessage("76260531"));
|
||
}
|
||
|
||
// ===== Step 2: 按 org_id 查找策略 =====
|
||
TenantVisitorFloorPolicyDto policy = findPolicyByOrgIds(personResult.getOrganizationIds());
|
||
|
||
// ===== Step 3: 确定生效楼层(二选一,不求交) =====
|
||
List<String> effectiveFloors;
|
||
boolean callerProvidedFloors = !CollectionUtils.isEmpty(param.getFloorIds());
|
||
|
||
if (policy != null) {
|
||
// 有策略:直接用 allow,忽略调用方 floorIds
|
||
effectiveFloors = resolveEffectiveFloors(
|
||
callerProvidedFloors ? param.getFloorIds() : hostFloors,
|
||
hostFloors, policy, param.getPersonId());
|
||
} else {
|
||
// 无策略:用调用方 floorIds 或 hostFloors
|
||
effectiveFloors = callerProvidedFloors ? param.getFloorIds() : hostFloors;
|
||
if (callerProvidedFloors) {
|
||
// UC-02 软校验:记录不在 hostFloors 中的楼层
|
||
Set<String> hostSet = new HashSet<>(hostFloors);
|
||
List<String> outliers = param.getFloorIds().stream()
|
||
.filter(f -> !hostSet.contains(f))
|
||
.collect(Collectors.toList());
|
||
if (!outliers.isEmpty()) {
|
||
this.logger.warn("UC-02 传入非被访人授权楼层 businessId={} personId={} outliers={}",
|
||
context.getCompany().getCompanyId(), param.getPersonId(), outliers);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (CollectionUtils.isEmpty(effectiveFloors)) {
|
||
return CloudwalkResult.fail("76260531", getMessage("76260531"));
|
||
}
|
||
param.setFloorIds(effectiveFloors);
|
||
|
||
// ===== Step 4: 落库(不变) =====
|
||
ZoneQueryParam zoneQueryParam = new ZoneQueryParam();
|
||
zoneQueryParam.setId(param.getFloorIds().get(0));
|
||
zoneQueryParam.setRowsOfPage(10);
|
||
zoneQueryParam.setCurrentPage(1);
|
||
CloudwalkResult<CloudwalkPageAble<ZoneResult>> zonePage = this.zoneService.page(zoneQueryParam, context);
|
||
List<ZoneResult> zoneResults = (List<ZoneResult>) ((CloudwalkPageAble) zonePage.getData()).getDatas();
|
||
String imageStoreId =
|
||
this.deviceImageStoreDao.getByBuildingId(((ZoneResult) zoneResults.get(0)).getParentId());
|
||
List<ImageRuleRefAddDto> insertList = new ArrayList<>();
|
||
for (String floorId : param.getFloorIds()) {
|
||
ImageRuleRefResultDto defaultRule = this.imageRuleRefDao.getDefaultByZoneId(floorId);
|
||
ImageRuleRefAddDto addDto = new ImageRuleRefAddDto();
|
||
addDto.setId(genUUID());
|
||
addDto.setBusinessId(context.getCompany().getCompanyId());
|
||
addDto.setPersonId(param.getVisitorId());
|
||
addDto.setParentRule(defaultRule.getId());
|
||
addDto.setName(defaultRule.getName());
|
||
addDto.setZoneId(defaultRule.getZoneId());
|
||
addDto.setZoneName(defaultRule.getZoneName());
|
||
addDto.setCreateTime(Long.valueOf(System.currentTimeMillis()));
|
||
addDto.setLastUpdateTime(Long.valueOf(System.currentTimeMillis()));
|
||
addDto.setPersonDelete(Integer.valueOf(0));
|
||
insertList.add(addDto);
|
||
}
|
||
this.logger.info("访客添加派梯权限开始,数据为=[{}]", JSONObject.toJSONString(insertList));
|
||
if (!CollectionUtils.isEmpty(insertList)) {
|
||
this.imageRuleRefDao.insertList(insertList);
|
||
}
|
||
ImageStorePersonBindParam imageStorePersonBindParam = new ImageStorePersonBindParam();
|
||
imageStorePersonBindParam.setImageStoreId(imageStoreId);
|
||
imageStorePersonBindParam.setPersonIds(Collections.singletonList(param.getVisitorId()));
|
||
imageStorePersonBindParam.setNullDateIsLongTerm(Boolean.valueOf(true));
|
||
imageStorePersonBindParam.setExpiryBeginDate(param.getBegVisitorTime());
|
||
imageStorePersonBindParam.setExpiryEndDate(param.getEndVisitorTime());
|
||
this.logger.info("远程调用绑定人员图库开始,imageStorePersonBindParam=[{}], CloudwalkCallContext=[{}]",
|
||
JSONObject.toJSONString(imageStorePersonBindParam), JSONObject.toJSONString(context));
|
||
CloudwalkResult<ImgStoreBatchBindPersonResult> bindResult =
|
||
this.imageStorePersonService.batchBind(imageStorePersonBindParam, context);
|
||
if (!bindResult.isSuccess()) {
|
||
this.logger.error("远程调用绑定人员图库异常,原因:[{}],失败人员id:[{}]", bindResult.getMessage(), param.getVisitorId());
|
||
return CloudwalkResult.fail(bindResult.getCode(), bindResult.getMessage());
|
||
}
|
||
UpdateGroupPersonRefParam refParam = new UpdateGroupPersonRefParam();
|
||
refParam.setBusinessId(context.getCompany().getCompanyId());
|
||
refParam.setPersonIds(Collections.singletonList(param.getVisitorId()));
|
||
refParam.setImageStoreId(imageStoreId);
|
||
this.imageStorePersonService.updateGroupPersonRef(refParam, context);
|
||
} catch (ServiceException e) {
|
||
throw e;
|
||
} catch (Exception e) {
|
||
this.logger.error("根据被访人添加访客派梯权限失败,原因:[{}]", e);
|
||
throw new ServiceException("76260530", getMessage("76260530"));
|
||
}
|
||
return CloudwalkResult.success(Boolean.valueOf(true));
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 添加两个新辅助方法 + 修改 W2(JSON 日志升级)**
|
||
|
||
在 `addVisitor` 方法之后插入:
|
||
|
||
```java
|
||
/**
|
||
* 按 org_id 查找策略,遍历 organizationIds 取第一个命中。
|
||
*/
|
||
private TenantVisitorFloorPolicyDto findPolicyByOrgIds(List<String> orgIds) {
|
||
if (CollectionUtils.isEmpty(orgIds)) return null;
|
||
for (String orgId : orgIds) {
|
||
TenantVisitorFloorPolicyDto p = this.tenantVisitorFloorPolicyDao.selectEnabledByOrgId(orgId);
|
||
if (p != null && p.getEnabled() != null && p.getEnabled().intValue() == 1) {
|
||
List<String> allow = parseAllowZoneIds(p.getAllowZoneIds());
|
||
if (!CollectionUtils.isEmpty(allow)) return p;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 二选一:用 allow 替换 fallbackFloors。
|
||
* 约束:allow 必须是 hostFloors 的子集,否则拒绝(76260533)。
|
||
*/
|
||
private List<String> resolveEffectiveFloors(
|
||
List<String> fallbackFloorsUnused, List<String> hostFloors,
|
||
TenantVisitorFloorPolicyDto policy, String personId) {
|
||
List<String> allow = parseAllowZoneIds(policy.getAllowZoneIds());
|
||
if (CollectionUtils.isEmpty(allow)) return fallbackFloorsUnused;
|
||
|
||
// 安全校验:allow 中每个值必须在 hostFloors 中存在
|
||
Set<String> hostSet = new HashSet<>(hostFloors);
|
||
List<String> unknownAllow = allow.stream()
|
||
.filter(a -> !hostSet.contains(a))
|
||
.collect(Collectors.toList());
|
||
if (!unknownAllow.isEmpty()) {
|
||
this.logger.error("策略配置错误:allow 包含不在被访人 floorList 中的 zoneId!"
|
||
+ "orgId={} policyId={} personId={} unknownAllow={} hostFloors={}",
|
||
policy.getOrgId(), policy.getId(), personId, unknownAllow, hostFloors);
|
||
throw new ServiceException("76260533",
|
||
"策略配置了被访人无权访问的楼层,请联系管理员");
|
||
}
|
||
|
||
this.logger.info("策略生效 orgId={} policyId={} v={} allowSize={} hostSize={}",
|
||
policy.getOrgId(), policy.getId(), policy.getPolicyVersion(),
|
||
allow.size(), hostFloors.size());
|
||
return allow;
|
||
}
|
||
```
|
||
|
||
同时修改 `parseAllowZoneIds` 的 catch 块(W2 修复):
|
||
|
||
```java
|
||
// 旧代码:
|
||
// this.logger.warn("allow_zone_ids JSON 无效,按无策略处理: {}", e.getMessage());
|
||
|
||
// 新代码:
|
||
this.logger.error("allow_zone_ids JSON 无效,策略失效!policyId={} raw={}",
|
||
"policy.id", json, e); // 注意:此处无法获取 policy.id,改用实际可用字段
|
||
```
|
||
|
||
> 实际实现时,`parseAllowZoneIds` 不持有 `policyId`,可以在 `resolveEffectiveFloors` 中调用 `parseAllowZoneIds` 之前先做 null 检查,将 ERROR 日志放在调用处:
|
||
|
||
```java
|
||
private List<String> resolveEffectiveFloors(...) {
|
||
String rawJson = policy.getAllowZoneIds();
|
||
List<String> allow = parseAllowZoneIds(rawJson);
|
||
if (CollectionUtils.isEmpty(allow)) {
|
||
if (!StringUtils.isBlank(rawJson)) {
|
||
this.logger.error("allow_zone_ids JSON 无效或为空,策略失效!orgId={} policyId={} raw={}",
|
||
policy.getOrgId(), policy.getId(), rawJson);
|
||
}
|
||
return fallbackFloorsUnused;
|
||
}
|
||
// ... 后续校验
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 删除旧辅助方法 `intersectPreserveHostOrder`(不再需要)**
|
||
|
||
该方法已被 `resolveEffectiveFloors` 替代,可删除或保留(无调用方即可)。
|
||
|
||
- [ ] **Step 4: 验证编译**
|
||
|
||
```bash
|
||
cd maven-cw-elevator-application && mvn compile -DskipTests
|
||
```
|
||
|
||
期望: BUILD SUCCESS。
|
||
|
||
- [ ] **Step 5: 提交**
|
||
|
||
```bash
|
||
git add maven-cw-elevator-application/cw-elevator-application-service/src/main/java/cn/cloudwalk/elevator/person/impl/PersonRuleServiceImpl.java
|
||
git commit -m "feat: rewrite addVisitor with org_id policy lookup and either-or semantics
|
||
|
||
- Replace business_id policy key with org_id from PersonResult.getOrganizationIds()
|
||
- Change from intersection (floorList ∩ allow) to either-or (policy? allow : floorList)
|
||
- Add resolveEffectiveFloors with allow ⊆ floorList safety check (76260533)
|
||
- UC-02 now also checks policy (policy takes precedence over caller floorIds)
|
||
- Upgrade JSON parse failure log from WARN to ERROR
|
||
- Remove unused intersectPreserveHostOrder method"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: 错误码注册(76260533)
|
||
|
||
**Files:**
|
||
- Check: `maven-cw-elevator-application/cw-elevator-application-starter/src/main/resources/access-control.properties`(或对应的 messages 资源文件)
|
||
|
||
- [ ] **Step 1: 查找错误码资源文件**
|
||
|
||
```bash
|
||
grep -rn "76260531\|76260532" --include="*.properties" --include="*.xml" maven-cw-elevator-application/
|
||
```
|
||
|
||
- [ ] **Step 2: 在对应的 messages 文件中新增**
|
||
|
||
```properties
|
||
76260533=策略配置了被访人无权访问的楼层,请联系管理员
|
||
```
|
||
|
||
- [ ] **Step 3: 提交**
|
||
|
||
```bash
|
||
git add <错误码资源文件路径>
|
||
git commit -m "feat: add error code 76260533 for policy-host floor mismatch"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: 数据迁移 SQL
|
||
|
||
**Files:**
|
||
- Create: `docs/sql/tenant_visitor_floor_policy_migrate_org_id.sql`
|
||
|
||
- [ ] **Step 1: 编写迁移脚本**
|
||
|
||
```sql
|
||
-- 租户访客楼层策略:business_id → org_id 数据迁移
|
||
-- 前提:DDL(Task 1)已执行
|
||
-- 执行方式:人工确认 org_id 对应关系后逐行执行
|
||
|
||
USE cw-elevator-application;
|
||
|
||
-- 1. 列出所有公司级组织节点(供确认)
|
||
-- 在 component-organization 库执行:
|
||
-- SELECT o.ID, o.NAME, o.PARENT_ID
|
||
-- FROM `component-organization`.cw_is_organization o
|
||
-- WHERE o.BUSINESS_ID = '2524639890ba4f2cba9ba1a4eeaa4015'
|
||
-- AND o.IS_DEL = 0
|
||
-- ORDER BY o.NAME;
|
||
|
||
-- 2. 为现有策略行填入 org_id(示例:广发基金)
|
||
-- 请先确认 NAME 匹配正确
|
||
UPDATE tenant_visitor_floor_policy
|
||
SET org_id = '<广发基金的 org_id>',
|
||
business_id = NULL -- 可选:标记 business_id 已废弃
|
||
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
|
||
|
||
-- 3. 为其他公司新增策略行(模板)
|
||
-- INSERT INTO tenant_visitor_floor_policy
|
||
-- (id, org_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, remark, created_at, updated_at)
|
||
-- VALUES
|
||
-- (REPLACE(UUID(),'-',''), '<公司 org_id>', 'INTERSECT_ALLOWLIST',
|
||
-- '["<zone_id>"]', NULL, 1, 1, '', UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000);
|
||
|
||
-- 4. 验证迁移结果
|
||
SELECT id, org_id, business_id, policy_type, allow_zone_ids, enabled
|
||
FROM tenant_visitor_floor_policy
|
||
ORDER BY org_id;
|
||
```
|
||
|
||
- [ ] **Step 2: 提交**
|
||
|
||
```bash
|
||
git add docs/sql/tenant_visitor_floor_policy_migrate_org_id.sql
|
||
git commit -m "docs: add org_id data migration SQL for tenant_visitor_floor_policy"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: 构建验证 + 发布准备
|
||
|
||
- [ ] **Step 1: 全量构建**
|
||
|
||
```bash
|
||
cd maven-cw-elevator-application && mvn clean install -DskipTests
|
||
```
|
||
|
||
期望: BUILD SUCCESS,无编译错误。
|
||
|
||
- [ ] **Step 2: formatter 校验**
|
||
|
||
```bash
|
||
cd maven-cw-elevator-application && mvn formatter:validate -Dformatter-maven-plugin.version=2.16.0
|
||
```
|
||
|
||
期望: 无格式化违规。
|
||
|
||
- [ ] **Step 3: 生成发布包**
|
||
|
||
```bash
|
||
bash scripts/release-cw-elevator-application.sh 2.0.10
|
||
```
|
||
|
||
- [ ] **Step 4: 提交发布包**
|
||
|
||
```bash
|
||
git add releases/
|
||
git commit -m "release: cw-elevator-application v2.0.10 with org_id policy fix"
|
||
```
|
||
|
||
---
|
||
|
||
## 回滚方案
|
||
|
||
| 步骤 | 操作 |
|
||
|------|------|
|
||
| 1. 回滚应用包 | 部署旧版本 JAR(用 `business_id` 查询的代码) |
|
||
| 2. 回滚 DDL(可选) | `DROP INDEX uk_org_building; ALTER TABLE ... DROP COLUMN org_id; ADD UNIQUE KEY uk_biz_building (business_id, building_id);` |
|
||
| 3. 恢复数据(可选) | `UPDATE tenant_visitor_floor_policy SET business_id = '252463...' WHERE org_id IS NOT NULL;` |
|
||
|
||
> DDL 回滚不影响旧代码行为(旧代码不查 `org_id` 列)。
|
||
|
||
---
|
||
|
||
## 发布顺序(生产环境)
|
||
|
||
```
|
||
1. DDL 上线(Task 1) → 表结构变更,不影响线上行为
|
||
2. 数据迁移(Task 7) → 运维手工填 org_id
|
||
3. 发应用包(Task 8) → 代码切换到 org_id 查询
|
||
4. 验证(Task 8 后) → 抽样确认策略生效
|
||
```
|
||
|
||
---
|
||
|
||
## 完成检查清单
|
||
|
||
- [ ] DDL 在开发库执行成功
|
||
- [ ] `TenantVisitorFloorPolicyDto` 有 `orgId` 字段
|
||
- [ ] Mapper XML/Java 使用 `org_id` 查询
|
||
- [ ] DAO 接口/实现已切换
|
||
- [ ] `addVisitor` 使用 `findPolicyByOrgIds` + `resolveEffectiveFloors`
|
||
- [ ] W2 修复:JSON 解析失败打 ERROR 而非 WARN
|
||
- [ ] 错误码 76260533 已在资源文件注册
|
||
- [ ] 数据迁移 SQL 已编写
|
||
- [ ] `mvn clean install` 通过
|
||
- [ ] `mvn formatter:validate` 通过
|