rk
13 小时以前 ab9cd2c82bd64de8e33510db1d1e78a5b3b4de70
代码生成
已添加9个文件
已删除1个文件
已修改27个文件
已重命名1个文件
2065 ■■■■ 文件已修改
server/services/db/db_change.sql 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/biz/system/impl/AreasBizImpl.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/config/alipay/AlipayFundTransUniTransfer.java 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/config/alipay/AlipayProperties.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/config/wx/WxMiniConfig.java 52 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/config/wx/WxMiniUtilService.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/config/wx/WxPayProperties.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/config/wx/WxPayV3Service.java 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/core/constants/Constants.java 168 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/core/utils/aliyun/AliSmsService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/core/utils/geocode/MapUtil.java 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/core/utils/tencent/MapUtil.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/NoticeMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/DriverInfo.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/Notice.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/OrdersRefund.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/ShopInfo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/AlipayTransferDTO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/DriverPickupDTO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/DriverVerifyRequest.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/ShopApplyDTO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/ShopUpdateDTO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/PlatformAboutVO.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/ShopDetailVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/DriverInfoService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/NoticeService.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/DriverInfoServiceImpl.java 203 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/MemberServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/NoticeServiceImpl.java 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/OrdersServiceImpl.java 262 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/ShopInfoServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/WithdrawalOrdersServiceImpl.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/resources/application-dev.yml 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/resources/application-pro.yml 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/resources/application-test.yml 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/ConfigApi.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/DriverInfoApi.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/PaymentCallback.java 169 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/db/db_change.sql
@@ -5,6 +5,20 @@
-- ============================================================
-- 2026/04/20 å•†æˆ·ä¸Žå¸æœºå¢žåŠ æ”¯ä»˜å®å®žåå§“åå­—æ®µ
-- ============================================================
ALTER TABLE `shop_info` ADD COLUMN `ALI_NAME` VARCHAR(50) DEFAULT NULL COMMENT '支付宝实名姓名' AFTER `ALI_ACCOUNT`;
ALTER TABLE `driver_info` ADD COLUMN `ALI_ACCOUNT` VARCHAR(100) DEFAULT NULL COMMENT '支付宝提现账户' AFTER `LATITUDE`;
ALTER TABLE `driver_info` ADD COLUMN `ALI_NAME` VARCHAR(50) DEFAULT NULL COMMENT '支付宝实名姓名' AFTER `ALI_ACCOUNT`;
-- ============================================================
-- 2026/04/20 è®¢å•退款记录表增加退款状态字段
-- ============================================================
ALTER TABLE `orders_refund` ADD COLUMN `STATUS` INT DEFAULT 0 COMMENT '退款状态:0=退款中;1=退款成功;2=退款失败' AFTER `REFUND_REMARK`;
-- ============================================================
-- 2026/04/20 è®¢å•表增加物品级别字段
-- ============================================================
ALTER TABLE `orders` ADD COLUMN `GOOD_LEVEL` INT DEFAULT NULL COMMENT '物品级别(category主键,type=3)' AFTER `GOOD_TYPE`;
server/services/src/main/java/com/doumee/biz/system/impl/AreasBizImpl.java
@@ -1,8 +1,10 @@
package com.doumee.biz.system.impl;
import com.doumee.biz.system.AreasBiz;
import com.doumee.dao.business.AreasMapper;
import com.doumee.dao.business.model.Areas;
import com.doumee.service.business.AreasService;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -16,15 +18,27 @@
public class AreasBizImpl implements AreasBiz {
    @Autowired
    private AreasService areasService;
    private AreasMapper areasMapper;
    @Override
    public Areas resolveArea(Integer areaId) {
        if (areaId == null) {
            return null;
        }
        // cacheData ä¸­å·²ä¸ºæ¯æ¡åŒºåŽ¿è®°å½•å¡«å……äº† provinceId/provinceName/cityId/cityName
        return areasService.findById(areaId);
        return areasMapper.selectJoinOne(Areas.class,new MPJLambdaWrapper<Areas>()
                .selectAll(Areas.class)
                .select(" a1.id ",Areas::getCityId)
                .select(" a1.name ",Areas::getCityName)
                .select(" a2.id ",Areas::getProvinceId)
                .select(" a2.name ",Areas::getProvinceName)
                .leftJoin("areas a1 on a1.id = t.PARENT_ID")
                .leftJoin("areas a2 on a2.id = a1.PARENT_ID")
                .eq(Areas::getId, areaId)
        );
//        if (areaId == null) {
//            return null;
//        }
//        // cacheData ä¸­å·²ä¸ºæ¯æ¡åŒºåŽ¿è®°å½•å¡«å……äº† provinceId/provinceName/cityId/cityName
//        return areasMapper.selectById(areaId);
    }
}
server/services/src/main/java/com/doumee/config/alipay/AlipayFundTransUniTransfer.java
@@ -1,87 +1,108 @@
package com.doumee.config.alipay;
import com.alipay.v3.ApiException;
import com.alipay.v3.ApiClient;
import com.alipay.v3.util.model.AlipayConfig;
import com.alipay.v3.ApiException;
import com.alipay.v3.Configuration;
import com.alipay.v3.api.AlipayFundTransUniApi;
import com.alipay.v3.model.*;
import com.alipay.v3.util.model.AlipayConfig;
import com.doumee.config.mybatis.SpringUtils;
import com.doumee.core.constants.ResponseStatus;
import com.doumee.core.exception.BusinessException;
import com.doumee.dao.dto.AlipayTransferDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
 * æ”¯ä»˜å®å•笔转账工具类
 * @author rk
 * @date 2026/04/20
 */
@Slf4j
@org.springframework.context.annotation.Configuration
public class AlipayFundTransUniTransfer {
    public static void main(String[] args) throws ApiException  {
        // åˆå§‹åŒ–SDK
        ApiClient defaultClient = Configuration.getDefaultApiClient();
        // åˆå§‹åŒ–alipay参数(全局设置一次)
        defaultClient.setAlipayConfig(getAlipayConfig());
        // æž„造请求参数以调用接口
        AlipayFundTransUniApi api = new AlipayFundTransUniApi();
        AlipayFundTransUniTransferModel data = new AlipayFundTransUniTransferModel();
        // è®¾ç½®å•†å®¶ä¾§å”¯ä¸€è®¢å•号
        data.setOutBizNo("202606300001");
        // è®¾ç½®è®¢å•总金额
        data.setTransAmount("1");
        // è®¾ç½®æè¿°ç‰¹å®šçš„业务场景
        data.setBizScene("DIRECT_TRANSFER");
        // è®¾ç½®ä¸šåŠ¡äº§å“ç 
        data.setProductCode("TRANS_ACCOUNT_NO_PWD");
        // è®¾ç½®è½¬è´¦ä¸šåŠ¡çš„æ ‡é¢˜
        data.setOrderTitle("201905代发");
        // è®¾ç½®åŽŸæ”¯ä»˜å®ä¸šåŠ¡å•å·
        data.setOriginalOrderId("20190620110075000006640000063056");
        // è®¾ç½®æ”¶æ¬¾æ–¹ä¿¡æ¯
        Participant payeeInfo = new Participant();
        payeeInfo.setIdentity("15345690849");
        payeeInfo.setName("江萍");
        payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
        data.setPayeeInfo(payeeInfo);
        // è®¾ç½®ä¸šåŠ¡å¤‡æ³¨
        data.setRemark("201905代发");
        // è®¾ç½®è½¬è´¦åœºæ™¯åç§°
        data.setTransferSceneName("佣金报酬");
    private static AlipayConfig alipayConfig;
        // è®¾ç½®è½¬è´¦åœºæ™¯ä¸ŠæŠ¥ä¿¡æ¯
        List<TransferSceneReportInfo> transferSceneReportInfos = new ArrayList<TransferSceneReportInfo>();
        TransferSceneReportInfo transferSceneReportInfos0 = new TransferSceneReportInfo();
        transferSceneReportInfos0.setInfoType("佣金报酬说明");
        transferSceneReportInfos0.setInfoContent("3月家政服务报酬");
        transferSceneReportInfos.add(transferSceneReportInfos0);
        data.setTransferSceneReportInfos(transferSceneReportInfos);
    @Autowired
    private AlipayProperties alipayProperties;
        // è®¾ç½®è½¬è´¦ä¸šåŠ¡è¯·æ±‚çš„æ‰©å±•å‚æ•°
        data.setBusinessParams("{\"payer_show_name_use_alias\":\"true\"}");
    public static AlipayFundTransUniTransfer me() {
        return SpringUtils.get().getBean(AlipayFundTransUniTransfer.class);
    }
       /* // è®¾ç½®ç­¾åä¿¡æ¯
        SignData signData = new SignData();
        signData.setOriSign("EqHFP0z4a9iaQ1ep==");
        signData.setPartnerId("签名被授权方支付宝账号ID");
        signData.setOriAppId("2021000185629012");
        signData.setOriOutBizNo("商户订单号");
        signData.setOriSignType("RSA2");
        signData.setOriCharSet("UTF-8");
        data.setSignData(signData);
*/
    @PostConstruct
    void init() {
        this.loadAlipayConfig();
    }
    public void loadAlipayConfig() {
        try {
            AlipayFundTransUniTransferResponseModel response = api.transfer(data);
        } catch (ApiException e) {
            AlipayFundTransUniTransferDefaultResponse errorObject = (AlipayFundTransUniTransferDefaultResponse) e.getErrorObject();
            System.out.println("调用失败:" + errorObject);
            AlipayConfig config = new AlipayConfig();
            config.setServerUrl("https://openapi.alipay.com");
            config.setAppId(StringUtils.trimToNull(alipayProperties.getAppId()));
            config.setPrivateKey(StringUtils.trimToNull(alipayProperties.getPrivateKey()));
            config.setAppCertPath(StringUtils.trimToNull(alipayProperties.getAppCertPath()));
            config.setAlipayPublicCertPath(StringUtils.trimToNull(alipayProperties.getAlipayPublicCertPath()));
            config.setRootCertPath(StringUtils.trimToNull(alipayProperties.getRootCertPath()));
            alipayConfig = config;
            log.info("支付宝配置初始化成功, appId={}", config.getAppId());
        } catch (Exception e) {
            log.error("支付宝配置初始化失败: {}", e.getMessage(), e);
        }
    }
    private static AlipayConfig getAlipayConfig() {
        AlipayConfig alipayConfig = new AlipayConfig();
        alipayConfig.setServerUrl("https://openapi.alipay.com");
        alipayConfig.setAppId("2021006147660139");
        String privateKey  = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDEeqqkZztBHfK+vpyBi6ejgDTHZuZ3yiuXds+lRBbMo/g24F5trH+oLHW0gMhSxihFfQBBIpVBXDsPQK4ZkhDWTaOBktnU1UMRoOEiaaZU6EiWy10ePFUmpdXpkCQEp7rc88OwI90p58S3+L+Ckak60WqNwJBdB5YGBaUunryBA78U4zW1KNY7JvoRnZDcFMQiczikwUzhO7EAi0yVrVpsVsc9s87mcS4uOJKx4qb2E83r9RJ1z30db+cIIZRiLP2oNZBLYzgKpOouE+uIgxhQzlh6cOASNZQulXuUjoT/+Y9w4njfl4TmKIXWcJFKIMc6kMiux9tTncpp0TqRwk1tAgMBAAECggEALkSYtJheusnbpRFr95G0i2sggqh3s1PXihZ/dXKgT9Z5GCsj8X3Cng7CNRxykBN73kk+axhCv56Bhej8Vqcv8ddcnqG/TEBgR+Fzws/QTIRau6/uILWic7RvuE2qPbJl7aw1s9/uL/UVPSGFr7CvgltYVUM4e7/Sk1529JCK4XJfoXP5tKJ3OaXssvaFnCKEU8IGQkjRG+lUZJhAHVtClGHtgrhevgRhy2zre5wp2qSa8d/MqrPruSYS02hn9b5Nl6i2PlUS6dGlJ4lrxYTG22ukYYoxAPNPS7gnvmveXonWP7b5tPhKRpZjnoySojz3WECUlhz/v8wM1cDrpq+GQQKBgQDsc7y2rlx4f77a7ORfb5/qWHCOJs1cIzggj0kJ7TgFGe71kbCQ5nywD/Fe5V9OwbW+DCxOME+SrrHeiK4axWiu5si/1JlurJoxNy+4k4ywk3ZA3Nv2aBhlPqfkwDhJ0z7Mgsq2c/YgnVddmSvKZoC39wA77ovks4GDxaBOt8N7PQKBgQDUuPGgzkwcgb60UdaxfMbacrPsW26vDxaE4ceuXo2m8KDiCIqkF2y9r6AdWMTgGGSJwOsk7+FP+21VdRivCg9HcOLWngveUc6xDIuqKHVpemMo3SdCF4Wqf96rRc3VOBr5cfIdWxeorZf5umMyKnIAjAFETOOrK7eLTTmjyLD98QKBgB82S+Plcklpu3zUpnS+nGJn2Du7fYI7F+6cW2zXBn0N5lA+Mgt+kVkAUcFQD9uqkF4M51BO6kIXk10nt6vLAT2NM1S3MKW+XQBAI6l+uKSaYpK/VL3bEdVThwAYK5X7L5/5Z97bwdKeUmkFjhVCoJ0oGrzOiWLgGymUzct2UHSVAoGBAMb+7Cs+Ub0pMrmFBY6r52pbey1Uq0pglvRgMmhQU7sjx50r2GaA81zPer15WVM5/nNPYaoALYqg7jrPe/PjOT/fvpR+7SNg7DZ8QftANfYiY7jKifst/gDt9ePLPS6FedZ4XcJQgOVu34jicAFx64vPbS/zrddm4iEScSVijRBBAoGAXCheERsx8+n16Us/DttXFUa1nc7+D8WR6buM1QMZgQCVF2qp3XtM+FusCKL4+q1+dtag8svLjJFp9QbaAXqX8Zk7rn8wUHbDloPTPy9XWgrPowyL9MPU+e/Rq8Hr6TWPDBd4TU64YzIEfBQYpJXfZbXhVYmK3o7xHXKB1x4vvEM=";
        alipayConfig.setPrivateKey(privateKey);
        alipayConfig.setAppCertPath("pay/pro/appCertPublicKey.crt");
        alipayConfig.setAlipayPublicCertPath("pay/pro/alipayCertPublicKey_RSA2.crt");
        alipayConfig.setRootCertPath("pay/pro/alipayRootCert.crt");
    /**
     * å•笔转账到支付宝账户
     *
     * @param dto è½¬è´¦å‚æ•°
     * @return æ”¯ä»˜å®è½¬è´¦å•号
     */
    public static String transfer(AlipayTransferDTO dto) throws ApiException {
        ApiClient defaultClient = Configuration.getDefaultApiClient();
        defaultClient.setAlipayConfig(alipayConfig);
        AlipayFundTransUniApi api = new AlipayFundTransUniApi();
        AlipayFundTransUniTransferModel data = new AlipayFundTransUniTransferModel();
        data.setOutBizNo(dto.getOutBizNo());
        data.setTransAmount(dto.getTransAmount().setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString());
        data.setBizScene("DIRECT_TRANSFER");
        data.setProductCode("TRANS_ACCOUNT_NO_PWD");
        data.setOrderTitle("佣金提现");
        return alipayConfig;
        Participant payeeInfo = new Participant();
        payeeInfo.setIdentity(dto.getPayeeAccount());
        payeeInfo.setName(dto.getPayeeName());
        payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
        data.setPayeeInfo(payeeInfo);
        data.setRemark(dto.getRemark());
        data.setTransferSceneName("佣金报酬");
        List<TransferSceneReportInfo> reportInfos = new ArrayList<>();
        TransferSceneReportInfo reportInfo = new TransferSceneReportInfo();
        reportInfo.setInfoType("佣金报酬说明");
        reportInfo.setInfoContent("佣金提现");
        reportInfos.add(reportInfo);
        data.setTransferSceneReportInfos(reportInfos);
        data.setBusinessParams("{\"payer_show_name_use_alias\":\"true\"}");
        try {
            AlipayFundTransUniTransferResponseModel response = api.transfer(data);
            log.info("支付宝转账成功, outBizNo={}, orderId={}", dto.getOutBizNo(), response.getOrderId());
            return response.getOrderId();
        } catch (ApiException e) {
            AlipayFundTransUniTransferDefaultResponse errorObject =
                    (AlipayFundTransUniTransferDefaultResponse) e.getErrorObject();
            log.error("支付宝转账失败, outBizNo={}, error={}", dto.getOutBizNo(), errorObject);
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(),
                    "转账失败:" + (errorObject != null ? errorObject.toString() : e.getMessage()));
        }
    }
}
}
server/services/src/main/java/com/doumee/config/alipay/AlipayProperties.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package com.doumee.config.alipay;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * æ”¯ä»˜å®é…ç½®å±žæ€§
 * @author rk
 * @date 2026/04/20
 */
