feat: 初始化短剧服务端(管理后台 API + 客户端 API)

Spring Boot 3.2 / MyBatis-Plus / PostgreSQL,7 个 Maven 模块;含认证鉴权、内容与媒资管理、操作日志,以及客户端 /app/** 接口。
This commit is contained in:
Vicky committed 2026-10-14 18:02:47 +08:00
commit a4aa749cc6
152 files changed
+8027

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.reelshort</groupId>
<artifactId>reelshort</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>reelshort-module-system</artifactId>
<name>reelshort-module-system</name>
<description>系统域:管理员、角色、权限、菜单、配置</description>
<dependencies>
<dependency>
<groupId>com.reelshort</groupId>
<artifactId>reelshort-framework</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,36 @@
package com.reelshort.system.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("sys_admin_user")
public class AdminUser {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String realName;
private String mobile;
private String email;
private String avatar;
private Integer status;
private String ipWhitelist;
private Integer twoFactor;
private LocalDateTime lastLoginAt;
private String lastLoginIp;
private LocalDateTime createdAt;
/** updated_at 交给数据库的 ON UPDATE CURRENT_TIMESTAMP 维护 */
@TableField(updateStrategy = FieldStrategy.NEVER)
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}
@@ -0,0 +1,25 @@
package com.reelshort.system.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_admin_user_role")
public class AdminUserRole {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long roleId;
public AdminUserRole(Long userId, Long roleId) {
this.userId = userId;
this.roleId = roleId;
}
}
@@ -0,0 +1,36 @@
package com.reelshort.system.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("sys_menu")
public class Menu {
@TableId(type = IdType.AUTO)
private Long id;
private Long parentId;
private String title;
private String routePath;
private String routeName;
private String component;
private String redirect;
private String icon;
private String permissionCode;
private String type;
private Integer alwaysShow;
private Integer affix;
private Integer sort;
private Integer visible;
private Integer status;
private LocalDateTime createdAt;
/** updated_at 交给数据库的 ON UPDATE CURRENT_TIMESTAMP 维护 */
@TableField(updateStrategy = FieldStrategy.NEVER)
private LocalDateTime updatedAt;
}
@@ -0,0 +1,31 @@
package com.reelshort.system.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("sys_operation_log")
public class OperationLog {
@TableId(type = IdType.AUTO)
private Long id;
private Long adminUserId;
private String username;
private String module;
private String action;
private String requestMethod;
private String requestUrl;
private String requestParams;
private String requestBody;
private String responseBody;
/** SUCCESS / FAIL */
private String status;
private String errorMsg;
private Integer durationMs;
private String ip;
private LocalDateTime createdAt;
}
@@ -0,0 +1,19 @@
package com.reelshort.system.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("sys_permission")
public class Permission {
@TableId(type = IdType.AUTO)
private Long id;
private String code;
private String name;
private String groupName;
private String type;
private Integer sort;
}
@@ -0,0 +1,32 @@
package com.reelshort.system.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("sys_role")
public class Role {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String code;
private Long categoryId;
private Integer level;
private String description;
private Integer status;
private Integer isBuiltin;
private LocalDateTime createdAt;
/** updated_at 交给数据库的 ON UPDATE CURRENT_TIMESTAMP 维护 */
@TableField(updateStrategy = FieldStrategy.NEVER)
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}
@@ -0,0 +1,28 @@
package com.reelshort.system.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("sys_role_category")
public class RoleCategory {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String description;
private Integer sort;
private LocalDateTime createdAt;
/** updated_at 交给数据库的 ON UPDATE CURRENT_TIMESTAMP 维护 */
@TableField(updateStrategy = FieldStrategy.NEVER)
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}
@@ -0,0 +1,25 @@
package com.reelshort.system.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_role_menu")
public class RoleMenu {
@TableId(type = IdType.AUTO)
private Long id;
private Long roleId;
private Long menuId;
public RoleMenu(Long roleId, Long menuId) {
this.roleId = roleId;
this.menuId = menuId;
}
}
@@ -0,0 +1,25 @@
package com.reelshort.system.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_role_permission")
public class RolePermission {
@TableId(type = IdType.AUTO)
private Long id;
private Long roleId;
private Long permissionId;
public RolePermission(Long roleId, Long permissionId) {
this.roleId = roleId;
this.permissionId = permissionId;
}
}
@@ -0,0 +1,17 @@
package com.reelshort.system.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("sys_config")
public class SysConfig {
@TableId(type = IdType.AUTO)
private Long id;
private String configKey;
private String configValue;
private String remark;
}
@@ -0,0 +1,7 @@
package com.reelshort.system.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reelshort.system.dal.entity.AdminUser;
public interface AdminUserMapper extends BaseMapper<AdminUser> {
}
@@ -0,0 +1,7 @@
package com.reelshort.system.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reelshort.system.dal.entity.AdminUserRole;
public interface AdminUserRoleMapper extends BaseMapper<AdminUserRole> {
}
@@ -0,0 +1,7 @@
package com.reelshort.system.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reelshort.system.dal.entity.Menu;
public interface MenuMapper extends BaseMapper<Menu> {
}
@@ -0,0 +1,7 @@
package com.reelshort.system.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reelshort.system.dal.entity.OperationLog;
public interface OperationLogMapper extends BaseMapper<OperationLog> {
}
@@ -0,0 +1,7 @@
package com.reelshort.system.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reelshort.system.dal.entity.Permission;
public interface PermissionMapper extends BaseMapper<Permission> {
}
@@ -0,0 +1,7 @@
package com.reelshort.system.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reelshort.system.dal.entity.RoleCategory;
public interface RoleCategoryMapper extends BaseMapper<RoleCategory> {
}
@@ -0,0 +1,7 @@
package com.reelshort.system.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reelshort.system.dal.entity.Role;
public interface RoleMapper extends BaseMapper<Role> {
}
@@ -0,0 +1,7 @@
package com.reelshort.system.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reelshort.system.dal.entity.RoleMenu;
public interface RoleMenuMapper extends BaseMapper<RoleMenu> {
}
@@ -0,0 +1,7 @@
package com.reelshort.system.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reelshort.system.dal.entity.RolePermission;
public interface RolePermissionMapper extends BaseMapper<RolePermission> {
}
@@ -0,0 +1,7 @@
package com.reelshort.system.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reelshort.system.dal.entity.SysConfig;
public interface SysConfigMapper extends BaseMapper<SysConfig> {
}
@@ -0,0 +1,197 @@
package com.reelshort.system.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reelshort.common.exception.ErrorCode;
import com.reelshort.common.exception.ServiceException;
import com.reelshort.common.pojo.PageResult;
import com.reelshort.system.dal.entity.AdminUser;
import com.reelshort.system.dal.entity.AdminUserRole;
import com.reelshort.system.dal.mapper.AdminUserMapper;
import com.reelshort.system.dal.mapper.AdminUserRoleMapper;
import com.reelshort.system.vo.AdminUserPageReqVO;
import com.reelshort.system.vo.AdminUserRespVO;
import com.reelshort.system.vo.AdminUserSaveReqVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class AdminUserService {
/** 内置超级管理员账号,不允许删除或停用。 */
public static final String BUILTIN_ADMIN = "admin";
private final AdminUserMapper adminUserMapper;
private final AdminUserRoleMapper adminUserRoleMapper;
private final RoleService roleService;
private final PasswordEncoder passwordEncoder;
public PageResult<AdminUserRespVO> page(AdminUserPageReqVO req) {
List<Long> userIdsOfRole = req.getRoleId() == null ? null
: adminUserRoleMapper.selectList(Wrappers.<AdminUserRole>lambdaQuery()
.eq(AdminUserRole::getRoleId, req.getRoleId()))
.stream().map(AdminUserRole::getUserId).toList();
if (userIdsOfRole != null && userIdsOfRole.isEmpty()) {
return PageResult.empty();
}
Page<AdminUser> page = new Page<>(req.getSafePageIndex(), req.getSafePageSize());
IPage<AdminUser> result = adminUserMapper.selectPage(page, Wrappers.<AdminUser>lambdaQuery()
.like(StringUtils.hasText(req.getUsername()), AdminUser::getUsername, req.getUsername())
.eq(req.getStatus() != null, AdminUser::getStatus, req.getStatus())
.in(userIdsOfRole != null, AdminUser::getId, userIdsOfRole)
.orderByAsc(AdminUser::getId));
return new PageResult<>(result.getTotal(), toRespList(result.getRecords()));
}
public AdminUserRespVO get(Long id) {
AdminUser user = getExisting(id);
return toRespList(List.of(user)).get(0);
}
public AdminUser getByUsername(String username) {
return adminUserMapper.selectOne(
Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getUsername, username));
}
public AdminUser getExisting(Long id) {
AdminUser user = id == null ? null : adminUserMapper.selectById(id);
if (user == null) {
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "管理员不存在");
}
return user;
}
@Transactional(rollbackFor = Exception.class)
public Long create(AdminUserSaveReqVO req) {
if (!StringUtils.hasText(req.getUsername())) {
throw new ServiceException(ErrorCode.BAD_REQUEST, "请输入登录账号");
}
if (!StringUtils.hasText(req.getPassword())) {
throw new ServiceException(ErrorCode.BAD_REQUEST, "请输入初始密码");
}
if (getByUsername(req.getUsername()) != null) {
throw new ServiceException(ErrorCode.DATA_EXISTS, "登录账号已存在");
}
roleService.getExisting(req.getRoleId());
AdminUser user = new AdminUser();
user.setUsername(req.getUsername());
user.setPassword(passwordEncoder.encode(req.getPassword()));
user.setRealName(req.getName());
user.setStatus(req.getStatus() == null ? 1 : req.getStatus());
user.setIpWhitelist(req.getIpWhitelist() == null ? "" : req.getIpWhitelist());
user.setTwoFactor(req.getTwoFactor() == null ? 0 : req.getTwoFactor());
user.setMobile("");
user.setEmail("");
user.setAvatar("");
user.setLastLoginIp("");
adminUserMapper.insert(user);
adminUserRoleMapper.insert(new AdminUserRole(user.getId(), req.getRoleId()));
return user.getId();
}
@Transactional(rollbackFor = Exception.class)
public void update(Long id, AdminUserSaveReqVO req) {
AdminUser user = getExisting(id);
roleService.getExisting(req.getRoleId());
user.setRealName(req.getName());
if (req.getIpWhitelist() != null) {
user.setIpWhitelist(req.getIpWhitelist());
}
if (req.getTwoFactor() != null) {
user.setTwoFactor(req.getTwoFactor());
}
if (req.getStatus() != null) {
if (BUILTIN_ADMIN.equals(user.getUsername()) && req.getStatus() != 1) {
throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "内置管理员账号不能停用");
}
user.setStatus(req.getStatus());
}
adminUserMapper.updateById(user);
adminUserRoleMapper.delete(Wrappers.<AdminUserRole>lambdaQuery().eq(AdminUserRole::getUserId, id));
adminUserRoleMapper.insert(new AdminUserRole(id, req.getRoleId()));
}
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
AdminUser user = getExisting(id);
if (BUILTIN_ADMIN.equals(user.getUsername())) {
throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "内置管理员账号不能删除");
}
adminUserMapper.deleteById(id);
adminUserRoleMapper.delete(Wrappers.<AdminUserRole>lambdaQuery().eq(AdminUserRole::getUserId, id));
}
public void updateStatus(Long id, Integer status) {
AdminUser user = getExisting(id);
if (BUILTIN_ADMIN.equals(user.getUsername()) && status != null && status != 1) {
throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "内置管理员账号不能停用");
}
user.setStatus(status == null ? 1 : status);
adminUserMapper.updateById(user);
}
public void resetPassword(Long id, String newPassword) {
AdminUser user = getExisting(id);
if (!StringUtils.hasText(newPassword) || newPassword.length() < 6) {
throw new ServiceException(ErrorCode.BAD_REQUEST, "新密码长度不能少于 6 位");
}
user.setPassword(passwordEncoder.encode(newPassword));
adminUserMapper.updateById(user);
}
public void changePassword(Long id, String oldPassword, String newPassword) {
AdminUser user = getExisting(id);
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
throw new ServiceException(ErrorCode.BAD_REQUEST, "原密码不正确");
}
user.setPassword(passwordEncoder.encode(newPassword));
adminUserMapper.updateById(user);
}
public void recordLogin(Long id, String ip) {
AdminUser user = adminUserMapper.selectById(id);
if (user == null) {
return;
}
user.setLastLoginAt(LocalDateTime.now());
user.setLastLoginIp(ip == null ? "" : ip);
adminUserMapper.updateById(user);
}
private List<AdminUserRespVO> toRespList(List<AdminUser> users) {
if (users.isEmpty()) {
return List.of();
}
Map<Long, String> roleNames = roleService.roleNameMap();
return users.stream().map(user -> {
List<Long> roleIds = roleService.listRoleIdsByUserId(user.getId());
Long roleId = roleIds.isEmpty() ? null : roleIds.get(0);
AdminUserRespVO vo = new AdminUserRespVO();
vo.setId(user.getId());
vo.setUsername(user.getUsername());
vo.setName(user.getRealName());
vo.setRoleId(roleId);
vo.setRole(roleId == null ? "" : roleNames.getOrDefault(roleId, ""));
vo.setIpWhitelist(StringUtils.hasText(user.getIpWhitelist()) ? user.getIpWhitelist() : "不限");
vo.setStatus(user.getStatus());
vo.setStatusText(user.getStatus() != null && user.getStatus() == 1 ? "启用" : "停用");
vo.setTwoFactor(user.getTwoFactor());
vo.setTwoFactorText(user.getTwoFactor() != null && user.getTwoFactor() == 1 ? "已开启" : "未开启");
vo.setCreatedAt(user.getCreatedAt());
vo.setLastLogin(user.getLastLoginAt());
return vo;
}).toList();
}
}
@@ -0,0 +1,80 @@
package com.reelshort.system.service;
import com.reelshort.common.exception.ErrorCode;
import com.reelshort.common.exception.ServiceException;
import com.reelshort.framework.security.JwtTokenProvider;
import com.reelshort.framework.security.LoginUser;
import com.reelshort.system.dal.entity.AdminUser;
import com.reelshort.system.dal.entity.Role;
import com.reelshort.system.vo.LoginReqVO;
import com.reelshort.system.vo.LoginRespVO;
import com.reelshort.system.vo.MenuVO;
import com.reelshort.system.vo.ProfileRespVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class AuthService {
private final AdminUserService adminUserService;
private final RoleService roleService;
private final MenuService menuService;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider tokenProvider;
public LoginRespVO login(LoginReqVO req, String ip) {
AdminUser user = adminUserService.getByUsername(req.getUsername());
if (user == null || !passwordEncoder.matches(req.getPassword(), user.getPassword())) {
throw new ServiceException(ErrorCode.LOGIN_FAILED);
}
if (user.getStatus() == null || user.getStatus() != 1) {
throw new ServiceException(ErrorCode.ACCOUNT_DISABLED);
}
Role role = roleService.getRoleOfUser(user.getId());
List<String> permissions = role == null ? List.of() : roleService.listCodesByRoleId(role.getId());
LoginUser loginUser = new LoginUser(
user.getId(), user.getUsername(), role == null ? null : role.getCode(), permissions);
adminUserService.recordLogin(user.getId(), ip);
return LoginRespVO.builder()
.token(tokenProvider.createAdminToken(loginUser))
.userId(user.getId())
.username(user.getUsername())
.name(user.getRealName())
.avatar(user.getAvatar())
.role(role == null ? null : role.getCode())
.roleName(role == null ? null : role.getName())
.roleId(role == null ? null : role.getId())
.permissions(permissions)
.build();
}
public List<MenuVO> listMenus(Long userId) {
return menuService.listForUser(userId);
}
/** 当前登录人信息(含实时权限),前端每次进入应用会重新拉取。 */
public ProfileRespVO profile(Long userId) {
AdminUser user = adminUserService.getExisting(userId);
Role role = roleService.getRoleOfUser(userId);
List<String> permissions = role == null ? List.of() : roleService.listCodesByRoleId(role.getId());
return ProfileRespVO.builder()
.userId(user.getId())
.username(user.getUsername())
.name(user.getRealName())
.avatar(user.getAvatar())
.role(role == null ? null : role.getCode())
.roleName(role == null ? null : role.getName())
.roleId(role == null ? null : role.getId())
.permissions(permissions)
.build();
}
public void changePassword(Long userId, String oldPassword, String newPassword) {
adminUserService.changePassword(userId, oldPassword, newPassword);
}
}
@@ -0,0 +1,49 @@
package com.reelshort.system.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.reelshort.system.dal.entity.SysConfig;
import com.reelshort.system.dal.mapper.SysConfigMapper;
import com.reelshort.system.vo.ConfigItemVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class ConfigService {
private final SysConfigMapper sysConfigMapper;
public List<ConfigItemVO> list() {
return sysConfigMapper.selectList(null).stream()
.map(config -> new ConfigItemVO(config.getConfigKey(), config.getConfigValue(), config.getRemark()))
.toList();
}
public String get(String key, String defaultValue) {
SysConfig config = sysConfigMapper.selectOne(
Wrappers.<SysConfig>lambdaQuery().eq(SysConfig::getConfigKey, key));
return config == null || config.getConfigValue() == null ? defaultValue : config.getConfigValue();
}
@Transactional(rollbackFor = Exception.class)
public void update(Map<String, String> values) {
values.forEach((key, value) -> {
SysConfig config = sysConfigMapper.selectOne(
Wrappers.<SysConfig>lambdaQuery().eq(SysConfig::getConfigKey, key));
if (config == null) {
config = new SysConfig();
config.setConfigKey(key);
config.setConfigValue(value);
config.setRemark("");
sysConfigMapper.insert(config);
} else {
config.setConfigValue(value);
sysConfigMapper.updateById(config);
}
});
}
}
@@ -0,0 +1,143 @@
package com.reelshort.system.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.reelshort.common.exception.ErrorCode;
import com.reelshort.common.exception.ServiceException;
import com.reelshort.system.dal.entity.Menu;
import com.reelshort.system.dal.entity.Role;
import com.reelshort.system.dal.mapper.MenuMapper;
import com.reelshort.system.vo.MenuSaveReqVO;
import com.reelshort.system.vo.MenuVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class MenuService {
private final MenuMapper menuMapper;
private final RoleService roleService;
/** 当前用户可见的菜单路由树。 */
public List<MenuVO> listForUser(Long userId) {
Role role = roleService.getRoleOfUser(userId);
if (role == null) {
return List.of();
}
List<Menu> all = listEnabled();
if (RoleService.SUPER_ADMIN_CODE.equals(role.getCode())) {
return buildTree(all);
}
List<String> codes = roleService.listCodesByRoleId(role.getId());
return buildTree(all.stream()
.filter(menu -> codes.contains(menu.getPermissionCode()))
.toList());
}
/** 菜单管理页使用的完整菜单树。 */
public List<MenuVO> listAll() {
return buildTree(menuMapper.selectList(
Wrappers.<Menu>lambdaQuery().orderByAsc(Menu::getParentId).orderByAsc(Menu::getSort)));
}
public Long create(MenuSaveReqVO req) {
Menu menu = new Menu();
apply(menu, req);
menuMapper.insert(menu);
return menu.getId();
}
public void update(Long id, MenuSaveReqVO req) {
Menu menu = menuMapper.selectById(id);
if (menu == null) {
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "菜单不存在");
}
if (id.equals(req.getParentId())) {
throw new ServiceException(ErrorCode.BAD_REQUEST, "上级菜单不能是自己");
}
apply(menu, req);
menuMapper.updateById(menu);
}
public void delete(Long id) {
Long children = menuMapper.selectCount(
Wrappers.<Menu>lambdaQuery().eq(Menu::getParentId, id));
if (children != null && children > 0) {
throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "请先删除子菜单");
}
menuMapper.deleteById(id);
}
private List<Menu> listEnabled() {
return menuMapper.selectList(Wrappers.<Menu>lambdaQuery()
.eq(Menu::getStatus, 1)
.eq(Menu::getVisible, 1)
.orderByAsc(Menu::getSort));
}
private void apply(Menu menu, MenuSaveReqVO req) {
menu.setParentId(req.getParentId() == null ? 0L : req.getParentId());
menu.setTitle(req.getTitle());
menu.setRoutePath(nullToEmpty(req.getRoutePath()));
menu.setRouteName(nullToEmpty(req.getRouteName()));
menu.setComponent(nullToEmpty(req.getComponent()));
menu.setRedirect(nullToEmpty(req.getRedirect()));
menu.setIcon(nullToEmpty(req.getIcon()));
menu.setPermissionCode(nullToEmpty(req.getPermissionCode()));
menu.setType(req.getType() == null ? "MENU" : req.getType());
menu.setSort(req.getSort() == null ? 1 : req.getSort());
menu.setVisible(req.getVisible() == null ? 1 : req.getVisible());
menu.setStatus(req.getStatus() == null ? 1 : req.getStatus());
boolean catalog = "CATALOG".equals(menu.getType());
menu.setAlwaysShow(catalog ? 1 : 0);
menu.setAffix(menu.getAffix() == null ? 0 : menu.getAffix());
}
private String nullToEmpty(String value) {
return value == null ? "" : value;
}
private List<MenuVO> buildTree(List<Menu> menus) {
Map<Long, MenuVO> nodes = new LinkedHashMap<>();
for (Menu menu : menus) {
MenuVO vo = new MenuVO();
vo.setId(menu.getId());
vo.setParentId(menu.getParentId());
vo.setPath(menu.getRoutePath());
vo.setName(menu.getRouteName());
vo.setComponent(menu.getComponent());
vo.setRedirect(menu.getRedirect());
vo.setIcon(menu.getIcon());
vo.setType(menu.getType());
vo.setPermissionCode(menu.getPermissionCode());
vo.setSort(menu.getSort());
vo.setVisible(menu.getVisible());
vo.setStatus(menu.getStatus());
vo.setMeta(new MenuVO.Meta(
menu.getTitle(),
menu.getIcon().isEmpty() ? null : menu.getIcon(),
menu.getAlwaysShow() != null && menu.getAlwaysShow() == 1 ? Boolean.TRUE : null,
menu.getAffix() != null && menu.getAffix() == 1 ? Boolean.TRUE : null));
nodes.put(menu.getId(), vo);
}
List<MenuVO> roots = new ArrayList<>();
for (Menu menu : menus) {
MenuVO node = nodes.get(menu.getId());
MenuVO parent = menu.getParentId() == null ? null : nodes.get(menu.getParentId());
if (parent == null) {
roots.add(node);
} else {
if (parent.getChildren() == null) {
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(node);
}
}
return roots;
}
}
@@ -0,0 +1,95 @@
package com.reelshort.system.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reelshort.common.exception.ErrorCode;
import com.reelshort.common.exception.ServiceException;
import com.reelshort.common.pojo.PageResult;
import com.reelshort.framework.log.OperationLogEntry;
import com.reelshort.framework.log.OperationLogSink;
import com.reelshort.system.dal.entity.OperationLog;
import com.reelshort.system.dal.mapper.OperationLogMapper;
import com.reelshort.system.vo.OperationLogPageReqVO;
import com.reelshort.system.vo.OperationLogRespVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 操作日志:由 {@code @OperationLog} 切面自动写入,后台只读查询。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OperationLogService implements OperationLogSink {
private final OperationLogMapper operationLogMapper;
@Override
public void save(OperationLogEntry entry) {
OperationLog entity = new OperationLog();
entity.setAdminUserId(entry.getUserId() == null ? 0L : entry.getUserId());
entity.setUsername(entry.getUsername() == null ? "" : entry.getUsername());
entity.setModule(entry.getModule() == null ? "" : entry.getModule());
entity.setAction(entry.getAction() == null ? "" : entry.getAction());
entity.setRequestMethod(entry.getRequestMethod() == null ? "" : entry.getRequestMethod());
entity.setRequestUrl(entry.getRequestUrl() == null ? "" : entry.getRequestUrl());
entity.setRequestParams(entry.getRequestParams());
entity.setRequestBody(entry.getRequestBody());
entity.setResponseBody(entry.getResponseBody());
entity.setStatus(entry.getStatus() == null ? "SUCCESS" : entry.getStatus());
entity.setErrorMsg(entry.getErrorMsg() == null ? "" : entry.getErrorMsg());
entity.setDurationMs(entry.getDurationMs() == null ? 0 : entry.getDurationMs());
entity.setIp(entry.getIp() == null ? "" : entry.getIp());
operationLogMapper.insert(entity);
}
public PageResult<OperationLogRespVO> page(OperationLogPageReqVO req) {
Page<OperationLog> page = new Page<>(req.getSafePageIndex(), req.getSafePageSize());
IPage<OperationLog> result = operationLogMapper.selectPage(page,
Wrappers.<OperationLog>lambdaQuery()
.like(StringUtils.hasText(req.getUsername()), OperationLog::getUsername, req.getUsername())
.eq(StringUtils.hasText(req.getModule()), OperationLog::getModule, req.getModule())
.like(StringUtils.hasText(req.getAction()), OperationLog::getAction, req.getAction())
.eq(StringUtils.hasText(req.getStatus()), OperationLog::getStatus, req.getStatus())
.ge(req.getStartTime() != null, OperationLog::getCreatedAt, req.getStartTime())
.le(req.getEndTime() != null, OperationLog::getCreatedAt, req.getEndTime())
.orderByDesc(OperationLog::getId));
return new PageResult<>(result.getTotal(), toRespList(result.getRecords()));
}
public OperationLogRespVO get(Long id) {
OperationLog entity = id == null ? null : operationLogMapper.selectById(id);
if (entity == null) {
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "操作日志不存在");
}
return toRespList(List.of(entity)).get(0);
}
private List<OperationLogRespVO> toRespList(List<OperationLog> logs) {
return logs.stream().map(entity -> {
OperationLogRespVO vo = new OperationLogRespVO();
vo.setId(entity.getId());
vo.setUserId(entity.getAdminUserId());
vo.setUsername(entity.getUsername());
vo.setModule(entity.getModule());
vo.setAction(entity.getAction());
vo.setRequestMethod(entity.getRequestMethod());
vo.setRequestUrl(entity.getRequestUrl());
vo.setRequestParams(entity.getRequestParams());
vo.setRequestBody(entity.getRequestBody());
vo.setResponseBody(entity.getResponseBody());
vo.setStatus(entity.getStatus());
vo.setStatusText("SUCCESS".equals(entity.getStatus()) ? "成功" : "失败");
vo.setErrorMsg(entity.getErrorMsg());
vo.setDurationMs(entity.getDurationMs());
vo.setIp(entity.getIp());
vo.setCreatedAt(entity.getCreatedAt());
return vo;
}).toList();
}
}
@@ -0,0 +1,62 @@
package com.reelshort.system.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.reelshort.common.exception.ErrorCode;
import com.reelshort.common.exception.ServiceException;
import com.reelshort.system.dal.entity.Permission;
import com.reelshort.system.dal.mapper.PermissionMapper;
import com.reelshort.system.vo.PermissionGroupVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 权限点主数据。
*/
@Service
@RequiredArgsConstructor
public class PermissionService {
private final PermissionMapper permissionMapper;
public List<Permission> listAll() {
return permissionMapper.selectList(
Wrappers.<Permission>lambdaQuery().orderByAsc(Permission::getSort));
}
public List<PermissionGroupVO> listGroups() {
Map<String, List<PermissionGroupVO.Item>> grouped = new LinkedHashMap<>();
for (Permission permission : listAll()) {
grouped.computeIfAbsent(permission.getGroupName(), key -> new ArrayList<>())
.add(new PermissionGroupVO.Item(permission.getCode(), permission.getName()));
}
return grouped.entrySet().stream()
.map(entry -> new PermissionGroupVO(entry.getKey(), entry.getValue()))
.toList();
}
public List<Long> listIdsByCodes(List<String> codes) {
if (codes == null || codes.isEmpty()) {
return List.of();
}
List<Permission> permissions = permissionMapper.selectList(
Wrappers.<Permission>lambdaQuery().in(Permission::getCode, codes));
if (permissions.size() != codes.stream().distinct().count()) {
throw new ServiceException(ErrorCode.BAD_REQUEST, "存在无效的权限码");
}
return permissions.stream().map(Permission::getId).toList();
}
public List<String> listCodesByIds(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
return List.of();
}
return permissionMapper.selectList(
Wrappers.<Permission>lambdaQuery().in(Permission::getId, ids))
.stream().map(Permission::getCode).toList();
}
}
@@ -0,0 +1,90 @@
package com.reelshort.system.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.reelshort.common.exception.ErrorCode;
import com.reelshort.common.exception.ServiceException;
import com.reelshort.system.dal.entity.Role;
import com.reelshort.system.dal.entity.RoleCategory;
import com.reelshort.system.dal.mapper.RoleCategoryMapper;
import com.reelshort.system.dal.mapper.RoleMapper;
import com.reelshort.system.vo.RoleCategoryRespVO;
import com.reelshort.system.vo.RoleCategorySaveReqVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class RoleCategoryService {
private final RoleCategoryMapper roleCategoryMapper;
private final RoleMapper roleMapper;
public List<RoleCategoryRespVO> list() {
Map<Long, Integer> counts = new HashMap<>();
roleMapper.selectList(null).forEach(role ->
counts.merge(role.getCategoryId(), 1, Integer::sum));
return roleCategoryMapper.selectList(
Wrappers.<RoleCategory>lambdaQuery().orderByAsc(RoleCategory::getSort))
.stream().map(category -> {
RoleCategoryRespVO vo = new RoleCategoryRespVO();
vo.setId(category.getId());
vo.setName(category.getName());
vo.setDescription(category.getDescription());
vo.setRoleCount(counts.getOrDefault(category.getId(), 0));
return vo;
}).toList();
}
public Long create(RoleCategorySaveReqVO req) {
ensureNameAvailable(req.getName(), null);
RoleCategory category = new RoleCategory();
category.setName(req.getName());
category.setDescription(req.getDescription() == null ? "" : req.getDescription());
Long count = roleCategoryMapper.selectCount(null);
category.setSort((count == null ? 0 : count.intValue()) + 1);
roleCategoryMapper.insert(category);
return category.getId();
}
public void update(Long id, RoleCategorySaveReqVO req) {
RoleCategory category = getExisting(id);
ensureNameAvailable(req.getName(), id);
category.setName(req.getName());
if (req.getDescription() != null) {
category.setDescription(req.getDescription());
}
roleCategoryMapper.updateById(category);
}
public void delete(Long id) {
RoleCategory category = getExisting(id);
Long used = roleMapper.selectCount(
Wrappers.<Role>lambdaQuery().eq(Role::getCategoryId, id));
if (used != null && used > 0) {
throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED,
"分类「" + category.getName() + "」下仍有角色,无法删除");
}
roleCategoryMapper.deleteById(id);
}
private RoleCategory getExisting(Long id) {
RoleCategory category = id == null ? null : roleCategoryMapper.selectById(id);
if (category == null) {
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "角色分类不存在");
}
return category;
}
private void ensureNameAvailable(String name, Long excludeId) {
Long count = roleCategoryMapper.selectCount(Wrappers.<RoleCategory>lambdaQuery()
.eq(RoleCategory::getName, name)
.ne(excludeId != null, RoleCategory::getId, excludeId));
if (count != null && count > 0) {
throw new ServiceException(ErrorCode.DATA_EXISTS, "分类名称已存在");
}
}
}
@@ -0,0 +1,236 @@
package com.reelshort.system.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reelshort.common.exception.ErrorCode;
import com.reelshort.common.exception.ServiceException;
import com.reelshort.common.pojo.PageResult;
import com.reelshort.system.dal.entity.AdminUserRole;
import com.reelshort.system.dal.entity.Menu;
import com.reelshort.system.dal.entity.Role;
import com.reelshort.system.dal.entity.RoleCategory;
import com.reelshort.system.dal.entity.RoleMenu;
import com.reelshort.system.dal.entity.RolePermission;
import com.reelshort.system.dal.mapper.AdminUserRoleMapper;
import com.reelshort.system.dal.mapper.MenuMapper;
import com.reelshort.system.dal.mapper.RoleCategoryMapper;
import com.reelshort.system.dal.mapper.RoleMapper;
import com.reelshort.system.dal.mapper.RoleMenuMapper;
import com.reelshort.system.dal.mapper.RolePermissionMapper;
import com.reelshort.system.vo.RoleRespVO;
import com.reelshort.system.vo.RoleSaveReqVO;
import com.reelshort.framework.security.PermissionProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class RoleService implements PermissionProvider {
public static final String SUPER_ADMIN_CODE = "super_admin";
private final RoleMapper roleMapper;
private final RoleCategoryMapper roleCategoryMapper;
private final RolePermissionMapper rolePermissionMapper;
private final RoleMenuMapper roleMenuMapper;
private final AdminUserRoleMapper adminUserRoleMapper;
private final MenuMapper menuMapper;
private final PermissionService permissionService;
public PageResult<RoleRespVO> page(String keyword, Integer pageIndex, Integer pageSize) {
Page<Role> page = new Page<>(pageIndex == null || pageIndex < 1 ? 1 : pageIndex,
pageSize == null || pageSize < 1 ? 10 : pageSize);
IPage<Role> result = roleMapper.selectPage(page,
Wrappers.<Role>lambdaQuery()
.like(StringUtils.hasText(keyword), Role::getName, keyword)
.orderByAsc(Role::getLevel));
return new PageResult<>(result.getTotal(), toRespList(result.getRecords()));
}
public List<RoleRespVO> listAll() {
List<Role> roles = roleMapper.selectList(
Wrappers.<Role>lambdaQuery().orderByAsc(Role::getLevel));
return toRespList(roles);
}
public RoleRespVO get(Long id) {
Role role = getExisting(id);
return toRespList(List.of(role)).get(0);
}
@Transactional(rollbackFor = Exception.class)
public Long create(RoleSaveReqVO req) {
ensureNameAvailable(req.getName(), null);
Role role = new Role();
role.setName(req.getName());
role.setCode(StringUtils.hasText(req.getCode()) ? req.getCode() : req.getName());
role.setCategoryId(req.getCategoryId() == null ? 0L : req.getCategoryId());
role.setLevel(req.getLevel() == null ? 9 : req.getLevel());
role.setDescription(req.getDescription() == null ? "" : req.getDescription());
role.setStatus(req.getStatus() == null ? 1 : req.getStatus());
role.setIsBuiltin(0);
roleMapper.insert(role);
if (req.getPermissions() != null) {
replacePermissions(role.getId(), req.getPermissions());
}
return role.getId();
}
@Transactional(rollbackFor = Exception.class)
public void update(Long id, RoleSaveReqVO req) {
Role role = getExisting(id);
ensureNameAvailable(req.getName(), id);
role.setName(req.getName());
if (StringUtils.hasText(req.getCode()) && !SUPER_ADMIN_CODE.equals(role.getCode())) {
role.setCode(req.getCode());
}
if (req.getCategoryId() != null) {
role.setCategoryId(req.getCategoryId());
}
if (req.getLevel() != null) {
role.setLevel(req.getLevel());
}
if (req.getDescription() != null) {
role.setDescription(req.getDescription());
}
if (req.getStatus() != null && role.getIsBuiltin() != 1) {
role.setStatus(req.getStatus());
}
roleMapper.updateById(role);
if (req.getPermissions() != null) {
replacePermissions(id, req.getPermissions());
}
}
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
Role role = getExisting(id);
if (role.getIsBuiltin() == 1) {
throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "内置角色不可删除");
}
roleMapper.deleteById(id);
rolePermissionMapper.delete(Wrappers.<RolePermission>lambdaQuery().eq(RolePermission::getRoleId, id));
roleMenuMapper.delete(Wrappers.<RoleMenu>lambdaQuery().eq(RoleMenu::getRoleId, id));
}
@Transactional(rollbackFor = Exception.class)
public void updatePermissions(Long roleId, List<String> codes) {
getExisting(roleId);
replacePermissions(roleId, codes);
}
public List<String> listCodesByRoleId(Long roleId) {
List<RolePermission> links = rolePermissionMapper.selectList(
Wrappers.<RolePermission>lambdaQuery().eq(RolePermission::getRoleId, roleId));
return permissionService.listCodesByIds(links.stream().map(RolePermission::getPermissionId).toList());
}
public List<String> listCodesByUserId(Long userId) {
List<Long> roleIds = listRoleIdsByUserId(userId);
if (roleIds.isEmpty()) {
return List.of();
}
List<RolePermission> links = rolePermissionMapper.selectList(
Wrappers.<RolePermission>lambdaQuery().in(RolePermission::getRoleId, roleIds));
return permissionService.listCodesByIds(
links.stream().map(RolePermission::getPermissionId).distinct().toList());
}
/** 供框架层实时判断权限使用:角色的权限改了立即生效,不需要重新登录。 */
@Override
public List<String> listPermissionsByUserId(Long userId) {
return listCodesByUserId(userId);
}
public List<Long> listRoleIdsByUserId(Long userId) {
return adminUserRoleMapper.selectList(
Wrappers.<AdminUserRole>lambdaQuery().eq(AdminUserRole::getUserId, userId))
.stream().map(AdminUserRole::getRoleId).toList();
}
public Role getRoleOfUser(Long userId) {
List<Long> roleIds = listRoleIdsByUserId(userId);
if (roleIds.isEmpty()) {
return null;
}
return roleMapper.selectById(roleIds.get(0));
}
public Role getExisting(Long id) {
Role role = id == null ? null : roleMapper.selectById(id);
if (role == null) {
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "角色不存在");
}
return role;
}
/** 权限变化时同步角色可访问的菜单,菜单树由权限码派生。 */
private void replacePermissions(Long roleId, List<String> codes) {
List<Long> permissionIds = permissionService.listIdsByCodes(codes);
rolePermissionMapper.delete(Wrappers.<RolePermission>lambdaQuery().eq(RolePermission::getRoleId, roleId));
permissionIds.forEach(permissionId -> rolePermissionMapper.insert(new RolePermission(roleId, permissionId)));
List<Long> menuIds = codes == null || codes.isEmpty() ? List.of()
: menuMapper.selectList(Wrappers.<Menu>lambdaQuery().in(Menu::getPermissionCode, codes))
.stream().map(Menu::getId).toList();
roleMenuMapper.delete(Wrappers.<RoleMenu>lambdaQuery().eq(RoleMenu::getRoleId, roleId));
menuIds.forEach(menuId -> roleMenuMapper.insert(new RoleMenu(roleId, menuId)));
}
private void ensureNameAvailable(String name, Long excludeId) {
Long count = roleMapper.selectCount(Wrappers.<Role>lambdaQuery()
.eq(Role::getName, name)
.ne(excludeId != null, Role::getId, excludeId));
if (count != null && count > 0) {
throw new ServiceException(ErrorCode.DATA_EXISTS, "角色名称已存在");
}
}
private List<RoleRespVO> toRespList(List<Role> roles) {
if (roles.isEmpty()) {
return List.of();
}
Map<Long, String> categoryNames = roleCategoryMapper.selectList(null).stream()
.collect(Collectors.toMap(RoleCategory::getId, RoleCategory::getName, (a, b) -> a));
Map<Long, Integer> memberCounts = new HashMap<>();
adminUserRoleMapper.selectList(null)
.forEach(link -> memberCounts.merge(link.getRoleId(), 1, Integer::sum));
Map<Long, List<String>> permissionCache = new HashMap<>();
return roles.stream().map(role -> {
RoleRespVO vo = new RoleRespVO();
vo.setId(role.getId());
vo.setName(role.getName());
vo.setCode(role.getCode());
vo.setDescription(role.getDescription());
vo.setCategoryId(role.getCategoryId());
vo.setCategory(categoryNames.getOrDefault(role.getCategoryId(), ""));
vo.setLevel(role.getLevel());
vo.setStatus(role.getStatus());
vo.setStatusText(role.getStatus() != null && role.getStatus() == 1 ? "启用" : "停用");
vo.setBuiltin(role.getIsBuiltin() != null && role.getIsBuiltin() == 1);
vo.setMemberCount(memberCounts.getOrDefault(role.getId(), 0));
vo.setPermissions(permissionCache.computeIfAbsent(role.getId(), this::listCodesByRoleId));
return vo;
}).toList();
}
public Map<Long, String> roleNameMap() {
return roleMapper.selectList(null).stream()
.collect(Collectors.toMap(Role::getId, Role::getName, (a, b) -> a));
}
public Function<Long, String> roleNameResolver() {
Map<Long, String> names = roleNameMap();
return id -> names.getOrDefault(id, "");
}
}
@@ -0,0 +1,14 @@
package com.reelshort.system.vo;
import com.reelshort.common.pojo.PageParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class AdminUserPageReqVO extends PageParam {
private String username;
private Long roleId;
private Integer status;
}
@@ -0,0 +1,27 @@
package com.reelshort.system.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class AdminUserRespVO {
private Long id;
private String username;
private String name;
private Long roleId;
private String role;
private String ipWhitelist;
private Integer status;
private String statusText;
private Integer twoFactor;
private String twoFactorText;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime lastLogin;
}
@@ -0,0 +1,24 @@
package com.reelshort.system.vo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class AdminUserSaveReqVO {
/** 新增时必填,编辑时忽略 */
private String username;
private String password;
@NotBlank(message = "请输入姓名")
private String name;
@NotNull(message = "请选择角色")
private Long roleId;
private String ipWhitelist;
private Integer status;
private Integer twoFactor;
}
@@ -0,0 +1,13 @@
package com.reelshort.system.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ConfigItemVO {
private String key;
private String value;
private String remark;
}
@@ -0,0 +1,14 @@
package com.reelshort.system.vo;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginReqVO {
@NotBlank(message = "请输入账号")
private String username;
@NotBlank(message = "请输入密码")
private String password;
}
@@ -0,0 +1,22 @@
package com.reelshort.system.vo;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class LoginRespVO {
private String token;
private Long userId;
private String username;
private String name;
private String avatar;
/** 角色编码,如 super_admin */
private String role;
private String roleName;
private Long roleId;
private List<String> permissions;
}
@@ -0,0 +1,24 @@
package com.reelshort.system.vo;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class MenuSaveReqVO {
private Long parentId;
@NotBlank(message = "请输入菜单名称")
private String title;
private String routePath;
private String routeName;
private String component;
private String redirect;
private String icon;
private String permissionCode;
private String type;
private Integer sort;
private Integer visible;
private Integer status;
}
@@ -0,0 +1,40 @@
package com.reelshort.system.vo;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
/**
* 菜单路由树,结构必须与前端 element-plus-admin 的动态路由一致。
*/
@Data
public class MenuVO {
private Long id;
private Long parentId;
private String path;
private String name;
private String component;
private String redirect;
private String icon;
/** CATALOG / MENU / BUTTON */
private String type;
private String permissionCode;
private Integer sort;
private Integer visible;
private Integer status;
private Meta meta;
private List<MenuVO> children;
@Data
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class Meta {
private String title;
private String icon;
private Boolean alwaysShow;
private Boolean affix;
}
}
@@ -0,0 +1,28 @@
package com.reelshort.system.vo;
import com.reelshort.common.pojo.PageParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = true)
public class OperationLogPageReqVO extends PageParam {
/** 操作人账号,模糊匹配 */
private String username;
/** 模块,精确匹配 */
private String module;
/** 动作,模糊匹配 */
private String action;
/** SUCCESS / FAIL */
private String status;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startTime;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endTime;
}
@@ -0,0 +1,29 @@
package com.reelshort.system.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class OperationLogRespVO {
private Long id;
private Long userId;
private String username;
private String module;
private String action;
private String requestMethod;
private String requestUrl;
private String requestParams;
private String requestBody;
private String responseBody;
private String status;
private String statusText;
private String errorMsg;
private Integer durationMs;
private String ip;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
}
@@ -0,0 +1,16 @@
package com.reelshort.system.vo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class PasswordChangeReqVO {
@NotBlank(message = "请输入原密码")
private String oldPassword;
@NotBlank(message = "请输入新密码")
@Size(min = 6, max = 32, message = "新密码长度需在 6-32 位之间")
private String newPassword;
}
@@ -0,0 +1,21 @@
package com.reelshort.system.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
@Data
@AllArgsConstructor
public class PermissionGroupVO {
private String name;
private List<Item> items;
@Data
@AllArgsConstructor
public static class Item {
private String code;
private String name;
}
}
@@ -0,0 +1,24 @@
package com.reelshort.system.vo;
import lombok.Builder;
import lombok.Data;
import java.util.List;
/**
* 当前登录人信息,字段与登录返回保持一致,便于前端直接覆盖本地缓存的用户信息。
*/
@Data
@Builder
public class ProfileRespVO {
private Long userId;
private String username;
private String name;
private String avatar;
/** 角色编码,如 super_admin */
private String role;
private String roleName;
private Long roleId;
private List<String> permissions;
}
@@ -0,0 +1,12 @@
package com.reelshort.system.vo;
import lombok.Data;
@Data
public class RoleCategoryRespVO {
private Long id;
private String name;
private String description;
private Integer roleCount;
}
@@ -0,0 +1,13 @@
package com.reelshort.system.vo;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class RoleCategorySaveReqVO {
@NotBlank(message = "请输入分类名称")
private String name;
private String description;
}
@@ -0,0 +1,13 @@
package com.reelshort.system.vo;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Data
public class RolePermissionSaveReqVO {
@NotNull(message = "权限不能为空")
private List<String> permissions;
}
@@ -0,0 +1,22 @@
package com.reelshort.system.vo;
import lombok.Data;
import java.util.List;
@Data
public class RoleRespVO {
private Long id;
private String name;
private String code;
private String description;
private Long categoryId;
private String category;
private Integer level;
private Integer status;
private String statusText;
private Boolean builtin;
private Integer memberCount;
private List<String> permissions;
}
@@ -0,0 +1,24 @@
package com.reelshort.system.vo;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.List;
@Data
public class RoleSaveReqVO {
@NotBlank(message = "请输入角色名称")
private String name;
/** 不传时按名称生成 */
private String code;
private String description;
private Long categoryId;
private Integer level;
private Integer status;
/** 角色权限码,传则整体覆盖 */
private List<String> permissions;
}