Merge branch 'dev_market'

This commit is contained in:
JIAN 2024-10-01 15:57:17 +08:00
commit 921a83d5c3
65 changed files with 2658 additions and 27 deletions

View File

@ -0,0 +1,39 @@
package com.jzo2o.api.market;
import com.jzo2o.api.market.dto.request.CouponUseBackReqDTO;
import com.jzo2o.api.market.dto.request.CouponUseReqDTO;
import com.jzo2o.api.market.dto.response.AvailableCouponsResDTO;
import com.jzo2o.api.market.dto.response.CouponUseResDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
import java.util.List;
/**
* 内部接口 - 优惠卷相关接口
* @author JIAN
*/
@FeignClient(contextId = "jzo2o-market", value = "jzo2o-market", path = "/market/inner/coupon")
public interface CouponApi {
/**
* 根据订单金额获取当前用户可用优惠卷
*/
@GetMapping("/getAvailable")
List<AvailableCouponsResDTO> getAvailableCoupon(@RequestParam BigDecimal totalAmount);
/**
* 用户核销优惠卷返回优惠金额
*/
@PostMapping("/use")
CouponUseResDTO useCoupon(@RequestBody CouponUseReqDTO couponUseReqDTO);
/**
* 用户取消订单退回使用优惠卷
*/
@PostMapping("/useBack")
void useBack(@RequestBody CouponUseBackReqDTO couponUseBackReqDTO);
}

View File

@ -4,8 +4,8 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ApiModel("优惠券使用退回请求模型")
@Data
@ApiModel("优惠券使用退回请求模型")
public class CouponUseBackReqDTO {
@ApiModelProperty("优惠券id")
private Long id;
@ -13,4 +13,4 @@ public class CouponUseBackReqDTO {
private Long ordersId;
@ApiModelProperty("用户id")
private Long userId;
}
}

View File

@ -2,11 +2,13 @@ package com.jzo2o.api.market.dto.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Builder
@ApiModel("优惠券使用模型")
public class CouponUseReqDTO {
@ApiModelProperty("优惠券id")
@ -15,4 +17,4 @@ public class CouponUseReqDTO {
private Long ordersId;
@ApiModelProperty("总金额")
private BigDecimal totalAmount;
}
}

View File

@ -3,12 +3,17 @@ package com.jzo2o.api.market.dto.response;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* @author JIAN
*/
@Data
@ApiModel("优惠券信息")
@Accessors(chain = true)
public class AvailableCouponsResDTO {
@ApiModelProperty("优惠券id")
private Long id;
@ -16,34 +21,34 @@ public class AvailableCouponsResDTO {
/**
* 优惠券名称
*/
@ApiModelProperty(value = "活动名称",required = true)
@ApiModelProperty(value = "活动名称", required = true)
private String name;
/**
* 活动id
*/
@ApiModelProperty(value = "活动id",required = true)
@ApiModelProperty(value = "活动id", required = true)
private Long activityId;
@ApiModelProperty(value = "使用类型1满减2折扣",required = true)
@ApiModelProperty(value = "使用类型1满减2折扣", required = true)
private Integer type;
/**
* 折扣
*/
@ApiModelProperty(value = "折扣",required = false)
@ApiModelProperty(value = "折扣")
private Integer discountRate;
/**
* 优惠金额
*/
@ApiModelProperty(value = "优惠金额",required = false)
@ApiModelProperty(value = "优惠金额")
private BigDecimal discountAmount;
/**
* 满减金额
*/
@ApiModelProperty(value = "满减条件,0:表示无门槛",required = true)
@ApiModelProperty(value = "满减条件,0:表示无门槛", required = true)
private BigDecimal amountCondition;
/**
@ -51,4 +56,4 @@ public class AvailableCouponsResDTO {
*/
@ApiModelProperty("优惠券过期时间")
private LocalDateTime validityTime;
}
}

View File

@ -2,13 +2,17 @@ package com.jzo2o.api.market.dto.response;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@ApiModel("优惠券使用返回信息模型")
@NoArgsConstructor
@AllArgsConstructor
public class CouponUseResDTO {
@ApiModelProperty("优惠金额")
private BigDecimal discountAmount;
}
}

9
jzo2o-market/Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM openjdk:11-jdk
LABEL maintainer="研究院研发组 <research-maint@itcast.cn>"
RUN echo "Asia/Shanghai" > /etc/timezone
ARG PACKAGE_PATH=./target/jzo2o-market.jar
ADD ${PACKAGE_PATH:-./} app.jar
EXPOSE 11510
ENTRYPOINT ["sh","-c","java -jar $JAVA_OPTS app.jar"]

103
jzo2o-market/pom.xml Normal file
View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>jzo2o-market</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<artifactId>jzo2o-parent</artifactId>
<groupId>com.jzo2o</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-mvc</artifactId>
</dependency>
<dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-knife4j-web</artifactId>
</dependency>
<dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-mysql</artifactId>
</dependency>
<dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-api</artifactId>
</dependency>
<dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-redis</artifactId>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--canal-->
<dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-canal-sync</artifactId>
</dependency>
<dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-xxl-job</artifactId>
</dependency>
<!-- <dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-seata</artifactId>
</dependency>-->
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.jzo2o.market.MarketApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,20 @@
package com.jzo2o.market;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@EnableAspectJAutoProxy
@SpringBootApplication
@Slf4j
@MapperScan("com.jzo2o.market.mapper")
public class MarketApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(MarketApplication.class)
.build(args)
.run(args);
log.info("家政服务-营销中心启动");
}
}

View File

@ -0,0 +1,23 @@
package com.jzo2o.market.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
/**
* 加载Redis的Lua脚本配置类
* @author JIAN
*/
@Configuration
public class RedisLuaConfiguration {
@Bean("seizeCouponScript")
public DefaultRedisScript<Integer> seizeCouponScript() {
DefaultRedisScript<Integer> redisScript = new DefaultRedisScript<>();
// resource目录下的scripts文件下的seizeCouponScript.lua文件
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/seizeCouponScript.lua")));
redisScript.setResultType(Integer.class);
return redisScript;
}
}

View File

@ -0,0 +1,28 @@
package com.jzo2o.market.config;
import com.jzo2o.redis.properties.RedisSyncProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 线程池配置
* @author JIAN
*/
@Configuration
public class TreadPoolConfiguration {
@Bean("syncThreadPool")
public ThreadPoolExecutor threadPoolExecutor(RedisSyncProperties redisSyncProperties) {
return new ThreadPoolExecutor(
1,
redisSyncProperties.getQueueNum(),
120,
TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadPoolExecutor.DiscardPolicy()
);
}
}

View File

@ -0,0 +1,8 @@
package com.jzo2o.market.constants;
public class ErrorInfo {
public static class Msg {
public static final String SEIZE_COUPON_FAILD = "单子已经被抢走了";
}
}

View File

@ -0,0 +1,52 @@
package com.jzo2o.market.constants;
public class RedisConstants {
public static final class RedisKey {
/**
* 优惠券库存表 由Canal同步程序写入
* redis key格式COUPON:RESOURCE:STOCK:{序号} 序号=活动id% 10
* hash结构 (hashKey:活动id,hashValue:库存数量)
*/
public static final String COUPON_RESOURCE_STOCK = "COUPON:RESOURCE:STOCK:{%s}";
/**
* 抢券同步队列存储抢券成功记录由抢券程序(Lua)写入
* redis key格式 QUEUE:COUPON:SEIZE:SYNC:{序号} 序号=活动id% 10
* hash结构 (hashKey:活动id,hashValue:用户id)
*/
public static final String COUPON_SEIZE_SYNC_QUEUE_NAME = "COUPON:SEIZE:SYNC";
/**
* 抢券成功列表用户抢券成功写入记录
* redis key格式 COUPON:SEIZE:LIST:活动id_{序号} 序号=活动id% 10
* hash结构 (hashKey:用户id,hashValue:1)
*/
public static final String COUPON_SEIZE_LIST = "COUPON:SEIZE:LIST:%s_{%s}";
/**
* 活动列表 由于活动预热程序写入待开始及进行中的活动
* redis key格式:ACTIVITY:LIST
* string 结构: value=活动列表json串
*/
public static final String ACTIVITY_CACHE_LIST = "ACTIVITY:LIST";
}
public static final class Formatter {
/**
* 优惠券抢券同步
*/
public static final String COUPON_SEIZE_HANDLE_LOCK = "COUPON:SEIZE:RESULT_PROCESS";
/**
* 活动预热
*/
public static final String ACTIVITY_PREHEAT = "ACTIVITY:PREHEAT";
/**
* 活动结束
*/
public static final String ACTIVITY_FINISHED = "ACTIVITY:FINISHED";
}
}

View File

@ -0,0 +1,13 @@
package com.jzo2o.market.constants;
public class TabTypeConstants {
/**
* 抢单中
*/
public static final int SEIZING = 1;
/**
* 未开始
*/
public static final int NO_START = 2;
}

View File

@ -0,0 +1,44 @@
package com.jzo2o.market.controller.consumer;
import com.jzo2o.common.expcetions.BadRequestException;
import com.jzo2o.market.enums.ActivityStatusEnum;
import com.jzo2o.market.model.dto.response.SeizeCouponInfoResDTO;
import com.jzo2o.market.service.IActivityService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* 用户端 - 活动管理控制器
* @author JIAN
*/
@Slf4j
@RestController("consumerActivityController")
@RequestMapping("/consumer/activity")
public class ActivityController {
@Resource
private IActivityService activityService;
/**
* 用户端查询缓存中的活动信息
* @param tabType 1 进行中 2 待生效
*/
@GetMapping("/list")
public List<SeizeCouponInfoResDTO> listActivity(@RequestParam Integer tabType) {
ActivityStatusEnum activityStatusEnum;
if (tabType == 1) {
activityStatusEnum = ActivityStatusEnum.DISTRIBUTING;
} else if (tabType == 2) {
activityStatusEnum = ActivityStatusEnum.NO_DISTRIBUTE;
} else {
throw new BadRequestException("请求的状态出错");
}
return activityService.getCachedActivity(activityStatusEnum);
}
}

View File