@Component
@ConfigurationProperties(prefix = "alipay.pay")
@Data
public class AlipayProperties {
    /**
     * åº”用ID
     */
    private String appId;
    /**
     * åº”用私钥
     */
    private String privateKey;
    /**
     * åº”用公钥证书路径
     */
    private String appCertPath;
    /**
     * æ”¯ä»˜å®å…¬é’¥è¯ä¹¦è·¯å¾„
     */
    private String alipayPublicCertPath;
    /**
     * æ”¯ä»˜å®æ ¹è¯ä¹¦è·¯å¾„
     */
    private String rootCertPath;
}
server/services/src/main/java/com/doumee/config/wx/WxMiniConfig.java
@@ -3,9 +3,7 @@
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
import com.doumee.biz.system.SystemDictDataBiz;
import com.doumee.config.mybatis.SpringUtils;
import com.doumee.core.constants.Constants;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.service.WxPayService;
@@ -13,12 +11,11 @@
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.RSAPublicKeyConfig;
import com.wechat.pay.java.core.http.HostName;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RSAPublicKeyNotificationConfig;
import com.wechat.pay.java.service.billdownload.BillDownloadService;
import com.wechat.pay.java.service.partnerpayments.jsapi.JsapiService;
import com.wechat.pay.java.service.partnerpayments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.refund.RefundService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
@@ -28,15 +25,20 @@
/**
 * å¾®ä¿¡å°ç¨‹åºç»„ä»¶
 */
@Slf4j
@Configuration
public class WxMiniConfig {
    /********微信小程序服务**********/
    public static WxMaService wxMaService;
    /********微信小程序支付**********/
    /********微信小程序支付 V2**********/
    public static WxPayService wxPayService;
    /********微信APP支付**********/
    public static WxPayService wxAppPayService;
    /********微信支付 V3**********/
    public static JsapiServiceExtension v3JsapiService;
    public static RefundService v3RefundService;
    public static NotificationParser v3NotificationParser;
    @Autowired
    private WxPayProperties wxPayProperties;
@@ -49,6 +51,7 @@
    void init() {
        this.load_WxMaService();
        this.load_wxPayService();
        this.load_wxPayV3Service();
//        this.load_wxAppPayService();
    }
    /**
@@ -59,15 +62,13 @@
        config.setAppid(StringUtils.trimToNull(wxPayProperties.getAppId()));
        config.setSecret(StringUtils.trimToNull(wxPayProperties.getAppSecret()));
        config.setMsgDataFormat("JSON");
        //config.setToken("");
        //config.setAesKey("");
        WxMaService wxMaService = new WxMaServiceImpl();
        wxMaService.setWxMaConfig(config);
        this.wxMaService = wxMaService;
    }
    /**
     * åˆå§‹åŒ–微信小程序支付
     * åˆå§‹åŒ–微信小程序支付 V2
     */
    public void load_wxPayService() {
        WxPayConfig payConfig = new WxPayConfig();
@@ -83,8 +84,39 @@
        this.wxPayService = wxPayService;
    }
    /**
     * åˆå§‹åŒ–微信支付 V3(JSAPI + é€€æ¬¾ + å›žè°ƒéªŒç­¾ï¼‰
     */
    public void load_wxPayV3Service() {
        try {
            Config config =
                    new RSAPublicKeyConfig.Builder()
                            .merchantId(wxPayProperties.getMchId()) //微信支付的商户号
                            .privateKeyFromPath(wxPayProperties.getPrivateKeyPath()) // å•†æˆ·API证书私钥的存放路径
                            .merchantSerialNumber(wxPayProperties.getSerialNumer()) //商户API证书序列号
                            .publicKeyFromPath(wxPayProperties.getPubKeyPath()) //微信支付公钥的存放路径
                            .publicKeyId(wxPayProperties.getPublicKeyId()) //微信支付公钥ID
                            .apiV3Key(wxPayProperties.getApiV3Key()) //APIv3密钥
                            .build();
            // æ”¯ä»˜å…¬é’¥é…ç½®ï¼ˆç”¨äºŽå›žè°ƒéªŒç­¾ï¼‰
            RSAPublicKeyNotificationConfig notifyConfig = new RSAPublicKeyNotificationConfig.Builder()
                    .publicKeyFromPath(wxPayProperties.getPubKeyPath())
                    .publicKeyId(wxPayProperties.getPublicKeyId())
                    .apiV3Key(wxPayProperties.getApiV3Key())
                    .build();
            v3JsapiService = new JsapiServiceExtension.Builder().config(config).build();
            v3RefundService = new RefundService.Builder().config(config).build();
            v3NotificationParser = new NotificationParser(notifyConfig);
            log.info("微信支付V3初始化成功");
        } catch (Exception e) {
            log.error("微信支付V3初始化失败: {}", e.getMessage(), e);
        }
    }
//    /**
//     * åˆå§‹åŒ–App支付
//     */
server/services/src/main/java/com/doumee/config/wx/WxMiniUtilService.java
ÎļþÒÑɾ³ý
server/services/src/main/java/com/doumee/config/wx/WxPayProperties.java
@@ -42,6 +42,16 @@
    private String notifyUrl;
    /**
     * V3支付回调地址
     */
    private String v3NotifyUrl;
    /**
     * V3退款回调地址
     */
    private String v3RefundNotifyUrl;
    /**
     * æ”¯ä»˜è¯ä¹¦(p12)
     */
    private String keyPath;
@@ -76,13 +86,15 @@
     */
    private String privateCertPath;
    /**
     * æ”¯ä»˜key
     */
    private String privateKeyPath;
    /**
     * å¾®ä¿¡æ”¯ä»˜å…¬é’¥ID
     */
    private String publicKeyId;
}
server/services/src/main/java/com/doumee/config/wx/WxPayV3Service.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,186 @@
package com.doumee.config.wx;
import com.doumee.core.constants.ResponseStatus;
import com.doumee.core.exception.BusinessException;
import com.doumee.core.utils.ID;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
import com.wechat.pay.java.service.refund.model.AmountReq;
import com.wechat.pay.java.service.refund.model.CreateRequest;
import com.wechat.pay.java.service.refund.model.Refund;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
 * å¾®ä¿¡æ”¯ä»˜V3版本服务(JSAPI下单 + é€€æ¬¾ + å›žè°ƒéªŒç­¾ï¼‰
 *
 * @author rk
 * @date 2026/04/20
 */
@Service
@Slf4j
public class WxPayV3Service {
    @Autowired
    private WxPayProperties wxPayProperties;
    /**
     * JSAPI下单,返回前端调起支付所需的参数
     *
     * @param outTradeNo  å•†æˆ·è®¢å•号
     * @param description å•†å“æè¿°
     * @param totalCents  é‡‘额(分)
     * @param openid      ç”¨æˆ·openid
     * @param notifyUrl   æ”¯ä»˜å›žè°ƒåœ°å€
     * @param attach      é™„加数据(回调时原样返回,用于路由不同业务)
     * @return å‰ç«¯è°ƒèµ·æ”¯ä»˜å‚数(appId, timeStamp, nonceStr, package, signType, paySign)
     */
    public Map<String, String> createOrder(String outTradeNo, String description,
                                            Long totalCents, String openid,
                                            String notifyUrl, String attach) {
        if (WxMiniConfig.v3JsapiService == null) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "微信支付V3未初始化");
        }
        try {
            PrepayRequest request = new PrepayRequest();
            request.setAppid(wxPayProperties.getAppId());
            request.setMchid(wxPayProperties.getMchId());
            request.setDescription(description);
            request.setOutTradeNo(outTradeNo);
            request.setNotifyUrl(notifyUrl);
            if (attach != null && !attach.isEmpty()) {
                request.setAttach(attach);
            }
            com.wechat.pay.java.service.payments.jsapi.model.Amount amount =
                    new com.wechat.pay.java.service.payments.jsapi.model.Amount();
            amount.setTotal(1);//totalCents.intValue());
            amount.setCurrency("CNY");
            request.setAmount(amount);
            com.wechat.pay.java.service.payments.jsapi.model.Payer payer =
                    new com.wechat.pay.java.service.payments.jsapi.model.Payer();
            payer.setOpenid(openid);
            request.setPayer(payer);
            PrepayWithRequestPaymentResponse response =
                    WxMiniConfig.v3JsapiService.prepayWithRequestPayment(request);
            Map<String, String> result = new HashMap<>();
            result.put("appId", response.getAppId());
            result.put("timeStamp", response.getTimeStamp());
            result.put("nonceStr", response.getNonceStr());
            result.put("package", response.getPackageVal());
            result.put("signType", response.getSignType());
            result.put("paySign", response.getPaySign());
            return result;
        } catch (Exception e) {
            log.error("微信支付V3下单失败: {}", e.getMessage(), e);
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "支付调起失败:" + e.getMessage());
        }
    }
    /**
     * V3退款
     *
     * @param outTradeNo   å•†æˆ·è®¢å•号
     * @param totalCents   åŽŸè®¢å•é‡‘é¢ï¼ˆåˆ†ï¼‰
     * @param refundCents  é€€æ¬¾é‡‘额(分)
     * @param reason       é€€æ¬¾åŽŸå› 
     * @param notifyUrl    é€€æ¬¾å›žè°ƒåœ°å€
     * @return Refund é€€æ¬¾ç»“果(包含 outRefundNo、status ç­‰ï¼‰
     */
    public Refund refund(String outTradeNo, Long totalCents, Long refundCents,
                         String reason, String notifyUrl) {
        if (WxMiniConfig.v3RefundService == null) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "微信支付V3未初始化");
        }
        try {
            String outRefundNo = ID.nextGUID();
            CreateRequest request = new CreateRequest();
            request.setOutTradeNo(outTradeNo);
            request.setOutRefundNo(outRefundNo);
            request.setReason(reason);
            request.setNotifyUrl(notifyUrl);
            AmountReq amount = new AmountReq();
            amount.setRefund(1L);//refundCents);
            amount.setTotal(1L);//totalCents);
            amount.setCurrency("CNY");
            request.setAmount(amount);
            Refund result = WxMiniConfig.v3RefundService.create(request);
            log.info("微信支付V3退款申请结果, outTradeNo={}, outRefundNo={}, status={}",
                    outTradeNo, outRefundNo, result.getStatus());
            return result;
        } catch (Exception e) {
            log.error("微信支付V3退款失败: {}", e.getMessage(), e);
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "退款失败:" + e.getMessage());
        }
    }
    /**
     * è§£æžV3支付回调通知
     *
     * @param serialNumber  è¯ä¹¦åºåˆ—号(Wechatpay-Serial header)
     * @param timestamp     æ—¶é—´æˆ³ï¼ˆWechatpay-Timestamp header)
     * @param nonce         éšæœºä¸²ï¼ˆWechatpay-Nonce header)
     * @param signature     ç­¾åï¼ˆWechatpay-Signature header)
     * @param body          è¯·æ±‚体JSON
     * @return è§£æžåŽçš„Transaction对象
     */
    public com.wechat.pay.java.service.payments.model.Transaction parsePayNotify(
            String serialNumber, String timestamp, String nonce,
            String signature, String body) {
        try {
            RequestParam requestParam = new RequestParam.Builder()
                    .serialNumber(serialNumber)
                    .timestamp(timestamp)
                    .nonce(nonce)
                    .signature(signature)
                    .body(body)
                    .build();
            return WxMiniConfig.v3NotificationParser.parse(requestParam,
                    com.wechat.pay.java.service.payments.model.Transaction.class);
        } catch (Exception e) {
            log.error("微信支付V3回调验签失败: {}", e.getMessage(), e);
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "回调验签失败");
        }
    }
    /**
     * è§£æžV3退款回调通知
     *
     * @param serialNumber  è¯ä¹¦åºåˆ—号
     * @param timestamp     æ—¶é—´æˆ³
     * @param nonce         éšæœºä¸²
     * @param signature     ç­¾å
     * @param body          è¯·æ±‚体JSON
     * @return è§£æžåŽçš„RefundNotification对象
     */
    public com.wechat.pay.java.service.refund.model.RefundNotification parseRefundNotify(
            String serialNumber, String timestamp, String nonce,
            String signature, String body) {
        try {
            RequestParam requestParam = new RequestParam.Builder()
                    .serialNumber(serialNumber)
                    .timestamp(timestamp)
                    .nonce(nonce)
                    .signature(signature)
                    .body(body)
                    .build();
            return WxMiniConfig.v3NotificationParser.parse(requestParam,
                    com.wechat.pay.java.service.refund.model.RefundNotification.class);
        } catch (Exception e) {
            log.error("微信支付V3退款回调验签失败: {}", e.getMessage(), e);
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "退款回调验签失败");
        }
    }
}
server/services/src/main/java/com/doumee/core/constants/Constants.java
@@ -101,6 +101,7 @@
    public static final int ORDER_LOG_ASSIGN_DRIVER = 3;    // æŒ‡æ´¾å¸æœº
    public static final int ORDER_LOG_CANCEL = 4;           // å–消订单
    public static final int ORDER_LOG_CONFIRM_ARRIVE = 5;   // ç¡®è®¤é¡¾å®¢åˆ°åº—
    public static final int ORDER_LOG_DRIVER_PICKUP = 6;    // å¸æœºå®Œæˆå–ä»¶
    public static final String SUCCESS = "SUCCESS";
    public static final String FAIL = "FAIL";
