Skip to content

/meds/{medId}/file/upload 接口介绍 ​

接口背景 ​

/meds/{medId}/file/upload 提供给商户的 MED 证据文件上传接口。商户可为特定的 MED 上传分析文件(证据文件),如交易凭证、沟通记录等。每个文件必须通过 evidenceType 归类到该 MED 的证据材料清单,上传后将被安全存储并与该 MED 关联,用于后续分析。

仅当 MED 处于 WAITING 或 EVIDENCE_REQUIRED 状态且未过举证截止时间(dueTime)时才能上传文件;在 MED 保持上述状态期间,可为单个 MED 上传多个文件。

接口请求地址 ​

项目内容
请求方式POST
请求路径/meds/{medId}/file/upload
Content-Typemultipart/form-data
接口用途为 MED 上传分析证据文件

认证 ​

使用 ES256 请求签名,必须携带 X-Merchant-Id、X-Timestamp、X-Nonce、Digest 和 Authorization。X-Merchant-Id 填写商户号,keyId 为密钥版本号,默认 v1。签名值为 DER 编码的 ECDSA 签名经标准 Base64 编码后的结果,具体规则见 请求签名。

示例中的时间戳、Nonce、摘要和签名占位符需按每次实际请求生成;签名串包含实际路径及原始 query,分页或筛选条件变化后必须重新签名。

接口请求字段 ​

字段名位置类型是否必填说明
medIdPathstring是MED 违规报告唯一标识符(平台案件号,medc 前缀 + 数字),最大长度 64 个字符。
evidenceTypeBodystring是证据材料类型,必须是该 MED 当前材料清单中的有效类型。
fileBodyfile是分析证据文件;支持 PDF、DOC、DOCX、TXT、JPG、JPEG、PNG 格式,最大 10 MB。出于安全考虑,文件扩展名会与内容类型(content type)及文件内容进行校验。

请求示例 ​

Digest 针对实际发送的完整请求体字节计算,服务端收到后会重算比对。因此各环节必须使用同一份字节:先拼好完整请求体文件,再对它的全部字节计算摘要并签名,最后原样发送;任何环节改变字节都会导致验签失败。

第一步:拼出 .multipart 请求体文件。 boundary 固定为 MED_UPLOAD_BOUNDARY,不能随机生成(Content-Type 头中的 boundary 必须与文件内一致)。PDF 示例包含 evidenceType=MERCHANT_ORDER_RECORD 和 file(evidence-document.pdf,application/pdf);图片示例包含 evidenceType=LOGIN_IP_DEVICE_RECORD 和 file(screenshot-proof.png,image/png)。文件必须包含全部表单字段、分段头、文件原始字节及 CRLF 分隔符,结构如下(\r\n 表示回车加换行两个字节;文件内容原样嵌入,不是 Base64):

text
--MED_UPLOAD_BOUNDARY\r\n
Content-Disposition: form-data; name="evidenceType"\r\n
\r\n
MERCHANT_ORDER_RECORD\r\n
--MED_UPLOAD_BOUNDARY\r\n
Content-Disposition: form-data; name="file"; filename="evidence-document.pdf"\r\n
Content-Type: application/pdf\r\n
\r\n
<evidence-document.pdf 的原始字节>\r\n
--MED_UPLOAD_BOUNDARY--\r\n

生成该文件并计算 Digest 的各语言示例(图片示例替换 evidenceType、文件名和 Content-Type 即可):

go
// Go 1.16+,仅标准库(更低版本可用 ioutil 包)。
package main

import (
	"bytes"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"os"
)

const boundary = "MED_UPLOAD_BOUNDARY"

func main() {
	fileBytes, err := os.ReadFile("evidence-document.pdf")
	if err != nil {
		panic(err)
	}

	var body bytes.Buffer
	body.WriteString("--" + boundary + "\r\n")
	body.WriteString("Content-Disposition: form-data; name=\"evidenceType\"\r\n\r\n")
	body.WriteString("MERCHANT_ORDER_RECORD\r\n")
	body.WriteString("--" + boundary + "\r\n")
	body.WriteString("Content-Disposition: form-data; name=\"file\"; filename=\"evidence-document.pdf\"\r\n")
	body.WriteString("Content-Type: application/pdf\r\n\r\n")
	body.Write(fileBytes)
	body.WriteString("\r\n--" + boundary + "--\r\n")

	if err := os.WriteFile("evidence-document.multipart", body.Bytes(), 0o644); err != nil {
		panic(err)
	}

	// 对完整请求体的全部字节计算 Digest,而不是只对 PDF 文件本身
	sum := sha256.Sum256(body.Bytes())
	fmt.Println("SHA-256=" + base64.StdEncoding.EncodeToString(sum[:]))
}
java
// Java 8+,仅标准库。
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.Base64;

