发票识别插件API对接文档

小票追加接口

创建于 2026-08-14 / 最近更新于 2026-08-14 / 104
字体: [默认] [大] [更大]

1. 接入准备


1. 在商家后台 **开票设备管理** 中新增开票设备,获取 `device_code`(设备码),并确保设备状态为 **启用**。
2. 设备需绑定 **默认开票公司**(公司配置 + 门店),否则开票申请会失败(返回"请提醒商家配置默认开票公司")。
3. 客户端保存设备码与下方 RSA 公钥。

2. 接口地址


POST https://{域名}/hy_invoice/api/plugins/receipt/create

- 请求方式:`POST`,仅支持 HTTPS。
- 请求体:支持 `application/json`(推荐)或 `application/x-www-form-urlencoded`。
- 返回格式:`application/json`。

3. 请求头


| 请求头          | 必填 | 说明                                                         |
| --------------- | ---- | ------------------------------------------------------------ |
| `Content-Type`  | 是   | `application/json` 或 `application/x-www-form-urlencoded`    |
| `x-hy-echostr`  | 是   | RSA 公钥加密后的设备参数串(Base64),见第 5.1 节            |
| `x-hy-sign`     | 是   | 请求签名(SHA256 小写十六进制),见第 5.2 节                 |

4. 整体鉴权流程


