Merge branch 'develop'

This commit is contained in:
haoxr 2021-07-08 00:36:06 +08:00
commit ec158e84a2
49 changed files with 1432 additions and 184 deletions

Binary file not shown.

View File

@ -0,0 +1,95 @@
package com.youlai.mall.sms.pojo.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.youlai.mall.sms.pojo.enums.CouponCategoryEnum;
import com.youlai.mall.sms.pojo.enums.DistributeTargetEnum;
import com.youlai.mall.sms.pojo.enums.ProductLineEnum;
import com.youlai.mall.sms.pojo.vo.TemplateRuleVO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* @author xinyi
* @desc: 优惠券模板实体类基础属性 + 规则属性
* @date 2021/6/26
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SmsCouponTemplate {
/**
* 主键自增ID
*/
@TableId
private Long id;
/**
* 是否是可用状态
*/
private Integer available;
/**
* 是否过期
*/
private Integer expired;
/**
* 优惠券名称
*/
private String name;
/**
* 优惠券logo
*/
private String logo;
/**
* 优惠券描述
*/
private String description;
/**
* 优惠券分类
*/
private CouponCategoryEnum category;
/**
* 产品线
*/
private ProductLineEnum productLine;
/**
* 总数
*/
private Integer total;
/**
* 优惠券模板编码
*/
private String code;
/**
* 目标用户
*/
private DistributeTargetEnum target;
/**
* 优惠券规则
*/
private TemplateRuleVO rule;
private Date gmtCreate;
private String gmtCreatedBy;
private Date gmtModified;
private String gmtModifiedBy;
}

View File

@ -41,15 +41,14 @@ public class SmsSeckillSession implements Serializable {
/**
* 创建时间
*/
private Date createTime;
private Date gmtCreate;
/**
* 修改时间
*/
private Date updateTime;
private Date gmtModified;
@TableField(exist = false)
private List<SmsSeckillSkuRelation> relations;
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,59 @@
package com.youlai.mall.sms.pojo.dto;
import com.youlai.mall.sms.pojo.vo.TemplateRuleVO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author xinyi
* @desc微服务之间调用的优惠券模板信息
* @date 2021/6/27
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CouponTemplateDTO {
/**
* 优惠券模板主键ID
*/
private Long id;
/**
* 优惠券模板名称
*/
private String name;
/**
* 描述
*/
private String desc;
/**
* 分类
*/
private String category;
/**
* 产品线
*/
private String productLine;
/**
* 优惠券模板编码
*/
private String code;
/**
* 目标用户
*/
private Integer target;
/**
* 优惠券规则
*/
private TemplateRuleVO rule;
}

View File

@ -0,0 +1,43 @@
package com.youlai.mall.sms.pojo.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
import java.util.stream.Stream;
/**
* @author xinyi
* @desc: 优惠券分类
* @date 2021/6/26
*/
@Getter
@AllArgsConstructor
public enum CouponCategoryEnum {
MANJIAN("满减券", "001"),
ZHEKOU("折扣券", "002"),
LIJIAN("立减券", "003"),
;
/**
* 优惠券描述分类
*/
@JsonValue
private String desc;
/**
* 优惠券分类编码
*/
@EnumValue
private String code;
public static CouponCategoryEnum of(String code) {
Objects.requireNonNull(code);
return Stream.of(values()).filter(bean -> bean.code.equals(code))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(code + " code not exist"));
}
}

View File

@ -0,0 +1,42 @@
package com.youlai.mall.sms.pojo.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
import java.util.stream.Stream;
/**
* @author xinyi
* @desc优惠券分发目标枚举
* @date 2021/6/26
*/
@Getter
@AllArgsConstructor
public enum DistributeTargetEnum {
SINGLE("单用户",1),
MULTI("多用户",2),
;
/**
* 分发目标描述
*/
@JsonValue
private String desc;
/**
* 分发目标编码
*/
@EnumValue
private Integer code;
public static DistributeTargetEnum of(Integer code) {
Objects.requireNonNull(code);
return Stream.of(values()).filter(bean -> bean.code.equals(code))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(code + " code not exist"));
}
}

View File

@ -0,0 +1,43 @@
package com.youlai.mall.sms.pojo.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
import java.util.stream.Stream;
/**
* @author xinyi
* @desc有效期类型枚举
* @date 2021/6/26
*/
@Getter
@AllArgsConstructor
public enum PeriodTypeEnum {
REGULAR("固定的(固定日期)",1),
SHIFT("变动的(以领取之日开始计算)",2),
;
/**
* 有效期描述
*/
@JsonValue
private String desc;
/**
* 有效期编码
*/
@EnumValue
private Integer code;
public static PeriodTypeEnum of(Integer code) {
Objects.requireNonNull(code);
return Stream.of(values()).filter(bean -> bean.code.equals(code))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(code + " code not exist"));
}
}

View File

@ -0,0 +1,43 @@
package com.youlai.mall.sms.pojo.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
import java.util.stream.Stream;
/**
* @author xinyi
* @desc: 产品线枚举
* @date 2021/6/26
*/
@Getter
@AllArgsConstructor
public enum ProductLineEnum {
YOULAI("有来", 1),
WUHUI("无回", 2),
;
/**
* 产品线描述
*/
@JsonValue
private String desc;
/**
* 产品线编码
*/
@EnumValue
private Integer code;
public static ProductLineEnum of(String code) {
Objects.requireNonNull(code);
return Stream.of(values()).filter(bean -> bean.code.equals(code))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(code + " code not exist"));
}
}

View File

