diff --git a/AGENTS.md b/AGENTS.md index ac5ad24c0..a23b2ecef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,15 +18,15 @@ | `common-audit/` | 审计注解、切面、脱敏与日志/HTTP 发送 | `BusinessAuditAspect.java`、`SensitiveDataSanitizer.java`、`HttpAuditSink.java` | | `common-event-contract/` | 跨区域事件信封、类型、标识、哈希与校验 | `EventEnvelope.java`、`EventTypes.java`、`EventEnvelopeValidator.java` | | `gateway/` | 路由、CORS、入口安全、访问域隔离 | `config/`、`filter/`、`src/main/resources/application*.yml` | -| `auth/` | 登录、令牌、注册、账号与密码 | `controller/`、`service/`、`src/main/resources/db/` | -| `iam/` | 租户、成员、企业资料、RBAC 与授权快照 | `controller/`、`service/`、`README-IAM.md` | +| `auth/` | 登录、令牌、注册、账号与密码 | `InternalAccountController.java`、`AccountManagementService.java`、`src/main/resources/db/` | +| `iam/` | 租户、商户成员、平台内部用户、企业资料、RBAC 与授权快照 | `ProfileResourceController.java`、`PlatformInternalUserService.java`、`RbacController.java` | | `order/` | 订单主数据、状态编排、快照与 Outbox | `controller/`、`OrderApplicationService.java`、`README-ORDER.md` | | `firstleg/` | 履约任务、操作分配与分配历史 | `PlatformFulfillmentAssignmentController.java`、`FulfillmentTaskService.java` | | `warehouse/` | 仓库、地址、服务能力与往来单位 | `controller/`、`WarehouseApplicationService.java`、`PartnerQueryService.java` | | `file/` | 企业资质文件本地存储、预览与鉴权 | `controller/`、`LocalQualificationStorage.java`;元数据表结构尚未接入 | | `print/` | 打印中心边界 | 当前为应用、健康检查和 `print/src/main/resources/db/print.sql` 骨架 | | `audit-service/` | 审计事件接收与持久化 | `AuditIngestController.java`、`AuditIngestService.java`;当前接收接口未实现内部令牌保护 | -| `integration-service/` | OCR、Amazon 等第三方适配 | `provider/`、`IntegrationProperties.java` | +| `integration-service/` | 重量 OCR、百度 OCR、Amazon 等第三方适配 | `controller/WeightOcrController.java`、`service/ocr/`、`provider/ocr/`、`IntegrationProperties.java` | | `event-bridge-service/` | 跨区域事件接收、校验与 Inbox 幂等 | `EventBridgeController.java`、`EventInboxService.java` | ## 配置、数据库与验证入口 diff --git a/Jenkinsfile b/Jenkinsfile index 69d19047a..bd4d1c55b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -66,6 +66,7 @@ pipeline { booleanParam(name: 'SERVICE_AUDIT', defaultValue: false, description: '审计服务(audit-service)') choice(name: 'ENVIRONMENT', choices: ['test', 'staging', 'production'], description: '部署环境') booleanParam(name: 'DEPLOY', defaultValue: false, description: '构建成功后是否部署') + booleanParam(name: 'INITIALIZE_TEST_DATABASE', defaultValue: false, description: '仅 test:幂等初始化 IAM、订单、仓库与往来单位表结构及仓库样例数据') booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: '是否跳过测试;生产构建不建议勾选') string(name: 'IMAGE_REPOSITORY', defaultValue: '8.163.30.128:10082/java-project/baiyue-firstleg', description: '预发布/生产镜像仓库地址;test 环境自动使用本机镜像,无需修改') string(name: 'DOCKER_BASE_IMAGE', defaultValue: 'docker.m.daocloud.io/library/eclipse-temurin:17-jre-alpine', description: 'Java 17 基础镜像;默认使用国内镜像代理') @@ -194,6 +195,17 @@ pipeline { } } + stage('测试数据库初始化(可选)') { + when { + allOf { + expression { params.ENVIRONMENT == 'test' && params.INITIALIZE_TEST_DATABASE } + } + } + steps { + sh 'sh deploy/scripts/initialize-test-databases.sh' + } + } + stage('部署') { when { allOf { diff --git a/auth/AGENTS.md b/auth/AGENTS.md index be1cae42c..f550774e8 100644 --- a/auth/AGENTS.md +++ b/auth/AGENTS.md @@ -13,7 +13,7 @@ | 登录、注册、刷新、退出、改密 API | `controller/AuthController.java`(`/api/auth`) | | 内部账号管理 API | `controller/InternalAccountController.java`(`/internal/auth/accounts`) | | 认证与令牌编排 | `service/AuthService.java`、`TokenSessionStore.java` | -| 账号状态、密码重置 | `service/AccountManagementService.java` | +| 账号创建、显示名同步、状态和密码重置 | `service/AccountManagementService.java`;平台账号使用 `default_app_code=PLATFORM`、`tenant_id=0` | | 商户注册协作 | `service/MerchantRegistrationService.java` | | IAM 调用与授权快照 | `client/IamAuthorizationClient.java`、`IamOnboardingClient.java`、`AuthorizationSnapshot.java` | | 账号与 Refresh Token 持久化 | `entity/`、`mapper/`、`repository/` | @@ -26,4 +26,5 @@ - 密码必须安全散列;不得记录密码、Token、密钥或完整敏感 Claim。 - 修改 Token Claim、会话 Redis Key、过期或撤销逻辑时同步检查 `common-security/`、`gateway/` 和 IAM 授权快照。 - 注册流程跨 Auth/IAM,失败补偿和幂等必须成对检查;不要跨库写 IAM 表。 +- 内部账号创建允许相同账号 ID 与相同归属幂等重试;不同身份占用同一 ID 必须冲突。内部接口只接收 IAM 的服务令牌,不直接暴露给前端。 - 从父目录运行 `mvn -pl auth -am test`,认证接口变更再运行相关 `scripts/` 回归。 diff --git a/auth/src/main/java/com/baiyue/auth/controller/InternalAccountController.java b/auth/src/main/java/com/baiyue/auth/controller/InternalAccountController.java index 01c56a0ec..7b714c93e 100644 --- a/auth/src/main/java/com/baiyue/auth/controller/InternalAccountController.java +++ b/auth/src/main/java/com/baiyue/auth/controller/InternalAccountController.java @@ -6,6 +6,7 @@ import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; @@ -17,13 +18,14 @@ import java.security.MessageDigest; @RestController @RequestMapping("/internal/auth/accounts") public class InternalAccountController { - private final AccountManagementService service; + @Resource + private AccountManagementService service; + private final String configuredToken; private byte[] tokenBytes; - public InternalAccountController(AccountManagementService service, + public InternalAccountController( @Value("${baiyue.security.internal-service-token}") String configuredToken) { - this.service = service; this.configuredToken = configuredToken; } @@ -44,6 +46,11 @@ public class InternalAccountController { @PathVariable Long id, @Valid @RequestBody UpdateAccountStatusRequest request) { verify(token); service.updateStatus(id, request); return ApiResponse.success(null); } + @PutMapping("/{id}/profile") + public ApiResponse profile(@RequestHeader(value="X-Internal-Service-Token", required=false) String token, + @PathVariable Long id, @Valid @RequestBody UpdateAccountProfileRequest request) { + verify(token); service.updateProfile(id, request); return ApiResponse.success(null); + } @PutMapping("/{id}/password") public ApiResponse reset(@RequestHeader(value="X-Internal-Service-Token", required=false) String token, @PathVariable Long id, @Valid @RequestBody ResetPasswordRequest request) { diff --git a/auth/src/main/java/com/baiyue/auth/dto/AccountManagementDtos.java b/auth/src/main/java/com/baiyue/auth/dto/AccountManagementDtos.java index bd6763fe5..407082eb4 100644 --- a/auth/src/main/java/com/baiyue/auth/dto/AccountManagementDtos.java +++ b/auth/src/main/java/com/baiyue/auth/dto/AccountManagementDtos.java @@ -17,7 +17,10 @@ public final class AccountManagementDtos { @NotBlank @Size(min = 8, max = 64) @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password, @NotBlank String displayName, @NotNull Long tenantId, - @NotNull Long membershipId) { } + @NotNull Long membershipId, + @Pattern(regexp = "MERCHANT|PLATFORM", message = "应用端只能是 MERCHANT 或 PLATFORM") + String appCode) { } + public record UpdateAccountProfileRequest(@NotBlank @Size(max = 64) String displayName) { } public record UpdateAccountStatusRequest(@NotBlank String status) { } public record ResetPasswordRequest(@NotBlank @Size(min = 8, max = 64) @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String newPassword) { } diff --git a/auth/src/main/java/com/baiyue/auth/service/AccountManagementService.java b/auth/src/main/java/com/baiyue/auth/service/AccountManagementService.java index 8b0426157..82ea35053 100644 --- a/auth/src/main/java/com/baiyue/auth/service/AccountManagementService.java +++ b/auth/src/main/java/com/baiyue/auth/service/AccountManagementService.java @@ -11,6 +11,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.baiyue.common.audit.BusinessAudit; +import java.util.Locale; +import java.util.Objects; + /** 账号生命周期管理,所有密码只以 BCrypt 摘要写入数据库。 */ @Service public class AccountManagementService { @@ -35,7 +38,15 @@ public class AccountManagementService { @BusinessAudit(event = "ACCOUNT_CREATE", resourceType = "ACCOUNT", resourceId = "#request.accountId", description = "创建认证账号") public AccountSummary create(CreateAccountRequest request) { - if (repository.existsByUsername(request.username()) || repository.findById(request.accountId()) != null) { + String appCode = request.appCode() == null ? "MERCHANT" + : request.appCode().trim().toUpperCase(Locale.ROOT); + validateScope(appCode, request.tenantId()); + AuthAccount existing = repository.findById(request.accountId()); + if (existing != null) { + if (sameIdentity(existing, request, appCode)) return summary(existing); + throw new BizException(ResultCode.CONFLICT, "认证账号标识已被其他成员使用"); + } + if (repository.existsByUsername(request.username())) { throw new BizException(ResultCode.CONFLICT, "登录账号已存在"); } AuthAccount account = new AuthAccount(); @@ -49,7 +60,7 @@ public class AccountManagementService { account.setDefaultTenantId(request.tenantId()); account.setOperatorTenantId(request.tenantId()); account.setDefaultMembershipId(request.membershipId()); - account.setDefaultAppCode("MERCHANT"); + account.setDefaultAppCode(appCode); account.setDeleted(0); repository.insert(account); return summary(account); @@ -66,6 +77,16 @@ public class AccountManagementService { bumpVersion(account); } + @Transactional + @BusinessAudit(event = "ACCOUNT_PROFILE_CHANGE", resourceType = "ACCOUNT", resourceId = "#accountId", + description = "同步账号显示名称", captureAfter = false) + public void updateProfile(Long accountId, UpdateAccountProfileRequest request) { + AuthAccount account = required(accountId); + if (Objects.equals(account.getDisplayName(), request.displayName())) return; + account.setDisplayName(request.displayName()); + bumpVersion(account); + } + @Transactional @BusinessAudit(event = "PASSWORD_RESET", resourceType = "ACCOUNT", resourceId = "#accountId", description = "管理员重置密码", captureAfter = false) public void resetPassword(Long accountId, ResetPasswordRequest request) { @@ -108,4 +129,23 @@ public class AccountManagementService { private AccountSummary summary(AuthAccount a) { return new AccountSummary(a.getId(), a.getUsername(), a.getDisplayName(), a.getAccountStatus()); } + + private void validateScope(String appCode, Long tenantId) { + if (!"MERCHANT".equals(appCode) && !"PLATFORM".equals(appCode)) { + throw new BizException(ResultCode.BAD_REQUEST, "应用端只能是 MERCHANT 或 PLATFORM"); + } + if (("PLATFORM".equals(appCode) && tenantId != 0L) + || ("MERCHANT".equals(appCode) && tenantId <= 0L)) { + throw new BizException(ResultCode.BAD_REQUEST, "账号应用端与租户范围不匹配"); + } + } + + private boolean sameIdentity(AuthAccount account, CreateAccountRequest request, String appCode) { + return Objects.equals(account.getUsername(), request.username()) + && Objects.equals(account.getDefaultTenantId(), request.tenantId()) + && Objects.equals(account.getOperatorTenantId(), request.tenantId()) + && Objects.equals(account.getDefaultMembershipId(), request.membershipId()) + && Objects.equals(account.getDefaultAppCode(), appCode) + && !Integer.valueOf(1).equals(account.getDeleted()); + } } diff --git a/auth/src/test/java/com/baiyue/auth/service/AccountManagementServiceTest.java b/auth/src/test/java/com/baiyue/auth/service/AccountManagementServiceTest.java index bce35b8b3..1180692cb 100644 --- a/auth/src/test/java/com/baiyue/auth/service/AccountManagementServiceTest.java +++ b/auth/src/test/java/com/baiyue/auth/service/AccountManagementServiceTest.java @@ -1,6 +1,8 @@ package com.baiyue.auth.service; import com.baiyue.auth.dto.AccountManagementDtos.ChangePasswordRequest; +import com.baiyue.auth.dto.AccountManagementDtos.CreateAccountRequest; +import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountProfileRequest; import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountStatusRequest; import com.baiyue.auth.entity.AuthAccount; import com.baiyue.auth.repository.AuthAccountRepository; @@ -49,4 +51,41 @@ class AccountManagementServiceTest { assertEquals("DISABLED", account.getAccountStatus()); verify(sessions).revokeAll(10L); } + + @Test void 创建平台账号保存平台应用域且密码仅保存摘要() { + when(repository.findById(20L)).thenReturn(null); + when(repository.existsByUsername("platform.user")).thenReturn(false); + + var result = service.create(new CreateAccountRequest(20L, "platform.user", "Password1", + "平台用户", 0L, 30L, "PLATFORM")); + + assertEquals("platform.user", result.username()); + var captor = org.mockito.ArgumentCaptor.forClass(AuthAccount.class); + verify(repository).insert(captor.capture()); + assertEquals("PLATFORM", captor.getValue().getDefaultAppCode()); + assertTrue(encoder.matches("Password1", captor.getValue().getPasswordHash())); + assertNotEquals("Password1", captor.getValue().getPasswordHash()); + } + + @Test void 相同账号标识和归属重试时幂等返回() { + account.setUsername("platform.user"); account.setDisplayName("平台用户"); + account.setDefaultTenantId(0L); account.setOperatorTenantId(0L); + account.setDefaultMembershipId(30L); account.setDefaultAppCode("PLATFORM"); account.setDeleted(0); + + var result = service.create(new CreateAccountRequest(10L, "platform.user", "Password1", + "平台用户", 0L, 30L, "PLATFORM")); + + assertEquals(10L, result.accountId()); + verify(repository, never()).insert(any()); + } + + @Test void 同步显示名撤销已有会话() { + account.setDisplayName("旧名称"); + + service.updateProfile(10L, new UpdateAccountProfileRequest("新名称")); + + assertEquals("新名称", account.getDisplayName()); + verify(repository).save(account); + verify(sessions).revokeAll(10L); + } } diff --git a/docs/phase1-local-api.md b/docs/phase1-local-api.md index 1f11e78bc..ba7f58e2a 100644 --- a/docs/phase1-local-api.md +++ b/docs/phase1-local-api.md @@ -77,6 +77,8 @@ Content-Type: application/json | PUT | `/api/merchant/v1/iam/tenant/profile` | 更新当前租户资料 | | GET | `/api/merchant/v1/iam/tenant/qualifications` | 返回当前租户资质列表 | +IAM 的商户资质接口只读。旧的 `POST /api/merchant/v1/iam/tenant/qualifications` 已移除,上传必须使用 File 服务的 multipart 接口,避免数据库出现没有真实文件支撑的 URL。 + 个人资料与修改密码: | 方法 | 路径 | 说明 | @@ -142,6 +144,8 @@ Content-Type: application/json | GET | `/api/merchant/v1/files/qualifications/{fileName}` | 商户下载自己的资质 | | GET | `/api/platform/v1/files/qualifications/{tenantId}/{fileName}` | 平台查看指定租户资质 | +历史 `/phase1-seed/...` 路径不符合 File 服务契约,不能直接改成不存在的 UUID。处理步骤见 `docs/企业资质文件迁移说明.md`。 + 上传采用 `multipart/form-data`:`file` 必填,`certificateNo` 可选。只接受文件内容真实为 JPG/PNG 的图片,最大 5MB。上传成功 `data`: ```json diff --git a/event-bridge-service/src/main/java/com/baiyue/eventbridge/controller/EventBridgeController.java b/event-bridge-service/src/main/java/com/baiyue/eventbridge/controller/EventBridgeController.java index 4b4f4fec9..af0707c23 100644 --- a/event-bridge-service/src/main/java/com/baiyue/eventbridge/controller/EventBridgeController.java +++ b/event-bridge-service/src/main/java/com/baiyue/eventbridge/controller/EventBridgeController.java @@ -10,7 +10,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; +import jakarta.annotation.Resource; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -25,10 +25,12 @@ import org.springframework.web.bind.annotation.RestController; @Tag(name = "事件桥内部接口", description = "跨区域事件的幂等接收与能力状态") @RestController @RequestMapping("/internal/event-bridge") -@RequiredArgsConstructor public class EventBridgeController { - private final EventInboxService eventInboxService; - private final InternalServiceTokenGuard internalServiceTokenGuard; + @Resource + private EventInboxService eventInboxService; + + @Resource + private InternalServiceTokenGuard internalServiceTokenGuard; @Value("${baiyue.region:CN}") private String region; diff --git a/file/AGENTS.md b/file/AGENTS.md index 72f978520..27d1fcaf1 100644 --- a/file/AGENTS.md +++ b/file/AGENTS.md @@ -2,6 +2,8 @@ ## 职责边界 +- File 服务是企业资质文件的唯一公开上传入口。必须先持久化并验证真实文件,再调用 IAM 内部接口登记元数据;禁止只生成或改写一个不存在文件的 URL。 + - 负责企业资质文件的上传、存储、读取和访问鉴权;当前实现只使用本地磁盘,文件内容不进入 Git。 - `src/main/resources/db/file.sql` 是文件元数据、下载令牌和下载日志的目标表结构,当前代码尚未接入 Entity、Mapper 或 Repository,不能据此判断元数据持久化已实现。 - 企业资质审核状态和元数据归 `iam/`;本模块通过 IAM 客户端校验访问权限,不跨库读取 IAM 数据。 diff --git a/iam/AGENTS.md b/iam/AGENTS.md index c88be2c4c..17ae068cd 100644 --- a/iam/AGENTS.md +++ b/iam/AGENTS.md @@ -2,6 +2,8 @@ ## 职责边界 +- 商户侧资质公开接口在 IAM 中只提供查询;真实文件上传统一由 File 服务的 multipart 接口完成,IAM 仅接受 File 服务携带内部令牌提交的资质元数据。 + - 负责租户、用户、成员关系、企业资料与审核、角色、权限、数据范围、个人资料和授权快照。 - 账号凭据与 Token 属于 `auth/`;资质文件内容属于 `file/`。IAM 只维护业务元数据和跨服务协作。 @@ -12,7 +14,8 @@ | 商户个人与企业资料 | `controller/MerchantTenantController.java` | `PersonalProfileService.java`、`TenantService.java` | | 商户成员增改、启停、重置密码 | `MerchantTenantController.java` | `MemberService.java`,账号协作看 `client/AuthAccountClient.java` | | 平台创建企业与入驻审核 | `controller/PlatformTenantController.java` | `MerchantOnboardingService.java`、`TenantService.java` | -| 平台内部用户、企业地址与资质元数据 | `controller/ProfileResourceController.java` | `ProfileResourceService.java`、`ProfileResourceRepository.java` | +| 平台内部用户 | `controller/ProfileResourceController.java` | `PlatformInternalUserService.java`;账号创建、显示名、启停和重置密码经 `AuthAccountClient` 编排 | +| 企业地址与资质元数据 | `controller/ProfileResourceController.java` | `ProfileResourceService.java`、`ProfileResourceRepository.java` | | 角色、权限、成员角色、角色授权 | `controller/RbacController.java` | `RbacService.java`、`RbacRepository.java` | | Auth/Order 内部授权与资格查询 | `InternalAuthorizationController.java`、`InternalTenantController.java` | `AuthorizationSnapshotService.java`、`TenantGuard.java` | | DTO | `src/main/java/com/baiyue/iam/dto/` | 按 Authorization、Member、Onboarding、Profile、Rbac、Tenant 分类 | @@ -26,5 +29,6 @@ - `tenant_id` 是商户数据隔离核心;平台端与商户端访问域、角色和权限不可混用。 - 按钮隐藏不能代替后端 permission code / permissionIds 校验;权限快照变化需同步检查 Auth 与 Gateway Claim 消费。 - 成员账号操作跨 IAM/Auth,保持幂等与补偿;资质上传跨 IAM/File,IAM 不保存文件内容。 +- 平台内部用户的公开主键是 `membershipId`;列表必须回显 `roleIds`,密码只允许通过 DTO 转交 Auth,禁止进入 IAM 实体、日志或审计快照。 - SQL 增量保持可重复部署顺序,修改 Entity 时同步检查 Mapper、Repository、DTO 与种子数据。 - 从父目录运行 `mvn -pl iam -am test`,再按影响执行一期认证/IAM API 回归。 diff --git a/iam/src/main/java/com/baiyue/iam/client/AuthAccountClient.java b/iam/src/main/java/com/baiyue/iam/client/AuthAccountClient.java index 19358f3aa..ef80f6ca5 100644 --- a/iam/src/main/java/com/baiyue/iam/client/AuthAccountClient.java +++ b/iam/src/main/java/com/baiyue/iam/client/AuthAccountClient.java @@ -14,14 +14,22 @@ public interface AuthAccountClient { @PathVariable Long id); @PutMapping("/{id}/status") ApiResponse status(@RequestHeader("X-Internal-Service-Token") String token, @PathVariable Long id, @RequestBody StatusRequest request); + @PutMapping("/{id}/profile") ApiResponse profile(@RequestHeader("X-Internal-Service-Token") String token, + @PathVariable Long id, @RequestBody ProfileRequest request); @PutMapping("/{id}/password") ApiResponse reset(@RequestHeader("X-Internal-Service-Token") String token, @PathVariable Long id, @RequestBody PasswordRequest request); @DeleteMapping("/{id}/compensation") ApiResponse compensate(@RequestHeader("X-Internal-Service-Token") String token, @PathVariable Long id); record CreateAccountRequest(Long accountId, String username, String password, String displayName, - Long tenantId, Long membershipId) { } + Long tenantId, Long membershipId, String appCode) { + public CreateAccountRequest(Long accountId, String username, String password, String displayName, + Long tenantId, Long membershipId) { + this(accountId, username, password, displayName, tenantId, membershipId, "MERCHANT"); + } + } record AccountSummary(Long accountId, String username, String displayName, String status) { } record StatusRequest(String status) { } + record ProfileRequest(String displayName) { } record PasswordRequest(String newPassword) { } } diff --git a/iam/src/main/java/com/baiyue/iam/controller/MerchantTenantController.java b/iam/src/main/java/com/baiyue/iam/controller/MerchantTenantController.java index d54e08b41..77569392c 100644 --- a/iam/src/main/java/com/baiyue/iam/controller/MerchantTenantController.java +++ b/iam/src/main/java/com/baiyue/iam/controller/MerchantTenantController.java @@ -54,6 +54,7 @@ public class MerchantTenantController { } @GetMapping("/tenant/profile") + @PreAuthorize("hasAuthority('merchant:profile:view')") @Operation(summary = "Get current enterprise profile / 查询当前企业资料", description = "Tenant ID is taken only from the authenticated merchant JWT. / 租户 ID 仅从已认证商户 JWT 获取。") @ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Profile returned / 查询成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Tenant not found / 企业不存在")}) public ApiResponse profile(@AuthenticationPrincipal AuthenticatedUser user) { diff --git a/iam/src/main/java/com/baiyue/iam/controller/PlatformTenantController.java b/iam/src/main/java/com/baiyue/iam/controller/PlatformTenantController.java index f53766baa..e0b10c8fa 100644 --- a/iam/src/main/java/com/baiyue/iam/controller/PlatformTenantController.java +++ b/iam/src/main/java/com/baiyue/iam/controller/PlatformTenantController.java @@ -1,6 +1,7 @@ package com.baiyue.iam.controller; import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.core.api.PageResult; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.iam.dto.TenantDtos.RegisterRequest; import com.baiyue.iam.dto.TenantDtos.ReviewRequest; @@ -13,9 +14,12 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.Valid; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -25,11 +29,30 @@ import java.util.List; */ @RestController @RequestMapping("/api/platform/v1/iam") +@Validated @Tag(name = "Platform tenant administration / 平台企业管理", description = "Enterprise registration review and tenant activation for platform operators. / 平台运营人员进行企业注册审核和租户启用。") public class PlatformTenantController { @Resource private TenantService service; + @GetMapping("/tenants") + @PreAuthorize("hasAuthority('platform:customer:list')") + @Operation(summary = "List enterprise tenants / 查询企业列表", description = "Returns paged merchant enterprise tenants for platform operators. / 平台端分页查询商户企业,支持企业名称、统一社会信用代码、联系人和状态筛选。") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Tenant list returned / 企业列表查询成功"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid paging parameters / 分页参数无效"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限") + }) + public ApiResponse> page( + @RequestParam(defaultValue = "1") @Min(1) long page, + @RequestParam(defaultValue = "20") @Min(1) @Max(200) long size, + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String status, + @AuthenticationPrincipal AuthenticatedUser user) { + TenantGuard.tenant(user, "PLATFORM"); + return ApiResponse.success(service.pageMerchants(page, size, keyword, status)); + } + @PostMapping("/tenants") @PreAuthorize("hasAuthority('platform:customer:create')") @Operation(summary = "Register enterprise tenant / 提交企业注册", description = "Create a pending merchant tenant and its registration review ticket. Platform token required. / 创建待审核商户租户及注册审核单,需要平台端令牌。") diff --git a/iam/src/main/java/com/baiyue/iam/controller/ProfileResourceController.java b/iam/src/main/java/com/baiyue/iam/controller/ProfileResourceController.java index 470649548..bf8fc3be0 100644 --- a/iam/src/main/java/com/baiyue/iam/controller/ProfileResourceController.java +++ b/iam/src/main/java/com/baiyue/iam/controller/ProfileResourceController.java @@ -3,12 +3,11 @@ package com.baiyue.iam.controller; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.iam.dto.ProfileDtos.AddressRequest; -import com.baiyue.iam.dto.ProfileDtos.CreateUserRequest; -import com.baiyue.iam.dto.ProfileDtos.QualificationRequest; +import com.baiyue.iam.dto.PlatformInternalUserDtos.*; import com.baiyue.iam.entity.IamTenantAddress; import com.baiyue.iam.entity.IamTenantQualification; -import com.baiyue.iam.entity.IamUser; import com.baiyue.iam.service.ProfileResourceService; +import com.baiyue.iam.service.PlatformInternalUserService; import com.baiyue.iam.service.TenantGuard; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -29,23 +28,57 @@ import java.util.List; public class ProfileResourceController { @Resource private ProfileResourceService service; + @Resource + private PlatformInternalUserService internalUserService; @GetMapping("/api/platform/v1/iam/internal-users") + @PreAuthorize("hasAuthority('platform:user:list')") @Operation(summary = "List platform internal users / 查询平台内部用户", description = "Platform scope is required. / 需要平台端权限域。") @ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "User list returned / 查询成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限域")}) - public ApiResponse> internalUsers(@AuthenticationPrincipal AuthenticatedUser user) { + public ApiResponse> internalUsers(@AuthenticationPrincipal AuthenticatedUser user) { TenantGuard.tenant(user, "PLATFORM"); - return ApiResponse.success(service.internalUsers()); + return ApiResponse.success(internalUserService.list()); } @PostMapping("/api/platform/v1/iam/internal-users") @PreAuthorize("hasAuthority('platform:user:create')") @Operation(summary = "Create platform internal user / 新增平台内部用户", description = "Create an IAM profile linked to an auth-service account. / 创建关联统一认证账号的 IAM 用户资料。") @ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "User created / 创建成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid user profile / 用户资料不合法")}) - public ApiResponse addUser(@Valid @RequestBody CreateUserRequest request, - @AuthenticationPrincipal AuthenticatedUser user) { + public ApiResponse addUser(@Valid @RequestBody CreateInternalUserRequest request, + @AuthenticationPrincipal AuthenticatedUser user) { TenantGuard.tenant(user, "PLATFORM"); - return ApiResponse.success(service.addUser(request)); + return ApiResponse.success(internalUserService.create(request)); + } + + @PutMapping("/api/platform/v1/iam/internal-users/{membershipId:\\d+}") + @PreAuthorize("hasAuthority('platform:user:edit')") + @Operation(summary = "编辑平台内部用户") + public ApiResponse updateInternalUser( + @PathVariable Long membershipId, @Valid @RequestBody UpdateInternalUserRequest request, + @AuthenticationPrincipal AuthenticatedUser user) { + TenantGuard.tenant(user, "PLATFORM"); + return ApiResponse.success(internalUserService.update(membershipId, request)); + } + + @PutMapping("/api/platform/v1/iam/internal-users/{membershipId:\\d+}/status") + @PreAuthorize("hasAuthority('platform:user:status')") + @Operation(summary = "启用或停用平台内部用户") + public ApiResponse changeInternalUserStatus( + @PathVariable Long membershipId, @Valid @RequestBody ChangeInternalUserStatusRequest request, + @AuthenticationPrincipal AuthenticatedUser user) { + TenantGuard.tenant(user, "PLATFORM"); + return ApiResponse.success(internalUserService.changeStatus(membershipId, request, user.membershipId())); + } + + @PutMapping("/api/platform/v1/iam/internal-users/{membershipId:\\d+}/password") + @PreAuthorize("hasAuthority('platform:user:reset-password')") + @Operation(summary = "重置平台内部用户密码") + public ApiResponse resetInternalUserPassword( + @PathVariable Long membershipId, @Valid @RequestBody ResetInternalUserPasswordRequest request, + @AuthenticationPrincipal AuthenticatedUser user) { + TenantGuard.tenant(user, "PLATFORM"); + internalUserService.resetPassword(membershipId, request); + return ApiResponse.success(null); } @GetMapping("/api/merchant/v1/iam/tenant/addresses") @@ -79,12 +112,4 @@ public class ProfileResourceController { return ApiResponse.success(service.qualifications(tenantId)); } - @PostMapping("/api/merchant/v1/iam/tenant/qualifications") - @PreAuthorize("hasAuthority('merchant:qualification:upload')") - @Operation(summary = "Submit enterprise qualification / 提交企业资质", description = "Create a pending qualification record; document content is stored externally. / 创建待审核资质记录,文件内容由外部对象存储保存。") - @ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Qualification submitted / 提交成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid qualification / 资质信息不合法")}) - public ApiResponse addQualification(@Valid @RequestBody QualificationRequest request, - @AuthenticationPrincipal AuthenticatedUser user) { - return ApiResponse.success(service.addQualification(TenantGuard.tenant(user, "MERCHANT"), request)); - } } diff --git a/iam/src/main/java/com/baiyue/iam/dto/ProfileDtos.java b/iam/src/main/java/com/baiyue/iam/dto/ProfileDtos.java index 059927156..6dc36f880 100644 --- a/iam/src/main/java/com/baiyue/iam/dto/ProfileDtos.java +++ b/iam/src/main/java/com/baiyue/iam/dto/ProfileDtos.java @@ -2,18 +2,12 @@ package com.baiyue.iam.dto; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; -import java.time.LocalDate; - /** 用户档案、地址和资质的数据契约。 */ public final class ProfileDtos { private ProfileDtos() { } - public record CreateUserRequest(@NotNull Long authUserId, @NotBlank String userType, - @NotBlank String displayName, String phone, String email) { } - public record PersonalProfileResponse(Long userId, Long authUserId, String username, String displayName, String phone, String email) { } @@ -25,6 +19,4 @@ public final class ProfileDtos { String city, String district, @NotBlank String detail, String postalCode, Boolean isDefault) { } - public record QualificationRequest(@NotBlank String qualificationType, String certificateNo, - @NotBlank String documentUrl, LocalDate validFrom, LocalDate validTo) { } } diff --git a/iam/src/main/java/com/baiyue/iam/repository/ProfileResourceRepository.java b/iam/src/main/java/com/baiyue/iam/repository/ProfileResourceRepository.java index e08d1cbb5..043235c80 100644 --- a/iam/src/main/java/com/baiyue/iam/repository/ProfileResourceRepository.java +++ b/iam/src/main/java/com/baiyue/iam/repository/ProfileResourceRepository.java @@ -27,13 +27,11 @@ public class ProfileResourceRepository { this.qualificationMapper = qualificationMapper; } - public void insertUser(IamUser user) { userMapper.insert(user); } public IamUser findUserByAuthId(Long authUserId) { return userMapper.selectOne(new LambdaQueryWrapper().eq(IamUser::getAuthUserId, authUserId).last("LIMIT 1")); } public void saveUser(IamUser user) { userMapper.updateById(user); } public void insertAddress(IamTenantAddress address) { addressMapper.insert(address); } public void insertQualification(IamTenantQualification qualification) { qualificationMapper.insert(qualification); } public void saveQualification(IamTenantQualification qualification) { qualificationMapper.updateById(qualification); } - public List findInternalUsers() { return userMapper.selectList(new LambdaQueryWrapper().eq(IamUser::getUserType, "INTERNAL")); } public List findAddresses(long tenantId) { return addressMapper.selectList(new LambdaQueryWrapper().eq(IamTenantAddress::getTenantId, tenantId)); } public List findQualifications(long tenantId) { return qualificationMapper.selectList(new LambdaQueryWrapper().eq(IamTenantQualification::getTenantId, tenantId)); } public long countApprovedQualifications(long tenantId) { diff --git a/iam/src/main/java/com/baiyue/iam/repository/TenantRepository.java b/iam/src/main/java/com/baiyue/iam/repository/TenantRepository.java index aa93883e2..7b54b5a45 100644 --- a/iam/src/main/java/com/baiyue/iam/repository/TenantRepository.java +++ b/iam/src/main/java/com/baiyue/iam/repository/TenantRepository.java @@ -1,11 +1,14 @@ package com.baiyue.iam.repository; +import com.baiyue.common.core.api.PageResult; import com.baiyue.iam.entity.IamTenant; import com.baiyue.iam.entity.IamTenantReview; import com.baiyue.iam.mapper.IamTenantMapper; import com.baiyue.iam.mapper.IamTenantReviewMapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.stereotype.Repository; +import org.springframework.util.StringUtils; import java.util.List; @@ -45,4 +48,21 @@ public class TenantRepository { .eq(IamTenantReview::getStatus, "PENDING") .orderByAsc(IamTenantReview::getId)); } + + public PageResult pageMerchants(long page, long size, String keyword, String status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(IamTenant::getTenantType, "MERCHANT") + .eq(StringUtils.hasText(status), IamTenant::getStatus, status) + .orderByDesc(IamTenant::getCreatedAt); + if (StringUtils.hasText(keyword)) { + wrapper.and(condition -> condition + .like(IamTenant::getTenantCode, keyword) + .or().like(IamTenant::getTenantName, keyword) + .or().like(IamTenant::getLegalName, keyword) + .or().like(IamTenant::getUnifiedSocialCreditCode, keyword) + .or().like(IamTenant::getContactName, keyword) + .or().like(IamTenant::getContactPhone, keyword)); + } + return PageResult.from(tenantMapper.selectPage(new Page<>(page, size), wrapper)); + } } diff --git a/iam/src/main/java/com/baiyue/iam/service/ProfileResourceService.java b/iam/src/main/java/com/baiyue/iam/service/ProfileResourceService.java index 716f31950..ee753b121 100644 --- a/iam/src/main/java/com/baiyue/iam/service/ProfileResourceService.java +++ b/iam/src/main/java/com/baiyue/iam/service/ProfileResourceService.java @@ -6,13 +6,10 @@ import com.baiyue.common.core.exception.BizException; import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionRequest; import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionResponse; import com.baiyue.iam.dto.ProfileDtos.AddressRequest; -import com.baiyue.iam.dto.ProfileDtos.CreateUserRequest; -import com.baiyue.iam.dto.ProfileDtos.QualificationRequest; import com.baiyue.iam.entity.IamTenant; import com.baiyue.iam.entity.IamTenantAddress; import com.baiyue.iam.entity.IamTenantQualification; import com.baiyue.iam.entity.IamTenantReview; -import com.baiyue.iam.entity.IamUser; import com.baiyue.iam.repository.ProfileResourceRepository; import com.baiyue.iam.repository.TenantRepository; import com.baomidou.mybatisplus.core.toolkit.IdWorker; @@ -29,16 +26,6 @@ public class ProfileResourceService { @Resource private ProfileResourceRepository repository; @Resource private TenantRepository tenantRepository; - @BusinessAudit(event = "INTERNAL_USER_CREATE", resourceType = "IAM_USER", - resourceId = "#q.authUserId", description = "新增平台内部用户") - public IamUser addUser(CreateUserRequest q) { - IamUser u = new IamUser(); - u.setId(IdWorker.getId()); u.setAuthUserId(q.authUserId()); u.setUserType(q.userType()); - u.setDisplayName(q.displayName()); u.setPhone(q.phone()); u.setEmail(q.email()); u.setStatus("ACTIVE"); - repository.insertUser(u); return u; - } - - public List internalUsers() { return repository.findInternalUsers(); } public List addresses(long tenant) { return repository.findAddresses(tenant); } @BusinessAudit(event = "TENANT_ADDRESS_CREATE", resourceType = "TENANT_ADDRESS", @@ -54,11 +41,6 @@ public class ProfileResourceService { public List qualifications(long tenant) { return repository.findQualifications(tenant); } - public IamTenantQualification addQualification(long tenant, QualificationRequest q) { - throw new BizException(ResultCode.CONFLICT, - "Upload the business license through the qualification file API"); - } - @Transactional @BusinessAudit(event = "TENANT_QUALIFICATION_SUBMIT", resourceType = "TENANT_QUALIFICATION", resourceId = "#request.tenantId", description = "提交企业资质", captureAfter = false) diff --git a/iam/src/main/java/com/baiyue/iam/service/TenantService.java b/iam/src/main/java/com/baiyue/iam/service/TenantService.java index 70704bc15..ebf5739fc 100644 --- a/iam/src/main/java/com/baiyue/iam/service/TenantService.java +++ b/iam/src/main/java/com/baiyue/iam/service/TenantService.java @@ -2,6 +2,7 @@ package com.baiyue.iam.service; import jakarta.annotation.Resource; import com.baiyue.common.audit.BusinessAudit; +import com.baiyue.common.core.api.PageResult; import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.iam.entity.IamTenant; @@ -72,6 +73,12 @@ public class TenantService { return repository.findPendingReviews(); } + public PageResult pageMerchants(long page, long size, String keyword, String status) { + PageResult source = repository.pageMerchants(page, size, keyword, status); + return new PageResult<>(source.records().stream().map(this::response).toList(), + source.page(), source.size(), source.total(), source.pages()); + } + @BusinessAudit( event = "TENANT_REVIEW", resourceType = "TENANT_REVIEW", diff --git a/iam/src/main/resources/db/iam.sql b/iam/src/main/resources/db/iam.sql index 7ffb12b24..21a89a701 100644 --- a/iam/src/main/resources/db/iam.sql +++ b/iam/src/main/resources/db/iam.sql @@ -1,5 +1,6 @@ SET NAMES utf8mb4; -- IAM 服务独占数据库:负责租户、成员、双端角色、菜单/页面/按钮/API权限和数据范围。 + CREATE DATABASE IF NOT EXISTS baiyue_iam DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; USE baiyue_iam; diff --git a/iam/src/main/resources/db/phase1-onboarding.sql b/iam/src/main/resources/db/phase1-onboarding.sql index 5d2c493eb..44ba26145 100644 --- a/iam/src/main/resources/db/phase1-onboarding.sql +++ b/iam/src/main/resources/db/phase1-onboarding.sql @@ -61,11 +61,5 @@ FROM iam_permission WHERE app_code='MERCHANT' AND tenant_id=0 AND permission_type IN ('MENU','BUTTON') AND status='ACTIVE' AND deleted=0; --- Keep the phase-one demo merchant eligible for existing order acceptance tests. -INSERT INTO iam_tenant_qualification ( - id, tenant_id, qualification_type, certificate_no, document_url, status, deleted -) VALUES ( - 7000101, 10001, 'BUSINESS_LICENSE', '91310000PHASE10001', - '/phase1-seed/demo-business-license.jpg', 'APPROVED', 0 -) -ON DUPLICATE KEY UPDATE status='APPROVED', deleted=0; +-- 不再写入伪造的资质 URL。资质必须先由 File 服务保存真实图片,再由 IAM 记录文件地址和审核结果。 +-- 已部署环境中的 /phase1-seed/ 历史记录按 docs/企业资质文件迁移说明.md 处理,本脚本不得将其改成不存在的 UUID。 diff --git a/iam/src/main/resources/db/rbac-role-management-v2.sql b/iam/src/main/resources/db/rbac-role-management-v2.sql index fe97b6c9a..125ec6108 100644 --- a/iam/src/main/resources/db/rbac-role-management-v2.sql +++ b/iam/src/main/resources/db/rbac-role-management-v2.sql @@ -12,6 +12,9 @@ VALUES (5101025, 0, 'PLATFORM', 'BUTTON', 5101020, 'platform:role:delete', '删除角色', 50, 'ACTIVE', 0), (5101040, 0, 'PLATFORM', 'MENU', 5101000, 'platform:member:list', '平台成员', 40, 'ACTIVE', 0), (5101041, 0, 'PLATFORM', 'BUTTON', 5101040, 'platform:member:grant-role', '分配成员角色', 10, 'ACTIVE', 0), + (5101012, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:edit', '编辑用户', 20, 'ACTIVE', 0), + (5101013, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:status', '启停用户', 30, 'ACTIVE', 0), + (5101014, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:reset-password', '重置密码', 40, 'ACTIVE', 0), (5200033, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:edit', '编辑角色', 30, 'ACTIVE', 0), (5200034, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:status', '启停角色', 40, 'ACTIVE', 0), (5200035, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:delete', '删除角色', 50, 'ACTIVE', 0), @@ -28,7 +31,8 @@ INSERT INTO iam_role_permission (role_id, permission_id, tenant_id) SELECT 4000001, id, 0 FROM iam_permission WHERE tenant_id = 0 AND app_code = 'PLATFORM' AND permission_code IN ('platform:role:edit', 'platform:role:status', 'platform:role:delete', - 'platform:member:list', 'platform:member:grant-role', 'platform:customer:create') + 'platform:member:list', 'platform:member:grant-role', 'platform:customer:create', + 'platform:user:edit', 'platform:user:status', 'platform:user:reset-password') ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id); INSERT INTO iam_role_permission (role_id, permission_id, tenant_id) diff --git a/iam/src/main/resources/openapi/iam-v1.yaml b/iam/src/main/resources/openapi/iam-v1.yaml index c4928ea49..00a1d05e9 100644 --- a/iam/src/main/resources/openapi/iam-v1.yaml +++ b/iam/src/main/resources/openapi/iam-v1.yaml @@ -77,24 +77,73 @@ paths: /api/platform/v1/iam/internal-users: get: tags: [IAM Resources / IAM 资源] - summary: List platform internal users / 查询平台内部用户 + summary: 查询平台内部用户 responses: '200': {$ref: '#/components/responses/Success'} '403': {$ref: '#/components/responses/Forbidden'} post: tags: [IAM Resources / IAM 资源] - summary: Create platform internal user / 新增平台内部用户 - description: Create an IAM profile linked to an existing authentication account. / 创建关联已有认证账号的 IAM 用户资料。 + summary: 新增平台内部用户 + description: 创建完整的 Auth/IAM 平台账号,也兼容通过 authUserId 关联已有账号。 requestBody: required: true content: application/json: - schema: {$ref: '#/components/schemas/CreateIamUserRequest'} + schema: {$ref: '#/components/schemas/CreateInternalUserRequest'} responses: '200': {$ref: '#/components/responses/Success'} '400': {$ref: '#/components/responses/BadRequest'} '403': {$ref: '#/components/responses/Forbidden'} '409': {$ref: '#/components/responses/Conflict'} + /api/platform/v1/iam/internal-users/{membershipId}: + parameters: + - {$ref: '#/components/parameters/MembershipId'} + put: + tags: [IAM Resources / IAM 资源] + summary: 编辑平台内部用户 + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/UpdateInternalUserRequest'} + responses: + '200': {$ref: '#/components/responses/Success'} + '400': {$ref: '#/components/responses/BadRequest'} + '403': {$ref: '#/components/responses/Forbidden'} + '404': {$ref: '#/components/responses/NotFound'} + '409': {$ref: '#/components/responses/Conflict'} + /api/platform/v1/iam/internal-users/{membershipId}/status: + parameters: + - {$ref: '#/components/parameters/MembershipId'} + put: + tags: [IAM Resources / IAM 资源] + summary: 启用或停用平台内部用户 + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/ChangeInternalUserStatusRequest'} + responses: + '200': {$ref: '#/components/responses/Success'} + '400': {$ref: '#/components/responses/BadRequest'} + '403': {$ref: '#/components/responses/Forbidden'} + '404': {$ref: '#/components/responses/NotFound'} + /api/platform/v1/iam/internal-users/{membershipId}/password: + parameters: + - {$ref: '#/components/parameters/MembershipId'} + put: + tags: [IAM Resources / IAM 资源] + summary: 重置平台内部用户密码 + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/ResetInternalUserPasswordRequest'} + responses: + '200': {$ref: '#/components/responses/Success'} + '400': {$ref: '#/components/responses/BadRequest'} + '403': {$ref: '#/components/responses/Forbidden'} + '404': {$ref: '#/components/responses/NotFound'} /api/merchant/v1/iam/tenant/profile: get: tags: [Merchant Tenant / 商户企业] @@ -165,18 +214,6 @@ paths: responses: '200': {$ref: '#/components/responses/Success'} '403': {$ref: '#/components/responses/Forbidden'} - post: - tags: [IAM Resources / IAM 资源] - summary: Add tenant qualification / 新增企业资质 - requestBody: - required: true - content: - application/json: - schema: {$ref: '#/components/schemas/QualificationRequest'} - responses: - '200': {$ref: '#/components/responses/Success'} - '400': {$ref: '#/components/responses/BadRequest'} - '403': {$ref: '#/components/responses/Forbidden'} /api/{app}/v1/iam/roles: parameters: - {$ref: '#/components/parameters/App'} @@ -276,6 +313,12 @@ components: description: Application domain / 应用域 schema: {type: string, enum: [platform, merchant]} example: platform + MembershipId: + name: membershipId + in: path + required: true + description: Platform membership ID / 平台成员关系 ID + schema: {type: integer, format: int64} responses: Success: {description: Operation succeeded / 操作成功, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}} BadRequest: {description: Invalid request / 请求参数错误, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}} @@ -317,15 +360,53 @@ components: type: object properties: comment: {type: string, description: Review comment or rejection reason / 审核意见或驳回原因} - CreateIamUserRequest: + CreateInternalUserRequest: type: object - required: [authUserId, userType, displayName] + required: [displayName] properties: + authUserId: {type: integer, format: int64, description: 兼容旧接口;填写后关联已有认证账号} + userType: {type: string, enum: [INTERNAL, PLATFORM], description: 兼容旧接口,可省略} + username: {type: string, minLength: 4, maxLength: 64} + password: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true} + displayName: {type: string, maxLength: 64} + phone: {type: string, maxLength: 32} + email: {type: string, format: email, maxLength: 128} + memberNo: {type: string, maxLength: 64} + roleIds: {type: array, items: {type: integer, format: int64}} + UpdateInternalUserRequest: + type: object + required: [displayName, memberNo, roleIds] + properties: + displayName: {type: string, maxLength: 64} + phone: {type: string, maxLength: 32} + email: {type: string, format: email, maxLength: 128} + memberNo: {type: string, maxLength: 64} + roleIds: {type: array, items: {type: integer, format: int64}} + ChangeInternalUserStatusRequest: + type: object + required: [status] + properties: + status: {type: string, enum: [ACTIVE, DISABLED]} + ResetInternalUserPasswordRequest: + type: object + required: [newPassword] + properties: + newPassword: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true} + InternalUserResponse: + type: object + required: [membershipId, userId, authUserId, username, displayName, memberType, status, roleIds] + properties: + membershipId: {type: integer, format: int64} + userId: {type: integer, format: int64} authUserId: {type: integer, format: int64} - userType: {type: string, enum: [PLATFORM, ENTERPRISE]} + username: {type: string} displayName: {type: string} phone: {type: string} email: {type: string, format: email} + memberType: {type: string, enum: [EMPLOYEE]} + memberNo: {type: string} + status: {type: string, enum: [ACTIVE, DISABLED]} + roleIds: {type: array, items: {type: integer, format: int64}} CreateMemberRequest: type: object required: [username, password, displayName, memberType] @@ -350,15 +431,6 @@ components: detail: {type: string} postalCode: {type: string} isDefault: {type: boolean} - QualificationRequest: - type: object - required: [qualificationType, documentUrl] - properties: - qualificationType: {type: string, example: BUSINESS_LICENSE} - certificateNo: {type: string} - documentUrl: {type: string, format: uri} - validFrom: {type: string, format: date} - validTo: {type: string, format: date} CreateRoleRequest: type: object required: [roleCode, roleName] diff --git a/integration-service/AGENTS.md b/integration-service/AGENTS.md index 49d6b752d..18603c154 100644 --- a/integration-service/AGENTS.md +++ b/integration-service/AGENTS.md @@ -12,12 +12,17 @@ | 应用与配置扫描 | `src/main/java/com/baiyue/integration/IntegrationServiceApplication.java` | | `baiyue.integration` 配置 | `config/IntegrationProperties.java` | | 内部供应商状态 API | `controller/IntegrationProviderController.java` | +| 重量 OCR 内部 API | `controller/WeightOcrController.java` | | 能力状态模型与服务 | `dto/ProviderStatus.java`、`service/ProviderStatusService.java` | | 适配器抽象 | `provider/IntegrationProviderAdapter.java` | -| 百度 OCR | `provider/BaiduOcrAdapter.java` | +| 百度 OCR 状态与客户端 | `provider/BaiduOcrAdapter.java`、`provider/ocr/BaiduMeterClient.java` | +| 重量 OCR 协议模型 | `dto/ocr/` | +| 重量 OCR 批处理 | `service/ocr/WeightOcrBatchService.java` | +| 图片裁剪与重量筛选 | `service/ocr/ImageCropper.java`、`service/ocr/WeightCandidateSelector.java` | +| OCR 文件访问边界 | `service/ocr/WeightOcrStoragePolicy.java` | | Amazon | `provider/AmazonLogisticsAdapter.java` | | 内部服务令牌 | `security/InternalServiceTokenGuard.java` | -| 配置测试 | `src/test/java/com/baiyue/integration/config/IntegrationPropertiesTest.java` | +| 配置与 OCR 测试 | `src/test/java/com/baiyue/integration/config/IntegrationPropertiesTest.java`、`src/test/java/com/baiyue/integration/service/ocr/` | | 本地/Nacos 配置 | `src/main/resources/application.yml`、`../config/nacos/{local,cn}/integration-service.yml` | ## 修改与验证 @@ -25,4 +30,7 @@ - 第三方密钥、Token、请求原文和客户敏感数据不得写入源码或日志;配置应支持按供应商独立关闭。 - 适配器负责外部协议转换、超时、错误归一化和安全降级;业务编排留在调用方服务。 - 启用真实调用前补齐超时、重试边界、限流、幂等和可观测性,并避免在测试中访问真实供应商。 +- OCR 目录批处理只能访问 `WEIGHT_OCR_INPUT_ROOT` 和 `WEIGHT_OCR_OUTPUT_ROOT` 内的路径,不得恢复任意服务器路径读取。 +- OCR 接口只允许内部服务令牌调用;图片大小、解码像素数、单批数量和并发批次限制不得绕过。 +- OCR 原始响应和第三方密钥不得写入应用日志,真实供应商调用必须保持默认关闭。 - 从父目录运行 `mvn -pl integration-service -am test`。 diff --git a/order/AGENTS.md b/order/AGENTS.md index ea6ed86ba..c6082b29a 100644 --- a/order/AGENTS.md +++ b/order/AGENTS.md @@ -9,8 +9,8 @@ | 功能 | 入口 | |---|---| -| 商户订单创建、编辑、列表、详情、提交、取消 | `controller/MerchantFirstLegOrderController.java` | -| 平台订单列表、详情、审核、接单、开始、完成、取消 | `controller/PlatformFirstLegOrderController.java` | +| 商户订单创建、编辑、列表、详情、提交、取消 | `controller/MerchantFirstLegOrderController.java`;列表校验 `merchant:order:list`,详情校验 `merchant:order:view` | +| 平台订单列表、详情、审核、接单、开始、完成、取消 | `controller/PlatformFirstLegOrderController.java`;列表和详情校验 `platform:order:list` | | 内部备注与快递信息 | `PlatformFirstLegOrderController.java` | | 核心编排与状态机 | `service/OrderApplicationService.java`、`entity/OrderStatus.java` | | 企业下单资格 | `service/OrderEligibilityGuard.java`、`client/TenantEligibilityClient.java` | @@ -20,11 +20,12 @@ | 主单、货物、参与方、历史、备注、Outbox | `entity/`、`mapper/`、`repository/OrderRepository.java` | | 表结构与一期扩展 | `src/main/resources/db/order.sql`、`phase1-masterdata-extension.sql`、`phase1-order-extension.sql` | | 模块说明 | `README-ORDER.md` | -| 状态与主数据测试 | `src/test/java/com/baiyue/order/service/` | +| 状态、主数据与接口权限测试 | `src/test/java/com/baiyue/order/service/`、`src/test/java/com/baiyue/order/controller/` | ## 修改与验证 - 所有商户查询和写入必须执行租户/访问范围校验;平台操作必须验证权限及合法状态迁移。 +- 查询接口同样必须校验权限码;沿用 IAM 已初始化的权限资源,新增权限码时同步补充 IAM 初始化脚本与角色授权。 - 已提交订单中的仓库、往来单位等快照不能随主数据修改而变化。 - 跨服务只通过 `client/` 或事件演进,不跨库查询 IAM/Warehouse;跨域通知优先使用 Outbox。 - 新增状态或动作时同步修改 Controller、Service 状态机、历史记录、DTO、SQL、前端消费者和测试。 diff --git a/order/src/main/java/com/baiyue/order/controller/MerchantFirstLegOrderController.java b/order/src/main/java/com/baiyue/order/controller/MerchantFirstLegOrderController.java index 4e1c72388..20490f3ca 100644 --- a/order/src/main/java/com/baiyue/order/controller/MerchantFirstLegOrderController.java +++ b/order/src/main/java/com/baiyue/order/controller/MerchantFirstLegOrderController.java @@ -80,6 +80,7 @@ public class MerchantFirstLegOrderController { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Merchant scope required / 需要商户端权限") }) @GetMapping + @PreAuthorize("hasAuthority('merchant:order:list')") public ApiResponse> page(@AuthenticationPrincipal AuthenticatedUser user, @Valid OrderRequests.QueryRequest query) { return ApiResponse.success(service.page(OrderScopeGuard.merchant(user), query)); @@ -92,6 +93,7 @@ public class MerchantFirstLegOrderController { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Order not found / 订单不存在") }) @GetMapping("/{id}") + @PreAuthorize("hasAuthority('merchant:order:view')") public ApiResponse detail( @AuthenticationPrincipal AuthenticatedUser user, @Parameter(description = "Order ID / 订单 ID", example = "190000000000000001", required = true) @PathVariable long id) { diff --git a/order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java b/order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java index 418ebfcf8..046c3c0d9 100644 --- a/order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java +++ b/order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java @@ -47,6 +47,7 @@ public class PlatformFirstLegOrderController { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限") }) @GetMapping + @PreAuthorize("hasAuthority('platform:order:list')") public ApiResponse> page( @AuthenticationPrincipal AuthenticatedUser user, @Parameter(description = "Target tenant ID / 目标租户 ID", example = "10001", required = true) @RequestParam long tenantId, @@ -61,6 +62,7 @@ public class PlatformFirstLegOrderController { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Order not found in target tenant / 目标租户下订单不存在") }) @GetMapping("/{id}") + @PreAuthorize("hasAuthority('platform:order:list')") public ApiResponse detail( @AuthenticationPrincipal AuthenticatedUser user, @Parameter(description = "Target tenant ID / 目标租户 ID", example = "10001", required = true) @RequestParam long tenantId, diff --git a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java index 7866225d4..80f0ca723 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java +++ b/order/src/main/java/com/baiyue/order/service/OrderApplicationService.java @@ -6,6 +6,7 @@ import com.baiyue.order.dto.OrderDetailResponse; import com.baiyue.order.dto.OrderRequests; import com.baiyue.order.entity.*; import com.baiyue.order.repository.OrderRepository; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.Resource; @@ -14,10 +15,8 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; -import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; @Service @@ -33,7 +32,8 @@ public class OrderApplicationService { validatePrototypeFields(request); var snapshots = masterDataGuard.validate(tenantId, request); OrderEntity order = new OrderEntity(); - order.setTenantId(tenantId); order.setOrderNo(nextNo()); order.setStatus(OrderStatus.DRAFT); + long orderId = IdWorker.getId(); + order.setId(orderId); order.setTenantId(tenantId); order.setOrderNo(nextNo(orderId)); order.setStatus(OrderStatus.DRAFT); apply(order, request, snapshots); order.setCreatedBy(userId); order.setUpdatedBy(userId); repository.insertOrder(order); saveChildren(tenantId, userId, order.getId(), request); @@ -182,8 +182,9 @@ public class OrderApplicationService { order.setShipmentId(request.shipmentId()); order.setReferenceId(request.referenceId()); } private String formatAddress(com.baiyue.order.client.WarehouseClient.Detail detail) { - if (detail.addresses() == null || detail.addresses().isEmpty()) return null; - var a = detail.addresses().get(0); + if (detail.addresses() == null) return null; + var a = detail.addresses().stream().filter(java.util.Objects::nonNull).findFirst().orElse(null); + if (a == null) return null; return java.util.stream.Stream.of(a.countryCode(), a.stateProvince(), a.city(), a.district(), a.addressLine1(), a.addressLine2(), a.postalCode()) .filter(StringUtils::hasText).collect(Collectors.joining(" ")); @@ -191,7 +192,7 @@ public class OrderApplicationService { private String formatReceivingTime(com.baiyue.order.client.WarehouseClient.Detail detail) { if (detail.capabilities() == null) return null; return detail.capabilities().stream() - .filter(c -> "RECEIVING".equals(c.capabilityCode()) && Boolean.TRUE.equals(c.enabled())) + .filter(c -> c != null && "RECEIVING".equals(c.capabilityCode()) && Boolean.TRUE.equals(c.enabled())) .map(c -> c.cutoffTime() == null ? "支持收货" : "每日 " + c.cutoffTime() + " 前收货") .findFirst().orElse(null); } @@ -241,8 +242,7 @@ public class OrderApplicationService { event.setPublishStatus("PENDING"); event.setRetryCount(0); event.setCreatedAt(LocalDateTime.now()); repository.insertOutbox(event); } - private String nextNo() { - return "FL" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) - + ThreadLocalRandom.current().nextInt(100, 1000); + private String nextNo(long orderId) { + return "FL" + orderId; } } diff --git a/order/src/main/java/com/baiyue/order/service/OrderMasterDataGuard.java b/order/src/main/java/com/baiyue/order/service/OrderMasterDataGuard.java index c8dae67f6..3f6be0b78 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderMasterDataGuard.java +++ b/order/src/main/java/com/baiyue/order/service/OrderMasterDataGuard.java @@ -8,6 +8,9 @@ import com.baiyue.order.dto.OrderRequests; import feign.FeignException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.Objects; @Service public class OrderMasterDataGuard { @@ -23,10 +26,10 @@ public class OrderMasterDataGuard { try { var partner=loadPartner(tenantId,request.customerId()); var receiving=loadWarehouse(tenantId,request.receivingWarehouseId(),"收货仓库"); - if (receiving.addresses()==null || receiving.addresses().isEmpty()) + if (receiving.addresses()==null || receiving.addresses().stream().noneMatch(Objects::nonNull)) throw new BizException(ResultCode.VALIDATION_FAILED,"收货仓库未配置地址"); if (receiving.capabilities()==null || receiving.capabilities().stream().noneMatch(c -> - "RECEIVING".equals(c.capabilityCode()) && Boolean.TRUE.equals(c.enabled()))) + c != null && "RECEIVING".equals(c.capabilityCode()) && Boolean.TRUE.equals(c.enabled()))) throw new BizException(ResultCode.VALIDATION_FAILED,"仓库未启用收货能力"); WarehouseClient.Detail destination=null; if (request.destinationWarehouseId()!=null) { @@ -57,6 +60,7 @@ public class OrderMasterDataGuard { var value=response.data(); if (!Long.valueOf(tenantId).equals(value.tenantId()) || !"ACTIVE".equals(value.status())) throw new BizException(ResultCode.FORBIDDEN,"往来单位不属于当前企业或已停用"); + if (!StringUtils.hasText(value.partnerName())) throw unavailable("往来单位"); return value; } private WarehouseClient.Detail loadWarehouse(long tenantId,long id,String name) { @@ -66,6 +70,9 @@ public class OrderMasterDataGuard { var value=response.data(); if (!Long.valueOf(tenantId).equals(value.warehouse().tenantId()) || !"ACTIVE".equals(value.warehouse().status())) throw new BizException(ResultCode.FORBIDDEN,name+"不属于当前企业或已停用"); + if (!Long.valueOf(id).equals(value.warehouse().id()) || !StringUtils.hasText(value.warehouse().warehouseName())) { + throw unavailable(name); + } return value; } private BizException unavailable(String name) { diff --git a/order/src/main/resources/db/order.sql b/order/src/main/resources/db/order.sql index 77a537c8c..1f7617b90 100644 --- a/order/src/main/resources/db/order.sql +++ b/order/src/main/resources/db/order.sql @@ -1,5 +1,6 @@ SET NAMES utf8mb4; -- 订单服务独占数据库。其他服务只能通过 OpenAPI 或领域事件获取订单数据。 + CREATE DATABASE IF NOT EXISTS baiyue_order DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; USE baiyue_order; diff --git a/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java b/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java index a10311684..a12b7b99f 100644 --- a/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java +++ b/order/src/test/java/com/baiyue/order/service/OrderMasterDataGuardTest.java @@ -1,12 +1,14 @@ package com.baiyue.order.service; import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.order.client.PartnerClient; import com.baiyue.order.client.WarehouseClient; import com.baiyue.order.dto.OrderRequests; import org.junit.jupiter.api.Test; import java.math.BigDecimal; +import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -32,6 +34,31 @@ class OrderMasterDataGuardTest { assertThatThrownBy(() -> new OrderMasterDataGuard(warehouses,partners,"token").validate(10002,request(7L,8L))) .isInstanceOf(BizException.class).hasMessageContaining("服务暂时不可用"); } + @Test + void 主数据响应含空元素时返回受控业务错误而不是空指针异常() { + WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class); + var partner=new PartnerClient.Partner(7L,10001L,"服务端客户名","ACTIVE"); + var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,10001L,"深圳仓","ACTIVE"), + Collections.singletonList(null), Collections.singletonList(null)); + when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(partner)); + when(warehouses.active("token",10001,8)).thenReturn(ApiResponse.success(wh)); + + assertThatThrownBy(() -> new OrderMasterDataGuard(warehouses,partners,"token").validate(10001,request(7L,8L))) + .isInstanceOf(BizException.class) + .extracting(exception -> ((BizException) exception).getResultCode()) + .isEqualTo(ResultCode.VALIDATION_FAILED); + } + @Test + void 缺失快照名称的主数据响应按服务不可用处理() { + WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class); + when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,10001L,null,"ACTIVE"))); + + assertThatThrownBy(() -> new OrderMasterDataGuard(warehouses,partners,"token").validate(10001,request(7L,8L))) + .isInstanceOf(BizException.class) + .extracting(exception -> ((BizException) exception).getResultCode()) + .isEqualTo(ResultCode.SERVICE_UNAVAILABLE); + verifyNoInteractions(warehouses); + } private OrderRequests.SaveRequest request(Long partner,Long warehouse) { var party=new OrderRequests.Party("公司","联系人","13800138000",null,"CN",null,"深圳",null,"地址",null); var cargo=new OrderRequests.Cargo("货物",null,"CARTON",1,BigDecimal.ONE,BigDecimal.ONE, diff --git a/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalPartnerController.java b/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalPartnerController.java index b0bcb0493..78e63e315 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalPartnerController.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalPartnerController.java @@ -5,6 +5,7 @@ import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.warehouse.entity.PartnerEntity; import com.baiyue.warehouse.service.PartnerQueryService; +import jakarta.annotation.Resource; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import java.nio.charset.StandardCharsets; @@ -13,10 +14,14 @@ import java.security.MessageDigest; @RestController @RequestMapping("/internal/warehouse-partners") public class InternalPartnerController { - private final PartnerQueryService service; private final byte[] expected; - public InternalPartnerController(PartnerQueryService service, + @Resource + private PartnerQueryService service; + + private final byte[] expected; + + public InternalPartnerController( @Value("${baiyue.security.internal-service-token}") String token) { - this.service=service; this.expected=token.getBytes(StandardCharsets.UTF_8); + this.expected=token.getBytes(StandardCharsets.UTF_8); } @GetMapping("/{tenantId}/{id}/active") public ApiResponse active(@RequestHeader(value="X-Internal-Service-Token",required=false) String token, diff --git a/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalWarehouseController.java b/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalWarehouseController.java index fd5fadca9..2f216a6c4 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalWarehouseController.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalWarehouseController.java @@ -5,6 +5,7 @@ import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.warehouse.dto.WarehouseDetailResponse; import com.baiyue.warehouse.service.WarehouseApplicationService; +import jakarta.annotation.Resource; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import java.nio.charset.StandardCharsets; @@ -13,11 +14,14 @@ import java.security.MessageDigest; @RestController @RequestMapping("/internal/warehouses") public class InternalWarehouseController { - private final WarehouseApplicationService service; + @Resource + private WarehouseApplicationService service; + private final byte[] expected; - public InternalWarehouseController(WarehouseApplicationService service, + + public InternalWarehouseController( @Value("${baiyue.security.internal-service-token}") String token) { - this.service = service; this.expected = token.getBytes(StandardCharsets.UTF_8); + this.expected = token.getBytes(StandardCharsets.UTF_8); } @GetMapping("/{tenantId}/{id}/active") public ApiResponse active( diff --git a/warehouse/src/main/java/com/baiyue/warehouse/controller/MerchantPartnerController.java b/warehouse/src/main/java/com/baiyue/warehouse/controller/MerchantPartnerController.java index f6881a830..4e8374903 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/controller/MerchantPartnerController.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/controller/MerchantPartnerController.java @@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.warehouse.entity.PartnerEntity; import com.baiyue.warehouse.service.PartnerQueryService; +import jakarta.annotation.Resource; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -11,8 +12,9 @@ import java.util.List; @RestController @RequestMapping("/api/merchant/v1/partners") public class MerchantPartnerController { - private final PartnerQueryService service; - public MerchantPartnerController(PartnerQueryService service) { this.service=service; } + @Resource + private PartnerQueryService service; + @GetMapping("/candidates") public ApiResponse> candidates(@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required=false) String keyword,@RequestParam(required=false) String type) {