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` 等字段。
|
||||
|
||||
### 系统站内信
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `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 被覆盖为空 |
|
||||
|
||||
@ -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',
|
||||
|
||||
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 { 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,
|
||||
],
|
||||
|
||||
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