小票追加接口
创建于 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
- 取 请求体全部业务参数(不含请求头)。
- 按参数名 key 升序排序(字典序,等价于 PHP
ksort($data, SORT_STRING))。 - 拼接为
key1=value1&key2=value2:- 值为字符串 / 数字时直接拼接,不做 URL 编码;
- 值为数组时,先序列化为 JSON 字符串(等价于 PHP
json_encode($value, JSON_UNESCAPED_UNICODE):紧凑无空格、不转义非 ASCII 字符),再参与拼接。
- 在拼接结果末尾追加 echostr 明文串(即
device_code|noncestr|timestamp)。 - 对整串计算 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 人点赞过