原创

Java-美团企业版对接文档-免登录接口对接

Java-美团企业版对接文档-免登录接口对接

以下是将两个代码文件合并后的完整文档,用于对接美团企业版H5系统。文档包含配置类、控制器和工具类,并添加了必要的注释说明:

参考文档:美团企业版开放平台


import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.google.common.collect.Maps;
import com.manyun.common.core.utils.SpringUtils;
import com.meituan.sqt.enums.EncryptionAlgorithmEnum;
import com.meituan.sqt.enums.ResponseStatusEnum;
import com.meituan.sqt.model.HttpResponse;
import com.meituan.sqt.request.in.cashier.ThirdCashierPayCallbackRequest;
import com.meituan.sqt.request.in.login.LoginRequest;
import com.meituan.sqt.request.in.order.OrderDetailQueryRequest;
import com.meituan.sqt.response.in.BaseApiResponse;
import com.meituan.sqt.response.in.cashier.ThirdCashierPayCallbackResult;
import com.meituan.sqt.response.in.order.OrderDetailQueryResult;
import com.meituan.sqt.utils.EncryptUtil;
import com.meituan.sqt.utils.HttpClientUtil;
import com.meituan.sqt.utils.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@Slf4j
@Component
public class MeituanUtil {
   


    private static final MeituanConfig meituanConfig = SpringUtils.getBean(MeituanConfig.class);


    /** * 发送支付成功通知到美团 * * @param snOrderNo 美团交易号 * @param orderNo 第三方交易号 * @param tradeAmount 交易金额 * @param notifyUrl 美团通知URL * @return 是否发送成功 */
    public static boolean sendPaymentNotify(String snOrderNo, String orderNo,
                                            String tradeAmount, String notifyUrl) {
   
        try {
   
            // 验证必要参数
            if (StrUtil.isBlank(snOrderNo)) {
   
                throw new IllegalArgumentException("交易号(snOrderNo)不能为空");
            }
            if (StrUtil.isBlank(orderNo)) {
   
                throw new IllegalArgumentException("第三方交易号(orderNo)不能为空");
            }
            if (StrUtil.isBlank(tradeAmount)) {
   
                throw new IllegalArgumentException("交易金额(tradeAmount)不能为空");
            }
            if (StrUtil.isBlank(notifyUrl)) {
   
                throw new IllegalArgumentException("通知URL(notifyUrl)不能为空");
            }

            // 验证金额格式
            if (!tradeAmount.matches("^\d+(\.\d{1,2})?$")) {
   
                throw new IllegalArgumentException("交易金额格式不正确");
            }

            // 构建请求对象
            ThirdCashierPayCallbackRequest request = new ThirdCashierPayCallbackRequest();
            request.setTs(System.currentTimeMillis());
            request.setEntId(meituanConfig.getEntId());
            // 美团企业版交易号
            request.setTradeNo(snOrderNo);
            // 三方收银台交易号
            request.setThirdTradeNo(orderNo);
            request.setTradeAmount(tradeAmount);


            // API调用
            // 注意:超时时间默认以请求对象中注解ApiMeta上设置的为准,也可以自定义传递对应的超时时间
            BaseApiResponse<ThirdCashierPayCallbackResult> response = meituanConfig.getSqtClient().invokeApi(notifyUrl, request, null, null);
            // 响应结果为空处理
            if (response == null) {
   
                // 处理响应结果为空情况
                // ...
                return false;
            }
            // 获取结果
            if (ResponseStatusEnum.SUCCESS.getCode().intValue() == response.getStatus()) {
   
                //处理成功 打印详情
                log.info("发送支付成功通知成功 - tradeNo: {} ,suc:{}", snOrderNo, response.getData());
                return true;
            } else {
   
                log.error("发送支付成功通知失败 - tradeNo: {} ,err:{}", snOrderNo, response.getMsg());
                return false;
            }

        } catch (Exception e) {
   
            log.error("发送支付成功通知失败 - tradeNo: {}", snOrderNo, e);
            return false;
        }
    }


