Merge branch 'voucher' into price_inquery

This commit is contained in:
2025-08-21 01:13:54 +08:00
19 changed files with 964 additions and 0 deletions

View 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字段标记删除状态
## 扩展说明
本模块设计为独立功能模块,不依赖支付系统,为后续接入其他优惠策略预留了扩展空间。所有接口都提供了完整的错误处理和参数验证,确保系统的稳定性和可维护性。

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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> {
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}