You've already forked FrameTour-BE
feat(voucher): 实现券码核销功能模块
- 添加券码批次管理和券码管理相关接口和实现 - 新增券码生成、领取、使用等核心业务逻辑 - 实现了全场免费、商品降价、商品打折三种优惠模式 - 添加了券码状态管理和统计功能 - 优化了数据库表结构和索引 - 编写了详细的开发文档和使用示例
This commit is contained in:
179
src/main/java/com/ycwl/basic/voucher/CLAUDE.md
Normal file
179
src/main/java/com/ycwl/basic/voucher/CLAUDE.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# 券码核销功能模块
|
||||
|
||||
本模块实现景区券码的批量创建、分发和核销管理功能。支持全场免费、商品降价、商品打折三种优惠模式,确保每个用户在每个景区只能领取一次券码。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **批量创建券码**:管理员可创建券码批次,自动生成指定数量的唯一券码
|
||||
- **精准分发控制**:通过景区ID、推客ID、用户faceId进行精准投放
|
||||
- **三种优惠模式**:全场免费、商品降价、商品打折
|
||||
- **唯一性保证**:同一用户在同一景区只能领取一次券码
|
||||
- **完整管理功能**:批次管理、券码查询、使用统计、手动核销
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### 券码批次表 (voucher_batch)
|
||||
```sql
|
||||
CREATE TABLE voucher_batch (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
batch_name VARCHAR(100) NOT NULL COMMENT '券码批次名称',
|
||||
scenic_id BIGINT NOT NULL COMMENT '景区ID',
|
||||
broker_id BIGINT NOT NULL COMMENT '推客ID',
|
||||
discount_type TINYINT NOT NULL COMMENT '优惠类型:0=全场免费,1=商品降价,2=商品打折',
|
||||
discount_value DECIMAL(10,2) COMMENT '优惠值(降价金额或折扣百分比)',
|
||||
total_count INT NOT NULL COMMENT '总券码数量',
|
||||
used_count INT DEFAULT 0 COMMENT '已使用数量',
|
||||
claimed_count INT DEFAULT 0 COMMENT '已领取数量',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:0=禁用,1=启用',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
create_by BIGINT COMMENT '创建人ID',
|
||||
deleted TINYINT DEFAULT 0,
|
||||
deleted_at DATETIME
|
||||
);
|
||||
```
|
||||
|
||||
### 券码表 (voucher_code)
|
||||
```sql
|
||||
CREATE TABLE voucher_code (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
batch_id BIGINT NOT NULL COMMENT '批次ID',
|
||||
scenic_id BIGINT NOT NULL COMMENT '景区ID',
|
||||
code VARCHAR(32) NOT NULL UNIQUE COMMENT '券码',
|
||||
status TINYINT DEFAULT 0 COMMENT '状态:0=未领取,1=已领取未使用,2=已使用',
|
||||
face_id BIGINT COMMENT '领取人faceId',
|
||||
claimed_time DATETIME COMMENT '领取时间',
|
||||
used_time DATETIME COMMENT '使用时间',
|
||||
remark VARCHAR(500) COMMENT '使用备注',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted TINYINT DEFAULT 0,
|
||||
deleted_at DATETIME
|
||||
);
|
||||
```
|
||||
|
||||
## 包结构说明
|
||||
|
||||
```
|
||||
com.ycwl.basic.voucher/
|
||||
├── controller/
|
||||
│ └── VoucherController.java # 控制器:PC端管理和移动端用户接口
|
||||
├── service/
|
||||
│ ├── VoucherBatchService.java # 券码批次服务接口
|
||||
│ ├── VoucherCodeService.java # 券码服务接口
|
||||
│ └── impl/
|
||||
│ ├── VoucherBatchServiceImpl.java # 券码批次服务实现
|
||||
│ └── VoucherCodeServiceImpl.java # 券码服务实现
|
||||
├── mapper/
|
||||
│ ├── VoucherBatchMapper.java # 券码批次数据访问
|
||||
│ └── VoucherCodeMapper.java # 券码数据访问
|
||||
├── entity/
|
||||
│ ├── VoucherBatchEntity.java # 券码批次实体
|
||||
│ └── VoucherCodeEntity.java # 券码实体
|
||||
├── dto/
|
||||
│ ├── req/ # 请求DTO
|
||||
│ │ ├── VoucherBatchCreateReq.java # 创建批次请求
|
||||
│ │ ├── VoucherBatchQueryReq.java # 批次查询请求
|
||||
│ │ ├── VoucherCodeQueryReq.java # 券码查询请求
|
||||
│ │ └── VoucherClaimReq.java # 券码领取请求
|
||||
│ └── resp/ # 响应DTO
|
||||
│ ├── VoucherBatchResp.java # 批次响应
|
||||
│ ├── VoucherCodeResp.java # 券码响应
|
||||
│ └── VoucherBatchStatsResp.java # 批次统计响应
|
||||
└── enums/
|
||||
├── VoucherDiscountType.java # 优惠类型枚举
|
||||
└── VoucherCodeStatus.java # 券码状态枚举
|
||||
```
|
||||
|
||||
## 核心业务逻辑
|
||||
|
||||
### 1. 券码生成规则
|
||||
- 使用UUID生成8位大写字母数字组合
|
||||
- 确保每个券码在系统中唯一
|
||||
- 券码初始状态为"未领取"
|
||||
|
||||
### 2. 领取验证逻辑
|
||||
```java
|
||||
// 核心验证:同一faceId在同一scenicId中只能领取一次
|
||||
public boolean canClaimVoucher(Long faceId, Long scenicId) {
|
||||
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);
|
||||
return count == 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 优惠类型说明
|
||||
- **全场免费(0)**:所有商品免费,discountValue可为空
|
||||
- **商品降价(1)**:每个商品减免固定金额,discountValue为减免金额
|
||||
- **商品打折(2)**:每个商品按百分比打折,discountValue为折扣百分比
|
||||
|
||||
### 4. 状态流转
|
||||
券码状态:未领取(0) → 已领取未使用(1) → 已使用(2)
|
||||
|
||||
## 主要接口说明
|
||||
|
||||
### PC端管理接口
|
||||
- `POST /api/voucher/batch/create` - 创建券码批次
|
||||
- `POST /api/voucher/batch/list` - 批次列表查询
|
||||
- `GET /api/voucher/batch/{id}` - 批次详情
|
||||
- `GET /api/voucher/batch/{id}/stats` - 批次统计
|
||||
- `PUT /api/voucher/batch/{id}/status` - 启用/禁用批次
|
||||
- `POST /api/voucher/codes` - 券码列表查询
|
||||
- `PUT /api/voucher/code/{id}/use` - 手动标记券码已使用
|
||||
- `GET /api/voucher/scenic/{scenicId}/users` - 查看景区下用户领取情况
|
||||
|
||||
### 移动端用户接口
|
||||
- `POST /api/voucher/mobile/claim` - 领取券码(一步到位)
|
||||
- `GET /api/voucher/mobile/my-codes` - 我的券码列表
|
||||
|
||||
## 关键技术特点
|
||||
|
||||
### 1. 事务管理
|
||||
- 使用`@Transactional`确保券码领取操作的原子性
|
||||
- 批次创建和券码生成在同一事务中完成
|
||||
|
||||
### 2. 并发控制
|
||||
- 通过应用层验证避免数据库唯一约束冲突
|
||||
- 使用数据库索引优化查询性能
|
||||
|
||||
### 3. 参数验证
|
||||
- 在Service层进行手动参数校验
|
||||
- 使用BizException统一错误处理
|
||||
|
||||
### 4. 数据统计
|
||||
- 实时更新批次的已领取数量和已使用数量
|
||||
- 提供详细的使用统计和分析数据
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 创建券码批次
|
||||
```java
|
||||
VoucherBatchCreateReq req = new VoucherBatchCreateReq();
|
||||
req.setBatchName("春节特惠券");
|
||||
req.setScenicId(1001L);
|
||||
req.setBrokerId(2001L);
|
||||
req.setDiscountType(1); // 商品降价
|
||||
req.setDiscountValue(new BigDecimal("10.00"));
|
||||
req.setTotalCount(1000);
|
||||
|
||||
Long batchId = voucherBatchService.createBatch(req);
|
||||
```
|
||||
|
||||
### 用户领取券码
|
||||
```java
|
||||
VoucherClaimReq req = new VoucherClaimReq();
|
||||
req.setScenicId(1001L);
|
||||
req.setBrokerId(2001L);
|
||||
req.setFaceId(3001L);
|
||||
|
||||
VoucherCodeResp result = voucherCodeService.claimVoucher(req);
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **唯一性限制**:同一个faceId在同一个scenicId中只能领取一次券码
|
||||
2. **批次状态**:只有启用状态的批次才能领取券码
|
||||
3. **券码数量**:确保批次有可用券码才能成功领取
|
||||
4. **优惠值验证**:除全场免费外,其他优惠类型必须设置优惠值
|
||||
5. **删除机制**:使用逻辑删除,deleted字段标记删除状态
|
||||
|
||||
## 扩展说明
|
||||
|
||||
本模块设计为独立功能模块,不依赖支付系统,为后续接入其他优惠策略预留了扩展空间。所有接口都提供了完整的错误处理和参数验证,确保系统的稳定性和可维护性。
|
@@ -0,0 +1,92 @@
|
||||
package com.ycwl.basic.voucher.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.voucher.dto.req.VoucherBatchCreateReq;
|
||||
import com.ycwl.basic.voucher.dto.req.VoucherBatchQueryReq;
|
||||
import com.ycwl.basic.voucher.dto.req.VoucherClaimReq;
|
||||
import com.ycwl.basic.voucher.dto.req.VoucherCodeQueryReq;
|
||||
import com.ycwl.basic.voucher.dto.resp.VoucherBatchResp;
|
||||
import com.ycwl.basic.voucher.dto.resp.VoucherBatchStatsResp;
|
||||
import com.ycwl.basic.voucher.dto.resp.VoucherCodeResp;
|
||||
import com.ycwl.basic.voucher.service.VoucherBatchService;
|
||||
import com.ycwl.basic.voucher.service.VoucherCodeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/voucher")
|
||||
@RequiredArgsConstructor
|
||||
public class VoucherController {
|
||||
|
||||
private final VoucherBatchService voucherBatchService;
|
||||
private final VoucherCodeService voucherCodeService;
|
||||
|
||||
@PostMapping("/batch/create")
|
||||
public ApiResponse<Long> createBatch(@RequestBody VoucherBatchCreateReq req) {
|
||||
Long batchId = voucherBatchService.createBatch(req);
|
||||
return ApiResponse.success(batchId);
|
||||
}
|
||||
|
||||
@PostMapping("/batch/list")
|
||||
public ApiResponse<Page<VoucherBatchResp>> getBatchList(@RequestBody VoucherBatchQueryReq req) {
|
||||
Page<VoucherBatchResp> page = voucherBatchService.queryBatchList(req);
|
||||
return ApiResponse.success(page);
|
||||
}
|
||||
|
||||
@GetMapping("/batch/{id}")
|
||||
public ApiResponse<VoucherBatchResp> getBatchDetail(@PathVariable Long id) {
|
||||
VoucherBatchResp batch = voucherBatchService.getBatchDetail(id);
|
||||
return ApiResponse.success(batch);
|
||||
}
|
||||
|
||||
@GetMapping("/batch/{id}/stats")
|
||||
public ApiResponse<VoucherBatchStatsResp> getBatchStats(@PathVariable Long id) {
|
||||
VoucherBatchStatsResp stats = voucherBatchService.getBatchStats(id);
|
||||
return ApiResponse.success(stats);
|
||||
}
|
||||
|
||||
@PutMapping("/batch/{id}/status")
|
||||
public ApiResponse<Void> updateBatchStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||
voucherBatchService.updateBatchStatus(id, status);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@PostMapping("/codes")
|
||||
public ApiResponse<Page<VoucherCodeResp>> getCodeList(@RequestBody VoucherCodeQueryReq req) {
|
||||
Page<VoucherCodeResp> page = voucherCodeService.queryCodeList(req);
|
||||
return ApiResponse.success(page);
|
||||
}
|
||||
|
||||
@PutMapping("/code/{id}/use")
|
||||
public ApiResponse<Void> markCodeAsUsed(@PathVariable Long id, @RequestParam(required = false) String remark) {
|
||||
voucherCodeService.markCodeAsUsed(id, remark);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@GetMapping("/scenic/{scenicId}/users")
|
||||
public ApiResponse<Page<VoucherCodeResp>> getUsersInScenic(@PathVariable Long scenicId,
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
VoucherCodeQueryReq req = new VoucherCodeQueryReq();
|
||||
req.setScenicId(scenicId);
|
||||
req.setPageNum(pageNum);
|
||||
req.setPageSize(pageSize);
|
||||
Page<VoucherCodeResp> page = voucherCodeService.queryCodeList(req);
|
||||
return ApiResponse.success(page);
|
||||
}
|
||||
|
||||
@PostMapping("/mobile/claim")
|
||||
public ApiResponse<VoucherCodeResp> claimVoucher(@RequestBody VoucherClaimReq req) {
|
||||
VoucherCodeResp result = voucherCodeService.claimVoucher(req);
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
|
||||
@GetMapping("/mobile/my-codes")
|
||||
public ApiResponse<List<VoucherCodeResp>> getMyVoucherCodes(@RequestParam Long faceId) {
|
||||
List<VoucherCodeResp> codes = voucherCodeService.getMyVoucherCodes(faceId);
|
||||
return ApiResponse.success(codes);
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.ycwl.basic.voucher.dto.req;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class VoucherBatchCreateReq {
|
||||
private String batchName;
|
||||
private Long scenicId;
|
||||
private Long brokerId;
|
||||
private Integer discountType;
|
||||
private BigDecimal discountValue;
|
||||
private Integer totalCount;
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package com.ycwl.basic.voucher.dto.req;
|
||||
|
||||
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class VoucherBatchQueryReq extends BaseQueryParameterReq {
|
||||
private Long scenicId;
|
||||
private Long brokerId;
|
||||
private Integer status;
|
||||
private String batchName;
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.ycwl.basic.voucher.dto.req;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VoucherClaimReq {
|
||||
private Long scenicId;
|
||||
private Long brokerId;
|
||||
private Long faceId;
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.ycwl.basic.voucher.dto.req;
|
||||
|
||||
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class VoucherCodeQueryReq extends BaseQueryParameterReq {
|
||||
private Long batchId;
|
||||
private Long scenicId;
|
||||
private Long faceId;
|
||||
private Integer status;
|
||||
private String code;
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package com.ycwl.basic.voucher.dto.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class VoucherBatchResp {
|
||||
private Long id;
|
||||
private String batchName;
|
||||
private Long scenicId;
|
||||
private Long brokerId;
|
||||
private Integer discountType;
|
||||
private String discountTypeName;
|
||||
private BigDecimal discountValue;
|
||||
private Integer totalCount;
|
||||
private Integer usedCount;
|
||||
private Integer claimedCount;
|
||||
private Integer availableCount;
|
||||
private Integer status;
|
||||
private String statusName;
|
||||
private Date createTime;
|
||||
private Long createBy;
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.ycwl.basic.voucher.dto.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VoucherBatchStatsResp {
|
||||
private Long batchId;
|
||||
private String batchName;
|
||||
private Integer totalCount;
|
||||
private Integer claimedCount;
|
||||
private Integer usedCount;
|
||||
private Integer availableCount;
|
||||
private Double claimedRate;
|
||||
private Double usedRate;
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.ycwl.basic.voucher.dto.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class VoucherCodeResp {
|
||||
private Long id;
|
||||
private Long batchId;
|
||||
private String batchName;
|
||||
private Long scenicId;
|
||||
private String code;
|
||||
private Integer status;
|
||||
private String statusName;
|
||||
private Long faceId;
|
||||
private Date claimedTime;
|
||||
private Date usedTime;
|
||||
private String remark;
|
||||
private Date createTime;
|
||||
|
||||
private Integer discountType;
|
||||
private String discountTypeName;
|
||||
private String discountDescription;
|
||||
private BigDecimal discountValue;
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.ycwl.basic.voucher.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@TableName("voucher_batch")
|
||||
public class VoucherBatchEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String batchName;
|
||||
private Long scenicId;
|
||||
private Long brokerId;
|
||||
private Integer discountType;
|
||||
private BigDecimal discountValue;
|
||||
private Integer totalCount;
|
||||
private Integer usedCount;
|
||||
private Integer claimedCount;
|
||||
private Integer status;
|
||||
private Date createTime;
|
||||
private Long createBy;
|
||||
private Integer deleted;
|
||||
private Date deletedAt;
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.ycwl.basic.voucher.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@TableName("voucher_code")
|
||||
public class VoucherCodeEntity {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private Long batchId;
|
||||
private Long scenicId;
|
||||
private String code;
|
||||
private Integer status;
|
||||
private Long faceId;
|
||||
private Date claimedTime;
|
||||
private Date usedTime;
|
||||
private String remark;
|
||||
private Date createTime;
|
||||
private Integer deleted;
|
||||
private Date deletedAt;
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.ycwl.basic.voucher.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum VoucherCodeStatus {
|
||||
UNCLAIMED(0, "未领取"),
|
||||
CLAIMED_UNUSED(1, "已领取未使用"),
|
||||
USED(2, "已使用");
|
||||
|
||||
private final Integer code;
|
||||
private final String name;
|
||||
|
||||
public static VoucherCodeStatus getByCode(Integer code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (VoucherCodeStatus status : values()) {
|
||||
if (status.getCode().equals(code)) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package com.ycwl.basic.voucher.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum VoucherDiscountType {
|
||||
FREE_ALL(0, "全场免费", "所有商品免费"),
|
||||
REDUCE_PRICE(1, "商品降价", "每个商品减免指定金额"),
|
||||
DISCOUNT(2, "商品打折", "每个商品按百分比打折");
|
||||
|
||||
private final Integer code;
|
||||
private final String name;
|
||||
private final String description;
|
||||
|
||||
public static VoucherDiscountType getByCode(Integer code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (VoucherDiscountType type : values()) {
|
||||
if (type.getCode().equals(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.ycwl.basic.voucher.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.voucher.entity.VoucherBatchEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface VoucherBatchMapper extends BaseMapper<VoucherBatchEntity> {
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package com.ycwl.basic.voucher.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.voucher.entity.VoucherCodeEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface VoucherCodeMapper extends BaseMapper<VoucherCodeEntity> {
|
||||
|
||||
@Select("SELECT COUNT(*) FROM voucher_code WHERE scenic_id = #{scenicId} AND face_id = #{faceId} AND status != 0 AND deleted = 0")
|
||||
Integer countByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId);
|
||||
|
||||
@Select("SELECT * FROM voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT 1")
|
||||
VoucherCodeEntity findFirstAvailableByBatchId(@Param("batchId") Long batchId);
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.ycwl.basic.voucher.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.voucher.dto.req.VoucherBatchCreateReq;
|
||||
import com.ycwl.basic.voucher.dto.req.VoucherBatchQueryReq;
|
||||
import com.ycwl.basic.voucher.dto.resp.VoucherBatchResp;
|
||||
import com.ycwl.basic.voucher.dto.resp.VoucherBatchStatsResp;
|
||||
import com.ycwl.basic.voucher.entity.VoucherBatchEntity;
|
||||
|
||||
public interface VoucherBatchService {
|
||||
|
||||
Long createBatch(VoucherBatchCreateReq req);
|
||||
|
||||
Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req);
|
||||
|
||||
VoucherBatchResp getBatchDetail(Long id);
|
||||
|
||||
VoucherBatchStatsResp getBatchStats(Long id);
|
||||
|
||||
void updateBatchStatus(Long id, Integer status);
|
||||
|
||||
void updateBatchClaimedCount(Long batchId);
|
||||
|
||||
void updateBatchUsedCount(Long batchId);
|
||||
|
||||
VoucherBatchEntity getAvailableBatch(Long scenicId, Long brokerId);
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package com.ycwl.basic.voucher.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.voucher.dto.req.VoucherClaimReq;
|
||||
import com.ycwl.basic.voucher.dto.req.VoucherCodeQueryReq;
|
||||
import com.ycwl.basic.voucher.dto.resp.VoucherCodeResp;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface VoucherCodeService {
|
||||
|
||||
void generateVoucherCodes(Long batchId, Long scenicId, Integer count);
|
||||
|
||||
VoucherCodeResp claimVoucher(VoucherClaimReq req);
|
||||
|
||||
Page<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req);
|
||||
|
||||
List<VoucherCodeResp> getMyVoucherCodes(Long faceId);
|
||||
|
||||
void markCodeAsUsed(Long codeId, String remark);
|
||||
|
||||
boolean canClaimVoucher(Long faceId, Long scenicId);
|
||||
}
|
@@ -0,0 +1,190 @@
|
||||
package com.ycwl.basic.voucher.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import com.ycwl.basic.interceptor.BaseContextHandler;
|
||||
import com.ycwl.basic.voucher.dto.req.VoucherBatchCreateReq;
|
||||
import com.ycwl.basic.voucher.dto.req.VoucherBatchQueryReq;
|
||||
import com.ycwl.basic.voucher.dto.resp.VoucherBatchResp;
|
||||
import com.ycwl.basic.voucher.dto.resp.VoucherBatchStatsResp;
|
||||
import com.ycwl.basic.voucher.entity.VoucherBatchEntity;
|
||||
import com.ycwl.basic.voucher.enums.VoucherDiscountType;
|
||||
import com.ycwl.basic.voucher.mapper.VoucherBatchMapper;
|
||||
import com.ycwl.basic.voucher.service.VoucherBatchService;
|
||||
import com.ycwl.basic.voucher.service.VoucherCodeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VoucherBatchServiceImpl implements VoucherBatchService {
|
||||
|
||||
private final VoucherBatchMapper voucherBatchMapper;
|
||||
private final VoucherCodeService voucherCodeService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long createBatch(VoucherBatchCreateReq req) {
|
||||
if (req.getBatchName() == null || req.getBatchName().trim().isEmpty()) {
|
||||
throw new BizException(400, "券码批次名称不能为空");
|
||||
}
|
||||
if (req.getScenicId() == null) {
|
||||
throw new BizException(400, "景区ID不能为空");
|
||||
}
|
||||
if (req.getBrokerId() == null) {
|
||||
throw new BizException(400, "推客ID不能为空");
|
||||
}
|
||||
if (req.getDiscountType() == null) {
|
||||
throw new BizException(400, "优惠类型不能为空");
|
||||
}
|
||||
if (req.getTotalCount() == null || req.getTotalCount() < 1) {
|
||||
throw new BizException(400, "券码数量必须大于0");
|
||||
}
|
||||
|
||||
VoucherDiscountType discountType = VoucherDiscountType.getByCode(req.getDiscountType());
|
||||
if (discountType == null) {
|
||||
throw new BizException(400, "无效的优惠类型");
|
||||
}
|
||||
|
||||
if (discountType != VoucherDiscountType.FREE_ALL && req.getDiscountValue() == null) {
|
||||
throw new BizException(400, "优惠金额不能为空");
|
||||
}
|
||||
|
||||
VoucherBatchEntity batch = new VoucherBatchEntity();
|
||||
BeanUtils.copyProperties(req, batch);
|
||||
batch.setUsedCount(0);
|
||||
batch.setClaimedCount(0);
|
||||
batch.setStatus(1);
|
||||
batch.setCreateTime(new Date());
|
||||
batch.setCreateBy(BaseContextHandler.getUserId());
|
||||
batch.setDeleted(0);
|
||||
|
||||
voucherBatchMapper.insert(batch);
|
||||
|
||||
voucherCodeService.generateVoucherCodes(batch.getId(), req.getScenicId(), req.getTotalCount());
|
||||
|
||||
return batch.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<VoucherBatchResp> queryBatchList(VoucherBatchQueryReq req) {
|
||||
Page<VoucherBatchEntity> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||
|
||||
LambdaQueryWrapper<VoucherBatchEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(VoucherBatchEntity::getDeleted, 0)
|
||||
.eq(req.getScenicId() != null, VoucherBatchEntity::getScenicId, req.getScenicId())
|
||||
.eq(req.getBrokerId() != null, VoucherBatchEntity::getBrokerId, req.getBrokerId())
|
||||
.eq(req.getStatus() != null, VoucherBatchEntity::getStatus, req.getStatus())
|
||||
.like(StringUtils.hasText(req.getBatchName()), VoucherBatchEntity::getBatchName, req.getBatchName())
|
||||
.orderByDesc(VoucherBatchEntity::getCreateTime);
|
||||
|
||||
Page<VoucherBatchEntity> entityPage = voucherBatchMapper.selectPage(page, wrapper);
|
||||
|
||||
Page<VoucherBatchResp> respPage = new Page<>();
|
||||
BeanUtils.copyProperties(entityPage, respPage);
|
||||
|
||||
respPage.setRecords(entityPage.getRecords().stream().map(this::convertToResp).toList());
|
||||
|
||||
return respPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherBatchResp getBatchDetail(Long id) {
|
||||
VoucherBatchEntity batch = voucherBatchMapper.selectById(id);
|
||||
if (batch == null || batch.getDeleted() == 1) {
|
||||
throw new BizException(404, "券码批次不存在");
|
||||
}
|
||||
|
||||
return convertToResp(batch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherBatchStatsResp getBatchStats(Long id) {
|
||||
VoucherBatchEntity batch = voucherBatchMapper.selectById(id);
|
||||
if (batch == null || batch.getDeleted() == 1) {
|
||||
throw new BizException(404, "券码批次不存在");
|
||||
}
|
||||
|
||||
VoucherBatchStatsResp stats = new VoucherBatchStatsResp();
|
||||
stats.setBatchId(batch.getId());
|
||||
stats.setBatchName(batch.getBatchName());
|
||||
stats.setTotalCount(batch.getTotalCount());
|
||||
stats.setClaimedCount(batch.getClaimedCount());
|
||||
stats.setUsedCount(batch.getUsedCount());
|
||||
stats.setAvailableCount(batch.getTotalCount() - batch.getClaimedCount());
|
||||
|
||||
if (batch.getTotalCount() > 0) {
|
||||
stats.setClaimedRate((double) batch.getClaimedCount() / batch.getTotalCount() * 100);
|
||||
stats.setUsedRate((double) batch.getUsedCount() / batch.getTotalCount() * 100);
|
||||
} else {
|
||||
stats.setClaimedRate(0.0);
|
||||
stats.setUsedRate(0.0);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBatchStatus(Long id, Integer status) {
|
||||
VoucherBatchEntity batch = new VoucherBatchEntity();
|
||||
batch.setId(id);
|
||||
batch.setStatus(status);
|
||||
|
||||
int updated = voucherBatchMapper.updateById(batch);
|
||||
if (updated == 0) {
|
||||
throw new BizException(404, "券码批次不存在");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBatchClaimedCount(Long batchId) {
|
||||
VoucherBatchEntity batch = voucherBatchMapper.selectById(batchId);
|
||||
if (batch != null) {
|
||||
batch.setClaimedCount(batch.getClaimedCount() + 1);
|
||||
voucherBatchMapper.updateById(batch);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBatchUsedCount(Long batchId) {
|
||||
VoucherBatchEntity batch = voucherBatchMapper.selectById(batchId);
|
||||
if (batch != null) {
|
||||
batch.setUsedCount(batch.getUsedCount() + 1);
|
||||
voucherBatchMapper.updateById(batch);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoucherBatchEntity getAvailableBatch(Long scenicId, Long brokerId) {
|
||||
LambdaQueryWrapper<VoucherBatchEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(VoucherBatchEntity::getScenicId, scenicId)
|
||||
.eq(VoucherBatchEntity::getBrokerId, brokerId)
|
||||
.eq(VoucherBatchEntity::getStatus, 1)
|
||||
.eq(VoucherBatchEntity::getDeleted, 0)
|
||||
.lt(VoucherBatchEntity::getClaimedCount, VoucherBatchEntity::getTotalCount)
|
||||
.orderByDesc(VoucherBatchEntity::getCreateTime);
|
||||
|
||||
return voucherBatchMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
private VoucherBatchResp convertToResp(VoucherBatchEntity batch) {
|
||||
VoucherBatchResp resp = new VoucherBatchResp();
|
||||
BeanUtils.copyProperties(batch, resp);
|
||||
|
||||
VoucherDiscountType discountType = VoucherDiscountType.getByCode(batch.getDiscountType());
|
||||
if (discountType != null) {
|
||||
resp.setDiscountTypeName(discountType.getName());
|
||||
}
|
||||
|
||||
resp.setStatusName(batch.getStatus() == 1 ? "启用" : "禁用");
|
||||
resp.setAvailableCount(batch.getTotalCount() - batch.getClaimedCount());
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
@@ -0,0 +1,194 @@
|
||||
package com.ycwl.basic.voucher.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ycwl.basic.exception.BizException;
|
||||
import com.ycwl.basic.voucher.dto.req.VoucherClaimReq;
|
||||
import com.ycwl.basic.voucher.dto.req.VoucherCodeQueryReq;
|
||||
import com.ycwl.basic.voucher.dto.resp.VoucherCodeResp;
|
||||
import com.ycwl.basic.voucher.entity.VoucherBatchEntity;
|
||||
import com.ycwl.basic.voucher.entity.VoucherCodeEntity;
|
||||
import com.ycwl.basic.voucher.enums.VoucherCodeStatus;
|
||||
import com.ycwl.basic.voucher.enums.VoucherDiscountType;
|
||||
import com.ycwl.basic.voucher.mapper.VoucherBatchMapper;
|
||||
import com.ycwl.basic.voucher.mapper.VoucherCodeMapper;
|
||||
import com.ycwl.basic.voucher.service.VoucherBatchService;
|
||||
import com.ycwl.basic.voucher.service.VoucherCodeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VoucherCodeServiceImpl implements VoucherCodeService {
|
||||
|
||||
private final VoucherCodeMapper voucherCodeMapper;
|
||||
private final VoucherBatchMapper voucherBatchMapper;
|
||||
private final VoucherBatchService voucherBatchService;
|
||||
|
||||
@Override
|
||||
public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) {
|
||||
List<VoucherCodeEntity> codes = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
VoucherCodeEntity code = new VoucherCodeEntity();
|
||||
code.setBatchId(batchId);
|
||||
code.setScenicId(scenicId);
|
||||
code.setCode(generateVoucherCode());
|
||||
code.setStatus(VoucherCodeStatus.UNCLAIMED.getCode());
|
||||
code.setCreateTime(new Date());
|
||||
code.setDeleted(0);
|
||||
codes.add(code);
|
||||
}
|
||||
|
||||
for (VoucherCodeEntity code : codes) {
|
||||
voucherCodeMapper.insert(code);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
|
||||
if (req.getScenicId() == null) {
|
||||
throw new BizException(400, "景区ID不能为空");
|
||||
}
|
||||
if (req.getBrokerId() == null) {
|
||||
throw new BizException(400, "推客ID不能为空");
|
||||
}
|
||||
if (req.getFaceId() == null) {
|
||||
throw new BizException(400, "用户faceId不能为空");
|
||||
}
|
||||
|
||||
if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) {
|
||||
throw new BizException(400, "该用户在此景区已领取过券码");
|
||||
}
|
||||
|
||||
VoucherBatchEntity batch = voucherBatchService.getAvailableBatch(req.getScenicId(), req.getBrokerId());
|
||||
if (batch == null) {
|
||||
throw new BizException(400, "暂无可用券码批次");
|
||||
}
|
||||
|
||||
VoucherCodeEntity availableCode = voucherCodeMapper.findFirstAvailableByBatchId(batch.getId());
|
||||
if (availableCode == null) {
|
||||
throw new BizException(400, "券码已领完");
|
||||
}
|
||||
|
||||
availableCode.setFaceId(req.getFaceId());
|
||||
availableCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
|
||||
availableCode.setClaimedTime(new Date());
|
||||
|
||||
voucherCodeMapper.updateById(availableCode);
|
||||
|
||||
voucherBatchService.updateBatchClaimedCount(batch.getId());
|
||||
|
||||
return convertToResp(availableCode, batch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req) {
|
||||
Page<VoucherCodeEntity> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||
|
||||
LambdaQueryWrapper<VoucherCodeEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(VoucherCodeEntity::getDeleted, 0)
|
||||
.eq(req.getBatchId() != null, VoucherCodeEntity::getBatchId, req.getBatchId())
|
||||
.eq(req.getScenicId() != null, VoucherCodeEntity::getScenicId, req.getScenicId())
|
||||
.eq(req.getFaceId() != null, VoucherCodeEntity::getFaceId, req.getFaceId())
|
||||
.eq(req.getStatus() != null, VoucherCodeEntity::getStatus, req.getStatus())
|
||||
.like(StringUtils.hasText(req.getCode()), VoucherCodeEntity::getCode, req.getCode())
|
||||
.orderByDesc(VoucherCodeEntity::getCreateTime);
|
||||
|
||||
Page<VoucherCodeEntity> entityPage = voucherCodeMapper.selectPage(page, wrapper);
|
||||
|
||||
Page<VoucherCodeResp> respPage = new Page<>();
|
||||
BeanUtils.copyProperties(entityPage, respPage);
|
||||
|
||||
List<VoucherCodeResp> respList = new ArrayList<>();
|
||||
for (VoucherCodeEntity code : entityPage.getRecords()) {
|
||||
VoucherBatchEntity batch = voucherBatchMapper.selectById(code.getBatchId());
|
||||
respList.add(convertToResp(code, batch));
|
||||
}
|
||||
respPage.setRecords(respList);
|
||||
|
||||
return respPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VoucherCodeResp> getMyVoucherCodes(Long faceId) {
|
||||
LambdaQueryWrapper<VoucherCodeEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(VoucherCodeEntity::getFaceId, faceId)
|
||||
.eq(VoucherCodeEntity::getDeleted, 0)
|
||||
.orderByDesc(VoucherCodeEntity::getClaimedTime);
|
||||
|
||||
List<VoucherCodeEntity> codes = voucherCodeMapper.selectList(wrapper);
|
||||
|
||||
List<VoucherCodeResp> respList = new ArrayList<>();
|
||||
for (VoucherCodeEntity code : codes) {
|
||||
VoucherBatchEntity batch = voucherBatchMapper.selectById(code.getBatchId());
|
||||
respList.add(convertToResp(code, batch));
|
||||
}
|
||||
|
||||
return respList;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void markCodeAsUsed(Long codeId, String remark) {
|
||||
VoucherCodeEntity code = voucherCodeMapper.selectById(codeId);
|
||||
if (code == null || code.getDeleted() == 1) {
|
||||
throw new BizException(404, "券码不存在");
|
||||
}
|
||||
|
||||
if (code.getStatus() != VoucherCodeStatus.CLAIMED_UNUSED.getCode()) {
|
||||
throw new BizException(400, "券码状态异常,无法使用");
|
||||
}
|
||||
|
||||
code.setStatus(VoucherCodeStatus.USED.getCode());
|
||||
code.setUsedTime(new Date());
|
||||
code.setRemark(remark);
|
||||
|
||||
voucherCodeMapper.updateById(code);
|
||||
|
||||
voucherBatchService.updateBatchUsedCount(code.getBatchId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canClaimVoucher(Long faceId, Long scenicId) {
|
||||
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
private String generateVoucherCode() {
|
||||
return UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
|
||||
}
|
||||
|
||||
private VoucherCodeResp convertToResp(VoucherCodeEntity code, VoucherBatchEntity batch) {
|
||||
VoucherCodeResp resp = new VoucherCodeResp();
|
||||
BeanUtils.copyProperties(code, resp);
|
||||
|
||||
if (batch != null) {
|
||||
resp.setBatchName(batch.getBatchName());
|
||||
resp.setDiscountType(batch.getDiscountType());
|
||||
resp.setDiscountValue(batch.getDiscountValue());
|
||||
|
||||
VoucherDiscountType discountType = VoucherDiscountType.getByCode(batch.getDiscountType());
|
||||
if (discountType != null) {
|
||||
resp.setDiscountTypeName(discountType.getName());
|
||||
resp.setDiscountDescription(discountType.getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
VoucherCodeStatus status = VoucherCodeStatus.getByCode(code.getStatus());
|
||||
if (status != null) {
|
||||
resp.setStatusName(status.getName());
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user