| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- package com.ruoyi.app.order;
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
- import com.ruoyi.app.mendian.dto.SelfDeliveryConfigInput;
- import com.ruoyi.app.mendian.dto.SelfDeliveryConfigInput.SelfDeliveryHourInput;
- import com.ruoyi.app.utils.SelfDeliveryUtil;
- import com.ruoyi.common.exception.ServiceException;
- import com.ruoyi.common.utils.MessageUtils;
- import com.ruoyi.system.domain.InfoUser;
- import com.ruoyi.system.domain.PosStore;
- import com.ruoyi.system.domain.PosStoreSelfDeliveryHours;
- import com.ruoyi.system.mapper.PosStoreSelfDeliveryHoursMapper;
- import com.ruoyi.system.service.IPosStoreService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import java.time.LocalDateTime;
- import java.util.Comparator;
- import java.util.List;
- import java.util.Map;
- import java.util.stream.Collectors;
- /**
- * 商家自配送服务(029):配置主体解析、下单时段判定、配置保存。
- * 配置主体规则:普通门店=自身行;夜市场景配置存于夜市主自己的门店行(isNightMarket=1),
- * 摊位经 nightMarketId 解析读取——保证同一夜市统一模式(research D1/F3)。
- */
- @Service
- public class SelfDeliveryService {
- /** HH:mm 定长格式(配置校验用,保证字符串比较等价时间比较) */
- private static final String TIME_PATTERN = "^([01]\\d|2[0-3]):[0-5]\\d$";
- @Autowired
- private PosStoreSelfDeliveryHoursMapper hoursMapper;
- @Autowired
- private IPosStoreService posStoreService;
- /**
- * 配置主体解析:摊位(isStall=1 且 nightMarketId 非空)返回夜市主门店行,其余返回自身。
- * 夜市主门店行缺失返回 null——视为未配置,判定自然回落骑手池(research D1)。
- */
- public PosStore resolveConfigStore(PosStore store) {
- if (store == null) {
- return null;
- }
- boolean stall = Integer.valueOf(1).equals(store.getIsStall());
- if (!stall || store.getNightMarketId() == null) {
- return store;
- }
- // 夜市主门店行:归属夜市主(userId=nightMarketId)且 isNightMarket=1
- return posStoreService.getOne(new LambdaQueryWrapper<PosStore>()
- .eq(PosStore::getUserId, store.getNightMarketId())
- .eq(PosStore::getIsNightMarket, 1)
- .last("LIMIT 1"));
- }
- /**
- * 夜市主登录态反查自己的夜市主门店行(isNightMarket=1 且 user_id=登录 userId)。
- * 夜市主账号(info_user)不持有门店 ID,PC「我的自配送」接口只能按归属反查;
- * 查不到说明该账号没有夜市主门店行,调用方按"未找到夜市门店"提示。
- */
- public PosStore resolveNightMarketStoreByOwner(Long userId) {
- return posStoreService.getOne(new LambdaQueryWrapper<PosStore>()
- .eq(PosStore::getUserId, userId)
- .eq(PosStore::getIsNightMarket, 1)
- .last("LIMIT 1"));
- }
- /**
- * 2026-10-08 变更:夜市主门店行缺失时按登录夜市主自动补建。
- * 历史上没有任何路径产出过该行(测试库 is_night_market=1 全库为 0 条),
- * 夜市主首次保存自配送配置时补一条纯配置锚点行(is_night_market=1、user_id=夜市主);
- * 后建摊位经 night_market_id 解析到该行。C 端不受影响:夜市列表读 info_user
- * (selectNightMarketListByDistance),摊位列表过滤 is_stall=1。
- */
- public PosStore resolveOrCreateNightMarketStoreByOwner(InfoUser owner) {
- PosStore row = resolveNightMarketStoreByOwner(owner.getUserId());
- if (row != null) {
- return row;
- }
- PosStore anchor = new PosStore();
- anchor.setUserId(owner.getUserId());
- anchor.setPosName(owner.getUserName());
- anchor.setIsNightMarket(1);
- anchor.setIsStall(0);
- anchor.setDelFlag("0");
- anchor.setCretim(LocalDateTime.now());
- posStoreService.save(anchor);
- return anchor;
- }
- /**
- * 判定基准时刻(029):预约单(delryTime 非空)按预约送达时间段、取其开始时刻,
- * 即时单按当前时间。delryTime 格式为"yyyy-MM-dd HH:mm-HH:mm"(预约时间段,见 DateUtil.isNearOrPastStartTime);
- * 解析失败按即时单处理(历史脏数据兜底,不阻断下单)。
- */
- public LocalDateTime resolveJudgeTime(String delryTime) {
- if (delryTime == null || delryTime.isBlank()) {
- return LocalDateTime.now();
- }
- try {
- String[] parts = delryTime.trim().split(" ");
- String[] range = parts[1].split("-");
- return LocalDateTime.parse(parts[0].trim() + "T" + range[0].trim());
- } catch (Exception e) {
- return LocalDateTime.now();
- }
- }
- /**
- * 下单判定:订单门店是否命中自配送时段;结果由调用方固化到订单快照列 self_delivery。
- *
- * @param target 判定基准时刻:即时单=下单时刻,预约单=预约送达时间(spec 决策②)
- */
- public boolean isSelfDelivery(PosStore store, LocalDateTime target) {
- PosStore configStore = resolveConfigStore(store);
- if (configStore == null) {
- return false;
- }
- List<PosStoreSelfDeliveryHours> hours = listHours(Long.valueOf(configStore.getId()));
- return SelfDeliveryUtil.matches(configStore.getSelfDeliveryEnabled(),
- configStore.getSelfDeliveryMode(), hours, target);
- }
- /** 读取配置主体门店的自定义时段列表(按星期与开始时刻排序) */
- public List<PosStoreSelfDeliveryHours> listHours(Long mdId) {
- return hoursMapper.selectList(new LambdaQueryWrapper<PosStoreSelfDeliveryHours>()
- .eq(PosStoreSelfDeliveryHours::getMdId, mdId)
- .orderByAsc(PosStoreSelfDeliveryHours::getDayOfWeek)
- .orderByAsc(PosStoreSelfDeliveryHours::getStartTime));
- }
- /**
- * 保存配置:整体覆盖式(先删后插,与营业时段保存一致)。
- * 仅处理配置主体门店;摊位拦截与归属校验在 Controller 完成(contracts/api.md §1)。
- */
- @Transactional(rollbackFor = Exception.class)
- public void saveConfig(SelfDeliveryConfigInput input) {
- validate(input);
- int enabledFlag = Boolean.TRUE.equals(input.getEnabled()) ? 1 : 0;
- Integer mode = enabledFlag == 1 ? input.getMode() : null;
- // 关闭时需把 mode 显式置空:updateById 会忽略 null 字段,必须用 UpdateWrapper set;
- // 用字符串列名(Lambda 列缓存在纯单测中不可用,仓库 QueryWrapper 亦有字符串列名先例)
- posStoreService.update(new UpdateWrapper<PosStore>()
- .eq("id", input.getMdId())
- .set("self_delivery_enabled", enabledFlag)
- .set("self_delivery_mode", mode));
- hoursMapper.delete(new LambdaQueryWrapper<PosStoreSelfDeliveryHours>()
- .eq(PosStoreSelfDeliveryHours::getMdId, Long.valueOf(input.getMdId())));
- if (enabledFlag == 1 && input.getMode() == SelfDeliveryUtil.MODE_CUSTOM) {
- for (SelfDeliveryHourInput h : input.getHours()) {
- PosStoreSelfDeliveryHours row = new PosStoreSelfDeliveryHours();
- row.setMdId(Long.valueOf(input.getMdId()));
- row.setDayOfWeek(h.getDayOfWeek());
- row.setStartTime(h.getStartTime());
- row.setEndTime(h.getEndTime());
- hoursMapper.insert(row);
- }
- }
- }
- private void validate(SelfDeliveryConfigInput input) {
- if (input == null || input.getMdId() == null || input.getEnabled() == null) {
- throw invalid();
- }
- if (Boolean.TRUE.equals(input.getEnabled())) {
- Integer mode = input.getMode();
- boolean modeOk = mode != null
- && (mode == SelfDeliveryUtil.MODE_ALL_DAY || mode == SelfDeliveryUtil.MODE_CUSTOM);
- if (!modeOk) {
- throw invalid();
- }
- if (mode == SelfDeliveryUtil.MODE_CUSTOM) {
- List<SelfDeliveryHourInput> hours = input.getHours();
- if (hours == null || hours.isEmpty() || hours.stream().anyMatch(h -> !hourValid(h))
- || hasOverlap(hours)) {
- throw invalid();
- }
- }
- }
- }
- /**
- * 同一星期内时段重叠校验(spec Edge Cases:重叠时段易误配)。
- * 相邻衔接(前段结束=后段开始)不算重叠。
- */
- private static boolean hasOverlap(List<SelfDeliveryHourInput> hours) {
- Map<Integer, List<SelfDeliveryHourInput>> byDay = hours.stream()
- .collect(Collectors.groupingBy(SelfDeliveryHourInput::getDayOfWeek));
- for (List<SelfDeliveryHourInput> day : byDay.values()) {
- day.sort(Comparator.comparing(SelfDeliveryHourInput::getStartTime));
- for (int i = 1; i < day.size(); i++) {
- if (day.get(i).getStartTime().compareTo(day.get(i - 1).getEndTime()) < 0) {
- return true;
- }
- }
- }
- return false;
- }
- private static boolean hourValid(SelfDeliveryHourInput h) {
- return h != null && h.getDayOfWeek() != null && h.getDayOfWeek() >= 1 && h.getDayOfWeek() <= 7
- && h.getStartTime() != null && h.getStartTime().matches(TIME_PATTERN)
- && h.getEndTime() != null && h.getEndTime().matches(TIME_PATTERN)
- // 结束必须晚于开始(定长 HH:mm 字符串比较),不支持跨天时段
- && h.getEndTime().compareTo(h.getStartTime()) > 0;
- }
- private ServiceException invalid() {
- return new ServiceException(MessageUtils.message("no.store.selfdelivery.hours.invalid"));
- }
- }
|