diff --git a/.env b/.env index 6b652c6..82e4ec3 100644 --- a/.env +++ b/.env @@ -1,3 +1,6 @@ TAVILY_API_KEY=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 DEEPSEEK_API_KEY=sk-a58ccd82b7ba4ce3ac176a88c9381095 -BAIDU_MAP_AUTH_TOKEN=sk-ap-9xcqwNJ3FyJGUoAoQoCgWLqPd5o2tJKCA1xaSMRaZT0zDo0PCFm7rczL4anUuXf5 \ No newline at end of file +BAIDU_MAP_AUTH_TOKEN=sk-ap-9xcqwNJ3FyJGUoAoQoCgWLqPd5o2tJKCA1xaSMRaZT0zDo0PCFm7rczL4anUuXf5 +PORT=9004 +WS_PORT=9005 +USER_SERVICE_URL=http://127.0.0.1:9010 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 80dfd1e..121cda6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /node_modules/ /logs/ /data/ +.env +conf.json diff --git a/agent/escort/task.js b/agent/escort/task.js index 9809663..5a10418 100644 --- a/agent/escort/task.js +++ b/agent/escort/task.js @@ -11,15 +11,29 @@ class ChatTask { maxIterations: options.maxIterations || 10, }; - this.agents = {}; + this.agents = new Map(); + this.maxAgents = options.maxAgents || 100; } async streamChat(userInfo, message, callback) { const userId = userInfo ? userInfo._id : message.appId; - if (!this.agents[userId]) { - this.agents[userId] = new EscortAgent(); + + let agent = this.agents.get(userId); + if (agent) { + // LRU:重新插入以更新使用顺序 + this.agents.delete(userId); + } else { + agent = new EscortAgent(); } - return this.agents[userId].streamChat(userInfo, [message], callback); + this.agents.set(userId, agent); + + // 超出上限时淘汰最久未使用的 Agent,避免内存泄漏 + if (this.agents.size > this.maxAgents) { + const oldest = this.agents.keys().next().value; + this.agents.delete(oldest); + } + + return agent.streamChat(userInfo, [message], callback); } } diff --git a/handler/escort_record.js b/handler/escort_record.js index 943de1b..6526826 100644 --- a/handler/escort_record.js +++ b/handler/escort_record.js @@ -5,21 +5,51 @@ class HandlerEscortRecord { constructor() { } + // 白名单:只允许写入 schema 定义的顶层字段(兼容 'patient.name' 等点号路径) + static RECORD_FIELDS = [ + "userId", "healthProfileId", "patient", "escort", + "attendant", "hospital", "schedule", "payment", "notes", "status", + ]; + + static pickFields(body) { + const picked = {}; + for (const key of Object.keys(body || {})) { + const allowed = HandlerEscortRecord.RECORD_FIELDS.some( + (f) => key === f || key.startsWith(f + ".") + ); + if (allowed) { + picked[key] = body[key]; + } + } + return picked; + } + + // 按条件查询记录(管理端,可查全部) async getRecords(ctx) { + return this._listRecords(ctx, false); + } + + // 查询"我的"记录:强制以登录用户身份查询,忽略 query 中的 userId + async getMyRecords(ctx) { + return this._listRecords(ctx, true); + } + + async _listRecords(ctx, forceSelf) { try { const { page = 1, pageSize = 20, status, userId, appointmentDate } = ctx.request.query; + const effectiveUserId = forceSelf ? ctx.state.user?._id : userId; // status解析成数组 let statusArray = null; if (status && status.length > 0) { statusArray = status.split(',') - } + } const records = await DBModel.EscortRecord.findRecords({ page: parseInt(page), pageSize: parseInt(pageSize), status: statusArray, - userId, + userId: effectiveUserId, appointmentDate, }); @@ -31,7 +61,8 @@ class HandlerEscortRecord { async getAttendantRecords(ctx) { try { - const attendantId = ctx.state.user?._id || ctx.request.query?.attendantId; + // 只允许查询当前登录陪诊员自己的记录 + const attendantId = ctx.state.user?._id; if (!attendantId) { return ResponseUtil.badRequest(ctx, "缺少陪诊员ID"); } @@ -74,7 +105,7 @@ class HandlerEscortRecord { return ResponseUtil.badRequest(ctx, "缺少患者信息"); } - const newRecord = await DBModel.EscortRecord.createRecord(record); + const newRecord = await DBModel.EscortRecord.createRecord(HandlerEscortRecord.pickFields(record)); return ResponseUtil.success(ctx, { record: newRecord }, "创建成功"); } catch (err) { return ResponseUtil.internalError(ctx, err.message); @@ -90,7 +121,7 @@ class HandlerEscortRecord { return ResponseUtil.badRequest(ctx, "缺少记录ID"); } - const updatedRecord = await DBModel.EscortRecord.updateRecord(id, update); + const updatedRecord = await DBModel.EscortRecord.updateRecord(id, HandlerEscortRecord.pickFields(update)); if (!updatedRecord) { return ResponseUtil.error(ctx, "陪诊记录不存在", null, 404); } diff --git a/handler/health_profile.js b/handler/health_profile.js index 2e360d1..48ae11d 100644 --- a/handler/health_profile.js +++ b/handler/health_profile.js @@ -5,6 +5,22 @@ class HandlerHealthProfile { constructor() { } + // 白名单:只允许写入 schema 定义的顶层字段(兼容 'profile.name' 等点号路径) + static PROFILE_FIELDS = ["userId", "profile", "health"]; + + static pickFields(body) { + const picked = {}; + for (const key of Object.keys(body || {})) { + const allowed = HandlerHealthProfile.PROFILE_FIELDS.some( + (f) => key === f || key.startsWith(f + ".") + ); + if (allowed) { + picked[key] = body[key]; + } + } + return picked; + } + async getProfiles(ctx) { try { const { page = 1, pageSize = 20, userId, name, mobile, sortBy } = ctx.request.query; @@ -46,7 +62,7 @@ class HandlerHealthProfile { try { const body = ctx.request.body; - const newProfile = await DBModel.HealthProfile.createProfile(body); + const newProfile = await DBModel.HealthProfile.createProfile(HandlerHealthProfile.pickFields(body)); return ResponseUtil.success(ctx, { profile: newProfile }, "创建成功"); } catch (err) { return ResponseUtil.internalError(ctx, err.message); @@ -62,7 +78,7 @@ class HandlerHealthProfile { return ResponseUtil.badRequest(ctx, "缺少档案ID"); } - const updatedProfile = await DBModel.HealthProfile.updateProfileById(id, update); + const updatedProfile = await DBModel.HealthProfile.updateProfileById(id, HandlerHealthProfile.pickFields(update)); if (!updatedProfile) { return ResponseUtil.notFound(ctx, "健康档案不存在"); } diff --git a/handler/org.js b/handler/org.js deleted file mode 100644 index 1caf4cf..0000000 --- a/handler/org.js +++ /dev/null @@ -1,170 +0,0 @@ -import { DBModel } from "../models/index.js"; -import ResponseUtil from "../utils/responseUtil.js"; - -class HandlerHospital { - constructor() { - } - - async searchHospitalByName(ctx) { - try { - const { name, page = 1, pageSize = 20 } = ctx.request.query; - if (!name) { - return ResponseUtil.badRequest(ctx, "缺少搜索关键词"); - } - - const hospitals = await DBModel.Hospital.findByName(name, { - page: parseInt(page), - pageSize: parseInt(pageSize), - }); - - return ResponseUtil.success(ctx, { hospitals }, "查询成功"); - } catch (err) { - return ResponseUtil.internalError(ctx, err.message); - } - } - - async getHospitalsByCity(ctx) { - try { - const { city, page = 1, pageSize = 20, level, type } = ctx.request.query; - if (!city) { - return ResponseUtil.badRequest(ctx, "缺少城市参数"); - } - - const hospitals = await DBModel.Hospital.findByCity(city, { - page: parseInt(page), - pageSize: parseInt(pageSize), - level, - type, - }); - - return ResponseUtil.success(ctx, { hospitals }, "查询成功"); - } catch (err) { - return ResponseUtil.internalError(ctx, err.message); - } - } - - async getHospitalSelector(ctx) { - try { - const { city, level, type } = ctx.request.query; - const hospitals = await DBModel.Hospital.getHospitalSelector({ - city, - level, - type, - }); - - return ResponseUtil.success(ctx, { hospitals }, "查询成功"); - } catch (err) { - return ResponseUtil.internalError(ctx, err.message); - } - } - - async getHospitalById(ctx) { - try { - const { id } = ctx.params; - if (!id) { - return ResponseUtil.badRequest(ctx, "缺少医院ID"); - } - - const hospital = await DBModel.Hospital.findById(id); - if (!hospital) { - return ResponseUtil.notFound(ctx, "医院不存在"); - } - - return ResponseUtil.success(ctx, { hospital }, "查询成功"); - } catch (err) { - return ResponseUtil.internalError(ctx, err.message); - } - } - - async createHospital(ctx) { - try { - const hospital = ctx.request.body; - if (!hospital.basic?.name) { - return ResponseUtil.badRequest(ctx, "缺少医院名称"); - } - - const newHospital = await DBModel.Hospital.createHospital(hospital); - return ResponseUtil.success(ctx, { hospital: newHospital }, "创建成功"); - } catch (err) { - return ResponseUtil.internalError(ctx, err.message); - } - } - - async updateHospital(ctx) { - try { - const { id } = ctx.params; - const update = ctx.request.body; - - if (!id) { - return ResponseUtil.badRequest(ctx, "缺少医院ID"); - } - - const updatedHospital = await DBModel.Hospital.updateHospital(id, update); - if (!updatedHospital) { - return ResponseUtil.notFound(ctx, "医院不存在"); - } - - return ResponseUtil.success(ctx, { hospital: updatedHospital }, "更新成功"); - } catch (err) { - return ResponseUtil.internalError(ctx, err.message); - } - } - - async setHospitalStatus(ctx) { - try { - const { id } = ctx.params; - const { isEnabled } = ctx.request.body; - - if (!id) { - return ResponseUtil.badRequest(ctx, "缺少医院ID"); - } - - const updatedHospital = await DBModel.Hospital.setHospitalStatus(id, isEnabled); - if (!updatedHospital) { - return ResponseUtil.notFound(ctx, "医院不存在"); - } - - return ResponseUtil.success(ctx, { hospital: updatedHospital }, "状态更新成功"); - } catch (err) { - return ResponseUtil.internalError(ctx, err.message); - } - } - - async getAllHospitals(ctx) { - try { - const { page = 1, pageSize = 20 } = ctx.request.query; - const skip = (parseInt(page) - 1) * parseInt(pageSize); - - const hospitals = await DBModel.Hospital.find({}) - .sort({ "service.sortOrder": 1, "basic.name": 1 }) - .skip(skip) - .limit(parseInt(pageSize)); - - const total = await DBModel.Hospital.countDocuments({}); - - return ResponseUtil.success(ctx, { hospitals, total }, "查询成功"); - } catch (err) { - return ResponseUtil.internalError(ctx, err.message); - } - } - - async deleteHospital(ctx) { - try { - const { id } = ctx.params; - if (!id) { - return ResponseUtil.badRequest(ctx, "缺少医院ID"); - } - - const deletedHospital = await DBModel.Hospital.findByIdAndDelete(id); - if (!deletedHospital) { - return ResponseUtil.notFound(ctx, "医院不存在"); - } - - return ResponseUtil.success(ctx, null, "删除成功"); - } catch (err) { - return ResponseUtil.internalError(ctx, err.message); - } - } -} - -export { HandlerHospital }; \ No newline at end of file diff --git a/index.js b/index.js index 734414b..a6a6a63 100644 --- a/index.js +++ b/index.js @@ -4,8 +4,8 @@ import WebSocketServerManager from './websocket.js' // HTTP server const koaApp = new APP(); -koaApp.start(9004); +koaApp.start(parseInt(process.env.PORT || "9004")); // WebSocket server -const wsServer = new WebSocketServerManager(9005); +const wsServer = new WebSocketServerManager(parseInt(process.env.WS_PORT || "9005")); wsServer.start(); diff --git a/models/index.js b/models/index.js index c52696d..d5d67b6 100644 --- a/models/index.js +++ b/models/index.js @@ -2,7 +2,6 @@ import mongoose from 'mongoose'; import { EscortRecordSchema } from "./schema/escort_record.js" -import { OrganizationSchema } from "./schema/org.js" import { HealthProfileSchema } from "./schema/health_profile.js" import config from '../conf.json' with { type: 'json' }; import logger from '../utils/logger.js'; @@ -21,22 +20,22 @@ class MongoDBSchema { this.dbConnection = mongoose.createConnection(config.mongodb.str, config.mongodb.option); this.dbConnection.on("error", () => { - logger.error.bind(logger, "...mongodb connect error ...") + logger.error("...mongodb connect error ..."); }); this.dbConnection.on("connected", async () => { - logger.info("Mongodb: " + config.mongodb.str + " connected"); + logger.info(`Mongodb: ${config.mongodb.host}/${config.mongodb.option.dbName} connected`); }); this.dbConnection.on("disconnected", () => - logger.warn("Mongodb: " + config.mongodb.str + " disconnected") + logger.warn(`Mongodb: ${config.mongodb.host}/${config.mongodb.option.dbName} disconnected`) ); this.dbConnection.on("reconnected", () => - logger.info("Mongodb: " + config.mongodb.str + " reconnected") + logger.info(`Mongodb: ${config.mongodb.host}/${config.mongodb.option.dbName} reconnected`) ); this.dbConnection.on("disconnecting", () => - logger.warn("Mongodb: " + config.mongodb.str + " disconnecting") + logger.warn(`Mongodb: ${config.mongodb.host}/${config.mongodb.option.dbName} disconnecting`) ); this.dbConnection.on("close", () => - logger.warn("Mongodb: " + config.mongodb.str + " closed") + logger.warn(`Mongodb: ${config.mongodb.host}/${config.mongodb.option.dbName} closed`) ); this.EscortRecord = this.dbConnection.model('escort_record', EscortRecordSchema) diff --git a/models/schema/escort_record.js b/models/schema/escort_record.js index cf2860f..e117233 100644 --- a/models/schema/escort_record.js +++ b/models/schema/escort_record.js @@ -7,7 +7,7 @@ import mongoose from "mongoose"; * * 用于记录和管理陪诊服务全流程数据,包含以下主要分类: * - 基础信息:userId(订单提交用户)、healthProfileId(关联健康档案ID) - * - 患者信息:patient(姓名、电话、性别、年龄、身份证号) + * - 患者信息:patient(姓名、电话、性别、出生年月、身份证号) * - 陪诊服务:escort(服务ID、服务名称) * - 就诊信息:hospital(医院省份、名称、地址、科室、医生、病历号) * - 时间安排:schedule(预约时间、开始时间、结束时间、时长) @@ -50,7 +50,7 @@ const EscortRecordSchema = mongoose.Schema( name: { type: String, default: "", comment: "患者姓名" }, mobile: { type: String, default: "", index: true, comment: "患者联系电话" }, sex: { type: String, enum: ["male", "female"], comment: "患者性别" }, - age: { type: Number, default: 0, comment: "患者年龄" }, + birth: { type: String, default: "", comment: "患者出生年月(YYYY-MM-DD)" }, weight: { type: Number, default: 0, comment: "患者体重(kg)" }, height: { type: Number, default: 0, comment: "患者身高(cm)" }, idnumber: { type: String, default: "", comment: "患者身份证号" }, @@ -193,6 +193,34 @@ EscortRecordSchema.statics.findRecords = async function (options = {}, cb) { .exec(cb); }; +/** + * 查找陪诊员的陪诊记录 + * + * @param {ObjectId|string} attendantId - 陪诊员用户ID + * @param {Object} options - 查询选项 + * @param {number} [options.page=1] - 页码 + * @param {number} [options.pageSize=20] - 每页数量 + * @param {string} [options.status] - 状态筛选(可选) + * @param {Function} [cb] - 可选的回调函数 + * @returns {Promise} 陪诊记录列表(按预约日期倒序) + */ +EscortRecordSchema.statics.findRecordsByAttendant = async function (attendantId, options = {}, cb) { + const { page = 1, pageSize = 20, status } = options; + const filter = { "attendant.id": attendantId }; + + if (status) { + filter.status = status; + } + + const skip = (page - 1) * pageSize; + + return await this.find(filter) + .sort({ "schedule.date": -1 }) + .skip(skip) + .limit(pageSize) + .exec(cb); +}; + /** * 创建陪诊记录 * @@ -219,13 +247,8 @@ EscortRecordSchema.statics.createRecord = async function (record, cb) { * @returns {Promise} 更新后的记录,失败返回null */ EscortRecordSchema.statics.updateRecord = async function (id, update, cb) { - try { - update["meta.updatetime"] = Date.now(); - return await this.findByIdAndUpdate(id, { $set: update }, { new: true }, cb); - } catch (error) { - console.error("更新陪诊记录失败:", error); - return null; - } + const $set = { ...update, "meta.updatetime": Date.now() }; + return await this.findByIdAndUpdate(id, { $set }, { new: true }, cb); }; /** @@ -268,12 +291,7 @@ EscortRecordSchema.statics.findRecordsByStatus = async function (status, options * @returns {Promise} 删除的记录,失败返回null */ EscortRecordSchema.statics.deleteRecord = async function (id, cb) { - try { - return await this.findByIdAndDelete(id, cb); - } catch (error) { - console.error("删除陪诊记录失败:", error); - return null; - } + return await this.findByIdAndDelete(id, cb); }; // ==================== 索引定义 ==================== diff --git a/models/schema/health_profile.js b/models/schema/health_profile.js index d180d49..bd9dd3a 100644 --- a/models/schema/health_profile.js +++ b/models/schema/health_profile.js @@ -21,7 +21,7 @@ const HealthProfileSchema = mongoose.Schema( name: { type: String, default: "", comment: "患者姓名" }, mobile: { type: String, default: "", index: true, comment: "患者电话" }, sex: { type: String, enum: ["male", "female", ""], default: "", comment: "性别" }, - age: { type: Number, default: 0, comment: "年龄" }, + birth: { type: String, default: "", comment: "出生年月(YYYY-MM-DD)" }, idnumber: { type: String, default: "", comment: "身份证号" }, }, @@ -76,7 +76,9 @@ HealthProfileSchema.statics.findByUserId = async function (userId) { filter.userId = userId; } if (name) { - filter["profile.name"] = { $regex: name, $options: "i" }; + // 转义正则特殊字符,避免查询报错或 ReDoS + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + filter["profile.name"] = { $regex: escaped, $options: "i" }; } if (mobile) { filter["profile.mobile"] = mobile; @@ -132,12 +134,7 @@ HealthProfileSchema.statics.updateProfile = async function (userId, update) { * 根据ID删除健康档案 */ HealthProfileSchema.statics.deleteProfileById = async function (id) { - try { - return await this.findByIdAndDelete(id).exec(); - } catch (error) { - console.error("删除健康档案失败:", error); - return null; - } + return await this.findByIdAndDelete(id).exec(); }; // ==================== 索引定义 ==================== diff --git a/routes/index.js b/routes/index.js index 6540188..2ce55b2 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,6 +2,28 @@ import Router from "koa-router"; import { HandlerEscortRecord } from "../handler/escort_record.js"; import { HandlerHealthProfile } from "../handler/health_profile.js"; import { HandlerResource } from "../handler/resource.js"; +import ResponseUtil from "../utils/responseUtil.js"; +import { getUserByToken } from "../utils/user_service.js"; + +/** + * 鉴权中间件:校验 token 并将用户信息挂到 ctx.state.user + * token→user 结果带 5 分钟进程内缓存(见 utils/user_service.js) + */ +async function requireAuth(ctx, next) { + const token = ctx.header["token"] || ctx.header["authorization"] || ctx.request.query?.token; + if (!token) { + ResponseUtil.unauthorized(ctx, "缺少token"); + return; + } + + const user = await getUserByToken(token); + if (!user) { + ResponseUtil.unauthorized(ctx, "登录已失效"); + return; + } + ctx.state.user = user; + await next(); +} function printRoutes(stack) { for (const layer of stack) { @@ -19,19 +41,21 @@ function registerRoutes(app) { const handlerHealthProfile = new HandlerHealthProfile(); const handlerResource = new HandlerResource(); - router.get("/escort-record/my", handlerEscortRecord.getRecords.bind(handlerEscortRecord)); - router.get("/escort-record/attendant", handlerEscortRecord.getAttendantRecords.bind(handlerEscortRecord)); - router.get("/escort-record/:id", handlerEscortRecord.getRecordById.bind(handlerEscortRecord)); - router.post("/escort-record", handlerEscortRecord.createRecord.bind(handlerEscortRecord)); - router.put("/escort-record/:id", handlerEscortRecord.updateRecord.bind(handlerEscortRecord)); - router.patch("/escort-record/:id/status", handlerEscortRecord.updateStatus.bind(handlerEscortRecord)); - router.delete("/escort-record/:id", handlerEscortRecord.deleteRecord.bind(handlerEscortRecord)); + // 敏感数据路由需要登录后访问 + router.get("/escort-record/my", requireAuth, handlerEscortRecord.getMyRecords.bind(handlerEscortRecord)); + router.get("/escort-record/attendant", requireAuth, handlerEscortRecord.getAttendantRecords.bind(handlerEscortRecord)); + router.get("/escort-record", requireAuth, handlerEscortRecord.getRecords.bind(handlerEscortRecord)); + router.get("/escort-record/:id", requireAuth, handlerEscortRecord.getRecordById.bind(handlerEscortRecord)); + router.post("/escort-record", requireAuth, handlerEscortRecord.createRecord.bind(handlerEscortRecord)); + router.put("/escort-record/:id", requireAuth, handlerEscortRecord.updateRecord.bind(handlerEscortRecord)); + router.patch("/escort-record/:id/status", requireAuth, handlerEscortRecord.updateStatus.bind(handlerEscortRecord)); + router.delete("/escort-record/:id", requireAuth, handlerEscortRecord.deleteRecord.bind(handlerEscortRecord)); - router.get("/health-profile", handlerHealthProfile.getProfiles.bind(handlerHealthProfile)); - router.get("/health-profile/:id", handlerHealthProfile.getProfileById.bind(handlerHealthProfile)); - router.post("/health-profile", handlerHealthProfile.createProfile.bind(handlerHealthProfile)); - router.put("/health-profile/:id", handlerHealthProfile.updateProfile.bind(handlerHealthProfile)); - router.delete("/health-profile/:id", handlerHealthProfile.deleteProfile.bind(handlerHealthProfile)); + router.get("/health-profile", requireAuth, handlerHealthProfile.getProfiles.bind(handlerHealthProfile)); + router.get("/health-profile/:id", requireAuth, handlerHealthProfile.getProfileById.bind(handlerHealthProfile)); + router.post("/health-profile", requireAuth, handlerHealthProfile.createProfile.bind(handlerHealthProfile)); + router.put("/health-profile/:id", requireAuth, handlerHealthProfile.updateProfile.bind(handlerHealthProfile)); + router.delete("/health-profile/:id", requireAuth, handlerHealthProfile.deleteProfile.bind(handlerHealthProfile)); router.get("/service", handlerResource.getServices.bind(handlerResource)); router.get("/agreement", handlerResource.getAgreement.bind(handlerResource)); diff --git a/utils/authToken.js b/utils/authToken.js index 49ef726..5814ec8 100644 --- a/utils/authToken.js +++ b/utils/authToken.js @@ -118,11 +118,8 @@ AuthToken.prototype.koaRequest = async function (ctx, next) { let tokenData = await this.get(token); if (tokenData && tokenData.uid) { - const { DBModel } = await import("../models/index.js"); - const user = await DBModel.User.findOne({ _id: tokenData.uid }); - if (user) { - ctx.userInfo = user; - } + // 用户体系在外部服务(user 服务),tokenData 中已包含 uid + ctx.userInfo = tokenData; } ctx.authToken = this; diff --git a/utils/user_service.js b/utils/user_service.js new file mode 100644 index 0000000..05d4c29 --- /dev/null +++ b/utils/user_service.js @@ -0,0 +1,99 @@ +import fetch from "node-fetch"; + +const USER_SERVICE_URL = process.env.USER_SERVICE_URL || "http://127.0.0.1:9010"; +const CACHE_TTL = 5 * 60 * 1000; // 成功结果缓存 5 分钟 +const NEGATIVE_TTL = 60 * 1000; // 无效 token 短缓存,防止打爆 user 服务 +const MAX_CACHE_SIZE = 2000; + +// key -> { user, expiresAt },Map 按插入序实现简易 LRU +const cache = new Map(); + +function cacheGet(key) { + const hit = cache.get(key); + if (!hit) return undefined; + if (Date.now() > hit.expiresAt) { + cache.delete(key); + return undefined; + } + // 重新插入以更新 LRU 顺序 + cache.delete(key); + cache.set(key, hit); + return hit.user; +} + +function cacheSet(key, user, ttl) { + cache.set(key, { user, expiresAt: Date.now() + ttl }); + if (cache.size > MAX_CACHE_SIZE) { + cache.delete(cache.keys().next().value); + } +} + +async function lookup(body) { + const res = await fetch(`${USER_SERVICE_URL}/user/userInfo`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + const data = await res.json(); + return data?.data?.user || null; +} + +/** + * 获取用户信息(带 5 分钟进程内缓存,token→user) + * + * 注意:缓存期内登出/账户锁定/用户信息变更最多延迟 5 分钟生效 + * (user 服务自身接口即时生效) + * + * @param {string} token 登录 token + * @param {string} [userId] 备用用户ID(token 缺失或失效时的兜底路径,保持原 WS 行为) + * @returns {Promise} 用户信息或 null + */ +export async function getUserInfo(token, userId) { + if (!token && !userId) return null; + + // 1. token 路径(带缓存) + if (token) { + const key = `tk:${token}`; + const cached = cacheGet(key); + if (cached !== undefined) return cached; + + try { + const user = await lookup({ token }); + if (user) { + cacheSet(key, user, CACHE_TTL); + return user; + } + // 无效 token 短缓存,保护 user 服务 + cacheSet(key, null, NEGATIVE_TTL); + } catch (err) { + // 网络异常不缓存,走 userId 兜底 + console.error("getUserByToken error:", err.message); + } + } + + // 2. userId 兜底路径 + if (userId) { + const key = `uid:${userId}`; + const cached = cacheGet(key); + if (cached !== undefined) return cached; + + try { + const user = await lookup({ userId }); + if (user) { + cacheSet(key, user, CACHE_TTL); + return user; + } + } catch (err) { + console.error("getUserById error:", err.message); + } + } + + return null; +} + +/** + * 仅按 token 获取用户信息(REST 鉴权用,无 userId 兜底) + */ +export async function getUserByToken(token) { + return getUserInfo(token, null); +} diff --git a/websocket.js b/websocket.js index f0437e9..9a976c4 100644 --- a/websocket.js +++ b/websocket.js @@ -1,9 +1,9 @@ import WebSocket, { WebSocketServer } from 'ws'; import http from 'http'; -import fetch from 'node-fetch'; import { DBModel } from "./models/index.js"; import { chatTask } from "./agent/escort/task.js"; import { adminAgent } from "./agent/escort-admin/agent.js" +import { getUserInfo } from "./utils/user_service.js"; export default class WebSocketServerManager { constructor(port = 8080) { @@ -81,16 +81,33 @@ export default class WebSocketServerManager { } if (msg.type === 'chat' || msg.type === 'clear') { - if (msg.agent === 'escort-admin') { - const userInfo = await this.getUserInfo(msg.token, msg.userId); - adminAgent.streamChat(userInfo, [msg], (source, type, content, id) => { + // 客户端断开后不再处理消息 + if (ws.readyState !== WebSocket.OPEN) return; + + const send = (source, type, content, id) => { + if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ source, type, content, id })); - }); - } else { - const userInfo = await this.getUserInfo(msg.token, msg.userId); - chatTask.streamChat(userInfo, msg, (source, type, content, id) => { - ws.send(JSON.stringify({ source, type, content, id })); - }); + } + }; + + try { + const userInfo = await getUserInfo(msg.token, msg.userId); + if (!userInfo) { + ws.send(JSON.stringify({ type: 'error', content: '登录已失效' })); + return; + } + if (msg.agent === 'escort-admin') { + await adminAgent.streamChat(userInfo, [msg], (source, type, content, id) => { + send(source, type, content, id); + }); + } else { + await chatTask.streamChat(userInfo, msg, (source, type, content, id) => { + send(source, type, content, id); + }); + } + } catch (err) { + console.error('streamChat error:', err); + send('system', 'error', '处理消息失败'); } return; } @@ -132,25 +149,4 @@ export default class WebSocketServerManager { if (!this.wss) return 0; return this.wss.clients.size; } - - async getUserInfo(token, userId) { - try { - if (!token && !userId) return null; - - const url = "http://127.0.0.1:9010/user/userInfo"; - const res = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - token, - userId - }) - }); - const data = await res.json(); - return data.data.user; - } catch (error) { - console.error('Error fetching user info:', error); - return null; - } - } }