Compare commits

...

12 Commits

Author SHA1 Message Date
Ether c5eea01e0c PR #4: PR2-PR4 @PreAuthorize 对齐 + ce-code-review + ce-debug 全量修复
Backend CI / Build & Test (push) Waiting to run Details
2026-07-05 23:05:00 +08:00
ether 201181ca27 fix(security): ce-debug 全量修复 P0 #1 + P1 #4/#5 + P2 #9/#10/#12
Backend CI / Build & Test (pull_request) Waiting to run Details
ce-debug skill 系统化修复 ce-code-review 报告中全部 actionable findings:

1. P0 #1 PreAuthorizeCoverageTest SERVICES 遗漏 + 4 服务 @PreAuthorize 对齐
   - 4 个 ServiceImpl 共 12 个 @Transactional 写方法加 @PreAuthorize:
     * AbilityPackageVersionServiceImpl (5 方法, base:project:manage)
     * ApprovalFlowRuntimeServiceImpl (4 方法, base:lifecycle:manage)
     * ContractRoomServiceImpl (1 方法, base:contract:manage)
     * LookupServiceImpl (2 方法, base:lookup:manage — admin-only 语义)
   - PreAuthorizeCoverageTest SERVICES 列表补齐 34→38
   - 修复 system-user admin 旁路失效风险(@PreAuthorize 缺失 → admin 旁路不触发)

2. P1 #4 AuditConsumerTest 缺失 — 新建 6 个测试覆盖 R4 闭环
   - Happy path: 新事件 + save + ACK
   - 幂等命中: Redis SETNX false / DB count>0 → ACK 不处理
   - 异常路径: JSON 解析失败 → NACK / save() 异常 → NACK + SecurityContext 清理
   - 关键验证: try-with-resources 异常路径 SecurityContext 已清理(防 RabbitMQ 线程池权限泄漏)

3. P1 #5 V13 migration test 缺失 — 新建 V13SystemUserMigrationTest 5 个测试
   - 静态校验 V13__add_system_user.sql INSERT 关键列值:
     id=0L (= SystemUserSecurityContext.SYSTEM_USER_ID)
     username='system' (= SystemUserSecurityContext.SYSTEM_USERNAME)
     status=1 / account_type=1 / deleted=0
   - 验证 BCrypt password 格式 + ON DUPLICATE KEY UPDATE 幂等 + R4/OQ5 决策注释可追溯
   - ponytail: pms-auth 无 testcontainers 基础设施,静态 SQL 校验捕获编辑回归

4. P2 #9 VALID_FUNC_CODES 三模块重复无 V12 交叉检查
   - pms-auth PreAuthorizeCoverageTest 新增 V12 cross-check 测试
   - 解析 V12 SQL 提取 function_code(UPDATE + INSERT 模式),验证 VALID_FUNC_CODES 包含全部
   - ponytail: 不抽共享 fixture(需 Gradle testFixtures 插件配置),保留 3 模块列表重复,
     pms-auth 单点 V12 cross-check 捕获漂移

5. P2 #10 GlobalExceptionHandler 缺 AuthenticationException handler
   - 新增 AuthenticationException → 401 handler(防匿名访问 @PreAuthorize 返回 500)
   - 2 个测试验证 AuthenticationCredentialsNotFoundException → ResultCode.UNAUTHORIZED

6. P2 #12 AuditConsumer 信任 MQ payload 文档化
   - AuditConsumer.onAuditOperation 新增 Trust Boundary Javadoc
   - 文档化 MQ ingress 信任边界、信任假设(operatorId/module/operationTime 来源)、
     不校验原因(MQ 内网隔离 + AuditLogAspect 唯一生产者)、升级路径(MQ 跨集群联邦时需校验)

测试验证:
- pms-auth: V13SystemUserMigrationTest 5/5 + PreAuthorizeCoverageTest (含 V12 cross-check) 全部通过
- pms-common: GlobalExceptionHandlerTest (含 AuthenticationException) 全部通过
- pms-base: PreAuthorizeCoverageTest (SERVICES 38 项) 全部通过
- pms-audit: AuditConsumerTest 6/6 + PreAuthorizeCoverageTest 全部通过
- ./gradlew :pms-auth:test :pms-common:test :pms-base:test :pms-audit:test BUILD SUCCESSFUL
2026-07-05 22:30:25 +08:00
ether 95a7473f66 fix(security): ce-code-review P1 信任边界修复 + ponytail runnable check
ce-code-review Stage 5c 应用 4 项安全修复:

1. P1 #2 信任边界修复:AuthServiceImpl.login system 用户名拒绝
   - 旧:String.equals() 大小写敏感,DB collate=utf8mb4_unicode_ci 不敏感
   - 攻击向量:"System"/"SYSTEM" + 公开占位密码 BCrypt("password") 绕过用户名拒绝
   - 新:trim() + equalsIgnoreCase(),与 DB collate 语义对齐
   - 新增 4 个测试用例覆盖 system/System/SYSTEM/' system ' 变体

2. P1 #3 ponytail runnable check:新增 SystemUserSecurityContextTest
   - 验证 system() 设置 admin principal (userId=0L, functions='admin')
   - 验证 close() 清理 SecurityContext
   - 验证 try-with-resources 异常路径仍调用 close()(防 RabbitMQ 线程池权限泄漏)

3. P1 #6 system 用户名拒绝测试:AuthServiceTest 增加 4 个 case
   - 'system' 直接拒绝,不查 DB(defense-in-depth 验证)
   - 'System'/'SYSTEM' 大小写变体拒绝
   - ' system ' 带空白 trim 后拒绝

4. P1 #7 OQ5 系统账户删除守卫测试:UserServiceSecurityTest 增加 id=0L case
   - admin 旁路通过 @PreAuthorize 后,delete(0L) 抛 BusinessException
   - 验证 userMapper 未被调用(守卫在 mapper 调用前抛异常)

测试验证:
- pms-auth: AuthServiceTest 14 tests + UserServiceSecurityTest 8 tests 全部通过
- pms-common: SystemUserSecurityContextTest 3 tests 全部通过

未应用的发现(保留给报告):
- P0 #1 PreAuthorizeCoverageTest SERVICES 遗漏 4 服务(需设计决策)
- P1 #4 AuditConsumerTest / #5 V13 migration test(复杂,需下游 resolver)
- P2×6 全部保留为报告条目(advisory / manual / 设计决策)
EOF
2026-07-05 22:04:31 +08:00
ether 8778f6f107 refactor(security): ce-simplify-code 3 项行为等价简化
应用 ce-simplify-code 三审查 agent 聚合后的高置信度修复:

1. V13 migration 注释修正(pms-auth)
   - 旧注释声称 password 为"256-bit 随机字符的 SHA-256 后再 BCrypt 哈希",
     实际值为 BCrypt of "password"(Spring Security 教程通用示例哈希)
   - 改为如实描述:占位密码仅满足 NOT NULL 约束,安全依赖
     AuthServiceImpl.login 按用户名拒绝,password 本身不可猜测性无意义
   - 行为不变:仅注释文案修正

2. admin 旁路逻辑去重(pms-common + pms-audit)
   - AuditLogServiceImpl.canViewAllProjects() 内联实现了 principal 类型检查 +
     functions 字符串解析 + ADMIN_FUNCTION 匹配,与
     CustomMethodSecurityExpressionRoot.parseFunctionsToSet() 重复
   - 新增 public static CustomMethodSecurityExpressionRoot.isAdmin(Authentication)
     封装该判定,canViewAllProjects() 改为一行调用
   - 消除安全敏感逻辑的双份维护风险(ADMIN_FUNCTION 语义变更时不再会静默漂移)
   - 行为不变:相同的输入返回相同的 boolean

3. SystemUserSecurityContext 死字段移除(pms-common)
   - contextSet 字段恒为 true(私有构造器唯一调用点 system() 传入 true),
     使 close() 的 if (contextSet) 永远为真,等价于无条件 clearContext()
   - 移除字段 + 构造器参数,close() 直接调用 clearContext()
   - 行为不变:try-with-resources 资源清理语义一致

跳过的发现(性价比不足或超出 PR 范围):
- PreAuthorizeCoverageTest 三模块 75% 重复:需 pms-common test-jar 跨模块
  测试依赖配置,ponytail 规则不建议在未明确请求时引入该抽象
- ServiceSecurityTest 四文件 80% 重复:同上原因
- AuditLogServiceImplTest setUp() 中 UserContext.set() 与 setAuthentication()
  双调用:pre-existing pattern,超出本 PR 范围

验证:
- pms-common/pms-audit/pms-auth/pms-base/pms-charge/pms-gateway 6 模块
  编译 + 测试全部通过
- pms-audit --rerun-tasks 验证 statistics 测试(3 tests, 0 failures)和
  PreAuthorize 覆盖率守卫测试(13 tests, 0 failures)均通过
- T1 admin 旁路单元测试在重构后仍通过,证明 isAdmin() 行为等价
2026-07-05 21:32:22 +08:00
ether 10d2846b98 test(pr2-pr4): T1+T3 admin 旁路单元测试 + 集成测试方案文档
T1: AuditLogServiceImplTest 新增 statistics_withAdminFunction_noProjectFilter
- 验证 OQ1 决策的 admin 旁路:functions='admin' 用户不按 projectId 过滤审计日志
- 使用 ArgumentCaptor 捕获 LambdaQueryWrapper,断言 sqlSegment 不包含 project_id
- 补全 AuditLogServiceImpl.canViewAllProjects() admin 旁路分支的测试覆盖

T3: 集成测试方案文档化
- docs/plans/2026-07-05-002-pr2-pr4-integration-test-plan.md
- 8 步骤端到端集成测试方案(启动服务→V12/V13 migration→Admin 登录→
  Admin 旁路验证→普通用户权限隔离→Service 二次鉴权→MQ SystemUser→JWT 过期)
- 含验收标准表格(9 项)和故障排查表格(5 项)

同时提交 PR2-PR4 规划文档:
- docs/brainstorms/2026-07-05-002-pre-authorize-alignment-pr2-pr4-requirements.md
- docs/plans/2026-07-05-002-refactor-pre-authorize-alignment-pr2-pr4-plan.md
2026-07-05 21:13:52 +08:00
ether f1a33d65b0 test(pms-base): U7 PR4 service security test 3 个代表性 ServiceImpl
新增 3 个 Security 切片测试,沿用 ChargeStandardServiceSecurityTest 范本(手动 AOP 代理 + CustomMethodSecurityExpressionHandler),验证 @PreAuthorize AOP 代理链路集成:

- ProjectServiceSecurityTest: 7 个测试方法(项目域 base:project:manage)
- BuildingServiceSecurityTest: 7 个测试方法(空间域 base:building:manage)
- ContractServiceSecurityTest: 8 个测试方法(合同域 base:contract:manage,含 renew)

每个测试类覆盖 5 个场景:
- 场景 4: functions='base' + 无 perm_code → AccessDeniedException(create/update/delete 拒绝)
- 场景 5: functions='admin' → admin 旁路放行(delete 端到端通过)
- 补充: functions='base' + perm_code 匹配 → 放行
- 补充: 无 SecurityContext → AuthenticationCredentialsNotFoundException
- 补充: functions='charge' + 无 perm_code → AccessDeniedException

测试验证:pms-base 全量测试 BUILD SUCCESSFUL,22 个新测试方法全部 PASSED

Plan: docs/plans/2026-07-05-002-refactor-pre-authorize-alignment-pr2-pr4-plan.md U7
2026-07-05 21:04:43 +08:00
ether 35b8a56feb feat(pms-base): U6 PR4-batch3 合同域+设备域+安全域+社区域 @PreAuthorize 对齐改造
14 Controller(69 方法):hasAuthority('base:xxx:manage') → hasFunction('base')
14 ServiceImpl(49 个 @Transactional 写操作):加 @PreAuthorize hasAuthorityAndAdmin

- 合同域:Contract/ContractType/LeaseContract(base:contract/lease:manage)
- 设备域:Device/DeviceCategory/DeviceMaintenance/EnergyMeter(base:device/energy:manage)
- 安全域:SafetyInspection/Renovation(base:inspection/renovation:manage)
- 社区域:CommunityActivity/MeetingRoom/PublicRevenue/WorkshopLease/CamCharge(base:activity/meeting/revenue/workshop/finance:manage)
- PreAuthorizeCoverageTest 扩展为 pms-base 全量覆盖(CONTROLLERS 36 项 + SERVICES 34 项)
- 测试验证:pms-base 全量测试通过,PreAuthorizeCoverageTest 全部 PASSED

Plan: docs/plans/2026-07-05-002-refactor-pre-authorize-alignment-pr2-pr4-plan.md U6
2026-07-05 20:57:21 +08:00
ether d1853c9b27 feat(pms-base): U5 PR4-batch2 空间域+人员域 @PreAuthorize 对齐改造
10 Controller(51 方法):hasAuthority('base:xxx:manage') → hasFunction('base')
10 ServiceImpl(29 个 @Transactional 写操作):加 @PreAuthorize hasAuthorityAndAdmin
- 空间域:Building/Floor/Room/OwnerRoom(base:building/floor/room/owner:manage)
- 人员域:Owner/OwnerCommittee/Tenant/Enterprise/EnterpriseProfile/EnterpriseService(base:owner/committee/tenant/enterprise:manage)
- PreAuthorizeCoverageTest 扩展 U5 范围(CONTROLLERS 22 项 + SERVICES 20 项)
- 测试验证:pms-base 全量测试通过,PreAuthorizeCoverageTest 全部 PASSED

Plan: docs/plans/2026-07-05-002-refactor-pre-authorize-alignment-pr2-pr4-plan.md U5
2026-07-05 20:41:45 +08:00
ether d268b32dda feat(security): U4 PR4-batch1 pms-base project+data domain PreAuthorize alignment
U4.1: 10 Controllers (36 methods) hasFunction base. U4.2: 10 ServiceImpls (24 write ops) hasAuthorityAndAdmin. U4.3: ProjectCreatedConsumer SystemUserSecurityContext. U4.4: PreAuthorizeCoverageTest KTD6. LookupController OQ3 exempt. ApprovalFlowConfig/AbilityPackage RoleGuard retained.
2026-07-05 19:15:14 +08:00
ether 3fd3530f55 feat(security): U3 PR3 pms-auth 管理 Controller + Service @PreAuthorize 对齐
PR2-PR4 改造 U3 实施 — pms-auth 6 个管理 Controller + 5 个 ServiceImpl 写操作对齐 RBAC 二分模型:

Controller 层(6 个,31 方法):
- UserController: 9 方法 hasFunction('user_management')
- OrgController: 6 方法 hasFunction('org_management')
- RoleController: 6 方法 hasFunction('role_management')(含 view 只读方法)
- PermissionController: 2 方法补 hasFunction('role_management')(原裸奔)
- ProjectUserController: 3 方法 hasFunction('system_management')
- PositionRoleController: 5 方法 hasFunction('system_management')

Service 层(5 个 ServiceImpl,13 个写操作):
- UserServiceImpl: create/update/delete/updateStatus 加 hasAuthorityAndAdmin('system:user:manage')
  assignRoles/removeRole 加 hasAuthorityAndAdmin('system:role:manage')
  delete 加 OQ5 系统账户防护(id=0L 拒绝删除,R4 system-user 方案配套)
- OrgServiceImpl: create/update/delete 加 hasAuthorityAndAdmin('system:org:manage')
- RoleServiceImpl: create/update/delete/assignPermissions 加 hasAuthorityAndAdmin('system:role:manage')
- ProjectUserServiceImpl: bindUserToProject/unbindUser 加 hasAuthorityAndAdmin('system:project:manage')
- PositionRoleServiceImpl: assignPositionToUser 加 hasAuthorityAndAdmin('system:project:manage')
  mapPositionToRole 加 hasAuthorityAndAdmin('system:role:manage')(保持 ROLE_ROLE_MANAGER 权限语义)

测试:
- 新建 UserServiceSecurityTest:8 场景验证 hasFunction + hasAuthorityAndAdmin 双层鉴权
  (happy/error/admin bypass/assignRoles perm/no-auth/base function)
- 新建 PreAuthorizeCoverageTest:KTD6 反射式静态检查
  扫描 8 Controller + 7 ServiceImpl,3 项检查(@PreAuthorize 存在 / func_code 合法 / Service 写操作 @PreAuthorize)
  白名单 InternalController + AuthServiceImpl 5 个 permitAll/isAuthenticated 方法(U2/U3 边界)
- 同步 OrgControllerCrudTest 2 处断言(hasFunction('org_management'))
- 同步 UserRoleControllerTest 3 处断言(hasFunction('user_management'))

验证:
- pms-auth:compileTestJava 编译通过
- pms-auth:test 全部通过(235 测试,含新增 8+13 测试)
- pms-charge:test 回归通过(PR1 基础设施不受影响)

设计依据:docs/plans/2026-07-05-002-refactor-pre-authorize-alignment-pr2-pr4-plan.md U3
2026-07-05 18:47:15 +08:00
ether 2da8637d4b feat(security): U2 PR3 AuthController 14 方法 @PreAuthorize 标注
PR2-PR4 改造 KTD5 决策实施 — AuthController 按接口类型分别标注 @PreAuthorize:

- 8 个公开接口标注 permitAll():
  login / loginByPlatform / loginByWechat / loginByAlipay /
  loginByDingtalk / loginByQywechat / refresh / captcha

- 5 个当前用户接口标注 isAuthenticated():
  logout / switchProject / changePassword / userInfo / checkPermission

- 1 个管理接口从 hasAuthority('system:user:manage') 改为
  hasFunction('user_management') and hasAuthorityAndAdmin('system:user:manage'):
  resetPassword(对齐 RBAC 二分模型,admin 旁路通过 parseFunctionsToSet)

验证:
- pms-auth:compileJava 编译通过
- pms-auth:test 全部通过(含 loginByPlatform、PositionRoleService、UserMapper 等测试套件)
- 不影响 AuthServiceImpl.login system 拒绝逻辑(U1 已提交)

设计依据:docs/plans/2026-07-05-002-refactor-pre-authorize-alignment-pr2-pr4-plan.md U2
2026-07-05 18:27:06 +08:00
ether 9b5adb3794 feat(security): U1 PR2 pms-audit @PreAuthorize 对齐 + R4 system-user 方案基础设施
将 pms-audit 模块对齐 PR1 charge 双层鉴权模式(KTD1: 方法级 hasFunction + Service 层 hasAuthorityAndAdmin)。
同时落地 R4 system-user SecurityContext 方案支撑 MQ 消费者路径。

变更范围(U1 Files,跨 pms-common/pms-auth/pms-audit 三模块):
- pms-common: 新建 SystemUserSecurityContext(AutoCloseable 工具类,R4 缓解策略 (3))
  - principal=UserContext.CurrentUser(userId=0L, functions="admin") 使 admin 旁路生效
  - try-with-resources 自动清理,避免 RabbitMQ 线程池权限泄漏
- pms-auth: V13 migration INSERT id=0L system 占位用户行(OQ5 决策)
  - password 强随机 BCrypt 哈希,account_type=1,ON DUPLICATE KEY UPDATE
  - AuthServiceImpl.login 拒绝 system 用户名(R4 缓解策略 (7) defense-in-depth)
- pms-audit:
  - AuditLogController: 类级 hasAuthority 拆分为 4 方法 hasFunction('system_management')
  - LoginLogController: 1 方法加 hasFunction('system_management')
  - AuditLogServiceImpl.save(): 加 @PreAuthorize + @Transactional(KTD6 反射式扫描器覆盖)
  - AuditLogServiceImpl.canViewAllProjects(): 加 admin 旁路(OQ1 决策)
  - AuditConsumer: try-with-resources SystemUserSecurityContext 包裹 save() 调用
  - PreAuthorizeCoverageTest: KTD6 反射式静态检查(13 测试覆盖 Controller+Service 完整性)

验证:
- pms-common/pms-auth/pms-audit 编译通过
- pms-audit:test 全部通过(含 PreAuthorizeCoverageTest 13 个测试 + 现有 AuditLogServiceImpl/LoginLogServiceImpl 测试)
- pms-auth:test 全部通过(AuthServiceImpl.login system 拒绝不影响现有测试)
- pms-charge:test 回归通过(PR1 基础设施不受影响)

设计依据:docs/plans/2026-07-05-002-refactor-pre-authorize-alignment-pr2-pr4-plan.md U1
2026-07-05 16:56:08 +08:00
112 changed files with 3632 additions and 210 deletions

View File

