feat(app): resolve bean conflicts and complete role/department migrations

This commit is contained in:
2026-02-15 18:31:09 +08:00
parent c8e270b791
commit d0a05040f4
34 changed files with 2460 additions and 185 deletions
@@ -1,29 +1,15 @@
package com.ljwx.platform.app.controller;
import com.ljwx.platform.app.facade.MenuFacade;
import com.ljwx.platform.app.facade.dto.menu.CreateMenuRequest;
import com.ljwx.platform.app.facade.dto.menu.MenuResponse;
import com.ljwx.platform.app.facade.dto.menu.UpdateMenuRequest;
import com.ljwx.platform.app.facade.dto.menu.*;
import com.ljwx.platform.core.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "data-plane", description = "菜单管理")
@RestController @RequestMapping("/api/v1/menus")
@RestController
@RequestMapping("/api/v1/menus")
public class MenuController {
private final MenuFacade menuFacade;
@@ -32,49 +18,36 @@ public class MenuController {
this.menuFacade = menuFacade;
}
@Operation(summary = "查询所有菜单(扁平列表,含 parentId)")
@PreAuthorize("hasAuthority('menu:read')")
@GetMapping
public Result<List<MenuResponse>> listAll() {
return Result.success(menuFacade.listAll());
public Result<List<MenuResponse>> list() {
return Result.success(menuFacade.list());
}
@Operation(summary = "查询直接子菜单")
@PreAuthorize("hasAuthority('menu:read')")
@GetMapping("/children")
public Result<List<MenuResponse>> listChildren(
@Parameter(description = "父菜单ID(0=顶级)") @RequestParam Long parentId) {
return Result.success(menuFacade.listChildren(parentId));
}
@Operation(summary = "根据ID查询菜单")
@PreAuthorize("hasAuthority('menu:read')")
@GetMapping("/{id}")
public Result<MenuResponse> getById(
@Parameter(description = "菜单ID") @PathVariable Long id) {
return Result.success(menuFacade.getById(id));
public Result<MenuResponse> get(@PathVariable Long id) {
return Result.success(menuFacade.get(id));
}
@Operation(summary = "创建菜单")
@PreAuthorize("hasAuthority('menu:write')")
@PostMapping
public Result<MenuResponse> create(@Valid @RequestBody CreateMenuRequest request) {
return Result.success(menuFacade.create(request));
}
@Operation(summary = "更新菜单")
@PreAuthorize("hasAuthority('menu:write')")
@PutMapping
public Result<MenuResponse> update(@Valid @RequestBody UpdateMenuRequest request) {
return Result.success(menuFacade.update(request));
@PutMapping("/{id}")
public Result<MenuResponse> update(@PathVariable Long id,
@Valid @RequestBody UpdateMenuRequest request) {
return Result.success(menuFacade.update(id, request));
}
@Operation(summary = "删除菜单(级联软删所有子菜单)")
@PreAuthorize("hasAuthority('menu:write')")
@DeleteMapping("/{id}")
public Result<Void> delete(
@Parameter(description = "菜单ID") @PathVariable Long id) {
public Result<Void> delete(@PathVariable Long id) {
menuFacade.delete(id);
return Result.success();
return Result.success(null);
}
}
/** Returns a tree of menus filtered by current user's authorities. */
@GetMapping("/me")
public Result<List<MenuResponse>> myMenus() {
return Result.success(menuFacade.getMyMenuTree());
}
}
@@ -1,26 +1,13 @@
package com.ljwx.platform.app.controller;
import com.ljwx.platform.app.facade.RoleFacade;
import com.ljwx.platform.app.facade.dto.CreateRoleRequest;
import com.ljwx.platform.app.facade.dto.RoleDTO;
import com.ljwx.platform.app.facade.dto.UpdateRoleRequest;
import com.ljwx.platform.core.result.PageResult;
import com.ljwx.platform.app.facade.dto.role.*;
import com.ljwx.platform.core.result.Result;
import com.ljwx.platform.core.util.PageRequest;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Role CRUD endpoints (tenant-scoped).
*/
@RestController
@RequestMapping("/api/v1/roles")
public class RoleController {
@@ -32,37 +19,47 @@ public class RoleController {
}
@GetMapping
@PreAuthorize("hasAuthority('role:read')")
public Result<PageResult<RoleDTO>> list(PageRequest pageRequest) {
PageResult<RoleDTO> page = roleFacade.list(pageRequest);
return Result.success(page);
public Result<List<RoleResponse>> list() {
return Result.success(roleFacade.list());
}
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('role:read')")
public Result<RoleDTO> getById(@PathVariable Long id) {
RoleDTO role = roleFacade.getById(id);
return Result.success(role);
public Result<RoleResponse> get(@PathVariable Long id) {
return Result.success(roleFacade.get(id));
}
@PostMapping
@PreAuthorize("hasAuthority('role:write')")
public Result<RoleDTO> create(@Valid @RequestBody CreateRoleRequest request) {
RoleDTO role = roleFacade.create(request);
return Result.success("Role created", role);
public Result<RoleResponse> create(@Valid @RequestBody CreateRoleRequest request) {
return Result.success(roleFacade.create(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('role:write')")
public Result<RoleDTO> update(@PathVariable Long id, @Valid @RequestBody UpdateRoleRequest request) {
RoleDTO role = roleFacade.update(id, request);
return Result.success("Role updated", role);
public Result<RoleResponse> update(@PathVariable Long id,
@Valid @RequestBody UpdateRoleRequest request) {
return Result.success(roleFacade.update(id, request));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('role:write')")
public Result<Void> delete(@PathVariable Long id) {
roleFacade.delete(id);
return Result.success("Role deleted", null);
return Result.success(null);
}
}
@PutMapping("/{id}/permissions")
public Result<Void> assignPermissions(@PathVariable Long id,
@Valid @RequestBody AssignPermissionsRequest request) {
roleFacade.assignPermissions(id, request);
return Result.success(null);
}
@PutMapping("/user-roles")
public Result<Void> assignUserRoles(@Valid @RequestBody AssignUserRolesRequest request) {
roleFacade.assignUserRoles(request);
return Result.success(null);
}
@GetMapping("/user-roles/{userId}")
public Result<List<Long>> getUserRoleIds(@PathVariable Long userId) {
return Result.success(roleFacade.getUserRoleIds(userId));
}
}
@@ -0,0 +1,24 @@
package com.ljwx.platform.app.domain.repository;
import com.ljwx.platform.app.domain.model.UserRole;
import java.util.List;
public interface UserRoleRepository {
List<Long> findRoleIdsByUserId(Long userId, Long tenantId);
/** Soft-delete all active bindings for a user in a tenant. */
void softDeleteByUserId(Long userId, Long tenantId, String updateUser, Long updateUserId);
/** Soft-delete a specific user-role binding. */
void softDeleteByUserIdAndRoleId(Long userId, Long roleId, Long tenantId,
String updateUser, Long updateUserId);
void batchInsert(List<UserRole> userRoles);
boolean existsActiveBinding(Long userId, Long roleId, Long tenantId);
/** Soft-delete all bindings for a role (used when deleting a role). */
void softDeleteByRoleId(Long roleId, Long tenantId, String updateUser, Long updateUserId);
}
@@ -1,52 +1,69 @@
package com.ljwx.platform.app.facade;
import com.ljwx.platform.app.appservice.RoleAppService;
import com.ljwx.platform.app.domain.model.Role;
import com.ljwx.platform.app.facade.converter.RoleConverter;
import com.ljwx.platform.app.facade.dto.CreateRoleRequest;
import com.ljwx.platform.app.facade.dto.RoleDTO;
import com.ljwx.platform.app.facade.dto.UpdateRoleRequest;
import com.ljwx.platform.core.result.PageResult;
import com.ljwx.platform.core.util.PageRequest;
import com.ljwx.platform.app.facade.dto.role.*;
import com.ljwx.platform.app.service.RoleAppService;
import com.ljwx.platform.app.service.UserRoleAppService;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* Role facade: DTO conversion + delegates to RoleAppService.
*/
@Component
public class RoleFacade {
private final RoleAppService roleAppService;
private final UserRoleAppService userRoleAppService;
public RoleFacade(RoleAppService roleAppService) {
public RoleFacade(RoleAppService roleAppService,
UserRoleAppService userRoleAppService) {
this.roleAppService = roleAppService;
this.userRoleAppService = userRoleAppService;
}
public RoleDTO getById(Long id) {
public List<RoleResponse> list() {
return roleAppService.list().stream()
.map(role -> {
role.setPermissionIds(
roleAppService.getById(role.getId()).getPermissionIds());
return RoleConverter.toResponse(role);
})
.collect(Collectors.toList());
}
public RoleResponse get(Long id) {
Role role = roleAppService.getById(id);
return RoleConverter.toDTO(role);
role.setPermissionIds(
roleAppService.getById(id).getPermissionIds());
return RoleConverter.toResponse(role);
}
public PageResult<RoleDTO> list(PageRequest pageRequest) {
List<Role> roles = roleAppService.listByTenant(pageRequest.getOffset(), pageRequest.getSize());
long total = roleAppService.countByTenant();
List<RoleDTO> dtos = roles.stream().map(RoleConverter::toDTO).toList();
return PageResult.of(dtos, total, pageRequest.getPage(), pageRequest.getSize());
public RoleResponse create(CreateRoleRequest request) {
Role role = RoleConverter.toModel(request);
Role created = roleAppService.create(role);
return RoleConverter.toResponse(created);
}
public RoleDTO create(CreateRoleRequest request) {
Role role = roleAppService.create(request.getCode(), request.getName(), request.getDescription());
return RoleConverter.toDTO(role);
}
public RoleDTO update(Long id, UpdateRoleRequest request) {
Role role = roleAppService.update(id, request.getName(), request.getDescription());
return RoleConverter.toDTO(role);
public RoleResponse update(Long id, UpdateRoleRequest request) {
Role updates = RoleConverter.toModel(request);
Role updated = roleAppService.update(id, updates);
return RoleConverter.toResponse(updated);
}
public void delete(Long id) {
roleAppService.delete(id);
}
}
public void assignPermissions(Long roleId, AssignPermissionsRequest request) {
roleAppService.assignPermissions(roleId, request.getPermissionIds());
}
public void assignUserRoles(AssignUserRolesRequest request) {
userRoleAppService.assignRoles(request.getUserId(), request.getRoleIds());
}
public List<Long> getUserRoleIds(Long userId) {
return userRoleAppService.getRoleIds(userId);
}
}
@@ -1,35 +1,46 @@
package com.ljwx.platform.app.facade.converter;
import com.ljwx.platform.app.domain.model.Role;
import com.ljwx.platform.app.facade.dto.CreateRoleRequest;
import com.ljwx.platform.app.facade.dto.RoleDTO;
import com.ljwx.platform.app.facade.dto.role.CreateRoleRequest;
import com.ljwx.platform.app.facade.dto.role.RoleResponse;
import com.ljwx.platform.app.facade.dto.role.UpdateRoleRequest;
/**
* Role entity ↔ DTO converter (hand-written, MVP).
*/
public final class RoleConverter {
public class RoleConverter {
private RoleConverter() {
private RoleConverter() {}
public static Role toModel(CreateRoleRequest req) {
Role r = new Role();
r.setCode(req.getCode());
r.setName(req.getName());
r.setDescription(req.getDescription());
r.setSortOrder(req.getSortOrder());
r.setStatus(req.getStatus());
return r;
}
public static RoleDTO toDTO(Role role) {
if (role == null) return null;
RoleDTO dto = new RoleDTO();
dto.setId(role.getId());
dto.setCode(role.getCode());
dto.setName(role.getName());
dto.setDescription(role.getDescription());
dto.setCreatedAt(role.getCreatedAt());
dto.setUpdatedAt(role.getUpdatedAt());
return dto;
public static Role toModel(UpdateRoleRequest req) {
Role r = new Role();
r.setName(req.getName());
r.setDescription(req.getDescription());
r.setSortOrder(req.getSortOrder());
r.setStatus(req.getStatus());
return r;
}
public static Role fromCreateRequest(CreateRoleRequest request, Long tenantId) {
Role role = new Role();
role.setTenantId(tenantId);
role.setCode(request.getCode());
role.setName(request.getName());
role.setDescription(request.getDescription());
return role;
public static RoleResponse toResponse(Role role) {
RoleResponse resp = new RoleResponse();
resp.setId(role.getId());
resp.setCode(role.getCode());
resp.setName(role.getName());
resp.setDescription(role.getDescription());
resp.setSortOrder(role.getSortOrder());
resp.setStatus(role.getStatus());
resp.setPermissionIds(role.getPermissionIds());
resp.setCreateUser(role.getCreateUser());
resp.setCreateTime(role.getCreateTime());
resp.setUpdateUser(role.getUpdateUser());
resp.setUpdateTime(role.getUpdateTime());
return resp;
}
}
}
@@ -0,0 +1,13 @@
package com.ljwx.platform.app.facade.dto.role;
import jakarta.validation.constraints.NotNull;
import java.util.List;
public class AssignPermissionsRequest {
@NotNull(message = "权限ID列表不能为空")
private List<Long> permissionIds;
public List<Long> getPermissionIds() { return permissionIds; }
public void setPermissionIds(List<Long> permissionIds) { this.permissionIds = permissionIds; }
}
@@ -0,0 +1,19 @@
package com.ljwx.platform.app.facade.dto.role;
import jakarta.validation.constraints.NotNull;
import java.util.List;
public class AssignUserRolesRequest {
@NotNull(message = "用户ID不能为空")
private Long userId;
@NotNull(message = "角色ID列表不能为空")
private List<Long> roleIds;
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public List<Long> getRoleIds() { return roleIds; }
public void setRoleIds(List<Long> roleIds) { this.roleIds = roleIds; }
}
@@ -0,0 +1,36 @@
package com.ljwx.platform.app.facade.dto.role;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class CreateRoleRequest {
@NotBlank(message = "角色编码不能为空")
@Size(max = 50, message = "角色编码最长50字符")
private String code;
@NotBlank(message = "角色名称不能为空")
@Size(max = 100, message = "角色名称最长100字符")
private String name;
@Size(max = 200, message = "描述最长200字符")
private String description;
private Integer sortOrder;
private Integer status;
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Integer getSortOrder() { return sortOrder; }
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
}
@@ -0,0 +1,55 @@
package com.ljwx.platform.app.facade.dto.role;
import java.time.LocalDateTime;
import java.util.List;
public class RoleResponse {
private Long id;
private String code;
private String name;
private String description;
private Integer sortOrder;
private Integer status;
private List<Long> permissionIds;
private String createUser;
private LocalDateTime createTime;
private String updateUser;
private LocalDateTime updateTime;
// ── getters & setters ──
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Integer getSortOrder() { return sortOrder; }
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public List<Long> getPermissionIds() { return permissionIds; }
public void setPermissionIds(List<Long> permissionIds) { this.permissionIds = permissionIds; }
public String getCreateUser() { return createUser; }
public void setCreateUser(String createUser) { this.createUser = createUser; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
public String getUpdateUser() { return updateUser; }
public void setUpdateUser(String updateUser) { this.updateUser = updateUser; }
public LocalDateTime getUpdateTime() { return updateTime; }
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
}
@@ -0,0 +1,29 @@
package com.ljwx.platform.app.facade.dto.role;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class UpdateRoleRequest {
@NotBlank(message = "角色名称不能为空")
@Size(max = 100, message = "角色名称最长100字符")
private String name;
@Size(max = 200, message = "描述最长200字符")
private String description;
private Integer sortOrder;
private Integer status;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Integer getSortOrder() { return sortOrder; }
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
}
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
@Repository("legacyRoleRepository")
public class RoleRepositoryImpl implements RoleRepository {
private final RoleMapper roleMapper;
@@ -46,4 +46,24 @@ public class RoleRepositoryImpl implements RoleRepository {
public void deleteById(Long id, Long tenantId) {
roleMapper.deleteById(id, tenantId);
}
}
@Override
public List<Long> findPermissionIdsByRoleId(Long roleId) {
return List.of();
}
@Override
public void deletePermissionsByRoleId(Long roleId) {
// no-op in legacy implementation
}
@Override
public void insertRolePermission(Long roleId, Long permissionId) {
// no-op in legacy implementation
}
@Override
public boolean existsPermission(Long permissionId) {
return false;
}
}
@@ -0,0 +1,37 @@
package com.ljwx.platform.app.infrastructure.mybatis.mapper;
import com.ljwx.platform.app.domain.model.Role;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface NewRoleMapper {
List<Role> selectAllByTenantId(@Param("tenantId") Long tenantId);
Role selectById(@Param("id") Long id, @Param("tenantId") Long tenantId);
Role selectByCode(@Param("code") String code, @Param("tenantId") Long tenantId);
void insert(Role role);
void update(Role role);
void softDelete(@Param("id") Long id,
@Param("tenantId") Long tenantId,
@Param("updateUser") String updateUser,
@Param("updateUserId") Long updateUserId);
// ── permission binding ──
List<Long> selectPermissionIdsByRoleId(@Param("roleId") Long roleId);
void deletePermissionsByRoleId(@Param("roleId") Long roleId);
void insertRolePermission(@Param("roleId") Long roleId,
@Param("permissionId") Long permissionId);
int countPermissionById(@Param("permissionId") Long permissionId);
}
@@ -0,0 +1,36 @@
package com.ljwx.platform.app.infrastructure.mybatis.mapper;
import com.ljwx.platform.app.domain.model.UserRole;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface NewUserRoleMapper {
List<Long> selectRoleIdsByUserId(@Param("userId") Long userId,
@Param("tenantId") Long tenantId);
void softDeleteByUserId(@Param("userId") Long userId,
@Param("tenantId") Long tenantId,
@Param("updateUser") String updateUser,
@Param("updateUserId") Long updateUserId);
void softDeleteByUserIdAndRoleId(@Param("userId") Long userId,
@Param("roleId") Long roleId,
@Param("tenantId") Long tenantId,
@Param("updateUser") String updateUser,
@Param("updateUserId") Long updateUserId);
void batchInsert(@Param("list") List<UserRole> userRoles);
int countActiveBinding(@Param("userId") Long userId,
@Param("roleId") Long roleId,
@Param("tenantId") Long tenantId);
void softDeleteByRoleId(@Param("roleId") Long roleId,
@Param("tenantId") Long tenantId,
@Param("updateUser") String updateUser,
@Param("updateUserId") Long updateUserId);
}
@@ -0,0 +1,71 @@
package com.ljwx.platform.app.infrastructure.mybatis.repository;
import com.ljwx.platform.app.domain.model.Role;
import com.ljwx.platform.app.domain.repository.RoleRepository;
import com.ljwx.platform.app.infrastructure.mybatis.mapper.NewRoleMapper;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository("mybatisRoleRepository")
@Primary
public class RoleRepositoryImpl implements RoleRepository {
private final NewRoleMapper mapper;
public RoleRepositoryImpl(NewRoleMapper mapper) {
this.mapper = mapper;
}
@Override
public List<Role> findAllByTenantId(Long tenantId) {
return mapper.selectAllByTenantId(tenantId);
}
@Override
public Optional<Role> findById(Long id, Long tenantId) {
return Optional.ofNullable(mapper.selectById(id, tenantId));
}
@Override
public Optional<Role> findByCode(String code, Long tenantId) {
return Optional.ofNullable(mapper.selectByCode(code, tenantId));
}
@Override
public void insert(Role role) {
mapper.insert(role);
}
@Override
public void update(Role role) {
mapper.update(role);
}
@Override
public void softDelete(Long id, Long tenantId, String updateUser, Long updateUserId) {
mapper.softDelete(id, tenantId, updateUser, updateUserId);
}
@Override
public List<Long> findPermissionIdsByRoleId(Long roleId) {
return mapper.selectPermissionIdsByRoleId(roleId);
}
@Override
public void deletePermissionsByRoleId(Long roleId) {
mapper.deletePermissionsByRoleId(roleId);
}
@Override
public void insertRolePermission(Long roleId, Long permissionId) {
mapper.insertRolePermission(roleId, permissionId);
}
@Override
public boolean existsPermission(Long permissionId) {
return mapper.countPermissionById(permissionId) > 0;
}
}
@@ -0,0 +1,55 @@
package com.ljwx.platform.app.infrastructure.mybatis.repository;
import com.ljwx.platform.app.domain.model.UserRole;
import com.ljwx.platform.app.domain.repository.UserRoleRepository;
import com.ljwx.platform.app.infrastructure.mybatis.mapper.NewUserRoleMapper;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository("mybatisUserRoleRepository")
@Primary
public class UserRoleRepositoryImpl implements UserRoleRepository {
private final NewUserRoleMapper mapper;
public UserRoleRepositoryImpl(NewUserRoleMapper mapper) {
this.mapper = mapper;
}
@Override
public List<Long> findRoleIdsByUserId(Long userId, Long tenantId) {
return mapper.selectRoleIdsByUserId(userId, tenantId);
}
@Override
public void softDeleteByUserId(Long userId, Long tenantId,
String updateUser, Long updateUserId) {
mapper.softDeleteByUserId(userId, tenantId, updateUser, updateUserId);
}
@Override
public void softDeleteByUserIdAndRoleId(Long userId, Long roleId, Long tenantId,
String updateUser, Long updateUserId) {
mapper.softDeleteByUserIdAndRoleId(userId, roleId, tenantId, updateUser, updateUserId);
}
@Override
public void batchInsert(List<UserRole> userRoles) {
if (userRoles != null && !userRoles.isEmpty()) {
mapper.batchInsert(userRoles);
}
}
@Override
public boolean existsActiveBinding(Long userId, Long roleId, Long tenantId) {
return mapper.countActiveBinding(userId, roleId, tenantId) > 0;
}
@Override
public void softDeleteByRoleId(Long roleId, Long tenantId,
String updateUser, Long updateUserId) {
mapper.softDeleteByRoleId(roleId, tenantId, updateUser, updateUserId);
}
}
@@ -0,0 +1,112 @@
package com.ljwx.platform.app.service;
import com.ljwx.platform.app.domain.model.UserRole;
import com.ljwx.platform.app.domain.repository.UserRoleRepository;
import com.ljwx.platform.web.security.SecurityUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
@Service
public class UserRoleAppService {
private final UserRoleRepository userRoleRepository;
public UserRoleAppService(UserRoleRepository userRoleRepository) {
this.userRoleRepository = userRoleRepository;
}
public List<Long> getRoleIds(Long userId) {
Long tenantId = SecurityUtil.currentTenantId();
return userRoleRepository.findRoleIdsByUserId(userId, tenantId);
}
/**
* Full replacement: soft-delete existing bindings, insert new ones.
*/
@Transactional
public void assignRoles(Long userId, List<Long> roleIds) {
List<Long> uniqueIds = dedup(roleIds);
Long tenantId = SecurityUtil.currentTenantId();
String username = SecurityUtil.currentUsername();
Long currentUserId = SecurityUtil.currentUserId();
// soft-delete all existing
userRoleRepository.softDeleteByUserId(userId, tenantId, username, currentUserId);
// insert new bindings
if (!uniqueIds.isEmpty()) {
List<UserRole> records = buildRecords(userId, uniqueIds, tenantId, username, currentUserId);
userRoleRepository.batchInsert(records);
}
}
/**
* Incremental add: only insert roles not already bound.
*/
@Transactional
public void addRoles(Long userId, List<Long> roleIds) {
List<Long> uniqueIds = dedup(roleIds);
Long tenantId = SecurityUtil.currentTenantId();
String username = SecurityUtil.currentUsername();
Long currentUserId = SecurityUtil.currentUserId();
List<UserRole> toInsert = new ArrayList<>();
for (Long roleId : uniqueIds) {
if (!userRoleRepository.existsActiveBinding(userId, roleId, tenantId)) {
UserRole ur = new UserRole();
ur.setTenantId(tenantId);
ur.setUserId(userId);
ur.setRoleId(roleId);
ur.setCreateUser(username);
ur.setCreateUserId(currentUserId);
ur.setCreateTime(LocalDateTime.now());
toInsert.add(ur);
}
}
if (!toInsert.isEmpty()) {
userRoleRepository.batchInsert(toInsert);
}
}
/**
* Remove specific role bindings.
*/
@Transactional
public void removeRoles(Long userId, List<Long> roleIds) {
Long tenantId = SecurityUtil.currentTenantId();
String username = SecurityUtil.currentUsername();
Long currentUserId = SecurityUtil.currentUserId();
for (Long roleId : roleIds) {
userRoleRepository.softDeleteByUserIdAndRoleId(userId, roleId, tenantId, username, currentUserId);
}
}
// ── private helpers ──
private static List<Long> dedup(List<Long> ids) {
if (ids == null || ids.isEmpty()) return List.of();
return new ArrayList<>(new LinkedHashSet<>(ids));
}
private List<UserRole> buildRecords(Long userId, List<Long> roleIds,
Long tenantId, String username, Long currentUserId) {
LocalDateTime now = LocalDateTime.now();
List<UserRole> list = new ArrayList<>(roleIds.size());
for (Long roleId : roleIds) {
UserRole ur = new UserRole();
ur.setTenantId(tenantId);
ur.setUserId(userId);
ur.setRoleId(roleId);
ur.setCreateUser(username);
ur.setCreateUserId(currentUserId);
ur.setCreateTime(now);
list.add(ur);
}
return list;
}
}
@@ -0,0 +1,223 @@
-- V110: Enhance users table, add tenant_id to roles, create user_roles table.
-- ============================================================
-- 1. users 表:新增业务字段
-- ============================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'nickname') THEN
ALTER TABLE users ADD COLUMN nickname VARCHAR(50) NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'phone') THEN
ALTER TABLE users ADD COLUMN phone VARCHAR(20) NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'email') THEN
ALTER TABLE users ADD COLUMN email VARCHAR(100) NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'avatar') THEN
ALTER TABLE users ADD COLUMN avatar VARCHAR(255) NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'status') THEN
ALTER TABLE users ADD COLUMN status SMALLINT NOT NULL DEFAULT 1;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'remark') THEN
ALTER TABLE users ADD COLUMN remark VARCHAR(200) NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'department_id') THEN
ALTER TABLE users ADD COLUMN department_id BIGINT NULL;
END IF;
END
$$;
-- ============================================================
-- 2. roles 表:追加 tenant_id、审计字段、软删除
-- ============================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'roles' AND column_name = 'tenant_id') THEN
ALTER TABLE roles ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'roles' AND column_name = 'description') THEN
ALTER TABLE roles ADD COLUMN description VARCHAR(200) NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'roles' AND column_name = 'sort_order') THEN
ALTER TABLE roles ADD COLUMN sort_order INT NOT NULL DEFAULT 0;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'roles' AND column_name = 'status') THEN
ALTER TABLE roles ADD COLUMN status SMALLINT NOT NULL DEFAULT 1;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'roles' AND column_name = 'is_deleted') THEN
ALTER TABLE roles ADD COLUMN is_deleted SMALLINT NOT NULL DEFAULT 0;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'roles' AND column_name = 'create_user') THEN
ALTER TABLE roles ADD COLUMN create_user VARCHAR(40) NOT NULL DEFAULT 'system';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'roles' AND column_name = 'create_user_id') THEN
ALTER TABLE roles ADD COLUMN create_user_id BIGINT NOT NULL DEFAULT 0;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'roles' AND column_name = 'create_time') THEN
ALTER TABLE roles ADD COLUMN create_time TIMESTAMP NOT NULL DEFAULT NOW();
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'roles' AND column_name = 'update_user') THEN
ALTER TABLE roles ADD COLUMN update_user VARCHAR(40) NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'roles' AND column_name = 'update_user_id') THEN
ALTER TABLE roles ADD COLUMN update_user_id BIGINT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'roles' AND column_name = 'update_time') THEN
ALTER TABLE roles ADD COLUMN update_time TIMESTAMP NULL;
END IF;
END
$$;
CREATE INDEX IF NOT EXISTS idx_roles_tenant_id ON roles (tenant_id);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_indexes
WHERE tablename = 'roles' AND indexname = 'uk_roles_tenant_code') THEN
CREATE UNIQUE INDEX uk_roles_tenant_code ON roles (tenant_id, code)
WHERE is_deleted = 0;
END IF;
END
$$;
-- ============================================================
-- 3. user_roles 表:建表(如不存在)
-- ============================================================
CREATE TABLE IF NOT EXISTS user_roles (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 1,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
is_deleted SMALLINT NOT NULL DEFAULT 0,
create_user VARCHAR(40) NOT NULL DEFAULT 'system',
create_user_id BIGINT NOT NULL DEFAULT 0,
create_time TIMESTAMP NOT NULL DEFAULT NOW(),
update_user VARCHAR(40) NULL,
update_user_id BIGINT NULL,
update_time TIMESTAMP NULL
);
-- ============================================================
-- 3b. user_roles 表:如果表已存在但缺少列,逐个补齐
-- ============================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_roles' AND column_name = 'tenant_id') THEN
ALTER TABLE user_roles ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_roles' AND column_name = 'is_deleted') THEN
ALTER TABLE user_roles ADD COLUMN is_deleted SMALLINT NOT NULL DEFAULT 0;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_roles' AND column_name = 'create_user') THEN
ALTER TABLE user_roles ADD COLUMN create_user VARCHAR(40) NOT NULL DEFAULT 'system';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_roles' AND column_name = 'create_user_id') THEN
ALTER TABLE user_roles ADD COLUMN create_user_id BIGINT NOT NULL DEFAULT 0;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_roles' AND column_name = 'create_time') THEN
ALTER TABLE user_roles ADD COLUMN create_time TIMESTAMP NOT NULL DEFAULT NOW();
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_roles' AND column_name = 'update_user') THEN
ALTER TABLE user_roles ADD COLUMN update_user VARCHAR(40) NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_roles' AND column_name = 'update_user_id') THEN
ALTER TABLE user_roles ADD COLUMN update_user_id BIGINT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_roles' AND column_name = 'update_time') THEN
ALTER TABLE user_roles ADD COLUMN update_time TIMESTAMP NULL;
END IF;
END
$$;
-- ============================================================
-- 3c. user_roles 索引(列已确保存在后再创建)
-- ============================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_indexes
WHERE tablename = 'user_roles'
AND indexname = 'uk_user_roles_tenant_user_role') THEN
CREATE UNIQUE INDEX uk_user_roles_tenant_user_role
ON user_roles (tenant_id, user_id, role_id)
WHERE is_deleted = 0;
END IF;
END
$$;
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles (user_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_role_id ON user_roles (role_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_tenant_id ON user_roles (tenant_id);
-- ============================================================
-- 4. role_permissions 表:确保存在
-- ============================================================
CREATE TABLE IF NOT EXISTS role_permissions (
id BIGSERIAL PRIMARY KEY,
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
UNIQUE (role_id, permission_id)
);
CREATE INDEX IF NOT EXISTS idx_role_permissions_role_id ON role_permissions (role_id);
-- ============================================================
-- 5. permissions 表:确保存在
-- ============================================================
CREATE TABLE IF NOT EXISTS permissions (
id BIGSERIAL PRIMARY KEY,
key VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description VARCHAR(200) NULL,
create_time TIMESTAMP NOT NULL DEFAULT NOW()
);
@@ -0,0 +1,53 @@
-- V111: Seed permissions, bind to ALL tenant ADMIN roles, fill menu permission_key.
-- ============================================================
-- 1. Upsert 9 permissions (列名 code,不是 key)
-- ============================================================
INSERT INTO permissions (code, name) VALUES ('user:read', '用户查看') ON CONFLICT (code) DO NOTHING;
INSERT INTO permissions (code, name) VALUES ('user:write', '用户管理') ON CONFLICT (code) DO NOTHING;
INSERT INTO permissions (code, name) VALUES ('role:read', '角色查看') ON CONFLICT (code) DO NOTHING;
INSERT INTO permissions (code, name) VALUES ('role:write', '角色管理') ON CONFLICT (code) DO NOTHING;
INSERT INTO permissions (code, name) VALUES ('dept:read', '部门查看') ON CONFLICT (code) DO NOTHING;
INSERT INTO permissions (code, name) VALUES ('dict:read', '字典查看') ON CONFLICT (code) DO NOTHING;
INSERT INTO permissions (code, name) VALUES ('config:read', '配置查看') ON CONFLICT (code) DO NOTHING;
INSERT INTO permissions (code, name) VALUES ('menu:read', '菜单查看') ON CONFLICT (code) DO NOTHING;
INSERT INTO permissions (code, name) VALUES ('menu:write', '菜单管理') ON CONFLICT (code) DO NOTHING;
-- ============================================================
-- 2. Bind all 9 permissions to EVERY tenant's ADMIN role
-- ============================================================
DO $$
DECLARE
admin_role RECORD;
perm RECORD;
BEGIN
FOR admin_role IN
SELECT id FROM roles
WHERE code = 'ADMIN' AND is_deleted = 0
LOOP
FOR perm IN
SELECT id FROM permissions
WHERE code IN (
'user:read', 'user:write',
'role:read', 'role:write',
'dept:read', 'dict:read', 'config:read',
'menu:read', 'menu:write'
)
LOOP
INSERT INTO role_permissions (role_id, permission_id)
VALUES (admin_role.id, perm.id)
ON CONFLICT (role_id, permission_id) DO NOTHING;
END LOOP;
END LOOP;
END
$$;
-- ============================================================
-- 3. Fill permission_key for all 6 second-level menus
-- ============================================================
UPDATE menus SET permission_key = 'user:read' WHERE menu_code = 'MENU_USER' AND permission_key IS NULL;
UPDATE menus SET permission_key = 'role:read' WHERE menu_code = 'MENU_ROLE' AND permission_key IS NULL;
UPDATE menus SET permission_key = 'dept:read' WHERE menu_code = 'MENU_DEPT' AND permission_key IS NULL;
UPDATE menus SET permission_key = 'dict:read' WHERE menu_code = 'MENU_DICT' AND permission_key IS NULL;
UPDATE menus SET permission_key = 'config:read' WHERE menu_code = 'MENU_CONFIG' AND permission_key IS NULL;
UPDATE menus SET permission_key = 'menu:read' WHERE menu_code = 'MENU_MENU' AND permission_key IS NULL;
@@ -0,0 +1,101 @@
-- ============================================================
-- V112 全局审计字段对齐
--
-- 规范:
-- create_user VARCHAR(40) NOT NULL
-- create_user_id BIGINT NOT NULL
-- create_time TIMESTAMP NOT NULL DEFAULT NOW()
-- update_user VARCHAR(40) NULL (no default)
-- update_user_id BIGINT NULL (no default)
-- update_time TIMESTAMP NULL (no default)
-- is_deleted SMALLINT NOT NULL DEFAULT 0
--
-- 实际存在的表:users, dict_types, dict_data, sys_configs,
-- menus, roles, user_roles
-- 注意:departments 表不存在,已排除
-- ============================================================
DO $$
DECLARE
t TEXT;
BEGIN
FOREACH t IN ARRAY ARRAY[
'users',
'dict_types',
'dict_data',
'sys_configs',
'menus',
'roles',
'user_roles'
]
LOOP
-- ── create_user: VARCHAR(40) NOT NULL ──
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = t AND column_name = 'create_user'
) THEN
EXECUTE format('ALTER TABLE %I ALTER COLUMN create_user TYPE VARCHAR(40)', t);
EXECUTE format('ALTER TABLE %I ALTER COLUMN create_user SET NOT NULL', t);
END IF;
-- ── create_user_id: BIGINT NOT NULL ──
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = t AND column_name = 'create_user_id'
) THEN
EXECUTE format('ALTER TABLE %I ALTER COLUMN create_user_id TYPE BIGINT', t);
EXECUTE format('ALTER TABLE %I ALTER COLUMN create_user_id SET NOT NULL', t);
END IF;
-- ── create_time: TIMESTAMP NOT NULL DEFAULT NOW() ──
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = t AND column_name = 'create_time'
) THEN
EXECUTE format('ALTER TABLE %I ALTER COLUMN create_time TYPE TIMESTAMP', t);
EXECUTE format('ALTER TABLE %I ALTER COLUMN create_time SET NOT NULL', t);
EXECUTE format('ALTER TABLE %I ALTER COLUMN create_time SET DEFAULT NOW()', t);
END IF;
-- ── update_user: VARCHAR(40) NULL, no default ──
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = t AND column_name = 'update_user'
) THEN
EXECUTE format('ALTER TABLE %I ALTER COLUMN update_user TYPE VARCHAR(40)', t);
EXECUTE format('ALTER TABLE %I ALTER COLUMN update_user DROP NOT NULL', t);
EXECUTE format('ALTER TABLE %I ALTER COLUMN update_user DROP DEFAULT', t);
END IF;
-- ── update_user_id: BIGINT NULL, no default ──
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = t AND column_name = 'update_user_id'
) THEN
EXECUTE format('ALTER TABLE %I ALTER COLUMN update_user_id TYPE BIGINT', t);
EXECUTE format('ALTER TABLE %I ALTER COLUMN update_user_id DROP NOT NULL', t);
EXECUTE format('ALTER TABLE %I ALTER COLUMN update_user_id DROP DEFAULT', t);
END IF;
-- ── update_time: TIMESTAMP NULL, no default ──
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = t AND column_name = 'update_time'
) THEN
EXECUTE format('ALTER TABLE %I ALTER COLUMN update_time TYPE TIMESTAMP', t);
EXECUTE format('ALTER TABLE %I ALTER COLUMN update_time DROP NOT NULL', t);
EXECUTE format('ALTER TABLE %I ALTER COLUMN update_time DROP DEFAULT', t);
END IF;
-- ── is_deleted: SMALLINT NOT NULL DEFAULT 0 ──
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = t AND column_name = 'is_deleted'
) THEN
EXECUTE format('ALTER TABLE %I ALTER COLUMN is_deleted TYPE SMALLINT USING is_deleted::SMALLINT', t);
EXECUTE format('ALTER TABLE %I ALTER COLUMN is_deleted SET NOT NULL', t);
EXECUTE format('ALTER TABLE %I ALTER COLUMN is_deleted SET DEFAULT 0', t);
END IF;
END LOOP;
END $$;
@@ -0,0 +1,40 @@
-- V113: Create departments table.
CREATE TABLE IF NOT EXISTS departments (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL DEFAULT 1,
parent_id BIGINT NULL,
dept_code VARCHAR(50) NOT NULL,
dept_name VARCHAR(100) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
leader_user_id BIGINT NULL,
phone VARCHAR(20) NULL,
email VARCHAR(100) NULL,
status SMALLINT NOT NULL DEFAULT 1,
remark VARCHAR(200) NULL,
-- audit fields (aligned with V112 convention)
create_user VARCHAR(40) NOT NULL DEFAULT 'system',
create_user_id BIGINT NOT NULL DEFAULT 0,
create_time TIMESTAMP NOT NULL DEFAULT NOW(),
update_user VARCHAR(40) NULL,
update_user_id BIGINT NULL,
update_time TIMESTAMP NULL,
is_deleted SMALLINT NOT NULL DEFAULT 0
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_departments_tenant_id ON departments (tenant_id);
CREATE INDEX IF NOT EXISTS idx_departments_parent_id ON departments (parent_id);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_indexes
WHERE tablename = 'departments'
AND indexname = 'uk_departments_tenant_code') THEN
CREATE UNIQUE INDEX uk_departments_tenant_code
ON departments (tenant_id, dept_code)
WHERE is_deleted = 0;
END IF;
END
$$;
@@ -0,0 +1,61 @@
-- V114: Align audit fields on departments table (supplement to V112).
-- This is a no-op if V113 already created the table with correct types,
-- but ensures safety if the table existed before V113.
DO $$
BEGIN
-- create_user
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'departments' AND column_name = 'create_user') THEN
EXECUTE 'ALTER TABLE departments ALTER COLUMN create_user TYPE VARCHAR(40)';
EXECUTE 'ALTER TABLE departments ALTER COLUMN create_user SET NOT NULL';
END IF;
-- create_user_id
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'departments' AND column_name = 'create_user_id') THEN
EXECUTE 'ALTER TABLE departments ALTER COLUMN create_user_id TYPE BIGINT';
EXECUTE 'ALTER TABLE departments ALTER COLUMN create_user_id SET NOT NULL';
END IF;
-- create_time
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'departments' AND column_name = 'create_time') THEN
EXECUTE 'ALTER TABLE departments ALTER COLUMN create_time TYPE TIMESTAMP';
EXECUTE 'ALTER TABLE departments ALTER COLUMN create_time SET NOT NULL';
EXECUTE 'ALTER TABLE departments ALTER COLUMN create_time SET DEFAULT NOW()';
END IF;
-- update_user
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'departments' AND column_name = 'update_user') THEN
EXECUTE 'ALTER TABLE departments ALTER COLUMN update_user TYPE VARCHAR(40)';
EXECUTE 'ALTER TABLE departments ALTER COLUMN update_user DROP NOT NULL';
EXECUTE 'ALTER TABLE departments ALTER COLUMN update_user DROP DEFAULT';
END IF;
-- update_user_id
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'departments' AND column_name = 'update_user_id') THEN
EXECUTE 'ALTER TABLE departments ALTER COLUMN update_user_id TYPE BIGINT';
EXECUTE 'ALTER TABLE departments ALTER COLUMN update_user_id DROP NOT NULL';
EXECUTE 'ALTER TABLE departments ALTER COLUMN update_user_id DROP DEFAULT';
END IF;
-- update_time
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'departments' AND column_name = 'update_time') THEN
EXECUTE 'ALTER TABLE departments ALTER COLUMN update_time TYPE TIMESTAMP';
EXECUTE 'ALTER TABLE departments ALTER COLUMN update_time DROP NOT NULL';
EXECUTE 'ALTER TABLE departments ALTER COLUMN update_time DROP DEFAULT';
END IF;
-- is_deleted
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'departments' AND column_name = 'is_deleted') THEN
EXECUTE 'ALTER TABLE departments ALTER COLUMN is_deleted TYPE SMALLINT USING is_deleted::SMALLINT';
EXECUTE 'ALTER TABLE departments ALTER COLUMN is_deleted SET NOT NULL';
EXECUTE 'ALTER TABLE departments ALTER COLUMN is_deleted SET DEFAULT 0';
END IF;
END
$$;
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ljwx.platform.app.infrastructure.mybatis.mapper.NewRoleMapper">
<resultMap id="roleMap" type="com.ljwx.platform.app.domain.model.Role">
<id property="id" column="id"/>
<result property="tenantId" column="tenant_id"/>
<result property="code" column="code"/>
<result property="name" column="name"/>
<result property="description" column="description"/>
<result property="sortOrder" column="sort_order"/>
<result property="status" column="status"/>
<result property="isDeleted" column="is_deleted"/>
<result property="createUser" column="create_user"/>
<result property="createUserId" column="create_user_id"/>
<result property="createTime" column="create_time"/>
<result property="updateUser" column="update_user"/>
<result property="updateUserId" column="update_user_id"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<!-- List all active roles for a tenant, ordered by sort_order then id -->
<select id="selectAllByTenantId" resultMap="roleMap">
SELECT * FROM roles
WHERE tenant_id = #{tenantId} AND is_deleted = 0
ORDER BY sort_order ASC, id ASC
</select>
<select id="selectById" resultMap="roleMap">
SELECT * FROM roles
WHERE id = #{id} AND tenant_id = #{tenantId} AND is_deleted = 0
</select>
<select id="selectByCode" resultMap="roleMap">
SELECT * FROM roles
WHERE code = #{code} AND tenant_id = #{tenantId} AND is_deleted = 0
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO roles (tenant_id, code, name, description, sort_order, status, is_deleted,
create_user, create_user_id, create_time)
VALUES (#{tenantId}, #{code}, #{name}, #{description}, #{sortOrder}, #{status}, 0,
#{createUser}, #{createUserId}, #{createTime})
</insert>
<update id="update">
UPDATE roles
SET name = #{name},
description = #{description},
sort_order = #{sortOrder},
status = #{status},
update_user = #{updateUser},
update_user_id = #{updateUserId},
update_time = #{updateTime}
WHERE id = #{id} AND tenant_id = #{tenantId} AND is_deleted = 0
</update>
<update id="softDelete">
UPDATE roles
SET is_deleted = 1,
update_user = #{updateUser},
update_user_id = #{updateUserId},
update_time = NOW()
WHERE id = #{id} AND tenant_id = #{tenantId} AND is_deleted = 0
</update>
<!-- permission binding -->
<select id="selectPermissionIdsByRoleId" resultType="java.lang.Long">
SELECT permission_id FROM role_permissions WHERE role_id = #{roleId}
</select>
<delete id="deletePermissionsByRoleId">
DELETE FROM role_permissions WHERE role_id = #{roleId}
</delete>
<insert id="insertRolePermission">
INSERT INTO role_permissions (role_id, permission_id)
VALUES (#{roleId}, #{permissionId})
</insert>
<select id="countPermissionById" resultType="int">
SELECT COUNT(*) FROM permissions WHERE id = #{permissionId}
</select>
</mapper>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ljwx.platform.app.infrastructure.mybatis.mapper.NewUserRoleMapper">
<select id="selectRoleIdsByUserId" resultType="java.lang.Long">
SELECT role_id FROM user_roles
WHERE user_id = #{userId} AND tenant_id = #{tenantId} AND is_deleted = 0
</select>
<update id="softDeleteByUserId">
UPDATE user_roles
SET is_deleted = 1,
update_user = #{updateUser},
update_user_id = #{updateUserId},
update_time = NOW()
WHERE user_id = #{userId} AND tenant_id = #{tenantId} AND is_deleted = 0
</update>
<update id="softDeleteByUserIdAndRoleId">
UPDATE user_roles
SET is_deleted = 1,
update_user = #{updateUser},
update_user_id = #{updateUserId},
update_time = NOW()
WHERE user_id = #{userId} AND role_id = #{roleId}
AND tenant_id = #{tenantId} AND is_deleted = 0
</update>
<insert id="batchInsert">
INSERT INTO user_roles (tenant_id, user_id, role_id, is_deleted,
create_user, create_user_id, create_time)
VALUES
<foreach collection="list" item="ur" separator=",">
(#{ur.tenantId}, #{ur.userId}, #{ur.roleId}, 0,
#{ur.createUser}, #{ur.createUserId}, #{ur.createTime})
</foreach>
</insert>
<select id="countActiveBinding" resultType="int">
SELECT COUNT(*) FROM user_roles
WHERE user_id = #{userId} AND role_id = #{roleId}
AND tenant_id = #{tenantId} AND is_deleted = 0
</select>
<update id="softDeleteByRoleId">
UPDATE user_roles
SET is_deleted = 1,
update_user = #{updateUser},
update_user_id = #{updateUserId},
update_time = NOW()
WHERE role_id = #{roleId} AND tenant_id = #{tenantId} AND is_deleted = 0
</update>
</mapper>
@@ -0,0 +1,59 @@
package com.ljwx.platform.app.facade;
import com.ljwx.platform.app.domain.model.Role;
import com.ljwx.platform.app.facade.dto.role.*;
import com.ljwx.platform.app.service.RoleAppService;
import com.ljwx.platform.app.service.UserRoleAppService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class RoleFacadeTest {
@Mock private RoleAppService roleAppService;
@Mock private UserRoleAppService userRoleAppService;
@InjectMocks private RoleFacade facade;
@Test
void create_returnsResponse() {
Role created = new Role();
created.setId(1L); created.setCode("EDITOR"); created.setName("编辑");
when(roleAppService.create(any())).thenReturn(created);
CreateRoleRequest req = new CreateRoleRequest();
req.setCode("EDITOR"); req.setName("编辑");
RoleResponse resp = facade.create(req);
assertEquals(1L, resp.getId());
assertEquals("EDITOR", resp.getCode());
}
@Test
void delete_delegatesToService() {
facade.delete(1L);
verify(roleAppService).delete(1L);
}
@Test
void assignUserRoles_delegatesToService() {
AssignUserRolesRequest req = new AssignUserRolesRequest();
req.setUserId(100L); req.setRoleIds(List.of(1L, 2L));
facade.assignUserRoles(req);
verify(userRoleAppService).assignRoles(100L, List.of(1L, 2L));
}
@Test
void getUserRoleIds_returnsIds() {
when(userRoleAppService.getRoleIds(100L)).thenReturn(List.of(1L, 2L));
List<Long> ids = facade.getUserRoleIds(100L);
assertEquals(2, ids.size());
}
}
@@ -0,0 +1,140 @@
package com.ljwx.platform.app.integration;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@ActiveProfiles("test")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class AuditFieldAlignmentIT {
@Autowired
private JdbcTemplate jdbc;
private static final String[] TABLES = {
"users", "dict_types", "dict_data", "sys_configs",
"departments", "menus", "roles", "user_roles"
};
@Test @Order(1)
void createUser_isNotNull() {
for (String tbl : TABLES) {
String nullable = jdbc.queryForObject(
"SELECT is_nullable FROM information_schema.columns " +
"WHERE table_name = ? AND column_name = 'create_user'",
String.class, tbl);
assertEquals("NO", nullable, tbl + ".create_user should be NOT NULL");
}
}
@Test @Order(2)
void createUserId_isNotNull() {
for (String tbl : TABLES) {
String nullable = jdbc.queryForObject(
"SELECT is_nullable FROM information_schema.columns " +
"WHERE table_name = ? AND column_name = 'create_user_id'",
String.class, tbl);
assertEquals("NO", nullable, tbl + ".create_user_id should be NOT NULL");
}
}
@Test @Order(3)
void createTime_isNotNull() {
for (String tbl : TABLES) {
String nullable = jdbc.queryForObject(
"SELECT is_nullable FROM information_schema.columns " +
"WHERE table_name = ? AND column_name = 'create_time'",
String.class, tbl);
assertEquals("NO", nullable, tbl + ".create_time should be NOT NULL");
}
}
@Test @Order(4)
void updateUser_isNullable() {
for (String tbl : TABLES) {
String nullable = jdbc.queryForObject(
"SELECT is_nullable FROM information_schema.columns " +
"WHERE table_name = ? AND column_name = 'update_user'",
String.class, tbl);
assertEquals("YES", nullable, tbl + ".update_user should be nullable");
}
}
@Test @Order(5)
void updateUserId_isNullable() {
for (String tbl : TABLES) {
String nullable = jdbc.queryForObject(
"SELECT is_nullable FROM information_schema.columns " +
"WHERE table_name = ? AND column_name = 'update_user_id'",
String.class, tbl);
assertEquals("YES", nullable, tbl + ".update_user_id should be nullable");
}
}
@Test @Order(6)
void updateTime_isNullable() {
for (String tbl : TABLES) {
String nullable = jdbc.queryForObject(
"SELECT is_nullable FROM information_schema.columns " +
"WHERE table_name = ? AND column_name = 'update_time'",
String.class, tbl);
assertEquals("YES", nullable, tbl + ".update_time should be nullable");
}
}
@Test @Order(7)
void isDeleted_isSmallintNotNullDefaultZero() {
for (String tbl : TABLES) {
// type
String dataType = jdbc.queryForObject(
"SELECT data_type FROM information_schema.columns " +
"WHERE table_name = ? AND column_name = 'is_deleted'",
String.class, tbl);
assertEquals("smallint", dataType, tbl + ".is_deleted should be smallint");
// not null
String nullable = jdbc.queryForObject(
"SELECT is_nullable FROM information_schema.columns " +
"WHERE table_name = ? AND column_name = 'is_deleted'",
String.class, tbl);
assertEquals("NO", nullable, tbl + ".is_deleted should be NOT NULL");
// default
String def = jdbc.queryForObject(
"SELECT column_default FROM information_schema.columns " +
"WHERE table_name = ? AND column_name = 'is_deleted'",
String.class, tbl);
assertNotNull(def, tbl + ".is_deleted should have a default");
assertTrue(def.contains("0"), tbl + ".is_deleted default should contain 0");
}
}
@Test @Order(8)
void createUser_type_isVarchar40() {
for (String tbl : TABLES) {
Integer maxLen = jdbc.queryForObject(
"SELECT character_maximum_length FROM information_schema.columns " +
"WHERE table_name = ? AND column_name = 'create_user'",
Integer.class, tbl);
assertNotNull(maxLen, tbl + ".create_user should be varchar");
assertEquals(40, maxLen, tbl + ".create_user should be VARCHAR(40)");
}
}
@Test @Order(9)
void updateUser_type_isVarchar40() {
for (String tbl : TABLES) {
Integer maxLen = jdbc.queryForObject(
"SELECT character_maximum_length FROM information_schema.columns " +
"WHERE table_name = ? AND column_name = 'update_user'",
Integer.class, tbl);
assertNotNull(maxLen, tbl + ".update_user should be varchar");
assertEquals(40, maxLen, tbl + ".update_user should be VARCHAR(40)");
}
}
}
@@ -0,0 +1,208 @@
package com.ljwx.platform.app.integration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ljwx.platform.app.facade.dto.role.*;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class RoleControllerIT {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
private static Long createdRoleId;
// ── authentication ──
@Test @Order(1)
void unauthenticated_returns401() throws Exception {
mockMvc.perform(get("/api/v1/roles"))
.andExpect(status().isUnauthorized());
}
// ── list ──
@Test @Order(2)
@WithMockUser(username = "admin", authorities = {"role:read"})
void list_returnsSeededRoles() throws Exception {
mockMvc.perform(get("/api/v1/roles"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data", hasSize(greaterThanOrEqualTo(1))));
}
// ── create ──
@Test @Order(3)
@WithMockUser(username = "admin", authorities = {"role:write"})
void create_success() throws Exception {
CreateRoleRequest req = new CreateRoleRequest();
req.setCode("IT_TEST_ROLE");
req.setName("集成测试角色");
String body = objectMapper.writeValueAsString(req);
String response = mockMvc.perform(post("/api/v1/roles")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.code").value("IT_TEST_ROLE"))
.andReturn().getResponse().getContentAsString();
// extract created ID for later tests
createdRoleId = objectMapper.readTree(response).path("data").path("id").asLong();
}
@Test @Order(4)
@WithMockUser(username = "admin", authorities = {"role:write"})
void create_duplicateCode_returns400() throws Exception {
CreateRoleRequest req = new CreateRoleRequest();
req.setCode("IT_TEST_ROLE");
req.setName("重复角色");
String body = objectMapper.writeValueAsString(req);
mockMvc.perform(post("/api/v1/roles")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(status().isBadRequest());
}
@Test @Order(5)
@WithMockUser(username = "admin", authorities = {"role:write"})
void create_invalidRequest_returns400() throws Exception {
CreateRoleRequest req = new CreateRoleRequest();
// missing code and name
String body = objectMapper.writeValueAsString(req);
mockMvc.perform(post("/api/v1/roles")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(status().isBadRequest());
}
// ── get ──
@Test @Order(6)
@WithMockUser(username = "admin", authorities = {"role:read"})
void get_success() throws Exception {
mockMvc.perform(get("/api/v1/roles/" + createdRoleId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.id").value(createdRoleId));
}
@Test @Order(7)
@WithMockUser(username = "admin", authorities = {"role:read"})
void get_notFound_returns404() throws Exception {
mockMvc.perform(get("/api/v1/roles/999999"))
.andExpect(status().isNotFound());
}
// ── update ──
@Test @Order(8)
@WithMockUser(username = "admin", authorities = {"role:write"})
void update_success() throws Exception {
UpdateRoleRequest req = new UpdateRoleRequest();
req.setName("更新后的名称");
req.setSortOrder(10);
String body = objectMapper.writeValueAsString(req);
mockMvc.perform(put("/api/v1/roles/" + createdRoleId)
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.name").value("更新后的名称"))
.andExpect(jsonPath("$.data.code").value("IT_TEST_ROLE")); // immutable
}
// ── assign permissions ──
@Test @Order(9)
@WithMockUser(username = "admin", authorities = {"role:write"})
void assignPermissions_success() throws Exception {
// first, get valid permission IDs from the DB via the role list
AssignPermissionsRequest req = new AssignPermissionsRequest();
req.setPermissionIds(List.of()); // empty is valid (clears all)
String body = objectMapper.writeValueAsString(req);
mockMvc.perform(put("/api/v1/roles/" + createdRoleId + "/permissions")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(status().isOk());
}
// ── user-role assignment ──
@Test @Order(10)
@WithMockUser(username = "admin", authorities = {"role:write"})
void assignUserRoles_success() throws Exception {
AssignUserRolesRequest req = new AssignUserRolesRequest();
req.setUserId(1L);
req.setRoleIds(List.of(createdRoleId));
String body = objectMapper.writeValueAsString(req);
mockMvc.perform(put("/api/v1/roles/user-roles")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(status().isOk());
}
@Test @Order(11)
@WithMockUser(username = "admin", authorities = {"role:read"})
void getUserRoleIds_success() throws Exception {
mockMvc.perform(get("/api/v1/roles/user-roles/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data", hasSize(greaterThanOrEqualTo(1))));
}
// ── delete ──
@Test @Order(12)
@WithMockUser(username = "admin", authorities = {"role:write"})
void delete_success() throws Exception {
mockMvc.perform(delete("/api/v1/roles/" + createdRoleId))
.andExpect(status().isOk());
}
@Test @Order(13)
@WithMockUser(username = "admin", authorities = {"role:read"})
void get_afterDelete_returns404() throws Exception {
mockMvc.perform(get("/api/v1/roles/" + createdRoleId))
.andExpect(status().isNotFound());
}
@Test @Order(14)
@WithMockUser(username = "admin", authorities = {"role:write"})
void delete_adminRole_returns400() throws Exception {
// find ADMIN role id
String resp = mockMvc.perform(get("/api/v1/roles"))
.andReturn().getResponse().getContentAsString();
// attempt delete on ADMIN
Long adminId = objectMapper.readTree(resp).path("data").get(0).path("id").asLong();
mockMvc.perform(delete("/api/v1/roles/" + adminId))
.andExpect(status().isBadRequest());
}
// ── menus/me tree ──
@Test @Order(15)
@WithMockUser(username = "admin", authorities = {"menu:read", "dept:read", "dict:read"})
void menusMe_returnsTree() throws Exception {
mockMvc.perform(get("/api/v1/menus/me"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").isArray());
}
@Test @Order(16)
void menusMe_unauthenticated_returns401() throws Exception {
mockMvc.perform(get("/api/v1/menus/me"))
.andExpect(status().isUnauthorized());
}
}
@@ -0,0 +1,173 @@
package com.ljwx.platform.app.integration;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@ActiveProfiles("test")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserRoleMigrationIT {
@Autowired
private JdbcTemplate jdbc;
// ── V110: schema checks ──
@Test @Order(1)
void v110_rolesTable_hasTenantId() {
Integer cnt = jdbc.queryForObject(
"SELECT COUNT(*) FROM information_schema.columns " +
"WHERE table_name = 'roles' AND column_name = 'tenant_id'", Integer.class);
assertEquals(1, cnt);
}
@Test @Order(2)
void v110_userRolesTable_exists() {
Integer cnt = jdbc.queryForObject(
"SELECT COUNT(*) FROM information_schema.tables " +
"WHERE table_name = 'user_roles'", Integer.class);
assertEquals(1, cnt);
}
@Test @Order(3)
void v110_userRoles_hasAuditColumns() {
for (String col : new String[]{"create_user", "create_user_id", "create_time",
"update_user", "update_user_id", "update_time", "is_deleted"}) {
Integer cnt = jdbc.queryForObject(
"SELECT COUNT(*) FROM information_schema.columns " +
"WHERE table_name = 'user_roles' AND column_name = ?",
Integer.class, col);
assertEquals(1, cnt, "Missing column: " + col);
}
}
@Test @Order(4)
void v110_userRoles_partialUniqueIndex() {
Integer cnt = jdbc.queryForObject(
"SELECT COUNT(*) FROM pg_indexes " +
"WHERE tablename = 'user_roles' AND indexname = 'uk_user_roles_tenant_user_role'",
Integer.class);
assertEquals(1, cnt);
}
@Test @Order(5)
void v110_users_hasNewColumns() {
for (String col : new String[]{"nickname", "phone", "email", "avatar", "status", "remark"}) {
Integer cnt = jdbc.queryForObject(
"SELECT COUNT(*) FROM information_schema.columns " +
"WHERE table_name = 'users' AND column_name = ?",
Integer.class, col);
assertEquals(1, cnt, "Missing column on users: " + col);
}
}
// ── V111: seed checks ──
@Test @Order(6)
void v111_ninePermissionsExist() {
Integer cnt = jdbc.queryForObject(
"SELECT COUNT(*) FROM permissions WHERE key IN " +
"('user:read','user:write','role:read','role:write'," +
"'dept:read','dict:read','config:read','menu:read','menu:write')",
Integer.class);
assertEquals(9, cnt);
}
@Test @Order(7)
void v111_allAdminRolesHavePermissions() {
// every ADMIN role should have exactly 9 permission bindings
Integer adminCount = jdbc.queryForObject(
"SELECT COUNT(*) FROM roles WHERE code = 'ADMIN' AND is_deleted = 0",
Integer.class);
if (adminCount > 0) {
Integer minBindings = jdbc.queryForObject(
"SELECT MIN(cnt) FROM (" +
" SELECT r.id, COUNT(rp.permission_id) AS cnt " +
" FROM roles r " +
" LEFT JOIN role_permissions rp ON rp.role_id = r.id " +
" WHERE r.code = 'ADMIN' AND r.is_deleted = 0 " +
" GROUP BY r.id" +
") sub", Integer.class);
assertEquals(9, minBindings, "Not all ADMIN roles have 9 permissions");
}
}
@Test @Order(8)
void v111_allSecondLevelMenusHavePermissionKey() {
// 6 second-level menus under SYSTEM should all have non-null permission_key
Integer cnt = jdbc.queryForObject(
"SELECT COUNT(*) FROM menus " +
"WHERE parent_id = (SELECT id FROM menus WHERE code = 'SYSTEM') " +
"AND permission_key IS NOT NULL",
Integer.class);
assertEquals(6, cnt);
}
@Test @Order(9)
void v111_permissionKeyValues() {
// verify specific permission keys
String[][] expected = {
{"MENU_USER", "user:read"},
{"MENU_ROLE", "role:read"},
{"MENU_DEPT", "dept:read"},
{"MENU_DICT", "dict:read"},
{"MENU_CONFIG", "config:read"},
{"MENU_MENU", "menu:read"}
};
for (String[] pair : expected) {
String actual = jdbc.queryForObject(
"SELECT permission_key FROM menus WHERE code = ?",
String.class, pair[0]);
assertEquals(pair[1], actual, "Wrong permission_key for " + pair[0]);
}
}
// ── audit column nullability ──
@Test @Order(10)
void v110_roles_updateColumnsNullable() {
for (String col : new String[]{"update_user", "update_user_id", "update_time"}) {
String nullable = jdbc.queryForObject(
"SELECT is_nullable FROM information_schema.columns " +
"WHERE table_name = 'roles' AND column_name = ?",
String.class, col);
assertEquals("YES", nullable, col + " should be nullable");
}
}
@Test @Order(11)
void v110_userRoles_updateColumnsNullable() {
for (String col : new String[]{"update_user", "update_user_id", "update_time"}) {
String nullable = jdbc.queryForObject(
"SELECT is_nullable FROM information_schema.columns " +
"WHERE table_name = 'user_roles' AND column_name = ?",
String.class, col);
assertEquals("YES", nullable, col + " should be nullable");
}
}
@Test @Order(12)
void v110_userRoles_isDeleted_defaultZero() {
String def = jdbc.queryForObject(
"SELECT column_default FROM information_schema.columns " +
"WHERE table_name = 'user_roles' AND column_name = 'is_deleted'",
String.class);
assertNotNull(def);
assertTrue(def.contains("0"), "is_deleted default should contain 0");
}
@Test @Order(13)
void v110_roles_isDeleted_defaultZero() {
String def = jdbc.queryForObject(
"SELECT column_default FROM information_schema.columns " +
"WHERE table_name = 'roles' AND column_name = 'is_deleted'",
String.class);
assertNotNull(def);
assertTrue(def.contains("0"), "is_deleted default should contain 0");
}
}
@@ -0,0 +1,246 @@
package com.ljwx.platform.app.service;
import com.ljwx.platform.app.domain.model.Role;
import com.ljwx.platform.app.domain.repository.RoleRepository;
import com.ljwx.platform.app.domain.repository.UserRoleRepository;
import com.ljwx.platform.core.exception.BusinessException;
import com.ljwx.platform.web.security.SecurityUtil;
import com.ljwx.platform.web.security.UserContext;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.time.LocalDateTime;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class RoleAppServiceTest {
@Mock private RoleRepository roleRepository;
@Mock private UserRoleRepository userRoleRepository;
@InjectMocks private RoleAppService service;
@BeforeEach
void setUpSecurity() {
UserContext ctx = new UserContext(1L, 1L, "admin");
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(ctx, null, List.of());
SecurityContextHolder.getContext().setAuthentication(auth);
}
@AfterEach
void clearSecurity() {
SecurityContextHolder.clearContext();
}
// ── list ──
@Test
void list_returnsTenantRoles() {
Role r = new Role(); r.setId(1L); r.setCode("ADMIN");
when(roleRepository.findAllByTenantId(1L)).thenReturn(List.of(r));
List<Role> result = service.list();
assertEquals(1, result.size());
verify(roleRepository).findAllByTenantId(1L);
}
// ── getById ──
@Test
void getById_found() {
Role r = new Role(); r.setId(1L);
when(roleRepository.findById(1L, 1L)).thenReturn(Optional.of(r));
assertNotNull(service.getById(1L));
}
@Test
void getById_notFound_throwsException() {
when(roleRepository.findById(99L, 1L)).thenReturn(Optional.empty());
assertThrows(BusinessException.class, () -> service.getById(99L));
}
// ── create ──
@Test
void create_success() {
Role input = new Role();
input.setCode("EDITOR");
input.setName("编辑者");
when(roleRepository.findByCode("EDITOR", 1L)).thenReturn(Optional.empty());
doAnswer(inv -> { inv.getArgument(0, Role.class).setId(10L); return null; })
.when(roleRepository).insert(any(Role.class));
Role created = service.create(input);
assertEquals(10L, created.getId());
assertEquals(1L, created.getTenantId());
assertEquals("admin", created.getCreateUser());
assertEquals(1, created.getStatus());
verify(roleRepository).insert(any());
}
@Test
void create_duplicateCode_throwsException() {
Role input = new Role(); input.setCode("ADMIN");
when(roleRepository.findByCode("ADMIN", 1L)).thenReturn(Optional.of(new Role()));
assertThrows(BusinessException.class, () -> service.create(input));
}
// ── update ──
@Test
void update_success() {
Role existing = new Role();
existing.setId(1L); existing.setTenantId(1L);
existing.setCode("ADMIN"); existing.setName("Old");
existing.setSortOrder(0); existing.setStatus(1);
when(roleRepository.findById(1L, 1L)).thenReturn(Optional.of(existing));
Role updates = new Role();
updates.setName("New Name");
updates.setDescription("desc");
updates.setSortOrder(5);
updates.setStatus(0);
Role result = service.update(1L, updates);
assertEquals("New Name", result.getName());
assertEquals("desc", result.getDescription());
assertEquals(5, result.getSortOrder());
assertEquals("ADMIN", result.getCode()); // immutable
verify(roleRepository).update(any());
}
@Test
void update_notFound_throwsException() {
when(roleRepository.findById(99L, 1L)).thenReturn(Optional.empty());
assertThrows(BusinessException.class, () -> service.update(99L, new Role()));
}
// ── delete ──
@Test
void delete_success() {
Role r = new Role(); r.setId(2L); r.setCode("EDITOR"); r.setTenantId(1L);
when(roleRepository.findById(2L, 1L)).thenReturn(Optional.of(r));
service.delete(2L);
verify(roleRepository).deletePermissionsByRoleId(2L);
verify(userRoleRepository).softDeleteByRoleId(eq(2L), eq(1L), anyString(), anyLong());
verify(roleRepository).softDelete(eq(2L), eq(1L), anyString(), anyLong());
}
@Test
void delete_adminRole_throwsException() {
Role r = new Role(); r.setId(1L); r.setCode("ADMIN");
when(roleRepository.findById(1L, 1L)).thenReturn(Optional.of(r));
assertThrows(BusinessException.class, () -> service.delete(1L));
}
@Test
void delete_notFound_throwsException() {
when(roleRepository.findById(99L, 1L)).thenReturn(Optional.empty());
assertThrows(BusinessException.class, () -> service.delete(99L));
}
// ── assignPermissions ──
@Test
void assignPermissions_success() {
Role r = new Role(); r.setId(1L);
when(roleRepository.findById(1L, 1L)).thenReturn(Optional.of(r));
when(roleRepository.existsPermission(anyLong())).thenReturn(true);
service.assignPermissions(1L, List.of(10L, 20L, 30L));
verify(roleRepository).deletePermissionsByRoleId(1L);
verify(roleRepository, times(3)).insertRolePermission(eq(1L), anyLong());
}
@Test
void assignPermissions_deduplicates() {
Role r = new Role(); r.setId(1L);
when(roleRepository.findById(1L, 1L)).thenReturn(Optional.of(r));
when(roleRepository.existsPermission(anyLong())).thenReturn(true);
service.assignPermissions(1L, List.of(10L, 10L, 20L, 20L));
verify(roleRepository, times(2)).insertRolePermission(eq(1L), anyLong());
}
@Test
void assignPermissions_invalidPermission_throwsException() {
Role r = new Role(); r.setId(1L);
when(roleRepository.findById(1L, 1L)).thenReturn(Optional.of(r));
when(roleRepository.existsPermission(999L)).thenReturn(false);
assertThrows(BusinessException.class,
() -> service.assignPermissions(1L, List.of(999L)));
}
@Test
void assignPermissions_roleNotFound_throwsException() {
when(roleRepository.findById(99L, 1L)).thenReturn(Optional.empty());
assertThrows(BusinessException.class,
() -> service.assignPermissions(99L, List.of(1L)));
}
// ── create defaults ──
@Test
void create_defaultSortOrder() {
Role input = new Role(); input.setCode("NEW"); input.setName("New");
when(roleRepository.findByCode("NEW", 1L)).thenReturn(Optional.empty());
doAnswer(inv -> { inv.getArgument(0, Role.class).setId(1L); return null; })
.when(roleRepository).insert(any());
Role result = service.create(input);
assertEquals(0, result.getSortOrder());
}
@Test
void create_defaultStatus() {
Role input = new Role(); input.setCode("NEW"); input.setName("New");
when(roleRepository.findByCode("NEW", 1L)).thenReturn(Optional.empty());
doAnswer(inv -> { inv.getArgument(0, Role.class).setId(1L); return null; })
.when(roleRepository).insert(any());
Role result = service.create(input);
assertEquals(1, result.getStatus());
}
// ── update immutability ──
@Test
void update_codeRemainsImmutable() {
Role existing = new Role();
existing.setId(1L); existing.setTenantId(1L);
existing.setCode("ORIGINAL"); existing.setName("old");
existing.setSortOrder(0); existing.setStatus(1);
when(roleRepository.findById(1L, 1L)).thenReturn(Optional.of(existing));
Role updates = new Role();
updates.setName("Updated");
updates.setCode("HACKED"); // should be ignored
Role result = service.update(1L, updates);
assertEquals("ORIGINAL", result.getCode());
}
@Test
void update_auditFieldsPopulated() {
Role existing = new Role();
existing.setId(1L); existing.setTenantId(1L);
existing.setCode("R1"); existing.setName("old");
existing.setSortOrder(0); existing.setStatus(1);
when(roleRepository.findById(1L, 1L)).thenReturn(Optional.of(existing));
Role updates = new Role(); updates.setName("new");
Role result = service.update(1L, updates);
assertEquals("admin", result.getUpdateUser());
assertNotNull(result.getUpdateTime());
}
}
@@ -0,0 +1,117 @@
package com.ljwx.platform.app.service;
import com.ljwx.platform.app.domain.model.UserRole;
import com.ljwx.platform.app.domain.repository.UserRoleRepository;
import com.ljwx.platform.web.security.UserContext;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserRoleAppServiceTest {
@Mock private UserRoleRepository userRoleRepository;
@InjectMocks private UserRoleAppService service;
@BeforeEach
void setUpSecurity() {
UserContext ctx = new UserContext(1L, 1L, "admin");
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(ctx, null, List.of());
SecurityContextHolder.getContext().setAuthentication(auth);
}
@AfterEach
void clearSecurity() {
SecurityContextHolder.clearContext();
}
// ── getRoleIds ──
@Test
void getRoleIds_returnsList() {
when(userRoleRepository.findRoleIdsByUserId(100L, 1L)).thenReturn(List.of(1L, 2L));
List<Long> ids = service.getRoleIds(100L);
assertEquals(2, ids.size());
}
// ── assignRoles ──
@Test
void assignRoles_softDeletesThenInserts() {
service.assignRoles(100L, List.of(1L, 2L));
verify(userRoleRepository).softDeleteByUserId(eq(100L), eq(1L), anyString(), anyLong());
verify(userRoleRepository).batchInsert(argThat(list -> list.size() == 2));
}
@Test
void assignRoles_emptyList_softDeletesOnly() {
service.assignRoles(100L, List.of());
verify(userRoleRepository).softDeleteByUserId(eq(100L), eq(1L), anyString(), anyLong());
verify(userRoleRepository, never()).batchInsert(any());
}
@Test
void assignRoles_dedupInput_insertsOnce() {
service.assignRoles(100L, List.of(1L, 1L, 2L, 2L, 3L));
verify(userRoleRepository).batchInsert(argThat(list -> list.size() == 3));
}
// ── addRoles ──
@Test
void addRoles_skipsExisting() {
when(userRoleRepository.existsActiveBinding(100L, 1L, 1L)).thenReturn(true);
when(userRoleRepository.existsActiveBinding(100L, 2L, 1L)).thenReturn(false);
service.addRoles(100L, List.of(1L, 2L));
verify(userRoleRepository).batchInsert(argThat(list -> list.size() == 1));
}
@Test
void addRoles_allExisting_noInsert() {
when(userRoleRepository.existsActiveBinding(anyLong(), anyLong(), anyLong())).thenReturn(true);
service.addRoles(100L, List.of(1L, 2L));
verify(userRoleRepository, never()).batchInsert(any());
}
@Test
void addRoles_dedupInput_insertsOnce() {
when(userRoleRepository.existsActiveBinding(100L, 5L, 1L)).thenReturn(false);
service.addRoles(100L, List.of(5L, 5L, 5L));
verify(userRoleRepository).batchInsert(argThat(list -> list.size() == 1));
}
// ── removeRoles ──
@Test
void removeRoles_softDeletesEach() {
service.removeRoles(100L, List.of(1L, 2L));
verify(userRoleRepository, times(2))
.softDeleteByUserIdAndRoleId(eq(100L), anyLong(), eq(1L), anyString(), anyLong());
}
// ── null/empty handling ──
@Test
void assignRoles_nullList_noError() {
service.assignRoles(100L, null);
verify(userRoleRepository).softDeleteByUserId(eq(100L), eq(1L), anyString(), anyLong());
verify(userRoleRepository, never()).batchInsert(any());
}
@Test
void addRoles_emptyList_noInsert() {
service.addRoles(100L, List.of());
verify(userRoleRepository, never()).batchInsert(any());
}
}
@@ -1,20 +1,5 @@
package com.ljwx.platform.core.exception;
/**
* Centralized error code enumeration.
* <p>
* All error codes used in the platform MUST be defined here.
* Business modules should extend with BIZ_{MODULE}_{DESCRIPTION} pattern.
* </p>
* <p>
* Mapping to HTTP status:
* TOKEN_INVALID, TOKEN_EXPIRED → 401
* PERMISSION_DENIED, TENANT_ACCESS_DENIED → 403
* VALIDATION_FAILED → 400
* RESOURCE_NOT_FOUND → 404
* SYSTEM_ERROR → 500
* </p>
*/
public enum ErrorCode {
// --- 401 Unauthorized ---
@@ -28,12 +13,18 @@ public enum ErrorCode {
// --- 400 Bad Request ---
VALIDATION_FAILED("VALIDATION_FAILED", "Validation failed", 400),
BAD_REQUEST("400", "请求参数错误", 400),
// --- 404 Not Found ---
RESOURCE_NOT_FOUND("RESOURCE_NOT_FOUND", "Resource not found", 404),
NOT_FOUND("404", "资源不存在", 404),
// --- 500 Internal Server Error ---
SYSTEM_ERROR("SYSTEM_ERROR", "Internal server error", 500);
SYSTEM_ERROR("SYSTEM_ERROR", "Internal server error", 500),
INTERNAL_ERROR("500", "系统异常", 500),
// --- alias for 403 ---
FORBIDDEN("403", "权限不足", 403);
private final String code;
private final String message;
@@ -54,6 +54,14 @@ public class Result<T> {
return new Result<>(code, message, null);
}
public static <T> Result<T> fail(String code, String message) {
return error(code, message);
}
public static <T> Result<T> fail(int code, String message) {
return error(String.valueOf(code), message);
}
public String getCode() {
return code;
}
@@ -85,4 +93,4 @@ public class Result<T> {
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}
}
@@ -0,0 +1,88 @@
package com.ljwx.platform.web.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Centralized utility for extracting current user info from the security context.
* <p>
* Priority: UserContext fields → auth.getName() → fallback defaults.
*/
public final class SecurityUtil {
private SecurityUtil() {}
/**
* Returns the current {@link UserContext}, or {@code null} if not available.
*/
public static UserContext currentUserContext() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof UserContext ctx) {
return ctx;
}
return null;
}
public static boolean isAuthenticated() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null && auth.isAuthenticated()
&& !"anonymousUser".equals(auth.getPrincipal());
}
/**
* Current username. Falls back to auth.getName(), then "system".
*/
public static String currentUsername() {
UserContext ctx = currentUserContext();
if (ctx != null && ctx.getUsername() != null && !ctx.getUsername().isBlank()) {
return ctx.getUsername();
}
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getName() != null && !auth.getName().isBlank()
&& !"anonymousUser".equals(auth.getName())) {
return auth.getName();
}
return "system";
}
/**
* Current user ID. Falls back to 0.
*/
public static Long currentUserId() {
UserContext ctx = currentUserContext();
if (ctx != null && ctx.getUserId() != null) {
return ctx.getUserId();
}
return 0L;
}
/**
* Current tenant ID. Falls back to 1 (default tenant).
*/
public static Long currentTenantId() {
UserContext ctx = currentUserContext();
if (ctx != null && ctx.getTenantId() != null) {
return ctx.getTenantId();
}
return 1L;
}
/**
* Returns the set of authority strings for the current user.
*/
public static Set<String> currentAuthorities() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) return Collections.emptySet();
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
if (authorities == null) return Collections.emptySet();
return authorities.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
}
}
@@ -2,53 +2,34 @@ package com.ljwx.platform.web.security;
import java.io.Serializable;
/**
* 认证用户上下文,由 JWT Filter 解析后设置到 Authentication#principal。
* 所有 AppService 通过此类获取当前用户信息。
*/
public class UserContext implements Serializable {
private static final long serialVersionUID = 1L;
private Long userId;
private String username;
private Long tenantId;
private String username;
public UserContext() {
}
public UserContext() {}
public UserContext(Long userId, String username, Long tenantId) {
public UserContext(Long userId, Long tenantId, String username) {
this.userId = userId;
this.username = username;
this.tenantId = tenantId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Long getTenantId() {
return tenantId;
}
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public void setTenantId(Long tenantId) {
this.tenantId = tenantId;
}
public Long getTenantId() { return tenantId; }
public void setTenantId(Long tenantId) { this.tenantId = tenantId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
@Override
public String toString() {
return username != null ? username : "UserContext[" + userId + "]";
return "UserContext{userId=" + userId + ", tenantId=" + tenantId +
", username='" + username + "'}";
}
}
@@ -0,0 +1,140 @@
package com.ljwx.platform.web.security;
import org.junit.jupiter.api.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
class SecurityUtilTest {
@AfterEach
void clearContext() {
SecurityContextHolder.clearContext();
}
// ── currentUsername ──
@Test
void currentUsername_fromUserContext() {
setUpAuth(new UserContext(1L, 1L, "alice"));
assertEquals("alice", SecurityUtil.currentUsername());
}
@Test
void currentUsername_fallsBackToAuthName() {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("bob", null, List.of()));
assertEquals("bob", SecurityUtil.currentUsername());
}
@Test
void currentUsername_fallsBackToSystem() {
SecurityContextHolder.clearContext();
assertEquals("system", SecurityUtil.currentUsername());
}
@Test
void currentUsername_blankUsername_fallsBackToAuthName() {
setUpAuth(new UserContext(1L, 1L, " "));
// auth.getName() returns UserContext.toString(), not blank
// so it will fall back to auth.getName()
assertNotNull(SecurityUtil.currentUsername());
}
// ── currentUserId ──
@Test
void currentUserId_fromUserContext() {
setUpAuth(new UserContext(42L, 1L, "admin"));
assertEquals(42L, SecurityUtil.currentUserId());
}
@Test
void currentUserId_fallsBackToZero() {
SecurityContextHolder.clearContext();
assertEquals(0L, SecurityUtil.currentUserId());
}
// ── currentTenantId ──
@Test
void currentTenantId_fromUserContext() {
setUpAuth(new UserContext(1L, 5L, "admin"));
assertEquals(5L, SecurityUtil.currentTenantId());
}
@Test
void currentTenantId_fallsBackToOne() {
SecurityContextHolder.clearContext();
assertEquals(1L, SecurityUtil.currentTenantId());
}
// ── isAuthenticated ──
@Test
void isAuthenticated_true() {
setUpAuth(new UserContext(1L, 1L, "admin"));
assertTrue(SecurityUtil.isAuthenticated());
}
@Test
void isAuthenticated_false_noAuth() {
SecurityContextHolder.clearContext();
assertFalse(SecurityUtil.isAuthenticated());
}
@Test
void isAuthenticated_false_anonymous() {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("anonymousUser", null, List.of()));
assertFalse(SecurityUtil.isAuthenticated());
}
// ── currentAuthorities ──
@Test
void currentAuthorities_returnSet() {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
new UserContext(1L, 1L, "admin"), null,
List.of(new SimpleGrantedAuthority("role:read"),
new SimpleGrantedAuthority("role:write"))));
Set<String> auths = SecurityUtil.currentAuthorities();
assertEquals(2, auths.size());
assertTrue(auths.contains("role:read"));
assertTrue(auths.contains("role:write"));
}
@Test
void currentAuthorities_empty_noAuth() {
SecurityContextHolder.clearContext();
assertTrue(SecurityUtil.currentAuthorities().isEmpty());
}
// ── currentUserContext ──
@Test
void currentUserContext_returnsInstance() {
UserContext ctx = new UserContext(1L, 1L, "admin");
setUpAuth(ctx);
assertSame(ctx, SecurityUtil.currentUserContext());
}
@Test
void currentUserContext_null_noAuth() {
SecurityContextHolder.clearContext();
assertNull(SecurityUtil.currentUserContext());
}
// ── helpers ──
private void setUpAuth(UserContext ctx) {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(ctx, null, List.of()));
}
}