SelfDeliveryService.java 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. package com.ruoyi.app.order;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
  4. import com.ruoyi.app.mendian.dto.SelfDeliveryConfigInput;
  5. import com.ruoyi.app.mendian.dto.SelfDeliveryConfigInput.SelfDeliveryHourInput;
  6. import com.ruoyi.app.utils.SelfDeliveryUtil;
  7. import com.ruoyi.common.exception.ServiceException;
  8. import com.ruoyi.common.utils.MessageUtils;
  9. import com.ruoyi.system.domain.InfoUser;
  10. import com.ruoyi.system.domain.PosStore;
  11. import com.ruoyi.system.domain.PosStoreSelfDeliveryHours;
  12. import com.ruoyi.system.mapper.PosStoreSelfDeliveryHoursMapper;
  13. import com.ruoyi.system.service.IPosStoreService;
  14. import org.springframework.beans.factory.annotation.Autowired;
  15. import org.springframework.stereotype.Service;
  16. import org.springframework.transaction.annotation.Transactional;
  17. import java.time.LocalDateTime;
  18. import java.util.Comparator;
  19. import java.util.List;
  20. import java.util.Map;
  21. import java.util.stream.Collectors;
  22. /**
  23. * 商家自配送服务(029):配置主体解析、下单时段判定、配置保存。
  24. * 配置主体规则:普通门店=自身行;夜市场景配置存于夜市主自己的门店行(isNightMarket=1),
  25. * 摊位经 nightMarketId 解析读取——保证同一夜市统一模式(research D1/F3)。
  26. */
  27. @Service
  28. public class SelfDeliveryService {
  29. /** HH:mm 定长格式(配置校验用,保证字符串比较等价时间比较) */
  30. private static final String TIME_PATTERN = "^([01]\\d|2[0-3]):[0-5]\\d$";
  31. @Autowired
  32. private PosStoreSelfDeliveryHoursMapper hoursMapper;
  33. @Autowired
  34. private IPosStoreService posStoreService;
  35. /**
  36. * 配置主体解析:摊位(isStall=1 且 nightMarketId 非空)返回夜市主门店行,其余返回自身。
  37. * 夜市主门店行缺失返回 null——视为未配置,判定自然回落骑手池(research D1)。
  38. */
  39. public PosStore resolveConfigStore(PosStore store) {
  40. if (store == null) {
  41. return null;
  42. }
  43. boolean stall = Integer.valueOf(1).equals(store.getIsStall());
  44. if (!stall || store.getNightMarketId() == null) {
  45. return store;
  46. }
  47. // 夜市主门店行:归属夜市主(userId=nightMarketId)且 isNightMarket=1
  48. return posStoreService.getOne(new LambdaQueryWrapper<PosStore>()
  49. .eq(PosStore::getUserId, store.getNightMarketId())
  50. .eq(PosStore::getIsNightMarket, 1)
  51. .last("LIMIT 1"));
  52. }
  53. /**
  54. * 夜市主登录态反查自己的夜市主门店行(isNightMarket=1 且 user_id=登录 userId)。
  55. * 夜市主账号(info_user)不持有门店 ID,PC「我的自配送」接口只能按归属反查;
  56. * 查不到说明该账号没有夜市主门店行,调用方按"未找到夜市门店"提示。
  57. */
  58. public PosStore resolveNightMarketStoreByOwner(Long userId) {
  59. return posStoreService.getOne(new LambdaQueryWrapper<PosStore>()
  60. .eq(PosStore::getUserId, userId)
  61. .eq(PosStore::getIsNightMarket, 1)
  62. .last("LIMIT 1"));
  63. }
  64. /**
  65. * 2026-10-08 变更:夜市主门店行缺失时按登录夜市主自动补建。
  66. * 历史上没有任何路径产出过该行(测试库 is_night_market=1 全库为 0 条),
  67. * 夜市主首次保存自配送配置时补一条纯配置锚点行(is_night_market=1、user_id=夜市主);
  68. * 后建摊位经 night_market_id 解析到该行。C 端不受影响:夜市列表读 info_user
  69. * (selectNightMarketListByDistance),摊位列表过滤 is_stall=1。
  70. */
  71. public PosStore resolveOrCreateNightMarketStoreByOwner(InfoUser owner) {
  72. PosStore row = resolveNightMarketStoreByOwner(owner.getUserId());
  73. if (row != null) {
  74. return row;
  75. }
  76. PosStore anchor = new PosStore();
  77. anchor.setUserId(owner.getUserId());
  78. anchor.setPosName(owner.getUserName());
  79. anchor.setIsNightMarket(1);
  80. anchor.setIsStall(0);
  81. anchor.setDelFlag("0");
  82. anchor.setCretim(LocalDateTime.now());
  83. posStoreService.save(anchor);
  84. return anchor;
  85. }
  86. /**
  87. * 判定基准时刻(029):预约单(delryTime 非空)按预约送达时间段、取其开始时刻,
  88. * 即时单按当前时间。delryTime 格式为"yyyy-MM-dd HH:mm-HH:mm"(预约时间段,见 DateUtil.isNearOrPastStartTime);
  89. * 解析失败按即时单处理(历史脏数据兜底,不阻断下单)。
  90. */
  91. public LocalDateTime resolveJudgeTime(String delryTime) {
  92. if (delryTime == null || delryTime.isBlank()) {
  93. return LocalDateTime.now();
  94. }
  95. try {
  96. String[] parts = delryTime.trim().split(" ");
  97. String[] range = parts[1].split("-");
  98. return LocalDateTime.parse(parts[0].trim() + "T" + range[0].trim());
  99. } catch (Exception e) {
  100. return LocalDateTime.now();
  101. }
  102. }
  103. /**
  104. * 下单判定:订单门店是否命中自配送时段;结果由调用方固化到订单快照列 self_delivery。
  105. *
  106. * @param target 判定基准时刻:即时单=下单时刻,预约单=预约送达时间(spec 决策②)
  107. */
  108. public boolean isSelfDelivery(PosStore store, LocalDateTime target) {
  109. PosStore configStore = resolveConfigStore(store);
  110. if (configStore == null) {
  111. return false;
  112. }
  113. List<PosStoreSelfDeliveryHours> hours = listHours(Long.valueOf(configStore.getId()));
  114. return SelfDeliveryUtil.matches(configStore.getSelfDeliveryEnabled(),
  115. configStore.getSelfDeliveryMode(), hours, target);
  116. }
  117. /** 读取配置主体门店的自定义时段列表(按星期与开始时刻排序) */
  118. public List<PosStoreSelfDeliveryHours> listHours(Long mdId) {
  119. return hoursMapper.selectList(new LambdaQueryWrapper<PosStoreSelfDeliveryHours>()
  120. .eq(PosStoreSelfDeliveryHours::getMdId, mdId)
  121. .orderByAsc(PosStoreSelfDeliveryHours::getDayOfWeek)
  122. .orderByAsc(PosStoreSelfDeliveryHours::getStartTime));
  123. }
  124. /**
  125. * 保存配置:整体覆盖式(先删后插,与营业时段保存一致)。
  126. * 仅处理配置主体门店;摊位拦截与归属校验在 Controller 完成(contracts/api.md §1)。
  127. */
  128. @Transactional(rollbackFor = Exception.class)
  129. public void saveConfig(SelfDeliveryConfigInput input) {
  130. validate(input);
  131. int enabledFlag = Boolean.TRUE.equals(input.getEnabled()) ? 1 : 0;
  132. Integer mode = enabledFlag == 1 ? input.getMode() : null;
  133. // 关闭时需把 mode 显式置空:updateById 会忽略 null 字段,必须用 UpdateWrapper set;
  134. // 用字符串列名(Lambda 列缓存在纯单测中不可用,仓库 QueryWrapper 亦有字符串列名先例)
  135. posStoreService.update(new UpdateWrapper<PosStore>()
  136. .eq("id", input.getMdId())
  137. .set("self_delivery_enabled", enabledFlag)
  138. .set("self_delivery_mode", mode));
  139. hoursMapper.delete(new LambdaQueryWrapper<PosStoreSelfDeliveryHours>()
  140. .eq(PosStoreSelfDeliveryHours::getMdId, Long.valueOf(input.getMdId())));
  141. if (enabledFlag == 1 && input.getMode() == SelfDeliveryUtil.MODE_CUSTOM) {
  142. for (SelfDeliveryHourInput h : input.getHours()) {
  143. PosStoreSelfDeliveryHours row = new PosStoreSelfDeliveryHours();
  144. row.setMdId(Long.valueOf(input.getMdId()));
  145. row.setDayOfWeek(h.getDayOfWeek());
  146. row.setStartTime(h.getStartTime());
  147. row.setEndTime(h.getEndTime());
  148. hoursMapper.insert(row);
  149. }
  150. }
  151. }
  152. private void validate(SelfDeliveryConfigInput input) {
  153. if (input == null || input.getMdId() == null || input.getEnabled() == null) {
  154. throw invalid();
  155. }
  156. if (Boolean.TRUE.equals(input.getEnabled())) {
  157. Integer mode = input.getMode();
  158. boolean modeOk = mode != null
  159. && (mode == SelfDeliveryUtil.MODE_ALL_DAY || mode == SelfDeliveryUtil.MODE_CUSTOM);
  160. if (!modeOk) {
  161. throw invalid();
  162. }
  163. if (mode == SelfDeliveryUtil.MODE_CUSTOM) {
  164. List<SelfDeliveryHourInput> hours = input.getHours();
  165. if (hours == null || hours.isEmpty() || hours.stream().anyMatch(h -> !hourValid(h))
  166. || hasOverlap(hours)) {
  167. throw invalid();
  168. }
  169. }
  170. }
  171. }
  172. /**
  173. * 同一星期内时段重叠校验(spec Edge Cases:重叠时段易误配)。
  174. * 相邻衔接(前段结束=后段开始)不算重叠。
  175. */
  176. private static boolean hasOverlap(List<SelfDeliveryHourInput> hours) {
  177. Map<Integer, List<SelfDeliveryHourInput>> byDay = hours.stream()
  178. .collect(Collectors.groupingBy(SelfDeliveryHourInput::getDayOfWeek));
  179. for (List<SelfDeliveryHourInput> day : byDay.values()) {
  180. day.sort(Comparator.comparing(SelfDeliveryHourInput::getStartTime));
  181. for (int i = 1; i < day.size(); i++) {
  182. if (day.get(i).getStartTime().compareTo(day.get(i - 1).getEndTime()) < 0) {
  183. return true;
  184. }
  185. }
  186. }
  187. return false;
  188. }
  189. private static boolean hourValid(SelfDeliveryHourInput h) {
  190. return h != null && h.getDayOfWeek() != null && h.getDayOfWeek() >= 1 && h.getDayOfWeek() <= 7
  191. && h.getStartTime() != null && h.getStartTime().matches(TIME_PATTERN)
  192. && h.getEndTime() != null && h.getEndTime().matches(TIME_PATTERN)
  193. // 结束必须晚于开始(定长 HH:mm 字符串比较),不支持跨天时段
  194. && h.getEndTime().compareTo(h.getStartTime()) > 0;
  195. }
  196. private ServiceException invalid() {
  197. return new ServiceException(MessageUtils.message("no.store.selfdelivery.hours.invalid"));
  198. }
  199. }