添加C端权限相关

This commit is contained in:
zk
2026-03-12 11:48:24 +08:00
parent 0444a5c751
commit 636ca5413f
14 changed files with 816 additions and 0 deletions
@@ -0,0 +1,22 @@
package org.jiayunet.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 功能权限校验注解
* <p>标记在 Controller 方法上,切面会校验当前用户是否拥有该功能权限并扣减库存</p>
*
* @author zk
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FuncPermission {
/**
* 功能权限编码,对应 bg_func_permission.func_code
*/
String value();
}
@@ -0,0 +1,46 @@
package org.jiayunet.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.jiayunet.annotation.FuncPermission;
import org.jiayunet.server.FuncPermissionServer;
import org.jiayunet.tool.UserSecurityTool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 功能权限校验切面
* <p>拦截 @FuncPermission 注解,校验权限并扣减库存后放行</p>
*
* @author zk
*/
@Aspect
@Component
@Slf4j
public class FuncPermissionAspect {
@Autowired
private FuncPermissionServer funcPermissionServer;
@Around("@annotation(funcPermission)")
public Object check(ProceedingJoinPoint joinPoint, FuncPermission funcPermission) throws Throwable {
Long userId = UserSecurityTool.getUserId();
String funcCode = funcPermission.value();
log.info("功能权限校验 userId:{} funcCode:{}", userId, funcCode);
// 校验权限 + 扣减库存
funcPermissionServer.checkAndDeduct(userId, funcCode);
// 放行,业务异常时回退库存
try {
return joinPoint.proceed();
} catch (Exception e) {
funcPermissionServer.rollbackCount(userId, funcCode);
throw e;
}
}
}
@@ -0,0 +1,33 @@
package org.jiayunet.controller;
import lombok.AllArgsConstructor;
import org.jiayunet.pojo.vo.RouteMenuVo;
import org.jiayunet.server.RouteMenuServer;
import org.jiayunet.tool.UserSecurityTool;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 路由菜单控制类
*
* @author zk
*/
@RestController
@RequestMapping("/route")
@AllArgsConstructor
public class RouteMenuController {
private RouteMenuServer routeMenuServer;
/**
* 获取当前用户有效路由菜单(树形结构)
*/
@GetMapping("/menu")
public List<RouteMenuVo> getUserRoutes() {
Long userId = UserSecurityTool.getUserId();
return routeMenuServer.getUserRoutes(userId);
}
}
@@ -0,0 +1,59 @@
package org.jiayunet.pojo.vo;
import lombok.Data;
import java.util.List;
/**
* 路由菜单树形VO
*
* @author zk
*/
@Data
public class RouteMenuVo {
/**
* 主键
*/
private Long id;
/**
* 根节点ID
*/
private Long rootId;
/**
* 父级路由ID
*/
private Long parentId;
/**
* 路由名称
*/
private String routeName;
/**
* 前端路径
*/
private String routePath;
/**
* 前端组件路径
*/
private String component;
/**
* 图标
*/
private String icon;
/**
* 排序
*/
private Integer sortOrder;
/**
* 子菜单
*/
private List<RouteMenuVo> children;
}
@@ -0,0 +1,188 @@
package org.jiayunet.server;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import lombok.extern.slf4j.Slf4j;
import org.jiayunet.exception.BusinessException;
import org.jiayunet.exception.BusinessExpCodeEnum;
import org.jiayunet.mapper.UserFuncPermissionStockMapper;
import org.jiayunet.pojo.po.UserFuncPermissionStock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.Instant;
/**
* 功能权限服务(校验、扣减、查询、添加库存)
*
* @author zk
*/
@Service
@Slf4j
public class FuncPermissionServer {
@Autowired
private UserFuncPermissionStockMapper userFuncPermissionStockMapper;
/**
* 校验用户功能权限并扣减库存
*
* @param userId 用户ID
* @param funcCode 功能权限编码
*/
public void checkAndDeduct(Long userId, String funcCode) {
// 查询用户功能权限库存
UserFuncPermissionStock stock = userFuncPermissionStockMapper.selectOne(
new LambdaQueryWrapper<UserFuncPermissionStock>()
.eq(UserFuncPermissionStock::getUserId, userId)
.eq(UserFuncPermissionStock::getFuncCode, funcCode)
);
// 无记录,无权限
if (stock == null) {
throw new BusinessException(BusinessExpCodeEnum.PERMISSION_DENIED, "无该功能权限");
}
// 时间维度校验
if (stock.getTimeLimit() == 1 && stock.getExpireTime() != null && stock.getExpireTime().isBefore(Instant.now())) {
throw new BusinessException(BusinessExpCodeEnum.PERMISSION_DENIED, "功能权限已过期");
}
// 不限次,直接放行
if (stock.getCountLimit() == 0) {
return;
}
// 限次,扣减库存(乐观锁,SQL原子扣减)
int rows = userFuncPermissionStockMapper.update(null,
new LambdaUpdateWrapper<UserFuncPermissionStock>()
.setSql("remain_count = remain_count - 1")
.eq(UserFuncPermissionStock::getUserId, userId)
.eq(UserFuncPermissionStock::getFuncCode, funcCode)
.gt(UserFuncPermissionStock::getRemainCount, 0)
);
if (rows == 0) {
throw new BusinessException(BusinessExpCodeEnum.PERMISSION_DENIED, "功能使用次数已用完");
}
}
/**
* 查询用户功能权限库存
*
* @param userId 用户ID
* @param funcCode 功能权限编码
* @return 库存记录,不存在返回null
*/
public UserFuncPermissionStock query(Long userId, String funcCode) {
return userFuncPermissionStockMapper.selectOne(
new LambdaQueryWrapper<UserFuncPermissionStock>()
.eq(UserFuncPermissionStock::getUserId, userId)
.eq(UserFuncPermissionStock::getFuncCode, funcCode)
);
}
/**
* 添加或更新时间维度库存
*
* @param userId 用户ID
* @param funcCode 功能权限编码
* @param timeLimit 0=不限时 1=限时
* @param expireTime 过期时间,timeLimit=1时有值
*/
public void addTimeStock(Long userId, String funcCode, Integer timeLimit, Instant expireTime) {
UserFuncPermissionStock existing = query(userId, funcCode);
if (existing == null) {
// 无记录,新增
UserFuncPermissionStock stock = new UserFuncPermissionStock();
stock.setUserId(userId);
stock.setFuncCode(funcCode);
stock.setTimeLimit(timeLimit);
stock.setExpireTime(expireTime);
userFuncPermissionStockMapper.insert(stock);
return;
}
// 已经不限时,不降级
if (existing.getTimeLimit() == 0) {
return;
}
LambdaUpdateWrapper<UserFuncPermissionStock> wrapper = new LambdaUpdateWrapper<UserFuncPermissionStock>()
.eq(UserFuncPermissionStock::getId, existing.getId());
if (timeLimit == 0) {
// 升级为不限时
wrapper.set(UserFuncPermissionStock::getTimeLimit, 0)
.set(UserFuncPermissionStock::getExpireTime, null);
} else {
// 限时,取最远过期时间
Instant finalExpire = existing.getExpireTime() == null || expireTime.isAfter(existing.getExpireTime())
? expireTime : existing.getExpireTime();
wrapper.set(UserFuncPermissionStock::getExpireTime, finalExpire);
}
userFuncPermissionStockMapper.update(null, wrapper);
}
/**
* 添加或更新次数维度库存
*
* @param userId 用户ID
* @param funcCode 功能权限编码
* @param countLimit 0=不限次 1=限次
* @param addCount 新增次数,countLimit=1时有值
*/
public void addCountStock(Long userId, String funcCode, Integer countLimit, Integer addCount) {
UserFuncPermissionStock existing = query(userId, funcCode);
if (existing == null) {
// 无记录,新增
UserFuncPermissionStock stock = new UserFuncPermissionStock();
stock.setUserId(userId);
stock.setFuncCode(funcCode);
stock.setCountLimit(countLimit);
stock.setRemainCount(addCount);
userFuncPermissionStockMapper.insert(stock);
return;
}
// 已经不限次,不降级
if (existing.getCountLimit() == 0) {
return;
}
LambdaUpdateWrapper<UserFuncPermissionStock> wrapper = new LambdaUpdateWrapper<UserFuncPermissionStock>()
.eq(UserFuncPermissionStock::getId, existing.getId());
if (countLimit == 0) {
// 升级为不限次
wrapper.set(UserFuncPermissionStock::getCountLimit, 0)
.set(UserFuncPermissionStock::getRemainCount, null);
} else {
// 限次,累加
int newCount = (existing.getRemainCount() == null ? 0 : existing.getRemainCount()) + addCount;
wrapper.set(UserFuncPermissionStock::getRemainCount, newCount);
}
userFuncPermissionStockMapper.update(null, wrapper);
}
/**
* 回退次数库存(业务异常时调用,仅限次维度有效)
*
* @param userId 用户ID
* @param funcCode 功能权限编码
*/
public void rollbackCount(Long userId, String funcCode) {
userFuncPermissionStockMapper.update(null,
new LambdaUpdateWrapper<UserFuncPermissionStock>()
.setSql("remain_count = remain_count + 1")
.eq(UserFuncPermissionStock::getUserId, userId)
.eq(UserFuncPermissionStock::getFuncCode, funcCode)
.eq(UserFuncPermissionStock::getCountLimit, 1)
);
}
}
@@ -0,0 +1,169 @@
package org.jiayunet.server;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import lombok.extern.slf4j.Slf4j;
import org.jiayunet.mapper.RouteMenuMapper;
import org.jiayunet.mapper.UserRouteMenuStockMapper;
import org.jiayunet.pojo.po.RouteMenu;
import org.jiayunet.pojo.po.UserRouteMenuStock;
import org.jiayunet.pojo.vo.RouteMenuVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
/**
* 路由菜单服务(查询、添加库存、获取用户菜单)
*
* @author zk
*/
@Service
@Slf4j
public class RouteMenuServer {
@Autowired
private UserRouteMenuStockMapper userRouteMenuStockMapper;
@Autowired
private RouteMenuMapper routeMenuMapper;
/**
* 查询用户某个路由的库存
*
* @param userId 用户ID
* @param routeId 路由ID
* @return 库存记录,不存在返回null
*/
public UserRouteMenuStock query(Long userId, Long routeId) {
return userRouteMenuStockMapper.selectOne(
new LambdaQueryWrapper<UserRouteMenuStock>()
.eq(UserRouteMenuStock::getUserId, userId)
.eq(UserRouteMenuStock::getRouteId, routeId)
);
}
/**
* 添加或更新时间维度库存
*
* @param userId 用户ID
* @param routeId 路由ID
* @param timeLimit 0=不限时 1=限时
* @param expireTime 过期时间,timeLimit=1时有值
*/
public void addTimeStock(Long userId, Long routeId, Integer timeLimit, Instant expireTime) {
UserRouteMenuStock existing = query(userId, routeId);
if (existing == null) {
UserRouteMenuStock stock = new UserRouteMenuStock();
stock.setUserId(userId);
stock.setRouteId(routeId);
stock.setTimeLimit(timeLimit);
stock.setExpireTime(expireTime);
userRouteMenuStockMapper.insert(stock);
return;
}
// 已经不限时,不降级
if (existing.getTimeLimit() == 0) {
return;
}
LambdaUpdateWrapper<UserRouteMenuStock> wrapper = new LambdaUpdateWrapper<UserRouteMenuStock>()
.eq(UserRouteMenuStock::getId, existing.getId());
if (timeLimit == 0) {
// 升级为不限时
wrapper.set(UserRouteMenuStock::getTimeLimit, 0)
.set(UserRouteMenuStock::getExpireTime, null);
} else {
// 限时,取最远过期时间
Instant finalExpire = existing.getExpireTime() == null || expireTime.isAfter(existing.getExpireTime())
? expireTime : existing.getExpireTime();
wrapper.set(UserRouteMenuStock::getExpireTime, finalExpire);
}
userRouteMenuStockMapper.update(null, wrapper);
}
/**
* 获取用户有效路由菜单(树形结构,支持多级菜单)
*
* @param userId 用户ID
* @return 树形路由菜单
*/
public List<RouteMenuVo> getUserRoutes(Long userId) {
// 查询用户有效库存(不限时 或 未过期)
Instant now = Instant.now();
List<UserRouteMenuStock> stocks = userRouteMenuStockMapper.selectList(
new LambdaQueryWrapper<UserRouteMenuStock>()
.eq(UserRouteMenuStock::getUserId, userId)
.and(w -> w
.eq(UserRouteMenuStock::getTimeLimit, 0)
.or()
.gt(UserRouteMenuStock::getExpireTime, now)
)
);
if (stocks == null || stocks.isEmpty()) {
return Collections.emptyList();
}
Set<Long> validRouteIds = stocks.stream()
.map(UserRouteMenuStock::getRouteId)
.collect(Collectors.toSet());
if (validRouteIds.isEmpty()) {
return Collections.emptyList();
}
// 一次查出所有启用的菜单
List<RouteMenu> allMenus = routeMenuMapper.selectList( new LambdaQueryWrapper<RouteMenu>().eq(RouteMenu::getStatus, 1));
if (allMenus.isEmpty()) {
return Collections.emptyList();
}
// 收集有权限的节点及其所有父节点ID
Map<Long, RouteMenu> menuMap = allMenus.stream().collect(Collectors.toMap(RouteMenu::getId, m -> m));
Set<Long> needIds = new HashSet<>(validRouteIds);
for (Long routeId : validRouteIds) {
RouteMenu current = menuMap.get(routeId);
while (current != null && current.getParentId() != 0) {
needIds.add(current.getParentId());
current = menuMap.get(current.getParentId());
}
}
// 转VO
List<RouteMenuVo> voList = allMenus.stream()
.filter(menu -> needIds.contains(menu.getId()))
.map(menu -> {
RouteMenuVo vo = new RouteMenuVo();
BeanUtils.copyProperties(menu, vo);
return vo;
})
.collect(Collectors.toList());
// 构建树形结构
Map<Long, List<RouteMenuVo>> parentMap = voList.stream()
.collect(Collectors.groupingBy(RouteMenuVo::getParentId));
voList.forEach(vo -> {
List<RouteMenuVo> children = parentMap.get(vo.getId());
if (children != null) {
children.sort(Comparator.comparingInt(RouteMenuVo::getSortOrder));
vo.setChildren(children);
}
});
return voList.stream()
.filter(vo -> vo.getParentId() == 0)
.sorted(Comparator.comparingInt(RouteMenuVo::getSortOrder))
.collect(Collectors.toList());
}
}
@@ -0,0 +1,14 @@
package org.jiayunet.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.jiayunet.pojo.po.FuncPermission;
/**
* 功能权限Mapper
*
* @author zk
*/
@Mapper
public interface FuncPermissionMapper extends CommonMapper<FuncPermission> {
}
@@ -0,0 +1,14 @@
package org.jiayunet.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.jiayunet.pojo.po.RouteMenu;
/**
* 路由菜单Mapper
*
* @author zk
*/
@Mapper
public interface RouteMenuMapper extends CommonMapper<RouteMenu> {
}
@@ -0,0 +1,14 @@
package org.jiayunet.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.jiayunet.pojo.po.UserFuncPermissionStock;
/**
* 用户功能权限库存Mapper
*
* @author zk
*/
@Mapper
public interface UserFuncPermissionStockMapper extends CommonMapper<UserFuncPermissionStock> {
}
@@ -0,0 +1,14 @@
package org.jiayunet.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.jiayunet.pojo.po.UserRouteMenuStock;
/**
* 用户路由菜单库存Mapper
*
* @author zk
*/
@Mapper
public interface UserRouteMenuStockMapper extends CommonMapper<UserRouteMenuStock> {
}
@@ -0,0 +1,53 @@
package org.jiayunet.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.Instant;
/**
* 功能权限表
*
* @author zk
*/
@Data
@TableName(value = "bg_func_permission")
public class FuncPermission {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 权限编码,最长12个字符
*/
private String funcCode;
/**
* 功能名称
*/
private String funcName;
/**
* 状态 1=启用 0=禁用
*/
private Integer status;
/**
* 创建时间
*/
private Instant createTime;
/**
* 修改时间
*/
private Instant updateTime;
/**
* 删除标识 0正常 非0删除
*/
@TableLogic(value = "0", delval = "new()")
private Long isDelete;
}
@@ -0,0 +1,78 @@
package org.jiayunet.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.Instant;
/**
* 路由菜单表
*
* @author zk
*/
@Data
@TableName(value = "bg_route_menu")
public class RouteMenu {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 根节点ID
*/
private Long rootId;
/**
* 父级路由ID
*/
private Long parentId;
/**
* 路由名称
*/
private String routeName;
/**
* 前端路径
*/
private String routePath;
/**
* 前端组件路径
*/
private String component;
/**
* 图标
*/
private String icon;
/**
* 排序
*/
private Integer sortOrder;
/**
* 状态 1=启用 0=禁用
*/
private Integer status;
/**
* 创建时间
*/
private Instant createTime;
/**
* 修改时间
*/
private Instant updateTime;
/**
* 删除标识 0正常 非0删除
*/
@TableLogic(value = "0", delval = "new()")
private Long isDelete;
}
@@ -0,0 +1,61 @@
package org.jiayunet.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.Instant;
/**
* 用户功能权限库存表
*
* @author zk
*/
@Data
@TableName(value = "bg_user_func_permission_stock")
public class UserFuncPermissionStock {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 权限编码
*/
private String funcCode;
/**
* 0=不限时 1=限时
*/
private Integer timeLimit;
/**
* 0=不限次 1=限次
*/
private Integer countLimit;
/**
* 过期时间,timeLimit=1时有值
*/
private Instant expireTime;
/**
* 剩余次数,countLimit=1时有值
*/
private Integer remainCount;
/**
* 创建时间
*/
private Instant createTime;
/**
* 修改时间
*/
private Instant updateTime;
}
@@ -0,0 +1,51 @@
package org.jiayunet.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.Instant;
/**
* 用户路由菜单库存表
*
* @author zk
*/
@Data
@TableName(value = "bg_user_route_menu_stock")
public class UserRouteMenuStock {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 关联菜单ID
*/
private Long routeId;
/**
* 0=不限时 1=限时
*/
private Integer timeLimit;
/**
* 过期时间,timeLimit=1时有值
*/
private Instant expireTime;
/**
* 创建时间
*/
private Instant createTime;
/**
* 修改时间
*/
private Instant updateTime;
}