feat: im图片存储

This commit is contained in:
huxuejian 2026-08-25 18:43:04 +08:00
parent 44ac5d3a02
commit eb58dfdbbe
6 changed files with 41 additions and 3 deletions

View File

@ -10,8 +10,6 @@ CONFIG_NODE_PORT=8080
CONFIG_ZYB_YB_APPID=7D8D8E29924048539165F5E2E08C4616 CONFIG_ZYB_YB_APPID=7D8D8E29924048539165F5E2E08C4616
CONFIG_ZYB_YB_SECRET=77F9610253B94263BCF8DBB61BF90D92 CONFIG_ZYB_YB_SECRET=77F9610253B94263BCF8DBB61BF90D92
## 支付宝回调地址
CONFIG_ALIPAY_PAY_NOTIFY=https://patient.youcan365.com/zyt/alipay-pay-notify
## 支付宝小程序appId ## 支付宝小程序appId
CONFIG_ALIPAY_MINI_APPID = 2021004129669493 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_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo7Ulswxr/3dJpX0RrPylV9rR3STdwGtcbV4ppNEiR0R37/9L/JpO/Y5gbEaaPn9S/xALzGKwI4N3/v1H2x566LJIQvzh5szCviy20NNGc+TbyxLJ9gLBxcsBDT8+o+a8CdaHbU/xvutZvLRa34D5pa6lcDCONC6q8flwWmXemUZ5widtW10F+4Niyufr+8hKkIpRfQMWYbsuA3EpXwjhztJglxPnjMN7r33vq9Pd5cU+KOOGuelXdB8aMwWcaXbffb7OKaTtZYnbUezJKbqZ+LtYgGF8+E5CKP7tzlEnZvQZwlPgS516EG3WRcCqjnEYO7RpG/eZ/0569d4TW8UPrwIDAQAB

View File

@ -11,7 +11,7 @@ CONFIG_ZYB_YB_SECRET=77F9610253B94263BCF8DBB61BF90D92
CONFIG_SMS_GATEWAY_DISABLED=false CONFIG_SMS_GATEWAY_DISABLED=false
## 支付宝小程序appId ## 支付宝小程序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_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= 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=

View File

@ -28,6 +28,15 @@ CONFIG_IM_IMAGE_ALLOWED_HOSTS=.my-imcloud.com,.im.qcloud.com,.myqcloud.com
`private-files` 不会通过 Express 静态目录公开。该目录不能在发布时被清理; `private-files` 不会通过 Express 静态目录公开。该目录不能在发布时被清理;
Docker 部署时必须挂载持久化卷。 Docker 部署时必须挂载持久化卷。
服务启动时会主动创建并检查该目录是否可读写。启动日志中应出现:
```text
腾讯 IM 图片归档目录已就绪,/opt/apps/private-files/im-chat
```
如果日志中出现 `EACCES`,需要给实际运行 Node/PM2 的 Linux 用户授予该目录的
读写权限。
归档成功后可通过下面的接口读取图片: 归档成功后可通过下面的接口读取图片:
```text ```text

View File

@ -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() { function getMaxFileSize() {
const configured = Number(process.env.CONFIG_IM_IMAGE_MAX_BYTES); const configured = Number(process.env.CONFIG_IM_IMAGE_MAX_BYTES);
return Number.isFinite(configured) && configured > 0 return Number.isFinite(configured) && configured > 0
@ -398,6 +406,7 @@ async function sendArchivedImage(req, res, db) {
module.exports = { module.exports = {
findOriginalImage, findOriginalImage,
prepareMessageForArchive, prepareMessageForArchive,
initializeStorage,
enqueueArchive, enqueueArchive,
resumePendingArchives, resumePendingArchives,
sendArchivedImage, sendArchivedImage,

View File

@ -107,6 +107,18 @@ describe("IM image archive", () => {
).toBe(path.resolve("/opt/apps", "private-files", "im-chat")); ).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 () => { test("downloads an image and marks the database record successful", async () => {
const jpeg = Buffer.concat([ const jpeg = Buffer.concat([
Buffer.from([0xff, 0xd8, 0xff, 0xe0]), Buffer.from([0xff, 0xd8, 0xff, 0xe0]),

View File

@ -201,6 +201,12 @@ const databaseReadyPromise = connectToMongoDB().catch((err) => {
return null; return null;
}); });
// 启动时立即创建并校验 IM 图片归档目录,避免等到首张图片下载时才暴露路径或权限问题。
const imageStorageReadyPromise = imImageArchive.initializeStorage().catch((err) => {
logger.error("腾讯 IM 图片归档目录初始化失败:", err);
return null;
});
app.get( app.get(
"/zyt/stats", "/zyt/stats",
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {
@ -727,6 +733,10 @@ trigger.initSchedule();
databaseReadyPromise.then(async (connected) => { databaseReadyPromise.then(async (connected) => {
if (!connected) return; if (!connected) return;
try { try {
const imageStorageRoot = await imageStorageReadyPromise;
if (!imageStorageRoot) {
throw new Error("腾讯 IM 图片归档目录不可用");
}
const internetHospitalDb = await getDatabase("Internet-hospital"); const internetHospitalDb = await getDatabase("Internet-hospital");
await consultTrigger( await consultTrigger(
{ {