@@ -276,97 +277,6 @@
    public enum WorkOrderStatus{
        waitConfirm( 0, "待分配WTS","{title}上报","","待分配WTS" ),
        waitAllocation(1, "待分配任务","待分配任务","","待分配任务"),
        waitDeal(2, "待处理","待处理","","待工程师处理"),
        sheClose(3, "已解决","SHE已关闭","","SHE已关闭"),
        wtsClose(4, "已解决","WTS已关闭","","WTS已关闭"),
        close(5, "已解决","已解决","","工程师关闭"),
        urge (6, "催促","","","")
        ;
        private int status;
        private String statusInfo;
        private String statusName;
        private String logTitle;
        private String noticeContent;
        // æž„造方法
        WorkOrderStatus(int status, String statusInfo,String logTitle,String noticeContent,String statusName ) {
            this.status = status;
            this.statusInfo = statusInfo;
            this.logTitle = logTitle;
            this.noticeContent = noticeContent;
            this.statusName = statusName;
        }
        public static String getName(int index) {
            for (WorkOrderStatus c : WorkOrderStatus.values()) {
                if (c.getKey() == index) {
                    return c.statusInfo;
                }
            }
            return null;
        }
        public static String getStatusName(int index) {
            for (WorkOrderStatus c : WorkOrderStatus.values()) {
                if (c.getKey() == index) {
                    return c.statusName;
                }
            }
            return null;
        }
        public int getKey() {
            return status;
        }
        public void setKey(int key) {
            this.status = status;
        }
        public int getStatus() {
            return status;
        }
        public void setStatus(int status) {
            this.status = status;
        }
        public String getStatusName() {
            return statusName;
        }
        public void setStatusName(String statusName) {
            this.statusName = statusName;
        }
        public String getStatusInfo() {
            return statusInfo;
        }
        public void setStatusInfo(String statusInfo) {
            this.statusInfo = statusInfo;
        }
        public String getLogTitle() {
            return logTitle;
        }
        public void setLogTitle(String logTitle) {
            this.logTitle = logTitle;
        }
        public String getNoticeContent() {
            return noticeContent;
        }
        public void setNoticeContent(String noticeContent) {
            this.noticeContent = noticeContent;
        }
    }
    /**
     * é™„件类型(对应 multifile.objType)
     */
@@ -497,6 +407,82 @@
    }
    /**
     * è®¢å•站内信通知枚举
     * title: é€šçŸ¥æ ‡é¢˜
     * content: é€šçŸ¥æ–‡æ¡ˆæ¨¡æ¿ï¼Œå ä½ç¬¦ç”¨ {xxx} è¡¨ç¤º
     */
    @Getter
    @AllArgsConstructor
    public enum MemberOrderNotify {
        WAIT_PAY("waitPay", "订单待支付", "您的行李订单:{orderNo}已创建,请在{timeout}分钟内完成支付,超时将自动取消"),
        WAIT_VERIFY("waitVerify", "订单待核验", "您的行李订单:{orderNo}已提交,等待门店核验物品信息,存件码{storeCode}"),
        WAIT_GRAB("waitGrab", "订单待抢单", "您的行李订单:{orderNo}已核验,正在为您安排取件司机"),
        WAIT_PICKUP_REMIND("waitPickupRemind", "订单待取件", "订单{orderNo}行李已寄存,请记得在预约取件时间凭取件码前往指定门店取回"),
        WAIT_PICKUP_GRABBED("waitPickupGrabbed", "订单已抢单", "您的行李订单:已有司机{driverName}抢单,正前往取件地点"),
        DELIVERING("delivering", "订单配送中", "您的行李订单:{orderNo}已由司机{driverName}取件,正运往目的地"),
        ARRIVED_NO_SHOP("arrivedNoShop", "订单已送达", "您的行李订单:{orderNo}已送达{destination},请及时确认收货"),
        ARRIVED_HAS_SHOP("arrivedHasShop", "订单已送达", "您的行李订单:{orderNo}已送达{destination},请及时取件,取件码{pickupCode}"),
        FINISHED("finished", "订单已完成", "您的行李订单:{orderNo}已完成,感谢您的支持,请对本次服务做出评价"),
        EVALUATED("evaluated", "订单已评价", "您的行李订单:{orderNo}评价已提交,感谢您的用心评价,祝您出行顺利,旅途愉快!"),
        CANCELLED("cancelled", "订单已取消", "您的行李订单:{orderNo}已取消,感谢您的支持,欢迎下次再会!"),
        DRIVER_CHANGED("driverChanged", "司机变更提醒", "您的行李订单:{orderNo}原司机已取消,系统正在为您匹配新司机,请留意通知。"),
        REFUNDING("refunding", "订单退款中", "您的行李订单:{orderNo}退款申请已提交,平台会将尽快为您处理退款"),
        REFUNDED("refunded", "订单已退款", "您的行李订单:{orderNo}退款已完成,金额{amount}元将原路退回,请注意查收")
        ;
        private final String key;
        private final String title;
        private final String content;
        /**
         * æ ¼å¼åŒ–通知内容
         * @param params é”®å€¼å¯¹ï¼Œå¦‚ "orderNo","123" äº¤æ›¿ä¼ å…¥
         */
        public String format(String... params) {
            String result = this.content;
            for (int i = 0; i < params.length - 1; i += 2) {
                result = result.replace("{" + params[i] + "}", params[i + 1]);
            }
            return result;
        }
    }
    /**
     * é—¨åº—订单站内信通知枚举
     * title: é€šçŸ¥æ ‡é¢˜
     * content: é€šçŸ¥æ–‡æ¡ˆæ¨¡æ¿ï¼Œå ä½ç¬¦ç”¨ {xxx} è¡¨ç¤º
     */
    @Getter
    @AllArgsConstructor
    public enum ShopOrderNotify {
        WAIT_VERIFY("waitVerify", "订单待核验", "新行李订单:{orderNo}已支付,请尽快核验用户物品信息"),
        WAIT_PICKUP("waitPickup", "订单待取件", "行李订单:{orderNo}已抢单,等待{name}取件"),
        REFUNDING("refunding", "订单退款中", "行李订单:{orderNo}用户提交退款申请,请知悉"),
        DELIVERING("delivering", "订单配送中", "行李订单:{orderNo}已由司机{driverName}取件,正在配送中"),
        ARRIVED("arrived", "已送达", "行李订单:{orderNo}已送达{destination},请联系用户确认签收"),
        FINISHED("finished", "订单已完成", "行李订单:{orderNo}已完成,相关订单结算会在{settleDays}个工作日内完成"),
        EVALUATED("evaluated", "订单已评价", "行李订单:{orderNo}用户已完成评价,可查看评价内容"),
        SETTLED("settled", "订单结算", "行李订单:{orderNo}平台已完成结算,金额为{amount}元,请注意查收。")
        ;
        private final String key;
        private final String title;
        private final String content;
        /**
         * æ ¼å¼åŒ–通知内容
         * @param params é”®å€¼å¯¹ï¼Œå¦‚ "orderNo","123" äº¤æ›¿ä¼ å…¥
         */
        public String format(String... params) {
            String result = this.content;
            for (int i = 0; i < params.length - 1; i += 2) {
                result = result.replace("{" + params[i] + "}", params[i + 1]);
            }
            return result;
        }
    }
    /**
     * å¾—到request对象
     *
     * @return
server/services/src/main/java/com/doumee/core/utils/aliyun/AliSmsService.java
@@ -24,17 +24,15 @@
@Slf4j
public class AliSmsService {
    private final static String ACCESS_KEY_ID = "LTAI5tMkg7wwV74a8H6Bm3Ej";
    private final static String ACCESS_KEY_SECRET = "FcHKST36sfwfo706L6bvrweGFIbp3n";
    private final static String SING_NAME = "橙桔天下科技";
    private final static String ACCESS_KEY_ID = "LTAI5t835zTU4aaYpGHJCccJ";
    private final static String ACCESS_KEY_SECRET = "98sAF2NchWVuIzu62zcLq0Ns7LIQTp";
    private final static String SING_NAME = "南京三只鹤";
    public static void main(String[] args) {
        Map<String,Object> tempParam = new java.util.HashMap<>();
        tempParam.put("order","葡萄采摘工");
        tempParam.put("time1","07-23");
        tempParam.put("time2","07-24");
        AliSmsService.sendSms("18055151023","SMS_491055243", JSONObject.toJSONString(tempParam));
        tempParam.put("code","1234");
        AliSmsService.sendSms("15345690849","SMS_333770877", JSONObject.toJSONString(tempParam));
    }
server/services/src/main/java/com/doumee/core/utils/geocode/MapUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,168 @@
package com.doumee.core.utils.geocode;
import com.alibaba.fastjson.JSONObject;
import com.doumee.core.utils.Http;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URLEncoder;
/**
 * é«˜å¾·åœ°å›¾å·¥å…·ç±»
 *
 * @Author : Rk
 * @create 2026/4/20
 */
@Slf4j
@Component
public class MapUtil {
    private static String amapKey;
    /** é€†åœ°ç†è§£æž */
    private static final String GEO_URL = "https://restapi.amap.com/v3/geocode/regeo";
    /** é©¾è½¦è·¯å¾„规划 */
    private static final String DRIVING_URL = "https://restapi.amap.com/v3/direction/driving";
    /** éª‘行路径规划 */
    private static final String BICYCLING_URL = "https://restapi.amap.com/v4/direction/bicycling";
    @Value("${geocode_map_key:}")
    public void setAmapKey(String amapKey) {
        MapUtil.amapKey = amapKey;
    }
    /**
     * é€†åœ°ç†è§£æž - æ ¹æ®ç»çº¬åº¦èŽ·å–åœ°å€ä¿¡æ¯
     * é«˜å¾·åæ ‡ç³»ä¸º lng,lat(与腾讯 lat,lng ç›¸åï¼‰
     *
     * @param lat çº¬åº¦
     * @param lng ç»åº¦
     * @return åŒ…含 ad_info åŸŽå¸‚信息的 JSONObject(兼容腾讯返回格式)
     */
    public static JSONObject reverseGeocode(double lat, double lng) {
        try {
            String url = GEO_URL
                    + "?key=" + amapKey
                    + "&location=" + lng + "," + lat;
            log.info("高德地图逆地理解析请求: lat={}, lng={}", lat, lng);
            JSONObject json = new Http().build(url)
                    .setConnectTimeout(5000)
                    .setReadTimeout(10000)
                    .get()
                    .toJSONObject();
            log.info("高德地图逆地理解析响应: {}", json);
            if (!"1".equals(json.getString("status"))) {
                throw new RuntimeException("高德地图逆地理解析失败: " + json.getString("info"));
            }
            // è½¬æ¢ä¸ºå…¼å®¹è…¾è®¯è¿”回格式: result.ad_info.city / adcode / district
            JSONObject regeocode = json.getJSONObject("regeocode");
            JSONObject addressComponent = regeocode.getJSONObject("addressComponent");
            JSONObject adInfo = new JSONObject();
            adInfo.put("adcode", addressComponent.getString("adcode"));
            adInfo.put("city", addressComponent.getString("city"));
            adInfo.put("district", addressComponent.getString("district"));
            adInfo.put("province", addressComponent.getString("province"));
            adInfo.put("nation", addressComponent.getString("country"));
            JSONObject result = new JSONObject();
            result.put("ad_info", adInfo);
            result.put("formatted_addresses", regeocode.getString("formatted_address"));
            return result;
        } catch (IOException e) {
            log.error("高德地图逆地理解析异常", e);
            throw new RuntimeException("高德地图逆地理解析异常", e);
        }
    }
    /**
     * åˆ¤æ–­ä¸¤ä¸ªç»çº¬åº¦æ˜¯å¦åœ¨åŒä¸€ä¸ªåŸŽå¸‚
     */
    public static boolean isSameCity(double lat1, double lng1, double lat2, double lng2) {
        JSONObject result1 = reverseGeocode(lat1, lng1);
        JSONObject result2 = reverseGeocode(lat2, lng2);
        String city1 = result1.getJSONObject("ad_info").getString("city");
        String city2 = result2.getJSONObject("ad_info").getString("city");
        log.info("判断同城: ({},{}) => city={}, ({},{}) => city={}", lat1, lng1, city1, lat2, lng2, city2);
        return city1 != null && city1.equals(city2);
    }
    /**
     * è·¯å¾„规划(统一入口)
     * å†…部根据 mode è°ƒç”¨é«˜å¾·ä¸åŒçš„路径规划接口
     *
     * @param mode æ¨¡å¼ï¼šdriving(驾车)、bicycling(骑行)
     * @param from èµ·ç‚¹ï¼Œæ ¼å¼ï¼šlat,lng
     * @param to   ç»ˆç‚¹ï¼Œæ ¼å¼ï¼šlat,lng
     * @return JSONObject åŒ…含 distance(ç±³) å’Œ duration(秒)
     */
    public static JSONObject direction(String mode, String from, String to) {
        // é«˜å¾·åæ ‡ç³»ä¸º lng,lat
        String[] fromArr = from.split(",");
        String[] toArr = to.split(",");
        String origin = fromArr[1] + "," + fromArr[0];   // lng,lat
        String destination = toArr[1] + "," + toArr[0];  // lng,lat
        try {
            String url;
            if ("bicycling".equals(mode)) {
                url = BICYCLING_URL
                        + "?key=" + amapKey
                        + "&origin=" + URLEncoder.encode(origin, "UTF-8")
                        + "&destination=" + URLEncoder.encode(destination, "UTF-8");
            } else {
                // é»˜è®¤é©¾è½¦
                url = DRIVING_URL
                        + "?key=" + amapKey
                        + "&origin=" + URLEncoder.encode(origin, "UTF-8")
                        + "&destination=" + URLEncoder.encode(destination, "UTF-8");
            }
            log.info("高德地图路径规划请求: mode={}, from={}, to={}", mode, from, to);
            JSONObject json = new Http().build(url)
                    .setConnectTimeout(5000)
                    .setReadTimeout(10000)
                    .get()
                    .toJSONObject();
            log.info("高德地图路径规划响应: {}", json);
            if (!"1".equals(json.getString("status"))) {
                throw new RuntimeException("高德地图路径规划失败: " + json.getString("info"));
            }
            // æå–第一条路径的 distance å’Œ duration
            JSONObject routeData;
            if ("bicycling".equals(mode)) {
                routeData = json.getJSONObject("data");
            } else {
                routeData = json.getJSONObject("route");
            }
            JSONObject path = routeData.getJSONArray("paths").getJSONObject(0);
            long distance = path.getLongValue("distance");
            long duration = path.getLongValue("duration");
            JSONObject result = new JSONObject();
            result.put("distance", distance);
            result.put("duration", duration);
            return result;
        } catch (IOException e) {
            log.error("高德地图路径规划异常", e);
            throw new RuntimeException("高德地图路径规划异常", e);
        }
    }
}
server/services/src/main/java/com/doumee/core/utils/tencent/MapUtil.java
ÎļþÃû´Ó server/services/src/main/java/com/doumee/core/utils/Tencent/MapUtil.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.doumee.core.utils.Tencent;
package com.doumee.core.utils.tencent;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
@@ -20,7 +20,7 @@
 * @create 2026/4/14 15:58
 */
