1. 项目全貌与框架选型思路
做潇湘知茶这个茶叶商城小程序之前,我其实纠结了很久技术选型。市面上的商城系统模板不少,但真正做起来,你会发现茶叶这个品类有它自己的脾气——不是简单的“商品上架+购物车+下单支付”三件套就能糊弄过去的。茶叶讲究产地、山头、年份、工艺,客户买的是“信任”二字,所以商品信息展示要和内容运营深度绑定。正因如此,最终定下来的方案是:后端双框架并行,ThinkPHP负责老业务系统的兼容与日常后台管理,Laravel负责小程序API层的全新开发,小程序端用uniapp跨端编译成微信小程序。
这个标题里同时出现了ThinkPHP和Laravel,很多人第一反应是“一个项目为什么用两个框架?这不是给自己找事吗?”我当初也有这样的疑问,但深入梳理需求之后才明白,这其实是很多传统电商项目都会面临的“历史包袱与新技术诉求并存”的局面。老的管理后台、订单处理逻辑、ERP对接等都是基于ThinkPHP 3.2写的,运行了好几年,资料和人员都比较熟悉,贸然全部推翻成本太高;而小程序端面对的是移动互联网用户,要求接口规范、响应快速、便于后期迭代,Laravel在中间件、队列、缓存、ORM方面的设计更合适。两个框架各司其职,反而比“死撑一个框架硬啃到底”更划算。
1.1 为什么是“茶叶+小程序”这个组合
茶叶消费的核心场景是“熟人推荐”和“内容种草”。长辈喝什么茶、朋友送什么礼盒、某个山头的春茶上了没有——这些决策链路上,社交关系占的比重远高于算法推荐。小程序恰恰长在微信这个社交土壤里,分享、拼团、分销都有天然优势,不需要额外下载App,扫码即用,用完即走,特别契合茶叶这类低频但强信任的消费品。
同时茶叶的客单价不低,尤其是礼盒装、收藏级普洱这类产品,用户在下单前往往会反复查看茶叶产地介绍、冲泡视频、用户评价。小程序的页面加载速度、图片缓存机制、视频播放体验,直接决定了用户的信任感。基于这些原因,潇湘知茶最终把小程序作为核心交易前端,PC端和公众号H5作为补充入口,后台统一由双框架后端支撑。
1.2 ThinkPHP与Laravel双框架的分工逻辑
很多同行问我,既然要用Laravel,为什么老系统还在跑ThinkPHP?我的回答是:“能用”和“好用”之间,差一个系统的演进节奏。老系统里的商品管理、库存管理、供应商结算、快递单打印这些功能,都已经跑得很稳定了,而且存在大量Excel导入导出、复杂的报表统计逻辑,直接迁移到Laravel要付出不小的测试代价。但如果让小程序API也走老框架,又会面临接口规范不统一、缓存机制薄弱、扩展性不足的问题。
所以我采取了“双轨制”:ThinkPHP服务端继续维持原有PC商城后台和内部管理系统,Laravel服务端专注于小程序API,两边的数据通过中间库和消息队列做同步。比如商品基础信息在ThinkPHP后台维护,修改后同步到Laravel的Redis缓存和MySQL表;订单数据在Laravel侧落地,再通过队列回调回写ThinkPHP老系统的结算报表。这样既保证了老系统的稳定,又让新业务能够轻装上阵。
2. 商城核心模块与数据设计
2.1 茶叶商品体系与SKU设计
茶叶商品的SKU设计和普通服装、数码产品不太一样。一件T恤的SKU是颜色加尺码,而一饼普洱茶的SKU则需要考虑年份、规格(357克饼、200克砖、100克沱)、包装形式(散茶、礼盒、茶饼)、甚至山头批次。如果直接把所有维度都塞进一个SKU表,后期维护会非常痛苦。
我当时的做法是采用“SPU + 多维度属性 + SKU”三层模型:
- SPU表(商品表):存茶叶名称、品牌、产地、制作年份、工艺类型、茶叶品种等公共信息。比如“潇湘知茶·古丈毛尖2024明前特级”是一个SPU。
- 属性表:用JSON字段存规格维度,例如
{"规格": "250g", "包装": "礼盒装"}。 - SKU表:存具体价格、库存、货号、条码。对应“250g礼盒装古丈毛尖”这个具体可售商品。
表结构大体如下:
CREATE TABLE `product_spu` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL COMMENT '商品名称', `subtitle` varchar(255) DEFAULT '' COMMENT '副标题', `category_id` int(11) NOT NULL COMMENT '分类ID', `origin_place` varchar(100) DEFAULT '' COMMENT '产地', `tea_type` varchar(50) DEFAULT '' COMMENT '茶叶品类:绿茶/红茶/黑茶/白茶等', `craft_year` int(4) DEFAULT NULL COMMENT '制作年份', `description` text COMMENT '商品详情(富文本)', `status` tinyint(1) DEFAULT '1' COMMENT '上架状态', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='茶叶商品SPU表';CREATE TABLE `product_sku` ( `id` int(11) NOT NULL AUTO_INCREMENT, `spu_id` int(11) NOT NULL COMMENT '所属SPU', `attrs` json NOT NULL COMMENT '规格属性,如{"规格":"250g","包装":"礼盒装"}', `price` decimal(10,2) NOT NULL COMMENT '售价', `market_price` decimal(10,2) DEFAULT NULL COMMENT '市场价/划线价', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存', `sku_code` varchar(64) DEFAULT '' COMMENT 'SKU编码', `barcode` varchar(64) DEFAULT NULL COMMENT '条码', `image` varchar(500) DEFAULT '' COMMENT '规格图', PRIMARY KEY (`id`), KEY `idx_spu` (`spu_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU表';这套设计的核心好处是,同一个SPU下面的SKU可以自由组合,管理端只需要维护SPU的公共描述和图片,SKU只补充差异化信息,数据冗余少,改动灵活。比如古丈毛尖要做一款“五一劳动节限定礼盒”,只需要新增一个规格维度,不影响原商品数据。
2.2 订单状态机与购物车逻辑
商城系统的订单状态容易写成一团乱麻,尤其是茶叶商城涉及多个子订单发货的情况。用户买一份春茶预售,再买一盒即发礼盒,这两种商品很可能需要分批发货、分批结算,订单状态就不能只用“待付款、待发货、待收货、已完成”这四态粗暴管理。
我当时把订单拆成“主订单 + 子订单”两层,主订单管总金额、总状态、用户信息,子订单对应单件商品的物流和售后流程。状态流转图虽然不画图,但定义得很明确:
- 主订单状态:待付款 -> 已付款/配货中 -> 已完成 / 已取消 / 已退款
- 子订单状态:待发货 -> 待收货 -> 已完成 / 售后中
购物车这个模块看起来简单,但真正做的时候容易踩坑。茶叶用户经常搞“批量加购”,比如一口气加3盒黑茶礼盒、2罐红茶、1套茶具,购物车里又有数量增减、规格切换、失效商品提示。我的建议是购物车持久化到服务端,而不是只存本地Storage,因为用户可能在小程序下单、在H5端补单,跨端同步很重要。另外,购物车商品数量要实时校验库存,前端展示参考价,后端在下单接口再次校验,防止超卖。
2.3 会员体系与分销裂变
茶叶商城的复购核心是会员体系和口碑裂变。潇湘知茶小程序上线了分级会员:普通会员、银卡会员、金卡会员、黑金会员。升级条件主要看累计消费金额,不同等级享受不同折扣和积分倍率。积分能换茶样、兑换优惠券、抵扣现金,这样把一次性买家慢慢转化为长期茶友。
分销功能是茶叶商城比较见效的玩法。用户A分享小程序给好友B,B下单后A可以获得佣金返还。这里需要注意安全性和合规性,不能搞多级分销和金字塔模型,只做一级佣金。实现上,我在Laravel API层写了一个InviteRecord表,记录邀约关系,用户支付成功后通过队列异步给邀请人发放佣金。佣金可以提现到微信零钱,通过企业付款到零钱接口实现。
3. 双框架协同的落地实践
3.1 API层与业务层的边界划分
Laravel端的核心职责是“对外输出标准API”,所以我在项目里采用了restful api风格设计,配合资源控制器和表单请求验证(FormRequest)。这样前后端可以完全分离,小程序端只需要关注接口契约,不必关心后端是Laravel实现还是ThinkPHP实现。
接口设计上,我统一返回结构:
{ "code": 0, "message": "success", "data": { "list": [], "total": 100 } }小程序端封装了统一的request方法,任何接口异常都能弹toast提示。API版本控制在URL中加/api/v1/前缀,后期即使升级大版本,老版本小程序也不会因为接口变更直接挂掉。
3.2 中间件与登录态设计
Laravel的中间件是这个项目的点睛之笔。微信小程序每次调用后端接口都会携带token,我写了两个核心中间件:CheckToken(校验登录态)和RefreshToken(自动续期)。
CheckToken中间件的实现思路是:
- 从请求Header中读取
Authorization: Bearer {token}。 - 解码token,取得用户ID和过期时间。
- 判断Redis中是否存在对应的session信息,如果不存在则返回401。
- 如果存在,将用户信息注入到Request实例中,供后续控制器使用。
namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redis; class CheckToken { public function handle(Request $request, Closure $next) { $token = $request->bearerToken(); if (!$token) { return response()->json(['code' => 401, 'message' => '未登录'], 401); } $userId = Redis::get('user_token:' . $token); if (!$userId) { return response()->json(['code' => 401, 'message' => '登录已过期'], 401); } $request->merge(['user_id' => (int)$userId]); return $next($request); } }这里有个细节:token在Redis里的有效期一般是7天,但用户频繁操作时每次只校验不续期,体验不太好。所以RefreshToken中间件会在token剩余有效期低于2天时,自动延长7天,小程序端无感知。实际跑下来,用户掉线率明显降低。
3.3 兼容与迁移:老代码往新架构过渡
如果只把老数据表直接搬到新框架,后面会越来越别扭。我在迁移过程中做了几个关键动作:
- 字段统一:老ThinkPHP系统中的字段有的是驼峰,有的是下划线,统一改成下划线命名。
- 时间戳统一:Laravel默认管理
created_at和updated_at,老数据里没有的字段补上。 - 软删除:老系统商品删除是物理删除,容易误操作且无法追溯。迁移到Laravel后全部使用
SoftDeletestrait,只做标记删除。 - 关联关系:ThinkPHP的关联写法比较直白,Laravel用Eloquent模型关联反而更优雅。比如订单模型可以直接
$order->items拿到子订单,$order->user拿到买家信息,代码可读性高了不少。
很多团队在双框架切换时失败,不是技术不行,而是数据迁移准备不足。我踩过的坑是:老系统里历史订单的status字段是老枚举值(如0表示未付款),新系统用的是字符串枚举(如pending_payment),如果不做映射转换,小程序端拿到旧数据就会显示异常。解决方案是写了一个数据清洗脚本,分批处理历史订单,把枚举值映射到新体系。
4. 小程序端的关键实现
4.1 技术栈选择:原生还是uniapp
小程序端开发我最终选了uniapp,原因很简单:团队熟悉Vue语法,而且后期如果要做抖音小程序、支付宝小程序,uniapp可以一套代码多端编译,节省重复开发成本。但uniapp有些组件和原生小程序的行为不完全一致,比如scroll-view内部使用日期选择器时可能滚动冲突。这类问题还是要靠实际调试解决,不能完全依赖框架的默认行为。
如果你只想专注微信生态,原生小程序开发其实也不错,性能和原生组件的控制力更强。但考虑到潇湘知茶未来可能需要App端和H5端,uniapp的性价比更高。小程序端的代码结构大致按页面维度划分:首页、分类、购物车、个人中心、商品详情、订单列表、订单详情、分销中心、积分商城等。
4.2 商品展示与搜索加载优化
茶叶商城的小程序端图片资源非常多,尤其是详情页的茶山实拍图、冲泡视频。为了不拖垮加载速度,主要做了三件事:
- 图片懒加载:页面滚动时才加载可视区域的图片。小程序源头像TP-UI、uView都有懒加载组件,直接用就行。
- CDN加速:所有上传的图片统一走CDN,并将原图压缩为webp格式,按需生成不同尺寸的缩略图。列表页用大图300px、详情页用原图,避免一次加载几兆的大图。
- 接口分页:商品列表接口默认只返回20条,上拉触底时自动加载下一页,减少首屏压力。
搜索功能不能只做SQL的LIKE %关键词%,这样商品多了性能会很差。我当时在Laravel端引入了scout和TNTSearch做本地全文索引,虽然数据量不大,但搜索速度确实比LIKE快很多,支持关键词高亮和分词。
4.3 支付回调与订单流程闭环
微信支付接入是商城项目的必修课。小程序端调用wx.requestPayment拉起微信支付,服务端通过预下单接口拿到prepay_id后返回给前端。支付回调我采用了Laravel的队列处理——支付回调里只做通知验签和订单状态标记,后续的库存扣减、积分发放、消息通知等操作全部丢到队列里异步执行。这样即使某个环节失败,也能通过重试机制保证最终一致性,不会因为一次回调超时导致用户付款了但订单还是待付款。
回调处理最核心的一点:必须校验签名和订单金额。不能只看resultCode是SUCCESS就更新订单,要核对回调里的totalFee和订单表中的金额是否一致,防止意外情况。
5. 常见问题排查与避坑记录
5.1 ThinkPHP版本兼容与历史漏洞
标题热搜词里提到了“thinkphp 3.2 版本兼容php8”,这个我太有体会了。老ThinkPHP项目原本跑在PHP 5.6上,后来服务器升级到PHP 7.4就出了不少兼容问题,更别说PHP 8。each()函数在PHP 8中已被移除,count()对非数组传参会抛异常,这些老代码里遍地都是。我的做法是:
- 先做全量代码扫描,找出废弃函数的使用点。
- 写一个兼容层函数文件,把老函数重命名或封装兼容。
- 升级前在测试环境完整回归一轮,尤其是后台的Excel导出和订单打印。
另外,ThinkPHP历史上出过不少远程代码执行漏洞,公共互联网上经常有人扫描旧框架站点。只要你用的是老版本,又没及时打补丁,很容易被批量攻击。我们在Nginx层做了IP访问限制和管理后台独立域名处理,同时在框架入口处加了请求过滤和函数禁用白名单,从源头上降低风险。如果预算允许,强烈建议后期把老系统逐步迁移到Laravel或新版ThinkPHP,安全维护成本会低很多。
5.2 Laravel中间件与跨域问题
做小程序API时,跨域问题一开始没太注意,因为小程序端wx.request默认不受浏览器同源策略限制。但后期增加了H5管理端调试和PC端入口后,CORS报错就冒出来了——“Access to XMLHttpRequest at ... has been blocked by CORS policy”。解决方案是写一个全局中间件处理CORS头,允许指定域名访问。
class CorsMiddleware { public function handle($request, Closure $next) { $origin = $request->header('Origin'); $allowedOrigins = ['https://admin.example.com', 'https://h5.example.com']; if (in_array($origin, $allowedOrigins)) { header('Access-Control-Allow-Origin: ' . $origin); header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE'); header('Access-Control-Allow-Headers: Content-Type, Authorization'); } if ($request->isMethod('OPTIONS')) { return response('', 204); } return $next($request); } }这里有个坑:预检请求(OPTIONS)如果不直接返回204,请求会一直挂起。很多新手在这里卡一整天。另一个容易犯的错是,CORS中间件必须注册到全局中间件里,不能只放到路由组中间件,否则跨域请求根本走不到路由层就挂了。
5.3 小程序生态里的坑
小程序开发踩过的坑比较多,挑几个有代表性的说说。第一个是“支付能力对应的商户号异常”,新注册的小程序需要先申请微信支付商户号并完成关联绑定,否则wx.requestPayment会报“商户号参数不正确”或“支付能力已被限制”。这个不是代码问题,是运营资质问题,需要提前去微信支付商户平台完成产品开通和结算账户验证。
第二个是“小程序动态设置标题”。很多页面需要根据数据动态设置顶部标题,比如商品详情页显示茶叶名称,但是微信小程序的导航栏标题有长度限制,而且自定义导航栏场景下页面层级很复杂。我封装了一个工具方法,在onLoad里根据接口返回的title字段调用uni.setNavigationBarTitle(),同时考虑标题超过20个字的截断逻辑,避免标题显示不全。
第三个是“文件下载和导出Excel”。后台需要一个订单导出功能,小程序端接收返回的文件流,用uni.downloadFile下载后用uni.openDocument打开预览。这里要注意微信小程序的下载域名白名单配置,还有iOS和Android对文件类型的支持差异,PDF和Excel一般没问题,但如果是CSV文件,iOS可能无法直接预览,最好后端统一输出为xlsx格式。
6. 安全、性能与运营层面的经验沉淀
6.1 接口安全加固的几个细节点
电商类小程序最怕的就是接口被刷、数据被爬。我在Laravel端做了几层防护:
- 签名校验:小程序端请求头带上
timestamp和签名sign,服务端用密钥计算签名并比对,防止请求被篡改。 - 频率限制:使用Laravel自带
throttle中间件,登录接口和短信验证码接口限制每分钟调用次数。 - 参数过滤:所有用户输入都经过Laravel验证器过滤,商品数量、价格等敏感字段以后端为准,前端传的金额直接忽略。
- 反爬策略:商品详情接口对频繁访问的IP做临时封禁,微信小程序端通过UnionID维度限制批量抓取行为。
安全这东西,平时看着没用,一旦线上被攻击一次,付出的代价足够让团队把这套机制补个遍。与其亡羊补牢,不如一开始就设计进去。
6.2 性能优化:从数据库到Redis缓存
茶叶商城商品的详情页访问量最高,而详情页包含富文本、多张图片、关联推荐等数据。如果每次都从数据库查,压力很大。我的方案是:
- 商品详情接口首先查Redis缓存,命中直接返回。
- 没命中就从数据库查询,组装好数据后写入Redis,并设置10分钟过期时间。
- 管理后台修改商品上下架、价格、库存后,主动删除相关缓存。
- 首页的轮播图、活动位、热销榜单,通过定时任务每5分钟刷新一次缓存。
实际运行下来,接口平均响应时间从原来的300ms降到60ms左右,大部分时间花在传输图片URL和JSON序列化上,数据库压力大幅下降。订单相关接口保持实时查询,因为订单状态需要强一致,不能缓存。
6.3 运营数据埋点与用户画像
一个小程序商城的“后劲”取决于能不能看懂用户行为。我在小程序端接入了自定义埋点,收集核心事件:扫码进入、搜索关键词、商品停留时长、加购、提交订单、支付成功、分享。数据上报到后端统一打点接口,写入ClickHouse或MySQL分析表。运营人员可以在后台看到哪些茶叶品类最受欢迎、用户在哪个页面流失最多、哪个渠道带来的新客转化率最高。
这些数据听起来高大上,但哪怕是基础版的埋点统计,也已经足够帮助运营做决策了。比如我们发现“古丈毛尖”详情页的跳出率高,后来通过分析评论区发现是用户觉得价格偏贵,运营随即增加了同品类入门级茶样链接,跳出率明显下降。
7. 一些个人体会与扩展思路
做潇湘知茶这个项目,让我感触最深的一点是,技术选型没有绝对的“最好”,只有“当前阶段最合适”。ThinkPHP和Laravel同时存在,表面上看起来有点“违和”,但实际落地中,老系统不用推翻重来,新业务又能轻装上阵,团队的开发效率和系统稳定性都得到了保证。对于茶叶这类垂直品类电商,业务模型并不复杂,真正拉开差距的是对商品的理解、对用户信任感的维护以及围绕小程序社交链路的精细化运营。
如果你正准备从零搭建一个小程序商城,我的建议是先把商品模型和订单状态机设计清楚,这决定了后期扩展的灵活度。再花时间梳理清楚支付回调的闭环逻辑,不要把库存扣减和积分发放放在回调主流程里。最后,安全意识和性能意识要从第一天就建立起来,不然线上出问题的时候再补,代价会高得多。
这个项目的后续扩展空间还很大。比如可以在小程序里加入茶友社区和圈子,做内容带货和直播卖茶;也可以接入企业微信,把高复购用户迁移到私域流量池,做会员日专属活动和茶山游学等线下体验服务。商城只是一个交易入口,茶叶品牌的长期价值还是要靠内容和口碑沉淀下来。希望我踩过的这些坑和总结的经验,能帮你少走一段弯路。