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

260 lines
8.7 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.
"use strict";
import mongoose from "mongoose";
import { pinyin } from "pinyin-pro";
/**
* 培训记录子文档
*
* 国标 GB/T 47801—2026 4.2.1.5 把「培训记录」列为陪同人员信息公示项,
* 故按一条一次培训存成数组,便于逐条核对;不用 _id,这些条目只跟着员工走
*/
const TrainingSchema = mongoose.Schema(
{
title: { type: String, default: "", comment: "培训名称/课程" },
org: { type: String, default: "", comment: "培训机构" },
date: { type: Date, comment: "结业日期" },
},
{ _id: false }
);
/**
* Employee Schema
* 员工/人员信息,支持陪诊员、护士、医生、管理员等角色
*/
const EmployeeSchema = mongoose.Schema(
{
// 工号(业务雇佣标识)
employeeNo: { type: String, default: "", unique: true, sparse: true, comment: "工号" },
// 登录账号(关联用户体系)
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "user",
index: true,
comment: "关联用户ID",
},
// 所属机构
orgId: {
type: mongoose.Schema.Types.ObjectId,
ref: "organization",
index: true,
comment: "所属机构ID",
},
// 角色/岗位
role: {
type: String,
enum: ["attendant", "nurse", "doctor", "admin", "operator", "other"],
default: "attendant",
comment: "角色:陪诊员、护士、医生、管理员、运营、其他",
},
// 个人信息
profile: {
name: { type: String, required: true, comment: "姓名" },
pinyin: { type: String, default: "", index: true, comment: "姓名的拼音,用于搜索" },
pinyinFL: { type: String, default: "", index: true, comment: "姓名拼音的首字母,用于搜索" },
sex: { type: String, enum: ["male", "female", "other", ""], default: "", comment: "性别" },
birthday: { type: Date, comment: "出生日期" },
idNumber: { type: String, default: "", comment: "身份证号" },
phone: { type: String, default: "", comment: "手机号" },
email: { type: String, default: "", comment: "邮箱" },
avatar: { type: String, default: "", comment: "头像URL" },
},
// 工作信息
work: {
department: { type: String, default: "", comment: "所属科室/部门" },
title: { type: String, default: "", comment: "职称/头衔" },
entryDate: { type: Date, comment: "入职日期" },
workYears: { type: Number, default: 0, comment: "工作年限" },
qualification: { type: String, default: "", comment: "资质证书编号" },
// 健康证明:国标 4.2.1.5 公示的是「健康证明有效期」。
// 公示接口只输出"是否在有效期内"+ 到期日;number 属资质原文,
// 与 qualification 一样只作内部留存,绝不随公示接口下发
healthCertificate: {
number: { type: String, default: "", comment: "健康证明编号(不对外公示)" },
org: { type: String, default: "", comment: "发证机构" },
issueDate: { type: Date, comment: "签发日期" },
expireDate: { type: Date, comment: "有效期至" },
},
// 培训记录:同为 4.2.1.5 公示项
trainings: { type: [TrainingSchema], default: [], comment: "培训记录列表" },
},
// 陪诊员特有信息
attendantInfo: {
serviceAreas: { type: [String], default: [], comment: "服务区域/城市列表" },
specialties: { type: [String], default: [], comment: "擅长科室/领域" },
languages: { type: [String], default: ["普通话"], comment: "掌握语言" },
// 注意:本字段全项目没有任何写入点(恒为默认 5.0),不可作为评级来源。
// 对外评级一律由 employee_review 真实评价聚合得出(employee_review.js),
// 达阈值才展示,详见 .trae/documents/陪诊师公示与指定陪诊师.md
rating: { type: Number, default: 5.0, min: 1, max: 5, comment: "评分(无写入点,勿用于公示)" },
orderCount: { type: Number, default: 0, comment: "接单数量" },
isOnline: { type: Boolean, default: false, comment: "是否在线" },
},
// 紧急联系人
emergencyContact: {
name: { type: String, default: "", comment: "紧急联系人姓名" },
phone: { type: String, default: "", comment: "紧急联系人电话" },
relation: { type: String, default: "", comment: "关系" },
},
// 状态
status: {
type: String,
enum: ["active", "inactive", "resigned", "suspended"],
default: "active",
comment: "状态:在职、停用、离职、暂停",
},
// 简介/备注
introduction: { type: String, default: "", comment: "个人简介" },
remark: { type: String, default: "", comment: "备注" },
// 元数据
meta: {
createtime: { type: Date, default: Date.now, comment: "创建时间" },
updatetime: { type: Date, default: Date.now, comment: "更新时间" },
},
},
{
minimize: false,
strict: false,
collection: "employee",
timestamps: false,
}
);
// ==================== 拼音生成 ====================
/**
* 姓名 → 全拼(小写、无分隔,如 "张三" → "zhangsan")
*/
const genPinyin = (name) =>
pinyin(String(name), { toneType: "none", nonZh: "consecutive" }).toLowerCase().replace(/[^a-z0-9]/g, "");
/**
* 姓名 → 拼音首字母(如 "张三" → "zs")
*/
const genPinyinFL = (name) =>
pinyin(String(name), { pattern: "first", toneType: "none", nonZh: "consecutive" }).toLowerCase().replace(/[^a-z0-9]/g, "");
// 保存时自动生成/同步拼音字段
EmployeeSchema.pre("save", function (next) {
const name = this.profile?.name;
if (!name) return next();
if (this.isModified("profile.name") || !this.profile.pinyin || !this.profile.pinyinFL) {
this.profile.pinyin = genPinyin(name);
this.profile.pinyinFL = genPinyinFL(name);
}
next();
});
// ==================== 静态方法 ====================
/**
* 根据机构ID查询员工列表
*/
EmployeeSchema.statics.findByOrg = async function (orgId, options = {}) {
const { page = 1, pageSize = 20, role, status } = options;
const filter = { orgId };
if (role) filter.role = role;
if (status) filter.status = status;
const skip = (page - 1) * pageSize;
return await this.find(filter)
.skip(skip)
.limit(pageSize)
.exec();
};
/**
* 根据角色查询员工
*/
EmployeeSchema.statics.findByRole = async function (role, options = {}) {
const { page = 1, pageSize = 20, status = "active" } = options;
const filter = { role };
if (status) filter.status = status;
const skip = (page - 1) * pageSize;
return await this.find(filter)
.skip(skip)
.limit(pageSize)
.exec();
};
/**
* 根据用户ID查找员工
*/
EmployeeSchema.statics.findByUserId = async function (userId) {
return await this.findOne({ userId }).exec();
};
/**
* 根据ID查找员工
*/
EmployeeSchema.statics.findByEmployeeId = async function (id) {
return await this.findById(id).exec();
};
/**
* 创建员工
*/
EmployeeSchema.statics.createEmployee = async function (data) {
data.meta = { createtime: Date.now(), updatetime: Date.now() };
const doc = new this(data);
return await doc.save();
};
/**
* 更新员工
*/
EmployeeSchema.statics.updateEmployee = async function (id, update) {
const newName = update.name || update["profile.name"];
if (newName) {
update["profile.name"] = newName;
delete update.name;
update["profile.pinyin"] = genPinyin(newName);
update["profile.pinyinFL"] = genPinyinFL(newName);
}
update["meta.updatetime"] = Date.now();
return await this.findByIdAndUpdate(id, { $set: update }, { new: true }).exec();
};
/**
* 查询在线陪诊员列表
*
* 排序口径与 handler/employee.js 的公示列表保持一致:服务年限优先、累计服务次数次之。
* 曾按 attendantInfo.rating 排,但该字段没有任何写入点(恒为 5.0),等于没排序
*/
EmployeeSchema.statics.findOnlineAttendants = async function (options = {}) {
const { page = 1, pageSize = 20 } = options;
const skip = (page - 1) * pageSize;
return await this.find({
role: "attendant",
status: "active",
"attendantInfo.isOnline": true,
})
.sort({ "work.workYears": -1, "attendantInfo.orderCount": -1 })
.skip(skip)
.limit(pageSize)
.exec();
};
// ==================== 索引定义 ====================
EmployeeSchema.index({ "profile.name": 1 });
EmployeeSchema.index({ employeeNo: 1 });
EmployeeSchema.index({ userId: 1 });
EmployeeSchema.index({ orgId: 1, role: 1, status: 1 });
EmployeeSchema.index({ role: 1, status: 1 });
EmployeeSchema.index({ "profile.phone": 1 });
EmployeeSchema.index({ status: 1 });
export { EmployeeSchema };