333 lines
12 KiB
JavaScript
333 lines
12 KiB
JavaScript
const { ObjectId } = require("mongodb");
|
|
const validator = require("../../utils/validator");
|
|
const Sm4Util = require("../../utils/sm4-util");
|
|
const hnHis = require("../hn-his");
|
|
|
|
const COLLECTION_NAME = "hlw-patient-self-auth";
|
|
const PATIENT_COLLECTION_NAME = "hlw-patient";
|
|
const AUTH_VALID_MS = 10 * 60 * 1000;
|
|
const RECORD_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
const indexPromises = new WeakMap();
|
|
|
|
async function removeLegacyAuthIdIndex(collection) {
|
|
try {
|
|
await collection.dropIndex("authId_1");
|
|
} catch (error) {
|
|
// 旧索引不存在时无需处理;其他数据库错误仍应暴露,避免留下隐蔽的唯一索引冲突。
|
|
if (error && (error.code === 27 || error.codeName === "IndexNotFound")) return;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function ensureIndexes(db) {
|
|
if (!indexPromises.has(db)) {
|
|
const collection = db.collection(COLLECTION_NAME);
|
|
const promise = removeLegacyAuthIdIndex(collection)
|
|
.then(() => Promise.all([
|
|
collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }),
|
|
collection.createIndex({ ownerUserId: 1, status: 1 }),
|
|
]))
|
|
.catch((error) => {
|
|
indexPromises.delete(db);
|
|
throw error;
|
|
});
|
|
indexPromises.set(db, promise);
|
|
}
|
|
return indexPromises.get(db);
|
|
}
|
|
|
|
function parseRequestId(value) {
|
|
const id = typeof value === "string" ? value.trim() : "";
|
|
if (!/^[a-f\d]{24}$/i.test(id) || !ObjectId.isValid(id)) {
|
|
return { success: false, message: "授权链接无效" };
|
|
}
|
|
return {
|
|
success: true,
|
|
id,
|
|
objectId: new ObjectId(id),
|
|
};
|
|
}
|
|
|
|
function isExpired(requestTimestamp, now = Date.now()) {
|
|
return now - requestTimestamp > AUTH_VALID_MS;
|
|
}
|
|
|
|
function getCurrentUserId(context) {
|
|
const internalUserId = context && typeof context.userId === "string"
|
|
? context.userId.trim()
|
|
: "";
|
|
return ObjectId.isValid(internalUserId) ? internalUserId : "";
|
|
}
|
|
|
|
function decryptPatient(patient) {
|
|
return {
|
|
id: patient._id.toString(),
|
|
accountId: patient.accountId,
|
|
name: Sm4Util.decryptDataForSm3(patient.anotherName),
|
|
certNo: Sm4Util.decryptDataForSm3(patient.anotherIdNo),
|
|
mobile: Sm4Util.decryptDataForSm3(patient.anotherMobile),
|
|
address: typeof patient.address === "string" ? patient.address : "",
|
|
};
|
|
}
|
|
|
|
function maskCertNo(certNo) {
|
|
if (typeof certNo !== "string" || certNo.length <= 7) return "--";
|
|
return `${certNo.slice(0, 4)}${"*".repeat(certNo.length - 7)}${certNo.slice(-3)}`;
|
|
}
|
|
|
|
function getPersonInfo(certNo, now = new Date()) {
|
|
if (!validator.isChinaId(certNo)) return { age: "", sex: "" };
|
|
const birthYear = Number(certNo.slice(6, 10));
|
|
const birthMonth = Number(certNo.slice(10, 12));
|
|
const birthDay = Number(certNo.slice(12, 14));
|
|
const birthday = new Date(birthYear, birthMonth - 1, birthDay);
|
|
if (
|
|
birthday.getFullYear() !== birthYear
|
|
|| birthday.getMonth() + 1 !== birthMonth
|
|
|| birthday.getDate() !== birthDay
|
|
|| birthday > now
|
|
) {
|
|
return { age: "", sex: "" };
|
|
}
|
|
let age = now.getFullYear() - birthYear;
|
|
const birthdayPassed = now.getMonth() + 1 > birthMonth
|
|
|| (now.getMonth() + 1 === birthMonth && now.getDate() >= birthDay);
|
|
if (!birthdayPassed) age -= 1;
|
|
return {
|
|
age: Math.max(0, age),
|
|
sex: Number(certNo.charAt(16)) % 2 === 0 ? "女" : "男",
|
|
};
|
|
}
|
|
|
|
async function findRequest(db, parsed) {
|
|
return db.collection(COLLECTION_NAME).findOne({ _id: parsed.objectId });
|
|
}
|
|
|
|
async function createSelfAuthRequest(item, db, context) {
|
|
const patientId = item && typeof item.patientId === "string" ? item.patientId.trim() : "";
|
|
if (!ObjectId.isValid(patientId)) {
|
|
return { success: false, message: "就诊人ID格式不正确" };
|
|
}
|
|
|
|
try {
|
|
await ensureIndexes(db);
|
|
const ownerUserId = getCurrentUserId(context);
|
|
const accountId = item && typeof item.accountId === "string" ? item.accountId.trim() : "";
|
|
if (!ownerUserId || !accountId) return { success: false, message: "登录信息无效,请重新登录" };
|
|
|
|
const patient = await db.collection(PATIENT_COLLECTION_NAME).findOne({
|
|
_id: new ObjectId(patientId),
|
|
accountId,
|
|
disabled: false,
|
|
});
|
|
if (!patient) return { success: false, message: "就诊人不存在或无权操作" };
|
|
|
|
const requestTimestamp = Date.now();
|
|
const insertResult = await db.collection(COLLECTION_NAME).insertOne({
|
|
patientId: new ObjectId(patientId),
|
|
ownerAccountId: accountId,
|
|
ownerUserId,
|
|
requestTimestamp,
|
|
status: "pending",
|
|
attempts: 0,
|
|
createdAt: new Date(requestTimestamp),
|
|
updatedAt: new Date(requestTimestamp),
|
|
expiresAt: new Date(requestTimestamp + RECORD_RETENTION_MS),
|
|
});
|
|
return {
|
|
success: true,
|
|
message: "授权请求创建成功",
|
|
data: { id: insertResult.insertedId.toString() },
|
|
};
|
|
} catch (error) {
|
|
return { success: false, message: error.message || "授权请求创建失败" };
|
|
}
|
|
}
|
|
|
|
async function getSelfAuthPatient(item, db) {
|
|
const parsed = parseRequestId(item && item.id);
|
|
if (!parsed.success) return parsed;
|
|
|
|
try {
|
|
await ensureIndexes(db);
|
|
const request = await findRequest(db, parsed);
|
|
if (!request) return { success: false, message: "授权链接不存在或已失效" };
|
|
if (request.status !== "success" && isExpired(request.requestTimestamp)) {
|
|
return { success: false, message: "授权链接已过期,请重新发起授权" };
|
|
}
|
|
|
|
const patient = await db.collection(PATIENT_COLLECTION_NAME).findOne({
|
|
_id: request.patientId,
|
|
accountId: request.ownerAccountId,
|
|
disabled: false,
|
|
});
|
|
if (!patient) return { success: false, message: "就诊人不存在" };
|
|
|
|
const data = decryptPatient(patient);
|
|
const personInfo = getPersonInfo(data.certNo);
|
|
return {
|
|
success: true,
|
|
message: "获取就诊人成功",
|
|
data: {
|
|
name: data.name,
|
|
certNo: maskCertNo(data.certNo),
|
|
age: personInfo.age,
|
|
sex: personInfo.sex,
|
|
status: request.status,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
return { success: false, message: error.message || "获取授权信息失败" };
|
|
}
|
|
}
|
|
|
|
async function submitSelfAuth(item, db, context) {
|
|
const parsed = parseRequestId(item && item.id);
|
|
if (!parsed.success) return parsed;
|
|
const psnToken = item && typeof item.psnToken === "string" ? item.psnToken.trim() : "";
|
|
if (!psnToken || psnToken.length > 2048) {
|
|
return { success: false, message: "医保授权信息无效" };
|
|
}
|
|
|
|
const collection = db.collection(COLLECTION_NAME);
|
|
try {
|
|
await ensureIndexes(db);
|
|
const request = await findRequest(db, parsed);
|
|
if (!request) return { success: false, message: "授权链接不存在或已失效" };
|
|
if (request.status === "success") {
|
|
return { success: true, message: "已完成授权", data: { status: "success" } };
|
|
}
|
|
if (isExpired(request.requestTimestamp)) {
|
|
return { success: false, message: "授权链接已过期,请重新发起授权" };
|
|
}
|
|
|
|
const authorizerUserId = getCurrentUserId(context);
|
|
if (!authorizerUserId) return { success: false, message: "登录信息无效,请重新登录" };
|
|
|
|
const claimed = await collection.findOneAndUpdate(
|
|
{ _id: parsed.objectId, status: { $in: ["pending", "failed"] } },
|
|
{
|
|
$set: {
|
|
status: "processing",
|
|
authorizerUserId,
|
|
errorMessage: "",
|
|
updatedAt: new Date(),
|
|
},
|
|
$inc: { attempts: 1 },
|
|
},
|
|
{ returnDocument: "after" }
|
|
);
|
|
if (!claimed) {
|
|
const latest = await collection.findOne({ _id: parsed.objectId });
|
|
if (latest && latest.status === "success") {
|
|
return { success: true, message: "已完成授权", data: { status: "success" } };
|
|
}
|
|
return { success: false, message: "授权正在处理中,请勿重复提交", data: { status: "processing" } };
|
|
}
|
|
|
|
const patientRecord = await db.collection(PATIENT_COLLECTION_NAME).findOne({
|
|
_id: claimed.patientId,
|
|
accountId: claimed.ownerAccountId,
|
|
disabled: false,
|
|
});
|
|
if (!patientRecord) throw new Error("就诊人不存在");
|
|
const patient = decryptPatient(patientRecord);
|
|
|
|
const hisResult = await hnHis({
|
|
type: "addHisCustomer",
|
|
idCard: patient.certNo,
|
|
name: patient.name,
|
|
mobile: patient.mobile,
|
|
address: patient.address,
|
|
psnToken,
|
|
});
|
|
const patients = Array.isArray(hisResult && hisResult.list) ? hisResult.list : [];
|
|
const hisArchive = patients.find((entry) => entry && entry.socialno === patient.certNo);
|
|
if (!hisResult || !hisResult.success || !hisArchive) {
|
|
throw new Error((hisResult && hisResult.message) || "医保建档失败,未找到匹配档案");
|
|
}
|
|
if (hisArchive.isyb !== "1") throw new Error("医保建档失败");
|
|
|
|
const result = {
|
|
...hisArchive,
|
|
socialno: hisArchive.socialno || patient.certNo,
|
|
tmbxx: Array.isArray(hisResult.tmbxx) ? hisResult.tmbxx : [],
|
|
};
|
|
const completedAt = new Date();
|
|
await collection.updateOne(
|
|
{ _id: parsed.objectId, status: "processing" },
|
|
{
|
|
$set: {
|
|
status: "success",
|
|
encryptedResult: Sm4Util.encryptDataForSm3(JSON.stringify(result)),
|
|
encryption: "SM4",
|
|
completedAt,
|
|
updatedAt: completedAt,
|
|
},
|
|
$unset: { errorMessage: "" },
|
|
}
|
|
);
|
|
return { success: true, message: "授权并建档成功", data: { status: "success" } };
|
|
} catch (error) {
|
|
await collection.updateOne(
|
|
{ _id: parsed.objectId, status: "processing" },
|
|
{ $set: { status: "failed", errorMessage: error.message || "医保建档失败", updatedAt: new Date() } }
|
|
).catch(() => { });
|
|
return { success: false, message: error.message || "医保建档失败", data: { status: "failed" } };
|
|
}
|
|
}
|
|
|
|
async function getSelfAuthResult(item, db, context) {
|
|
const parsed = parseRequestId(item && item.id);
|
|
console.log('parsed: ', parsed)
|
|
if (!parsed.success) return parsed;
|
|
try {
|
|
await ensureIndexes(db);
|
|
const ownerUserId = getCurrentUserId(context);
|
|
if (!ownerUserId) return { success: false, message: "登录信息无效,请重新登录" };
|
|
const request = await findRequest(db, parsed);
|
|
console.log('request: ', request)
|
|
if (!request || request.ownerUserId !== ownerUserId) {
|
|
return { success: false, message: "授权记录不存在或无权访问" };
|
|
}
|
|
|
|
if (request.status === "success") {
|
|
if (!request.encryptedResult || request.encryption !== "SM4") {
|
|
return { success: false, message: "授权结果无效" };
|
|
}
|
|
const hisArchive = JSON.parse(Sm4Util.decryptDataForSm3(request.encryptedResult));
|
|
return { success: true, message: "授权成功", data: { status: "success", hisArchive } };
|
|
}
|
|
if (request.status !== "processing" && isExpired(request.requestTimestamp)) {
|
|
return { success: true, message: "授权链接已过期", data: { status: "expired" } };
|
|
}
|
|
return {
|
|
success: true,
|
|
message: request.errorMessage || "等待就诊人授权",
|
|
data: { status: request.status, errorMessage: request.errorMessage || "" },
|
|
};
|
|
} catch (error) {
|
|
return { success: false, message: error.message || "查询授权结果失败" };
|
|
}
|
|
}
|
|
|
|
module.exports = async (item, db, context) => {
|
|
switch (item.type) {
|
|
case "createSelfAuthRequest":
|
|
return createSelfAuthRequest(item, db, context);
|
|
case "getSelfAuthPatient":
|
|
return getSelfAuthPatient(item, db);
|
|
case "submitSelfAuth":
|
|
return submitSelfAuth(item, db, context);
|
|
case "getSelfAuthResult":
|
|
return getSelfAuthResult(item, db, context);
|
|
default:
|
|
return { success: false, message: "未找到本人授权接口" };
|
|
}
|
|
};
|
|
|
|
module.exports.parseRequestId = parseRequestId;
|
|
module.exports.maskCertNo = maskCertNo;
|
|
module.exports.getPersonInfo = getPersonInfo;
|
|
module.exports.constants = { AUTH_VALID_MS, RECORD_RETENTION_MS, COLLECTION_NAME };
|