2 Commits 12896a34b1 ... 79abe57e14

Auteur SHA1 Message Date
  wangzhijun 79abe57e14 Merge remote-tracking branch 'origin/dev' into dev il y a 1 mois
  wangzhijun baf16086fb 增加商户出发接口,修改商户到达、签到接口 il y a 1 mois

+ 64 - 5
nightFragrance-common/src/main/java/com/ylx/common/utils/PhoneUtils.java

@@ -32,18 +32,77 @@ public class PhoneUtils {
 
         // 2. 格式校验:如果是标准的11位手机号,则进行脱敏
         if (PHONE_PATTERN.matcher(phoneNumber).matches()) {
-            // 方式一:使用正则替换(推荐,简洁优雅)
             return phoneNumber.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
-
-            // 方式二:使用字符串截取(备选方案)
-            // return phoneNumber.substring(0, 3) + "****" + phoneNumber.substring(7);
         }
 
         // 3. 如果不是标准手机号(如座机、海外号码、虚拟号等),可选择返回原值或全部打码
-        // 这里为了安全起见,非标准手机号直接返回原值
         return phoneNumber;
     }
 
+    /**
+     * 手机号高强度脱敏处理(隐藏中间6位)
+     * 规则:保留前3位,中间6位用*替换,保留后2位
+     * 示例:13812345678 -> 138******78
+     *
+     * @param phoneNumber 原始手机号
+     * @return 脱敏后的手机号,若传入为空或格式不对则返回原值
+     */
+    public static String maskPhoneNumberStrict(String phoneNumber) {
+        if (StringUtils.isBlank(phoneNumber)) {
+            return null;
+        }
+        phoneNumber = phoneNumber.trim();
+
+        // 仅对标准11位手机号执行6位脱敏
+        if (PHONE_PATTERN.matcher(phoneNumber).matches()) {
+            // 正则分组:(前3位)(中间6位)(后2位)
+            return phoneNumber.replaceAll("(\\d{3})\\d{6}(\\d{2})", "$1******$2");
+        }
+
+        return phoneNumber;
+    }
+
+    /**
+     * 获取手机号后4位
+     * @param phoneNumber 原始手机号
+     * @return 后4位;空/不足4位则返回null
+     */
+    public static String getPhoneTail4(String phoneNumber) {
+        // 空值校验
+        if (StringUtils.isBlank(phoneNumber)) {
+            return null;
+        }
+        // 去除首尾空格
+        String realPhone = phoneNumber.trim();
+        // 长度不足4位直接返回null
+        if (realPhone.length() < 4) {
+            return null;
+        }
+        // 截取末尾4字符
+        return realPhone.substring(realPhone.length() - 4);
+    }
+
+    /**
+     * 校验手机号后四位是否匹配
+     * 签到场景专用:完整手机号 vs 前端传入4位尾号
+     * @param fullPhone 数据库完整11位手机号
+     * @param inputTail4 用户输入的后4位
+     * @return true=匹配,false=不匹配/手机号无效
+     */
+    public static boolean matchPhoneTail4(String fullPhone, String inputTail4) {
+        // 输入尾号必须是4位数字
+        if (StringUtils.isBlank(inputTail4) || inputTail4.length() != 4 || !StringUtils.isNumeric(inputTail4)) {
+            return false;
+        }
+        String realTail = getPhoneTail4(fullPhone);
+        // 手机号无效(空/不足4位)直接返回不匹配
+        if (StringUtils.isBlank(realTail)) {
+            return false;
+        }
+        // 相等校验
+        return StringUtils.equals(realTail, inputTail4);
+    }
+
     /**
      * 验证是否为合法的手机号格式
      */

+ 7 - 0
nightFragrance-massage/src/main/java/com/ylx/order/controller/MerchantOrderController.java

@@ -58,6 +58,13 @@ public class MerchantOrderController {
         return R.ok(vo);
     }
 
