Compare commits

...

2 Commits

24 changed files with 277 additions and 32 deletions

View File

@ -82,7 +82,7 @@ mybatis-plus:
logging:
level:
com.pms: debug
com.pms: debug # 生产环境应设为 info
management:
endpoints:

View File

@ -102,7 +102,7 @@ jwt:
# 日志
logging:
level:
com.pms: debug
com.pms: debug # 生产环境应设为 info
# 端点暴露
management:

View File

@ -166,12 +166,22 @@ public class InternalController {
dto.setProjectId(owner.getProjectId());
dto.setName(owner.getOwnerName());
dto.setPhone(owner.getPhone());
dto.setIdNo(owner.getIdNo());
dto.setIdNo(maskIdNo(owner.getIdNo()));
dto.setType(owner.getIdType());
dto.setStatus(owner.getStatus());
return dto;
}
/**
* 身份证号脱敏保留前6位和后4位中间用 **** 替换
*/
private String maskIdNo(String idNo) {
if (idNo == null || idNo.length() < 10) {
return idNo;
}
return idNo.substring(0, 6) + "****" + idNo.substring(idNo.length() - 4);
}
private InternalBaseDTOs.DeviceDTO toInternalDeviceDTO(DeviceDTO device) {
InternalBaseDTOs.DeviceDTO dto = new InternalBaseDTOs.DeviceDTO();
dto.setId(device.getId());

View File

@ -1,6 +1,7 @@
package com.pms.base.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import com.pms.common.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -38,4 +39,8 @@ public class CamCharge extends BaseEntity {
/** 备注 */
private String remark;
/** 乐观锁版本号 */
@Version
private Integer version;
}

View File

@ -1,6 +1,7 @@
package com.pms.base.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import com.pms.common.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -38,4 +39,8 @@ public class CommunityActivity extends BaseEntity {
/** 备注 */
private String remark;
/** 乐观锁版本号 */
@Version
private Integer version;
}

View File

@ -1,6 +1,7 @@
package com.pms.base.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import com.pms.common.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -51,4 +52,8 @@ public class LeaseContract extends BaseEntity {
/** 备注 */
private String remark;
/** 乐观锁版本号 */
@Version
private Integer version;
}

View File

@ -1,6 +1,7 @@
package com.pms.base.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import com.pms.common.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -30,4 +31,8 @@ public class MeetingRoom extends BaseEntity {
/** 备注 */
private String remark;
/** 乐观锁版本号 */
@Version
private Integer version;
}

View File

@ -1,6 +1,7 @@
package com.pms.base.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import com.pms.common.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -59,4 +60,8 @@ public class Renovation extends BaseEntity {
/** 备注 */
private String remark;
/** 乐观锁版本号 */
@Version
private Integer version;
}

View File

@ -1,6 +1,7 @@
package com.pms.base.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import com.pms.common.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -36,4 +37,8 @@ public class SafetyInspection extends BaseEntity {
/** 备注 */
private String remark;
/** 乐观锁版本号 */
@Version
private Integer version;
}

View File

@ -1,6 +1,7 @@
package com.pms.base.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import com.pms.common.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -39,4 +40,8 @@ public class WorkshopLease extends BaseEntity {
/** 备注 */
private String remark;
/** 乐观锁版本号 */
@Version
private Integer version;
}

View File

@ -14,6 +14,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -130,8 +131,13 @@ public class AbilityPackageServiceImpl implements AbilityPackageService {
pa.setUpdatedAt(now);
pa.setCreatedBy(userId);
pa.setUpdatedBy(userId);
projectAbilityMapper.insert(pa);
inserted++;
try {
projectAbilityMapper.insert(pa);
inserted++;
} catch (DuplicateKeyException e) {
// 并发情况下可能已插入视为成功幂等
log.info("能力包关联已存在,跳过: projectId={}, packageId={}", projectId, pkg.getId());
}
}
log.info("项目 {} 关联能力包 {} 完成,新增 {} 个关联",
projectId, packageCodes, inserted);

View File

@ -1,6 +1,7 @@
package com.pms.base.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pms.base.entity.CommunityActivity;
@ -123,19 +124,18 @@ public class CommunityActivityServiceImpl implements CommunityActivityService {
if (existing.getStatus() != STATUS_OPEN) {
throw new BusinessException(ErrorCode.OPERATION_FAILED, "当前活动不在报名中");
}
int max = existing.getMaxParticipants() != null ? existing.getMaxParticipants() : 0;
int current = existing.getCurrentCount() != null ? existing.getCurrentCount() : 0;
if (max > 0 && current >= max) {
throw new BusinessException(ErrorCode.OPERATION_FAILED, "活动报名人数已满");
// 原子更新current_count = current_count + 1 WHERE id = ? AND (max_participants <= 0 OR current_count < max_participants)
UpdateWrapper<CommunityActivity> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", id)
.apply("(max_participants <= 0 OR current_count < max_participants)")
.setSql("current_count = current_count + 1")
.set("updated_at", System.currentTimeMillis())
.set("updated_by", UserContext.getUserId());
int updated = communityActivityMapper.update(null, updateWrapper);
if (updated == 0) {
throw new BusinessException(ErrorCode.OPERATION_FAILED, "报名人数已满");
}
CommunityActivity update = new CommunityActivity();
update.setId(id);
update.setCurrentCount(current + 1);
update.setUpdatedAt(System.currentTimeMillis());
update.setUpdatedBy(UserContext.getUserId());
communityActivityMapper.updateById(update);
log.info("活动报名成功: id={}, currentCount={}", id, current + 1);
log.info("活动报名成功: id={}", id);
}
@Override

View File

@ -1,6 +1,7 @@
package com.pms.base.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.pms.base.entity.LookupDict;
import com.pms.base.entity.LookupItem;
import com.pms.base.mapper.LookupDictMapper;
@ -12,6 +13,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -146,7 +148,21 @@ public class LookupServiceImpl implements LookupService {
item.setUpdatedAt(now);
item.setCreatedBy(userId);
item.setUpdatedBy(userId);
lookupItemMapper.insert(item);
try {
lookupItemMapper.insert(item);
} catch (DuplicateKeyException e) {
// 并发情况下已插入改为更新
log.info("项目级字典项已存在,改为更新: dictCode={}, itemCode={}", dictCode, itemCode);
UpdateWrapper<LookupItem> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("dict_id", dict.getId())
.eq("item_code", itemCode)
.eq("scope", SCOPE_PROJECT)
.eq("project_id", projectId)
.set("enabled", CommonConstants.STATUS_DISABLED)
.set("updated_at", now)
.set("updated_by", userId);
lookupItemMapper.update(null, updateWrapper);
}
}
log.info("项目 {} 字典 {} 禁用项 {}", projectId, dictCode, itemCode);
}

View File

@ -88,7 +88,7 @@ crypto:
logging:
level:
com.pms: debug
com.pms: debug # 生产环境应设为 info
management:
endpoints:

View File

@ -0,0 +1,10 @@
-- CR #7: 为 7 个状态实体添加 version 乐观锁字段
-- MyBatis-Plus OptimisticLockerInnerInterceptor 已在所有模块配置
ALTER TABLE t_renovation ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号';
ALTER TABLE t_lease_contract ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号';
ALTER TABLE t_safety_inspection ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号';
ALTER TABLE t_meeting_room ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号';
ALTER TABLE t_community_activity ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号';
ALTER TABLE t_cam_charge ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号';
ALTER TABLE t_workshop_lease ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号';

View File

@ -388,12 +388,11 @@ class ResidentialT2FlowTest {
activity.setMaxParticipants(50);
activity.setCurrentCount(10);
when(communityActivityMapper.selectById(7002L)).thenReturn(activity);
when(communityActivityMapper.update(eq(null), any())).thenReturn(1);
communityActivityService.signUp(7002L);
ArgumentCaptor<CommunityActivity> captor = ArgumentCaptor.forClass(CommunityActivity.class);
verify(communityActivityMapper).updateById(captor.capture());
assertThat(captor.getValue().getCurrentCount()).isEqualTo(11);
verify(communityActivityMapper).update(eq(null), any());
}
@Test
@ -403,12 +402,13 @@ class ResidentialT2FlowTest {
activity.setMaxParticipants(20);
activity.setCurrentCount(20);
when(communityActivityMapper.selectById(7003L)).thenReturn(activity);
when(communityActivityMapper.update(eq(null), any())).thenReturn(0);
assertThatThrownBy(() -> communityActivityService.signUp(7003L))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("报名人数已满");
verify(communityActivityMapper, never()).updateById(any());
verify(communityActivityMapper).update(eq(null), any());
}
@Test
@ -418,12 +418,11 @@ class ResidentialT2FlowTest {
activity.setMaxParticipants(0);
activity.setCurrentCount(0);
when(communityActivityMapper.selectById(7004L)).thenReturn(activity);
when(communityActivityMapper.update(eq(null), any())).thenReturn(1);
communityActivityService.signUp(7004L);
ArgumentCaptor<CommunityActivity> captor = ArgumentCaptor.forClass(CommunityActivity.class);
verify(communityActivityMapper).updateById(captor.capture());
assertThat(captor.getValue().getCurrentCount()).isEqualTo(1);
verify(communityActivityMapper).update(eq(null), any());
}
}

