feat: 初始化 ChatOA 企业协作 OA 系统

This commit is contained in:
2026-08-20 19:36:07 +08:00
commit 6d7a1f6e63
90 changed files with 20067 additions and 0 deletions
+9
View File
@@ -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 {}
+367
View File
@@ -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;
}