@ -0,0 +1,50 @@
package com.jzo2o.market.controller.consumer;
import com.jzo2o.common.expcetions.BadRequestException;
import com.jzo2o.common.utils.ObjectUtils;
import com.jzo2o.market.enums.CouponStatusEnum;
import com.jzo2o.market.model.dto.request.SeizeCouponReqDTO;
import com.jzo2o.market.model.dto.response.CouponSimpleInfoResDTO;
import com.jzo2o.market.service.IActivityService;
import com.jzo2o.market.service.ICouponService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 用户端 - 优惠卷管理控制器
* @author JIAN
*/
@Slf4j
@RestController("consumerCouponController")
@RequestMapping("/consumer/coupon")
public class CouponController {
@Resource
private IActivityService activityService;
@Resource
private ICouponService couponService;
/**
* 获取当前用户的优惠卷(滚动查询)
*/
@GetMapping("/my")
public List<CouponSimpleInfoResDTO> currentUserCoupon(@RequestParam Integer status,
@RequestParam(required = false) Integer lastId) {
CouponStatusEnum couponStatusEnum = CouponStatusEnum.statusOf(status);
if (ObjectUtils.isEmpty(couponStatusEnum)) {
throw new BadRequestException("优惠卷状态出错");
}
return couponService.getCurrentUserCoupon(couponStatusEnum, lastId);
}
/**
* 用户抢卷接口
*/
@PostMapping("/seize")
public void seizeCoupon(@RequestBody SeizeCouponReqDTO seizeCouponReqDTO) {
activityService.seizeCoupon(seizeCouponReqDTO);
}
}

View File

@ -0,0 +1,59 @@
package com.jzo2o.market.controller.inner;
import com.jzo2o.api.market.CouponApi;
import com.jzo2o.api.market.dto.request.CouponUseBackReqDTO;
import com.jzo2o.api.market.dto.request.CouponUseReqDTO;
import com.jzo2o.api.market.dto.response.AvailableCouponsResDTO;
import com.jzo2o.api.market.dto.response.CouponUseResDTO;
import com.jzo2o.market.service.ICouponService;
import com.jzo2o.market.service.ICouponUseBackService;
import com.jzo2o.market.service.ICouponWriteOffService;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
/**
* 内部接口 - 优惠卷管理控制器
* @author JIAN
*/
@Slf4j
@RestController("innerCouponController")
@RequestMapping("/inner/coupon")
public class CouponController implements CouponApi {
@Resource
private ICouponService couponService;
@Resource
private ICouponWriteOffService couponWriteOffService;
@Resource
private ICouponUseBackService couponUseBackService;
/**
* 根据订单金额获取当前用户可用优惠卷
*/
@Override
@GetMapping("/getAvailable")
@ApiOperation("获取可用优惠券列表")
@ApiImplicitParam(name = "totalAmount", value = "总金额,单位分", required = true, dataTypeClass = BigDecimal.class)
public List<AvailableCouponsResDTO> getAvailableCoupon(@RequestParam BigDecimal totalAmount) {
return couponService.getAvailableCoupon(totalAmount);
}
@Override
@PostMapping("/use")
@ApiOperation("使用优惠卷并返回优惠金额")
public CouponUseResDTO useCoupon(@RequestBody CouponUseReqDTO couponUseReqDTO) {
return couponWriteOffService.use(couponUseReqDTO);
}
@Override
@PostMapping("/useBack")
@ApiOperation("优惠券退回接口")
public void useBack(@RequestBody CouponUseBackReqDTO couponUseBackReqDTO) {
couponUseBackService.useBack(couponUseBackReqDTO);
}
}

View File

@ -0,0 +1,43 @@
package com.jzo2o.market.controller.operation;
import com.jzo2o.common.model.PageResult;
import com.jzo2o.market.model.dto.request.ActivityPageQueryDTO;
import com.jzo2o.market.model.dto.request.ActivitySaveReqDTO;
import com.jzo2o.market.model.dto.response.ActivityInfoResDTO;
import com.jzo2o.market.service.IActivityService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 运营端 - 活动管理控制器
* @author JIAN
*/
@Slf4j
@RestController("operationActivityController")
@RequestMapping("/operation/activity")
public class ActivityController {
@Resource
private IActivityService activityService;
@GetMapping("/page")
public PageResult<ActivityInfoResDTO> pageActivity(ActivityPageQueryDTO activityPageQueryDTO) {
return activityService.page(activityPageQueryDTO);
}
@GetMapping("/{id}")
public ActivityInfoResDTO getActivity(@PathVariable Long id) {
return activityService.getDetailById(id);
}
@PostMapping("/save")
public void saveOrUpdateActivity(@RequestBody ActivitySaveReqDTO activitySaveReqDTO) {
activityService.saveOrUpdate(activitySaveReqDTO);
}
@PostMapping("/revoke/{id}")
public void revokeActivity(@PathVariable Long id) {
activityService.revoke(id);
}
}

View File

@ -0,0 +1,29 @@
package com.jzo2o.market.controller.operation;
import com.jzo2o.common.model.PageResult;
import com.jzo2o.market.model.dto.request.CouponPageQueryDTO;
import com.jzo2o.market.model.dto.response.CouponPageInfoResDTO;
import com.jzo2o.market.service.ICouponService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 运营端 - 优惠卷管理控制器
* @author JIAN
*/
@Slf4j
@RestController("operationCouponController")
@RequestMapping("/operation/coupon")
public class CouponController {
@Resource
private ICouponService couponService;
@GetMapping("/page")
public PageResult<CouponPageInfoResDTO> pageCoupon(CouponPageQueryDTO couponPageQueryDTO) {
return couponService.page(couponPageQueryDTO);
}
}

View File

@ -0,0 +1,28 @@
package com.jzo2o.market.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 活动状态枚举类
* @author JIAN
*/
@Getter
@AllArgsConstructor
public enum ActivityStatusEnum {
NO_DISTRIBUTE(1, "待生效"),
DISTRIBUTING(2, "进行中"),
LOSE_EFFICACY(3, "已失效"),
VOIDED(4, "作废");
@EnumValue
@JsonValue
private int status;
private String name;
public boolean equals(Integer status) {
return status != null && this.status == status;
}
}

View File

@ -0,0 +1,39 @@
package com.jzo2o.market.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 优惠卷状态枚举类
* @author JIAN
*/
@Getter
@AllArgsConstructor
public enum CouponStatusEnum {
NO_USE(1, "未使用"),
USED(2, "已使用"),
INVALID(3, "已失效"),
VOIDED(4, "已作废");
@EnumValue
@JsonValue
private int status;
private String name;
public static CouponStatusEnum statusOf(Integer status) {
if (status == null) {
return null;
}
for (CouponStatusEnum couponStatusEnum : CouponStatusEnum.values()) {
if (couponStatusEnum.getStatus() == status) {
return couponStatusEnum;
}
}
// not found
return null;
}
}

View File

@ -0,0 +1,41 @@
package com.jzo2o.market.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 优惠券类型枚举类
* @author JIAN
*/
@Getter
@AllArgsConstructor
public enum CouponTypeEnum {
AMOUNT_DISCOUNT(1, "满减"),
RATE_DISCOUNT(2, "打折");
@EnumValue
@JsonValue
private int type;
private String name;
public boolean equals(Integer type) {
return type != null && type.equals(this.type);
}
public static CouponTypeEnum typeOf(Integer type) {
if (type == null) {
return null;
}
for (CouponTypeEnum couponTypeEnum : CouponTypeEnum.values()) {
if (couponTypeEnum.getType() == type) {
return couponTypeEnum;
}
}
// not found
return null;
}
}

View File

@ -0,0 +1,38 @@
package com.jzo2o.market.handler;
import com.jzo2o.common.expcetions.CommonException;
import com.jzo2o.market.service.ICouponService;
import com.jzo2o.redis.handler.SyncProcessHandler;
import com.jzo2o.redis.model.SyncMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import static com.jzo2o.market.constants.RedisConstants.RedisKey.COUPON_SEIZE_SYNC_QUEUE_NAME;
/**
* 抢卷同步队列同步处理器
* @author JIAN
*/
@Slf4j
@Component(COUPON_SEIZE_SYNC_QUEUE_NAME)
public class SeizeCouponSyncProcessHandler implements SyncProcessHandler<Object> {
@Resource
private ICouponService couponService;
@Override
public void batchProcess(List<SyncMessage<Object>> multiData) {
throw new CommonException("不支持批量处理");
}
@Override
public void singleProcess(SyncMessage<Object> singleData) {
long userId = Long.parseLong(singleData.getKey());
long activityId = Long.parseLong(singleData.getValue().toString());
log.info("同步优惠卷信息, 活动id: {}, 用户id: {}", activityId, userId);
couponService.syncCouponRecord(activityId, userId);
}
}

View File

@ -0,0 +1,77 @@
package com.jzo2o.market.handler;
import com.jzo2o.market.service.IActivityService;
import com.jzo2o.market.service.ICouponService;
import com.jzo2o.redis.sync.SyncManager;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.concurrent.ThreadPoolExecutor;
import static com.jzo2o.market.constants.RedisConstants.RedisKey.COUPON_SEIZE_SYNC_QUEUE_NAME;
import static com.jzo2o.redis.constants.RedisSyncQueueConstants.MODE_SINGLE;
import static com.jzo2o.redis.constants.RedisSyncQueueConstants.STORAGE_TYPE_HASH;
/**
* XxlJob任务处理器
* @author JIAN
*/
@Slf4j
@Component
@SuppressWarnings("unused")
public class XxlJobHandler {
@Resource
private IActivityService activityService;
@Resource
private ICouponService couponService;
@Resource(name = "syncThreadPool")
private ThreadPoolExecutor syncThreadPool;
@Resource
private SyncManager syncManager;
/**
* 自动修改活动状态(1分钟1次)
* 1.活动进行中状态修改
* 2.活动已失效状态修改
*/
@XxlJob("updateActivityStatus")
@Transactional
public void updateActivityStatus() {
log.info("自动修改活动状态任务开始");
activityService.updateActivityStatus();
log.info("自动修改活动状态任务完成");
}
/**
* 自动过期已领取优惠券(1小时1次)
*/
@XxlJob("processExpireCoupon")
public void processExpireCoupon() {
log.info("自动过期已领取优惠券任务开始");
couponService.invalidExpiredCoupon();
log.info("自动过期已领取优惠券任务完成");
}
/**
* 自动预热(缓存)1个月内的活动
*/
@XxlJob("activityPreheat")
public void activityPreheat() {
log.info("自动预热1个月内的活动任务开始");
activityService.cacheComingActivity();
log.info("自动预热1个月内的活动任务完成");
}
/**
* 自动从Redis同步抢卷结果到数据库
*/
@XxlJob("seizeCouponSyncJob")
public void seizeCouponSyncJob() {
log.info("自动从Redis同步抢卷结果到数据库任务开始");
syncManager.start(COUPON_SEIZE_SYNC_QUEUE_NAME, STORAGE_TYPE_HASH, MODE_SINGLE, syncThreadPool);
}
}

