OrderServiceImpl.java 62 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408
  1. package com.haha.service.impl;
  2. import com.alibaba.fastjson2.JSON;
  3. import com.alibaba.fastjson2.JSONArray;
  4. import com.alibaba.fastjson2.JSONObject;
  5. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  6. import com.baomidou.mybatisplus.core.metadata.IPage;
  7. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  8. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  9. import com.haha.common.constant.OrderConstants;
  10. import com.haha.common.enums.OrderStatus;
  11. import com.haha.common.enums.PayScoreState;
  12. import com.haha.common.enums.PayStatus;
  13. import com.haha.common.enums.PaymentChannel;
  14. import com.haha.common.config.CommonConfig;
  15. import com.haha.common.enums.UserTag;
  16. import com.haha.common.utils.EntityLabelUtils;
  17. import com.haha.common.utils.OrderUtils;
  18. import com.haha.common.vo.OrderItemVO;
  19. import com.haha.common.vo.OrderStatisticsVO;
  20. import com.haha.common.vo.OrderVO;
  21. import com.haha.entity.Device;
  22. import com.haha.entity.Order;
  23. import com.haha.entity.OrderGoods;
  24. import com.haha.entity.Shop;
  25. import com.haha.entity.User;
  26. import com.haha.mapper.DeviceMapper;
  27. import com.haha.mapper.OrderGoodsMapper;
  28. import com.haha.mapper.OrderMapper;
  29. import com.haha.mapper.ShopMapper;
  30. import com.haha.mapper.UserMapper;
  31. import com.haha.service.UserTagService;
  32. import com.haha.service.OrderGoodsService;
  33. import com.haha.service.RefundService;
  34. import com.haha.service.OrderService;
  35. import com.haha.service.InviteActivityService;
  36. import com.haha.service.payment.PaymentService;
  37. import com.haha.service.payment.RefundResult;
  38. import com.haha.service.payment.payscore.PayScoreCreateRequest;
  39. import com.haha.service.payment.payscore.PayScoreResult;
  40. import com.haha.service.payment.payscore.PayScoreService;
  41. import lombok.RequiredArgsConstructor;
  42. import lombok.extern.slf4j.Slf4j;
  43. import org.springframework.beans.factory.annotation.Autowired;
  44. import org.springframework.context.annotation.Lazy;
  45. import org.springframework.scheduling.annotation.Async;
  46. import org.springframework.stereotype.Service;
  47. import org.springframework.transaction.annotation.Transactional;
  48. import java.math.BigDecimal;
  49. import java.math.RoundingMode;
  50. import java.time.LocalDate;
  51. import java.time.LocalDateTime;
  52. import java.time.LocalTime;
  53. import java.util.ArrayList;
  54. import java.util.HashMap;
  55. import java.util.List;
  56. import java.util.Map;
  57. import java.util.Objects;
  58. import java.util.stream.Collectors;
  59. @Slf4j
  60. @Service
  61. @RequiredArgsConstructor
  62. public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
  63. private final DeviceMapper deviceMapper;
  64. private final ShopMapper shopMapper;
  65. private final UserMapper userMapper;
  66. private final CommonConfig commonConfig;
  67. private final UserTagService userTagService;
  68. @Autowired
  69. @Lazy
  70. private PaymentService paymentService;
  71. @Autowired
  72. @Lazy
  73. private PayScoreService payScoreService;
  74. @Autowired
  75. @Lazy
  76. private RefundService refundService;
  77. @Autowired
  78. @Lazy
  79. private OrderGoodsService orderGoodsService;
  80. @Autowired
  81. @Lazy
  82. private InviteActivityService inviteActivityService;
  83. @Autowired(required = false)
  84. private com.haha.sdk.HahaClient hahaClient;
  85. @Autowired
  86. @Lazy
  87. private OrderGoodsMapper orderGoodsMapper;
  88. @Autowired
  89. @Lazy
  90. private com.haha.service.ProductService productService;
  91. @Autowired
  92. @Lazy
  93. private com.haha.service.OrderInventoryService orderInventoryService;
  94. @Override
  95. public IPage<Order> getPage(int page, int pageSize, String orderNo, String deviceId,
  96. String payStatus, Integer status, String startDate, String endDate,
  97. List<Integer> statusList) {
  98. return getPage(page, pageSize, orderNo, deviceId, payStatus, status, startDate, endDate, statusList, null);
  99. }
  100. @Override
  101. public IPage<Order> getPage(int page, int pageSize, String orderNo, String deviceId,
  102. String payStatus, Integer status, String startDate, String endDate,
  103. List<Integer> statusList, String phone) {
  104. // 确保分页参数有效(MyBatis-Plus 要求页码从 1 开始)
  105. if (page < 1) {
  106. page = 1;
  107. }
  108. if (pageSize < 1 || pageSize > 100) {
  109. pageSize = 10; // 默认每页 10 条,最大 100 条
  110. }
  111. log.info("订单分页查询:page={}, pageSize={}, orderNo={}, deviceId={}, phone={}", page, pageSize, orderNo, deviceId, phone);
  112. LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
  113. // 手机号查询:先根据手机号查出用户ID,再按用户ID过滤订单
  114. List<Long> userIdsByPhone = null;
  115. if (phone != null && !phone.isEmpty()) {
  116. LambdaQueryWrapper<User> userWrapper = new LambdaQueryWrapper<>();
  117. userWrapper.like(User::getPhone, phone);
  118. List<User> users = userMapper.selectList(userWrapper);
  119. if (users != null && !users.isEmpty()) {
  120. userIdsByPhone = users.stream().map(User::getId).collect(Collectors.toList());
  121. wrapper.in(Order::getUserId, userIdsByPhone);
  122. } else {
  123. // 没有匹配到用户,返回空结果
  124. return new Page<>(page, pageSize);
  125. }
  126. }
  127. // 构建其他查询条件
  128. wrapper.like(orderNo != null && !orderNo.isEmpty(), Order::getOrderNo, orderNo)
  129. .like(deviceId != null && !deviceId.isEmpty(), Order::getDeviceId, deviceId)
  130. .eq(payStatus != null && !payStatus.isEmpty(), Order::getPayStatus, payStatus)
  131. .eq(status != null, Order::getStatus, status)
  132. .in(statusList != null && !statusList.isEmpty(), Order::getStatus, statusList);
  133. // 时间范围查询
  134. if (startDate != null && !startDate.isEmpty()) {
  135. LocalDateTime startDateTime = LocalDate.parse(startDate).atStartOfDay();
  136. wrapper.ge(Order::getCreateTime, startDateTime);
  137. }
  138. if (endDate != null && !endDate.isEmpty()) {
  139. LocalDateTime endDateTime = LocalDate.parse(endDate).atTime(LocalTime.MAX);
  140. wrapper.le(Order::getCreateTime, endDateTime);
  141. }
  142. // 按创建时间倒序
  143. wrapper.orderByDesc(Order::getCreateTime);
  144. // 分页查询
  145. IPage<Order> orderPage = this.page(new Page<>(page, pageSize), wrapper);
  146. // 批量填充标签字段和关联信息(含用户消费标签)
  147. fillOrderLabels(orderPage.getRecords());
  148. return orderPage;
  149. }
  150. @Override
  151. public Order getDetailById(Long id) {
  152. Order order = this.getById(id);
  153. if (order == null) {
  154. return null;
  155. }
  156. fillOrderLabels(List.of(order));
  157. return order;
  158. }
  159. @Override
  160. public OrderVO getDetailVOById(Long id) {
  161. Order order = this.getById(id);
  162. if (order == null) {
  163. return null;
  164. }
  165. // 填充关联标签信息(门店名称、设备名称等)
  166. fillOrderLabels(List.of(order));
  167. OrderVO vo = new OrderVO();
  168. vo.setId(order.getId());
  169. vo.setOrderNo(order.getOrderNo());
  170. vo.setOrderName(order.getOrderName());
  171. vo.setOutTradeNo(order.getOutTradeNo());
  172. vo.setActivityId(order.getActivityId());
  173. vo.setDeviceId(order.getDeviceId());
  174. vo.setTotalAmount(order.getTotalAmount());
  175. vo.setDiscountAmount(order.getDiscountAmount());
  176. vo.setPaidAmount(order.getPaidAmount());
  177. vo.setPayStatus(order.getPayStatus());
  178. vo.setPayStatusLabel(getPayStatusLabel(order.getPayStatus()));
  179. vo.setStatus(order.getStatus());
  180. vo.setStatusText(getStatusLabel(order.getStatus()));
  181. vo.setCreateTime(order.getCreateTime());
  182. vo.setPayTime(order.getPayTime());
  183. vo.setVideoUrl(order.getVideoUrl());
  184. vo.setConfidence(order.getConfidence());
  185. vo.setShopName(order.getStoreName() != null ? order.getStoreName() : order.getShopName());
  186. vo.setDeviceName(order.getDeviceName());
  187. vo.setPayType(order.getPayType());
  188. List<OrderItemVO> products = orderGoodsService.getVOByOrderId(id);
  189. // 填充每个商品的已退款数量
  190. if (refundService != null && !products.isEmpty()) {
  191. Map<Long, Integer> refundedQtyMap = refundService.getRefundedQuantityMap(id);
  192. for (OrderItemVO p : products) {
  193. p.setRefundedQuantity(refundedQtyMap.getOrDefault(p.getId(), 0));
  194. }
  195. }
  196. vo.setProducts(products);
  197. // 填充用户消费标签
  198. if (order.getUserId() != null) {
  199. Map<Long, UserTag> tagMap = userTagService.batchComputeUserTags(List.of(order.getUserId()));
  200. UserTag tag = tagMap.get(order.getUserId());
  201. if (tag != null) {
  202. vo.setUserTag(tag.getCode());
  203. vo.setUserTagLabel(tag.getDescription());
  204. }
  205. }
  206. return vo;
  207. }
  208. private String getPayStatusLabel(String payStatus) {
  209. if (payStatus == null) {
  210. return "未知";
  211. }
  212. switch (payStatus) {
  213. case OrderConstants.PAY_STATUS_UNPAID:
  214. return "未支付";
  215. case OrderConstants.PAY_STATUS_PAID:
  216. return "已支付";
  217. case OrderConstants.PAY_STATUS_PARTIAL_REFUND:
  218. return "部分退款";
  219. case OrderConstants.PAY_STATUS_REFUND:
  220. return "全额退款";
  221. default:
  222. return "未知";
  223. }
  224. }
  225. private String getStatusLabel(Integer status) {
  226. if (status == null) {
  227. return "未知";
  228. }
  229. switch (status) {
  230. case OrderConstants.STATUS_PENDING_PAYMENT:
  231. return "待支付";
  232. case OrderConstants.STATUS_COMPLETED:
  233. return "已完成";
  234. case OrderConstants.STATUS_CANCELLED:
  235. return "已取消";
  236. case OrderConstants.STATUS_CLOSED:
  237. return "已关闭";
  238. default:
  239. return "未知";
  240. }
  241. }
  242. @Override
  243. public OrderStatisticsVO getStatistics(String startDate, String endDate) {
  244. LocalDateTime startDateTime = null;
  245. LocalDateTime endDateTime = null;
  246. if (startDate != null && !startDate.isEmpty()) {
  247. startDateTime = LocalDate.parse(startDate).atStartOfDay();
  248. }
  249. if (endDate != null && !endDate.isEmpty()) {
  250. endDateTime = LocalDate.parse(endDate).atTime(LocalTime.MAX);
  251. }
  252. // 时间范围内的订单总数
  253. Long totalOrders = baseMapper.countByDateRange(startDateTime, endDateTime);
  254. // 时间范围内的已完成订单数
  255. Long completedOrders = baseMapper.countCompletedByDateRange(startDateTime, endDateTime);
  256. // 时间范围内的已退款订单数
  257. Long refundOrders = baseMapper.countRefundedByDateRange(startDateTime, endDateTime);
  258. // 时间范围内的总销售额(所有已完成订单)
  259. BigDecimal totalAmount = baseMapper.sumCompletedOrderAmount(startDateTime);
  260. if (endDateTime != null) {
  261. totalAmount = baseMapper.selectObjs(
  262. new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Order>()
  263. .eq(Order::getStatus, OrderConstants.STATUS_COMPLETED)
  264. .eq(Order::getPayStatus, OrderConstants.PAY_STATUS_PAID)
  265. .ge(startDateTime != null, Order::getCreateTime, startDateTime)
  266. .le(endDateTime != null, Order::getCreateTime, endDateTime)
  267. .select(Order::getPaidAmount)
  268. ).stream()
  269. .map(obj -> obj instanceof BigDecimal ? (BigDecimal) obj : BigDecimal.ZERO)
  270. .reduce(BigDecimal.ZERO, BigDecimal::add);
  271. }
  272. // 按支付渠道统计金额
  273. List<Map<String, Object>> channelStats = baseMapper.sumAmountByChannel(startDateTime, endDateTime);
  274. BigDecimal wechatAmount = BigDecimal.ZERO;
  275. BigDecimal alipayAmount = BigDecimal.ZERO;
  276. BigDecimal otherAmount = BigDecimal.ZERO;
  277. if (channelStats != null) {
  278. for (Map<String, Object> stat : channelStats) {
  279. String channel = (String) stat.get("channel");
  280. BigDecimal amount = stat.get("amount") instanceof BigDecimal
  281. ? (BigDecimal) stat.get("amount")
  282. : new BigDecimal(stat.get("amount").toString());
  283. if (channel == null) {
  284. otherAmount = otherAmount.add(amount);
  285. continue;
  286. }
  287. String lowerChannel = channel.toLowerCase();
  288. if (lowerChannel.contains(PaymentChannel.WECHAT.getCode())) {
  289. wechatAmount = wechatAmount.add(amount);
  290. } else if (lowerChannel.contains(PaymentChannel.ALIPAY.getCode())) {
  291. alipayAmount = alipayAmount.add(amount);
  292. } else {
  293. otherAmount = otherAmount.add(amount);
  294. }
  295. }
  296. }
  297. return OrderStatisticsVO.builder()
  298. .totalOrders(totalOrders != null ? totalOrders : 0)
  299. .completedOrders(completedOrders != null ? completedOrders : 0)
  300. .refundOrders(refundOrders != null ? refundOrders : 0)
  301. .totalAmount(totalAmount != null ? totalAmount.setScale(2, RoundingMode.HALF_UP).toString() : "0.00")
  302. .wechatAmount(wechatAmount.setScale(2, RoundingMode.HALF_UP).toString())
  303. .alipayAmount(alipayAmount.setScale(2, RoundingMode.HALF_UP).toString())
  304. .otherAmount(otherAmount.setScale(2, RoundingMode.HALF_UP).toString())
  305. .build();
  306. }
  307. @Override
  308. @Transactional(rollbackFor = Exception.class)
  309. public boolean refund(Long orderId, String reason) {
  310. // 委托给 PaymentService 处理退款
  311. // PaymentService.refund() 内部已经处理了:
  312. // - 订单状态校验
  313. // - 调用第三方退款 API
  314. // - 更新退款状态和支付状态
  315. RefundResult result = paymentService.refund(orderId, reason);
  316. return result.isSuccess();
  317. }
  318. @Override
  319. public List<Order> getOrderListByUserId(Long userId, Integer status) {
  320. if (status != null) {
  321. return lambdaQuery()
  322. .eq(Order::getUserId, userId)
  323. .eq(Order::getStatus, status)
  324. .ne(Order::getOrderType, OrderConstants.ORDER_TYPE_IN)
  325. .orderByDesc(Order::getId)
  326. .list();
  327. } else {
  328. return lambdaQuery()
  329. .eq(Order::getUserId, userId)
  330. .ne(Order::getOrderType, OrderConstants.ORDER_TYPE_IN)
  331. .orderByDesc(Order::getId)
  332. .list();
  333. }
  334. }
  335. @Override
  336. public boolean cancelOrder(Long orderId) {
  337. return lambdaUpdate().eq(Order::getId, orderId).set(Order::getStatus, OrderConstants.STATUS_CANCELLED).update();
  338. }
  339. @Override
  340. public boolean updateOrderStatus(Long orderId, Integer status) {
  341. return lambdaUpdate().eq(Order::getId, orderId).set(Order::getStatus, status).update();
  342. }
  343. @Override
  344. public Order getOrderByOutTradeNo(String outTradeNo) {
  345. return lambdaQuery().eq(Order::getOutTradeNo, outTradeNo).one();
  346. }
  347. @Override
  348. public Order getOrderBydeviceId(String deviceId) {
  349. return lambdaQuery().eq(Order::getDeviceId, deviceId).orderByDesc(Order::getCreateTime).one();
  350. }
  351. @Override
  352. public Order getOrderByActivityId(String activityId) {
  353. if (activityId == null || activityId.isEmpty()) {
  354. return null;
  355. }
  356. return lambdaQuery().eq(Order::getActivityId, activityId).one();
  357. }
  358. @Override
  359. public boolean updatePayStatus(String orderNo, String payStatus) {
  360. boolean updated = lambdaUpdate().eq(Order::getOrderNo, orderNo).set(Order::getPayStatus, payStatus).update();
  361. if (updated && (OrderConstants.PAY_STATUS_PAID.equals(payStatus)
  362. || OrderConstants.PAY_STATUS_REFUND.equals(payStatus)
  363. || OrderConstants.PAY_STATUS_PARTIAL_REFUND.equals(payStatus))) {
  364. notifyHahaOrderStatusChanged(orderNo);
  365. }
  366. return updated;
  367. }
  368. @Override
  369. public void notifyHahaOrderStatusChanged(String orderNo) {
  370. if (hahaClient == null || orderNo == null || orderNo.isEmpty()) {
  371. return;
  372. }
  373. try {
  374. // 优先使用哈哈平台的 orderId,本地 orderNo 作为 fallback
  375. Order order = lambdaQuery().eq(Order::getOrderNo, orderNo).one();
  376. String hahaRef = (order != null && order.getHahaOrderId() != null && !order.getHahaOrderId().isEmpty())
  377. ? order.getHahaOrderId() : orderNo;
  378. hahaClient.getOrderApi().setPayStatus(hahaRef);
  379. log.info("订单状态已同步到哈哈平台 - hahaRef: {}, localOrderNo: {}", hahaRef, orderNo);
  380. } catch (Exception e) {
  381. log.error("同步订单状态到哈哈平台失败 - orderNo: {}", orderNo, e);
  382. }
  383. }
  384. /**
  385. * 从AI识别结果创建订单
  386. *
  387. * 业务流程:
  388. * 1. 幂等性检查:防止重复创建订单
  389. * 2. 保存商品识别信息(此时不计算金额,因为AI识别结果不包含价格)
  390. * 3. 解析视频URL等资源信息
  391. * 4. 创建订单记录,状态为待支付,金额为0(等待订单回调时由设备端提供实付金额)
  392. *
  393. * 注意:
  394. * - AI识别结果(sku_list)只包含商品编码(code)和数量(quantity),不包含价格
  395. * - 商品价格在订单回调时由设备端计算并传来
  396. * - 此方法只创建订单框架,实际金额在handleOrderCallback中更新
  397. *
  398. * @param activityId 活动ID(设备开门会话ID)
  399. * @param deviceId 设备ID
  400. * @param userId 用户ID
  401. * @param skuListStr 商品列表JSON字符串,格式:[{"code":"C001","quantity":2}](不包含price)
  402. * @param resourceInfoStr 资源信息JSON字符串,包含视频URL等
  403. * @param confidence AI识别置信度
  404. * @return 创建的订单对象,失败返回null
  405. */
  406. @Override
  407. @Transactional
  408. public Order createOrderFromRecognition(String activityId, String deviceId, String userId,
  409. String skuListStr, String resourceInfoStr, BigDecimal confidence) {
  410. // 【步骤1】幂等性检查:如果该activityId已存在订单,直接返回,避免重复创建
  411. if (activityId != null && !activityId.isEmpty()) {
  412. Order existingOrder = getOrderByActivityId(activityId);
  413. if (existingOrder != null) {
  414. log.info("订单已存在,跳过创建 - activityId: {}, orderId: {}", activityId, existingOrder.getId());
  415. return existingOrder;
  416. }
  417. }
  418. // 【步骤2】解析视频URL等资源信息
  419. String videoUrl = null;
  420. if (resourceInfoStr != null && !resourceInfoStr.isEmpty()) {
  421. JSONObject resourceInfo = JSON.parseObject(resourceInfoStr);
  422. videoUrl = resourceInfo.getString("video_url");
  423. }
  424. // 【步骤3】创建订单记录
  425. // 注意:
  426. // 1. AI识别结果不包含商品价格,只包含商品编码和数量
  427. // 2. 此时订单金额设为0(或NULL),等待设备端回调时更新为实付金额
  428. // 3. 设备端会根据商品编码查询价格,并计算优惠后得到最终实付金额
  429. if (userId != null && deviceId != null) {
  430. Order order = new Order();
  431. order.setOrderNo(OrderUtils.getOrderNo());
  432. order.setActivityId(activityId); // 开门会话ID
  433. order.setUserId(Long.parseLong(userId)); // 用户ID
  434. order.setDeviceId(deviceId); // 设备ID
  435. // 下单时写入门店信息,避免后续设备换门店导致历史订单门店失真
  436. try {
  437. Device device = deviceMapper.selectByDeviceId(deviceId);
  438. if (device != null && device.getShopId() != null) {
  439. order.setShopId(device.getShopId());
  440. Shop shop = shopMapper.selectById(device.getShopId());
  441. if (shop != null) {
  442. order.setStoreName(shop.getName());
  443. order.setShopName(shop.getName());
  444. }
  445. }
  446. } catch (Exception e) {
  447. log.warn("写入订单门店信息失败: deviceId={}", deviceId, e);
  448. }
  449. order.setTotalAmount(BigDecimal.ZERO); // 初始金额为0,等待回调更新
  450. order.setItems(skuListStr); // 商品列表JSON(包含code和quantity)
  451. order.setStatus(OrderConstants.STATUS_PENDING_PAYMENT); // 订单状态:待支付
  452. order.setPayStatus(OrderConstants.PAY_STATUS_UNPAID); // 支付状态:未支付
  453. order.setCreateTime(LocalDateTime.now()); // 创建时间
  454. if (videoUrl != null) {
  455. order.setVideoUrl(videoUrl); // 购物视频URL
  456. }
  457. if (confidence != null) {
  458. order.setConfidence(confidence); // AI识别置信度
  459. }
  460. boolean saved = this.save(order);
  461. if (saved) {
  462. log.info("订单创建成功 - 订单ID: {}, 用户: {}, 等待回调更新金额", order.getId(), userId);
  463. return order;
  464. } else {
  465. log.error("订单创建失败 - 用户: {}, 设备: {}", userId, deviceId);
  466. return null;
  467. }
  468. } else {
  469. log.warn("订单创建跳过 - 缺少必要参数: userId={}, deviceId={}", userId, deviceId);
  470. return null;
  471. }
  472. }
  473. /**
  474. * 创建订单(带支付分预授权信息),支付分字段在 INSERT 时直接写入
  475. */
  476. public Order createOrderFromRecognition(String activityId, String deviceId, String userId,
  477. String skuListStr, String resourceInfoStr, BigDecimal confidence,
  478. String payScoreOrderId, String payScoreState, String serviceId) {
  479. Order order = createOrderFromRecognition(activityId, deviceId, userId, skuListStr, resourceInfoStr, confidence);
  480. if (order != null && payScoreOrderId != null) {
  481. order.setPayScoreOrderId(payScoreOrderId);
  482. order.setPayScoreState(payScoreState != null ? payScoreState : PayScoreState.CREATED.getCode());
  483. order.setServiceId(serviceId);
  484. order.setPayChannel(PaymentChannel.WECHAT_PAYSCORE.getCode());
  485. order.setPayType("微信支付分");
  486. this.updateById(order);
  487. }
  488. return order;
  489. }
  490. /**
  491. * 批量填充订单标签字段(含用户消费标签)
  492. * 优化:使用批量SQL预计算用户标签,避免N+1查询
  493. */
  494. private void fillOrderLabels(List<Order> orders) {
  495. if (orders == null || orders.isEmpty()) {
  496. return;
  497. }
  498. // 1. 批量计算用户消费标签
  499. List<Long> userIds = orders.stream()
  500. .map(Order::getUserId)
  501. .filter(Objects::nonNull)
  502. .distinct()
  503. .collect(Collectors.toList());
  504. Map<Long, UserTag> userTagMap = userTagService.batchComputeUserTags(userIds);
  505. // 2. 批量查询用户信息(手机号)
  506. Map<Long, User> userMap = new HashMap<>();
  507. if (!userIds.isEmpty()) {
  508. List<User> users = userMapper.selectBatchIds(userIds);
  509. if (users != null) {
  510. userMap = users.stream().collect(Collectors.toMap(User::getId, u -> u, (a, b) -> a));
  511. }
  512. }
  513. // 3. 批量查询设备信息,避免循环内逐条查询(N+1)
  514. List<String> deviceIds = orders.stream()
  515. .map(Order::getDeviceId)
  516. .filter(id -> id != null && !id.isEmpty())
  517. .distinct()
  518. .collect(Collectors.toList());
  519. Map<String, Device> deviceMap = new HashMap<>();
  520. if (!deviceIds.isEmpty()) {
  521. List<Device> devices = deviceMapper.selectList(
  522. new LambdaQueryWrapper<Device>().in(Device::getDeviceId, deviceIds));
  523. if (devices != null) {
  524. deviceMap = devices.stream().collect(Collectors.toMap(Device::getDeviceId, d -> d, (a, b) -> a));
  525. }
  526. }
  527. // 4. 批量查询门店信息(用于历史订单未写入 shopId 时回退查询)
  528. Map<Long, Shop> shopMap = new HashMap<>();
  529. List<Long> shopIds = deviceMap.values().stream()
  530. .map(Device::getShopId)
  531. .filter(Objects::nonNull)
  532. .distinct()
  533. .collect(Collectors.toList());
  534. if (!shopIds.isEmpty()) {
  535. List<Shop> shops = shopMapper.selectBatchIds(shopIds);
  536. if (shops != null) {
  537. shopMap = shops.stream().collect(Collectors.toMap(Shop::getId, s -> s, (a, b) -> a));
  538. }
  539. }
  540. // 5. 逐条填充标签(使用预加载的 Map,无额外 SQL)
  541. for (Order order : orders) {
  542. // 用户消费标签 + 手机号
  543. if (order.getUserId() != null) {
  544. UserTag tag = userTagMap.get(order.getUserId());
  545. if (tag != null) {
  546. order.setUserTag(tag.getCode());
  547. order.setUserTagLabel(tag.getDescription());
  548. }
  549. // 填充手机号
  550. User user = userMap.get(order.getUserId());
  551. if (user != null) {
  552. order.setPhone(user.getPhone());
  553. }
  554. }
  555. // 支付状态标签
  556. if (order.getPayStatus() != null) {
  557. var payLabel = EntityLabelUtils.getPayStatusLabel(order.getPayStatus());
  558. order.setPayStatusLabel(payLabel.getLabel());
  559. }
  560. // 订单状态标签
  561. if (order.getStatus() != null) {
  562. var statusLabel = EntityLabelUtils.getStatusLabel("order", order.getStatus());
  563. order.setStatusLabel(statusLabel.getLabel());
  564. }
  565. // 如果 payType 为空但 payChannel 有值,从 payChannel 获取描述
  566. if (order.getPayType() == null || order.getPayType().isEmpty()) {
  567. if (order.getPayChannel() != null && !order.getPayChannel().isEmpty()) {
  568. PaymentChannel channel = PaymentChannel.fromCode(order.getPayChannel());
  569. if (channel != null) {
  570. order.setPayType(channel.getDescription());
  571. }
  572. } else if (order.getOutTradeNo() != null && !order.getOutTradeNo().isEmpty()) {
  573. order.setPayType("微信支付"); // 兼容旧数据
  574. } else if (PayStatus.isPaid(order.getPayStatus())) {
  575. order.setPayType("免费");
  576. }
  577. }
  578. // 补充设备/门店名称(从预加载的Map取值,无额外SQL)
  579. if (order.getDeviceId() != null && !order.getDeviceId().isEmpty()) {
  580. Device device = deviceMap.get(order.getDeviceId());
  581. if (device != null) {
  582. order.setDeviceName(device.getName());
  583. // 历史订单未写入门店信息时,回退到 device→shop
  584. if (order.getShopId() == null && device.getShopId() != null) {
  585. order.setShopId(device.getShopId());
  586. Shop shop = shopMap.get(device.getShopId());
  587. if (shop != null) {
  588. order.setShopName(shop.getName());
  589. order.setStoreName(shop.getName());
  590. }
  591. }
  592. }
  593. }
  594. }
  595. }
  596. @Override
  597. @Transactional
  598. public Order createOrderWithPayScore(String activityId, String deviceId, String userId,
  599. String skuListStr, String resourceInfoStr, BigDecimal confidence,
  600. String openId) {
  601. Order order = createOrderFromRecognition(activityId, deviceId, userId,
  602. skuListStr, resourceInfoStr, confidence);
  603. if (order == null) {
  604. return null;
  605. }
  606. // 立即设置支付渠道,防止后续回调到达时 payChannel 为 null 而跳过扣费
  607. order.setPayChannel(PaymentChannel.WECHAT_PAYSCORE.getCode());
  608. order.setPayType("微信支付分");
  609. this.updateById(order);
  610. if (openId == null || openId.isEmpty()) {
  611. log.warn("[支付分集成] openId为空,跳过支付分服务订单创建 - orderId: {}", order.getId());
  612. return order;
  613. }
  614. try {
  615. PayScoreResult result = payScoreService.createPayScoreOrder(order.getId(), openId);
  616. if (result.isSuccess()) {
  617. log.info("[支付分集成] 支付分服务订单创建成功 - orderId: {}, outOrderNo: {}",
  618. order.getId(), result.getOutOrderNo());
  619. order.setPayScoreOrderId(result.getOutOrderNo());
  620. order.setPayScoreState(result.getState());
  621. this.updateById(order);
  622. } else {
  623. log.warn("[支付分集成] 支付分服务订单创建失败 - orderId: {}, errorCode: {}, errorMsg: {}",
  624. order.getId(), result.getErrorCode(), result.getErrorMsg());
  625. }
  626. } catch (Exception e) {
  627. log.error("[支付分集成] 支付分服务订单创建异常 - orderId: {}", order.getId(), e);
  628. }
  629. return order;
  630. }
  631. /**
  632. * 构建后付费项目列表(post_payments)
  633. * 从 order_goods 表查询已保存的商品,按微信支付分智慧零售规范构建
  634. * name="商品信息", description=商品名, amount=单价(元), count=数量
  635. */
  636. private List<PayScoreCreateRequest.PostPayment> buildPostPayments(Long orderId, BigDecimal totalAmount) {
  637. List<PayScoreCreateRequest.PostPayment> payments = new ArrayList<>();
  638. try {
  639. List<OrderGoods> goodsList = orderGoodsMapper.selectList(
  640. new LambdaQueryWrapper<OrderGoods>()
  641. .eq(OrderGoods::getOrderId, orderId));
  642. if (goodsList == null || goodsList.isEmpty()) {
  643. log.warn("未找到订单商品信息,使用订单金额构建fallback post_payments - orderId: {}", orderId);
  644. Order order = this.getById(orderId);
  645. if (order != null) {
  646. PayScoreCreateRequest.PostPayment payment = new PayScoreCreateRequest.PostPayment();
  647. payment.setName("商品信息");
  648. payment.setAmount(order.getTotalAmount() != null ? order.getTotalAmount() : BigDecimal.ZERO);
  649. payment.setDescription(order.getOrderName() != null ? order.getOrderName() : "商品");
  650. payment.setCount(1);
  651. payments.add(payment);
  652. }
  653. return payments;
  654. }
  655. for (OrderGoods goods : goodsList) {
  656. PayScoreCreateRequest.PostPayment payment = new PayScoreCreateRequest.PostPayment();
  657. payment.setName("商品信息");
  658. // amount 为行总金额(单价 × 数量),微信校验: total_amount = sum(post_payments[].amount)
  659. BigDecimal unitPrice = goods.getPrice() != null ? goods.getPrice()
  660. : (goods.getMoney() != null ? goods.getMoney() : BigDecimal.ZERO);
  661. int quantity = goods.getProductNum() != null ? goods.getProductNum() : 1;
  662. payment.setAmount(unitPrice.multiply(BigDecimal.valueOf(quantity)));
  663. payment.setDescription(goods.getProductName() != null ? goods.getProductName() : "未知商品");
  664. payment.setCount(1);
  665. payments.add(payment);
  666. }
  667. // 修正四舍五入累计误差:确保 sum(post_payments) 严格等于 totalAmount
  668. if (totalAmount != null && !payments.isEmpty()) {
  669. BigDecimal sum = payments.stream()
  670. .map(PayScoreCreateRequest.PostPayment::getAmount)
  671. .reduce(BigDecimal.ZERO, BigDecimal::add);
  672. BigDecimal diff = totalAmount.subtract(sum);
  673. if (diff.compareTo(BigDecimal.ZERO) != 0) {
  674. PayScoreCreateRequest.PostPayment last = payments.get(payments.size() - 1);
  675. last.setAmount(last.getAmount().add(diff));
  676. log.info("post_payments余额平账: 商品sum={}, 订单total={}, 差额={}, 调整末项amount为{}",
  677. sum, totalAmount, diff, last.getAmount());
  678. }
  679. }
  680. log.info("构建post_payments成功 - orderId: {}, 商品数量: {}", orderId, payments.size());
  681. } catch (Exception e) {
  682. log.error("构建post_payments失败 - orderId: {}", orderId, e);
  683. }
  684. return payments;
  685. }
  686. /**
  687. * 使用支付分完结订单(扣费)
  688. *
  689. * 业务流程:
  690. * 1. 验证订单是否存在
  691. * 2. 检查是否创建了支付分服务订单
  692. * 3. 调用微信支付分完结接口进行扣费
  693. * 4. 扣费成功后触发邀请激活逻辑(异步)
  694. *
  695. * 注意:
  696. * - 如果订单未使用支付分,直接更新订单状态
  697. * - 优惠金额已在设备端计算,totalAmount为实付金额
  698. *
  699. * @param orderId 订单ID
  700. * @param totalAmount 订单总金额(设备端已计算优惠后的实付金额)
  701. * @return 是否完结成功
  702. */
  703. @Override
  704. @Transactional
  705. public boolean completeOrderWithPayScore(Long orderId, BigDecimal totalAmount) {
  706. Order order = this.getById(orderId);
  707. if (order == null) {
  708. log.error("[支付分集成] 订单不存在 - orderId: {}", orderId);
  709. return false;
  710. }
  711. if (order.getPayScoreOrderId() == null) {
  712. log.warn("[支付分集成] 订单未创建支付分服务订单,直接更新状态 - orderId: {}", orderId);
  713. order.setTotalAmount(totalAmount);
  714. order.setPayStatus(PayStatus.PAID.getCode());
  715. order.setStatus(OrderStatus.COMPLETED.getCode());
  716. order.setPayTime(LocalDateTime.now());
  717. boolean updateResult = this.updateById(order);
  718. // 支付成功后扣减库存
  719. if (updateResult) {
  720. try {
  721. orderInventoryService.decreaseStockOnPaid(order);
  722. } catch (Exception e) {
  723. log.error("[支付分集成] 扣减库存异常 - orderNo={}", order.getOrderNo(), e);
  724. }
  725. triggerInviteActivationAsync(order.getUserId(), orderId);
  726. }
  727. return updateResult;
  728. }
  729. try {
  730. List<PayScoreCreateRequest.PostPayment> payments = buildPostPayments(orderId, totalAmount);
  731. PayScoreResult result = payScoreService.completePayScoreOrder(orderId, totalAmount, payments);
  732. if (result.isSuccess()) {
  733. log.info("[支付分集成] 支付分完结成功 - orderId: {}, state: {}", orderId, result.getState());
  734. // 订单完成后异步触发邀请激活逻辑
  735. triggerInviteActivationAsync(order.getUserId(), orderId);
  736. return true;
  737. } else {
  738. log.error("[支付分集成] 支付分完结失败 - orderId: {}, errorCode: {}, errorMsg: {}",
  739. orderId, result.getErrorCode(), result.getErrorMsg());
  740. return false;
  741. }
  742. } catch (Exception e) {
  743. log.error("[支付分集成] 支付分完结异常 - orderId: {}", orderId, e);
  744. return false;
  745. }
  746. }
  747. /**
  748. * 异步触发邀请活动激活
  749. * 当被邀请人完成首单后,自动激活邀请关系并发放奖励优惠券
  750. *
  751. * 使用 @Async 异步执行,避免阻塞订单主流程
  752. * 邀请激活失败不影响订单流程的正常运行
  753. *
  754. * @param userId 下单用户ID(被邀请人)
  755. * @param orderId 订单ID
  756. */
  757. @Async("taskExecutor")
  758. public void triggerInviteActivationAsync(Long userId, Long orderId) {
  759. try {
  760. log.info("[邀请激活] 收到订单完成事件,开始处理邀请激活 - orderId: {}, userId: {}", orderId, userId);
  761. // 调用邀请服务激活邀请记录并发放奖励
  762. boolean success = inviteActivityService.activateInviteRecord(userId, orderId);
  763. if (success) {
  764. log.info("[邀请激活] 邀请激活成功 - orderId: {}, userId: {}", orderId, userId);
  765. } else {
  766. log.warn("[邀请激活] 邀请激活失败或无待激活记录(可能该用户未被邀请或已激活)- orderId: {}, userId: {}", orderId, userId);
  767. }
  768. } catch (Exception e) {
  769. // 邀请激活异常不应影响订单流程,仅记录错误日志
  770. // 失败的奖励可以通过定时任务或手动补发来处理
  771. log.error("[邀请激活] 处理订单完成事件时发生异常 - orderId: {}, userId: {}", orderId, userId, e);
  772. }
  773. }
  774. // ==================== 用户端订单业务方法 ====================
  775. @Override
  776. public List<Map<String, Object>> getUserOrderListWithProducts(Long userId, Integer status) {
  777. List<Order> orders = getOrderListByUserId(userId, status);
  778. if (orders.isEmpty()) {
  779. return new ArrayList<>();
  780. }
  781. return buildOrderListBatch(orders);
  782. }
  783. @Override
  784. public List<Map<String, Object>> getUserOrderListWithProducts(Long userId, Map<String, Object> params) {
  785. Integer status = null;
  786. if (params != null && params.containsKey("status")) {
  787. status = Integer.valueOf(params.get("status").toString());
  788. }
  789. return getUserOrderListWithProducts(userId, status);
  790. }
  791. @Override
  792. public Map<String, Object> getUserOrderDetail(Long userId, Long orderId, String orderNo, String outTradeNo) {
  793. Order order = null;
  794. if (orderId != null) {
  795. order = this.getById(orderId);
  796. } else if (orderNo != null && !orderNo.isEmpty()) {
  797. order = this.lambdaQuery()
  798. .eq(Order::getOrderNo, orderNo)
  799. .one();
  800. } else if (outTradeNo != null && !outTradeNo.isEmpty()) {
  801. order = this.getOrderByOutTradeNo(outTradeNo);
  802. }
  803. if (order == null) {
  804. return null;
  805. }
  806. if (!order.getUserId().equals(userId)) {
  807. return null;
  808. }
  809. // IN类型订单不在用户端展示
  810. if (OrderConstants.ORDER_TYPE_IN.equals(order.getOrderType())) {
  811. return null;
  812. }
  813. // 补充设备名称;历史订单未写入门店信息时,回退到 device→shop 查询
  814. if (order.getDeviceId() != null && !order.getDeviceId().isEmpty()) {
  815. try {
  816. Device device = deviceMapper.selectByDeviceId(order.getDeviceId());
  817. if (device != null) {
  818. order.setDeviceName(device.getName());
  819. if (order.getShopId() == null && device.getShopId() != null) {
  820. order.setShopId(device.getShopId());
  821. Shop shop = shopMapper.selectById(device.getShopId());
  822. if (shop != null) {
  823. order.setShopName(shop.getName());
  824. order.setStoreName(shop.getName());
  825. }
  826. }
  827. }
  828. } catch (Exception e) {
  829. log.warn("获取订单关联门店信息失败: orderId={}, deviceId={}", order.getId(), order.getDeviceId(), e);
  830. }
  831. }
  832. Map<String, Object> detail = buildOrderMap(order);
  833. detail.put("statusText", OrderConstants.getStatusDesc(order.getStatus()));
  834. // 加载退款申请记录
  835. try {
  836. if (refundService != null) {
  837. List<com.haha.entity.Refund> refundList = refundService.getByOrderId(order.getId());
  838. if (refundList != null && !refundList.isEmpty()) {
  839. List<Map<String, Object>> refundRecords = new ArrayList<>();
  840. for (com.haha.entity.Refund refund : refundList) {
  841. Map<String, Object> record = new HashMap<>();
  842. record.put("id", refund.getId());
  843. record.put("refundNo", refund.getRefundNo());
  844. record.put("refundAmount", refund.getRefundAmount());
  845. record.put("status", refund.getStatus());
  846. record.put("reason", refund.getReason());
  847. record.put("remark", refund.getRemark());
  848. record.put("createTime", refund.getCreateTime());
  849. record.put("reviewTime", refund.getReviewTime());
  850. // 加载退款商品明细
  851. List<com.haha.entity.RefundItem> items = refundService.getRefundItems(refund.getId());
  852. if (items != null && !items.isEmpty()) {
  853. List<Map<String, Object>> itemMaps = new ArrayList<>();
  854. for (com.haha.entity.RefundItem item : items) {
  855. Map<String, Object> im = new HashMap<>();
  856. im.put("productName", item.getProductName());
  857. im.put("quantity", item.getQuantity());
  858. im.put("price", item.getPrice());
  859. im.put("refundAmount", item.getRefundAmount());
  860. itemMaps.add(im);
  861. }
  862. record.put("items", itemMaps);
  863. }
  864. refundRecords.add(record);
  865. }
  866. detail.put("refunds", refundRecords);
  867. }
  868. }
  869. } catch (Exception e) {
  870. log.warn("加载订单 {} 退款记录失败: {}", order.getId(), e.getMessage());
  871. }
  872. return detail;
  873. }
  874. @Override
  875. public Map<String, Object> getUserOrderDetail(Long userId, Map<String, Object> params) {
  876. Long orderId = null;
  877. String orderNo = null;
  878. String outTradeNo = null;
  879. if (params.containsKey("orderId")) {
  880. orderId = Long.valueOf(params.get("orderId").toString());
  881. } else if (params.containsKey("orderNo")) {
  882. orderNo = params.get("orderNo").toString();
  883. } else if (params.containsKey("outTradeNo")) {
  884. outTradeNo = params.get("outTradeNo").toString();
  885. } else {
  886. return null;
  887. }
  888. return getUserOrderDetail(userId, orderId, orderNo, outTradeNo);
  889. }
  890. @Override
  891. public boolean cancelUserOrder(Long userId, Long orderId) {
  892. Order order = this.getById(orderId);
  893. if (order == null) {
  894. return false;
  895. }
  896. if (!order.getUserId().equals(userId)) {
  897. return false;
  898. }
  899. if (order.getStatus() != OrderConstants.STATUS_PENDING_PAYMENT) {
  900. return false;
  901. }
  902. return cancelOrder(orderId);
  903. }
  904. @Override
  905. public boolean cancelUserOrder(Long userId, Map<String, Object> params) {
  906. if (params == null || !params.containsKey("orderId")) {
  907. return false;
  908. }
  909. Long orderId = Long.valueOf(params.get("orderId").toString());
  910. return cancelUserOrder(userId, orderId);
  911. }
  912. /**
  913. * 批量构建订单列表(一次查询商品和退款数据,避免 N+1)
  914. */
  915. private List<Map<String, Object>> buildOrderListBatch(List<Order> orders) {
  916. List<Long> orderIds = orders.stream().map(Order::getId).collect(Collectors.toList());
  917. // 批量查询所有订单的商品
  918. Map<Long, List<OrderItemVO>> goodsMap = orderGoodsService.getVOByOrderIds(orderIds);
  919. // 批量查询所有订单的退款商品数量
  920. Map<Long, Map<Long, Integer>> refundQtyMap = null;
  921. if (refundService != null) {
  922. refundQtyMap = refundService.getRefundedQuantityMapByOrderIds(orderIds);
  923. }
  924. List<Map<String, Object>> orderList = new ArrayList<>();
  925. for (Order order : orders) {
  926. Map<String, Object> orderMap = buildOrderMapBase(order);
  927. List<OrderItemVO> goodsList = goodsMap.getOrDefault(order.getId(), new ArrayList<>());
  928. Map<Long, Integer> refundQtys = refundQtyMap != null
  929. ? refundQtyMap.getOrDefault(order.getId(), new HashMap<>())
  930. : new HashMap<>();
  931. orderMap.put("products", buildProductList(goodsList, refundQtys));
  932. orderList.add(orderMap);
  933. }
  934. return orderList;
  935. }
  936. /**
  937. * 构建订单Map基础字段(不含 products)
  938. */
  939. private Map<String, Object> buildOrderMapBase(Order order) {
  940. Map<String, Object> orderMap = new HashMap<>();
  941. orderMap.put("id", order.getId());
  942. orderMap.put("orderNo", order.getOrderNo());
  943. orderMap.put("orderName", order.getOrderName() != null ? order.getOrderName() : "");
  944. orderMap.put("outTradeNo", order.getOutTradeNo());
  945. orderMap.put("deviceId", order.getDeviceId());
  946. orderMap.put("storeName", order.getStoreName() != null ? order.getStoreName() : "");
  947. orderMap.put("totalAmount", order.getTotalAmount());
  948. orderMap.put("discountAmount", order.getDiscountAmount() != null ? order.getDiscountAmount() : 0);
  949. orderMap.put("paidAmount", order.getPaidAmount() != null ? order.getPaidAmount() : order.getTotalAmount());
  950. orderMap.put("payStatus", order.getPayStatus());
  951. orderMap.put("payChannel", order.getPayChannel());
  952. orderMap.put("status", order.getStatus());
  953. orderMap.put("createTime", order.getCreateTime());
  954. orderMap.put("payTime", order.getPayTime());
  955. orderMap.put("confidence", order.getConfidence());
  956. orderMap.put("refundStatus", order.getRefundStatus());
  957. orderMap.put("refundAmount", order.getRefundAmount());
  958. orderMap.put("currentRefundId", order.getCurrentRefundId());
  959. orderMap.put("payScoreOrderId", order.getPayScoreOrderId());
  960. return orderMap;
  961. }
  962. /**
  963. * 构建商品列表(使用预查询数据)
  964. */
  965. private List<Map<String, Object>> buildProductList(List<OrderItemVO> goodsList, Map<Long, Integer> refundQtys) {
  966. List<Map<String, Object>> products = new ArrayList<>();
  967. for (OrderItemVO goods : goodsList) {
  968. Map<String, Object> product = new HashMap<>();
  969. product.put("id", goods.getId());
  970. product.put("orderGoodsId", goods.getId());
  971. product.put("productId", goods.getProductId());
  972. product.put("name", goods.getProductName());
  973. product.put("price", goods.getPrice());
  974. product.put("quantity", goods.getProductNum());
  975. product.put("image", normalizeImageUrl(goods.getPic()));
  976. product.put("pic", goods.getPic());
  977. product.put("subtotal", goods.getPrice() != null && goods.getProductNum() != null
  978. ? goods.getPrice().multiply(new BigDecimal(goods.getProductNum()))
  979. : new BigDecimal(0));
  980. product.put("refundedQuantity", refundQtys.getOrDefault(goods.getId(), 0));
  981. products.add(product);
  982. }
  983. return products;
  984. }
  985. /**
  986. * 构建订单Map(含商品信息)
  987. */
  988. private Map<String, Object> buildOrderMap(Order order) {
  989. Map<String, Object> orderMap = new HashMap<>();
  990. orderMap.put("id", order.getId());
  991. orderMap.put("orderNo", order.getOrderNo());
  992. orderMap.put("orderName", order.getOrderName() != null ? order.getOrderName() : "");
  993. orderMap.put("outTradeNo", order.getOutTradeNo());
  994. orderMap.put("deviceId", order.getDeviceId());
  995. orderMap.put("storeName", order.getStoreName() != null ? order.getStoreName() : "");
  996. orderMap.put("totalAmount", order.getTotalAmount());
  997. orderMap.put("discountAmount", order.getDiscountAmount() != null ? order.getDiscountAmount() : 0);
  998. orderMap.put("paidAmount", order.getPaidAmount() != null ? order.getPaidAmount() : order.getTotalAmount());
  999. orderMap.put("payStatus", order.getPayStatus());
  1000. orderMap.put("payChannel", order.getPayChannel());
  1001. orderMap.put("status", order.getStatus());
  1002. orderMap.put("createTime", order.getCreateTime());
  1003. orderMap.put("payTime", order.getPayTime());
  1004. orderMap.put("confidence", order.getConfidence());
  1005. orderMap.put("refundStatus", order.getRefundStatus());
  1006. orderMap.put("refundAmount", order.getRefundAmount());
  1007. orderMap.put("currentRefundId", order.getCurrentRefundId());
  1008. orderMap.put("payScoreOrderId", order.getPayScoreOrderId());
  1009. orderMap.put("products", getProductsFromDatabase(order.getId()));
  1010. return orderMap;
  1011. }
  1012. /**
  1013. * 从数据库查询订单商品信息
  1014. */
  1015. private List<Map<String, Object>> getProductsFromDatabase(Long orderId) {
  1016. List<Map<String, Object>> products = new ArrayList<>();
  1017. if (orderId == null) {
  1018. return products;
  1019. }
  1020. try {
  1021. // 查询已退款数量
  1022. Map<Long, Integer> refundedQuantityMap = null;
  1023. try {
  1024. if (refundService != null) {
  1025. refundedQuantityMap = refundService.getRefundedQuantityMap(orderId);
  1026. }
  1027. } catch (Exception e) {
  1028. log.warn("查询订单 {} 已退款商品数量失败: {}", orderId, e.getMessage());
  1029. }
  1030. List<OrderItemVO> goodsList = orderGoodsService.getVOByOrderId(orderId);
  1031. if (goodsList != null && !goodsList.isEmpty()) {
  1032. for (OrderItemVO goods : goodsList) {
  1033. Map<String, Object> product = new HashMap<>();
  1034. product.put("id", goods.getId());
  1035. product.put("orderGoodsId", goods.getId());
  1036. product.put("productId", goods.getProductId());
  1037. product.put("name", goods.getProductName());
  1038. product.put("price", goods.getPrice());
  1039. product.put("quantity", goods.getProductNum());
  1040. product.put("image", normalizeImageUrl(goods.getPic()));
  1041. product.put("pic", goods.getPic());
  1042. product.put("subtotal", goods.getPrice() != null && goods.getProductNum() != null
  1043. ? goods.getPrice().multiply(new BigDecimal(goods.getProductNum()))
  1044. : new BigDecimal(0));
  1045. int refundedQty = refundedQuantityMap != null
  1046. ? refundedQuantityMap.getOrDefault(goods.getId(), 0) : 0;
  1047. product.put("refundedQuantity", refundedQty);
  1048. products.add(product);
  1049. }
  1050. }
  1051. } catch (Exception e) {
  1052. log.error("查询订单 {} 的商品信息失败:{}", orderId, e.getMessage());
  1053. }
  1054. return products;
  1055. }
  1056. /**
  1057. * 修复缺失商品记录的订单:解析 orders.items (sku_list) 补写 t_order_goods
  1058. * 优先使用订单 paidAmount,其次 totalAmount 作为商品金额基准
  1059. *
  1060. * @param activityId 指定 activityId 修复单个订单,为 null 则批量修复所有缺失商品的订单
  1061. * @return 修复的订单数
  1062. */
  1063. @Transactional(rollbackFor = Exception.class)
  1064. public int repairOrdersMissingGoods(String activityId) {
  1065. LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
  1066. wrapper.isNotNull(Order::getItems).ne(Order::getItems, "").ne(Order::getItems, "[]");
  1067. if (activityId != null && !activityId.isEmpty()) {
  1068. wrapper.eq(Order::getActivityId, activityId);
  1069. }
  1070. List<Order> orders = this.list(wrapper);
  1071. int fixed = 0;
  1072. for (Order order : orders) {
  1073. String items = order.getItems();
  1074. if (items == null || items.isEmpty()) {
  1075. continue;
  1076. }
  1077. try {
  1078. JSONArray skuList = JSON.parseArray(items);
  1079. if (skuList == null || skuList.isEmpty()) {
  1080. continue;
  1081. }
  1082. // 先删除已有商品记录(可能是之前占位写入的零售价数据),以最新金额重写
  1083. orderGoodsMapper.delete(
  1084. new LambdaQueryWrapper<OrderGoods>().eq(OrderGoods::getOrderId, order.getId()));
  1085. // 计算每个商品的单价:用订单实付金额(或订单金额)均摊
  1086. BigDecimal orderAmount = order.getPaidAmount() != null
  1087. && order.getPaidAmount().compareTo(BigDecimal.ZERO) > 0
  1088. ? order.getPaidAmount()
  1089. : order.getTotalAmount() != null ? order.getTotalAmount() : BigDecimal.ZERO;
  1090. // 计算总商品数量,按数量比例分配金额
  1091. int totalQty = 0;
  1092. for (int i = 0; i < skuList.size(); i++) {
  1093. JSONObject sku = skuList.getJSONObject(i);
  1094. Integer number = sku.getInteger("number");
  1095. totalQty += (number != null ? Math.abs(number) : 1);
  1096. }
  1097. BigDecimal unitPrice = totalQty > 0
  1098. ? orderAmount.divide(BigDecimal.valueOf(totalQty), 2, RoundingMode.HALF_UP)
  1099. : orderAmount;
  1100. for (int i = 0; i < skuList.size(); i++) {
  1101. JSONObject sku = skuList.getJSONObject(i);
  1102. String code = sku.getString("code");
  1103. Integer number = sku.getInteger("number");
  1104. if (code == null || code.isEmpty()) {
  1105. continue;
  1106. }
  1107. int qty = number != null ? Math.abs(number) : 1;
  1108. com.haha.entity.Product product = productService.getProductByCode(code);
  1109. OrderGoods goods = new OrderGoods();
  1110. goods.setOrderId(order.getId());
  1111. goods.setOrderNo(order.getOrderNo());
  1112. goods.setActivityId(order.getActivityId());
  1113. goods.setDeviceId(order.getDeviceId());
  1114. goods.setUserId(order.getUserId());
  1115. goods.setCode(code);
  1116. goods.setProductNum(qty);
  1117. goods.setCreateTime(LocalDateTime.now());
  1118. goods.setPrice(unitPrice);
  1119. goods.setMoney(unitPrice.multiply(BigDecimal.valueOf(qty)));
  1120. if (product != null) {
  1121. goods.setProductId(product.getId());
  1122. goods.setBarCode(product.getBarcode());
  1123. goods.setProductName(product.getName());
  1124. goods.setPic(product.getPic());
  1125. }
  1126. orderGoodsMapper.insert(goods);
  1127. }
  1128. fixed++;
  1129. log.info("补写订单商品成功 - orderId: {}, activityId: {}, 金额基准: {}",
  1130. order.getId(), order.getActivityId(), orderAmount);
  1131. } catch (Exception e) {
  1132. log.error("补写订单商品失败 - orderId: {}", order.getId(), e);
  1133. }
  1134. }
  1135. return fixed;
  1136. }
  1137. /**
  1138. * 批量修复已支付订单的支付状态同步到哈哈平台
  1139. * 匹配规则:优先 hahaOrderId,其次 16 位纯数字 orderNo(曾被哈哈覆盖过的历史数据),否则跳过
  1140. *
  1141. * @return { synced: 成功数, skipped: 跳过数, failed: 失败数 }
  1142. */
  1143. @Transactional(rollbackFor = Exception.class)
  1144. public Map<String, Object> repairPayStatusSync() {
  1145. List<Order> orders = lambdaQuery()
  1146. .eq(Order::getPayStatus, OrderConstants.PAY_STATUS_PAID)
  1147. .list();
  1148. int synced = 0, skipped = 0, failed = 0;
  1149. for (Order order : orders) {
  1150. String hahaRef = order.getHahaOrderId();
  1151. if (hahaRef == null || hahaRef.isEmpty()) {
  1152. // 历史订单可能被哈哈短号覆盖过 orderNo(16位纯数字 = 旧行为)
  1153. if (order.getOrderNo() != null && order.getOrderNo().matches("^\\d{16}$")) {
  1154. hahaRef = order.getOrderNo();
  1155. }
  1156. }
  1157. if (hahaRef == null || hahaRef.isEmpty()) {
  1158. log.info("跳过同步 - orderNo: {}, 无法确定哈哈订单号", order.getOrderNo());
  1159. skipped++;
  1160. continue;
  1161. }
  1162. try {
  1163. hahaClient.getOrderApi().setPayStatus(hahaRef);
  1164. synced++;
  1165. log.info("支付状态同步成功 - hahaRef: {}, localOrderNo: {}", hahaRef, order.getOrderNo());
  1166. } catch (Exception e) {
  1167. failed++;
  1168. log.error("支付状态同步失败 - hahaRef: {}, localOrderNo: {}, error: {}",
  1169. hahaRef, order.getOrderNo(), e.getMessage());
  1170. }
  1171. }
  1172. Map<String, Object> result = new HashMap<>();
  1173. result.put("total", orders.size());
  1174. result.put("synced", synced);
  1175. result.put("skipped", skipped);
  1176. result.put("failed", failed);
  1177. log.info("批量同步支付状态完成 - total: {}, synced: {}, skipped: {}, failed: {}",
  1178. orders.size(), synced, skipped, failed);
  1179. return result;
  1180. }
  1181. /**
  1182. * 修复指定订单的支付分扣费,支持订单ID或订单编号
  1183. *
  1184. * @param orderIdOrNo 订单ID(数字)或订单编号
  1185. * @param payScoreOrderId 微信支付分服务订单号(outOrderNo),为空则使用订单上的值
  1186. * @return { success: true/false, message: "..." }
  1187. */
  1188. @Transactional(rollbackFor = Exception.class)
  1189. public Map<String, Object> repairPayScorePayment(String orderIdOrNo, String payScoreOrderId) {
  1190. Order order;
  1191. if (orderIdOrNo.matches("^\\d{1,19}$")) {
  1192. order = this.getById(Long.parseLong(orderIdOrNo));
  1193. } else {
  1194. order = lambdaQuery().eq(Order::getOrderNo, orderIdOrNo).one();
  1195. }
  1196. if (order == null) {
  1197. return Map.of("success", false, "message", "订单不存在: " + orderIdOrNo);
  1198. }
  1199. // 如果本地丢失 payScoreOrderId,尝试从 Redis 自动恢复
  1200. if ((order.getPayScoreOrderId() == null || order.getPayScoreOrderId().isEmpty())
  1201. && (payScoreOrderId == null || payScoreOrderId.isEmpty())) {
  1202. String recovered = payScoreService.tryRecoverPayScoreOrderId(order.getUserId(), order.getDeviceId());
  1203. if (recovered != null) {
  1204. payScoreOrderId = recovered;
  1205. log.info("[支付分修复] 自动恢复 payScoreOrderId - orderId: {}, recovered: {}", order.getId(), recovered);
  1206. }
  1207. }
  1208. // 如果提供了 outOrderNo(手动或自动恢复),先回填到本地订单
  1209. if (payScoreOrderId != null && !payScoreOrderId.isEmpty()) {
  1210. if (order.getPayScoreOrderId() == null || order.getPayScoreOrderId().isEmpty()) {
  1211. order.setPayScoreOrderId(payScoreOrderId);
  1212. order.setPayScoreState(PayScoreState.DOING.getCode());
  1213. order.setPayChannel(PaymentChannel.WECHAT_PAYSCORE.getCode());
  1214. order.setPayType("微信支付分");
  1215. this.updateById(order);
  1216. log.info("[支付分修复] 回填 payScoreOrderId - orderId: {}, orderNo: {}, payScoreOrderId: {}",
  1217. order.getId(), order.getOrderNo(), payScoreOrderId);
  1218. } else if (!payScoreOrderId.equals(order.getPayScoreOrderId())) {
  1219. return Map.of("success", false, "message",
  1220. "提供的 outOrderNo 与订单上的不一致: " + order.getPayScoreOrderId());
  1221. }
  1222. }
  1223. // 重新加载以获取最新状态
  1224. order = this.getById(order.getId());
  1225. if (order.getPayScoreOrderId() == null || order.getPayScoreOrderId().isEmpty()) {
  1226. return Map.of("success", false, "message",
  1227. "未找到支付分商户单号,请从微信商户后台查找后手动输入");
  1228. }
  1229. if (PayScoreState.isFinished(order.getPayScoreState())) {
  1230. return Map.of("success", false, "message", "支付分订单已完结,无需修复: " + order.getPayScoreState());
  1231. }
  1232. if (!OrderConstants.PAY_STATUS_UNPAID.equals(order.getPayStatus())) {
  1233. return Map.of("success", false, "message", "订单已支付,无需修复");
  1234. }
  1235. if (order.getTotalAmount() == null || order.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) {
  1236. return Map.of("success", false, "message", "订单金额为0,无法扣费");
  1237. }
  1238. BigDecimal amount = order.getPaidAmount() != null
  1239. && order.getPaidAmount().compareTo(BigDecimal.ZERO) > 0
  1240. ? order.getPaidAmount() : order.getTotalAmount();
  1241. boolean result = completeOrderWithPayScore(order.getId(), amount);
  1242. log.info("[支付分修复] orderId: {}, orderNo: {}, payScoreOrderId: {}, amount: {}, result: {}",
  1243. order.getId(), order.getOrderNo(), order.getPayScoreOrderId(), amount, result);
  1244. return result
  1245. ? Map.of("success", true, "message", "扣费成功")
  1246. : Map.of("success", false, "message", "扣费失败,请查看服务端日志");
  1247. }
  1248. /**
  1249. * 标准化图片URL
  1250. */
  1251. private String normalizeImageUrl(String picUrl) {
  1252. if (picUrl == null || picUrl.isEmpty()) {
  1253. return picUrl;
  1254. }
  1255. if (picUrl.startsWith("http://") || picUrl.startsWith("https://")) {
  1256. return picUrl;
  1257. }
  1258. return commonConfig.getImageDomainPrefix() + picUrl;
  1259. }
  1260. }