"use strict"; import mongoose from "mongoose"; /** * 员工评价 Schema * * 用途:为「员工信息公示」提供唯一可信的评级来源。当前只有陪诊师(role: attendant) * 有对外的评价公示场景,故本表目前只被 handler/employee.js 的陪诊师详情页消费; * 但表本身按「员工」建,不绑死陪诊师——后期护士 / 医生等角色接评价时直接复用, * 不必再建一张平行的表(这也是它叫 employee_review 而不是 attendant_review 的原因)。 * * 设计前提(见 .trae/documents/陪诊师公示与指定陪诊师.md): * - 一次服务最多产生一条评价,故 recordId 建唯一索引,从数据层断掉刷分 * - 评级不由 employee.attendantInfo.rating 提供——那个字段没有任何写入点 * - 只有 status 为 published 的评价计入评分,运营隐藏掉的不参与 * - 是否对外展示分数由阈值决定(阈值在 handler/employee.js),本表只负责算平均分 */ const EmployeeReviewSchema = mongoose.Schema( { /** * 关联的服务记录ID(一次服务一条评价) * * 当前评价只发生在陪诊服务上,故指向 escort_record。 * 若未来非陪诊类评价(如问诊)接入,此处需补一个来源标识字段来区分记录所属集合—— * ObjectId 全局唯一,唯一索引可以照旧沿用,但读取时要先知道去哪个集合取原文。 * 该字段现在不加:不想再造一个没有写入点、等不到用处的空字段。 * * @type {ObjectId} * @ref escort_record */ recordId: { type: mongoose.Schema.Types.ObjectId, ref: "escort_record", required: true, comment: "关联服务记录ID", }, /** * 被评价的员工ID * * 名字用 employeeId 而非 attendantId:面向 employee 集合,不预设角色。 * 角色本身不冗余在这里,需要时由 employee 文档的 role 提供,避免两处口径打架。 * * @type {ObjectId} * @ref employee */ employeeId: { type: mongoose.Schema.Types.ObjectId, ref: "employee", index: true, required: true, comment: "被评价员工ID", }, /** * 评价人用户ID * @type {ObjectId} * @ref user */ userId: { type: mongoose.Schema.Types.ObjectId, ref: "user", index: true, comment: "评价人用户ID", }, /** * 评分(1-5 星) */ score: { type: Number, min: 1, max: 5, required: true, comment: "评分(1-5)", }, /** * 评价正文 */ content: { type: String, default: "", comment: "评价正文" }, /** * 状态 * - published: 已发布 - 计入评分,可对外展示 * - hidden: 已隐藏 - 运营判定为不当内容,不计入评分 */ status: { type: String, enum: ["published", "hidden"], default: "published", comment: "状态:已发布、已隐藏", }, /** * 元数据 */ meta: { createtime: { type: Date, default: Date.now, comment: "创建时间" }, updatetime: { type: Date, default: Date.now, comment: "更新时间" }, }, }, { minimize: false, strict: false, collection: "employee_review", timestamps: false, } ); /** * 统计某位员工的公开评价数与平均分 * * 只聚合 published,隐藏评价不参与;平均分保留一位小数, * 公示页只展示一位,多出来的小数位没有意义。 * 是否对外展示由调用方按阈值决定,本方法不做门槛判断 * * @param {ObjectId|string} employeeId - 员工ID * @returns {Promise<{count: number, score: number|null}>} 评价条数与平均分(无评价时 score 为 null) */ EmployeeReviewSchema.statics.statsByEmployee = async function (employeeId) { // 非法 ID 直接返回空统计,不抛 CastError if (!employeeId || !mongoose.Types.ObjectId.isValid(employeeId)) { return { count: 0, score: null }; } const rows = await this.aggregate([ { $match: { employeeId: new mongoose.Types.ObjectId(employeeId), status: "published", }, }, { $group: { _id: null, count: { $sum: 1 }, score: { $avg: "$score" }, }, }, ]); if (!rows.length) { return { count: 0, score: null }; } return { count: rows[0].count, score: Math.round(rows[0].score * 10) / 10, }; }; // ==================== 索引定义 ==================== /** * 服务记录唯一索引 - 一次服务只允许一条评价,从数据层堵住刷分 */ EmployeeReviewSchema.index({ recordId: 1 }, { unique: true }); /** * 员工 + 状态索引 - 用于按员工聚合公开评价 */ EmployeeReviewSchema.index({ employeeId: 1, status: 1 }); export { EmployeeReviewSchema };