167 lines
4.8 KiB
JavaScript
167 lines
4.8 KiB
JavaScript
"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 };
|