diff --git a/server/app/common/model/system/SysMessage.php b/server/app/common/model/system/SysMessage.php index b85ff11bb364c80009616e0808376fa6c211c31d..3858b0f4c376190719742d803f5d6b26bba72db1 100644 --- a/server/app/common/model/system/SysMessage.php +++ b/server/app/common/model/system/SysMessage.php @@ -54,8 +54,7 @@ class SysMessage extends BaseModel 'channel', 'related_id', 'related_type', - 'action_url', - 'action_params', + 'jump_params', 'message_uuid', 'read_at', 'created_at', diff --git a/server/app/common/queue/redis/AdminAnnouncementPushConsumer.php b/server/app/common/queue/redis/AdminAnnouncementPushConsumer.php index ecc903a2e18dfc16caae1aecb4ca9c5a0c6aa69f..55f120929b8d0f24a1e50663224138f02b1ce679 100644 --- a/server/app/common/queue/redis/AdminAnnouncementPushConsumer.php +++ b/server/app/common/queue/redis/AdminAnnouncementPushConsumer.php @@ -67,7 +67,6 @@ class AdminAnnouncementPushConsumer implements Consumer Logger::debug("公告推送完成: {$data['title']}"); return true; } catch (\Throwable $e) { - var_dump($e->getMessage()); Logger::error("公告推送失败: " . $e->getMessage(), [ 'error' => $e->getTraceAsString(), 'data' => $data, @@ -144,13 +143,8 @@ class AdminAnnouncementPushConsumer implements Consumer // 如果有UUID,则排除已推送的用户 if (!empty($uuid)) { - $query->where(function ($q) use ($uuid) { - $q->whereDoesntHave('message', function ($subQuery) use ($uuid) { - $subQuery->where('message_uuid', $uuid); - }) - ->orWhereHas('message', function ($subQuery) { - $subQuery->whereNull('message_uuid'); - }); + $query->whereDoesntHave('message', function ($q) use ($uuid) { + $q->where('message_uuid', $uuid); }); } @@ -193,4 +187,4 @@ class AdminAnnouncementPushConsumer implements Consumer Notification::batchSend(PushClientType::BACKEND, $tenantId, $sendData); } -} \ No newline at end of file +} diff --git a/server/config/core/app.php b/server/config/core/app.php index 46570404709516fe839efc520bf05e8efb3cea1d..77b2a13c029bc066ae09258aa26d70017c997cfe 100644 --- a/server/config/core/app.php +++ b/server/config/core/app.php @@ -12,5 +12,5 @@ return [ 'enable' => true, - 'version' => '4.0.3', + 'version' => '4.0.4', ]; diff --git a/server/core/context/RequestTenantContext.php b/server/core/context/RequestTenantContext.php index 10949432ec75dedb359fad4a79f93d74d74c147c..889c2a04044ba413fb54cea487ca81ed317a44ba 100644 --- a/server/core/context/RequestTenantContext.php +++ b/server/core/context/RequestTenantContext.php @@ -13,7 +13,9 @@ namespace core\context; use core\enum\platform\IsolationMode; +use Illuminate\Database\Connection; use InvalidArgumentException; +use support\Db; class RequestTenantContext implements RequestTenantContextInterface { @@ -159,6 +161,16 @@ class RequestTenantContext implements RequestTenantContextInterface return $expirationTime !== null && time() > $expirationTime; } + /** + * 获取数据库连接 + * + * @return Connection + */ + public function db(): Connection + { + return Db::connection($this->getDatabaseConnection()); + } + private function validateIsolationMode(string|int $mode): void { if (!in_array($mode, IsolationMode::valuesArray(), true)) { diff --git a/server/core/context/TenantContext.php b/server/core/context/TenantContext.php index 258a28f0e0cf828e56073e6737cb274c65ea007a..9130803e95d61b11b56f42e39aa29e30d584ef50 100644 --- a/server/core/context/TenantContext.php +++ b/server/core/context/TenantContext.php @@ -34,6 +34,7 @@ use support\Container; * @method static ?int getExpirationTime() * @method static bool isExpired() * @method static getTenantId() + * @method static db() */ class TenantContext { diff --git a/server/core/enum/system/MessageEvent.php b/server/core/enum/system/MessageEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..d2e9b8f28616925e2a68fdce2de25cf6bfbafbd3 --- /dev/null +++ b/server/core/enum/system/MessageEvent.php @@ -0,0 +1,33 @@ + '默认', + }; + } +} diff --git a/server/core/enum/system/MessageType.php b/server/core/enum/system/MessageType.php index d4c323740a78adbe6d2d0dfd7df0bbf20176ca3e..e782ec23058842dd392c7e882f2e42e880b082e1 100644 --- a/server/core/enum/system/MessageType.php +++ b/server/core/enum/system/MessageType.php @@ -12,38 +12,92 @@ namespace core\enum\system; -enum MessageType: int +enum MessageType: string { - case SYSTEM_MESSAGE = 1;//系统消息 - case ANNOUNCEMENT = 2;//公告 - case OPERATION_LOG = 3;//操作日志 - case WARNING = 4;//警告 - case USER_PRIVATE_MESSAGE = 5;//用户私信 - case WORKFLOW_MESSAGE = 6;//工作流消息 - - // 定义一个方法以获取状态的描述 + // 系统类消息 + case SYSTEM_MESSAGE = 'system_message'; // 系统消息 + case ANNOUNCEMENT = 'announcement'; // 公告 + case OPERATION_LOG = 'operation_log'; // 操作日志 + case WARNING = 'warning'; // 警告 + case NOTIFICATION = 'notification'; // 通知 + case USER_PRIVATE_MESSAGE = 'private_message'; // 用户私信 + case DATA_ALERT = 'data_alert'; // 数据警报 + case SCHEDULED_REPORT = 'scheduled_report'; // 定时报告 + + // 工作流相关消息(添加wf_前缀) + case WF_TODO_TASK = 'wf_todo_task'; // 待办任务 + case WF_WORKFLOW_MESSAGE = 'wf_workflow'; // 工作流消息 + case WF_REMINDER = 'wf_reminder'; // 提醒 + case WF_AUDIT_MESSAGE = 'wf_audit'; // 审核消息 + case WF_APPROVAL_REQUEST = 'wf_approval'; // 审批请求 + + /** + * 获取消息类型的中文标签 + */ public function label(): string { return match ($this) { - self::SYSTEM_MESSAGE => '系统', + self::SYSTEM_MESSAGE => '系统消息', self::ANNOUNCEMENT => '公告', self::OPERATION_LOG => '操作日志', self::WARNING => '警告', + self::WF_TODO_TASK => '待办任务', + self::NOTIFICATION => '通知', self::USER_PRIVATE_MESSAGE => '私信', - self::WORKFLOW_MESSAGE => '工作流', + self::WF_WORKFLOW_MESSAGE => '工作流消息', + self::WF_REMINDER => '提醒', + self::WF_AUDIT_MESSAGE => '审核消息', + self::WF_APPROVAL_REQUEST => '审批请求', + self::DATA_ALERT => '数据警报', + self::SCHEDULED_REPORT => '定时报告', }; } - // 设置标签颜色 + /** + * 获取消息类型的颜色标识 + */ public function color(): string { return match ($this) { - self::SYSTEM_MESSAGE => 'blue', // 系统消息 - self::ANNOUNCEMENT => 'green', // 公告 - self::OPERATION_LOG => 'grey', // 操作日志 - self::WARNING => 'orange', // 警告 - self::USER_PRIVATE_MESSAGE => 'purple', // 私信 - self::WORKFLOW_MESSAGE => 'cyan', // 工作流 + self::SYSTEM_MESSAGE => 'blue', // 系统消息 - 蓝色 + self::ANNOUNCEMENT => 'green', // 公告 - 绿色 + self::OPERATION_LOG => 'grey', // 操作日志 - 灰色 + self::WARNING => 'orange', // 警告 - 橙色 + self::WF_TODO_TASK => 'purple', // 待办任务 - 紫色 + self::NOTIFICATION => 'teal', // 通知 - 青绿色 + self::USER_PRIVATE_MESSAGE => 'pink',// 私信 - 粉色 + self::WF_WORKFLOW_MESSAGE => 'cyan', // 工作流消息 - 青色 + self::WF_REMINDER => 'yellow', // 提醒 - 黄色 + self::WF_AUDIT_MESSAGE => 'indigo', // 审核消息 - 靛蓝色 + self::WF_APPROVAL_REQUEST => 'deep-purple', // 审批请求 - 深紫色 + self::DATA_ALERT => 'red', // 数据警报 - 红色 + self::SCHEDULED_REPORT => 'light-blue', // 定时报告 - 浅蓝色 }; } + + /** + * 获取所有消息类型选项(用于下拉选择等场景) + */ + public static function options(): array + { + $options = []; + foreach (self::cases() as $case) { + $options[$case->value] = $case->label(); + } + return $options; + } + + /** + * 从字符串值创建枚举实例 + */ + public static function fromValue(string $value): ?self + { + foreach (self::cases() as $case) { + if ($case->value === $value) { + return $case; + } + } + return null; + } + } diff --git a/server/core/notify/NotificationService.php b/server/core/notify/NotificationService.php index 70828a96da58d58278b8f9f5728bd637dc22a995..e2f4c63b63cee484ebc465e24d2f7cd1c4ee1cc7 100644 --- a/server/core/notify/NotificationService.php +++ b/server/core/notify/NotificationService.php @@ -72,6 +72,7 @@ final class NotificationService ?string $socketId = null ): array { + $userIds = Arr::normalize($userIds); $result = [ 'push_count' => 0, @@ -177,9 +178,6 @@ final class NotificationService return $results; } - /** - * 仅推送消息(不记录) - */ /** * 仅推送消息(不记录) */ @@ -231,17 +229,13 @@ final class NotificationService $messages = []; foreach ($receiverIds as $receiverId) { - try { - $message = $this->messageModel->create(array_merge( - $defaults, - $data, - ['message_uuid' => $this->generateMessageUuid($data['message_uuid'] ?? null)], - ['receiver_id' => $receiverId] - )); - $messages[] = $message->toArray(); // 直接返回模型数据 - } catch (\Throwable $e) { - throw new \Exception($e->getMessage()); - } + $message = $this->messageModel->create(array_merge( + $defaults, + $data, + ['message_uuid' => $this->generateMessageUuid($data['message_uuid'] ?? null)], + ['receiver_id' => $receiverId] + )); + $messages[] = $message->toArray(); // 直接返回模型数据 } return $messages; @@ -270,7 +264,7 @@ final class NotificationService /** * 计算过期时间戳 */ - private function calculateExpireTimestamp(?int $expireDays): int + private function calculateExpireTimestamp(int|null $expireDays): int { if ($expireDays === null) { $expireDays = self::DEFAULT_EXPIRE_DAYS; diff --git a/server/core/utils/AssertHelper.php b/server/core/utils/AssertHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..67b9520aaa7801c88438147379ff36c5cd0d3c0b --- /dev/null +++ b/server/core/utils/AssertHelper.php @@ -0,0 +1,224 @@ + $max) { + $message = $message ?? sprintf("[Assertion failed] - Value must be between %s and %s", $min, $max); + throw new AdminException($message); + } + } + + /** + * 断言字符串匹配正则表达式 + * + * @throws \core\exception\handler\AdminException + */ + public static function matchesRegex(string $value, string $pattern, string $message = null): void + { + if (!preg_match($pattern, $value)) { + $message = $message ?? sprintf("[Assertion failed] - Value does not match pattern %s", $pattern); + throw new AdminException($message); + } + } + + /** + * 断言两个值相等(松散比较 ==) + * + * @throws \core\exception\handler\AdminException + */ + public static function equals(mixed $actual, mixed $expected, string $message = null): void + { + if ($actual != $expected) { + $message = $message ?? sprintf("[Assertion failed] - Expected %s, got %s", var_export($expected, true), var_export($actual, true)); + throw new AdminException($message); + } + } + + /** + * 断言两个值严格相等(===) + * + * @throws \core\exception\handler\AdminException + */ + public static function strictEquals(mixed $actual, mixed $expected, string $message = null): void + { + if ($actual !== $expected) { + $message = $message ?? sprintf("[Assertion failed] - Expected strict %s, got %s", var_export($expected, true), var_export($actual, true)); + throw new AdminException($message); + } + } + + /** + * 断言值为有效的电子邮件格式 + * + * @throws \core\exception\handler\AdminException + */ + public static function isEmail(string $value, string $message = "[Assertion failed] - Value is not a valid email address"): void + { + if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { + throw new AdminException($message); + } + } + + /** + * 断言值为有效的URL格式 + * + * @throws \core\exception\handler\AdminException + */ + public static function isUrl(string $value, string $message = "[Assertion failed] - Value is not a valid URL"): void + { + if (!filter_var($value, FILTER_VALIDATE_URL)) { + throw new AdminException($message); + } + } + + /** + * 断言目录存在 + * + * @throws \core\exception\handler\AdminException + */ + public static function directoryExists(string $path, string $message = null): void + { + if (!is_dir($path)) { + $message = $message ?? sprintf("[Assertion failed] - Directory %s does not exist", $path); + throw new AdminException($message); + } + } + + /** + * 断言文件存在 + * + * @throws \core\exception\handler\AdminException + */ + public static function fileExists(string $path, string $message = null): void + { + if (!file_exists($path)) { + $message = $message ?? sprintf("[Assertion failed] - File %s does not exist", $path); + throw new AdminException($message); + } + } +} diff --git a/server/scripts/sql/install.sql b/server/scripts/sql/install.sql index 1768862fbe26abb2fad98d71f0d520175590e543..7257d350d3cbb0378e2e2785ce60f177358e0f24 100644 --- a/server/scripts/sql/install.sql +++ b/server/scripts/sql/install.sql @@ -505,6 +505,8 @@ CREATE TABLE `ma_sys_message` `priority` tinyint(1) NULL DEFAULT 3 COMMENT '优先级(1紧急 2急迫 3普通)', `channel` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'message' COMMENT '发送渠道', `related_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '关联业务ID(如订单号、日志ID等)', + `related_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '关联业务类型', + `jump_params` json NULL COMMENT '跳转关联业务参数', `message_uuid` varchar(50) NULL DEFAULT NULL COMMENT '消息uuid', `created_at` bigint(20) NULL DEFAULT NULL COMMENT '创建时间', `expired_at` bigint(20) NULL DEFAULT NULL COMMENT '过期时间', diff --git a/server/scripts/sql/update4.0.4.sql b/server/scripts/sql/update4.0.4.sql new file mode 100644 index 0000000000000000000000000000000000000000..1756e7d292c10d50aaf98966116a3fee1cff0949 --- /dev/null +++ b/server/scripts/sql/update4.0.4.sql @@ -0,0 +1,7 @@ +START TRANSACTION; +ALTER TABLE `ma_sys_message` ADD COLUMN `related_type` VARCHAR(50) NULL DEFAULT NULL COMMENT '业务类型'; +ALTER TABLE `ma_sys_message` ADD COLUMN `jump_params` JSON(0) NULL DEFAULT NULL COMMENT '跳转参数'; + +COMMIT; + + diff --git a/web/src/store/modules/notify.ts b/web/src/store/modules/notify.ts index 921e99a7f7c9e81e5c78e4af810f66c2bc332816..ce71eff629db55e0375d83e8cc5b960089f547f2 100644 --- a/web/src/store/modules/notify.ts +++ b/web/src/store/modules/notify.ts @@ -1,6 +1,6 @@ import type { NotificationItem } from '#/components/common/effects/layouts'; -import { computed, ref } from 'vue'; +import { computed, h, ref, RendererElement, RendererNode, VNode, VNodeArrayChildren } from 'vue'; import { useUserStore } from '#/components/common/stores'; @@ -60,10 +60,27 @@ export const useNotifyStore = defineStore( String(notification.channel === String(data.channel)) ); if (!exists) { + const formattedContent = computed(() => { + const text = data.content + .replace(/\\n/g, '\n') // 还原 \\n → \n + .replace(/\n\n/g, '\n\n') // 保留双换行标记 + .replace(/\n/g, '\n'); // 保留单换行标记 + + // 按 \n\n 分割段落 + const paragraphs = text.split('\n\n'); + return h('div', + paragraphs.map((para: string) => + h('p', + para.split('\n').map((line: string | number | boolean | VNode | VNodeArrayChildren | (() => any) | { [name: string]: unknown; $stable?: boolean; } | undefined) => h('span', { style: { display: 'block' } }, line)) + ) + ) + ); + }); + notification.success({ message: data.title || '新消息提醒', duration: 5, - description: data.content || '', + description: formattedContent.value, }); notificationList.value.unshift({ id: String(data.id), @@ -100,24 +117,6 @@ export const useNotifyStore = defineStore( auth: '/plugin/webman/push/auth' // 订阅鉴权(仅限于私有频道) }); - - // 公共订阅-弃用 - // const notices_channel = connection.subscribe('backend-'+'admin-' + tenantId.value+'-*'); - //公告订阅 - // notices_channel.on('notice', function (message: any) { - // if (Array.isArray(message)) { - // message.forEach((data: any) => { - // data['receiver_id'] = userId.value;//公告没有对应的接收人把本地登录的用户id追加到数据流 - // addUniqueNotification(data); - // }); - // } else { - // message['receiver_id'] = userId.value;//公告没有对应的接收人把本地登录的用户id追加到数据流 - // addUniqueNotification(message); - // } - // }); - - - /** * * 订阅后端消息通道(格式:backend-{模块}-{租户ID}-{用户ID})