简介:likeshop企业单商户商城V3.03版本是一套基于ThinkPHP与uniapp开发的开源B2C自营商城系统,目标用户是需要快速构建一体化电商平台的中小企业和独立开发者。系统打通PC商城、H5商城、微信小程序商城与APP商城,管理后台统一处理数据,内置分销裂变、限时秒杀、拼团活动、砍价活动、优惠券、大转盘抽奖、每日签到、小票打印、积分商城、会员价、微信零钱到账以及多种消息通知等常用营销模块。免费企业版与付费企业版功能基本一致,可通过Docker一键启动体验。源码包共2001个文件,约98.26MB,主要包含505个PHP后端接口文件、386个HTML页面、306个JS脚本、219个Vue组件以及Java、SQL、Shell、Nginx配置等,类型覆盖后端逻辑、前端交互、数据库脚本和部署环境配置,目录结构清晰,适合二次开发和部署迁移;目前已有177人学习下载。通过这份资源可获得完整商城前后端源码、Docker部署配置和开发文档,既能帮助学习者掌握ThinkPHP与uniapp多端开发技巧,也可以在企业合规前提下免费商用,快速搭建自有品牌商城。
1. 单商户商城为什么选 likeshop:免费商用许可是第一道分水岭
企业要做品牌独立商城,往往第一反应是自己搭一套后端,可一旦进入订单、支付、售后、营销分层,团队就会被细节拖住几个月。likeshop 企业单商户商城 v3.03 把这类需求做成开源系统:后端走 thinkphp,前端用 uniapp,商品、购物车、订单、会员、营销这类单商户核心模块开箱即有,代码里明确写着支持免费商用。对企业决策者来说,“免费商用”比源码本身更关键,个人学习没问题、放进生产环境也不怕许可证纠纷。另一个要读懂的词是“单商户”:一个后台管一个店铺,多商户是另一套独立代码,选型时不能混。下面不假装你看过源码,而是按一个接手工程师的视角,从一个还没部署的压缩包开始,讲完部署、多端跑通、二开排错,最后用一个 thinkphp 的 SQL 监听技巧收尾。
2. 基于 thinkphp 部署 likeshop 后端:从环境校验到管理后台初始化
2.1 先看懂 thinkphp 的多应用目录,再决定要不要动配置
likeshop v3.03 的后端走 ThinkPHP 6 LTS 这条稳定线。TP6 和很多老项目用的 TP3.2 是完全不同的骨架,容器、门面、中间件这套机制在 3.2 里都不存在,所以接手时千万不要拿 TP3.2 的“单入口挂一堆控制器”的思维去找代码。TP6 默认是多应用模式,入口在 public/index.php,应用按目录拆分,比如管理端是 admin 应用,用户端接口是 api 应用,支付回调是 notify 应用。URL 第一段决定进哪个应用,这就是为什么你访问域名/admin能进后台,访问域名/api/v1/goods能拿商品数据。多应用的好处是权限和参数配置能够按应用隔离,后台改烂了不影响用户端接口。拿到压缩包第一步不是装环境,而是快速扫一眼目录结构和.env文件,确认配置路径都被你认全了。
2.2 部署前要确认的 PHP、MySQL 与 Redis 参数
多数安装失败的根因不是代码问题,而是版本组合太新或太旧。下面这张表是 likeshop 这类 thinkphp 商城项目最常见的要求,不保证每个版本完全一致,但按这个基线去对,能少走弯路。
| 项目 | 最低要求 | 说明 |
|---|---|---|
| PHP | 7.4,推荐 8.0 或 8.1 | ThinkPHP 6 LTS 路线对 PHP 8 兼容性已经成熟,不要再上 PHP 5.6 |
| PHP 扩展 | fileinfo、redis、bcmath、openssl、pdo_mysql | 缺少 fileinfo 会出现文件上传类功能莫名报错 |
| MySQL | 5.7 以上,utf8mb4 | 商品标题、地址字段必须用 utf8mb4 才存得下 emoji 和生僻字 |
| Redis | 3.0 以上 | token、短信验证码、队列都会用到,装好先跑redis-cli ping验证 |
| Web 服务器 | Nginx/Apache | 伪静态必须指向 public 目录,否则 thinkphp 路由全 404 |
MySQL 和 Redis 的账号密码不要用 root 加空密码直接上生产。likeshop 后台的配置修改、数据库连接都在.env里,用独立账号能降低配置文件泄露造成的连带危害。PHP 版本如果在 8.2 以上,先跑一遍 composer install 看有没有 warning,部分第三方支付 SDK 的老代码可能报 Deprecated 但不影响运行,出现 fatal 再回退 8.1。
2.3 用命令完成 composer 安装、数据库迁移与伪静态
likeshop 的安装由根目录的 composer.json 管理,常见做法是先装依赖,再做数据初始化。下面这组命令适用于具备命令行环境的服务器,如果你用的是面板,步骤等价,只是把 shell 操作换成面板按钮。
# 1 安装 composer 依赖,生产环境跳过开发包并生成类映射 composer install --no-dev --optimize-autoloader # 2 复制环境变量模板并编辑数据库、redis 配置 cp .env.example .env # vim .env 修改 DB_HOST、DB_NAME、DB_USER、DB_PASSWORD、REDIS_HOST # 3 执行数据库迁移,建表并写入初始数据 php think migrate:run # 4 初始化管理员账号,具体命令以项目 README 为准 php think admin:init # 5 给运行时目录写权限,Nginx 用户必须有权限,否则后台提交就 500 chmod -R 775 runtime chown -R www-data:www-data runtime命令参数的逻辑拆开说:--no-dev在部署机上有两重意义,一是减少依赖数量、降低内存消耗,二是避免把 phpunit 这类开发工具暴露在生产目录里;--optimize-autoloader会为已安装的类生成映射,PHP 加载时少一段文件扫描,这对商城这种请求密集的系统有意义。migrate:run 是 TP6 的标准迁移命令,如果项目没有用迁移而是提供 install.sql,直接把 SQL 导入 MySQL 后再执行后面的初始化步骤。admin:init 的具体名字,likeshop 不同小版本差异很大,有的叫 create_admin,有的直接在安装引导页里完成,跑命令前先看 README,报 Command not found 就说明这一步应该用网页安装。chmod 与 chown 的目的是让 php-fpm 的 www-data 用户能写 runtime 目录里的缓存、日志和 session 文件,权限给到 775 就够了,不用 777。
伪静态配置如下,核心是让所有不存在的路径回到 index.php,ThinkPHP 6 才能按路由分发表解析。
server { listen 80; server_name shop.example.com; root /var/www/likeshop/public; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } } location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/run/php/php8.1-fpm.sock; } }root 必须指向 public,不是项目根目录,否则 thinkphp 的入口文件、静态资源路径全乱。rewrite 把真实不存在的请求交给 index.php 处理,s 参数里的$1会被框架作为 PATHINFO 解析。php 的 location 用正则匹配,优先级高于前缀 location,fastcgi_pass 的 socket 路径要与你机器上实际安装的 PHP 版本一致,8.1 写成 php8.1-fpm.sock,8.0 就改成 8.0。部署后先访问域名根,能打开说明入口没问题,再访问/admin看后台是否出来。后台 404 基本就是这段配置没生效,不是 thinkphp 路由写错。
2.4 后台初始化与数据表校验:改后台路径、核对支付配置
后台能打开只是第一步。likeshop 默认后台路径是 /admin,上线前把它改掉是最低成本的加固。多应用模式下,把 admin 应用目录重命名,或者用路由别名映射,两者都行。如果不想动代码,也可以在 Nginx 里做一层访问限制,只允许办公网 IP 访问 /admin。进入后台后,先不急着看商品,去系统设置里过一遍存储引擎、短信密钥、支付参数。默认的本地存储配置再好看,生产环境早晚要切到 OSS 或 COS,这个切换的字段在后台就能改,不用动代码。
数据表校验这块,单商户商城必然有用户表、用户令牌表、商品表、订单表、订单明细和支付回调记录这几类结构。likeshop 的表前缀常规是ls_,实际以你安装后生成的 SQL 为准。打开数据库把表扫一遍,重点确认订单表里是否有pay_status、pay_time、transaction_id,支付回调表里是否有原始报文和验签结果字段,这些字段在后续对账时缺一不可。如果发现表结构和你业务预期差异很大,先看是不是装错了版本,v3.03 是单商户版,别拿多商户的表结构来套。
3. 用 uniapp 前端跑通 H5、微信小程序与 App 三端
3.1 先分清 uniapp 工程是 HBuilderX 工程还是 CLI 工程
likeshop 的前端是一个 uniapp 工程,第一步不是到处找运行按钮,而是打开 manifest.json 和 package.json 判断它依赖什么运行环境。如果你在根目录看到了app-plus、mp-weixin这些目录,同时没有 node_modules,那它是 HBuilderX 工程,直接用 HBuilderX 导入后运行内置浏览器;如果根目录有 package.json 并且依赖里出现@dcloudio/vite-plugin-uni,那就是 CLI 工程,得先npm install。老版本 likeshop 的 uniapp 前端大多是 vue2 语法,直接用 vue3 的思维去改 option API 代码,反而会掉进this.$refs和生命周期差异的坑。如果要做 vue2 转 vue3,先看 package.json 里的依赖版本和公共组件写法,别指望一次性全量升级。
运行起来不代表多端都正常。HBuilderX 内置浏览器是 H5 环境,小程序编译还要另装微信开发者工具,App 端要真机或模拟器。mumu 这类 Android 模拟器在 HBuilderX 里选择“运行到手机或模拟器”就能识别,但模拟器里的定位、扫码硬件和真机不一样,别在模拟器里验证这些能力。
3.2 修改 api 基地址与 token 存储:一次封装同时适配三端
uniapp 的差异化在于 H5 有 localStorage、小程序有 wx storage、App 有本地文件,但表面 API 都是uni.getStorageSync、uni.setStorageSync,所以封装层必须统一用 uni 提供的方法。likeshop 前端的请求地址一般集中在 utils/request.js 或 config.js 里,只需要改一个 BASE_URL。下面这段是常见的请求封装骨架。
// utils/request.js const BASE_URL = 'https://shop.example.com/api'; export function request(path, options = {}) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + path, method: options.method || 'GET', header: { 'token': uni.getStorageSync('token'), 'content-type': 'application/json' }, data: options.data || {}, success: (res) => { // likeshop 接口以 code 字段表示业务状态,具体取值以实际联调为准 if (res.statusCode === 200 && res.data.code === 1) { resolve(res.data); } else if (res.statusCode === 401) { // token 失效统一回到登录页 uni.removeStorageSync('token'); uni.navigateTo({ url: '/pages/login/login' }); reject(res); } else { uni.showToast({ title: res.data.msg, icon: 'none' }); reject(res); } }, fail: (err) => reject(err) }); }); }header 里塞 token 是从 storage 同步读出来的,三端都能拿到;401 分支处理了后端踢出登录的场景,避免每个页面各自判断错误码。path 前面要带斜杠,BASE_URL 末尾不要带斜杠,否则拼接出双斜杠地址,部分 CDN 和网关会直接拒绝。code 的判断值,likeshop 有的版本用 1 表示成功,有的用 200,你拿到源码后先看接口真实返回再定,不要照抄任何网上教程。
3.2.1 合法域名与白名单
微信小程序正式环境要求所有请求域名都是 HTTPS 且备案,开发者工具里可以勾“不校验合法域名”用于调试,真机和上架前必须把shop.example.com换成真实域名并配到小程序后台的 request 合法域名里。H5 嵌入公众号时,JS-SDK 的签名要求当前页面 URL 必须与公众号后台配置的 JS 接口安全域名一致,最关键的是域名精确匹配,不能带路径通配,也不能用 IP。
3.3 H5 嵌入公众号获取定位:签名和 URL 是最大坑
热搜里“uniapp 开发 h5 嵌入微信公众号中获取定位”问的人很多。原因是 H5 页面的 uni.getLocation 走浏览器定位,在微信内置浏览器里会被提示无法获取地理位置信息。生产环境应该接公众号的 JS-SDK wx.getLocation,需要先拿到签名。签名接口在后端做,前端只取参数再调 wx.config。
// app/api/controller/Wechat.php 中的签名方法 public function jsSdkConfig() { // 关键:签名用到的 URL 必须是当前页面完整地址,且去掉 # 后的部分 $url = request()->header('referer'); $url = strstr($url, '#', true) ?: $url; $ticket = (new WechatService())->getJsTicket(); $params = [ 'noncestr' => uniqid(), 'jsapi_ticket' => $ticket, 'timestamp' => time(), 'url' => $url ]; ksort($params); $signature = sha1(http_build_query($params)); return json([ 'appId' => '公众号 appId', 'timestamp' => $params['timestamp'], 'nonceStr' => $params['noncestr'], 'signature' => $signature ]); }微信 JS-SDK 签名算法是固定步骤,所有参数先按字典序排序再拼成查询字符串,最后 sha1。jsapi_ticket要自己缓存,微信公众号接口有频率限制,建议把 ticket 存 Redis 并加上 7000 秒过期。url 参数必须与前端调用 wx.config 时location.href去掉 # 后的值一字不差,很多人签名失败都是因为保留了 hash 或者多了结尾斜杠。前端拿到参数后在页面里调 wx.config,再在 wx.ready 里执行 wx.getLocation,定位坐标再交给高德做逆地理编码,likeshop 的收货地址、门店距离这类功能就能跑通。
3.4 微信小程序与安卓/iOS 打包的 4 个必调参数
uniapp 打包的参数大多在 manifest.json 的可视化界面里,改成源码模式也可以直接编辑。下面 4 个参数是上架和真机验证时最容易漏的。
| 参数 | 配置位置 | 说明 |
|---|---|---|
| 小程序 appid | manifest.json -> mp-weixin | 填真实 appid,测试号无法提交审核 |
| Android 包名 | manifest.json -> App 模块 -> Android | 反向域名格式,例如 com.example.shop,应用市场以此识别应用 |
| iOS Bundle ID | manifest.json -> App 模块 -> iOS | 需要与开发者后台创建的 App ID 完全一致 |
| 统计模块 | manifest.json -> modules 勾选 | 报“打包时未添加 statistics 模块”就回来勾 |
特别说明“修改刚进入的加载页面”:小程序冷启动的加载页不是 App.vue 控制的,优先看 pages.json 里配置的第一个页面和页面自带的 onLoad 跳转逻辑,以及自定义启动图设置。安卓上架应用市场必须用正式签名,HBuilderX 云打包会在生成 keystore 时让你设置口令,这个口令一旦丢了,后续所有更新包都无法覆盖安装,一定要做双人备份。iOS 打包只能通过 mac 的 Xcode 或云端打包,.p12 证书与描述文件里要带推送权限。公众号里做自定义分享的,注意微信分享要求的通用链接也需要在苹果开发者后台配置,这跟 uniapp 端没有关系,是微信开放平台和苹果侧的联动。
4. 二开与多端联调:支付回调、扫码和常见报错的排查路径
4.1 在 thinkphp 里给单商户追加一个接口的常规做法
likeshop 后端二开的核心套路是:控制器只做参数接收和返回,业务封装在 service 层,数据操作通过模型或 Db。这样后台管理端和用户端接口可以共同调用同一套业务方法。举个例子,要给商品详情加一个“查询库存和划线价”的接口,常规写法如下。
// 文件位置:app/api/controller/Goods.php <?php namespace app\api\controller; use think\facade\Db; class Goods extends Base { public function stock() { $id = (int) $this->request->param('id'); if (!$id) { return json(['code' => 0, 'msg' => '参数缺失']); } $goods = Db::name('goods') ->where('id', $id) ->field('id, name, stock, line_price') ->find(); return json(['code' => 1, 'data' => $goods]); } }Db::name('goods')在配置了prefix=ls_时自动转成ls_goods,你写 SQL 时不能自己再加前缀。field里只取页面实际要用的字段,商城商品表字段常多达几十个,SELECT 全字段会让接口响应和网络带宽双输。(int)强转参数是为了防止把数组传进来触发 thinkphp 的参数绑定异常。如果要做订单闭环,控制器到这里就该收手,库存扣减和订单生成必须挪到 service,让后台和接口共用同一套事务逻辑。
4.2 uniapp 扫码、蓝牙与 NFC 的接入边界
likeshop 的核销、优惠券场景常要扫码。uni.scanCode 在小程序和 App 端可用,在 H5 端不可用,所以网页端核销只能接微信 JS-SDK 的扫一扫,这是第一层认知。很多人反馈“scancode 扫码扫出来是一串数字”,这不一定是 bug。微信扫同一张码如果是链接,uniapp 扫出来反而是一串数字,首先要确认码内容本身是文本还是网址。码制上要区分 QR Code 和常见的条形码,uni.scanCode 默认能处理多种码制,但某些加密二维码会扫码结果异常。
蓝牙打印、NFC 读卡这类能力在 uniapp 上必须走原生插件机制,HBuilderX 打包时勾选对应 module,否则运行到真机会报“未添加模块”。uniappx 和 uniapp 的区别也在这里:uniappx 面向更底层的新平台,插件体系不通用,likeshop 前端的组件和插件不要尝试直接改后缀硬套。接入这些原生能力之前,先在 manifest.json 的模块列表里核对权限,再决定是找现成插件还是写原生扩展。
4.3 伪静态、runtime 权限与 thinkphp 安全基线
thinkphp 项目在 Nginx 上最常见的两个状态是“后台 404”和“保存后 500”。前者是伪静态没配,后者往往是 runtime 目录权限不足,用ls -ld runtime看是不是属于 www-data。生产中必须把 APP_DEBUG 设为 false,debug 开着时 thinkphp 会把 SQL、文件路径、环境配置都打进错误页,等于把部署情报直接交给访问者。
对 thinkphp 这类知名框架,安全通告里多次提到的是缓存文件投递和参数反序列化,防御思路不是停止使用,而是把暴露面收窄。至少做三步:一是把.env挡在 Web 根之外访问,二是后台路径改名或做 IP 白名单,三是支付回调必须验签。回调验签必须在 notify 应用里做,用支付平台下发的公钥验完签名再更新订单状态,不能只看请求来源 IP。Nginx 的最小屏蔽配置如下。
location ~* ^/(runtime|backup|\.env) { deny all; } location ~ \.php$ { # 保持原有 include、fastcgi_param 等配置 include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/run/php/php8.1-fpm.sock; }正则 location^/(runtime|backup|\.env)匹配以 runtime、backup 或 .env 开头的 URL,直接 deny all 返回 403,阻断的是通过 URL 读取配置和缓存的路径。注意这个配置放在 server 块里,与伪静态并存。fastcgi_param后的 SCRIPT_FILENAME 要确认$document_root与 root 指令一致,不然 PHP 文件路径拼错。
4.4 联调时最容易混淆的三类错误
| 场景 | 现象 | 排查点 |
|---|---|---|
| H5 接口正常、微信真机接口失败 | 请求能发出但签名报错 | 公众号后台 JS 安全域名与当前 URL 不一致 |
| 小程序请求 200 但业务 code 不是成功 | token 已带上仍提示未登录 | 自定义请求头被网关过滤,检查 header key 大小写 |
| App 打包后全部接口失败 | 页面空白,网络报错 | Android 9+ 默认禁 HTTP,需开 cleartext 或用 HTTPS |
联调的正确姿势是先做一个最小复现页面:页面里什么都不放,只调一个接口,把 network 面板里的完整请求头、响应头和响应体抄下来对比。likeshop 后端是否接收到 token,可以通过 thinkphp 日志查看,把 runtime/log 里的当日日志 tail 出来看请求入口和参数。大多数联调问题都不是 likeshop 本身的问题,而是开发工具和真机环境的差异,比如开发者工具里勾了不校验域名后一片大好,真机上一关就露出原形。
5. 给 likeshop 的二次开发加一层 SQL 监听:用 thinkphp 的 Db::listen 定位慢查询
5.1 监听代码加在哪里
网上搜“thinkphp 监听 sql 的代码一般添加在哪里”,答案集中在两个位置:项目根目录的 app/common.php,或者 app/provider.php 的服务提供者注册方法里。common.php 会被框架在初始化时自动加载,写在这里最简单;provider.php 适合和容器绑定、事件订阅一起做。对 likeshop 这种应用目录比较多的项目,放在 app/common.php 一处注册,全应用生效。
5.2 只打慢 SQL 的监听器
// app/common.php 追加到文件末尾 use think\facade\Db; use think\facade\Log; Db::listen(function ($sql, $time) { // $time 单位是秒,只记录超过 1 秒的执行语句 if ($time > 1.0) { Log::warning(sprintf('[SQL慢查询] %.2fs | %s', $time, $sql)); } });Db::listen 是 ThinkPHP 6 的事件钩子,任何通过 Db 门面或模型执行的 SQL 都会触发回调,$sql是带参数绑定的最终语句,$time是执行时间。1.0 秒这个阈值在本地开发可能太宽松,建议本地用 0.5 秒抓得更直接;生产环境如果磁盘 IO 紧张,阈值提到 2 秒并且把日志通道指向独立文件,避免和业务日志抢写。
5.3 配合请求标识定位并发订单问题
慢 SQL 看多了会发现,单条查询慢不一定说明问题,订单高峰期的死锁和行锁等待才是大头。在监听器里区分读写,再结合订单号去 MySQL 侧确认锁等待。
Db::listen(function ($sql, $time) { if ($time <= 1.0 && strpos($sql, 'INSERT') === false && strpos($sql, 'UPDATE') === false) { return; } $traceId = request()->param('trace_id', 'unknown'); Log::warning(sprintf('[SQL诊断] %s | %.2fs | %s', $traceId, $time, $sql)); });这样上线前压测时,写操作单独成一条日志,INSERT 和 UPDATE 又慢又集中在订单表,就去查 MySQL 的 innodb 锁等待。likeshop 的商品库存预扣和订单生成如果都在同一个事务里,把事务里的 SQL 逐条打出来,往往能看到某条 UPDATE 在等之前事务的锁。定位到具体语句后,再决定是加索引还是减少事务里的无关查询。这一层监听在正式环境建议作为临时手段,问题定位完就关,长时间开着会放大日志写入带来的额外开销;同时确认 runtime 日志目录没有写满,否则日志本身会变成商城首页的瓶颈。
本文还有配套的精品资源,点击获取