1. 客户端生成:device_code(设备码)、noncestr(随机串)、timestamp(Unix秒)
2. 明文串:  device_code|noncestr|timestamp
3. RSA公钥加密明文串(PKCS#1 v1.5) → Base64 → 放入请求头 x-hy-echostr
4. 请求参数按key升序排序 → 拼 k1=v1&k2=v2 → 末尾拼接明文串 → SHA256 → x-hy-sign
5. 携带请求头发送 POST 请求

服务端校验顺序:解密 echostr → 校验 device_code / noncestr / timestamp 非空 → 校验时间戳(|差值| ≤ 600 秒)→ nonce 防重放 → 校验签名 → 按 device_code 查询启用的设备并绑定其公司 / 门店。


5. 加密与签名规则

5.1 生成 x-hy-echostr

明文串格式(使用 | 分隔,无空格):


{device_code}|{noncestr}|{timestamp}

字段 说明
device_code 商家后台开票设备管理中获取的设备码
noncestr 随机字符串,建议 32 位随机字符(如 Guid.NewGuid().ToString("N")),每次请求必须唯一
timestamp 当前 Unix 时间戳(秒),与服务器时间差超过 600 秒将被拒绝

用 RSA 公钥(2048 bit) 对明文串加密,填充方式为 PKCS#1 v1.5,结果 Base64 编码后放入 x-hy-echostr 请求头。

注意:RSA-2048 + PKCS1 单次加密上限为 245 字节,请保证明文串(设备码 + 随机串 + 时间戳)不超过该长度。

5.2 生成 x-hy-sign

  1. 取 请求体全部业务参数(不含请求头)。
  2. 按参数名 key 升序排序(字典序,等价于 PHP ksort($data, SORT_STRING))。
  3. 拼接为 key1=value1&key2=value2:
    • 值为字符串 / 数字时直接拼接,不做 URL 编码;
    • 值为数组时,先序列化为 JSON 字符串(等价于 PHP json_encode($value, JSON_UNESCAPED_UNICODE):紧凑无空格、不转义非 ASCII 字符),再参与拼接。
  4. 在拼接结果末尾追加 echostr 明文串(即 device_code|noncestr|timestamp)。
  5. 对整串计算 SHA256,输出 小写十六进制,放入 x-hy-sign 请求头。

签名串构造示例:


请求参数:image=xxxx&image_url=(仅示例)
排序拼接:image=xxxx
追加明文:image=xxxxDEV001|8f2a9c3d...|1755187200
x-hy-sign = sha256(上面整串) 的小写hex

5.3 RSA 公钥

Base64(PKCS#8 / SubjectPublicKeyInfo 格式):


MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAooniVphejHcEHKqPrqos8BYa6xee1ViOuXxTR+UUEIcQrCOlK/cTboY2uJqp38ZupV01oNacuuJ4+iEJ6kbr1DzVaiaKYDQ6fZ6xxCxgOKW1CHbkXVgr2uUYlQ1/62ZLDzZo+97JRJN/EFH0fbBNjU+ga5iBcaH5kHyrx7fXJ+9uOeUjgZEp+Jy2YudZSu4e6JlzZgxlm9dl+gAlyEIFn/9faVu3baZY2cJg5nT98B9IcVVGRzmjMu5JfpdknlEQjC4IeYT032wjtTJMT6k26HKIHeEJha9jEPw4+5rzsDl9tiQzZ1tjt+EqSlzLNTBfl9plZ9yFt/ChaXpHg/F0nwIDAQAB

5.4 安全说明

  • 只有持有对应私钥的设备才能生成合法 echostr,保证请求来源真实(防伪造设备)。
  • 时间戳窗口 600 秒 + nonce 一次性(同设备同 nonce 在 600 秒内重复提交将被拒绝),防重放。
  • 签名覆盖全部业务参数,防请求体篡改。
  • 服务端使用常量时间比较(hash_equals)校验签名。

6. 请求参数(请求体)

参数 类型 必填 说明
image string 四选一 小票图片的 Base64 内容(原始 Base64,不含 data:image/...;base64, 前缀)
image_url string 四选一 小票图片 URL
ocr_json string 四选一 已识别的小票 OCR 结果
txt_json string 四选一 打印的文字内容

说明:

  • image / image_url / ocr_json 必须提供其一,都不传时返回"请提交图片参数"。
  • 签名所用的参数集合 = 实际发送的全部业务参数,务必与发送内容保持一致。

请求体示例(JSON):

{
  "image": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBD...(Base64 内容)"
}

7. 返回结果

7.1 成功

{
  "msg": "ok",
  "errno": 0,
  "status": true,
  "data": {
    "id": 1024,
    "invoice_amount": "88.50",
    "custom_invoice_no": "20260814120001",
    "receipt_type": 1,
    "audit_status": 3
  }
}

data 为生成的开票申请信息(已过滤商家敏感字段)。

7.2 业务失败

{
  "msg": "识别失败",
  "code": 80001,
  "errno": 80001,
  "status": false,
  "data": []
}

7.3 鉴权失败(异常抛出)

{
  "code": 80001,
  "msg": "缺少code参数,请重新提交",
  "data": [],
  "errno": 80001,
  "status": false
}

8. 错误码

错误码 返回信息 触发场景
80001 缺少code参数,请重新提交 x-hy-echostr 缺失或解密失败
80002 缺少noncestr参数,请重新提交 解密串中缺少 noncestr
80003 缺少timestamp参数,请重新提交 解密串中缺少 timestamp(非数字)
80004 缺少sign参数,请重新提交 缺少 x-hy-sign
80005 缺少device_code参数,请重新提交 解密串中缺少 device_code
- 本次请求超时,请重试 时间戳与服务器时间差超过 600 秒
- 请勿重复提交请求 同一设备同一 nonce 在 600 秒内重复提交
- 签名错误,请重试 x-hy-sign 校验不通过
- 设备不存在或未启用 device_code 无匹配设备或设备被禁用
- 请提交图片参数 image/image_url/ocr_json 均未提供
- 识别失败 OCR 未能识别出有效小票内容
- 请提醒商家配置开票公司 设备未绑定默认开票公司

9. C# 对接示例(.NET 6+)

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace ReceiptApiDemo
{
    /// <summary>
    /// 开票小二 · 小票开票插件 API 客户端示例(.NET 6+)
    /// 依赖:System.Text.Json(.NET 内置)
    /// </summary>
    public class ReceiptApiClient
    {
        // ================= 配置区 =================
        private const string ApiUrl = "https://你的域名.com/hy_invoice/api/plugins/receipt/create";
        private const string DeviceCode = "商家后台获取的设备码";
        // RSA 公钥(PKCS#8 SubjectPublicKeyInfo,Base64)
        private const string PublicKeyBase64 =
            "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAooniVphejHcEHKqPrqos8BYa6xee1ViOuXxTR+UUEIcQrCOlK/cTboY2uJqp38ZupV01oNacuuJ4+iEJ6kbr1DzVaiaKYDQ6fZ6xxCxgOKW1CHbkXVgr2uUYlQ1/62ZLDzZo+97JRJN/EFH0fbBNjU+ga5iBcaH5kHyrx7fXJ+9uOeUjgZEp+Jy2YudZSu4e6JlzZgxlm9dl+gAlyEIFn/9faVu3baZY2cJg5nT98B9IcVVGRzmjMu5JfpdknlEQjC4IeYT032wjtTJMT6k26HKIHeEJha9jEPw4+5rzsDl9tiQzZ1tjt+EqSlzLNTBfl9plZ9yFt/ChaXpHg/F0nwIDAQAB";

        // 与 PHP json_encode($v, JSON_UNESCAPED_UNICODE) 保持一致的 JSON 序列化配置
        private static readonly JsonSerializerOptions JsonOptions = new()
        {
            Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
        };

        private readonly HttpClient _http = new();

        /// <summary>
        /// 调用小票开票创建接口
        /// </summary>
        /// <param name="bizParams">业务参数(image / image_url / ocr_json / 商品数组等)</param>
        public async Task<string> CreateReceiptAsync(Dictionary<string, object> bizParams)
        {
            // 1. 生成随机串与时间戳,组装 echostr 明文
            string noncestr = Guid.NewGuid().ToString("N");
            long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            string echostrPlain = $"{DeviceCode}|{noncestr}|{timestamp}";

            // 2. RSA 公钥加密(PKCS#1 v1.5) + Base64 → x-hy-echostr
            string echostr = RsaEncrypt(echostrPlain, PublicKeyBase64);

            // 3. 计算 SHA256 签名
            string sign = BuildSign(bizParams, echostrPlain);

            // 4. 发送请求(JSON 请求体)
            using var request = new HttpRequestMessage(HttpMethod.Post, ApiUrl);
            request.Headers.Add("x-hy-echostr", echostr);
            request.Headers.Add("x-hy-sign", sign);
            string json = JsonSerializer.Serialize(bizParams, JsonOptions);
            request.Content = new StringContent(json, Encoding.UTF8, "application/json");

            using var response = await _http.SendAsync(request);
            return await response.Content.ReadAsStringAsync();
        }

        /// <summary>
        /// RSA 公钥加密(PKCS#1 v1.5),返回 Base64
        /// </summary>
        private static string RsaEncrypt(string plainText, string publicKeyBase64)
        {
            using var rsa = RSA.Create();
            rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicKeyBase64), out _);
            byte[] encrypted = rsa.Encrypt(Encoding.UTF8.GetBytes(plainText), RSAEncryptionPadding.Pkcs1);
            return Convert.ToBase64String(encrypted);
        }

        /// <summary>
        /// 计算签名:参数按 key 字典序升序 → 拼 k1=v1&k2=v2 → 追加 echostr 明文 → SHA256 小写 hex
        /// </summary>
        private static string BuildSign(Dictionary<string, object> bizParams, string echostrPlain)
        {
            var sb = new StringBuilder();
            foreach (var kv in bizParams.OrderBy(p => p.Key, StringComparer.Ordinal))
            {
                if (sb.Length > 0) sb.Append('&');
                sb.Append(kv.Key).Append('=').Append(FormatValue(kv.Value));
            }
            sb.Append(echostrPlain);

            using var sha = SHA256.Create();
            byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString()));
            return Convert.ToHexString(hash).ToLowerInvariant();
        }

        /// <summary>
        /// 值格式化:数组按 PHP json_encode($v, JSON_UNESCAPED_UNICODE) 序列化,其余直接转字符串
        /// </summary>
        private static string FormatValue(object value)
        {
            if (value is string str) return str;
            if (value is IEnumerable<object>)
                return JsonSerializer.Serialize(value, JsonOptions);
            return value?.ToString() ?? "";
        }
    }
}

调用示例:

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        var client = new ReceiptApiClient();

        // 方式一:上传小票图片(Base64)
        string imageBase64 = Convert.ToBase64String(await File.ReadAllBytesAsync(@"C:\receipt\小票.jpg"));
        var bizParams = new Dictionary<string, object>
        {
            ["image"] = imageBase64
        };

        string result = await client.CreateReceiptAsync(bizParams);
        Console.WriteLine(result);
    }
}

对接注意事项

1. **签名参数必须与实际发送的请求体完全一致**,否则校验失败。
2. 排序为按 key 的字典序(`StringComparer.Ordinal`),与 PHP `ksort(SORT_STRING)` 一致。
3. 数组值 JSON 序列化必须**不转义非 ASCII**(示例中已用 `UnsafeRelaxedJsonEscaping`),否则含中文的商品参数会导致签名不一致。
4. 每个请求的 `noncestr` 必须唯一;重试时请重新生成 nonce 和时间戳,勿复用上次请求的 echostr。
5. `ImportSubjectPublicKeyInfo` 需要 **.NET 5+ / .NET Core 3.0+**;若使用 .NET Framework 4.x,请改用 BouncyCastle 或自行解析公钥 DER 构建 `RSAParameters`。
0 人点赞过