Appearance
支付流程
流程概述
- 要完成支付流程需要先申请成为Mos商户,获取商户IDmcId,配置到微应用后端服务
- 微应用调用微应用后端服务创建预支付订单,此时需要用到上一步申请的商户ID
- 微应用调用
mos.pay()
唤起支付页面 - 用户输入支付密码,完成支付,此时订单状态未知
- 通过回调或者主动查询获取订单支付状态:
- 回调:如果微应用后端服务在调用创建预支付订单时传入了回调地址,并正确提供了回调接口
- 主动查询:微应用服后端服务主动调用Mos接口服务接口获取订单支付状态
前端实现
支付示例代码
javascript
// 唤起支付页面,在用户输入支付密码后支付完成
const payRes = await mos.pay({
amount: "16.99",
currency: "USD",
appKey: "your_app_key",
prepayId: "your_prepay_id",
});
const { result } = payRes;
if (result === "SUCCESS") {
// 支付完成后的业务逻辑,如跳转到支付完成页面
}
后端实现
java
import cn.hutool.crypto.digest.MD5;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import java.util.TreeMap;
/**
* 微应用支付公共服务
*
* @author tom
*/
@Slf4j
@Component
public class MiniAppPayService {
/**
* Mos微应用的调用地址
*/
@Value("${mos.mini-app.base-url}")
private String baseUrl;
/**
* Mos微应用的appKey
*/
@Value("${mos.mini-app.app-key}")
private String appKey;
/**
* Mos微应用的appSecret
*/
@Value("${mos.mini-app.app-secret}")
private String appSecret;
private final RestTemplate restTemplate;
public MiniAppPayService() {
SimpleClientHttpRequestFactory httpRequestFactory = new SimpleClientHttpRequestFactory();
httpRequestFactory.setConnectTimeout(30000);
httpRequestFactory.setReadTimeout(60000);
this.restTemplate = new RestTemplate(httpRequestFactory);
}
/**
* 请求Mos服务端微应用开放接口
*
* @param uri 接口URI
* @param ao 请求参数
* @param clazz 响应类型
* @return 响应对象
*/
private <T> T call(String uri, BaseSignedAo ao, Class<T> clazz) {
// 签名
ao.setAppKey(this.appKey);
Map<String, String> data = JSON.parseObject(JSON.toJSONString(ao), new TypeReference<Map<String, String>>() {
});
String sign = MiniAppUtil.generateSignature(data, this.appSecret);
ao.setSign(sign);
// 发起请求
String url = this.baseUrl + uri;
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, ao, String.class);
if (responseEntity.getStatusCode() != HttpStatus.OK || responseEntity.getBody() == null) {
log.error("微应用请求异常 uri={} ao={}", uri, JSON.toJSONString(ao));
return null;
}
// 解析响应 code(int) message(String) data(T)
JSONObject result = JSON.parseObject(responseEntity.getBody());
if ((int)result.get("code") != 200) {
log.error("微应用请求异常 uri={} ao={} result={}", uri, JSON.toJSONString(ao), JSON.toJSONString(result));
return null;
}
return JSON.to(clazz, result.get("data"));
}
public PrepayOrderVo prepay(CreatePrepayOrderAo ao) {
return this.call("/open-apis/mp/v1/pay/prepay", ao, PrepayOrderVo.class);
}
public OrderQueryVo orderQuery(OrderQueryAo ao) {
return this.call("/open-apis/mp/v1/pay/orderQuery", ao, OrderQueryVo.class);
}
@Data
public static class BaseSignedAo {
/**
* 微应用 appKey
*/
@NotNull
private String appKey;
/**
* 签名
*/
@NotNull
private String sign;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class CreatePrepayOrderAo extends BaseSignedAo {
/**
* mcId
*/
@NotNull
private String mcId;
/**
* 随机字符串,需保证系统内唯一
*/
private String nonceStr;
/**
* 订单描述
*/
private String desc;
/**
* 商户微应用系统订单号
*/
@NotNull
private String outTradeNo;
/**
* 货币单位 USD-美元 KHR-瑞尔
*/
@NotNull
private String currency;
/**
* 订单金额
*/
@NotNull
private String totalAmount;
/**
* 回调地址
*/
@NotNull
private String notifyUrl;
/**
* openid
*/
@NotNull
private String openid;
/**
* 订单失效时间(时间戳)
*/
private String expireTime;
}
@Data
public class PrepayOrderVo {
/**
* 预支付订单id
*/
private String prepayId;
}
@Data
@EqualsAndHashCode(callSuper = true)
public class OrderQueryAo extends BaseSignedAo {
/**
* 32位随机字符串,需保证系统内唯一
*/
@NotNull
private String nonceStr;
/**
* 商户微应用系统订单号
*/
@NotNull
private String outTradeNo;
}
@Data
public class OrderQueryVo {
/**
* openid
*/
private String openid;
/**
* 预支付订单id
*/
private String prepayId;
/**
* 商户微应用系统订单号
*/
private String outTradeNo;
/**
* 国家编码
*/
private String country;
/**
* 货币类型
*/
private String currency;
/**
* 订单金额
*/
private String totalAmount;
/**
* 商品描述
*/
private String desc;
/**
* 订单状态
*/
private String status;
/**
* 订单失效时间
*/
private Long expireTime;
/**
* 创建时间
*/
private Long createTime;
}
public static class MiniAppUtil {
/**
* 生成签名
*/
public static String generateSignature(Map<String, String> data, String appSecret) {
// 1. Sort the parameters alphabetically
Map<String, String> sortedData = new TreeMap<>(data);
// 2. Concatenate the parameters in the format key1=value1&key2=value2...
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedData.entrySet()) {
if ("sign".equals(entry.getKey()) || entry.getValue() == null || entry.getValue().isEmpty()) {
// sign 不参与签名 && 空值不参与签名
continue;
}
sb.append(entry.getKey()).append("=").append(entry.getValue().trim()).append("&");
}
sb.append("secret=").append(appSecret);
// 3. Hash the concatenated string using MD5
return new MD5().digestHex(sb.toString());
}
}
}
java
import cn.hutool.crypto.digest.MD5;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import java.util.TreeMap;
/**
* 微应用支付公共服务
*
* @author tom
*/
@Slf4j
@Component
public class MiniAppPayService {
/**
* Mos微应用的调用地址
*/
@Value("${mos.mini-app.base-url}")
private String baseUrl;
/**
* Mos微应用的appKey
*/
@Value("${mos.mini-app.app-key}")
private String appKey;
/**
* Mos微应用的appSecret
*/
@Value("${mos.mini-app.app-secret}")
private String appSecret;
private final RestTemplate restTemplate;
public MiniAppPayService() {
SimpleClientHttpRequestFactory httpRequestFactory = new SimpleClientHttpRequestFactory();
httpRequestFactory.setConnectTimeout(30000);
httpRequestFactory.setReadTimeout(60000);
this.restTemplate = new RestTemplate(httpRequestFactory);
}
/**
* 请求Mos服务端微应用开放接口
*
* @param uri 接口URI
* @param ao 请求参数
* @param clazz 响应类型
* @return 响应对象
*/
private <T> T call(String uri, BaseSignedAo ao, Class<T> clazz) {
// 签名
ao.setAppKey(this.appKey);
Map<String, String> data = JSON.parseObject(JSON.toJSONString(ao), new TypeReference<Map<String, String>>() {
});
String sign = MiniAppUtil.generateSignature(data, this.appSecret);
ao.setSign(sign);
// 发起请求
String url = this.baseUrl + uri;
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, ao, String.class);
if (responseEntity.getStatusCode() != HttpStatus.OK || responseEntity.getBody() == null) {
log.error("微应用请求异常 uri={} ao={}", uri, JSON.toJSONString(ao));
return null;
}
// 解析响应 code(int) message(String) data(T)
JSONObject result = JSON.parseObject(responseEntity.getBody());
if ((int)result.get("code") != 200) {
log.error("微应用请求异常 uri={} ao={} result={}", uri, JSON.toJSONString(ao), JSON.toJSONString(result));
return null;
}
return JSON.to(clazz, result.get("data"));
}
public PrepayOrderVo prepay(CreatePrepayOrderAo ao) {
return this.call("/open-apis/mp/v1/pay/prepay", ao, PrepayOrderVo.class);
}
public OrderQueryVo orderQuery(OrderQueryAo ao) {
return this.call("/open-apis/mp/v1/pay/orderQuery", ao, OrderQueryVo.class);
}
@Data
public static class BaseSignedAo {
/**
* 微应用 appKey
*/
@NotNull
private String appKey;
/**
* 签名
*/
@NotNull
private String sign;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class CreatePrepayOrderAo extends BaseSignedAo {
/**
* mcId
*/
@NotNull
private String mcId;
/**
* 随机字符串,需保证系统内唯一
*/
private String nonceStr;
/**
* 订单描述
*/
private String desc;
/**
* 商户微应用系统订单号
*/
@NotNull
private String outTradeNo;
/**
* 货币单位 USD-美元 KHR-瑞尔
*/
@NotNull
private String currency;
/**
* 订单金额
*/
@NotNull
private String totalAmount;
/**
* 回调地址
*/
@NotNull
private String notifyUrl;
/**
* openid
*/
@NotNull
private String openid;
/**
* 订单失效时间(时间戳)
*/
private String expireTime;
}
@Data
public class PrepayOrderVo {
/**
* 预支付订单id
*/
private String prepayId;
}
@Data
@EqualsAndHashCode(callSuper = true)
public class OrderQueryAo extends BaseSignedAo {
/**
* 32位随机字符串,需保证系统内唯一
*/
@NotNull
private String nonceStr;
/**
* 商户微应用系统订单号
*/
@NotNull
private String outTradeNo;
}
@Data
public class OrderQueryVo {
/**
* openid
*/
private String openid;
/**
* 预支付订单id
*/
private String prepayId;
/**
* 商户微应用系统订单号
*/
private String outTradeNo;
/**
* 国家编码
*/
private String country;
/**
* 货币类型
*/
private String currency;
/**
* 订单金额
*/
private String totalAmount;
/**
* 商品描述
*/
private String desc;
/**
* 订单状态
*/
private String status;
/**
* 订单失效时间
*/
private Long expireTime;
/**
* 创建时间
*/
private Long createTime;
}
public static class MiniAppUtil {
/**
* 生成签名
*/
public static String generateSignature(Map<String, String> data, String appSecret) {
// 1. Sort the parameters alphabetically
Map<String, String> sortedData = new TreeMap<>(data);
// 2. Concatenate the parameters in the format key1=value1&key2=value2...
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedData.entrySet()) {
if ("sign".equals(entry.getKey()) || entry.getValue() == null || entry.getValue().isEmpty()) {
// sign 不参与签名 && 空值不参与签名
continue;
}
sb.append(entry.getKey()).append("=").append(entry.getValue().trim()).append("&");
}
sb.append("secret=").append(appSecret);
// 3. Hash the concatenated string using MD5
return new MD5().digestHex(sb.toString());
}
}
}
微应用后端服务的公共模块提供了通用的登录方法,及其相应的鉴权拦截器和和获取用户登录的方法,详见《微应用后端开发》章节
支付回调
回调描述
用户使用支付功能,当用户成功支付订单后,mos支付会通过POST的请求方式,向商户预先设置的回调地址发送回调通知,让商户知晓用户已完成支付
回调处理步骤
- 商户接收回调通知报文 mos支付会通过POST的方式向回调地址发送回调报文,回调通知的请求主体中会包含JSON格式的通知参数,具体的通知参数如下:
java
{
"timeEnd": "1751265114241",
"country": "KH",
"mcId": "1719971391999086594",
"openid": "531d4fc958c9cbe5e5e719013b779c0b",
"resultCode": "SUCCESS",
"sign": "bcf0b5093e67ba0a9af4b5aaa57f2998",
"resultMsg": "OK",
"totalAmount": "14.500",
"outTradeNo": "692751186858053",
"appKey": "58e5a50ad0b243408c77622d696ff76f",
"currency": "USD",
"prepayId": "d7a7d0f1a0c44d289b1db93aad921c8e"
}
参数说明如下:
属性 | 类型 | 必填 | 说明 |
---|---|---|---|
timeEnd | String | 是 | 支付完成的时间,UTC时间戳 |
country | String | 是 | 订单所属国家编码 |
mcId | String | 是 | 支付商户id,微应用提前申请 |
appKey | String | 是 | 微应用appKey,可到微应用控制台获取 |
openid | String | 是 | 下单用户openid |
resultCode | String | 是 | 订单状态码:SUCCESS-成功,FAIL-失败 |
resultMsg | String | 是 | 成功或失败原因 |
totalAmount | String | 是 | 订单总金额 |
currency | String | 是 | 币种 |
outTradeNo | String | 是 | 商户微应用系统订单号 |
prepayId | String | 是 | 预支付订单id |
desc | String | 是 | 商品描述 |
businessData | String | 是 | 外部业务字段 |
sign | String | 是 | 签名,用于客户端验证接口有效性。签名生成方法见前面工具类 |
2. 回调验签
商户接收到回调通知报文后,需在5秒内完成对报文的验签,并应答回调通知。 对回调通知进行验签,以保证该次接口回调的有效性,具体验签方法见前面工具类。
3. 对回调通知进行应答
商户验签后,根据验签结果对回调进行应答,以json格式返回处理结果。
成功范例:
json
{
"returnCode": "SUCCESS",
"returnMsg": "OK"
}
失败范例:
json
{
"returnCode": "FAIL",
"returnMsg": "Internal server error"
}
4. mos支付回调处理机制说明
mos支付接收到商户的应答后,会根据应答结果做对应的逻辑处理:
- 若商户应答回调接收成功,mos支付将不再重复发送该回调通知。若因网络或其他原因,商户收到了重复的回调通知,请按正常业务流程进行处理并应答。
- 若商户应答回调接收失败,或超时(5s)未应答时,mos支付会按照(15s/30s/3m/10m/20m/30m/60m/3h/6h)的频次重复发送回调通知,直至mos支付接收到商户应答成功,或达到最大发送次数(9次)