Files
api_health/models/schema/health_profile.js
T
2026-09-09 17:36:45 +08:00

196 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.
"use strict";
import mongoose from "mongoose";
import { pinyin } from "pinyin-pro";
/**
* HealthProfile Schema
* 健康档案 - 记录患者的基本健康信息,内容保持简单
*/
const HealthProfileSchema = mongoose.Schema(
{
// 关联用户ID(可为空)
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "user",
index: { unique: false, sparse: true },
comment: "所属用户ID",
},
// 患者信息
profile: {
name: { type: String, default: "", comment: "患者姓名" },
pinyin: { type: String, default: "", index: true, comment: "患者姓名全拼(小写无分隔,用于搜索)" },
pinyinFL: { type: String, default: "", index: true, comment: "患者姓名拼音首字母(用于搜索)" },
mobile: { type: String, default: "", index: true, comment: "患者电话" },
sex: { type: String, enum: ["male", "female", ""], default: "", comment: "性别" },
birth: { type: String, default: "", comment: "出生年月(YYYY-MM-DD" },
idnumber: { type: String, default: "", comment: "证件号(身份证/护照等)" },
},
// 所在地(国外用户填国家;国内用户填省市区 + 详细地址)
location: {
country: { type: String, default: "中国", comment: "国家" },
province: { type: String, default: "", comment: "省" },
city: { type: String, default: "", comment: "市" },
district: { type: String, default: "", comment: "区/县" },
address: { type: String, default: "", comment: "详细地址" },
},
// 健康信息
health: {
height: { type: Number, default: 0, comment: "身高(cm" },
weight: { type: Number, default: 0, comment: "体重(kg" },
bloodType: { 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: "health_profile",
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, "");
// 保存时自动生成/同步姓名拼音字段
HealthProfileSchema.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();
});
// ==================== 静态方法 ====================
/**
* 更新包含姓名时同步拼音字段(update 为带 dotted path 的扁平对象)
*/
const syncPinyinOnUpdate = (update) => {
const name = update["profile.name"] ?? update.profile?.name;
if (name) {
update["profile.pinyin"] = genPinyin(name);
update["profile.pinyinFL"] = genPinyinFL(name);
}
};
/**
* 根据用户ID查找健康档案
*/
HealthProfileSchema.statics.findByUserId = async function (userId) {
return await this.findOne({ userId }).exec();
};
/**
* 查询健康档案列表(支持分页与筛选)
*
* @param {Object} options - 查询选项
* @param {number} [options.page=1] - 页码
* @param {number} [options.pageSize=20] - 每页数量
* @param {ObjectId} [options.userId] - 所属用户ID筛选(可选)
* @param {string} [options.name] - 患者姓名模糊筛选(可选)
* @param {string} [options.mobile] - 患者电话筛选(可选)
* @param {string} [options.sortBy="createtime"] - 排序字段:createtime | updatetime(可选)
* @returns {Promise<Object>} 返回 `{ list, total, page, pageSize }`
*/
HealthProfileSchema.statics.findProfiles = async function (options = {}) {
const { page = 1, pageSize = 20, userId, name, mobile, sortBy = "createtime" } = options;
const filter = {};
if (userId) {
filter.userId = userId;
}
if (name) {
// 转义正则特殊字符,避免查询报错或 ReDoS;姓名支持中文、全拼、拼音首字母匹配
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const re = { $regex: escaped, $options: "i" };
filter.$or = [{ "profile.name": re }, { "profile.pinyin": re }, { "profile.pinyinFL": re }];
}
if (mobile) {
filter["profile.mobile"] = mobile;
}
const sortField = sortBy === "updatetime" ? "meta.updatetime" : "meta.createtime";
const skip = (page - 1) * pageSize;
const [list, total] = await Promise.all([
this.find(filter)
.sort({ [sortField]: -1 })
.skip(skip)
.limit(pageSize)
.exec(),
this.countDocuments(filter).exec(),
]);
return { list, total, page: parseInt(page), pageSize: parseInt(pageSize) };
};
/**
* 根据ID查找健康档案
*/
HealthProfileSchema.statics.findProfileById = async function (id) {
return await this.findById(id).exec();
};
/**
* 创建健康档案
*/
HealthProfileSchema.statics.createProfile = async function (data) {
data.meta = { createtime: Date.now(), updatetime: Date.now() };
const doc = new this(data);
return await doc.save();
};
/**
* 根据ID更新健康档案(更新姓名时同步拼音字段)
*/
HealthProfileSchema.statics.updateProfileById = async function (id, update) {
syncPinyinOnUpdate(update);
update["meta.updatetime"] = Date.now();
return await this.findByIdAndUpdate(id, { $set: update }, { new: true }).exec();
};
/**
* 根据用户ID更新健康档案(更新姓名时同步拼音字段)
*/
HealthProfileSchema.statics.updateProfile = async function (userId, update) {
syncPinyinOnUpdate(update);
update["meta.updatetime"] = Date.now();
return await this.findOneAndUpdate({ userId }, { $set: update }, { new: true }).exec();
};
/**
* 根据ID删除健康档案
*/
HealthProfileSchema.statics.deleteProfileById = async function (id) {
return await this.findByIdAndDelete(id).exec();
};
// ==================== 索引定义 ====================
HealthProfileSchema.index({ userId: 1 }, { sparse: true });
HealthProfileSchema.index({ "profile.mobile": 1 });
export { HealthProfileSchema, genPinyin, genPinyinFL };