feat(printer): 添加批量创建虚拟订单功能

- 在PrinterTvController中新增printerService和orderService依赖注入
- 添加getPrinterListByScenicId接口获取景区下启用状态的打印机列表
- 新增createVirtualOrder接口支持批量创建虚拟用户订单
- 新增queryOrder接口用于查询订单支付状态
- 创建TvCreateVirtualOrderRequest请求参数类
- 在PrinterService中实现createBatchVirtualOrder批量创建订单逻辑
- 支持通过faceSampleIds自动查找关联照片素材聚合为一笔订单
- 支持是否需要实际支付的配置选项
- 实现订单价格计算和微信支付集成
This commit is contained in:
2026-02-13 14:44:57 +08:00
parent b2012f9209
commit 959eb6077e
4 changed files with 308 additions and 0 deletions

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

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

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