系统搭建优惠券,思路如下:1)创建 2)领取 3)核销
- 创建优惠券
- 选择优惠券类型(如:满减券、折扣券、无门槛券等)
- 优惠券审核(审核可用时间、门槛条件、是否拥有优惠券)
- 投放优惠券:用户领取、系统自动投放(生日券、新人券等)
- 领取(定时、抢券)
- 使用优惠券,金额使用(购物车、订单的使用)
- 优惠券的核销(优惠券金额平摊到货物上)
从业务的复用性,一般我们都会创建一个优惠券模板,该模板会注明:发放时间、开始时间和结束时间。
优惠券使用范围:商品分类(商品分类的哪些类)、指定商品、指定品牌、指定商品、
活动的理解: 整合所有优惠券的专题和整合活动的商品
优惠券与活动的关系(活动跟分类必须有关系)
方法一:优惠券->分类->活动
SPU->Category->优惠券
方案二:优惠券->活动->分类
SPU->Category ->Activity ->优惠券
下面我们先看看Activity和Coupon的Model
Activity(Model)
package com.lin.missyou.model;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Where;
import javax.persistence.*;
import java.util.Date;
import java.util.List;
@Entity
@Setter
@Getter
@Where(clause = "delete_time is null and online = 1")
public class Activity extends BaseEntity {
@Id
private Long id;
private String title;
private String name;
private String description;
private Date startTime;
private Date endTime;
private Boolean online;
private String entranceImg;
private String internalTopImg;
private String remark;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name="activityId")
private List<Coupon> couponList;
}
Coupon( Model )
package com.lin.missyou.model;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Where;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Entity
@Getter
@Setter
@Where(clause = "delete_time is null")
public class Coupon extends BaseEntity {
@Id
private Long id;
private Long activityId;
private String title;
private Date startTime;
private Date endTime;
private String description;
private BigDecimal fullMoney;
private BigDecimal minus;
/*折扣比例*/
private BigDecimal rate;
/*优惠券的描述*/
private String remark;
/*是否全场券*/
private Boolean wholeStore;
/*优惠券的类型*/
private Integer type;
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "couponList")
private List<Category> categoryList;
}
思考一个问题?为什么前文说到,在活动与优惠券之间引入一个优惠券券模板呢?
首先我们明确:活动与优惠券的关系是多对多;任何一个多对多都可以转换成两个一对多;
例如我们可以通过:活动–>活动模板(一对多)–>优惠券(一对多)
下面通过展示活动接口来讲解一些高级用法
菜单
1、优惠券的领取
创建Controller
package com.lin.missyou.api.v1;
import com.lin.missyou.exception.http.NotFoundException;
import com.lin.missyou.model.Activity;
import com.lin.missyou.service.ActivityService;
import com.lin.missyou.vo.ActivityCouponVO;
import com.lin.missyou.vo.ActivityPureVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("activity")
@RestController
public class ActivityController {
@Autowired
private ActivityService activityService;
/* 标准活动内容 */
@GetMapping("/name/{name}")
public ActivityPureVO getHomeActivity(@PathVariable String name) {
Activity activity = activityService.getByName(name);
if (activity == null) {
throw new NotFoundException(40001);
}
ActivityPureVO vo = new ActivityPureVO(activity);
return vo;
}
/* 带优惠券活动容 */
@GetMapping("/name/{name}/with_coupon")
public ActivityCouponVO getActivityWithCoupons(@PathVariable String name) {
Activity activity = activityService.getByName(name);
if (activity == null) {
throw new NotFoundException(40001);
}
return new ActivityCouponVO(activity);
}
}
创建Service
package com.lin.missyou.service;
import com.lin.missyou.model.Activity;
import com.lin.missyou.repository.ActivityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service
public class ActivityService {
@Autowired
private ActivityRepository activityRepository;
public Activity getByName(String name) {
return activityRepository.findByName(name);
}
}
创建Repository
package com.lin.missyou.repository;
import com.lin.missyou.model.Activity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@Repository
public interface ActivityRepository extends JpaRepository<Activity, Long> {
Activity findByName(String name);
Optional<Activity> findByCouponListId(Long couponId);
}
创建ActivityPureVO 用于输出标准活动接口
package com.lin.missyou.vo;
import com.lin.missyou.model.Activity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.beans.BeanUtils;
@Getter
@Setter
@NoArgsConstructor
public class ActivityPureVO {
private Long id;
private String title;
private String entranceImg;
private Boolean online;
private String remark;
private String startTime;
private String endTime;
public ActivityPureVO(Activity activity) {
BeanUtils.copyProperties(activity,this);
}
public ActivityPureVO(Object object){
BeanUtils.copyProperties(object, this);
}
}
创建ActivityCouponVO 用于输出带优惠券的活动接口
package com.lin.missyou.vo;
import com.lin.missyou.model.Activity;
import com.lin.missyou.model.Coupon;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
import java.util.stream.Collectors;
@Getter
@Setter
public class ActivityCouponVO extends ActivityPureVO {
private List<CouponPureVO> coupons;
public ActivityCouponVO(Activity activity) {
super(activity);
coupons = activity.getCouponList()
.stream().map(CouponPureVO::new)
.collect(Collectors.toList());
}
}
创建CouponPureVO 优惠券过滤VO
package com.lin.missyou.vo;
import com.lin.missyou.model.Coupon;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.beans.BeanUtils;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Getter
@Setter
@NoArgsConstructor
public class CouponPureVO {
private Long id;
private String title;
private Date startTime;
private Date endTime;
private String description;
private BigDecimal fullMoney;
private BigDecimal minus;
private BigDecimal rate;
private Integer type;
private String remark;
private Boolean wholeStore;
public CouponPureVO(Object[] objects){
Coupon coupon = (Coupon) objects[0];
BeanUtils.copyProperties(coupon, this);
}
public CouponPureVO(Coupon coupon){
BeanUtils.copyProperties(coupon, this);
}
public static List<CouponPureVO> getList(List<Coupon> couponList) {
return couponList.stream()
.map(CouponPureVO::new)
.collect(Collectors.toList());
}
}
=========扩展学习JPQL
在业务中,我们需要根据活动ID获得所适应的优惠券。下面将从原生SQL和Model的两种JPQ进行讲解。
原生SQL的JPQL
Alt+Enter 选择“Edit Spring Data QL Fragment”
package com.lin.missyou.repository;
import com.lin.missyou.model.Coupon;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Date;
import java.util.List;
import java.util.Optional;
public interface CouponRepository extends JpaRepository<Coupon, Long>, JpaSpecificationExecutor<Coupon> {
@Query("select * from coupon\n" +
"join coupon_catepory\n" +
"on coupon.id=coupon_catepory.cpupon_id\n" +
"join category\n" +
"on coupon_catepory.categpry_id=category.id\n" +
"join Activity a on a.id=coupon_catepory.activityId\n" +
"where category.id=:id\n" +
"and a.startTime < :now\n" +
"and a.endTime > :now")
List<Coupon> findByCategory(Long cid, Date now);
}
原生SQL写起来比较复杂,而且在序列化成数组型Json需要写逻辑代码。
Model的JPQL
首先我们先看看coupon、category和Activity的三个model
couponModel
package com.lin.missyou.model;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Where;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Entity
@Getter
@Setter
@Where(clause = "delete_time is null")
public class Coupon extends BaseEntity {
@Id
private Long id;
private Long activityId;
private String title;
private Date startTime;
private Date endTime;
private String description;
private BigDecimal fullMoney;
private BigDecimal minus;
/*折扣比例*/
private BigDecimal rate;
/*优惠券的描述*/
private String remark;
/*是否全场券*/
private Boolean wholeStore;
/*优惠券的类型*/
private Integer type;
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "couponList")
private List<Category> categoryList;
}
categoryModel
package com.lin.missyou.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Where;
import javax.persistence.*;
import java.util.List;
@Entity
@Getter
@Setter
@Where(clause = "delete_time is null and online = 1")
public class Category extends BaseEntity {
@Id
private Long id;
private String name;
private String description;
private Boolean isRoot;
private String img;
private Long parentId;
private Long index;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "coupon_category",
joinColumns = @JoinColumn(name = "category_id"),
inverseJoinColumns = @JoinColumn(name = "coupon_id"))
private List<Coupon> couponList;
}
ActivityModel
package com.lin.missyou.model;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Where;
import javax.persistence.*;
import java.util.Date;
import java.util.List;
@Entity
@Setter
@Getter
@Where(clause = "delete_time is null and online = 1")
public class Activity extends BaseEntity {
@Id
private Long id;
private String title;
private String name;
private String description;
private Date startTime;
private Date endTime;
private Boolean online;
private String entranceImg;
private String internalTopImg;
private String remark;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name="activityId")
private List<Coupon> couponList;
}
ModelSPQL
package com.lin.missyou.repository;
import com.lin.missyou.model.Coupon;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Date;
import java.util.List;
import java.util.Optional;
public interface CouponRepository extends JpaRepository<Coupon, Long>, JpaSpecificationExecutor<Coupon> {
@Query("select c from Coupon c\n" +
"join c.categoryList ca\n" +
"join Activity a on a.id = c.activityId\n" +
"where ca.id = :cid\n" +
"and a.startTime < :now\n" +
"and a.endTime > :now\n")
List<Coupon> findByCategory(Long cid, Date now);
}
关于单表查询和联表查询的一些看法:
单表查询:
JAVA逻辑代码 进行过滤盒组合。使用内存
搜索或分页:使用ES中
联表查询:
select join 但是会有因为笛卡尔级,不符合阿里的开发规范。性能比较低
2、优惠券的核销
关于优惠券的核销,我们需要查看超权问题;
- 当前用户是否存在此优惠券
- 优惠券是否过期
- 优惠券状态
问题一:如果获得当前用户信息?
- 请求中带上uid【安全性极差】
- 通过依赖注入的方式,把token,再通过Jwt解析获得uid
- 编写通用的LocalUser类
方法二:把token通过JWT解析加密的uid
String bearerToken = request.getHeader("Authorization");
if(bearerToken == null){
throw new UnAuthenticatedException(10001);
}
if (!bearerToken.startsWith("Bearer")) {
throw new UnAuthenticatedException(10004);
}
String tokens[] = bearerToken.split(" ");
if (!(tokens.length == 2)) {
throw new UnAuthenticatedException(10004);
}
String token = tokens[1];
Optional<Map<String, Claim>> optionalMap = JwtToken.getClaims(token);
/*解析token,获得之前生成token的信息
* 在service/VankeAuthenticationServiceImpl这里的make token传的参数
* */
Map<String, Claim> map = optionalMap
.orElseThrow(() -> new UnAuthenticatedException(10004));
方法三:编写通用的LocalUser类
首先我们写一个Localuser的类
然后在拦截器中set用户信息,再使用的地方getUser。但我们需要注意,该类是静态方法,如果多个用户同时操作的话,会存有问题。如何避免这个问题呢?下面引入线程来处理。
首先在拦截器 PermissionInterceptor 中定义方法 setToThreadLocal
package gani.vankek3api.core.interceptors;
import com.auth0.jwt.interfaces.Claim;
import gani.vankek3api.core.LocalUser;
import gani.vankek3api.exception.ForbiddenException;
import gani.vankek3api.exception.UnAuthenticatedException;
import gani.vankek3api.model.User;
import gani.vankek3api.service.UserService;
import gani.vankek3api.util.JwtToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.Optional;
@Deprecated
public class PermissionInterceptor extends HandlerInterceptorAdapter {
@Autowired
private UserService userService;
public PermissionInterceptor() {
super();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Optional<ScopeLevel> scopeLevel = this.getScopeLevel(handler);
if (!scopeLevel.isPresent()) {
return true;
}
String bearerToken = request.getHeader("Authorization");
if(bearerToken == null){
throw new UnAuthenticatedException(10001);
}
if (!bearerToken.startsWith("Bearer")) {
throw new UnAuthenticatedException(10004);
}
String tokens[] = bearerToken.split(" ");
if (!(tokens.length == 2)) {
throw new UnAuthenticatedException(10004);
}
String token = tokens[1];
Optional<Map<String, Claim>> optionalMap = JwtToken.getClaims(token);
/*解析token,获得之前生成token的信息
* 在service/VankeAuthenticationServiceImpl这里的make token传的参数
* */
Map<String, Claim> map = optionalMap
.orElseThrow(() -> new UnAuthenticatedException(10004));
boolean valid = this.hasPermission(scopeLevel.get(), map);
if(valid){
this.setToThreadLocal(map);
}
return valid;
}
private void setToThreadLocal(Map<String,Claim> map) {
Long uid = map.get("uid").asLong();
Integer scope = map.get("scope").asInt();
User user = this.userService.getUserInfoById(uid);
LocalUser.set(user, scope);
}
/* 判断请求的token权限与ScopeLevel注解等级对比 */
private boolean hasPermission(ScopeLevel scopeLevel, Map<String, Claim> map) {
Integer level = scopeLevel.value();
Integer scope = map.get("scope").asInt();
if (level > scope) {
throw new ForbiddenException(10005);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
super.afterCompletion(request, response, handler, ex);
}
private Optional<ScopeLevel> getScopeLevel(Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
ScopeLevel scopeLevel = handlerMethod.getMethod().getAnnotation(ScopeLevel.class);
if (scopeLevel == null) {
return Optional.empty();
}
return Optional.of(scopeLevel);
}
return Optional.empty();
}
}
修改LocalUser类,使用 ThreadLocal
注意,使用线程后,为了不让资源被占用,记得定义清除线程的方法。
package gani.vankek3api.core;
import gani.vankek3api.model.User;
import java.util.HashMap;
import java.util.Map;
public class LocalUser {
private static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<>();
public static void set(User user, Integer scope) {
Map<String, Object> map = new HashMap<>();
map.put("user", user);
map.put("scope", scope);
LocalUser.threadLocal.set(map);
}
public static void clear() {
LocalUser.threadLocal.remove();
}
public static User getUser() {
Map<String, Object> map = LocalUser.threadLocal.get();
User user = (User)map.get("user");
return user;
}
public static Integer getScope() {
Map<String, Object> map = LocalUser.threadLocal.get();
Integer scope = (Integer)map.get("scope");
return scope;
}
}
在 PermissionInterceptor的afterCompletion方法处,加入LocalUser.clear() 。
至此,以后调用的时候直接使用LocalUser.getUser().getId(); 即可获得用户ID。
详细可参考之前的文章token认证文章
JPA扩展学习
JPA查询导航属性
一对多的情况下,根据多方的id反查一方
package com.lin.missyou.repository;
import com.lin.missyou.model.Activity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@Repository
public interface ActivityRepository extends JpaRepository<Activity, Long> {
Optional<Activity> findByCouponListId(Long couponId);
}
扩展学习JPA保存数据库及枚举的使用
在core/enumeration下创建枚举CouponStatus
package com.lin.missyou.core.enumeration;
import java.util.stream.Stream;
public enum CouponStatus {
AVAILABLE(1, "可以使用,未过期"),
USED(2, "已使用"),
EXPIRED(3, "未使用,已过期");
private Integer value;
public Integer getValue() {
return this.value;
}
CouponStatus(Integer value, String description) {
this.value =value;
}
public static CouponStatus toType(int value) {
return Stream.of(CouponStatus.values())
.filter(c -> c.value == value)
.findAny()
.orElse(null);
}
}
使用JPA保存数据
UserCoupon userCouponNew = UserCoupon.builder()
.userId(uid)
.couponId(couponId)
.status(CouponStatus.AVAILABLE.getValue())
.createTime(now)
.build();
userCouponRepository.save(userCouponNew);
创建更新数据库返回状态
- CreateSuccess
- DeleteSuccess
- UpdateSuccess
在exception下面创建以上三个类
package com.lin.missyou.exception;
import com.lin.missyou.exception.http.HttpException;
public class CreateSuccess extends HttpException {
public CreateSuccess(int code){
this.httpStatusCode = 201;
this.code = code;
}
}
package com.lin.missyou.exception;
import com.lin.missyou.exception.http.HttpException;
public class DeleteSuccess extends HttpException {
public DeleteSuccess(int code){
this.httpStatusCode = 200;
this.code = code;
}
}
package com.lin.missyou.exception;
import com.lin.missyou.exception.http.HttpException;
public class UpdateSuccess extends HttpException {
public UpdateSuccess(int code){
this.httpStatusCode = 200;
this.code = code;
}
}
修改UnifyResponse
package com.lin.missyou.core;
import com.lin.missyou.exception.CreateSuccess;
import com.lin.missyou.exception.DeleteSuccess;
import com.lin.missyou.exception.UpdateSuccess;
public class UnifyResponse {
private int code;
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public String getRequest() {
return request;
}
private String message;
private String request;
public UnifyResponse(int code, String message, String request){
this.code = code;
this.message = message;
this.request = request;
}
public static void createSuccess(int code) {
throw new CreateSuccess(code);
}
public static void deleteSuccess(int code) {
throw new DeleteSuccess(code);
}
public static void updateSuccess(int code) {
throw new UpdateSuccess(code);
}
}
修改InterceptorConfiguration
package gani.vankek3api.core.configuration;
import gani.vankek3api.core.interceptors.PermissionInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
@Bean
public HandlerInterceptor getPermissionInterceptor() {
return new PermissionInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.getPermissionInterceptor());
}
}
在调用更新save方法后,抛出Success
UnifyResponse.createSuccess(0);
枚举的类型转换
前文我们已经定义了枚举 CouponStatus
那我们怎么使用类型定义的类型呢?
switch(CouponStatus.toType(status)){
case USED:
##do something
break;
case AVAILABLE:
##do something
break;
case EXPIRED:
##do something
break;
}
=====触发器
优惠券的过期问题
优惠券的状态:未使用、已使用、已过期
可以通过触发机制,主要分为两种:
1、主动触发:轮询,写一个线程,定时扫描数据库,根据Start-EndTime 判断,最终更新优惠劵的状态
关于优惠券的状态:
优惠券的状态:未使用、已使用、已过期 ,那么如何修改呢此状态呢?
- 主动触发:轮询,写一个线程,定时扫描数据库,根据Start-EndTime 判断,最终更新优惠劵的状态
- 被动触发:使用第三方的库。例如Redis、RocketMQ