先交代一个很有意思的引子。前几天在技术群里看到一句吐槽:“是什么样的单主,让我打出了四星评价?”这句话乍一听是用户情绪表达,但放在交易、接单、服务类平台里,它就变成了一条非常典型的产品信号:四星评价没有到“差评”的程度,但又明显低于满分五星。如果你只盯着平均分,四星很容易被淹没;而真正落地过评价系统的人会知道,四星附近往往藏着大量“勉强满意但不愿意推荐”的真实体验问题。
这篇教程不准备讨论具体某个平台的运营策略,而是把这类问题工程化:当你需要开发一套“星级评价系统”时,评价数据表怎么设计?后端如何保证一单一评?四星评价如何被有效识别并进入回访流程?前端星级组件怎么封装?本文会从需求背景、数据库设计、后端接口、前端页面到常见问题排查,完整走一遍实现流程,适合刚接触订单评价模块的开发者,也适合想把“四星评价”从黑盒变成可分析数据的后端工程师。
在整个阅读过程中,本文会一直围绕一个核心目标:不仅让用户能打星,还能让系统从一颗星里挖掘出业务价值。
1. 背景与核心概念
1.1 什么是单主和四星评价场景
在交易、接单或服务类平台中,发起订单的一方通常被叫作“单主”,本质上就是买家、需求方或客户。平台完成一单服务后,买家有机会对本次服务进行评分,这个评分通常以星级形式体现,从 1 星到 5 星。
四星评价的特殊之处在于,它不像 1 星或 2 星那样带有强烈负面情绪,但也意味着用户潜意识里觉得“这次体验没有达到完美标准”。如果平台只做“好评/差评”的简单二分法,四星大概率会被归入“好评”,从而丢失一次改进机会。
从产品层面看,四星评价可能是以下原因造成的:
- 服务质量达标,但响应速度偏慢。
- 结果符合预期,但沟通过程中不够顺畅。
- 整体体验不错,但某个细节让用户不太舒服。
- 用户对“五星”的定义很严格,习惯默认打四星。
这就是为什么评价系统不能只保存一个总分。一个完整的评价系统,至少要能够回答三类问题:
- 谁在什么订单上打了星?
- 用户对哪些维度不满意?
- 低分或四星评价是否触发了回访和跟进?
1.2 评价系统的核心职责
评价模块听起来简单,无非是一张表加一个提交接口,但真正放到交易闭环里,它通常承担以下职责:
- 约束评价资格:只有已完成的订单才能评价,且只有买家本人可以评价。
- 保证唯一性:同一笔订单只能评价一次,不能重复刷评。
- 采集多维数据:总分之外,还要记录服务、质量、速度等维度分数。
- 提供统计能力:展示卖家或服务提供方的综合评分、评分分布。
- 支撑运营动作:将四星以下或特定维度的低分评价转入回访任务。
评价系统的建设难点并不在打分组件,而在数据模型和状态约束。如果订单状态和评价状态没有对齐,就会出现“订单没完成也能评价”“同一个人对同一个订单评价两次”这种数据污染。
1.3 几个容易混淆的概念
在开始写代码前,有必要区分几组概念。
第一,订单评价和商品评价不同。商品评价可以针对同一个 SKU 重复发生,而订单评价是由“单次交易关系”唯一约束的。商品评价通常挂在商品维度,订单评价必须挂订单维度,并且一个订单只能产生一条主评价。
第二,综合评分和维度评分不同。综合评分是用户对整体体验的感受,维度评分可以拆成服务态度、完成质量、响应速度。维度评分的作用是解释综合评分为什么是四星而不是五星。
第三,均分和评分分布不同。均分是平均值,只能反映整体水平;评分分布是 5 星、4 星、3 星及以下的占比。只看均分容易掩盖问题,比如一个 4.8 分的卖家,4 星评价占比可能已经很高,意味着忠实推荐度其实在持续下降。
2. 环境准备与版本说明
本文采用“Spring Boot + Vue + MySQL”这套在企业中普及度很高的组合。版本不需要完全照抄,建议根据你的现有项目情况选择,但示例代码会标明关键配置。
2.1 技术栈清单
| 分层 | 技术选型 | 说明 |
|---|---|---|
| 后端语言 | Java 8 或 17 | JDK 17 建议配合 Spring Boot 3.x,JDK 8 建议使用 Spring Boot 2.7.x |
| 后端框架 | Spring Boot | 简化依赖管理和请求处理 |
| ORM | MyBatis-Plus 3.5.x | 提供基础 CRUD 和 LambdaQueryWrapper |
| 数据库 | MySQL 5.7+ / 8.x | 本文 SQL 使用 utf8mb4 字符集 |
| 前端 | Vue 3 + Element Plus | 星级组件使用 el-rate |
| 构建工具 | Maven 3.6+ | 管理后端依赖 |
| IDE | IntelliJ IDEA 或 Eclipse | 按习惯选择即可 |
需要注意,Spring Boot 3.x 对 JDK 版本有要求,如果你的环境是 JDK 8,建议继续使用 Spring Boot 2.7.x。下面示例中不会使用过于“新版本限定”的 API,核心逻辑可以平移到老版本。
2.2 项目目录结构设计
为了让代码便于阅读,我按前后端分离的结构来组织项目。你可以直接复用这个目录骨架。
order-review-system ├── backend │ ├── pom.xml │ └── src/main │ ├── java/com/example/review │ │ ├── ReviewApplication.java │ │ ├── common │ │ ├── controller │ │ ├── mapper │ │ ├── service │ │ └── entity │ └── resources │ ├── application.yml │ └── mapper/ReviewMapper.xml ├── frontend │ └── src │ └── views │ └── ReviewForm.vue └── sql └── init.sql这是一个比较常规的单体项目结构。如果你的业务量比较大,后续可以把“评价提交”和“评价统计”拆成独立服务,但在初期阶段,单体结构足够支撑业务。
2.3 数据库环境检查
在动手写代码前,建议先确认 MySQL 可以正常连接,并创建一个独立数据库。为了避免 emoji 评论写入报错,数据库字符集一定要使用utf8mb4。
create database if not exists order_review default character set utf8mb4 collate utf8mb4_general_ci;后面建表时,也要显式使用utf8mb4,否则一旦用户在评论里输入表情符号,就可能出现Incorrect string value的错误。
3. 数据库设计:把“为什么打四星”拆成可分析的维度
数据库设计是整个评价系统最关键的部分。很多团队最初只建一张评价表,包含订单 ID、用户 ID、分数、内容,但这种设计只能回答“打了几分”,很难回答“到底是哪方面没做好”。
3.1 订单表设计
订单表不是本文重点,但评价系统强依赖订单状态,所以先设计一张精简订单表。实际业务中订单表结构会更复杂,这里只保留评价模块需要的核心字段。
create table `orders` ( id bigint primary key auto_increment, order_no varchar(32) not null comment '订单编号', buyer_id bigint not null comment '买家id,也就是单主', seller_id bigint not null comment '卖家/接单方id', order_status tinyint not null default 1 comment '订单状态 0-已取消 1-进行中 2-已完成', finish_time datetime default null comment '完成时间', create_time datetime not null default current_timestamp comment '下单时间', unique key uk_order_no (order_no), key idx_buyer_status (buyer_id, order_status), key idx_seller_status (seller_id, order_status) ) engine=innodb default charset=utf8mb4 comment='订单表';订单状态是评价资格判断的依据。后端不能只判断订单 ID 存在,还必须检查当前订单是否处于已完成状态。否则用户完全可以在订单未完成时提交评价,后续履约内容发生变化后,评分会失真。
3.2 评价主表设计
评价主表用于保存每笔订单的主评价信息。核心约束是order_id,必须保证唯一。
create table review ( id bigint primary key auto_increment, order_id bigint not null comment '订单id', buyer_id bigint not null comment '买家id', seller_id bigint not null comment '卖家id', total_score tinyint not null comment '综合评分 1~5', service_score tinyint not null comment '服务态度评分 1~5', quality_score tinyint not null comment '交付质量评分 1~5', speed_score tinyint not null comment '交付速度评分 1~5', comment varchar(500) default null comment '文字评价', anonymous tinyint not null default 0 comment '是否匿名 0-否 1-是', create_time datetime not null default current_timestamp comment '评价时间', unique key uk_order (order_id), key idx_seller_score (seller_id, total_score), key idx_create_time (create_time) ) engine=innodb default charset=utf8mb4 comment='订单评价表';这里有几个设计细节需要重点说明。
第一,order_id上的唯一索引uk_order是防止重复评价的兜底手段。后端代码可以做幂等判断,但并发请求同时到达时,只有数据库唯一索引才能真正挡住。
第二,buyer_id和seller_id可以冗余在评价表中。虽然可以从订单表关联查询,但评价数据往往需要长期独立展示,冗余可以减少不必要的关联查询。
第三,total_score用tinyint存储 1 到 5 的整数。如果不允许半星,这个设计是合理的。如果业务允许四星半这种半星评分,就不能再用tinyint,而应该改成decimal(2,1),这一点后面会再提到。
第四,anonymous字段表示用户是否匿名评价。匿名评价展示时不能暴露买家昵称和头像,但后台仍然需要buyer_id来做权限判断。
3.3 维度评价的另外两种设计思路
上面的表把三个维度分作为固定字段放在主表里,适合评价指标相对稳定的场景。如果业务希望后续可以动态增加评价维度,更推荐拆成子表。
维度子表方案如下:
create table review_dimension ( id bigint primary key auto_increment, review_id bigint not null comment '评价主表id', dimension_code varchar(32) not null comment '维度编码 service/quality/speed', dimension_name varchar(50) not null comment '维度名称', score tinyint not null comment '维度评分 1~5', unique key uk_review_dim (review_id, dimension_code) ) engine=innodb default charset=utf8mb4 comment='评价维度表';两种方案没有绝对优劣:
- 固定字段方案查询简单,统计三个维度时不需要
group by,适合指标确定的小型项目。 - 子表方案扩展性好,新增一个“包装满意度”维度时无需改主表结构,但统计时代码更复杂。
建议在没有明确扩展需求前,先用固定字段。后续如果要扩展,可以从主表迁移到子表,或者新增一张维度元数据配置表。
3.4 评价字段的最终数据流
到这里,评价数据已经能够回答很多问题:
- 总评分是多少:看
total_score。 - 用户不满意的点在哪里:看三个维度分。
- 笔订单对应哪笔交易:看
order_id。 - 评价是否匿名显示:看
anonymous。 - 评价发生在什么时间:看
create_time。
这套结构既支撑详情展示,也能支撑后续统计报表。接下来开始写后端代码。
4. 后端实现:评价提交与统计接口
4.1 添加 Maven 依赖
后端使用 Spring Boot 搭建,核心依赖如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>这里使用的 MyBatis-Plus 版本只是一个示例,实际使用时应根据你的项目版本统一。如果项目已经引入 MyBatis-Plus,不要重复引入。
4.2 配置文件
在application.yml中配置数据源和 MyBatis-Plus 相关参数。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/order_review?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case用来把数据库下划线字段自动映射成 Java 驼峰属性,MyBatis-Plus 默认开启,这里再显式写一次以免混淆。
serverTimezone=Asia/Shanghai用来避免数据库连接时出现时区差异导致的时间问题。
4.3 实体类
先创建评价实体类,对应review表。文件路径为src/main/java/com/example/review/entity/Review.java。
package com.example.review.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("review") public class Review { @TableId(type = IdType.AUTO) private Long id; private Long orderId; private Long buyerId; private Long sellerId; private Integer totalScore; private Integer serviceScore; private Integer qualityScore; private Integer speedScore; private String comment; private Integer anonymous; private LocalDateTime createTime; }实体字段和表字段通过驼峰映射自动对应,total_score会映射为totalScore。
同样地,订单表实体不需要把所有字段都写出来,只保留评价逻辑需要判断的字段即可。文件路径为src/main/java/com/example/review/entity/Order.java。
package com.example.review.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("orders") public class Order { @TableId(type = IdType.AUTO) private Long id; private String orderNo; private Long buyerId; private Long sellerId; private Integer orderStatus; private LocalDateTime finishTime; }这里有一个细节:表名orders中的order在 MySQL 里是保留关键字,所以表名要加上复数形式,或者在建表时使用反引号。实体类使用@TableName("orders")指定表名即可。
4.4 接收参数 DTO
前端提交评价时,不应该直接把实体对象作为入参。原因是实体里包含buyerId、sellerId、createTime等后端应该自己决定的字段,如果完全信任前端传入,就存在越权风险。
新增一个请求 DTO,文件路径为src/main/java/com/example/review/dto/ReviewCreateRequest.java。
package com.example.review.dto; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.Data; @Data public class ReviewCreateRequest { @NotNull(message = "订单id不能为空") private Long orderId; @NotNull(message = "综合评分不能为空") @Min(value = 1, message = "综合评分最小为1") @Max(value = 5, message = "综合评分最大为5") private Integer totalScore; @NotNull(message = "服务态度评分不能为空") @Min(value = 1, message = "服务态度评分最小为1") @Max(value = 5, message = "服务态度评分最大为5") private Integer serviceScore; @NotNull(message = "交付质量评分不能为空") @Min(value = 1, message = "交付质量评分最小为1") @Max(value = 5, message = "交付质量评分最大为5") private Integer qualityScore; @NotNull(message = "交付速度评分不能为空") @Min(value = 1, message = "交付速度评分最小为1") @Max(value = 5, message = "交付速度评分最大为5") private Integer speedScore; private String comment; private Integer anonymous; }通过校验注解可以拦截无效分数,例如用户传入 6 分或 0 分,后端接口会直接返回参数校验异常。
4.5 Mapper 层
为了让代码结构完整,先创建ReviewMapper和OrderMapper。文件路径为src/main/java/com/example/review/mapper/ReviewMapper.java。
package com.example.review.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.review.entity.Review; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.Map; @Mapper public interface ReviewMapper extends BaseMapper<Review> { Map<String, Object> selectScoreStat(@Param("sellerId") Long sellerId); Map<String, Object> selectDimensionAvg(@Param("sellerId") Long sellerId); }selectScoreStat用于统计某个卖家的总体评分情况,selectDimensionAvg用于统计各维度平均分。对应的 SQL 写在 XML 里。
OrderMapper只需要继承BaseMapper就行:
package com.example.review.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.review.entity.Order; import org.apache.ibatis.annotations.Mapper; @Mapper public interface OrderMapper extends BaseMapper<Order> { }MyBatis-Plus 的BaseMapper已经提供了selectById、insert、updateById等基础方法,这里不需要额外手写。
4.6 核心业务 Service
评价提交的完整流程图可以拆成下面几步:
- 根据
orderId查询订单。 - 判断订单是否存在,不存在则提示“订单不存在”。
- 判断当前登录用户是否是订单中的买家。如果不是,拒绝提交。
- 判断订单状态是否为“已完成”。
- 判断该订单是否已经被评价过。如果已评价,提示用户不能重复评价。
- 构造评价对象并插入数据库。
- 如果遇到数据库唯一索引冲突,也视为重复评价。
- 如果评分为四星或更低,触发售后回访逻辑。
文件路径为src/main/java/com/example/review/service/ReviewService.java。
package com.example.review.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.review.dto.ReviewCreateRequest; import com.example.review.entity.Order; import com.example.review.entity.Review; import com.example.review.mapper.OrderMapper; import com.example.review.mapper.ReviewMapper; import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; import java.util.Map; import java.util.Objects; @Service @RequiredArgsConstructor public class ReviewService { private final ReviewMapper reviewMapper; private final OrderMapper orderMapper; @Transactional(rollbackFor = Exception.class) public void createReview(ReviewCreateRequest request, Long currentUserId) { if (currentUserId == null) { throw new IllegalArgumentException("登录状态失效"); } Order order = orderMapper.selectById(request.getOrderId()); if (order == null) { throw new IllegalArgumentException("订单不存在"); } if (!Objects.equals(order.getBuyerId(), currentUserId)) { throw new IllegalArgumentException("只能评价自己下单的订单"); } if (!Objects.equals(order.getOrderStatus(), 2)) { throw new IllegalArgumentException("订单未完成,不能评价"); } Long count = reviewMapper.selectCount( new LambdaQueryWrapper<Review>() .eq(Review::getOrderId, order.getId()) ); if (count != null && count > 0) { throw new IllegalArgumentException("该订单已评价,不能重复评价"); } Review review = new Review(); review.setOrderId(order.getId()); review.setBuyerId(order.getBuyerId()); review.setSellerId(order.getSellerId()); review.setTotalScore(request.getTotalScore()); review.setServiceScore(request.getServiceScore()); review.setQualityScore(request.getQualityScore()); review.setSpeedScore(request.getSpeedScore()); review.setComment(request.getComment()); review.setAnonymous(request.getAnonymous() == null ? 0 : request.getAnonymous()); try { reviewMapper.insert(review); } catch (DuplicateKeyException e) { throw new IllegalArgumentException("该订单已评价,不能重复评价"); } handleFourStarReview(review); } private void handleFourStarReview(Review review) { if (review.getTotalScore() == null || review.getTotalScore() >= 5) { return; } boolean needFollowUp = false; if (review.getServiceScore() <= 3) { needFollowUp = true; } if (review.getQualityScore() <= 3) { needFollowUp = true; } if (review.getSpeedScore() <= 3) { needFollowUp = true; } if (needFollowUp) { // 实际项目中,这里可以写入回访任务表,或者发送 MQ 消息给售后系统。 // 伪代码: // FollowUpTask task = new FollowUpTask(); // task.setOrderId(review.getOrderId()); // task.setReviewId(review.getId()); // task.setType("LOW_DIMENSION_SCORE"); // task.setStatus("PENDING"); // followUpTaskMapper.insert(task); } } }这里有几个地方值得展开说明。
第一,方法上加了@Transactional,保证订单校验、重复评价判断、评价插入这几个动作在同一事务中。如果中途出现异常,已经执行的插入操作会回滚。
第二,先查一次订单是否已评价,再插入评价,是为了给用户返回更友好的提示。纯粹依赖数据库唯一索引虽然能防止脏数据,但抛出DuplicateKeyException后,接口层需要做一层异常转换,否则用户会看到晦涩的数据库错误。
第三,handleFourStarReview只是一个示例方法。四星评价不一定是负面评价,但在售后运营中,四星评价配合“某个维度低于 3 分”这个条件,通常能识别出一批真实体验有瑕疵的订单。实际落地时可以把判断规则放进消息通知或回访工单系统。
4.7 统计接口
卖家详情页需要展示三个关键数据:综合平均分、评分分布、维度平均分。我们通过自定义 XML 查询来完成。
文件路径为src/main/resources/mapper/ReviewMapper.xml。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.review.mapper.ReviewMapper"> <select id="selectScoreStat" resultType="map"> SELECT ROUND(AVG(r.total_score), 2) AS avgScore, COUNT(*) AS totalCount, IFNULL(SUM(CASE WHEN r.total_score = 5 THEN 1 ELSE 0 END), 0) AS fiveStarCount, IFNULL(SUM(CASE WHEN r.total_score = 4 THEN 1 ELSE 0 END), 0) AS fourStarCount, IFNULL(SUM(CASE WHEN r.total_score <= 3 THEN 1 ELSE 0 END), 0) AS lowStarCount FROM review r WHERE r.seller_id = #{sellerId} </select> <select id="selectDimensionAvg" resultType="map"> SELECT ROUND(AVG(r.service_score), 2) AS serviceAvg, ROUND(AVG(r.quality_score), 2) AS qualityAvg, ROUND(AVG(r.speed_score), 2) AS speedAvg FROM review r WHERE r.seller_id = #{sellerId} </select> </mapper>注意 XML 中<=需要写成<=,否则 XML 解析会出错。如果这个 SQL 写不进 XML,也可以改用大于号的条件再取反,但不如转义符直观。
然后在ReviewService中新增统计方法:
public Map<String, Object> statBySeller(Long sellerId) { Map<String, Object> result = new HashMap<>(); result.put("scoreStat", reviewMapper.selectScoreStat(sellerId)); result.put("dimensionAvg", reviewMapper.selectDimensionAvg(sellerId)); return result; }统计结果大致长这样:
{ "scoreStat": { "totalCount": 120, "avgScore": 4.72, "fiveStarCount": 90, "fourStarCount": 24, "lowStarCount": 6 }, "dimensionAvg": { "serviceAvg": 4.80, "qualityAvg": 4.75, "speedAvg": 4.55 } }如果发现avgScore不低,但fourStarCount占比明显偏高,说明大量用户对服务存在“基本满意但未达惊喜”的状态,这是提升复购和口碑时需要重点关注的人群。
4.8 Controller 层
文件路径为src/main/java/com/example/review/controller/ReviewController.java。
package com.example.review.controller; import com.example.review.dto.ReviewCreateRequest; import com.example.review.service.ReviewService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/review") @RequiredArgsConstructor public class ReviewController { private final ReviewService reviewService; @PostMapping public String submit(@RequestBody @Valid ReviewCreateRequest request, @RequestAttribute("currentUserId") Long currentUserId) { reviewService.createReview(request, currentUserId); return "评价成功"; } @GetMapping("/stat") public Map<String, Object> stat(@RequestParam Long sellerId) { return reviewService.statBySeller(sellerId); } }实际项目里,currentUserId一般由登录拦截器从 Token 中解析后放入RequestAttribute,而不是由前端直接传 userId。这里用@RequestAttribute只是为了演示后端如何获取当前操作人。如果直接接收前端传入的buyerId,会存在越权评价风险。
5. 前端实现:星级评分组件
后端接口准备好之后,前端需要完成两件事:一个是买家提交评价的表单,另一个是卖家评分展示区。
5.1 评价表单页面
使用 Vue 3 + Element Plus 实现评价表单。核心评分组件是el-rate。文件路径为frontend/src/views/ReviewForm.vue。
<template> <div class="review-form"> <h3>评价本次订单</h3> <div class="rate-row"> <span class="rate-label">综合评分</span> <el-rate v-model="form.totalScore" :max="5" show-score /> </div> <div class="rate-row"> <span class="rate-label">服务态度</span> <el-rate v-model="form.serviceScore" :max="5" /> </div> <div class="rate-row"> <span class="rate-label">交付质量</span> <el-rate v-model="form.qualityScore" :max="5" /> </div> <div class="rate-row"> <span class="rate-label">交付速度</span> <el-rate v-model="form.speedScore" :max="5" /> </div> <el-input v-model="form.comment" type="textarea" maxlength="500" show-word-limit placeholder="说说这次体验吧,可以给其他用户一些参考" /> <div class="form-footer"> <el-checkbox v-model="form.anonymous">匿名评价</el-checkbox> <el-button type="primary" :loading="submitting" @click="submitReview"> 提交评价 </el-button> </div> </div> </template> <script setup> import { reactive, ref } from 'vue' import { ElMessage } from 'element-plus' import request from '@/utils/request' const props = defineProps({ orderId: { type: Number, required: true } }) const submitting = ref(false) const form = reactive({ orderId: props.orderId, totalScore: 5, serviceScore: 5, qualityScore: 5, speedScore: 5, comment: '', anonymous: false }) async function submitReview() { if (!form.totalScore || !form.serviceScore || !form.qualityScore || !form.speedScore) { ElMessage.warning('请完成所有评分项') return } submitting.value = true