    /** * 获取订单详情 */
    public static String getOrderDetail(String snOrderNo) {
   
        try {
   

            OrderDetailQueryRequest orderDetailQueryRequest = new OrderDetailQueryRequest();
            orderDetailQueryRequest.setTs(System.currentTimeMillis());
            orderDetailQueryRequest.setEntId(meituanConfig.getEntId());
            orderDetailQueryRequest.setSqtBizOrderId(Long.valueOf(snOrderNo));

            // 2. API调用
            // 注意:超时时间默认以请求对象中注解ApiMeta上设置的为准,也可以自定义传递对应的超时时间
            BaseApiResponse<OrderDetailQueryResult> response = meituanConfig.getSqtClient().invokeApi(meituanConfig.getGetOrderDetailUrl(), orderDetailQueryRequest, null, null);

            // 3. 响应结果为空处理
            if (response == null) {
   
                throw new RuntimeException("获取订单详情失败");
            }

            // 4. 获取结果
            if (Objects.equals(ResponseStatusEnum.SUCCESS.getCode(), response.getStatus())) {
   
                // 4.1 请求成功,获取订单详情查询结果
                OrderDetailQueryResult realData = response.getRealData();

                // 判断是否成功
                return realData.getOrigOrderInfo();

            } else {
   
                throw new RuntimeException("获取订单详情失败");
            }

        } catch (Exception e) {
   
            log.error("获取订单详情失败 - tradeNo: {}", snOrderNo, e);
            throw new RuntimeException("获取订单详情失败");
        }
    }

    private static final Map<Integer, String> locationMap = Map.of(1, "mt_waimai", 2, "mt_maicai", 3, "sqt_order_detail");

    /** * 跳转美团 * location 1=mt_waimai 2=mt_maicai 3=sqt_order_detail */
    public static String getMeituanUrl(Long userId, String phone, Integer location, String snOrderNo, String tokenValue) {
   
        try {
   
            Map<String, String> headerMap = Maps.newHashMap();
            headerMap.put("Accept", "application/json");
            headerMap.put("Content-type", "application/x-www-form-urlencoded; charset=utf-8");
            Map<String, String> params = buildRequestMap(buildLoginRequestBody(userId, phone, location, snOrderNo, tokenValue));
            log.info("params = " + params);
            HttpResponse httpResponse = HttpClientUtil.invokePost(meituanConfig.getLoginUrl(), headerMap, params, 2000, 3000);
            // 如需json方式调用接口,参考以下注释代码
// httpResponse = HttpClientUtil.invokePost2Json(LOGIN_URL_TEST, JsonUtil.object2Json(requestParamMap), 2000,3000);
            // 成功
            if (httpResponse.getStatus() == 302) {
   
                System.out.println("url = " + httpResponse.getHeader().get("Location").get(0));
                return httpResponse.getHeader().get("Location").get(0);
            } else {
   
                System.out.println("httpResponse = " + JsonUtil.object2Json(httpResponse));
                throw new RuntimeException("获取美团链接失败");
            }
        } catch (Exception e) {
   
            // 调用接口超时或其他场景 可以根据需要增加重试
            System.out.println("e = " + e);
            throw new RuntimeException(e);
        }
    }

    public static Map<String, String> buildRequestMap(String rawContent) throws Exception {
   
        String iv = genIvParam(meituanConfig.getSqtClient().getEncryptionAlgorithm());
        String content = EncryptUtil.encrypt(meituanConfig.getSqtClient().getEncryptionAlgorithm(), rawContent, meituanConfig.getSecretKey(), iv);
        log.info("rawContent = " + rawContent);
        Map<String, String> requestParamMap = Maps.newHashMap();
        requestParamMap.put("accessKey", meituanConfig.getAccessKey());
        requestParamMap.put("content", content);
        if (StringUtils.isNotBlank(iv)) {
   
            requestParamMap.put("iv", iv);
        }

        return requestParamMap;
    }

    private static String genIvParam(EncryptionAlgorithmEnum encryptionAlgorithmEnum) {
   
        // ecb模式不需要iv参数
        if (EncryptionAlgorithmEnum.ecbMode(encryptionAlgorithmEnum)) {
   
            return null;
        }
        int length = 16;
        if (EncryptionAlgorithmEnum.gcmMode(encryptionAlgorithmEnum)) {
   
            length = 12;
        }
        byte[] iv = new byte[length]; // 创建一个指定长度的字节数组来存储IV
        SecureRandom random = new SecureRandom(); // 实例化一个安全随机数生成器
        random.nextBytes(iv); // 使用随机数填充字节数组
        return java.util.Base64.getEncoder().encodeToString(iv);
    }

