暂存提交
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.iam;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
/**
|
||||
* 身份与权限服务启动入口,负责装配租户、成员、双端 RBAC 和授权快照相关组件。
|
||||
*/
|
||||
@MapperScan("com.baiyue.iam.mapper")
|
||||
@EnableFeignClients(basePackages = "com.baiyue.iam.client")
|
||||
@SpringBootApplication(scanBasePackages = "com.baiyue")
|
||||
public class BaiyueIamApplication {
|
||||
public static void main(String[] args) { SpringApplication.run(BaiyueIamApplication.class, args); }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.iam.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/** auth-service 内部账号管理客户端。 */
|
||||
@FeignClient(name = "auth-service", contextId = "authAccountClient",
|
||||
url = "${baiyue.clients.auth.url:}", path = "/internal/auth/accounts")
|
||||
public interface AuthAccountClient {
|
||||
@PostMapping ApiResponse<AccountSummary> create(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody CreateAccountRequest request);
|
||||
@GetMapping("/{id}") ApiResponse<AccountSummary> summary(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable Long id);
|
||||
@PutMapping("/{id}/status") ApiResponse<Void> status(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable Long id, @RequestBody StatusRequest request);
|
||||
@PutMapping("/{id}/password") ApiResponse<Void> reset(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable Long id, @RequestBody PasswordRequest request);
|
||||
@DeleteMapping("/{id}/compensation") ApiResponse<Void> 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) { }
|
||||
record AccountSummary(Long accountId, String username, String displayName, String status) { }
|
||||
record StatusRequest(String status) { }
|
||||
record PasswordRequest(String newPassword) { }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IAM 服务健康探针接口,用于确认身份与权限服务实例可用。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/iam")
|
||||
public class IamHealthController {
|
||||
@GetMapping("/status")
|
||||
public ApiResponse<Map<String, String>> status() {
|
||||
return ApiResponse.success(Map.of("service", "iam-service", "status", "UP"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.AuthorizationDtos.SnapshotRequest;
|
||||
import com.baiyue.iam.dto.AuthorizationDtos.SnapshotResponse;
|
||||
import com.baiyue.iam.service.AuthorizationSnapshotService;
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
/**
|
||||
* IAM 内部授权接口,仅供认证服务查询账号在指定租户和应用域下的角色、权限及数据范围快照。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/internal/iam")
|
||||
public class InternalAuthorizationController {
|
||||
@Resource
|
||||
private AuthorizationSnapshotService service;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalTokenValue;
|
||||
private byte[] internalToken;
|
||||
|
||||
@PostConstruct
|
||||
void initializeInternalToken() {
|
||||
if (internalTokenValue == null || internalTokenValue.getBytes(StandardCharsets.UTF_8).length < 32) {
|
||||
throw new IllegalStateException("INTERNAL_SERVICE_TOKEN 至少需要 32 字节");
|
||||
}
|
||||
internalToken = internalTokenValue.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@PostMapping("/authorization-snapshot")
|
||||
public ApiResponse<SnapshotResponse> load(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody SnapshotRequest request) {
|
||||
byte[] supplied = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8);
|
||||
if (!MessageDigest.isEqual(internalToken, supplied)) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "无效的服务间凭据");
|
||||
}
|
||||
return ApiResponse.success(service.load(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.OnboardingDtos.MerchantRegistrationRequest;
|
||||
import com.baiyue.iam.dto.OnboardingDtos.MerchantRegistrationResponse;
|
||||
import com.baiyue.iam.dto.OnboardingDtos.OrderEligibilityResponse;
|
||||
import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionRequest;
|
||||
import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionResponse;
|
||||
import com.baiyue.iam.service.MerchantOnboardingService;
|
||||
import com.baiyue.iam.service.ProfileResourceService;
|
||||
import com.baiyue.iam.service.TenantService;
|
||||
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.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/internal/iam")
|
||||
public class InternalTenantController {
|
||||
@Resource private MerchantOnboardingService onboardingService;
|
||||
@Resource private ProfileResourceService profileResourceService;
|
||||
@Resource private TenantService tenantService;
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalTokenValue;
|
||||
private byte[] internalToken;
|
||||
|
||||
@PostConstruct
|
||||
void initializeToken() { internalToken = internalTokenValue.getBytes(StandardCharsets.UTF_8); }
|
||||
|
||||
@PostMapping("/merchant-registrations")
|
||||
public ApiResponse<MerchantRegistrationResponse> register(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody MerchantRegistrationRequest request) {
|
||||
verify(token);
|
||||
return ApiResponse.success(onboardingService.register(request));
|
||||
}
|
||||
|
||||
@PostMapping("/qualifications")
|
||||
public ApiResponse<QualificationSubmissionResponse> submitQualification(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody QualificationSubmissionRequest request) {
|
||||
verify(token);
|
||||
return ApiResponse.success(profileResourceService.submitQualification(request));
|
||||
}
|
||||
|
||||
@GetMapping("/tenants/{tenantId}/order-eligibility")
|
||||
public ApiResponse<OrderEligibilityResponse> orderEligibility(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@PathVariable long tenantId) {
|
||||
verify(token);
|
||||
return ApiResponse.success(tenantService.orderEligibility(tenantId));
|
||||
}
|
||||
|
||||
private void verify(String token) {
|
||||
byte[] supplied = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8);
|
||||
if (!MessageDigest.isEqual(internalToken, supplied)) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "Invalid internal service token");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.iam.dto.MemberDtos.*;
|
||||
import com.baiyue.iam.dto.ProfileDtos.PersonalProfileResponse;
|
||||
import com.baiyue.iam.dto.ProfileDtos.UpdatePersonalProfileRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.TenantResponse;
|
||||
import com.baiyue.iam.dto.TenantDtos.UpdateProfileRequest;
|
||||
import com.baiyue.iam.service.MemberService;
|
||||
import com.baiyue.iam.service.PersonalProfileService;
|
||||
import com.baiyue.iam.service.TenantGuard;
|
||||
import com.baiyue.iam.service.TenantService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商户端租户接口,提供当前租户资料维护、成员列表与成员新增能力,并强制使用登录租户边界。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/iam")
|
||||
@Tag(name = "Merchant tenant and members / 商户企业与成员", description = "Manage the current JWT tenant profile and enterprise members. / 管理当前 JWT 租户的企业资料和成员。")
|
||||
public class MerchantTenantController {
|
||||
@Resource
|
||||
private TenantService tenantService;
|
||||
@Resource
|
||||
private MemberService memberService;
|
||||
@Resource
|
||||
private PersonalProfileService personalProfileService;
|
||||
|
||||
@GetMapping("/profile")
|
||||
@Operation(summary = "查看当前登录用户个人资料")
|
||||
public ApiResponse<PersonalProfileResponse> personalProfile(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
long tenantId = TenantGuard.tenant(user, "MERCHANT");
|
||||
return ApiResponse.success(personalProfileService.get(tenantId, user.userId(), user.username()));
|
||||
}
|
||||
|
||||
@PutMapping("/profile")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Operation(summary = "编辑当前登录用户个人资料")
|
||||
public ApiResponse<PersonalProfileResponse> updatePersonalProfile(
|
||||
@Valid @RequestBody UpdatePersonalProfileRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
long tenantId = TenantGuard.tenant(user, "MERCHANT");
|
||||
return ApiResponse.success(personalProfileService.update(tenantId, user.userId(), user.username(), request));
|
||||
}
|
||||
|
||||
@GetMapping("/tenant/profile")
|
||||
@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<TenantResponse> profile(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(tenantService.profile(TenantGuard.tenant(user, "MERCHANT")));
|
||||
}
|
||||
|
||||
@PutMapping("/tenant/profile")
|
||||
@PreAuthorize("hasAuthority('merchant:profile:edit')")
|
||||
@Operation(summary = "Update current enterprise profile / 更新当前企业资料", description = "Update basic profile fields within the authenticated tenant. / 在当前认证租户内更新企业基础资料。")
|
||||
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Profile updated / 更新成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid profile / 资料不合法")})
|
||||
public ApiResponse<TenantResponse> update(@Valid @RequestBody UpdateProfileRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(tenantService.update(TenantGuard.tenant(user, "MERCHANT"), request));
|
||||
}
|
||||
|
||||
@GetMapping("/members")
|
||||
@Operation(summary = "List enterprise members / 查询企业成员", description = "List members belonging to the current JWT tenant. / 查询属于当前 JWT 租户的成员。")
|
||||
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Member list returned / 查询成功")})
|
||||
@PreAuthorize("hasAuthority('merchant:member:list')")
|
||||
public ApiResponse<List<MemberResponse>> members(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(memberService.list(TenantGuard.tenant(user, "MERCHANT")));
|
||||
}
|
||||
|
||||
@GetMapping("/members/{membershipId:\\d+}")
|
||||
@PreAuthorize("hasAuthority('merchant:member:list')")
|
||||
@Operation(summary = "查询成员详情及当前角色")
|
||||
public ApiResponse<MemberResponse> member(@PathVariable Long membershipId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(memberService.detail(TenantGuard.tenant(user, "MERCHANT"), membershipId));
|
||||
}
|
||||
|
||||
@PostMapping("/members")
|
||||
@Operation(summary = "Add enterprise member / 新增企业成员", description = "Create a membership under the current JWT tenant. / 在当前 JWT 租户下创建成员关系。")
|
||||
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Member created / 创建成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "Member already exists / 成员已存在")})
|
||||
@PreAuthorize("hasAuthority('merchant:member:create')")
|
||||
public ApiResponse<MemberResponse> addMember(@Valid @RequestBody CreateMemberRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(memberService.create(TenantGuard.tenant(user, "MERCHANT"), request));
|
||||
}
|
||||
|
||||
@PutMapping("/members/{membershipId:\\d+}")
|
||||
@PreAuthorize("hasAuthority('merchant:member:edit')")
|
||||
@Operation(summary = "编辑企业成员及角色")
|
||||
public ApiResponse<MemberResponse> updateMember(@PathVariable Long membershipId,
|
||||
@Valid @RequestBody UpdateMemberRequest request, @AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(memberService.update(TenantGuard.tenant(user, "MERCHANT"), membershipId, request));
|
||||
}
|
||||
|
||||
@PutMapping("/members/{membershipId:\\d+}/status")
|
||||
@PreAuthorize("hasAuthority('merchant:member:status')")
|
||||
@Operation(summary = "启用或停用企业成员")
|
||||
public ApiResponse<MemberResponse> changeMemberStatus(@PathVariable Long membershipId,
|
||||
@Valid @RequestBody ChangeMemberStatusRequest request, @AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(memberService.changeStatus(TenantGuard.tenant(user, "MERCHANT"),
|
||||
membershipId, request, user.membershipId()));
|
||||
}
|
||||
|
||||
@PutMapping("/members/{membershipId:\\d+}/password")
|
||||
@PreAuthorize("hasAuthority('merchant:member:reset-password')")
|
||||
@Operation(summary = "管理员重置成员密码")
|
||||
public ApiResponse<Void> resetMemberPassword(@PathVariable Long membershipId,
|
||||
@Valid @RequestBody ResetMemberPasswordRequest request, @AuthenticationPrincipal AuthenticatedUser user) {
|
||||
memberService.resetPassword(TenantGuard.tenant(user, "MERCHANT"), membershipId, request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.iam.dto.TenantDtos.RegisterRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.ReviewRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.TenantResponse;
|
||||
import com.baiyue.iam.entity.IamTenantReview;
|
||||
import com.baiyue.iam.service.TenantGuard;
|
||||
import com.baiyue.iam.service.TenantService;
|
||||
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 jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 后台端租户管理接口,提供商户入驻登记、待审列表和审批处理能力。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/iam")
|
||||
@Tag(name = "Platform tenant administration / 平台企业管理", description = "Enterprise registration review and tenant activation for platform operators. / 平台运营人员进行企业注册审核和租户启用。")
|
||||
public class PlatformTenantController {
|
||||
@Resource
|
||||
private TenantService service;
|
||||
|
||||
@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. / 创建待审核商户租户及注册审核单,需要平台端令牌。")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Registration created / 注册已创建"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid enterprise data / 企业资料不合法"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限域"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "Enterprise already exists / 企业已存在")
|
||||
})
|
||||
public ApiResponse<TenantResponse> register(@Valid @RequestBody RegisterRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.register(request));
|
||||
}
|
||||
|
||||
@GetMapping("/reviews")
|
||||
@Operation(summary = "List pending enterprise reviews / 查询待审核企业", description = "Return pending enterprise registration review tickets ordered by creation. / 按创建顺序返回待审核企业注册单。")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Review list returned / 查询成功"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限域")
|
||||
})
|
||||
public ApiResponse<List<IamTenantReview>> reviews(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.pendingReviews());
|
||||
}
|
||||
|
||||
@PostMapping("/reviews/{id}/approve")
|
||||
@PreAuthorize("hasAuthority('platform:customer:approve')")
|
||||
@Operation(summary = "Approve enterprise registration / 审核通过企业注册", description = "Approve a pending review and activate the tenant. The operation is audited. / 审核通过并激活租户,操作写入审计日志。")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Approved / 审核通过"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Review not found / 审核单不存在"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "Review already completed / 审核已完成")
|
||||
})
|
||||
public ApiResponse<Void> approve(@Parameter(description = "Review ticket ID / 审核单 ID", required = true, example = "190000000000000001") @PathVariable long id,
|
||||
@RequestBody ReviewRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
service.review(id, user.userId(), true, request.comment());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@PostMapping("/reviews/{id}/reject")
|
||||
@PreAuthorize("hasAuthority('platform:customer:reject')")
|
||||
@Operation(summary = "Reject enterprise registration / 驳回企业注册", description = "Reject a pending review and retain the rejection comment. The operation is audited. / 驳回待审核企业并保留审核意见,操作写入审计日志。")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Rejected / 已驳回"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Review not found / 审核单不存在"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "Review already completed / 审核已完成")
|
||||
})
|
||||
public ApiResponse<Void> reject(@Parameter(description = "Review ticket ID / 审核单 ID", required = true, example = "190000000000000001") @PathVariable long id,
|
||||
@RequestBody ReviewRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
service.review(id, user.userId(), false, request.comment());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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.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.TenantGuard;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IAM 档案资源接口,管理内部用户、租户地址和租户资质等身份附属资料。
|
||||
*/
|
||||
@RestController
|
||||
@Tag(name = "IAM profiles and qualifications / IAM 用户、地址与资质", description = "Platform internal users and merchant tenant profile resources. / 平台内部用户及商户企业地址、资质资源。")
|
||||
public class ProfileResourceController {
|
||||
@Resource
|
||||
private ProfileResourceService service;
|
||||
|
||||
@GetMapping("/api/platform/v1/iam/internal-users")
|
||||
@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<List<IamUser>> internalUsers(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.internalUsers());
|
||||
}
|
||||
|
||||
@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<IamUser> addUser(@Valid @RequestBody CreateUserRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.addUser(request));
|
||||
}
|
||||
|
||||
@GetMapping("/api/merchant/v1/iam/tenant/addresses")
|
||||
@Operation(summary = "List enterprise addresses / 查询企业地址", description = "Tenant ID is taken from the merchant JWT. / 租户 ID 从商户 JWT 获取。")
|
||||
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Address list returned / 查询成功")})
|
||||
public ApiResponse<List<IamTenantAddress>> addresses(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(service.addresses(TenantGuard.tenant(user, "MERCHANT")));
|
||||
}
|
||||
|
||||
@PostMapping("/api/merchant/v1/iam/tenant/addresses")
|
||||
@PreAuthorize("hasAuthority('merchant:profile:edit')")
|
||||
@Operation(summary = "Add enterprise address / 新增企业地址", description = "Add an operating, registered or return address to the current tenant. / 为当前租户新增经营、注册地址或退货地址。")
|
||||
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Address created / 创建成功"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid address / 地址不合法")})
|
||||
public ApiResponse<IamTenantAddress> addAddress(@Valid @RequestBody AddressRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(service.addAddress(TenantGuard.tenant(user, "MERCHANT"), request));
|
||||
}
|
||||
|
||||
@GetMapping("/api/merchant/v1/iam/tenant/qualifications")
|
||||
@Operation(summary = "List enterprise qualifications / 查询企业资质", description = "List qualification files owned by the current tenant. / 查询当前租户的资质文件。")
|
||||
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Qualification list returned / 查询成功")})
|
||||
public ApiResponse<List<IamTenantQualification>> qualifications(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(service.qualifications(TenantGuard.tenant(user, "MERCHANT")));
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/iam/tenants/{tenantId}/qualifications")
|
||||
@PreAuthorize("hasAuthority('platform:customer:review')")
|
||||
public ApiResponse<List<IamTenantQualification>> platformQualifications(
|
||||
@PathVariable long tenantId, @AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
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<IamTenantQualification> addQualification(@Valid @RequestBody QualificationRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(service.addQualification(TenantGuard.tenant(user, "MERCHANT"), request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.iam.dto.RbacDtos.*;
|
||||
import com.baiyue.iam.entity.IamPermission;
|
||||
import com.baiyue.iam.entity.IamRole;
|
||||
import com.baiyue.iam.service.RbacService;
|
||||
import com.baiyue.iam.service.TenantGuard;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 平台端和商户端的角色权限管理接口。 */
|
||||
@RestController
|
||||
@RequestMapping("/api/{app}/v1/iam")
|
||||
@Tag(name = "角色与权限管理")
|
||||
public class RbacController {
|
||||
@Resource private RbacService service;
|
||||
|
||||
@GetMapping("/roles")
|
||||
@Operation(summary = "查询角色及当前权限")
|
||||
@PreAuthorize("hasAuthority(#app + ':role:list')")
|
||||
public ApiResponse<List<RoleResponse>> roles(@PathVariable String app,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
Scope scope = scope(app, user);
|
||||
return ApiResponse.success(service.roles(scope.tenantId(), scope.appCode()));
|
||||
}
|
||||
|
||||
@GetMapping("/roles/{roleId}")
|
||||
@Operation(summary = "查询角色详情及当前权限")
|
||||
@PreAuthorize("hasAuthority(#app + ':role:list')")
|
||||
public ApiResponse<RoleResponse> role(@PathVariable String app, @PathVariable Long roleId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
Scope scope = scope(app, user);
|
||||
return ApiResponse.success(service.role(scope.tenantId(), scope.appCode(), roleId));
|
||||
}
|
||||
|
||||
@PostMapping("/roles")
|
||||
@Operation(summary = "创建角色")
|
||||
@PreAuthorize("hasAuthority(#app + ':role:create')")
|
||||
public ApiResponse<IamRole> createRole(@PathVariable String app,
|
||||
@Valid @RequestBody CreateRoleRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
Scope scope = scope(app, user);
|
||||
return ApiResponse.success(service.createRole(scope.tenantId(), scope.appCode(), request));
|
||||
}
|
||||
|
||||
@PutMapping("/roles/{roleId}")
|
||||
@Operation(summary = "编辑自定义角色")
|
||||
@PreAuthorize("hasAuthority(#app + ':role:edit')")
|
||||
public ApiResponse<RoleResponse> updateRole(@PathVariable String app, @PathVariable Long roleId,
|
||||
@Valid @RequestBody UpdateRoleRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
Scope scope = scope(app, user);
|
||||
return ApiResponse.success(service.updateRole(scope.tenantId(), scope.appCode(), roleId, request));
|
||||
}
|
||||
|
||||
@PutMapping("/roles/{roleId}/status")
|
||||
@Operation(summary = "启用或停用自定义角色")
|
||||
@PreAuthorize("hasAuthority(#app + ':role:status')")
|
||||
public ApiResponse<RoleResponse> changeStatus(@PathVariable String app, @PathVariable Long roleId,
|
||||
@Valid @RequestBody ChangeRoleStatusRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
Scope scope = scope(app, user);
|
||||
return ApiResponse.success(service.changeStatus(scope.tenantId(), scope.appCode(), roleId, request));
|
||||
}
|
||||
|
||||
@DeleteMapping("/roles/{roleId}")
|
||||
@Operation(summary = "删除未分配成员的自定义角色")
|
||||
@PreAuthorize("hasAuthority(#app + ':role:delete')")
|
||||
public ApiResponse<Void> deleteRole(@PathVariable String app, @PathVariable Long roleId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
Scope scope = scope(app, user);
|
||||
service.deleteRole(scope.tenantId(), scope.appCode(), roleId);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@GetMapping("/members/{membershipId}/roles")
|
||||
@Operation(summary = "查询成员当前角色")
|
||||
@PreAuthorize("hasAuthority(#app + ':member:list')")
|
||||
public ApiResponse<MemberRolesResponse> memberRoles(@PathVariable String app,
|
||||
@PathVariable Long membershipId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
Scope scope = scope(app, user);
|
||||
return ApiResponse.success(service.memberRoles(scope.tenantId(), scope.appCode(), membershipId));
|
||||
}
|
||||
|
||||
@PutMapping("/members/roles")
|
||||
@Operation(summary = "全量设置成员角色,空集合表示清空")
|
||||
@PreAuthorize("hasAuthority(#app + ':member:grant-role')")
|
||||
public ApiResponse<Void> assignRoles(@PathVariable String app,
|
||||
@Valid @RequestBody AssignRolesRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
Scope scope = scope(app, user);
|
||||
service.assignRoles(scope.tenantId(), scope.appCode(), request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@GetMapping("/permissions")
|
||||
@Operation(summary = "查询权限资源")
|
||||
@PreAuthorize("hasAuthority(#app + ':role:list')")
|
||||
public ApiResponse<List<IamPermission>> permissions(@PathVariable String app,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
Scope scope = scope(app, user);
|
||||
return ApiResponse.success(service.permissions(scope.tenantId(), scope.appCode()));
|
||||
}
|
||||
|
||||
@PostMapping("/permissions")
|
||||
@Operation(summary = "创建权限资源")
|
||||
@PreAuthorize("hasAuthority(#app + ':permission:create')")
|
||||
public ApiResponse<IamPermission> createPermission(@PathVariable String app,
|
||||
@Valid @RequestBody CreatePermissionRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
Scope scope = scope(app, user);
|
||||
return ApiResponse.success(service.createPermission(scope.tenantId(), scope.appCode(), request));
|
||||
}
|
||||
|
||||
@PutMapping("/roles/permissions")
|
||||
@Operation(summary = "全量设置角色权限,空集合表示清空")
|
||||
@PreAuthorize("hasAuthority(#app + ':role:grant')")
|
||||
public ApiResponse<Void> grant(@PathVariable String app,
|
||||
@Valid @RequestBody GrantPermissionsRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
Scope scope = scope(app, user);
|
||||
service.grantPermissions(scope.tenantId(), scope.appCode(), request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
private Scope scope(String app, AuthenticatedUser user) {
|
||||
String appCode = switch (app) {
|
||||
case "platform" -> "PLATFORM";
|
||||
case "merchant" -> "MERCHANT";
|
||||
default -> throw new IllegalArgumentException("不支持的应用端");
|
||||
};
|
||||
return new Scope(TenantGuard.tenant(user, appCode), appCode);
|
||||
}
|
||||
|
||||
private record Scope(long tenantId, String appCode) { }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.baiyue.iam.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 授权快照内部接口的数据契约集合,定义快照查询请求和包含角色、权限、数据范围的响应模型。
|
||||
*/
|
||||
public final class AuthorizationDtos {
|
||||
private AuthorizationDtos() { }
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SnapshotRequest {
|
||||
@NotNull
|
||||
private Long userId;
|
||||
@NotNull
|
||||
private Long tenantId;
|
||||
@NotNull
|
||||
private Long membershipId;
|
||||
@NotBlank
|
||||
private String appCode;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SnapshotResponse {
|
||||
private Long userId;
|
||||
private Long tenantId;
|
||||
private Long membershipId;
|
||||
private String appCode;
|
||||
private String clientType;
|
||||
private Set<String> roles;
|
||||
private Set<String> permissions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.baiyue.iam.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 企业成员接口的数据契约。 */
|
||||
public final class MemberDtos {
|
||||
private MemberDtos() { }
|
||||
|
||||
public record CreateMemberRequest(
|
||||
@NotBlank @Pattern(regexp = "^[A-Za-z0-9_.@-]{4,64}$") String username,
|
||||
@NotBlank @Size(min = 8, max = 64)
|
||||
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password,
|
||||
@NotBlank @Size(max = 64) String displayName,
|
||||
@Size(max = 32) String phone,
|
||||
@Email @Size(max = 128) String email,
|
||||
@NotBlank String memberType,
|
||||
@Size(max = 64) String memberNo,
|
||||
List<Long> roleIds) {
|
||||
public CreateMemberRequest {
|
||||
roleIds = roleIds == null ? List.of() : List.copyOf(roleIds);
|
||||
}
|
||||
}
|
||||
|
||||
public record UpdateMemberRequest(
|
||||
@NotBlank @Size(max = 64) String displayName,
|
||||
@Size(max = 32) String phone,
|
||||
@Email @Size(max = 128) String email,
|
||||
@NotBlank String memberType,
|
||||
@Size(max = 64) String memberNo,
|
||||
List<Long> roleIds) {
|
||||
public UpdateMemberRequest {
|
||||
roleIds = roleIds == null ? List.of() : List.copyOf(roleIds);
|
||||
}
|
||||
}
|
||||
|
||||
public record ChangeMemberStatusRequest(@NotBlank String status) { }
|
||||
|
||||
public record ResetMemberPasswordRequest(@NotBlank @Size(min = 8, max = 64)
|
||||
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String newPassword) { }
|
||||
|
||||
public record MemberResponse(Long membershipId, Long userId, Long authUserId,
|
||||
String displayName, String phone, String email,
|
||||
String memberType, String memberNo, String status, List<Long> roleIds) { }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.iam.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public final class OnboardingDtos {
|
||||
private OnboardingDtos() { }
|
||||
|
||||
public record MerchantRegistrationRequest(
|
||||
@NotNull Long authUserId,
|
||||
@NotBlank String displayName,
|
||||
@NotBlank String companyName,
|
||||
@NotBlank String legalPerson,
|
||||
@NotBlank String legalPhone,
|
||||
String contactEmail,
|
||||
String countryCode,
|
||||
String address
|
||||
) { }
|
||||
|
||||
public record MerchantRegistrationResponse(Long tenantId, Long membershipId, String tenantStatus) { }
|
||||
|
||||
public record QualificationSubmissionRequest(
|
||||
@NotNull Long tenantId,
|
||||
@NotBlank String qualificationType,
|
||||
String certificateNo,
|
||||
@NotBlank String documentUrl
|
||||
) { }
|
||||
|
||||
public record QualificationSubmissionResponse(Long qualificationId, Long reviewId, String tenantStatus) { }
|
||||
|
||||
public record OrderEligibilityResponse(boolean eligible, String tenantStatus, String reason) { }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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) { }
|
||||
|
||||
public record UpdatePersonalProfileRequest(@NotBlank @Size(max = 64) String displayName,
|
||||
@Size(max = 32) String phone,
|
||||
@Email @Size(max = 128) String email) { }
|
||||
|
||||
public record AddressRequest(@NotBlank String addressType, String countryCode, String province,
|
||||
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) { }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.iam.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/** RBAC 管理接口的数据契约。 */
|
||||
public final class RbacDtos {
|
||||
private RbacDtos() { }
|
||||
|
||||
public record CreateRoleRequest(@NotBlank String roleCode,
|
||||
@NotBlank String roleName,
|
||||
String description) { }
|
||||
|
||||
public record UpdateRoleRequest(@NotBlank String roleName,
|
||||
String description) { }
|
||||
|
||||
public record ChangeRoleStatusRequest(
|
||||
@NotBlank @Pattern(regexp = "ACTIVE|DISABLED", message = "角色状态只能是 ACTIVE 或 DISABLED")
|
||||
String status) { }
|
||||
|
||||
/** 角色回显模型,permissionIds 可直接用于全量授权保存。 */
|
||||
public record RoleResponse(Long id, Long tenantId, String appCode, String roleCode,
|
||||
String roleName, String roleType, String status,
|
||||
String description, Set<Long> permissionIds) { }
|
||||
|
||||
public record MemberRolesResponse(Long membershipId, Set<Long> roleIds) { }
|
||||
|
||||
public record CreatePermissionRequest(
|
||||
@Schema(allowableValues = {"MENU", "PAGE", "BUTTON", "API"}) @NotBlank String type,
|
||||
@NotBlank String code,
|
||||
@NotBlank String name,
|
||||
Long parentId,
|
||||
String routePath,
|
||||
String httpMethod,
|
||||
Integer sortNo) { }
|
||||
|
||||
public record AssignRolesRequest(@NotNull Long membershipId,
|
||||
@NotNull Set<Long> roleIds) { }
|
||||
|
||||
public record GrantPermissionsRequest(@NotNull Long roleId,
|
||||
@NotNull Set<Long> permissionIds) { }
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.baiyue.iam.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
/**
|
||||
* 租户生命周期数据契约集合,定义入驻、资料更新、审核请求和租户响应。
|
||||
*/
|
||||
public final class TenantDtos {
|
||||
private TenantDtos() { }
|
||||
|
||||
@Schema(name = "TenantRegisterRequest", description = "Enterprise tenant registration / 企业租户注册资料")
|
||||
public record RegisterRequest(
|
||||
@Schema(description = "Enterprise display name / 企业名称", example = "深圳虫虫出海科技有限公司") @NotBlank String tenantName,
|
||||
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
|
||||
@Schema(description = "18-character unified social credit code / 18 位统一社会信用代码", example = "91440300MA5EXAMPLE") @NotBlank @Pattern(regexp = "[0-9A-Z]{18}") String unifiedSocialCreditCode,
|
||||
@Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName,
|
||||
@Schema(description = "Contact phone / 联系电话", example = "13800138000") @NotBlank String contactPhone,
|
||||
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") @Email String contactEmail
|
||||
) { }
|
||||
|
||||
@Schema(name = "TenantProfileUpdateRequest", description = "Enterprise profile update / 企业资料更新")
|
||||
public record UpdateProfileRequest(
|
||||
@Schema(description = "Enterprise display name / 企业名称", example = "深圳虫虫出海科技有限公司") @NotBlank String tenantName,
|
||||
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
|
||||
@Schema(description = "Primary contact / 联系人", example = "李四") @NotBlank String contactName,
|
||||
@Schema(description = "Contact phone / 联系电话", example = "13900139000") @NotBlank String contactPhone,
|
||||
@Schema(description = "Contact email / 联系邮箱", example = "service@example.com") @Email String contactEmail
|
||||
) { }
|
||||
|
||||
@Schema(name = "TenantReviewRequest", description = "Enterprise review comment / 企业审核意见")
|
||||
public record ReviewRequest(
|
||||
@Schema(description = "Approval or rejection comment / 审核意见", example = "资料完整,审核通过") String comment
|
||||
) { }
|
||||
|
||||
@Schema(name = "TenantResponse", description = "Enterprise tenant summary / 企业租户概要")
|
||||
public record TenantResponse(Long id, String tenantCode, String tenantName, String status,
|
||||
String legalName, String unifiedSocialCreditCode, String contactName,
|
||||
String contactPhone, String contactEmail) { }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.baiyue.iam.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/** 角色数据范围规则,仅用于保证角色删除时关联数据一致清理。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("iam_data_scope")
|
||||
public class IamDataScope {
|
||||
@TableId private Long id;
|
||||
private Long roleId;
|
||||
private Long tenantId;
|
||||
private String scopeType;
|
||||
private String scopeExpression;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.baiyue.iam.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import com.baiyue.common.core.model.BaseEntity; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.time.LocalDateTime;
|
||||
/**
|
||||
* 租户成员关系实体,对应表 {@code iam_membership};关联用户与租户并保存成员类型和状态,是租户内授权的主体。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("iam_membership") public class IamMembership extends BaseEntity {
|
||||
@TableId private Long id; private Long tenantId; private Long userId; private String memberType; private String memberNo; private String status; private LocalDateTime joinedAt; private LocalDateTime leftAt;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.baiyue.iam.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import com.baomidou.mybatisplus.annotation.TableName; import java.time.LocalDateTime;
|
||||
/**
|
||||
* 成员角色关联实体,对应表 {@code iam_membership_role};在租户维度记录成员获授角色的多对多关系。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("iam_membership_role") public class IamMembershipRole {
|
||||
private Long membershipId; private Long roleId; private Long tenantId; private LocalDateTime createdAt;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.baiyue.iam.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
/**
|
||||
* 权限资源实体,对应表 {@code iam_permission};按租户和应用域定义菜单、按钮或接口权限编码及资源属性。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("iam_permission")
|
||||
public class IamPermission {
|
||||
@TableId
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private String appCode;
|
||||
private String permissionType;
|
||||
private Long parentId;
|
||||
private String permissionCode;
|
||||
private String permissionName;
|
||||
private String routePath;
|
||||
private String httpMethod;
|
||||
private Integer sortNo;
|
||||
private String status;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.baiyue.iam.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import com.baiyue.common.core.model.BaseEntity;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
/**
|
||||
* 角色实体,对应表 {@code iam_role};按租户和应用域维护平台端或商户端角色,实现两套权限体系相互隔离。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("iam_role")
|
||||
public class IamRole extends BaseEntity {
|
||||
@TableId
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private String appCode;
|
||||
private String roleCode;
|
||||
private String roleName;
|
||||
private String roleType;
|
||||
private String status;
|
||||
private String description;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.baiyue.iam.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import com.baomidou.mybatisplus.annotation.TableName; import java.time.LocalDateTime;
|
||||
/**
|
||||
* 角色权限关联实体,对应表 {@code iam_role_permission};在租户维度记录角色获授菜单、按钮或接口权限的关系。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("iam_role_permission") public class IamRolePermission {
|
||||
private Long roleId; private Long permissionId; private Long tenantId; private LocalDateTime createdAt;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.baiyue.iam.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import com.baiyue.common.core.model.BaseEntity;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 租户主数据实体,对应表 {@code iam_tenant};保存商户租户编码、企业信息和生命周期状态,是 SaaS 租户隔离的核心边界。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("iam_tenant")
|
||||
public class IamTenant extends BaseEntity {
|
||||
@TableId private Long id;
|
||||
private String tenantCode;
|
||||
private String tenantName;
|
||||
private String tenantType;
|
||||
private String status;
|
||||
private String legalName;
|
||||
private String unifiedSocialCreditCode;
|
||||
private String contactName;
|
||||
private String contactPhone;
|
||||
private String contactEmail;
|
||||
private LocalDateTime approvedAt;
|
||||
private Long approvedBy;
|
||||
private String rejectReason;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.baiyue.iam.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName;
|
||||
/**
|
||||
* 租户地址实体,对应表 {@code iam_tenant_address};按租户保存经营、提货或联系地址及默认地址标识。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("iam_tenant_address") public class IamTenantAddress { @TableId private Long id; private Long tenantId; private String addressType; private String countryCode; private String province; private String city; private String district; private String detail; private String postalCode; private Boolean isDefault; }
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.baiyue.iam.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.time.LocalDate;
|
||||
/**
|
||||
* 租户资质实体,对应表 {@code iam_tenant_qualification};按租户保存营业执照等证照、有效期、文件位置和审核状态。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("iam_tenant_qualification") public class IamTenantQualification { @TableId private Long id; private Long tenantId; private String qualificationType; private String certificateNo; private String documentUrl; private LocalDate validFrom; private LocalDate validTo; private String status; private String reviewComment; }
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.baiyue.iam.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.time.LocalDateTime;
|
||||
/**
|
||||
* 租户审核实体,对应表 {@code iam_tenant_review};记录商户入驻或资料变更的审核单、审核人、结论和意见。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("iam_tenant_review")
|
||||
public class IamTenantReview {
|
||||
@TableId private Long id; private Long tenantId; private String reviewType; private String status; private Long reviewerId; private String reviewComment; private LocalDateTime reviewedAt; private LocalDateTime createdAt;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.baiyue.iam.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName;
|
||||
/**
|
||||
* IAM 用户档案实体,对应表 {@code iam_user};关联认证账号并保存内部或商户人员的展示名、联系方式和启停状态。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("iam_user") public class IamUser { @TableId private Long id; private Long authUserId; private String userType; private String displayName; private String phone; private String email; private String status; }
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.baiyue.iam.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 授权快照只读查询 Mapper,聚合查询成员角色、权限编码和数据范围,避免在认证流程逐表读取。
|
||||
*/
|
||||
@Mapper
|
||||
public interface AuthorizationQueryMapper {
|
||||
@Select("""
|
||||
SELECT COUNT(1)
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
WHERE u.auth_user_id = #{userId}
|
||||
AND u.status = 'ACTIVE' AND u.deleted = 0
|
||||
AND m.id = #{membershipId} AND m.tenant_id = #{tenantId}
|
||||
AND m.status = 'ACTIVE' AND m.deleted = 0
|
||||
""")
|
||||
int countActiveIdentity(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT r.role_code
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
JOIN iam_membership_role mr ON mr.membership_id = m.id AND mr.tenant_id = m.tenant_id
|
||||
JOIN iam_role r ON r.id = mr.role_id
|
||||
WHERE u.auth_user_id = #{userId}
|
||||
AND m.id = #{membershipId} AND m.tenant_id = #{tenantId}
|
||||
AND (r.tenant_id = 0 OR r.tenant_id = #{tenantId})
|
||||
AND r.app_code = #{appCode}
|
||||
AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
ORDER BY r.role_code
|
||||
""")
|
||||
List<String> findRoles(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId,
|
||||
@Param("appCode") String appCode);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT p.permission_code
|
||||
FROM iam_user u
|
||||
JOIN iam_membership m ON m.user_id = u.id
|
||||
JOIN iam_membership_role mr ON mr.membership_id = m.id AND mr.tenant_id = m.tenant_id
|
||||
JOIN iam_role r ON r.id = mr.role_id
|
||||
JOIN iam_role_permission rp ON rp.role_id = r.id
|
||||
JOIN iam_permission p ON p.id = rp.permission_id
|
||||
WHERE u.auth_user_id = #{userId}
|
||||
AND m.id = #{membershipId} AND m.tenant_id = #{tenantId}
|
||||
AND (r.tenant_id = 0 OR r.tenant_id = #{tenantId})
|
||||
AND r.app_code = #{appCode} AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
AND (p.tenant_id = 0 OR p.tenant_id = #{tenantId})
|
||||
AND p.app_code = #{appCode} AND p.status = 'ACTIVE'
|
||||
ORDER BY p.permission_code
|
||||
""")
|
||||
List<String> findPermissions(@Param("userId") long userId,
|
||||
@Param("tenantId") long tenantId,
|
||||
@Param("membershipId") long membershipId,
|
||||
@Param("appCode") String appCode);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.iam.mapper;
|
||||
|
||||
import com.baiyue.iam.entity.IamDataScope;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/** 角色数据范围 Mapper。 */
|
||||
@Mapper
|
||||
public interface IamDataScopeMapper extends BaseMapper<IamDataScope> { }
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 租户成员关系 MyBatis-Plus Mapper,负责 {@code iam_membership} 表的基础持久化操作。
|
||||
*/
|
||||
package com.baiyue.iam.mapper; import com.baiyue.iam.entity.IamMembership; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IamMembershipMapper extends BaseMapper<IamMembership>{}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 成员角色关联 MyBatis-Plus Mapper,负责 {@code iam_membership_role} 表的基础持久化操作。
|
||||
*/
|
||||
package com.baiyue.iam.mapper; import com.baiyue.iam.entity.IamMembershipRole; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IamMembershipRoleMapper extends BaseMapper<IamMembershipRole>{}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 权限资源 MyBatis-Plus Mapper,负责 {@code iam_permission} 表的基础持久化操作。
|
||||
*/
|
||||
package com.baiyue.iam.mapper; import com.baiyue.iam.entity.IamPermission; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IamPermissionMapper extends BaseMapper<IamPermission>{}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 角色 MyBatis-Plus Mapper,负责 {@code iam_role} 表的基础持久化操作。
|
||||
*/
|
||||
package com.baiyue.iam.mapper; import com.baiyue.iam.entity.IamRole; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IamRoleMapper extends BaseMapper<IamRole>{}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 角色权限关联 MyBatis-Plus Mapper,负责 {@code iam_role_permission} 表的基础持久化操作。
|
||||
*/
|
||||
package com.baiyue.iam.mapper; import com.baiyue.iam.entity.IamRolePermission; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IamRolePermissionMapper extends BaseMapper<IamRolePermission>{}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 租户地址 MyBatis-Plus Mapper,负责 {@code iam_tenant_address} 表的基础持久化操作。
|
||||
*/
|
||||
package com.baiyue.iam.mapper; import com.baiyue.iam.entity.IamTenantAddress; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IamTenantAddressMapper extends BaseMapper<IamTenantAddress>{}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 租户主数据 MyBatis-Plus Mapper,负责 {@code iam_tenant} 表的基础持久化操作。
|
||||
*/
|
||||
package com.baiyue.iam.mapper; import com.baiyue.iam.entity.IamTenant; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IamTenantMapper extends BaseMapper<IamTenant>{}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 租户资质 MyBatis-Plus Mapper,负责 {@code iam_tenant_qualification} 表的基础持久化操作。
|
||||
*/
|
||||
package com.baiyue.iam.mapper; import com.baiyue.iam.entity.IamTenantQualification; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IamTenantQualificationMapper extends BaseMapper<IamTenantQualification>{}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 租户审核 MyBatis-Plus Mapper,负责 {@code iam_tenant_review} 表的基础持久化操作。
|
||||
*/
|
||||
package com.baiyue.iam.mapper; import com.baiyue.iam.entity.IamTenantReview; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IamTenantReviewMapper extends BaseMapper<IamTenantReview>{}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* IAM 用户档案 MyBatis-Plus Mapper,负责 {@code iam_user} 表的基础持久化操作。
|
||||
*/
|
||||
package com.baiyue.iam.mapper; import com.baiyue.iam.entity.IamUser; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IamUserMapper extends BaseMapper<IamUser>{}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import com.baiyue.iam.entity.IamMembership;
|
||||
import com.baiyue.iam.entity.IamMembershipRole;
|
||||
import com.baiyue.iam.entity.IamUser;
|
||||
import com.baiyue.iam.mapper.IamMembershipMapper;
|
||||
import com.baiyue.iam.mapper.IamMembershipRoleMapper;
|
||||
import com.baiyue.iam.mapper.IamUserMapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 企业成员仓储,所有成员查询均显式携带租户条件。 */
|
||||
@Repository
|
||||
public class MemberRepository {
|
||||
private final IamMembershipMapper membershipMapper;
|
||||
private final IamUserMapper userMapper;
|
||||
private final IamMembershipRoleMapper roleMapper;
|
||||
|
||||
public MemberRepository(IamMembershipMapper membershipMapper, IamUserMapper userMapper,
|
||||
IamMembershipRoleMapper roleMapper) {
|
||||
this.membershipMapper = membershipMapper;
|
||||
this.userMapper = userMapper;
|
||||
this.roleMapper = roleMapper;
|
||||
}
|
||||
|
||||
public List<IamMembership> findByTenant(long tenantId) {
|
||||
return membershipMapper.selectList(new LambdaQueryWrapper<IamMembership>()
|
||||
.eq(IamMembership::getTenantId, tenantId).orderByDesc(IamMembership::getJoinedAt));
|
||||
}
|
||||
public IamMembership findById(Long id) { return membershipMapper.selectById(id); }
|
||||
public IamMembership findByTenantAndId(long tenantId, Long id) {
|
||||
return membershipMapper.selectOne(new LambdaQueryWrapper<IamMembership>()
|
||||
.eq(IamMembership::getTenantId, tenantId).eq(IamMembership::getId, id).last("LIMIT 1"));
|
||||
}
|
||||
public IamMembership findByTenantAndAuthUser(long tenantId, Long authUserId) {
|
||||
IamUser user = findUserByAuthId(authUserId);
|
||||
if (user == null) return null;
|
||||
return membershipMapper.selectOne(new LambdaQueryWrapper<IamMembership>()
|
||||
.eq(IamMembership::getTenantId, tenantId).eq(IamMembership::getUserId, user.getId()).last("LIMIT 1"));
|
||||
}
|
||||
public boolean exists(long tenantId, Long userId) {
|
||||
return membershipMapper.selectCount(new LambdaQueryWrapper<IamMembership>()
|
||||
.eq(IamMembership::getTenantId, tenantId).eq(IamMembership::getUserId, userId)) > 0;
|
||||
}
|
||||
public boolean existsMemberNo(long tenantId, String memberNo, Long excludedMembershipId) {
|
||||
if (memberNo == null || memberNo.isBlank()) return false;
|
||||
LambdaQueryWrapper<IamMembership> query = new LambdaQueryWrapper<IamMembership>()
|
||||
.eq(IamMembership::getTenantId, tenantId).eq(IamMembership::getMemberNo, memberNo);
|
||||
if (excludedMembershipId != null) query.ne(IamMembership::getId, excludedMembershipId);
|
||||
return membershipMapper.selectCount(query) > 0;
|
||||
}
|
||||
public IamUser findUser(Long id) { return userMapper.selectById(id); }
|
||||
public IamUser findUserByAuthId(Long authId) {
|
||||
return userMapper.selectOne(new LambdaQueryWrapper<IamUser>()
|
||||
.eq(IamUser::getAuthUserId, authId).last("LIMIT 1"));
|
||||
}
|
||||
public List<Long> findRoleIds(long tenantId, Long membershipId) {
|
||||
return roleMapper.selectList(new LambdaQueryWrapper<IamMembershipRole>()
|
||||
.eq(IamMembershipRole::getTenantId, tenantId)
|
||||
.eq(IamMembershipRole::getMembershipId, membershipId))
|
||||
.stream().map(IamMembershipRole::getRoleId).toList();
|
||||
}
|
||||
public void replaceRoles(long tenantId, Long membershipId, List<Long> roleIds) {
|
||||
roleMapper.delete(new QueryWrapper<IamMembershipRole>().eq("tenant_id", tenantId)
|
||||
.eq("membership_id", membershipId));
|
||||
for (Long roleId : roleIds) {
|
||||
IamMembershipRole item = new IamMembershipRole();
|
||||
item.setTenantId(tenantId); item.setMembershipId(membershipId); item.setRoleId(roleId);
|
||||
item.setCreatedAt(java.time.LocalDateTime.now()); roleMapper.insert(item);
|
||||
}
|
||||
}
|
||||
public void insert(IamMembership value) { membershipMapper.insert(value); }
|
||||
public void save(IamMembership value) { membershipMapper.updateById(value); }
|
||||
public void insertUser(IamUser value) { userMapper.insert(value); }
|
||||
public void saveUser(IamUser value) { userMapper.updateById(value); }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import com.baiyue.iam.entity.IamTenantAddress;
|
||||
import com.baiyue.iam.entity.IamTenantQualification;
|
||||
import com.baiyue.iam.entity.IamUser;
|
||||
import com.baiyue.iam.mapper.IamTenantAddressMapper;
|
||||
import com.baiyue.iam.mapper.IamTenantQualificationMapper;
|
||||
import com.baiyue.iam.mapper.IamUserMapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IAM 档案资源仓储,封装用户档案、租户地址和租户资质的写入与分租户查询。
|
||||
*/
|
||||
@Repository
|
||||
public class ProfileResourceRepository {
|
||||
private final IamUserMapper userMapper;
|
||||
private final IamTenantAddressMapper addressMapper;
|
||||
private final IamTenantQualificationMapper qualificationMapper;
|
||||
|
||||
public ProfileResourceRepository(IamUserMapper userMapper, IamTenantAddressMapper addressMapper,
|
||||
IamTenantQualificationMapper qualificationMapper) {
|
||||
this.userMapper = userMapper;
|
||||
this.addressMapper = addressMapper;
|
||||
this.qualificationMapper = qualificationMapper;
|
||||
}
|
||||
|
||||
public void insertUser(IamUser user) { userMapper.insert(user); }
|
||||
public IamUser findUserByAuthId(Long authUserId) { return userMapper.selectOne(new LambdaQueryWrapper<IamUser>().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<IamUser> findInternalUsers() { return userMapper.selectList(new LambdaQueryWrapper<IamUser>().eq(IamUser::getUserType, "INTERNAL")); }
|
||||
public List<IamTenantAddress> findAddresses(long tenantId) { return addressMapper.selectList(new LambdaQueryWrapper<IamTenantAddress>().eq(IamTenantAddress::getTenantId, tenantId)); }
|
||||
public List<IamTenantQualification> findQualifications(long tenantId) { return qualificationMapper.selectList(new LambdaQueryWrapper<IamTenantQualification>().eq(IamTenantQualification::getTenantId, tenantId)); }
|
||||
public long countApprovedQualifications(long tenantId) {
|
||||
return qualificationMapper.selectCount(new LambdaQueryWrapper<IamTenantQualification>()
|
||||
.eq(IamTenantQualification::getTenantId, tenantId)
|
||||
.eq(IamTenantQualification::getStatus, "APPROVED"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import com.baiyue.iam.entity.*;
|
||||
import com.baiyue.iam.mapper.*;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/** RBAC 仓储,所有业务查询均显式携带租户和应用域。 */
|
||||
@Repository
|
||||
public class RbacRepository {
|
||||
private final IamRoleMapper roleMapper;
|
||||
private final IamPermissionMapper permissionMapper;
|
||||
private final IamMembershipRoleMapper membershipRoleMapper;
|
||||
private final IamRolePermissionMapper rolePermissionMapper;
|
||||
private final IamDataScopeMapper dataScopeMapper;
|
||||
|
||||
public RbacRepository(IamRoleMapper roleMapper, IamPermissionMapper permissionMapper,
|
||||
IamMembershipRoleMapper membershipRoleMapper,
|
||||
IamRolePermissionMapper rolePermissionMapper,
|
||||
IamDataScopeMapper dataScopeMapper) {
|
||||
this.roleMapper = roleMapper;
|
||||
this.permissionMapper = permissionMapper;
|
||||
this.membershipRoleMapper = membershipRoleMapper;
|
||||
this.rolePermissionMapper = rolePermissionMapper;
|
||||
this.dataScopeMapper = dataScopeMapper;
|
||||
}
|
||||
|
||||
public IamRole findRole(Long id, long tenantId, String appCode) {
|
||||
return roleMapper.selectOne(new LambdaQueryWrapper<IamRole>()
|
||||
.eq(IamRole::getId, id).eq(IamRole::getTenantId, tenantId)
|
||||
.eq(IamRole::getAppCode, appCode));
|
||||
}
|
||||
|
||||
public IamPermission findPermission(Long id, long tenantId, String appCode) {
|
||||
return permissionMapper.selectOne(new LambdaQueryWrapper<IamPermission>()
|
||||
.eq(IamPermission::getId, id)
|
||||
.in(IamPermission::getTenantId, List.of(0L, tenantId))
|
||||
.eq(IamPermission::getAppCode, appCode));
|
||||
}
|
||||
|
||||
public void insertRole(IamRole role) { roleMapper.insert(role); }
|
||||
public void updateRole(IamRole role) { roleMapper.updateById(role); }
|
||||
public void insertPermission(IamPermission permission) { permissionMapper.insert(permission); }
|
||||
|
||||
public List<IamRole> findRoles(long tenantId, String appCode) {
|
||||
return roleMapper.selectList(new LambdaQueryWrapper<IamRole>()
|
||||
.eq(IamRole::getTenantId, tenantId).eq(IamRole::getAppCode, appCode)
|
||||
.orderByAsc(IamRole::getRoleCode));
|
||||
}
|
||||
|
||||
public boolean existsRoleCode(long tenantId, String appCode, String roleCode, Long excludeId) {
|
||||
LambdaQueryWrapper<IamRole> query = new LambdaQueryWrapper<IamRole>()
|
||||
.eq(IamRole::getTenantId, tenantId).eq(IamRole::getAppCode, appCode)
|
||||
.eq(IamRole::getRoleCode, roleCode);
|
||||
if (excludeId != null) query.ne(IamRole::getId, excludeId);
|
||||
return roleMapper.selectCount(query) > 0;
|
||||
}
|
||||
|
||||
public List<IamPermission> findPermissions(long tenantId, String appCode) {
|
||||
return permissionMapper.selectList(new LambdaQueryWrapper<IamPermission>()
|
||||
.in(IamPermission::getTenantId, List.of(0L, tenantId))
|
||||
.eq(IamPermission::getAppCode, appCode)
|
||||
.orderByAsc(IamPermission::getPermissionCode));
|
||||
}
|
||||
|
||||
public boolean existsPermissionCode(long tenantId, String appCode, String permissionCode) {
|
||||
return permissionMapper.selectCount(new LambdaQueryWrapper<IamPermission>()
|
||||
.eq(IamPermission::getTenantId, tenantId)
|
||||
.eq(IamPermission::getAppCode, appCode)
|
||||
.eq(IamPermission::getPermissionCode, permissionCode)) > 0;
|
||||
}
|
||||
|
||||
public Set<Long> findRolePermissionIds(long tenantId, Long roleId) {
|
||||
List<IamRolePermission> rows = rolePermissionMapper.selectList(
|
||||
new LambdaQueryWrapper<IamRolePermission>()
|
||||
.eq(IamRolePermission::getTenantId, tenantId)
|
||||
.eq(IamRolePermission::getRoleId, roleId)
|
||||
.orderByAsc(IamRolePermission::getPermissionId));
|
||||
Set<Long> result = new LinkedHashSet<>();
|
||||
rows.forEach(row -> result.add(row.getPermissionId()));
|
||||
return result;
|
||||
}
|
||||
|
||||
public Set<Long> findMemberRoleIds(long tenantId, Long membershipId) {
|
||||
List<IamMembershipRole> rows = membershipRoleMapper.selectList(
|
||||
new LambdaQueryWrapper<IamMembershipRole>()
|
||||
.eq(IamMembershipRole::getTenantId, tenantId)
|
||||
.eq(IamMembershipRole::getMembershipId, membershipId)
|
||||
.orderByAsc(IamMembershipRole::getRoleId));
|
||||
Set<Long> result = new LinkedHashSet<>();
|
||||
rows.forEach(row -> result.add(row.getRoleId()));
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isRoleAssigned(long tenantId, Long roleId) {
|
||||
return membershipRoleMapper.selectCount(new LambdaQueryWrapper<IamMembershipRole>()
|
||||
.eq(IamMembershipRole::getTenantId, tenantId)
|
||||
.eq(IamMembershipRole::getRoleId, roleId)) > 0;
|
||||
}
|
||||
|
||||
public void deleteRole(long tenantId, String appCode, Long roleId) {
|
||||
rolePermissionMapper.delete(new LambdaQueryWrapper<IamRolePermission>()
|
||||
.eq(IamRolePermission::getTenantId, tenantId)
|
||||
.eq(IamRolePermission::getRoleId, roleId));
|
||||
dataScopeMapper.delete(new LambdaQueryWrapper<IamDataScope>()
|
||||
.eq(IamDataScope::getTenantId, tenantId)
|
||||
.eq(IamDataScope::getRoleId, roleId));
|
||||
roleMapper.delete(new LambdaQueryWrapper<IamRole>()
|
||||
.eq(IamRole::getTenantId, tenantId).eq(IamRole::getAppCode, appCode)
|
||||
.eq(IamRole::getId, roleId));
|
||||
}
|
||||
|
||||
public void replaceMemberRoles(long tenantId, String appCode, Long membershipId, Iterable<Long> roleIds) {
|
||||
// 仅删除当前应用域角色,避免一个应用端保存时误清另一个应用端的角色。
|
||||
List<Long> scopedRoleIds = findRoles(tenantId, appCode).stream().map(IamRole::getId).toList();
|
||||
if (!scopedRoleIds.isEmpty()) {
|
||||
membershipRoleMapper.delete(new LambdaQueryWrapper<IamMembershipRole>()
|
||||
.eq(IamMembershipRole::getTenantId, tenantId)
|
||||
.eq(IamMembershipRole::getMembershipId, membershipId)
|
||||
.in(IamMembershipRole::getRoleId, scopedRoleIds));
|
||||
}
|
||||
for (Long roleId : roleIds) {
|
||||
IamMembershipRole relation = new IamMembershipRole();
|
||||
relation.setMembershipId(membershipId);
|
||||
relation.setRoleId(roleId);
|
||||
relation.setTenantId(tenantId);
|
||||
relation.setCreatedAt(LocalDateTime.now());
|
||||
membershipRoleMapper.insert(relation);
|
||||
}
|
||||
}
|
||||
|
||||
public void replaceRolePermissions(long tenantId, Long roleId, Iterable<Long> permissionIds) {
|
||||
rolePermissionMapper.delete(new LambdaQueryWrapper<IamRolePermission>()
|
||||
.eq(IamRolePermission::getTenantId, tenantId)
|
||||
.eq(IamRolePermission::getRoleId, roleId));
|
||||
for (Long permissionId : permissionIds) {
|
||||
IamRolePermission relation = new IamRolePermission();
|
||||
relation.setRoleId(roleId);
|
||||
relation.setPermissionId(permissionId);
|
||||
relation.setTenantId(tenantId);
|
||||
relation.setCreatedAt(LocalDateTime.now());
|
||||
rolePermissionMapper.insert(relation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
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 org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 租户仓储,封装租户及审核单的唯一性校验、查询、创建、更新和待审列表操作。
|
||||
*/
|
||||
@Repository
|
||||
public class TenantRepository {
|
||||
private final IamTenantMapper tenantMapper;
|
||||
private final IamTenantReviewMapper reviewMapper;
|
||||
|
||||
public TenantRepository(IamTenantMapper tenantMapper, IamTenantReviewMapper reviewMapper) {
|
||||
this.tenantMapper = tenantMapper;
|
||||
this.reviewMapper = reviewMapper;
|
||||
}
|
||||
|
||||
public boolean existsByCreditCode(String creditCode) {
|
||||
return tenantMapper.selectCount(new LambdaQueryWrapper<IamTenant>()
|
||||
.eq(IamTenant::getUnifiedSocialCreditCode, creditCode)) > 0;
|
||||
}
|
||||
|
||||
public IamTenant findTenant(Long tenantId) { return tenantMapper.selectById(tenantId); }
|
||||
public IamTenantReview findReview(Long reviewId) { return reviewMapper.selectById(reviewId); }
|
||||
public IamTenantReview findPendingReviewByTenant(Long tenantId) {
|
||||
return reviewMapper.selectOne(new LambdaQueryWrapper<IamTenantReview>()
|
||||
.eq(IamTenantReview::getTenantId, tenantId)
|
||||
.eq(IamTenantReview::getStatus, "PENDING")
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
public void insertTenant(IamTenant tenant) { tenantMapper.insert(tenant); }
|
||||
public void saveTenant(IamTenant tenant) { tenantMapper.updateById(tenant); }
|
||||
public void insertReview(IamTenantReview review) { reviewMapper.insert(review); }
|
||||
public void saveReview(IamTenantReview review) { reviewMapper.updateById(review); }
|
||||
|
||||
public List<IamTenantReview> findPendingReviews() {
|
||||
return reviewMapper.selectList(new LambdaQueryWrapper<IamTenantReview>()
|
||||
.eq(IamTenantReview::getStatus, "PENDING")
|
||||
.orderByAsc(IamTenantReview::getId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.baiyue.iam.service;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.AuthorizationDtos.SnapshotRequest;
|
||||
import com.baiyue.iam.dto.AuthorizationDtos.SnapshotResponse;
|
||||
import com.baiyue.iam.mapper.AuthorizationQueryMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 授权快照服务,校验账号与成员关系,并聚合指定租户、应用域下的角色、权限和数据范围。
|
||||
*/
|
||||
@Service
|
||||
public class AuthorizationSnapshotService {
|
||||
private static final Set<String> APPS = Set.of("MERCHANT", "PLATFORM");
|
||||
@Resource
|
||||
private AuthorizationQueryMapper mapper;
|
||||
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public SnapshotResponse load(SnapshotRequest request) {
|
||||
String appCode = request.getAppCode().trim().toUpperCase(java.util.Locale.ROOT);
|
||||
if (!APPS.contains(appCode)) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "未知的应用身份域");
|
||||
}
|
||||
if ("MERCHANT".equals(appCode) && request.getTenantId() <= 0) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "商户账号缺少有效租户");
|
||||
}
|
||||
if ("PLATFORM".equals(appCode) && request.getTenantId() != 0) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "平台账号必须属于平台租户");
|
||||
}
|
||||
if (mapper.countActiveIdentity(request.getUserId(), request.getTenantId(), request.getMembershipId()) == 0) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "IAM 用户或成员身份无效");
|
||||
}
|
||||
|
||||
Set<String> roles = new LinkedHashSet<>(mapper.findRoles(
|
||||
request.getUserId(), request.getTenantId(), request.getMembershipId(), appCode));
|
||||
Set<String> permissions = new LinkedHashSet<>(mapper.findPermissions(
|
||||
request.getUserId(), request.getTenantId(), request.getMembershipId(), appCode));
|
||||
return new SnapshotResponse(request.getUserId(), request.getTenantId(), request.getMembershipId(),
|
||||
appCode, appCode, Set.copyOf(roles), Set.copyOf(permissions));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package com.baiyue.iam.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.common.audit.BusinessAudit;
|
||||
import com.baiyue.iam.client.AuthAccountClient;
|
||||
import com.baiyue.iam.dto.MemberDtos.*;
|
||||
import com.baiyue.iam.entity.IamMembership;
|
||||
import com.baiyue.iam.entity.IamRole;
|
||||
import com.baiyue.iam.entity.IamUser;
|
||||
import com.baiyue.iam.mapper.IamRoleMapper;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import feign.FeignException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** 企业成员闭环服务,账号凭据归 auth-service,人员档案与租户关系归 IAM。 */
|
||||
@Service
|
||||
public class MemberService {
|
||||
private static final Logger log = LoggerFactory.getLogger(MemberService.class);
|
||||
private final MemberRepository repository;
|
||||
private final IamRoleMapper roleMapper;
|
||||
private final AuthAccountClient authClient;
|
||||
private final String internalToken;
|
||||
|
||||
public MemberService(MemberRepository repository, IamRoleMapper roleMapper, AuthAccountClient authClient,
|
||||
@Value("${baiyue.security.internal-service-token}") String internalToken) {
|
||||
this.repository = repository;
|
||||
this.roleMapper = roleMapper;
|
||||
this.authClient = authClient;
|
||||
this.internalToken = internalToken;
|
||||
}
|
||||
|
||||
public List<MemberResponse> list(long tenantId) {
|
||||
return repository.findByTenant(tenantId).stream().map(m -> response(tenantId, m)).toList();
|
||||
}
|
||||
|
||||
public MemberResponse detail(long tenantId, Long membershipId) {
|
||||
return response(tenantId, required(tenantId, membershipId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "MEMBER_CREATE", resourceType = "MEMBERSHIP", resourceId = "#request.username", description = "新增企业成员")
|
||||
public MemberResponse create(long tenantId, CreateMemberRequest request) {
|
||||
rejectOwnerMutation(request.memberType());
|
||||
validateRoles(tenantId, request.roleIds());
|
||||
validateMemberNo(tenantId, request.memberNo(), null);
|
||||
long authId = IdWorker.getId();
|
||||
long userId = IdWorker.getId();
|
||||
long membershipId = IdWorker.getId();
|
||||
call(() -> authClient.create(internalToken, new AuthAccountClient.CreateAccountRequest(
|
||||
authId, request.username(), request.password(), request.displayName(), tenantId, membershipId)));
|
||||
try {
|
||||
IamUser user = new IamUser();
|
||||
user.setId(userId); user.setAuthUserId(authId); user.setUserType("ENTERPRISE");
|
||||
user.setDisplayName(request.displayName()); user.setPhone(request.phone()); user.setEmail(request.email());
|
||||
user.setStatus("ACTIVE"); repository.insertUser(user);
|
||||
|
||||
IamMembership membership = new IamMembership();
|
||||
membership.setId(membershipId); membership.setTenantId(tenantId); membership.setUserId(userId);
|
||||
membership.setMemberType(request.memberType()); membership.setMemberNo(request.memberNo());
|
||||
membership.setStatus("ACTIVE"); membership.setJoinedAt(LocalDateTime.now()); repository.insert(membership);
|
||||
repository.replaceRoles(tenantId, membershipId, request.roleIds());
|
||||
return response(tenantId, membership, user);
|
||||
} catch (RuntimeException exception) {
|
||||
try {
|
||||
authClient.compensate(internalToken, authId);
|
||||
} catch (RuntimeException compensationFailure) {
|
||||
log.error("成员创建失败且认证账号补偿失败,authUserId={}", authId, compensationFailure);
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,
|
||||
"成员创建未完成,认证账号补偿失败,请按 authUserId=" + authId + " 进行一致性修复");
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "MEMBER_UPDATE", resourceType = "MEMBERSHIP", resourceId = "#membershipId", description = "编辑企业成员")
|
||||
public MemberResponse update(long tenantId, Long membershipId, UpdateMemberRequest request) {
|
||||
rejectOwnerMutation(request.memberType());
|
||||
validateRoles(tenantId, request.roleIds());
|
||||
validateMemberNo(tenantId, request.memberNo(), membershipId);
|
||||
IamMembership membership = required(tenantId, membershipId);
|
||||
if ("OWNER".equals(membership.getMemberType())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "企业所有者不允许通过成员管理接口编辑");
|
||||
}
|
||||
IamUser user = requiredUser(membership.getUserId());
|
||||
user.setDisplayName(request.displayName()); user.setPhone(request.phone()); user.setEmail(request.email());
|
||||
membership.setMemberType(request.memberType()); membership.setMemberNo(request.memberNo());
|
||||
repository.saveUser(user); repository.save(membership);
|
||||
repository.replaceRoles(tenantId, membershipId, request.roleIds());
|
||||
return response(tenantId, membership);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "MEMBER_STATUS_CHANGE", resourceType = "MEMBERSHIP", resourceId = "#membershipId", description = "启停企业成员")
|
||||
public MemberResponse changeStatus(long tenantId, Long membershipId, ChangeMemberStatusRequest request,
|
||||
Long operatorMembershipId) {
|
||||
if (!"ACTIVE".equals(request.status()) && !"DISABLED".equals(request.status()))
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "成员状态只允许 ACTIVE 或 DISABLED");
|
||||
IamMembership membership = required(tenantId, membershipId);
|
||||
if ("OWNER".equals(membership.getMemberType()) && "DISABLED".equals(request.status()))
|
||||
throw new BizException(ResultCode.FORBIDDEN, "企业所有者不能被停用");
|
||||
if (membershipId.equals(operatorMembershipId) && "DISABLED".equals(request.status()))
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "不能停用当前登录成员");
|
||||
IamUser user = requiredUser(membership.getUserId());
|
||||
String oldStatus = membership.getStatus();
|
||||
call(() -> authClient.status(internalToken, user.getAuthUserId(),
|
||||
new AuthAccountClient.StatusRequest(request.status())));
|
||||
try {
|
||||
membership.setStatus(request.status());
|
||||
membership.setLeftAt("DISABLED".equals(request.status()) ? LocalDateTime.now() : null);
|
||||
user.setStatus(request.status()); repository.saveUser(user); repository.save(membership);
|
||||
} catch (RuntimeException localFailure) {
|
||||
try {
|
||||
authClient.status(internalToken, user.getAuthUserId(), new AuthAccountClient.StatusRequest(oldStatus));
|
||||
} catch (RuntimeException compensationFailure) {
|
||||
log.error("成员状态写入失败且认证账号状态补偿失败,authUserId={}", user.getAuthUserId(), compensationFailure);
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,
|
||||
"成员状态未完成,认证账号补偿失败,请按 authUserId=" + user.getAuthUserId() + " 进行一致性修复");
|
||||
}
|
||||
throw localFailure;
|
||||
}
|
||||
return response(tenantId, membership);
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "MEMBER_PASSWORD_RESET", resourceType = "MEMBERSHIP", resourceId = "#membershipId", description = "管理员重置成员密码", captureAfter = false)
|
||||
public void resetPassword(long tenantId, Long membershipId, ResetMemberPasswordRequest request) {
|
||||
IamMembership membership = required(tenantId, membershipId);
|
||||
if ("OWNER".equals(membership.getMemberType())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "企业所有者只能自行修改密码");
|
||||
}
|
||||
IamUser user = requiredUser(membership.getUserId());
|
||||
call(() -> authClient.reset(internalToken, user.getAuthUserId(),
|
||||
new AuthAccountClient.PasswordRequest(request.newPassword())));
|
||||
}
|
||||
|
||||
private MemberResponse response(long tenantId, IamMembership membership) {
|
||||
IamUser user = requiredUser(membership.getUserId());
|
||||
return response(tenantId, membership, user);
|
||||
}
|
||||
private MemberResponse response(long tenantId, IamMembership m, IamUser u) {
|
||||
return new MemberResponse(m.getId(), u.getId(), u.getAuthUserId(), u.getDisplayName(),
|
||||
u.getPhone(), u.getEmail(), m.getMemberType(), m.getMemberNo(), m.getStatus(),
|
||||
repository.findRoleIds(tenantId, m.getId()));
|
||||
}
|
||||
private IamMembership required(long tenantId, Long membershipId) {
|
||||
IamMembership value = repository.findByTenantAndId(tenantId, membershipId);
|
||||
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "成员不存在");
|
||||
return value;
|
||||
}
|
||||
private IamUser requiredUser(Long id) {
|
||||
IamUser user = repository.findUser(id);
|
||||
if (user == null) throw new BizException(ResultCode.NOT_FOUND, "成员档案不存在");
|
||||
return user;
|
||||
}
|
||||
private void validateRoles(long tenantId, List<Long> ids) {
|
||||
for (Long id : ids) {
|
||||
IamRole role = roleMapper.selectById(id);
|
||||
if (role == null || role.getTenantId() == null || role.getTenantId() != tenantId
|
||||
|| !"MERCHANT".equals(role.getAppCode()) || !"ACTIVE".equals(role.getStatus())
|
||||
|| "MERCHANT_OWNER".equals(role.getRoleCode()))
|
||||
throw new BizException(ResultCode.FORBIDDEN, "禁止分配其他租户、其他应用或已停用角色");
|
||||
}
|
||||
}
|
||||
private void rejectOwnerMutation(String memberType) {
|
||||
if ("OWNER".equals(memberType)) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "不能通过成员管理接口创建或转让企业所有者");
|
||||
}
|
||||
if (!"EMPLOYEE".equals(memberType)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "成员类型只能是 EMPLOYEE");
|
||||
}
|
||||
}
|
||||
private void validateMemberNo(long tenantId, String memberNo, Long excludedId) {
|
||||
if (repository.existsMemberNo(tenantId, memberNo, excludedId))
|
||||
throw new BizException(ResultCode.CONFLICT, "企业成员编号已存在");
|
||||
}
|
||||
private <T> ApiResponse<T> call(RemoteCall<T> action) {
|
||||
try {
|
||||
ApiResponse<T> result = action.call();
|
||||
if (result == null || !result.successful()) throw unavailable();
|
||||
return result;
|
||||
} catch (FeignException exception) {
|
||||
if (exception.status() == 409) throw new BizException(ResultCode.CONFLICT, "登录账号已存在");
|
||||
if (exception.status() == 404) throw new BizException(ResultCode.NOT_FOUND, "认证账号不存在");
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
private <T> T data(ApiResponse<T> value) { return value.data(); }
|
||||
private BizException unavailable() { return new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用"); }
|
||||
@FunctionalInterface private interface RemoteCall<T> { ApiResponse<T> call(); }
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.OnboardingDtos.MerchantRegistrationRequest;
|
||||
import com.baiyue.iam.dto.OnboardingDtos.MerchantRegistrationResponse;
|
||||
import com.baiyue.iam.entity.IamMembership;
|
||||
import com.baiyue.iam.entity.IamMembershipRole;
|
||||
import com.baiyue.iam.entity.IamRole;
|
||||
import com.baiyue.iam.entity.IamTenant;
|
||||
import com.baiyue.iam.entity.IamTenantAddress;
|
||||
import com.baiyue.iam.entity.IamUser;
|
||||
import com.baiyue.iam.mapper.IamMembershipMapper;
|
||||
import com.baiyue.iam.mapper.IamMembershipRoleMapper;
|
||||
import com.baiyue.iam.mapper.IamRoleMapper;
|
||||
import com.baiyue.iam.mapper.IamTenantAddressMapper;
|
||||
import com.baiyue.iam.mapper.IamTenantMapper;
|
||||
import com.baiyue.iam.mapper.IamUserMapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class MerchantOnboardingService {
|
||||
@Resource private IamTenantMapper tenantMapper;
|
||||
@Resource private IamUserMapper userMapper;
|
||||
@Resource private IamMembershipMapper membershipMapper;
|
||||
@Resource private IamRoleMapper roleMapper;
|
||||
@Resource private IamMembershipRoleMapper membershipRoleMapper;
|
||||
@Resource private IamTenantAddressMapper addressMapper;
|
||||
|
||||
@Transactional
|
||||
public MerchantRegistrationResponse register(MerchantRegistrationRequest request) {
|
||||
if (userMapper.selectCount(new LambdaQueryWrapper<IamUser>()
|
||||
.eq(IamUser::getAuthUserId, request.authUserId())) > 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Registration already exists");
|
||||
}
|
||||
IamRole ownerRole = roleMapper.selectOne(new LambdaQueryWrapper<IamRole>()
|
||||
.eq(IamRole::getTenantId, 0L)
|
||||
.eq(IamRole::getAppCode, "MERCHANT")
|
||||
.eq(IamRole::getRoleCode, "MERCHANT_OWNER")
|
||||
.eq(IamRole::getStatus, "ACTIVE")
|
||||
.last("LIMIT 1"));
|
||||
if (ownerRole == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "Merchant role template is not initialized");
|
||||
}
|
||||
|
||||
long tenantId = IdWorker.getId();
|
||||
long userId = IdWorker.getId();
|
||||
long membershipId = IdWorker.getId();
|
||||
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(tenantId);
|
||||
tenant.setTenantCode("TEN" + tenantId);
|
||||
tenant.setTenantName(request.companyName());
|
||||
tenant.setTenantType("MERCHANT");
|
||||
tenant.setStatus("PROFILE_REQUIRED");
|
||||
tenant.setLegalName(request.companyName());
|
||||
tenant.setContactName(request.legalPerson());
|
||||
tenant.setContactPhone(request.legalPhone());
|
||||
tenant.setContactEmail(request.contactEmail());
|
||||
tenant.setDeleted(false);
|
||||
tenant.setVersion(0);
|
||||
tenantMapper.insert(tenant);
|
||||
|
||||
IamUser user = new IamUser();
|
||||
user.setId(userId);
|
||||
user.setAuthUserId(request.authUserId());
|
||||
user.setUserType("ENTERPRISE");
|
||||
user.setDisplayName(request.displayName());
|
||||
user.setPhone(request.legalPhone());
|
||||
user.setEmail(request.contactEmail());
|
||||
user.setStatus("ACTIVE");
|
||||
userMapper.insert(user);
|
||||
|
||||
IamMembership membership = new IamMembership();
|
||||
membership.setId(membershipId);
|
||||
membership.setTenantId(tenantId);
|
||||
membership.setUserId(userId);
|
||||
membership.setMemberType("OWNER");
|
||||
membership.setMemberNo("OWNER-" + membershipId);
|
||||
membership.setStatus("ACTIVE");
|
||||
membership.setJoinedAt(LocalDateTime.now());
|
||||
membership.setCreatedBy(request.authUserId());
|
||||
membership.setUpdatedBy(request.authUserId());
|
||||
membership.setDeleted(false);
|
||||
membership.setVersion(0);
|
||||
membershipMapper.insert(membership);
|
||||
|
||||
IamMembershipRole membershipRole = new IamMembershipRole();
|
||||
membershipRole.setMembershipId(membershipId);
|
||||
membershipRole.setRoleId(ownerRole.getId());
|
||||
membershipRole.setTenantId(tenantId);
|
||||
membershipRole.setCreatedAt(LocalDateTime.now());
|
||||
membershipRoleMapper.insert(membershipRole);
|
||||
|
||||
if (StringUtils.hasText(request.address())) {
|
||||
IamTenantAddress address = new IamTenantAddress();
|
||||
address.setId(IdWorker.getId());
|
||||
address.setTenantId(tenantId);
|
||||
address.setAddressType("REGISTERED");
|
||||
address.setCountryCode(StringUtils.hasText(request.countryCode())
|
||||
? request.countryCode().toUpperCase() : "CN");
|
||||
address.setDetail(request.address());
|
||||
address.setIsDefault(true);
|
||||
addressMapper.insert(address);
|
||||
}
|
||||
return new MerchantRegistrationResponse(tenantId, membershipId, tenant.getStatus());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.ProfileDtos.PersonalProfileResponse;
|
||||
import com.baiyue.iam.dto.ProfileDtos.UpdatePersonalProfileRequest;
|
||||
import com.baiyue.iam.entity.IamMembership;
|
||||
import com.baiyue.iam.entity.IamUser;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
|
||||
/** 当前用户个人资料服务,必须同时匹配认证账号与当前租户成员关系。 */
|
||||
@Service
|
||||
public class PersonalProfileService {
|
||||
private final MemberRepository repository;
|
||||
public PersonalProfileService(MemberRepository repository) { this.repository = repository; }
|
||||
|
||||
public PersonalProfileResponse get(long tenantId, Long authUserId, String username) {
|
||||
return response(tenantId, authUserId, username);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PERSONAL_PROFILE_UPDATE", resourceType = "IAM_USER", resourceId = "#authUserId", description = "编辑个人资料")
|
||||
public PersonalProfileResponse update(long tenantId, Long authUserId, String username,
|
||||
UpdatePersonalProfileRequest request) {
|
||||
IamMembership membership = repository.findByTenantAndAuthUser(tenantId, authUserId);
|
||||
if (membership == null || !"ACTIVE".equals(membership.getStatus())) throw forbidden();
|
||||
IamUser user = repository.findUser(membership.getUserId());
|
||||
if (user == null || !authUserId.equals(user.getAuthUserId())) throw forbidden();
|
||||
user.setDisplayName(request.displayName()); user.setPhone(request.phone()); user.setEmail(request.email());
|
||||
repository.saveUser(user);
|
||||
return new PersonalProfileResponse(user.getId(), authUserId, username,
|
||||
user.getDisplayName(), user.getPhone(), user.getEmail());
|
||||
}
|
||||
|
||||
private PersonalProfileResponse response(long tenantId, Long authUserId, String username) {
|
||||
IamMembership membership = repository.findByTenantAndAuthUser(tenantId, authUserId);
|
||||
if (membership == null) throw forbidden();
|
||||
IamUser user = repository.findUser(membership.getUserId());
|
||||
if (user == null || !authUserId.equals(user.getAuthUserId())) throw forbidden();
|
||||
return new PersonalProfileResponse(user.getId(), authUserId, username,
|
||||
user.getDisplayName(), user.getPhone(), user.getEmail());
|
||||
}
|
||||
private BizException forbidden() { return new BizException(ResultCode.FORBIDDEN, "当前身份不属于该租户"); }
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
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;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
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<IamUser> internalUsers() { return repository.findInternalUsers(); }
|
||||
public List<IamTenantAddress> addresses(long tenant) { return repository.findAddresses(tenant); }
|
||||
|
||||
@BusinessAudit(event = "TENANT_ADDRESS_CREATE", resourceType = "TENANT_ADDRESS",
|
||||
resourceId = "#tenant", description = "新增企业地址")
|
||||
public IamTenantAddress addAddress(long tenant, AddressRequest q) {
|
||||
IamTenantAddress a = new IamTenantAddress();
|
||||
a.setId(IdWorker.getId()); a.setTenantId(tenant); a.setAddressType(q.addressType());
|
||||
a.setCountryCode(q.countryCode() == null ? "CN" : q.countryCode()); a.setProvince(q.province());
|
||||
a.setCity(q.city()); a.setDistrict(q.district()); a.setDetail(q.detail());
|
||||
a.setPostalCode(q.postalCode()); a.setIsDefault(Boolean.TRUE.equals(q.isDefault()));
|
||||
repository.insertAddress(a); return a;
|
||||
}
|
||||
|
||||
public List<IamTenantQualification> 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)
|
||||
public QualificationSubmissionResponse submitQualification(QualificationSubmissionRequest request) {
|
||||
IamTenant tenant = tenantRepository.findTenant(request.tenantId());
|
||||
if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Enterprise not found");
|
||||
}
|
||||
if ("ACTIVE".equals(tenant.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Enterprise is already approved");
|
||||
}
|
||||
if (tenantRepository.findPendingReviewByTenant(tenant.getId()) != null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Enterprise information is already under review");
|
||||
}
|
||||
if (repository.findQualifications(tenant.getId()).stream()
|
||||
.anyMatch(item -> !"REJECTED".equals(item.getStatus()))) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Phase one permits one enterprise qualification image");
|
||||
}
|
||||
|
||||
IamTenantQualification qualification = createQualification(tenant.getId(),
|
||||
request.qualificationType(), request.certificateNo(), request.documentUrl());
|
||||
if (StringUtils.hasText(request.certificateNo())
|
||||
&& request.certificateNo().trim().toUpperCase().matches("[0-9A-Z]{18}")) {
|
||||
tenant.setUnifiedSocialCreditCode(request.certificateNo().trim().toUpperCase());
|
||||
}
|
||||
tenant.setStatus("PENDING");
|
||||
tenant.setRejectReason(null);
|
||||
tenantRepository.saveTenant(tenant);
|
||||
|
||||
IamTenantReview review = new IamTenantReview();
|
||||
review.setId(IdWorker.getId()); review.setTenantId(tenant.getId());
|
||||
review.setReviewType("REGISTER"); review.setStatus("PENDING");
|
||||
review.setCreatedAt(LocalDateTime.now());
|
||||
tenantRepository.insertReview(review);
|
||||
return new QualificationSubmissionResponse(qualification.getId(), review.getId(), tenant.getStatus());
|
||||
}
|
||||
|
||||
private IamTenantQualification createQualification(long tenant, String type, String certificateNo, String url) {
|
||||
IamTenantQualification x = new IamTenantQualification();
|
||||
x.setId(IdWorker.getId()); x.setTenantId(tenant); x.setQualificationType(type);
|
||||
x.setCertificateNo(certificateNo); x.setDocumentUrl(url); x.setStatus("PENDING");
|
||||
repository.insertQualification(x); return x;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.RbacDtos.*;
|
||||
import com.baiyue.iam.entity.IamMembership;
|
||||
import com.baiyue.iam.entity.IamPermission;
|
||||
import com.baiyue.iam.entity.IamRole;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
import com.baiyue.iam.repository.RbacRepository;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Comparator;
|
||||
|
||||
/** 双端 RBAC 应用服务,严格按租户和应用域隔离。 */
|
||||
@Service
|
||||
public class RbacService {
|
||||
@Resource private RbacRepository repository;
|
||||
@Resource private MemberRepository memberRepository;
|
||||
|
||||
public List<RoleResponse> roles(long tenantId, String appCode) {
|
||||
return repository.findRoles(tenantId, appCode).stream()
|
||||
.map(role -> response(role, repository.findRolePermissionIds(tenantId, role.getId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public RoleResponse role(long tenantId, String appCode, Long roleId) {
|
||||
IamRole role = requireRole(tenantId, appCode, roleId);
|
||||
return response(role, repository.findRolePermissionIds(tenantId, roleId));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "ROLE_CREATE", resourceType = "ROLE", resourceId = "#request.roleCode",
|
||||
description = "创建角色", captureBefore = false, captureAfter = true)
|
||||
public IamRole createRole(long tenantId, String appCode, CreateRoleRequest request) {
|
||||
if (repository.existsRoleCode(tenantId, appCode, request.roleCode(), null)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "角色编码已存在");
|
||||
}
|
||||
IamRole role = new IamRole();
|
||||
role.setId(IdWorker.getId());
|
||||
role.setTenantId(tenantId);
|
||||
role.setAppCode(appCode);
|
||||
role.setRoleCode(request.roleCode());
|
||||
role.setRoleName(request.roleName());
|
||||
role.setDescription(request.description());
|
||||
role.setRoleType("CUSTOM");
|
||||
role.setStatus("ACTIVE");
|
||||
try {
|
||||
repository.insertRole(role);
|
||||
} catch (DataIntegrityViolationException exception) {
|
||||
// 数据库唯一键负责兜底并发创建,向调用方保持稳定的业务错误。
|
||||
throw new BizException(ResultCode.CONFLICT, "角色编码已存在");
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "ROLE_UPDATE", resourceType = "ROLE", resourceId = "#roleId",
|
||||
description = "编辑角色", captureBefore = true, captureAfter = true)
|
||||
public RoleResponse updateRole(long tenantId, String appCode, Long roleId, UpdateRoleRequest request) {
|
||||
IamRole role = requireCustomRole(tenantId, appCode, roleId);
|
||||
role.setRoleName(request.roleName());
|
||||
role.setDescription(request.description());
|
||||
repository.updateRole(role);
|
||||
return response(role, repository.findRolePermissionIds(tenantId, roleId));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "ROLE_STATUS_CHANGE", resourceType = "ROLE", resourceId = "#roleId",
|
||||
description = "启停角色", captureBefore = true, captureAfter = true)
|
||||
public RoleResponse changeStatus(long tenantId, String appCode, Long roleId,
|
||||
ChangeRoleStatusRequest request) {
|
||||
IamRole role = requireCustomRole(tenantId, appCode, roleId);
|
||||
role.setStatus(request.status());
|
||||
repository.updateRole(role);
|
||||
return response(role, repository.findRolePermissionIds(tenantId, roleId));
|
||||
}
|
||||
|
||||
/** 删除策略:系统角色禁止删除;已分配给成员的角色拒绝删除;其余角色清理授权关系后逻辑删除。 */
|
||||
@BusinessAudit(event = "ROLE_DELETE", resourceType = "ROLE", resourceId = "#roleId",
|
||||
description = "删除角色", captureBefore = true, captureAfter = false)
|
||||
@Transactional
|
||||
public void deleteRole(long tenantId, String appCode, Long roleId) {
|
||||
IamRole role = requireCustomRole(tenantId, appCode, roleId);
|
||||
if (repository.isRoleAssigned(tenantId, role.getId())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "角色已分配给成员,不能删除");
|
||||
}
|
||||
repository.deleteRole(tenantId, appCode, roleId);
|
||||
}
|
||||
|
||||
public List<IamPermission> permissions(long tenantId, String appCode) {
|
||||
Map<String, IamPermission> effective = new LinkedHashMap<>();
|
||||
repository.findPermissions(tenantId, appCode).stream()
|
||||
.sorted(Comparator
|
||||
.comparingInt((IamPermission permission) ->
|
||||
permission.getTenantId() != null && permission.getTenantId() == tenantId ? 0 : 1)
|
||||
.thenComparing(IamPermission::getPermissionCode))
|
||||
.forEach(permission -> effective.putIfAbsent(permission.getPermissionCode(), permission));
|
||||
return List.copyOf(effective.values());
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "PERMISSION_CREATE", resourceType = "PERMISSION", resourceId = "#request.code",
|
||||
description = "创建权限资源", captureBefore = false, captureAfter = true)
|
||||
public IamPermission createPermission(long tenantId, String appCode, CreatePermissionRequest request) {
|
||||
if (!Set.of("MENU", "PAGE", "BUTTON", "API").contains(request.type())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "权限类型只能是 MENU、PAGE、BUTTON 或 API");
|
||||
}
|
||||
if (repository.existsPermissionCode(tenantId, appCode, request.code())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "权限编码已存在");
|
||||
}
|
||||
if (request.parentId() != null && request.parentId() > 0
|
||||
&& repository.findPermission(request.parentId(), tenantId, appCode) == null) {
|
||||
throw forbidden();
|
||||
}
|
||||
IamPermission permission = new IamPermission();
|
||||
permission.setId(IdWorker.getId());
|
||||
permission.setTenantId(tenantId);
|
||||
permission.setAppCode(appCode);
|
||||
permission.setPermissionType(request.type());
|
||||
permission.setPermissionCode(request.code());
|
||||
permission.setPermissionName(request.name());
|
||||
permission.setParentId(request.parentId() == null ? 0L : request.parentId());
|
||||
permission.setRoutePath(request.routePath());
|
||||
permission.setHttpMethod(request.httpMethod());
|
||||
permission.setSortNo(request.sortNo() == null ? 0 : request.sortNo());
|
||||
permission.setStatus("ACTIVE");
|
||||
try {
|
||||
repository.insertPermission(permission);
|
||||
} catch (DataIntegrityViolationException exception) {
|
||||
throw new BizException(ResultCode.CONFLICT, "权限编码已存在");
|
||||
}
|
||||
return permission;
|
||||
}
|
||||
|
||||
public MemberRolesResponse memberRoles(long tenantId, String appCode, Long membershipId) {
|
||||
requireMembership(tenantId, membershipId);
|
||||
Set<Long> scopedIds = new LinkedHashSet<>();
|
||||
for (Long roleId : repository.findMemberRoleIds(tenantId, membershipId)) {
|
||||
if (repository.findRole(roleId, tenantId, appCode) != null) scopedIds.add(roleId);
|
||||
}
|
||||
return new MemberRolesResponse(membershipId, Set.copyOf(scopedIds));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "MEMBER_ROLE_ASSIGN", resourceType = "MEMBERSHIP",
|
||||
resourceId = "#request.membershipId", description = "分配成员角色",
|
||||
captureBefore = true, captureAfter = true)
|
||||
@Transactional
|
||||
public void assignRoles(long tenantId, String appCode, AssignRolesRequest request) {
|
||||
IamMembership membership = requireMembership(tenantId, request.membershipId());
|
||||
if ("OWNER".equals(membership.getMemberType())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "企业所有者角色不允许通过普通授权接口修改");
|
||||
}
|
||||
for (Long roleId : request.roleIds()) {
|
||||
IamRole role = requireRole(tenantId, appCode, roleId);
|
||||
if ("MERCHANT_OWNER".equals(role.getRoleCode())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "企业所有者角色不能通过普通授权接口分配");
|
||||
}
|
||||
if (!"ACTIVE".equals(role.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "停用角色不能分配给成员");
|
||||
}
|
||||
}
|
||||
repository.replaceMemberRoles(tenantId, appCode, request.membershipId(), request.roleIds());
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "ROLE_PERMISSION_GRANT", resourceType = "ROLE",
|
||||
resourceId = "#request.roleId", description = "设置角色权限",
|
||||
captureBefore = true, captureAfter = true)
|
||||
@Transactional
|
||||
public void grantPermissions(long tenantId, String appCode, GrantPermissionsRequest request) {
|
||||
requireCustomRole(tenantId, appCode, request.roleId());
|
||||
for (Long permissionId : request.permissionIds()) {
|
||||
IamPermission permission = repository.findPermission(permissionId, tenantId, appCode);
|
||||
if (permission == null) throw forbidden();
|
||||
if (!"ACTIVE".equals(permission.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "停用权限不能授予角色");
|
||||
}
|
||||
}
|
||||
repository.replaceRolePermissions(tenantId, request.roleId(), request.permissionIds());
|
||||
}
|
||||
|
||||
private IamRole requireRole(long tenantId, String appCode, Long roleId) {
|
||||
IamRole role = repository.findRole(roleId, tenantId, appCode);
|
||||
if (role == null) throw forbidden();
|
||||
return role;
|
||||
}
|
||||
|
||||
private IamRole requireCustomRole(long tenantId, String appCode, Long roleId) {
|
||||
IamRole role = requireRole(tenantId, appCode, roleId);
|
||||
if ("SYSTEM".equals(role.getRoleType())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "系统角色不允许修改、停用或删除");
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
private IamMembership requireMembership(long tenantId, Long membershipId) {
|
||||
IamMembership membership = memberRepository.findById(membershipId);
|
||||
if (membership == null || membership.getTenantId() == null
|
||||
|| membership.getTenantId().longValue() != tenantId) throw forbidden();
|
||||
return membership;
|
||||
}
|
||||
|
||||
private RoleResponse response(IamRole role, Set<Long> permissionIds) {
|
||||
return new RoleResponse(role.getId(), role.getTenantId(), role.getAppCode(), role.getRoleCode(),
|
||||
role.getRoleName(), role.getRoleType(), role.getStatus(), role.getDescription(),
|
||||
Set.copyOf(permissionIds));
|
||||
}
|
||||
|
||||
private BizException forbidden() {
|
||||
return new BizException(ResultCode.FORBIDDEN, "禁止跨租户或跨应用访问");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.baiyue.iam.service;
|
||||
import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.common.security.AuthenticatedUser;
|
||||
/**
|
||||
* 租户访问守卫,从登录身份提取租户并校验目标应用域,防止平台端、商户端或不同租户间越权。
|
||||
*/
|
||||
public final class TenantGuard {
|
||||
private TenantGuard(){}
|
||||
public static long tenant(AuthenticatedUser user,String expectedApp){
|
||||
if(user==null||user.tenantId()==null) throw new BizException(ResultCode.UNAUTHORIZED,"Authentication required / 请先登录");
|
||||
if(!expectedApp.equals(user.appCode())) throw new BizException(ResultCode.FORBIDDEN,"Wrong application scope / 应用权限域不匹配");
|
||||
if("PLATFORM".equals(expectedApp)) return 0L;
|
||||
if(user.tenantId()<=0) throw new BizException(ResultCode.FORBIDDEN,"Merchant tenant required / 缺少商户租户");
|
||||
return user.tenantId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.baiyue.iam.service;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.entity.IamTenant;
|
||||
import com.baiyue.iam.entity.IamTenantReview;
|
||||
import com.baiyue.iam.repository.TenantRepository;
|
||||
import com.baiyue.iam.repository.ProfileResourceRepository;
|
||||
import com.baiyue.iam.dto.OnboardingDtos.OrderEligibilityResponse;
|
||||
|
||||
import com.baiyue.iam.dto.TenantDtos.RegisterRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.TenantResponse;
|
||||
import com.baiyue.iam.dto.TenantDtos.UpdateProfileRequest;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 租户生命周期应用服务,编排商户入驻、资料维护、审核流转和租户状态变更事务。
|
||||
*/
|
||||
@Service
|
||||
public class TenantService {
|
||||
@Resource
|
||||
private TenantRepository repository;
|
||||
@Resource
|
||||
private ProfileResourceRepository profileRepository;
|
||||
|
||||
|
||||
@BusinessAudit(
|
||||
event = "TENANT_REGISTER",
|
||||
resourceType = "TENANT",
|
||||
description = "Register tenant / 企业注册",
|
||||
captureBefore = false,
|
||||
captureAfter = false
|
||||
)
|
||||
@Transactional
|
||||
public TenantResponse register(RegisterRequest request) {
|
||||
if (repository.existsByCreditCode(request.unifiedSocialCreditCode())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Enterprise already exists / 企业已存在");
|
||||
}
|
||||
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(IdWorker.getId());
|
||||
tenant.setTenantCode("TEN" + tenant.getId());
|
||||
tenant.setTenantName(request.tenantName());
|
||||
tenant.setTenantType("MERCHANT");
|
||||
tenant.setStatus("PENDING");
|
||||
tenant.setLegalName(request.legalName());
|
||||
tenant.setUnifiedSocialCreditCode(request.unifiedSocialCreditCode());
|
||||
tenant.setContactName(request.contactName());
|
||||
tenant.setContactPhone(request.contactPhone());
|
||||
tenant.setContactEmail(request.contactEmail());
|
||||
repository.insertTenant(tenant);
|
||||
|
||||
IamTenantReview review = new IamTenantReview();
|
||||
review.setId(IdWorker.getId());
|
||||
review.setTenantId(tenant.getId());
|
||||
review.setReviewType("REGISTER");
|
||||
review.setStatus("PENDING");
|
||||
review.setCreatedAt(LocalDateTime.now());
|
||||
repository.insertReview(review);
|
||||
return response(tenant);
|
||||
}
|
||||
|
||||
public List<IamTenantReview> pendingReviews() {
|
||||
return repository.findPendingReviews();
|
||||
}
|
||||
|
||||
@BusinessAudit(
|
||||
event = "TENANT_REVIEW",
|
||||
resourceType = "TENANT_REVIEW",
|
||||
resourceId = "#reviewId",
|
||||
description = "Review tenant registration / 企业注册审核",
|
||||
captureBefore = false,
|
||||
captureAfter = false
|
||||
)
|
||||
@Transactional
|
||||
public void review(long reviewId, long reviewerId, boolean approve, String comment) {
|
||||
IamTenantReview review = repository.findReview(reviewId);
|
||||
if (review == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Review not found / 审核单不存在");
|
||||
}
|
||||
if (!"PENDING".equals(review.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Review already completed / 审核已完成");
|
||||
}
|
||||
|
||||
review.setStatus(approve ? "APPROVED" : "REJECTED");
|
||||
review.setReviewerId(reviewerId);
|
||||
review.setReviewComment(comment);
|
||||
review.setReviewedAt(LocalDateTime.now());
|
||||
repository.saveReview(review);
|
||||
|
||||
IamTenant tenant = repository.findTenant(review.getTenantId());
|
||||
if (tenant == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Tenant not found / 企业不存在");
|
||||
}
|
||||
tenant.setStatus(approve ? "ACTIVE" : "REJECTED");
|
||||
tenant.setRejectReason(approve ? null : comment);
|
||||
tenant.setApprovedAt(approve ? LocalDateTime.now() : null);
|
||||
tenant.setApprovedBy(approve ? reviewerId : null);
|
||||
repository.saveTenant(tenant);
|
||||
for (var qualification : profileRepository.findQualifications(tenant.getId())) {
|
||||
if ("PENDING".equals(qualification.getStatus())) {
|
||||
qualification.setStatus(approve ? "APPROVED" : "REJECTED");
|
||||
qualification.setReviewComment(comment);
|
||||
profileRepository.saveQualification(qualification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public OrderEligibilityResponse orderEligibility(long tenantId) {
|
||||
IamTenant tenant = repository.findTenant(tenantId);
|
||||
if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) {
|
||||
return new OrderEligibilityResponse(false, "NOT_FOUND", "Enterprise does not exist");
|
||||
}
|
||||
if (!"ACTIVE".equals(tenant.getStatus())) {
|
||||
return new OrderEligibilityResponse(false, tenant.getStatus(),
|
||||
"Enterprise information must be approved before ordering");
|
||||
}
|
||||
if (profileRepository.countApprovedQualifications(tenantId) < 1) {
|
||||
return new OrderEligibilityResponse(false, tenant.getStatus(),
|
||||
"At least one approved enterprise qualification is required");
|
||||
}
|
||||
return new OrderEligibilityResponse(true, tenant.getStatus(), null);
|
||||
}
|
||||
|
||||
public TenantResponse profile(long tenantId) {
|
||||
IamTenant tenant = repository.findTenant(tenantId);
|
||||
if (tenant == null || !tenantIdEquals(tenant, tenantId)) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Tenant not found / 企业不存在");
|
||||
}
|
||||
return response(tenant);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "TENANT_PROFILE_UPDATE", resourceType = "TENANT",
|
||||
resourceId = "#tenantId", description = "编辑企业资料")
|
||||
public TenantResponse update(long tenantId, UpdateProfileRequest request) {
|
||||
IamTenant tenant = repository.findTenant(tenantId);
|
||||
if (tenant == null || !tenantIdEquals(tenant, tenantId)) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Tenant not found / 企业不存在");
|
||||
}
|
||||
tenant.setTenantName(request.tenantName());
|
||||
tenant.setLegalName(request.legalName());
|
||||
tenant.setContactName(request.contactName());
|
||||
tenant.setContactPhone(request.contactPhone());
|
||||
tenant.setContactEmail(request.contactEmail());
|
||||
repository.saveTenant(tenant);
|
||||
return response(tenant);
|
||||
}
|
||||
|
||||
private boolean tenantIdEquals(IamTenant tenant, long id) {
|
||||
return tenant.getId() != null && tenant.getId().longValue() == id;
|
||||
}
|
||||
|
||||
private TenantResponse response(IamTenant tenant) {
|
||||
return new TenantResponse(
|
||||
tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), tenant.getStatus(),
|
||||
tenant.getLegalName(), tenant.getUnifiedSocialCreditCode(), tenant.getContactName(),
|
||||
tenant.getContactPhone(), tenant.getContactEmail()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
server:
|
||||
port: ${IAM_PORT:9201}
|
||||
spring:
|
||||
application:
|
||||
name: iam-service
|
||||
config:
|
||||
import:
|
||||
- optional:nacos:baiyue-common.yml?group=BAIYUE&refreshEnabled=true
|
||||
- optional:nacos:${spring.application.name}.yml?group=BAIYUE&refreshEnabled=true
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
discovery:
|
||||
enabled: ${NACOS_DISCOVERY_ENABLED:true}
|
||||
register-enabled: ${NACOS_REGISTER_ENABLED:true}
|
||||
group: ${NACOS_GROUP:BAIYUE}
|
||||
namespace: ${NACOS_NAMESPACE:public}
|
||||
config:
|
||||
namespace: ${NACOS_NAMESPACE:public}
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: ${IAM_DB_URL:jdbc:mysql://127.0.0.1:3306/baiyue_iam?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
|
||||
username: ${IAM_DB_USERNAME:root}
|
||||
password: ${IAM_DB_PASSWORD:123456}
|
||||
baiyue:
|
||||
clients:
|
||||
auth:
|
||||
url: ${AUTH_SERVICE_URL:}
|
||||
security:
|
||||
jwt-secret: ${JWT_SECRET:change-me-to-at-least-32-bytes-long-before-start}
|
||||
internal-service-token: ${INTERNAL_SERVICE_TOKEN:change-me-to-an-internal-token-at-least-32-bytes-long}
|
||||
management.endpoints.web.exposure.include: health,info,prometheus
|
||||
@@ -0,0 +1,199 @@
|
||||
SET NAMES utf8mb4;
|
||||
-- IAM 服务独占数据库:负责租户、成员、双端角色、菜单/页面/按钮/API权限和数据范围。
|
||||
CREATE DATABASE IF NOT EXISTS baiyue_iam
|
||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
USE baiyue_iam;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_tenant (
|
||||
id BIGINT NOT NULL COMMENT '租户ID(雪花ID)',
|
||||
tenant_code VARCHAR(64) NOT NULL COMMENT '全局唯一租户编码',
|
||||
tenant_name VARCHAR(128) NOT NULL COMMENT '租户名称',
|
||||
tenant_type VARCHAR(20) NOT NULL DEFAULT 'MERCHANT' COMMENT '租户类型',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED' COMMENT '租户状态:PROFILE_REQUIRED、PENDING、ACTIVE、REJECTED、DISABLED',
|
||||
legal_name VARCHAR(128) NULL COMMENT '企业法定名称',
|
||||
unified_social_credit_code VARCHAR(32) NULL COMMENT '统一社会信用代码',
|
||||
contact_name VARCHAR(64) NULL COMMENT '企业联系人',
|
||||
contact_phone VARCHAR(32) NULL COMMENT '联系电话',
|
||||
contact_email VARCHAR(128) NULL COMMENT '联系邮箱',
|
||||
approved_at DATETIME(3) NULL COMMENT '审核通过时间',
|
||||
approved_by BIGINT NULL COMMENT '审核人ID',
|
||||
reject_reason VARCHAR(500) NULL COMMENT '驳回原因',
|
||||
created_by BIGINT NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT NULL COMMENT '最后修改人ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_iam_tenant_code (tenant_code),
|
||||
UNIQUE KEY uk_iam_tenant_credit (unified_social_credit_code),
|
||||
KEY idx_iam_tenant_status (status, created_at)
|
||||
) ENGINE=InnoDB COMMENT='企业租户主表:作为SaaS数据隔离边界,保存商户企业资料、审核结果和启停状态';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_tenant_review (
|
||||
id BIGINT NOT NULL COMMENT '审核记录ID',
|
||||
tenant_id BIGINT NOT NULL COMMENT '被审核租户ID',
|
||||
review_type VARCHAR(20) NOT NULL DEFAULT 'REGISTER' COMMENT '审核类型',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态',
|
||||
reviewer_id BIGINT NULL COMMENT '审核人ID',
|
||||
review_comment VARCHAR(500) NULL COMMENT '审核意见',
|
||||
reviewed_at DATETIME(3) NULL COMMENT '审核完成时间',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '进入审核队列时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_review_queue (status, created_at),
|
||||
KEY idx_review_tenant (tenant_id)
|
||||
) ENGINE=InnoDB COMMENT='租户审核记录表:保存企业注册、资料变更等审核过程和审核意见';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_tenant_address (
|
||||
id BIGINT NOT NULL COMMENT '租户地址ID',
|
||||
tenant_id BIGINT NOT NULL COMMENT '所属租户ID',
|
||||
address_type VARCHAR(20) NOT NULL COMMENT '地址类型',
|
||||
country_code CHAR(2) NOT NULL DEFAULT 'CN' COMMENT '国家二字码',
|
||||
province VARCHAR(64) NULL COMMENT '省/州',
|
||||
city VARCHAR(64) NULL COMMENT '城市',
|
||||
district VARCHAR(64) NULL COMMENT '区县',
|
||||
detail VARCHAR(255) NOT NULL COMMENT '详细地址',
|
||||
postal_code VARCHAR(16) NULL COMMENT '邮政编码',
|
||||
is_default TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认地址',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_address_tenant (tenant_id, address_type)
|
||||
) ENGINE=InnoDB COMMENT='租户地址表:保存企业注册地址、办公地址等租户级资料';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_tenant_qualification (
|
||||
id BIGINT NOT NULL COMMENT '资质记录ID',
|
||||
tenant_id BIGINT NOT NULL COMMENT '所属租户ID',
|
||||
qualification_type VARCHAR(32) NOT NULL COMMENT '资质类型',
|
||||
certificate_no VARCHAR(128) NULL COMMENT '证书编号',
|
||||
document_url VARCHAR(500) NULL COMMENT '资质文件存储地址',
|
||||
valid_from DATE NULL COMMENT '有效期开始日期',
|
||||
valid_to DATE NULL COMMENT '有效期结束日期',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '资质审核状态',
|
||||
review_comment VARCHAR(500) NULL COMMENT '审核意见',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_qualification_tenant (tenant_id, status)
|
||||
) ENGINE=InnoDB COMMENT='租户资质表:保存企业证照、有效期、文件地址和审核状态';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_user (
|
||||
id BIGINT NOT NULL COMMENT 'IAM用户资料ID',
|
||||
auth_user_id BIGINT NOT NULL COMMENT '认证服务账号逻辑ID',
|
||||
user_type VARCHAR(20) NOT NULL DEFAULT 'ENTERPRISE' COMMENT '用户类型',
|
||||
display_name VARCHAR(64) NOT NULL COMMENT '用户展示名称',
|
||||
phone VARCHAR(32) NULL COMMENT '手机号',
|
||||
email VARCHAR(128) NULL COMMENT '邮箱',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '用户资料状态',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_iam_user_auth (auth_user_id),
|
||||
KEY idx_iam_user_status (status)
|
||||
) ENGINE=InnoDB COMMENT='IAM用户资料表:连接统一认证账号与业务身份资料,不保存密码';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_membership (
|
||||
id BIGINT NOT NULL COMMENT '租户成员关系ID',
|
||||
tenant_id BIGINT NOT NULL COMMENT '所属租户ID',
|
||||
user_id BIGINT NOT NULL COMMENT 'IAM用户资料ID',
|
||||
member_type VARCHAR(20) NOT NULL DEFAULT 'EMPLOYEE' COMMENT '成员类型',
|
||||
member_no VARCHAR(64) NULL COMMENT '租户内部成员编号',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '成员状态',
|
||||
joined_at DATETIME(3) NULL COMMENT '加入租户时间',
|
||||
left_at DATETIME(3) NULL COMMENT '离开租户时间',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_membership_tenant_user (tenant_id, user_id),
|
||||
UNIQUE KEY uk_membership_tenant_no (tenant_id, member_no),
|
||||
KEY idx_membership_user (user_id, status),
|
||||
KEY idx_membership_tenant (tenant_id, status)
|
||||
) ENGINE=InnoDB COMMENT='租户成员关系表:表示一个用户在某个企业租户中的成员身份,是分配商户端角色的主体';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_role (
|
||||
id BIGINT NOT NULL COMMENT '角色ID',
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '角色所属租户;0表示平台预置角色',
|
||||
app_code VARCHAR(20) NOT NULL COMMENT '应用端:MERCHANT或PLATFORM',
|
||||
role_code VARCHAR(64) NOT NULL COMMENT '作用域内唯一角色编码',
|
||||
role_name VARCHAR(128) NOT NULL COMMENT '角色名称',
|
||||
role_type VARCHAR(20) NOT NULL DEFAULT 'CUSTOM' COMMENT '角色类型:SYSTEM或CUSTOM',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '角色状态',
|
||||
description VARCHAR(255) NULL COMMENT '角色用途说明',
|
||||
created_by BIGINT NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT NULL COMMENT '更新人ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_role_scope_code (tenant_id, app_code, role_code),
|
||||
KEY idx_role_tenant (tenant_id, app_code, status)
|
||||
) ENGINE=InnoDB COMMENT='角色表:按租户和应用端隔离商户端、平台后台两套角色体系';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_membership_role (
|
||||
membership_id BIGINT NOT NULL COMMENT '租户成员关系ID',
|
||||
role_id BIGINT NOT NULL COMMENT '角色ID',
|
||||
tenant_id BIGINT NOT NULL COMMENT '冗余租户ID,用于租户隔离校验和查询',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '授权时间',
|
||||
PRIMARY KEY (membership_id, role_id),
|
||||
KEY idx_mr_tenant (tenant_id)
|
||||
) ENGINE=InnoDB COMMENT='成员角色关联表:将租户成员分配到对应应用端的一个或多个角色';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_permission (
|
||||
id BIGINT NOT NULL COMMENT '权限资源ID',
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '权限所属租户;0表示平台预置权限',
|
||||
app_code VARCHAR(20) NOT NULL COMMENT '所属应用端:MERCHANT或PLATFORM',
|
||||
permission_type VARCHAR(20) NOT NULL COMMENT '权限类型:MENU、PAGE、BUTTON或API',
|
||||
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父权限ID,用于构造菜单和页面树',
|
||||
permission_code VARCHAR(128) NOT NULL COMMENT '鉴权使用的唯一权限码',
|
||||
permission_name VARCHAR(128) NOT NULL COMMENT '权限名称',
|
||||
route_path VARCHAR(255) NULL COMMENT '前端路由或API路径',
|
||||
http_method VARCHAR(16) NULL COMMENT 'API权限对应的HTTP方法',
|
||||
sort_no INT NOT NULL DEFAULT 0 COMMENT '同级显示顺序',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '权限状态',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_permission_scope_code (tenant_id, app_code, permission_code),
|
||||
KEY idx_permission_parent (parent_id),
|
||||
KEY idx_permission_api (http_method, route_path)
|
||||
) ENGINE=InnoDB COMMENT='统一权限资源表:支持商户端和平台后台两套菜单、页面、按钮及API权限,权限最细可控制到按钮';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_role_permission (
|
||||
role_id BIGINT NOT NULL COMMENT '角色ID',
|
||||
permission_id BIGINT NOT NULL COMMENT '权限资源ID',
|
||||
tenant_id BIGINT NOT NULL COMMENT '冗余租户ID,用于隔离校验和查询',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '授权时间',
|
||||
PRIMARY KEY (role_id, permission_id),
|
||||
KEY idx_rp_tenant (tenant_id)
|
||||
) ENGINE=InnoDB COMMENT='角色权限关联表:定义角色可访问的菜单、页面、按钮和API资源';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_data_scope (
|
||||
id BIGINT NOT NULL COMMENT '数据范围规则ID',
|
||||
role_id BIGINT NOT NULL COMMENT '角色ID',
|
||||
tenant_id BIGINT NOT NULL COMMENT '规则所属租户ID',
|
||||
scope_type VARCHAR(20) NOT NULL DEFAULT 'SELF' COMMENT '范围类型:SELF、DEPARTMENT、TENANT或CUSTOM',
|
||||
scope_expression JSON NULL COMMENT '自定义数据范围表达式',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_scope_role (role_id)
|
||||
) ENGINE=InnoDB COMMENT='角色数据范围表:在功能权限之外限制角色可查看和操作的租户内数据集合';
|
||||
|
||||
-- 存量数据库重复执行初始化脚本时,不重复创建审核状态索引。
|
||||
SET @review_status_index_sql = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'iam_tenant_review'
|
||||
AND index_name = 'idx_tenant_review_status'),
|
||||
'SELECT 1',
|
||||
'CREATE INDEX idx_tenant_review_status ON iam_tenant_review (tenant_id, status)'
|
||||
);
|
||||
PREPARE review_status_index_stmt FROM @review_status_index_sql;
|
||||
EXECUTE review_status_index_stmt;
|
||||
DEALLOCATE PREPARE review_status_index_stmt;
|
||||
@@ -0,0 +1,185 @@
|
||||
-- Phase-one administrator identities and RBAC seed data.
|
||||
-- Scope is intentionally limited to first-level menus, second-level menus and buttons.
|
||||
-- PAGE and API permission resources are not part of phase one.
|
||||
USE baiyue_iam;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- Keep the role table aligned with the shared BaseEntity audit fields.
|
||||
SET @ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'iam_role' AND column_name = 'created_by'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE iam_role ADD COLUMN created_by BIGINT NULL AFTER description'
|
||||
);
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @ddl = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'iam_role' AND column_name = 'updated_by'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE iam_role ADD COLUMN updated_by BIGINT NULL AFTER created_at'
|
||||
);
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
INSERT INTO iam_tenant (
|
||||
id, tenant_code, tenant_name, tenant_type, status, legal_name,
|
||||
unified_social_credit_code, contact_name, contact_phone, contact_email,
|
||||
approved_at, approved_by, created_by, deleted, version
|
||||
) VALUES
|
||||
(0, 'PLATFORM', '虫虫出海平台', 'PLATFORM', 'ACTIVE', '虫虫出海平台',
|
||||
NULL, '平台管理员', NULL, NULL, NOW(3), 1000001, 1000001, 0, 0),
|
||||
(10001, 'PHASE1_DEMO', '一期演示企业', 'MERCHANT', 'ACTIVE', '一期演示企业有限公司',
|
||||
'91310000PHASE10001', '企业管理员', '13800000000', 'merchant@example.com',
|
||||
NOW(3), 1000001, 1000001, 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tenant_name = VALUES(tenant_name), tenant_type = VALUES(tenant_type),
|
||||
status = 'ACTIVE', legal_name = VALUES(legal_name),
|
||||
contact_name = VALUES(contact_name), contact_phone = VALUES(contact_phone),
|
||||
contact_email = VALUES(contact_email), approved_at = COALESCE(approved_at, NOW(3)),
|
||||
approved_by = VALUES(approved_by), deleted = 0;
|
||||
|
||||
INSERT INTO iam_user (
|
||||
id, auth_user_id, user_type, display_name, phone, email, status, deleted, version
|
||||
) VALUES
|
||||
(2000001, 1000001, 'INTERNAL', '平台管理员', NULL, NULL, 'ACTIVE', 0, 0),
|
||||
(2000002, 1000002, 'ENTERPRISE', '一期演示企业管理员', '13800000000',
|
||||
'merchant@example.com', 'ACTIVE', 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
display_name = VALUES(display_name), phone = VALUES(phone), email = VALUES(email),
|
||||
status = 'ACTIVE', deleted = 0;
|
||||
|
||||
INSERT INTO iam_membership (
|
||||
id, tenant_id, user_id, member_type, member_no, status, joined_at, deleted, version
|
||||
) VALUES
|
||||
(3000001, 0, 2000001, 'EMPLOYEE', 'PLATFORM-ADMIN-001', 'ACTIVE', NOW(3), 0, 0),
|
||||
(3000002, 10001, 2000002, 'OWNER', 'MERCHANT-OWNER-001', 'ACTIVE', NOW(3), 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
member_type = VALUES(member_type), member_no = VALUES(member_no),
|
||||
status = 'ACTIVE', joined_at = COALESCE(joined_at, NOW(3)), left_at = NULL, deleted = 0;
|
||||
|
||||
INSERT INTO iam_role (
|
||||
id, tenant_id, app_code, role_code, role_name, role_type, status, description, deleted, version
|
||||
) VALUES
|
||||
(4000001, 0, 'PLATFORM', 'PLATFORM_ADMIN', '平台超级管理员', 'SYSTEM', 'ACTIVE', '一期平台全部菜单和按钮', 0, 0),
|
||||
(4000002, 0, 'PLATFORM', 'CUSTOMER_REVIEWER', '企业审核员', 'SYSTEM', 'ACTIVE', '企业资料与入驻审核', 0, 0),
|
||||
(4000003, 0, 'PLATFORM', 'ORDER_OPERATOR', '平台订单运营', 'SYSTEM', 'ACTIVE', '头程订单审核与履约处理', 0, 0),
|
||||
(4000101, 10001, 'MERCHANT', 'MERCHANT_OWNER', '企业管理员', 'SYSTEM', 'ACTIVE', '本企业一期全部菜单和按钮', 0, 0),
|
||||
(4000102, 10001, 'MERCHANT', 'MERCHANT_ORDER_MANAGER', '企业订单管理员', 'SYSTEM', 'ACTIVE', '本企业头程订单操作', 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
role_name = VALUES(role_name), role_type = VALUES(role_type), status = 'ACTIVE',
|
||||
description = VALUES(description), deleted = 0;
|
||||
|
||||
-- Platform: level-one menus.
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, deleted)
|
||||
VALUES
|
||||
(5100000, 0, 'PLATFORM', 'MENU', 0, 'platform:customer', '客户管理', '/customers', NULL, 10, 'ACTIVE', 0),
|
||||
(5101000, 0, 'PLATFORM', 'MENU', 0, 'platform:system', '系统管理', '/system', NULL, 20, 'ACTIVE', 0),
|
||||
(5102000, 0, 'PLATFORM', 'MENU', 0, 'platform:order', '头程订单', '/first-leg-orders', NULL, 30, 'ACTIVE', 0),
|
||||
(5103000, 0, 'PLATFORM', 'MENU', 0, 'platform:master-data', '基础资料', '/master-data', NULL, 40, 'ACTIVE', 0),
|
||||
(5104000, 0, 'PLATFORM', 'MENU', 0, 'platform:audit', '审计管理', '/audit', NULL, 50, 'ACTIVE', 0),
|
||||
-- Platform: level-two menus.
|
||||
(5100010, 0, 'PLATFORM', 'MENU', 5100000, 'platform:customer:list', '企业客户', '/customers/list', NULL, 10, 'ACTIVE', 0),
|
||||
(5100020, 0, 'PLATFORM', 'MENU', 5100000, 'platform:customer:review', '企业审核', '/customers/reviews', NULL, 20, 'ACTIVE', 0),
|
||||
(5101010, 0, 'PLATFORM', 'MENU', 5101000, 'platform:user:list', '内部用户', '/system/users', NULL, 10, 'ACTIVE', 0),
|
||||
(5101020, 0, 'PLATFORM', 'MENU', 5101000, 'platform:role:list', '角色管理', '/system/roles', NULL, 20, 'ACTIVE', 0),
|
||||
(5101030, 0, 'PLATFORM', 'MENU', 5101000, 'platform:permission:list', '菜单按钮', '/system/permissions', NULL, 30, 'ACTIVE', 0),
|
||||
(5102010, 0, 'PLATFORM', 'MENU', 5102000, 'platform:order:list', '订单列表', '/first-leg-orders/list', NULL, 10, 'ACTIVE', 0),
|
||||
(5103010, 0, 'PLATFORM', 'MENU', 5103000, 'platform:warehouse:list', '国内仓/FBA仓', '/master-data/warehouses', NULL, 10, 'ACTIVE', 0),
|
||||
(5103020, 0, 'PLATFORM', 'MENU', 5103000, 'platform:express:list', '快递公司', '/master-data/express', NULL, 20, 'ACTIVE', 0),
|
||||
(5103030, 0, 'PLATFORM', 'MENU', 5103000, 'platform:counterparty:list', '往来单位', '/master-data/counterparties', NULL, 30, 'ACTIVE', 0),
|
||||
(5104010, 0, 'PLATFORM', 'MENU', 5104000, 'platform:audit:list', '操作日志', '/audit/logs', NULL, 10, 'ACTIVE', 0),
|
||||
-- Platform: buttons.
|
||||
(5100021, 0, 'PLATFORM', 'BUTTON', 5100020, 'platform:customer:approve', '审核通过', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5100022, 0, 'PLATFORM', 'BUTTON', 5100020, 'platform:customer:reject', '审核驳回', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5101011, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:create', '新增用户', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5101021, 0, 'PLATFORM', 'BUTTON', 5101020, 'platform:role:create', '新增角色', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5101022, 0, 'PLATFORM', 'BUTTON', 5101020, 'platform:role:grant', '角色授权', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5101031, 0, 'PLATFORM', 'BUTTON', 5101030, 'platform:permission:create', '新增菜单按钮', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5102011, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:approve', '订单审核通过', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5102012, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:reject', '订单驳回', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5102013, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:accept', '订单受理', NULL, NULL, 30, 'ACTIVE', 0),
|
||||
(5102014, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:process', '开始处理', NULL, NULL, 40, 'ACTIVE', 0),
|
||||
(5102015, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:complete', '完成订单', NULL, NULL, 50, 'ACTIVE', 0),
|
||||
(5102016, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:cancel', '取消订单', NULL, NULL, 60, 'ACTIVE', 0),
|
||||
(5102017, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:note', '内部备注', NULL, NULL, 70, 'ACTIVE', 0),
|
||||
(5102018, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:fulfillment', '登记履约快递', NULL, NULL, 80, 'ACTIVE', 0),
|
||||
(5103011, 0, 'PLATFORM', 'BUTTON', 5103010, 'platform:warehouse:create', '新增仓库', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5103012, 0, 'PLATFORM', 'BUTTON', 5103010, 'platform:warehouse:edit', '编辑仓库', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5103013, 0, 'PLATFORM', 'BUTTON', 5103010, 'platform:warehouse:status', '启用停用', NULL, NULL, 30, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_type = VALUES(permission_type), parent_id = VALUES(parent_id),
|
||||
permission_name = VALUES(permission_name), route_path = VALUES(route_path),
|
||||
http_method = NULL, sort_no = VALUES(sort_no), status = 'ACTIVE', deleted = 0;
|
||||
|
||||
-- Merchant: level-one menus, level-two menus and buttons.
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, deleted)
|
||||
VALUES
|
||||
(5200000, 10001, 'MERCHANT', 'MENU', 0, 'merchant:enterprise', '企业管理', '/enterprise', NULL, 10, 'ACTIVE', 0),
|
||||
(5201000, 10001, 'MERCHANT', 'MENU', 0, 'merchant:order', '头程订单', '/first-leg-orders', NULL, 20, 'ACTIVE', 0),
|
||||
(5202000, 10001, 'MERCHANT', 'MENU', 0, 'merchant:master-data', '基础资料', '/master-data', NULL, 30, 'ACTIVE', 0),
|
||||
(5200010, 10001, 'MERCHANT', 'MENU', 5200000, 'merchant:profile:view', '企业资料', '/enterprise/profile', NULL, 10, 'ACTIVE', 0),
|
||||
(5200020, 10001, 'MERCHANT', 'MENU', 5200000, 'merchant:member:list', '企业成员', '/enterprise/members', NULL, 20, 'ACTIVE', 0),
|
||||
(5200030, 10001, 'MERCHANT', 'MENU', 5200000, 'merchant:role:list', '角色管理', '/enterprise/roles', NULL, 30, 'ACTIVE', 0),
|
||||
(5201010, 10001, 'MERCHANT', 'MENU', 5201000, 'merchant:order:list', '我的订单', '/first-leg-orders/list', NULL, 10, 'ACTIVE', 0),
|
||||
(5201020, 10001, 'MERCHANT', 'MENU', 5201000, 'merchant:order:create-page', '新建订单', '/first-leg-orders/create', NULL, 20, 'ACTIVE', 0),
|
||||
(5202010, 10001, 'MERCHANT', 'MENU', 5202000, 'merchant:warehouse:list', '收货仓选择', '/master-data/warehouses', NULL, 10, 'ACTIVE', 0),
|
||||
(5202020, 10001, 'MERCHANT', 'MENU', 5202000, 'merchant:counterparty:list', '往来单位', '/master-data/counterparties', NULL, 20, 'ACTIVE', 0),
|
||||
(5200011, 10001, 'MERCHANT', 'BUTTON', 5200010, 'merchant:profile:edit', '编辑企业资料', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5200012, 10001, 'MERCHANT', 'BUTTON', 5200010, 'merchant:qualification:upload', '上传企业资质', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5200021, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:create', '新增成员', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5200022, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:grant-role', '分配角色', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5200031, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:create', '新增角色', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5200032, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:grant', '角色授权', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5201011, 10001, 'MERCHANT', 'BUTTON', 5201010, 'merchant:order:view', '查看订单', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5201012, 10001, 'MERCHANT', 'BUTTON', 5201010, 'merchant:order:cancel', '取消订单', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5201021, 10001, 'MERCHANT', 'BUTTON', 5201020, 'merchant:order:create', '创建草稿', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5201022, 10001, 'MERCHANT', 'BUTTON', 5201020, 'merchant:order:edit', '编辑草稿', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5201023, 10001, 'MERCHANT', 'BUTTON', 5201020, 'merchant:order:submit', '提交订单', NULL, NULL, 30, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_type = VALUES(permission_type), parent_id = VALUES(parent_id),
|
||||
permission_name = VALUES(permission_name), route_path = VALUES(route_path),
|
||||
http_method = NULL, sort_no = VALUES(sort_no), status = 'ACTIVE', deleted = 0;
|
||||
|
||||
INSERT INTO iam_membership_role (membership_id, role_id, tenant_id) VALUES
|
||||
(3000001, 4000001, 0),
|
||||
(3000002, 4000101, 10001)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
-- Full administrator roles receive all phase-one permissions in their application scope.
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000001, id, 0 FROM iam_permission
|
||||
WHERE app_code = 'PLATFORM' AND tenant_id = 0 AND permission_type IN ('MENU', 'BUTTON')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000101, id, 10001 FROM iam_permission
|
||||
WHERE app_code = 'MERCHANT' AND tenant_id = 10001 AND permission_type IN ('MENU', 'BUTTON')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
-- Limited platform and merchant operational roles.
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000002, id, 0 FROM iam_permission
|
||||
WHERE permission_code IN (
|
||||
'platform:customer', 'platform:customer:list', 'platform:customer:review',
|
||||
'platform:customer:approve', 'platform:customer:reject'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000003, id, 0 FROM iam_permission
|
||||
WHERE permission_code LIKE 'platform:order%'
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000102, id, 10001 FROM iam_permission
|
||||
WHERE permission_code LIKE 'merchant:order%' OR permission_code IN ('merchant:master-data', 'merchant:warehouse:list')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_data_scope (id, role_id, tenant_id, scope_type, scope_expression) VALUES
|
||||
(6000001, 4000001, 0, 'TENANT', NULL),
|
||||
(6000002, 4000002, 0, 'TENANT', NULL),
|
||||
(6000003, 4000003, 0, 'TENANT', NULL),
|
||||
(6000101, 4000101, 10001, 'TENANT', NULL),
|
||||
(6000102, 4000102, 10001, 'TENANT', NULL)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id), scope_type = VALUES(scope_type), scope_expression = NULL;
|
||||
@@ -0,0 +1,38 @@
|
||||
SET NAMES utf8mb4;
|
||||
-- 企业成员管理按钮权限增量;部署后应将权限按需授予企业管理员角色。
|
||||
USE baiyue_iam;
|
||||
|
||||
-- 为存量数据库补齐企业内成员编号唯一约束,防止并发重复创建。
|
||||
SET @member_no_index_sql = IF(
|
||||
EXISTS(SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'iam_membership'
|
||||
AND index_name = 'uk_membership_tenant_no'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE iam_membership ADD UNIQUE KEY uk_membership_tenant_no (tenant_id, member_no)'
|
||||
);
|
||||
PREPARE member_no_index_stmt FROM @member_no_index_sql;
|
||||
EXECUTE member_no_index_stmt;
|
||||
DEALLOCATE PREPARE member_no_index_stmt;
|
||||
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, sort_no, status, deleted)
|
||||
VALUES
|
||||
(5200023, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:edit', '编辑成员', 30, 'ACTIVE', 0),
|
||||
(5200024, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:status', '启停成员', 40, 'ACTIVE', 0),
|
||||
(5200025, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:reset-password', '重置成员密码', 50, 'ACTIVE', 0),
|
||||
(5300023, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:edit', '编辑成员', 30, 'ACTIVE', 0),
|
||||
(5300024, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:status', '启停成员', 40, 'ACTIVE', 0),
|
||||
(5300025, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:reset-password', '重置成员密码', 50, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE permission_name = VALUES(permission_name), status = VALUES(status), deleted = 0;
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000101, id, 10001 FROM iam_permission
|
||||
WHERE tenant_id = 10001 AND permission_code IN
|
||||
('merchant:member:edit', 'merchant:member:status', 'merchant:member:reset-password')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000100, id, 0 FROM iam_permission
|
||||
WHERE tenant_id = 0 AND permission_code IN
|
||||
('merchant:member:edit', 'merchant:member:status', 'merchant:member:reset-password')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
@@ -0,0 +1,71 @@
|
||||
-- Phase-one merchant onboarding state migration.
|
||||
USE baiyue_iam;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE iam_tenant MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED'
|
||||
COMMENT 'PROFILE_REQUIRED, PENDING, ACTIVE, REJECTED or DISABLED';
|
||||
DROP PROCEDURE IF EXISTS phase1_iam_add_column;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE phase1_iam_add_column(
|
||||
IN p_table_name VARCHAR(64), IN p_column_name VARCHAR(64), IN p_definition VARCHAR(1000)
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = p_table_name AND COLUMN_NAME = p_column_name
|
||||
) THEN
|
||||
SET @phase1_iam_sql = CONCAT('ALTER TABLE `', p_table_name, '` ADD COLUMN `',
|
||||
p_column_name, '` ', p_definition);
|
||||
PREPARE phase1_iam_stmt FROM @phase1_iam_sql;
|
||||
EXECUTE phase1_iam_stmt;
|
||||
DEALLOCATE PREPARE phase1_iam_stmt;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL phase1_iam_add_column('iam_tenant', 'approved_at', "DATETIME(3) NULL COMMENT 'Approval time'");
|
||||
CALL phase1_iam_add_column('iam_tenant', 'approved_by', "BIGINT NULL COMMENT 'Reviewer ID'");
|
||||
CALL phase1_iam_add_column('iam_tenant', 'reject_reason', "VARCHAR(500) NULL COMMENT 'Rejection reason'");
|
||||
CALL phase1_iam_add_column('iam_tenant_qualification', 'review_comment', "VARCHAR(500) NULL COMMENT 'Review comment'");
|
||||
CALL phase1_iam_add_column('iam_membership', 'created_by', "BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Creator ID'");
|
||||
CALL phase1_iam_add_column('iam_membership', 'updated_by', "BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Updater ID'");
|
||||
DROP PROCEDURE phase1_iam_add_column;
|
||||
|
||||
-- A global merchant owner template is assigned to newly registered enterprises.
|
||||
INSERT INTO iam_role (
|
||||
id, tenant_id, app_code, role_code, role_name, role_type, status,
|
||||
description, created_by, updated_by, deleted, version
|
||||
) VALUES (
|
||||
4000100, 0, 'MERCHANT', 'MERCHANT_OWNER', '企业管理员', 'SYSTEM', 'ACTIVE',
|
||||
'一期企业全部菜单和操作按钮模板', 1000001, 1000001, 0, 0
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE status='ACTIVE', deleted=0;
|
||||
|
||||
-- Clone the一期 merchant menu/button template into the global scope.
|
||||
INSERT INTO iam_permission (
|
||||
id, tenant_id, app_code, permission_type, parent_id, permission_code,
|
||||
permission_name, route_path, http_method, sort_no, status, deleted
|
||||
)
|
||||
SELECT id + 100000, 0, app_code, permission_type,
|
||||
CASE WHEN parent_id = 0 THEN 0 ELSE parent_id + 100000 END,
|
||||
permission_code, permission_name, route_path, NULL, sort_no, 'ACTIVE', 0
|
||||
FROM iam_permission
|
||||
WHERE tenant_id = 10001 AND app_code = 'MERCHANT'
|
||||
AND permission_type IN ('MENU', 'BUTTON')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_name=VALUES(permission_name), route_path=VALUES(route_path),
|
||||
parent_id=VALUES(parent_id), sort_no=VALUES(sort_no), status='ACTIVE', deleted=0;
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000100, id, 0
|
||||
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;
|
||||
@@ -0,0 +1,44 @@
|
||||
SET NAMES utf8mb4;
|
||||
-- 角色管理闭环权限迁移,可重复执行。
|
||||
USE baiyue_iam;
|
||||
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code,
|
||||
permission_name, sort_no, status, deleted)
|
||||
VALUES
|
||||
(5100011, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:create', '新增企业客户', 10, 'ACTIVE', 0),
|
||||
(5101023, 0, 'PLATFORM', 'BUTTON', 5101020, 'platform:role:edit', '编辑角色', 30, 'ACTIVE', 0),
|
||||
(5101024, 0, 'PLATFORM', 'BUTTON', 5101020, 'platform:role:status', '启停角色', 40, 'ACTIVE', 0),
|
||||
(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),
|
||||
(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),
|
||||
(5300033, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:edit', '编辑角色', 30, 'ACTIVE', 0),
|
||||
(5300034, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:status', '启停角色', 40, 'ACTIVE', 0),
|
||||
(5300035, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:delete', '删除角色', 50, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_type = VALUES(permission_type), parent_id = VALUES(parent_id),
|
||||
permission_name = VALUES(permission_name), sort_no = VALUES(sort_no),
|
||||
status = 'ACTIVE', deleted = 0;
|
||||
|
||||
-- 示例管理员获得新增权限;其他角色仍由管理员按需授权。
|
||||
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')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000101, id, 10001 FROM iam_permission
|
||||
WHERE tenant_id = 10001 AND app_code = 'MERCHANT'
|
||||
AND permission_code IN ('merchant:role:edit', 'merchant:role:status', 'merchant:role:delete')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000100, id, 0 FROM iam_permission
|
||||
WHERE tenant_id = 0 AND app_code = 'MERCHANT'
|
||||
AND permission_code IN ('merchant:role:edit', 'merchant:role:status', 'merchant:role:delete')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
@@ -0,0 +1,391 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Baiyue IAM API / 百悦身份与权限 API
|
||||
description: Tenant, member, role, menu, page, button and API permission management. / 租户、成员、角色、菜单、页面、按钮和 API 权限管理。
|
||||
version: 1.0.1
|
||||
servers:
|
||||
- url: /
|
||||
security:
|
||||
- bearerAuth: []
|
||||
tags:
|
||||
- name: Platform Tenant / 平台租户
|
||||
description: Enterprise registration and review / 企业注册与审核
|
||||
- name: Merchant Tenant / 商户企业
|
||||
description: Current tenant profile and members / 当前企业资料与成员
|
||||
- name: IAM Resources / IAM 资源
|
||||
description: Internal users, addresses and qualifications / 内部用户、地址与资质
|
||||
- name: RBAC
|
||||
description: Platform and merchant role-permission management / 平台端与商户端角色权限管理
|
||||
paths:
|
||||
/api/platform/v1/iam/tenants:
|
||||
post:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: Register enterprise tenant / 创建企业租户
|
||||
description: Create a pending enterprise tenant and review task. Platform identity is required. / 创建待审核企业租户及审核任务,要求平台端身份。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/RegisterRequest'}
|
||||
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/reviews:
|
||||
get:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: List pending tenant reviews / 查询待审核企业
|
||||
description: Return enterprise registrations waiting for a platform decision. / 返回等待平台处理的企业注册审核记录。
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/platform/v1/iam/reviews/{id}/approve:
|
||||
post:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: Approve enterprise registration / 审核通过企业注册
|
||||
description: Approve a pending review and activate the tenant. / 通过待审核申请并激活企业租户。
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/Id'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ReviewRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/platform/v1/iam/reviews/{id}/reject:
|
||||
post:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: Reject enterprise registration / 驳回企业注册
|
||||
description: Reject a pending registration and save the review comment. / 驳回待审核注册申请并保存审核意见。
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/Id'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ReviewRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/platform/v1/iam/internal-users:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: List platform internal users / 查询平台内部用户
|
||||
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 用户资料。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/CreateIamUserRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/merchant/v1/iam/tenant/profile:
|
||||
get:
|
||||
tags: [Merchant Tenant / 商户企业]
|
||||
summary: Get current tenant profile / 查询当前企业资料
|
||||
description: Tenant ID comes only from the verified JWT. / 租户 ID 仅取自已验证 JWT。
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
put:
|
||||
tags: [Merchant Tenant / 商户企业]
|
||||
summary: Update current tenant profile / 更新当前企业资料
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/UpdateProfileRequest'}
|
||||
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/members:
|
||||
get:
|
||||
tags: [Merchant Tenant / 商户企业]
|
||||
summary: List enterprise members / 查询企业成员
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
post:
|
||||
tags: [Merchant Tenant / 商户企业]
|
||||
summary: Add enterprise member / 新增企业成员
|
||||
description: Attach an existing IAM user to the current tenant. / 将已有 IAM 用户加入当前企业。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/CreateMemberRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/merchant/v1/iam/tenant/addresses:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: List tenant addresses / 查询企业地址
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
post:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: Add tenant address / 新增企业地址
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/AddressRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/merchant/v1/iam/tenant/qualifications:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: List tenant qualifications / 查询企业资质
|
||||
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'}
|
||||
get:
|
||||
tags: [RBAC]
|
||||
summary: List roles / 查询角色
|
||||
description: List roles in the selected application and tenant scope. / 查询指定应用及租户范围内的角色。
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
post:
|
||||
tags: [RBAC]
|
||||
summary: Create role / 新增角色
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/CreateRoleRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/{app}/v1/iam/members/roles:
|
||||
put:
|
||||
tags: [RBAC]
|
||||
summary: Assign roles to member / 设置成员角色
|
||||
description: Replace member role assignments in the same tenant and application. / 在同一租户和应用域内替换成员角色。
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/App'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/AssignRolesRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/{app}/v1/iam/permissions:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/App'}
|
||||
get:
|
||||
tags: [RBAC]
|
||||
summary: List menu, page, button and API permissions / 查询菜单、页面、按钮和 API 权限
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
post:
|
||||
tags: [RBAC]
|
||||
summary: Create permission / 新增权限
|
||||
description: Create a MENU, PAGE, BUTTON or API permission node. / 创建 MENU、PAGE、BUTTON 或 API 权限节点。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/CreatePermissionRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/{app}/v1/iam/roles/permissions:
|
||||
put:
|
||||
tags: [RBAC]
|
||||
summary: Grant permissions to role / 设置角色权限
|
||||
description: Replace role permissions; cross-tenant and cross-application grants are rejected. / 替换角色权限,拒绝跨租户和跨应用授权。
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/App'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/GrantPermissionsRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
parameters:
|
||||
Id:
|
||||
name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Resource ID / 资源 ID
|
||||
schema: {type: integer, format: int64}
|
||||
example: 190000000000000001
|
||||
App:
|
||||
name: app
|
||||
in: path
|
||||
required: true
|
||||
description: Application domain / 应用域
|
||||
schema: {type: string, enum: [platform, merchant]}
|
||||
example: platform
|
||||
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'}}}}
|
||||
Unauthorized: {description: Authentication required / 需要登录, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
Forbidden: {description: Permission or tenant scope rejected / 权限或租户范围不匹配, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
NotFound: {description: Resource not found / 资源不存在, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
Conflict: {description: Resource conflict / 资源冲突, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
schemas:
|
||||
ApiResponse:
|
||||
type: object
|
||||
required: [code, message, timestamp]
|
||||
properties:
|
||||
code: {type: string, example: '00000'}
|
||||
message: {type: string, example: Success}
|
||||
data: {}
|
||||
requestId: {type: string}
|
||||
traceId: {type: string}
|
||||
timestamp: {type: string, format: date-time}
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required: [tenantName, legalName, unifiedSocialCreditCode, contactName, contactPhone]
|
||||
properties:
|
||||
tenantName: {type: string, example: 深圳虫虫出海科技有限公司}
|
||||
legalName: {type: string, example: 张三}
|
||||
unifiedSocialCreditCode: {type: string, pattern: '^[0-9A-Z]{18}$', example: 91440300MA5ABCDE12}
|
||||
contactName: {type: string, example: 李四}
|
||||
contactPhone: {type: string, example: '13800138000'}
|
||||
contactEmail: {type: string, format: email, example: contact@example.com}
|
||||
UpdateProfileRequest:
|
||||
type: object
|
||||
required: [tenantName, legalName, contactName, contactPhone]
|
||||
properties:
|
||||
tenantName: {type: string}
|
||||
legalName: {type: string}
|
||||
contactName: {type: string}
|
||||
contactPhone: {type: string}
|
||||
contactEmail: {type: string, format: email}
|
||||
ReviewRequest:
|
||||
type: object
|
||||
properties:
|
||||
comment: {type: string, description: Review comment or rejection reason / 审核意见或驳回原因}
|
||||
CreateIamUserRequest:
|
||||
type: object
|
||||
required: [authUserId, userType, displayName]
|
||||
properties:
|
||||
authUserId: {type: integer, format: int64}
|
||||
userType: {type: string, enum: [PLATFORM, ENTERPRISE]}
|
||||
displayName: {type: string}
|
||||
phone: {type: string}
|
||||
email: {type: string, format: email}
|
||||
CreateMemberRequest:
|
||||
type: object
|
||||
required: [username, password, displayName, memberType]
|
||||
properties:
|
||||
username: {type: string, minLength: 4, maxLength: 64}
|
||||
password: {type: string, format: password, minLength: 8, maxLength: 64}
|
||||
displayName: {type: string, maxLength: 64}
|
||||
phone: {type: string, maxLength: 32}
|
||||
email: {type: string, format: email, maxLength: 128}
|
||||
memberType: {type: string, enum: [OWNER, ADMIN, EMPLOYEE]}
|
||||
memberNo: {type: string}
|
||||
roleIds: {type: array, items: {type: integer, format: int64}}
|
||||
AddressRequest:
|
||||
type: object
|
||||
required: [addressType, detail]
|
||||
properties:
|
||||
addressType: {type: string, enum: [OPERATING, BILLING, RETURN]}
|
||||
countryCode: {type: string, example: CN}
|
||||
province: {type: string}
|
||||
city: {type: string}
|
||||
district: {type: string}
|
||||
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]
|
||||
properties:
|
||||
roleCode: {type: string, example: ORDER_OPERATOR}
|
||||
roleName: {type: string, example: 订单操作员}
|
||||
description: {type: string}
|
||||
CreatePermissionRequest:
|
||||
type: object
|
||||
required: [type, code, name]
|
||||
properties:
|
||||
type: {type: string, enum: [MENU, PAGE, BUTTON, API], example: BUTTON}
|
||||
code: {type: string, example: 'order:firstleg:submit'}
|
||||
name: {type: string, example: 提交头程订单}
|
||||
parentId: {type: integer, format: int64, example: 0}
|
||||
routePath: {type: string}
|
||||
httpMethod: {type: string, enum: [GET, POST, PUT, PATCH, DELETE]}
|
||||
sortNo: {type: integer, example: 10}
|
||||
AssignRolesRequest:
|
||||
type: object
|
||||
required: [membershipId, roleIds]
|
||||
properties:
|
||||
membershipId: {type: integer, format: int64}
|
||||
roleIds: {type: array, items: {type: integer, format: int64}}
|
||||
GrantPermissionsRequest:
|
||||
type: object
|
||||
required: [roleId, permissionIds]
|
||||
properties:
|
||||
roleId: {type: integer, format: int64}
|
||||
permissionIds: {type: array, items: {type: integer, format: int64}}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
import org.springframework.http.server.PathContainer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/** 防止成员数字主键路由再次拦截固定的角色分配路径。 */
|
||||
class MerchantMemberRouteTest {
|
||||
private final PathPatternParser parser = new PathPatternParser();
|
||||
|
||||
@Test
|
||||
void 成员编辑路由只接受数字主键() {
|
||||
var pattern = parser.parse("/api/merchant/v1/iam/members/{membershipId:\\d+}");
|
||||
|
||||
assertThat(pattern.matches(PathContainer.parsePath("/api/merchant/v1/iam/members/123"))).isTrue();
|
||||
assertThat(pattern.matches(PathContainer.parsePath("/api/merchant/v1/iam/members/roles"))).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.AuthorizationDtos.SnapshotRequest;
|
||||
import com.baiyue.iam.mapper.AuthorizationQueryMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class AuthorizationSnapshotServiceTest {
|
||||
@Test
|
||||
void loadsRolesAndPermissionsForActiveMerchantIdentity() {
|
||||
AuthorizationQueryMapper mapper = mock(AuthorizationQueryMapper.class);
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT"))
|
||||
.thenReturn(List.of("ADMIN", "OPERATOR"));
|
||||
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT"))
|
||||
.thenReturn(List.of("order:read", "order:create"));
|
||||
|
||||
var result = service(mapper)
|
||||
.load(new SnapshotRequest(42L, 7L, 9L, "merchant"));
|
||||
|
||||
assertThat(result.getAppCode()).isEqualTo("MERCHANT");
|
||||
assertThat(result.getClientType()).isEqualTo("MERCHANT");
|
||||
assertThat(result.getRoles()).containsExactlyInAnyOrder("ADMIN", "OPERATOR");
|
||||
assertThat(result.getPermissions()).containsExactlyInAnyOrder("order:read", "order:create");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsPlatformIdentityOutsidePlatformTenant() {
|
||||
AuthorizationQueryMapper mapper = mock(AuthorizationQueryMapper.class);
|
||||
|
||||
assertThatThrownBy(() -> service(mapper)
|
||||
.load(new SnapshotRequest(42L, 7L, 9L, "PLATFORM")))
|
||||
.isInstanceOfSatisfying(BizException.class,
|
||||
exception -> assertThat(exception.getResultCode()).isEqualTo(ResultCode.FORBIDDEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInactiveMembership() {
|
||||
AuthorizationQueryMapper mapper = mock(AuthorizationQueryMapper.class);
|
||||
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(0);
|
||||
|
||||
assertThatThrownBy(() -> service(mapper)
|
||||
.load(new SnapshotRequest(42L, 7L, 9L, "MERCHANT")))
|
||||
.isInstanceOfSatisfying(BizException.class,
|
||||
exception -> assertThat(exception.getResultCode()).isEqualTo(ResultCode.FORBIDDEN));
|
||||
verify(mapper).countActiveIdentity(42L, 7L, 9L);
|
||||
}
|
||||
|
||||
private AuthorizationSnapshotService service(AuthorizationQueryMapper mapper) {
|
||||
AuthorizationSnapshotService service = new AuthorizationSnapshotService();
|
||||
ReflectionTestUtils.setField(service, "mapper", mapper);
|
||||
return service;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.baiyue.iam.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.iam.client.AuthAccountClient;
|
||||
import com.baiyue.iam.dto.MemberDtos.*;
|
||||
import com.baiyue.iam.entity.IamMembership;
|
||||
import com.baiyue.iam.entity.IamUser;
|
||||
import com.baiyue.iam.mapper.IamRoleMapper;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class MemberServiceTest {
|
||||
private final MemberRepository repository = mock(MemberRepository.class);
|
||||
private final IamRoleMapper roleMapper = mock(IamRoleMapper.class);
|
||||
private final AuthAccountClient auth = mock(AuthAccountClient.class);
|
||||
private final MemberService service = new MemberService(repository, roleMapper, auth, "internal-token");
|
||||
private IamMembership member;
|
||||
private IamUser user;
|
||||
|
||||
@BeforeEach void setUp() {
|
||||
member = new IamMembership(); member.setId(20L); member.setTenantId(100L); member.setUserId(30L);
|
||||
member.setMemberType("EMPLOYEE"); member.setStatus("ACTIVE");
|
||||
user = new IamUser(); user.setId(30L); user.setAuthUserId(40L); user.setDisplayName("测试成员");
|
||||
when(repository.findByTenantAndId(100L, 20L)).thenReturn(member);
|
||||
when(repository.findUser(30L)).thenReturn(user);
|
||||
when(repository.findRoleIds(100L, 20L)).thenReturn(List.of(50L));
|
||||
}
|
||||
|
||||
@Test void 成员详情返回联系方式和已有角色() {
|
||||
user.setPhone("13800138000"); user.setEmail("member@example.com");
|
||||
MemberResponse result = service.detail(100L, 20L);
|
||||
assertEquals("测试成员", result.displayName());
|
||||
assertEquals("13800138000", result.phone());
|
||||
assertEquals(List.of(50L), result.roleIds());
|
||||
verifyNoInteractions(auth);
|
||||
}
|
||||
|
||||
@Test void 跨租户查询被拒绝() {
|
||||
when(repository.findByTenantAndId(200L, 20L)).thenReturn(null);
|
||||
assertThrows(BizException.class, () -> service.detail(200L, 20L));
|
||||
}
|
||||
|
||||
@Test void 企业所有者不可停用() {
|
||||
member.setMemberType("OWNER");
|
||||
BizException exception = assertThrows(BizException.class, () -> service.changeStatus(100L, 20L,
|
||||
new ChangeMemberStatusRequest("DISABLED"), 20L));
|
||||
assertEquals(ResultCode.FORBIDDEN, exception.getResultCode());
|
||||
verify(auth, never()).status(anyString(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test void 停用成员同步认证账号并保存本地状态() {
|
||||
when(auth.status(anyString(), eq(40L), any())).thenReturn(ApiResponse.success(null));
|
||||
MemberResponse result = service.changeStatus(100L, 20L,
|
||||
new ChangeMemberStatusRequest("DISABLED"), 99L);
|
||||
assertEquals("DISABLED", result.status());
|
||||
verify(repository).save(member);
|
||||
verify(repository).saveUser(user);
|
||||
}
|
||||
|
||||
@Test void 认证服务失败时不得写入本地状态() {
|
||||
when(auth.status(anyString(), eq(40L), any())).thenThrow(new RuntimeException("网络失败"));
|
||||
assertThrows(RuntimeException.class, () -> service.changeStatus(100L, 20L,
|
||||
new ChangeMemberStatusRequest("DISABLED"), 99L));
|
||||
verify(repository, never()).save(member);
|
||||
}
|
||||
|
||||
@Test void 企业所有者不能通过成员编辑接口降级或变更角色() {
|
||||
member.setMemberType("OWNER");
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("所有者", null, null,
|
||||
"EMPLOYEE", null, List.of());
|
||||
|
||||
assertThrows(BizException.class, () -> service.update(100L, 20L, request));
|
||||
|
||||
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
|
||||
}
|
||||
|
||||
@Test void 企业所有者密码不能被成员管理员重置() {
|
||||
member.setMemberType("OWNER");
|
||||
|
||||
assertThrows(BizException.class, () -> service.resetPassword(100L, 20L,
|
||||
new ResetMemberPasswordRequest("OwnerNew123")));
|
||||
|
||||
verify(auth, never()).reset(anyString(), anyLong(), any());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.ProfileDtos.UpdatePersonalProfileRequest;
|
||||
import com.baiyue.iam.entity.IamMembership;
|
||||
import com.baiyue.iam.entity.IamUser;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class PersonalProfileServiceTest {
|
||||
private final MemberRepository repository = mock(MemberRepository.class);
|
||||
private final PersonalProfileService service = new PersonalProfileService(repository);
|
||||
|
||||
@Test void 只能修改当前租户中的本人资料() {
|
||||
IamMembership membership = new IamMembership(); membership.setUserId(3L); membership.setStatus("ACTIVE");
|
||||
IamUser user = new IamUser(); user.setId(3L); user.setAuthUserId(2L);
|
||||
when(repository.findByTenantAndAuthUser(1L, 2L)).thenReturn(membership);
|
||||
when(repository.findUser(3L)).thenReturn(user);
|
||||
var result = service.update(1L, 2L, "tester",
|
||||
new UpdatePersonalProfileRequest("新姓名", "13800138000", "a@example.com"));
|
||||
assertEquals("新姓名", result.displayName());
|
||||
verify(repository).saveUser(user);
|
||||
}
|
||||
|
||||
@Test void 跨租户身份被拒绝() {
|
||||
assertThrows(BizException.class, () -> service.get(9L, 2L, "tester"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.RbacDtos.AssignRolesRequest;
|
||||
import com.baiyue.iam.dto.RbacDtos.GrantPermissionsRequest;
|
||||
import com.baiyue.iam.dto.RbacDtos.CreateRoleRequest;
|
||||
import com.baiyue.iam.dto.RbacDtos.UpdateRoleRequest;
|
||||
import com.baiyue.iam.dto.RbacDtos.ChangeRoleStatusRequest;
|
||||
import com.baiyue.iam.dto.RbacDtos.CreatePermissionRequest;
|
||||
import com.baiyue.iam.entity.IamMembership;
|
||||
import com.baiyue.iam.entity.IamPermission;
|
||||
import com.baiyue.iam.entity.IamRole;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
import com.baiyue.iam.repository.RbacRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RbacServiceTest {
|
||||
@Mock private RbacRepository repository;
|
||||
@Mock private MemberRepository memberRepository;
|
||||
private RbacService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new RbacService();
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
ReflectionTestUtils.setField(service, "memberRepository", memberRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 角色列表返回当前权限用于安全回显() {
|
||||
IamRole role = role(11L, 100L, "MERCHANT", "CUSTOM");
|
||||
when(repository.findRoles(100L, "MERCHANT")).thenReturn(List.of(role));
|
||||
when(repository.findRolePermissionIds(100L, 11L)).thenReturn(Set.of(21L, 22L));
|
||||
|
||||
var result = service.roles(100L, "MERCHANT");
|
||||
|
||||
assertEquals(Set.of(21L, 22L), result.get(0).permissionIds());
|
||||
assertEquals(100L, result.get(0).tenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 权限列表同码时优先当前租户并去除全局模板重复项() {
|
||||
IamPermission global = permission(21L, 0L, "merchant:member:list");
|
||||
IamPermission tenant = permission(22L, 100L, "merchant:member:list");
|
||||
IamPermission fallback = permission(23L, 0L, "merchant:member:status");
|
||||
when(repository.findPermissions(100L, "MERCHANT"))
|
||||
.thenReturn(List.of(global, fallback, tenant));
|
||||
|
||||
var result = service.permissions(100L, "MERCHANT");
|
||||
|
||||
assertEquals(2, result.size());
|
||||
assertEquals(22L, result.get(0).getId());
|
||||
assertEquals(23L, result.get(1).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 跨租户角色访问被拒绝() {
|
||||
when(repository.findRole(11L, 200L, "MERCHANT")).thenReturn(null);
|
||||
|
||||
BizException exception = assertThrows(BizException.class,
|
||||
() -> service.role(200L, "MERCHANT", 11L));
|
||||
|
||||
assertEquals(ResultCode.FORBIDDEN, exception.getResultCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 系统角色禁止删除() {
|
||||
when(repository.findRole(11L, 0L, "PLATFORM"))
|
||||
.thenReturn(role(11L, 0L, "PLATFORM", "SYSTEM"));
|
||||
|
||||
BizException exception = assertThrows(BizException.class,
|
||||
() -> service.deleteRole(0L, "PLATFORM", 11L));
|
||||
|
||||
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
||||
verify(repository, never()).deleteRole(anyLong(), any(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 已分配成员的自定义角色禁止删除() {
|
||||
when(repository.findRole(11L, 100L, "MERCHANT"))
|
||||
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
|
||||
when(repository.isRoleAssigned(100L, 11L)).thenReturn(true);
|
||||
|
||||
BizException exception = assertThrows(BizException.class,
|
||||
() -> service.deleteRole(100L, "MERCHANT", 11L));
|
||||
|
||||
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
||||
verify(repository, never()).deleteRole(anyLong(), any(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 空权限集合可以明确清空授权() {
|
||||
when(repository.findRole(11L, 100L, "MERCHANT"))
|
||||
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
|
||||
|
||||
service.grantPermissions(100L, "MERCHANT", new GrantPermissionsRequest(11L, Set.of()));
|
||||
|
||||
verify(repository).replaceRolePermissions(100L, 11L, Set.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 空角色集合可以明确清空成员授权() {
|
||||
IamMembership membership = new IamMembership();
|
||||
membership.setId(31L);
|
||||
membership.setTenantId(100L);
|
||||
when(memberRepository.findById(31L)).thenReturn(membership);
|
||||
|
||||
service.assignRoles(100L, "MERCHANT", new AssignRolesRequest(31L, Set.of()));
|
||||
|
||||
verify(repository).replaceMemberRoles(100L, "MERCHANT", 31L, Set.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 成员角色回显只返回当前应用域角色() {
|
||||
IamMembership membership = new IamMembership();
|
||||
membership.setId(31L);
|
||||
membership.setTenantId(100L);
|
||||
when(memberRepository.findById(31L)).thenReturn(membership);
|
||||
when(repository.findMemberRoleIds(100L, 31L)).thenReturn(Set.of(11L, 12L));
|
||||
when(repository.findRole(11L, 100L, "MERCHANT"))
|
||||
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
|
||||
when(repository.findRole(12L, 100L, "MERCHANT")).thenReturn(null);
|
||||
|
||||
var result = service.memberRoles(100L, "MERCHANT", 31L);
|
||||
|
||||
assertEquals(Set.of(11L), result.roleIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 同一租户同一应用角色编码重复时返回冲突() {
|
||||
when(repository.existsRoleCode(100L, "MERCHANT", "ORDER_ADMIN", null)).thenReturn(true);
|
||||
|
||||
BizException exception = assertThrows(BizException.class, () -> service.createRole(
|
||||
100L, "MERCHANT", new CreateRoleRequest("ORDER_ADMIN", "订单管理员", null)));
|
||||
|
||||
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
||||
verify(repository, never()).insertRole(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 停用角色不得继续分配给成员() {
|
||||
IamMembership membership = new IamMembership();
|
||||
membership.setId(31L);
|
||||
membership.setTenantId(100L);
|
||||
when(memberRepository.findById(31L)).thenReturn(membership);
|
||||
IamRole disabled = role(11L, 100L, "MERCHANT", "CUSTOM");
|
||||
disabled.setStatus("DISABLED");
|
||||
when(repository.findRole(11L, 100L, "MERCHANT")).thenReturn(disabled);
|
||||
|
||||
BizException exception = assertThrows(BizException.class, () -> service.assignRoles(
|
||||
100L, "MERCHANT", new AssignRolesRequest(31L, Set.of(11L))));
|
||||
|
||||
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
||||
verify(repository, never()).replaceMemberRoles(anyLong(), any(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 自定义角色可以编辑和停用并保持权限回显() {
|
||||
IamRole role = role(11L, 100L, "MERCHANT", "CUSTOM");
|
||||
when(repository.findRole(11L, 100L, "MERCHANT")).thenReturn(role);
|
||||
when(repository.findRolePermissionIds(100L, 11L)).thenReturn(Set.of(21L));
|
||||
|
||||
var updated = service.updateRole(100L, "MERCHANT", 11L,
|
||||
new UpdateRoleRequest("新名称", "新说明"));
|
||||
var disabled = service.changeStatus(100L, "MERCHANT", 11L,
|
||||
new ChangeRoleStatusRequest("DISABLED"));
|
||||
|
||||
assertEquals("新名称", updated.roleName());
|
||||
assertEquals(Set.of(21L), updated.permissionIds());
|
||||
assertEquals("DISABLED", disabled.status());
|
||||
verify(repository, times(2)).updateRole(role);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 未使用的自定义角色可以安全删除() {
|
||||
when(repository.findRole(11L, 100L, "MERCHANT"))
|
||||
.thenReturn(role(11L, 100L, "MERCHANT", "CUSTOM"));
|
||||
when(repository.isRoleAssigned(100L, 11L)).thenReturn(false);
|
||||
|
||||
service.deleteRole(100L, "MERCHANT", 11L);
|
||||
|
||||
verify(repository).deleteRole(100L, "MERCHANT", 11L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 企业所有者角色不能通过普通成员授权接口修改() {
|
||||
IamMembership owner = new IamMembership();
|
||||
owner.setId(31L);
|
||||
owner.setTenantId(100L);
|
||||
owner.setMemberType("OWNER");
|
||||
when(memberRepository.findById(31L)).thenReturn(owner);
|
||||
|
||||
BizException exception = assertThrows(BizException.class, () -> service.assignRoles(
|
||||
100L, "MERCHANT", new AssignRolesRequest(31L, Set.of())));
|
||||
|
||||
assertEquals(ResultCode.FORBIDDEN, exception.getResultCode());
|
||||
verify(repository, never()).replaceMemberRoles(anyLong(), any(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 系统角色权限不能通过普通授权接口修改() {
|
||||
when(repository.findRole(11L, 0L, "PLATFORM"))
|
||||
.thenReturn(role(11L, 0L, "PLATFORM", "SYSTEM"));
|
||||
|
||||
BizException exception = assertThrows(BizException.class, () -> service.grantPermissions(
|
||||
0L, "PLATFORM", new GrantPermissionsRequest(11L, Set.of())));
|
||||
|
||||
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
||||
verify(repository, never()).replaceRolePermissions(anyLong(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 企业所有者系统角色不能分配给普通成员() {
|
||||
IamMembership member = new IamMembership();
|
||||
member.setId(31L);
|
||||
member.setTenantId(100L);
|
||||
member.setMemberType("EMPLOYEE");
|
||||
when(memberRepository.findById(31L)).thenReturn(member);
|
||||
IamRole ownerRole = role(11L, 100L, "MERCHANT", "SYSTEM");
|
||||
ownerRole.setRoleCode("MERCHANT_OWNER");
|
||||
when(repository.findRole(11L, 100L, "MERCHANT")).thenReturn(ownerRole);
|
||||
|
||||
BizException exception = assertThrows(BizException.class, () -> service.assignRoles(
|
||||
100L, "MERCHANT", new AssignRolesRequest(31L, Set.of(11L))));
|
||||
|
||||
assertEquals(ResultCode.FORBIDDEN, exception.getResultCode());
|
||||
verify(repository, never()).replaceMemberRoles(anyLong(), any(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 权限编码重复时返回稳定冲突且不写数据库() {
|
||||
when(repository.existsPermissionCode(100L, "MERCHANT", "merchant:role:test"))
|
||||
.thenReturn(true);
|
||||
CreatePermissionRequest request = new CreatePermissionRequest(
|
||||
"BUTTON", "merchant:role:test", "测试按钮", 0L, null, null, 1);
|
||||
|
||||
BizException exception = assertThrows(BizException.class,
|
||||
() -> service.createPermission(100L, "MERCHANT", request));
|
||||
|
||||
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
||||
verify(repository, never()).insertPermission(any());
|
||||
}
|
||||
|
||||
private IamRole role(Long id, Long tenantId, String appCode, String roleType) {
|
||||
IamRole role = new IamRole();
|
||||
role.setId(id);
|
||||
role.setTenantId(tenantId);
|
||||
role.setAppCode(appCode);
|
||||
role.setRoleCode("ROLE_" + id);
|
||||
role.setRoleName("测试角色");
|
||||
role.setRoleType(roleType);
|
||||
role.setStatus("ACTIVE");
|
||||
return role;
|
||||
}
|
||||
|
||||
private IamPermission permission(Long id, Long tenantId, String code) {
|
||||
IamPermission permission = new IamPermission();
|
||||
permission.setId(id);
|
||||
permission.setTenantId(tenantId);
|
||||
permission.setAppCode("MERCHANT");
|
||||
permission.setPermissionCode(code);
|
||||
permission.setPermissionName(code);
|
||||
permission.setPermissionType("BUTTON");
|
||||
permission.setStatus("ACTIVE");
|
||||
return permission;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
-- Any returned row represents a failed phase-one seed assertion.
|
||||
SELECT 'missing_auth_accounts' AS failure
|
||||
WHERE (SELECT COUNT(*) FROM baiyue_auth.auth_account
|
||||
WHERE username IN ('platform_admin', 'merchant_admin') AND account_status = 'ACTIVE') <> 2
|
||||
UNION ALL
|
||||
SELECT 'missing_active_identities'
|
||||
WHERE (SELECT COUNT(*) FROM baiyue_iam.iam_user u
|
||||
JOIN baiyue_iam.iam_membership m ON m.user_id = u.id
|
||||
WHERE u.auth_user_id IN (1000001, 1000002)
|
||||
AND u.status = 'ACTIVE' AND m.status = 'ACTIVE') <> 2
|
||||
UNION ALL
|
||||
SELECT 'unexpected_permission_type'
|
||||
WHERE EXISTS (SELECT 1 FROM baiyue_iam.iam_permission
|
||||
WHERE id BETWEEN 5100000 AND 5299999 AND permission_type NOT IN ('MENU', 'BUTTON'))
|
||||
UNION ALL
|
||||
SELECT 'invalid_menu_depth'
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM baiyue_iam.iam_permission child
|
||||
JOIN baiyue_iam.iam_permission parent ON parent.id = child.parent_id
|
||||
WHERE child.id BETWEEN 5100000 AND 5299999
|
||||
AND child.permission_type = 'MENU' AND parent.parent_id <> 0
|
||||
)
|
||||
UNION ALL
|
||||
SELECT 'invalid_button_parent'
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM baiyue_iam.iam_permission button_permission
|
||||
LEFT JOIN baiyue_iam.iam_permission menu_permission ON menu_permission.id = button_permission.parent_id
|
||||
WHERE button_permission.id BETWEEN 5100000 AND 5299999
|
||||
AND button_permission.permission_type = 'BUTTON'
|
||||
AND (menu_permission.id IS NULL OR menu_permission.permission_type <> 'MENU' OR menu_permission.parent_id = 0)
|
||||
)
|
||||
UNION ALL
|
||||
SELECT 'platform_admin_not_fully_granted'
|
||||
WHERE (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission WHERE role_id = 4000001)
|
||||
<> (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id = 0 AND app_code = 'PLATFORM' AND permission_type IN ('MENU', 'BUTTON'))
|
||||
UNION ALL
|
||||
SELECT 'merchant_owner_not_fully_granted'
|
||||
WHERE (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission WHERE role_id = 4000101)
|
||||
<> (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id = 10001 AND app_code = 'MERCHANT' AND permission_type IN ('MENU', 'BUTTON'));
|
||||
Reference in New Issue
Block a user