ARTICLE DETAIL

资讯详情

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

基于微信小程序与JavaScript的二手书回收系统开发实战

基于微信小程序与JavaScript的二手书回收系统开发实战 简介基于JavaScript与微信小程序开发的二手书回收系统源码包定位于小程序开发与二手交易场景的入门级综合示例面向前端学习者、小程序爱好者以及需要项目参考的高校学生。压缩包约911KB共77个文件资源类型覆盖全面15个JSON配置负责页面路由与基础数据设置13个JavaScript脚本承载用户验证、书籍信息、订单处理等核心逻辑13个JPG和12个PNG图片提供书籍封面与界面图标12个WXSS样式表定义整体视觉风格11个WXML模板搭建列表、详情、购物车、个人中心等页面结构另有1个说明文档便于快速上手。目前已有376人学习下载。这套源码能够帮助读者完整串联小程序端从页面渲染、样式适配到交互反馈的开发流程也可以直接提取其中的二手书回收、分类查询、购物车与下单等功能模块用于二次开发是课程设计、毕业设计或个人练手的实用参考。1. 二手书回收系统的迷你工程为什么它值得你照着做一遍学生在宿舍楼下把一摞考研书扫码下单回收员上门取走管理员在后台完成验收打款。基于JavaScript和微信小程序的二手书回收系统就是把这个业务闭环放进微信生态的完整工程适合做课程设计、毕设也适合给校园书店或社区书站做一个轻量的收书工具。它不是一个展示型小程序而是把授权登录、扫ISBN、回收下单、订单流转、管理端审核串在一起的业务系统。和普通网页相比小程序自带微信登录和消息触达用户不用下载App就能走完整个回收流程对开发者来说原生小程序的工程结构清晰JavaScript作为逻辑层配合WXML/WXSS做表现层一套代码就能覆盖用户端和管理端。适合谁要么是正在选毕设题目的学生要么是手上有书源、想快速上线一个收书入口的运营者。接下来按可复现的方式把这套系统从数据设计到接口联调、上线避坑讲清楚。2. 从业务链路到数据模型先把回收订单状态机钉死2.1 用户端四条主链路登录授权、录入书单、回收下单、订单跟踪做这套系统最忌讳一上来就写页面。我一般先用一张纸把用户端动作画出来。登录链路用户打开小程序先静默wx.login拿到code再在用户点击“我要卖书”时触发授权换取头像昵称。二手书回收相对低频入口别做太重避免首屏就让用户做选择。录入书单分两步走。第一用微信自带的wx.scanCode扫书背面的ISBN条码扫完调后端书目接口把书名、作者、出版社、参考价带出来第二让用户补充书况是否勾画、是否缺页、有无水渍这些字段直接参与估价。回收下单选择预计交书时间填地址或选择上门取件点提交后生成回收单号。订单跟踪用户在小程序里看回收单当前状态待接单、已估价、待取书、验收中、已打款、已取消。这里JavaScript的作用不只是事件绑定。单据上的每一项展示字段几乎都依赖一组转换函数价格格式化、状态码到中文文案的映射、时间戳的本地化展示。这些函数在工程里集中在utils/format.js页面只负责组装数据展示层不做复杂运算。2.2 管理端的三块面板书库审核、订单流转、结算对账管理端是同一个小程序里的独立身份页面通过登录者的openid或后端返回的角色字段区分。管理员进入后台先看三块内容书库管理回收书录入后的待审核列表支持改价、上下架订单管理推进回收单状态结算管理按批次导出打款名单。不建议给这版系统做太重的权限体系。常规做法是在用户表加一个role字段0普通用户、1回收员、2管理员页面按钮根据role显隐。管理端页面不放进tabBar在“我的”页留一个隐藏入口减少普通用户误入。2.3 状态字段与数据表设计回收单状态机与关键字段清单状态机是整个系统的“宪法”。前后端联调前先把下面这张表定下来后端的接口命名、前端的按钮显隐逻辑都从这张表推导。状态码状态名称前端可见动作管理端可执行动作0待接单取消回收单接单、修改估价1已估价确认/放弃回收下发估价2待取书查看上门信息分配回收员3验收中查看验收进度上传验收图片、确认书况4已打款查看收款记录发起打款5已取消重新下单查看取消原因对应的数据表字段在不同项目里写法会有差异但这几个字段是少不了的order_no回收单号建议用年月日加随机数user_id用户IDbook_list存书ID加估价信息用JSON数组status当前状态码status_log记录每一步的操作人、时间和备注。前端不管后端用什么数据库接口返回里带上这五个字段页面就能正常渲染。提示很多基于JavaScript的源码工程会把book_list直接拼成字符串结果订单详情页每次都要先切分再渲染数据一多就出怪问题。第5章会展开讲这类坑。3. 用微信开发者工具跑通最小可运行版本工程结构与三个关键页面3.1 新建工程与原生框架配置先让列表页跑起来在微信开发者工具里新建项目AppID可以先选测试号后端接口未就绪时在“详情-本地设置”勾选“不校验合法域名”。原生小程序工程不需要构建工具目录按约定摆好就能跑miniprogram/ ├── app.js ├── app.json ├── app.wxss ├── pages/ │ ├── index/ │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.js │ ├── recycle/ │ ├── orders/ │ └── mine/ ├── components/ │ └── book-card/ └── utils/ ├── request.js └── format.js先用app.json把页面注册好。这是整个小程序最先被读取的配置文件页面路径、窗口样式、底部导航都在这里声明{ pages: [ pages/index/index, pages/recycle/recycle, pages/orders/orders, pages/mine/mine ], window: { navigationBarTitleText: 二手书回收, navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black }, tabBar: { color: #9b9b9b, selectedColor: #07c160, list: [ { pagePath: pages/index/index, text: 书库 }, { pagePath: pages/orders/orders, text: 订单 }, { pagePath: pages/mine/mine, text: 我的 } ] } }这里navigationBarTextStyle只支持black和white两个值写十六进制色值会编译报错。tabBar里的pagePath必须和pages里注册的路径完全一致顺序或大小写差一个字符工具直接提示找不到页面。3.2 首页书单展示与列表加载更多分页不是拉到底就完事首页按“书库”的逻辑展示可回收书目。最小实现先在onLoad请求第一页滚动到底时继续拉下一页。WXML清单列表这样写view classbook-list block wx:for{{books}} wx:keyid book-card book{{item}} bindtaponBookTap / /block view classload-more wx:if{{hasMore}} text{{isLoading ? 加载中... : 上拉加载更多}}/text /view view classload-end wx:if{{!hasMore books.length 0}} text没有更多了/text /view /viewbook-card是自定义组件需要在页面json里用usingComponents注册。这里不展开组件的内部样式先把index.js的分页逻辑说明白维护books列表、page页码、hasMore是否还有下一页这三个状态每次拉取新数据用concat合并不要整页覆盖。const { get } require(../../utils/request); Page({ data: { books: [], page: 1, pageSize: 10, hasMore: true, isLoading: false, }, onLoad() { this.loadBooks(1); }, onReachBottom() { if (this.data.isLoading || !this.data.hasMore) return; this.loadBooks(this.data.page 1); }, async loadBooks(page) { if (this.data.isLoading) return; this.setData({ isLoading: true }); try { const { list, total } await get(/books, { page, pageSize: this.data.pageSize, }); const books page 1 ? list : this.data.books.concat(list); this.setData({ books, page, hasMore: books.length total, }); } finally { this.setData({ isLoading: false }); } }, });page和hasMore单独管理能避免“上一页请求还没回来下一页已经发出去”的翻车。列表加载更多核心就是三点页码只在接口成功后累加、isLoading防重入、hasMore用后端总数判断。如果后端没返回total一个可行的降级方案是下一页返回条数小于pageSize就置hasMore为false。3.3 回收下单页面与wx.scanCode扫书号从ISBN到估价单回收入口放在书库详情页也可以在“我的”页放一个“我要卖书”按钮。核心动作是扫码// pages/recycle/recycle.js scanBook() { wx.scanCode({ scanType: [barCode], success: async (res) { const isbn res.result.replace(/-/g, ).trim(); wx.showLoading({ title: 查询书目 }); try { const book await get(/books/isbn, { isbn }); const books this.data.books.concat([{ id: book.id, title: book.title, author: book.author, price: book.price, condition: unselected, }]); this.setData({ books }); } finally { wx.hideLoading(); } }, }); }wx.scanCode返回的result同一本书有时是13位纯数字有时带连字符有时混入空格。先做一次清洗再往后端发能省掉大量“明明扫了书却不识别”的反馈。如果后端还没有完整的ISBN库前端可以先把书放进一个“待录入”集合等回收员上门时在管理端补全。估价单上必须让用户填书况这直接决定后续验收阶段的扣款纠纷。一组radio就够radio-group bindchangeonConditionChange label wx:for{{conditions}} wx:key*this radio value{{item.value}} checked{{item.checked}} / text{{item.label}}/text /label /radio-groupconst conditions [ { label: 全新/无笔记, value: new, discount: 0.7 }, { label: 有笔记/无缺页, value: marked, discount: 0.4 }, { label: 缺页/水渍, value: damaged, discount: 0.15 }, ];书况字段在提交时跟着书ID一起进book_list。估价计算最好放在后端前端只用discount做一个预估展示。估价是这套系统的敏感点前端改价很容易被逆向生产环境必须服务端算。3.4 订单详情与状态推进按钮显隐看状态机订单详情页不要做“所有按钮都显示、点完再提示不允许”的交互。按状态机渲染只有状态为0时显示取消按钮状态为1时显示确认和放弃两个按钮。把状态码到按钮配置做成一张映射表const orderActions { 0: [{ label: 取消回收, action: cancel }], 1: [ { label: 确认回收, type: primary, action: accept }, { label: 放弃回收, action: cancel }, ], 2: [{ label: 查看取书信息, action: viewPickup }], 3: [], 4: [{ label: 查看收款记录, action: viewPayment }], 5: [], };不管页面结构多复杂订单页的onLoad里不要直接散落接口调用把数据请求收拢到refreshOrder方法。每次状态推进后调用一次顺便更新状态码和操作按钮页面逻辑会清爽很多。回收单的状态流转几乎都要走后端接口前端让页面扁平化后面接真实接口时会少很多黑匣子问题。4. JavaScript层的数据调度request封装、登录态与接口约定4.1 用Promise封装wx.request别在每个页面重复写回调原生小程序的wx.request是回调式API页面一多每个页面都来一套success/fail代码会变得很难维护。常规做法是包一层request方法把基础路径、token注入、错误提示统一收敛// utils/request.js const BASE_URL http://localhost:3000/api; function request({ url, method GET, data {}, header {} }) { const token wx.getStorageSync(token) || ; return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method, data, header: { content-type: application/json, Authorization: token ? Bearer ${token} : , ...header, }, success(res) { const body res.data || {}; if (res.statusCode 200 res.statusCode 300) { resolve(body.data ! undefined ? body.data : body); return; } if (res.statusCode 401) { wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(new Error(登录已过期)); return; } wx.showToast({ title: body.msg || 请求失败, icon: none }); reject(new Error(body.msg || request failed)); }, fail(err) { wx.showToast({ title: 网络异常请稍后重试, icon: none }); reject(err); }, }); }); } const get (url, data) request({ url, data }); const post (url, data) request({ url, data, method: POST }); module.exports { request, get, post };这个封装解决几个实际问题token注入收敛到一个地方业务错误和网络错误分开页面里只用try/catch就能接住对body.data做了一层兼容不同后端返回结构不统一时页面不用每个字段都去判空。注意wx.request默认不会自动携带cookie小程序登录态约定俗成用header里的token维护。如果后端返回code字段而不是HTTP状态码把success里的判断改成if (body.code 0)即可。两种风格选一种别混着用否则页面里会出现一半判statusCode、一半判code的情况。4.2 登录态与token管理wx.login换openid与授权时机微信登录链路是这样的前端调wx.login拿到一个临时code把code发给后端后端用code调微信接口换取该用户的openid再签发自己的token返回给前端。前端把token存进Storage后续请求带上它。utils/auth.js里做一层封装// utils/auth.js const { post } require(./request); function login() { return new Promise((resolve, reject) { wx.login({ success: async (res) { if (!res.code) return reject(new Error(登录失败)); try { const data await post(/user/login, { code: res.code }); wx.setStorageSync(token, data.token); wx.setStorageSync(userId, data.userId); resolve(data); } catch (err) { reject(err); } }, fail: reject, }); }); } module.exports { login };授权按钮尽量用“用户触发后再弹”的方式不要在onLoad里自动调wx.getUserProfile。微信对头像昵称的获取规则改过好几轮现在更推荐用“头像昵称填写能力”用一个button的open-typechooseAvatar直接选头像昵称用input让用户自己填。依赖旧接口的源码工程如果想上线这一步基本都要补改。4.3 关键接口清单与返回结构约定前后端照着同一张表开发这个系统最少需要这些接口接口方法入参要点返回要点/user/loginPOSTcodetoken、userId/books/listGETpage、pageSize、keywordlist、total/books/isbnGETisbntitle、author、price、id/recycle/createPOSTbookList、condition、pickupTimeorder_no/recycle/statusPUTorder_no、action当前状态/orders/listGETpage、pageSize、statuslist、totalJavaScript里有个容易忽略的环节判断接口返回数据类型。total有时是字符串、有时是数字list偶尔会是null而不是空数组。在request.js里做一层防御有必要但也别过度包装页面里对list做一次Array.isArray判断对total做一次Number()转换就够了。把数据类型判断写进每一个渲染节点反而是过度设计。5. 二手书回收系统的常见坑与排查5条血泪经验5.1 wx.request并发限制导致接口偶发超时现象书库页加载和首页推荐同时发请求偶尔一个请求一直不返回页面白屏过一会儿又全出来了。原因微信小程序对单域名的并发请求数量有限制超出部分会排队。源码工程里如果每个页面各自发请求很容易在onLoad阶段把并发打满。解决给request封装加一个简单的请求队列或者同页面内的多个接口请求做合并。更省事的做法是列表接口加一个版本号参数服务端做短缓存前端只在真正需要时发请求不要每个页面都重复拉同一份书库数据。5.2 自定义导航栏在不同机型上的高度差异现象用了自定义导航栏之后iPhone上按钮跑到了安全区外安卓上标题又偏上偏下。原因顶部导航栏高度包含状态栏高度和胶囊按钮高度不同机型差异很大硬编码一个44px或64px都会翻车。解决用wx.getWindowInfo()拿statusBarHeight用wx.getMenuButtonBoundingClientRect()拿胶囊按钮的位置导航栏高度按胶囊的top height 4来计算。搜索词里的“微信小程序顶部导航栏高度”就是指这一段把所有用到这个值的组件抽成一个公共方法全局统一。5.3 wx.scanCode返回的ISBN格式不统一现象同一本书有的用户扫出来是13位数字有的扫出来带连字符提交后后端说ISBN不存在。原因ISBN条码存在ISBN-10和ISBN-13两种标准部分条码还携带前缀或空格。解决提交前统一做replace(/-/g, )和trim()然后按长度分流10位转13位、13位直接提交。更推荐前端只做清洗把10/13位的判定交给后端估值规则在后端统一管理前端不要写两套ISBN逻辑。5.4 列表加载更多时出现重复数据现象快速上拉列表里出现连续两条相同书目翻页错乱。原因page在接口返回前就被加了onReachBottom在loading未置位前又被触发一次同一个页码请求了两次。解决用this.data.isLoading做防重入判断接口真正成功后再更新page在loadBooks开头加一个if (this.data.isLoading) return。这是列表加载下一页最常见的翻车点翻一次后面所有数据都错位。5.5 setData大列表导致页面卡顿现象书库列表50条数据用户快速滑动时明显卡顿个别低端机直接白屏。原因setData每次更新都会把数据从逻辑层传到渲染层长列表被整包重传开销很大。解决分页数据用concat追加不要整页setData列表项里有图片的话先压缩再入库如果只是某一项的某几个字段变化用精确路径this.setData({ books[2].price: 12 })更新。这一条在基于JavaScript的源码里太容易被忽略但往往决定整个系统流畅度。6. 发布前验证与一个改造技巧从体验版到状态机Store6.1 体验版邀请与真机自测清单在开发者工具界面点“上传”到小程序管理后台把版本设为体验版再把成员加进体验名单就能让同事真机试用。这步比模拟器可靠得多模拟器里正常的授权、扫码、滚动真机上往往会暴露问题。至少让两个人用不同手机完整走一遍“扫码-估价-下单-查看订单”把反馈收回来再改。6.2 审核前检查这类交易系统对类目资质要求比较严个人主体通常过不了审核用企业主体注册更稳妥。提交审核前检查三件事首页不能留测试数据隐私协议里声明手机号、位置、相册权限的用途回收价宣传不要出现“最高”“百分百”这类绝对化用词。6.3 值得立刻做的改造把订单状态机抽成独立Store页面多了以后状态码散落在各个页面是必然的坑。把“状态码-文案-可用操作”收拢到一个模块// utils/order-store.js const STATUS { 0: { label: 待接单, nextActions: [cancel] }, 1: { label: 已估价, nextActions: [accept, cancel] }, 2: { label: 待取书, nextActions: [viewPickup] }, 3: { label: 验收中, nextActions: [] }, 4: { label: 已打款, nextActions: [viewPayment] }, 5: { label: 已取消, nextActions: [] }, }; function getStatusView(status) { return STATUS[status] || { label: 未知状态, nextActions: [] }; } module.exports { getStatusView };改完之后订单列表页和详情页共用同一套映射不会再出现“列表显示已打款、详情页还显示待取书”的前后不一致。同类改造还包括价格格式化、时间格式化都收进utils层让页面组件保持薄。这套改造意识比多写一个页面更值钱。我自己的习惯是把nextActions当作按钮开关来用后端加新状态时前端只补一行配置不改页面逻辑。以前接过一个源码工程状态码散落在三个页面里后期加了一个“退款中”状态我在页面里找了一个下午的魔法数字。把状态机钉死在独立模块里这类问题就绝迹了。希望这些经验能让你在二手书回收系统这条路上少走点弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表