Compare commits

..

10 Commits

Author SHA1 Message Date
09d142aa98 feat(coupon): 添加优惠券领取后有效期配置功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 在数据库插入和更新操作中添加 valid_days_after_claim 字段支持
- 在使用优惠券时增加对优惠券全局有效性和领取记录过期时间的验证逻辑
- 添加对已过期领取记录的筛选和错误提示处理
- 新增优惠券使用请求、状态枚举和异常类的测试用例
- 实现优惠券配置的完整有效期验证流程
2026-02-14 15:14:42 +08:00
143185926c feat(pricing): 添加全局配置查询功能并优化价格计算逻辑
- 在PriceProductConfigMapper中新增selectGlobalByProductTypeAndId方法,用于查询全局配置(排除景区级配置)
- 在PriceTierConfigMapper中新增selectGlobalByProductTypeAndQuantity方法,用于查询全局阶梯价格配置
- 移除价格计算服务中的default配置兜底逻辑,简化价格计算流程
- 更新ProductConfigServiceImpl中的配置查询逻辑,使用新的全局配置查询方法替代原有查询方式
- 优化配置查找顺序,提高全局配置的优先级和查询效率
2026-02-14 13:48:31 +08:00
cbbdd02003 feat(voucher): 优化券码领取接口返回结果
- 添加success和message字段到VoucherCodeResp响应类
- 修改券码领取逻辑,失败时不抛出异常而是返回失败结果
- 实现券码状态、用户权限和批次验证的统一响应格式
- 成功领取时设置success为true并返回成功消息
- 失败时设置success为false并返回具体失败原因
- 保持券码基础信息在失败情况下仍然返回给前端
2026-02-14 05:07:08 +08:00
1110b5409b refactor(voucher): 移除券码领取接口中的景区ID参数验证
- 删除 VoucherClaimReq 中的 scenicId 字段
- 移除券码领取接口中对景区ID的空值检查
- 更新查询条件,不再按景区ID过滤券码
- 修改错误提示信息为"券码不存在"
- 调整领券权限校验逻辑,使用券码关联的景区ID进行验证
2026-02-14 05:07:04 +08:00
4ac59b1f31 fix(puzzle): 解决拼图生成服务重复内容检测问题
- 添加对正在生成中记录的状态检查,避免并发重复写入
- 实现等待机制处理相同内容正在生成的情况
- 优化数据库查询逻辑,同时匹配成功和生成中的记录
- 仅对成功记录标记素材版本缓存,避免生成中记录失败时的错误标记
- 更新日志输出包含记录状态信息以便调试
- 添加超时机制确保系统稳定性
2026-02-14 05:07:00 +08:00
671cad4687 feat(goods): 添加摄影师拍照模式支持
- 在GoodsServiceImpl中集成景区配置管理器检查
- 添加摄影模式下的成员资源查询逻辑
- 实现视频任务状态成功设置和计数返回
- 更新FaceServiceImpl中的显示文本为更准确的描述
- 将"记录大片"改为"拍摄照片"以匹配实际功能
2026-02-14 05:06:54 +08:00
90fb0df69c feat(face): 添加摄影师拍照功能支持
- 在ContentPageVO中新增origUrl字段用于存储原始图片地址
- 集成DeviceV2DTO设备数据结构支持摄影师设备管理
- 添加SourceRepository依赖注入实现资源数据访问
- 实现景区模式2下的摄影师拍照内容展示逻辑
- 支持按设备分组显示摄影师拍摄的照片内容
- 添加摄影师拍照相关的购买状态和锁定类型控制
- 更新人脸识别页面查询返回摄影师拍摄的内容列表
- 优化景区配置管理器变量
2026-02-13 20:06:23 +08:00
383f9c4a31 refactor(pricing): 将券码系统中的faceId替换为userId
- 移除AppVoucherController中的人脸相关依赖和验证逻辑
- 修改VoucherClaimReq和VoucherCodeQueryReq数据传输对象,将faceId字段替换为userId
- 更新VoucherCodeResp和VoucherDetailResp响应对象中的用户标识字段
- 修改数据库实体PriceVoucherCode中领取人标识字段从faceId改为userId
- 更新PriceVoucherCodeMapper中所有SQL查询的人脸ID参数为用户ID参数
- 修改VoucherCodeServiceImpl中券码领取和查询逻辑使用用户ID进行操作
- 更新VoucherServiceImpl中券码验证和使用记录的相关用户标识处理
- 统一数据库表字段和代码中的命名规范,确保用户标识一致性
2026-02-13 20:06:13 +08:00
9a92a4943a feat(face): 根据景区模式动态设置人脸识别状态显示文本
- 获取景区配置管理器以判断景区模式
- 当景区模式为2时显示"去拍摄点免费拍照吧"
- 其他模式下显示"快去智能机位打卡吧"
- 保持原有业务逻辑不变
2026-02-13 16:11:29 +08:00
959eb6077e feat(printer): 添加批量创建虚拟订单功能
- 在PrinterTvController中新增printerService和orderService依赖注入
- 添加getPrinterListByScenicId接口获取景区下启用状态的打印机列表
- 新增createVirtualOrder接口支持批量创建虚拟用户订单
- 新增queryOrder接口用于查询订单支付状态
- 创建TvCreateVirtualOrderRequest请求参数类
- 在PrinterService中实现createBatchVirtualOrder批量创建订单逻辑
- 支持通过faceSampleIds自动查找关联照片素材聚合为一笔订单
- 支持是否需要实际支付的配置选项
- 实现订单价格计算和微信支付集成
2026-02-13 14:44:57 +08:00
34 changed files with 797 additions and 175 deletions

View File

@@ -249,6 +249,9 @@ public class OrderBiz {
case 13: // AI微单
sourceRepository.setUserIsBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId(), order.getId());
break;
case 14: // 单张照片
sourceRepository.setUserIsBuyItemBySourceId(order.getMemberId(), item.getGoodsId(), order.getFaceId(), order.getId());
break;
case 3:
printerService.setUserIsBuyItem(order.getMemberId(), item.getGoodsId(), order.getId());
break;
@@ -287,6 +290,9 @@ public class OrderBiz {
case 2: // 照片原素材
sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId());
break;
case 14: // 单张照片
sourceRepository.setUserNotBuyItemBySourceId(order.getMemberId(), item.getGoodsId(), order.getFaceId());
break;
}
});
orderRepository.clearOrderCache(orderId); // 更新完了,清理下
@@ -311,6 +317,9 @@ public class OrderBiz {
case 2: // 照片原素材
sourceRepository.setUserNotBuyItem(order.getMemberId(), item.getGoodsType(), item.getGoodsId());
break;
case 14: // 单张照片
sourceRepository.setUserNotBuyItemBySourceId(order.getMemberId(), item.getGoodsId(), order.getFaceId());
break;
}
});
orderRepository.clearOrderCache(orderId); // 更新完了,清理下

View File

