简介:面向摄影工作室、独立摄影师及有约拍需求的个人,这是一套可直接运行的微信小程序预约拍照项目源码,通过摄影师设置可预约档期、客户按日程自由选择时段,将预约流程自动化,显著提升排期效率、降低沟通成本。压缩包仅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 class="slot-grid"> <view wx:for="{{slots}}" wx:key="time" class="slot {{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 次就拉取当日排期快照,把它存到日志里,方便事后复盘约拍档期利用率。
本文还有配套的精品资源,点击获取