    private static String buildLoginRequestBody(Long userId, String phone, Integer location, String snOrderNo, String tokenValue) {
   
        LoginRequest loginRequest = new LoginRequest();
        loginRequest.setTs(System.currentTimeMillis());
        loginRequest.setEntId(meituanConfig.getEntId());
        loginRequest.setStaffInfo(buildLoginStaffInfo(userId, phone));
        loginRequest.setNonce(RandomStringUtils.random(36, true, true));
        loginRequest.setProductType(locationMap.get(location));
// 这里如果要使用bizParam,并且map的value不是String和包装类型(Integer等),需自行定义java类
        if (location == 3) {
   
            Map<String, Object> bizParam = new HashMap<>();
            bizParam.put("sqtBizOrderId", snOrderNo);
            loginRequest.setBizParam(bizParam);
        }
        JSONObject ssoUser = new JSONObject();
        ssoUser.set("token", tokenValue);
        ssoUser.set("userId", userId);
        loginRequest.setSsoUser(JSONUtil.toJsonStr(ssoUser));
        return JsonUtil.object2Json(loginRequest, false);
    }

    private static LoginRequest.LoginStaffInfo buildLoginStaffInfo(Long userId, String phone) {
   
        LoginRequest.LoginStaffInfo loginStaffInfo = new LoginRequest.LoginStaffInfo();
        // 传企业的唯一标识,这里以手机号为例
        loginStaffInfo.setStaffPhone(phone);
        return loginStaffInfo;
    }
}


import com.meituan.sqt.client.SqtClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "meituan")
@Data
public class MeituanConfig {
   

    public String accessKey;

    public String secretKey;

    public Long entId;

    public String paySuccessUrl;

    public String getOrderDetailUrl;

    public String loginUrl;

    @Bean
    public SqtClient getSqtClient() {
   
        return new SqtClient.Builder()
            .setEntId(entId)
            .setAccessKey(accessKey)
            .setSecretKey(secretKey)
            .build();
    }

}

import cn.dev33.satoken.annotation.SaIgnore;
import com.manyun.app.meituan.MeituanConfig;
import com.manyun.app.service.ExternalOrderService;
import com.manyun.common.core.utils.SpringUtils;
import com.meituan.sqt.request.out.ApiRequest;
import com.meituan.sqt.request.out.cashier.StandardCashierCloseOrderRequest;
import com.meituan.sqt.request.out.cashier.StandardCashierCreateOrderRequest;
import com.meituan.sqt.request.out.cashier.StandardCashierPayQueryRequest;
import com.meituan.sqt.request.out.cashier.StandardCashierRefundRequest;
import com.meituan.sqt.response.out.budget.BaseApiResponse;
import com.meituan.sqt.response.out.cashier.StandardCashierCloseOrderResponse;
import com.meituan.sqt.response.out.cashier.StandardCashierCreateOrderResponse;
import com.meituan.sqt.response.out.cashier.StandardCashierPayQueryResponse;
import com.meituan.sqt.response.out.cashier.StandardCashierRefundResponse;
import com.meituan.sqt.utils.EncryptUtil;
import com.meituan.sqt.utils.JsonUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@CrossOrigin
@RestController
@RequestMapping("/app/meituan")
@Slf4j
@SaIgnore
public class MeituanNotifyController {
   

    @Resource
    private ExternalOrderService externalOrderService;

    private static final MeituanConfig meituanConfig = SpringUtils.getBean(MeituanConfig.class);