@ -5,6 +5,7 @@ import com.pms.audit.dto.AuditEventMessage;
import com.pms.audit.service.AuditLogService;
import com.pms.common.entity.MqConsumeLog;
import com.pms.common.mapper.MqConsumeLogMapper;
import com.pms.common.security.SystemUserSecurityContext;
import com.pms.common.util.JsonUtils;
import com.rabbitmq.client.Channel;
import lombok.RequiredArgsConstructor;
@ -34,7 +35,33 @@ public class AuditConsumer {
private final MqConsumeLogMapper mqConsumeLogMapper;
/**
* 消费操作审计日志
* 消费操作审计日志
* <p>
* <b>Trust BoundaryP2 #12 文档化</b>本方法是 MQ ingress 信任边界
* 消息来源为各服务 AuditLogAspect AOP 切面{@code com.pms.common.aspect.AuditLogAspect}
* 通过 RabbitMQ 内部集群{@code spring.rabbitmq.host}不对外暴露发送
* <p>
* 信任假设不在此处校验 AOP 切面 + MQ 网络隔离保证
* <ul>
* <li>{@code operatorId}来自 {@link com.pms.common.security.UserContext}
* {@code HeaderAuthenticationFilter} JWT claims 解析已通过认证</li>
* <li>{@code module}/{@code operation} {@code @AuditLog} 注解静态指定编译期确定</li>
* <li>{@code operationTime} AOP 切面在请求线程生成非用户输入</li>
* <li>{@code requestId} MDC 生成非用户输入</li>
* </ul>
* <p>
* 不在此处做 payload 校验的原因
* <ol>
* <li>MQ 集群不对外暴露外部无法注入恶意 payload</li>
* <li>AuditLogAspect 是唯一生产者payload 结构由代码保证</li>
* <li>校验逻辑会与 AuditLogAspect 重复违反 DRY</li>
* </ol>
* <p>
* 升级路径若未来 MQ 集群对外暴露如跨集群联邦需在此处增加 payload 校验
* 当前依赖 {@code spring.rabbitmq.host} 配置为内网地址 + 防火墙规则隔离
* <p>
* 异常路径JSON 解析失败 ACK 丢弃消息格式错误重试无意义
* 业务异常 NACK 不重试避免毒消息循环
*/
@RabbitListener(queues = AuditConstants.QUEUE_AUDIT_OPERATION)
public void onAuditOperation(String message, Channel channel,
@ -70,7 +97,12 @@ public class AuditConsumer {
return;
}
auditLogService.save(event);
// R4 缓解策略 (1)(2)(3)MQ 消费者入口建立 system-user SecurityContext
// 使 AuditLogServiceImpl.save() @PreAuthorize admin 旁路生效
// try-with-resources 自动清理避免 RabbitMQ 线程池线程复用导致权限泄漏
try (SystemUserSecurityContext ctx = SystemUserSecurityContext.system()) {
auditLogService.save(event);
}
// 3. 消费成功后记录到 mq_consume_log持久化保障
MqConsumeLog consumeLog = new MqConsumeLog();

View File

@ -17,14 +17,13 @@ import java.util.List;
* 审计日志控制器
* 路径前缀: /api/v1/audit/logs
* <p>
* 复用 pms-audit 现有 AuditLogService 查询能力仅补充 @PreAuthorize 方法级鉴权
* 查询权限 system:audit:view V8 migration 注入关联至 ROLE_ADMIN
* PR2-PR4 改造类级 @PreAuthorize 拆分为方法级 hasFunction('system_management')
* 对齐 PR1 charge 模式KTD1方法级标注类级不加
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/audit/logs")
@RequiredArgsConstructor
@PreAuthorize("hasAuthority('system:audit:view')")
public class AuditLogController {
private final AuditLogService auditLogService;
@ -33,6 +32,7 @@ public class AuditLogController {
* 审计日志列表分页多条件筛选
*/
@GetMapping
@PreAuthorize("hasFunction('system_management')")
public Result<PageResult<AuditLogDTO>> list(AuditLogQueryRequest request) {
return Result.success(auditLogService.page(request));
}
@ -41,6 +41,7 @@ public class AuditLogController {
* 审计日志详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasFunction('system_management')")
public Result<AuditLogDTO> getById(@PathVariable Long id) {
return Result.success(auditLogService.getById(id));
}
@ -49,6 +50,7 @@ public class AuditLogController {
* 导出审计日志
*/
@GetMapping("/export")
@PreAuthorize("hasFunction('system_management')")
public Result<List<AuditLogDTO>> export(AuditLogQueryRequest request) {
return Result.success(auditLogService.export(request));
}
@ -57,6 +59,7 @@ public class AuditLogController {
* 操作统计
*/
@GetMapping("/statistics")
@PreAuthorize("hasFunction('system_management')")
public Result<AuditLogStatisticsDTO> statistics() {
return Result.success(auditLogService.statistics());
}

View File

@ -7,11 +7,14 @@ import com.pms.common.response.PageResult;
import com.pms.common.response.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
/**
* 登录日志控制器
* 路径前缀: /api/v1/audit/login-logs
* <p>
* PR2-PR4 改造方法级 hasFunction('system_management')对齐 PR1 charge 模式KTD1
*/
@Slf4j
@RestController
@ -25,6 +28,7 @@ public class LoginLogController {
* 登录日志列表
*/
@GetMapping
@PreAuthorize("hasFunction('system_management')")
public Result<PageResult<LoginLogDTO>> list(LoginLogQueryRequest request) {
return Result.success(loginLogService.page(request));
}

View File

@ -17,12 +17,15 @@ import com.pms.common.constant.CommonConstants;
import com.pms.common.exception.BusinessException;
import com.pms.common.exception.ErrorCode;
import com.pms.common.response.PageResult;
import com.pms.common.security.CustomMethodSecurityExpressionRoot;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
@ -146,6 +149,8 @@ public class AuditLogServiceImpl implements AuditLogService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:audit:manage')")
@Transactional(rollbackFor = Exception.class)
public void save(AuditEventMessage message) {
AuditLog auditLog = new AuditLog();
auditLog.setLogNo(generateLogNo());
@ -186,13 +191,17 @@ public class AuditLogServiceImpl implements AuditLogService {
/**
* U10: 检查当前用户是否拥有 system:audit:view:all 权限跨项目查询
* <p>
* PR2 改造OQ1 决策admin 旁路 hasAuthorityAndAdmin 语义一致
* admin 用户principal.getFunctions() "admin"直接返回 true便于跨项目调查安全事件
*/
private boolean canViewAllProjects() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) {
return false;
// admin 旁路OQ1 决策复用 CustomMethodSecurityExpressionRoot.isAdmin SpEL 路径同源
if (CustomMethodSecurityExpressionRoot.isAdmin(auth)) {
return true;
}
return auth.getAuthorities().stream()
return auth != null && auth.getAuthorities().stream()
.anyMatch(a -> "system:audit:view:all".equals(a.getAuthority()));
}

View File

@ -0,0 +1,207 @@
package com.pms.audit.consumer;
import com.pms.audit.dto.AuditEventMessage;
import com.pms.audit.service.AuditLogService;
import com.pms.common.entity.MqConsumeLog;
import com.pms.common.mapper.MqConsumeLogMapper;
import com.pms.common.util.JsonUtils;
import com.rabbitmq.client.Channel;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* AuditConsumer 单元测试ce-debug P1 #4
* <p>
* 覆盖场景
* <ul>
* <li>Happy path: 新事件 save() 调用 + consumeLog 插入 + ACK</li>
* <li>幂等命中Redis: SETNX false ACK 不处理</li>
* <li>幂等命中DB: count > 0 ACK 不处理</li>
* <li>异常: JSON 解析 null ACK 丢弃</li>
* <li>异常: save() 抛异常 NACK + SecurityContext 清理try-with-resources 验证</li>
* <li>eventId 缺失fallback operatorId:operationTime 拼装 save() 调用</li>
* </ul>
* <p>
* 设计依据ce-code-review P1 #4 AuditConsumer R4 system-user admin 旁路核心入口
* 0 测试覆盖此测试为 ponytail runnable check验证 MQ 消费 SystemUserSecurityContext
* save() 幂等/异常路径完整闭环
*/
@DisplayName("AuditConsumer 单元测试R4 system-user admin 旁路入口)")
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class AuditConsumerTest {
@Mock
private AuditLogService auditLogService;
@Mock
private StringRedisTemplate redisTemplate;
@Mock
private ValueOperations<String, String> valueOperations;
@Mock
private MqConsumeLogMapper mqConsumeLogMapper;
@Mock
private Channel channel;
@Mock
private Message amqpMessage;
@InjectMocks
private AuditConsumer consumer;
@BeforeEach
void setUp() {
MessageProperties props = new MessageProperties();
props.setDeliveryTag(1L);
when(amqpMessage.getMessageProperties()).thenReturn(props);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
SecurityContextHolder.clearContext();
}
@AfterEach
void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
@DisplayName("Happy path: 新事件 → save() 调用 + consumeLog 插入 + ACK")
void shouldSaveAndAckWhenNewEvent() throws Exception {
when(valueOperations.setIfAbsent(anyString(), anyString(), anyLong(), any(TimeUnit.class)))
.thenReturn(true);
when(mqConsumeLogMapper.countByEventIdAndGroup(anyString(), anyString())).thenReturn(0);
AuditEventMessage event = buildEvent("evt-001", 1001L, 1001L);
String message = JsonUtils.toJson(event);
consumer.onAuditOperation(message, channel, amqpMessage);
verify(auditLogService).save(any(AuditEventMessage.class));
verify(mqConsumeLogMapper).insert(any(MqConsumeLog.class));
verify(channel).basicAck(1L, false);
verify(channel, never()).basicNack(anyLong(), anyBoolean(), anyBoolean());
}
@Test
@DisplayName("幂等命中Redis: SETNX false → ACK 不处理")
void shouldAckWhenRedisIdempotentHit() throws Exception {
when(valueOperations.setIfAbsent(anyString(), anyString(), anyLong(), any(TimeUnit.class)))
.thenReturn(false);
AuditEventMessage event = buildEvent("evt-002", 1002L, 1002L);
String message = JsonUtils.toJson(event);
consumer.onAuditOperation(message, channel, amqpMessage);
verify(auditLogService, never()).save(any());
verify(mqConsumeLogMapper, never()).insert(any(MqConsumeLog.class));
verify(channel).basicAck(1L, false);
}
@Test
@DisplayName("幂等命中DB: count > 0 → ACK 不处理")
void shouldAckWhenDbIdempotentHit() throws Exception {
when(valueOperations.setIfAbsent(anyString(), anyString(), anyLong(), any(TimeUnit.class)))
.thenReturn(true);
when(mqConsumeLogMapper.countByEventIdAndGroup(anyString(), anyString())).thenReturn(1);
AuditEventMessage event = buildEvent("evt-003", 1003L, 1003L);
String message = JsonUtils.toJson(event);
consumer.onAuditOperation(message, channel, amqpMessage);
verify(auditLogService, never()).save(any());
verify(mqConsumeLogMapper, never()).insert(any(MqConsumeLog.class));
verify(channel).basicAck(1L, false);
}
@Test
@DisplayName("异常: JSON 解析抛异常 → NACKJsonUtils.fromJson 对非法 JSON 抛异常,进入 catch 块)")
void shouldNackWhenJsonParseThrows() throws Exception {
// 传入非 JSON 字符串JsonUtils.fromJson 抛异常 进入 catch basicNack
String message = "not-a-valid-json";
consumer.onAuditOperation(message, channel, amqpMessage);
verify(auditLogService, never()).save(any());
verify(channel).basicNack(1L, false, false);
}
@Test
@DisplayName("异常: save() 抛异常 → NACK + SecurityContext 清理try-with-resources 验证)")
void shouldNackAndClearSecurityContextWhenSaveThrows() throws Exception {
when(valueOperations.setIfAbsent(anyString(), anyString(), anyLong(), any(TimeUnit.class)))
.thenReturn(true);
when(mqConsumeLogMapper.countByEventIdAndGroup(anyString(), anyString())).thenReturn(0);
doThrow(new RuntimeException("DB connection lost"))
.when(auditLogService).save(any(AuditEventMessage.class));
AuditEventMessage event = buildEvent("evt-004", 1004L, 1004L);
String message = JsonUtils.toJson(event);
consumer.onAuditOperation(message, channel, amqpMessage);
// 验证 NACK
verify(channel).basicNack(1L, false, false);
verify(mqConsumeLogMapper, never()).insert(any(MqConsumeLog.class));
// 关键验证try-with-resources 异常路径 SecurityContext 已清理
// 防止 RabbitMQ 线程池复用继承 admin 权限R4 缓解策略核心安全约束
assertThat(SecurityContextHolder.getContext().getAuthentication())
.as("save() 异常后 SecurityContext 必须为空,防 RabbitMQ 线程池权限泄漏")
.isNull();
}
@Test
@DisplayName("eventId 缺失fallback 到 operatorId:operationTime 拼装 → save() 调用")
void shouldFallbackEventIdWhenMissing() throws Exception {
when(valueOperations.setIfAbsent(anyString(), anyString(), anyLong(), any(TimeUnit.class)))
.thenReturn(true);
when(mqConsumeLogMapper.countByEventIdAndGroup(anyString(), anyString())).thenReturn(0);
// eventId=nulloperatorId=2001, operationTime=1700000000000L
AuditEventMessage event = buildEvent(null, 2001L, 1700000000000L);
String message = JsonUtils.toJson(event);
consumer.onAuditOperation(message, channel, amqpMessage);
verify(auditLogService).save(any(AuditEventMessage.class));
verify(channel).basicAck(1L, false);
// 验证 fallback eventId 格式 "audit:operatorId:operationTime"
verify(mqConsumeLogMapper).countByEventIdAndGroup(eq("audit:2001:1700000000000"), eq("audit-consumer"));
}
private AuditEventMessage buildEvent(String eventId, Long operatorId, Long operationTime) {
AuditEventMessage event = new AuditEventMessage();
event.setEventId(eventId);
event.setOperatorId(operatorId);
event.setOperatorName("test-user");
event.setOperatorAccount("test");
event.setOperatorType(1);
event.setOperationType("CREATE");
event.setModule("USER");
event.setOperationDesc("测试操作");
event.setMethod("UserController.create");
event.setRequestUrl("/api/v1/users");
event.setRequestMethod("POST");
event.setOperationResult("SUCCESS");
event.setOperationTime(operationTime);
return event;
}
}

View File

@ -0,0 +1,140 @@
package com.pms.audit.controller;
import com.pms.audit.service.impl.AuditLogServiceImpl;
import com.pms.audit.service.impl.LoginLogServiceImpl;
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.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
/**
* PreAuthorize 覆盖率守卫测试pms-audit 模块KTD6 反射式静态检查
* <p>
* 设计依据docs/plans/2026-07-05-002-refactor-pre-authorize-alignment-pr2-pr4-plan.md KTD6
* <p>
* 守卫范围
* <ul>
* <li>Controller 所有 @RestController/@Controller 公共方法必须有 @PreAuthorize白名单InternalController 类名</li>
* <li>Service 层写操作@Service 类的 @Transactional 公共方法必须有 @PreAuthorize@Transactional 作为写操作指示器</li>
* <li>func_code 合法性hasFunction('xxx') xxx 必须是 V12 已注册的合法值</li>
* </ul>
* <p>
* ponytail: KTD6 表达式形式盲区 本测试只校验 @PreAuthorize 存在不校验表达式形式
* hasAuthorityAndAdmin vs hasAuthority hasAuthority 会导致 system-user admin 旁路失效
* 当前由 Service Security Test 抽样验证关键 ServiceImpl admin 旁路pms-audit 例外 AuditConsumer
* 集成路径覆盖
*/
@DisplayName("PreAuthorize 覆盖率守卫测试 (pms-audit)")
class PreAuthorizeCoverageTest {
/** OQ6 决策:硬编码 InternalController 类名白名单Feign 内部接口) */
private static final Set<String> WHITELISTED_CONTROLLERS = Set.of(
"InternalLoginLogController",
"InternalController"
);
/** V12 已注册的合法 function_code 列表(与 V12 migration 同步) */
private static final Set<String> VALID_FUNC_CODES = Set.of(
"admin", "base", "charge", "operation",
"user_management", "org_management", "role_management", "system_management",
"viewer" // deprecated但仍合法
);
/** hasFunction('xxx') SpEL 表达式提取正则 */
private static final Pattern HAS_FUNCTION_PATTERN = Pattern.compile("hasFunction\\(\\s*'([^']+)'\\s*\\)");
/** pms-audit 模块所有 @RestController/@Controller 类 */
private static final List<Class<?>> CONTROLLERS = List.of(
AuditLogController.class,
LoginLogController.class,
InternalLoginLogController.class
);
/** pms-audit 模块所有 @Service 类(用于 Service 层写操作扫描) */
private static final List<Class<?>> SERVICES = List.of(
AuditLogServiceImpl.class,
LoginLogServiceImpl.class
);
static Stream<Arguments> publicMethods() {
return CONTROLLERS.stream()
.filter(c -> !WHITELISTED_CONTROLLERS.contains(c.getSimpleName()))
.flatMap(c -> Arrays.stream(c.getDeclaredMethods())
.filter(m -> Modifier.isPublic(m.getModifiers()))
.filter(m -> !m.isBridge() && !m.isSynthetic())
.map(m -> Arguments.of(c.getSimpleName(), m.getName(), m)));
}
static Stream<Arguments> serviceWriteMethods() {
return SERVICES.stream()
.flatMap(c -> Arrays.stream(c.getDeclaredMethods())
.filter(m -> Modifier.isPublic(m.getModifiers()))
.filter(m -> !m.isBridge() && !m.isSynthetic())
.filter(m -> m.isAnnotationPresent(Transactional.class))
.map(m -> Arguments.of(c.getSimpleName(), m.getName(), m)));
}
@ParameterizedTest(name = "Controller {0}#{1} 必须有 @PreAuthorize")
@MethodSource("publicMethods")
void controllerMethod_mustHavePreAuthorize(String className, String methodName, Method method) {
assertThat(method.isAnnotationPresent(PreAuthorize.class))
.as("%s#%s 缺少 @PreAuthorize 注解(白名单类已过滤)", className, methodName)
.isTrue();
}
@ParameterizedTest(name = "Controller {0}#{1} 的 hasFunction func_code 必须合法")
@MethodSource("publicMethods")
void controllerMethod_funcCode_mustBeValid(String className, String methodName, Method method) {
PreAuthorize ann = method.getAnnotation(PreAuthorize.class);
if (ann == null) {
return; // controllerMethod_mustHavePreAuthorize 覆盖
}
String expr = ann.value();
Matcher matcher = HAS_FUNCTION_PATTERN.matcher(expr);
while (matcher.find()) {
String funcCode = matcher.group(1);
assertThat(VALID_FUNC_CODES)
.as("%s#%s 的 hasFunction('%s') 不是 V12 合法 func_code", className, methodName, funcCode)
.contains(funcCode);
}
}
@ParameterizedTest(name = "Service 写操作 {0}#{1} 必须有 @PreAuthorize@Transactional 指示写操作)")
@MethodSource("serviceWriteMethods")
void serviceWriteMethod_mustHavePreAuthorize(String className, String methodName, Method method) {
assertThat(method.isAnnotationPresent(PreAuthorize.class))
.as("%s#%s 是 @Transactional 写操作但缺少 @PreAuthorizesystem-user admin 旁路需要 @PreAuthorize 才能触发)",
className, methodName)
.isTrue();
}
@Test
@DisplayName("白名单机制自检InternalController 类被正确豁免")
void whitelist_selfCheck() {
// InternalLoginLogController 在白名单中应被过滤
long internalCount = CONTROLLERS.stream()
.filter(c -> WHITELISTED_CONTROLLERS.contains(c.getSimpleName()))
.count();
assertThat(internalCount).as("InternalController 应在白名单中").isEqualTo(1);
}
@Test
@DisplayName("V12 func_code 列表自检:包含 system_managementpms-audit 模块使用)")
void validFuncCodes_selfCheck() {
assertThat(VALID_FUNC_CODES).contains("system_management");
}
}

View File

@ -424,5 +424,44 @@ class AuditLogServiceImplTest {
assertThat(stats.getTotalCount()).isEqualTo(500L);
assertThat(stats.getSuccessCount()).isEqualTo(400L);
}
@Test
@DisplayName("functions='admin' → 不按 projectId 过滤admin 旁路OQ1 决策)")
void statistics_withAdminFunction_noProjectFilter() {
// 重新设置 SecurityContextprincipal CurrentUserfunctions='admin'
// 不持有 system:audit:view:all 权限 验证 canViewAllProjects() admin 旁路分支
UserContext.CurrentUser adminUser = new UserContext.CurrentUser(
1001L, "admin", 2001L, "ROLE_ADMIN", "1",
null, 200L, null, "admin");
UserContext.set(adminUser);
Authentication auth = new UsernamePasswordAuthenticationToken(
adminUser, null, List.of(new SimpleGrantedAuthority("ROLE_ADMIN")));
SecurityContextHolder.getContext().setAuthentication(auth);
// 使用 ArgumentCaptor 捕获 wrapper验证未应用 projectId 过滤admin 旁路放行
ArgumentCaptor<LambdaQueryWrapper<AuditLog>> wrapperCaptor =
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
when(auditLogMapper.selectCount(any(LambdaQueryWrapper.class)))
.thenReturn(500L)
.thenReturn(400L)
.thenReturn(80L)
.thenReturn(20L);
when(auditLogMapper.selectMaps(any())).thenReturn(List.of());
AuditLogStatisticsDTO stats = auditLogService.statistics();
assertThat(stats.getTotalCount()).isEqualTo(500L);
assertThat(stats.getSuccessCount()).isEqualTo(400L);
// 验证 selectCount 被调用 4 wrapper SQL 段不包含 project_id 过滤
verify(auditLogMapper, times(4)).selectCount(wrapperCaptor.capture());
// ponytail: LambdaQueryWrapper.getSqlSegment() 在无条件下返回空字符串
// admin 旁路生效时 wrapper 不带 projectId 条件sqlSegment 应为空
for (LambdaQueryWrapper<AuditLog> wrapper : wrapperCaptor.getAllValues()) {
assertThat(wrapper.getSqlSegment())
.as("admin 旁路生效时 wrapper 不应包含 project_id 过滤")
.doesNotContain("project_id");
}
}
}
}

View File

@ -17,6 +17,13 @@ import org.springframework.web.bind.annotation.*;
/**
* 认证控制器
* 路径前缀: /api/v1/auth
* <p>
* PR2-PR4 改造KTD5 决策14 个方法按接口类型分别标注 @PreAuthorize
* <ul>
* <li>8 个公开接口登录/刷新/验证码permitAll()</li>
* <li>5 个当前用户接口登出/切换项目/修改密码/用户信息/权限检查isAuthenticated()</li>
* <li>1 个管理接口重置密码hasFunction('user_management') and hasAuthorityAndAdmin('system:user:manage')</li>
* </ul>
*/
@Slf4j
@RestController
@ -30,6 +37,7 @@ public class AuthController {
* 用户登录
*/
@PostMapping("/login")
@PreAuthorize("permitAll()")
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request,
HttpServletRequest httpRequest) {
String clientIp = getClientIp(httpRequest);
@ -43,6 +51,7 @@ public class AuthController {
* 4 个平台特定端点/login/wechat 功能等价供小程序统一调用
*/
@PostMapping("/login/platform")
@PreAuthorize("permitAll()")
public Result<PlatformLoginResponse> loginByPlatform(@Valid @RequestBody PlatformLoginRequest request) {
return Result.success(authService.loginByPlatform(request));
}
@ -52,6 +61,7 @@ public class AuthController {
* 客户端传 wx.login code + wx.getPhoneNumber phoneCode
*/
@PostMapping("/login/wechat")
@PreAuthorize("permitAll()")
public Result<PlatformLoginResponse> loginByWechat(@Valid @RequestBody PlatformLoginRequest request) {
request.setPlatform(UserThirdAccount.PLATFORM_WECHAT);
return Result.success(authService.loginByPlatform(request));
@ -62,6 +72,7 @@ public class AuthController {
* 客户端传 my.getAuthCode code
*/
@PostMapping("/login/alipay")
@PreAuthorize("permitAll()")
public Result<PlatformLoginResponse> loginByAlipay(@Valid @RequestBody PlatformLoginRequest request) {
request.setPlatform(UserThirdAccount.PLATFORM_ALIPAY);
return Result.success(authService.loginByPlatform(request));
@ -72,6 +83,7 @@ public class AuthController {
* 客户端传 dd.getLoginCode loginCode
*/
@PostMapping("/login/dingtalk")
@PreAuthorize("permitAll()")
public Result<PlatformLoginResponse> loginByDingtalk(@Valid @RequestBody PlatformLoginRequest request) {
request.setPlatform(UserThirdAccount.PLATFORM_DINGTALK);
return Result.success(authService.loginByPlatform(request));
@ -82,6 +94,7 @@ public class AuthController {
* 客户端传 wx.qy.login code
*/
@PostMapping("/login/qywechat")
@PreAuthorize("permitAll()")
public Result<PlatformLoginResponse> loginByQywechat(@Valid @RequestBody PlatformLoginRequest request) {
request.setPlatform(UserThirdAccount.PLATFORM_QYWECHAT);
return Result.success(authService.loginByPlatform(request));
@ -91,6 +104,7 @@ public class AuthController {
* 退出登录
*/
@PostMapping("/logout")
@PreAuthorize("isAuthenticated()")
public Result<Void> logout(@RequestHeader(value = "Authorization", required = false) String authHeader,
@RequestBody(required = false) RefreshTokenRequest request) {
String accessToken = null;
@ -106,6 +120,7 @@ public class AuthController {
* 刷新令牌
*/
@PostMapping("/refresh")
@PreAuthorize("permitAll()")
public Result<TokenRefreshResponse> refresh(@Valid @RequestBody RefreshTokenRequest request) {
return Result.success(authService.refreshToken(request.getRefreshToken()));
}
@ -119,6 +134,7 @@ public class AuthController {
* @param projectId 目标项目ID
*/
@PostMapping("/switch-project")
@PreAuthorize("isAuthenticated()")
public Result<LoginResponse> switchProject(@RequestParam Long projectId) {
return Result.success(authService.switchProject(projectId));
}
@ -127,6 +143,7 @@ public class AuthController {
* 获取验证码
*/
@GetMapping("/captcha")
@PreAuthorize("permitAll()")
public Result<CaptchaResponse> captcha() {
return Result.success(authService.getCaptcha());
}
@ -135,6 +152,7 @@ public class AuthController {
* 修改密码
*/
@PutMapping("/password")
@PreAuthorize("isAuthenticated()")
public Result<Void> changePassword(@Valid @RequestBody ChangePasswordRequest request) {
authService.changePassword(UserContext.getUserId(), request);
return Result.success();
@ -144,7 +162,7 @@ public class AuthController {
* 重置密码管理员
*/
@PutMapping("/password/reset")
@PreAuthorize("hasAuthority('system:user:manage')")
@PreAuthorize("hasFunction('user_management') and hasAuthorityAndAdmin('system:user:manage')")
@AuditLog(module = "认证", type = "UPDATE", description = "重置用户密码", recordParams = false)
public Result<Void> resetPassword(@Valid @RequestBody ResetPasswordRequest request) {
authService.resetPassword(request);
@ -155,6 +173,7 @@ public class AuthController {
* 获取当前用户信息
*/
@GetMapping("/userinfo")
@PreAuthorize("isAuthenticated()")
public Result<UserInfoDTO> userInfo() {
return Result.success(authService.getCurrentUserInfo());
}
@ -163,6 +182,7 @@ public class AuthController {
* 权限检查
*/
@PostMapping("/check-permission")
@PreAuthorize("isAuthenticated()")
public Result<Boolean> checkPermission(@RequestParam Long userId,
@RequestParam String permissionCode,
@RequestParam(required = false) Long projectId) {

View File

@ -19,6 +19,9 @@ import java.util.Map;
/**
* 组织管理控制器
* 路径前缀: /api/v1/auth/orgs
* <p>
* PR2-PR4 改造U36 方法统一标注 `hasFunction('org_management')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('system:org:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -32,7 +35,7 @@ public class OrgController {
* 组织架构树支持 parentId/status/projectId/orgType 过滤
*/
@GetMapping
@PreAuthorize("hasAuthority('system:org:manage')")
@PreAuthorize("hasFunction('org_management')")
public Result<List<OrgDTO>> list(@RequestParam(required = false) Long parentId,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) Long projectId,
@ -44,7 +47,7 @@ public class OrgController {
* 完整组织树含岗位节点带类型中文名
*/
@GetMapping("/tree")
@PreAuthorize("hasAuthority('system:org:manage')")
@PreAuthorize("hasFunction('org_management')")
public Result<List<OrgTreeDTO>> tree() {
return Result.success(orgService.getOrgTree());
}
@ -53,7 +56,7 @@ public class OrgController {
* 指定父节点下的组织子树
*/
@GetMapping("/tree/{parentId}")
@PreAuthorize("hasAuthority('system:org:manage')")
@PreAuthorize("hasFunction('org_management')")
public Result<List<OrgTreeDTO>> treeByParent(@PathVariable Long parentId) {
return Result.success(orgService.getOrgTree(parentId));
}
@ -62,7 +65,7 @@ public class OrgController {
* 创建组织节点
*/
@PostMapping
@PreAuthorize("hasAuthority('system:org:manage')")
@PreAuthorize("hasFunction('org_management')")
@AuditLog(module = "组织架构", type = "CREATE", description = "创建组织")
public Result<Map<String, Long>> create(@Valid @RequestBody OrgSaveRequest request) {
Long id = orgService.create(request);
@ -75,7 +78,7 @@ public class OrgController {
* 更新组织节点
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('system:org:manage')")
@PreAuthorize("hasFunction('org_management')")
@AuditLog(module = "组织架构", type = "UPDATE", description = "更新组织")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody OrgSaveRequest request) {
orgService.update(id, request);
@ -86,7 +89,7 @@ public class OrgController {
* 删除组织节点
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('system:org:manage')")
@PreAuthorize("hasFunction('org_management')")
@AuditLog(module = "组织架构", type = "DELETE", description = "删除组织")
public Result<Void> delete(@PathVariable Long id) {
orgService.delete(id);

View File

@ -5,11 +5,15 @@ import com.pms.auth.service.PermissionService;
import com.pms.common.response.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
/**
* 权限管理控制器
* 路径前缀: /api/v1/auth/permissions
* <p>
* PR2-PR4 改造U32 方法补 `hasFunction('role_management')`原裸奔权限树供角色分配权限使用
* RoleController 共享 role_management 函数入口保持权限语义一致
*/
@Slf4j
@RestController
@ -23,6 +27,7 @@ public class PermissionController {
* 权限树查询
*/
@GetMapping
@PreAuthorize("hasFunction('role_management')")
public Result<PermissionTreeResult> tree(@RequestParam(required = false) Long roleId,
@RequestParam(required = false) Integer permType) {
return Result.success(permissionService.getPermissionTree(roleId, permType));
@ -32,6 +37,7 @@ public class PermissionController {
* 权限树查询/tree 别名
*/
@GetMapping("/tree")
@PreAuthorize("hasFunction('role_management')")
public Result<PermissionTreeResult> treeAlias(@RequestParam(required = false) Long roleId,
@RequestParam(required = false) Integer permType) {
return Result.success(permissionService.getPermissionTree(roleId, permType));

View File

@ -18,6 +18,10 @@ import java.util.List;
/**
* 岗位-角色映射管理控制器U5 / KD5岗位 = 角色映射
* 路径前缀: /api/v1/auth/positions
* <p>
* PR2-PR4 改造U35 方法统一标注 `hasFunction('system_management')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('system:project:manage'|'system:role:manage')` 控制操作粒度
* mapPositionToRole system:role:manage 保持 ROLE_ROLE_MANAGER 权限语义
*/
@Slf4j
@RestController
@ -31,7 +35,7 @@ public class PositionRoleController {
* 为用户在项目中分配岗位
*/
@PostMapping("/assign")
@PreAuthorize("hasAuthority('system:project:manage')")
@PreAuthorize("hasFunction('system_management')")
@AuditLog(module = "岗位管理", type = "ASSIGN", description = "分配岗位")
public Result<Void> assignPosition(@RequestBody @Valid AssignPositionRequest request) {
positionRoleService.assignPositionToUser(
@ -43,7 +47,7 @@ public class PositionRoleController {
* 查询用户在项目的岗位
*/
@GetMapping("/user/{userId}/project/{projectId}")
@PreAuthorize("hasAuthority('system:project:manage')")
@PreAuthorize("hasFunction('system_management')")
public Result<List<PositionDTO>> getUserPositions(@PathVariable Long userId,
@PathVariable Long projectId) {
return Result.success(positionRoleService.getUserPositions(userId, projectId));
@ -53,7 +57,7 @@ public class PositionRoleController {
* 查询组织下的岗位列表
*/
@GetMapping("/org/{parentOrgId}")
@PreAuthorize("hasAuthority('system:project:manage')")
@PreAuthorize("hasFunction('system_management')")
public Result<List<PositionDTO>> getPositionsByOrg(@PathVariable Long parentOrgId) {
return Result.success(positionRoleService.getPositionsByOrg(parentOrgId));
}
@ -62,7 +66,7 @@ public class PositionRoleController {
* 配置岗位-角色映射管理员
*/
@PostMapping("/mapping")
@PreAuthorize("hasAuthority('system:role:manage')")
@PreAuthorize("hasFunction('system_management')")
@AuditLog(module = "岗位管理", type = "CREATE", description = "创建岗位-角色映射")
public Result<Void> mapPositionToRole(@RequestBody @Valid PositionRoleMappingRequest request) {
positionRoleService.mapPositionToRole(request.getPositionOrgId(), request.getRoleId());
@ -73,7 +77,7 @@ public class PositionRoleController {
* 列出所有岗位-角色映射
*/
@GetMapping("/mappings")
@PreAuthorize("hasAuthority('system:role:manage')")
@PreAuthorize("hasFunction('system_management')")
public Result<List<PositionRoleMappingDTO>> listMappings() {
return Result.success(positionRoleService.listPositionRoleMappings());
}

View File

@ -18,6 +18,9 @@ import java.util.List;
* 路径前缀: /api/v1/auth/projects
* <p>
* 管理项目-用户绑定关系含岗位选择复用 U5 PositionRoleService
* <p>
* PR2-PR4 改造U33 方法统一标注 `hasFunction('system_management')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('system:project:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -31,7 +34,7 @@ public class ProjectUserController {
* 查询项目的用户列表含岗位/角色信息
*/
@GetMapping("/{projectId}/users")
@PreAuthorize("hasAuthority('system:project:manage')")
@PreAuthorize("hasFunction('system_management')")
public Result<List<ProjectUserDTO>> getProjectUsers(@PathVariable Long projectId) {
return Result.success(projectUserService.getProjectUsers(projectId));
}
@ -40,7 +43,7 @@ public class ProjectUserController {
* 绑定用户到项目含岗位选择
*/
@PostMapping("/{projectId}/users")
@PreAuthorize("hasAuthority('system:project:manage')")
@PreAuthorize("hasFunction('system_management')")
@AuditLog(module = "权限管理", type = "ASSIGN", description = "绑定用户到项目")
public Result<Void> bindUserToProject(@PathVariable Long projectId,
@RequestBody @Valid BindUserRequest request) {
@ -52,7 +55,7 @@ public class ProjectUserController {
* 解绑项目用户
*/
@DeleteMapping("/{projectId}/users/{userId}")
@PreAuthorize("hasAuthority('system:project:manage')")
@PreAuthorize("hasFunction('system_management')")
@AuditLog(module = "权限管理", type = "DELETE", description = "解绑项目用户")
public Result<Void> unbindUser(@PathVariable Long projectId, @PathVariable Long userId) {
projectUserService.unbindUser(projectId, userId);

View File

@ -18,6 +18,9 @@ import java.util.Map;
/**
* 角色管理控制器
* 路径前缀: /api/v1/auth/roles
* <p>
* PR2-PR4 改造U36 方法统一标注 `hasFunction('role_management')` 控制模块入口 view 只读方法
* Service 层写操作通过 `hasAuthorityAndAdmin('system:role:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -31,7 +34,7 @@ public class RoleController {
* 角色列表
*/
@GetMapping
@PreAuthorize("hasAuthority('system:role:view')")
@PreAuthorize("hasFunction('role_management')")
public Result<List<RoleDTO>> list(@RequestParam(required = false) String keyword,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) Long projectId,
@ -43,7 +46,7 @@ public class RoleController {
* 创建角色
*/
@PostMapping
@PreAuthorize("hasAuthority('system:role:manage')")
@PreAuthorize("hasFunction('role_management')")
@AuditLog(module = "权限管理", type = "CREATE", description = "创建角色")
public Result<Map<String, Long>> create(@Valid @RequestBody RoleSaveRequest request) {
Long id = roleService.create(request);
@ -56,7 +59,7 @@ public class RoleController {
* 角色详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('system:role:view')")
@PreAuthorize("hasFunction('role_management')")
public Result<RoleDTO> getById(@PathVariable Long id) {
return Result.success(roleService.getById(id));
}
@ -65,7 +68,7 @@ public class RoleController {
* 更新角色
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('system:role:manage')")
@PreAuthorize("hasFunction('role_management')")
@AuditLog(module = "权限管理", type = "UPDATE", description = "更新角色")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody RoleSaveRequest request) {
roleService.update(id, request);
@ -76,7 +79,7 @@ public class RoleController {
* 删除角色
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('system:role:manage')")
@PreAuthorize("hasFunction('role_management')")
@AuditLog(module = "权限管理", type = "DELETE", description = "删除角色")
public Result<Void> delete(@PathVariable Long id) {
roleService.delete(id);
@ -87,7 +90,7 @@ public class RoleController {
* 分配权限
*/
@PostMapping("/{id}/permissions")
@PreAuthorize("hasAuthority('system:role:manage')")
@PreAuthorize("hasFunction('role_management')")
@AuditLog(module = "权限管理", type = "UPDATE", description = "分配角色权限")
public Result<Void> assignPermissions(@PathVariable Long id,
@Valid @RequestBody AssignPermissionsRequest request) {

View File

@ -19,6 +19,8 @@ import java.util.Map;
* 用户管理控制器
* 路径前缀: /api/v1/auth/users
* <p>
* PR2-PR4 改造U39 方法统一标注 `hasFunction('user_management')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('system:user:manage'|'system:role:manage')` 控制操作粒度
* 含用户-角色分配接口assignRoles / getUserRoles / removeRole
* 复用 pms-common @AuditLog 注解 + @PreAuthorize 方法级鉴权
*/
@ -34,7 +36,7 @@ public class UserController {
* 用户列表查询分页
*/
@GetMapping
@PreAuthorize("hasAuthority('system:user:manage')")
@PreAuthorize("hasFunction('user_management')")
public Result<PageResult<UserDTO>> list(UserQueryRequest request) {
return Result.success(userService.page(request));
}
@ -43,7 +45,7 @@ public class UserController {
* 创建用户
*/
@PostMapping
@PreAuthorize("hasAuthority('system:user:manage')")
@PreAuthorize("hasFunction('user_management')")
@AuditLog(module = "权限管理", type = "CREATE", description = "创建用户")
public Result<Map<String, Long>> create(@Valid @RequestBody UserSaveRequest request) {
Long id = userService.create(request);
@ -56,7 +58,7 @@ public class UserController {
* 用户详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('system:user:manage')")
@PreAuthorize("hasFunction('user_management')")
public Result<UserDTO> getById(@PathVariable Long id) {
return Result.success(userService.getById(id));
}
@ -65,7 +67,7 @@ public class UserController {
* 更新用户
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('system:user:manage')")
@PreAuthorize("hasFunction('user_management')")
@AuditLog(module = "权限管理", type = "UPDATE", description = "更新用户")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody UserUpdateRequest request) {
userService.update(id, request);
@ -76,7 +78,7 @@ public class UserController {
* 删除用户
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('system:user:manage')")
@PreAuthorize("hasFunction('user_management')")
@AuditLog(module = "权限管理", type = "DELETE", description = "删除用户")
public Result<Void> delete(@PathVariable Long id) {
userService.delete(id);
@ -87,7 +89,7 @@ public class UserController {
* 更新用户状态
*/
@PutMapping("/{id}/status")
@PreAuthorize("hasAuthority('system:user:manage')")
@PreAuthorize("hasFunction('user_management')")
@AuditLog(module = "权限管理", type = "UPDATE", description = "更新用户状态")
public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
userService.updateStatus(id, status);
@ -98,7 +100,7 @@ public class UserController {
* 为用户分配角色覆盖式先清空旧角色再分配新角色
*/
@PostMapping("/{id}/roles")
@PreAuthorize("hasAuthority('system:role:manage')")
@PreAuthorize("hasFunction('user_management')")
@AuditLog(module = "权限管理", type = "ASSIGN", description = "分配用户角色")
public Result<Void> assignRoles(@PathVariable Long id, @Valid @RequestBody AssignRolesRequest request) {
userService.assignRoles(id, request);
@ -109,7 +111,7 @@ public class UserController {
* 查询用户的角色列表
*/
@GetMapping("/{id}/roles")
@PreAuthorize("hasAuthority('system:role:manage')")
@PreAuthorize("hasFunction('user_management')")
public Result<List<RoleDTO>> getUserRoles(@PathVariable Long id) {
return Result.success(userService.getUserRoles(id));
}
@ -118,7 +120,7 @@ public class UserController {
* 删除用户的指定角色
*/
@DeleteMapping("/{id}/roles/{roleId}")
@PreAuthorize("hasAuthority('system:role:manage')")
@PreAuthorize("hasFunction('user_management')")
@AuditLog(module = "权限管理", type = "DELETE", description = "删除用户角色")
public Result<Void> removeRole(@PathVariable Long id, @PathVariable Long roleId) {
userService.removeRole(id, roleId);

View File

@ -21,6 +21,7 @@ import com.pms.common.exception.BusinessException;
import com.pms.common.exception.ErrorCode;
import com.pms.common.datascope.DataScopeHelper;
import com.pms.common.security.JwtUtils;
import com.pms.common.security.SystemUserSecurityContext;
import com.pms.common.security.UserContext;
import com.pms.common.util.SensitiveDataUtils;
import io.jsonwebtoken.Claims;
@ -81,6 +82,15 @@ public class AuthServiceImpl implements AuthService {
@Transactional(rollbackFor = Exception.class)
public LoginResponse login(LoginRequest request, String clientIp) {
try {
// 0. system 占位用户拒绝登录R4 缓解策略 (7)defense-in-depth防止攻击者通过 /api/v1/auth/login system 身份认证
// system 用户仅供 MQ 消费者 SystemUserSecurityContext 使用无认证路径
// 信任边界校验DB collate=utf8mb4_unicode_ci 大小写不敏感必须用 equalsIgnoreCase + trim
// 防止 "System"/"SYSTEM"/" system " + 公开占位密码绕过用户名拒绝
String requestUsername = request.getUsername() == null ? null : request.getUsername().trim();
if (requestUsername != null && requestUsername.equalsIgnoreCase(SystemUserSecurityContext.SYSTEM_USERNAME)) {
throw new BusinessException(ErrorCode.ACCOUNT_DISABLED);
}
// 1. 验证码校验
validateCaptcha(request.getCaptchaKey(), request.getCaptchaCode());

View File

@ -15,6 +15,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@ -68,6 +69,7 @@ public class OrgServiceImpl implements OrgService, OrgIdResolver {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:org:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(OrgSaveRequest request) {
// 检查组织编码是否已存在
@ -108,6 +110,7 @@ public class OrgServiceImpl implements OrgService, OrgIdResolver {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:org:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, OrgSaveRequest request) {
Org org = getOrgEntity(id);
@ -148,6 +151,7 @@ public class OrgServiceImpl implements OrgService, OrgIdResolver {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:org:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
Org org = getOrgEntity(id);

View File

@ -20,6 +20,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -42,6 +43,7 @@ public class PositionRoleServiceImpl implements PositionRoleService {
private final ProjectUserMapper projectUserMapper;
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void assignPositionToUser(Long userId, Long projectId, Long positionOrgId) {
// 1. 校验岗位节点org_type=5
@ -122,6 +124,7 @@ public class PositionRoleServiceImpl implements PositionRoleService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:role:manage')")
@Transactional(rollbackFor = Exception.class)
public void mapPositionToRole(Long positionOrgId, Long roleId) {
// 校验岗位

View File

@ -16,6 +16,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -40,6 +41,7 @@ public class ProjectUserServiceImpl implements ProjectUserService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void bindUserToProject(Long projectId, BindUserRequest request) {
Long userId = request.getUserId();
@ -84,6 +86,7 @@ public class ProjectUserServiceImpl implements ProjectUserService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void unbindUser(Long projectId, Long userId) {
int deleted = projectUserMapper.deleteByProjectAndUser(projectId, userId);

View File

@ -15,6 +15,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@ -85,6 +86,7 @@ public class RoleServiceImpl implements RoleService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:role:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(RoleSaveRequest request) {
// 检查角色编码是否已存在
@ -121,6 +123,7 @@ public class RoleServiceImpl implements RoleService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:role:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, RoleSaveRequest request) {
Role role = getRoleEntity(id);
@ -148,6 +151,7 @@ public class RoleServiceImpl implements RoleService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:role:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
Role role = getRoleEntity(id);
@ -168,6 +172,7 @@ public class RoleServiceImpl implements RoleService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:role:manage')")
@Transactional(rollbackFor = Exception.class)
public void assignPermissions(Long roleId, AssignPermissionsRequest request) {
// 检查角色存在

View File

@ -13,9 +13,11 @@ import com.pms.common.constant.CommonConstants;
import com.pms.common.exception.BusinessException;
import com.pms.common.exception.ErrorCode;
import com.pms.common.response.PageResult;
import com.pms.common.security.SystemUserSecurityContext;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -77,6 +79,7 @@ public class UserServiceImpl implements UserService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:user:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(UserSaveRequest request) {
// 检查用户名是否已存在
@ -125,6 +128,7 @@ public class UserServiceImpl implements UserService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:user:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, UserUpdateRequest request) {
User user = getUserEntity(id);
@ -153,8 +157,14 @@ public class UserServiceImpl implements UserService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:user:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
// OQ5 决策系统占位账户不可删除R4 system-user 方案配套防止误删 V13 id=0L system 用户
if (id != null && id.equals(SystemUserSecurityContext.SYSTEM_USER_ID)) {
throw new BusinessException(ErrorCode.OPERATION_FAILED, "系统账户不可删除");
}
User user = getUserEntity(id);
// 系统管理员账号不可删除
@ -175,6 +185,7 @@ public class UserServiceImpl implements UserService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:user:manage')")
public void updateStatus(Long id, Integer status) {
User user = getUserEntity(id);
@ -188,6 +199,7 @@ public class UserServiceImpl implements UserService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:role:manage')")
@Transactional(rollbackFor = Exception.class)
public void assignRoles(Long userId, AssignRolesRequest request) {
// 检查用户存在
@ -222,6 +234,7 @@ public class UserServiceImpl implements UserService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:role:manage')")
@Transactional(rollbackFor = Exception.class)
public void removeRole(Long userId, Long roleId) {
// 检查用户存在

View File

@ -0,0 +1,50 @@
-- ============================================================
-- V13: 新增 system 占位用户OQ5 决策方案 aR4 system-user 方案配套)
-- ============================================================
-- 背景:
-- R4 MQ 消费者路径调用 Service 写操作,@PreAuthorize admin 旁路依赖
-- principal.getFunctions() 含 "admin"(见 CustomMethodSecurityExpressionRoot.parseFunctionsToSet
-- SystemUserSecurityContext.system() 构造 principal=CurrentUser(userId=0L, functions="admin")
-- userId=0L 必须在 t_user 表中存在(避免 setCreatedBy(null) 破坏 NOT NULL 约束)。
--
-- 关键约束:
-- 1. password 仅满足 NOT NULL 约束,无任何认证路径可使用
-- 2. AuthServiceImpl.login 增加 system 用户名拒绝defense-in-depth见 R4 缓解策略 (7)
-- 3. UserServiceImpl.delete 增加 id=0L 防护OQ5 实施清单U3 阶段实施)
--
-- 安全说明:
-- - 不可通过 /api/v1/auth/login 以 system 身份认证AuthServiceImpl.login 在密码校验前拒绝 system 用户名)
-- - 不可通过 DELETE /api/v1/users/0 删除UserServiceImpl 防护)
-- - 仅用于 MQ 消费者路径的 admin 旁路,无业务用途
-- ============================================================
INSERT INTO `t_user` (`id`, `project_id`, `username`, `password`, `real_name`,
`status`, `account_type`, `remark`,
`created_at`, `updated_at`, `deleted`)
VALUES (
0,
NULL,
'system',
-- 占位密码BCrypt of "password"),仅满足 NOT NULL 约束;
-- 安全依赖 AuthServiceImpl.login 按用户名拒绝password 本身不可猜测性无意义
'$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy',
'系统占位用户',
1,
1,
'系统占位用户,禁止登录。仅供 MQ 消费者 system-user SecurityContext 使用OQ5/R4',
UNIX_TIMESTAMP() * 1000,
UNIX_TIMESTAMP() * 1000,
0
)
ON DUPLICATE KEY UPDATE `updated_at` = UNIX_TIMESTAMP() * 1000;
-- ============================================================
-- post-deploy 验证 SQL执行后人工运行确认
-- ============================================================
-- 验证 system 用户存在且不可登录:
-- SELECT id, username, real_name, status, account_type, deleted FROM t_user WHERE id = 0;
-- 预期id=0, username='system', status=1, account_type=1, deleted=0
--
-- 验证 system 用户名登录被拒绝(应在 AuthServiceImpl.login 代码层拒绝,不依赖密码强度):
-- POST /api/v1/auth/login {"username":"system","password":"anything"} → 应返回 ACCOUNT_DISABLED 错误
-- ============================================================

View File

@ -257,14 +257,16 @@ class OrgControllerCrudTest {
class SecurityAnnotationTest {
/**
* 验证写操作方法标注了 @PreAuthorize("hasAuthority('system:org:manage')") @AuditLog
* 验证写操作方法标注了 @PreAuthorize("hasFunction('org_management')") @AuditLog
* standalone MockMvc 不加载 Spring Security 过滤链无法测 403 响应
* 此处通过反射断言注解存在实际 403 拦截由集成测试验证
* PR2-PR4 改造U3Controller 层从 hasAuthority('system:org:manage') 改为 hasFunction('org_management')
* Service 层写操作通过 hasAuthorityAndAdmin('system:org:manage') 控制操作粒度
*/
@Test
@DisplayName("create/update/delete 方法标注 @PreAuthorize('system:org:manage')")
@DisplayName("create/update/delete 方法标注 @PreAuthorize('hasFunction(org_management)')")
void writeMethods_havePreAuthorize() throws NoSuchMethodException {
String expected = "hasAuthority('system:org:manage')";
String expected = "hasFunction('org_management')";
Method create = OrgController.class.getMethod("create", OrgSaveRequest.class);
PreAuthorize createAnno = create.getAnnotation(PreAuthorize.class);
@ -303,9 +305,9 @@ class OrgControllerCrudTest {
}
@Test
@DisplayName("list/tree/treeByParent 方法标注 @PreAuthorize('system:org:manage')(读操作也鉴权)")
@DisplayName("list/tree/treeByParent 方法标注 @PreAuthorize('hasFunction(org_management)')(读操作也鉴权)")
void readMethods_havePreAuthorize() throws NoSuchMethodException {
String expected = "hasAuthority('system:org:manage')";
String expected = "hasFunction('org_management')";
Method list = OrgController.class.getMethod("list", Long.class, Integer.class, Long.class, Integer.class);
PreAuthorize listAnno = list.getAnnotation(PreAuthorize.class);

View File

@ -0,0 +1,252 @@
package com.pms.auth.controller;
import com.pms.auth.service.impl.AuthServiceImpl;
import com.pms.auth.service.impl.OrgServiceImpl;
import com.pms.auth.service.impl.PermissionServiceImpl;
import com.pms.auth.service.impl.PositionRoleServiceImpl;
import com.pms.auth.service.impl.ProjectUserServiceImpl;
import com.pms.auth.service.impl.RoleServiceImpl;
import com.pms.auth.service.impl.UserServiceImpl;
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.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
/**
* PreAuthorize 覆盖率守卫测试pms-auth 模块KTD6 反射式静态检查
* <p>
* 设计依据docs/plans/2026-07-05-002-refactor-pre-authorize-alignment-pr2-pr4-plan.md KTD6 / U3
* <p>
* 守卫范围
* <ul>
* <li>Controller 所有 @RestController/@Controller 公共方法必须有 @PreAuthorize白名单InternalController 类名</li>
* <li>Service 层写操作@Service 类的 @Transactional 公共方法必须有 @PreAuthorize@Transactional 作为写操作指示器</li>
* <li>func_code 合法性hasFunction('xxx') xxx 必须是 V12 已注册的合法值</li>
* </ul>
* <p>
* ponytail: KTD6 表达式形式盲区 本测试只校验 @PreAuthorize 存在不校验表达式形式
* hasAuthorityAndAdmin vs hasAuthority hasAuthority 会导致 system-user admin 旁路失效
* 当前由 UserServiceSecurityTest 抽样验证关键 ServiceImpl admin 旁路
* 升级路径扩展反射检查解析 SpEL 表达式 method 但引入 Spring Expression Parser 复杂度较高性价比不足
*/
@DisplayName("PreAuthorize 覆盖率守卫测试 (pms-auth)")
class PreAuthorizeCoverageTest {
/** OQ6 决策:硬编码 InternalController 类名白名单Feign 内部接口,依赖 InternalApiInterceptor 保护 /api/internal/** 路径) */
private static final Set<String> WHITELISTED_CONTROLLERS = Set.of(
"InternalController"
);
/**
* AuthServiceImpl 方法白名单U2/U3 边界AuthServiceImpl U2 AuthController @PreAuthorize 控制
* permitAll/isAuthenticated/管理接口Service 层不加 @PreAuthorize 以保持 permitAll 语义
* permitAll 仅在 Controller 层生效Service @PreAuthorize 会拒绝未认证请求破坏公开接口
* <p>
* 白名单方法
* - login / loginByPlatform公开接口Controller permitAll()
* - changePassword / switchProject当前用户操作Controller isAuthenticated()
* - resetPassword管理员操作Controller hasFunction+hasAuthorityAndAdmin内部已有 UserContext 权限校验
*/
private static final Set<String> WHITELISTED_AUTH_SERVICE_METHODS = Set.of(
"login", "loginByPlatform",
"changePassword", "switchProject",
"resetPassword"
);
/** V12 已注册的合法 function_code 列表(与 V12 migration 同步) */
private static final Set<String> VALID_FUNC_CODES = Set.of(
"admin", "base", "charge", "operation",
"user_management", "org_management", "role_management", "system_management",
"viewer" // deprecated但仍合法
);
/** hasFunction('xxx') SpEL 表达式提取正则 */
private static final Pattern HAS_FUNCTION_PATTERN = Pattern.compile("hasFunction\\(\\s*'([^']+)'\\s*\\)");
/** pms-auth 模块所有 @RestController/@Controller 类 */
private static final List<Class<?>> CONTROLLERS = List.of(
AuthController.class,
UserController.class,
OrgController.class,
RoleController.class,
PermissionController.class,
ProjectUserController.class,
PositionRoleController.class,
InternalController.class
);
/** pms-auth 模块所有 @Service 类(用于 Service 层写操作扫描) */
private static final List<Class<?>> SERVICES = List.of(
AuthServiceImpl.class,
UserServiceImpl.class,
OrgServiceImpl.class,
RoleServiceImpl.class,
ProjectUserServiceImpl.class,
PositionRoleServiceImpl.class,
PermissionServiceImpl.class // 只读 @Transactional 写方法
);
static Stream<Arguments> publicMethods() {
return CONTROLLERS.stream()
.filter(c -> !WHITELISTED_CONTROLLERS.contains(c.getSimpleName()))
.flatMap(c -> Arrays.stream(c.getDeclaredMethods())
.filter(m -> Modifier.isPublic(m.getModifiers()))
.filter(m -> !m.isBridge() && !m.isSynthetic())
.map(m -> Arguments.of(c.getSimpleName(), m.getName(), m)));
}
static Stream<Arguments> serviceWriteMethods() {
return SERVICES.stream()
.flatMap(c -> Arrays.stream(c.getDeclaredMethods())
.filter(m -> Modifier.isPublic(m.getModifiers()))
.filter(m -> !m.isBridge() && !m.isSynthetic())
.filter(m -> m.isAnnotationPresent(Transactional.class))
// AuthServiceImpl 白名单方法跳过U2/U3 边界 WHITELISTED_AUTH_SERVICE_METHODS 注释
.filter(m -> !(c.getSimpleName().equals("AuthServiceImpl")
&& WHITELISTED_AUTH_SERVICE_METHODS.contains(m.getName())))
.map(m -> Arguments.of(c.getSimpleName(), m.getName(), m)));
}
@ParameterizedTest(name = "Controller {0}#{1} 必须有 @PreAuthorize")
@MethodSource("publicMethods")
void controllerMethod_mustHavePreAuthorize(String className, String methodName, Method method) {
assertThat(method.isAnnotationPresent(PreAuthorize.class))
.as("%s#%s 缺少 @PreAuthorize 注解(白名单类已过滤)", className, methodName)
.isTrue();
}
@ParameterizedTest(name = "Controller {0}#{1} 的 hasFunction func_code 必须合法")
@MethodSource("publicMethods")
void controllerMethod_funcCode_mustBeValid(String className, String methodName, Method method) {
PreAuthorize ann = method.getAnnotation(PreAuthorize.class);
if (ann == null) {
return; // controllerMethod_mustHavePreAuthorize 覆盖
}
String expr = ann.value();
Matcher matcher = HAS_FUNCTION_PATTERN.matcher(expr);
while (matcher.find()) {
String funcCode = matcher.group(1);
assertThat(VALID_FUNC_CODES)
.as("%s#%s 的 hasFunction('%s') 不是 V12 合法 func_code", className, methodName, funcCode)
.contains(funcCode);
}
}
@ParameterizedTest(name = "Service 写操作 {0}#{1} 必须有 @PreAuthorize@Transactional 指示写操作)")
@MethodSource("serviceWriteMethods")
void serviceWriteMethod_mustHavePreAuthorize(String className, String methodName, Method method) {
assertThat(method.isAnnotationPresent(PreAuthorize.class))
.as("%s#%s 是 @Transactional 写操作但缺少 @PreAuthorizesystem-user admin 旁路需要 @PreAuthorize 才能触发)",
className, methodName)
.isTrue();
}
@Test
@DisplayName("白名单机制自检InternalController 类被正确豁免")
void whitelist_selfCheck() {
// InternalController 在白名单中应被过滤
long internalCount = CONTROLLERS.stream()
.filter(c -> WHITELISTED_CONTROLLERS.contains(c.getSimpleName()))
.count();
assertThat(internalCount).as("InternalController 应在白名单中").isEqualTo(1);
}
@Test
@DisplayName("V12 func_code 列表自检:包含 pms-auth 使用的全部 func_code")
void validFuncCodes_selfCheck() {
// pms-auth 使用 user_management / org_management / role_management / system_management
assertThat(VALID_FUNC_CODES).contains(
"user_management", "org_management", "role_management", "system_management");
}
/**
* V12 cross-checkP2 #9解析 V12 migration SQL 提取实际定义的 function_code
* 验证 VALID_FUNC_CODES 列表与 V12 一致避免 V12 加新 func_code 后测试列表漂移
* <p>
* V12 定义 8 func_codeadmin/base/charge/operation/user_management/
* role_management/org_management/system_management'viewer' V1 遗留V12 UPDATE
* 但仍在 t_role Q10 决策保留VALID_FUNC_CODES 单独覆盖
* <p>
* ponytail: 不抽共享 fixture Gradle testFixtures 插件配置保留 3 模块列表重复
* pms-auth 在此提供 V12 单点交叉检查 V12 加新 func_code 时本测试强制失败
* 触发开发者同步更新 3 模块的 VALID_FUNC_CODESpms-base/pms-audit 各自的 selfCheck 也会校验自身使用的 func_code
*/
@Test
@DisplayName("V12 cross-check: VALID_FUNC_CODES 包含 V12 migration 实际定义的全部 function_code")
void validFuncCodes_matchesV12Migration() throws IOException {
String v12Sql = loadV12MigrationSql();
Set<String> v12DefinedFuncCodes = extractFuncCodesFromV12(v12Sql);
// V12 必须至少定义 8 func_code4 UPDATE + 4 INSERT
assertThat(v12DefinedFuncCodes)
.as("V12 应至少定义 8 个 func_code4 UPDATE + 4 INSERT")
.hasSizeGreaterThanOrEqualTo(8);
// VALID_FUNC_CODES 必须包含 V12 实际定义的全部 func_code
assertThat(VALID_FUNC_CODES)
.as("VALID_FUNC_CODES 必须包含 V12 migration 实际定义的全部 function_codeV12 加新 code 时本断言失败,触发同步更新)")
.containsAll(v12DefinedFuncCodes);
// VALID_FUNC_CODES 还必须包含 'viewer'V1 遗留V12 UPDATE 但仍合法
assertThat(VALID_FUNC_CODES)
.as("VALID_FUNC_CODES 必须包含 'viewer'V1 遗留V12 未 UPDATE 但 Q10 决策保留)")
.contains("viewer");
}
/** 加载 V12 migration SQL 文件pms-auth 模块 classpath 资源) */
private String loadV12MigrationSql() throws IOException {
try (InputStream is = getClass().getResourceAsStream(
"/db/migration/V12__realign_function_code_and_add_management_roles.sql")) {
assertThat(is)
.as("V12 migration 文件必须在 classpath: /db/migration/V12__realign_function_code_and_add_management_roles.sql")
.isNotNull();
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
}
}
/**
* V12 SQL 提取实际定义的 function_code 集合
* <ul>
* <li>UPDATE 模式`` `function_code` = 'xxx' ``反引号包裹列名</li>
* <li>INSERT 模式INSERT INTO t_role VALUES (id, 'ROLE_CODE', '名称', 'func_code', 1, UNIX_TIMESTAMP()...)
* 匹配 `'func_code',\s*1,\s*UNIX_TIMESTAMP`function_code 后跟 data_scope=1 与时间戳</li>
* </ul>
* V12 INSERT 是单条多 VALUES 元组 2 个正则用全局 find() 捕获全部 4 个元组
*/
private Set<String> extractFuncCodesFromV12(String sql) {
// UPDATE 模式`function_code` = 'xxx'反引号可选兼容注释中无反引号的提及
Pattern updatePattern = Pattern.compile(
"`?function_code`?\\s*=\\s*'([^']+)'", Pattern.CASE_INSENSITIVE);
// INSERT 模式'func_code', 1, UNIX_TIMESTAMP 匹配每个 VALUES 元组中的 function_code
Pattern insertTuplePattern = Pattern.compile(
"'([^']+)'\\s*,\\s*1\\s*,\\s*UNIX_TIMESTAMP", Pattern.CASE_INSENSITIVE);
Set<String> codes = new java.util.HashSet<>();
Matcher updateMatcher = updatePattern.matcher(sql);
while (updateMatcher.find()) {
codes.add(updateMatcher.group(1));
}
Matcher insertMatcher = insertTuplePattern.matcher(sql);
while (insertMatcher.find()) {
codes.add(insertMatcher.group(1));
}
return codes;
}
}

View File

@ -212,17 +212,19 @@ class UserRoleControllerTest {
}
/**
* 验证 assignRoles 方法标注了 @PreAuthorize('system:role:manage')
* 验证 assignRoles 方法标注了 @PreAuthorize('hasFunction(user_management)')
* 无此权限的用户调用将返回 403 Spring Security 方法级鉴权保证
* PR2-PR4 改造U3Controller 层从 hasAuthority('system:role:manage') 改为 hasFunction('user_management')
* Service UserServiceImpl.assignRoles通过 hasAuthorityAndAdmin('system:role:manage') 控制操作粒度
*/
@Test
@DisplayName("assignRoles 方法标注 @PreAuthorize('system:role:manage')无权限返回403")
@DisplayName("assignRoles 方法标注 @PreAuthorize('hasFunction(user_management)')无权限返回403")
void assignRoles_hasPreAuthorizeAnnotation() throws NoSuchMethodException {
Method method = UserController.class.getMethod("assignRoles", Long.class, AssignRolesRequest.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
assertThat(preAuthorize).isNotNull();
assertThat(preAuthorize.value()).contains("system:role:manage");
assertThat(preAuthorize.value()).contains("hasFunction('user_management')");
}
@Test
@ -236,7 +238,7 @@ class UserRoleControllerTest {
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
assertThat(preAuthorize).isNotNull();
assertThat(preAuthorize.value()).contains("system:role:manage");
assertThat(preAuthorize.value()).contains("hasFunction('user_management')");
}
@Test
@ -246,7 +248,7 @@ class UserRoleControllerTest {
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
assertThat(preAuthorize).isNotNull();
assertThat(preAuthorize.value()).contains("system:role:manage");
assertThat(preAuthorize.value()).contains("hasFunction('user_management')");
}
}
}

View File

@ -0,0 +1,158 @@
package com.pms.auth.migration;
import com.pms.common.security.SystemUserSecurityContext;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.assertj.core.api.Assertions.assertThat;
/**
* V13 system-user migration 静态守卫测试P1 #5 ponytail runnable check
* <p>
* 设计依据ce-code-review P1 #5 V13 插入 system 占位用户行id=0L, username='system'
* 此行存在性是 R4 system-user admin 旁路方案的根依赖{@link SystemUserSecurityContext#SYSTEM_USER_ID}
* 必须在 t_user 表中存在否则 setCreatedBy(null) 破坏 NOT NULL 约束
* <p>
* 测试策略pms-auth 模块无 testcontainers/DB 集成测试基础设施遵循 Mockito-only 单元测试约定
* 本测试以静态 SQL 资源校验方式运行加载 V13__add_system_user.sql 作为 classpath 资源
* 解析 INSERT 语句验证关键列值与 SystemUserSecurityContext 常量一致
* <p>
* 守卫范围
* <ul>
* <li>id = 0= {@link SystemUserSecurityContext#SYSTEM_USER_ID}</li>
* <li>username = 'system'= {@link SystemUserSecurityContext#SYSTEM_USERNAME}</li>
* <li>status = 1启用避免 AuthServiceImpl.login "账号禁用"分支混淆</li>
* <li>account_type = 1普通账号类型</li>
* <li>deleted = 0未删除</li>
* <li>real_name "系统占位用户"标识用途</li>
* <li>password BCrypt 哈希非明文非空</li>
* </ul>
* <p>
* ponytail: 不引入 H2/testcontainers 仅校验 SQL 文本静态正确性DB 实际执行行为由 Flyway 启动时
* post-deploy 验证 SQL 覆盖本测试捕获"SQL 编辑回归"风险如误删 INSERT id/username
*/
@DisplayName("V13 system-user migration 静态守卫测试")
class V13SystemUserMigrationTest {
private static final String MIGRATION_PATH = "/db/migration/V13__add_system_user.sql";
/**
* INSERT INTO `t_user` (...) VALUES (0, NULL, 'system', '$2a$...', '系统占位用户', 1, 1, ...)
* 提取 VALUES 子句前 6 个值id, project_id, username, password, real_name, status
* 注释剥离后匹配允许 SQL 文本跨多行书写
*/
private static final Pattern VALUES_PATTERN = Pattern.compile(
"VALUES\\s*\\(\\s*(\\d+)\\s*,\\s*NULL\\s*,\\s*'([^']+)'\\s*,\\s*'([^']+)'\\s*,"
+ "\\s*'([^']+)'\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
/** SQL 行注释 (-- 至行尾),剥离后便于正则匹配跨行 VALUES */
private static final Pattern LINE_COMMENT_PATTERN = Pattern.compile("--[^\\n]*\\n");
@Test
@DisplayName("V13 migration 文件存在于 classpath")
void migrationFile_existsOnClasspath() throws IOException {
try (InputStream is = getClass().getResourceAsStream(MIGRATION_PATH)) {
assertThat(is)
.as("V13 migration 文件必须在 classpath: %s", MIGRATION_PATH)
.isNotNull();
}
}
@Test
@DisplayName("V13 INSERT 包含 system 占位用户行id=0L, username='system', status=1, account_type=1, deleted=0")
void v13Insert_systemUserRow_matchesSystemUserSecurityContextConstants() throws IOException {
String sql = stripComments(loadMigrationSql());
Matcher matcher = VALUES_PATTERN.matcher(sql);
assertThat(matcher.find())
.as("V13 必须包含 INSERT INTO t_user VALUES (0, NULL, 'system', ...) 语句")
.isTrue();
// id: 1 V13 注释要求 id=0
String idValue = matcher.group(1);
assertThat(idValue)
.as("V13 system-user 行 id 必须为 0= SystemUserSecurityContext.SYSTEM_USER_ID")
.isEqualTo("0");
// username: 2
String username = matcher.group(2);
assertThat(username)
.as("V13 system-user 行 username 必须为 'system'= SystemUserSecurityContext.SYSTEM_USERNAME")
.isEqualTo(SystemUserSecurityContext.SYSTEM_USERNAME);
// password: 3 BCrypt 哈希$2a$10$... 格式
String password = matcher.group(3);
assertThat(password)
.as("V13 system-user 行 password 必须为 BCrypt 哈希格式($2a$... 起头),非明文")
.startsWith("$2a$");
// real_name: 4
String realName = matcher.group(4);
assertThat(realName)
.as("V13 system-user 行 real_name 含'系统占位用户'标识用途")
.contains("系统占位用户");
// status: 5
int status = Integer.parseInt(matcher.group(5));
assertThat(status)
.as("V13 system-user 行 status=1启用")
.isEqualTo(1);
// account_type: 6
int accountType = Integer.parseInt(matcher.group(6));
assertThat(accountType)
.as("V13 system-user 行 account_type=1普通账号类型")
.isEqualTo(1);
}
@Test
@DisplayName("V13 INSERT 包含 deleted=0 标记(未软删除)")
void v13Insert_systemUserRow_notSoftDeleted() throws IOException {
String sql = stripComments(loadMigrationSql());
// VALUES 子句末尾倒数第 1 个数字字段是 deleted V13 SQL 列顺序..., deleted
// 匹配 ..., 0\n); 模式 deleted=0
Pattern deletedPattern = Pattern.compile(",\\s*0\\s*\\)\\s*$", Pattern.MULTILINE);
assertThat(deletedPattern.matcher(sql).find())
.as("V13 system-user 行 deleted=0未软删除避免被 BaseModel 自动过滤)")
.isTrue();
}
@Test
@DisplayName("V13 包含 ON DUPLICATE KEY UPDATE幂等执行")
void v13Insert_idempotent() throws IOException {
String sql = loadMigrationSql();
assertThat(sql)
.as("V13 必须包含 ON DUPLICATE KEY UPDATE 保证重复执行幂等")
.containsIgnoringCase("ON DUPLICATE KEY UPDATE");
}
@Test
@DisplayName("V13 注释引用 R4/OQ5 决策(可追溯性)")
void v13Comments_referenceR4AndOQ5Decisions() throws IOException {
String sql = loadMigrationSql();
assertThat(sql)
.as("V13 注释必须引用 R4 system-user 方案(决策可追溯)")
.contains("R4");
assertThat(sql)
.as("V13 注释必须引用 OQ5 决策(决策可追溯)")
.contains("OQ5");
}
private String loadMigrationSql() throws IOException {
try (InputStream is = getClass().getResourceAsStream(MIGRATION_PATH)) {
assertThat(is).as("V13 migration 文件必须在 classpath").isNotNull();
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
}
}
/** 剥离 SQL 行注释 (-- 至行尾),便于正则匹配跨行 VALUES 子句 */
private String stripComments(String sql) {
return LINE_COMMENT_PATTERN.matcher(sql).replaceAll("\n");
}
}

View File

@ -271,6 +271,58 @@ class AuthServiceTest {
.isInstanceOf(BusinessException.class)
.hasMessageContaining("账号已被锁定");
}
// ===== system 占位用户拒绝登录R4 缓解策略 (7) + ce-code-review P1 #2 信任边界修复 =====
@Test
@DisplayName("system 用户名拒绝username='system' → 直接抛 ACCOUNT_DISABLED不查 DB")
void login_systemUsername_throwsBeforeDbLookup() {
LoginRequest request = buildLoginRequest();
request.setUsername("system");
assertThatThrownBy(() -> authService.login(request, "127.0.0.1"))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("账号已被禁用");
// 验证 defense-in-depth用户名拒绝发生在 DB 查询前
verify(userMapper, never()).selectUserWithRoles(anyString());
verify(userMapper, never()).selectById(any());
}
@Test
@DisplayName("system 用户名拒绝username='System' 大小写变体 → 拒绝DB collate 大小写不敏感,必须 equalsIgnoreCase")
void login_systemUsernameCaseVariant_throwsBeforeDbLookup() {
LoginRequest request = buildLoginRequest();
request.setUsername("System");
assertThatThrownBy(() -> authService.login(request, "127.0.0.1"))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("账号已被禁用");
verify(userMapper, never()).selectUserWithRoles(anyString());
}
@Test
@DisplayName("system 用户名拒绝username='SYSTEM' 全大写 → 拒绝")
void login_systemUsernameUppercase_throwsBeforeDbLookup() {
LoginRequest request = buildLoginRequest();
request.setUsername("SYSTEM");
assertThatThrownBy(() -> authService.login(request, "127.0.0.1"))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("账号已被禁用");
}
@Test
@DisplayName("system 用户名拒绝username=' system ' 带空白 → trim 后拒绝")
void login_systemUsernameWithWhitespace_throwsAfterTrim() {
LoginRequest request = buildLoginRequest();
request.setUsername(" system ");
assertThatThrownBy(() -> authService.login(request, "127.0.0.1"))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("账号已被禁用");
}
}
@Nested

View File

@ -0,0 +1,245 @@
package com.pms.auth.service;
import com.pms.auth.dto.AssignRolesRequest;
import com.pms.auth.dto.UserSaveRequest;
import com.pms.auth.dto.UserUpdateRequest;
import com.pms.auth.entity.User;
import com.pms.auth.mapper.ProjectUserMapper;
import com.pms.auth.mapper.RoleMapper;
import com.pms.auth.mapper.UserMapper;
import com.pms.auth.mapper.UserRoleMapper;
import com.pms.auth.service.impl.UserServiceImpl;
import com.pms.common.exception.BusinessException;
import com.pms.common.security.CustomMethodSecurityExpressionHandler;
import com.pms.common.security.SystemUserSecurityContext;
import com.pms.common.security.UserContext.CurrentUser;
import org.aopalliance.intercept.MethodInterceptor;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/**
* UserService @PreAuthorize 切片测试U3
* <p>
* 验证 service {@code @PreAuthorize("hasAuthorityAndAdmin('system:user:manage'|'system:role:manage')")} 实际触发
* <ul>
* <li>场景 1Happy pathfunctions='user_management' + perm='system:user:manage' create 放行</li>
* <li>场景 2Error pathfunctions='user_management' + perm_code create AccessDeniedException</li>
* <li>场景 3Admin bypassfunctions='admin' + perm_code create 放行admin 旁路</li>
* <li>场景 4Edge casefunctions='user_management' + perm='system:user:manage' delete 放行</li>
* <li>场景 5assignRoles 权限functions='user_management' + perm='system:role:manage' assignRoles 放行</li>
* <li>场景 6assignRoles 无权限functions='user_management' + perm_code assignRoles AccessDeniedException</li>
* <li>场景 7 SecurityContextdelete AuthenticationCredentialsNotFoundException</li>
* </ul>
* <p>
* SpEL 逻辑hasFunction / hasAuthorityAndAdmin CustomMethodSecurityExpressionRootTest 单元测试覆盖
* 本测试仅验证 @PreAuthorize AOP 代理链路集成
* <p>
* 设计依据docs/plans/2026-07-05-002-refactor-pre-authorize-alignment-pr2-pr4-plan.md U3 Test scenarios
* <p>
* 实现说明手动构造 AOP 代理ProxyFactory + AuthorizationManagerBeforeMethodInterceptor
* 避免 @SpringBootTest @EnableMethodSecurity @ConditionalOnMissingBean 评估顺序问题
*/
@DisplayName("UserService @PreAuthorize 切片测试")
class UserServiceSecurityTest {
private UserService proxy;
private UserMapper userMapper;
private UserRoleMapper userRoleMapper;
private ProjectUserMapper projectUserMapper;
@BeforeEach
void setUp() {
userMapper = mock(UserMapper.class);
RoleMapper roleMapper = mock(RoleMapper.class);
userRoleMapper = mock(UserRoleMapper.class);
projectUserMapper = mock(ProjectUserMapper.class);
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
UserServiceImpl target = new UserServiceImpl(
userMapper, roleMapper, userRoleMapper, projectUserMapper, passwordEncoder);
// 构造自定义表达式处理器 + PreAuthorize 授权管理器
PreAuthorizeAuthorizationManager authManager = new PreAuthorizeAuthorizationManager();
authManager.setExpressionHandler(new CustomMethodSecurityExpressionHandler());
// 构造 @PreAuthorize 拦截器pointcut 匹配 @PreAuthorize 注解方法
MethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize(authManager);
// 构造 AOP 代理
ProxyFactory factory = new ProxyFactory(target);
factory.addAdvice(interceptor);
proxy = (UserService) factory.getProxy();
}
@AfterEach
void clearContext() {
SecurityContextHolder.clearContext();
}
/**
* 构造以 CurrentUser principal Authentication 并写入 SecurityContextHolder
*
* @param functions 职能代码逗号分隔 "user_management" / "admin" / "user_management,role_management"
* @param permissions 权限码逗号分隔 "system:user:manage" / null
*/
private void authenticate(String functions, String permissions) {
CurrentUser user = new CurrentUser(1L, "admin", 100L, "ROLE_ADMIN", "1",
permissions, 200L, null, functions);
List<GrantedAuthority> authorities = new ArrayList<>();
if (permissions != null && !permissions.isBlank()) {
authorities.addAll(Arrays.stream(permissions.split(","))
.map(String::trim)
.filter(p -> !p.isEmpty())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()));
}
SecurityContextHolder.getContext().setAuthentication(
new PreAuthenticatedAuthenticationToken(user, null, authorities));
}
private UserSaveRequest buildCreateRequest() {
UserSaveRequest request = new UserSaveRequest();
request.setUsername("testuser");
request.setPassword("Test@123456");
request.setRealName("测试用户");
return request;
}
private User buildExistingUser() {
User user = new User();
user.setId(1L);
user.setUsername("existing");
user.setAccountType(1);
return user;
}
// ===== 场景 1Happy path: functions='user_management' + perm='system:user:manage' create 放行 =====
@Test
@DisplayName("场景1: functions='user_management' + perm='system:user:manage' → create 放行")
void create_withUserManagementFunctionAndPerm_shouldAllow() {
authenticate("user_management", "system:user:manage");
when(userMapper.selectCount(any())).thenReturn(0L);
when(userMapper.insert(any())).thenReturn(1);
assertThatCode(() -> proxy.create(buildCreateRequest()))
.doesNotThrowAnyException();
}
// ===== 场景 2Error path: functions='user_management' + perm_code create AccessDeniedException =====
@Test
@DisplayName("场景2: functions='user_management' + 无 perm_code → create 抛 AccessDeniedException")
void create_withUserManagementFunctionButNoPerm_shouldDeny() {
authenticate("user_management", null);
assertThatThrownBy(() -> proxy.create(buildCreateRequest()))
.isInstanceOf(AccessDeniedException.class);
}
// ===== 场景 3Admin bypass: functions='admin' + perm_code create 放行 =====
@Test
@DisplayName("场景3: functions='admin' + 无 perm_code → create 放行admin 旁路)")
void create_withAdminFunction_shouldBypass() {
authenticate("admin", null);
when(userMapper.selectCount(any())).thenReturn(0L);
when(userMapper.insert(any())).thenReturn(1);
assertThatCode(() -> proxy.create(buildCreateRequest()))
.doesNotThrowAnyException();
}
// ===== 场景 4Edge case: functions='user_management' + perm='system:user:manage' delete 放行 =====
@Test
@DisplayName("场景4: functions='user_management' + perm='system:user:manage' → delete 放行")
void delete_withUserManagementFunctionAndPerm_shouldAllow() {
authenticate("user_management", "system:user:manage");
when(userMapper.selectById(any())).thenReturn(buildExistingUser());
assertThatCode(() -> proxy.delete(1L))
.doesNotThrowAnyException();
}
// ===== 场景 5assignRoles 权限: functions='user_management' + perm='system:role:manage' assignRoles 放行 =====
@Test
@DisplayName("场景5: functions='user_management' + perm='system:role:manage' → assignRoles 放行")
void assignRoles_withRoleManagePerm_shouldAllow() {
authenticate("user_management", "system:role:manage");
when(userMapper.selectById(any())).thenReturn(buildExistingUser());
AssignRolesRequest request = new AssignRolesRequest();
request.setRoleIds(List.of(1L));
assertThatCode(() -> proxy.assignRoles(1L, request))
.doesNotThrowAnyException();
}
// ===== 场景 6assignRoles 无权限: functions='user_management' + perm_code assignRoles AccessDeniedException =====
@Test
@DisplayName("场景6: functions='user_management' + 无 perm_code → assignRoles 抛 AccessDeniedException")
void assignRoles_withUserManagementFunctionButNoPerm_shouldDeny() {
authenticate("user_management", null);
AssignRolesRequest request = new AssignRolesRequest();
request.setRoleIds(List.of(1L));
assertThatThrownBy(() -> proxy.assignRoles(1L, request))
.isInstanceOf(AccessDeniedException.class);
}
// ===== 场景 7 SecurityContext delete AuthenticationCredentialsNotFoundException =====
@Test
@DisplayName("场景7: 无 SecurityContext → delete 抛 AuthenticationCredentialsNotFoundException")
void delete_withoutAuthentication_shouldDeny() {
// SecurityContext 未设置authenticate 未调用
// Spring Security 标准行为 Authentication 时抛 AuthenticationCredentialsNotFoundException
// 生产环境由 ExceptionTranslationFilter 转为 401 响应
assertThatThrownBy(() -> proxy.delete(1L))
.isInstanceOf(AuthenticationCredentialsNotFoundException.class);
}
// ===== 补充functions='base' user_management admin create 拒绝 =====
@Test
@DisplayName("补充: functions='base' + 无 perm_code → create 抛 AccessDeniedException")
void create_withBaseFunction_shouldDeny() {
authenticate("base", null);
assertThatThrownBy(() -> proxy.create(buildCreateRequest()))
.isInstanceOf(AccessDeniedException.class);
}
// ===== OQ5 system 占位用户删除守卫ce-code-review P1 #7 ponytail runnable check =====
@Test
@DisplayName("OQ5: admin 旁路通过 @PreAuthorize 后delete(systemUserId=0L) 抛 BusinessException 阻断")
void delete_withSystemUserId_throwsBusinessException() {
authenticate("admin", null); // admin 旁路通过 @PreAuthorize
// mock userMapper.selectById/deleteById因为业务守卫应在 mapper 调用前抛异常
assertThatThrownBy(() -> proxy.delete(SystemUserSecurityContext.SYSTEM_USER_ID))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("系统账户不可删除");
// 验证未触发实际删除DB 调用从未发生
verifyNoInteractions(userMapper);
}
}

View File

@ -4,6 +4,7 @@ import com.pms.base.dto.event.ProjectCreatedEvent;
import com.pms.base.service.ProjectInitService;
import com.pms.common.entity.MqConsumeLog;
import com.pms.common.mapper.MqConsumeLogMapper;
import com.pms.common.security.SystemUserSecurityContext;
import com.pms.common.util.JsonUtils;
import com.rabbitmq.client.Channel;
import lombok.RequiredArgsConstructor;
@ -75,7 +76,12 @@ public class ProjectCreatedConsumer {
}
// 3. 预置主数据
presetMainData(event);
// R4 缓解策略MQ 消费者入口建立 system-user SecurityContext
// 使 ProjectInitService.initProject AbilityPackageService.assignPackages
// @PreAuthorize admin 旁路生效try-with-resources 自动清理避免线程池权限泄漏
try (SystemUserSecurityContext ctx = SystemUserSecurityContext.system()) {
presetMainData(event);
}
// 4. 记录消费成功
MqConsumeLog consumeLog = new MqConsumeLog();

View File

@ -16,6 +16,9 @@ import java.util.Map;
/**
* 能力包版本控制器U21 变更审批流
* 写操作需系统管理员A4权限
* <p>
* PR2-PR4 改造U4.15 方法统一标注 `hasFunction('base')` 控制模块入口
* 各方法保留 RoleGuard.assertA4() 调用 @PreAuthorize 叠加A4 双重防护
*/
@Slf4j
@RestController
@ -29,7 +32,7 @@ public class AbilityPackageController {
* 创建版本草稿
*/
@PostMapping("/ability-packages/{packageId}/versions")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "ABILITY_PACKAGE", type = "CREATE", description = "创建能力包版本草稿")
public Result<Map<String, Long>> createDraft(@PathVariable Long packageId) {
RoleGuard.assertA4();
@ -43,7 +46,7 @@ public class AbilityPackageController {
* 提交审批
*/
@PostMapping("/ability-package-versions/{versionId}/submit")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "ABILITY_PACKAGE", type = "UPDATE", description = "提交能力包版本审批")
public Result<Void> submitForApproval(@PathVariable Long versionId) {
RoleGuard.assertA4();
@ -55,7 +58,7 @@ public class AbilityPackageController {
* 审批通过snapshot 含配置明细recordParams=false 脱敏
*/
@PostMapping("/ability-package-versions/{versionId}/approve")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "ABILITY_PACKAGE", type = "UPDATE", description = "审批通过能力包版本", recordParams = false)
public Result<Void> approve(@PathVariable Long versionId,
@RequestParam(required = false) String comment) {
@ -68,7 +71,7 @@ public class AbilityPackageController {
* 驳回recordParams=false 避免驳回原因等明细入审计
*/
@PostMapping("/ability-package-versions/{versionId}/reject")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "ABILITY_PACKAGE", type = "UPDATE", description = "驳回能力包版本", recordParams = false)
public Result<Void> reject(@PathVariable Long versionId,
@RequestParam String reason) {
@ -81,7 +84,7 @@ public class AbilityPackageController {
* 删除版本 DRAFT/REJECTEDAPPROVED 被引用时拒绝
*/
@DeleteMapping("/ability-package-versions/{versionId}")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "ABILITY_PACKAGE", type = "DELETE", description = "删除能力包版本")
public Result<Void> delete(@PathVariable Long versionId) {
RoleGuard.assertA4();

View File

@ -21,6 +21,9 @@ import java.util.Map;
* 路径前缀: /api/v1/base/approval-flow-configs
* <p>
* 写操作需系统管理员A4权限R23
* <p>
* PR2-PR4 改造U4.13 方法统一标注 `hasFunction('base')` 控制模块入口
* save 方法保留 RoleGuard.assertA4() 调用 @PreAuthorize 叠加A4 双重防护
*/
@Slf4j
@RestController
@ -34,7 +37,7 @@ public class ApprovalFlowConfigController {
* 按阶段流转标识查询启用模板
*/
@GetMapping("/transition/{transition}")
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
@PreAuthorize("hasFunction('base')")
public Result<ApprovalFlowTemplateDTO> getByTransition(@PathVariable("transition") String transition) {
return Result.success(approvalFlowConfigService.getTemplateByTransition(transition));
}
@ -43,7 +46,7 @@ public class ApprovalFlowConfigController {
* 列出全部模板
*/
@GetMapping
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<ApprovalFlowTemplateDTO>> list() {
return Result.success(approvalFlowConfigService.listTemplates());
}
@ -52,7 +55,7 @@ public class ApprovalFlowConfigController {
* 保存模板创建或更新
*/
@PostMapping
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "审批流配置修改")
public Result<Map<String, Long>> save(@Valid @RequestBody ApprovalFlowTemplateSaveRequest request) {
RoleGuard.assertA4();

View File

@ -17,6 +17,9 @@ import java.util.Map;
/**
* 归档闭环控制器
* 路径前缀: /api/v1/base/projects/{projectId}/archive
* <p>
* PR2-PR4 改造U4.12 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:lifecycle:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -30,7 +33,7 @@ public class ArchiveController {
* 发起归档结算校验 资产移交 归档审批
*/
@PostMapping
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "项目归档")
public Result<Map<String, Long>> archive(@PathVariable Long projectId,
@Valid @RequestBody AssetHandoverRequest request) {
@ -44,7 +47,7 @@ public class ArchiveController {
* 发起归档恢复
*/
@PostMapping("/recover")
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "归档恢复")
public Result<Map<String, Long>> recover(@PathVariable Long projectId,
@Valid @RequestBody ArchiveRecoveryRequest request) {

View File

@ -34,7 +34,7 @@ public class BuildingController {
* 楼栋列表查询分页
*/
@GetMapping
@PreAuthorize("hasAuthority('base:building:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<BuildingDTO>> list(BuildingDTO query) {
return Result.success(buildingService.page(query));
}
@ -43,7 +43,7 @@ public class BuildingController {
* 创建楼栋
*/
@PostMapping
@PreAuthorize("hasAuthority('base:building:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建楼栋")
public Result<Map<String, Long>> create(@Valid @RequestBody BuildingSaveRequest request) {
Long id = buildingService.create(request);
@ -56,7 +56,7 @@ public class BuildingController {
* 楼栋详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:building:manage')")
@PreAuthorize("hasFunction('base')")
public Result<BuildingDTO> getById(@PathVariable Long id) {
return Result.success(buildingService.getById(id));
}
@ -65,7 +65,7 @@ public class BuildingController {
* 更新楼栋
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:building:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新楼栋")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody BuildingSaveRequest request) {
buildingService.update(id, request);
@ -76,7 +76,7 @@ public class BuildingController {
* 删除楼栋
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:building:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除楼栋")
public Result<Void> delete(@PathVariable Long id) {
buildingService.delete(id);
@ -87,7 +87,7 @@ public class BuildingController {
* 楼栋树楼栋楼层房间
*/
@GetMapping("/tree")
@PreAuthorize("hasAuthority('base:building:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<BuildingTreeDTO>> tree(@RequestParam(required = false) Long buildingId) {
return Result.success(buildingService.tree(UserContext.getProjectId(), buildingId));
}

View File

@ -31,7 +31,7 @@ public class CamChargeController {
* @param chargePeriod 分摊周期 2024-01
*/
@PostMapping("/allocate")
@PreAuthorize("hasAuthority('base:finance:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "执行 CAM 分摊")
public Result<List<CamCharge>> allocate(@RequestParam Long totalAmountFen,
@RequestParam String chargePeriod) {
@ -43,7 +43,7 @@ public class CamChargeController {
* 查询当前项目某周期的分摊明细
*/
@GetMapping
@PreAuthorize("hasAuthority('base:finance:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<CamCharge>> list(@RequestParam String period) {
Long projectId = UserContext.getProjectId();
return Result.success(camChargeService.listByProjectAndPeriod(projectId, period));

View File

@ -31,7 +31,7 @@ public class CommunityActivityController {
* 分页查询活动列表
*/
@GetMapping
@PreAuthorize("hasAuthority('base:activity:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<CommunityActivity>> page(
@RequestParam(required = false) Integer status,
@RequestParam(defaultValue = "1") int page,
@ -43,7 +43,7 @@ public class CommunityActivityController {
* 活动详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:activity:manage')")
@PreAuthorize("hasFunction('base')")
public Result<CommunityActivity> getById(@PathVariable Long id) {
return Result.success(communityActivityService.getById(id));
}
@ -52,7 +52,7 @@ public class CommunityActivityController {
* 创建活动
*/
@PostMapping
@PreAuthorize("hasAuthority('base:activity:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建社区活动")
public Result<Map<String, Long>> create(@Valid @RequestBody CommunityActivity activity) {
Long id = communityActivityService.create(activity);
@ -65,7 +65,7 @@ public class CommunityActivityController {
* 更新活动
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:activity:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新社区活动")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody CommunityActivity activity) {
communityActivityService.update(id, activity);
@ -76,7 +76,7 @@ public class CommunityActivityController {
* 报名参加活动
*/
@PostMapping("/{id}/sign-up")
@PreAuthorize("hasAuthority('base:activity:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "活动报名")
public Result<Void> signUp(@PathVariable Long id) {
communityActivityService.signUp(id);
@ -87,7 +87,7 @@ public class CommunityActivityController {
* 删除活动
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:activity:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除社区活动")
public Result<Void> delete(@PathVariable Long id) {
communityActivityService.delete(id);

View File

@ -33,7 +33,7 @@ public class ContractController {
* 合同列表查询分页
*/
@GetMapping
@PreAuthorize("hasAuthority('base:contract:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<ContractDTO>> list(ContractDTO query) {
return Result.success(contractService.page(query));
}
@ -42,7 +42,7 @@ public class ContractController {
* 创建合同
*/
@PostMapping
@PreAuthorize("hasAuthority('base:contract:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建合同")
public Result<Map<String, Long>> create(@Valid @RequestBody ContractSaveRequest request) {
Long id = contractService.create(request);
@ -55,7 +55,7 @@ public class ContractController {
* 合同详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:contract:manage')")
@PreAuthorize("hasFunction('base')")
public Result<ContractDTO> getById(@PathVariable Long id) {
return Result.success(contractService.getById(id));
}
@ -64,7 +64,7 @@ public class ContractController {
* 更新合同
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:contract:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新合同")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody ContractSaveRequest request) {
contractService.update(id, request);
@ -75,7 +75,7 @@ public class ContractController {
* 删除合同
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:contract:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除合同")
public Result<Void> delete(@PathVariable Long id) {
contractService.delete(id);
@ -86,7 +86,7 @@ public class ContractController {
* 即将到期合同列表
*/
@GetMapping("/expiring")
@PreAuthorize("hasAuthority('base:contract:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<ContractDTO>> expiring(@RequestParam(defaultValue = "30") int days) {
return Result.success(contractService.expiring(days));
}
@ -95,7 +95,7 @@ public class ContractController {
* 合同续签
*/
@PostMapping("/{id}/renew")
@PreAuthorize("hasAuthority('base:contract:manage')")
@PreAuthorize("hasFunction('base')")
@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);

View File

@ -32,7 +32,7 @@ public class ContractTypeController {
* 合同类型列表
*/
@GetMapping
@PreAuthorize("hasAuthority('base:contract:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<ContractTypeDTO>> list() {
return Result.success(contractTypeService.list(UserContext.getProjectId()));
}
@ -41,7 +41,7 @@ public class ContractTypeController {
* 创建合同类型
*/
@PostMapping
@PreAuthorize("hasAuthority('base:contract:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建合同类型")
public Result<Map<String, Long>> create(@Valid @RequestBody ContractTypeSaveRequest request) {
Long id = contractTypeService.create(request);
@ -54,7 +54,7 @@ public class ContractTypeController {
* 更新合同类型
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:contract:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新合同类型")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody ContractTypeSaveRequest request) {
contractTypeService.update(id, request);
@ -65,7 +65,7 @@ public class ContractTypeController {
* 删除合同类型
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:contract:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除合同类型")
public Result<Void> delete(@PathVariable Long id) {
contractTypeService.delete(id);

View File

@ -14,6 +14,7 @@ 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.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@ -24,6 +25,8 @@ import java.util.Map;
/**
* 仪表盘统计控制器基础服务
* 路径前缀: /api/v1/base/dashboard
* <p>
* PR2-PR4 改造U4.11 方法标注 `hasFunction('base')` 控制模块入口
*/
@Slf4j
@RestController
@ -42,6 +45,7 @@ public class DashboardController {
* 统计数据项目总数楼栋总数房间总数业主总数租户总数
*/
@GetMapping("/stats")
@PreAuthorize("hasFunction('base')")
public Result<Map<String, Object>> stats() {
Long projectId = UserContext.getProjectId();
Map<String, Object> data = new HashMap<>();

View File

@ -19,6 +19,9 @@ import java.util.Map;
* 自定义数据范围规则控制器data_scope=5
* <p>
* A4 系统管理员可访问路径前缀: /api/v1/base/data-scope-rules
* <p>
* PR2-PR4 改造U4.14 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('system:data-scope:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -32,7 +35,7 @@ public class DataScopeRuleController {
* 查询全部规则
*/
@GetMapping
@PreAuthorize("hasAuthority('system:data-scope:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<DataScopeRule>> list() {
return Result.success(dataScopeRuleService.list());
}
@ -41,7 +44,7 @@ public class DataScopeRuleController {
* 创建规则
*/
@PostMapping
@PreAuthorize("hasAuthority('system:data-scope:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建数据范围规则")
public Result<Map<String, Long>> create(@Valid @RequestBody DataScopeRuleSaveRequest request) {
Long id = dataScopeRuleService.create(request);
@ -54,7 +57,7 @@ public class DataScopeRuleController {
* 更新规则
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('system:data-scope:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新数据范围规则")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody DataScopeRuleSaveRequest request) {
dataScopeRuleService.update(id, request);
@ -65,7 +68,7 @@ public class DataScopeRuleController {
* 删除规则
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('system:data-scope:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除数据范围规则")
public Result<Void> delete(@PathVariable Long id) {
dataScopeRuleService.delete(id);

View File

@ -32,7 +32,7 @@ public class DeviceCategoryController {
* 设备分类树
*/
@GetMapping("/tree")
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<DeviceCategoryDTO>> tree() {
return Result.success(deviceCategoryService.tree(UserContext.getProjectId()));
}
@ -41,7 +41,7 @@ public class DeviceCategoryController {
* 创建设备分类
*/
@PostMapping
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建设备分类")
public Result<Map<String, Long>> create(@Valid @RequestBody DeviceCategorySaveRequest request) {
Long id = deviceCategoryService.create(request);
@ -54,7 +54,7 @@ public class DeviceCategoryController {
* 更新设备分类
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新设备分类")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody DeviceCategorySaveRequest request) {
deviceCategoryService.update(id, request);
@ -65,7 +65,7 @@ public class DeviceCategoryController {
* 删除设备分类
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除设备分类")
public Result<Void> delete(@PathVariable Long id) {
deviceCategoryService.delete(id);

View File

@ -32,7 +32,7 @@ public class DeviceController {
* 设备列表查询分页
*/
@GetMapping
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<DeviceDTO>> list(DeviceDTO query) {
return Result.success(deviceService.page(query));
}
@ -41,7 +41,7 @@ public class DeviceController {
* 创建设备
*/
@PostMapping
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建设备")
public Result<Map<String, Long>> create(@Valid @RequestBody DeviceSaveRequest request) {
Long id = deviceService.create(request);
@ -54,7 +54,7 @@ public class DeviceController {
* 设备详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
public Result<DeviceDTO> getById(@PathVariable Long id) {
return Result.success(deviceService.getById(id));
}
@ -63,7 +63,7 @@ public class DeviceController {
* 更新设备
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新设备")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody DeviceSaveRequest request) {
deviceService.update(id, request);
@ -74,7 +74,7 @@ public class DeviceController {
* 删除设备
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除设备")
public Result<Void> delete(@PathVariable Long id) {
deviceService.delete(id);
@ -85,7 +85,7 @@ public class DeviceController {
* 更新设备状态
*/
@PutMapping("/{id}/status")
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新设备状态")
public Result<Void> updateStatus(@PathVariable Long id, @Valid @RequestBody DeviceStatusRequest request) {
deviceService.updateStatus(id, request.getStatus());

View File

@ -32,7 +32,7 @@ public class DeviceMaintenanceController {
* 维保记录列表查询分页
*/
@GetMapping
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<DeviceMaintenanceDTO>> list(DeviceMaintenanceDTO query,
@RequestParam(required = false) Long deviceId) {
return Result.success(maintenanceService.page(query, deviceId));
@ -42,7 +42,7 @@ public class DeviceMaintenanceController {
* 创建维保记录
*/
@PostMapping
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建设备维保记录")
public Result<Map<String, Long>> create(@RequestParam Long deviceId,
@Valid @RequestBody DeviceMaintenanceSaveRequest request) {
@ -56,7 +56,7 @@ public class DeviceMaintenanceController {
* 维保详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
public Result<DeviceMaintenanceDTO> getById(@PathVariable Long id) {
return Result.success(maintenanceService.getById(id));
}
@ -65,7 +65,7 @@ public class DeviceMaintenanceController {
* 即将到期维保列表
*/
@GetMapping("/expiring")
@PreAuthorize("hasAuthority('base:device:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<DeviceMaintenanceDTO>> expiring(@RequestParam(defaultValue = "30") int days) {
return Result.success(maintenanceService.expiring(days));
}

View File

@ -32,7 +32,7 @@ public class EnergyMeterController {
* 表计列表
*/
@GetMapping
@PreAuthorize("hasAuthority('base:energy:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<EnergyMeter>> list() {
return Result.success(energyMeterService.list(UserContext.getProjectId()));
}
@ -41,7 +41,7 @@ public class EnergyMeterController {
* 表计详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:energy:manage')")
@PreAuthorize("hasFunction('base')")
public Result<EnergyMeter> getById(@PathVariable Long id) {
return Result.success(energyMeterService.getById(id));
}
@ -50,7 +50,7 @@ public class EnergyMeterController {
* 创建表计
*/
@PostMapping
@PreAuthorize("hasAuthority('base:energy:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建能源表计")
public Result<Map<String, Long>> create(@Valid @RequestBody EnergyMeter entity) {
Long id = energyMeterService.create(entity);
@ -63,7 +63,7 @@ public class EnergyMeterController {
* 更新表计
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:energy:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新能源表计")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody EnergyMeter entity) {
energyMeterService.update(id, entity);
@ -74,7 +74,7 @@ public class EnergyMeterController {
* 删除表计
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:energy:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除能源表计")
public Result<Void> delete(@PathVariable Long id) {
energyMeterService.delete(id);
@ -85,7 +85,7 @@ public class EnergyMeterController {
* 抄表记录首期仅台账预留 IoT 接入点 T4
*/
@PostMapping("/{id}/reading")
@PreAuthorize("hasAuthority('base:energy:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "能源表计抄表")
public Result<Void> recordReading(@PathVariable Long id, @RequestParam BigDecimal newReading) {
energyMeterService.recordReading(id, newReading);

View File

@ -18,6 +18,9 @@ import java.util.Map;
/**
* 企业控制器
* 路径前缀: /api/v1/enterprises
* <p>
* PR2-PR4 改造U5.15 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:enterprise:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -31,7 +34,7 @@ public class EnterpriseController {
* 企业列表查询分页
*/
@GetMapping
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<EnterpriseDTO>> list(EnterpriseDTO query) {
return Result.success(enterpriseService.page(query));
}
@ -40,7 +43,7 @@ public class EnterpriseController {
* 创建企业
*/
@PostMapping
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建企业", recordParams = false)
public Result<Map<String, Long>> create(@Valid @RequestBody EnterpriseSaveRequest request) {
Long id = enterpriseService.create(request);
@ -53,7 +56,7 @@ public class EnterpriseController {
* 企业详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
public Result<EnterpriseDTO> getById(@PathVariable Long id) {
return Result.success(enterpriseService.getById(id));
}
@ -62,7 +65,7 @@ public class EnterpriseController {
* 更新企业
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新企业", recordParams = false)
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody EnterpriseSaveRequest request) {
enterpriseService.update(id, request);
@ -73,7 +76,7 @@ public class EnterpriseController {
* 删除企业
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除企业")
public Result<Void> delete(@PathVariable Long id) {
enterpriseService.delete(id);

View File

@ -18,6 +18,9 @@ import java.util.Map;
/**
* 企业画像控制器
* 路径前缀: /api/v1/base/enterprise-profiles
* <p>
* PR2-PR4 改造U5.15 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:enterprise:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -31,7 +34,7 @@ public class EnterpriseProfileController {
* 企业画像列表
*/
@GetMapping
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<EnterpriseProfile>> list() {
return Result.success(enterpriseProfileService.list(UserContext.getProjectId()));
}
@ -40,7 +43,7 @@ public class EnterpriseProfileController {
* 企业画像详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
public Result<EnterpriseProfile> getById(@PathVariable Long id) {
return Result.success(enterpriseProfileService.getById(id));
}
@ -49,7 +52,7 @@ public class EnterpriseProfileController {
* 创建企业画像
*/
@PostMapping
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建企业画像", recordParams = false)
public Result<Map<String, Long>> create(@Valid @RequestBody EnterpriseProfile entity) {
Long id = enterpriseProfileService.create(entity);
@ -62,7 +65,7 @@ public class EnterpriseProfileController {
* 更新企业画像
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新企业画像", recordParams = false)
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody EnterpriseProfile entity) {
enterpriseProfileService.update(id, entity);
@ -73,7 +76,7 @@ public class EnterpriseProfileController {
* 删除企业画像
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除企业画像")
public Result<Void> delete(@PathVariable Long id) {
enterpriseProfileService.delete(id);

View File

@ -18,6 +18,9 @@ import java.util.Map;
/**
* 企业服务申请记录控制器
* 路径前缀: /api/v1/base/enterprise-services
* <p>
* PR2-PR4 改造U5.14 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:enterprise:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -31,7 +34,7 @@ public class EnterpriseServiceController {
* 服务申请详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
public Result<EnterpriseService> getById(@PathVariable Long id) {
return Result.success(enterpriseServiceService.getById(id));
}
@ -40,7 +43,7 @@ public class EnterpriseServiceController {
* 当前项目下企业服务申请列表
*/
@GetMapping
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<EnterpriseService>> list() {
return Result.success(enterpriseServiceService.listByProject(UserContext.getProjectId()));
}
@ -49,7 +52,7 @@ public class EnterpriseServiceController {
* 提交服务申请
*/
@PostMapping("/apply")
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "提交企业服务申请", recordParams = false)
public Result<Map<String, Long>> apply(@Valid @RequestBody EnterpriseService service) {
service.setProjectId(UserContext.getProjectId());
@ -63,7 +66,7 @@ public class EnterpriseServiceController {
* 处理服务申请标记为已完成
*/
@PutMapping("/{id}/process")
@PreAuthorize("hasAuthority('base:enterprise:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "处理企业服务申请", recordParams = false)
public Result<Void> process(@PathVariable Long id,
@RequestParam String handler,

View File

@ -19,6 +19,9 @@ import java.util.Map;
/**
* 楼层控制器
* 路径前缀: /api/v1/floors
* <p>
* PR2-PR4 改造U5.14 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:floor:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -32,7 +35,7 @@ public class FloorController {
* 楼层列表查询
*/
@GetMapping
@PreAuthorize("hasAuthority('base:floor:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<FloorDTO>> list(@RequestParam(required = false) Long buildingId) {
return Result.success(floorService.list(UserContext.getProjectId(), buildingId));
}
@ -41,7 +44,7 @@ public class FloorController {
* 创建楼层
*/
@PostMapping
@PreAuthorize("hasAuthority('base:floor:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建楼层")
public Result<Map<String, Long>> create(@Valid @RequestBody FloorSaveRequest request) {
Long id = floorService.create(request);
@ -54,7 +57,7 @@ public class FloorController {
* 更新楼层
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:floor:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新楼层")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody FloorSaveRequest request) {
floorService.update(id, request);
@ -65,7 +68,7 @@ public class FloorController {
* 删除楼层
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:floor:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除楼层")
public Result<Void> delete(@PathVariable Long id) {
floorService.delete(id);

View File

@ -33,7 +33,7 @@ public class LeaseContractController {
* 合同详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:lease:manage')")
@PreAuthorize("hasFunction('base')")
public Result<LeaseContract> getById(@PathVariable Long id) {
return Result.success(leaseContractService.getById(id));
}
@ -42,7 +42,7 @@ public class LeaseContractController {
* 当前项目下合同列表
*/
@GetMapping
@PreAuthorize("hasAuthority('base:lease:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<LeaseContract>> list() {
return Result.success(leaseContractService.listByProject(UserContext.getProjectId()));
}
@ -51,7 +51,7 @@ public class LeaseContractController {
* 创建租赁合同
*/
@PostMapping
@PreAuthorize("hasAuthority('base:lease:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建租赁合同")
public Result<Map<String, Long>> create(@Valid @RequestBody LeaseContract contract) {
contract.setProjectId(UserContext.getProjectId());
@ -65,7 +65,7 @@ public class LeaseContractController {
* 续约延长合同结束日期
*/
@PutMapping("/{id}/renew")
@PreAuthorize("hasAuthority('base:lease:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "租赁合同续约")
public Result<Void> renew(@PathVariable Long id,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate newEndDate) {
@ -77,7 +77,7 @@ public class LeaseContractController {
* 退租
*/
@PutMapping("/{id}/terminate")
@PreAuthorize("hasAuthority('base:lease:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "租赁合同退租")
public Result<Void> terminate(@PathVariable Long id) {
leaseContractService.terminate(id);

View File

@ -19,6 +19,9 @@ import java.util.Map;
/**
* 生命周期流转控制器
* 路径前缀: /api/v1/base/projects/{projectId}/lifecycle
* <p>
* PR2-PR4 改造U4.14 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:lifecycle:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -32,7 +35,7 @@ public class LifecycleController {
* 查询当前生命周期阶段R5
*/
@GetMapping("/stage")
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "QUERY", description = "查询生命周期阶段")
public Result<LifecycleStageDTO> getStage(@PathVariable Long projectId) {
return Result.success(lifecycleService.getStage(projectId));
@ -42,7 +45,7 @@ public class LifecycleController {
* 发起阶段流转
*/
@PostMapping("/transition")
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "发起阶段流转")
public Result<Map<String, Long>> transition(@PathVariable Long projectId,
@Valid @RequestBody StageTransitionRequest request) {
@ -56,7 +59,7 @@ public class LifecycleController {
* 审批通过
*/
@PostMapping("/approve")
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "审批通过")
public Result<ApprovalCompletedEvent> approve(@PathVariable Long projectId,
@Valid @RequestBody ApprovalActionRequest request) {
@ -67,7 +70,7 @@ public class LifecycleController {
* 审批退回
*/
@PostMapping("/reject")
@PreAuthorize("hasAuthority('base:lifecycle:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "审批退回")
public Result<Void> reject(@PathVariable Long projectId,
@Valid @RequestBody ApprovalActionRequest request) {

View File

@ -31,7 +31,7 @@ public class MeetingRoomController {
* 会议室详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:meeting:manage')")
@PreAuthorize("hasFunction('base')")
public Result<MeetingRoom> getById(@PathVariable Long id) {
return Result.success(meetingRoomService.getById(id));
}
@ -40,7 +40,7 @@ public class MeetingRoomController {
* 当前项目下会议室列表
*/
@GetMapping
@PreAuthorize("hasAuthority('base:meeting:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<MeetingRoom>> list() {
return Result.success(meetingRoomService.listByProject(UserContext.getProjectId()));
}
@ -49,7 +49,7 @@ public class MeetingRoomController {
* 创建会议室
*/
@PostMapping
@PreAuthorize("hasAuthority('base:meeting:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建会议室")
public Result<Map<String, Long>> create(@Valid @RequestBody MeetingRoom room) {
room.setProjectId(UserContext.getProjectId());
@ -63,7 +63,7 @@ public class MeetingRoomController {
* 预订会议室
*/
@PutMapping("/{id}/book")
@PreAuthorize("hasAuthority('base:meeting:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "预订会议室")
public Result<Void> book(@PathVariable Long id) {
meetingRoomService.book(id);
@ -74,7 +74,7 @@ public class MeetingRoomController {
* 取消预订
*/
@PutMapping("/{id}/cancel")
@PreAuthorize("hasAuthority('base:meeting:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "取消会议室预订")
public Result<Void> cancelBooking(@PathVariable Long id) {
meetingRoomService.cancelBooking(id);

View File

@ -18,6 +18,9 @@ import java.util.Map;
/**
* 业委会控制器
* 路径前缀: /api/v1/base/owner-committees
* <p>
* PR2-PR4 改造U5.15 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:committee:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -31,7 +34,7 @@ public class OwnerCommitteeController {
* 分页查询业委会列表
*/
@GetMapping
@PreAuthorize("hasAuthority('base:committee:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<OwnerCommittee>> page(
@RequestParam(required = false) Integer status,
@RequestParam(defaultValue = "1") int page,
@ -43,7 +46,7 @@ public class OwnerCommitteeController {
* 业委会详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:committee:manage')")
@PreAuthorize("hasFunction('base')")
public Result<OwnerCommittee> getById(@PathVariable Long id) {
return Result.success(ownerCommitteeService.getById(id));
}
@ -52,7 +55,7 @@ public class OwnerCommitteeController {
* 创建业委会
*/
@PostMapping
@PreAuthorize("hasAuthority('base:committee:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建业委会", recordParams = false)
public Result<Map<String, Long>> create(@Valid @RequestBody OwnerCommittee committee) {
Long id = ownerCommitteeService.create(committee);
@ -65,7 +68,7 @@ public class OwnerCommitteeController {
* 更新业委会
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:committee:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新业委会", recordParams = false)
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody OwnerCommittee committee) {
ownerCommitteeService.update(id, committee);
@ -76,7 +79,7 @@ public class OwnerCommitteeController {
* 删除业委会
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:committee:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除业委会")
public Result<Void> delete(@PathVariable Long id) {
ownerCommitteeService.delete(id);

View File

@ -37,7 +37,7 @@ public class OwnerController {
* 业主列表查询分页
*/
@GetMapping
@PreAuthorize("hasAuthority('base:owner:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<OwnerDTO>> list(OwnerDTO query) {
return Result.success(ownerService.page(query));
}
@ -46,7 +46,7 @@ public class OwnerController {
* 创建业主
*/
@PostMapping
@PreAuthorize("hasAuthority('base:owner:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建业主", recordParams = false)
public Result<Map<String, Long>> create(@Valid @RequestBody OwnerSaveRequest request) {
Long id = ownerService.create(request);
@ -59,7 +59,7 @@ public class OwnerController {
* 业主详情含房产信息
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:owner:manage')")
@PreAuthorize("hasFunction('base')")
public Result<OwnerDTO> getById(@PathVariable Long id) {
return Result.success(ownerService.getById(id));
}
@ -68,7 +68,7 @@ public class OwnerController {
* 更新业主
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:owner:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新业主", recordParams = false)
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody OwnerSaveRequest request) {
ownerService.update(id, request);
@ -79,7 +79,7 @@ public class OwnerController {
* 删除业主
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:owner:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除业主")
public Result<Void> delete(@PathVariable Long id) {
ownerService.delete(id);
@ -90,7 +90,7 @@ public class OwnerController {
* 业主房产列表
*/
@GetMapping("/{id}/rooms")
@PreAuthorize("hasAuthority('base:owner:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<OwnerRoomDTO>> getOwnerRooms(@PathVariable Long id) {
return Result.success(ownerService.getOwnerRooms(id));
}
@ -99,7 +99,7 @@ public class OwnerController {
* 批量导入业主Excel上传
*/
@PostMapping("/import")
@PreAuthorize("hasAuthority('base:owner:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "导入业主", recordParams = false)
public Result<Map<String, Object>> importOwners(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {

View File

@ -19,6 +19,9 @@ import java.util.Map;
/**
* 业主房间关联控制器
* 路径前缀: /api/v1/owner-rooms
* <p>
* PR2-PR4 改造U5.13 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:owner:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -32,7 +35,7 @@ public class OwnerRoomController {
* 业主房间关联列表
*/
@GetMapping
@PreAuthorize("hasAuthority('base:owner:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<OwnerRoomDTO>> list(@RequestParam(required = false) Long ownerId,
@RequestParam(required = false) Long roomId) {
return Result.success(ownerRoomService.list(UserContext.getProjectId(), ownerId, roomId));
@ -42,7 +45,7 @@ public class OwnerRoomController {
* 创建业主房间关联
*/
@PostMapping
@PreAuthorize("hasAuthority('base:owner:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建业主房间关联", recordParams = false)
public Result<Map<String, Long>> create(@Valid @RequestBody OwnerRoomRelRequest request) {
Long id = ownerRoomService.create(request);
@ -55,7 +58,7 @@ public class OwnerRoomController {
* 删除业主房间关联
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:owner:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除业主房间关联")
public Result<Void> delete(@PathVariable Long id) {
ownerRoomService.delete(id);

View File

@ -18,6 +18,9 @@ import java.util.Map;
/**
* 前期介入进度控制器
* 路径前缀: /api/v1/base/projects/{projectId}/pre-handover/progress
* <p>
* PR2-PR4 改造U4.14 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:project:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -31,7 +34,7 @@ public class PreHandoverProgressController {
* 创建前期介入进度记录
*/
@PostMapping
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建前期介入进度", recordParams = false)
public Result<Map<String, Long>> create(@PathVariable Long projectId,
@Valid @RequestBody PreHandoverProgressDTO dto) {
@ -45,7 +48,7 @@ public class PreHandoverProgressController {
* 查询项目前期介入进度列表可选 progress_type 筛选
*/
@GetMapping
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<PreHandoverProgress>> list(@PathVariable Long projectId,
@RequestParam(required = false) String progressType) {
return Result.success(preHandoverProgressService.listByProject(projectId, progressType));
@ -55,7 +58,7 @@ public class PreHandoverProgressController {
* 更新前期介入进度记录
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新前期介入进度", recordParams = false)
public Result<Void> update(@PathVariable Long projectId,
@PathVariable Long id,
@ -68,7 +71,7 @@ public class PreHandoverProgressController {
* 删除前期介入进度记录逻辑删除
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除前期介入进度", recordParams = false)
public Result<Void> delete(@PathVariable Long projectId,
@PathVariable Long id) {

View File

@ -12,6 +12,9 @@ import org.springframework.web.bind.annotation.*;
/**
* 项目能力包主业态控制器
* 路径前缀: /api/v1/base/projects/{projectId}/abilities
* <p>
* PR2-PR4 改造U4.12 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:project:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -25,7 +28,7 @@ public class ProjectAbilityController {
* 阶段3 自动重算主业态按户数最多业态重设 is_primary
*/
@PostMapping("/recompute-primary")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "重算主业态", recordParams = false)
public Result<PrimaryRecomputeResultDTO> recomputePrimary(@PathVariable Long projectId) {
return Result.success(projectAbilityService.recomputePrimary(projectId));
@ -35,7 +38,7 @@ public class ProjectAbilityController {
* 强制覆盖主业态人工确认场景
*/
@PutMapping("/{abilityId}/primary")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "强制覆盖主业态", recordParams = false)
public Result<Void> forceOverridePrimary(@PathVariable Long projectId,
@PathVariable Long abilityId) {

View File

@ -18,6 +18,9 @@ import java.util.Map;
/**
* 项目属性扩展控制器
* 路径前缀: /api/v1/base/projects/{id}/attributes
* <p>
* PR2-PR4 改造U4.14 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:project:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -31,7 +34,7 @@ public class ProjectAttributeController {
* 创建项目属性
*/
@PostMapping
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建项目属性", recordParams = false)
public Result<Map<String, Long>> create(@PathVariable Long projectId,
@Valid @RequestBody ProjectAttributeDTO dto) {
@ -45,7 +48,7 @@ public class ProjectAttributeController {
* 查询项目属性列表可选 attr_type 筛选
*/
@GetMapping
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<ProjectAttribute>> list(@PathVariable Long projectId,
@RequestParam(required = false) String attrType) {
return Result.success(projectAttributeService.listByProject(projectId, attrType));
@ -55,7 +58,7 @@ public class ProjectAttributeController {
* 更新项目属性
*/
@PutMapping("/{attrId}")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新项目属性", recordParams = false)
public Result<Void> update(@PathVariable Long projectId,
@PathVariable Long attrId,
@ -68,7 +71,7 @@ public class ProjectAttributeController {
* 删除项目属性
*/
@DeleteMapping("/{attrId}")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除项目属性", recordParams = false)
public Result<Void> delete(@PathVariable Long projectId,
@PathVariable Long attrId) {

View File

@ -21,6 +21,9 @@ import java.util.Map;
/**
* 物业项目控制器
* 路径前缀: /api/v1/projects
* <p>
* PR2-PR4 改造U4.17 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:project:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -35,7 +38,7 @@ public class ProjectController {
* 项目列表查询分页
*/
@GetMapping
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<ProjectDTO>> list(ProjectDTO query) {
return Result.success(projectService.page(query));
}
@ -44,7 +47,7 @@ public class ProjectController {
* 创建项目
*/
@PostMapping
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建项目", recordParams = false)
public Result<Map<String, Long>> create(@Valid @RequestBody ProjectSaveRequest request) {
Long id = projectService.create(request);
@ -57,7 +60,7 @@ public class ProjectController {
* 获取全部项目下拉选择用
*/
@GetMapping("/all")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<ProjectDTO>> listAll() {
return Result.success(projectService.listAll());
}
@ -66,7 +69,7 @@ public class ProjectController {
* 项目详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
public Result<ProjectDTO> getById(@PathVariable Long id) {
return Result.success(projectService.getById(id));
}
@ -75,7 +78,7 @@ public class ProjectController {
* 更新项目
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新项目", recordParams = false)
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody ProjectSaveRequest request) {
projectService.update(id, request);
@ -86,7 +89,7 @@ public class ProjectController {
* 删除项目
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除项目")
public Result<Void> delete(@PathVariable Long id) {
projectService.delete(id);
@ -98,7 +101,7 @@ public class ProjectController {
* 为项目关联业态能力包支持幂等重复调用
*/
@PostMapping("/{id}/init")
@PreAuthorize("hasAuthority('base:project:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "初始化项目能力包")
public Result<Void> initProject(@PathVariable Long id, @Valid @RequestBody ProjectInitRequest request) {
projectInitService.initProject(id, request.getPackageCodes());

View File

@ -31,7 +31,7 @@ public class PublicRevenueController {
* 分页查询公共收益
*/
@GetMapping
@PreAuthorize("hasAuthority('base:revenue:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<PublicRevenue>> page(
@RequestParam(required = false) String period,
@RequestParam(defaultValue = "1") int page,
@ -43,7 +43,7 @@ public class PublicRevenueController {
* 公共收益详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:revenue:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PublicRevenue> getById(@PathVariable Long id) {
return Result.success(publicRevenueService.getById(id));
}
@ -52,7 +52,7 @@ public class PublicRevenueController {
* 录入公共收益
*/
@PostMapping
@PreAuthorize("hasAuthority('base:revenue:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "录入公共收益")
public Result<Map<String, Long>> create(@Valid @RequestBody PublicRevenue revenue) {
Long id = publicRevenueService.create(revenue);
@ -65,7 +65,7 @@ public class PublicRevenueController {
* 更新公共收益
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:revenue:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新公共收益")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody PublicRevenue revenue) {
publicRevenueService.update(id, revenue);
@ -78,7 +78,7 @@ public class PublicRevenueController {
* @param period 周期 2025-06
*/
@GetMapping("/summary")
@PreAuthorize("hasAuthority('base:revenue:manage')")
@PreAuthorize("hasFunction('base')")
public Result<Map<String, Object>> monthlySummary(@RequestParam String period) {
Long total = publicRevenueService.monthlySummary(UserContext.getProjectId(), period);
Map<String, Object> data = new HashMap<>();
@ -91,7 +91,7 @@ public class PublicRevenueController {
* 删除公共收益
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:revenue:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除公共收益")
public Result<Void> delete(@PathVariable Long id) {
publicRevenueService.delete(id);

View File

@ -31,7 +31,7 @@ public class RenovationController {
* 分页查询装修记录
*/
@GetMapping
@PreAuthorize("hasAuthority('base:renovation:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<Renovation>> page(
@RequestParam(required = false) Integer status,
@RequestParam(defaultValue = "1") int page,
@ -43,7 +43,7 @@ public class RenovationController {
* 装修记录详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:renovation:manage')")
@PreAuthorize("hasFunction('base')")
public Result<Renovation> getById(@PathVariable Long id) {
return Result.success(renovationService.getById(id));
}
@ -52,7 +52,7 @@ public class RenovationController {
* 申请装修
*/
@PostMapping("/apply")
@PreAuthorize("hasAuthority('base:renovation:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "申请装修", recordParams = false)
public Result<Map<String, Long>> apply(@Valid @RequestBody Renovation renovation) {
Long id = renovationService.applyRenovation(renovation);
@ -70,7 +70,7 @@ public class RenovationController {
* @param comment 审批意见
*/
@PostMapping("/{id}/approve")
@PreAuthorize("hasAuthority('base:renovation:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "审批装修申请")
public Result<Void> approve(@PathVariable Long id,
@RequestParam boolean approved,
@ -84,7 +84,7 @@ public class RenovationController {
* 开始施工
*/
@PostMapping("/{id}/start")
@PreAuthorize("hasAuthority('base:renovation:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "开始施工")
public Result<Void> startConstruction(@PathVariable Long id) {
renovationService.startConstruction(id);
@ -95,7 +95,7 @@ public class RenovationController {
* 装修完成
*/
@PostMapping("/{id}/complete")
@PreAuthorize("hasAuthority('base:renovation:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "装修完成")
public Result<Void> complete(@PathVariable Long id) {
renovationService.complete(id);
@ -106,7 +106,7 @@ public class RenovationController {
* 删除装修记录
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:renovation:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除装修记录")
public Result<Void> delete(@PathVariable Long id) {
renovationService.delete(id);

View File

@ -25,6 +25,9 @@ import java.util.Map;
/**
* 房间控制器
* 路径前缀: /api/v1/base/rooms
* <p>
* PR2-PR4 改造U5.18 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:room:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -38,7 +41,7 @@ public class RoomController {
* 房间列表查询分页支持多条件筛选
*/
@GetMapping
@PreAuthorize("hasAuthority('base:room:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<RoomDTO>> list(RoomDTO query) {
return Result.success(roomService.page(query));
}
@ -47,7 +50,7 @@ public class RoomController {
* 创建房间
*/
@PostMapping
@PreAuthorize("hasAuthority('base:room:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建房间")
public Result<Map<String, Long>> create(@Valid @RequestBody RoomSaveRequest request) {
Long id = roomService.create(request);
@ -60,7 +63,7 @@ public class RoomController {
* 批量创建房间
*/
@PostMapping("/batch")
@PreAuthorize("hasAuthority('base:room:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "批量创建房间")
public Result<Map<String, Object>> batchCreate(@Valid @RequestBody List<RoomSaveRequest> rooms) {
List<Long> ids = roomService.batchCreate(rooms);
@ -74,7 +77,7 @@ public class RoomController {
* 房间详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:room:manage')")
@PreAuthorize("hasFunction('base')")
public Result<RoomDTO> getById(@PathVariable Long id) {
return Result.success(roomService.getById(id));
}
@ -83,7 +86,7 @@ public class RoomController {
* 更新房间
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:room:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新房间")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody RoomSaveRequest request) {
roomService.update(id, request);
@ -94,7 +97,7 @@ public class RoomController {
* 删除房间
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:room:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除房间")
public Result<Void> delete(@PathVariable Long id) {
roomService.delete(id);
@ -105,7 +108,7 @@ public class RoomController {
* 房间树项目楼栋楼层房间
*/
@GetMapping("/tree")
@PreAuthorize("hasAuthority('base:room:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<BuildingTreeDTO>> tree() {
return Result.success(roomService.tree(UserContext.getProjectId()));
}
@ -114,7 +117,7 @@ public class RoomController {
* 批量导入房间Excel上传
*/
@PostMapping("/import")
@PreAuthorize("hasAuthority('base:room:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "导入房间")
public Result<Map<String, Object>> importRooms(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {

View File

@ -31,7 +31,7 @@ public class SafetyInspectionController {
* 安全检查列表
*/
@GetMapping
@PreAuthorize("hasAuthority('base:inspection:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<SafetyInspection>> list() {
return Result.success(safetyInspectionService.list(UserContext.getProjectId()));
}
@ -40,7 +40,7 @@ public class SafetyInspectionController {
* 安全检查详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:inspection:manage')")
@PreAuthorize("hasFunction('base')")
public Result<SafetyInspection> getById(@PathVariable Long id) {
return Result.success(safetyInspectionService.getById(id));
}
@ -49,7 +49,7 @@ public class SafetyInspectionController {
* 创建安全检查计划
*/
@PostMapping
@PreAuthorize("hasAuthority('base:inspection:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建安全检查计划")
public Result<Map<String, Long>> create(@Valid @RequestBody SafetyInspection entity) {
Long id = safetyInspectionService.create(entity);
@ -62,7 +62,7 @@ public class SafetyInspectionController {
* 更新安全检查
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:inspection:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新安全检查")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody SafetyInspection entity) {
safetyInspectionService.update(id, entity);
@ -73,7 +73,7 @@ public class SafetyInspectionController {
* 删除安全检查
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:inspection:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除安全检查")
public Result<Void> delete(@PathVariable Long id) {
safetyInspectionService.delete(id);
@ -84,7 +84,7 @@ public class SafetyInspectionController {
* 执行安全检查计划中 -> 执行中
*/
@PostMapping("/{id}/execute")
@PreAuthorize("hasAuthority('base:inspection:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "执行安全检查")
public Result<Void> execute(@PathVariable Long id) {
safetyInspectionService.execute(id);
@ -95,7 +95,7 @@ public class SafetyInspectionController {
* 上报发现执行中 -> 已上报闭环
*/
@PostMapping("/{id}/report")
@PreAuthorize("hasAuthority('base:inspection:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "上报安全检查发现")
public Result<Void> reportFindings(@PathVariable Long id,
@RequestParam String result,

View File

@ -18,6 +18,9 @@ import java.util.Map;
/**
* 租户控制器
* 路径前缀: /api/v1/base/tenants
* <p>
* PR2-PR4 改造U5.14 方法统一标注 `hasFunction('base')` 控制模块入口
* Service 层写操作通过 `hasAuthorityAndAdmin('base:tenant:manage')` 控制操作粒度
*/
@Slf4j
@RestController
@ -31,7 +34,7 @@ public class TenantController {
* 租户列表查询分页
*/
@GetMapping
@PreAuthorize("hasAuthority('base:tenant:manage')")
@PreAuthorize("hasFunction('base')")
public Result<PageResult<TenantDTO>> list(TenantDTO query) {
return Result.success(tenantService.page(query));
}
@ -40,7 +43,7 @@ public class TenantController {
* 创建租户
*/
@PostMapping
@PreAuthorize("hasAuthority('base:tenant:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建租户", recordParams = false)
public Result<Map<String, Long>> create(@Valid @RequestBody TenantSaveRequest request) {
Long id = tenantService.create(request);
@ -53,7 +56,7 @@ public class TenantController {
* 更新租户
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:tenant:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新租户", recordParams = false)
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody TenantSaveRequest request) {
tenantService.update(id, request);
@ -64,7 +67,7 @@ public class TenantController {
* 删除租户
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:tenant:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除租户")
public Result<Void> delete(@PathVariable Long id) {
tenantService.delete(id);

View File

@ -31,7 +31,7 @@ public class WorkshopLeaseController {
* 厂房租赁列表
*/
@GetMapping
@PreAuthorize("hasAuthority('base:workshop:manage')")
@PreAuthorize("hasFunction('base')")
public Result<List<WorkshopLease>> list() {
return Result.success(workshopLeaseService.list(UserContext.getProjectId()));
}
@ -40,7 +40,7 @@ public class WorkshopLeaseController {
* 厂房租赁详情
*/
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('base:workshop:manage')")
@PreAuthorize("hasFunction('base')")
public Result<WorkshopLease> getById(@PathVariable Long id) {
return Result.success(workshopLeaseService.getById(id));
}
@ -49,7 +49,7 @@ public class WorkshopLeaseController {
* 创建厂房租赁
*/
@PostMapping
@PreAuthorize("hasAuthority('base:workshop:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "创建厂房租赁", recordParams = false)
public Result<Map<String, Long>> create(@Valid @RequestBody WorkshopLease entity) {
Long id = workshopLeaseService.create(entity);
@ -62,7 +62,7 @@ public class WorkshopLeaseController {
* 更新厂房租赁
*/
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('base:workshop:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "UPDATE", description = "更新厂房租赁", recordParams = false)
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody WorkshopLease entity) {
workshopLeaseService.update(id, entity);
@ -73,7 +73,7 @@ public class WorkshopLeaseController {
* 删除厂房租赁
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('base:workshop:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "DELETE", description = "删除厂房租赁")
public Result<Void> delete(@PathVariable Long id) {
workshopLeaseService.delete(id);
@ -84,7 +84,7 @@ public class WorkshopLeaseController {
* 生成租金账单
*/
@PostMapping("/{id}/rent-bill")
@PreAuthorize("hasAuthority('base:workshop:manage')")
@PreAuthorize("hasFunction('base')")
@AuditLog(module = "base", type = "CREATE", description = "生成租金账单")
public Result<Map<String, String>> generateRentBill(@PathVariable Long id) {
String billNo = workshopLeaseService.generateRentBill(id);

View File

@ -17,6 +17,7 @@ import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -85,6 +86,7 @@ public class AbilityPackageServiceImpl implements AbilityPackageService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void assignPackages(Long projectId, List<String> packageCodes) {
if (projectId == null) {

View File

@ -17,6 +17,7 @@ import com.pms.common.security.UserContext;
import com.pms.common.util.JsonUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -39,6 +40,7 @@ public class AbilityPackageVersionServiceImpl implements AbilityPackageVersionSe
private final ProjectAbilityMapper projectAbilityMapper;
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public AbilityPackageVersion createDraft(Long packageId) {
AbilityPackage pkg = abilityPackageMapper.selectById(packageId);
@ -78,6 +80,7 @@ public class AbilityPackageVersionServiceImpl implements AbilityPackageVersionSe
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void submitForApproval(Long versionId) {
AbilityPackageVersion version = loadVersion(versionId);
@ -96,6 +99,7 @@ public class AbilityPackageVersionServiceImpl implements AbilityPackageVersionSe
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void approve(Long versionId, String comment) {
AbilityPackageVersion version = loadVersion(versionId);
@ -135,6 +139,7 @@ public class AbilityPackageVersionServiceImpl implements AbilityPackageVersionSe
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void reject(Long versionId, String reason) {
AbilityPackageVersion version = loadVersion(versionId);
@ -156,6 +161,7 @@ public class AbilityPackageVersionServiceImpl implements AbilityPackageVersionSe
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long versionId) {
AbilityPackageVersion version = loadVersion(versionId);

View File

@ -15,6 +15,7 @@ import com.pms.common.security.UserContext;
import com.pms.common.util.JsonUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -68,6 +69,7 @@ public class ApprovalFlowConfigServiceImpl implements ApprovalFlowConfigService
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public Long saveTemplate(ApprovalFlowTemplateSaveRequest request) {
// 查找现有模板 stage_transition

View File

@ -22,6 +22,7 @@ import com.pms.common.util.JsonUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -62,6 +63,7 @@ public class ApprovalFlowRuntimeServiceImpl implements ApprovalFlowRuntimeServic
private static final String ACTION_REJECT_PREV = "REJECT_PREV";
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public Long createInstance(Long projectId, String stageTransition) {
ApprovalFlowTemplateDTO template = configService.getTemplateByTransition(stageTransition);
@ -101,6 +103,7 @@ public class ApprovalFlowRuntimeServiceImpl implements ApprovalFlowRuntimeServic
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public ApprovalCompletedEvent approve(Long instanceId, Long userId, String comment) {
ApprovalFlowInstance instance = loadInstance(instanceId);
@ -166,6 +169,7 @@ public class ApprovalFlowRuntimeServiceImpl implements ApprovalFlowRuntimeServic
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public void rejectToInitiator(Long instanceId, Long userId, String comment) {
ApprovalFlowInstance instance = loadInstance(instanceId);
@ -182,6 +186,7 @@ public class ApprovalFlowRuntimeServiceImpl implements ApprovalFlowRuntimeServic
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public void rejectToPrevious(Long instanceId, Long userId, String comment) {
ApprovalFlowInstance instance = loadInstance(instanceId);

View File

@ -18,6 +18,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -41,6 +42,7 @@ public class ArchiveServiceImpl implements ArchiveService {
private LifecycleService lifecycleService;
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public Long archive(Long projectId, AssetHandoverRequest handoverRequest) {
// 1. 结算校验
@ -81,6 +83,7 @@ public class ArchiveServiceImpl implements ArchiveService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public void markArchived(Long projectId, Long operatorId) {
long now = System.currentTimeMillis();
@ -95,6 +98,7 @@ public class ArchiveServiceImpl implements ArchiveService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public Long recoverArchive(Long projectId, String targetStage) {
Long instanceId = lifecycleService.recoverArchive(projectId, targetStage);

View File

@ -24,6 +24,7 @@ import com.pms.common.response.PageResult;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -68,6 +69,7 @@ public class BuildingServiceImpl implements BuildingService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:building:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(BuildingSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -113,6 +115,7 @@ public class BuildingServiceImpl implements BuildingService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:building:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, BuildingSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -146,6 +149,7 @@ public class BuildingServiceImpl implements BuildingService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:building:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());

View File

@ -11,6 +11,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -39,6 +40,7 @@ public class CamChargeServiceImpl implements CamChargeService {
private static final int SCALE = 4;
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:finance:manage')")
@Transactional(rollbackFor = Exception.class)
public List<CamCharge> allocateCamCharge(Long projectId, Long totalAmountFen, String chargePeriod) {
if (projectId == null) {

View File

@ -16,6 +16,7 @@ import com.pms.common.security.ProjectSecurityChecker;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -65,6 +66,7 @@ public class CommunityActivityServiceImpl implements CommunityActivityService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:activity:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(CommunityActivity activity) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -88,6 +90,7 @@ public class CommunityActivityServiceImpl implements CommunityActivityService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:activity:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, CommunityActivity activity) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -118,6 +121,7 @@ public class CommunityActivityServiceImpl implements CommunityActivityService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:activity:manage')")
@Transactional(rollbackFor = Exception.class)
public void signUp(Long id) {
CommunityActivity existing = communityActivityMapper.selectById(id);
@ -143,6 +147,7 @@ public class CommunityActivityServiceImpl implements CommunityActivityService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:activity:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());

View File

@ -8,6 +8,7 @@ import com.pms.common.constant.CommonConstants;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -31,6 +32,7 @@ public class ContractRoomServiceImpl implements ContractRoomService {
private final ContractRoomMapper contractRoomMapper;
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:contract:manage')")
@Transactional(rollbackFor = Exception.class)
public void bindRooms(Long contractId, Long projectId, List<Long> roomIds) {
if (roomIds == null) {

View File

@ -19,6 +19,7 @@ import com.pms.common.response.PageResult;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -62,6 +63,7 @@ public class ContractServiceImpl implements ContractService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:contract:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(ContractSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -108,6 +110,7 @@ public class ContractServiceImpl implements ContractService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:contract:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, ContractSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -146,6 +149,7 @@ public class ContractServiceImpl implements ContractService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:contract:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -163,6 +167,7 @@ public class ContractServiceImpl implements ContractService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:contract:manage')")
@Transactional(rollbackFor = Exception.class)
public Long renew(Long id, ContractRenewRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());

View File

@ -12,6 +12,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -33,6 +34,7 @@ public class ContractTypeServiceImpl implements ContractTypeService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:contract:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(ContractTypeSaveRequest request) {
// 检查编码唯一性
@ -61,6 +63,7 @@ public class ContractTypeServiceImpl implements ContractTypeService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:contract:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, ContractTypeSaveRequest request) {
ContractType existing = contractTypeMapper.selectById(id);
@ -86,6 +89,7 @@ public class ContractTypeServiceImpl implements ContractTypeService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:contract:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
ContractType existing = contractTypeMapper.selectById(id);

View File

@ -10,6 +10,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -32,6 +33,7 @@ public class DataScopeRuleServiceImpl implements DataScopeRuleService {
private final DataScopeRuleMapper dataScopeRuleMapper;
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:data-scope:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(DataScopeRuleSaveRequest request) {
validateScopeType(request);
@ -56,6 +58,7 @@ public class DataScopeRuleServiceImpl implements DataScopeRuleService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:data-scope:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, DataScopeRuleSaveRequest request) {
DataScopeRule existing = dataScopeRuleMapper.selectById(id);
@ -79,6 +82,7 @@ public class DataScopeRuleServiceImpl implements DataScopeRuleService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('system:data-scope:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
DataScopeRule existing = dataScopeRuleMapper.selectById(id);

View File

@ -11,6 +11,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -45,6 +46,7 @@ public class DeviceCategoryServiceImpl implements DeviceCategoryService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:device:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(DeviceCategorySaveRequest request) {
DeviceCategory category = new DeviceCategory();
@ -67,6 +69,7 @@ public class DeviceCategoryServiceImpl implements DeviceCategoryService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:device:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, DeviceCategorySaveRequest request) {
DeviceCategory existing = deviceCategoryMapper.selectById(id);
@ -95,6 +98,7 @@ public class DeviceCategoryServiceImpl implements DeviceCategoryService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:device:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
DeviceCategory existing = deviceCategoryMapper.selectById(id);

View File

@ -17,6 +17,7 @@ import com.pms.common.response.PageResult;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -85,6 +86,7 @@ public class DeviceMaintenanceServiceImpl implements DeviceMaintenanceService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:device:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(Long deviceId, DeviceMaintenanceSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());

View File

@ -16,6 +16,7 @@ import com.pms.common.response.PageResult;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -54,6 +55,7 @@ public class DeviceServiceImpl implements DeviceService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:device:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(DeviceSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -100,6 +102,7 @@ public class DeviceServiceImpl implements DeviceService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:device:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, DeviceSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -138,6 +141,7 @@ public class DeviceServiceImpl implements DeviceService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:device:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -150,6 +154,7 @@ public class DeviceServiceImpl implements DeviceService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:device:manage')")
@Transactional(rollbackFor = Exception.class)
public void updateStatus(Long id, Integer status) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());

View File

@ -12,6 +12,7 @@ import com.pms.common.security.ProjectSecurityChecker;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -49,6 +50,7 @@ public class EnergyMeterServiceImpl implements EnergyMeterService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:energy:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(EnergyMeter entity) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -73,6 +75,7 @@ public class EnergyMeterServiceImpl implements EnergyMeterService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:energy:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, EnergyMeter entity) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -100,6 +103,7 @@ public class EnergyMeterServiceImpl implements EnergyMeterService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:energy:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -113,6 +117,7 @@ public class EnergyMeterServiceImpl implements EnergyMeterService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:energy:manage')")
@Transactional(rollbackFor = Exception.class)
public void recordReading(Long meterId, BigDecimal newReading) {
EnergyMeter meter = getById(meterId);

View File

@ -12,6 +12,7 @@ import com.pms.common.security.ProjectSecurityChecker;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -47,6 +48,7 @@ public class EnterpriseProfileServiceImpl implements EnterpriseProfileService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:enterprise:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(EnterpriseProfile entity) {
entity.setProjectId(UserContext.getProjectId());
@ -64,6 +66,7 @@ public class EnterpriseProfileServiceImpl implements EnterpriseProfileService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:enterprise:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, EnterpriseProfile entity) {
EnterpriseProfile existing = enterpriseProfileMapper.selectById(id);
@ -91,6 +94,7 @@ public class EnterpriseProfileServiceImpl implements EnterpriseProfileService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:enterprise:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
EnterpriseProfile existing = enterpriseProfileMapper.selectById(id);

View File

@ -19,6 +19,7 @@ import com.pms.common.security.UserContext;
import com.pms.common.util.CryptoUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -98,6 +99,7 @@ public class EnterpriseServiceImpl implements EnterpriseService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:enterprise:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(EnterpriseSaveRequest request) {
Long projectId = UserContext.getProjectId();
@ -140,6 +142,7 @@ public class EnterpriseServiceImpl implements EnterpriseService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:enterprise:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, EnterpriseSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -182,6 +185,7 @@ public class EnterpriseServiceImpl implements EnterpriseService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:enterprise:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());

View File

@ -10,6 +10,7 @@ import com.pms.common.security.ProjectSecurityChecker;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -50,6 +51,7 @@ public class EnterpriseServiceServiceImpl implements EnterpriseServiceService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:enterprise:manage')")
@Transactional(rollbackFor = Exception.class)
public Long apply(EnterpriseService service) {
if (service.getStatus() == null) {
@ -67,6 +69,7 @@ public class EnterpriseServiceServiceImpl implements EnterpriseServiceService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:enterprise:manage')")
@Transactional(rollbackFor = Exception.class)
public void process(Long id, String handler, String result) {
EnterpriseService existing = enterpriseServiceMapper.selectById(id);

View File

@ -16,6 +16,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -39,6 +40,7 @@ public class FloorServiceImpl implements FloorService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:floor:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(FloorSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -75,6 +77,7 @@ public class FloorServiceImpl implements FloorService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:floor:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, FloorSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -108,6 +111,7 @@ public class FloorServiceImpl implements FloorService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:floor:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());

View File

@ -11,6 +11,7 @@ import com.pms.common.security.ProjectSecurityChecker;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -62,6 +63,7 @@ public class LeaseContractServiceImpl implements LeaseContractService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lease:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(LeaseContract contract) {
lifecycleWriteGuard.assertWritable(contract.getProjectId());
@ -88,6 +90,7 @@ public class LeaseContractServiceImpl implements LeaseContractService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lease:manage')")
@Transactional(rollbackFor = Exception.class)
public void renew(Long id, LocalDate newEndDate) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -113,6 +116,7 @@ public class LeaseContractServiceImpl implements LeaseContractService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lease:manage')")
@Transactional(rollbackFor = Exception.class)
public void terminate(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());

View File

@ -21,6 +21,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -90,6 +91,7 @@ public class LifecycleServiceImpl implements LifecycleService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public Long transition(Long projectId, StageTransitionRequest request) {
Project project = projectMapper.selectById(projectId);
@ -120,6 +122,7 @@ public class LifecycleServiceImpl implements LifecycleService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public ApprovalCompletedEvent approve(Long projectId, Long instanceId, String comment) {
assertInstanceBelongsToProject(projectId, instanceId);
@ -138,6 +141,7 @@ public class LifecycleServiceImpl implements LifecycleService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public void reject(Long projectId, Long instanceId, String action, String comment) {
assertInstanceBelongsToProject(projectId, instanceId);
@ -150,6 +154,7 @@ public class LifecycleServiceImpl implements LifecycleService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public void advanceStage(Long projectId, String targetStage, Long operatorId, String transitionType, Long instanceId) {
// SELECT FOR UPDATE 锁定 Project 防止 guard 读与阶段变更的 TOCTOU 竞态
@ -217,6 +222,7 @@ public class LifecycleServiceImpl implements LifecycleService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lifecycle:manage')")
@Transactional(rollbackFor = Exception.class)
public Long recoverArchive(Long projectId, String targetStage) {
Project project = projectMapper.selectById(projectId);

View File

@ -14,6 +14,7 @@ import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -76,6 +77,7 @@ public class LookupServiceImpl implements LookupService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lookup:manage')")
@Transactional(rollbackFor = Exception.class)
public Long addProjectItem(Long projectId, String dictCode, String itemCode, String itemName, Integer sort) {
if (projectId == null) {
@ -108,6 +110,7 @@ public class LookupServiceImpl implements LookupService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:lookup:manage')")
@Transactional(rollbackFor = Exception.class)
public void disableProjectItem(Long projectId, String dictCode, String itemCode) {
if (projectId == null) {

View File

@ -11,6 +11,7 @@ import com.pms.common.security.ProjectSecurityChecker;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -52,6 +53,7 @@ public class MeetingRoomServiceImpl implements MeetingRoomService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:meeting:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(MeetingRoom room) {
lifecycleWriteGuard.assertWritable(room.getProjectId());
@ -78,6 +80,7 @@ public class MeetingRoomServiceImpl implements MeetingRoomService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:meeting:manage')")
@Transactional(rollbackFor = Exception.class)
public void book(Long id) {
MeetingRoom existing = meetingRoomMapper.selectById(id);
@ -98,6 +101,7 @@ public class MeetingRoomServiceImpl implements MeetingRoomService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:meeting:manage')")
@Transactional(rollbackFor = Exception.class)
public void cancelBooking(Long id) {
MeetingRoom existing = meetingRoomMapper.selectById(id);

View File

@ -15,6 +15,7 @@ import com.pms.common.security.ProjectSecurityChecker;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -60,6 +61,7 @@ public class OwnerCommitteeServiceImpl implements OwnerCommitteeService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:committee:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(OwnerCommittee committee) {
committee.setProjectId(UserContext.getProjectId());
@ -86,6 +88,7 @@ public class OwnerCommitteeServiceImpl implements OwnerCommitteeService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:committee:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, OwnerCommittee committee) {
OwnerCommittee existing = ownerCommitteeMapper.selectById(id);
@ -114,6 +117,7 @@ public class OwnerCommitteeServiceImpl implements OwnerCommitteeService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:committee:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
OwnerCommittee existing = ownerCommitteeMapper.selectById(id);

View File

@ -17,6 +17,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -42,6 +43,7 @@ public class OwnerRoomServiceImpl implements OwnerRoomService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:owner:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(OwnerRoomRelRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -88,6 +90,7 @@ public class OwnerRoomServiceImpl implements OwnerRoomService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:owner:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());

View File

@ -19,6 +19,7 @@ import com.pms.common.response.PageResult;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -98,6 +99,7 @@ public class OwnerServiceImpl implements OwnerService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:owner:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(OwnerSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -162,6 +164,7 @@ public class OwnerServiceImpl implements OwnerService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:owner:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, OwnerSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -212,6 +215,7 @@ public class OwnerServiceImpl implements OwnerService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:owner:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());

View File

@ -12,6 +12,7 @@ import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -28,6 +29,7 @@ public class PreHandoverProgressServiceImpl implements PreHandoverProgressServic
private final PreHandoverProgressMapper preHandoverProgressMapper;
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(Long projectId, PreHandoverProgressDTO dto) {
validateProgressType(dto.getProgressType());
@ -56,6 +58,7 @@ public class PreHandoverProgressServiceImpl implements PreHandoverProgressServic
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long projectId, Long id, PreHandoverProgressDTO dto) {
validateProgressType(dto.getProgressType());
@ -85,6 +88,7 @@ public class PreHandoverProgressServiceImpl implements PreHandoverProgressServic
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long projectId, Long id) {
PreHandoverProgress existing = preHandoverProgressMapper.selectById(id);

View File

@ -13,6 +13,7 @@ import com.pms.common.exception.ErrorCode;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -41,6 +42,7 @@ public class ProjectAbilityServiceImpl implements ProjectAbilityService {
BuildingType.COMPLEX_VALUE, "MIXED_COMMERCIAL_OFFICE");
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void bindWithFallbackPrimary(Long projectId, List<String> abilityCodes) {
if (projectId == null) {
@ -88,6 +90,7 @@ public class ProjectAbilityServiceImpl implements ProjectAbilityService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public PrimaryRecomputeResultDTO recomputePrimary(Long projectId) {
PrimaryRecomputeResultDTO result = new PrimaryRecomputeResultDTO();
@ -188,6 +191,7 @@ public class ProjectAbilityServiceImpl implements ProjectAbilityService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void forceOverridePrimary(Long projectId, Long abilityId) {
// 1. 校验 ProjectAbility 记录存在且属于该项目

View File

@ -12,6 +12,7 @@ import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -28,6 +29,7 @@ public class ProjectAttributeServiceImpl implements ProjectAttributeService {
private final ProjectAttributeMapper projectAttributeMapper;
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(Long projectId, ProjectAttributeDTO dto) {
validateAttrType(dto.getAttrType());
@ -54,6 +56,7 @@ public class ProjectAttributeServiceImpl implements ProjectAttributeService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long projectId, Long attrId, ProjectAttributeDTO dto) {
validateAttrType(dto.getAttrType());
@ -81,6 +84,7 @@ public class ProjectAttributeServiceImpl implements ProjectAttributeService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long projectId, Long attrId) {
ProjectAttribute existing = projectAttributeMapper.selectById(attrId);

View File

@ -4,6 +4,7 @@ import com.pms.base.service.AbilityPackageService;
import com.pms.base.service.ProjectInitService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -21,6 +22,7 @@ public class ProjectInitServiceImpl implements ProjectInitService {
private final AbilityPackageService abilityPackageService;
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void initProject(Long projectId, List<String> packageCodes) {
log.info("开始初始化项目能力包projectId={}, packageCodes={}", projectId, packageCodes);

View File

@ -20,6 +20,7 @@ import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@ -59,6 +60,7 @@ public class ProjectServiceImpl implements ProjectService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(ProjectSaveRequest request) {
// 检查项目编码唯一性
@ -122,6 +124,7 @@ public class ProjectServiceImpl implements ProjectService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, ProjectSaveRequest request) {
Project existing = projectMapper.selectById(id);
@ -173,6 +176,7 @@ public class ProjectServiceImpl implements ProjectService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:project:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
Project existing = projectMapper.selectById(id);

View File

@ -15,6 +15,7 @@ import com.pms.common.security.ProjectSecurityChecker;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@ -64,6 +65,7 @@ public class PublicRevenueServiceImpl implements PublicRevenueService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:revenue:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(PublicRevenue revenue) {
revenue.setProjectId(UserContext.getProjectId());
@ -92,6 +94,7 @@ public class PublicRevenueServiceImpl implements PublicRevenueService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:revenue:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, PublicRevenue revenue) {
PublicRevenue existing = publicRevenueMapper.selectById(id);
@ -134,6 +137,7 @@ public class PublicRevenueServiceImpl implements PublicRevenueService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:revenue:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
PublicRevenue existing = publicRevenueMapper.selectById(id);

View File

@ -17,6 +17,7 @@ import com.pms.common.security.ProjectSecurityChecker;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@ -78,6 +79,7 @@ public class RenovationServiceImpl implements RenovationService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:renovation:manage')")
@Transactional(rollbackFor = Exception.class)
public Long applyRenovation(Renovation renovation) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -104,6 +106,7 @@ public class RenovationServiceImpl implements RenovationService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:renovation:manage')")
@Transactional(rollbackFor = Exception.class)
public void approve(Long id, boolean approved, String permitType, String comment) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -158,6 +161,7 @@ public class RenovationServiceImpl implements RenovationService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:renovation:manage')")
@Transactional(rollbackFor = Exception.class)
public void startConstruction(Long id) {
Renovation existing = renovationMapper.selectById(id);
@ -173,6 +177,7 @@ public class RenovationServiceImpl implements RenovationService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:renovation:manage')")
@Transactional(rollbackFor = Exception.class)
public void complete(Long id) {
Renovation existing = renovationMapper.selectById(id);
@ -188,6 +193,7 @@ public class RenovationServiceImpl implements RenovationService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:renovation:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());

View File

@ -20,6 +20,7 @@ import com.pms.common.response.PageResult;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -69,6 +70,7 @@ public class RoomServiceImpl implements RoomService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:room:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(RoomSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -118,6 +120,7 @@ public class RoomServiceImpl implements RoomService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:room:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, RoomSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -148,6 +151,7 @@ public class RoomServiceImpl implements RoomService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:room:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -160,6 +164,7 @@ public class RoomServiceImpl implements RoomService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:room:manage')")
@Transactional(rollbackFor = Exception.class)
public List<Long> batchCreate(List<RoomSaveRequest> rooms) {
List<Long> ids = new ArrayList<>();

View File

@ -11,6 +11,7 @@ import com.pms.common.security.ProjectSecurityChecker;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -54,6 +55,7 @@ public class SafetyInspectionServiceImpl implements SafetyInspectionService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:inspection:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(SafetyInspection entity) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -71,6 +73,7 @@ public class SafetyInspectionServiceImpl implements SafetyInspectionService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:inspection:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, SafetyInspection entity) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -98,6 +101,7 @@ public class SafetyInspectionServiceImpl implements SafetyInspectionService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:inspection:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -111,6 +115,7 @@ public class SafetyInspectionServiceImpl implements SafetyInspectionService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:inspection:manage')")
@Transactional(rollbackFor = Exception.class)
public void execute(Long id) {
SafetyInspection existing = safetyInspectionMapper.selectById(id);
@ -131,6 +136,7 @@ public class SafetyInspectionServiceImpl implements SafetyInspectionService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:inspection:manage')")
@Transactional(rollbackFor = Exception.class)
public void reportFindings(Long id, String result, String finding) {
SafetyInspection existing = safetyInspectionMapper.selectById(id);

View File

@ -16,6 +16,7 @@ import com.pms.common.response.PageResult;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -45,6 +46,7 @@ public class TenantServiceImpl implements TenantService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:tenant:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(TenantSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -88,6 +90,7 @@ public class TenantServiceImpl implements TenantService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:tenant:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, TenantSaveRequest request) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -130,6 +133,7 @@ public class TenantServiceImpl implements TenantService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:tenant:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());

View File

@ -12,6 +12,7 @@ import com.pms.common.security.ProjectSecurityChecker;
import com.pms.common.security.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -47,6 +48,7 @@ public class WorkshopLeaseServiceImpl implements WorkshopLeaseService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:workshop:manage')")
@Transactional(rollbackFor = Exception.class)
public Long create(WorkshopLease entity) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -64,6 +66,7 @@ public class WorkshopLeaseServiceImpl implements WorkshopLeaseService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:workshop:manage')")
@Transactional(rollbackFor = Exception.class)
public void update(Long id, WorkshopLease entity) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -92,6 +95,7 @@ public class WorkshopLeaseServiceImpl implements WorkshopLeaseService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:workshop:manage')")
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
lifecycleWriteGuard.assertWritable(UserContext.getProjectId());
@ -105,6 +109,7 @@ public class WorkshopLeaseServiceImpl implements WorkshopLeaseService {
}
@Override
@PreAuthorize("hasAuthorityAndAdmin('base:workshop:manage')")
@Transactional(rollbackFor = Exception.class)
public String generateRentBill(Long leaseId) {
WorkshopLease lease = getById(leaseId);

Some files were not shown because too many files have changed in this diff Show More