简介:基于JSP的商品管理与评价系统完整项目,面向Java Web学习者和课程设计/毕业设计场景,重点解决电商平台中商品维护与用户评分反馈的实现问题。系统涉及管理员端商品增删改查、浏览者评分评价、登录验证、权限控制与事务处理等典型环节,可作为理解MVC分层开发的入门范例。压缩包共275个文件、1.39MB,主要包含38个JSP页面、58个Java源文件及关联的class文件,另有jar依赖库和一批gif/jpg/bmp图片素材,以及项目配置与元数据文件;目录结构清晰,适合导入开发环境对照学习。已有131人学习。通过研读源码和页面,可以快速掌握商品表单处理、文件上传、数据库读写、权限校验和评分统计等常见Web开发思路,并便于扩展购物车、订单管理等电商功能。
1. star.zip 商品管理评价系统:拆开压缩包前先弄明白它值不值得做
一个命名像star.zip的压缩包,放到电商后台项目里,多半就是一套商品管理加评价系统。这类系统做起来不算难,但坑特别多:商品状态一变,评价还要跟着审核;用户刚提交的五星好评,可能因为缓存问题显示不出来;库存超卖导致的退款,会让商品评分被恶意差评带节奏。这篇笔记我从数据模型、接口设计到评分聚合算法,把这个方案按可复现的步骤拆开,适合刚接触电商后台的后端开发,或者打算自建商城的小团队。别一上来就去找现成轮子,先搞懂它内部该怎么搭,后面改起来才不慌。
2. 从数据模型入手:商品与评价的表结构怎么设计才不返工
商品管理和评价系统,本质是一对一的主从关系:一个商品对应多条评价,评价反过来影响商品的展示分数。如果一上来只建两张表,后面加标签、加图片、加审核状态,就会改得想骂人。我一般会先画清楚核心实体,再写建表 SQL。常见做法是商品表单独存商品基本信息,评价表冗余订单与用户关联字段,避免频繁 join。
2.1 商品表用单表还是 SPU/SKU 双表?小项目我建议先单表
很多教程一开口就是 SPU、SKU,好像不做双表就不专业。实际中小商城一个商品就是一个规格,强行拆成 SPU/SKU 只是增加关联查询。我见过一个项目,商品就几千条,却用了三张表,联表查询慢得吓人,最后又回到单表。合理的做法是:如果商家不需要多规格(颜色、尺码),单表就够了;等真正有规格需求,再把sku_id作为后加字段拆分出去。
商品表的最小字段应该包括:商品编号(业务唯一键)、商品名称、分类 id、主图 URL、售价、成本价(可选)、库存、状态、创建时间。其中状态是核心,后面接口要依赖它。多说一句,不少二手项目从 Excel 导入商品,编号是乱写的,后面同步订单时会拼不起来,所以我建表时把product_no设计为 NOT NULL UNIQUE,并且初始化时用规则生成,比如分类前缀加时间戳再加随机数,避免靠肉眼判断是哪一条。
2.2 评价表的五个必备维度:评分、内容、图片、标签、状态
评价系统最容易犯的错是只存一个 score 和 comment。实际上评价有三个要素:分数、文字、图片,还有一个时常被忽略的标签(商家回复/追评)。做评价表时,我建议把以下字段列为基础:
score:1~5 整数,注意前端和后端都要校验范围content:文本,最长 1000 字左右images:JSON 数组,不要拆成子表(除非需要识别图片内容)is_anonymous:匿名标记,影响展示status:pending/approved/rejected 状态order_id与user_id:关联订单和用户,防刷的关键- 追评字段:
parent_id或append_content
这里最重要的约束是唯一索引:一个用户对一个订单的一个商品,只能有一条评价。这是后面防重复提交的兜底。还有一个我踩过的坑:为了“灵活”把图片拆成子表,后台编辑是方便了,但列表查询多一次 join,图片子表在促销季迅速膨胀到十几万行,最后不得已又合并回去。所以我的建议是,除非你要对每张图片做内容和审核打标,否则 JSON 字段够用,而且还能省掉一次关联查询。
2.3 建表 SQL 与关键索引:把这些约束一次性加对
拿 MySQL 8 的 InnoDB 举例,我会写下面这样两张表。先创建商品表:
CREATE TABLE `product` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `product_no` VARCHAR(32) NOT NULL COMMENT '业务商品编号', `name` VARCHAR(128) NOT NULL, `category_id` BIGINT NOT NULL DEFAULT 0, `price` DECIMAL(10,2) NOT NULL, `stock` INT UNSIGNED NOT NULL DEFAULT 0, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0下架 1草稿 2上架', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_product_no` (`product_no`), KEY `idx_category_status` (`category_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;商品表要注意两点:一是用product_no做业务键,而不是直接用自增 id,方便后续同步和排查历史数据;二是category_id和status的联合索引,让按分类查上架商品的列表走索引,避免全表扫。
再建评价表:
CREATE TABLE `evaluation` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `product_id` BIGINT NOT NULL, `user_id` BIGINT NOT NULL, `order_id` BIGINT NOT NULL, `score` TINYINT NOT NULL COMMENT '1-5', `content` VARCHAR(1000) DEFAULT '', `images` JSON DEFAULT NULL, `is_anonymous` TINYINT NOT NULL DEFAULT 0, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待审 1通过 2驳回', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_order_product_user` (`order_id`, `product_id`, `user_id`), KEY `idx_product_status` (`product_id`, `status`), KEY `idx_product_score` (`product_id`, `score`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里最关键的坑踩过的人多半都记得:唯一索引字段顺序很重要。如果把user_id放第一个,可能造成同一用户在同一订单中给不同商品评价时被误锁。uk_order_product_user的意思是同一个订单下同一个商品同一个用户最多一条评价,这是营销活动时用户重复评价的第一道防线。
评价表的images字段用 JSON,可以省一张子表,但在统计“有图评价”时需要JSON_EXTRACT,数据量大时会慢。等真的需要按图片维度做审核,再单独拆图库表也不迟。
商品表后面还要加评分、评论数、好评率这些冗余字段。别怕冗余,查询性能才是命。直接加:
ALTER TABLE product ADD COLUMN rating_score DECIMAL(2, 1) NOT NULL DEFAULT 0 COMMENT '综合评分', ADD COLUMN review_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '评价总数', ADD COLUMN good_rate DECIMAL(3, 2) NOT NULL DEFAULT 0 COMMENT '好评率';这三列会被列表页高频读取,如果每次都去评价表聚合,后面必然翻车。更新时机在第 4 章讲。
还有一个容易被忽略的点:商品表不要物理删除。如果一个商品已经产生订单和评价,物理删行会导致订单详情页查不到商品名。加一个deleted字段(TINYINT,默认0),查询条件统一过滤deleted=0。评价表同理,用户要求删除自己的评价时,把status置为已删除,而不是真的 DELETE 一行。
3. 把商品管理跑起来:接口设计与状态流转
商品管理最能体现设计功底的是状态流转。上架、下架、删除,每一步都影响用户能否购买和评价。评价接口则比预想的复杂:你必须校验订单归属,否则任何人都能给任意商品刷分。下面的代码我基于 FastAPI 写,逻辑可以直接移植到 Spring Boot 或其他框架。
3.1 商品上架流程:草稿→上架→下架的状态机
商品状态用数字表示,0下架,1草稿,2上架。用户在前台只能看到状态为 2 的商品。后端接口只接收两个动作:上架(从 0 或 1 到 2)和下架(从 2 到 1)。不允许随便跳转,比如从下架直接到草稿。状态机写得太散是后端常见的“血泪教训”,所以我习惯把合法变化集中到一张表里,用表驱动:
from fastapi import APIRouter, HTTPException router = APIRouter(prefix="/products", tags=["products"]) VALID_TRANSITIONS = { "publish": {(0, 2), (1, 2)}, "unpublish": {(2, 1)}, } @router.put("/{product_id}/status") def change_status(product_id: int, action: str): product = get_product_by_id(product_id) if product is None: raise HTTPException(status_code=404, detail="商品不存在") current = product.status target = 2 if action == "publish" else 1 if (current, target) not in VALID_TRANSITIONS.get(action, set()): raise HTTPException(status_code=400, detail=f"非法的状态变化:{current} -> {target}") # 乐观锁更新,防止两个管理员同时对同一个商品操作 updated = update_product_status( product_id, old_status=current, new_status=target ) if not updated: raise HTTPException(status_code=409, detail="状态已被修改,请重试") return {"ok": True, "new_status": target}这段代码有几个关键点。第一,用VALID_TRANSITIONS把状态变化表驱动,而不是散落在 if/else 里,后续加“售罄自动下架”只需加一条规则。第二,update_product_status必须用UPDATE ... WHERE status = 旧状态的方式,避免两个管理员同时上架导致脏状态。第三,action参数只传动词,不直接传目标状态,防止前端想传什么就传什么。
实际项目里还会把“上架”进一步限制为“库存大于 0 且商品价格大于 0”,否则会出现上架了却无法购买的黑匣子问题。建议在事务里先校验这些商业规则,再执行状态更新。另外状态字段在代码里最好用枚举类,不要到处写裸数字,不然排查问题时你会看到一堆status == 2,完全不知道 2 是什么意思。
3.2 商品查询接口:列表分页与排序参数
商品列表接口是读写比最高的接口,要支持分类筛选、关键字搜索、价格/销量/评分排序。排序字段如果直接拼进 SQL,容易被注入,所以我会先做白名单校验:
from fastapi import Query # sort 只允许枚举值,禁止用户传任意字段名 @router.get("") def list_products( category_id: int | None = None, keyword: str | None = None, sort: str = Query("latest", pattern="^(latest|price_asc|price_desc|rating|sales)$"), page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100) ): query = select(Product).where(Product.status == 2, Product.deleted == 0) if category_id: query = query.where(Product.category_id == category_id) if keyword: query = query.where(Product.name.like(f"%{keyword}%")) if sort == "price_asc": query = query.order_by(Product.price.asc()) elif sort == "price_desc": query = query.order_by(Product.price.desc()) elif sort == "rating": query = query.order_by(Product.rating_score.desc()) elif sort == "sales": query = query.order_by(Product.sales.desc()) else: query = query.order_by(Product.create_time.desc()) products = paginate(query, page=page, size=size) return {"items": products, "total": get_total_hit_count(category_id, keyword)}这里有两个坑。第一,keyword 的 LIKE 查询在数据量过 10 万后会很慢,建议改为 MySQL 的全文索引或者单独上 Elasticsearch,但对于小项目 LIKE 足够。第二,sort=rating不应该实时去评价表聚合,而是直接读商品表里的rating_score冗余字段。这个字段由写入侧维护,列表页只负责展示。
扩展一下,价格排序我刻意拆成price_asc和price_desc两个枚举,而不是一个price再带方向参数。原因是前端经常把方向写反,拆开以后语义清楚,后端也少一层判断。
3.3 评价提交接口:星级校验与订单关联
评价提交是评价系统的入口,也是刷分重灾区。
from pydantic import BaseModel, Field class EvaluationIn(BaseModel): order_id: int product_id: int score: int = Field(ge=1, le=5) content: str = Field(default="", max_length=1000) images: list[str] = [] is_anonymous: bool = False @router.post("/evaluations") def create_evaluation(req: EvaluationIn, user_id: int): # 1. 校验订单属于该用户且包含该商品 order = get_order_by_id(req.order_id) if order is None or order.user_id != user_id: raise HTTPException(status_code=403, detail="订单不存在或无权评价") if req.product_id not in order.items: raise HTTPException(status_code=400, detail="该商品不在订单中") # 2. 校验订单状态,已收货才能评价 if order.status != "completed": raise HTTPException(status_code=400, detail="订单未完成不能评价") # 3. 插入评价,依赖数据库唯一索引兜底 eval_id = insert_evaluation(req, user_id) # 4. 标记录订单已评价,避免用户重复发起 mark_order_reviewed(req.order_id, req.product_id, user_id) return {"ok": True, "evaluation_id": eval_id}这里的细节值得每个入参说明。score的Field(ge=1, le=5)是后端的最后一道门,前端可能校验失灵,但后端必须兜住。order_id和user_id的校验防止跨订单刷分,是整个防刷的核心。order.status要求已完成,避免货都没收到就开始差评。
插入评价后,还有一个重要步骤:顺手给订单标记“已评价”。这个标记可以在订单表加一个review_status字段,也可以用订单评价记录表。如果不做这个标记,用户就会反复进入评价页,提交第二次时被唯一索引拦截,却看不到任何友好提示。我建议返回一个统一的错误码,比如PRODUCT_REVIEWED,前端拿到后直接跳转“已评价列表”。
商品管理和评价提交的最小闭环到这里已经跑通。但还有个大问题等着你:用户的评分提交后,商品详情页上的“综合评分”是怎么算出来的?这就是下一章的内容。
4. 评分聚合与排序:评价系统的难点在聚合计算
很多系统上线后出现“好评如潮但商品分一动不动”的翻车问题,根源是把聚合逻辑放错了地方。评价提交后,商品表的rating_score必须更新,但这个更新不是简单重算平均值,而是要处理匿名、未审核、恶意低分等干扰。
4.1 商品综合评分怎么算:加权平均与好评率的配合
一个商品的详情页通常会显示两块内容:星级评分(4.8 分)和好评率(98%)。这两者看似一样,实际不同。星级是用户打分的算术平均,好评率是score >= 4的评价占比。你要两个都算,不能只算一个。
常见做法是在评价表里加一个is_good冗余字段,分数大于等于 4 就是好,提交时算好,避免聚合时再逐条判断。商品表则冗余rating_score和good_rate。
计算逻辑用 SQL 可以一次搞定:
-- 提交一条审核通过的评价后,重新聚合该商品 UPDATE product p JOIN ( SELECT product_id, ROUND(AVG(score), 1) AS avg_score, ROUND(SUM(CASE WHEN score >= 4 THEN 1 ELSE 0 END) / COUNT(*), 2) AS good_rate, COUNT(*) AS review_count FROM evaluation WHERE status = 1 GROUP BY product_id ) t ON p.id = t.product_id SET p.rating_score = t.avg_score, p.good_rate = t.good_rate, p.review_count = t.review_count WHERE p.id = 目标商品;注意这里有两个参数容易调错。第一,只统计status = 1(通过)的评价,待审和驳回的绝不能进均分;第二,AVG 要 ROUND 到一位小数,否则前端展示 4.833333 很丑。第三,好评阈值不是拍脑袋定 4,很多平台把 4 星和 5 星算好评,3 星算中评,这是约定的业务规则;你可以用参数表配好,别写死在代码里。
但这段 SQL 是同步重算,如果评价提交接口里直接跑,且商品评价多,数据库会被拖垮。所以更合理的方案是把聚合放到异步队列里。后面第 6 章会专门说兜底逻辑。
4.2 评价列表排序:时间、热度与置顶的平衡
商品评价列表看起来只有“最新/最热”两个排序,实际业务场景里还有商家置顶、精华评价、追评加权。我踩过的一个问题:直接按时间倒序,商家回复过的评价和普通评价混在一起,用户体验混乱。后来我加了一个top_weight字段,置顶评价排序权重高。
排序 SQL 设计:
SELECT id, score, content, images, create_time FROM evaluation WHERE product_id = ? AND status = 1 ORDER BY top_weight DESC, create_time DESC LIMIT ?, ?;top_weight默认 0,商家在管理系统里可以针对某条评价设 1。这个字段与create_time联合排序,可以在不把普通评价挤没的情况下把重点评价放前面。
热度排序则不能简单用点赞数。点赞表如果拆成子表,每次排序都要 count,很快性能就崩。我给 evaluation 表冗余一个like_count字段,点赞时做 INCR,排序时直接 order bylike_count。恶意刷点赞的问题,防刷策略在第 6 章展开。
另外,追评的数据结构也要处理。如果追评作为新行插入,列表页会出现两个评价。我使用的是在同一行里加append_content和append_time字段,追评本质是一次 update,而不是 insert。这样列表查询不用处理父子级,代码简单很多。
4.3 用缓存扛住热点商品的评价统计
商品详情页的评分、好评率、评价总数通常不会每秒变化,但热点商品每秒被访问几十次。如果每次请求都去 group by 聚合,数据库再强也扛不住。这里我会用一个简单的 Redis 缓存方案。
更新策略是“写后失效”:
def invalidate_product_rating(product_id: int): redis_client.delete(f"rating:{product_id}") def get_product_rating(product_id: int): cache_key = f"rating:{product_id}" cached = redis_client.get(cache_key) if cached: return json.loads(cached) # 从 product 表读取冗余字段 data = load_rating_from_db(product_id) redis_client.setex(cache_key, 600, json.dumps(data)) return data缓存时间设 600 秒,也就是 10 分钟。为什么要设过期而不是永久?因为一旦异步聚合任务失败,最多只能影响 10 分钟,不会永远黑匣子。这也是我遇到的真实事故:一次消息队列积压导致评分两个小时没更新,用户端评分和详情页评论数对不上,投诉一片。后来我把失效时间调短,并加了对账定时任务。
还有一个细节:评价提交接口里,事务提交后要先 update product 冗余字段,再 delete cache,让下一次读回源。不要用“先删缓存后更新 DB”的顺序,那会在并发时写入旧数据。具体操作请记住:事务提交后,先 update product 冗余字段,再 delete cache。
5. 商品管理与评价系统的五个必踩坑:现象、原因、怎么解
这个阶段最容易踩的不是新功能,而是边界问题。下面五条是我在真实项目中处理过的,按“现象→原因→解决”的顺序写,方便新手照方抓药。
5.1 商品改价后,用户购物车还显示旧价格并下单
现象:后台把商品从 100 元改成 120 元,前端详情页显示新价,但购物车里还是 100 元,用户结算成功,对不上账。
原因:购物车表里存了价格快照,但没有在下单时重新校验商品实时价。这是很多人犯的低级错误。
解决:在创建订单接口里,不要直接信任购物车传过来的价格,而是从商品表里重新读取当前价格,并和购物车快照做比对。如果变了,要么提示用户价格已更新,要么以最新价为准。我的做法是后端以商品表为准,前端购物车只展示参考价格。订单快照里则保存下单瞬间的价格,避免后续历史订单金额随商品改价而变动。
5.2 评价提交成功后,商品页评分一直没有变化
现象:用户提交五星好评,商品详情页的评分还是昨天的 4.6。
原因:三个地方只做了其中一环。要么提交接口只插了 evaluation 表,没有触发聚合;要么聚合是异步的但队列失败;要么缓存没失效,一直返回旧值。
解决:按顺序排查。先看 evaluation 里有没有这条记录,status 是否为通过。再看异步任务日志里有没有报错。最后查 Redis 里rating:{product_id}的 TTL,如果缓存还在,手动删掉再看。如果是状态机问题,记住,只有status = 通过的评价才能进入聚合,待审的不算;你刚刚提交的评价可能还在审核队列里。
5.3 商品列表按销量排序,结果数字和实际订单对不上
现象:管理后台的销量是 1000,用户端排序却把销量 500 的商品排在前面。
原因:销量字段更新时机不对。有些实现是在订单完成时给product.sales加 1,但订单完成后又发生退款,没有做减量。还有的是在支付回调里加,但扫码支付回调偶发丢失。
解决:两个动作必须成对操作。支付成功增加 sales,退款成功减少 sales。如果已经出现不一致,直接写一条 SQL 从订单明细表重新聚合一次:
UPDATE product p LEFT JOIN ( SELECT item.product_id, COUNT(*) AS sales FROM order_item item JOIN orders o ON item.order_id = o.id WHERE o.status = 'paid' GROUP BY item.product_id ) t ON p.id = t.product_id SET p.sales = IFNULL(t.sales, 0);注意统计口径:未支付的订单不算,退款的单要在order.status里排除。确认好这个口径后,把它同样应用到商品列表的排序缓存上,避免 SQL 里算的是一套、缓存里是另一套。
5.4 并发下单导致库存变成负数
现象:100 件库存,两个人同时下单,成功 150 件,库存显示 -50。
原因:库存扣减没有用原子操作。常见的错误是先查库存,if 库存大于 0,再 update 库存减一,中间有并发空档。
解决:用一句 SQL 扣库存:
UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0;affected rows 为 0 就说明库存不足。绝不能用UPDATE ... SET stock = stock - 1不带stock > 0条件。再配合订单创建放在事务里,加上唯一索引防重复,超卖就能解决。评论系统虽然和库存无关,但商品都超卖了,用户收货后差评说“等了三天没货”,最终影响的是评价分。
5.5 评价上传的图片在商品页显示裂图
现象:评价里能看见图片,但商品评价列表里图片打不开,控制台报 403。
原因:图片上传到了本地临时目录,或者云存储 Bucket 权限设成了私有,前端没有带签名 URL。
解决:把图片迁移到对象存储,上传路径写为evaluation/{product_id}/{timestamp}.jpg,读取时通过 CDN 域名访问,并设置公开读权限。如果必须是私有读,则列表接口要返回临时签名 URL,而不是原始 URL。本地存储只用于开发环境,但也要加一个静态目录映射,否则换个环境就裂。
6. 进阶:评价审核流与防刷策略,让系统上线后少挨骂
6.1 审核状态机:待审→通过/驳回
评价表里的 status 字段,就是审核状态机。提交时是待审,管理员操作后变成通过或驳回。这里有一个小技巧:不要把驳回理由写到 status 字段里,而是单独加reject_reason。否则你要么多一张审核记录表,要么每次审核后 status 都变成一个带理由的复合值,查询时没法用索引。
我习惯的接口是PUT /evaluations/{id}/review,请求体传pass=true或者pass=false + reason。通过后立即触发评分聚合,驳回后给用户发站内信通知。不要漏了通知,不然用户不知道评价为什么消失。
6.2 防刷与重复评价校验:订单维度才是第一道防线
评价刷分最常见的手法是用小号反复买,然后给同一个商品刷五星或一星。靠 IP 限制基本没用,手机基站一换 IP 就变。靠用户注册时间也不准。真正能压住的是订单维度校验:一个用户在同一订单中对同一商品只能评价一次,并且订单状态必须是已完成。
除此之外,加一个幂等键是很有必要的。评价提交接口接收client_token,前端在进入评价页时生成 UUID,提交时带上。后端用这个 token 做去重,可以挡住用户快速点击提交带来的重复插入。甚至比单靠唯一索引更友好,因为重复请求不会报数据库冲突错,而是直接返回第一次的 evaluation_id。
6.3 异步兜底:定时重算保证数据最终一致
聚合计算放在评价提交接口里同步执行,在小流量阶段没问题,但一次活动就能把数据库拖垮。常见方案是只更新evaluation表和商品冗余字段的缓存失效标记,真正的聚合 SQL 交给消息队列去跑。如果消息队列挂了,就需要一个兜底定时任务。
-- 每天凌晨重算前一天活跃商品的评分 UPDATE product p JOIN ( SELECT product_id, ROUND(AVG(score), 1) AS avg_score, ROUND(SUM(CASE WHEN score >= 4 THEN 1 ELSE 0 END) / COUNT(*), 2) AS good_rate, COUNT(*) AS review_count FROM evaluation WHERE status = 1 AND create_time >= DATE_SUB(CURDATE(), INTERVAL 1 DAY) GROUP BY product_id ) t ON p.id = t.product_id SET p.rating_score = t.avg_score, p.good_rate = t.good_rate, p.review_count = t.review_count WHERE t.review_count > 0;这个定时任务跑完,再清理对应的 Redis 缓存,让下个请求重新回源。注意不要全量重算所有商品,只算前一天有更新的商品,否则几千个商品会引发一批无用 update。
上线前多想想审核流和防刷,比上线后连夜打补丁强得多。我自己就吃过没有幂等键的亏,活动开始半小时,评价表里出现几百条重复记录,商品评分直接被刷花。从那以后,所有写接口我都会先问一句:同一个用户同一时间点两次,数据库会不会产生脏数据?想清楚这个问题,评价系统的大半坑就避开了。希望上面的方案能帮你少踩几个坑。
本文还有配套的精品资源,点击获取