21 KiB
Oscar 照片打印版 / 电子版购买对接
更新日期:2026-09-14。本文说明本次接口契约和人工联调标准;真实微信支付、设备打印、FaceBox 回调及相册下载仍需在部署后联调,示例不是线上验收记录。
1. 适用范围和业务规则
本次在大屏人脸照片流程 type=2 增加 purchase_mode=electronic。打印版 print 保持原有打印、套餐计价和相册权益;type=1 小程序上传的接口响应和业务流程保持旧版。type=3 小程序人脸流程本次不新增电子版购买。
| 参数 / 配置 | 约定 |
|---|---|
purchase_mode |
仅接受 print、electronic,区分大小写;省略时默认 print |
| 显式空值或非法模式 | null、空字符串、其他字符串均报错,不能自动改成 print |
electronic |
仅 type=2,只购买照片,不打印,不支持混入视频 |
| 电子版基础价 | price_photo_digital,未配置可为 null;后台新提交价格必须为 0.01–99999.99 元 |
| 电子版阶梯 | 独立开关和独立档位,只根据去重照片张数选取已达到的最大门槛,命中单价用于本单全部照片 |
| 免费与首张配置 | 电子版不使用 free_digital_enabled、free_num、one_order_amount,这些字段继续服务原打印逻辑 |
| 免费电子版 | 本次不支持;基础价缺失或旧数据非正数视为电子版不可购买,新档位也不能配置零价 |
| 用户身份 | 大屏仅代表设备;用户登录小程序扫码后创建实体订单,并以扫码用户为订单归属 |
| 金额 | 对外为元、两位小数字符串;数据库和预选缓存金额使用整数分;不可用价格用 JSON null |
普通用户的付款金额采用获取支付链接时锁定的金额。现有内部员工/景区员工 1 分钱优惠保留,这是设备报价与实际付款金额一致性的明确例外;员工身份由服务端判断,不能由 App 上传金额或身份覆盖。
2. 鉴权与公共响应
设备接口前缀:/api/oscar/order。每次请求带设备鉴权头:
Content-Type: application/json
sn: <设备SN>
timestamp: <当前秒级时间戳>
token: <md5(sn + 设备api_token + timestamp)>
设备必须为已注册且已绑定景区的大屏设备,时间戳允许偏差为 1 小时。api_token 为设备已配置密钥,不作为请求体字段发送。照片素材、订单号和购买模式不能替代设备鉴权。
HTTP 成功响应的最外层 code 为 100000:
{
"code": 100000,
"msg": "success",
"data": {},
"time": "2026-09-14 12:00:00"
}
业务错误可能仍使用 HTTP 200,必须判断最外层 code 并显示 msg。不要把业务消息 type=5 或 WebSocket 的外层 code=5 当作 HTTP 成功码。下文未特别说明的响应示例仅展示 HTTP data 部分。
3. App 调用顺序
- 选照片后调用
verify-result,同时展示打印版报价和可用的电子版报价;没有选择照片时仅展示单价,总额为零。 - 用户选择购买模式,调用
get-pay-url。使用该响应的金额展示支付二维码,保存其order_number与purchase_mode。 - 用户登录小程序扫码支付。App 监听 WebSocket 购买完成消息,并可调用
pay-success-message补查。 - 仅确认该订单
order_status=30或收到匹配订单的购买完成推送后进入完成流程。electronic展示相册二维码;print继续原打印流程及相册领取流程。 - 电子版不发起打印、不扣纸、不调用
print-notify或print-complete。订单购买完成已取得电子版相册权益,文件转存允许稍后完成。
App 应按订单号防止 WebSocket 与 HTTP 重复结果触发重复打印、重复跳页。切换购买模式或照片集合后应重新获取支付链接,不能继续复用前一张二维码。
电子版独立完成页仅展示下载二维码和 90 秒返回首页倒计时。倒计时只控制 App 返回首页,不代表已购照片下载权限到期。
4. 报价:POST /api/oscar/order/verify-result
请求:
{
"type": 2,
"image_id": [101, 102, 103, 104, 105],
"video_id": []
}
照片和视频 ID 均为整数数组;照片按去重数量参与计价。电子版只计照片,App 电子版路径保持 video_id=[]。此接口同时报价,不需要 purchase_mode。
示例:打印套餐单价 12 元、电子版基础价 5 元、电子版满 2 张单价 4 元、满 5 张单价 3.50 元,打印旧赠送和首张优惠关闭:
{
"price_image": "12.00",
"price_video": "0.00",
"amount": "60.00",
"price_electronic": "3.50",
"amount_electronic": "17.50"
}
| 字段 | 含义 |
|---|---|
price_image |
原打印流程照片单价;完整打印总额仍以 amount 为准,可能涉及原赠送/首张规则 |
price_video |
原视频单价 |
amount |
原打印流程总额 |
price_electronic |
当前照片张数对应的电子版单价 |
amount_electronic |
电子版照片单价 × 去重照片张数 |
电子版未配置时,新增的两个字段都为 null,打印报价正常返回。App 显示电子版不可购买;不能把 null 转成零元、不能自动改成打印版购买。
{
"price_image": "12.00",
"price_video": "0.00",
"amount": "60.00",
"price_electronic": null,
"amount_electronic": null
}
空选 image_id=[]、video_id=[] 时,总额为 "0.00",单价仍返回基础单价;如果电子版未配置,电子版两个字段继续为 null。空选可报价,但不能生成购买订单。
type=1 继续只返回 price_image、price_video、amount 三个旧字段,不新增电子版字段。
5. 获取支付链接:POST /api/oscar/order/get-pay-url
请求:
{
"type": 2,
"image_id": [101, 102, 103, 104, 105, 105],
"video_id": [],
"purchase_mode": "electronic"
}
响应:
{
"url": "https://<业务域名>/scan/pay?order_number=<订单号>",
"order_number": "<订单号>",
"purchase_mode": "electronic",
"amount": "17.50",
"order_status": 10
}
该接口生成预选缓存和订单号,尚未创建实体订单,也不表示付款成功。缓存有效期 24 小时,冻结设备 SN、type、去重照片/视频集合、购买模式、项目 ID、景区 ID、报价和整数分总额。
扫码后小程序沿用 POST /api/mini/capture/scan-pay,提交 order_number,由登录身份建单支付。扫码前后台改价不会重算本次已锁定的普通用户金额;项目已下线或设备绑定景区与锁定信息不一致时,拒绝建单,提示重新选择照片。缓存过期且尚未建单时同样重新选择。
同一订单重复扫码支付复用已有订单和金额,不允许改成其他购买者。完成后的重复扫码不能重复收款。
约束:
image_id必须至少包含一张有效照片;重复 ID 去重。- 电子版价格不可用、总额非正数或超出可存储范围时,不返回支付链接。
- 电子版带视频报错;
type=1/3请求电子版报错。 - 省略
purchase_mode等同print;显式非法值报错。 type=1响应仍只有旧字段url、order_number,保持原扫码付款流程。
6. HTTP 补查:POST /api/oscar/order/pay-success-message
请求:
{"order_number": "<订单号>"}
尚未扫码但有效的本设备 type=2 预选缓存也可以查询,返回待付款 10:
{
"order_status": 10,
"order_status_name": "待付款",
"sn": "<设备SN>",
"type": 5,
"data": {
"order_number": "<订单号>",
"capture_type": 2,
"image_id": [101, 102, 103, 104, 105],
"purchase_mode": "electronic"
}
}
购买完成时结构相同,order_status=30、order_status_name="已完成"。type=5 是业务消息类别,待付款响应也带该值;HTTP 补查必须检查 order_status,不能只看 type=5 就打印或开放下载。
| 订单状态 | App 处理 |
|---|---|
10 待付款 |
保持支付页面;可能只有缓存,也可能已建单等待支付 |
30 已完成 |
按响应的 purchase_mode 分流到打印或相册领取 |
40 已取消 / 50 已退款 |
退出支付或完成等待,显示对应状态 |
| 其他状态 | 不当作本次照片购买成功;按服务端提示处理 |
本次直接从待付款到完成,不新增状态。60 不是本次订单状态,不能用作免费领取、电子版完成或等待转存状态。未找到实体订单且缓存也失效时返回错误;其他设备的订单不可查询,也不能用旧缓存遮盖实体订单状态。
7. WebSocket 购买完成通知
沿用既有 Oscar WebSocket 连接和鉴权,外层消息码为 code=5,其中业务消息为 type=5。电子版完成消息示例:
{
"code": 5,
"data": {
"sn": "<设备SN>",
"type": 5,
"data": {
"order_number": "<订单号>",
"capture_type": 2,
"image_id": [101, 102, 103, 104, 105],
"purchase_mode": "electronic"
}
}
}
外层沿用现有网关协议,示例只列相关字段。服务端在购买完成事务提交后发送消息;HTTP 和 WebSocket 使用同一个 type=5 业务数据构造方法,所以模式和照片集合一致。HTTP 的 order_status 属于补查外壳,不要求 WebSocket 业务载荷增加该字段。
capture_type=1 仍使用旧的 uuid、file_map 载荷;capture_type=2 使用 image_id 与新增 purchase_mode。收到其他订单、其他抓拍流程消息时,不应驱动当前页面。推送丢失或连接恢复后,通过 HTTP 补查恢复状态。
8. 相册二维码与文件准备
接口:POST /api/oscar/order/save-album-url。
{"order_number": "<订单号>"}
响应:
{"url": "https://<业务域名>/scan/share?order_number=<订单号>"}
仅本设备已完成的照片订单可获取链接。电子版在购买完成时立即取得相册展示权益并发起 FaceBox 转存,无需等待 print-complete;打印版保持已有相册权益和扫码展示规则,不因本次新增模式减少原权益。
付款完成与文件转存完成是两个时刻。FaceBox 尚未回传原图时,相册可处于“上传中/准备中”;App 不应再次要求付费,不应把暂时没有素材当作未购买。
允许重复调用本接口:若此前转存请求没有成功受理,可重试;已受理的任务不会重复发起。成功取得二维码仅说明订单可领取,不保证所有原图已经准备完毕。已受理后长期未回调的任务仍需检查 FaceBox 状态,不能把反复取二维码等同于强制重建已受理任务。
FaceBox 成功回调以订单号和所购照片集合入库,重复回调不应产生重复素材。购买用户的订单与素材归属保持一致。
打印版继续调用原接口:
POST /api/oscar/order/print-notify:单张打印状态,字段为order_number、capture_type、image_id、print_status,可附remaining_paper_num。POST /api/oscar/order/print-complete:打印流程结束上报,字段为order_number、capture_type。
服务端校验设备归属和购买模式。电子版调用以上两个接口会报错,不能写打印记录或更新剩余纸张数。打印完成上报不承担购买授权或相册转存的触发职责。
9. 后台电子版配置
沿用 POST /backend/project/add、POST /backend/project/edit、POST /backend/project/detail。项目类型仍为 22,下列字段放在原有 extra 内;保存项目时同时提供既有接口要求的项目和打印配置。
{
"extra": {
"price_photo_digital": "5.00",
"multi_photo_digital_discount_enabled": 1,
"multi_photo_digital_prices": [
{"min_photo_num": 2, "price_photo_digital": "4.00"},
{"min_photo_num": 5, "price_photo_digital": "3.50"}
]
}
}
| 字段 | 保存约束 |
|---|---|
price_photo_digital |
可为 null,非空值为 0.01–99999.99 元,最多两位小数 |
multi_photo_digital_discount_enabled |
0 关闭、1 开启;默认关闭 |
multi_photo_digital_prices |
独立电子版档位数组,不混用打印或套餐档位 |
min_photo_num |
整数,至少 2,同一策略内不得重复 |
档位 price_photo_digital |
0.01–99999.99 元,最多两位小数 |
开启电子版优惠必须配置正数基础价和至少一个档位。未达到门槛使用基础价;达到多个门槛时使用最大门槛的单价。保存前按门槛排序,金额转为整数分存储。
编辑时未提交的基础价、旧赠送字段、首张金额或策略配置保留原值;关闭策略并提交空数组不会清除已存档位。已存电子优惠仍开启时,不能只把基础价清空;应同时关闭电子优惠。旧数据里的基础价 0 读取时按电子版未配置处理,新保存时显式提交 0 会被拒绝。
电子档位复用 project_type_face_print_multi_price:price_type=1 打印,2 套餐,3 电子版。项目保存后刷新该景区价格缓存;旧缓存没有电子档位数组时重新加载。平台获取配置时也会核对共享数据库中的基础配置版本,因此景区端改价、店铺新版本审核上线后,即使三端缓存前缀不同也会重新加载当前价格和项目。
后台价格试算
接口:POST /backend/project/face-print-price-preview,使用当前未保存配置。请求示例:
{
"extra": {
"free_digital_enabled": 0,
"free_num": null,
"one_order_amount": null,
"price_photo_print": "8.00",
"price_photo_combo": "12.00",
"price_photo_digital": "5.00",
"multi_photo_print_discount_enabled": 0,
"multi_photo_print_prices": [],
"multi_photo_combo_discount_enabled": 0,
"multi_photo_combo_prices": [],
"multi_photo_digital_discount_enabled": 1,
"multi_photo_digital_prices": [
{"min_photo_num": 2, "price_photo_digital": "4.00"},
{"min_photo_num": 5, "price_photo_digital": "3.50"}
]
},
"page": 1,
"page_size": 2
}
响应 data:
{
"list": [
{
"photo_num": 1,
"price_photo_print": "8.00",
"price_photo_combo": "12.00",
"amount_upload_print": "8.00",
"amount_face_print": "12.00",
"price_photo_digital": "5.00",
"amount_electronic": "5.00"
},
{
"photo_num": 2,
"price_photo_print": "8.00",
"price_photo_combo": "12.00",
"amount_upload_print": "16.00",
"amount_face_print": "24.00",
"price_photo_digital": "4.00",
"amount_electronic": "8.00"
}
],
"total": 6,
"page": 1,
"page_size": 2
}
后台预览的电子单价字段叫 price_photo_digital,App 报价接口叫 price_electronic,二者不要混用。两处电子总额都叫 amount_electronic。未配置电子价时,预览电子单价和总额均为 null。
10. 旧版兼容与异常处理
| 场景 | 约定 |
|---|---|
| 旧 App 不传模式 | 按 print 处理,保持原打印权益 |
上线前生成、没有 purchase_mode 的预选缓存 |
按旧 print 流程建单;未锁定价格的旧缓存仍采用旧计价路径 |
| 已存在的旧订单 | 数据库新增字段默认 print |
明确标记 electronic 的新缓存损坏或过期 |
返回错误并重新选择,不降级为打印版或重新套用旧打印计价 |
| 同一笔支付的重复通知 | 幂等处理、不重复完成,素材回调不重复入库 |
| 付款完成但 WebSocket 通知失败 | 通过 HTTP 补查恢复;通知失败不能让已支付订单重新付款 |
| 转存请求失败 | 已购买权益保留,后续获取相册链接可重试尚未受理的上传 |
| 非本设备订单或抓拍类型不匹配 | 拒绝查询、取相册或打印上报 |
对于服务端报错,App 显示服务端提示并恢复 loading。缓存失效、项目失效或模式价格不可用时,返回选片/报价步骤重新生成二维码,不复用旧金额。
11. SQL 与上线顺序
SQL 文件:20260914_face_print_electronic.sql。此文件只供用户/数据库维护人员执行,开发助手未执行 SQL,也不执行 php artisan migrate。
平台、店铺、景区共用数据库。上线顺序:
- 对目标数据库只读检查表和列定义,确认已存在打印/套餐阶梯表;缺少既有阶梯表时先核对既有
20260818_face_print_multi_price.sql的部署情况。 - 用户按新 SQL 中说明执行一次性新增列:
order.purchase_mode默认print,project_type_face_print.multi_photo_digital_discount_enabled默认0;更新既有price_type注释支持3。列已存在时跳过相应ADD COLUMN,不要直接重跑整份脚本。 - 发布平台后端与后台配置页面,同时发布景区、店铺 API 的配置保留改动。景区编辑保留未提交配置;店铺编辑或启用项目创建新版本时复制原阶梯配置。按现有发布流程重启长驻服务并处理框架缓存。先有数据库列,再发布会写这些列的代码。
- 在后台配置正数电子版价格和需要的阶梯,保存后核对详情回显与试算。 小程序继续使用原扫码支付和相册下载接口;本次付费电子版已沿用这些能力,发布新版 App 前仍须核对小程序能显示实际支付金额及照片准备中状态。
- 用支持
purchase_mode的 App 做下表人工联调,通过后再开放电子版入口;旧 App 可继续走默认打印。 - 回退应用版本时保留新增列和已有购买数据,先核对待支付/已购买电子订单的处理能力,避免旧代码把电子版订单当作打印订单。
12. 人工联调验收矩阵
以下均为待执行的联调项,不代表已经通过。
| 场景 | 操作 | 预期 |
|---|---|---|
| 基础打印 | 不传模式获取二维码,普通用户付款 | 模式 print;金额和打印、相册权益与原流程一致 |
| 基础电子版 | 正数基础价,关闭电子阶梯,选 1 张付款 | 电子单价和总额为基础价;完成后不打印,能领取原图 |
| 电子阶梯边界 | 依次选门槛前、门槛上、门槛后张数 | 最大已达到门槛单价用于全部去重照片 |
| 独立计价 | 调整旧赠送张数和首张金额 | 打印保留旧规则;相同电子配置的电子版金额不变 |
| 重复照片 | 同一 ID 提交多次 | 报价、锁定金额、HTTP 与推送中的照片集合均按去重计算 |
| 空选 | 空数组报价,再请求支付链接 | 返回基础单价与零总额;禁止空选下单 |
| 未配置电子价 | 基础价设 null;另验证旧数据库值为 0 的读取 |
打印可用;电子两字段 null,电子支付链接报错 |
| 非法价格/档位 | 提交零价、负价、3 位小数、重复门槛、门槛 1 | 后台拒绝;未保存坏配置 |
| 开关与保留 | 关闭电子优惠并传空数组;编辑旧字段时省略电子配置 | 原电子档位保留;未提交字段不被清空 |
| 跨端保留 | 景区 API 编辑时省略电子价;店铺 API 编辑或启用并审核新版本 | 原电子基础价、开关和各类阶梯保留;平台新报价读取当前项目配置 |
| 非法模式 | 提交空、null、拼写错误模式 |
报错,不自动打印 |
| 电子混入视频 | type=2、电子模式携带视频 |
获取支付链接失败,不能发生打印或扣纸 |
| 锁价 | 取二维码后后台改价,普通用户扫码 | 本单仍用已返回金额;重新取码采用新价 |
| 员工优惠 | 使用符合原有规则的员工账号扫码 | 允许实际付款 0.01 元,记录为已知金额例外 |
| 未扫码补查 | 仅生成预选缓存后调用支付结果接口 | 返回 10,type=5 不能被误判为购买成功 |
| 过期/失效 | 缓存过期、项目下线、设备改绑景区后扫码 | 拒绝或提示重新选择,不重新解释为其他模式 |
| 双人/重复扫码 | 两个账号扫描同一订单、同人重复支付 | 已建单归属不可被抢占;复用业务订单与原金额,已完成订单不再发起支付 |
| WebSocket 丢失 | 付款时断开推送,再走 HTTP 补查 | 返回 30、正确模式和同一照片集合,只处理一次 |
| 上传暂不可用 | 付款时使 FaceBox 上传请求失败,再恢复并重复取相册链接 | 购买仍完成;可重试转存,准备完成后可下载 |
| 重复素材回调 | 重放相同成功回调,夹带未购买照片 ID | 不重复素材,未购买照片不进入本单相册 |
| 电子禁打印 | 对电子订单调用两个打印回调接口 | 拒绝,打印记录和纸张数量不变化 |
| 跨设备订单 | 用另一设备鉴权查询、取相册或上报打印 | 拒绝,不泄露或修改其他设备订单 |
| 旧版上传 | type=1 完整上传、报价、扫码、打印 |
旧响应字段与旧业务流程保持一致 |
| 小程序人脸购买 | type=3 购买照片、视频及纯视频 |
保持原计价与相册展示,不支持新增电子模式 |
| 旧缓存 | 使用上线前无模式的有效缓存扫码 | 继续原 print 流程;新电子缓存绝不走该降级路径 |
验收记录应单独注明环境、设备、普通/员工账号、订单号、实付金额、HTTP/推送结果及实际下载结果。未完成真实支付、设备打印或相册下载时,应如实标记“未验证”,不能用语法检查、静态计算或示例响应替代端到端验收。