简介:基于thinkphp与uniapp开发的租赁小程序源码,面向需要搭建租借、归还、押金管理等场景的开发者与企业。系统以小程序为前端载体,后端采用ThinkPHP框架,支持多角色平台管理,内置装修、门店、商品、订单、财务、优惠券、会员与配置中心等核心模块,并支持二级分销体系。压缩包共2000个文件,以js、vue、css等前端代码为主,同时包含html页面、json配置、md说明文档以及SQL数据库脚本,整体约25.14MB,资源结构较完整。功能上,商品模块支持多规格单品和组合套餐,后台可灵活调整库存与价格;订单模块可管理租赁与充值订单,员工端可执行出库、归还操作;财务模块提供提现申请、充值套餐和余额明细管理。完整源码附带了数据库脚本和可运行的小程序页面,有助于快速理解ThinkPHP接口与UniApp前端交互逻辑。目前已有348人学习浏览,适合具备PHP与前端基础的开发者参考。
1. 租赁业务和普通商城最大的区别,不在功能表里
做过商城系统的人拿到这套源码,第一反应通常是先找商品、订单、支付这三个模块。但租赁场景下真正决定系统能不能跑起来的,是「押金 + 租金周期 + 出库归还」这条链路。普通电商订单付款即结束,租赁订单却在付款后拉开一个完整生命周期:商品要出库、按天计费、到期归还、验货退款。这套基于 ThinkPHP 5.1(FastAdmin 框架)加 UniApp 的租赁商城小程序源码,把这条链路完整拆成了用户端、员工端、后台管理端三套交互。小程序端面向 C 端用户浏览下单,员工端处理出库与归还,后台配置商品价格策略、会员余额和优惠券。它不追求大而全的中台设计,胜在业务闭环清晰——每个角色该干什么、订单走到哪一步该做什么,表结构和权限都给你铺好了。适合三类人:有实体物资想做起租业务的商家,比如工具租赁、礼服租赁、电子设备租赁;接外包需要快速交付租赁类小程序的开发者;以及想研究 ThinkPHP 多端项目结构的后端工程师。下面从架构链路讲到部署避坑,最后落在库存并发的处理技巧上。
2. FastAdmin 底座与 UniApp 三端:这套源码的项目结构
2.1 为什么是 ThinkPHP 5.1 + FastAdmin,而不是从零搭接口
这套源码的后端没有自己手搓框架,而是基于 FastAdmin 二次开发。FastAdmin 是 ThinkPHP 5.1 生态里常用的后台开发框架,自带 RBAC 权限管理、后台模板、插件机制和一键生成 CRUD 的能力。选择它做租赁小程序底座,最直接的好处是后台管理界面不用从头写,菜单、管理员、角色权限这些通用能力开箱即用。你在后台看到的门店管理、员工权限、财务提现等模块,本质上都是在 FastAdmin 的控制器、模型、视图三层结构上扩展出来的业务。
对于小程序端,UniApp 的价值在于一套代码可编译到微信小程序、H5 和 App。但要注意,这套源码的实际业务重心在微信小程序端,H5 和 App 属于附带能力,App 打包后如果要上架安卓应用市场,还需要额外处理软著和隐私合规,这部分后面部署章节会说。
请求链路按 FastAdmin 的默认路由走:小程序端uni.request发起 HTTP 请求,经 Nginx 转发到public/index.php,入口文件根据路由参数定位到对应的 API 控制器。控制器里有两个默认行为:检测管理员登录态和客户端用户登录态。FastAdmin 的 API 模块把这两个登录态分得很开,后台管理员走Admin中间件,小程序端用户走User中间件,二者 Token 互不通用。实际开发时最容易犯的错就是把用户 Token 塞到后台请求头里,然后拿不到数据还找不到原因。
这套源码的目录结构大致如下:
application/ admin/ # 后台管理控制器(门店、商品、订单、财务) api/ # 小程序端接口控制器(用户登录、商品列表、下单) extend/ fast/ # FastAdmin 核心扩展 public/ assets/ # 后台静态资源(bootstrap.css、fastadmin.css 等) addons/ # 插件目录后台静态文件里出现了bootstrap.min.css、fastadmin.min.css这类文件,说明后台界面是 Bootstrap 3 加 FastAdmin 自研样式组合,前端交互少不了 jQuery 和 RequireJS。这套组合对后端开发者很友好——不需要懂 Vue 也能维护后台页面。但也意味着后台页面和 Uniapp 小程序前端是两套技术栈,改动后台页面需要直接操作 PHP 模板文件。
2.2 后台登录态与小程序端用户态的隔离设计
FastAdmin 后台的登录是基于think\facade\Session的服务端会话,管理员登录后 Session 里记录admin_id,访问控制器时通过后台基类的_initialize方法做鉴权。而小程序端的用户态走的是无状态 Token 机制:用户通过微信登录拿到code,后端调用code2Session接口换取openid,然后签发一个自定义 Token 返回给前端。前端每次请求在 Header 里带token,后端用中间件解析并写入当前用户信息。
// application/api/controller/User.php 登录逻辑示意 public function login() { $code = $this->request->post('code'); $wxRes = $this->wechat->login($code); // 小程序 code 换 openid $user = UserModel::where('openid', $wxRes['openid'])->find(); if (!$user) { $user = UserModel::create([ 'openid' => $wxRes['openid'], 'nickname' => '微信用户', ]); } $token = md5(uniqid() . $user->id); cache('user_token_' . $token, $user->id, 7200); // Token 存缓存,2小时过期 return json(['token' => $token, 'user_id' => $user->id]); }这段逻辑里最关键的是 Token 写入缓存而不是数据库。用cache()函数存储 Token 映射关系,请求进来时从缓存读,过期自动失效,不需要额外建表。如果要延长登录态有效期,把第三个参数从 7200 改成 2592000(30天)即可。注意:每次请求必须校验token,这个校验逻辑通常写在 API 基类的_initialize方法里,不要在每个控制器里重复写,否则后续维护时漏了一个接口就等于把用户信息裸奔出去。
2.3 表结构设计的业务重心在订单和库存记录
这套源码的核心数据表可以从后台菜单反推出来。门店管理对应store表,公告对应store_notice表,员工权限直接复用 FastAdmin 的auth_group和admin表,商品相关的是goods和goods_sku多规格表,租赁订单是rent_order和rent_order_items,用户余额记录是user_money_log。下面用表格梳理关键表和它们承担的业务职责:
| 表名 | 业务职责 | 关键字段 |
|---|---|---|
| store | 门店基础信息、公告、地址定位 | name、notice、latitude、longitude |
| goods | 租赁商品主表 | status、store_id、deposit、rent_type |
| goods_sku | 商品规格库存价格 | goods_id、spec、stock、day_price、month_price |
| rent_order | 租赁主订单 | order_no、user_id、total_deposit、rent_fee、status |
| rent_order_items | 订单里的明细商品 | order_id、goods_id、sku_id、start_date、end_date |
| user_money_log | 余额变动流水 | user_id、amount、type |
| distribution_log | 分销佣金记录 | user_id、order_id、level、amount |
租赁业务里经常把「押金」和「租金」混在一个字段里,但这套系统的设计是拆开的。deposit字段在归还验收前都是冻结状态,rent_fee按租赁天数计算,结算时要分开算。rent_order_items里的start_date和end_date直接决定了租金的计算区间,而不是用下单时间替代——这是租赁业务和卖货业务在数据设计上最大的差异。
3. 租赁订单状态机与押金/租金结算逻辑
3.1 订单状态如何在用户、员工、后台间流转
租赁订单的状态流不是简单的「待付款→已完成」,而是多了一个「租用中」和「已归还待结算」的中间态。用户下单付款后订单是待提货,员工在线下把实物交给用户时在员工端点击确认出库,订单才进入租用中;用户归还商品后员工确认收货并检查商品损伤,订单变为已归还,此时押金才进入退款审核。下面这张状态表对应的是后台订单列表里看到的状态字段:
| 状态值 | 含义 | 触发方式 | 押金状态 |
|---|---|---|---|
| 0 | 待付款 | 用户下单未支付 | 未收 |
| 1 | 待提货 | 支付成功,等待员工出库 | 已收 |
| 2 | 租用中 | 员工端确认出库,计时开始 | 冻结中 |
| 3 | 待归还 | 用户申请归还或租期到期 | 冻结中 |
| 4 | 已归还 | 员工确认收货,等待结算 | 待退还 |
| 5 | 已完成 | 押金退还完成 | 已退还 |
| 6 | 已取消 | 用户主动取消或超时未支付 | 无 |
前端状态展示直接拿这个数字做映射。员工端的出库操作对应状态 1 到 2,归还操作对应状态 3 到 4,后台的退款操作对应 4 到 5。这个链路的精髓在于「每个状态变更都强制绑定一个角色的操作」,而不是系统自动流转,这样出了纠纷能定位到是谁在什么时间动的手。
要在后台增加一个「强制归还」的操作按钮,只需要在订单管理控制器里加一个自定义方法,把订单状态直接改到 3,并记录操作日志。源码的 FastAdmin 后台列表页通过addtabs和ajax请求来实现操作,不需要刷新整个页面。
3.2 租金自动计算的两种维度:按日租和按月租
商品发布时有「日租价」和「月租价」两个价格档次,下单时用户选择起止时间,后端根据所选区间判断该按哪个价格计算。计算规则写在一个公共方法里,这样员工端和用户端调用的是同一套逻辑,不会出现前端展示价和后端实收价不一致的情况。
// 计算租金,$startDate 和 $endDate 为日期字符串,格式 Y-m-d public function calcRent($sku, $startDate, $endDate) { $days = (strtotime($endDate) - strtotime($startDate)) / 86400 + 1; if ($days <= 0) { throw new \Exception('结束日期不能早于开始日期'); } // 超过30天按整月折算,不足整月按日租价补齐 $months = intdiv($days, 30); $remainDays = $days % 30; $fee = $months * $sku['month_price'] + $remainDays * $sku['day_price']; // 单独计算押金,押金不计入租金 $deposit = $sku['deposit']; return [ 'days' => $days, 'months' => $months, 'rent_fee' => $fee, 'deposit' => $deposit, ]; }这套算法属于业务上最常见、也最容易说服用户的方案:租期越长越便宜,但不会出现「租 31 天比租 30 天还便宜」的荒谬结果。比如一件商品日租 20 元、月租 500 元,租 31 天时结果为 500 + 20 = 520 元,租 30 天是 500 元,多一天多 20 元,符合直觉。如果要改成按自然月结算,需要把months的判断改成检查日期是否跨月,但那套逻辑更适合长期租赁,短租场景用天数折算反而简单可靠。
3.3 员工端出库与归还的接口约定
员工端本质上是嵌在用户端小程序里的一个隐藏入口——员工用分配给自己的手机号登录后,小程序通过角色判断渲染出工作台菜单。出库操作提交的参数只有order_no和员工操作人id,后端校验订单状态为待提货后,更新库存并把状态置为租用中。
// 员工出库接口 public function outbound() { $orderNo = $this->request->post('order_no'); $adminId = $this->request->post('admin_id'); $order = RentOrderModel::where('order_no', $orderNo)->find(); if ($order['status'] != 1) { return json(['code' => 0, 'msg' => '当前状态不可出库']); } Db::startTrans(); try { // 扣减实际库存 GoodsSkuModel::where('id', $order['sku_id']) ->dec('stock', 1) ->update(); // 订单流转到租用中 $order->status = 2; $order->outbound_admin_id = $adminId; $order->outbound_time = time(); $order->save(); Db::commit(); return json(['code' => 1, 'msg' => '出库成功']); } catch (\Exception $e) { Db::rollback(); return json(['code' => 0, 'msg' => '出库失败']); } }这里用事务把库存扣减和订单状态变更绑在一起,保证不会出现「库存扣了但订单状态没改」的不一致情况。dec是 ThinkPHP 的原子自减方法,比select + update两步操作更安全,在线下门店同时有多人扫码出库的场景下能避免超卖。等待用户归还时,逆向流程调用归还接口,库存做inc自增,状态从 3 变到 4。归还接口里还可以增加一个「损坏赔偿金额」字段,由员工填写,系统自动从押金里扣除后再走退款。
4. UniApp 小程序端的页面结构与角色化路由
4.1 从pages.json看小程序端的功能划分
UniApp 的页面结构和路由都写在pages.json里。这套源码的小程序端页面可以分成三组:用户端常规页面、支付与订单页面、员工端工作台页面。用户端首页和商城页是装修模块的动态渲染容器,商品详情页负责展示规格选择起租时间,订单确认页整合了余额和微信支付两种支付方式。员工端则独立在staff目录下,和用户端页面完全分离。
{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "租赁首页" } }, { "path": "pages/goods/detail", "style": { "navigationBarTitleText": "商品详情" } }, { "path": "pages/order/confirm", "style": { "navigationBarTitleText": "确认订单" } }, { "path": "pages/staff/dashboard", "style": { "navigationBarTitleText": "员工工作台" } }, { "path": "pages/staff/scan", "style": { "navigationBarTitleText": "扫码出库" } } ] }页面拆分的核心思路是「员工端和用户端不混在同一个页面里」。虽然二者共用一套登录态,但页面入口、底部 TabBar 和操作按钮完全不同。staff/scan页面使用uni.scanCode扫码接口,扫描用户出示的订单二维码,然后把码内携带的order_no传给后端出库接口。这样员工不用手动输入订单号,降低线下操作出错率。
4.2 装修模块的动态渲染机制
后台装修模块改的不只是图片和文案,而是整个首页的组件结构和主题色。后台把装修配置以 JSON 格式存到config表,小程序端首页请求接口拿到 JSON 后动态渲染。JSON 结构大致如下:
{ "theme_color": "#ff5f17", "components": [ { "type": "banner", "data": { "images": ["https://xxx/banner1.png"] } }, { "type": "notice", "data": { "text": "新用户首租立减20元" } }, { "type": "goods_list", "data": { "category_id": 3, "limit": 10 } } ] }前面的/后面我在前面已经写过了,注意 JSON 值里的https://xxx在替换成实际域名时,需要在小程序后台配置 downloadFile 合法域名,纯 http 在内网可以调试但真机预览会直接白图。小程序端拿到theme_color后通过 CSS 变量赋值,全局按钮、导航栏、价格文字都会跟着变。
// 动态设置主题色 const app = getApp(); app.globalData.themeColor = res.data.theme_color; uni.setNavigationBarColor({ frontColor: '#ffffff', backgroundColor: res.data.theme_color }); document.documentElement.style.setProperty('--theme-color', res.data.theme_color);这里document只在 H5 端可用,在微信小程序里要换思路:组件样式中直接使用 CSS 变量,而page根节点的style由 JS 动态设置。小程序端的做法是给每个页面的根 view 绑定:style="{ '--theme-color': themeColor }",这样子组件里写color: var(--theme-color)就能全局生效。后台保存装修后会生成一个版本号,小程序端本地缓存接口返回的配置,下次进入先渲染缓存再对比版本号做增量更新,避免首页每次打开都白屏等待。
4.3 登录态与前端路由守卫的配合
小程序端登录流程不再是传统的账号密码,而是调用uni.login获取临时code,交给后端换openid并返回业务token。开发者工具里可以模拟,但真机调试必须用真实 AppID,否则 code 换不到 openid。用户信息接口返回的数据里包含is_staff字段,前端根据这个字段决定是否渲染员工工作台的入口。
// 路由守卫:进入员工页面前校验角色 uni.request({ url: '/api/user/info', header: { token: uni.getStorageSync('token') }, success: (res) => { if (res.data.is_staff === 1) { uni.navigateTo({ url: '/pages/staff/dashboard' }); } else { uni.showToast({ title: '无权限访问', icon: 'none' }); } } });后端user表里存了is_staff字段,员工账号由后台门店模块创建并绑定到门店。这样员工离职后只要后台把账号禁用,小程序端该账号立即失去工作台权限,不需要重新发版。如果要做更细的权限控制,比如仓库员工只能看出库单不能操作退款,可以再建一张staff_auth表和admin表做映射,FastAdmin 后台的管理员表天然支持分组权限,扩展起来不难。
5. 部署上线:伪静态、微信支付配置与库存并发扣减技巧
5.1 LNMP 环境的 Nginx 配置要点
这套源码是 ThinkPHP 5.x 项目,PHP 版本建议 7.4。有些 ThinkPHP 历史版本在 PHP 8.0 下会出现 DEPRECATED 警告甚至直接白屏,主要是each()、create_function()这类函数被移除导致。如果你只拿到了源码包没有说明文档,部署时报 500 错误,先打开runtime/log里的日志文件,绝大多数原因集中在目录权限不够和 PHP 扩展缺失(特别是fileinfo和redis)。Nginx 伪静态配置如下:
server { listen 80; server_name yourdomain.com; root /var/www/rent/public; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~ \.(js|css|png|jpg)$ { expires 30d; } }root指向的是public目录而不是项目根目录,这是 ThinkPHP 安全部署的标准做法——入口文件外露,数据库配置和业务代码都在上一级目录,即使某个静态文件解析异常,攻击者也拿不到application/database.php。部署完成后记得改config/database.php里的连接信息,并把后台入口从admin.php改成随机字符串,能挡掉大部分扫描后台路径的脚本。ThinkPHP 历史上出过多次远程代码执行漏洞,公网环境务必开启app_debug => false,关闭错误回显。
5.2 小程序发布前的微信支付与域名配置
小程序端上传代码前,要在manifest.json的小程序配置里填入自己申请的 AppID,同时确认后台 API 请求地址是 HTTPS 域名而不是 IP。微信支付配置分散在两个地方:小程序后台的「微信支付」关联商户号,后端支付参数配置在后台的配置中心,需要填mch_id、api_key和证书路径。这里要特别提醒,退款和微信支付回调验签必须用 API v2 的 HMAC-SHA256 方式,很多源码用的是 MD5 方式的旧接口,微信侧 2024 年后已逐步收紧,如果退款一直报签名错误,检查是否升级到 v2 的 HMAC-SHA256。
测试环境常见的坑是「真机预览请求失败」。开发工具勾选「不校验合法域名」后接口能通,但真机上由于没有关闭域名校验,HTTPS 证书必须是由正规 CA 签发的,不能是自签名。同时微信公众平台要把request合法域名和downloadFile合法域名都配置完整,后者管的是用户头像、商品图片、装修 banner 的加载。
5.3 高并发下租赁库存扣减的 Redis 原子操作
最后收在一个具体技巧上:多门店同时出库时,dec方法虽然保证单次操作的原子性,但遇到秒杀场景还是要上 Redis。源码里的库存字段在 MySQL,可以先同步一份到 Redis,出库时改用decr:
# 商品 SKU 初始化库存 SET goods_sku_stock_123 100 # 出库扣减 DECR goods_sku_stock_123 # 归还回补 INCR goods_sku_stock_123Redis 的DECR是原子操作,两个员工同时出库不会出现扣减覆盖。扣减成功后用消息队列异步把结果写回 MySQL,MySQL 表里存一个version字段做乐观锁,更新时WHERE id = 123 AND version = 旧版本号,不匹配就重新读取重试。这方案比单纯 MySQL 的dec在高并发下表现更稳,而且归还操作走了INCR回补,Redis 里的值和 MySQL 最终一致。如果后续要做「租期到期自动扣费提醒」,用定时任务扫描end_date < now()的待归还订单,提前一天给用户推订阅消息,这套表结构已经足够支撑。
本文还有配套的精品资源,点击获取