简介面向宿舍与公共场所扫码打印场景的智慧云打印微信小程序完整源码包适合小程序开发者、云打印服务研究者参考解决免安装、扫码即打的轻量打印需求。压缩包共796个文件约5.13MB主要包含js逻辑、vue页面、wxml模板、wxss样式及json配置等前端资源文件类型覆盖194个png图片、165个js脚本、143个vue组件目录层级清晰便于定位打印任务、设备管理与订单模块。内容涵盖用户上传文件、云端任务分发、扫码绑定设备等关键流程并配有iconfont等静态资源与多场景页面骨架预览、费用计算、订单管理等功能均可在源码中对应梳理适合基于微信开发者工具二次开发与功能扩展。已有827人学习下载。1. 微信小程序智慧云打印宿舍下单、现场扫码取件的同一套任务流“微信小程序智慧云打印-包含微信版本(宿舍)扫码设备操作(现场).zip”这个工程名把宿舍和现场两个使用场景放到了一起宿舍里远程提交打印任务现场扫码设备完成确认和取件。智慧云打印的核心不是给打印机加一个微信入口而是打通“用户—任务—设备”三条线文件上传、云端排队、设备绑定、状态回写每一步都依赖状态机而不是靠人盯。这篇文章适合两类读者用云开发快速跑通小程序打印原型的学生或独立开发者以及给校内打印店、共享打印机做方案的后端工程师。整套方案不依赖专用硬件普通打印机加树莓派或扫码枪也能搭。2. 云打印系统拆解任务队列、设备绑定与状态机2.1 为什么打印任务不能从小程序直连打印机第一个要破除的直觉是“小程序调用打印机 API”。很多打印机确实支持网络指令但小程序的运行环境限制很死wx.request 只能请求 HTTPS 域名且域名需要在小程序后台备案并配置而宿舍里的打印机通常挂在 WiFi 下没有公网 IP也没有合法 TLS 证书。即使打印机支持局域网 IP小程序拿到的是经过微信转发的请求无法直连内网地址。所以智慧云打印必须有一个服务端作为中转站。小程序把文件上传到云存储提交一个任务服务端把任务塞进队列根据设备号把任务推给打印机端打印机端完成后回调状态。这样打印机端不需要公网地址只需要能主动访问服务端即可。这个模型也顺便解决了鉴权和计费谁提交的任务、要不要扣费都在服务端统一判断而不是信任打印机本地的黑名单。2.2 任务状态机与字段设计任务从提交到完成至少要经历这几个状态CREATED用户提交、UPLOADED文件上传完成、QUEUED进入打印队列、PRINTING设备正在打印、COMPLETED打印完成、FAILED打印失败、CANCELED用户取消。字段名类型说明taskIdstring全局唯一任务号建议用日期随机数生成方便扫码时短链接传输userIdstring微信小程序的 openid 或业务侧用户 IDdeviceIdstring现场扫码后从 scene 参数解析出的设备编码fileTypestringpdf、docx、xlsx、jpg 等决定打印端用哪个解析流程statusstring任务状态机中的值paidboolean是否已支付宿舍版常见用积分或预付卡createTimenumber任务创建时间戳用于超时清理我一般会把 deviceId 和打印任务解耦设备表只记录设备物理位置和状态任务表只关联 deviceId不存具体 IP。这样打印机端即使换了网络任务依然能通过设备编码找到它。状态更新用时间戳对比而不是直接覆盖能减少并发回调时的脏数据。2.3 用云开发还是自建服务如果只是跑通宿舍场景微信云开发是最快路径。云开发自带云存储、云函数和数据库小程序端免鉴权拿 openid不需要自己搭 Nginx 和 MySQL。缺点是云函数冷启动延迟高高峰期打印任务调度如果全走云函数可能出现 1 秒以上的等待。自建 HTTP 服务更适合打印店现场多台设备并发拉任务的情况可以部署在校园网内的一台 Linux 机器上设备轮询不用经过公网。缺点是要自己处理小程序登录态、HTTPS 证书和备案。标题里的工程如果以“包含微信版本”和“扫码设备操作”为划分我建议核心调度用云开发打印设备端直接对接云函数等到设备数量超过 20 台再迁到自建服务。2.4 最小服务端实现云函数创建打印任务用云开发时创建任务最直接的做法是前端先把文件传到云存储拿到 fileID 后调用云函数。下面是一个 createPrintTask 云函数的精简实现// cloudfunctions/createPrintTask/index.js const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db cloud.database() exports.main async (event) { const { OPENID } cloud.getWXContext() const { fileID, fileName, deviceId, copies, duplex } event if (!fileID || !fileName || !deviceId) { return { code: 400, msg: 缺少必要参数 } } const taskId P Date.now() Math.floor(Math.random() * 1000) const task { taskId, userId: OPENID, deviceId, fileName, fileID, copies: Math.min(Math.max(parseInt(copies) || 1, 1), 99), duplex: !!duplex, status: QUEUED, paid: false, createTime: Date.now(), updateTime: Date.now() } await db.collection(print_tasks).add({ data: task }) return { code: 0, data: { taskId, status: task.status } } }这段代码做的事情是从微信上下文取 openid 作为 userId校验 fileID、fileName、deviceId 三个必填参数生成 taskId把 copies 限制在 1 到 99 之间duplex 转成布尔值最后把任务写入 print_tasks 集合。注意这里把设备号 deviceId 放在创建任务时传进来。宿舍版用户在首页可能没有扫码我们需要先在“选择打印机”页面扫设备码把 deviceId 暂存到 globalData再调用这个云函数。不要把 deviceId 放在前端随便填否则别人可以把自己的打印任务挂到任意打印机上现场会乱套。3. 宿舍场景的微信小程序打印端上传文件、设置参数与实时状态3.1 原生微信小程序还是 uni-app如果只做微信端我建议直接用原生小程序语法因为云开发的 wx.cloud API 在原生端支持最完整调试也最直接。uni-app 的好处是以后想复用同一套代码出 H5 或 App但云开发的能力在 uni-app 里需要额外封装 uniCloud而且第三方组件在真机上踩的坑比原生多。工程名里写的是“微信版本(宿舍)”说明宿舍端只需要微信小程序不需要考虑多端。这个前提下用原生 wx.chooseMessageFile 就能调起微信的聊天文件选择器不需要额外依赖。3.2 上传文件到云存储并创建任务宿舍用户的操作流一般是进入小程序 - 选择打印机 - 选择文件 - 设置份数和单双面 - 提交。上传和创建任务最好是两步避免一个请求里既有大文件上传又有数据库写入导致中途失败后状态不一致。// pages/print/print.js async function onChooseFile() { const res await wx.chooseMessageFile({ count: 1, type: file, extension: [pdf, doc, docx, xlsx, jpg, png] }) const file res.tempFiles[0] const cloudPath print/${Date.now()}_${file.name} const uploadRes await wx.cloud.uploadFile({ cloudPath, filePath: file.path }) this.setData({ fileID: uploadRes.fileID, fileName: file.name }) } async function onSubmit(e) { const { fileID, fileName } this.data const deviceId getApp().globalData.deviceId const { copies, duplex, paperSize } e.detail.value const res await wx.cloud.callFunction({ name: createPrintTask, data: { fileID, fileName, deviceId, copies, duplex, paperSize } }) wx.showToast({ title: 已提交, icon: success }) }这个片段先用 wx.chooseMessageFile 让用户从聊天记录或收藏里选文件限制单次一个文件避免上传体积过大。上传时 cloudPath 加时间戳前缀防止同名文件互相覆盖。提交时从 globalData 取 deviceId这是之前扫码或手动选择打印机时写入的全局值。参数说明copies 直接透传给云函数由云函数做范围限制duplex 是布尔值对应是否双面打印paperSize 传字符串比如 A4 或 A5。这里要注意微信开发者工具里选择 .docx 文件经常拿不到扩展名最好在 chooseMessageFile 的 extension 里明确写上类型否则后续打印端解析文件格式时只能靠 content-type 猜测。3.3 打印参数模板纸张、份数与单双面宿舍打印没有打印店那么复杂但参数太少会出现“用户以为选了 A3实际出来是默认 A4”的投诉。我建议至少保留下面四个可调参数并且每个参数都有默认值。参数取值示例默认值说明paperSizeA4 / A3 / B5A4打印纸尺寸设备端做尺寸映射copies1 - 991份数放云函数校验duplextrue / falsefalse单双面双面打印可能涉及长边翻转colorModecolor / graygray宿舍打印默认灰色价格低参数 UI 建议用 form 表单加 picker 实现不要在 setData 里塞太多嵌套对象。我见过很多宿舍小程序把整个表单对象一次性传到云函数后续打印端解析失败时很难定位是哪个字段丢了。更好的做法是提交前在页面里做一层白名单过滤只保留表里这四项。3.4 任务状态实时刷新任务创建后宿舍用户希望在小程序里看到“排队中/打印中/已完成”。最简单的方式是定时轮询云函数2 到 3 秒请求一次。轮询的函数如下// cloudfunctions/getPrintTask/index.js exports.main async (event) { const { OPENID } cloud.getWXContext() const { taskId } event const db cloud.database() const res await db.collection(print_tasks) .where({ taskId, userId: OPENID }) .limit(1) .get() if (!res.data.length) return { code: 404, msg: 任务不存在 } return { code: 0, data: res.data[0] } }小程序端用 setInterval 每 3 秒拉一次拿到 status 后更新页面状态条。注意轮询时要把 taskId 作为参数传过去并且云函数里必须带上 userId: OPENID 条件防止别人通过遍历 taskId 查到其他人的打印任务。轮询的缺点是比较费云函数资源如果同时在线人数超过 100建议改为订阅消息任务完成时云函数给用户发一条微信订阅消息用户不用一直盯着页面。大学生宿舍场景里大家更愿意把手机丢在一边做别的事收到通知再下楼取件所以我在宿舍版本里通常只保留“提交成功”和“完成通知”两次订阅消息授权。4. 现场扫码设备操作一码绑定设备、免登录提交打印4.1 二维码内容设计scene 绑定设备号现场环节要解决的是“用户到了打印机前怎么让小程序知道要用的设备”。常见做法是在每台设备上贴一个小程序码用户扫一扫直接打开小程序。但这个码不能是普通的小程序码因为打开后小程序根本不知道用户扫的是哪台机器。正确做法是用微信的 getUnlimited 接口生成带 scene 参数的小程序码。scene 是短字符串最大 32 个字符一般把 deviceId 放进去。比如 deviceId 是 D1001scene 就设为 deviceD1001。如果你还想在扫码后自动填入上一次选好的文件也可以把 taskId 一起拼接但要控制在 32 字符内所以设备号不能太长。参数示例说明scenedeviceD1001小程序码携带的设备标识32 字符内output小程序码图片 BuffergetUnlimited 返回的图片二进制env_versionrelease生成正式版小程序码体验版用 trial设置 env_version 为 release 时生成的是正式版小程序码体验版和开发版二维码只能在开发调试阶段使用。现场贴码前一定要先发布小程序再拿正式码去打印否则用户扫出来会提示“无法打开”。4.2 小程序端解析 scene 并绑定设备用户扫码后小程序的 onLoad 里能拿到 options.scene。scene 是经过 URL encode 的字符串需要 decodeURIComponent 解开再解析出 deviceId。// app.js 或首页 onLoad onLoad(options) { if (options.scene) { const scene decodeURIComponent(options.scene) const params new URLSearchParams(scene) const deviceId params.get(device) getApp().globalData.deviceId deviceId wx.setStorageSync(deviceId, deviceId) wx.showToast({ title: 已绑定${deviceId}, icon: none }) } }这里把 deviceId 同时写进 globalData 和本地缓存是为了用户离开小程序再回来时不需要重新扫码。注意 URLSearchParams 在微信基础库中可以直接用但在低版本 Android 上有兼容问题保险起见可以用 split 手动拆。拿到 deviceId 之后还要做一次设备校验。因为 scene 参数可以被伪造小程序端不能直接信任 options.scene应该调用一个 bindDevice 云函数把 deviceId 和当前 openid 绑定并返回设备是否在线。如果设备不在线前端弹提示并阻止提交任务。4.3 设备端如何拉取任务并回写状态现场扫码设备我理解有两类一类是带屏幕的智能打印终端另一类是普通打印机加一个扫码枪或树莓派。后者的实现更通用也是智慧云打印落地的典型方案。设备端需要定时从服务端拉取本设备待打印的任务。下面是树莓派上 Python 脚本的简化逻辑# device_poller.py import requests, time BASE_URL https://your-domain.com/api def fetch_tasks(device_id, token): resp requests.get( f{BASE_URL}/device/tasks, params{deviceId: device_id, token: token}, timeout5 ) return resp.json().get(tasks, []) def update_status(task_id, status): requests.post(f{BASE_URL}/device/tasks/{task_id}/status, json{status: status}) while True: tasks fetch_tasks(D1001, DEVICE_TOKEN) for task in tasks: update_status(task[taskId], PRINTING) print_file(task[fileID], task[copies], task[duplex]) update_status(task[taskId], COMPLETED) time.sleep(2)设备端用 deviceToken 而不是普通用户身份token 在设备第一次注册时写入服务端校验 token 和设备 ID 必须匹配。每次轮询拉回的任务按 createTime 排序先提交的先打印。这里有一个关键点设备端拿到任务后要立刻把状态改成 PRINTING否则多台设备同时轮询时会重复取到同一个任务。改成 PRINTING 后再开始打印即使打印中途断电重启后也能根据状态恢复而不会重复打印。4.4 扫码失败与多设备共存的边界现场最容易出现的问题是用户扫到旧码、设备搬迁后 deviceId 没有更新。我一般会在 bindDevice 接口里返回设备的最新位置和状态小程序端展示“设备3 号打印机在线”。如果扫码后提示设备不存在大概率是二维码贴错或者数据库被清过这时候要允许用户手动输入设备编号不能只依赖扫码。现场可能还有多人同时扫码的情况。判断依据用 taskId 而不是用户 openid因为一个用户可能连续打印多个文件而一个任务只能对应一条打印记录。服务端在任务表里加一个 picked 字段设备轮询时用原子更新把 CREATED 改成 PRINTING才能避免同一个任务被两台设备抢走。5. 从加载页到真实打印机调参、抓包与上线避坑5.1 修改刚进入的加载页面减少白屏和参数丢失用户扫小程序码后微信先加载小程序的启动页如果启动页里没有处理 scene 参数会出现“闪一下首页”再跳转的情况。我常用的做法是在 app.js 的 onLaunch 里先不渲染主页面用 wx.showLoading 挡住界面等解析完 scene 后再切到打印页。// app.js onLaunch(options) { wx.showLoading({ title: 加载中, mask: true }) this.globalData.scene options.scene ? decodeURIComponent(options.scene) : } // 首页 onShow onShow() { if (getApp().globalData.scene) { this.handleScene(getApp().globalData.scene) } wx.hideLoading() }场景参数必须在 onLaunch 里第一时间缓存因为 onLoad 只能在页面加载时拿到一次用户从后台切换回来不会再触发。加载页的 title 可以改成“正在识别打印机”比默认的“加载中”更符合现场预期。5.2 用 Charles 抓电脑端微信小程序的请求小程序真机调试时看不到完整的网络日志我习惯用 Charles 抓电脑端微信的请求。先在电脑端微信左下角设置里打开调试模式把微信进程退出再重进然后在开发者工具里用预览码唤起电脑微信Charles 就能看到所有 HTTPS 请求。在 Charles 的 SSL 解密配置里加入 servicewechat.com 和你的云开发环境域名证书安装到系统根凭据后重启微信。抓到请求之后重点看 createPrintTask 返回的 taskId 和设备端回写的状态 URL很多“任务已提交但打印机不动”的问题就是卡在设备端没有把状态从 QUEUED 改成 PRINTING。注意Charles 抓包只能用于本地开发调试不要把证书或监听配置提交到项目仓库。抓包时如果真机上也装了证书记得在调试结束后移除否则会影响日常访问。5.3 顶部导航栏高度与扫码页布局现场扫码进入的小程序往往需要展示一台设备的信息和“开始打印”按钮。微信小程序右上角胶囊按钮在不同机型上高度不同如果页面自定义导航栏按钮很容易被胶囊挡住。const menuRect wx.getMenuButtonBoundingClientRect() Page({ data: { navHeight: menuRect.bottom 8, menuWidth: menuRect.width } })拿到胶囊底部位置后把自定义导航栏的高度设置成 menuRect.bottom 加状态栏高度这样在不同尺寸的安卓和 iOS 设备上右侧按钮都不会和胶囊重叠。宿舍版页面如果不需要自定义导航直接用原生导航栏最省事。5.4 上线前按这张清单过一遍检查项操作常见问题scene 长度生成小程序码时验证超过 32 字符会被截断解析失败云环境 IDapp.js 里 cloud.init 使用正确的 env本地能用真机报 env 错误设备离线时间设备端记录最后上报时间超过 5 分钟显示离线重复打印防护任务状态用原子更新多设备轮询导致同一任务打两次错误码收敛统一返回 code/msg 结构前端无法区分失败原因最后一个容易踩的点是正式版小程序码必须等小程序发布后才会被微信识别所以现场贴码前要确认当前项目已经通过审核并发布而不是还停留在体验版一旦发布后改了 scene 拼接规则旧码要全部替换否则用户扫码会绑定到错误设备上。本文还有配套的精品资源点击获取