+    @PostMapping("/startGoOut")
+    @ApiOperation("商户出发")
+    public R<Void> startGoOut(@Validated @RequestBody MerchantOrderOperateDTO dto) {
+        this.orderService.startGoOut(dto);
+        return R.ok();
+    }
+
     @PostMapping("/arriveSign")
     @ApiOperation("商户签到到达客户地址")
     public R<Void> orderArriveSign(@Validated @RequestBody MerchantOrderOperateDTO dto) {

+ 3 - 0
nightFragrance-massage/src/main/java/com/ylx/order/domain/dto/MerchantOrderOperateDTO.java

@@ -22,6 +22,9 @@ public class MerchantOrderOperateDTO {
     @ApiModelProperty("短信验证码")
     private String code;
 
+    @ApiModelProperty("手机号后4位")
+    private String phoneTail;
+
     @ApiModelProperty("图片URL列表,最多9张")
     @Size(max = 9, message = "最多只能上传9张图片")
     private List<String> imageUrls;

+ 37 - 0
nightFragrance-massage/src/main/java/com/ylx/order/enums/ExecStatusEnum.java

@@ -0,0 +1,37 @@
+package com.ylx.order.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum ExecStatusEnum {
+
+    WAIT_DEPART(0, "待出发/待签到"),
+    DEPARTED(1, "已出发"),
+    ARRIVED(2, "已到达/已签到");
+
+    private final Integer code;
+    private final String info;
+
+    ExecStatusEnum(Integer code, String info) {
+        this.code = code;
+        this.info = info;
+    }
+
+    public static ExecStatusEnum fromCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (ExecStatusEnum status : values()) {
+            if (status.code.equals(code)) {
+                return status;
+            }
+        }
+        return null;
+    }
+
+    public static String getInfoByCode(Integer code) {
+        ExecStatusEnum status = fromCode(code);
+        return status == null ? "未知" : status.getInfo();
+    }
+
+}

+ 2 - 0
nightFragrance-massage/src/main/java/com/ylx/order/service/TOrderService.java

@@ -83,4 +83,6 @@ public interface TOrderService extends IService<TOrder> {
     MerchantOrderDetailVO getMerchantOrderDetail(MerchantOrderDetailDTO dto);
 
     List<BookingSlotsVO> bookingSlots(BookingSlotsDTO dto);
+
+    void startGoOut(MerchantOrderOperateDTO dto);
 }

+ 85 - 27
nightFragrance-massage/src/main/java/com/ylx/order/service/impl/TOrderServiceImpl.java

@@ -18,6 +18,7 @@ import com.ylx.common.exception.ServiceException;
 import com.ylx.common.utils.*;
 import com.ylx.common.weixinPay.enums.WxPayTypeEnum;
 import com.ylx.common.weixinPay.service.WxPayV3Service;
+import com.ylx.coupon.service.CouponService;
 import com.ylx.fareSetting.service.IMaProjectFareSettingService;
 import com.ylx.massage.domain.MaProject;
 import com.ylx.massage.domain.MaTechnician;
@@ -25,7 +26,6 @@ import com.ylx.massage.domain.TGeoFence;
 import com.ylx.massage.domain.TWxUser;
 import com.ylx.massage.mapper.MaProjectMapper;
 import com.ylx.massage.mapper.MaTechnicianMapper;
-import com.ylx.coupon.service.CouponService;
 import com.ylx.massage.service.IMaTechnicianService;
 import com.ylx.massage.service.TGeoFenceService;
 import com.ylx.massage.service.TWxUserService;
@@ -414,7 +414,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         order.setCouponId(dto.getCouponId());
         order.setCreateTime(DateUtils.getNowDate());
         order.setStatus(OrderStatusEnum.PENDING_PAYMENT.getCode());
-        order.setExecStatus(0);
+        order.setExecStatus(ExecStatusEnum.WAIT_DEPART.getCode());
         order.setDispatchedStatus(DispatchedStatusEnum.UN_DISPATCHED.getCode());
         order.setPaymentMethod(dto.getPaymentMethod());
 
@@ -924,6 +924,16 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     @Transactional(rollbackFor = Exception.class)
     public void startService(MerchantOrderOperateDTO dto) {
 
+        // 校验图片不能为null
+        if (ObjectUtil.isEmpty(dto.getImageUrls())) {
+            throw new ServiceException("请至少上传1张图片");
+        }
+
+        // 再检查是否有空/空白字符串
+        if (dto.getImageUrls().stream().anyMatch(StrUtil::isBlank)) {
+            throw new ServiceException("图片URL列表中不能包含空值或空白字符串");
+        }
+
         // 1. 获取当前登录商户
         WxLoginUser loginUser = getCurrentWxLoginUser();
         Long merchantId = Long.parseLong(loginUser.getId());
@@ -1031,21 +1041,11 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         WxLoginUser loginUser = getCurrentWxLoginUser();
         Long merchantId = Long.parseLong(loginUser.getId());
         Long orderId = dto.getOrderId();
-        String code = dto.getCode();
-        String phone = dto.getPhone();
-
-        // 手机号或者验证码 校验
-        if (ObjectUtil.hasNull(phone, code)) {
-            throw new ServiceException("手机号或者验证码不能为空");
-        }
-
-        if (!validateVerifyCode(phone, code)) {
-            throw new ServiceException("验证码错误");
-        }
+        String phoneTail = dto.getPhoneTail();
 
         // 查询订单
         TOrder order = baseMapper.selectOne(Wrappers.lambdaQuery(TOrder.class)
-                .select(TOrder::getId, TOrder::getMerchantId, TOrder::getStatus)
+                .select(TOrder::getId, TOrder::getMerchantId, TOrder::getExecStatus, TOrder::getContactPhoneNumber)
                 .eq(TOrder::getId, orderId)
                 .eq(TOrder::getMerchantId, merchantId)
                 .eq(TOrder::getIsDelete, NOT_DELETE));
@@ -1053,26 +1053,44 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
             throw new ServiceException("订单不存在或不属于当前商户");
         }
 
-        // 状态仅允许待服务
-        Integer status = order.getStatus();
-        if (!OrderStatusEnum.PENDING_SERVICE.getCode().equals(status)) {
-            throw new ServiceException("仅【待服务】订单可签到到达,当前状态:" + OrderStatusEnum.getInfoByCode(status));
-        }
+        Integer execStatus = order.getExecStatus();
 
-        // 更新到达信息
+        // 更新签到信息、履约信息
         LocalDateTime now = LocalDateTime.now();
         LambdaUpdateWrapper<TOrder> update = Wrappers.lambdaUpdate(TOrder.class)
-                .set(TOrder::getArrivedTime, now)
-                .set(TOrder::getSignTime, now)
                 .set(TOrder::getUpdateTime, now)
                 .set(TOrder::getUpdateBy, loginUser.getCNickName())
-                .set(TOrder::getArrivalPhoto, StrUtil.join(",", dto.getImageUrls()))
-                .eq(TOrder::getId, orderId)
-                .eq(TOrder::getStatus, status);
+                .set(TOrder::getExecStatus, ExecStatusEnum.ARRIVED.getCode())
+                .eq(TOrder::getId, orderId);
+
+        // 自动区分两种业务:phoneTail非空=签到;空=纯到达
+        boolean isSignMode = StrUtil.isNotBlank(phoneTail);
+        if (isSignMode) {
+            log.info("商户上门签到操作,订单ID:{},商户ID:{},客户尾号:{}", orderId, merchantId, phoneTail);
+            if (ObjectUtil.notEqual(ExecStatusEnum.WAIT_DEPART.getCode(), execStatus)) {
+                throw new ServiceException("仅【待出发】订单可签到,当前状态:" + ExecStatusEnum.getInfoByCode(execStatus));
+            }
+            // 上门签到:执行全部手机号校验、状态强限制、写入signTime
+            if (phoneTail.length() != 4 || !StrUtil.isNumeric(phoneTail)) {
+                throw new ServiceException("手机号后4位必须为4位数字");
+            }
+            if (!PhoneUtils.matchPhoneTail4(order.getContactPhoneNumber(), phoneTail)) {
+                throw new ServiceException("手机号后4位不正确");
+            }
+            update.set(TOrder::getSignTime, now);
+        } else {
+            log.info("纯标记订单到达操作,订单ID:{},商户ID:{}", orderId, merchantId);
+            if (ObjectUtil.notEqual(ExecStatusEnum.DEPARTED.getCode(), execStatus)) {
+                throw new ServiceException("仅【待出发】订单可签到,当前状态:" + ExecStatusEnum.getInfoByCode(execStatus));
+            }
+            update.set(TOrder::getArrivedTime, now);
+        }
+
         int count = baseMapper.update(null, update);
         if (count <= 0) {
-            throw new ServiceException("订单状态已变更,请刷新重试");
+            throw new ServiceException("订单履约状态已变更,请刷新重试");
         }
+        log.info("订单到达操作执行完成,订单ID:{}", orderId);
     }
 
     @Override
