feat(认证/通知): 单设备登录互踢 + 站内通知铃铛系统

- 单设备登录:登录成功即 last-login-wins 绑定 users.machine;非超管旧 token
  在下一次受保护请求抛 4011 下线,超管豁免;仅认 token 内签名 deviceId,不用请求头。
  前端两端接入踢下线跳转(?kicked=1 提示),V118 清空历史 machine
- 站内通知:新增 notification 模块(任务失败扫描 / 密钥欠费 / 下游服务探测三类来源),
  前后台铃铛组件 + 轮询;后台列表按主管数据范围(UserDataScopeSupport)过滤;V116 建表

均已于 2026-09-13 部署上线,此次补提交源码(此前仅存在于已部署 JAR/构建产物中)
This commit is contained in:
2026-09-13 23:08:22 +08:00
parent a0f6582914
commit b70557a077
53 changed files with 3982 additions and 101 deletions
@@ -71,6 +71,7 @@ public class AdminApiGuardFilter extends OncePerRequestFilter {
*/
private static final String[] SELF_SERVICE_PREFIXES = {
"/api/user-secrets",
"/api/notifications",
};
private final AdminAuthSupport adminAuthSupport;
@@ -0,0 +1,46 @@
package com.nanri.aiimage.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 站内通知(铃铛)扫描与探测配置:
* 任务失败扫描、下游服务健康探测、已读通知保留期。
*/
@Data
@ConfigurationProperties(prefix = "aiimage.notification")
public class NotificationProperties {
/** 扫描总开关:关闭后任务失败扫描与服务探测都不执行(应急降噪)。 */
private boolean scanEnabled = true;
/** 扫描间隔(毫秒),默认 5 分钟(任务失败聚合按小时去重,高频扫描不会重复提醒)。 */
private long scanIntervalMs = 5 * 60 * 1000L;
/** 首次执行延迟(毫秒),默认 2 分钟,避开启动阶段的数据库压力。 */
private long scanInitialDelayMs = 2 * 60 * 1000L;
/** 任务失败扫描开关。 */
private boolean taskScanEnabled = true;
/** 任务失败回看窗口(分钟):窗口内进入失败终态的任务参与聚合。 */
private int taskFailedWindowMinutes = 60;
/** 单轮任务扫描最多处理条数(超出下一轮继续,避免大表拖垮扫描)。 */
private int taskScanMaxRows = 1000;
/** 服务健康探测开关。 */
private boolean serviceProbeEnabled = true;
/** 品牌检测服务地址(主机A 15126,探测 /api/version);留空=跳过该项探测。 */
private String brandServiceUrl = "";
/** 跟价任务 API 地址(主机B 18960,探测根路径);留空=跳过该项探测。 */
private String priceTrackApiUrl = "";
/** jikip 代理接口探测开关(复用余额查询接口判活,使用 user-secret 的 jikip 配置)。 */
private boolean jikipProbeEnabled = true;
/** 已读通知保留天数(超期自动清理),默认 90 天。 */
private int readRetentionDays = 90;
}
@@ -4,6 +4,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties({OssProperties.class, TransientStorageProperties.class, StorageProperties.class, BrandProgressProperties.class, DeleteBrandProgressProperties.class, BrandCheckProperties.class, ZiniaoProperties.class, ModuleCleanupProperties.class, TaskPressureProperties.class, TaskImageCacheCleanupProperties.class, AppearancePatentProperties.class, SimilarAsinProperties.class, ImageVideoProperties.class, InstanceRoutingProperties.class, CapacityPlanProperties.class, UserSecretProperties.class})
@EnableConfigurationProperties({OssProperties.class, TransientStorageProperties.class, StorageProperties.class, BrandProgressProperties.class, DeleteBrandProgressProperties.class, BrandCheckProperties.class, ZiniaoProperties.class, ModuleCleanupProperties.class, TaskPressureProperties.class, TaskImageCacheCleanupProperties.class, AppearancePatentProperties.class, SimilarAsinProperties.class, ImageVideoProperties.class, InstanceRoutingProperties.class, CapacityPlanProperties.class, UserSecretProperties.class, NotificationProperties.class})
public class PropertiesConfig {
}
@@ -36,4 +36,10 @@ public class UserSecretProperties {
/** jikip 用户 ID(余量查询参数)。 */
private String jikipUserId = "";
/**
* 巡检发现欠费/密钥失效时是否推送站内通知(桌面端用户 + 后台管理员);
* 关闭后巡检只更新检测状态、不发通知(应急降噪开关)。
*/
private boolean notifyEnabled = true;
}