优化
This commit is contained in:
@@ -47,7 +47,7 @@ public class AuthController {
|
|||||||
@SecurityRequirements
|
@SecurityRequirements
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
@Operation(summary = "Merchant registration / 客户注册",
|
@Operation(summary = "Merchant registration / 客户注册",
|
||||||
description = "Create a login account and an enterprise profile awaiting four required qualification files.")
|
description = "Create a login account and an enterprise profile awaiting the required business license and legal-person ID; other qualification files are optional.")
|
||||||
public ApiResponse<MerchantRegisterResponse> register(@Valid @RequestBody MerchantRegisterRequest request) {
|
public ApiResponse<MerchantRegisterResponse> register(@Valid @RequestBody MerchantRegisterRequest request) {
|
||||||
return ApiResponse.success(registrationService.register(request));
|
return ApiResponse.success(registrationService.register(request));
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -35,7 +35,7 @@
|
|||||||
- 平台客户档案维护使用 `PlatformTenantController` 的列表、详情、新建、编辑、启停接口;详情和维护仅接受 `tenant_type=MERCHANT`。PENDING/REJECTED 只能由审核流程变更,不得由启停接口绕过。
|
- 平台客户档案维护使用 `PlatformTenantController` 的列表、详情、新建、编辑、启停接口;详情和维护仅接受 `tenant_type=MERCHANT`。PENDING/REJECTED 只能由审核流程变更,不得由启停接口绕过。
|
||||||
- 平台代客户上传资质的公开 multipart 入口在 File 服务;IAM 的 `/internal/iam/platform-qualifications` 只登记元数据。当前仅 `PROFILE_REQUIRED/REJECTED` 可上传,同类型重复需显式 `replace=true`;替换时旧记录标记 REJECTED 并把旧 `documentUrl` 返回 File 清理。
|
- 平台代客户上传资质的公开 multipart 入口在 File 服务;IAM 的 `/internal/iam/platform-qualifications` 只登记元数据。当前仅 `PROFILE_REQUIRED/REJECTED` 可上传,同类型重复需显式 `replace=true`;替换时旧记录标记 REJECTED 并把旧 `documentUrl` 返回 File 清理。
|
||||||
- 平台资质登记在事务内通过 `ProfileResourceRepository.findQualificationsForUpdate` 锁定目标租户现有资质,避免并发上传或替换产生多个有效同类型记录;该查询不得改回普通无锁列表。
|
- 平台资质登记在事务内通过 `ProfileResourceRepository.findQualificationsForUpdate` 锁定目标租户现有资质,避免并发上传或替换产生多个有效同类型记录;该查询不得改回普通无锁列表。
|
||||||
- 企业送审和审核通过都要求 `BUSINESS_LICENSE`、`LEGAL_PERSON_ID`、`FREIGHT_FORWARDER_CERTIFICATE` 与 `BANK_ACCOUNT_LICENSE` 四类资质均为 `PENDING`;审核由 `TenantService.review()` 同事务更新审核单、租户和所有 PENDING 资质,行锁负责并发防重。
|
- 企业送审和审核通过只要求 `BUSINESS_LICENSE`、`LEGAL_PERSON_ID` 两类必填资质均为 `PENDING`;`FREIGHT_FORWARDER_CERTIFICATE` 与 `BANK_ACCOUNT_LICENSE` 为选填,已上传时仍随审核统一处理。审核由 `TenantService.review()` 同事务更新审核单、租户和所有 PENDING 资质,行锁负责并发防重。
|
||||||
- 平台客户列表使用 `platform:customer:list` 并脱敏电话、邮箱;完整详情、平台地址和平台资质只读查询统一使用独立的 `platform:customer:view`,不得复用 `platform:customer:review`。
|
- 平台客户列表使用 `platform:customer:list` 并脱敏电话、邮箱;完整详情、平台地址和平台资质只读查询统一使用独立的 `platform:customer:view`,不得复用 `platform:customer:review`。
|
||||||
- 权限资源的 code/type 不可变;只有 `built_in=0` 的自定义资源可编辑、启停或删除,删除前必须无子节点且无角色绑定。
|
- 权限资源的 code/type 不可变;只有 `built_in=0` 的自定义资源可编辑、启停或删除,删除前必须无子节点且无角色绑定。
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
- 平台内部用户的公开主键是 `membershipId`;列表必须回显 `roleIds`,密码只允许通过 DTO 转交 Auth,禁止进入 IAM 实体、日志或审计快照。
|
- 平台内部用户的公开主键是 `membershipId`;列表必须回显 `roleIds`,密码只允许通过 DTO 转交 Auth,禁止进入 IAM 实体、日志或审计快照。
|
||||||
- 平台内部用户角色分配使用 `PUT /api/platform/v1/iam/internal-users/{membershipId}/roles`,仅允许平台租户下已启用的平台角色;空 `roleIds` 表示清空。
|
- 平台内部用户角色分配使用 `PUT /api/platform/v1/iam/internal-users/{membershipId}/roles`,仅允许平台租户下已启用的平台角色;空 `roleIds` 表示清空。
|
||||||
- 商户成员新增、编辑采用字段级授权:基础资料分别校验 `merchant:member:create`、`merchant:member:edit`;创建时非空角色、编辑时角色集合发生任何变化(含空数组清空)还必须校验 `merchant:member:grant-role`。详情和列表实时回显当前 `roleIds`,比较和写入均限定当前租户。
|
- 商户成员新增、编辑采用字段级授权:基础资料分别校验 `merchant:member:create`、`merchant:member:edit`;创建时非空角色、编辑时角色集合发生任何变化(含空数组清空)还必须校验 `merchant:member:grant-role`。详情和列表实时回显当前 `roleIds`,比较和写入均限定当前租户。
|
||||||
- 企业 `ACTIVE` 只表示平台审核已通过;下单资格还必须四类必填资质全部为 `APPROVED`。任意一类缺失、驳回、已替换或过期都必须由服务端阻止提交订单。
|
- 企业 `ACTIVE` 只表示平台审核已通过;下单资格还必须营业执照和法人身份证两类必填资质均为 `APPROVED`。任一必填资质缺失、驳回、已替换或过期都必须由服务端阻止提交订单;选填资质不影响下单资格。
|
||||||
- SQL 增量保持可重复部署顺序,修改 Entity 时同步检查 Mapper、Repository、DTO 与种子数据。`phase1-platform-master-data-and-permission-management.sql` 必须在 `phase1-admin-rbac.sql` 和 `rbac-role-management-v2.sql` 之后执行;它标记既有系统资源并补齐平台客户、快递公司、往来单位与权限资源管理权限。
|
- SQL 增量保持可重复部署顺序,修改 Entity 时同步检查 Mapper、Repository、DTO 与种子数据。`phase1-platform-master-data-and-permission-management.sql` 必须在 `phase1-admin-rbac.sql` 和 `rbac-role-management-v2.sql` 之后执行;它标记既有系统资源并补齐平台客户、快递公司、往来单位与权限资源管理权限。
|
||||||
- 存量库新增一期客户档案字段时执行 `phase1-customer-profile.sql`;该脚本只扩展 `iam_tenant`,可重复执行,不得把运单数、财务金额、套餐计费、代客登录或物理删除能力并入 IAM。
|
- 存量库新增一期客户档案字段时执行 `phase1-customer-profile.sql`;该脚本只扩展 `iam_tenant`,可重复执行,不得把运单数、财务金额、套餐计费、代客登录或物理删除能力并入 IAM。
|
||||||
- `deploy/scripts/initialize-test-databases.sh` 的 IAM 客户档案和 Warehouse 主数据脚本顺序必须与当前文件保持一致;测试环境正式增量仍以 `../deploy/sql/README.md` 指定的统一迁移为准。
|
- `deploy/scripts/initialize-test-databases.sh` 的 IAM 客户档案和 Warehouse 主数据脚本顺序必须与当前文件保持一致;测试环境正式增量仍以 `../deploy/sql/README.md` 指定的统一迁移为准。
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class MerchantTenantController {
|
|||||||
|
|
||||||
@PostMapping("/tenant/qualifications/submit-review")
|
@PostMapping("/tenant/qualifications/submit-review")
|
||||||
@PreAuthorize("hasAuthority('merchant:qualification:upload')")
|
@PreAuthorize("hasAuthority('merchant:qualification:upload')")
|
||||||
@Operation(summary = "提交当前企业资质审核", description = "校验四类必填资质齐全后创建审核单。上传或替换文件不会自动送审。")
|
@Operation(summary = "提交当前企业资质审核", description = "校验营业执照和法人身份证齐全后创建审核单;货代资质证书和银行开户许可证选填。上传或替换文件不会自动送审。")
|
||||||
public ApiResponse<TenantResponse> submitQualificationReview(@AuthenticationPrincipal AuthenticatedUser user) {
|
public ApiResponse<TenantResponse> submitQualificationReview(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||||
return ApiResponse.success(tenantService.submitMerchantReview(TenantGuard.tenant(user, "MERCHANT")));
|
return ApiResponse.success(tenantService.submitMerchantReview(TenantGuard.tenant(user, "MERCHANT")));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import java.util.Set;
|
|||||||
@Service
|
@Service
|
||||||
public class TenantService {
|
public class TenantService {
|
||||||
private static final Set<String> REQUIRED_QUALIFICATION_TYPES = Set.of(
|
private static final Set<String> REQUIRED_QUALIFICATION_TYPES = Set.of(
|
||||||
"BUSINESS_LICENSE", "LEGAL_PERSON_ID", "FREIGHT_FORWARDER_CERTIFICATE", "BANK_ACCOUNT_LICENSE");
|
"BUSINESS_LICENSE", "LEGAL_PERSON_ID");
|
||||||
@Resource
|
@Resource
|
||||||
private TenantRepository repository;
|
private TenantRepository repository;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -273,7 +273,7 @@ public class TenantService {
|
|||||||
var qualifications = profileRepository.findQualifications(tenantId);
|
var qualifications = profileRepository.findQualifications(tenantId);
|
||||||
if (!hasAllPendingQualifications(qualifications)) {
|
if (!hasAllPendingQualifications(qualifications)) {
|
||||||
throw new BizException(ResultCode.BAD_REQUEST,
|
throw new BizException(ResultCode.BAD_REQUEST,
|
||||||
"Business license, legal-person ID, freight-forwarder certificate and bank account license are required / 营业执照、法人身份证、货代资质证书和银行开户许可证均为必填资质");
|
"Business license and legal-person ID are required / 营业执照和法人身份证为必填资质");
|
||||||
}
|
}
|
||||||
IamTenantReview review = new IamTenantReview();
|
IamTenantReview review = new IamTenantReview();
|
||||||
review.setId(IdWorker.getId()); review.setTenantId(tenantId);
|
review.setId(IdWorker.getId()); review.setTenantId(tenantId);
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ paths:
|
|||||||
post:
|
post:
|
||||||
tags: [IAM Resources / IAM 资源]
|
tags: [IAM Resources / IAM 资源]
|
||||||
summary: Submit enterprise qualifications for review / 提交企业资质审核
|
summary: Submit enterprise qualifications for review / 提交企业资质审核
|
||||||
description: Uploading or replacing a qualification keeps the tenant in draft state. This endpoint creates the review only after all four required qualification types have been provided. / 上传或替换资质不会自动送审;四类必填资质齐全后由此接口创建审核单。
|
description: Uploading or replacing a qualification keeps the tenant in draft state. This endpoint creates the review after the business license and legal-person ID have been provided; the freight-forwarder certificate and bank account license are optional. / 上传或替换资质不会自动送审;营业执照和法人身份证齐全后由此接口创建审核单,货代资质证书和银行开户许可证选填。
|
||||||
responses:
|
responses:
|
||||||
'200': {$ref: '#/components/responses/TenantSuccess'}
|
'200': {$ref: '#/components/responses/TenantSuccess'}
|
||||||
'400': {$ref: '#/components/responses/BadRequest'}
|
'400': {$ref: '#/components/responses/BadRequest'}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class TenantServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void explicitSubmissionRequiresAllPrototypeQualificationsAndCreatesReview() {
|
void explicitSubmissionRequiresOnlyBusinessLicenseAndLegalPersonIdAndCreatesReview() {
|
||||||
TenantRepository repository = mock(TenantRepository.class);
|
TenantRepository repository = mock(TenantRepository.class);
|
||||||
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
||||||
TenantService service = new TenantService();
|
TenantService service = new TenantService();
|
||||||
@@ -143,8 +143,7 @@ class TenantServiceTest {
|
|||||||
IamTenant tenant = merchant(10001L, "PROFILE_REQUIRED");
|
IamTenant tenant = merchant(10001L, "PROFILE_REQUIRED");
|
||||||
when(repository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
when(repository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||||
when(profileRepository.findQualifications(10001L)).thenReturn(List.of(
|
when(profileRepository.findQualifications(10001L)).thenReturn(List.of(
|
||||||
qualification("BUSINESS_LICENSE"), qualification("LEGAL_PERSON_ID"),
|
qualification("BUSINESS_LICENSE"), qualification("LEGAL_PERSON_ID")));
|
||||||
qualification("FREIGHT_FORWARDER_CERTIFICATE"), qualification("BANK_ACCOUNT_LICENSE")));
|
|
||||||
|
|
||||||
var submitted = service.submitMerchantReview(10001L);
|
var submitted = service.submitMerchantReview(10001L);
|
||||||
|
|
||||||
@@ -162,7 +161,7 @@ class TenantServiceTest {
|
|||||||
ReflectionTestUtils.setField(service, "profileRepository", profileRepository);
|
ReflectionTestUtils.setField(service, "profileRepository", profileRepository);
|
||||||
when(repository.findTenantForUpdate(10001L)).thenReturn(merchant(10001L, "PROFILE_REQUIRED"));
|
when(repository.findTenantForUpdate(10001L)).thenReturn(merchant(10001L, "PROFILE_REQUIRED"));
|
||||||
when(profileRepository.findQualifications(10001L)).thenReturn(List.of(
|
when(profileRepository.findQualifications(10001L)).thenReturn(List.of(
|
||||||
qualification("BUSINESS_LICENSE"), qualification("LEGAL_PERSON_ID")));
|
qualification("BUSINESS_LICENSE")));
|
||||||
|
|
||||||
BizException exception = assertThrows(BizException.class, () -> service.submitMerchantReview(10001L));
|
BizException exception = assertThrows(BizException.class, () -> service.submitMerchantReview(10001L));
|
||||||
|
|
||||||
@@ -205,8 +204,7 @@ class TenantServiceTest {
|
|||||||
when(repository.findReviewForUpdate(71L)).thenReturn(review);
|
when(repository.findReviewForUpdate(71L)).thenReturn(review);
|
||||||
when(repository.findTenant(10001L)).thenReturn(tenant);
|
when(repository.findTenant(10001L)).thenReturn(tenant);
|
||||||
when(profileRepository.findQualifications(10001L)).thenReturn(List.of(
|
when(profileRepository.findQualifications(10001L)).thenReturn(List.of(
|
||||||
qualification("BUSINESS_LICENSE"), qualification("LEGAL_PERSON_ID"),
|
qualification("BUSINESS_LICENSE"), qualification("LEGAL_PERSON_ID")));
|
||||||
qualification("FREIGHT_FORWARDER_CERTIFICATE"), qualification("BANK_ACCOUNT_LICENSE")));
|
|
||||||
when(provisioning.initialize(any())).thenReturn(
|
when(provisioning.initialize(any())).thenReturn(
|
||||||
TenantAdministratorProvisioningService.ProvisioningResult.created(
|
TenantAdministratorProvisioningService.ProvisioningResult.created(
|
||||||
81L, "merchant_10001", "Password1234"));
|
81L, "merchant_10001", "Password1234"));
|
||||||
@@ -285,7 +283,7 @@ class TenantServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void 企业激活不等于具备下单资格仍需四类资质全部审核通过() {
|
void 企业激活后仅需两类必填资质审核通过即可下单() {
|
||||||
TenantRepository repository = mock(TenantRepository.class);
|
TenantRepository repository = mock(TenantRepository.class);
|
||||||
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
||||||
TenantService service = new TenantService();
|
TenantService service = new TenantService();
|
||||||
@@ -295,8 +293,7 @@ class TenantServiceTest {
|
|||||||
when(profileRepository.findQualifications(10001L)).thenReturn(
|
when(profileRepository.findQualifications(10001L)).thenReturn(
|
||||||
List.of(approvedQualification("BUSINESS_LICENSE")),
|
List.of(approvedQualification("BUSINESS_LICENSE")),
|
||||||
List.of(approvedQualification("BUSINESS_LICENSE"), approvedQualification("LEGAL_PERSON_ID"),
|
List.of(approvedQualification("BUSINESS_LICENSE"), approvedQualification("LEGAL_PERSON_ID"),
|
||||||
approvedQualification("FREIGHT_FORWARDER_CERTIFICATE"),
|
rejectedQualification("FREIGHT_FORWARDER_CERTIFICATE")));
|
||||||
approvedQualification("BANK_ACCOUNT_LICENSE")));
|
|
||||||
|
|
||||||
var withoutQualification = service.orderEligibility(10001L);
|
var withoutQualification = service.orderEligibility(10001L);
|
||||||
var withQualification = service.orderEligibility(10001L);
|
var withQualification = service.orderEligibility(10001L);
|
||||||
@@ -332,6 +329,12 @@ class TenantServiceTest {
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IamTenantQualification rejectedQualification(String type) {
|
||||||
|
IamTenantQualification item = qualification(type);
|
||||||
|
item.setStatus("REJECTED");
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
private IamTenantReview review(long id, long tenantId) {
|
private IamTenantReview review(long id, long tenantId) {
|
||||||
IamTenantReview review = new IamTenantReview();
|
IamTenantReview review = new IamTenantReview();
|
||||||
review.setId(id); review.setTenantId(tenantId); review.setStatus("PENDING");
|
review.setId(id); review.setTenantId(tenantId); review.setStatus("PENDING");
|
||||||
|
|||||||
Reference in New Issue
Block a user