@Slf4j
@Component
//@Component
public class MapUtil {
    private static String tencentKey;
server/services/src/main/java/com/doumee/dao/business/NoticeMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package com.doumee.dao.business;
import com.doumee.dao.business.model.Notice;
import com.github.yulichang.base.MPJBaseMapper;
/**
 * æ¶ˆæ¯é€šçŸ¥ä¿¡æ¯Mapper
 * @author rk
 * @date 2026/04/20
 */
public interface NoticeMapper extends MPJBaseMapper<Notice> {
}
server/services/src/main/java/com/doumee/dao/business/model/DriverInfo.java
@@ -150,6 +150,12 @@
    @ApiModelProperty(value = "定位纬度", example = "39.915")
    private Double latitude;
    @ApiModelProperty(value = "支付宝提现账户")
    private String aliAccount;
    @ApiModelProperty(value = "支付宝实名姓名")
    private String aliName;
    @ApiModelProperty(value = "车辆照片列表")
    @TableField(exist = false)
    private List<Multifile> carImgList = new ArrayList<>();
server/services/src/main/java/com/doumee/dao/business/model/Notice.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
package com.doumee.dao.business.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
 * æ¶ˆæ¯é€šçŸ¥ä¿¡æ¯
 * @author rk
 * @date 2026/04/20
 */
@Data
@ApiModel("消息通知信息")
@TableName("`notice`")
public class Notice {
    @TableId(type = IdType.AUTO)
    @ApiModelProperty(value = "主键", example = "1")
    private Integer id;
    @ApiModelProperty(value = "创建人编码", example = "1")
    private Integer creator;
    @ApiModelProperty(value = "创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createDate;
    @ApiModelProperty(value = "更新人编码", example = "1")
    private Integer editor;
    @ApiModelProperty(value = "更新时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date editDate;
    @ApiModelProperty(value = "是否删除 0否 1是", example = "0")
    private Integer isdeleted;
    @ApiModelProperty(value = "备注")
    private String remark;
    @ApiModelProperty(value = "用户类型:0=会员用户;1=司机;2=店铺人员", example = "0")
    private Integer userType;
    @ApiModelProperty(value = "用户主键(member/driver_info/shop_info)", example = "1")
    private Integer userId;
    @ApiModelProperty(value = "标题")
    private String title;
    @ApiModelProperty(value = "内容")
    private String content;
    @ApiModelProperty(value = "对象编码", example = "1")
    private Integer objId;
    @ApiModelProperty(value = "对象类型:0=订单类型;99=其他类型", example = "0")
    private Integer objType;
    @ApiModelProperty(value = "状态:0=未读;1=已读", example = "0")
    private Integer status;
}
server/services/src/main/java/com/doumee/dao/business/model/OrdersRefund.java
@@ -66,5 +66,8 @@
    @ApiModelProperty(value = "平台操作人(type=1使用)", example = "0")
    private Integer userId;
    @ApiModelProperty(value = "退款状态:0=退款中;1=退款成功;2=退款失败", example = "0")
    private Integer status;
}
server/services/src/main/java/com/doumee/dao/business/model/ShopInfo.java
@@ -165,6 +165,9 @@
    @ApiModelProperty(value = "支付宝提现账户")
    private String aliAccount;
    @ApiModelProperty(value = "支付宝实名姓名")
    private String aliName;
    @ApiModelProperty(value = "营业时间")
    private String shopHours;
server/services/src/main/java/com/doumee/dao/dto/AlipayTransferDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package com.doumee.dao.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
 * æ”¯ä»˜å®å•笔转账请求参数
 * @author rk
 * @date 2026/04/20
 */
@Data
public class AlipayTransferDTO {
    /**
     * å•†æˆ·ä¾§å”¯ä¸€è®¢å•号
     */
    private String outBizNo;
    /**
     * è½¬è´¦é‡‘额(元)
     */
    private BigDecimal transAmount;
    /**
     * æ”¶æ¬¾æ–¹æ”¯ä»˜å®è´¦å·
     */
    private String payeeAccount;
    /**
     * æ”¶æ¬¾æ–¹å§“名
     */
    private String payeeName;
    /**
     * ä¸šåŠ¡å¤‡æ³¨
     */
    private String remark;
}
server/services/src/main/java/com/doumee/dao/dto/DriverPickupDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.doumee.dao.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;
/**
 * å¸æœºå®Œæˆå–件请求
 * @author rk
 * @date 2026/04/20
 */
@Data
@ApiModel("司机完成取件请求")
public class DriverPickupDTO {
    @NotNull(message = "订单主键不能为空")
    @ApiModelProperty(value = "订单主键", required = true, example = "1")
    private Integer orderId;
    @NotNull(message = "取件图片不能为空")
    @Size(min = 1, max = 3, message = "取件图片1-3å¼ ")
    @ApiModelProperty(value = "取件图片列表(最多3张)", required = true)
    private List<String> images;
}
server/services/src/main/java/com/doumee/dao/dto/DriverVerifyRequest.java
@@ -72,4 +72,12 @@
    @ApiModelProperty(value = "其他资料照片(最多3张,mutifile objType=8)")
    private List<String> otherImgUrls;
    @NotEmpty(message = "支付宝提现账户不能为空")
    @ApiModelProperty(value = "支付宝提现账户", required = true)
    private String aliAccount;
    @NotEmpty(message = "支付宝实名姓名不能为空")
    @ApiModelProperty(value = "支付宝实名姓名", required = true)
    private String aliName;
}
server/services/src/main/java/com/doumee/dao/dto/ShopApplyDTO.java
@@ -107,4 +107,8 @@
    @ApiModelProperty(value = "支付宝提现账户", required = true)
    @NotBlank(message = "支付宝提现账户不能为空")
    private String aliAccount;
    @ApiModelProperty(value = "支付宝实名姓名", required = true)
    @NotBlank(message = "支付宝实名姓名不能为空")
    private String aliName;
}
server/services/src/main/java/com/doumee/dao/dto/ShopUpdateDTO.java
@@ -110,4 +110,12 @@
    @Size(max = 3, message = "社保缴纳证明最多3å¼ ")
    private List<String> socialSecurityImgs;
    @ApiModelProperty(value = "支付宝提现账户", required = true)
    @NotBlank(message = "支付宝提现账户不能为空")
    private String aliAccount;
    @ApiModelProperty(value = "支付宝实名姓名", required = true)
    @NotBlank(message = "支付宝实名姓名不能为空")
    private String aliName;
}
server/services/src/main/java/com/doumee/dao/vo/PlatformAboutVO.java
@@ -23,6 +23,6 @@
    @ApiModelProperty(value = "隐私协议")
    private String privacyAgreement;
    @ApiModelProperty(value = "服务介绍")
    @ApiModelProperty(value = "规范须知")
    private String serverIntroduce;
}
server/services/src/main/java/com/doumee/dao/vo/ShopDetailVO.java
@@ -4,6 +4,7 @@
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@@ -108,6 +109,11 @@
    @ApiModelProperty(value = "审批人名称")
    private String auditName;
    @ApiModelProperty(value = "支付宝提现账户")
    private String aliAccount;
    @ApiModelProperty(value = "支付宝实名姓名")
    private String aliName;
    @ApiModelProperty(value = "OPENID")
    private String openid;
server/services/src/main/java/com/doumee/service/business/DriverInfoService.java
@@ -6,6 +6,7 @@
import com.doumee.dao.dto.DriverGrabOrderDTO;
import com.doumee.dao.business.model.DriverInfo;
import com.doumee.dao.dto.DriverLoginRequest;
import com.doumee.dao.dto.DriverPickupDTO;
import com.doumee.dao.dto.DriverRegisterRequest;
import com.doumee.dao.dto.DriverVerifyRequest;
import com.doumee.dao.vo.AccountResponse;
@@ -224,4 +225,20 @@
     */
    void cancelOrder(Integer driverId, Integer orderId, String reason);
    /**
     * å¸æœºæŠ¢å•,订单状态从已寄存(2)变为已接单(3)
     *
     * @param driverId å¸æœºä¸»é”®
     * @param orderId  è®¢å•主键
     */
    void grabOrder(Integer driverId, Integer orderId);
    /**
     * å¸æœºå®Œæˆå–件,订单状态从已接单(3)变为派送中(4)
     *
     * @param driverId å¸æœºä¸»é”®
     * @param dto      å–件请求参数
     */
    void confirmPickup(Integer driverId, DriverPickupDTO dto);
}
server/services/src/main/java/com/doumee/service/business/NoticeService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package com.doumee.service.business;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.Notice;
import java.util.List;
/**
 * æ¶ˆæ¯é€šçŸ¥ä¿¡æ¯Service定义
 * @author rk
 * @date 2026/04/20
 */
