开头直接进入主题。先说清楚这个项目是做什么的。
昆明西山区那边的家政服务市场挺有意思——老小区多、新楼盘也密集,家庭对保洁、月嫂、育儿嫂、养老护理的需求一直很旺。但线下找阿姨基本靠邻居推荐和朋友圈转发,服务怎么样完全凭运气。被介绍来的阿姨到底靠不靠谱、有没有不良记录、之前服务过的家庭评价如何,这些信息全是黑盒。我接到的这个项目,就是给西山区一个本土家政平台开发一套评价系统,让用户下单、阿姨上门、服务完评价、评价展示沉淀成一条完整的数据链。整体技术方案在ThinkPHP和Laravel之间反复比较过,最终落地用的是ThinkPHP 8。
这篇博文会把整个项目从业务拆解、技术选型、库表设计、功能实现到部署上线的完整过程都翻出来讲讲,重点说清楚那些写代码时容易漏、但上线后一定会踩的坑,比如关联删除、评价审核流程、二级域名部署等等。适合正在做类似评价类系统、或者准备在ThinkPHP框架下做业务系统的朋友参考。
1. 业务盘点:家政评价这潭水里藏着哪些需求
1.1 西山区市场的一个关键特征
西山区的用户群体很有意思。一方面是大规模回迁房和刚需盘,居住密度高,双职工家庭多,请保洁是刚需;另一方面是这类家庭对“安全性”的敏感度非常高——让一个陌生人进家门,服务质量差一点可以忍,但人的基本情况靠不靠谱,这是第一道门槛。
所以这个评价系统从一开始就不是简单地搞个“好评差评”按钮。它要解决的是三个层面的问题:
- 给用户一个表达真实体验的出口,包括工完是否及时、工具是否自带、是否损坏物品、态度如何、有没有额外收费。
- 给平台一个审核和风控的抓手,把不靠谱的服务人员挡在推荐池之外。
- 给新用户一个可信任的决策参考,而不是听平台自卖自夸。
按照这个思路,系统角色分了三种:普通用户端(微信端网页为主)、家政人员端(同样在网页端)、平台管理端(后台审核与数据运营)。整体功能按角色分域设计,评价不是孤立的,它需要和用户、订单、阿姨档案、投诉工单四个模块联动。
1.2 评价不是“打分器”,而是服务闭环的一部分
在设计需求的时候,平台方一度把评价系统理解成“订单完成后弹个窗,让用户打几颗星”。这太单薄了。
真实的家政场景里,评价至少要和这几个东西挂钩:
- 订单:必须一单一评,没有订单的评价没有参考价值,也防不住刷单。
- 阿姨档案:评价会聚合到阿姨的个人页上,形成好评率和服务标签云,直接影响阿姨之后的派单权重。
- 结算与奖惩:平台通过评价数据给阿姨评级,连续差评会触发约谈甚至下架。
- 投诉与申诉:差评要允许家政人员发起申诉,避免恶意差评或者用户主观臆断毁掉一个阿姨的接单生涯。
所以我在做需求评审时,直接把系统定义成“以订单为链路、以评价为核心的轻量CRM”。评价表的设计必须能支撑这些业务动作,而不是孤零零一张打分表。
1.3 三类用户的诉求差异
把需求拆细一点,会发现三方诉求完全不同,在设计功能时都要照顾到。
用户关注的是:评价是否方便、是否能匿名、内容是否真实。很多用户其实不愿意让阿姨知道自己给了差评,怕后续尴尬甚至被纠缠,所以匿名评价必须是强需求。这个点我在功能设计时直接做成默认开启,用户手动取消。
家政人员关注的是:被差评了有没有申诉的权利、评价是否公开了敏感个人信息。所以评价展示里绝对不能直接展示用户手机号、详细住址,只能展示小区名或片区,比如“西山区金碧街道某小区”,黑白名单机制由此而来。
平台关注的是:数据能不能辅助运营。比如通过评价关键词的分析,发现某个片区的阿姨普遍被反映“工具不全”,就可以在阿姨培训时做针对性提醒。这些功能我们在后台做了一套简单的关键词聚合,不需要多高级的人工智能,光是分组统计就很有用了。
2. 框架取舍:为什么选了ThinkPHP而不是Laravel
2.1 项目标题里同时出现两个框架的原因
标题里写了“Thinkphp_Laravel框架”,很多人可能会疑惑:到底是用了哪个?
实际情况是,项目初期团队内部有人提过用Laravel,因为Laravel的生态确实更现代、队列和事件机制用起来更顺手。而且Laravel对规范的要求更高,适合长期演进的大型项目。
但经过一周的评估,我们最终选择ThinkPHP。原因很实在,逐个说:
| 对比维度 | ThinkPHP 8 | Laravel 11 |
|---|---|---|
| PHP版本要求 | PHP 8.0+,兼容性好 | PHP 8.2+,要求更高 |
| 部署环境 | 国内虚拟主机和面板都能直接跑 | 对伪静态和目录权限要求更严格 |
| 中文文档与学习成本 | 文档齐全,中文资料多 | 文档优秀但中文生态稍弱 |
| 全家桶程度 | MVC各层都有现成封装 | 组件更灵活但要自己选型 |
| 二次开发维护 | 国内开发者基数大,找人容易 | 招人渠道相对窄一些 |
| 性能基准 | 基础性能不错,ORM够用 | 功能丰富但默认加载内容多 |
说到底这个项目不是SaaS级产品,不需要分布式、不需要复杂队列,核心就是把业务快而稳地跑起来。ThinkPHP的模型关联、软删除、验证器、中间件这些能力已经覆盖了评价系统90%的需求,没有理由引入更重的框架增加团队学习成本。
2.2 两个框架在评价系统场景下的实际表现
如果纯粹从功能实现角度对比,两者差异在评价系统这个场景里其实不明显。比如模型关联,Laravel有Eloquent的with预加载,ThinkPHP同样支持关联预加载;软删除两者都有;验证器也都有现成组件。
真正的差异在部署和运维。
小皮面板(phpStudy)这类国内环境,ThinkPHP能直接跑,运行目录设置好,伪静态写完就完事。Laravel在Windows开发环境和Linux线上环境之间经常遇到storage权限、composer依赖不一致、public目录软链等问题,处理起来比较折腾。
还有一点,家政平台运营方是本土中小团队,后续维护很可能找一个本地外包公司来做。与其用一个他们不熟悉的框架,不如用一个生态更普及、中文资料搜索即得的技术栈。技术选型不是给自己炫技,是要对项目未来一年、三年的维护成本负责。
所以最终结构是:ThinkPHP 8 + MySQL 5.7 + Nginx(小皮面板环境),前端用服务端渲染加少量原生JavaScript,管理后台直接使用模板引擎,这套方案能最大化降低整体复杂度。
2.3 目录结构与分层思路
ThinkPHP 8 默认的多应用模式非常适合这个项目。我把前后台拆成两个应用:
project_root ├── app │ ├── index // 用户端应用 │ │ ├── controller │ │ ├── model │ │ └── view │ ├── admin // 管理后台应用 │ │ ├── controller │ │ ├── model │ │ └── view │ └── api // 接口应用(小程序/客户端预留) ├── config ├── public // Web运行目录 │ ├── index.php │ └── static ├── route └── vendor评价相关业务放在app/api里做接口,前端页面通过ajax调用。这样的好处是以后如果要出微信小程序,可以直接复用这套接口,不用再单独开发一套后端。路由方面使用ThinkPHP的路由规则,把评价提交、审核、申诉这些接口都定义了明确的伪静态路径。
3. 核心库表设计:评价数据怎么组织才扛得住业务变化
3.1 整体表结构规划
评价系统涉及的表面看起来只有一张评价表,但只要一细想就会发现,图集、标签、回复、申诉、审核日志这些全部要落库。我这边的核心表如下:
users用户表service_providers家政服务人员表orders订单表evaluations评价主表evaluation_images评价图片表evaluation_tags评价标签表evaluation_replies评价回复表(商家/平台回复)evaluation_appeals申诉表audit_logs审核日志表system_users后台管理员表
外键没有在数据库层面去强约束,统一在模型层维护。这是ThinkPHP项目常用的做法,方便后续分库分表,也不至于MySQL在删除时被外键卡住。
3.2 评价主表字段设计
评价主表是核心,字段设计一点都不能含糊。我贴一下简化后的建表语句,保留关键字段:
CREATE TABLE `evaluations` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `order_id` bigint(20) unsigned NOT NULL COMMENT '订单ID', `user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID', `provider_id` bigint(20) unsigned NOT NULL COMMENT '家政人员ID', `service_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '服务类型:1保洁 2月嫂 3育儿嫂 4养老护理', `star_level` tinyint(4) NOT NULL DEFAULT '5' COMMENT '综合评分1-5', `price_score` tinyint(4) NOT NULL DEFAULT '5' COMMENT '价格评分', `attitude_score` tinyint(4) NOT NULL DEFAULT '5' COMMENT '服务态度评分', `quality_score` tinyint(4) NOT NULL DEFAULT '5' COMMENT '服务质量评分', `punctuality_score` tinyint(4) NOT NULL DEFAULT '5' COMMENT '守时评分', `content` text COMMENT '评价内容', `is_anonymous` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否匿名', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态 0待审核 1已通过 2已驳回 3已删除', `audit_admin_id` bigint(20) DEFAULT NULL COMMENT '审核管理员ID', `audit_remark` varchar(255) DEFAULT NULL COMMENT '审核备注', `audit_time` datetime DEFAULT NULL COMMENT '审核时间', `like_count` int(11) NOT NULL DEFAULT '0' COMMENT '点赞数', `created_at` datetime DEFAULT NULL, `updated_at` datetime DEFAULT NULL, `deleted_at` datetime DEFAULT NULL COMMENT '软删除时间', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`), KEY `idx_provider_id` (`provider_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家政服务评价表';几个字段单独说一下为什么这么设计:
多维度评分。家政行业的体验不是一个“总星”能概括的。比如阿姨干活很干净但总迟到,或者态度特别好但收纳水平不行,都需要分维度展示。我在前端评价表单里把综合评分拆成四个子项,列表页展示综合分,详情页展示四维雷达图。数据上多存几个int型字段,成本不高,但体验完全不同。
status状态机。评价不是提交后立刻上墙的,尤其家政行业直接进家门,用户情绪化差评、恶意差评、虚假评价都可能有,所以审核状态必须落库。0待审核、1已通过、2已驳回、3已删除,这四个状态对应完整的操作流。
is_anonymous默认1。前面提到匿名是刚需,为了最大程度保护用户评价意愿,默认是匿名状态,用户如果愿意展示自己可以手动关掉。很多平台把默认设成不匿名,结果用户根本不敢真实评价,这个细节非常影响数据质量。
3.3 标签表和图片表:非结构化内容的结构化沉淀
评价光有一段文字是不够的。家政用户特别喜欢用“守时”“工具齐全”“干活麻利”“沟通顺畅”“价格公道”这类词,如果让用户自己打字,输入成本高,而且信息不结构化。所以我在表单里提供标签点选,用户可以直接点选多个标签。
标签表设计:
CREATE TABLE `evaluation_tags` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `evaluation_id` bigint(20) unsigned NOT NULL, `tag_name` varchar(30) NOT NULL COMMENT '标签名', `tag_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1好评标签 2差评标签', PRIMARY KEY (`id`), KEY `idx_evaluation_id` (`evaluation_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;标签值本来可以像很多系统一样用逗号拼在一个字段里,但我还是用了子表。原因是后续要按标签聚合统计,比如想知道这个月西山区的阿姨被好评最多的标签是什么、差评最集中的标签是什么,SQL直接group by tag_name就能出结果,字符串字段没法高效做这事。
图片表设计相对来说简单,一个evaluation_id外键,加上图片URL、排序权重、创建时间。需要注意的是一定要在上传时就做压缩和防盗链,不然前端页面容易被大图拖慢速度,图片URL还会被别站直接盗用。压缩这块我在上传时用ThinkPHP的think\Image类做了尺寸限制,超过1200px的等比压缩。
3.4 订单与评价的绑定关系
评价不能凭空产生,必须挂在一个真实的订单下。所以订单表和评价表是严格一对一的关系。
为保证这一点,我在orders表加了一个evaluation_status字段,用来记录这个订单是否已经评价过。同时在评价提交接口里做了强制校验,一个订单只能提交一次评价。两条防线一起作用,避免并发请求下出现一单多评的脏数据。
家政务的订单状态是:待支付 → 待服务 → 服务中 → 已完成 → 已取消。只有状态为“已完成”的订单才能发起评价,服务中或已取消的订单后端一律拒绝。评价之后,订单表里的evaluation_status更新为1,同时把评价ID回填到订单表的evaluation_id字段,查询时直接关联,减少一次检索。
4. 从提交评价到审核上墙:全流程功能实现
4.1 评价提交接口与参数校验
用户端评价提交通道设计成一个单独的接口:POST /api/evaluation/submit。控制器层面的逻辑分三步:身份校验、订单验证、参数验证。
参数验证用ThinkPHP的验证器来做:
<?php namespace app\api\validate; use think\Validate; class EvaluationValidate extends Validate { protected $rule = [ 'order_id' => 'require|integer|gt:0', 'star_level' => 'require|between:1,5', 'price_score' => 'require|between:1,5', 'attitude_score' => 'require|between:1,5', 'quality_score' => 'require|between:1,5', 'punctuality_score' => 'require|between:1,5', 'content' => 'max:500', 'images' => 'array|max:6', 'tags' => 'array|max:10', 'is_anonymous' => 'in:0,1', ]; protected $message = [ 'order_id.require' => '订单参数缺失', 'star_level.between' => '综合评分必须在1-5之间', 'content.max' => '评价内容不能超过500字', 'images.array' => '图片格式错误', ]; }这里有一点要注意,content不是必填,用户完全可以只点标签不打字。但至少要有内容或者标签其中一样,否则一个空壳评价没有参考价值。这个逻辑在控制器里单独判断:
if (empty($data['content']) && empty($data['tags'])) { return json(['code' => 400, 'msg' => '请填写评价内容或选择标签']); }4.2 一单一评的幂等处理
评价提交最大的风险是重复提交。用户手一抖点两次提交按钮,或者前端没有做loading禁用,请求就会并发进来。如果后端不做幂等处理,一条评价就变成两条了。
我的处理方案是三层校验:
- 数据库唯一索引:给
evaluations表的order_id添加唯一索引UNIQUE KEY uk_order_id (order_id)。这是最硬的一层保障,就算应用层有逻辑漏洞,数据库也会直接拒绝第二条插入。 - 订单状态预检查:提交时先查订单的
evaluation_status,如果已经是1,直接返回“该订单已评价”。 - 事务包裹:评价主表和评价标签、图片子表在一个事务里写入,任何一个子表写入失败则整体回滚。
最后这点尤其重要。万一主表插入成功、标签表插入失败,就会出现一条没有标签、没有图片的半残评价,数据不一致很难排查。用事务包裹以后,这种问题直接从根上杜绝。
4.3 敏感词过滤与自动审核流程
评价系统的审核流程,是这个项目的重头戏。
家政平台上的评价一旦展示出来就会直接影响其他人是否约这个阿姨,所以审核不能形同虚设。但纯人工审核又有问题:单量少还好,单量一多审核就变成瓶颈,评价要及时上墙才能给用户好的反馈体验。
我的方案是“自动审核为主、人工审核兜底”。
自动审核的规则分三块:
- 敏感词过滤。维护一个敏感词表,覆盖政治敏感、辱骂人身攻击、涉黄涉暴、垃圾广告等词汇。用ThinkPHP的字符串匹配方式逐条跑一遍,如果命中,状态直接置为2并记录驳回原因。
- 图片安全检测。评价上传的图片必须过一遍鉴黄接口。平台刚起步没必要自建模型,直接对接云厂商的内容安全API,返回
pass的才允许进入展示,review或block的进入人工复审或直接拒绝。 - 垃圾内容检测。比如相同内容短时间内被多个账号提交、包含微信号或手机号之类的引流内容,这类要单独识别,通常直接放进待人工审核状态,由管理员确认是真实评价还是广告。
自动审核的代码大致长这样:
public function autoAudit(Evaluation $evaluation) { // 1. 敏感词检查 if (SensitiveWordService::check($evaluation->content)) { $evaluation->status = 2; $evaluation->audit_remark = '包含敏感词'; $evaluation->save(); return; } // 2. 图片检查(存在图片时) if ($evaluation->images()->count() > 0) { $checkResult = ImageAuditService::check($evaluation->images()->column('url')); if ($checkResult['status'] === 'block') { $evaluation->status = 2; $evaluation->audit_remark = '图片内容违规'; $evaluation->save(); return; } } // 3. 通过自动审核 $evaluation->status = 1; $evaluation->audit_admin_id = 0; // 系统自动审核 $evaluation->audit_time = date('Y-m-d H:i:s'); $evaluation->save(); }这里有一个细节,就是评价提交后不能立刻显示在前台。所以前端提交后要跳转到一个“审核中”的提示页,告诉用户“评价已提交,审核通过后将展示在阿姨主页”。等审核通过后,再通过刷新或消息通知让用户看到自己的评价已经上墙。
4.4 审核后如何聚合到前台
评价通过审核之后,不只是简单地在评价列表里多一条记录。它至少要同步影响到三个地方:
- 家政人员主页的评价聚合:平均分、好评率、评价条数、标签云。
- 平台首页的推荐逻辑:综合评分高的阿姨排名靠前。
- 后台的数据看板:今日新增评价数、通过率、驳回率、平均分变化趋势。
我用ThinkPHP的模型观察器来做这个联动,Evaluation模型定义afterInsert、afterUpdate事件,在评价写入或状态变更时触发对应的统计任务。
public static function onAfterUpdate(Evaluation $evaluation) { // 只有当状态变为已通过时才重新聚合 if ($evaluation->status == 1) { ProviderStatService::refresh($evaluation->provider_id); } }ProviderStatService里的逻辑就是重新算一遍该服务人员的平均分和各维度的均值,更新到service_providers表的冗余字段里。这样前台查询阿姨列表的时候不用去evaluations表里现算avg(),直接读冗余字段,性能上好很多。
4.5 商家回复与申诉机制
评价审核通过上墙了,不代表这件事就结束了。家政平台上,一个差评对阿姨影响很大,所以必须给家政人员端配“回复”功能和“申诉”功能。
回复功能比较简单,就是evaluation_replies表,一个评价允许一条商家回复,回复内容同样要走敏感词过滤。考虑到家政人员文化水平跨度比较大,后台我把回复框做成了模板选择加自定义两种方式,推荐话术模板直接生成“尊敬的客户,感谢您的反馈,我们会改进XX问题”,降低回复门槛。
申诉功能则是当家政人员认为某条差评不实或者属于恶意评价时,提交申诉工单,附上聊天记录截图或录音证据。管理员在后台看到申诉后,可以决定维持原评还是隐藏评价。被隐藏的评价状态标记为“已隐藏”,前端不再展示,但保留在数据库里供平台分析。
5. 最容易被忽视的删除逻辑:关联数据处理实战
5.1 删除业务数据的几种真实场景
评价系统的“删除”比想象中复杂得多。日常运营中会遇到这些情况:
- 用户申请注销账号,他名下所有评价怎么处理?
- 家政人员离职或者被平台清退,他名下的历史评价怎么处理?
- 用户在后台联系客服说“我这条评价写错了,帮我删掉”,运营人员删掉评价后,关联的图片、标签、回复怎么处理?
- 有一些违规评价被强制删除,删除后是否要在前台留一个占位提示?
新手最容易犯的错,就是在控制器里直接delete()一条主表记录,然后发现评价详情页报错、图片残留、统计数字对不上、分离的碎片数据越来越多。
这个项目里,所有删除操作全部走了软删除和关联清理机制,系统性解决数据一致性的问题。
5.2 ThinkPHP的软删除与模型事件
ThinkPHP的模型层提供了SoftDeleteTrait,在模型里引入后,delete()方法不会真正执行数据库删除,而是把deleted_at字段写入当前时间,查询时默认带上deleted_at IS NULL条件。
但软删除只是第一步,关联数据的清理才是重点。我的处理方式是给Evaluation模型注册模型事件,在删除时同步清理关联子表:
<?php namespace app\common\model; use think\model\concern\SoftDelete; use app\common\model\EvaluationImage; use app\common\model\EvaluationTag; use app\common\model\EvaluationReply; class Evaluation extends BaseModel { use SoftDelete; protected $name = 'evaluations'; protected $deleteTime = 'deleted_at'; public static function onBeforeDelete($evaluation) { // 删除评价时同步清理关联的图片和标签 EvaluationImage::where('evaluation_id', $evaluation->id)->delete(); EvaluationTag::where('evaluation_id', $evaluation->id)->delete(); } public static function onAfterDelete($evaluation) { // 如果有商家回复,也一并软删除 EvaluationReply::where('evaluation_id', $evaluation->id)->delete(); // 刷新家政人员的统计数据 if ($evaluation->provider_id) { ProviderStatService::refresh($evaluation->provider_id); } } }这里解释一下为什么不用数据库外键ON DELETE CASCADE。家政评价数据属于业务核心数据,直接物理级联删除的风险太大,万一误删想恢复都难。用模型事件的方式,以后想改逻辑、想加个回收站都很灵活。而且ThinkPHP的模型事件在软删除时也会触发,正好满足“删除后同步处理关联”的需求。
5.3 三个典型场景的具体处理策略
场景一:用户注销账号
用户注销不意味着评价要跟着消失。如果用户注销后评价全部消失,家政人员的评分、好评率瞬间就崩了,这对平台生态不公平,也容易被阿姨投诉。我的做法是:用户注销后,他名下的评价保留,但统一做匿名化处理——user_id置为0,is_anonymous置为1,昵称显示为“已注销用户”。这样既保护了注销用户的信息,又保留了对家政人员有用的评价数据。
具体代码在用户控制器注销逻辑里补一段:
Evaluation::where('user_id', $userId)->update([ 'user_id' => 0, 'is_anonymous' => 1 ]);场景二:家政人员下架或离职
家政人员被清退后,他在前台的主页要下线,但已产生的订单评价还需要仲裁依据,平台方可能因此涉及用户退费或者法律纠纷。所以不能物理删除,只能操作service_providers表的status字段,切换到“已下架”状态。历史评价仍然保留,但前台展示时打上一个“该服务人员已暂停服务”的标识,用户可以继续看到评价内容作为参考,但不能预约下单。
场景三:运营后台软删除违规评价
运营人员在前台看到一条违规评价,处理路径是:后台搜索确认 → 点“删除” → 模型事件自动清理图片和标签 → 评价状态变为已删除 → 前台列表不再展示 → 聚合统计重新计算。
一套流程下来,前台体验无感知,但后台数据没有任何残留和断层。这套“软删除+模型事件+统计刷新”的组合,在评价系统里是最省心的维护方案。
6. 部署上线:运行目录、二级域名和那些安全细节
6.1 小皮面板建站与运行目录设置
这个项目部署使用的Windows服务器加小皮面板(phpStudy),环境是Nginx + PHP 8.1 + MySQL 5.7。很多人在本地开发好好的,一部署到小皮面板就各种404、500,原因多半是运行目录没有指向public。
ThinkPHP的入口文件在public/index.php,Nginx的站点根目录必须指向public目录。如果直接指向项目根目录,访问会出现两种情况:首页能打开但所有静态资源都404,或者直接报错。更严重的是,如果根目录配置不当,用户直接访问/app、/.env这类路径,源码和数据库配置会被直接暴露,这是高危漏洞。
小皮面板创建站点时要在“运行目录”里选择/public,同时开启“防跨站”选项,把目录锁定在站点范围内。这一步做完,常规的目录穿越攻击就能挡住大部分。
6.2 用户端和管理端的二级域名配置
这个项目有两个面向端:用户端评价展示页、管理后台审核页。两个端使用同一个ThinkPHP多应用项目,通过二级域名区分路由:
- 用户端:
www.xxx.com - 管理后台:
admin.xxx.com
小皮面板的操作步骤是:
- 先在域名解析商那里给两个子域名配好A记录,指向服务器IP。
- 在Nginx里创建两个server块,分别设置
server_name。 - admin域名的
root同样指向public,因为ThinkPHP多应用模式只靠URL前缀区分应用,不靠不同的目录。
然后修改config/app.php,把后台应用的域名绑定设置好:
'app_map' => [ 'admin' => 'admin', ], 'domain_bind' => [ 'admin.xxx.com' => 'admin', ],这样访问admin.xxx.com时直接进入后台应用,不会在URL里出现/admin前缀,更干净也更好记。
6.3 伪静态配置与URL美化
ThinkPHP默认的URL带/index.php?s=/...,既不美观又有暴露框架路径的风险。用Nginx重写可以去掉index.php:
location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } }注意这一段一定要写在location /块内,并且root指向的是public目录。如果伪静态配完还是404,优先检查root路径对不对,其次看看是不是try_files和rewrite冲突了。
伪静态配置完成后,评价详情的URL就变成/evaluation/detail/123.html这样的形式,对SEO也友好一些。
6.4 图片存储与安全加固
评价图片的上传是安全重灾区。我在设计上传接口时做了几个限制:
- 类型白名单:只允许jpg、jpeg、png、webp,禁止上传php、phtml、asp等任何可执行文件。
- 文件头校验:不只检查扩展名,还要用
finfo_open读文件的MIME类型,防止有人改扩展名绕过。 - 随机文件名:上传后的文件名用
uniqid + 随机串重新生成,不保留用户原始文件名,避免包含路径穿越字符。 - 存储路径隔离:图片存放在
public/uploads/evaluation/{日期}/目录下,Nginx配置里对uploads目录禁止执行PHP脚本。
最后这一点很多人会忽略。如果上传目录里被塞了一个shell.php,而Nginx又允许PHP执行,那整个服务器就沦陷了。只要在Nginx配置里加一行就可以解决:
location ~* ^/uploads/.*\.(php|php5)$ { deny all; }6.5 数据备份与灰度切换
家政评价系统虽然业务量不如电商那么大,但如果数据丢了,用户的评价记录、家政人员的评分档案全部清零,这个损失是平台方完全无法承受的。
我用的备份策略很简单但也够用:
- MySQL定时任务:每天凌晨3点执行
mysqldump,保留最近7天的备份文件。 - 图片异地备份:因为图片量还不算大,每周把
uploads目录压缩同步到另一台存储空间。 - 手工快照:每次上线新版本前,先打一次数据库快照,出问题可以即时回滚。
家政评价系统的数据量在早期不会太大,不需要上分库分表那一套,但备份一定不能省。这是上线前做过多次压力测试后得出的经验,越是看着规模小的系统,越要防止雪崩式流失。
这套系统从立项到上线用了三周多的时间,总体不算快也不算慢。核心功能跑通之后,平台方最满意的反而是两件事:一是每个订单都能严格一单一评,数据很干净;二是运营后台审核评价的流程很顺畅,不用跨系统去操作。开发过程中的那些坑基本都出现在我上面提到的位置——选型纠结、表结构设计反复改、删除逻辑差点漏掉关联数据、部署时目录不对导致404。把这个项目过程整理出来,希望对你设计和开发同类评价系统有所帮助。