View File

@ -82,7 +82,7 @@ mybatis-plus:
logging:
level:
com.pms: debug
com.pms: debug # 生产环境应设为 info
management:
endpoints:

View File

@ -1,5 +1,6 @@
package com.pms.common.dto.internal;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serial;
@ -113,6 +114,7 @@ public final class InternalBaseDTOs {
@Serial
private static final long serialVersionUID = 1L;
@Size(max = 500, message = "批量查询ID数量不能超过500")
private java.util.List<Long> roomIds;
}
}

View File

@ -124,7 +124,7 @@ mybatis-plus:
logging:
level:
com.pms: debug
com.pms: debug # 生产环境应设为 info
management:
endpoints:

View File

@ -68,7 +68,7 @@ jwt:
# 日志
logging:
level:
com.pms: debug
com.pms: debug # 生产环境应设为 info
org.springframework.cloud.gateway: info
# 端点暴露

View File

@ -142,7 +142,7 @@ notify:
logging:
level:
com.pms: debug
com.pms: debug # 生产环境应设为 info
management:
endpoints:

View File

@ -93,7 +93,7 @@ crypto:
logging:
level:
com.pms: debug
com.pms: debug # 生产环境应设为 info
management:
endpoints:

View File

@ -0,0 +1,142 @@
---
title: Flyway 迁移脚本回滚操作手册
problem_type: migration-rollback
component: backend (pms-base, pms-charge, pms-operation)
severity: warning
created: 2026-07-01
related_cr: CR #10
---
# Flyway 迁移脚本回滚操作手册
## 背景
Flyway 不支持自动 down script生产环境需要回滚时需手动执行 SQL。本文档覆盖 `feat/multi-property-config` 引入的 V2-V9 迁移脚本pms-base、V3 数据迁移pms-charge、V2-V3 列类型迁移pms-operation
## 回滚原则
1. **逆序回滚**:从最高版本号开始,逐版本降级
2. **先备份**:执行前 `mysqldump` 备份相关表
3. **更新 Flyway 元数据**:回滚后需修正 `flyway_schema_history`
## pms-base 迁移回滚
### V9 → V8移除 version 列
```sql
ALTER TABLE t_renovation DROP COLUMN version;
ALTER TABLE t_lease_contract DROP COLUMN version;
ALTER TABLE t_safety_inspection DROP COLUMN version;
ALTER TABLE t_meeting_room DROP COLUMN version;
ALTER TABLE t_community_activity DROP COLUMN version;
ALTER TABLE t_cam_charge DROP COLUMN version;
ALTER TABLE t_workshop_lease DROP COLUMN version;
```
### V8 → V7删除产业园 T2 表
```sql
DROP TABLE IF EXISTS t_enterprise_profile;
DROP TABLE IF EXISTS t_safety_inspection;
DROP TABLE IF EXISTS t_energy_meter;
DROP TABLE IF EXISTS t_workshop_lease;
```
### V7 → V6删除商办 T2 表
```sql
DROP TABLE IF EXISTS t_enterprise_service;
DROP TABLE IF EXISTS t_meeting_room;
DROP TABLE IF EXISTS t_cam_charge;
DROP TABLE IF EXISTS t_lease_contract;
```
### V6 → V5删除住宅 T2 表
```sql
DROP TABLE IF EXISTS t_public_revenue;
DROP TABLE IF EXISTS t_community_activity;
DROP TABLE IF EXISTS t_renovation;
DROP TABLE IF EXISTS t_owner_committee;
```
### V5 → V4删除收费 Lookup 种子数据
```sql
DELETE FROM t_lookup_item WHERE dict_id IN (
SELECT id FROM t_lookup_dict WHERE dict_code IN ('FEE_TYPE', 'CHARGE_RULE', 'BILL_STATUS', 'CALC_RULE')
);
DELETE FROM t_lookup_dict WHERE dict_code IN ('FEE_TYPE', 'CHARGE_RULE', 'BILL_STATUS', 'CALC_RULE');
```
### V4 → V3删除能力包种子数据
```sql
DELETE FROM t_ability_package_item WHERE package_id IN (
SELECT id FROM t_ability_package WHERE code LIKE 'T%'
);
DELETE FROM t_ability_package WHERE code LIKE 'T%';
```
### V3 → V2删除 Lookup 字典表
```sql
DROP TABLE IF EXISTS t_lookup_item;
DROP TABLE IF EXISTS t_lookup_dict;
```
### V2 → V1删除能力包表
```sql
DROP TABLE IF EXISTS t_project_ability;
DROP TABLE IF EXISTS t_ability_package_item;
DROP TABLE IF EXISTS t_ability_package;
```
## pms-charge 迁移回滚
### V3SpEL 计费规则数据迁移FIXED → unitPrice
**不可逆**:此迁移将 `calc_rule='FIXED'` 的记录改为 `calc_rule='unitPrice'`,并设置了 `calc_expression` 字段。
**回滚方案**
```sql
-- 恢复 FIXED 类型(仅对未修改 unitPrice 的记录)
UPDATE t_charge_standard SET calc_rule = 'FIXED', calc_expression = NULL
WHERE calc_rule = 'unitPrice' AND calc_expression IS NULL;
-- 注意:已设置 SpEL 表达式的记录无法自动回滚,需根据业务判断
```
## pms-operation 迁移回滚
### V2-V3工单/巡检类型 TINYINT → Lookup 字典
**不可逆**:此迁移将 `type` 字段从 TINYINT 改为 VARCHAR并迁移了数据。
**回滚方案**
```sql
-- 需要先备份原始 TINYINT 值,回滚时恢复
-- 由于列类型已改变,需 ALTER TABLE 修改回 TINYINT
```
## 修正 Flyway 元数据
回滚完成后,修正 `flyway_schema_history` 表:
```sql
-- 删除已回滚版本的记录
DELETE FROM flyway_schema_history WHERE version > '目标版本号';
-- 例如回滚到 V8删除 V9
DELETE FROM flyway_schema_history WHERE version = '9';
```
## 紧急回滚流程
1. 停止应用服务
2. `mysqldump -u root -p pms_base > pms_base_backup.sql`
3. 执行回滚 SQL按版本逆序
4. 修正 `flyway_schema_history`
5. 部署旧版本代码
6. 启动应用服务并验证