    @PostMapping(value = "/createOrder", consumes = "application/json; charset=utf-8", produces = "application/json")
    @ResponseBody
    public BaseApiResponse createOrder(@RequestBody ApiRequest apiRequest) {
   
        log.info("【三方收银-收单】 apiRequest:{}", JsonUtil.object2Json(apiRequest));
        try {
   
            // 解密
            String decryptContent = EncryptUtil.aesDecrypt(apiRequest.getContent(), meituanConfig.getSecretKey());
            StandardCashierCreateOrderRequest request = JsonUtil.json2Object(decryptContent, StandardCashierCreateOrderRequest.class);
            log.info("【三方收银-收单】解密得到 request:{}", JsonUtil.object2Json(request));

            // 企业内部业务逻辑
            StandardCashierCreateOrderResponse respBody = externalOrderService.addMeituanOrder(request);
            log.info("【三方收银-收单】response:{}", JsonUtil.object2Json(respBody));

            // 响应
            final String encryptData = EncryptUtil.aesEncrypt(JsonUtil.object2Json(respBody), meituanConfig.getSecretKey());
            log.info("【三方收银-收单】response:{}", JsonUtil.object2Json(respBody));
            return BaseApiResponse.success(encryptData, request.getTraceId());
        } catch (Exception e) {
   
            // 异常处理示例
            log.error("【三方收银-收单】异常, apiRequest: {}", JsonUtil.object2Json(apiRequest), e);
            return BaseApiResponse.fail(500, "收单内部异常");
        }
    }

    @PostMapping(value = "/refund", consumes = "application/json; charset=utf-8", produces = "application/json")
    @ResponseBody
    public BaseApiResponse refund(@RequestBody ApiRequest apiRequest) {
   
        log.info("【三方收银-退款】 apiRequest:{}", JsonUtil.object2Json(apiRequest));
        try {
   
            // 解密
            String decryptContent = EncryptUtil.aesDecrypt(apiRequest.getContent(), meituanConfig.getSecretKey());
            StandardCashierRefundRequest request = JsonUtil.json2Object(decryptContent, StandardCashierRefundRequest.class);
            log.info("【三方收银-退款】解密得到 request:{}", JsonUtil.object2Json(request));

            // 企业内部业务逻辑
            StandardCashierRefundResponse respBody = externalOrderService.refundMeituanOrder(request);
            log.info("【三方收银-退款】response:{}", JsonUtil.object2Json(respBody));
            if (respBody == null) {
   
                return BaseApiResponse.fail(410, "订单不存在");
            }
            // 响应
            final String encryptData = EncryptUtil.aesEncrypt(JsonUtil.object2Json(respBody), meituanConfig.getSecretKey());
            log.info("【三方收银-退款】apiResponse:{}", JsonUtil.object2Json(respBody));
            return BaseApiResponse.success(encryptData, request.getTraceId());
        } catch (RuntimeException e) {
   
            log.error("【三方收银-退款】异常, apiRequest: {}", JsonUtil.object2Json(apiRequest), e);
            return BaseApiResponse.fail( 411, "退款超额");
        } catch (Exception e) {
   
            // 异常处理示例
            log.error("【三方收银-退款】异常, apiRequest: {}", JsonUtil.object2Json(apiRequest), e);
            return BaseApiResponse.fail(500, "退款内部异常");
        }
    }


    @PostMapping(value = "/payStatusQuery", consumes = "application/json; charset=utf-8", produces = "application/json")
    @ResponseBody
    public BaseApiResponse payStatusQuery(@RequestBody ApiRequest apiRequest) {
   
        log.info("【三方收银-支付状态查询】 apiRequest:{}", JsonUtil.object2Json(apiRequest));
        try {
   
            // 解密
            String decryptContent = EncryptUtil.aesDecrypt(apiRequest.getContent(), meituanConfig.getSecretKey());
            StandardCashierPayQueryRequest request = JsonUtil.json2Object(decryptContent, StandardCashierPayQueryRequest.class);
            log.info("【三方收银-支付状态查询】解密得到 request:{}", JsonUtil.object2Json(request));

            // 企业内部业务逻辑
            StandardCashierPayQueryResponse respBody = externalOrderService.meituanOrderPayQuery(request);
            log.info("【三方收银-支付状态查询】response:{}", JsonUtil.object2Json(respBody));
            if (respBody == null) {
   
                return BaseApiResponse.fail(410, "订单不存在");
            }
            // 响应
            final String encryptData = EncryptUtil.aesEncrypt(JsonUtil.object2Json(respBody), meituanConfig.getSecretKey());
            log.info("【三方收银-支付状态查询】apiResponse:{}", JsonUtil.object2Json(respBody));
            return BaseApiResponse.success(encryptData, request.getTraceId());
        } catch (Exception e) {
   
            // 异常处理示例
            log.error("【三方收银-支付状态查询】异常, apiRequest: {}", JsonUtil.object2Json(apiRequest), e);
            return BaseApiResponse.fail(500, "支付状态查询内部异常");
        }
    }

