☰
ThinkPHP+Vue前后端分离打造茶园茶农文化交流平台
2026/10/14 10:00:51 网站建设 项目流程

去年年初接到一个挺有意思的项目:给某产茶区的农户搭一套文化交流平台。需求方说得很直白,茶农们分散在各个山头,种茶经验、制茶手艺、茶叶行情全靠熟人圈子口口相传,新入行的年轻人想学点东西都找不到入口。就想做一个线上平台,把茶园、茶农、茶产品、茶文化这几块串起来。技术栈没太多纠结,后台用ThinkPHP,前台用Vue,前后端分离。这个组合在国内中小型项目里太成熟了,资料多、上手快、部署成本低,我折腾两个月就把平台从0到1捋顺了,上线后跑得很稳。这篇文章就把这段项目经历完整拆一遍,从需求拆解、表结构设计、接口开发,到前端联调、上线部署和坑位记录,尽量把能直接抄作业的部分都写出来。如果你正打算拿这个标题做课程设计或者毕设,或者准备接类似的农业垂直领域项目,可以少走不少弯路。

1. 项目定位与需求拆解

1.1 平台要解决的核心问题

先说说背景。一个产茶区的茶园、茶农数量其实不少,但信息极度分散:哪家茶园可以参观、什么季节能体验采茶、某个品种怎么种植养护、今年鲜叶什么价格,这些信息基本没有线上渠道,全靠在村里打听或者熟人介绍。年轻人外出打工的多,老一辈的制茶经验面临断档。茶园茶农文化交流平台要做的事,本质上就是给这群人提供一个“线上村口”加“线上展厅”。

用大白话讲,它要解决三件事:

  • 让茶园信息可被看见:茶园的实景照片、位置、面积、海拔、主栽品种都能展示出来,游客和采购商可以直接找上门。
  • 让茶农经验能流动:通过发帖、回帖的方式,把种植、采摘、炒制、存储这些实用经验沉淀下来,新手随时能检索到。
  • 让茶叶产品有稳定出口:茶农上架自己的茶叶,游客能按品种、价格、产地筛选,把中间环节尽量压缩。

系统本身不搞复杂交易闭环,重点是信息展示和社区交流,交易可以在线下撮合。这也是后面所有表结构和功能设计的前提。很多做这个题目的同学一上来就想着做商城,把购物车、支付、优惠券都往上堆,其实偏离了“文化交流平台”的定位,最后做出来四不像。先把定位想清楚,后面设计就会很顺。

1.2 用户角色与核心功能映射

平台用户分成三类,每一类的诉求差别很大。我在需求阶段直接把角色、诉求和功能模块映射成了一张表,后面所有开发工作都围绕这张表展开:

用户角色核心诉求对应功能
普通游客了解茶园情况、看茶文化内容、选茶买茶茶园浏览、社区阅读、茶叶检索与筛选
注册茶农展示自家茶园、分享经验、发布产品茶园资料维护、发帖回帖、产品上架
平台管理员把控内容质量、维护用户秩序内容审核、用户管理、数据统计

游客不需要登录就能看大部分内容,只有发帖、评论、收藏这类互动操作才要求登录,这个设计能降低门槛,也更符合大家的浏览习惯。茶农是内容的生产者,注册后默认有“茶农身份”,可以在个人中心维护茶园信息并上架茶叶。管理员在后台对所有用户发布的内容做审核,通过状态位控制是否在前台展示。

实际开发里,这三种角色对应到代码就是user表里的role字段,接口层配合中间件做权限校验。不过每种权限的差别不只是字段值那么简单,具体怎么实现,我放到第3章详细讲。

2. 技术选型与整体架构

2.1 为什么选ThinkPHP + Vue这对组合

现在前后端分离已经不算什么新鲜思路,但放到“毕设、个人项目、中小型落地”这个场景里,选型真的要实用主义一点。

先说后端。ThinkPHP这套框架在国内用了十几年,生态成熟,文档全中文,上手门槛低。对比Spring Boot那一套,ThinkPHP写一个小型内容平台,启动速度和代码量都友好得多。它自带的Db类和ORM模型,对单表增删改查、多表关联、分页查询的支持很顺滑,做API接口效率非常高。尤其是项目做到后期,你会发现ThinkPHP的路由定义和中间件机制够用且不繁琐,调试起来也简单。