@@ -1117,7 +1135,7 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         vo.setAppointmentTimeRange(formatTimeRange(order.getAppointmentStartTime(), order.getProjectDuration()));
 
         // 3. 联系信息脱敏处理
-        vo.setContactPhoneNumber(PhoneUtils.maskPhoneNumber(order.getContactPhoneNumber()));
+        vo.setContactPhoneNumber(PhoneUtils.maskPhoneNumberStrict(order.getContactPhoneNumber()));
 
         // 4. 计算距离与告警提示 (依赖地理围栏表 t_geo_fence)
         List<TGeoFence> validFenceList = geoFenceService.selectValidFences(order.getCityCode());
@@ -1188,6 +1206,46 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
         return voList;
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void startGoOut(MerchantOrderOperateDTO dto) {
+
+        // 1. 获取当前登录商户
+        WxLoginUser loginUser = getCurrentWxLoginUser();
+        Long merchantId = Long.parseLong(loginUser.getId());
+        Long orderId = dto.getOrderId();
+
+        // 查询订单
+        TOrder order = baseMapper.selectOne(Wrappers.lambdaQuery(TOrder.class)
+                .select(TOrder::getId, TOrder::getMerchantId, TOrder::getStatus)
+                .eq(TOrder::getId, orderId)
+                .eq(TOrder::getMerchantId, merchantId)
+                .eq(TOrder::getIsDelete, NOT_DELETE));
+        if (ObjectUtil.isNull(order)) {
+            throw new ServiceException("订单不存在或不属于当前商户");
+        }
+
+        // 获取履约状态
+        Integer execStatus = order.getExecStatus();
+        if (!ExecStatusEnum.WAIT_DEPART.getCode().equals(execStatus)) {
+            throw new ServiceException("仅【待出发/待签到】订单可修改为已出发,当前状态:" + ExecStatusEnum.getInfoByCode(execStatus));
+        }
+
+        // 更新出发信息、履约信息
+        LocalDateTime now = LocalDateTime.now();
+        LambdaUpdateWrapper<TOrder> update = Wrappers.lambdaUpdate(TOrder.class)
+                .set(TOrder::getDepartedTime, now)
+                .set(TOrder::getUpdateTime, now)
+                .set(TOrder::getUpdateBy, loginUser.getCNickName())
+                .set(TOrder::getExecStatus, ExecStatusEnum.DEPARTED.getCode())
+                .eq(TOrder::getId, orderId);
+        int count = baseMapper.update(null, update);
+        if (count <= 0) {
+            throw new ServiceException("订单履约状态已变更,请刷新重试");
+        }
+
+    }
+
     /**
      * 判断预约时段是否与已有订单冲突(含30分钟缓冲)
      */