View File

@ -0,0 +1,16 @@
package com.jzo2o.market.mapper;
import com.jzo2o.market.model.domain.Activity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author itcast
* @since 2023-09-16
*/
public interface ActivityMapper extends BaseMapper<Activity> {
}

View File

@ -0,0 +1,16 @@
package com.jzo2o.market.mapper;
import com.jzo2o.market.model.domain.Coupon;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author itcast
* @since 2023-09-16
*/
public interface CouponMapper extends BaseMapper<Coupon> {
}

View File

@ -0,0 +1,16 @@
package com.jzo2o.market.mapper;
import com.jzo2o.market.model.domain.CouponUseBack;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 优惠券使用回退记录 Mapper 接口
* </p>
*
* @author itcast
* @since 2023-09-18
*/
public interface CouponUseBackMapper extends BaseMapper<CouponUseBack> {
}

View File

@ -0,0 +1,16 @@
package com.jzo2o.market.mapper;
import com.jzo2o.market.model.domain.CouponWriteOff;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 优惠券核销表 Mapper 接口
* </p>
*
* @author itcast
* @since 2023-09-22
*/
public interface CouponWriteOffMapper extends BaseMapper<CouponWriteOff> {
}

View File

@ -0,0 +1,113 @@
package com.jzo2o.market.model.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.jzo2o.market.enums.ActivityStatusEnum;
import com.jzo2o.market.enums.CouponTypeEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 活动实体类
* @author itcast
* @since 2023-09-16
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Activity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 优惠券配置id
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 优惠券名称可以和活动名称保持一致
*/
private String name;
/**
* 使用类型1满减2折扣
*/
private CouponTypeEnum type;
/**
* 使用条件0表示无门槛其他值最低消费金额
*/
private BigDecimal amountCondition;
/**
* 折扣率折扣类型的折扣率8折就是存80
*/
private Integer discountRate;
/**
* 优惠金额满减或无门槛的优惠金额
*/
private BigDecimal discountAmount;
/**
* 优惠券有效期天数
*/
private Integer validityDays;
/**
* 发放开始时间
*/
private LocalDateTime distributeStartTime;
/**
* 发放结束时间
*/
private LocalDateTime distributeEndTime;
/**
* 优惠券配置状态1待生效2进行中3已失效
*/
private ActivityStatusEnum status;
/**
* 发放数量0表示无限量其他正数表示最大发放量
*/
private Integer totalNum;
/**
* 库存数量
*/
private Integer stockNum;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
private Long createBy;
/**
* 更新人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
/**
* 逻辑删除
*/
private Integer isDeleted;
}

View File

@ -0,0 +1,116 @@
package com.jzo2o.market.model.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.jzo2o.market.enums.CouponTypeEnum;
import com.jzo2o.market.enums.CouponStatusEnum;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* <p>
*
* </p>
* @author itcast
* @since 2023-09-16
*/
@Data
@Builder
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Coupon implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 优惠券id
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 优惠券名称
*/
private String name;
/**
* 优惠券的拥有者
*/
private Long userId;
/**
* 用户姓名
*/
private String userName;
/**
* 用户手机号
*/
private String userPhone;
/**
* 活动id
*/
private Long activityId;
/**
* 使用类型1满减2折扣
*/
private CouponTypeEnum type;
/**
* 折扣
*/
private Integer discountRate;
/**
* 优惠金额
*/
private BigDecimal discountAmount;
/**
* 满减金额
*/
private BigDecimal amountCondition;
/**
* 有效期
*/
private LocalDateTime validityTime;
/**
* 使用时间
*/
private LocalDateTime useTime;
/**
* 优惠券状态1:未使用2:已使用3:已过期
*/
private CouponStatusEnum status;
/**
* 订单id
*/
private String ordersId;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 逻辑删除
*/
private Integer isDeleted;
}

View File

@ -0,0 +1,53 @@
package com.jzo2o.market.model.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 优惠券使用回退记录
* </p>
* @author itcast
* @since 2023-09-18
*/
@Data
@Builder
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class CouponUseBack implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 回退记录id
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 优惠券id
*/
private Long couponId;
/**
* 用户id
*/
private Long userId;
/**
* 回退时间
*/
private LocalDateTime useBackTime;
/**
* 核销时间
*/
private LocalDateTime writeOffTime;
}

View File

@ -0,0 +1,65 @@
package com.jzo2o.market.model.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 优惠券核销表
* </p>
* @author itcast
* @since 2023-09-22
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CouponWriteOff implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 优惠券id
*/
private Long couponId;
/**
* 用户id
*/
private Long userId;
/**
* 核销时使用的订单号
*/
private Long ordersId;
/**
* 活动id
*/
private Long activityId;
/**
* 核销时间
*/
private LocalDateTime writeOffTime;
/**
* 核销人手机号
*/
private String writeOffManPhone;
/**
* 核销人姓名
*/
private String writeOffManName;
}

View File

@ -0,0 +1,21 @@
package com.jzo2o.market.model.dto.request;
import com.jzo2o.common.model.dto.PageQueryDTO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@ApiModel("活动分页查询模型")
@EqualsAndHashCode(callSuper = true)
public class ActivityPageQueryDTO extends PageQueryDTO {
@ApiModelProperty("活动id")
private Long id;
@ApiModelProperty("活动名称")
private String name;
@ApiModelProperty("类型1满减2折扣")
private Integer type;
@ApiModelProperty("优惠券配置状态1待生效2进行中3已失效")
private Integer status;
}

View File

@ -0,0 +1,85 @@
package com.jzo2o.market.model.dto.request;
import com.jzo2o.common.expcetions.BadRequestException;
import com.jzo2o.common.utils.DateUtils;
import com.jzo2o.common.utils.ObjectUtils;
import com.jzo2o.market.enums.CouponTypeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Min;
import javax.validation.constraints.Null;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@ApiModel("活动保存请求模型")
@Validated
public class ActivitySaveReqDTO {
@ApiModelProperty(value = "活动id,新增时不填,修改时必填")
private Long id;
@ApiModelProperty(value = "活动名称", required = true)
@Size(max = 20, message = "活动名称超出20个字符无法输入")
@Null(message = "活动名称为空,请输入活动名称")
private String name;
@ApiModelProperty(value = "优惠券类型1满减2折扣", required = true)
private Integer type;
@ApiModelProperty(value = "满减限额0表示无门槛其他值最低消费金额")
@Min(value = 0, message = "满额限制请输入大于/等于 0的整数")
@Null(message = "满额限制为空,请输入满额限制")
private BigDecimal amountCondition;
@ApiModelProperty(value = "折扣率折扣类型的折扣率例如8,打8折, type为2时必填")
private Integer discountRate;
@ApiModelProperty(value = "优惠金额,满减或无门槛的优惠金额", required = true)
private BigDecimal discountAmount;
@ApiModelProperty(value = "发放开始时间", required = true)
@Null(message = "发放时间为空,请输入发放时间")
private LocalDateTime distributeStartTime;
@Null(message = "发放时间为空,请输入发放时间")
@ApiModelProperty(value = "发放结束时间", required = true)
private LocalDateTime distributeEndTime;
@ApiModelProperty(value = "发放数量0表示无限量其他正数表示最大发放量")
private Integer totalNum = 0;
@ApiModelProperty(value = "有效期天数", required = true)
@Null(message = "使用期限请输入大于0的整数")
@Min(value = 0, message = "使用期限请输入大于0的整数")
private Integer validityDays;
public void check() {
if (CouponTypeEnum.AMOUNT_DISCOUNT.equals(type)) {
// 满减
//discountAmount字段不能为空且值为正数
if (ObjectUtils.isNull(discountAmount)) {
throw new BadRequestException("折扣金额为空,请输入折扣金额");
} else if (discountAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new BadRequestException("折扣金额请输入大于0的整数");
}
} else if (CouponTypeEnum.RATE_DISCOUNT.equals(type)) {
// 折扣
if (ObjectUtils.isNull(discountRate)) {
throw new BadRequestException("折扣比例为空,请输入折扣比例");
} else if (discountRate.compareTo(0) < 0 || discountRate.compareTo(100) > 0) {
throw new BadRequestException("折扣比例请输入大于0小于10的整数");
}
} else {
throw new BadRequestException("优惠券类型不存在");
}
// 发放时间
if (distributeStartTime.isAfter(distributeEndTime)) {
throw new BadRequestException("结束时间不能早于开始时间");
}
if (distributeEndTime.isBefore(DateUtils.now())) {
throw new BadRequestException("发放时间已过期");
}
}
}

View File

@ -0,0 +1,20 @@
package com.jzo2o.market.model.dto.request;
import com.jzo2o.common.model.dto.PageQueryDTO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Null;
@Data
@EqualsAndHashCode(callSuper = true)
@Validated
@ApiModel("运营端优惠券查询模型")
public class CouponPageQueryDTO extends PageQueryDTO {
@ApiModelProperty(value = "活动id",required = true)
@Null(message = "请先选择活动")
private Long activityId;
}

View File

@ -0,0 +1,15 @@
package com.jzo2o.market.model.dto.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.Null;
@Data
@ApiModel
public class SeizeCouponReqDTO {
@ApiModelProperty("活动id")
@Null(message = "请求失败")
private Long id;
}

View File

