PHP+微信小程序酒店预订系统源码解析:从数据库到支付全流程
做酒店类小程序很多人上来就问能不能直接给我一套能跑的源码PHP做后端、微信小程序做前端这个组合确实被有些人看成“老一套”但实际交付时它比你想的稳得多。我前后拿这套PHP酒店预订微信小程序的结构改过好几版从单店民宿到连锁酒店都能用。今天不扯概念直接把源码里的核心设计、数据库、登录支付流程、还有我踩过的坑摊开讲一遍给想复现或者二次开发的人一个参考。这套源码解决的最核心问题是把“找房—下单—支付—订单管理”这条闭环跑通。小程序端负责展示和交互PHP后端提供接口数据库承载房间、订单、用户三类核心数据。适合正在做毕业设计、接私活或者刚学完小程序想了解后端接口怎么写的开发者。不需要高配服务器一台普通云主机甚至本地环境就能跑起来。1. 项目整体设计与技术选型1.1 为什么选择PHP做小程序后端先回答一个最常见的疑问现在都流行Java、Go、Node为什么还要用PHP我的回答很实际中小型酒店预订项目最大的特点就是并发量不高、管理功能繁杂、交付周期短、服务器成本敏感。这几个条件凑一起PHP的优势就被放大了。首先是部署成本极低。一套PHP环境加一个MySQL数据库几分钟就能搭好虚拟主机也能跑不像Java动不动就要配Tomcat、Maven运维门槛低很多。其次是开发效率高PHP语法灵活数组和JSON的互转方便到让人偷懒。小程序后端接口说白了就是“收参数、查数据库、返回JSON”PHP在这种情境下写起来很顺手。这套源码没有依赖重型框架而是用原生PHP加PDO数据库扩展实现的。选择原生PHP而不是框架当时有两个考虑一是方便二次开发不管你会不会框架只要看得懂PHP语法就能改二是减少学习成本源码结构透明每个接口文件做了什么一眼就能看出来。缺点当然也有比如没有框架自带的容器和中间件需要自己写公共方法但这些在项目规模可控时完全能接受。1.2 功能模块拆解整套系统按角色可以分成用户端和管理端虽然源码包不大但模块划分很清晰。我习惯把功能模块画成这样一张表模块小程序端后端接口数据表用户认证微信一键登录登录接口、用户信息更新用户表房间展示列表、详情、条件筛选房型列表、房型详情房型表预订下单选日期、确认订单创建订单、计算价格订单表支付拉起微信支付支付参数生成、回调处理订单表、支付日志订单管理我的订单、取消订单订单列表、取消接口订单表管理后台非小程序部分房型维护、订单查询房型表、订单表这里我特意没有把“管理后台”做进小程序里而是单独做了一个Web端页面。原因很简单酒店管理员在手机上用一个HTML页面就够用了没必要塞进小程序增加包体积。小程序端只需要面对C端用户功能越纯粹越好。1.3 项目目录结构设计源码到手后先别急着看细节把目录结构认清楚。这套代码的目录组织不复杂我简写一下核心结构project/ ├─server/ # PHP后端目录 │ ├─config/ │ │ └─database.php # 数据库配置 │ ├─public/ │ │ └─index.php # 统一入口文件 │ ├─api/ │ │ ├─user.php # 用户登录接口 │ │ ├─room.php # 房型接口 │ │ └─order.php # 订单接口 │ ├─lib/ │ │ ├─Db.php # PDO数据库封装 │ │ ├─Response.php # JSON响应封装 │ │ └─WxPay.php # 微信支付相关 │ └─sql/ │ └─hotel.sql # 初始化脚本 ├─miniprogram/ # 微信小程序前端目录 │ ├─pages/ │ │ ├─index/ # 首页 │ │ ├─roomList/ # 房型列表 │ │ ├─roomDetail/ # 房型详情 │ │ ├─orderConfirm/ # 确认下单 │ │ ├─orderList/ # 订单列表 │ │ └─mine/ # 个人中心 │ └─utils/ │ └─request.js # 封装wx.request └─admin/ # 简易管理端入口文件只做一件事解析请求参数把请求分发到对应API文件。接口路径设计成/api/room.php?actionlist这种形式虽然不像RESTful那么优雅但胜在简单不容易出路由匹配的幺蛾子。如果你喜欢优雅路由可以自己加上但我觉得现有方式就够用了。2. 数据库设计与接口规范2.1 数据表结构设计那套源码最值得反复看的地方其实是数据库设计。酒店预订的核心不是代码而是数据关系。我拆解一下三张核心表用户表、房型表、订单表。用户表记录微信用户的基础身份信息字段不算多。房型表要保存房价、库存、面积、床型、图片这些展示信息。订单表是最复杂的得把用户、房型、入住日期、离店日期、订单金额、支付状态都串起来。以订单表为例关键字段我列一下CREATE TABLE room_order ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id int(11) NOT NULL COMMENT 下单用户ID, room_type_id int(11) NOT NULL COMMENT 房型ID, check_in_date date NOT NULL COMMENT 入住日期, check_out_date date NOT NULL COMMENT 离店日期, nights int(11) NOT NULL COMMENT 入住晚数, total_price decimal(10,2) NOT NULL COMMENT 订单金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付,1已支付,2已取消,3已完成, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL COMMENT 支付时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_room_type_id (room_type_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;有几个细节我特别说一下。订单号必须要唯一索引这既是业务需要也是防止重复下单的重要手段。status字段用数字而不是字符串是为了缩短存储空间和比较速度。nights字段看起来可以通过两个日期算出来但冗余存下来能避免日后再写复杂的日期计算逻辑查询时也直观。2.2 接口安全与鉴权设计小程序端的接口不能裸奔至少要做一层用户身份校验。这套源码里的做法是用户登录成功后后端生成一个随机token返回给小程序小程序每次请求都在header里带上这个token后端再去查这个token对应用户是谁。token本身只是一个随机字符串我用md5(uniqid() . $openid)生成然后存到数据表里。虽然没有JWT那么多讲究但胜在实现简单、不易伪造。只要保证token不泄露安全性就足够了。关键是要在每次查询数据时都拿token解析出来的userId去过滤数据避免用户A越权查到用户B的订单。支付回调的安全更需要注意后面我会单独讲。总结下来接口安全设计了三层登录鉴权、用户数据隔离、支付验签。2.3 接口清单与统一返回格式前后端联调最怕接口格式不统一所以这套源码从一开始就定了统一的返回结构{ code: 0, msg: success, data: {} }code为0代表成功非0代表业务错误msg是给前端提示信息data放实际业务数据。前端小程序封装的request.js只需要判断code是否为0然后决定是进入业务逻辑还是弹出错误提示。接口清单大概长这样方法接口路径功能主要参数POST/api/user.php?actionlogin微信登录codeGET/api/room.php?actionlist房型列表可选日期GET/api/room.php?actiondetail房型详情idPOST/api/order.php?actioncreate创建订单room_type_id, check_in_date, check_out_datePOST/api/order.php?actioncancel取消订单order_noGET/api/order.php?actionlist我的订单status可选POST/api/order.php?actionpay获取支付参数order_noPOST/api/pay.php?actionnotify微信支付回调微信支付报文每个接口实现都不复杂核心逻辑放在订单创建和支付这两块也最容易写错。3. 核心功能实现与源码解读3.1 微信登录与用户体系实现小程序的微信登录流程初学者第一次看容易懵。其实拆开就三步小程序调用wx.login拿到一个一次性code后端拿这个code去微信服务器换openid再用openid去用户表查或建用户。关键代码片段如下$code $_POST[code] ?? ; $url https://api.weixin.qq.com/sns/jscode2session; $params [ appid $config[appid], secret $config[secret], js_code $code, grant_type authorization_code, ]; $response file_get_contents($url . ? . http_build_query($params)); $result json_decode($response, true); if (isset($result[errcode])) { // 根据errcode处理错误 Response::error($result[errmsg]); } $openid $result[openid]; $user Db::queryOne(SELECT * FROM user WHERE openid ?, [$openid]); if (!$user) { Db::execute(INSERT INTO user (openid, nickname, create_time) VALUES (?,?,NOW()), [$openid, 微信用户]); $userId Db::lastInsertId(); } else { $userId $user[id]; } // 生成token $token md5(uniqid() . $openid); Db::execute(UPDATE user SET token ? WHERE id ?, [$token, $userId]); // 返回token和用户信息这里有几个坑值得说。第一code只能用一次用完就作废所以前端不能在短时间内重复调用登录接口。第二微信返回的session_key不要存数据库也不需要用它做本地加密因为小程序端根本不需要解密用户信息用openid就足够识别用户。第三如果将来要读取手机号才需要用到session_key来解密数据但基本场景下用不到。3.2 房型列表和可用房查询酒店预订小程序里最核心的业务逻辑是一个房型每天有多少间可订用户选择日期后前端要展示剩余数量。源码里采用的是“总库存减去已有订单占用间数”的计算方式。房型表里有个stock字段表示房间总数订单表里记录了每笔订单的入住和离店日期。查询某段时间的可订数量SQL大概长这样SELECT rt.*, rt.stock - IFNULL(SUM( CASE WHEN o.check_in_date ? AND o.check_out_date ? THEN 1 ELSE 0 END ), 0) AS available_stock FROM room_type rt LEFT JOIN room_order o ON rt.id o.room_type_id AND o.status 1 WHERE rt.status 1 GROUP BY rt.id我解释下这段SQL。入住日期check_in_date到离店日期check_out_date判断两个日期范围是否有重叠可以用o.check_in_date checkOutDate AND o.check_out_date checkInDate。有重叠就说明这一晚被占了。因为订单是按整间房算的所以每个重叠订单累加1。最后用总库存减掉占用数就能算出用户选择日期范围内还有几间房可订。需要注意上面的SQL是最基础的版本实际项目里可能还要考虑已经被取消的订单不能算占用加一个o.status 1已经过滤了已支付状态的订单。如果用户下单后未支付也不能占用库存所以在创建订单时如果使用“先锁定库存”的策略就得把待支付订单也统计进去还要加时间限制防止僵尸订单。这套源码选的是简单策略未支付订单不锁库存支付回调成功后才真正扣减。3.3 下单与支付回调处理下单接口是整个项目里业务逻辑最密集的地方。它的流程是接收房型ID、入住日期、离店日期校验参数计算晚数和价格生成订单号然后调用微信支付统一下单接口把支付参数返回给小程序前端拉起支付。计算价格时我用的是最直接的公式$nights (strtotime($checkOutDate) - strtotime($checkInDate)) / 86400; $totalPrice $roomType[price] * $nights;这里有个小坑strtotime算出来的天数可能因为夏令时等问题产生误差但国内没有夏令时所以除以86400是安全的。保险起见仍然建议用DateTime::diff来算晚数代码可读性也更好。创建订单时我特意在事务里做了库存校验防止两个用户同时下单导致超卖。伪代码如下Db::beginTransaction(); $available Db::queryOne(SELECT stock FROM room_type WHERE id ? FOR UPDATE, [$roomTypeId]); // 计算已占用房间数并判断 if ($available 0) { Db::rollBack(); Response::error(房间已满); } Db::execute(INSERT INTO room_order ...); Db::commit();这里用到了SELECT ... FOR UPDATE它会把对应房型记录锁住直到事务结束。第一次写并发代码的人很容易忽略这个锁导致测试环境没问题真一上线就有超卖订单。后面排查章节我会再展开。支付回调是有一套独立逻辑的直接暴露给微信服务器调用。回调地址必须是可以公网访问的HTTPS地址。回调收到微信发来的XML或JSON数据包后第一件事不是改订单状态而是验签。验签通过后再去订单表里查一下这笔订单是不是已经处理过如果处理过就直接返回成功不再重复修改这叫幂等处理。回调接口的大致流程我梳理成几条接收微信服务器POST的数据记录原始报文到日志。用微信支付密钥校验签名签名不通过直接返回错误。检查订单号是否存在金额是否与订单一致。将订单状态从“待支付”改为“已支付”记录支付时间。给微信返回xmlreturn_code![CDATA[SUCCESS]]/return_codereturn_msg![CDATA[OK]]/return_msg/xml。第5步尤其重要。微信支付回调有多重通知机制如果我们的接口没有正确返回“成功”标识微信服务器会认为通知失败然后每隔一段时间重试一次直到成功为止。如果业务代码有Bug就可能导致同一笔订单被重复处理。3.4 订单状态机设计订单状态我用了四个数字常量对应四类状态状态值常量名含义可流转到0WAIT_PAY待支付1、21PAID已支付32CANCELED已取消无3COMPLETED已完成无这个状态机很朴素但已经覆盖了正常业务场景。待支付订单可以取消已支付订单入住完成后置为已完成。如果要做退款还需要加一个“退款中”“已退款”的状态那就要联系微信支付退款接口复杂度会上升不少。源码里没有做退款功能因为对一个小型酒店预订系统来说先做“到店改签”“电话退款”这类人工处理反而更快技术债也少。4. 常见问题与排查实录4.1 微信登录失败和code换取openid报错我在联调阶段遇到过几次登录失败最典型的是前端拿到的code传到后端后后端调用微信接口返回了40029之类的错误码。后来查了下多半是appid和secret没配对或者请求参数名写错了。微信要求参数名是js_code很多人顺手写成code这就导致解析失败。还有一个容易被忽略的点就是小程序后台的服务器域名白名单。如果后端接口域名没有配置在小程序后台的“服务器域名”里真机上请求会被拦截开发工具里一般可以勾选“不校验合法域名”但真机调试是绕不过去的。所以遇到登录失败先看是不是环境问题再看代码问题。4.2 支付回调接收不到或者重复通知第一次联调微信支付时最容易卡在回调上。我用内网穿透工具把本地服务暴露出去调试结果微信服务器请求过来回调日志打出来了但订单状态一直没变。最后发现是回调校验签名时把$_POST里的数据直接用成了原始报文但微信支付新版回调数据可能是JSON格式跟旧版XML不一样签名算法也不同导致验签失败后直接return微信那边就不断重试。调试这类问题我的经验是第一时间打开接口日志把微信发的原始数据完整记录下来然后把日志里的数据和代码里打印的数据对比。签名计算的参数顺序必须严格按照文档来我用过很多次都是因为appid和mch_id在参与签名的时候顺序错了。4.3 并发下单导致超卖这个问题是后来做压测时发现的。一开始创建订单的代码只判断了stock大于0就放行结果用脚本模拟100个用户同时下单订单数竟然超过了库存数。原因很简单两个请求同时读到相同的剩余库存然后同时插入订单。加了SELECT ... FOR UPDATE之后问题就解决了。具体原理不复杂数据库的行锁会让第二个请求等待第一个请求的事务提交或回滚之后再去读库存读到的就是最新值。如果项目并发量再大一点比如上千人同时抢特价房那建议引入Redis预扣库存把库存操作放到内存里做MySQL只负责最终落单。但小型酒店一般用不到那么高并发行锁足够。4.4 部署上线要注意的几个细节部署这块除了常规的服务器环境配置有三个地方我建议别偷懒。第一数据库连接的字符集一定要是utf8mb4不要用utf8。因为utf8在MySQL里实际上存不了emoji和部分生僻字用户昵称里一个小表情就能让入库失败。第二PHP的display_errors在生产环境必须关掉。否则SQL报错信息会直接返回到小程序端用户看到的不只是难看还可能泄露表结构。我一般把日志写到文件里接口层统一返回“服务器异常”。第三小程序要求的接口域名必须是HTTPS证书别过期了。我遇到过客户拿着项目去备案证书装好了但HTTP跳转HTTPS没配好导致支付回调走了HTTP被微信拒绝。配好HTTPS之后最好用微信支付官方提供的工具测一下回调地址能不能通。这套代码拿到手真正要花心思的地方不在增删改查而是那些并发和状态一致性的细节。我前后改过好几轮印象最深的不是写代码而是调试支付回调。后来我把微信回调的日志级别调高每个请求都记录完整报文排查问题快了很多。如果你打算拿这套源码二次开发建议先跑通一条最简单的链路用户登录、选房、下单、支付回调、订单状态更新之后再往上面加促销、优惠券、会员等级这些花活。核心链路稳了其他功能都只是锦上添花。