diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java new file mode 100644 index 0000000..b3dc2f0 --- /dev/null +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppVoucherController.java @@ -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 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); + } +} diff --git a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java index 385b1ac..9680742 100644 --- a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherClaimReq.java @@ -7,4 +7,5 @@ public class VoucherClaimReq { private Long scenicId; private Long brokerId; private Long faceId; + private String code; } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherPrintReq.java b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherPrintReq.java new file mode 100644 index 0000000..dcfe54e --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/req/VoucherPrintReq.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherPrintResp.java b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherPrintResp.java new file mode 100644 index 0000000..4a9e552 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/dto/resp/VoucherPrintResp.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/entity/VoucherPrintRecord.java b/src/main/java/com/ycwl/basic/pricing/entity/VoucherPrintRecord.java new file mode 100644 index 0000000..49311c1 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/entity/VoucherPrintRecord.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java index 732d414..d7b0048 100644 --- a/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java +++ b/src/main/java/com/ycwl/basic/pricing/mapper/PriceVoucherCodeMapper.java @@ -91,4 +91,11 @@ public interface PriceVoucherCodeMapper extends BaseMapper { * @return 可用券码 */ PriceVoucherCode findFirstAvailableByBatchId(@Param("batchId") Long batchId); + + /** + * 随机获取一个未被打印过的可用券码 + * @param scenicId 景区ID + * @return 可用券码 + */ + PriceVoucherCode findRandomUnprintedVoucher(@Param("scenicId") Long scenicId); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java b/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java new file mode 100644 index 0000000..505f326 --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/mapper/VoucherPrintRecordMapper.java @@ -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 { + + /** + * 根据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); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java b/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java new file mode 100644 index 0000000..c90e65f --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/VoucherPrintService.java @@ -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); +} \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java index d3710b2..b2cabfc 100644 --- a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherCodeServiceImpl.java @@ -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 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 diff --git a/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java new file mode 100644 index 0000000..56858ae --- /dev/null +++ b/src/main/java/com/ycwl/basic/pricing/service/impl/VoucherPrintServiceImpl.java @@ -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 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()); + } +} \ No newline at end of file diff --git a/src/main/resources/mapper/PriceVoucherCodeMapper.xml b/src/main/resources/mapper/PriceVoucherCodeMapper.xml new file mode 100644 index 0000000..2084c0d --- /dev/null +++ b/src/main/resources/mapper/PriceVoucherCodeMapper.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, + remark, create_time, update_time, deleted, deleted_at + + + + + + + + + + + + UPDATE price_voucher_code + SET status = 1, + face_id = #{faceId}, + claimed_time = #{claimedTime}, + update_time = NOW() + WHERE id = #{id} + AND status = 0 + AND deleted = 0 + + + + UPDATE price_voucher_code + SET status = 2, + used_time = #{usedTime}, + remark = #{remark}, + update_time = NOW() + WHERE code = #{code} + AND status = 1 + AND deleted = 0 + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/VoucherPrintRecordMapper.xml b/src/main/resources/mapper/VoucherPrintRecordMapper.xml new file mode 100644 index 0000000..87a8dfa --- /dev/null +++ b/src/main/resources/mapper/VoucherPrintRecordMapper.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + id, code, face_id, broker_id, scenic_id, voucher_code_id, voucher_code, + print_status, error_message, create_time, update_time, deleted, deleted_at + + + + + + + + UPDATE voucher_print_record + SET print_status = #{printStatus}, + error_message = #{errorMessage}, + update_time = NOW() + WHERE id = #{id} + + + \ No newline at end of file