PAYATHON 2026

Flutter 中如何生成 Montypay 的有效 hash

支付阿杰

结论

Montypay 通常不是将参数拼接后直接计算大写 MD5,而是按下面的顺序生成 hash:

SHA1(MD5(UPPERCASE(orderNumber + orderAmount + orderCurrency + orderDescription + merchantPass)))

具体步骤如下:

  1. 按规定顺序拼接字段,不添加任何分隔符。
  2. 将拼接后的整个字符串转为大写。
  3. 计算 MD5,得到小写十六进制字符串。
  4. 将这个 MD5 字符串作为输入,再计算 SHA-1。
  5. 最终提交小写的 SHA-1 十六进制字符串,长度通常为 40。

当前代码只计算了 MD5,还将结果转成了大写,所以无法通过服务端校验。

Flutter 实现

使用 Dart 的 crypto 包:

dependencies:
  crypto: ^3.0.0

生成 hash:

import 'dart:convert';
import 'package:crypto/crypto.dart';

String generateMontypayHash({
  required String orderNumber,
  required String orderAmount,
  required String orderCurrency,
  required String orderDescription,
  required String merchantPass,
}) {
  final source = (
    orderNumber +
    orderAmount +
    orderCurrency +
    orderDescription +
    merchantPass
  ).toUpperCase();

  // md5.convert(...).toString() 返回小写十六进制字符串
  final md5String = md5.convert(utf8.encode(source)).toString();

  // SHA-1 的输入是 MD5 的十六进制文本,不是 MD5 原始字节
  return sha1.convert(utf8.encode(md5String)).toString();
}

调用方式:

final hashString = generateMontypayHash(
  orderNumber: 'order-1234',
  orderAmount: '0.19',
  orderCurrency: 'usd',
  orderDescription: 'Important gift',
  merchantPass: 'merchantPass',
);

print(hashString);

将 hashString 原样放入请求的 hash 字段,不要再次调用 toUpperCase()。

对应的计算过程

同样的逻辑也可以写成:

final input =
    '$orderNumber$orderAmount$orderCurrency$orderDescription$merchantPass'
        .toUpperCase();

final md5Hex = md5.convert(utf8.encode(input)).toString();
final hashString = sha1.convert(utf8.encode(md5Hex)).toString();

这里最容易忽略的是,第二次哈希接收的是 MD5 的十六进制字符串,例如:

e10adc3949ba59abbe56e057f20f883e

它接收的不是 md5.convert(...).bytes 返回的 16 个原始字节。两种输入计算出的 SHA-1 完全不同。

如何核对 Postman 的 Pre-request Script

在 Postman 中打开对应的请求或 Collection,进入 Scripts → Pre-request。脚本通常类似下面这样:

const source = (
  orderNumber +
  orderAmount +
  orderCurrency +
  orderDescription +
  merchantPass
).toUpperCase();

const md5Hash = CryptoJS.MD5(source).toString();
const hash = CryptoJS.SHA1(md5Hash).toString();

pm.variables.set("hash", hash);

可以临时加入以下日志,确认真正参与计算的值:

console.log("source:", source);
console.log("md5Hash:", md5Hash);
console.log("hash:", hash);

打开 Postman Console 后重新发送请求,再将输出的 source、MD5 和最终 hash 与 Flutter 的结果逐项比较。不要只复制 Postman 最终生成的 hash。订单号、金额或描述发生变化时,hash 也需要重新计算。

如果 Collection 使用环境变量,还需要检查脚本实际取得的值:

console.log(pm.variables.replaceIn("{{order_number}}"));
console.log(pm.variables.replaceIn("{{merchant_pass}}"));

Postman 变量可能来自 Environment、Collection 或请求级变量。界面显示的初始值不一定是脚本当前读取的值。

仍然校验失败时重点检查

即使算法没有问题,下面这些差异仍会导致 Hash is not valid:

  • 字段顺序必须与 Pre-request Script 完全一致。
  • orderAmount 必须使用请求中发送的原始文本。0.19、0.190 和 0.1900 会生成不同的 hash。
  • 不要先将金额转换为 double,再重新格式化。
  • 币种、描述和订单号中的大小写、空格及标点必须与请求正文一致。
  • 不要额外执行 trim(),除非 Postman 脚本也进行了相同处理。
  • merchantPass 必须是商户密钥或 merchant password,不能用 merchant_key、登录密码或示例字符串代替。
  • 测试环境和生产环境通常使用不同的商户凭据。
  • 拼接内容中不能加入 &、=、空格、换行或其他分隔符。
  • 确认请求字段没有被表单编码、JSON 序列化或其他中间逻辑改动。

排查时,最好让 Postman 和 Flutter 同时输出拼接后的 source、中间结果 md5Hex 以及最终的 SHA-1。三个阶段的输入完全一致时,最终 hash 也应该一致。如果当前 Montypay Collection 的 Pre-request Script 与上述公式不同,应以 Collection 实际采用的字段顺序和转换规则为准。

备注:内容仅供参考。