Initial commit: 风影 OA 全栈源码(API/Web/Flutter/IM)
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as argon2 from 'argon2';
|
||||
import { MENUS, type MenuNode } from './menus';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const passwordHash = await argon2.hash('Demo@123456');
|
||||
const adminHash = await argon2.hash('Admin@123456');
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.operationLog.deleteMany();
|
||||
await tx.paymentRecord.deleteMany();
|
||||
await tx.projectChange.deleteMany();
|
||||
await tx.sealItem.deleteMany();
|
||||
await tx.numberRule.deleteMany();
|
||||
await tx.todoTask.deleteMany();
|
||||
await tx.approvalTask.deleteMany();
|
||||
await tx.workReport.deleteMany();
|
||||
await tx.payrollItem.deleteMany();
|
||||
await tx.payrollRun.deleteMany();
|
||||
await tx.attendanceMonth.deleteMany();
|
||||
await tx.employmentEvent.deleteMany();
|
||||
await tx.expenseClaim.deleteMany();
|
||||
await tx.loanRecord.deleteMany();
|
||||
await tx.bondRecord.deleteMany();
|
||||
await tx.invoiceRecord.deleteMany();
|
||||
await tx.timesheet.deleteMany();
|
||||
await tx.projectAcceptance.deleteMany();
|
||||
await tx.projectBudget.deleteMany();
|
||||
await tx.projectTask.deleteMany();
|
||||
await tx.purchaseRequest.deleteMany();
|
||||
await tx.assetOccupancy.deleteMany();
|
||||
await tx.contractReviewer.deleteMany();
|
||||
await tx.contractActionLog.deleteMany();
|
||||
await tx.paymentMilestone.deleteMany();
|
||||
await tx.project.deleteMany();
|
||||
await tx.sealRequest.deleteMany();
|
||||
await tx.contract.deleteMany();
|
||||
await tx.bidDocVersion.deleteMany();
|
||||
await tx.bidAssignee.deleteMany();
|
||||
await tx.bidActionLog.deleteMany();
|
||||
await tx.bidReviewer.deleteMany();
|
||||
await tx.credentialBorrow.deleteMany();
|
||||
await tx.bidCase.deleteMany();
|
||||
await tx.partyContact.deleteMany();
|
||||
await tx.qualification.deleteMany();
|
||||
await tx.userRole.deleteMany();
|
||||
await tx.rolePermission.deleteMany();
|
||||
await tx.refreshToken.deleteMany();
|
||||
await tx.user.deleteMany();
|
||||
await tx.employee.deleteMany();
|
||||
await tx.department.deleteMany();
|
||||
await tx.permission.deleteMany();
|
||||
await tx.role.deleteMany();
|
||||
await tx.dictItem.deleteMany();
|
||||
await tx.attendanceRule.deleteMany();
|
||||
await tx.hrSetting.deleteMany();
|
||||
await tx.legalEntity.deleteMany();
|
||||
await tx.businessParty.deleteMany();
|
||||
|
||||
const root = await tx.department.create({
|
||||
data: {
|
||||
name: '演示科技有限公司',
|
||||
code: 'ROOT',
|
||||
deptType: 'ADMIN',
|
||||
sortNo: 0,
|
||||
},
|
||||
});
|
||||
const depts = await Promise.all(
|
||||
[
|
||||
['商务部', 'BIZ', 'BUSINESS', null, 10],
|
||||
['三维部门', 'TECH_3D', 'TECH', 'CAT_3D', 20],
|
||||
['摄制部门', 'TECH_FILM', 'TECH', 'CAT_FILM', 30],
|
||||
['物资部门', 'TECH_MATERIAL', 'TECH', 'CAT_MATERIAL', 40],
|
||||
['软件研发部门', 'TECH_SOFTWARE', 'TECH', 'CAT_SOFTWARE', 50],
|
||||
['财务部', 'FIN', 'FINANCE', null, 60],
|
||||
['人事行政', 'HR', 'HR', null, 70],
|
||||
].map(([name, code, deptType, categoryCode, sortNo]) =>
|
||||
tx.department.create({
|
||||
data: {
|
||||
parentId: root.id,
|
||||
name: name as string,
|
||||
code: code as string,
|
||||
deptType: deptType as string,
|
||||
categoryCode: categoryCode as string | null,
|
||||
sortNo: sortNo as number,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
const byCode = Object.fromEntries(depts.map((d) => [d.code, d]));
|
||||
|
||||
const permIds: Record<string, string> = {};
|
||||
const writeMenus = async (nodes: MenuNode[], parentId: string | null, root = true) => {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const menu = nodes[i];
|
||||
const row = await tx.permission.create({
|
||||
data: {
|
||||
parentId,
|
||||
name: menu.name,
|
||||
code: menu.code,
|
||||
permType: 'MENU',
|
||||
path: menu.path,
|
||||
icon: menu.icon,
|
||||
sortNo: root ? (i + 1) * 10 : i + 1,
|
||||
},
|
||||
});
|
||||
permIds[menu.code] = row.id;
|
||||
if (menu.children?.length) await writeMenus(menu.children, row.id, false);
|
||||
}
|
||||
};
|
||||
await writeMenus(MENUS, null);
|
||||
|
||||
const roles = {
|
||||
admin: await tx.role.create({ data: { name: '系统管理员', code: 'admin', dataScope: 'ALL' } }),
|
||||
owner: await tx.role.create({ data: { name: '企业负责人', code: 'owner', dataScope: 'ALL' } }),
|
||||
biz_director: await tx.role.create({ data: { name: '商务总监', code: 'biz_director', dataScope: 'DEPT' } }),
|
||||
biz_staff: await tx.role.create({ data: { name: '商务专员', code: 'biz_staff', dataScope: 'SELF' } }),
|
||||
tech_director: await tx.role.create({ data: { name: '技术总监', code: 'tech_director', dataScope: 'DEPT' } }),
|
||||
finance: await tx.role.create({ data: { name: '财务人员', code: 'finance', dataScope: 'ALL' } }),
|
||||
hr: await tx.role.create({ data: { name: '人事', code: 'hr', dataScope: 'ALL' } }),
|
||||
employee: await tx.role.create({ data: { name: '员工', code: 'employee', dataScope: 'SELF' } }),
|
||||
};
|
||||
|
||||
const allPerms = await tx.permission.findMany();
|
||||
const grant = async (roleId: string, codes: string[]) => {
|
||||
const selected =
|
||||
codes[0] === '*'
|
||||
? allPerms
|
||||
: allPerms.filter((p) => codes.some((c) => p.code === c || p.code.startsWith(c)));
|
||||
await tx.rolePermission.createMany({
|
||||
data: selected.map((p) => ({ roleId, permissionId: p.id })),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
};
|
||||
await grant(roles.admin.id, ['*']);
|
||||
await grant(roles.owner.id, ['*']);
|
||||
await grant(roles.biz_director.id, ['office', 'party', 'seal', 'bid', 'contract', 'finance', 'report', 'hr:performance']);
|
||||
await grant(roles.biz_staff.id, ['office', 'party', 'seal', 'bid', 'contract']);
|
||||
await grant(roles.tech_director.id, ['office', 'bid', 'project', 'report', 'hr:performance']);
|
||||
await grant(roles.finance.id, ['office', 'finance', 'contract', 'report', 'bid:result']);
|
||||
await grant(roles.hr.id, ['office', 'hr', 'system:hr', 'system:org', 'system:attendance-rules']);
|
||||
await grant(roles.employee.id, ['office', 'project:timesheets']);
|
||||
|
||||
const mkEmp = (
|
||||
name: string,
|
||||
no: string,
|
||||
deptCode: string,
|
||||
title: string,
|
||||
rest: string,
|
||||
baseSalary: number,
|
||||
perfBase: number,
|
||||
socialBase: number,
|
||||
) =>
|
||||
tx.employee.create({
|
||||
data: {
|
||||
name,
|
||||
employeeNo: no,
|
||||
departmentId: byCode[deptCode].id,
|
||||
title,
|
||||
restSchedule: rest,
|
||||
employmentStatus: 'REGULAR',
|
||||
hiredAt: new Date('2022-01-01'),
|
||||
baseSalary,
|
||||
perfBase,
|
||||
socialBase,
|
||||
},
|
||||
});
|
||||
|
||||
const emps = {
|
||||
admin: await mkEmp('系统管理员', 'E000', 'HR', '管理员', 'DOUBLE', 8000, 0, 8000),
|
||||
owner: await mkEmp('陈总', 'E001', 'BIZ', '企业负责人', 'SINGLE', 25000, 10000, 20000),
|
||||
bizdir: await mkEmp('王商务', 'E010', 'BIZ', '商务总监', 'SINGLE', 18000, 8000, 18000),
|
||||
bizstaff: await mkEmp('李专员', 'E011', 'BIZ', '商务专员', 'SINGLE', 9000, 2500, 9000),
|
||||
dir3d: await mkEmp('赵三维', 'E020', 'TECH_3D', '三维总监', 'SINGLE', 15000, 5000, 15000),
|
||||
dirfilm: await mkEmp('孙摄制', 'E021', 'TECH_FILM', '摄制总监', 'SINGLE', 15000, 5000, 15000),
|
||||
dirmaterial: await mkEmp('周物资', 'E022', 'TECH_MATERIAL', '物资总监', 'SINGLE', 15000, 5000, 15000),
|
||||
dirsoft: await mkEmp('吴软件', 'E023', 'TECH_SOFTWARE', '软件总监', 'DOUBLE', 15000, 5000, 15000),
|
||||
finance: await mkEmp('郑财务', 'E030', 'FIN', '会计', 'SINGLE', 10000, 2000, 10000),
|
||||
hr: await mkEmp('冯人事', 'E040', 'HR', '人事专员', 'SINGLE', 9000, 2000, 9000),
|
||||
employee: await mkEmp('钱员工', 'E100', 'TECH_SOFTWARE', '工程师', 'DOUBLE', 10000, 3000, 10000),
|
||||
};
|
||||
|
||||
const softpm = await mkEmp('软件项目经理', 'E101', 'TECH_SOFTWARE', '项目经理', 'DOUBLE', 12000, 4000, 12000);
|
||||
await tx.employee.update({
|
||||
where: { id: emps.employee.id },
|
||||
data: { managerId: softpm.id },
|
||||
});
|
||||
await tx.employee.update({
|
||||
where: { id: softpm.id },
|
||||
data: { managerId: emps.dirsoft.id },
|
||||
});
|
||||
|
||||
const mkUser = async (
|
||||
username: string,
|
||||
hash: string,
|
||||
emp: { id: string; name: string },
|
||||
roleId: string,
|
||||
) => {
|
||||
const u = await tx.user.create({
|
||||
data: {
|
||||
username,
|
||||
passwordHash: hash,
|
||||
displayName: emp.name,
|
||||
employeeId: emp.id,
|
||||
},
|
||||
});
|
||||
await tx.userRole.create({ data: { userId: u.id, roleId } });
|
||||
return u;
|
||||
};
|
||||
|
||||
const admin = await mkUser('admin', adminHash, emps.admin, roles.admin.id);
|
||||
await mkUser('owner', passwordHash, emps.owner, roles.owner.id);
|
||||
await mkUser('bizdir', passwordHash, emps.bizdir, roles.biz_director.id);
|
||||
await mkUser('bizstaff', passwordHash, emps.bizstaff, roles.biz_staff.id);
|
||||
const dirsoft = await mkUser('dirsoft', passwordHash, emps.dirsoft, roles.tech_director.id);
|
||||
const dir3d = await mkUser('dir3d', passwordHash, emps.dir3d, roles.tech_director.id);
|
||||
await mkUser('dirfilm', passwordHash, emps.dirfilm, roles.tech_director.id);
|
||||
await mkUser('dirmaterial', passwordHash, emps.dirmaterial, roles.tech_director.id);
|
||||
await mkUser('finance', passwordHash, emps.finance, roles.finance.id);
|
||||
await mkUser('hr', passwordHash, emps.hr, roles.hr.id);
|
||||
await mkUser('employee', passwordHash, emps.employee, roles.employee.id);
|
||||
await mkUser('softpm', passwordHash, softpm, roles.employee.id);
|
||||
|
||||
const demoEntity = await tx.legalEntity.create({
|
||||
data: { name: '演示科技有限公司', shortName: '演示科技', creditNo: '91110000MA0000001X', isDefault: true },
|
||||
});
|
||||
const party = await tx.businessParty.create({
|
||||
data: { name: '某市城投集团', creditNo: '91110000MA9999999X', level: 'A' },
|
||||
});
|
||||
await tx.partyContact.create({
|
||||
data: { partyId: party.id, name: '招标办张工', phone: '13800001111', title: '项目联系人' },
|
||||
});
|
||||
await tx.qualification.createMany({
|
||||
data: [
|
||||
{
|
||||
name: '电子与智能化工程专业承包',
|
||||
code: 'QUAL_INTEL',
|
||||
keeper: '行政',
|
||||
expiresAt: new Date(Date.now() + 400 * 86400000),
|
||||
},
|
||||
{
|
||||
name: '涉密信息系统集成乙级',
|
||||
code: 'SECRET_B',
|
||||
keeper: '行政',
|
||||
expiresAt: new Date(Date.now() + 20 * 86400000),
|
||||
},
|
||||
{
|
||||
name: '建筑装修装饰工程专业承包',
|
||||
code: 'QUAL_DECOR',
|
||||
keeper: '行政',
|
||||
expiresAt: new Date(Date.now() - 10 * 86400000),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const dicts: [string, string, string, number][] = [
|
||||
['projectType', '软件', '软件', 1],
|
||||
['projectType', '三维', '三维', 2],
|
||||
['projectType', '摄制', '摄制', 3],
|
||||
['projectType', '物资', '物资', 4],
|
||||
['projectType', '集成', '集成', 5],
|
||||
['tenderMethod', '公开招标', '公开招标', 1],
|
||||
['tenderMethod', '邀请招标', '邀请招标', 2],
|
||||
['tenderMethod', '询价', '询价', 3],
|
||||
['tenderMethod', '竞谈【综合评分】', '竞谈【综合评分】', 4],
|
||||
['tenderMethod', '竞谈【最低价中】', '竞谈【最低价中】', 5],
|
||||
['tenderMethod', '单一来源', '单一来源', 6],
|
||||
['tenderMethod', '其他', '其他', 7],
|
||||
['qualificationNeed', '无需资质', '无需资质', 1],
|
||||
['qualificationNeed', '军二', '军二', 2],
|
||||
['qualificationNeed', '涉乙', '涉乙', 3],
|
||||
['qualificationNeed', '其他', '其他', 4],
|
||||
['expenseCategory', '交通', '交通', 1],
|
||||
['expenseCategory', '餐饮', '餐饮', 2],
|
||||
['expenseCategory', '住宿', '住宿', 3],
|
||||
['expenseCategory', '办公', '办公', 4],
|
||||
['expenseCategory', '招待', '招待', 5],
|
||||
['expenseCategory', '其他', '其他', 6],
|
||||
['gender', '男', 'MALE', 1],
|
||||
['gender', '女', 'FEMALE', 2],
|
||||
['education', '高中及以下', 'HIGH_SCHOOL', 1],
|
||||
['education', '专科', 'COLLEGE', 2],
|
||||
['education', '本科', 'BACHELOR', 3],
|
||||
['education', '硕士', 'MASTER', 4],
|
||||
['education', '博士', 'DOCTOR', 5],
|
||||
];
|
||||
await tx.dictItem.createMany({
|
||||
data: dicts.map(([dictType, label, value, sortNo]) => ({ dictType, label, value, sortNo })),
|
||||
});
|
||||
await tx.numberRule.createMany({
|
||||
data: [
|
||||
{ code: 'BID', name: '投标编号', prefix: 'BID', padLength: 5 },
|
||||
{ code: 'HT', name: '合同编号', prefix: 'HT', padLength: 5 },
|
||||
{ code: 'PRJ', name: '项目编号', prefix: 'PRJ', padLength: 5 },
|
||||
{ code: 'BX', name: '报销编号', prefix: 'BX', padLength: 5 },
|
||||
{ code: 'JK', name: '借款编号', prefix: 'JK', padLength: 5 },
|
||||
{ code: 'BZ', name: '保证金编号', prefix: 'BZ', padLength: 5 },
|
||||
],
|
||||
});
|
||||
await tx.hrSetting.create({
|
||||
data: { id: 'default', tenantId: '1', idMask: true, autoAccount: false, probationMonths: 3, defaultRest: 'SINGLE', workStart: '09:00', workEnd: '18:00' },
|
||||
});
|
||||
await tx.attendanceRule.createMany({
|
||||
data: [
|
||||
{ name: '单休班次', restSchedule: 'SINGLE', workStart: '09:00', workEnd: '18:00', sortNo: 1, remark: '每周休一天,薪酬按 26.09 天核算' },
|
||||
{ name: '双休班次', restSchedule: 'DOUBLE', workStart: '09:00', workEnd: '18:00', sortNo: 2, remark: '每周休两天,薪酬按 21.75 天核算' },
|
||||
],
|
||||
});
|
||||
const sealTypes = ['公章', '合同章', '法人章', '法人签名章', '项目负责人章', '其他章'];
|
||||
await tx.sealItem.createMany({
|
||||
data: sealTypes.map((sealType) => ({
|
||||
name: `${demoEntity.shortName}${sealType}`,
|
||||
sealType,
|
||||
keeper: '行政',
|
||||
legalEntityId: demoEntity.id,
|
||||
status: 'IN_STOCK',
|
||||
})),
|
||||
});
|
||||
|
||||
const bid = await tx.bidCase.create({
|
||||
data: {
|
||||
bidNo: 'BID-2026-00001',
|
||||
name: '某市展厅数字化升级',
|
||||
externalNo: 'ZFCG-2026-088',
|
||||
projectType: '集成',
|
||||
source: '招标公告',
|
||||
qualificationNeed: '涉乙',
|
||||
tenderMethod: '公开招标',
|
||||
applyMethod: '网上申领',
|
||||
openMethod: '线下开标',
|
||||
openPlace: '市公共资源交易中心',
|
||||
openAt: new Date(Date.now() + 7 * 86400000),
|
||||
partyId: party.id,
|
||||
status: 'TECH_INITIAL',
|
||||
createdById: admin.id,
|
||||
reviewers: {
|
||||
create: [
|
||||
{ userId: dirsoft.id, stage: 'TECH_INITIAL' },
|
||||
{ userId: dir3d.id, stage: 'TECH_INITIAL' },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
await tx.bidActionLog.create({
|
||||
data: {
|
||||
bidCaseId: bid.id,
|
||||
actorId: admin.id,
|
||||
action: 'CREATE',
|
||||
toStatus: 'TECH_INITIAL',
|
||||
comment: `发起筛选 ${bid.bidNo}`,
|
||||
},
|
||||
});
|
||||
await tx.approvalTask.createMany({
|
||||
data: [
|
||||
{
|
||||
title: `技术初审会签:${bid.name}`,
|
||||
bizType: 'BID_TECH_INITIAL',
|
||||
bizId: bid.id,
|
||||
assigneeId: dirsoft.id,
|
||||
},
|
||||
{
|
||||
title: `技术初审会签:${bid.name}`,
|
||||
bizType: 'BID_TECH_INITIAL',
|
||||
bizId: bid.id,
|
||||
assigneeId: dir3d.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
console.log('seed ok');
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user