feat: 初始化 ChatOA 企业协作 OA 系统
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SeedService } from './seed.service';
|
||||
import { ChatModule } from '../chat/chat.module';
|
||||
|
||||
@Module({
|
||||
imports: [ChatModule],
|
||||
providers: [SeedService],
|
||||
})
|
||||
export class SeedModule {}
|
||||
@@ -0,0 +1,367 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { StorageService } from '../common/storage/storage.service';
|
||||
import { User } from '../auth/auth.service';
|
||||
import { conversationKey, ChatService } from '../chat/chat.service';
|
||||
|
||||
interface SeedUserDef {
|
||||
username: string;
|
||||
nickname: string;
|
||||
department: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const SEED_USERS: SeedUserDef[] = [
|
||||
{ username: 'demo', nickname: '演示用户', department: '产品部', title: '产品经理' },
|
||||
{ username: 'alice', nickname: '小艾', department: '行政部', title: '行政专员' },
|
||||
{ username: 'bob', nickname: '小波', department: '研发部', title: '前端工程师' },
|
||||
{ username: 'carol', nickname: '小卡', department: '财务部', title: '会计' },
|
||||
];
|
||||
|
||||
const SEED_PASSWORD = '123456';
|
||||
|
||||
@Injectable()
|
||||
export class SeedService implements OnModuleInit {
|
||||
private readonly logger = new Logger(SeedService.name);
|
||||
|
||||
constructor(
|
||||
private readonly storage: StorageService,
|
||||
private readonly chat: ChatService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
const users = this.storage.collection<User>('users');
|
||||
const count = await users.count();
|
||||
// 已存在数据则不重复初始化(保留用户修改/新增的数据)
|
||||
if (count > 0) {
|
||||
this.logger.log('seed skipped: users already exist');
|
||||
return;
|
||||
}
|
||||
this.logger.log('seeding demo data...');
|
||||
const hash = bcrypt.hashSync(SEED_PASSWORD, 10);
|
||||
const created: User[] = [];
|
||||
for (const def of SEED_USERS) {
|
||||
const u = await users.insertOne({
|
||||
username: def.username,
|
||||
passwordHash: hash,
|
||||
nickname: def.nickname,
|
||||
department: def.department,
|
||||
title: def.title,
|
||||
});
|
||||
created.push(u);
|
||||
}
|
||||
await this.seedChats(created);
|
||||
await this.seedSchedule(created);
|
||||
await this.seedForms(created);
|
||||
await this.seedKnowledge(created);
|
||||
this.logger.log('seed done');
|
||||
}
|
||||
|
||||
private async seedChats(users: User[]) {
|
||||
const demo = users.find((u) => u.username === 'demo')!;
|
||||
const others = users.filter((u) => u.username !== 'demo');
|
||||
const seedMessages: Array<{
|
||||
from: string;
|
||||
to: string;
|
||||
type: 'text' | 'image';
|
||||
content: string;
|
||||
}> = [];
|
||||
const convs: Array<{ a: string; b: string; updatedAt: number }> = [];
|
||||
|
||||
// 与 alice 的对话
|
||||
const alice = users.find((u) => u.username === 'alice')!;
|
||||
seedMessages.push(
|
||||
{ from: alice.id, to: demo.id, type: 'text', content: '早上好!今天 10 点开周会,别忘了哦~' },
|
||||
{ from: demo.id, to: alice.id, type: 'text', content: '收到,会议室订好了吗?' },
|
||||
{ from: alice.id, to: demo.id, type: 'text', content: '已订 3 楼小会议室,投影正常。' },
|
||||
);
|
||||
convs.push({ a: demo.id, b: alice.id, updatedAt: Date.now() - 3600_000 });
|
||||
|
||||
// 与 bob 的对话
|
||||
const bob = users.find((u) => u.username === 'bob')!;
|
||||
seedMessages.push(
|
||||
{ from: bob.id, to: demo.id, type: 'text', content: 'ChatOA 的消息模块联调完成,可以体验了 🎉' },
|
||||
{ from: demo.id, to: bob.id, type: 'text', content: '辛苦了!图片消息也测了吗?' },
|
||||
{ from: bob.id, to: demo.id, type: 'text', content: '测了,发送一张示意图你看看:' },
|
||||
);
|
||||
convs.push({ a: demo.id, b: bob.id, updatedAt: Date.now() - 10 * 60_000 });
|
||||
|
||||
for (const m of seedMessages) {
|
||||
await this.chat.sendMessage(m.from, {
|
||||
toUserId: m.to,
|
||||
type: m.type,
|
||||
content: m.content,
|
||||
});
|
||||
}
|
||||
// 修正会话更新时间,使列表顺序符合演示
|
||||
for (const c of convs) {
|
||||
const col = this.storage.collection('conversations');
|
||||
await col.updateById(conversationKey(c.a, c.b), { updatedAt: c.updatedAt });
|
||||
}
|
||||
void others;
|
||||
}
|
||||
|
||||
private async seedSchedule(users: User[]) {
|
||||
const demo = users.find((u) => u.username === 'demo')!;
|
||||
const schedule = this.storage.collection('schedule_items');
|
||||
const day = 24 * 3600_000;
|
||||
const now = Date.now();
|
||||
await schedule.insertOne({
|
||||
ownerId: demo.id,
|
||||
title: '周例会',
|
||||
allDay: false,
|
||||
startAt: now + 2 * 3600_000,
|
||||
endAt: now + 2.5 * 3600_000,
|
||||
location: '3 楼小会议室',
|
||||
notes: '各部门同步本周进展',
|
||||
});
|
||||
const todayStart = new Date();
|
||||
todayStart.setHours(0, 0, 0, 0);
|
||||
await schedule.insertOne({
|
||||
ownerId: demo.id,
|
||||
title: '产品评审会',
|
||||
allDay: false,
|
||||
startAt: todayStart.getTime() + 15 * 3600_000,
|
||||
endAt: todayStart.getTime() + 16 * 3600_000,
|
||||
location: '线上会议',
|
||||
});
|
||||
await schedule.insertOne({
|
||||
ownerId: demo.id,
|
||||
title: '提交季度报销',
|
||||
allDay: true,
|
||||
startAt: todayStart.getTime() + 2 * day,
|
||||
endAt: todayStart.getTime() + 2 * day + day - 1,
|
||||
notes: '记得整理发票',
|
||||
});
|
||||
}
|
||||
|
||||
private async seedForms(users: User[]) {
|
||||
const demo = users.find((u) => u.username === 'demo')!;
|
||||
const templates = this.storage.collection('form_templates');
|
||||
const instances = this.storage.collection('form_instances');
|
||||
const day = 24 * 3600_000;
|
||||
const today = Date.now();
|
||||
|
||||
// ===== 表单模板(表单库 = 可选的空白表单,可基于其填写) =====
|
||||
const leave = await templates.insertOne({
|
||||
name: '请假申请',
|
||||
description: 'OA 请假审批(Demo)',
|
||||
ownerId: demo.id,
|
||||
fields: [
|
||||
{ name: 'leaveType', label: '请假类型', type: 'select', required: true, options: [
|
||||
{ label: '事假', value: '事假' }, { label: '病假', value: '病假' },
|
||||
{ label: '年假', value: '年假' }, { label: '调休', value: '调休' },
|
||||
]},
|
||||
{ name: 'startDate', label: '开始日期', type: 'date', required: true },
|
||||
{ name: 'endDate', label: '结束日期', type: 'date', required: true },
|
||||
{ name: 'days', label: '请假天数', type: 'number', required: true },
|
||||
{ name: 'reason', label: '请假事由', type: 'textarea', required: true },
|
||||
],
|
||||
});
|
||||
const meeting = await templates.insertOne({
|
||||
name: '会议室预约',
|
||||
description: '预约会议室(Demo)',
|
||||
ownerId: demo.id,
|
||||
fields: [
|
||||
{ name: 'room', label: '会议室', type: 'select', required: true, options: [
|
||||
{ label: '1 号会议室(10 人)', value: '1' },
|
||||
{ label: '2 号会议室(20 人)', value: '2' },
|
||||
{ label: '3 号会议室(6 人)', value: '3' },
|
||||
]},
|
||||
{ name: 'date', label: '使用日期', type: 'date', required: true },
|
||||
{ name: 'purpose', label: '用途说明', type: 'textarea', required: true },
|
||||
{ name: 'needProjector', label: '需要投影仪', type: 'switch' },
|
||||
],
|
||||
});
|
||||
const office = await templates.insertOne({
|
||||
name: '办公室借用',
|
||||
description: '借用办公室 / 工位 / 洽谈室等场地(Demo)',
|
||||
ownerId: demo.id,
|
||||
fields: [
|
||||
{ name: 'place', label: '借用地点', type: 'select', required: true, options: [
|
||||
{ label: '独立办公室', value: '独立办公室' },
|
||||
{ label: '开放工位', value: '开放工位' },
|
||||
{ label: '洽谈室', value: '洽谈室' },
|
||||
{ label: '培训室', value: '培训室' },
|
||||
]},
|
||||
{ name: 'date', label: '借用日期', type: 'date', required: true },
|
||||
{ name: 'timeRange', label: '使用时段', type: 'select', required: true, options: [
|
||||
{ label: '上午', value: '上午' }, { label: '下午', value: '下午' },
|
||||
{ label: '全天', value: '全天' },
|
||||
]},
|
||||
{ name: 'seats', label: '使用人数', type: 'number', required: true },
|
||||
{ name: 'purpose', label: '借用用途', type: 'textarea', required: true },
|
||||
],
|
||||
});
|
||||
const car = await templates.insertOne({
|
||||
name: '公车借用',
|
||||
description: '申请使用公司公务车辆(Demo)',
|
||||
ownerId: demo.id,
|
||||
fields: [
|
||||
{ name: 'car', label: '车辆', type: 'select', required: true, options: [
|
||||
{ label: '商务车(7 座)', value: '商务车' },
|
||||
{ label: '轿车(5 座)', value: '轿车' },
|
||||
{ label: '厢式货车', value: '货车' },
|
||||
]},
|
||||
{ name: 'date', label: '使用日期', type: 'date', required: true },
|
||||
{ name: 'timeRange', label: '使用时段', type: 'select', required: true, options: [
|
||||
{ label: '上午', value: '上午' }, { label: '下午', value: '下午' },
|
||||
{ label: '全天', value: '全天' },
|
||||
]},
|
||||
{ name: 'destination', label: '目的地', type: 'text', required: true },
|
||||
{ name: 'passengers', label: '乘车人数', type: 'number', required: true },
|
||||
{ name: 'reason', label: '借车事由', type: 'textarea', required: true },
|
||||
],
|
||||
});
|
||||
|
||||
// ===== 演示实例(进行中 / 已完成,月历红点依据 dueDate) =====
|
||||
await instances.insertOne({
|
||||
templateId: leave.id,
|
||||
submitterId: demo.id,
|
||||
status: 'pending',
|
||||
dueDate: today + 4 * day,
|
||||
data: {
|
||||
leaveType: '年假',
|
||||
startDate: '2026-08-24',
|
||||
endDate: '2026-08-25',
|
||||
days: 2,
|
||||
reason: '家庭出行',
|
||||
},
|
||||
});
|
||||
await instances.insertOne({
|
||||
templateId: office.id,
|
||||
submitterId: demo.id,
|
||||
status: 'processing',
|
||||
dueDate: today + 1 * day,
|
||||
data: {
|
||||
place: '洽谈室',
|
||||
date: '2026-08-21',
|
||||
timeRange: '下午',
|
||||
seats: 6,
|
||||
purpose: '接待客户进行方案评审',
|
||||
},
|
||||
});
|
||||
await instances.insertOne({
|
||||
templateId: car.id,
|
||||
submitterId: demo.id,
|
||||
status: 'pending',
|
||||
dueDate: today + 2 * day,
|
||||
data: {
|
||||
car: '商务车',
|
||||
date: '2026-08-22',
|
||||
timeRange: '全天',
|
||||
destination: '机场',
|
||||
passengers: 4,
|
||||
reason: '接送外地来访客户',
|
||||
},
|
||||
});
|
||||
await instances.insertOne({
|
||||
templateId: meeting.id,
|
||||
submitterId: demo.id,
|
||||
status: 'done',
|
||||
data: {
|
||||
room: '2',
|
||||
date: '2026-08-19',
|
||||
purpose: '季度产品规划会',
|
||||
needProjector: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async seedKnowledge(users: User[]) {
|
||||
const demo = users.find((u) => u.username === 'demo')!;
|
||||
const docs = this.storage.collection('knowledge_docs');
|
||||
const now = Date.now();
|
||||
|
||||
// 根级目录
|
||||
const rootCompany = await docs.insertOne({
|
||||
title: '公司制度',
|
||||
content: '',
|
||||
tags: [],
|
||||
ownerId: demo.id,
|
||||
type: 'folder',
|
||||
parentId: null,
|
||||
updatedAt: now,
|
||||
});
|
||||
const rootTemplate = await docs.insertOne({
|
||||
title: '模板库',
|
||||
content: '',
|
||||
tags: [],
|
||||
ownerId: demo.id,
|
||||
type: 'folder',
|
||||
parentId: null,
|
||||
updatedAt: now - dayMs(1),
|
||||
});
|
||||
const rootProduct = await docs.insertOne({
|
||||
title: '产品文档',
|
||||
content: '',
|
||||
tags: [],
|
||||
ownerId: demo.id,
|
||||
type: 'folder',
|
||||
parentId: null,
|
||||
updatedAt: now - dayMs(2),
|
||||
});
|
||||
|
||||
// 二级目录(多级示例)
|
||||
const subFinance = await docs.insertOne({
|
||||
title: '财务规范',
|
||||
content: '',
|
||||
tags: [],
|
||||
ownerId: demo.id,
|
||||
type: 'folder',
|
||||
parentId: rootCompany.id,
|
||||
updatedAt: now - dayMs(3),
|
||||
});
|
||||
|
||||
// 笔记
|
||||
await docs.insertOne({
|
||||
title: 'ChatOA 项目说明',
|
||||
content: '基于 IM 的 OA 客户端工具,通过即时通讯实现对话、文件互传、表单签批。整体结构参考微信电脑端:第一列功能导航、第二列功能区、第三列主工作区。',
|
||||
tags: ['产品', '项目'],
|
||||
ownerId: demo.id,
|
||||
type: 'note',
|
||||
parentId: rootProduct.id,
|
||||
updatedAt: now,
|
||||
});
|
||||
await docs.insertOne({
|
||||
title: '报销流程指引',
|
||||
content: '1. 填写报销单并附发票照片;2. 部门负责人审批;3. 财务复核;4. 打款。\n发票需为增值税普通发票或电子发票,报销时限为费用发生后 30 天内。',
|
||||
tags: ['财务', '流程'],
|
||||
ownerId: demo.id,
|
||||
type: 'note',
|
||||
parentId: subFinance.id,
|
||||
updatedAt: now - dayMs(3),
|
||||
});
|
||||
await docs.insertOne({
|
||||
title: '考勤管理办法',
|
||||
content: '工作时间:周一至周五 9:00-18:00。\n- 迟到 30 分钟内按 1 次提醒\n- 月度满勤奖励 200 元\n- 请假需提前在表单模块提交「请假申请」',
|
||||
tags: ['人事', '制度'],
|
||||
ownerId: demo.id,
|
||||
type: 'note',
|
||||
parentId: rootCompany.id,
|
||||
updatedAt: now - dayMs(4),
|
||||
});
|
||||
await docs.insertOne({
|
||||
title: '周报模板',
|
||||
content: '本周完成:\n- 工作项 1\n- 工作项 2\n下周计划:\n- 计划项 1\n风险与问题:\n- 无',
|
||||
tags: ['模板'],
|
||||
ownerId: demo.id,
|
||||
type: 'note',
|
||||
parentId: rootTemplate.id,
|
||||
updatedAt: now - dayMs(7),
|
||||
});
|
||||
await docs.insertOne({
|
||||
title: '会议纪要模板',
|
||||
content: '会议主题:\n时间:\n参会人:\n结论:\n待办事项:\n- 事项 / 负责人 / 截止日期',
|
||||
tags: ['模板'],
|
||||
ownerId: demo.id,
|
||||
type: 'note',
|
||||
parentId: rootTemplate.id,
|
||||
updatedAt: now - dayMs(8),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function dayMs(n: number) {
|
||||
return n * 24 * 3600_000;
|
||||
}
|
||||
Reference in New Issue
Block a user