@ -0,0 +1,47 @@
package com.jzo2o.market.model.dto.response;
import com.jzo2o.market.enums.ActivityStatusEnum;
import com.jzo2o.market.enums.CouponTypeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@ApiModel("活动分页字段模型")
@Accessors(chain = true)
public class ActivityInfoResDTO {
@ApiModelProperty("活动id")
private Long id;
@ApiModelProperty("活动名称")
private String name;
@ApiModelProperty("优惠券类型1满减2折扣")
private CouponTypeEnum type;
@ApiModelProperty("满减限额0表示无门槛其他值最低消费金额")
private BigDecimal amountCondition;
@ApiModelProperty("折扣率折扣类型的折扣率例如8,打8折")
private Integer discountRate;
@ApiModelProperty("优惠金额,满减或无门槛的优惠金额")
private BigDecimal discountAmount;
@ApiModelProperty("发放开始时间")
private LocalDateTime distributeStartTime;
@ApiModelProperty("发放结束时间")
private LocalDateTime distributeEndTime;
@ApiModelProperty("优惠券配置状态1待生效2进行中3已失效")
private ActivityStatusEnum status;
@ApiModelProperty("发放数量0表示无限量其他正数表示最大发放量")
private Integer totalNum;
@ApiModelProperty("领取数量")
private Integer receiveNum;
@ApiModelProperty("核销数量")
private Integer writeOffNum;
@ApiModelProperty("有效期天数")
private Integer validityDays;
@ApiModelProperty("创建时间")
private LocalDateTime createTime;
@ApiModelProperty("更新时间")
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,57 @@
package com.jzo2o.market.model.dto.response;
import com.jzo2o.market.enums.CouponStatusEnum;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 优惠卷分页模型
* @author itcast
* @since 2023-09-16
*/
@Data
public class CouponPageInfoResDTO implements Serializable {
@ApiModelProperty(value = "优惠券id", required = true)
private Long id;
@ApiModelProperty("用户姓名")
private String userName;
@ApiModelProperty("用户手机号")
private String userPhone;
/**
* 活动id
*/
@ApiModelProperty(value = "活动id", required = true)
private Long activityId;
@ApiModelProperty("使用时间")
private LocalDateTime useTime;
/**
* 优惠券状态1:未使用2:已使用3:已过期
*/
@ApiModelProperty("优惠券状态1:未使用2:已使用3:已过期")
private CouponStatusEnum status;
/**
* 订单id
*/
private String ordersId;
/**
* 创建时间
*/
@ApiModelProperty(value = "创建时间", required = true)
private LocalDateTime createTime;
/**
* 更新时间
*/
@ApiModelProperty(value = "更新时间", required = true)
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,48 @@
package com.jzo2o.market.model.dto.response;
import com.jzo2o.market.enums.CouponStatusEnum;
import com.jzo2o.market.enums.CouponTypeEnum;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 优惠卷用户查询模型
* @author itcast
* @since 2023-09-16
*/
@Data
public class CouponSimpleInfoResDTO implements Serializable {
@ApiModelProperty(value = "优惠券id", required = true)
private Long id;
@ApiModelProperty(value = "活动名称", required = true)
private String name;
@ApiModelProperty(value = "活动id", required = true)
private Long activityId;
@ApiModelProperty(value = "使用类型1满减2折扣", required = true)
private CouponTypeEnum type;
@ApiModelProperty(value = "折扣")
private Integer discountRate;
@ApiModelProperty(value = "优惠金额")
private BigDecimal discountAmount;
@ApiModelProperty(value = "满减条件,0:表示无门槛", required = true)
private BigDecimal amountCondition;
@ApiModelProperty("优惠券过期时间")
private LocalDateTime validityTime;
@ApiModelProperty("使用时间")
private LocalDateTime useTime;
@ApiModelProperty("优惠券状态1:未使用2:已使用3:已过期")
private CouponStatusEnum status;
}

View File

@ -0,0 +1,39 @@
package com.jzo2o.market.model.dto.response;
import com.jzo2o.market.enums.ActivityStatusEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@ApiModel("抢券列表信息")
@Accessors(chain = true)
public class SeizeCouponInfoResDTO implements Serializable {
@ApiModelProperty("活动id")
private Long id;
@ApiModelProperty("活动名称")
private String name;
@ApiModelProperty("优惠券类型1满减2折扣")
private Integer type;
@ApiModelProperty("满减限额0表示无门槛其他值最低消费金额")
private BigDecimal amountCondition;
@ApiModelProperty("折扣率折扣类型的折扣率例如8,打8折")
private Integer discountRate;
@ApiModelProperty("优惠金额,满减或无门槛的优惠金额")
private BigDecimal discountAmount;
@ApiModelProperty("发放开始时间")
private LocalDateTime distributeStartTime;
@ApiModelProperty("发放结束时间")
private LocalDateTime distributeEndTime;
@ApiModelProperty("优惠券配置状态1待生效2进行中3已失效")
private ActivityStatusEnum status;
@ApiModelProperty("发放数量")
private Integer totalNum;
@ApiModelProperty("优惠券剩余数量(库存数量)")
private Integer remainNum;
}

View File

@ -0,0 +1,63 @@
package com.jzo2o.market.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jzo2o.common.model.PageResult;
import com.jzo2o.market.enums.ActivityStatusEnum;
import com.jzo2o.market.model.domain.Activity;
import com.jzo2o.market.model.dto.request.ActivityPageQueryDTO;
import com.jzo2o.market.model.dto.request.ActivitySaveReqDTO;
import com.jzo2o.market.model.dto.request.SeizeCouponReqDTO;
import com.jzo2o.market.model.dto.response.ActivityInfoResDTO;
import com.jzo2o.market.model.dto.response.SeizeCouponInfoResDTO;
import java.util.List;
/**
* <p>
* 服务类
* </p>
* @author itcast
* @since 2023-09-16
*/
public interface IActivityService extends IService<Activity> {
/**
* 分页查询活动数据
*/
PageResult<ActivityInfoResDTO> page(ActivityPageQueryDTO activityPageQueryDTO);
/**
* 查询活动详细数据
*/
ActivityInfoResDTO getDetailById(Long id);
/**
* 获取缓存的活动信息
* @param status 筛选的活动状态
*/
List<SeizeCouponInfoResDTO> getCachedActivity(ActivityStatusEnum status);
/**
* 新增/插入活动信息
*/
void saveOrUpdate(ActivitySaveReqDTO activitySaveReqDTO);
/**
* 撤销活动
*/
void revoke(Long id);
/**
* 根据当前时间更新活动状态
*/
void updateActivityStatus();
/**
* 缓存近1个月生效的活动信息
*/
void cacheComingActivity();
/**
* 用户端进行抢卷操作
*/
void seizeCoupon(SeizeCouponReqDTO seizeCouponReqDTO);
}

View File

@ -0,0 +1,54 @@
package com.jzo2o.market.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jzo2o.api.market.dto.response.AvailableCouponsResDTO;
import com.jzo2o.common.model.PageResult;
import com.jzo2o.market.enums.CouponStatusEnum;
import com.jzo2o.market.model.domain.Coupon;
import com.jzo2o.market.model.dto.request.CouponPageQueryDTO;
import com.jzo2o.market.model.dto.response.CouponPageInfoResDTO;
import com.jzo2o.market.model.dto.response.CouponSimpleInfoResDTO;
import java.math.BigDecimal;
import java.util.List;
/**
* <p>
* 服务类
* </p>
* @author itcast
* @since 2023-09-16
*/
public interface ICouponService extends IService<Coupon> {
/**
* 分页查询优惠卷信息(通过活动id)
*/
PageResult<CouponPageInfoResDTO> page(CouponPageQueryDTO couponPageQueryDTO);
/**
* 用户端滚动查询(抢卷时间降序)
*/
List<CouponSimpleInfoResDTO> getCurrentUserCoupon(CouponStatusEnum status, Integer lastId);
/**
* 更新过期优惠卷的状态为失效中
*/
void invalidExpiredCoupon();
/**
* 同步优惠卷数据到数据库中
*/
void syncCouponRecord(long activityId, long userId);
/**
* 根据订单金额获取当前用户可用优惠卷
*/
List<AvailableCouponsResDTO> getAvailableCoupon(BigDecimal totalAmount);
/**
* 计算优惠卷的优惠价格
* @param coupon 优惠卷信息
* @param totalAmount 订单总金额
*/
BigDecimal calcDiscountAmount(Coupon coupon, BigDecimal totalAmount);
}

View File

@ -0,0 +1,19 @@
package com.jzo2o.market.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jzo2o.api.market.dto.request.CouponUseBackReqDTO;
import com.jzo2o.market.model.domain.CouponUseBack;
/**
* <p>
* 优惠券使用回退记录 服务类
* </p>
* @author itcast
* @since 2023-09-18
*/
public interface ICouponUseBackService extends IService<CouponUseBack> {
/**
* 用户退回优惠卷
*/
void useBack(CouponUseBackReqDTO couponUseBackReqDTO);
}

View File

@ -0,0 +1,20 @@
package com.jzo2o.market.service;
import com.jzo2o.api.market.dto.request.CouponUseReqDTO;
import com.jzo2o.api.market.dto.response.CouponUseResDTO;
import com.jzo2o.market.model.domain.CouponWriteOff;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 优惠券核销表 服务类
* </p>
* @author itcast
* @since 2023-09-22
*/
public interface ICouponWriteOffService extends IService<CouponWriteOff> {
/**
* 核销指定的优惠卷并返回优惠金额
*/
CouponUseResDTO use(CouponUseReqDTO couponUseReqDTO);
}

View File

@ -0,0 +1,296 @@
package com.jzo2o.market.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers;
import com.jzo2o.common.constants.ErrorInfo;
import com.jzo2o.common.expcetions.CommonException;
import com.jzo2o.common.expcetions.DBException;
import com.jzo2o.common.expcetions.ForbiddenOperationException;
import com.jzo2o.common.model.PageResult;
import com.jzo2o.common.utils.BeanUtils;
import com.jzo2o.common.utils.CollUtils;
import com.jzo2o.common.utils.JsonUtils;
import com.jzo2o.market.constants.RedisConstants;
import com.jzo2o.market.enums.ActivityStatusEnum;
import com.jzo2o.market.enums.CouponStatusEnum;
import com.jzo2o.market.enums.CouponTypeEnum;
import com.jzo2o.market.mapper.ActivityMapper;
import com.jzo2o.market.model.domain.Activity;
import com.jzo2o.market.model.domain.Coupon;
import com.jzo2o.market.model.dto.request.ActivityPageQueryDTO;
import com.jzo2o.market.model.dto.request.ActivitySaveReqDTO;
import com.jzo2o.market.model.dto.request.SeizeCouponReqDTO;
import com.jzo2o.market.model.dto.response.ActivityInfoResDTO;
import com.jzo2o.market.model.dto.response.SeizeCouponInfoResDTO;
import com.jzo2o.market.service.IActivityService;
import com.jzo2o.market.service.ICouponService;
import com.jzo2o.mvc.utils.UserContext;
import com.jzo2o.mysql.utils.PageUtils;
import com.jzo2o.redis.properties.RedisSyncProperties;
import com.jzo2o.redis.utils.RedisSyncQueueUtils;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.jzo2o.market.constants.RedisConstants.RedisKey.*;
/**
* <p>
* 服务实现类
* </p>
* @author itcast
* @since 2023-09-16
*/
@Service
public class ActivityServiceImpl extends ServiceImpl<ActivityMapper, Activity> implements IActivityService {
@Resource
private ICouponService couponService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource(name = "seizeCouponScript")
private RedisScript<Integer> seizeCouponScript;
@Resource
private RedisSyncProperties redisSyncProperties;
@Override
@SuppressWarnings("unchecked")
public PageResult<ActivityInfoResDTO> page(ActivityPageQueryDTO activityPageQueryDTO) {
// 分页查询
Page<Activity> activityPage = PageUtils.parsePageQuery(activityPageQueryDTO, Activity.class);
LambdaQueryWrapper<Activity> queryWrapper = Wrappers.<Activity>lambdaQuery()
.eq(ObjectUtils.isNotEmpty(activityPageQueryDTO.getId()), Activity::getId, activityPageQueryDTO.getId())
.like(ObjectUtils.isNotEmpty(activityPageQueryDTO.getName()), Activity::getName, activityPageQueryDTO.getName())
.eq(ObjectUtils.isNotEmpty(activityPageQueryDTO.getType()), Activity::getType, activityPageQueryDTO.getType())
.eq(ObjectUtils.isNotEmpty(activityPageQueryDTO.getStatus()), Activity::getStatus, activityPageQueryDTO.getStatus())
// 新创建的活动显示在列表前面
.orderByDesc(Activity::getCreateTime);
Page<Activity> pageAns = baseMapper.selectPage(activityPage, queryWrapper);
return PageUtils.toPage(pageAns, ActivityInfoResDTO.class);
}
@Override
public ActivityInfoResDTO getDetailById(Long id) {
Activity activity = baseMapper.selectById(id);
if (ObjectUtils.isEmpty(activity)) {
return new ActivityInfoResDTO();
}
ActivityInfoResDTO activityInfoResDTO = BeanUtils.toBean(activity, ActivityInfoResDTO.class);
List<Coupon> coupons = couponService.lambdaQuery()
.eq(Coupon::getActivityId, id)
.select(Coupon::getStatus)
.list();
activityInfoResDTO.setReceiveNum(coupons.size());
activityInfoResDTO.setWriteOffNum((int) coupons.stream()
.filter(coupon -> coupon.getStatus() == CouponStatusEnum.USED)
.count());
return activityInfoResDTO;
}
@Override
public List<SeizeCouponInfoResDTO> getCachedActivity(ActivityStatusEnum status) {
List<SeizeCouponInfoResDTO> couponInfoList = JsonUtils.toList((String) redisTemplate.opsForValue()
.get(ACTIVITY_CACHE_LIST), SeizeCouponInfoResDTO.class);
if (CollUtils.isEmpty(couponInfoList)) {
return new ArrayList<>();
}
LocalDateTime nowTime = LocalDateTime.now();
return couponInfoList.stream()
.peek(coupon -> {
// 防止缓存中的状态出错
if (coupon.getDistributeEndTime().isBefore(nowTime)) {
coupon.setStatus(ActivityStatusEnum.LOSE_EFFICACY);
} else if (coupon.getDistributeStartTime().isBefore(nowTime)) {
coupon.setStatus(ActivityStatusEnum.DISTRIBUTING);
}
})
.filter(coupon -> coupon.getStatus() == status)
.collect(Collectors.toList());
}
@Override
@Transactional
public void saveOrUpdate(ActivitySaveReqDTO activitySaveReqDTO) {
// 参数检验
activitySaveReqDTO.check();
Activity activity = BeanUtils.toBean(activitySaveReqDTO, Activity.class);
activity.setType(CouponTypeEnum.typeOf(activitySaveReqDTO.getType()));
if (ObjectUtils.isEmpty(activity.getId())) {
// 新增记录初始化相关状态
activity.setStatus(ActivityStatusEnum.NO_DISTRIBUTE)
.setStockNum(activity.getTotalNum());
} else {
// 更新需要检查活动状态
Activity activityInDb = baseMapper.selectById(activity.getId());
if (ObjectUtils.isEmpty(activityInDb)) {
throw new ForbiddenOperationException("活动不存在无法修改");
} else if (activityInDb.getStatus() != ActivityStatusEnum.NO_DISTRIBUTE) {
throw new ForbiddenOperationException("活动状态错误无法修改");
}
}
if (!this.saveOrUpdate(activity)) {
throw new DBException("新增/更新活动信息失败");
}
}
@Override
@Transactional
public void revoke(Long id) {
Activity activityInDb = baseMapper.selectById(id);
if (ObjectUtils.isEmpty(activityInDb)) {
throw new ForbiddenOperationException("活动不存在无法撤销");
} else if (activityInDb.getStatus() != ActivityStatusEnum.NO_DISTRIBUTE
&& activityInDb.getStatus() != ActivityStatusEnum.DISTRIBUTING) {
throw new ForbiddenOperationException("活动状态错误无法撤销");
}
if (!lambdaUpdate()
.eq(Activity::getId, id)
.set(Activity::getStatus, ActivityStatusEnum.VOIDED)
.update()) {
throw new DBException("更新活动表失败");
}
// 优惠卷可能没有发放 -> 更新0行
ChainWrappers.lambdaUpdateChain(couponService.getBaseMapper())
.eq(Coupon::getActivityId, id)
.set(Coupon::getStatus, CouponStatusEnum.VOIDED)
.update();
}
@Override
public void updateActivityStatus() {
LocalDateTime nowTime = LocalDateTime.now();
List<Activity> activityList = lambdaQuery()
.in(Activity::getStatus, ActivityStatusEnum.NO_DISTRIBUTE, ActivityStatusEnum.DISTRIBUTING)
// 不处理还没有发生的活动
.le(Activity::getDistributeStartTime, nowTime)
.list();
if (CollUtils.isEmpty(activityList)) {
return;
}
for (Activity activity : activityList) {
ActivityStatusEnum status;
if (activity.getDistributeEndTime().isBefore(nowTime)) {
// 活动结束
status = ActivityStatusEnum.LOSE_EFFICACY;
} else if (activity.getDistributeStartTime().isBefore(nowTime)) {
// 活动开始
status = ActivityStatusEnum.DISTRIBUTING;
} else {
continue;
}
if (!lambdaUpdate()
.eq(Activity::getId, activity.getId())
.set(Activity::getStatus, status)
.update()) {
throw new DBException("更新活动状态失败");
}
}
}
@Override
public void cacheComingActivity() {
LocalDateTime nowTime = LocalDateTime.now();
// 获取近一个月未开始/已开始的活动
@SuppressWarnings("unchecked")
List<Activity> activityList = lambdaQuery()
.le(Activity::getDistributeStartTime, nowTime.plusDays(30))
.in(Activity::getStatus, Arrays.asList(ActivityStatusEnum.NO_DISTRIBUTE, ActivityStatusEnum.DISTRIBUTING))
.orderByAsc(Activity::getDistributeStartTime)
.list();
if (CollUtils.isEmpty(activityList)) {
activityList = new ArrayList<>();
}
List<SeizeCouponInfoResDTO> couponInfoList = activityList.stream()
.map(activity -> BeanUtils
.toBean(activity, SeizeCouponInfoResDTO.class)
.setRemainNum(activity.getStockNum())
.setType(activity.getType().getType()))
.collect(Collectors.toList());
// 缓存活动列表
redisTemplate.opsForValue().set(RedisConstants.RedisKey.ACTIVITY_CACHE_LIST, JsonUtils.toJsonStr(couponInfoList));
// 缓存优惠卷库存
HashOperations<String, Object, Object> hashOperations = redisTemplate.opsForHash();
couponInfoList.forEach(coupon -> {
String key = String.format(RedisConstants.RedisKey.COUPON_RESOURCE_STOCK, coupon.getId() % redisSyncProperties.getQueueNum());
// 防止进行中的活动的库存被覆盖
if (coupon.getStatus() == ActivityStatusEnum.NO_DISTRIBUTE) {
hashOperations.put(key, coupon.getId(), coupon.getRemainNum());
} else {
hashOperations.putIfAbsent(key, coupon.getId(), coupon.getRemainNum());
}
});
}
@Override
public void seizeCoupon(SeizeCouponReqDTO seizeCouponReqDTO) {
Long activityId = seizeCouponReqDTO.getId();
Activity activity = lambdaQuery()
.eq(Activity::getId, activityId)
.select(Activity::getDistributeStartTime, Activity::getDistributeEndTime)
.one();
int seizeCouponFailedCode = ErrorInfo.Code.SEIZE_COUPON_FAILD;
if (ObjectUtils.isEmpty(activity)) {
throw new CommonException(seizeCouponFailedCode, "活动不存在无法抢卷");
} else if (activity.getDistributeStartTime().isAfter(LocalDateTime.now())) {
throw new CommonException(seizeCouponFailedCode, "活动未开始无法抢卷");
} else if (activity.getDistributeEndTime().isBefore(LocalDateTime.now())) {
throw new CommonException(seizeCouponFailedCode, "活动已结束无法抢卷");
}
Long userId = UserContext.currentUserId();
if (ObjectUtils.isEmpty(userId)) {
throw new CommonException(seizeCouponFailedCode, "用户信息不存在无法抢卷");
}
int activityTag = (int) (activityId % redisSyncProperties.getQueueNum());
String syncQueueKey = RedisSyncQueueUtils.getQueueRedisKey(COUPON_SEIZE_SYNC_QUEUE_NAME, activityTag);
String stockTable = String.format(COUPON_RESOURCE_STOCK, activityTag);
String seizeSuccessList = String.format(COUPON_SEIZE_LIST, activityId, activityTag);
List<String> keys = Arrays.asList(syncQueueKey, stockTable, seizeSuccessList);
// 调用抢卷脚本
int state = Optional
.ofNullable(redisTemplate.execute(seizeCouponScript, keys, activityId, userId))
.orElseThrow(() -> new CommonException(seizeCouponFailedCode, "抢卷失败"));
if (state < 0) {
String failMag = state == -1 ? "请勿重复抢卷" :
(state == -2 || state == -4 ? "库存不足抢卷失败" : "内部错误抢卷失败");
throw new CommonException(seizeCouponFailedCode, failMag);
}
}
}

View File

@ -0,0 +1,197 @@
package com.jzo2o.market.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jzo2o.api.customer.CommonUserApi;
import com.jzo2o.api.customer.dto.response.CommonUserResDTO;
import com.jzo2o.api.market.dto.response.AvailableCouponsResDTO;
import com.jzo2o.common.expcetions.BadRequestException;
import com.jzo2o.common.expcetions.CommonException;
import com.jzo2o.common.expcetions.DBException;
import com.jzo2o.common.model.PageResult;
import com.jzo2o.common.utils.BeanUtils;
import com.jzo2o.common.utils.CollUtils;
import com.jzo2o.common.utils.ObjectUtils;
import com.jzo2o.market.enums.CouponStatusEnum;
import com.jzo2o.market.enums.CouponTypeEnum;
import com.jzo2o.market.mapper.CouponMapper;
import com.jzo2o.market.model.domain.Activity;
import com.jzo2o.market.model.domain.Coupon;
import com.jzo2o.market.model.dto.request.CouponPageQueryDTO;
import com.jzo2o.market.model.dto.response.CouponPageInfoResDTO;
import com.jzo2o.market.model.dto.response.CouponSimpleInfoResDTO;
import com.jzo2o.market.service.IActivityService;
import com.jzo2o.market.service.ICouponService;
import com.jzo2o.mvc.utils.UserContext;
import com.jzo2o.mysql.utils.PageUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* <p>
* 服务实现类
* </p>
* @author itcast
* @since 2023-09-16
*/
@Service
@Slf4j
public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> implements ICouponService {
@Resource
private CommonUserApi commonUserApi;
@Resource
private IActivityService activityService;
@Override
public PageResult<CouponPageInfoResDTO> page(CouponPageQueryDTO couponPageQueryDTO) {
if (ObjectUtils.isEmpty(couponPageQueryDTO.getActivityId())) {
return new PageResult<>(0L, 0L, new ArrayList<>());
}
Page<Coupon> couponPage = PageUtils.parsePageQuery(couponPageQueryDTO, Coupon.class);
LambdaQueryWrapper<Coupon> queryWrapper = Wrappers.<Coupon>lambdaQuery()
.eq(Coupon::getActivityId, couponPageQueryDTO.getActivityId());
return PageUtils.toPage(baseMapper.selectPage(couponPage, queryWrapper), CouponPageInfoResDTO.class);
}
@Override
@SuppressWarnings("unchecked")
public List<CouponSimpleInfoResDTO> getCurrentUserCoupon(CouponStatusEnum status, Integer lastId) {
Long userId = UserContext.currentUserId();
if (ObjectUtils.isEmpty(userId)) {
throw new BadRequestException("用户未授权");
}
List<Coupon> coupons = lambdaQuery()
.eq(Coupon::getUserId, userId)
.eq(Coupon::getStatus, status)
.lt(ObjectUtils.isNotEmpty(lastId), Coupon::getId, lastId)
.orderByDesc(Coupon::getCreateTime)
.last("LIMIT 10")
.list();
return CollUtils.isEmpty(coupons) ? new ArrayList<>() :
coupons.stream()
.map(coupon -> BeanUtils.toBean(coupon, CouponSimpleInfoResDTO.class))
.collect(Collectors.toList());
}
@Override
@Transactional
public void invalidExpiredCoupon() {
List<Coupon> couponList = lambdaQuery()
.eq(Coupon::getStatus, CouponStatusEnum.NO_USE)
// 不处理还没到有效期的优惠卷
.le(Coupon::getValidityTime, LocalDateTime.now())
.select(Coupon::getId)
.list();
if (CollUtils.isEmpty(couponList)) {
return;
}
// 设置状态已失效
couponList = couponList.stream()
.map(coupon -> coupon.setStatus(CouponStatusEnum.INVALID))
.collect(Collectors.toList());
if (!this.updateBatchById(couponList)) {
throw new DBException("更新优惠卷状态失败");
}
}
@Override
@Transactional
public void syncCouponRecord(long activityId, long userId) {
Activity activity = activityService.getById(activityId);
CommonUserResDTO user = commonUserApi.findById(userId);
if (!this.save(Coupon.builder()
.userId(userId)
.userName(user.getNickname())
.userPhone(user.getPhone())
.activityId(activityId)
.name(activity.getName())
.type(activity.getType())
.amountCondition(activity.getAmountCondition())
.discountRate(activity.getDiscountRate())
.discountAmount(activity.getDiscountAmount())
.validityTime(LocalDateTime.now().plusDays(activity.getValidityDays()))
.status(CouponStatusEnum.NO_USE)
.build())) {
throw new DBException("插入优惠卷表失败");
}
if (!activityService.lambdaUpdate()
.setSql("stock_num = stock_num - 1")
.eq(Activity::getId, activityId)
.gt(Activity::getStockNum, 0)
.update()) {
throw new DBException("更新活动库存失败");
}
}
@Override
public List<AvailableCouponsResDTO> getAvailableCoupon(BigDecimal totalAmount) {
Long userId = Optional
.ofNullable(UserContext.currentUserId())
.orElseThrow(() -> new CommonException("用户信息不存在"));
List<Coupon> couponList = lambdaQuery()
.eq(Coupon::getUserId, userId)
.le(Coupon::getAmountCondition, totalAmount)
.eq(Coupon::getStatus, CouponStatusEnum.NO_USE)
.ge(Coupon::getValidityTime, LocalDateTime.now())
.and(and -> and
.isNull(Coupon::getDiscountAmount)
.or(or -> or.le(Coupon::getDiscountAmount, totalAmount)))
.list();
if (CollUtils.isEmpty(couponList)) {
return new ArrayList<>();
}
return couponList.stream()
.map(coupon -> BeanUtils
.toBean(coupon, AvailableCouponsResDTO.class)
.setType(coupon.getType().getType())
.setDiscountAmount(this.calcDiscountAmount(coupon, totalAmount)))
// 默认BigDecimal的比较器排序从小到大 需要按优惠金额从大到小排序
.sorted(Comparator.comparing(AvailableCouponsResDTO::getDiscountAmount).reversed())
.collect(Collectors.toList());
}
/**
* 计算优惠卷的优惠价格
* @param coupon 优惠卷信息
* @param totalAmount 订单总金额
*/
@Override
public BigDecimal calcDiscountAmount(Coupon coupon, BigDecimal totalAmount) {
CouponTypeEnum type = coupon.getType();
BigDecimal discountAmount = BigDecimal.ZERO;
if (type == CouponTypeEnum.AMOUNT_DISCOUNT) {
discountAmount = coupon.getDiscountAmount();
} else if (type == CouponTypeEnum.RATE_DISCOUNT) {
// 1 <= coupon.getDiscountRate() <= 99
BigDecimal discountRate = new BigDecimal(String.format("0.%02d", 100 - coupon.getDiscountRate()));
discountAmount = totalAmount.multiply(discountRate);
}
return discountAmount;
}
}

View File

@ -0,0 +1,69 @@
package com.jzo2o.market.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jzo2o.api.market.dto.request.CouponUseBackReqDTO;
import com.jzo2o.common.expcetions.BadRequestException;
import com.jzo2o.common.expcetions.DBException;
import com.jzo2o.common.utils.ObjectUtils;
import com.jzo2o.market.enums.CouponStatusEnum;
import com.jzo2o.market.mapper.CouponUseBackMapper;
import com.jzo2o.market.model.domain.Coupon;
import com.jzo2o.market.model.domain.CouponUseBack;
import com.jzo2o.market.service.ICouponService;
import com.jzo2o.market.service.ICouponUseBackService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* <p>
* 优惠券使用回退记录 服务实现类
* </p>
* @author itcast
* @since 2023-09-18
*/
@Service
public class CouponUseBackServiceImpl extends ServiceImpl<CouponUseBackMapper, CouponUseBack> implements ICouponUseBackService {
@Resource
private ICouponService couponService;
@Override
@Transactional
public void useBack(CouponUseBackReqDTO couponUseBackReqDTO) {
Long couponId = couponUseBackReqDTO.getId();
Long userId = couponUseBackReqDTO.getUserId();
Long ordersId = couponUseBackReqDTO.getOrdersId();
Coupon coupon = couponService.getById(couponId);
if (ObjectUtils.isEmpty(coupon) || ObjectUtils.notEqual(coupon.getUserId(), userId)) {
throw new BadRequestException("优惠卷不存在");
} else if (coupon.getStatus() != CouponStatusEnum.USED) {
throw new BadRequestException("优惠卷未使用");
} else if (ObjectUtils.notEqual(coupon.getOrdersId(), ordersId)) {
throw new BadRequestException("优惠券对应订单错误");
}
LocalDateTime nowTime = LocalDateTime.now();
CouponStatusEnum status = coupon.getValidityTime().isAfter(nowTime) ? CouponStatusEnum.NO_USE : CouponStatusEnum.INVALID;
if (!couponService.lambdaUpdate()
.eq(Coupon::getId, couponId)
.set(Coupon::getUseTime, null)
.set(Coupon::getStatus, status)
.set(Coupon::getOrdersId, null)
.update()) {
throw new DBException("更新优惠卷表失败");
}
if (!this.save(CouponUseBack.builder()
.couponId(couponId)
.userId(userId)
.useBackTime(nowTime)
.writeOffTime(coupon.getUseTime())
.build())) {
throw new DBException("更新退回表失败");
}
}
}

View File

@ -0,0 +1,77 @@
package com.jzo2o.market.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jzo2o.api.market.dto.request.CouponUseReqDTO;
import com.jzo2o.api.market.dto.response.CouponUseResDTO;
import com.jzo2o.common.expcetions.BadRequestException;
import com.jzo2o.common.expcetions.DBException;
import com.jzo2o.common.utils.ObjectUtils;
import com.jzo2o.market.enums.CouponStatusEnum;
import com.jzo2o.market.mapper.CouponWriteOffMapper;
import com.jzo2o.market.model.domain.Coupon;
import com.jzo2o.market.model.domain.CouponWriteOff;
import com.jzo2o.market.service.ICouponService;
import com.jzo2o.market.service.ICouponWriteOffService;
import com.jzo2o.mvc.utils.UserContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* <p>
* 优惠券核销表 服务实现类
* </p>
* @author itcast
* @since 2023-09-22
*/
@Service
public class CouponWriteOffServiceImpl extends ServiceImpl<CouponWriteOffMapper, CouponWriteOff> implements ICouponWriteOffService {
@Resource
private ICouponService couponService;
@Override
@Transactional
public CouponUseResDTO use(CouponUseReqDTO couponUseReqDTO) {
Long couponId = couponUseReqDTO.getId();
BigDecimal totalAmount = couponUseReqDTO.getTotalAmount();
Long userId = UserContext.currentUserId();
LocalDateTime nowTime = LocalDateTime.now();
Coupon coupon = couponService.getById(couponId);
if (ObjectUtils.isEmpty(coupon) || ObjectUtils.notEqual(coupon.getUserId(), userId)) {
throw new BadRequestException("优惠卷不存在");
} else if (coupon.getStatus() != CouponStatusEnum.NO_USE
|| coupon.getValidityTime().isBefore(nowTime)) {
throw new BadRequestException("优惠卷已使用/已失效");
} else if (coupon.getAmountCondition().compareTo(totalAmount) > 0) {
throw new BadRequestException("订单金额未达到满减金额");
}
Long ordersId = couponUseReqDTO.getOrdersId();
if (!couponService.lambdaUpdate()
.eq(Coupon::getId, couponId)
.set(Coupon::getUseTime, nowTime)
.set(Coupon::getStatus, CouponStatusEnum.USED)
.set(Coupon::getOrdersId, ordersId)
.update()) {
throw new DBException("更新优惠卷表失败");
}
if (!this.save(CouponWriteOff.builder()
.couponId(couponId)
.userId(userId)
.ordersId(ordersId)
.activityId(coupon.getActivityId())
.writeOffTime(nowTime)
.writeOffManName(coupon.getUserName())
.writeOffManPhone(coupon.getUserPhone())
.build())) {
throw new DBException("更新核销表失败");
}
return new CouponUseResDTO(couponService.calcDiscountAmount(coupon, totalAmount));
}
}

View File

@ -0,0 +1,18 @@
spring:
cloud:
nacos:
username: nacos
password: nacos
server-addr: 192.168.122.135:8848
config:
namespace: 75a593f5-33e6-4c65-b2a0-18c403d20f63
file-extension: yaml
discovery:
namespace: 75a593f5-33e6-4c65-b2a0-18c403d20f63
ip: ${ACCESS_IP:}
################# 日志配置 #################
logging:
level:
com.jzo2o: debug
org.mongodb.driver: info

View File

@ -0,0 +1,14 @@
spring:
cloud:
nacos:
username: ${NACOS_USERNAME}
password: ${NACOS_PASSWORD}
server-addr: ${NACOS_ADDR}
config:
namespace: ${NACOS_NAMESPACE}
file-extension: yaml
discovery:
namespace: ${NACOS_NAMESPACE}
logging:
level:
com.jzo2o: debug

View File

@ -0,0 +1,14 @@
spring:
cloud:
nacos:
username: ${NACOS_USERNAME}
password: ${NACOS_PASSWORD}
server-addr: ${NACOS_ADDR}
config:
namespace: ${NACOS_NAMESPACE}
file-extension: yaml
discovery:
namespace: ${NACOS_NAMESPACE}
logging:
level:
com.jzo2o: debug

View File

@ -0,0 +1,76 @@
################# 服务器配置 #################
server:
port: 11510
undertow:
accesslog:
enabled: true
pattern: "%t %a &quot;%r&quot; %s (%D ms)"
dir: /data/logs/undertow/${spring.application.name}/access-logs/
servlet:
context-path: /market
################# spring公共配置 #################
spring:
mvc:
path-match:
matching-strategy: ant_path_matcher
format:
date: yyyy-MM-dd HH:mm:ss
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
profiles:
active: dev
application:
name: jzo2o-market
main:
# 支持循环依赖注入
allow-circular-references: true
# bean名相同覆盖
allow-bean-definition-overriding: true
cloud:
nacos:
username: ${NACOS_USERNAME}
password: ${NACOS_PASSWORD}
server-addr: ${NACOS_ADDR}
discovery:
namespace: ${NACOS_NAMESPACE}
config:
namespace: ${NACOS_NAMESPACE}
file-extension: yaml
shared-configs: # 共享配置
- data-id: shared-redis-cluster.yaml # 共享redis集群配置
refresh: false
- data-id: shared-xxl-job.yaml # xxl-job配置
refresh: false
- data-id: shared-rabbitmq.yaml # rabbitmq配置
refresh: false
- data-id: shared-mysql.yaml # mysql配置
refresh: false
# - data-id: shared-spring-seata.yaml # seata
# refresh: false
################# 项目独有配置 #################
mysql:
db-name: jzo2o-market
mybatis:
mapper-locations: mapper/*.xml
type-aliases-package: com.jzo2o.market.mapper
swagger:
enable: true
package-path: com.jzo2o.market.controller
title: 家政服务-促销中心接口文档
description: 用于活动优惠券的管理和使用
contact-name: 传智教育·研究院
contact-url: http://www.itcast.cn/
contact-email: yjy@itcast.cn
version: v1.0
################# 日志配置 #################
logging:
level:
com.jzo2o: debug
org.mongodb.driver: info
feign:
enable: true

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jzo2o.market.mapper.ActivityMapper">
</mapper>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jzo2o.market.mapper.CouponMapper">
</mapper>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jzo2o.market.mapper.CouponUseBackMapper">
</mapper>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jzo2o.market.mapper.CouponWriteOffMapper">
</mapper>

View File

@ -0,0 +1,36 @@
-- 抢券lua实现
-- key: 抢券同步队列, 资源库存, 抢券成功列表
-- argv活动id, 用户id
-- 优惠券是否已经抢过
local couponNum = redis.call("HGET", KEYS[3], ARGV[2])
-- hget 获取不到数据返回false而不是nil
if couponNum ~= false and tonumber(couponNum) >= 1
then
return "-1"; -- 已抢卷
end
-- 库存是否充足校验
local stockNum = redis.call("HGET", KEYS[2], ARGV[1])
if stockNum == false or tonumber(stockNum) < 1
then
return "-2"; -- 库存不足抢卷失败
end
-- 抢券成功列表
local listNum = redis.call("HSET", KEYS[3], ARGV[2], 1)
if listNum == false or tonumber(listNum) < 1
then
return "-3"; -- 写入抢卷成功列表失败
end
-- 减少库存
stockNum = redis.call("HINCRBY", KEYS[2], ARGV[1], -1)
if tonumber(stockNum) < 0
then
return "-4" -- 库存不足抢卷失败
end
-- 抢卷结果写入同步队列
local result = redis.call("HSETNX", KEYS[1], ARGV[2], ARGV[1])
if result > 0
then
return "100" -- 抢卷成功返回活动id
end
return "-5" -- 写入同步队列失败

View File

@ -1,5 +1,6 @@
package com.jzo2o.orders.manager.controller.consumer;
import com.jzo2o.api.market.dto.response.AvailableCouponsResDTO;
import com.jzo2o.api.orders.dto.request.OrderCancelReqDTO;
import com.jzo2o.api.orders.dto.response.OrderResDTO;
import com.jzo2o.api.orders.dto.response.OrderSimpleResDTO;
@ -104,4 +105,15 @@ public class ConsumerOrdersController {
.currentUserType(currentUser.getUserType())
.build());
}
@GetMapping("/getAvailableCoupons")
@ApiOperation("获取可用优惠券")
@ApiImplicitParams({
@ApiImplicitParam(name = "serveId", value = "服务id", required = true, dataTypeClass = Integer.class),
@ApiImplicitParam(name = "purNum", value = "购买数量默认1", dataTypeClass = Long.class)
})
public List<AvailableCouponsResDTO> getAvailableCoupons(@RequestParam Long serveId,
@RequestParam(required = false, defaultValue = "1") Integer purNum) {
return ordersCreateService.getAvailableCoupons(serveId, purNum);
}
}

View File

@ -41,7 +41,7 @@ public class OrderCancelJob {
// 取消所有超时未支付订单
transactionTemplate.executeWithoutResult(status ->
orderList.forEach(order -> ordersCanceledService.cancelPayOverTimeOrder(order.getId())));
orderList.forEach(order -> ordersCanceledService.cancelPayOverTimeOrder(order)));
}
@XxlJob("handlerRefundOrder")

View File

@ -1,6 +1,7 @@
package com.jzo2o.orders.manager.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jzo2o.orders.base.model.domain.Orders;
import com.jzo2o.orders.base.model.domain.OrdersCanceled;
import com.jzo2o.orders.manager.model.dto.OrderCancelDTO;
@ -22,5 +23,5 @@ public interface IOrdersCanceledService extends IService<OrdersCanceled> {
* 系统取消超时订单(无前置判断)
* <br><b>仅内部使用!!!</b>
*/
void cancelPayOverTimeOrder(Long id);
void cancelPayOverTimeOrder(Orders order);
}

View File

@ -1,12 +1,15 @@
package com.jzo2o.orders.manager.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jzo2o.api.market.dto.response.AvailableCouponsResDTO;
import com.jzo2o.orders.base.model.domain.Orders;
import com.jzo2o.orders.manager.model.dto.request.OrdersPayReqDTO;
import com.jzo2o.orders.manager.model.dto.request.PlaceOrderReqDTO;
import com.jzo2o.orders.manager.model.dto.response.OrdersPayResDTO;
import com.jzo2o.orders.manager.model.dto.response.PlaceOrderResDTO;
import java.util.List;
/**
* <p>
* 下单服务类
@ -35,4 +38,9 @@ public interface IOrdersCreateService extends IService<Orders> {
* 客户端获取支付状态
*/
OrdersPayResDTO getPayResult(Long id);
/**
* 客户端获取可用优惠卷
*/
List<AvailableCouponsResDTO> getAvailableCoupons(Long serveId, Integer purNum);
}

