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. 使用流程
-
配置美团参数(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 -
调用创建订单接口生成支付链接
-
用户支付成功后,美团会异步通知到
paySuccessUrl -
使用
MeituanUtil.sendPaymentNotify()确认支付
5. 注意事项
-
所有金额参数单位:元(保留两位小数)
-
加解密使用美团提供的EncryptUtil工具类
-
跳转链接有效期15分钟,需及时使用
-
支付/退款通知需做幂等处理,防止重复通知
此文档包含美团企业版H5支付的核心对接代码,可直接用于项目集成。实际使用时需配合美团官方SDK和商户后台配置。
正文到此结束
- 本文标签: 其他
- 本文链接: https://www.battr.cn/article/120
- 版权声明: 本文由Jack.Chen原创发布,转载请遵循《 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)》许可协议授权