HarmonyOS宠物邻里实战第17篇:寄养评价发布、重复提交防护与通知回归测试
HarmonyOS宠物邻里实战第17篇:寄养评价发布、重复提交防护与通知回归测试
摘要
寄养业务做到 done 并不算结束。对宠物主人来说,完整闭环还要包含“给寄养人评价”;对寄养人来说,评价会影响后续信任、接单意愿和个人主页数据。评价接口如果只做一个简单 insertOne,很容易出现未完成就评价、寄养人给自己刷评价、同一条记录重复评价、评分越界、标签过长、通知不触达等问题。
本文基于宠物邻里 HarmonyOS + Express + MongoDB 项目,复盘寄养评价发布链路:
- 为什么评价必须依赖
fosterRecords.status === 'done'; - 后端如何校验 1 到 5 星评分;
- 为什么只有宠物主人可以评价;
- 如何防止同一寄养记录重复评价;
fosterReviews、profiles.hostStats、notices三个集合如何同步;- HarmonyOS 前端如何展示“待评价”和“已发布评价”;
- 端到端集成测试如何覆盖 400、403、409 和成功路径;
- 最后给出上线前验收清单。
这篇文章重点不是“做一个评价表单”,而是把评价当成寄养状态机的最后一环来验证。
工程背景与版本信息
| 文件 | 作用 |
|---|---|
houduan/test/routes/api.js |
寄养记录、评价、通知接口 |
houduan/test/test/integration.js |
端到端集成测试 |
houduan/test/db.js |
MongoDB 连接 |
MyApp/entry/src/main/ets/pages/foster/FosterReviewListPage.ets |
待评价和已评价列表 |
MyApp/entry/src/main/ets/pages/foster/FosterReviewCreatePage.ets |
评价发布页 |
MyApp/entry/src/main/ets/components/foster/FosterReviewForm.ets |
星级、标签、内容输入 |
MyApp/entry/src/main/ets/pages/foster/FosterRecordDetailPage.ets |
记录完成后的评价入口 |
MyApp/entry/src/main/ets/services/BackendService.ets |
后端接口语义层 |
环境信息:
| 项目 | 值 |
|---|---|
| HarmonyOS 工程模型 | modelVersion: 6.0.2 |
| target SDK | 6.0.2(22) |
| 后端框架 | Express ~4.16.1 |
| MongoDB Driver | ^4.17.2 |
| 测试命令 | npm run check、npm run test:integration |
| 评价接口 | POST /api/foster-records/:id/reviews |
版本边界也要写清楚。本文的 @StorageLink、@Watch、TextArea.onChange、Express 4 路由中间件、MongoDB Driver 4 的写入返回结构,都基于当前项目验证。如果升级到 Express 5 或更换 MongoDB Driver 大版本,需要重新跑评价链路回归测试,重点检查异步错误处理、insertOne 返回、findOne 查询条件和前端状态刷新。
cd D:\APP\chong_wu_guan_li\houduan\test
npm run check
npm run test:integration

