TOrderServiceImpl.java 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642
  1. package com.ylx.massage.service.impl;
  2. import cn.hutool.core.collection.CollUtil;
  3. import cn.hutool.core.collection.CollectionUtil;
  4. import cn.hutool.core.util.ObjectUtil;
  5. import cn.hutool.json.JSONUtil;
  6. import com.alibaba.fastjson.JSONArray;
  7. import com.alibaba.fastjson.JSONObject;
  8. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  9. import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
  10. import com.baomidou.mybatisplus.core.toolkit.StringUtils;
  11. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  12. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  13. import com.ylx.common.config.WechatAccountConfig;
  14. import com.ylx.common.constant.MassageConstants;
  15. import com.ylx.common.core.domain.R;
  16. import com.ylx.common.exception.ServiceException;
  17. import com.ylx.common.utils.SecurityUtils;
  18. import com.ylx.lottery.domain.LotteryCountLog;
  19. import com.ylx.lottery.domain.vo.LocalActivityTableVO;
  20. import com.ylx.lottery.domain.vo.LotteryActivityRulesProductVO;
  21. import com.ylx.lottery.domain.vo.LotteryActivityVO;
  22. import com.ylx.lottery.domain.vo.LotteryStatVO;
  23. import com.ylx.lottery.service.LotteryCountLogService;
  24. import com.ylx.lottery.service.LotteryCountService;
  25. import com.ylx.massage.domain.*;
  26. import com.ylx.massage.domain.vo.*;
  27. import com.ylx.massage.enums.BillTypeEnum;
  28. import com.ylx.massage.enums.JsStatusEnum;
  29. import com.ylx.massage.enums.OrderStatusEnum;
  30. import com.ylx.massage.mapper.TOrderMapper;
  31. import com.ylx.massage.service.*;
  32. import com.ylx.massage.utils.*;
  33. import com.ylx.point.service.IPointUserActivityTaskCompletionService;
  34. import com.ylx.usercenter.domain.dto.UnifiedUserCenterDTO;
  35. import com.ylx.usercenter.service.UnifiedUserCenterService;
  36. import lombok.extern.slf4j.Slf4j;
  37. import org.apache.commons.compress.utils.Lists;
  38. import org.springframework.stereotype.Service;
  39. import org.springframework.transaction.annotation.Transactional;
  40. import org.springframework.transaction.support.TransactionSynchronization;
  41. import org.springframework.transaction.support.TransactionSynchronizationManager;
  42. import javax.annotation.Resource;
  43. import java.math.BigDecimal;
  44. import java.math.RoundingMode;
  45. import java.time.Duration;
  46. import java.time.LocalDate;
  47. import java.time.LocalDateTime;
  48. import java.time.LocalTime;
  49. import java.util.*;
  50. import java.util.stream.Collectors;
  51. /**
  52. * 订单表 服务实现类
  53. */
  54. @Service
  55. @Slf4j
  56. public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> implements TOrderService {
  57. @Resource
  58. private TOrderMapper orderMapper;
  59. @Resource
  60. private WechatAccountConfig wxPayProperties;
  61. @Resource
  62. private LocationUtil locationUtil;
  63. @Resource
  64. private TWxUserService wxUserService;
  65. @Resource
  66. private TRechargeService rechargeService;
  67. @Resource
  68. private TXiangmuService xiangmuService;
  69. @Resource
  70. private OrderNumberGenerator generator;
  71. @Resource
  72. private TJsService jsService;
  73. @Resource
  74. private TAddressService addressService;
  75. @Resource
  76. private TConsumptionLogService consumptionLogService;
  77. @Resource
  78. private MassageUtil massageUtil;
  79. @Resource
  80. private CouponReceiveService couponReceiveService;
  81. @Resource
  82. private CouponService couponService;
  83. @Resource
  84. private WeChatUtil weChatUtil;
  85. @Resource
  86. private RefundVoucherService refundVoucherService;
  87. @Resource
  88. private OrderValidationService orderValidationService;
  89. @Resource
  90. private OrderNotificationService orderNotificationService;
  91. @Resource
  92. private IPointUserActivityTaskCompletionService pointUserActivityTaskCompletionService;
  93. @Resource
  94. private OrderAllocationLogService allocationLogService;
  95. @Resource
  96. private CancelOrderApplicationService cancelOrderApplicationService;
  97. @Resource
  98. private UnifiedUserCenterService unifiedUserCenterService;
  99. @Resource
  100. private LotteryCountService lotteryCountService;
  101. @Resource
  102. private LotteryCountLogService lotteryCountLogService;
  103. /**
  104. * 判断是否免车费
  105. * 时间段判断:
  106. * - 白天时段(7:30-20:00):距离 ≤ 技师白天免车费里程 → 免费
  107. * - 夜间时段(20:00-7:30):距离 ≤ 技师夜间免车费里程 → 免费
  108. *
  109. * @param js
  110. * @param distance
  111. * @return Boolean
  112. */
  113. public Boolean isFree(TJs js, BigDecimal distance) {
  114. Date date = new Date();
  115. //白天免车费(07.30-20.00)
  116. long current = Long.parseLong(DateTimeUtils.numTime(date));
  117. if (current >= MassageConstants.START_FREE && current <= MassageConstants.END_FREE) {
  118. if (js.getDaytimeMileage().compareTo(distance) >= 0) {
  119. //免车费
  120. return Boolean.TRUE;
  121. } else {
  122. return Boolean.FALSE;
  123. }
  124. } else {
  125. //夜间免车费(20.00-07.30)
  126. if (js.getNigthMileage().compareTo(distance) >= 0) {
  127. //免车费
  128. return Boolean.TRUE;
  129. } else {
  130. return Boolean.FALSE;
  131. }
  132. }
  133. }
  134. /**
  135. * 添加订单
  136. *
  137. * @param order
  138. * @return TOrder
  139. */
  140. @Override
  141. @Transactional(rollbackFor = Exception.class)
  142. public TOrder addOrder(TOrder order) {
  143. String jsId = order.getcJsId();
  144. // 1. 基础参数校验
  145. if (StringUtils.isBlank(jsId)) {
  146. throw new ServiceException("请选择技师");
  147. }
  148. if (order.getcGoods().isEmpty()) {
  149. throw new ServiceException("请选择项目");
  150. }
  151. TJs js = jsService.getById(jsId);
  152. if (js == null) {
  153. throw new ServiceException("技师不存在");
  154. }
  155. Integer techType = js.getTechType();
  156. // 虚拟技师
  157. if (techType.equals(1)) {
  158. //虚拟技师订单
  159. order.setVirtualOrderFlag(1);
  160. //虚拟技师订单未分配
  161. order.setVirtualOrderAllocation(1);
  162. } else {
  163. //真实订单
  164. order.setVirtualOrderFlag(0);
  165. order.setVirtualOrderAllocation(0);
  166. }
  167. // 2. 【新增】订单状态锁校验 - 检查技师是否可以接单
  168. // 确保技师没有进行中的订单,保证服务时间互斥
  169. log.info("开始校验技师 {} 是否可以接单,订单号:{}", order.getcJsId(), order.getOrderNo());
  170. orderValidationService.canAcceptOrder(order.getcJsId(), order);
  171. log.info("技师 {} 接单校验通过,继续创建订单", order.getcJsId());
  172. //优惠卷减免
  173. // List<CouponReceiveVo> coupons = couponReceiveService.getByOpenId(order.getcOpenId());
  174. // BigDecimal preferential = this.setCoupon(coupons);
  175. // order.setPreferential(preferential);
  176. // 生成订单号
  177. order.setOrderNo(generator.generateNextOrderNumber(OrderNumberGenerator.KEY_PREFIX_ORDER));
  178. //订单价格
  179. List<TXiangmu> list = JSONObject.parseArray(order.getcGoods().toJSONString(), TXiangmu.class);
  180. BigDecimal sum = list.stream().map(TXiangmu::getSum).reduce(BigDecimal.ZERO, BigDecimal::add);
  181. //订单总金额
  182. order.setdTotalMoney(sum);
  183. //获取用户默认地址
  184. TAddress address = addressService.getByOpenId(order.getcOpenId());
  185. if (address == null) {
  186. throw new ServiceException("请先添加地址");
  187. }
  188. //添加技师位置信息
  189. locationUtil.geoAdd(LocationUtil.GEO_KEY_USER, js.getcOpenId() + order.getOrderNo(), Double.parseDouble(js.getLongitude().toString()), Double.parseDouble(js.getLatitude().toString()));
  190. //添加用户位置信息
  191. locationUtil.geoAdd(LocationUtil.GEO_KEY_USER, order.getcOpenId() + order.getOrderNo(), Double.parseDouble(address.getLongitude().toString()), Double.parseDouble(address.getLatitude().toString()));
  192. //计算距离
  193. double distance = locationUtil.getDistance(js.getcOpenId() + order.getOrderNo(), order.getcOpenId() + order.getOrderNo());
  194. log.info("技师与用户之间的距离:{}", distance);
  195. locationUtil.remove(LocationUtil.GEO_KEY_USER, js.getcOpenId() + order.getOrderNo(), order.getcOpenId() + order.getOrderNo());
  196. order.setDistance(new BigDecimal(distance));
  197. //计算车费
  198. if (order.getDistance() != null && order.getDistance().compareTo(BigDecimal.ZERO) > 0 && StringUtils.isBlank(order.getParentNo())) {
  199. //判断是否可以免车费
  200. if (!this.isFree(js, order.getDistance())) {
  201. BigDecimal bigDecimal = massageUtil.calculateTaxiFare(order.getDistance(), js.getDeptId());
  202. order.setFare(bigDecimal.setScale(MassageConstants.INTEGER_TWO, RoundingMode.HALF_UP));
  203. }
  204. }
  205. //总价 = 订单 + 车费
  206. order.setTotalPrice(sum.add(Optional.ofNullable(order.getFare()).orElse(BigDecimal.ZERO)));
  207. if (order.getParentNo() != null && order.getOrderType() == 2) {
  208. //升级订单 补差价
  209. TOrder partOrder = this.getByNo(order.getParentNo());
  210. order.setPriceDifference(order.getTotalPrice().subtract(partOrder.getTotalPrice()));
  211. }
  212. order.setAddress(address.getAddress());
  213. order.setName(address.getName());
  214. order.setLatitude(address.getLatitude());
  215. order.setLongitude(address.getLongitude());
  216. order.setcPhone(address.getPhone());
  217. //设置用户姓名
  218. order.setcName(address.getUserName());
  219. order.setAtlasAdd(address.getAtlasAdd());
  220. order.setDeptId(js.getDeptId());
  221. order.setDeptName(js.getCity());
  222. //设置订单状态:待支付
  223. order.setnStatus(OrderStatusEnum.WAIT_PAY.getCode());
  224. order.setDtCreateTime(LocalDateTime.now());
  225. Date date = DateTimeUtils.addMinute(new Date(), 10);
  226. order.setcTime(DateTimeUtils.formatDate(date, "yyyy-MM-dd HH:mm:ss"));
  227. save(order);
  228. return order;
  229. }
  230. private BigDecimal setCoupon(List<CouponReceiveVo> coupons) {
  231. //过滤过期的优惠券
  232. coupons = coupons.stream().filter(coupon -> coupon.getExpirationTime().after(new Date())).collect(Collectors.toList());
  233. //无门槛优惠券
  234. //List<CouponReceiveVo> collect = coupons.stream().filter(coupon -> coupon.getDiscountType().equals(DiscountTypeEnum.NO_THRESHOLD.getCode())).collect(Collectors.toList());
  235. //支付成功 后 删除优惠卷
  236. // couponReceiveService.removeCoupons(collect);
  237. //计算优惠金额
  238. //return collect.stream().map(CouponReceiveVo::getDiscountValue).reduce(BigDecimal.ZERO, BigDecimal::add);
  239. return BigDecimal.ZERO;
  240. }
  241. @Override
  242. public void payNotifyOrder(String outTradeNo) {
  243. //查询未支付的订单
  244. LambdaQueryWrapper<TOrder> queryWrapper = new LambdaQueryWrapper<>();
  245. queryWrapper.eq(TOrder::getOrderNo, outTradeNo).eq(TOrder::getnStatus, OrderStatusEnum.WAIT_PAY.getCode());
  246. TOrder orderNew = this.getOne(queryWrapper);
  247. if (orderNew == null) {
  248. log.error("订单 {} 未支付状态不存在", outTradeNo);
  249. return;
  250. }
  251. // 设置微信支付
  252. orderNew.setPayType(1);
  253. TWxUser user = wxUserService.getByOpenId(orderNew.getcOpenId());
  254. orderPayManage(user, orderNew);
  255. }
  256. @Override
  257. public Object updateAddressById(TOrder borrow) {
  258. TOrder order = this.getById(borrow.getcId());
  259. if (borrow.getLatitude() != null && borrow.getLatitude() != 0 && borrow.getLongitude() != null && borrow.getLongitude() != 0) {
  260. order.setAtlasAdd(borrow.getAtlasAdd());
  261. order.setcName(borrow.getcName());
  262. order.setcPhone(borrow.getcPhone());
  263. order.setName(borrow.getName());
  264. order.setAddress(borrow.getAddress());
  265. order.setLatitude(borrow.getLatitude());
  266. order.setLongitude(borrow.getLongitude());
  267. TJs js = jsService.getById(order.getcJsId());
  268. //添加技师位置信息
  269. locationUtil.geoAdd(LocationUtil.GEO_KEY_USER, js.getcOpenId() + order.getOrderNo(), Double.parseDouble(js.getLongitude().toString()), Double.parseDouble(js.getLatitude().toString()));
  270. //添加用户位置信息
  271. locationUtil.geoAdd(LocationUtil.GEO_KEY_USER, order.getcOpenId() + order.getOrderNo(), Double.parseDouble(borrow.getLongitude().toString()), Double.parseDouble(borrow.getLatitude().toString()));
  272. double distance = locationUtil.getDistance(js.getcOpenId() + order.getOrderNo(), order.getcOpenId() + order.getOrderNo());
  273. locationUtil.remove(LocationUtil.GEO_KEY_USER, js.getcOpenId() + order.getOrderNo(), order.getcOpenId() + order.getOrderNo());
  274. order.setDistance(new BigDecimal(distance));
  275. //计算车费
  276. if (order.getDistance() != null && order.getDistance().compareTo(BigDecimal.ZERO) > 0) {
  277. //判断是否可以免车费
  278. if (!this.isFree(js, order.getDistance())) {
  279. BigDecimal bigDecimal = massageUtil.calculateTaxiFare(order.getDistance(), order.getDeptId());
  280. order.setFare(bigDecimal.setScale(MassageConstants.INTEGER_TWO, RoundingMode.HALF_UP));
  281. }
  282. }
  283. order.setTotalPrice(order.getdTotalMoney().add(Optional.ofNullable(order.getFare()).orElse(BigDecimal.ZERO)));
  284. this.updateById(order);
  285. }
  286. return order;
  287. }
  288. @Override
  289. public Object depart(TOrder order) {
  290. LambdaQueryWrapper<TOrder> wrapper = new LambdaQueryWrapper<>();
  291. wrapper.eq(TOrder::getcId, order.getcId()).eq(TOrder::getnStatus, OrderStatusEnum.RECEIVED_ORDER.getCode());
  292. //设置订单状态:已出发
  293. order.setnStatus(OrderStatusEnum.DEPART.getCode());
  294. order.setDepartTime(new Date());
  295. order.setDepartLongitude(Optional.ofNullable(order.getDepartLongitude()).orElse(BigDecimal.ZERO));
  296. order.setDepartLatitude(Optional.ofNullable(order.getDepartLatitude()).orElse(BigDecimal.ZERO));
  297. return this.update(order, wrapper);
  298. }
  299. @Override
  300. public Integer getOrderNum(String jsid, Date startDate, Date endDate) {
  301. return orderMapper.getOrderNum(jsid, startDate, endDate);
  302. }
  303. @Override
  304. public Integer getAddNum(String jsid, Date startDate, Date endDate) {
  305. return orderMapper.getAddNum(jsid, startDate, endDate);
  306. }
  307. @Override
  308. public Integer getUpgradeNum(String jsid, Date startDate, Date endDate) {
  309. return orderMapper.getUpgradeNum(jsid, startDate, endDate);
  310. }
  311. @Override
  312. public BigDecimal getTurnover(String jsid, Date startDate, Date endDate) {
  313. return orderMapper.getTurnover(jsid, startDate, endDate);
  314. }
  315. @Override
  316. @Transactional(rollbackFor = Exception.class)
  317. public TOrder transferOrder(TOrder order) {
  318. // ========== 第1步:参数校验 ==========
  319. if (StringUtils.isBlank(order.getcId())) {
  320. throw new ServiceException("订单id不能为空");
  321. }
  322. if (StringUtils.isBlank(order.getcJsId())) {
  323. throw new ServiceException("转单技师ID不能为空");
  324. }
  325. // 定义操作结果(默认为失败)
  326. Integer operationResult = 1; // 1-失败
  327. // 定义操作记录所需的变量
  328. String orderId = null;
  329. String orderNo = null;
  330. String oldTechnicianId = null;
  331. String oldTechnicianName = null;
  332. Integer oldTechnicianStatusBefore = null;
  333. Integer oldTechnicianStatusAfter = null;
  334. String newTechnicianId = null;
  335. String newTechnicianName = null;
  336. Integer newTechnicianStatusBefore = null;
  337. Integer newTechnicianStatusAfter = null;
  338. Integer orderStatusBefore = null;
  339. Integer orderStatusAfter = null;
  340. String operatorId = null;
  341. String operatorName = null;
  342. String operationReason = "虚拟订单分配";
  343. try {
  344. // ========== 第2步:查询原订单信息 ==========
  345. TOrder oldOrder = this.getById(order.getcId());
  346. if (oldOrder == null) {
  347. throw new ServiceException("订单不存在");
  348. }
  349. //原技师ID
  350. oldTechnicianId = oldOrder.getcJsId();
  351. //新技师ID
  352. newTechnicianId = order.getcJsId();
  353. // 记录订单操作前状态
  354. orderStatusBefore = oldOrder.getnStatus();
  355. orderId = oldOrder.getcId();
  356. orderNo = oldOrder.getOrderNo();
  357. log.info("开始转单操作 - 订单号:{}, 原技师ID:{}, 新技师ID:{}, 订单状态:{}", oldOrder.getOrderNo(), oldTechnicianId, newTechnicianId, orderStatusBefore);
  358. // ========== 第3步:查询原技师信息 ==========
  359. TJs oldTechnician = jsService.getById(oldTechnicianId);
  360. if (oldTechnician == null) {
  361. throw new ServiceException("原技师不存在");
  362. }
  363. oldTechnicianName = oldTechnician.getcName();
  364. oldTechnicianStatusBefore = oldTechnician.getnStatus();
  365. // ========== 第4步:查询新技师信息 ==========
  366. TJs newTechnician = jsService.getById(newTechnicianId);
  367. if (newTechnician == null) {
  368. throw new ServiceException("新技师不存在");
  369. }
  370. newTechnicianName = newTechnician.getcName();
  371. newTechnicianStatusBefore = newTechnician.getnStatus();
  372. // ========== 第5步:更新订单技师信息 ==========
  373. oldOrder.setOldJsId(oldTechnicianId); // 保存原技师ID
  374. oldOrder.setcJsId(newTechnicianId); // 更新为新技师ID
  375. log.info("更新订单技师 - 订单号:{}, 原技师:[ID:{}, 姓名:{}], 新技师:[ID:{}, 姓名:{}]", oldOrder.getOrderNo(), oldTechnicianId, oldTechnicianName, newTechnicianId, newTechnicianName);
  376. if (!this.updateById(oldOrder)) {
  377. throw new ServiceException("转单失败:更新订单技师信息失败");
  378. }
  379. // 记录订单操作后状态(转单后状态通常保持不变)
  380. orderStatusAfter = oldOrder.getnStatus();
  381. // ========== 第6步:更新新技师状态(可服务 → 服务中)==========
  382. TJs newJsUpdate = new TJs();
  383. newJsUpdate.setId(newTechnicianId);
  384. newJsUpdate.setnStatus(JsStatusEnum.JS_SERVICE.getCode());
  385. if (!jsService.updateById(newJsUpdate)) {
  386. throw new ServiceException("转单失败:更新新技师状态失败");
  387. }
  388. newTechnicianStatusAfter = JsStatusEnum.JS_SERVICE.getCode();
  389. // ========== 第7步:更新原技师状态(服务中 → 可服务)==========
  390. TJs oldJsUpdate = new TJs();
  391. oldJsUpdate.setId(oldTechnicianId);
  392. oldJsUpdate.setnStatus(JsStatusEnum.JS_SERVICEABLE.getCode());
  393. if (!jsService.updateById(oldJsUpdate)) {
  394. throw new ServiceException("转单失败:更新原技师状态失败");
  395. }
  396. oldTechnicianStatusAfter = JsStatusEnum.JS_SERVICEABLE.getCode();
  397. log.info("更新技师状态完成 - 新技师:{} {}→{}, 原技师:{} {}→{}", newTechnicianName, getStatusName(newTechnicianStatusBefore), getStatusName(newTechnicianStatusAfter), oldTechnicianName, getStatusName(oldTechnicianStatusBefore), getStatusName(oldTechnicianStatusAfter));
  398. // ========== 第8步:获取操作人信息 ==========
  399. operatorId = SecurityUtils.getUserId() != null ? SecurityUtils.getUserId().toString() : "ADMIN";
  400. operatorName = SecurityUtils.getUsername() != null ? SecurityUtils.getUsername() : "系统管理员";
  401. // ========== 第9步:转单成功,设置操作结果为成功 ==========
  402. operationResult = 0; // 0-成功
  403. log.info("转单操作完成 - 订单号:{}", oldOrder.getOrderNo());
  404. return oldOrder;
  405. } catch (ServiceException e) {
  406. // 业务异常,操作失败
  407. log.error("转单操作失败 - 订单号:{}, 错误信息:{}", orderNo, e.getMessage());
  408. operationResult = 1; // 1-失败
  409. throw e;
  410. } catch (Exception e) {
  411. // 系统异常,操作失败
  412. log.error("转单操作异常 - 订单号:{}, 异常信息:{}", orderNo, e.getMessage(), e);
  413. operationResult = 1; // 1-失败
  414. throw new ServiceException("转单操作异常:" + e.getMessage());
  415. } finally {
  416. // ========== 第10步:记录转单操作日志(无论成功或失败都记录)==========
  417. try {
  418. // 只有在获取到基本信息后才记录日志
  419. if (orderId != null && orderNo != null && oldTechnicianId != null && newTechnicianId != null) {
  420. allocationLogService.recordTransferOrder(orderId, // orderId
  421. orderNo, // orderNo
  422. oldTechnicianId, // oldTechnicianId
  423. oldTechnicianName, // oldTechnicianName
  424. oldTechnicianStatusBefore, // oldTechnicianStatusBefore
  425. oldTechnicianStatusAfter, // oldTechnicianStatusAfter
  426. newTechnicianId, // newTechnicianId
  427. newTechnicianName, // newTechnicianName
  428. newTechnicianStatusBefore, // newTechnicianStatusBefore
  429. newTechnicianStatusAfter, // newTechnicianStatusAfter
  430. orderStatusBefore, // orderStatusBefore
  431. orderStatusAfter, // orderStatusAfter
  432. operatorId, // operatorId
  433. operatorName, // operatorName
  434. operationReason, // operationReason
  435. operationResult // operationResult(0-成功,1-失败)
  436. );
  437. String resultDesc = operationResult == 0 ? "成功" : "失败";
  438. log.info("转单操作记录已保存 - 订单号:{}, 操作结果:{}", orderNo, resultDesc);
  439. }
  440. } catch (Exception e) {
  441. // 记录日志失败不影响转单操作
  442. log.error("记录转单操作日志失败 - 订单号:{}, 错误信息:{}", orderNo, e.getMessage(), e);
  443. }
  444. }
  445. }
  446. /**
  447. * 获取技师状态名称
  448. *
  449. * @param status 状态码
  450. * @return String 状态名称
  451. */
  452. private String getStatusName(Integer status) {
  453. if (status == null) {
  454. return "未知";
  455. }
  456. switch (status) {
  457. case 0:
  458. return "可服务";
  459. case 1:
  460. return "服务中";
  461. case 2:
  462. return "不可服务";
  463. default:
  464. return "未知(" + status + ")";
  465. }
  466. }
  467. @Override
  468. public List<HomeBlock> getBlock(Date start, Date end, String deptId) {
  469. return orderMapper.getBlock(start, end, deptId);
  470. }
  471. @Override
  472. public OrderVerificationVo verification(TOrder order) {
  473. if (StringUtils.isBlank(order.getCouponReceiveId())) {
  474. throw new ServiceException("认领优惠券id为空");
  475. }
  476. if (StringUtils.isBlank(order.getcId())) {
  477. throw new ServiceException("订单id为空");
  478. }
  479. OrderVerificationVo orderVerificationVo = new OrderVerificationVo();
  480. TOrder tOrder = this.getById(order.getcId());
  481. orderVerificationVo.setCouponReceiveId(order.getCouponReceiveId());
  482. CouponReceive couponReceive = couponReceiveService.getById(order.getCouponReceiveId());
  483. Coupon coupon = couponService.getById(couponReceive.getCouponId());
  484. log.info("订单信息,{}", tOrder);
  485. log.info("优惠卷信息,{}", coupon);
  486. //折扣券
  487. if (coupon.getDiscountType() == 2) {
  488. //判断门槛金额
  489. if (tOrder.getTotalPrice().compareTo(coupon.getThresholdAmount()) >= 0) {
  490. //折扣值
  491. BigDecimal divide = coupon.getRebValue().divide(new BigDecimal(10));
  492. //优惠后的金额 = 订单总金额*折扣值
  493. BigDecimal bigDecimal = tOrder.getTotalPrice().multiply(divide).setScale(MassageConstants.INTEGER_TWO, RoundingMode.HALF_UP);
  494. //优惠值
  495. orderVerificationVo.setPreferential(tOrder.getTotalPrice().subtract(bigDecimal));
  496. orderVerificationVo.setTotalPrice(bigDecimal);
  497. } else {
  498. throw new ServiceException("不满足优惠券门槛金额");
  499. }
  500. } else {
  501. if (tOrder.getTotalPrice().compareTo(coupon.getThresholdAmount()) >= 0) {
  502. //优惠值
  503. orderVerificationVo.setPreferential(coupon.getDiscountValue());
  504. orderVerificationVo.setTotalPrice(tOrder.getTotalPrice().subtract(coupon.getDiscountValue()));
  505. } else {
  506. throw new ServiceException("不满足优惠券门槛金额");
  507. }
  508. }
  509. if (orderVerificationVo.getTotalPrice().compareTo(BigDecimal.ZERO) < 0) {
  510. throw new ServiceException("当前项目不可用");
  511. }
  512. return orderVerificationVo;
  513. }
  514. private TOrder getOrder(TOrder tOrder) {
  515. if (updateById(tOrder)) {
  516. return tOrder;
  517. } else {
  518. throw new ServiceException("优惠券核销失败");
  519. }
  520. }
  521. /**
  522. * 支付订单
  523. *
  524. * @param order
  525. * @return R
  526. */
  527. @Override
  528. public R payOrder(TOrder order) throws Exception {
  529. // 根据订单ID查询订单信息
  530. TOrder orderNew = getById(order.getcId());
  531. if (!orderNew.getnStatus().equals(OrderStatusEnum.WAIT_PAY.getCode())) {
  532. throw new ServiceException("该订单已经支付或者超时被取消");
  533. }
  534. TJs js = jsService.getById(orderNew.getcJsId());
  535. if (StringUtils.isBlank(orderNew.getParentNo())) {
  536. if (null == js || js.getnStatus().equals(JsStatusEnum.JS_SERVICE.getCode())) {
  537. throw new ServiceException("该技师已在服务中请重新下单");
  538. }
  539. }
  540. orderNew.setPayType(order.getPayType());
  541. //优惠券核销
  542. if (StringUtils.isNotBlank(order.getCouponReceiveId())) {
  543. orderNew.setCouponReceiveId(order.getCouponReceiveId());
  544. orderNew.setPreferential(order.getPreferential());
  545. orderNew.setTotalPrice(order.getTotalPrice());
  546. if (!updateById(orderNew)) {
  547. throw new ServiceException("支付失败");
  548. }
  549. }
  550. //判断支付方式
  551. if (order.getPayType().equals(MassageConstants.INTEGER_ONE)) {
  552. //微信支付
  553. R resp = rechargeService.getPay(orderNew.getOrderNo(), orderNew.getTotalPrice(), orderNew.getcOpenId(), BillTypeEnum.WX_PAY.getInfo(), BillTypeEnum.WX_PAY.getCode().toString());
  554. //添加待接单消息通知(技师侧)这块的逻辑在回调接口中
  555. //orderNotificationService.sendPendingRemindNotification(orderNew);
  556. return resp;
  557. }
  558. TWxUser user = wxUserService.getByOpenId(orderNew.getcOpenId());
  559. if (null == user) {
  560. throw new ServiceException("用户不存在");
  561. }
  562. //现金支付
  563. if (order.getPayType().equals(MassageConstants.INTEGER_THREE)) {
  564. //现金支付
  565. orderPayManage(user, orderNew);
  566. //添加待接单消息通知(技师侧)
  567. orderNotificationService.sendPendingRemindNotification(orderNew);
  568. return R.ok();
  569. }
  570. if (user.getdBalance().compareTo(orderNew.getTotalPrice()) < MassageConstants.INTEGER_ZERO) {
  571. throw new ServiceException("账户金额不够请充值");
  572. } else {
  573. //余额支付
  574. orderPayManage(user, orderNew);
  575. //添加待接单消息通知(技师侧)
  576. orderNotificationService.sendPendingRemindNotification(orderNew);
  577. return R.ok();
  578. }
  579. }
  580. /**
  581. * 新订单通知
  582. *
  583. * @param order
  584. */
  585. public void newOrderNotification(TOrder order) {
  586. cn.hutool.json.JSONObject param = JSONUtil.createObj();
  587. //订单号
  588. param.set("character_string9", JSONUtil.createObj().set("value", order.getOrderNo()));
  589. //电话
  590. param.set("phone_number14", JSONUtil.createObj().set("value", order.getcPhone()));
  591. param.set("thing18", JSONUtil.createObj().set("value", order.getcName()));
  592. param.set("time6", JSONUtil.createObj().set("value", DateTimeUtils.formatDate(new Date(), DateTimeUtils.DATE_NUMBER_YEAR_MONTH_FORMAT)));
  593. param.set("thing27", JSONUtil.createObj().set("value", order.getName()));
  594. TJs js = jsService.getById(order.getcJsId());
  595. weChatUtil.notification(js.getcOpenId(), wxPayProperties.getTemplateId1(), param);
  596. }
  597. /**
  598. * 订单支付管理
  599. *
  600. * @param user
  601. * @param orderNew
  602. */
  603. @Transactional(rollbackFor = Exception.class)
  604. public void orderPayManage(TWxUser user, TOrder orderNew) {
  605. //更新优惠卷状态
  606. if (StringUtils.isNotBlank(orderNew.getCouponReceiveId())) {
  607. CouponReceive couponReceive = new CouponReceive();
  608. couponReceive.setId(orderNew.getCouponReceiveId());
  609. couponReceive.setCouponStatus(MassageConstants.INTEGER_TWO);
  610. if (!couponReceiveService.updateById(couponReceive)) {
  611. log.error("优惠券状态更新失败id:,{}", orderNew.getCouponReceiveId());
  612. }
  613. }
  614. // 更新用户金额 及下单此时
  615. TWxUser paramUser = new TWxUser();
  616. paramUser.setcOpenid(user.getcOpenid());
  617. // 余额支付
  618. if (orderNew.getPayType().equals(MassageConstants.INTEGER_TWO)) {
  619. paramUser.setdBalance(user.getdBalance().subtract(orderNew.getTotalPrice()));
  620. }
  621. paramUser.setdMoney(user.getdMoney().add(orderNew.getTotalPrice()));
  622. paramUser.setnNum(user.getnNum() + MassageConstants.INTEGER_ONE);
  623. paramUser.setId(user.getId());
  624. wxUserService.updateById(paramUser);
  625. //增加个人消费记录
  626. TConsumptionLog tConsumptionLog = new TConsumptionLog();
  627. tConsumptionLog.setAmount(orderNew.getTotalPrice().negate());
  628. tConsumptionLog.setBillNo(orderNew.getOrderNo());
  629. tConsumptionLog.setOpenId(orderNew.getcOpenId());
  630. // 余额支付
  631. if (orderNew.getPayType().equals(MassageConstants.INTEGER_TWO)) {
  632. tConsumptionLog.setBillType(BillTypeEnum.BALANCE_PAYMENT.getCode());
  633. tConsumptionLog.setNote("余额支付");
  634. } else if (orderNew.getPayType().equals(MassageConstants.INTEGER_ONE)) {
  635. tConsumptionLog.setBillType(BillTypeEnum.WX_PAY.getCode());
  636. tConsumptionLog.setNote("微信支付");
  637. } else {
  638. tConsumptionLog.setBillType(BillTypeEnum.CASH_PAYMENT.getCode());
  639. tConsumptionLog.setNote("现金支付");
  640. }
  641. consumptionLogService.save(tConsumptionLog);
  642. // 更新项目数据
  643. JSONArray objects = orderNew.getcGoods();
  644. objects.forEach(item -> {
  645. UpdateWrapper<TXiangmu> wrapper = new UpdateWrapper<>();
  646. // 获取参数
  647. wrapper.lambda().eq(TXiangmu::getcId, ((JSONObject) item).getString("cId"));
  648. // 设置数量
  649. wrapper.setSql(" n_sale_number = n_sale_number + " + ((JSONObject) item).getInteger("number"));
  650. xiangmuService.update(wrapper);
  651. });
  652. TOrder orderParam = new TOrder();
  653. orderParam.setPayType(orderNew.getPayType());
  654. orderParam.setcId(orderNew.getcId());
  655. orderParam.setnStatus(OrderStatusEnum.WAIT_JD.getCode());
  656. orderParam.setPayTime(new Date());
  657. //加钟的订单支付完直接服务中
  658. if (StringUtils.isNotBlank(orderNew.getParentNo())) {
  659. orderParam.setnStatus(OrderStatusEnum.SERVICE.getCode());
  660. }
  661. // orderParam.setnStatus(OrderStatusEnum.SERVICE.getCode());
  662. //更新技师状态
  663. updateJs(orderNew);
  664. // 更新订单的信息
  665. updateById(orderParam);
  666. //添加待接单消息通知(技师侧)
  667. orderNotificationService.sendPendingRemindNotification(orderNew);
  668. //电话通知
  669. TJs js = jsService.getById(orderNew.getcJsId());
  670. Sendvoice.sendPhone(js.getcPhone());
  671. // 任务奖励 或者 消费奖励 —— 发放抽奖次数日志
  672. grantLotteryCountForPay(user, orderNew);
  673. // 已绑定一账通 → 异步同步抽奖次数
  674. syncLotteryCountIfNeeded(user);
  675. }
  676. /**
  677. * 拒绝订单
  678. *
  679. * @param order
  680. * @return Boolean 是否成功
  681. */
  682. @Override
  683. public Boolean jujue(TOrder order) {
  684. TOrder orderNew = getById(order.getcId());
  685. TWxUser user = wxUserService.getByOpenId(orderNew.getcOpenId());
  686. // 更新用户金额 及下单此时
  687. TWxUser paramUser = new TWxUser();
  688. paramUser.setcOpenid(user.getcOpenid());
  689. paramUser.setId(user.getId());
  690. // 余额记录
  691. TConsumptionLog tConsumptionLog = new TConsumptionLog();
  692. tConsumptionLog.setAmount(orderNew.getTotalPrice());
  693. tConsumptionLog.setBillNo(orderNew.getOrderNo());
  694. tConsumptionLog.setOpenId(orderNew.getcOpenId());
  695. if (orderNew.getPayType() == 2) {
  696. // 金额归还对应账户
  697. paramUser.setdBalance(user.getdBalance().add(orderNew.getTotalPrice()));
  698. tConsumptionLog.setBillType(BillTypeEnum.REFUSE_ACCEPT_REFUND.getCode());
  699. tConsumptionLog.setNote("拒绝接单退款到余额");
  700. } else {
  701. // 微信支付
  702. // 生成退款单退款
  703. RefundVoucher refundVoucher = new RefundVoucher();
  704. refundVoucher.setRefundNo(generator.generateNextOrderNumber(OrderNumberGenerator.KEY_PREFIX_REFUND));
  705. refundVoucher.setOrderNo(orderNew.getOrderNo());
  706. refundVoucher.setMoney(orderNew.getTotalPrice());
  707. refundVoucher.setOpenId(orderNew.getcOpenId());
  708. refundVoucher.setReStatus(MassageConstants.INTEGER_ZERO);
  709. refundVoucher.setReason("技师拒绝接单");
  710. refundVoucherService.save(refundVoucher);
  711. tConsumptionLog.setBillType(BillTypeEnum.REFUSE_ACCEPT_REFUND.getCode());
  712. tConsumptionLog.setNote("拒绝接单退款到余额");
  713. // 微信退款原路返回
  714. rechargeService.refund(refundVoucher.getRefundNo(), null, orderNew.getOrderNo(), orderNew.getTotalPrice());
  715. }
  716. consumptionLogService.save(tConsumptionLog);
  717. //退优惠卷
  718. if (StringUtils.isNotBlank(orderNew.getCouponReceiveId())) {
  719. CouponReceive couponReceive = couponReceiveService.getById(orderNew.getCouponReceiveId());
  720. couponReceive.setCouponStatus(MassageConstants.INTEGER_ZERO);
  721. couponReceiveService.updateById(couponReceive);
  722. }
  723. log.info("余额支付退款user:{}", user);
  724. // 消费金额对应减少
  725. paramUser.setdMoney(user.getdMoney().subtract(orderNew.getTotalPrice()));
  726. // 下单次数减一
  727. paramUser.setnNum(user.getnNum() - MassageConstants.INTEGER_ONE);
  728. wxUserService.updateById(paramUser);
  729. // 更新项目数据
  730. JSONArray objects = orderNew.getcGoods();
  731. objects.forEach(item -> {
  732. UpdateWrapper<TXiangmu> wrapper = new UpdateWrapper<>();
  733. // 获取参数
  734. wrapper.lambda().eq(TXiangmu::getcId, ((JSONObject) item).getString("cId"));
  735. // 设置数量
  736. wrapper.setSql(" n_sale_number = n_sale_number - " + ((JSONObject) item).getInteger("number"));
  737. xiangmuService.update(wrapper);
  738. });
  739. TOrder orderParam = new TOrder();
  740. orderParam.setcId(orderNew.getcId());
  741. orderParam.setnStatus(OrderStatusEnum.REFUSE.getCode());
  742. orderParam.setReasonRefusal(order.getReasonRefusal());
  743. updateJs(orderNew);
  744. return updateById(orderParam);
  745. }
  746. /**
  747. * 确认服务完成
  748. *
  749. * @param order
  750. * @return Boolean
  751. */
  752. @Override
  753. @Transactional(rollbackFor = Exception.class)
  754. public Boolean confirm(TOrder order) {
  755. // 获取订单信息
  756. TOrder orderNew = getById(order.getcId());
  757. if (!orderNew.getnStatus().equals(OrderStatusEnum.SERVICE.getCode())) {
  758. throw new ServiceException("订单状态不是服务中");
  759. }
  760. // 更新技师信息
  761. TJs jsParam = new TJs();
  762. jsParam.setId(orderNew.getcJsId());
  763. jsParam.setnStatus(JsStatusEnum.JS_SERVICEABLE.getCode());
  764. //判断热度标识
  765. List<TOrder> list = list(new LambdaQueryWrapper<TOrder>().eq(TOrder::getcJsId, orderNew.getcJsId()).ge(TOrder::getDtCreateTime, DateTimeUtils.addDays(new Date(), -3)).ge(TOrder::getnStatus, OrderStatusEnum.WAIT_EVALUATE.getCode()));
  766. if (list.size() >= 2) {
  767. // 设置热度标识:1
  768. jsParam.setnB3(MassageConstants.INTEGER_ONE);
  769. }
  770. // 更新技师状态
  771. jsService.updateById(jsParam);
  772. // 更新技师钱包金额
  773. TJs jsById = jsService.getById(orderNew.getcJsId());
  774. // 获取技师抽成
  775. BigDecimal multiply = orderNew.getTotalPrice().multiply(new BigDecimal(jsById.getnBili()));
  776. multiply = multiply.divide(new BigDecimal(100), MassageConstants.INTEGER_TWO, RoundingMode.HALF_UP);
  777. // 获取技师所对应的用户
  778. TWxUser jsUser = wxUserService.getByOpenId(jsById.getcOpenId());
  779. // 更新余额
  780. jsUser.setdBalance(jsUser.getdBalance().add(multiply));
  781. // 更新总钱数
  782. jsUser.setdAllMoney(jsUser.getdAllMoney().add(multiply));
  783. wxUserService.updateById(jsUser);
  784. //增加消费记录
  785. TConsumptionLog tConsumptionLog = new TConsumptionLog();
  786. tConsumptionLog.setAmount(multiply);
  787. tConsumptionLog.setBillNo(orderNew.getOrderNo());
  788. tConsumptionLog.setOpenId(jsUser.getcOpenid());
  789. tConsumptionLog.setBillType(BillTypeEnum.INCOME.getCode());
  790. tConsumptionLog.setNote("技师收益");
  791. consumptionLogService.save(tConsumptionLog);
  792. // 如果该技师有推荐人员 一级
  793. if (StringUtils.isNotBlank(jsUser.getcUpUser())) {
  794. // 获取技师上级对应的用户
  795. TWxUser jsUp = wxUserService.getByOpenId(jsUser.getcUpUser());
  796. extracted(orderNew, jsUp);
  797. //二级
  798. if (StringUtils.isNotBlank(jsUp.getcUpUser())) {
  799. TWxUser jsUpTwo = wxUserService.getByOpenId(jsUp.getcUpUser());
  800. extracted(orderNew, jsUpTwo);
  801. //三级
  802. if (StringUtils.isNotBlank(jsUpTwo.getcUpUser())) {
  803. TWxUser jsUpThree = wxUserService.getByOpenId(jsUpTwo.getcUpUser());
  804. extracted(orderNew, jsUpThree);
  805. }
  806. }
  807. }
  808. // 更新订单
  809. // 订单状态:待评价
  810. orderNew.setnStatus(OrderStatusEnum.WAIT_EVALUATE.getCode());
  811. orderNew.setEndTime(LocalDateTime.now());
  812. updateById(orderNew);
  813. // 添加订单完成消息通知(用户侧)
  814. orderNotificationService.sendCompletedNotification(orderNew);
  815. // 完成积分任务(按照新手活动、每日活动、每月活动的优先级顺序)
  816. try {
  817. this.pointUserActivityTaskCompletionService.completeOrderTaskByPriority(orderNew.getcOpenId());
  818. } catch (Exception e) {
  819. log.error("完成积分任务失败 - 订单号:{}, 错误信息:{}", orderNew.getOrderNo(), e.getMessage(), e);
  820. }
  821. return true;
  822. }
  823. private void extracted(TOrder orderNew, TWxUser jsUp) {
  824. log.info("TOrderServiceImpl->extracted->jsUp,{}", JSONUtil.toJsonStr(jsUp));
  825. log.info("TOrderServiceImpl->extracted->orderNew,{}", JSONUtil.toJsonStr(orderNew));
  826. BigDecimal up = orderNew.getdTotalMoney().multiply(new BigDecimal("10"));
  827. up = up.divide(new BigDecimal(100), 2, RoundingMode.HALF_UP);
  828. // 更新余额
  829. jsUp.setdBalance(jsUp.getdBalance().add(up));
  830. // 更新总钱数
  831. jsUp.setdAllMoney(jsUp.getdAllMoney().add(up));
  832. jsUp.setDistributionAmount(up);
  833. wxUserService.updateById(jsUp);
  834. //记录分销收益
  835. TConsumptionLog tConsumptionLog = new TConsumptionLog();
  836. tConsumptionLog.setAmount(up);
  837. tConsumptionLog.setBillNo(orderNew.getOrderNo());
  838. tConsumptionLog.setOpenId(jsUp.getcOpenid());
  839. tConsumptionLog.setBillType(BillTypeEnum.DISTRIBUTION.getCode());
  840. tConsumptionLog.setNote("分销收益");
  841. consumptionLogService.save(tConsumptionLog);
  842. }
  843. /**
  844. * 获取技师当天可预约时间
  845. *
  846. * @param technicianId 技师ID
  847. * @param dateStr 查询日期(格式:yyyy-MM-dd),为null则查询当天
  848. * @return TechnicianAvailabilityVo 技师当天可预约时间VO
  849. */
  850. @Override
  851. public TechnicianAvailabilityVo getTechnicianAvailability(String technicianId, String dateStr) {
  852. // 1. 参数校验
  853. if (StringUtils.isBlank(technicianId)) {
  854. throw new ServiceException("技师ID不能为空");
  855. }
  856. // 2. 查询技师信息
  857. TJs js = jsService.getById(technicianId);
  858. if (js == null) {
  859. throw new ServiceException("技师不存在");
  860. }
  861. // 3. 解析日期,默认为当天
  862. LocalDate queryDate;
  863. if (StringUtils.isBlank(dateStr)) {
  864. queryDate = LocalDate.now();
  865. } else {
  866. try {
  867. queryDate = LocalDate.parse(dateStr);
  868. } catch (Exception e) {
  869. throw new ServiceException("日期格式错误,请使用 yyyy-MM-dd 格式");
  870. }
  871. }
  872. // 4. 定义当天的所有时间段(以30分钟为间隔,从00:00到23:30)
  873. List<TimeSlotVo> timeSlots = new ArrayList<>();
  874. for (int hour = 0; hour < 24; hour++) {
  875. // 每小时生成两个时间段:xx:00 和 xx:30
  876. timeSlots.add(TimeSlotVo.builder().time(String.format("%02d:00", hour)).available(true).build());
  877. timeSlots.add(TimeSlotVo.builder().time(String.format("%02d:30", hour)).available(true).build());
  878. }
  879. // 5. 查询技师当天所有进行中的订单
  880. // 开始时间
  881. LocalDateTime startOfDay = queryDate.atStartOfDay();
  882. // 结束时间
  883. LocalDateTime endOfDay = queryDate.plusDays(1).atStartOfDay();
  884. log.info("开始时间:{},结束时间:{}", startOfDay, endOfDay);
  885. LambdaQueryWrapper<TOrder> queryWrapper = new LambdaQueryWrapper<>();
  886. queryWrapper.eq(TOrder::getcJsId, technicianId).in(TOrder::getnStatus, OrderStatusEnum.WAIT_JD.getCode(), OrderStatusEnum.RECEIVED_ORDER.getCode(), OrderStatusEnum.DEPART.getCode(), OrderStatusEnum.ARRIVED.getCode(), OrderStatusEnum.SERVICE.getCode()).ge(TOrder::getDtCreateTime, startOfDay).lt(TOrder::getDtCreateTime, endOfDay).eq(TOrder::getIsDelete, 0);
  887. List<TOrder> orders = this.list(queryWrapper);
  888. log.info("技师{},在{}天共有 {} 个进行中的订单", technicianId, queryDate, orders.size());
  889. // 6. 标记不可预约的时间段
  890. LocalDateTime now = LocalDateTime.now();
  891. for (TOrder order : orders) {
  892. // 6.1 计算订单的开始时间和结束时间
  893. LocalDateTime orderStart = OrderTimeRangeUtils.estimateStartTime(order);
  894. LocalDateTime orderEnd = OrderTimeRangeUtils.estimateEndTime(order);
  895. if (orderStart == null || orderEnd == null) {
  896. log.warn("订单 {} 的时间信息不完整,跳过", order.getOrderNo());
  897. continue;
  898. }
  899. // 6.2 限制在查询日期范围内
  900. LocalDateTime effectiveStart = orderStart.isBefore(startOfDay) ? startOfDay : orderStart;
  901. LocalDateTime effectiveEnd = orderEnd.isAfter(endOfDay) ? endOfDay : orderEnd;
  902. // 6.3 标记不可预约的时间段
  903. markTimeSlotsUnavailable(timeSlots, effectiveStart, effectiveEnd, order.getOrderNo());
  904. }
  905. // 7. 根据查询日期判断是否可预约
  906. LocalDate today = LocalDate.now();
  907. if (queryDate.isBefore(today)) {
  908. // 查询日期是过去的日期,所有时间段都不可预约
  909. markAllTimeSlotsUnavailable(timeSlots, "日期已过期");
  910. } else if (queryDate.equals(today)) {
  911. // 查询日期是今天,标记过去的时间为不可预约
  912. markPastTimeSlotsUnavailable(timeSlots, now);
  913. }
  914. // 查询日期是未来的日期,所有时间段默认可预约,无需处理
  915. // 8. 构建返回结果
  916. return TechnicianAvailabilityVo.builder().date(queryDate.toString()).technicianId(technicianId).technicianName(js.getcName()).timeSlots(timeSlots).build();
  917. }
  918. /**
  919. * 标记指定时间范围内的时间段为不可预约
  920. *
  921. * @param timeSlots 时间段列表
  922. * @param start 开始时间
  923. * @param end 结束时间
  924. * @param orderNo 订单号
  925. */
  926. private void markTimeSlotsUnavailable(List<TimeSlotVo> timeSlots, LocalDateTime start, LocalDateTime end, String orderNo) {
  927. LocalTime startTime = start.toLocalTime();
  928. LocalTime endTime = end.toLocalTime();
  929. for (TimeSlotVo slot : timeSlots) {
  930. LocalTime slotTime = LocalTime.parse(slot.getTime());
  931. // 判断时间段是否在订单时间范围内
  932. boolean isInRange = !slotTime.isBefore(startTime) && slotTime.isBefore(endTime);
  933. if (isInRange) {
  934. slot.setAvailable(false);
  935. slot.setReason("已有订单");
  936. slot.setOrderNo(orderNo);
  937. }
  938. }
  939. }
  940. /**
  941. * 标记所有时间段为不可预约
  942. *
  943. * @param timeSlots 时间段列表
  944. * @param reason 不可预约原因
  945. */
  946. private void markAllTimeSlotsUnavailable(List<TimeSlotVo> timeSlots, String reason) {
  947. for (TimeSlotVo slot : timeSlots) {
  948. slot.setAvailable(false);
  949. slot.setReason(reason);
  950. slot.setOrderNo(null);
  951. }
  952. }
  953. /**
  954. * 标记过去的时间段为不可预约
  955. *
  956. * @param timeSlots 时间段列表
  957. * @param now 当前时间
  958. */
  959. private void markPastTimeSlotsUnavailable(List<TimeSlotVo> timeSlots, LocalDateTime now) {
  960. LocalTime currentTime = now.toLocalTime();
  961. for (TimeSlotVo slot : timeSlots) {
  962. LocalTime slotTime = LocalTime.parse(slot.getTime());
  963. // 如果当前时间已经过了这个时间段,标记为不可预约
  964. if (slotTime.isBefore(currentTime)) {
  965. slot.setAvailable(false);
  966. slot.setReason("已过期");
  967. slot.setOrderNo(null);
  968. }
  969. }
  970. }
  971. /**
  972. * 取消订单
  973. *
  974. * @param order
  975. * @return Boolean
  976. */
  977. @Override
  978. @Transactional(rollbackFor = Exception.class)
  979. public Boolean cancle(TOrder order) {
  980. // 获取订单信息
  981. // 根据orderid查询订单信息
  982. TOrder orderNew = getById(order.getcId());
  983. //待接单
  984. if (Objects.equals(orderNew.getnStatus(), OrderStatusEnum.WAIT_JD.getCode())) {
  985. TWxUser user = wxUserService.getByOpenId(orderNew.getcOpenId());
  986. // 更新用户金额 及下单此时
  987. TWxUser paramUser = new TWxUser();
  988. paramUser.setId(user.getId());
  989. paramUser.setcOpenid(user.getcOpenid());
  990. TConsumptionLog tConsumptionLog = new TConsumptionLog();
  991. tConsumptionLog.setAmount(orderNew.getTotalPrice());
  992. tConsumptionLog.setBillNo(orderNew.getOrderNo());
  993. tConsumptionLog.setOpenId(orderNew.getcOpenId());
  994. // 余额支付
  995. if (orderNew.getPayType() == 2) {
  996. // 金额归还对应账户
  997. paramUser.setdBalance(user.getdBalance().add(orderNew.getTotalPrice()));
  998. // 余额记录
  999. tConsumptionLog.setBillType(BillTypeEnum.CANCEL_ACCEPT_REFUND.getCode());
  1000. tConsumptionLog.setNote("取消订单退款到余额");
  1001. //自己取消的不退优惠卷
  1002. } else {
  1003. // 微信支付
  1004. // 生成退款单退款
  1005. RefundVoucher refundVoucher = new RefundVoucher();
  1006. refundVoucher.setRefundNo(generator.generateNextOrderNumber(OrderNumberGenerator.KEY_PREFIX_REFUND));
  1007. refundVoucher.setOrderNo(orderNew.getOrderNo());
  1008. refundVoucher.setMoney(orderNew.getTotalPrice());
  1009. refundVoucher.setOpenId(orderNew.getcOpenId());
  1010. refundVoucher.setReStatus(MassageConstants.INTEGER_ZERO);
  1011. refundVoucher.setReason("技师拒绝接单");
  1012. refundVoucherService.save(refundVoucher);
  1013. tConsumptionLog.setBillType(BillTypeEnum.CANCEL_WX_REFUND.getCode());
  1014. tConsumptionLog.setNote("取消订单退款到微信");
  1015. // 微信退款原路返回
  1016. rechargeService.refund(refundVoucher.getRefundNo(), null, orderNew.getOrderNo(), orderNew.getTotalPrice());
  1017. }
  1018. consumptionLogService.save(tConsumptionLog);
  1019. // 消费金额对应减少
  1020. paramUser.setdMoney(user.getdMoney().subtract(orderNew.getTotalPrice()));
  1021. // 下单次数减一
  1022. paramUser.setnNum(user.getnNum() - MassageConstants.INTEGER_ONE);
  1023. wxUserService.updateById(paramUser);
  1024. // 更新项目数据
  1025. JSONArray objects = orderNew.getcGoods();
  1026. objects.forEach(item -> {
  1027. UpdateWrapper<TXiangmu> wrapper = new UpdateWrapper<>();
  1028. // 获取参数
  1029. wrapper.lambda().eq(TXiangmu::getcId, ((JSONObject) item).getString("cId"));
  1030. // 设置数量
  1031. wrapper.setSql(" n_sale_number = n_sale_number - " + ((JSONObject) item).getInteger("number"));
  1032. xiangmuService.update(wrapper);
  1033. });
  1034. TOrder orderParam = new TOrder();
  1035. orderParam.setcId(orderNew.getcId());
  1036. orderParam.setnStatus(OrderStatusEnum.CANCEL.getCode());
  1037. //更新技师状态
  1038. TJs tJs = new TJs();
  1039. tJs.setId(orderNew.getcJsId());
  1040. tJs.setnStatus(JsStatusEnum.JS_SERVICEABLE.getCode());
  1041. jsService.updateById(tJs);
  1042. updateById(orderParam);
  1043. // 添加取消订单通知(用户侧)
  1044. orderNotificationService.sendCancelledNotification(orderNew);
  1045. // 添加取消订单通知(技师侧)
  1046. orderNotificationService.sendTechnicianCancelledNotification(orderNew);
  1047. return true;
  1048. } else if (Objects.equals(orderNew.getnStatus(), OrderStatusEnum.WAIT_PAY.getCode())) {//待付款
  1049. TOrder orderParam = new TOrder();
  1050. orderParam.setcId(orderNew.getcId());
  1051. orderParam.setnStatus(OrderStatusEnum.CANCEL.getCode());
  1052. updateById(orderParam);
  1053. // 添加取消订单通知
  1054. orderNotificationService.sendCancelledNotification(orderNew);
  1055. // 添加取消订单通知(技师侧)
  1056. orderNotificationService.sendTechnicianCancelledNotification(orderNew);
  1057. return true;
  1058. } else {
  1059. return false;
  1060. }
  1061. }
  1062. @Override
  1063. public TOrder getByNo(String orderNo) {
  1064. LambdaQueryWrapper<TOrder> objectLambdaQueryWrapper = new LambdaQueryWrapper<>();
  1065. return this.getOne(objectLambdaQueryWrapper.eq(TOrder::getOrderNo, orderNo));
  1066. }
  1067. // private TOrder gettOrder(TOrder order) {
  1068. // LambdaUpdateWrapper<TOrder> objectLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
  1069. // objectLambdaUpdateWrapper.eq(TOrder::getOrderNo, order.getOrderNo());
  1070. // return this.getOne(objectLambdaUpdateWrapper);
  1071. // }
  1072. @Override
  1073. public Page<TOrder> getAll(Page<TOrder> page, TOrder order) {
  1074. Page<TOrder> orderPage = orderMapper.getAll(page, order);
  1075. if (orderPage != null && CollectionUtil.isNotEmpty(orderPage.getRecords())) {
  1076. ArrayList<TOrder> ordersList = Lists.newArrayList();
  1077. orderPage.getRecords().forEach(orders -> {
  1078. orders.setStatusName(OrderStatusEnum.getDescByCode(orders.getnStatus()));
  1079. orders.setJsPhone(orders.getJs().getcPhone());
  1080. orders.setJsName(orders.getJs().getcName());
  1081. if (StringUtils.isEmpty(orders.getcTime())) {
  1082. orders.setRemainingTime(0L);
  1083. }
  1084. if (StringUtils.isNotBlank(orders.getcTime()) && DateTimeUtils.dateStringToStamp(orders.getcTime()) > DateTimeUtils.dateToStamp(new Date())) {
  1085. orders.setRemainingTime((DateTimeUtils.dateStringToStamp(orders.getcTime()) - DateTimeUtils.dateToStamp(new Date())) / 1000);
  1086. }
  1087. if (StringUtils.isNotBlank(orders.getcTime()) && DateTimeUtils.dateStringToStamp(orders.getcTime()) < DateTimeUtils.dateToStamp(new Date())) {
  1088. orders.setRemainingTime(0L);
  1089. }
  1090. if (StringUtils.isNotBlank(orders.getOldJsId())) {
  1091. orders.setOldJs(jsService.getById(orders.getOldJsId()));
  1092. }
  1093. // 计算已服务时长
  1094. Long serviceDuration = calculateServiceDuration(orders);
  1095. orders.setServiceDuration(String.valueOf(serviceDuration));
  1096. ordersList.add(orders);
  1097. });
  1098. orderPage.setRecords(ordersList);
  1099. }
  1100. return orderPage;
  1101. }
  1102. /**
  1103. * 计算已服务时长(分钟)
  1104. *
  1105. * @param order 订单对象
  1106. * @return Long 已服务时长(分钟)
  1107. */
  1108. private Long calculateServiceDuration(TOrder order) {
  1109. try {
  1110. LocalDateTime startTime = order.getStartTime();
  1111. LocalDateTime endTime = order.getEndTime();
  1112. if (startTime != null && endTime != null) {
  1113. long minutes = Duration.between(startTime, endTime).toMinutes();
  1114. if (minutes > 0) {
  1115. return minutes;
  1116. }
  1117. } else if (startTime != null) {
  1118. // 如果只有开始时间,计算到现在的时长
  1119. long minutes = Duration.between(startTime, LocalDateTime.now()).toMinutes();
  1120. if (minutes > 0) {
  1121. return minutes;
  1122. }
  1123. }
  1124. } catch (Exception e) {
  1125. log.warn("计算服务时长失败,订单ID:{}", order.getcId(), e);
  1126. }
  1127. return 0L;
  1128. }
  1129. @Override
  1130. @Transactional(rollbackFor = Exception.class)
  1131. public void takingOrders(TOrder order) {
  1132. String orderId = order.getcId();
  1133. if (orderId == null || StringUtils.isBlank(orderId)) {
  1134. throw new IllegalArgumentException("订单ID不能为空");
  1135. }
  1136. TOrder orderNew = this.getById(orderId);
  1137. // 【新增】订单状态锁校验 - 检查技师是否可以接单
  1138. log.info("开始校验技师 {} 是否可以接单,订单号:{}", orderNew.getcJsId(), orderNew.getOrderNo());
  1139. orderValidationService.canAcceptOrder(orderNew.getcJsId(), orderNew);
  1140. log.info("技师 {} 接单校验通过,继续接单流程", orderNew.getcJsId());
  1141. // 检查订单对应的技师是否存在
  1142. // updateJs (orderNew);
  1143. TOrder orderParam = new TOrder();
  1144. orderParam.setcId(orderId);
  1145. //设置订单状态:已接单
  1146. orderParam.setnStatus(OrderStatusEnum.RECEIVED_ORDER.getCode());
  1147. orderParam.setAcceptanceTime(LocalDateTime.now());
  1148. this.updateById(orderParam);
  1149. // 已接单消息通知(用户侧)
  1150. orderNotificationService.sendReceivedNotification(orderNew);
  1151. // 已接单消息通知(技师侧)
  1152. orderNotificationService.sendTechnicianReceivedNotification(orderNew);
  1153. }
  1154. /**
  1155. * 更新技师状态
  1156. *
  1157. * @param orderNew
  1158. */
  1159. private void updateJs(TOrder orderNew) {
  1160. TJs js = jsService.getById(orderNew.getcJsId());
  1161. if (js == null) {
  1162. throw new IllegalStateException("无法找到对应的技师");
  1163. }
  1164. if (Objects.equals(js.getnStatus(), JsStatusEnum.JS_SERVICEABLE.getCode())) {
  1165. // 更新技师状态 服务中
  1166. js.setnStatus(JsStatusEnum.JS_SERVICE.getCode());
  1167. // 确保js.getnNum()不为null,避免 NullPointerException
  1168. int num = js.getnNum() == null ? 0 : js.getnNum();
  1169. js.setnNum(num + MassageConstants.INTEGER_ONE);
  1170. } else {
  1171. // 更新技师状态 可服务
  1172. js.setnStatus(JsStatusEnum.JS_SERVICEABLE.getCode());
  1173. // 确保js.getnNum()不为null,避免 NullPointerException
  1174. int num = js.getnNum() == null ? 0 : js.getnNum();
  1175. js.setnNum(num - MassageConstants.INTEGER_ONE);
  1176. }
  1177. jsService.updateById(js);
  1178. }
  1179. /**
  1180. * 申请取消订单(退单申请)
  1181. * <p>
  1182. * 业务流程:
  1183. * 1. 校验订单状态(仅进行中的订单可申请退单)
  1184. * 2. 创建退单申请记录
  1185. * 3. 更新订单状态为"退单待审核"
  1186. *
  1187. * @param order 订单对象
  1188. * @return Boolean 申请结果
  1189. */
  1190. @Override
  1191. @Transactional(rollbackFor = Exception.class)
  1192. public Boolean applyCancle(String cId, String cancelReason) {
  1193. log.info("开始处理退单申请,订单ID:{}", cId);
  1194. // 1. 参数校验
  1195. if (StringUtils.isBlank(cId)) {
  1196. throw new ServiceException("订单ID不能为空");
  1197. }
  1198. if (StringUtils.isBlank(cancelReason)) {
  1199. throw new ServiceException("退单原因不能为空");
  1200. }
  1201. // 2. 根据订单ID查询订单信息
  1202. TOrder existingOrder = this.getById(cId);
  1203. if (existingOrder == null) {
  1204. throw new ServiceException("订单不存在");
  1205. }
  1206. // 3. 创建退单申请记录(内部会校验订单状态和其他业务规则)
  1207. String applicationId;
  1208. try {
  1209. applicationId = cancelOrderApplicationService.createApplication(existingOrder.getcId(), cancelReason);
  1210. log.info("退单申请记录创建成功,申请ID:{}", applicationId);
  1211. } catch (ServiceException e) {
  1212. log.error("创建退单申请失败:{}", e.getMessage());
  1213. throw e;
  1214. }
  1215. // 4. 更新订单状态为"退单待审核"
  1216. existingOrder.setnStatus(OrderStatusEnum.CANCEL_APPLICATION_PENDING.getCode());
  1217. this.updateById(existingOrder);
  1218. log.info("退单申请处理完成,订单ID:{},申请ID:{}", existingOrder.getcId(), applicationId);
  1219. return Boolean.TRUE;
  1220. }
  1221. /**
  1222. * 取消退单申请
  1223. * 用户主动取消退单申请,恢复订单状态
  1224. * <p>
  1225. * 业务流程:
  1226. * 1. 参数校验(订单ID不能为空)
  1227. * 2. 查询订单和退单申请记录
  1228. * 3. 校验订单状态必须为"退单待审核"(6)
  1229. * 4. 校验退单审核状态必须为"待审核"(0)
  1230. * 5. 调用退单申请服务取消申请
  1231. * 6. 恢复订单状态到申请前的原始状态
  1232. *
  1233. * @param order 订单对象,需要包含cId(订单ID)
  1234. * @return Boolean 操作结果
  1235. */
  1236. @Override
  1237. @Transactional(rollbackFor = Exception.class)
  1238. public Boolean cancelApplyCancle(TOrder order) {
  1239. log.info("开始取消退单申请,订单ID:{}", order.getcId());
  1240. // 1. 参数校验
  1241. if (StringUtils.isBlank(order.getcId())) {
  1242. throw new ServiceException("订单ID不能为空");
  1243. }
  1244. // 2. 查询订单信息
  1245. TOrder existingOrder = this.getById(order.getcId());
  1246. if (existingOrder == null) {
  1247. throw new ServiceException("订单不存在");
  1248. }
  1249. // 3. 校验订单状态 - 只有"退单待审核"状态的订单才能取消申请
  1250. Integer currentStatus = existingOrder.getnStatus();
  1251. if (!OrderStatusEnum.CANCEL_APPLICATION_PENDING.getCode().equals(currentStatus)) {
  1252. throw new ServiceException("当前订单状态不允许取消退单申请");
  1253. }
  1254. // 4. 查询退单申请记录
  1255. com.ylx.massage.domain.CancelOrderApplication application = cancelOrderApplicationService.getInfoByOrderId(order.getcId());
  1256. if (application == null) {
  1257. throw new ServiceException("退单申请记录不存在");
  1258. }
  1259. // 5. 校验退单申请状态 - 只有"待审核"的申请才能取消
  1260. if (application.getAuditStatus() != 0) {
  1261. throw new ServiceException("当前退单申请状态不允许取消");
  1262. }
  1263. // 6. 获取订单原始状态(在申请退单时保存的状态)
  1264. Integer originalStatus = application.getOrderStatus();
  1265. if (originalStatus == null) {
  1266. throw new ServiceException("无法获取订单原始状态,取消申请失败");
  1267. }
  1268. // 7. 调用退单申请服务取消申请
  1269. try {
  1270. cancelOrderApplicationService.cancelApplication(order.getcId());
  1271. log.info("退单申请记录取消成功,订单ID:{}", order.getcId());
  1272. } catch (ServiceException e) {
  1273. log.error("取消退单申请失败:{}", e.getMessage());
  1274. throw e;
  1275. }
  1276. // 8. 恢复订单状态到原始状态
  1277. existingOrder.setnStatus(originalStatus);
  1278. boolean updated = this.updateById(existingOrder);
  1279. if (!updated) {
  1280. throw new ServiceException("恢复订单状态失败");
  1281. }
  1282. log.info("取消退单申请处理完成,订单ID:{},恢复到状态:{}", order.getcId(), originalStatus);
  1283. return Boolean.TRUE;
  1284. }
  1285. @Override
  1286. public int countCompletedOrders(String openId, Date queryTime) {
  1287. // 统计用户在指定时间之前完成的订单数量
  1288. // 完成状态包括:4-待评价(已完成)和5-已完成(已评价)
  1289. LambdaQueryWrapper<TOrder> queryWrapper = new LambdaQueryWrapper<>();
  1290. queryWrapper.eq(TOrder::getcOpenId, openId).in(TOrder::getnStatus, OrderStatusEnum.WAIT_EVALUATE.getCode(), OrderStatusEnum.COMPLETE.getCode()).lt(TOrder::getEndTime, queryTime);
  1291. return Math.toIntExact(this.count(queryWrapper));
  1292. }
  1293. private void grantLotteryCountForPay(TWxUser user, TOrder orderParam) {
  1294. // 获取按摩订单中的商品ID
  1295. JSONArray array = orderParam.getcGoods();
  1296. JSONObject obj = array.getJSONObject(0);
  1297. String cId = obj.getString("cId");
  1298. List<LotteryActivityVO> activityList = lotteryCountService.queryActivityRules();
  1299. if (CollUtil.isEmpty(activityList)) {
  1300. return;
  1301. }
  1302. // 查询抽奖次数统计
  1303. LambdaQueryWrapper<LotteryCountLog> queryWrapper = new LambdaQueryWrapper<>();
  1304. queryWrapper.eq(LotteryCountLog::getUserId, user.getId());
  1305. queryWrapper.eq(LotteryCountLog::getIsDelete, 0);
  1306. List<LotteryStatVO> lotteryStatVOS = lotteryCountLogService.selectSumByGroup(queryWrapper);
  1307. // 生成活动ID -> 已获次数 的 Map
  1308. Map<String, Integer> activityGrantedCountMap = new HashMap<>();
  1309. if (CollUtil.isNotEmpty(lotteryStatVOS)) {
  1310. for (LotteryStatVO statVO : lotteryStatVOS) {
  1311. activityGrantedCountMap.put(statVO.getLocalActivityTableId(), statVO.getTotalNum());
  1312. }
  1313. }
  1314. for (LotteryActivityVO activity : activityList) {
  1315. String activityId = activity.getId();
  1316. String activityUrl = activity.getActivityUrl();
  1317. // 用户获取的最大抽奖次数
  1318. Integer productUserMaxNum = activity.getProductUserMaxNum();
  1319. // 检查 productUserMaxNum 是否为 null,如果是,则认为无限制
  1320. boolean hasLimit = !ObjectUtil.isNull(productUserMaxNum);
  1321. if (ObjectUtil.equals(activity.getParticipationRules(), 3)) { // 任务奖励类型
  1322. List<LocalActivityTableVO> localTables = activity.getLocalActivityTables();
  1323. if (CollUtil.isEmpty(localTables)) {
  1324. continue;
  1325. }
  1326. // 类型=2 → 支付有礼
  1327. for (LocalActivityTableVO table : localTables) {
  1328. if (ObjectUtil.equals(table.getType(), "2") && ObjectUtil.equals(table.getIsProduct(), "1")) {
  1329. if (ObjectUtil.equals(table.getProductId(), cId)) {
  1330. Integer userLotteryCount = activityGrantedCountMap.getOrDefault(activityId, 0);
  1331. // 本次请求发放的数量
  1332. Integer requestedLotteryNum = 1;
  1333. // 计算实际可发放的数量
  1334. int actualLotteryNumToGrant = calculateActualGrant(hasLimit, userLotteryCount, requestedLotteryNum, productUserMaxNum);
  1335. if (actualLotteryNumToGrant > 0) {
  1336. // 发放计算后的数量
  1337. saveLotteryLog(user, activityId, activityUrl, actualLotteryNumToGrant, 2);
  1338. }
  1339. break;
  1340. }
  1341. }
  1342. }
  1343. } else if (ObjectUtil.equals(activity.getParticipationRules(), 4)) { // 消费奖励类型
  1344. if (ObjectUtil.isNull(activity.getProductRestriction())) {
  1345. continue;
  1346. }
  1347. Integer requestedLotteryNum = null;
  1348. boolean conditionMet = false;
  1349. if (ObjectUtil.equals(activity.getProductRestriction(), 1)) {
  1350. List<LotteryActivityRulesProductVO> lotteryActivityRulesProducts = activity.getLotteryActivityRulesProducts();
  1351. if (CollUtil.isEmpty(lotteryActivityRulesProducts)) {
  1352. continue;
  1353. }
  1354. requestedLotteryNum = activity.getProductLotteryNum();
  1355. if (!ObjectUtil.isNull(requestedLotteryNum)) {
  1356. for (LotteryActivityRulesProductVO activityRulesProduct : lotteryActivityRulesProducts) {
  1357. if (ObjectUtil.equals(activityRulesProduct.getProductId(), cId)) {
  1358. conditionMet = true;
  1359. break;
  1360. }
  1361. }
  1362. }
  1363. } else if (ObjectUtil.equals(activity.getProductRestriction(), 2)) {
  1364. // 用户消费满几元
  1365. Integer consumeAmount = activity.getConsumeAmount();
  1366. // 消费奖励 消费满几元的几次抽奖机会
  1367. requestedLotteryNum = activity.getConsumeAmountLottery();
  1368. if (!ObjectUtil.isNull(consumeAmount) && !ObjectUtil.isNull(requestedLotteryNum) && !ObjectUtil.isNull(orderParam.getdTotalMoney())) {
  1369. BigDecimal thresholdAmount = new BigDecimal(consumeAmount);
  1370. BigDecimal totalMoney = orderParam.getdTotalMoney();
  1371. conditionMet = totalMoney.compareTo(thresholdAmount) >= 0;
  1372. }
  1373. }
  1374. // 如果条件满足,则进行次数检查并发放
  1375. if (conditionMet && !ObjectUtil.isNull(requestedLotteryNum)) {
  1376. // --- 修复: 使用 getOrDefault 避免 NPE ---
  1377. Integer userLotteryCount = activityGrantedCountMap.getOrDefault(activityId, 0);
  1378. // --- 新增: 计算实际可发放的数量 ---
  1379. int actualLotteryNumToGrant = calculateActualGrant(hasLimit, userLotteryCount, requestedLotteryNum, productUserMaxNum);
  1380. if (actualLotteryNumToGrant > 0) {
  1381. saveLotteryLog(user, activityId, activityUrl, actualLotteryNumToGrant, 3);
  1382. }
  1383. }
  1384. }
  1385. }
  1386. }
  1387. private void syncLotteryCountIfNeeded(TWxUser user) {
  1388. if (ObjectUtil.equals(user.getIsBind(), 1) && com.ylx.common.utils.StringUtils.isNotBlank(user.getLocalLiveUserId())) {
  1389. UnifiedUserCenterDTO dto = new UnifiedUserCenterDTO();
  1390. dto.setSourceUserId(user.getLocalLiveUserId());
  1391. dto.setTargetUserId(user.getId());
  1392. // 👇 加上这个 if 判断,只有当当前存在活跃事务时,才去注册 afterCommit 回调
  1393. if (TransactionSynchronizationManager.isSynchronizationActive()) {
  1394. TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
  1395. @Override
  1396. public void afterCommit() {
  1397. // 事务提交完成!现在调用异步,一定能查到数据
  1398. unifiedUserCenterService.syncLotteryCount(dto);
  1399. }
  1400. });
  1401. } else {
  1402. // 👇 兜底逻辑:如果事务没开启(比如同类自调用导致失效),就直接同步执行,避免报错
  1403. log.warn("当前没有活跃事务,无法注册 afterCommit 回调,将直接同步执行抽奖次数同步!");
  1404. unifiedUserCenterService.syncLotteryCount(dto);
  1405. }
  1406. }
  1407. }
  1408. /**
  1409. * 统一保存抽奖次数日志
  1410. */
  1411. private void saveLotteryLog(TWxUser user, String activityId, String activityUrl, Integer lotteryNum, Integer activityType) {
  1412. LotteryCountLog log = new LotteryCountLog();
  1413. log.setOpenId(user.getcOpenid());
  1414. log.setUserId(user.getId());
  1415. log.setUserPhone(user.getcPhone());
  1416. log.setActivityType(activityType);
  1417. log.setLocalActivityTableId(activityId);
  1418. log.setLotteryNum(lotteryNum);
  1419. log.setReceiveTime(new Date());
  1420. log.setIsDelete("0");
  1421. log.setStatus(0); // 未同步
  1422. log.setIsLottery(0);
  1423. log.setType(1);
  1424. log.setActivityUrl(activityUrl);
  1425. lotteryCountLogService.save(log);
  1426. }
  1427. /**
  1428. * 根据已有次数、请求发放次数和最大次数限制,计算实际可发放的次数
  1429. *
  1430. * @param hasLimit 是否有限制
  1431. * @param currentCount 当前已获得次数
  1432. * @param requestedCount 请求发放次数
  1433. * @param maxCount 最大次数限制
  1434. * @return 实际可发放次数
  1435. */
  1436. private int calculateActualGrant(boolean hasLimit, Integer currentCount, Integer requestedCount, Integer maxCount) {
  1437. if (!hasLimit) {
  1438. // 如果没有限制,直接返回请求的次数
  1439. return requestedCount;
  1440. }
  1441. // 如果有限制
  1442. int current = currentCount != null ? currentCount : 0;
  1443. int max = maxCount != null ? maxCount : 0;
  1444. int requested = requestedCount != null ? requestedCount : 0;
  1445. // 计算还能发放多少次
  1446. int remainingQuota = max - current;
  1447. // 实际发放次数为:请求次数 和 剩余配额 的较小值
  1448. return Math.max(0, Math.min(requested, remainingQuota));
  1449. }
  1450. }