添加权限 按天授权

This commit is contained in:
zk
2026-08-03 12:11:49 +08:00
parent 45dd6efe79
commit 09cf4cd5ed
5 changed files with 160 additions and 4 deletions
@@ -19,4 +19,10 @@ public @interface FuncPermission {
* 功能权限编码,对应 bg_func_permission.func_code * 功能权限编码,对应 bg_func_permission.func_code
*/ */
String value(); String value();
/**
* 业务资源标识表达式,SpEL,从方法参数中取值
* <p>如 #jobId、#dto.jobId,为空表示不做当日幂等,每次调用均扣减</p>
*/
String key() default "";
} }
@@ -4,15 +4,25 @@ import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.jiayunet.annotation.FuncPermission; import org.jiayunet.annotation.FuncPermission;
import org.jiayunet.service.FuncPermissionService; import org.jiayunet.service.FuncPermissionService;
import org.jiayunet.tool.UserSecurityTool; import org.jiayunet.tool.UserSecurityTool;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/** /**
* 功能权限校验切面 * 功能权限校验切面
* <p>拦截 @FuncPermission 注解,校验权限并扣减库存后放行,业务异常时自动回退</p> * <p>拦截 @FuncPermission 注解,当日已解锁的资源直接放行,否则校验权限并扣减库存后放行,业务异常时自动回退</p>
* *
* @author zk * @author zk
*/ */
@@ -21,6 +31,10 @@ import org.springframework.stereotype.Component;
@Slf4j @Slf4j
public class FuncPermissionAspect { public class FuncPermissionAspect {
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
@Autowired @Autowired
private FuncPermissionService funcPermissionService; private FuncPermissionService funcPermissionService;
@@ -29,11 +43,18 @@ public class FuncPermissionAspect {
Long userId = UserSecurityTool.getUserId(); Long userId = UserSecurityTool.getUserId();
String funcCode = funcPermission.value(); String funcCode = funcPermission.value();
String bizKey = resolveBizKey(joinPoint, funcPermission.key());
log.info("功能权限校验 userId:{} funcCode:{}", userId, funcCode); log.info("功能权限校验 userId:{} funcCode:{} bizKey:{}", userId, funcCode, bizKey);
// 当日已解锁该资源,直接放行不再扣减
if (!bizKey.isEmpty() && funcPermissionService.existsTodayUnlock(userId, funcCode, bizKey)) {
log.info("当日已解锁,跳过扣减 userId:{} funcCode:{} bizKey:{}", userId, funcCode, bizKey);
return joinPoint.proceed();
}
// 校验权限 + 扣减库存,返回使用记录ID // 校验权限 + 扣减库存,返回使用记录ID
Long logId = funcPermissionService.checkAndDeduct(userId, funcCode); Long logId = funcPermissionService.checkAndDeduct(userId, funcCode, bizKey);
// 放行,业务异常时回退使用记录和库存 // 放行,业务异常时回退使用记录和库存
try { try {
@@ -44,4 +65,27 @@ public class FuncPermissionAspect {
throw e; throw e;
} }
} }
/**
* 解析业务资源标识
* <p>注解 key 为 SpEL 表达式,从方法参数取值,如 #jobId、#dto.jobId</p>
*
* @param joinPoint 连接点
* @param keyExpression SpEL 表达式
* @return 业务资源标识实际值,表达式为空或求值为 null 时返回空串
*/
private String resolveBizKey(ProceedingJoinPoint joinPoint, String keyExpression) {
if (keyExpression == null || keyExpression.isEmpty()) {
return "";
}
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = AopUtils.getMostSpecificMethod(signature.getMethod(), joinPoint.getTarget().getClass());
EvaluationContext context = new MethodBasedEvaluationContext(
joinPoint.getTarget(), method, joinPoint.getArgs(), PARAMETER_NAME_DISCOVERER);
Object value = EXPRESSION_PARSER.parseExpression(keyExpression).getValue(context);
return value == null ? "" : value.toString();
}
} }
@@ -7,9 +7,11 @@ import org.jiayunet.exception.BusinessException;
import org.jiayunet.exception.BusinessExpCodeEnum; import org.jiayunet.exception.BusinessExpCodeEnum;
import org.jiayunet.mapper.FuncPermissionMapper; import org.jiayunet.mapper.FuncPermissionMapper;
import org.jiayunet.mapper.UserFuncPermissionStockMapper; import org.jiayunet.mapper.UserFuncPermissionStockMapper;
import org.jiayunet.mapper.UserFuncUnlockMapper;
import org.jiayunet.mapper.UserFuncUsageLogMapper; import org.jiayunet.mapper.UserFuncUsageLogMapper;
import org.jiayunet.pojo.po.FuncPermission; import org.jiayunet.pojo.po.FuncPermission;
import org.jiayunet.pojo.po.UserFuncPermissionStock; import org.jiayunet.pojo.po.UserFuncPermissionStock;
import org.jiayunet.pojo.po.UserFuncUnlock;
import org.jiayunet.pojo.po.UserFuncUsageLog; import org.jiayunet.pojo.po.UserFuncUsageLog;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -38,15 +40,34 @@ public class FuncPermissionService {
@Autowired @Autowired
private UserFuncUsageLogMapper userFuncUsageLogMapper; private UserFuncUsageLogMapper userFuncUsageLogMapper;
@Autowired
private UserFuncUnlockMapper userFuncUnlockMapper;
/**
* 检查当日是否已存在该业务资源的授权记录
* <p>存在说明用户当日已解锁该资源,可直接放行不再扣减</p>
*
* @param userId 用户ID
* @param funcCode 功能权限编码
* @param bizKey 业务资源标识实际值
* @return true=当日已授权
*/
public boolean existsTodayUnlock(Long userId, String funcCode, String bizKey) {
return userFuncUnlockMapper.exists(new LambdaQueryWrapper<UserFuncUnlock>().eq(UserFuncUnlock::getUserId, userId).eq(UserFuncUnlock::getFuncCode, funcCode).eq(UserFuncUnlock::getUnlockDate, LocalDate.now()).eq(UserFuncUnlock::getBizKey, bizKey));
}
/** /**
* 校验用户功能权限并扣减库存 * 校验用户功能权限并扣减库存
* <p>1. 查功能权限定义 2. 判断每日免费额度是否充足 3. 免费额度用完则查付费库存 4. 校验时间维度 5. 校验次数维度并原子扣减 6. 插入使用记录</p> * <p>1. 查功能权限定义 2. 判断每日免费额度是否充足 3. 免费额度用完则查付费库存 4. 校验时间维度 5. 校验次数维度并原子扣减 6. 插入使用记录</p>
* *
* @param userId 用户ID * @param userId 用户ID
* @param funcCode 功能权限编码 * @param funcCode 功能权限编码
* @param bizKey 业务资源标识实际值,为空表示不做当日幂等
* @return 使用记录ID(用于异常回退) * @return 使用记录ID(用于异常回退)
*/ */
public Long checkAndDeduct(Long userId, String funcCode) { public Long checkAndDeduct(Long userId, String funcCode, String bizKey) {
// 1. 查功能权限定义 // 1. 查功能权限定义
FuncPermission funcPermission = funcPermissionMapper.selectOne( FuncPermission funcPermission = funcPermissionMapper.selectOne(
new LambdaQueryWrapper<FuncPermission>() new LambdaQueryWrapper<FuncPermission>()
@@ -69,6 +90,7 @@ public class FuncPermissionService {
); );
if (todayUsed < dailyFreeCount) { if (todayUsed < dailyFreeCount) {
// 免费额度未用完,插入使用记录,直接放行 // 免费额度未用完,插入使用记录,直接放行
insertUnlock(userId, funcCode, bizKey);
return insertUsageLog(userId, funcCode); return insertUsageLog(userId, funcCode);
} }
} }
@@ -91,6 +113,7 @@ public class FuncPermissionService {
// 5. 次数维度校验 // 5. 次数维度校验
if (stock.getCountLimit() == 0) { if (stock.getCountLimit() == 0) {
// 不限次,直接放行 // 不限次,直接放行
insertUnlock(userId, funcCode, bizKey);
return insertUsageLog(userId, funcCode); return insertUsageLog(userId, funcCode);
} }
@@ -106,9 +129,30 @@ public class FuncPermissionService {
throw new BusinessException(BusinessExpCodeEnum.PERMISSION_DENIED, "功能使用次数已用完"); throw new BusinessException(BusinessExpCodeEnum.PERMISSION_DENIED, "功能使用次数已用完");
} }
insertUnlock(userId, funcCode, bizKey);
return insertUsageLog(userId, funcCode); return insertUsageLog(userId, funcCode);
} }
/**
* 插入当日解锁记录
* <p>bizKey 为空表示不做当日幂等,跳过插入</p>
*
* @param userId 用户ID
* @param funcCode 功能编码
* @param bizKey 业务资源标识实际值
*/
private void insertUnlock(Long userId, String funcCode, String bizKey) {
if (bizKey == null || bizKey.isEmpty()) {
return;
}
UserFuncUnlock unlock = new UserFuncUnlock();
unlock.setUserId(userId);
unlock.setFuncCode(funcCode);
unlock.setUnlockDate(LocalDate.now());
unlock.setBizKey(bizKey);
userFuncUnlockMapper.insert(unlock);
}
/** /**
* 插入使用记录 * 插入使用记录
* *
@@ -0,0 +1,14 @@
package org.jiayunet.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.jiayunet.pojo.po.UserFuncUnlock;
/**
* 用户功能当日解锁记录Mapper
*
* @author zk
*/
@Mapper
public interface UserFuncUnlockMapper extends CommonMapper<UserFuncUnlock> {
}
@@ -0,0 +1,48 @@
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;
import java.time.LocalDate;
/**
* 用户功能当日解锁记录表
* <p>同一用户当日对同一业务资源重复访问时不再扣减次数</p>
*
* @author zk
*/
@Data
@TableName(value = "bg_user_func_unlock")
public class UserFuncUnlock {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 功能编码
*/
private String funcCode;
/**
* 解锁日期
*/
private LocalDate unlockDate;
/**
* 业务资源标识,如岗位ID
*/
private String bizKey;
/**
* 创建时间
*/
private Instant createTime;
}