一、评价链路在业务中的位置
寄养主链路可以拆成下面几步:
发布寄养需求
-> 寄养人申请
-> 宠物主人接受申请
-> 自动生成 fosterRecord(waitingIn)
-> 寄养开始 ongoing
-> 寄养完成 done
-> 宠物主人发布评价
-> 寄养人收到通知
评价不是独立功能,它依赖前面的状态机。如果 fosterRecord 还没有进入 done,就允许发布评价,数据就会提前沉淀到 fosterReviews,后续再取消或回退状态时会变得很难处理。
因此评价接口的第一条原则是:只允许完成后的寄养记录发布评价。
二、后端接口入口
评价接口在 routes/api.js 中:
router.post('/foster-records/:id/reviews', async function(req, res, next) {
try {
const db = getDatabase();
const record = await db.collection('fosterRecords').findOne({ id: req.params.id });
if (!record) {
return fail(res, 404, '寄养记录不存在');
}
if (record.status !== 'done') {
return fail(res, 400, '寄养结束后才能评价');
}
const authorId = req.auth.profileId;
const rating = Number(req.body.rating || 0);
const content = text(req.body.content, 2000);
if (!authorId || !Number.isInteger(rating) || rating < 1 || rating > 5 || !content) {
return fail(res, 400, '请填写完整评价');
}
const pet = await db.collection('pets').findOne({ id: record.petId, ownerId: authorId });
if (!pet) {
return fail(res, 403, '只有宠物主人可以评价寄养服务');
}
if (await db.collection('fosterReviews').findOne({ recordId: req.params.id, authorId })) {
return fail(res, 409, '该寄养记录已经评价');
}
const review = {
id: text(req.body.id, 80) || `rev_${Date.now()}`,
recordId: req.params.id,
authorId,
hostId: record.hostId,
rating,
ratingTags: textArray(req.body.ratingTags, 10, 30),
content,
photos: textArray(req.body.photos, 9, 300),
publishedAt: text(req.body.publishedAt, 30) || new Date().toISOString().slice(0, 10),
createdAt: new Date()
};
await db.collection('fosterReviews').insertOne(review);
await db.collection('profiles').updateOne(
{ id: record.hostId },
{ $inc: { 'hostStats.fosterCount': 1 } }
);
await db.collection('notices').insertOne({
id: `n_${Date.now()}`,
targetUserId: record.hostId,
kind: 'system',
fromUserId: authorId,
title: '收到新的寄养评价',
text: content,
time: '刚刚',
unread: true,
createdAt: new Date()
});
ok(res, withoutMongoId(review), '评价已发布');
} catch (error) {
next(error);
}
});
这段代码把评价写入、寄养人统计和通知触达放在一个接口里处理。测试时也要同时验证这三件事,而不是只断言接口返回 200。
三、接口行为表
| 场景 | 请求用户 | 预期状态码 | 预期结果 |
|---|---|---|---|
| 记录不存在 | 宠物主人 | 404 | 不写入评价 |
| 记录未完成 | 宠物主人 | 400 | 不允许提前评价 |
| 评分为空或越界 | 宠物主人 | 400 | 返回“请填写完整评价” |
| 内容为空 | 宠物主人 | 400 | 返回“请填写完整评价” |
| 寄养人评价自己的服务 | 寄养人 | 403 | 拒绝自评 |
| 第三方评价 | 第三方用户 | 403 | 拒绝越权 |
| 宠物主人首次评价 | 宠物主人 | 200 | 写入评价和通知 |
| 宠物主人重复评价 | 宠物主人 | 409 | 不重复写入 |
这个表可以直接转成测试用例。评价接口的风险点主要在权限和幂等性。
四、记录必须存在
接口第一步查询寄养记录:
const record = await db.collection('fosterRecords').findOne({ id: req.params.id });
if (!record) {
return fail(res, 404, '寄养记录不存在');
}
如果记录不存在,不能继续创建评价。否则数据库里会出现没有寄养记录支撑的 fosterReviews,前端评价详情页也找不到对应的宠物和寄养人。
测试时可以专门请求一个不存在的 ID:
let response = await api('/foster-records/not_exists/reviews', 'POST', owner.token, {
rating: 5,
content: '不存在记录不能评价'
});
assert.strictEqual(response.status, 404);
五、必须完成后才能评价
状态校验:
if (record.status !== 'done') {
return fail(res, 400, '寄养结束后才能评价');
}
这里要注意,前端隐藏评价按钮只是体验优化,不能替代后端判断。用户可以绕过前端直接调用接口,所以后端必须根据 fosterRecords.status 自己判断。
对应测试:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', owner.token, {
rating: 5,
content: '还没有完成,不能评价。'
});
assert.strictEqual(response.status, 400);
这一步要放在 waitingIn 或 ongoing 阶段测试。
六、评分范围和内容校验
接口把评分转换成数字:
const rating = Number(req.body.rating || 0);
const content = text(req.body.content, 2000);
if (!authorId || !Number.isInteger(rating) || rating < 1 || rating > 5 || !content) {
return fail(res, 400, '请填写完整评价');
}
评分必须是整数,并且只能是 1 到 5。内容最多保留 2000 字,避免用户提交过长文本拖慢列表渲染。
测试建议至少覆盖:
for (const badRating of [0, 6, 4.5, 'bad']) {
response = await api(`/foster-records/${recordId}/reviews`, 'POST', owner.token, {
rating: badRating,
content: '评分不合法'
});
assert.strictEqual(response.status, 400);
}
内容为空也要测:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', owner.token, {
rating: 5,
content: ''
});
assert.strictEqual(response.status, 400);
七、只有宠物主人能评价
权限判断通过宠物归属完成:
const pet = await db.collection('pets').findOne({ id: record.petId, ownerId: authorId });
if (!pet) {
return fail(res, 403, '只有宠物主人可以评价寄养服务');
}
这比只判断 record.requestId 更稳。评价的主体是“宠物主人对寄养人服务的反馈”,所以接口回到 pets.ownerId 做最终判断。
测试场景:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', asker.token, {
rating: 5,
content: '寄养人不能给自己评价。'
});
assert.strictEqual(response.status, 403);
response = await api(`/foster-records/${recordId}/reviews`, 'POST', third.token, {
rating: 5,
content: '第三方也不能评价。'
});
assert.strictEqual(response.status, 403);
这两个测试能防止自评刷数据和第三方越权写评价。
八、防止重复评价
重复评价检查:
if (await db.collection('fosterReviews').findOne({ recordId: req.params.id, authorId })) {
return fail(res, 409, '该寄养记录已经评价');
}
同一个宠物主人对同一条寄养记录只能评价一次。否则寄养人主页统计会被重复增加,评价列表也会出现多条重复记录。
成功发布后立刻再发一次:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', owner.token, {
rating: 5,
content: '重复评价应该失败。'
});
assert.strictEqual(response.status, 409);
如果业务未来允许追加评价或追评,也应该新增 reviewReplies 或 reviewAppendices,不要把重复评价当成普通新增。
九、评价数据结构
评价对象:
const review = {
id: text(req.body.id, 80) || `rev_${Date.now()}`,
recordId: req.params.id,
authorId,
hostId: record.hostId,
rating,
ratingTags: textArray(req.body.ratingTags, 10, 30),
content,
photos: textArray(req.body.photos, 9, 300),
publishedAt: text(req.body.publishedAt, 30) || new Date().toISOString().slice(0, 10),
createdAt: new Date()
};
字段设计可以这样理解:
| 字段 | 含义 |
|---|---|
recordId |
评价对应哪条寄养记录 |
authorId |
谁发布评价,应该是宠物主人 |
hostId |
被评价的寄养人 |
rating |
1 到 5 星 |
ratingTags |
沟通、反馈、环境等短标签 |
content |
评价正文 |
photos |
评价图片,可选 |
publishedAt |
前端展示日期 |
createdAt |
后端排序和审计时间 |
ratingTags 和 photos 都用 textArray 做数量和长度限制,避免写入不可控数组。
十、写入评价集合
核心写入:
await db.collection('fosterReviews').insertOne(review);
写入后应该能通过 MongoDB 直接验证:
const storedReview = await db.collection('fosterReviews').findOne({
recordId,
authorId: owner.profileId
});
assert.strictEqual(storedReview.rating, 5);
assert.strictEqual(storedReview.hostId, asker.profileId);
assert.ok(storedReview.ratingTags.includes('沟通顺畅'));
这一步比只看接口响应更可靠,因为它证明数据真正落库。
十一、寄养人统计更新
发布评价后,接口增加寄养人统计:
await db.collection('profiles').updateOne(
{ id: record.hostId },
{ $inc: { 'hostStats.fosterCount': 1 } }
);
这里当前项目把 fosterCount 作为已完成服务量的一个累计。需要注意,它和评价条数有关联,但不应该完全混用。如果以后要展示“完成寄养次数”和“收到评价数”,建议拆成两个字段:
{
hostStats: {
fosterCount: 12,
reviewCount: 10,
ratingAvg: 4.8
}
}
当前测试至少要验证计数没有重复增加:
const beforeHost = await db.collection('profiles').findOne({ id: asker.profileId });
// 发布评价
const afterHost = await db.collection('profiles').findOne({ id: asker.profileId });
assert.strictEqual(afterHost.hostStats.fosterCount, beforeHost.hostStats.fosterCount + 1);
重复评价返回 409 后,再查一次,计数不应该继续增加。
十二、寄养人通知
评价发布后通知寄养人:
await db.collection('notices').insertOne({
id: `n_${Date.now()}`,
targetUserId: record.hostId,
kind: 'system',
fromUserId: authorId,
title: '收到新的寄养评价',
text: content,
time: '刚刚',
unread: true,
createdAt: new Date()
});
通知是评价闭环里非常容易漏掉的一步。如果没有通知,寄养人必须主动打开评价页才知道收到反馈,体验上不完整。
测试可以通过 /bootstrap 快照验证:
const askerSnapshot = await api('/bootstrap', 'GET', asker.token);
const reviewNotice = askerSnapshot.json.data.notices.find((item) =>
item.title === '收到新的寄养评价' && item.fromUserId === owner.profileId
);
assert.ok(reviewNotice);
assert.strictEqual(reviewNotice.kind, 'system');
assert.strictEqual(reviewNotice.unread, true);
也可以直接查 MongoDB:
const reviewNotice = await db.collection('notices').findOne({
targetUserId: asker.profileId,
title: '收到新的寄养评价',
fromUserId: owner.profileId
});
assert.ok(reviewNotice);
十三、端到端测试准备
测试脚本骨架:
const assert = require('assert');
const { spawn } = require('child_process');
const { MongoClient } = require('mongodb');
const port = 3115;
const baseUrl = `http://127.0.0.1:${port}/api`;
const password = 'TestPass123';
const suffix = String(Date.now()).slice(-8);
async function api(path, method = 'GET', token = '', body) {
const response = await fetch(baseUrl + path, {
method,
headers: {
'content-type': 'application/json',
...(token ? { authorization: `Bearer ${token}` } : {})
},
body: body === undefined ? undefined : JSON.stringify(body)
});
return { status: response.status, json: await response.json() };
}
启动服务:
const server = spawn(process.execPath, ['./bin/www'], {
cwd: __dirname + '/..',
env: {
...process.env,
PORT: String(port),
HOST: '127.0.0.1',
API_RATE_LIMIT: '1000',
LOGIN_RATE_LIMIT: '100',
REGISTER_RATE_LIMIT: '100'
},
stdio: 'ignore',
windowsHide: true
});
注册用户:
async function register(prefix) {
const username = prefix + suffix;
const response = await api('/auth/register', 'POST', '', { username, password });
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
return {
username,
profileId: response.json.data.profileId,
token: response.json.data.token
};
}
十四、创建寄养记录
准备 owner、asker、third 三个用户:
const owner = await register('review_owner_');
const asker = await register('review_asker_');
const third = await register('review_third_');
const petId = `review_pet_${suffix}`;
const requestId = `review_request_${suffix}`;
const applicationId = `review_application_${suffix}`;
创建宠物:
let response = await api('/pets', 'POST', owner.token, {
id: petId,
name: '评价测试宠物',
species: '犬',
gender: 'female',
ageDesc: '3岁'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
创建寄养需求:
response = await api('/foster-requests', 'POST', owner.token, {
id: requestId,
title: '寄养评价回归测试',
petId,
fosterType: '家庭寄养',
startDate: '2026-07-10',
endDate: '2026-07-12',
location: '上海市 徐汇区',
latitude: 31.1884,
longitude: 121.4368,
budget: '100元/天',
feedRequirement: '每日两次',
walkRequirement: '每日两次',
notesRequirement: '需要按时反馈状态'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
寄养人申请:
response = await api(`/foster-requests/${requestId}/applications`, 'POST', asker.token, {
id: applicationId,
message: '我可以按要求照顾。'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
宠物主人接受申请:
response = await api(`/foster-applications/${applicationId}/status`, 'PUT', owner.token, {
status: 'accepted'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
此时记录 ID 是:
const recordId = `rec_${requestId}`;
十五、未完成不能评价
刚接受申请后,记录状态是 waitingIn。这时发布评价应该失败:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', owner.token, {
rating: 5,
ratingTags: ['沟通顺畅'],
content: '记录还没完成,不能评价。'
});
assert.strictEqual(response.status, 400);
然后推进状态:
response = await api(`/foster-records/${recordId}/status`, 'PUT', asker.token, {
status: 'ongoing'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
response = await api(`/foster-records/${recordId}/status`, 'PUT', owner.token, {
status: 'done'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
状态完成后再进入评价测试。
十六、越权评价测试
寄养人自评应该失败:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', asker.token, {
rating: 5,
ratingTags: ['照护细心'],
content: '寄养人不能给自己发布服务评价。'
});
assert.strictEqual(response.status, 403);
第三方评价也应该失败:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', third.token, {
rating: 5,
ratingTags: ['沟通顺畅'],
content: '第三方没有参与寄养,不能评价。'
});
assert.strictEqual(response.status, 403);
这两个断言可以避免评价系统被刷数据。
十七、评分和内容校验测试
评分越界:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', owner.token, {
rating: 6,
content: '评分不能超过五分。'
});
assert.strictEqual(response.status, 400);
非整数评分:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', owner.token, {
rating: 4.5,
content: '评分必须是整数。'
});
assert.strictEqual(response.status, 400);
内容为空:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', owner.token, {
rating: 5,
content: ''
});
assert.strictEqual(response.status, 400);
这些测试不复杂,但能挡住大量脏数据。
十八、成功发布评价
成功路径:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', owner.token, {
id: `review_${suffix}`,
rating: 5,
ratingTags: ['沟通顺畅', '信息透明'],
content: '寄养期间反馈及时,接送安排清楚,宠物状态记录完整。'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
assert.strictEqual(response.json.data.recordId, recordId);
assert.strictEqual(response.json.data.hostId, asker.profileId);
assert.strictEqual(response.json.data.rating, 5);
重复评价:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', owner.token, {
rating: 5,
content: '第二次评价应该被拒绝。'
});
assert.strictEqual(response.status, 409);
十九、MongoDB 落库断言
直连数据库:
const client = new MongoClient(process.env.MONGODB_URL || 'mongodb://127.0.0.1:27017');
await client.connect();
const db = client.db(process.env.MONGODB_DB || 'chongwu');
检查评价:
const storedReview = await db.collection('fosterReviews').findOne({
recordId,
authorId: owner.profileId
});
assert.ok(storedReview);
assert.strictEqual(storedReview.rating, 5);
assert.strictEqual(storedReview.hostId, asker.profileId);
assert.ok(storedReview.ratingTags.includes('沟通顺畅'));
检查重复评价没有新增:
const reviewCount = await db.collection('fosterReviews').countDocuments({
recordId,
authorId: owner.profileId
});
assert.strictEqual(reviewCount, 1);
检查通知:
const notice = await db.collection('notices').findOne({
targetUserId: asker.profileId,
title: '收到新的寄养评价',
fromUserId: owner.profileId
});
assert.ok(notice);
assert.strictEqual(notice.kind, 'system');
assert.strictEqual(notice.unread, true);
关闭连接:
await client.close();
二十、通过 bootstrap 验证前端快照
后端直接查库是一层,前端使用的数据快照也要验证:
const ownerSnapshot = await api('/bootstrap', 'GET', owner.token);
const askerSnapshot = await api('/bootstrap', 'GET', asker.token);
const review = ownerSnapshot.json.data.fosterReviews.find((item) =>
item.recordId === recordId && item.authorId === owner.profileId
);
assert.ok(review);
const reviewNotice = askerSnapshot.json.data.notices.find((item) =>
item.title === '收到新的寄养评价' && item.fromUserId === owner.profileId
);
assert.ok(reviewNotice);
如果 /bootstrap 没有返回 fosterReviews,前端评价列表就无法在刷新后同步后端数据。这种问题不能只靠页面手动点出来,测试里应该有快照断言。
二十一、HarmonyOS 评价列表
FosterReviewListPage.ets 会同时展示“待评价”和“已发布评价”:
@StorageLink('fosterVersion') @Watch('refresh') fosterVersion: number = 0;
@State reviews: FosterReview[] = [];
@State pendingRecords: FosterRecord[] = [];
aboutToAppear(): void {
this.refresh();
}
refresh(): void {
this.reviews = MockStore.myFosterReviews();
this.pendingRecords = MockStore.fosterRecords.filter((record: FosterRecord) => {
const pet = MockStore.getPet(record.petId);
return record.status === FosterStatus.Done &&
pet !== null &&
pet.ownerId === MockStore.meId &&
MockStore.reviewForRecord(record.id) === null;
});
}
这个页面的关键点是过滤条件:
| 条件 | 目的 |
|---|---|
record.status === FosterStatus.Done |
只展示完成后的记录 |
pet.ownerId === MockStore.meId |
只有宠物主人看到待评价 |
reviewForRecord(record.id) === null |
已评价记录不再显示待评价 |
评价发布后要提升 fosterVersion 或刷新后端快照,这样“待评价”才能变成“已发布评价”。
二十二、HarmonyOS 评价表单
FosterReviewForm.ets 负责星级、标签和内容输入:
@Prop rating: number = 5;
@Prop selectedTags: string[] = [];
@Prop content: string = '';
onRatingChange: (rating: number) => void = () => {};
onTagToggle: (tag: string) => void = () => {};
onContentChange: (content: string) => void = () => {};
private tags: string[] = ['照护细心', '沟通顺畅', '反馈及时', '环境整洁'];
星级渲染:
ForEach([1, 2, 3, 4, 5], (value: number) => {
Text(value <= this.rating ? '★' : '☆')
.fontSize(34)
.fontColor(value <= this.rating ? $r('app.color.brand_primary') : $r('app.color.text_muted'))
.margin({ right: Spacing.sm })
.onClick(() => { this.onRatingChange(value); })
}, (value: number) => `${value}`)
内容输入:
TextArea({ placeholder: '分享寄养期间的照护体验', text: this.content })
.height(150).fontSize(FontSize.md)
.backgroundColor($r('app.color.surface_card')).borderRadius(Radius.md)
.onChange((value: string) => { this.onContentChange(value); })
前端可以先做最小长度校验,后端再做最终校验。
二十三、评价发布页
FosterReviewCreatePage.ets 的提交逻辑:
private submit(): void {
if (this.content.trim().length < 5) {
this.errorText = '请至少填写 5 个字的评价';
return;
}
const review: FosterReview | null =
MockStore.addFosterReview(this.recordId, this.rating, this.tags, this.content);
if (review === null) {
this.errorText = '当前记录无法评价或已经评价';
return;
}
const p: RouteIdParam = { id: review.id };
RouterService.clearAndPush(RouteName.FosterReviewDetail, p);
}
如果接后端,可以把本地方法替换为:
static async createFosterReview(
recordId: string,
rating: number,
ratingTags: string[],
content: string
): Promise<FosterReview> {
const response: ApiEnvelope = await ApiClient.post(
'/foster-records/' + encodeURIComponent(recordId) + '/reviews',
{ rating, ratingTags, content }
);
BackendService.requireOk(response);
return response.data as FosterReview;
}
发布成功后建议执行:
await MockStore.refreshFromBackend();
AppStorage.setOrCreate('fosterVersion', Date.now());
AppStorage.setOrCreate('noticeVersion', Date.now());
这样评价列表和通知中心都会刷新。
二十四、记录详情页的评价入口
FosterRecordDetailPage 中,评价按钮应该同时满足:
- 记录状态是
done; - 当前用户是宠物主人;
- 当前记录还没有评价。
伪代码:
private canReview(): boolean {
if (this.rec === null || this.rec.status !== FosterStatus.Done) {
return false;
}
const pet = MockStore.getPet(this.rec.petId);
if (pet === null || pet.ownerId !== MockStore.meId) {
return false;
}
return MockStore.reviewForRecord(this.rec.id) === null;
}
按钮点击后进入评价页:
const p: RouteIdParam = { id: this.rec.id };
RouterService.push(RouteName.FosterReviewCreate, p);
前端入口和后端权限要双保险。前端负责不让用户误点,后端负责防止绕过。
二十五、常见问题排查
| 现象 | 可能原因 | 排查方式 |
|---|---|---|
| 未完成记录能评价 | 后端漏掉 record.status !== 'done' |
用 waitingIn 记录发评价 |
| 寄养人能给自己评价 | 只判断了登录态,没有查宠物主人 | 用 host token 发请求 |
| 第三方能评价 | 没查 pets.ownerId |
用 third token 发请求 |
| 同一记录多条评价 | 缺少 recordId + authorId 重复校验 |
连续 POST 两次 |
| 评分出现 0 或 6 | 没限制评分范围 | 测 0、6、4.5 |
| 评价后寄养人没通知 | 未写入 notices |
查 targetUserId 是 hostId 的通知 |
| 待评价列表不消失 | 前端未刷新 fosterVersion |
发布后检查 pendingRecords |
| 已评价列表为空 | /bootstrap 没返回 fosterReviews |
查快照数据 |
| 统计重复增加 | 重复评价仍执行 $inc |
409 后再次查 hostStats |
二十六、上线前验收清单
- 不存在记录评价返回 404;
waitingIn或ongoing记录评价返回 400;- 评分 0、6、4.5、字符串都返回 400;
- 内容为空返回 400;
- 寄养人自评返回 403;
- 第三方评价返回 403;
- 宠物主人完成后评价返回 200;
fosterReviews写入recordId、authorId、hostId、rating;ratingTags和photos有长度和数量限制;- 重复评价返回 409;
- 重复评价不会新增第二条
fosterReviews; - 重复评价不会重复增加
hostStats.fosterCount; - 评价成功后寄养人收到系统通知;
/bootstrap能返回评价和通知;- HarmonyOS 待评价列表在发布后移除该记录;
- 已发布评价列表能显示星级、标签、内容和日期;
- 集成测试结束后清理测试账号和关联数据。
总结
寄养评价是宠物邻里项目的信任闭环。后端要守住四件事:记录必须存在、状态必须完成、评价人必须是宠物主人、同一记录不能重复评价。写入评价后,还要同步寄养人统计和系统通知,避免评价只是“数据库里多了一条记录”。
HarmonyOS 前端则负责把完成记录转成待评价卡片,把评价发布转成已发布列表和通知刷新。最终验证不能停在页面能点通,而要通过 Express 接口、MongoDB 落库、/bootstrap 快照和前端状态刷新一起确认。只有这些断言都通过,寄养评价链路才算真正闭环。
更多推荐


所有评论(0)