Files
2026-10-06 21:44:42 +08:00

167 lines
4.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 };