View File

@ -5,7 +5,7 @@ component: backend (pms-base, pms-charge, pms-operation, pms-common)
severity: critical/warning/info
created: 2026-07-01
branch: feat/multi-property-config
status: open
status: resolved
---
# feat/multi-property-config 代码审查遗留问题
@ -149,3 +149,28 @@ status: open
3. **第二个 Sprint**CR #4(事务内 Feign+ CR #7(乐观锁)+ CR #5CAM 幂等,待补 Controller 时)
4. **第三个 Sprint**CR #8-13竞态条件批量处理+ CR #10(回滚文档)
5. **持续改进**CR #14-17Info 级别)
## 修复状态总览2026-07-01 更新)
### 已修复fix/code-review-residuals 分支)
- ✅ CR #113 个 Service 添加 ProjectSecurityChecker projectId 归属校验
- ✅ CR #24 个 Service create 强制 setProjectIdfeat 分支修复)
- ✅ CR #34 个 Service update 改用白名单字段构造
- ✅ CR #4CamAllocationService + ChargeBillServiceImpl Feign 调用移出事务
- ✅ CR #5CAM 分摊添加幂等检查
- ✅ CR #6LeaseContractServiceImpl.renew 添加状态+日期校验feat 分支修复)
- ✅ CR #12pms-operation SafetyInspectionServiceImpl 添加 projectId 校验
### 已修复fix/code-review-p1-p2 分支)
- ✅ CR #77 个状态实体添加 @Version 乐观锁 + V9 迁移脚本
- ✅ CR #8assignPackages catch DuplicateKeyException 幂等处理
- ✅ CR #9signUp 原子更新防超员 `current_count = current_count + 1 WHERE current_count < max_participants`
- ✅ CR #10Flyway 迁移回滚操作手册(见 docs/solutions/2026-07-01-flyway-migration-rollback-guide.md
- ✅ CR #13disableProjectItem catch DuplicateKeyException 后重试 update
- ✅ CR #14RoomBatchQueryRequest 添加 @Size(max=500) 限制
- ✅ CR #158 个 application.yml 添加生产环境日志级别注释
- ✅ CR #17InternalController 身份证号脱敏前6后4中间****
### 已评估(不修改代码)
- ⚠️ CR #11:维持无 FK 架构。理由:微服务架构下 FK 不跨服务;应用层已通过 ProjectSecurityChecker 校验CR #1 修复);项目既有约定为无 FK。建议在 CI 中加入数据完整性检查脚本作为补充。
- ⚠️ CR #16Internal 接口鉴权建议在基础设施 Sprint 中统一处理。方案IP 白名单K8s NetworkPolicy或 header token 校验Spring Interceptor。当前依赖网关保护Pod 间网络隔离作为临时措施。