HarmonyOS宠物邻里实战第17篇:寄养评价发布、重复提交防护与通知回归测试

摘要

寄养业务做到 done 并不算结束。对宠物主人来说,完整闭环还要包含“给寄养人评价”;对寄养人来说,评价会影响后续信任、接单意愿和个人主页数据。评价接口如果只做一个简单 insertOne,很容易出现未完成就评价、寄养人给自己刷评价、同一条记录重复评价、评分越界、标签过长、通知不触达等问题。

本文基于宠物邻里 HarmonyOS + Express + MongoDB 项目,复盘寄养评价发布链路:

  • 为什么评价必须依赖 fosterRecords.status === 'done'
  • 后端如何校验 1 到 5 星评分;
  • 为什么只有宠物主人可以评价;
  • 如何防止同一寄养记录重复评价;
  • fosterReviewsprofiles.hostStatsnotices 三个集合如何同步;
  • 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 checknpm run test:integration
评价接口 POST /api/foster-records/:id/reviews

版本边界也要写清楚。本文的 @StorageLink@WatchTextArea.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);

这一步要放在 waitingInongoing 阶段测试。

六、评分范围和内容校验

接口把评分转换成数字:

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);

如果业务未来允许追加评价或追评,也应该新增 reviewRepliesreviewAppendices,不要把重复评价当成普通新增。

九、评价数据结构

评价对象:

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 后端排序和审计时间

ratingTagsphotos 都用 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 中,评价按钮应该同时满足:

  1. 记录状态是 done
  2. 当前用户是宠物主人;
  3. 当前记录还没有评价。

伪代码:

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;
  • waitingInongoing 记录评价返回 400;
  • 评分 0、6、4.5、字符串都返回 400;
  • 内容为空返回 400;
  • 寄养人自评返回 403;
  • 第三方评价返回 403;
  • 宠物主人完成后评价返回 200;
  • fosterReviews 写入 recordIdauthorIdhostIdrating
  • ratingTagsphotos 有长度和数量限制;
  • 重复评价返回 409;
  • 重复评价不会新增第二条 fosterReviews
  • 重复评价不会重复增加 hostStats.fosterCount
  • 评价成功后寄养人收到系统通知;
  • /bootstrap 能返回评价和通知;
  • HarmonyOS 待评价列表在发布后移除该记录;
  • 已发布评价列表能显示星级、标签、内容和日期;
  • 集成测试结束后清理测试账号和关联数据。

总结

寄养评价是宠物邻里项目的信任闭环。后端要守住四件事:记录必须存在、状态必须完成、评价人必须是宠物主人、同一记录不能重复评价。写入评价后,还要同步寄养人统计和系统通知,避免评价只是“数据库里多了一条记录”。

HarmonyOS 前端则负责把完成记录转成待评价卡片,把评价发布转成已发布列表和通知刷新。最终验证不能停在页面能点通,而要通过 Express 接口、MongoDB 落库、/bootstrap 快照和前端状态刷新一起确认。只有这些断言都通过,寄养评价链路才算真正闭环。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