ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

微信小程序摄影师预约系统源码解析:排期与防冲突实战

2026/9/16 21:34:36 拓冰建站 浏览量
微信小程序摄影师预约系统源码解析:排期与防冲突实战 简介面向摄影工作室、独立摄影师及有约拍需求的个人这是一套可直接运行的微信小程序预约拍照项目源码通过摄影师设置可预约档期、客户按日程自由选择时段将预约流程自动化显著提升排期效率、降低沟通成本。压缩包仅9.83MB共602个文件其中包含222个JavaScript脚本、121个WXSS样式表、107个WXML结构文件、95个JSON配置以及PNG/JPG图片、GIF动效、WXS脚本等资源覆盖逻辑交互、页面布局、数据绑定与接口配置等小程序完整开发链路。压缩包内附安装使用手册并配合清晰的目录结构适合快速部署运行也可作为学习微信小程序组件化开发、前后端交互与工具方法封装的实战范本。目前已有438人学习浏览适合具备一定小程序基础、希望参考完整项目源码进行二次开发或毕业设计的开发者。1. 微信小程序摄影师预约排期可见比聊天工具更省沟通成本摄影师约拍最常见的低效点就是“在吗这周六有空吗你那边几点方便”这种来回拉扯。这个基于微信小程序的摄影师预约拍照源码把精力放在两件事上摄影师维护自己的可预约时间客户按日历选档并提交预约而不是在聊天窗口里对表。整个包 600 个文件核心代码集中在 JavaScript、WXML、WXSS外加 JSON 配置适合正处于“从口头约拍转向线上排期”阶段的小型摄影工作室也适合想学习微信小程序预约类业务前后端如何拆分的开发者。它的价值不在于功能多而在于把沟通成本转化成状态判断哪个时段可约、被谁约走、还剩多少空档一目了然。2. 源码结构拆解meet_service 预约服务与 600 个文件的职责边界拿到源码包第一件事不是急着搜 index.js而是先看目录层级。这个包虽然铺了 600 个文件但核心路径其实很清晰业务逻辑在 JavaScript 里页面结构在 WXML样式在 WXSS配置在 JSON。下面按常见打包后的目录结构来说明实际工程中命名可能略有出入但职责对应关系一致。2.1 文件类型与目录分布一个典型的工程拆法如下project/ ├── app.js # 小程序入口逻辑 ├── app.json # 全局配置注册页面与窗口样式 ├── app.wxss # 全局样式变量 ├── utils/ │ ├── meet_service.js # 预约服务层排期、下单、取消 │ ├── page_helper.js # 节流、toast、页面跳转辅助 │ ├── faker_lib.js # 模拟数据生成开发联调用 │ ├── qrcode_lib.js # 预约核销二维码绘制 │ └── wxcharts-min.js # 图表绘制用于统计页 ├── pages/ │ ├── index/ # 首页摄影师列表与作品 │ ├── schedule/ # 排期设置摄影师生效时段维护 │ ├── booking/ # 预约页日历选档 提交 │ └── order/ # 我的预约状态流转与核销 └── assets/ # 静态图片、GIF、示例封面这不是精确路径但一个成熟的小程序预约源码包会做类似分层。文件类型方面直接看扩展名就能判断该改哪里文件类型扩展名在本包中的职责逻辑脚本.js页面事件、预约服务、二维码、图表、模拟数据结构文件.wxml页面节点与数据绑定样式表.wxss预约日历、列表、按钮的布局与状态色配置.json导航栏、页面注册、组件开关文档.md/.docx安装与使用手册另外包里出现 WXS 脚本它能在视图层直接做轻量格式化比如把09:30转成“上午 9:30”避免在 JS 里反复setData。真机上 WXS 的解析速度比同逻辑的 JS 快不少适合用在高频渲染的时段列表上。如果要在真机上跑这套源码记得先确认 app.json 里是否把 schedule、booking、order 这些页面全部注册了。源码包里有些 jpg 是设计稿或预览图会被构建工具当成普通资源打入包体不影响运行但会占用上传体积。建议发布前把 assets 里未引用的图片清理一遍否则小程序主包很容易超过 2MB 限制。2.2 meet_service.js预约状态机的核心封装在项目里我一般会把所有预约操作收敛到一个模块里页面只负责展示。meet_service.js 就扮演这个角色。它内部维护一份“时间片”状态映射暴露给页面的方法通常类似下面这样// utils/meet_service.js 核心接口示意 const TIME_SLOT_SIZE 30; // 每个时间片 30 分钟 const MAX_ADVANCE_DAYS 30; // 最多提前 30 天预约 // 状态枚举0 不可约1 可约2 已选3 已被约 const SLOT_STATUS { DISABLED: 0, AVAILABLE: 1, SELECTED: 2, BOOKED: 3 }; function buildAvailableMap(template, bookedList) { const map new Map(); for (const [date, timeList] of Object.entries(template)) { for (const time of timeList) { const key date time; const status bookedList.includes(key) ? SLOT_STATUS.BOOKED : SLOT_STATUS.AVAILABLE; map.set(key, status); } } return map; } function bookSlot(photographerId, slotKey, customer) { // 先检查本地缓存再走 wx.request 提交 const cachedStatus wx.getStorageSync(slot_ slotKey); if (cachedStatus SLOT_STATUS.BOOKED) { return Promise.reject({ code: 409, msg: slot already taken }); } return new Promise((resolve, reject) { wx.request({ url: https://api.example.com/v1/bookings, method: POST, data: { photographerId, slot: slotKey, customer }, success: (res) { if (res.statusCode 200) { wx.setStorageSync(slot_ slotKey, SLOT_STATUS.BOOKED); resolve(res.data); } else { reject({ code: res.statusCode, msg: res.data.msg }); } }, fail: reject }); }); }为什么用 Map 而不是普通对象因为 slotKey 是字符串日期排序、存在性查询都更直接。TIME_SLOT_SIZE和MAX_ADVANCE_DAYS是业务参数前者决定了一天有多少个候选值后者影响日历能翻多少屏。修改这两个值时要注意模板里的时间必须能被TIME_SLOT_SIZE整除否则会出现“09:15 好约但 09:20 不可约”这种让用户困惑的状态。wx.getStorageSync在这里只做本地临时锁真正防冲突要靠服务端后面会细说。2.3 page_helper.js 与 qrcode_lib.js页面交互与凭证生成page_helper.js 负责跟页面 UI 相关的通用能力。预约页的提交按钮很怕用户连点两下导致重复创建订单所以节流是标配// page_helper.js function throttle(fn, gap 300) { let last 0; return function (...args) { const now Date.now(); if (now - last gap) { last now; return fn.apply(this, args); } }; } // 跳转失败时回退到首页 function safeNavigateTo(url, fallback) { wx.navigateTo({ url, fail: () wx.switchTab({ url: fallback }) }); } module.exports { throttle, safeNavigateTo };gap参数300ms 是常规值但如果预约接口本身要 1 秒以上建议 500ms否则用户快速双击后第三次点击仍会被放行。safeNavigateTo主要处理跳转异常比如从分享卡片进入时页面栈里没有上一页wx.navigateTo会失败此时要降级到switchTab。qrcode_lib.js 用在“已预约”页面生成一单一个的核销二维码。实际使用场景是到店后摄影师扫客户手机上的二维码完成确认而不是口头报手机号。二维码内容建议是appid:orderId:nonce这样的签名串只放 orderId 很容易被仿造。3. 预约日历与时间段选择从服务端日程到 WXML 渲染的状态计算3.1 可预约时间的数据结构设计摄影师设置可预约时间常见两种做法一种是周模板比如固定周一、周三 14:00-18:00另一种是日期粒度直接在某一天勾选时间段。这个源码包的核心诉求是“方便双方安排”因此更常用周模板 日期例外。服务端会下发类似下面的 JSON{ photographerId: pg_1001, weekRules: { 1: [09:00, 09:30, 10:00], 3: [14:00, 14:30], 5: [16:00, 16:30] }, dateExclude: [2025-05-01, 2025-05-02], dateExtra: { 2025-05-10: [10:00, 10:30] } }weekRules的 key 是getDay()的返回值0 是周日1 是周一。dateExclude用于节假日、休息日dateExtra用于临时补开时段。优先判断dateExclude再读weekRules最后叠加dateExtra。如果后端字段还没有统一容易把dateExclude和dateExtra混在一个 map 里前端就要做兼容否则日期状态计算会错位。比如某个摄影师周六固定可约但下周六要参加展会就必须在前端先拿dateExclude把周六过滤掉否则周模板会把它重新点亮。3.2 用状态数组驱动日历与时间段 UI在微信小程序里我习惯把选档页的视图状态收敛成 viewModel 数组避免 WXML 里写复杂逻辑。核心函数是buildSlotList// pages/booking/booking.js const SLOT_STATUS { DISABLED: 0, AVAILABLE: 1, SELECTED: 2, BOOKED: 3 }; function buildSlotList(day, weekRules, excludeList, bookedList) { if (excludeList.includes(day)) return []; const dateObj new Date(day.replace(/-/g, /)); const weekday String(dateObj.getDay()); let timeList weekRules[weekday] || []; // 临时加场 const extra weekRules.dateExtra?.[day]; timeList timeList.concat(extra || []); return timeList.map(time { const fullKey day time; return { time, status: bookedList.includes(fullKey) ? SLOT_STATUS.BOOKED : SLOT_STATUS.AVAILABLE }; }); }提示new Date(day)在 iOS 上解析YYYY-MM-DD不稳定所以习惯用day.replace(/-/g, /)转成YYYY/MM/DD再构造日期。这个坑十次有九次踩在真机预览上。WXML 侧只需要循环渲染view classslot-grid view wx:for{{slots}} wx:keytime classslot {{item.status 1 ? available : }} {{item.status 2 ? selected : }} {{item.status 3 ? booked : }} >function getNextNDays(n) { const days []; const base new Date(); base.setHours(0, 0, 0, 0); for (let i 0; i n; i) { const d new Date(base.getTime() i * 86400000); days.push(formatDate(d)); } return days; } function formatDate(d) { const m String(d.getMonth() 1).padStart(2, 0); const day String(d.getDate()).padStart(2, 0); return d.getFullYear() - m - day; }边界条件按下表来判断场景处理今天已经过了 18:00用户还能约今晚 19:00 吗可以只要时段在当天剩余时间范围内摄影师设置的明天时段跟本地日期不一致用 localeDateString 重新计算日期不要直接拼字符串两个时段重叠14:00 与 14:30时长为 60 分钟的拍摄预约时长应作为独立参数传入不能只看开始时间跨天预约23:30 开始拍到次日 00:30前后两天都要占用提交时传结束时间最后一点是很多人忽略的如果一次拍摄时长超过一个时间片那么 14:00-16:00 的拍摄会占用三个 30 分钟片被占用的中间片也要在bookedList里标记否则用户会发现 15:00 被标成可约但提交时又说没时间。另外weekRules的 key 到底用 0-6 还是 1-7各团队习惯不同。拿到源码后第一件事就是打印new Date().getDay()确认代码里的 1 到底是周一还是周日。我在排错时见过最隐蔽的问题是把周一写成 0结果周日的模板被重复渲染了两次。4. 预约提交与防冲突在微信小程序端做乐观锁和二次确认4.1 提交前校验与本地防重预约页提交不是点了按钮就完事前端第一道拦截能省掉大量无效请求。下面这个校验函数是我一般会放在 booking 页面里的function preCheck(bookingData) { const { selectedDate, selectedTime, phone } bookingData; if (!selectedDate || !selectedTime) { wx.showToast({ title: 请选择时间段, icon: none }); return false; } if (!/^1\d{10}$/.test(phone)) { wx.showToast({ title: 手机号格式不正确, icon: none }); return false; } const slotDate new Date(selectedDate.replace(/-/g, /) selectedTime); const gapMs slotDate.getTime() - Date.now(); if (gapMs 30 * 60 * 1000) { wx.showModal({ title: 距离太近, content: 约拍开始前 30 分钟不能在线预约请直接电话联系摄影师。, showCancel: false }); return false; } return true; }参数说明phone 正则限制了大陆手机号30 分钟这个阈值要根据摄影师到场准备时间调整。如果是外景拍摄摄影师可能要从很远的地方赶过来建议改成 60 分钟以上。gapMs的判断要留 buffer不要按毫秒精算。校验顺序先格式后业务能够尽早结束错误请求避免用户填完日期电话后才发现时间太近。4.2 乐观锁与服务端冲突响应多人同时约同一个时段前端做得再花哨也必须要靠服务端兜底。常见做法是给每个时段加一个版本号或更新时间提交时携带该值。开发中我习惯让服务端返回 409 表示“时段冲突”这样前端可以明确区分网络错误和业务冲突。// utils/meet_service.js function submitBooking(photographerId, slotKey, customer, slotVersion) { return new Promise((resolve, reject) { wx.request({ url: https://api.example.com/v1/bookings, method: POST, data: { photographerId, slot: slotKey, customer, slotVersion }, success(res) { if (res.statusCode 409) { wx.showModal({ title: 手慢了, content: 该时间段刚刚被约走请选其他时间, showCancel: false }); reject(new Error(SLOT_CONFLICT)); return; } if (res.statusCode 200) { resolve(res.data); } else { reject(new Error(SUBMIT_FAIL)); } }, fail: reject }); }); }slotVersion一般由后端生成前端不用关心它的具体值只要在拉取排期时缓存下来提交时原样带回。如果这个源码包还没有接后端只是纯前端演示可以用 storage 模拟乐观锁提交前先wx.getStorageSync(slot_ slotKey)读到已经 BOOKED 就警告否则先写一个临时状态等接口返回成功后再置为 BOOKED。注意storage 模拟乐观锁仅用于纯前端演示真实项目必须由服务端保证并发写入的原子性。4.3 失败后的状态回滚提交失败后最忌讳的是直接把页面留在“已选”状态。用户以为自己约上了结果订单列表里什么都没有。我会在失败回调里做三件事清空当前选中状态、重新拉取该日期的时段列表、把之前的操作按钮恢复可用。function handleSubmitFail() { this.setData({ selectedTime: , selectedStatus: -1 }); this.refreshCurrentDay(); // 重新请求本日排期 this.setData({ submitting: false }); }refreshCurrentDay可以复用排期接口这个接口要支持传入日期参数。如果源码里的页面没有暴露刷新方法至少要在onPullDownRefresh里做相同逻辑让用户手动下拉也能恢复。还有一个容易漏掉的状态预约取消。如果客户在详情页取消预约即使取消了当天该时间片也不能立刻回到可约列表除非摄影师手动确认重新开放。源码里的 meet_service 如果只维护了 BOOKED 状态取消单就会变成“幽灵时段”这一点务必在联调时验证。常见失败原因和处理方式整理如下失败场景前端表现处理时段冲突409 或自定义 error弹窗提示后刷新当日排期网络超时request 无响应显示重试按钮禁止再次提交同一个 slotKey用户切后台后返回页面 resume重新调订单查询接口以服务端状态为准支付中断预约成功后支付失败保留预约单标记待支付不直接删除这些边界不处理上线后每天都会有人来私信问“我明明预约成功了怎么订单没了”。实际排查时先看微信开发者工具的 Network 面板里请求是卡在 pending 还是收到了 500再对应到上面的处理策略。5. 用 faker_lib 生成压测数据再用 wxcharts-min 观察排期利用率5.1 faker_lib 制造真实感数据没有服务端联调时faker_lib 可以帮你模拟一批客户预约行为。它在本包里的存在意义不是造假数据糊弄甲方而是让你在前端开发阶段就看到列表满不满、日期滚动是否流畅。一般可以这样用// 示例生成一周的预约流水 const faker require(../../utils/faker_lib.js); function generateWeekBookings(photographerId, days 7) { const result []; for (let i 0; i days; i) { const date getNextNDays(i)[0]; const times [09:00, 10:30, 14:00, 16:30]; times.forEach((time, idx) { if (Math.random() 0.6) { result.push({ id: ${photographerId}_${date}_${time}, slot: ${date} ${time}, customer: faker.customerName(), status: idx % 3 0 ? CANCELED : CONFIRMED }); } }); } return result; }Math.random()控制占用率想让日历看起来更满就把阈值从 0.6 调到 0.3越高保留的时间越少。status字段里要保留 CANCELED因为被取消的时间片通常是可以重新放开的不能简单从 bookedList 里去除否则会少给摄影师释放档期。5.2 wxcharts-min 画排期利用率柱状图wxcharts-min 是一个轻量图表库在小程序里不需要 canvas 原生组件封装直接传数据就行。观察“哪一天被约得最满”对摄影师调整休息日很有用。示例const wxCharts require(../../utils/wxcharts-min.js); function drawBarChart(canvasId, labels, dataList) { new wxCharts({ canvasId, type: column, categories: labels, series: [{ name: 预约占用率, data: dataList }], yAxis: { format: val val % }, width: 320, height: 200, dataLabel: true }); }yAxis.format改成val val %之后图表纵轴会显示百分比适合看占用率dataLabel为 true 时每个柱子顶部直接显示数值不用再去看刻度。如果数据量超过 7 天建议把 labels 换成MM-DD的缩写否则画出来的柱状图文字会被截断。5.3 验证排期状态回环最后分享一个我用模拟数据验证状态机的技巧把 faker_lib 生成的订单列表直接喂给 meet_service 的buildAvailableMap然后对比页面上的可约时段数量是否等于理论值。理论值 摄影师周模板时段总数 - 已约时段数 - 已取消但未释放的时段数。一旦发现不一致优先检查getBookedSlots里是不是把取消订单也算进去了。这个技巧的代码不用刻意封装线上环境也可以留一个隐藏入口比如连续点击版本号 5 次就拉取当日排期快照把它存到日志里方便事后复盘约拍档期利用率。本文还有配套的精品资源点击获取