diff --git a/_config/sql/update-datas.sql b/_config/sql/update-datas.sql
new file mode 100644
index 000000000..0d36f9a02
--- /dev/null
+++ b/_config/sql/update-datas.sql
@@ -0,0 +1,15 @@
+-- 通知模块菜单数据(增量, 幂等)
+-- 执行顺序: update-tables.sql -> update-datas.sql
+-- 字段顺序同 iam_perm_menu: id, pid, menu_code, client_code, name, title_cn, title_en,
+-- i18n_key, icon, hidden, hide_children_menu, component, path, redirect, sort_no,
+-- root, keep_alive, affix_tab, creator, last_modifier, version, deleted, menu_type,
+-- active_icon, badge, badge_type, badge_variants, iframe_src, link, create_time, last_modified_time
+
+-- 公告通知菜单(挂在系统管理 id=3 下, 与配置/日志/权限同级)
+INSERT INTO "iam_perm_menu" VALUES (
+ 309, 3, 'system:notify', 'admin', 'SystemNotify', '公告通知', 'Notification',
+ 'menu.system.notify', 'lucide:bell', false, false,
+ '/system/notify/notice/NoticeList', '/system/notify', NULL, 20,
+ false, true, false, 1, 1, 0, false, 'menu', NULL, NULL, NULL, NULL, NULL,
+ '2026-06-24 16:00:00+00', '2026-06-24 16:00:00+00'
+) ON CONFLICT (id) DO NOTHING;
diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql
index 1d156b840..8b0f69996 100644
--- a/_config/sql/update-tables.sql
+++ b/_config/sql/update-tables.sql
@@ -1,24 +1,101 @@
--- 第三方平台登录配置表: 合并前端地址字段, 移除 state 超时与作用域字段
--- 将 frontend_base_url + callback_path 合并为 frontend_callback_url,
--- state_timeout 不再由平台配置维护, 改用系统默认常量(300秒),
--- scopes 字段从未参与授权请求, 各平台 scope 已硬编码, 移除该死字段
-ALTER TABLE iam_social_config ADD COLUMN IF NOT EXISTS frontend_callback_url VARCHAR(384);
--- 迁移已有数据(拼接基础地址与回调路径)
-UPDATE iam_social_config
- SET frontend_callback_url = CONCAT(COALESCE(frontend_base_url, ''), COALESCE(callback_path, ''))
- WHERE frontend_callback_url IS NULL AND (frontend_base_url IS NOT NULL OR callback_path IS NOT NULL);
-ALTER TABLE iam_social_config DROP COLUMN IF EXISTS frontend_base_url;
-ALTER TABLE iam_social_config DROP COLUMN IF EXISTS callback_path;
-ALTER TABLE iam_social_config DROP COLUMN IF EXISTS state_timeout;
-ALTER TABLE iam_social_config DROP COLUMN IF EXISTS scopes;
-ALTER TABLE iam_social_config DROP COLUMN IF EXISTS name;
-ALTER TABLE iam_social_config DROP COLUMN IF EXISTS frontend_callback_url;
+-- ===================================
+-- 通知模块: 公告(广播) + 已读记录 + 个人消息(预留)
+-- ===================================
--- client_secret 改为加密存储(AES-256-GCM, 由 DataEncryptTypeHandler 透明加解密)
--- 字段长度 VARCHAR(256) 可容纳密文(appSecret 多为 32~64 字符, 密文 < 200 字符), 无需调整
--- 开发阶段无历史明文数据, 跳过迁移; 生产环境启用前需写一次性逻辑加密历史明文
-COMMENT ON COLUMN iam_social_config.client_secret IS '客户端密钥(加密存储)';
+-- 公告主体表(广播型通知, 1条公告 N人可见)
+CREATE TABLE IF NOT EXISTS notify_notice (
+ id bigint NOT NULL,
+ title varchar(128) NOT NULL,
+ content text NOT NULL,
+ severity varchar(16) NOT NULL DEFAULT 'normal',
+ is_top boolean NOT NULL DEFAULT false,
+ effective_time timestamptz(6),
+ expire_time timestamptz(6),
+ status varchar(16) NOT NULL DEFAULT 'draft',
+ creator bigint,
+ create_time timestamptz(6),
+ last_modifier bigint,
+ last_modified_time timestamptz(6),
+ version int NOT NULL DEFAULT 0,
+ deleted boolean NOT NULL DEFAULT false,
+ CONSTRAINT notify_notice_pkey PRIMARY KEY (id)
+);
--- 回调地址不再由社交配置维护, 改由端点配置(PlatformUrlConfig)的 baseUrl 自动生成
--- 实际回调地址为 {adminBaseUrl|merchantBaseUrl}/auth/oauth-callback/{source}
-ALTER TABLE iam_social_config DROP COLUMN IF EXISTS redirect_uri;
+COMMENT ON TABLE notify_notice IS '公告通知(广播型, 1条对多用户可见)';
+COMMENT ON COLUMN notify_notice.id IS '主键';
+COMMENT ON COLUMN notify_notice.title IS '标题';
+COMMENT ON COLUMN notify_notice.content IS '正文(Markdown原文)';
+COMMENT ON COLUMN notify_notice.severity IS '重要程度(normal普通/important重要)';
+COMMENT ON COLUMN notify_notice.is_top IS '是否置顶';
+COMMENT ON COLUMN notify_notice.effective_time IS '生效时间(为空则立即生效)';
+COMMENT ON COLUMN notify_notice.expire_time IS '过期时间(为空则永久有效)';
+COMMENT ON COLUMN notify_notice.status IS '状态(draft草稿/published发布/offline下线)';
+COMMENT ON COLUMN notify_notice.creator IS '创建人ID';
+COMMENT ON COLUMN notify_notice.create_time IS '创建时间';
+COMMENT ON COLUMN notify_notice.last_modifier IS '最后修改人ID';
+COMMENT ON COLUMN notify_notice.last_modified_time IS '最后修改时间';
+COMMENT ON COLUMN notify_notice.version IS '版本号(乐观锁)';
+COMMENT ON COLUMN notify_notice.deleted IS '逻辑删除标志';
+
+CREATE INDEX IF NOT EXISTS idx_notify_notice_status ON notify_notice (status, deleted, effective_time, expire_time);
+
+-- 公告已读记录(用户 x 公告, 记录已读/忽略状态)
+CREATE TABLE IF NOT EXISTS notify_notice_read (
+ id bigint NOT NULL,
+ user_id bigint NOT NULL,
+ notice_id bigint NOT NULL,
+ read_time timestamptz(6),
+ is_ignored boolean NOT NULL DEFAULT false,
+ creator bigint,
+ create_time timestamptz(6),
+ CONSTRAINT notify_notice_read_pkey PRIMARY KEY (id),
+ CONSTRAINT uk_notify_notice_read UNIQUE (user_id, notice_id)
+);
+
+COMMENT ON TABLE notify_notice_read IS '公告已读记录(用户x公告)';
+COMMENT ON COLUMN notify_notice_read.id IS '主键';
+COMMENT ON COLUMN notify_notice_read.user_id IS '用户ID';
+COMMENT ON COLUMN notify_notice_read.notice_id IS '公告ID';
+COMMENT ON COLUMN notify_notice_read.read_time IS '阅读时间';
+COMMENT ON COLUMN notify_notice_read.is_ignored IS '是否忽略(用户主动隐藏该公告)';
+COMMENT ON COLUMN notify_notice_read.creator IS '创建人ID';
+COMMENT ON COLUMN notify_notice_read.create_time IS '创建时间';
+
+CREATE INDEX IF NOT EXISTS idx_notify_notice_read_user ON notify_notice_read (user_id);
+
+-- 个人消息表(定向通知, 本次预留建表, 暂不接入业务)
+CREATE TABLE IF NOT EXISTS notify_message (
+ id bigint NOT NULL,
+ user_id bigint NOT NULL,
+ title varchar(128) NOT NULL,
+ content varchar(1024),
+ source varchar(32),
+ link varchar(255),
+ extra text,
+ is_read boolean NOT NULL DEFAULT false,
+ creator bigint,
+ create_time timestamptz(6),
+ last_modifier bigint,
+ last_modified_time timestamptz(6),
+ version int NOT NULL DEFAULT 0,
+ deleted boolean NOT NULL DEFAULT false,
+ CONSTRAINT notify_message_pkey PRIMARY KEY (id)
+);
+
+COMMENT ON TABLE notify_message IS '个人消息(定向通知, 1条对1用户)';
+COMMENT ON COLUMN notify_message.id IS '主键';
+COMMENT ON COLUMN notify_message.user_id IS '接收用户ID';
+COMMENT ON COLUMN notify_message.title IS '标题';
+COMMENT ON COLUMN notify_message.content IS '正文内容';
+COMMENT ON COLUMN notify_message.source IS '业务来源(预留, 如TRADE/REFUND等)';
+COMMENT ON COLUMN notify_message.link IS '跳转链接(内部路由或完整http外链)';
+COMMENT ON COLUMN notify_message.extra IS '跳转附加参数(JSON字符串)';
+COMMENT ON COLUMN notify_message.is_read IS '是否已读';
+COMMENT ON COLUMN notify_message.creator IS '创建人ID';
+COMMENT ON COLUMN notify_message.create_time IS '创建时间';
+COMMENT ON COLUMN notify_message.last_modifier IS '最后修改人ID';
+COMMENT ON COLUMN notify_message.last_modified_time IS '最后修改时间';
+COMMENT ON COLUMN notify_message.version IS '版本号(乐观锁)';
+COMMENT ON COLUMN notify_message.deleted IS '逻辑删除标志';
+
+CREATE INDEX IF NOT EXISTS idx_notify_message_user ON notify_message (user_id, deleted, is_read);
diff --git a/daxpay-payment/daxpay-payment-admin/pom.xml b/daxpay-payment/daxpay-payment-admin/pom.xml
index af0961c02..c5f5a17c2 100644
--- a/daxpay-payment/daxpay-payment-admin/pom.xml
+++ b/daxpay-payment/daxpay-payment-admin/pom.xml
@@ -70,6 +70,12 @@
service-baseapi
${project.version}
+
+
+ cn.daxpay.open
+ service-notify
+ ${project.version}
+
cn.daxpay.open
diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notify_severity.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notify_severity.json
new file mode 100644
index 000000000..01bf3cc8c
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notify_severity.json
@@ -0,0 +1,4 @@
+{
+ "normal": "Normal",
+ "important": "Important"
+}
diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notify_status.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notify_status.json
new file mode 100644
index 000000000..2c7ba2e0f
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notify_status.json
@@ -0,0 +1,5 @@
+{
+ "draft": "Draft",
+ "published": "Published",
+ "offline": "Offline"
+}
diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notify_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notify_type.json
new file mode 100644
index 000000000..0c61adc58
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notify_type.json
@@ -0,0 +1,4 @@
+{
+ "notice": "Notice",
+ "message": "Message"
+}
diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/notify/notice.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/notify/notice.json
new file mode 100644
index 000000000..e4b22f606
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/notify/notice.json
@@ -0,0 +1,3 @@
+{
+ "notExist": "Notice not found"
+}
diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notify_severity.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notify_severity.json
new file mode 100644
index 000000000..2a97d193e
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notify_severity.json
@@ -0,0 +1,4 @@
+{
+ "normal": "普通",
+ "important": "重要"
+}
diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notify_status.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notify_status.json
new file mode 100644
index 000000000..25f9a3f9d
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notify_status.json
@@ -0,0 +1,5 @@
+{
+ "draft": "草稿",
+ "published": "发布",
+ "offline": "下线"
+}
diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notify_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notify_type.json
new file mode 100644
index 000000000..3c22f94a2
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notify_type.json
@@ -0,0 +1,4 @@
+{
+ "notice": "公告",
+ "message": "消息"
+}
diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/notify/notice.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/notify/notice.json
new file mode 100644
index 000000000..c8030c723
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/notify/notice.json
@@ -0,0 +1,3 @@
+{
+ "notExist": "公告不存在"
+}
diff --git a/daxpay-platform/daxpay-platform-service/pom.xml b/daxpay-platform/daxpay-platform-service/pom.xml
index bc7405fb2..2ec17665e 100644
--- a/daxpay-platform/daxpay-platform-service/pom.xml
+++ b/daxpay-platform/daxpay-platform-service/pom.xml
@@ -12,6 +12,7 @@
service-baseapi
service-iam
service-system
+ service-notify
4.0.0
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/pom.xml b/daxpay-platform/daxpay-platform-service/service-notify/pom.xml
new file mode 100644
index 000000000..1829081a8
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/pom.xml
@@ -0,0 +1,83 @@
+
+
+ 4.0.0
+
+ cn.daxpay.open
+ daxpay-platform-service
+ 4.0.0-SNAPSHOT
+
+
+ service-notify
+ 站内通知(公告 + 个人消息)
+
+
+
+
+ cn.daxpay.open
+ common-translate
+ ${project.version}
+
+
+
+ cn.daxpay.open
+ common-i18n
+ ${project.version}
+
+
+
+ cn.daxpay.open
+ common-mybatis-plus
+ ${project.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ cn.daxpay.open
+ common-json
+ ${project.version}
+
+
+
+ cn.daxpay.open
+ common-spring
+ ${project.version}
+
+
+
+ cn.daxpay.open
+ common-redis
+ ${project.version}
+
+
+
+ cn.hutool
+ hutool-cache
+ ${hutool.version}
+
+
+
+ cn.daxpay.open
+ capability-auth
+ ${project.version}
+
+
+
+ cn.daxpay.open
+ capability-audit-log
+ ${project.version}
+
+
+
+ cn.daxpay.open
+ common-config
+ ${project.version}
+
+
+
+
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/NotifyApplication.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/NotifyApplication.java
new file mode 100644
index 000000000..cfd122d63
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/NotifyApplication.java
@@ -0,0 +1,16 @@
+package cn.daxpay.open.platform.notify;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/// 通知模块(公告 + 个人消息)
+@AutoConfiguration
+@ComponentScan
+@EnableScheduling
+@MapperScan(annotationClass = Mapper.class)
+public class NotifyApplication {
+
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/controller/notice/NotifyNoticeController.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/controller/notice/NotifyNoticeController.java
new file mode 100644
index 000000000..4f471cac2
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/controller/notice/NotifyNoticeController.java
@@ -0,0 +1,91 @@
+package cn.daxpay.open.platform.notify.controller.notice;
+
+import cn.daxpay.open.platform.core.annotation.PermCode;
+import cn.daxpay.open.platform.core.rest.Res;
+import cn.daxpay.open.platform.core.rest.param.PageParam;
+import cn.daxpay.open.platform.core.rest.result.PageResult;
+import cn.daxpay.open.platform.core.rest.result.Result;
+import cn.daxpay.open.platform.core.util.ValidationUtil;
+import cn.daxpay.open.platform.core.validation.ValidationGroup;
+import cn.daxpay.open.platform.notify.param.notice.NotifyNoticeParam;
+import cn.daxpay.open.platform.notify.param.notice.NotifyNoticeQuery;
+import cn.daxpay.open.platform.notify.result.notice.NotifyNoticeResult;
+import cn.daxpay.open.platform.notify.service.notice.NotifyNoticeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/// 公告管理(管理端)
+@PermCode(menuCode = "system:notify")
+@Validated
+@Tag(name = "公告管理")
+@RestController
+@RequestMapping("/notify/notice")
+@RequiredArgsConstructor
+public class NotifyNoticeController {
+
+ private final NotifyNoticeService noticeService;
+
+ @PermCode(code = "notice:add", nameCn = "公告管理", nameEn = "Notice Manage")
+ @Operation(summary = "新建公告")
+ @PostMapping("/add")
+ public Result add(@RequestBody NotifyNoticeParam param) {
+ ValidationUtil.validateParam(param, ValidationGroup.add.class);
+ noticeService.add(param);
+ return Res.ok();
+ }
+
+ @PermCode(code = "notice:add", nameCn = "公告管理", nameEn = "Notice Manage")
+ @Operation(summary = "编辑公告")
+ @PostMapping("/update")
+ public Result update(@RequestBody NotifyNoticeParam param) {
+ ValidationUtil.validateParam(param, ValidationGroup.edit.class);
+ noticeService.update(param);
+ return Res.ok();
+ }
+
+ @PermCode(code = "notice:add", nameCn = "公告管理", nameEn = "Notice Manage")
+ @Operation(summary = "删除公告")
+ @PostMapping("/delete")
+ public Result delete(@NotNull(message = "{validation.field.id.notNull}") Long id) {
+ noticeService.delete(id);
+ return Res.ok();
+ }
+
+ @PermCode(code = "notice:publish", nameCn = "公告发布", nameEn = "Notice Publish")
+ @Operation(summary = "发布公告")
+ @PostMapping("/publish")
+ public Result publish(@NotNull(message = "{validation.field.id.notNull}") Long id) {
+ noticeService.publish(id);
+ return Res.ok();
+ }
+
+ @PermCode(code = "notice:publish", nameCn = "公告发布", nameEn = "Notice Publish")
+ @Operation(summary = "下线公告")
+ @PostMapping("/offline")
+ public Result offline(@NotNull(message = "{validation.field.id.notNull}") Long id) {
+ noticeService.offline(id);
+ return Res.ok();
+ }
+
+ @PermCode(code = "notice:view", nameCn = "公告查看", nameEn = "Notice View")
+ @Operation(summary = "公告详情")
+ @GetMapping("/get")
+ public Result findById(@NotNull(message = "{validation.field.id.notNull}") Long id) {
+ return Res.ok(noticeService.findById(id));
+ }
+
+ @PermCode(code = "notice:view", nameCn = "公告查看", nameEn = "Notice View")
+ @Operation(summary = "公告分页")
+ @GetMapping("/page")
+ public Result> page(PageParam pageParam, NotifyNoticeQuery query) {
+ return Res.ok(noticeService.page(pageParam, query));
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/controller/notice/NotifyUserController.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/controller/notice/NotifyUserController.java
new file mode 100644
index 000000000..7cbf8712c
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/controller/notice/NotifyUserController.java
@@ -0,0 +1,83 @@
+package cn.daxpay.open.platform.notify.controller.notice;
+
+import cn.daxpay.open.platform.capability.auth.util.SecurityUtil;
+import cn.daxpay.open.platform.core.rest.Res;
+import cn.daxpay.open.platform.core.rest.result.Result;
+import cn.daxpay.open.platform.notify.param.notice.NotifyUserNoticeQuery;
+import cn.daxpay.open.platform.notify.result.notice.NotifyNoticeBriefResult;
+import cn.daxpay.open.platform.notify.result.notice.NotifyUnreadCountResult;
+import cn.daxpay.open.platform.notify.service.notice.NotifySseService;
+import cn.daxpay.open.platform.notify.service.notice.NotifyUserNoticeService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.util.List;
+
+/// 站内通知(用户端, 登录即可访问)
+@Validated
+@Tag(name = "站内通知")
+@RestController
+@RequestMapping("/notify/user")
+@RequiredArgsConstructor
+public class NotifyUserController {
+
+ private final NotifyUserNoticeService userNoticeService;
+
+ private final NotifySseService sseService;
+
+ @Operation(summary = "未读数")
+ @GetMapping("/unread-count")
+ public Result unreadCount() {
+ return Res.ok(userNoticeService.unreadCount());
+ }
+
+ @Operation(summary = "铃铛通知列表")
+ @GetMapping("/page")
+ public Result> page(NotifyUserNoticeQuery query) {
+ return Res.ok(userNoticeService.list(query));
+ }
+
+ @Operation(summary = "标记单条已读")
+ @PostMapping("/read")
+ public Result read(@NotBlank(message = "{validation.field.type.notBlank}") String type,
+ @NotNull(message = "{validation.field.id.notNull}") Long id) {
+ userNoticeService.markRead(type, id);
+ return Res.ok();
+ }
+
+ @Operation(summary = "全部已读(清空)")
+ @PostMapping("/read-all")
+ public Result readAll() {
+ userNoticeService.readAll();
+ return Res.ok();
+ }
+
+ @Operation(summary = "忽略(隐藏)")
+ @PostMapping("/ignore")
+ public Result ignore(@NotBlank(message = "{validation.field.type.notBlank}") String type,
+ @NotNull(message = "{validation.field.id.notNull}") Long id) {
+ userNoticeService.ignore(type, id);
+ return Res.ok();
+ }
+
+ /// 建立实时推送连接(Server-Sent Events)
+ ///
+ /// 浏览器 EventSource 同源连接, 依赖 Sa-Token 会话识别(同源 cookie);
+ /// 收到推送时前端刷新未读数与铃铛列表.
+ @Operation(summary = "建立实时推送连接(SSE)")
+ @GetMapping(value = "/sse/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ public SseEmitter sseConnect() {
+ Long userId = SecurityUtil.getUserId();
+ return sseService.connect(userId);
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/convert/notice/NotifyNoticeConvert.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/convert/notice/NotifyNoticeConvert.java
new file mode 100644
index 000000000..9f1449bf0
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/convert/notice/NotifyNoticeConvert.java
@@ -0,0 +1,44 @@
+package cn.daxpay.open.platform.notify.convert.notice;
+
+import cn.daxpay.open.platform.notify.entity.message.NotifyMessage;
+import cn.daxpay.open.platform.notify.entity.notice.NotifyNotice;
+import cn.daxpay.open.platform.notify.param.notice.NotifyNoticeParam;
+import cn.daxpay.open.platform.notify.result.notice.NotifyNoticeBriefResult;
+import cn.daxpay.open.platform.notify.result.notice.NotifyNoticeResult;
+import org.mapstruct.BeanMapping;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.MappingTarget;
+import org.mapstruct.NullValuePropertyMappingStrategy;
+import org.mapstruct.factory.Mappers;
+
+/// 通知转换
+@Mapper
+public interface NotifyNoticeConvert {
+
+ NotifyNoticeConvert CONVERT = Mappers.getMapper(NotifyNoticeConvert.class);
+
+ /// 参数转实体
+ NotifyNotice convert(NotifyNoticeParam in);
+
+ /// 实体转详情结果
+ NotifyNoticeResult convert(NotifyNotice in);
+
+ /// 复制属性(更新时, 忽略空值)
+ @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
+ void copy(NotifyNoticeParam param, @MappingTarget NotifyNotice entity);
+
+ /// 公告转铃铛摘要(类型固定为公告, 内容摘要来自正文, 已读状态由服务层填充)
+ @Mapping(target = "type", constant = "notice")
+ @Mapping(target = "message", source = "content")
+ @Mapping(target = "isRead", ignore = true)
+ @Mapping(target = "link", ignore = true)
+ NotifyNoticeBriefResult toBrief(NotifyNotice in);
+
+ /// 个人消息转铃铛摘要(类型固定为个人消息, 内容摘要来自正文)
+ @Mapping(target = "type", constant = "message")
+ @Mapping(target = "message", source = "content")
+ @Mapping(target = "severity", ignore = true)
+ @Mapping(target = "isTop", ignore = true)
+ NotifyNoticeBriefResult convert(NotifyMessage in);
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/message/NotifyMessageManager.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/message/NotifyMessageManager.java
new file mode 100644
index 000000000..79497b616
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/message/NotifyMessageManager.java
@@ -0,0 +1,22 @@
+package cn.daxpay.open.platform.notify.dao.message;
+
+import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
+import cn.daxpay.open.platform.notify.entity.message.NotifyMessage;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/// 个人消息(预留)
+@Repository
+@AllArgsConstructor
+public class NotifyMessageManager extends BaseManager {
+
+ /// 查询用户未读个人消息
+ public List findAllByUserAndUnread(Long userId) {
+ return lambdaQuery()
+ .eq(NotifyMessage::getUserId, userId)
+ .eq(NotifyMessage::getIsRead, false)
+ .list();
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/message/NotifyMessageMapper.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/message/NotifyMessageMapper.java
new file mode 100644
index 000000000..12679e514
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/message/NotifyMessageMapper.java
@@ -0,0 +1,11 @@
+package cn.daxpay.open.platform.notify.dao.message;
+
+import cn.daxpay.open.platform.notify.entity.message.NotifyMessage;
+import com.github.yulichang.base.MPJBaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/// 个人消息(预留)
+@Mapper
+public interface NotifyMessageMapper extends MPJBaseMapper {
+
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/notice/NotifyNoticeManager.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/notice/NotifyNoticeManager.java
new file mode 100644
index 000000000..5bad34f92
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/notice/NotifyNoticeManager.java
@@ -0,0 +1,30 @@
+package cn.daxpay.open.platform.notify.dao.notice;
+
+import cn.daxpay.open.platform.common.mybatisplus.base.MpIdEntity;
+import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
+import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil;
+import cn.daxpay.open.platform.core.rest.param.PageParam;
+import cn.daxpay.open.platform.notify.entity.notice.NotifyNotice;
+import cn.daxpay.open.platform.notify.param.notice.NotifyNoticeQuery;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+/// 公告
+@Repository
+@AllArgsConstructor
+public class NotifyNoticeManager extends BaseManager {
+
+ /// 管理端分页查询
+ public Page page(PageParam pageParam, NotifyNoticeQuery query) {
+ Page mpPage = MpUtil.getMpPage(pageParam);
+ return lambdaQuery()
+ .like(StrUtil.isNotBlank(query.getTitle()), NotifyNotice::getTitle, query.getTitle())
+ .eq(StrUtil.isNotBlank(query.getStatus()), NotifyNotice::getStatus, query.getStatus())
+ .eq(StrUtil.isNotBlank(query.getSeverity()), NotifyNotice::getSeverity, query.getSeverity())
+ .orderByDesc(NotifyNotice::getIsTop)
+ .orderByDesc(MpIdEntity::getId)
+ .page(mpPage);
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/notice/NotifyNoticeMapper.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/notice/NotifyNoticeMapper.java
new file mode 100644
index 000000000..d89cd703f
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/notice/NotifyNoticeMapper.java
@@ -0,0 +1,11 @@
+package cn.daxpay.open.platform.notify.dao.notice;
+
+import cn.daxpay.open.platform.notify.entity.notice.NotifyNotice;
+import com.github.yulichang.base.MPJBaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/// 公告
+@Mapper
+public interface NotifyNoticeMapper extends MPJBaseMapper {
+
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/notice/NotifyNoticeReadManager.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/notice/NotifyNoticeReadManager.java
new file mode 100644
index 000000000..4333d2790
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/notice/NotifyNoticeReadManager.java
@@ -0,0 +1,38 @@
+package cn.daxpay.open.platform.notify.dao.notice;
+
+import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
+import cn.daxpay.open.platform.notify.entity.notice.NotifyNoticeRead;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+/// 公告已读记录
+@Repository
+@AllArgsConstructor
+public class NotifyNoticeReadManager extends BaseManager {
+
+ /// 查询用户对某条公告的已读记录
+ public Optional findByUserAndNotice(Long userId, Long noticeId) {
+ return lambdaQuery()
+ .eq(NotifyNoticeRead::getUserId, userId)
+ .eq(NotifyNoticeRead::getNoticeId, noticeId)
+ .oneOpt();
+ }
+
+ /// 查询用户所有未忽略的已读记录
+ public List findAllByUserAndNotIgnored(Long userId) {
+ return lambdaQuery()
+ .eq(NotifyNoticeRead::getUserId, userId)
+ .eq(NotifyNoticeRead::getIsIgnored, false)
+ .list();
+ }
+
+ /// 查询用户所有已读记录(含忽略)
+ public List findAllByUser(Long userId) {
+ return lambdaQuery()
+ .eq(NotifyNoticeRead::getUserId, userId)
+ .list();
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/notice/NotifyNoticeReadMapper.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/notice/NotifyNoticeReadMapper.java
new file mode 100644
index 000000000..5e97e6bf2
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/dao/notice/NotifyNoticeReadMapper.java
@@ -0,0 +1,11 @@
+package cn.daxpay.open.platform.notify.dao.notice;
+
+import cn.daxpay.open.platform.notify.entity.notice.NotifyNoticeRead;
+import com.github.yulichang.base.MPJBaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/// 公告已读记录
+@Mapper
+public interface NotifyNoticeReadMapper extends MPJBaseMapper {
+
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/entity/message/NotifyMessage.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/entity/message/NotifyMessage.java
new file mode 100644
index 000000000..a0c97e885
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/entity/message/NotifyMessage.java
@@ -0,0 +1,46 @@
+package cn.daxpay.open.platform.notify.entity.message;
+
+import cn.daxpay.open.platform.common.mybatisplus.base.MpBaseEntity;
+import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
+import cn.daxpay.open.platform.notify.convert.notice.NotifyNoticeConvert;
+import cn.daxpay.open.platform.notify.result.notice.NotifyNoticeBriefResult;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+/// 个人消息(定向通知, 本次预留建表, 暂不接入业务)
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@TableName("notify_message")
+public class NotifyMessage extends MpBaseEntity implements ToResult {
+
+ /// 接收用户ID
+ private Long userId;
+
+ /// 标题
+ private String title;
+
+ /// 正文内容
+ private String content;
+
+ /// 业务来源(预留)
+ private String source;
+
+ /// 跳转链接(内部路由或完整http外链)
+ private String link;
+
+ /// 跳转附加参数(JSON字符串)
+ private String extra;
+
+ /// 是否已读
+ @TableField("is_read")
+ private Boolean isRead;
+
+ @Override
+ public NotifyNoticeBriefResult toResult() {
+ return NotifyNoticeConvert.CONVERT.convert(this);
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/entity/notice/NotifyNotice.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/entity/notice/NotifyNotice.java
new file mode 100644
index 000000000..5af7ed1ba
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/entity/notice/NotifyNotice.java
@@ -0,0 +1,48 @@
+package cn.daxpay.open.platform.notify.entity.notice;
+
+import cn.daxpay.open.platform.common.mybatisplus.base.MpBaseEntity;
+import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
+import cn.daxpay.open.platform.notify.convert.notice.NotifyNoticeConvert;
+import cn.daxpay.open.platform.notify.result.notice.NotifyNoticeResult;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.time.OffsetDateTime;
+
+/// 公告通知(广播型)
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@TableName("notify_notice")
+public class NotifyNotice extends MpBaseEntity implements ToResult {
+
+ /// 标题
+ private String title;
+
+ /// 正文(Markdown原文)
+ private String content;
+
+ /// 重要程度(normal普通/important重要)
+ private String severity;
+
+ /// 是否置顶
+ @TableField("is_top")
+ private Boolean isTop;
+
+ /// 生效时间(为空则立即生效)
+ private OffsetDateTime effectiveTime;
+
+ /// 过期时间(为空则永久有效)
+ private OffsetDateTime expireTime;
+
+ /// 状态(draft草稿/published发布/offline下线)
+ private String status;
+
+ @Override
+ public NotifyNoticeResult toResult() {
+ return NotifyNoticeConvert.CONVERT.convert(this);
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/entity/notice/NotifyNoticeRead.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/entity/notice/NotifyNoticeRead.java
new file mode 100644
index 000000000..9035b1bed
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/entity/notice/NotifyNoticeRead.java
@@ -0,0 +1,31 @@
+package cn.daxpay.open.platform.notify.entity.notice;
+
+import cn.daxpay.open.platform.common.mybatisplus.base.MpCreateEntity;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.time.OffsetDateTime;
+
+/// 公告已读记录(用户 x 公告)
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@TableName("notify_notice_read")
+public class NotifyNoticeRead extends MpCreateEntity {
+
+ /// 用户ID
+ private Long userId;
+
+ /// 公告ID
+ private Long noticeId;
+
+ /// 阅读时间
+ private OffsetDateTime readTime;
+
+ /// 是否忽略(用户主动隐藏该公告)
+ @TableField("is_ignored")
+ private Boolean isIgnored;
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/enums/NotifySeverityEnum.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/enums/NotifySeverityEnum.java
new file mode 100644
index 000000000..199873bfe
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/enums/NotifySeverityEnum.java
@@ -0,0 +1,24 @@
+package cn.daxpay.open.platform.notify.enums;
+
+import cn.daxpay.open.platform.core.i18n.I18nSupport;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/// 公告重要程度
+@Getter
+@RequiredArgsConstructor
+public enum NotifySeverityEnum implements I18nSupport {
+
+ /// 普通
+ normal("normal"),
+
+ /// 重要
+ important("important");
+
+ private final String code;
+
+ @Override
+ public String getI18nPrefix() {
+ return "enum.notify_severity";
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/enums/NotifyStatusEnum.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/enums/NotifyStatusEnum.java
new file mode 100644
index 000000000..607e69c59
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/enums/NotifyStatusEnum.java
@@ -0,0 +1,27 @@
+package cn.daxpay.open.platform.notify.enums;
+
+import cn.daxpay.open.platform.core.i18n.I18nSupport;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/// 公告状态
+@Getter
+@RequiredArgsConstructor
+public enum NotifyStatusEnum implements I18nSupport {
+
+ /// 草稿
+ draft("draft"),
+
+ /// 发布
+ published("published"),
+
+ /// 下线
+ offline("offline");
+
+ private final String code;
+
+ @Override
+ public String getI18nPrefix() {
+ return "enum.notify_status";
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/enums/NotifyTypeEnum.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/enums/NotifyTypeEnum.java
new file mode 100644
index 000000000..775c40427
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/enums/NotifyTypeEnum.java
@@ -0,0 +1,24 @@
+package cn.daxpay.open.platform.notify.enums;
+
+import cn.daxpay.open.platform.core.i18n.I18nSupport;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/// 通知类型(用于 SSE 推送载荷与前端分桶)
+@Getter
+@RequiredArgsConstructor
+public enum NotifyTypeEnum implements I18nSupport {
+
+ /// 公告(广播)
+ notice("notice"),
+
+ /// 个人消息(定向)
+ message("message");
+
+ private final String code;
+
+ @Override
+ public String getI18nPrefix() {
+ return "enum.notify_type";
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/param/notice/NotifyNoticeParam.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/param/notice/NotifyNoticeParam.java
new file mode 100644
index 000000000..0fc4f3f53
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/param/notice/NotifyNoticeParam.java
@@ -0,0 +1,43 @@
+package cn.daxpay.open.platform.notify.param.notice;
+
+import cn.daxpay.open.platform.core.validation.ValidationGroup;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Null;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.OffsetDateTime;
+
+/// 公告参数(管理端 新建/编辑)
+@Data
+@Accessors(chain = true)
+@Schema(title = "公告参数")
+public class NotifyNoticeParam {
+
+ @Null(message = "{validation.field.id.mustBeNullOnAdd}", groups = ValidationGroup.add.class)
+ @NotNull(message = "{validation.field.id.notNull}", groups = ValidationGroup.edit.class)
+ @Schema(description = "主键")
+ private Long id;
+
+ @NotBlank(message = "{validation.field.title.notBlank}", groups = ValidationGroup.add.class)
+ @Schema(description = "标题")
+ private String title;
+
+ @NotBlank(message = "{validation.field.content.notBlank}", groups = ValidationGroup.add.class)
+ @Schema(description = "正文(Markdown)")
+ private String content;
+
+ @Schema(description = "重要程度(normal普通/important重要)")
+ private String severity;
+
+ @Schema(description = "是否置顶")
+ private Boolean isTop;
+
+ @Schema(description = "生效时间(为空则立即生效)")
+ private OffsetDateTime effectiveTime;
+
+ @Schema(description = "过期时间(为空则永久有效)")
+ private OffsetDateTime expireTime;
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/param/notice/NotifyNoticeQuery.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/param/notice/NotifyNoticeQuery.java
new file mode 100644
index 000000000..904fc3918
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/param/notice/NotifyNoticeQuery.java
@@ -0,0 +1,21 @@
+package cn.daxpay.open.platform.notify.param.notice;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/// 公告查询(管理端分页)
+@Data
+@Accessors(chain = true)
+@Schema(title = "公告查询")
+public class NotifyNoticeQuery {
+
+ @Schema(description = "标题")
+ private String title;
+
+ @Schema(description = "状态")
+ private String status;
+
+ @Schema(description = "重要程度")
+ private String severity;
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/param/notice/NotifyUserNoticeQuery.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/param/notice/NotifyUserNoticeQuery.java
new file mode 100644
index 000000000..6e0a18410
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/param/notice/NotifyUserNoticeQuery.java
@@ -0,0 +1,15 @@
+package cn.daxpay.open.platform.notify.param.notice;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/// 用户端通知查询(铃铛列表)
+@Data
+@Accessors(chain = true)
+@Schema(title = "用户端通知查询")
+public class NotifyUserNoticeQuery {
+
+ @Schema(description = "是否只看未读")
+ private Boolean onlyUnread;
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/result/notice/NotifyNoticeBriefResult.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/result/notice/NotifyNoticeBriefResult.java
new file mode 100644
index 000000000..8b5165d12
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/result/notice/NotifyNoticeBriefResult.java
@@ -0,0 +1,41 @@
+package cn.daxpay.open.platform.notify.result.notice;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.OffsetDateTime;
+
+/// 通知项(铃铛列表, 公告与个人消息统一结构)
+@Data
+@Accessors(chain = true)
+@Schema(title = "通知项")
+public class NotifyNoticeBriefResult {
+
+ @Schema(description = "主键")
+ private Long id;
+
+ @Schema(description = "通知类型(notice公告/message个人消息)")
+ private String type;
+
+ @Schema(description = "标题")
+ private String title;
+
+ @Schema(description = "内容摘要")
+ private String message;
+
+ @Schema(description = "重要程度(公告专用)")
+ private String severity;
+
+ @Schema(description = "是否置顶(公告专用)")
+ private Boolean isTop;
+
+ @Schema(description = "是否已读")
+ private Boolean isRead;
+
+ @Schema(description = "跳转链接(个人消息专用)")
+ private String link;
+
+ @Schema(description = "创建时间")
+ private OffsetDateTime createTime;
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/result/notice/NotifyNoticeResult.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/result/notice/NotifyNoticeResult.java
new file mode 100644
index 000000000..9ab7fc531
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/result/notice/NotifyNoticeResult.java
@@ -0,0 +1,47 @@
+package cn.daxpay.open.platform.notify.result.notice;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.OffsetDateTime;
+
+/// 公告详情(管理端)
+@Data
+@Accessors(chain = true)
+@Schema(title = "公告详情")
+public class NotifyNoticeResult {
+
+ @Schema(description = "主键")
+ private Long id;
+
+ @Schema(description = "标题")
+ private String title;
+
+ @Schema(description = "正文(Markdown原文)")
+ private String content;
+
+ @Schema(description = "重要程度(normal普通/important重要)")
+ private String severity;
+
+ @Schema(description = "是否置顶")
+ private Boolean isTop;
+
+ @Schema(description = "生效时间")
+ private OffsetDateTime effectiveTime;
+
+ @Schema(description = "过期时间")
+ private OffsetDateTime expireTime;
+
+ @Schema(description = "状态(draft草稿/published发布/offline下线)")
+ private String status;
+
+ @Schema(description = "创建人ID")
+ private Long creator;
+
+ @Schema(description = "创建时间")
+ private OffsetDateTime createTime;
+
+ @Schema(description = "最后修改时间")
+ private OffsetDateTime lastModifiedTime;
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/result/notice/NotifyUnreadCountResult.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/result/notice/NotifyUnreadCountResult.java
new file mode 100644
index 000000000..7771d8bae
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/result/notice/NotifyUnreadCountResult.java
@@ -0,0 +1,21 @@
+package cn.daxpay.open.platform.notify.result.notice;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/// 未读数
+@Data
+@Accessors(chain = true)
+@Schema(title = "未读数")
+public class NotifyUnreadCountResult {
+
+ @Schema(description = "公告未读数")
+ private Integer noticeCount;
+
+ @Schema(description = "个人消息未读数")
+ private Integer messageCount;
+
+ @Schema(description = "合计未读数")
+ private Integer total;
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/service/notice/NotifyNoticeService.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/service/notice/NotifyNoticeService.java
new file mode 100644
index 000000000..3282f2f6d
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/service/notice/NotifyNoticeService.java
@@ -0,0 +1,101 @@
+package cn.daxpay.open.platform.notify.service.notice;
+
+import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil;
+import cn.daxpay.open.platform.core.exception.DataNotExistException;
+import cn.daxpay.open.platform.core.rest.param.PageParam;
+import cn.daxpay.open.platform.core.rest.result.PageResult;
+import cn.daxpay.open.platform.notify.convert.notice.NotifyNoticeConvert;
+import cn.daxpay.open.platform.notify.dao.notice.NotifyNoticeManager;
+import cn.daxpay.open.platform.notify.entity.notice.NotifyNotice;
+import cn.daxpay.open.platform.notify.enums.NotifySeverityEnum;
+import cn.daxpay.open.platform.notify.enums.NotifyStatusEnum;
+import cn.daxpay.open.platform.notify.param.notice.NotifyNoticeParam;
+import cn.daxpay.open.platform.notify.param.notice.NotifyNoticeQuery;
+import cn.daxpay.open.platform.notify.result.notice.NotifyNoticeResult;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/// 公告管理端服务(发布/编辑/下线/分页/详情)
+@Service
+@AllArgsConstructor
+public class NotifyNoticeService {
+
+ private final NotifyNoticeManager noticeManager;
+
+ private final NotifySseService sseService;
+
+ /// 新建公告(默认草稿状态, 通过发布接口上线)
+ @Transactional(rollbackFor = Exception.class)
+ public NotifyNoticeResult add(NotifyNoticeParam param) {
+ NotifyNotice notice = NotifyNoticeConvert.CONVERT.convert(param);
+ // 重要程度默认普通
+ if (notice.getSeverity() == null) {
+ notice.setSeverity(NotifySeverityEnum.normal.getCode());
+ }
+ // 置顶默认否
+ if (notice.getIsTop() == null) {
+ notice.setIsTop(false);
+ }
+ // 新建默认草稿
+ notice.setStatus(NotifyStatusEnum.draft.getCode());
+ noticeManager.save(notice);
+ return notice.toResult();
+ }
+
+ /// 编辑公告(允许编辑已发布, 状态与已读记录不变)
+ @Transactional(rollbackFor = Exception.class)
+ public NotifyNoticeResult update(NotifyNoticeParam param) {
+ NotifyNotice notice = noticeManager.findById(param.getId())
+ .orElseThrow(() -> new DataNotExistException("error.notify.notice.notExist"));
+ NotifyNoticeConvert.CONVERT.copy(param, notice);
+ noticeManager.updateById(notice);
+ return notice.toResult();
+ }
+
+ /// 删除公告(逻辑删除)
+ @Transactional(rollbackFor = Exception.class)
+ public void delete(Long id) {
+ if (!noticeManager.existedById(id)) {
+ throw new DataNotExistException("error.notify.notice.notExist");
+ }
+ noticeManager.deleteById(id);
+ }
+
+ /// 发布公告(草稿 -> 发布)
+ @Transactional(rollbackFor = Exception.class)
+ public void publish(Long id) {
+ NotifyNotice notice = noticeManager.findById(id)
+ .orElseThrow(() -> new DataNotExistException("error.notify.notice.notExist"));
+ notice.setStatus(NotifyStatusEnum.published.getCode());
+ noticeManager.updateById(notice);
+ // 实时推送给所有在线用户新公告
+ sseService.publishToAll(java.util.Map.of(
+ "type", "notice",
+ "event", "published",
+ "id", notice.getId(),
+ "title", notice.getTitle() == null ? "" : notice.getTitle()
+ ));
+ }
+
+ /// 下线公告(发布 -> 下线, 已读记录保留)
+ @Transactional(rollbackFor = Exception.class)
+ public void offline(Long id) {
+ NotifyNotice notice = noticeManager.findById(id)
+ .orElseThrow(() -> new DataNotExistException("error.notify.notice.notExist"));
+ notice.setStatus(NotifyStatusEnum.offline.getCode());
+ noticeManager.updateById(notice);
+ }
+
+ /// 查询公告详情
+ public NotifyNoticeResult findById(Long id) {
+ return noticeManager.findById(id)
+ .map(NotifyNotice::toResult)
+ .orElseThrow(DataNotExistException::new);
+ }
+
+ /// 分页查询
+ public PageResult page(PageParam pageParam, NotifyNoticeQuery query) {
+ return MpUtil.toPageResult(noticeManager.page(pageParam, query));
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/service/notice/NotifySseService.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/service/notice/NotifySseService.java
new file mode 100644
index 000000000..fbf475b30
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/service/notice/NotifySseService.java
@@ -0,0 +1,94 @@
+package cn.daxpay.open.platform.notify.service.notice;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/// SSE 实时推送服务(管理在线用户的 SseEmitter)
+///
+/// 单实例方案: 以 userId 维护本地 emitter 映射; 公告发布时推送给所有在线用户.
+/// 多实例横向扩展时需引入 Redis Pub/Sub 跨实例广播(预留扩展点).
+@Slf4j
+@Service
+public class NotifySseService {
+
+ /// userId -> SseEmitter
+ private final Map emitters = new ConcurrentHashMap<>();
+
+ /// 建立连接
+ public SseEmitter connect(Long userId) {
+ // 顶掉旧连接
+ SseEmitter old = emitters.remove(userId);
+ if (old != null) {
+ try {
+ old.complete();
+ } catch (Exception ignored) {
+ }
+ }
+ // 0L 表示不超时, 依靠心跳维持
+ SseEmitter emitter = new SseEmitter(0L);
+ emitters.put(userId, emitter);
+ emitter.onCompletion(() -> emitters.remove(userId, emitter));
+ emitter.onTimeout(() -> emitters.remove(userId, emitter));
+ emitter.onError(e -> emitters.remove(userId, emitter));
+ return emitter;
+ }
+
+ /// 主动断开
+ public void disconnect(Long userId) {
+ SseEmitter emitter = emitters.remove(userId);
+ if (emitter != null) {
+ try {
+ emitter.complete();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ /// 推送给所有在线用户(公告发布场景)
+ public void publishToAll(Object payload) {
+ if (emitters.isEmpty()) {
+ return;
+ }
+ emitters.forEach((userId, emitter) -> {
+ try {
+ emitter.send(SseEmitter.event().data(payload));
+ } catch (IOException e) {
+ emitters.remove(userId, emitter);
+ }
+ });
+ }
+
+ /// 推送给指定用户(个人消息场景, 预留)
+ public void publishToUser(Long userId, Object payload) {
+ SseEmitter emitter = emitters.get(userId);
+ if (emitter == null) {
+ return;
+ }
+ try {
+ emitter.send(SseEmitter.event().data(payload));
+ } catch (IOException e) {
+ emitters.remove(userId, emitter);
+ }
+ }
+
+ /// 心跳: 每 25 秒发注释行, 防止 Nginx/代理超时断开
+ @Scheduled(fixedRate = 25_000)
+ public void heartbeat() {
+ if (emitters.isEmpty()) {
+ return;
+ }
+ emitters.forEach((userId, emitter) -> {
+ try {
+ emitter.send(SseEmitter.event().comment("heartbeat"));
+ } catch (IOException e) {
+ emitters.remove(userId, emitter);
+ }
+ });
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/service/notice/NotifyUserNoticeService.java b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/service/notice/NotifyUserNoticeService.java
new file mode 100644
index 000000000..16a2ff2c0
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/java/cn/daxpay/open/platform/notify/service/notice/NotifyUserNoticeService.java
@@ -0,0 +1,188 @@
+package cn.daxpay.open.platform.notify.service.notice;
+
+import cn.daxpay.open.platform.capability.auth.util.SecurityUtil;
+import cn.daxpay.open.platform.common.mybatisplus.base.MpIdEntity;
+import cn.daxpay.open.platform.notify.convert.notice.NotifyNoticeConvert;
+import cn.daxpay.open.platform.notify.dao.message.NotifyMessageManager;
+import cn.daxpay.open.platform.notify.dao.notice.NotifyNoticeManager;
+import cn.daxpay.open.platform.notify.dao.notice.NotifyNoticeReadManager;
+import cn.daxpay.open.platform.notify.entity.message.NotifyMessage;
+import cn.daxpay.open.platform.notify.entity.notice.NotifyNotice;
+import cn.daxpay.open.platform.notify.entity.notice.NotifyNoticeRead;
+import cn.daxpay.open.platform.notify.enums.NotifyStatusEnum;
+import cn.daxpay.open.platform.notify.enums.NotifyTypeEnum;
+import cn.daxpay.open.platform.notify.param.notice.NotifyUserNoticeQuery;
+import cn.daxpay.open.platform.notify.result.notice.NotifyNoticeBriefResult;
+import cn.daxpay.open.platform.notify.result.notice.NotifyUnreadCountResult;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/// 用户端通知服务(未读数/列表/已读/清空/忽略)
+///
+/// 公告采用"广播 + 已读表"模型: 一条公告对全员可见, 单独维护用户阅读/忽略状态;
+/// 个人消息采用"定向"模型, 直接带 user_id. 两类在铃铛中聚合展示.
+@Service
+@AllArgsConstructor
+public class NotifyUserNoticeService {
+
+ private final NotifyNoticeManager noticeManager;
+
+ private final NotifyNoticeReadManager readManager;
+
+ private final NotifyMessageManager messageManager;
+
+ /// 未读数(公告 + 个人消息)
+ public NotifyUnreadCountResult unreadCount() {
+ Long userId = SecurityUtil.getUserId();
+ // 可见公告
+ List visibleNotices = findVisibleNotices();
+ // 用户已读(且未忽略)的公告id集合
+ Set readNoticeIds = readManager.findAllByUserAndNotIgnored(userId).stream()
+ .map(NotifyNoticeRead::getNoticeId)
+ .collect(Collectors.toSet());
+ int noticeUnread = (int) visibleNotices.stream()
+ .filter(n -> !readNoticeIds.contains(n.getId()))
+ .count();
+ // 个人消息未读数(预留, 当前表无数据返回0)
+ int messageUnread = messageManager.findAllByUserAndUnread(userId).size();
+ return new NotifyUnreadCountResult()
+ .setNoticeCount(noticeUnread)
+ .setMessageCount(messageUnread)
+ .setTotal(noticeUnread + messageUnread);
+ }
+
+ /// 铃铛列表(可见公告 + 个人消息聚合, 排除被忽略的公告)
+ public List list(NotifyUserNoticeQuery query) {
+ Long userId = SecurityUtil.getUserId();
+
+ // 可见公告
+ List visibleNotices = findVisibleNotices();
+ // 用户阅读记录
+ List reads = readManager.findAllByUser(userId);
+ Map readMap = reads.stream()
+ .collect(Collectors.toMap(NotifyNoticeRead::getNoticeId, Function.identity(), (a, b) -> a));
+ Set ignoredIds = reads.stream()
+ .filter(r -> Boolean.TRUE.equals(r.getIsIgnored()))
+ .map(NotifyNoticeRead::getNoticeId)
+ .collect(Collectors.toSet());
+
+ List list = new ArrayList<>();
+ // 组装公告(排除被忽略的)
+ for (NotifyNotice notice : visibleNotices) {
+ if (ignoredIds.contains(notice.getId())) {
+ continue;
+ }
+ NotifyNoticeBriefResult brief = NotifyNoticeConvert.CONVERT.toBrief(notice);
+ NotifyNoticeRead read = readMap.get(notice.getId());
+ // 有阅读记录视为已读
+ brief.setIsRead(read != null);
+ list.add(brief);
+ }
+
+ // 个人消息(未删除的全部展示, 已读后前端可删除)
+ List messages = messageManager.lambdaQuery()
+ .eq(NotifyMessage::getUserId, userId)
+ .orderByDesc(MpIdEntity::getId)
+ .list();
+ for (NotifyMessage message : messages) {
+ list.add(NotifyNoticeConvert.CONVERT.convert(message));
+ }
+
+ // 只看未读
+ if (Boolean.TRUE.equals(query.getOnlyUnread())) {
+ list = list.stream()
+ .filter(b -> !Boolean.TRUE.equals(b.getIsRead()))
+ .collect(Collectors.toList());
+ }
+ return list;
+ }
+
+ /// 标记单条已读
+ @Transactional(rollbackFor = Exception.class)
+ public void markRead(String type, Long id) {
+ if (NotifyTypeEnum.notice.getCode().equals(type)) {
+ markNoticeRead(id, false);
+ }
+ // 个人消息标记已读预留(暂不接入业务)
+ }
+
+ /// 清空(全部标记已读): 为所有可见未读公告补阅读记录
+ @Transactional(rollbackFor = Exception.class)
+ public void readAll() {
+ Long userId = SecurityUtil.getUserId();
+ OffsetDateTime now = OffsetDateTime.now();
+ List visibleNotices = findVisibleNotices();
+ Set readNoticeIds = readManager.findAllByUser(userId).stream()
+ .map(NotifyNoticeRead::getNoticeId)
+ .collect(Collectors.toSet());
+ for (NotifyNotice notice : visibleNotices) {
+ if (readNoticeIds.contains(notice.getId())) {
+ continue;
+ }
+ NotifyNoticeRead read = new NotifyNoticeRead();
+ read.setUserId(userId);
+ read.setNoticeId(notice.getId());
+ read.setReadTime(now);
+ read.setIsIgnored(false);
+ readManager.save(read);
+ }
+ }
+
+ /// 忽略(用户主动隐藏)
+ @Transactional(rollbackFor = Exception.class)
+ public void ignore(String type, Long id) {
+ Long userId = SecurityUtil.getUserId();
+ if (NotifyTypeEnum.notice.getCode().equals(type)) {
+ markNoticeRead(id, true);
+ } else if (NotifyTypeEnum.message.getCode().equals(type)) {
+ // 个人消息忽略=逻辑删除(预留)
+ NotifyMessage message = messageManager.findById(id).orElse(null);
+ if (message != null && message.getUserId().equals(userId)) {
+ messageManager.deleteById(id);
+ }
+ }
+ }
+
+ /// 标记公告已读/忽略(无记录则新增)
+ private void markNoticeRead(Long noticeId, boolean ignored) {
+ Long userId = SecurityUtil.getUserId();
+ OffsetDateTime now = OffsetDateTime.now();
+ Optional existing = readManager.findByUserAndNotice(userId, noticeId);
+ if (existing.isPresent()) {
+ NotifyNoticeRead read = existing.get();
+ read.setReadTime(now);
+ read.setIsIgnored(ignored);
+ readManager.updateById(read);
+ } else {
+ NotifyNoticeRead read = new NotifyNoticeRead();
+ read.setUserId(userId);
+ read.setNoticeId(noticeId);
+ read.setReadTime(now);
+ read.setIsIgnored(ignored);
+ readManager.save(read);
+ }
+ }
+
+ /// 查询当前生效的可见公告(已发布 + 生效期内, 置顶/时间倒序)
+ private List findVisibleNotices() {
+ OffsetDateTime now = OffsetDateTime.now();
+ return noticeManager.lambdaQuery()
+ .eq(NotifyNotice::getStatus, NotifyStatusEnum.published.getCode())
+ .and(w -> w.isNull(NotifyNotice::getEffectiveTime).or().le(NotifyNotice::getEffectiveTime, now))
+ .and(w -> w.isNull(NotifyNotice::getExpireTime).or().gt(NotifyNotice::getExpireTime, now))
+ .orderByDesc(NotifyNotice::getIsTop)
+ .orderByDesc(MpIdEntity::getId)
+ .list();
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-service/service-notify/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/daxpay-platform/daxpay-platform-service/service-notify/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 000000000..1bc112b0a
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-service/service-notify/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+cn.daxpay.open.platform.notify.NotifyApplication