public interface NoticeService {
    Integer create(Notice notice);
    void deleteById(Integer id);
    void delete(Notice notice);
    void deleteByIdInBatch(List<Integer> ids);
    void updateById(Notice notice);
    void updateByIdInBatch(List<Notice> notices);
    Notice findById(Integer id);
    Notice findOne(Notice notice);
    List<Notice> findList(Notice notice);
    PageData<Notice> findPage(PageWrap<Notice> pageWrap);
    long count(Notice notice);
}
server/services/src/main/java/com/doumee/service/business/impl/DriverInfoServiceImpl.java
@@ -32,19 +32,15 @@
import com.doumee.dao.business.model.Orders;
import com.doumee.dao.business.model.OrdersDetail;
import com.doumee.dao.business.model.Revenue;
import com.doumee.dao.dto.*;
import com.doumee.dao.vo.AccountResponse;
import com.doumee.dao.vo.DriverCenterVO;
import com.doumee.dao.vo.DriverGrabOrderVO;
import com.doumee.dao.vo.DriverOrderDetailVO;
import com.doumee.dao.dto.AuditDTO;
import com.doumee.dao.dto.ChangeStatusDTO;
import com.doumee.dao.dto.DriverLoginRequest;
import com.doumee.dao.dto.DriverRegisterRequest;
import com.doumee.dao.dto.DriverVerifyRequest;
import com.doumee.dao.dto.DriverActiveOrderDTO;
import com.doumee.dao.dto.DriverGrabOrderDTO;
import com.doumee.core.utils.aliyun.AliSmsService;
import com.doumee.dao.business.model.Notice;
import com.doumee.service.business.DriverInfoService;
import com.doumee.service.business.NoticeService;
import com.alibaba.fastjson.JSONObject;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.apache.commons.lang3.RandomStringUtils;
@@ -107,6 +103,43 @@
    @Autowired
    private OperationConfigBiz operationConfigBiz;
    @Autowired
    private NoticeService noticeService;
    /**
     * å‘送订单站内信通知
     */
    private void sendOrderNotice(Integer memberId, Constants.MemberOrderNotify notify, Integer orderId, String... params) {
        Notice notice = new Notice();
        notice.setUserType(0); // 0=会员
        notice.setUserId(memberId);
        notice.setTitle(notify.getTitle());
        notice.setContent(notify.format(params));
        notice.setObjId(orderId);
        notice.setObjType(0); // 0=订单
        notice.setStatus(0);  // 0=未读
        notice.setIsdeleted(Constants.ZERO);
        notice.setCreateDate(new Date());
        noticeService.create(notice);
    }
    /**
     * å‘送门店站内信通知
     */
    private void sendShopNotice(Integer shopId, Constants.ShopOrderNotify notify, Integer orderId, String... params) {
        Notice notice = new Notice();
        notice.setUserType(2); // 2=门店
        notice.setUserId(shopId);
        notice.setTitle(notify.getTitle());
        notice.setContent(notify.format(params));
        notice.setObjId(orderId);
        notice.setObjType(0); // 0=订单
        notice.setStatus(0);  // 0=未读
        notice.setIsdeleted(Constants.ZERO);
        notice.setCreateDate(new Date());
        noticeService.create(notice);
    }
    @Override
    public Integer create(DriverInfo driverInfo) {
@@ -462,7 +495,8 @@
                .set(DriverInfo::getCardEndDate, request.getCardEndDate())
                .set(DriverInfo::getIdcardImg, request.getIdcardImg())
                .set(DriverInfo::getIdcardImgBack, request.getIdcardImgBack())
                .set(DriverInfo::getAuditStatus, Constants.ZERO)
                .set(DriverInfo::getAliAccount, request.getAliAccount())
                .set(DriverInfo::getAliName, request.getAliName())
                .set(DriverInfo::getUpdateTime, now)
                .set(DriverInfo::getAuditRemark, null)
                .set(DriverInfo::getAuditTime, null)
@@ -1131,6 +1165,159 @@
        log.setCreateTime(new Date());
        log.setDeleted(Constants.ZERO);
        orderLogMapper.insert(log);
        // é€šçŸ¥ä¼šå‘˜ï¼šå¸æœºå˜æ›´
        sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.DRIVER_CHANGED, orderId,
                "orderNo", order.getCode());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void grabOrder(Integer driverId, Integer orderId) {
        // 1. æ ¡éªŒå¸æœº
        DriverInfo driver = driverInfoMapper.selectById(driverId);
        if (driver == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "司机信息不存在");
        }
        // 2. æ ¡éªŒå¸æœºæŽ¥å•状态
        if (!Constants.ONE.equals(driver.getAcceptingStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "请先开启接单状态");
        }
        if (!Integer.valueOf(3).equals(driver.getAuditStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "司机审核未通过或未缴纳押金");
        }
        if (driver.getDriverLevel() == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "司机等级未设置");
        }
        // 3. æ ¡éªŒè®¢å•
        Orders order = ordersMapper.selectById(orderId);
        if (order == null || Constants.ONE.equals(order.getDeleted())) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        if (!Constants.ONE.equals(order.getType())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅异地寄存订单可抢单");
        }
        if (!Constants.TWO.equals(order.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许抢单");
        }
        // 4. æ ¡éªŒå¸æœºç­‰çº§ â‰¥ è®¢å•等级
        if (order.getGoodLevel() != null && driver.getDriverLevel() < order.getGoodLevel()) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "司机等级不足,无法抢该订单");
        }
        // 5. åŽŸå­æ›´æ–°ï¼šå¸¦ status=2 æ¡ä»¶é˜²æ­¢å¹¶å‘重复抢单
        Date now = new Date();
        int rows = ordersMapper.update(new UpdateWrapper<Orders>().lambda()
                .set(Orders::getAcceptDriver, driverId)
                .set(Orders::getAcceptTime, now)
                .set(Orders::getAcceptType, 0) // 0=手动抢单
                .set(Orders::getStatus, Constants.OrderStatus.accepted.getStatus())
                .set(Orders::getUpdateTime, now)
                .eq(Orders::getId, orderId)
                .eq(Orders::getStatus, Constants.TWO));
        if (rows == 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "抢单失败,订单已被抢或状态已变更");
        }
        // 6. å†™å…¥æ“ä½œæ—¥å¿—
        OrderLog log = new OrderLog();
        log.setOrderId(orderId);
        log.setTitle("司机抢单");
        log.setLogInfo("司机【" + driver.getName() + "】抢单成功");
        log.setObjType(Constants.ORDER_LOG_DRIVER_PICKUP);
        log.setOptUserId(driver.getMemberId());
        log.setOptUserType(Constants.ONE);
        log.setOrderStatus(Constants.OrderStatus.accepted.getStatus());
        log.setCreateTime(now);
        log.setDeleted(Constants.ZERO);
        orderLogMapper.insert(log);
        // 7. é€šçŸ¥ä¼šå‘˜ï¼šå¸æœºå·²æŠ¢å•
        sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.WAIT_PICKUP_GRABBED, orderId,
                "driverName", driver.getName());
        // é€šçŸ¥å­˜ä»¶é—¨åº—:订单已抢单待取件
        if (order.getDepositShopId() != null) {
            sendShopNotice(order.getDepositShopId(), Constants.ShopOrderNotify.WAIT_PICKUP, orderId,
                    "orderNo", order.getCode());
        }
    }
    @Override
    @Transactional
    public void confirmPickup(Integer driverId, DriverPickupDTO dto) {
        Integer orderId = dto.getOrderId();
        // 1. æ ¡éªŒå¸æœº
        DriverInfo driver = driverInfoMapper.selectById(driverId);
        if (driver == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "司机信息不存在");
        }
        // 2. æ ¡éªŒè®¢å•
        Orders order = ordersMapper.selectById(orderId);
        if (order == null || Constants.ONE.equals(order.getDeleted())) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        if (!Constants.ONE.equals(order.getType())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅异地寄存订单支持此操作");
        }
        if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.accepted.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许取件确认");
        }
        if (!driverId.equals(order.getAcceptDriver())) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "无权操作该订单");
        }
        // 3. ä¿å­˜å–件图片
        Date now = new Date();
        if (dto.getImages() != null && !dto.getImages().isEmpty()) {
            int sortNum = 0;
            for (String url : dto.getImages()) {
                Multifile multifile = new Multifile();
                multifile.setObjId(orderId);
                multifile.setObjType(Constants.FileType.DRIVER_TAKE.getKey());
                multifile.setType(Constants.ZERO);
                multifile.setFileurl(url);
                multifile.setIsdeleted(Constants.ZERO);
                multifile.setCreateDate(now);
                multifile.setSortnum(sortNum++);
                multifileMapper.insert(multifile);
            }
        }
        // 4. æ›´æ–°è®¢å•状态为派送中(4)
        ordersMapper.update(new UpdateWrapper<Orders>().lambda()
                .set(Orders::getStatus, Constants.OrderStatus.delivering.getStatus())
                .set(Orders::getUpdateTime, now)
                .eq(Orders::getId, orderId));
        // 5. å†™å…¥æ“ä½œæ—¥å¿—
        OrderLog log = new OrderLog();
        log.setOrderId(orderId);
        log.setTitle("司机完成取件");
        log.setLogInfo("司机【" + driver.getName() + "】完成取件,开始派送");
        log.setObjType(Constants.ORDER_LOG_DRIVER_PICKUP);
        log.setOptUserId(driver.getMemberId());
        log.setOptUserType(Constants.ONE);
        log.setOrderStatus(Constants.OrderStatus.delivering.getStatus());
        log.setCreateTime(now);
        log.setDeleted(Constants.ZERO);
        orderLogMapper.insert(log);
        // é€šçŸ¥ä¼šå‘˜ï¼šè®¢å•配送中
        sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.DELIVERING, orderId,
                "orderNo", order.getCode(),
                "driverName", driver.getName());
        // é€šçŸ¥å–件门店:订单配送中
        if (order.getTakeShopId() != null) {
            sendShopNotice(order.getTakeShopId(), Constants.ShopOrderNotify.DELIVERING, orderId,
                    "orderNo", order.getCode(),
                    "driverName", driver.getName());
        }
    }
    private List<String> getFileUrls(Integer orderId, int objType, String prefix) {
server/services/src/main/java/com/doumee/service/business/impl/MemberServiceImpl.java
@@ -350,15 +350,13 @@
     * é—¨åº—用户身份时,填充门店审核状态
     */
    private void fillShopInfo(UserCenterVO userCenterVO, Member member) {
        if (Constants.TWO.equals(member.getUserType())) {
            ShopInfo shopInfo = shopInfoMapper.selectOne(new QueryWrapper<ShopInfo>().lambda()
                    .eq(ShopInfo::getRegionMemberId, member.getId())
                    .eq(ShopInfo::getDeleted, Constants.ZERO)
                    .last("limit 1"));
            if (shopInfo != null) {
                userCenterVO.setShopId(shopInfo.getId());
                userCenterVO.setShopAuditStatus(shopInfo.getAuditStatus());
            }
        ShopInfo shopInfo = shopInfoMapper.selectOne(new QueryWrapper<ShopInfo>().lambda()
                .eq(ShopInfo::getRegionMemberId, member.getId())
                .eq(ShopInfo::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (shopInfo != null) {
            userCenterVO.setShopId(shopInfo.getId());
            userCenterVO.setShopAuditStatus(shopInfo.getAuditStatus());
        }
        // æ ¹æ®openid查询当前绑定的门店
        if (StringUtils.isNotBlank(member.getOpenid())) {
server/services/src/main/java/com/doumee/service/business/impl/NoticeServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,126 @@
package com.doumee.service.business.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.NoticeMapper;
import com.doumee.dao.business.model.Notice;
import com.doumee.service.business.NoticeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * æ¶ˆæ¯é€šçŸ¥ä¿¡æ¯Service实现
 * @author rk
 * @date 2026/04/20
 */
@Service
public class NoticeServiceImpl implements NoticeService {
    @Autowired
    private NoticeMapper noticeMapper;
    @Override
    public Integer create(Notice notice) {
        noticeMapper.insert(notice);
        return notice.getId();
    }
    @Override
    public void deleteById(Integer id) {
        noticeMapper.deleteById(id);
    }
    @Override
    public void delete(Notice notice) {
        QueryWrapper<Notice> deleteWrapper = new QueryWrapper<>(notice);
        noticeMapper.delete(deleteWrapper);
    }
    @Override
    public void deleteByIdInBatch(List<Integer> ids) {
        if (ids == null || ids.isEmpty()) {
            return;
        }
        noticeMapper.deleteBatchIds(ids);
    }
    @Override
    public void updateById(Notice notice) {
        noticeMapper.updateById(notice);
    }
    @Override
    public void updateByIdInBatch(List<Notice> notices) {
        if (notices == null || notices.isEmpty()) {
            return;
        }
        for (Notice notice : notices) {
            this.updateById(notice);
        }
    }
    @Override
    public Notice findById(Integer id) {
        return noticeMapper.selectById(id);
    }
    @Override
    public Notice findOne(Notice notice) {
        QueryWrapper<Notice> wrapper = new QueryWrapper<>(notice);
        return noticeMapper.selectOne(wrapper);
    }
    @Override
    public List<Notice> findList(Notice notice) {
        QueryWrapper<Notice> wrapper = new QueryWrapper<>(notice);
        return noticeMapper.selectList(wrapper);
    }
    @Override
    public PageData<Notice> findPage(PageWrap<Notice> pageWrap) {
        IPage<Notice> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        QueryWrapper<Notice> queryWrapper = new QueryWrapper<>();
        if (pageWrap.getModel() != null) {
            if (pageWrap.getModel().getIsdeleted() != null) {
                queryWrapper.lambda().eq(Notice::getIsdeleted, pageWrap.getModel().getIsdeleted());
            }
            if (pageWrap.getModel().getUserType() != null) {
                queryWrapper.lambda().eq(Notice::getUserType, pageWrap.getModel().getUserType());
            }
            if (pageWrap.getModel().getUserId() != null) {
                queryWrapper.lambda().eq(Notice::getUserId, pageWrap.getModel().getUserId());
            }
            if (pageWrap.getModel().getObjType() != null) {
                queryWrapper.lambda().eq(Notice::getObjType, pageWrap.getModel().getObjType());
            }
            if (pageWrap.getModel().getStatus() != null) {
                queryWrapper.lambda().eq(Notice::getStatus, pageWrap.getModel().getStatus());
            }
            if (pageWrap.getModel().getTitle() != null) {
                queryWrapper.lambda().like(Notice::getTitle, pageWrap.getModel().getTitle());
            }
            if (pageWrap.getModel().getCreateDate() != null) {
                queryWrapper.lambda().ge(Notice::getCreateDate, pageWrap.getModel().getCreateDate());
            }
        }
        for (PageWrap.SortData sortData : pageWrap.getSorts()) {
            if (sortData.getDirection().equalsIgnoreCase(PageWrap.DESC)) {
                queryWrapper.orderByDesc(sortData.getProperty());
            } else {
                queryWrapper.orderByAsc(sortData.getProperty());
            }
        }
        return PageData.from(noticeMapper.selectPage(page, queryWrapper));
    }
    @Override
    public long count(Notice notice) {
        QueryWrapper<Notice> wrapper = new QueryWrapper<>(notice);
        return noticeMapper.selectCount(wrapper);
    }
}
server/services/src/main/java/com/doumee/service/business/impl/OrdersServiceImpl.java
@@ -8,14 +8,16 @@
import com.doumee.biz.system.OperationConfigBiz;
import com.doumee.biz.system.SystemDictDataBiz;
import com.doumee.config.wx.WxMiniConfig;
import com.doumee.config.wx.WxMiniUtilService;
import com.doumee.config.wx.WxPayProperties;
import com.doumee.config.wx.WxPayV3Service;
import com.wechat.pay.java.service.refund.model.Refund;
import com.doumee.core.constants.Constants;
import com.doumee.core.constants.ResponseStatus;
import com.doumee.core.exception.BusinessException;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.core.utils.DateUtil;
import com.doumee.core.utils.Tencent.MapUtil;
import com.doumee.core.utils.geocode.MapUtil;
import com.doumee.core.utils.Utils;
import com.doumee.dao.business.*;
import com.doumee.dao.business.model.*;
@@ -30,11 +32,12 @@
import com.doumee.dao.dto.MyOrderDTO;
import com.doumee.dao.dto.OrderItemDTO;
import com.doumee.dao.vo.*;
import com.doumee.service.business.NoticeService;
import com.doumee.service.business.OrderLogService;
import com.doumee.service.business.OrdersService;
import com.doumee.dao.business.model.Notice;
import com.doumee.service.business.AreasService;
import com.doumee.service.business.PricingRuleService;
import com.github.binarywang.wxpay.bean.request.BaseWxPayRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.xiaoymin.knife4j.core.util.CollectionUtils;
@@ -45,17 +48,11 @@
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
 * å¯„存订单信息Service实现
@@ -104,8 +101,7 @@
    @Autowired
    private RevenueMapper revenueMapper;
    @Autowired
    private WxMiniUtilService wxMiniUtilService;
    @Autowired
    private SystemUserMapper systemUserMapper;
@@ -127,6 +123,15 @@
    @Autowired
    private AreasService areasService;
    @Autowired
    private NoticeService noticeService;
    @Autowired
    private WxPayV3Service wxPayV3Service;
    @Autowired
    private WxPayProperties wxPayProperties;
    @Override
    public Integer create(Orders orders) {
@@ -413,7 +418,7 @@
        // 1. è°ƒç”¨è…¾è®¯åœ°å›¾è·ç¦»çŸ©é˜µAPI计算驾车距离
        String from = dto.getFromLat() + "," + dto.getFromLgt();
        String to = dto.getToLat() + "," + dto.getToLgt();
        JSONObject distanceResult = MapUtil.distanceSingle("driving", from, to);
        JSONObject distanceResult = MapUtil.direction("driving", from, to);
        BigDecimal distance = distanceResult.getBigDecimal("distance");
        // distance å•位为米,转为公里
        BigDecimal distanceKm = distance.divide(new BigDecimal(1000), 2, RoundingMode.HALF_UP);
@@ -518,16 +523,17 @@
        // 5. åŠ æ€¥è´¹ç”¨ï¼šç‰©å“ä»·æ ¼ Ã— åŠ æ€¥ç³»æ•°(字典 URGENT_COEFFICIENT)
        long urgentFeeFen = 0L;
        if (Boolean.TRUE.equals(dto.getUrgent())) {
            String urgentRateStr = systemDictDataBiz.queryByCode(
                    Constants.OPERATION_CONFIG, Constants.OP_URGENT_COEFFICIENT).getCode();
            BigDecimal urgentRate = new BigDecimal(urgentRateStr);
            urgentFeeFen = new BigDecimal(itemPriceTotal).multiply(urgentRate)
                    .setScale(0, RoundingMode.HALF_UP).longValue();
        }
        String urgentRateStr = systemDictDataBiz.queryByCode(
                Constants.OPERATION_CONFIG, Constants.OP_URGENT_COEFFICIENT).getCode();
        BigDecimal urgentRate = new BigDecimal(urgentRateStr);
        urgentFeeFen = new BigDecimal(itemPriceTotal).multiply(urgentRate)
                .setScale(0, RoundingMode.HALF_UP).longValue();
        // 6. æ€»ä»·æ ¼ = ç‰©å“ä»·æ ¼ + ä¿ä»·è´¹ç”¨ + åŠ æ€¥è´¹ç”¨
        long totalPrice = itemPriceTotal + insuranceFeeFen + urgentFeeFen;
        // 6. æ€»ä»·æ ¼ = ç‰©å“ä»·æ ¼ + ä¿ä»·è´¹ç”¨ + åŠ æ€¥è´¹ç”¨ï¼ˆåŠ æ€¥æ—¶æ‰åŒ…å«åŠ æ€¥è´¹ï¼‰
        long totalPrice = itemPriceTotal + insuranceFeeFen;
        if (Boolean.TRUE.equals(dto.getUrgent())) {
            totalPrice += urgentFeeFen;
        }
        PriceCalculateVO result = new PriceCalculateVO();
        result.setItemList(itemList);
@@ -666,9 +672,9 @@
                takeLocationValue = takeShop.getAddress();
            } else if (dto.getTakeLat() != null && dto.getTakeLgt() != null && StringUtils.isNotBlank(dto.getTakeLocation())) {
                // æ— å–件门店,校验存件点与自选取件点是否在同一城市
                if (MapUtil.isSameCity(depositShop.getLatitude(), depositShop.getLongitude(),
                if (!MapUtil.isSameCity(depositShop.getLatitude(), depositShop.getLongitude(),
                        dto.getTakeLat().doubleValue(), dto.getTakeLgt().doubleValue())) {
                    throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "异地寄存订单存取点不能在同一城市,如需同城寄存请选择就近门店");
                    throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "异地寄存订单存取点不在同一城市,如需请选择同城门店");
                }
                takeLat = dto.getTakeLat();
                takeLgt = dto.getTakeLgt();
@@ -836,7 +842,8 @@
        if (member == null || StringUtils.isBlank(member.getOpenid())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "用户信息异常,无法发起支付");
        }
        PayResponse payResponse = wxPay(orders, member.getOpenid(), Constants.OrdersAttach.STORAGE_ORDER);
        PayResponse payResponse = wxPayV3(orders.getOutTradeNo(), orders.getTotalAmount(), orders.getId(),
                member.getOpenid(), Constants.OrdersAttach.STORAGE_ORDER);
        payResponse.setLockKey(lockKey);
        return payResponse;
    }
@@ -871,7 +878,8 @@
        if (member == null || StringUtils.isBlank(member.getOpenid())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "用户信息异常,无法发起支付");
        }
        return wxPay(orders, member.getOpenid(), Constants.OrdersAttach.STORAGE_ORDER);
        return wxPayV3(orders.getOutTradeNo(), orders.getTotalAmount(), orders.getId(),
                member.getOpenid(), Constants.OrdersAttach.STORAGE_ORDER);
    }
    /**
@@ -906,7 +914,32 @@
        }
    }
    /**
     * å”¤èµ·å¾®ä¿¡æ”¯ä»˜V3
     *
     * @param outTradeNo   å•†æˆ·è®¢å•号
     * @param totalCents   æ”¯ä»˜é‡‘额(分)
     * @param orderId      è®¢å•主键
     * @param openid       ç”¨æˆ·å¾®ä¿¡openid
     * @param ordersAttach è®¢å•支付类型
     * @return PayResponse åŒ…含微信调起参数和订单主键
     */
    private PayResponse wxPayV3(String outTradeNo, Long totalCents, Integer orderId,
                                String openid, Constants.OrdersAttach ordersAttach) {
        Map<String, String> payParams = wxPayV3Service.createOrder(
                outTradeNo,
                ordersAttach.getName(),
                totalCents != null ? totalCents : 0L,
                openid,
                wxPayProperties.getV3NotifyUrl(),
                ordersAttach.getKey()
        );
        PayResponse payResponse = new PayResponse();
        payResponse.setResponse(payParams);
        payResponse.setOrderId(orderId);
        return payResponse;
    }
@@ -1240,14 +1273,17 @@
        // å¯„件门店占比:fieldA=0(企业寄)/1(个人寄)
        int depositFieldA = Constants.equalsInteger(depositShop.getCompanyType(), Constants.ONE) ? Constants.ZERO : Constants.ONE;
        BigDecimal depositShopRata = getRevenueShareRata(cityId, depositFieldA);
        // å–件门店占比:fieldA=2(企业取)/3(个人取)
        int takeFieldA = Constants.equalsInteger(takeShop.getCompanyType(), Constants.ONE) ? Constants.TWO : Constants.THREE;
        BigDecimal takeShopRata = getRevenueShareRata(cityId, takeFieldA);
        // å–件门店占比:无取件门店时比例为0
        BigDecimal takeShopRata = BigDecimal.ZERO;
        if (takeShop != null) {
            int takeFieldA = Constants.equalsInteger(takeShop.getCompanyType(), Constants.ONE) ? Constants.TWO : Constants.THREE;
            takeShopRata = getRevenueShareRata(cityId, takeFieldA);
        }
        // è®¡ç®—薪酬(分):totalAmount ä¸ºåˆ†ï¼Œrata ä¸ºæ¯”例值(如 0.15 è¡¨ç¤º 15%)
        long driverFee = new BigDecimal(totalAmount).multiply(driverRata).longValue();
        long depositShopFee = new BigDecimal(totalAmount).multiply(depositShopRata).longValue();
        long takeShopFee = totalAmount - driverFee - depositShopFee;
        long takeShopFee = new BigDecimal(totalAmount).multiply(takeShopRata).longValue();
        orders.setDriverFee(driverFee);
        orders.setDepositShopFee(depositShopFee);
@@ -1664,10 +1700,11 @@
            refund.setCreateTime(now);
            refund.setDeleted(Constants.ZERO);
            // è°ƒç”¨å¾®ä¿¡é€€æ¬¾ï¼Œå…¨é¢é€€æ¬¾
            String refundCode = wxMiniUtilService.wxRefund(order.getOutTradeNo(), order.getPayAmount(), order.getPayAmount());
            refund.setRefundCode(refundCode);
            refund.setRefundTime(new Date());
            // è°ƒç”¨å¾®ä¿¡é€€æ¬¾V3,全额退款
            Refund refundResult = wxPayV3Service.refund(order.getOutTradeNo(), order.getPayAmount(), order.getPayAmount(),
                    "订单退款", wxPayProperties.getV3RefundNotifyUrl());
            refund.setRefundCode(refundResult.getOutRefundNo());
            refund.setStatus(Constants.ZERO); // é€€æ¬¾ä¸­
            ordersRefundMapper.insert(refund);
            order.setStatus(Constants.OrderStatus.cancelled.getStatus());
@@ -1676,6 +1713,9 @@
            ordersMapper.updateById(order);
            saveCancelLog(order, "会员取消订单(待寄存,全额退款)", reason, memberId);
            // é€šçŸ¥ä¼šå‘˜ï¼šé€€æ¬¾ä¸­
            sendOrderNotice(memberId, Constants.MemberOrderNotify.REFUNDING, orderId,
                    "orderNo", order.getCode());
            return;
        }
@@ -1686,6 +1726,11 @@
            order.setCancelTime(now);
            ordersMapper.updateById(order);
            saveCancelLog(order, "会员申请取消订单(已寄存/已接单)", reason, memberId);
            // é€šçŸ¥å­˜ä»¶é—¨åº—:退款申请
            if (order.getDepositShopId() != null) {
                sendShopNotice(order.getDepositShopId(), Constants.ShopOrderNotify.REFUNDING, orderId,
                        "orderNo", order.getCode());
            }
            return;
        }
@@ -1724,6 +1769,52 @@
        log.setCreateTime(new Date());
        log.setDeleted(Constants.ZERO);
        orderLogService.create(log);
    }
    /**
     * å‘送订单站内信通知
     */
    private void sendOrderNotice(Integer memberId, Constants.MemberOrderNotify notify, Integer orderId, String... params) {
        Notice notice = new Notice();
        notice.setUserType(0); // 0=会员
        notice.setUserId(memberId);
        notice.setTitle(notify.getTitle());
        notice.setContent(notify.format(params));
        notice.setObjId(orderId);
        notice.setObjType(0); // 0=订单
        notice.setStatus(0);  // 0=未读
        notice.setIsdeleted(Constants.ZERO);
        notice.setCreateDate(new Date());
        noticeService.create(notice);
    }
    /**
     * å‘送门店站内信通知
     */
    private void sendShopNotice(Integer shopId, Constants.ShopOrderNotify notify, Integer orderId, String... params) {
        Notice notice = new Notice();
        notice.setUserType(2); // 2=门店
        notice.setUserId(shopId);
        notice.setTitle(notify.getTitle());
        notice.setContent(notify.format(params));
        notice.setObjId(orderId);
        notice.setObjType(0); // 0=订单
        notice.setStatus(0);  // 0=未读
        notice.setIsdeleted(Constants.ZERO);
        notice.setCreateDate(new Date());
        noticeService.create(notice);
    }
    /**
     * é€šçŸ¥å­˜ä»¶é—¨åº—和取件门店(订单完成/评价等)
     */
    private void notifyBothShops(Orders order, Constants.ShopOrderNotify notify, String... params) {
        if (order.getDepositShopId() != null) {
            sendShopNotice(order.getDepositShopId(), notify, order.getId(), params);
        }
        if (order.getTakeShopId() != null) {
            sendShopNotice(order.getTakeShopId(), notify, order.getId(), params);
        }
    }
    @Override
