feat: 补齐站内信菜单与权限同步能力
This commit is contained in:
parent
5f0351132d
commit
eab71cf0b6
13
API.md
13
API.md
@ -172,6 +172,18 @@ Admin、Component、Dict、MinIO、Blog 管理、WordPress 管理和 QQBot 管
|
|||||||
|
|
||||||
日志行包含 `timestamp`、`level`、`message`、`method`、`path`、`statusCode`、`durationMs`、`requestId`、`raw` 等字段。
|
日志行包含 `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 本地内容
|
||||||
|
|
||||||
`/blog/*` 是本地博客内容能力,供 `Vue/kt-blog-web` 和 Admin 博客管理使用。
|
`/blog/*` 是本地博客内容能力,供 `Vue/kt-blog-web` 和 Admin 博客管理使用。
|
||||||
@ -422,6 +434,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
|||||||
| `sql/blog-menu.sql` | 初始化 Blog 管理菜单 |
|
| `sql/blog-menu.sql` | 初始化 Blog 管理菜单 |
|
||||||
| `sql/qqbot-init.sql` | 初始化 QQBot 表、插件命令和字典 |
|
| `sql/qqbot-init.sql` | 初始化 QQBot 表、插件命令和字典 |
|
||||||
| `sql/system-log-menu.sql` | 初始化系统日志菜单和权限 |
|
| `sql/system-log-menu.sql` | 初始化系统日志菜单和权限 |
|
||||||
|
| `sql/system-notice-menu.sql` | 初始化系统站内信表与菜单权限 |
|
||||||
| `sql/migrate-dict-to-admin-dict.sql` | 旧 `dict` 迁移到 `admin_dict` |
|
| `sql/migrate-dict-to-admin-dict.sql` | 旧 `dict` 迁移到 `admin_dict` |
|
||||||
| `sql/migrate-component-to-admin-component.sql` | 旧 `component` 迁移到 `admin_component` |
|
| `sql/migrate-component-to-admin-component.sql` | 旧 `component` 迁移到 `admin_component` |
|
||||||
| `sql/fix-admin-menu-meta.sql` | 修复菜单 meta 被覆盖为空 |
|
| `sql/fix-admin-menu-meta.sql` | 修复菜单 meta 被覆盖为空 |
|
||||||
|
|||||||
@ -21,6 +21,10 @@ SET `meta` = CASE `name`
|
|||||||
WHEN 'SystemDeptCreate' THEN '{"title":"common.create"}'
|
WHEN 'SystemDeptCreate' THEN '{"title":"common.create"}'
|
||||||
WHEN 'SystemDeptEdit' THEN '{"title":"common.edit"}'
|
WHEN 'SystemDeptEdit' THEN '{"title":"common.edit"}'
|
||||||
WHEN 'SystemDeptDelete' THEN '{"title":"common.delete"}'
|
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 '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 '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"}'
|
WHEN 'VbenDocument' THEN '{"icon":"carbon:book","iframeSrc":"https://doc.vben.pro","title":"demos.vben.document"}'
|
||||||
@ -45,6 +49,10 @@ WHERE `name` IN (
|
|||||||
'SystemDeptCreate',
|
'SystemDeptCreate',
|
||||||
'SystemDeptEdit',
|
'SystemDeptEdit',
|
||||||
'SystemDeptDelete',
|
'SystemDeptDelete',
|
||||||
|
'SystemNotice',
|
||||||
|
'SystemNoticeCreate',
|
||||||
|
'SystemNoticeEdit',
|
||||||
|
'SystemNoticeDelete',
|
||||||
'SystemLog',
|
'SystemLog',
|
||||||
'Project',
|
'Project',
|
||||||
'VbenDocument',
|
'VbenDocument',
|
||||||
|
|||||||
99
sql/system-notice-menu.sql
Normal file
99
sql/system-notice-menu.sql
Normal file
@ -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;
|
||||||
@ -16,6 +16,7 @@ import { AdminMenuService } from './menu/admin-menu.service';
|
|||||||
import { AdminRoleController } from './role/admin-role.controller';
|
import { AdminRoleController } from './role/admin-role.controller';
|
||||||
import { AdminRole } from './role/admin-role.entity';
|
import { AdminRole } from './role/admin-role.entity';
|
||||||
import { AdminRoleService } from './role/admin-role.service';
|
import { AdminRoleService } from './role/admin-role.service';
|
||||||
|
import { NoticeModule } from './notice/notice.module';
|
||||||
import { SystemLogController } from './system-log/system-log.controller';
|
import { SystemLogController } from './system-log/system-log.controller';
|
||||||
import { SystemLogService } from './system-log/system-log.service';
|
import { SystemLogService } from './system-log/system-log.service';
|
||||||
import { AdminTimezoneController } from './timezone/admin-timezone.controller';
|
import { AdminTimezoneController } from './timezone/admin-timezone.controller';
|
||||||
@ -38,6 +39,7 @@ import { WordpressModule } from '@/wordpress/wordpress.module';
|
|||||||
]),
|
]),
|
||||||
AdminAuthGuardModule,
|
AdminAuthGuardModule,
|
||||||
DictModule,
|
DictModule,
|
||||||
|
NoticeModule,
|
||||||
MinioClientModule,
|
MinioClientModule,
|
||||||
WordpressModule,
|
WordpressModule,
|
||||||
],
|
],
|
||||||
|
|||||||
89
src/admin/notice/admin-notice.controller.ts
Normal file
89
src/admin/notice/admin-notice.controller.ts
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
122
src/admin/notice/admin-notice.dto.ts
Normal file
122
src/admin/notice/admin-notice.dto.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
114
src/admin/notice/admin-notice.entity.ts
Normal file
114
src/admin/notice/admin-notice.entity.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
285
src/admin/notice/admin-notice.service.ts
Normal file
285
src/admin/notice/admin-notice.service.ts
Normal file
@ -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<AdminNotice>,
|
||||||
|
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<Repository<AdminNotice>['createQueryBuilder']>,
|
||||||
|
field: keyof Pick<AdminNotice, 'notifyUsers'>,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/admin/notice/notice.module.ts
Normal file
13
src/admin/notice/notice.module.ts
Normal file
@ -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 {}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user