@ -0,0 +1,73 @@
package com.youlai.mall.sms.pojo.form;
import com.youlai.mall.sms.pojo.vo.TemplateRuleVO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* @author xinyi
* @desc: 优惠券模板创建请求对象
* @date 2021/6/26
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CouponTemplateForm {
private Long id;
/**
* 优惠券模板名称
*/
@NotBlank(message = "请填写优惠券模板名称")
private String name;
/**
* 优惠券 logo
*/
private String logo;
/**
* 优惠券描述
*/
@NotBlank(message = "请填写优惠券描述")
private String desc;
/**
* 优惠券分类
*/
@NotBlank(message = "请选择优惠券分类")
private String category;
/**
* 优惠券产品线
*/
@NotBlank(message = "请选择优惠券产品线")
private Integer productLine;
/**
* 优惠券总数量
*/
@NotNull(message = "请输入优惠券总数量")
@Min(value = 1, message = "优惠券数量必须大于1")
@Max(value = Integer.MAX_VALUE, message = "优惠券数量不能大于 " + Integer.MAX_VALUE)
private Integer total;
/**
* 目标用户
*/
private Integer target;
/**
* 优惠券规则
*/
private TemplateRuleVO rule;
}

View File

@ -0,0 +1,83 @@
package com.youlai.mall.sms.pojo.vo;
import com.youlai.common.base.BaseVO;
import com.youlai.mall.sms.pojo.enums.CouponCategoryEnum;
import com.youlai.mall.sms.pojo.enums.DistributeTargetEnum;
import com.youlai.mall.sms.pojo.enums.ProductLineEnum;
import io.swagger.annotations.ApiModel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author xinyi
* @desc: 优惠券模板实体类
* @date 2021/7/3
*/
@ApiModel(value = "优惠券模板模型",description = "优惠券模板模型")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CouponTemplateVO extends BaseVO {
/**
* 主键自增ID
*/
private Long id;
/**
* 是否是可用状态
*/
private Integer available;
/**
* 是否过期
*/
private Integer expired;
/**
* 优惠券名称
*/
private String name;
/**
* 优惠券logo
*/
private String logo;
/**
* 优惠券描述
*/
private String intro;
/**
* 优惠券分类
*/
private CouponCategoryEnum category;
/**
* 产品线
*/
private ProductLineEnum productLine;
/**
* 总数
*/
private Integer total;
/**
* 优惠券模板编码
*/
private String code;
/**
* 目标用户
*/
private DistributeTargetEnum target;
/**
* 优惠券规则
*/
private TemplateRuleVO rule;
}

View File

@ -0,0 +1,126 @@
package com.youlai.mall.sms.pojo.vo;
import com.youlai.mall.sms.pojo.enums.PeriodTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
/**
* @author xinyi
* @desc: 优惠券规则对象定义
* @date 2021/6/26
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TemplateRuleVO {
/**
* 优惠券过期规则
*/
private Expiration expiration;
/**
* 折扣
*/
private Discount discount;
/**
* 限领张数限制
*/
private Integer limitation;
/**
* 使用范围地域 + 商品类型
*/
private Usage usage;
/**
* 权重可以和哪些优惠券叠加使用同一类的优惠券一定不能叠加
*/
private String weight;
/**
* 有效期规则定义
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Expiration {
/**
* 有效期规则对应 PeriodType code 字段
*/
private Integer period;
/**
* 有效间隔只对变动性有效期有效
*/
private Integer gap;
/**
* 优惠券模板的失效日期两类规则都有效
*/
private Long deadline;
boolean validate() {
// 最简化校验
return null != PeriodTypeEnum.of(period) && gap > 0;
}
}
/**
* 折扣的规则
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Discount {
/**
* 额度满减20折扣85立减10
*/
private Integer quota;
/**
* 基准需要满多少才可用
*/
private Integer base;
boolean validate() {
// 最简化校验
return quota > 0 && base > 0;
}
}
/**
* 优惠券使用范围
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Usage {
/**
* 省份
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 商品类型list[文娱生鲜家居全品类]
*/
private String goodsType;
boolean validate() {
return StringUtils.isNotEmpty(province)
&& StringUtils.isNotEmpty(province)
&& StringUtils.isNotEmpty(province);
}
}
}

View File

@ -4,6 +4,7 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 优惠营销系统
@ -16,6 +17,7 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableScheduling
public class SmsApplication {
public static void main(String[] args) {

View File

@ -0,0 +1,63 @@
package com.youlai.mall.sms.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author xinyi
* @desc: 自定义异步线程池
* @date 2021/6/27
*/
@Slf4j
@EnableAsync
@Configuration
public class AsyncPoolConfig implements AsyncConfigurer {
@Bean
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(20);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("youlai_async_");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
@SuppressWarnings("all")
class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
// 1打印异常堆栈
throwable.printStackTrace();
// 2日志记录错误信息
log.error("AsyncError:{}, Method:{}, Param:{}", throwable.getMessage(), method.getName(), Arrays.asList(objects));
// 3TODO 发生异常后通知管理人员邮件短信进一步处理
}
}
}

View File

@ -0,0 +1,67 @@
package com.youlai.mall.sms.controller.admin;
import com.youlai.common.result.Result;
import com.youlai.mall.sms.pojo.form.CouponTemplateForm;
import com.youlai.mall.sms.pojo.vo.CouponTemplateVO;
import com.youlai.mall.sms.service.ISmsCouponTemplateService;
import com.youlai.mall.sms.service.ITemplateBaseService;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author xinyi
* @desc优惠券模板API接口
* @date 2021/6/26
*/
@Slf4j
@Api(tags = "优惠券模板API接口")
@RestController
@RequestMapping("/api/v1/coupon_template")
public class CouponTemplateController {
@Autowired
private ITemplateBaseService templateBaseService;
@Autowired
private ISmsCouponTemplateService couponTemplateService;
/**
* 创建优惠券模板
*
* @param form 提交表单
* @return result
*/
@PostMapping("/template")
public Result<Object> createTemplate(@RequestBody CouponTemplateForm form) {
log.info("Create Coupon Template , form:{}", form);
couponTemplateService.createTemplate(form);
return Result.success();
}
/**
* 修改优惠券模板
* @param form 提交表单
* @return result
*/
@PutMapping("/template")
public Result<Object> updateTemplate(@RequestBody CouponTemplateForm form){
log.info("Update Coupon Templateform:{}",form);
couponTemplateService.updateTemplate(form);
return Result.success();
}
/**
* 查询优惠券模板详情
* @param id 优惠券模板ID
* @return result
*/
@GetMapping("/template/info")
public Result<CouponTemplateVO> getTemplateInfo(@RequestParam("id") Long id){
log.info("Query Coupon Template Info , id:{}",id);
CouponTemplateVO templateVO = templateBaseService.queryTemplateInfo(id);
return Result.success(templateVO);
}
}