public class BuildMultipartBody {
    public static void main(String[] args) throws Exception {
        String boundary = "MED_UPLOAD_BOUNDARY";
        byte[] fileBytes = Files.readAllBytes(Paths.get("evidence-document.pdf"));

        StringBuilder head = new StringBuilder();
        head.append("--").append(boundary).append("\r\n");
        head.append("Content-Disposition: form-data; name=\"evidenceType\"\r\n\r\n");
        head.append("MERCHANT_ORDER_RECORD\r\n");
        head.append("--").append(boundary).append("\r\n");
        head.append("Content-Disposition: form-data; name=\"file\"; filename=\"evidence-document.pdf\"\r\n");
        head.append("Content-Type: application/pdf\r\n\r\n");

        byte[] headBytes = head.toString().getBytes(StandardCharsets.UTF_8);
        byte[] tailBytes = ("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8);
        byte[] body = new byte[headBytes.length + fileBytes.length + tailBytes.length];
        System.arraycopy(headBytes, 0, body, 0, headBytes.length);
        System.arraycopy(fileBytes, 0, body, headBytes.length, fileBytes.length);
        System.arraycopy(tailBytes, 0, body, headBytes.length + fileBytes.length, tailBytes.length);

        Files.write(Paths.get("evidence-document.multipart"), body);

        // 对完整请求体的全部字节计算 Digest,而不是只对 PDF 文件本身
        String digest = "SHA-256=" + Base64.getEncoder().encodeToString(
                MessageDigest.getInstance("SHA-256").digest(body));
        System.out.println(digest);
    }
}
js
// Node.js 14+,仅内置模块。
import crypto from "node:crypto";
import fs from "node:fs";

const boundary = "MED_UPLOAD_BOUNDARY";
const fileBytes = fs.readFileSync("evidence-document.pdf");

const body = Buffer.concat([
  Buffer.from(
    `--${boundary}\r\n` +
      'Content-Disposition: form-data; name="evidenceType"\r\n\r\n' +
      "MERCHANT_ORDER_RECORD\r\n" +
      `--${boundary}\r\n` +
      'Content-Disposition: form-data; name="file"; filename="evidence-document.pdf"\r\n' +
      "Content-Type: application/pdf\r\n\r\n"
  ),
  fileBytes,
  Buffer.from(`\r\n--${boundary}--\r\n`),
]);

fs.writeFileSync("evidence-document.multipart", body);

// 对完整请求体的全部字节计算 Digest,而不是只对 PDF 文件本身
const digest = "SHA-256=" + crypto.createHash("sha256").update(body).digest("base64");
console.log(digest);
php
<?php
// PHP 7.1+,仅标准扩展。

$boundary = 'MED_UPLOAD_BOUNDARY';
$fileBytes = file_get_contents('evidence-document.pdf');

$body =
    "--{$boundary}\r\n" .
    'Content-Disposition: form-data; name="evidenceType"' . "\r\n\r\n" .
    "MERCHANT_ORDER_RECORD\r\n" .
    "--{$boundary}\r\n" .
    'Content-Disposition: form-data; name="file"; filename="evidence-document.pdf"' . "\r\n" .
    "Content-Type: application/pdf\r\n\r\n" .
    $fileBytes .
    "\r\n--{$boundary}--\r\n";

file_put_contents('evidence-document.multipart', $body);

// 对完整请求体的全部字节计算 Digest,而不是只对 PDF 文件本身
echo 'SHA-256=' . base64_encode(hash('sha256', $body, true)), PHP_EOL;
python
# Python 3.7+,仅标准库。
import base64
import hashlib
from pathlib import Path

boundary = "MED_UPLOAD_BOUNDARY"
file_bytes = Path("evidence-document.pdf").read_bytes()

body = (
    f"--{boundary}\r\n"
    'Content-Disposition: form-data; name="evidenceType"\r\n\r\n'
    "MERCHANT_ORDER_RECORD\r\n"
    f"--{boundary}\r\n"
    'Content-Disposition: form-data; name="file"; filename="evidence-document.pdf"\r\n'
    "Content-Type: application/pdf\r\n\r\n"
).encode() + file_bytes + f"\r\n--{boundary}--\r\n".encode()

Path("evidence-document.multipart").write_bytes(body)

# 对完整请求体的全部字节计算 Digest,而不是只对 PDF 文件本身
digest = "SHA-256=" + base64.b64encode(hashlib.sha256(body).digest()).decode()

第二步:生成签名。 使用上一步得到的 Digest,按请求签名构造签名串并加签((request-target) 行使用实际请求路径 /meds/{medId}/file/upload),得到 Authorization 头。

第三步:原样发送。 用 --data-binary 将 .multipart 文件一字节不改地发出,见下方示例(Python 中等价于把 body 直接作为请求数据传递,不要使用 files= 等会重新构造请求体的参数)。

两个常见错误都会导致验签失败:

  • 只对上传文件本身计算摘要——服务端是对收到的完整 multipart 请求体重算的,两个摘要必然不一致。
  • 签名后用 --form(或 files= 等自动构造 multipart 的参数)重新生成 boundary 或请求体——客户端会重新随机生成 boundary 并自行排列字段,发送的字节与签名时不同。

PDF 文件示例:

bash
curl --request POST \
  --url '<base_url>/meds/medc2874510938274639021/file/upload' \
  --header 'X-Merchant-Id: <MERCHANT_ID>' \
  --header 'X-Timestamp: <UNIX_TIMESTAMP_SECONDS>' \
  --header 'X-Nonce: <UNIQUE_NONCE>' \
  --header 'Digest: SHA-256=<MULTIPART_BODY_SHA256_BASE64>' \
  --header 'Authorization: Signature keyId="v1",alg="ES256",headers="(request-target) x-timestamp x-nonce digest",signature="<ES256_SIGNATURE_BASE64>"' \
  --header 'Content-Type: multipart/form-data; boundary=MED_UPLOAD_BOUNDARY' \
  --data-binary '@evidence-document.multipart'

图片文件示例:

bash
curl --request POST \
  --url '<base_url>/meds/medc2874510938274639021/file/upload' \
  --header 'X-Merchant-Id: <MERCHANT_ID>' \
  --header 'X-Timestamp: <UNIX_TIMESTAMP_SECONDS>' \
  --header 'X-Nonce: <UNIQUE_NONCE>' \
  --header 'Digest: SHA-256=<MULTIPART_BODY_SHA256_BASE64>' \
  --header 'Authorization: Signature keyId="v1",alg="ES256",headers="(request-target) x-timestamp x-nonce digest",signature="<ES256_SIGNATURE_BASE64>"' \
  --header 'Content-Type: multipart/form-data; boundary=MED_UPLOAD_BOUNDARY' \
  --data-binary '@screenshot-proof.multipart'

接口响应字段 ​

接口使用统一的 status、msg、data 响应结构。已上传文件的信息位于 data 中。

字段名类型是否必返说明
statusint是响应码
msgstring是与 status 对应
dataobject成功必返已上传文件的信息;请求在验签或协议解析阶段失败时可能不返回。
data.idstring是上传文件的唯一标识符。
data.medIdstring是该文件关联的 MED ID。
data.subMerchantNostring是该 MED 所属二级商户号;直连商户固定为空字符串,可忽略。
data.evidenceTypestring是该文件归属的证据材料类型。
data.createdAtstring是文件上传时间,ISO 8601 UTC 时间字符串(yyyy-MM-ddTHH:mm:ssZ)。

响应示例 ​

json
{
  "status": 200,
  "msg": "sucesso",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "medId": "medc2874510938274639021",
    "subMerchantNo": "",
    "evidenceType": "MERCHANT_ORDER_RECORD",
    "createdAt": "2026-06-02T09:10:00Z"
  }
}

响应错误码 ​

业务规则 ​

文件上传要求 ​

  • 最大文件大小为 10 MB。
  • 支持格式:PDF、DOC、DOCX、TXT、JPG、JPEG、PNG。
  • 出于安全考虑,文件扩展名会与内容类型(content type)进行校验。
  • 每次上传只能指定一个 evidenceType,同一材料类型可以上传多个文件。
  • evidenceType 必须来自查询证据材料要求接口返回的 data.requirements[].evidenceType。
  • 操作方必须有权访问与该 MED 关联的账户。

MED 状态校验 ​

  • 仅当 MED 处于 WAITING 或 EVIDENCE_REQUIRED 状态时才能上传文件。
  • MED 处于其他状态时,不能上传文件;平台打回(EVIDENCE_REQUIRED)后可补充上传。
  • 已提交分析且待平台审核(UNDER_REVIEW 待审核)期间,不能再上传文件。
  • 已过举证截止时间(dueTime)后,不能上传文件。
  • 在 MED 保持 WAITING 或 EVIDENCE_REQUIRED 状态期间,可为单个 MED 上传多个文件。

返回 MED API 总览