From 3b75e265995cfcd95b0428c6269e8ff9670dfc87 Mon Sep 17 00:00:00 2001 From: xiaoquan <838115837@qq.com> Date: Tue, 2 Jun 2026 12:03:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B3=BB=E7=BB=9F=E5=AF=86=E9=92=A5?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=88=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=8A=A0=E5=AF=86=E5=AD=98=E5=82=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - 新增 system_secrets 表(AES-256-GCM 加密存储) - 新增 crypto.util.ts 加解密工具 - 新增 SecretService 共享服务(CRUD + 加解密) - 新增 AdminSecretController 管理端 API(仅超管) - API 返回值脱敏(*** + 最后4位) 前端(平台后台): - 新增系统密钥管理页面(按分组展示、CRUD 操作) - 侧边栏新增「系统密钥」菜单 管理员可在后台网页管理所有密钥,不再需要 SSH 到服务器改配置 Co-Authored-By: Claude Opus 4.8 --- apps/platform-admin/src/App.tsx | 2 + apps/platform-admin/src/api/secrets.ts | 31 ++ .../platform-admin/src/layouts/MainLayout.tsx | 2 + .../src/pages/SystemSecrets.tsx | 294 ++++++++++++++++++ apps/server/src/common/utils/crypto.util.ts | 64 ++++ .../src/entities/system-secret.entity.ts | 34 ++ apps/server/src/modules/admin/admin.module.ts | 2 + .../modules/admin/secret/dto/secret.dto.ts | 69 ++++ .../modules/admin/secret/secret.controller.ts | 64 ++++ .../src/modules/admin/secret/secret.module.ts | 9 + .../modules/shared/secret/secret.module.ts | 11 + .../modules/shared/secret/secret.service.ts | 125 ++++++++ .../src/modules/shared/shared.module.ts | 3 + database/migrations/003_system_secrets.sql | 13 + 14 files changed, 723 insertions(+) create mode 100644 apps/platform-admin/src/api/secrets.ts create mode 100644 apps/platform-admin/src/pages/SystemSecrets.tsx create mode 100644 apps/server/src/common/utils/crypto.util.ts create mode 100644 apps/server/src/entities/system-secret.entity.ts create mode 100644 apps/server/src/modules/admin/secret/dto/secret.dto.ts create mode 100644 apps/server/src/modules/admin/secret/secret.controller.ts create mode 100644 apps/server/src/modules/admin/secret/secret.module.ts create mode 100644 apps/server/src/modules/shared/secret/secret.module.ts create mode 100644 apps/server/src/modules/shared/secret/secret.service.ts create mode 100644 database/migrations/003_system_secrets.sql diff --git a/apps/platform-admin/src/App.tsx b/apps/platform-admin/src/App.tsx index a07690a..3cf413b 100644 --- a/apps/platform-admin/src/App.tsx +++ b/apps/platform-admin/src/App.tsx @@ -17,6 +17,7 @@ import CouponList from '@/pages/coupon/CouponList'; import CouponForm from '@/pages/coupon/CouponForm'; import Promotion from '@/pages/Promotion'; import SystemSettings from '@/pages/SystemSettings'; +import SystemSecrets from '@/pages/SystemSecrets'; import FinanceSettlements from '@/pages/finance/Settlements'; import FinanceWithdrawals from '@/pages/finance/Withdrawals'; import FinanceEarnings from '@/pages/finance/Earnings'; @@ -74,6 +75,7 @@ const App: React.FC = () => ( } /> } /> } /> + } /> diff --git a/apps/platform-admin/src/api/secrets.ts b/apps/platform-admin/src/api/secrets.ts new file mode 100644 index 0000000..dfb8c3a --- /dev/null +++ b/apps/platform-admin/src/api/secrets.ts @@ -0,0 +1,31 @@ +import request from '@/utils/request'; + +// 获取密钥列表 +export function getSecrets(params?: any) { + return request.get('/api/admin/secrets', { params }); +} + +// 获取密钥详情 +export function getSecret(id: number) { + return request.get(`/api/admin/secrets/${id}`); +} + +// 新增密钥 +export function createSecret(data: any) { + return request.post('/api/admin/secrets', data); +} + +// 更新密钥 +export function updateSecret(id: number, data: any) { + return request.put(`/api/admin/secrets/${id}`, data); +} + +// 删除密钥 +export function deleteSecret(id: number) { + return request.delete(`/api/admin/secrets/${id}`); +} + +// 获取分组列表 +export function getSecretGroups() { + return request.get('/api/admin/secrets/group/list'); +} diff --git a/apps/platform-admin/src/layouts/MainLayout.tsx b/apps/platform-admin/src/layouts/MainLayout.tsx index 4e29b6e..efec858 100644 --- a/apps/platform-admin/src/layouts/MainLayout.tsx +++ b/apps/platform-admin/src/layouts/MainLayout.tsx @@ -22,6 +22,7 @@ import { TagOutlined, CreditCardOutlined, SafetyOutlined, + KeyOutlined, } from '@ant-design/icons'; import { useAuthStore } from '@/store/auth'; @@ -56,6 +57,7 @@ const menuItems = [ { key: '/promotions', icon: , label: '推广管理' }, { key: '/admins', icon: , label: '管理员管理' }, { key: '/settings', icon: , label: '系统设置' }, + { key: '/system-secrets', icon: , label: '系统密钥' }, ]; const MainLayout: React.FC = () => { diff --git a/apps/platform-admin/src/pages/SystemSecrets.tsx b/apps/platform-admin/src/pages/SystemSecrets.tsx new file mode 100644 index 0000000..47668da --- /dev/null +++ b/apps/platform-admin/src/pages/SystemSecrets.tsx @@ -0,0 +1,294 @@ +import React, { useState, useEffect } from 'react'; +import { Table, Button, Modal, Form, Input, Select, message, Popconfirm, Tag, Card } from 'antd'; +import { PlusOutlined, KeyOutlined } from '@ant-design/icons'; +import { getSecrets, createSecret, updateSecret, deleteSecret } from '@/api/secrets'; + +const { Search } = Input; + +// 分组中文映射 +const GROUP_LABELS: Record = { + database: '数据库', + jwt: 'JWT', + sms: '短信', + wechat: '微信', + wechat_pay: '微信支付', + alipay: '支付宝', + default: '其他', +}; + +// 分组颜色 +const GROUP_COLORS: Record = { + database: 'red', + jwt: 'orange', + sms: 'green', + wechat: 'blue', + wechat_pay: 'cyan', + alipay: 'purple', + default: 'default', +}; + +// 环境映射 +const ENV_LABELS: Record = { + all: '通用', + prod: '生产', + test: '测试', +}; + +const ENV_COLORS: Record = { + all: 'default', + prod: 'red', + test: 'blue', +}; + +const SystemSecrets: React.FC = () => { + const [secrets, setSecrets] = useState([]); + const [loading, setLoading] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [editingSecret, setEditingSecret] = useState(null); + const [form] = Form.useForm(); + const [filterGroup, setFilterGroup] = useState(); + + const fetchData = async () => { + setLoading(true); + try { + const params: any = {}; + if (filterGroup) params.groupName = filterGroup; + const res = await getSecrets(params); + setSecrets(res.data || []); + } catch { + message.error('获取密钥列表失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, [filterGroup]); + + const handleAdd = () => { + setEditingSecret(null); + form.resetFields(); + setModalVisible(true); + }; + + const handleEdit = (record: any) => { + setEditingSecret(record); + form.setFieldsValue({ + secretKey: record.secretKey, + description: record.description, + groupName: record.groupName, + env: record.env, + secretValue: '', // 编辑时不回显原值 + }); + setModalVisible(true); + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + if (editingSecret) { + // 更新时如果没有填新值则不更新 + const updateData: any = { + description: values.description, + groupName: values.groupName, + env: values.env, + }; + if (values.secretValue) { + updateData.secretValue = values.secretValue; + } + await updateSecret(editingSecret.id, updateData); + message.success('密钥已更新'); + } else { + await createSecret(values); + message.success('密钥已创建'); + } + setModalVisible(false); + fetchData(); + } catch { + message.error('操作失败'); + } + }; + + const handleDelete = async (id: number) => { + try { + await deleteSecret(id); + message.success('密钥已删除'); + fetchData(); + } catch { + message.error('删除失败'); + } + }; + + const columns = [ + { + title: '密钥名', + dataIndex: 'secretKey', + key: 'secretKey', + width: 220, + render: (text: string) => {text}, + }, + { + title: '当前值', + dataIndex: 'maskedValue', + key: 'maskedValue', + width: 140, + render: (text: string) => {text}, + }, + { + title: '说明', + dataIndex: 'description', + key: 'description', + ellipsis: true, + }, + { + title: '分组', + dataIndex: 'groupName', + key: 'groupName', + width: 100, + render: (text: string) => ( + + {GROUP_LABELS[text] || text} + + ), + }, + { + title: '环境', + dataIndex: 'env', + key: 'env', + width: 80, + render: (text: string) => ( + {ENV_LABELS[text]} + ), + }, + { + title: '更新时间', + dataIndex: 'updatedAt', + key: 'updatedAt', + width: 170, + render: (text: string) => text?.replace('T', ' ').slice(0, 19), + }, + { + title: '操作', + key: 'action', + width: 120, + render: (_: any, record: any) => ( + + + handleDelete(record.id)} + okText="确定" + cancelText="取消" + > + + + + ), + }, + ]; + + return ( +
+ +
+
+ +
+ +
+ + + + + setModalVisible(false)} + okText="保存" + cancelText="取消" + width={520} + > +
+ + + + + + + + + + + + +
+ + + + + + + +
+ +
+ + ); +}; + +export default SystemSecrets; diff --git a/apps/server/src/common/utils/crypto.util.ts b/apps/server/src/common/utils/crypto.util.ts new file mode 100644 index 0000000..a71496e --- /dev/null +++ b/apps/server/src/common/utils/crypto.util.ts @@ -0,0 +1,64 @@ +import * as crypto from 'crypto'; + +const ALGORITHM = 'aes-256-gcm'; +const IV_LENGTH = 12; +const AUTH_TAG_LENGTH = 16; + +/** + * 获取加密密钥 + */ +function getEncryptionKey(): Buffer { + const key = process.env.ENCRYPTION_KEY; + if (!key) { + throw new Error('ENCRYPTION_KEY 环境变量未配置'); + } + return Buffer.from(key, 'hex'); +} + +/** + * AES-256-GCM 加密 + * 返回格式:iv:authTag:ciphertext(均为 base64) + */ +export function encrypt(plaintext: string): string { + const key = getEncryptionKey(); + const iv = crypto.randomBytes(IV_LENGTH); + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + + let encrypted = cipher.update(plaintext, 'utf8', 'base64'); + encrypted += cipher.final('base64'); + const authTag = cipher.getAuthTag(); + + return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted}`; +} + +/** + * AES-256-GCM 解密 + */ +export function decrypt(encryptedValue: string): string { + const key = getEncryptionKey(); + const parts = encryptedValue.split(':'); + if (parts.length !== 3) { + throw new Error('密文格式错误'); + } + + const iv = Buffer.from(parts[0], 'base64'); + const authTag = Buffer.from(parts[1], 'base64'); + const ciphertext = parts[2]; + + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(authTag); + + let decrypted = decipher.update(ciphertext, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; +} + +/** + * 脱敏显示:保留最后4位,前面用 *** 替代 + */ +export function maskValue(value: string): string { + if (!value || value.length <= 4) { + return '****'; + } + return '***' + value.slice(-4); +} diff --git a/apps/server/src/entities/system-secret.entity.ts b/apps/server/src/entities/system-secret.entity.ts new file mode 100644 index 0000000..937be9c --- /dev/null +++ b/apps/server/src/entities/system-secret.entity.ts @@ -0,0 +1,34 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('system_secrets') +export class SystemSecret { + @PrimaryGeneratedColumn({ type: 'bigint', unsigned: true }) + id: number; + + @Column({ name: 'secret_key', length: 100, comment: '密钥名' }) + secretKey: string; + + @Column({ name: 'secret_value', type: 'text', comment: '加密后的值' }) + secretValue: string; + + @Column({ name: 'description', length: 200, nullable: true, comment: '说明' }) + description: string; + + @Column({ name: 'group_name', length: 50, default: 'default', comment: '分组' }) + groupName: string; + + @Column({ type: 'enum', enum: ['all', 'prod', 'test'], default: 'all', comment: '适用环境' }) + env: 'all' | 'prod' | 'test'; + + @CreateDateColumn({ name: 'created_at', comment: '创建时间' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', comment: '更新时间' }) + updatedAt: Date; +} diff --git a/apps/server/src/modules/admin/admin.module.ts b/apps/server/src/modules/admin/admin.module.ts index e742a14..08f7055 100644 --- a/apps/server/src/modules/admin/admin.module.ts +++ b/apps/server/src/modules/admin/admin.module.ts @@ -11,6 +11,7 @@ import { AdminConfigModule } from './config/config.module'; import { AdminFinanceModule } from './finance/finance.module'; import { AdminWebsiteModule } from './website/website.module'; import { AdminManageModule } from './admin-manage/admin-manage.module'; +import { AdminSecretModule } from './secret/secret.module'; @Module({ imports: [ @@ -26,6 +27,7 @@ import { AdminManageModule } from './admin-manage/admin-manage.module'; AdminFinanceModule, AdminWebsiteModule, AdminManageModule, + AdminSecretModule, ], }) export class AdminModule {} diff --git a/apps/server/src/modules/admin/secret/dto/secret.dto.ts b/apps/server/src/modules/admin/secret/dto/secret.dto.ts new file mode 100644 index 0000000..7ded33d --- /dev/null +++ b/apps/server/src/modules/admin/secret/dto/secret.dto.ts @@ -0,0 +1,69 @@ +import { IsString, IsOptional, IsEnum, MaxLength, MinLength } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateSecretDto { + @ApiProperty({ description: '密钥名', example: 'WECHAT_SECRET' }) + @IsString() + @MinLength(1) + @MaxLength(100) + secretKey: string; + + @ApiProperty({ description: '密钥值(明文,服务端加密存储)' }) + @IsString() + @MinLength(1) + secretValue: string; + + @ApiPropertyOptional({ description: '说明', example: '微信小程序 Secret' }) + @IsString() + @IsOptional() + @MaxLength(200) + description?: string; + + @ApiPropertyOptional({ description: '分组', example: 'wechat', default: 'default' }) + @IsString() + @IsOptional() + @MaxLength(50) + groupName?: string; + + @ApiPropertyOptional({ description: '适用环境', enum: ['all', 'prod', 'test'], default: 'all' }) + @IsEnum(['all', 'prod', 'test']) + @IsOptional() + env?: string; +} + +export class UpdateSecretDto { + @ApiPropertyOptional({ description: '密钥值(明文,服务端加密存储)' }) + @IsString() + @IsOptional() + @MinLength(1) + secretValue?: string; + + @ApiPropertyOptional({ description: '说明' }) + @IsString() + @IsOptional() + @MaxLength(200) + description?: string; + + @ApiPropertyOptional({ description: '分组' }) + @IsString() + @IsOptional() + @MaxLength(50) + groupName?: string; + + @ApiPropertyOptional({ description: '适用环境', enum: ['all', 'prod', 'test'] }) + @IsEnum(['all', 'prod', 'test']) + @IsOptional() + env?: string; +} + +export class QuerySecretDto { + @ApiPropertyOptional({ description: '按分组筛选' }) + @IsString() + @IsOptional() + groupName?: string; + + @ApiPropertyOptional({ description: '按环境筛选', enum: ['all', 'prod', 'test'] }) + @IsEnum(['all', 'prod', 'test']) + @IsOptional() + env?: string; +} diff --git a/apps/server/src/modules/admin/secret/secret.controller.ts b/apps/server/src/modules/admin/secret/secret.controller.ts new file mode 100644 index 0000000..febc346 --- /dev/null +++ b/apps/server/src/modules/admin/secret/secret.controller.ts @@ -0,0 +1,64 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Param, + Body, + Query, + ParseIntPipe, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '@/common/guards/jwt-auth.guard'; +import { RolesGuard } from '@/common/guards/roles.guard'; +import { Roles } from '@/common/decorators/roles.decorator'; +import { AdminRole } from '@/entities/admin.entity'; +import { SecretService } from '@/modules/shared/secret/secret.service'; +import { CreateSecretDto, UpdateSecretDto, QuerySecretDto } from './dto/secret.dto'; + +@ApiTags('管理端-系统密钥') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles(AdminRole.SUPER_ADMIN) +@Controller('admin/secrets') +export class AdminSecretController { + constructor(private readonly secretService: SecretService) {} + + @Get('group/list') + @ApiOperation({ summary: '获取密钥分组列表' }) + getGroups() { + return this.secretService.getGroups(); + } + + @Get() + @ApiOperation({ summary: '获取密钥列表(值脱敏)' }) + findAll(@Query() query: QuerySecretDto) { + return this.secretService.findAll(query); + } + + @Get(':id') + @ApiOperation({ summary: '获取密钥详情(值脱敏)' }) + findOne(@Param('id', ParseIntPipe) id: number) { + return this.secretService.findOne(id); + } + + @Post() + @ApiOperation({ summary: '新增密钥' }) + create(@Body() dto: CreateSecretDto) { + return this.secretService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新密钥' }) + update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateSecretDto) { + return this.secretService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除密钥' }) + remove(@Param('id', ParseIntPipe) id: number) { + return this.secretService.remove(id); + } +} diff --git a/apps/server/src/modules/admin/secret/secret.module.ts b/apps/server/src/modules/admin/secret/secret.module.ts new file mode 100644 index 0000000..ae3bdd6 --- /dev/null +++ b/apps/server/src/modules/admin/secret/secret.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { SecretModule } from '@/modules/shared/secret/secret.module'; +import { AdminSecretController } from './secret.controller'; + +@Module({ + imports: [SecretModule], + controllers: [AdminSecretController], +}) +export class AdminSecretModule {} diff --git a/apps/server/src/modules/shared/secret/secret.module.ts b/apps/server/src/modules/shared/secret/secret.module.ts new file mode 100644 index 0000000..0f95402 --- /dev/null +++ b/apps/server/src/modules/shared/secret/secret.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SystemSecret } from '@/entities/system-secret.entity'; +import { SecretService } from './secret.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([SystemSecret])], + providers: [SecretService], + exports: [SecretService], +}) +export class SecretModule {} diff --git a/apps/server/src/modules/shared/secret/secret.service.ts b/apps/server/src/modules/shared/secret/secret.service.ts new file mode 100644 index 0000000..4db9926 --- /dev/null +++ b/apps/server/src/modules/shared/secret/secret.service.ts @@ -0,0 +1,125 @@ +import { Injectable, Logger, NotFoundException, ConflictException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { SystemSecret } from '@/entities/system-secret.entity'; +import { encrypt, decrypt, maskValue } from '@/common/utils/crypto.util'; + +@Injectable() +export class SecretService { + private readonly logger = new Logger(SecretService.name); + + constructor( + @InjectRepository(SystemSecret) + private readonly secretRepo: Repository, + ) {} + + /** + * 获取所有密钥列表(脱敏) + */ + async findAll(query?: { groupName?: string; env?: string }) { + const where: any = {}; + if (query?.groupName) where.groupName = query.groupName; + if (query?.env) where.env = query.env; + + const secrets = await this.secretRepo.find({ where, order: { groupName: 'ASC', secretKey: 'ASC' } }); + return secrets.map(s => ({ + id: s.id, + secretKey: s.secretKey, + maskedValue: maskValue(decrypt(s.secretValue)), + description: s.description, + groupName: s.groupName, + env: s.env, + createdAt: s.createdAt, + updatedAt: s.updatedAt, + })); + } + + /** + * 获取单个密钥详情(脱敏) + */ + async findOne(id: number) { + const secret = await this.secretRepo.findOne({ where: { id } }); + if (!secret) throw new NotFoundException('密钥不存在'); + return { + id: secret.id, + secretKey: secret.secretKey, + maskedValue: maskValue(decrypt(secret.secretValue)), + description: secret.description, + groupName: secret.groupName, + env: secret.env, + createdAt: secret.createdAt, + updatedAt: secret.updatedAt, + }; + } + + /** + * 获取解密后的密钥值(供其他服务调用) + */ + async getDecryptedValue(secretKey: string, env: string = 'all'): Promise { + const secret = await this.secretRepo.findOne({ + where: [ + { secretKey, env: 'all' }, + { secretKey, env }, + ], + }); + if (!secret) return null; + return decrypt(secret.secretValue); + } + + /** + * 创建密钥 + */ + async create(data: { secretKey: string; secretValue: string; description?: string; groupName?: string; env?: string }) { + // 检查是否已存在 + const existing = await this.secretRepo.findOne({ + where: { secretKey: data.secretKey, env: (data.env as any) || 'all' }, + }); + if (existing) throw new ConflictException(`密钥 ${data.secretKey} 已存在`); + + const secret = this.secretRepo.create({ + secretKey: data.secretKey, + secretValue: encrypt(data.secretValue), + description: data.description, + groupName: data.groupName || 'default', + env: data.env || 'all', + }); + return this.secretRepo.save(secret); + } + + /** + * 更新密钥 + */ + async update(id: number, data: { secretValue?: string; description?: string; groupName?: string; env?: string }) { + const secret = await this.secretRepo.findOne({ where: { id } }); + if (!secret) throw new NotFoundException('密钥不存在'); + + if (data.secretValue !== undefined) { + secret.secretValue = encrypt(data.secretValue); + } + if (data.description !== undefined) secret.description = data.description; + if (data.groupName !== undefined) secret.groupName = data.groupName; + if (data.env !== undefined) secret.env = data.env as any; + + return this.secretRepo.save(secret); + } + + /** + * 删除密钥 + */ + async remove(id: number) { + const secret = await this.secretRepo.findOne({ where: { id } }); + if (!secret) throw new NotFoundException('密钥不存在'); + await this.secretRepo.remove(secret); + } + + /** + * 获取所有分组 + */ + async getGroups() { + const result = await this.secretRepo + .createQueryBuilder('s') + .select('DISTINCT s.groupName', 'groupName') + .getRawMany(); + return result.map(r => r.groupName); + } +} diff --git a/apps/server/src/modules/shared/shared.module.ts b/apps/server/src/modules/shared/shared.module.ts index 5e554ab..6bf61d2 100644 --- a/apps/server/src/modules/shared/shared.module.ts +++ b/apps/server/src/modules/shared/shared.module.ts @@ -3,6 +3,7 @@ import { UploadModule } from './upload/upload.module'; import { FinanceModule } from './finance/finance.module'; import { CouponModule } from './coupon/coupon.module'; import { ConfigModule } from './config/config.module'; +import { SecretModule } from './secret/secret.module'; @Module({ imports: [ @@ -10,12 +11,14 @@ import { ConfigModule } from './config/config.module'; FinanceModule, CouponModule, ConfigModule, + SecretModule, ], exports: [ UploadModule, FinanceModule, CouponModule, ConfigModule, + SecretModule, ], }) export class SharedModule {} diff --git a/database/migrations/003_system_secrets.sql b/database/migrations/003_system_secrets.sql new file mode 100644 index 0000000..6eb816f --- /dev/null +++ b/database/migrations/003_system_secrets.sql @@ -0,0 +1,13 @@ +-- 系统密钥管理表 +CREATE TABLE IF NOT EXISTS `system_secrets` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `secret_key` varchar(100) NOT NULL COMMENT '密钥名', + `secret_value` text NOT NULL COMMENT '加密后的值', + `description` varchar(200) DEFAULT NULL COMMENT '说明', + `group_name` varchar(50) DEFAULT 'default' COMMENT '分组', + `env` enum('all','prod','test') DEFAULT 'all' COMMENT '适用环境', + `created_at` datetime(6) DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间', + `updated_at` datetime(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_secret_key_env` (`secret_key`, `env`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统密钥管理';