设置(仅管理员) - 可配置是否启用、通知时间(默认 01:00 Asia/Shanghai)、多名通知用户 - 收件人在后端规范化:去空、去重、限长限量,配置持久化到 system_config - 接口不返回通知中心密钥与对象存储密钥,只给是否已配置与脱敏预览 定时任务 - 单实例锁(MySQL GET_LOCK),按配置时间触发,命中后 30 分钟内失败最多补跑 3 次 - 服务端签发 15 分钟短时效 JWT 作为专用身份,令牌只写入浏览器 localStorage - 等数据真正渲染完成(有数据行、无加载中、无错误提示)才截图,展开滚动容器后整页截图 - 对象键形如 daily-inventory/2026-09-25/daily-inventory-20260925-010000.png - 截图上传成功后才逐用户发通知,单人失败不影响他人;正式推送按「业务日期+收件人」幂等 - 立即测试走同一链路但幂等键带随机数,不影响次日正式推送 - 每次执行记录时间、截图地址、通知对象、成功状态与失败原因 测试 - 覆盖收件人去重、通知时间校验、幂等键、业务日期与对象键、重试退避(17 项)
82 lines
2.7 KiB
JavaScript
82 lines
2.7 KiB
JavaScript
'use strict';
|
|
|
|
// 每日进销存 · 定时截图通知(管理员专用)
|
|
//
|
|
// 安全约定:
|
|
// · 全部接口要求登录且 role === 'admin'
|
|
// · 响应里只有「是否已配置」与脱敏预览,绝不返回通知中心密钥 / 对象存储密钥
|
|
// · 收件人名单由后端校验与规范化,前端只能提交姓名文本
|
|
|
|
const express = require('express');
|
|
const {
|
|
getDailyInventoryNotificationAdminConfig,
|
|
listRunRecords,
|
|
saveDailyInventoryNotificationConfig,
|
|
} = require('../services/dailyInventoryNotificationSettings');
|
|
const {
|
|
runDailyInventoryNotificationJob,
|
|
} = require('../jobs/dailyInventoryNotificationJob');
|
|
|
|
const router = express.Router();
|
|
|
|
function sendError(res, error, fallback) {
|
|
const status = Number(error?.statusCode) || 500;
|
|
console.error('[DailyInventoryNotification]', {
|
|
message: error?.message || fallback,
|
|
code: error?.code || null,
|
|
status,
|
|
});
|
|
res.status(status).json({ success: false, error: error.message || fallback });
|
|
}
|
|
|
|
/** 仅管理员可查看与修改通知设置、执行立即测试 */
|
|
function requireAdmin(req, res, next) {
|
|
if (req?.user?.role !== 'admin') {
|
|
return res.status(403).json({ success: false, error: '仅管理员可操作通知设置' });
|
|
}
|
|
return next();
|
|
}
|
|
|
|
router.use(requireAdmin);
|
|
|
|
// 读取通知设置(含通知中心 / 对象存储的配置状态与最近执行记录)
|
|
router.get('/config', async (_req, res) => {
|
|
try {
|
|
res.json({ success: true, data: await getDailyInventoryNotificationAdminConfig() });
|
|
} catch (error) {
|
|
sendError(res, error, '加载每日进销存通知设置失败');
|
|
}
|
|
});
|
|
|
|
// 保存通知设置
|
|
router.put('/config', async (req, res) => {
|
|
try {
|
|
const saved = await saveDailyInventoryNotificationConfig(req.body || {});
|
|
res.json({ success: true, data: { ...saved, admin: await getDailyInventoryNotificationAdminConfig() } });
|
|
} catch (error) {
|
|
sendError(res, error, '保存每日进销存通知设置失败');
|
|
}
|
|
});
|
|
|
|
// 立即测试:真实走一次截图 + 上传 + 通知,但不写入正式幂等记录
|
|
router.post('/test', async (_req, res) => {
|
|
try {
|
|
const result = await runDailyInventoryNotificationJob({ mode: 'test' });
|
|
res.json({ success: result?.success === true, data: result });
|
|
} catch (error) {
|
|
sendError(res, error, '执行每日进销存通知测试失败');
|
|
}
|
|
});
|
|
|
|
// 最近执行记录(管理员排障用)
|
|
router.get('/runs', async (req, res) => {
|
|
try {
|
|
const limit = Math.min(Math.max(Number(req.query?.limit) || 10, 1), 50);
|
|
res.json({ success: true, data: await listRunRecords(limit) });
|
|
} catch (error) {
|
|
sendError(res, error, '加载每日进销存通知执行记录失败');
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|