一、为什么要做本地 POS 对接
传统 OCPP 充电流程通常是“扫码进入 App—创建订单—远程启动”。在园区、车队、酒店和海外站点,用户更习惯直接在设备旁刷信用卡。POS 本地对接的关键是:支付动作发生在桩旁,OCPP 仍然负责桩的连接和充电控制,平台负责订单、风控和最终结算。
两套协议的职责应明确分开:
| 层次 | 协议 | 主要职责 |
|---|---|---|
| 充电控制 | OCPP 1.6(WebSocket) | BootNotification、心跳、状态、RemoteStartTransaction、RemoteStopTransaction、计量值和交易事件 |
| 支付交互 | 信用卡充电扩展协议 V1.8(桩内网/串口或 TCP) | 插卡/挥卡、预授权、扣款、撤销、终端结果和离线流水 |
| 平台内部 | POS Adapter 统一模型 | 把 V1.8 报文转换为订单和支付状态,提供幂等、重试和审计 |
不要让 POS 直接调用 OCPP 数据库,也不要让桩端自行计算最终应收金额。桩端只上送计量和交易事件,金额由平台根据费率和结算规则计算,POS 只负责持卡人认证和支付结果。
二、推荐架构
信用卡/手机
|
POS 终端 --(V1.8)--> 充电桩控制器 --(OCPP 1.6/WebSocket)--> OCPP 网关
| |
+-------------------------+
订单服务
|
支付适配器 / 收单机构 / 对账服务
POS Adapter 建议部署在 OCPP 网关或站点边缘服务中,向南提供 V1.8 的同步/异步报文接口,向北调用订单服务和支付渠道。平台至少保存四类标识:stationId、connectorId、transactionId、paymentId。任何重试都使用原始交易号和支付号,不能重新生成订单。
三、把 V1.8 映射为统一支付模型
不同版本的协议表可能使用不同的命令码和字段名,接入时先建立一张“协议字段—平台字段”映射表。建议统一为以下模型:
| 平台字段 | 含义 | 来源/生成时机 |
|---|---|---|
requestId |
请求流水,保证报文幂等 | 平台生成,原样回传 |
terminalId |
POS 终端号 | V1.8 终端信息 |
stationId / connectorId |
站点和枪口 | OCPP 连接上下文 |
transactionId |
OCPP 充电交易号 | StartTransaction.conf |
paymentId |
支付渠道流水 | 预授权或扣款成功后生成 |
amount |
金额,最小货币单位 | 平台费率计算 |
meterStart / meterStop |
起止表底 | OCPP 计量值 |
resultCode |
POS/渠道结果码 | V1.8 响应 |
mac/sign |
报文完整性校验 | V1.8 安全字段 |
金额、时间和枚举值必须按 V1.8 的长度、编码、补零和字节序实现。适配器不要直接把协议字符串传入订单库,而应先做长度校验、字符集转换、枚举白名单校验,再落库。
四、完整业务流程
1. 终端上线
POS 启动后发送 V1.8 登录/签到报文,平台校验终端号、站点绑定和协议版本,返回成功响应。OCPP 侧桩完成 BootNotification 并进入 Available。平台只有在两侧都在线时才允许开始支付。
2. 预授权与创建订单
用户在 POS 上选择枪口或输入充电金额,POS 发起预授权请求。平台先检查枪口状态、费率、黑名单和并发订单,再调用收单渠道。预授权成功后创建订单,状态置为 AUTHORIZED,并通过 OCPP RemoteStartTransaction 启动充电。
建议采用“平台生成启动令牌”的方式关联两条链路:idTag = POS-{requestId}。收到 StartTransaction.conf 后,将 OCPP 返回的 transactionId 写回订单;如果启动被拒绝,立即撤销预授权。
3. 充电中
平台监听 MeterValues、StatusNotification 和心跳,按固定周期刷新已用电量和预估金额。POS 可通过 V1.8 查询报文显示“充电中、已充电量、预估金额”,但展示数据不是最终结算依据。
4. 停止、结算与签购单
用户在 POS 按停止、拔枪,或平台根据预授权上限停止交易。平台发送 RemoteStopTransaction,并等待桩上送 StopTransaction.req;平台取该请求中的 meterStop 和停止原因计算最终金额,回复 StopTransaction.conf 后再调用渠道完成扣款或部分撤销。
扣款成功后,订单变为 COMPLETED,平台通过 V1.8 返回支付结果、金额、授权码和凭证信息;扣款失败时订单进入 PAYMENT_RETRY,不能把充电订单直接标记为成功。重复的停止通知、支付回调和 POS 查询都必须返回同一最终结果。
五、状态机和幂等设计
推荐的订单状态如下:
CREATED -> AUTHORIZING -> AUTHORIZED -> CHARGING
| |
STOPPING <-+
|
SETTLING -> COMPLETED
| |
PAYMENT_RETRY REFUNDING
每个外部请求建立幂等键:terminalId + requestId + operation。数据库对该组合建立唯一索引,并保存原始请求、响应、协议版本和时间戳。处理重试时先查幂等表,已成功则直接返回历史响应,处理中则返回“处理中”,避免重复扣款。
状态迁移必须使用条件更新,例如只有 AUTHORIZED 才能进入 CHARGING,只有 STOPPING 才能进入 SETTLING。这样即使 OCPP 消息乱序,也不会把已完成订单回退到充电中。
六、断网、超时和异常处理
- POS 到平台断网:桩端缓存 V1.8 流水和计量摘要,使用递增序号;网络恢复后按序补传。补传接口仍使用原
requestId,平台按幂等规则处理。 - OCPP 断线:平台将订单置为
CHARGING_UNKNOWN。OCPP 1.6 没有标准的交易状态查询命令,桩重连后应结合补传的StopTransaction、StatusNotification、后续MeterValues,或双方约定的DataTransfer查询扩展核对状态;不能仅凭 WebSocket 断开就结算。 - 预授权成功但启动失败:执行撤销;撤销也要有独立幂等号,并进入对账队列等待渠道确认。
- 停止成功但扣款超时:保留订单和计量数据,进入
PAYMENT_RETRY,按指数退避重试,超过阈值转人工/风控队列。 - 重复扣款风险:支付请求必须携带原支付号和商户订单号,渠道超时后先查询再重试,禁止盲目重新发起扣款。
七、实现要点
协议适配器
适配器分为编解码、校验、业务路由三层。编解码只处理 V1.8 的定长/变长字段和校验码;校验层负责版本、长度、终端绑定、时间窗和签名;业务路由把命令转换成 AuthorizePayment、StartCharge、StopCharge、SettlePayment 等内部命令。这样未来升级协议时,不需要改订单状态机。
OCPP 关联
保存 connectorId 和 transactionId 的双向索引。RemoteStart 发出后设置超时定时器;在超时窗口内收到 StartTransaction.conf 才确认启动。停止时先记录“停止意图”,再发送 RemoteStop,最后以 StopTransaction.req 的最终表底为准。
安全
全链路使用 TLS(或站点专用安全隧道),V1.8 的 MAC/签名密钥按终端独立管理,密钥不得写入日志。日志中对 PAN、磁道数据、有效期和 CVV 做脱敏;平台只保存支付渠道返回的令牌、授权码和末四位。生产环境启用重放保护:校验时间戳、流水号和 nonce,并拒绝过期报文。
八、测试清单
- 正常刷卡、预授权、启动、充电、停止、扣款全流程。
- POS 重发同一请求、平台重复回调、OCPP 消息乱序。
- 预授权成功但桩拒绝启动、拔枪停止、急停和桩离线。
- 扣款超时、撤销失败、部分退款和对账补单。
- 断网缓存、恢复补传、重启恢复和跨时区时间处理。
- 非法长度、错误签名、过期流水、未知命令码和不支持的协议版本。
上线前建议用沙箱渠道跑完至少一轮“成功、失败、超时、重试、冲正”五类交易,并把 POS 原始报文、OCPP 原始报文、订单事件和渠道流水放在同一条可检索链路中。
九、结语
OCPP 1.6 负责“桩能否充电”,信用卡扩展协议负责“持卡人能否完成支付”。通过 POS Adapter 做协议隔离,以统一订单状态机串联预授权、充电和结算,再配合幂等、断网补偿和对账机制,就能在不修改 OCPP 核心实现的情况下,为现有充电桩平台增加本地刷卡能力。
实际落地时,请以《信用卡充电扩展协议 V1.8》中的命令码、字段长度、编码方式、校验算法和错误码表替换本文示例模型,并由收单机构确认 PCI DSS、预授权有效期及撤销规则。