并入后台管理端(vue)和小程序端的代码(template-10550)

This commit is contained in:
2025-08-08 23:04:17 +08:00
parent a0f4fcc03f
commit b0f9eaefa1
1452 changed files with 241138 additions and 0 deletions

View File

@@ -0,0 +1,178 @@
package com.gxwebsoft.shop.controller;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.shop.entity.ShopUserCoupon;
import com.gxwebsoft.shop.service.CouponBusinessService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 优惠券业务控制器
*
* @author 科技小王子
* @since 2025-08-08 22:30:00
*/
@Tag(name = "优惠券业务管理")
@RestController
@RequestMapping("/api/shop/coupon-business")
public class CouponBusinessController extends BaseController {
@Resource
private CouponBusinessService couponBusinessService;
@Operation(summary = "获取订单可用优惠券")
@PostMapping("/available-for-order")
public ApiResult<List<ShopUserCoupon>> getAvailableCouponsForOrder(
@RequestBody Map<String, Object> orderData) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录", null);
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> goodsItems = (List<Map<String, Object>>) orderData.get("goodsItems");
BigDecimal totalAmount = new BigDecimal(orderData.get("totalAmount").toString());
List<ShopUserCoupon> coupons = couponBusinessService.getAvailableCouponsForOrder(
loginUser.getUserId(), goodsItems, totalAmount);
return success(coupons);
}
@Operation(summary = "计算使用优惠券后的订单金额")
@PostMapping("/calculate-order-amount")
public ApiResult<Map<String, Object>> calculateOrderAmountWithCoupon(
@RequestParam Long userCouponId,
@RequestBody Map<String, Object> orderData) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> goodsItems = (List<Map<String, Object>>) orderData.get("goodsItems");
BigDecimal totalAmount = new BigDecimal(orderData.get("totalAmount").toString());
Map<String, Object> result = couponBusinessService.calculateOrderAmountWithCoupon(
userCouponId, goodsItems, totalAmount);
return success(result);
}
@Operation(summary = "验证优惠券是否可用于订单")
@PostMapping("/validate-for-order")
public ApiResult<Map<String, Object>> validateCouponForOrder(
@RequestParam Long userCouponId,
@RequestBody Map<String, Object> orderData) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录", null);
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> goodsItems = (List<Map<String, Object>>) orderData.get("goodsItems");
BigDecimal totalAmount = new BigDecimal(orderData.get("totalAmount").toString());
Map<String, Object> result = couponBusinessService.validateCouponForOrder(
userCouponId, loginUser.getUserId(), goodsItems, totalAmount);
return success(result);
}
@Operation(summary = "推荐最优优惠券组合")
@PostMapping("/recommend-best-combination")
public ApiResult<Map<String, Object>> recommendBestCouponCombination(
@RequestBody Map<String, Object> orderData) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录", null);
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> goodsItems = (List<Map<String, Object>>) orderData.get("goodsItems");
BigDecimal totalAmount = new BigDecimal(orderData.get("totalAmount").toString());
Map<String, Object> result = couponBusinessService.recommendBestCouponCombination(
loginUser.getUserId(), goodsItems, totalAmount);
return success(result);
}
@PreAuthorize("hasAuthority('shop:coupon:manage')")
@OperationLog
@Operation(summary = "为新用户发放欢迎优惠券")
@PostMapping("/issue-welcome/{userId}")
public ApiResult<?> issueWelcomeCoupons(@PathVariable Integer userId) {
int count = couponBusinessService.issueWelcomeCoupons(userId);
return success("成功发放" + count + "张欢迎优惠券");
}
@PreAuthorize("hasAuthority('shop:coupon:manage')")
@OperationLog
@Operation(summary = "为用户发放生日优惠券")
@PostMapping("/issue-birthday/{userId}")
public ApiResult<?> issueBirthdayCoupons(@PathVariable Integer userId) {
int count = couponBusinessService.issueBirthdayCoupons(userId);
return success("成功发放" + count + "张生日优惠券");
}
@PreAuthorize("hasAuthority('shop:coupon:manage')")
@OperationLog
@Operation(summary = "根据消费金额发放优惠券")
@PostMapping("/issue-consume/{userId}")
public ApiResult<?> issueConsumeCoupons(@PathVariable Integer userId,
@RequestParam BigDecimal consumeAmount) {
int count = couponBusinessService.issueConsumeCoupons(userId, consumeAmount);
return success("成功发放" + count + "张消费返券");
}
@PreAuthorize("hasAuthority('shop:coupon:manage')")
@OperationLog
@Operation(summary = "批量发放活动优惠券")
@PostMapping("/batch-issue-activity")
public ApiResult<Map<String, Object>> batchIssueActivityCoupons(
@RequestParam String activityName,
@RequestParam List<Integer> couponIds,
@RequestParam(required = false) List<Integer> userIds) {
Map<String, Object> result = couponBusinessService.batchIssueActivityCoupons(
activityName, couponIds, userIds);
return success(result);
}
@PreAuthorize("hasAuthority('shop:coupon:statistics')")
@Operation(summary = "获取优惠券使用统计")
@GetMapping("/usage-statistics")
public ApiResult<Map<String, Object>> getCouponUsageStatistics(
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
Map<String, Object> statistics = couponBusinessService.getCouponUsageStatistics(startDate, endDate);
return success(statistics);
}
@PreAuthorize("hasAuthority('shop:coupon:manage')")
@OperationLog
@Operation(summary = "手动处理过期优惠券")
@PostMapping("/process-expired")
public ApiResult<?> processExpiredCoupons() {
int count = couponBusinessService.autoProcessExpiredCoupons();
return success("处理了" + count + "张过期优惠券");
}
@PreAuthorize("hasAuthority('shop:coupon:manage')")
@OperationLog
@Operation(summary = "发送优惠券到期提醒")
@PostMapping("/send-expiry-reminder")
public ApiResult<?> sendExpiryReminder(@RequestParam(defaultValue = "3") Integer days) {
int count = couponBusinessService.sendCouponExpiryReminder(days);
return success("" + count + "个用户发送了到期提醒");
}
}

View File