再说前端。为什么不用ThinkPHP自带的模板引擎直接渲染页面?因为这类平台有大量列表、详情、表单交互,还有个人中心这种状态较多的页面,用Vue做组件化开发,体验和开发效率都明显更好。我当时用的Vue 3加Vite,配合Vue Router、Pinia、Axios,这是目前Vue生态里最主流的一套。组件化带来的好处是,茶园卡片、帖子列表、产品卡片这些UI块可以被反复复用,比如首页的茶园卡片、列表页的茶园卡片、收藏页的茶园卡片,同一套组件改几行props就能用起来。

有同学可能会问,要不要上微服务、要不要把Redis、消息队列都堆上?我的建议是看需求体量。一个文化交流平台,核心场景是读多写少,把MySQL索引建好、关联查询优化好,比引入一大堆中间件更实际。框架只是工具,把东西做成、做好用,才是第一位的。

2.2 前后端分离的目录结构设计

项目分成了两个独立工程:后端think目录、前端vue目录。平时联调时前端通过开发代理转发到后端,上线时打包成静态文件丢到nginx里,后端接口也由nginx做反向代理。这算是前后端分离项目的标准形态。

前端目录大致是这样的:

vue/ ├─ public/ # 静态资源 ├─ src/ │ ├─ api/ # 接口请求封装 │ ├─ assets/ # 样式、图片 │ ├─ components/ # 公共组件 │ ├─ router/ # 路由配置 │ ├─ stores/ # Pinia状态管理 │ ├─ views/ # 页面组件 │ └─ App.vue ├─ package.json

后端目录则遵循ThinkPHP的多应用模式,我按模块划分了controller、model、validate、middleware、service:

think/ ├─ app/ │ ├─ controller/ # 控制器:接收请求并返回JSON │ ├─ model/ # 模型:处理数据表读写 │ ├─ validate/ # 验证器:参数合法性校验 │ ├─ middleware/ # 中间件:登录校验、CORS等 │ └─ service/ # 业务逻辑层:复杂业务拆解 ├─ route/ # 路由定义 ├─ config/ # 配置目录(数据库、缓存、上传等)

把业务逻辑从控制器中抽到service层,是我个人比较推荐的做法。很多初学者图省事把所有代码都堆在控制器里,一开始没什么,一旦发帖功能要同时处理内容校验、图片处理、敏感词过滤、通知推送,控制器就会变成几百行的大杂烩,后面改一个需求都得翻半天。拆成service层之后,每个方法只负责一件事,排查问题的定位速度快很多。

2.3 数据库表设计:核心实体与关键字段

数据表设计决定项目能走多远,我前后优化过两版,核心思想是“把实体理清楚,再做关联”。

这个平台的核心实体有六个:用户、茶园、社区帖子、评论、茶叶产品、分类。在此基础上扩展出收藏表和点赞表。

user表关键字段:

字段说明
id主键
nickname昵称
phone登录账号
password加密后的密码
avatar头像URL
role用户角色,0游客/1茶农/2管理员
status状态,1正常/0禁用

tea_garden表关键字段:

字段说明
id主键
user_id关联user表
name茶园名称
cover封面图
location详细地址
altitude海拔高度
area种植面积
variety主栽品种
description茶园介绍
audit_status审核状态,0待审/1通过/2驳回

thread表是社区动态,包含title、content、images、user_id、view_count、like_count、comment_count、status等字段。comment表用thread_id关联帖子,再加上parent_id表示是否为回复某条评论。product表里比较重要的是category_id、cover、images、price、stock、status,status用来控制上下架。

所有内容表我都加了软删除字段delete_time,避免用户发错内容后“彻底消失”带来的麻烦,管理员在后台也能恢复误删数据。另外,view_count这类统计字段直接冗余在主表上,查列表时不用做count子查询,性能能好不少。这个细节如果你不说,很容易被忽略,等数据量上来之后差距会非常明显。

3. 核心功能模块设计与实现

3.1 用户认证与权限控制:JWT令牌与中间件拦截

用户认证这块是前后端分离项目的标配。后端在用户登录成功后返回一个JWT令牌,前端把它存在本地,每次请求在Authorization头里带上,后端中间件校验令牌并解析出用户身份。

ThinkPHP实现这个很直接。先装好firebase/php-jwt这个库,登录接口里生成token:

use Firebase\JWT\JWT; use Firebase\JWT\Key; public function login(Request $request) { $phone = $request->param('phone'); $password = $request->param('password'); $user = User::where('phone', $phone)->find(); if (!$user || !password_verify($password, $user->password)) { return json(['code' => 0, 'msg' => '账号或密码错误']); } $payload = [ 'uid' => $user->id, 'role' => $user->role, 'iat' => time(), 'exp' => time() + 7 * 86400 // 7天有效期 ]; $token = JWT::encode($payload, env('JWT_SECRET'), 'HS256'); return json(['code' => 1, 'data' => [ 'token' => $token, 'user' => $user->only('id', 'nickname', 'avatar', 'role') ]]); }

password字段入库前用password_hash()加密,绝对不要明文存密码。登录校验JWT的中间件按需挂载到路由上,比如发帖、评论、上架产品这些必须登录的接口单独加中间件,浏览类接口不挂,性能和体验都更好。

权限层级上我只做了两层:第一层是“必须登录”,第二层是“必须管理员”。管理员判断就检查JWT解析出来的role是否等于2。注册茶农的管理员、用户管理这类接口在控制器开头做一次角色校验,不满足就返回403。这个思路简单可靠,对中小型后台完全够用。

3.2 茶园信息展示与内容发布

茶园信息是平台里最有“生产资料”属性的内容。茶农注册登录后,可以在个人中心的“我的茶园”里填写:茶园名称、地理位置、海拔、面积、主要品种、种植年份、茶园故事。字段看着不多,每个字段的类型和校验都得抠细。

比如海拔我用的数值类型,前端用数字输入框,后端validate里加整数规则,防止有人填带逗号的字符串。面积字段我用字符串,因为不同农户习惯的单位不一样,有写亩的也有写平方的,存字符串反而更灵活。description是textarea,允许换行,前端展示时用css的white-space:pre-wrap把换行还原,这个细节不处理,段落全会挤成一团。

茶园图片采用“单主图加多图集”的方式,主图用于列表封面,多图用于详情轮播。后端上传接口做了文件类型和大小限制,只允许jpg、png、webp,单张不超过5MB。文件命名用时间戳加随机串,避免中文文件名的编码问题,存储路径按日期分目录,方便后续按时间清理。

审核状态是茶园模块一个很重要的设计点。茶园信息提交后并不会直接展示,默认audit_status为0待审核,管理员后台通过后改为1才上线。上线前我特别跟需求方确认过:“为什么我编辑了茶园资料前台看不到?”统一答复是后台审核通过后就会展示。这个设计能拦住一部分恶意内容,平台内容质量更有保障。

3.3 茶文化交流社区:发帖、回帖、点赞与收藏

社区模块是平台最有社区感的地方,功能拆成发帖、回帖、点赞、收藏四块。

发帖我用的富文本编辑器,后端接收content字段并原样保存到thread表。这里最容易踩的坑是XSS。富文本里可以嵌图片、视频甚至script标签,如果不过滤就渲染,整个平台的安全防线等于白搭。我的做法是入库前过滤script和onclick这类危险标签,展示前再做一次安全过滤,双保险。如果你用的是其他编辑器组件,一定要留意它输出内容的清洗逻辑,不能直接信任编辑器。

帖子列表我做了三个排序维度:最新、最热、精华。最新按create_time倒序,最热按view_count和like_count的加权值排,精华则是管理员标记了is_top的帖子固定靠前。分页用ThinkPHP自带的分页类,前端传page和page_size,后端返回总条数、总页数和当前页数据,配合前端分页组件实现完整交互。

评论这边,我把每条评论挂在thread_id下面,再给评论一个parent_id字段。如果parent_id为0,就是楼主的直接评论;如果指向另一条评论,就作为其回复。前端渲染时,直接评论按时间铺开,每条下面的回复做缩进展示,结构比较直观。点赞和收藏都各自建了关联表,字段就user_id、target_id、type、create_time,唯一索引防止重复操作。

3.4 茶叶产品展示与检索

茶叶产品模块的作用是帮茶农把货“亮出来”。产品字段包含标题、分类、封面图、详情图、价格、库存、产地、上市年份和一段描述。分类用category表维护,一个产品只能属于一个分类,这样筛选项可以按品种维度展开,比如绿茶、红茶、白茶、乌龙茶。

产品列表页的核心交互是筛选和搜索。搜索走全文模糊查询,条件是title like '%关键词%' or description like '%关键词%'。数据量不大的时候这样没问题,可真上了几万条产品,like '%xxx%'会让索引失效,需要额外考虑搜索引擎方案。对中小项目来说,先做好基础的联合索引和缓存,不盲目上重型中间件,够用就好。

价格排序我也做了,通过sort参数控制price asc或price desc。这里有一个容易忽略的点:前端传排序参数时必须做白名单校验,不能直接拼接进order by,否则就是一个SQL注入口子。我在后端用switch判断排序类型,只允许白名单里的几个值,其他一律走默认排序。

产品上下架状态和茶园审核逻辑不同,产品是茶农自己控制的。茶农可以把采摘季结束的产品一键下架,不用等管理员审核。管理员可以在后台看到所有产品,但只对明显违规的做强制下架。商业行为自由度更高,内容发布需要审核,这种差异化管理符合平台定位,也减少后台人工干预成本。

3.5 后台管理模块

管理后台是另一个视角的项目,我用Vue单独做了一套带侧边栏的布局,左侧菜单、右侧内容区,路由懒加载。

关键功能有三个:

  • 内容审核:待审核的茶园、帖子、产品列成表格,支持预览详情后一键通过或驳回。驳回时要填原因,平台会把理由展示给用户,减少“莫名被拒”的误解。
  • 用户管理:查看注册用户列表,支持禁用/启用。对发布违规内容的茶农可以拉黑,禁用后登录接口会直接拒绝并提示。
  • 数据统计:用一套简单的SQL统计脚本,展示累计注册用户数、累计帖子数、累计产品数、本周新增数这些指标。

后台的安全要求比前端更高。除了要求管理员登录,所有后台接口统一加了“管理员身份”校验中间件,登录令牌有效期缩短到一天。后台前端页面里做低权限隐藏,非管理员的账号即使绕过前端进到页面,接口返回403也会让功能不可用。

4. 实操过程与核心环节实现

4.1 后端从0到1的关键步骤

项目搭建我用Composer,一条命令把ThinkPHP 6核心拉下来:composer create-project topthink/think think。接着在.env文件里配置数据库连接,然后跑一个测试迁移确认连通。

我习惯把控制器和模型分开建。下面以帖子列表接口为例,展示一下完整实现思路。

先写模型:

namespace app\model; use think\Model; use think\model\concern\SoftDelete; class Thread extends Model { use SoftDelete; protected $name = 'thread'; public function user() { return $this->belongsTo(User::class, 'user_id'); } }

再在控制器里组装数据:

namespace app\controller; use app\model\Thread; class ThreadController { public function list() { $page = request()->param('page', 1); $pageSize = request()->param('page_size', 10); $sort = request()->param('sort', 'new'); $query = Thread::with(['user' => function($q) { $q->field('id, nickname, avatar'); }])->where('status', 1); if ($sort === 'hot') { $query->orderRaw('view_count + like_count * 2 desc'); } else { $query->order('create_time desc'); } $paginate = $query->paginate(['list_rows' => $pageSize, 'page' => $page]); return json([ 'code' => 1, 'data' => [ 'total' => $paginate->total(), 'list' => $paginate->items(), 'page' => $page, 'page_size' => $pageSize ] ]); } }

路由注册顺手配置在route/app.php里:

Route::get('thread/list', 'ThreadController/list');

这里必须提一下with预加载。如果不写with,循环里每次访问$item->user都会发一条SQL查用户表,帖子里有20条就会多出20次查询,数据量稍大接口直接卡顿。with之后ThinkPHP会把用户ID收集起来批量查出再关联回去,速度差别是肉眼可见的。这种“N+1查询”问题在列表接口里太常见了,一定要养成写关联预加载的习惯。

4.2 前端Vue项目搭建与接口对接

前端我用Vite创建Vue 3项目:npm create vite@latest vue,然后按需安装router、pinia、axios。创建工程后的第一件事是配置开发环境的代理,在vite.config.js里把/api开头的请求转发到本地的后端服务:

export default defineConfig({ server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } })

这样前端代码里请求一律写/api/thread/list,联调阶段不会出现跨域问题。上线后代理由nginx接管,不需要改接口代码。

axios封装我单独建了一个request.js,统一设置baseURL、超时时间,请求拦截器里注入token,响应拦截器里统一处理code和状态码。像401统一跳登录页、code为0的业务错误统一用message提醒,这些全局逻辑写在一个文件里,业务组件会变得非常干净:

import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) config.headers.Authorization = `Bearer ${token}` return config }) request.interceptors.response.use( response => { const res = response.data if (res.code === 0) { // 业务错误统一提示 return Promise.reject(new Error(res.msg)) } return res.data }, error => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } ) export default request

页面实现上,首页我放了一个茶园列表组件和帖子流组件,公共卡片都抽到components里。比如TeaGardenCard.vue接收一个garden对象,渲染封面、名称、海拔、品种,点击跳详情。产品列表和帖子列表也类似,各自抽成独立组件,views层只做数据拉取和组件拼接。状态管理用Pinia存用户信息和登录态,刷新页面时从localStorage恢复。

4.3 联调、打包与部署

本地联调开始阶段最大的障碍是跨域。前端代理配好后,开发环境已经没有跨域问题,但上线后如果前后端部署在同一台nginx下,通常也不存在跨域。如果前后端域名分开,后端控制器需要加CORS响应头。ThinkPHP在app/middleware.php里注册一个中间件,对Access-Control-Allow-Origin等响应头赋值即可,我把允许来源放在了.env配置里,灵活调整。

打包上线时,前端执行npm run build,产物是dist目录。nginx配置核心两件事:把dist作为静态站点目录,把所有非静态文件请求回退到index.html,解决Vue history路由刷新404的问题:

location / { root /www/wwwroot/tea-platform/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; }

后端ThinkPHP运行在nginx的php-fpm里,伪静态规则要把非静态文件的请求交给index.php处理,和Vue的history路由回退加起来,整个应用访问就很顺畅了。图片上传目录我单独做了一个路径映射。对象存储虽然方便,但对小项目来说多一层外部依赖,先用本地目录存着,后续量大了再迁移。

提示:上线前务必检查php.ini里的upload_max_filesize和post_max_size,默认值经常只有2MB,茶农拍的高清大图根本传不上去。这个坑不提前踩一遍,上线当天一定会被用户截图吐槽。

5. 常见问题与排查技巧实录

5.1 高频问题速查表

几个月开发下来,我把高频问题整理成了一个表,属于那种“没踩过时觉得没多大事,踩过了想骂人”的系列:

问题现象可能原因解决方式
前端请求后端接口报跨域前后端域名不一致,后端没返回CORS头接口层加CORS中间件,或上线后统一走nginx同源代理
刷新详情页出现404Vue history路由没有服务端回退nginx配置try_files参数回退到index.html
登录状态一会儿就失效JWT有效期设得太短,或签发时iat和exp时区不对检查服务端时区,调整token有效期
列表接口越调越慢关联查询产生N+1问题用with预加载批量查询,减少循环查询
上传大图总是失败服务器上传限制或php上传限制没放开调php.ini里upload_max_filesize、post_max_size等参数
富文本编辑器发布后样式错乱保存时把HTML实体转义了保存时不过滤p、br等基础标签,渲染时用v-html并做安全过滤
软删除后列表把已删内容过滤了忘了模型里使用软删除traits使用SoftDelete trait,查询自动带delete_time为空条件

这个表本身不能完全替代排查,但很多初学者项目卡住,八成都能从里面找到答案。尤其是N+1和跨域,十个项目得有八个栽在这。

5.2 三个印象深刻的坑

第一个坑是软删除和关联查询的配合。帖子表用了软删除,用户表暂时没做,用Thread::with('user')查列表时,如果某些用户被禁用了,关联出来的user却仍然存在,因为软删除只作用于Thread本身。要解决这个问题,需要在用户模型的处理上统一策略。后来我对所有内容表统一加了软删除,关联查询就没有这种奇奇怪怪的现象了。

第二个坑是富文本编辑器的安全问题。最初版本的编辑器允许嵌入脚本,我本地测得好好的,后来模拟攻击时发现可以插入危险代码,虽然没有真正造成数据崩溃,但已经足够让意识到:富文本内容必须在后端做白名单过滤。后来我加了一个专门做HTML过滤的扩展,把事件绑定属性全部去掉,只保留基础排版标签和图片标签,这才算踏实。

第三个坑是列表排序字段的安全校验。一开始我直接拿前端传的sort参数拼接SQL,后来做安全测试时发现可以通过sort传入一段注入语句。这个问题属于SQL注入的常规变种,反手把参数改成白名单switch判断,彻底封住了口子。

6. 做完这个项目我的一些体会

做完这个项目我最大的感触是,一个平台的骨架往往不是功能多丰富,而是实体关系理得清、权限边界划得明。把用户、茶园、帖子、产品这几个核心实体的关系吃透,后面的实现全都顺理成章。

想给后来者三个建议:

  • 先画清楚角色和权限,再写一行代码。角色搞混了,后面所有接口都得返工。
  • 安全意识从第一天就有。密码加密、JWT过期、XSS过滤、排序参数白名单,这些不是上线前才补的作业,而是架构期就该留出来的空位。
  • 别盲目堆新技术。Vue和ThinkPHP对这类中小型内容平台刚刚好,把基础功能打磨顺,比用一堆热门前沿框架但做不完强得多。

平台后续如果还想做得更深入,可以考虑加一个茶文化专题栏目,按节气、制茶工艺、历史典故组织内容;也可以增加茶园地图展示,把地图模块接进来,游客一眼就能规划路线。前端选Vue的好处就是扩展起来不费劲,给后续留足了余地。这个项目本身不算大,但从需求分析到部署上线的完整闭环,做完之后对前后端分离项目的把握会明显上一个台阶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询