Former-commit-id: 1de24b7eb79676d1aba9d799a58c5a753290cf52
24 KiB
租户访客楼层策略 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: 确认分支与编译环境
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 脚本
-- 租户访客楼层策略: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
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: 提交
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 之后插入:
// 新增字段
private String orgId;
public String getOrgId() {
return orgId;
}
public void setOrgId(String orgId) {
this.orgId = orgId;
}
注意:
businessId字段保留不删,兼容旧序列化。
- Step 2: 验证编译
cd maven-cw-elevator-application && mvn compile -pl cw-elevator-application-data -am -DskipTests
期望: BUILD SUCCESS。
- Step 3: 提交
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 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 接口
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: 验证编译
cd maven-cw-elevator-application && mvn compile -pl cw-elevator-application-data -am -DskipTests
期望: BUILD SUCCESS。
- Step 4: 提交
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 接口
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 实现
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: 验证编译
cd maven-cw-elevator-application && mvn compile -pl cw-elevator-application-data -am -DskipTests
期望: BUILD SUCCESS。
- Step 4: 提交
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 行)
完整替换:
@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 方法之后插入:
/**
* 按 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 修复):
// 旧代码:
// 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 日志放在调用处:
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: 验证编译
cd maven-cw-elevator-application && mvn compile -DskipTests
期望: BUILD SUCCESS。
- Step 5: 提交
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: 查找错误码资源文件
grep -rn "76260531\|76260532" --include="*.properties" --include="*.xml" maven-cw-elevator-application/
- Step 2: 在对应的 messages 文件中新增
76260533=策略配置了被访人无权访问的楼层,请联系管理员
- Step 3: 提交
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: 编写迁移脚本
-- 租户访客楼层策略: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: 提交
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: 全量构建
cd maven-cw-elevator-application && mvn clean install -DskipTests
期望: BUILD SUCCESS,无编译错误。
- Step 2: formatter 校验
cd maven-cw-elevator-application && mvn formatter:validate -Dformatter-maven-plugin.version=2.16.0
期望: 无格式化违规。
- Step 3: 生成发布包
bash scripts/release-cw-elevator-application.sh 2.0.10
- Step 4: 提交发布包
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通过