diff --git a/client-api/src/main/java/org/jiayunet/annotation/FuncPermission.java b/client-api/src/main/java/org/jiayunet/annotation/FuncPermission.java index 7aeb865..1830393 100644 --- a/client-api/src/main/java/org/jiayunet/annotation/FuncPermission.java +++ b/client-api/src/main/java/org/jiayunet/annotation/FuncPermission.java @@ -19,4 +19,10 @@ public @interface FuncPermission { * 功能权限编码,对应 bg_func_permission.func_code */ String value(); + + /** + * 业务资源标识表达式,SpEL,从方法参数中取值 + *

如 #jobId、#dto.jobId,为空表示不做当日幂等,每次调用均扣减

+ */ + String key() default ""; } diff --git a/client-api/src/main/java/org/jiayunet/aop/FuncPermissionAspect.java b/client-api/src/main/java/org/jiayunet/aop/FuncPermissionAspect.java index 8117526..2ea751f 100644 --- a/client-api/src/main/java/org/jiayunet/aop/FuncPermissionAspect.java +++ b/client-api/src/main/java/org/jiayunet/aop/FuncPermissionAspect.java @@ -4,15 +4,25 @@ import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; import org.jiayunet.annotation.FuncPermission; import org.jiayunet.service.FuncPermissionService; import org.jiayunet.tool.UserSecurityTool; +import org.springframework.aop.support.AopUtils; 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 java.lang.reflect.Method; + /** * 功能权限校验切面 - *

拦截 @FuncPermission 注解,校验权限并扣减库存后放行,业务异常时自动回退

+ *

拦截 @FuncPermission 注解,当日已解锁的资源直接放行,否则校验权限并扣减库存后放行,业务异常时自动回退

* * @author zk */ @@ -21,6 +31,10 @@ import org.springframework.stereotype.Component; @Slf4j public class FuncPermissionAspect { + private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + @Autowired private FuncPermissionService funcPermissionService; @@ -29,11 +43,18 @@ public class FuncPermissionAspect { Long userId = UserSecurityTool.getUserId(); 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 - Long logId = funcPermissionService.checkAndDeduct(userId, funcCode); + Long logId = funcPermissionService.checkAndDeduct(userId, funcCode, bizKey); // 放行,业务异常时回退使用记录和库存 try { @@ -44,4 +65,27 @@ public class FuncPermissionAspect { throw e; } } + + /** + * 解析业务资源标识 + *

注解 key 为 SpEL 表达式,从方法参数取值,如 #jobId、#dto.jobId

+ * + * @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(); + } } diff --git a/client-api/src/main/java/org/jiayunet/service/FuncPermissionService.java b/client-api/src/main/java/org/jiayunet/service/FuncPermissionService.java index 2ea3fe9..fa21d3b 100644 --- a/client-api/src/main/java/org/jiayunet/service/FuncPermissionService.java +++ b/client-api/src/main/java/org/jiayunet/service/FuncPermissionService.java @@ -7,9 +7,11 @@ import org.jiayunet.exception.BusinessException; import org.jiayunet.exception.BusinessExpCodeEnum; import org.jiayunet.mapper.FuncPermissionMapper; import org.jiayunet.mapper.UserFuncPermissionStockMapper; +import org.jiayunet.mapper.UserFuncUnlockMapper; import org.jiayunet.mapper.UserFuncUsageLogMapper; import org.jiayunet.pojo.po.FuncPermission; import org.jiayunet.pojo.po.UserFuncPermissionStock; +import org.jiayunet.pojo.po.UserFuncUnlock; import org.jiayunet.pojo.po.UserFuncUsageLog; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -38,15 +40,34 @@ public class FuncPermissionService { @Autowired private UserFuncUsageLogMapper userFuncUsageLogMapper; + @Autowired + private UserFuncUnlockMapper userFuncUnlockMapper; + + /** + * 检查当日是否已存在该业务资源的授权记录 + *

存在说明用户当日已解锁该资源,可直接放行不再扣减

+ * + * @param userId 用户ID + * @param funcCode 功能权限编码 + * @param bizKey 业务资源标识实际值 + * @return true=当日已授权 + */ + public boolean existsTodayUnlock(Long userId, String funcCode, String bizKey) { + + return userFuncUnlockMapper.exists(new LambdaQueryWrapper().eq(UserFuncUnlock::getUserId, userId).eq(UserFuncUnlock::getFuncCode, funcCode).eq(UserFuncUnlock::getUnlockDate, LocalDate.now()).eq(UserFuncUnlock::getBizKey, bizKey)); + + } + /** * 校验用户功能权限并扣减库存 *

1. 查功能权限定义 2. 判断每日免费额度是否充足 3. 免费额度用完则查付费库存 4. 校验时间维度 5. 校验次数维度并原子扣减 6. 插入使用记录

* * @param userId 用户ID * @param funcCode 功能权限编码 + * @param bizKey 业务资源标识实际值,为空表示不做当日幂等 * @return 使用记录ID(用于异常回退) */ - public Long checkAndDeduct(Long userId, String funcCode) { + public Long checkAndDeduct(Long userId, String funcCode, String bizKey) { // 1. 查功能权限定义 FuncPermission funcPermission = funcPermissionMapper.selectOne( new LambdaQueryWrapper() @@ -69,6 +90,7 @@ public class FuncPermissionService { ); if (todayUsed < dailyFreeCount) { // 免费额度未用完,插入使用记录,直接放行 + insertUnlock(userId, funcCode, bizKey); return insertUsageLog(userId, funcCode); } } @@ -91,6 +113,7 @@ public class FuncPermissionService { // 5. 次数维度校验 if (stock.getCountLimit() == 0) { // 不限次,直接放行 + insertUnlock(userId, funcCode, bizKey); return insertUsageLog(userId, funcCode); } @@ -106,9 +129,30 @@ public class FuncPermissionService { throw new BusinessException(BusinessExpCodeEnum.PERMISSION_DENIED, "功能使用次数已用完"); } + insertUnlock(userId, funcCode, bizKey); return insertUsageLog(userId, funcCode); } + /** + * 插入当日解锁记录 + *

bizKey 为空表示不做当日幂等,跳过插入

+ * + * @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); + } + /** * 插入使用记录 * diff --git a/manager/src/main/java/org/jiayunet/mapper/UserFuncUnlockMapper.java b/manager/src/main/java/org/jiayunet/mapper/UserFuncUnlockMapper.java new file mode 100644 index 0000000..de75b17 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/mapper/UserFuncUnlockMapper.java @@ -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 { + +} diff --git a/manager/src/main/java/org/jiayunet/pojo/po/UserFuncUnlock.java b/manager/src/main/java/org/jiayunet/pojo/po/UserFuncUnlock.java new file mode 100644 index 0000000..c02231a --- /dev/null +++ b/manager/src/main/java/org/jiayunet/pojo/po/UserFuncUnlock.java @@ -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; + +/** + * 用户功能当日解锁记录表 + *

同一用户当日对同一业务资源重复访问时不再扣减次数

+ * + * @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; +}