first commit

This commit is contained in:
dev
2026-02-22 12:12:02 +08:00
commit b236048a24
1467 changed files with 160403 additions and 0 deletions
@@ -0,0 +1,133 @@
# 优惠券中心模块 (ruoyi-mall-coupon)
## 模块简介
`ruoyi-mall-coupon` 是商城系统的优惠券中心模块,提供独立的优惠券服务,支持发券、核销、过期处理等功能。
## 功能特性
### 1. 优惠券管理 ✅
- ✅ 优惠券CRUD操作
- ✅ 优惠券类型管理(满减、折扣、现金)
- ✅ 优惠券发放规则配置
- ✅ 优惠券有效期管理
- ✅ 优惠券状态管理(启用/禁用)
- ✅ 优惠券导出功能
### 2. 用户优惠券 ✅
- ✅ 用户领券
- ✅ 用户优惠券列表查询
- ✅ 优惠券使用(订单结算时调用)
- ✅ 优惠券退还(订单取消时调用)
- ✅ 优惠券过期处理(定时任务)✅
### 3. 优惠券统计
- [ ] 发放统计(可在管理端通过列表查询实现)
- [ ] 使用统计(可在管理端通过列表查询实现)
- [ ] 核销统计(可在管理端通过列表查询实现)
## 数据库表
- `mall_coupon` - 优惠券表
- `mall_coupon_user` - 用户优惠券表
详细表结构请参考:`script/sql/ry_mall.sql`
## 模块结构
```
org.dromara.mall.coupon
├── controller
│ ├── admin // 管理端API
│ │ ├── AdminMallCouponController.java
│ │ └── AdminMallCouponUserController.java
│ └── app // 用户端API
│ ├── AppMallCouponController.java
│ └── AppMallCouponUserController.java
├── domain
│ ├── MallCoupon.java
│ ├── MallCouponUser.java
│ ├── bo // 业务对象
│ │ ├── MallCouponBo.java
│ │ └── MallCouponUserBo.java
│ └── vo // 视图对象
│ ├── MallCouponVo.java
│ └── MallCouponUserVo.java
├── mapper
│ ├── MallCouponMapper.java
│ └── MallCouponUserMapper.java
├── service
│ ├── MallCouponService.java
│ ├── MallCouponUserService.java
│ └── impl
│ ├── MallCouponServiceImpl.java
│ └── MallCouponUserServiceImpl.java
├── job // 定时任务
│ └── MallCouponJob.java
└── config
└── MallCouponAutoConfiguration.java
```
## API接口
### 管理端接口
#### 优惠券管理 (`/mall/coupon`)
- `GET /mall/coupon/list` - 查询优惠券列表(分页)
- `GET /mall/coupon/{id}` - 获取优惠券详情
- `POST /mall/coupon` - 新增优惠券
- `PUT /mall/coupon` - 修改优惠券
- `DELETE /mall/coupon/{ids}` - 删除优惠券
- `POST /mall/coupon/export` - 导出优惠券列表
- `PUT /mall/coupon/changeStatus/{id}` - 启用/禁用优惠券
#### 用户优惠券管理 (`/mall/coupon/user`)
- `GET /mall/coupon/user/list` - 查询用户优惠券列表(分页)
- `GET /mall/coupon/user/{id}` - 获取用户优惠券详情
- `DELETE /mall/coupon/user/{ids}` - 删除用户优惠券
- `POST /mall/coupon/user/export` - 导出用户优惠券列表
### 移动端接口
#### 优惠券 (`/app/mall/coupon`)
- `GET /app/mall/coupon/available/list` - 查询可领取的优惠券列表
- `GET /app/mall/coupon/{id}` - 获取优惠券详情
#### 用户优惠券 (`/app/mall/coupon/user`)
- `GET /app/mall/coupon/user/list` - 查询用户优惠券列表
- `GET /app/mall/coupon/user/available/list` - 查询用户可用优惠券列表(未使用且未过期)
- `GET /app/mall/coupon/user/{id}` - 获取用户优惠券详情
- `POST /app/mall/coupon/user/receive/{couponId}` - 领取优惠券
## 优惠券类型说明
- **0 - 满减券**: 需要设置优惠金额和使用门槛
- **1 - 折扣券**: 需要设置折扣率(百分比)和使用门槛
- **2 - 现金券**: 需要设置优惠金额,无使用门槛
## 依赖模块
- `ruoyi-common-*` - 通用工具模块
- `ruoyi-common-excel` - Excel导出功能
- `ruoyi-common-satoken` - 用户身份认证
## 待实现功能
- [ ] 优惠券统计功能(可在管理端通过列表查询和筛选实现)
## 使用说明
### 订单结算时使用优惠券
在订单提交时,可以调用 `MallCouponUserService.useCoupon(couponUserId, orderId)` 来使用优惠券。
### 订单取消时退还优惠券
在订单取消时,可以调用 `MallCouponUserService.returnCoupon(couponUserId)` 来退还优惠券。
@@ -0,0 +1,67 @@
<?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">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-mall</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-mall-coupon</artifactId>
<description>
ruoyi-mall-coupon 优惠券中心模块
</description>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-doc</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-security</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-log</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,23 @@
package org.dromara.mall.coupon.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* 优惠券中心自动配置类
* 主要用于扫描优惠券中心模块的组件
*
* @author pengles
*/
@Slf4j
@Configuration
@ComponentScan(basePackages = "org.dromara.mall.coupon")
@ConditionalOnProperty(prefix = "ruoyi.mall.coupon", name = "enabled", havingValue = "true", matchIfMissing = true)
public class MallCouponAutoConfiguration {
public MallCouponAutoConfiguration() {
log.info("优惠券中心模块已启用");
}
}
@@ -0,0 +1,140 @@
package org.dromara.mall.coupon.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.mall.coupon.domain.bo.MallCouponBo;
import org.dromara.mall.coupon.domain.vo.CouponStatisticsVo;
import org.dromara.mall.coupon.domain.vo.MallCouponVo;
import org.dromara.mall.coupon.service.MallCouponService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* 优惠券管理
*
* @author pengles
*/
@Tag(name = "管理端-优惠券管理", description = "优惠券管理接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/mall/coupon")
public class AdminMallCouponController extends BaseController {
private final MallCouponService mallCouponService;
/**
* 查询优惠券列表
*/
@Operation(summary = "查询优惠券列表", description = "分页查询优惠券列表")
@SaCheckPermission("mall:coupon:list")
@GetMapping("/list")
public TableDataInfo<MallCouponVo> list(@Parameter(description = "优惠券查询条件") MallCouponBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
return mallCouponService.queryPageList(bo, pageQuery);
}
/**
* 导出售后申请列表
*/
@Operation(summary = "导出优惠券列表", description = "导出优惠券到Excel")
@SaCheckPermission("mall:coupon:export")
@Log(title = "优惠券", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Parameter(description = "优惠券查询条件") MallCouponBo bo,
@Parameter(description = "响应对象") HttpServletResponse response) {
List<MallCouponVo> list = mallCouponService.queryList(bo);
ExcelUtil.exportExcel(list, "优惠券", MallCouponVo.class, response);
}
/**
* 获取优惠券详细信息
*/
@Operation(summary = "获取优惠券详情", description = "根据优惠券ID获取详细信息")
@SaCheckPermission("mall:coupon:query")
@GetMapping("/{id}")
public R<MallCouponVo> getInfo(@Parameter(description = "优惠券ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(mallCouponService.queryById(id));
}
/**
* 新增优惠券
*/
@Operation(summary = "新增优惠券", description = "创建新的优惠券")
@SaCheckPermission("mall:coupon:add")
@Log(title = "优惠券", businessType = BusinessType.INSERT)
@PostMapping
public R<Void> add(@Parameter(description = "优惠券信息") @Validated @RequestBody MallCouponBo bo) {
return toAjax(mallCouponService.insertByBo(bo));
}
/**
* 修改优惠券
*/
@Operation(summary = "修改优惠券", description = "更新优惠券信息")
@SaCheckPermission("mall:coupon:edit")
@Log(title = "优惠券", businessType = BusinessType.UPDATE)
@PutMapping
public R<Void> edit(@Parameter(description = "优惠券信息") @Validated @RequestBody MallCouponBo bo) {
return toAjax(mallCouponService.updateByBo(bo));
}
/**
* 删除优惠券
*/
@Operation(summary = "删除优惠券", description = "删除优惠券")
@SaCheckPermission("mall:coupon:remove")
@Log(title = "优惠券", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@Parameter(description = "优惠券ID数组", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(mallCouponService.deleteWithValidByIds(Arrays.asList(ids), true));
}
/**
* 启用/禁用优惠券
*/
@Operation(summary = "启用/禁用优惠券", description = "更改优惠券状态")
@SaCheckPermission("mall:coupon:edit")
@Log(title = "优惠券", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus/{id}")
public R<Void> changeStatus(@Parameter(description = "优惠券ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id,
@Parameter(description = "状态", required = true)
@NotNull(message = "状态不能为空")
@RequestParam Integer status) {
return toAjax(mallCouponService.changeStatus(id, status));
}
/**
* 获取优惠券统计信息
*/
@Operation(summary = "获取优惠券统计信息", description = "获取优惠券的发放、使用、核销等统计数据")
@SaCheckPermission("mall:coupon:query")
@GetMapping("/statistics")
public R<CouponStatisticsVo> getStatistics(
@Parameter(description = "优惠券ID,为空则统计所有优惠券")
@RequestParam(required = false) Long couponId) {
return R.ok(mallCouponService.getStatistics(couponId));
}
}
@@ -0,0 +1,89 @@
package org.dromara.mall.coupon.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.mall.coupon.domain.bo.MallCouponUserBo;
import org.dromara.mall.coupon.domain.vo.MallCouponUserVo;
import org.dromara.mall.coupon.service.MallCouponUserService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* 用户优惠券管理
*
* @author pengles
*/
@Tag(name = "管理端-用户优惠券管理", description = "用户优惠券管理接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/mall/coupon/user")
public class AdminMallCouponUserController extends BaseController {
private final MallCouponUserService mallCouponUserService;
/**
* 查询用户优惠券列表
*/
@Operation(summary = "查询用户优惠券列表", description = "分页查询用户优惠券列表")
@SaCheckPermission("mall:coupon:user:list")
@GetMapping("/list")
public TableDataInfo<MallCouponUserVo> list(@Parameter(description = "用户优惠券查询条件") MallCouponUserBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
return mallCouponUserService.queryPageList(bo, pageQuery);
}
/**
* 导出用户优惠券列表
*/
@Operation(summary = "导出用户优惠券列表", description = "导出用户优惠券到Excel")
@SaCheckPermission("mall:coupon:user:export")
@Log(title = "用户优惠券", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Parameter(description = "用户优惠券查询条件") MallCouponUserBo bo,
@Parameter(description = "响应对象") HttpServletResponse response) {
List<MallCouponUserVo> list = mallCouponUserService.queryList(bo);
ExcelUtil.exportExcel(list, "用户优惠券", MallCouponUserVo.class, response);
}
/**
* 获取用户优惠券详细信息
*/
@Operation(summary = "获取用户优惠券详情", description = "根据用户优惠券ID获取详细信息")
@SaCheckPermission("mall:coupon:user:query")
@GetMapping("/{id}")
public R<MallCouponUserVo> getInfo(@Parameter(description = "用户优惠券ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(mallCouponUserService.queryById(id));
}
/**
* 删除用户优惠券
*/
@Operation(summary = "删除用户优惠券", description = "删除用户优惠券")
@SaCheckPermission("mall:coupon:user:remove")
@Log(title = "用户优惠券", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@Parameter(description = "用户优惠券ID数组", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(mallCouponUserService.deleteWithValidByIds(Arrays.asList(ids), true));
}
}
@@ -0,0 +1,50 @@
package org.dromara.mall.coupon.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.mall.coupon.domain.vo.MallCouponVo;
import org.dromara.mall.coupon.service.MallCouponService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 优惠券(移动端)
*
* @author pengles
*/
@Tag(name = "移动端-优惠券", description = "优惠券接口")
@RequiredArgsConstructor
@RestController
@RequestMapping("/app/mall/coupon")
public class AppMallCouponController {
private final MallCouponService mallCouponService;
/**
* 查询可领取的优惠券列表
*/
@Operation(summary = "查询可领取的优惠券列表", description = "查询用户可以领取的优惠券列表")
@GetMapping("/available/list")
public R<List<MallCouponVo>> getAvailableList() {
List<MallCouponVo> list = mallCouponService.queryAvailableList();
return R.ok(list);
}
/**
* 获取优惠券详情
*/
@Operation(summary = "获取优惠券详情", description = "根据优惠券ID获取详细信息")
@GetMapping("/{id}")
public R<MallCouponVo> getInfo(@Parameter(description = "优惠券ID", required = true)
@PathVariable Long id) {
return R.ok(mallCouponService.queryById(id));
}
}
@@ -0,0 +1,78 @@
package org.dromara.mall.coupon.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.mall.coupon.domain.bo.MallCouponUserBo;
import org.dromara.mall.coupon.domain.vo.MallCouponUserVo;
import org.dromara.mall.coupon.service.MallCouponUserService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户优惠券(移动端)
*
* @author pengles
*/
@Tag(name = "移动端-用户优惠券", description = "用户优惠券接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/app/mall/coupon/user")
public class AppMallCouponUserController {
private final MallCouponUserService mallCouponUserService;
/**
* 查询用户优惠券列表
*/
@Operation(summary = "查询用户优惠券列表", description = "查询当前用户的优惠券列表")
@GetMapping("/list")
public R<TableDataInfo<MallCouponUserVo>> list(@Parameter(description = "用户优惠券查询条件") MallCouponUserBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
// 移动端只能查询当前用户的优惠券
TableDataInfo<MallCouponUserVo> result = mallCouponUserService.queryPageList(bo, pageQuery);
return R.ok(result);
}
/**
* 查询用户可用优惠券列表
*/
@Operation(summary = "查询用户可用优惠券列表", description = "查询当前用户可用的优惠券列表(未使用且未过期)")
@GetMapping("/available/list")
public R<List<MallCouponUserVo>> getAvailableList() {
Long userId = org.dromara.common.satoken.utils.LoginHelper.getUserId();
List<MallCouponUserVo> list = mallCouponUserService.queryAvailableList(userId);
return R.ok(list);
}
/**
* 获取用户优惠券详情
*/
@Operation(summary = "获取用户优惠券详情", description = "根据用户优惠券ID获取详细信息")
@GetMapping("/{id}")
public R<MallCouponUserVo> getInfo(@Parameter(description = "用户优惠券ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(mallCouponUserService.queryById(id));
}
/**
* 领取优惠券
*/
@Operation(summary = "领取优惠券", description = "用户领取优惠券")
@PostMapping("/receive/{couponId}")
public R<Void> receiveCoupon(@Parameter(description = "优惠券ID", required = true)
@NotNull(message = "优惠券ID不能为空")
@PathVariable Long couponId) {
return R.ok(mallCouponUserService.receiveCoupon(couponId));
}
}
@@ -0,0 +1,118 @@
package org.dromara.mall.coupon.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 优惠券对象 mall_coupon
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@TableName("mall_coupon")
public class MallCoupon implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 优惠券ID
*/
@TableId
private Long id;
/**
* 优惠券名称
*/
private String name;
/**
* 优惠券类型(0满减 1折扣 2现金)
*/
private Integer type;
/**
* 优惠金额(分)
*/
private Integer discountAmount;
/**
* 折扣率(%)
*/
private BigDecimal discountRate;
/**
* 使用门槛(分)
*/
private Integer minAmount;
/**
* 发放总数(null表示不限)
*/
private Integer totalCount;
/**
* 已使用数量
*/
private Integer usedCount;
/**
* 每人限领数量
*/
private Integer perLimit;
/**
* 有效开始时间
*/
private Date validStartTime;
/**
* 有效结束时间
*/
private Date validEndTime;
/**
* 状态(0启用 1禁用)
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
@@ -0,0 +1,54 @@
package org.dromara.mall.coupon.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* 用户优惠券对象 mall_coupon_user
*
* @author pengles
*/
@Data
@TableName("mall_coupon_user")
public class MallCouponUser {
/**
* ID
*/
@TableId
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 优惠券ID
*/
private Long couponId;
/**
* 状态(0未使用 1已使用 2已过期)
*/
private Integer status;
/**
* 使用时间
*/
private Date usedTime;
/**
* 使用的订单ID
*/
private Long orderId;
/**
* 领取时间
*/
private Date createTime;
}
@@ -0,0 +1,99 @@
package org.dromara.mall.coupon.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.mall.coupon.domain.MallCoupon;
import java.math.BigDecimal;
import java.util.Date;
/**
* 优惠券业务对象 mall_coupon
*
* @author pengles
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = MallCoupon.class, reverseConvertGenerate = false)
public class MallCouponBo extends BaseEntity {
/**
* 优惠券ID
*/
private Long id;
/**
* 优惠券名称
*/
@NotBlank(message = "优惠券名称不能为空")
private String name;
/**
* 优惠券类型(0满减 1折扣 2现金)
*/
@NotNull(message = "优惠券类型不能为空")
private Integer type;
/**
* 优惠金额(分)
*/
@Min(value = 0, message = "优惠金额不能为负数")
private Integer discountAmount;
/**
* 折扣率(%)
*/
@Min(value = 0, message = "折扣率不能为负数")
private BigDecimal discountRate;
/**
* 使用门槛(分)
*/
@Min(value = 0, message = "使用门槛不能为负数")
private Integer minAmount;
/**
* 发放总数(null表示不限)
*/
@Min(value = 1, message = "发放总数必须大于0")
private Integer totalCount;
/**
* 已使用数量
*/
private Integer usedCount;
/**
* 每人限领数量
*/
@Min(value = 1, message = "每人限领数量必须大于0")
private Integer perLimit;
/**
* 有效开始时间
*/
@NotNull(message = "有效开始时间不能为空")
private Date validStartTime;
/**
* 有效结束时间
*/
@NotNull(message = "有效结束时间不能为空")
private Date validEndTime;
/**
* 状态(0启用 1禁用)
*/
private Integer status;
/**
* 备注
*/
private String remark;
}
@@ -0,0 +1,51 @@
package org.dromara.mall.coupon.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.dromara.mall.coupon.domain.MallCouponUser;
import java.util.Date;
/**
* 用户优惠券业务对象 mall_coupon_user
*
* @author pengles
*/
@Data
@AutoMapper(target = MallCouponUser.class, reverseConvertGenerate = false)
public class MallCouponUserBo {
/**
* ID
*/
private Long id;
/**
* 用户ID
*/
@NotNull(message = "用户ID不能为空")
private Long userId;
/**
* 优惠券ID
*/
@NotNull(message = "优惠券ID不能为空")
private Long couponId;
/**
* 状态(0未使用 1已使用 2已过期)
*/
private Integer status;
/**
* 使用时间
*/
private Date usedTime;
/**
* 使用的订单ID
*/
private Long orderId;
}
@@ -0,0 +1,49 @@
package org.dromara.mall.coupon.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 优惠券统计数据视图对象
*
* @author pengles
*/
@Data
public class CouponStatisticsVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 优惠券ID
*/
private Long couponId;
/**
* 优惠券名称
*/
private String couponName;
/**
* 发放总数(已领取数量)
*/
private Long issuedCount;
/**
* 使用数量(已使用数量)
*/
private Long usedCount;
/**
* 使用率(使用数量/发放数量,百分比)
*/
private Double usageRate;
/**
* 核销金额(分,基于订单优惠券使用记录表)
*/
private Long redeemedAmount;
}
@@ -0,0 +1,63 @@
package org.dromara.mall.coupon.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.mall.coupon.domain.MallCouponUser;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 用户优惠券视图对象 mall_coupon_user
*
* @author pengles
*/
@Data
@AutoMapper(target = MallCouponUser.class)
public class MallCouponUserVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 优惠券ID
*/
private Long couponId;
/**
* 状态(0未使用 1已使用 2已过期)
*/
private Integer status;
/**
* 使用时间
*/
private Date usedTime;
/**
* 使用的订单ID
*/
private Long orderId;
/**
* 领取时间
*/
private Date createTime;
/**
* 优惠券信息(关联查询)
*/
private MallCouponVo couponInfo;
}
@@ -0,0 +1,99 @@
package org.dromara.mall.coupon.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.mall.coupon.domain.MallCoupon;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 优惠券视图对象 mall_coupon
*
* @author pengles
*/
@Data
@AutoMapper(target = MallCoupon.class)
public class MallCouponVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 优惠券ID
*/
private Long id;
/**
* 优惠券名称
*/
private String name;
/**
* 优惠券类型(0满减 1折扣 2现金)
*/
private Integer type;
/**
* 优惠金额(分)
*/
private Integer discountAmount;
/**
* 折扣率(%)
*/
private BigDecimal discountRate;
/**
* 使用门槛(分)
*/
private Integer minAmount;
/**
* 发放总数(null表示不限)
*/
private Integer totalCount;
/**
* 已使用数量
*/
private Integer usedCount;
/**
* 每人限领数量
*/
private Integer perLimit;
/**
* 有效开始时间
*/
private Date validStartTime;
/**
* 有效结束时间
*/
private Date validEndTime;
/**
* 状态(0启用 1禁用)
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
@@ -0,0 +1,37 @@
package org.dromara.mall.coupon.job;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.mall.coupon.service.MallCouponUserService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 优惠券定时任务
*
* @author pengles
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class MallCouponJob {
private final MallCouponUserService couponUserService;
/**
* 优惠券过期处理任务
* 每天凌晨2点执行一次
* 处理所有已过期的优惠券,将状态更新为已过期(status=2)
*/
@Scheduled(cron = "0 0 2 * * ?")
public void processExpiredCoupons() {
log.info("开始执行优惠券过期处理任务");
try {
couponUserService.processExpiredCoupons();
log.info("优惠券过期处理任务执行完成");
} catch (Exception e) {
log.error("优惠券过期处理任务执行失败", e);
}
}
}
@@ -0,0 +1,16 @@
package org.dromara.mall.coupon.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.mall.coupon.domain.MallCoupon;
import org.dromara.mall.coupon.domain.vo.MallCouponVo;
/**
* 优惠券Mapper接口
*
* @author pengles
*/
@Mapper
public interface MallCouponMapper extends BaseMapperPlus<MallCoupon, MallCouponVo> {
}
@@ -0,0 +1,16 @@
package org.dromara.mall.coupon.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.mall.coupon.domain.MallCouponUser;
import org.dromara.mall.coupon.domain.vo.MallCouponUserVo;
/**
* 用户优惠券Mapper接口
*
* @author pengles
*/
@Mapper
public interface MallCouponUserMapper extends BaseMapperPlus<MallCouponUser, MallCouponUserVo> {
}
@@ -0,0 +1,67 @@
package org.dromara.mall.coupon.service;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.mall.coupon.domain.bo.MallCouponBo;
import org.dromara.mall.coupon.domain.vo.CouponStatisticsVo;
import org.dromara.mall.coupon.domain.vo.MallCouponVo;
import java.util.Collection;
import java.util.List;
/**
* 优惠券Service接口
*
* @author pengles
*/
public interface MallCouponService {
/**
* 查询优惠券
*/
MallCouponVo queryById(Long id);
/**
* 查询优惠券列表
*/
TableDataInfo<MallCouponVo> queryPageList(MallCouponBo bo, PageQuery pageQuery);
/**
* 查询优惠券列表
*/
List<MallCouponVo> queryList(MallCouponBo bo);
/**
* 查询可领取的优惠券列表(移动端)
*/
List<MallCouponVo> queryAvailableList();
/**
* 新增优惠券
*/
Boolean insertByBo(MallCouponBo bo);
/**
* 修改优惠券
*/
Boolean updateByBo(MallCouponBo bo);
/**
* 校验并批量删除优惠券信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 启用/禁用优惠券
*/
Boolean changeStatus(Long id, Integer status);
/**
* 获取优惠券统计信息
*
* @param couponId 优惠券ID,如果为空则统计所有优惠券
* @return 优惠券统计数据
*/
CouponStatisticsVo getStatistics(Long couponId);
}
@@ -0,0 +1,63 @@
package org.dromara.mall.coupon.service;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.mall.coupon.domain.bo.MallCouponUserBo;
import org.dromara.mall.coupon.domain.vo.MallCouponUserVo;
import java.util.Collection;
import java.util.List;
/**
* 用户优惠券Service接口
*
* @author pengles
*/
public interface MallCouponUserService {
/**
* 查询用户优惠券
*/
MallCouponUserVo queryById(Long id);
/**
* 查询用户优惠券列表
*/
TableDataInfo<MallCouponUserVo> queryPageList(MallCouponUserBo bo, PageQuery pageQuery);
/**
* 查询用户优惠券列表
*/
List<MallCouponUserVo> queryList(MallCouponUserBo bo);
/**
* 查询用户可用优惠券列表
*/
List<MallCouponUserVo> queryAvailableList(Long userId);
/**
* 用户领取优惠券
*/
Boolean receiveCoupon(Long couponId);
/**
* 使用优惠券(订单结算时调用)
*/
Boolean useCoupon(Long couponUserId, Long orderId);
/**
* 退还优惠券(订单取消时调用)
*/
Boolean returnCoupon(Long couponUserId);
/**
* 批量删除用户优惠券信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 处理过期优惠券(定时任务调用)
*/
void processExpiredCoupons();
}
@@ -0,0 +1,264 @@
package org.dromara.mall.coupon.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.mall.coupon.domain.MallCoupon;
import org.dromara.mall.coupon.domain.MallCouponUser;
import org.dromara.mall.coupon.domain.bo.MallCouponBo;
import org.dromara.mall.coupon.domain.vo.CouponStatisticsVo;
import org.dromara.mall.coupon.domain.vo.MallCouponVo;
import org.dromara.mall.coupon.mapper.MallCouponMapper;
import org.dromara.mall.coupon.mapper.MallCouponUserMapper;
import org.dromara.mall.coupon.service.MallCouponService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* 优惠券Service业务层处理
*
* @author pengles
*/
@RequiredArgsConstructor
@Service
public class MallCouponServiceImpl implements MallCouponService {
private final MallCouponMapper baseMapper;
private final MallCouponUserMapper couponUserMapper;
/**
* 查询优惠券
*/
@Override
public MallCouponVo queryById(Long id) {
return baseMapper.selectVoById(id);
}
/**
* 查询优惠券列表
*/
@Override
public TableDataInfo<MallCouponVo> queryPageList(MallCouponBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<MallCoupon> lqw = buildQueryWrapper(bo);
Page<MallCouponVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询优惠券列表
*/
@Override
public List<MallCouponVo> queryList(MallCouponBo bo) {
LambdaQueryWrapper<MallCoupon> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
/**
* 查询可领取的优惠券列表(移动端)
*/
@Override
public List<MallCouponVo> queryAvailableList() {
LambdaQueryWrapper<MallCoupon> lqw = new LambdaQueryWrapper<>();
lqw.eq(MallCoupon::getStatus, 0); // 启用状态
Date now = new Date();
lqw.le(MallCoupon::getValidStartTime, now); // 已开始
lqw.ge(MallCoupon::getValidEndTime, now); // 未结束
// 发放总数未限制或已发放数量小于总数
lqw.and(wrapper -> wrapper
.isNull(MallCoupon::getTotalCount)
.or(wrapper2 -> wrapper2.apply("used_count < total_count"))
);
lqw.orderByDesc(MallCoupon::getCreateTime);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<MallCoupon> buildQueryWrapper(MallCouponBo bo) {
LambdaQueryWrapper<MallCoupon> lqw = new LambdaQueryWrapper<>();
lqw.like(StringUtils.isNotBlank(bo.getName()), MallCoupon::getName, bo.getName());
lqw.eq(bo.getType() != null, MallCoupon::getType, bo.getType());
lqw.eq(bo.getStatus() != null, MallCoupon::getStatus, bo.getStatus());
lqw.ge(bo.getValidStartTime() != null, MallCoupon::getValidStartTime, bo.getValidStartTime());
lqw.le(bo.getValidEndTime() != null, MallCoupon::getValidEndTime, bo.getValidEndTime());
lqw.orderByDesc(MallCoupon::getCreateTime);
return lqw;
}
/**
* 新增优惠券
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean insertByBo(MallCouponBo bo) {
MallCoupon add = MapstructUtils.convert(bo, MallCoupon.class);
validEntityBeforeSave(add);
// 设置默认值
if (add.getMinAmount() == null) {
add.setMinAmount(0);
}
if (add.getUsedCount() == null) {
add.setUsedCount(0);
}
if (add.getPerLimit() == null) {
add.setPerLimit(1);
}
if (add.getStatus() == null) {
add.setStatus(0); // 默认启用
}
// 校验优惠券类型和对应字段
validateCouponType(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改优惠券
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(MallCouponBo bo) {
MallCoupon update = MapstructUtils.convert(bo, MallCoupon.class);
validEntityBeforeSave(update);
// 校验优惠券类型和对应字段
validateCouponType(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(MallCoupon entity) {
// 校验有效期
if (entity.getValidStartTime() != null && entity.getValidEndTime() != null) {
if (entity.getValidEndTime().before(entity.getValidStartTime())) {
throw new ServiceException("有效结束时间不能早于开始时间");
}
}
}
/**
* 校验优惠券类型和对应字段
*/
private void validateCouponType(MallCoupon entity) {
if (entity.getType() == null) {
return;
}
// 0-满减,1-折扣,2-现金
if (entity.getType() == 0) {
// 满减类型必须设置优惠金额
if (entity.getDiscountAmount() == null || entity.getDiscountAmount() <= 0) {
throw new ServiceException("满减类型优惠券必须设置优惠金额");
}
} else if (entity.getType() == 1) {
// 折扣类型必须设置折扣率
if (entity.getDiscountRate() == null || entity.getDiscountRate().compareTo(java.math.BigDecimal.ZERO) <= 0) {
throw new ServiceException("折扣类型优惠券必须设置折扣率");
}
if (entity.getDiscountRate().compareTo(new java.math.BigDecimal("100")) > 0) {
throw new ServiceException("折扣率不能大于100%");
}
} else if (entity.getType() == 2) {
// 现金类型必须设置优惠金额
if (entity.getDiscountAmount() == null || entity.getDiscountAmount() <= 0) {
throw new ServiceException("现金类型优惠券必须设置优惠金额");
}
}
}
/**
* 批量删除优惠券
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// 可以添加业务校验逻辑,如:已使用数量大于0的优惠券不能删除
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 启用/禁用优惠券
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean changeStatus(Long id, Integer status) {
MallCoupon coupon = baseMapper.selectById(id);
if (coupon == null) {
throw new ServiceException("优惠券不存在");
}
if (status != 0 && status != 1) {
throw new ServiceException("状态值不正确");
}
coupon.setStatus(status);
return baseMapper.updateById(coupon) > 0;
}
/**
* 获取优惠券统计信息
*/
@Override
public CouponStatisticsVo getStatistics(Long couponId) {
CouponStatisticsVo vo = new CouponStatisticsVo();
vo.setCouponId(couponId);
// 如果指定了优惠券ID,获取优惠券信息
if (couponId != null) {
MallCouponVo couponVo = baseMapper.selectVoById(couponId);
if (couponVo != null) {
vo.setCouponName(couponVo.getName());
}
}
// 统计发放数量(已领取数量,即mall_coupon_user表中的总记录数)
LambdaQueryWrapper<MallCouponUser> issuedWrapper = new LambdaQueryWrapper<>();
if (couponId != null) {
issuedWrapper.eq(MallCouponUser::getCouponId, couponId);
}
Long issuedCount = couponUserMapper.selectCount(issuedWrapper);
vo.setIssuedCount(issuedCount != null ? issuedCount : 0L);
// 统计使用数量(status=1的记录数)
LambdaQueryWrapper<MallCouponUser> usedWrapper = new LambdaQueryWrapper<>();
if (couponId != null) {
usedWrapper.eq(MallCouponUser::getCouponId, couponId);
}
usedWrapper.eq(MallCouponUser::getStatus, 1); // 已使用
Long usedCount = couponUserMapper.selectCount(usedWrapper);
vo.setUsedCount(usedCount != null ? usedCount : 0L);
// 计算使用率
if (vo.getIssuedCount() > 0) {
double usageRate = (double) vo.getUsedCount() / vo.getIssuedCount() * 100;
vo.setUsageRate(Math.round(usageRate * 100.0) / 100.0);
} else {
vo.setUsageRate(0.0);
}
// 核销金额统计(基于mall_order_coupon表,需要在交易中心模块查询)
// 由于跨模块依赖问题,这里暂时设为0
// 如果需要在优惠券模块直接查询,可以考虑通过Feign调用交易中心模块的接口,或者将统计功能放在交易中心模块
// 当前实现:如果交易中心模块已实现订单优惠券记录,可以通过依赖注入MallOrderCouponService来查询
// TODO: 考虑通过服务间调用或事件机制来获取核销金额统计
vo.setRedeemedAmount(0L);
return vo;
}
}
@@ -0,0 +1,296 @@
package org.dromara.mall.coupon.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.mall.coupon.domain.MallCoupon;
import org.dromara.mall.coupon.domain.MallCouponUser;
import org.dromara.mall.coupon.domain.bo.MallCouponUserBo;
import org.dromara.mall.coupon.domain.vo.MallCouponUserVo;
import org.dromara.mall.coupon.domain.vo.MallCouponVo;
import org.dromara.mall.coupon.mapper.MallCouponMapper;
import org.dromara.mall.coupon.mapper.MallCouponUserMapper;
import org.dromara.mall.coupon.service.MallCouponUserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 用户优惠券Service业务层处理
*
* @author pengles
*/
@RequiredArgsConstructor
@Service
public class MallCouponUserServiceImpl implements MallCouponUserService {
private final MallCouponUserMapper baseMapper;
private final MallCouponMapper couponMapper;
/**
* 查询用户优惠券
*/
@Override
public MallCouponUserVo queryById(Long id) {
MallCouponUserVo vo = baseMapper.selectVoById(id);
if (vo != null && vo.getCouponId() != null) {
// 关联查询优惠券信息
MallCouponVo couponVo = couponMapper.selectVoById(vo.getCouponId());
vo.setCouponInfo(couponVo);
}
return vo;
}
/**
* 查询用户优惠券列表
*/
@Override
public TableDataInfo<MallCouponUserVo> queryPageList(MallCouponUserBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<MallCouponUser> lqw = buildQueryWrapper(bo);
Page<MallCouponUserVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
// 关联查询优惠券信息
fillCouponInfo(result.getRecords());
return TableDataInfo.build(result);
}
/**
* 查询用户优惠券列表
*/
@Override
public List<MallCouponUserVo> queryList(MallCouponUserBo bo) {
LambdaQueryWrapper<MallCouponUser> lqw = buildQueryWrapper(bo);
List<MallCouponUserVo> list = baseMapper.selectVoList(lqw);
// 关联查询优惠券信息
fillCouponInfo(list);
return list;
}
/**
* 查询用户可用优惠券列表
*/
@Override
public List<MallCouponUserVo> queryAvailableList(Long userId) {
LambdaQueryWrapper<MallCouponUser> lqw = new LambdaQueryWrapper<>();
lqw.eq(MallCouponUser::getUserId, userId);
lqw.eq(MallCouponUser::getStatus, 0); // 未使用
lqw.orderByDesc(MallCouponUser::getCreateTime);
List<MallCouponUserVo> list = baseMapper.selectVoList(lqw);
// 过滤已过期的优惠券
Date now = new Date();
list = list.stream().filter(vo -> {
if (vo.getCouponId() == null) {
return false;
}
MallCouponVo couponVo = couponMapper.selectVoById(vo.getCouponId());
if (couponVo == null || couponVo.getStatus() != 0) {
return false;
}
return couponVo.getValidEndTime() != null && couponVo.getValidEndTime().after(now);
}).collect(Collectors.toList());
// 关联查询优惠券信息
fillCouponInfo(list);
return list;
}
/**
* 填充优惠券信息
*/
private void fillCouponInfo(List<MallCouponUserVo> list) {
if (list == null || list.isEmpty()) {
return;
}
for (MallCouponUserVo vo : list) {
if (vo.getCouponId() != null) {
MallCouponVo couponVo = couponMapper.selectVoById(vo.getCouponId());
vo.setCouponInfo(couponVo);
}
}
}
private LambdaQueryWrapper<MallCouponUser> buildQueryWrapper(MallCouponUserBo bo) {
LambdaQueryWrapper<MallCouponUser> lqw = new LambdaQueryWrapper<>();
lqw.eq(bo.getUserId() != null, MallCouponUser::getUserId, bo.getUserId());
lqw.eq(bo.getCouponId() != null, MallCouponUser::getCouponId, bo.getCouponId());
lqw.eq(bo.getStatus() != null, MallCouponUser::getStatus, bo.getStatus());
lqw.orderByDesc(MallCouponUser::getCreateTime);
return lqw;
}
/**
* 用户领取优惠券
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean receiveCoupon(Long couponId) {
Long userId = LoginHelper.getUserId();
// 查询优惠券信息
MallCoupon coupon = couponMapper.selectById(couponId);
if (coupon == null) {
throw new ServiceException("优惠券不存在");
}
if (coupon.getStatus() != 0) {
throw new ServiceException("优惠券已禁用");
}
// 校验有效期
Date now = new Date();
if (coupon.getValidStartTime().after(now) || coupon.getValidEndTime().before(now)) {
throw new ServiceException("优惠券不在有效期内");
}
// 校验发放数量
if (coupon.getTotalCount() != null) {
if (coupon.getUsedCount() >= coupon.getTotalCount()) {
throw new ServiceException("优惠券已发放完毕");
}
}
// 校验每人限领数量
LambdaQueryWrapper<MallCouponUser> countWrapper = new LambdaQueryWrapper<>();
countWrapper.eq(MallCouponUser::getUserId, userId);
countWrapper.eq(MallCouponUser::getCouponId, couponId);
Long receivedCount = baseMapper.selectCount(countWrapper);
if (receivedCount >= coupon.getPerLimit()) {
throw new ServiceException("已达到每人限领数量");
}
// 创建用户优惠券记录
MallCouponUser couponUser = new MallCouponUser();
couponUser.setUserId(userId);
couponUser.setCouponId(couponId);
couponUser.setStatus(0); // 未使用
couponUser.setCreateTime(now);
boolean flag = baseMapper.insert(couponUser) > 0;
if (flag) {
// 更新优惠券已发放数量(注意:这里used_count实际存储的是已发放数量,不是已使用数量)
// 如果业务需要区分,可以考虑增加字段
LambdaUpdateWrapper<MallCoupon> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(MallCoupon::getId, couponId);
updateWrapper.setSql("used_count = used_count + 1");
couponMapper.update(null, updateWrapper);
}
return flag;
}
/**
* 使用优惠券(订单结算时调用)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean useCoupon(Long couponUserId, Long orderId) {
MallCouponUser couponUser = baseMapper.selectById(couponUserId);
if (couponUser == null) {
throw new ServiceException("用户优惠券不存在");
}
if (couponUser.getStatus() != 0) {
throw new ServiceException("优惠券已使用或已过期");
}
// 校验优惠券是否过期
MallCoupon coupon = couponMapper.selectById(couponUser.getCouponId());
if (coupon == null) {
throw new ServiceException("优惠券不存在");
}
Date now = new Date();
if (coupon.getValidEndTime().before(now)) {
throw new ServiceException("优惠券已过期");
}
// 更新用户优惠券状态
couponUser.setStatus(1); // 已使用
couponUser.setUsedTime(now);
couponUser.setOrderId(orderId);
return baseMapper.updateById(couponUser) > 0;
}
/**
* 退还优惠券(订单取消时调用)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean returnCoupon(Long couponUserId) {
MallCouponUser couponUser = baseMapper.selectById(couponUserId);
if (couponUser == null) {
throw new ServiceException("用户优惠券不存在");
}
if (couponUser.getStatus() != 1) {
throw new ServiceException("优惠券状态不正确,无法退还");
}
// 校验优惠券是否过期
MallCoupon coupon = couponMapper.selectById(couponUser.getCouponId());
if (coupon == null) {
throw new ServiceException("优惠券不存在");
}
Date now = new Date();
if (coupon.getValidEndTime().before(now)) {
// 已过期,状态改为已过期
couponUser.setStatus(2);
} else {
// 未过期,状态改为未使用
couponUser.setStatus(0);
}
couponUser.setUsedTime(null);
couponUser.setOrderId(null);
return baseMapper.updateById(couponUser) > 0;
}
/**
* 批量删除用户优惠券
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// 可以添加业务校验逻辑
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 处理过期优惠券(定时任务调用)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void processExpiredCoupons() {
Date now = new Date();
// 查询所有未使用的用户优惠券
LambdaQueryWrapper<MallCouponUser> lqw = new LambdaQueryWrapper<>();
lqw.eq(MallCouponUser::getStatus, 0); // 未使用
List<MallCouponUser> list = baseMapper.selectList(lqw);
for (MallCouponUser couponUser : list) {
MallCoupon coupon = couponMapper.selectById(couponUser.getCouponId());
if (coupon != null && coupon.getValidEndTime().before(now)) {
// 优惠券已过期,更新状态
couponUser.setStatus(2); // 已过期
baseMapper.updateById(couponUser);
}
}
}
}
@@ -0,0 +1,3 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.dromara.mall.coupon.config.MallCouponAutoConfiguration