You've already forked FrameTour-BE
feat(pricing): 实现优惠券打印功能
- 新增 AppVoucherController 控制器处理打印请求 - 实现 VoucherPrintService 接口和 VoucherPrintServiceImpl 实现类 - 添加 VoucherPrintReq 请求对象和 VoucherPrintResp 响应对象 - 创建 VoucherPrintRecord 实体和对应的 Mapper - 更新 PriceVoucherCodeMapper 接口,添加随机获取未打印券码的方法 - 实现分布式锁机制防止重复打印- 生成流水号并记录打印状态
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package com.ycwl.basic.controller.mobile;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
|
||||
import com.ycwl.basic.pricing.service.VoucherPrintService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/mobile/voucher/v1")
|
||||
public class AppVoucherController {
|
||||
|
||||
@Autowired
|
||||
private VoucherPrintService voucherPrintService;
|
||||
|
||||
/**
|
||||
* 打印小票
|
||||
* @param request 打印请求
|
||||
* @return 打印结果
|
||||
*/
|
||||
@PostMapping("/print")
|
||||
public ApiResponse<VoucherPrintResp> printVoucherTicket(@RequestBody VoucherPrintReq request) {
|
||||
log.info("收到打印小票请求: faceId={}, brokerId={}, scenicId={}",
|
||||
request.getFaceId(), request.getBrokerId(), request.getScenicId());
|
||||
|
||||
VoucherPrintResp response = voucherPrintService.printVoucherTicket(request);
|
||||
|
||||
log.info("打印小票完成: code={}, voucherCode={}, status={}",
|
||||
response.getCode(), response.getVoucherCode(), response.getPrintStatus());
|
||||
|
||||
return ApiResponse.success(response);
|
||||
}
|
||||
}
|
@@ -7,4 +7,5 @@ public class VoucherClaimReq {
|
||||
private Long scenicId;
|
||||
private Long brokerId;
|
||||
private Long faceId;
|
||||
private String code;
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package com.ycwl.basic.pricing.dto.req;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 打印小票请求
|
||||
*/
|
||||
@Data
|
||||
public class VoucherPrintReq {
|
||||
|
||||
private Long faceId;
|
||||
|
||||
private Long brokerId;
|
||||
|
||||
private Long scenicId;
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package com.ycwl.basic.pricing.dto.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 打印小票响应
|
||||
*/
|
||||
@Data
|
||||
public class VoucherPrintResp {
|
||||
|
||||
/**
|
||||
* 流水号
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 券码
|
||||
*/
|
||||
private String voucherCode;
|
||||
|
||||
/**
|
||||
* 打印状态:0=待打印,1=打印成功,2=打印失败
|
||||
*/
|
||||
private Integer printStatus;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
package com.ycwl.basic.pricing.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 优惠券打印记录实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("voucher_print_record")
|
||||
public class VoucherPrintRecord {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 流水号
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 用户faceId
|
||||
*/
|
||||
private Long faceId;
|
||||
|
||||
/**
|
||||
* 经纪人ID
|
||||
*/
|
||||
private Long brokerId;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
private Long scenicId;
|
||||
|
||||
/**
|
||||
* 券码ID
|
||||
*/
|
||||
private Long voucherCodeId;
|
||||
|
||||
/**
|
||||
* 券码
|
||||
*/
|
||||
private String voucherCode;
|
||||
|
||||
/**
|
||||
* 打印状态:0=待打印,1=打印成功,2=打印失败
|
||||
*/
|
||||
private Integer printStatus;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
@TableField("create_time")
|
||||
private Date createTime;
|
||||
|
||||
@TableField("update_time")
|
||||
private Date updateTime;
|
||||
|
||||
private Integer deleted;
|
||||
|
||||
private Date deletedAt;
|
||||
}
|
@@ -91,4 +91,11 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
|
||||
* @return 可用券码
|
||||
*/
|
||||
PriceVoucherCode findFirstAvailableByBatchId(@Param("batchId") Long batchId);
|
||||
|
||||
/**
|
||||
* 随机获取一个未被打印过的可用券码
|
||||
* @param scenicId 景区ID
|
||||
* @return 可用券码
|
||||
*/
|
||||
PriceVoucherCode findRandomUnprintedVoucher(@Param("scenicId") Long scenicId);
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package com.ycwl.basic.pricing.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ycwl.basic.pricing.entity.VoucherPrintRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 优惠券打印记录Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface VoucherPrintRecordMapper extends BaseMapper<VoucherPrintRecord> {
|
||||
|
||||
/**
|
||||
* 根据faceId、brokerId、scenicId查询已存在的打印记录
|
||||
* @param faceId 用户faceId
|
||||
* @param brokerId 经纪人ID
|
||||
* @param scenicId 景区ID
|
||||
* @return 打印记录
|
||||
*/
|
||||
VoucherPrintRecord selectByFaceBrokerScenic(@Param("faceId") Long faceId,
|
||||
@Param("brokerId") Long brokerId,
|
||||
@Param("scenicId") Long scenicId);
|
||||
|
||||
/**
|
||||
* 根据券码ID查询是否已被打印
|
||||
* @param voucherCodeId 券码ID
|
||||
* @return 打印记录
|
||||
*/
|
||||
VoucherPrintRecord selectByVoucherCodeId(@Param("voucherCodeId") Long voucherCodeId);
|
||||
|
||||
/**
|
||||
* 更新打印状态
|
||||
* @param id 记录ID
|
||||
* @param printStatus 打印状态
|
||||
* @param errorMessage 错误信息(可为null)
|
||||
* @return 影响行数
|
||||
*/
|
||||
int updatePrintStatus(@Param("id") Long id,
|
||||
@Param("printStatus") Integer printStatus,
|
||||
@Param("errorMessage") String errorMessage);
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package com.ycwl.basic.pricing.service;
|
||||
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
|
||||
|
||||
/**
|
||||
* 优惠券打印服务
|
||||
*/
|
||||
public interface VoucherPrintService {
|
||||
|
||||
/**
|
||||
* 打印小票
|
||||
* @param request 打印请求
|
||||
* @return 打印响应
|
||||
*/
|
||||
VoucherPrintResp printVoucherTicket(VoucherPrintReq request);
|
||||
}
|
@@ -80,30 +80,50 @@ public class VoucherCodeServiceImpl implements VoucherCodeService {
|
||||
if (req.getFaceId() == null) {
|
||||
throw new BizException(400, "用户faceId不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(req.getCode())) {
|
||||
throw new BizException(400, "券码不能为空");
|
||||
}
|
||||
|
||||
// 验证券码是否存在且未被领取
|
||||
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PriceVoucherCode::getCode, req.getCode())
|
||||
.eq(PriceVoucherCode::getScenicId, req.getScenicId())
|
||||
.eq(PriceVoucherCode::getDeleted, 0);
|
||||
|
||||
PriceVoucherCode voucherCode = voucherCodeMapper.selectOne(wrapper);
|
||||
if (voucherCode == null) {
|
||||
throw new BizException(400, "券码不存在或不属于该景区");
|
||||
}
|
||||
|
||||
if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) {
|
||||
throw new BizException(400, "券码已被领取或已使用");
|
||||
}
|
||||
|
||||
if (!canClaimVoucher(req.getFaceId(), req.getScenicId())) {
|
||||
throw new BizException(400, "该用户在此景区已领取过券码");
|
||||
}
|
||||
|
||||
PriceVoucherBatchConfig batch = voucherBatchService.getAvailableBatch(req.getScenicId(), req.getBrokerId());
|
||||
if (batch == null) {
|
||||
throw new BizException(400, "暂无可用券码批次");
|
||||
// 获取券码所属批次
|
||||
PriceVoucherBatchConfig batch = voucherBatchMapper.selectById(voucherCode.getBatchId());
|
||||
if (batch == null || batch.getDeleted() == 1) {
|
||||
throw new BizException(400, "券码批次不存在");
|
||||
}
|
||||
|
||||
PriceVoucherCode availableCode = voucherCodeMapper.findFirstAvailableByBatchId(batch.getId());
|
||||
if (availableCode == null) {
|
||||
throw new BizException(400, "券码已领完");
|
||||
// 验证批次是否可用于该推客
|
||||
if (!Objects.equals(batch.getBrokerId(), req.getBrokerId())) {
|
||||
throw new BizException(400, "券码不属于该推客");
|
||||
}
|
||||
|
||||
availableCode.setFaceId(req.getFaceId());
|
||||
availableCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
|
||||
availableCode.setClaimedTime(new Date());
|
||||
// 更新券码状态
|
||||
voucherCode.setFaceId(req.getFaceId());
|
||||
voucherCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
|
||||
voucherCode.setClaimedTime(new Date());
|
||||
|
||||
voucherCodeMapper.updateById(availableCode);
|
||||
voucherCodeMapper.updateById(voucherCode);
|
||||
|
||||
voucherBatchService.updateBatchClaimedCount(batch.getId());
|
||||
|
||||
return convertToResp(availableCode, batch);
|
||||
return convertToResp(voucherCode, batch);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -0,0 +1,177 @@
|
||||
package com.ycwl.basic.pricing.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.ycwl.basic.constant.BaseContextHandler;
|
||||
import com.ycwl.basic.exception.BaseException;
|
||||
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
|
||||
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
|
||||
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
|
||||
import com.ycwl.basic.pricing.entity.PriceVoucherCode;
|
||||
import com.ycwl.basic.pricing.entity.VoucherPrintRecord;
|
||||
import com.ycwl.basic.pricing.mapper.PriceVoucherCodeMapper;
|
||||
import com.ycwl.basic.pricing.mapper.VoucherPrintRecordMapper;
|
||||
import com.ycwl.basic.pricing.service.VoucherPrintService;
|
||||
import com.ycwl.basic.repository.FaceRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 优惠券打印服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class VoucherPrintServiceImpl implements VoucherPrintService {
|
||||
|
||||
@Autowired
|
||||
private VoucherPrintRecordMapper voucherPrintRecordMapper;
|
||||
|
||||
@Autowired
|
||||
private PriceVoucherCodeMapper priceVoucherCodeMapper;
|
||||
|
||||
@Autowired
|
||||
private FaceRepository faceRepository;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private static final String PRINT_LOCK_KEY = "voucher_print_lock:%s:%s:%s"; // faceId:brokerId:scenicId
|
||||
private static final String CODE_PREFIX = "VT"; // Voucher Ticket
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public VoucherPrintResp printVoucherTicket(VoucherPrintReq request) {
|
||||
// 参数验证
|
||||
if (request.getFaceId() == null) {
|
||||
throw new BaseException("用户faceId不能为空");
|
||||
}
|
||||
if (request.getBrokerId() == null) {
|
||||
throw new BaseException("经纪人ID不能为空");
|
||||
}
|
||||
if (request.getScenicId() == null) {
|
||||
throw new BaseException("景区ID不能为空");
|
||||
}
|
||||
|
||||
Long currentUserId = Long.valueOf(BaseContextHandler.getUserId());
|
||||
|
||||
// 验证faceId是否属于当前用户
|
||||
validateFaceOwnership(request.getFaceId(), currentUserId);
|
||||
|
||||
// 使用Redis分布式锁防止重复打印
|
||||
String lockKey = String.format(PRINT_LOCK_KEY, request.getFaceId(), request.getBrokerId(), request.getScenicId());
|
||||
String lockValue = UUID.randomUUID().toString();
|
||||
|
||||
try {
|
||||
// 尝试获取锁,超时时间30秒
|
||||
Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
|
||||
if (!lockAcquired) {
|
||||
throw new BaseException("请求处理中,请稍后再试");
|
||||
}
|
||||
|
||||
// 检查是否已存在相同的打印记录
|
||||
VoucherPrintRecord existingRecord = voucherPrintRecordMapper.selectByFaceBrokerScenic(
|
||||
request.getFaceId(), request.getBrokerId(), request.getScenicId());
|
||||
|
||||
if (existingRecord != null) {
|
||||
log.info("找到已存在的打印记录,返回该记录: {}", existingRecord.getId());
|
||||
return buildResponse(existingRecord);
|
||||
}
|
||||
|
||||
// 获取一个可用的券码(未被打印过的)
|
||||
PriceVoucherCode availableVoucher = getAvailableUnprintedVoucher(request.getScenicId());
|
||||
if (availableVoucher == null) {
|
||||
throw new BaseException("暂无可用优惠券");
|
||||
}
|
||||
|
||||
// 生成流水号
|
||||
String code = generateCode();
|
||||
|
||||
// 创建打印记录
|
||||
VoucherPrintRecord printRecord = new VoucherPrintRecord();
|
||||
printRecord.setCode(code);
|
||||
printRecord.setFaceId(request.getFaceId());
|
||||
printRecord.setBrokerId(request.getBrokerId());
|
||||
printRecord.setScenicId(request.getScenicId());
|
||||
printRecord.setVoucherCodeId(availableVoucher.getId());
|
||||
printRecord.setVoucherCode(availableVoucher.getCode());
|
||||
printRecord.setPrintStatus(0); // 待打印
|
||||
printRecord.setCreateTime(new Date());
|
||||
printRecord.setUpdateTime(new Date());
|
||||
printRecord.setDeleted(0);
|
||||
|
||||
voucherPrintRecordMapper.insert(printRecord);
|
||||
|
||||
// TODO: 调用打印机接口打印小票
|
||||
// printTicket(printRecord);
|
||||
|
||||
// 暂时标记为打印成功状态(实际应该在打印成功回调中更新)
|
||||
printRecord.setPrintStatus(1);
|
||||
voucherPrintRecordMapper.updatePrintStatus(printRecord.getId(), 1, null);
|
||||
|
||||
log.info("成功创建打印记录: {}, 券码: {}", code, availableVoucher.getCode());
|
||||
return buildResponse(printRecord);
|
||||
|
||||
} finally {
|
||||
// 释放锁
|
||||
if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
|
||||
redisTemplate.delete(lockKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证faceId是否属于当前用户
|
||||
*/
|
||||
private void validateFaceOwnership(Long faceId, Long currentUserId) {
|
||||
FaceEntity face = faceRepository.getFace(faceId);
|
||||
if (face == null) {
|
||||
throw new BaseException("用户人脸信息不存在");
|
||||
}
|
||||
|
||||
if (!currentUserId.equals(face.getMemberId())) {
|
||||
throw new BaseException("无权限操作该人脸信息");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用且未被打印过的券码
|
||||
*/
|
||||
private PriceVoucherCode getAvailableUnprintedVoucher(Long scenicId) {
|
||||
return priceVoucherCodeMapper.findRandomUnprintedVoucher(scenicId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成流水号
|
||||
*/
|
||||
private String generateCode() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||
String timestamp = sdf.format(new Date());
|
||||
String randomSuffix = String.valueOf((int)(Math.random() * 1000)).formatted("%03d");
|
||||
return CODE_PREFIX + timestamp + randomSuffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建响应对象
|
||||
*/
|
||||
private VoucherPrintResp buildResponse(VoucherPrintRecord record) {
|
||||
VoucherPrintResp response = new VoucherPrintResp();
|
||||
BeanUtils.copyProperties(record, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用打印机接口(待实现)
|
||||
*/
|
||||
private void printTicket(VoucherPrintRecord record) {
|
||||
// TODO: 实现打印机接口调用逻辑
|
||||
log.info("TODO: 调用打印机打印小票,记录ID: {}, 券码: {}", record.getId(), record.getVoucherCode());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user