Files
api_health/handler/employee.js
T
2026-10-06 21:44:42 +08:00

170 lines
6.5 KiB
JavaScript
Raw 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.
import mongoose from "mongoose";
import { DBModel } from "../models/index.js";
import ResponseUtil from "../utils/responseUtil.js";
/**
* 陪诊师公示:面向 C 端患者的公开信息展示
*
* 依据 GB/T 47801—2026 4.2.1.5「陪同人员信息公示机制」组织字段:
* 基本信息 —— 健康证明有效期、培训记录、服务年限、累计服务次数
* 专业技能 —— 擅长服务场景、特殊技能(语言)
*
* 两条红线:
* 1. 白名单式输出,绝不返回手机号 / 身份证号 / 证书编号原文;
* 「资质已核验」与健康证明都只对外给布尔值 / 有效期,不给编号
* 2. 评分只来自 employee_review 的真实评价聚合,且条数达
* MIN_PUBLIC_REVIEW_COUNT 才输出。attendantInfo.rating 恒为默认 5.0,
* 任何分支都不得读它——那是无依据背书
*/
/**
* 评价对外公示的最低条数
*
* 少于这个条数时平均分不具备参考性:一条 5 分就等于满分,展示反而误导用户。
* 故未达阈值时只回条数、分数给 null,由前端显示"暂不显示评分"
*/
const MIN_PUBLIC_REVIEW_COUNT = 5;
class HandlerEmployee {
// 姓名脱敏:只留姓 + 「陪诊师」,不给全名,降低私下联系(跳单)的可能
static maskName(name) {
const trimmed = String(name || "").trim();
if (!trimmed) return "陪诊师";
return trimmed.slice(0, 1) + "陪诊师";
}
// 日期 → YYYY-MM-DD;无值或非法值返回空串,前端按空串隐藏该行
static formatDate(value) {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${date.getFullYear()}-${month}-${day}`;
}
// 健康证明是否在有效期内。
// 没登到期日一律判为"不有效"——不能默认有效,那等于无依据背书(同 rating 的处理)。
// 到期当天仍算有效,故与当天 24:00 比较
static isHealthCertificateValid(expireDate) {
if (!expireDate) return false;
const endOfDay = new Date(expireDate);
if (Number.isNaN(endOfDay.getTime())) return false;
endOfDay.setHours(23, 59, 59, 999);
return endOfDay.getTime() >= Date.now();
}
// 培训记录:只公示培训名称 / 机构 / 结业日期,内部备注一类不外泄
static toPublicTrainings(work) {
const list = Array.isArray(work.trainings) ? work.trainings : [];
return list
.map((item) => ({
title: item.title || "",
org: item.org || "",
date: HandlerEmployee.formatDate(item.date),
}))
// 没有名称的条目是脏数据,公示出去只是一行空白
.filter((item) => item.title);
}
// 白名单式脱敏:逐字段挑选,避免 employee 文档整体外泄
static toPublicAttendant(employee) {
const profile = employee.profile || {};
const work = employee.work || {};
const info = employee.attendantInfo || {};
return {
id: employee._id,
name: HandlerEmployee.maskName(profile.name),
avatar: profile.avatar || "",
sex: profile.sex || "",
workYears: work.workYears || 0,
qualificationVerified: !!work.qualification,
// 健康证明:4.2.1.5 要求公示的是「有效期」,
// 只给是否在有效期内 + 到期日,编号与发证机构不外泄(同 qualification)
healthCertificate: {
valid: HandlerEmployee.isHealthCertificateValid(work.healthCertificate?.expireDate),
expireDate: HandlerEmployee.formatDate(work.healthCertificate?.expireDate),
},
specialties: info.specialties || [],
serviceAreas: info.serviceAreas || [],
languages: info.languages || [],
orderCount: info.orderCount || 0,
introduction: employee.introduction || "",
};
}
// 公开的陪诊师列表。
// 与 /service、/agreement 等公示类资源一致,不加 requireAuth——
// 公示信息本身不敏感,且避免登录态异常导致页面空白
async getAttendants(ctx) {
try {
const page = parseInt(ctx.request.query.page) || 1;
const pageSize = parseInt(ctx.request.query.pageSize) || 50;
// 只取在职陪诊员。
// 排序不按 attendantInfo.rating(无可信来源),改按服务年限 / 累计接单数
const list = await DBModel.Employee.find({
role: "attendant",
status: "active",
})
.sort({ "work.workYears": -1, "attendantInfo.orderCount": -1 })
.skip((page - 1) * pageSize)
.limit(pageSize)
.exec();
return ResponseUtil.success(
ctx,
{ attendants: list.map(HandlerEmployee.toPublicAttendant) },
"查询成功"
);
} catch (err) {
return ResponseUtil.internalError(ctx, err.message);
}
}
// 单个陪诊师的公开详情。
// 与列表共用同一套白名单——详情页不因为"多进一层"就多给字段,
// 手机号 / 身份证号 / 证书编号在这个分支里同样不出现。
// 筛选条件也照抄列表:只认在职陪诊员,否则列表里看不到的人却能按 ID 查到
async getAttendantById(ctx) {
try {
const id = ctx.params.id;
// 非法 ObjectId 会让 mongoose 抛 CastError,先挡掉,也避免把驱动栈信息带出去
if (!mongoose.Types.ObjectId.isValid(id)) {
return ResponseUtil.badRequest(ctx, "陪诊师ID不合法");
}
const employee = await DBModel.Employee.findOne({
_id: id,
role: "attendant",
status: "active",
}).exec();
if (!employee) {
return ResponseUtil.notFound(ctx, "陪诊师不存在或已下架");
}
const attendant = HandlerEmployee.toPublicAttendant(employee);
// 详情比列表多两块:
// 1) 培训记录——它是数组、体积不确定,列表接口不带
attendant.trainings = HandlerEmployee.toPublicTrainings(employee.work || {});
// 2) 评价聚合——多一次查询,只有详情这一个入口需要
const review = await DBModel.EmployeeReview.statsByEmployee(id);
attendant.review = {
count: review.count,
// 未达阈值不给分数:条数太少时平均分不具备参考性
score: review.count >= MIN_PUBLIC_REVIEW_COUNT ? review.score : null,
};
return ResponseUtil.success(ctx, { attendant }, "查询成功");
} catch (err) {
return ResponseUtil.internalError(ctx, err.message);
}
}
}
export { HandlerEmployee };