@@ -4,6 +4,7 @@ import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.mapper.VideoMapper;
import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
@@ -95,6 +96,12 @@ public class AppOrderV2Controller {
request.setFaceId(video.getFaceId());
}
case RECORDING_SET, PHOTO_SET, AI_CAM_PHOTO_SET -> request.setFaceId(Long.valueOf(productItem.getProductId()));
case PHOTO -> {
MemberSourceEntity ms = sourceMapper.getMemberSourceByMemberAndSourceId(currentUserId, Long.valueOf(productItem.getProductId()));
if (ms != null) {
request.setFaceId(ms.getFaceId());
}
}
}
}
@@ -141,6 +148,9 @@ public class AppOrderV2Controller {
Integer _count = sourceMapper.countUser(aiPhotoSetReqQuery);
product.setQuantity(_count);
break;
case PHOTO:
product.setQuantity(1);
break;
default:
log.warn("未知的商品类型,跳过重复购买检查: productType={}", product.getProductType());
break;

View File

@@ -1,15 +1,12 @@
package com.ycwl.basic.controller.mobile;
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.VoucherClaimReq;
import com.ycwl.basic.pricing.dto.req.VoucherPrintReq;
import com.ycwl.basic.pricing.dto.resp.VoucherCodeResp;
import com.ycwl.basic.pricing.dto.resp.VoucherPrintResp;
import com.ycwl.basic.pricing.service.VoucherCodeService;
import com.ycwl.basic.pricing.service.VoucherPrintService;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.utils.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -30,8 +27,6 @@ public class AppVoucherController {
private VoucherPrintService voucherPrintService;
@Autowired
private VoucherCodeService voucherCodeService;
@Autowired
private FaceRepository faceRepository;
/**
* 打印小票
@@ -60,11 +55,6 @@ public class AppVoucherController {
@PostMapping("/claim")
public ApiResponse<VoucherCodeResp> claimVoucher(@RequestBody VoucherClaimReq req) {
FaceEntity face = faceRepository.getFace(req.getFaceId());
if (face == null) {
throw new BaseException("请选择人脸");
}
req.setScenicId(face.getScenicId());
VoucherCodeResp result = voucherCodeService.claimVoucher(req);
return ApiResponse.success(result);
}

View File

@@ -9,12 +9,17 @@ import com.ycwl.basic.model.mobile.face.FaceRecognizeResp;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.mp.MpConfigEntity;
import com.ycwl.basic.model.pc.printer.resp.PrinterResp;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.model.printer.req.TvCreateVirtualOrderRequest;
import com.ycwl.basic.pay.entity.PayResponse;
import com.ycwl.basic.repository.DeviceRepository;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.service.pc.FaceService;
import com.ycwl.basic.service.pc.OrderService;
import com.ycwl.basic.service.printer.PrinterService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.WxMpUtil;
import jakarta.servlet.http.HttpServletResponse;
@@ -22,6 +27,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -31,6 +37,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
@IgnoreToken
// 打印机大屏对接接口
@@ -44,6 +51,8 @@ public class PrinterTvController {
private final FaceRepository faceRepository;
private final FaceService pcFaceService;
private final SourceMapper sourceMapper;
private final PrinterService printerService;
private final OrderService orderService;
/**
* 获取景区列表
@@ -191,4 +200,58 @@ public class PrinterTvController {
response.sendRedirect(face.getFaceUrl());
}
/**
* 获取景区下的打印机列表
*
* @param scenicId 景区ID
* @return 启用状态的打印机列表
*/
@GetMapping("/printer/list")
public ApiResponse<List<PrinterResp>> getPrinterListByScenicId(@RequestParam Long scenicId) {
return ApiResponse.success(printerService.listByScenicId(scenicId));
}
/**
* 批量创建虚拟用户订单
* 传入faceSampleIds,自动查找关联的照片素材(type=2),聚合为一笔订单、一次支付
*
* @param request 请求参数(含faceSampleIds列表)
* @return 聚合订单结果
*/
@PostMapping("/createVirtualOrder")
public ApiResponse<Map<String, Object>> createVirtualOrder(@RequestBody TvCreateVirtualOrderRequest request) {
if (request.getFaceSampleIds() == null || request.getFaceSampleIds().isEmpty()) {
return ApiResponse.fail("faceSampleIds不能为空");
}
try {
List<SourceEntity> sources = sourceMapper.listByFaceSampleIdsAndType(request.getFaceSampleIds(), 2);
if (sources.isEmpty()) {
return ApiResponse.fail("未找到关联的照片素材");
}
List<Long> sourceIds = sources.stream().map(SourceEntity::getId).toList();
Map<String, Object> result = printerService.createBatchVirtualOrder(
sourceIds,
request.getScenicId(),
request.getPrinterId(),
request.getNeedEnhance(),
request.getPrintImgUrl(),
request.getNeedActualPayment()
);
return ApiResponse.success(result);
} catch (Exception e) {
return ApiResponse.fail(e.getMessage());
}
}
/**
* 查询订单支付状态
*
* @param orderId 订单ID
* @return 支付状态信息
*/
@GetMapping("/order/query")
public ApiResponse<PayResponse> queryOrder(@RequestParam("orderId") Long orderId) {
return ApiResponse.success(orderService.queryOrder(orderId));
}
}

View File

@@ -8,6 +8,7 @@ import com.ycwl.basic.model.pc.source.entity.SourceWatermarkEntity;
import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
import com.ycwl.basic.model.pc.source.resp.SourceRespVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
@@ -223,4 +224,19 @@ public interface SourceMapper {
* @return 有数据的时间桶列表
*/
List<DeviceSourceTimelineVO> getDeviceSourceTimeline(Long deviceId, Date startTime, Date endTime);
/**
* 根据会员ID和素材ID查询 member_source 关联记录
* @param memberId 会员ID
* @param sourceId 素材ID
* @return 关联记录(含 faceId 等信息)
*/
MemberSourceEntity getMemberSourceByMemberAndSourceId(@Param("memberId") Long memberId, @Param("sourceId") Long sourceId);
/**
* 根据会员ID和素材ID更新 member_source 关联记录的购买状态
* @param memberSourceEntity 包含 memberId、sourceId、isBuy、orderId
* @return 影响行数
*/
int updateRelationBySourceId(MemberSourceEntity memberSourceEntity);
}

View File

@@ -25,6 +25,7 @@ public class ContentPageVO {
private int lockType;
// 内容id contentType为0或1时才有值
private Long contentId;
private String origUrl;
private String videoUrl;
// 模版id
private Long templateId;

View File

@@ -0,0 +1,44 @@
package com.ycwl.basic.model.printer.req;
import lombok.Data;
import java.util.List;
/**
* 打印机大屏创建虚拟订单请求参数
* 通过 faceSampleIds 自动查找关联的照片素材进行下单
*/
@Data
public class TvCreateVirtualOrderRequest {
/**
* 人脸样本ID列表,系统自动查找这些样本关联的所有照片素材(type=2)
*/
private List<Long> faceSampleIds;
/**
* 景区ID
*/
private Long scenicId;
/**
* 打印机ID(可选)
*/
private Integer printerId;
/**
* 是否需要图像增强(可选,默认不增强)
*/
private Boolean needEnhance;
/**
* 打印图片URL(可选,如果提供则使用此URL进行打印)
*/
private String printImgUrl;
/**
* 是否需要实际支付(可选,默认false)
* false/null: 创建0元虚拟订单,立即完成购买
* true: 创建待支付订单(计算实际价格)
*/
private Boolean needActualPayment;
}

View File

@@ -1,6 +1,7 @@
package com.ycwl.basic.pricing.controller;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReq;
import com.ycwl.basic.pricing.dto.req.VoucherBatchCreateReqV2;
import com.ycwl.basic.pricing.dto.req.VoucherBatchQueryReq;
@@ -96,8 +97,9 @@ public class VoucherManagementController {
}
@GetMapping("/mobile/my-codes")
public ApiResponse<List<VoucherCodeResp>> getMyVoucherCodes(@RequestParam Long faceId) {
List<VoucherCodeResp> codes = voucherCodeService.getMyVoucherCodes(faceId);
public ApiResponse<List<VoucherCodeResp>> getMyVoucherCodes() {
Long userId = Long.valueOf(BaseContextHandler.getUserId());
List<VoucherCodeResp> codes = voucherCodeService.getMyVoucherCodes(userId);
return ApiResponse.success(codes);
}
}

View File

@@ -4,7 +4,5 @@ import lombok.Data;
@Data
public class VoucherClaimReq {
private Long scenicId;
private Long faceId;
private String code;
}

View File

@@ -9,7 +9,7 @@ import lombok.EqualsAndHashCode;
public class VoucherCodeQueryReq extends BaseQueryParameterReq {
private Long batchId;
private Long scenicId;
private Long faceId;
private Long userId;
private Integer status;
private String code;
}

View File

@@ -7,6 +7,15 @@ import java.util.Date;
@Data
public class VoucherCodeResp {
/**
* 领取是否成功
*/
private Boolean success;
/**
* 结果描述(失败时为原因说明)
*/
private String message;
private Long id;
private Long batchId;
private String batchName;
@@ -14,7 +23,7 @@ public class VoucherCodeResp {
private String code;
private Integer status;
private String statusName;
private Long faceId;
private Long userId;
private Date claimedTime;
private Date usedTime;
private String remark;

View File

@@ -102,9 +102,9 @@ public class VoucherDetailResp {
@Data
public static class UserInfo {
/**
* 用户人脸ID
* 用户ID
*/
private Long faceId;
private Long userId;
/**
* 该用户已使用此券码的次数

View File

@@ -39,9 +39,9 @@ public class PriceVoucherCode {
private Integer status;
/**
* 领取人faceId
* 领取人用户ID
*/
private Long faceId;
private Long userId;
/**
* 领取时间

View File

@@ -61,11 +61,11 @@ public interface PriceCouponConfigMapper extends BaseMapper<PriceCouponConfig> {
*/
@Insert("INSERT INTO price_coupon_config (coupon_name, coupon_type, discount_value, min_amount, " +
"max_discount, applicable_products, required_attribute_keys, total_quantity, used_quantity, " +
"claimed_quantity, user_claim_limit, valid_from, valid_until, " +
"claimed_quantity, user_claim_limit, valid_from, valid_until, valid_days_after_claim, " +
"is_active, scenic_id, create_time, update_time) VALUES " +
"(#{couponName}, #{couponType}, #{discountValue}, #{minAmount}, #{maxDiscount}, " +
"#{applicableProducts}, #{requiredAttributeKeys}, #{totalQuantity}, #{usedQuantity}, " +
"#{claimedQuantity}, #{userClaimLimit}, #{validFrom}, #{validUntil}, " +
"#{claimedQuantity}, #{userClaimLimit}, #{validFrom}, #{validUntil}, #{validDaysAfterClaim}, " +
"#{isActive}, #{scenicId}, NOW(), NOW())")
int insertCoupon(PriceCouponConfig coupon);
@@ -76,7 +76,8 @@ public interface PriceCouponConfigMapper extends BaseMapper<PriceCouponConfig> {
"discount_value = #{discountValue}, min_amount = #{minAmount}, max_discount = #{maxDiscount}, " +
"applicable_products = #{applicableProducts}, required_attribute_keys = #{requiredAttributeKeys}, " +
"total_quantity = #{totalQuantity}, user_claim_limit = #{userClaimLimit}, " +
"valid_from = #{validFrom}, valid_until = #{validUntil}, is_active = #{isActive}, " +
"valid_from = #{validFrom}, valid_until = #{validUntil}, valid_days_after_claim = #{validDaysAfterClaim}, " +
"is_active = #{isActive}, " +
"scenic_id = #{scenicId}, update_time = NOW() WHERE id = #{id}")
int updateCoupon(PriceCouponConfig coupon);

View File

@@ -34,6 +34,12 @@ public interface PriceProductConfigMapper extends BaseMapper<PriceProductConfig>
@Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND is_active = 1")
PriceProductConfig selectByProductTypeAndId(String productType, String productId);
/**
* 根据商品类型和商品ID查询全局配置(排除景区级配置)
*/
@Select("SELECT * FROM price_product_config WHERE product_type = #{productType} AND product_id = #{productId} AND (scenic_id IS NULL OR scenic_id = '') AND is_active = 1")
PriceProductConfig selectGlobalByProductTypeAndId(String productType, String productId);
/**
* 根据商品类型、商品ID和景区ID查询配置(支持景区维度)
*/

View File

@@ -27,6 +27,17 @@ public interface PriceTierConfigMapper extends BaseMapper<PriceTierConfig> {
@Param("productId") String productId,
@Param("quantity") Integer quantity);
/**
* 根据商品类型、商品ID和数量查询全局匹配的阶梯价格(排除景区级配置)
*/
@Select("SELECT * FROM price_tier_config WHERE product_type = #{productType} " +
"AND product_id = #{productId} AND (scenic_id IS NULL OR scenic_id = '') " +
"AND #{quantity} >= min_quantity AND #{quantity} <= max_quantity " +
"AND is_active = 1 ORDER BY sort_order ASC LIMIT 1")
PriceTierConfig selectGlobalByProductTypeAndQuantity(@Param("productType") String productType,
@Param("productId") String productId,
@Param("quantity") Integer quantity);
/**
* 根据商品类型、商品ID、数量和景区ID查询匹配的阶梯价格(支持景区维度)
*/

View File

@@ -21,31 +21,31 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param code 券码
* @return 券码信息
*/
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
@Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE code = #{code} AND deleted = 0 LIMIT 1")
PriceVoucherCode selectByCode(@Param("code") String code);
/**
* 根据faceId和scenicId统计已领取的券码数量
* @param faceId 用户faceId
* 根据userId和scenicId统计已领取的券码数量
* @param userId 用户ID
* @param scenicId 景区ID
* @return 数量
*/
@Select("SELECT COUNT(1) FROM price_voucher_code WHERE face_id = #{faceId} AND scenic_id = #{scenicId} AND deleted = 0")
Integer countByFaceIdAndScenicId(@Param("faceId") Long faceId, @Param("scenicId") Long scenicId);
@Select("SELECT COUNT(1) FROM price_voucher_code WHERE user_id = #{userId} AND scenic_id = #{scenicId} AND deleted = 0")
Integer countByUserIdAndScenicId(@Param("userId") Long userId, @Param("scenicId") Long scenicId);
/**
* 查询用户在指定景区的可用券码列表
* @param faceId 用户faceId
* @param userId 用户ID
* @param scenicId 景区ID
* @return 券码列表
*/
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
@Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE face_id = #{faceId} AND scenic_id = #{scenicId} AND status = 1 AND deleted = 0 " +
"FROM price_voucher_code WHERE user_id = #{userId} AND scenic_id = #{scenicId} AND status = 1 AND deleted = 0 " +
"ORDER BY claimed_time DESC")
List<PriceVoucherCode> selectAvailableVouchersByFaceIdAndScenicId(@Param("faceId") Long faceId,
List<PriceVoucherCode> selectAvailableVouchersByUserIdAndScenicId(@Param("userId") Long userId,
@Param("scenicId") Long scenicId);
/**
@@ -54,7 +54,7 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param limit 限制数量
* @return 券码列表
*/
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
@Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT #{limit}")
List<PriceVoucherCode> selectUnclaimedVouchersByBatchId(@Param("batchId") Long batchId,
@@ -63,14 +63,14 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
/**
* 领取券码(更新状态为已领取)
* @param id 券码ID
* @param faceId 用户faceId
* @param userId 用户ID
* @param claimedTime 领取时间
* @return 影响行数
*/
@Update("UPDATE price_voucher_code SET status = 1, face_id = #{faceId}, claimed_time = #{claimedTime}, " +
@Update("UPDATE price_voucher_code SET status = 1, user_id = #{userId}, claimed_time = #{claimedTime}, " +
"update_time = NOW() WHERE id = #{id} AND status = 0 AND deleted = 0")
int claimVoucher(@Param("id") Long id,
@Param("faceId") Long faceId,
@Param("userId") Long userId,
@Param("claimedTime") LocalDateTime claimedTime);
/**
@@ -78,25 +78,25 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param batchId 批次ID
* @return 券码列表
*/
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
@Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE batch_id = #{batchId} AND deleted = 0 ORDER BY create_time DESC")
List<PriceVoucherCode> selectByBatchId(@Param("batchId") Long batchId);
/**
* 查询用户的券码列表
* @param faceId 用户faceId
* @param userId 用户ID
* @param scenicId 景区ID(可选)
* @return 券码列表
*/
@Select("<script>" +
"SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
"SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE face_id = #{faceId}" +
"FROM price_voucher_code WHERE user_id = #{userId}" +
"<if test='scenicId != null'> AND scenic_id = #{scenicId}</if>" +
" AND deleted = 0 ORDER BY claimed_time DESC" +
"</script>")
List<PriceVoucherCode> selectUserVouchers(@Param("faceId") Long faceId,
List<PriceVoucherCode> selectUserVouchers(@Param("userId") Long userId,
@Param("scenicId") Long scenicId);
/**
@@ -104,7 +104,7 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param batchId 批次ID
* @return 可用券码
*/
@Select("SELECT id, batch_id, scenic_id, code, status, face_id, claimed_time, used_time, " +
@Select("SELECT id, batch_id, scenic_id, code, status, user_id, claimed_time, used_time, " +
"current_use_count, last_used_time, remark, create_time, update_time, deleted, deleted_at " +
"FROM price_voucher_code WHERE batch_id = #{batchId} AND status = 0 AND deleted = 0 LIMIT 1")
PriceVoucherCode findFirstAvailableByBatchId(@Param("batchId") Long batchId);
@@ -114,7 +114,7 @@ public interface PriceVoucherCodeMapper extends BaseMapper<PriceVoucherCode> {
* @param scenicId 景区ID
* @return 可用券码
*/
@Select("SELECT pvc.id, pvc.batch_id, pvc.scenic_id, pvc.code, pvc.status, pvc.face_id, pvc.claimed_time, pvc.used_time, " +
@Select("SELECT pvc.id, pvc.batch_id, pvc.scenic_id, pvc.code, pvc.status, pvc.user_id, pvc.claimed_time, pvc.used_time, " +
"pvc.current_use_count, pvc.last_used_time, pvc.remark, pvc.create_time, pvc.update_time, pvc.deleted, pvc.deleted_at " +
"FROM price_voucher_code pvc WHERE pvc.scenic_id = #{scenicId} AND pvc.status = 0 AND pvc.deleted = 0 " +
"AND NOT EXISTS (SELECT 1 FROM voucher_print_record vpr WHERE vpr.voucher_code_id = pvc.id AND vpr.deleted = 0) " +

View File

@@ -15,9 +15,9 @@ public interface VoucherCodeService {
PageInfo<VoucherCodeResp> queryCodeList(VoucherCodeQueryReq req);
List<VoucherCodeResp> getMyVoucherCodes(Long faceId);
List<VoucherCodeResp> getMyVoucherCodes(Long userId);
void markCodeAsUsed(Long codeId, String remark);
boolean canClaimVoucher(Long faceId, Long scenicId);
boolean canClaimVoucher(Long userId, Long scenicId);
}

View File

@@ -224,6 +224,18 @@ public class CouponServiceImpl implements ICouponService {
@Override
@Transactional
public CouponUseResult useCoupon(CouponUseRequest request) {
Date now = new Date();
PriceCouponConfig coupon = couponConfigMapper.selectById(request.getCouponId());
if (coupon == null || coupon.getDeleted() == 1 || !Boolean.TRUE.equals(coupon.getIsActive())) {
throw new CouponInvalidException("优惠券不存在或已失效");
}
if (coupon.getValidFrom() != null && now.before(coupon.getValidFrom())) {
throw new CouponInvalidException("优惠券尚未生效");
}
if (coupon.getValidUntil() != null && !now.before(coupon.getValidUntil())) {
throw new CouponInvalidException("优惠券已过期");
}
List<PriceCouponClaimRecord> records = couponClaimRecordMapper.selectUserCouponRecords(
request.getUserId(), request.getCouponId());
@@ -234,10 +246,18 @@ public class CouponServiceImpl implements ICouponService {
// 查找一张可用的优惠券(状态为CLAIMED)
PriceCouponClaimRecord record = records.stream()
.filter(r -> r.getStatus() == CouponStatus.CLAIMED)
.filter(r -> r.getExpireTime() == null || r.getExpireTime().after(now))
.findFirst()
.orElse(null);
if (record == null) {
boolean hasClaimedButExpired = records.stream()
.anyMatch(r -> r.getStatus() == CouponStatus.CLAIMED
&& r.getExpireTime() != null
&& !r.getExpireTime().after(now));
if (hasClaimedButExpired) {
throw new CouponInvalidException("优惠券已过期");
}
// 如果没有可用的,抛出异常。为了错误信息准确,可以检查最后一张的状态
CouponStatus lastStatus = records.getFirst().getStatus();
throw new CouponInvalidException("优惠券状态无效: " + lastStatus);

View File

@@ -401,29 +401,10 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
log.warn("未找到具体商品配置: productType={}, productId={}, scenicId={}, 尝试使用通用配置",
productType, productId, scenicId);
// 兜底:使用default配置(带景区ID)
try {
PriceProductConfig defaultConfig = productConfigService.getProductConfig(productType.getCode(), "default", scenicId);
if (defaultConfig != null) {
actualPrice = defaultConfig.getBasePrice();
originalPrice = defaultConfig.getOriginalPrice();
if (isQuantityBasedPricing(capability)) {
actualPrice = actualPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
if (originalPrice != null) {
originalPrice = originalPrice.multiply(BigDecimal.valueOf(product.getQuantity()));
}
}
} else {
throw new PriceCalculationException("无法找到default配置");
}
} catch (Exception defaultEx) {
log.warn("未找到default配置: productType={}, scenicId={}", productType.getCode(), scenicId);
// 最后兜底:使用通用配置(向后兼容)
List<PriceProductConfig> configs = productConfigService.getProductConfig(productType.getCode());
if (!configs.isEmpty()) {
PriceProductConfig baseConfig = configs.getFirst(); // 使用第一个配置作为默认
PriceProductConfig baseConfig = configs.getFirst();
actualPrice = baseConfig.getBasePrice();
originalPrice = baseConfig.getOriginalPrice();
@@ -438,7 +419,6 @@ public class PriceCalculationServiceImpl implements IPriceCalculationService {
}
}
}
}
return new ProductPriceInfo(actualPrice, originalPrice);
}

View File

@@ -76,29 +76,41 @@ public class ProductConfigServiceImpl implements IProductConfigService {
return getProductConfig(productType, productId);
}
String scenicIdStr = scenicId.toString();
// 查询优先级:
// 1. 景区+商品ID
PriceProductConfig config = productConfigMapper.selectByProductTypeIdAndScenic(
productType, productId, scenicId.toString());
productType, productId, scenicIdStr);
if (config != null) {
log.debug("使用景区特定商品配置: productType={}, productId={}, scenicId={}",
productType, productId, scenicId);
return config;
}
// 2. 景区+默认
// 2. 景区+景区ID作为商品ID(productId未命中时回退)
if (!scenicIdStr.equals(productId)) {
config = productConfigMapper.selectByProductTypeIdAndScenic(
productType, scenicIdStr, scenicIdStr);
if (config != null) {
log.debug("使用景区ID作为商品ID的配置: productType={}, scenicId={}", productType, scenicId);
return config;
}
}
// 3. 景区+默认
if (!"default".equals(productId)) {
config = productConfigMapper.selectByProductTypeIdAndScenic(
productType, "default", scenicId.toString());
productType, "default", scenicIdStr);
if (config != null) {
log.debug("使用景区默认配置: productType={}, scenicId={}", productType, scenicId);
return config;
}
}
// 3. 全局+商品ID (兜底)
// 4. 全局+商品ID (兜底)
try {
config = productConfigMapper.selectByProductTypeAndId(productType, productId);
config = productConfigMapper.selectGlobalByProductTypeAndId(productType, productId);
if (config != null) {
log.debug("使用全局商品配置: productType={}, productId={}", productType, productId);
return config;
@@ -107,8 +119,8 @@ public class ProductConfigServiceImpl implements IProductConfigService {
log.debug("全局商品配置未找到: productType={}, productId={}", productType, productId);
}
// 4. 全局+默认 (最后兜底)
config = productConfigMapper.selectByProductTypeAndId(productType, "default");
// 5. 全局+默认 (最后兜底)
config = productConfigMapper.selectGlobalByProductTypeAndId(productType, "default");
if (config != null) {
log.debug("使用全局默认配置: productType={}", productType);
return config;
@@ -130,20 +142,33 @@ public class ProductConfigServiceImpl implements IProductConfigService {
return getTierConfig(productType, productId, quantity);
}
String scenicIdStr = scenicId.toString();
// 查询优先级:
// 1. 景区+商品ID
PriceTierConfig config = tierConfigMapper.selectByProductTypeQuantityAndScenic(
productType, productId, quantity, scenicId.toString());
productType, productId, quantity, scenicIdStr);
if (config != null) {
log.debug("使用景区特定阶梯定价: productType={}, productId={}, quantity={}, scenicId={}",
productType, productId, quantity, scenicId);
return config;
}
// 2. 景区+默认
// 2. 景区+景区ID作为商品ID(productId未命中时回退)
if (!scenicIdStr.equals(productId)) {
config = tierConfigMapper.selectByProductTypeQuantityAndScenic(
productType, scenicIdStr, quantity, scenicIdStr);
if (config != null) {
log.debug("使用景区ID作为商品ID的阶梯定价: productType={}, quantity={}, scenicId={}",
productType, quantity, scenicId);
return config;
}
}
// 3. 景区+默认
if (!"default".equals(productId)) {
config = tierConfigMapper.selectByProductTypeQuantityAndScenic(
productType, "default", quantity, scenicId.toString());
productType, "default", quantity, scenicIdStr);
if (config != null) {
log.debug("使用景区默认阶梯定价: productType={}, quantity={}, scenicId={}",
productType, quantity, scenicId);
@@ -151,16 +176,16 @@ public class ProductConfigServiceImpl implements IProductConfigService {
}
}
// 3. 全局+商品ID (兜底)
config = tierConfigMapper.selectByProductTypeAndQuantity(productType, productId, quantity);
// 4. 全局+商品ID (兜底)
config = tierConfigMapper.selectGlobalByProductTypeAndQuantity(productType, productId, quantity);
if (config != null) {
log.debug("使用全局阶梯定价: productType={}, productId={}, quantity={}",
productType, productId, quantity);
return config;
}
// 4. 全局+默认 (最后兜底)
config = tierConfigMapper.selectByProductTypeAndQuantity(productType, "default", quantity);
// 5. 全局+默认 (最后兜底)
config = tierConfigMapper.selectGlobalByProductTypeAndQuantity(productType, "default", quantity);
if (config != null) {
log.debug("使用全局默认阶梯定价: productType={}, quantity={}", productType, quantity);
}

View File

@@ -3,6 +3,7 @@ package com.ycwl.basic.pricing.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ycwl.basic.constant.BaseContextHandler;
import com.ycwl.basic.exception.BizException;
import com.ycwl.basic.pricing.dto.req.VoucherClaimReq;
import com.ycwl.basic.pricing.dto.req.VoucherCodeQueryReq;
@@ -73,43 +74,49 @@ public void generateVoucherCodes(Long batchId, Long scenicId, Integer count) {
@Override
@Transactional
public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
if (req.getScenicId() == null) {
throw new BizException(400, "景区ID不能为空");
}
if (req.getFaceId() == null) {
throw new BizException(400, "用户faceId不能为空");
}
if (!StringUtils.hasText(req.getCode())) {
throw new BizException(400, "券码不能为空");
}
// 验证券码是否存在且未被领取
Long userId = Long.valueOf(BaseContextHandler.getUserId());
// 查询券码
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, "券码不存在或不属于该景区");
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 = voucherBatchMapper.selectById(voucherCode.getBatchId());
// 券码已找到,后续校验失败时仍返回 scenicId 等信息
if (!Objects.equals(voucherCode.getStatus(), VoucherCodeStatus.UNCLAIMED.getCode())) {
VoucherCodeResp resp = convertToResp(voucherCode, batch);
resp.setSuccess(false);
resp.setMessage("券码已被领取或已使用");
return resp;
}
if (!canClaimVoucher(userId, voucherCode.getScenicId())) {
VoucherCodeResp resp = convertToResp(voucherCode, batch);
resp.setSuccess(false);
resp.setMessage("该用户在此景区已领取过券码");
return resp;
}
if (batch == null || batch.getDeleted() == 1) {
throw new BizException(400, "券码批次不存在");
VoucherCodeResp resp = convertToResp(voucherCode, batch);
resp.setSuccess(false);
resp.setMessage("券码批次不存在");
return resp;
}
// 更新券码状态
voucherCode.setFaceId(req.getFaceId());
voucherCode.setUserId(userId);
voucherCode.setStatus(VoucherCodeStatus.CLAIMED_UNUSED.getCode());
voucherCode.setClaimedTime(new Date());
// 确保currentUseCount被初始化
@@ -121,7 +128,10 @@ public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
voucherBatchService.updateBatchClaimedCount(batch.getId());
return convertToResp(voucherCode, batch);
VoucherCodeResp resp = convertToResp(voucherCode, batch);
resp.setSuccess(true);
resp.setMessage("领取成功");
return resp;
}
@Override
@@ -132,7 +142,7 @@ public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
wrapper.eq(PriceVoucherCode::getDeleted, 0)
.eq(req.getBatchId() != null, PriceVoucherCode::getBatchId, req.getBatchId())
.eq(req.getScenicId() != null, PriceVoucherCode::getScenicId, req.getScenicId())
.eq(req.getFaceId() != null, PriceVoucherCode::getFaceId, req.getFaceId())
.eq(req.getUserId() != null, PriceVoucherCode::getUserId, req.getUserId())
.eq(req.getStatus() != null, PriceVoucherCode::getStatus, req.getStatus())
.like(StringUtils.hasText(req.getCode()), PriceVoucherCode::getCode, req.getCode())
.orderByDesc(PriceVoucherCode::getId);
@@ -149,9 +159,9 @@ public VoucherCodeResp claimVoucher(VoucherClaimReq req) {
}
@Override
public List<VoucherCodeResp> getMyVoucherCodes(Long faceId) {
public List<VoucherCodeResp> getMyVoucherCodes(Long userId) {
LambdaQueryWrapper<PriceVoucherCode> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PriceVoucherCode::getFaceId, faceId)
wrapper.eq(PriceVoucherCode::getUserId, userId)
.eq(PriceVoucherCode::getDeleted, 0)
.orderByDesc(PriceVoucherCode::getClaimedTime);
@@ -193,8 +203,8 @@ public void markCodeAsUsed(Long codeId, String remark) {
}
@Override
public boolean canClaimVoucher(Long faceId, Long scenicId) {
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);
public boolean canClaimVoucher(Long userId, Long scenicId) {
Integer count = voucherCodeMapper.countByUserIdAndScenicId(userId, scenicId);
return count == 0;
}

View File

@@ -132,7 +132,7 @@ public class VoucherServiceImpl implements IVoucherService {
if (faceId == null) {
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("用户信息缺失,无法验证券码权限");
} else if (!faceId.equals(voucherCodeEntity.getFaceId())) {
} else if (!faceId.equals(voucherCodeEntity.getUserId())) {
voucherInfo.setAvailable(false);
voucherInfo.setUnavailableReason("券码已被其他用户领取");
} else {
@@ -176,7 +176,7 @@ public class VoucherServiceImpl implements IVoucherService {
return new ArrayList<>();
}
List<PriceVoucherCode> voucherCodes = voucherCodeMapper.selectAvailableVouchersByFaceIdAndScenicId(faceId, scenicId);
List<PriceVoucherCode> voucherCodes = voucherCodeMapper.selectAvailableVouchersByUserIdAndScenicId(faceId, scenicId);
List<VoucherInfo> voucherInfos = new ArrayList<>();
for (PriceVoucherCode voucherCode : voucherCodes) {
@@ -234,7 +234,7 @@ public void markVoucherAsUsed(String voucherCode, String remark, String orderId,
PriceVoucherUsageRecord usageRecord = new PriceVoucherUsageRecord();
usageRecord.setVoucherCodeId(voucherCodeEntity.getId());
usageRecord.setVoucherCode(voucherCode);
usageRecord.setFaceId(faceId != null ? faceId : voucherCodeEntity.getFaceId());
usageRecord.setFaceId(faceId != null ? faceId : voucherCodeEntity.getUserId());
usageRecord.setScenicId(voucherCodeEntity.getScenicId());
usageRecord.setBatchId(voucherCodeEntity.getBatchId());
usageRecord.setUsageSequence(newUseCount); // 设置使用序号,表示这是该券码的第几次使用
@@ -279,7 +279,7 @@ public void markVoucherAsUsed(String voucherCode, String remark, String orderId,
return false;
}
Integer count = voucherCodeMapper.countByFaceIdAndScenicId(faceId, scenicId);
Integer count = voucherCodeMapper.countByUserIdAndScenicId(faceId, scenicId);
return count == 0;
}
@@ -435,7 +435,7 @@ public void markVoucherAsUsed(String voucherCode, String remark, String orderId,
// 设置用户信息
if (faceId != null) {
VoucherDetailResp.UserInfo userInfo = new VoucherDetailResp.UserInfo();
userInfo.setFaceId(faceId);
userInfo.setUserId(faceId);
// 计算该用户使用此券码的次数
List<PriceVoucherUsageRecord> userUsageRecords = usageRecordMapper.selectByVoucherCodeAndFaceId(voucherCodeEntity.getId(), faceId);

View File

@@ -141,6 +141,8 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
template.getId(), contentHash, resolvedScenicId
);
if (duplicateRecord != null) {
if (duplicateRecord.getStatus() == 1) {
// 已有成功记录,直接复用
long duration = System.currentTimeMillis() - startTime;
log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms",
duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration);
@@ -158,6 +160,31 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
true,
duplicateRecord.getId()
);
} else if (duplicateRecord.getStatus() == 0) {
// 相同内容正在生成中,等待完成后复用
log.info("检测到相同内容正在生成中,等待完成: recordId={}", duplicateRecord.getId());
PuzzleGenerationRecordEntity completedRecord = waitForRecordCompletion(duplicateRecord.getId(), 30_000);
if (completedRecord != null && completedRecord.getStatus() == 1) {
long duration = System.currentTimeMillis() - startTime;
log.info("等待生成中记录完成,复用结果: recordId={}, imageUrl={}, duration={}ms",
completedRecord.getId(), completedRecord.getResultImageUrl(), duration);
if (request.getFaceId() != null) {
faceStatusManager.markPuzzleSourceVersion(request.getFaceId(), template.getId(), 0);
}
return PuzzleGenerateResponse.success(
completedRecord.getResultImageUrl(),
completedRecord.getResultFileSize(),
completedRecord.getResultWidth(),
completedRecord.getResultHeight(),
(int) duration,
completedRecord.getId(),
true,
completedRecord.getId()
);
}
// 超时或失败,兜底创建新记录
log.warn("等待生成中记录超时或失败,创建新记录: originalRecordId={}", duplicateRecord.getId());
}
}
// 7. 创建生成记录
@@ -290,10 +317,10 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
);
if (duplicateRecord != null) {
long duration = System.currentTimeMillis() - startTime;
log.info("检测到重复内容,复用历史记录: recordId={}, imageUrl={}, duration={}ms",
duplicateRecord.getId(), duplicateRecord.getResultImageUrl(), duration);
// 标记素材版本缓存
if (request.getFaceId() != null) {
log.info("检测到重复内容,复用历史记录: recordId={}, status={}, imageUrl={}, duration={}ms",
duplicateRecord.getId(), duplicateRecord.getStatus(), duplicateRecord.getResultImageUrl(), duration);
// 仅成功记录才标记素材版本缓存(生成中的记录可能会失败)
if (request.getFaceId() != null && duplicateRecord.getStatus() == 1) {
faceStatusManager.markPuzzleSourceVersion(request.getFaceId(), template.getId(), 0);
}
return duplicateRecord.getId();
@@ -326,6 +353,33 @@ public class PuzzleGenerateServiceImpl implements IPuzzleGenerateService {
return record.getId();
}
/**
* 等待生成中的记录完成
* 轮询数据库直到记录状态变为非生成中(成功或失败),或超时返回null
*
* @param recordId 记录ID
* @param timeoutMs 超时时间(毫秒)
* @return 完成后的记录,超时返回null
*/
private PuzzleGenerationRecordEntity waitForRecordCompletion(Long recordId, long timeoutMs) {
long deadline = System.currentTimeMillis() + timeoutMs;
while (System.currentTimeMillis() < deadline) {
PuzzleGenerationRecordEntity record = recordMapper.getById(recordId);
if (record == null || record.getStatus() != 0) {
return record;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("等待记录完成被中断: recordId={}", recordId);
return null;
}
}
log.warn("等待记录完成超时: recordId={}, timeoutMs={}", recordId, timeoutMs);
return null;
}
/**
* 校验请求参数
*/

View File

@@ -235,6 +235,28 @@ public class SourceRepository {
faceStatusManager.invalidatePuzzleSourceVersion(faceId);
}
public void setUserIsBuyItemBySourceId(Long memberId, Long sourceId, Long faceId, Long orderId) {
MemberSourceEntity memberSource = new MemberSourceEntity();
memberSource.setMemberId(memberId);
memberSource.setSourceId(sourceId);
memberSource.setOrderId(orderId);
memberSource.setIsBuy(1);
sourceMapper.updateRelationBySourceId(memberSource);
memberRelationRepository.clearSCacheByFace(faceId);
faceStatusManager.invalidatePuzzleSourceVersion(faceId);
}
public void setUserNotBuyItemBySourceId(Long memberId, Long sourceId, Long faceId) {
MemberSourceEntity memberSource = new MemberSourceEntity();
memberSource.setMemberId(memberId);
memberSource.setSourceId(sourceId);
memberSource.setOrderId(null);
memberSource.setIsBuy(0);
sourceMapper.updateRelationBySourceId(memberSource);
memberRelationRepository.clearSCacheByFace(faceId);
faceStatusManager.invalidatePuzzleSourceVersion(faceId);
}
public SourceEntity getSource(Long id) {
return sourceMapper.getEntity(id);
}

View File

@@ -296,6 +296,15 @@ public class GoodsServiceImpl implements GoodsService {
return response;
}
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
if (Integer.valueOf(2).equals(scenicConfig.getInteger("scenic_mode", 0))) {
// 摄影师拍照
List<MemberSourceEntity> list = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
response.setStatus(VideoTaskStatus.SUCCESS.getCode());
response.setCount(list.size());
return response;
}
// ==================== 第三步:检查模板渲染状态 ====================
// 获取该景区的所有视频模板
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(response.getScenicId());

View File

@@ -12,6 +12,7 @@ import com.ycwl.basic.face.pipeline.enums.FaceMatchingScene;
import com.ycwl.basic.face.pipeline.factory.FaceMatchingPipelineFactory;
import com.ycwl.basic.facebody.adapter.IFaceBodyAdapter;
import com.ycwl.basic.facebody.entity.SearchFaceResultItem;
import com.ycwl.basic.integration.device.dto.device.DeviceV2DTO;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.ProjectMapper;
import com.ycwl.basic.mapper.SourceMapper;
@@ -60,6 +61,7 @@ import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.repository.MemberRelationRepository;
import com.ycwl.basic.repository.OrderRepository;
import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.repository.SourceRepository;
import com.ycwl.basic.repository.TemplateRepository;
import com.ycwl.basic.repository.VideoRepository;
import com.ycwl.basic.repository.VideoTaskRepository;
@@ -210,6 +212,8 @@ public class FaceServiceImpl implements FaceService {
private OrderRepository orderRepository;
@Autowired
private com.ycwl.basic.biz.FaceStatusManager faceStatusManager;
@Autowired
private SourceRepository sourceRepository;
@Override
public ApiResponse<PageInfo<FaceRespVO>> pageQuery(FaceReqQuery faceReqQuery) {
@@ -472,7 +476,47 @@ public class FaceServiceImpl implements FaceService {
if (face == null) {
return Collections.emptyList();
}
Long userId = face.getMemberId();
Long userId = Long.parseLong(BaseContextHandler.getUserId());
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
if (Integer.valueOf(2).equals(scenicConfig.getInteger("scenic_mode", 0))) {
List<ContentPageVO> result = new ArrayList<>();
// 摄影师拍照
List<DeviceV2DTO> deviceList = deviceRepository.getAllDeviceByScenicId(face.getScenicId());
List<SourceEntity> sourceEntityList = sourceMapper.listSourceByFaceRelation(face.getId(), 2);
for (SourceEntity sourceEntity : sourceEntityList) {
ContentPageVO content = new ContentPageVO();
content.setName("摄影师拍照");
deviceList.stream().filter(device -> device.getId().equals(sourceEntity.getDeviceId())).findFirst().ifPresent(device -> {
content.setGroup(device.getName());
});
content.setContentId(sourceEntity.getId());
content.setGoodsType(2);
content.setContentType(2);
content.setScenicId(sourceEntity.getScenicId());
content.setSourceType(2);
content.setOrigUrl(sourceEntity.getUrl());
content.setTemplateCoverUrl(sourceEntity.getThumbUrl());
content.setIsBuy(sourceEntity.getIsBuy());
content.setLockType(-1);
result.add(content);
}
List<Long> containedDeviceId = sourceEntityList.stream().map(SourceEntity::getDeviceId).filter(Objects::nonNull).distinct().toList();
deviceList.stream().filter(device -> !containedDeviceId.contains(device.getId())).forEach(device -> {
ContentPageVO content = new ContentPageVO();
content.setName(device.getName());
content.setGroup(device.getName());
content.setContentId(device.getId());
content.setGoodsType(2);
content.setContentType(2);
content.setScenicId(face.getScenicId());
content.setSourceType(2);
content.setTemplateCoverUrl("");
content.setIsBuy(0);
content.setLockType(1);
result.add(content);
});
return result;
}
List<TemplateRespVO> templateList = templateRepository.getTemplateListByScenicId(face.getScenicId());
List<ContentPageVO> contentList = templateList.stream().map(template -> {
/// select t.id templateId, t.scenic_id, t.`group`, t.`name`, pid, t.cover_url templateCoverUrl,
@@ -607,7 +651,6 @@ public class FaceServiceImpl implements FaceService {
sourceVideoContent.setGroup("直出原片");
sourceImageContent.setGroup("直出原片");
sourceAiCamContent.setGroup("智能连连拍");
ScenicConfigManager configManager = scenicRepository.getScenicConfigManager(face.getScenicId());
if (!scenicConfigFacade.isDisableSourceImage(face.getScenicId())) {
IsBuyRespVO isBuyRespVO = orderBiz.isBuy(face.getScenicId(), userId, faceId, SourceType.IMAGE.getCode(), faceId);
sourceImageContent.setSourceType(isBuyRespVO.getGoodsType());
@@ -679,7 +722,7 @@ public class FaceServiceImpl implements FaceService {
} else if (type == 3) {
sourceAiCamContent.setSourceType(13);
sourceAiCamContent.setLockType(-1);
sourceAiCamContent.setTemplateCoverUrl(configManager.getString("ai_camera_cover_url"));
sourceAiCamContent.setTemplateCoverUrl(scenicConfig.getString("ai_camera_cover_url"));
}
});
return contentList;
@@ -780,14 +823,31 @@ public class FaceServiceImpl implements FaceService {
sourceReqQuery.setMemberId(face.getMemberId());
sourceReqQuery.setFaceId(faceId);
sourceReqQuery.setType(2);
ScenicConfigManager scenicConfig = scenicRepository.getScenicConfigManager(face.getScenicId());
List<MemberSourceEntity> countUser = memberRelationRepository.listSourceByFaceRelation(faceId, 2);
if (countUser != null && !countUser.isEmpty()) {
statusResp.setStep2Status(true);
} else {
statusResp.setStep2Status(false);
if (Integer.valueOf(2).equals(scenicConfig.getInteger("scenic_mode", 0))) {
statusResp.setDisplayText("Hey,去拍摄点免费拍照吧");
} else {
statusResp.setDisplayText("Hey,快去智能机位打卡吧");
}
return statusResp;
}
if (Integer.valueOf(2).equals(scenicConfig.getInteger("scenic_mode", 0))) {
// 摄影模式
if (!countUser.isEmpty()) {
statusResp.setStep3Status(true);
statusResp.setDisplayText("已为您拍摄" + countUser.size() + "张照片");
return statusResp;
} else {
statusResp.setStep3Status(false);
statusResp.setDisplayText("Hey,去拍摄点免费拍照吧");
return statusResp;
}
} else {
VideoTaskStatusVO taskStatusByFaceId = goodsService.getTaskStatusByFaceId(faceId);
if (Integer.valueOf(1).equals(taskStatusByFaceId.getStatus())) {
if (taskStatusByFaceId.getCount() > 0) {
@@ -801,6 +861,7 @@ public class FaceServiceImpl implements FaceService {
statusResp.setStep3Status(false);
statusResp.setDisplayText("帧途AI正在为您渲染vlog,请稍候");
}
}
return statusResp;
}

View File

@@ -930,6 +930,7 @@ public class OrderServiceImpl implements OrderService {
Integer type = switch (productItem.getProductType()) {
case PHOTO_LOG -> 5;
case PHOTO_SET -> 2;
case PHOTO -> 14;
case VLOG_VIDEO -> 0;
case RECORDING_SET -> 1;
case AI_CAM_PHOTO_SET -> 13;
@@ -937,6 +938,7 @@ public class OrderServiceImpl implements OrderService {
};
Long goodsId = switch (productItem.getProductType()) {
case PHOTO_LOG -> Long.valueOf(productItem.getProductId());
case PHOTO -> Long.valueOf(productItem.getProductId());
case PHOTO_SET, RECORDING_SET -> face.getId();
case AI_CAM_PHOTO_SET -> face.getId();
case VLOG_VIDEO -> {

View File

@@ -157,6 +157,19 @@ public interface PrinterService {
*/
Map<String, Object> createVirtualOrder(Long sourceId, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment);
/**
* 批量创建虚拟用户订单(多个sourceId聚合为一笔订单、一次支付)
*
* @param sourceIds source记录ID列表
* @param scenicId 景区ID
* @param printerId 打印机ID(可选)
* @param needEnhance 是否需要图像增强(可选)
* @param printImgUrl 打印图片URL(可选)
* @param needActualPayment 是否需要实际支付
* @return 订单信息
*/
Map<String, Object> createBatchVirtualOrder(List<Long> sourceIds, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment);
/**
* 根据accessKey获取打印机详情
* @param accessKey 打印机accessKey

View File

@@ -1931,6 +1931,194 @@ public class PrinterServiceImpl implements PrinterService {
return result;
}
@Override
public Map<String, Object> createBatchVirtualOrder(List<Long> sourceIds, Long scenicId, Integer printerId, Boolean needEnhance, String printImgUrl, Boolean needActualPayment) {
if (sourceIds == null || sourceIds.isEmpty()) {
throw new BaseException("sourceIds不能为空");
}
// 1. 校验所有source并收集faceSample
List<SourceEntity> sources = new ArrayList<>();
FaceSampleEntity firstFaceSample = null;
for (Long sourceId : sourceIds) {
SourceEntity source = sourceMapper.getEntity(sourceId);
if (source == null) {
throw new BaseException("Source记录不存在: " + sourceId);
}
if (!scenicId.equals(source.getScenicId())) {
throw new BaseException("Source记录不属于该景区: " + sourceId);
}
FaceSampleEntity faceSample = faceSampleMapper.getEntity(source.getFaceSampleId());
if (faceSample == null) {
throw new BaseException("人脸样本不存在, sourceId=" + sourceId);
}
if (firstFaceSample == null) {
firstFaceSample = faceSample;
}
sources.add(source);
}
// 2. 生成一个虚拟用户 + 一条人脸记录
Long virtualMemberId = SnowFlakeUtil.getLongId();
Long faceId = SnowFlakeUtil.getLongId();
FaceEntity face = new FaceEntity();
face.setId(faceId);
face.setScenicId(scenicId);
face.setMemberId(virtualMemberId);
face.setFaceUrl(firstFaceSample.getFaceUrl());
face.setCreateAt(new Date());
faceMapper.add(face);
log.info("批量下单 - 创建虚拟用户: virtualMemberId={}, faceId={}, sourceCount={}", virtualMemberId, faceId, sourceIds.size());
// 3. 为每个source创建member_print记录
List<Integer> memberPrintIds = new ArrayList<>();
for (SourceEntity source : sources) {
String photoUrl = (printImgUrl != null && !printImgUrl.isEmpty()) ? printImgUrl : source.getUrl();
Integer memberPrintId = addUserPhoto(virtualMemberId, scenicId, photoUrl, faceId, source.getId());
if (memberPrintId == null) {
throw new BaseException("创建member_print记录失败, sourceId=" + source.getId());
}
setPhotoQuantity(virtualMemberId, scenicId, memberPrintId.longValue(), 1);
memberPrintIds.add(memberPrintId);
}
// 4. 验证打印机
if (printerId == null) {
List<PrinterResp> printerList = printerMapper.listByScenicId(scenicId);
if (printerList.isEmpty()) {
throw new BaseException("该景区没有可用的打印机");
}
if (printerList.size() != 1) {
throw new BaseException("请选择打印机");
}
printerId = printerList.getFirst().getId();
}
PrinterEntity printer = printerMapper.getById(printerId);
if (printer == null) {
throw new BaseException("打印机不存在");
}
if (printer.getStatus() != 1) {
throw new BaseException("打印机已停用");
}
if (!printer.getScenicId().equals(scenicId)) {
throw new BaseException("打印机不属于该景区");
}
// 5. 创建订单
OrderEntity order = new OrderEntity();
Long orderId = SnowFlakeUtil.getLongId();
redisTemplate.opsForValue().set("printer_size:" + orderId, printer.getPreferPaper(), 60, TimeUnit.SECONDS);
order.setId(orderId);
order.setMemberId(virtualMemberId);
order.setFaceId(faceId);
order.setOpenId("");
order.setScenicId(scenicId);
order.setType(3);
batchSetUserPhotoListToPrinter(virtualMemberId, scenicId, printerId);
List<MemberPrintResp> userPhotoList = getUserPhotoList(virtualMemberId, scenicId, faceId);
List<OrderItemEntity> orderItems = userPhotoList.stream().map(goods -> {
OrderItemEntity orderItem = new OrderItemEntity();
orderItem.setOrderId(orderId);
orderItem.setGoodsId(Long.valueOf(goods.getId()));
orderItem.setGoodsType(3);
return orderItem;
}).collect(Collectors.toList());
boolean actualPayment = Boolean.TRUE.equals(needActualPayment);
if (actualPayment) {
PriceCalculationRequest priceRequest = new PriceCalculationRequest();
priceRequest.setUserId(virtualMemberId);
priceRequest.setScenicId(scenicId);
List<ProductItem> productItems = new ArrayList<>();
ProductItem photoItem = new ProductItem();
photoItem.setProductType(ProductType.PHOTO_PRINT);
photoItem.setProductId(scenicId.toString());
photoItem.setQuantity(sourceIds.size());
photoItem.setPurchaseCount(sourceIds.size());
photoItem.setScenicId(scenicId.toString());
productItems.add(photoItem);
priceRequest.setProducts(productItems);
priceRequest.setAutoUseCoupon(false);
priceRequest.setPreviewOnly(false);
PriceCalculationResult priceResult = priceCalculationService.calculatePrice(priceRequest);
order.setPrice(priceResult.getFinalAmount());
order.setSlashPrice(priceResult.getOriginalAmount());
order.setPayPrice(priceResult.getFinalAmount());
order.setStatus(OrderStateEnum.UNPAID.getState());
log.info("批量下单 - 待支付订单: orderId={}, price={}, count={}", orderId, priceResult.getFinalAmount(), sourceIds.size());
if (needEnhance != null) {
redisTemplate.opsForValue().set("virtual_order_enhance:" + orderId, needEnhance.toString(), 24, TimeUnit.HOURS);
}
} else {
order.setPrice(BigDecimal.ZERO);
order.setSlashPrice(BigDecimal.ZERO);
order.setPayPrice(BigDecimal.ZERO);
order.setStatus(OrderStateEnum.PAID.getState());
order.setPayAt(new Date());
}
orderMapper.add(order);
int addOrderItems = orderMapper.addOrderItems(orderItems);
if (addOrderItems == NumberConstant.ZERO) {
throw new BaseException("订单添加失败");
}
log.info("批量下单 - 订单创建成功: orderId={}, itemCount={}", orderId, orderItems.size());
Map<String, Object> result = new HashMap<>();
result.put("orderId", orderId);
result.put("faceId", faceId);
result.put("virtualMemberId", virtualMemberId);
result.put("memberPrintIds", memberPrintIds);
result.put("sourceIds", sourceIds);
if (actualPayment) {
if (order.getPayPrice().compareTo(BigDecimal.ZERO) <= 0) {
order.setStatus(OrderStateEnum.PAID.getState());
order.setPayAt(new Date());
orderMapper.updateOrder(order);
log.info("批量下单 - 价格为0直接完成: orderId={}", orderId);
result.put("needPay", false);
} else {
IPayAdapter payAdapter = scenicService.getScenicPayAdapter(scenicId);
if (payAdapter instanceof WxMpPayAdapter adapter) {
NativePayService nativePayService = new NativePayService.Builder().config(adapter.getConfig()).build();
PrepayRequest prepayRequest = new PrepayRequest();
prepayRequest.setAppid(adapter._config().getAppId());
prepayRequest.setMchid(adapter._config().getMerchantId());
prepayRequest.setDescription("照片打印 x" + sourceIds.size());
prepayRequest.setOutTradeNo(String.valueOf(orderId));
prepayRequest.setNotifyUrl("https://zhentuai.com/api/mobile/wx/pay/v1/" + scenicId + "/payNotify");
Amount amount = new Amount();
amount.setTotal(order.getPayPrice().multiply(new BigDecimal(100)).intValue());
prepayRequest.setAmount(amount);
PrepayResponse prepayResponse = nativePayService.prepay(prepayRequest);
result.put("payCode", prepayResponse.getCodeUrl());
} else {
throw new BaseException("该景区不支持 Native 支付");
}
result.put("needPay", true);
result.put("price", order.getPayPrice());
}
} else {
result.put("needPay", false);
}
// 触发购买后逻辑(setUserIsBuyItem 内部遍历 orderItems 处理所有 memberPrint)
setUserIsBuyItem(virtualMemberId, memberPrintIds.getFirst().longValue(), orderId, needEnhance);
log.info("批量下单 - 购买后逻辑完成: orderId={}", orderId);
return result;
}
@Override
public PrinterEntity getByAccessKey(String accessKey) {
if (accessKey == null) {

View File

@@ -128,15 +128,15 @@
WHERE id = #{id}
</update>
<!-- 根据内容哈希查询历史记录(用于去重) -->
<!-- 根据内容哈希查询历史记录(用于去重,同时匹配成功和生成中的记录,防止并发重复写入) -->
<select id="findByContentHash" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM puzzle_generation_record
WHERE template_id = #{templateId}
AND content_hash = #{contentHash}
AND scenic_id = #{scenicId}
AND status = 1
ORDER BY create_time DESC
AND (status = 1 OR (status = 0 AND create_time > DATE_SUB(NOW(), INTERVAL 5 MINUTE)))
ORDER BY status DESC, create_time DESC
LIMIT 1
</select>

View File

@@ -595,4 +595,15 @@
GROUP BY FLOOR(UNIX_TIMESTAMP(create_time) / 300)
ORDER BY time
</select>
<select id="getMemberSourceByMemberAndSourceId" resultType="com.ycwl.basic.model.pc.source.entity.MemberSourceEntity">
SELECT * FROM member_source WHERE member_id = #{memberId} AND source_id = #{sourceId} AND deleted = 0 LIMIT 1
</select>
<update id="updateRelationBySourceId">
update member_source
<set>
<if test="isBuy!=null">is_buy = #{isBuy}, </if>
<if test="orderId!=null">order_id = #{orderId}, </if>
</set>
where member_id = #{memberId} and source_id = #{sourceId}
</update>
</mapper>

View File

@@ -3,8 +3,11 @@ package com.ycwl.basic.pricing.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.pricing.dto.CouponClaimRequest;
import com.ycwl.basic.pricing.dto.CouponClaimResult;
import com.ycwl.basic.pricing.dto.CouponUseRequest;
import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord;
import com.ycwl.basic.pricing.entity.PriceCouponConfig;
import com.ycwl.basic.pricing.enums.CouponStatus;
import com.ycwl.basic.pricing.exception.CouponInvalidException;
import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper;
import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper;
import com.ycwl.basic.pricing.service.impl.CouponServiceImpl;
@@ -96,6 +99,61 @@ class CouponServiceImplTest {
assertEquals(CouponClaimResult.ERROR_COUPON_OUT_OF_STOCK, result.getErrorCode());
}
@Test
void shouldFailToUseCouponWhenClaimRecordExpiredByClaimWindow() {
PriceCouponConfig coupon = baseCoupon();
when(couponConfigMapper.selectById(1L)).thenReturn(coupon);
PriceCouponClaimRecord expiredRecord = new PriceCouponClaimRecord();
expiredRecord.setId(99L);
expiredRecord.setStatus(CouponStatus.CLAIMED);
expiredRecord.setExpireTime(new Date(System.currentTimeMillis() - 1_000L));
when(couponClaimRecordMapper.selectUserCouponRecords(10L, 1L)).thenReturn(java.util.List.of(expiredRecord));
CouponUseRequest request = buildUseRequest();
CouponInvalidException exception = assertThrows(CouponInvalidException.class, () -> couponService.useCoupon(request));
assertEquals("优惠券已过期", exception.getMessage());
verify(couponConfigMapper, never()).incrementUsedQuantity(anyLong());
}
@Test
void shouldFailToUseCouponWhenCouponGlobalValidityExpired() {
PriceCouponConfig coupon = baseCoupon();
coupon.setValidUntil(new Date(System.currentTimeMillis() - 1_000L));
when(couponConfigMapper.selectById(1L)).thenReturn(coupon);
CouponUseRequest request = buildUseRequest();
CouponInvalidException exception = assertThrows(CouponInvalidException.class, () -> couponService.useCoupon(request));
assertEquals("优惠券已过期", exception.getMessage());
verify(couponConfigMapper, never()).incrementUsedQuantity(anyLong());
}
@Test
void shouldUseCouponWhenClaimRecordAndGlobalWindowAreValid() {
PriceCouponConfig coupon = baseCoupon();
when(couponConfigMapper.selectById(1L)).thenReturn(coupon);
when(couponConfigMapper.incrementUsedQuantity(1L)).thenReturn(1);
PriceCouponClaimRecord validClaimRecord = new PriceCouponClaimRecord();
validClaimRecord.setId(99L);
validClaimRecord.setStatus(CouponStatus.CLAIMED);
validClaimRecord.setExpireTime(new Date(System.currentTimeMillis() + 10_000L));
when(couponClaimRecordMapper.selectUserCouponRecords(10L, 1L)).thenReturn(java.util.List.of(validClaimRecord));
CouponUseRequest request = buildUseRequest();
couponService.useCoupon(request);
verify(couponConfigMapper).incrementUsedQuantity(1L);
verify(couponClaimRecordMapper).updateCouponStatus(
eq(99L),
eq(CouponStatus.USED),
any(Date.class),
eq("ORDER-1"),
eq("SCENIC-1"));
}
private CouponClaimRequest buildRequest() {
CouponClaimRequest request = new CouponClaimRequest();
request.setUserId(10L);
@@ -104,6 +162,15 @@ class CouponServiceImplTest {
return request;
}
private CouponUseRequest buildUseRequest() {
CouponUseRequest request = new CouponUseRequest();
request.setUserId(10L);
request.setCouponId(1L);
request.setOrderId("ORDER-1");
request.setScenicId("SCENIC-1");
return request;
}
private PriceCouponConfig baseCoupon() {
PriceCouponConfig coupon = new PriceCouponConfig();
coupon.setId(1L);

View File

@@ -53,7 +53,7 @@ public class ReusableVoucherServiceTest {
testVoucherCode.setCode("TEST123");
testVoucherCode.setBatchId(1L);
testVoucherCode.setScenicId(1L);
testVoucherCode.setFaceId(1001L);
testVoucherCode.setUserId(1001L);
testVoucherCode.setStatus(VoucherCodeStatus.CLAIMED_AVAILABLE.getCode());
testVoucherCode.setCurrentUseCount(1);
testVoucherCode.setLastUsedTime(new Date());