简介:这是一套采用全新UI设计的自助图文打印系统小程序源码,包含PHP后端与前端小程序,适合需要搭建线上打印服务的开发者或商家。前端基于微信小程序,后端基于ThinkPHP,附带安装部署教程,覆盖域名配置、HTTPS开启、数据库修改等关键步骤。压缩包共2000个文件,约72.59MB,以JavaScript文件为主(1660个),配合HTML、CSS、JSON等前端资源,以及Markdown文档和说明文件,便于查看与二次开发。目前在CSDN已有580人学习下载。资源提供完整前后端代码、后端环境配置说明(Nginx+PHP7.4+MySQL5.6,需sg11扩展)、数据库文件位置及后台登录信息,并附前端导入微信开发者工具及合法域名配置指引。读者可基于此快速搭建一套可运行的图文打印系统,并参考UI与业务逻辑进行个性化改造。
1. 全新UI自助图文打印系统小程序源码 PHP后端:从一张上传文件开始的生意
我见过不少想做自助打印的人,一开始把注意力全放在“UI界面好不好看”上,结果后端接口和订单状态一塌糊涂,图传上来了,店员却不知道要不要印。这里说的全新UI自助图文打印系统小程序源码 PHP后端 附教程,本质是一条完整的链路——小程序负责客户选文件、选参数、付款,PHP后端负责生成打印任务、算价格、回调发货,教程负责把部署和配置讲得能落地。它适合两类人:一类是学校附近或图文店想接小程序订单的店主,另一类是接外包项目、需要快速给客户交付可运行系统的开发者。今天这篇就按这个链路把数据库、接口、部署和坑一次说透。
2. PHP后端先立住:数据库、订单状态机和登录手机号接口是怎样合在一起跑的
自助打印小程序看着不复杂,但真正做起来,前端UI只是壳,后端必须同时管住登录、价格、订单、打印任务、支付回调五件事。这一章我先讲后端的边界如何划分,再给出一套能直接建表的关系型数据库设计,最后落到微信小程序登录获取手机号这个最容易挡住新手的第一步。
2.1 自助打印系统的后端边界:PHP后端该管哪几件事
很多人把这类系统做成一个“上传文件+给打印机发消息”的小玩具,结果生意一开张就崩。原因在于,自助图文打印系统不是简单把文件扔给打印机,它要处理的是“客户操作和门店执行之间的账目核对”。后端至少要拆成五个模块:用户身份、价格计算、订单流转、任务分发、支付回调。
用户身份解决“这个人是谁、手机号能不能联系上”;价格计算要按纸型、色彩、单双面、份数动态算出金额;订单流转要记录从待支付到打印完成的每次状态变化;任务分发要决定哪个打印队列接收这个文件;支付回调是微信支付往你的后端发来支付成功通知,后端拿到回调才能把订单置为待打印。这五个模块中,最容易被忽略的是任务分发,因为它不像支付那样有现成SDK,却直接决定你的打印店一天能接多少单。
自助打印小程序不像小程序商城那样有固定的SKU和库存表,商品的参数是组合出来的:A4黑白单面是0.5元,A4彩色双面可能就是1.5元,再加上份数、页数,前端拿到的是一个计算好的总价。前后端分离项目实战里的常见毛病,是把价格计算放在小程序里,后端只存总额。这样做一旦门店调价,小程序不发新版本就没法生效。我的习惯是前端只负责展示,所有金额计算都在PHP后端完成,并且每次报价都要带一个价格版本号,订单生成时锁定当时价格,避免用户下了单再看账单时金额变了。
UI层和PHP后端分离以后,还有一个好处:小程序端可以专心把页面做得清爽,换UI框架、换主题都不用动业务逻辑。后端接口保持稳定,前端只要保证请求路径和参数名对得上,就可以单独迭代。很多“源码包带UI”的项目,UI层和后端耦合得很深,改个按钮颜色都要翻PHP文件,这是最初设计时就该避免的。
2.2 给订单建模:用户表、打印任务表和价格配置表怎么设计
回到可执行的层面,我一般会建四张核心表:用户表、门店打印机表、打印订单表、价格配置表。辅助的表还有支付流水表、退款记录表和登录日志,但前四张决定系统能不能跑通。用户表不存微信原始openid以外的敏感信息,手机号单独存加密后的值;打印机表记录设备和门店的关系;订单表是整个系统的状态中心;价格配置表不写死价格,方便门店后台随时调。
下面是一份能直接执行的建表SQL,结构上兼顾了查询速度和状态追溯:
CREATE TABLE `user` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL DEFAULT '', `mobile` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '加密后的手机号', `nickname` VARCHAR(64) DEFAULT '', `status` TINYINT NOT NULL DEFAULT 1, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `printer` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `store_id` INT UNSIGNED NOT NULL DEFAULT 0, `printer_name` VARCHAR(64) NOT NULL DEFAULT '', `ip_address` VARCHAR(64) NOT NULL DEFAULT '', `port` INT NOT NULL DEFAULT 9100, `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1在线 0离线', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `print_order` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL DEFAULT '', `user_id` INT UNSIGNED NOT NULL, `printer_id` INT UNSIGNED NOT NULL, `file_path` VARCHAR(255) NOT NULL DEFAULT '', `pages` INT NOT NULL DEFAULT 1, `copies` INT NOT NULL DEFAULT 1, `color_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1黑白 2彩色', `paper_size` VARCHAR(16) NOT NULL DEFAULT 'A4', `amount_fen` INT NOT NULL DEFAULT 0 COMMENT '金额,单位分', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2待打印 3打印中 4完成 -1取消 -2退款', `fail_reason` VARCHAR(255) NOT NULL DEFAULT '', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user` (`user_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `price_config` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `store_id` INT UNSIGNED NOT NULL DEFAULT 0, `paper_size` VARCHAR(16) NOT NULL DEFAULT 'A4', `color_type` TINYINT NOT NULL DEFAULT 1, `price_per_page_fen` INT NOT NULL DEFAULT 50, `status` TINYINT NOT NULL DEFAULT 1, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段SQL里值得说的是amount_fen,金额一律用“分”存整数,避免浮点计算带来的精度问题。print_order.status用整数状态位,后面在PHP枚举里映射成文案,前端不需要知道数字是什么意思。fail_reason字段很关键,打印失败时把打印机返回的错误信息写进去,否则售后时只能靠客户截图。
参数上注意两点:user.mobile存的是加密值而不是明文,这样即便数据库被拖走,手机号也不是裸奔的;print_order.order_no要设置唯一索引,订单号生成规则用“日期+门店ID+随机串”,目的是并发时不容易冲突。price_config不需要存总价,每次下单时用页数、份数、单价计算,并把当时的单价快照写进订单扩展字段,防止用户打印一半改价产生纠纷。
2.3 微信小程序登录获取手机号:后端要做code换session和手机号解密
小程序端的第一步不是上传文件,而是登录。微信小程序登录获取手机号的标准链路是:前端调用wx.login拿code,再把code发到PHP后端,后端通过code2session接口换openid和session_key;之后用户点击“手机号快捷登录”时,前端拿到iv和encryptedData,注意还有获取手机号的code,一并交给后端解密。
代码上我建议拆成两个接口:一个处理登录换openid,一个处理手机号解密。登录接口的PHP骨架大致是这样:
public function login(Request $request) { $code = $request->input('code'); $appid = config('wechat.mini.appid'); $secret = config('wechat.mini.secret'); $url = "https://api.weixin.qq.com/sns/jscode2session?" . "appid={$appid}&secret={$secret}&js_code={$code}&grant_type=authorization_code"; $resp = file_get_contents($url); $data = json_decode($resp, true); if (isset($data['errcode']) && $data['errcode'] !== 0) { return $this->fail('登录失败'); } $user = User::firstOrCreate(['openid' => $data['openid']]); $token = bin2hex(random_bytes(32)); // 建议把token写入redis或cache,设置7天过期 cache()->put('user_token_' . $token, $user->id, 604800); return $this->success([ 'token' => $token, 'user_id' => $user->id, ]); }这里用了file_get_contents调微信接口,简单直接,但生产环境建议换成curl并加超时设置,比如超时3秒。返回里的token是后端自己生成的不透明token,不要让小程序直接拿openid当前端身份凭证,否则别人改个openid就变成另一个人。第一次登录用firstOrCreate很合适,用户不存在就创建,已经存在就复用。
手机号解密接口的PHP代码如下:
public function decryptMobile(Request $request) { $encryptedData = $request->input('encrypted_data'); $iv = $request->input('iv'); $sessionKey = $this->getSessionKeyByToken($request->input('token')); $aesKey = base64_decode($sessionKey); $aesIv = base64_decode($iv); $aesCipher = base64_decode($encryptedData); $result = openssl_decrypt($aesCipher, 'AES-128-CBC', $aesKey, OPENSSL_RAW_DATA, $aesIv); $result = json_decode($result, true); if (!$result || isset($result['purePhoneNumber']) === false) { return $this->fail('手机号解密失败'); } User::where('id', $request->input('user_id')) ->update(['mobile' => encrypt($result['purePhoneNumber'])]); return $this->success(['mobile' => $result['purePhoneNumber']]); }这段代码的关键是:session_key只能用来解密,绝不能返回给前端。openssl_decrypt的算法是AES-128-CBC,填充方式微信要求OPENSSL_RAW_DATA,如果你的PHP版本或扩展不一样,经常会在这里出现解密失败。解密成功以后,手机号用Laravel的encrypt函数入库,这样就算日志里打了用户信息,看到的也是一串密文。
参数上要注意,手机号解密用的code和登录code是两回事,小程序端需要分别获取。iv每次点击都会变,不能缓存。如果解密总是失败,先检查session_key是否过期,微信小程序里session_key有效期不确定,用户长期不打开小程序再次进入时,后端应该让它重新走一遍登录流程。
3. 小程序UI层对接PHP后端:传图下单和登录态失效处理的三段关键代码
后端接口写好以后,进入小程序UI层对接的阶段。这一章把UI层的页面结构、网络请求封装和文件上传三件事讲明白。很多人在这里翻车,就是因为直接把wx.request撒在每一个页面里,登录态失效时没有任何统一处理,用户看到的报错永远是“网络错误”。
3.1 小程序UI层页面结构:用UI框架把自助打印流程拆成四个页面
自助打印的小程序端不需要做成商城那样几十个页面,核心流程四页就够:首页选择打印类型、上传文件页、确认订单页、订单列表页。再加上一个“我的”页面放个人信息和联系客服入口,已经能覆盖九成场景。UI层独立的好处是,可以选一套现成的UI框架,比如Vant Weapp或者uni-app自带的组件,这样按钮、弹窗、加载状态统一,不需要自己写样式。
我一般会用原生微信小程序配合一套轻量组件库。原因是自助打印项目的交互复杂度不高,没必要为了几个弹窗引入重框架;但列表和表单控件建议用现成组件,能减少UI界面卡顿的隐患。页面之间的参数传递不要用全局变量存文件路径,小程序在iOS上会回收不活跃页面的内存,全局变量一丢,文件就找不回来了。正确做法是上传页拿到路径后,立即把文件上传到PHP后端,页面只保存服务器返回的file_id。
页面结构上,每个页面都有一个独立的onLoad方法拉取数据,这看起来重复,但好处是逻辑清晰。首页拉取价格配置和打印机状态,确认页拉取订单详情和支付参数,订单列表页分页加载历史订单。UI层和后端接口的映射文件我习惯放在utils/api.js里,统一管理路径,否则后端改了URL你要挨个页面去找。
所谓“全新UI”的落地,重点不在于把颜色换新,而是把打印流程里的反馈状态做清楚。比如上传成功后,按钮变成可点击的绿色,而不是只弹一个toast;订单列表里每个状态都有色块区分。这些反馈全由后端返回的状态值驱动,前端不能自己猜。
3.2 封装request请求和状态码拦截,别让登录失效把整个页面拖崩
小程序原生的wx.request不支持Promise,直接写在页面里会出现“回调地狱”,而且没法统一处理token过期。我的做法是封装一个request函数,所有页面都从这里面调用。返回结构统一为{code, msg, data},code为0是成功,401是登录失效,403是无权限。小程序拿到401后不再跳页面,而是清掉本地token,直接跳登录页,然后返回用户刚才想进的页面。
下面是一段可以直接放进utils/request.js的封装代码:
const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token'); wx.request({ url: getApp().globalData.baseUrl + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, success: (res) => { if (res.data.code === 401) { wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(res.data); } else if (res.data.code === 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail: (err) => { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); }; module.exports = { request };这段代码的逻辑是:每个请求都带上本地存的token;后端只要返回code=401,说明token无效或过期,前端立刻清掉旧token并且不继续执行业务逻辑,直接跳登录。整体用Promise封装,页面里可以await this.$request('/api/order/list')这样写,不用再嵌套回调。
参数说明里有个容易被忽略的点:getApp().globalData.baseUrl在开发环境写http://127.0.0.1/,上线前必须改成HTTPS域名,并且要在小程序后台配置合法域名。wx.request的header里不要写死Content-Type为application/json,因为上传文件时表单类型不同,统一封装后,上传接口需要单独设置header。还有一点:fail分支里的“网络异常”提示,在真正上线时建议区分超时和断网,比如用err.errMsg里是否包含timeout来判断。
3.3 上传文件和打印参数一起提交:wx.uploadFile的formData用法
上传文件是自助打印系统里最容易出bug的环节。很多人先wx.uploadFile传文件,再调接口传打印参数,结果订单里文件有路径,参数却丢了,查半天发现是第二个请求还没有回来,订单已经创建了。正确做法是把文件路径和订单参数放进同一次wx.uploadFile请求里,PHP后端一次性接收文件和表单字段,创建订单。
下面是一段上传文件到后端的示例代码:
const uploadFile = (filePath, orderParams) => { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token'); wx.uploadFile({ url: getApp().globalData.baseUrl + '/api/order/upload', filePath: filePath, name: 'file', formData: Object.assign({ token: token }, orderParams), success: (res) => { const data = JSON.parse(res.data); if (data.code === 0) { resolve(data.data); } else { wx.showToast({ title: data.msg, icon: 'none' }); reject(data); } }, fail: (err) => { wx.showToast({ title: '上传失败', icon: 'none' }); reject(err); } }); }); };这里的关键是filePath直接传小程序拍照或从相册选出来的本地路径,name: 'file'要跟PHP端的接收字段名保持一致。formData里除了token,还要带上printer_id、pages、copies、color_type、paper_size这些订单参数。PHP端在接收文件时,同时从$_POST里读这些参数,一个请求就能完成“文件入库+订单创建”。
参数说明时记得:wx.uploadFile的返回值res.data在开发者工具里是字符串,不是对象,所以要先JSON.parse。后端接口接收的$_FILES['file']里会有error字段,PHP端必须先检查这个字段,再判断移动文件到业务目录。还有一个容易踩的坑是orderParams里不能放嵌套数组,formData不支持复杂类型,所有参数都应该是字符串或数字;如果确实要传多个文件,就循环调用上传接口,而不是塞一个数组进去。
4. 本地跑通和服务器部署:PHP环境参数、Nginx伪静态与上传目录权限
拿到源码以后,第一步不是改代码,而是把环境跑通。这一章我从本地开发环境说到服务器部署,重点解决三件事:PHP版本和扩展匹配、Nginx伪静态和PHP-FPM的边界、上传目录的安全权限。这三件事任何一个没做对,教程里再详细也救不了你。
4.1 从源码压缩包到本地跑通,先看PHP版本和扩展
自助打印系统的PHP后端多半跑在Laravel或ThinkPHP这类框架上,也有可能是一个结构简单的原生PHP项目。不管哪种,你拿到压缩包后第一件事是看composer.json或者入口文件。Laravel项目要PHP 8.0以上,ThinkPHP 6也要求7.4以上;如果代码用了match或者构造函数属性提升,那PHP版本至少在8.0。用命令行执行php -v确认版本后,再执行php -m看扩展。
常见的依赖扩展是pdo_mysql、openssl、mbstring、gd、curl、fileinfo。其中fileinfo最容易被忽略,上传文件时识别MIME类型会用到它;gd在很多打印项目里用来生成缩略图或水印。缺扩展会直接导致接口报“Call to undefined function”,这种错误在浏览器里看到时很难第一时间想到是扩展缺失。
本地环境我一般直接用php -S 127.0.0.1:8000起个内置服务器先跑通控制器逻辑。框架的路由要单独配置,Laravel的入口指向public/index.php,所以命令是php -S 127.0.0.1:8000 -t public。这一步不追求快,而是最快暴露PHP版本和扩展问题。本地跑通了再上服务器,能减少一半的部署时间。
注意:内置服务器只用于开发调试,不要直接用线上流量。运行起来之后先访问健康检查接口,确认数据库连接、缓存连接都没问题再继续。
4.2 Nginx伪静态与PHP-FPM的边界配置,接口404和502都出在这里
部署到Linux服务器上,常见组合是Nginx + PHP-FPM。接口404不是PHP代码问题,多半是伪静态没配置好;接口502则是Nginx连不上PHP-FPM,或者fastcgi_pass地址写错了。这两类错误是自助打印部署教程里最常提到的问题,但很少有人把两者分开讲。
Nginx配置里,一个最小可用的站点配置是这样:
server { listen 80; server_name your-domain.com; root /var/www/print/public; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/run/php/php8.1-fpm.sock; } location ~* \.(jpg|jpeg|png|gif|pdf)$ { expires 30d; } }这里的try_files是关键,改成try_files $uri $uri/ /index.php?$query_string后,所有不存在的路径都会交给前端控制器,PHP框架才能解析路由。很多教程只写前端分离的客户端路由,导致服务端发来的请求直接404。fastcgi_pass用Unix socket还是TCP? 在同一台机器上建议用socket,延迟更低;如果在Docker里跨容器访问,才改用127.0.0.1:9000。
还有一个参数位最容易忽视:fastcgi_param SCRIPT_FILENAME。如果写成$document_root$fastcgi_script_name而项目根路径不对,就会出现“File not found”的提示,实际是路径拼接错了。上传大文件时,还要在http段或server段加client_max_body_size 20m;,否则传PDF超过默认1M就直接413。
4.3 上传目录的安全边界:让PHP跑在它该跑的位置
自助打印系统最危险的地方不是支付,而是上传目录。如果用户传上去的文件能被当作PHP脚本执行,攻击者上传一个shell.php,服务器就沦陷了。这是这类源码包里最常见的安全漏洞。解决思路不是让代码“防御”,而是从目录层面把上传目录和PHP执行目录隔离。
首先,上传目录放在public/uploads下,访问权限正常开放给nginx;然后在这个目录的Nginx配置里显式禁止执行PHP脚本:
location ^~ /uploads/ { location ~ \.php$ { deny all; } }这段配置的意思是:所有/uploads/开头的请求都会先命中这个location,里面如果包含.php则直接拒绝,返回403。deny all不会影响正常图片下载,因为图片不经过PHP解释器。接下来再配合操作系统的目录权限,让运行PHP的用户只能写这个目录,不能写其他业务目录。chown时把uploads的owner设为www-data,其他目录的owner设为项目管理员,这样即使上传的PHP脚本没有被Nginx拦截,也会因为没有写权限而无法产生后续文件。
数据库连接配置里也要注意,不要把数据库密码写在代码里然后提交到公开仓库。习惯是放到.env文件,并在Nginx的location配置里把.env访问挡掉。我的习惯是额外在Nginx加一条规则:location ~ /\.env { deny all; }。因为很多源码包把配置写在根目录,一旦被访问,数据库密码直接裸奔。
5. 自助图文打印系统避坑记录:UI卡顿、跨域和支付回调的五个踩坑现场
这一章写的是我实际会踩到的坑,也是自助图文打印系统最常出问题的五个位置。每条按现象、原因、解决思路来写。看完你会觉得很多“玄学”问题,其实都是配置和边界没处理好。
5.1 UI界面卡顿不是手机问题,是列表没有分页和懒加载
现象:小程序在订单列表页不断往下滑,过了五十条以后明显变卡,有些安卓机上甚至白屏。原因:订单列表一次性把后端返回的所有数据全渲染出来,没有分页和懒加载。自助打印系统的订单往往包含图片路径、文件大小、状态标签,一个订单就是一个复杂组件,渲染几十个DOM节点还能撑住,到了几百个就一定卡。
解决:后端接口改成分页返回,默认一页20条;小程序端用“上拉触底加载”的方式追加下一页。数据量再大时,订单列表里的文件缩略图不要用原图,后端在生成订单时或访问时压缩一张标准图。UI层只负责把has_more字段作为是否还能继续翻页的依据,避免请求第100页还在返回数据。
后端分页接口一般返回{list, page, has_more}。我用过最省事的做法是前端定义一个page变量,每次请求成功后自增,同时把返回值里的list用concat追加到现有数组。不要用setData直接改整个数组,那样会触发整个列表重新渲染;要setData({ 'list[0].status': 5 })这种路径写法,只更新变化的那一项。
5.2 小程序请求接口报错,跨域和合法域名是两件事
现象:开发时PHP接口用http://127.0.0.1:8000访问正常,小程序一请求就报“url not in domain list”或者直接fail。原因:这是小程序平台的合法域名校验,不是跨域问题。很多人拿到报错就去后端加跨域头,其实小程序不存在浏览器跨域限制,是微信客户端强制要求请求域名必须在小程序后台白名单里。
解决:开发阶段在开发者工具里勾选“不校验合法域名”,本地跑通业务;上线前一定要把PHP后端部署到HTTPS域名,并到微信公众平台配置“request合法域名”和“uploadFile合法域名”。如果后端还有Web管理后台访问,那才是真正的跨域,PHP里加几行响应头就行,但小程序这个场景不要混淆。
还有一点:wx.request的url必须是完整的https://协议,不能只写/api/xxx。PHP端如果部署了强制跳转HTTPS,要确认静态资源和接口都在HTTPS下没有混合内容。我第一次交付时就是漏掉了合法域名配置,客户真机上扫码一直转圈,小程序后台数据包能看到请求全部失败,最后查了半天才发现是白名单没加。
5.3 支付异步回调收不到:先查服务器端口和验签顺序
现象:微信支付下单成功,用户的钱扣了,后台订单状态还停在“待支付”,只能在数据库里手工改状态。原因:支付回调URL被防火墙挡了,或者异步通知里的验签逻辑写错,微信重试几次后放弃。更隐蔽的情况是教程里的回调地址写的是http://ip:8080/notify,没有域名没有证书,微信服务器请求过不去。
解决:回调地址必须是外网能访问的HTTPS地址,且不能放在登录态保护的路由里。微信支付的回调通知不是用户发起的,不会有token,后端收到通知后先拿签名做校验,校验通过再查订单、改状态。排查时先看Nginx访问日志,有没有来自微信支付IP的POST请求;如果有,再看PHP日志,是不是验签失败。
验签顺序也经常写反。正确做法是:先商户平台下载微信支付API证书,用证书里的公钥验签,验签通过后再更新订单。如果先用订单号去查单,再验签,单子没查到就会直接返回“失败”,微信会继续重试,重试期间订单其实已经在库里了。另外,回调处理完以后要返回{"code":"SUCCESS"}给微信,不要返回空字符串;返回值格式错了,微信同样会重试。
5.4 打印机连接超时:串口、HTTP、队列三处超时要分开调
现象:一台打印机明明是通电的,上传文件以后界面一直转圈,后台看任务队列一直卡在“打印中”。原因:打印任务下发时只设了一个笼统的超时时间,打印机响应慢一点就断开了;或者打印机走的是局域网HTTP接口,但PHP进程没有连上打印机的网络。自助打印系统里,打印机的连接方式有串口、USB转网络、HTTP接口,三者的超时行为完全不同。
解决:串口打印要按波特率和数据位配置,不要用默认值;网络打印要区分“连接超时”和“发送超时”,前者设2秒,后者设10秒;HTTP接口要确认打印机的IP和端口在PHP服务器上能连通,不能只在小程序端测通。我一般会在打印队列里加一个“尝试次数”字段,失败一次就在任务表里标记一次,超过三次再进入人工处理,而不是让任务无限卡住。
排查时用一句telnet 192.168.1.200 9100先从服务器上看端口通不通,再检查PHP的fsockopen超时参数。很多打印机自带的状态查询接口,队列在打印前先轮询一次打印机在线状态,比“发出去失败了再补救”要靠谱得多。
5.5 上传图片时提示文件过大或格式不对,PHP默认参数在偷着拦
现象:用户在小程序里上传一个5MB的PDF,前端已经传完,后端却返回“文件过大”。原因:PHP默认的上传大小是2M,post_max_size和upload_max_filesize都没调,文件到了PHP就被扔掉了。更隐蔽的是max_file_uploads或max_input_time限制,导致多文件上传时只有第一张成功。
解决:修改PHP配置。upload_max_filesize、post_max_size、max_execution_time、max_input_time这四个参数要配套调。post_max_size要大于upload_max_filesize,否则传一个刚好接近上限的文件会被当成POST数据超限拦下。改完配置后重启PHP-FPM,再用phpinfo()确认参数生效。
格式不对也是同一类的坑。有的打印机不识别PDF,需要后端装Ghostscript把PDF转成图片;有的教程让你直接传PDF,打印店里却出乱码。解决方式是后端在订单生成时先解析文件头判断真实格式,前端传参里的扩展名不能信。我这里会先在本地测一遍“5MB的PDF转图片”耗时多少,如果超过10秒,就要给用户一个“正在处理”的反馈,而不是让他傻等。
6. 交付前给项目补上重试和验收清单:打印队列、订单推送和习惯检查
6.1 打印队列失败重试:给每台打印机加一个任务状态机
自助打印上线以后,真正让你睡不好觉的不是订单量,而是“这一单到底打没打”。我的做法是在订单表里增加retry_count和last_try_at字段,定时任务扫描状态为“待打印”且last_try_at超过两分钟的任务,重新提交到打印机。重试次数超过三次后,任务变成“需人工”,并给店长发一条模板消息,而不是无限重试。
重试代码要注意幂等:同一个order_no不能因为重试被打印两次。解决办法是在打印机返回“已接收”后立刻把订单状态改成“打印中”,重试任务只认“待打印”状态。这个小改动比任何高级队列都管用。
6.2 客户验收清单:教程写不出但能少吵架的交付物
交付源码时,我给客户写一份清单:浏览器能访问后台吗?小程序真机上下单后支付回调是否更新状态?打印队列失败重试是否生效?上传目录是否禁止PHP执行?每个勾选都有对应操作步骤。教程教人跑通,清单教人验收,前者不一定覆盖边界,后者才是交付的安全网。
6.3 结尾技巧:习惯性查看PHP错误日志,别盯着屏幕“看起来正常”
我自己的习惯是部署完毕后先看一期PHP错误日志,再关掉日志,而不是只在页面上看“200 OK”。PHP的display_errors在线上要关掉,但error_log要开着,否则很多接口失败只显示500,你根本不知道是哪个文件哪一行报错。花了最多时间的调试,往往都是日志没看。
这套系统做下来,我最大的教训是:UI界面卡顿、支付回调、上传限制这些“小问题”,比业务逻辑更消耗交付时间。把状态机、超时和错误日志这三样东西提前安排好,后面真的能少去很多次现场。希望帮到你。
本文还有配套的精品资源,点击获取