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
@@ -318,6 +318,22 @@ aiimage:
jikip-balance-url: ${AIIMAGE_USER_SECRET_JIKIP_BALANCE_URL:https://api.jikip.com/find-balance}
jikip-plan-id: ${AIIMAGE_USER_SECRET_JIKIP_PLAN_ID:}
jikip-user-id: ${AIIMAGE_USER_SECRET_JIKIP_USER_ID:}
# 巡检发现欠费/密钥失效时是否推送站内通知(铃铛)
notify-enabled: ${AIIMAGE_USER_SECRET_NOTIFY_ENABLED:true}
# 站内通知(铃铛):任务失败扫描 + 下游服务健康探测
notification:
scan-enabled: ${AIIMAGE_NOTIFICATION_SCAN_ENABLED:true}
scan-interval-ms: ${AIIMAGE_NOTIFICATION_SCAN_INTERVAL_MS:300000}
scan-initial-delay-ms: ${AIIMAGE_NOTIFICATION_SCAN_INITIAL_DELAY_MS:120000}
task-scan-enabled: ${AIIMAGE_NOTIFICATION_TASK_SCAN_ENABLED:true}
task-failed-window-minutes: ${AIIMAGE_NOTIFICATION_TASK_FAILED_WINDOW_MINUTES:60}
task-scan-max-rows: ${AIIMAGE_NOTIFICATION_TASK_SCAN_MAX_ROWS:1000}
service-probe-enabled: ${AIIMAGE_NOTIFICATION_SERVICE_PROBE_ENABLED:true}
# 探测地址留空=跳过该项;部署时按主机拓扑配置(15126 品牌检测 / 18960 跟价任务 API
brand-service-url: ${AIIMAGE_NOTIFICATION_BRAND_SERVICE_URL:}
price-track-api-url: ${AIIMAGE_NOTIFICATION_PRICE_TRACK_API_URL:}
jikip-probe-enabled: ${AIIMAGE_NOTIFICATION_JIKIP_PROBE_ENABLED:true}
read-retention-days: ${AIIMAGE_NOTIFICATION_READ_RETENTION_DAYS:90}
security:
shop-credential-key: ${AIIMAGE_SHOP_CREDENTIAL_KEY:change-me-shop-credential-key}
internal-token: ${AIIMAGE_INTERNAL_TOKEN:}
@@ -328,6 +344,7 @@ aiimage:
cookie-name: ${AIIMAGE_AUTH_COOKIE_NAME:aiimage_token}
cookie-secure: ${AIIMAGE_AUTH_COOKIE_SECURE:false}
cookie-same-site: ${AIIMAGE_AUTH_COOKIE_SAME_SITE:Lax}
single-device-enabled: ${AIIMAGE_AUTH_SINGLE_DEVICE_ENABLED:true}
ziniao:
enabled: ${AIIMAGE_ZINIAO_ENABLED:false}
base-url: ${AIIMAGE_ZINIAO_BASE_URL:https://sbappstoreapi.ziniao.com/openapi-router}
@@ -0,0 +1,21 @@
-- V116: 站内通知(铃铛)
-- 桌面端用户与后台管理员共用的通知表,按 audience 区分接收端;
-- 通知来源:密钥巡检(欠费/失效)、任务失败扫描、下游服务健康探测。
-- dedupe_key 自带时间粒度(当天/当小时/任务号),插入前精确查重实现去重。
CREATE TABLE IF NOT EXISTS `biz_user_notification` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL COMMENT '接收者IDusers.id',
`audience` VARCHAR(16) NOT NULL DEFAULT 'user' COMMENT '接收端:user=桌面端/admin=后台',
`scene` VARCHAR(32) NOT NULL DEFAULT 'system' COMMENT '场景:secret_balance/secret_invalid/task_failed/service_down/system',
`level` VARCHAR(16) NOT NULL DEFAULT 'warning' COMMENT '级别:info/warning/error',
`title` VARCHAR(128) NOT NULL COMMENT '通知标题',
`content` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '通知内容',
`dedupe_key` VARCHAR(160) NOT NULL DEFAULT '' COMMENT '去重键(自带时间粒度,插入前精确查重)',
`read_at` DATETIME NULL COMMENT '已读时间(NULL=未读)',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_user_audience_read` (`user_id`, `audience`, `read_at`, `id`),
KEY `idx_dedupe` (`dedupe_key`),
KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='站内通知(桌面端用户 + 后台管理员)';
@@ -0,0 +1,9 @@
-- V118: 清空 users.machine,为单设备登录(互踢)做干净起点
-- 背景:users.machine 是早期「设备绑定」遗留列,绑定逻辑停用后一直没维护,
-- 存量值与用户当前设备普遍不一致。单设备登录上线后校验 machine 与
-- token 内签名 deviceId 是否一致,若不清空,历史残留会导致用户部署后
-- 立刻被判「已在其他设备登录」而下线(提示还是错的)。
-- 清空后:已登录用户不受影响,每人下一次登录时写入当前设备,自然进入新规则。
-- 幂等:只清非空值,重复执行无副作用。
UPDATE `users` SET `machine` = NULL WHERE `machine` IS NOT NULL;