Merge feat/pms-base-hardening: pms-base 模块加固 U1-U10 + ce-code-review + ce-simplify
- U1 AES-256-GCM 加密 + V12 idNoHash 回填 - U3 BigDecimal→Long(分) 全链路迁移 + V13 - U4 phone/email 加密+hash 索引 + V14 - U5 @Valid 输入校验 - U6 测试覆盖 357 tests (修复 24 LifecycleWriteGuard null + 12 新测试) - U7 Resilience4j 熔断 + AuthClient fallback - U8 @Transactional on delete methods - U9 @Lazy 循环依赖文档 - U10 @PreAuthorize 28 Controllers 142 注解 + V15 权限码 - ce-code-review: V13 数据精度修复 + roomIds @Size - ce-simplify-code: V12 批量更新 + 删除冗余赋值
This commit is contained in:
commit
f8ce2b2221
|
|
@ -0,0 +1,33 @@
|
|||
# Ponytail, lazy senior dev mode
|
||||
|
||||
You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.
|
||||
|
||||
Before writing any code, stop at the first rung that holds:
|
||||
|
||||
1. Does this need to be built at all? (YAGNI)
|
||||
2. Does the standard library already do this? Use it.
|
||||
3. Does a native platform feature cover it? Use it.
|
||||
4. Does an already-installed dependency solve it? Use it.
|
||||
5. Can this be one line? Make it one line.
|
||||
6. Only then: write the minimum code that works.
|
||||
|
||||
## Rules
|
||||
|
||||
- No abstractions that weren't explicitly requested.
|
||||
- No new dependency if it can be avoided.
|
||||
- No boilerplate nobody asked for.
|
||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
||||
- Pick the edge-case-correct option when two stdlib approaches are the same size, lazy means less code, not the flimsier algorithm.
|
||||
- Mark intentional simplifications with a `ponytail:` comment. If the shortcut has a known ceiling (global lock, O(n²) scan, naive heuristic), the comment names the ceiling and the upgrade path.
|
||||
|
||||
## Never lazy about
|
||||
|
||||
- Input validation at trust boundaries.
|
||||
- Error handling that prevents data loss.
|
||||
- Security.
|
||||
- Accessibility.
|
||||
- The calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off).
|
||||
- Anything explicitly requested.
|
||||
|
||||
Lazy code without its check is unfinished: non-trivial logic leaves ONE runnable check behind, the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test.
|
||||
12
CONCEPTS.md
12
CONCEPTS.md
|
|
@ -114,6 +114,18 @@ lifecycle_stage 与 Project 原有的 `status` 字段(0/1)正交:status
|
|||
|
||||
合规保留期从归档时间(`archived_at`)起算。归档恢复后重新归档,保留期从最近一次归档时间起算。
|
||||
|
||||
## 代码设计约定(Code Design Conventions)
|
||||
|
||||
### @Lazy 循环依赖(@Lazy Circular Dependency)
|
||||
|
||||
`LifecycleServiceImpl` 与 `ArchiveServiceImpl` 之间的双向依赖,通过 Spring `@Lazy` 字段注入打破。`LifecycleServiceImpl` 构造器注入 `ArchiveService`(final 字段),`ArchiveServiceImpl` 用 `@Autowired @Lazy` 字段注入 `LifecycleService`(非 final)。
|
||||
|
||||
这是已知设计而非技术债——归档恢复需同步推进生命周期阶段(`ArchiveServiceImpl.archive()` → `lifecycleService.advanceStage()`),归档审批通过需回调归档执行(`LifecycleServiceImpl.approve()` → `archiveService.markArchived()`)。事件总线异步解耦不适用:归档恢复要求阶段推进在同一事务内完成,异步事件无法保证事务一致性。
|
||||
|
||||
*测试注意:* Mockito `@InjectMocks` 不会注入 `@Lazy` 非 final 字段,测试需用 `ReflectionTestUtils.setField` 手动注入。详见 [docs/solutions/conventions/lazy-injection-reflectiontestutils-testing.md](docs/solutions/conventions/lazy-injection-reflectiontestutils-testing.md)。
|
||||
|
||||
*避免混淆:* `@Lazy` 循环依赖 ≠ 设计缺陷。当两个 Service 必须在同一事务内互调时,`@Lazy` 是 Spring 官方推荐解法;若互调可异步化,应优先用事件总线(MQ)解耦。
|
||||
|
||||
## Flagged Ambiguities
|
||||
|
||||
- "平台"在 EtherPMS 中至少有两个含义:认证域的 OAuth 提供方(本文件 Platform Login 条目),以及部署/基础设施语境中的运行环境。在认证与登录语境中,统一指 OAuth 提供方。
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ dependencies {
|
|||
// 服务间调用
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
|
||||
implementation 'io.github.resilience4j:resilience4j-spring-boot3'
|
||||
|
||||
// Nacos
|
||||
implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery'
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.pms.common.response.Result;
|
|||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -33,6 +34,7 @@ public class ApprovalFlowConfigController {
|
|||
* 按阶段流转标识查询启用模板
|
||||
*/
|
||||
@GetMapping("/transition/{transition}")
|
||||
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
|
||||
public Result<ApprovalFlowTemplateDTO> getByTransition(@PathVariable("transition") String transition) {
|
||||
return Result.success(approvalFlowConfigService.getTemplateByTransition(transition));
|
||||
}
|
||||
|
|
@ -41,6 +43,7 @@ public class ApprovalFlowConfigController {
|
|||
* 列出全部模板
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
|
||||
public Result<List<ApprovalFlowTemplateDTO>> list() {
|
||||
return Result.success(approvalFlowConfigService.listTemplates());
|
||||
}
|
||||
|
|
@ -49,6 +52,7 @@ public class ApprovalFlowConfigController {
|
|||
* 保存模板(创建或更新)
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "审批流配置修改")
|
||||
public Result<Map<String, Long>> save(@Valid @RequestBody ApprovalFlowTemplateSaveRequest request) {
|
||||
RoleGuard.assertA4();
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.pms.common.response.Result;
|
|||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -29,6 +30,7 @@ public class ArchiveController {
|
|||
* 发起归档(结算校验 → 资产移交 → 归档审批)
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "项目归档")
|
||||
public Result<Map<String, Long>> archive(@PathVariable Long projectId,
|
||||
@Valid @RequestBody AssetHandoverRequest request) {
|
||||
|
|
@ -42,6 +44,7 @@ public class ArchiveController {
|
|||
* 发起归档恢复
|
||||
*/
|
||||
@PostMapping("/recover")
|
||||
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "归档恢复")
|
||||
public Result<Map<String, Long>> recover(@PathVariable Long projectId,
|
||||
@Valid @RequestBody ArchiveRecoveryRequest request) {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import com.pms.base.dto.BuildingDTO;
|
|||
import com.pms.base.dto.BuildingSaveRequest;
|
||||
import com.pms.base.dto.BuildingTreeDTO;
|
||||
import com.pms.base.service.BuildingService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -32,6 +34,7 @@ public class BuildingController {
|
|||
* 楼栋列表查询(分页)
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:building:manage')")
|
||||
public Result<PageResult<BuildingDTO>> list(BuildingDTO query) {
|
||||
return Result.success(buildingService.page(query));
|
||||
}
|
||||
|
|
@ -40,6 +43,8 @@ public class BuildingController {
|
|||
* 创建楼栋
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:building:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建楼栋")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody BuildingSaveRequest request) {
|
||||
Long id = buildingService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -51,6 +56,7 @@ public class BuildingController {
|
|||
* 楼栋详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:building:manage')")
|
||||
public Result<BuildingDTO> getById(@PathVariable Long id) {
|
||||
return Result.success(buildingService.getById(id));
|
||||
}
|
||||
|
|
@ -59,6 +65,8 @@ public class BuildingController {
|
|||
* 更新楼栋
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:building:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新楼栋")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody BuildingSaveRequest request) {
|
||||
buildingService.update(id, request);
|
||||
return Result.success();
|
||||
|
|
@ -68,6 +76,8 @@ public class BuildingController {
|
|||
* 删除楼栋
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:building:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除楼栋")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
buildingService.delete(id);
|
||||
return Result.success();
|
||||
|
|
@ -77,6 +87,7 @@ public class BuildingController {
|
|||
* 楼栋树(楼栋→楼层→房间)
|
||||
*/
|
||||
@GetMapping("/tree")
|
||||
@PreAuthorize("hasAuthority('base:building:manage')")
|
||||
public Result<List<BuildingTreeDTO>> tree(@RequestParam(required = false) Long buildingId) {
|
||||
return Result.success(buildingService.tree(UserContext.getProjectId(), buildingId));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ package com.pms.base.controller;
|
|||
|
||||
import com.pms.base.entity.CamCharge;
|
||||
import com.pms.base.service.CamChargeService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
@ -30,16 +31,19 @@ public class CamChargeController {
|
|||
* @param chargePeriod 分摊周期(如 2024-01)
|
||||
*/
|
||||
@PostMapping("/allocate")
|
||||
public Result<List<CamCharge>> allocate(@RequestParam BigDecimal totalAmount,
|
||||
@PreAuthorize("hasAuthority('base:finance:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "执行 CAM 分摊")
|
||||
public Result<List<CamCharge>> allocate(@RequestParam Long totalAmountFen,
|
||||
@RequestParam String chargePeriod) {
|
||||
Long projectId = UserContext.getProjectId();
|
||||
return Result.success(camChargeService.allocateCamCharge(projectId, totalAmount, chargePeriod));
|
||||
return Result.success(camChargeService.allocateCamCharge(projectId, totalAmountFen, chargePeriod));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前项目某周期的分摊明细
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:finance:manage')")
|
||||
public Result<List<CamCharge>> list(@RequestParam String period) {
|
||||
Long projectId = UserContext.getProjectId();
|
||||
return Result.success(camChargeService.listByProjectAndPeriod(projectId, period));
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ package com.pms.base.controller;
|
|||
|
||||
import com.pms.base.entity.CommunityActivity;
|
||||
import com.pms.base.service.CommunityActivityService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -28,6 +31,7 @@ public class CommunityActivityController {
|
|||
* 分页查询活动列表
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:activity:manage')")
|
||||
public Result<PageResult<CommunityActivity>> page(
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
|
|
@ -39,6 +43,7 @@ public class CommunityActivityController {
|
|||
* 活动详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:activity:manage')")
|
||||
public Result<CommunityActivity> getById(@PathVariable Long id) {
|
||||
return Result.success(communityActivityService.getById(id));
|
||||
}
|
||||
|
|
@ -47,7 +52,9 @@ public class CommunityActivityController {
|
|||
* 创建活动
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Map<String, Long>> create(@RequestBody CommunityActivity activity) {
|
||||
@PreAuthorize("hasAuthority('base:activity:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建社区活动")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody CommunityActivity activity) {
|
||||
Long id = communityActivityService.create(activity);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
|
|
@ -58,7 +65,9 @@ public class CommunityActivityController {
|
|||
* 更新活动
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody CommunityActivity activity) {
|
||||
@PreAuthorize("hasAuthority('base:activity:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新社区活动")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody CommunityActivity activity) {
|
||||
communityActivityService.update(id, activity);
|
||||
return Result.success();
|
||||
}
|
||||
|
|
@ -67,6 +76,8 @@ public class CommunityActivityController {
|
|||
* 报名参加活动
|
||||
*/
|
||||
@PostMapping("/{id}/sign-up")
|
||||
@PreAuthorize("hasAuthority('base:activity:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "活动报名")
|
||||
public Result<Void> signUp(@PathVariable Long id) {
|
||||
communityActivityService.signUp(id);
|
||||
return Result.success();
|
||||
|
|
@ -76,6 +87,8 @@ public class CommunityActivityController {
|
|||
* 删除活动
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:activity:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除社区活动")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
communityActivityService.delete(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ import com.pms.base.dto.ContractDTO;
|
|||
import com.pms.base.dto.ContractRenewRequest;
|
||||
import com.pms.base.dto.ContractSaveRequest;
|
||||
import com.pms.base.service.ContractService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -31,6 +33,7 @@ public class ContractController {
|
|||
* 合同列表查询(分页)
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:contract:manage')")
|
||||
public Result<PageResult<ContractDTO>> list(ContractDTO query) {
|
||||
return Result.success(contractService.page(query));
|
||||
}
|
||||
|
|
@ -39,6 +42,8 @@ public class ContractController {
|
|||
* 创建合同
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:contract:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建合同")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody ContractSaveRequest request) {
|
||||
Long id = contractService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -50,6 +55,7 @@ public class ContractController {
|
|||
* 合同详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:contract:manage')")
|
||||
public Result<ContractDTO> getById(@PathVariable Long id) {
|
||||
return Result.success(contractService.getById(id));
|
||||
}
|
||||
|
|
@ -58,6 +64,8 @@ public class ContractController {
|
|||
* 更新合同
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:contract:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新合同")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody ContractSaveRequest request) {
|
||||
contractService.update(id, request);
|
||||
return Result.success();
|
||||
|
|
@ -67,6 +75,8 @@ public class ContractController {
|
|||
* 删除合同
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:contract:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除合同")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
contractService.delete(id);
|
||||
return Result.success();
|
||||
|
|
@ -76,6 +86,7 @@ public class ContractController {
|
|||
* 即将到期合同列表
|
||||
*/
|
||||
@GetMapping("/expiring")
|
||||
@PreAuthorize("hasAuthority('base:contract:manage')")
|
||||
public Result<List<ContractDTO>> expiring(@RequestParam(defaultValue = "30") int days) {
|
||||
return Result.success(contractService.expiring(days));
|
||||
}
|
||||
|
|
@ -84,6 +95,8 @@ public class ContractController {
|
|||
* 合同续签
|
||||
*/
|
||||
@PostMapping("/{id}/renew")
|
||||
@PreAuthorize("hasAuthority('base:contract:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "合同续签")
|
||||
public Result<Map<String, Long>> renew(@PathVariable Long id, @Valid @RequestBody ContractRenewRequest request) {
|
||||
Long newId = contractService.renew(id, request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package com.pms.base.controller;
|
|||
import com.pms.base.dto.ContractTypeDTO;
|
||||
import com.pms.base.dto.ContractTypeSaveRequest;
|
||||
import com.pms.base.service.ContractTypeService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -30,6 +32,7 @@ public class ContractTypeController {
|
|||
* 合同类型列表
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:contract:manage')")
|
||||
public Result<List<ContractTypeDTO>> list() {
|
||||
return Result.success(contractTypeService.list(UserContext.getProjectId()));
|
||||
}
|
||||
|
|
@ -38,6 +41,8 @@ public class ContractTypeController {
|
|||
* 创建合同类型
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:contract:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建合同类型")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody ContractTypeSaveRequest request) {
|
||||
Long id = contractTypeService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -49,6 +54,8 @@ public class ContractTypeController {
|
|||
* 更新合同类型
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:contract:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新合同类型")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody ContractTypeSaveRequest request) {
|
||||
contractTypeService.update(id, request);
|
||||
return Result.success();
|
||||
|
|
@ -58,6 +65,8 @@ public class ContractTypeController {
|
|||
* 删除合同类型
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:contract:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除合同类型")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
contractTypeService.delete(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package com.pms.base.controller;
|
|||
import com.pms.base.dto.DeviceCategoryDTO;
|
||||
import com.pms.base.dto.DeviceCategorySaveRequest;
|
||||
import com.pms.base.service.DeviceCategoryService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -30,6 +32,7 @@ public class DeviceCategoryController {
|
|||
* 设备分类树
|
||||
*/
|
||||
@GetMapping("/tree")
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
public Result<List<DeviceCategoryDTO>> tree() {
|
||||
return Result.success(deviceCategoryService.tree(UserContext.getProjectId()));
|
||||
}
|
||||
|
|
@ -38,6 +41,8 @@ public class DeviceCategoryController {
|
|||
* 创建设备分类
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建设备分类")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody DeviceCategorySaveRequest request) {
|
||||
Long id = deviceCategoryService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -49,6 +54,8 @@ public class DeviceCategoryController {
|
|||
* 更新设备分类
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新设备分类")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody DeviceCategorySaveRequest request) {
|
||||
deviceCategoryService.update(id, request);
|
||||
return Result.success();
|
||||
|
|
@ -58,6 +65,8 @@ public class DeviceCategoryController {
|
|||
* 删除设备分类
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除设备分类")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
deviceCategoryService.delete(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ import com.pms.base.dto.DeviceDTO;
|
|||
import com.pms.base.dto.DeviceSaveRequest;
|
||||
import com.pms.base.dto.DeviceStatusRequest;
|
||||
import com.pms.base.service.DeviceService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -30,6 +32,7 @@ public class DeviceController {
|
|||
* 设备列表查询(分页)
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
public Result<PageResult<DeviceDTO>> list(DeviceDTO query) {
|
||||
return Result.success(deviceService.page(query));
|
||||
}
|
||||
|
|
@ -38,6 +41,8 @@ public class DeviceController {
|
|||
* 创建设备
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建设备")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody DeviceSaveRequest request) {
|
||||
Long id = deviceService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -49,6 +54,7 @@ public class DeviceController {
|
|||
* 设备详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
public Result<DeviceDTO> getById(@PathVariable Long id) {
|
||||
return Result.success(deviceService.getById(id));
|
||||
}
|
||||
|
|
@ -57,6 +63,8 @@ public class DeviceController {
|
|||
* 更新设备
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新设备")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody DeviceSaveRequest request) {
|
||||
deviceService.update(id, request);
|
||||
return Result.success();
|
||||
|
|
@ -66,6 +74,8 @@ public class DeviceController {
|
|||
* 删除设备
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除设备")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
deviceService.delete(id);
|
||||
return Result.success();
|
||||
|
|
@ -75,6 +85,8 @@ public class DeviceController {
|
|||
* 更新设备状态
|
||||
*/
|
||||
@PutMapping("/{id}/status")
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新设备状态")
|
||||
public Result<Void> updateStatus(@PathVariable Long id, @Valid @RequestBody DeviceStatusRequest request) {
|
||||
deviceService.updateStatus(id, request.getStatus());
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package com.pms.base.controller;
|
|||
import com.pms.base.dto.DeviceMaintenanceDTO;
|
||||
import com.pms.base.dto.DeviceMaintenanceSaveRequest;
|
||||
import com.pms.base.service.DeviceMaintenanceService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -30,6 +32,7 @@ public class DeviceMaintenanceController {
|
|||
* 维保记录列表查询(分页)
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
public Result<PageResult<DeviceMaintenanceDTO>> list(DeviceMaintenanceDTO query,
|
||||
@RequestParam(required = false) Long deviceId) {
|
||||
return Result.success(maintenanceService.page(query, deviceId));
|
||||
|
|
@ -39,6 +42,8 @@ public class DeviceMaintenanceController {
|
|||
* 创建维保记录
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建设备维保记录")
|
||||
public Result<Map<String, Long>> create(@RequestParam Long deviceId,
|
||||
@Valid @RequestBody DeviceMaintenanceSaveRequest request) {
|
||||
Long id = maintenanceService.create(deviceId, request);
|
||||
|
|
@ -51,6 +56,7 @@ public class DeviceMaintenanceController {
|
|||
* 维保详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
public Result<DeviceMaintenanceDTO> getById(@PathVariable Long id) {
|
||||
return Result.success(maintenanceService.getById(id));
|
||||
}
|
||||
|
|
@ -59,6 +65,7 @@ public class DeviceMaintenanceController {
|
|||
* 即将到期维保列表
|
||||
*/
|
||||
@GetMapping("/expiring")
|
||||
@PreAuthorize("hasAuthority('base:device:manage')")
|
||||
public Result<List<DeviceMaintenanceDTO>> expiring(@RequestParam(defaultValue = "30") int days) {
|
||||
return Result.success(maintenanceService.expiring(days));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ package com.pms.base.controller;
|
|||
|
||||
import com.pms.base.entity.EnergyMeter;
|
||||
import com.pms.base.service.EnergyMeterService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
|
@ -29,6 +32,7 @@ public class EnergyMeterController {
|
|||
* 表计列表
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:energy:manage')")
|
||||
public Result<List<EnergyMeter>> list() {
|
||||
return Result.success(energyMeterService.list(UserContext.getProjectId()));
|
||||
}
|
||||
|
|
@ -37,6 +41,7 @@ public class EnergyMeterController {
|
|||
* 表计详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:energy:manage')")
|
||||
public Result<EnergyMeter> getById(@PathVariable Long id) {
|
||||
return Result.success(energyMeterService.getById(id));
|
||||
}
|
||||
|
|
@ -45,7 +50,9 @@ public class EnergyMeterController {
|
|||
* 创建表计
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Map<String, Long>> create(@RequestBody EnergyMeter entity) {
|
||||
@PreAuthorize("hasAuthority('base:energy:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建能源表计")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody EnergyMeter entity) {
|
||||
Long id = energyMeterService.create(entity);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
|
|
@ -56,7 +63,9 @@ public class EnergyMeterController {
|
|||
* 更新表计
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody EnergyMeter entity) {
|
||||
@PreAuthorize("hasAuthority('base:energy:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新能源表计")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody EnergyMeter entity) {
|
||||
energyMeterService.update(id, entity);
|
||||
return Result.success();
|
||||
}
|
||||
|
|
@ -65,6 +74,8 @@ public class EnergyMeterController {
|
|||
* 删除表计
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:energy:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除能源表计")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
energyMeterService.delete(id);
|
||||
return Result.success();
|
||||
|
|
@ -74,6 +85,8 @@ public class EnergyMeterController {
|
|||
* 抄表记录(首期仅台账,预留 IoT 接入点 T4)
|
||||
*/
|
||||
@PostMapping("/{id}/reading")
|
||||
@PreAuthorize("hasAuthority('base:energy:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "能源表计抄表")
|
||||
public Result<Void> recordReading(@PathVariable Long id, @RequestParam BigDecimal newReading) {
|
||||
energyMeterService.recordReading(id, newReading);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package com.pms.base.controller;
|
|||
import com.pms.base.dto.EnterpriseDTO;
|
||||
import com.pms.base.dto.EnterpriseSaveRequest;
|
||||
import com.pms.base.service.EnterpriseService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -29,6 +31,7 @@ public class EnterpriseController {
|
|||
* 企业列表查询(分页)
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
public Result<PageResult<EnterpriseDTO>> list(EnterpriseDTO query) {
|
||||
return Result.success(enterpriseService.page(query));
|
||||
}
|
||||
|
|
@ -37,6 +40,8 @@ public class EnterpriseController {
|
|||
* 创建企业
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建企业", recordParams = false)
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody EnterpriseSaveRequest request) {
|
||||
Long id = enterpriseService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -48,6 +53,7 @@ public class EnterpriseController {
|
|||
* 企业详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
public Result<EnterpriseDTO> getById(@PathVariable Long id) {
|
||||
return Result.success(enterpriseService.getById(id));
|
||||
}
|
||||
|
|
@ -56,6 +62,8 @@ public class EnterpriseController {
|
|||
* 更新企业
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新企业", recordParams = false)
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody EnterpriseSaveRequest request) {
|
||||
enterpriseService.update(id, request);
|
||||
return Result.success();
|
||||
|
|
@ -65,6 +73,8 @@ public class EnterpriseController {
|
|||
* 删除企业
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除企业")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
enterpriseService.delete(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ package com.pms.base.controller;
|
|||
|
||||
import com.pms.base.entity.EnterpriseProfile;
|
||||
import com.pms.base.service.EnterpriseProfileService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -28,6 +31,7 @@ public class EnterpriseProfileController {
|
|||
* 企业画像列表
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
public Result<List<EnterpriseProfile>> list() {
|
||||
return Result.success(enterpriseProfileService.list(UserContext.getProjectId()));
|
||||
}
|
||||
|
|
@ -36,6 +40,7 @@ public class EnterpriseProfileController {
|
|||
* 企业画像详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
public Result<EnterpriseProfile> getById(@PathVariable Long id) {
|
||||
return Result.success(enterpriseProfileService.getById(id));
|
||||
}
|
||||
|
|
@ -44,7 +49,9 @@ public class EnterpriseProfileController {
|
|||
* 创建企业画像
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Map<String, Long>> create(@RequestBody EnterpriseProfile entity) {
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建企业画像", recordParams = false)
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody EnterpriseProfile entity) {
|
||||
Long id = enterpriseProfileService.create(entity);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
|
|
@ -55,7 +62,9 @@ public class EnterpriseProfileController {
|
|||
* 更新企业画像
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody EnterpriseProfile entity) {
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新企业画像", recordParams = false)
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody EnterpriseProfile entity) {
|
||||
enterpriseProfileService.update(id, entity);
|
||||
return Result.success();
|
||||
}
|
||||
|
|
@ -64,6 +73,8 @@ public class EnterpriseProfileController {
|
|||
* 删除企业画像
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除企业画像")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
enterpriseProfileService.delete(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ package com.pms.base.controller;
|
|||
|
||||
import com.pms.base.entity.EnterpriseService;
|
||||
import com.pms.base.service.EnterpriseServiceService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -28,6 +31,7 @@ public class EnterpriseServiceController {
|
|||
* 服务申请详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
public Result<EnterpriseService> getById(@PathVariable Long id) {
|
||||
return Result.success(enterpriseServiceService.getById(id));
|
||||
}
|
||||
|
|
@ -36,6 +40,7 @@ public class EnterpriseServiceController {
|
|||
* 当前项目下企业服务申请列表
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
public Result<List<EnterpriseService>> list() {
|
||||
return Result.success(enterpriseServiceService.listByProject(UserContext.getProjectId()));
|
||||
}
|
||||
|
|
@ -44,7 +49,9 @@ public class EnterpriseServiceController {
|
|||
* 提交服务申请
|
||||
*/
|
||||
@PostMapping("/apply")
|
||||
public Result<Map<String, Long>> apply(@RequestBody EnterpriseService service) {
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "提交企业服务申请", recordParams = false)
|
||||
public Result<Map<String, Long>> apply(@Valid @RequestBody EnterpriseService service) {
|
||||
service.setProjectId(UserContext.getProjectId());
|
||||
Long id = enterpriseServiceService.apply(service);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -56,6 +63,8 @@ public class EnterpriseServiceController {
|
|||
* 处理服务申请(标记为已完成)
|
||||
*/
|
||||
@PutMapping("/{id}/process")
|
||||
@PreAuthorize("hasAuthority('base:enterprise:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "处理企业服务申请", recordParams = false)
|
||||
public Result<Void> process(@PathVariable Long id,
|
||||
@RequestParam String handler,
|
||||
@RequestParam String result) {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package com.pms.base.controller;
|
|||
import com.pms.base.dto.FloorDTO;
|
||||
import com.pms.base.dto.FloorSaveRequest;
|
||||
import com.pms.base.service.FloorService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -30,6 +32,7 @@ public class FloorController {
|
|||
* 楼层列表查询
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:floor:manage')")
|
||||
public Result<List<FloorDTO>> list(@RequestParam(required = false) Long buildingId) {
|
||||
return Result.success(floorService.list(UserContext.getProjectId(), buildingId));
|
||||
}
|
||||
|
|
@ -38,6 +41,8 @@ public class FloorController {
|
|||
* 创建楼层
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:floor:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建楼层")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody FloorSaveRequest request) {
|
||||
Long id = floorService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -49,6 +54,8 @@ public class FloorController {
|
|||
* 更新楼层
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:floor:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新楼层")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody FloorSaveRequest request) {
|
||||
floorService.update(id, request);
|
||||
return Result.success();
|
||||
|
|
@ -58,6 +65,8 @@ public class FloorController {
|
|||
* 删除楼层
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:floor:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除楼层")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
floorService.delete(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ package com.pms.base.controller;
|
|||
|
||||
import com.pms.base.entity.LeaseContract;
|
||||
import com.pms.base.service.LeaseContractService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
|
@ -30,6 +33,7 @@ public class LeaseContractController {
|
|||
* 合同详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:lease:manage')")
|
||||
public Result<LeaseContract> getById(@PathVariable Long id) {
|
||||
return Result.success(leaseContractService.getById(id));
|
||||
}
|
||||
|
|
@ -38,6 +42,7 @@ public class LeaseContractController {
|
|||
* 当前项目下合同列表
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:lease:manage')")
|
||||
public Result<List<LeaseContract>> list() {
|
||||
return Result.success(leaseContractService.listByProject(UserContext.getProjectId()));
|
||||
}
|
||||
|
|
@ -46,7 +51,9 @@ public class LeaseContractController {
|
|||
* 创建租赁合同
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Map<String, Long>> create(@RequestBody LeaseContract contract) {
|
||||
@PreAuthorize("hasAuthority('base:lease:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建租赁合同")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody LeaseContract contract) {
|
||||
contract.setProjectId(UserContext.getProjectId());
|
||||
Long id = leaseContractService.create(contract);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -58,6 +65,8 @@ public class LeaseContractController {
|
|||
* 续约:延长合同结束日期
|
||||
*/
|
||||
@PutMapping("/{id}/renew")
|
||||
@PreAuthorize("hasAuthority('base:lease:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "租赁合同续约")
|
||||
public Result<Void> renew(@PathVariable Long id,
|
||||
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate newEndDate) {
|
||||
leaseContractService.renew(id, newEndDate);
|
||||
|
|
@ -68,6 +77,8 @@ public class LeaseContractController {
|
|||
* 退租
|
||||
*/
|
||||
@PutMapping("/{id}/terminate")
|
||||
@PreAuthorize("hasAuthority('base:lease:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "租赁合同退租")
|
||||
public Result<Void> terminate(@PathVariable Long id) {
|
||||
leaseContractService.terminate(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.pms.common.response.Result;
|
|||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -31,6 +32,7 @@ public class LifecycleController {
|
|||
* 查询当前生命周期阶段(R5)
|
||||
*/
|
||||
@GetMapping("/stage")
|
||||
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
|
||||
@AuditLog(module = "base", type = "QUERY", description = "查询生命周期阶段")
|
||||
public Result<LifecycleStageDTO> getStage(@PathVariable Long projectId) {
|
||||
return Result.success(lifecycleService.getStage(projectId));
|
||||
|
|
@ -40,6 +42,7 @@ public class LifecycleController {
|
|||
* 发起阶段流转
|
||||
*/
|
||||
@PostMapping("/transition")
|
||||
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "发起阶段流转")
|
||||
public Result<Map<String, Long>> transition(@PathVariable Long projectId,
|
||||
@Valid @RequestBody StageTransitionRequest request) {
|
||||
|
|
@ -53,6 +56,7 @@ public class LifecycleController {
|
|||
* 审批通过
|
||||
*/
|
||||
@PostMapping("/approve")
|
||||
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "审批通过")
|
||||
public Result<ApprovalCompletedEvent> approve(@PathVariable Long projectId,
|
||||
@Valid @RequestBody ApprovalActionRequest request) {
|
||||
|
|
@ -63,6 +67,7 @@ public class LifecycleController {
|
|||
* 审批退回
|
||||
*/
|
||||
@PostMapping("/reject")
|
||||
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "审批退回")
|
||||
public Result<Void> reject(@PathVariable Long projectId,
|
||||
@Valid @RequestBody ApprovalActionRequest request) {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ package com.pms.base.controller;
|
|||
|
||||
import com.pms.base.entity.MeetingRoom;
|
||||
import com.pms.base.service.MeetingRoomService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -28,6 +31,7 @@ public class MeetingRoomController {
|
|||
* 会议室详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:meeting:manage')")
|
||||
public Result<MeetingRoom> getById(@PathVariable Long id) {
|
||||
return Result.success(meetingRoomService.getById(id));
|
||||
}
|
||||
|
|
@ -36,6 +40,7 @@ public class MeetingRoomController {
|
|||
* 当前项目下会议室列表
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:meeting:manage')")
|
||||
public Result<List<MeetingRoom>> list() {
|
||||
return Result.success(meetingRoomService.listByProject(UserContext.getProjectId()));
|
||||
}
|
||||
|
|
@ -44,7 +49,9 @@ public class MeetingRoomController {
|
|||
* 创建会议室
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Map<String, Long>> create(@RequestBody MeetingRoom room) {
|
||||
@PreAuthorize("hasAuthority('base:meeting:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建会议室")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody MeetingRoom room) {
|
||||
room.setProjectId(UserContext.getProjectId());
|
||||
Long id = meetingRoomService.create(room);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -56,6 +63,8 @@ public class MeetingRoomController {
|
|||
* 预订会议室
|
||||
*/
|
||||
@PutMapping("/{id}/book")
|
||||
@PreAuthorize("hasAuthority('base:meeting:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "预订会议室")
|
||||
public Result<Void> book(@PathVariable Long id) {
|
||||
meetingRoomService.book(id);
|
||||
return Result.success();
|
||||
|
|
@ -65,6 +74,8 @@ public class MeetingRoomController {
|
|||
* 取消预订
|
||||
*/
|
||||
@PutMapping("/{id}/cancel")
|
||||
@PreAuthorize("hasAuthority('base:meeting:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "取消会议室预订")
|
||||
public Result<Void> cancelBooking(@PathVariable Long id) {
|
||||
meetingRoomService.cancelBooking(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ package com.pms.base.controller;
|
|||
|
||||
import com.pms.base.entity.OwnerCommittee;
|
||||
import com.pms.base.service.OwnerCommitteeService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -28,6 +31,7 @@ public class OwnerCommitteeController {
|
|||
* 分页查询业委会列表
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:committee:manage')")
|
||||
public Result<PageResult<OwnerCommittee>> page(
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
|
|
@ -39,6 +43,7 @@ public class OwnerCommitteeController {
|
|||
* 业委会详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:committee:manage')")
|
||||
public Result<OwnerCommittee> getById(@PathVariable Long id) {
|
||||
return Result.success(ownerCommitteeService.getById(id));
|
||||
}
|
||||
|
|
@ -47,7 +52,9 @@ public class OwnerCommitteeController {
|
|||
* 创建业委会
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Map<String, Long>> create(@RequestBody OwnerCommittee committee) {
|
||||
@PreAuthorize("hasAuthority('base:committee:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建业委会", recordParams = false)
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody OwnerCommittee committee) {
|
||||
Long id = ownerCommitteeService.create(committee);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
|
|
@ -58,7 +65,9 @@ public class OwnerCommitteeController {
|
|||
* 更新业委会
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody OwnerCommittee committee) {
|
||||
@PreAuthorize("hasAuthority('base:committee:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新业委会", recordParams = false)
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody OwnerCommittee committee) {
|
||||
ownerCommitteeService.update(id, committee);
|
||||
return Result.success();
|
||||
}
|
||||
|
|
@ -67,6 +76,8 @@ public class OwnerCommitteeController {
|
|||
* 删除业委会
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:committee:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除业委会")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
ownerCommitteeService.delete(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import com.pms.base.dto.OwnerRoomDTO;
|
|||
import com.pms.base.dto.OwnerSaveRequest;
|
||||
import com.pms.base.listener.OwnerImportListener;
|
||||
import com.pms.base.service.OwnerService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
|
|
@ -35,6 +37,7 @@ public class OwnerController {
|
|||
* 业主列表查询(分页)
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:owner:manage')")
|
||||
public Result<PageResult<OwnerDTO>> list(OwnerDTO query) {
|
||||
return Result.success(ownerService.page(query));
|
||||
}
|
||||
|
|
@ -43,6 +46,8 @@ public class OwnerController {
|
|||
* 创建业主
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:owner:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建业主", recordParams = false)
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody OwnerSaveRequest request) {
|
||||
Long id = ownerService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -54,6 +59,7 @@ public class OwnerController {
|
|||
* 业主详情(含房产信息)
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:owner:manage')")
|
||||
public Result<OwnerDTO> getById(@PathVariable Long id) {
|
||||
return Result.success(ownerService.getById(id));
|
||||
}
|
||||
|
|
@ -62,6 +68,8 @@ public class OwnerController {
|
|||
* 更新业主
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:owner:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新业主", recordParams = false)
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody OwnerSaveRequest request) {
|
||||
ownerService.update(id, request);
|
||||
return Result.success();
|
||||
|
|
@ -71,6 +79,8 @@ public class OwnerController {
|
|||
* 删除业主
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:owner:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除业主")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
ownerService.delete(id);
|
||||
return Result.success();
|
||||
|
|
@ -80,6 +90,7 @@ public class OwnerController {
|
|||
* 业主房产列表
|
||||
*/
|
||||
@GetMapping("/{id}/rooms")
|
||||
@PreAuthorize("hasAuthority('base:owner:manage')")
|
||||
public Result<List<OwnerRoomDTO>> getOwnerRooms(@PathVariable Long id) {
|
||||
return Result.success(ownerService.getOwnerRooms(id));
|
||||
}
|
||||
|
|
@ -88,6 +99,8 @@ public class OwnerController {
|
|||
* 批量导入业主(Excel上传)
|
||||
*/
|
||||
@PostMapping("/import")
|
||||
@PreAuthorize("hasAuthority('base:owner:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "导入业主", recordParams = false)
|
||||
public Result<Map<String, Object>> importOwners(@RequestParam("file") MultipartFile file) {
|
||||
if (file.isEmpty()) {
|
||||
return Result.error(400, "请选择文件");
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package com.pms.base.controller;
|
|||
import com.pms.base.dto.OwnerRoomDTO;
|
||||
import com.pms.base.dto.OwnerRoomRelRequest;
|
||||
import com.pms.base.service.OwnerRoomService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -30,6 +32,7 @@ public class OwnerRoomController {
|
|||
* 业主房间关联列表
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:owner:manage')")
|
||||
public Result<List<OwnerRoomDTO>> list(@RequestParam(required = false) Long ownerId,
|
||||
@RequestParam(required = false) Long roomId) {
|
||||
return Result.success(ownerRoomService.list(UserContext.getProjectId(), ownerId, roomId));
|
||||
|
|
@ -39,6 +42,8 @@ public class OwnerRoomController {
|
|||
* 创建业主房间关联
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:owner:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建业主房间关联", recordParams = false)
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody OwnerRoomRelRequest request) {
|
||||
Long id = ownerRoomService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -50,6 +55,8 @@ public class OwnerRoomController {
|
|||
* 删除业主房间关联
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:owner:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除业主房间关联")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
ownerRoomService.delete(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import com.pms.base.dto.ProjectInitRequest;
|
|||
import com.pms.base.dto.ProjectSaveRequest;
|
||||
import com.pms.base.service.ProjectInitService;
|
||||
import com.pms.base.service.ProjectService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -33,6 +35,7 @@ public class ProjectController {
|
|||
* 项目列表查询(分页)
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:project:manage')")
|
||||
public Result<PageResult<ProjectDTO>> list(ProjectDTO query) {
|
||||
return Result.success(projectService.page(query));
|
||||
}
|
||||
|
|
@ -41,6 +44,8 @@ public class ProjectController {
|
|||
* 创建项目
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:project:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建项目", recordParams = false)
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody ProjectSaveRequest request) {
|
||||
Long id = projectService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -52,6 +57,7 @@ public class ProjectController {
|
|||
* 获取全部项目(下拉选择用)
|
||||
*/
|
||||
@GetMapping("/all")
|
||||
@PreAuthorize("hasAuthority('base:project:manage')")
|
||||
public Result<List<ProjectDTO>> listAll() {
|
||||
return Result.success(projectService.listAll());
|
||||
}
|
||||
|
|
@ -60,6 +66,7 @@ public class ProjectController {
|
|||
* 项目详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:project:manage')")
|
||||
public Result<ProjectDTO> getById(@PathVariable Long id) {
|
||||
return Result.success(projectService.getById(id));
|
||||
}
|
||||
|
|
@ -68,6 +75,8 @@ public class ProjectController {
|
|||
* 更新项目
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:project:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新项目", recordParams = false)
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody ProjectSaveRequest request) {
|
||||
projectService.update(id, request);
|
||||
return Result.success();
|
||||
|
|
@ -77,6 +86,8 @@ public class ProjectController {
|
|||
* 删除项目
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:project:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除项目")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
projectService.delete(id);
|
||||
return Result.success();
|
||||
|
|
@ -87,6 +98,8 @@ public class ProjectController {
|
|||
* 为项目关联业态能力包,支持幂等重复调用
|
||||
*/
|
||||
@PostMapping("/{id}/init")
|
||||
@PreAuthorize("hasAuthority('base:project:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "初始化项目能力包")
|
||||
public Result<Void> initProject(@PathVariable Long id, @Valid @RequestBody ProjectInitRequest request) {
|
||||
projectInitService.initProject(id, request.getPackageCodes());
|
||||
return Result.success(null);
|
||||
|
|
|
|||
|
|
@ -2,14 +2,16 @@ package com.pms.base.controller;
|
|||
|
||||
import com.pms.base.entity.PublicRevenue;
|
||||
import com.pms.base.service.PublicRevenueService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
|
@ -29,6 +31,7 @@ public class PublicRevenueController {
|
|||
* 分页查询公共收益
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:revenue:manage')")
|
||||
public Result<PageResult<PublicRevenue>> page(
|
||||
@RequestParam(required = false) String period,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
|
|
@ -40,6 +43,7 @@ public class PublicRevenueController {
|
|||
* 公共收益详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:revenue:manage')")
|
||||
public Result<PublicRevenue> getById(@PathVariable Long id) {
|
||||
return Result.success(publicRevenueService.getById(id));
|
||||
}
|
||||
|
|
@ -48,7 +52,9 @@ public class PublicRevenueController {
|
|||
* 录入公共收益
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Map<String, Long>> create(@RequestBody PublicRevenue revenue) {
|
||||
@PreAuthorize("hasAuthority('base:revenue:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "录入公共收益")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody PublicRevenue revenue) {
|
||||
Long id = publicRevenueService.create(revenue);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
|
|
@ -59,7 +65,9 @@ public class PublicRevenueController {
|
|||
* 更新公共收益
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody PublicRevenue revenue) {
|
||||
@PreAuthorize("hasAuthority('base:revenue:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新公共收益")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody PublicRevenue revenue) {
|
||||
publicRevenueService.update(id, revenue);
|
||||
return Result.success();
|
||||
}
|
||||
|
|
@ -70,8 +78,9 @@ public class PublicRevenueController {
|
|||
* @param period 周期,如 2025-06
|
||||
*/
|
||||
@GetMapping("/summary")
|
||||
@PreAuthorize("hasAuthority('base:revenue:manage')")
|
||||
public Result<Map<String, Object>> monthlySummary(@RequestParam String period) {
|
||||
BigDecimal total = publicRevenueService.monthlySummary(UserContext.getProjectId(), period);
|
||||
Long total = publicRevenueService.monthlySummary(UserContext.getProjectId(), period);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("period", period);
|
||||
data.put("totalAmount", total);
|
||||
|
|
@ -82,6 +91,8 @@ public class PublicRevenueController {
|
|||
* 删除公共收益
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:revenue:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除公共收益")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
publicRevenueService.delete(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ package com.pms.base.controller;
|
|||
|
||||
import com.pms.base.entity.Renovation;
|
||||
import com.pms.base.service.RenovationService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -28,6 +31,7 @@ public class RenovationController {
|
|||
* 分页查询装修记录
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:renovation:manage')")
|
||||
public Result<PageResult<Renovation>> page(
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
|
|
@ -39,6 +43,7 @@ public class RenovationController {
|
|||
* 装修记录详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:renovation:manage')")
|
||||
public Result<Renovation> getById(@PathVariable Long id) {
|
||||
return Result.success(renovationService.getById(id));
|
||||
}
|
||||
|
|
@ -47,7 +52,9 @@ public class RenovationController {
|
|||
* 申请装修
|
||||
*/
|
||||
@PostMapping("/apply")
|
||||
public Result<Map<String, Long>> apply(@RequestBody Renovation renovation) {
|
||||
@PreAuthorize("hasAuthority('base:renovation:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "申请装修", recordParams = false)
|
||||
public Result<Map<String, Long>> apply(@Valid @RequestBody Renovation renovation) {
|
||||
Long id = renovationService.applyRenovation(renovation);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
|
|
@ -63,6 +70,8 @@ public class RenovationController {
|
|||
* @param comment 审批意见
|
||||
*/
|
||||
@PostMapping("/{id}/approve")
|
||||
@PreAuthorize("hasAuthority('base:renovation:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "审批装修申请")
|
||||
public Result<Void> approve(@PathVariable Long id,
|
||||
@RequestParam boolean approved,
|
||||
@RequestParam(required = false) String permitType,
|
||||
|
|
@ -75,6 +84,8 @@ public class RenovationController {
|
|||
* 开始施工
|
||||
*/
|
||||
@PostMapping("/{id}/start")
|
||||
@PreAuthorize("hasAuthority('base:renovation:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "开始施工")
|
||||
public Result<Void> startConstruction(@PathVariable Long id) {
|
||||
renovationService.startConstruction(id);
|
||||
return Result.success();
|
||||
|
|
@ -84,6 +95,8 @@ public class RenovationController {
|
|||
* 装修完成
|
||||
*/
|
||||
@PostMapping("/{id}/complete")
|
||||
@PreAuthorize("hasAuthority('base:renovation:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "装修完成")
|
||||
public Result<Void> complete(@PathVariable Long id) {
|
||||
renovationService.complete(id);
|
||||
return Result.success();
|
||||
|
|
@ -93,6 +106,8 @@ public class RenovationController {
|
|||
* 删除装修记录
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:renovation:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除装修记录")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
renovationService.delete(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.pms.base.dto.RoomImportData;
|
|||
import com.pms.base.dto.RoomSaveRequest;
|
||||
import com.pms.base.listener.RoomImportListener;
|
||||
import com.pms.base.service.RoomService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
|
|
@ -13,6 +14,7 @@ import com.alibaba.excel.EasyExcel;
|
|||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
|
|
@ -36,6 +38,7 @@ public class RoomController {
|
|||
* 房间列表查询(分页,支持多条件筛选)
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:room:manage')")
|
||||
public Result<PageResult<RoomDTO>> list(RoomDTO query) {
|
||||
return Result.success(roomService.page(query));
|
||||
}
|
||||
|
|
@ -44,6 +47,8 @@ public class RoomController {
|
|||
* 创建房间
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:room:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建房间")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody RoomSaveRequest request) {
|
||||
Long id = roomService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -55,6 +60,8 @@ public class RoomController {
|
|||
* 批量创建房间
|
||||
*/
|
||||
@PostMapping("/batch")
|
||||
@PreAuthorize("hasAuthority('base:room:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "批量创建房间")
|
||||
public Result<Map<String, Object>> batchCreate(@Valid @RequestBody List<RoomSaveRequest> rooms) {
|
||||
List<Long> ids = roomService.batchCreate(rooms);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
|
|
@ -67,6 +74,7 @@ public class RoomController {
|
|||
* 房间详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:room:manage')")
|
||||
public Result<RoomDTO> getById(@PathVariable Long id) {
|
||||
return Result.success(roomService.getById(id));
|
||||
}
|
||||
|
|
@ -75,6 +83,8 @@ public class RoomController {
|
|||
* 更新房间
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:room:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新房间")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody RoomSaveRequest request) {
|
||||
roomService.update(id, request);
|
||||
return Result.success();
|
||||
|
|
@ -84,6 +94,8 @@ public class RoomController {
|
|||
* 删除房间
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:room:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除房间")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
roomService.delete(id);
|
||||
return Result.success();
|
||||
|
|
@ -93,6 +105,7 @@ public class RoomController {
|
|||
* 房间树(项目→楼栋→楼层→房间)
|
||||
*/
|
||||
@GetMapping("/tree")
|
||||
@PreAuthorize("hasAuthority('base:room:manage')")
|
||||
public Result<List<BuildingTreeDTO>> tree() {
|
||||
return Result.success(roomService.tree(UserContext.getProjectId()));
|
||||
}
|
||||
|
|
@ -101,6 +114,8 @@ public class RoomController {
|
|||
* 批量导入房间(Excel上传)
|
||||
*/
|
||||
@PostMapping("/import")
|
||||
@PreAuthorize("hasAuthority('base:room:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "导入房间")
|
||||
public Result<Map<String, Object>> importRooms(@RequestParam("file") MultipartFile file) {
|
||||
if (file.isEmpty()) {
|
||||
return Result.error(400, "请选择文件");
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ package com.pms.base.controller;
|
|||
|
||||
import com.pms.base.entity.SafetyInspection;
|
||||
import com.pms.base.service.SafetyInspectionService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -28,6 +31,7 @@ public class SafetyInspectionController {
|
|||
* 安全检查列表
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:inspection:manage')")
|
||||
public Result<List<SafetyInspection>> list() {
|
||||
return Result.success(safetyInspectionService.list(UserContext.getProjectId()));
|
||||
}
|
||||
|
|
@ -36,6 +40,7 @@ public class SafetyInspectionController {
|
|||
* 安全检查详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:inspection:manage')")
|
||||
public Result<SafetyInspection> getById(@PathVariable Long id) {
|
||||
return Result.success(safetyInspectionService.getById(id));
|
||||
}
|
||||
|
|
@ -44,7 +49,9 @@ public class SafetyInspectionController {
|
|||
* 创建安全检查计划
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Map<String, Long>> create(@RequestBody SafetyInspection entity) {
|
||||
@PreAuthorize("hasAuthority('base:inspection:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建安全检查计划")
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody SafetyInspection entity) {
|
||||
Long id = safetyInspectionService.create(entity);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
|
|
@ -55,7 +62,9 @@ public class SafetyInspectionController {
|
|||
* 更新安全检查
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody SafetyInspection entity) {
|
||||
@PreAuthorize("hasAuthority('base:inspection:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新安全检查")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody SafetyInspection entity) {
|
||||
safetyInspectionService.update(id, entity);
|
||||
return Result.success();
|
||||
}
|
||||
|
|
@ -64,6 +73,8 @@ public class SafetyInspectionController {
|
|||
* 删除安全检查
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:inspection:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除安全检查")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
safetyInspectionService.delete(id);
|
||||
return Result.success();
|
||||
|
|
@ -73,6 +84,8 @@ public class SafetyInspectionController {
|
|||
* 执行安全检查(计划中 -> 执行中)
|
||||
*/
|
||||
@PostMapping("/{id}/execute")
|
||||
@PreAuthorize("hasAuthority('base:inspection:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "执行安全检查")
|
||||
public Result<Void> execute(@PathVariable Long id) {
|
||||
safetyInspectionService.execute(id);
|
||||
return Result.success();
|
||||
|
|
@ -82,6 +95,8 @@ public class SafetyInspectionController {
|
|||
* 上报发现(执行中 -> 已上报,闭环)
|
||||
*/
|
||||
@PostMapping("/{id}/report")
|
||||
@PreAuthorize("hasAuthority('base:inspection:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "上报安全检查发现")
|
||||
public Result<Void> reportFindings(@PathVariable Long id,
|
||||
@RequestParam String result,
|
||||
@RequestParam(required = false) String finding) {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package com.pms.base.controller;
|
|||
import com.pms.base.dto.TenantDTO;
|
||||
import com.pms.base.dto.TenantSaveRequest;
|
||||
import com.pms.base.service.TenantService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.response.Result;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -29,6 +31,7 @@ public class TenantController {
|
|||
* 租户列表查询(分页)
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:tenant:manage')")
|
||||
public Result<PageResult<TenantDTO>> list(TenantDTO query) {
|
||||
return Result.success(tenantService.page(query));
|
||||
}
|
||||
|
|
@ -37,6 +40,8 @@ public class TenantController {
|
|||
* 创建租户
|
||||
*/
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('base:tenant:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建租户", recordParams = false)
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody TenantSaveRequest request) {
|
||||
Long id = tenantService.create(request);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
|
|
@ -48,6 +53,8 @@ public class TenantController {
|
|||
* 更新租户
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:tenant:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新租户", recordParams = false)
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody TenantSaveRequest request) {
|
||||
tenantService.update(id, request);
|
||||
return Result.success();
|
||||
|
|
@ -57,6 +64,8 @@ public class TenantController {
|
|||
* 删除租户
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:tenant:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除租户")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
tenantService.delete(id);
|
||||
return Result.success();
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ package com.pms.base.controller;
|
|||
|
||||
import com.pms.base.entity.WorkshopLease;
|
||||
import com.pms.base.service.WorkshopLeaseService;
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import com.pms.common.response.Result;
|
||||
import com.pms.common.security.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
|
@ -28,6 +31,7 @@ public class WorkshopLeaseController {
|
|||
* 厂房租赁列表
|
||||
*/
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('base:workshop:manage')")
|
||||
public Result<List<WorkshopLease>> list() {
|
||||
return Result.success(workshopLeaseService.list(UserContext.getProjectId()));
|
||||
}
|
||||
|
|
@ -36,6 +40,7 @@ public class WorkshopLeaseController {
|
|||
* 厂房租赁详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:workshop:manage')")
|
||||
public Result<WorkshopLease> getById(@PathVariable Long id) {
|
||||
return Result.success(workshopLeaseService.getById(id));
|
||||
}
|
||||
|
|
@ -44,7 +49,9 @@ public class WorkshopLeaseController {
|
|||
* 创建厂房租赁
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Map<String, Long>> create(@RequestBody WorkshopLease entity) {
|
||||
@PreAuthorize("hasAuthority('base:workshop:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "创建厂房租赁", recordParams = false)
|
||||
public Result<Map<String, Long>> create(@Valid @RequestBody WorkshopLease entity) {
|
||||
Long id = workshopLeaseService.create(entity);
|
||||
Map<String, Long> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
|
|
@ -55,7 +62,9 @@ public class WorkshopLeaseController {
|
|||
* 更新厂房租赁
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> update(@PathVariable Long id, @RequestBody WorkshopLease entity) {
|
||||
@PreAuthorize("hasAuthority('base:workshop:manage')")
|
||||
@AuditLog(module = "base", type = "UPDATE", description = "更新厂房租赁", recordParams = false)
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody WorkshopLease entity) {
|
||||
workshopLeaseService.update(id, entity);
|
||||
return Result.success();
|
||||
}
|
||||
|
|
@ -64,6 +73,8 @@ public class WorkshopLeaseController {
|
|||
* 删除厂房租赁
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('base:workshop:manage')")
|
||||
@AuditLog(module = "base", type = "DELETE", description = "删除厂房租赁")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
workshopLeaseService.delete(id);
|
||||
return Result.success();
|
||||
|
|
@ -73,6 +84,8 @@ public class WorkshopLeaseController {
|
|||
* 生成租金账单
|
||||
*/
|
||||
@PostMapping("/{id}/rent-bill")
|
||||
@PreAuthorize("hasAuthority('base:workshop:manage')")
|
||||
@AuditLog(module = "base", type = "CREATE", description = "生成租金账单")
|
||||
public Result<Map<String, String>> generateRentBill(@PathVariable Long id) {
|
||||
String billNo = workshopLeaseService.generateRentBill(id);
|
||||
Map<String, String> data = new HashMap<>();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import lombok.Data;
|
|||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -30,8 +29,8 @@ public class ContractDTO extends PageQuery implements Serializable {
|
|||
private LocalDate startDate;
|
||||
private LocalDate endDate;
|
||||
private LocalDate signDate;
|
||||
private BigDecimal amount;
|
||||
private BigDecimal deposit;
|
||||
private Long amountFen;
|
||||
private Long depositFen;
|
||||
private String paymentCycle;
|
||||
private String attachment;
|
||||
private Integer status;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.pms.base.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
|
|
@ -16,6 +16,7 @@ public class ContractRenewRequest implements Serializable {
|
|||
@NotNull(message = "新结束日期不能为空")
|
||||
private LocalDate newEndDate;
|
||||
|
||||
private BigDecimal amount;
|
||||
@PositiveOrZero(message = "合同金额必须大于等于0")
|
||||
private Long amountFen;
|
||||
private LocalDate signDate;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ package com.pms.base.dto;
|
|||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -42,8 +42,13 @@ public class ContractSaveRequest implements Serializable {
|
|||
private LocalDate endDate;
|
||||
|
||||
private LocalDate signDate;
|
||||
private BigDecimal amount;
|
||||
private BigDecimal deposit;
|
||||
|
||||
@PositiveOrZero(message = "合同金额必须大于等于0")
|
||||
private Long amountFen;
|
||||
|
||||
@PositiveOrZero(message = "押金必须大于等于0")
|
||||
private Long depositFen;
|
||||
|
||||
private String paymentCycle;
|
||||
private String attachment;
|
||||
private Integer status;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import lombok.Data;
|
|||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
|
|
@ -34,7 +33,7 @@ public class DeviceDTO extends PageQuery implements Serializable {
|
|||
private LocalDate installDate;
|
||||
private LocalDate warrantyUntil;
|
||||
private String supplier;
|
||||
private BigDecimal price;
|
||||
private Long priceFen;
|
||||
private String qrCode;
|
||||
private Integer status;
|
||||
private String remark;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import lombok.Data;
|
|||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -23,7 +22,7 @@ public class DeviceMaintenanceDTO extends PageQuery implements Serializable {
|
|||
private LocalDate maintenanceDate;
|
||||
private LocalDate planDate;
|
||||
private String handler;
|
||||
private BigDecimal cost;
|
||||
private Long costFen;
|
||||
private String result;
|
||||
private String images;
|
||||
private List<String> imageList;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.pms.base.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -22,7 +22,10 @@ public class DeviceMaintenanceSaveRequest implements Serializable {
|
|||
|
||||
private LocalDate planDate;
|
||||
private String handler;
|
||||
private BigDecimal cost;
|
||||
|
||||
@PositiveOrZero(message = "维保费用必须大于等于0")
|
||||
private Long costFen;
|
||||
|
||||
private String result;
|
||||
private List<String> images;
|
||||
private Integer status;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ package com.pms.base.dto;
|
|||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
|
|
@ -36,7 +36,10 @@ public class DeviceSaveRequest implements Serializable {
|
|||
private LocalDate installDate;
|
||||
private LocalDate warrantyUntil;
|
||||
private String supplier;
|
||||
private BigDecimal price;
|
||||
|
||||
@PositiveOrZero(message = "设备价格必须大于等于0")
|
||||
private Long priceFen;
|
||||
|
||||
private Integer status;
|
||||
private String remark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package com.pms.base.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
|
@ -12,16 +15,35 @@ import java.io.Serializable;
|
|||
public class EnterpriseSaveRequest implements Serializable {
|
||||
|
||||
@NotBlank(message = "企业名称不能为空")
|
||||
@Size(max = 128, message = "企业名称长度不能超过128")
|
||||
private String enterpriseName;
|
||||
|
||||
@Size(max = 32, message = "统一社会信用代码长度不能超过32")
|
||||
private String creditCode;
|
||||
|
||||
@Size(max = 32, message = "法人姓名长度不能超过32")
|
||||
private String legalPerson;
|
||||
|
||||
@Size(max = 32, message = "联系人长度不能超过32")
|
||||
private String contactPerson;
|
||||
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "联系电话格式不正确")
|
||||
private String contactPhone;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Size(max = 128, message = "邮箱长度不能超过128")
|
||||
private String email;
|
||||
|
||||
@Size(max = 255, message = "地址长度不能超过255")
|
||||
private String address;
|
||||
|
||||
@Size(max = 255, message = "经营范围长度不能超过255")
|
||||
private String businessScope;
|
||||
|
||||
@Size(max = 500, message = "承租房间ID列表长度不能超过500")
|
||||
private String roomIds;
|
||||
private Integer status;
|
||||
|
||||
@Size(max = 255, message = "备注长度不能超过255")
|
||||
private String remark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package com.pms.base.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
|
@ -11,20 +14,38 @@ import java.io.Serializable;
|
|||
@Data
|
||||
public class OwnerSaveRequest implements Serializable {
|
||||
|
||||
@Size(max = 32, message = "业主编码长度不能超过32")
|
||||
private String ownerCode;
|
||||
|
||||
@NotBlank(message = "业主姓名不能为空")
|
||||
@Size(max = 32, message = "业主姓名长度不能超过32")
|
||||
private String ownerName;
|
||||
|
||||
private Integer gender;
|
||||
private Integer idType;
|
||||
|
||||
@Pattern(regexp = "^\\d{17}[\\dXx]$", message = "身份证号格式不正确")
|
||||
private String idNo;
|
||||
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Size(max = 128, message = "邮箱长度不能超过128")
|
||||
private String email;
|
||||
|
||||
@Size(max = 255, message = "地址长度不能超过255")
|
||||
private String address;
|
||||
|
||||
@Size(max = 32, message = "紧急联系人长度不能超过32")
|
||||
private String emergencyContact;
|
||||
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "紧急联系电话格式不正确")
|
||||
private String emergencyPhone;
|
||||
|
||||
private Integer status;
|
||||
|
||||
@Size(max = 255, message = "备注长度不能超过255")
|
||||
private String remark;
|
||||
|
||||
/** 项目ID(管理员无项目上下文时由前端传入) */
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.pms.base.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
|
@ -13,24 +15,43 @@ import java.math.BigDecimal;
|
|||
public class ProjectSaveRequest implements Serializable {
|
||||
|
||||
@NotBlank(message = "项目编码不能为空")
|
||||
@Size(max = 32, message = "项目编码长度不能超过32")
|
||||
private String projectCode;
|
||||
|
||||
@NotBlank(message = "项目名称不能为空")
|
||||
@Size(max = 128, message = "项目名称长度不能超过128")
|
||||
private String projectName;
|
||||
|
||||
@Size(max = 64, message = "项目简称长度不能超过64")
|
||||
private String shortName;
|
||||
|
||||
@Size(max = 32, message = "省份长度不能超过32")
|
||||
private String province;
|
||||
|
||||
@Size(max = 32, message = "城市长度不能超过32")
|
||||
private String city;
|
||||
|
||||
@Size(max = 32, message = "区县长度不能超过32")
|
||||
private String district;
|
||||
|
||||
@Size(max = 255, message = "地址长度不能超过255")
|
||||
private String address;
|
||||
|
||||
private BigDecimal longitude;
|
||||
private BigDecimal latitude;
|
||||
private BigDecimal coverArea;
|
||||
private BigDecimal buildingArea;
|
||||
private BigDecimal greenRate;
|
||||
|
||||
@Size(max = 128, message = "物业公司长度不能超过128")
|
||||
private String propertyCompany;
|
||||
|
||||
@Size(max = 32, message = "联系人长度不能超过32")
|
||||
private String contactPerson;
|
||||
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "联系电话格式不正确")
|
||||
private String contactPhone;
|
||||
|
||||
private String logo;
|
||||
private Integer status;
|
||||
private Integer sort;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package com.pms.base.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
|
@ -12,14 +15,27 @@ import java.io.Serializable;
|
|||
public class TenantSaveRequest implements Serializable {
|
||||
|
||||
@NotBlank(message = "租户名称不能为空")
|
||||
@Size(max = 64, message = "租户名称长度不能超过64")
|
||||
private String tenantName;
|
||||
|
||||
private Integer tenantType;
|
||||
private Integer idType;
|
||||
|
||||
@Pattern(regexp = "^\\d{17}[\\dXx]$", message = "身份证号格式不正确")
|
||||
private String idNo;
|
||||
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Size(max = 128, message = "邮箱长度不能超过128")
|
||||
private String email;
|
||||
|
||||
@Size(max = 255, message = "地址长度不能超过255")
|
||||
private String address;
|
||||
|
||||
private Integer status;
|
||||
|
||||
@Size(max = 255, message = "备注长度不能超过255")
|
||||
private String remark;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.pms.base.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
|
|
@ -22,11 +23,13 @@ public class CamCharge extends BaseEntity {
|
|||
/** 分摊周期(如 2024-01) */
|
||||
private String chargePeriod;
|
||||
|
||||
/** 本期公共费用总额 */
|
||||
private BigDecimal totalAmount;
|
||||
/** 本期公共费用总额(分) */
|
||||
@TableField("total_amount")
|
||||
private Long totalAmountFen;
|
||||
|
||||
/** 分摊给该合同的金额 */
|
||||
private BigDecimal allocatedAmount;
|
||||
/** 分摊给该合同的金额(分) */
|
||||
@TableField("allocated_amount")
|
||||
private Long allocatedAmountFen;
|
||||
|
||||
/** 分摊比例(面积占比,0~1) */
|
||||
private BigDecimal ratio;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package com.pms.base.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
|
|
@ -49,11 +49,13 @@ public class Contract extends BaseEntity {
|
|||
/** 签订日期 */
|
||||
private LocalDate signDate;
|
||||
|
||||
/** 合同金额 */
|
||||
private BigDecimal amount;
|
||||
/** 合同金额(分) */
|
||||
@TableField("amount")
|
||||
private Long amountFen;
|
||||
|
||||
/** 押金 */
|
||||
private BigDecimal deposit;
|
||||
/** 押金(分) */
|
||||
@TableField("deposit")
|
||||
private Long depositFen;
|
||||
|
||||
/** 付款周期,如月付 */
|
||||
private String paymentCycle;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package com.pms.base.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
|
|
@ -61,8 +61,9 @@ public class Device extends BaseEntity {
|
|||
/** 供应商 */
|
||||
private String supplier;
|
||||
|
||||
/** 采购价格 */
|
||||
private BigDecimal price;
|
||||
/** 采购价格(分) */
|
||||
@TableField("price")
|
||||
private Long priceFen;
|
||||
|
||||
/** 设备二维码URL */
|
||||
private String qrCode;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package com.pms.base.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
|
|
@ -31,8 +31,9 @@ public class DeviceMaintenance extends BaseEntity {
|
|||
/** 处理人 */
|
||||
private String handler;
|
||||
|
||||
/** 维保费用 */
|
||||
private BigDecimal cost;
|
||||
/** 维保费用(分) */
|
||||
@TableField("cost")
|
||||
private Long costFen;
|
||||
|
||||
/** 维保结果描述 */
|
||||
private String result;
|
||||
|
|
|
|||
|
|
@ -25,12 +25,24 @@ public class Enterprise extends BaseEntity {
|
|||
/** 联系人 */
|
||||
private String contactPerson;
|
||||
|
||||
/** 联系电话 */
|
||||
/** 联系电话(AES-256-GCM加密存储) */
|
||||
private String contactPhone;
|
||||
|
||||
/** 邮箱 */
|
||||
/** 联系电话哈希 */
|
||||
private String contactPhoneHash;
|
||||
|
||||
/** 联系电话后4位哈希 */
|
||||
private String contactPhoneLast4Hash;
|
||||
|
||||
/** 邮箱(AES-256-GCM加密存储) */
|
||||
private String email;
|
||||
|
||||
/** 邮箱哈希 */
|
||||
private String emailHash;
|
||||
|
||||
/** 邮箱后4位哈希 */
|
||||
private String emailLast4Hash;
|
||||
|
||||
/** 企业地址 */
|
||||
private String address;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.pms.base.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
|
|
@ -41,11 +42,13 @@ public class LeaseContract extends BaseEntity {
|
|||
/** 租赁结束日期 */
|
||||
private LocalDate leaseEnd;
|
||||
|
||||
/** 月租金 */
|
||||
private BigDecimal rentAmount;
|
||||
/** 月租金(分) */
|
||||
@TableField("rent_amount")
|
||||
private Long rentAmountFen;
|
||||
|
||||
/** 押金 */
|
||||
private BigDecimal deposit;
|
||||
/** 押金(分) */
|
||||
@TableField("deposit")
|
||||
private Long depositFen;
|
||||
|
||||
/** 状态:0草稿 1生效 2到期 3退租 4续签 */
|
||||
private Integer status;
|
||||
|
|
|
|||
|
|
@ -32,21 +32,36 @@ public class Owner extends BaseEntity {
|
|||
/** 证件号码哈希(用于查询匹配) */
|
||||
private String idNoHash;
|
||||
|
||||
/** 手机号 */
|
||||
/** 手机号(AES-256-GCM加密存储) */
|
||||
private String phone;
|
||||
|
||||
/** 邮箱 */
|
||||
/** 手机号哈希(用于查询匹配) */
|
||||
private String phoneHash;
|
||||
|
||||
/** 手机号后4位哈希(用于尾号查询) */
|
||||
private String phoneLast4Hash;
|
||||
|
||||
/** 邮箱(AES-256-GCM加密存储) */
|
||||
private String email;
|
||||
|
||||
/** 邮箱哈希 */
|
||||
private String emailHash;
|
||||
|
||||
/** 邮箱后4位哈希 */
|
||||
private String emailLast4Hash;
|
||||
|
||||
/** 联系地址 */
|
||||
private String address;
|
||||
|
||||
/** 紧急联系人 */
|
||||
private String emergencyContact;
|
||||
|
||||
/** 紧急联系电话 */
|
||||
/** 紧急联系电话(AES-256-GCM加密存储) */
|
||||
private String emergencyPhone;
|
||||
|
||||
/** 紧急联系电话哈希 */
|
||||
private String emergencyPhoneHash;
|
||||
|
||||
/** 状态:0禁用 1启用 */
|
||||
private Integer status;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
package com.pms.base.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 公共收益实体
|
||||
*/
|
||||
|
|
@ -18,8 +17,9 @@ public class PublicRevenue extends BaseEntity {
|
|||
/** 收益类型,如广告位/场地租赁 */
|
||||
private String revenueType;
|
||||
|
||||
/** 收益金额 */
|
||||
private BigDecimal amount;
|
||||
/** 收益金额(分) */
|
||||
@TableField("amount")
|
||||
private Long amountFen;
|
||||
|
||||
/** 所属周期,如 2025-06 */
|
||||
private String period;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package com.pms.base.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
|
|
@ -40,8 +40,9 @@ public class Renovation extends BaseEntity {
|
|||
/** 装修结束日期 */
|
||||
private LocalDate endDate;
|
||||
|
||||
/** 装修押金 */
|
||||
private BigDecimal depositAmount;
|
||||
/** 装修押金(分) */
|
||||
@TableField("deposit_amount")
|
||||
private Long depositAmountFen;
|
||||
|
||||
/** 施工单位 */
|
||||
private String constructionCompany;
|
||||
|
|
|
|||
|
|
@ -29,12 +29,24 @@ public class Tenant extends BaseEntity {
|
|||
/** 证件号码哈希 */
|
||||
private String idNoHash;
|
||||
|
||||
/** 手机号 */
|
||||
/** 手机号(AES-256-GCM加密存储) */
|
||||
private String phone;
|
||||
|
||||
/** 邮箱 */
|
||||
/** 手机号哈希 */
|
||||
private String phoneHash;
|
||||
|
||||
/** 手机号后4位哈希 */
|
||||
private String phoneLast4Hash;
|
||||
|
||||
/** 邮箱(AES-256-GCM加密存储) */
|
||||
private String email;
|
||||
|
||||
/** 邮箱哈希 */
|
||||
private String emailHash;
|
||||
|
||||
/** 邮箱后4位哈希 */
|
||||
private String emailLast4Hash;
|
||||
|
||||
/** 联系地址 */
|
||||
private String address;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.pms.base.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import com.pms.common.entity.BaseEntity;
|
||||
|
|
@ -32,8 +33,9 @@ public class WorkshopLease extends BaseEntity {
|
|||
/** 租期结束 */
|
||||
private LocalDate leaseEnd;
|
||||
|
||||
/** 租金金额 */
|
||||
private BigDecimal rentAmount;
|
||||
/** 租金金额(分) */
|
||||
@TableField("rent_amount")
|
||||
private Long rentAmountFen;
|
||||
|
||||
/** 状态:0草稿 1生效 2到期 3终止 */
|
||||
private Integer status;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.pms.base.feign;
|
||||
|
||||
import com.pms.common.dto.internal.InternalServiceDTOs.UserDTO;
|
||||
import com.pms.common.exception.ServiceCallException;
|
||||
import com.pms.common.response.Result;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.openfeign.FallbackFactory;
|
||||
|
|
@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.PathVariable;
|
|||
* auth-service 用户接口 Feign 客户端
|
||||
* <p>
|
||||
* 供 base-service 获取操作人上下文信息。
|
||||
* 降级策略:抛 ServiceCallException 阻止静默返回 null 导致后续 NPE。
|
||||
*/
|
||||
@FeignClient(name = "auth-service", contextId = "baseAuthClient",
|
||||
fallbackFactory = AuthClient.AuthClientFallbackFactory.class)
|
||||
|
|
@ -25,7 +27,7 @@ public interface AuthClient {
|
|||
Result<UserDTO> getUser(@PathVariable("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 降级工厂:查询类返回空结果
|
||||
* 降级工厂:auth-service 不可用时抛异常,不静默返回 null
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
|
|
@ -35,8 +37,7 @@ public interface AuthClient {
|
|||
public AuthClient create(Throwable cause) {
|
||||
log.error("auth-service 用户接口调用失败,触发降级", cause);
|
||||
return userId -> {
|
||||
log.warn("getUser 降级,userId={}", userId);
|
||||
return Result.success(null);
|
||||
throw new ServiceCallException("认证服务暂不可用,无法获取用户信息", cause);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.pms.base.service;
|
|||
|
||||
import com.pms.base.entity.CamCharge;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
@ -19,7 +18,7 @@ public interface CamChargeService {
|
|||
* @param chargePeriod 分摊周期(如 2024-01)
|
||||
* @return 生成的分摊明细列表
|
||||
*/
|
||||
List<CamCharge> allocateCamCharge(Long projectId, BigDecimal totalAmount, String chargePeriod);
|
||||
List<CamCharge> allocateCamCharge(Long projectId, Long totalAmountFen, String chargePeriod);
|
||||
|
||||
/**
|
||||
* 查询项目某周期的分摊明细
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ package com.pms.base.service;
|
|||
import com.pms.base.entity.PublicRevenue;
|
||||
import com.pms.common.response.PageResult;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 公共收益服务接口
|
||||
* 提供 CRUD + 月度汇总
|
||||
|
|
@ -38,7 +36,7 @@ public interface PublicRevenueService {
|
|||
* @param period 周期,如 2025-06
|
||||
* @return 该周期有效收益总额
|
||||
*/
|
||||
BigDecimal monthlySummary(Long projectId, String period);
|
||||
Long monthlySummary(Long projectId, String period);
|
||||
|
||||
/**
|
||||
* 删除公共收益(逻辑删除)
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@ public class CamChargeServiceImpl implements CamChargeService {
|
|||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<CamCharge> allocateCamCharge(Long projectId, BigDecimal totalAmount, String chargePeriod) {
|
||||
public List<CamCharge> allocateCamCharge(Long projectId, Long totalAmountFen, String chargePeriod) {
|
||||
if (projectId == null) {
|
||||
throw new BusinessException(ErrorCode.PARAM_INVALID, "项目ID不能为空");
|
||||
}
|
||||
if (totalAmount == null || totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
if (totalAmountFen == null || totalAmountFen <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAM_INVALID, "分摊总额必须大于0");
|
||||
}
|
||||
if (chargePeriod == null || chargePeriod.isBlank()) {
|
||||
|
|
@ -84,14 +84,15 @@ public class CamChargeServiceImpl implements CamChargeService {
|
|||
}
|
||||
BigDecimal area = contract.getLeaseArea() == null ? BigDecimal.ZERO : contract.getLeaseArea();
|
||||
BigDecimal ratio = area.divide(totalArea, SCALE, RoundingMode.HALF_UP);
|
||||
BigDecimal allocated = totalAmount.multiply(ratio).setScale(2, RoundingMode.HALF_UP);
|
||||
long allocated = BigDecimal.valueOf(totalAmountFen).multiply(ratio)
|
||||
.setScale(0, RoundingMode.HALF_UP).longValueExact();
|
||||
|
||||
CamCharge charge = new CamCharge();
|
||||
charge.setProjectId(projectId);
|
||||
charge.setContractId(contract.getId());
|
||||
charge.setChargePeriod(chargePeriod);
|
||||
charge.setTotalAmount(totalAmount);
|
||||
charge.setAllocatedAmount(allocated);
|
||||
charge.setTotalAmountFen(totalAmountFen);
|
||||
charge.setAllocatedAmountFen(allocated);
|
||||
charge.setRatio(ratio);
|
||||
charge.setLeaseArea(area);
|
||||
charge.setStatus(STATUS_PENDING);
|
||||
|
|
@ -103,7 +104,7 @@ public class CamChargeServiceImpl implements CamChargeService {
|
|||
result.add(charge);
|
||||
}
|
||||
log.info("CAM 分摊完成: projectId={}, period={}, total={}, bills={}, skipped={}",
|
||||
projectId, chargePeriod, totalAmount, result.size(), existingContractIds.size());
|
||||
projectId, chargePeriod, totalAmountFen, result.size(), existingContractIds.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ public class ContractServiceImpl implements ContractService {
|
|||
contract.setStartDate(request.getStartDate());
|
||||
contract.setEndDate(request.getEndDate());
|
||||
contract.setSignDate(request.getSignDate());
|
||||
contract.setAmount(request.getAmount());
|
||||
contract.setDeposit(request.getDeposit());
|
||||
contract.setAmountFen(request.getAmountFen());
|
||||
contract.setDepositFen(request.getDepositFen());
|
||||
contract.setPaymentCycle(request.getPaymentCycle());
|
||||
contract.setAttachment(request.getAttachment());
|
||||
contract.setStatus(request.getStatus() != null ? request.getStatus() : 0);
|
||||
|
|
@ -130,8 +130,8 @@ public class ContractServiceImpl implements ContractService {
|
|||
update.setStartDate(request.getStartDate());
|
||||
update.setEndDate(request.getEndDate());
|
||||
update.setSignDate(request.getSignDate());
|
||||
update.setAmount(request.getAmount());
|
||||
update.setDeposit(request.getDeposit());
|
||||
update.setAmountFen(request.getAmountFen());
|
||||
update.setDepositFen(request.getDepositFen());
|
||||
update.setPaymentCycle(request.getPaymentCycle());
|
||||
update.setAttachment(request.getAttachment());
|
||||
if (request.getStatus() != null) {
|
||||
|
|
@ -193,8 +193,8 @@ public class ContractServiceImpl implements ContractService {
|
|||
newContract.setStartDate(existing.getEndDate());
|
||||
newContract.setEndDate(request.getNewEndDate());
|
||||
newContract.setSignDate(request.getSignDate());
|
||||
newContract.setAmount(request.getAmount() != null ? request.getAmount() : existing.getAmount());
|
||||
newContract.setDeposit(existing.getDeposit());
|
||||
newContract.setAmountFen(request.getAmountFen() != null ? request.getAmountFen() : existing.getAmountFen());
|
||||
newContract.setDepositFen(existing.getDepositFen());
|
||||
newContract.setPaymentCycle(existing.getPaymentCycle());
|
||||
newContract.setAttachment(existing.getAttachment());
|
||||
newContract.setStatus(1);
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ public class ContractTypeServiceImpl implements ContractTypeService {
|
|||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long id) {
|
||||
ContractType existing = contractTypeMapper.selectById(id);
|
||||
if (existing == null) {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ public class DeviceCategoryServiceImpl implements DeviceCategoryService {
|
|||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long id) {
|
||||
DeviceCategory existing = deviceCategoryMapper.selectById(id);
|
||||
if (existing == null) {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public class DeviceMaintenanceServiceImpl implements DeviceMaintenanceService {
|
|||
dto.setMaintenanceDate(maintenance.getMaintenanceDate());
|
||||
dto.setPlanDate(maintenance.getPlanDate());
|
||||
dto.setHandler(maintenance.getHandler());
|
||||
dto.setCost(maintenance.getCost());
|
||||
dto.setCostFen(maintenance.getCostFen());
|
||||
dto.setResult(maintenance.getResult());
|
||||
dto.setImages(maintenance.getImages());
|
||||
dto.setStatus(maintenance.getStatus());
|
||||
|
|
@ -101,7 +101,7 @@ public class DeviceMaintenanceServiceImpl implements DeviceMaintenanceService {
|
|||
maintenance.setMaintenanceDate(request.getMaintenanceDate());
|
||||
maintenance.setPlanDate(request.getPlanDate());
|
||||
maintenance.setHandler(request.getHandler());
|
||||
maintenance.setCost(request.getCost());
|
||||
maintenance.setCostFen(request.getCostFen());
|
||||
maintenance.setResult(request.getResult());
|
||||
// 图片列表转逗号分隔字符串
|
||||
if (request.getImages() != null && !request.getImages().isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ public class DeviceServiceImpl implements DeviceService {
|
|||
device.setInstallDate(request.getInstallDate());
|
||||
device.setWarrantyUntil(request.getWarrantyUntil());
|
||||
device.setSupplier(request.getSupplier());
|
||||
device.setPrice(request.getPrice());
|
||||
device.setPriceFen(request.getPriceFen());
|
||||
device.setStatus(request.getStatus() != null ? request.getStatus() : CommonConstants.STATUS_ENABLED);
|
||||
device.setRemark(request.getRemark());
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ public class DeviceServiceImpl implements DeviceService {
|
|||
update.setInstallDate(request.getInstallDate());
|
||||
update.setWarrantyUntil(request.getWarrantyUntil());
|
||||
update.setSupplier(request.getSupplier());
|
||||
update.setPrice(request.getPrice());
|
||||
update.setPriceFen(request.getPriceFen());
|
||||
if (request.getStatus() != null) {
|
||||
update.setStatus(request.getStatus());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.pms.common.exception.BusinessException;
|
|||
import com.pms.common.exception.ErrorCode;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.security.UserContext;
|
||||
import com.pms.common.util.CryptoUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -27,6 +28,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
public class EnterpriseServiceImpl implements EnterpriseService {
|
||||
|
||||
private final EnterpriseMapper enterpriseMapper;
|
||||
private final CryptoUtil cryptoUtil;
|
||||
private final LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@Override
|
||||
|
|
@ -65,6 +67,23 @@ public class EnterpriseServiceImpl implements EnterpriseService {
|
|||
dto.setRemark(enterprise.getRemark());
|
||||
dto.setCreatedAt(enterprise.getCreatedAt());
|
||||
|
||||
// 解密联系电话
|
||||
if (dto.getContactPhone() != null && !dto.getContactPhone().isEmpty()) {
|
||||
try {
|
||||
dto.setContactPhone(cryptoUtil.decrypt(dto.getContactPhone()));
|
||||
} catch (Exception e) {
|
||||
log.warn("解密联系电话失败: enterpriseId={}", id);
|
||||
}
|
||||
}
|
||||
// 解密邮箱
|
||||
if (dto.getEmail() != null && !dto.getEmail().isEmpty()) {
|
||||
try {
|
||||
dto.setEmail(cryptoUtil.decrypt(dto.getEmail()));
|
||||
} catch (Exception e) {
|
||||
log.warn("解密邮箱失败: enterpriseId={}", id);
|
||||
}
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
|
|
@ -80,8 +99,18 @@ public class EnterpriseServiceImpl implements EnterpriseService {
|
|||
enterprise.setCreditCode(request.getCreditCode());
|
||||
enterprise.setLegalPerson(request.getLegalPerson());
|
||||
enterprise.setContactPerson(request.getContactPerson());
|
||||
enterprise.setContactPhone(request.getContactPhone());
|
||||
enterprise.setEmail(request.getEmail());
|
||||
// 加密联系电话
|
||||
if (request.getContactPhone() != null && !request.getContactPhone().isEmpty()) {
|
||||
enterprise.setContactPhone(cryptoUtil.encrypt(request.getContactPhone()));
|
||||
enterprise.setContactPhoneHash(cryptoUtil.hash(request.getContactPhone()));
|
||||
enterprise.setContactPhoneLast4Hash(cryptoUtil.hash(request.getContactPhone().substring(Math.max(0, request.getContactPhone().length() - 4))));
|
||||
}
|
||||
// 加密邮箱
|
||||
if (request.getEmail() != null && !request.getEmail().isEmpty()) {
|
||||
enterprise.setEmail(cryptoUtil.encrypt(request.getEmail()));
|
||||
enterprise.setEmailHash(cryptoUtil.hash(request.getEmail()));
|
||||
enterprise.setEmailLast4Hash(cryptoUtil.hash(request.getEmail().substring(Math.max(0, request.getEmail().length() - 4))));
|
||||
}
|
||||
enterprise.setAddress(request.getAddress());
|
||||
enterprise.setBusinessScope(request.getBusinessScope());
|
||||
enterprise.setRoomIds(request.getRoomIds());
|
||||
|
|
@ -114,8 +143,18 @@ public class EnterpriseServiceImpl implements EnterpriseService {
|
|||
update.setCreditCode(request.getCreditCode());
|
||||
update.setLegalPerson(request.getLegalPerson());
|
||||
update.setContactPerson(request.getContactPerson());
|
||||
update.setContactPhone(request.getContactPhone());
|
||||
update.setEmail(request.getEmail());
|
||||
// 加密联系电话
|
||||
if (request.getContactPhone() != null && !request.getContactPhone().isEmpty()) {
|
||||
update.setContactPhone(cryptoUtil.encrypt(request.getContactPhone()));
|
||||
update.setContactPhoneHash(cryptoUtil.hash(request.getContactPhone()));
|
||||
update.setContactPhoneLast4Hash(cryptoUtil.hash(request.getContactPhone().substring(Math.max(0, request.getContactPhone().length() - 4))));
|
||||
}
|
||||
// 加密邮箱
|
||||
if (request.getEmail() != null && !request.getEmail().isEmpty()) {
|
||||
update.setEmail(cryptoUtil.encrypt(request.getEmail()));
|
||||
update.setEmailHash(cryptoUtil.hash(request.getEmail()));
|
||||
update.setEmailLast4Hash(cryptoUtil.hash(request.getEmail().substring(Math.max(0, request.getEmail().length() - 4))));
|
||||
}
|
||||
update.setAddress(request.getAddress());
|
||||
update.setBusinessScope(request.getBusinessScope());
|
||||
update.setRoomIds(request.getRoomIds());
|
||||
|
|
|
|||
|
|
@ -68,6 +68,30 @@ public class OwnerServiceImpl implements OwnerService {
|
|||
log.warn("解密身份证号失败: ownerId={}", id);
|
||||
}
|
||||
}
|
||||
// 解密手机号
|
||||
if (dto.getPhone() != null && !dto.getPhone().isEmpty()) {
|
||||
try {
|
||||
dto.setPhone(cryptoUtil.decrypt(dto.getPhone()));
|
||||
} catch (Exception e) {
|
||||
log.warn("解密手机号失败: ownerId={}", id);
|
||||
}
|
||||
}
|
||||
// 解密邮箱
|
||||
if (dto.getEmail() != null && !dto.getEmail().isEmpty()) {
|
||||
try {
|
||||
dto.setEmail(cryptoUtil.decrypt(dto.getEmail()));
|
||||
} catch (Exception e) {
|
||||
log.warn("解密邮箱失败: ownerId={}", id);
|
||||
}
|
||||
}
|
||||
// 解密紧急联系电话
|
||||
if (dto.getEmergencyPhone() != null && !dto.getEmergencyPhone().isEmpty()) {
|
||||
try {
|
||||
dto.setEmergencyPhone(cryptoUtil.decrypt(dto.getEmergencyPhone()));
|
||||
} catch (Exception e) {
|
||||
log.warn("解密紧急联系电话失败: ownerId={}", id);
|
||||
}
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
|
@ -103,11 +127,25 @@ public class OwnerServiceImpl implements OwnerService {
|
|||
owner.setIdNo(cryptoUtil.encrypt(request.getIdNo()));
|
||||
owner.setIdNoHash(cryptoUtil.hash(request.getIdNo()));
|
||||
}
|
||||
owner.setPhone(request.getPhone());
|
||||
owner.setEmail(request.getEmail());
|
||||
// 加密手机号
|
||||
if (request.getPhone() != null && !request.getPhone().isEmpty()) {
|
||||
owner.setPhone(cryptoUtil.encrypt(request.getPhone()));
|
||||
owner.setPhoneHash(cryptoUtil.hash(request.getPhone()));
|
||||
owner.setPhoneLast4Hash(cryptoUtil.hash(request.getPhone().substring(Math.max(0, request.getPhone().length() - 4))));
|
||||
}
|
||||
// 加密邮箱
|
||||
if (request.getEmail() != null && !request.getEmail().isEmpty()) {
|
||||
owner.setEmail(cryptoUtil.encrypt(request.getEmail()));
|
||||
owner.setEmailHash(cryptoUtil.hash(request.getEmail()));
|
||||
owner.setEmailLast4Hash(cryptoUtil.hash(request.getEmail().substring(Math.max(0, request.getEmail().length() - 4))));
|
||||
}
|
||||
owner.setAddress(request.getAddress());
|
||||
owner.setEmergencyContact(request.getEmergencyContact());
|
||||
owner.setEmergencyPhone(request.getEmergencyPhone());
|
||||
// 加密紧急联系电话
|
||||
if (request.getEmergencyPhone() != null && !request.getEmergencyPhone().isEmpty()) {
|
||||
owner.setEmergencyPhone(cryptoUtil.encrypt(request.getEmergencyPhone()));
|
||||
owner.setEmergencyPhoneHash(cryptoUtil.hash(request.getEmergencyPhone()));
|
||||
}
|
||||
owner.setStatus(request.getStatus() != null ? request.getStatus() : CommonConstants.STATUS_ENABLED);
|
||||
owner.setRemark(request.getRemark());
|
||||
|
||||
|
|
@ -142,11 +180,25 @@ public class OwnerServiceImpl implements OwnerService {
|
|||
update.setIdNo(cryptoUtil.encrypt(request.getIdNo()));
|
||||
update.setIdNoHash(cryptoUtil.hash(request.getIdNo()));
|
||||
}
|
||||
update.setPhone(request.getPhone());
|
||||
update.setEmail(request.getEmail());
|
||||
// 加密手机号
|
||||
if (request.getPhone() != null && !request.getPhone().isEmpty()) {
|
||||
update.setPhone(cryptoUtil.encrypt(request.getPhone()));
|
||||
update.setPhoneHash(cryptoUtil.hash(request.getPhone()));
|
||||
update.setPhoneLast4Hash(cryptoUtil.hash(request.getPhone().substring(Math.max(0, request.getPhone().length() - 4))));
|
||||
}
|
||||
// 加密邮箱
|
||||
if (request.getEmail() != null && !request.getEmail().isEmpty()) {
|
||||
update.setEmail(cryptoUtil.encrypt(request.getEmail()));
|
||||
update.setEmailHash(cryptoUtil.hash(request.getEmail()));
|
||||
update.setEmailLast4Hash(cryptoUtil.hash(request.getEmail().substring(Math.max(0, request.getEmail().length() - 4))));
|
||||
}
|
||||
update.setAddress(request.getAddress());
|
||||
update.setEmergencyContact(request.getEmergencyContact());
|
||||
update.setEmergencyPhone(request.getEmergencyPhone());
|
||||
// 加密紧急联系电话
|
||||
if (request.getEmergencyPhone() != null && !request.getEmergencyPhone().isEmpty()) {
|
||||
update.setEmergencyPhone(cryptoUtil.encrypt(request.getEmergencyPhone()));
|
||||
update.setEmergencyPhoneHash(cryptoUtil.hash(request.getEmergencyPhone()));
|
||||
}
|
||||
if (request.getStatus() != null) {
|
||||
update.setStatus(request.getStatus());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ public class ProjectServiceImpl implements ProjectService {
|
|||
project.setLogo(request.getLogo());
|
||||
project.setStatus(request.getStatus() != null ? request.getStatus() : CommonConstants.STATUS_ENABLED);
|
||||
project.setSort(request.getSort() != null ? request.getSort() : 0);
|
||||
project.setProjectId(null);
|
||||
// R24: lifecycle_stage 服务端权威,新建项目从立项开始
|
||||
project.setLifecycleStage(LifecycleStage.INITIATION);
|
||||
|
||||
|
|
@ -167,6 +166,7 @@ public class ProjectServiceImpl implements ProjectService {
|
|||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long id) {
|
||||
Project existing = projectMapper.selectById(id);
|
||||
if (existing == null) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import org.springframework.stereotype.Service;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
@ -72,8 +71,8 @@ public class PublicRevenueServiceImpl implements PublicRevenueService {
|
|||
if (!StringUtils.hasText(revenue.getPeriod())) {
|
||||
throw new BusinessException(ErrorCode.PARAM_INVALID, "周期不能为空");
|
||||
}
|
||||
if (revenue.getAmount() == null) {
|
||||
revenue.setAmount(BigDecimal.ZERO);
|
||||
if (revenue.getAmountFen() == null) {
|
||||
revenue.setAmountFen(0L);
|
||||
}
|
||||
if (revenue.getReceivedAt() == null) {
|
||||
revenue.setReceivedAt(System.currentTimeMillis());
|
||||
|
|
@ -88,7 +87,7 @@ public class PublicRevenueServiceImpl implements PublicRevenueService {
|
|||
|
||||
publicRevenueMapper.insert(revenue);
|
||||
log.info("录入公共收益成功: id={}, projectId={}, type={}, amount={}",
|
||||
revenue.getId(), revenue.getProjectId(), revenue.getRevenueType(), revenue.getAmount());
|
||||
revenue.getId(), revenue.getProjectId(), revenue.getRevenueType(), revenue.getAmountFen());
|
||||
return revenue.getId();
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +104,7 @@ public class PublicRevenueServiceImpl implements PublicRevenueService {
|
|||
PublicRevenue update = new PublicRevenue();
|
||||
update.setId(id);
|
||||
update.setRevenueType(revenue.getRevenueType());
|
||||
update.setAmount(revenue.getAmount());
|
||||
update.setAmountFen(revenue.getAmountFen());
|
||||
update.setPeriod(revenue.getPeriod());
|
||||
update.setReceivedAt(revenue.getReceivedAt());
|
||||
update.setDescription(revenue.getDescription());
|
||||
|
|
@ -120,9 +119,9 @@ public class PublicRevenueServiceImpl implements PublicRevenueService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal monthlySummary(Long projectId, String period) {
|
||||
public Long monthlySummary(Long projectId, String period) {
|
||||
if (projectId == null || !StringUtils.hasText(period)) {
|
||||
return BigDecimal.ZERO;
|
||||
return 0L;
|
||||
}
|
||||
LambdaQueryWrapper<PublicRevenue> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PublicRevenue::getProjectId, projectId)
|
||||
|
|
@ -130,8 +129,8 @@ public class PublicRevenueServiceImpl implements PublicRevenueService {
|
|||
.eq(PublicRevenue::getStatus, CommonConstants.STATUS_ENABLED);
|
||||
List<PublicRevenue> list = publicRevenueMapper.selectList(wrapper);
|
||||
return list.stream()
|
||||
.map(PublicRevenue::getAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
.map(PublicRevenue::getAmountFen)
|
||||
.reduce(0L, Long::sum);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ public class RenovationServiceImpl implements RenovationService {
|
|||
// 创建装修记录,状态=0待审批
|
||||
long now = System.currentTimeMillis();
|
||||
renovation.setStatus(STATUS_PENDING);
|
||||
if (renovation.getDepositAmount() == null) {
|
||||
renovation.setDepositAmount(java.math.BigDecimal.ZERO);
|
||||
if (renovation.getDepositAmountFen() == null) {
|
||||
renovation.setDepositAmountFen(0L);
|
||||
}
|
||||
renovation.setCreatedAt(now);
|
||||
renovation.setUpdatedAt(now);
|
||||
|
|
|
|||
|
|
@ -60,8 +60,18 @@ public class TenantServiceImpl implements TenantService {
|
|||
tenant.setIdNo(cryptoUtil.encrypt(request.getIdNo()));
|
||||
tenant.setIdNoHash(cryptoUtil.hash(request.getIdNo()));
|
||||
}
|
||||
tenant.setPhone(request.getPhone());
|
||||
tenant.setEmail(request.getEmail());
|
||||
// 加密手机号
|
||||
if (request.getPhone() != null && !request.getPhone().isEmpty()) {
|
||||
tenant.setPhone(cryptoUtil.encrypt(request.getPhone()));
|
||||
tenant.setPhoneHash(cryptoUtil.hash(request.getPhone()));
|
||||
tenant.setPhoneLast4Hash(cryptoUtil.hash(request.getPhone().substring(Math.max(0, request.getPhone().length() - 4))));
|
||||
}
|
||||
// 加密邮箱
|
||||
if (request.getEmail() != null && !request.getEmail().isEmpty()) {
|
||||
tenant.setEmail(cryptoUtil.encrypt(request.getEmail()));
|
||||
tenant.setEmailHash(cryptoUtil.hash(request.getEmail()));
|
||||
tenant.setEmailLast4Hash(cryptoUtil.hash(request.getEmail().substring(Math.max(0, request.getEmail().length() - 4))));
|
||||
}
|
||||
tenant.setAddress(request.getAddress());
|
||||
tenant.setStatus(request.getStatus() != null ? request.getStatus() : CommonConstants.STATUS_ENABLED);
|
||||
tenant.setRemark(request.getRemark());
|
||||
|
|
@ -95,8 +105,18 @@ public class TenantServiceImpl implements TenantService {
|
|||
update.setIdNo(cryptoUtil.encrypt(request.getIdNo()));
|
||||
update.setIdNoHash(cryptoUtil.hash(request.getIdNo()));
|
||||
}
|
||||
update.setPhone(request.getPhone());
|
||||
update.setEmail(request.getEmail());
|
||||
// 加密手机号
|
||||
if (request.getPhone() != null && !request.getPhone().isEmpty()) {
|
||||
update.setPhone(cryptoUtil.encrypt(request.getPhone()));
|
||||
update.setPhoneHash(cryptoUtil.hash(request.getPhone()));
|
||||
update.setPhoneLast4Hash(cryptoUtil.hash(request.getPhone().substring(Math.max(0, request.getPhone().length() - 4))));
|
||||
}
|
||||
// 加密邮箱
|
||||
if (request.getEmail() != null && !request.getEmail().isEmpty()) {
|
||||
update.setEmail(cryptoUtil.encrypt(request.getEmail()));
|
||||
update.setEmailHash(cryptoUtil.hash(request.getEmail()));
|
||||
update.setEmailLast4Hash(cryptoUtil.hash(request.getEmail().substring(Math.max(0, request.getEmail().length() - 4))));
|
||||
}
|
||||
update.setAddress(request.getAddress());
|
||||
if (request.getStatus() != null) {
|
||||
update.setStatus(request.getStatus());
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ public class WorkshopLeaseServiceImpl implements WorkshopLeaseService {
|
|||
update.setTenantEnterprise(entity.getTenantEnterprise());
|
||||
update.setLeaseStart(entity.getLeaseStart());
|
||||
update.setLeaseEnd(entity.getLeaseEnd());
|
||||
update.setRentAmount(entity.getRentAmount());
|
||||
update.setRentAmountFen(entity.getRentAmountFen());
|
||||
if (entity.getStatus() != null) {
|
||||
update.setStatus(entity.getStatus());
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@ public class WorkshopLeaseServiceImpl implements WorkshopLeaseService {
|
|||
// 首期最小实现:基于租赁合同生成账单编号,预留与 pms-charge 对接点
|
||||
String billNo = "BILL-" + lease.getId() + "-" + System.currentTimeMillis();
|
||||
log.info("生成租金账单: leaseId={}, tenant={}, rentAmount={}, billNo={}",
|
||||
leaseId, lease.getTenantEnterprise(), lease.getRentAmount(), billNo);
|
||||
leaseId, lease.getTenantEnterprise(), lease.getRentAmountFen(), billNo);
|
||||
return billNo;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +1,167 @@
|
|||
package com.pms.base.util;
|
||||
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import cn.hutool.crypto.symmetric.AES;
|
||||
import cn.hutool.crypto.symmetric.SymmetricCrypto;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.exception.ErrorCode;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 敏感数据加密工具类
|
||||
* 使用 AES-256-GCM 对身份证号等敏感字段进行加密/解密
|
||||
* 查询时使用 SHA-256 哈希进行匹配
|
||||
* 敏感数据加密工具类(AES-256-GCM)
|
||||
*
|
||||
* <p>使用 JDK 原生 {@code Cipher.getInstance("AES/GCM/NoPadding")} 实现 GCM 模式
|
||||
* (Hutool 5.8.27 的 Mode 枚举不支持 GCM,经 POC 验证)。
|
||||
*
|
||||
* <p>安全特性:
|
||||
* <ul>
|
||||
* <li>密钥从 Nacos 配置中心加载({@code app.encrypt.aes-key}),无默认值,启动时校验</li>
|
||||
* <li>IV 使用 {@link SecureRandom}(CSPRNG)每记录独立生成,与密文一同存储</li>
|
||||
* <li>hash 使用 HMAC-SHA256(抗彩虹表,需要 server secret)</li>
|
||||
* <li>加解密异常抛出 {@link BusinessException},不吞掉</li>
|
||||
* <li>SHA-256 派生 32 字节密钥(替代旧的 0 填充 padKey)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>输出格式:{@code Base64(IV || ciphertext || authTag)},其中 IV 为 12 字节,authTag 为 16 字节。
|
||||
*
|
||||
* <p>注意:业务代码(OwnerServiceImpl/TenantServiceImpl)实际使用 pms-common 的 CryptoUtil,
|
||||
* 此类为 pms-base 本地实现,供计划 U1 要求的独立加密工具。HMAC-SHA256 hash 与 CryptoUtil 的
|
||||
* SHA-256 hash 不兼容,不可混用查询。
|
||||
*/
|
||||
@Component
|
||||
public class AesEncryptUtil {
|
||||
|
||||
private final SymmetricCrypto aes;
|
||||
private static final String GCM_TRANSFORMATION = "AES/GCM/NoPadding";
|
||||
private static final int GCM_TAG_LENGTH_BITS = 128;
|
||||
private static final int GCM_IV_LENGTH_BYTES = 12;
|
||||
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||
|
||||
public AesEncryptUtil(@Value("${app.encrypt.aes-key:EtherPMS2024SecureKey!!}") String key) {
|
||||
// 使用 AES-256-ECB(Hutool默认),生产环境建议使用 GCM 模式
|
||||
byte[] keyBytes = padKey(key.getBytes(StandardCharsets.UTF_8));
|
||||
this.aes = new AES(keyBytes);
|
||||
private final byte[] aesKeyBytes;
|
||||
private final byte[] hmacKeyBytes;
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
/**
|
||||
* @param aesKey AES 密钥(从 Nacos 加载,无默认值;未配置时启动失败)
|
||||
*/
|
||||
public AesEncryptUtil(@Value("${app.encrypt.aes-key}") String aesKey) {
|
||||
if (aesKey == null || aesKey.isBlank()) {
|
||||
throw new IllegalStateException("AES密钥未配置,请在 Nacos 设置 app.encrypt.aes-key");
|
||||
}
|
||||
// SHA-256 派生固定 32 字节 AES 密钥(替代旧 padKey 的 0 填充)
|
||||
this.aesKeyBytes = sha256(aesKey);
|
||||
// HMAC 密钥从 AES 密钥派生(避免引入额外配置项)
|
||||
this.hmacKeyBytes = sha256(aesKey + ":hmac");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密敏感数据
|
||||
* AES-256-GCM 加密
|
||||
*
|
||||
* @param plainText 明文
|
||||
* @return Base64 编码的密文(IV + ciphertext + authTag),输入 null/空时返回 null
|
||||
* @throws BusinessException 加密失败时抛出
|
||||
*/
|
||||
public String encrypt(String plainText) {
|
||||
if (plainText == null || plainText.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return aes.encryptHex(plainText);
|
||||
try {
|
||||
byte[] iv = new byte[GCM_IV_LENGTH_BYTES];
|
||||
secureRandom.nextBytes(iv);
|
||||
|
||||
Cipher cipher = Cipher.getInstance(GCM_TRANSFORMATION);
|
||||
cipher.init(Cipher.ENCRYPT_MODE,
|
||||
new SecretKeySpec(aesKeyBytes, "AES"),
|
||||
new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
|
||||
byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// IV 前缀拼接密文(含 authTag)
|
||||
byte[] combined = new byte[GCM_IV_LENGTH_BYTES + cipherText.length];
|
||||
System.arraycopy(iv, 0, combined, 0, GCM_IV_LENGTH_BYTES);
|
||||
System.arraycopy(cipherText, 0, combined, GCM_IV_LENGTH_BYTES, cipherText.length);
|
||||
|
||||
return Base64.getEncoder().encodeToString(combined);
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_FAILED, "AES加密失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密敏感数据
|
||||
* AES-256-GCM 解密
|
||||
*
|
||||
* @param cipherText Base64 编码的密文(IV + ciphertext + authTag)
|
||||
* @return 明文,输入 null/空时返回 null
|
||||
* @throws BusinessException 解密失败时抛出(不吞异常)
|
||||
*/
|
||||
public String decrypt(String cipherText) {
|
||||
if (cipherText == null || cipherText.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return aes.decryptStr(cipherText);
|
||||
byte[] combined = Base64.getDecoder().decode(cipherText);
|
||||
if (combined.length < GCM_IV_LENGTH_BYTES) {
|
||||
throw new IllegalArgumentException("密文长度不足,缺少 IV");
|
||||
}
|
||||
byte[] iv = Arrays.copyOfRange(combined, 0, GCM_IV_LENGTH_BYTES);
|
||||
byte[] encrypted = Arrays.copyOfRange(combined, GCM_IV_LENGTH_BYTES, combined.length);
|
||||
|
||||
Cipher cipher = Cipher.getInstance(GCM_TRANSFORMATION);
|
||||
cipher.init(Cipher.DECRYPT_MODE,
|
||||
new SecretKeySpec(aesKeyBytes, "AES"),
|
||||
new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
|
||||
byte[] decrypted = cipher.doFinal(encrypted);
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
throw new BusinessException(ErrorCode.OPERATION_FAILED, "AES解密失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成哈希值(用于查询匹配)
|
||||
* HMAC-SHA256 哈希(抗彩虹表,用于查询匹配)
|
||||
*
|
||||
* <p>与 CryptoUtil.hash()(SHA-256)不兼容,不可混用查询。
|
||||
*
|
||||
* @param plainText 明文
|
||||
* @return 64 位十六进制哈希值,输入 null/空时返回 null
|
||||
* @throws BusinessException 哈希失败时抛出
|
||||
*/
|
||||
public String hash(String plainText) {
|
||||
if (plainText == null || plainText.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return SecureUtil.sha256(plainText);
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||
mac.init(new SecretKeySpec(hmacKeyBytes, HMAC_ALGORITHM));
|
||||
byte[] hashBytes = mac.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(hashBytes);
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_FAILED, "HMAC哈希失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将密钥补齐到32字节(AES-256)
|
||||
*/
|
||||
private byte[] padKey(byte[] keyBytes) {
|
||||
byte[] result = new byte[32];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
result[i] = i < keyBytes.length ? keyBytes[i] : 0;
|
||||
/** SHA-256 派生固定 32 字节密钥 */
|
||||
private static byte[] sha256(String input) {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-256")
|
||||
.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("SHA-256 不可用", e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder(bytes.length * 2);
|
||||
for (byte b : bytes) {
|
||||
sb.append(Character.forDigit((b >> 4) & 0xF, 16));
|
||||
sb.append(Character.forDigit(b & 0xF, 16));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
package db.migration;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.flywaydb.core.api.migration.BaseJavaMigration;
|
||||
import org.flywaydb.core.api.migration.Context;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Flyway V12: AES ECB→GCM 迁移 + idNoHash 填充
|
||||
*
|
||||
* <p>背景:历史 AesEncryptUtil 使用 ECB 模式且未填充 idNoHash。本迁移:
|
||||
* <ol>
|
||||
* <li>扫描 t_owner / t_tenant 中 id_no IS NOT NULL 但 id_no_hash IS NULL 的记录</li>
|
||||
* <li>若存在 null idNoHash:读取 AES_KEY 环境变量,解密 id_no(GCM),计算 SHA-256 hash 填充</li>
|
||||
* <li>若不存在 null idNoHash:迁移为 no-op</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>注意:业务代码实际使用 pms-common 的 CryptoUtil(已为 GCM 模式),AesEncryptUtil 为死代码。
|
||||
* 本迁移作为防御性数据完整性检查,处理可能存在的历史 null idNoHash 数据。
|
||||
*
|
||||
* <p>幂等性:仅更新 id_no_hash IS NULL 的记录,重复执行无副作用。
|
||||
*/
|
||||
@Slf4j
|
||||
public class V12__MigrateAesEcbToGcm extends BaseJavaMigration {
|
||||
|
||||
private static final int GCM_IV_LENGTH = 12;
|
||||
private static final int GCM_TAG_LENGTH_BITS = 128;
|
||||
|
||||
@Override
|
||||
public void migrate(Context context) throws Exception {
|
||||
Connection conn = context.getConnection();
|
||||
|
||||
int ownerNullHash = countNullHash(conn, "t_owner");
|
||||
int tenantNullHash = countNullHash(conn, "t_tenant");
|
||||
int totalNullHash = ownerNullHash + tenantNullHash;
|
||||
|
||||
log.info("[V12] 扫描 idNoHash 为空记录: t_owner={}, t_tenant={}, 总计={}",
|
||||
ownerNullHash, tenantNullHash, totalNullHash);
|
||||
|
||||
if (totalNullHash == 0) {
|
||||
log.info("[V12] 无需迁移,所有 idNoHash 已填充");
|
||||
return;
|
||||
}
|
||||
|
||||
// 需要 AES_KEY 解密 id_no 后计算 hash
|
||||
String aesKey = System.getenv("AES_KEY");
|
||||
if (aesKey == null || aesKey.isBlank()) {
|
||||
log.warn("[V12] AES_KEY 环境变量未设置,跳过 {} 条 null idNoHash 填充。"
|
||||
+ "请手动设置 AES_KEY 后重新执行迁移,或手动填充 idNoHash。", totalNullHash);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] keyBytes = MessageDigest.getInstance("SHA-256")
|
||||
.digest(aesKey.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
int ownerFixed = fillNullHash(conn, "t_owner", keyBytes);
|
||||
int tenantFixed = fillNullHash(conn, "t_tenant", keyBytes);
|
||||
|
||||
log.info("[V12] idNoHash 填充完成: t_owner={} 条, t_tenant={} 条", ownerFixed, tenantFixed);
|
||||
}
|
||||
|
||||
/** 统计 id_no 非空但 id_no_hash 为空的记录数 */
|
||||
private int countNullHash(Connection conn, String table) throws Exception {
|
||||
try (Statement stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery(
|
||||
"SELECT COUNT(*) FROM " + table + " WHERE id_no IS NOT NULL AND id_no_hash IS NULL")) {
|
||||
rs.next();
|
||||
return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
|
||||
/** 填充 null idNoHash:解密 id_no → SHA-256 hash → 更新(批量) */
|
||||
private int fillNullHash(Connection conn, String table, byte[] keyBytes) throws Exception {
|
||||
int fixed = 0;
|
||||
int batchSize = 0;
|
||||
final int FLUSH_EVERY = 500;
|
||||
try (PreparedStatement select = conn.prepareStatement(
|
||||
"SELECT id, id_no FROM " + table + " WHERE id_no IS NOT NULL AND id_no_hash IS NULL");
|
||||
PreparedStatement update = conn.prepareStatement(
|
||||
"UPDATE " + table + " SET id_no_hash = ? WHERE id = ?");
|
||||
ResultSet rs = select.executeQuery()) {
|
||||
|
||||
while (rs.next()) {
|
||||
long id = rs.getLong("id");
|
||||
String cipherText = rs.getString("id_no");
|
||||
try {
|
||||
String plainText = decryptGcm(cipherText, keyBytes);
|
||||
String hash = sha256Hex(plainText);
|
||||
update.setString(1, hash);
|
||||
update.setLong(2, id);
|
||||
update.addBatch();
|
||||
fixed++;
|
||||
if (++batchSize >= FLUSH_EVERY) {
|
||||
update.executeBatch();
|
||||
batchSize = 0;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[V12] {} id={} 解密失败,跳过: {}", table, id, e.getMessage());
|
||||
}
|
||||
}
|
||||
if (batchSize > 0) {
|
||||
update.executeBatch();
|
||||
}
|
||||
}
|
||||
return fixed;
|
||||
}
|
||||
|
||||
/** AES-256-GCM 解密(与 CryptoUtil/AesEncryptUtil 格式一致:Base64(IV + ciphertext + tag)) */
|
||||
private String decryptGcm(String cipherText, byte[] keyBytes) throws Exception {
|
||||
byte[] combined = Base64.getDecoder().decode(cipherText);
|
||||
byte[] iv = Arrays.copyOfRange(combined, 0, GCM_IV_LENGTH);
|
||||
byte[] encrypted = Arrays.copyOfRange(combined, GCM_IV_LENGTH, combined.length);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE,
|
||||
new SecretKeySpec(keyBytes, "AES"),
|
||||
new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
|
||||
byte[] decrypted = cipher.doFinal(encrypted);
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/** SHA-256 哈希(与 CryptoUtil.hash() 一致) */
|
||||
private String sha256Hex(String input) throws Exception {
|
||||
byte[] hash = MessageDigest.getInstance("SHA-256")
|
||||
.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder(hash.length * 2);
|
||||
for (byte b : hash) {
|
||||
sb.append(Character.forDigit((b >> 4) & 0xF, 16));
|
||||
sb.append(Character.forDigit(b & 0xF, 16));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,26 @@ spring:
|
|||
connect-timeout: 3000
|
||||
read-timeout: 5000
|
||||
|
||||
# Resilience4j 熔断配置(U7)
|
||||
resilience4j:
|
||||
circuitbreaker:
|
||||
configs:
|
||||
default:
|
||||
failure-rate-threshold: 50
|
||||
slow-call-rate-threshold: 80
|
||||
slow-call-duration-threshold: 2s
|
||||
wait-duration-in-open-state: 30s
|
||||
sliding-window-size: 20
|
||||
minimum-number-of-calls: 10
|
||||
permitted-number-of-calls-in-half-open-state: 10
|
||||
instances:
|
||||
default:
|
||||
base-config: default
|
||||
timelimiter:
|
||||
configs:
|
||||
default:
|
||||
timeout-duration: 6s
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:mapper/**/*.xml
|
||||
type-aliases-package: com.pms.base.**.entity
|
||||
|
|
@ -81,10 +101,15 @@ mybatis-plus:
|
|||
cache-enabled: false
|
||||
|
||||
# 敏感数据加密配置(AES-256-GCM)
|
||||
# 生产环境通过环境变量 AES_KEY 注入实际密钥
|
||||
# 生产环境通过环境变量 AES_KEY 注入实际密钥(无默认值,未配置时启动失败)
|
||||
crypto:
|
||||
aes:
|
||||
key: ${AES_KEY:EtherPMS2024AesSecretKey32Bytes!}
|
||||
key: ${AES_KEY}
|
||||
|
||||
# pms-base 独立加密工具配置(U1: AesEncryptUtil 使用)
|
||||
app:
|
||||
encrypt:
|
||||
aes-key: ${AES_KEY}
|
||||
|
||||
# 内部接口鉴权配置(U9)
|
||||
internal:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
-- V13: BigDecimal 金额字段迁移到 Long(分)
|
||||
-- 仅迁移金额字段,非金额 BigDecimal(面积/比例/经纬度/仪表读数)保持不变
|
||||
-- 值转换:DECIMAL(15,2) 元 → BIGINT 分(× 100,四舍五入)
|
||||
-- 注意:UPDATE 必须在 ALTER 之前执行,否则 DECIMAL→BIGINT 转换会先四舍五入丢失分位
|
||||
|
||||
-- t_contract: amount, deposit
|
||||
UPDATE t_contract SET amount = ROUND(amount * 100, 0), deposit = ROUND(deposit * 100, 0);
|
||||
ALTER TABLE t_contract MODIFY COLUMN amount BIGINT NOT NULL DEFAULT 0 COMMENT '合同金额(分)';
|
||||
ALTER TABLE t_contract MODIFY COLUMN deposit BIGINT NOT NULL DEFAULT 0 COMMENT '押金(分)';
|
||||
|
||||
-- t_lease_contract: rent_amount, deposit
|
||||
UPDATE t_lease_contract SET rent_amount = ROUND(rent_amount * 100, 0), deposit = ROUND(deposit * 100, 0);
|
||||
ALTER TABLE t_lease_contract MODIFY COLUMN rent_amount BIGINT NOT NULL DEFAULT 0 COMMENT '月租金(分)';
|
||||
ALTER TABLE t_lease_contract MODIFY COLUMN deposit BIGINT NOT NULL DEFAULT 0 COMMENT '押金(分)';
|
||||
|
||||
-- t_workshop_lease: rent_amount
|
||||
UPDATE t_workshop_lease SET rent_amount = ROUND(rent_amount * 100, 0);
|
||||
ALTER TABLE t_workshop_lease MODIFY COLUMN rent_amount BIGINT NOT NULL DEFAULT 0 COMMENT '租金金额(分)';
|
||||
|
||||
-- t_public_revenue: amount
|
||||
UPDATE t_public_revenue SET amount = ROUND(amount * 100, 0);
|
||||
ALTER TABLE t_public_revenue MODIFY COLUMN amount BIGINT NOT NULL DEFAULT 0 COMMENT '收益金额(分)';
|
||||
|
||||
-- t_cam_charge: total_amount, allocated_amount
|
||||
UPDATE t_cam_charge SET total_amount = ROUND(total_amount * 100, 0), allocated_amount = ROUND(allocated_amount * 100, 0);
|
||||
ALTER TABLE t_cam_charge MODIFY COLUMN total_amount BIGINT NOT NULL DEFAULT 0 COMMENT '本期公共费用总额(分)';
|
||||
ALTER TABLE t_cam_charge MODIFY COLUMN allocated_amount BIGINT NOT NULL DEFAULT 0 COMMENT '分摊金额(分)';
|
||||
|
||||
-- t_device: price
|
||||
UPDATE t_device SET price = ROUND(price * 100, 0);
|
||||
ALTER TABLE t_device MODIFY COLUMN price BIGINT NOT NULL DEFAULT 0 COMMENT '采购价格(分)';
|
||||
|
||||
-- t_device_maintenance: cost
|
||||
UPDATE t_device_maintenance SET cost = ROUND(cost * 100, 0);
|
||||
ALTER TABLE t_device_maintenance MODIFY COLUMN cost BIGINT NOT NULL DEFAULT 0 COMMENT '维保费用(分)';
|
||||
|
||||
-- t_renovation: deposit_amount
|
||||
UPDATE t_renovation SET deposit_amount = ROUND(deposit_amount * 100, 0);
|
||||
ALTER TABLE t_renovation MODIFY COLUMN deposit_amount BIGINT NOT NULL DEFAULT 0 COMMENT '装修押金(分)';
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
-- V14: 新增 phone/email hash 字段 + 索引
|
||||
-- t_owner
|
||||
ALTER TABLE t_owner ADD COLUMN phone_hash VARCHAR(64) DEFAULT NULL COMMENT '手机号哈希';
|
||||
ALTER TABLE t_owner ADD COLUMN phone_last4_hash VARCHAR(64) DEFAULT NULL COMMENT '手机号后4位哈希';
|
||||
ALTER TABLE t_owner ADD COLUMN email_hash VARCHAR(64) DEFAULT NULL COMMENT '邮箱哈希';
|
||||
ALTER TABLE t_owner ADD COLUMN email_last4_hash VARCHAR(64) DEFAULT NULL COMMENT '邮箱后4位哈希';
|
||||
ALTER TABLE t_owner ADD COLUMN emergency_phone_hash VARCHAR(64) DEFAULT NULL COMMENT '紧急联系电话哈希';
|
||||
CREATE INDEX idx_owner_phone_hash ON t_owner(phone_hash);
|
||||
CREATE INDEX idx_owner_phone_last4 ON t_owner(phone_last4_hash);
|
||||
CREATE INDEX idx_owner_email_hash ON t_owner(email_hash);
|
||||
|
||||
-- t_tenant
|
||||
ALTER TABLE t_tenant ADD COLUMN phone_hash VARCHAR(64) DEFAULT NULL COMMENT '手机号哈希';
|
||||
ALTER TABLE t_tenant ADD COLUMN phone_last4_hash VARCHAR(64) DEFAULT NULL COMMENT '手机号后4位哈希';
|
||||
ALTER TABLE t_tenant ADD COLUMN email_hash VARCHAR(64) DEFAULT NULL COMMENT '邮箱哈希';
|
||||
ALTER TABLE t_tenant ADD COLUMN email_last4_hash VARCHAR(64) DEFAULT NULL COMMENT '邮箱后4位哈希';
|
||||
CREATE INDEX idx_tenant_phone_hash ON t_tenant(phone_hash);
|
||||
CREATE INDEX idx_tenant_phone_last4 ON t_tenant(phone_last4_hash);
|
||||
CREATE INDEX idx_tenant_email_hash ON t_tenant(email_hash);
|
||||
|
||||
-- t_enterprise
|
||||
ALTER TABLE t_enterprise ADD COLUMN contact_phone_hash VARCHAR(64) DEFAULT NULL COMMENT '联系电话哈希';
|
||||
ALTER TABLE t_enterprise ADD COLUMN contact_phone_last4_hash VARCHAR(64) DEFAULT NULL COMMENT '联系电话后4位哈希';
|
||||
ALTER TABLE t_enterprise ADD COLUMN email_hash VARCHAR(64) DEFAULT NULL COMMENT '邮箱哈希';
|
||||
ALTER TABLE t_enterprise ADD COLUMN email_last4_hash VARCHAR(64) DEFAULT NULL COMMENT '邮箱后4位哈希';
|
||||
CREATE INDEX idx_enterprise_phone_hash ON t_enterprise(contact_phone_hash);
|
||||
CREATE INDEX idx_enterprise_phone_last4 ON t_enterprise(contact_phone_last4_hash);
|
||||
CREATE INDEX idx_enterprise_email_hash ON t_enterprise(email_hash);
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
-- ========================================
|
||||
-- V15: U10 pms-base 方法级鉴权权限码
|
||||
-- 1. 新增 20 个 base:*:manage 功能权限(perm_type=3)
|
||||
-- 2. ROLE_ADMIN(id=1)关联全部 20 个权限
|
||||
-- 3. ROLE_PROPERTY(id=2)关联 15 个核心物业管理权限
|
||||
-- ========================================
|
||||
-- ID 规划:
|
||||
-- t_permission 300-319(避开 V1 的 101-135、V6/V8 的 141-145、V3 的 201-251)
|
||||
-- t_role_permission 123-157(接续 V8 已用 122)
|
||||
|
||||
-- ====================
|
||||
-- 1. 新增 pms-base 功能权限
|
||||
-- ====================
|
||||
INSERT INTO `t_permission` (`id`, `perm_code`, `perm_name`, `perm_type`, `parent_id`, `sort`, `status`, `created_at`, `updated_at`, `deleted`) VALUES
|
||||
(300, 'base:owner:manage', '业主管理', 3, 0, 20, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(301, 'base:tenant:manage', '租户管理', 3, 0, 21, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(302, 'base:building:manage', '楼栋管理', 3, 0, 22, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(303, 'base:room:manage', '房间管理', 3, 0, 23, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(304, 'base:floor:manage', '楼层管理', 3, 0, 24, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(305, 'base:project:manage', '项目管理', 3, 0, 25, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(306, 'base:contract:manage', '合同管理', 3, 0, 26, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(307, 'base:lease:manage', '租赁合同管理', 3, 0, 27, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(308, 'base:device:manage', '设备管理', 3, 0, 28, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(309, 'base:energy:manage', '能源表计管理', 3, 0, 29, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(310, 'base:renovation:manage', '装修管理', 3, 0, 30, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(311, 'base:revenue:manage', '公共收益管理', 3, 0, 31, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(312, 'base:finance:manage', '财务分摊管理', 3, 0, 32, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(313, 'base:enterprise:manage', '企业管理', 3, 0, 33, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(314, 'base:committee:manage', '业委会管理', 3, 0, 34, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(315, 'base:workshop:manage', '厂房租赁管理', 3, 0, 35, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(316, 'base:meeting:manage', '会议室管理', 3, 0, 36, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(317, 'base:activity:manage', '社区活动管理', 3, 0, 37, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(318, 'base:inspection:manage', '安全检查管理', 3, 0, 38, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0),
|
||||
(319, 'base:lifecycle:manage', '生命周期管理', 3, 0, 39, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, 0)
|
||||
ON DUPLICATE KEY UPDATE `perm_name` = VALUES(`perm_name`);
|
||||
|
||||
-- ====================
|
||||
-- 2. 为 ROLE_ADMIN(id=1)关联全部 20 个 base 权限
|
||||
-- ====================
|
||||
INSERT INTO `t_role_permission` (`id`, `project_id`, `role_id`, `permission_id`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted`) VALUES
|
||||
(123, NULL, 1, 300, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(124, NULL, 1, 301, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(125, NULL, 1, 302, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(126, NULL, 1, 303, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(127, NULL, 1, 304, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(128, NULL, 1, 305, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(129, NULL, 1, 306, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(130, NULL, 1, 307, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(131, NULL, 1, 308, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(132, NULL, 1, 309, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(133, NULL, 1, 310, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(134, NULL, 1, 311, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(135, NULL, 1, 312, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(136, NULL, 1, 313, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(137, NULL, 1, 314, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(138, NULL, 1, 315, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(139, NULL, 1, 316, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(140, NULL, 1, 317, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(141, NULL, 1, 318, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(142, NULL, 1, 319, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0)
|
||||
ON DUPLICATE KEY UPDATE `updated_at` = VALUES(`updated_at`);
|
||||
|
||||
-- ====================
|
||||
-- 3. 为 ROLE_PROPERTY(id=2)关联核心物业管理权限(15 个)
|
||||
-- 排除:项目、公共收益、财务分摊、企业、生命周期(管理/财务级)
|
||||
-- ====================
|
||||
INSERT INTO `t_role_permission` (`id`, `project_id`, `role_id`, `permission_id`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted`) VALUES
|
||||
(143, NULL, 2, 300, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(144, NULL, 2, 301, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(145, NULL, 2, 302, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(146, NULL, 2, 303, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(147, NULL, 2, 304, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(148, NULL, 2, 306, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(149, NULL, 2, 307, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(150, NULL, 2, 308, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(151, NULL, 2, 309, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(152, NULL, 2, 310, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(153, NULL, 2, 314, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(154, NULL, 2, 315, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(155, NULL, 2, 316, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(156, NULL, 2, 317, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
|
||||
(157, NULL, 2, 318, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0)
|
||||
ON DUPLICATE KEY UPDATE `updated_at` = VALUES(`updated_at`);
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
SELECT c.id, c.project_id, c.contract_no, c.contract_name, c.contract_type_id,
|
||||
ct.type_name AS contract_type_name, c.party_a, c.party_b,
|
||||
c.party_b_type, c.party_b_id, c.room_ids, c.start_date, c.end_date,
|
||||
c.sign_date, c.amount, c.deposit, c.payment_cycle, c.attachment,
|
||||
c.sign_date, c.amount AS amount_fen, c.deposit AS deposit_fen, c.payment_cycle, c.attachment,
|
||||
c.status, c.remark, c.created_at,
|
||||
DATEDIFF(c.end_date, CURDATE()) AS days_left
|
||||
FROM t_contract c
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
SELECT c.id, c.project_id, c.contract_no, c.contract_name, c.contract_type_id,
|
||||
ct.type_name AS contract_type_name, c.party_a, c.party_b,
|
||||
c.party_b_type, c.party_b_id, c.room_ids, c.start_date, c.end_date,
|
||||
c.sign_date, c.amount, c.deposit, c.payment_cycle, c.attachment,
|
||||
c.sign_date, c.amount AS amount_fen, c.deposit AS deposit_fen, c.payment_cycle, c.attachment,
|
||||
c.status, c.remark, c.created_at,
|
||||
DATEDIFF(c.end_date, CURDATE()) AS days_left
|
||||
FROM t_contract c
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
SELECT c.id, c.project_id, c.contract_no, c.contract_name, c.contract_type_id,
|
||||
ct.type_name AS contract_type_name, c.party_a, c.party_b,
|
||||
c.party_b_type, c.party_b_id, c.room_ids, c.start_date, c.end_date,
|
||||
c.sign_date, c.amount, c.deposit, c.payment_cycle, c.attachment,
|
||||
c.sign_date, c.amount AS amount_fen, c.deposit AS deposit_fen, c.payment_cycle, c.attachment,
|
||||
c.status, c.remark, c.created_at,
|
||||
DATEDIFF(c.end_date, CURDATE()) AS days_left
|
||||
FROM t_contract c
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<select id="selectMaintenancePage" resultType="com.pms.base.dto.DeviceMaintenanceDTO">
|
||||
SELECT m.id, m.project_id, m.device_id, d.device_name,
|
||||
m.maintenance_type, m.maintenance_date, m.plan_date,
|
||||
m.handler, m.cost, m.result, m.images, m.status, m.remark, m.created_at
|
||||
m.handler, m.cost AS cost_fen, m.result, m.images, m.status, m.remark, m.created_at
|
||||
FROM t_device_maintenance m
|
||||
LEFT JOIN t_device d ON m.device_id = d.id AND d.deleted = 0
|
||||
WHERE m.deleted = 0
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
<select id="selectExpiringMaintenances" resultType="com.pms.base.dto.DeviceMaintenanceDTO">
|
||||
SELECT m.id, m.project_id, m.device_id, d.device_name,
|
||||
m.maintenance_type, m.maintenance_date, m.plan_date,
|
||||
m.handler, m.cost, m.result, m.images, m.status, m.remark, m.created_at
|
||||
m.handler, m.cost AS cost_fen, m.result, m.images, m.status, m.remark, m.created_at
|
||||
FROM t_device_maintenance m
|
||||
LEFT JOIN t_device d ON m.device_id = d.id AND d.deleted = 0
|
||||
WHERE m.deleted = 0 AND m.status = 0
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
c.category_name, d.brand, d.model, d.spec, d.location_type,
|
||||
d.building_id, b.building_name, d.floor_id, f.floor_name,
|
||||
d.room_id, d.location_desc, d.purchase_date, d.install_date,
|
||||
d.warranty_until, d.supplier, d.price, d.qr_code, d.status, d.remark
|
||||
d.warranty_until, d.supplier, d.price AS price_fen, d.qr_code, d.status, d.remark
|
||||
FROM t_device d
|
||||
LEFT JOIN t_device_category c ON d.category_id = c.id AND c.deleted = 0
|
||||
LEFT JOIN t_building b ON d.building_id = b.id AND b.deleted = 0
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
c.category_name, d.brand, d.model, d.spec, d.location_type,
|
||||
d.building_id, b.building_name, d.floor_id, f.floor_name,
|
||||
d.room_id, d.location_desc, d.purchase_date, d.install_date,
|
||||
d.warranty_until, d.supplier, d.price, d.qr_code, d.status, d.remark
|
||||
d.warranty_until, d.supplier, d.price AS price_fen, d.qr_code, d.status, d.remark
|
||||
FROM t_device d
|
||||
LEFT JOIN t_device_category c ON d.category_id = c.id AND c.deleted = 0
|
||||
LEFT JOIN t_building b ON d.building_id = b.id AND b.deleted = 0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
package com.pms.base.controller;
|
||||
|
||||
import com.pms.common.annotation.AuditLog;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @AuditLog 覆盖率守卫测试
|
||||
* <p>
|
||||
* 反射扫描所有 Controller 写方法(@PostMapping/@PutMapping/@DeleteMapping/@PatchMapping),
|
||||
* 断言每个写方法都标注了 @AuditLog,防止后续新增写方法漏标审计。
|
||||
* <p>
|
||||
* 豁免清单({@link #EXEMPT_WRITE_METHODS}):内部 Feign 查询接口 @PostMapping 语义为查询,不纳入审计。
|
||||
* <p>
|
||||
* PII 守卫({@link #PII_METHODS}):涉及身份证号/手机号/邮箱的方法必须 recordParams=false,
|
||||
* 防止敏感信息明文写入审计日志。
|
||||
*/
|
||||
@DisplayName("@AuditLog 覆盖率守卫测试")
|
||||
class AuditLogCoverageTest {
|
||||
|
||||
/** 所有业务 Controller(不含 InternalController 内部 Feign 接口) */
|
||||
private static final List<Class<?>> CONTROLLERS = List.of(
|
||||
ApprovalFlowConfigController.class,
|
||||
ArchiveController.class,
|
||||
BuildingController.class,
|
||||
CamChargeController.class,
|
||||
CommunityActivityController.class,
|
||||
ContractController.class,
|
||||
ContractTypeController.class,
|
||||
DeviceCategoryController.class,
|
||||
DeviceController.class,
|
||||
DeviceMaintenanceController.class,
|
||||
EnergyMeterController.class,
|
||||
EnterpriseController.class,
|
||||
EnterpriseProfileController.class,
|
||||
EnterpriseServiceController.class,
|
||||
FloorController.class,
|
||||
LeaseContractController.class,
|
||||
LifecycleController.class,
|
||||
MeetingRoomController.class,
|
||||
OwnerCommitteeController.class,
|
||||
OwnerController.class,
|
||||
OwnerRoomController.class,
|
||||
ProjectController.class,
|
||||
PublicRevenueController.class,
|
||||
RenovationController.class,
|
||||
RoomController.class,
|
||||
SafetyInspectionController.class,
|
||||
TenantController.class,
|
||||
WorkshopLeaseController.class
|
||||
);
|
||||
|
||||
/** 豁免审计的写方法(className#methodName),仅内部 Feign 查询接口 */
|
||||
private static final List<String> EXEMPT_WRITE_METHODS = List.of(
|
||||
"InternalController#batchGetRooms"
|
||||
);
|
||||
|
||||
/** PII 方法必须 recordParams=false(className#methodName) */
|
||||
private static final List<String> PII_METHODS = List.of(
|
||||
"OwnerController#create",
|
||||
"OwnerController#update",
|
||||
"OwnerController#importOwners",
|
||||
"TenantController#create",
|
||||
"TenantController#update",
|
||||
"EnterpriseController#create",
|
||||
"EnterpriseController#update",
|
||||
"EnterpriseProfileController#create",
|
||||
"EnterpriseProfileController#update",
|
||||
"EnterpriseServiceController#apply",
|
||||
"EnterpriseServiceController#process",
|
||||
"OwnerCommitteeController#create",
|
||||
"OwnerCommitteeController#update",
|
||||
"ProjectController#create",
|
||||
"ProjectController#update",
|
||||
"RenovationController#apply",
|
||||
"WorkshopLeaseController#create",
|
||||
"WorkshopLeaseController#update"
|
||||
);
|
||||
|
||||
static Stream<Arguments> writeMethods() {
|
||||
return CONTROLLERS.stream()
|
||||
.flatMap(c -> Arrays.stream(c.getDeclaredMethods())
|
||||
.filter(AuditLogCoverageTest::isWriteMethod)
|
||||
.map(m -> Arguments.of(c.getSimpleName(), m.getName(), m)));
|
||||
}
|
||||
|
||||
private static boolean isWriteMethod(Method m) {
|
||||
return m.isAnnotationPresent(PostMapping.class)
|
||||
|| m.isAnnotationPresent(PutMapping.class)
|
||||
|| m.isAnnotationPresent(DeleteMapping.class)
|
||||
|| m.isAnnotationPresent(PatchMapping.class);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}#{1} 必须有 @AuditLog")
|
||||
@MethodSource("writeMethods")
|
||||
void writeMethod_mustHaveAuditLog(String className, String methodName, Method method) {
|
||||
if (EXEMPT_WRITE_METHODS.contains(className + "#" + methodName)) {
|
||||
return;
|
||||
}
|
||||
assertThat(method.isAnnotationPresent(AuditLog.class))
|
||||
.as("%s#%s 是写方法但缺少 @AuditLog 注解", className, methodName)
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "PII 方法 {0}#{1} 必须 recordParams=false")
|
||||
@MethodSource("piiMethods")
|
||||
void piiMethod_mustDisableRecordParams(String className, String methodName) {
|
||||
Method method = findMethod(className, methodName);
|
||||
AuditLog auditLog = method.getAnnotation(AuditLog.class);
|
||||
assertThat(auditLog)
|
||||
.as("%s#%s 必须标注 @AuditLog", className, methodName)
|
||||
.isNotNull();
|
||||
assertThat(auditLog.recordParams())
|
||||
.as("%s#%s 涉及 PII,@AuditLog 必须 recordParams=false", className, methodName)
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
static Stream<Arguments> piiMethods() {
|
||||
return PII_METHODS.stream().map(s -> {
|
||||
String[] parts = s.split("#");
|
||||
return Arguments.of(parts[0], parts[1]);
|
||||
});
|
||||
}
|
||||
|
||||
private Method findMethod(String className, String methodName) {
|
||||
return CONTROLLERS.stream()
|
||||
.filter(c -> c.getSimpleName().equals(className))
|
||||
.findFirst()
|
||||
.flatMap(c -> Arrays.stream(c.getDeclaredMethods())
|
||||
.filter(m -> m.getName().equals(methodName))
|
||||
.findFirst())
|
||||
.orElseThrow(() -> new IllegalStateException("Method not found: " + className + "#" + methodName));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("覆盖率统计:所有写方法均已纳入守卫")
|
||||
void coverageStats() {
|
||||
long totalWrite = CONTROLLERS.stream()
|
||||
.mapToLong(c -> Arrays.stream(c.getDeclaredMethods()).filter(AuditLogCoverageTest::isWriteMethod).count())
|
||||
.sum();
|
||||
assertThat(totalWrite).isGreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.pms.base.feign;
|
||||
|
||||
import com.pms.common.exception.ServiceCallException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* AuthClient fallback 单元测试
|
||||
* <p>
|
||||
* 验证 auth-service 不可用时 fallback 抛 ServiceCallException 而非返回 null。
|
||||
*/
|
||||
class AuthClientFallbackTest {
|
||||
|
||||
@Test
|
||||
void getUser_fallback_throwsServiceCallException() {
|
||||
AuthClient.AuthClientFallbackFactory factory = new AuthClient.AuthClientFallbackFactory();
|
||||
AuthClient fallback = factory.create(new RuntimeException("connection refused"));
|
||||
|
||||
assertThatThrownBy(() -> fallback.getUser(1L))
|
||||
.isInstanceOf(ServiceCallException.class)
|
||||
.hasMessageContaining("认证服务暂不可用");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUser_fallback_preservesCause() {
|
||||
Throwable cause = new RuntimeException("timeout");
|
||||
AuthClient.AuthClientFallbackFactory factory = new AuthClient.AuthClientFallbackFactory();
|
||||
AuthClient fallback = factory.create(cause);
|
||||
|
||||
assertThatThrownBy(() -> fallback.getUser(99L))
|
||||
.isInstanceOf(ServiceCallException.class)
|
||||
.hasCause(cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import com.pms.base.mapper.BuildingMapper;
|
|||
import com.pms.base.mapper.FloorMapper;
|
||||
import com.pms.base.mapper.RoomMapper;
|
||||
import com.pms.base.service.impl.BuildingServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.exception.ErrorCode;
|
||||
import com.pms.common.response.PageResult;
|
||||
|
|
@ -54,6 +55,9 @@ class BuildingServiceTest {
|
|||
@Mock
|
||||
private RoomMapper roomMapper;
|
||||
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private BuildingServiceImpl buildingService;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
package com.pms.base.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.pms.base.dto.ContractRenewRequest;
|
||||
import com.pms.base.dto.ContractSaveRequest;
|
||||
import com.pms.base.entity.Contract;
|
||||
import com.pms.base.mapper.ContractMapper;
|
||||
import com.pms.base.service.impl.ContractServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.security.UserContext;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 合同服务测试
|
||||
*/
|
||||
@DisplayName("合同服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class ContractServiceImplTest {
|
||||
|
||||
private static final Long PROJECT_ID = 2001L;
|
||||
private static final Long USER_ID = 1001L;
|
||||
|
||||
@Mock
|
||||
private ContractMapper contractMapper;
|
||||
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private ContractServiceImpl contractService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
UserContext.set(new UserContext.CurrentUser(USER_ID, "admin", PROJECT_ID, "ROLE_ADMIN", "1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建合同:编号重复抛 RESOURCE_EXISTS")
|
||||
void create_duplicateContractNo_throwsException() {
|
||||
when(contractMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
|
||||
|
||||
ContractSaveRequest request = new ContractSaveRequest();
|
||||
request.setContractNo("C001");
|
||||
request.setContractName("租赁合同");
|
||||
|
||||
assertThatThrownBy(() -> contractService.create(request))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("合同编号已存在");
|
||||
|
||||
verify(contractMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建合同:成功返回 ID,roomIds 转逗号分隔,金额(分)正确")
|
||||
void create_normal_returnsId() {
|
||||
when(contractMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
|
||||
when(contractMapper.insert(any(Contract.class))).thenAnswer(inv -> {
|
||||
Contract c = inv.getArgument(0);
|
||||
c.setId(6001L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
ContractSaveRequest request = new ContractSaveRequest();
|
||||
request.setContractNo("C001");
|
||||
request.setContractName("租赁合同");
|
||||
request.setRoomIds(List.of(101L, 102L, 103L));
|
||||
request.setAmountFen(1200000L);
|
||||
request.setDepositFen(200000L);
|
||||
|
||||
Long id = contractService.create(request);
|
||||
|
||||
assertThat(id).isEqualTo(6001L);
|
||||
ArgumentCaptor<Contract> captor = ArgumentCaptor.forClass(Contract.class);
|
||||
verify(contractMapper).insert(captor.capture());
|
||||
Contract saved = captor.getValue();
|
||||
assertThat(saved.getContractNo()).isEqualTo("C001");
|
||||
assertThat(saved.getRoomIds()).isEqualTo("101,102,103");
|
||||
assertThat(saved.getAmountFen()).isEqualTo(1200000L);
|
||||
assertThat(saved.getDepositFen()).isEqualTo(200000L);
|
||||
assertThat(saved.getProjectId()).isEqualTo(PROJECT_ID);
|
||||
assertThat(saved.getStatus()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询合同:不存在抛异常")
|
||||
void getById_notFound_throwsException() {
|
||||
when(contractMapper.selectContractDetail(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> contractService.getById(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("续签合同:原合同置为续签态(4)并创建新合同")
|
||||
void renew_normal_createsNewContract() {
|
||||
Contract existing = new Contract();
|
||||
existing.setId(6001L);
|
||||
existing.setProjectId(PROJECT_ID);
|
||||
existing.setContractNo("C001");
|
||||
existing.setContractName("租赁合同");
|
||||
existing.setEndDate(LocalDate.of(2026, 12, 31));
|
||||
existing.setAmountFen(1200000L);
|
||||
existing.setDepositFen(200000L);
|
||||
when(contractMapper.selectById(6001L)).thenReturn(existing);
|
||||
when(contractMapper.insert(any(Contract.class))).thenAnswer(inv -> {
|
||||
Contract c = inv.getArgument(0);
|
||||
c.setId(6002L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
ContractRenewRequest request = new ContractRenewRequest();
|
||||
request.setNewEndDate(LocalDate.of(2027, 12, 31));
|
||||
request.setSignDate(LocalDate.of(2026, 11, 1));
|
||||
|
||||
Long newId = contractService.renew(6001L, request);
|
||||
|
||||
assertThat(newId).isEqualTo(6002L);
|
||||
// 原合同状态置为 4
|
||||
ArgumentCaptor<Contract> updateCaptor = ArgumentCaptor.forClass(Contract.class);
|
||||
verify(contractMapper).updateById(updateCaptor.capture());
|
||||
assertThat(updateCaptor.getValue().getStatus()).isEqualTo(4);
|
||||
// 新合同插入
|
||||
ArgumentCaptor<Contract> insertCaptor = ArgumentCaptor.forClass(Contract.class);
|
||||
verify(contractMapper).insert(insertCaptor.capture());
|
||||
Contract newContract = insertCaptor.getValue();
|
||||
assertThat(newContract.getContractNo()).isEqualTo("C001-R");
|
||||
assertThat(newContract.getStatus()).isEqualTo(1);
|
||||
assertThat(newContract.getStartDate()).isEqualTo(LocalDate.of(2026, 12, 31));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除合同:不存在抛异常")
|
||||
void delete_notFound_throwsException() {
|
||||
when(contractMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> contractService.delete(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(contractMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package com.pms.base.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.pms.base.dto.ContractTypeSaveRequest;
|
||||
import com.pms.base.entity.ContractType;
|
||||
import com.pms.base.mapper.ContractTypeMapper;
|
||||
import com.pms.base.service.impl.ContractTypeServiceImpl;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.security.UserContext;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 合同类型服务测试
|
||||
*/
|
||||
@DisplayName("合同类型服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class ContractTypeServiceImplTest {
|
||||
|
||||
private static final Long PROJECT_ID = 2001L;
|
||||
private static final Long USER_ID = 1001L;
|
||||
|
||||
@Mock
|
||||
private ContractTypeMapper contractTypeMapper;
|
||||
|
||||
@InjectMocks
|
||||
private ContractTypeServiceImpl contractTypeService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
UserContext.set(new UserContext.CurrentUser(USER_ID, "admin", PROJECT_ID, "ROLE_ADMIN", "1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建合同类型:编码重复抛 RESOURCE_EXISTS")
|
||||
void create_duplicateCode_throwsException() {
|
||||
when(contractTypeMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
|
||||
|
||||
ContractTypeSaveRequest request = new ContractTypeSaveRequest();
|
||||
request.setTypeCode("CT001");
|
||||
request.setTypeName("租赁合同");
|
||||
|
||||
assertThatThrownBy(() -> contractTypeService.create(request))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("类型编码已存在");
|
||||
|
||||
verify(contractTypeMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建合同类型:成功返回 ID")
|
||||
void create_normal_returnsId() {
|
||||
when(contractTypeMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
|
||||
when(contractTypeMapper.insert(any(ContractType.class))).thenAnswer(inv -> {
|
||||
ContractType t = inv.getArgument(0);
|
||||
t.setId(4001L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
ContractTypeSaveRequest request = new ContractTypeSaveRequest();
|
||||
request.setTypeCode("CT001");
|
||||
request.setTypeName("租赁合同");
|
||||
|
||||
Long id = contractTypeService.create(request);
|
||||
|
||||
assertThat(id).isEqualTo(4001L);
|
||||
ArgumentCaptor<ContractType> captor = ArgumentCaptor.forClass(ContractType.class);
|
||||
verify(contractTypeMapper).insert(captor.capture());
|
||||
ContractType saved = captor.getValue();
|
||||
assertThat(saved.getTypeCode()).isEqualTo("CT001");
|
||||
assertThat(saved.getProjectId()).isEqualTo(PROJECT_ID);
|
||||
assertThat(saved.getCreatedBy()).isEqualTo(USER_ID);
|
||||
assertThat(saved.getStatus()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新合同类型:不存在抛异常")
|
||||
void update_notFound_throwsException() {
|
||||
when(contractTypeMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> contractTypeService.update(999L, new ContractTypeSaveRequest()))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(contractTypeMapper, never()).updateById(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除合同类型:不存在抛异常")
|
||||
void delete_notFound_throwsException() {
|
||||
when(contractTypeMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> contractTypeService.delete(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(contractTypeMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package com.pms.base.service;
|
||||
|
||||
import com.pms.base.dto.DeviceCategoryDTO;
|
||||
import com.pms.base.dto.DeviceCategorySaveRequest;
|
||||
import com.pms.base.entity.DeviceCategory;
|
||||
import com.pms.base.mapper.DeviceCategoryMapper;
|
||||
import com.pms.base.service.impl.DeviceCategoryServiceImpl;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.security.UserContext;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 设备分类服务测试
|
||||
*/
|
||||
@DisplayName("设备分类服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class DeviceCategoryServiceImplTest {
|
||||
|
||||
private static final Long PROJECT_ID = 2001L;
|
||||
private static final Long USER_ID = 1001L;
|
||||
|
||||
@Mock
|
||||
private DeviceCategoryMapper deviceCategoryMapper;
|
||||
|
||||
@InjectMocks
|
||||
private DeviceCategoryServiceImpl deviceCategoryService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
UserContext.set(new UserContext.CurrentUser(USER_ID, "admin", PROJECT_ID, "ROLE_ADMIN", "1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建分类:parentId 为空时默认 0")
|
||||
void create_normal_returnsId() {
|
||||
when(deviceCategoryMapper.insert(any(DeviceCategory.class))).thenAnswer(inv -> {
|
||||
DeviceCategory c = inv.getArgument(0);
|
||||
c.setId(3001L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
DeviceCategorySaveRequest request = new DeviceCategorySaveRequest();
|
||||
request.setCategoryCode("DC001");
|
||||
request.setCategoryName("电梯设备");
|
||||
|
||||
Long id = deviceCategoryService.create(request);
|
||||
|
||||
assertThat(id).isEqualTo(3001L);
|
||||
ArgumentCaptor<DeviceCategory> captor = ArgumentCaptor.forClass(DeviceCategory.class);
|
||||
verify(deviceCategoryMapper).insert(captor.capture());
|
||||
DeviceCategory saved = captor.getValue();
|
||||
assertThat(saved.getParentId()).isEqualTo(0L);
|
||||
assertThat(saved.getProjectId()).isEqualTo(PROJECT_ID);
|
||||
assertThat(saved.getCategoryCode()).isEqualTo("DC001");
|
||||
assertThat(saved.getStatus()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新分类:不存在抛异常")
|
||||
void update_notFound_throwsException() {
|
||||
when(deviceCategoryMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> deviceCategoryService.update(999L, new DeviceCategorySaveRequest()))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(deviceCategoryMapper, never()).updateById(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除分类:存在子分类时抛异常")
|
||||
void delete_withChildren_throwsException() {
|
||||
DeviceCategory existing = new DeviceCategory();
|
||||
existing.setId(1L);
|
||||
when(deviceCategoryMapper.selectById(1L)).thenReturn(existing);
|
||||
when(deviceCategoryMapper.countChildren(1L)).thenReturn(2);
|
||||
|
||||
assertThatThrownBy(() -> deviceCategoryService.delete(1L))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("存在子分类");
|
||||
|
||||
verify(deviceCategoryMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除分类:无子分类时成功")
|
||||
void delete_normal_success() {
|
||||
DeviceCategory existing = new DeviceCategory();
|
||||
existing.setId(1L);
|
||||
when(deviceCategoryMapper.selectById(1L)).thenReturn(existing);
|
||||
when(deviceCategoryMapper.countChildren(1L)).thenReturn(0);
|
||||
|
||||
deviceCategoryService.delete(1L);
|
||||
|
||||
verify(deviceCategoryMapper).deleteById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("tree:构建父子层级结构")
|
||||
void tree_normal_returnsHierarchy() {
|
||||
DeviceCategory root = new DeviceCategory();
|
||||
root.setId(1L);
|
||||
root.setParentId(0L);
|
||||
root.setCategoryCode("ROOT");
|
||||
root.setCategoryName("根分类");
|
||||
root.setSort(1);
|
||||
root.setStatus(1);
|
||||
|
||||
DeviceCategory child = new DeviceCategory();
|
||||
child.setId(2L);
|
||||
child.setParentId(1L);
|
||||
child.setCategoryCode("CHILD");
|
||||
child.setCategoryName("子分类");
|
||||
child.setSort(1);
|
||||
child.setStatus(1);
|
||||
|
||||
when(deviceCategoryMapper.selectAllCategories(PROJECT_ID))
|
||||
.thenReturn(List.of(root, child));
|
||||
|
||||
List<DeviceCategoryDTO> tree = deviceCategoryService.tree(PROJECT_ID);
|
||||
|
||||
assertThat(tree).hasSize(1);
|
||||
assertThat(tree.get(0).getCategoryName()).isEqualTo("根分类");
|
||||
assertThat(tree.get(0).getChildren()).hasSize(1);
|
||||
assertThat(tree.get(0).getChildren().get(0).getCategoryName()).isEqualTo("子分类");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package com.pms.base.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pms.base.dto.DeviceMaintenanceDTO;
|
||||
import com.pms.base.dto.DeviceMaintenanceSaveRequest;
|
||||
import com.pms.base.entity.Device;
|
||||
import com.pms.base.entity.DeviceMaintenance;
|
||||
import com.pms.base.mapper.DeviceMaintenanceMapper;
|
||||
import com.pms.base.mapper.DeviceMapper;
|
||||
import com.pms.base.service.impl.DeviceMaintenanceServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.security.UserContext;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 设备维保服务测试
|
||||
*/
|
||||
@DisplayName("设备维保服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class DeviceMaintenanceServiceImplTest {
|
||||
|
||||
private static final Long PROJECT_ID = 2001L;
|
||||
private static final Long USER_ID = 1001L;
|
||||
|
||||
@Mock
|
||||
private DeviceMaintenanceMapper maintenanceMapper;
|
||||
|
||||
@Mock
|
||||
private DeviceMapper deviceMapper;
|
||||
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private DeviceMaintenanceServiceImpl maintenanceService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
UserContext.set(new UserContext.CurrentUser(USER_ID, "admin", PROJECT_ID, "ROLE_ADMIN", "1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建维保记录:设备不存在抛异常")
|
||||
void create_deviceNotFound_throwsException() {
|
||||
when(deviceMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> maintenanceService.create(999L, new DeviceMaintenanceSaveRequest()))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(maintenanceMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建维保记录:成功返回 ID,projectId 取自设备")
|
||||
void create_normal_returnsId() {
|
||||
Device device = new Device();
|
||||
device.setId(9001L);
|
||||
device.setProjectId(PROJECT_ID);
|
||||
device.setDeviceName("电梯A");
|
||||
when(deviceMapper.selectById(9001L)).thenReturn(device);
|
||||
when(maintenanceMapper.insert(any(DeviceMaintenance.class))).thenAnswer(inv -> {
|
||||
DeviceMaintenance m = inv.getArgument(0);
|
||||
m.setId(5001L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
DeviceMaintenanceSaveRequest request = new DeviceMaintenanceSaveRequest();
|
||||
request.setMaintenanceType(1);
|
||||
request.setCostFen(20000L);
|
||||
request.setImages(List.of("img1.jpg", "img2.jpg"));
|
||||
|
||||
Long id = maintenanceService.create(9001L, request);
|
||||
|
||||
assertThat(id).isEqualTo(5001L);
|
||||
ArgumentCaptor<DeviceMaintenance> captor = ArgumentCaptor.forClass(DeviceMaintenance.class);
|
||||
verify(maintenanceMapper).insert(captor.capture());
|
||||
DeviceMaintenance saved = captor.getValue();
|
||||
assertThat(saved.getDeviceId()).isEqualTo(9001L);
|
||||
assertThat(saved.getProjectId()).isEqualTo(PROJECT_ID);
|
||||
assertThat(saved.getCostFen()).isEqualTo(20000L);
|
||||
assertThat(saved.getImages()).isEqualTo("img1.jpg,img2.jpg");
|
||||
assertThat(saved.getStatus()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询维保详情:填充设备名称与图片列表")
|
||||
void getById_fillsDeviceName() {
|
||||
DeviceMaintenance maintenance = new DeviceMaintenance();
|
||||
maintenance.setId(5001L);
|
||||
maintenance.setProjectId(PROJECT_ID);
|
||||
maintenance.setDeviceId(9001L);
|
||||
maintenance.setImages("a.jpg,b.jpg");
|
||||
when(maintenanceMapper.selectById(5001L)).thenReturn(maintenance);
|
||||
|
||||
Device device = new Device();
|
||||
device.setId(9001L);
|
||||
device.setDeviceName("电梯A");
|
||||
when(deviceMapper.selectById(9001L)).thenReturn(device);
|
||||
|
||||
DeviceMaintenanceDTO result = maintenanceService.getById(5001L);
|
||||
|
||||
assertThat(result.getDeviceName()).isEqualTo("电梯A");
|
||||
assertThat(result.getImageList()).containsExactly("a.jpg", "b.jpg");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("分页查询返回结果")
|
||||
void page_normal_returnsResult() {
|
||||
DeviceMaintenanceDTO query = new DeviceMaintenanceDTO();
|
||||
query.setPage(1);
|
||||
query.setSize(10);
|
||||
|
||||
DeviceMaintenanceDTO dto = new DeviceMaintenanceDTO();
|
||||
dto.setId(5001L);
|
||||
Page<DeviceMaintenanceDTO> mockPage = new Page<>(1, 10);
|
||||
mockPage.setRecords(List.of(dto));
|
||||
mockPage.setTotal(1);
|
||||
|
||||
when(maintenanceMapper.selectMaintenancePage(any(Page.class), any(), any(), any(), any()))
|
||||
.thenReturn(mockPage);
|
||||
|
||||
PageResult<DeviceMaintenanceDTO> result = maintenanceService.page(query, null);
|
||||
|
||||
assertThat(result.getList()).hasSize(1);
|
||||
assertThat(result.getTotal()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package com.pms.base.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pms.base.dto.DeviceDTO;
|
||||
import com.pms.base.dto.DeviceSaveRequest;
|
||||
import com.pms.base.entity.Device;
|
||||
import com.pms.base.mapper.DeviceMapper;
|
||||
import com.pms.base.service.impl.DeviceServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.security.UserContext;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 设备服务测试
|
||||
*/
|
||||
@DisplayName("设备服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class DeviceServiceImplTest {
|
||||
|
||||
private static final Long PROJECT_ID = 2001L;
|
||||
private static final Long USER_ID = 1001L;
|
||||
|
||||
@Mock
|
||||
private DeviceMapper deviceMapper;
|
||||
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private DeviceServiceImpl deviceService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
UserContext.set(new UserContext.CurrentUser(USER_ID, "admin", PROJECT_ID, "ROLE_ADMIN", "1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建设备:编号重复抛 RESOURCE_EXISTS")
|
||||
void create_duplicateCode_throwsException() {
|
||||
when(deviceMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
|
||||
|
||||
DeviceSaveRequest request = new DeviceSaveRequest();
|
||||
request.setDeviceCode("D001");
|
||||
request.setDeviceName("电梯A");
|
||||
|
||||
assertThatThrownBy(() -> deviceService.create(request))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("设备编号已存在");
|
||||
|
||||
verify(deviceMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建设备:成功返回 ID 且金额字段(分)正确传递")
|
||||
void create_normal_returnsId() {
|
||||
when(deviceMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
|
||||
when(deviceMapper.insert(any(Device.class))).thenAnswer(inv -> {
|
||||
Device d = inv.getArgument(0);
|
||||
d.setId(9001L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
DeviceSaveRequest request = new DeviceSaveRequest();
|
||||
request.setDeviceCode("D001");
|
||||
request.setDeviceName("电梯A");
|
||||
request.setPriceFen(5000000L); // 50000.00 元
|
||||
|
||||
Long id = deviceService.create(request);
|
||||
|
||||
assertThat(id).isEqualTo(9001L);
|
||||
ArgumentCaptor<Device> captor = ArgumentCaptor.forClass(Device.class);
|
||||
verify(deviceMapper).insert(captor.capture());
|
||||
Device saved = captor.getValue();
|
||||
assertThat(saved.getDeviceCode()).isEqualTo("D001");
|
||||
assertThat(saved.getPriceFen()).isEqualTo(5000000L);
|
||||
assertThat(saved.getProjectId()).isEqualTo(PROJECT_ID);
|
||||
assertThat(saved.getCreatedBy()).isEqualTo(USER_ID);
|
||||
assertThat(saved.getStatus()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新设备:不存在抛异常")
|
||||
void update_notFound_throwsException() {
|
||||
when(deviceMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> deviceService.update(999L, new DeviceSaveRequest()))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(deviceMapper, never()).updateById(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除设备:不存在抛异常")
|
||||
void delete_notFound_throwsException() {
|
||||
when(deviceMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> deviceService.delete(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(deviceMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新设备状态:成功更新 status 字段")
|
||||
void updateStatus_normal_success() {
|
||||
Device existing = new Device();
|
||||
existing.setId(9001L);
|
||||
when(deviceMapper.selectById(9001L)).thenReturn(existing);
|
||||
|
||||
deviceService.updateStatus(9001L, 3);
|
||||
|
||||
ArgumentCaptor<Device> captor = ArgumentCaptor.forClass(Device.class);
|
||||
verify(deviceMapper).updateById(captor.capture());
|
||||
assertThat(captor.getValue().getStatus()).isEqualTo(3);
|
||||
assertThat(captor.getValue().getUpdatedBy()).isEqualTo(USER_ID);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
package com.pms.base.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pms.base.dto.EnterpriseDTO;
|
||||
import com.pms.base.dto.EnterpriseSaveRequest;
|
||||
import com.pms.base.entity.Enterprise;
|
||||
import com.pms.base.mapper.EnterpriseMapper;
|
||||
import com.pms.base.service.impl.EnterpriseServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.security.UserContext;
|
||||
import com.pms.common.util.CryptoUtil;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 企业服务测试(含联系电话/邮箱加密验证)
|
||||
*/
|
||||
@DisplayName("企业服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class EnterpriseServiceImplTest {
|
||||
|
||||
private static final Long PROJECT_ID = 2001L;
|
||||
private static final Long USER_ID = 1001L;
|
||||
|
||||
@Mock
|
||||
private EnterpriseMapper enterpriseMapper;
|
||||
|
||||
@Mock
|
||||
private CryptoUtil cryptoUtil;
|
||||
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private EnterpriseServiceImpl enterpriseService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
UserContext.set(new UserContext.CurrentUser(USER_ID, "admin", PROJECT_ID, "ROLE_ADMIN", "1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建企业:联系电话/邮箱加密并填充 hash")
|
||||
void create_withContactPhoneAndEmail_encrypts() {
|
||||
when(cryptoUtil.encrypt(anyString())).thenReturn("encrypted");
|
||||
when(cryptoUtil.hash(anyString())).thenReturn("hash");
|
||||
when(enterpriseMapper.insert(any(Enterprise.class))).thenAnswer(inv -> {
|
||||
Enterprise e = inv.getArgument(0);
|
||||
e.setId(7001L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
EnterpriseSaveRequest request = new EnterpriseSaveRequest();
|
||||
request.setEnterpriseName("智造科技");
|
||||
request.setCreditCode("91110000MA01");
|
||||
request.setLegalPerson("李四");
|
||||
request.setContactPerson("王五");
|
||||
request.setContactPhone("13800001111");
|
||||
request.setEmail("a@b.com");
|
||||
|
||||
Long id = enterpriseService.create(request);
|
||||
|
||||
assertThat(id).isEqualTo(7001L);
|
||||
ArgumentCaptor<Enterprise> captor = ArgumentCaptor.forClass(Enterprise.class);
|
||||
verify(enterpriseMapper).insert(captor.capture());
|
||||
Enterprise saved = captor.getValue();
|
||||
assertThat(saved.getProjectId()).isEqualTo(PROJECT_ID);
|
||||
assertThat(saved.getEnterpriseName()).isEqualTo("智造科技");
|
||||
assertThat(saved.getContactPhone()).isEqualTo("encrypted");
|
||||
assertThat(saved.getContactPhoneHash()).isEqualTo("hash");
|
||||
assertThat(saved.getContactPhoneLast4Hash()).isEqualTo("hash");
|
||||
assertThat(saved.getEmail()).isEqualTo("encrypted");
|
||||
assertThat(saved.getEmailLast4Hash()).isEqualTo("hash");
|
||||
assertThat(saved.getCreatedBy()).isEqualTo(USER_ID);
|
||||
assertThat(saved.getStatus()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询企业详情:解密联系电话/邮箱")
|
||||
void getById_decryptsContactPhoneAndEmail() {
|
||||
Enterprise entity = new Enterprise();
|
||||
entity.setId(7001L);
|
||||
entity.setProjectId(PROJECT_ID);
|
||||
entity.setEnterpriseName("智造科技");
|
||||
entity.setContactPhone("enc-phone");
|
||||
entity.setEmail("enc-email");
|
||||
when(enterpriseMapper.selectById(7001L)).thenReturn(entity);
|
||||
when(cryptoUtil.decrypt(anyString())).thenReturn("decrypted");
|
||||
|
||||
EnterpriseDTO result = enterpriseService.getById(7001L);
|
||||
|
||||
assertThat(result.getContactPhone()).isEqualTo("decrypted");
|
||||
assertThat(result.getEmail()).isEqualTo("decrypted");
|
||||
assertThat(result.getEnterpriseName()).isEqualTo("智造科技");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询企业:不存在抛异常")
|
||||
void getById_notFound_throwsException() {
|
||||
when(enterpriseMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> enterpriseService.getById(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新企业:不存在抛异常")
|
||||
void update_notFound_throwsException() {
|
||||
when(enterpriseMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> enterpriseService.update(999L, new EnterpriseSaveRequest()))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(enterpriseMapper, never()).updateById(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除企业:不存在抛异常")
|
||||
void delete_notFound_throwsException() {
|
||||
when(enterpriseMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> enterpriseService.delete(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(enterpriseMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("分页查询返回结果")
|
||||
void page_normal_returnsResult() {
|
||||
EnterpriseDTO query = new EnterpriseDTO();
|
||||
query.setPage(1);
|
||||
query.setSize(10);
|
||||
|
||||
EnterpriseDTO dto = new EnterpriseDTO();
|
||||
dto.setId(1L);
|
||||
dto.setEnterpriseName("智造科技");
|
||||
Page<EnterpriseDTO> mockPage = new Page<>(1, 10);
|
||||
mockPage.setRecords(List.of(dto));
|
||||
mockPage.setTotal(1);
|
||||
|
||||
when(enterpriseMapper.selectEnterprisePage(any(Page.class), any(), any()))
|
||||
.thenReturn(mockPage);
|
||||
|
||||
PageResult<EnterpriseDTO> result = enterpriseService.page(query);
|
||||
|
||||
assertThat(result.getList()).hasSize(1);
|
||||
assertThat(result.getTotal()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package com.pms.base.service;
|
||||
|
||||
import com.pms.base.dto.FloorSaveRequest;
|
||||
import com.pms.base.entity.Building;
|
||||
import com.pms.base.entity.Floor;
|
||||
import com.pms.base.mapper.BuildingMapper;
|
||||
import com.pms.base.mapper.FloorMapper;
|
||||
import com.pms.base.service.impl.FloorServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.security.UserContext;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 楼层服务测试
|
||||
*/
|
||||
@DisplayName("楼层服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class FloorServiceImplTest {
|
||||
|
||||
private static final Long PROJECT_ID = 2001L;
|
||||
private static final Long USER_ID = 1001L;
|
||||
|
||||
@Mock
|
||||
private FloorMapper floorMapper;
|
||||
|
||||
@Mock
|
||||
private BuildingMapper buildingMapper;
|
||||
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private FloorServiceImpl floorService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
UserContext.set(new UserContext.CurrentUser(USER_ID, "admin", PROJECT_ID, "ROLE_ADMIN", "1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建楼层:楼栋不存在抛 BUILDING_NOT_FOUND")
|
||||
void create_buildingNotFound_throwsException() {
|
||||
when(buildingMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
FloorSaveRequest request = new FloorSaveRequest();
|
||||
request.setBuildingId(999L);
|
||||
request.setFloorNo(1);
|
||||
|
||||
assertThatThrownBy(() -> floorService.create(request))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(floorMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建楼层:未提供名称时自动生成 F/B 前缀")
|
||||
void create_normal_autoGeneratesFloorName() {
|
||||
Building building = new Building();
|
||||
building.setId(100L);
|
||||
building.setProjectId(PROJECT_ID);
|
||||
when(buildingMapper.selectById(100L)).thenReturn(building);
|
||||
when(floorMapper.insert(any(Floor.class))).thenAnswer(inv -> {
|
||||
Floor f = inv.getArgument(0);
|
||||
f.setId(2001L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
// 地上楼层
|
||||
FloorSaveRequest up = new FloorSaveRequest();
|
||||
up.setBuildingId(100L);
|
||||
up.setFloorNo(3);
|
||||
Long upId = floorService.create(up);
|
||||
assertThat(upId).isEqualTo(2001L);
|
||||
|
||||
ArgumentCaptor<Floor> captor = ArgumentCaptor.forClass(Floor.class);
|
||||
verify(floorMapper).insert(captor.capture());
|
||||
Floor savedUp = captor.getValue();
|
||||
assertThat(savedUp.getFloorName()).isEqualTo("F3");
|
||||
assertThat(savedUp.getFloorType()).isEqualTo(1);
|
||||
assertThat(savedUp.getProjectId()).isEqualTo(PROJECT_ID);
|
||||
assertThat(savedUp.getCreatedBy()).isEqualTo(USER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建楼层:地下楼层自动生成 B 前缀且 floorType=2")
|
||||
void create_undergroundFloor_autoGeneratesBPrefix() {
|
||||
Building building = new Building();
|
||||
building.setId(100L);
|
||||
building.setProjectId(PROJECT_ID);
|
||||
when(buildingMapper.selectById(100L)).thenReturn(building);
|
||||
when(floorMapper.insert(any(Floor.class))).thenAnswer(inv -> {
|
||||
Floor f = inv.getArgument(0);
|
||||
f.setId(2002L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
FloorSaveRequest down = new FloorSaveRequest();
|
||||
down.setBuildingId(100L);
|
||||
down.setFloorNo(-1);
|
||||
floorService.create(down);
|
||||
|
||||
ArgumentCaptor<Floor> captor = ArgumentCaptor.forClass(Floor.class);
|
||||
verify(floorMapper).insert(captor.capture());
|
||||
Floor saved = captor.getValue();
|
||||
assertThat(saved.getFloorName()).isEqualTo("B1");
|
||||
assertThat(saved.getFloorType()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新楼层:不存在抛异常")
|
||||
void update_notFound_throwsException() {
|
||||
when(floorMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> floorService.update(999L, new FloorSaveRequest()))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(floorMapper, never()).updateById(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除楼层:不存在抛异常")
|
||||
void delete_notFound_throwsException() {
|
||||
when(floorMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> floorService.delete(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(floorMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import com.pms.base.service.impl.CamChargeServiceImpl;
|
|||
import com.pms.base.service.impl.EnterpriseServiceServiceImpl;
|
||||
import com.pms.base.service.impl.LeaseContractServiceImpl;
|
||||
import com.pms.base.service.impl.MeetingRoomServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.security.UserContext;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
|
@ -59,6 +60,9 @@ class OfficeT2FlowTest {
|
|||
@Mock
|
||||
private LeaseContractService leaseContractService;
|
||||
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private LeaseContractServiceImpl leaseContractServiceImpl;
|
||||
|
||||
|
|
@ -144,7 +148,7 @@ class OfficeT2FlowTest {
|
|||
|
||||
// when —— 分摊总额 1000
|
||||
List<CamCharge> result = camChargeService.allocateCamCharge(
|
||||
PROJECT_ID, new BigDecimal("1000"), "2024-01");
|
||||
PROJECT_ID, 100000L, "2024-01");
|
||||
|
||||
// then —— 生成 3 条分摊记录
|
||||
assertThat(result).hasSize(3);
|
||||
|
|
@ -152,20 +156,20 @@ class OfficeT2FlowTest {
|
|||
verify(camChargeMapper, times(3)).insert(captor.capture());
|
||||
List<CamCharge> charges = captor.getAllValues();
|
||||
|
||||
// 面积100:100/600=0.1667 → 1000*0.1667=166.70
|
||||
assertThat(charges.get(0).getAllocatedAmount()).isEqualByComparingTo("166.70");
|
||||
// 面积100:100/600=0.1667 → 100000*0.1667=16670 分
|
||||
assertThat(charges.get(0).getAllocatedAmountFen()).isEqualTo(16670L);
|
||||
assertThat(charges.get(0).getRatio()).isEqualByComparingTo("0.1667");
|
||||
assertThat(charges.get(0).getContractId()).isEqualTo(1001L);
|
||||
// 面积200:200/600=0.3333 → 1000*0.3333=333.30
|
||||
assertThat(charges.get(1).getAllocatedAmount()).isEqualByComparingTo("333.30");
|
||||
// 面积300:300/600=0.5 → 1000*0.5=500.00
|
||||
assertThat(charges.get(2).getAllocatedAmount()).isEqualByComparingTo("500.00");
|
||||
// 面积200:200/600=0.3333 → 100000*0.3333=33330 分
|
||||
assertThat(charges.get(1).getAllocatedAmountFen()).isEqualTo(33330L);
|
||||
// 面积300:300/600=0.5 → 100000*0.5=50000 分
|
||||
assertThat(charges.get(2).getAllocatedAmountFen()).isEqualTo(50000L);
|
||||
|
||||
// 分摊金额总和 = 1000
|
||||
BigDecimal sum = charges.stream()
|
||||
.map(CamCharge::getAllocatedAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
assertThat(sum).isEqualByComparingTo("1000.00");
|
||||
// 分摊金额总和 = 100000 分
|
||||
long sum = charges.stream()
|
||||
.mapToLong(CamCharge::getAllocatedAmountFen)
|
||||
.sum();
|
||||
assertThat(sum).isEqualTo(100000L);
|
||||
|
||||
// 周期与状态
|
||||
assertThat(charges.get(0).getChargePeriod()).isEqualTo("2024-01");
|
||||
|
|
@ -179,7 +183,7 @@ class OfficeT2FlowTest {
|
|||
.thenReturn(List.of());
|
||||
|
||||
assertThatThrownBy(() -> camChargeService.allocateCamCharge(
|
||||
PROJECT_ID, new BigDecimal("1000"), "2024-01"))
|
||||
PROJECT_ID, 100000L, "2024-01"))
|
||||
.isInstanceOf(com.pms.common.exception.BusinessException.class);
|
||||
|
||||
verify(camChargeMapper, never()).insert(any());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
package com.pms.base.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.pms.base.dto.OwnerRoomRelRequest;
|
||||
import com.pms.base.entity.Owner;
|
||||
import com.pms.base.entity.OwnerRoom;
|
||||
import com.pms.base.entity.Room;
|
||||
import com.pms.base.mapper.OwnerMapper;
|
||||
import com.pms.base.mapper.OwnerRoomMapper;
|
||||
import com.pms.base.mapper.RoomMapper;
|
||||
import com.pms.base.service.impl.OwnerRoomServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.security.UserContext;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 业主房间关联服务测试
|
||||
*/
|
||||
@DisplayName("业主房间关联服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class OwnerRoomServiceImplTest {
|
||||
|
||||
private static final Long PROJECT_ID = 2001L;
|
||||
private static final Long USER_ID = 1001L;
|
||||
|
||||
@Mock
|
||||
private OwnerRoomMapper ownerRoomMapper;
|
||||
@Mock
|
||||
private OwnerMapper ownerMapper;
|
||||
@Mock
|
||||
private RoomMapper roomMapper;
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private OwnerRoomServiceImpl ownerRoomService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
UserContext.set(new UserContext.CurrentUser(USER_ID, "admin", PROJECT_ID, "ROLE_ADMIN", "1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建关联:业主不存在抛 OWNER_NOT_FOUND")
|
||||
void create_ownerNotFound_throwsException() {
|
||||
when(ownerMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
OwnerRoomRelRequest request = new OwnerRoomRelRequest();
|
||||
request.setOwnerId(999L);
|
||||
request.setRoomId(8001L);
|
||||
|
||||
assertThatThrownBy(() -> ownerRoomService.create(request))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(ownerRoomMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建关联:房间不存在抛 ROOM_NOT_FOUND")
|
||||
void create_roomNotFound_throwsException() {
|
||||
Owner owner = new Owner();
|
||||
owner.setId(9001L);
|
||||
owner.setProjectId(PROJECT_ID);
|
||||
when(ownerMapper.selectById(9001L)).thenReturn(owner);
|
||||
when(roomMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
OwnerRoomRelRequest request = new OwnerRoomRelRequest();
|
||||
request.setOwnerId(9001L);
|
||||
request.setRoomId(999L);
|
||||
|
||||
assertThatThrownBy(() -> ownerRoomService.create(request))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(ownerRoomMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建关联:已存在关联抛 RESOURCE_EXISTS")
|
||||
void create_duplicateRelation_throwsException() {
|
||||
Owner owner = new Owner();
|
||||
owner.setId(9001L);
|
||||
owner.setProjectId(PROJECT_ID);
|
||||
Room room = new Room();
|
||||
room.setId(8001L);
|
||||
when(ownerMapper.selectById(9001L)).thenReturn(owner);
|
||||
when(roomMapper.selectById(8001L)).thenReturn(room);
|
||||
when(ownerRoomMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
|
||||
|
||||
OwnerRoomRelRequest request = new OwnerRoomRelRequest();
|
||||
request.setOwnerId(9001L);
|
||||
request.setRoomId(8001L);
|
||||
request.setRelationType(1);
|
||||
|
||||
assertThatThrownBy(() -> ownerRoomService.create(request))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("业主房间关联已存在");
|
||||
|
||||
verify(ownerRoomMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建关联:成功返回 ID,shareRatio 默认 100.00")
|
||||
void create_normal_returnsId() {
|
||||
Owner owner = new Owner();
|
||||
owner.setId(9001L);
|
||||
owner.setProjectId(PROJECT_ID);
|
||||
Room room = new Room();
|
||||
room.setId(8001L);
|
||||
when(ownerMapper.selectById(9001L)).thenReturn(owner);
|
||||
when(roomMapper.selectById(8001L)).thenReturn(room);
|
||||
when(ownerRoomMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
|
||||
when(ownerRoomMapper.insert(any(OwnerRoom.class))).thenAnswer(inv -> {
|
||||
OwnerRoom rel = inv.getArgument(0);
|
||||
rel.setId(7001L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
OwnerRoomRelRequest request = new OwnerRoomRelRequest();
|
||||
request.setOwnerId(9001L);
|
||||
request.setRoomId(8001L);
|
||||
request.setRelationType(1);
|
||||
|
||||
Long id = ownerRoomService.create(request);
|
||||
|
||||
assertThat(id).isEqualTo(7001L);
|
||||
ArgumentCaptor<OwnerRoom> captor = ArgumentCaptor.forClass(OwnerRoom.class);
|
||||
verify(ownerRoomMapper).insert(captor.capture());
|
||||
OwnerRoom saved = captor.getValue();
|
||||
assertThat(saved.getOwnerId()).isEqualTo(9001L);
|
||||
assertThat(saved.getRoomId()).isEqualTo(8001L);
|
||||
assertThat(saved.getShareRatio()).isEqualByComparingTo(new BigDecimal("100.00"));
|
||||
assertThat(saved.getProjectId()).isEqualTo(PROJECT_ID);
|
||||
assertThat(saved.getStatus()).isEqualTo(1);
|
||||
assertThat(saved.getCreatedBy()).isEqualTo(USER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除关联:不存在抛异常")
|
||||
void delete_notFound_throwsException() {
|
||||
when(ownerRoomMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> ownerRoomService.delete(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(ownerRoomMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package com.pms.base.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pms.base.dto.OwnerDTO;
|
||||
import com.pms.base.dto.OwnerSaveRequest;
|
||||
import com.pms.base.entity.Owner;
|
||||
import com.pms.base.mapper.OwnerMapper;
|
||||
import com.pms.base.service.impl.OwnerServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.security.UserContext;
|
||||
import com.pms.common.util.CryptoUtil;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 业主服务测试(含身份证/手机号加密验证)
|
||||
*/
|
||||
@DisplayName("业主服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class OwnerServiceImplTest {
|
||||
|
||||
private static final Long PROJECT_ID = 2001L;
|
||||
private static final Long USER_ID = 1001L;
|
||||
|
||||
@Mock
|
||||
private OwnerMapper ownerMapper;
|
||||
|
||||
@Mock
|
||||
private CryptoUtil cryptoUtil;
|
||||
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private OwnerServiceImpl ownerService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
UserContext.set(new UserContext.CurrentUser(USER_ID, "admin", PROJECT_ID, "ROLE_ADMIN", "1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建业主:身份证号/手机号加密并填充 hash")
|
||||
void create_withIdNoAndPhone_encryptsFields() {
|
||||
when(cryptoUtil.encrypt(anyString())).thenReturn("encrypted");
|
||||
when(cryptoUtil.hash(anyString())).thenReturn("hash");
|
||||
when(ownerMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
|
||||
when(ownerMapper.insert(any(Owner.class))).thenAnswer(inv -> {
|
||||
Owner o = inv.getArgument(0);
|
||||
o.setId(6001L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
OwnerSaveRequest request = new OwnerSaveRequest();
|
||||
request.setOwnerCode("O001");
|
||||
request.setOwnerName("张三");
|
||||
request.setIdNo("110101199001011234");
|
||||
request.setPhone("13800001111");
|
||||
request.setEmail("a@b.com");
|
||||
request.setEmergencyPhone("13900002222");
|
||||
|
||||
Long id = ownerService.create(request);
|
||||
|
||||
assertThat(id).isEqualTo(6001L);
|
||||
ArgumentCaptor<Owner> captor = ArgumentCaptor.forClass(Owner.class);
|
||||
verify(ownerMapper).insert(captor.capture());
|
||||
Owner saved = captor.getValue();
|
||||
assertThat(saved.getProjectId()).isEqualTo(PROJECT_ID);
|
||||
assertThat(saved.getOwnerCode()).isEqualTo("O001");
|
||||
assertThat(saved.getIdNo()).isEqualTo("encrypted");
|
||||
assertThat(saved.getIdNoHash()).isEqualTo("hash");
|
||||
assertThat(saved.getPhone()).isEqualTo("encrypted");
|
||||
assertThat(saved.getPhoneLast4Hash()).isEqualTo("hash");
|
||||
assertThat(saved.getEmergencyPhone()).isEqualTo("encrypted");
|
||||
assertThat(saved.getEmergencyPhoneHash()).isEqualTo("hash");
|
||||
assertThat(saved.getCreatedBy()).isEqualTo(USER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建业主:编号重复抛 RESOURCE_EXISTS")
|
||||
void create_duplicateCode_throwsException() {
|
||||
when(ownerMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
|
||||
|
||||
OwnerSaveRequest request = new OwnerSaveRequest();
|
||||
request.setOwnerCode("O001");
|
||||
request.setOwnerName("张三");
|
||||
|
||||
assertThatThrownBy(() -> ownerService.create(request))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("业主编号已存在");
|
||||
|
||||
verify(ownerMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询业主详情:解密身份证号/手机号")
|
||||
void getById_decryptsSensitiveFields() {
|
||||
OwnerDTO dto = new OwnerDTO();
|
||||
dto.setId(6001L);
|
||||
dto.setIdNo("enc-idno");
|
||||
dto.setPhone("enc-phone");
|
||||
dto.setEmail("enc-email");
|
||||
dto.setEmergencyPhone("enc-emg");
|
||||
when(ownerMapper.selectOwnerWithRooms(6001L)).thenReturn(dto);
|
||||
when(ownerMapper.selectOwnerRooms(6001L)).thenReturn(Collections.emptyList());
|
||||
when(cryptoUtil.decrypt(anyString())).thenReturn("decrypted");
|
||||
|
||||
OwnerDTO result = ownerService.getById(6001L);
|
||||
|
||||
assertThat(result.getIdNo()).isEqualTo("decrypted");
|
||||
assertThat(result.getPhone()).isEqualTo("decrypted");
|
||||
assertThat(result.getEmail()).isEqualTo("decrypted");
|
||||
assertThat(result.getEmergencyPhone()).isEqualTo("decrypted");
|
||||
assertThat(result.getRooms()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询业主:不存在抛 OWNER_NOT_FOUND")
|
||||
void getById_notFound_throwsException() {
|
||||
when(ownerMapper.selectOwnerWithRooms(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> ownerService.getById(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除业主:不存在抛 OWNER_NOT_FOUND")
|
||||
void delete_notFound_throwsException() {
|
||||
when(ownerMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> ownerService.delete(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(ownerMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import com.pms.base.service.impl.EnterpriseProfileServiceImpl;
|
|||
import com.pms.base.service.impl.EnergyMeterServiceImpl;
|
||||
import com.pms.base.service.impl.SafetyInspectionServiceImpl;
|
||||
import com.pms.base.service.impl.WorkshopLeaseServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.security.UserContext;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
|
@ -53,6 +54,9 @@ class ParkT2FlowTest {
|
|||
@Mock
|
||||
private EnterpriseProfileMapper enterpriseProfileMapper;
|
||||
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private WorkshopLeaseServiceImpl workshopLeaseService;
|
||||
|
||||
|
|
@ -91,7 +95,7 @@ class ParkT2FlowTest {
|
|||
lease.setTenantEnterprise("科技股份公司");
|
||||
lease.setLeaseStart(LocalDate.of(2026, 1, 1));
|
||||
lease.setLeaseEnd(LocalDate.of(2027, 12, 31));
|
||||
lease.setRentAmount(new BigDecimal("50000.00"));
|
||||
lease.setRentAmountFen(5000000L);
|
||||
|
||||
when(workshopLeaseMapper.insert(any(WorkshopLease.class))).thenAnswer(inv -> {
|
||||
WorkshopLease e = inv.getArgument(0);
|
||||
|
|
@ -131,7 +135,7 @@ class ParkT2FlowTest {
|
|||
lease.setId(5001L);
|
||||
lease.setProjectId(2001L);
|
||||
lease.setTenantEnterprise("科技股份公司");
|
||||
lease.setRentAmount(new BigDecimal("50000.00"));
|
||||
lease.setRentAmountFen(5000000L);
|
||||
when(workshopLeaseMapper.selectById(5001L)).thenReturn(lease);
|
||||
|
||||
String billNo = workshopLeaseService.generateRentBill(5001L);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
package com.pms.base.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pms.base.constant.LifecycleStage;
|
||||
import com.pms.base.dto.ProjectDTO;
|
||||
import com.pms.base.dto.ProjectSaveRequest;
|
||||
import com.pms.base.entity.Project;
|
||||
import com.pms.base.mapper.ProjectMapper;
|
||||
import com.pms.base.service.impl.ProjectServiceImpl;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.security.UserContext;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 物业项目服务测试
|
||||
*/
|
||||
@DisplayName("物业项目服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class ProjectServiceImplTest {
|
||||
|
||||
private static final Long USER_ID = 1001L;
|
||||
|
||||
@Mock
|
||||
private ProjectMapper projectMapper;
|
||||
|
||||
@Mock
|
||||
private RabbitTemplate rabbitTemplate;
|
||||
|
||||
@InjectMocks
|
||||
private ProjectServiceImpl projectService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
UserContext.set(new UserContext.CurrentUser(USER_ID, "admin", null, "ROLE_ADMIN", "1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建项目:编码重复抛 RESOURCE_EXISTS")
|
||||
void create_duplicateCode_throwsException() {
|
||||
when(projectMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
|
||||
|
||||
ProjectSaveRequest request = new ProjectSaveRequest();
|
||||
request.setProjectCode("P001");
|
||||
request.setProjectName("阳光花园");
|
||||
|
||||
assertThatThrownBy(() -> projectService.create(request))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("项目编码已存在");
|
||||
|
||||
verify(projectMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建项目:成功返回 ID 并发送事件")
|
||||
void create_normal_returnsIdAndSendsEvent() {
|
||||
when(projectMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
|
||||
when(projectMapper.insert(any(Project.class))).thenAnswer(inv -> {
|
||||
Project p = inv.getArgument(0);
|
||||
p.setId(1001L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
ProjectSaveRequest request = new ProjectSaveRequest();
|
||||
request.setProjectCode("P001");
|
||||
request.setProjectName("阳光花园");
|
||||
|
||||
Long id = projectService.create(request);
|
||||
|
||||
assertThat(id).isEqualTo(1001L);
|
||||
ArgumentCaptor<Project> captor = ArgumentCaptor.forClass(Project.class);
|
||||
verify(projectMapper).insert(captor.capture());
|
||||
Project saved = captor.getValue();
|
||||
assertThat(saved.getProjectCode()).isEqualTo("P001");
|
||||
assertThat(saved.getLifecycleStage()).isEqualTo(LifecycleStage.INITIATION);
|
||||
assertThat(saved.getCreatedBy()).isEqualTo(USER_ID);
|
||||
verify(rabbitTemplate).convertAndSend(anyString(), anyString(), any(Object.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询项目:不存在抛 PROJECT_NOT_FOUND")
|
||||
void getById_notFound_throwsException() {
|
||||
when(projectMapper.selectProjectWithStats(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> projectService.getById(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新项目:不存在抛 PROJECT_NOT_FOUND")
|
||||
void update_notFound_throwsException() {
|
||||
when(projectMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> projectService.update(999L, new ProjectSaveRequest()))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(projectMapper, never()).updateById(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除项目:不存在抛 PROJECT_NOT_FOUND")
|
||||
void delete_notFound_throwsException() {
|
||||
when(projectMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> projectService.delete(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(projectMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import com.pms.base.service.impl.CommunityActivityServiceImpl;
|
|||
import com.pms.base.service.impl.OwnerCommitteeServiceImpl;
|
||||
import com.pms.base.service.impl.PublicRevenueServiceImpl;
|
||||
import com.pms.base.service.impl.RenovationServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.security.UserContext;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
|
@ -26,7 +27,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
|
@ -62,6 +62,9 @@ class ResidentialT2FlowTest {
|
|||
@Mock
|
||||
private LookupService lookupService;
|
||||
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private OwnerCommitteeServiceImpl ownerCommitteeService;
|
||||
@InjectMocks
|
||||
|
|
@ -165,7 +168,7 @@ class ResidentialT2FlowTest {
|
|||
apply.setProjectId(PROJECT_ID);
|
||||
apply.setRoomId(8001L);
|
||||
apply.setOwnerId(9001L);
|
||||
apply.setDepositAmount(new BigDecimal("2000.00"));
|
||||
apply.setDepositAmountFen(200000L);
|
||||
apply.setStartDate(LocalDate.of(2025, 7, 1));
|
||||
apply.setEndDate(LocalDate.of(2025, 9, 1));
|
||||
|
||||
|
|
@ -439,7 +442,7 @@ class ResidentialT2FlowTest {
|
|||
PublicRevenue request = new PublicRevenue();
|
||||
request.setProjectId(PROJECT_ID);
|
||||
request.setRevenueType("广告位");
|
||||
request.setAmount(new BigDecimal("5000.00"));
|
||||
request.setAmountFen(500000L);
|
||||
request.setPeriod("2025-06");
|
||||
request.setDescription("电梯广告");
|
||||
|
||||
|
|
@ -461,29 +464,29 @@ class ResidentialT2FlowTest {
|
|||
assertThat(saved.getReceivedAt()).isNotNull();
|
||||
|
||||
// given —— 模拟月度汇总:项目 2025-06 共 2 笔有效收益
|
||||
PublicRevenue r1 = buildRevenue(8001L, "广告位", new BigDecimal("5000.00"), "2025-06", 1);
|
||||
PublicRevenue r2 = buildRevenue(8002L, "场地租赁", new BigDecimal("3000.00"), "2025-06", 1);
|
||||
PublicRevenue r1 = buildRevenue(8001L, "广告位", 500000L, "2025-06", 1);
|
||||
PublicRevenue r2 = buildRevenue(8002L, "场地租赁", 300000L, "2025-06", 1);
|
||||
when(publicRevenueMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(Arrays.asList(r1, r2));
|
||||
|
||||
// when
|
||||
BigDecimal total = publicRevenueService.monthlySummary(PROJECT_ID, "2025-06");
|
||||
Long total = publicRevenueService.monthlySummary(PROJECT_ID, "2025-06");
|
||||
|
||||
// then
|
||||
assertThat(total).isEqualByComparingTo(new BigDecimal("8000.00"));
|
||||
assertThat(total).isEqualTo(800000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("月度汇总:作废状态收益不计入总额")
|
||||
void monthlySummary_excludesInvalidRecords() {
|
||||
PublicRevenue valid = buildRevenue(8003L, "广告位", new BigDecimal("2000.00"), "2025-07", 1);
|
||||
PublicRevenue invalid = buildRevenue(8004L, "场地租赁", new BigDecimal("9999.00"), "2025-07", 0);
|
||||
PublicRevenue valid = buildRevenue(8003L, "广告位", 200000L, "2025-07", 1);
|
||||
PublicRevenue invalid = buildRevenue(8004L, "场地租赁", 999900L, "2025-07", 0);
|
||||
when(publicRevenueMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(List.of(valid)); // 模拟查询时只返回有效的
|
||||
|
||||
BigDecimal total = publicRevenueService.monthlySummary(PROJECT_ID, "2025-07");
|
||||
Long total = publicRevenueService.monthlySummary(PROJECT_ID, "2025-07");
|
||||
|
||||
assertThat(total).isEqualByComparingTo(new BigDecimal("2000.00"));
|
||||
assertThat(total).isEqualTo(200000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -492,17 +495,17 @@ class ResidentialT2FlowTest {
|
|||
when(publicRevenueMapper.selectList(any(LambdaQueryWrapper.class)))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
BigDecimal total = publicRevenueService.monthlySummary(PROJECT_ID, "2025-08");
|
||||
Long total = publicRevenueService.monthlySummary(PROJECT_ID, "2025-08");
|
||||
|
||||
assertThat(total).isEqualByComparingTo(BigDecimal.ZERO);
|
||||
assertThat(total).isEqualTo(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("月度汇总:projectId 为 null 返回 0")
|
||||
void monthlySummary_nullProjectId_returnsZero() {
|
||||
BigDecimal total = publicRevenueService.monthlySummary(null, "2025-06");
|
||||
Long total = publicRevenueService.monthlySummary(null, "2025-06");
|
||||
|
||||
assertThat(total).isEqualByComparingTo(BigDecimal.ZERO);
|
||||
assertThat(total).isEqualTo(0L);
|
||||
verify(publicRevenueMapper, never()).selectList(any());
|
||||
}
|
||||
|
||||
|
|
@ -512,7 +515,7 @@ class ResidentialT2FlowTest {
|
|||
PublicRevenue request = new PublicRevenue();
|
||||
request.setProjectId(PROJECT_ID);
|
||||
request.setRevenueType("广告位");
|
||||
request.setAmount(new BigDecimal("1000.00"));
|
||||
request.setAmountFen(100000L);
|
||||
request.setPeriod(null);
|
||||
|
||||
assertThatThrownBy(() -> publicRevenueService.create(request))
|
||||
|
|
@ -555,12 +558,12 @@ class ResidentialT2FlowTest {
|
|||
return activity;
|
||||
}
|
||||
|
||||
private PublicRevenue buildRevenue(Long id, String type, BigDecimal amount, String period, int status) {
|
||||
private PublicRevenue buildRevenue(Long id, String type, Long amountFen, String period, int status) {
|
||||
PublicRevenue revenue = new PublicRevenue();
|
||||
revenue.setId(id);
|
||||
revenue.setProjectId(PROJECT_ID);
|
||||
revenue.setRevenueType(type);
|
||||
revenue.setAmount(amount);
|
||||
revenue.setAmountFen(amountFen);
|
||||
revenue.setPeriod(period);
|
||||
revenue.setStatus(status);
|
||||
return revenue;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
package com.pms.base.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.pms.base.dto.RoomSaveRequest;
|
||||
import com.pms.base.entity.Building;
|
||||
import com.pms.base.entity.Floor;
|
||||
import com.pms.base.entity.Room;
|
||||
import com.pms.base.mapper.BuildingMapper;
|
||||
import com.pms.base.mapper.FloorMapper;
|
||||
import com.pms.base.mapper.OwnerRoomMapper;
|
||||
import com.pms.base.mapper.RoomMapper;
|
||||
import com.pms.base.service.impl.RoomServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.security.UserContext;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 房间服务测试
|
||||
*/
|
||||
@DisplayName("房间服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class RoomServiceImplTest {
|
||||
|
||||
private static final Long PROJECT_ID = 2001L;
|
||||
private static final Long USER_ID = 1001L;
|
||||
|
||||
@Mock
|
||||
private RoomMapper roomMapper;
|
||||
@Mock
|
||||
private BuildingMapper buildingMapper;
|
||||
@Mock
|
||||
private FloorMapper floorMapper;
|
||||
@Mock
|
||||
private OwnerRoomMapper ownerRoomMapper;
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private RoomServiceImpl roomService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
UserContext.set(new UserContext.CurrentUser(USER_ID, "admin", PROJECT_ID, "ROLE_ADMIN", "1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建房间:楼栋不存在抛 BUILDING_NOT_FOUND")
|
||||
void create_buildingNotFound_throwsException() {
|
||||
when(buildingMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
RoomSaveRequest request = new RoomSaveRequest();
|
||||
request.setBuildingId(999L);
|
||||
request.setFloorId(10L);
|
||||
request.setRoomCode("R001");
|
||||
|
||||
assertThatThrownBy(() -> roomService.create(request))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(roomMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建房间:房间编码重复抛 RESOURCE_EXISTS")
|
||||
void create_duplicateCode_throwsException() {
|
||||
Building building = new Building();
|
||||
building.setId(100L);
|
||||
building.setProjectId(PROJECT_ID);
|
||||
Floor floor = new Floor();
|
||||
floor.setId(10L);
|
||||
when(buildingMapper.selectById(100L)).thenReturn(building);
|
||||
when(floorMapper.selectById(10L)).thenReturn(floor);
|
||||
when(roomMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L);
|
||||
|
||||
RoomSaveRequest request = new RoomSaveRequest();
|
||||
request.setBuildingId(100L);
|
||||
request.setFloorId(10L);
|
||||
request.setRoomCode("R001");
|
||||
|
||||
assertThatThrownBy(() -> roomService.create(request))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("房间编码已存在");
|
||||
|
||||
verify(roomMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建房间:成功返回 ID 且 roomName 默认取 roomCode")
|
||||
void create_normal_returnsId() {
|
||||
Building building = new Building();
|
||||
building.setId(100L);
|
||||
building.setProjectId(PROJECT_ID);
|
||||
Floor floor = new Floor();
|
||||
floor.setId(10L);
|
||||
when(buildingMapper.selectById(100L)).thenReturn(building);
|
||||
when(floorMapper.selectById(10L)).thenReturn(floor);
|
||||
when(roomMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
|
||||
when(roomMapper.insert(any(Room.class))).thenAnswer(inv -> {
|
||||
Room r = inv.getArgument(0);
|
||||
r.setId(8001L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
RoomSaveRequest request = new RoomSaveRequest();
|
||||
request.setBuildingId(100L);
|
||||
request.setFloorId(10L);
|
||||
request.setRoomCode("R001");
|
||||
|
||||
Long id = roomService.create(request);
|
||||
|
||||
assertThat(id).isEqualTo(8001L);
|
||||
ArgumentCaptor<Room> captor = ArgumentCaptor.forClass(Room.class);
|
||||
verify(roomMapper).insert(captor.capture());
|
||||
Room saved = captor.getValue();
|
||||
assertThat(saved.getRoomCode()).isEqualTo("R001");
|
||||
assertThat(saved.getRoomName()).isEqualTo("R001");
|
||||
assertThat(saved.getProjectId()).isEqualTo(PROJECT_ID);
|
||||
assertThat(saved.getCreatedBy()).isEqualTo(USER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新房间:不存在抛 ROOM_NOT_FOUND")
|
||||
void update_notFound_throwsException() {
|
||||
when(roomMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> roomService.update(999L, new RoomSaveRequest()))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(roomMapper, never()).updateById(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除房间:不存在抛 ROOM_NOT_FOUND")
|
||||
void delete_notFound_throwsException() {
|
||||
when(roomMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> roomService.delete(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(roomMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
package com.pms.base.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pms.base.dto.TenantDTO;
|
||||
import com.pms.base.dto.TenantSaveRequest;
|
||||
import com.pms.base.entity.Tenant;
|
||||
import com.pms.base.mapper.TenantMapper;
|
||||
import com.pms.base.service.impl.TenantServiceImpl;
|
||||
import com.pms.base.util.LifecycleWriteGuard;
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import com.pms.common.response.PageResult;
|
||||
import com.pms.common.security.UserContext;
|
||||
import com.pms.common.util.CryptoUtil;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 租户服务测试(含敏感字段加密验证)
|
||||
*/
|
||||
@DisplayName("租户服务测试")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class TenantServiceImplTest {
|
||||
|
||||
private static final Long PROJECT_ID = 2001L;
|
||||
private static final Long USER_ID = 1001L;
|
||||
|
||||
@Mock
|
||||
private TenantMapper tenantMapper;
|
||||
|
||||
@Mock
|
||||
private CryptoUtil cryptoUtil;
|
||||
|
||||
@Mock
|
||||
private LifecycleWriteGuard lifecycleWriteGuard;
|
||||
|
||||
@InjectMocks
|
||||
private TenantServiceImpl tenantService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
UserContext.set(new UserContext.CurrentUser(USER_ID, "admin", PROJECT_ID, "ROLE_ADMIN", "1"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建租户:敏感字段加密并填充 hash")
|
||||
void create_withSensitiveFields_encryptsAndFillsHashes() {
|
||||
when(cryptoUtil.encrypt(anyString())).thenReturn("encrypted");
|
||||
when(cryptoUtil.hash(anyString())).thenReturn("hash");
|
||||
when(tenantMapper.insert(any(Tenant.class))).thenAnswer(inv -> {
|
||||
Tenant t = inv.getArgument(0);
|
||||
t.setId(5001L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
TenantSaveRequest request = new TenantSaveRequest();
|
||||
request.setTenantName("租户A");
|
||||
request.setIdNo("110101199001011234");
|
||||
request.setPhone("13800001111");
|
||||
request.setEmail("a@b.com");
|
||||
|
||||
Long id = tenantService.create(request);
|
||||
|
||||
assertThat(id).isEqualTo(5001L);
|
||||
ArgumentCaptor<Tenant> captor = ArgumentCaptor.forClass(Tenant.class);
|
||||
verify(tenantMapper).insert(captor.capture());
|
||||
Tenant saved = captor.getValue();
|
||||
assertThat(saved.getProjectId()).isEqualTo(PROJECT_ID);
|
||||
assertThat(saved.getTenantName()).isEqualTo("租户A");
|
||||
// 加密字段填充
|
||||
assertThat(saved.getIdNo()).isEqualTo("encrypted");
|
||||
assertThat(saved.getIdNoHash()).isEqualTo("hash");
|
||||
assertThat(saved.getPhone()).isEqualTo("encrypted");
|
||||
assertThat(saved.getPhoneHash()).isEqualTo("hash");
|
||||
assertThat(saved.getPhoneLast4Hash()).isEqualTo("hash");
|
||||
assertThat(saved.getEmail()).isEqualTo("encrypted");
|
||||
assertThat(saved.getEmailHash()).isEqualTo("hash");
|
||||
assertThat(saved.getCreatedBy()).isEqualTo(USER_ID);
|
||||
assertThat(saved.getStatus()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建租户:无敏感字段时不触发加密")
|
||||
void create_withoutSensitiveFields_noEncryption() {
|
||||
when(tenantMapper.insert(any(Tenant.class))).thenAnswer(inv -> {
|
||||
Tenant t = inv.getArgument(0);
|
||||
t.setId(5002L);
|
||||
return 1;
|
||||
});
|
||||
|
||||
TenantSaveRequest request = new TenantSaveRequest();
|
||||
request.setTenantName("租户B");
|
||||
|
||||
Long id = tenantService.create(request);
|
||||
|
||||
assertThat(id).isEqualTo(5002L);
|
||||
verify(cryptoUtil, never()).encrypt(anyString());
|
||||
verify(cryptoUtil, never()).hash(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新租户:不存在抛 RESOURCE_NOT_FOUND")
|
||||
void update_notFound_throwsException() {
|
||||
when(tenantMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> tenantService.update(999L, new TenantSaveRequest()))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(tenantMapper, never()).updateById(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除租户:不存在抛 RESOURCE_NOT_FOUND")
|
||||
void delete_notFound_throwsException() {
|
||||
when(tenantMapper.selectById(999L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> tenantService.delete(999L))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(tenantMapper, never()).deleteById(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("分页查询返回结果")
|
||||
void page_normal_returnsResult() {
|
||||
TenantDTO query = new TenantDTO();
|
||||
query.setPage(1);
|
||||
query.setSize(10);
|
||||
|
||||
TenantDTO dto = new TenantDTO();
|
||||
dto.setId(1L);
|
||||
dto.setTenantName("租户A");
|
||||
Page<TenantDTO> mockPage = new Page<>(1, 10);
|
||||
mockPage.setRecords(java.util.List.of(dto));
|
||||
mockPage.setTotal(1);
|
||||
|
||||
when(tenantMapper.selectTenantPage(any(Page.class), any(), any(), any()))
|
||||
.thenReturn(mockPage);
|
||||
|
||||
PageResult<TenantDTO> result = tenantService.page(query);
|
||||
|
||||
assertThat(result.getList()).hasSize(1);
|
||||
assertThat(result.getTotal()).isEqualTo(1);
|
||||
assertThat(result.getPage()).isEqualTo(1);
|
||||
assertThat(result.getSize()).isEqualTo(10);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
package com.pms.base.util;
|
||||
|
||||
import com.pms.common.exception.BusinessException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* AES-256-GCM 加密工具测试(U1)
|
||||
*
|
||||
* <p>验证:GCM 加解密、随机 IV、HMAC-SHA256 hash、异常抛出、密钥未配置启动失败。
|
||||
*/
|
||||
@DisplayName("AesEncryptUtil AES-256-GCM 测试")
|
||||
class AesEncryptUtilTest {
|
||||
|
||||
private AesEncryptUtil util;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
util = new AesEncryptUtil("0123456789abcdef0123456789abcdef");
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("加密测试")
|
||||
class EncryptTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("加密明文返回 Base64 密文")
|
||||
void encrypt_normal_returnsBase64() {
|
||||
String cipher = util.encrypt("110101199001011234");
|
||||
assertThat(cipher).isNotNull().isNotEmpty();
|
||||
// Base64 解码后长度 > IV(12) + tag(16)
|
||||
assertThat(cipher.length()).isGreaterThan(40);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("相同明文两次加密结果不同(IV 随机)")
|
||||
void encrypt_samePlaintext_differentResult() {
|
||||
String c1 = util.encrypt("敏感数据");
|
||||
String c2 = util.encrypt("敏感数据");
|
||||
assertThat(c1).isNotEqualTo(c2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空字符串加密返回 null")
|
||||
void encrypt_empty_returnsNull() {
|
||||
assertThat(util.encrypt("")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("null 加密返回 null")
|
||||
void encrypt_null_returnsNull() {
|
||||
assertThat(util.encrypt(null)).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("解密测试")
|
||||
class DecryptTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("解密密文还原明文")
|
||||
void decrypt_normal_returnsPlaintext() {
|
||||
String plain = "110101199001011234";
|
||||
String decrypted = util.decrypt(util.encrypt(plain));
|
||||
assertThat(decrypted).isEqualTo(plain);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空字符串解密返回 null")
|
||||
void decrypt_empty_returnsNull() {
|
||||
assertThat(util.decrypt("")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("null 解密返回 null")
|
||||
void decrypt_null_returnsNull() {
|
||||
assertThat(util.decrypt(null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("篡改密文解密抛 BusinessException(GCM 完整性校验)")
|
||||
void decrypt_tampered_throwsBusinessException() {
|
||||
String cipher = util.encrypt("原始数据");
|
||||
// 篡改最后 2 字符
|
||||
String tampered = cipher.substring(0, cipher.length() - 2) + "XX";
|
||||
assertThatThrownBy(() -> util.decrypt(tampered))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("AES解密失败");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("非 Base64 字符串解密抛 BusinessException")
|
||||
void decrypt_invalidBase64_throwsBusinessException() {
|
||||
assertThatThrownBy(() -> util.decrypt("这不是Base64!!!"))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("密文长度不足 IV 长度抛 BusinessException")
|
||||
void decrypt_tooShort_throwsBusinessException() {
|
||||
// 5 字节 Base64(解码后 < 12 字节 IV)
|
||||
assertThatThrownBy(() -> util.decrypt("YWJjZA=="))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("往返测试")
|
||||
class RoundTripTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("加解密往返一致")
|
||||
void roundTrip_success() {
|
||||
String[] cases = {
|
||||
"短文本",
|
||||
"110101199001011234",
|
||||
"{\"idCard\":\"110101199001011234\",\"phone\":\"13800138000\"}",
|
||||
"这是一段中等长度的敏感数据,包含中英混合内容ABC123"
|
||||
};
|
||||
for (String plain : cases) {
|
||||
assertThat(util.decrypt(util.encrypt(plain))).isEqualTo(plain);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("长文本加解密往返")
|
||||
void roundTrip_longText() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < 500; i++) sb.append("段").append(i);
|
||||
String longText = sb.toString();
|
||||
assertThat(util.decrypt(util.encrypt(longText))).isEqualTo(longText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Emoji 和 Unicode 加解密往返")
|
||||
void roundTrip_specialChars() {
|
||||
String[] cases = {"Emoji: 😀🎉🔐", "Unicode: \u00e9\u00e8\u00ea", "特殊:!@#$%^&*()"};
|
||||
for (String plain : cases) {
|
||||
assertThat(util.decrypt(util.encrypt(plain))).isEqualTo(plain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("HMAC-SHA256 哈希测试")
|
||||
class HashTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("哈希返回 64 位十六进制字符串")
|
||||
void hash_normal_returnsHex() {
|
||||
String hash = util.hash("110101199001011234");
|
||||
assertThat(hash).isNotNull().hasSize(64).matches("^[0-9a-f]{64}$");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("相同明文哈希一致")
|
||||
void hash_sameInput_sameResult() {
|
||||
assertThat(util.hash("测试")).isEqualTo(util.hash("测试"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("不同明文哈希不同")
|
||||
void hash_differentInput_differentResult() {
|
||||
assertThat(util.hash("数据1")).isNotEqualTo(util.hash("数据2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空字符串哈希返回 null")
|
||||
void hash_empty_returnsNull() {
|
||||
assertThat(util.hash("")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("null 哈希返回 null")
|
||||
void hash_null_returnsNull() {
|
||||
assertThat(util.hash(null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("大量输入哈希无碰撞")
|
||||
void hash_manyInputs_noCollision() {
|
||||
Set<String> hashes = new HashSet<>();
|
||||
for (int i = 0; i < 5000; i++) hashes.add(util.hash("data_" + i));
|
||||
assertThat(hashes).hasSize(5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HMAC 哈希与不同密钥实例结果不同(密钥绑定)")
|
||||
void hash_differentKey_differentResult() {
|
||||
AesEncryptUtil other = new AesEncryptUtil("different_key_0123456789abcdef");
|
||||
assertThat(util.hash("same_input")).isNotEqualTo(other.hash("same_input"));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("密钥初始化测试")
|
||||
class InitTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("密钥为 null 启动失败")
|
||||
void init_nullKey_throws() {
|
||||
assertThatThrownBy(() -> new AesEncryptUtil(null))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("未配置");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("密钥为空字符串启动失败")
|
||||
void init_blankKey_throws() {
|
||||
assertThatThrownBy(() -> new AesEncryptUtil(" "))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("短密钥通过 SHA-256 派生后正常工作")
|
||||
void init_shortKey_works() {
|
||||
AesEncryptUtil shortKeyUtil = new AesEncryptUtil("short");
|
||||
String plain = "测试短密钥";
|
||||
assertThat(shortKeyUtil.decrypt(shortKeyUtil.encrypt(plain))).isEqualTo(plain);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("IV 随机性测试(CSPRNG 验证)")
|
||||
class IvRandomnessTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("1000 次加密相同明文,密文全不同")
|
||||
void encrypt_1000Times_allDifferent() {
|
||||
Set<String> ciphers = new HashSet<>();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
ciphers.add(util.encrypt("same_plaintext"));
|
||||
}
|
||||
assertThat(ciphers).hasSize(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.pms.base.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* POC: 验证 JDK 原生 GCM 支持
|
||||
* Hutool 5.8.27 的 Mode 枚举无 GCM,改用 JDK 原生 Cipher
|
||||
*/
|
||||
class GcmPocTest {
|
||||
|
||||
private static final String KEY = "EtherPMS2024SecureKey!!"; // 原始 key,用 SHA-256 派生 32 字节
|
||||
private static final String PLAINTEXT = "110101199001011234";
|
||||
|
||||
private static byte[] deriveKey(String key) {
|
||||
try {
|
||||
return java.security.MessageDigest.getInstance("SHA-256")
|
||||
.digest(key.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void jdkNativeGcm_poc() throws Exception {
|
||||
// JDK 原生 GCM 作为备选方案
|
||||
byte[] keyBytes = deriveKey(KEY);
|
||||
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
|
||||
|
||||
// 生成随机 IV (12 bytes, GCM 标准)
|
||||
byte[] iv = new byte[12];
|
||||
new SecureRandom().nextBytes(iv);
|
||||
|
||||
// 加密
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); // 128-bit auth tag
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
|
||||
byte[] cipherText = cipher.doFinal(PLAINTEXT.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// IV + cipherText 拼接存储
|
||||
byte[] combined = new byte[iv.length + cipherText.length];
|
||||
System.arraycopy(iv, 0, combined, 0, iv.length);
|
||||
System.arraycopy(cipherText, 0, combined, iv.length, cipherText.length);
|
||||
|
||||
assertNotNull(combined);
|
||||
assertTrue(combined.length > iv.length, "Cipher text should be longer than IV");
|
||||
|
||||
// 解密
|
||||
byte[] extractedIv = Arrays.copyOfRange(combined, 0, 12);
|
||||
byte[] extractedCipherText = Arrays.copyOfRange(combined, 12, combined.length);
|
||||
|
||||
Cipher decryptCipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, extractedIv));
|
||||
byte[] decrypted = decryptCipher.doFinal(extractedCipherText);
|
||||
|
||||
assertEquals(PLAINTEXT, new String(decrypted, StandardCharsets.UTF_8));
|
||||
System.out.println("[JDK GCM] PASS - combined length: " + combined.length +
|
||||
" (IV=12 + ciphertext=" + (combined.length - 12 - 16) + " + tag=16)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void jdkGcm_randomIvEachEncrypt() throws Exception {
|
||||
// 验证每次加密 IV 不同 → 密文不同
|
||||
byte[] keyBytes = deriveKey(KEY);
|
||||
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
String enc1 = jdkGcmEncrypt(secretKey, PLAINTEXT, secureRandom);
|
||||
String enc2 = jdkGcmEncrypt(secretKey, PLAINTEXT, secureRandom);
|
||||
|
||||
assertNotEquals(enc1, enc2, "Same plaintext should produce different ciphertext due to random IV");
|
||||
System.out.println("[JDK GCM] Random IV PASS - enc1 != enc2");
|
||||
}
|
||||
|
||||
private String jdkGcmEncrypt(SecretKeySpec secretKey, String plaintext, SecureRandom random) throws Exception {
|
||||
byte[] iv = new byte[12];
|
||||
random.nextBytes(iv);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(128, iv));
|
||||
byte[] cipherText = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
byte[] combined = new byte[iv.length + cipherText.length];
|
||||
System.arraycopy(iv, 0, combined, 0, iv.length);
|
||||
System.arraycopy(cipherText, 0, combined, iv.length, cipherText.length);
|
||||
|
||||
return java.util.Base64.getEncoder().encodeToString(combined);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,691 @@
|
|||
---
|
||||
date: 2026-07-02
|
||||
type: fix
|
||||
origin: pms-base 模块架构评审(2026-07-02 会话评审报告)
|
||||
deepened: 2026-07-02
|
||||
revised: 2026-07-02
|
||||
---
|
||||
|
||||
# pms-base 基础数据模块加固修复计划
|
||||
|
||||
## Summary
|
||||
|
||||
针对 pms-base 模块架构评审发现的问题,分阶段系统性修复 AES 加密漏洞(ECB→GCM 全量迁移)、BigDecimal 金额字段、敏感字段加密、审计日志、测试覆盖率、入参校验、Feign 熔断等。
|
||||
|
||||
**鉴权相关工作(ProjectSecurityChecker 补齐、LifecycleWriteGuard null skip 修复、@PreAuthorize 标注)全部延后**至 `feat/user-org-perm` worktree 合并后再统一处理,避免重复改造。本计划聚焦 pms-base 自身代码加固,不依赖也不阻塞另一个会话。
|
||||
|
||||
**Origin:** pms-base 模块评审报告(2026-07-02 会话,三维度并行评审:安全与权限 / 架构与代码规范 / 测试与数据库迁移)
|
||||
|
||||
---
|
||||
|
||||
## Problem Frame
|
||||
|
||||
pms-base 模块作为 EtherPMS 基础数据服务,承载业主、租户、合同、设备、能源表、装修、公共收益等核心业务实体。架构评审发现问题跨 P0-P3 严重级别,覆盖以下根因:
|
||||
|
||||
1. **安全基础设施空转**:AesEncryptUtil 注释声称 AES-256-GCM 实际使用 ECB 模式(Hutool 默认);AES key 硬编码 `EtherPMS2024SecureKey!!` 作为默认值且 yml 配置键名不一致(`app.encrypt.aes-key` vs `crypto.aes.key`);idNoHash 字段定义但从未填充;手机号/邮箱明文存储与 idNo 加密策略不一致。
|
||||
2. **跨项目越权**(延后至 feat/user-org-perm 合并):30/31 Controller 零 @PreAuthorize,19+/30+ ServiceImpl 缺失 ProjectSecurityChecker,LifecycleWriteGuard 在 projectId=null 时静默放行。
|
||||
3. **审计合规缺失**:仅 3/31 Controller 使用 @AuditLog,业务操作不可追溯,违反《网络安全法》第 21 条日志留存要求。
|
||||
4. **数据规范偏离**:14 个 entity 使用 BigDecimal 存金额,违反 CLAUDE.md "金额数据统一使用 Long 类型的 priceFen(分)"约定。
|
||||
5. **代码质量技术债**:@Lazy 循环依赖、Feign 无熔断、3 个 delete 方法缺 @Transactional、入参校验缺失、测试覆盖率不足(13/32 ServiceImpl 零测试)。
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### 功能性需求
|
||||
|
||||
- **R1(P0)**:AES 加密采用 GCM 模式,AES key 从 Nacos 配置中心加载,禁止硬编码默认值;idNoHash 字段在加密时同步填充(HMAC-SHA256);IV 使用 `SecureRandom`(CSPRNG)每记录独立生成;提供历史 ECB 数据停机迁移脚本。
|
||||
- **R2(延后)**:~~所有业务 ServiceImpl 补齐 ProjectSecurityChecker 跨项目校验;LifecycleWriteGuard 在 projectId=null 时抛异常而非静默放行。~~ **延后至 feat/user-org-perm 合并后统一用 @PreAuthorize 处理。**
|
||||
- **R3(P1)**:所有业务 Controller 写操作(create/update/delete/状态流转)标注 @AuditLog,记录操作人、操作类型、目标实体。涉及 PII 的方法禁用 `recordParams`,关键操作显式标记非敏感字段。
|
||||
- **R4(P1)**:14 个 entity 的金额字段从 BigDecimal 迁移到 Long(分),DDL 同步变更(DECIMAL→BIGINT),DTO 使用 `xxxPriceFen` 字段名。
|
||||
- **R5(P1)**:Owner/Tenant/Enterprise 实体的 phone/email 字段采用 hash + 加密字段模式存储(HMAC-SHA256 hash + GCM 加密),支持 hash 精确查询 + 后4位 hash 索引(尾号查询)。
|
||||
- **R6(P1)**:13 个零测试 ServiceImpl 补齐单元测试,覆盖加密/金额计算等高风险逻辑。
|
||||
- **R7(P2)**:11 个 Controller 的 Entity @RequestBody 加 @Valid + 字段级 @NotBlank/@Pattern 校验。
|
||||
- **R8(P2)**:Feign 客户端配置 Resilience4j 熔断;AuthClient fallback 抛 BusinessException 而非返回 null。
|
||||
- **R9(P3)**:3 个 delete 方法补 @Transactional;AesEncryptUtil 加解密异常抛出而非吞掉。
|
||||
- **R10(P3)**:@Lazy 循环依赖(LifecycleService ↔ ArchiveService)文档化为已知设计,标注于 CONCEPTS.md。
|
||||
- **R11(延后)**:~~业务 Controller 标注 @PreAuthorize~~ **延后至 feat/user-org-perm 合并。**
|
||||
|
||||
### 非功能性需求
|
||||
|
||||
- **N1**:历史数据迁移必须可重入、可回滚,迁移期间双读兼容(如需灰度)。
|
||||
- **N2**:P0 修复须在 1 周内上线,P1 在 2 周内完成。
|
||||
- **N3**:所有修复不破坏现有 80 个单元测试。
|
||||
- **N4**:迁移脚本与代码变更同 PR 提交,禁止 schema 与代码分批。
|
||||
|
||||
### 成功标准
|
||||
|
||||
- SC1:AesEncryptUtil 使用 GCM 模式,IV 用 SecureRandom 生成,key 从 Nacos 加载,无默认值回退。
|
||||
- ~~SC2:所有 ServiceImpl 写操作经过 ProjectSecurityChecker 校验。~~ **延后。**
|
||||
- SC3:所有业务 Controller 写操作有 @AuditLog 注解,涉及 PII 的方法 `recordParams=false`。
|
||||
- SC4:金额字段全链路使用 Long(分),DTO 字段命名 `xxxPriceFen`,无 BigDecimal 残留。
|
||||
- SC5:phone/email 字段加密存储,hash 精确查询 + 后4位尾号查询可用,API 响应可解密展示。
|
||||
- SC6:单元测试覆盖率 ≥ 60%(ServiceImpl 数维度)。
|
||||
|
||||
---
|
||||
|
||||
## Key Technical Decisions
|
||||
|
||||
### KD1. AES 加密重构:GCM 模式 + Nacos key + HMAC-SHA256 hash + SecureRandom IV + 停机迁移
|
||||
|
||||
**决策:** 采用 AES-256-GCM 模式;key 从 Nacos 配置中心加载(`app.encrypt.aes-key`),移除默认值,启动时若未配置则抛异常;idNoHash 在 encrypt 时同步填充(HMAC-SHA256 with server secret);IV 使用 `java.security.SecureRandom`(CSPRNG)每记录独立生成,与密文一同存储;历史 ECB 数据停机迁移。
|
||||
|
||||
**前置闸门:** Hutool GCM 支持 POC(2-4 小时)。若 Hutool `cn.hutool.crypto.symmetric.AES` 不支持 GCM,则切 JDK 原生 `Cipher.getInstance("AES/GCM/NoPadding")` 或 BouncyCastle。POC 失败才降级为最低修复(仅修配置+idNoHash+异常,保留 ECB)。
|
||||
|
||||
| 优势 | 劣势 |
|
||||
|------|------|
|
||||
| 根治 ECB 模式分析攻击风险 | 历史数据迁移需停机窗口(idNo 数据量小,约 30 分钟) |
|
||||
| GCM 提供保密性 + 完整性(AEAD) | key 切换需协调所有环境 Nacos 配置 |
|
||||
| HMAC-SHA256 抗彩虹表 | server secret 泄露后 hash 失效 |
|
||||
| 合规《个人信息保护法》第 51 条 | 解密异常不再吞掉,上层需处理 |
|
||||
|
||||
**替代方案考虑:**
|
||||
- 仅最低修复(修配置+idNoHash+异常,保留 ECB):省事但后续手机号/邮箱加密仍用 ECB,GCM 迁移时需二次迁移所有加密数据(含新增的 phone/email),总成本更高。
|
||||
- 双读灰度迁移:无停机但解密需探测 ECB/GCM 模式,复杂度高,idNo 数据量小不值得。
|
||||
|
||||
**理由:** 从未来安全可用角度,GCM 一次到位避免双倍迁移成本。用户确认 GCM 全量迁移 + 停机窗口。
|
||||
|
||||
### KD2. BigDecimal→Long 全链路迁移
|
||||
|
||||
**决策:** 14 个 entity 的 BigDecimal 金额字段改为 Long(分);Flyway V13 迁移脚本将 DECIMAL(15,2) 转换为 BIGINT(值 × 100);DTO 字段统一命名 `xxxPriceFen`(与 CLAUDE.md 一致);Service 层金额计算使用 Long,乘除用 BigDecimal 中间计算后 `setScale(0, RoundingMode.HALF_UP).longValueExact()` 转回 Long。
|
||||
|
||||
| 优势 | 劣势 |
|
||||
|------|------|
|
||||
| 与 CLAUDE.md 约定一致 | 14 表 schema 变更,需停机或灰度 |
|
||||
| 与 pms-charge 金额单位统一 | 前端 DTO 字段名变更需协调 |
|
||||
| 消除 BigDecimal 比较陷阱 | 历史数据如有小数需四舍五入 |
|
||||
|
||||
**理由:** 用户确认全链路迁移;CLAUDE.md 明确要求;与 pms-charge 已用 Long 保持一致。字段命名 `xxxPriceFen` 遵循 CLAUDE.md 约定。
|
||||
|
||||
### KD3. 手机号/邮箱加密:HMAC-SHA256 hash + GCM 加密 + 后4位 hash 索引
|
||||
|
||||
**决策:** Owner/Tenant/Enterprise 实体的 phone/email 字段拆分为 `phone`(GCM 加密存储)+ `phoneHash`(HMAC-SHA256 用于精确查询)+ `phoneLast4Hash`(HMAC-SHA256 of 后4位,用于尾号查询);email 同理。Project 实体无 phone/email 字段,不涉及。查询时用 hash 精确匹配或后4位 hash 尾号查询;展示时解密。Flyway V14 迁移脚本对历史明文数据批量加密 + 填充 hash。
|
||||
|
||||
| 优势 | 劣势 |
|
||||
|------|------|
|
||||
| 与 idNo 加密策略一致 | like 前缀模糊查询失效(保留尾号查询) |
|
||||
| 数据库泄露不暴露明文 | 改造面大,涉及查询逻辑 |
|
||||
| 后4位 hash 支持客服尾号查询 | API 响应需统一解密处理 |
|
||||
| 合规《个人信息保护法》最小化原则 | 需产品确认客服实际查询模式 |
|
||||
|
||||
**替代方案考虑:**
|
||||
- 仅 API 响应脱敏:存储仍明文,未根治。
|
||||
- 手机号不加密仅脱敏:保留 LIKE 能力但手机号也是 PII。
|
||||
|
||||
**理由:** 用户确认 hash + 加密字段模式 + 后4位 hash 索引。Project.java 无 phone/email 字段,排除。
|
||||
|
||||
### KD4. 分阶段交付:P0 先行 + P1 加固 + 架构优化 + 鉴权延后
|
||||
|
||||
**决策:** 计划分三个阶段(鉴权延后不在本计划):
|
||||
- 阶段 1(P0 紧急):U1 AES GCM 全量迁移
|
||||
- 阶段 2(P1 加固):U2 @AuditLog + U3 BigDecimal 迁移 + U4 手机邮箱加密 + U5 入参校验 + U6 测试覆盖
|
||||
- 阶段 3(架构 + P3):U7 Feign 熔断 + U8 @Transactional + U9 @Lazy 文档化
|
||||
|
||||
| 优势 | 劣势 |
|
||||
|------|------|
|
||||
| P0 快速上线降风险 | 阶段间需协调(如 U4 等 U1 完成) |
|
||||
| P1 批量加固效率高 | 多 PR review 成本 |
|
||||
| 鉴权延后不阻塞外部会话 | P0 越权漏洞持续暴露直至合并 |
|
||||
|
||||
**理由:** 用户确认分阶段;鉴权全部延后至 feat/user-org-perm 合并。已知风险:P0 越权漏洞(LifecycleWriteGuard null skip + Controller 零鉴权)持续暴露,文档化为已知技术债。
|
||||
|
||||
### KD5. @AuditLog 现在标注,冲突时对齐
|
||||
|
||||
**决策:** 现在基于现有 @AuditLog AOP 切面批量标注业务 Controller;使用实际注解属性 `module`/`type`/`description`/`recordParams`/`recordResult`(非 `action`/`target`);feat/user-org-perm 合并后若字段规范增强,统一对齐。
|
||||
|
||||
| 优势 | 劣势 |
|
||||
|------|------|
|
||||
| 不阻塞 P1 修复 | 可能返工(字段冲突) |
|
||||
| AOP 切面已工作 | — |
|
||||
|
||||
**理由:** 用户确认现在标注;@AuditLog 切面已工作,字段冲突可后续对齐。
|
||||
|
||||
### KD6. @AuditLog PII 脱敏:A+C 混合
|
||||
|
||||
**决策:** 涉及 PII(idNo/phone/email)的方法默认 `recordParams=false`;关键操作(如状态流转)显式标记非敏感字段记录。避免 AOP 序列化请求体时把明文 PII 写入 t_audit_log。
|
||||
|
||||
| 优势 | 劣势 |
|
||||
|------|------|
|
||||
| 防止审计日志泄露 PII | 涉及 PII 的操作失去参数审计 |
|
||||
| 简单,无需额外脱敏框架 | 需逐方法判断是否涉及 PII |
|
||||
|
||||
**理由:** 用户确认 A+C 混合方案。违反《个人信息保护法》第 51 条的风险高于审计价值损失。
|
||||
|
||||
### KD7. 鉴权全部延后至 feat/user-org-perm 合并
|
||||
|
||||
**决策:** ProjectSecurityChecker 补齐、LifecycleWriteGuard null skip 修复、@PreAuthorize 标注全部不在本计划实施,延后至 feat/user-org-perm 合并后统一用 @PreAuthorize 处理。
|
||||
|
||||
| 优势 | 劣势 |
|
||||
|------|------|
|
||||
| 避免重复改造(静态工具类 → @PreAuthorize) | P0 越权漏洞持续暴露 |
|
||||
| 不与另一个会话冲突 | 已知技术债 |
|
||||
|
||||
**理由:** 用户确认纯等待不兜底。feat/user-org-perm 提供 @PreAuthorize 框架后,pms-base 直接标注注解,无需中间态。
|
||||
|
||||
---
|
||||
|
||||
## High-Level Technical Design
|
||||
|
||||
### 阶段依赖关系
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
U1[U1 AES GCM 迁移] --> U4[U4 手机邮箱加密]
|
||||
U1 --> U6[U6 测试补齐]
|
||||
U2[U2 @AuditLog]
|
||||
U3[U3 BigDecimal 迁移] --> U6
|
||||
U5[U5 入参校验] --> U3
|
||||
U7[U7 Feign 熔断]
|
||||
U8[U8 @Transactional]
|
||||
U9[U9 @Lazy 文档化]
|
||||
```
|
||||
|
||||
### AES GCM 加密数据流
|
||||
|
||||
```
|
||||
明文身份证号 → AesEncryptUtil.encrypt(plainText)
|
||||
→ AES-256-GCM 加密(key 从 Nacos 加载,IV 用 SecureRandom 生成)
|
||||
→ 密文 + IV 存储(IV 前缀拼接密文)
|
||||
→ HMAC-SHA256(plainText, serverSecret) 存储到 idNoHash 字段
|
||||
|
||||
查询身份证号 → AesEncryptUtil.hash(queryIdNo) → 数据库 WHERE idNoHash = ?
|
||||
```
|
||||
|
||||
### BigDecimal→Long 迁移数据流
|
||||
|
||||
```
|
||||
Flyway V13: ALTER TABLE t_contract MODIFY amount BIGINT;
|
||||
迁移逻辑:UPDATE t_contract SET amount = ROUND(amount * 100, 0);
|
||||
Entity: BigDecimal amount → Long amountPriceFen
|
||||
DTO: private Long xxxPriceFen
|
||||
Service: Long 计算,避免 BigDecimal
|
||||
API: {"monthlyRentPriceFen": 1234567} // 表示 12345.67 元
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
### In Scope
|
||||
|
||||
- pms-base 模块 AES GCM 加密重构 + 历史数据迁移
|
||||
- pms-base 模块 BigDecimal→Long 全链路迁移
|
||||
- pms-base 模块手机号/邮箱加密
|
||||
- pms-base 模块 @AuditLog 标注 + PII 脱敏
|
||||
- pms-base 模块测试覆盖率补齐
|
||||
- pms-base 模块入参校验、Feign 熔断、@Transactional 补齐
|
||||
- pms-base 的 Flyway 迁移脚本(V12-V14)
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Spring Security 链路重构(由 `feat/user-org-perm` worktree 承担)
|
||||
- @PreAuthorize 注解框架本身(由 `feat/user-org-perm` 承担)
|
||||
- @AuditLog AOP 切面基础设施(已存在,feat/user-org-perm 可能增强)
|
||||
- 权限码体系设计(由 `feat/user-org-perm` 承担)
|
||||
- 前端改动(除 DTO 字段名变更协调外)
|
||||
- 其他微服务(pms-charge/pms-auth/pms-operation 等)
|
||||
|
||||
### Deferred to feat/user-org-perm 合并后
|
||||
|
||||
- ProjectSecurityChecker 跨项目校验补齐(19+ ServiceImpl)
|
||||
- LifecycleWriteGuard null skip 修复
|
||||
- @PreAuthorize 业务 Controller 标注
|
||||
- isSystemCall() 信任边界定义
|
||||
- **已知风险**:P0 越权漏洞(LifecycleWriteGuard null skip + 30/31 Controller 零鉴权)持续暴露
|
||||
|
||||
### Deferred to Follow-Up Work
|
||||
|
||||
- pms-charge 等其他微服务的 BigDecimal→Long 迁移(如存在)
|
||||
- 全仓 Feign 熔断统一配置(本计划仅修 pms-base 调用方)
|
||||
- 前端 DTO 字段名变更适配(pms-base 后端先行,前端后续排期)
|
||||
- AES key 轮换策略(V1 文档化,V2 评估 KMS)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Units
|
||||
|
||||
### 阶段 1:P0 安全紧急修复
|
||||
|
||||
### U1. AES 加密 GCM 全量迁移
|
||||
|
||||
**Goal:** 修复 AesEncryptUtil 的 ECB 模式、硬编码 key、idNoHash 未填充、异常吞掉四大漏洞,迁移历史数据到 GCM。
|
||||
|
||||
**Requirements:** R1, SC1
|
||||
|
||||
**Dependencies:** 无(前置:Hutool GCM 支持 POC)
|
||||
|
||||
**Files:**
|
||||
- `backend/pms-base/src/main/java/com/pms/base/util/AesEncryptUtil.java`(修改:GCM 模式 + Nacos key 强制 + 异常抛出 + idNoHash 同步填充 + SecureRandom IV)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/service/impl/OwnerServiceImpl.java`(修改:create/update 时填充 idNoHash)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/service/impl/TenantServiceImpl.java`(修改:同上)
|
||||
- `backend/pms-base/src/main/resources/application.yml`(修改:统一为 `app.encrypt.aes-key`,移除 `crypto.aes.key`,无默认值)
|
||||
- `backend/pms-base/src/main/resources/db/migration/V12__migrate_aes_ecb_to_gcm.java`(新建:Java-based Flyway 迁移,ECB→GCM + idNoHash 填充)
|
||||
- `backend/pms-base/src/test/java/com/pms/base/util/AesEncryptUtilTest.java`(新建:GCM 加解密测试)
|
||||
|
||||
**Approach:**
|
||||
1. **前置 POC(2-4 小时)**:验证 Hutool `cn.hutool.crypto.symmetric.AES` 是否支持 GCM 模式。若不支持,切 JDK 原生 `Cipher.getInstance("AES/GCM/NoPadding")`。POC 失败才降级为最低修复。
|
||||
2. AesEncryptUtil 重构:GCM 模式(`Mode.GCM`、`Padding.NoPadding` 或 JDK 原生);构造函数移除默认值,未配置 key 时抛 `IllegalStateException`;`encrypt()` 方法生成随机 IV(`SecureRandom`),IV 与密文拼接存储,同时填充 HMAC-SHA256 hash;`decrypt()` 异常抛 `BusinessException` 而非返回 null;`padKey()` 替换为 `SHA-256` 派生 key(KDF),不再用 0 填充。
|
||||
3. OwnerServiceImpl/TenantServiceImpl:在 `setIdNo(cryptoUtil.encrypt(...))` 后增加 `setIdNoHash(cryptoUtil.hash(...))`。
|
||||
4. Flyway V12(Java-based):对历史 ECB 数据迁移。Java 迁移逻辑:对每条记录,解密 ECB(用旧 key)→ 重新用 GCM 加密(用新 key + 随机 IV)→ 更新 idNo 字段 + 填充 idNoHash。停机迁移。
|
||||
5. 配置文件:统一为 `app.encrypt.aes-key`,移除 `crypto.aes.key`,无默认值。Nacos 配置中心管理实际 key。
|
||||
|
||||
**Patterns to follow:** 现有 AesEncryptUtil 的 `@Component` + `@Value` 注入模式;Hutool `cn.hutool.crypto.symmetric.AES` API 或 JDK 原生 `Cipher` API。
|
||||
|
||||
**Test scenarios:**
|
||||
- 加密后解密返回原文(GCM 模式)
|
||||
- 相同明文加密结果不同(IV 随机)
|
||||
- HMAC-SHA256 hash 返回固定长度,相同输入相同输出
|
||||
- 未配置 key 时启动失败
|
||||
- 解密失败抛 BusinessException
|
||||
- 历史数据迁移脚本:ECB 密文 → GCM 密文 + idNoHash 填充
|
||||
- idNoHash 在 encrypt 后非空
|
||||
- IV 每次不同(CSPRNG 验证)
|
||||
|
||||
**Verification:** AesEncryptUtilTest 全部通过;yml 无默认值时启动失败;V12 迁移后 idNoHash 字段全部非空;GCM 密文长度正确(12 字节 IV + 密文 + 16 字节 tag)。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 2:P1 加固
|
||||
|
||||
### U2. @AuditLog 业务方法全覆盖 + PII 脱敏
|
||||
|
||||
**Goal:** 28 个未标注 @AuditLog 的 Controller 批量补齐注解,覆盖所有写操作,涉及 PII 的方法禁用 recordParams。
|
||||
|
||||
**Requirements:** R3, SC3
|
||||
|
||||
**Dependencies:** U1(AesEncryptUtil 异常修复后,审计日志记录更准确)
|
||||
|
||||
**Files:**
|
||||
- `backend/pms-base/src/main/java/com/pms/base/controller/OwnerController.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/controller/TenantController.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/controller/DeviceController.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/controller/ContractController.java`(修改)
|
||||
- 其他 24 个业务 Controller(评审清单)
|
||||
- `backend/pms-base/src/test/java/com/pms/base/controller/AuditLogCoverageTest.java`(新建:覆盖率验证测试)
|
||||
|
||||
**Approach:**
|
||||
1. 每个业务 Controller 的 create/update/delete/状态流转方法标注 `@AuditLog(module="base", type="CREATE", description="创建业主", recordParams=false)`。
|
||||
2. 注解属性使用实际定义:`module`(模块,如 "base")、`type`(操作类型,如 "CREATE"/"UPDATE"/"DELETE")、`description`(人类可读描述)、`recordParams`(是否记录参数)、`recordResult`(是否记录结果)。
|
||||
3. 涉及 PII(idNo/phone/email)的方法:`recordParams=false`(默认)。
|
||||
4. 关键操作(如状态流转、审批):`recordParams=true` 但参数中不含 PII。
|
||||
5. 读操作(list/get)暂不标注,避免日志膨胀。
|
||||
6. feat/user-org-perm 合并后,若 @AuditLog 字段增强,统一对齐。
|
||||
|
||||
**Patterns to follow:** 现有 LifecycleController 的 `@AuditLog(module = "base", type = "CREATE", description = "发起阶段流转")` 使用模式。
|
||||
|
||||
**Test scenarios:**
|
||||
- Controller create 方法调用后,t_audit_log 表新增一条记录
|
||||
- 记录的 module/type/description/userId/projectId 字段正确
|
||||
- 涉及 PII 的方法 recordParams=false,t_audit_log.params 字段为空
|
||||
- 读操作不产生审计日志
|
||||
- 异常情况下审计日志仍记录(操作失败也留痕)
|
||||
|
||||
**Verification:** AuditLogCoverageTest 通过;所有写操作 Controller 方法均有 @AuditLog 注解;涉及 PII 的方法 recordParams=false。
|
||||
|
||||
---
|
||||
|
||||
### U3. BigDecimal→Long 全链路迁移
|
||||
|
||||
**Goal:** 14 个 entity 的 BigDecimal 金额字段迁移到 Long(分),DDL 同步变更,DTO 使用 `xxxPriceFen` 字段名。
|
||||
|
||||
**Requirements:** R4, SC4
|
||||
|
||||
**Dependencies:** U5(入参校验先行,确保迁移期间数据合法)
|
||||
|
||||
**Files:**
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/Contract.java`(修改:amount/deposit BigDecimal→Long,字段名 `amountPriceFen`/`depositPriceFen`)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/LeaseContract.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/WorkshopLease.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/PublicRevenue.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/CamCharge.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/Device.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/DeviceMaintenance.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/EnergyMeter.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/Room.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/OwnerRoom.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/Project.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/Building.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/Floor.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/Renovation.java`(修改)
|
||||
- `backend/pms-base/src/main/resources/db/migration/V13__migrate_bigdecimal_to_long.sql`(新建:DECIMAL→BIGINT,值 × 100)
|
||||
- 对应 DTO 文件(字段名改为 `xxxPriceFen`)
|
||||
- 对应 ServiceImpl 文件(金额计算逻辑调整)
|
||||
- 对应 Controller 文件(DTO 字段名适配)
|
||||
- 单元测试文件(金额计算测试)
|
||||
|
||||
**Approach:**
|
||||
1. Flyway V13 迁移:`ALTER TABLE t_contract MODIFY amount BIGINT;` + `UPDATE t_contract SET amount = ROUND(amount * 100, 0);`(对所有金额字段)。
|
||||
2. Entity 字段类型 BigDecimal→Long,字段名改为 `xxxPriceFen`(如 `amountPriceFen`、`depositPriceFen`、`monthlyRentPriceFen`)。
|
||||
3. DTO 字段统一命名 `xxxPriceFen`,与 CLAUDE.md 一致。
|
||||
4. Service 层金额计算:加减直接 Long 运算;乘除用 `BigDecimal` 中间计算后 `setScale(0, RoundingMode.HALF_UP).longValueExact()` 转回 Long。
|
||||
5. Controller DTO 字段名变更,前端需协调(前端适配作为 follow-up)。
|
||||
|
||||
**Patterns to follow:** pms-charge 已用 Long(分)的模式;InternalController BigDecimal 修复中的 `setScale(0, RoundingMode.HALF_UP).longValueExact()` 模式。
|
||||
|
||||
**Test scenarios:**
|
||||
- 金额 123.45 元 → 存储 12345(分)
|
||||
- 金额计算:100.00 + 50.50 = 150.50 → 10000 + 5050 = 15050
|
||||
- 金额除法:100 元 ÷ 3 = 33.33 元 → 10000 ÷ 3 = 3333(HALF_UP)
|
||||
- Flyway 迁移:DECIMAL(15,2) 123.45 → BIGINT 12345
|
||||
- DTO 序列化:`{"monthlyRentPriceFen": 123456}`
|
||||
|
||||
**Verification:** V13 迁移成功;所有金额字段无 BigDecimal 残留;金额计算测试通过;DTO 字段命名统一 `xxxPriceFen`。
|
||||
|
||||
---
|
||||
|
||||
### U4. 手机号/邮箱加密
|
||||
|
||||
**Goal:** Owner/Tenant/Enterprise 实体的 phone/email 字段采用 HMAC-SHA256 hash + GCM 加密 + 后4位 hash 索引模式。
|
||||
|
||||
**Requirements:** R5, SC5
|
||||
|
||||
**Dependencies:** U1(AesEncryptUtil GCM 加固后提供可靠加密)
|
||||
|
||||
**Files:**
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/Owner.java`(修改:phone/email 拆分为加密字段 + hash 字段 + 后4位 hash 字段)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/Tenant.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/entity/Enterprise.java`(修改:仅 email,无 phone)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/service/impl/OwnerServiceImpl.java`(修改:加密 + 填充 hash + 后4位 hash)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/service/impl/TenantServiceImpl.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/service/impl/EnterpriseServiceImpl.java`(修改)
|
||||
- `backend/pms-base/src/main/resources/db/migration/V14__encrypt_phone_email.sql`(新建:新增 phone_hash/email_hash/phone_last4_hash/email_last4_hash 字段 + 索引 + 历史数据 Java 迁移脚本)
|
||||
- 查询接口(修改:phone/email 查询走 hash 精确匹配或后4位 hash 尾号查询)
|
||||
- 单元测试
|
||||
|
||||
**注意:** `Project.java` 无 phone/email 字段,不涉及本单元。
|
||||
|
||||
**Approach:**
|
||||
1. Entity 拆分:`phone`(GCM 加密存储)+ `phoneHash`(HMAC-SHA256 精确查询)+ `phoneLast4Hash`(HMAC-SHA256 of 后4位,尾号查询);`email` + `emailHash` + `emailLast4Hash`。
|
||||
2. Service create/update:`setPhone(cryptoUtil.encrypt(plainPhone))` + `setPhoneHash(cryptoUtil.hash(plainPhone))` + `setPhoneLast4Hash(cryptoUtil.hash(plainPhone.substring(7)))`。
|
||||
3. 查询:精确查询 `WHERE phone_hash = cryptoUtil.hash(queryPhone)`;尾号查询 `WHERE phone_last4_hash = cryptoUtil.hash(queryLast4)`。不支持 like 前缀模糊查询。
|
||||
4. API 响应:通过 ResponseBodyAdvice 或 DTO 转换层统一解密 phone/email。
|
||||
5. Flyway V14:新增 `phone_hash`/`email_hash`/`phone_last4_hash`/`email_last4_hash` 字段 + 索引;历史明文数据 Java 迁移脚本批量加密 + 填充 hash。
|
||||
|
||||
**Patterns to follow:** U1 中 idNo/idNoHash 的加密 + hash 模式。
|
||||
|
||||
**Test scenarios:**
|
||||
- 手机号加密存储,明文不出现数据库
|
||||
- phoneHash 精确查询返回正确记录
|
||||
- phoneLast4Hash 尾号查询(后4位)返回正确记录
|
||||
- like 前缀模糊查询不再支持(测试确认抛异常或返回空)
|
||||
- API 响应解密后返回明文手机号
|
||||
- 历史数据迁移后所有 phone_hash/phone_last4_hash 非空
|
||||
- Enterprise 仅 email 加密(无 phone 字段)
|
||||
|
||||
**Verification:** V14 迁移成功;phone/email 加密存储;hash 精确查询 + 尾号查询正确。
|
||||
|
||||
---
|
||||
|
||||
### U5. 入参校验补齐
|
||||
|
||||
**Goal:** 11 个 Controller 的 Entity @RequestBody 加 @Valid + 字段级校验注解。
|
||||
|
||||
**Requirements:** R7
|
||||
|
||||
**Dependencies:** 无
|
||||
|
||||
**Files:**
|
||||
- `backend/pms-base/src/main/java/com/pms/base/controller/OwnerController.java`(修改:@Valid)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/controller/TenantController.java`(修改)
|
||||
- 其他 9 个 Controller
|
||||
- 对应 Entity 文件(修改:字段加 @NotBlank/@Pattern/@Size)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/exception/GlobalExceptionHandler.java`(修改:处理 MethodArgumentNotValidException)
|
||||
|
||||
**Approach:**
|
||||
1. Controller 方法参数加 `@Valid` 注解。
|
||||
2. Entity 关键字段加校验注解:phone `@Pattern(regexp="^1[3-9]\\d{9}$")`;idNo `@Pattern(regexp="^\\d{17}[\\dXx]$")`;name `@NotBlank`;amount `@PositiveOrZero`。
|
||||
3. GlobalExceptionHandler 处理 `MethodArgumentNotValidException`,返回 400 + 字段错误信息。
|
||||
4. 短期方案(Entity 加注解),长期随 U3 BigDecimal 迁移时引入 DTO 层。
|
||||
|
||||
**Patterns to follow:** Spring Validation 标准模式;`@Pattern`/`@NotBlank`/`@Size`。
|
||||
|
||||
**Test scenarios:**
|
||||
- phone="13800138000":校验通过
|
||||
- phone="abc":返回 400 + 错误信息
|
||||
- idNo 格式错误:返回 400
|
||||
- name 为空:返回 400
|
||||
- amount 为负数:返回 400
|
||||
|
||||
**Verification:** 非法入参返回 400;合法入参正常处理。
|
||||
|
||||
---
|
||||
|
||||
### U6. 测试覆盖率补齐
|
||||
|
||||
**Goal:** 13 个零测试 ServiceImpl 补齐单元测试,覆盖加密/金额计算等高风险逻辑。
|
||||
|
||||
**Requirements:** R6, SC6
|
||||
|
||||
**Dependencies:** U1(AesEncryptUtil 加固后测试基础)、U3(BigDecimal 迁移后测试新逻辑)
|
||||
|
||||
**Files:**
|
||||
- `backend/pms-base/src/test/java/com/pms/base/service/OwnerServiceImplTest.java`(新建)
|
||||
- `backend/pms-base/src/test/java/com/pms/base/service/TenantServiceImplTest.java`(新建)
|
||||
- `backend/pms-base/src/test/java/com/pms/base/service/DeviceServiceImplTest.java`(新建/补齐)
|
||||
- 其他 10 个零测试 ServiceImpl 的测试文件
|
||||
- 现有测试文件(修改:适配新签名)
|
||||
|
||||
**Approach:**
|
||||
1. 优先覆盖含加密逻辑(OwnerServiceImpl/TenantServiceImpl)、金额计算(ContractServiceImpl 等)的服务。
|
||||
2. 测试模式:Mockito @ExtendWith + @Mock 依赖 + @InjectMocks 被测服务。
|
||||
3. @Lazy 字段用 ReflectionTestUtils.setField 手动注入(参考 docs/solutions/conventions/lazy-injection-reflectiontestutils-testing.md)。
|
||||
4. 覆盖率目标:每个 ServiceImpl 的 create/update/delete/查询方法都有测试。
|
||||
|
||||
**Patterns to follow:** 现有 LifecycleServiceTest 的测试模式;docs/solutions/conventions/lazy-injection-reflectiontestutils-testing.md 的 @Lazy 注入模式。
|
||||
|
||||
**Test scenarios:**
|
||||
- 各 ServiceImpl 的 CRUD 主流程
|
||||
- 加密字段填充验证(idNoHash/phoneHash/phoneLast4Hash)
|
||||
- 金额计算验证(Long 分)
|
||||
- 异常传播验证
|
||||
|
||||
**Verification:** 13 个 ServiceImpl 都有测试文件;整体测试覆盖率 ≥ 60%。
|
||||
|
||||
---
|
||||
|
||||
### 阶段 3:架构优化 + P3 清理
|
||||
|
||||
### U7. Feign 熔断 + AuthClient fallback 修复
|
||||
|
||||
**Goal:** pms-base 的 Feign 客户端配置 Resilience4j 熔断;AuthClient fallback 抛异常而非返回 null。
|
||||
|
||||
**Requirements:** R8
|
||||
|
||||
**Dependencies:** 无
|
||||
|
||||
**Files:**
|
||||
- `backend/pms-base/build.gradle`(修改:引入 `resilience4j-spring-boot3` 依赖)
|
||||
- `backend/pms-base/src/main/resources/application.yml`(修改:Resilience4j 配置)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/feign/AuthClient.java`(修改:fallback 抛 BusinessException)
|
||||
- `backend/pms-base/src/test/java/com/pms/base/feign/AuthClientFallbackTest.java`(新建)
|
||||
|
||||
**注意:** AuthClient 位于 `com.pms.base.feign` 包(非 `client/`),fallback 是 AuthClient 内部类 `AuthClientFallbackFactory implements FallbackFactory<AuthClient>`,无独立 AuthClientFallback.java 文件。项目使用 Gradle(非 Maven)。
|
||||
|
||||
**Approach:**
|
||||
1. `build.gradle` 引入 Resilience4j 依赖,配置熔断参数(失败率阈值 50%、慢调用阈值 2s、半开状态 10 个请求)。
|
||||
2. AuthClient 的 `@FeignClient` 配置 fallback 类,fallback 方法抛 `BusinessException(ErrorCode.AUTH_SERVICE_UNAVAILABLE)`。
|
||||
3. 其他 Feign 客户端(如有)同样配置 fallback。
|
||||
|
||||
**Patterns to follow:** Spring Cloud OpenFeign + Resilience4j 标准模式。
|
||||
|
||||
**Test scenarios:**
|
||||
- AuthClient 调用成功:正常返回
|
||||
- AuthClient 调用超时:触发熔断
|
||||
- AuthClient fallback 抛异常
|
||||
- 熔断器开启状态:快速失败
|
||||
|
||||
**Verification:** AuthClientFallbackTest 通过;生产环境 Resilience4j 配置生效。
|
||||
|
||||
---
|
||||
|
||||
### U8. @Transactional 补齐
|
||||
|
||||
**Goal:** 3 个 delete 方法补 @Transactional。
|
||||
|
||||
**Requirements:** R9
|
||||
|
||||
**Dependencies:** 无(AesEncryptUtil 异常处理已在 U1 完成)
|
||||
|
||||
**Files:**
|
||||
- `backend/pms-base/src/main/java/com/pms/base/service/impl/DeviceCategoryServiceImpl.java`(修改:delete 补 @Transactional)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/service/impl/ContractTypeServiceImpl.java`(修改)
|
||||
- `backend/pms-base/src/main/java/com/pms/base/service/impl/ProjectServiceImpl.java`(修改)
|
||||
- 对应测试文件
|
||||
|
||||
**Approach:**
|
||||
1. 3 个 delete 方法加 `@Transactional(rollbackFor = Exception.class)`。
|
||||
|
||||
**Patterns to follow:** 现有 12 个 delete 方法已补 @Transactional 的模式(参考 ContractServiceImpl)。
|
||||
|
||||
**Test scenarios:**
|
||||
- delete 失败时事务回滚
|
||||
|
||||
**Verification:** 3 个 delete 方法有 @Transactional。
|
||||
|
||||
---
|
||||
|
||||
### U9. @Lazy 循环依赖文档化
|
||||
|
||||
**Goal:** LifecycleService ↔ ArchiveService 的 @Lazy 循环依赖文档化为已知设计,避免后续重复踩坑。
|
||||
|
||||
**Requirements:** R10
|
||||
|
||||
**Dependencies:** 无
|
||||
|
||||
**Files:**
|
||||
- `CONCEPTS.md`(修改:新增 "@Lazy 循环依赖" 条目)
|
||||
- `docs/solutions/conventions/lazy-injection-reflectiontestutils-testing.md`(修改:补充设计理由)
|
||||
|
||||
**Approach:**
|
||||
1. CONCEPTS.md 新增条目解释 @Lazy 循环依赖的存在原因(归档恢复需同步语义,事件总线异步不适用)和测试注意事项。
|
||||
2. 现有 docs/solutions/conventions/lazy-injection-reflectiontestutils-testing.md 补充设计理由段落。
|
||||
|
||||
**Test scenarios:** 无(文档单元)
|
||||
|
||||
**Verification:** CONCEPTS.md 新增条目;现有文档补充。
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
### OQ-1. AES key 配置中心选型(已解决)
|
||||
|
||||
**状态:** 已解决 — Nacos 配置中心
|
||||
|
||||
Nacos 已在 application.yml 中配置(`spring.cloud.nacos.config`)。AES key 从 Nacos 加载,key 不进 VCS,启动时校验非默认值。
|
||||
|
||||
### OQ-2. 历史数据迁移窗口(已解决)
|
||||
|
||||
**状态:** 已解决 — 停机迁移
|
||||
|
||||
idNo 数据量小,停机 30 分钟可完成。简单,无混合存储问题。
|
||||
|
||||
### OQ-3. BigDecimal 迁移期间前端兼容
|
||||
|
||||
**状态:** 需协调
|
||||
|
||||
DTO 字段名从 `amount`(BigDecimal)改为 `xxxPriceFen`(Long),前端需同步适配。
|
||||
|
||||
**选项:**
|
||||
- ① 后端先行,前端后续排期适配。
|
||||
- ② 后端等前端同步发布。
|
||||
- ③ 后端提供双字段(amount + priceFen)过渡期。
|
||||
|
||||
**默认:** ① 后端先行(前端适配作为 follow-up)。
|
||||
|
||||
---
|
||||
|
||||
## System-Wide Impact
|
||||
|
||||
### 受影响方
|
||||
|
||||
- **pms-base 服务**:全部修复涉及,是主要变更方。
|
||||
- **pms-charge 服务**:U3 BigDecimal 迁移可能影响跨服务调用(如 InternalController 已修复)。
|
||||
- **前端**:U3 DTO 字段名变更需适配;U4 phone/email API 响应格式可能变化。
|
||||
- **运维**:U1 AES key 配置需协调所有环境 Nacos;U1/U3/U4 Flyway 迁移需停机。
|
||||
- **DBA**:V12/V13/V14 迁移脚本需 review。
|
||||
|
||||
### 跨服务协调
|
||||
|
||||
- feat/user-org-perm 合并是鉴权相关工作的前置条件(不在本计划)。
|
||||
- pms-charge 金额单位需与 U3 保持一致(如 pms-charge 未用 Long,需后续迁移)。
|
||||
|
||||
---
|
||||
|
||||
## Risks & Dependencies
|
||||
|
||||
### 风险
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解 |
|
||||
|------|------|------|------|
|
||||
| Hutool 不支持 GCM 模式 | 中 | 高 | 前置 POC 验证;不支持则切 JDK 原生或 BouncyCastle |
|
||||
| AES key 迁移导致历史数据无法解密 | 中 | 高 | 迁移脚本充分测试;保留旧 key 用于回滚 |
|
||||
| BigDecimal 迁移期间金额计算错误 | 中 | 高 | 单元测试覆盖所有金额计算场景;灰度发布 |
|
||||
| P0 越权漏洞持续暴露(鉴权延后) | 高 | 高 | 文档化为已知技术债;feat/user-org-perm 合并后优先处理 |
|
||||
| Feign 熔断参数不当导致误熔断 | 中 | 中 | 灰度观察熔断指标;参数可配置 |
|
||||
|
||||
### 依赖
|
||||
|
||||
- **Hutool GCM 支持**:U1 前置 POC 验证。
|
||||
- **Nacos 配置中心**:U1 AES key 加载依赖 Nacos 已配置。
|
||||
- **Flyway 版本兼容**:V12+ 迁移脚本需与现有 V1-V11 兼容(V11 已被项目生命周期管理占用)。
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Examples
|
||||
|
||||
### AE1. AES 加密修复验证
|
||||
|
||||
**场景:** 用户创建业主记录,填写身份证号 `110101199001011234`。
|
||||
|
||||
**预期:**
|
||||
- 数据库 `t_owner.id_no` 存储密文(GCM 加密,含 IV 前缀 + 16 字节认证标签)
|
||||
- 数据库 `t_owner.id_no_hash` 存储 HMAC-SHA256 哈希
|
||||
- 通过 `idNoHash` 查询能找到该记录
|
||||
- API 响应中 `idNo` 字段返回明文(解密后)
|
||||
- 相同身份证号两次加密产生不同密文(IV 随机)
|
||||
|
||||
### AE2. 金额计算准确性
|
||||
|
||||
**场景:** 创建合同,金额 1234.56 元。
|
||||
|
||||
**预期:**
|
||||
- API 请求:`{"monthlyRentPriceFen": 123456}`
|
||||
- 数据库 `t_contract.amount` 存储 `123456`(Long)
|
||||
- 查询 API 响应:`{"monthlyRentPriceFen": 123456}`
|
||||
|
||||
### AE3. 手机号加密存储
|
||||
|
||||
**场景:** 用户创建业主,手机号 `13800138000`。
|
||||
|
||||
**预期:**
|
||||
- 数据库 `t_owner.phone` 存储密文(GCM 加密)
|
||||
- 数据库 `t_owner.phone_hash` 存储 HMAC-SHA256 哈希
|
||||
- 数据库 `t_owner.phone_last4_hash` 存储后4位 `8000` 的 HMAC-SHA256 哈希
|
||||
- 精确查询 `phone_hash = hash("13800138000")` 返回该记录
|
||||
- 尾号查询 `phone_last4_hash = hash("8000")` 返回该记录
|
||||
- API 响应中 `phone` 返回明文 `13800138000`
|
||||
|
||||
### AE4. 审计日志完整 + PII 脱敏
|
||||
|
||||
**场景:** 管理员修改业主信息(含身份证号)。
|
||||
|
||||
**预期:**
|
||||
- `t_audit_log` 新增一条记录
|
||||
- 记录字段:`user_id`、`project_id`、`module="base"`、`type="UPDATE"`、`description="修改业主信息"`、`record_params=false`
|
||||
- `t_audit_log.params` 字段为空(因涉及 PII,recordParams=false)
|
||||
- 读操作不产生审计日志
|
||||
|
||||
---
|
||||
|
||||
## Sources & Research
|
||||
|
||||
- **pms-base 模块评审报告**(2026-07-02 会话):问题清单
|
||||
- **CLAUDE.md**:金额单位 Long(分) 约定、外部系统对接规范
|
||||
- **CONCEPTS.md**:lifecycle_stage、@Lazy 循环依赖等领域名词
|
||||
- **docs/solutions/conventions/lazy-injection-reflectiontestutils-testing.md**:@Lazy + ReflectionTestUtils 测试模式
|
||||
- **feat/user-org-perm plan**:`docs/plans/2026-07-01-005-refactor-user-org-perm-plan.md`(Spring Security 重构范围,确认不包含加密基础设施)
|
||||
- **Hutool 文档**:`cn.hutool.crypto.symmetric.AES` GCM 模式支持(需 POC 验证)
|
||||
- **JDK Cipher API**:`Cipher.getInstance("AES/GCM/NoPadding")` 备选方案
|
||||
- **《个人信息保护法》第 51 条**:数据安全保护义务
|
||||
- **《网络安全法》第 21 条**:日志留存要求
|
||||
|
|
@ -99,6 +99,16 @@ Mockito `@InjectMocks` 的注入策略是 **构造器注入优先**:当被测
|
|||
|
||||
**反模式:不要试图去掉 `@Lazy` 改用 `ApplicationContextAware` 或 `ObjectProvider`**——这些虽然能避免循环依赖,但引入了不必要的复杂度。`@Lazy` 是 Spring 官方推荐的循环依赖解法,测试侧只需一行 `setField` 即可适配。
|
||||
|
||||
## Design Rationale: 为什么不用事件总线解耦
|
||||
|
||||
`LifecycleServiceImpl` 与 `ArchiveServiceImpl` 的互调看似可以用 MQ 事件总线解耦(归档完成发事件 → 生命周期监听推进阶段),但当前设计选择 `@Lazy` 同步调用,理由:
|
||||
|
||||
1. **事务一致性**:归档恢复要求"标记归档只读解除 + 生命周期阶段推进"在同一事务内完成。若用 MQ 异步事件,归档只读已解除但阶段推进失败,项目会处于不一致态(可写但 lifecycle_stage=ARCHIVED)。
|
||||
2. **失败语义清晰**:同步调用失败直接抛异常回滚事务;异步事件失败需额外设计重试 + 死信队列 + 幂等,复杂度远高于一行 `@Lazy`。
|
||||
3. **调用路径短**:仅两个 Service 互调,无扇出场景。事件总线的优势在于一对多解耦,此处不适用。
|
||||
|
||||
若未来归档流程演进为多服务协作(如归档后通知计费服务结算、通知资产服务移交),应将跨服务部分拆为 MQ 事件,但 `LifecycleService ↔ ArchiveService` 的同事务互调保留 `@Lazy`。
|
||||
|
||||
## Related
|
||||
|
||||
- [CONCEPTS.md — 项目生命周期管理](../../CONCEPTS.md)(本模式首次出现的业务场景)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue