From eb58dfdbbe5da6f8fff8764d9582840a5eab8092 Mon Sep 17 00:00:00 2001 From: huxuejian Date: Tue, 25 Aug 2026 18:43:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20im=E5=9B=BE=E7=89=87=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 2 -- .env.pro | 2 +- README.md | 9 +++++++++ hlw/im-image-archive/index.js | 9 +++++++++ hlw/im-image-archive/index.test.js | 12 ++++++++++++ index.js | 10 ++++++++++ 6 files changed, 41 insertions(+), 3 deletions(-) diff --git a/.env.dev b/.env.dev index a255c81..bd63162 100644 --- a/.env.dev +++ b/.env.dev @@ -10,8 +10,6 @@ CONFIG_NODE_PORT=8080 CONFIG_ZYB_YB_APPID=7D8D8E29924048539165F5E2E08C4616 CONFIG_ZYB_YB_SECRET=77F9610253B94263BCF8DBB61BF90D92 -## 支付宝回调地址 -CONFIG_ALIPAY_PAY_NOTIFY=https://patient.youcan365.com/zyt/alipay-pay-notify ## 支付宝小程序appId CONFIG_ALIPAY_MINI_APPID = 2021004129669493 CONFIG_ALIPAY_MINI_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo7Ulswxr/3dJpX0RrPylV9rR3STdwGtcbV4ppNEiR0R37/9L/JpO/Y5gbEaaPn9S/xALzGKwI4N3/v1H2x566LJIQvzh5szCviy20NNGc+TbyxLJ9gLBxcsBDT8+o+a8CdaHbU/xvutZvLRa34D5pa6lcDCONC6q8flwWmXemUZ5widtW10F+4Niyufr+8hKkIpRfQMWYbsuA3EpXwjhztJglxPnjMN7r33vq9Pd5cU+KOOGuelXdB8aMwWcaXbffb7OKaTtZYnbUezJKbqZ+LtYgGF8+E5CKP7tzlEnZvQZwlPgS516EG3WRcCqjnEYO7RpG/eZ/0569d4TW8UPrwIDAQAB diff --git a/.env.pro b/.env.pro index d27fa67..179ef18 100644 --- a/.env.pro +++ b/.env.pro @@ -11,7 +11,7 @@ CONFIG_ZYB_YB_SECRET=77F9610253B94263BCF8DBB61BF90D92 CONFIG_SMS_GATEWAY_DISABLED=false ## 支付宝小程序appId -CONFIG_ALIPAY_MINI_APPID = 2021005110656858 +CONFIG_ALIPAY_MINI_APPID = 2021004129669493 CONFIG_ALIPAY_MINI_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo7Ulswxr/3dJpX0RrPylV9rR3STdwGtcbV4ppNEiR0R37/9L/JpO/Y5gbEaaPn9S/xALzGKwI4N3/v1H2x566LJIQvzh5szCviy20NNGc+TbyxLJ9gLBxcsBDT8+o+a8CdaHbU/xvutZvLRa34D5pa6lcDCONC6q8flwWmXemUZ5widtW10F+4Niyufr+8hKkIpRfQMWYbsuA3EpXwjhztJglxPnjMN7r33vq9Pd5cU+KOOGuelXdB8aMwWcaXbffb7OKaTtZYnbUezJKbqZ+LtYgGF8+E5CKP7tzlEnZvQZwlPgS516EG3WRcCqjnEYO7RpG/eZ/0569d4TW8UPrwIDAQAB CONFIG_ALIPAY_MINI_PRIVATE_KEY=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCirWNE4gjlUOV6+PNo6CPyLUnJOe/yw8ODZInBun6P2h3rs33WBLFBGdbV9Ct5pXXou48l2icKPsFAWVI5MmtZUIHGBkdvxzQeLNeKpKlJvpujWS/oLQq232MeM7YmWa4tjr40SHqlhVzxcgYST55jBXmVo9KJNrBj5i5zlR9N49x5EJZrUXsVUprsavk1z1JJyUKthw/FrycGsS47YQ5jHyQUFsqEviyOY5Bgz4qZ4xhHJ7p7zhwkiXHqscyeF0+Eu1xf4jS23jlbOiV+5yPo4eWcHElf0aD+k+ngPVpgVqNHMYBTSW1u8pFCpUVDv02QUNg1NRJpBcTAtnLY7yyhAgMBAAECggEAKl54ZV9tq7QnIKEoXGFhw1iSSCuj2BmumVEdI+zbu8G2W0YzdwNf2zHMp4rhq9wxWn1HKjeoRV3U3VqmNKlyFiAGvLmz72P1qgpJ1R1Pzhor1R6MgK8ChGCoIxyay2A+IjV0So+6CdQzQdtNw4UjHm01noi9c+lfhZVU8dub/J/62cGCfcBh/NX+juyI+5Vb9BLnG0CbFZ1SbssK8FN9w6MiKNq/fgIdMyvZXWt9Zoj21Bc9eyxd0EUPp800P6eeThzwNVyeSOETehIDSGwndrbxbLX1nCmievnEYZ/pT6T0J+UuPkdLjZq+VQQry0IxAlVvRim33JS4w99gBYIHcQKBgQDSQsEL5QtSLNg3xs5R3vMG+C8wJbm0eYiPlpSLcFgLxedoxRPdjzeT/Wfi245mA1L8NADRn37gfgPukc5TgWzTV340KzlScAv0Y2HHiQV9HII6b5I/f2S3AsM2IY4jDN+PryBDT5mTYUwW7LhtxC97LqCozPhY6aPozoD6VKBhFQKBgQDGEMAcrPSmXNjxhg/1lPJoe7fe4A3uyLPfHIVqZTFSxrfzwteL3bMuhsx8eSNnmT35trQfKXYT8Pz85IWGcoBPuhDkrjDQy+w5u4QeRcyJQRyUqtDxruIwNbvN/EL6FME24u6bIng6FjHW+0j2SFrFIWpUo1v3B5mSRGzvTWZIXQKBgAXI1lzIaV5SY4RtUMHJ22/Gpi2x7AD+Eva4Us/fQFzajfpA6cH/razBnPkNx0YhM0hC+f7KX4g3j04h7sY3WlIaJxuyULpm4L1ReqAtsdv2S9LlvOVIUPID/nDfzXRDZ2chj0aqyvPWCmzbCFvkuNWvu7XJJc/0ZZEDFWNNrrfdAoGAB/2dAosnw55zUisqcan0rapwHJnFdiC6wQGRkF1MxB4oRblSoN3s4K+niE4K058nFO/+DFPL47oROtKtYGM2MgE7tT4h0gqoGRwrlF0Uh2UNFiJeE6IOlgtU7FaFDTnRIbm+DpwxZYaHtDinFOiOYO2cMJX2m9y+oVwJWQ4yVjECgYEAg01aEYSOYQVeeub8g1e1wHKj3L4v/xECpZNzoQWF/WhZDHiKCqN2VD54XYl0dUHFLqBXn01tfg0gt4kO/roiWJdH273DBYu52ttwNdcRVlud6RlTBDO//M8dglrxOWz51HhSTepOXmJflF1dept9OPVddzQAJdFN65umF27DIVI= diff --git a/README.md b/README.md index 669589c..3c68af9 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,15 @@ CONFIG_IM_IMAGE_ALLOWED_HOSTS=.my-imcloud.com,.im.qcloud.com,.myqcloud.com `private-files` 不会通过 Express 静态目录公开。该目录不能在发布时被清理; Docker 部署时必须挂载持久化卷。 +服务启动时会主动创建并检查该目录是否可读写。启动日志中应出现: + +```text +腾讯 IM 图片归档目录已就绪,/opt/apps/private-files/im-chat +``` + +如果日志中出现 `EACCES`,需要给实际运行 Node/PM2 的 Linux 用户授予该目录的 +读写权限。 + 归档成功后可通过下面的接口读取图片: ```text diff --git a/hlw/im-image-archive/index.js b/hlw/im-image-archive/index.js index 7f013d8..cf48b22 100644 --- a/hlw/im-image-archive/index.js +++ b/hlw/im-image-archive/index.js @@ -28,6 +28,14 @@ function getImageRoot() { ); } +async function initializeStorage() { + const imageRoot = getImageRoot(); + await fsp.mkdir(imageRoot, { recursive: true }); + await fsp.access(imageRoot, fs.constants.R_OK | fs.constants.W_OK); + logger.info("腾讯 IM 图片归档目录已就绪", imageRoot); + return imageRoot; +} + function getMaxFileSize() { const configured = Number(process.env.CONFIG_IM_IMAGE_MAX_BYTES); return Number.isFinite(configured) && configured > 0 @@ -398,6 +406,7 @@ async function sendArchivedImage(req, res, db) { module.exports = { findOriginalImage, prepareMessageForArchive, + initializeStorage, enqueueArchive, resumePendingArchives, sendArchivedImage, diff --git a/hlw/im-image-archive/index.test.js b/hlw/im-image-archive/index.test.js index 6e71ed7..3d62241 100644 --- a/hlw/im-image-archive/index.test.js +++ b/hlw/im-image-archive/index.test.js @@ -107,6 +107,18 @@ describe("IM image archive", () => { ).toBe(path.resolve("/opt/apps", "private-files", "im-chat")); }); + test("creates and validates the configured storage directory", async () => { + const configuredRoot = path.join(temporaryRoot, "nested", "im-chat"); + process.env.CONFIG_IM_IMAGE_ROOT = configuredRoot; + + await expect(archive.initializeStorage()).resolves.toBe( + path.resolve(configuredRoot) + ); + await expect(fsp.access(configuredRoot, fs.constants.W_OK)).resolves.toBe( + undefined + ); + }); + test("downloads an image and marks the database record successful", async () => { const jpeg = Buffer.concat([ Buffer.from([0xff, 0xd8, 0xff, 0xe0]), diff --git a/index.js b/index.js index c6a0fa9..221c99e 100644 --- a/index.js +++ b/index.js @@ -201,6 +201,12 @@ const databaseReadyPromise = connectToMongoDB().catch((err) => { return null; }); +// 启动时立即创建并校验 IM 图片归档目录,避免等到首张图片下载时才暴露路径或权限问题。 +const imageStorageReadyPromise = imImageArchive.initializeStorage().catch((err) => { + logger.error("腾讯 IM 图片归档目录初始化失败:", err); + return null; +}); + app.get( "/zyt/stats", asyncHandler(async (req, res) => { @@ -727,6 +733,10 @@ trigger.initSchedule(); databaseReadyPromise.then(async (connected) => { if (!connected) return; try { + const imageStorageRoot = await imageStorageReadyPromise; + if (!imageStorageRoot) { + throw new Error("腾讯 IM 图片归档目录不可用"); + } const internetHospitalDb = await getDatabase("Internet-hospital"); await consultTrigger( {