frontend-app-printing.md 6.8 KB

商家 App:电子发票打印接入(商米 V3)

后端在 ezPay 开票成功落库后,后台自动准备 PDF 和打印图片。App 不需要再排版或把 PDF 转图片;使用商米原生 SDK 的 printBitmap 打印 PNG。平台继续查看 PDF。

这是电子发票证明联,与 /getOrderPrintInfo 餐点小票不同。App 代码由前端团队接入,本次后端不修改 App。

接口前缀:/system/orderShOprate,请求头传商家 token;orderId 使用 pos_order.id。普通商家、夜市、摊位、授权子账号沿用订单权限。

1. 查看明细及打印次数

GET /getInvoicePrintDetail?orderId=3173

返回 invoiceNumber、printCount、fileName、contentType、pdfBase64。此 PDF 仅交易明细,无兑奖二维码,重复查看不占首次资格。

  • printCount=0 显示“打印发票”;大于0显示“申请补印”。仅统计本系统。
  • 自动预生成文件、平台查看文件均不增加次数。

2. 用户点击打印时申请文件

POST /printInvoice

{
  "orderId": 3173,
  "requestId": "78490c8a-6755-4a49-bd36-2b661f46f653",
  "reprint": false
}

requestId 是本次打印申请 UUID,调用前保存。超时、断网、响应丢失时使用同一值和同一参数重试,不能每次产生新编号。按钮提交中禁用。

成功响应示意(图片数量随页长变化,以下仅列一段):

{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "printJobId": 123,
    "invoiceNumber": "AB12345678",
    "printSequence": 1,
    "reprint": false,
    "fileName": "AB12345678-1.pdf",
    "contentType": "application/pdf",
    "pdfBase64": "JVBERi0...",
    "reversePdfBase64": "JVBERi0...",
    "printerProfile": "SUNMI_58_384",
    "dotsPerMm": 8,
    "images": [
      {
        "pageNumber": 1,
        "partNumber": 1,
        "width": 384,
        "height": 720,
        "lastPart": true,
        "separateAfter": true,
        "pngBase64": "iVBORw0..."
      }
    ]
  }
}

新增字段:

字段 用法
printerProfile 当前为 SUNMI_58_384,面向58mm纸卷、384点有效打印区
dotsPerMm 8点/mm;按原像素输出,禁止再次缩放
images[] PNG分段,必须按数组原顺序连续输出
pageNumber / partNumber 从1开始;同页可能有多段
width / height 图片实际像素尺寸,宽固定384,单段高最多2048
lastPart 是否当前PDF页的最后一段
separateAfter 此段后需要将证明联与明细分离;不是每段都切纸
pngBase64 无Data URL前缀的PNG Base64

保留的 pdfBase64 用于预览/兼容;打印以 images 为准。PDF纸面仍为57mm宽,PNG只包含居中的48mm有效打印区,已经去除白边,没有把整张PDF缩小。

3. 商米 V3 打印步骤

  1. App 运行在 V3 上时,连接商米内置打印服务。uni-app 通过 Android 原生插件或 UTS 对接商米 SDK。
  2. 校验实际设备有效宽度为384点,再使用本打印配置;不同打印宽度不能强行缩放套用。
  3. 将 pngBase64 解码为 PNG,再解码为 Bitmap;关闭资源密度自动缩放,确认 Bitmap 宽度为384。
  4. 按 images 顺序调用 printBitmap,处理异步回调,不能并发发送导致乱序。同页分段之间不额外换行、走纸或裁切。
  5. 遇到 separateAfter=true,按设备能力分离/提示撕纸后继续。个人票证明联与明细分开;企业票是同一长页,中途不能分离。V3是否具备切刀以实机为准,不能无条件调用切刀接口。
  6. 正常结束后处理设备出纸结果。onRunResult只表示指令执行情况;事务打印的onPrintResult用于确认打印结果。缺纸、开盖等由App提示,不能因回调不明确就自动重复整张原件。

reversePdfBase64是背面参考,不属于images,不要接在正面后打印。使用符合要求的预印背面纸卷;背面及纸张仍需实际验收。

4. 文件未准备好与重试

  • 后台每10秒扫描,包含已开纸本存量;每轮最多10张。积压或ezPay故障时可能更久,不能把10秒当成完成承诺。
  • 文件未就绪返回业务错误,msg为“发票打印文件正在准备,请稍后重试”(支持国际化)。本次不新增打印记录、不占次数;不要自动换成补印。
  • 后台失败按1/5/15/60分钟退避,此后每小时重试;不影响已成功的开票状态。
  • 实际申请打印仍会查询ezPay确认发票有效,因此网络故障时仍可能等待或失败;App按返回msg提示。
  • 旧版本已保存的打印申请可能返回images=[],保留原PDF供查看;不要把旧PDF任意缩放或裁边输出。需要再次出纸时进入补印流程,使用新的requestId。

5. 补印

{
  "orderId": 3173,
  "requestId": "a25ece72-8030-475d-97c9-3c49ce308d26",
  "reprint": true,
  "reprintReason": "原证明联字迹不清,已核对内容一致",
  "originalAvailable": true,
  "contentsVerified": true
}

原因最多200字,两个声明必须为true。成功后printSequence>=2,图片和PDF已经带“補印”,App不用拼字。仅适用于已确认的补印情形,需要保留原联。

首次/补印资格按成功受理打印申请记录,不是按预生成文件记录,也不是按设备出纸回执记录。原requestId恢复响应不会新增次数;不能用缓存原件反复打印。作废后拒绝打印,重开后的新发票重新计数并使用新requestId。

6. 平台查看与上线

  • 平台现有“查看PDF”接口不变:已有打印记录看最新记录;没有打印记录时看已预生成原件。因此商家尚未打印,平台也可以看到PDF。
  • 先手动执行updatesql/sql.md中Phase12建表及“2026-11-02 开票后预生成PDF及商米V3图片”迁移,再发布后端;不需要配置若依sys_job。
  • 联调:新开纸本自动生成但次数为0、首次、重复请求、补印、缺纸/失败、作废/重开、越权、B2C/B2B长明细、重启后失败任务恢复。
  • 正式使用前需在真实V3上检查纸张尺寸、文字、Code39和两个二维码;后端本地图片回读不等于真实热敏打印验收或官方认可。

商米SDK参考:内置打印机开发者文档。

2026-11-02 字体修复说明

后端字体改为完整 Noto Sans CJK TC(固定 Regular 字重),随 JAR 打包。支持简繁中文、英文、数字与常用标点混排,App 不安装字体、不转换文字,打印仍使用后端图片。历史“测试门店”缺字导致的失败文件,更新后端后会在原定重试时间重新生成;不需要重新开票,本次无新增 SQL。此前 Phase12/13 迁移仍须已执行。特殊 Emoji 等不保证覆盖,遇到不支持的字形保留服务端错误原因,不静默丢字。