View File

@ -6,7 +6,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface SmsCouponDao extends BaseMapper<SmsCoupon> {
public interface SmsCouponMapper extends BaseMapper<SmsCoupon> {
int deleteByPrimaryKey(Long id);
int insertSelective(SmsCoupon record);

View File

@ -5,7 +5,7 @@ import com.youlai.mall.sms.pojo.domain.SmsCouponRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SmsCouponRecordDao extends BaseMapper<SmsCouponRecord> {
public interface SmsCouponRecordMapper extends BaseMapper<SmsCouponRecord> {
int deleteByPrimaryKey(Long id);
int insertSelective(SmsCouponRecord record);

View File

@ -0,0 +1,14 @@
package com.youlai.mall.sms.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.mall.sms.pojo.domain.SmsCouponTemplate;
import org.apache.ibatis.annotations.Mapper;
/**
* @author xinyi
* @desc优惠券模板Dao接口定义
* @date 2021/6/26
*/
@Mapper
public interface SmsCouponTemplateMapper extends BaseMapper<SmsCouponTemplate> {
}

View File

@ -0,0 +1,17 @@
package com.youlai.mall.sms.service;
import com.youlai.mall.sms.pojo.domain.SmsCouponTemplate;
/**
* @author xinyi
* @desc异步服务接口
* @date 2021/6/27
*/
public interface IAsyncService {
/**
* 通过优惠券模板异步的创建优惠券码
* @param template {@link SmsCouponTemplate} 优惠券模板实体
*/
void asyncConstructCouponByTemplate(SmsCouponTemplate template);
}

View File

@ -0,0 +1,49 @@
package com.youlai.mall.sms.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.mall.sms.pojo.domain.SmsCouponTemplate;
import com.youlai.mall.sms.pojo.form.CouponTemplateForm;
import java.util.List;
/**
* @author xinyi
* @desc: 优惠券模板业务接口
* @date 2021/6/26
*/
public interface ISmsCouponTemplateService extends IService<SmsCouponTemplate> {
/**
* 创建优惠券模板
*
* @param form {@link CouponTemplateForm} 模板信息请求对象
* @return {@link SmsCouponTemplate} 优惠券模板实体类
*/
SmsCouponTemplate createTemplate(CouponTemplateForm form);
/**
* 修改优惠券模板
* @param form {@link CouponTemplateForm} 模板信息请求对象
* @return {@link SmsCouponTemplate} 优惠券模板实体类
*/
SmsCouponTemplate updateTemplate(CouponTemplateForm form);
/**
* 查询所有可用优惠券模板列表
*
* @param available 是否可用
* @param expired 是否过期
* @return 优惠券模板
*/
List<SmsCouponTemplate> findAllUsableTemplate(boolean available, boolean expired);
/**
* 查询未过期的优惠券模板列表
*
* @param expired 是否过期
* @return 优惠券模板列表
*/
List<SmsCouponTemplate> findAllNotExpiredTemplate(Integer expired);
}

View File

@ -0,0 +1,38 @@
package com.youlai.mall.sms.service;
import com.youlai.mall.sms.pojo.domain.SmsCouponTemplate;
import com.youlai.mall.sms.pojo.vo.CouponTemplateVO;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* @author xinyi
* @desc: 优惠券模板基础业务接口
* @date 2021/7/3
*/
public interface ITemplateBaseService {
/**
* 根据优惠券模板id 获取优惠券模板信息
* @param id 优惠券模板ID
* @return {@link SmsCouponTemplate} 优惠券模板实体类
*/
CouponTemplateVO queryTemplateInfo(Long id);
/**
* 查询所有可用优惠券模板列表
* @return 优惠券模板列表
*/
List<SmsCouponTemplate> findAllUsableTemplate();
/**
* 根据优惠券模板ID集合查询可用模板列表
* @param ids 优惠券模板ID集合
* @return 优惠券模板列表
*/
Map<Long, SmsCouponTemplate> findAllTemplateByIds(Collection<Long> ids);
}

View File

@ -0,0 +1,100 @@
package com.youlai.mall.sms.service.impl;
import cn.hutool.core.date.format.FastDateFormat;
import com.google.common.base.Stopwatch;
import com.youlai.common.constant.RedisConstants;
import com.youlai.common.redis.utils.RedisUtils;
import com.youlai.mall.sms.mapper.SmsCouponTemplateMapper;
import com.youlai.mall.sms.pojo.domain.SmsCouponTemplate;
import com.youlai.mall.sms.service.IAsyncService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @author xinyi
* @desc异步服务接口实现
* @date 2021/6/27
*/
@Slf4j
@Service
@AllArgsConstructor
public class AsyncServiceImpl implements IAsyncService {
private final RedisUtils redisUtils;
private final SmsCouponTemplateMapper couponTemplateMapper;
@Async("getAsyncExecutor")
@Override
public void asyncConstructCouponByTemplate(SmsCouponTemplate template) {
Stopwatch watch = Stopwatch.createStarted();
Set<String> couponCodes = buildCouponCode(template);
String couponCodeKey = String.format("%s%s", RedisConstants.SMS_COUPON_TEMPLATE_CODE_KEY, String.valueOf(template.getId()));
redisUtils.lSet(couponCodeKey, couponCodes);
log.info("Push CouponCode To Redis, Coupon Template " +
"ID:{}, Name:{}, TOTAL:{}", template.getId(), template.getName(), template.getTotal());
template.setAvailable(1);
couponTemplateMapper.updateById(template);
watch.stop();
log.info("Construct CouponCode By Coupon Template Use:{}ms", watch.elapsed(TimeUnit.MILLISECONDS));
}
/**
* 构造优惠券码
* 优惠券码对应于每一张优惠券一共18位
* 前四位产品线+类型
* 中间六位日期随机
* 后八位0 ~ 9 随机数构成
*
* @param template {@link SmsCouponTemplate} 优惠券模板实体类
* @return Set<String> template.total 总优惠券数量
*/
private Set<String> buildCouponCode(SmsCouponTemplate template) {
Stopwatch watch = Stopwatch.createStarted();
Set<String> result = new HashSet<>(template.getTotal());
String prefix4 = template.getProductLine().getCode() + template.getCategory().getCode();
String date = FastDateFormat.getInstance("yyMMdd").format(new Date());
for (Integer i = 0; i < template.getTotal(); i++) {
result.add(buildCouponCodeSuffix14(date));
}
while (result.size() < template.getTotal()) {
result.add(prefix4 + buildCouponCodeSuffix14(date));
}
// 断言
assert result.size() == template.getTotal();
watch.stop();
log.info("Build Coupon Code use:{}ms", watch.elapsed(TimeUnit.MILLISECONDS));
return result;
}
/**
* 构造优惠券码后十四位
*
* @param date
* @return
*/
private String buildCouponCodeSuffix14(String date) {
char[] bases = new char[]{'1', '2', '3', '4', '5', '6', '7', '8', '9'};
// 中间六位
List<Character> chars = date.chars().mapToObj(obj -> (char) obj).collect(Collectors.toList());
Collections.shuffle(chars);
String mid6 = chars.stream().map(Objects::toString).collect(Collectors.joining());
// 后八位
String suffix8 = RandomStringUtils.random(1, bases) + RandomStringUtils.randomNumeric(7);
return mid6 + suffix8;
}
}

View File

@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.web.exception.BizException;
import com.youlai.common.web.util.JwtUtils;
import com.youlai.mall.sms.mapper.SmsCouponRecordDao;
import com.youlai.mall.sms.mapper.SmsCouponRecordMapper;
import com.youlai.mall.sms.pojo.domain.SmsCoupon;
import com.youlai.mall.sms.pojo.domain.SmsCouponRecord;
import com.youlai.mall.sms.pojo.enums.CouponStateEnum;
@ -30,7 +30,7 @@ import static com.youlai.mall.sms.pojo.constant.AppConstants.COUPON_LOCK;
*/
@Service
@Slf4j
public class CouponRecordServiceImpl extends ServiceImpl<SmsCouponRecordDao, SmsCouponRecord> implements ICouponRecordService {
public class CouponRecordServiceImpl extends ServiceImpl<SmsCouponRecordMapper, SmsCouponRecord> implements ICouponRecordService {
@Autowired
private ISmsCouponService couponService;

View File

@ -1,7 +1,7 @@
package com.youlai.mall.sms.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.mall.sms.mapper.SmsCouponRecordDao;
import com.youlai.mall.sms.mapper.SmsCouponRecordMapper;
import com.youlai.mall.sms.pojo.domain.SmsCouponRecord;
import com.youlai.mall.sms.service.ISmsCouponRecordService;
import lombok.extern.slf4j.Slf4j;
@ -15,5 +15,5 @@ import org.springframework.stereotype.Service;
*/
@Slf4j
@Service
public class SmsCouponRecordServiceImpl extends ServiceImpl<SmsCouponRecordDao, SmsCouponRecord> implements ISmsCouponRecordService {
public class SmsCouponRecordServiceImpl extends ServiceImpl<SmsCouponRecordMapper, SmsCouponRecord> implements ISmsCouponRecordService {
}

View File

@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.base.BasePageQuery;
import com.youlai.common.web.util.BeanMapperUtils;
import com.youlai.mall.sms.mapper.SmsCouponDao;
import com.youlai.mall.sms.mapper.SmsCouponMapper;
import com.youlai.mall.sms.pojo.domain.SmsCoupon;
import com.youlai.mall.sms.pojo.form.CouponForm;
import com.youlai.mall.sms.pojo.vo.SmsCouponVO;
@ -22,7 +22,7 @@ import org.springframework.stereotype.Service;
*/
@Service
@Slf4j
public class SmsCouponServiceImpl extends ServiceImpl<SmsCouponDao, SmsCoupon> implements ISmsCouponService {
public class SmsCouponServiceImpl extends ServiceImpl<SmsCouponMapper, SmsCoupon> implements ISmsCouponService {
@Override
public SmsCouponVO detail(String couponId) {
log.info("根据优惠券ID获取优惠券详情couponId={}", couponId);

View File

@ -0,0 +1,95 @@
package com.youlai.mall.sms.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.common.web.exception.BizException;
import com.youlai.mall.sms.mapper.SmsCouponTemplateMapper;
import com.youlai.mall.sms.pojo.domain.SmsCouponTemplate;
import com.youlai.mall.sms.pojo.enums.CouponCategoryEnum;
import com.youlai.mall.sms.pojo.enums.DistributeTargetEnum;
import com.youlai.mall.sms.pojo.form.CouponTemplateForm;
import com.youlai.mall.sms.service.IAsyncService;
import com.youlai.mall.sms.service.ISmsCouponTemplateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author xinyi
* @desc: 优惠券模板业务实现类
* @date 2021/6/26
*/
@Slf4j
@Service
public class SmsCouponTemplateServiceImpl extends ServiceImpl<SmsCouponTemplateMapper, SmsCouponTemplate>
implements ISmsCouponTemplateService {
@Autowired
private IAsyncService asyncService;
@Override
public SmsCouponTemplate createTemplate(CouponTemplateForm form) {
// 1form 表单参数校验
// 2不允许出现同名的模板
if (null == findByCouponTemplateName(form.getName())) {
throw new BizException("Coupon Template Name Exist");
}
// 构造 CouponTemplate 并保存到数据库
SmsCouponTemplate template = formToTemplate(form);
this.save(template);
// 根据优惠券模板异步生成优惠券码
asyncService.asyncConstructCouponByTemplate(template);
return template;
}
@Override
public SmsCouponTemplate updateTemplate(CouponTemplateForm form) {
// TODO 开发中
return null;
}
@Override
public List<SmsCouponTemplate> findAllUsableTemplate(boolean available, boolean expired) {
QueryWrapper<SmsCouponTemplate> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("available", available)
.eq("expired", expired);
return this.list(queryWrapper);
}
@Override
public List<SmsCouponTemplate> findAllNotExpiredTemplate(Integer expired) {
QueryWrapper<SmsCouponTemplate> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("expired", expired);
return this.list(queryWrapper);
}
/**
* 根据模板名称查询实体类
*
* @param name 模板名称
* @return 模板实体类
*/
private SmsCouponTemplate findByCouponTemplateName(String name) {
QueryWrapper<SmsCouponTemplate> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", name);
return getOne(queryWrapper);
}
private SmsCouponTemplate formToTemplate(CouponTemplateForm form) {
SmsCouponTemplate template = new SmsCouponTemplate();
template.setName(form.getName());
template.setLogo(form.getLogo());
template.setCategory(CouponCategoryEnum.of(form.getCategory()));
template.setTotal(form.getTotal());
template.setTarget(DistributeTargetEnum.of(form.getTarget()));
template.setRule(form.getRule());
template.setCode("");
return template;
}
}

View File

@ -0,0 +1,54 @@
package com.youlai.mall.sms.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import com.youlai.common.web.exception.BizException;
import com.youlai.common.web.util.BeanMapperUtils;
import com.youlai.mall.sms.pojo.domain.SmsCouponTemplate;
import com.youlai.mall.sms.pojo.vo.CouponTemplateVO;
import com.youlai.mall.sms.service.ISmsCouponTemplateService;
import com.youlai.mall.sms.service.ITemplateBaseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author xinyi
* @desc优惠券模板基础业务实现类
* @date 2021/7/3
*/
@Service
public class TemplateBaseServiceImpl implements ITemplateBaseService {
@Autowired
private ISmsCouponTemplateService couponTemplateService;
@Override
public CouponTemplateVO queryTemplateInfo(Long id) {
SmsCouponTemplate template = couponTemplateService.getById(id);
if (null == template) {
throw new BizException("Template Is Not Exist: " + id);
}
return BeanMapperUtils.map(template,CouponTemplateVO.class);
}
@Override
public List<SmsCouponTemplate> findAllUsableTemplate() {
return couponTemplateService.findAllUsableTemplate(true, false);
}
@Override
public Map<Long, SmsCouponTemplate> findAllTemplateByIds(Collection<Long> ids) {
List<SmsCouponTemplate> templates = couponTemplateService.listByIds(ids);
if (CollUtil.isEmpty(templates)) {
return MapUtil.empty();
}
return templates.stream()
.collect(Collectors.toMap(SmsCouponTemplate::getId, Function.identity()));
}
}

View File

@ -0,0 +1,54 @@
package com.youlai.mall.sms.shedule;
import cn.hutool.core.collection.CollUtil;
import com.youlai.mall.sms.pojo.domain.SmsCouponTemplate;
import com.youlai.mall.sms.pojo.vo.TemplateRuleVO;
import com.youlai.mall.sms.service.ISmsCouponTemplateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author xinyi
* @desc定时清理已过期的优惠券模板
* @date 2021/7/3
*/
@Slf4j
@Component
public class ScheduleTask {
@Autowired
private ISmsCouponTemplateService couponTemplateService;
/**
* 下线已过期的优惠券模板
*/
@Scheduled(fixedRate = 60 * 60 * 1000)
public void offlineCouponTemplate() {
log.info("Start To Expired CouponTemplate.");
// 查询未过期的优惠券模板
List<SmsCouponTemplate> templates = couponTemplateService.findAllNotExpiredTemplate(1);
if (CollUtil.isEmpty(templates)) {
log.info("Done To Expired CouponTemplate.");
return;
}
Date nowTime = new Date();
List<SmsCouponTemplate> expiredTemplates = new ArrayList<>(templates.size());
for (SmsCouponTemplate template : templates) {
TemplateRuleVO rule = template.getRule();
if (rule.getExpiration().getDeadline() < nowTime.getTime()) {
template.setExpired(0);
expiredTemplates.add(template);
}
}
couponTemplateService.updateBatchById(expiredTemplates);
log.info("Update Expired CouponTemplate Num:{}", expiredTemplates.size());
log.info("Done To Expired CouponTemplate.");
}
}

View File

@ -0,0 +1,18 @@
package com.youlai.mall.sms;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author xinyi
* @desc: 营销系统测试
* @date 2021/7/3
*/
@SpringBootTest
public class SmsApplicationTest {
@Test
public void contestLoad(){
}
}

View File

@ -21,6 +21,7 @@ public class SysPermissionServiceImpl extends ServiceImpl<SysPermissionMapper, S
@Autowired
private RedisTemplate redisTemplate;
@Override
public List<SysPermission> listPermRoles() {
return this.baseMapper.listPermRoles();
@ -50,6 +51,7 @@ public class SysPermissionServiceImpl extends ServiceImpl<SysPermissionMapper, S
urlPermRoles.put(perm, roles);
});
redisTemplate.opsForHash().putAll(GlobalConstants.URL_PERM_ROLES_KEY, urlPermRoles);
redisTemplate.convertAndSend("cleanRoleLocalCache","true");
}
// 初始化URL按钮->角色(集合)规则
List<SysPermission> btnPermList = permissions.stream()

View File

@ -11,26 +11,3 @@ spring:
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml
sentinel:
enabled: true
eager: true # 取消控制台懒加载项目启动即连接Sentinel
transport:
client-ip: localhost
dashboard: localhost:8080
datasource:
# 限流规则flow为key随便定义
flow:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: flow
# 降级规则
degrade:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-degrade-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: degrade

View File

@ -61,7 +61,7 @@ public class OAuthController {
* 方式一client_idclient_secret放在请求路径中(当前版本已废弃)
* 方式二放在请求头Request Headers中的Authorization字段且经过加密例如 Basic Y2xpZW50OnNlY3JldA== 明文等于 client:secret
*/
String clientId = JwtUtils.getAuthClientId();
String clientId = JwtUtils.getOAuthClientId();
OAuthClientEnum client = OAuthClientEnum.getByClientId(clientId);
switch (client) {
case TEST: // knife4j接口测试文档使用 client_id/client_secret : client/123456

View File

@ -124,11 +124,9 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap
/**
* 密码编码器
* <p>
*
* 委托方式根据密码的前缀选择对应的encoder例如{bcypt}前缀->标识BCYPT算法加密{noop}->标识不使用任何加密即明文的方式
* 密码判读 DaoAuthenticationProvider#additionalAuthenticationChecks
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {

View File

@ -34,7 +34,7 @@ public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String clientId = JwtUtils.getAuthClientId();
String clientId = JwtUtils.getOAuthClientId();
OAuthClientEnum client = OAuthClientEnum.getByClientId(clientId);
Result result;

View File

@ -9,38 +9,8 @@ spring:
discovery:
server-addr: http://localhost:8848
config:
# docker启动nacos-server需要配置
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml
group: DEFAULT_GROUP
sentinel:
enabled: true
eager: true # 取消控制台懒加载项目启动即连接Sentinel
transport:
client-ip: localhost
dashboard: localhost:8080
datasource:
# 降级规则
degrade:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-degrade-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: degrade
# 开启feign对sentinel的支持
feign:
sentinel:
enabled: true
# jwt 配置
jwt:
config:
enabled: true
key-location: jwt.jks
key-alias: jwt
key-pass: 123456
iss: youlai.tech
sub: all
access-exp-days: 30

View File

@ -2,7 +2,6 @@ package com.youlai.common.constant;
public interface AuthConstants {
/**
* 认证请求头key
*/

View File

@ -0,0 +1,14 @@
package com.youlai.common.constant;
/**
* @author xinyi
* @desc: MQ消息常量
* @date 2021/6/27
*/
public interface MQConstants {
/**
* SMS服务优惠券消息Topic
*/
String SMS_COUPON_TOPIC = "sms_coupon_topic";
}

View File

@ -4,4 +4,26 @@ public interface RedisConstants {
String BUSINESS_NO_PREFIX = "business_no:";
/**
* 优惠券码KEY前缀
*/
String SMS_COUPON_TEMPLATE_CODE_KEY = "sms_coupon_template_code_";
/**
* 用户当前所有可用优惠券key
*/
String SMS_USER_COUPON_USABLE_KEY = "sms_user_coupon_usable_";
/**
* 用户当前所有已使用优惠券key
*/
String SMS_USER_COUPON_USED_KEY = "sms_user_coupon_used_";
/**
* 用户当前所有已过期优惠券key
*/
String SMS_USER_COUPON_EXPIRED_KEY = "sms_user_coupon_expired_";
}

View File

@ -16,7 +16,6 @@ import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {

View File

@ -10,26 +10,40 @@ import org.apache.logging.log4j.util.Strings;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import sun.misc.BASE64Decoder;
import javax.servlet.http.HttpServletRequest;
import java.net.URLDecoder;
import java.util.List;
import java.util.stream.Collectors;
/**
* JWT工具类
*
* @author xianrui
*/
@Slf4j
public class JwtUtils {
@SneakyThrows
public static JSONObject getJwtPayload() {
String jwtPayload = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader(AuthConstants.JWT_PAYLOAD_KEY);
JSONObject jsonObject = JSONUtil.parseObj(jwtPayload);
String payload = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader(AuthConstants.JWT_PAYLOAD_KEY);
JSONObject jsonObject = JSONUtil.parseObj(URLDecoder.decode(payload,"UTF-8"));
return jsonObject;
}
/**
* 解析JWT获取用户ID
*
* @return
*/
public static Long getUserId() {
Long id = getJwtPayload().getLong(AuthConstants.USER_ID_KEY);
return id;
}
/**
* 解析JWT获取获取用户名
*
* @return
*/
public static String getUsername() {
String username = getJwtPayload().getStr(AuthConstants.USER_NAME_KEY);
return username;
@ -37,7 +51,7 @@ public class JwtUtils {
/**
* 获取登录认证的客户端ID
* <p>
*
* 兼容两种方式获取Oauth2客户端信息client_idclient_secret
* 方式一client_idclient_secret放在请求路径中
* 方式二放在请求头Request Headers中的Authorization字段且经过加密例如 Basic Y2xpZW50OnNlY3JldA== 明文等于 client:secret
@ -45,7 +59,7 @@ public class JwtUtils {
* @return
*/
@SneakyThrows
public static String getAuthClientId() {
public static String getOAuthClientId() {
String clientId;
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
@ -66,15 +80,17 @@ public class JwtUtils {
return clientId;
}
/**
* JWT获取用户角色列表
*
* @return 角色列表
*/
public static List<String> getRoles() {
List<String> roles = null;
JSONObject payload = getJwtPayload();
if (payload != null && payload.size() > 0) {
List<String> list = payload.get(AuthConstants.JWT_AUTHORITIES_KEY, List.class);
List<String> roles = list.stream().collect(Collectors.toList());
return roles;
if (payload != null && payload.containsKey(AuthConstants.JWT_AUTHORITIES_KEY)) {
roles = payload.get(AuthConstants.JWT_AUTHORITIES_KEY, List.class);
}
return null;
return roles;
}
}

View File

@ -0,0 +1,22 @@
package com.youlai.gateway.component;
import com.youlai.common.constant.GlobalConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import java.nio.charset.StandardCharsets;
public class RedisChannelListener implements MessageListener {
@Autowired
private UrlPermRolesLocalCache urlPermRolesLocalCache;
@Override
public void onMessage(Message message, byte[] bytes) {
String msg = new String(message.getBody(), StandardCharsets.UTF_8);
String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
urlPermRolesLocalCache.remove(GlobalConstants.URL_PERM_ROLES_KEY);
}
}

View File

@ -0,0 +1,44 @@
package com.youlai.gateway.component;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
/**
* @author DaniR
* @version 1.0
* @description 本地缓存设置
* @createDate 2021/6/16 10:08
*/
@Slf4j
@Component
public class UrlPermRolesLocalCache<T> {
private Cache<String,T> localCache = null;
@PostConstruct
private void init(){
localCache = CacheBuilder.newBuilder()
//设置本地缓存容器的初始容量
.initialCapacity(1)
//设置本地缓存的最大容量
.maximumSize(10)
//设置写缓存后多少秒过期
.expireAfterWrite(120, TimeUnit.SECONDS).build();
}
public void setLocalCache(String key,T object){
localCache.put(key,object);
}
public <T> T getCache(String key){
return (T) localCache.getIfPresent(key);
}
public void remove(String key){
localCache.invalidate(key);
}
}

View File

@ -0,0 +1,40 @@
package com.youlai.gateway.config;
import com.youlai.gateway.component.RedisChannelListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
@Configuration
public class RedisGatewyConfig {
@Autowired
private RedisConnectionFactory connectionFactory;
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(messageListenerAdapter(),channelTopic());
return container;
}
@Bean
MessageListenerAdapter messageListenerAdapter(){
return new MessageListenerAdapter(redisChannelListener());
}
@Bean
RedisChannelListener redisChannelListener(){
return new RedisChannelListener();
}
@Bean
ChannelTopic channelTopic(){
return new ChannelTopic("cleanRoleLocalCache");
}
}

View File

@ -69,9 +69,7 @@ public class ResourceServerConfig {
}
/**
* 未授权
*
* @return
* 自定义未授权响应
*/
@Bean
ServerAccessDeniedHandler accessDeniedHandler() {
@ -114,7 +112,6 @@ public class ResourceServerConfig {
/**
* 本地获取JWT验签公钥
* @return
*/
@SneakyThrows
@Bean

View File

@ -3,13 +3,13 @@ package com.youlai.gateway.security;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.youlai.common.constant.AuthConstants;
import com.youlai.common.constant.GlobalConstants;
import com.youlai.gateway.component.UrlPermRolesLocalCache;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
@ -23,22 +23,28 @@ import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 网关自定义鉴权管理器
*
* @author haoxr
* @date 2020-05-01
* @author <a href="mailto:xianrui0365@163.com">xianrui</a>
*/
@Component
@AllArgsConstructor
@RequiredArgsConstructor
@Slf4j
public class ResourceServerManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private RedisTemplate redisTemplate;
private final RedisTemplate redisTemplate;
// 本地缓存
private final UrlPermRolesLocalCache urlPermRolesLocalCache;
// 是否演示环境
@Value("${local-cache.enabled}")
private Boolean localCacheEnabled;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
@ -47,61 +53,68 @@ public class ResourceServerManager implements ReactiveAuthorizationManager<Autho
if (request.getMethod() == HttpMethod.OPTIONS) {
return Mono.just(new AuthorizationDecision(true));
}
PathMatcher pathMatcher = new AntPathMatcher(); // 声明定义Ant路径匹配模式请求路径和缓存中权限规则的URL权限标识匹配
PathMatcher pathMatcher = new AntPathMatcher(); // Ant匹配器
String method = request.getMethodValue();
String path = request.getURI().getPath();
String restfulPath = method + ":" + path; // Restful接口权限设计 @link https://www.cnblogs.com/haoxianrui/p/14961707.html
// 移动端请求需认证但无需鉴权判断
String token = request.getHeaders().getFirst(AuthConstants.AUTHORIZATION_KEY);
// 移动端请求无需鉴权只需认证即JWT的验签和是否过期判断
if (pathMatcher.match(GlobalConstants.APP_API_PATTERN, path)) {
// 如果token以"bearer "为前缀到这一步说明是经过NimbusReactiveJwtDecoder#decode和JwtTimestampValidator#validate等解析和验证通过的即已认证
if (StrUtil.isNotBlank(token) && token.startsWith(AuthConstants.AUTHORIZATION_PREFIX)) {
// 如果token以"bearer "为前缀到这里说明JWT有效即已认证
if (StrUtil.isNotBlank(token)
&& token.startsWith(AuthConstants.AUTHORIZATION_PREFIX)) {
return Mono.just(new AuthorizationDecision(true));
} else {
return Mono.just(new AuthorizationDecision(false));
}
}
// Restful接口权限设计 @link https://www.cnblogs.com/haoxianrui/p/14396990.html
String restfulPath = request.getMethodValue() + ":" + path;
log.info("请求方法:RESTFul请求路径{}", restfulPath);
// 缓存取 URL权限-角色集合 规则数据
// urlPermRolesRules = [{'key':'GET:/api/v1/users/*','value':['ADMIN','TEST']},...]
Map<String, Object> urlPermRolesRules;
if (localCacheEnabled) {
urlPermRolesRules = (Map<String, Object>) urlPermRolesLocalCache.getCache(GlobalConstants.URL_PERM_ROLES_KEY);
if (null == urlPermRolesRules) {
urlPermRolesRules = redisTemplate.opsForHash().entries(GlobalConstants.URL_PERM_ROLES_KEY);
urlPermRolesLocalCache.setLocalCache(GlobalConstants.URL_PERM_ROLES_KEY, urlPermRolesRules);
}
} else {
urlPermRolesRules = redisTemplate.opsForHash().entries(GlobalConstants.URL_PERM_ROLES_KEY);
}
// 缓存取URL权限标识->角色集合权限规则
Map<String, Object> permRolesRules = redisTemplate.opsForHash().entries(GlobalConstants.URL_PERM_ROLES_KEY);
// 根据 请求路径 权限规则中的URL权限标识进行Ant匹配得出拥有权限的角色集合
Set<String> hasPermissionRoles = CollectionUtil.newHashSet(); // 声明定义有权限的角色集合
boolean needToCheck = false; // 声明定义是否需要被拦截检查的请求如果缓存中权限规则中没有任何URL权限标识和此次请求的URL匹配默认不需要被鉴权
// 根据请求路径判断有访问权限的角色列表
List<String> authorizedRoles = new ArrayList<>(); // 拥有访问权限的角色
boolean requireCheck = false; // 是否需要鉴权默认没有设置权限规则不用鉴权
for (Map.Entry<String, Object> permRoles : permRolesRules.entrySet()) {
String perm = permRoles.getKey(); // 缓存权限规则的键URL权限标识
for (Map.Entry<String, Object> permRoles : urlPermRolesRules.entrySet()) {
String perm = permRoles.getKey();
if (pathMatcher.match(perm, restfulPath)) {
List<String> roles = Convert.toList(String.class, permRoles.getValue()); // 缓存权限规则的值有请求路径访问权限的角色集合
hasPermissionRoles.addAll(Convert.toList(String.class, roles));
if (needToCheck == false) {
needToCheck = true;
List<String> roles = Convert.toList(String.class, permRoles.getValue());
authorizedRoles.addAll(Convert.toList(String.class, roles));
if (requireCheck == false) {
requireCheck = true;
}
}
}
log.info("拥有接口访问权限的角色:{}", hasPermissionRoles.toString());
// 没有设置权限规则放行如果默认想拦截所有的请求请移除needToCheck变量逻辑即可根据需求定制
if (needToCheck == false) {
if (requireCheck == false) {
return Mono.just(new AuthorizationDecision(true));
}
// 判断用户JWT中携带的角色是否有能通过权限拦截的角色
// 判断JWT中携带的用户角色是否有权限访问
Mono<AuthorizationDecision> authorizationDecisionMono = mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authority -> {
log.info("用户权限 : {}", authority); // ROLE_ROOT
String role = authority.substring(AuthConstants.AUTHORITY_PREFIX.length()); // 角色编码ROOT
if (GlobalConstants.ROOT_ROLE_CODE.equals(role)) { // 如果是超级管理员则放行
return true;
String roleCode = authority.substring(AuthConstants.AUTHORITY_PREFIX.length()); // 用户的角色
if (GlobalConstants.ROOT_ROLE_CODE.equals(roleCode)) {
return true; // 如果是超级管理员则放行
}
boolean hasPermission = CollectionUtil.isNotEmpty(hasPermissionRoles) && hasPermissionRoles.contains(role); // 用户角色中只要有一个满足则通过权限校验
return hasPermission;
boolean hasAuthorized = CollectionUtil.isNotEmpty(authorizedRoles) && authorizedRoles.contains(roleCode);
return hasAuthorized;
})
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));

View File

@ -7,10 +7,10 @@ import com.nimbusds.jose.JWSObject;
import com.youlai.common.constant.AuthConstants;
import com.youlai.common.result.ResultCode;
import com.youlai.gateway.util.ResponseUtils;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
@ -23,22 +23,23 @@ import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URLEncoder;
/**
* 安全拦截全局过滤器
*
* @author haoxr
* @date 2020-06-12
* @author <a href="mailto:xianrui0365@163.com">xianrui</a>
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class SecurityGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private RedisTemplate redisTemplate;
private final RedisTemplate redisTemplate;
// 是否演示环境
@Value("${demo}")
private Boolean isDemoEnv;
@Value("${spring.profiles.active}")
private String env;
@SneakyThrows
@Override
@ -47,8 +48,8 @@ public class SecurityGlobalFilter implements GlobalFilter, Ordered {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 演示环境禁止删除和修改
if (isDemoEnv
// 线上演示环境禁止修改和删除
if (env.equals("prod")
&& (HttpMethod.DELETE.toString().equals(request.getMethodValue()) // 删除方法
|| HttpMethod.PUT.toString().equals(request.getMethodValue())) // 修改方法
) {
@ -74,7 +75,7 @@ public class SecurityGlobalFilter implements GlobalFilter, Ordered {
// 存在token且不是黑名单request写入JWT的载体信息
request = exchange.getRequest().mutate()
.header(AuthConstants.JWT_PAYLOAD_KEY, payload)
.header(AuthConstants.JWT_PAYLOAD_KEY, URLEncoder.encode(payload,"UTF-8"))
.build();
exchange = exchange.mutate().request(request).build();
return chain.filter(exchange);

View File

@ -11,7 +11,7 @@ import springfox.documentation.swagger.web.*;
import java.util.Optional;
/**
* @Author haoxr
* @author xianrui
* @Date 2021-02-25 16:34
* @Version 1.0.0
* @ https://gitee.com/xiaoym/swagger-bootstrap-ui-demo/blob/master/knife4j-spring-cloud-gateway/service-doc/src/main/java/com/xiaominfo/swagger/service/doc/handler/SwaggerHandler.java

View File

@ -8,9 +8,9 @@ import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
/**
* @auth haoxr
* @link https://gitee.com/xiaoym/swagger-bootstrap-ui-demo/blob/master/knife4j-spring-cloud-gateway/service-doc/src/main/java/com/xiaominfo/swagger/service/doc/config/SwaggerHeaderFilter.java
* @auth xianrui
* @date 2021-02-25 16:29
* @link https://gitee.com/xiaoym/swagger-bootstrap-ui-demo/blob/master/knife4j-spring-cloud-gateway/service-doc/src/main/java/com/xiaominfo/swagger/service/doc/config/SwaggerHeaderFilter.java
*/
@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {

View File

@ -13,29 +13,7 @@ spring:
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml
sentinel:
enabled: false # 网关流控开关
eager: true # 取消控制台懒加载项目启动即连接Sentinel
transport:
client-ip: localhost
dashboard: localhost:8080
datasource:
# 网关限流规则gw-flow为key随便定义
gw-flow:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-gw-flow-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: gw-flow
# 网关API自定义分组
gw-api-group:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-gw-api-group-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: gw-api-group

View File

@ -15,24 +15,3 @@ spring:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml
namespace: prod_namespace_id
sentinel:
eager: true
transport:
dashboard: e.youlai.tech:8858
datasource:
# 网关限流
gw-flow:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-gw-flow-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: gw-flow
# 网关API自定义分组
gw-api-group:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-gw-api-group-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: gw-api-group