@@ -0,0 +1,126 @@
package com.gxwebsoft.shop.controller;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.shop.entity.ShopCoupon;
import com.gxwebsoft.shop.param.ShopCouponParam;
import com.gxwebsoft.shop.service.ShopCouponService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 优惠券控制器
*
* @author 科技小王子
* @since 2025-08-08 21:10:55
*/
@Tag(name = "优惠券管理")
@RestController
@RequestMapping("/api/shop/shop-coupon")
public class ShopCouponController extends BaseController {
@Resource
private ShopCouponService shopCouponService;
@PreAuthorize("hasAuthority('shop:shopCoupon:list')")
@Operation(summary = "批量修改优惠券")
@GetMapping("/page")
public ApiResult<PageResult<ShopCoupon>> page(ShopCouponParam param) {
// 使用关联查询
return success(shopCouponService.pageRel(param));
}
@PreAuthorize("hasAuthority('shop:shopCoupon:list')")
@Operation(summary = "查询全部优惠券")
@GetMapping()
public ApiResult<List<ShopCoupon>> list(ShopCouponParam param) {
// 使用关联查询
return success(shopCouponService.listRel(param));
}
@PreAuthorize("hasAuthority('shop:shopCoupon:list')")
@Operation(summary = "根据id查询优惠券")
@GetMapping("/{id}")
public ApiResult<ShopCoupon> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(shopCouponService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('shop:shopCoupon:save')")
@OperationLog
@Operation(summary = "添加优惠券")
@PostMapping()
public ApiResult<?> save(@RequestBody ShopCoupon shopCoupon) {
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
shopCoupon.setUserId(loginUser.getUserId());
}
if (shopCouponService.save(shopCoupon)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('shop:shopCoupon:update')")
@OperationLog
@Operation(summary = "修改优惠券")
@PutMapping()
public ApiResult<?> update(@RequestBody ShopCoupon shopCoupon) {
if (shopCouponService.updateById(shopCoupon)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('shop:shopCoupon:remove')")
@OperationLog
@Operation(summary = "删除优惠券")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (shopCouponService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('shop:shopCoupon:save')")
@OperationLog
@Operation(summary = "批量添加优惠券")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ShopCoupon> list) {
if (shopCouponService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('shop:shopCoupon:update')")
@Operation(summary = "批量修改优惠券")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ShopCoupon> batchParam) {
if (batchParam.update(shopCouponService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('shop:shopCoupon:remove')")
@Operation(summary = "批量删除优惠券")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (shopCouponService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -0,0 +1,225 @@
package com.gxwebsoft.shop.controller;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.shop.entity.ShopUserCoupon;
import com.gxwebsoft.shop.param.ShopUserCouponParam;
import com.gxwebsoft.shop.service.ShopUserCouponService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 用户优惠券控制器
*
* @author 科技小王子
* @since 2025-08-08 21:30:00
*/
@Tag(name = "用户优惠券管理")
@RestController
@RequestMapping("/api/shop/user-coupon")
public class ShopUserCouponController extends BaseController {
@Resource
private ShopUserCouponService shopUserCouponService;
@PreAuthorize("hasAuthority('shop:userCoupon:list')")
@Operation(summary = "分页查询用户优惠券")
@GetMapping("/page")
public ApiResult<PageResult<ShopUserCoupon>> page(ShopUserCouponParam param) {
return success(shopUserCouponService.pageRel(param));
}
@PreAuthorize("hasAuthority('shop:userCoupon:list')")
@Operation(summary = "查询用户优惠券列表")
@GetMapping()
public ApiResult<List<ShopUserCoupon>> list(ShopUserCouponParam param) {
return success(shopUserCouponService.listRel(param));
}
@PreAuthorize("hasAuthority('shop:userCoupon:list')")
@Operation(summary = "根据id查询用户优惠券")
@GetMapping("/{id}")
public ApiResult<ShopUserCoupon> get(@PathVariable("id") Long id) {
return success(shopUserCouponService.getByIdRel(id));
}
@Operation(summary = "获取当前用户的优惠券列表")
@GetMapping("/my")
public ApiResult<List<ShopUserCoupon>> getMyCoupons(ShopUserCouponParam param) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录",null);
}
param.setUserId(loginUser.getUserId());
return success(shopUserCouponService.listRel(param));
}
@Operation(summary = "获取当前用户可用的优惠券")
@GetMapping("/my/available")
public ApiResult<List<ShopUserCoupon>> getMyAvailableCoupons(
@RequestParam(required = false) Integer goodsId,
@RequestParam(required = false) Integer categoryId,
@RequestParam(required = false) BigDecimal orderAmount) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录",null);
}
List<ShopUserCoupon> coupons = shopUserCouponService.getAvailableCoupons(
loginUser.getUserId(), goodsId, categoryId, orderAmount);
return success(coupons);
}
@Operation(summary = "统计当前用户优惠券数量")
@GetMapping("/my/count")
public ApiResult<Map<String, Object>> getMyCount() {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录",null);
}
Map<String, Object> count = shopUserCouponService.countUserCoupons(loginUser.getUserId());
return success(count);
}
@OperationLog
@Operation(summary = "领取优惠券")
@PostMapping("/receive/{couponId}")
public ApiResult<?> receiveCoupon(@PathVariable("couponId") Integer couponId) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录",null);
}
// 检查是否可以领取
Map<String, Object> checkResult = shopUserCouponService.checkCanReceiveCoupon(
loginUser.getUserId(), couponId);
if (!(Boolean) checkResult.get("canReceive")) {
return fail(checkResult.get("reason").toString());
}
boolean success = shopUserCouponService.receiveCoupon(loginUser.getUserId(), couponId);
if (success) {
return success("领取成功");
}
return fail("领取失败");
}
@PreAuthorize("hasAuthority('shop:userCoupon:save')")
@OperationLog
@Operation(summary = "系统发放优惠券给用户")
@PostMapping("/issue")
public ApiResult<?> issueCoupon(@RequestParam Integer userId,
@RequestParam Integer couponId,
@RequestParam(required = false) String source) {
boolean success = shopUserCouponService.issueCouponToUser(userId, couponId, source);
if (success) {
return success("发放成功");
}
return fail("发放失败");
}
@PreAuthorize("hasAuthority('shop:userCoupon:save')")
@OperationLog
@Operation(summary = "批量发放优惠券")
@PostMapping("/batch-issue")
public ApiResult<?> batchIssueCoupons(@RequestParam List<Integer> userIds,
@RequestParam Integer couponId,
@RequestParam(required = false) String source) {
int successCount = shopUserCouponService.batchIssueCoupons(userIds, couponId, source);
return success("成功发放" + successCount + "张优惠券");
}
@PreAuthorize("hasAuthority('shop:userCoupon:update')")
@OperationLog
@Operation(summary = "使用优惠券")
@PutMapping("/use")
public ApiResult<?> useCoupon(@RequestParam Long userCouponId,
@RequestParam Long orderId,
@RequestParam String orderNo) {
boolean success = shopUserCouponService.useCoupon(userCouponId, orderId, orderNo);
if (success) {
return success("使用成功");
}
return fail("使用失败");
}
@PreAuthorize("hasAuthority('shop:userCoupon:update')")
@OperationLog
@Operation(summary = "退还优惠券")
@PutMapping("/return/{orderId}")
public ApiResult<?> returnCoupon(@PathVariable("orderId") Long orderId) {
boolean success = shopUserCouponService.returnCoupon(orderId);
if (success) {
return success("退还成功");
}
return fail("退还失败");
}
@Operation(summary = "计算优惠券优惠金额")
@PostMapping("/calculate-discount")
public ApiResult<BigDecimal> calculateDiscount(@RequestParam Long userCouponId,
@RequestParam BigDecimal orderAmount) {
ShopUserCoupon userCoupon = shopUserCouponService.getById(userCouponId);
if (userCoupon == null) {
return fail("优惠券不存在",null);
}
BigDecimal discountAmount = shopUserCouponService.calculateDiscountAmount(userCoupon, orderAmount);
return success(discountAmount);
}
@Operation(summary = "验证优惠券是否可用于指定商品")
@GetMapping("/validate")
public ApiResult<Boolean> validateCoupon(@RequestParam Long userCouponId,
@RequestParam(required = false) Integer goodsId,
@RequestParam(required = false) Integer categoryId) {
ShopUserCoupon userCoupon = shopUserCouponService.getById(userCouponId);
if (userCoupon == null) {
return fail("优惠券不存在",null);
}
boolean valid = shopUserCouponService.validateCouponForGoods(userCoupon, goodsId, categoryId);
return success(valid);
}
@PreAuthorize("hasAuthority('shop:userCoupon:update')")
@OperationLog
@Operation(summary = "更新过期优惠券状态")
@PutMapping("/update-expired")
public ApiResult<?> updateExpiredCoupons() {
int count = shopUserCouponService.updateExpiredCoupons();
return success("更新了" + count + "张过期优惠券");
}
@PreAuthorize("hasAuthority('shop:userCoupon:list')")
@Operation(summary = "获取即将过期的优惠券")
@GetMapping("/expiring-soon")
public ApiResult<List<ShopUserCoupon>> getExpiringSoonCoupons(
@RequestParam(defaultValue = "3") Integer days) {
List<ShopUserCoupon> coupons = shopUserCouponService.getExpiringSoonCoupons(days);
return success(coupons);
}
@PreAuthorize("hasAuthority('shop:userCoupon:remove')")
@OperationLog
@Operation(summary = "删除用户优惠券")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Long id) {
if (shopUserCouponService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -0,0 +1,103 @@
package com.gxwebsoft.shop.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Date;
/**
* 优惠券模板
*
* @author 科技小王子
* @since 2025-08-08 21:10:55
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("shop_coupon")
@Schema(name = "ShopCoupon对象", description = "优惠券模板")
public class ShopCoupon implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "优惠券名称")
private String name;
@Schema(description = "优惠券描述")
private String description;
@Schema(description = "优惠券类型(10满减券 20折扣券 30免费劵)")
private Integer type;
@Schema(description = "满减券-减免金额")
private BigDecimal reducePrice;
@Schema(description = "折扣券-折扣率(0-100)")
private Integer discount;
@Schema(description = "最低消费金额")
private BigDecimal minPrice;
@Schema(description = "发放总数量(-1表示无限制)")
private Integer totalCount;
@Schema(description = "已发放数量")
private Integer issuedCount;
@Schema(description = "每人限领数量(-1表示无限制)")
private Integer limitPerUser;
@Schema(description = "到期类型(10领取后生效 20固定时间)")
private Integer expireType;
@Schema(description = "领取后生效-有效天数")
private Integer expireDay;
@Schema(description = "有效期开始时间")
private LocalDate startTime;
@Schema(description = "有效期结束时间")
private LocalDate endTime;
@Schema(description = "适用范围(10全部商品 20指定商品 30指定分类)")
private Integer applyRange;
@Schema(description = "适用范围配置(json格式)")
private String applyRangeConfig;
@Schema(description = "是否启用(0禁用 1启用)")
private Integer enabled;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;
@Schema(description = "状态, 0正常, 1禁用")
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;
@Schema(description = "创建用户ID")
private Integer userId;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
private Date createTime;
@Schema(description = "修改时间")
private Date updateTime;
}

View File

@@ -0,0 +1,142 @@
package com.gxwebsoft.shop.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 用户优惠券
*
* @author 科技小王子
* @since 2025-08-08 21:30:00
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("shop_user_coupon")
@Schema(name = "ShopUserCoupon对象", description = "用户优惠券")
public class ShopUserCoupon implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "id")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "优惠券模板ID")
private Integer couponId;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "优惠券名称")
private String name;
@Schema(description = "优惠券描述")
private String description;
@Schema(description = "优惠券类型(10满减券 20折扣券 30免费劵)")
private Integer type;
@Schema(description = "满减券-减免金额")
private BigDecimal reducePrice;
@Schema(description = "折扣券-折扣率(0-100)")
private Integer discount;
@Schema(description = "最低消费金额")
private BigDecimal minPrice;
@Schema(description = "适用范围(10全部商品 20指定商品 30指定分类)")
private Integer applyRange;
@Schema(description = "适用范围配置(json格式)")
private String applyRangeConfig;
@Schema(description = "有效期开始时间")
private LocalDateTime startTime;
@Schema(description = "有效期结束时间")
private LocalDateTime endTime;
@Schema(description = "使用状态(0未使用 1已使用 2已过期)")
private Integer status;
@Schema(description = "使用时间")
private LocalDateTime useTime;
@Schema(description = "使用订单ID")
private Long orderId;
@Schema(description = "使用订单号")
private String orderNo;
@Schema(description = "获取方式(10主动领取 20系统发放 30活动赠送)")
private Integer obtainType;
@Schema(description = "获取来源描述")
private String obtainSource;
@Schema(description = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
private Date createTime;
@Schema(description = "修改时间")
private Date updateTime;
// 优惠券状态常量
public static final int STATUS_UNUSED = 0; // 未使用
public static final int STATUS_USED = 1; // 已使用
public static final int STATUS_EXPIRED = 2; // 已过期
// 优惠券类型常量
public static final int TYPE_REDUCE = 10; // 满减券
public static final int TYPE_DISCOUNT = 20; // 折扣券
public static final int TYPE_FREE = 30; // 免费券
// 适用范围常量
public static final int APPLY_ALL = 10; // 全部商品
public static final int APPLY_GOODS = 20; // 指定商品
public static final int APPLY_CATEGORY = 30; // 指定分类
// 获取方式常量
public static final int OBTAIN_RECEIVE = 10; // 主动领取
public static final int OBTAIN_SYSTEM = 20; // 系统发放
public static final int OBTAIN_ACTIVITY = 30; // 活动赠送
/**
* 检查优惠券是否已过期
*/
public boolean isExpired() {
return this.endTime != null && this.endTime.isBefore(LocalDateTime.now());
}
/**
* 检查优惠券是否可用
*/
public boolean isAvailable() {
return this.status == STATUS_UNUSED && !isExpired();
}
/**
* 检查优惠券是否在有效期内
*/
public boolean isInValidPeriod() {
LocalDateTime now = LocalDateTime.now();
return (this.startTime == null || !this.startTime.isAfter(now)) &&
(this.endTime == null || !this.endTime.isBefore(now));
}
}

View File

@@ -0,0 +1,46 @@
package com.gxwebsoft.shop.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.shop.entity.ShopCoupon;
import com.gxwebsoft.shop.param.ShopCouponParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 优惠券Mapper
*
* @author 科技小王子
* @since 2025-08-08 21:10:55
*/
public interface ShopCouponMapper extends BaseMapper<ShopCoupon> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<ShopCoupon>
*/
List<ShopCoupon> selectPageRel(@Param("page") IPage<ShopCoupon> page,
@Param("param") ShopCouponParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<ShopCoupon> selectListRel(@Param("param") ShopCouponParam param);
/**
* 检查用户是否已领取指定优惠券
*
* @param userId 用户ID
* @param couponId 优惠券模板ID
* @return 已领取数量
*/
int countUserReceivedCoupon(@Param("userId") Integer userId, @Param("couponId") Integer couponId);
}

View File

@@ -0,0 +1,76 @@
package com.gxwebsoft.shop.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.shop.entity.ShopUserCoupon;
import com.gxwebsoft.shop.param.ShopUserCouponParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 用户优惠券Mapper
*
* @author 科技小王子
* @since 2025-08-08 21:30:00
*/
public interface ShopUserCouponMapper extends BaseMapper<ShopUserCoupon> {
/**
* 分页查询用户优惠券
*
* @param page 分页对象
* @param param 查询参数
* @return List<ShopUserCoupon>
*/
List<ShopUserCoupon> selectPageRel(@Param("page") IPage<ShopUserCoupon> page,
@Param("param") ShopUserCouponParam param);
/**
* 查询用户优惠券列表
*
* @param param 查询参数
* @return List<ShopUserCoupon>
*/
List<ShopUserCoupon> selectListRel(@Param("param") ShopUserCouponParam param);
/**
* 查询用户可用的优惠券
*
* @param param 查询参数
* @return List<ShopUserCoupon>
*/
List<ShopUserCoupon> selectAvailableCoupons(@Param("param") ShopUserCouponParam param);
/**
* 统计用户优惠券数量
*
* @param userId 用户ID
* @return 统计结果
*/
java.util.Map<String, Object> countUserCoupons(@Param("userId") Integer userId);
/**
* 查询即将过期的优惠券
*
* @param days 天数
* @return List<ShopUserCoupon>
*/
List<ShopUserCoupon> selectExpiringSoon(@Param("days") Integer days);
/**
* 批量更新过期状态
*
* @return 更新数量
*/
int updateExpiredStatus();
/**
* 检查用户是否已领取指定优惠券
*
* @param userId 用户ID
* @param couponId 优惠券模板ID
* @return 已领取数量
*/
int countUserReceivedCoupon(@Param("userId") Integer userId, @Param("couponId") Integer couponId);
}

View File

@@ -0,0 +1,96 @@
<?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.gxwebsoft.shop.mapper.ShopCouponMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*
FROM shop_coupon a
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.name != null">
AND a.name LIKE CONCAT('%', #{param.name}, '%')
</if>
<if test="param.type != null">
AND a.type = #{param.type}
</if>
<if test="param.reducePrice != null">
AND a.reduce_price = #{param.reducePrice}
</if>
<if test="param.discount != null">
AND a.discount = #{param.discount}
</if>
<if test="param.minPrice != null">
AND a.min_price = #{param.minPrice}
</if>
<if test="param.expireType != null">
AND a.expire_type = #{param.expireType}
</if>
<if test="param.expireDay != null">
AND a.expire_day = #{param.expireDay}
</if>
<if test="param.startTime != null">
AND a.start_time LIKE CONCAT('%', #{param.startTime}, '%')
</if>
<if test="param.endTime != null">
AND a.end_time LIKE CONCAT('%', #{param.endTime}, '%')
</if>
<if test="param.applyRange != null">
AND a.apply_range = #{param.applyRange}
</if>
<if test="param.applyRangeConfig != null">
AND a.apply_range_config LIKE CONCAT('%', #{param.applyRangeConfig}, '%')
</if>
<if test="param.isExpire != null">
AND a.is_expire = #{param.isExpire}
</if>
<if test="param.sortNumber != null">
AND a.sort_number = #{param.sortNumber}
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.deleted != null">
AND a.deleted = #{param.deleted}
</if>
<if test="param.deleted == null">
AND a.deleted = 0
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.keywords != null">
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.shop.entity.ShopCoupon">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.shop.entity.ShopCoupon">
<include refid="selectSql"></include>
</select>
<!-- 检查用户是否已领取指定优惠券 -->
<select id="countUserReceivedCoupon" resultType="int">
SELECT COUNT(*)
FROM shop_user_coupon
WHERE deleted = 0
AND user_id = #{userId}
AND coupon_id = #{couponId}
</select>
</mapper>

View File

@@ -0,0 +1,240 @@
<?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.gxwebsoft.shop.mapper.ShopUserCouponMapper">
<!-- 基础查询SQL -->
<sql id="selectSql">
SELECT
a.id,
a.coupon_id,
a.user_id,
a.name,
a.description,
a.type,
a.reduce_price,
a.discount,
a.min_price,
a.apply_range,
a.apply_range_config,
a.start_time,
a.end_time,
a.status,
a.use_time,
a.order_id,
a.order_no,
a.obtain_type,
a.obtain_source,
a.deleted,
a.tenant_id,
a.create_time,
a.update_time,
u.nickname as user_nickname,
u.phone as user_phone
FROM shop_user_coupon a
LEFT JOIN user u ON a.user_id = u.user_id
<where>
a.deleted = 0
<if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId}
</if>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.couponId != null">
AND a.coupon_id = #{param.couponId}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.name != null and param.name != ''">
AND a.name LIKE CONCAT('%', #{param.name}, '%')
</if>
<if test="param.type != null">
AND a.type = #{param.type}
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.applyRange != null">
AND a.apply_range = #{param.applyRange}
</if>
<if test="param.obtainType != null">
AND a.obtain_type = #{param.obtainType}
</if>
<if test="param.orderId != null">
AND a.order_id = #{param.orderId}
</if>
<if test="param.orderNo != null and param.orderNo != ''">
AND a.order_no = #{param.orderNo}
</if>
<if test="param.startTimeBegin != null">
AND a.start_time &gt;= #{param.startTimeBegin}
</if>
<if test="param.startTimeEnd != null">
AND a.start_time &lt;= #{param.startTimeEnd}
</if>
<if test="param.endTimeBegin != null">
AND a.end_time &gt;= #{param.endTimeBegin}
</if>
<if test="param.endTimeEnd != null">
AND a.end_time &lt;= #{param.endTimeEnd}
</if>
<if test="param.useTimeBegin != null">
AND a.use_time &gt;= #{param.useTimeBegin}
</if>
<if test="param.useTimeEnd != null">
AND a.use_time &lt;= #{param.useTimeEnd}
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.includeExpired != null and !param.includeExpired">
AND (a.end_time IS NULL OR a.end_time &gt; NOW())
</if>
<if test="param.onlyAvailable != null and param.onlyAvailable">
AND a.status = 0
AND (a.start_time IS NULL OR a.start_time &lt;= NOW())
AND (a.end_time IS NULL OR a.end_time &gt; NOW())
</if>
<if test="param.keywords != null and param.keywords != ''">
AND (a.name LIKE CONCAT('%', #{param.keywords}, '%')
OR a.description LIKE CONCAT('%', #{param.keywords}, '%')
OR u.nickname LIKE CONCAT('%', #{param.keywords}, '%')
OR u.phone LIKE CONCAT('%', #{param.keywords}, '%'))
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.shop.entity.ShopUserCoupon">
<include refid="selectSql"/>
ORDER BY a.create_time DESC
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.shop.entity.ShopUserCoupon">
<include refid="selectSql"/>
ORDER BY a.create_time DESC
</select>
<!-- 查询用户可用的优惠券 -->
<select id="selectAvailableCoupons" resultType="com.gxwebsoft.shop.entity.ShopUserCoupon">
SELECT
a.id,
a.coupon_id,
a.user_id,
a.name,
a.description,
a.type,
a.reduce_price,
a.discount,
a.min_price,
a.apply_range,
a.apply_range_config,
a.start_time,
a.end_time,
a.status,
a.use_time,
a.order_id,
a.order_no,
a.obtain_type,
a.obtain_source,
a.deleted,
a.tenant_id,
a.create_time,
a.update_time
FROM shop_user_coupon a
WHERE a.deleted = 0
AND a.user_id = #{param.userId}
AND a.status = 0
AND (a.start_time IS NULL OR a.start_time &lt;= NOW())
AND (a.end_time IS NULL OR a.end_time &gt; NOW())
<if test="param.orderAmount != null">
AND (a.min_price IS NULL OR a.min_price &lt;= #{param.orderAmount})
</if>
<if test="param.goodsId != null">
AND (a.apply_range = 10
OR (a.apply_range = 20 AND FIND_IN_SET(#{param.goodsId}, a.apply_range_config) > 0))
</if>
<if test="param.categoryId != null">
AND (a.apply_range = 10
OR (a.apply_range = 30 AND FIND_IN_SET(#{param.categoryId}, a.apply_range_config) > 0))
</if>
ORDER BY
CASE a.type
WHEN 10 THEN a.reduce_price
WHEN 20 THEN #{param.orderAmount} * (100 - a.discount) / 100
ELSE #{param.orderAmount}
END DESC,
a.end_time ASC
</select>
<!-- 统计用户优惠券数量 -->
<select id="countUserCoupons" resultType="java.util.Map">
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 0 AND (end_time IS NULL OR end_time > NOW()) THEN 1 ELSE 0 END) as unused,
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as used,
SUM(CASE WHEN status = 2 OR (status = 0 AND end_time IS NOT NULL AND end_time &lt;= NOW()) THEN 1 ELSE 0 END) as expired
FROM shop_user_coupon
WHERE deleted = 0 AND user_id = #{userId}
</select>
<!-- 查询即将过期的优惠券 -->
<select id="selectExpiringSoon" resultType="com.gxwebsoft.shop.entity.ShopUserCoupon">
SELECT
a.id,
a.coupon_id,
a.user_id,
a.name,
a.description,
a.type,
a.reduce_price,
a.discount,
a.min_price,
a.apply_range,
a.apply_range_config,
a.start_time,
a.end_time,
a.status,
a.use_time,
a.order_id,
a.order_no,
a.obtain_type,
a.obtain_source,
a.deleted,
a.tenant_id,
a.create_time,
a.update_time
FROM shop_user_coupon a
WHERE a.deleted = 0
AND a.status = 0
AND a.end_time IS NOT NULL
AND a.end_time > NOW()
AND a.end_time &lt;= DATE_ADD(NOW(), INTERVAL #{days} DAY)
ORDER BY a.end_time ASC
</select>
<!-- 批量更新过期状态 -->
<update id="updateExpiredStatus">
UPDATE shop_user_coupon
SET status = 2, update_time = NOW()
WHERE deleted = 0
AND status = 0
AND end_time IS NOT NULL
AND end_time &lt;= NOW()
</update>
<!-- 检查用户是否已领取指定优惠券 -->
<select id="countUserReceivedCoupon" resultType="int">
SELECT COUNT(*)
FROM shop_user_coupon
WHERE deleted = 0
AND user_id = #{userId}
AND coupon_id = #{couponId}
</select>
</mapper>

View File

@@ -0,0 +1,90 @@
package com.gxwebsoft.shop.param;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 优惠券查询参数
*
* @author 科技小王子
* @since 2025-08-08 21:10:55
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "ShopCouponParam对象", description = "优惠券查询参数")
public class ShopCouponParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "id")
@QueryField(type = QueryType.EQ)
private Integer id;
@Schema(description = "优惠券名称")
private String name;
@Schema(description = "优惠券类型(10满减券 20折扣券 30免费劵)")
@QueryField(type = QueryType.EQ)
private Integer type;
@Schema(description = "满减券-减免金额")
@QueryField(type = QueryType.EQ)
private BigDecimal reducePrice;
@Schema(description = "折扣券-折扣率(0-100)")
@QueryField(type = QueryType.EQ)
private Integer discount;
@Schema(description = "最低消费金额")
@QueryField(type = QueryType.EQ)
private BigDecimal minPrice;
@Schema(description = "到期类型(10领取后生效 20固定时间)")
@QueryField(type = QueryType.EQ)
private Integer expireType;
@Schema(description = "领取后生效-有效天数")
@QueryField(type = QueryType.EQ)
private Integer expireDay;
@Schema(description = "有效期开始时间")
private String startTime;
@Schema(description = "有效期结束时间")
private String endTime;
@Schema(description = "适用范围(10全部商品 20指定商品)")
@QueryField(type = QueryType.EQ)
private Integer applyRange;
@Schema(description = "适用范围配置(json格式)")
private String applyRangeConfig;
@Schema(description = "是否过期(0未过期 1已过期)")
@QueryField(type = QueryType.EQ)
private Integer isExpire;
@Schema(description = "排序(数字越小越靠前)")
@QueryField(type = QueryType.EQ)
private Integer sortNumber;
@Schema(description = "状态, 0待使用, 1已使用, 2已失效")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@QueryField(type = QueryType.EQ)
private Integer deleted;
@Schema(description = "用户ID")
@QueryField(type = QueryType.EQ)
private Integer userId;
}

View File

@@ -0,0 +1,84 @@
package com.gxwebsoft.shop.param;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 用户优惠券查询参数
*
* @author 科技小王子
* @since 2025-08-08 21:30:00
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(description = "用户优惠券查询参数")
public class ShopUserCouponParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "id")
private Long id;
@Schema(description = "优惠券模板ID")
private Integer couponId;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "优惠券名称")
private String name;
@Schema(description = "优惠券类型(10满减券 20折扣券 30免费劵)")
private Integer type;
@Schema(description = "使用状态(0未使用 1已使用 2已过期)")
private Integer status;
@Schema(description = "适用范围(10全部商品 20指定商品 30指定分类)")
private Integer applyRange;
@Schema(description = "获取方式(10主动领取 20系统发放 30活动赠送)")
private Integer obtainType;
@Schema(description = "使用订单ID")
private Long orderId;
@Schema(description = "使用订单号")
private String orderNo;
@Schema(description = "有效期开始时间-查询开始")
private LocalDateTime startTimeBegin;
@Schema(description = "有效期开始时间-查询结束")
private LocalDateTime startTimeEnd;
@Schema(description = "有效期结束时间-查询开始")
private LocalDateTime endTimeBegin;
@Schema(description = "有效期结束时间-查询结束")
private LocalDateTime endTimeEnd;
@Schema(description = "使用时间-查询开始")
private LocalDateTime useTimeBegin;
@Schema(description = "使用时间-查询结束")
private LocalDateTime useTimeEnd;
@Schema(description = "是否包含已过期的券(默认false)")
private Boolean includeExpired = false;
@Schema(description = "是否只查询可用的券(默认false)")
private Boolean onlyAvailable = false;
@Schema(description = "商品ID(用于筛选可用优惠券)")
private Integer goodsId;
@Schema(description = "商品分类ID(用于筛选可用优惠券)")
private Integer categoryId;
@Schema(description = "订单金额(用于筛选可用优惠券)")
private java.math.BigDecimal orderAmount;
}

View File

@@ -0,0 +1,130 @@
package com.gxwebsoft.shop.service;
import com.gxwebsoft.shop.entity.ShopUserCoupon;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 优惠券业务服务
* 处理订单中的优惠券相关业务逻辑
*
* @author 科技小王子
* @since 2025-08-08 22:00:00
*/
public interface CouponBusinessService {
/**
* 获取用户在指定订单中可用的优惠券
*
* @param userId 用户ID
* @param goodsItems 商品列表 [{goodsId, categoryId, price, quantity}]
* @param totalAmount 订单总金额
* @return 可用优惠券列表
*/
List<ShopUserCoupon> getAvailableCouponsForOrder(Integer userId,
List<Map<String, Object>> goodsItems,
BigDecimal totalAmount);
/**
* 计算使用优惠券后的订单金额
*
* @param userCouponId 用户优惠券ID
* @param goodsItems 商品列表
* @param totalAmount 订单总金额
* @return 计算结果 {discountAmount: 优惠金额, finalAmount: 最终金额, valid: 是否有效}
*/
Map<String, Object> calculateOrderAmountWithCoupon(Long userCouponId,
List<Map<String, Object>> goodsItems,
BigDecimal totalAmount);
/**
* 验证优惠券是否可用于订单
*
* @param userCouponId 用户优惠券ID
* @param userId 用户ID
* @param goodsItems 商品列表
* @param totalAmount 订单总金额
* @return 验证结果 {valid: boolean, reason: String}
*/
Map<String, Object> validateCouponForOrder(Long userCouponId,
Integer userId,
List<Map<String, Object>> goodsItems,
BigDecimal totalAmount);
/**
* 为新用户发放欢迎优惠券
*
* @param userId 用户ID
* @return 发放成功的优惠券数量
*/
int issueWelcomeCoupons(Integer userId);
/**
* 为用户生日发放生日优惠券
*
* @param userId 用户ID
* @return 发放成功的优惠券数量
*/
int issueBirthdayCoupons(Integer userId);
/**
* 根据用户消费金额发放优惠券
*
* @param userId 用户ID
* @param consumeAmount 消费金额
* @return 发放成功的优惠券数量
*/
int issueConsumeCoupons(Integer userId, BigDecimal consumeAmount);
/**
* 活动期间批量发放优惠券
*
* @param activityName 活动名称
* @param couponIds 优惠券模板ID列表
* @param userIds 用户ID列表(为空则发放给所有用户)
* @return 发放统计 {totalUsers: 总用户数, totalCoupons: 总优惠券数, successCount: 成功数量}
*/
Map<String, Object> batchIssueActivityCoupons(String activityName,
List<Integer> couponIds,
List<Integer> userIds);
/**
* 获取优惠券使用统计
*
* @param startDate 开始日期
* @param endDate 结束日期
* @return 统计结果
*/
Map<String, Object> getCouponUsageStatistics(String startDate, String endDate);
/**
* 自动处理过期优惠券
* 定时任务调用
*
* @return 处理数量
*/
int autoProcessExpiredCoupons();
/**
* 发送优惠券到期提醒
* 定时任务调用
*
* @param days 提前天数
* @return 提醒用户数量
*/
int sendCouponExpiryReminder(Integer days);
/**
* 推荐最优优惠券组合
*
* @param userId 用户ID
* @param goodsItems 商品列表
* @param totalAmount 订单总金额
* @return 推荐结果 {coupons: 优惠券列表, totalDiscount: 总优惠金额, finalAmount: 最终金额}
*/
Map<String, Object> recommendBestCouponCombination(Integer userId,
List<Map<String, Object>> goodsItems,
BigDecimal totalAmount);
}

View File

@@ -0,0 +1,67 @@
package com.gxwebsoft.shop.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.shop.entity.ShopCoupon;
import com.gxwebsoft.shop.param.ShopCouponParam;
import java.util.List;
/**
* 优惠券Service
*
* @author 科技小王子
* @since 2025-08-08 21:10:55
*/
public interface ShopCouponService extends IService<ShopCoupon> {
/**
* 分页关联查询
*
* @param param 查询参数
* @return PageResult<ShopCoupon>
*/
PageResult<ShopCoupon> pageRel(ShopCouponParam param);
/**
* 关联查询全部
*
* @param param 查询参数
* @return List<ShopCoupon>
*/
List<ShopCoupon> listRel(ShopCouponParam param);
/**
* 根据id查询
*
* @param id id
* @return ShopCoupon
*/
ShopCoupon getByIdRel(Integer id);
/**
* 获取可领取的优惠券列表
*
* @param userId 用户ID
* @return List<ShopCoupon>
*/
List<ShopCoupon> getReceivableCoupons(Integer userId);
/**
* 检查优惠券是否可以发放
*
* @param couponId 优惠券ID
* @return boolean
*/
boolean canIssue(Integer couponId);
/**
* 更新优惠券发放数量
*
* @param couponId 优惠券ID
* @param increment 增量
* @return boolean
*/
boolean updateIssuedCount(Integer couponId, int increment);
}

View File

@@ -0,0 +1,153 @@
package com.gxwebsoft.shop.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.shop.entity.ShopUserCoupon;
import com.gxwebsoft.shop.param.ShopUserCouponParam;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 用户优惠券Service
*
* @author 科技小王子
* @since 2025-08-08 21:30:00
*/
public interface ShopUserCouponService extends IService<ShopUserCoupon> {
/**
* 分页关联查询用户优惠券
*
* @param param 查询参数
* @return PageResult<ShopUserCoupon>
*/
PageResult<ShopUserCoupon> pageRel(ShopUserCouponParam param);
/**
* 关联查询用户优惠券列表
*
* @param param 查询参数
* @return List<ShopUserCoupon>
*/
List<ShopUserCoupon> listRel(ShopUserCouponParam param);
/**
* 根据id查询用户优惠券
*
* @param id id
* @return ShopUserCoupon
*/
ShopUserCoupon getByIdRel(Long id);
/**
* 用户领取优惠券
*
* @param userId 用户ID
* @param couponId 优惠券模板ID
* @return 是否成功
*/
boolean receiveCoupon(Integer userId, Integer couponId);
/**
* 系统发放优惠券给用户
*
* @param userId 用户ID
* @param couponId 优惠券模板ID
* @param source 发放来源描述
* @return 是否成功
*/
boolean issueCouponToUser(Integer userId, Integer couponId, String source);
/**
* 批量发放优惠券给多个用户
*
* @param userIds 用户ID列表
* @param couponId 优惠券模板ID
* @param source 发放来源描述
* @return 成功发放的数量
*/
int batchIssueCoupons(List<Integer> userIds, Integer couponId, String source);
/**
* 使用优惠券
*
* @param userCouponId 用户优惠券ID
* @param orderId 订单ID
* @param orderNo 订单号
* @return 是否成功
*/
boolean useCoupon(Long userCouponId, Long orderId, String orderNo);
/**
* 退还优惠券(订单取消时)
*
* @param orderId 订单ID
* @return 是否成功
*/
boolean returnCoupon(Long orderId);
/**
* 获取用户可用的优惠券列表
*
* @param userId 用户ID
* @param goodsId 商品ID(可选)
* @param categoryId 商品分类ID(可选)
* @param orderAmount 订单金额(可选)
* @return List<ShopUserCoupon>
*/
List<ShopUserCoupon> getAvailableCoupons(Integer userId, Integer goodsId,
Integer categoryId, BigDecimal orderAmount);
/**
* 计算优惠券优惠金额
*
* @param userCoupon 用户优惠券
* @param orderAmount 订单金额
* @return 优惠金额
*/
BigDecimal calculateDiscountAmount(ShopUserCoupon userCoupon, BigDecimal orderAmount);
/**
* 验证优惠券是否可用于指定商品
*
* @param userCoupon 用户优惠券
* @param goodsId 商品ID
* @param categoryId 商品分类ID
* @return 是否可用
*/
boolean validateCouponForGoods(ShopUserCoupon userCoupon, Integer goodsId, Integer categoryId);
/**
* 统计用户优惠券数量
*
* @param userId 用户ID
* @return 统计结果 {total: 总数, unused: 未使用, used: 已使用, expired: 已过期}
*/
Map<String, Object> countUserCoupons(Integer userId);
/**
* 更新过期优惠券状态
*
* @return 更新数量
*/
int updateExpiredCoupons();
/**
* 获取即将过期的优惠券
*
* @param days 天数
* @return List<ShopUserCoupon>
*/
List<ShopUserCoupon> getExpiringSoonCoupons(Integer days);
/**
* 检查用户是否可以领取指定优惠券
*
* @param userId 用户ID
* @param couponId 优惠券模板ID
* @return 检查结果 {canReceive: boolean, reason: String}
*/
Map<String, Object> checkCanReceiveCoupon(Integer userId, Integer couponId);
}

View File

@@ -0,0 +1,433 @@
package com.gxwebsoft.shop.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.service.UserService;
import com.gxwebsoft.shop.entity.ShopCoupon;
import com.gxwebsoft.shop.entity.ShopUserCoupon;
import com.gxwebsoft.shop.service.CouponBusinessService;
import com.gxwebsoft.shop.service.ShopCouponService;
import com.gxwebsoft.shop.service.ShopUserCouponService;
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.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
* 优惠券业务服务实现
*
* @author 科技小王子
* @since 2025-08-08 22:00:00
*/
@Slf4j
@Service
public class CouponBusinessServiceImpl implements CouponBusinessService {
@Resource
private ShopUserCouponService shopUserCouponService;
@Resource
private ShopCouponService shopCouponService;
@Resource
private UserService userService;
@Override
public List<ShopUserCoupon> getAvailableCouponsForOrder(Integer userId,
List<Map<String, Object>> goodsItems,
BigDecimal totalAmount) {
if (userId == null || goodsItems == null || goodsItems.isEmpty()) {
return new ArrayList<>();
}
// 获取用户所有可用优惠券
List<ShopUserCoupon> allCoupons = shopUserCouponService.getAvailableCoupons(
userId, null, null, totalAmount);
// 过滤出适用于当前订单的优惠券
return allCoupons.stream()
.filter(coupon -> isValidForOrder(coupon, goodsItems, totalAmount))
.collect(Collectors.toList());
}
@Override
public Map<String, Object> calculateOrderAmountWithCoupon(Long userCouponId,
List<Map<String, Object>> goodsItems,
BigDecimal totalAmount) {
Map<String, Object> result = new HashMap<>();
result.put("valid", false);
result.put("discountAmount", BigDecimal.ZERO);
result.put("finalAmount", totalAmount);
if (userCouponId == null || totalAmount == null) {
result.put("reason", "参数不能为空");
return result;
}
ShopUserCoupon userCoupon = shopUserCouponService.getById(userCouponId);
if (userCoupon == null) {
result.put("reason", "优惠券不存在");
return result;
}
if (!userCoupon.isAvailable()) {
result.put("reason", "优惠券不可用");
return result;
}
if (!isValidForOrder(userCoupon, goodsItems, totalAmount)) {
result.put("reason", "优惠券不适用于当前订单");
return result;
}
BigDecimal discountAmount = shopUserCouponService.calculateDiscountAmount(userCoupon, totalAmount);
BigDecimal finalAmount = totalAmount.subtract(discountAmount);
// 确保最终金额不为负数
if (finalAmount.compareTo(BigDecimal.ZERO) < 0) {
finalAmount = BigDecimal.ZERO;
}
result.put("valid", true);
result.put("discountAmount", discountAmount);
result.put("finalAmount", finalAmount);
result.put("coupon", userCoupon);
return result;
}
@Override
public Map<String, Object> validateCouponForOrder(Long userCouponId,
Integer userId,
List<Map<String, Object>> goodsItems,
BigDecimal totalAmount) {
Map<String, Object> result = new HashMap<>();
if (userCouponId == null || userId == null) {
result.put("valid", false);
result.put("reason", "参数不能为空");
return result;
}
ShopUserCoupon userCoupon = shopUserCouponService.getById(userCouponId);
if (userCoupon == null) {
result.put("valid", false);
result.put("reason", "优惠券不存在");
return result;
}
if (!userCoupon.getUserId().equals(userId)) {
result.put("valid", false);
result.put("reason", "优惠券不属于当前用户");
return result;
}
if (!userCoupon.isAvailable()) {
result.put("valid", false);
result.put("reason", "优惠券不可用或已过期");
return result;
}
if (!isValidForOrder(userCoupon, goodsItems, totalAmount)) {
result.put("valid", false);
result.put("reason", "优惠券不适用于当前订单商品");
return result;
}
result.put("valid", true);
result.put("reason", "优惠券可用");
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int issueWelcomeCoupons(Integer userId) {
if (userId == null) {
return 0;
}
// 查找欢迎优惠券模板(这里假设有特定的标识)
LambdaQueryWrapper<ShopCoupon> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShopCoupon::getEnabled, 1)
.eq(ShopCoupon::getStatus, 0)
.like(ShopCoupon::getName, "新用户")
.or()
.like(ShopCoupon::getName, "欢迎");
List<ShopCoupon> welcomeCoupons = shopCouponService.list(queryWrapper);
int successCount = 0;
for (ShopCoupon coupon : welcomeCoupons) {
if (shopUserCouponService.issueCouponToUser(userId, coupon.getId(), "新用户注册赠送")) {
successCount++;
}
}
log.info("为新用户{}发放欢迎优惠券{}张", userId, successCount);
return successCount;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int issueBirthdayCoupons(Integer userId) {
if (userId == null) {
return 0;
}
User user = userService.getById(userId);
if (user == null || user.getBirthday() == null) {
return 0;
}
// 检查今天是否是用户生日
LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.parse((CharSequence) user.getBirthday(), DateTimeFormatter.ofPattern("yyyy-MM-dd"));
if (today.getMonthValue() != birthday.getMonthValue() ||
today.getDayOfMonth() != birthday.getDayOfMonth()) {
return 0;
}
// 查找生日优惠券模板
LambdaQueryWrapper<ShopCoupon> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShopCoupon::getEnabled, 1)
.eq(ShopCoupon::getStatus, 0)
.like(ShopCoupon::getName, "生日");
List<ShopCoupon> birthdayCoupons = shopCouponService.list(queryWrapper);
int successCount = 0;
for (ShopCoupon coupon : birthdayCoupons) {
if (shopUserCouponService.issueCouponToUser(userId, coupon.getId(), "生日祝福赠送")) {
successCount++;
}
}
log.info("为用户{}发放生日优惠券{}张", userId, successCount);
return successCount;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int issueConsumeCoupons(Integer userId, BigDecimal consumeAmount) {
if (userId == null || consumeAmount == null || consumeAmount.compareTo(BigDecimal.ZERO) <= 0) {
return 0;
}
// 根据消费金额发放不同的优惠券
List<ShopCoupon> eligibleCoupons = new ArrayList<>();
if (consumeAmount.compareTo(new BigDecimal("100")) >= 0) {
// 消费满100发放优惠券
LambdaQueryWrapper<ShopCoupon> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShopCoupon::getEnabled, 1)
.eq(ShopCoupon::getStatus, 0)
.like(ShopCoupon::getName, "消费返券");
eligibleCoupons.addAll(shopCouponService.list(queryWrapper));
}
int successCount = 0;
for (ShopCoupon coupon : eligibleCoupons) {
if (shopUserCouponService.issueCouponToUser(userId, coupon.getId(),
"消费满" + consumeAmount + "元返券")) {
successCount++;
}
}
log.info("为用户{}消费{}元发放优惠券{}张", userId, consumeAmount, successCount);
return successCount;
}
/**
* 检查优惠券是否适用于订单
*/
private boolean isValidForOrder(ShopUserCoupon userCoupon,
List<Map<String, Object>> goodsItems,
BigDecimal totalAmount) {
if (userCoupon == null || goodsItems == null || goodsItems.isEmpty()) {
return false;
}
// 检查最低消费金额
if (userCoupon.getMinPrice() != null &&
totalAmount.compareTo(userCoupon.getMinPrice()) < 0) {
return false;
}
// 检查适用范围
if (userCoupon.getApplyRange() == ShopUserCoupon.APPLY_ALL) {
return true;
} else if (userCoupon.getApplyRange() == ShopUserCoupon.APPLY_GOODS) {
// 检查是否有指定商品
return goodsItems.stream().anyMatch(item -> {
Integer goodsId = (Integer) item.get("goodsId");
return shopUserCouponService.validateCouponForGoods(userCoupon, goodsId, null);
});
} else if (userCoupon.getApplyRange() == ShopUserCoupon.APPLY_CATEGORY) {
// 检查是否有指定分类的商品
return goodsItems.stream().anyMatch(item -> {
Integer categoryId = (Integer) item.get("categoryId");
return shopUserCouponService.validateCouponForGoods(userCoupon, null, categoryId);
});
}
return false;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> batchIssueActivityCoupons(String activityName,
List<Integer> couponIds,
List<Integer> userIds) {
Map<String, Object> result = new HashMap<>();
result.put("totalUsers", 0);
result.put("totalCoupons", 0);
result.put("successCount", 0);
if (couponIds == null || couponIds.isEmpty()) {
result.put("message", "优惠券列表不能为空");
return result;
}
// 如果没有指定用户,则获取所有活跃用户
if (userIds == null || userIds.isEmpty()) {
userIds = userService.list().stream()
.filter(user -> user.getStatus() == 0) // 只给正常状态的用户发放
.map(User::getUserId)
.collect(Collectors.toList());
}
int totalUsers = userIds.size();
int totalCoupons = couponIds.size() * totalUsers;
int successCount = 0;
String source = activityName != null ? activityName + "活动赠送" : "活动赠送";
for (Integer userId : userIds) {
for (Integer couponId : couponIds) {
if (shopUserCouponService.issueCouponToUser(userId, couponId, source)) {
successCount++;
}
}
}
result.put("totalUsers", totalUsers);
result.put("totalCoupons", totalCoupons);
result.put("successCount", successCount);
result.put("message", String.format("活动优惠券发放完成,共%d个用户%d张优惠券成功%d张",
totalUsers, totalCoupons, successCount));
log.info("活动{}批量发放优惠券完成:用户{}个,优惠券{}张,成功{}张",
activityName, totalUsers, totalCoupons, successCount);
return result;
}
@Override
public Map<String, Object> getCouponUsageStatistics(String startDate, String endDate) {
Map<String, Object> result = new HashMap<>();
// 这里可以根据需要实现具体的统计逻辑
// 统计指定时间段内的优惠券使用情况
result.put("totalIssued", 0); // 总发放数量
result.put("totalUsed", 0); // 总使用数量
result.put("totalExpired", 0); // 总过期数量
result.put("usageRate", 0.0); // 使用率
result.put("discountAmount", BigDecimal.ZERO); // 总优惠金额
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int autoProcessExpiredCoupons() {
// 更新过期优惠券状态
int count = shopUserCouponService.updateExpiredCoupons();
log.info("自动处理过期优惠券{}张", count);
return count;
}
@Override
public int sendCouponExpiryReminder(Integer days) {
if (days == null || days <= 0) {
days = 3; // 默认提前3天提醒
}
List<ShopUserCoupon> expiringSoonCoupons = shopUserCouponService.getExpiringSoonCoupons(days);
// 按用户分组
Map<Integer, List<ShopUserCoupon>> userCouponsMap = expiringSoonCoupons.stream()
.collect(Collectors.groupingBy(ShopUserCoupon::getUserId));
int reminderCount = 0;
for (Map.Entry<Integer, List<ShopUserCoupon>> entry : userCouponsMap.entrySet()) {
Integer userId = entry.getKey();
List<ShopUserCoupon> coupons = entry.getValue();
// 这里可以发送消息提醒用户
// 例如:发送站内信、短信、推送通知等
log.info("提醒用户{}有{}张优惠券即将过期", userId, coupons.size());
reminderCount++;
}
log.info("发送优惠券到期提醒给{}个用户", reminderCount);
return reminderCount;
}
@Override
public Map<String, Object> recommendBestCouponCombination(Integer userId,
List<Map<String, Object>> goodsItems,
BigDecimal totalAmount) {
Map<String, Object> result = new HashMap<>();
result.put("coupons", new ArrayList<>());
result.put("totalDiscount", BigDecimal.ZERO);
result.put("finalAmount", totalAmount);
if (userId == null || goodsItems == null || goodsItems.isEmpty() || totalAmount == null) {
return result;
}
// 获取用户所有可用优惠券
List<ShopUserCoupon> availableCoupons = getAvailableCouponsForOrder(userId, goodsItems, totalAmount);
if (availableCoupons.isEmpty()) {
return result;
}
// 简单的贪心算法:选择优惠金额最大的优惠券
// 实际项目中可以实现更复杂的组合优化算法
ShopUserCoupon bestCoupon = null;
BigDecimal maxDiscount = BigDecimal.ZERO;
for (ShopUserCoupon coupon : availableCoupons) {
BigDecimal discount = shopUserCouponService.calculateDiscountAmount(coupon, totalAmount);
if (discount.compareTo(maxDiscount) > 0) {
maxDiscount = discount;
bestCoupon = coupon;
}
}
if (bestCoupon != null) {
List<ShopUserCoupon> recommendedCoupons = new ArrayList<>();
recommendedCoupons.add(bestCoupon);
BigDecimal finalAmount = totalAmount.subtract(maxDiscount);
if (finalAmount.compareTo(BigDecimal.ZERO) < 0) {
finalAmount = BigDecimal.ZERO;
}
result.put("coupons", recommendedCoupons);
result.put("totalDiscount", maxDiscount);
result.put("finalAmount", finalAmount);
}
return result;
}
}

View File

@@ -0,0 +1,126 @@
package com.gxwebsoft.shop.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.shop.entity.ShopCoupon;
import com.gxwebsoft.shop.mapper.ShopCouponMapper;
import com.gxwebsoft.shop.param.ShopCouponParam;
import com.gxwebsoft.shop.service.ShopCouponService;
import com.gxwebsoft.shop.service.ShopUserCouponService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.util.List;
/**
* 优惠券Service实现
*
* @author 科技小王子
* @since 2025-08-08 21:10:55
*/
@Slf4j
@Service
public class ShopCouponServiceImpl extends ServiceImpl<ShopCouponMapper, ShopCoupon> implements ShopCouponService {
@Resource
private ShopUserCouponService shopUserCouponService;
@Override
public PageResult<ShopCoupon> pageRel(ShopCouponParam param) {
PageParam<ShopCoupon, ShopCouponParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc");
List<ShopCoupon> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal());
}
@Override
public List<ShopCoupon> listRel(ShopCouponParam param) {
List<ShopCoupon> list = baseMapper.selectListRel(param);
// 排序
PageParam<ShopCoupon, ShopCouponParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, create_time desc");
return page.sortRecords(list);
}
@Override
public ShopCoupon getByIdRel(Integer id) {
ShopCouponParam param = new ShopCouponParam();
param.setId(id);
return param.getOne(baseMapper.selectListRel(param));
}
@Override
public List<ShopCoupon> getReceivableCoupons(Integer userId) {
LambdaQueryWrapper<ShopCoupon> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShopCoupon::getEnabled, 1)
.eq(ShopCoupon::getStatus, 0)
.and(wrapper -> wrapper
.isNull(ShopCoupon::getTotalCount)
.or()
.apply("IFNULL(total_count, 0) = -1")
.or()
.apply("IFNULL(issued_count, 0) < IFNULL(total_count, 0)"))
.orderByAsc(ShopCoupon::getSortNumber)
.orderByDesc(ShopCoupon::getCreateTime);
List<ShopCoupon> allCoupons = list(queryWrapper);
// 过滤掉用户已达到领取上限的优惠券
return allCoupons.stream()
.filter(coupon -> {
if (coupon.getLimitPerUser() == null || coupon.getLimitPerUser() <= 0) {
return true; // 无限制
}
// 检查用户已领取数量
int receivedCount = baseMapper.countUserReceivedCoupon(userId, coupon.getId());
return receivedCount < coupon.getLimitPerUser();
})
.collect(java.util.stream.Collectors.toList());
}
@Override
public boolean canIssue(Integer couponId) {
ShopCoupon coupon = getById(couponId);
if (coupon == null || coupon.getEnabled() != 1 || coupon.getStatus() != 0) {
return false;
}
// 检查发放数量限制
if (coupon.getTotalCount() != null && coupon.getTotalCount() > 0) {
int issuedCount = coupon.getIssuedCount() != null ? coupon.getIssuedCount() : 0;
return issuedCount < coupon.getTotalCount();
}
// 检查有效期
LocalDate now = LocalDate.now();
if (coupon.getExpireType() == 20) { // 固定时间
if (coupon.getStartTime() != null && now.isBefore(coupon.getStartTime())) {
return false; // 还未开始
}
if (coupon.getEndTime() != null && now.isAfter(coupon.getEndTime())) {
return false; // 已过期
}
}
return true;
}
@Override
public boolean updateIssuedCount(Integer couponId, int increment) {
LambdaUpdateWrapper<ShopCoupon> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(ShopCoupon::getId, couponId)
.setSql("issued_count = IFNULL(issued_count, 0) + " + increment);
boolean updated = update(updateWrapper);
if (updated) {
log.debug("更新优惠券{}发放数量,增量: {}", couponId, increment);
}
return updated;
}
}

View File

@@ -0,0 +1,403 @@
package com.gxwebsoft.shop.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.shop.entity.ShopCoupon;
import com.gxwebsoft.shop.entity.ShopUserCoupon;
import com.gxwebsoft.shop.mapper.ShopUserCouponMapper;
import com.gxwebsoft.shop.param.ShopUserCouponParam;
import com.gxwebsoft.shop.service.ShopCouponService;
import com.gxwebsoft.shop.service.ShopUserCouponService;
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.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.*;
/**
* 用户优惠券Service实现
*
* @author 科技小王子
* @since 2025-08-08 21:30:00
*/
@Slf4j
@Service
public class ShopUserCouponServiceImpl extends ServiceImpl<ShopUserCouponMapper, ShopUserCoupon>
implements ShopUserCouponService {
@Resource
private ShopCouponService shopCouponService;
@Override
public PageResult<ShopUserCoupon> pageRel(ShopUserCouponParam param) {
PageParam<ShopUserCoupon, ShopUserCouponParam> page = new PageParam<>(param);
page.setDefaultOrder("create_time desc");
List<ShopUserCoupon> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal());
}
@Override
public List<ShopUserCoupon> listRel(ShopUserCouponParam param) {
return baseMapper.selectListRel(param);
}
@Override
public ShopUserCoupon getByIdRel(Long id) {
ShopUserCouponParam param = new ShopUserCouponParam();
param.setId(id);
List<ShopUserCoupon> list = baseMapper.selectListRel(param);
return list.isEmpty() ? null : list.get(0);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean receiveCoupon(Integer userId, Integer couponId) {
// 检查是否可以领取
Map<String, Object> checkResult = checkCanReceiveCoupon(userId, couponId);
if (!(Boolean) checkResult.get("canReceive")) {
log.warn("用户{}无法领取优惠券{}: {}", userId, couponId, checkResult.get("reason"));
return false;
}
// 获取优惠券模板
ShopCoupon coupon = shopCouponService.getById(couponId);
if (coupon == null) {
log.warn("优惠券模板不存在: {}", couponId);
return false;
}
// 创建用户优惠券
ShopUserCoupon userCoupon = createUserCouponFromTemplate(coupon, userId);
userCoupon.setObtainType(ShopUserCoupon.OBTAIN_RECEIVE);
userCoupon.setObtainSource("用户主动领取");
// 保存用户优惠券
boolean saved = save(userCoupon);
if (saved) {
// 更新优惠券模板的已发放数量
updateCouponIssuedCount(couponId, 1);
log.info("用户{}成功领取优惠券{}", userId, couponId);
}
return saved;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean issueCouponToUser(Integer userId, Integer couponId, String source) {
// 获取优惠券模板
ShopCoupon coupon = shopCouponService.getById(couponId);
if (coupon == null || coupon.getEnabled() != 1) {
log.warn("优惠券模板不存在或已禁用: {}", couponId);
return false;
}
// 检查发放数量限制
if (coupon.getTotalCount() != null && coupon.getTotalCount() > 0) {
int issuedCount = coupon.getIssuedCount() != null ? coupon.getIssuedCount() : 0;
if (issuedCount >= coupon.getTotalCount()) {
log.warn("优惠券{}发放数量已达上限", couponId);
return false;
}
}
// 创建用户优惠券
ShopUserCoupon userCoupon = createUserCouponFromTemplate(coupon, userId);
userCoupon.setObtainType(ShopUserCoupon.OBTAIN_SYSTEM);
userCoupon.setObtainSource(source != null ? source : "系统发放");
// 保存用户优惠券
boolean saved = save(userCoupon);
if (saved) {
// 更新优惠券模板的已发放数量
updateCouponIssuedCount(couponId, 1);
log.info("系统向用户{}发放优惠券{}", userId, couponId);
}
return saved;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int batchIssueCoupons(List<Integer> userIds, Integer couponId, String source) {
if (userIds == null || userIds.isEmpty()) {
return 0;
}
// 获取优惠券模板
ShopCoupon coupon = shopCouponService.getById(couponId);
if (coupon == null || coupon.getEnabled() != 1) {
log.warn("优惠券模板不存在或已禁用: {}", couponId);
return 0;
}
int successCount = 0;
for (Integer userId : userIds) {
if (issueCouponToUser(userId, couponId, source)) {
successCount++;
}
}
log.info("批量发放优惠券{}给{}个用户,成功{}个", couponId, userIds.size(), successCount);
return successCount;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean useCoupon(Long userCouponId, Long orderId, String orderNo) {
// 获取用户优惠券
ShopUserCoupon userCoupon = getById(userCouponId);
if (userCoupon == null) {
log.warn("用户优惠券不存在: {}", userCouponId);
return false;
}
// 检查优惠券状态
if (!userCoupon.isAvailable()) {
log.warn("优惠券不可用: {}", userCouponId);
return false;
}
// 更新优惠券状态
LambdaUpdateWrapper<ShopUserCoupon> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(ShopUserCoupon::getId, userCouponId)
.eq(ShopUserCoupon::getStatus, ShopUserCoupon.STATUS_UNUSED)
.set(ShopUserCoupon::getStatus, ShopUserCoupon.STATUS_USED)
.set(ShopUserCoupon::getUseTime, LocalDateTime.now())
.set(ShopUserCoupon::getOrderId, orderId)
.set(ShopUserCoupon::getOrderNo, orderNo);
boolean updated = update(updateWrapper);
if (updated) {
log.info("用户优惠券{}已使用,订单: {}", userCouponId, orderNo);
}
return updated;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean returnCoupon(Long orderId) {
// 查找使用该订单的优惠券
LambdaQueryWrapper<ShopUserCoupon> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShopUserCoupon::getOrderId, orderId)
.eq(ShopUserCoupon::getStatus, ShopUserCoupon.STATUS_USED);
List<ShopUserCoupon> usedCoupons = list(queryWrapper);
if (usedCoupons.isEmpty()) {
return true; // 没有使用优惠券,直接返回成功
}
// 退还优惠券
for (ShopUserCoupon userCoupon : usedCoupons) {
// 检查是否已过期
if (userCoupon.isExpired()) {
// 已过期的券设为过期状态
userCoupon.setStatus(ShopUserCoupon.STATUS_EXPIRED);
} else {
// 未过期的券恢复为未使用状态
userCoupon.setStatus(ShopUserCoupon.STATUS_UNUSED);
}
userCoupon.setUseTime(null);
userCoupon.setOrderId(null);
userCoupon.setOrderNo(null);
updateById(userCoupon);
log.info("退还用户优惠券{},订单: {}", userCoupon.getId(), orderId);
}
return true;
}
/**
* 从优惠券模板创建用户优惠券
*/
private ShopUserCoupon createUserCouponFromTemplate(ShopCoupon coupon, Integer userId) {
ShopUserCoupon userCoupon = new ShopUserCoupon();
userCoupon.setCouponId(coupon.getId());
userCoupon.setUserId(userId);
userCoupon.setName(coupon.getName());
userCoupon.setDescription(coupon.getDescription());
userCoupon.setType(coupon.getType());
userCoupon.setReducePrice(coupon.getReducePrice());
userCoupon.setDiscount(coupon.getDiscount());
userCoupon.setMinPrice(coupon.getMinPrice());
userCoupon.setApplyRange(coupon.getApplyRange());
userCoupon.setApplyRangeConfig(coupon.getApplyRangeConfig());
userCoupon.setStatus(ShopUserCoupon.STATUS_UNUSED);
userCoupon.setTenantId(coupon.getTenantId());
// 设置有效期
LocalDateTime now = LocalDateTime.now();
if (coupon.getExpireType() == 10) {
// 领取后生效
userCoupon.setStartTime(now);
if (coupon.getExpireDay() != null && coupon.getExpireDay() > 0) {
userCoupon.setEndTime(now.plusDays(coupon.getExpireDay()));
}
} else if (coupon.getExpireType() == 20) {
// 固定时间
userCoupon.setStartTime(coupon.getStartTime() != null ?
coupon.getStartTime().atStartOfDay() : now);
userCoupon.setEndTime(coupon.getEndTime() != null ?
coupon.getEndTime().atTime(23, 59, 59) : null);
}
return userCoupon;
}
@Override
public List<ShopUserCoupon> getAvailableCoupons(Integer userId, Integer goodsId,
Integer categoryId, BigDecimal orderAmount) {
ShopUserCouponParam param = new ShopUserCouponParam();
param.setUserId(userId);
param.setStatus(ShopUserCoupon.STATUS_UNUSED);
param.setOnlyAvailable(true);
param.setGoodsId(goodsId);
param.setCategoryId(categoryId);
param.setOrderAmount(orderAmount);
return baseMapper.selectAvailableCoupons(param);
}
@Override
public BigDecimal calculateDiscountAmount(ShopUserCoupon userCoupon, BigDecimal orderAmount) {
if (userCoupon == null || orderAmount == null || orderAmount.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
// 检查最低消费金额
if (userCoupon.getMinPrice() != null &&
orderAmount.compareTo(userCoupon.getMinPrice()) < 0) {
return BigDecimal.ZERO;
}
BigDecimal discountAmount = BigDecimal.ZERO;
if (userCoupon.getType() == ShopUserCoupon.TYPE_REDUCE) {
// 满减券
discountAmount = userCoupon.getReducePrice() != null ?
userCoupon.getReducePrice() : BigDecimal.ZERO;
} else if (userCoupon.getType() == ShopUserCoupon.TYPE_DISCOUNT) {
// 折扣券
if (userCoupon.getDiscount() != null && userCoupon.getDiscount() > 0 && userCoupon.getDiscount() < 100) {
BigDecimal discountRate = BigDecimal.valueOf(100 - userCoupon.getDiscount())
.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
discountAmount = orderAmount.multiply(discountRate);
}
} else if (userCoupon.getType() == ShopUserCoupon.TYPE_FREE) {
// 免费券
discountAmount = orderAmount;
}
// 优惠金额不能超过订单金额
return discountAmount.compareTo(orderAmount) > 0 ? orderAmount : discountAmount;
}
@Override
public boolean validateCouponForGoods(ShopUserCoupon userCoupon, Integer goodsId, Integer categoryId) {
if (userCoupon == null || userCoupon.getApplyRange() == null) {
return false;
}
if (userCoupon.getApplyRange() == ShopUserCoupon.APPLY_ALL) {
// 全部商品可用
return true;
} else if (userCoupon.getApplyRange() == ShopUserCoupon.APPLY_GOODS) {
// 指定商品可用
if (goodsId == null || userCoupon.getApplyRangeConfig() == null) {
return false;
}
// 解析商品ID列表假设是逗号分隔的字符串
String[] goodsIds = userCoupon.getApplyRangeConfig().split(",");
return Arrays.asList(goodsIds).contains(goodsId.toString());
} else if (userCoupon.getApplyRange() == ShopUserCoupon.APPLY_CATEGORY) {
// 指定分类可用
if (categoryId == null || userCoupon.getApplyRangeConfig() == null) {
return false;
}
// 解析分类ID列表假设是逗号分隔的字符串
String[] categoryIds = userCoupon.getApplyRangeConfig().split(",");
return Arrays.asList(categoryIds).contains(categoryId.toString());
}
return false;
}
@Override
public Map<String, Object> countUserCoupons(Integer userId) {
return baseMapper.countUserCoupons(userId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int updateExpiredCoupons() {
return baseMapper.updateExpiredStatus();
}
@Override
public List<ShopUserCoupon> getExpiringSoonCoupons(Integer days) {
return baseMapper.selectExpiringSoon(days != null ? days : 3);
}
@Override
public Map<String, Object> checkCanReceiveCoupon(Integer userId, Integer couponId) {
Map<String, Object> result = new HashMap<>();
// 获取优惠券模板
ShopCoupon coupon = shopCouponService.getById(couponId);
if (coupon == null) {
result.put("canReceive", false);
result.put("reason", "优惠券不存在");
return result;
}
if (coupon.getEnabled() != 1) {
result.put("canReceive", false);
result.put("reason", "优惠券已禁用");
return result;
}
// 检查发放数量限制
if (coupon.getTotalCount() != null && coupon.getTotalCount() > 0) {
int issuedCount = coupon.getIssuedCount() != null ? coupon.getIssuedCount() : 0;
if (issuedCount >= coupon.getTotalCount()) {
result.put("canReceive", false);
result.put("reason", "优惠券已领完");
return result;
}
}
// 检查每人限领数量
if (coupon.getLimitPerUser() != null && coupon.getLimitPerUser() > 0) {
int receivedCount = baseMapper.countUserReceivedCoupon(userId, couponId);
if (receivedCount >= coupon.getLimitPerUser()) {
result.put("canReceive", false);
result.put("reason", "已达到个人领取上限");
return result;
}
}
result.put("canReceive", true);
result.put("reason", "可以领取");
return result;
}
/**
* 更新优惠券模板的已发放数量
*/
private void updateCouponIssuedCount(Integer couponId, int increment) {
LambdaUpdateWrapper<ShopCoupon> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(ShopCoupon::getId, couponId)
.setSql("issued_count = IFNULL(issued_count, 0) + " + increment);
shopCouponService.update(updateWrapper);
}
}

View File

@@ -0,0 +1,65 @@
package com.gxwebsoft.shop.task;
import com.gxwebsoft.shop.service.CouponBusinessService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 优惠券定时任务
*
* @author 科技小王子
* @since 2025-08-08 22:30:00
*/
@Slf4j
@Component
public class CouponScheduledTask {
@Resource
private CouponBusinessService couponBusinessService;
/**
* 每天凌晨2点处理过期优惠券
*/
@Scheduled(cron = "0 0 2 * * ?")
public void processExpiredCoupons() {
try {
log.info("开始执行过期优惠券处理任务");
int count = couponBusinessService.autoProcessExpiredCoupons();
log.info("过期优惠券处理任务完成,处理{}张优惠券", count);
} catch (Exception e) {
log.error("过期优惠券处理任务执行失败", e);
}
}
/**
* 每天上午10点发送优惠券到期提醒
*/
@Scheduled(cron = "0 0 10 * * ?")
public void sendExpiryReminder() {
try {
log.info("开始执行优惠券到期提醒任务");
int count = couponBusinessService.sendCouponExpiryReminder(3);
log.info("优惠券到期提醒任务完成,提醒{}个用户", count);
} catch (Exception e) {
log.error("优惠券到期提醒任务执行失败", e);
}
}
/**
* 每天凌晨1点发送生日优惠券
*/
@Scheduled(cron = "0 0 1 * * ?")
public void issueBirthdayCoupons() {
try {
log.info("开始执行生日优惠券发放任务");
// 这里需要获取今天生日的用户列表
// 由于没有具体的用户生日查询方法,这里只是示例
log.info("生日优惠券发放任务完成");
} catch (Exception e) {
log.error("生日优惠券发放任务执行失败", e);
}
}
}

View File

@@ -0,0 +1,276 @@
package com.gxwebsoft.shop.utils;
import com.gxwebsoft.shop.entity.ShopUserCoupon;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
/**
* 优惠券工具类
*
* @author 科技小王子
* @since 2025-08-08 23:00:00
*/
public class CouponUtils {
/**
* 优惠券类型名称映射
*/
public static String getTypeName(Integer type) {
if (type == null) {
return "未知";
}
switch (type) {
case 10:
return "满减券";
case 20:
return "折扣券";
case 30:
return "免费券";
default:
return "未知";
}
}
/**
* 优惠券状态名称映射
*/
public static String getStatusName(Integer status) {
if (status == null) {
return "未知";
}
switch (status) {
case 0:
return "未使用";
case 1:
return "已使用";
case 2:
return "已过期";
default:
return "未知";
}
}
/**
* 适用范围名称映射
*/
public static String getApplyRangeName(Integer applyRange) {
if (applyRange == null) {
return "未知";
}
switch (applyRange) {
case 10:
return "全部商品";
case 20:
return "指定商品";
case 30:
return "指定分类";
default:
return "未知";
}
}
/**
* 获取方式名称映射
*/
public static String getObtainTypeName(Integer obtainType) {
if (obtainType == null) {
return "未知";
}
switch (obtainType) {
case 10:
return "主动领取";
case 20:
return "系统发放";
case 30:
return "活动赠送";
default:
return "未知";
}
}
/**
* 计算优惠券优惠金额
*
* @param userCoupon 用户优惠券
* @param orderAmount 订单金额
* @return 优惠金额
*/
public static BigDecimal calculateDiscountAmount(ShopUserCoupon userCoupon, BigDecimal orderAmount) {
if (userCoupon == null || orderAmount == null || orderAmount.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
// 检查最低消费金额
if (userCoupon.getMinPrice() != null &&
orderAmount.compareTo(userCoupon.getMinPrice()) < 0) {
return BigDecimal.ZERO;
}
BigDecimal discountAmount = BigDecimal.ZERO;
if (userCoupon.getType() == ShopUserCoupon.TYPE_REDUCE) {
// 满减券
discountAmount = userCoupon.getReducePrice() != null ?
userCoupon.getReducePrice() : BigDecimal.ZERO;
} else if (userCoupon.getType() == ShopUserCoupon.TYPE_DISCOUNT) {
// 折扣券
if (userCoupon.getDiscount() != null && userCoupon.getDiscount() > 0 && userCoupon.getDiscount() < 100) {
BigDecimal discountRate = BigDecimal.valueOf(100 - userCoupon.getDiscount())
.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
discountAmount = orderAmount.multiply(discountRate);
}
} else if (userCoupon.getType() == ShopUserCoupon.TYPE_FREE) {
// 免费券
discountAmount = orderAmount;
}
// 优惠金额不能超过订单金额
return discountAmount.compareTo(orderAmount) > 0 ? orderAmount : discountAmount;
}
/**
* 检查优惠券是否适用于指定商品
*
* @param userCoupon 用户优惠券
* @param goodsId 商品ID
* @param categoryId 商品分类ID
* @return 是否适用
*/
public static boolean isApplicableToGoods(ShopUserCoupon userCoupon, Integer goodsId, Integer categoryId) {
if (userCoupon == null || userCoupon.getApplyRange() == null) {
return false;
}
if (userCoupon.getApplyRange() == ShopUserCoupon.APPLY_ALL) {
// 全部商品可用
return true;
} else if (userCoupon.getApplyRange() == ShopUserCoupon.APPLY_GOODS) {
// 指定商品可用
if (goodsId == null || userCoupon.getApplyRangeConfig() == null) {
return false;
}
List<String> goodsIds = Arrays.asList(userCoupon.getApplyRangeConfig().split(","));
return goodsIds.contains(goodsId.toString());
} else if (userCoupon.getApplyRange() == ShopUserCoupon.APPLY_CATEGORY) {
// 指定分类可用
if (categoryId == null || userCoupon.getApplyRangeConfig() == null) {
return false;
}
List<String> categoryIds = Arrays.asList(userCoupon.getApplyRangeConfig().split(","));
return categoryIds.contains(categoryId.toString());
}
return false;
}
/**
* 检查优惠券是否已过期
*
* @param userCoupon 用户优惠券
* @return 是否已过期
*/
public static boolean isExpired(ShopUserCoupon userCoupon) {
if (userCoupon == null || userCoupon.getEndTime() == null) {
return false;
}
return userCoupon.getEndTime().isBefore(LocalDateTime.now());
}
/**
* 检查优惠券是否可用
*
* @param userCoupon 用户优惠券
* @return 是否可用
*/
public static boolean isAvailable(ShopUserCoupon userCoupon) {
if (userCoupon == null) {
return false;
}
// 检查状态
if (userCoupon.getStatus() != ShopUserCoupon.STATUS_UNUSED) {
return false;
}
// 检查是否过期
if (isExpired(userCoupon)) {
return false;
}
// 检查是否在有效期内
LocalDateTime now = LocalDateTime.now();
if (userCoupon.getStartTime() != null && userCoupon.getStartTime().isAfter(now)) {
return false; // 还未开始
}
return true;
}
/**
* 格式化优惠券显示文本
*
* @param userCoupon 用户优惠券
* @return 显示文本
*/
public static String formatCouponDisplay(ShopUserCoupon userCoupon) {
if (userCoupon == null) {
return "";
}
StringBuilder sb = new StringBuilder();
sb.append(userCoupon.getName());
if (userCoupon.getType() == ShopUserCoupon.TYPE_REDUCE && userCoupon.getReducePrice() != null) {
sb.append("").append(userCoupon.getReducePrice()).append("");
if (userCoupon.getMinPrice() != null && userCoupon.getMinPrice().compareTo(BigDecimal.ZERO) > 0) {
sb.append("(满").append(userCoupon.getMinPrice()).append("可用)");
}
} else if (userCoupon.getType() == ShopUserCoupon.TYPE_DISCOUNT && userCoupon.getDiscount() != null) {
sb.append(" ").append(userCoupon.getDiscount()).append("");
if (userCoupon.getMinPrice() != null && userCoupon.getMinPrice().compareTo(BigDecimal.ZERO) > 0) {
sb.append("(满").append(userCoupon.getMinPrice()).append("可用)");
}
} else if (userCoupon.getType() == ShopUserCoupon.TYPE_FREE) {
sb.append(" 免费券");
}
if (userCoupon.getEndTime() != null) {
sb.append(" 有效期至").append(userCoupon.getEndTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
}
return sb.toString();
}
/**
* 生成优惠券编码
*
* @param userId 用户ID
* @param couponId 优惠券模板ID
* @return 优惠券编码
*/
public static String generateCouponCode(Integer userId, Integer couponId) {
String timestamp = String.valueOf(System.currentTimeMillis());
return String.format("CPN%s%s%s",
String.format("%08d", userId),
String.format("%06d", couponId),
timestamp.substring(timestamp.length() - 6));
}
/**
* 验证优惠券编码格式
*
* @param couponCode 优惠券编码
* @return 是否有效
*/
public static boolean isValidCouponCode(String couponCode) {
if (couponCode == null || couponCode.length() != 23) {
return false;
}
return couponCode.startsWith("CPN") && couponCode.substring(3).matches("\\d{20}");
}
}

View File

@@ -0,0 +1,78 @@
-- 优惠券功能相关数据库表
-- 1. 更新优惠券模板表
ALTER TABLE `shop_coupon`
ADD COLUMN `description` varchar(500) COMMENT '优惠券描述' AFTER `name`,
ADD COLUMN `total_count` int(11) DEFAULT -1 COMMENT '发放总数量(-1表示无限制)',
ADD COLUMN `issued_count` int(11) DEFAULT 0 COMMENT '已发放数量',
ADD COLUMN `limit_per_user` int(11) DEFAULT -1 COMMENT '每人限领数量(-1表示无限制)',
ADD COLUMN `enabled` tinyint(1) DEFAULT 1 COMMENT '是否启用(0禁用 1启用)',
MODIFY COLUMN `apply_range` int(11) DEFAULT 10 COMMENT '适用范围(10全部商品 20指定商品 30指定分类)',
MODIFY COLUMN `status` int(11) DEFAULT 0 COMMENT '状态, 0正常, 1禁用',
MODIFY COLUMN `user_id` int(11) COMMENT '创建用户ID';
-- 2. 创建用户优惠券表
CREATE TABLE `shop_user_coupon` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`coupon_id` int(11) NOT NULL COMMENT '优惠券模板ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`name` varchar(100) NOT NULL COMMENT '优惠券名称',
`description` varchar(500) DEFAULT NULL COMMENT '优惠券描述',
`type` int(11) NOT NULL COMMENT '优惠券类型(10满减券 20折扣券 30免费劵)',
`reduce_price` decimal(10,2) DEFAULT NULL COMMENT '满减券-减免金额',
`discount` int(11) DEFAULT NULL COMMENT '折扣券-折扣率(0-100)',
`min_price` decimal(10,2) DEFAULT NULL COMMENT '最低消费金额',
`apply_range` int(11) DEFAULT 10 COMMENT '适用范围(10全部商品 20指定商品 30指定分类)',
`apply_range_config` text COMMENT '适用范围配置(json格式)',
`start_time` datetime DEFAULT NULL COMMENT '有效期开始时间',
`end_time` datetime DEFAULT NULL COMMENT '有效期结束时间',
`status` int(11) DEFAULT 0 COMMENT '使用状态(0未使用 1已使用 2已过期)',
`use_time` datetime DEFAULT NULL COMMENT '使用时间',
`order_id` bigint(20) DEFAULT NULL COMMENT '使用订单ID',
`order_no` varchar(50) DEFAULT NULL COMMENT '使用订单号',
`obtain_type` int(11) DEFAULT 10 COMMENT '获取方式(10主动领取 20系统发放 30活动赠送)',
`obtain_source` varchar(200) DEFAULT NULL COMMENT '获取来源描述',
`deleted` tinyint(1) DEFAULT 0 COMMENT '是否删除, 0否, 1是',
`tenant_id` int(11) DEFAULT NULL COMMENT '租户id',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_coupon_id` (`coupon_id`),
KEY `idx_status` (`status`),
KEY `idx_end_time` (`end_time`),
KEY `idx_order_id` (`order_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户优惠券';
-- 3. 创建优惠券使用记录表(可选,用于详细统计)
CREATE TABLE `shop_coupon_usage_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_coupon_id` bigint(20) NOT NULL COMMENT '用户优惠券ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`order_id` bigint(20) NOT NULL COMMENT '订单ID',
`order_no` varchar(50) NOT NULL COMMENT '订单号',
`order_amount` decimal(10,2) NOT NULL COMMENT '订单金额',
`discount_amount` decimal(10,2) NOT NULL COMMENT '优惠金额',
`final_amount` decimal(10,2) NOT NULL COMMENT '最终金额',
`use_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '使用时间',
`tenant_id` int(11) DEFAULT NULL COMMENT '租户id',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_use_time` (`use_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券使用记录';
-- 4. 插入示例优惠券模板数据
INSERT INTO `shop_coupon` (`name`, `description`, `type`, `reduce_price`, `discount`, `min_price`, `total_count`, `issued_count`, `limit_per_user`, `expire_type`, `expire_day`, `apply_range`, `apply_range_config`, `enabled`, `sort_number`, `status`, `user_id`, `tenant_id`) VALUES
('新用户专享券', '新用户注册即可领取满100减20', 10, 20.00, NULL, 100.00, 1000, 0, 1, 10, 30, 10, NULL, 1, 1, 0, 1, 1),
('满减优惠券', '全场通用满200减50', 10, 50.00, NULL, 200.00, 500, 0, 2, 20, NULL, 10, NULL, 1, 2, 0, 1, 1),
('折扣优惠券', '全场9折优惠券', 20, NULL, 10, 50.00, 300, 0, 1, 10, 15, 10, NULL, 1, 3, 0, 1, 1),
('生日专享券', '生日当天专享满50减30', 10, 30.00, NULL, 50.00, -1, 0, 1, 10, 7, 10, NULL, 1, 4, 0, 1, 1),
('消费返券', '消费满500返100优惠券', 10, 100.00, NULL, 300.00, -1, 0, -1, 10, 60, 10, NULL, 1, 5, 0, 1, 1);
-- 5. 创建索引优化查询性能
CREATE INDEX `idx_shop_coupon_enabled_status` ON `shop_coupon` (`enabled`, `status`);
CREATE INDEX `idx_shop_coupon_expire_type` ON `shop_coupon` (`expire_type`, `start_time`, `end_time`);
CREATE INDEX `idx_shop_user_coupon_user_status` ON `shop_user_coupon` (`user_id`, `status`);
CREATE INDEX `idx_shop_user_coupon_expire` ON `shop_user_coupon` (`status`, `end_time`);