Files
starRiverProperty/docs/superpowers/plans/2026-05-01-org-id-policy-fix.md
T
hpd840321 7b2bd307f1 Initial commit: reorganized source tree
- backend/: 13 Maven modules (cw-elevator-application, cloudwalk-cloud, intelligent-cwoscomponent, ninca-crk, etc.)
- frontend/: 4 Vue projects (elevator-front, cwos-portal, alarm-front, front_acs) + decompiled + scripts
- scripts/: build, test-env, tools (Docker Compose, service templates, API parity)
- docs/: AGENTS.md, superpowers specs, architecture docs
- .gitignore: standard Java/Maven exclusions

Moved from legacy maven-*/ root layout to backend/ organized structure.
2026-05-09 09:56:45 +08:00

648 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 租户访客楼层策略 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 组织节点 IDcw_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 数据迁移
-- 前提:DDLTask 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` 通过