    @PostMapping(value = "/closeOrder", consumes = "application/json; charset=utf-8", produces = "application/json")
    @ResponseBody
    public BaseApiResponse closeOrder(@RequestBody ApiRequest apiRequest) {
   
        log.info("【三方收银-关单】 apiRequest:{}", JsonUtil.object2Json(apiRequest));
        try {
   
            // 解密
            String decryptContent = EncryptUtil.aesDecrypt(apiRequest.getContent(), meituanConfig.getSecretKey());
            StandardCashierCloseOrderRequest request = JsonUtil.json2Object(decryptContent, StandardCashierCloseOrderRequest.class);
            log.info("【三方收银-关单】解密得到 request:{}", JsonUtil.object2Json(request));

            // 企业内部业务逻辑
            StandardCashierCloseOrderResponse respBody = externalOrderService.closeMeituanOrder(request);
            log.info("【三方收银-关单】response:{}", JsonUtil.object2Json(respBody));
            if (respBody == null) {
   
                return BaseApiResponse.fail(410, "订单不存在");
            }

            // 响应
            final String encryptData = EncryptUtil.aesEncrypt(JsonUtil.object2Json(respBody), meituanConfig.getSecretKey());
            log.info("【三方收银-关单】apiResponse:{}", JsonUtil.object2Json(respBody));
            return BaseApiResponse.success(encryptData, request.getTraceId());
        } catch (Exception e) {
   
            // 异常处理示例
            log.error("【三方收银-关单】异常, apiRequest: {}", JsonUtil.object2Json(apiRequest), e);
            return BaseApiResponse.fail(500, "关单内部异常");
        }
    }
}

文档说明

1. 核心组件
  • MeituanConfig:美团支付配置类,包含所有必要的配置参数

  • MeituanUtil:支付工具类,封装常用操作(通知、查询、跳转等)

  • MeituanNotifyController:支付回调控制器,处理美团所有支付相关回调

  • 美团SDK下载地址美团企业版开放平台

2. 接口说明
接口路径 功能说明 请求参数 响应格式
/app/meituan/createOrder 创建支付订单 加密的订单数据 加密的响应数据
/app/meituan/refund 处理退款请求 加密的退款数据 加密的响应数据
/app/meituan/payStatusQuery 支付状态查询 加密的查询参数 加密的响应数据
/app/meituan/closeOrder 关闭未支付订单 加密的关单参数 加密的响应数据
3. 安全机制
  • 所有请求/响应数据使用AES加密

  • 请求需包含美团分配的accessKey和secretKey

  • 支付通知需验证金额格式和参数完整性

4. 使用流程
  1. 配置美团参数(application.yml):

    meituan:
      # 美团企业版分配的配置
      accessKey: xxxxxxxxx
      secretKey: xxxxxxxxx
      entId: xxxx
      # 支付相关配置
      paySuccessUrl: xxxxxxxxx
      # 获取美团订单详情接口 
      # 正式:https://bep-openapi.meituan.com/api/sqt/open/order/queryDetail
      # 测试:https://waimai-openapi.apigw.test.meituan.com/api/sqt/open/order/queryDetail
      getOrderDetailUrl: https://bep-openapi.meituan.com/api/sqt/open/order/queryDetail
      # 免登录登录接口 
      # 正式:https://bep-openapi.meituan.com/api/sqt/open/login/h5/loginFree/redirection
      # 测试:https://waimai-openapi.apigw.test.meituan.com/api/sqt/open/login/h5/loginFree/redirection?test_open_swimlane=test-open
      loginUrl: https://bep-openapi.meituan.com/api/sqt/open/login/h5/loginFree/redirection
    
  2. 调用创建订单接口生成支付链接

  3. 用户支付成功后,美团会异步通知到paySuccessUrl

  4. 使用MeituanUtil.sendPaymentNotify()确认支付

5. 注意事项
  • 所有金额参数单位:元(保留两位小数)

  • 加解密使用美团提供的EncryptUtil工具类

  • 跳转链接有效期15分钟,需及时使用

  • 支付/退款通知需做幂等处理,防止重复通知

此文档包含美团企业版H5支付的核心对接代码,可直接用于项目集成。实际使用时需配合美团官方SDK和商户后台配置。

正文到此结束
本文目录