简介这是一份完整的微信小程序电影购票源码面向有一定前端基础、希望掌握微信小程序完整开发流程的学习者。项目从影院列表、电影排期、选座到在线支付均有实现能帮助读者理解WXML页面结构、WXSS样式布局、JavaScript业务逻辑三大核心模块的配合方式以及如何调用微信API完成登录授权与订单处理。压缩包共92个文件包含14个js逻辑文件、10个wxml页面结构、10个wxss样式、7个json配置文件图标与界面素材为33个jpg和16个png整体仅394KB目录层次清晰易于检索。开源示例还附带名为atakeaway的外卖模块可作为扩展功能参考学习如何在小程序内集成多种服务并管理订单状态。已有6438人学习过这份源码对想要快速上手小程序电商类项目、研究真实业务实现的开发者来说是一份紧凑但完整的实战参考。1. 微信小程序电影购票源码这套代码到手后先看哪一层市面上的“微信小程序源码电影购票”资源很多但能导入开发者工具就跑通的并不算多。这套电影购票小程序源码我拆过之后确认是完整的前后端结构小程序前端负责页面交互Node.js 后端提供影片、场次、座位、订单等接口数据库脚本放在 sql 目录里导入 MySQL、改三个配置项就能把本地购票链路跑通。适合两类人一是拿它当课程设计或毕业设计底子的开发者二是打算快速搭一套区域影院小程序做业务验证的从业者。刚拿到手别急着在开发者工具里点预览先绕到后端把数据库和接口启动起来再回头改前端的接口地址这个顺序最稳。2. 代码包结构拆解前端页面、后端接口与数据库脚本各管哪一段拿到源码包后第一件事不是直接拖进微信开发者工具而是先把包解开看一眼目录。这套电影购票小程序的代码结构是典型的“原生小程序前端 后端服务 数据库脚本”三段式不是云开发那套免后端的写法。它的好处很明显业务逻辑在你自己的服务器上后续接域名、改接口、加功能都灵活坏处是环境配置比云开发多几个步骤需要你先搞定 MySQL 和 Node.js。2.1 小程序端页面文件首页、详情、选座与订单页各自承担什么小程序端用的是原生框架目录结构大致如下project/ ├── pages/ │ ├── index/ 首页正在热映、即将上映两个列表 │ ├── detail/ 电影详情简介、演职员、场次列表 │ ├── seat/ 选座座位图渲染与已售/锁定状态 │ ├── confirm/ 订单确认选座结果、价格计算、优惠码 │ ├── order/ 订单列表与订单详情状态展示、退改入口 │ └── user/ 个人中心登录状态、我的订单入口 ├── components/ │ ├── movie-card/ 影片卡片海报、评分、上映状态 │ └── seat-map/ 座位图组件按场次数据和座位状态渲染 ├── utils/ │ ├── request.js 封装 wx.request统一处理 token 和错误码 │ └── auth.js wx.login 登录和登录态持久化 └── app.jspages/index 是 tabBar 的首页pages/user 是“我的”页其余都是业务子页面通过 wx.navigateTo 跳转。components 里两个自定义组件属于高频复用movie-card 在首页和搜索结果页都会出现seat-map 是选座页的核心后面会单独拆开讲。页面之间传参数我一般建议只传 id比如从 detail 跳到 seat 时带 sessionId不要把整个场次对象带过去。微信小程序的页面路径长度有限对象里夹带数组或长字符串容易出现“路径带参数丢失”的玄学问题传 id 最稳。每个页面对应 .js、.wxml、.wxss、.json 四个文件.json 配导航栏标题和页面背景色.wxml 写视图结构.js 管数据请求和事件处理。这套源码里页面文件命名规整想改成自己的业务直接复制一个页面文件夹再改注册信息就能跑不用动 app.json 里其他页面配置。2.2 后端与数据库Express 路由、MySQL 表设计与种子数据后端单独放在 server 目录常见做法是用 Node.js 的 Express 框架起 HTTP 服务默认监听 3000 端口。路由按业务模块拆分例如// server/routes/index.js const router require(express).Router(); router.use(/api/film, require(./film)); router.use(/api/session, require(./session)); router.use(/api/seat, require(./seat)); router.use(/api/order, require(./order)); router.use(/api/pay, require(./pay)); router.use(/api/user, require(./user)); module.exports router;每个路由文件内部再细分接口比如 /api/film/nowPlaying 和 /api/film/comingSoon、/api/session/list、/api/seat/lock 等。后端不渲染页面只返回 JSON小程序端通过 utils/request.js 统一请求这些地址。数据库脚本在 sql 目录里一般有两份init.sql 负责建库建表seed.sql 负责往库里灌电影、影院、场次和测试座位数据。理解这套代码的关键在表设计核心表大概六张film影片、cinema影院、session场次、seat座位、orders订单、user用户另外还有 banner 或 category 之类的辅助表。orders 表是字段最集中的一张表核心字段如下字段类型说明idint unsigned自增主键order_novarchar(64)业务订单号对账用session_idint场次iduser_idint下单用户idseat_idsvarchar(255)座位id列表逗号拼接total_pricedecimal(10,2)订单总金额单位元statustinyint0待支付 1已支付 2已退款 3已关闭created_atdatetime下单时间seat 表里必须有 session_id 字段因为同一个座位在不同场次下是独立存在的。一个常见错误是座位表做成“一个影厅只有一排座位”结果不同场次共用座位状态A 场有人买了 5 排 3 座B 场也变成已售。正确做法是座位挂在场次下或者挂在“场次 影厅”的组合下两场之间的座位状态互不干扰。2.3 跑通整套代码导入、建库、改配置、启动从零跑通这套代码按下面四步走。第一步初始化数据库mysql -u root -p server/sql/init.sql mysql -u root -p server/sql/seed.sqlinit.sql 会创建 movie_ticket 数据库和上面提到的核心表seed.sql 会写入测试影片、影厅和未来几天的场次数据。如果你的 MySQL 里已有同名数据库init.sql 通常会有 DROP DATABASE IF EXISTS 语句执行前先看清楚别把存量数据冲掉。第二步安装后端依赖并启动cd server npm install npm run devnpm install 会装 Express、MySQL 驱动和相关中间件。npm run dev 走的是开发模式用 nodemon 监听改代码自动重启。启动后后端监听 3000 端口先用浏览器访问 http://127.0.0.1:3000/api/film/nowPlaying 验证接口是否返回 JSON。第三步改数据库和密钥配置// server/config/index.js module.exports { port: 3000, mysql: { host: 127.0.0.1, port: 3306, user: root, password: 你的数据库密码, database: movie_ticket }, appid: 你的小程序appid, secret: 你的小程序secret };数据库名要和 init.sql 里一致user/password 用你本机 MySQL 的真实账号。appid 和 secret 在小程序后台的「开发管理 开发设置」里查申请的是测试号就先填测试号的。第四步在微信开发者工具里导入小程序前端打开微信开发者工具选择「导入项目」目录选中 project 或 mini-program 这一层注意不是选整个源码包的最外层。AppID 填自己注册的测试号或个人号后端地址在 utils/request.js 里改// utils/request.js const BASE_URL http://127.0.0.1:3000;提示开发阶段在开发者工具「详情 本地设置」里勾选「不校验合法域名」本地请求才能发出去。这个选项只对开发工具有效真机预览和线上环境必须用正式 HTTPS 域名否则会出现后文的真机白屏问题。四步走完首页能看到影片列表就算链路通了。列表空白先看后端日志有没有报错再看 seed 是否真正写进了数据。3. 购票主流程影片列表、选座与订单确认是怎么串起来的购票链路从首页点进影片详情再从场次列表进入选座页选完座去订单确认页下单最后进入支付。这条链路的前后端交互集中在三个环节。3.1 首页列表与详情页数据请求、加载态和路由传参首页 index.js 的 onLoad 里拉正在热映列表常见写法是// pages/index/index.js const api require(../../utils/request); Page({ data: { filmList: [], loading: true }, async onLoad() { try { const res await api.request(/api/film/nowPlaying, GET); this.setData({ filmList: res.list, loading: false }); } catch (e) { this.setData({ loading: false }); wx.showToast({ title: 加载失败请检查后端服务, icon: none }); } }, goDetail(e) { const id e.currentTarget.dataset.id; wx.navigateTo({ url: /pages/detail/detail?id${id} }); } });utils/request 封装成 Promise 风格接口统一返回 { code: 0, data: ... } 结构code 非 0 时在 request.js 内部抛错。参数说明onLoad 只在页面首次加载触发从详情页返回首页想刷新数据要在 onShow 里再请求一次loading 字段配合 wxml 上的 wx:if 做加载中提示。detail 页在 onLoad(options) 里通过 options.id 拿电影 id然后并行请求影片详情和场次列表。场次列表不能只返回时间还要带上影厅和剩余座位数因为详情页的场次卡片上要展示这些信息。剩余座位数的计算不要在前端遍历所有座位再数那是拿前端性能开玩笑后端一条 SQL 就能算出来SELECT s.id, s.show_time, s.language, s.hall_name, COUNT(CASE WHEN seat.status IN (1, 2) THEN 1 END) AS sold_count FROM session s LEFT JOIN seat ON seat.session_id s.id WHERE s.film_id 3 GROUP BY s.id;status 为 1 是已售2 是锁定统计它们作为已占座位剩余座位用总座位数减去 sold_count。这一步放后端详情页几乎没有计算压力。3.2 选座组件二维数组、座位状态与锁定后的交互选座页是这套代码里前端交互最重的一块核心是 seat-map 组件。它拿到后端返回的座位列表后把一维数组转成按行分组的二维结构// components/seat-map/index.js buildSeatMap(seatList) { const rowMap {}; seatList.forEach(seat { if (!rowMap[seat.rowNo]) rowMap[seat.rowNo] []; rowMap[seat.rowNo].push(seat); }); const seatMap Object.keys(rowMap) .sort((a, b) a - b) .map(rowNo ({ rowNo, seats: rowMap[rowNo].sort((a, b) a.colNo - b.colNo) })); this.setData({ seatMap }); }座位对象通常包含 id、rowNo、colNo、status 四个字段status 的取值建议约定为0 空座、1 已售、2 锁定、3 当前已选。渲染时不同状态给不同 classview classseat-row wx:for{{seatMap}} wx:keyrowNo view classseat-row__no{{item.rowNo}}/view view wx:for{{item.seats}} wx:keyid classseat {{item.status 1 ? seat--sold : }} {{item.status 2 ? seat--locked : }} {{item.status 3 ? seat--selected : }} bindtaponSeatTap >// pages/confirm/index.js submitOrder() { const sessionId this.data.sessionId; const seatIds this.data.selectedSeats.map(seat seat.id); const totalPrice this.data.totalPrice; if (!seatIds.length) { wx.showToast({ title: 请先选择座位, icon: none }); return; } api.request(/api/order/create, POST, { sessionId, seatIds, totalPrice }) .then(order { wx.navigateTo({ url: /pages/order/order?orderNo${order.orderNo} }); }) .catch(err { wx.showToast({ title: err.message || 下单失败, icon: none }); }); }注意 totalPrice 不要只依赖前端算出来的值前端只做展示。后端要根据 session 表的票价和 seatIds 数量重新计算总价否则改一下请求体就能以任意价格下单。下单接口返回的是订单号不是直接调起支付真正的支付请求在进入 order 详情页后触发。后端 /api/order/create 的具体逻辑放在第 4 章它的职责是同时保证座位不被重复卖、订单号和外部单号一致、状态从待支付开始流转。4. 后端接口与支付回调订单状态机是购票系统的关键前端能跑通只是第一步这套代码能不能真上线要看后端订单状态机写得稳不稳。一次完整购票状态流转是待支付 → 已支付 → 已出票/已消费中间还有锁座、过期释放、退款回到可售。下面三个接口是重头戏。4.1 用户登录wx.login 换 openidtoken 怎么发微信小程序没有传统密码登录标准做法是小程序端 wx.login 拿临时 code后端拿 code 找微信服务端换 openid再发自己的 token 维持登录态。// server/routes/user.js router.post(/api/user/login, async (req, res) { const { code } req.body; const { openid, session_key, errcode } await exchangeCode(code); if (errcode) { return res.json({ code: 40001, msg: 登录失败请重试 }); } const userId await findOrCreateUser(openid); const token jwt.sign({ userId }, config.secret, { expiresIn: 7d }); res.json({ code: 0, data: { token, userInfo: { id: userId, nickname: , avatar: } } }); });参数说明exchangeCode 内部请求微信的 jscode2session 接口入参是 appid、secret 和 code。openid 是用户在当前小程序下的唯一标识session_key 是会话密钥业务上一般不需要长期保存。token 用 JWT 下发有效期 7 天小程序端每次请求把 token 放在 header 的 Authorization 字段里。这套源码里有几个细节值得注意第一没有把 openid 直接当 token 用而是换成了业务自签 token后续登出、续期、踢人都好处理第二用户头像昵称没有从微信资料里硬取需要在小程序端引导用户用 button 的 chooseAvatar 能力自行补全第三JWT 密钥就是 config 里的 secret上线前一定要换成一个足够长的随机字符串。4.2 锁座与下单事务、行级锁与超时释放下单接口简化下来是三件事校验场次有效、把座位从空座改成锁定、生成待支付订单。难点在并发两个用户同时点同一个座位不能都下单成功。START TRANSACTION; SELECT id FROM seat WHERE session_id 8 AND id IN (102, 103) AND status 0 FOR UPDATE; UPDATE seat SET status 2 WHERE session_id 8 AND id IN (102, 103) AND status 0; INSERT INTO orders (order_no, session_id, user_id, seat_ids, total_price, status, created_at) VALUES (MT20250101001, 8, 1, 102,103, 79.80, 0, NOW()); COMMIT;SELECT ... FOR UPDATE 会给命中行加排他锁第二个事务查到同一批座位时会阻塞等第一个事务提交后读到最新状态发现 status 已经变成 2不满足 status 0更新影响行数为 0事务回滚并提示“座位已被占用”。如果把这三条 SQL 拆成三次独立请求又不放在事务里就会复现两边选同一座位都下单成功的超卖问题。实际代码里常用 Node 的 mysql 连接池做事务并在应用层判断 update 影响行数const [result] await pool.execute( UPDATE seat SET status 2 WHERE session_id ? AND id IN (?) AND status 0, [sessionId, seatIds] ); if (result.affectedRows ! seatIds.length) { await conn.rollback(); throw new Error(部分座位已被锁定或售出请重新选座); }affectedRows 等于传入座位数才继续创建订单否则直接回滚。订单号要唯一一般用前缀 日期 随机数生成不要拿自增 id 当订单号否则支付回调对账时容易被猜出订单量。下单成功生成 status 0 的待支付订单座位 status 变成 2锁定。锁定不是永久状态15 分钟没支付要释放。server 里需要有一个定时任务比如每分钟扫一次async function releaseExpiredOrders() { const expiredOrders await pool.query( SELECT id, seat_ids FROM orders WHERE status 0 AND created_at NOW() - INTERVAL 15 MINUTE ); for (const order of expiredOrders) { await pool.query( UPDATE seat SET status 0 WHERE id IN (?) AND status 2, [order.seat_ids.split(,)] ); await pool.query(UPDATE orders SET status 3 WHERE id ?, [order.id]); } }status 2 的锁定座位释放回空座订单变成已关闭。这个定时任务用 node-cron 或 setInterval 都行注意加事务避免座位释放了但订单状态没更新造成两边不一致。4.3 支付回调验签、幂等更新与座位最终落定用户点支付后小程序端发起 wx.requestPayment微信支付扣款成功后微信服务器会异步请求后端回调地址。回调地址在创建订单时随参数传给微信必须是你自己服务器上可公网访问的 HTTPS 地址。// server/routes/pay.js router.post(/api/pay/notify, async (req, res) { const body req.body; // 1. 验签用微信支付商户密钥校验签名 if (!verifySign(body)) { return res.send(failXml(签名错误)); } const { out_trade_no, transaction_id, result_code } body; if (result_code ! SUCCESS) { return res.send(successXml()); } // 2. 查订单幂等处理 const order await findOrderByNo(out_trade_no); if (!order) { return res.send(failXml(订单不存在)); } if (order.status 1) { // 已支付直接返回成功避免重复处理 return res.send(successXml()); } // 3. 事务里更新订单和座位 await orderPaid(order, transaction_id); res.send(successXml()); });successXml 返回给微信的是固定 XML 格式xmlreturn_code![CDATA[SUCCESS]]/return_code/xml微信收到这个才算回调成功否则会按策略重试多次。verifySign 一定要做否则任何人往回调地址发个伪造报文订单就被改成已支付。orderPaid 在事务里把 orders 的 status 改成 1写入微信侧 transaction_id再把对应 seat 表的 status 从 2 改为 1这一步是座位最终落定。如果回调处理中抛了异常不要直接返回 500正确做法是 catch 住、记日志、返回失败 XML等微信重试时再修复处理。5. 避坑与常见问题排查真机、并发、回调与订单号这几个翻车点这一章写常见问题每条按“现象 → 原因 → 解决”展开基本覆盖从本地跑到真机再到上线的完整过程。5.1 开发者工具里一切正常真机预览白屏、所有请求失败现象开发者工具中能正常刷出影片列表扫码在手机上预览时白屏Network 面板里请求全部失败。原因开发者工具勾选了“不校验合法域名”绕过了域名限制真机小程序要求所有 wx.request 的 URL 必须是 HTTPS且域名要提前在小程序管理后台的「开发 开发设置 服务器域名」里配置。解决开发阶段用测试号加勾选不校验域名做联调没问题要真机测试或发布后端接口必须挂到配了 HTTPS 证书的服务器上域名在小程序后台配置 request 合法域名。证书用云服务商免费的即可小程序端 BASE_URL 改成 https 域名就行代码逻辑不用动。补充一点配置合法域名后有缓存不是立刻生效改完等一小会儿再重新编译。如果用的是 IP 地址而不是域名也会被拒微信不允许直接请求 IP。5.2 选座页面能选、提交时总提示“座位已被占用”现象用户明明看到座位是空的点下一步时后端返回“部分座位已被锁定或售出”。原因这套源码的锁座发生在下单接口用户停留在选座页的几分钟里后端没有预占动作。另一个用户可能已经把同一座位在确认页提交并锁定前端看到的是旧数据。解决两个层面。简单方案是进入选座页时请求 /api/seat/lock把整场座位状态拉回来用户点选后前端先在前端标记严格方案是下单接口的事务锁不能省并且失败时前端要重新拉取座位状态把冲突座位标红。锁座有代价用户选了位置不下单座位会被白锁 15 分钟所以选座页最好提示用户“锁座后请尽快支付”前端离开页面时主动调用释放接口没有就想办法靠定时任务兜底。5.3 支付成功但订单一直停在待支付用户收不到票现象微信支付账单已经扣款后端数据库订单 status 还是 0小程序里订单显示未支付票取不出来。原因大概率是支付回调没被正确消费。可能是回调地址不是公网 HTTPS、回调代码抛了异常、验签失败或者 out_trade_no 与本地订单号对应不上。解决先看后端日志有没有收到 /api/pay/notify 请求。收到但失败多半是验签或订单号问题完全没收到检查创建订单时传给微信的 notifyUrl 是不是公网 HTTPS。排查时可以先在回调接口第一行记录 body保证先能收到请求再往下查。幂等处理必须做否则微信重试回调时同一笔订单会被重复出票或重复处理。5.4 场次时间显示比实际多/少 8 小时现象数据库存的是 20:00 的场次小程序上显示成 12:00 或次日 04:00。原因MySQL 连接配置没设置时区服务器用的 UTC 时间小程序端 new Date 解析时按本地时区换算于是出现 8 小时偏移。解决在 MySQL 连接配置里指定时区或在连接串上设置 serverTimezone 相关参数后端返回给前端的时间统一用时间戳或 ISO 8601 字符串不要直接拼“YYYY-MM-DD HH:mm:ss”格式的字符串。我一般会在后端统一做一个时间格式化出口所有时间字段经过它输出前端不做任何时区换算。5.5 订单号重复导致回调对账失败现象偶尔出现支付成功但订单状态不更新日志显示“订单不存在”或“订单号已存在”。原因订单号生成太简单直接用时间戳或自增 id高并发下生成重复单号下单被覆盖或回调找不到唯一记录。解决订单号用前缀 日期 随机串生成随机串至少 6 位以上数据库给 order_no 建唯一索引下单时捕获唯一索引冲突再重新生成。这个习惯不难但治的是最让人头疼的对账问题。6. 进阶改造把单影院商城改成多影院版只动四个层次如果这套代码要拿来做区域影院或多门店购票业务单影院逻辑需要改成多影院。改动并不复杂因为它已经具备 cinema 表和 session 表的关联基础缺的主要是产品入口和订单归属。第一个层次数据入口加城市或影院筛选。首页接口 /api/film/nowPlaying 增加 cinema_id 参数后端查询 film 时通过 session 关联 cinema 过滤。小程序端在首页顶部加影院切换器点击进入影院列表页选中后把 cinema_id 存入全局数据后续请求都带上。第二个层次选座数据按场次隔离。座位表本身挂在 session_id 下多影院共用一套表结构没问题只要场次创建时正确关联 cinema_id。影院管理后台排片时选影院、选影厅、生成场次前端不感知差异。建议单独建 hall 影厅表seat 关联 hall_id而不是在 seat 表里写死座位数。第三个层次订单归属和统计。orders 表增加 cinema_id 字段创建订单时从 session 对应的 cinema 写入后续按影院维度统计票房、结算票款、做渠道分成都靠这个字段分组。要做门店独立核算可以在订单明细里再加一个 seller_id方便按渠道对账。第四个层次分享与渠道识别。生成小程序码时在 scene 里带上 cinema_id用户扫码进入后首页自动定位到对应影院。这样区域推广时每个影院有自己的入口和统计维度而不是所有用户挤在统一入口里。改造层级和对应文件可以看这张表改造点主要涉及位置核心内容影院筛选入口pages/index、pages/cinema-list新增影院列表页全局存储 cinema_id排片与场次server/routes/session创建场次时关联 cinema_id 和 hall_id订单归属server/routes/order、orders 表订单写入 cinema_id按影院统计小程序码入口server/routes/qrcodescene 带 cinema_id 参数从那以后我接手每一套带订单的源码都先把订单状态流转路径画一遍把待支付、已支付、已关闭、已退款和对应座位状态列在一张纸上再动代码。这套购票源码里的订单状态机算得上完整但在改成多影院、加优惠券、接第三方支付时一定要先确认状态变更的触发点和幂等逻辑否则上线后丢单、重复支付的坑会一个一个找上来。希望帮到你。本文还有配套的精品资源点击获取