View File

@ -0,0 +1,38 @@
package com.jzo2o.orders.manager.service.client;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.jzo2o.api.foundations.ServeApi;
import com.jzo2o.api.foundations.dto.response.ServeAggregationResDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 自定义Feign客户端用于熔断降级
* @author JIAN
*/
@Slf4j
@Component
@SuppressWarnings("unused")
public class FoundationClient {
@Resource
private ServeApi serveApi;
@SentinelResource(value = "serveById",
fallback = "getServeByIdFallback", blockHandler = "getServeByIdBlockHandler")
public ServeAggregationResDTO getServeById(Long id) {
return serveApi.findById(id);
}
public ServeAggregationResDTO getServeByIdFallback(Long id, Throwable throwable) {
log.warn("服务信息接口异常(未触发熔断), 服务id: {}", id, throwable);
return null;
}
public ServeAggregationResDTO getServeByIdBlockHandler(Long id, BlockException blockException) {
log.warn("服务信息接口异常(触发熔断降级), 服务id: {}", id, blockException);
return null;
}
}

View File

@ -0,0 +1,58 @@
package com.jzo2o.orders.manager.service.client;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.jzo2o.api.market.CouponApi;
import com.jzo2o.api.market.dto.request.CouponUseReqDTO;
import com.jzo2o.api.market.dto.response.AvailableCouponsResDTO;
import com.jzo2o.api.market.dto.response.CouponUseResDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
/**
* 自定义Feign客户端用于熔断降级
* @author JIAN
*/
@Slf4j
@Component
@SuppressWarnings("unused")
public class MarketClient {
@Resource
private CouponApi couponApi;
@SentinelResource(value = "availableCoupon",
fallback = "getAvailableCouponFallback", blockHandler = "getAvailableCouponBlockHandler")
public List<AvailableCouponsResDTO> getAvailableCoupon(BigDecimal totalAmount) {
return couponApi.getAvailableCoupon(totalAmount);
}
public List<AvailableCouponsResDTO> getAvailableCouponFallback(BigDecimal totalAmount, Throwable throwable) {
log.warn("优惠卷接口异常(未触发熔断), 总金额: {}", totalAmount, throwable);
return null;
}
public List<AvailableCouponsResDTO> getAvailableCouponBlockHandler(BigDecimal totalAmount, BlockException blockException) {
log.warn("优惠卷接口异常(触发熔断降级), 总金额: {}", totalAmount, blockException);
return null;
}
@SentinelResource(value = "useCoupon",
fallback = "useCouponFallback", blockHandler = "useCouponBlockHandler")
public CouponUseResDTO useCoupon(CouponUseReqDTO couponUseReqDTO) {
return couponApi.useCoupon(couponUseReqDTO);
}
public CouponUseResDTO useCouponFallback(CouponUseReqDTO couponUseReqDTO, Throwable throwable) {
log.warn("优惠卷接口异常(未触发熔断), 相关信息: {}", couponUseReqDTO, throwable);
return null;
}
public CouponUseResDTO useCouponBlockHandler(CouponUseReqDTO couponUseReqDTO, BlockException blockException) {
log.warn("优惠卷接口异常(触发熔断降级), 相关信息: {}", couponUseReqDTO, blockException);
return null;
}
}