@@ -1767,6 +1858,17 @@
            }
        }
        ordersMapper.updateById(order);
        // é€šçŸ¥ä¼šå‘˜ï¼šè®¢å•待核验
        sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.WAIT_VERIFY, order.getId(),
                "orderNo", order.getCode(),
                "storeCode", order.getMemberVerifyCode());
        // å°±åœ°å¯„存订单:通知存件门店待核验
        if (Constants.ZERO.equals(order.getType()) && order.getDepositShopId() != null) {
            sendShopNotice(order.getDepositShopId(), Constants.ShopOrderNotify.WAIT_VERIFY, order.getId(),
                    "orderNo", order.getCode());
        }
    }
    @Override
@@ -1810,8 +1912,9 @@
        otherOrders.setCreateTime(now);
        otherOrdersMapper.insert(otherOrders);
        // 5. å”¤èµ·å¾®ä¿¡æ”¯ä»˜
        return wxPayForOtherOrder(otherOrders, member.getOpenid(), Constants.OrdersAttach.OVERDUE_FEE);
        // 5. å”¤èµ·å¾®ä¿¡æ”¯ä»˜V3
        return wxPayV3(otherOrders.getOutTradeNo(), otherOrders.getPayAccount(), otherOrders.getId(),
                member.getOpenid(), Constants.OrdersAttach.OVERDUE_FEE);
    }
    @Override
@@ -1910,8 +2013,9 @@
        otherOrders.setCreateTime(now);
        otherOrdersMapper.insert(otherOrders);
        // 5. å”¤èµ·å¾®ä¿¡æ”¯ä»˜
        return wxPayForOtherOrder(otherOrders, member.getOpenid(), Constants.OrdersAttach.SHOP_DEPOSIT);
        // 5. å”¤èµ·å¾®ä¿¡æ”¯ä»˜V3
        return wxPayV3(otherOrders.getOutTradeNo(), otherOrders.getPayAccount(), otherOrders.getId(),
                member.getOpenid(), Constants.OrdersAttach.SHOP_DEPOSIT);
    }
    @Override
@@ -2048,6 +2152,12 @@
                    }
                }
            }
            // é€šçŸ¥ç›¸å…³é—¨åº—:订单已结算
            notifyBothShops(order, Constants.ShopOrderNotify.SETTLED,
                    "orderNo", order.getCode(),
                    "amount", String.valueOf(Constants.getFormatMoney(
                            order.getTotalAmount() != null ? order.getTotalAmount() : 0L)));
        }
    }
