From eab71cf0b6294ea3c05e99da02410e9d629bd139 Mon Sep 17 00:00:00 2001 From: sunlei Date: Wed, 10 Jun 2026 13:20:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E9=BD=90=E7=AB=99=E5=86=85?= =?UTF-8?q?=E4=BF=A1=E8=8F=9C=E5=8D=95=E4=B8=8E=E6=9D=83=E9=99=90=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API.md | 13 + sql/fix-admin-menu-meta.sql | 8 + sql/system-notice-menu.sql | 99 +++++++ src/admin/admin.module.ts | 2 + src/admin/notice/admin-notice.controller.ts | 89 ++++++ src/admin/notice/admin-notice.dto.ts | 122 +++++++++ src/admin/notice/admin-notice.entity.ts | 114 ++++++++ src/admin/notice/admin-notice.service.ts | 285 ++++++++++++++++++++ src/admin/notice/notice.module.ts | 13 + 9 files changed, 745 insertions(+) create mode 100644 sql/system-notice-menu.sql create mode 100644 src/admin/notice/admin-notice.controller.ts create mode 100644 src/admin/notice/admin-notice.dto.ts create mode 100644 src/admin/notice/admin-notice.entity.ts create mode 100644 src/admin/notice/admin-notice.service.ts create mode 100644 src/admin/notice/notice.module.ts diff --git a/API.md b/API.md index 6ed8c6a..71b5921 100644 --- a/API.md +++ b/API.md @@ -172,6 +172,18 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管 日志行包含 `timestamp`、`level`、`message`、`method`、`path`、`statusCode`、`durationMs`、`requestId`、`raw` 等字段。 +### 系统站内信 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/system/notice/list` | 站内信分页列表,支持 `keyword`、`level`、`status`、`isTop`、`notifyUsers`、`pageNo`、`pageSize` | +| `GET` | `/system/notice/detail/:id` | 查询站内信详情 | +| `POST` | `/system/notice/save` | 新增站内信 | +| `POST` | `/system/notice/update` | 更新站内信 | +| `DELETE` | `/system/notice/:id` | 删除站内信(逻辑删除) | +| `POST` | `/system/notice/toggle` | 切换状态(`id`、`status`) | +| `POST` | `/system/notice/top` | 切换置顶(`id`、`isTop`) | + ## Blog 本地内容 `/blog/*` 是本地博客内容能力,供 `Vue/kt-blog-web` 和 Admin 博客管理使用。 @@ -422,6 +434,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT | `sql/blog-menu.sql` | 初始化 Blog 管理菜单 | | `sql/qqbot-init.sql` | 初始化 QQBot 表、插件命令和字典 | | `sql/system-log-menu.sql` | 初始化系统日志菜单和权限 | +| `sql/system-notice-menu.sql` | 初始化系统站内信表与菜单权限 | | `sql/migrate-dict-to-admin-dict.sql` | 旧 `dict` 迁移到 `admin_dict` | | `sql/migrate-component-to-admin-component.sql` | 旧 `component` 迁移到 `admin_component` | | `sql/fix-admin-menu-meta.sql` | 修复菜单 meta 被覆盖为空 | diff --git a/sql/fix-admin-menu-meta.sql b/sql/fix-admin-menu-meta.sql index 904af73..b33e814 100644 --- a/sql/fix-admin-menu-meta.sql +++ b/sql/fix-admin-menu-meta.sql @@ -21,6 +21,10 @@ SET `meta` = CASE `name` WHEN 'SystemDeptCreate' THEN '{"title":"common.create"}' WHEN 'SystemDeptEdit' THEN '{"title":"common.edit"}' WHEN 'SystemDeptDelete' THEN '{"title":"common.delete"}' + WHEN 'SystemNotice' THEN '{"icon":"mdi:bell-outline","title":"system.notice.title"}' + WHEN 'SystemNoticeCreate' THEN '{"title":"common.create"}' + WHEN 'SystemNoticeEdit' THEN '{"title":"common.edit"}' + WHEN 'SystemNoticeDelete' THEN '{"title":"common.delete"}' WHEN 'SystemLog' THEN '{"icon":"lucide:scroll-text","title":"system.log.title"}' WHEN 'Project' THEN '{"badgeType":"dot","icon":"carbon:data-center","order":9998,"title":"demos.vben.title"}' WHEN 'VbenDocument' THEN '{"icon":"carbon:book","iframeSrc":"https://doc.vben.pro","title":"demos.vben.document"}' @@ -45,6 +49,10 @@ WHERE `name` IN ( 'SystemDeptCreate', 'SystemDeptEdit', 'SystemDeptDelete', + 'SystemNotice', + 'SystemNoticeCreate', + 'SystemNoticeEdit', + 'SystemNoticeDelete', 'SystemLog', 'Project', 'VbenDocument', diff --git a/sql/system-notice-menu.sql b/sql/system-notice-menu.sql new file mode 100644 index 0000000..027fa83 --- /dev/null +++ b/sql/system-notice-menu.sql @@ -0,0 +1,99 @@ +-- 增量初始化系统站内信表与菜单权限。 +-- 用途:已有库不需要重跑完整 vben-admin-init.sql 时,补齐站内信能力。 + +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS `admin_notice` ( + `id` bigint NOT NULL, + `title` varchar(255) NOT NULL, + `content` longtext NOT NULL, + `summary` text DEFAULT NULL, + `level` int NOT NULL DEFAULT 1, + `status` int NOT NULL DEFAULT 1, + `is_top` tinyint(1) NOT NULL DEFAULT 0, + `notify_users` text DEFAULT NULL, + `created_by` bigint DEFAULT NULL, + `is_deleted` tinyint(1) NOT NULL DEFAULT 0, + `create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`), + KEY `idx_admin_notice_status` (`status`), + KEY `idx_admin_notice_is_top` (`is_top`), + KEY `idx_admin_notice_is_deleted` (`is_deleted`), + KEY `idx_admin_notice_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO `admin_menu` ( + `id`, + `pid`, + `name`, + `path`, + `component`, + `redirect`, + `auth_code`, + `type`, + `meta`, + `status`, + `sort` +) +VALUES ( + 2041700000000100206, + 2041700000000100002, + 'SystemNotice', + '/system/notice', + '/system/notice/list', + NULL, + 'System:Notice:List', + 'menu', + '{"icon":"mdi:bell-outline","title":"system.notice.title"}', + 1, + 7 +) +ON DUPLICATE KEY UPDATE + `pid` = VALUES(`pid`), + `path` = VALUES(`path`), + `component` = VALUES(`component`), + `redirect` = VALUES(`redirect`), + `auth_code` = VALUES(`auth_code`), + `type` = VALUES(`type`), + `meta` = VALUES(`meta`), + `status` = VALUES(`status`), + `sort` = VALUES(`sort`), + `is_deleted` = 0; + +INSERT INTO `admin_menu` ( + `id`, + `pid`, + `name`, + `path`, + `component`, + `redirect`, + `auth_code`, + `type`, + `meta`, + `status`, + `sort` +) +VALUES + (2041700000000120211, 2041700000000100206, 'SystemNoticeCreate', NULL, NULL, NULL, 'System:Notice:Create', 'button', '{"title":"common.create"}', 1, 0), + (2041700000000120212, 2041700000000100206, 'SystemNoticeEdit', NULL, NULL, NULL, 'System:Notice:Edit', 'button', '{"title":"common.edit"}', 1, 1), + (2041700000000120213, 2041700000000100206, 'SystemNoticeDelete', NULL, NULL, NULL, 'System:Notice:Delete', 'button', '{"title":"common.delete"}', 1, 2) +ON DUPLICATE KEY UPDATE + `pid` = VALUES(`pid`), + `path` = VALUES(`path`), + `component` = VALUES(`component`), + `redirect` = VALUES(`redirect`), + `auth_code` = VALUES(`auth_code`), + `type` = VALUES(`type`), + `meta` = VALUES(`meta`), + `status` = VALUES(`status`), + `sort` = VALUES(`sort`), + `is_deleted` = 0; + +INSERT IGNORE INTO `admin_role_menu` (`role_id`, `menu_id`) +SELECT role.`id`, menu.`id` +FROM `admin_role` role +JOIN `admin_menu` menu ON menu.`name` IN ('SystemNotice', 'SystemNoticeCreate', 'SystemNoticeEdit', 'SystemNoticeDelete') +WHERE role.`role_code` IN ('super', 'admin') + AND role.`is_deleted` = 0 + AND menu.`is_deleted` = 0; diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts index bd327a2..9788092 100644 --- a/src/admin/admin.module.ts +++ b/src/admin/admin.module.ts @@ -16,6 +16,7 @@ import { AdminMenuService } from './menu/admin-menu.service'; import { AdminRoleController } from './role/admin-role.controller'; import { AdminRole } from './role/admin-role.entity'; import { AdminRoleService } from './role/admin-role.service'; +import { NoticeModule } from './notice/notice.module'; import { SystemLogController } from './system-log/system-log.controller'; import { SystemLogService } from './system-log/system-log.service'; import { AdminTimezoneController } from './timezone/admin-timezone.controller'; @@ -38,6 +39,7 @@ import { WordpressModule } from '@/wordpress/wordpress.module'; ]), AdminAuthGuardModule, DictModule, + NoticeModule, MinioClientModule, WordpressModule, ], diff --git a/src/admin/notice/admin-notice.controller.ts b/src/admin/notice/admin-notice.controller.ts new file mode 100644 index 0000000..30061e6 --- /dev/null +++ b/src/admin/notice/admin-notice.controller.ts @@ -0,0 +1,89 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { + CurrentAdminUser, + vbenPage, + vbenSuccess, +} from '@/common'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { AdminUser } from '../user/admin-user.entity'; +import { + AdminNoticeBodyDto, + AdminNoticeQueryDto, + AdminNoticeUpdateDto, +} from './admin-notice.dto'; +import { AdminNoticeService } from './admin-notice.service'; + +@ApiTags('Admin - 站内信管理') +@Controller('system/notice') +@UseGuards(JwtAuthGuard) +export class AdminNoticeController { + constructor(private readonly noticeService: AdminNoticeService) {} + + @Get('list') + @ApiOperation({ + description: + '查询站内信列表:分页、标题/内容模糊检索、级别、状态、置顶状态、通知用户过滤', + summary: '查询站内信列表', + }) + @ApiQuery({ name: 'pageNo', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + async list(@Query() query: AdminNoticeQueryDto) { + const page = await this.noticeService.page(query); + return vbenPage(page.items, page.total); + } + + @Get('detail/:id') + @ApiOperation({ summary: '查询站内信详情' }) + async detail(@Param('id') id: string) { + return vbenSuccess(await this.noticeService.get(id)); + } + + @Post('save') + @ApiOperation({ summary: '新增站内信' }) + async save( + @Body() body: AdminNoticeBodyDto, + @CurrentAdminUser() currentUser: AdminUser, + ) { + return vbenSuccess( + await this.noticeService.create(body, currentUser?.id), + ); + } + + @Post('update') + @ApiOperation({ summary: '编辑站内信' }) + async update(@Body() body: AdminNoticeUpdateDto) { + return vbenSuccess(await this.noticeService.update(body)); + } + + @Delete(':id') + @ApiOperation({ summary: '删除站内信' }) + async remove(@Param('id') id: string) { + return vbenSuccess(await this.noticeService.remove(id)); + } + + @Post('toggle') + @ApiOperation({ summary: '启停站内信' }) + @ApiQuery({ name: 'id', type: String }) + @ApiQuery({ name: 'status', type: Number }) + async toggleStatus(@Query('id') id: string, @Query('status') status: string) { + return vbenSuccess(await this.noticeService.toggleStatus(id, status)); + } + + @Post('top') + @ApiOperation({ summary: '置顶/取消站内信' }) + @ApiQuery({ name: 'id', type: String }) + @ApiQuery({ name: 'isTop', type: Number }) + async toggleTop(@Query('id') id: string, @Query('isTop') isTop: string) { + return vbenSuccess(await this.noticeService.toggleTop(id, isTop)); + } +} diff --git a/src/admin/notice/admin-notice.dto.ts b/src/admin/notice/admin-notice.dto.ts new file mode 100644 index 0000000..25cd2ff --- /dev/null +++ b/src/admin/notice/admin-notice.dto.ts @@ -0,0 +1,122 @@ +import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger'; +import { FormatDateTime } from '@/common'; + +export class AdminNoticeDto { + @ApiProperty({ + example: '2041700000000300001', + }) + id: string; + + @ApiProperty({ + example: '系统公告', + }) + title: string; + + @ApiProperty({ + example: '站内公告正文内容', + }) + content: string; + + @ApiPropertyOptional({ + example: '站内公告摘要', + }) + summary?: string; + + @ApiProperty({ + example: 1, + }) + level: number; + + @ApiProperty({ + example: 1, + }) + status: number; + + @ApiProperty({ + example: false, + }) + isTop: boolean; + + @ApiPropertyOptional({ + example: '100001,100002', + }) + notifyUsers?: string; + + @ApiPropertyOptional({ + example: '2041700000000100001', + }) + createdBy?: string; + + @ApiProperty({ + example: false, + }) + isDeleted: boolean; + + @ApiPropertyOptional({ + example: '2026-06-03 20:00:00', + }) + @FormatDateTime() + createTime?: Date; + + @ApiPropertyOptional({ + example: '2026-06-03 20:00:00', + }) + @FormatDateTime() + updateTime?: Date; +} + +export class AdminNoticeQueryDto { + @ApiPropertyOptional() + page?: number | string; + + @ApiPropertyOptional() + pageNo?: number | string; + + @ApiPropertyOptional() + pageSize?: number | string; + + @ApiPropertyOptional() + keyword?: string; + + @ApiPropertyOptional() + level?: number | string; + + @ApiPropertyOptional() + status?: number | string; + + @ApiPropertyOptional() + isTop?: boolean | number | string; + + @ApiPropertyOptional() + notifyUsers?: string; +} + +export class AdminNoticeBodyDto { + @ApiProperty({ example: '系统公告' }) + title: string; + + @ApiProperty({ example: '站内公告正文内容' }) + content: string; + + @ApiPropertyOptional({ example: '站内公告摘要' }) + summary?: string; + + @ApiPropertyOptional({ example: 1 }) + level?: number | string; + + @ApiPropertyOptional({ example: 1 }) + status?: number | string; + + @ApiPropertyOptional({ example: false }) + isTop?: boolean | number | string; + + @ApiPropertyOptional({ example: '100001,100002' }) + notifyUsers?: string; +} + +export class AdminNoticeUpdateDto extends PartialType(AdminNoticeBodyDto) { + @ApiProperty({ + example: '2041700000000300001', + }) + id: string; +} diff --git a/src/admin/notice/admin-notice.entity.ts b/src/admin/notice/admin-notice.entity.ts new file mode 100644 index 0000000..2e3fcee --- /dev/null +++ b/src/admin/notice/admin-notice.entity.ts @@ -0,0 +1,114 @@ +import { + BeforeInsert, + Column, + CreateDateColumn, + Entity, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ensureSnowflakeId, FormatDateTime } from '@/common'; + +@Entity('admin_notice') +export class AdminNotice { + @ApiPropertyOptional() + @PrimaryColumn({ + type: 'bigint', + }) + id: string; + + @ApiProperty({ + example: '系统公告', + }) + @Column() + title: string; + + @ApiProperty({ + example: '这是站内信内容', + }) + @Column({ + type: 'longtext', + }) + content: string; + + @ApiPropertyOptional({ + example: '系统公告摘要', + }) + @Column({ + nullable: true, + type: 'text', + }) + summary?: string; + + @ApiProperty({ + example: 1, + }) + @Column({ + default: 1, + }) + level: number; + + @ApiProperty({ + example: 1, + }) + @Column({ + default: 1, + }) + status: number; + + @ApiProperty({ + example: false, + }) + @Column({ + default: false, + name: 'is_top', + type: 'boolean', + }) + isTop: boolean; + + @ApiPropertyOptional({ + example: '100001,100002', + }) + @Column({ + name: 'notify_users', + nullable: true, + type: 'text', + }) + notifyUsers?: string; + + @ApiPropertyOptional({ + example: '2041700000000100001', + }) + @Column({ + name: 'created_by', + nullable: true, + }) + createdBy?: string; + + @ApiProperty({ + example: false, + }) + @Column({ + default: false, + name: 'is_deleted', + }) + isDeleted: boolean; + + @CreateDateColumn({ + name: 'create_time', + }) + @FormatDateTime() + createTime: Date; + + @UpdateDateColumn({ + name: 'update_time', + }) + @FormatDateTime() + updateTime: Date; + + @BeforeInsert() + createId() { + ensureSnowflakeId(this); + } +} + diff --git a/src/admin/notice/admin-notice.service.ts b/src/admin/notice/admin-notice.service.ts new file mode 100644 index 0000000..e3a6212 --- /dev/null +++ b/src/admin/notice/admin-notice.service.ts @@ -0,0 +1,285 @@ +import { HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Brackets, Repository } from 'typeorm'; +import { + throwVbenError, + ToolsService, +} from '@/common'; +import { AdminNotice } from './admin-notice.entity'; +import type { + AdminNoticeBodyDto, + AdminNoticeQueryDto, + AdminNoticeUpdateDto, +} from './admin-notice.dto'; + +@Injectable() +export class AdminNoticeService { + constructor( + @InjectRepository(AdminNotice) + private readonly noticeRepository: Repository, + private readonly toolsService: ToolsService, + ) {} + + async page(query: AdminNoticeQueryDto = {}) { + const pageNo = this.toolsService.toPositiveNumber( + query.pageNo ?? query.page, + 1, + ); + const pageSize = this.toolsService.toPositiveNumber(query.pageSize, 20); + const builder = this.noticeRepository + .createQueryBuilder('notice') + .where('notice.isDeleted = :isDeleted', { isDeleted: false }); + + const keyword = this.toolsService.toTrimmedString(query.keyword); + if (keyword) { + builder.andWhere( + new Brackets((subBuilder) => { + subBuilder + .where('notice.title LIKE :keyword', { keyword: `%${keyword}%` }) + .orWhere('notice.content LIKE :keyword', { + keyword: `%${keyword}%`, + }) + .orWhere('notice.summary LIKE :keyword', { + keyword: `%${keyword}%`, + }); + }), + ); + } + + this.applyLikeFilter(builder, 'notifyUsers', query.notifyUsers); + + const level = this.normalizeLevel(query.level); + if (Number.isFinite(level)) { + builder.andWhere('notice.level = :level', { level }); + } + + const status = this.normalizeStatus(query.status); + if (Number.isFinite(status)) { + builder.andWhere('notice.status = :status', { status }); + } + + const isTop = this.normalizeBoolean(query.isTop); + if (isTop !== undefined) { + builder.andWhere('notice.isTop = :isTop', { isTop }); + } + + const [items, total] = await builder + .orderBy('notice.isTop', 'DESC') + .addOrderBy('notice.createTime', 'DESC') + .skip((pageNo - 1) * pageSize) + .take(pageSize) + .getManyAndCount(); + + return { + items: items.map((item) => this.serialize(item)), + total, + }; + } + + async get(id: string) { + const noticeId = this.toolsService.toTrimmedString(id); + if (!noticeId) throwVbenError('站内信ID不能为空', HttpStatus.BAD_REQUEST); + + const notice = await this.noticeRepository.findOne({ + where: { + id: noticeId, + isDeleted: false, + }, + }); + if (!notice) throwVbenError('站内信不存在', HttpStatus.BAD_REQUEST); + + return this.serialize(notice); + } + + async create(body: AdminNoticeBodyDto, createdBy?: string) { + const input = this.normalizeInput(body); + const notice = this.noticeRepository.create({ + ...input, + createdBy, + }); + + const saved = await this.noticeRepository.save(notice); + return saved.id; + } + + async update(body: AdminNoticeUpdateDto) { + const id = this.toolsService.toTrimmedString(body.id); + if (!id) throwVbenError('站内信ID不能为空', HttpStatus.BAD_REQUEST); + + const notice = await this.noticeRepository.findOne({ + where: { + id, + isDeleted: false, + }, + }); + if (!notice) throwVbenError('站内信不存在', HttpStatus.BAD_REQUEST); + + const input = this.normalizeInput({ + ...body, + content: body.content || notice.content, + title: body.title || notice.title, + }); + await this.noticeRepository.save( + this.noticeRepository.merge(notice, input), + ); + return null; + } + + async remove(id: string) { + const noticeId = this.toolsService.toTrimmedString(id); + if (!noticeId) throwVbenError('站内信ID不能为空', HttpStatus.BAD_REQUEST); + + const notice = await this.noticeRepository.findOne({ + where: { + id: noticeId, + isDeleted: false, + }, + }); + if (!notice) throwVbenError('站内信不存在', HttpStatus.BAD_REQUEST); + + await this.noticeRepository.update( + { + id: noticeId, + }, + { + isDeleted: true, + }, + ); + return null; + } + + async toggleStatus(id: string, status: number | string) { + const normalizedStatus = this.normalizeStatus(status); + if (Number.isNaN(normalizedStatus)) { + throwVbenError('status 参数不合法', HttpStatus.BAD_REQUEST); + } + const noticeId = this.toolsService.toTrimmedString(id); + if (!noticeId) throwVbenError('站内信ID不能为空', HttpStatus.BAD_REQUEST); + + const notice = await this.noticeRepository.findOne({ + where: { + id: noticeId, + isDeleted: false, + }, + }); + if (!notice) throwVbenError('站内信不存在', HttpStatus.BAD_REQUEST); + + await this.noticeRepository.save( + this.noticeRepository.merge(notice, { + status: normalizedStatus, + }), + ); + + return null; + } + + async toggleTop(id: string, isTop: boolean | number | string) { + const noticeId = this.toolsService.toTrimmedString(id); + if (!noticeId) throwVbenError('站内信ID不能为空', HttpStatus.BAD_REQUEST); + + const normalizedIsTop = this.normalizeBoolean(isTop); + if (normalizedIsTop === undefined) { + throwVbenError('isTop 参数不合法', HttpStatus.BAD_REQUEST); + } + + const notice = await this.noticeRepository.findOne({ + where: { + id: noticeId, + isDeleted: false, + }, + }); + if (!notice) throwVbenError('站内信不存在', HttpStatus.BAD_REQUEST); + + await this.noticeRepository.save( + this.noticeRepository.merge(notice, { + isTop: normalizedIsTop, + }), + ); + + return null; + } + + private applyLikeFilter( + builder: ReturnType['createQueryBuilder']>, + field: keyof Pick, + value?: string, + ) { + const normalizedValue = this.toolsService.toTrimmedString(value); + if (!normalizedValue) return; + + builder.andWhere(`notice.${field} LIKE :${field}`, { + [field]: `%${normalizedValue}%`, + }); + } + + private normalizeBoolean(value: boolean | number | string | undefined) { + if (value === undefined || value === null) return undefined; + if (value === true || value === 1 || `${value}` === '1') return true; + if (value === false || value === 0 || `${value}` === '0') return false; + return undefined; + } + + private normalizeLevel(level?: number | string) { + const normalizedLevel = Number(level); + return Number.isFinite(normalizedLevel) ? normalizedLevel : Number.NaN; + } + + private normalizeStatus(status?: number | string) { + const normalizedStatus = Number(status); + return normalizedStatus === 0 || normalizedStatus === 1 + ? normalizedStatus + : NaN; + } + + private normalizeNotifyUsers(notifyUsers?: string) { + const normalized = this.toolsService.toTrimmedString(notifyUsers); + if (!normalized) return undefined; + + const userIds = normalized + .split(',') + .map((item) => this.toolsService.toTrimmedString(item)) + .filter((item) => !!item) + .filter((item, index, array) => array.indexOf(item) === index); + + return userIds.length ? userIds.join(',') : undefined; + } + + private normalizeInput(body: AdminNoticeBodyDto) { + const title = this.toolsService.toTrimmedString(body.title); + const content = this.toolsService.toTrimmedString(body.content); + const summary = this.toolsService.toTrimmedString(body.summary) || undefined; + const level = this.normalizeLevel(body.level); + const status = this.normalizeStatus(body.status); + const isTop = this.normalizeBoolean(body.isTop); + + if (!title) throwVbenError('标题不能为空', HttpStatus.BAD_REQUEST); + if (!content) throwVbenError('内容不能为空', HttpStatus.BAD_REQUEST); + + return { + content, + level: Number.isFinite(level) ? level : 1, + notifyUsers: this.normalizeNotifyUsers(body.notifyUsers), + isTop: isTop ?? false, + status: status === 0 || status === 1 ? status : 1, + summary, + title, + }; + } + + private serialize(notice: AdminNotice) { + return { + content: notice.content, + createTime: notice.createTime, + createdBy: notice.createdBy, + id: notice.id, + isDeleted: notice.isDeleted, + isTop: notice.isTop, + level: notice.level, + notifyUsers: notice.notifyUsers, + status: notice.status, + summary: notice.summary, + title: notice.title, + updateTime: notice.updateTime, + }; + } +} diff --git a/src/admin/notice/notice.module.ts b/src/admin/notice/notice.module.ts new file mode 100644 index 0000000..0258725 --- /dev/null +++ b/src/admin/notice/notice.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AdminNoticeController } from './admin-notice.controller'; +import { AdminNotice } from './admin-notice.entity'; +import { AdminNoticeService } from './admin-notice.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([AdminNotice])], + controllers: [AdminNoticeController], + providers: [AdminNoticeService], +}) +export class NoticeModule {} +