暂存提交

This commit is contained in:
2026-07-31 16:53:07 +08:00
parent 45358ef40f
commit 704cb15ccf
98 changed files with 1944 additions and 134 deletions
@@ -25,18 +25,17 @@ import java.util.Set;
* 因此角色被停用或权限被回收后,下一次查询会立即生效。</p>
*/
@RestController
@Tag(name = "Authorization / 当前用户授权", description = "Returns current member effective permission codes.")
@Tag(name = "当前用户授权", description = "返回当前成员的有效权限编码。")
public class AuthorizationController {
@Resource
private AuthorizationSnapshotService authorizationSnapshotService;
@GetMapping("/api/{app}/v1/iam/authorization/permissions")
@Operation(summary = "Get current effective permissions / 查询当前用户有效权限",
description = "Does not require role-management permissions. / 不依赖角色管理权限。")
@Operation(summary = "查询当前用户有效权限", description = "不依赖角色管理权限。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Permissions returned"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "Authentication required"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Wrong application scope or inactive identity")
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "权限查询成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "需要登录"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "应用权限域错误或身份已停用")
})
public ApiResponse<Set<String>> permissions(@PathVariable String app,
@AuthenticationPrincipal AuthenticatedUser user) {
@@ -49,7 +48,7 @@ public class AuthorizationController {
return switch (app) {
case "platform" -> "PLATFORM";
case "merchant" -> "MERCHANT";
default -> throw new BizException(ResultCode.BAD_REQUEST, "Unsupported application scope / 不支持的应用端");
default -> throw new BizException(ResultCode.BAD_REQUEST, "不支持的应用端");
};
}
}
@@ -6,6 +6,8 @@ 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.dto.TenantDtos.UpdateTenantRequest;
import com.baiyue.iam.dto.TenantDtos.ChangeTenantStatusRequest;
import com.baiyue.iam.entity.IamTenantReview;
import com.baiyue.iam.service.TenantGuard;
import com.baiyue.iam.service.TenantService;
@@ -38,7 +40,7 @@ public class PlatformTenantController {
@GetMapping("/tenants")
@PreAuthorize("hasAuthority('platform:customer:list')")
@Operation(summary = "List enterprise tenants / 查询企业列表", description = "Returns paged merchant enterprise tenants for platform operators. / 平台端分页查询商户企业,支持企业名称、统一社会信用代码、联系人和状态筛选。")
@Operation(summary = "查询企业列表", description = "平台端分页查询商户企业,支持企业名称、统一社会信用代码、联系人和状态筛选。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "企业列表查询成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "分页参数类型无效"),
@@ -57,6 +59,15 @@ public class PlatformTenantController {
return ApiResponse.success(service.pageMerchants(page, size, keyword, status));
}
@GetMapping("/tenants/{tenantId:\\d+}")
@PreAuthorize("hasAuthority('platform:customer:list')")
@Operation(summary = "查询企业客户档案")
public ApiResponse<TenantResponse> tenant(@PathVariable long tenantId,
@AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM");
return ApiResponse.success(service.merchant(tenantId));
}
@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. / 创建待审核商户租户及注册审核单,需要平台端令牌。")
@@ -72,7 +83,28 @@ public class PlatformTenantController {
return ApiResponse.success(service.register(request));
}
@PutMapping("/tenants/{tenantId:\\d+}")
@PreAuthorize("hasAuthority('platform:customer:edit')")
@Operation(summary = "编辑企业客户档案")
public ApiResponse<TenantResponse> updateTenant(@PathVariable long tenantId,
@Valid @RequestBody UpdateTenantRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM");
return ApiResponse.success(service.updateMerchant(tenantId, request));
}
@PutMapping("/tenants/{tenantId:\\d+}/status")
@PreAuthorize("hasAuthority('platform:customer:status')")
@Operation(summary = "启停企业客户")
public ApiResponse<TenantResponse> changeTenantStatus(@PathVariable long tenantId,
@Valid @RequestBody ChangeTenantStatusRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM");
return ApiResponse.success(service.changeMerchantStatus(tenantId, request));
}
@GetMapping("/reviews")
@PreAuthorize("hasAuthority('platform:customer:review')")
@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 / 查询成功"),
@@ -81,6 +81,16 @@ public class ProfileResourceController {
return ApiResponse.success(null);
}
@PutMapping("/api/platform/v1/iam/internal-users/{membershipId:\\d+}/roles")
@PreAuthorize("hasAuthority('platform:user:grant-role')")
@Operation(summary = "分配平台内部用户角色")
public ApiResponse<InternalUserResponse> assignInternalUserRoles(
@PathVariable Long membershipId, @Valid @RequestBody AssignInternalUserRolesRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM");
return ApiResponse.success(internalUserService.assignRoles(membershipId, 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 / 查询成功")})
@@ -122,6 +122,36 @@ public class RbacController {
return ApiResponse.success(service.createPermission(scope.tenantId(), scope.appCode(), request));
}
@PutMapping("/permissions/{permissionId}")
@Operation(summary = "编辑自定义权限资源")
@PreAuthorize("hasAuthority(#app + ':permission:edit')")
public ApiResponse<IamPermission> updatePermission(@PathVariable String app, @PathVariable Long permissionId,
@Valid @RequestBody UpdatePermissionRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
Scope scope = scope(app, user);
return ApiResponse.success(service.updatePermission(scope.tenantId(), scope.appCode(), permissionId, request));
}
@PutMapping("/permissions/{permissionId}/status")
@Operation(summary = "启停自定义权限资源")
@PreAuthorize("hasAuthority(#app + ':permission:status')")
public ApiResponse<IamPermission> changePermissionStatus(@PathVariable String app, @PathVariable Long permissionId,
@Valid @RequestBody ChangePermissionStatusRequest request,
@AuthenticationPrincipal AuthenticatedUser user) {
Scope scope = scope(app, user);
return ApiResponse.success(service.changePermissionStatus(scope.tenantId(), scope.appCode(), permissionId, request));
}
@DeleteMapping("/permissions/{permissionId}")
@Operation(summary = "删除未绑定的自定义权限资源")
@PreAuthorize("hasAuthority(#app + ':permission:delete')")
public ApiResponse<Void> deletePermission(@PathVariable String app, @PathVariable Long permissionId,
@AuthenticationPrincipal AuthenticatedUser user) {
Scope scope = scope(app, user);
service.deletePermission(scope.tenantId(), scope.appCode(), permissionId);
return ApiResponse.success(null);
}
@PutMapping("/roles/permissions")
@Operation(summary = "全量设置角色权限,空集合表示清空")
@PreAuthorize("hasAuthority(#app + ':role:grant')")
@@ -44,6 +44,9 @@ public final class PlatformInternalUserDtos {
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字")
String newPassword) { }
/** 全量替换平台成员角色;空集合表示清空全部自定义角色。 */
public record AssignInternalUserRolesRequest(@NotNull List<Long> roleIds) { }
public record InternalUserResponse(
Long membershipId,
Long userId,
@@ -38,6 +38,17 @@ public final class RbacDtos {
String httpMethod,
Integer sortNo) { }
/** 权限编码和类型参与服务端鉴权,编辑时不可变更。 */
public record UpdatePermissionRequest(
@NotBlank String name,
Long parentId,
String routePath,
String httpMethod,
Integer sortNo) { }
public record ChangePermissionStatusRequest(
@NotBlank @Pattern(regexp = "ACTIVE|DISABLED") String status) { }
public record AssignRolesRequest(@NotNull Long membershipId,
@NotNull Set<Long> roleIds) { }
@@ -30,6 +30,21 @@ public final class TenantDtos {
@Schema(description = "Contact email / 联系邮箱", example = "service@example.com") @Email String contactEmail
) { }
/** 平台端客户档案维护。统一社会信用代码是注册标识,不允许经由普通编辑接口修改。 */
@Schema(name = "PlatformTenantUpdateRequest", description = "Platform maintained merchant tenant profile")
public record UpdateTenantRequest(
@NotBlank String tenantName,
@NotBlank String legalName,
@NotBlank String contactName,
@NotBlank String contactPhone,
@Email String contactEmail
) { }
@Schema(name = "TenantStatusChangeRequest", description = "Platform controlled merchant tenant status")
public record ChangeTenantStatusRequest(
@NotBlank @Pattern(regexp = "ACTIVE|DISABLED") String status
) { }
@Schema(name = "TenantReviewRequest", description = "Enterprise review comment / 企业审核意见")
public record ReviewRequest(
@Schema(description = "Approval or rejection comment / 审核意见", example = "资料完整,审核通过") String comment
@@ -4,6 +4,7 @@ import lombok.Getter;
import lombok.Setter;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
/**
@@ -25,6 +26,10 @@ public class IamPermission {
private String httpMethod;
private Integer sortNo;
private String status;
/** 系统预置资源不得通过租户权限管理接口修改。 */
private Boolean builtIn;
@TableLogic
private Integer deleted;
@@ -46,6 +46,30 @@ public class RbacRepository {
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 void updatePermission(IamPermission permission) { permissionMapper.updateById(permission); }
/** 写操作必须精确匹配所属权限域;全局模板可读,但商户不可修改。 */
public IamPermission findOwnedPermission(Long id, long tenantId, String appCode) {
return permissionMapper.selectOne(new LambdaQueryWrapper<IamPermission>()
.eq(IamPermission::getId, id)
.eq(IamPermission::getTenantId, tenantId)
.eq(IamPermission::getAppCode, appCode));
}
public boolean hasPermissionChildren(long tenantId, String appCode, Long permissionId) {
return permissionMapper.selectCount(new LambdaQueryWrapper<IamPermission>()
.eq(IamPermission::getTenantId, tenantId)
.eq(IamPermission::getAppCode, appCode)
.eq(IamPermission::getParentId, permissionId)) > 0;
}
public boolean isPermissionGranted(long tenantId, Long permissionId) {
return rolePermissionMapper.selectCount(new LambdaQueryWrapper<IamRolePermission>()
.eq(IamRolePermission::getTenantId, tenantId)
.eq(IamRolePermission::getPermissionId, permissionId)) > 0;
}
public void deletePermission(IamPermission permission) { permissionMapper.deleteById(permission.getId()); }
public List<IamRole> findRoles(long tenantId, String appCode) {
return roleMapper.selectList(new LambdaQueryWrapper<IamRole>()
@@ -155,6 +155,19 @@ public class PlatformInternalUserService {
new AuthAccountClient.PasswordRequest(request.newPassword())));
}
@Transactional
@BusinessAudit(event = "PLATFORM_INTERNAL_USER_ROLE_ASSIGN", resourceType = "MEMBERSHIP",
resourceId = "#membershipId", description = "分配平台内部用户角色", captureAfter = false)
public InternalUserResponse assignRoles(Long membershipId, AssignInternalUserRolesRequest request) {
IamMembership membership = required(membershipId);
List<Long> roleIds = normalizeRoles(request.roleIds());
validateRoles(roleIds);
repository.replaceRoles(PLATFORM_TENANT_ID, membershipId, roleIds);
IamUser user = requiredUser(membership.getUserId());
AuthAccountClient.AccountSummary authAccount = account(user.getAuthUserId());
return response(membership, user, authAccount.username(), roleIds);
}
private InternalUserResponse linkExistingAccount(CreateInternalUserRequest request, List<Long> roleIds) {
IamUser existingUser = repository.findUserByAuthId(request.authUserId());
IamMembership existingMembership = repository.findByTenantAndAuthUser(
@@ -131,6 +131,7 @@ public class RbacService {
permission.setHttpMethod(request.httpMethod());
permission.setSortNo(request.sortNo() == null ? 0 : request.sortNo());
permission.setStatus("ACTIVE");
permission.setBuiltIn(false);
try {
repository.insertPermission(permission);
} catch (DataIntegrityViolationException exception) {
@@ -139,6 +140,46 @@ public class RbacService {
return permission;
}
@BusinessAudit(event = "PERMISSION_UPDATE", resourceType = "PERMISSION", resourceId = "#permissionId",
description = "编辑权限资源", captureBefore = true, captureAfter = true)
public IamPermission updatePermission(long tenantId, String appCode, Long permissionId,
UpdatePermissionRequest request) {
IamPermission permission = requireMutablePermission(tenantId, appCode, permissionId);
validatePermissionParent(tenantId, appCode, permissionId, request.parentId());
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());
repository.updatePermission(permission);
return permission;
}
@BusinessAudit(event = "PERMISSION_STATUS_CHANGE", resourceType = "PERMISSION", resourceId = "#permissionId",
description = "启停权限资源", captureBefore = true, captureAfter = true)
public IamPermission changePermissionStatus(long tenantId, String appCode, Long permissionId,
ChangePermissionStatusRequest request) {
IamPermission permission = requireMutablePermission(tenantId, appCode, permissionId);
permission.setStatus(request.status());
repository.updatePermission(permission);
return permission;
}
/** 带有子资源或已授权给角色的资源不得直接删除。 */
@BusinessAudit(event = "PERMISSION_DELETE", resourceType = "PERMISSION", resourceId = "#permissionId",
description = "删除权限资源", captureBefore = true, captureAfter = false)
@Transactional
public void deletePermission(long tenantId, String appCode, Long permissionId) {
IamPermission permission = requireMutablePermission(tenantId, appCode, permissionId);
if (repository.hasPermissionChildren(tenantId, appCode, permissionId)) {
throw new BizException(ResultCode.CONFLICT, "Permission has child resources / 权限资源存在子节点,不能删除");
}
if (repository.isPermissionGranted(tenantId, permissionId)) {
throw new BizException(ResultCode.CONFLICT, "Permission is granted to roles / 权限资源已被角色授予,不能删除");
}
repository.deletePermission(permission);
}
public MemberRolesResponse memberRoles(long tenantId, String appCode, Long membershipId) {
requireMembership(tenantId, membershipId);
Set<Long> scopedIds = new LinkedHashSet<>();
@@ -191,6 +232,34 @@ public class RbacService {
return role;
}
private IamPermission requireMutablePermission(long tenantId, String appCode, Long permissionId) {
IamPermission permission = repository.findOwnedPermission(permissionId, tenantId, appCode);
if (permission == null) throw forbidden();
if (Boolean.TRUE.equals(permission.getBuiltIn())) {
throw new BizException(ResultCode.CONFLICT,
"Built-in permission cannot be changed / 系统预置权限不允许编辑、启停或删除");
}
return permission;
}
private void validatePermissionParent(long tenantId, String appCode, Long permissionId, Long parentId) {
if (parentId == null || parentId == 0) return;
if (parentId.equals(permissionId)) {
throw new BizException(ResultCode.BAD_REQUEST, "Permission cannot be its own parent / 权限不能设置自身为父节点");
}
IamPermission parent = repository.findPermission(parentId, tenantId, appCode);
if (parent == null) throw forbidden();
Long cursor = parent.getParentId();
while (cursor != null && cursor > 0) {
if (cursor.equals(permissionId)) {
throw new BizException(ResultCode.BAD_REQUEST, "Permission parent cycle is not allowed / 权限节点不能形成环");
}
IamPermission ancestor = repository.findPermission(cursor, tenantId, appCode);
if (ancestor == null || cursor.equals(ancestor.getParentId())) break;
cursor = ancestor.getParentId();
}
}
private IamRole requireCustomRole(long tenantId, String appCode, Long roleId) {
IamRole role = requireRole(tenantId, appCode, roleId);
if ("SYSTEM".equals(role.getRoleType())) {
@@ -14,6 +14,8 @@ 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.baiyue.iam.dto.TenantDtos.UpdateTenantRequest;
import com.baiyue.iam.dto.TenantDtos.ChangeTenantStatusRequest;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import org.springframework.stereotype.Service;
@@ -145,6 +147,39 @@ public class TenantService {
return response(tenant);
}
/** 平台端查询客户档案:仅能查商户租户,不暴露平台租户。 */
public TenantResponse merchant(long tenantId) {
return response(requireMerchant(tenantId));
}
@Transactional
@BusinessAudit(event = "PLATFORM_TENANT_UPDATE", resourceType = "TENANT", resourceId = "#tenantId",
description = "平台编辑企业客户档案")
public TenantResponse updateMerchant(long tenantId, UpdateTenantRequest request) {
IamTenant tenant = requireMerchant(tenantId);
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);
}
@Transactional
@BusinessAudit(event = "PLATFORM_TENANT_STATUS_CHANGE", resourceType = "TENANT", resourceId = "#tenantId",
description = "平台启停企业客户")
public TenantResponse changeMerchantStatus(long tenantId, ChangeTenantStatusRequest request) {
IamTenant tenant = requireMerchant(tenantId);
// PENDING/REJECTED 必须走审核流程,不允许状态维护接口绕过审核。
if (!"ACTIVE".equals(tenant.getStatus()) && !"DISABLED".equals(tenant.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "Tenant status must be changed through review / 当前租户状态必须通过审核流程变更");
}
tenant.setStatus(request.status());
repository.saveTenant(tenant);
return response(tenant);
}
@Transactional
@BusinessAudit(event = "TENANT_PROFILE_UPDATE", resourceType = "TENANT",
resourceId = "#tenantId", description = "编辑企业资料")
@@ -166,6 +201,14 @@ public class TenantService {
return tenant.getId() != null && tenant.getId().longValue() == id;
}
private IamTenant requireMerchant(long tenantId) {
IamTenant tenant = repository.findTenant(tenantId);
if (tenant == null || !tenantIdEquals(tenant, tenantId) || !"MERCHANT".equals(tenant.getTenantType())) {
throw new BizException(ResultCode.NOT_FOUND, "Merchant tenant not found / 企业客户不存在");
}
return tenant;
}
private TenantResponse response(IamTenant tenant) {
return new TenantResponse(
tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), tenant.getStatus(),
+1
View File
@@ -158,6 +158,7 @@ CREATE TABLE IF NOT EXISTS iam_permission (
http_method VARCHAR(16) NULL COMMENT 'API权限对应的HTTP方法',
sort_no INT NOT NULL DEFAULT 0 COMMENT '同级显示顺序',
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '权限状态',
built_in 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 '逻辑删除标记',
@@ -121,6 +121,26 @@ ON DUPLICATE KEY UPDATE
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_permission
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, deleted)
VALUES
(5102019, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:create', '平台代客创建订单', NULL, NULL, 90, 'ACTIVE', 0),
(5102024, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:edit', '平台编辑订单草稿', NULL, NULL, 100, 'ACTIVE', 0),
(5102025, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:submit', '平台代客提交订单', NULL, NULL, 110, 'ACTIVE', 0)
ON DUPLICATE KEY UPDATE
permission_name = VALUES(permission_name), parent_id = VALUES(parent_id), status = 'ACTIVE', deleted = 0;
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
SELECT 4000001, id, 0 FROM iam_permission
WHERE id IN (5102019, 5102024, 5102025)
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 id IN (5102019, 5102024, 5102025)
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
-- 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)
@@ -0,0 +1,52 @@
-- 权限资源维护与平台主数据权限迁移。
-- 在 phase1-admin-rbac.sql、rbac-role-management-v2.sql 之后执行,可重复执行。
USE baiyue_iam;
-- 兼容不支持 ADD COLUMN IF NOT EXISTS 的 MySQL 版本,通过元数据判断后执行。
SET @iam_permission_built_in_sql = IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'iam_permission' AND column_name = 'built_in'),
'SELECT 1',
"ALTER TABLE iam_permission ADD COLUMN built_in TINYINT NOT NULL DEFAULT 0 COMMENT '是否为系统预置权限;预置资源不可通过管理接口修改' AFTER status"
);
PREPARE iam_permission_built_in_stmt FROM @iam_permission_built_in_sql;
EXECUTE iam_permission_built_in_stmt;
DEALLOCATE PREPARE iam_permission_built_in_stmt;
-- 一期固定编号范围内的资源均为种子资源,运行期生成的雪花编号不作预置标记。
UPDATE iam_permission
SET built_in = 1
WHERE id BETWEEN 5000000 AND 5399999 AND built_in = 0;
INSERT INTO iam_permission
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name,
sort_no, status, built_in, deleted)
VALUES
(5100012, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:edit', '编辑企业客户', 20, 'ACTIVE', 1, 0),
(5100013, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:status', '启停企业客户', 30, 'ACTIVE', 1, 0),
(5101015, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:grant-role', '分配内部用户角色', 50, 'ACTIVE', 1, 0),
(5101032, 0, 'PLATFORM', 'BUTTON', 5101030, 'platform:permission:edit', '编辑权限资源', 20, 'ACTIVE', 1, 0),
(5101033, 0, 'PLATFORM', 'BUTTON', 5101030, 'platform:permission:status', '启停权限资源', 30, 'ACTIVE', 1, 0),
(5101034, 0, 'PLATFORM', 'BUTTON', 5101030, 'platform:permission:delete', '删除权限资源', 40, 'ACTIVE', 1, 0),
(5103021, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:create', '新增快递公司', 10, 'ACTIVE', 1, 0),
(5103022, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:edit', '编辑快递公司', 20, 'ACTIVE', 1, 0),
(5103023, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:status', '启停快递公司', 30, 'ACTIVE', 1, 0),
(5103031, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:create', '新增往来单位', 10, 'ACTIVE', 1, 0),
(5103032, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:edit', '编辑往来单位', 20, 'ACTIVE', 1, 0),
(5103033, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:status', '启停往来单位', 30, 'ACTIVE', 1, 0)
ON DUPLICATE KEY UPDATE
permission_name = VALUES(permission_name), sort_no = VALUES(sort_no), status = 'ACTIVE',
built_in = 1, 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:customer:edit', 'platform:customer:status', 'platform:user:grant-role',
'platform:permission:edit', 'platform:permission:status', 'platform:permission:delete',
'platform:express:list', 'platform:express:create', 'platform:express:edit', 'platform:express:status',
'platform:counterparty:list', 'platform:counterparty:create', 'platform:counterparty:edit', 'platform:counterparty:status'
)
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
@@ -15,6 +15,7 @@ VALUES
(5101012, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:edit', '编辑用户', 20, 'ACTIVE', 0),
(5101013, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:status', '启停用户', 30, 'ACTIVE', 0),
(5101014, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:reset-password', '重置密码', 40, 'ACTIVE', 0),
(5101015, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:grant-role', '分配内部用户角色', 50, '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),
@@ -32,7 +33,8 @@ SELECT 4000001, id, 0 FROM iam_permission
WHERE tenant_id = 0 AND app_code = 'PLATFORM'
AND permission_code IN ('platform:role:edit', 'platform:role:status', 'platform:role:delete',
'platform:member:list', 'platform:member:grant-role', 'platform:customer:create',
'platform:user:edit', 'platform:user:status', 'platform:user:reset-password')
'platform:user:edit', 'platform:user:status', 'platform:user:reset-password',
'platform:user:grant-role')
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
+162 -7
View File
@@ -20,8 +20,8 @@ paths:
/api/platform/v1/iam/tenants:
get:
tags: [Platform Tenant / 平台租户]
summary: List enterprise tenants / 查询企业客户列表
description: Returns paged merchant tenants. Requires `platform:customer:list`. / 分页查询商户企业客户,需要 `platform:customer:list` 权限。
summary: 查询企业客户列表
description: 分页查询商户企业客户,需要 `platform:customer:list` 权限。
parameters:
- {name: page, in: query, required: false, schema: {type: integer, minimum: 1, default: 1}}
- {name: size, in: query, required: false, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}
@@ -54,8 +54,8 @@ paths:
/api/platform/v1/iam/authorization/permissions:
get:
tags: [IAM Resources / IAM 资源]
summary: Get current effective permissions / 查询当前用户有效权限
description: Returns the current active platform membership's permission codes. It only requires a valid PLATFORM JWT and does not require `platform:role:list`. / 返回当前有效平台成员权限编码;只要求有效 PLATFORM JWT,不依赖 `platform:role:list`。
summary: 查询当前用户有效权限
description: 返回当前有效平台成员权限编码;只要求有效 PLATFORM JWT,不依赖 `platform:role:list`。
responses:
'200':
description: 当前平台成员权限查询成功
@@ -64,11 +64,51 @@ paths:
schema: {$ref: '#/components/schemas/PermissionCodesApiResponse'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
/api/platform/v1/iam/tenants/{tenantId}:
parameters:
- {$ref: '#/components/parameters/TenantId'}
get:
tags: [Platform Tenant / 平台租户]
summary: 查询企业客户档案
responses:
'200': {$ref: '#/components/responses/Success'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
put:
tags: [Platform Tenant / 平台租户]
summary: 编辑企业客户档案
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/UpdateTenantRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
/api/platform/v1/iam/tenants/{tenantId}/status:
parameters:
- {$ref: '#/components/parameters/TenantId'}
put:
tags: [Platform Tenant / 平台租户]
summary: 启停企业客户
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ChangeTenantStatusRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/api/merchant/v1/iam/authorization/permissions:
get:
tags: [IAM Resources / IAM 资源]
summary: Get current effective permissions / 查询当前用户有效权限
description: Returns the current active merchant membership's permission codes. / 返回当前有效商户成员权限编码。
summary: 查询当前用户有效权限
description: 返回当前有效商户成员权限编码。
responses:
'200':
description: 当前商户成员权限查询成功
@@ -193,6 +233,27 @@ paths:
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
/api/platform/v1/iam/internal-users/{membershipId}/roles:
parameters:
- {$ref: '#/components/parameters/MembershipId'}
put:
tags: [IAM 资源]
summary: 全量分配平台内部用户角色
description: 仅允许分配平台租户下已启用的平台端角色;空集合表示清空角色。
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/AssignInternalUserRolesRequest'}
responses:
'200':
description: 角色分配成功并返回最新内部用户模型
content:
application/json:
schema: {$ref: '#/components/schemas/InternalUserApiResponse'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
/api/merchant/v1/iam/tenant/profile:
get:
tags: [Merchant Tenant / 商户企业]
@@ -341,6 +402,49 @@ paths:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
/api/{app}/v1/iam/permissions/{permissionId}:
parameters:
- {$ref: '#/components/parameters/App'}
- {$ref: '#/components/parameters/PermissionId'}
put:
tags: [RBAC]
summary: 编辑自定义权限资源
description: 权限编码和类型不可变更,系统预置资源不可修改。
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/UpdatePermissionRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
delete:
tags: [RBAC]
summary: 删除未绑定的自定义权限资源
description: 系统预置资源、存在子资源或已绑定角色的资源不可删除。
responses:
'200': {$ref: '#/components/responses/Success'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
/api/{app}/v1/iam/permissions/{permissionId}/status:
parameters:
- {$ref: '#/components/parameters/App'}
- {$ref: '#/components/parameters/PermissionId'}
put:
tags: [RBAC]
summary: 启停自定义权限资源
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ChangePermissionStatusRequest'}
responses:
'200': {$ref: '#/components/responses/Success'}
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
components:
securitySchemes:
bearerAuth:
@@ -366,7 +470,19 @@ components:
name: membershipId
in: path
required: true
description: Platform membership ID / 平台成员关系 ID
description: 平台成员关系 ID
schema: {type: integer, format: int64}
TenantId:
name: tenantId
in: path
required: true
description: 企业客户租户 ID
schema: {type: integer, format: int64, minimum: 1}
PermissionId:
name: permissionId
in: path
required: true
description: 权限资源 ID
schema: {type: integer, format: int64}
responses:
Success: {description: Operation succeeded / 操作成功, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
@@ -416,6 +532,12 @@ components:
- type: object
properties:
data: {type: array, items: {$ref: '#/components/schemas/InternalUserResponse'}}
InternalUserApiResponse:
allOf:
- {$ref: '#/components/schemas/ApiResponse'}
- type: object
properties:
data: {$ref: '#/components/schemas/InternalUserResponse'}
TenantResponse:
type: object
required: [id, tenantCode, tenantName, status]
@@ -448,6 +570,20 @@ components:
contactName: {type: string}
contactPhone: {type: string}
contactEmail: {type: string, format: email}
UpdateTenantRequest:
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}
ChangeTenantStatusRequest:
type: object
required: [status]
properties:
status: {type: string, enum: [ACTIVE, DISABLED]}
ReviewRequest:
type: object
properties:
@@ -484,6 +620,11 @@ components:
required: [newPassword]
properties:
newPassword: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true}
AssignInternalUserRolesRequest:
type: object
required: [roleIds]
properties:
roleIds: {type: array, items: {type: integer, format: int64}}
InternalUserResponse:
type: object
required: [membershipId, userId, authUserId, username, displayName, memberType, status, roleIds]
@@ -541,6 +682,20 @@ components:
routePath: {type: string}
httpMethod: {type: string, enum: [GET, POST, PUT, PATCH, DELETE]}
sortNo: {type: integer, example: 10}
UpdatePermissionRequest:
type: object
required: [name]
properties:
name: {type: string}
parentId: {type: integer, format: int64}
routePath: {type: string}
httpMethod: {type: string, enum: [GET, POST, PUT, PATCH, DELETE]}
sortNo: {type: integer}
ChangePermissionStatusRequest:
type: object
required: [status]
properties:
status: {type: string, enum: [ACTIVE, DISABLED]}
AssignRolesRequest:
type: object
required: [membershipId, roleIds]