@@ -2144,6 +2254,14 @@
        if (isRemote && order.getAcceptDriver() != null) {
            updateTargetScore(Constants.THREE, order.getAcceptDriver());
        }
        // é€šçŸ¥ä¼šå‘˜ï¼šè®¢å•已评价
        sendOrderNotice(memberId, Constants.MemberOrderNotify.EVALUATED, order.getId(),
                "orderNo", order.getCode());
        // é€šçŸ¥å­˜ä»¶é—¨åº—和取件门店:订单已评价
        notifyBothShops(order, Constants.ShopOrderNotify.EVALUATED,
                "orderNo", order.getCode());
    }
    /**
@@ -2246,6 +2364,16 @@
            saveVerifyImages(order.getId(), images, Constants.FileType.ORDER_DEPOSIT.getKey(), shopId);
            // è®°å½•订单日志
            saveShopVerifyLog(order, "门店确认寄存", "门店【" + shopName + "】确认寄存", remark, shopId);
            // é€šçŸ¥ä¼šå‘˜ï¼šé—¨åº—核销成功
            if (Constants.equalsInteger(order.getType(), Constants.ONE)) {
                // å¼‚地寄存 â†’ å¾…抢单
                sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.WAIT_GRAB, order.getId(),
                        "orderNo", order.getCode());
            } else {
                // å°±åœ°å¯„å­˜ â†’ å¾…取件提醒
                sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.WAIT_PICKUP_REMIND, order.getId(),
                        "orderNo", order.getCode());
            }
        } else if (Constants.equalsInteger(status, Constants.OrderStatus.arrived.getStatus())) {
            // å¼‚地寄存 + æ— å–件门店 â†’ æ— æ³•核销(客户自取,无门店操作)
            if (Constants.equalsInteger(order.getType(), Constants.ONE) && order.getTakeShopId() == null) {
@@ -2268,6 +2396,14 @@
            generateRevenueRecords(order.getId());
            // è®°å½•订单日志
            saveShopVerifyLog(order, "门店确认取件", "门店【" + shopName + "】确认取件,订单完成", remark, shopId);
            // é€šçŸ¥ä¼šå‘˜ï¼šè®¢å•已完成
            sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.FINISHED, order.getId(),
                    "orderNo", order.getCode());
            // é€šçŸ¥å­˜ä»¶é—¨åº—和取件门店:订单已完成
            String settleDays = operationConfigBiz.getConfig().getSettlementDate();
            notifyBothShops(order, Constants.ShopOrderNotify.FINISHED,
                    "orderNo", order.getCode(),
                    "settleDays", settleDays != null ? settleDays : "7");
        } else {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许核销");
        }
@@ -2344,13 +2480,14 @@
            refundRecord.setDeleted(Constants.ZERO);
            ordersRefundMapper.insert(refundRecord);
            // è°ƒç”¨å¾®ä¿¡é€€æ¬¾ï¼ˆæ”¾åœ¨æœ€åŽï¼Œç¡®ä¿å‰ç½®æ“ä½œå…¨éƒ¨æˆåŠŸï¼‰
            String refundCode = wxMiniUtilService.wxRefund(
                    order.getOutTradeNo(), order.getPayAmount(), order.getRefundAmount());
            // è°ƒç”¨å¾®ä¿¡é€€æ¬¾V3(放在最后,确保前置操作全部成功)
            Refund refundResult = wxPayV3Service.refund(
                    order.getOutTradeNo(), order.getPayAmount(), order.getRefundAmount(),
                    "订单退款", wxPayProperties.getV3RefundNotifyUrl());
            // é€€æ¬¾æˆåŠŸåŽå›žå¡«é€€æ¬¾å•å·å’Œæ—¶é—´
            refundRecord.setRefundCode(refundCode);
            refundRecord.setRefundTime(new Date());
            // é€€æ¬¾æˆåŠŸåŽå›žå¡«é€€æ¬¾å•å·ï¼Œæ ‡è®°é€€æ¬¾ä¸­
            refundRecord.setRefundCode(refundResult.getOutRefundNo());
            refundRecord.setStatus(Constants.ZERO); // é€€æ¬¾ä¸­
            ordersRefundMapper.updateById(refundRecord);
        }
@@ -2364,6 +2501,14 @@
            logInfo += ",退款" + Constants.getFormatMoney(order.getRefundAmount()) + "元";
        }
        saveShopVerifyLog(order, "门店确认出库", logInfo, remark, shopId);
        // é€šçŸ¥ä¼šå‘˜ï¼šè®¢å•已完成
        sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.FINISHED, order.getId(),
                "orderNo", order.getCode());
        // é€šçŸ¥å­˜ä»¶é—¨åº—和取件门店:订单已完成
        String settleDays = operationConfigBiz.getConfig().getSettlementDate();
        notifyBothShops(order, Constants.ShopOrderNotify.FINISHED,
                "orderNo", order.getCode(),
                "settleDays", settleDays != null ? settleDays : "7");
    }
    @Override
@@ -2401,6 +2546,15 @@
        // 7. ç”Ÿæˆæ”¶ç›Šè®°å½•
        calculateAndSaveOrderFees(orderId);
        generateRevenueRecords(orderId);
        // é€šçŸ¥ä¼šå‘˜ï¼šè®¢å•已完成
        sendOrderNotice(memberId, Constants.MemberOrderNotify.FINISHED, orderId,
                "orderNo", order.getCode());
        // é€šçŸ¥å­˜ä»¶é—¨åº—和取件门店:订单已完成
        String settleDays = operationConfigBiz.getConfig().getSettlementDate();
        notifyBothShops(order, Constants.ShopOrderNotify.FINISHED,
                "orderNo", order.getCode(),
                "settleDays", settleDays != null ? settleDays : "7");
    }
    @Override
@@ -2552,6 +2706,26 @@
        // ä¿å­˜é™„件(obj_type=3 é—¨åº—入库图片,最多3张)
        saveVerifyImages(order.getId(), images, Constants.FileType.ORDER_TAKE.getKey(), driverId);
        // é€šçŸ¥ä¼šå‘˜ï¼šè®¢å•已送达
        String destination = order.getTakeShopAddress() != null ? order.getTakeShopAddress() : "";
        if (order.getMemberVerifyCode() != null) {
            sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.ARRIVED_HAS_SHOP, order.getId(),
                    "orderNo", order.getCode(),
                    "destination", destination,
                    "pickupCode", order.getMemberVerifyCode());
        } else {
            sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.ARRIVED_NO_SHOP, order.getId(),
                    "orderNo", order.getCode(),
                    "destination", destination);
        }
        // é€šçŸ¥å–件门店:订单已送达
        if (order.getTakeShopId() != null) {
            sendShopNotice(order.getTakeShopId(), Constants.ShopOrderNotify.ARRIVED, order.getId(),
                    "orderNo", order.getCode(),
                    "destination", destination);
        }
    }
    /**
@@ -2953,7 +3127,7 @@
        // è…¾è®¯åœ°å›¾è·ç¦»çŸ©é˜µAPI计算实际距离
        String from = fromLat + "," + fromLng;
        String to = toLat + "," + toLng;
        JSONObject distanceResult = MapUtil.distanceSingle("driving", from, to);
        JSONObject distanceResult = MapUtil.direction("driving", from, to);
        // èŽ·å–è·ç¦»ï¼ˆç±³ï¼‰ï¼Œè½¬å…¬é‡Œ
        int distanceMeters = distanceResult.getIntValue("distance");
server/services/src/main/java/com/doumee/service/business/impl/ShopInfoServiceImpl.java
@@ -315,7 +315,7 @@
            existing.setPassword(encryptedPassword);
            existing.setSalt(salt);
            existing.setAliAccount(request.getAliAccount());
            existing.setAuditStatus(Constants.ZERO);
            existing.setAliName(request.getAliName());
            existing.setUpdateTime(now);
            existing.setAuditRemark(null);
            existing.setAuditTime(null);
@@ -344,8 +344,7 @@
            shopInfo.setPassword(encryptedPassword);
            shopInfo.setSalt(salt);
            shopInfo.setAliAccount(request.getAliAccount());
            shopInfo.setOpenid(member.getOpenid());
            shopInfo.setAuditStatus(Constants.ZERO);
            shopInfo.setAliName(request.getAliName());
            shopInfo.setStatus(Constants.ZERO);
            shopInfo.setDeleted(Constants.ZERO);
            shopInfo.setCreateTime(now);
@@ -552,6 +551,8 @@
        shopInfo.setLegalPersonName(request.getLegalPersonName());
        shopInfo.setLegalPersonPhone(request.getLegalPersonPhone());
        shopInfo.setLegalPersonCard(request.getLegalPersonCard());
        shopInfo.setAliAccount(request.getAliAccount());
        shopInfo.setAliName(request.getAliName());
        shopInfo.setUpdateTime(now);
        shopInfoMapper.updateById(shopInfo);
@@ -655,6 +656,8 @@
        vo.setPayStatus(shopInfo.getPayStatus());
        vo.setScore(shopInfo.getScore());
        vo.setCreateTime(shopInfo.getCreateTime());
        vo.setAliAccount(shopInfo.getAliAccount());
        vo.setAliName(shopInfo.getAliName());
        // æ‹¼æŽ¥å›¾ç‰‡å‰ç¼€
        String imgPrefix = "";
server/services/src/main/java/com/doumee/service/business/impl/WithdrawalOrdersServiceImpl.java
@@ -19,6 +19,8 @@
import com.doumee.dao.business.model.ShopInfo;
import com.doumee.dao.business.model.WithdrawalOrders;
import com.doumee.dao.dto.WithdrawalApproveDTO;
import com.doumee.config.alipay.AlipayFundTransUniTransfer;
import com.doumee.dao.dto.AlipayTransferDTO;
import com.doumee.dao.dto.WithdrawalDTO;
import com.doumee.dao.system.SystemUserMapper;
import com.doumee.dao.system.model.SystemUser;
@@ -27,6 +29,7 @@
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
@@ -229,6 +232,7 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void approve(WithdrawalApproveDTO dto) {
        if (dto == null || dto.getId() == null || dto.getStatus() == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "审批参数不完整");
@@ -249,13 +253,95 @@
        Integer currentUserId = getCurrentUserId();
        Date now = new Date();
        // å®¡æ‰¹é€šè¿‡æ—¶ï¼Œè°ƒç”¨æ”¯ä»˜å®æ‰“款
        Integer finalStatus = dto.getStatus();
        String doneInfo = null;
        if (Constants.ONE.equals(dto.getStatus())) {
            String aliAccount = order.getAliAccount();
            String aliName = null;
            // ä»Žå¸æœºæˆ–门店获取支付宝账户和实名姓名
            if (StringUtils.isBlank(aliAccount)) {
                if (Constants.equalsInteger(order.getMemberType(), Constants.ONE)) {
                    DriverInfo driver = driverInfoMapper.selectOne(new QueryWrapper<DriverInfo>().lambda()
                            .eq(DriverInfo::getMemberId, order.getMemberId())
                            .eq(DriverInfo::getDeleted, Constants.ZERO)
                            .last("limit 1"));
                    if (driver != null) {
                        aliAccount = driver.getAliAccount();
                        aliName = driver.getAliName();
                    }
                } else if (Constants.equalsInteger(order.getMemberType(), Constants.TWO)) {
                    ShopInfo shop = shopInfoMapper.selectOne(new QueryWrapper<ShopInfo>().lambda()
                            .eq(ShopInfo::getRegionMemberId, order.getMemberId())
                            .eq(ShopInfo::getDeleted, Constants.ZERO)
                            .last("limit 1"));
                    if (shop != null) {
                        aliAccount = shop.getAliAccount();
                        aliName = shop.getAliName();
                    }
                }
            } else {
                // ä»Ž WithdrawalOrders çš„ aliAccount æŸ¥å¯¹åº”姓名
                if (Constants.equalsInteger(order.getMemberType(), Constants.ONE)) {
                    DriverInfo driver = driverInfoMapper.selectOne(new QueryWrapper<DriverInfo>().lambda()
                            .eq(DriverInfo::getMemberId, order.getMemberId())
                            .eq(DriverInfo::getDeleted, Constants.ZERO)
                            .last("limit 1"));
                    if (driver != null) {
                        aliName = driver.getAliName();
                    }
                } else if (Constants.equalsInteger(order.getMemberType(), Constants.TWO)) {
                    ShopInfo shop = shopInfoMapper.selectOne(new QueryWrapper<ShopInfo>().lambda()
                            .eq(ShopInfo::getRegionMemberId, order.getMemberId())
                            .eq(ShopInfo::getDeleted, Constants.ZERO)
                            .last("limit 1"));
                    if (shop != null) {
                        aliName = shop.getAliName();
                    }
                }
            }
            if (StringUtils.isBlank(aliAccount)) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "支付宝提现账户未配置,无法打款");
            }
            // é‡‘额转换:分 â†’ å…ƒ
            Long amountFen = order.getAmount() != null ? order.getAmount() : 0L;
            BigDecimal transAmount = new BigDecimal(amountFen).divide(new BigDecimal(100), 2, BigDecimal.ROUND_HALF_UP);
            AlipayTransferDTO transferDTO = new AlipayTransferDTO();
            transferDTO.setOutBizNo(order.getOutBillNo());
            transferDTO.setTransAmount(transAmount);
            transferDTO.setPayeeAccount(aliAccount);
            transferDTO.setPayeeName(aliName);
            transferDTO.setRemark("提现打款");
            String alipayOrderId;
            try {
                alipayOrderId = AlipayFundTransUniTransfer.transfer(transferDTO);
            } catch (BusinessException e) {
                throw e;
            } catch (Exception e) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "支付宝打款失败:" + e.getMessage());
            }
            doneInfo = "支付宝转账单号:" + alipayOrderId;
        }
        WithdrawalOrders update = new WithdrawalOrders();
        update.setId(dto.getId());
        update.setStatus(dto.getStatus());
        update.setStatus(finalStatus);
        update.setUserId(currentUserId);
        update.setApproveTime(now);
        update.setApproveRemark(dto.getApproveRemark());
        update.setUpdateTime(now);
        if (Constants.ONE.equals(finalStatus)) {
            update.setDoneTime(now);
            update.setDoneInfo(doneInfo);
        } else if (doneInfo != null) {
            update.setDoneTime(now);
            update.setDoneInfo(doneInfo);
        }
        withdrawalOrdersMapper.updateById(update);
        // æ›´æ–°å…³è”的提现 Revenue è®°å½•状态
@@ -267,15 +353,15 @@
                .eq(Revenue::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (withdrawalRevenue != null) {
            Integer revenueStatus = Constants.ONE.equals(dto.getStatus()) ? Constants.ZERO : Constants.ONE; // é€šè¿‡=0成功, é©³å›ž=1失败
            Integer revenueStatus = Constants.ONE.equals(finalStatus) ? Constants.ZERO : Constants.ONE; // é€šè¿‡=0成功, é©³å›ž=1失败
            revenueMapper.update(new UpdateWrapper<Revenue>().lambda()
                    .set(Revenue::getStatus, revenueStatus)
                    .set(Revenue::getUpdateTime, now)
                    .eq(Revenue::getId, withdrawalRevenue.getId()));
        }
        // é©³å›žæ—¶é€€å›žä½™é¢
        if (Constants.TWO.equals(dto.getStatus())) {
        // é©³å›žæˆ–打款失败时退回余额
        if (Constants.TWO.equals(finalStatus)) {
            Long amountFen = order.getAmount() != null ? order.getAmount() : 0L;
            if (amountFen > 0 && order.getMemberId() != null) {
                if (Constants.equalsInteger(order.getMemberType(), Constants.ONE)) {
server/services/src/main/resources/application-dev.yml
@@ -95,12 +95,15 @@
    mchKey: u4TSNtv0wFP7WRfnxBgijYOtRhS9FvlM #商户秘钥
    apiV3Key: 7tG4Vk9Zp2L8dXw5Jq0N3hR6yE1sF3cB #apiV3Key
    serialNumer: 3FE90C2F3D40A56E1C51926F31B8A8D22426CCE0 #商户证书序列号
    notifyUrl: http://xiaopiqiu2.natapp1.cc/web/wxPayNotify
    refundNotifyUrl: http://xiaopiqiu2.natapp1.cc/web/wxRefundNotify
    publicKeyId: PUB_KEY_ID_0112298170022025071700291836000600
    pubKeyPath: D:\DouMee\1229817002_20220310_cert\pub_key.pem #商户支付公钥
    keyPath: D:\DouMee\1229817002_20220310_cert\apiclient_cert.p12
    privateCertPath: D:\DouMee\1229817002_20220310_cert\apiclient_cert.pem
    privateKeyPath: D:\DouMee\1229817002_20220310_cert\apiclient_key.pem
    pubKeyPath: D:\DouMee\1229817002_20220310_cert\pub_key.pem #商户支付公钥
    notifyUrl: http://xiaopiqiu2.natapp1.cc/web/wxPayNotify
    refundNotifyUrl: http://xiaopiqiu2.natapp1.cc/web/wxRefundNotify
    v3NotifyUrl: http://xiaopiqiu2.natapp1.cc/web/api/wxPayV3Notify
    v3RefundNotifyUrl: http://xiaopiqiu2.natapp1.cc/web/api/wxRefundV3Notify
#    appId: wx6264b4f3a697cbe8
#    appSecret: 23734577e8978138c946b727f0394027
@@ -123,5 +126,9 @@
# è…¾è®¯åœ°å›¾apikey
tencent_key: WE3BZ-HN6WS-ONDOH-62QCV-MNL6F-5NFNE
# é«˜å¾·åœ°å›¾apikey
geocode_map_key: 9a62636d82d6c7c2372e57d80f99287c
server/services/src/main/resources/application-pro.yml
@@ -60,6 +60,8 @@
    serialNumer: 3FE90C2F3D40A56E1C51926F31B8A8D22426CCE0 #商户证书序列号
    notifyUrl: http://xiaopiqiu2.natapp1.cc/web/wxPayNotify
    refundNotifyUrl: http://xiaopiqiu2.natapp1.cc/web/wxRefundNotify
    v3NotifyUrl: http://xiaopiqiu2.natapp1.cc/web/api/wxPayV3Notify
    v3RefundNotifyUrl: http://xiaopiqiu2.natapp1.cc/web/api/wxRefundV3Notify
    keyPath: D:\DouMee\1229817002_20220310_cert\apiclient_cert.p12
    privateCertPath: D:\DouMee\1229817002_20220310_cert\apiclient_cert.pem
    privateKeyPath: D:\DouMee\1229817002_20220310_cert\apiclient_key.pem
@@ -84,4 +86,7 @@
# è…¾è®¯åœ°å›¾apikey
tencent_key: WE3BZ-HN6WS-ONDOH-62QCV-MNL6F-5NFNE
tencent_key: WE3BZ-HN6WS-ONDOH-62QCV-MNL6F-5NFNE
# é«˜å¾·åœ°å›¾apikey
geocode_map_key:
server/services/src/main/resources/application-test.yml
@@ -83,6 +83,8 @@
    #mchKey: W97N53Q71326D6JZ2E9HY5M4VT4BAC8S
    notifyUrl: https://test.doumee.cn/jinkuai_admin/web/wxPayNotify
    refundNotifyUrl: https://test.doumee.cn/jinkuai_admin/web/wxRefundNotify
    v3NotifyUrl: https://test.doumee.cn/jinkuai_admin/web/api/wxPayV3Notify
    v3RefundNotifyUrl: https://test.doumee.cn/jinkuai_admin/web/api/wxRefundV3Notify
    keyPath: /usr/local/jars/payFile/apiclient_cert.p12
    privateCertPath: /usr/local/jars/payFile/apiclient_cert.pem
    privateKeyPath: /usr/local/jars/payFile/apiclient_key.pem
@@ -117,4 +119,7 @@
# è…¾è®¯åœ°å›¾apikey
tencent_key: WE3BZ-HN6WS-ONDOH-62QCV-MNL6F-5NFNE
tencent_key: WE3BZ-HN6WS-ONDOH-62QCV-MNL6F-5NFNE
# é«˜å¾·åœ°å›¾apikey
geocode_map_key:
server/web/src/main/java/com/doumee/api/web/ConfigApi.java
@@ -3,7 +3,7 @@
import com.doumee.core.annotation.LoginRequired;
import com.doumee.core.annotation.trace.Trace;
import com.doumee.core.model.ApiResponse;
import com.doumee.core.utils.Tencent.MapUtil;
import com.doumee.core.utils.geocode.MapUtil;
import com.doumee.dao.business.model.Areas;
import com.doumee.dao.business.model.Banner;
import com.doumee.dao.business.model.Category;
server/web/src/main/java/com/doumee/api/web/DriverInfoApi.java
@@ -10,6 +10,7 @@
import com.doumee.dao.dto.DriverActiveOrderDTO;
import com.doumee.dao.dto.DriverGrabOrderDTO;
import com.doumee.dao.dto.DriverLoginRequest;
import com.doumee.dao.dto.DriverPickupDTO;
import com.doumee.dao.dto.DriverRegisterRequest;
import com.doumee.dao.dto.DriverVerifyRequest;
import com.doumee.dao.vo.AccountResponse;
@@ -142,6 +143,19 @@
    @LoginDriverRequired
    @Trace
    @ApiOperation(value = "司机抢单", notes = "对已寄存(status=2)的异地寄存订单发起抢单")
    @PostMapping("/grabOrder")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
            @ApiImplicitParam(paramType = "query", dataType = "Integer", name = "orderId", value = "订单主键", required = true)
    })
    public ApiResponse grabOrder(@RequestParam Integer orderId) {
        driverInfoService.grabOrder(this.getDriverId(), orderId);
        return ApiResponse.success("抢单成功");
    }
    @LoginDriverRequired
    @Trace
    @ApiOperation(value = "司机进行中订单", notes = "查询已抢单(status=3)或派送中(status=4)的订单列表")
    @PostMapping("/activeOrders")
    @ApiImplicitParams({
@@ -176,4 +190,16 @@
        return ApiResponse.success("取消成功");
    }
    @LoginDriverRequired
    @Trace
    @ApiOperation(value = "司机完成取件", notes = "已接单(status=3)时确认取件,订单进入派送中(status=4)")
    @PostMapping("/confirmPickup")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true)
    })
    public ApiResponse confirmPickup(@RequestBody @Valid DriverPickupDTO dto) {
        driverInfoService.confirmPickup(this.getDriverId(), dto);
        return ApiResponse.success("操作成功");
    }
}
server/web/src/main/java/com/doumee/api/web/PaymentCallback.java
@@ -1,21 +1,28 @@
package com.doumee.api.web;
import com.doumee.config.wx.WxMiniConfig;
import com.doumee.config.wx.WxPayV3Service;
import com.doumee.core.constants.Constants;
import com.doumee.core.utils.ID;
import com.doumee.dao.business.OrdersRefundMapper;
import com.doumee.dao.business.OrdersMapper;
import com.doumee.dao.business.model.Notice;
import com.doumee.dao.business.model.Orders;
import com.doumee.dao.business.model.OrdersRefund;
import com.doumee.service.business.NoticeService;
import com.doumee.service.business.OrdersService;
import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.model.RefundNotification;
import com.wechat.pay.java.service.refund.model.Status;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.Objects;
import java.util.HashMap;
import java.util.Map;
/**
 * æ”¯ä»˜å›žè°ƒ
@@ -31,37 +38,43 @@
    @Autowired
    private OrdersService ordersService;
    @Autowired
    private WxPayV3Service wxPayV3Service;
    @Autowired
    private OrdersRefundMapper ordersRefundMapper;
    @Autowired
    private OrdersMapper ordersMapper;
    @Autowired
    private NoticeService noticeService;
    // ==================== V2 å›žè°ƒ ====================
    @PostMapping("/web/api/wxPayNotify")
    public String wxPay_notify(@RequestBody String xmlResult) {
        String wxId = ID.nextGUID();
        log.info("支付回调信息("+wxId+") = > "  + xmlResult);
        if (StringUtils.isEmpty(xmlResult)){
        log.info("V2支付回调信息(" + wxId + ") => " + xmlResult);
        if (StringUtils.isEmpty(xmlResult)) {
            return null;
        }
        try {
            WxPayOrderNotifyResult result = WxMiniConfig.wxPayService.parseOrderNotifyResult(xmlResult);
            //自定义订单号
            String outTradeNo = result.getOutTradeNo();
            //微信订单号
            String paymentNo = result.getTransactionId();
            if (Constants.SUCCESS.equals(result.getReturnCode())) {
                // æ”¯ä»˜æˆåŠŸ
                switch (result.getAttach()) {
                    //寄存订单
                    case "storageOrder": {
                        ordersService.handleStorageOrderPayNotify(outTradeNo, paymentNo);
                        break;
                    }
                    //店铺押金订单
                    case "shopDeposit": {
                        ordersService.handleShopDepositPayNotify(outTradeNo, paymentNo);
                        break;
                    }
                    //逾期费用订单
                    case "overdueFee": {
                        ordersService.handleOverdueFeePayNotify(outTradeNo, paymentNo);
                        break;
@@ -72,11 +85,133 @@
            return WxPayNotifyResponse.fail(result.getReturnMsg());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("微信回调结果异常,异常原因{}", e.getLocalizedMessage());
            log.error("V2微信回调结果异常,异常原因{}", e.getLocalizedMessage());
            return WxPayNotifyResponse.fail(e.getMessage());
        }
    }
    // ==================== V3 å›žè°ƒ ====================
    /**
     * V3支付回调
     */
    @PostMapping("/web/api/wxPayV3Notify")
    public Map<String, String> wxPayV3Notify(
            @RequestHeader("Wechatpay-Serial") String serialNumber,
            @RequestHeader("Wechatpay-Timestamp") String timestamp,
            @RequestHeader("Wechatpay-Nonce") String nonce,
            @RequestHeader("Wechatpay-Signature") String signature,
            @RequestBody String body) {
        String wxId = ID.nextGUID();
        log.info("V3支付回调信息({}) => {}", wxId, body);
        try {
            Transaction transaction = wxPayV3Service.parsePayNotify(
                    serialNumber, timestamp, nonce, signature, body);
            String outTradeNo = transaction.getOutTradeNo();
            String paymentNo = transaction.getTransactionId();
            if (Transaction.TradeStateEnum.SUCCESS.equals(transaction.getTradeState())) {
                String attach = transaction.getAttach();
                if (StringUtils.isNotBlank(attach)) {
                    switch (attach) {
                        case "storageOrder":
                            ordersService.handleStorageOrderPayNotify(outTradeNo, paymentNo);
                            break;
                        case "shopDeposit":
                            ordersService.handleShopDepositPayNotify(outTradeNo, paymentNo);
                            break;
                        case "overdueFee":
                            ordersService.handleOverdueFeePayNotify(outTradeNo, paymentNo);
                            break;
                    }
                }
            }
            Map<String, String> response = new HashMap<>();
            response.put("code", "SUCCESS");
            response.put("message", "处理成功");
            return response;
        } catch (Exception e) {
            log.error("V3支付回调异常,异常原因{}", e.getLocalizedMessage());
            Map<String, String> response = new HashMap<>();
            response.put("code", "FAIL");
            response.put("message", e.getMessage());
            return response;
        }
    }
    /**
     * V3退款回调
     */
    @PostMapping("/web/api/wxRefundV3Notify")
    public Map<String, String> wxRefundV3Notify(
            @RequestHeader("Wechatpay-Serial") String serialNumber,
            @RequestHeader("Wechatpay-Timestamp") String timestamp,
            @RequestHeader("Wechatpay-Nonce") String nonce,
            @RequestHeader("Wechatpay-Signature") String signature,
            @RequestBody String body) {
        String wxId = ID.nextGUID();
        log.info("V3退款回调信息({}) => {}", wxId, body);
        try {
            RefundNotification refundNotification = wxPayV3Service.parseRefundNotify(
                    serialNumber, timestamp, nonce, signature, body);
            log.info("V3退款回调结果, outTradeNo={}, outRefundNo={}, refundStatus={}",
                    refundNotification.getOutTradeNo(),
                    refundNotification.getOutRefundNo(),
                    refundNotification.getRefundStatus());
            // æ ¹æ®é€€æ¬¾å•号更新退款记录状态
            String outRefundNo = refundNotification.getOutRefundNo();
            OrdersRefund refundRecord = ordersRefundMapper.selectOne(
                    new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<OrdersRefund>().lambda()
                            .eq(OrdersRefund::getRefundCode, outRefundNo)
                            .eq(OrdersRefund::getDeleted, Constants.ZERO)
                            .last("limit 1"));
            if (refundRecord != null) {
                Status refundStatus = refundNotification.getRefundStatus();
                if (Status.SUCCESS.equals(refundStatus)) {
                    refundRecord.setStatus(Constants.ONE); // é€€æ¬¾æˆåŠŸ
                    refundRecord.setRefundTime(new java.util.Date());
                } else if (Status.CLOSED.equals(refundStatus) || Status.ABNORMAL.equals(refundStatus)) {
                    refundRecord.setStatus(Constants.TWO); // é€€æ¬¾å¤±è´¥
                }
                ordersRefundMapper.updateById(refundRecord);
                log.info("退款记录状态已更新, refundRecordId={}, status={}", refundRecord.getId(), refundRecord.getStatus());
                // é€€æ¬¾æˆåŠŸ â†’ é€šçŸ¥ä¼šå‘˜
                if (Status.SUCCESS.equals(refundStatus) && refundRecord.getOrderId() != null) {
                    Orders refundOrder = ordersMapper.selectById(refundRecord.getOrderId());
                    if (refundOrder != null) {
                        Notice notice = new Notice();
                        notice.setUserType(0);
                        notice.setUserId(refundOrder.getMemberId());
                        notice.setTitle(Constants.MemberOrderNotify.REFUNDED.getTitle());
                        notice.setContent(Constants.MemberOrderNotify.REFUNDED.format(
                                "orderNo", refundOrder.getCode(),
                                "amount", String.valueOf(Constants.getFormatMoney(refundOrder.getRefundAmount() != null ? refundOrder.getRefundAmount() : 0L))));
                        notice.setObjId(refundOrder.getId());
                        notice.setObjType(0);
                        notice.setStatus(0);
                        notice.setIsdeleted(Constants.ZERO);
                        notice.setCreateDate(new java.util.Date());
                        noticeService.create(notice);
                    }
                }
            }
            Map<String, String> response = new HashMap<>();
            response.put("code", "SUCCESS");
            response.put("message", "处理成功");
            return response;
        } catch (Exception e) {
            log.error("V3退款回调异常,异常原因{}", e.getLocalizedMessage());
            Map<String, String> response = new HashMap<>();
            response.put("code", "FAIL");
            response.put("message", e.getMessage());
            return response;
        }
    }
}