ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

易支付收银台模板实战:从接口签名到门店订单闭环

易支付收银台模板实战:从接口签名到门店订单闭环 简介易支付收银台模板是一套面向支付业务场景的精美前端界面与配套代码包适合需要搭建门店收银、云支付收银台或聚合支付入口的开发者、建站人员使用。模板以视觉设计和交互体验为核心内置聚合收银台界面及聚合码替换包并加入Apple Pay支付选项用户可在收银台直接完成快捷支付整体操作流程简洁流畅。资源共1087个文件压缩包大小约9.16MB其中562个PHP文件承担后端渲染与业务逻辑275个PNG及31个JPG提供界面切图与图标素材64个CSS与50个JS用于布局样式和前端交互另有少量SVG、字体、证书、SQL等文件便于适配部署。目前已有54人学习下载适合具备一定PHP开发基础的读者用于研究收银台界面实现、聚合支付接入方式或二次开发参考。需要留意资源仅限学习研究使用请勿用于商业运营或违法用途。1. 收银台模板不是一张图片这是一套能跑通的支付闭环很多人下载收银台模板以为拿到的是一套好看的 HTML 皮肤换个 logo 就能上线。实际拆完这份「易支付 精美设计的支付收银台模板 门店收银管理系统 云支付收银台」之后我得先纠正这个预期它包含了前端收银台页面、门店收银管理需要的接口对接逻辑以及一套围绕易支付体系设计的订单流转方案。你拿到的不只是界面而是一个可以直接对接易支付接口、在门店电脑或平板上完成扫码收银、订单查询和日结对账的最小可用系统。适合三类人接外包要快速交付门店收银项目的开发者、正在自建云支付收银台的创业团队、以及二开易支付源码想省掉前端开发时间的 PHP 工程师。这篇笔记按「模板结构 → 接口对接 → 收银流程 → 踩坑 → 验证」的顺序拆跟着走完你能在当地环境把它跑起来。2. 模板结构与前端改造先知道文件各自在干什么2.1 模板目录不复杂但入口别搞错这份模板解压后核心目录我建议这样认assets/css放样式assets/js放交互逻辑index.html是收银台主界面config.js是你几乎唯一需要手工改的配置文件。很多第一次接触的人会把index.html当成静态页面双击打开结果页面白屏或者二维码区一直转圈——原因后面避坑章节细说这里先记住一件事这个收银台是给「Web 服务器环境」跑的不是本地文件协议能跑起来的。模板里收银台的典型布局是三段式顶部是店铺信息和订单号中间是金额展示和支付方式切换支付宝 / 微信底部是扫码枪输入区或手动输入订单号的表单。这套布局对应的是门店真实场景顾客选完商品店员在收银台输入应收金额顾客扫码店员在页面上看到支付结果。文件之间的调用关系是index.html引入config.jsconfig.js里读到的配置项决定页面显示哪个店铺名、金额格式怎么展示、支付方式默认选中哪一个。我在实际项目里会先做一件看似多余但能省大麻烦的事把模板原始目录整体备份一份然后在副本上改。因为这类模板下载下来经常带着原作者留的调试字段、测试商户号你在config.js里改漏一个参数后续排查会分不清是你改坏了还是原包就有问题。2.2 config.js 是你唯一必须改的配置文件打开config.js你会看到类似下面这样一坨集中配置。我把它单独抽出来讲是因为 90% 的定制需求都集中在这个文件里// 收银台核心配置改这里就够了 window.CASHIER_CONFIG { // 易支付商户ID在易支付后台申请 merchant_id: 1000, // 易支付商户密钥保持私密不要泄露给前端用户 merchant_key: YOUR_MERCHANT_KEY, // 易支付网关地址自建易支付就填你自己的域名 api_base: https://pay.example.com/pay.php, // 异步通知地址支付成功后易支付会POST订单结果到这里 notify_url: https://your-site.com/notify.php, // 支付成功后的跳转地址用户看到的结果页 return_url: https://your-site.com/result.html, // 门店名称显示在收银台顶部 store_name: 示例便利店, // 订单超时分钟数超过后前端禁止发起支付 expire_minutes: 15, // 支付方式分组按需保留 pay_types: [alipay, wxpay] };参数含义拆一下merchant_id和merchant_key是易支付后台的一组身份凭证前者是公开的后者必须藏在服务端。api_base是易支付下单单接口地址如果你是用开源的易支付系统搭的就是你自己服务器上的pay.php路径。notify_url关系到自动对账非常关键易支付的服务器在用户支付成功后会往这个地址发一条异步通知你的后端必须在收到通知后更新订单状态才算完成闭环。expire_minutes控制收银台上的倒计时门店场景我一般设 15 分钟既给顾客留足操作时间又能及时释放未支付订单占用的流水号。改完之后别急着关花三十秒做个视觉走查金额格式化是否保留了两位小数、店铺名是否过长导致头部换行、支付方式图标是否能正常加载。这类前端小问题在门店屏幕上比在开发环境显眼得多早点发现早点改。2.3 定制收银台视觉和交互逻辑哪些能改、哪些别碰模板的视觉定制主要集中在assets/css下的主题变量里比如主色、圆角、间距。我一般会把主题色从默认的蓝色改成门店品牌色改完顺手检查对比度别让浅色文字压在浅色背景上手持扫码枪的店员没空眯着眼看屏幕。assets/js里有一个initCashier()入口函数负责绑定金额输入、支付方式选择、倒计时刷新这些事件尽量不要动它的执行顺序只改你需要的业务细节。一个最常用也最容易被改坏的交互是「金额输入框自动聚焦」。门店用扫码枪的场景店员扫完商品条码收银台要能无缝切换到收银界面。常见做法是在页面加载时调用.focus()但很多模板没处理扫码枪会触发keydown事件的问题——扫码枪本质是一个快速输入的键盘它会先输入一串字符最后再补一个回车。如果你的输入框监听的是change事件而不是回车事件就会漏单。我建议你在这个模板里保留一个专门用来接收扫码枪输入的隐藏或半隐藏输入框然后监听回车事件来触发查询。这样既能兼容扫码枪又不会干扰手动输入金额的店员。具体的事件监听代码我放在第四章门店收银管理那边和订单流程放一起更连贯。3. 易支付接口对接签名、下单与异步通知3.1 接口签名规则是先决条件这里最容易翻车易支付这类聚合支付系统的接口风格大同小异核心是 MD5 签名。签约流程用一句话总结把所有业务参数按参数名 ASCII 升序排列拼接成参数名参数值参数名参数值的字符串末尾拼上商户密钥再做一次 MD5。这套规则在易支付前后版本里基本稳定你要做的是先拿官方给的测试商户号跑通一次再换自己的真实商户号。下面是我常用的 PHP 签名函数模板对接时可以直接复用function build_sign(array $params, string $key): string { // 过滤空值和签名本身避免拼出脏数据 // 易支付的约定空参数不参与签名 $filtered []; foreach ($params as $k $v) { if ($v || $v null || $k sign) { continue; } $filtered[$k] $v; } // 按参数名ASCII升序排序 ksort($filtered); // 拼接成 kvkv 形式 $segments []; foreach ($filtered as $k $v) { $segments[] $k . . $v; } $query implode(, $segments); // 末尾拼接商户密钥MD5后转小写 return md5($query . $key); }逻辑拆解是这个函数最关键的部分。第一步过滤空值很多人漏掉这一步导致易支付服务器验签不过报「签名错误」。第二步排序顺序错了签出来的值一定不对。第三部拼接注意是keyvalue中间没有空格也没有 URL 编码。最后md5之后易支付约定是小写如果改成大写也要全链路统一。调试时如果签不上最快的排查办法不是死盯代码而是error_log把拼好的字符串打出来自己手动做一次 MD5对比易支付后台收到的签名值。3.2 发起支付把收银台表单转成一条跳轉链接商户在前台填写完金额、选好支付方式后收银台要做的事是把订单信息发给易支付网关。这里我强烈建议前端拿到config.js里的merchant_key做签名是绝对禁止的密钥一旦暴露在浏览器端等于把整个收银系统交给别人。正确流程是前端把订单号、金额、支付方式提交到你的后端后端做签名然后再跳转易支付。下面是后端发起支付的示例我用的是 ThinkPHP 风格的简单控制器写法但核心逻辑是通用的public function createPayOrder() { // 只接收必要的字段别把整个POST直接透传 $amount (int)($_POST[amount] ?? 0); // 单位分前端传入 $orderNo (string)($_POST[order_no] ?? ); $payType (string)($_POST[pay_type] ?? alipay); // 校验金额和订单号防止0元单和超长字符 if ($amount 0 || $orderNo || !in_array($payType, [alipay, wxpay])) { exit(json_encode([code 400, msg 订单参数不正确])); } // 读取服务端配置密钥从这里拿 $merchantId CASHIER_CONFIG[merchant_id]; $merchantKey CASHIER_CONFIG[merchant_key]; $apiBase CASHIER_CONFIG[api_base]; $notifyUrl CASHIER_CONFIG[notify_url]; $returnUrl CASHIER_CONFIG[return_url]; // 组装易支付要求的业务参数 $params [ pid $merchantId, type $payType, out_trade_no $orderNo, notify_url $notifyUrl, return_url $returnUrl, name 门店收银订单- . $orderNo, money $amount, // 注意易支付按元还是按分以你用的版本为准 ]; // 签名后拼接跳转地址 $params[sign] build_sign($params, $merchantKey); $payUrl $apiBase . ? . http_build_query($params); // 302跳转到易支付收银台 header(Location: . $payUrl); exit; }这里有一个参数单位必须确认易支付原始版本是金额以「元」为单位但有些二开版本改成了分。模板的config.js里如果写的是元而后端按照分来传会导致实际支付金额差一百倍——这是收银系统里最可怕的坑必须在联调阶段用一笔 0.01 元的测试单先验一遍。out_trade_no是你自己的订单号必须保证唯一易支付侧通常也会做重复校验重复的订单号会给后续退款对账留下隐患。还有一点容易被忽略模板里money参数有的版本要求是字符串有的要求是数字。我的习惯是统一传字符串因为签名拼接时money1.00和money1是两个完全不同的签名串易支付回调回来的参数类型如果和下单时不一致验签也会失败。为了避免这种类型问题后端在组装参数时把money统一转成number_format($amount, 2, ., )这样回调时也用相同格式生成签名两边就能对上。3.3 异步通知处理验签、查单、改状态、回 success支付完成后易支付服务器会向notify_url发送一条异步通知通知内容是 POST 和 GET 都有可能不同版本有差异包含订单号、实付金额、交易状态、签名等字段。你的notify.php要做四步先取原始数据、再验签、然后核对订单金额、最后改库并回success。public function notify() { // 易支付版本不同回调数据可能在POST也可能在GET $data $_POST; if (empty($data)) { $data $_GET; } // 第一步固定记录原始回调内容排查问题时的后悔药 file_put_contents(/tmp/notify_ . date(Ymd) . .log, json_encode($data) . \n, FILE_APPEND); // 第二步验签注意要把sign从原数据里剥掉再算 $sign $data[sign] ?? ; unset($data[sign]); // 金额单位统一成分为后面比较做准备 $callbackMoney $data[money] ?? 0; if (build_sign($data, CASHIER_CONFIG[merchant_key]) ! $sign) { echo fail; exit; } // 第三步查本地订单比对金额和订单号 $order Db::name(cashier_order) -where(order_no, $data[out_trade_no]) -find(); if (!$order || $order[status] ! 0) { // 订单不存在或已处理也回success防止对方重复推送 echo success; exit; } // 比对金额单位分转元或元转分后严格相等 if (abs($order[amount] - $callbackMoney * 100) 1) { error_log(金额不一致: order . $order[amount] . callback . $callbackMoney); echo fail; exit; } // 第四步改状态加一个事务 Db::name(cashier_order) -where(order_no, $data[out_trade_no]) -update([status 1, notify_time date(Y-m-d H:i:s)]); echo success; }这段里最容易出问题的是第二步和第三步。第二步验签失败时你回fail易支付会重试这不是坏事但每次重试你都要能看到日志所以第一步的记录千万不能省。第三步比对金额这一步是很多支付系统被刷单的核心漏洞——只验签不比对金额攻击者可以伪造一条合法签名的低金额订单盗刷高金额商品。这里我用abs(...) 1做容差避免浮点误差但这只是兜底正常逻辑下单笔金额必须严格相等。还有一个细节订单状态判断里我要求status ! 0时直接回success。原因是如果回调重复推送你已经处理过这笔订单再执行一次会重复改状态虽然结果幂等但会污染日志。回success让对方停止重试是这个场景下的标准做法。4. 门店收银管理落地扫码、退款与日结4.1 门店场景下的订单状态机与表结构设计把易支付回调跑通之后收银台才算真正接进了业务。门店收银管理系统里订单状态不是简单一个字段能覆盖的我在这套模板里通常维护一个四态订单表待支付、已支付、退款中、已退款。退款中这个状态容易被新手遗漏但线下门店退款经常有店长审批环节中间态必不可少。表结构遵循电商惯例金额全部用「分」存储为整数避免任何浮点运算CREATE TABLE cashier_order ( id int(11) unsigned NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL DEFAULT , amount int(11) unsigned NOT NULL DEFAULT 0 COMMENT 订单金额单位分, paid_amount int(11) unsigned NOT NULL DEFAULT 0 COMMENT 实付金额单位分, pay_type varchar(10) NOT NULL DEFAULT COMMENT alipay/wxpay/cash, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2退款中 3已退款, notify_time datetime DEFAULT NULL COMMENT 支付回调时间, refund_time datetime DEFAULT NULL COMMENT 退款完成时间, operator varchar(32) NOT NULL DEFAULT COMMENT 收银员, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT门店收银订单表;几个字段的用意说一下order_no有唯一索引防止并发重复插入amount和paid_amount分开存是因为易支付偶尔会出现支付金额小于订单金额的极端情况比如用户支付时手动改了金额店长对账时看到差异能立刻定位operator存收银员账号门店有交接班需求时可按人拉账单。idx_status_created联合索引专门服务日结查询——按状态过滤并按时间排序没有这个索引门店营业一整年的订单数据查询会慢到店长怀疑人生。4.2 扫码枪输入订单号回车事件才是本体门店收银台最常用的操作不是手动输入而是扫码枪扫顾客的付款码或扫商品条码。我前文说扫码枪本质是键盘这里给出模板改造后的完整监听逻辑// 绑定扫码枪输入监听核心是keydown里的Enter分支 const input document.getElementById(cashierScanInput); let scanBuffer ; input.addEventListener(keydown, function (e) { if (e.key Enter) { e.preventDefault(); // 扫码枪输入完成触发一次查询动作 const orderNo scanBuffer.trim(); if (/^\d{6,32}$/.test(orderNo)) { queryOrder(orderNo); } else { showToast(订单号格式不正确: orderNo); } // 清空缓冲区准备下一单 scanBuffer ; return; } // 只累计数字字符过滤掉Shift/Ctrl等功能键产生的干扰 if (e.key /^\d$/.test(e.key) !e.ctrlKey !e.metaKey) { scanBuffer e.key; } });逻辑上要注意两点。第一我没有用input.value来取订单号而是自行维护scanBuffer因为扫码枪输入速度极快浏览器合成事件未必能保证input.value在Enter触发时已经完整更新。自建缓冲区可以规避这个时序问题实测下来更稳。第二正则限制了 6 到 32 位数字门店扫码枪扫出来的订单号一般是纯数字如果误扫了二维码或者条码带了字母这里会直接拦截并提示避免把脏数据带进查询链路。queryOrder函数就是向后端GET /order/simple-info发送请求返回订单金额、支付状态、支付时间。拿到结果后页面弹一个小浮层显示「订单已支付实付 12.50 元支付宝」同时把按钮从「发起收款」变成「打印小票」。这个小交互是门店感知最直接的体验点一定要做清楚不能只是改一行文字了事。4.3 退款与日结对账数据库事务和统计 SQL门店每天都会出现几种退款场景顾客买错、扫码后店里发现商品缺货、重复收款。易支付不一定都支持原路退回传统做法是调易支付的退款接口如果没有就标记人工退款。我在模板里统一封装成两步第一步发起退款前先在本地状态改到退款中记录操作员、退款原因、操作时间Db::transaction(function () { // 悲观锁锁定订单行防止两个人同时退款 $order Db::name(cashier_order) -lock(true) -where(order_no, $refundOrderNo) -find(); if (!$order || $order[status] ! 1) { throw new \Exception(订单不存在或不可退款); } Db::name(cashier_order) -where(order_no, $refundOrderNo) -update([ status 2, refund_operator $currentUser, refund_reason $reason ]); });第二步等易支付返回退款结果或运营确认打款后把状态改成已退款同时记录refund_time。中间这一步如果系统崩了订单会一直卡在退款中所以我在管理后台加了一个定时扫描每十五分钟捞一次超过三十分钟仍然在退款中的订单自动标记退款异常人工介入。日结是所有老板最关心的功能。我用一条聚合 SQL 就能出当天经营报表SELECT DATE_FORMAT(notify_time, %Y-%m-%d) AS biz_date, SUM(CASE WHEN status 1 THEN paid_amount ELSE 0 END) AS real_amount, SUM(CASE WHEN status 3 THEN paid_amount ELSE 0 END) AS refund_amount, COUNT(DISTINCT CASE WHEN status 1 THEN order_no END) AS pay_count FROM cashier_order WHERE notify_time CURDATE() AND notify_time CURDATE() INTERVAL 1 DAY GROUP BY biz_date;这条 SQL 的细节在于用了notify_time而不是created_at作为营业日归属。因为created_at是下单时间晚 23:59 下的单可能 00:05 才支付成功如果按下单时间归日这笔交易会被算到前一天第二天对账怎么都对不上。按支付回调时间归日跟易支付后台的账单口径一致这是我从一次对账对到半夜学来的经验。5. 常见问题与避坑模板跑不通的五个高频现场5.1 双击打开 index.html 白屏接口 404现象本地没有启动 Web 服务器直接用浏览器打开index.html页面样式能加载一部分但扫码、查询、下单这些功能全部报错打开 F12 能看到接口请求路径是file:///开头。原因模板里的 JS 代码用了相对路径请求后端接口比如/notify.php、/api/order/query.php浏览器在file://协议下无法解析这些地址。这不是模板写错了而是运行环境不对。解决用 PHP 内置服务器或 Nginx 搭建站运行。本地最快的方式是在模板目录执行php -S 127.0.0.1:8080然后浏览器访问http://127.0.0.1:8080。配置好虚拟主机之后再回来测试所有功能。从那以后我拿到任何 Web 模板的第一件事就是先确认它在 http 协议下能跑通。5.2 易支付回调验签失败日志显示签名不一致现象用户支付成功但notify.php一直回fail易支付后台显示通知失败订单状态停留在待支付。日志里记录的签名值和你本地算出来的不一致。原因常见有三种。一是易支付回调数据里带了sign之外的特殊字段你没有过滤干净二是金额等字段的格式和下单下单时不一致比如下单传的是1.00回调收到的是1三是merchant_key前后有多余空格单看日志很难发现。解决第一步把回调原始数据完整打印到日志确认有哪些字段是你不认识的。第二步把所有参与签名的字段值做格式统一金额统一number_format保留两位小数字符串字段确认没有UTF-8 BOM。第三步给merchant_key加个trim()再缓存到配置里一次根治空格问题。血泪经验是签不上名时先怀疑格式再怀疑逻辑不要一上来就翻易支付源码。5.3 用户支付成功但订单状态没变回调没有进来现象支付流程看着走到了易支付收银台二维码也扫了钱也扣了但门店管理系统里订单还是显示待支付顾客催促店员。原因notify_url配置成写死了或者写成了localhost、内网 IP易支付的服务器根本访问不到。还有一种可能是你的服务器防火墙把易支付服务器的出口 IP 挡了。解决线上环境的notify_url必须是一个公网可访问的 HTTPS 地址不能带任何内网映射。自建易支付的话在网关后台检查一下回调记录。本地联调时可以用内网穿透工具把本地notify.php暴露到公网但正式上线后一定记得改成线上域名。这里最靠谱的验证方式是在易支付后台手动重发一次通知如果你的notify.php有日志一发就知道通没通。5.4 金额显示是对的但支付出来差一分钱现象收银台显示应收 12.50 元顾客扫码支付易支付扣了 12.50 元但系统日志里记录的金额是 12.49 或 12.51日结时不平。原因前端把金额从元转换成分的时候用了parseFloat(value) * 100JavaScript 浮点运算在处理12.50时会出现1250.0000000000002转成整数时被四舍五入搞掉了。这属于经典浮点精度问题不只在收银台出现但在这里危害最大。解决前端不参与金额数值运算收银台把输入框的字符串原样传给后端由后端用整数分计算。PHP 侧用bcmul($amount_text, 100, 0)或者手动正则提取整数部分乘以 100。如果一定要前端转换用Math.round(parseFloat(value) * 100)不要直接用parseInt截断。5.5 扫码枪扫了没反应或者扫出一个残缺订单号现象店员扫码枪扫一下付款码收银台没有任何动作有时候扫进去的订单号少一位多一位。换一把扫码枪又好了。原因扫码枪的键盘模拟设置不同有的默认在数字前面补前缀码有的输入完不自动回车需要手动按一下回车。另外扫码枪和键盘同时使用时焦点没停在输入框上扫码枪的输出全部丢了。解决在收银台页面加载时强制document.getElementById(cashierScanInput).focus()同时按第四章的方式监听keydown的Enter。对于前缀码问题在 JS 里做一个清洗收到完整串后用正则把非数字前缀剥离只保留末尾的数字位。如果真的兼容不了用支持「回车后缀」模式扫码枪在枪的说明书里找Enter Suffix设置项。6. 进阶验证本地一键把收银台整个跑通再上线收银系统是资金链路容不得「上线再调」。我养成的习惯是在本地跑一个完整闭环验证脚本确认签名、下单、回调、改库每一步都正常然后才让门店接入真实配置。这套验证思路不依赖买服务器一条 PHP 内置服务器加一个脚本就够了。第一步启动本地 Web 服务把模板和后端代码放在同一个根目录# 在模板根目录下执行启动PHP内置服务器 php -S 127.0.0.1:8080 -t ./第二步写一个测试订单脚本往cashier_order表插一条待支付订单伪造一条易支付回调请求用curl打到notify.php。这里注意伪造的签名要和你本地build_sign算出来的一致验证方法是直接在脚本里调用同一个签名函数# 伪造一条易支付回调测试notify.php是否正常处理 curl -d out_trade_noT20240301153000money12.50trade_statusTRADE_SUCCESSsign$(php test_build_sign.php T20240301153000 12.50) http://127.0.0.1:8080/notify.phptest_build_sign.php里做的事就是在脚本里拼好参数数组调用build_sign输出签名。跑完这一步如果notify.php回声success然后查库看到订单状态从 0 变成了 1说明签名、验签、改库这条链路是通着的。第三步把整个流程连起来做一轮业务穿越测试打开收银台页面输入测试金额选择支付方式点确认收款页面跳到易支付的测试网关如果你用开源自建的本地起一个模拟网关就行然后再手动触发回调。这一步验证的是前端config.js配置的后端地址是否对、后端下单参数是否正确、回调后收银台页面有没有正常刷新状态。从那以后我每次改完模板或接口参数都会强制自己走一遍这个三步验证能挡掉至少一半的线上故障。门店老板不懂技术他只知道「昨天还好好的今天你改了之后钱进不来」这个锅你背不起。希望这篇拆解能帮你把这套易支付收银台模板真正跑出价值少走我之前踩过的弯路。本文还有配套的精品资源点击获取
返回列表