本文目录导读:

在PHP项目中对接发票开具接口(如百望、航天信息、微信/支付宝电子发票、诺诺网等),通常遵循标准的HTTP API流程,以下是通用的对接步骤、核心代码示例及注意事项。
对接前的准备工作
- 获取API文档:向发票服务商申请接口文档(通常为RESTful API)。
- 申请密钥:
- Access Key (appKey / appId):用于身份标识。
- Secret Key (appSecret / signKey):用于签名计算(绝对不能泄露到前端)。
- 配置参数:税号、开票员、收款人、复核人等信息。
- 沙箱测试:先用测试环境(模拟开票)确保流程走通。
- 网络环境:确保服务器能访问API域名(部分厂商要求IP白名单)。
核心对接流程
大多数发票接口的流程如下:
- 客户端提交开票数据:前端收集订单号、购买方信息(名称、税号)、商品明细、金额等。
- PHP后端拼接请求参数:按API文档拼装JSON/XML。
- 生成签名:使用协商好的算法(如MD5、SHA256、RSA等)对参数进行签名。
- 发送HTTP请求:使用
cURL或Guzzle等库发送POST/GET请求。 - 处理响应:解析返回的JSON/XML,保存发票号码、发票代码、PDF下载链接等。
- 异步回调:部分接口采用异步(先返回受理成功,后回调通知开票结果),需配置回调接收地址。
通用PHP代码示例(基于cURL + JSON + 签名)
假设使用最常见的 MD5+签名 方式或 签名在请求头 的方式。
构建请求类
<?php
class InvoiceClient
{
private $appId;
private $appSecret;
private $baseUrl;
public function __construct($appId, $appSecret, $baseUrl)
{
$this->appId = $appId;
$this->appSecret = $appSecret;
$this->baseUrl = $baseUrl;
}
/**
* 生成签名 (示例使用MD5,实际以文档为准)
*/
private function generateSign(array $params, $timestamp): string
{
// 1. 按参数名排序
ksort($params);
// 2. 拼接成字符串: key1=value1&key2=value2
$queryString = urldecode(http_build_query($params));
// 3. 在字符串前后加上secret
$signStr = $this->appSecret . $queryString . $this->appSecret;
// 4. MD5加密并转大写
return strtoupper(md5($signStr));
}
/**
* 发送请求(通用方法)
*/
public function sendRequest(string $method, string $apiPath, array $data = []): array
{
$timestamp = time() * 1000; // 毫秒时间戳
$nonce = bin2hex(random_bytes(16));
// 构造请求头或参数 (根据API文档)
$params = [
'appId' => $this->appId,
'timestamp' => $timestamp,
'nonce' => $nonce,
'data' => json_encode($data, JSON_UNESCAPED_UNICODE) // 业务数据
];
// 生成签名
$sign = $this->generateSign($params, $timestamp);
// 将签名加入参数或header (示例加入参数)
$params['sign'] = $sign;
$url = rtrim($this->baseUrl, '/') . '/' . ltrim($apiPath, '/');
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 生产环境应设为true
if (strtoupper($method) === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
// 方式1: JSON body
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params, JSON_UNESCAPED_UNICODE));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json; charset=utf-8',
'Accept: application/json'
]);
} else {
// GET请求拼接参数
curl_setopt($ch, CURLOPT_URL, $url . '?' . http_build_query($params));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return ['code' => -1, 'msg' => '请求失败: ' . $error];
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return ['code' => -2, 'msg' => '响应不是有效JSON', 'raw' => $response];
}
// 注意: 这里假设响应包含code/message/data字段 (具体看API)
return [
'code' => $result['code'] ?? 0,
'msg' => $result['message'] ?? 'success',
'data' => $result['data'] ?? []
];
}
/**
* 开票接口 (示例)
*/
public function createInvoice(array $invoiceData): array
{
// 业务数据组装 (参考具体接口文档)
$data = [
'orderNo' => $invoiceData['order_no'],
'buyerName' => $invoiceData['buyer_name'],
'buyerTaxNo' => $invoiceData['buyer_tax_no'] ?? '',
'buyerAddress' => $invoiceData['buyer_address'] ?? '',
'buyerPhone' => $invoiceData['buyer_phone'] ?? '',
'amount' => $invoiceData['amount'],
'taxRate' => $invoiceData['tax_rate'],
'items' => $invoiceData['items'] // 商品明细数组
];
return $this->sendRequest('POST', '/api/v1/invoice/create', $data);
}
}
调用示例
// 配置
$config = [
'appId' => 'your_app_id',
'appSecret' => 'your_app_secret',
'baseUrl' => 'https://sandbox-api.invoice.com' // 测试环境
];
$client = new InvoiceClient($config['appId'], $config['appSecret'], $config['baseUrl']);
// 准备开票数据
$orderData = [
'order_no' => 'ORDER202410001',
'buyer_name' => '某某科技有限公司',
'buyer_tax_no' => '91440101MA5XXXXX',
'amount' => 1000.00,
'tax_rate' => 0.13,
'items' => [
[
'name' => '技术服务费',
'quantity' => 1,
'price' => 1000.00,
'taxRate' => 0.13
]
]
];
// 发起开票
$result = $client->createInvoice($orderData);
if ($result['code'] == 0) {
// 成功
echo "开票成功,发票号码: " . $result['data']['invoiceNo'] . "\n";
// 保存发票数据到数据库
} else {
// 失败,记录日志
echo "开票失败: " . $result['msg'] . "\n";
}
几种常见厂商的特殊处理
| 厂商 | 签名方式 | 传输方式 | 特殊点 |
|---|---|---|---|
| 百望 | RSA/国密SM2 | XML | 商品编码需预先配置,支持全电发票,接口返回PDF或OFD下载链接 |
| 航天信息 | MD5+签名字符串 | JSON/XML | 需定期同步税号、开票点、商品编码 |
| 支付宝/微信 | RSA2(支付宝)/ MD5+盐 | JSON | 走支付通道,开票数据需关联交易单号,注意官方SDK |
| 诺诺网 | MD5+密钥 | JSON | 需先下单(提交开票申请),再查询结果 |
项目中需要注意的要点
-
数据一致性:
- 使用本地订单号作为幂等键,防止重复开票。
- 如果接口返回“处理中”,需单独设计定时任务轮询开票结果。
-
异常处理:
- 网络超时、服务端返回错误码时,不要立即放弃,应设计重试机制(最多3次,间隔递增)。
- 记录详细的请求和响应日志(不要记录敏感密钥,但可记录加密后的数据)。
-
同步 vs 异步:
- 同步:发请求后直接返回结果(适合即时开票),需控制超时时间(如60秒)。
- 异步:返回受理成功,需提供回调URL(
/api/invoice/callback),处理回调时更新数据库状态。
-
发票下载与展示:
- 开票成功后获取PDF/OFD下载URL。
- 推荐保存文件到本地云存储(OSS)并设置过期时间,避免原链接失效。
-
全电发票:
- 现在很多接口支持“全电发票”(无纸质发票,XML文件直接入账)。
- XML文件需保存到数据库或对象存储,用户可下载的格式通常是PDF(由服务商转换)。
安全与性能
- 密钥安全:密钥绝对不要写在代码里,使用环境变量(
.env文件)或配置中心(如阿里云ACM)。 - HTTPS:必须使用HTTPS,防止中间人攻击。
- 限流:接口通常有调用次数限制(如QPS=100),合理使用队列(Redis/Beanstalkd)缓冲。
- 数据脱敏:日志中的请求数据(如买家税号、手机号)应进行脱敏处理(如
914401*******X)。
常见错误及排查
- 签名错误(1001/401):检查参数顺序、编码(
http_build_query默认会对特殊字符编码,可能需要urldecode)、密钥是否正确。 - 商品编码不存在:需要先在发票平台维护商品编码(税收分类编码)。
- 金额不符:开票金额和明细合计必须与订单完全一致(需精确到分或以上)。
- 税号问题:购买方税号格式验证(企业必须为18位统一社会信用代码,个人发票可填空)。
推荐架构
┌─────────────┐ ┌─────────────┐ ┌────────────────┐
│ 前端应用 │────▶│ PHP后端 │────▶│ 发票API网关 │
│ (提交开票) │ │ (队列+处理) │ │ (百望/航信) │
└─────────────┘ └──────┬──────┘ └────────────────┘
│
▼
┌─────────────┐
│ 数据库 │
│ (订单+发票) │
└─────────────┘
- 队列:防止开票接口慢导致请求阻塞(如订单支付成功后,将开票任务丢进队列)。
- 定时任务:处理回调超时或轮询未完成的发票。
对接发票接口的核心是:理解文档 → 生成签名 → 发送请求 → 处理响应 → 状态同步,遇到问题时,仔细查看官方文档的“签名示例”和“错误码说明”,并在测试环境充分验证,如果使用成熟框架(如Laravel),可以考虑封装成Service Provider,使代码更整洁。