View File

@ -74,18 +74,24 @@ public class OrdersCanceledServiceImpl extends ServiceImpl<OrdersCanceledMapper,
}
@Override
public void cancelPayOverTimeOrder(Long id) {
public void cancelPayOverTimeOrder(Orders order) {
// 二次确认防止在此期间支付
TradingResDTO tradingResDTO = tradingApi.findTradResultByTradingOrderNo(id);
if (ObjectUtils.isEmpty(tradingResDTO) || tradingResDTO.getTradingState() != TradingStateEnum.YJS) {
cancelNoPayOrder(OrderCancelDTO.builder()
.id(id)
.cancelReason("订单超时未支付自动取消")
.currentUserId(-1L)
.currentUserName("SYSTEM")
.currentUserType(UserType.SYSTEM)
.build());
Long tradingOrderNo = order.getTradingOrderNo();
if (ObjectUtils.isNotEmpty(tradingOrderNo)) {
// 再次请求防止已支付
TradingResDTO tradingResDTO = tradingApi.findTradResultByTradingOrderNo(tradingOrderNo);
if (ObjectUtils.isNotEmpty(tradingResDTO) && tradingResDTO.getTradingState() == TradingStateEnum.YJS) {
return;
}
}
cancelNoPayOrder(OrderCancelDTO.builder()
.id(order.getId())
.cancelReason("订单超时未支付自动取消")
.currentUserId(-1L)
.currentUserName("SYSTEM")
.currentUserType(UserType.SYSTEM)
.build());
}
private void cancelNoPayOrder(OrderCancelDTO orderCancelDTO) {

View File

@ -3,6 +3,9 @@ package com.jzo2o.orders.manager.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jzo2o.api.customer.dto.response.AddressBookResDTO;
import com.jzo2o.api.foundations.dto.response.ServeAggregationResDTO;
import com.jzo2o.api.market.dto.request.CouponUseReqDTO;
import com.jzo2o.api.market.dto.response.AvailableCouponsResDTO;
import com.jzo2o.api.market.dto.response.CouponUseResDTO;
import com.jzo2o.api.trade.NativePayApi;
import com.jzo2o.api.trade.TradingApi;
import com.jzo2o.api.trade.dto.request.NativePayReqDTO;
@ -10,6 +13,7 @@ import com.jzo2o.api.trade.dto.response.NativePayResDTO;
import com.jzo2o.api.trade.dto.response.TradingResDTO;
import com.jzo2o.api.trade.enums.PayChannelEnum;
import com.jzo2o.api.trade.enums.TradingStateEnum;
import com.jzo2o.common.expcetions.BadRequestException;
import com.jzo2o.common.expcetions.CommonException;
import com.jzo2o.common.expcetions.ForbiddenOperationException;
import com.jzo2o.common.utils.BeanUtils;
@ -30,6 +34,8 @@ import com.jzo2o.orders.manager.model.dto.response.PlaceOrderResDTO;
import com.jzo2o.orders.manager.porperties.TradeProperties;
import com.jzo2o.orders.manager.service.IOrdersCreateService;
import com.jzo2o.orders.manager.service.client.CustomerClient;
import com.jzo2o.orders.manager.service.client.FoundationClient;
import com.jzo2o.orders.manager.service.client.MarketClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@ -38,6 +44,9 @@ import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* <p>
@ -49,6 +58,10 @@ import java.time.LocalDateTime;
@Slf4j
@Service
public class OrdersCreateServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements IOrdersCreateService {
@Resource
private MarketClient marketClient;
@Resource
private FoundationClient foundationClient;
@Resource
private CustomerClient customerClient;
@Resource
@ -87,10 +100,22 @@ public class OrdersCreateServiceImpl extends ServiceImpl<OrdersMapper, Orders> i
}
// 获取订单id
Long orderId = generateOrderId();
// TODO 获取优惠卷相关信息
BigDecimal discountAmount = BigDecimal.ZERO;
// 计算价格
// 计算总金额
BigDecimal totalAmount = serve.getPrice().multiply(BigDecimal.valueOf(placeOrderReqDTO.getPurNum()));
// 计算优惠卷相关金额
BigDecimal discountAmount = BigDecimal.ZERO;
Long couponId = placeOrderReqDTO.getCouponId();
if (ObjectUtils.isNotEmpty(couponId)) {
CouponUseResDTO couponUseResDTO = Optional
.ofNullable(marketClient.useCoupon(CouponUseReqDTO.builder()
.ordersId(orderId)
.id(couponId)
.totalAmount(totalAmount)
.build()))
.orElseThrow(() -> new ForbiddenOperationException("优惠卷核销失败下单失败"));
discountAmount = couponUseResDTO.getDiscountAmount();
}
// 计算实际金额
BigDecimal realPayAmount = totalAmount.subtract(discountAmount);
// 组装订单信息插入数据库完成下单
@ -224,4 +249,17 @@ public class OrdersCreateServiceImpl extends ServiceImpl<OrdersMapper, Orders> i
return ordersPayResDTO;
}
@Override
public List<AvailableCouponsResDTO> getAvailableCoupons(Long serveId, Integer purNum) {
ServeAggregationResDTO serve = foundationClient.getServeById(serveId);
if (ObjectUtils.isEmpty(serve) || serve.getSaleStatus() != 2) {
throw new BadRequestException("服务不可用");
}
BigDecimal totalAmount = serve.getPrice().multiply(BigDecimal.valueOf(purNum));
return Optional
.ofNullable(marketClient.getAvailableCoupon(totalAmount))
.orElseGet(ArrayList::new);
}
}

View File

@ -93,7 +93,7 @@ public class OrdersManagerServiceImpl extends ServiceImpl<OrdersMapper, Orders>
// 订单超过15分钟未支付则自动取消
if (OrderStatusEnum.NO_PAY.getStatus().equals(orders.getOrdersStatus())
&& orders.getCreateTime().isBefore(LocalDateTime.now().minusMinutes(PAY_OVERTIME_MINUTE))) {
ordersCanceledService.cancelPayOverTimeOrder(id);
ordersCanceledService.cancelPayOverTimeOrder(orders);
orderResDTO.setOrdersStatus(OrderStatusEnum.CANCELED.getStatus());
}