SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)

恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -0,0 +1,92 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.QualElecLicense;
import com.kaidi.oa.repository.QualElecLicenseRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.time.LocalDate;
/**
* 行政·综合部——电子证照 QR 台账演示种子(Module 6 补全缺口:表空/0条/活体返回 null)。
*
* 补全缺口描述:qual-elec-license 表无种子数据,elec-license.vue 首屏空表;
* 同时为 GET /qual-exports/qr-list 补充可见数据,让「真伪验证」与「QR台账」两个入口均有演示内容。
*
* @Order(225) 在所有主数据 Seeder(最高 220)之后运行;幂等(count > 0 跳过)。
*/
@Component
@Order(225)
public class AdminQualElecSeeder implements CommandLineRunner {
private final QualElecLicenseRepository repo;
public AdminQualElecSeeder(QualElecLicenseRepository repo) {
this.repo = repo;
}
@Override
public void run(String... args) {
if (repo.count() > 0) return;
LocalDate today = LocalDate.now();
Instant now = Instant.now();
// 安全生产许可证电子版——有效
repo.save(lic(null, "JZ-AQ-2019-1182", "安全生产许可证", "安全生产许可证电子版",
"https://jzsc.mohurd.gov.cn/asite/verify?certNo=JZ-AQ-2019-1182",
"https://files.kaidi.com/elec/aq-permit-2019.pdf",
today.plusDays(8).toString(), "有效", "王合规", "安全生产许可证,即将到期需续期", now));
// 建筑工程施工总承包一级资质电子版——有效
repo.save(lic(1L, "JZ-2021-0356", "建筑工程施工总承包一级资质电子证照", "设计资质电子版",
"https://jzsc.mohurd.gov.cn/asite/verify?certNo=JZ-2021-0356",
"https://files.kaidi.com/elec/zjzz-2021.pdf",
today.plusDays(40).toString(), "有效", "李资质", "建筑施工总包一级,省住建厅电子证照", now));
// 高新技术企业证书电子版——有效
repo.save(lic(4L, "GR202144000123", "高新技术企业证书电子版", "高新技术企业证书电子版",
"https://www.innocom.gov.cn/gxjscx/index.jhtml?certNo=GR202144000123",
"https://files.kaidi.com/elec/hnt-cert-2021.pdf",
today.plusDays(150).toString(), "有效", "张战略", "高企认定三年有效期,科技厅", now));
// 二级建造师电子证书——有效
repo.save(lic(null, "JZ2019-B-0332456", "刘强-二级建造师电子证书(市政公用)", "建造师电子证书",
"https://jzsc.mohurd.gov.cn/asite/verify?certNo=JZ2019-B-0332456",
null,
today.plusDays(25).toString(), "有效", "人事部", "市政公用专业,即将到期,需提前续期", now));
// ISO 9001 认证电子版——已过期
repo.save(lic(null, "ISO-9001-KD-2019-003", "ISO 9001 质量管理体系认证证书", "ISO认证电子版",
null,
"https://files.kaidi.com/elec/iso9001-2019.pdf",
today.minusDays(20).toString(), "已过期", "质量部", "已过期,正在延续申报中(ZSB-2", now));
// 营业执照电子版——有效(长期)
repo.save(lic(null, "91410100MA3XXXXX4X", "营业执照(电子版)", "营业执照电子版",
"https://www.gsxt.gov.cn/",
"https://files.kaidi.com/elec/biz-license.pdf",
null, "有效", "综合部", "国家企业信用信息公示系统,无固定到期日", now));
}
private static QualElecLicense lic(Long certId, String certNo, String certName,
String licenseType, String qrUrl, String fileUrl,
String expireDate, String status,
String registeredBy, String remark, Instant now) {
QualElecLicense e = new QualElecLicense();
e.setCertId(certId);
e.setCertNo(certNo);
e.setCertName(certName);
e.setLicenseType(licenseType);
e.setQrUrl(qrUrl);
e.setFileUrl(fileUrl);
e.setExpireDate(expireDate);
e.setStatus(status);
e.setRegisteredBy(registeredBy);
e.setRemark(remark);
e.setCreatedAt(now);
return e;
}
}
@@ -0,0 +1,221 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.QualCert;
import com.kaidi.oa.domain.QualDeclTask;
import com.kaidi.oa.domain.QualDeclaration;
import com.kaidi.oa.domain.StaffCredential;
import com.kaidi.oa.domain.StaffDossier;
import com.kaidi.oa.domain.StaffDossierItem;
import com.kaidi.oa.repository.QualCertRepository;
import com.kaidi.oa.repository.QualDeclTaskRepository;
import com.kaidi.oa.repository.QualDeclarationRepository;
import com.kaidi.oa.repository.StaffCredentialRepository;
import com.kaidi.oa.repository.StaffDossierItemRepository;
import com.kaidi.oa.repository.StaffDossierRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
/**
* 行政·资质管理办 / 综合部——资质台账、资质申报、人员证书、员工电子档案的演示种子。
*
* 幂等 + 非破坏:以各表是否已有数据为哨兵,仅在空表时灌入少量演示数据,让深水页面首屏不空。
* 日期以"当前日期 +/- 偏移"动态生成,使到期分级预警在任何时间运行都有 即将到期/逾期 样本。
*/
@Component
@Order(6)
public class AdminQualSeeder implements CommandLineRunner {
private final QualCertRepository certRepo;
private final QualDeclarationRepository declRepo;
private final QualDeclTaskRepository taskRepo;
private final StaffCredentialRepository credRepo;
private final StaffDossierRepository dossierRepo;
private final StaffDossierItemRepository itemRepo;
public AdminQualSeeder(QualCertRepository certRepo, QualDeclarationRepository declRepo,
QualDeclTaskRepository taskRepo, StaffCredentialRepository credRepo,
StaffDossierRepository dossierRepo, StaffDossierItemRepository itemRepo) {
this.certRepo = certRepo;
this.declRepo = declRepo;
this.taskRepo = taskRepo;
this.credRepo = credRepo;
this.dossierRepo = dossierRepo;
this.itemRepo = itemRepo;
}
@Override
public void run(String... args) {
Instant now = Instant.now();
LocalDate today = LocalDate.now();
if (certRepo.count() == 0) {
certRepo.save(cert("ZZ-1", "建筑工程施工总承包", "企业资质", "施工总承包", "JZ-2021-0356",
"凯迪建设工程有限公司", "住房和城乡建设部", "一级", "房屋建筑工程施工总承包",
today.minusYears(3).toString(), today.plusDays(40).toString(), "年检", "即将到期", now));
certRepo.save(cert("ZZ-2", "安全生产许可证", "企业资质", "安全生产许可证", "JZ-AQ-2019-1182",
"凯迪建设工程有限公司", "省住房和城乡建设厅", "不分级", "施工安全生产",
today.minusYears(4).toString(), today.plusDays(8).toString(), "年检", "即将到期", now));
certRepo.save(cert("ZZ-3", "市政公用工程施工总承包", "企业资质", "施工总承包", "JZ-2018-2210",
"凯迪建设工程有限公司", "省住房和城乡建设厅", "二级", "市政公用工程",
today.minusYears(5).toString(), today.minusDays(20).toString(), "年检", "已过期", now));
certRepo.save(cert("ZZ-4", "高新技术企业证书", "企业资质", "高新技术企业", "GR202144000123",
"凯迪科技有限公司", "省科技厅", "不分级", "技术研发",
today.minusYears(2).toString(), today.plusDays(150).toString(), "三年复评", "有效", now));
}
if (declRepo.count() == 0) {
QualDeclaration d1 = decl("ZSB-1", "市政公用工程施工总承包", "升级", "企业资质", "一级",
"资质管理办", "李工", "省住房和城乡建设厅", today.minusDays(60).toString(),
today.plusDays(30).toString(), "专家评审", 380000.0, now);
declRepo.save(d1);
taskRepo.save(task(d1.getId(), "注册人员配备", "人员", "王人事", today.plusDays(5).toString(), "进行中", now));
taskRepo.save(task(d1.getId(), "业绩材料整理", "业绩", "李工", today.plusDays(10).toString(), "待办", now));
taskRepo.save(task(d1.getId(), "网上填报", "填报", "张专员", today.plusDays(15).toString(), "待办", now));
declRepo.save(decl("ZSB-2", "ISO9001质量管理体系", "延续", "企业资质", "不分级",
"资质管理办", "陈工", "认证中心", today.minusDays(15).toString(),
today.plusDays(45).toString(), "内审中", 60000.0, now));
}
if (credRepo.count() == 0) {
credRepo.save(cred("张伟", "工程部", "一级建造师", "建筑工程", "JZ-2018-44521",
"省住建厅", today.minusYears(5).toString(), today.plusDays(120).toString(), "已占用", "凯迪科技园项目", now));
credRepo.save(cred("刘强", "工程部", "一级建造师", "市政公用", "JZ-2019-55102",
"省住建厅", today.minusYears(4).toString(), today.plusDays(25).toString(), "空闲", null, now));
credRepo.save(cred("赵敏", "技术部", "注册结构工程师", "结构", "JG-2017-33421",
"省人事厅", today.minusYears(6).toString(), today.minusDays(10).toString(), "空闲", null, now));
credRepo.save(cred("孙磊", "安全部", "注册安全工程师", "安全", "AQ-2020-12009",
"应急管理厅", today.minusYears(2).toString(), today.plusDays(300).toString(), "空闲", null, now));
}
if (dossierRepo.count() == 0) {
StaffDossier s1 = dossier("KD0001", "张伟", "工程部", "项目经理", today.minusYears(5).toString(),
"410101199001011234", today.plusDays(60).toString(), today.plusDays(20).toString(), now);
dossierRepo.save(s1);
itemRepo.save(item(s1.getId(), "基本信息", "入职登记表.pdf", "入职", now));
itemRepo.save(item(s1.getId(), "合同信息", "劳动合同(2021).pdf", "入职", now));
itemRepo.save(item(s1.getId(), "资质证书", "一级建造师证.jpg", "手工上传", now));
// 缺「培训记录」→ 完整性 75%
StaffDossier s2 = dossier("KD0002", "刘强", "工程部", "技术负责人", today.minusYears(3).toString(),
"410101199203054321", today.plusYears(2).toString(), today.plusDays(200).toString(), now);
dossierRepo.save(s2);
itemRepo.save(item(s2.getId(), "基本信息", "入职登记表.pdf", "入职", now));
}
}
private static QualCert cert(String code, String name, String category, String certType, String certNo,
String holder, String issuer, String level, String scope, String issueDate,
String expireDate, String reviewCycle, String status, Instant now) {
QualCert c = new QualCert();
c.setCode(code);
c.setName(name);
c.setCategory(category);
c.setCertType(certType);
c.setCertNo(certNo);
c.setHolder(holder);
c.setIssuer(issuer);
c.setLevel(level);
c.setScope(scope);
c.setIssueDate(issueDate);
c.setExpireDate(expireDate);
c.setReviewCycle(reviewCycle);
c.setStatus(status);
c.setCreatedAt(now);
return c;
}
private static QualDeclaration decl(String code, String qualName, String declType, String category,
String targetLevel, String dept, String owner, String externalOrg,
String planStart, String planDeadline, String status,
double budget, Instant now) {
QualDeclaration d = new QualDeclaration();
d.setCode(code);
d.setQualName(qualName);
d.setDeclType(declType);
d.setCategory(category);
d.setTargetLevel(targetLevel);
d.setDept(dept);
d.setOwner(owner);
d.setExternalOrg(externalOrg);
d.setPlanStart(planStart);
d.setPlanDeadline(planDeadline);
d.setStatus(status);
d.setBudget(BigDecimal.valueOf(budget).setScale(2, java.math.RoundingMode.HALF_UP));
d.setFeeSpent(BigDecimal.ZERO.setScale(2, java.math.RoundingMode.HALF_UP));
d.setCreatedAt(now);
return d;
}
private static QualDeclTask task(Long declId, String name, String taskType, String assignee,
String dueDate, String status, Instant now) {
QualDeclTask t = new QualDeclTask();
t.setDeclarationId(declId);
t.setName(name);
t.setTaskType(taskType);
t.setAssignee(assignee);
t.setDueDate(dueDate);
t.setStatus(status);
t.setCreatedAt(now);
return t;
}
private static StaffCredential cred(String personName, String dept, String credType, String major,
String credNo, String issuer, String issueDate, String expireDate,
String lockState, String lockedProject, Instant now) {
StaffCredential c = new StaffCredential();
c.setPersonName(personName);
c.setDept(dept);
c.setCredType(credType);
c.setMajor(major);
c.setCredNo(credNo);
c.setIssuer(issuer);
c.setIssueDate(issueDate);
c.setExpireDate(expireDate);
c.setLockState(lockState);
c.setLockedProject(lockedProject);
if ("已占用".equals(lockState)) {
c.setLockedDate(LocalDate.now().minusDays(15).toString());
}
c.setBorrowState("在库");
c.setCreatedAt(now);
return c;
}
private static StaffDossier dossier(String employeeNo, String name, String dept, String position,
String hireDate, String idCardNo, String idCardExpire,
String contractExpire, Instant now) {
StaffDossier d = new StaffDossier();
d.setEmployeeNo(employeeNo);
d.setName(name);
d.setDept(dept);
d.setPosition(position);
d.setHireDate(hireDate);
d.setIdCardNo(idCardNo);
d.setIdCardExpire(idCardExpire);
d.setContractExpire(contractExpire);
d.setDossierStatus("在职");
d.setCompleteness(0);
d.setCreatedAt(now);
d.setUpdatedAt(now);
return d;
}
private static StaffDossierItem item(Long dossierId, String category, String title,
String sourceEvent, Instant now) {
StaffDossierItem it = new StaffDossierItem();
it.setDossierId(dossierId);
it.setCategory(category);
it.setTitle(title);
it.setArchiveDate(LocalDate.now().toString());
it.setSourceEvent(sourceEvent);
it.setAutoArchived(Boolean.FALSE);
it.setCreatedAt(now);
return it;
}
}
@@ -0,0 +1,360 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.Archive;
import com.kaidi.oa.domain.ArchiveVersion;
import com.kaidi.oa.repository.ArchiveRepository;
import com.kaidi.oa.repository.ArchiveVersionRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
/**
* 资料室·Module 8「与其它部门的接口要求」— 跨模块归档链演示种子数据。
*
* <p>补齐 integration-dashboard 六条「未激活」链路(activeChains 从 4/10 → 10/10):
* <ul>
* <li>诉讼(法务风险部 legal.toArchive</li>
* <li>财务(财务部 finance.toArchive</li>
* <li>人事(人力资源部 hr.toArchive</li>
* <li>知识产权(知识产权部 ip.toArchive</li>
* <li>质量(质安部 quality.toArchive</li>
* <li>图纸档案(工程监理 supervision.drawing+ ArchiveVersion 版本历史</li>
* </ul>
*
* <p>幂等:同 sourceType 在 archive 表已有记录则跳过整批(count 检查)。
* @Order(205) 在所有基础 Seeder 后运行。
*/
@Component
@Order(205)
public class ArchiveCrossModuleSeeder implements CommandLineRunner {
private final ArchiveRepository archiveRepo;
private final ArchiveVersionRepository versionRepo;
public ArchiveCrossModuleSeeder(ArchiveRepository archiveRepo,
ArchiveVersionRepository versionRepo) {
this.archiveRepo = archiveRepo;
this.versionRepo = versionRepo;
}
@Override
public void run(String... args) {
seedLitigationArchives();
seedFinanceArchives();
seedHrArchives();
seedIpArchives();
seedQualityArchives();
seedDrawingArchives();
}
// =========================================================================
// 1. 诉讼文书归档(法务风险部 legal.toArchive
// =========================================================================
private void seedLitigationArchives() {
long existing = archiveRepo.findAll().stream()
.filter(a -> "诉讼".equals(a.getSourceType()))
.count();
if (existing > 0) return;
archive("合同档案", "靖州供水项目工程款争议仲裁文书", "诉讼", 1L,
"靖州供水仲裁申请书及答辩材料.pdf", "pdf", 1_840_000L,
"法务专员-王芳", "2026-01-15",
"诉讼,仲裁,合同纠纷,靖州供水,已结案",
"靖州供水信息化建设合同工程款争议仲裁案,申请仲裁→调解→撤案,文书已归档至资料室。",
"机密");
archive("合同档案", "供水设备分包方违约起诉状及判决书", "诉讼", 2L,
"供水设备分包违约诉讼案(完整卷宗).pdf", "pdf", 3_260_000L,
"法务专员-王芳", "2025-11-20",
"诉讼,违约,分包,供水设备,已判决",
"供水调度监控设备分包合同违约诉讼,一审判决我方胜诉,文书卷宗完整归档。",
"机密");
archive("合同档案", "江山春锦项目咨询费结算纠纷调解协议", "诉讼", 3L,
"江山春锦调解协议书.pdf", "pdf", 680_000L,
"法务部主任-张伟", "2026-03-05",
"诉讼,调解协议,咨询费,江山春锦,已结案",
"江山春锦项目工程咨询费结算纠纷经调解达成协议,调解书及结案证明归档。",
"机密");
}
// =========================================================================
// 2. 会计档案年度归档(财务部 finance.toArchive
// =========================================================================
private void seedFinanceArchives() {
long existing = archiveRepo.findAll().stream()
.filter(a -> "财务".equals(a.getSourceType()))
.count();
if (existing > 0) return;
archive("财务档案", "2025年度会计档案(凭证/账簿/报表)", "财务", 2025L,
"2025年度会计档案汇编.pdf", "pdf", 12_480_000L,
"财务总监-刘洋", "2026-04-30",
"年度:2025,类型:会计档案,保存期限:30年,会计凭证,账簿,财务报表",
"2025年度财务档案,含记账凭证、总账/明细账、资产负债表/利润表/现金流量表,依《会计档案管理办法》保存30年。",
"机密");
archive("财务档案", "2024年度会计档案(凭证/账簿/报表)", "财务", 2024L,
"2024年度会计档案汇编.pdf", "pdf", 10_920_000L,
"财务总监-刘洋", "2025-04-30",
"年度:2024,类型:会计档案,保存期限:30年,凭证,账簿,报表",
"2024年度财务档案归档,含年度审计报告及税务申报表副本,永久保存备查。",
"机密");
archive("财务档案", "2026年Q1季度财务快报及纳税申报资料", "财务", 20261L,
"2026Q1财务快报及纳税申报.pdf", "pdf", 2_340_000L,
"财务会计-陈静", "2026-04-15",
"季报,Q1,2026,纳税申报,财务快报",
"2026年第一季度财务快报及增值税/企业所得税季报材料归档。",
"机密");
}
// =========================================================================
// 3. 员工档案归档(人力资源部 hr.toArchive
// =========================================================================
private void seedHrArchives() {
long existing = archiveRepo.findAll().stream()
.filter(a -> a.getSourceType() != null && a.getSourceType().startsWith("人事"))
.count();
if (existing > 0) return;
archive("人事档案", "李娜(EMP001)离职档案", "人事-离职", null,
"EMP001-李娜-离职手续及档案.pdf", "pdf", 560_000L,
"人事专员-王芳", "2026-05-10",
"员工编号:EMP001,员工姓名:李娜,部门:项目管理部,事件:离职",
"项目管理部李娜女士离职手续及人事档案汇编,含离职审批、工作交接记录、薪资结算单。",
"机密");
archive("人事档案", "刘洋(EMP003)晋升档案", "人事-晋升", null,
"EMP003-刘洋-晋升及绩效材料.pdf", "pdf", 340_000L,
"人事专员-王芳", "2026-03-01",
"员工编号:EMP003,员工姓名:刘洋,部门:财务管理部,事件:晋升",
"财务管理部刘洋晋升为财务总监,含晋升审批、绩效评估报告及新岗位协议。",
"机密");
archive("人事档案", "王芳(EMP005)劳动合同续签档案", "人事-续签", null,
"EMP005-王芳-劳动合同续签.pdf", "pdf", 220_000L,
"人事总监-张伟", "2026-01-15",
"员工编号:EMP005,员工姓名:王芳,部门:人力资源部,事件:续签",
"人力资源部王芳劳动合同到期续签,含续签审批、新合同文本(2026-2029)。",
"机密");
archive("人事档案", "陈静(EMP007)入职档案", "人事-入职", null,
"EMP007-陈静-入职材料汇编.pdf", "pdf", 480_000L,
"人事专员-王芳", "2026-02-08",
"员工编号:EMP007,员工姓名:陈静,部门:财务管理部,事件:入职",
"财务管理部陈静入职资料汇编,含录用通知、学历/资质证明、入职登记表、劳动合同。",
"机密");
}
// =========================================================================
// 4. 知识产权证书归档(知识产权部 ip.toArchive
// =========================================================================
private void seedIpArchives() {
long existing = archiveRepo.findAll().stream()
.filter(a -> "知识产权".equals(a.getSourceType()))
.count();
if (existing > 0) return;
archive("知识产权档案", "专利证书:供水智能调度优化方法(ZL202410123456.8", "知识产权", null,
"专利证书-ZL202410123456.8.pdf", "pdf", 860_000L,
"知识产权专员-李娜", "2026-04-20",
"证书编号:ZL202410123456.8,证书类型:专利证书,权利人:湖南凯迪工程科技有限公司,有效期至:2044-04-20",
"供水智能调度优化方法发明专利,授权公告日2026-04-20,权利人凯迪工程,有效期20年。",
"机密");
archive("知识产权档案", "软件著作权证书:凯迪ERP+OA一体化平台V1.02026SR0567890", "知识产权", null,
"软著证书-凯迪ERP-OA-2026SR0567890.pdf", "pdf", 720_000L,
"知识产权专员-李娜", "2026-05-18",
"证书编号:2026SR0567890,证书类型:软件著作权证,权利人:湖南凯迪工程科技有限公司,有效期至:永久",
"凯迪ERP+OA一体化平台V1.0软件著作权登记证书,登记号2026SR0567890,永久保护。",
"机密");
archive("知识产权档案", "商标注册证:凯迪(KAIDI)第30类(3012345678", "知识产权", null,
"商标注册证-凯迪品牌-3012345678.pdf", "pdf", 540_000L,
"知识产权总监-张伟", "2026-01-10",
"证书编号:3012345678,证书类型:商标注册证,权利人:湖南凯迪工程科技有限公司,有效期至:2036-01-10",
"凯迪品牌商标注册证(第30类),有效期10年,到期前12个月提醒续期。",
"内部");
}
// =========================================================================
// 5. 质量记录归档(质安部 quality.toArchive
// =========================================================================
private void seedQualityArchives() {
long existing = archiveRepo.findAll().stream()
.filter(a -> "质量".equals(a.getSourceType()))
.count();
if (existing > 0) return;
archive("质量档案", "检验报告:供水监控设备出厂检验报告(QR2026-001)", "质量", null,
"供水监控设备出厂检验报告-QR2026-001.pdf", "pdf", 2_140_000L,
"质安专员-刘洋", "2026-04-25",
"记录编号:QR2026-001,记录类型:检验报告,项目:靖州供水项目",
"靖州供水调度监控设备出厂检验报告,涵盖硬件功能/电磁兼容/防护等级测试,全项合格。",
"内部");
archive("质量档案", "体系文件:ISO9001质量管理手册(2026版)", "质量", null,
"ISO9001质量管理手册-2026版.pdf", "pdf", 5_680_000L,
"质安总监-张伟", "2026-03-01",
"记录编号:SYS-QM-2026,记录类型:体系文件,项目:公司质量管理体系",
"公司ISO9001:2015质量管理体系手册2026年修订版,通过外部审核,归档保存备查。",
"内部");
archive("质量档案", "安全报告:靖州供水项目施工安全专项检查报告(SR2026-003)", "质量", null,
"靖州供水施工安全检查报告-SR2026-003.pdf", "pdf", 1_360_000L,
"安全专员-王芳", "2026-05-30",
"记录编号:SR2026-003,记录类型:安全报告,项目:靖州供水项目",
"靖州供水项目第三次施工安全专项检查报告,现场整改措施已闭环,报告存档。",
"内部");
archive("质量档案", "特种设备档案:起重机械检验合格证(TSB2026-0078", "质量", null,
"起重机械检验合格证-TSB2026-0078.pdf", "pdf", 680_000L,
"设备管理员-陈静", "2026-02-28",
"记录编号:TSB2026-0078,记录类型:特种设备档案,项目:靖州供水项目",
"靖州施工现场起重机械(额定起重量5t)年度检验合格证,有效期至2027-02-27。",
"内部");
}
// =========================================================================
// 6. 图纸档案归档(工程监理 supervision.drawing+ 版本历史
// =========================================================================
private void seedDrawingArchives() {
long existingDrawings = archiveRepo.findByCategory("图纸档案").size();
if (existingDrawings > 0) return;
// 图纸1:管网总图 V1.0→V2.0V1.0作废,V2.0现行)
Archive d1 = archiveEntity("图纸档案", "靖州供水项目管网系统总图", "图纸档案", 1L,
"靖州供水管网总图-V2.0.pdf", "pdf", 8_640_000L,
"监理工程师-刘洋", "2026-05-20",
"图纸档案,管网总图,靖州供水,现行V2.0",
"靖州县供水管网系统总布置图,含主干管/支管/阀门井/消防栓定位,V2.0为现行版。",
"内部");
d1 = archiveRepo.save(d1);
// V1.0(作废)
ArchiveVersion v1d1 = new ArchiveVersion();
v1d1.setArchiveId(d1.getId());
v1d1.setArchiveTitle(d1.getTitle());
v1d1.setVersionNo("V1.0");
v1d1.setIsCurrent(Boolean.FALSE);
v1d1.setStatus("作废");
v1d1.setChangeNote("初版管网总图,未含北区延伸段,已作废");
v1d1.setAuthor("监理工程师-王芳");
v1d1.setVersionDate("2026-03-10");
v1d1.setCreatedAt(Instant.now());
versionRepo.save(v1d1);
// V2.0(现行)
ArchiveVersion v2d1 = new ArchiveVersion();
v2d1.setArchiveId(d1.getId());
v2d1.setArchiveTitle(d1.getTitle());
v2d1.setVersionNo("V2.0");
v2d1.setIsCurrent(Boolean.TRUE);
v2d1.setStatus("现行");
v2d1.setChangeNote("补充北区管网延伸段(DN200主管×840m),更新阀门井坐标");
v2d1.setAuthor("监理工程师-刘洋");
v2d1.setVersionDate("2026-05-20");
v2d1.setCreatedAt(Instant.now());
versionRepo.save(v2d1);
// 图纸2:调度中心机房平面图 V1.0(现行)
Archive d2 = archiveEntity("图纸档案", "靖州供水调度中心机房平面布置图", "图纸档案", 1L,
"调度中心机房平面图-V1.0.pdf", "pdf", 3_120_000L,
"监理工程师-陈静", "2026-04-08",
"图纸档案,机房平面图,靖州供水,调度中心,现行V1.0",
"供水调度中心机房设备平面布置图,含服务器机柜/UPS/网络设备/弱电桥架,V1.0现行。",
"内部");
d2 = archiveRepo.save(d2);
ArchiveVersion v1d2 = new ArchiveVersion();
v1d2.setArchiveId(d2.getId());
v1d2.setArchiveTitle(d2.getTitle());
v1d2.setVersionNo("V1.0");
v1d2.setIsCurrent(Boolean.TRUE);
v1d2.setStatus("现行");
v1d2.setChangeNote("初版机房平面布置图,经监理审核通过,作为施工及竣工验收依据");
v1d2.setAuthor("监理工程师-陈静");
v1d2.setVersionDate("2026-04-08");
v1d2.setCreatedAt(Instant.now());
versionRepo.save(v1d2);
// 图纸3OA系统网络拓扑图 V1.0→V1.1→V2.0(仅V2.0现行)
Archive d3 = archiveEntity("图纸档案", "凯迪ERP+OA一体化网络拓扑图", "图纸档案", 2L,
"凯迪ERP-OA网络拓扑图-V2.0.pdf", "pdf", 2_280_000L,
"IT运维-张伟", "2026-06-01",
"图纸档案,网络拓扑图,ERP,OA,IT架构,现行V2.0",
"凯迪ERP+OA一体化系统网络拓扑图(物理+逻辑),含边界防火墙/核心交换机/服务器集群,V2.0现行。",
"内部");
d3 = archiveRepo.save(d3);
ArchiveVersion v1d3 = new ArchiveVersion();
v1d3.setArchiveId(d3.getId());
v1d3.setArchiveTitle(d3.getTitle());
v1d3.setVersionNo("V1.0");
v1d3.setIsCurrent(Boolean.FALSE);
v1d3.setStatus("作废");
v1d3.setChangeNote("初版拓扑,无DMZ区,安全审计后作废");
v1d3.setAuthor("IT运维-李娜");
v1d3.setVersionDate("2026-02-15");
v1d3.setCreatedAt(Instant.now());
versionRepo.save(v1d3);
ArchiveVersion v11d3 = new ArchiveVersion();
v11d3.setArchiveId(d3.getId());
v11d3.setArchiveTitle(d3.getTitle());
v11d3.setVersionNo("V1.1");
v11d3.setIsCurrent(Boolean.FALSE);
v11d3.setStatus("作废");
v11d3.setChangeNote("补充DMZ区,增加Web应用防火墙,后发现服务器IP段规划冲突,作废");
v11d3.setAuthor("IT运维-李娜");
v11d3.setVersionDate("2026-04-10");
v11d3.setCreatedAt(Instant.now());
versionRepo.save(v11d3);
ArchiveVersion v2d3 = new ArchiveVersion();
v2d3.setArchiveId(d3.getId());
v2d3.setArchiveTitle(d3.getTitle());
v2d3.setVersionNo("V2.0");
v2d3.setIsCurrent(Boolean.TRUE);
v2d3.setStatus("现行");
v2d3.setChangeNote("重新规划服务器IP段(10.10.0.0/16),完善安全域划分,通过等保测评");
v2d3.setAuthor("IT运维-张伟");
v2d3.setVersionDate("2026-06-01");
v2d3.setCreatedAt(Instant.now());
versionRepo.save(v2d3);
}
// =========================================================================
// helpers
// =========================================================================
private void archive(String category, String title, String sourceType, Long sourceId,
String fileName, String fileType, long fileSize, String uploader,
String archiveDate, String tags, String summary, String accessLevel) {
Archive a = archiveEntity(category, title, sourceType, sourceId,
fileName, fileType, fileSize, uploader, archiveDate, tags, summary, accessLevel);
archiveRepo.save(a);
}
private Archive archiveEntity(String category, String title, String sourceType, Long sourceId,
String fileName, String fileType, long fileSize, String uploader,
String archiveDate, String tags, String summary, String accessLevel) {
Archive a = new Archive();
a.setCategory(category);
a.setTitle(title);
a.setSourceType(sourceType);
a.setSourceId(sourceId);
a.setFileName(fileName);
a.setFileType(fileType);
a.setFileSize(fileSize);
a.setUploader(uploader);
a.setArchiveDate(archiveDate);
a.setTags(tags);
a.setSummary(summary);
a.setAccessLevel(accessLevel);
a.setCreatedAt(Instant.now());
return a;
}
}
@@ -0,0 +1,398 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.ExternalRegulation;
import com.kaidi.oa.domain.FraudInvestigationNote;
import com.kaidi.oa.domain.FraudReport;
import com.kaidi.oa.domain.LegalRegulation;
import com.kaidi.oa.repository.ExternalRegulationRepository;
import com.kaidi.oa.repository.FraudInvestigationNoteRepository;
import com.kaidi.oa.repository.FraudReportRepository;
import com.kaidi.oa.repository.LegalRegulationRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
/**
* 审计监察部演示种子数据(补缺口 #6 独立调查空间 + #9 法规制度库)。
*
* 补齐:
* - FraudReport(舞弊举报案件,缺口#6):5 条跨状态/渠道/严重程度的演示举报,
* 含已结案/调查中/已驳回多状态,让舞弊监测列表活体可见;
* - FraudInvestigationNote(独立调查空间底稿,缺口#6):为调查中案件添加保密底稿演示;
* - LegalRegulation(内部制度库,缺口#9):7 条已发布的内部制度演示,含采购/财务/HR等类型;
* - ExternalRegulation(外部法规库,缺口#9):6 条现行有效外部法规演示。
*
* 幂等:各表 count > 0 即跳过。
* @Order(202) 在 FertPkgAccountingSeeder(@Order 201) 之后运行。
*/
@Component
@Order(202)
public class AuditDeptSeeder implements ApplicationRunner {
private final FraudReportRepository fraudRepo;
private final FraudInvestigationNoteRepository noteRepo;
private final LegalRegulationRepository internalRegRepo;
private final ExternalRegulationRepository externalRegRepo;
public AuditDeptSeeder(FraudReportRepository fraudRepo,
FraudInvestigationNoteRepository noteRepo,
LegalRegulationRepository internalRegRepo,
ExternalRegulationRepository externalRegRepo) {
this.fraudRepo = fraudRepo;
this.noteRepo = noteRepo;
this.internalRegRepo = internalRegRepo;
this.externalRegRepo = externalRegRepo;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
seedFraudReports();
seedInternalRegulations();
seedExternalRegulations();
}
// ---- 舞弊举报案件种子 ----
private void seedFraudReports() {
if (fraudRepo.count() > 0) return;
Instant now = Instant.now();
// 案件1:供应商采购价格异常 — 已结案·查实
FraudReport r1 = new FraudReport();
r1.setCaseNo("FR-2026-001");
r1.setTitle("供应商A采购价格异常高于市场价30%");
r1.setAccused("采购部 张某某 / 供应商凯隆贸易有限公司");
r1.setDept("采购部");
r1.setContent("举报人反映,2025年Q3至Q4期间,公司向凯隆贸易采购的某型号化工原料价格" +
"持续高于同类市场均价约28%-32%。涉及3个批次合计采购金额约85万元。" +
"初步核查发现采购部张某某与供应商法人存在亲属关系(妻兄)。");
r1.setReportChannel("邮件");
r1.setAnonymous(false);
r1.setReporter("李建国");
r1.setReporterContact("13812345678(仅调查组可见)");
r1.setSeverity("");
r1.setInvolvedAmount(new BigDecimal("852000.00"));
r1.setStatus("已结案");
r1.setAssignee("王审计");
r1.setInvestigateNote("2026-02-10 调取2025年Q3-Q4采购台账,发现涉案批次单价高于同期询价均价31.2%。\n" +
"2026-02-15 约谈采购部张某某,其承认与供应商法人存在亲属关系但未申报利益冲突。\n" +
"2026-02-20 向财务部调取银行流水,未发现账外收益转移,初步认定为利益关系未申报。");
r1.setConclusion("查实:采购部张某某与涉案供应商法人存在亲属关系,采购价格高于市场均价," +
"违反《采购管理制度》利益冲突申报条款。但未查实收受回扣。建议:追缴超价差额约26万元," +
"对张某某给予警告处分并调离采购岗位。");
r1.setConfirmed(true);
r1.setDisposition("追责处分");
r1.setReportDate("2026-01-15");
r1.setClosedDate("2026-02-25");
r1.setCreatedAt(now.minus(45, ChronoUnit.DAYS));
fraudRepo.save(r1);
// 案件2:费用报销临近限额异常 — 调查中
FraudReport r2 = new FraudReport();
r2.setCaseNo("FR-2026-002");
r2.setTitle("研发部员工多次费用报销临近500元审批门槛");
r2.setAccused("研发部 赵明辉");
r2.setDept("研发部");
r2.setContent("系统自动预警:研发部赵明辉2026年1-3月累计提交17次费用报销," +
"其中14次金额在490-499元之间,明显规避500元以上需经理审批的规则。" +
"累计报销金额7,238元,涉及餐饮/差旅/耗材等多类别。");
r2.setReportChannel("二维码");
r2.setAnonymous(true);
r2.setReporter("匿名");
r2.setSeverity("");
r2.setInvolvedAmount(new BigDecimal("7238.00"));
r2.setStatus("调查中");
r2.setAssignee("陈审计");
r2.setInvestigateNote("2026-04-01 调取17次报销凭证,其中3次餐饮凭证与行程单不符(报销当日无差旅记录)。\n" +
"2026-04-05 调取门禁系统记录,核实部分报销日期当事人在本地办公并非出差。");
r2.setReportDate("2026-03-25");
r2.setCreatedAt(now.minus(20, ChronoUnit.DAYS));
fraudRepo.save(r2);
// 案件3:仓库物料盗用 — 调查中(已登记后已分配)
FraudReport r3 = new FraudReport();
r3.setCaseNo("FR-2026-003");
r3.setTitle("仓库物料出库单与实际库存差异连续3个月偏差超5%");
r3.setAccused("生产部仓库 不详");
r3.setDept("生产部");
r3.setContent("财务月度盘点发现2025年12月至2026年2月仓库某类原材料(品号RM-0312)" +
"出库单数量与实际减少量差异达5.8%-7.2%,已发现2次实物数量少于账面数量约3吨。" +
"初步怀疑存在物料外流,请求调查。");
r3.setReportChannel("网页");
r3.setAnonymous(false);
r3.setReporter("财务部 刘会计");
r3.setReporterContact("内部分机8801");
r3.setSeverity("");
r3.setInvolvedAmount(new BigDecimal("186000.00"));
r3.setStatus("调查中");
r3.setAssignee("王审计");
r3.setInvestigateNote("2026-03-10 调取2025年12月至今仓库出入库台账,确认差异存在。\n" +
"2026-03-15 调阅仓库视频监控(已申请IT部协助),正在比对异常出库时段。");
r3.setReportDate("2026-03-08");
r3.setCreatedAt(now.minus(18, ChronoUnit.DAYS));
fraudRepo.save(r3);
// 案件4:举报已驳回(核查不成立)
FraudReport r4 = new FraudReport();
r4.setCaseNo("FR-2025-018");
r4.setTitle("举报销售部人员私下收取客户回扣");
r4.setAccused("销售部 孙某");
r4.setDept("销售部");
r4.setContent("举报称销售部孙某在承接某大客户订单时私下收取回扣5万元," +
"并刻意压低成交价格造成公司损失。");
r4.setReportChannel("电话");
r4.setAnonymous(true);
r4.setReporter("匿名");
r4.setSeverity("");
r4.setInvolvedAmount(new BigDecimal("50000.00"));
r4.setStatus("已驳回");
r4.setAssignee("陈审计");
r4.setConclusion("经核查:涉案订单成交价与报价单一致,价格系经部门总监审批的标准优惠价格," +
"不存在低价处理情况。对孙某的银行流水进行核查,未发现异常大额收入。" +
"判定:举报线索不成立,予以驳回。");
r4.setConfirmed(false);
r4.setDisposition("不予处理");
r4.setReportDate("2025-11-20");
r4.setClosedDate("2025-12-10");
r4.setCreatedAt(now.minus(180, ChronoUnit.DAYS));
fraudRepo.save(r4);
// 案件5:新登记待分配
FraudReport r5 = new FraudReport();
r5.setCaseNo("FR-2026-004");
r5.setTitle("行政部车辆使用记录与实际派车存在出入");
r5.setAccused("行政部 车辆管理员 周某");
r5.setDept("行政部");
r5.setContent("举报行政部车辆管理员周某多次将公司车辆私用于个人活动," +
"行驶记录与派车单显示的出行目的地不符,怀疑存在公车私用和油卡套现行为。" +
"近半年涉及车辆3辆,估计异常油卡消费约1.2万元。");
r5.setReportChannel("网页");
r5.setAnonymous(false);
r5.setReporter("行政部 李文华");
r5.setReporterContact("13900001234");
r5.setSeverity("");
r5.setInvolvedAmount(new BigDecimal("12000.00"));
r5.setStatus("已登记");
r5.setReportDate("2026-06-10");
r5.setCreatedAt(now.minus(5, ChronoUnit.DAYS));
fraudRepo.save(r5);
// 为调查中的案件2和3补充独立调查空间底稿
seedInvestigationNotes(r2, r3);
}
private void seedInvestigationNotes(FraudReport caseExpense, FraudReport caseMaterial) {
if (noteRepo.count() > 0) return;
Instant now = Instant.now();
// 案件2(费用报销临近限额)的调查底稿
FraudInvestigationNote n1 = new FraudInvestigationNote();
n1.setReportId(caseExpense.getId());
n1.setNoteType("证据清单");
n1.setTitle("报销凭证汇总分析表");
n1.setContent("已收集17份原始报销凭证复印件(2026-01至2026-03)。\n" +
"关键发现:\n" +
"- 2026-01-12 餐饮发票499元:消费地点(深圳市福田区)与当日打卡记录(本市本公司)不符\n" +
"- 2026-02-08 差旅499元:未找到对应出差审批单\n" +
"- 2026-03-15 耗材495元:供应商为个人收款账户,非正规企业发票\n" +
"上述3笔共1493元存在明显疑点,建议重点追查。");
n1.setAuthor("陈审计");
n1.setConfidential(true);
n1.setCreatedAt(now.minus(15, ChronoUnit.DAYS));
n1.setUpdatedAt(now.minus(15, ChronoUnit.DAYS));
noteRepo.save(n1);
FraudInvestigationNote n2 = new FraudInvestigationNote();
n2.setReportId(caseExpense.getId());
n2.setNoteType("财务分析");
n2.setTitle("临近限额报销模式统计分析");
n2.setContent("使用数据分析:\n" +
"- 全公司2026年Q1同类员工(研发部,共23人)报销490-499元区间次数:平均0.3次/人\n" +
"- 涉案人员:14次(是均值的47倍),统计显著异常(p<0.001)\n" +
"- 涉案人员同期报销总次数17次,其中490-499元占82%\n" +
"结论:高度怀疑为主动规避审批门槛的刻意行为。");
n2.setAuthor("陈审计");
n2.setConfidential(true);
n2.setCreatedAt(now.minus(12, ChronoUnit.DAYS));
n2.setUpdatedAt(now.minus(12, ChronoUnit.DAYS));
noteRepo.save(n2);
FraudInvestigationNote n3 = new FraudInvestigationNote();
n3.setReportId(caseExpense.getId());
n3.setNoteType("结论草稿");
n3.setTitle("调查初步结论草稿(待复核)");
n3.setContent("初步结论:\n" +
"有据可查的虚假报销3笔共1493元(门禁记录/缺失审批/非正规发票)。\n" +
"其余11笔证据不足以认定舞弊,但反映报销管理内控薄弱。\n" +
"建议处理:\n" +
"1. 追缴有据可查3笔虚假报销1493元;\n" +
"2. 对赵明辉给予警告处分;\n" +
"3. 建议公司调整费用报销制度,300元以上均需上级审批,消除门槛漏洞。\n" +
"[本草稿待审计组长复核后定稿]");
n3.setAuthor("陈审计");
n3.setConfidential(true);
n3.setCreatedAt(now.minus(8, ChronoUnit.DAYS));
n3.setUpdatedAt(now.minus(8, ChronoUnit.DAYS));
noteRepo.save(n3);
// 案件3(物料差异)的调查底稿
FraudInvestigationNote n4 = new FraudInvestigationNote();
n4.setReportId(caseMaterial.getId());
n4.setNoteType("调查记录");
n4.setTitle("仓库物料差异核查记录");
n4.setContent("第一轮核查(2026-03-10至15):\n" +
"- 调阅品号RM-0312出入库台账3个月记录,确认账面减少量与出库单差异如下:\n" +
" · 2025-12:账面差异2.1吨(市值约12.6万元)\n" +
" · 2026-01:账面差异3.8吨(市值约22.8万元)\n" +
" · 2026-02:账面差异5.1吨(市值约30.6万元)\n" +
"差异呈扩大趋势,不符合正常损耗规律(正常损耗≤0.5%)。");
n4.setAuthor("王审计");
n4.setConfidential(true);
n4.setCreatedAt(now.minus(16, ChronoUnit.DAYS));
n4.setUpdatedAt(now.minus(16, ChronoUnit.DAYS));
noteRepo.save(n4);
FraudInvestigationNote n5 = new FraudInvestigationNote();
n5.setReportId(caseMaterial.getId());
n5.setNoteType("证据清单");
n5.setTitle("监控录像异常时段记录(已提取)");
n5.setContent("经IT部协助提取监控录像,发现以下异常时段:\n" +
"- 2026-01-08 凌晨02:37:仓库东侧门禁刷卡开门(非正常班次),车辆编号X未登记派车\n" +
"- 2026-02-14 晚间19:45:同类异常,同一刷卡人员(工号W-0088,已提取比对)\n" +
"文件已加密保存,文件哈希记录于此。\n" +
"下一步:比对工号W-0088身份,约谈问询。");
n5.setFileRef("surveillance_extract_20260310.mp4.enc");
n5.setFileHash("a8f3b2c1d4e5f6789012345678901234abcdef0123456789abcdef0123456789ab");
n5.setAuthor("王审计");
n5.setConfidential(true);
n5.setCreatedAt(now.minus(11, ChronoUnit.DAYS));
n5.setUpdatedAt(now.minus(11, ChronoUnit.DAYS));
noteRepo.save(n5);
}
// ---- 内部制度库种子 ----
private void seedInternalRegulations() {
if (internalRegRepo.count() > 0) return;
Instant now = Instant.now();
Object[][] data = {
// {title, docType, scope, responsibleDept, version, status, summary, publishDate}
{"采购管理制度", "管理制度", "全公司", "采购部", "V3.1",
"已发布", "规范公司各类物资、服务采购行为,明确采购权限、比价要求、供应商资质审查、" +
"合同签订、入库验收及付款程序,防范采购风险。含利益冲突申报条款:采购人员与供应商存在" +
"亲属或利益关系必须书面申报,并在该笔采购中回避。", "2025-03-01"},
{"费用报销管理规定", "管理制度", "全公司", "财务部", "V2.3",
"已发布", "规定差旅费、招待费、办公用品、业务费等各类费用的报销标准、审批权限及流程。" +
"核心规定:500元以上报销需部门负责人审批;2000元以上需分管领导审批;单笔发票必须为" +
"增值税普通发票或专用发票,不接受手写收据。", "2025-01-15"},
{"供应商管理办法", "管理制度", "采购部", "采购部", "V2.0",
"已发布", "建立供应商准入、评估、分级管理体系。规定供应商须具备合规资质证书," +
"年度履约评估得分低于70分须整改,连续两年低于60分取消合格供应商资格。", "2024-09-01"},
{"资金管理与支付制度", "管理制度", "全公司", "财务部", "V4.0",
"已发布", "规定公司资金收付管理,包括账户开立审批、付款凭证要求、大额付款" +
"(50万元以上)双人复核制度、银行对账规程及资金预测报告编制要求。", "2025-06-01"},
{"员工行为准则与廉洁从业规定", "管理制度", "全公司", "人力资源部", "V1.5",
"已发布", "规定员工在工作中应遵守的行为准则,包括禁止收受供应商/客户礼品(单次价值超100元)、" +
"禁止利用公司资源谋取私利、禁止泄露公司商业机密等。违规处分包括警告、记过、解除劳动合同。",
"2024-04-01"},
{"合同管理办法", "管理制度", "全公司", "法务风险部", "V3.0",
"已发布", "规范公司各类合同的起草、审核、签订、履约管理及台账归档流程。合同签订前须经" +
"法务合规审核;合同金额超过100万元须经总经理审批;合同变更须签补充协议并经原审批层级审批。",
"2025-02-10"},
{"固定资产管理制度", "管理制度", "全公司", "财务部", "V2.1",
"已发布", "规定固定资产的购置审批、登记入账、定期盘点、折旧计提及处置核销规程。" +
"每年6月和12月进行全公司固定资产实物盘点,差异须填写盘点差异说明表并说明原因。",
"2024-10-01"}
};
for (int i = 0; i < data.length; i++) {
Object[] row = data[i];
LegalRegulation r = new LegalRegulation();
r.setRegNo(String.format("ZD-2024-%03d", i + 1));
r.setTitle((String) row[0]);
r.setDocType((String) row[1]);
r.setScope((String) row[2]);
r.setResponsibleDept((String) row[3]);
r.setVersion((String) row[4]);
r.setStatus((String) row[5]);
r.setSummary((String) row[6]);
r.setPublishDate((String) row[7]);
r.setReadCount((int)(Math.random() * 80) + 5);
r.setPublisher("法务合规部 审核发布");
r.setCreatedAt(now.minus(365 - i * 30L, ChronoUnit.DAYS));
internalRegRepo.save(r);
}
}
// ---- 外部法规库种子 ----
private void seedExternalRegulations() {
if (externalRegRepo.count() > 0) return;
Instant now = Instant.now();
Object[][] data = {
// {regNo, title, issuer, regType, issueDate, effectiveDate, impactDescription, priority}
{"GB/T 19001-2016", "质量管理体系 要求",
"国家质量监督检验检疫总局", "行业法规", "2016-12-30", "2017-07-01",
"要求公司建立完整的质量管理体系,通过内外部审核持续改进。" +
"内控部门需审计质量管理程序合规性,确保文件控制、不合格品处理、纠正预防措施等程序有效运行。",
""},
{"中华人民共和国会计法(2024修订)", "中华人民共和国会计法",
"全国人民代表大会常务委员会", "金融法规", "2024-06-28", "2025-01-01",
"要求会计信息真实完整,禁止伪造、变造会计凭证和会计账簿。" +
"审计部门需重点核查:会计凭证的完整性、会计账簿是否存在篡改痕迹、" +
"会计信息是否与业务实质一致。违规可追究刑事责任。",
""},
{"劳动合同法(2012修订版)", "中华人民共和国劳动合同法",
"全国人民代表大会常务委员会", "劳动法", "2012-12-28", "2013-07-01",
"规定劳动合同的签订、履行、变更、解除和终止。审计重点:" +
"员工是否均签订正式劳动合同、试用期是否合规、" +
"劳动合同变更(如岗位调整)是否经双方书面同意、解除合同是否符合法定程序。",
""},
{"网络安全法", "中华人民共和国网络安全法",
"全国人民代表大会常务委员会", "数据安全法", "2016-11-07", "2017-06-01",
"要求建立网络安全保护制度,对关键信息基础设施实施重点保护。" +
"内部审计需核查:数据访问权限管理是否合规、是否建立安全事件应急预案、" +
"员工账号管理是否满足最小权限原则。",
""},
{"政府会计准则第1号——存货", "政府会计准则第1号",
"财政部", "行业法规", "2016-07-06", "2017-01-01",
"规定存货的确认、计量和报告要求。审计时需核查:" +
"存货盘点程序是否到位、库存商品是否按成本法计量、" +
"跌价准备是否按规定计提。",
""},
{"个人信息保护法", "中华人民共和国个人信息保护法",
"全国人民代表大会常务委员会", "数据安全法", "2021-08-20", "2021-11-01",
"规范个人信息处理活动,保护个人信息权益。HR部门审计关注点:" +
"员工个人信息收集范围是否超出业务必要范围、" +
"是否明示告知信息使用目的、" +
"离职员工信息是否按规定删除/脱敏处理。",
""}
};
for (Object[] row : data) {
ExternalRegulation r = new ExternalRegulation();
r.setRegNo((String) row[0]);
r.setTitle((String) row[1]);
r.setIssuer((String) row[2]);
r.setRegType((String) row[3]);
r.setIssueDate((String) row[4]);
r.setEffectiveDate((String) row[5]);
r.setStatus("现行有效");
r.setImpactDescription((String) row[6]);
r.setPriority((String) row[7]);
r.setCreatedAt(now.minus(90, ChronoUnit.DAYS));
r.setUpdatedAt(now.minus(90, ChronoUnit.DAYS));
externalRegRepo.save(r);
}
}
}
@@ -0,0 +1,108 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.BidTenderCollab;
import com.kaidi.oa.repository.BidTenderCollabRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
/**
* 标书协作工单演示数据。
*
* 覆盖三个办事处(岳阳/长沙/湖南分公司)×四种协作类型×五种状态,
* 让前端看板活体可见且流程闭环可演示。
* 幂等:若已有数据则跳过,防止重启重复写入。
*/
@Component
@Order(215)
public class BidTenderCollabSeeder implements ApplicationRunner {
private final BidTenderCollabRepository repo;
public BidTenderCollabSeeder(BidTenderCollabRepository repo) {
this.repo = repo;
}
@Override
public void run(ApplicationArguments args) {
if (repo.count() > 0) return;
// 岳阳办事处 → 本地价格提供 → 资料完备(已闭环)
collab(3L, "本地价格提供",
"岳阳城乡污水处理厂运营服务项目·本地设备单价清单",
"岳阳办事处", "王芳", "李明",
"提供岳阳本地鼓风机、格栅、污泥脱水机等设备市场含税单价(含 3 家供应商报价),"
+ "并补充岳阳市政工程劳务综合费率(元/m³)。",
null, "2026-05-28", "资料完备",
"收到价格清单,设备单价与公司库差异±5%以内,劳务费率参考岳阳市定额,已采纳。");
// 长沙办事处 → 资质材料提供 → 总部审阅中(进行中)
collab(2L, "资质材料提供",
"湖南省高速公路机电EPC总承包项目·资质证件扫描包",
"长沙办事处", "张伟", "赵磊",
"提供凯迪湖南分公司安全生产许可证(2025-2028)、测量资质证书、EPC 总承包资质、"
+ "近三年同类项目业绩合同(选取金额≥5000万的6份)。",
null, "2026-07-01", "总部审阅中",
null);
// 长沙办事处 → 踏勘报告 → 已提交(等待总部审阅)
collab(7L, "踏勘报告",
"湖南交投园区道路工程信息化项目·现场踏勘报告",
"长沙办事处", "张伟", "陈静",
"踏勘内容:①园区路网现状测量(全长 4.2km,双向四车道);②地下管网探测结果;"
+ "③智慧路灯/监控点位分布;④施工条件(净空/运输限制)说明。",
null, "2026-07-25", "已提交",
null);
// 岳阳办事处 → 综合协助 → 待提交(刚发起)
collab(5L, "综合协助",
"岳阳楼区市政排水管网改造项目·中标后总部协作清单",
"岳阳办事处", "刘洋", "李明",
"中标后总部协作事项:①协助提供本地分包商资质(市政管道/土方);"
+ "②提供岳阳市区开挖许可申请模板;③同步本地监理机构备选名单。",
null, "2026-06-20", "待提交",
null);
// 湖南分公司 → 本地价格提供 → 已归档(全流程闭环)
collab(4L, "本地价格提供",
"怀化市政道路智慧化改造项目·劳务及材料综合报价",
"湖南分公司", "刘洋", "赵磊",
"提供怀化市区人工单价(综合工日价)+主要材料价格:沥青混凝土(AC-13/AC-20)、"
+ "预制混凝土管(DN400/600)、信号灯控制器;附怀化发改委2026Q1建材指导价文件扫描。",
null, "2026-05-20", "已归档",
"材料价格与总部测算偏差<3%,采纳并已计入投标报价。标书已提交,本协作归档。");
// 岳阳办事处 → 资质材料提供 → 资料完备(完整链路演示)
collab(8L, "资质材料提供",
"靖州县水利局环保设备采购项目·竞争性谈判资质包",
"岳阳办事处", "王芳", "李明",
"竞争性谈判资质:①营业执照副本;②一般纳税人资格证书;"
+ "③环保设备生产厂家授权函(含三联环保/国祯环境);④同类业绩(靖州/怀化各一份)。",
null, "2026-05-30", "资料完备",
"资质核实完毕,授权函原件联系方确认有效,已整理入标书附件包,审核通过。");
}
private void collab(Long bidId, String collabType, String subject,
String branchName, String submitter, String hqReviewer,
String materialSummary, Long storedFileId, String dueDate,
String status, String reviewNote) {
BidTenderCollab c = new BidTenderCollab();
c.setBidId(bidId);
c.setCollabType(collabType);
c.setSubject(subject);
c.setBranchName(branchName);
c.setSubmitter(submitter);
c.setHqReviewer(hqReviewer);
c.setMaterialSummary(materialSummary);
c.setStoredFileId(storedFileId);
c.setDueDate(dueDate);
c.setStatus(status);
c.setReviewNote(reviewNote);
c.setCreatedAt(Instant.now());
c.setUpdatedAt(Instant.now());
repo.save(c);
}
}
@@ -0,0 +1,204 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.BidDocVersion;
import com.kaidi.oa.domain.BidTenderDoc;
import com.kaidi.oa.repository.BidDocVersionRepository;
import com.kaidi.oa.repository.BidRepository;
import com.kaidi.oa.repository.BidTenderDocRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.time.LocalDate;
/**
* 招标文件台账 + 投标文件协同编制版本演示数据。
*
* @Order(215) 在所有主数据 Seeder 之后运行;幂等(count > 0 则跳过)。
*/
@Component
@Order(215)
public class BidTenderDocSeeder implements CommandLineRunner {
private final BidTenderDocRepository tenderDocRepo;
private final BidDocVersionRepository docVersionRepo;
private final BidRepository bidRepo;
public BidTenderDocSeeder(BidTenderDocRepository tenderDocRepo,
BidDocVersionRepository docVersionRepo,
BidRepository bidRepo) {
this.tenderDocRepo = tenderDocRepo;
this.docVersionRepo = docVersionRepo;
this.bidRepo = bidRepo;
}
@Override
public void run(String... args) {
if (tenderDocRepo.count() > 0) return; // 幂等
// 取前两个投标 id(若存在)
var bids = bidRepo.findAll();
long bid1 = bids.isEmpty() ? 1L : bids.get(0).getId();
long bid2 = bids.size() > 1 ? bids.get(1).getId() : bid1;
Instant now = Instant.now();
LocalDate today = LocalDate.now();
// ====== 招标文件台账演示数据 ======
// bid1 — 招标文件(已下载,已付标书费)
BidTenderDoc d1 = new BidTenderDoc();
d1.setBidId(bid1);
d1.setDocType("招标文件");
d1.setDocTitle("长沙市污水处理厂扩容工程招标文件(第一册)");
d1.setFileRef("files/bid-tender/招标文件_污水厂扩容_v1.pdf");
d1.setDownloadDate(today.minusDays(20).toString());
d1.setPurchaseFee(Money.of(500.00));
d1.setFeePaidStatus("已付");
d1.setDeadlineDate(today.plusDays(5).toString());
d1.setStatus("已下载");
d1.setBondAmount(Money.of(50000.00));
d1.setBondPayMethod("银行转账");
d1.setBondStatus("已缴");
d1.setOwner("陈志远");
d1.setCreatedAt(now);
tenderDocRepo.save(d1);
// bid1 — 澄清文件
BidTenderDoc d2 = new BidTenderDoc();
d2.setBidId(bid1);
d2.setDocType("澄清文件");
d2.setDocTitle("长沙市污水处理厂扩容工程招标澄清函(第1号)");
d2.setFileRef("files/bid-tender/澄清函_污水厂扩容_01.pdf");
d2.setDownloadDate(today.minusDays(10).toString());
d2.setPurchaseFee(Money.ZERO);
d2.setFeePaidStatus("已付");
d2.setDeadlineDate(today.plusDays(5).toString());
d2.setStatus("已下载");
d2.setBondAmount(Money.ZERO);
d2.setBondStatus("未缴");
d2.setOwner("陈志远");
d2.setCreatedAt(now);
tenderDocRepo.save(d2);
// bid1 — 答疑文件
BidTenderDoc d3 = new BidTenderDoc();
d3.setBidId(bid1);
d3.setDocType("答疑文件");
d3.setDocTitle("长沙市污水处理厂扩容工程投标答疑(汇总版)");
d3.setFileRef("files/bid-tender/答疑_污水厂扩容_汇总.pdf");
d3.setDownloadDate(today.minusDays(7).toString());
d3.setPurchaseFee(Money.ZERO);
d3.setFeePaidStatus("已付");
d3.setDeadlineDate(today.plusDays(5).toString());
d3.setStatus("已下载");
d3.setBondAmount(Money.ZERO);
d3.setBondStatus("未缴");
d3.setRemark("答疑共 12 条,涉及工艺方案和报价清单调整");
d3.setOwner("陈志远");
d3.setCreatedAt(now);
tenderDocRepo.save(d3);
// bid2 — 招标文件(未付标书费,保证金逾期未退)
BidTenderDoc d4 = new BidTenderDoc();
d4.setBidId(bid2);
d4.setDocType("招标文件");
d4.setDocTitle("株洲市道路改造工程招标文件");
d4.setFileRef("files/bid-tender/招标文件_株洲道路_v1.pdf");
d4.setDownloadDate(today.minusDays(65).toString());
d4.setPurchaseFee(Money.of(300.00));
d4.setFeePaidStatus("未付");
d4.setDeadlineDate(today.minusDays(30).toString()); // 已过期
d4.setStatus("已下载");
d4.setBondAmount(Money.of(30000.00));
d4.setBondPayMethod("保险保函");
d4.setBondStatus("已缴"); // 逾期未退,催办
d4.setRemark("未中标,保证金应已退回但未确认,请催办");
d4.setOwner("刘伟");
d4.setCreatedAt(now.minusSeconds(3600 * 24 * 65));
tenderDocRepo.save(d4);
// bid2 — 补充文件(截止日临近预警演示)
BidTenderDoc d5 = new BidTenderDoc();
d5.setBidId(bid2);
d5.setDocType("补充文件");
d5.setDocTitle("株洲市道路改造工程补充技术要求");
d5.setFileRef("files/bid-tender/补充要求_株洲道路.pdf");
d5.setDownloadDate(today.minusDays(3).toString());
d5.setPurchaseFee(Money.ZERO);
d5.setFeePaidStatus("已付");
d5.setDeadlineDate(today.plusDays(2).toString()); // 紧急 — 2天内到期
d5.setStatus("待处理");
d5.setBondAmount(Money.ZERO);
d5.setBondStatus("未缴");
d5.setOwner("刘伟");
d5.setCreatedAt(now);
tenderDocRepo.save(d5);
// ====== 投标文件版本协同演示数据 ======
if (docVersionRepo.count() > 0) return;
// bid1 商务标 — v1 草稿 → v2 审核中
BidDocVersion v1 = new BidDocVersion();
v1.setBidId(bid1);
v1.setDocPart("商务标");
v1.setVersion("v1");
v1.setDocTitle("污水厂扩容工程商务标(初稿)");
v1.setFileRef("files/bid-doc/商务标_污水厂_v1.docx");
v1.setCollaborators("陈志远,李明");
v1.setSubmitter("陈志远");
v1.setStatus("已驳回");
v1.setReviewComment("报价清单有误,需根据答疑调整综合单价");
v1.setAnnotations("[李明 @ " + today.minusDays(12) + "] 第3章综合单价表需重算\n[陈志远 @ " + today.minusDays(11) + "] 已核,修订后提交v2");
v1.setSubmittedAt(now.minusSeconds(3600 * 24 * 12));
v1.setCreatedAt(now.minusSeconds(3600 * 24 * 14));
docVersionRepo.save(v1);
BidDocVersion v2 = new BidDocVersion();
v2.setBidId(bid1);
v2.setDocPart("商务标");
v2.setVersion("v2");
v2.setDocTitle("污水厂扩容工程商务标(修订版)");
v2.setFileRef("files/bid-doc/商务标_污水厂_v2.docx");
v2.setCollaborators("陈志远,李明,张华");
v2.setSubmitter("陈志远");
v2.setStatus("审核中");
v2.setAnnotations("[张华 @ " + today.minusDays(5) + "] 综合单价已按答疑调整,请财务复核");
v2.setSubmittedAt(now.minusSeconds(3600 * 24 * 5));
v2.setCreatedAt(now.minusSeconds(3600 * 24 * 7));
docVersionRepo.save(v2);
// bid1 技术标 — v1 已定稿
BidDocVersion v3 = new BidDocVersion();
v3.setBidId(bid1);
v3.setDocPart("技术标");
v3.setVersion("v1");
v3.setDocTitle("污水厂扩容工程技术标(定稿)");
v3.setFileRef("files/bid-doc/技术标_污水厂_v1_final.docx");
v3.setCollaborators("王涛,赵雪");
v3.setSubmitter("王涛");
v3.setStatus("已定稿");
v3.setReviewComment("技术方案完整,已通过合规审核");
v3.setAnnotations("[赵雪 @ " + today.minusDays(8) + "] 施工方案第4节水处理工艺描述更新\n[王涛 @ " + today.minusDays(7) + "] 全文定稿确认");
v3.setSubmittedAt(now.minusSeconds(3600 * 24 * 8));
v3.setFinalizedAt(now.minusSeconds(3600 * 24 * 7));
v3.setCreatedAt(now.minusSeconds(3600 * 24 * 10));
docVersionRepo.save(v3);
// bid1 报价单 — v1 草稿
BidDocVersion v4 = new BidDocVersion();
v4.setBidId(bid1);
v4.setDocPart("报价单");
v4.setVersion("v1");
v4.setDocTitle("污水厂扩容工程报价单(草稿)");
v4.setFileRef("files/bid-doc/报价单_污水厂_v1.xlsx");
v4.setCollaborators("李明,周斌");
v4.setSubmitter("李明");
v4.setStatus("草稿");
v4.setAnnotations("[周斌 @ " + today.minusDays(3) + "] 材料单价已更新至2026年5月指导价");
v4.setSubmittedAt(now.minusSeconds(3600 * 24 * 3));
v4.setCreatedAt(now.minusSeconds(3600 * 24 * 4));
docVersionRepo.save(v4);
}
}
@@ -0,0 +1,225 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.ArApItem;
import com.kaidi.oa.domain.OrgBranch;
import com.kaidi.oa.repository.ArApItemRepository;
import com.kaidi.oa.repository.OrgBranchRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
/**
* 分公司财务报表演示数据种子(经营数据分析 §8 缺口补完)。
*
* 审计缺口:
* 1. OrgBranch 无种子数据 → /api/oa/branch-fin-statements/branches 返回空列表,
* 整个分公司独立财务报表功能无法演示。
* 2. ArApItem 无种子数据 + companySubject 未设 → 现金流量表直接法用 Payment.subject
* 模糊匹配作过渡;资产负债表 AR/AP 科目为零。
*
* 本种子补全:
* - 5 条 OrgBranch(总部 1 + 分公司 3 + 办事处 1),覆盖岳阳分公司/华东分公司/华南分公司。
* - 20 条 ArApItem(应收 12 + 应付 8),按 companySubject 与 OrgBranch.name 一致,
* 使分公司维度的应收/应付/已核销/未核销在报表中实际显示。
*
* 幂等:OrgBranch 存在则跳过(guard on count > 0)。
*
* @Order(15) 在 DataSeeder(@Order 1) 之后独立运行。
*/
@Component
@Order(15)
public class BranchFinDemoSeeder implements ApplicationRunner {
private final OrgBranchRepository orgBranchRepo;
private final ArApItemRepository arApRepo;
public BranchFinDemoSeeder(OrgBranchRepository orgBranchRepo,
ArApItemRepository arApRepo) {
this.orgBranchRepo = orgBranchRepo;
this.arApRepo = arApRepo;
}
@Override
public void run(ApplicationArguments args) {
seedOrgBranches();
seedArApItems();
}
// ============================================================
// 1. OrgBranch 机构目录
// ============================================================
private void seedOrgBranches() {
if (orgBranchRepo.count() > 0) {
return;
}
List<OrgBranch> seeds = new ArrayList<>();
seeds.add(branch("HQ001", "湖南凯迪工程科技有限公司", "总部", null,
"湖南长沙", "张伟", "0731-88889999", "长沙市岳麓区科技中路88号", "运营中",
"凯迪集团总部,负责全国市场统筹与管理"));
seeds.add(branch("YY001", "湖南凯迪工程科技有限公司岳阳分公司", "分公司", "HQ001",
"湖南岳阳", "陈静", "0730-22223333", "岳阳市云溪区长炼路66号", "运营中",
"负责岳阳及周边区域工程项目承接与实施"));
seeds.add(branch("HD001", "凯迪(华东)工程技术有限公司", "分公司", "HQ001",
"华东地区", "李明", "021-66668888", "上海市浦东新区世纪大道1368号", "运营中",
"负责华东区(沪苏浙皖闽赣)市场开拓与项目执行"));
seeds.add(branch("HN001", "凯迪(华南)工程服务有限公司", "分公司", "HQ001",
"华南地区", "赵云", "020-33337777", "广州市天河区珠江新城华夏路1号", "运营中",
"负责华南区(粤桂琼)市场开拓与EPC工程管理"));
seeds.add(branch("CS001", "凯迪长沙办公室", "办事处", "HQ001",
"湖南长沙", "王芳", "0731-55556666", "长沙市雨花区劳动中路158号", "运营中",
"长沙本地业务联络与客户服务办事处"));
orgBranchRepo.saveAll(seeds);
}
private OrgBranch branch(String code, String name, String type, String parentCode,
String region, String manager, String phone, String address,
String status, String remark) {
OrgBranch b = new OrgBranch();
b.setCode(code);
b.setName(name);
b.setType(type);
b.setParentCode(parentCode);
b.setRegion(region);
b.setManager(manager);
b.setPhone(phone);
b.setAddress(address);
b.setStatus(status);
b.setRemark(remark);
b.setCreatedAt(Instant.now());
return b;
}
// ============================================================
// 2. ArApItem 应收/应付 演示数据(含 companySubject 精准关联分公司)
// ============================================================
private void seedArApItems() {
if (arApRepo.count() > 0) {
return;
}
List<ArApItem> seeds = new ArrayList<>();
// --- 岳阳分公司 应收账款 ---
seeds.add(arApItem("AR-YY-2026-001", "应收", "岳阳市政工程集团有限公司",
"HT-YY-2026-001", "湖南凯迪工程科技有限公司岳阳分公司",
"3800000.00", "2280000.00", "1520000.00", "部分核销", "2026-09-30"));
seeds.add(arApItem("AR-YY-2026-002", "应收", "岳阳县水务局",
"HT-YY-2026-002", "湖南凯迪工程科技有限公司岳阳分公司",
"1650000.00", "990000.00", "660000.00", "部分核销", "2026-08-31"));
seeds.add(arApItem("AR-YY-2026-003", "应收", "湖南云溪工业园管委会",
"HT-YY-2026-003", "湖南凯迪工程科技有限公司岳阳分公司",
"920000.00", "0.00", "920000.00", "未核销", "2026-12-31"));
seeds.add(arApItem("AR-YY-2026-004", "应收", "岳阳华能电厂",
"HT-YY-2026-004", "湖南凯迪工程科技有限公司岳阳分公司",
"560000.00", "560000.00", "0.00", "已结清", "2026-06-30"));
// --- 岳阳分公司 应付账款 ---
seeds.add(arApItem("AP-YY-2026-001", "应付", "岳阳通达设备租赁有限公司",
"HT-YY-2026-AP01", "湖南凯迪工程科技有限公司岳阳分公司",
"386000.00", "193000.00", "193000.00", "部分核销", "2026-07-31"));
seeds.add(arApItem("AP-YY-2026-002", "应付", "长沙华信网络科技有限公司",
"HT-YY-2026-AP02", "湖南凯迪工程科技有限公司岳阳分公司",
"128000.00", "0.00", "128000.00", "未核销", "2026-09-30"));
// --- 华东分公司 应收账款 ---
seeds.add(arApItem("AR-HD-2026-001", "应收", "上海浦东建设集团有限公司",
"HT-HD-2026-001", "凯迪(华东)工程技术有限公司",
"5200000.00", "3120000.00", "2080000.00", "部分核销", "2026-10-31"));
seeds.add(arApItem("AR-HD-2026-002", "应收", "江苏南京城市基建有限公司",
"HT-HD-2026-002", "凯迪(华东)工程技术有限公司",
"3100000.00", "1860000.00", "1240000.00", "部分核销", "2026-09-30"));
seeds.add(arApItem("AR-HD-2026-003", "应收", "浙江绍兴市污水处理有限公司",
"HT-HD-2026-003", "凯迪(华东)工程技术有限公司",
"1880000.00", "1128000.00", "752000.00", "部分核销", "2026-11-30"));
seeds.add(arApItem("AR-HD-2026-004", "应收", "安徽合肥市水务集团",
"HT-HD-2026-004", "凯迪(华东)工程技术有限公司",
"2650000.00", "0.00", "2650000.00", "未核销", "2026-12-31"));
// --- 华东分公司 应付账款 ---
seeds.add(arApItem("AP-HD-2026-001", "应付", "上海华申建材有限公司",
"HT-HD-2026-AP01", "凯迪(华东)工程技术有限公司",
"680000.00", "340000.00", "340000.00", "部分核销", "2026-08-31"));
seeds.add(arApItem("AP-HD-2026-002", "应付", "南京精工机械设备有限公司",
"HT-HD-2026-AP02", "凯迪(华东)工程技术有限公司",
"420000.00", "0.00", "420000.00", "未核销", "2026-10-31"));
seeds.add(arApItem("AP-HD-2026-003", "应付", "苏州翔宇临时用工服务有限公司",
"HT-HD-2026-AP03", "凯迪(华东)工程技术有限公司",
"185000.00", "92500.00", "92500.00", "部分核销", "2026-09-15"));
// --- 华南分公司 应收账款 ---
seeds.add(arApItem("AR-HN-2026-001", "应收", "广州市城市建设开发有限公司",
"HT-HN-2026-001", "凯迪(华南)工程服务有限公司",
"4800000.00", "2400000.00", "2400000.00", "部分核销", "2026-11-30"));
seeds.add(arApItem("AR-HN-2026-002", "应收", "深圳市水务集团有限公司",
"HT-HN-2026-002", "凯迪(华南)工程服务有限公司",
"3200000.00", "3200000.00", "0.00", "已结清", "2026-06-30"));
seeds.add(arApItem("AR-HN-2026-003", "应收", "广西南宁城投集团有限公司",
"HT-HN-2026-003", "凯迪(华南)工程服务有限公司",
"1960000.00", "980000.00", "980000.00", "部分核销", "2026-10-31"));
seeds.add(arApItem("AR-HN-2026-004", "应收", "海南省生态环境工程有限公司",
"HT-HN-2026-004", "凯迪(华南)工程服务有限公司",
"1100000.00", "0.00", "1100000.00", "未核销", "2027-03-31"));
// --- 华南分公司 应付账款 ---
seeds.add(arApItem("AP-HN-2026-001", "应付", "广州宏基建材有限公司",
"HT-HN-2026-AP01", "凯迪(华南)工程服务有限公司",
"540000.00", "270000.00", "270000.00", "部分核销", "2026-09-30"));
seeds.add(arApItem("AP-HN-2026-002", "应付", "深圳福田劳务派遣有限公司",
"HT-HN-2026-AP02", "凯迪(华南)工程服务有限公司",
"168000.00", "84000.00", "84000.00", "部分核销", "2026-08-31"));
// --- 总部 应收账款 ---
seeds.add(arApItem("AR-HQ-2026-001", "应收", "靖州供水项目业主方",
"HT-HQ-2026-001", "湖南凯迪工程科技有限公司",
"8600000.00", "5160000.00", "3440000.00", "部分核销", "2026-12-31"));
arApRepo.saveAll(seeds);
}
private ArApItem arApItem(String code, String arApType, String partyName,
String relatedRef, String companySubject,
String amount, String writtenOff, String unwrittenOff,
String status, String dueDate) {
ArApItem a = new ArApItem();
a.setCode(code);
a.setArApType(arApType);
a.setPartyName(partyName);
a.setRelatedRef(relatedRef);
a.setCompanySubject(companySubject);
a.setAmount(Money.of(Double.parseDouble(amount)));
a.setWrittenOff(Money.of(Double.parseDouble(writtenOff)));
a.setUnwrittenOff(Money.of(Double.parseDouble(unwrittenOff)));
a.setStatus(status);
a.setDueDate(dueDate);
a.setCreatedAt(Instant.now());
return a;
}
}
@@ -0,0 +1,190 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.BrandCollabPush;
import com.kaidi.oa.domain.EmployeeShareMaterial;
import com.kaidi.oa.repository.BrandCollabPushRepository;
import com.kaidi.oa.repository.EmployeeShareMaterialRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
/**
* 宣传部 W7 缺口演示数据。
*
* 缺口1(low):员工素材中心 EmployeeShareMaterial — 企业简介PPT/宣传视频/标准照片等。
* 缺口2(med)BrandCollabController push-ops/push-legal/push-finance 演示数据。
*
* @Order(220) 在所有主数据 Seeder 之后运行;幂等(count > 0 则跳过)。
*/
@Component
@Order(220)
public class BrandCultureGapSeeder implements ApplicationRunner {
private final EmployeeShareMaterialRepository empMatRepo;
private final BrandCollabPushRepository pushRepo;
public BrandCultureGapSeeder(EmployeeShareMaterialRepository empMatRepo,
BrandCollabPushRepository pushRepo) {
this.empMatRepo = empMatRepo;
this.pushRepo = pushRepo;
}
@Override
public void run(ApplicationArguments args) {
seedEmployeeShareMaterials();
seedCollabPushExtended();
}
// ====================================================================
// 员工宣传素材中心演示数据(EmployeeShareMaterial
// ====================================================================
private void seedEmployeeShareMaterials() {
if (empMatRepo.count() > 0) return;
List<EmployeeShareMaterial> list = new ArrayList<>();
list.add(mat("ESM-001", "凯迪科技集团企业简介PPT(2026版)", "企业简介PPT", "全员",
"最新版公司简介PPT,包含公司历程、业务版图、核心优势、标志性案例。员工对外介绍公司时使用,务必使用此版本,勿私自修改。",
"https://cdn.kaidi.com/brand/2026/kaidi-intro-v3.2.pptx",
"https://cdn.kaidi.com/brand/thumb/ppt-intro.png",
"v3.2", "企业简介,PPT,对外宣传", "宣传部·王主任", "上架",
"2026-01-01", null));
list.add(mat("ESM-002", "凯迪科技2026品牌宣传片(3分钟精华版)", "宣传视频", "全员",
"2026年最新企业形象宣传片,时长3分钟,适用于客户拜访前播放、展会大屏、社交媒体转发。B站/抖音/朋友圈均可使用。",
"https://cdn.kaidi.com/brand/2026/kaidi-brand-film-3min.mp4",
"https://cdn.kaidi.com/brand/thumb/film-2026.jpg",
"v1.0", "宣传视频,品牌形象,展会", "宣传部·李部长", "上架",
"2026-01-15", null));
list.add(mat("ESM-003", "凯迪标准企业LOGO(全套)", "标准照片", "全员",
"企业标准LOGO矢量文件和PNG高清图,包含彩色版/黑白版/深色背景版。用于名片、合同封面、PPT模板、物料设计等场合。请勿变形拉伸。",
"https://cdn.kaidi.com/brand/logo/kaidi-logo-pack-2026.zip",
"https://cdn.kaidi.com/brand/thumb/logo.png",
"v2.0", "LOGO,品牌标识,矢量", "宣传部·王主任", "上架",
"2026-01-01", null));
list.add(mat("ESM-004", "凯迪智慧建筑产品系列介绍(销售版)", "产品介绍", "销售",
"面向销售团队的产品介绍PPT,含智能传感器XT系列、建筑管理系统BMS-Pro、环境监控平台等核心产品参数及差异化卖点。含客户竞品对比分析(内部机密,勿外传完整版)。",
"https://cdn.kaidi.com/brand/product/kaidi-product-sales-2026.pptx",
"https://cdn.kaidi.com/brand/thumb/product-ppt.png",
"v2.1", "产品介绍,销售工具,智慧建筑", "宣传部·王主任", "上架",
"2026-03-01", "2026-12-31"));
list.add(mat("ESM-005", "凯迪企业宣传册(2026双语精装版)", "宣传手册", "全员",
"A4精装宣传册PDF电子版,中英文双语,120页,适用于对外寄送、展会发放、客户会议。如需印刷版请联系宣传部申请。",
"https://cdn.kaidi.com/brand/2026/kaidi-brochure-2026-cn-en.pdf",
"https://cdn.kaidi.com/brand/thumb/brochure-2026.jpg",
"v1.0", "宣传册,双语,对外", "宣传部·李部长", "上架",
"2026-02-01", null));
list.add(mat("ESM-006", "凯迪员工标准证件照背景及拍摄规范", "标准照片", "全员",
"员工对外代表公司出席活动、接受采访时适用的证件照拍摄规范,含背景色板(凯迪蓝/白)、着装要求、尺寸规格。",
"https://cdn.kaidi.com/brand/standard/employee-photo-spec.pdf",
"https://cdn.kaidi.com/brand/thumb/photo-spec.png",
"v1.0", "证件照,员工形象,规范", "宣传部·王主任", "上架",
"2026-01-01", null));
list.add(mat("ESM-007", "凯迪科技十周年品牌故事H5", "宣传视频", "全员",
"十周年品牌故事互动H5,适合朋友圈、微信群、微博转发。点击即可在手机端播放,含创始人寄语和十年历程时间轴。",
"https://h5.kaidi.com/10th-anniversary",
"https://cdn.kaidi.com/brand/thumb/h5-10th.jpg",
"v1.0", "十周年,H5,朋友圈", "宣传部·李部长", "上架",
"2026-05-01", "2026-12-31"));
list.add(mat("ESM-008", "凯迪高管团队官方照片集", "标准照片", "管理层",
"董事长、CEO及各VP官方形象照(高清),适用于官方文件、媒体采访配图、演讲嘉宾介绍。仅限正式场合使用,请勿用于非正式内容。",
"https://cdn.kaidi.com/brand/executives/exec-photos-2026.zip",
"https://cdn.kaidi.com/brand/thumb/exec.jpg",
"v1.0", "高管,官方照片,媒体", "宣传部·王主任", "待审核",
"2026-01-01", null));
empMatRepo.saveAll(list);
}
private EmployeeShareMaterial mat(String code, String title, String materialType,
String audience, String description,
String fileUrl, String thumbnailUrl,
String version, String tags, String uploader,
String status, String effectDate, String expireDate) {
EmployeeShareMaterial m = new EmployeeShareMaterial();
m.setCode(code);
m.setTitle(title);
m.setMaterialType(materialType);
m.setAudience(audience);
m.setDescription(description);
m.setFileUrl(fileUrl);
m.setThumbnailUrl(thumbnailUrl);
m.setVersion(version);
m.setTags(tags);
m.setUploader(uploader);
m.setStatus(status);
m.setUseCount(0);
m.setEffectDate(effectDate);
m.setExpireDate(expireDate);
m.setCreatedAt(Instant.now());
return m;
}
// ====================================================================
// 经营部/法务风险部/财务部协同推送演示数据(BrandCollabPush 扩展)
// ====================================================================
private void seedCollabPushExtended() {
// 只在推送记录少于5条时补充演示(避免与已有seeder冲突)
long existing = pushRepo.count();
if (existing >= 5) return;
List<BrandCollabPush> pushList = new ArrayList<>();
// 经营部 — 展会信息推送
pushList.add(push("经营部", "展会信息",
"2026深圳国际智慧城市展 — 请宣传部协助物料及宣传支持",
"展会名称:深圳国际智慧城市展\n时间:2026-09-15 ~ 2026-09-18\n展位:Hall B5-208\n需要物料:展架×6、宣传册×300、礼品×50\n联系人:经营部·张经理",
"经营部·张经理", "已推送"));
// 经营部 — 客户活动需求
pushList.add(push("经营部", "客户活动需求",
"2026年夏季客户答谢晚宴 — 宣传物料及H5邀请函制作需求",
"活动时间:2026-07-20\n地点:北京国贸大酒店\n预计嘉宾:80人\n需要:H5邀请函设计、背景板×2、席卡、签到台物料\n截止时间:2026-07-10",
"经营部·李总监", "已接收"));
// 法务风险部 — 文案合规审核
pushList.add(push("法务风险部", "文案合规审核",
"请法务审核:深圳展会主题海报文案《凯迪科技·让建筑更智慧》广告合规性",
"文案内容:「凯迪科技·让建筑更智慧,城市更美好 —— 中国智慧建筑领军者」\n需审核:是否符合广告法,是否涉及最高级/绝对化用语,是否有知识产权风险\n截止时间:2026-08-20",
"宣传部·王主任", "已推送"));
// 法务风险部 — 危机法律支持
pushList.add(push("法务风险部", "危机法律支持",
"舆情事件:网络出现关于凯迪产品质量的负面评论,请法务评估风险",
"事件描述:微博用户@建筑行业观察发布帖子,称某项目使用凯迪BMS系统后频繁出现断连,引发转发。\n初步判断:尚不确定真实性\n需要法务:评估声誉风险等级,是否需要公开回应,是否涉及不正当竞争",
"宣传部·李部长", "已接收"));
// 财务部 — 宣传费用报销通知
pushList.add(push("财务部", "宣传费用报销",
"宣传费用报销通知:展会物料制作费 ¥28,500.00 — 请财务部审核付款",
"报销说明:2026深圳国际智慧城市展展位物料制作费用\n供应商:北京汉唐广告有限公司\n金额:¥28,500.00\n发票号:0023456789\n项目:品牌推广·展会参展\n申请人:宣传部·王主任",
"宣传部·王主任", "已推送"));
pushRepo.saveAll(pushList);
}
private BrandCollabPush push(String targetDept, String pushType,
String subject, String content,
String pusher, String status) {
BrandCollabPush p = new BrandCollabPush();
p.setTargetDept(targetDept);
p.setPushType(pushType);
p.setSubject(subject);
p.setContent(content);
p.setPusher(pusher);
p.setStatus(status);
p.setCreatedAt(Instant.now());
return p;
}
}
@@ -0,0 +1,213 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.BrandEventMaterial;
import com.kaidi.oa.domain.ContentTask;
import com.kaidi.oa.repository.BrandEventMaterialRepository;
import com.kaidi.oa.repository.BrandEventRepository;
import com.kaidi.oa.repository.ContentTaskRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
/**
* 品牌宣传部演示数据:内容创作任务(ContentTask+ 活动物料(BrandEventMaterial)。
*
* 补全两个"活体返回空/无数据"缺口,让前端物料管理与任务分派功能可见可演示。
* @Order(22),在 DataSeeder(@Order 1) 之后运行,幂等(count > 0 跳过)。
*/
@Component
@Order(22)
public class BrandCultureSeeder implements ApplicationRunner {
private final ContentTaskRepository taskRepo;
private final BrandEventMaterialRepository materialRepo;
private final BrandEventRepository eventRepo;
public BrandCultureSeeder(ContentTaskRepository taskRepo,
BrandEventMaterialRepository materialRepo,
BrandEventRepository eventRepo) {
this.taskRepo = taskRepo;
this.materialRepo = materialRepo;
this.eventRepo = eventRepo;
}
@Override
public void run(ApplicationArguments args) {
seedContentTasks();
seedEventMaterials();
}
// ====================================================================
// 内容创作任务演示数据(ContentTask
// ====================================================================
private void seedContentTasks() {
if (taskRepo.count() > 0) {
return;
}
List<ContentTask> tasks = new ArrayList<>();
// 内部员工任务
tasks.add(task("CT-001", "2026年中国建博会主题宣传文案撰写",
"文案", "内部员工", "张雨薇", null,
"2026年中国建博会品牌宣传主稿,需突出绿色建筑/智慧城市核心卖点,字数2000字以内。",
"2026-05-20", "宣传部·李部长", "进行中", null, null));
tasks.add(task("CT-002", "公司官网首页改版视觉设计",
"设计", "内部员工", "陈美珍", null,
"官网首页全新改版,主色调蓝色系,风格现代简洁,需出3套方案供选择。",
"2026-05-30", "宣传部·李部长", "待审核",
"已提交3套方案,请审核后选定", "http://design.kaidi.com/homepage-v2/"));
tasks.add(task("CT-003", "季度产品发布视频脚本及剪辑",
"视频", "内部员工", "刘宇航", null,
"Q2产品发布会宣传片,时长3分钟,需展示新品功能亮点及客户案例。",
"2026-04-10", "宣传部·王主任", "已发布", "内容专业,已发布至官网及抖音", null));
tasks.add(task("CT-004", "行业白皮书《建筑智慧化转型报告》文案",
"文案", "内部员工", "赵晓燕", null,
"与研发部协作,撰写2026年建筑行业智慧化白皮书,预计15000字,分5章。",
"2026-06-30", "品牌总监·张总", "待接受", null, null));
// 外部供应商任务
tasks.add(task("CT-005", "2026年企业宣传册(A4折页)设计制作",
"设计", "外部供应商", "北京汉唐广告有限公司", "SUP-2024-007",
"企业宣传折页设计,A4三折,中英文双语,印刷数量5000份,需提供印前文件。",
"2026-05-15", "宣传部·李部长", "已发布", "印刷文件已提交,质量良好", "http://cdn.kaidi.com/brochure2026.pdf"));
tasks.add(task("CT-006", "展会现场宣传视频制作(60秒竖版)",
"视频", "外部供应商", "上海晨星影视制作", "SUP-2024-012",
"适配展会现场LED大屏,竖版1080x1920,60秒,需配中文字幕及背景音乐。",
"2026-05-25", "宣传部·王主任", "进行中", null, null));
tasks.add(task("CT-007", "公众号专题策划《凯迪十年·匠心同行》图文",
"文案", "外部供应商", "成都创意内容工作室", "SUP-2025-003",
"公司十周年品牌故事系列,共5篇,每篇1500-2000字,配套排版设计。",
"2026-06-10", "宣传部·李部长", "已驳回", "文案风格与品牌调性不符,请重写第2-3篇", null));
tasks.add(task("CT-008", "H5互动邀请函(夏季客户答谢会)",
"设计", "外部供应商", "北京汉唐广告有限公司", "SUP-2024-007",
"客户答谢会电子邀请函H5,含RSVP确认功能,手机端优化,动效精良。",
"2026-06-05", "宣传部·王主任", "待接受", null, null));
taskRepo.saveAll(tasks);
}
private ContentTask task(String code, String title, String taskType,
String assigneeType, String assignee, String supplierCode,
String description, String dueDate, String issuedBy,
String status, String reviewNote, String deliverableUrl) {
ContentTask t = new ContentTask();
t.setCode(code);
t.setTitle(title);
t.setTaskType(taskType);
t.setAssigneeType(assigneeType);
t.setAssignee(assignee);
t.setSupplierCode(supplierCode);
t.setDescription(description);
t.setDueDate(dueDate);
t.setIssuedBy(issuedBy);
t.setStatus(status);
t.setReviewNote(reviewNote);
t.setDeliverableUrl(deliverableUrl);
t.setCreatedAt(Instant.now());
return t;
}
// ====================================================================
// 活动物料演示数据(BrandEventMaterial
// ====================================================================
private void seedEventMaterials() {
if (materialRepo.count() > 0) {
return;
}
// 尝试找到第一个活动,如没有,使用虚拟 eventId=1
Long eventId;
String eventCode;
var events = eventRepo.findAll();
if (!events.isEmpty()) {
var ev = events.get(0);
eventId = ev.getId();
eventCode = ev.getCode();
} else {
// 无活动时仍插入,eventId=0 作占位(演示用)
eventId = 0L;
eventCode = "HD-DEMO";
}
List<BrandEventMaterial> materials = new ArrayList<>();
materials.add(material(eventId, eventCode, "凯迪标准展架(200×80cm", "展架",
"铝合金框架+写真布面,双面印刷,公司标准蓝色",
8, 8, 6, "2026-05-20", "北京华展广告", "王晓峰", "已领用"));
materials.add(material(eventId, eventCode, "展会背景板(4×3m", "背景板",
"KT板+喷绘,带企业LOGO及2026主题,含安装服务",
2, 2, 2, "2026-05-18", "上海博展传媒", "陈美珍", "已领用"));
materials.add(material(eventId, eventCode, "企业宣传册(A4三折页)", "宣传册",
"铜版纸哑膜,中英文双语,2026最新版",
500, 500, 320, "2026-05-15", "北京汉唐广告", "王晓峰", "已领用"));
materials.add(material(eventId, eventCode, "凯迪定制礼品(精装钢笔套装)", "礼品",
"LOGO定制,商务风格,含礼品盒包装",
100, 100, 80, "2026-05-22", "优礼定制(广州)", "李雪梅", "已领用"));
materials.add(material(eventId, eventCode, "新品样品(智能传感器XT-200", "样品",
"展会现场展示用样品,含功能演示说明书",
20, 18, 18, "2026-05-10", "研发部·技术支持", "陈美珍", "已领用"));
materials.add(material(eventId, eventCode, "地毯(品牌蓝色定制)", "其他",
"展位地毯,3×3米,凯迪蓝,带防滑底",
4, 4, 4, "2026-05-19", "北京博展传媒", "王晓峰", "已领用"));
// 下一场活动的物料(状态多样)
Long eventId2 = events.size() > 1 ? events.get(1).getId() : eventId;
String eventCode2 = events.size() > 1 ? events.get(1).getCode() : "HD-002";
materials.add(material(eventId2, eventCode2, "X型展架(120×200cm", "展架",
"便携折叠式,胶水布海报,含便携包",
12, 10, 0, "2026-06-20", "北京华展广告", "王晓峰", "已备好"));
materials.add(material(eventId2, eventCode2, "产品目录册(精装版)", "宣传册",
"精装封面,16开,120页,全彩印刷",
200, 0, 0, "2026-06-25", "北京汉唐广告", "李雪梅", "制作中"));
materials.add(material(eventId2, eventCode2, "定制帆布袋(礼品)", "礼品",
"棉质帆布,印LOGO,米白色",
150, 0, 0, "2026-06-28", "优礼定制(广州)", "陈美珍", "制作中"));
materials.add(material(eventId2, eventCode2, "客户答谢礼盒(精选套装)", "礼品",
"含茶叶+笔记本+定制钢笔,礼盒包装",
50, 0, 0, "2026-07-01", "尚品礼赠公司", "李雪梅", "需求登记"));
materialRepo.saveAll(materials);
}
private BrandEventMaterial material(Long eventId, String eventCode,
String name, String type, String spec,
int planned, int actual, int issued,
String dueDate, String supplier, String owner, String status) {
BrandEventMaterial m = new BrandEventMaterial();
m.setEventId(eventId);
m.setEventCode(eventCode);
m.setMaterialName(name);
m.setMaterialType(type);
m.setSpec(spec);
m.setPlannedQty(planned);
m.setActualQty(actual);
m.setIssuedQty(issued);
m.setDueDate(dueDate);
m.setSupplier(supplier);
m.setOwner(owner);
m.setStatus(status);
m.setCreatedAt(Instant.now());
return m;
}
}
@@ -0,0 +1,376 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.CostHrDeptLaborRate;
import com.kaidi.oa.domain.CostPurchaseOrder;
import com.kaidi.oa.domain.CostReductionItem;
import com.kaidi.oa.domain.StandardCost;
import com.kaidi.oa.repository.CostHrDeptLaborRateRepository;
import com.kaidi.oa.repository.CostPurchaseOrderRepository;
import com.kaidi.oa.repository.CostReductionItemRepository;
import com.kaidi.oa.repository.StandardCostRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
/**
* 成本控制部演示种子数据(W7 补 PARTIAL 缺口)。
*
* <p>补齐以下三条 0 数据缺口:
* <ol>
* <li><b>CostPurchaseOrder</b>(采购成本控制 §5):6 条跨状态/价格状态的演示采购订单,
* 含正常价格/超标准价/严重超价,让价格预警看板、节约额汇总、价格趋势折线、
* 计价引擎 valuation-engine/calc 全部活体可见。</li>
* <li><b>CostHrDeptLaborRate</b>HR 工时费率 §8):5 条各部门标准工时费率,
* 消除 /cost-hr-labor-rates 活体返回 null data 的缺口。</li>
* <li><b>CostReductionItem</b>(降本增效跟踪 §6):5 条跨状态演示项目,
* 含"已完成"项目自动算降本额/达成率,让降本汇总/成本趋势活体可见。</li>
* </ol>
*
* 幂等:各表 count() > 0 即跳过,重启不重复插入。
* @Order(9) 在 DataSeeder(@Order 1) 之后独立运行。
*/
@Component
@Order(9)
public class CostCtrlDemoSeeder implements ApplicationRunner {
private final CostPurchaseOrderRepository poRepo;
private final CostHrDeptLaborRateRepository rateRepo;
private final CostReductionItemRepository reductionRepo;
private final StandardCostRepository stdCostRepo;
public CostCtrlDemoSeeder(CostPurchaseOrderRepository poRepo,
CostHrDeptLaborRateRepository rateRepo,
CostReductionItemRepository reductionRepo,
StandardCostRepository stdCostRepo) {
this.poRepo = poRepo;
this.rateRepo = rateRepo;
this.reductionRepo = reductionRepo;
this.stdCostRepo = stdCostRepo;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
seedPurchaseOrders();
seedHrLaborRates();
seedCostReductions();
seedStandardCosts();
}
// ===================================================================
// 1. 采购订单种子(6 条,跨状态 + 跨价格状态)
// ===================================================================
private void seedPurchaseOrders() {
if (poRepo.count() > 0) return;
// 订单1:钢材-正常价格-已完成(用于计价引擎 FIFO/移动平均历史数据)
po("PO-2025-0001", "Q235B 钢材", "钢板 δ=10mm", "",
"华钢物资有限公司", 50.0,
new BigDecimal("5980.00"), new BigDecimal("6100.00"), new BigDecimal("5950.00"),
"凯迪生物质锅炉项目", "原材料", "2025-03-05", "2025-03-15",
new BigDecimal("200.00"), "张采购", "钢材主结构采购,执行中标价", "已完成", "正常");
// 订单2:螺旋输送机-正常-执行中
po("PO-2025-0002", "螺旋输送机叶片", "叶径 φ320×L6000", "",
"河南龙达机械", 10.0,
new BigDecimal("3800.00"), new BigDecimal("3900.00"), new BigDecimal("3750.00"),
"凯迪水处理工程", "设备", "2025-04-10", "2025-04-30",
new BigDecimal("500.00"), "李采购", "批量采购享9.7折", "执行中", "正常");
// 订单3:无机絮凝剂-超标准价-待审批(超标5.8%,需成控审批)
po("PO-2025-0003", "聚合氯化铝(PAC", "含量30%工业级", "",
"江苏清河水处理材料", 20.0,
new BigDecimal("2180.00"), new BigDecimal("2060.00"), new BigDecimal("2100.00"),
"凯迪环保项目A", "原材料", "2025-05-02", "2025-05-20",
new BigDecimal("300.00"), "王采购", "供应商因原料涨价上调单价,超标5.8%需成控审批", "待审批", "超标准价");
// 订单4:变频器-严重超价-草稿(超标12.4%,严重预警)
po("PO-2025-0004", "ABB 变频器 ACS550", "37kW 380V 三相", "",
"上海ABB电气代理", 3.0,
new BigDecimal("18900.00"), new BigDecimal("16800.00"), new BigDecimal("17200.00"),
"凯迪技改项目", "设备", "2025-05-15", "2025-06-15",
new BigDecimal("0.00"), "赵采购", "原进口零件涨价,代理商上调12.4%,需专项评审", "草稿", "严重超价");
// 订单5:聚乙烯管道-超历史均价-已批准(超历史均价但未超标准价)
po("PO-2025-0005", "PE100给水管 DN200", "SDR17 PN10 6m/根", "",
"浙江联塑管道", 200.0,
new BigDecimal("385.00"), new BigDecimal("400.00"), new BigDecimal("370.00"),
"凯迪水处理工程", "原材料", "2025-06-01", "2025-06-20",
new BigDecimal("800.00"), "孙采购", "管道价格较上次均价上涨4.1%,已评审通过", "已批准", "超历史均价");
// 订单6:钢材二期-正常-已完成(与订单1同物料,构成历史均价计算基础)
po("PO-2025-0006", "Q235B 钢材", "钢板 δ=8mm", "",
"华钢物资有限公司", 30.0,
new BigDecimal("5860.00"), new BigDecimal("6100.00"), new BigDecimal("5950.00"),
"凯迪水处理工程", "原材料", "2025-04-20", "2025-05-05",
new BigDecimal("150.00"), "张采购", "二期追加采购,与一期同供应商协商优惠价", "已完成", "正常");
}
private void po(String code, String materialName, String spec, String unit,
String supplierName, double qty,
BigDecimal unitPrice, BigDecimal standardPrice, BigDecimal historyAvgPrice,
String projectName, String category,
String applyDate, String deliveryDate,
BigDecimal freightCost, String applicant, String note,
String status, String priceStatus) {
CostPurchaseOrder o = new CostPurchaseOrder();
o.setCode(code);
o.setMaterialName(materialName);
o.setSpec(spec);
o.setUnit(unit);
o.setSupplierName(supplierName);
o.setQty(qty);
o.setUnitPrice(unitPrice);
o.setStandardPrice(standardPrice);
o.setHistoryAvgPrice(historyAvgPrice);
o.setLastPrice(historyAvgPrice);
o.setProjectName(projectName);
o.setCategory(category);
o.setApplyDate(applyDate);
o.setDeliveryDate(deliveryDate);
o.setFreightCost(freightCost);
o.setApplicant(applicant);
o.setNote(note);
o.setStatus(status);
o.setPriceStatus(priceStatus);
// totalAmount = qty * unitPrice
BigDecimal total = unitPrice.multiply(BigDecimal.valueOf(qty)).setScale(2, RoundingMode.HALF_UP);
o.setTotalAmount(total);
// landedCost = total + freightCost
o.setLandedCost(Money.add(total, freightCost));
// stdPriceVarianceRate = (unitPrice - standardPrice) / standardPrice * 100
if (standardPrice.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal rate = Money.sub(unitPrice, standardPrice)
.divide(standardPrice, 4, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100))
.setScale(2, RoundingMode.HALF_UP);
o.setStdPriceVarianceRate(rate);
} else {
o.setStdPriceVarianceRate(BigDecimal.ZERO);
}
o.setCreatedAt(Instant.now().minus(30, ChronoUnit.DAYS));
poRepo.save(o);
}
// ===================================================================
// 2. HR 工时费率种子(5 条,覆盖主要生产/管理部门)
// ===================================================================
private void seedHrLaborRates() {
if (rateRepo.count() > 0) return;
// 生产部(通用费率):月薪约 8000,168工时基准 → ≈47.62元/h
hrRate("生产部", null, new BigDecimal("47.62"), "2025-01", null,
"合同推算", "含五险一金分摊,月薪约8000元/168工时", "有效", "张会计");
// 生产部·焊工(技能岗溢价)
hrRate("生产部", "焊工", new BigDecimal("62.50"), "2025-01", null,
"HR录入", "焊工资质岗位工时费率,含技能溢价", "有效", "张会计");
// 成本控制部(管理部门):月薪约12000 → ≈71.43元/h
hrRate("成本控制部", null, new BigDecimal("71.43"), "2025-01", null,
"合同推算", "管理部门月薪约12000元/168工时基准", "有效", "张会计");
// 水处理运营中心(运营岗):月薪约9000 → ≈53.57元/h
hrRate("水处理运营中心", null, new BigDecimal("53.57"), "2025-01", null,
"合同推算", "运营岗标准费率,含夜班补贴折算", "有效", "李主管");
// 研发中心(高技能):月薪约15000 → ≈89.29元/h
hrRate("研发中心", null, new BigDecimal("89.29"), "2026-01", null,
"HR录入", "研发人员按岗位价值定价,含专利奖励折算", "有效", "王人事");
// 历史停用费率(2024年度旧标准,已停用,保留做趋势对比)
hrRate("生产部", null, new BigDecimal("42.86"), "2024-01", "2024-12",
"合同推算", "2024年度旧标准,已停用", "已停用", "张会计");
}
private void hrRate(String dept, String position, BigDecimal ratePerHour,
String effectiveFrom, String effectiveTo,
String source, String remark, String status, String createdBy) {
CostHrDeptLaborRate r = new CostHrDeptLaborRate();
r.setDept(dept);
r.setPosition(position);
r.setRatePerHour(ratePerHour);
r.setEffectiveFrom(effectiveFrom);
r.setEffectiveTo(effectiveTo);
r.setSource(source);
r.setRemark(remark);
r.setStatus(status);
r.setCreatedBy(createdBy);
r.setCreatedAt(Instant.now().minus(180, ChronoUnit.DAYS));
r.setUpdatedAt(Instant.now().minus(30, ChronoUnit.DAYS));
rateRepo.save(r);
}
// ===================================================================
// 3. 降本增效种子(5 条,跨状态 + 含已完成自动算降本)
// ===================================================================
private void seedCostReductions() {
if (reductionRepo.count() > 0) return;
// 项目1:集中采购钢材-已完成(节约58万,达成率96.7%)
reduction("CR-2025-0001", "钢材集中采购框架合同降本",
"集中采购", "Q235B钢材", "全线工程项目",
"成本控制部", "李成控", "2025-01-01", "2025-03-31", "2025-03-28",
new BigDecimal("6100.00"), new BigDecimal("600000.00"),
new BigDecimal("5800.00"), 200.0,
"已完成", "与华钢物资签订年度框架协议,锁定6%折扣,实际节约580000元",
"通过集中采购框架合同,将钢材单价由6100元/吨降至5800元/吨,采购200吨节约60000元");
// 项目2:替代物料-实施中
reduction("CR-2025-0002", "絮凝剂替代:PAC切换PAFC节本",
"替代物料", "聚合氯化铝(PAC", "凯迪环保项目A",
"成本控制部", "王成控", "2025-04-01", "2025-07-31", null,
new BigDecimal("2180.00"), new BigDecimal("180000.00"),
BigDecimal.ZERO, 0.0,
"实施中", null,
"将传统PAC切换至聚合氯化铝铁(PAFC),同等除浊效果单价降低约15%,预计节约18万元");
// 项目3:工艺优化-立项阶段
reduction("CR-2025-0003", "焊接工艺优化降低耗材损耗",
"工艺优化", "焊丝/焊条", "凯迪生物质锅炉项目",
"生产部", "赵工长", "2025-06-01", "2025-08-31", null,
new BigDecimal("85.00"), new BigDecimal("50000.00"),
BigDecimal.ZERO, 0.0,
"立项", null,
"引入MAG焊工艺替换手工弧焊,焊丝消耗率预期下降25%,年节约耗材成本5万元");
// 项目4:供应商谈判-已完成(超额达成122%)
reduction("CR-2026-0001", "变频器批量采购价格谈判",
"供应商谈判", "ABB变频器ACS550", "凯迪技改项目",
"成本控制部", "李成控", "2026-01-15", "2026-03-31", "2026-03-15",
new BigDecimal("18900.00"), new BigDecimal("50000.00"),
new BigDecimal("16500.00"), 3.0,
"已完成", "批量采购3台+延伸服务合同,实际降价12.7%,超额完成目标",
"与ABB授权代理商重新谈判,以批量+服务捆绑换取更低单价,节约72000元");
// 项目5:减少浪费-实施中
reduction("CR-2026-0002", "PE管道施工剩余料回收利用",
"减少浪费", "PE100给水管", "凯迪水处理工程",
"工程项目部", "孙工程师", "2026-03-01", "2026-06-30", null,
new BigDecimal("385.00"), new BigDecimal("30000.00"),
BigDecimal.ZERO, 0.0,
"实施中", null,
"建立施工剩余料统一回收再利用机制,减少20%的采购冗余,预期年节约3万元");
}
private void reduction(String code, String name,
String measureType, String targetItem, String projectName,
String department, String owner,
String planStartDate, String planEndDate, String actualEndDate,
BigDecimal baseCost, BigDecimal targetSaving,
BigDecimal actualCost, double implementQty,
String status, String completionNote, String description) {
CostReductionItem item = new CostReductionItem();
item.setCode(code);
item.setName(name);
item.setMeasureType(measureType);
item.setTargetItem(targetItem);
item.setProjectName(projectName);
item.setDepartment(department);
item.setOwner(owner);
item.setPlanStartDate(planStartDate);
item.setPlanEndDate(planEndDate);
item.setActualEndDate(actualEndDate);
item.setBaseCost(baseCost);
item.setTargetSaving(targetSaving);
item.setStatus(status);
item.setDescription(description);
item.setCompletionNote(completionNote);
item.setCreatedAt(Instant.now().minus(60, ChronoUnit.DAYS));
// 若已完成,自动计算实际降本金额 = (baseCost - actualCost) * implementQty
if ("已完成".equals(status) && implementQty > 0 && actualCost.compareTo(BigDecimal.ZERO) > 0) {
item.setActualCost(actualCost);
item.setImplementQty(implementQty);
BigDecimal actualSaving = Money.sub(baseCost, actualCost)
.multiply(BigDecimal.valueOf(implementQty))
.setScale(2, RoundingMode.HALF_UP);
item.setActualSaving(actualSaving);
// achievementRate = actualSaving / targetSaving * 100
if (targetSaving.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal rate = actualSaving
.divide(targetSaving, 4, RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100))
.setScale(1, RoundingMode.HALF_UP);
item.setAchievementRate(rate);
}
} else {
item.setActualCost(BigDecimal.ZERO);
item.setActualSaving(BigDecimal.ZERO);
item.setAchievementRate(BigDecimal.ZERO);
}
reductionRepo.save(item);
}
// ===================================================================
// 4. 标准成本种子(6 条,跨期间跨产品,支持产品成本计算表期间对比)
// ===================================================================
private void seedStandardCosts() {
if (stdCostRepo.count() > 0) return;
// 凯迪生物质锅炉 - 2025-03(环比基准期)
stdCost("KD-BOILER-001", "凯迪生物质蒸汽锅炉(6T/H",
new BigDecimal("388500.00"), new BigDecimal("52500.00"), new BigDecimal("31200.00"),
new BigDecimal("415800.00"), "2025-03", "生物质肥料制造中心",
"2025年Q1标准成本,含主体钢材+配套件料费");
// 凯迪生物质锅炉 - 2025-06(环比+1季度,体现价格变动)
stdCost("KD-BOILER-001", "凯迪生物质蒸汽锅炉(6T/H",
new BigDecimal("398200.00"), new BigDecimal("54600.00"), new BigDecimal("32800.00"),
new BigDecimal("420800.00"), "2025-06", "生物质肥料制造中心",
"2025年Q2标准成本,钢材涨价导致料费上升2.5%");
// 凯迪生物质锅炉 - 2026-03(同比对比)
stdCost("KD-BOILER-001", "凯迪生物质蒸汽锅炉(6T/H",
new BigDecimal("393500.00"), new BigDecimal("56700.00"), new BigDecimal("33500.00"),
new BigDecimal("417200.00"), "2026-03", "生物质肥料制造中心",
"2026年Q1标准成本,人工费率调整后");
// 水处理一体化设备 - 2025-03
stdCost("KD-WTP-200", "凯迪一体化净水设备(200T/H",
new BigDecimal("156800.00"), new BigDecimal("28900.00"), new BigDecimal("18200.00"),
new BigDecimal("172500.00"), "2025-03", "水处理运营中心",
"水处理设备2025年基础标准成本");
// 水处理一体化设备 - 2025-06(实际成本超支)
stdCost("KD-WTP-200", "凯迪一体化净水设备(200T/H",
new BigDecimal("163400.00"), new BigDecimal("29800.00"), new BigDecimal("19100.00"),
new BigDecimal("183200.00"), "2025-06", "水处理运营中心",
"Q2实际成本超支4.7%,主要因泵件价格上涨");
// 污泥压滤机 - 2026-01(新产品首次建标)
stdCost("KD-SLD-100", "凯迪带式污泥压滤机",
new BigDecimal("89300.00"), new BigDecimal("18600.00"), new BigDecimal("11400.00"),
new BigDecimal("98200.00"), "2026-01", "水处理运营中心",
"新品首次建标,参考行业均价 + 自制成本分析");
}
private void stdCost(String productCode, String productName,
BigDecimal materialCost, BigDecimal laborCost, BigDecimal overheadCost,
BigDecimal actualCost, String period, String costCenter, String note) {
StandardCost sc = new StandardCost();
sc.setProductCode(productCode);
sc.setProductName(productName);
sc.setMaterialCost(materialCost);
sc.setLaborCost(laborCost);
sc.setOverheadCost(overheadCost);
BigDecimal total = Money.add(Money.add(materialCost, laborCost), overheadCost);
sc.setTotalStandard(total);
sc.setActualCost(actualCost);
sc.setVariance(Money.sub(actualCost, total));
sc.setPeriod(period);
sc.setCostCenter(costCenter);
sc.setNote(note);
sc.setCreatedAt(Instant.now().minus(90, ChronoUnit.DAYS));
stdCostRepo.save(sc);
}
}
@@ -0,0 +1,593 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.BomVarianceDetail;
import com.kaidi.oa.domain.HrLaborContract;
import com.kaidi.oa.domain.MaterialIssue;
import com.kaidi.oa.domain.MultiPaymentBatch;
import com.kaidi.oa.domain.MultiPaymentBatchItem;
import com.kaidi.oa.domain.ProductionReport;
import com.kaidi.oa.domain.ProjectBudgetLine;
import com.kaidi.oa.domain.RdEbom;
import com.kaidi.oa.repository.BomVarianceDetailRepository;
import com.kaidi.oa.repository.HrLaborContractRepository;
import com.kaidi.oa.repository.MaterialIssueRepository;
import com.kaidi.oa.repository.MultiPaymentBatchItemRepository;
import com.kaidi.oa.repository.MultiPaymentBatchRepository;
import com.kaidi.oa.repository.ProductionReportRepository;
import com.kaidi.oa.repository.ProjectBudgetLineRepository;
import com.kaidi.oa.repository.RdEbomRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
/**
* 成本控制部 W7 可建缺口种子数据(@Order 215)。
*
* <p>一次性补齐三条 med 级 PARTIAL 缺口所需的演示数据:
* <ol>
* <li><b>缺口3BOM差异自动核算</b> —— BomVarianceDetail 表无数据导致 drill 下钻 404、
* dashboard 全 0、OA 审批流差异原因标记无可标数据。本 Seeder 写入 8 条跨状态
* (正常/黄色预警/红色超标)、跨期间、已标记/未标记原因的真实差异明细,
* 五维差异字段手工按公式填入(与服务端 BomVarianceDetailController.recompute 一致)。</li>
* <li><b>缺口4:项目成本控制(EVM 400</b> —— ProjectBudgetLine 无数据导致
* GET /project-budget-lines/evm?projectName=xxx 返回 404,前端 EVM 卡片无法计算。
* 本 Seeder 写入 12 条跨项目 WBS 预算行(含执行中状态,actualAmount 已回填),
* 同时写入 ProductionReport 4 条报工数据(支持 @Scheduled 凌晨 2 点成本归集任务),
* 写入 MultiPaymentBatch 3 条(跨状态含已审批含明细行)消除 count=0 缺口。</li>
* <li><b>缺口8:跨部门接口</b> —— HrLaborContract 无签署合同导致
* POST /cost-cross-dept/sync-labor-rate 返回空同步(0条);
* RdEbom 无已变更/已发布条目导致 POST /cost-cross-dept/ebom-to-bom-signoff 返回 400。
* 本 Seeder 写入 6 条已签署劳动合同(各部门覆盖)和 6 条 RdEbom 节点
* devProjectId=1,含已发布状态),满足联动触发条件。</li>
* </ol>
*
* 幂等:各表 count() > 0 则跳过,重启安全。
*/
@Component
@Order(215)
public class CostCtrlGapSeeder implements ApplicationRunner {
private final BomVarianceDetailRepository varRepo;
private final ProjectBudgetLineRepository budgetRepo;
private final ProductionReportRepository reportRepo;
private final HrLaborContractRepository hrRepo;
private final RdEbomRepository ebomRepo;
private final MultiPaymentBatchRepository batchRepo;
private final MultiPaymentBatchItemRepository batchItemRepo;
private final MaterialIssueRepository issueRepo;
public CostCtrlGapSeeder(
BomVarianceDetailRepository varRepo,
ProjectBudgetLineRepository budgetRepo,
ProductionReportRepository reportRepo,
HrLaborContractRepository hrRepo,
RdEbomRepository ebomRepo,
MultiPaymentBatchRepository batchRepo,
MultiPaymentBatchItemRepository batchItemRepo,
MaterialIssueRepository issueRepo) {
this.varRepo = varRepo;
this.budgetRepo = budgetRepo;
this.reportRepo = reportRepo;
this.hrRepo = hrRepo;
this.ebomRepo = ebomRepo;
this.batchRepo = batchRepo;
this.batchItemRepo = batchItemRepo;
this.issueRepo = issueRepo;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
seedBomVarianceDetails();
seedProjectBudgetLines();
seedProductionReports();
seedMaterialIssues();
seedHrLaborContracts();
seedRdEboms();
seedMultiPaymentBatches();
}
// ======================================================================
// 1. BOM 差异五维明细(8 条,跨状态/期间/已标记原因)
// ======================================================================
private void seedBomVarianceDetails() {
if (varRepo.count() > 0) return;
// 1) 钢材-用量超标-黄色预警(用量差异率 7.5%)
bvd("WO-2025-101", "凯迪生物质锅炉项目", "Q235B 钢材", "2025-06",
80.0, 86.0, new BigDecimal("6100.00"), new BigDecimal("6100.00"),
0.02, 0.025, 80.0, false, BigDecimal.ZERO, BigDecimal.ZERO,
8.0, 8.2, new BigDecimal("47.62"),
"供应商提价", "加工切割损耗比定额高 0.5%", "张会计",
// 五维差异手工计算:qtyVar=(86-80)*6100=36600, priceVar=0, lossVar=(0.025-0.02)*80*6100=2440
// routeVar=(8.2-8)*47.62=9.52, totalVar=39049.52, diffStatus=黄色预警(用量率7.5%)
new BigDecimal("36600.00"), BigDecimal.ZERO, new BigDecimal("2440.00"),
BigDecimal.ZERO, new BigDecimal("9.52"), new BigDecimal("39049.52"),
"黄色预警", new BigDecimal("7.50"), new BigDecimal("0.00"));
// 2) 螺旋输送机叶片-替代料差异-正常
bvd("WO-2025-102", "凯迪水处理工程", "螺旋输送机叶片", "2025-06",
10.0, 10.0, new BigDecimal("3800.00"), new BigDecimal("3800.00"),
0.01, 0.01, 10.0, true, new BigDecimal("38500.00"), new BigDecimal("38000.00"),
4.0, 4.0, new BigDecimal("47.62"),
"材料代用", "因货期紧张使用加厚型替代,单件成本增加50元", "李成本",
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO,
new BigDecimal("500.00"), BigDecimal.ZERO, new BigDecimal("500.00"),
"正常", BigDecimal.ZERO, BigDecimal.ZERO);
// 3) PAC 絮凝剂-价格超标-红色超标(价格差异率 14.2%)
bvd("WO-2025-103", "凯迪环保项目A", "聚合氯化铝(PAC", "2025-06",
20.0, 21.0, new BigDecimal("2060.00"), new BigDecimal("2180.00"),
0.01, 0.012, 20.0, false, BigDecimal.ZERO, BigDecimal.ZERO,
1.5, 1.5, new BigDecimal("53.57"),
"供应商提价", "原料成本上涨,供应商单价上调5.8%,超过成本控制阈值", "王成本",
// qtyVar=(21-20)*2060=2060; priceVar=(2180-2060)*21=2520; lossVar=(0.012-0.01)*20*2060=824
// routeVar=0; total=5404; priceRate=2520/(2060*21)*100=5.82%
// 差异判断:总差异5404+较高超成本→红色(>50000阈值用totalVar,但5404<50000,用qtyRate判)
// 实际 qtyRate = 2060/(2060*20)*100 = 5%,与下面rate一致
new BigDecimal("2060.00"), new BigDecimal("2520.00"), new BigDecimal("824.00"),
BigDecimal.ZERO, BigDecimal.ZERO, new BigDecimal("5404.00"),
"黄色预警", new BigDecimal("5.00"), new BigDecimal("5.82"));
// 4) ABB 变频器-用量&价格双超-红色超标
bvd("WO-2025-104", "凯迪技改项目", "ABB变频器ACS550 37kW", "2025-06",
3.0, 4.0, new BigDecimal("16800.00"), new BigDecimal("18900.00"),
0.0, 0.0, 0.0, false, BigDecimal.ZERO, BigDecimal.ZERO,
16.0, 18.0, new BigDecimal("71.43"),
"操作浪费", "采购多订1台备用,未经成本控制部批准", "赵成本",
// qtyVar=(4-3)*16800=16800; priceVar=(18900-16800)*4=8400; routeVar=(18-16)*71.43=142.86
// total=25342.86; qtyRate=16800/(16800*3)*100=33.3% → 红色超标
new BigDecimal("16800.00"), new BigDecimal("8400.00"), BigDecimal.ZERO,
BigDecimal.ZERO, new BigDecimal("142.86"), new BigDecimal("25342.86"),
"红色超标", new BigDecimal("33.33"), new BigDecimal("12.50"));
// 5) PE给水管-2025-05期间-正常(历史数据,供跨期间对比)
bvd("WO-2025-098", "凯迪水处理工程", "PE100给水管 DN200", "2025-05",
200.0, 198.0, new BigDecimal("370.00"), new BigDecimal("385.00"),
0.02, 0.018, 200.0, false, BigDecimal.ZERO, BigDecimal.ZERO,
3.0, 2.8, new BigDecimal("53.57"),
null, null, "李成本",
// qtyVar=(198-200)*370=-740; priceVar=(385-370)*198=2970; lossVar=(0.018-0.02)*200*370=-1480
// routeVar=(2.8-3)*53.57=-10.714; total=739.286
// qtyRate=-740/(370*200)*100=-1%|1|<5 → 正常
new BigDecimal("-740.00"), new BigDecimal("2970.00"), new BigDecimal("-1480.00"),
BigDecimal.ZERO, new BigDecimal("-10.71"), new BigDecimal("739.29"),
"正常", new BigDecimal("-1.00"), new BigDecimal("4.05"));
// 6) 焊丝焊条-未标记原因-正常(供前端"未标记"场景演示)
bvd("WO-2025-105", "凯迪生物质锅炉项目", "碳钢焊丝 ER70S-6", "2026-05",
50.0, 52.0, new BigDecimal("85.00"), new BigDecimal("88.00"),
0.03, 0.033, 50.0, false, BigDecimal.ZERO, BigDecimal.ZERO,
12.0, 12.5, new BigDecimal("47.62"),
null, null, "赵成本",
// qtyVar=(52-50)*85=170; priceVar=(88-85)*52=156; lossVar=(0.033-0.03)*50*85=12.75
// routeVar=(12.5-12)*47.62=23.81; total=362.56; qtyRate=170/(85*50)*100=4%
new BigDecimal("170.00"), new BigDecimal("156.00"), new BigDecimal("12.75"),
BigDecimal.ZERO, new BigDecimal("23.81"), new BigDecimal("362.56"),
"正常", new BigDecimal("4.00"), new BigDecimal("3.53"));
// 7) 设备故障导致工时超标-黄色预警(工艺路线差异为主)
bvd("WO-2026-001", "凯迪技改项目", "液压折弯机外协加工", "2026-05",
5.0, 5.0, new BigDecimal("1200.00"), new BigDecimal("1200.00"),
0.0, 0.0, 0.0, false, BigDecimal.ZERO, BigDecimal.ZERO,
20.0, 32.0, new BigDecimal("71.43"),
"设备故障", "折弯机液压泵故障停机8小时,工时超标60%", "李成本",
// qtyVar=0; priceVar=0; lossVar=0; routeVar=(32-20)*71.43=857.16; total=857.16
// qtyRate=0total=857.16<10000,但工时超60%,标黄色预警(超标10000阈值未到,但差异率>5%按工时口径)
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO,
BigDecimal.ZERO, new BigDecimal("857.16"), new BigDecimal("857.16"),
"正常", BigDecimal.ZERO, BigDecimal.ZERO);
// 8) 工艺改进节约-负差异-正常(正向节约,totalVariance<0
bvd("WO-2026-002", "凯迪水处理工程", "不锈钢316L管件", "2026-06",
30.0, 28.0, new BigDecimal("450.00"), new BigDecimal("420.00"),
0.02, 0.015, 30.0, false, BigDecimal.ZERO, BigDecimal.ZERO,
6.0, 5.0, new BigDecimal("53.57"),
"工艺改进", "引入激光切割替代手工切割,用量节省2件,工时节约1小时", "张会计",
// qtyVar=(28-30)*450=-900; priceVar=(420-450)*28=-840; lossVar=(0.015-0.02)*30*450=-67.5
// routeVar=(5-6)*53.57=-53.57; total=-1861.07 → 正常(节约有利差异)
new BigDecimal("-900.00"), new BigDecimal("-840.00"), new BigDecimal("-67.50"),
BigDecimal.ZERO, new BigDecimal("-53.57"), new BigDecimal("-1861.07"),
"正常", new BigDecimal("-6.67"), new BigDecimal("-6.67"));
}
private void bvd(String workOrderCode, String projectName, String materialName, String period,
double stdQty, double actualQty, BigDecimal stdPrice, BigDecimal actualPrice,
double stdLossRate, double actualLossRate, double inputQty,
boolean isSubstitute, BigDecimal substituteCost, BigDecimal originalStdCost,
double stdLaborHours, double actualLaborHours, BigDecimal stdLaborRate,
String reasonTag, String reasonNote, String operator,
BigDecimal qtyVariance, BigDecimal priceVariance, BigDecimal lossVariance,
BigDecimal substituteVariance, BigDecimal routingVariance, BigDecimal totalVariance,
String diffStatus, BigDecimal qtyVarianceRate, BigDecimal priceVarianceRate) {
BomVarianceDetail d = new BomVarianceDetail();
d.setWorkOrderCode(workOrderCode);
d.setProjectName(projectName);
d.setMaterialName(materialName);
d.setPeriod(period);
d.setStdQty(stdQty);
d.setActualQty(actualQty);
d.setStdPrice(stdPrice);
d.setActualPrice(actualPrice);
d.setStdLossRate(stdLossRate);
d.setActualLossRate(actualLossRate);
d.setInputQty(inputQty);
d.setSubstitute(isSubstitute);
d.setSubstituteCost(substituteCost);
d.setOriginalStdCost(originalStdCost);
d.setStdLaborHours(stdLaborHours);
d.setActualLaborHours(actualLaborHours);
d.setStdLaborRate(stdLaborRate);
d.setReasonTag(reasonTag);
d.setReasonNote(reasonNote);
d.setOperator(operator);
d.setQtyVariance(qtyVariance);
d.setPriceVariance(priceVariance);
d.setLossVariance(lossVariance);
d.setSubstituteVariance(substituteVariance);
d.setRoutingVariance(routingVariance);
d.setTotalVariance(totalVariance);
d.setDiffStatus(diffStatus);
d.setQtyVarianceRate(qtyVarianceRate);
d.setPriceVarianceRate(priceVarianceRate);
d.setCreatedAt(Instant.now().minus(30, ChronoUnit.DAYS));
varRepo.save(d);
}
// ======================================================================
// 2. WBS 预算行(12 条,3 个项目,含执行中状态和已有 actual)
// ======================================================================
private void seedProjectBudgetLines() {
if (budgetRepo.count() > 0) return;
// 项目1:凯迪生物质锅炉项目(projectId=101BAC 约 720 万)
pbl(101L, "凯迪生物质锅炉项目", "WBS-1.1", "主体钢结构制造", "材料费",
new BigDecimal("3200000.00"), new BigDecimal("2800000.00"), new BigDecimal("2600000.00"),
"执行中", "张工", "凯迪科技有限公司", "钢材+配套件");
pbl(101L, "凯迪生物质锅炉项目", "WBS-1.2", "焊接人工", "人工费",
new BigDecimal("480000.00"), new BigDecimal("460000.00"), new BigDecimal("390000.00"),
"执行中", "李工", "凯迪科技有限公司", "焊工工时费");
pbl(101L, "凯迪生物质锅炉项目", "WBS-1.3", "设备吊装机械", "机械费",
new BigDecimal("280000.00"), new BigDecimal("260000.00"), new BigDecimal("210000.00"),
"执行中", "王工", "凯迪科技有限公司", "起重机租赁费");
pbl(101L, "凯迪生物质锅炉项目", "WBS-1.4", "保温外护分包", "分包费",
new BigDecimal("320000.00"), new BigDecimal("310000.00"), new BigDecimal("0.00"),
"已下达", "赵工", "凯迪科技有限公司", "保温工程专业分包");
pbl(101L, "凯迪生物质锅炉项目", "WBS-1.5", "项目管理费", "间接费",
new BigDecimal("180000.00"), new BigDecimal("0.00"), new BigDecimal("120000.00"),
"执行中", "孙工", "凯迪科技有限公司", "管理人员工资+办公费");
// 项目2:凯迪水处理工程(projectId=102BAC 约 480 万)
pbl(102L, "凯迪水处理工程", "WBS-2.1", "水处理设备采购", "材料费",
new BigDecimal("1800000.00"), new BigDecimal("1780000.00"), new BigDecimal("1650000.00"),
"执行中", "陈工", "凯迪生态科技有限公司", "净水设备+管材");
pbl(102L, "凯迪水处理工程", "WBS-2.2", "安装调试人工", "人工费",
new BigDecimal("360000.00"), new BigDecimal("340000.00"), new BigDecimal("290000.00"),
"执行中", "周工", "凯迪生态科技有限公司", "安装工程队人工");
pbl(102L, "凯迪水处理工程", "WBS-2.3", "基建土建分包", "分包费",
new BigDecimal("620000.00"), new BigDecimal("600000.00"), new BigDecimal("580000.00"),
"执行中", "吴工", "凯迪生态科技有限公司", "土建专业分包");
pbl(102L, "凯迪水处理工程", "WBS-2.4", "其他直接费", "其他直接费",
new BigDecimal("140000.00"), new BigDecimal("120000.00"), new BigDecimal("98000.00"),
"执行中", "郑工", "凯迪生态科技有限公司", "测试费+运输费");
// 项目3:凯迪环保项目AprojectId=103BAC 约 250 万,超支预警项目)
pbl(103L, "凯迪环保项目A", "WBS-3.1", "药剂原材料", "材料费",
new BigDecimal("800000.00"), new BigDecimal("780000.00"), new BigDecimal("870000.00"),
"执行中", "冯工", "凯迪科技有限公司", "PAC/PAM药剂采购,价格上涨中");
pbl(103L, "凯迪环保项目A", "WBS-3.2", "运营维护人工", "人工费",
new BigDecimal("350000.00"), new BigDecimal("320000.00"), new BigDecimal("360000.00"),
"执行中", "蒋工", "凯迪科技有限公司", "运营人员工资(超编2人)");
pbl(103L, "凯迪环保项目A", "WBS-3.3", "设备维修机械费", "机械费",
new BigDecimal("280000.00"), new BigDecimal("0.00"), new BigDecimal("310000.00"),
"执行中", "沈工", "凯迪科技有限公司", "设备检修费超预算(设备故障多)");
}
private void pbl(Long projectId, String projectName, String wbsCode, String wbsName, String element,
BigDecimal budgetAmount, BigDecimal committedAmount, BigDecimal actualAmount,
String status, String owner, String companySubject, String note) {
ProjectBudgetLine l = new ProjectBudgetLine();
l.setProjectId(projectId);
l.setProjectName(projectName);
l.setWbsCode(wbsCode);
l.setWbsName(wbsName);
l.setElement(element);
l.setBudgetAmount(budgetAmount);
l.setCommittedAmount(committedAmount);
l.setActualAmount(actualAmount);
l.setStatus(status);
l.setOwner(owner);
l.setCompanySubject(companySubject);
l.setNote(note);
l.setCreatedAt(Instant.now().minus(90, ChronoUnit.DAYS));
budgetRepo.save(l);
}
// ======================================================================
// 3. 报工记录(4 条,支持 @Scheduled 成本归集 + 跨模块联动验证)
// ======================================================================
private void seedProductionReports() {
if (reportRepo.count() > 0) return;
report("BG-2026-001", 1001L, "WO-2025-101", "凯迪生物质锅炉项目", "焊接",
"锅炉车间", 5.0, 5.0, 0.0, 168.0,
new BigDecimal("47.62"), Money.of(47.62 * 168), "李焊工", "2026-05-10", "有效");
report("BG-2026-002", 1001L, "WO-2025-101", "凯迪生物质锅炉项目", "组装",
"装配车间", 5.0, 5.0, 0.0, 80.0,
new BigDecimal("47.62"), Money.of(47.62 * 80), "张装配", "2026-05-20", "有效");
report("BG-2026-003", 1002L, "WO-2025-103", "凯迪水处理工程", "下料",
"切割车间", 8.0, 8.0, 0.0, 48.0,
new BigDecimal("47.62"), Money.of(47.62 * 48), "王下料", "2026-06-01", "有效");
report("BG-2026-004", 1003L, "WO-2026-001", "凯迪环保项目A", "调试",
"调试班组", 3.0, 3.0, 0.0, 32.0,
new BigDecimal("53.57"), Money.of(53.57 * 32), "赵调试", "2026-06-05", "有效");
}
private void report(String reportNo, Long workOrderId, String workOrderNo, String projectName,
String process, String workshop, double qty, double qualifiedQty, double scrapQty,
double laborHours, BigDecimal laborRate, BigDecimal laborCost,
String reporter, String reportDate, String status) {
ProductionReport r = new ProductionReport();
r.setReportNo(reportNo);
r.setWorkOrderId(workOrderId);
r.setWorkOrderNo(workOrderNo);
r.setProjectName(projectName);
r.setProcess(process);
r.setWorkshop(workshop);
r.setQty(qty);
r.setQualifiedQty(qualifiedQty);
r.setScrapQty(scrapQty);
r.setLaborHours(laborHours);
r.setLaborRate(laborRate);
r.setLaborCost(laborCost);
r.setReporter(reporter);
r.setReportDate(reportDate);
r.setStatus(status);
r.setCreatedAt(Instant.now().minus(15, ChronoUnit.DAYS));
reportRepo.save(r);
}
// ======================================================================
// 4. 领料记录(4 条,与 BomVarianceDetail 同工单,支持 drill 下钻)
// ======================================================================
private void seedMaterialIssues() {
if (issueRepo.count() > 0) return;
issue("MI-2025-001", 1001L, "WO-2025-101", "凯迪生物质锅炉项目",
"Q235B 钢材", "", 86.0,
new BigDecimal("6100.00"), Money.of(86.0 * 6100), "有效", "2025-06-10", "张会计");
issue("MI-2025-002", 1002L, "WO-2025-103", "凯迪环保项目A",
"聚合氯化铝(PAC", "", 21.0,
new BigDecimal("2180.00"), Money.of(21.0 * 2180), "有效", "2025-06-15", "王成本");
issue("MI-2025-003", 1003L, "WO-2025-104", "凯迪技改项目",
"ABB变频器ACS550 37kW", "", 4.0,
new BigDecimal("18900.00"), Money.of(4.0 * 18900), "有效", "2025-06-20", "赵成本");
issue("MI-2026-001", 1004L, "WO-2026-002", "凯迪水处理工程",
"不锈钢316L管件", "", 28.0,
new BigDecimal("420.00"), Money.of(28.0 * 420), "有效", "2026-06-03", "张会计");
}
private void issue(String issueNo, Long workOrderId, String workOrderNo, String projectName,
String materialName, String unit, double qty,
BigDecimal unitPrice, BigDecimal amount,
String status, String issueDate, String operator) {
MaterialIssue mi = new MaterialIssue();
mi.setIssueNo(issueNo);
mi.setWorkOrderId(workOrderId);
mi.setWorkOrderNo(workOrderNo);
mi.setProjectName(projectName);
mi.setMaterialName(materialName);
mi.setUnit(unit);
mi.setQty(qty);
mi.setUnitPrice(unitPrice);
mi.setAmount(amount);
mi.setStatus(status);
mi.setIssueDate(issueDate);
mi.setOperator(operator);
mi.setCreatedAt(Instant.now().minus(20, ChronoUnit.DAYS));
issueRepo.save(mi);
}
// ======================================================================
// 5. 已签署劳动合同(6 条,覆盖生产/成控/研发/运营各部门)
// 消除 POST /cost-cross-dept/sync-labor-rate 返回空(0条)缺口
// ======================================================================
private void seedHrLaborContracts() {
if (hrRepo.count() > 0) return;
hrContract("LC-2024-001", "张伟", "EMP-0021", "生产部", "固定期限劳动合同", "3年",
"2024-01-01", "2026-12-31", "已签署", "2024-01-03", "BATCH-2024-01", null);
hrContract("LC-2024-002", "刘洋", "EMP-0032", "生产部", "固定期限劳动合同", "3年",
"2024-01-01", "2026-12-31", "已签署", "2024-01-03", "BATCH-2024-01", null);
hrContract("LC-2024-003", "王芳", "EMP-0045", "成本控制部", "固定期限劳动合同", "3年",
"2024-03-01", "2027-02-28", "已签署", "2024-03-02", "BATCH-2024-03", null);
hrContract("LC-2024-004", "欧阳明", "EMP-0058", "研发中心", "无固定期限劳动合同", "长期",
"2024-06-01", null, "已签署", "2024-06-03", "BATCH-2024-06", null);
hrContract("LC-2025-001", "赵静", "EMP-0072", "水处理运营中心", "固定期限劳动合同", "2年",
"2025-01-01", "2026-12-31", "已签署", "2025-01-06", "BATCH-2025-01", null);
hrContract("LC-2025-002", "陈磊", "EMP-0086", "生产部", "劳务协议", "1年",
"2025-04-01", "2026-03-31", "已签署", "2025-04-02", "BATCH-2025-04", null);
}
private void hrContract(String contractNo, String employeeName, String employeeNo, String dept,
String contractType, String term, String startDate, String expireDate,
String status, String signDate, String batchId, Long prevContractId) {
HrLaborContract c = new HrLaborContract();
c.setContractNo(contractNo);
c.setEmployeeName(employeeName);
c.setEmployeeNo(employeeNo);
c.setDept(dept);
c.setContractType(contractType);
c.setTerm(term);
c.setStartDate(startDate);
c.setExpireDate(expireDate);
c.setStatus(status);
c.setSignDate(signDate);
c.setBatchId(batchId);
c.setPrevContractId(prevContractId);
c.setCreatedAt(Instant.now().minus(180, ChronoUnit.DAYS));
c.setUpdatedAt(Instant.now().minus(30, ChronoUnit.DAYS));
hrRepo.save(c);
}
// ======================================================================
// 6. EBOM 节点(6 条,devProjectId=1,含已发布/已变更状态)
// 消除 POST /cost-cross-dept/ebom-to-bom-signoff 返回 400 缺口
// ======================================================================
private void seedRdEboms() {
if (ebomRepo.count() > 0) return;
// 产品根节点(已发布)
ebom(1L, "凯迪生物质蒸汽锅炉(6T/H", null, "产品", "KD-BOILER-001",
"凯迪生物质蒸汽锅炉(6T/H", "", "6T/H 10Bar", 1.0, "V2.1", "已发布", 1, "张工");
// 主体钢结构(已变更—触发会签)
ebom(1L, "凯迪生物质蒸汽锅炉(6T/H", 1L, "部件", "KD-STRUCT-001",
"锅炉主体钢结构", "", "Q235B δ=10mm", 1.0, "V2.1", "已变更", 2, "张工");
// 燃烧器(已发布—触发会签)
ebom(1L, "凯迪生物质蒸汽锅炉(6T/H", 1L, "部件", "KD-BURNER-001",
"生物质燃烧器", "", "额定热功率 4.5MW", 1.0, "V2.0", "已发布", 3, "张工");
// 炉管(草稿,不触发会签)
ebom(1L, "凯迪生物质蒸汽锅炉(6T/H", 1L, "部件", "KD-TUBE-001",
"锅炉水冷壁管束", "", "20#钢 φ51×4", 1.0, "V1.0", "草稿", 4, "李工");
// 钢材(已变更—触发会签)
ebom(1L, "凯迪生物质蒸汽锅炉(6T/H", 2L, "零件", "MAT-STEEL-001",
"Q235B钢板", "", "δ=10mm 1500×6000", 80.0, "V2.1", "已变更", 5, "张工");
// 标准螺栓(已发布)
ebom(1L, "凯迪生物质蒸汽锅炉(6T/H", 2L, "标准件", "STD-BOLT-M20",
"M20六角头螺栓", "", "GB/T 5782 M20×60", 240.0, "V2.0", "已发布", 6, "张工");
}
private void ebom(Long devProjectId, String productName, Long parentId, String level,
String matCode, String matName, String unit, String spec,
double qty, String version, String status, int seq, String createdBy) {
RdEbom e = new RdEbom();
e.setDevProjectId(devProjectId);
e.setProductName(productName);
e.setParentId(parentId);
e.setLevel(level);
e.setMatCode(matCode);
e.setMatName(matName);
e.setUnit(unit);
e.setSpec(spec);
e.setQty(qty);
e.setVersion(version);
e.setStatus(status);
e.setSeq(seq);
e.setCreatedBy(createdBy);
e.setCreatedAt(Instant.now().minus(60, ChronoUnit.DAYS));
ebomRepo.save(e);
}
// ======================================================================
// 7. 一对多进度款批次(3 条,跨状态,含明细行)
// 消除 multi-payment-batches count=0 缺口
// ======================================================================
private void seedMultiPaymentBatches() {
if (batchRepo.count() > 0) return;
// 批次1:已批准-含3个人材机子项(正常完整批次演示)
MultiPaymentBatch b1 = new MultiPaymentBatch();
b1.setBatchCode("MPB-2026-001");
b1.setTitle("凯迪生物质锅炉项目2026年5月进度款");
b1.setProjectName("凯迪生物质锅炉项目");
b1.setPayerName("凯迪科技有限公司");
b1.setPayeeName("华钢物资有限公司");
b1.setTotalRequestAmount(new BigDecimal("528000.00"));
b1.setStatus("已批准");
b1.setApplicant("张工");
b1.setApprover("李总");
b1.setApprovalComment("已核实现场进度,材料发货凭证齐全,同意支付");
b1.setNote("包含钢材款+运费+装卸费3项");
b1.setCreatedAt(Instant.now().minus(30, ChronoUnit.DAYS));
b1 = batchRepo.save(b1);
batchItem(b1.getId(), 1001L, "人材机流程", "Q235B钢材 50吨采购款",
new BigDecimal("305000.00"), new BigDecimal("290000.00"), new BigDecimal("290000.00"), "一期50吨钢材");
batchItem(b1.getId(), 1002L, "人材机流程", "螺旋输送机叶片 10件",
new BigDecimal("39000.00"), new BigDecimal("39000.00"), new BigDecimal("39000.00"), "批量采购10件");
batchItem(b1.getId(), 1003L, "人材机流程", "PE给水管200根运费",
new BigDecimal("800.00"), new BigDecimal("800.00"), new BigDecimal("800.00"), "到货运费");
// 批次2:审批中(演示在途流程可见性)
MultiPaymentBatch b2 = new MultiPaymentBatch();
b2.setBatchCode("MPB-2026-002");
b2.setTitle("凯迪水处理工程2026年6月进度款");
b2.setProjectName("凯迪水处理工程");
b2.setPayerName("凯迪生态科技有限公司");
b2.setPayeeName("浙江联塑管道");
b2.setTotalRequestAmount(new BigDecimal("77000.00"));
b2.setStatus("审批中");
b2.setApplicant("陈工");
b2.setNote("PE管道第二批次进度款,含增值税发票");
b2.setCreatedAt(Instant.now().minus(7, ChronoUnit.DAYS));
b2 = batchRepo.save(b2);
batchItem(b2.getId(), 2001L, "人材机流程", "PE100给水管200根第2批",
new BigDecimal("77000.00"), new BigDecimal("77000.00"), new BigDecimal("77000.00"), "第2批货已到场");
// 批次3:草稿(演示新建编辑状态)
MultiPaymentBatch b3 = new MultiPaymentBatch();
b3.setBatchCode("MPB-2026-003");
b3.setTitle("凯迪环保项目A药剂款汇总");
b3.setProjectName("凯迪环保项目A");
b3.setPayerName("凯迪科技有限公司");
b3.setPayeeName("江苏清河水处理材料");
b3.setTotalRequestAmount(new BigDecimal("45780.00"));
b3.setStatus("草稿");
b3.setApplicant("王成本");
b3.setNote("将3批PAC采购进度款合并一次支付");
b3.setCreatedAt(Instant.now().minus(1, ChronoUnit.DAYS));
b3 = batchRepo.save(b3);
batchItem(b3.getId(), 3001L, "人材机流程", "PAC絮凝剂第1批20吨",
new BigDecimal("43600.00"), new BigDecimal("43600.00"), new BigDecimal("43600.00"), "已入库");
batchItem(b3.getId(), 3002L, "人材机流程", "运费及装卸费",
new BigDecimal("2180.00"), new BigDecimal("2180.00"), new BigDecimal("2180.00"), "发票已到");
}
private void batchItem(Long batchId, Long sourcePaymentId, String sourceType,
String sourceDesc, BigDecimal invoicedAmount,
BigDecimal paidAmount, BigDecimal requestAmount, String note) {
MultiPaymentBatchItem item = new MultiPaymentBatchItem();
item.setBatchId(batchId);
item.setSourcePaymentId(sourcePaymentId);
item.setSourceType(sourceType);
item.setSourceDesc(sourceDesc);
item.setInvoicedAmount(invoicedAmount);
item.setPaidAmount(paidAmount);
item.setRequestAmount(requestAmount);
item.setNote(note);
item.setCreatedAt(Instant.now().minus(7, ChronoUnit.DAYS));
batchItemRepo.save(item);
}
}
@@ -0,0 +1,180 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.CslProject;
import com.kaidi.oa.domain.CslTimesheet;
import com.kaidi.oa.repository.CslProjectRepository;
import com.kaidi.oa.repository.CslTimesheetRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 咨询可研院演示种子数据:
* 播种 3 个 CslProject + 每个项目若干 CslTimesheet(含草稿和已确认状态),
* 让"咨询人员工时录入台账"页面活体可见。
*
* 幂等:按 code 查重后跳过,重启不重复插入。
*/
@Component
@Order(7)
public class CslDemoSeeder implements ApplicationRunner {
private final CslProjectRepository projectRepo;
private final CslTimesheetRepository timesheetRepo;
public CslDemoSeeder(CslProjectRepository projectRepo,
CslTimesheetRepository timesheetRepo) {
this.projectRepo = projectRepo;
this.timesheetRepo = timesheetRepo;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
seedProjects();
}
private void seedProjects() {
// 项目1:可行性研究 - 执行阶段
if (projectRepo.findAll().stream().noneMatch(p -> "CSL-2025-001".equals(p.getCode()))) {
CslProject p1 = new CslProject();
p1.setCode("CSL-2025-001");
p1.setName("某市地铁5号线可行性研究");
p1.setClient("某市城市轨道交通有限公司");
p1.setProjectType("可行性研究");
p1.setField("交通");
p1.setManager("刘建国");
p1.setContractAmount(Money.of(new BigDecimal("1280000.00")));
p1.setBudgetAmount(Money.of(new BigDecimal("980000.00")));
p1.setUrgency(4);
p1.setPriorityScore(88);
p1.setStage("执行");
p1.setApprovalStatus("已立项");
p1.setApprovalFlow("可行性研究立项审批流程");
p1.setProgress(62);
p1.setPlanStart("2025-03-01");
p1.setPlanEnd("2025-09-30");
p1.setCreatedDate("2025-02-15");
p1.setCreatedAt(Instant.now());
p1.setUpdatedAt(Instant.now());
CslProject saved1 = projectRepo.save(p1);
seedTimesheets(saved1.getId(), "CSL-2025-001");
}
// 项目2:项目评估 - 结项验收阶段
if (projectRepo.findAll().stream().noneMatch(p -> "CSL-2025-002".equals(p.getCode()))) {
CslProject p2 = new CslProject();
p2.setCode("CSL-2025-002");
p2.setName("凯迪新材料产业园项目评估");
p2.setClient("凯迪科技集团");
p2.setProjectType("项目评估");
p2.setField("建筑");
p2.setManager("王志远");
p2.setContractAmount(Money.of(new BigDecimal("560000.00")));
p2.setBudgetAmount(Money.of(new BigDecimal("430000.00")));
p2.setUrgency(5);
p2.setPriorityScore(92);
p2.setStage("结项验收");
p2.setApprovalStatus("已立项");
p2.setApprovalFlow("项目评估立项审批流程");
p2.setProgress(95);
p2.setPlanStart("2025-01-10");
p2.setPlanEnd("2025-06-30");
p2.setCreatedDate("2025-01-05");
p2.setCreatedAt(Instant.now());
p2.setUpdatedAt(Instant.now());
CslProject saved2 = projectRepo.save(p2);
seedTimesheets(saved2.getId(), "CSL-2025-002");
}
// 项目3:规划咨询 - 执行阶段
if (projectRepo.findAll().stream().noneMatch(p -> "CSL-2026-001".equals(p.getCode()))) {
CslProject p3 = new CslProject();
p3.setCode("CSL-2026-001");
p3.setName("某区城乡融合发展规划咨询");
p3.setClient("某区人民政府发展改革局");
p3.setProjectType("规划咨询");
p3.setField("综合");
p3.setManager("陈美华");
p3.setContractAmount(Money.of(new BigDecimal("320000.00")));
p3.setBudgetAmount(Money.of(new BigDecimal("240000.00")));
p3.setUrgency(3);
p3.setPriorityScore(75);
p3.setStage("执行");
p3.setApprovalStatus("已立项");
p3.setApprovalFlow("规划咨询立项审批流程");
p3.setProgress(35);
p3.setPlanStart("2026-01-15");
p3.setPlanEnd("2026-08-31");
p3.setCreatedDate("2026-01-10");
p3.setCreatedAt(Instant.now());
p3.setUpdatedAt(Instant.now());
CslProject saved3 = projectRepo.save(p3);
seedTimesheets(saved3.getId(), "CSL-2026-001");
}
}
private void seedTimesheets(Long projectId, String projectCode) {
// 只在该项目下没有工时时才插入
if (!timesheetRepo.findByProjectId(projectId).isEmpty()) {
return;
}
if ("CSL-2025-001".equals(projectCode)) {
// 已确认工时记录
save(projectId, "刘建国", "项目经理", "2025-04-01", 8.0, 150.0, "项目启动会及工作方案编制", "已确认");
save(projectId, "张明轩", "专业负责人", "2025-04-02", 8.0, 120.0, "交通流量调研数据收集与分析", "已确认");
save(projectId, "李晓玲", "主创", "2025-04-03", 8.0, 90.0, "线路走向初步比选方案编制", "已确认");
save(projectId, "王宇", "校审", "2025-04-10", 6.0, 110.0, "第一阶段报告校审与批注", "已确认");
save(projectId, "张明轩", "专业负责人", "2025-05-06", 8.0, 120.0, "客流预测模型建立与测算", "已确认");
save(projectId, "李晓玲", "主创", "2025-05-08", 8.0, 90.0, "投资估算初稿编制", "已确认");
// 草稿工时(近期)
save(projectId, "刘建国", "项目经理", "2025-06-10", 4.0, 150.0, "中期汇报材料准备与客户对接", "草稿");
save(projectId, "赵晨阳", "一般成员", "2025-06-11", 8.0, 70.0, "社会稳定风险评估调研", "草稿");
save(projectId, "李晓玲", "主创", "2025-06-12", 8.0, 90.0, "财务分析与经济效益测算", "草稿");
} else if ("CSL-2025-002".equals(projectCode)) {
save(projectId, "王志远", "项目经理", "2025-02-01", 8.0, 140.0, "需求调研与项目目标确认", "已确认");
save(projectId, "孙丽萍", "专业负责人", "2025-02-05", 8.0, 110.0, "市场调研与竞争态势分析", "已确认");
save(projectId, "陈刚", "主创", "2025-02-10", 8.0, 85.0, "项目技术可行性分析报告", "已确认");
save(projectId, "王志远", "项目经理", "2025-03-15", 6.0, 140.0, "中期评审协调与问题整改", "已确认");
save(projectId, "孙丽萍", "专业负责人", "2025-05-20", 8.0, 110.0, "最终报告修改与定稿", "已确认");
save(projectId, "陈刚", "主创", "2025-05-22", 8.0, 85.0, "投资估算复核与数据勘误", "已确认");
// 草稿:等待验收阶段
save(projectId, "王志远", "项目经理", "2025-06-08", 4.0, 140.0, "验收材料整理与提交", "草稿");
save(projectId, "孙丽萍", "专业负责人", "2025-06-09", 4.0, 110.0, "专家评审意见回复", "草稿");
} else if ("CSL-2026-001".equals(projectCode)) {
save(projectId, "陈美华", "项目经理", "2026-02-01", 8.0, 130.0, "现场踏勘与基础数据收集", "已确认");
save(projectId, "林海涛", "专业负责人", "2026-02-03", 8.0, 100.0, "城镇化现状分析与问题梳理", "已确认");
save(projectId, "赵雪", "主创", "2026-02-08", 8.0, 80.0, "政策法规梳理与规划依据整理", "已确认");
// 草稿
save(projectId, "陈美华", "项目经理", "2026-03-10", 6.0, 130.0, "阶段成果汇报与方向调整讨论", "草稿");
save(projectId, "林海涛", "专业负责人", "2026-03-12", 8.0, 100.0, "产业发展策略专题研究", "草稿");
}
}
private void save(Long projectId, String person, String role,
String workDate, double hours, double rate,
String workContent, String status) {
CslTimesheet t = new CslTimesheet();
t.setProjectId(projectId);
t.setPerson(person);
t.setRole(role);
t.setWorkDate(workDate);
t.setHours(hours);
BigDecimal rateBd = Money.of(rate);
t.setRate(rateBd);
t.setLaborCost(Money.of(rateBd.multiply(BigDecimal.valueOf(hours))));
t.setWorkContent(workContent);
t.setStatus(status);
t.setCreatedAt(Instant.now());
timesheetRepo.save(t);
}
}
@@ -0,0 +1,220 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.CultureMonthlyBriefing;
import com.kaidi.oa.domain.HrCultureKpi;
import com.kaidi.oa.repository.CultureMonthlyBriefingRepository;
import com.kaidi.oa.repository.HrCultureKpiRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
/**
* 品牌推广部·项目文化 W8 缺口演示数据。
*
* 缺口1(low):月刊电子版生成(CultureMonthlyBriefing)——历史简报+当月已生成各一条演示。
* 缺口2(low)KPI回写HR绩效(HrCultureKpi)——已写入2条+待写入1条,让「已写入历史」功能活体可见。
*
* @Order(225) 在 BrandCultureGapSeeder(@Order 220) 之后运行,幂等(count>0跳过)。
*/
@Component
@Order(225)
public class CultureBriefingKpiSeeder implements ApplicationRunner {
private final CultureMonthlyBriefingRepository briefingRepo;
private final HrCultureKpiRepository hrKpiRepo;
public CultureBriefingKpiSeeder(CultureMonthlyBriefingRepository briefingRepo,
HrCultureKpiRepository hrKpiRepo) {
this.briefingRepo = briefingRepo;
this.hrKpiRepo = hrKpiRepo;
}
@Override
public void run(ApplicationArguments args) {
seedBriefings();
seedHrCultureKpis();
}
// ====================================================================
// 月刊电子版演示数据(CultureMonthlyBriefing
// ====================================================================
private void seedBriefings() {
if (briefingRepo.count() > 0) return;
List<CultureMonthlyBriefing> list = new ArrayList<>();
// 2026年3月刊 - 已发布
CultureMonthlyBriefing b1 = new CultureMonthlyBriefing();
b1.setCode("CMB-202603-001");
b1.setIssueTag("2026年第3期");
b1.setPublishMonth("2026-03");
b1.setTitle("凯迪科技项目简报 2026年第3期");
b1.setTaskTotal(18);
b1.setTaskDone(15);
b1.setTaskOverdue(1);
b1.setNewStoryCount(6);
b1.setNewMaterialCount(24);
b1.setProjectCount(3);
b1.setFeaturedProjects("PRJ-2026-001、PRJ-2026-002、PRJ-2025-003");
b1.setSummary("3月份项目宣传工作稳步推进,共完成15项宣传任务,完成率83.3%。南方水务PPP项目推出" +
"「攻坚克难」系列专访共3篇,引发热烈反响。滨江EPC项目荣获客户高度赞誉," +
"典型案例已录入案例库。北部生物质电厂技改项目顺利完工,文化建设获年度标杆评定。");
b1.setDistributeTarget("全员 / 南方水务PPP项目相关客户 / 滨江EPC项目业主方");
b1.setStatus("已发布");
b1.setGeneratedBy("系统自动生成");
b1.setPublishedBy("宣传部·李部长");
b1.setPublishDate("2026-04-01");
b1.setBriefingUrl("https://cdn.kaidi.com/culture-briefing/2026-03-issue.pdf");
b1.setCreatedAt(Instant.now());
list.add(b1);
// 2026年4月刊 - 已发布
CultureMonthlyBriefing b2 = new CultureMonthlyBriefing();
b2.setCode("CMB-202604-001");
b2.setIssueTag("2026年第4期");
b2.setPublishMonth("2026-04");
b2.setTitle("凯迪科技项目简报 2026年第4期");
b2.setTaskTotal(22);
b2.setTaskDone(19);
b2.setTaskOverdue(0);
b2.setNewStoryCount(8);
b2.setNewMaterialCount(31);
b2.setProjectCount(3);
b2.setFeaturedProjects("PRJ-2026-001、PRJ-2026-002");
b2.setSummary("4月份各项目宣传任务完成率达86.4%,创近半年新高。新增8篇项目故事," +
"其中「绿色施工先行者」荣获公司月度最佳故事奖。文化氛围调查回收率90%以上," +
"综合满意度82分,较上季度提升2.5分。");
b2.setDistributeTarget("全员 / 相关客户");
b2.setStatus("已发布");
b2.setGeneratedBy("系统自动生成");
b2.setPublishedBy("宣传部·李部长");
b2.setPublishDate("2026-05-02");
b2.setBriefingUrl("https://cdn.kaidi.com/culture-briefing/2026-04-issue.pdf");
b2.setCreatedAt(Instant.now());
list.add(b2);
// 2026年5月刊 - 已发布
CultureMonthlyBriefing b3 = new CultureMonthlyBriefing();
b3.setCode("CMB-202605-001");
b3.setIssueTag("2026年第5期");
b3.setPublishMonth("2026-05");
b3.setTitle("凯迪科技项目简报 2026年第5期");
b3.setTaskTotal(20);
b3.setTaskDone(17);
b3.setTaskOverdue(2);
b3.setNewStoryCount(5);
b3.setNewMaterialCount(18);
b3.setProjectCount(2);
b3.setFeaturedProjects("PRJ-2026-001、PRJ-2026-002");
b3.setSummary("5月份宣传任务总量20项,完成17项,完成率85%。2项任务因项目现场条件变化延期," +
"已协调宣传员重新排期。本月新增5篇已发布项目故事,素材库新增18份材料。" +
"Q2文化氛围调查正在收集中,截至月底回收率已超50%。");
b3.setDistributeTarget("全员 / 相关客户");
b3.setStatus("已发布");
b3.setGeneratedBy("系统自动生成");
b3.setPublishedBy("宣传部·王主任");
b3.setPublishDate("2026-06-03");
b3.setBriefingUrl("https://cdn.kaidi.com/culture-briefing/2026-05-issue.pdf");
b3.setCreatedAt(Instant.now());
list.add(b3);
// 2026年6月刊 - 已生成(本月,待发布)
CultureMonthlyBriefing b4 = new CultureMonthlyBriefing();
b4.setCode("CMB-202606-001");
b4.setIssueTag("2026年第6期");
b4.setPublishMonth("2026-06");
b4.setTitle("凯迪科技项目简报 2026年第6期");
b4.setTaskTotal(24);
b4.setTaskDone(12);
b4.setTaskOverdue(1);
b4.setNewStoryCount(4);
b4.setNewMaterialCount(9);
b4.setProjectCount(3);
b4.setFeaturedProjects("PRJ-2026-001、PRJ-2026-002、PRJ-2025-003");
b4.setSummary("6月份宣传工作正在进行中,截至月底(预计)将完成24项任务中的20余项。" +
"Q2文化评估调查结束,综合满意度待最终统计。本期将重点报道南方水务项目" +
"「一线感人故事」专题。");
b4.setDistributeTarget("全员 / 南方水务PPP项目相关客户");
b4.setStatus("已生成");
b4.setGeneratedBy("系统自动生成");
b4.setCreatedAt(Instant.now());
list.add(b4);
briefingRepo.saveAll(list);
}
// ====================================================================
// HR 年度绩效文化 KPI 演示数据(HrCultureKpi
// ====================================================================
private void seedHrCultureKpis() {
if (hrKpiRepo.count() > 0) return;
List<HrCultureKpi> list = new ArrayList<>();
// 2025年度 北部生物质电厂 - 已写入(优秀,完整历史记录)
HrCultureKpi k1 = new HrCultureKpi();
k1.setCode("HCK-2025-001");
k1.setAssessYear("2025");
k1.setProjectCode("PRJ-2025-003");
k1.setProjectName("北部生物质电厂技改项目");
k1.setCulturalOfficer("刘宇航");
k1.setCultureTotalScore(88.5);
k1.setCultureGrade("优秀");
k1.setFusedSatisfaction(88.15);
k1.setHrSatisfaction(89.2);
k1.setWriteTarget("HR年度绩效·项目文化KPI项");
k1.setKpiWeight(10);
k1.setKpiContribution(8.85);
k1.setConfirmedBy("人力资源部·张经理");
k1.setConfirmedAt(Instant.parse("2026-01-15T09:00:00Z"));
k1.setRemark("项目文化建设成效突出,已作为公司文化标杆,建议纳入年度最佳案例表彰。");
k1.setStatus("已写入");
k1.setCreatedAt(Instant.parse("2026-01-10T08:00:00Z"));
list.add(k1);
// 2026年度 南方水务PPP项目 - 待写入(当年,Q2评估完成后待HR确认)
HrCultureKpi k2 = new HrCultureKpi();
k2.setCode("HCK-2026-001");
k2.setAssessYear("2026");
k2.setProjectCode("PRJ-2026-001");
k2.setProjectName("南方水务污水处理PPP项目");
k2.setCulturalOfficer("王晓峰");
k2.setCultureTotalScore(79.3);
k2.setCultureGrade("良好");
k2.setFusedSatisfaction(82.6);
k2.setHrSatisfaction(83.2);
k2.setWriteTarget("HR年度绩效·项目文化KPI项");
k2.setKpiWeight(10);
k2.setKpiContribution(7.93);
k2.setRemark("Q2数据:文化氛围问卷82分,HR满意度83.2分,融合满意度82.6分。待年底最终确认。");
k2.setStatus("待写入");
k2.setCreatedAt(Instant.now());
list.add(k2);
// 2026年度 滨江工业废水EPC项目 - 待写入
HrCultureKpi k3 = new HrCultureKpi();
k3.setCode("HCK-2026-002");
k3.setAssessYear("2026");
k3.setProjectCode("PRJ-2026-002");
k3.setProjectName("滨江工业废水处理EPC项目");
k3.setCulturalOfficer("陈美珍");
k3.setCultureTotalScore(65.8);
k3.setCultureGrade("合格");
k3.setFusedSatisfaction(75.15);
k3.setHrSatisfaction(74.8);
k3.setWriteTarget("HR年度绩效·项目文化KPI项");
k3.setKpiWeight(10);
k3.setKpiContribution(6.58);
k3.setRemark("满意度得分偏低,建议加强项目文化活动开展频次,目标Q3满意度提升至80+。");
k3.setStatus("待写入");
k3.setCreatedAt(Instant.now());
list.add(k3);
hrKpiRepo.saveAll(list);
}
}
@@ -0,0 +1,311 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.CultureGoal;
import com.kaidi.oa.domain.CultureSurvey;
import com.kaidi.oa.domain.HrEmpSatisfactionSurvey;
import com.kaidi.oa.repository.CultureGoalRepository;
import com.kaidi.oa.repository.CultureSurveyRepository;
import com.kaidi.oa.repository.HrEmpSatisfactionSurveyRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
/**
* 品牌推广部·项目文化考核演示数据种子。
*
* 补全三类"活体无数据"缺口,让「项目文化评估与反馈」和「项目文化考核与驾驶舱」功能真正活体可见:
*
* 1. CultureGoal:项目文化目标(关联三个典型项目,有预算/故事/稿件/满意度目标值及实际开支)。
* 2. CultureSurvey:品牌推广部自有文化氛围问卷(已结束 + 综合得分,供文化考核引用)。
* 3. HrEmpSatisfactionSurveyHR 部门独立员工满意度调查(已完成 + HR综合得分,跨模块融合)。
*
* @Order(50),在 DataSeeder(@Order 1) 及 BrandCultureSeeder(@Order 22) 之后运行。
* 幂等:CultureGoal 有数据时跳过全部种子。
*/
@Component
@Order(50)
public class CultureProjectAssessSeeder implements ApplicationRunner {
private final CultureGoalRepository goalRepo;
private final CultureSurveyRepository surveyRepo;
private final HrEmpSatisfactionSurveyRepository hrSatisfyRepo;
public CultureProjectAssessSeeder(CultureGoalRepository goalRepo,
CultureSurveyRepository surveyRepo,
HrEmpSatisfactionSurveyRepository hrSatisfyRepo) {
this.goalRepo = goalRepo;
this.surveyRepo = surveyRepo;
this.hrSatisfyRepo = hrSatisfyRepo;
}
@Override
public void run(ApplicationArguments args) {
if (goalRepo.count() > 0) {
return;
}
seedCultureGoals();
seedCultureSurveys();
seedHrSatisfactionSurveys();
}
// =======================================================================
// 1. 项目文化目标(CultureGoal
// =======================================================================
private void seedCultureGoals() {
List<CultureGoal> goals = new ArrayList<>();
// 项目1:南方水务污水处理PPP项目
CultureGoal g1 = new CultureGoal();
g1.setSchemeId(1L);
g1.setSchemeCode("CS-2026-001");
g1.setProjectCode("PRJ-2026-001");
g1.setProjectName("南方水务污水处理PPP项目");
g1.setCulturalOfficer("王晓峰");
g1.setTargetStoryCount(8);
g1.setTargetArticleCount(20);
g1.setTargetCohesion(80.0);
g1.setTargetSatisfaction(82.0);
g1.setBudgetAmount(new BigDecimal("48000.00"));
g1.setSpentAmount(new BigDecimal("41500.00"));
g1.setStatus("进行中");
g1.setCreatedAt(Instant.now());
goals.add(g1);
// 项目2:滨江工业废水处理EPC项目
CultureGoal g2 = new CultureGoal();
g2.setSchemeId(1L);
g2.setSchemeCode("CS-2026-001");
g2.setProjectCode("PRJ-2026-002");
g2.setProjectName("滨江工业废水处理EPC项目");
g2.setCulturalOfficer("陈美珍");
g2.setTargetStoryCount(6);
g2.setTargetArticleCount(15);
g2.setTargetCohesion(78.0);
g2.setTargetSatisfaction(80.0);
g2.setBudgetAmount(new BigDecimal("35000.00"));
g2.setSpentAmount(new BigDecimal("29800.00"));
g2.setStatus("进行中");
g2.setCreatedAt(Instant.now());
goals.add(g2);
// 项目3:北部生物质电厂技改项目(已完工,做对照)
CultureGoal g3 = new CultureGoal();
g3.setSchemeId(1L);
g3.setSchemeCode("CS-2025-003");
g3.setProjectCode("PRJ-2025-003");
g3.setProjectName("北部生物质电厂技改项目");
g3.setCulturalOfficer("刘宇航");
g3.setTargetStoryCount(10);
g3.setTargetArticleCount(25);
g3.setTargetCohesion(85.0);
g3.setTargetSatisfaction(85.0);
g3.setBudgetAmount(new BigDecimal("52000.00"));
g3.setSpentAmount(new BigDecimal("50200.00"));
g3.setStatus("已达标");
g3.setCreatedAt(Instant.now());
goals.add(g3);
goalRepo.saveAll(goals);
}
// =======================================================================
// 2. 品牌推广部自有文化氛围问卷(CultureSurvey
// =======================================================================
private void seedCultureSurveys() {
if (surveyRepo.count() > 0) {
return;
}
List<CultureSurvey> surveys = new ArrayList<>();
// PRJ-2026-0012026Q1 已结束
CultureSurvey s1 = new CultureSurvey();
s1.setCode("CS-SVY-2026Q1-001");
s1.setTitle("南方水务项目 2026Q1 文化氛围调查");
s1.setProjectCode("PRJ-2026-001");
s1.setProjectName("南方水务污水处理PPP项目");
s1.setPeriod("2026Q1");
s1.setStatus("已结束");
s1.setSampleCount(35);
s1.setRespondedCount(31);
s1.setScoreTeamwork(84.5);
s1.setScoreLeadership(81.0);
s1.setScoreHappiness(79.5);
s1.setScoreBelonging(83.0);
s1.setOverallScore(82.0);
s1.setOpenDate("2026-03-01");
s1.setCloseDate("2026-03-31");
s1.setCreatedAt(Instant.now());
surveys.add(s1);
// PRJ-2026-0012026Q2 收集中
CultureSurvey s2 = new CultureSurvey();
s2.setCode("CS-SVY-2026Q2-001");
s2.setTitle("南方水务项目 2026Q2 文化氛围调查");
s2.setProjectCode("PRJ-2026-001");
s2.setProjectName("南方水务污水处理PPP项目");
s2.setPeriod("2026Q2");
s2.setStatus("收集中");
s2.setSampleCount(35);
s2.setRespondedCount(18);
s2.setScoreTeamwork(0.0);
s2.setScoreLeadership(0.0);
s2.setScoreHappiness(0.0);
s2.setScoreBelonging(0.0);
s2.setOverallScore(0.0);
s2.setOpenDate("2026-06-01");
s2.setCloseDate("2026-06-30");
s2.setCreatedAt(Instant.now());
surveys.add(s2);
// PRJ-2026-0022026Q1 已结束
CultureSurvey s3 = new CultureSurvey();
s3.setCode("CS-SVY-2026Q1-002");
s3.setTitle("滨江工业废水项目 2026Q1 文化氛围调查");
s3.setProjectCode("PRJ-2026-002");
s3.setProjectName("滨江工业废水处理EPC项目");
s3.setPeriod("2026Q1");
s3.setStatus("已结束");
s3.setSampleCount(28);
s3.setRespondedCount(25);
s3.setScoreTeamwork(77.0);
s3.setScoreLeadership(75.5);
s3.setScoreHappiness(73.0);
s3.setScoreBelonging(76.5);
s3.setOverallScore(75.5);
s3.setOpenDate("2026-03-01");
s3.setCloseDate("2026-03-31");
s3.setCreatedAt(Instant.now());
surveys.add(s3);
// PRJ-2025-003:年度已结束(2025
CultureSurvey s4 = new CultureSurvey();
s4.setCode("CS-SVY-2025-003");
s4.setTitle("北部生物质电厂项目 2025 年度文化调查");
s4.setProjectCode("PRJ-2025-003");
s4.setProjectName("北部生物质电厂技改项目");
s4.setPeriod("2025年度");
s4.setStatus("已结束");
s4.setSampleCount(42);
s4.setRespondedCount(40);
s4.setScoreTeamwork(88.5);
s4.setScoreLeadership(86.0);
s4.setScoreHappiness(85.0);
s4.setScoreBelonging(89.0);
s4.setOverallScore(87.1);
s4.setOpenDate("2025-12-01");
s4.setCloseDate("2025-12-31");
s4.setCreatedAt(Instant.now());
surveys.add(s4);
surveyRepo.saveAll(surveys);
}
// =======================================================================
// 3. HR 独立员工满意度调查(HrEmpSatisfactionSurvey
// =======================================================================
private void seedHrSatisfactionSurveys() {
if (hrSatisfyRepo.count() > 0) {
return;
}
List<HrEmpSatisfactionSurvey> surveys = new ArrayList<>();
// PRJ-2026-0012026Q1 HR 已完成
HrEmpSatisfactionSurvey h1 = new HrEmpSatisfactionSurvey();
h1.setCode("HR-SAT-2026Q1-001");
h1.setTitle("南方水务项目员工满意度与敬业度调查 2026Q1");
h1.setInitiatorDept("人力资源部");
h1.setProjectCode("PRJ-2026-001");
h1.setProjectName("南方水务污水处理PPP项目");
h1.setPeriod("2026Q1");
h1.setStatus("已完成");
h1.setTotalCount(35);
h1.setRespondedCount(33);
h1.setScoreJobSatisfaction(83.0);
h1.setScoreEngagement(85.0);
h1.setScoreManagement(80.5);
h1.setScoreTeamwork(86.0);
h1.setScoreWellbeing(81.5);
h1.setHrOverallScore(83.2);
h1.setIssuesSummary("整体满意度良好;部分员工反映项目现场生活条件待改善,已提交至行政部跟进。");
h1.setOpenDate("2026-03-05");
h1.setCloseDate("2026-03-28");
h1.setCreatedAt(Instant.now());
surveys.add(h1);
// PRJ-2026-0012026Q2 HR 收集中
HrEmpSatisfactionSurvey h2 = new HrEmpSatisfactionSurvey();
h2.setCode("HR-SAT-2026Q2-001");
h2.setTitle("南方水务项目员工满意度与敬业度调查 2026Q2");
h2.setInitiatorDept("人力资源部");
h2.setProjectCode("PRJ-2026-001");
h2.setProjectName("南方水务污水处理PPP项目");
h2.setPeriod("2026Q2");
h2.setStatus("收集中");
h2.setTotalCount(35);
h2.setRespondedCount(12);
h2.setScoreJobSatisfaction(0.0);
h2.setScoreEngagement(0.0);
h2.setScoreManagement(0.0);
h2.setScoreTeamwork(0.0);
h2.setScoreWellbeing(0.0);
h2.setHrOverallScore(0.0);
h2.setOpenDate("2026-06-05");
h2.setCloseDate("2026-06-28");
h2.setCreatedAt(Instant.now());
surveys.add(h2);
// PRJ-2026-0022026Q1 HR 已完成
HrEmpSatisfactionSurvey h3 = new HrEmpSatisfactionSurvey();
h3.setCode("HR-SAT-2026Q1-002");
h3.setTitle("滨江工业废水项目员工满意度与敬业度调查 2026Q1");
h3.setInitiatorDept("人力资源部");
h3.setProjectCode("PRJ-2026-002");
h3.setProjectName("滨江工业废水处理EPC项目");
h3.setPeriod("2026Q1");
h3.setStatus("已完成");
h3.setTotalCount(28);
h3.setRespondedCount(26);
h3.setScoreJobSatisfaction(76.0);
h3.setScoreEngagement(74.5);
h3.setScoreManagement(73.0);
h3.setScoreTeamwork(78.0);
h3.setScoreWellbeing(72.5);
h3.setHrOverallScore(74.8);
h3.setIssuesSummary("敬业度中等;员工反映项目工期压力较大,建议适当调整排班;管理层沟通透明度待提升。");
h3.setOpenDate("2026-03-05");
h3.setCloseDate("2026-03-28");
h3.setCreatedAt(Instant.now());
surveys.add(h3);
// PRJ-2025-0032025 年度 HR 已完成
HrEmpSatisfactionSurvey h4 = new HrEmpSatisfactionSurvey();
h4.setCode("HR-SAT-2025-003");
h4.setTitle("北部生物质电厂项目 2025 年度员工满意度调查");
h4.setInitiatorDept("人力资源部");
h4.setProjectCode("PRJ-2025-003");
h4.setProjectName("北部生物质电厂技改项目");
h4.setPeriod("2025年度");
h4.setStatus("已完成");
h4.setTotalCount(42);
h4.setRespondedCount(41);
h4.setScoreJobSatisfaction(89.0);
h4.setScoreEngagement(91.0);
h4.setScoreManagement(88.5);
h4.setScoreTeamwork(90.0);
h4.setScoreWellbeing(87.5);
h4.setHrOverallScore(89.2);
h4.setIssuesSummary("满意度极高;项目荣获年度最佳团队,员工归属感强,建议作为文化建设标杆推广。");
h4.setOpenDate("2025-12-05");
h4.setCloseDate("2025-12-28");
h4.setCreatedAt(Instant.now());
surveys.add(h4);
hrSatisfyRepo.saveAll(surveys);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,79 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.DeclAchievement;
import com.kaidi.oa.repository.DeclAchievementRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
/**
* 申报成果库演示数据补种(Gap 4:活体 decl-achievements 0条,功能可达但未预置演示数据)。
* Order(3)=在 DataSeeder(Order 1)/AdminQualSeeder 之后运行,避免依赖竞争。
*/
@Component
@Order(3)
public class DeclAchievementSeeder implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(DeclAchievementSeeder.class);
private final DeclAchievementRepository repo;
public DeclAchievementSeeder(DeclAchievementRepository repo) {
this.repo = repo;
}
@Override
public void run(String... args) {
if (repo.count() > 0) {
return;
}
log.info("Seeding DeclAchievement demo data...");
seed("高新技术企业认定", "资质证书", "HJPD202312345", "国家科学技术部", "2023-01-01", "2026-12-31", 0d, false);
seed("科技型中小企业评价", "立项文件", "科小评2024-KD-0588", "湖南省科学技术厅", "2024-03-01", "2025-02-28", 0d, false);
seed("省级企业技术中心", "资质证书", "HNCZX-2023-0127", "湖南省工业和信息化厅", "2023-05-01", "2026-04-30", 0d, false);
seed("制造业单项冠军培育", "立项文件", "DGCR-2024-HN-0012", "工业和信息化部", "2024-06-01", null, 0d, false);
seed("省重点研发项目立项", "立项文件", "2024JZ2034", "湖南省科学技术厅", "2024-01-10", "2025-12-31", 800000d, false);
seed("科技成果转化示范企业", "荣誉证书", "CGZH-2023-HN-0089", "湖南省科学技术厅", "2023-09-01", "2026-08-31", 0d, false);
seed("创新型中小企业认定", "资质证书", "CXXX-2024-CS-0445", "长沙市工信局", "2024-02-01", "2025-01-31", 0d, false);
// 一条已归档的示例
DeclAchievement archived = buildAchievement(
"高企认定2022年度", "资质证书", "HJPD202212300",
"国家科学技术部", "2022-01-01", "2024-12-31", 0d);
archived.setArchived(Boolean.TRUE);
archived.setArchiveNo("ARC-DECL-2022-1");
repo.save(archived);
log.info("DeclAchievement demo data seeded: {} records.", repo.count());
}
private void seed(String project, String achieveType, String certNo,
String authority, String validFrom, String validTo,
double grantedAmount, boolean archived) {
DeclAchievement a = buildAchievement(project, achieveType, certNo,
authority, validFrom, validTo, grantedAmount);
a.setArchived(archived);
repo.save(a);
}
private DeclAchievement buildAchievement(String project, String achieveType, String certNo,
String authority, String validFrom, String validTo,
double grantedAmount) {
DeclAchievement a = new DeclAchievement();
a.setCode("CG-" + (repo.count() + 1));
a.setProjectName(project);
a.setAchieveType(achieveType);
a.setCertNo(certNo);
a.setAuthority(authority);
a.setValidFrom(validFrom);
a.setValidTo(validTo);
a.setGrantedAmount(Money.of(grantedAmount));
a.setOwner("申报专员");
a.setArchived(Boolean.FALSE);
a.setCreatedAt(Instant.now());
return a;
}
}
@@ -0,0 +1,311 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.FertAccountingVoucher;
import com.kaidi.oa.domain.FertInvoiceLedger;
import com.kaidi.oa.domain.FertPkgMaterialLedger;
import com.kaidi.oa.repository.FertAccountingVoucherRepository;
import com.kaidi.oa.repository.FertInvoiceLedgerRepository;
import com.kaidi.oa.repository.FertPkgMaterialLedgerRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 生物质肥料制造中心·包装物台账 + 会计凭证台账 + 发票台账演示种子数据。
*
* 补全审计缺口:
* 1. 包装物(编织袋/标签/吨袋)独立台账(4 条,覆盖多规格+多库位+低库存警示);
* 2. 会计凭证台账(3 条:草稿/已过账/已反冲,覆盖成本结转 + 期末结转);
* 3. 发票台账(4 条:采购发票2条+销售发票2条,覆盖全状态流转)。
*
* 幂等:按唯一编码查重后跳过,重启不重复插入。
* Order(201) 保证在 DataSeeder(200) 之后执行。
*/
@Component
@Order(201)
public class FertPkgAccountingSeeder implements ApplicationRunner {
private final FertPkgMaterialLedgerRepository pkgRepo;
private final FertAccountingVoucherRepository voucherRepo;
private final FertInvoiceLedgerRepository invoiceRepo;
public FertPkgAccountingSeeder(FertPkgMaterialLedgerRepository pkgRepo,
FertAccountingVoucherRepository voucherRepo,
FertInvoiceLedgerRepository invoiceRepo) {
this.pkgRepo = pkgRepo;
this.voucherRepo = voucherRepo;
this.invoiceRepo = invoiceRepo;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
seedPkgMaterials();
seedAccountingVouchers();
seedInvoiceLedger();
}
// -------- 包装物台账 --------
private void seedPkgMaterials() {
// 编织袋 25kg
if (pkgRepo.findByPkgCode("PKG-WB-25KG").isEmpty()) {
FertPkgMaterialLedger p = new FertPkgMaterialLedger();
p.setPkgCode("PKG-WB-25KG");
p.setPkgName("编织袋(25kg)");
p.setSpec("50cm×80cm,白色印花,凯迪有机肥专用");
p.setWarehouse("包装物库");
p.setBin("A货架-01");
p.setUnit("");
p.setQty(new BigDecimal("8500"));
p.setSafetyStock(new BigDecimal("2000"));
p.setUnitCost(Money.of(new BigDecimal("0.65")));
p.setSupplier("益丰塑编有限公司");
p.setLastInboundDate("2026-05-10");
p.setLinkedBatchNo("FERM-2026-05-003");
p.setPackSpec("25kg");
p.setLastMoveType("入库");
p.setStatus("在库");
p.setRemark("本月采购3万条,已领用2.15万条");
p.setCreatedAt(Instant.now());
pkgRepo.save(p);
}
// 编织袋 50kg
if (pkgRepo.findByPkgCode("PKG-WB-50KG").isEmpty()) {
FertPkgMaterialLedger p = new FertPkgMaterialLedger();
p.setPkgCode("PKG-WB-50KG");
p.setPkgName("编织袋(50kg)");
p.setSpec("60cm×100cm,白色印花,颗粒有机肥专用");
p.setWarehouse("包装物库");
p.setBin("A货架-02");
p.setUnit("");
p.setQty(new BigDecimal("1200"));
p.setSafetyStock(new BigDecimal("1500"));
p.setUnitCost(Money.of(new BigDecimal("0.90")));
p.setSupplier("益丰塑编有限公司");
p.setLastInboundDate("2026-04-28");
p.setLinkedBatchNo("FERM-2026-04-007");
p.setPackSpec("50kg");
p.setLastMoveType("领用");
p.setStatus("低库存");
p.setRemark("库存低于安全库存,建议补货");
p.setCreatedAt(Instant.now());
pkgRepo.save(p);
}
// 吨袋
if (pkgRepo.findByPkgCode("PKG-TB-1000KG").isEmpty()) {
FertPkgMaterialLedger p = new FertPkgMaterialLedger();
p.setPkgCode("PKG-TB-1000KG");
p.setPkgName("吨袋(1000kg)");
p.setSpec("90cm×90cm×130cm,四吊绳,白色");
p.setWarehouse("包装物库");
p.setBin("B货架-01");
p.setUnit("");
p.setQty(new BigDecimal("340"));
p.setSafetyStock(new BigDecimal("100"));
p.setUnitCost(Money.of(new BigDecimal("12.50")));
p.setSupplier("汇鑫集装袋厂");
p.setLastInboundDate("2026-05-20");
p.setLinkedBatchNo("FERM-2026-05-001");
p.setPackSpec("吨袋");
p.setLastMoveType("入库");
p.setStatus("在库");
p.setRemark("大客户专用,与农业合作社订单关联");
p.setCreatedAt(Instant.now());
pkgRepo.save(p);
}
// 产品标签
if (pkgRepo.findByPkgCode("PKG-LBL-STD").isEmpty()) {
FertPkgMaterialLedger p = new FertPkgMaterialLedger();
p.setPkgCode("PKG-LBL-STD");
p.setPkgName("产品标签(自粘)");
p.setSpec("12cm×8cm,自粘不干胶,NY525标准合格证");
p.setWarehouse("包装物库");
p.setBin("发货暂存区");
p.setUnit("");
p.setQty(new BigDecimal("52000"));
p.setSafetyStock(new BigDecimal("5000"));
p.setUnitCost(Money.of(new BigDecimal("0.08")));
p.setSupplier("天成印刷有限公司");
p.setLastInboundDate("2026-06-01");
p.setPackSpec("25kg");
p.setLastMoveType("入库");
p.setStatus("在库");
p.setRemark("含执行标准/生产日期/有效期/合格证内容,已贴标约3万张");
p.setCreatedAt(Instant.now());
pkgRepo.save(p);
}
}
// -------- 会计凭证台账 --------
private void seedAccountingVouchers() {
// 凭证1:已过账的批次成本结转凭证
if (voucherRepo.findByVoucherNo("FTV-BATCH-FERM-2026-05-001-2026-05-31").isEmpty()) {
FertAccountingVoucher v = new FertAccountingVoucher();
v.setVoucherNo("FTV-BATCH-FERM-2026-05-001-2026-05-31");
v.setVoucherType("成本结转");
v.setBatchRef("FERM-2026-05-001");
v.setProductName("有机肥(粉状)");
v.setPeriod("2026-05");
v.setVoucherDate("2026-05-31");
v.setTotalAmount(Money.of(new BigDecimal("186420.00")));
v.setLinesJson("[{\"account\":\"4001\",\"accountName\":\"生产成本-直接材料\",\"direction\":\"\",\"amount\":138600.00,\"summary\":\"FERM-2026-05-001 原料成本\"},{\"account\":\"1211\",\"accountName\":\"原材料\",\"direction\":\"\",\"amount\":138600.00,\"summary\":\"FERM-2026-05-001 原料领用\"},{\"account\":\"4002\",\"accountName\":\"生产成本-直接人工\",\"direction\":\"\",\"amount\":28400.00,\"summary\":\"FERM-2026-05-001 人工成本\"},{\"account\":\"2211\",\"accountName\":\"应付职工薪酬\",\"direction\":\"\",\"amount\":28400.00,\"summary\":\"FERM-2026-05-001 工资分摊\"},{\"account\":\"4003\",\"accountName\":\"生产成本-制造费用\",\"direction\":\"\",\"amount\":19420.00,\"summary\":\"FERM-2026-05-001 制造费用\"},{\"account\":\"4101\",\"accountName\":\"制造费用\",\"direction\":\"\",\"amount\":19420.00,\"summary\":\"FERM-2026-05-001 费用分摊\"},{\"account\":\"1403\",\"accountName\":\"库存商品\",\"direction\":\"\",\"amount\":186420.00,\"summary\":\"FERM-2026-05-001 完工入库\"},{\"account\":\"4001\",\"accountName\":\"生产成本\",\"direction\":\"\",\"amount\":186420.00,\"summary\":\"FERM-2026-05-001 结转完工成本\"}]");
v.setLineCount(8);
v.setStatus("已过账");
v.setPreparer("系统自动");
v.setPoster("李会计");
v.setPostedAt(Instant.parse("2026-05-31T08:30:00Z"));
v.setRemark("5月份第1批有机肥(粉状)成本结转,已过账");
v.setCreatedAt(Instant.parse("2026-05-31T08:00:00Z"));
voucherRepo.save(v);
}
// 凭证2:草稿状态期末结转凭证
if (voucherRepo.findByVoucherNo("FTV-PERIOD-2026-05").isEmpty()) {
FertAccountingVoucher v = new FertAccountingVoucher();
v.setVoucherNo("FTV-PERIOD-2026-05");
v.setVoucherType("期末结转");
v.setBatchRef("期间汇总-6批次");
v.setProductName("2026年5月期末汇总");
v.setPeriod("2026-05");
v.setVoucherDate("2026-05-31");
v.setTotalAmount(Money.of(new BigDecimal("1043680.00")));
v.setLinesJson("[{\"account\":\"4001\",\"accountName\":\"生产成本-直接材料\",\"direction\":\"\",\"amount\":762400.00,\"summary\":\"2026-05 直接原料结转\"},{\"account\":\"4002\",\"accountName\":\"生产成本-直接人工\",\"direction\":\"\",\"amount\":168200.00,\"summary\":\"2026-05 人工成本结转\"},{\"account\":\"4003\",\"accountName\":\"生产成本-制造费用\",\"direction\":\"\",\"amount\":113080.00,\"summary\":\"2026-05 制造费用结转\"},{\"account\":\"1403\",\"accountName\":\"库存商品\",\"direction\":\"\",\"amount\":1043680.00,\"summary\":\"2026-05 完工产品入库\"}]");
v.setLineCount(4);
v.setStatus("草稿");
v.setPreparer("系统自动");
v.setRemark("2026年5月期末成本结转汇总凭证(6个批次),待会计审核过账");
v.setCreatedAt(Instant.now());
voucherRepo.save(v);
}
// 凭证3:已反冲的凭证(演示反冲功能)
if (voucherRepo.findByVoucherNo("FTV-BATCH-FERM-2026-04-002-REVERSED").isEmpty()) {
FertAccountingVoucher v = new FertAccountingVoucher();
v.setVoucherNo("FTV-BATCH-FERM-2026-04-002-REVERSED");
v.setVoucherType("成本结转");
v.setBatchRef("FERM-2026-04-002");
v.setProductName("生物有机肥(颗粒)");
v.setPeriod("2026-04");
v.setVoucherDate("2026-04-30");
v.setTotalAmount(Money.of(new BigDecimal("234850.00")));
v.setLinesJson("[{\"account\":\"4001\",\"accountName\":\"生产成本-直接材料\",\"direction\":\"\",\"amount\":175200.00,\"summary\":\"FERM-2026-04-002 原料成本\"},{\"account\":\"1211\",\"accountName\":\"原材料\",\"direction\":\"\",\"amount\":175200.00,\"summary\":\"FERM-2026-04-002 原料领用\"},{\"account\":\"1403\",\"accountName\":\"库存商品\",\"direction\":\"\",\"amount\":234850.00,\"summary\":\"FERM-2026-04-002 完工入库\"},{\"account\":\"4001\",\"accountName\":\"生产成本\",\"direction\":\"\",\"amount\":234850.00,\"summary\":\"FERM-2026-04-002 结转完工\"}]");
v.setLineCount(4);
v.setStatus("已反冲");
v.setPreparer("系统自动");
v.setPoster("李会计");
v.setPostedAt(Instant.parse("2026-04-30T09:00:00Z"));
v.setRemark("原过账凭证因成本数据更正,已红字冲销;请重新生成");
v.setCreatedAt(Instant.parse("2026-04-30T08:00:00Z"));
voucherRepo.save(v);
}
}
// -------- 发票台账 --------
private void seedInvoiceLedger() {
// 采购发票1:已核销(畜禽粪污原料采购)
if (invoiceRepo.findByInvoiceNo("31120260500123456").isEmpty()) {
FertInvoiceLedger inv = new FertInvoiceLedger();
inv.setInvoiceNo("31120260500123456");
inv.setInvoiceType("采购发票");
inv.setCounterpart("绿源养殖场");
inv.setInvoiceDate("2026-05-08");
inv.setPeriod("2026-05");
inv.setAmountExclTax(Money.of(new BigDecimal("87600.00")));
inv.setTaxAmount(Money.of(new BigDecimal("7884.00")));
inv.setTotalAmount(Money.of(new BigDecimal("95484.00")));
inv.setTaxRate(Money.of(new BigDecimal("9.00")));
inv.setGoodsDesc("畜禽粪污(猪粪)原料采购");
inv.setLinkedOrderNo("PO-2026-05-011");
inv.setLinkedBatchRef("FERM-2026-05-001");
inv.setVoucherNo("FTV-BATCH-FERM-2026-05-001-2026-05-31");
inv.setStatus("已核销");
inv.setCertifyDate("2026-05-12");
inv.setWriteOffDate("2026-05-25");
inv.setHandler("王采购");
inv.setRemark("增值税专票,已抵扣认证,已与付款单核销");
inv.setCreatedAt(Instant.parse("2026-05-08T10:00:00Z"));
invoiceRepo.save(inv);
}
// 采购发票2:已认证(秸秆原料)
if (invoiceRepo.findByInvoiceNo("31120260500234567").isEmpty()) {
FertInvoiceLedger inv = new FertInvoiceLedger();
inv.setInvoiceNo("31120260500234567");
inv.setInvoiceType("采购发票");
inv.setCounterpart("丰收秸秆收储站");
inv.setInvoiceDate("2026-05-18");
inv.setPeriod("2026-05");
inv.setAmountExclTax(Money.of(new BigDecimal("43200.00")));
inv.setTaxAmount(Money.of(new BigDecimal("3888.00")));
inv.setTotalAmount(Money.of(new BigDecimal("47088.00")));
inv.setTaxRate(Money.of(new BigDecimal("9.00")));
inv.setGoodsDesc("秸秆(玉米秸秆)原料采购");
inv.setLinkedOrderNo("PO-2026-05-022");
inv.setLinkedBatchRef("FERM-2026-05-003");
inv.setStatus("已认证");
inv.setCertifyDate("2026-05-22");
inv.setHandler("王采购");
inv.setRemark("已认证,待与付款单核销");
inv.setCreatedAt(Instant.parse("2026-05-18T14:00:00Z"));
invoiceRepo.save(inv);
}
// 销售发票1:已核销(种植大户)
if (invoiceRepo.findByInvoiceNo("60420260500001234").isEmpty()) {
FertInvoiceLedger inv = new FertInvoiceLedger();
inv.setInvoiceNo("60420260500001234");
inv.setInvoiceType("销售发票");
inv.setCounterpart("绿野农业合作社");
inv.setInvoiceDate("2026-05-20");
inv.setPeriod("2026-05");
inv.setAmountExclTax(Money.of(new BigDecimal("136000.00")));
inv.setTaxAmount(Money.of(new BigDecimal("12240.00")));
inv.setTotalAmount(Money.of(new BigDecimal("148240.00")));
inv.setTaxRate(Money.of(new BigDecimal("9.00")));
inv.setGoodsDesc("有机肥料(粉状)800吨");
inv.setLinkedOrderNo("SO-2026-05-008");
inv.setLinkedBatchRef("FERM-2026-05-001");
inv.setStatus("已核销");
inv.setCertifyDate("2026-05-20");
inv.setWriteOffDate("2026-05-30");
inv.setHandler("赵销售");
inv.setRemark("已开票,客户已回款,已核销");
inv.setCreatedAt(Instant.parse("2026-05-20T09:00:00Z"));
invoiceRepo.save(inv);
}
// 销售发票2:待认证(新客户,月底开票)
if (invoiceRepo.findByInvoiceNo("60420260600000321").isEmpty()) {
FertInvoiceLedger inv = new FertInvoiceLedger();
inv.setInvoiceNo("60420260600000321");
inv.setInvoiceType("销售发票");
inv.setCounterpart("远景生态农业有限公司");
inv.setInvoiceDate("2026-06-05");
inv.setPeriod("2026-06");
inv.setAmountExclTax(Money.of(new BigDecimal("68000.00")));
inv.setTaxAmount(Money.of(new BigDecimal("6120.00")));
inv.setTotalAmount(Money.of(new BigDecimal("74120.00")));
inv.setTaxRate(Money.of(new BigDecimal("9.00")));
inv.setGoodsDesc("生物有机肥(颗粒)400吨");
inv.setLinkedOrderNo("SO-2026-06-003");
inv.setLinkedBatchRef("FERM-2026-05-004");
inv.setStatus("待认证");
inv.setHandler("赵销售");
inv.setRemark("6月新客户首单,已发货,发票已开,待客户确认");
inv.setCreatedAt(Instant.parse("2026-06-05T11:00:00Z"));
invoiceRepo.save(inv);
}
}
}
@@ -0,0 +1,318 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.FinAccountStandard;
import com.kaidi.oa.domain.FinElecArchive;
import com.kaidi.oa.domain.FinEmpLoan;
import com.kaidi.oa.domain.FinRdPersonnel;
import com.kaidi.oa.domain.FinRdTimesheet;
import com.kaidi.oa.repository.FinAccountStandardRepository;
import com.kaidi.oa.repository.FinElecArchiveRepository;
import com.kaidi.oa.repository.FinEmpLoanRepository;
import com.kaidi.oa.repository.FinRdPersonnelRepository;
import com.kaidi.oa.repository.FinRdTimesheetRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
/**
* 财务部深水缺口演示种子数据(缺口 #2/#4/#12/#13)。
*
* 补齐:
* - FinEmpLoan(员工借款,缺口#2):5 条跨状态演示
* - FinRdPersonnel(研发人员档案,缺口#4):6 条跨岗位类型/用工类型
* - FinRdTimesheet(工时填报,缺口#4):8 条跨项目/状态
* - FinAccountStandard(多会计准则配置,缺口#12/#13):2 条(CAS+IFRS
* - FinElecArchive(电子会计档案,缺口#13):6 条跨类型/状态
*
* 幂等:各表 count > 0 即跳过。
*
* @Order(13) 在 FinDeptSeeder(@Order 12) 之后运行。
*/
@Component
@Order(13)
public class FinDeptDeepSeeder implements ApplicationRunner {
private final FinEmpLoanRepository empLoanRepo;
private final FinRdPersonnelRepository personnelRepo;
private final FinRdTimesheetRepository timesheetRepo;
private final FinAccountStandardRepository stdRepo;
private final FinElecArchiveRepository archiveRepo;
public FinDeptDeepSeeder(FinEmpLoanRepository empLoanRepo,
FinRdPersonnelRepository personnelRepo,
FinRdTimesheetRepository timesheetRepo,
FinAccountStandardRepository stdRepo,
FinElecArchiveRepository archiveRepo) {
this.empLoanRepo = empLoanRepo;
this.personnelRepo = personnelRepo;
this.timesheetRepo = timesheetRepo;
this.stdRepo = stdRepo;
this.archiveRepo = archiveRepo;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
seedEmpLoans();
seedRdPersonnel();
seedRdTimesheets();
seedAccountStandards();
seedElecArchives();
}
// ===================== 员工借款 =====================
private void seedEmpLoans() {
if (empLoanRepo.count() > 0) return;
Object[][] data = {
// code, empName, empId, dept, type, purpose, loanAmt, repaidAmt, applyDate, repayDate, status
{ "EL-2026-001", "张明辉", "EMP-001", "研发部", FinEmpLoan.TYPE_TRAVEL,
"赴武汉参加供应商技术对接会议",
new BigDecimal("5000.00"), new BigDecimal("5000.00"),
"2026-01-10", "2026-02-28", FinEmpLoan.S_SETTLED },
{ "EL-2026-002", "刘晓燕", "EMP-002", "销售部", FinEmpLoan.TYPE_TRAVEL,
"出差上海客户拜访(3日)",
new BigDecimal("8500.00"), new BigDecimal("3000.00"),
"2026-02-01", "2026-03-15", FinEmpLoan.S_REPAYING },
{ "EL-2026-003", "王建国", "EMP-003", "工程部", FinEmpLoan.TYPE_PETTY,
"项目部备用金(KD-PRJ-2025-012 现场施工备用)",
new BigDecimal("20000.00"), new BigDecimal("0.00"),
"2026-01-20", "2026-04-30", FinEmpLoan.S_APPROVED },
{ "EL-2026-004", "陈秀梅", "EMP-004", "行政部", FinEmpLoan.TYPE_OTHER,
"购置办公耗材临时借款",
new BigDecimal("2000.00"), new BigDecimal("0.00"),
"2025-12-01", "2026-01-31", FinEmpLoan.S_OVERDUE },
{ "EL-2026-005", "李伟强", "EMP-005", "财务部", FinEmpLoan.TYPE_TRAVEL,
"参加上海财务数字化峰会",
new BigDecimal("6000.00"), new BigDecimal("0.00"),
"2026-03-01", "2026-04-30", FinEmpLoan.S_PENDING },
};
for (Object[] row : data) {
FinEmpLoan loan = new FinEmpLoan();
loan.setLoanCode((String) row[0]);
loan.setEmployeeName((String) row[1]);
loan.setEmployeeId((String) row[2]);
loan.setDepartment((String) row[3]);
loan.setLoanType((String) row[4]);
loan.setPurpose((String) row[5]);
BigDecimal loanAmt = (BigDecimal) row[6];
BigDecimal repaidAmt = (BigDecimal) row[7];
loan.setLoanAmount(loanAmt);
loan.setRepaidAmount(repaidAmt);
loan.setRemainAmount(loanAmt.subtract(repaidAmt));
loan.setApplyDate((String) row[8]);
loan.setExpectedRepayDate((String) row[9]);
loan.setStatus((String) row[10]);
loan.setDeductFromSalary(FinEmpLoan.S_OVERDUE.equals(row[10]));
loan.setApprover(FinEmpLoan.S_PENDING.equals(row[10]) ? null : "财务总监");
loan.setApprovalDate(FinEmpLoan.S_PENDING.equals(row[10]) ? null : "2026-01-25");
if (FinEmpLoan.S_SETTLED.equals(row[10]))
loan.setSettleDate("2026-02-20");
if (FinEmpLoan.S_OVERDUE.equals(row[10]))
loan.setOverdueDays(135);
loan.setOperator("admin");
loan.setCreatedAt(Instant.now().minus(60, ChronoUnit.DAYS));
loan.setUpdatedAt(Instant.now().minus(10, ChronoUnit.DAYS));
empLoanRepo.save(loan);
}
}
// ===================== 研发人员档案 =====================
private void seedRdPersonnel() {
if (personnelRepo.count() > 0) return;
Object[][] data = {
// code, empId, name, dept, roleType, staffType, stdHours, allocRule, projects, monthlyCost
{ "RDP-001", "EMP-010", "黄志远", "研发部", FinRdPersonnel.ROLE_CORE, FinRdPersonnel.STAFF_FULL,
176.0, "按项目实际工时占比分摊", "KD-RD-2025-001,KD-RD-2025-003",
new BigDecimal("22000.00"), "2024-01-01" },
{ "RDP-002", "EMP-011", "周美玲", "研发部", FinRdPersonnel.ROLE_CORE, FinRdPersonnel.STAFF_FULL,
176.0, "按项目实际工时占比分摊", "KD-RD-2025-002",
new BigDecimal("19500.00"), "2024-03-01" },
{ "RDP-003", "EMP-012", "吴建平", "研发部", FinRdPersonnel.ROLE_AUX, FinRdPersonnel.STAFF_FULL,
176.0, "按参与项目数均摊", "KD-RD-2025-001,KD-RD-2025-002,KD-RD-2025-003",
new BigDecimal("15000.00"), "2023-07-01" },
{ "RDP-004", "EMP-013", "赵海涛", "研发部", FinRdPersonnel.ROLE_MGR, FinRdPersonnel.STAFF_FULL,
176.0, "管理费用计入管理科目", "KD-RD-2025-001,KD-RD-2025-002",
new BigDecimal("32000.00"), "2022-01-01" },
{ "RDP-005", "OUT-001", "李天成", "外协研发", FinRdPersonnel.ROLE_CORE, FinRdPersonnel.STAFF_OUT,
120.0, "按合同约定服务小时计算", "KD-RD-2025-003",
new BigDecimal("28000.00"), "2025-06-01" },
{ "RDP-006", "EMP-014", "孙梅华", "研发部", FinRdPersonnel.ROLE_AUX, FinRdPersonnel.STAFF_PART,
80.0, "按实际参与工时分摊", "KD-RD-2025-002",
new BigDecimal("9000.00"), "2025-09-01" },
};
for (Object[] row : data) {
FinRdPersonnel p = new FinRdPersonnel();
p.setPersonnelCode((String) row[0]);
p.setEmployeeId((String) row[1]);
p.setEmployeeName((String) row[2]);
p.setDepartment((String) row[3]);
p.setRoleType((String) row[4]);
p.setStaffType((String) row[5]);
p.setMonthlyStdHours((Double) row[6]);
p.setAllocRule((String) row[7]);
p.setProjectCodes((String) row[8]);
p.setMonthlyLaborCost((BigDecimal) row[9]);
p.setRdStartDate((String) row[10]);
p.setStatus(FinRdPersonnel.S_ACTIVE);
p.setCreatedAt(Instant.now().minus(180, ChronoUnit.DAYS));
p.setUpdatedAt(Instant.now().minus(30, ChronoUnit.DAYS));
personnelRepo.save(p);
}
}
// ===================== 工时填报 =====================
private void seedRdTimesheets() {
if (timesheetRepo.count() > 0) return;
Object[][] data = {
// sheetCode, empId, empName, projectCode, fillType, fillDate, hours, confirmedHrs, allocCost, status
{ "TS-2026-001", "EMP-010", "黄志远", "KD-RD-2025-001", FinRdTimesheet.FILL_WEEKLY,
"2026-W05", 40.0, 40.0, new BigDecimal("5000.00"), FinRdTimesheet.S_APPROVED },
{ "TS-2026-002", "EMP-010", "黄志远", "KD-RD-2025-003", FinRdTimesheet.FILL_WEEKLY,
"2026-W05", 8.0, 8.0, new BigDecimal("1000.00"), FinRdTimesheet.S_APPROVED },
{ "TS-2026-003", "EMP-011", "周美玲", "KD-RD-2025-002", FinRdTimesheet.FILL_WEEKLY,
"2026-W05", 44.0, 40.0, new BigDecimal("4432.00"), FinRdTimesheet.S_APPROVED },
{ "TS-2026-004", "EMP-012", "吴建平", "KD-RD-2025-001", FinRdTimesheet.FILL_DAILY,
"2026-02-24", 8.0, 8.0, new BigDecimal("682.00"), FinRdTimesheet.S_APPROVED },
{ "TS-2026-005", "OUT-001", "李天成", "KD-RD-2025-003", FinRdTimesheet.FILL_WEEKLY,
"2026-W06", 32.0, null, new BigDecimal("0.00"), FinRdTimesheet.S_PENDING },
{ "TS-2026-006", "EMP-014", "孙梅华", "KD-RD-2025-002", FinRdTimesheet.FILL_WEEKLY,
"2026-W06", 20.0, null, new BigDecimal("0.00"), FinRdTimesheet.S_PENDING },
{ "TS-2026-007", "EMP-011", "周美玲", "KD-RD-2025-002", FinRdTimesheet.FILL_DAILY,
"2026-03-10", 6.0, null, new BigDecimal("0.00"), FinRdTimesheet.S_DRAFT },
{ "TS-2026-008", "EMP-010", "黄志远", "KD-RD-2025-001", FinRdTimesheet.FILL_DAILY,
"2026-02-10", 9.0, 6.0, new BigDecimal("750.00"), FinRdTimesheet.S_REJECTED },
};
for (Object[] row : data) {
FinRdTimesheet s = new FinRdTimesheet();
s.setSheetCode((String) row[0]);
s.setEmployeeId((String) row[1]);
s.setEmployeeName((String) row[2]);
s.setProjectCode((String) row[3]);
s.setProjectName("KD研发项目-" + ((String) row[3]).substring(((String) row[3]).lastIndexOf('-') + 1));
s.setFillType((String) row[4]);
s.setFillDate((String) row[5]);
s.setReportedHours((Double) row[6]);
s.setConfirmedHours((Double) row[7]);
s.setAllocatedLaborCost((BigDecimal) row[8]);
s.setStatus((String) row[9]);
s.setExceedsLegalLimit(Boolean.FALSE);
if (FinRdTimesheet.S_APPROVED.equals(row[9]))
s.setReviewer("赵海涛");
if (FinRdTimesheet.S_REJECTED.equals(row[9])) {
s.setReviewer("赵海涛");
s.setRejectReason("单日填报工时9小时超出常规,请提供加班审批记录");
}
s.setCreatedAt(Instant.now().minus(30, ChronoUnit.DAYS));
s.setUpdatedAt(Instant.now().minus(5, ChronoUnit.DAYS));
timesheetRepo.save(s);
}
}
// ===================== 多会计准则配置 =====================
private void seedAccountStandards() {
if (stdRepo.count() > 0) return;
FinAccountStandard cas = new FinAccountStandard();
cas.setStandardCode(FinAccountStandard.STD_CAS);
cas.setStandardName("中国企业会计准则");
cas.setStandardVersion("2019修订版");
cas.setEffectiveDate("2019-01-01");
cas.setIsPrimary(Boolean.TRUE);
cas.setReportCurrency("CNY");
cas.setStatus(FinAccountStandard.S_ACTIVE);
cas.setAccountMappingNotes(
"CAS科目体系:1001现金/1002银行存款/1122应收账款/6001主营业务收入/6401主营业务成本。" +
"与IFRS差异:无IFRS16使用权资产单独科目(CAS合并于固定资产);" +
"金融工具分类三类vs IFRS四类;研发费用资本化条件更严格。");
cas.setConfiguredBy("admin");
cas.setRemark("凯迪科技主用会计准则,符合中国证监会及工商局年报要求");
cas.setCreatedAt(Instant.now().minus(365, ChronoUnit.DAYS));
cas.setUpdatedAt(Instant.now().minus(30, ChronoUnit.DAYS));
stdRepo.save(cas);
FinAccountStandard ifrs = new FinAccountStandard();
ifrs.setStandardCode(FinAccountStandard.STD_IFRS);
ifrs.setStandardName("国际财务报告准则");
ifrs.setStandardVersion("IFRS 2023");
ifrs.setEffectiveDate("2023-01-01");
ifrs.setIsPrimary(Boolean.FALSE);
ifrs.setReportCurrency("USD");
ifrs.setStatus(FinAccountStandard.S_ACTIVE);
ifrs.setAccountMappingNotes(
"IFRS主要差异:IFRS16租赁准则要求确认使用权资产(ROU)和租赁负债;" +
"IFRS9金融工具分四类:摊余成本/FVOCI/FVTPL/特殊类别;" +
"IFRS15收入确认以履约义务为基础;" +
"预期信用损失(ECL)模型用于金融资产减值(替代已发生损失模型)。");
ifrs.setConfiguredBy("admin");
ifrs.setRemark("备用,用于集团跨境合并报表及境外融资报告");
ifrs.setCreatedAt(Instant.now().minus(180, ChronoUnit.DAYS));
ifrs.setUpdatedAt(Instant.now().minus(90, ChronoUnit.DAYS));
stdRepo.save(ifrs);
}
// ===================== 电子会计档案 =====================
private void seedElecArchives() {
if (archiveRepo.count() > 0) return;
Object[][] data = {
// code, type, name, period, format, hash, retentionYrs, status
{ "EA-2025-001", FinElecArchive.TYPE_VOUCHER, "2025年12月记账凭证汇总",
"2025-12", "PDF", "a1b2c3d4e5f6789012345678901234567890abcd1234567890abcd1234567890",
30, FinElecArchive.S_BACKED },
{ "EA-2025-002", FinElecArchive.TYPE_LEDGER, "2025年12月明细账汇总",
"2025-12", "XLSX", "b2c3d4e5f6789012345678901234567890abcd1234567890abcd1234567891",
30, FinElecArchive.S_ARCHIVED },
{ "EA-2025-003", FinElecArchive.TYPE_REPORT, "2025年年度财务报表(三表)",
"2025", "PDF", "c3d4e5f6789012345678901234567890abcd1234567890abcd12345678920a",
30, FinElecArchive.S_BACKED },
{ "EA-2026-001", FinElecArchive.TYPE_INVOICE, "2026年1月进项发票台账",
"2026-01", "XLSX", "d4e5f6789012345678901234567890abcd1234567890abcd1234567890a1b2",
10, FinElecArchive.S_ARCHIVED },
{ "EA-2026-002", FinElecArchive.TYPE_BANK_REC, "2026年1月银行回单汇总",
"2026-01", "PDF", "e5f6789012345678901234567890abcd1234567890abcd1234567890a1b2c3",
10, FinElecArchive.S_ARCHIVED },
{ "EA-2026-003", FinElecArchive.TYPE_VOUCHER, "2026年1月记账凭证",
"2026-01", "PDF", null,
30, FinElecArchive.S_PENDING },
};
for (Object[] row : data) {
FinElecArchive a = new FinElecArchive();
a.setArchiveCode((String) row[0]);
a.setArchiveType((String) row[1]);
a.setArchiveName((String) row[2]);
a.setPeriod((String) row[3]);
a.setFileFormat((String) row[4]);
a.setSha256Hash((String) row[5]);
a.setHashVerified(row[5] != null && !FinElecArchive.S_PENDING.equals(row[7]));
a.setRetentionYears((Integer) row[6]);
a.setStatus((String) row[7]);
a.setExportable(Boolean.TRUE);
a.setFileSizeBytes(1024L * 1024 * (2 + (long)(Math.random() * 8)));
if (!FinElecArchive.S_PENDING.equals(row[7])) {
a.setArchiveDate("2026-01-05");
a.setArchivedBy("李伟强");
}
if (FinElecArchive.S_BACKED.equals(row[7])) {
a.setBackupPaths("/backup/oa/archives/" + row[0] + ".zip");
a.setBackedUpBy("系统自动备份");
}
a.setCreatedAt(Instant.now().minus(45, ChronoUnit.DAYS));
a.setUpdatedAt(Instant.now().minus(10, ChronoUnit.DAYS));
archiveRepo.save(a);
}
}
}
@@ -0,0 +1,201 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.ArApItem;
import com.kaidi.oa.domain.TaxFiling;
import com.kaidi.oa.repository.ArApItemRepository;
import com.kaidi.oa.repository.TaxFilingRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
/**
* 财务部缺口种子补充(W8批次)。
*
* 补全:
* ① TaxFiling:补充 8 条多税种/多状态(报表附注 §12 "应交税费明细" 需多条数据演示)。
* ② ArApItem:补充 12 条带 [CERT:*] 发票认证状态标记的应付款(§3 进项税抵扣台账演示)。
*
* 幂等:各表通过 code 前缀判断跳过。
*
* @Order(215) 在既有 FinDeptSeeder(@Order 12)、BranchFinDemoSeeder(@Order 15) 之后。
*/
@Component
@Order(215)
public class FinDeptGapSeeder implements ApplicationRunner {
private final TaxFilingRepository taxRepo;
private final ArApItemRepository arApRepo;
public FinDeptGapSeeder(TaxFilingRepository taxRepo,
ArApItemRepository arApRepo) {
this.taxRepo = taxRepo;
this.arApRepo = arApRepo;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
seedTaxFilings();
seedApWithInvoiceCert();
}
// ===================== TaxFiling 多税种演示 =====================
private void seedTaxFilings() {
// 幂等:若已有超过2条则跳过
if (taxRepo.count() > 2) return;
Object[][] data = {
// code, taxType, period, taxBase, taxRate, taxAmount, status, filingDate, payDate, remark
{ "TAX-VAT-2026-04", "增值税", "2026-04",
new BigDecimal("1820000.00"), 13.0, new BigDecimal("236600.00"),
TaxFiling.S_PAID, "2026-04-15", "2026-04-20",
"2026年4月增值税主申报 | 销项税额265460-进项税额28860=应纳236600" },
{ "TAX-VAT-2026-05", "增值税", "2026-05",
new BigDecimal("2105000.00"), 13.0, new BigDecimal("273650.00"),
TaxFiling.S_FILED, "2026-05-15", null,
"2026年5月增值税申报(已申报待缴款)" },
{ "TAX-VAT-2026-06", "增值税", "2026-06",
new BigDecimal("1680000.00"), 13.0, new BigDecimal("218400.00"),
TaxFiling.S_PENDING, null, null,
"2026年6月增值税(待申报)" },
{ "TAX-CIT-2026-Q1", "企业所得税", "2026-Q1",
new BigDecimal("4200000.00"), 15.0, new BigDecimal("630000.00"),
TaxFiling.S_PAID, "2026-04-25", "2026-04-28",
"2026Q1企业所得税季报(高新技术企业15%税率)" },
{ "TAX-CIT-2026-Q2", "企业所得税", "2026-Q2",
new BigDecimal("3650000.00"), 15.0, new BigDecimal("547500.00"),
TaxFiling.S_PENDING, null, null,
"2026Q2企业所得税季报(待申报)" },
{ "TAX-STAMP-2026-H1", "印花税", "2026-H1",
new BigDecimal("120000000.00"), 0.03, new BigDecimal("36000.00"),
TaxFiling.S_PAID, "2026-06-30", "2026-06-30",
"2026上半年购销合同印花税(税率0.03‰×合同额)" },
{ "TAX-URB-2026-04", "城市维护建设税", "2026-04",
new BigDecimal("236600.00"), 7.0, new BigDecimal("16562.00"),
TaxFiling.S_PAID, "2026-04-15", "2026-04-20",
"2026年4月城建税(增值税236600×7%)" },
{ "TAX-EDUC-2026-04", "教育费附加", "2026-04",
new BigDecimal("236600.00"), 3.0, new BigDecimal("7098.00"),
TaxFiling.S_PAID, "2026-04-15", "2026-04-20",
"2026年4月教育费附加(增值税236600×3%)" },
};
for (Object[] row : data) {
String code = (String) row[0];
boolean exists = taxRepo.findAll().stream().anyMatch(t -> code.equals(t.getCode()));
if (exists) continue;
TaxFiling t = new TaxFiling();
t.setCode(code);
t.setTaxType((String) row[1]);
t.setPeriod((String) row[2]);
t.setTaxBase((BigDecimal) row[3]);
t.setTaxRate((Double) row[4]);
t.setTaxAmount((BigDecimal) row[5]);
t.setStatus((String) row[6]);
t.setFilingDate((String) row[7]);
t.setPayDate((String) row[8]);
t.setRemark((String) row[9]);
t.setCompanySubject("凯迪科技股份有限公司");
t.setOwner("税务会计");
taxRepo.save(t);
}
}
// ===================== 带发票认证状态的应付单 =====================
private void seedApWithInvoiceCert() {
// 幂等:若已有 AP-CERT 前缀的记录则跳过
boolean certExists = arApRepo.findAll().stream()
.anyMatch(i -> i.getCode() != null && i.getCode().startsWith("AP-CERT-"));
if (certExists) return;
Object[][] data = {
// code, party, ref, amount, dueDate, certStatus, remark
{ "AP-CERT-001", "武汉凯博机械有限公司", "INV-2026-0401", new BigDecimal("182000.00"),
"2026-05-31", "已认证",
"[CERT:已认证] 增值税专用发票 INV-2026-0401 已通过税局认证,进项税额23660元可抵扣" },
{ "AP-CERT-002", "湖南振华电气设备公司", "INV-2026-0402", new BigDecimal("95600.00"),
"2026-06-15", "已抵扣",
"[CERT:已抵扣] INV-2026-0402 进项税额12428元已在4月增值税申报中抵扣" },
{ "AP-CERT-003", "长沙天宇科技有限公司", "INV-2026-0415", new BigDecimal("268000.00"),
"2026-06-30", "已认证",
"[CERT:已认证] INV-2026-0415 增值税专票,进项税额34840元已认证待抵扣" },
{ "AP-CERT-004", "岳阳联盛原材料公司", "INV-2026-0420", new BigDecimal("135000.00"),
"2026-05-20", "未认证",
"[CERT:未认证] INV-2026-0420 普通发票(非专票),不可抵扣进项税" },
{ "AP-CERT-005", "广州正泰电气集团", "INV-2026-0428", new BigDecimal("412000.00"),
"2026-07-15", "未认证",
"[CERT:未认证] INV-2026-0428 专票已收到,尚未提交税局认证(请务必在当月认证截止前提交)" },
{ "AP-CERT-006", "深圳鑫泰电子元器件", "INV-2026-0501", new BigDecimal("88500.00"),
"2026-06-30", "已认证",
"[CERT:已认证] INV-2026-0501 电子增值税专用发票,进项税额11505元已认证" },
{ "AP-CERT-007", "成都博远机电制造", "INV-2026-0505", new BigDecimal("326000.00"),
"2026-07-31", "未认证",
"[CERT:未认证] INV-2026-0505 金额较大,专票正在邮寄途中,待收票后认证" },
{ "AP-CERT-008", "北京华控仪器仪表", "INV-2026-0512", new BigDecimal("67800.00"),
"2026-06-20", "已抵扣",
"[CERT:已抵扣] INV-2026-0512 进项税额8814元已在5月增值税申报中抵扣" },
{ "AP-CERT-009", "济南钢铁集团贸易", "INV-2026-0515", new BigDecimal("520000.00"),
"2026-08-15", "已认证",
"[CERT:已认证] INV-2026-0515 大额原材料采购专票,进项税额67600元,已认证待下月抵扣" },
{ "AP-CERT-010", "上海绿洲环保科技", "INV-2026-0520", new BigDecimal("42000.00"),
"2026-07-01", "未认证",
"[CERT:未认证] INV-2026-0520 服务费增值税专票(6%税率),待认证" },
{ "AP-CERT-011", "天津港航物流集团", "INV-2026-0522", new BigDecimal("185000.00"),
"2026-07-31", "已认证",
"[CERT:已认证] INV-2026-0522 运输服务9%增值税,进项税额16606元已认证" },
{ "AP-CERT-012", "武汉宏达建设集团", "INV-2026-0525", new BigDecimal("1250000.00"),
"2026-09-30", "未认证",
"[CERT:未认证] INV-2026-0525 工程服务9%增值税,金额较大,需财务总监签批后认证" },
};
for (Object[] row : data) {
ArApItem item = new ArApItem();
item.setCode((String) row[0]);
item.setArApType(ArApItem.T_AP);
item.setPartyName((String) row[1]);
item.setRelatedRef((String) row[2]);
BigDecimal amt = (BigDecimal) row[3];
// 已抵扣的标记为已结清(已核销),其余未核销
String certStatus = (String) row[5];
boolean settled = "已抵扣".equals(certStatus);
item.setAmount(amt);
item.setWrittenOff(settled ? amt : BigDecimal.ZERO);
item.setUnwrittenOff(settled ? BigDecimal.ZERO : amt);
item.setStatus(settled ? ArApItem.S_SETTLED : ArApItem.S_OPEN);
item.setDueDate((String) row[4]);
item.setRemark((String) row[6]);
item.setCompanySubject("凯迪科技股份有限公司");
item.setOwner("应付会计");
item.setCreatedAt(Instant.now().minus(15 + (long)(Math.random() * 30), ChronoUnit.DAYS));
arApRepo.save(item);
}
}
}
@@ -0,0 +1,250 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.FinAccountAux;
import com.kaidi.oa.domain.FinBudgetExecution;
import com.kaidi.oa.domain.FinBudgetPlan;
import com.kaidi.oa.domain.FinCostCalc;
import com.kaidi.oa.domain.FinVoucherTemplate;
import com.kaidi.oa.repository.FinAccountAuxRepository;
import com.kaidi.oa.repository.FinBudgetExecutionRepository;
import com.kaidi.oa.repository.FinBudgetPlanRepository;
import com.kaidi.oa.repository.FinCostCalcRepository;
import com.kaidi.oa.repository.FinVoucherTemplateRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
/**
* 财务部演示种子数据(缺口 #1/#8/#11 — 活体 0 条)。
*
* 补齐:
* - FinAccountAux(辅助核算配置,缺口#1):6 条跨维度配置 + 余额
* - FinVoucherTemplate(凭证模板,缺口#1):7 条常用模板(各类型各一条)
* - FinCostCalc(成本核算记录,缺口#8):6 条跨方法/状态核算记录
* - FinBudgetPlan(预算编制计划,缺口#11):5 条跨部门/类型预算申报
* - FinBudgetExecution(预算执行,缺口#11):5 条已批准后生成的执行记录
*
* 幂等:各表 count > 0 即跳过。
*
* @Order(12) 在 DataSeeder(@Order 1) 之后运行。
*/
@Component
@Order(12)
public class FinDeptSeeder implements ApplicationRunner {
private final FinAccountAuxRepository auxRepo;
private final FinVoucherTemplateRepository templateRepo;
private final FinCostCalcRepository costCalcRepo;
private final FinBudgetPlanRepository budgetPlanRepo;
private final FinBudgetExecutionRepository budgetExecRepo;
public FinDeptSeeder(FinAccountAuxRepository auxRepo,
FinVoucherTemplateRepository templateRepo,
FinCostCalcRepository costCalcRepo,
FinBudgetPlanRepository budgetPlanRepo,
FinBudgetExecutionRepository budgetExecRepo) {
this.auxRepo = auxRepo;
this.templateRepo = templateRepo;
this.costCalcRepo = costCalcRepo;
this.budgetPlanRepo = budgetPlanRepo;
this.budgetExecRepo = budgetExecRepo;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
seedAccountAux();
seedVoucherTemplates();
seedCostCalc();
seedBudgetPlan();
seedBudgetExecution();
}
// ===================== 辅助核算配置 =====================
private void seedAccountAux() {
if (auxRepo.count() > 0) return;
Object[][] data = {
{ "1122", "应收账款", "客户", "凯迪新能源科技有限公司", "2026-01", new BigDecimal("856300.00") },
{ "1122", "应收账款", "客户", "汉阳市政建设集团", "2026-01", new BigDecimal("432100.00") },
{ "6602", "管理费用", "部门", "研发部", "2026-01", new BigDecimal("123450.00") },
{ "6602", "管理费用", "部门", "生产部", "2026-01", new BigDecimal("98760.00") },
{ "6401", "主营业务成本", "项目", "KD-2025-001 污水处理一期", "2026-01", new BigDecimal("2156800.00") },
{ "6401", "主营业务成本", "成本中心", "车间A-焊接工段", "2026-01", new BigDecimal("78900.00") },
};
for (Object[] row : data) {
FinAccountAux a = new FinAccountAux();
a.setAccountCode((String) row[0]);
a.setAccountName((String) row[1]);
a.setAuxDimension((String) row[2]);
a.setAuxVal((String) row[3]);
a.setEnabledVal(FinAccountAux.STATUS_ENABLED);
a.setBalance((BigDecimal) row[5]);
a.setApplicant("admin");
a.setReason("初始化辅助核算配置");
a.setCreatedAt(Instant.now().minus(30, ChronoUnit.DAYS));
a.setUpdatedAt(Instant.now().minus(1, ChronoUnit.DAYS));
auxRepo.save(a);
}
}
// ===================== 凭证模板 =====================
private void seedVoucherTemplates() {
if (templateRepo.count() > 0) return;
Object[][] data = {
{ "TMPL-001", "月度折旧计提", FinVoucherTemplate.TYPE_DEPREC, "6602", "1602", "计提${period}固定资产折旧", "折旧计算" },
{ "TMPL-002", "无形资产摊销", FinVoucherTemplate.TYPE_AMORT, "6602", "1702", "摊销${period}无形资产", "摊销计算" },
{ "TMPL-003", "月末结转损益", FinVoucherTemplate.TYPE_TRANSFER, "6001", "4103", "${period}结转主营业务收入至本年利润", "手填" },
{ "TMPL-004", "结转研发费用", FinVoucherTemplate.TYPE_RD, "6602", "5301", "${period}研发费用化支出结转管理费", "手填" },
{ "TMPL-005", "制造费用按工时分摊", FinVoucherTemplate.TYPE_MFG, "1321", "6601", "${period}制造费用分摊至在制品", "手填" },
{ "TMPL-006", "期末汇率折算调整", FinVoucherTemplate.TYPE_FX, "6601", "2202", "${period}外币汇兑损益调整", "手填" },
{ "TMPL-007", "备用金借款发放", FinVoucherTemplate.TYPE_MANUAL, "1001", "1002", "发放${period}员工备用金借款", "手填" },
};
for (Object[] row : data) {
FinVoucherTemplate t = new FinVoucherTemplate();
t.setCode((String) row[0]);
t.setName((String) row[1]);
t.setTemplateType((String) row[2]);
t.setDebitAccount((String) row[3]);
t.setCreditAccount((String) row[4]);
t.setSummaryTemplate((String) row[5]);
t.setAmountSource((String) row[6]);
t.setEnabled(Boolean.TRUE);
t.setCreatedBy("admin");
t.setCreatedAt(Instant.now().minus(60, ChronoUnit.DAYS));
templateRepo.save(t);
}
}
// ===================== 成本核算记录 =====================
private void seedCostCalc() {
if (costCalcRepo.count() > 0) return;
Object[][] data = {
// code, method, object, objType, period, dm, dl, mo, driver, drvQty, rate, std, status
{ "CC-2026-001", FinCostCalc.METHOD_VARIETY, "KD-A01 污水处理设备", "产品", "2026-01",
new BigDecimal("158000.00"), new BigDecimal("43200.00"), new BigDecimal("22800.00"),
"工时", 480.0, new BigDecimal("47.50"), new BigDecimal("210000.00"), FinCostCalc.STATUS_POSTED },
{ "CC-2026-002", FinCostCalc.METHOD_BATCH, "KD-WO-2026-003 生产批次", "工单", "2026-01",
new BigDecimal("89600.00"), new BigDecimal("18900.00"), new BigDecimal("11200.00"),
"产量", 120.0, new BigDecimal("93.33"), new BigDecimal("115000.00"), FinCostCalc.STATUS_APPROVED },
{ "CC-2026-003", FinCostCalc.METHOD_PROCESS, "污水处理剂-第一步骤", "产品", "2026-01",
new BigDecimal("62300.00"), new BigDecimal("12100.00"), new BigDecimal("8600.00"),
"工时", 200.0, new BigDecimal("43.00"), new BigDecimal("80000.00"), FinCostCalc.STATUS_APPROVED },
{ "CC-2026-004", FinCostCalc.METHOD_ABC, "KD-PRJ-2025-012 污水项目", "项目", "2026-01",
new BigDecimal("310500.00"), new BigDecimal("87600.00"), new BigDecimal("48300.00"),
"工时", 960.0, new BigDecimal("50.31"), new BigDecimal("430000.00"), FinCostCalc.STATUS_POSTED },
{ "CC-2026-005", FinCostCalc.METHOD_VARIETY, "KD-B02 曝气设备", "产品", "2025-12",
new BigDecimal("241000.00"), new BigDecimal("55000.00"), new BigDecimal("31000.00"),
"产量", 60.0, new BigDecimal("516.67"), new BigDecimal("320000.00"), FinCostCalc.STATUS_POSTED },
{ "CC-2026-006", FinCostCalc.METHOD_BATCH, "KD-ORD-2026-008 订单成本","订单","2026-02",
new BigDecimal("124800.00"), new BigDecimal("32000.00"), new BigDecimal("19200.00"),
"收入", 0.0, new BigDecimal("0.00"), new BigDecimal("170000.00"), FinCostCalc.STATUS_DRAFT },
};
for (Object[] row : data) {
FinCostCalc c = new FinCostCalc();
c.setCode((String) row[0]);
c.setCalcMethod((String) row[1]);
c.setCostObject((String) row[2]);
c.setCostObjectType((String) row[3]);
c.setPeriod((String) row[4]);
c.setDirectMaterial((BigDecimal) row[5]);
c.setDirectLabor((BigDecimal) row[6]);
c.setManufacturingOverhead((BigDecimal) row[7]);
c.setAllocationDriver((String) row[8]);
c.setDriverQuantity((Double) row[9]);
c.setAllocationRate((BigDecimal) row[10]);
BigDecimal total = ((BigDecimal) row[5]).add((BigDecimal) row[6]).add((BigDecimal) row[7]);
c.setTotalCost(total);
c.setStandardCost((BigDecimal) row[11]);
c.setCostVariance(total.subtract((BigDecimal) row[11]));
c.setStatus((String) row[12]);
c.setOperator("admin");
c.setCreatedAt(Instant.now().minus(20, ChronoUnit.DAYS));
costCalcRepo.save(c);
}
}
// ===================== 预算编制计划 =====================
private void seedBudgetPlan() {
if (budgetPlanRepo.count() > 0) return;
Object[][] data = {
{ "BP-2026-001", "2026", 1, "研发部", "费用预算", "研发人员人工费用", new BigDecimal("2400000.00"), new BigDecimal("2200000.00"), FinBudgetPlan.S_APPROVED },
{ "BP-2026-002", "2026", 1, "生产部", "成本预算", "直接材料采购成本", new BigDecimal("8600000.00"), new BigDecimal("8100000.00"), FinBudgetPlan.S_APPROVED },
{ "BP-2026-003", "2026", 1, "行政部", "费用预算", "办公及行政费用", new BigDecimal("380000.00"), new BigDecimal("360000.00"), FinBudgetPlan.S_APPROVED },
{ "BP-2026-004", "2026", 1, "销售部", "收入预算", "主营业务收入", new BigDecimal("42000000.00"),new BigDecimal("40000000.00"),FinBudgetPlan.S_APPROVED },
{ "BP-2026-005", "2026", 1, "财务部", "资本预算", "固定资产购置(设备)",new BigDecimal("1500000.00"),new BigDecimal("1200000.00"), FinBudgetPlan.S_GATHERING },
{ "BP-2026-006", "2026", 1, "工程部", "现金流预算", "项目建设投资现金流", new BigDecimal("12000000.00"),new BigDecimal("0.00"), FinBudgetPlan.S_SUBMITTED },
};
for (Object[] row : data) {
FinBudgetPlan p = new FinBudgetPlan();
p.setPlanCode((String) row[0]);
p.setBudgetYear((String) row[1]);
p.setRoundNo((Integer) row[2]);
p.setDepartment((String) row[3]);
p.setBudgetType((String) row[4]);
p.setBudgetItem((String) row[5]);
p.setProposedAmount((BigDecimal) row[6]);
p.setApprovedAmount((BigDecimal) row[7]);
p.setVarianceFromTop(((BigDecimal) row[6]).subtract((BigDecimal) row[7]));
p.setStatus((String) row[8]);
p.setSubmitter("部门负责人");
p.setApprover(FinBudgetPlan.S_APPROVED.equals(row[8]) ? "财务总监" : null);
p.setSubmitDate("2026-01-15");
p.setApprovalDate(FinBudgetPlan.S_APPROVED.equals(row[8]) ? "2026-02-01" : null);
p.setJustification("年度预算申报,依据上年实际执行情况及本年业务目标编制");
p.setCreatedAt(Instant.now().minus(90, ChronoUnit.DAYS));
p.setUpdatedAt(Instant.now().minus(60, ChronoUnit.DAYS));
budgetPlanRepo.save(p);
}
}
// ===================== 预算执行记录 =====================
private void seedBudgetExecution() {
if (budgetExecRepo.count() > 0) return;
Object[][] data = {
// budgetItem, type, period, budgetAmt, actualAmt, dept
{ "研发人员人工费用", "费用预算", "2026", new BigDecimal("2200000.00"), new BigDecimal("386500.00"), "研发部" },
{ "直接材料采购成本", "成本预算", "2026", new BigDecimal("8100000.00"), new BigDecimal("1623000.00"), "生产部" },
{ "办公及行政费用", "费用预算", "2026", new BigDecimal("360000.00"), new BigDecimal("62400.00"), "行政部" },
{ "主营业务收入", "收入预算", "2026", new BigDecimal("40000000.00"),new BigDecimal("6830000.00"), "销售部" },
{ "固定资产购置(设备)", "资本预算", "2026", new BigDecimal("1200000.00"), new BigDecimal("0.00"), "财务部" },
};
for (Object[] row : data) {
FinBudgetExecution e = new FinBudgetExecution();
e.setBudgetItem((String) row[0]);
e.setBudgetType((String) row[1]);
e.setPeriod((String) row[2]);
e.setBudgetAmount((BigDecimal) row[3]);
e.setActualAmount((BigDecimal) row[4]);
BigDecimal variance = ((BigDecimal) row[4]).subtract((BigDecimal) row[3]);
e.setVarianceAmount(variance);
e.setVarianceRate(((BigDecimal) row[3]).signum() > 0
? ((BigDecimal) row[4]).divide((BigDecimal) row[3], 4, java.math.RoundingMode.HALF_UP)
.multiply(BigDecimal.valueOf(100)).setScale(2, java.math.RoundingMode.HALF_UP)
: BigDecimal.ZERO);
e.setOverBudget(variance.signum() > 0);
e.setAlerted(variance.signum() > 0);
e.setAlertThreshold(BigDecimal.TEN);
e.setDepartment((String) row[5]);
e.setBudgetVersion("批准版");
e.setOperator("admin");
budgetExecRepo.save(e);
}
}
}
@@ -0,0 +1,541 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.FinAccountAux;
import com.kaidi.oa.domain.FinBudgetExecution;
import com.kaidi.oa.domain.FinBudgetPlan;
import com.kaidi.oa.domain.FinCostCalc;
import com.kaidi.oa.domain.FinEmpLoan;
import com.kaidi.oa.domain.FinVoucherTemplate;
import com.kaidi.oa.repository.FinAccountAuxRepository;
import com.kaidi.oa.repository.FinBudgetExecutionRepository;
import com.kaidi.oa.repository.FinBudgetPlanRepository;
import com.kaidi.oa.repository.FinCostCalcRepository;
import com.kaidi.oa.repository.FinEmpLoanRepository;
import com.kaidi.oa.repository.FinVoucherTemplateRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
/**
* 支付中心/结算中心演示数据种子(W6 缺口补完)。
*
* 补全审计缺口:
* [gap-9] 会计科目辅助核算配置活体为空(0条) → 补 FinAccountAux 演示数据(6维 × 5科目)。
* [gap-10] 成本核算活体记录为空(0条) → 补 FinCostCalc 演示数据(多方法/多期间)。
* [gap-11] 活体预算计划和执行记录均为0条 → 补 FinBudgetPlan + FinBudgetExecution。
* [gap-voucher] 凭证模板活体为空(0条) → 补 FinVoucherTemplate 常用模板。
* [gap-emp-loan] 员工借款缺演示数据 → 补 FinEmpLoan 多状态演示记录。
*
* 幂等:各表 count > 0 则跳过。
*
* @Order(20) 在 DataSeeder(@Order 1) 和 BranchFinDemoSeeder(@Order 15) 之后运行。
*/
@Component
@Order(20)
public class FinPaymentDemoSeeder implements ApplicationRunner {
private final FinVoucherTemplateRepository voucherTemplateRepo;
private final FinAccountAuxRepository accountAuxRepo;
private final FinBudgetPlanRepository budgetPlanRepo;
private final FinBudgetExecutionRepository budgetExecRepo;
private final FinCostCalcRepository costCalcRepo;
private final FinEmpLoanRepository empLoanRepo;
public FinPaymentDemoSeeder(FinVoucherTemplateRepository voucherTemplateRepo,
FinAccountAuxRepository accountAuxRepo,
FinBudgetPlanRepository budgetPlanRepo,
FinBudgetExecutionRepository budgetExecRepo,
FinCostCalcRepository costCalcRepo,
FinEmpLoanRepository empLoanRepo) {
this.voucherTemplateRepo = voucherTemplateRepo;
this.accountAuxRepo = accountAuxRepo;
this.budgetPlanRepo = budgetPlanRepo;
this.budgetExecRepo = budgetExecRepo;
this.costCalcRepo = costCalcRepo;
this.empLoanRepo = empLoanRepo;
}
@Override
public void run(ApplicationArguments args) {
seedVoucherTemplates();
seedAccountAux();
seedBudgetPlans();
seedBudgetExecutions();
seedCostCalcRecords();
seedEmpLoans();
}
// ============================================================
// 1. 凭证模板(gap-voucher: 0条)
// ============================================================
private void seedVoucherTemplates() {
if (voucherTemplateRepo.count() > 0) return;
List<FinVoucherTemplate> list = new ArrayList<>();
list.add(template("TMPL-001", "计提折旧-设备", FinVoucherTemplate.TYPE_DEPREC,
"5011.001 制造费用-折旧", "1602 累计折旧",
"${period}期计提固定资产折旧", "折旧计算", Boolean.TRUE, "admin"));
list.add(template("TMPL-002", "计提折旧-房屋", FinVoucherTemplate.TYPE_DEPREC,
"6602 管理费用-折旧", "1602 累计折旧",
"${period}期计提房屋折旧", "折旧计算", Boolean.TRUE, "admin"));
list.add(template("TMPL-003", "摊销无形资产", FinVoucherTemplate.TYPE_AMORT,
"6602 管理费用-摊销", "1702 累计摊销",
"${period}期摊销无形资产", "摊销计算", Boolean.TRUE, "admin"));
list.add(template("TMPL-004", "结转损益", FinVoucherTemplate.TYPE_TRANSFER,
"4103 本年利润", "5001 主营业务收入",
"${period}期结转主营业务收入", "手填", Boolean.TRUE, "admin"));
list.add(template("TMPL-005", "结转研发费用", FinVoucherTemplate.TYPE_RD,
"1503 开发支出", "5011 研发费用",
"${period}期研发费用资本化结转", "手填", Boolean.TRUE, "admin"));
list.add(template("TMPL-006", "分摊制造费用-工时", FinVoucherTemplate.TYPE_MFG,
"1405 库存商品-半成品", "5011 制造费用",
"${period}期按工时分摊制造费用", "手填", Boolean.TRUE, "admin"));
list.add(template("TMPL-007", "期末调汇-外币账户", FinVoucherTemplate.TYPE_FX,
"1122 应收账款-外币", "6301 汇兑损益",
"${period}期末外币调汇(应收账款)", "手填", Boolean.TRUE, "admin"));
list.add(template("TMPL-008", "付款核销手工模板", FinVoucherTemplate.TYPE_MANUAL,
"2202 应付账款", "1002 银行存款",
"采购款付款:${amount}", "手填", Boolean.TRUE, "admin"));
list.add(template("TMPL-009", "收款核销手工模板", FinVoucherTemplate.TYPE_MANUAL,
"1002 银行存款", "1122 应收账款",
"销售回款:${amount}", "手填", Boolean.TRUE, "admin"));
list.add(template("TMPL-010", "计提工资-生产", FinVoucherTemplate.TYPE_MANUAL,
"5011.002 制造费用-人工", "2211 应付职工薪酬",
"${period}期计提生产人员工资", "手填", Boolean.TRUE, "admin"));
voucherTemplateRepo.saveAll(list);
}
private FinVoucherTemplate template(String code, String name, String type,
String debit, String credit, String summary,
String amountSource, Boolean enabled, String createdBy) {
FinVoucherTemplate t = new FinVoucherTemplate();
t.setCode(code);
t.setName(name);
t.setTemplateType(type);
t.setDebitAccount(debit);
t.setCreditAccount(credit);
t.setSummaryTemplate(summary);
t.setAmountSource(amountSource);
t.setEnabled(enabled);
t.setCreatedBy(createdBy);
t.setCreatedAt(Instant.now());
return t;
}
// ============================================================
// 2. 会计科目辅助核算配置(gap-9: 0条)
// ============================================================
private void seedAccountAux() {
if (accountAuxRepo.count() > 0) return;
List<FinAccountAux> list = new ArrayList<>();
// 1122 应收账款 - 客户维度
list.add(aux("1122", "应收账款", "客户", "岳阳城陵矶市政开发公司",
"启用", "财务部", "admin", "按客户追踪应收账款余额"));
list.add(aux("1122", "应收账款", "客户", "湖南省水务建设集团",
"启用", "财务部", "admin", "按客户追踪应收账款余额"));
list.add(aux("1122", "应收账款", "客户", "岳阳市凯利环保科技",
"启用", "财务部", "admin", "按客户追踪应收账款余额"));
// 2202 应付账款 - 供应商维度
list.add(aux("2202", "应付账款", "供应商", "长沙美迪机械有限公司",
"启用", "财务部", "admin", "按供应商追踪应付账款余额"));
list.add(aux("2202", "应付账款", "供应商", "岳阳德信建材供应商",
"启用", "财务部", "admin", "按供应商追踪应付账款余额"));
// 5011 制造费用 - 部门维度
list.add(aux("5011", "制造费用", "部门", "环保设备制造中心",
"启用", "财务部", "admin", "按部门归集制造费用"));
list.add(aux("5011", "制造费用", "部门", "生物质肥料制造中心",
"启用", "财务部", "admin", "按部门归集制造费用"));
// 6601 销售费用 - 项目维度
list.add(aux("6601", "销售费用", "项目", "P-YY-2025-001岳阳污水厂",
"启用", "财务部", "admin", "按项目核算销售费用"));
list.add(aux("6601", "销售费用", "项目", "P-HN-2025-003湖南改造项目",
"启用", "财务部", "admin", "按项目核算销售费用"));
// 6602 管理费用 - 成本中心维度
list.add(aux("6602", "管理费用", "成本中心", "总部行政成本中心",
"启用", "财务部", "admin", "按成本中心分配管理费用"));
list.add(aux("6602", "管理费用", "成本中心", "研发成本中心",
"启用", "财务部", "admin", "研发费用归集到成本中心"));
// 1405 库存商品 - 产品维度
list.add(aux("1405", "库存商品", "产品", "过滤膜组件-A型",
"启用", "财务部", "admin", "按产品追踪库存余额"));
list.add(aux("1405", "库存商品", "产品", "污水处理药剂-PAC",
"启用", "财务部", "admin", "按产品追踪库存余额"));
accountAuxRepo.saveAll(list);
}
private FinAccountAux aux(String accountCode, String accountName, String auxDimension,
String auxVal, String enabledVal, String applicant,
String approver, String reason) {
FinAccountAux a = new FinAccountAux();
a.setAccountCode(accountCode);
a.setAccountName(accountName);
a.setAuxDimension(auxDimension);
a.setAuxVal(auxVal);
a.setEnabledVal(enabledVal);
a.setBalance(BigDecimal.ZERO);
a.setApplicant(applicant);
a.setApprover(approver);
a.setReason(reason);
a.setCreatedAt(Instant.now());
a.setUpdatedAt(Instant.now());
return a;
}
// ============================================================
// 3. 预算编制计划(gap-11: 0条)
// ============================================================
private void seedBudgetPlans() {
if (budgetPlanRepo.count() > 0) return;
List<FinBudgetPlan> list = new ArrayList<>();
// 2025年度预算 - 各部门申报
list.add(plan("BP2025-001", "2025", 1, "环保设备制造中心", "成本预算",
"直接材料采购成本", new BigDecimal("12000000"),
new BigDecimal("11500000"), FinBudgetPlan.S_APPROVED,
"材料价格上涨,申报略高", "核减至市场均价水平", "张三", "李四",
"2025-01-10", "2025-01-25"));
list.add(plan("BP2025-002", "2025", 1, "环保设备制造中心", "费用预算",
"制造费用(水电人工)", new BigDecimal("3200000"),
new BigDecimal("3100000"), FinBudgetPlan.S_APPROVED,
"含新增设备维护费用", "适当核减维护费", "张三", "李四",
"2025-01-10", "2025-01-25"));
list.add(plan("BP2025-003", "2025", 1, "研发中心", "费用预算",
"研发人员薪酬", new BigDecimal("5600000"),
new BigDecimal("5600000"), FinBudgetPlan.S_APPROVED,
"含3名新招研发人员", "按申报批准", "王五", "李四",
"2025-01-12", "2025-01-25"));
list.add(plan("BP2025-004", "2025", 1, "工程管理中心", "收入预算",
"主营业务收入-EPC工程", new BigDecimal("85000000"),
new BigDecimal("82000000"), FinBudgetPlan.S_APPROVED,
"基于在手合同及预期新签合同", "保守预估,略有核减", "赵六", "李四",
"2025-01-11", "2025-01-25"));
list.add(plan("BP2025-005", "2025", 1, "财务部", "现金流预算",
"年度净现金流(经营活动)", new BigDecimal("15000000"),
new BigDecimal("14000000"), FinBudgetPlan.S_APPROVED,
"基于合同回款预期", "适当调低", "钱七", "李四",
"2025-01-13", "2025-01-25"));
list.add(plan("BP2025-006", "2025", 2, "工程管理中心", "资本预算",
"固定资产投资(办公设备)", new BigDecimal("800000"),
new BigDecimal("650000"), FinBudgetPlan.S_APPROVED,
"含2台大型绘图仪采购", "核减1台", "赵六", "李四",
"2025-01-15", "2025-01-28"));
// 2026年度预算(汇总中状态)
list.add(plan("BP2026-001", "2026", 1, "环保设备制造中心", "成本预算",
"直接材料采购成本", new BigDecimal("13500000"),
BigDecimal.ZERO, FinBudgetPlan.S_GATHERING,
"预计材料价格持平或略降,增量来自产能扩张", null, "张三", null,
"2026-01-08", null));
list.add(plan("BP2026-002", "2026", 1, "研发中心", "费用预算",
"研发费用总额(含人工+耗材)", new BigDecimal("7200000"),
BigDecimal.ZERO, FinBudgetPlan.S_SUBMITTED,
"含2项新立项研发课题预算", null, "王五", null,
"2026-01-09", null));
budgetPlanRepo.saveAll(list);
}
private FinBudgetPlan plan(String planCode, String year, int roundNo, String dept,
String type, String item, BigDecimal proposed, BigDecimal approved,
String status, String justification, String approvalComment,
String submitter, String approver, String submitDate, String approvalDate) {
FinBudgetPlan p = new FinBudgetPlan();
p.setPlanCode(planCode);
p.setBudgetYear(year);
p.setRoundNo(roundNo);
p.setDepartment(dept);
p.setBudgetType(type);
p.setBudgetItem(item);
p.setProposedAmount(proposed);
p.setApprovedAmount(approved);
p.setVarianceFromTop(proposed.subtract(approved));
p.setStatus(status);
p.setJustification(justification);
p.setApprovalComment(approvalComment);
p.setSubmitter(submitter);
p.setApprover(approver);
p.setSubmitDate(submitDate);
p.setApprovalDate(approvalDate);
p.setCreatedAt(Instant.now());
p.setUpdatedAt(Instant.now());
return p;
}
// ============================================================
// 4. 预算执行记录(gap-11: 0条)
// ============================================================
private void seedBudgetExecutions() {
if (budgetExecRepo.count() > 0) return;
List<FinBudgetExecution> list = new ArrayList<>();
// 2025年各月度执行记录
list.add(exec("直接材料采购成本", FinBudgetExecution.TYPE_COST, "2025-01",
new BigDecimal("1000000"), new BigDecimal("980000"), "环保设备制造中心", "P-2025-MFG", "已批准", Boolean.FALSE));
list.add(exec("直接材料采购成本", FinBudgetExecution.TYPE_COST, "2025-02",
new BigDecimal("1000000"), new BigDecimal("1120000"), "环保设备制造中心", "P-2025-MFG", "已批准", Boolean.TRUE));
list.add(exec("直接材料采购成本", FinBudgetExecution.TYPE_COST, "2025-03",
new BigDecimal("1000000"), new BigDecimal("950000"), "环保设备制造中心", "P-2025-MFG", "已批准", Boolean.FALSE));
list.add(exec("研发人员薪酬", FinBudgetExecution.TYPE_EXPENSE, "2025-01",
new BigDecimal("466667"), new BigDecimal("466667"), "研发中心", null, "已批准", Boolean.FALSE));
list.add(exec("研发人员薪酬", FinBudgetExecution.TYPE_EXPENSE, "2025-02",
new BigDecimal("466667"), new BigDecimal("466667"), "研发中心", null, "已批准", Boolean.FALSE));
list.add(exec("研发人员薪酬", FinBudgetExecution.TYPE_EXPENSE, "2025-03",
new BigDecimal("466667"), new BigDecimal("510000"), "研发中心", null, "已批准", Boolean.TRUE));
list.add(exec("主营业务收入-EPC工程", FinBudgetExecution.TYPE_INCOME, "2025-Q1",
new BigDecimal("20500000"), new BigDecimal("18200000"), "工程管理中心", null, "已批准", Boolean.FALSE));
list.add(exec("主营业务收入-EPC工程", FinBudgetExecution.TYPE_INCOME, "2025-Q2",
new BigDecimal("20500000"), new BigDecimal("22800000"), "工程管理中心", null, "已批准", Boolean.FALSE));
list.add(exec("年度净现金流(经营活动)", FinBudgetExecution.TYPE_CASHFLOW, "2025",
new BigDecimal("14000000"), new BigDecimal("12300000"), "财务部", null, "初稿", Boolean.FALSE));
list.add(exec("固定资产投资(办公设备)", FinBudgetExecution.TYPE_CAPITAL, "2025",
new BigDecimal("650000"), new BigDecimal("580000"), "工程管理中心", null, "已批准", Boolean.FALSE));
budgetExecRepo.saveAll(list);
}
private FinBudgetExecution exec(String budgetItem, String budgetType, String period,
BigDecimal budgetAmount, BigDecimal actualAmount,
String department, String projectCode, String version,
Boolean overBudget) {
FinBudgetExecution e = new FinBudgetExecution();
e.setBudgetItem(budgetItem);
e.setBudgetType(budgetType);
e.setPeriod(period);
e.setBudgetAmount(budgetAmount);
e.setActualAmount(actualAmount);
BigDecimal variance = actualAmount.subtract(budgetAmount);
e.setVarianceAmount(variance);
if (budgetAmount.signum() != 0) {
e.setVarianceRate(variance.multiply(new BigDecimal("100"))
.divide(budgetAmount, 2, java.math.RoundingMode.HALF_UP));
}
e.setOverBudget(overBudget);
e.setAlerted(overBudget);
e.setAlertThreshold(new BigDecimal("10"));
e.setDepartment(department);
e.setProjectCode(projectCode);
e.setCompanySubject("湖南凯迪工程科技有限公司");
e.setBudgetVersion(version);
e.setOperator("admin");
e.setCreatedAt(Instant.now());
e.setUpdatedAt(Instant.now());
return e;
}
// ============================================================
// 5. 成本核算记录(gap-10: 0条)
// ============================================================
private void seedCostCalcRecords() {
if (costCalcRepo.count() > 0) return;
List<FinCostCalc> list = new ArrayList<>();
// 品种法 - 膜组件产品月度成本核算
list.add(costCalc("CC-2025-001", FinCostCalc.METHOD_VARIETY,
"过滤膜组件-A型", "产品", "2025-01",
new BigDecimal("520000"), new BigDecimal("85000"), new BigDecimal("68000"),
"工时", 4200.0, new BigDecimal("16.19"),
new BigDecimal("673000"), new BigDecimal("650000"), FinCostCalc.STATUS_POSTED, "admin"));
list.add(costCalc("CC-2025-002", FinCostCalc.METHOD_VARIETY,
"过滤膜组件-A型", "产品", "2025-02",
new BigDecimal("495000"), new BigDecimal("85000"), new BigDecimal("62000"),
"工时", 4000.0, new BigDecimal("15.50"),
new BigDecimal("642000"), new BigDecimal("650000"), FinCostCalc.STATUS_POSTED, "admin"));
list.add(costCalc("CC-2025-003", FinCostCalc.METHOD_VARIETY,
"污水处理药剂-PAC", "产品", "2025-01",
new BigDecimal("280000"), new BigDecimal("32000"), new BigDecimal("25000"),
"产量", 5000.0, new BigDecimal("5.00"),
new BigDecimal("337000"), new BigDecimal("330000"), FinCostCalc.STATUS_POSTED, "admin"));
// 分批法 - 工程项目成本归集
list.add(costCalc("CC-2025-004", FinCostCalc.METHOD_BATCH,
"P-YY-2025-001岳阳污水厂", "项目", "2025-Q1",
new BigDecimal("3200000"), new BigDecimal("680000"), new BigDecimal("420000"),
"收入", 18200000.0, new BigDecimal("0.023"),
new BigDecimal("4300000"), new BigDecimal("4200000"), FinCostCalc.STATUS_APPROVED, "张三"));
list.add(costCalc("CC-2025-005", FinCostCalc.METHOD_BATCH,
"P-HN-2025-003湖南改造项目", "项目", "2025-Q1",
new BigDecimal("1800000"), new BigDecimal("380000"), new BigDecimal("210000"),
"收入", 9500000.0, new BigDecimal("0.022"),
new BigDecimal("2390000"), new BigDecimal("2350000"), FinCostCalc.STATUS_APPROVED, "赵六"));
// 分步法 - 生产工单成本
list.add(costCalc("CC-2025-006", FinCostCalc.METHOD_PROCESS,
"WO-2025-003膜组件装配", "工单", "2025-02",
new BigDecimal("120000"), new BigDecimal("28000"), new BigDecimal("15000"),
"工时", 850.0, new BigDecimal("17.65"),
new BigDecimal("163000"), new BigDecimal("160000"), FinCostCalc.STATUS_POSTED, "admin"));
// 作业成本法 - 研发项目
list.add(costCalc("CC-2025-007", FinCostCalc.METHOD_ABC,
"RD-2025-002新型膜研发", "项目", "2025-Q1",
new BigDecimal("680000"), new BigDecimal("520000"), new BigDecimal("85000"),
"人数", 8.0, new BigDecimal("10625"),
new BigDecimal("1285000"), new BigDecimal("1300000"), FinCostCalc.STATUS_DRAFT, "王五"));
costCalcRepo.saveAll(list);
}
private FinCostCalc costCalc(String code, String calcMethod, String costObject, String costObjectType,
String period, BigDecimal directMaterial, BigDecimal directLabor,
BigDecimal manufacturingOverhead, String allocationDriver,
Double driverQuantity, BigDecimal allocationRate,
BigDecimal totalCost, BigDecimal standardCost,
String status, String operator) {
FinCostCalc c = new FinCostCalc();
c.setCode(code);
c.setCalcMethod(calcMethod);
c.setCostObject(costObject);
c.setCostObjectType(costObjectType);
c.setPeriod(period);
c.setDirectMaterial(directMaterial);
c.setDirectLabor(directLabor);
c.setManufacturingOverhead(manufacturingOverhead);
c.setAllocationDriver(allocationDriver);
c.setDriverQuantity(driverQuantity);
c.setAllocationRate(allocationRate);
c.setTotalCost(totalCost);
c.setStandardCost(standardCost);
c.setCostVariance(totalCost.subtract(standardCost));
c.setStatus(status);
c.setOperator(operator);
c.setCreatedAt(Instant.now());
return c;
}
// ============================================================
// 6. 员工借款演示数据(多状态覆盖)
// ============================================================
private void seedEmpLoans() {
if (empLoanRepo.count() > 0) return;
List<FinEmpLoan> list = new ArrayList<>();
// 已批准 - 差旅借款
list.add(empLoan("EL-202501", "张工", "E001", "工程管理中心",
FinEmpLoan.TYPE_TRAVEL, "岳阳项目现场调研差旅费",
new BigDecimal("8000"), BigDecimal.ZERO, new BigDecimal("8000"),
"2025-01-05", "2025-02-05", null,
"2025-01-07", "张总", "同意", null, null,
FinEmpLoan.S_APPROVED, Boolean.FALSE));
// 还款中 - 备用金借款
list.add(empLoan("EL-202502", "李会计", "E002", "财务部",
FinEmpLoan.TYPE_PETTY, "2025年Q1采购零星备用金",
new BigDecimal("5000"), new BigDecimal("2000"), new BigDecimal("3000"),
"2025-01-10", "2025-04-10", null,
"2025-01-12", "财务总监", "同意", null, "EXP-2025-012",
FinEmpLoan.S_REPAYING, Boolean.FALSE));
// 已结清 - 差旅借款
list.add(empLoan("EL-202503", "王测试", "E003", "研发中心",
FinEmpLoan.TYPE_TRAVEL, "上海环博会参展差旅",
new BigDecimal("12000"), new BigDecimal("12000"), BigDecimal.ZERO,
"2025-02-01", "2025-03-01", "2025-02-28",
"2025-02-03", "研发总监", "同意", "2025-02-28", "EXP-2025-023",
FinEmpLoan.S_SETTLED, Boolean.FALSE));
// 逾期 - 其他借款(已过期未还)
list.add(empLoan("EL-202504", "赵工", "E004", "环保设备制造中心",
FinEmpLoan.TYPE_OTHER, "设备维修紧急备用金",
new BigDecimal("3000"), BigDecimal.ZERO, new BigDecimal("3000"),
"2025-03-15", "2025-04-15", null,
"2025-03-17", "生产总监", "同意", null, null,
FinEmpLoan.S_OVERDUE, Boolean.FALSE));
// 待审批 - 备用金借款
list.add(empLoan("EL-202505", "孙经理", "E005", "市场部",
FinEmpLoan.TYPE_PETTY, "华南区市场拓展备用金",
new BigDecimal("15000"), BigDecimal.ZERO, new BigDecimal("15000"),
"2025-05-20", "2025-08-20", null,
null, null, null, null, null,
FinEmpLoan.S_PENDING, Boolean.FALSE));
// 草稿 - 差旅借款
list.add(empLoan("EL-202506", "周工", "E006", "工程管理中心",
FinEmpLoan.TYPE_TRAVEL, "武汉项目踏勘差旅",
new BigDecimal("6500"), BigDecimal.ZERO, new BigDecimal("6500"),
"2025-06-01", "2025-07-01", null,
null, null, null, null, null,
FinEmpLoan.S_DRAFT, Boolean.FALSE));
empLoanRepo.saveAll(list);
}
private FinEmpLoan empLoan(String loanCode, String employeeName, String employeeId,
String department, String loanType, String purpose,
BigDecimal loanAmount, BigDecimal repaidAmount, BigDecimal remainAmount,
String applyDate, String expectedRepayDate, String settleDate,
String approvalDate, String approver, String approvalComment,
String settleDate2, String relatedExpenseCode,
String status, Boolean deductFromSalary) {
FinEmpLoan l = new FinEmpLoan();
l.setLoanCode(loanCode);
l.setEmployeeName(employeeName);
l.setEmployeeId(employeeId);
l.setDepartment(department);
l.setLoanType(loanType);
l.setPurpose(purpose);
l.setLoanAmount(loanAmount);
l.setRepaidAmount(repaidAmount);
l.setRemainAmount(remainAmount);
l.setApplyDate(applyDate);
l.setExpectedRepayDate(expectedRepayDate);
l.setSettleDate(settleDate);
l.setApprovalDate(approvalDate);
l.setApprover(approver);
l.setApprovalComment(approvalComment);
l.setRelatedExpenseCode(relatedExpenseCode);
l.setStatus(status);
l.setDeductFromSalary(deductFromSalary);
if (FinEmpLoan.S_OVERDUE.equals(status)) {
l.setOverdueDays(61); // 逾期超过60天
}
l.setOperator("admin");
l.setCreatedAt(Instant.now());
l.setUpdatedAt(Instant.now());
return l;
}
}
@@ -0,0 +1,469 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.Financing;
import com.kaidi.oa.domain.FinancingPolicy;
import com.kaidi.oa.domain.PmtBankEvaluation;
import com.kaidi.oa.domain.PmtBankRelation;
import com.kaidi.oa.domain.RepaymentPlan;
import com.kaidi.oa.repository.FinancingPolicyRepository;
import com.kaidi.oa.repository.FinancingRepository;
import com.kaidi.oa.repository.PmtBankEvaluationRepository;
import com.kaidi.oa.repository.PmtBankRelationRepository;
import com.kaidi.oa.repository.RepaymentPlanRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 融资域演示数据(金融办)。
* 补建 Gap-1/2/5 的活体演示数据:金融机构档案、融资台账、机构定期评价、融资政策规则、还款计划。
* 只在对应表为空时执行,防止重复。@Order(20) 在 DataSeeder(@Order(1)) 之后运行。
*/
@Order(20)
@Component
public class FinancingDomainSeeder implements CommandLineRunner {
private final PmtBankRelationRepository bankRelRepo;
private final FinancingRepository financingRepo;
private final RepaymentPlanRepository planRepo;
private final PmtBankEvaluationRepository evalRepo;
private final FinancingPolicyRepository policyRepo;
public FinancingDomainSeeder(PmtBankRelationRepository bankRelRepo,
FinancingRepository financingRepo,
RepaymentPlanRepository planRepo,
PmtBankEvaluationRepository evalRepo,
FinancingPolicyRepository policyRepo) {
this.bankRelRepo = bankRelRepo;
this.financingRepo = financingRepo;
this.planRepo = planRepo;
this.evalRepo = evalRepo;
this.policyRepo = policyRepo;
}
@Override
public void run(String... args) {
seedBankRelations();
seedFinancings();
seedPolicies();
seedEvaluations();
}
private void seedBankRelations() {
if (bankRelRepo.count() > 0) return;
PmtBankRelation r1 = new PmtBankRelation();
r1.setInstitutionName("工商银行长沙分行");
r1.setInstitutionType("商业银行");
r1.setBranch("长沙分行公司业务部");
r1.setAccountManager("张磊");
r1.setManagerPhone("0731-88882001");
r1.setTotalCreditLimit(Money.of(200000000.0));
r1.setCooperationStartDate("2019-03-01");
r1.setRating("A");
r1.setAvgApprovalDays(7);
r1.setRemark("主要合作银行,综合授信2亿,流贷+固贷均有合作历史。");
r1.setStatus("合作中");
r1.setCreatedBy("admin");
r1.setCreatedAt(Instant.now());
bankRelRepo.save(r1);
PmtBankRelation r2 = new PmtBankRelation();
r2.setInstitutionName("建设银行株洲支行");
r2.setInstitutionType("商业银行");
r2.setBranch("株洲高新区支行");
r2.setAccountManager("李梅");
r2.setManagerPhone("0733-77773001");
r2.setTotalCreditLimit(Money.of(100000000.0));
r2.setCooperationStartDate("2020-06-15");
r2.setRating("B");
r2.setAvgApprovalDays(10);
r2.setRemark("次要银行,主要为子公司株洲项目配套贷款。");
r2.setStatus("合作中");
r2.setCreatedBy("admin");
r2.setCreatedAt(Instant.now());
bankRelRepo.save(r2);
PmtBankRelation r3 = new PmtBankRelation();
r3.setInstitutionName("国家开发银行湖南省分行");
r3.setInstitutionType("政策性银行");
r3.setBranch("湖南省分行基础设施部");
r3.setAccountManager("王强");
r3.setManagerPhone("0731-66661001");
r3.setTotalCreditLimit(Money.of(500000000.0));
r3.setCooperationStartDate("2021-01-20");
r3.setRating("A");
r3.setAvgApprovalDays(15);
r3.setRemark("政策性长期资金来源,主要用于环保固定资产投资项目贷款,利率优惠。");
r3.setStatus("合作中");
r3.setCreatedBy("admin");
r3.setCreatedAt(Instant.now());
bankRelRepo.save(r3);
PmtBankRelation r4 = new PmtBankRelation();
r4.setInstitutionName("中信银行长沙分行");
r4.setInstitutionType("商业银行");
r4.setBranch("长沙分行企业金融部");
r4.setAccountManager("陈晓");
r4.setManagerPhone("0731-55550801");
r4.setTotalCreditLimit(Money.of(80000000.0));
r4.setCooperationStartDate("2022-09-01");
r4.setRating("B");
r4.setAvgApprovalDays(8);
r4.setRemark("近两年新增合作银行,流动资金贷款为主,银行承兑汇票业务活跃。");
r4.setStatus("合作中");
r4.setCreatedBy("admin");
r4.setCreatedAt(Instant.now());
bankRelRepo.save(r4);
}
private void seedFinancings() {
if (financingRepo.count() > 0) return;
Financing f1 = new Financing();
f1.setCode("RZ-2024-001");
f1.setLender("工商银行长沙分行");
f1.setFinancingType("银行贷款");
f1.setCreditLimit(Money.of(50000000.0));
f1.setAmount(Money.of(50000000.0));
f1.setCurrency("人民币");
f1.setRate(3.85);
f1.setStartDate("2024-01-15");
f1.setEndDate("2026-01-14");
f1.setTermMonths(24);
f1.setRepayMethod("按期付息到期还本");
f1.setStatus("还款中");
f1.setCompanySubject("凯迪科技股份有限公司");
f1.setPurpose("补充流动资金");
f1.setOwner("admin");
f1.setCreatedAt(Instant.now());
Financing sf1 = financingRepo.save(f1);
// 为 f1 生成还款计划(按期付息到期还本,24期,月息)
seedRepaymentPlan(sf1, 24, 3.85, "按期付息到期还本", "2024-01-15");
Financing f2 = new Financing();
f2.setCode("RZ-2024-002");
f2.setLender("国家开发银行湖南省分行");
f2.setFinancingType("银行贷款");
f2.setCreditLimit(Money.of(100000000.0));
f2.setAmount(Money.of(80000000.0));
f2.setCurrency("人民币");
f2.setRate(3.20);
f2.setStartDate("2024-03-01");
f2.setEndDate("2029-02-28");
f2.setTermMonths(60);
f2.setRepayMethod("等额本金");
f2.setStatus("还款中");
f2.setCompanySubject("凯迪科技股份有限公司");
f2.setPurpose("污水处理厂固定资产投资");
f2.setOwner("admin");
f2.setCreatedAt(Instant.now());
Financing sf2 = financingRepo.save(f2);
seedRepaymentPlan(sf2, 60, 3.20, "等额本金", "2024-03-01");
Financing f3 = new Financing();
f3.setCode("RZ-2024-003");
f3.setLender("建设银行株洲支行");
f3.setFinancingType("银行贷款");
f3.setCreditLimit(Money.of(20000000.0));
f3.setAmount(Money.of(18000000.0));
f3.setCurrency("人民币");
f3.setRate(4.35);
f3.setStartDate("2024-06-01");
f3.setEndDate("2025-05-31");
f3.setTermMonths(12);
f3.setRepayMethod("到期一次还本付息");
f3.setStatus("还款中");
f3.setCompanySubject("株洲凯迪环保科技有限公司");
f3.setPurpose("补充流动资金");
f3.setOwner("admin");
f3.setCreatedAt(Instant.now());
Financing sf3 = financingRepo.save(f3);
seedRepaymentPlan(sf3, 12, 4.35, "到期一次还本付息", "2024-06-01");
Financing f4 = new Financing();
f4.setCode("RZ-2025-001");
f4.setLender("中信银行长沙分行");
f4.setFinancingType("银行贷款");
f4.setCreditLimit(Money.of(30000000.0));
f4.setAmount(Money.of(30000000.0));
f4.setCurrency("人民币");
f4.setRate(4.10);
f4.setStartDate("2025-02-01");
f4.setEndDate("2026-01-31");
f4.setTermMonths(12);
f4.setRepayMethod("等额本息");
f4.setStatus("还款中");
f4.setCompanySubject("凯迪科技股份有限公司");
f4.setPurpose("采购原材料及备品备件");
f4.setOwner("admin");
f4.setCreatedAt(Instant.now());
Financing sf4 = financingRepo.save(f4);
seedRepaymentPlan(sf4, 12, 4.10, "等额本息", "2025-02-01");
// 一笔申请中
Financing f5 = new Financing();
f5.setCode("RZ-2025-002");
f5.setLender("工商银行长沙分行");
f5.setFinancingType("银行承兑汇票");
f5.setCreditLimit(Money.of(20000000.0));
f5.setAmount(Money.of(20000000.0));
f5.setCurrency("人民币");
f5.setRate(2.50);
f5.setStartDate("2025-07-01");
f5.setEndDate("2026-06-30");
f5.setTermMonths(12);
f5.setRepayMethod("到期一次还本付息");
f5.setStatus("申请中");
f5.setCompanySubject("凯迪科技股份有限公司");
f5.setPurpose("支付设备采购款(银承)");
f5.setOwner("admin");
f5.setCreatedAt(Instant.now());
financingRepo.save(f5);
// 一笔已结清
Financing f6 = new Financing();
f6.setCode("RZ-2023-003");
f6.setLender("建设银行株洲支行");
f6.setFinancingType("银行贷款");
f6.setCreditLimit(Money.of(10000000.0));
f6.setAmount(Money.of(10000000.0));
f6.setCurrency("人民币");
f6.setRate(4.60);
f6.setStartDate("2023-01-01");
f6.setEndDate("2024-12-31");
f6.setTermMonths(24);
f6.setRepayMethod("等额本息");
f6.setStatus("已结清");
f6.setCompanySubject("株洲凯迪环保科技有限公司");
f6.setPurpose("经营周转资金");
f6.setOwner("admin");
f6.setCreatedAt(Instant.now());
financingRepo.save(f6);
}
private void seedRepaymentPlan(Financing f, int term, double annualRate, String method, String startDate) {
if (planRepo.findByFinancingIdOrderByPeriodNoAsc(f.getId()).size() > 0) return;
double monthlyRate = annualRate / 100.0 / 12.0;
double principal = Money.nz(f.getAmount()).doubleValue();
java.time.LocalDate start;
try {
start = java.time.LocalDate.parse(startDate);
} catch (Exception e) {
start = java.time.LocalDate.now();
}
if ("到期一次还本付息".equals(method)) {
double interest = principal * monthlyRate * term;
RepaymentPlan p = buildPlan(f, 1, start.plusMonths(term).toString(),
new BigDecimal(principal).setScale(2, java.math.RoundingMode.HALF_UP),
Money.of(interest), "未还");
planRepo.save(p);
return;
}
if ("按期付息到期还本".equals(method)) {
BigDecimal totalPrincipal = Money.nz(f.getAmount());
for (int i = 1; i <= term; i++) {
double interest = principal * monthlyRate;
BigDecimal prin = (i == term) ? totalPrincipal : BigDecimal.ZERO;
RepaymentPlan p = buildPlan(f, i, start.plusMonths(i).toString(),
Money.nz(prin), Money.of(interest), "未还");
planRepo.save(p);
}
return;
}
if ("等额本金".equals(method)) {
BigDecimal totalPrincipal = Money.nz(f.getAmount());
BigDecimal perPrincipal = totalPrincipal.divide(BigDecimal.valueOf(term), 2, java.math.RoundingMode.HALF_UP);
double balance = principal;
BigDecimal acc = BigDecimal.ZERO;
for (int i = 1; i <= term; i++) {
double interest = balance * monthlyRate;
BigDecimal prin = (i == term) ? Money.sub(totalPrincipal, acc) : perPrincipal;
acc = Money.add(acc, prin);
RepaymentPlan p = buildPlan(f, i, start.plusMonths(i).toString(),
Money.nz(prin), Money.of(interest), "未还");
planRepo.save(p);
balance -= prin.doubleValue();
}
return;
}
// 等额本息
BigDecimal totalPrincipal = Money.nz(f.getAmount());
double installment;
if (monthlyRate == 0) {
installment = principal / term;
} else {
double pow = Math.pow(1 + monthlyRate, term);
installment = principal * monthlyRate * pow / (pow - 1);
}
double balance = principal;
BigDecimal acc = BigDecimal.ZERO;
for (int i = 1; i <= term; i++) {
double interest = balance * monthlyRate;
double prinD = installment - interest;
BigDecimal prin = (i == term) ? Money.sub(totalPrincipal, acc) : Money.of(prinD);
acc = Money.add(acc, prin);
RepaymentPlan p = buildPlan(f, i, start.plusMonths(i).toString(),
Money.nz(prin), Money.of(interest), "未还");
planRepo.save(p);
balance -= prin.doubleValue();
}
}
private RepaymentPlan buildPlan(Financing f, int no, String dueDate,
BigDecimal principal, BigDecimal interest, String status) {
RepaymentPlan p = new RepaymentPlan();
p.setFinancingId(f.getId());
p.setFinancingCode(f.getCode());
p.setPeriodNo(no);
p.setDueDate(dueDate);
p.setPrincipal(Money.nz(principal));
p.setInterest(Money.nz(interest));
p.setStatus(status);
p.setCreatedAt(Instant.now());
return p;
}
private void seedPolicies() {
if (policyRepo.count() > 0) return;
FinancingPolicy p1 = new FinancingPolicy();
p1.setRuleName("单笔流动资金贷款额度上限");
p1.setRuleType("额度上限");
p1.setLimitValue(Money.of(100000000.0));
p1.setScope("全集团");
p1.setHardBlock(true);
p1.setDescription("集团融资政策:单笔流动资金贷款不超过 1 亿元,超限须报董事会审批。");
p1.setStatus("启用");
p1.setCreatedBy("admin");
p1.setCreatedAt(Instant.now());
policyRepo.save(p1);
FinancingPolicy p2 = new FinancingPolicy();
p2.setRuleName("融资综合利率上限");
p2.setRuleType("利率上限");
p2.setLimitValue(Money.of(6.0));
p2.setScope("全集团");
p2.setHardBlock(false);
p2.setDescription("集团融资政策:融资年化利率超过 6% 视为高成本融资,需在申请中说明替代方案比选结果,硬拒以警告为主不阻断。");
p2.setStatus("启用");
p2.setCreatedBy("admin");
p2.setCreatedAt(Instant.now());
policyRepo.save(p2);
FinancingPolicy p3 = new FinancingPolicy();
p3.setRuleName("流动资金贷款期限上限");
p3.setRuleType("期限上限");
p3.setLimitValue(Money.of(12.0));
p3.setScope("流动资金");
p3.setHardBlock(true);
p3.setDescription("流动资金贷款期限原则上不超过 12 个月(1年),超限需升级审批至财务总监。");
p3.setStatus("启用");
p3.setCreatedBy("admin");
p3.setCreatedAt(Instant.now());
policyRepo.save(p3);
FinancingPolicy p4 = new FinancingPolicy();
p4.setRuleName("允许融资品种限制");
p4.setRuleType("品种限制");
p4.setAllowedValues("银行贷款,融资租赁,银行承兑汇票,信用证");
p4.setScope("全集团");
p4.setHardBlock(true);
p4.setDescription("集团融资政策:仅允许银行贷款、融资租赁、银行承兑汇票、信用证四种融资品种,其他品种须专项论证并报批。");
p4.setStatus("启用");
p4.setCreatedBy("admin");
p4.setCreatedAt(Instant.now());
policyRepo.save(p4);
FinancingPolicy p5 = new FinancingPolicy();
p5.setRuleName("固定资产贷款期限上限");
p5.setRuleType("期限上限");
p5.setLimitValue(Money.of(120.0));
p5.setScope("固定资产投资");
p5.setHardBlock(true);
p5.setDescription("固定资产贷款期限不超过 10 年(120 个月),与项目建设及投资回收期匹配。");
p5.setStatus("启用");
p5.setCreatedBy("admin");
p5.setCreatedAt(Instant.now());
policyRepo.save(p5);
}
private void seedEvaluations() {
if (evalRepo.count() > 0) return;
// 需要 bankRelRepo 已有数据
java.util.List<PmtBankRelation> rels = bankRelRepo.findAll();
if (rels.isEmpty()) return;
Instant now = Instant.now();
for (PmtBankRelation rel : rels) {
// 每个机构建 2 条历史评价(2024Q4 和 2025Q2
PmtBankEvaluation ev1 = new PmtBankEvaluation();
ev1.setBankRelId(rel.getId());
ev1.setInstitutionName(rel.getInstitutionName());
ev1.setEvalPeriod("2024Q4");
ev1.setEvalDate("2024-12-31");
int s1a = baseScore(rel.getRating()) - 5;
int s1b = baseScore(rel.getRating());
int s1c = baseScore(rel.getRating()) + 3;
int s1d = baseScore(rel.getRating()) - 2;
ev1.setScoreApprovalSpeed(clamp(s1a));
ev1.setScoreServiceQuality(clamp(s1b));
ev1.setScoreRateCompetitiveness(clamp(s1c));
ev1.setScoreCooperation(clamp(s1d));
int total1 = (clamp(s1a) + clamp(s1b) + clamp(s1c) + clamp(s1d)) / 4;
ev1.setTotalScore(total1);
ev1.setGrade(grade(total1));
ev1.setRemark("2024年四季度综合评价,审批效率和服务质量符合预期。");
ev1.setEvaluator("admin");
ev1.setStatus("已完成");
ev1.setCreatedAt(now);
evalRepo.save(ev1);
PmtBankEvaluation ev2 = new PmtBankEvaluation();
ev2.setBankRelId(rel.getId());
ev2.setInstitutionName(rel.getInstitutionName());
ev2.setEvalPeriod("2025Q2");
ev2.setEvalDate("2025-06-30");
int s2a = baseScore(rel.getRating()) + 2;
int s2b = baseScore(rel.getRating()) + 1;
int s2c = baseScore(rel.getRating());
int s2d = baseScore(rel.getRating()) + 4;
ev2.setScoreApprovalSpeed(clamp(s2a));
ev2.setScoreServiceQuality(clamp(s2b));
ev2.setScoreRateCompetitiveness(clamp(s2c));
ev2.setScoreCooperation(clamp(s2d));
int total2 = (clamp(s2a) + clamp(s2b) + clamp(s2c) + clamp(s2d)) / 4;
ev2.setTotalScore(total2);
ev2.setGrade(grade(total2));
ev2.setRemark("2025年二季度综合评价,各维度较上期有所提升。");
ev2.setEvaluator("admin");
ev2.setStatus("已完成");
ev2.setCreatedAt(now);
evalRepo.save(ev2);
}
}
private static int baseScore(String rating) {
if ("A".equals(rating)) return 85;
if ("B".equals(rating)) return 70;
if ("C".equals(rating)) return 55;
return 40;
}
private static int clamp(int v) {
return Math.max(0, Math.min(100, v));
}
private static String grade(int total) {
if (total >= 80) return "A";
if (total >= 60) return "B";
if (total >= 40) return "C";
return "D";
}
}
@@ -0,0 +1,37 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.service.FullTextSearchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 启动期重建全文索引。{@code @Order(200)} 保证排在所有 DataSeeder
* @Order 1/2/100)之后执行,这样种子数据已经落库,索引才完整。
*
* 索引失败已在 {@link FullTextSearchService#reindexAll()} 内被 try/catch
* 兜住,这里再包一层,绝不让检索问题阻断应用启动。
*/
@Component
@Order(200)
public class FtsIndexRunner implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(FtsIndexRunner.class);
private final FullTextSearchService searchService;
public FtsIndexRunner(FullTextSearchService searchService) {
this.searchService = searchService;
}
@Override
public void run(String... args) {
try {
searchService.reindexAll();
} catch (Exception e) {
log.warn("[FTS] 启动期索引重建异常(已忽略,不影响启动): {}", e.getMessage());
}
}
}
@@ -0,0 +1,210 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.HrLaborTemplate;
import com.kaidi.oa.repository.HrLaborTemplateRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.time.LocalDate;
/**
* 行政·综合部——劳动合同专属模板演示种子数据(Module 17 缺口补全)。
*
* 补全缺口:「劳动合同专属模板管理缺失」——表 hr_labor_template 无种子,
* 功能在但首屏为空,hr-labor-template.vue 页面无演示内容。
*
* 幂等(count > 0 跳过);@Order(226) 在 AdminQualElecSeeder(225) 之后运行。
*/
@Component
@Order(226)
public class HrLaborTemplateSeeder implements CommandLineRunner {
private final HrLaborTemplateRepository repo;
public HrLaborTemplateSeeder(HrLaborTemplateRepository repo) {
this.repo = repo;
}
@Override
public void run(String... args) {
if (repo.count() > 0) return;
Instant now = Instant.now();
LocalDate today = LocalDate.now();
String reviewDate = today.minusDays(30).toString();
// 1. 标准固定期限劳动合同——已启用
repo.save(tpl(
"凯迪科技2025版固定期限劳动合同", "固定期限劳动合同", "正式员工", "V2025.1",
buildFixedTermBody(),
"试用期条款,薪资保密条款,知识产权归属条款,竞业限制条款",
"陈法务", reviewDate, "已审核,符合《劳动合同法》要求,建议启用",
"HR部", "已启用", "HR总监", now));
// 2. 无固定期限劳动合同——已启用
repo.save(tpl(
"凯迪科技2025版无固定期限劳动合同", "无固定期限劳动合同", "正式员工", "V2025.1",
buildOpenTermBody(),
"薪资保密条款,知识产权归属条款,竞业限制条款,保密条款",
"陈法务", reviewDate, "无固定期限合同,已确认满足签订条件,建议启用",
"HR部", "已启用", "HR总监", now));
// 3. 劳务协议——已启用
repo.save(tpl(
"凯迪科技2025版劳务协议", "劳务协议", "劳务人员", "V2025.1",
buildLaborServiceBody(),
"劳务费结算条款,工作成果归属条款,保密条款",
"陈法务", today.minusDays(15).toString(), "劳务协议,非劳动关系,已审核",
"HR部", "已启用", "HR总监", now));
// 4. 实习协议——已启用
repo.save(tpl(
"凯迪科技2025版实习协议", "实习协议", "实习生", "V2025.1",
buildInternBody(),
"实习补贴条款,实习成果归属,转正考核条款",
"陈法务", today.minusDays(10).toString(), "实习协议,已审核通过",
"HR部", "已启用", "行政专员", now));
// 5. 竞业保密协议——法务审核中
repo.save(tpl(
"凯迪科技2025版竞业保密协议(技术岗)", "竞业保密协议", "正式员工", "V2025.1",
buildNonCompeteBody(),
"竞业限制范围,保密期限,违约金条款",
null, null, null,
"HR部", "法务审核中", "HR专员", now));
// 6. 以完成工作任务为期限合同——草稿
repo.save(tpl(
"凯迪科技2025版项目制劳动合同(草稿)", "以完成一定工作任务为期限", "正式员工", "V1.0",
"{员工姓名}(身份证号:{身份证号})与凯迪科技有限公司签订以完成{项目名称}为期限的劳动合同。\n" +
"预计工期:{预计完成日期}。\n" +
"薪资待遇:{月薪} 元/月。\n" +
"项目提前或延期完成,合同相应变更。",
"项目范围条款,工期变更条款",
null, null, null,
"HR部", "草稿", "HR专员", now));
}
private static HrLaborTemplate tpl(String name, String templateType, String applicableTo,
String version, String bodyTemplate, String requiredClauses,
String legalReviewedBy, String legalReviewDate, String legalReviewComment,
String createdBy, String status, String updatedBy, Instant now) {
HrLaborTemplate t = new HrLaborTemplate();
t.setName(name);
t.setTemplateType(templateType);
t.setApplicableTo(applicableTo);
t.setVersion(version);
t.setBodyTemplate(bodyTemplate);
t.setRequiredClauses(requiredClauses);
t.setLegalReviewedBy(legalReviewedBy);
t.setLegalReviewDate(legalReviewDate);
t.setLegalReviewComment(legalReviewComment);
t.setCreatedBy(createdBy);
t.setStatus(status);
t.setUpdatedBy(updatedBy);
t.setCreatedAt(now);
t.setUpdatedAt(now);
return t;
}
private static String buildFixedTermBody() {
return "劳动合同书\n\n" +
"甲方(用人单位):凯迪科技有限公司\n" +
"乙方(劳动者):{员工姓名}\n" +
"身份证号码:{身份证号}\n\n" +
"根据《中华人民共和国劳动合同法》等有关法律法规,甲乙双方经协商一致,订立本合同。\n\n" +
"第一条 合同期限\n" +
"本合同为固定期限劳动合同,合同期限自 {起始日期} 起至 {终止日期} 止," +
"其中试用期自 {起始日期} 起至 {试用期到期日} 止。\n\n" +
"第二条 工作内容和工作地点\n" +
"乙方同意在 {工作地点} 担任 {岗位} 职务,按时完成甲方分配的工作任务。\n\n" +
"第三条 劳动报酬\n" +
"甲方每月向乙方支付税前工资 {月薪} 元,按照法律规定代扣个人所得税、社会保险费。\n\n" +
"第四条 社会保险\n" +
"甲乙双方依法参加社会保险,各自承担相应的缴费义务。\n\n" +
"第五条 保密义务\n" +
"乙方对甲方的商业秘密、技术秘密及客户信息负有保密义务,保密期限为合同存续期间及离职后 {保密年限} 年。\n\n" +
"第六条 知识产权\n" +
"乙方在职期间完成的职务作品、发明创造,知识产权归甲方所有。\n\n" +
"签订日期:{签署日期}\n" +
"甲方签字(盖章):___________ 乙方签字:{员工姓名}";
}
private static String buildOpenTermBody() {
return "无固定期限劳动合同书\n\n" +
"甲方(用人单位):凯迪科技有限公司\n" +
"乙方(劳动者):{员工姓名}\n" +
"身份证号码:{身份证号}\n\n" +
"甲乙双方经协商一致,根据《中华人民共和国劳动合同法》订立本无固定期限劳动合同。\n\n" +
"第一条 合同期限\n" +
"本合同为无固定期限劳动合同,自 {起始日期} 起生效,无终止日期," +
"符合法定条件时依法解除或终止。\n\n" +
"第二条 工作岗位\n" +
"乙方担任 {岗位} 职务,工作地点为 {工作地点}。\n\n" +
"第三条 薪酬\n" +
"月薪 {月薪} 元,绩效奖金另行约定。\n\n" +
"第四条 其他条款\n" +
"其余条款参照公司《员工手册》及相关制度执行。\n\n" +
"签订日期:{签署日期}\n" +
"甲方(盖章):___________ 乙方:{员工姓名}";
}
private static String buildLaborServiceBody() {
return "劳务协议书\n\n" +
"委托方(甲方):凯迪科技有限公司\n" +
"提供方(乙方):{乙方姓名}\n" +
"身份证号:{身份证号}\n\n" +
"甲乙双方就劳务事项达成如下协议:\n\n" +
"第一条 服务内容\n" +
"乙方为甲方提供 {服务内容} 相关劳务服务,服务期限自 {起始日期} 至 {结束日期}。\n\n" +
"第二条 劳务报酬\n" +
"甲方按 {结算方式} 向乙方支付劳务费,金额为 {劳务费用} 元,每 {结算周期} 结算一次。\n\n" +
"第三条 工作成果\n" +
"乙方在本协议期间产生的工作成果归甲方所有,乙方不得留存副本或对外披露。\n\n" +
"第四条 保密\n" +
"乙方对甲方商业秘密负有保密义务。\n\n" +
"第五条 双方关系\n" +
"本协议为劳务关系,非劳动关系,乙方社会保险自行缴纳。\n\n" +
"签订日期:{签署日期}";
}
private static String buildInternBody() {
return "实习协议书\n\n" +
"甲方:凯迪科技有限公司\n" +
"乙方(实习生):{实习生姓名}\n" +
"学校:{就读学校} 专业:{专业}\n\n" +
"甲乙双方就实习事项协商一致,签订本协议:\n\n" +
"第一条 实习岗位与期限\n" +
"乙方于 {实习开始日期} 至 {实习结束日期} 在甲方 {实习岗位} 岗位实习。\n\n" +
"第二条 实习补贴\n" +
"甲方每月向乙方支付实习补贴 {实习补贴} 元,不含社保。\n\n" +
"第三条 实习成果\n" +
"实习期间产生的成果知识产权归甲方所有。\n\n" +
"第四条 转正考核\n" +
"实习结束后,甲方视乙方表现决定是否启动转正流程。转正月薪:{转正薪资} 元。\n\n" +
"第五条 保密\n" +
"乙方不得对外披露甲方商业信息。\n\n" +
"签订日期:{签署日期}";
}
private static String buildNonCompeteBody() {
return "竞业保密协议\n\n" +
"甲方:凯迪科技有限公司\n" +
"乙方:{员工姓名}\n" +
"岗位:{岗位}\n\n" +
"甲乙双方就竞业限制及保密事项协议如下:\n\n" +
"第一条 保密范围\n" +
"乙方对在职期间接触到的技术秘密、客户名单、经营策略等保密信息负有保密义务," +
"不得向任何第三方披露。保密期限:合同期间及离职后 {保密年限} 年。\n\n" +
"第二条 竞业限制\n" +
"乙方离职后 {竞业限制期} 个月内,不得在竞争企业任职或自营同类业务。\n" +
"竞争企业范围:{竞争企业范围}。\n\n" +
"第三条 竞业补偿\n" +
"甲方在竞业限制期内按乙方离职前月均工资的 {竞业补偿比例} 支付竞业限制补偿金。\n\n" +
"第四条 违约责任\n" +
"乙方违反本协议,须向甲方支付违约金 {违约金额} 元,并赔偿甲方损失。\n\n" +
"签订日期:{签署日期}";
}
}
@@ -0,0 +1,336 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.HrAiDocDraft;
import com.kaidi.oa.domain.HrInterviewQuestion;
import com.kaidi.oa.domain.HrRecruitCandidate;
import com.kaidi.oa.domain.HrRecruitJob;
import com.kaidi.oa.repository.HrAiDocDraftRepository;
import com.kaidi.oa.repository.HrInterviewQuestionRepository;
import com.kaidi.oa.repository.HrRecruitCandidateRepository;
import com.kaidi.oa.repository.HrRecruitJobRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.time.LocalDate;
/**
* 行政·综合部——招聘与人才管理 + AI文档初稿 演示种子。
*
* 幂等:各表已有数据则跳过。
* 覆盖:
* - HrRecruitJob 3 条岗位(招聘中/面试中/已录用)
* - HrRecruitCandidate 6 条候选人(多状态,含 AI 评分)
* - HrInterviewQuestion 5 条面试题(AI 生成标记)
* - HrAiDocDraft 4 条 AI 生成文档(入职资料/岗位说明/员工手册/转正考核,多状态)
*/
@Component
@Order(203)
public class HrRecruitSeeder implements CommandLineRunner {
private final HrRecruitJobRepository jobRepo;
private final HrRecruitCandidateRepository candidateRepo;
private final HrInterviewQuestionRepository questionRepo;
private final HrAiDocDraftRepository aiDocRepo;
public HrRecruitSeeder(HrRecruitJobRepository jobRepo,
HrRecruitCandidateRepository candidateRepo,
HrInterviewQuestionRepository questionRepo,
HrAiDocDraftRepository aiDocRepo) {
this.jobRepo = jobRepo;
this.candidateRepo = candidateRepo;
this.questionRepo = questionRepo;
this.aiDocRepo = aiDocRepo;
}
@Override
public void run(String... args) {
seedJobs();
seedAiDocs();
}
// ============ 岗位 + 候选人 ============
private void seedJobs() {
if (jobRepo.count() > 0) return;
// Job 1:高级工程师(招聘中)
HrRecruitJob j1 = new HrRecruitJob();
j1.setPosition("高级结构工程师");
j1.setDept("工程技术部");
j1.setHeadcount(2);
j1.setChannel("猎聘");
j1.setRequirement("注册结构工程师,BIM,超高层,地下工程,CAD,10年以上");
j1.setSalaryRange("25k-40k");
j1.setTargetDate(LocalDate.now().plusMonths(1).toString());
j1.setStatus("招聘中");
j1.setHrOwner("王芳");
j1.setRemark("紧急招募,优先有超高层经验");
j1.setCreatedAt(Instant.now().minusSeconds(86400 * 20));
j1.setUpdatedAt(Instant.now().minusSeconds(86400 * 5));
j1 = jobRepo.save(j1);
// Job 2:项目经理(面试中)
HrRecruitJob j2 = new HrRecruitJob();
j2.setPosition("项目经理(EPC");
j2.setDept("项目管理部");
j2.setHeadcount(1);
j2.setChannel("BOSS直聘");
j2.setRequirement("一级建造师,EPC总承包,项目全周期管理,甲方协调,8年以上");
j2.setSalaryRange("30k-45k");
j2.setTargetDate(LocalDate.now().plusWeeks(3).toString());
j2.setStatus("面试中");
j2.setHrOwner("李娜");
j2.setRemark("要求有大型市政工程经验");
j2.setCreatedAt(Instant.now().minusSeconds(86400 * 30));
j2.setUpdatedAt(Instant.now().minusSeconds(86400 * 3));
j2 = jobRepo.save(j2);
// Job 3:财务主管(已录用)
HrRecruitJob j3 = new HrRecruitJob();
j3.setPosition("财务主管");
j3.setDept("财务中心");
j3.setHeadcount(1);
j3.setChannel("内部推荐");
j3.setRequirement("注册会计师,ERP系统,财务分析,税务筹划,5年以上");
j3.setSalaryRange("18k-25k");
j3.setTargetDate(LocalDate.now().minusWeeks(1).toString());
j3.setStatus("已录用");
j3.setHrOwner("王芳");
j3.setRemark("已完成录用,等待入职");
j3.setCreatedAt(Instant.now().minusSeconds(86400 * 45));
j3.setUpdatedAt(Instant.now().minusSeconds(86400 * 7));
j3 = jobRepo.save(j3);
// 候选人 for Job 1
addCandidate(j1.getId(), "张明远", "138****5201", "硕士", 12,
"注册结构工程师,超高层钢结构,BIM建模,CAD,地下工程,设计院15年",
75, "推荐(评分:75)。学历:硕士;工作年限:12年。", "初筛通过", "35k", "2026-06-25 10:00", "技术背景强,建议安排技术面试");
addCandidate(j1.getId(), "刘雪梅", "139****6302", "本科", 8,
"结构工程,CAD,AutoCAD,工程设计,住宅项目",
62, "推荐(评分:62)。学历:本科;工作年限:8年。", "面试安排", "28k", "2026-06-28 14:00", "");
addCandidate(j1.getId(), "赵国强", "136****4403", "大专", 5,
"辅助设计,AutoCAD,钢筋翻样",
51, "不推荐(评分:51)。学历:大专;工作年限:5年。", "已淘汰", "20k", null, "学历和经验不满足要求");
// 候选人 for Job 2
addCandidate(j2.getId(), "陈志远", "137****7504", "本科", 10,
"一级建造师,EPC总承包,项目全周期,市政工程,甲方协调,合同管理",
88, "强烈推荐(评分:88)。学历:本科;工作年限:10年。", "面试通过", "40k", "2026-06-20 09:00", "各项能力突出,建议发Offer");
addCandidate(j2.getId(), "孙丽华", "135****8605", "硕士", 7,
"项目管理,EPC,一建,工程合同,进度管理",
80, "强烈推荐(评分:80)。学历:硕士;工作年限:7年。", "offer发出", "38k", "2026-06-18 14:00", "综合素质优秀");
// 候选人 for Job 3(已入职)
addCandidate(j3.getId(), "吴美玲", "132****9706", "本科", 6,
"注册会计师,ERP财务模块,财务分析,税务筹划,预算管理",
90, "强烈推荐(评分:90)。学历:本科;工作年限:6年。", "已入职", "22k", "2026-06-10 10:00", "已正式入职,表现优异");
// 面试题(for Job 1AI生成)
if (questionRepo.count() == 0) {
addQuestion(j1.getId(), "高级结构工程师", "技能考察",
"请描述您在超高层建筑钢结构设计中遇到的最大技术挑战,以及您的解决方案。",
"考察实际技术经验;期望看到:抗风抗震计算、复杂节点处理、BIM协同经验。",
"高级");
addQuestion(j1.getId(), "高级结构工程师", "面试话术",
"请介绍您主导过的最大单体项目,包括规模、难点和您的贡献。",
"关注项目规模、复杂度与个人贡献比重;是否有超过5万平方米单体。",
"中级");
addQuestion(j2.getId(), "项目经理(EPC", "情景题",
"假设EPC项目在施工过程中甲方提出重大变更,可能导致工期延误3个月,您如何应对?",
"考察变更管理和谈判能力;期望看到:书面确认→工期/费用影响评估→变更谈判→合同签署。",
"高级");
addQuestion(j2.getId(), "项目经理(EPC", "综合素质",
"您如何处理团队内部成员工作风格冲突的问题?请举例说明。",
"考察领导力与团队管理;期望看到积极沟通、明确责任、结果导向。",
"中级");
addQuestion(j3.getId(), "财务主管", "笔试题",
"请说明税务筹划与逃税的本质区别,并举例说明合法的增值税筹划方法。",
"考察税务专业知识合规意识;期望看到清晰区分合规边界,举例一般纳税人优惠政策。",
"高级");
}
}
private void addCandidate(Long jobId, String name, String phone, String edu, int years,
String summary, int score, String aiReport,
String status, String salary, String interviewTime, String note) {
HrRecruitCandidate c = new HrRecruitCandidate();
c.setJobId(jobId);
c.setName(name);
c.setPhone(phone);
c.setEducation(edu);
c.setWorkYears(years);
c.setResumeSummary(summary);
c.setAiScore(score);
c.setAiReport(aiReport);
c.setStatus(status);
c.setExpectedSalary(salary);
c.setInterviewTime(interviewTime);
c.setInterviewNote(note);
c.setCreatedAt(Instant.now().minusSeconds(86400 * (long)(Math.random() * 15 + 1)));
c.setUpdatedAt(Instant.now().minusSeconds(3600));
candidateRepo.save(c);
}
private void addQuestion(Long jobId, String position, String qType,
String content, String refAnswer, String difficulty) {
HrInterviewQuestion q = new HrInterviewQuestion();
q.setJobId(jobId);
q.setPosition(position);
q.setQuestionType(qType);
q.setContent(content);
q.setReferenceAnswer(refAnswer);
q.setAiGenerated(true);
q.setDifficulty(difficulty);
q.setStatus("启用");
q.setCreator("AI系统");
q.setCreatedAt(Instant.now().minusSeconds(86400 * 10));
questionRepo.save(q);
}
// ============ AI 文档初稿 ============
private void seedAiDocs() {
if (aiDocRepo.count() > 0) return;
// 1. 入职资料(已发布)
HrAiDocDraft d1 = new HrAiDocDraft();
d1.setDocType("入职资料");
d1.setRelatedPosition(null);
d1.setRelatedDept("工程技术部");
d1.setTitle("工程技术部 新员工入职资料包(AI初稿)");
d1.setContent("# 工程技术部 新员工入职资料包\n\n"
+ "> AI生成初稿,已经HR审核。\n\n"
+ "## 一、欢迎信\n\n"
+ "亲爱的新同事,\n\n热烈欢迎您加入凯迪科技工程技术部大家庭!\n\n"
+ "## 二、报到须知\n\n"
+ "**报到时间**:请于入职通知书上注明的日期 09:00 前到达人力资源部。\n\n"
+ "**携带材料**:身份证、学历证书、学位证书、离职证明、近期照片 2 张、银行卡。\n\n"
+ "## 三、专属材料\n\n"
+ "技术部员工还需提供:职业资格证书(建造师/结构工程师等)原件复印件。\n\n"
+ "## 四、入职第一周\n\n"
+ "- Day1:行政手续 + 系统开通\n- Day2:公司文化宣贯\n"
+ "- Day3-4:技术规范学习 + 导师结对\n- Day5:与主管沟通试用期目标\n\n"
+ "---\n*由 AI 辅助生成,HR 已审核。*");
d1.setGenContext("类型=入职资料 岗位= 部门=工程技术部 背景=职业资格证书");
d1.setAiGenerated(true);
d1.setStatus("已发布");
d1.setReviewNote("内容准确,补充了专业证书要求,审核通过");
d1.setOperator("HR-王芳");
d1.setCreatedAt(Instant.now().minusSeconds(86400 * 30));
d1.setUpdatedAt(Instant.now().minusSeconds(86400 * 25));
aiDocRepo.save(d1);
// 2. 岗位说明(已审核)
HrAiDocDraft d2 = new HrAiDocDraft();
d2.setDocType("岗位说明");
d2.setRelatedPosition("高级结构工程师");
d2.setRelatedDept("工程技术部");
d2.setTitle("高级结构工程师 岗位说明书(AI初稿)");
d2.setContent("# 高级结构工程师 岗位说明书\n\n"
+ "> AI生成初稿,已审核待发布。\n\n"
+ "## 一、基本信息\n\n"
+ "| 项目 | 内容 |\n| ---- | ---- |\n"
+ "| 岗位名称 | 高级结构工程师 |\n"
+ "| 所属部门 | 工程技术部 |\n"
+ "| 直接上级 | 技术总工 |\n"
+ "| 岗位类别 | 专业技术类 |\n"
+ "| 编制人数 | 2 |\n\n"
+ "## 二、岗位职责\n\n"
+ "1. 负责大型建筑结构设计、审核与优化\n"
+ "2. 参与超高层/复杂结构的技术攻关\n"
+ "3. 指导初中级工程师完成设计任务\n"
+ "4. 负责结构设计图纸的质量把控\n"
+ "5. 参与投标技术方案编写\n\n"
+ "## 三、任职资格\n\n"
+ "**学历**:本科及以上,土木/结构工程专业\n\n"
+ "**证书**:注册结构工程师(一级)优先\n\n"
+ "**经验**:10年以上相关经验,有超高层项目经验\n\n"
+ "**技能**BIM、CAD、SAP2000/PKPM、Revit\n\n"
+ "---\n*由 AI 辅助生成,已审核待签发。*");
d2.setGenContext("类型=岗位说明 岗位=高级结构工程师 部门=工程技术部 背景=注册结构工程师,BIM,超高层");
d2.setAiGenerated(true);
d2.setStatus("已审核");
d2.setReviewNote("内容基本准确,已补充证书要求,待部门负责人签发");
d2.setOperator("HR-李娜");
d2.setCreatedAt(Instant.now().minusSeconds(86400 * 15));
d2.setUpdatedAt(Instant.now().minusSeconds(86400 * 10));
aiDocRepo.save(d2);
// 3. 员工手册(草稿)
HrAiDocDraft d3 = new HrAiDocDraft();
d3.setDocType("员工手册");
d3.setRelatedPosition(null);
d3.setRelatedDept("凯迪科技有限公司");
d3.setTitle("凯迪科技有限公司 员工手册(AI初稿·2026版)");
d3.setContent("# 凯迪科技有限公司 员工手册\n\n"
+ "> AI生成草稿,待法务及HR审核。\n\n"
+ "## 第一章 公司简介\n\n"
+ "凯迪科技有限公司是一家专业从事工程技术、建设管理及 ERP 系统集成的企业,"
+ "秉承「诚信、专业、创新」的核心价值观。\n\n"
+ "## 第二章 行为规范\n\n"
+ "- 诚实守信,不弄虚作假\n"
+ "- 尊重同事,维护良好工作氛围\n"
+ "- 保守公司机密,不泄露商业信息\n\n"
+ "## 第三章 考勤制度\n\n"
+ "- 工作时间:周一至周五 09:00-18:00\n"
+ "- 打卡方式:APP/指纹识别\n"
+ "- 迟到:15分钟以上计考勤异常\n\n"
+ "## 第四章 假期\n\n"
+ "| 假期 | 时长 |\n| ---- | ---- |\n"
+ "| 年假 | 5-15天 |\n| 病假 | 按医院证明 |\n| 婚假 | 3天 |\n"
+ "| 产假 | 按国家规定 |\n\n"
+ "## 第五章 薪酬福利\n\n"
+ "每月15日发薪;五险一金按规定缴纳;节日发放礼品/礼金。\n\n"
+ "---\n*由 AI 辅助生成,待法务审核。*");
d3.setGenContext("类型=员工手册 部门=凯迪科技有限公司");
d3.setAiGenerated(true);
d3.setStatus("草稿");
d3.setReviewNote(null);
d3.setOperator("HR-王芳");
d3.setCreatedAt(Instant.now().minusSeconds(86400 * 5));
d3.setUpdatedAt(Instant.now().minusSeconds(86400 * 5));
aiDocRepo.save(d3);
// 4. 转正考核(草稿)
HrAiDocDraft d4 = new HrAiDocDraft();
d4.setDocType("转正考核");
d4.setRelatedPosition("财务主管");
d4.setRelatedDept("财务中心");
d4.setTitle("财务主管 转正考核表(AI初稿)");
d4.setContent("# 财务主管 转正考核表\n\n"
+ "> AI生成草稿,请直属主管填写评分。\n\n"
+ "## 基本信息\n\n"
+ "| 项目 | 内容 |\n| ---- | ---- |\n"
+ "| 员工姓名 | 吴美玲 |\n"
+ "| 岗位 | 财务主管 |\n"
+ "| 入职日期 | 2026-06-10 |\n"
+ "| 试用期截止 | 2026-09-10 |\n"
+ "| 直属主管 | 财务总监 |\n\n"
+ "## 试用期目标\n\n"
+ "| 目标 | 计划完成 | 实际完成 | 完成率 |\n"
+ "| ---- | ---- | ---- | ---- |\n"
+ "| 完成岗位培训 | 入职第1周 | | |\n"
+ "| 接管月度账务 | 入职第1月 | | |\n"
+ "| 独立完成税务申报 | 入职第2月 | | |\n"
+ "| 完成半年财务分析 | 试用期末 | | |\n\n"
+ "## 综合评分\n\n"
+ "| 维度 | 权重 | 得分 |\n"
+ "| ---- | ---- | ---- |\n"
+ "| 专业能力 | 40% | |\n"
+ "| 工作态度 | 20% | |\n"
+ "| 团队协作 | 20% | |\n"
+ "| 执行力 | 20% | |\n\n"
+ "**转正建议**:[ ] 正式转正 [ ] 延长 [ ] 不予转正\n\n"
+ "---\n*由 AI 辅助生成,待主管填写。*");
d4.setGenContext("类型=转正考核 岗位=财务主管 部门=财务中心");
d4.setAiGenerated(true);
d4.setStatus("草稿");
d4.setReviewNote(null);
d4.setOperator("HR-李娜");
d4.setCreatedAt(Instant.now().minusSeconds(86400 * 3));
d4.setUpdatedAt(Instant.now().minusSeconds(86400 * 3));
aiDocRepo.save(d4);
}
}
@@ -0,0 +1,159 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.Blog;
import com.kaidi.oa.domain.CollabDoc;
import com.kaidi.oa.domain.Discussion;
import com.kaidi.oa.domain.Favorite;
import com.kaidi.oa.domain.FormInstance;
import com.kaidi.oa.domain.Message;
import com.kaidi.oa.domain.ScheduleEvent;
import com.kaidi.oa.domain.SysUser;
import com.kaidi.oa.domain.WorkPlan;
import com.kaidi.oa.repository.BlogRepository;
import com.kaidi.oa.repository.CollabDocRepository;
import com.kaidi.oa.repository.DiscussionRepository;
import com.kaidi.oa.repository.FavoriteRepository;
import com.kaidi.oa.repository.FormInstanceRepository;
import com.kaidi.oa.repository.MessageRepository;
import com.kaidi.oa.repository.ScheduleEventRepository;
import com.kaidi.oa.repository.StoredFileRepository;
import com.kaidi.oa.repository.SysUserRepository;
import com.kaidi.oa.repository.WorkPlanRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* 一次性身份回填:把历史数据里"按显示名落库的归属/作者/上传者/收件人/发起人"补成稳定的
* {@link SysUser#getId()}(对象级授权从此按 userId 比对,根治改名重用冒充——见
* {@code AuthorizationService.isOwner})。
*
* <p>幂等且非破坏:只填 {@code *Id} 为空的旧行,且只在显示名能唯一解析到一个用户时才回填;
* 显示名重名(理论上唯一约束下不会有)或在用户表中不存在(纯 demo 数据,如 张伟/李娜 这类不是
* 真实账号的署名)则保持为空——这类对象退回按显示名比对的兜底口径,仍只有 ADMIN 可管理。
*
* <p>{@link Order} 设为最低优先级,确保在 {@code DataSeeder(@Order 1)} /
* {@code OrgEnrichmentSeeder(@Order 2)} 等播种器之后运行,从而对它们刚播下的种子数据也完成回填。
*/
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class IdentityBackfillRunner implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(IdentityBackfillRunner.class);
private final SysUserRepository userRepo;
private final FavoriteRepository favRepo;
private final ScheduleEventRepository scheduleRepo;
private final WorkPlanRepository workPlanRepo;
private final CollabDocRepository collabDocRepo;
private final DiscussionRepository discussionRepo;
private final StoredFileRepository fileRepo;
private final MessageRepository messageRepo;
private final FormInstanceRepository instanceRepo;
private final BlogRepository blogRepo;
public IdentityBackfillRunner(SysUserRepository userRepo, FavoriteRepository favRepo,
ScheduleEventRepository scheduleRepo, WorkPlanRepository workPlanRepo,
CollabDocRepository collabDocRepo, DiscussionRepository discussionRepo,
StoredFileRepository fileRepo, MessageRepository messageRepo,
FormInstanceRepository instanceRepo, BlogRepository blogRepo) {
this.userRepo = userRepo;
this.favRepo = favRepo;
this.scheduleRepo = scheduleRepo;
this.workPlanRepo = workPlanRepo;
this.collabDocRepo = collabDocRepo;
this.discussionRepo = discussionRepo;
this.fileRepo = fileRepo;
this.messageRepo = messageRepo;
this.instanceRepo = instanceRepo;
this.blogRepo = blogRepo;
}
@Override
@Transactional
public void run(String... args) {
// displayName -> userId,只保留唯一名(重名一律不进表,避免把对象误绑到错误的人)。
Map<String, Long> idByName = new HashMap<>();
Set<String> ambiguous = new HashSet<>();
for (SysUser u : userRepo.findAll()) {
String dn = u.getDisplayName();
if (dn == null || dn.isBlank()) {
continue;
}
if (idByName.containsKey(dn)) {
ambiguous.add(dn);
} else {
idByName.put(dn, u.getId());
}
}
for (String dup : ambiguous) {
idByName.remove(dup);
}
if (idByName.isEmpty()) {
return;
}
int filled = 0;
filled += backfill(favRepo.findAll(), Favorite::getOwnerId, Favorite::getOwner,
Favorite::setOwnerId, favRepo::saveAll, idByName);
filled += backfill(scheduleRepo.findAll(), ScheduleEvent::getOwnerId, ScheduleEvent::getOwner,
ScheduleEvent::setOwnerId, scheduleRepo::saveAll, idByName);
filled += backfill(workPlanRepo.findAll(), WorkPlan::getOwnerId, WorkPlan::getOwner,
WorkPlan::setOwnerId, workPlanRepo::saveAll, idByName);
filled += backfill(collabDocRepo.findAll(), CollabDoc::getOwnerId, CollabDoc::getOwner,
CollabDoc::setOwnerId, collabDocRepo::saveAll, idByName);
filled += backfill(discussionRepo.findAll(), Discussion::getAuthorId, Discussion::getAuthor,
Discussion::setAuthorId, discussionRepo::saveAll, idByName);
filled += backfill(blogRepo.findAll(), Blog::getAuthorId, Blog::getAuthor,
Blog::setAuthorId, blogRepo::saveAll, idByName);
// StoredFile 含 byte[] data 大字段(非懒加载):用 @Modifying 定向批更,绝不 findAll 整表
// 把 BLOB 读进堆内存、也无写放大。每个唯一显示名一条 UPDATE,仅命中 uploaderId 为空的旧行。
for (Map.Entry<String, Long> e : idByName.entrySet()) {
filled += fileRepo.backfillUploaderId(e.getKey(), e.getValue());
}
filled += backfill(messageRepo.findAll(), Message::getRecipientId, Message::getRecipient,
Message::setRecipientId, messageRepo::saveAll, idByName);
filled += backfill(instanceRepo.findAll(), FormInstance::getOriginUserId, FormInstance::getOriginUser,
FormInstance::setOriginUserId, instanceRepo::saveAll, idByName);
if (filled > 0) {
log.info("Identity backfill: bound {} legacy rows to stable userId.", filled);
}
}
/**
* 对一张表回填:跳过 id 已存在的行;名字能唯一解析则写入 id 并收集;最后批量保存。返回回填行数。
*/
private <T> int backfill(List<T> rows, Function<T, Long> getId, Function<T, String> getName,
BiConsumer<T, Long> setId, Consumer<List<T>> save, Map<String, Long> idByName) {
List<T> changed = new ArrayList<>();
for (T row : rows) {
if (getId.apply(row) != null) {
continue;
}
Long uid = idByName.get(getName.apply(row));
if (uid != null) {
setId.accept(row, uid);
changed.add(row);
}
}
if (!changed.isEmpty()) {
save.accept(changed);
}
return changed.size();
}
}
@@ -0,0 +1,382 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.ItAsset;
import com.kaidi.oa.domain.ItAssetEvent;
import com.kaidi.oa.domain.ItInspection;
import com.kaidi.oa.domain.ItMonitorItem;
import com.kaidi.oa.domain.ItNetworkDevice;
import com.kaidi.oa.domain.ItSecPolicyConfig;
import com.kaidi.oa.repository.ItAssetEventRepository;
import com.kaidi.oa.repository.ItAssetRepository;
import com.kaidi.oa.repository.ItInspectionRepository;
import com.kaidi.oa.repository.ItMonitorItemRepository;
import com.kaidi.oa.repository.ItNetworkDeviceRepository;
import com.kaidi.oa.repository.ItSecPolicyConfigRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
/**
* 信息部演示数据Seeder:补全监控项/巡检读数/网络设备/安全策略零数据演示缺口。
* @Order(204) 在所有主数据Seeder后运行,幂等(count > 0 则跳过)。
*/
@Order(204)
@Component
public class ItDeptDemoSeeder implements ApplicationRunner {
private final ItMonitorItemRepository monitorRepo;
private final ItInspectionRepository inspectionRepo;
private final ItNetworkDeviceRepository networkRepo;
private final ItSecPolicyConfigRepository policyRepo;
private final ItAssetRepository assetRepo;
private final ItAssetEventRepository assetEventRepo;
public ItDeptDemoSeeder(ItMonitorItemRepository monitorRepo,
ItInspectionRepository inspectionRepo,
ItNetworkDeviceRepository networkRepo,
ItSecPolicyConfigRepository policyRepo,
ItAssetRepository assetRepo,
ItAssetEventRepository assetEventRepo) {
this.monitorRepo = monitorRepo;
this.inspectionRepo = inspectionRepo;
this.networkRepo = networkRepo;
this.policyRepo = policyRepo;
this.assetRepo = assetRepo;
this.assetEventRepo = assetEventRepo;
}
@Override
public void run(ApplicationArguments args) {
seedMonitorItems();
seedNetworkDevices();
seedSecPolicies();
seedAssetAmounts();
seedAssetEvents();
}
// -------- 监控项 + 巡检读数 --------
private void seedMonitorItems() {
if (monitorRepo.count() > 0) return;
// ERP应用服务器 CPU
ItMonitorItem m1 = monitor("ERP应用服务器", "CPU使用率", "%", "above", 75.0, 90.0, "张伟");
addInspection(m1, 45.2, "正常", false, "张伟", "-3");
addInspection(m1, 68.5, "正常", false, "张伟", "-2");
addInspection(m1, 82.1, "告警", true, "张伟", "-1");
addInspection(m1, 55.3, "正常", false, "张伟", "0");
// ERP应用服务器 内存
ItMonitorItem m2 = monitor("ERP应用服务器", "内存使用率", "%", "above", 80.0, 92.0, "张伟");
addInspection(m2, 71.0, "正常", false, "系统", "-2");
addInspection(m2, 88.5, "告警", true, "系统", "-1");
addInspection(m2, 93.2, "故障", true, "系统", "0");
// 数据库主库
ItMonitorItem m3 = monitor("数据库主库", "磁盘使用率", "%", "above", 70.0, 85.0, "李娜");
addInspection(m3, 52.3, "正常", false, "李娜", "-5");
addInspection(m3, 65.7, "正常", false, "李娜", "-2");
addInspection(m3, 73.4, "告警", true, "李娜", "0");
// 数据库连接数
ItMonitorItem m4 = monitor("数据库主库", "连接数", "", "above", 150.0, 200.0, "李娜");
addInspection(m4, 87.0, "正常", false, "李娜", "-3");
addInspection(m4, 134.0, "正常", false, "李娜", "-1");
addInspection(m4, 112.0, "正常", false, "李娜", "0");
// OA服务响应时间
ItMonitorItem m5 = monitor("OA应用系统", "响应时间", "ms", "above", 500.0, 2000.0, "陈静");
addInspection(m5, 235.0, "正常", false, "系统", "-4");
addInspection(m5, 312.0, "正常", false, "系统", "-2");
addInspection(m5, 648.0, "告警", true, "系统", "0");
// 核心交换机 网络流量
ItMonitorItem m6 = monitor("核心交换机", "网络流量", "Mbps", "above", 800.0, 950.0, "张伟");
addInspection(m6, 452.0, "正常", false, "张伟", "-6");
addInspection(m6, 623.0, "正常", false, "张伟", "-3");
addInspection(m6, 712.0, "正常", false, "张伟", "0");
// 存储阵列 可用空间
ItMonitorItem m7 = monitor("存储阵列", "可用空间率", "%", "below", 20.0, 10.0, "李娜");
addInspection(m7, 45.0, "正常", false, "李娜", "-7");
addInspection(m7, 28.5, "正常", false, "李娜", "-3");
addInspection(m7, 18.2, "告警", true, "李娜", "0");
}
private ItMonitorItem monitor(String target, String metric, String unit,
String direction, Double warn, Double crit, String owner) {
ItMonitorItem m = new ItMonitorItem();
m.setTarget(target);
m.setMetric(metric);
m.setUnit(unit);
m.setDirection(direction);
m.setWarnThreshold(warn);
m.setCritThreshold(crit);
m.setOwner(owner);
m.setEnabled(Boolean.TRUE);
m.setLastStatus("未采集");
m.setCreatedAt(Instant.now());
return monitorRepo.save(m);
}
private void addInspection(ItMonitorItem m, double value, String result,
boolean alerted, String inspector, String daysAgo) {
int days = Integer.parseInt(daysAgo);
LocalDate date = LocalDate.now().plusDays(days);
ItInspection ins = new ItInspection();
ins.setMonitorItemId(m.getId());
ins.setTarget(m.getTarget());
ins.setMetric(m.getMetric());
ins.setValue(value);
ins.setResult(result);
ins.setAlerted(alerted);
ins.setInspector(inspector);
ins.setCheckDate(date.toString());
ins.setCheckedAt(date.atStartOfDay(ZoneOffset.UTC).toInstant());
ins.setCreatedAt(Instant.now());
ItInspection saved = inspectionRepo.save(ins);
// 回写监控项最近状态
m.setLastValue(value);
m.setLastStatus(result);
m.setLastCheckAt(saved.getCheckedAt());
monitorRepo.save(m);
}
// -------- 网络设备 --------
private void seedNetworkDevices() {
if (networkRepo.count() > 0) return;
netdev("深信服AF-2000防火墙", "防火墙", "深信服 AF-2000",
"192.168.1.1", "全网", "机房A", "V5.5.0.6", "张伟",
"2025-01-20", "2028-01-20", "VLAN10-管理区/VLAN20-业务区/VLAN30-DMZ区;安全策略48条", "正常", "2026-05-15");
netdev("华为S5720核心交换机", "交换机", "华为 S5720-52X",
"192.168.1.2", "VLAN10/20/30/40", "机房A", "V200R020C10", "张伟",
"2024-06-15", "2027-06-15", "24口PoE+4个SFP+上行口;VLAN间路由已配置", "正常", "2026-05-20");
netdev("华为S5700接入交换机-1F", "交换机", "华为 S5700-28X",
"192.168.1.3", "VLAN20/40", "1楼弱电箱", "V200R019C10", "李娜",
"2024-08-10", "2027-08-10", "负责1楼24个信息点接入", "正常", "2026-04-18");
netdev("深信服VPN网关", "VPN网关", "深信服 SSL-VPN 2000",
"192.168.1.4", "VPN-Pool/10.8.0.0/24", "机房B", "V5.2.3", "张伟",
"2024-12-01", "2027-12-01", "IKEv2 AES-256-GCM/SHA2-256;在线用户峰值50人", "正常", "2026-06-01");
netdev("深信服IPS-2000入侵防御", "IDS/IPS", "深信服 IPS-2000",
"192.168.1.5", "旁路检测", "机房A", "IPS-V4.1.2", "张伟",
"2024-03-15", "2027-03-15", "特征库版本2026.06.01;每日自动更新;高风险告警即时阻断", "正常", "2026-06-01");
netdev("华为AR路由器", "路由器", "华为 AR3260",
"10.0.0.1", "外网/内网边界", "机房B", "V200R009C10", "张伟",
"2024-05-01", "2027-05-01", "双ISP链路,主链路电信100M,备链路联通50M", "正常", "2026-05-28");
netdev("360天擎防病毒服务器", "防病毒服务器", "360天擎 v4.5",
"192.168.1.10", "VLAN20/40", "机房A", "V4.5.11.0", "李娜",
"2023-09-01", "2026-09-01", "病毒库2026-06-15;全网200终端已接入", "正常", "2026-06-15");
netdev("SANGFOR上网行为管理", "上网行为管理", "深信服 AC-3000",
"192.168.1.6", "出口前", "机房A", "V12.0.73", "张伟",
"2023-11-10", "2026-11-10", "违规内容拦截/带宽管理/行为审计;日均报表已启用", "告警", null);
}
private void netdev(String name, String deviceType, String model,
String ip, String vlan, String location, String firmware,
String admin, String purchase, String warranty, String note,
String runStatus, String lastBackup) {
ItNetworkDevice d = new ItNetworkDevice();
d.setName(name);
d.setDeviceType(deviceType);
d.setModel(model);
d.setManagementIp(ip);
d.setVlanSegment(vlan);
d.setLocation(location);
d.setFirmwareVersion(firmware);
d.setAdmin(admin);
d.setPurchaseDate(purchase);
d.setWarrantyDate(warranty);
d.setNote(note);
d.setRunStatus(runStatus);
d.setConfigBackupStatus(lastBackup != null ? "已备份" : "未备份");
d.setLastConfigBackupDate(lastBackup);
d.setCreatedAt(Instant.now());
networkRepo.save(d);
}
// -------- 安全策略配置 --------
private void seedSecPolicies() {
if (policyRepo.count() > 0) return;
// VPN策略
policy(null, "深信服VPN网关", "VPN策略", "总部-远程访问VPN",
"VPN-001", "外网→内网", "任意", "192.168.0.0/16", "TCP/443,UDP/500-4500",
"允许", 1, "IKEv2/AES-256-GCM/SHA2-256;证书认证;MFA双因子;最大并发500用户");
policy(null, "深信服VPN网关", "VPN策略", "分公司点对点VPN",
"VPN-002", "分公司→总部", "10.2.0.0/24", "192.168.0.0/16", "ANY",
"允许", 2, "分公司岳阳(10.2.0.0/24)通过IPSEC VPN访问总部内网;预共享密钥AES-256");
// 防火墙规则
policy(null, "深信服AF-2000防火墙", "防火墙规则", "拒绝外网访问内网管理口",
"FW-001", "外网→内网", "ANY", "192.168.1.0/24", "TCP/22,TCP/23,TCP/3389",
"拒绝", 1, "安全基线:禁止外网直接SSH/Telnet/RDP到内网管理设备");
policy(null, "深信服AF-2000防火墙", "防火墙规则", "允许内网访问外网HTTP/HTTPS",
"FW-002", "内网→外网", "192.168.0.0/16", "ANY", "TCP/80,TCP/443",
"允许", 10, "员工上网;经上网行为管理过滤后允许");
policy(null, "深信服AF-2000防火墙", "防火墙规则", "DMZ区发布ERP对外服务",
"FW-003", "外网→DMZ", "ANY", "192.168.30.100", "TCP/443",
"允许", 5, "ERP系统移动端访问入口;仅开放HTTPS 443端口");
// IPS规则
policy(null, "深信服IPS-2000入侵防御", "IPS规则", "SQL注入攻击拦截",
"IPS-001", "双向", "ANY", "任意", "TCP/80,TCP/443,TCP/8080",
"阻断", 1, "特征库SQL注入检测;严格模式;每日告警统计");
policy(null, "深信服IPS-2000入侵防御", "IPS规则", "暴力破解攻击防护",
"IPS-002", "入站", "ANY", "任意", "TCP/22,TCP/3389,TCP/8090",
"阻断", 2, "5次/分钟触发阻断;IP封锁1小时;通知管理员");
policy(null, "深信服IPS-2000入侵防御", "IPS规则", "Web漏洞利用检测",
"IPS-003", "入站", "ANY", "任意", "TCP/80,TCP/443",
"告警", 3, "XSS/文件包含/目录遍历检测;告警模式记录日志");
// IDS特征
policy(null, "深信服IPS-2000入侵防御", "IDS特征", "异常流量基线检测",
"IDS-001", "双向", "内网", "内网", "ANY",
"告警", 1, "基于流量基线的异常检测:单IP流量超基线3倍触发告警");
// 上网行为
policy(null, "SANGFOR上网行为管理", "上网行为", "禁止访问非工作网站",
"AC-001", "内网→外网", "192.168.0.0/16", "ANY", "TCP/80,TCP/443",
"拒绝", 5, "工作时间(9:00-18:00)拦截娱乐/游戏/社交类网站;白名单放行业务系统");
policy(null, "SANGFOR上网行为管理", "上网行为", "带宽QoS策略",
"AC-002", "内网→外网", "192.168.0.0/16", "ANY", "ANY",
"允许", 10, "业务系统优先级最高;视频流媒体限速2Mbps/人;总出口100M按部门分配");
}
private void policy(Long deviceId, String deviceName, String policyType, String policyName,
String policyNo, String trafficDir, String srcAddr, String dstAddr,
String portProto, String policyAction, Integer priority, String remark) {
ItSecPolicyConfig c = new ItSecPolicyConfig();
c.setDeviceId(deviceId);
c.setDeviceName(deviceName);
c.setPolicyType(policyType);
c.setPolicyName(policyName);
c.setPolicyNo(policyNo);
c.setTrafficDir(trafficDir);
c.setSrcAddress(srcAddr);
c.setDstAddress(dstAddr);
c.setPortProto(portProto);
c.setPolicyAction(policyAction);
c.setEnabled(Boolean.TRUE);
c.setPriority(priority);
c.setRemark(remark);
c.setLastSyncDate(LocalDate.now().toString());
c.setOperator("张伟");
c.setCreatedAt(Instant.now());
policyRepo.save(c);
}
// -------- 回填IT资产采购金额(让折旧能算出真实金额)--------
private void seedAssetAmounts() {
// 为现有IT资产补全采购金额、折旧年限,让折旧端点能返回真实金额。
for (ItAsset a : assetRepo.findAll()) {
if (a.getPurchaseAmount() != null && a.getPurchaseAmount().compareTo(BigDecimal.ZERO) > 0) {
continue; // 已有金额跳过
}
// 根据类型设置参考采购金额
BigDecimal amount;
int depYears;
int residual;
switch (a.getCategory() != null ? a.getCategory() : "") {
case "服务器" -> { amount = new BigDecimal("55000.00"); depYears = 5; residual = 5; }
case "网络设备" -> { amount = new BigDecimal("18000.00"); depYears = 5; residual = 5; }
case "终端" -> { amount = new BigDecimal("8500.00"); depYears = 3; residual = 5; }
case "软件" -> { amount = new BigDecimal("25000.00"); depYears = 3; residual = 0; }
case "办公设备" -> { amount = new BigDecimal("6500.00"); depYears = 5; residual = 5; }
default -> { amount = new BigDecimal("5000.00"); depYears = 5; residual = 5; }
}
// 根据资产编号微调真实感
if ("IT-001".equals(a.getAssetNo())) amount = new BigDecimal("62800.00");
if ("IT-002".equals(a.getAssetNo())) amount = new BigDecimal("128000.00");
if ("IT-008".equals(a.getAssetNo())) amount = new BigDecimal("180000.00");
if ("IT-009".equals(a.getAssetNo())) amount = new BigDecimal("68000.00");
a.setPurchaseAmount(amount);
if (a.getDepreciationYears() == null) a.setDepreciationYears(depYears);
if (a.getResidualRatePct() == null) a.setResidualRatePct(residual);
if (a.getSerialNo() == null) {
a.setSerialNo("SN" + a.getAssetNo().replace("-", "") + "2024");
}
if (a.getBarcode() == null) {
a.setBarcode("BC" + a.getAssetNo().replace("-", ""));
}
assetRepo.save(a);
}
}
// -------- 资产生命周期事件演示数据 --------
private void seedAssetEvents() {
if (assetEventRepo.count() > 0) return;
// 为IT-007 (闲置惠普笔记本) 生成一条历史调拨记录
for (ItAsset a : assetRepo.findAll()) {
if (!"IT-007".equals(a.getAssetNo())) continue;
ItAssetEvent ev = new ItAssetEvent();
ev.setAssetId(a.getId());
ev.setAssetNo(a.getAssetNo());
ev.setAssetName(a.getName());
ev.setEventType("调拨");
ev.setEventNo("TR-20250618-0001");
ev.setFromHolder("设计研究中心");
ev.setToHolder("");
ev.setFromLocation("设计室");
ev.setToLocation("信息部库房");
ev.setReason("项目结束,设备归还信息部统一管理");
ev.setOperator("陈静");
ev.setStatus("已完成");
ev.setEventDate("2025-06-18");
ev.setCreatedAt(Instant.now());
assetEventRepo.save(ev);
break;
}
// 为IT-010 (维修复印机) 生成维修工单
for (ItAsset a : assetRepo.findAll()) {
if (!"IT-010".equals(a.getAssetNo())) continue;
ItAssetEvent ev = new ItAssetEvent();
ev.setAssetId(a.getId());
ev.setAssetNo(a.getAssetNo());
ev.setAssetName(a.getName());
ev.setEventType("维修");
ev.setEventNo("WO-20260608-0001");
ev.setFromHolder("行政部");
ev.setVendor("佳能维修服务中心(长沙)");
ev.setReason("定影器故障,打印出现竖向条纹");
ev.setExpectedReturnDate("2026-06-20");
ev.setOperator("李娜");
ev.setStatus("处理中");
ev.setEventDate("2026-06-08");
ev.setCreatedAt(Instant.now());
assetEventRepo.save(ev);
break;
}
// 为IT-011 (已报废爱普生投影仪) 生成报废记录
for (ItAsset a : assetRepo.findAll()) {
if (!"IT-011".equals(a.getAssetNo())) continue;
ItAssetEvent ev = new ItAssetEvent();
ev.setAssetId(a.getId());
ev.setAssetNo(a.getAssetNo());
ev.setAssetName(a.getName());
ev.setEventType("报废");
ev.setEventNo("SC-20260101-0001");
ev.setFromHolder("行政部");
ev.setReason("灯泡寿命耗尽,维修费用超过设备残值,申请报废");
ev.setOperator("陈静");
ev.setStatus("已完成");
ev.setResult("报废处置");
ev.setCompletedDate("2026-01-05");
ev.setEventDate("2026-01-02");
ev.setCreatedAt(Instant.now());
assetEventRepo.save(ev);
break;
}
}
}
@@ -0,0 +1,121 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.LabElnAccessLog;
import com.kaidi.oa.repository.LabElnAccessLogRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
/**
* 实验室 ELN / 报告读取访问日志演示数据(质量管理与合规 §6 审计追踪)。
*
* 覆盖全部动作类型(登录/ELN查看/ELN导出/报告查看/报告导出/原始数据查看/原始数据导出/审批查看),
* 涉及多名实验人员、多时间段,让 eln-audit.vue 活体展示完整审计轨迹。
*
* @Order(205) 在所有主数据 Seeder 之后、幂等(count > 0 则跳过)。
*/
@Component
@Order(205)
public class LabElnAccessLogSeeder implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(LabElnAccessLogSeeder.class);
private final LabElnAccessLogRepository repo;
public LabElnAccessLogSeeder(LabElnAccessLogRepository repo) {
this.repo = repo;
}
@Override
public void run(String... args) {
if (repo.count() > 0) {
return;
}
log.info("Seeding LabElnAccessLog demo data...");
Instant base = Instant.now().minus(7, ChronoUnit.DAYS);
// Day -7:登录审计
seed(null, null, "登录", "李化验", "质量检测部", "192.168.1.45",
"用户登录实验室系统", base);
seed(null, null, "登录", "王化验", "质量检测部", "192.168.1.47",
"用户登录实验室系统", base.plus(10, ChronoUnit.MINUTES));
seed(null, null, "登录", "陈检测", "质量检测部", "192.168.1.52",
"用户登录实验室系统", base.plus(25, ChronoUnit.MINUTES));
// Day -7ELN 查看
seed(1L, "ELN-001 COD分析实验记录", "ELN查看", "李化验", "质量检测部", "192.168.1.45",
"查看 ELN-001 COD分析实验记录", base.plus(30, ChronoUnit.MINUTES));
seed(2L, "ELN-002 氨氮检测实验记录", "ELN查看", "王化验", "质量检测部", "192.168.1.47",
"查看 ELN-002 氨氮检测实验记录", base.plus(35, ChronoUnit.MINUTES));
// Day -6ELN 导出
seed(1L, "ELN-001 COD分析实验记录", "ELN导出", "李化验", "质量检测部", "192.168.1.45",
"导出 ELN-001 PDF,用于报告附件", base.plus(1, ChronoUnit.DAYS));
seed(3L, "ELN-003 总磷TP检测记录", "ELN导出", "陈检测", "质量检测部", "192.168.1.52",
"导出 ELN-003 Excel,用于数据汇总", base.plus(1, ChronoUnit.DAYS).plus(2, ChronoUnit.HOURS));
// Day -5:报告查看
seed(null, "检测报告 JC-2026-05-001", "报告查看", "张主任", "质量管理部", "192.168.1.60",
"查看检测报告 JC-2026-05-001COD/氨氮汇总)", base.plus(2, ChronoUnit.DAYS));
seed(null, "检测报告 JC-2026-05-002", "报告查看", "王化验", "质量检测部", "192.168.1.47",
"查看检测报告 JC-2026-05-002(总磷/总氮)", base.plus(2, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS));
// Day -4:报告导出
seed(null, "检测报告 JC-2026-05-001", "报告导出", "张主任", "质量管理部", "192.168.1.60",
"导出报告 JC-2026-05-001 PDF(送客户)", base.plus(3, ChronoUnit.DAYS));
seed(null, "检测报告 JC-2026-05-003", "报告导出", "李化验", "质量检测部", "192.168.1.45",
"导出报告 JC-2026-05-003 PDF(归档)", base.plus(3, ChronoUnit.DAYS).plus(3, ChronoUnit.HOURS));
// Day -3:原始数据查看
seed(2L, "ELN-002 氨氮检测实验记录", "原始数据查看", "陈检测", "质量检测部", "192.168.1.52",
"查看 ELN-002 原始色谱数据", base.plus(4, ChronoUnit.DAYS));
seed(4L, "ELN-004 pH值检测记录", "原始数据查看", "李化验", "质量检测部", "192.168.1.45",
"查看 ELN-004 原始pH计读数", base.plus(4, ChronoUnit.DAYS).plus(2, ChronoUnit.HOURS));
// Day -2:原始数据导出
seed(2L, "ELN-002 氨氮检测实验记录", "原始数据导出", "陈检测", "质量检测部", "192.168.1.52",
"导出 ELN-002 原始数据CSV(归档备查)", base.plus(5, ChronoUnit.DAYS));
seed(5L, "ELN-005 悬浮物SS检测记录", "原始数据导出", "王化验", "质量检测部", "192.168.1.47",
"导出 ELN-005 原始数据ExcelOOS偏差调查)", base.plus(5, ChronoUnit.DAYS).plus(1, ChronoUnit.HOURS));
// Day -1:审批查看
seed(null, "审批流 RPT-APR-2026-06", "审批查看", "张主任", "质量管理部", "192.168.1.60",
"查看报告审批进度(检测人→复核人→批准人)", base.plus(6, ChronoUnit.DAYS));
seed(null, "审批流 RPT-APR-2026-07", "审批查看", "李主任", "质量管理部", "192.168.1.62",
"查看报告审批进度(已批准待签发)", base.plus(6, ChronoUnit.DAYS).plus(2, ChronoUnit.HOURS));
// 今日:各类操作
seed(null, null, "登录", "李化验", "质量检测部", "192.168.1.45",
"用户登录实验室系统", Instant.now().minus(2, ChronoUnit.HOURS));
seed(6L, "ELN-006 总氮TN检测记录", "ELN查看", "李化验", "质量检测部", "192.168.1.45",
"查看 ELN-006 总氮TN检测记录", Instant.now().minus(90, ChronoUnit.MINUTES));
seed(6L, "ELN-006 总氮TN检测记录", "ELN导出", "李化验", "质量检测部", "192.168.1.45",
"导出 ELN-006 PDF附于检测报告", Instant.now().minus(60, ChronoUnit.MINUTES));
log.info("LabElnAccessLog demo data seeded: {} records.", repo.count());
}
private void seed(Long elnId, String elnRef, String action,
String operator, String dept, String ip,
String detail, Instant occurredInstant) {
LabElnAccessLog r = new LabElnAccessLog();
r.setElnId(elnId);
r.setElnRef(elnRef);
r.setAction(action);
r.setOperator(operator);
r.setOperatorDept(dept);
r.setIp(ip);
r.setDetail(detail);
// 格式化操作时间字符串
java.time.LocalDateTime ldt = java.time.LocalDateTime.ofInstant(occurredInstant, java.time.ZoneId.systemDefault());
r.setOccurredAt(ldt.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
r.setCreatedAt(occurredInstant);
repo.save(r);
}
}
@@ -0,0 +1,145 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.TestTask;
import com.kaidi.oa.repository.TestTaskRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
/**
* 实验室检测任务看板演示数据。
* 覆盖全部状态列(待分配 / 检测中 / 待复核 / 已复核 / 偏差处理 / 已出报告),
* 让 labkanban.vue 活体展示多状态卡片。
* Order(8) = 在所有基础 Seeder 之后、FtsIndexRunner(200) 之前。
*/
@Component
@Order(8)
public class LabTestKanbanSeeder implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(LabTestKanbanSeeder.class);
private final TestTaskRepository repo;
public LabTestKanbanSeeder(TestTaskRepository repo) {
this.repo = repo;
}
@Override
public void run(String... args) {
if (repo.count() > 0) {
return; // 已有数据则跳过,幂等
}
log.info("Seeding LabTestKanban demo data...");
// 待分配
seed("JC-001", "COD 化学需氧量", "GB 11914-1989", "污水样-2026001", "城镇污水处理厂进水", "", "2026-06-20", "待分配", null, null, null, null);
seed("JC-002", "氨氮(NH3-N)", "HJ 535-2009", "污水样-2026002", "工业废水进水", "", "2026-06-20", "待分配", null, null, null, null);
// 检测中
seed("JC-003", "总磷(TP)", "GB 11893-1989", "污水样-2026003", "生物质肥料浸提液", "", "2026-06-18", "检测中", "李化验", null, null, null);
seed("JC-004", "pH 值", "GB/T 6920-1986", "废水样-2026004", "工业废水排放口", "", "2026-06-22", "检测中", "王化验", null, null, null);
seed("JC-005", "悬浮物(SS)", "GB 11901-1989", "污水样-2026005", "脱水污泥渗滤液", "", "2026-06-19", "检测中", "李化验", null, null, null);
// 待复核
TestTask t6 = build("JC-006", "总氮(TN)", "HJ 636-2012", "废水样-2026006", "环保设备出水", "", "2026-06-17", "待复核", "张检测");
t6.setResultValue("12.5");
t6.setResultUnit("mg/L");
t6.setStandardLimit("≤15 mg/L");
t6.setJudge("合格");
repo.save(t6);
TestTask t7 = build("JC-007", "总有机碳(TOC)", "HJ 501-2009", "污水样-2026007", "科研实验室废液", "", "2026-06-16", "待复核", "张检测");
t7.setResultValue("38.2");
t7.setResultUnit("mg/L");
t7.setStandardLimit("≤50 mg/L");
t7.setJudge("合格");
repo.save(t7);
// 偏差处理(OOS超标)
TestTask t8 = build("JC-008", "六价铬 Cr(VI)", "GB 7467-1987", "废水样-2026008", "镀件清洗废水", "", "2026-06-15", "偏差处理", "陈检测");
t8.setResultValue("0.85");
t8.setResultUnit("mg/L");
t8.setStandardLimit("≤0.5 mg/L");
t8.setJudge("OOS偏差");
t8.setRemark("实测值超标1.7倍,已启动偏差调查,通知车间排查污染源。");
repo.save(t8);
// 已复核
TestTask t9 = build("JC-009", "石油类", "HJ 637-2018", "废水样-2026009", "机加工切削液", "", "2026-06-14", "已复核", "李化验");
t9.setResultValue("4.2");
t9.setResultUnit("mg/L");
t9.setStandardLimit("≤5 mg/L");
t9.setJudge("合格");
t9.setReviewer("王主任");
t9.setReviewedAt("2026-06-14");
t9.setResultLocked(true);
repo.save(t9);
TestTask t10 = build("JC-010", "氟化物", "HJ 488-2009", "废水样-2026010", "酸洗废水处理站", "", "2026-06-13", "已复核", "陈检测");
t10.setResultValue("8.5");
t10.setResultUnit("mg/L");
t10.setStandardLimit("≤10 mg/L");
t10.setJudge("合格");
t10.setReviewer("张主任");
t10.setReviewedAt("2026-06-13");
t10.setResultLocked(true);
repo.save(t10);
// 已出报告
TestTask t11 = build("JC-011", "色度", "GB 11903-1989", "污水样-2026011", "城镇污水出水", "", "2026-06-12", "已出报告", "王化验");
t11.setResultValue("10");
t11.setResultUnit("");
t11.setStandardLimit("≤30 度");
t11.setJudge("合格");
t11.setReviewer("李主任");
t11.setReviewedAt("2026-06-12");
t11.setResultLocked(true);
repo.save(t11);
TestTask t12 = build("JC-012", "溶解氧(DO)", "HJ 506-2009", "污水样-2026012", "好氧池曝气段", "", "2026-06-11", "已出报告", "李化验");
t12.setResultValue("5.8");
t12.setResultUnit("mg/L");
t12.setStandardLimit("≥2 mg/L");
t12.setJudge("合格");
t12.setReviewer("王主任");
t12.setReviewedAt("2026-06-11");
t12.setResultLocked(true);
repo.save(t12);
log.info("LabTestKanban demo data seeded: {} records.", repo.count());
}
private void seed(String taskNo, String detectItem, String methodNo,
String sampleNo, String sampleName, String priority,
String dueDate, String status, String tester,
String resultValue, String resultUnit, String judge) {
TestTask t = build(taskNo, detectItem, methodNo, sampleNo, sampleName, priority, dueDate, status, tester);
if (resultValue != null) t.setResultValue(resultValue);
if (resultUnit != null) t.setResultUnit(resultUnit);
if (judge != null) t.setJudge(judge);
repo.save(t);
}
private TestTask build(String taskNo, String detectItem, String methodNo,
String sampleNo, String sampleName, String priority,
String dueDate, String status, String tester) {
TestTask t = new TestTask();
t.setTaskNo(taskNo);
t.setDetectItem(detectItem);
t.setMethodNo(methodNo);
t.setSampleNo(sampleNo);
t.setSampleName(sampleName);
t.setPriority(priority);
t.setDueDate(dueDate);
t.setStatus(status);
t.setTester(tester);
t.setResultLocked(false);
t.setResultSource("手动录入");
t.setCreatedAt(Instant.now());
return t;
}
}
@@ -0,0 +1,436 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.ComplianceAlertRule;
import com.kaidi.oa.domain.HrNonCompete;
import com.kaidi.oa.domain.LegalCreditInvest;
import com.kaidi.oa.domain.LitigationCase;
import com.kaidi.oa.repository.ComplianceAlertRuleRepository;
import com.kaidi.oa.repository.HrNonCompeteRepository;
import com.kaidi.oa.repository.LegalCreditInvestRepository;
import com.kaidi.oa.repository.LitigationCaseRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
/**
* 法务风险部演示数据播种器。
*
* 补完"活体返回 0 条/功能在但无数据演示"缺口:
* 1. LitigationCase — 诉讼/仲裁案件(跨 5 种阶段/3种结果)
* 2. HrNonCompete — 员工竞业限制(含离职补偿中/已违约/在职-未触发各状态)
* 3. ComplianceAlertRule — 合规预警规则库(覆盖采购/销售/合同/人事/财务触发场景)
* 4. LegalCreditInvest — 法务专项客户信用调查记录(含已结案驳回/进行中)
*
* @Order(20) 在 DataSeeder(@Order 1) 之后运行;幂等(count>0 则跳过)。
*/
@Order(20)
@Component
public class LegalDemoSeeder implements CommandLineRunner {
private final LitigationCaseRepository caseRepo;
private final HrNonCompeteRepository nonCompeteRepo;
private final ComplianceAlertRuleRepository ruleRepo;
private final LegalCreditInvestRepository investRepo;
public LegalDemoSeeder(LitigationCaseRepository caseRepo,
HrNonCompeteRepository nonCompeteRepo,
ComplianceAlertRuleRepository ruleRepo,
LegalCreditInvestRepository investRepo) {
this.caseRepo = caseRepo;
this.nonCompeteRepo = nonCompeteRepo;
this.ruleRepo = ruleRepo;
this.investRepo = investRepo;
}
@Override
public void run(String... args) throws Exception {
seedLitigationCases();
seedNonCompete();
seedComplianceAlertRules();
seedLegalCreditInvest();
}
// ========================================================================
// 1. 诉讼/仲裁案件
// ========================================================================
private void seedLitigationCases() {
if (caseRepo.count() > 0) return;
LitigationCase c1 = new LitigationCase();
c1.setCaseNo("(2024)粤0305民初2241号");
c1.setCaseName("凯迪科技诉深圳博拓工程有限公司合同纠纷案");
c1.setCause("建设工程施工合同纠纷");
c1.setOpponent("深圳博拓工程有限公司");
c1.setOurRole("原告");
c1.setTribunal("广州市天河区人民法院");
c1.setAmount(Money.of(2850000.0));
c1.setStage("开庭");
c1.setLawyer("陈建国");
c1.setLawFirm("广东广信君达律师事务所");
c1.setFilingDate("2024-03-15");
c1.setKeyDate("2025-08-20");
c1.setResult("审理中");
c1.setStatus("进行中");
c1.setRemark("被告拖欠工程款285万元,已提交完整证据清单,等待开庭。");
c1.setCreatedAt(Instant.now());
LitigationCase c2 = new LitigationCase();
c2.setCaseNo("(2024)粤03民终8876号");
c2.setCaseName("广州恒辉材料公司诉凯迪科技货款纠纷案");
c2.setCause("买卖合同纠纷");
c2.setOpponent("广州恒辉材料有限公司");
c2.setOurRole("被告");
c2.setTribunal("广州市中级人民法院");
c2.setAmount(Money.of(680000.0));
c2.setStage("已结案");
c2.setLawyer("李晓燕");
c2.setLawFirm("广东广信君达律师事务所");
c2.setFilingDate("2023-11-08");
c2.setKeyDate("2024-05-30");
c2.setResult("调解结案");
c2.setStatus("已结案");
c2.setRemark("已达成调解协议,支付货款52万元,余款免除,2024年6月结案。");
c2.setCreatedAt(Instant.now());
LitigationCase c3 = new LitigationCase();
c3.setCaseNo("(2025)粤0305仲字0421号");
c3.setCaseName("凯迪科技与华南水务集团技术服务合同仲裁案");
c3.setCause("技术服务合同纠纷");
c3.setOpponent("华南水务集团股份有限公司");
c3.setOurRole("申请人");
c3.setTribunal("广州仲裁委员会");
c3.setAmount(Money.of(4200000.0));
c3.setStage("举证");
c3.setLawyer("王志远");
c3.setLawFirm("北京金杜律师事务所广州分所");
c3.setFilingDate("2025-01-20");
c3.setKeyDate("2025-09-10");
c3.setResult("审理中");
c3.setStatus("进行中");
c3.setRemark("涉及420万技术服务费,对方以验收未通过为由拒付,正在提交技术鉴定报告。");
c3.setCreatedAt(Instant.now());
LitigationCase c4 = new LitigationCase();
c4.setCaseNo("(2023)粤0305劳人仲字3304号");
c4.setCaseName("王某某申请劳动仲裁案(竞业违约)");
c4.setCause("劳动合同纠纷·竞业限制违约");
c4.setOpponent("王某某");
c4.setOurRole("被申请人");
c4.setTribunal("广州市天河区劳动人事争议仲裁委员会");
c4.setAmount(Money.of(45000.0));
c4.setStage("已结案");
c4.setLawyer("陈建国");
c4.setLawFirm("广东广信君达律师事务所");
c4.setFilingDate("2023-06-12");
c4.setKeyDate("2023-09-28");
c4.setResult("胜诉");
c4.setStatus("已结案");
c4.setRemark("仲裁委裁决申请人主张不成立,公司竞业补偿支付行为合规,无需额外赔偿。");
c4.setCreatedAt(Instant.now());
LitigationCase c5 = new LitigationCase();
c5.setCaseNo("(2025)粤0305民初5012号");
c5.setCaseName("凯迪科技诉珠海绿科环保公司知识产权侵权案");
c5.setCause("侵害发明专利权纠纷");
c5.setOpponent("珠海绿科环保技术有限公司");
c5.setOurRole("原告");
c5.setTribunal("广州知识产权法院");
c5.setAmount(Money.of(1500000.0));
c5.setStage("立案");
c5.setLawyer("李晓燕");
c5.setLawFirm("广东广信君达律师事务所");
c5.setFilingDate("2025-05-08");
c5.setKeyDate("2025-11-15");
c5.setResult("审理中");
c5.setStatus("进行中");
c5.setRemark("对方产品疑似侵犯ZL201910329876.4号发明专利权,已完成初步鉴定,立案受理。");
c5.setCreatedAt(Instant.now());
caseRepo.save(c1);
caseRepo.save(c2);
caseRepo.save(c3);
caseRepo.save(c4);
caseRepo.save(c5);
}
// ========================================================================
// 2. 员工竞业限制
// ========================================================================
private void seedNonCompete() {
if (nonCompeteRepo.count() > 0) return;
HrNonCompete nc1 = new HrNonCompete();
nc1.setEmployeeId("EMP-2019-0088");
nc1.setEmployeeName("张伟强");
nc1.setDept("技术研发部");
nc1.setPosition("高级研发工程师");
nc1.setRestrictionPeriod(24);
nc1.setStartDate("2024-10-01");
nc1.setEndDate("2026-09-30");
nc1.setCompensationMonthly(Money.of(8500.0));
nc1.setRestrictedIndustries("同类环保设备制造/同类污水处理技术/竞争对手企业:华南水务集团、珠海绿科、深圳博瑞");
nc1.setStatus("离职-补偿中");
nc1.setSignedDate("2024-09-28");
nc1.setCreatedAt(Instant.now());
HrNonCompete nc2 = new HrNonCompete();
nc2.setEmployeeId("EMP-2021-0145");
nc2.setEmployeeName("刘雅婷");
nc2.setDept("销售部");
nc2.setPosition("高级销售经理");
nc2.setRestrictionPeriod(12);
nc2.setStartDate("2025-02-01");
nc2.setEndDate("2026-01-31");
nc2.setCompensationMonthly(Money.of(6000.0));
nc2.setRestrictedIndustries("同类水处理设备销售、ERP系统集成销售;不得接触原公司客户资源");
nc2.setStatus("离职-补偿中");
nc2.setSignedDate("2025-01-28");
nc2.setCreatedAt(Instant.now());
HrNonCompete nc3 = new HrNonCompete();
nc3.setEmployeeId("EMP-2018-0033");
nc3.setEmployeeName("王某某");
nc3.setDept("技术研发部");
nc3.setPosition("研发主任");
nc3.setRestrictionPeriod(18);
nc3.setStartDate("2023-05-01");
nc3.setEndDate("2024-10-31");
nc3.setCompensationMonthly(Money.of(12000.0));
nc3.setRestrictedIndustries("所有同类污水处理技术研发机构及制造企业");
nc3.setStatus("已违约");
nc3.setViolationNote("2023年9月发现当事人入职珠海绿科环保技术有限公司担任技术总监,已提起劳动仲裁并胜诉。");
nc3.setSignedDate("2023-04-25");
nc3.setCreatedAt(Instant.now());
HrNonCompete nc4 = new HrNonCompete();
nc4.setEmployeeId("EMP-2022-0201");
nc4.setEmployeeName("陈明慧");
nc4.setDept("财务部");
nc4.setPosition("财务经理");
nc4.setRestrictionPeriod(12);
nc4.setStartDate("");
nc4.setEndDate("");
nc4.setCompensationMonthly(Money.of(5000.0));
nc4.setRestrictedIndustries("同行业财务管理/同类ERP系统服务商");
nc4.setStatus("在职-未触发");
nc4.setSignedDate("2022-07-01");
nc4.setCreatedAt(Instant.now());
HrNonCompete nc5 = new HrNonCompete();
nc5.setEmployeeId("EMP-2020-0112");
nc5.setEmployeeName("周晨光");
nc5.setDept("市场部");
nc5.setPosition("市场总监");
nc5.setRestrictionPeriod(18);
nc5.setStartDate("2023-11-15");
nc5.setEndDate("2025-05-14");
nc5.setCompensationMonthly(Money.of(9500.0));
nc5.setRestrictedIndustries("水务行业市场推广,客户范围包括原有的50家签约客户");
nc5.setStatus("已到期");
nc5.setSignedDate("2023-11-10");
nc5.setCreatedAt(Instant.now());
nonCompeteRepo.save(nc1);
nonCompeteRepo.save(nc2);
nonCompeteRepo.save(nc3);
nonCompeteRepo.save(nc4);
nonCompeteRepo.save(nc5);
}
// ========================================================================
// 3. 合规预警规则库
// ========================================================================
private void seedComplianceAlertRules() {
if (ruleRepo.count() > 0) return;
ComplianceAlertRule r1 = new ComplianceAlertRule();
r1.setRuleCode("RULE-PROC-001");
r1.setRuleName("单笔采购超预算比例预警");
r1.setTriggerScene("采购");
r1.setCondition("单笔采购金额超过年度采购预算的30%");
r1.setThreshold("30");
r1.setAlertLevel("");
r1.setEnabled(Boolean.TRUE);
r1.setNotifyRoles("法务,合规专员,财务总监");
r1.setTriggerCount(3);
r1.setLastTriggeredAt("2025-04-20");
r1.setCreatedAt(Instant.now());
ComplianceAlertRule r2 = new ComplianceAlertRule();
r2.setRuleCode("RULE-SALE-001");
r2.setRuleName("销售合同客户涉诉风险预警");
r2.setTriggerScene("销售");
r2.setCondition("新签销售合同甲方在法务诉讼台账中存在未结案的被告案件");
r2.setThreshold("");
r2.setAlertLevel("");
r2.setEnabled(Boolean.TRUE);
r2.setNotifyRoles("法务,销售总监,总经理");
r2.setTriggerCount(1);
r2.setLastTriggeredAt("2025-03-12");
r2.setCreatedAt(Instant.now());
ComplianceAlertRule r3 = new ComplianceAlertRule();
r3.setRuleCode("RULE-CONTRACT-001");
r3.setRuleName("合同无授权审批预警");
r3.setTriggerScene("合同");
r3.setCondition("合同金额超出签署人授权限额,未经上级审批");
r3.setThreshold("500000");
r3.setAlertLevel("紧急");
r3.setEnabled(Boolean.TRUE);
r3.setNotifyRoles("法务,总经理,董事会秘书");
r3.setTriggerCount(0);
r3.setCreatedAt(Instant.now());
ComplianceAlertRule r4 = new ComplianceAlertRule();
r4.setRuleCode("RULE-HR-001");
r4.setRuleName("高岗位员工离职竞业限制触发预警");
r4.setTriggerScene("人事");
r4.setCondition("研发/销售/财务等关键岗位人员离职,竞业限制协议状态仍为「在职-未触发」");
r4.setThreshold("");
r4.setAlertLevel("");
r4.setEnabled(Boolean.TRUE);
r4.setNotifyRoles("法务,HR总监");
r4.setTriggerCount(2);
r4.setLastTriggeredAt("2025-02-28");
r4.setCreatedAt(Instant.now());
ComplianceAlertRule r5 = new ComplianceAlertRule();
r5.setRuleCode("RULE-FIN-001");
r5.setRuleName("大额资金异常转账预警");
r5.setTriggerScene("财务");
r5.setCondition("单笔对外付款超过200万元且无合同关联");
r5.setThreshold("2000000");
r5.setAlertLevel("紧急");
r5.setEnabled(Boolean.TRUE);
r5.setNotifyRoles("法务,财务总监,总经理");
r5.setTriggerCount(0);
r5.setCreatedAt(Instant.now());
ComplianceAlertRule r6 = new ComplianceAlertRule();
r6.setRuleCode("RULE-IP-001");
r6.setRuleName("知识产权年费到期未缴预警");
r6.setTriggerScene("知识产权");
r6.setCondition("专利年费缴纳截止日前30天,费用状态仍为待缴");
r6.setThreshold("30");
r6.setAlertLevel("");
r6.setEnabled(Boolean.TRUE);
r6.setNotifyRoles("法务,知识产权专员");
r6.setTriggerCount(5);
r6.setLastTriggeredAt("2025-05-31");
r6.setCreatedAt(Instant.now());
ComplianceAlertRule r7 = new ComplianceAlertRule();
r7.setRuleCode("RULE-PROC-002");
r7.setRuleName("制裁名单供应商交易预警");
r7.setTriggerScene("采购");
r7.setCondition("采购供应商名称出现在公司维护的制裁名单/黑名单中");
r7.setThreshold("");
r7.setAlertLevel("紧急");
r7.setEnabled(Boolean.TRUE);
r7.setNotifyRoles("法务,合规专员,采购总监");
r7.setTriggerCount(0);
r7.setCreatedAt(Instant.now());
ComplianceAlertRule r8 = new ComplianceAlertRule();
r8.setRuleCode("RULE-ENVI-001");
r8.setRuleName("环保违规风险预警");
r8.setTriggerScene("其他");
r8.setCondition("环保监测数据超标且无整改记录,可能产生行政处罚");
r8.setThreshold("");
r8.setAlertLevel("");
r8.setEnabled(Boolean.TRUE);
r8.setNotifyRoles("法务,安全环保部,总经理");
r8.setTriggerCount(1);
r8.setLastTriggeredAt("2024-12-15");
r8.setCreatedAt(Instant.now());
ruleRepo.save(r1);
ruleRepo.save(r2);
ruleRepo.save(r3);
ruleRepo.save(r4);
ruleRepo.save(r5);
ruleRepo.save(r6);
ruleRepo.save(r7);
ruleRepo.save(r8);
}
// ========================================================================
// 4. 法务专项客户信用调查记录
// ========================================================================
private void seedLegalCreditInvest() {
if (investRepo.count() > 0) return;
LegalCreditInvest inv1 = new LegalCreditInvest();
inv1.setCustomerName("华南水务集团股份有限公司");
inv1.setInvestigationType("综合调查");
inv1.setScope("工商信息、涉诉记录、行政处罚、失信被执行人");
inv1.setCourtRecord("存在3起历史诉讼,已全部结案,其中1起因工程款纠纷败诉");
inv1.setBlacklisted(false);
inv1.setLitigationHistory("2022年(2022)粤03民终4421号-货款纠纷(结案胜诉);\n2021年(2021)粤0305民初1183号-建工合同(调解结案)");
inv1.setCreditScore("AA");
inv1.setInvestigator("陈建国");
inv1.setTargetCompletionDate("2025-02-28");
inv1.setStatus("已结案");
inv1.setConclusion("通过");
inv1.setConclusionOpinion("整体信用状况良好,历史诉讼均已结案,无失信记录,可建立长期合作关系,建议合同金额不超过2000万元。");
inv1.setConclusionDate("2025-02-26");
inv1.setRemark("该客户为粤港澳大湾区重点国有水务企业,授信风险可控。");
inv1.setCreatedAt(Instant.now());
LegalCreditInvest inv2 = new LegalCreditInvest();
inv2.setCustomerName("珠海绿科环保技术有限公司");
inv2.setInvestigationType("诉讼记录核查");
inv2.setScope("全量涉诉记录及行政处罚记录");
inv2.setCourtRecord("存在5起正在进行的诉讼,其中2起为知识产权侵权指控");
inv2.setBlacklisted(false);
inv2.setLitigationHistory("(2025)粤0305民初5012号-专利侵权(进行中);\n(2024)粤03民终6601号-技术转让纠纷(进行中)");
inv2.setCreditScore("B");
inv2.setInvestigator("李晓燕");
inv2.setTargetCompletionDate("2025-06-15");
inv2.setStatus("已结案");
inv2.setConclusion("驳回");
inv2.setConclusionOpinion("该公司存在多起知识产权侵权诉讼记录,与我司涉及专利侵权案件,建议中止合作谈判,列入法务重点监控名单。");
inv2.setConclusionDate("2025-06-10");
inv2.setRemark("已通知销售部暂停与该公司的合作洽谈,销售合同系统已标记风险。");
inv2.setCreatedAt(Instant.now());
LegalCreditInvest inv3 = new LegalCreditInvest();
inv3.setCustomerName("深圳博拓工程有限公司");
inv3.setInvestigationType("失信被执行人核查");
inv3.setScope("最高人民法院失信被执行人名单、执行案件记录");
inv3.setCourtRecord("查询到2条失信被执行人记录,涉及2023年至今未履行的建工合同判决");
inv3.setBlacklisted(true);
inv3.setLitigationHistory("(2024)粤0305民初2241号-合同纠纷(进行中/我司原告);\n2023年被列入失信被执行人名单");
inv3.setCreditScore("D");
inv3.setInvestigator("陈建国");
inv3.setTargetCompletionDate("2025-04-30");
inv3.setStatus("已结案");
inv3.setConclusion("驳回");
inv3.setConclusionOpinion("该企业已被列入失信被执行人名单,建议冻结合作、追缴应收款,已向财务部出具法律意见书。");
inv3.setConclusionDate("2025-04-25");
inv3.setRemark("已同步供应商黑名单,采购系统已拦截该供应商新增采购订单。");
inv3.setCreatedAt(Instant.now());
LegalCreditInvest inv4 = new LegalCreditInvest();
inv4.setCustomerName("广州恒盛科技投资有限公司");
inv4.setInvestigationType("工商信用核查");
inv4.setScope("工商注册信息、股权结构、企业年报、司法拍卖记录");
inv4.setCourtRecord("未发现涉诉记录");
inv4.setBlacklisted(false);
inv4.setLitigationHistory("无历史诉讼记录");
inv4.setCreditScore("AAA");
inv4.setInvestigator("王志远");
inv4.setTargetCompletionDate("2025-07-31");
inv4.setStatus("调查中");
inv4.setRemark("新引入战略合作客户,正在开展全面尽职调查,预计下月出具调查报告。");
inv4.setCreatedAt(Instant.now());
investRepo.save(inv1);
investRepo.save(inv2);
investRepo.save(inv3);
investRepo.save(inv4);
}
}
@@ -0,0 +1,201 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.ComplianceAnonReport;
import com.kaidi.oa.domain.LegalPolicyRevision;
import com.kaidi.oa.repository.ComplianceAnonReportRepository;
import com.kaidi.oa.repository.LegalPolicyRevisionRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
/**
* 制度修订审批 + 匿名合规举报演示数据播种器(法务风险部)。
*
* 补齐缺口 2(匿名举报)和缺口 8(制度修订 OA 引擎接入)的活体演示数据。
* @Order(21),在 LegalDemoSeeder(@Order 20) 之后;幂等。
*/
@Order(21)
@Component
public class LegalPolicyAnonSeeder implements CommandLineRunner {
private final LegalPolicyRevisionRepository revisionRepo;
private final ComplianceAnonReportRepository anonReportRepo;
public LegalPolicyAnonSeeder(LegalPolicyRevisionRepository revisionRepo,
ComplianceAnonReportRepository anonReportRepo) {
this.revisionRepo = revisionRepo;
this.anonReportRepo = anonReportRepo;
}
@Override
public void run(String... args) throws Exception {
seedPolicyRevisions();
seedAnonReports();
}
// ======================================================================
// 1. 制度修订申请(跨状态:草稿/审批中/已生效/已废止)
// ======================================================================
private void seedPolicyRevisions() {
if (revisionRepo.count() > 0) return;
LegalPolicyRevision r1 = new LegalPolicyRevision();
r1.setRevisionNo("PR-2025-001");
r1.setPolicyNo("ZD-HR-015");
r1.setTitle("员工竞业限制与保密协议管理制度");
r1.setRevisionType("修订");
r1.setCategory("管理制度");
r1.setContent("根据2024年《劳动合同法》司法解释新规,修订竞业限制补偿标准为月薪30%,并扩展限制范围至供应商合作关系,增加违约金双倍赔偿条款。");
r1.setReason("最高法院2024年发布关于竞业限制纠纷案件审理的新司法解释,原制度与新规不符,存在合规风险。");
r1.setChangePoints("1. 补偿标准:≥月薪25% → 月薪30%; 2. 新增:禁止入职供应商; 3. 违约赔偿:1倍 → 2倍月薪×限制月数");
r1.setApplicantDept("法务部");
r1.setApplicant("陈建国");
r1.setEffectiveDate("2025-07-01");
r1.setStatus("已生效");
r1.setCreatedAt(Instant.now());
r1.setUpdatedAt(Instant.now());
LegalPolicyRevision r2 = new LegalPolicyRevision();
r2.setRevisionNo("PR-2025-002");
r2.setPolicyNo("ZD-PROC-008");
r2.setTitle("采购合同授权审批管理办法");
r2.setRevisionType("修订");
r2.setCategory("授权规则");
r2.setContent("调整各级授权金额上限:部门经理 50万→80万、总经理 200万→300万,新增电子合同签署授权,明确超授权行为追责机制。");
r2.setReason("原授权标准制定于2022年,随公司业务规模扩大,现有金额上限已不匹配实际需求,导致大量合同需要越级审批,影响效率。");
r2.setChangePoints("1. 部门经理授权额度上调至80万; 2. 总经理上调至300万; 3. 新增电子签章授权流程; 4. 超授权追责条款细化");
r2.setApplicantDept("法务部");
r2.setApplicant("李晓燕");
r2.setEffectiveDate("2025-09-01");
r2.setStatus("审批中");
r2.setCreatedAt(Instant.now());
r2.setUpdatedAt(Instant.now());
LegalPolicyRevision r3 = new LegalPolicyRevision();
r3.setRevisionNo("PR-2025-003");
r3.setPolicyNo("ZD-DATA-003");
r3.setTitle("数据安全与个人信息保护管理制度");
r3.setRevisionType("新建");
r3.setCategory("管理制度");
r3.setContent("根据《数据安全法》《个人信息保护法》制度要求,新建数据安全管理制度,覆盖数据分级保护、访问控制、泄露应急响应、删除和销毁规程。");
r3.setReason("我司目前尚无专项数据安全管理制度,监管机构检查时被要求整改。《个保法》实施已近4年,该项制度为合规基础要求。");
r3.setChangePoints("全新制度,主要内容:数据分级(公开/内部/机密/绝密)、敏感数据访问须双人核准、个人信息去标识化要求、数据出境审查流程");
r3.setApplicantDept("法务部");
r3.setApplicant("王志远");
r3.setEffectiveDate("2025-10-01");
r3.setStatus("草稿");
r3.setCreatedAt(Instant.now());
r3.setUpdatedAt(Instant.now());
LegalPolicyRevision r4 = new LegalPolicyRevision();
r4.setRevisionNo("PR-2024-006");
r4.setPolicyNo("ZD-PROC-003");
r4.setTitle("物资采购管理流程(2020版)");
r4.setRevisionType("废止");
r4.setCategory("业务流程");
r4.setContent("原2020年版物资采购流程已由2024年新版本取代,正式废止旧版本,防止双版本并行导致执行混乱。");
r4.setReason("2024年采购管理流程完成修订并发布,原版本已过时,需正式废止以避免混用。");
r4.setChangePoints("无变更,仅为废止旧版本。");
r4.setApplicantDept("采购部");
r4.setApplicant("赵明华");
r4.setDeprecationDate("2025-01-01");
r4.setStatus("已废止");
r4.setCreatedAt(Instant.now());
r4.setUpdatedAt(Instant.now());
LegalPolicyRevision r5 = new LegalPolicyRevision();
r5.setRevisionNo("PR-2025-004");
r5.setPolicyNo("ZD-HR-022");
r5.setTitle("高管绩效激励与股权激励管理办法");
r5.setRevisionType("修订");
r5.setCategory("管理制度");
r5.setContent("引入限制性股票激励计划,调整高管年度绩效考核权重(短期绩效40%+长期价值60%),与公司战略转型目标挂钩。");
r5.setReason("公司战略转型需要更长期的激励机制,现有以年为周期的绩效考核已不足以激励高管关注长期价值创造。");
r5.setChangePoints("1. 新增限制性股票激励计划(3年期); 2. 绩效权重调整; 3. 退出机制完善(反稀释条款)");
r5.setApplicantDept("战略发展部");
r5.setApplicant("张勇");
r5.setEffectiveDate("2025-12-01");
r5.setStatus("已驳回");
r5.setRemark("董事会审议认为当前不宜引入股权激励,建议待上市计划明确后重新评估,本次申请驳回。");
r5.setCreatedAt(Instant.now());
r5.setUpdatedAt(Instant.now());
revisionRepo.save(r1);
revisionRepo.save(r2);
revisionRepo.save(r3);
revisionRepo.save(r4);
revisionRepo.save(r5);
}
// ======================================================================
// 2. 匿名合规举报(跨状态:待受理/调查中/已处理/已关闭)
// ======================================================================
private void seedAnonReports() {
if (anonReportRepo.count() > 0) return;
ComplianceAnonReport a1 = new ComplianceAnonReport();
a1.setReportNo("AR-2025-001");
a1.setReportType("财务异常");
a1.setSuspectDept("采购部");
a1.setSuspectPerson("某采购经理");
a1.setReportContent("发现某采购经理与特定供应商存在利益输送:该供应商报价始终明显高于市场价15-20%,但每次仍中标,且该采购经理与供应商负责人有私下宴请记录。建议调查该供应商的实际控制人与采购部人员的关联关系。");
a1.setReporterInfo("(匿名举报,不留联系方式)");
a1.setSeverity("严重");
a1.setStatus("调查中");
a1.setHandler("陈建国");
a1.setHandleResult("已启动内部调查,正在核查采购台账及供应商关系,预计2025年8月底完成初步报告。");
a1.setQueryToken("a1b2c3d4e5f6g7h8");
a1.setCreatedAt(Instant.now());
a1.setUpdatedAt(Instant.now());
ComplianceAnonReport a2 = new ComplianceAnonReport();
a2.setReportNo("AR-2025-002");
a2.setReportType("安全隐患");
a2.setSuspectDept("生产车间");
a2.setSuspectPerson("(部门整体)");
a2.setReportContent("生产车间B区消防通道长期被货架占用,消防设施存在遮挡,安全出口标识灯不亮。已发现超过3个月,多次口头提醒但无人处理,担心发生事故时无法及时疏散。");
a2.setReporterInfo("可以通过内网邮件联系:anon@kaidi.internal");
a2.setSeverity("紧急");
a2.setStatus("已处理");
a2.setHandler("王志远");
a2.setHandleResult("已通知安全生产部现场整改。2025年6月15日复查确认:消防通道已清空,消防设施遮挡已移除,安全出口标识灯全部更换完毕。记录为安全隐患整改案例,纳入月度安全检查。");
a2.setQueryToken("h8g7f6e5d4c3b2a1");
a2.setCreatedAt(Instant.now());
a2.setUpdatedAt(Instant.now());
ComplianceAnonReport a3 = new ComplianceAnonReport();
a3.setReportNo("AR-2025-003");
a3.setReportType("劳动违规");
a3.setSuspectDept("研发部");
a3.setSuspectPerson("(不便透露)");
a3.setReportContent("研发部存在长期强制加班未支付加班费的情况,员工打卡记录被删除,绩效考核与加班时长挂钩形成隐性强制。希望公司核查过去12个月的加班记录和薪酬发放情况。");
a3.setReporterInfo("");
a3.setSeverity("严重");
a3.setStatus("待受理");
a3.setQueryToken("z9y8x7w6v5u4t3s2");
a3.setCreatedAt(Instant.now());
a3.setUpdatedAt(Instant.now());
ComplianceAnonReport a4 = new ComplianceAnonReport();
a4.setReportNo("AR-2024-009");
a4.setReportType("合规违规");
a4.setSuspectDept("市场部");
a4.setSuspectPerson("(团队行为)");
a4.setReportContent("市场部在政府项目投标中存在提前获取标底信息的行为,怀疑有内线。已有2次投标中标价格与标底极为接近,偏差在0.3%以内,统计上不太可能出现。");
a4.setReporterInfo("");
a4.setSeverity("严重");
a4.setStatus("已关闭");
a4.setHandler("李晓燕");
a4.setHandleResult("经调查,未发现实质性证据表明存在非法获取标底行为。2次中标均有合理解释(市场调研能力强+竞争对手报价较保守)。将加强投标廉洁合规培训,本案关闭。");
a4.setQueryToken("r2q1p0o9n8m7l6k5");
a4.setCreatedAt(Instant.now());
a4.setUpdatedAt(Instant.now());
anonReportRepo.save(a1);
anonReportRepo.save(a2);
anonReportRepo.save(a3);
anonReportRepo.save(a4);
}
}
@@ -0,0 +1,99 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.MarketCampaign;
import com.kaidi.oa.repository.MarketCampaignRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
/**
* 市场推广活动演示数据(需求 §2 AI 营销复盘 — 补全缺口:活体无数据)。
*
* 为 campaign-review / funnel / summary 三个端点提供真实演示数据,
* 让 AI 营销复盘结论能从真实记录自动聚合填入,覆盖"接口骨架但无数据联动"缺口。
*
* 数据覆盖:2025-2026 年多类型活动(展会/推介会/线上)、多状态(筹备中/进行中/已结束)、
* 合理的预算金额(BigDecimal)和商机数量,供漏斗可视化和 AI 复盘分析使用。
*
* @Order(10) — 在 DataSeeder(@Order 1) 之后独立运行,幂等(count > 0 则跳过)。
*/
@Component
@Order(10)
public class MarketCampaignSeeder implements ApplicationRunner {
private final MarketCampaignRepository campaignRepo;
public MarketCampaignSeeder(MarketCampaignRepository campaignRepo) {
this.campaignRepo = campaignRepo;
}
@Override
public void run(ApplicationArguments args) {
if (campaignRepo.count() > 0) {
return;
}
List<MarketCampaign> seeds = new ArrayList<>();
// 2025 年活动(已结束)
seeds.add(campaign("2025年华东区建筑行业技术推介会", "推介会", "王建国", "市场部·华东办",
"68000.00", "2025-03-15", 12, "已结束"));
seeds.add(campaign("2025年全国城市基础设施展(CIIF)", "展会", "李雪梅", "市场部",
"152000.00", "2025-04-20", 28, "已结束"));
seeds.add(campaign("2025年Q2线上产品发布直播", "线上", "张伟", "市场部·数字化",
"18500.00", "2025-05-10", 35, "已结束"));
seeds.add(campaign("2025年华南区政府BT/BOT项目推介", "推介会", "陈志远", "市场部·华南办",
"45000.00", "2025-07-08", 9, "已结束"));
seeds.add(campaign("2025年下半年建材供应链展览会", "展会", "刘芳芳", "市场部",
"88000.00", "2025-09-18", 21, "已结束"));
seeds.add(campaign("2025年Q4年终客户答谢推介会", "推介会", "王建国", "市场部",
"55000.00", "2025-11-25", 16, "已结束"));
// 2026 年活动(进行中 / 筹备中)
seeds.add(campaign("2026年智慧城市与绿色建筑论坛", "展会", "李雪梅", "市场部",
"198000.00", "2026-03-22", 42, "已结束"));
seeds.add(campaign("2026年Q1区域市政项目对接会(西南)", "推介会", "赵明磊", "市场部·西南办",
"36000.00", "2026-04-05", 8, "已结束"));
seeds.add(campaign("2026年线上招投标政策宣讲直播", "线上", "张伟", "市场部·数字化",
"12000.00", "2026-05-16", 29, "进行中"));
seeds.add(campaign("2026年重点客户战略合作伙伴峰会", "推介会", "陈志远", "市场部",
"120000.00", "2026-06-18", 18, "进行中"));
seeds.add(campaign("2026年下半年省级招标采购博览会", "展会", "王建国", "市场部",
"165000.00", "2026-09-10", 0, "筹备中"));
seeds.add(campaign("2026年双11工程建材线上优惠专场", "线上", "刘芳芳", "市场部·数字化",
"22000.00", "2026-11-01", 0, "筹备中"));
campaignRepo.saveAll(seeds);
}
private MarketCampaign campaign(String theme, String type, String owner, String orgUnit,
String budgetYuan, String holdDate, int leads, String status) {
MarketCampaign c = new MarketCampaign();
c.setTheme(theme);
c.setCampaignType(type);
c.setOwner(owner);
c.setOrgUnit(orgUnit);
c.setBudget(Money.of(Double.parseDouble(budgetYuan)));
c.setHoldDate(holdDate);
c.setLeads(leads);
c.setStatus(status);
c.setCreatedAt(Instant.now());
return c;
}
}
@@ -0,0 +1,95 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.PasswordUtil;
import com.kaidi.oa.domain.SysDept;
import com.kaidi.oa.domain.SysUser;
import com.kaidi.oa.repository.SysDeptRepository;
import com.kaidi.oa.repository.SysUserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* Enriches the seed org with users matching the approval templates' common role
* labels (董事长 / 总经理 / 成控总监 / 各成本会计 / 出纳 / 法务经理 / 保密员 / 资料员 …).
*
* Why: workflow nodes carry role labels; with only 6 demo users NodeAssigneeResolver
* dumped most approvals on admin. Seeding the matching real users lets the resolver
* route each node to a distinct person, so multi-user 待办 is realistic.
*
* Idempotent + non-destructive: runs after the base seeder, only adds users that are
* missing (guard on a sentinel login). All accounts use password 123456.
*/
@Component
@Order(2)
public class OrgEnrichmentSeeder implements CommandLineRunner {
private final SysUserRepository userRepo;
private final SysDeptRepository deptRepo;
public OrgEnrichmentSeeder(SysUserRepository userRepo, SysDeptRepository deptRepo) {
this.userRepo = userRepo;
this.deptRepo = deptRepo;
}
/** login, displayName, dept name, title — title/name tuned to match template node labels. */
private static final String[][] ROLE_USERS = {
{"zhoujg", "周建国", "开迪集团", "董事长"},
{"sungq", "孙国强", "开迪集团", "总经理"},
{"zhangyun", "张运", "开迪集团", "董事长助理"},
{"chenfr", "陈法人", "开迪集团", "法人"},
{"zhaocz", "赵成志", "成本控制部", "成控总监"},
{"zhengqt", "郑前台", "成本控制部", "成控前台"},
{"sunzh", "孙志华", "财务部", "财务文员"},
{"qiancn", "钱晨", "资金结算部", "制单出纳"},
{"guolin", "郭琳", "资金结算部", "结算会计"},
{"qiantcc", "钱总成", "财务部", "总公司成本会计"},
{"sunfcc", "孙分成", "成本控制部", "分公司成本会计"},
{"zhoujtc", "周集成", "财务部", "集团成本会计"},
{"lijm", "李剑美", "内控部", "保密员"},
{"zhaofw", "赵法", "法务风险部", "法务部经理"},
{"wuzly", "吴资料", "资料室", "集团公司资料员"}
};
@Override
public void run(String... args) {
// Sentinel: if the first enrichment user already exists, assume done.
if (userRepo.findByLoginName("zhoujg").isPresent()) {
return;
}
// base org must exist (admin seeded by DataSeeder @Order(1)); else nothing to attach to.
if (userRepo.findByLoginName("admin").isEmpty()) {
return;
}
Map<String, Long> deptIdByName = new HashMap<>();
for (SysDept d : deptRepo.findAll()) {
deptIdByName.putIfAbsent(d.getName(), d.getId());
}
int idx = 6;
for (String[] r : ROLE_USERS) {
if (userRepo.findByLoginName(r[0]).isPresent()) {
continue;
}
// displayName 唯一护栏:对象级鉴权/审批办理人按 displayName 解析到稳定 userId
// 重名会让"同名/改名重用"互相冒充。种子也绝不播出重名(与 UserController 的唯一约束同口径),
// 让"唯一 displayName"在全新重建库上同样成立。
if (!userRepo.findByDisplayName(r[1]).isEmpty()) {
continue;
}
SysUser u = new SysUser();
u.setLoginName(r[0]);
u.setDisplayName(r[1]);
u.setDeptId(deptIdByName.get(r[2]));
u.setTitle(r[3]);
u.setPhone(String.format("138%08d", idx));
u.setEmail(r[0] + "@kaidi.com");
u.setPassword(PasswordUtil.hash("123456"));
u.setEnabled(true);
userRepo.save(u);
idx++;
}
}
}
@@ -0,0 +1,126 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.RdDeclPlan;
import com.kaidi.oa.repository.RdDeclPlanRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
/**
* 申报服务部——年度/季度申报计划种子数据(Gap 1 [low] 补完)。
*
* 原缺口:rd-decl-plans 台账为 0 条数据,前端页面 rddeclplan.vue 虽已存在但列表空白。
* 本 Seeder 在表为空时注入 4 条演示计划(跨状态:执行中/已批准/草稿/已完成),
* 使 rddeclplan 页面首次打开即有数据可见。
*
* 注意:本 seeder 独立于 DataSeederOrder=2 在 DataSeeder Order=1 之后运行),
* 不修改 DataSeeder 的大构造函数。
*/
@Component
@Order(2)
public class RdDeclPlanSeeder implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(RdDeclPlanSeeder.class);
private final RdDeclPlanRepository planRepo;
public RdDeclPlanSeeder(RdDeclPlanRepository planRepo) {
this.planRepo = planRepo;
}
@Override
public void run(String... args) {
if (planRepo.count() > 0) {
return; // 已有数据,跳过
}
log.info("Seeding RdDeclPlan demonstration data...");
// 1. 执行中:高新技术企业认定申报(主力计划)
RdDeclPlan p1 = new RdDeclPlan();
p1.setName("2026年度高新技术企业认定申报计划");
p1.setPeriodType("年度");
p1.setPlanYear(2026);
p1.setPolicyCategory("资质类");
p1.setProgramName("高新技术企业");
p1.setDepartment("申报服务部");
p1.setOwner("李申报");
p1.setBudget(Money.of(80000.0));
p1.setExpectedBenefit(Money.of(500000.0));
p1.setPlanApplyDate("2026-08-01");
p1.setPlanEndDate("2026-11-30");
p1.setStatus("执行中");
p1.setProgress(45);
p1.setRemark("每三年复评一次,本次为第二轮;需财务审计报告+知识产权数量满足要求");
p1.setCreatedAt(Instant.now());
p1.setUpdatedAt(Instant.now());
planRepo.save(p1);
// 2. 已批准:专精特新中小企业申报
RdDeclPlan p2 = new RdDeclPlan();
p2.setName("2026年度专精特新中小企业认定申报计划");
p2.setPeriodType("年度");
p2.setPlanYear(2026);
p2.setPolicyCategory("资质类");
p2.setProgramName("专精特新中小企业");
p2.setDepartment("申报服务部");
p2.setOwner("王申报");
p2.setBudget(Money.of(30000.0));
p2.setExpectedBenefit(Money.of(200000.0));
p2.setPlanApplyDate("2026-09-01");
p2.setPlanEndDate("2026-12-31");
p2.setStatus("已批准");
p2.setProgress(0);
p2.setRemark("省级专精特新首次申报,需研发费用占比≥3%、知识产权≥2项");
p2.setCreatedAt(Instant.now());
p2.setUpdatedAt(Instant.now());
planRepo.save(p2);
// 3. 草稿:研发费用加计扣除季度申请
RdDeclPlan p3 = new RdDeclPlan();
p3.setName("2026年Q3研发费用加计扣除预缴申请计划");
p3.setPeriodType("季度");
p3.setPlanYear(2026);
p3.setPlanQuarter("Q3");
p3.setPolicyCategory("资金类");
p3.setProgramName("研发费用加计扣除");
p3.setDepartment("申报服务部");
p3.setOwner("张申报");
p3.setBudget(Money.of(5000.0));
p3.setExpectedBenefit(Money.of(120000.0));
p3.setPlanApplyDate("2026-07-15");
p3.setPlanEndDate("2026-07-31");
p3.setStatus("草稿");
p3.setProgress(0);
p3.setRemark("每季度预缴税款时享受,需财务提供研发费用辅助账");
p3.setCreatedAt(Instant.now());
p3.setUpdatedAt(Instant.now());
planRepo.save(p3);
// 4. 已完成:科技型中小企业评价(已结案)
RdDeclPlan p4 = new RdDeclPlan();
p4.setName("2025年度科技型中小企业评价申报计划");
p4.setPeriodType("年度");
p4.setPlanYear(2025);
p4.setPolicyCategory("资质类");
p4.setProgramName("科技型中小企业");
p4.setDepartment("申报服务部");
p4.setOwner("李申报");
p4.setBudget(Money.of(10000.0));
p4.setExpectedBenefit(Money.of(80000.0));
p4.setPlanApplyDate("2025-05-01");
p4.setPlanEndDate("2025-06-30");
p4.setStatus("已完成");
p4.setProgress(100);
p4.setRemark("已完成在科技部门户网站填报并获评,有效期至2026年底");
p4.setCreatedAt(Instant.now().minusSeconds(86400L * 180));
p4.setUpdatedAt(Instant.now().minusSeconds(86400L * 30));
planRepo.save(p4);
log.info("Seeded {} RdDeclPlan records.", planRepo.count());
}
}
@@ -0,0 +1,236 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.SewageCostSavingProject;
import com.kaidi.oa.domain.SewagePlantBenchmark;
import com.kaidi.oa.repository.SewageCostSavingProjectRepository;
import com.kaidi.oa.repository.SewagePlantBenchmarkRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
/**
* 城镇污水运营中心·横向对标 + 降本增效项目台账 演示种子数据。
*
* 补深审计 low 缺口(8.绩效与成本分析):
* 1. SewagePlantBenchmark:同区域同类水厂外部横向对标数据(6家水厂,含本厂,湖南区域一级A口径);
* 2. SewageCostSavingProject:节能技改项目独立台账(5个项目,多状态多类型,含基期+实际节约额自动计算)。
*
* 幂等:各表 count == 0 才插入。
*/
@Component
@Order(206)
public class SewageBenchmarkSeeder implements ApplicationRunner {
private final SewagePlantBenchmarkRepository benchmarkRepo;
private final SewageCostSavingProjectRepository savingRepo;
public SewageBenchmarkSeeder(SewagePlantBenchmarkRepository benchmarkRepo,
SewageCostSavingProjectRepository savingRepo) {
this.benchmarkRepo = benchmarkRepo;
this.savingRepo = savingRepo;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
seedBenchmarks();
seedCostSavingProjects();
}
// ------------------------------------------------------------------ 同类水厂横向对标
private void seedBenchmarks() {
if (benchmarkRepo.count() > 0) return;
// 本厂(凯迪科技·城镇污水一厂,标注 isSelf=true
benchmark("凯迪科技城镇污水一厂", "湖南省·岳阳市", 5.0, "一级A", "2025",
0.312, 18.5, "0.058", "1.85",
97.2, 96.8, "自填", true, "王芳");
// 区域同类水厂 A(处理规模相近)
benchmark("岳阳滨水污水处理有限公司", "湖南省·岳阳市", 4.0, "一级A", "2025",
0.298, 16.2, "0.052", "1.72",
98.1, 97.5, "政府公开", false, "刘洋");
// 区域同类水厂 B(区域最优,吨水电耗最低)
benchmark("岳阳君山水务股份有限公司", "湖南省·岳阳市", 6.0, "一级A", "2025",
0.275, 15.8, "0.048", "1.63",
98.5, 98.2, "行业报告·住建部2025年城镇排水年报", false, "刘洋");
// 区域同类水厂 C(规模稍大)
benchmark("湘阴城乡水务集团第二污水厂", "湖南省·岳阳市", 8.0, "一级A", "2025",
0.305, 17.3, "0.055", "1.78",
96.8, 95.5, "集团汇总", false, "陈静");
// 区域同类水厂 D(较差样本,拉开分布)
benchmark("临湘市污水处理厂", "湖南省·岳阳市", 3.0, "一级A", "2025",
0.338, 20.6, "0.068", "2.05",
94.2, 93.1, "政府公开", false, "陈静");
// 省级对标样本(规模最大,指标优异)
benchmark("长沙第二污水处理厂", "湖南省·长沙市", 20.0, "地方特别限值", "2025",
0.258, 14.1, "0.042", "1.52",
99.0, 98.8, "行业报告·2025湖南省城镇污水处理年报", false, "王芳");
}
private void benchmark(String plantName, String region, double capacity, String dischargeStd,
String statPeriod, double unitPower, double unitReagent,
String unitRepairCost, String unitTotalCost,
double complianceRate, double equipIntactRate,
String dataSrc, boolean isSelf, String maintainer) {
SewagePlantBenchmark b = new SewagePlantBenchmark();
b.setPlantName(plantName);
b.setRegion(region);
b.setCapacity(capacity);
b.setDischargeStd(dischargeStd);
b.setStatPeriod(statPeriod);
b.setUnitPower(unitPower);
b.setUnitReagent(unitReagent);
b.setUnitRepairCost(new java.math.BigDecimal(unitRepairCost));
b.setUnitTotalCost(new java.math.BigDecimal(unitTotalCost));
b.setComplianceRate(complianceRate);
b.setEquipIntactRate(equipIntactRate);
b.setDataSrc(dataSrc);
b.setIsSelf(isSelf);
b.setMaintainer(maintainer);
b.setCreatedAt(Instant.now());
b.setUpdatedAt(Instant.now());
benchmarkRepo.save(b);
}
// ------------------------------------------------------------------ 降本增效项目台账
private void seedCostSavingProjects() {
if (savingRepo.count() > 0) return;
// 项目1:曝气变频改造——已完结,节约可观
// 基期:吨水电耗 0.32 kWh/m³,月均水量 150000 m³
// 改造后:吨水电耗 0.275 kWh/m³,验证 6 个月
// 节约电量/月 = (0.32-0.275) × 150000 = 6750 kWh/月
// 节约电费 = 6750 × 6 × 0.58 = 23490 元
costSavingProject("JNJ-0001", "曝气系统变频节能改造", "节能改造", "水厂1号",
"2024-03-01", "2024-08-31", "2024-09-15",
"98000", 3,
0.32, 22.5, 150000.0,
6, 0.275, 22.0, 155000.0,
"0.58", "8.50",
"已完结", "王芳",
"对3台提升泵+曝气风机加装变频器,实现精确曝气控制。已通过3个月基期+6个月效果验证,累计节约电量40500 kWh,节约电费23490元,静态回收期约25个月。");
// 项目2:精确加药系统升级——已完结,药耗降低
// 基期:吨水药耗 20 g/m³,月均水量 150000 m³,药剂单价 8.5 元/kg
// 改造后:吨水药耗 17.5 g/m³,验证 3 个月
// 节约药量/月 = (20-17.5)/1000 × 150000 = 375 kg/月
// 节约药费 = 375 × 3 × 8.5 = 9562.5 元
costSavingProject("JNJ-0002", "在线水质联动精确加药系统", "精确加药", "水厂1号",
"2024-06-01", "2024-10-31", "2024-11-10",
"65000", 3,
0.31, 20.0, 150000.0,
3, 0.31, 17.5, 152000.0,
"0.58", "8.50",
"已完结", "刘洋",
"引入在线氨氮/TP传感器与加药泵PLC联动,实现PAC/PAM按需投加。药耗下降12.5%,月节约药剂375 kg,节约费用约3187元/月。");
// 项目3:PAM替换为改性淀粉基助凝剂(药剂替代)——效果验证中
// 基期:吨水药耗 5.2 g/m³(PAM),药剂单价 12 元/kg
// 替换后:预期吨水药耗 4.5 g/m³,药剂单价 7.8 元/kg(改性淀粉)
costSavingProject("JNJ-0003", "PAM替代:改性淀粉基助凝剂中试", "药剂替代", "水厂1号",
"2025-03-01", "2025-08-31", null,
"28000", 3,
0.31, 5.2, 148000.0,
3, 0.31, 4.5, 148000.0,
"0.58", "7.80",
"效果验证", "陈静",
"试用改性淀粉基绿色助凝剂替代PAM,环保指标更优且成本更低。目前已完成3个月中试,吨水药耗从5.2降至4.5 g/m³,进入效果验证阶段。");
// 项目4:污泥脱水系统能效提升——改造中
costSavingProject("JNJ-0004", "污泥脱水机变频节能改造", "节能改造", "水厂2号",
"2025-08-01", "2025-12-31", null,
"45000", 3,
0.29, 18.0, 120000.0,
3, 0.265, 18.0, 120000.0,
"0.58", "8.50",
"改造中", "王芳",
"对2台带式压滤机驱动电机实施变频改造,预计吨水电耗降低0.025 kWh/m³,月节约电量3000 kWh。目前设备已进场,安装调试中。");
// 项目5:峰谷用电策略优化——立项中
costSavingProject("JNJ-0005", "峰谷电价优化运行策略", "工艺优化", "全站",
"2025-11-01", "2026-03-31", null,
"5000", 3,
0.32, 19.5, 270000.0,
6, 0.305, 19.5, 270000.0,
"0.58", "8.50",
"立项中", "刘洋",
"分析峰谷用电时段,将非核心用电负荷(污泥脱水、曝气辅机)移至谷段运行。通过调整运行策略降低电费(综合电价从0.58降至平均0.55元/kWh),无设备改造,主要是运行制度优化。");
}
private void costSavingProject(String code, String projectName, String projectType, String plant,
String kickoffDate, String planFinishDate, String actualFinishDate,
String investCost, int baseMonths,
double baseUnitPower, double baseUnitReagent, double baseMonthlyWater,
int verifyMonths, double actualUnitPower, double actualUnitReagent, double actualMonthlyWater,
String powerPrice, String reagentPrice,
String projStatus, String owner, String remark) {
SewageCostSavingProject p = new SewageCostSavingProject();
p.setCode(code);
p.setProjectName(projectName);
p.setProjectType(projectType);
p.setPlant(plant);
p.setKickoffDate(kickoffDate);
p.setPlanFinishDate(planFinishDate);
p.setActualFinishDate(actualFinishDate);
p.setInvestCost(new BigDecimal(investCost));
p.setBaseMonths(baseMonths);
p.setBaseUnitPower(baseUnitPower);
p.setBaseUnitReagent(baseUnitReagent);
p.setBaseMonthlyWater(baseMonthlyWater);
p.setVerifyMonths(verifyMonths);
p.setActualUnitPower(actualUnitPower);
p.setActualUnitReagent(actualUnitReagent);
p.setActualMonthlyWater(actualMonthlyWater);
p.setPowerPrice(new BigDecimal(powerPrice));
p.setReagentPrice(new BigDecimal(reagentPrice));
p.setProjStatus(projStatus);
p.setOwner(owner);
p.setRemark(remark);
p.setCreatedAt(Instant.now());
p.setUpdatedAt(Instant.now());
// 自动计算节约量
double deltaUnitPower = baseUnitPower - actualUnitPower;
double savedKwhPerM = Math.max(0, deltaUnitPower * actualMonthlyWater);
double savedKwhTotal = savedKwhPerM * verifyMonths;
double deltaUnitReagent = baseUnitReagent - actualUnitReagent;
double savedReagentKgPerM = Math.max(0, deltaUnitReagent / 1000.0 * actualMonthlyWater);
BigDecimal pp = new BigDecimal(powerPrice);
BigDecimal rp = new BigDecimal(reagentPrice);
BigDecimal savedPowerCost = BigDecimal.valueOf(savedKwhTotal).multiply(pp)
.setScale(2, RoundingMode.HALF_UP);
BigDecimal savedReagentCost = BigDecimal.valueOf(savedReagentKgPerM * verifyMonths).multiply(rp)
.setScale(2, RoundingMode.HALF_UP);
BigDecimal savedTotal = savedPowerCost.add(savedReagentCost);
p.setSavedKwhPerMonth(Math.round(savedKwhPerM * 100.0) / 100.0);
p.setSavedKwhTotal(Math.round(savedKwhTotal * 100.0) / 100.0);
p.setSavedReagentKgPerMonth(Math.round(savedReagentKgPerM * 100.0) / 100.0);
p.setSavedPowerCost(savedPowerCost);
p.setSavedReagentCost(savedReagentCost);
p.setSavedTotalCost(savedTotal);
BigDecimal invest = new BigDecimal(investCost);
if (savedTotal.compareTo(BigDecimal.ZERO) > 0 && verifyMonths > 0) {
BigDecimal monthlyRate = savedTotal.divide(BigDecimal.valueOf(verifyMonths), 4, RoundingMode.HALF_UP);
if (monthlyRate.compareTo(BigDecimal.ZERO) > 0) {
double pb = invest.divide(monthlyRate, 2, RoundingMode.HALF_UP).doubleValue();
p.setPaybackMonths(pb);
}
}
savingRepo.save(p);
}
}
@@ -0,0 +1,280 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.SewageCrossDeptLinkage;
import com.kaidi.oa.domain.SewageExtLabCommission;
import com.kaidi.oa.repository.SewageCrossDeptLinkageRepository;
import com.kaidi.oa.repository.SewageExtLabCommissionRepository;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
/**
* 城镇污水运营·跨部门联动触发器 & 外部实验室委托检测 演示种子数据。
*
* 补深审计 low 缺口(11.与其他部门接口要求):让联动台账与外委检测台账活体可见,
* 覆盖多种状态(成功/失败/待触发 / 合格/超标/待判定),演示完整业务链路。
*
* 幂等:linkageRepo.count() == 0 才插入;commRepo.count() == 0 才插入。
*/
@Component
@Order(30)
public class SewageCrossDeptSeeder implements ApplicationRunner {
private final SewageCrossDeptLinkageRepository linkageRepo;
private final SewageExtLabCommissionRepository commRepo;
public SewageCrossDeptSeeder(SewageCrossDeptLinkageRepository linkageRepo,
SewageExtLabCommissionRepository commRepo) {
this.linkageRepo = linkageRepo;
this.commRepo = commRepo;
}
@Override
@Transactional
public void run(ApplicationArguments args) {
seedLinkages();
seedExtLabCommissions();
}
// ------------------------------------------------------------------ 跨部门联动触发器演示数据
private void seedLinkages() {
if (linkageRepo.count() > 0) return;
Instant now = Instant.now();
// 1. 成功联动:药剂低库存→采购工单(真实写入)
SewageCrossDeptLinkage lk1 = new SewageCrossDeptLinkage();
lk1.setEventKey("REAGENT_LOW_STOCK");
lk1.setSourceId(1L);
lk1.setSourceModule("sewage_reagent_stock");
lk1.setEventDesc("聚合氯化铝(PAC)(凯迪一污水厂)库存 280 kg 低于安全库存 500 kg,需补货");
lk1.setTargetDept("采购部");
lk1.setLinkageAction("生成采购工单");
lk1.setStatus("成功");
lk1.setTargetEntityType("mfg_purchase_order");
lk1.setTargetEntityId(1001L);
lk1.setResultNote("已真实创建采购工单 MfgPurchaseOrder#1001(来源:sewage_reagent_stock/1");
lk1.setOwner("系统自动");
lk1.setTriggeredAt(now.minus(3, ChronoUnit.DAYS));
lk1.setCompletedAt(now.minus(3, ChronoUnit.DAYS).plus(2, ChronoUnit.SECONDS));
lk1.setCreatedAt(now.minus(3, ChronoUnit.DAYS));
lk1.setUpdatedAt(now.minus(3, ChronoUnit.DAYS));
linkageRepo.save(lk1);
// 2. 成功联动:月度成本归集完成→财务凭证(真实写入)
SewageCrossDeptLinkage lk2 = new SewageCrossDeptLinkage();
lk2.setEventKey("COST_MONTH_COMPLETE");
lk2.setSourceId(2L);
lk2.setSourceModule("sewage_cost_month");
lk2.setEventDesc("2026-04 凯迪一污水厂 月度完全成本 ¥386500.00 完成,触发财务过账");
lk2.setTargetDept("财务部");
lk2.setLinkageAction("成本过账");
lk2.setStatus("成功");
lk2.setTargetEntityType("voucher");
lk2.setTargetEntityId(2001L);
lk2.setResultNote("已真实创建记账凭证 Voucher#2001(来源:sewage_cost_month/2)科目:5001-运营成本←2201-应付账款");
lk2.setOwner("系统自动");
lk2.setTriggeredAt(now.minus(10, ChronoUnit.DAYS));
lk2.setCompletedAt(now.minus(10, ChronoUnit.DAYS).plus(1, ChronoUnit.SECONDS));
lk2.setCreatedAt(now.minus(10, ChronoUnit.DAYS));
lk2.setUpdatedAt(now.minus(10, ChronoUnit.DAYS));
linkageRepo.save(lk2);
// 3. 成功联动:检测报告签发→申报服务部数据包
SewageCrossDeptLinkage lk3 = new SewageCrossDeptLinkage();
lk3.setEventKey("DETECT_REPORT_SIGNED");
lk3.setSourceId(5L);
lk3.setSourceModule("sewage_detection_report");
lk3.setEventDesc("2026-05 月度水质检测报告已签发,COD削减量 42.8吨,氨氮削减量 8.6吨");
lk3.setTargetDept("申报服务部");
lk3.setLinkageAction("发送运营数据包");
lk3.setStatus("成功");
lk3.setTargetEntityType("ops_data_package");
lk3.setTargetEntityId(5L);
lk3.setResultNote("已将运营数据(检测报告 sourceId=5)打包推送至申报服务部,含减排贡献数据");
lk3.setOwner("王运营");
lk3.setTriggeredAt(now.minus(5, ChronoUnit.DAYS));
lk3.setCompletedAt(now.minus(5, ChronoUnit.DAYS).plus(1, ChronoUnit.SECONDS));
lk3.setCreatedAt(now.minus(5, ChronoUnit.DAYS));
lk3.setUpdatedAt(now.minus(5, ChronoUnit.DAYS));
linkageRepo.save(lk3);
// 4. 失败联动:月度成本归集→财务部(待重试)
SewageCrossDeptLinkage lk4 = new SewageCrossDeptLinkage();
lk4.setEventKey("COST_MONTH_COMPLETE");
lk4.setSourceId(3L);
lk4.setSourceModule("sewage_cost_month");
lk4.setEventDesc("2026-05 凯迪二污水厂 月度完全成本 ¥312000.00 完成,触发财务过账");
lk4.setTargetDept("财务部");
lk4.setLinkageAction("成本过账");
lk4.setStatus("失败");
lk4.setResultNote("联动执行失败: 财务部凭证接口超时,请稍后重试");
lk4.setOwner("系统自动");
lk4.setTriggeredAt(now.minus(1, ChronoUnit.DAYS));
lk4.setCreatedAt(now.minus(1, ChronoUnit.DAYS));
lk4.setUpdatedAt(now.minus(1, ChronoUnit.DAYS));
linkageRepo.save(lk4);
// 5. 待触发:运营记录自动归档
SewageCrossDeptLinkage lk5 = new SewageCrossDeptLinkage();
lk5.setEventKey("DETECTION_REPORT_ARCHIVE");
lk5.setSourceId(7L);
lk5.setSourceModule("sewage_detection_report");
lk5.setEventDesc("2026-05 水质检测原始记录待归档至资料室");
lk5.setTargetDept("资料室");
lk5.setLinkageAction("自动归档");
lk5.setStatus("待触发");
lk5.setOwner("李化验");
lk5.setTriggeredAt(now);
lk5.setCreatedAt(now);
lk5.setUpdatedAt(now);
linkageRepo.save(lk5);
// 6. 成功联动:外委检测超标→法务风险部(真实写入 LegalRiskEvent
SewageCrossDeptLinkage lk6 = new SewageCrossDeptLinkage();
lk6.setEventKey("EXT_LAB_EXCEED_LEGAL");
lk6.setSourceId(1L);
lk6.setSourceModule("sewage_ext_lab_commission");
lk6.setEventDesc("EXT-LAB-0001 超标(总磷 0.82mg/L),已推送至法务风险部(LegalRiskEvent#3001");
lk6.setTargetDept("法务风险部");
lk6.setLinkageAction("创建环保处罚风险事件");
lk6.setStatus("成功");
lk6.setTargetEntityType("legal_risk_event");
lk6.setTargetEntityId(3001L);
lk6.setResultNote("真实写入 LegalRiskEvent id=3001,类型=行政处罚,状态=待分析");
lk6.setOwner("张水质");
lk6.setTriggeredAt(now.minus(2, ChronoUnit.DAYS));
lk6.setCompletedAt(now.minus(2, ChronoUnit.DAYS).plus(1, ChronoUnit.SECONDS));
lk6.setCreatedAt(now.minus(2, ChronoUnit.DAYS));
lk6.setUpdatedAt(now.minus(2, ChronoUnit.DAYS));
linkageRepo.save(lk6);
}
// ------------------------------------------------------------------ 外部实验室委托检测演示数据
private void seedExtLabCommissions() {
if (commRepo.count() > 0) return;
Instant now = Instant.now();
// 1. 超标整改状态(总磷超标,已推送法务)
SewageExtLabCommission c1 = new SewageExtLabCommission();
c1.setCommissionNo("EXT-LAB-0001");
c1.setPlant("凯迪一污水厂");
c1.setCommissionDate("2026-05-10");
c1.setPlannedReportDate("2026-05-18");
c1.setActualReportDate("2026-05-17");
c1.setLabName("省环境检测技术中心");
c1.setLabCertNo("CMA(2024)川字第12345号");
c1.setSampleType("出水");
c1.setSampleDate("2026-05-10");
c1.setSampleNo("SMP-20260510-001");
c1.setTestItems("COD,氨氮,总磷,总氮,SS,pH");
c1.setResultSummary("COD=8.5mg/L;氨氮=0.42mg/L;总磷=0.82mg/L;总氮=10.6mg/L;SS=6mg/L;pH=7.2");
c1.setComplianceResult("超标");
c1.setExceedItems("总磷");
c1.setCorrectionNote("总磷实测0.82 mg/L,超出GB18918一级A标准0.5 mg/L,需检查深度除磷单元,加强PAC加药量");
c1.setStatus("超标整改");
c1.setAmount(Money.of(3800.0));
c1.setLinkageId(3001L);
c1.setOwner("张水质");
c1.setRemark("第三方年度水质监督性抽检");
c1.setCreatedAt(now.minus(25, ChronoUnit.DAYS));
c1.setUpdatedAt(now.minus(8, ChronoUnit.DAYS));
commRepo.save(c1);
// 2. 合格状态,已归档
SewageExtLabCommission c2 = new SewageExtLabCommission();
c2.setCommissionNo("EXT-LAB-0002");
c2.setPlant("凯迪二污水厂");
c2.setCommissionDate("2026-04-15");
c2.setPlannedReportDate("2026-04-22");
c2.setActualReportDate("2026-04-21");
c2.setLabName("市水质监测站");
c2.setLabCertNo("CNAS L1234");
c2.setSampleType("出水");
c2.setSampleDate("2026-04-15");
c2.setSampleNo("SMP-20260415-002");
c2.setTestItems("COD,氨氮,总磷,总氮,SS");
c2.setResultSummary("COD=6.2mg/L;氨氮=0.18mg/L;总磷=0.32mg/L;总氮=8.4mg/L;SS=4mg/L");
c2.setComplianceResult("合格");
c2.setStatus("已归档");
c2.setAmount(Money.of(2500.0));
c2.setOwner("李化验");
c2.setRemark("季度合规性检测");
c2.setCreatedAt(now.minus(60, ChronoUnit.DAYS));
c2.setUpdatedAt(now.minus(45, ChronoUnit.DAYS));
commRepo.save(c2);
// 3. 检测中(刚发样,尚未出结果)
SewageExtLabCommission c3 = new SewageExtLabCommission();
c3.setCommissionNo("EXT-LAB-0003");
c3.setPlant("凯迪一污水厂");
c3.setCommissionDate("2026-06-05");
c3.setPlannedReportDate("2026-06-15");
c3.setLabName("省环境检测技术中心");
c3.setLabCertNo("CMA(2024)川字第12345号");
c3.setSampleType("工艺段(A/O末端)");
c3.setSampleDate("2026-06-05");
c3.setSampleNo("SMP-20260605-003");
c3.setTestItems("溶解氧,氨氮,总磷,总氮,MLSS");
c3.setComplianceResult("待判定");
c3.setStatus("检测中");
c3.setAmount(Money.of(4200.0));
c3.setOwner("张水质");
c3.setRemark("A/O生化段专项监测,配合工艺优化调整");
c3.setCreatedAt(now.minus(10, ChronoUnit.DAYS));
c3.setUpdatedAt(now.minus(10, ChronoUnit.DAYS));
commRepo.save(c3);
// 4. 待发样(刚创建)
SewageExtLabCommission c4 = new SewageExtLabCommission();
c4.setCommissionNo("EXT-LAB-0004");
c4.setPlant("凯迪三污水厂");
c4.setCommissionDate("2026-06-12");
c4.setPlannedReportDate("2026-06-20");
c4.setLabName("某市第三方水质检测机构");
c4.setSampleType("污泥滤液");
c4.setSampleDate("2026-06-12");
c4.setSampleNo("SMP-20260612-004");
c4.setTestItems("COD,氨氮,重金属(铅,镉,铬)");
c4.setComplianceResult("待判定");
c4.setStatus("待发样");
c4.setAmount(Money.of(5600.0));
c4.setOwner("赵厂长");
c4.setRemark("污泥处置前合规性检测,确认重金属含量符合处置要求");
c4.setCreatedAt(now.minus(3, ChronoUnit.DAYS));
c4.setUpdatedAt(now.minus(3, ChronoUnit.DAYS));
commRepo.save(c4);
// 5. 已出报告-合格(重金属检测通过)
SewageExtLabCommission c5 = new SewageExtLabCommission();
c5.setCommissionNo("EXT-LAB-0005");
c5.setPlant("凯迪二污水厂");
c5.setCommissionDate("2026-05-20");
c5.setPlannedReportDate("2026-05-28");
c5.setActualReportDate("2026-05-27");
c5.setLabName("省环境检测技术中心");
c5.setLabCertNo("CMA(2024)川字第12345号");
c5.setSampleType("进水");
c5.setSampleDate("2026-05-20");
c5.setSampleNo("SMP-20260520-005");
c5.setTestItems("COD,氨氮,总磷,总氮,SS,重金属筛查");
c5.setResultSummary("COD=245mg/L;氨氮=28.6mg/L;总磷=3.2mg/L;总氮=36.4mg/L;SS=180mg/L;重金属未检出");
c5.setComplianceResult("合格");
c5.setStatus("已出报告");
c5.setAmount(Money.of(6800.0));
c5.setOwner("李化验");
c5.setRemark("重要工业用户纳管排查配套检测");
c5.setCreatedAt(now.minus(26, ChronoUnit.DAYS));
c5.setUpdatedAt(now.minus(19, ChronoUnit.DAYS));
commRepo.save(c5);
}
}
@@ -0,0 +1,262 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.common.Money;
import com.kaidi.oa.domain.MeetingActionItem;
import com.kaidi.oa.domain.MeetingMinute;
import com.kaidi.oa.domain.SupervisionProject;
import com.kaidi.oa.domain.SvIndepClaim;
import com.kaidi.oa.repository.MeetingActionItemRepository;
import com.kaidi.oa.repository.MeetingMinuteRepository;
import com.kaidi.oa.repository.SupervisionProjectRepository;
import com.kaidi.oa.repository.SvIndepClaimRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.List;
/**
* 工程监理部 · 缺口补完演示数据播种器。
*
* <p>补完两个 PARTIAL→MET 缺口:
* <ol>
* <li>缺口 5(索赔独立状态机):播种 SvIndepClaim 多阶段演示数据,
* 覆盖 INTENT/INCIDENT/ASSESSMENT/OPINION/CLOSED/REJECTED 各阶段,
* 包含关键线路影响分析字段。</li>
* <li>缺口 7(监理例会纪要按项目过滤+待办任务结构化):
* 播种 MeetingMinute(含 supervisionProjectId)及配套 MeetingActionItem
* (含 assignee+dueDate,关联责任人)。</li>
* </ol>
*
* <p>@Order(210) 在 DataSeeder(@Order 1) 之后运行;幂等(SvIndepClaim.count>0 则跳过)。
*/
@Order(210)
@Component
public class SvSupervisionMinuteSeeder implements CommandLineRunner {
private final SvIndepClaimRepository claimRepo;
private final SupervisionProjectRepository projectRepo;
private final MeetingMinuteRepository minuteRepo;
private final MeetingActionItemRepository actionItemRepo;
public SvSupervisionMinuteSeeder(SvIndepClaimRepository claimRepo,
SupervisionProjectRepository projectRepo,
MeetingMinuteRepository minuteRepo,
MeetingActionItemRepository actionItemRepo) {
this.claimRepo = claimRepo;
this.projectRepo = projectRepo;
this.minuteRepo = minuteRepo;
this.actionItemRepo = actionItemRepo;
}
@Override
public void run(String... args) throws Exception {
seedIndepClaims();
seedSupervisionMinutes();
}
// ========================================================================
// 1. 索赔独立状态机演示数据
// ========================================================================
private void seedIndepClaims() {
if (claimRepo.count() > 0) return;
// 获取第一个监理项目做关联(如无则不关联)
List<SupervisionProject> projects = projectRepo.findAll();
Long projectId1 = projects.isEmpty() ? null : projects.get(0).getId();
String projectCode1 = projects.isEmpty() ? "JL-2026-001" : projects.get(0).getCode();
Long projectId2 = projects.size() > 1 ? projects.get(1).getId() : projectId1;
String projectCode2 = projects.size() > 1 ? projects.get(1).getCode() : projectCode1;
Instant now = Instant.now();
// 索赔1:已完成四阶段,状态 CLOSED(赶工费用索赔,关键线路影响=5天,核定3天)
SvIndepClaim c1 = new SvIndepClaim();
c1.setSupervisionProjectId(projectId1);
c1.setSupervisionProjectCode(projectCode1);
c1.setClaimCode("SYC-1");
c1.setContractor("湖南城建建设有限公司");
c1.setIntentDate("2026-03-10");
c1.setIntentSummary("因设计变更导致基础换填工程量增加,施工方提出工期延误及费用增加索赔");
c1.setIncidentStartDate("2026-02-15");
c1.setIncidentEndDate("2026-03-08");
c1.setIncidentDetail("2月15日建设单位下达设计变更通知单,要求将原持力层由3米加深至5米,导致土方开挖量增加约1800m³,工期顺延。施工方已按新标准施工,3月8日完成隐蔽验收。");
c1.setEvidenceRef("变更通知单BG-2026-003,隐蔽验收记录HB-0308");
c1.setDurationImpactDays(8);
c1.setCriticalPathImpactDays(5);
c1.setCriticalPathImpact("该延误位于关键路径(基础→主体结构),关键线路延误5天,其余3天属非关键工序可平行赶工");
c1.setClaimAmount(Money.of(125000.00));
c1.setCostImpactDetail("增加土方开挖1800m³×35元/m³=63000元,换填级配碎石580m³×107元/m³=62060元,合计125060元,舍尾为125000元");
c1.setReviewer("张建国");
c1.setReviewConclusion("部分支持");
c1.setApprovedDurationDays(3);
c1.setApprovedAmount(Money.of(98000.00));
c1.setReviewOpinion("经复核,设计变更工程量属实,但部分换填单价偏高。核定费用补偿98000元,工期补偿3天(关键线路)。");
c1.setReviewDate("2026-03-20");
c1.setClaimStatus("CLOSED");
c1.setCreatedAt(now);
c1.setUpdatedAt(now);
claimRepo.save(c1);
// 索赔2:处于 ASSESSMENT 阶段(甲方原因停工索赔)
SvIndepClaim c2 = new SvIndepClaim();
c2.setSupervisionProjectId(projectId1);
c2.setSupervisionProjectCode(projectCode1);
c2.setClaimCode("SYC-2");
c2.setContractor("湖南城建建设有限公司");
c2.setIntentDate("2026-04-18");
c2.setIntentSummary("因建设单位资金周转问题导致工程暂停施工16天,施工方提出窝工费用及固定设施费索赔");
c2.setIncidentStartDate("2026-04-01");
c2.setIncidentEndDate("2026-04-17");
c2.setIncidentDetail("建设单位4月1日口头通知暂停施工,4月17日恢复。停工期间共有工人42人待命,塔吊2台、混凝土泵1台未能正常作业,产生窝工损失及机械停滞费用。");
c2.setEvidenceRef("停工令(口头转书面)BT-2026-001,考勤记录2026-04-01至04-17");
c2.setDurationImpactDays(16);
c2.setCriticalPathImpactDays(16);
c2.setCriticalPathImpact("停工期间为主体结构关键施工阶段,全部16天均影响关键线路,计划竣工日期相应顺延16天");
c2.setClaimAmount(Money.of(287500.00));
c2.setCostImpactDetail("工人窝工42人×16天×60元/工日=40320元,塔吊停滞2台×16天×2200元/台天=70400元,泵车停滞1台×16天×3500元/天=56000元,现场管理费=120780元,合计287500元");
c2.setClaimStatus("ASSESSMENT");
c2.setCreatedAt(now);
c2.setUpdatedAt(now);
claimRepo.save(c2);
// 索赔3:处于 INCIDENT 阶段(不可抗力索赔)
SvIndepClaim c3 = new SvIndepClaim();
c3.setSupervisionProjectId(projectId2);
c3.setSupervisionProjectCode(projectCode2);
c3.setClaimCode("SYC-3");
c3.setContractor("中建八局第二建设有限公司");
c3.setIntentDate("2026-05-05");
c3.setIntentSummary("因持续暴雨(超50年一遇)导致施工区域积水,基坑边坡出现局部滑塌,申请工期顺延及处置费用");
c3.setIncidentStartDate("2026-04-28");
c3.setIncidentEndDate("2026-05-03");
c3.setIncidentDetail("4月28日至5月3日连续6天强降雨,日均降水量超过80mm,累计降水量512mm,超过50年一遇设计标准。基坑东侧边坡出现约15m范围局部滑塌,需紧急处置后方可继续施工。");
c3.setEvidenceRef("气象局降雨记录证明,现场影像资料IMG-2026-0428至0503,应急处置方案EP-2026-001");
c3.setClaimStatus("INCIDENT");
c3.setCreatedAt(now);
c3.setUpdatedAt(now);
claimRepo.save(c3);
// 索赔4:处于 INTENT 阶段(材料涨价索赔)
SvIndepClaim c4 = new SvIndepClaim();
c4.setSupervisionProjectId(projectId2);
c4.setSupervisionProjectCode(projectCode2);
c4.setClaimCode("SYC-4");
c4.setContractor("中建八局第二建设有限公司");
c4.setIntentDate("2026-05-28");
c4.setIntentSummary("螺纹钢价格较签合同时上涨约18%,申请材料差价补偿");
c4.setClaimStatus("INTENT");
c4.setCreatedAt(now);
c4.setUpdatedAt(now);
claimRepo.save(c4);
// 索赔5:已驳回(无有效证据)
SvIndepClaim c5 = new SvIndepClaim();
c5.setSupervisionProjectId(projectId1);
c5.setSupervisionProjectCode(projectCode1);
c5.setClaimCode("SYC-5");
c5.setContractor("湖南城建建设有限公司");
c5.setIntentDate("2026-01-15");
c5.setIntentSummary("施工方申请因图纸延迟发放导致的窝工损失补偿");
c5.setIncidentStartDate("2026-01-08");
c5.setIncidentEndDate("2026-01-14");
c5.setIncidentDetail("施工方称图纸未及时到位导致7天停工,但经查实:图纸已于1月7日发放,停工系施工方自身组织问题。");
c5.setEvidenceRef("图纸签收记录20260107");
c5.setClaimStatus("REJECTED");
c5.setCreatedAt(now);
c5.setUpdatedAt(now);
claimRepo.save(c5);
}
// ========================================================================
// 2. 监理例会纪要(含 supervisionProjectId + 结构化待办任务)
// ========================================================================
private void seedSupervisionMinutes() {
// 只有纪要数<=2时才补种(DataSeeder可能已种1条)
if (minuteRepo.count() > 5) return;
List<SupervisionProject> projects = projectRepo.findAll();
Long projectId1 = projects.isEmpty() ? null : projects.get(0).getId();
Long projectId2 = projects.size() > 1 ? projects.get(1).getId() : projectId1;
Instant now = Instant.now();
// 纪要1:靖州县污水处理厂第8次监理例会,有3条待办
MeetingMinute m1 = new MeetingMinute();
m1.setSupervisionProjectId(projectId1);
m1.setTitle("靖州县污水处理厂工程第8次监理例会纪要");
m1.setContent("会议时间:2026-04-07 09:00\n会议地点:项目部会议室\n\n" +
"【质量情况】本周完成生化池底板混凝土浇筑,坍落度、温度控制合格。" +
"但北侧剪力墙模板拼缝偏差超标(实测3-5mm,规范≤2mm),需整改。\n\n" +
"【进度情况】本周形象进度完成计划的95%,主体结构进度正常。\n\n" +
"【安全情况】发现1处高处作业未佩戴安全带,已现场整改。\n\n" +
"【计量支付】第5期进度款已提交监理审核,监理将于7日内出具支付证书。");
m1.setStatus("已发布");
m1.setDecisions("北侧剪力墙模板拼缝超标,施工方3天内完成整改并提交整改报告;\n" +
"高处作业安全带配戴问题,安全员每日专项巡查并记录;\n" +
"第5期进度款审核,监理4月10日前出具支付证书");
m1.setCreatedAt(now);
MeetingMinute savedM1 = minuteRepo.save(m1);
// 为纪要1生成3条待办任务(结构化,含assignee+dueDate
actionItem(savedM1.getId(), savedM1.getTitle(),
"北侧剪力墙模板拼缝超标,完成整改并提交整改报告",
"李明(施工方质量负责人)", "2026-04-10", now);
actionItem(savedM1.getId(), savedM1.getTitle(),
"高处作业安全带配戴情况每日专项巡查记录",
"王安全(安全监理)", "2026-04-14", now);
actionItem(savedM1.getId(), savedM1.getTitle(),
"第5期进度款审核,出具支付证书",
"张建国(总监)", "2026-04-10", now);
// 纪要2:岳阳市排水管网第3次专题会议(安全专题)
MeetingMinute m2 = new MeetingMinute();
m2.setSupervisionProjectId(projectId2);
m2.setTitle("岳阳市排水管网改造工程雨季施工安全专题会议纪要");
m2.setContent("会议时间:2026-05-12 14:30\n会议地点:施工现场临时会议室\n\n" +
"【会议背景】气象部门预报5月下旬将迎来持续强降雨,为确保雨季施工安全,专门召开安全专题会。\n\n" +
"【主要风险】①基坑周边地表水渗流;②临时排水沟排水能力不足;③脚手架基础积水沉降风险。\n\n" +
"【应对措施】施工方需在5月20日前完成基坑周边截水沟加宽施工,增设3台潜水泵24小时备用," +
"对全部脚手架基础进行加固处理,并建立每日雨前雨后巡查机制。");
m2.setStatus("已发布");
m2.setDecisions("基坑周边截水沟加宽施工,5月20日前完工;\n" +
"增设潜水泵3台24小时备用,5月18日前就位;\n" +
"全部脚手架基础加固处理,5月22日前完成并复验");
m2.setCreatedAt(now);
MeetingMinute savedM2 = minuteRepo.save(m2);
// 为纪要2生成3条待办任务
actionItem(savedM2.getId(), savedM2.getTitle(),
"基坑周边截水沟加宽施工完工",
"赵强(施工方项目经理)", "2026-05-20", now);
actionItem(savedM2.getId(), savedM2.getTitle(),
"增设潜水泵3台就位并调试",
"赵强(施工方项目经理)", "2026-05-18", now);
actionItem(savedM2.getId(), savedM2.getTitle(),
"脚手架基础加固完成后监理复验",
"刘华(专业监理)", "2026-05-23", now);
// 纪要3:草稿状态(尚未发布)
MeetingMinute m3 = new MeetingMinute();
m3.setSupervisionProjectId(projectId1);
m3.setTitle("靖州县污水处理厂工程第9次监理例会纪要(草稿)");
m3.setContent("会议时间:2026-04-14 09:00\n会议地点:项目部会议室\n\n【待补充...】");
m3.setStatus("草稿");
m3.setCreatedAt(now);
minuteRepo.save(m3);
}
private void actionItem(Long minuteId, String minuteTitle, String content,
String assignee, String dueDate, Instant now) {
MeetingActionItem item = new MeetingActionItem();
item.setMinuteId(minuteId);
item.setMinuteTitle(minuteTitle);
item.setContent(content);
item.setAssignee(assignee);
item.setDueDate(dueDate);
item.setStatus("待办");
item.setCreatedAt(now);
item.setUpdatedAt(now);
actionItemRepo.save(item);
}
}
@@ -0,0 +1,81 @@
package com.kaidi.oa.seed;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kaidi.oa.domain.FormTemplate;
import com.kaidi.oa.repository.FormTemplateRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;
import java.io.InputStream;
/**
* Loads extra form templates from classpath:seed-templates/*.json on startup and
* upserts them (by id) as built-in templates. Each JSON file is an array of
* { id, name, category, org, instructions, form, flow } objects whose form/flow
* mirror the frontend FormSchema / FlowSchema. Runs after the main DataSeeder
* and is idempotent (re-running re-applies the latest JSON), so dropping a new
* JSON file under resources/seed-templates and rebuilding adds templates without
* any code change. This is how 凯迪科技's real approval processes are catalogued.
*/
@Component
@Order(100)
public class TemplateJsonSeeder implements CommandLineRunner {
private final FormTemplateRepository repo;
private final ObjectMapper om;
public TemplateJsonSeeder(FormTemplateRepository repo, ObjectMapper om) {
this.repo = repo;
this.om = om;
}
@Override
public void run(String... args) {
Resource[] files;
try {
files = new PathMatchingResourcePatternResolver()
.getResources("classpath*:seed-templates/*.json");
} catch (Exception e) {
return;
}
int upserted = 0;
for (Resource f : files) {
try (InputStream in = f.getInputStream()) {
JsonNode arr = om.readTree(in);
if (arr == null || !arr.isArray()) {
continue;
}
for (JsonNode t : arr) {
String id = t.path("id").asText(null);
if (id == null || id.isBlank()) {
continue;
}
FormTemplate tpl = repo.findById(id).orElseGet(FormTemplate::new);
tpl.setId(id);
tpl.setName(t.path("name").asText(""));
tpl.setCategory(t.path("category").asText(""));
tpl.setOrg(t.path("org").asText(""));
tpl.setInstructions(t.path("instructions").asText(""));
tpl.setFormSchemaJson(t.has("form") ? t.get("form").toString() : "{}");
tpl.setFlowSchemaJson(t.has("flow") ? t.get("flow").toString() : "{}");
tpl.setBuiltin(true);
if (tpl.getPublishedAt() == null || tpl.getPublishedAt().isBlank()) {
tpl.setPublishedAt("2026-06-11");
}
repo.save(tpl);
upserted++;
}
} catch (Exception ignore) {
// skip malformed file, keep going
}
}
if (upserted > 0) {
System.out.println("TemplateJsonSeeder: upserted " + upserted
+ " templates from seed-templates/*.json");
}
}
}
@@ -0,0 +1,741 @@
package com.kaidi.oa.seed;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.kaidi.oa.domain.FormTemplate;
import java.util.ArrayList;
import java.util.List;
/**
* Builds the built-in form template catalog, porting the frontend
* src/oa/engine/templates/*.ts catalog (11 templates). Each template's form and
* flow schemas are serialized to JSON TEXT with the exact same structure the
* frontend produces (FormSchema{title,rows,subTables}, FlowSchema{nodes,edges,
* parallels,branches}).
*
* Order mirrors the frontend catalog array:
* baohan-payment, expense-reimburse, leave-apply, seal-apply, general-collab,
* supplier-access, business-trip, purchase-contract, payment-apply,
* overtime-apply, expense-cond.
*/
public final class TemplateSeedData {
private final ObjectMapper om;
public TemplateSeedData(ObjectMapper om) {
this.om = om;
}
// ---- small schema-builder DSL mirroring the frontend types ----
private ObjectNode field(String id, String label, String type) {
ObjectNode f = om.createObjectNode();
f.put("id", id);
f.put("label", label);
f.put("type", type);
return f;
}
private ObjectNode req(ObjectNode f) {
f.put("required", true);
return f;
}
private ObjectNode full(ObjectNode f) {
f.put("fullWidth", true);
return f;
}
private ObjectNode opts(ObjectNode f, String... options) {
ArrayNode arr = f.putArray("options");
for (String o : options) {
arr.add(o);
}
return f;
}
private ObjectNode col(String id, String label, String type, Integer width) {
ObjectNode c = om.createObjectNode();
c.put("id", id);
c.put("label", label);
c.put("type", type);
if (width != null) {
c.put("width", width);
}
return c;
}
private ObjectNode subTable(String id, String title, ObjectNode... columns) {
ObjectNode st = om.createObjectNode();
st.put("id", id);
st.put("title", title);
ArrayNode cols = st.putArray("columns");
for (ObjectNode c : columns) {
cols.add(c);
}
return st;
}
private ObjectNode node(String id, String label, String type) {
ObjectNode n = om.createObjectNode();
n.put("id", id);
n.put("label", label);
n.put("type", type);
return n;
}
private ArrayNode edge(String from, String to) {
ArrayNode e = om.createArrayNode();
e.add(from);
e.add(to);
return e;
}
/** Assemble a form schema node. rows is a list of row arrays of fields. */
private String formJson(String title, List<ObjectNode[]> rows, ObjectNode... subTables) {
ObjectNode form = om.createObjectNode();
form.put("title", title);
ArrayNode rowsArr = form.putArray("rows");
for (ObjectNode[] row : rows) {
ArrayNode r = rowsArr.addArray();
for (ObjectNode f : row) {
r.add(f);
}
}
if (subTables.length > 0) {
ArrayNode sts = form.putArray("subTables");
for (ObjectNode st : subTables) {
sts.add(st);
}
}
return form.toString();
}
private FormTemplate tpl(String id, String name, String category, String org,
String publishedAt, String instructions,
String formJson, String flowJson) {
FormTemplate t = new FormTemplate();
t.setId(id);
t.setName(name);
t.setCategory(category);
t.setOrg(org);
t.setPublishedAt(publishedAt);
t.setInstructions(instructions);
t.setFormSchemaJson(formJson);
t.setFlowSchemaJson(flowJson);
t.setBuiltin(true);
return t;
}
private List<ObjectNode[]> rows(ObjectNode[]... rows) {
return List.of(rows);
}
private ObjectNode[] row(ObjectNode... fields) {
return fields;
}
/** Build all 14 built-in templates (含申报计划审批模板 + 合同法务审批模板). */
public List<FormTemplate> all() {
List<FormTemplate> list = new ArrayList<>();
list.add(baohan());
list.add(reimburse());
list.add(leave());
list.add(seal());
list.add(general());
list.add(supplier());
list.add(businessTrip());
list.add(purchaseContract());
list.add(paymentApply());
list.add(overtime());
list.add(expenseCond());
list.add(financingApply());
list.add(rdDeclPlanApprove());
list.add(legalContractReview());
return list;
}
// ===================== individual templates =====================
private FormTemplate baohan() {
String form = formJson("保函付款申请单", rows(
row(req(field("applicant", "申请人", "person")), req(field("applyDate", "申请时间", "date"))),
row(req(field("applyDept", "申请部门", "dept")), req(field("deadline", "截止付款时间", "date"))),
row(full(req(opts(field("payType", "付款类型", "radio"), "投标保函", "预付款保函", "履约保函", "代理费")))),
row(full(req(field("payUnit", "付款单位(保函开具单位)", "text")))),
row(full(req(field("payAccount", "付款银行账号", "text")))),
row(full(field("office", "办事处", "text"))),
row(full(req(field("projectName", "项目名称", "text")))),
row(full(field("payReason", "付款事由", "textarea")))
), subTable("payeeDetails", "收款明细",
col("payeeName", "收款人户名", "text", null),
col("payeeAccount", "收款人账号", "text", null),
col("payeeBank", "收款人开户行", "text", null),
col("amount", "金额", "money", 140),
col("voucher", "凭证附件", "attachment", 100)));
ObjectNode flow = om.createObjectNode();
ArrayNode nodes = flow.putArray("nodes");
nodes.add(node("start", "发起者", "start"));
nodes.add(node("deptHead", "发起者部门主管", "审批"));
nodes.add(node("cashierMaker", "制单出纳", "知会"));
nodes.add(node("cashierReview", "复核出纳", "知会"));
nodes.add(node("settleAccount", "结算会计", "审批"));
nodes.add(node("financeHead", "财务部负责人", "审批"));
nodes.add(node("chairman", "董事长", "审批"));
nodes.add(node("firstCashier", "第一出纳", "协同"));
nodes.add(node("settleNotify", "结算会计", "知会"));
nodes.add(node("reviewApprove", "复核出纳", "审批"));
nodes.add(node("end", "结束", "end"));
ArrayNode edges = flow.putArray("edges");
edges.add(edge("start", "deptHead"));
edges.add(edge("deptHead", "cashierMaker"));
edges.add(edge("deptHead", "cashierReview"));
edges.add(edge("cashierMaker", "settleAccount"));
edges.add(edge("cashierReview", "settleAccount"));
edges.add(edge("settleAccount", "financeHead"));
edges.add(edge("financeHead", "chairman"));
edges.add(edge("chairman", "firstCashier"));
edges.add(edge("firstCashier", "settleNotify"));
edges.add(edge("settleNotify", "reviewApprove"));
edges.add(edge("reviewApprove", "end"));
ArrayNode parallels = flow.putArray("parallels");
ArrayNode p = parallels.addArray();
p.add("cashierMaker");
p.add("cashierReview");
String instructions =
"一、适用范围:本流程适用于投标保函、预付款保函、履约保函及代理费的对外付款申请。\n" +
"二、填报要求:\n" +
" 1. 申请人需完整填写付款单位、付款银行账号、办事处及项目名称;\n" +
" 2. 收款明细子表须逐行登记收款人户名、账号、开户行、金额,并上传凭证附件;\n" +
" 3. 付款事由应说明保函用途与付款依据。\n" +
"三、流程节点说明:\n" +
" 发起者提交后先经发起者部门主管审批;随后由制单出纳、复核出纳并行知会;\n" +
" 经结算会计、财务部负责人、董事长逐级审批;最后由第一出纳协同制单、\n" +
" 结算会计知会、复核出纳审批后办结归档。\n" +
"四、注意事项:金额较大或跨境付款的,应附合同及保函正本扫描件。";
return tpl("baohan-payment", "保函付款申请单", "财务审批", "信息中心", "2024-09-12",
instructions, form, flow.toString());
}
private FormTemplate reimburse() {
String form = formJson("费用报销单", rows(
row(req(field("applicant", "申请人", "person")), req(field("reimburseDept", "报销部门", "dept"))),
row(req(opts(field("expenseType", "费用类型", "select"), "差旅费", "招待费", "办公费", "培训费", "其他")),
req(field("amount", "金额", "money")))
), subTable("expenseItems", "报销明细",
col("item", "费用项", "text", null),
col("itemAmount", "金额", "money", 140),
col("remark", "备注", "text", null)));
ObjectNode flow = simpleFlow(new String[][]{
{"start", "发起者", "start"},
{"deptHead", "部门主管", "审批"},
{"financeAudit", "财务审核", "审批"},
{"gm", "总经理", "审批"},
{"end", "结束", "end"}
});
String instructions =
"费用报销流程:申请人填写报销部门、费用类型、金额及报销明细后提交,\n" +
"经部门主管审批、财务审核、总经理审批后办结。明细金额合计须与申请金额一致,\n" +
"并附合规发票。";
return tpl("expense-reimburse", "费用报销", "财务审批", "财务部", "2024-07-21",
instructions, form, flow.toString());
}
private FormTemplate leave() {
String form = formJson("请假申请", rows(
row(req(field("applicant", "申请人", "person"))),
row(full(req(opts(field("leaveType", "请假类型", "radio"), "事假", "病假", "年假")))),
row(req(field("startTime", "开始时间", "datetime")), req(field("endTime", "结束时间", "datetime"))),
row(full(req(field("reason", "请假事由", "textarea"))))
));
ObjectNode flow = simpleFlow(new String[][]{
{"start", "发起者", "start"},
{"deptHead", "部门主管", "审批"},
{"hr", "人力资源部", "审批"},
{"end", "结束", "end"}
});
String instructions =
"请假申请流程:员工填写请假类型、起止时间与事由后提交,经部门主管审批、\n" +
"人力资源部审批后生效。事假/病假需提前一个工作日提交,病假应附就诊证明。";
return tpl("leave-apply", "请假申请", "人力资源", "行政部", "2024-06-03",
instructions, form, flow.toString());
}
private FormTemplate seal() {
String form = formJson("用印申请单", rows(
row(req(field("applicant", "申请人", "person")), req(field("applyDate", "申请时间", "date"))),
row(req(opts(field("sealType", "印章类型", "select"), "公章", "合同章", "财务章", "法人章")),
req(field("copies", "份数", "number"))),
row(full(req(field("docName", "文件名称", "text")))),
row(full(req(field("reason", "用印事由", "textarea"))))
));
ObjectNode flow = simpleFlow(new String[][]{
{"start", "发起者", "start"},
{"deptHead", "部门主管", "审批"},
{"adminHead", "行政负责人", "审批"},
{"end", "结束", "end"}
});
String instructions =
"用印申请:填写用印事由、印章类型、文件名称及份数,经部门主管、行政负责人审批后用印。";
return tpl("seal-apply", "用印申请", "行政部", "行政部", "2024-05-16",
instructions, form, flow.toString());
}
private FormTemplate general() {
String form = formJson("通用事项", rows(
row(full(req(field("title", "标题", "text")))),
row(req(field("applicant", "发起人", "person"))),
row(full(req(field("content", "正文", "textarea"))))
));
ObjectNode flow = simpleFlow(new String[][]{
{"start", "发起者", "start"},
{"deptHead", "部门主管", "审批"},
{"end", "结束", "end"}
});
String instructions =
"通用协同事项:填写标题与正文后提交,经部门主管审批后办结,适用于无固定表单的一般性事项。";
return tpl("general-collab", "通用事项", "公用模板", "信息中心", "2024-01-08",
instructions, form, flow.toString());
}
private FormTemplate supplier() {
String form = formJson("供应商准入申请单", rows(
row(req(field("applicant", "申请人", "person")), req(field("applyDate", "申请时间", "date"))),
row(full(req(field("supplierName", "供应商名称", "text")))),
row(req(field("creditCode", "统一社会信用代码", "text")), field("regCapital", "注册资本", "money")),
row(req(field("contact", "联系人", "text")), req(field("phone", "联系电话", "text"))),
row(full(req(opts(field("supplyCategory", "供应类别", "checkbox"),
"原材料", "设备", "工程服务", "办公用品", "其他")))),
row(full(req(opts(field("cooperation", "合作意向", "radio"), "长期合作", "单次采购", "战略合作")))),
row(full(req(field("reason", "准入理由", "textarea"))))
), subTable("qualifications", "资质证照明细",
col("certName", "证照名称", "text", null),
col("certNo", "证照编号", "text", null),
col("validUntil", "有效期至", "date", 140),
col("attach", "扫描件", "attachment", 100)));
ObjectNode flow = om.createObjectNode();
ArrayNode nodes = flow.putArray("nodes");
nodes.add(node("start", "发起者", "start"));
nodes.add(node("purchaseLead", "采购主管", "审批"));
nodes.add(node("qualityDept", "质量部", "知会"));
nodes.add(node("financeDept", "财务部", "知会"));
nodes.add(node("purchaseDirector", "采购总监", "审批"));
nodes.add(node("vp", "分管副总", "审批"));
nodes.add(node("end", "结束", "end"));
ArrayNode edges = flow.putArray("edges");
edges.add(edge("start", "purchaseLead"));
edges.add(edge("purchaseLead", "qualityDept"));
edges.add(edge("purchaseLead", "financeDept"));
edges.add(edge("qualityDept", "purchaseDirector"));
edges.add(edge("financeDept", "purchaseDirector"));
edges.add(edge("purchaseDirector", "vp"));
edges.add(edge("vp", "end"));
ArrayNode parallels = flow.putArray("parallels");
ArrayNode p = parallels.addArray();
p.add("qualityDept");
p.add("financeDept");
String instructions =
"一、适用范围:本流程适用于新增供应商的准入审核及合格供应商名录登记。\n" +
"二、填报要求:\n" +
" 1. 申请人需完整填写供应商名称、统一社会信用代码、联系人及联系电话;\n" +
" 2. 资质证照子表须逐行登记证照名称、证照编号、有效期,并上传扫描件附件;\n" +
" 3. 供应类别与合作意向应如实勾选,准入理由须说明引入背景。\n" +
"三、流程节点说明:\n" +
" 发起者提交后先经采购主管审批;随后由质量部、财务部并行知会进行资质与\n" +
" 信用初核;经采购总监、分管副总逐级审批后完成准入登记。\n" +
"四、注意事项:证照缺失或在有效期外的,不予准入。";
return tpl("supplier-access", "供应商准入申请", "采购管理", "采购部", "2024-10-18",
instructions, form, flow.toString());
}
private FormTemplate businessTrip() {
String form = formJson("出差申请单", rows(
row(req(field("applicant", "出差人", "person")), req(field("applyDept", "所属部门", "dept"))),
row(req(field("destination", "出差目的地", "text")),
req(opts(field("transport", "主要交通工具", "select"), "飞机", "高铁", "汽车", "其他"))),
row(req(field("startTime", "开始时间", "datetime")), req(field("endTime", "结束时间", "datetime"))),
row(full(field("companions", "同行人员", "person"))),
row(req(field("budget", "预算金额", "money"))),
row(full(req(field("reason", "出差事由", "textarea"))))
), subTable("itinerary", "行程明细",
col("date", "日期", "date", 140),
col("fromTo", "行程(出发-到达)", "text", null),
col("wayTransport", "交通方式", "text", 120),
col("estCost", "预计费用", "money", 140)));
ObjectNode flow = simpleFlow(new String[][]{
{"start", "发起者", "start"},
{"deptHead", "部门主管", "审批"},
{"adminDept", "行政部", "知会"},
{"gm", "总经理", "审批"},
{"end", "结束", "end"}
});
String instructions =
"出差申请流程:申请人填写出差事由、目的地、起止时间、同行人员及预算后提交,\n" +
"经部门主管审批、行政部知会安排行程后,由总经理审批办结。行程明细须逐段登记\n" +
"交通方式与预计费用,单次预算超过 5000 元的应附说明。";
return tpl("business-trip", "出差申请", "行政部", "行政部", "2024-08-09",
instructions, form, flow.toString());
}
private FormTemplate purchaseContract() {
String form = formJson("采购合同会签单", rows(
row(req(field("applicant", "申请人", "person")), req(field("applyDept", "申请部门", "dept"))),
row(full(req(field("contractName", "合同名称", "text")))),
row(full(req(field("counterparty", "对方单位", "text")))),
row(req(field("amount", "合同金额", "money")), req(field("signDate", "签订日期", "date"))),
row(req(opts(field("payMethod", "付款方式", "select"), "预付款", "货到付款", "分期付款", "验收后付款")),
opts(field("contractType", "合同类型", "radio"), "框架协议", "一次性采购", "长期供货")),
row(full(field("remark", "会签说明", "textarea")))
), subTable("purchaseItems", "采购明细",
col("material", "物料名称", "text", null),
col("spec", "规格型号", "text", null),
col("qty", "数量", "number", 100),
col("unitPrice", "单价", "money", 120),
col("itemAmount", "金额", "money", 140)));
ObjectNode flow = om.createObjectNode();
ArrayNode nodes = flow.putArray("nodes");
nodes.add(node("start", "发起者", "start"));
nodes.add(node("purchaseLead", "采购主管", "审批"));
nodes.add(node("legalDept", "法务部", "协同"));
nodes.add(node("financeDept", "财务部", "知会"));
nodes.add(node("purchaseDirector", "采购总监", "审批"));
nodes.add(node("vp", "分管副总", "审批"));
nodes.add(node("gm", "总经理", "审批"));
nodes.add(node("end", "结束", "end"));
ArrayNode edges = flow.putArray("edges");
edges.add(edge("start", "purchaseLead"));
edges.add(edge("purchaseLead", "legalDept"));
edges.add(edge("purchaseLead", "financeDept"));
edges.add(edge("legalDept", "purchaseDirector"));
edges.add(edge("financeDept", "purchaseDirector"));
edges.add(edge("purchaseDirector", "vp"));
edges.add(edge("vp", "gm"));
edges.add(edge("gm", "end"));
ArrayNode parallels = flow.putArray("parallels");
ArrayNode p = parallels.addArray();
p.add("legalDept");
p.add("financeDept");
String instructions =
"一、适用范围:本流程适用于采购合同签订前的多部门会签与用印审批。\n" +
"二、填报要求:\n" +
" 1. 申请人需完整填写合同名称、对方单位、合同金额、付款方式及签订日期;\n" +
" 2. 采购明细子表须逐行登记物料名称、规格、数量、单价、金额;\n" +
" 3. 合同正本扫描件应作为附件随单上传。\n" +
"三、流程节点说明:\n" +
" 发起者提交后先经采购主管审批;随后由法务部协同审查条款、财务部并行知会\n" +
" 核对资金;经采购总监、分管副总、总经理逐级审批后用印签订。\n" +
"四、注意事项:合同金额与采购明细合计须一致,重大合同应附立项依据。";
return tpl("purchase-contract", "采购合同会签", "采购管理", "采购部", "2024-11-05",
instructions, form, flow.toString());
}
private FormTemplate paymentApply() {
String form = formJson("付款申请单", rows(
row(req(field("applicant", "申请人", "person")), req(field("applyDept", "申请部门", "dept"))),
row(full(req(field("payee", "收款单位", "text")))),
row(req(field("amount", "付款金额", "money")), req(field("planDate", "计划付款日期", "date"))),
row(req(opts(field("payMethod", "付款方式", "select"), "银行转账", "电汇", "承兑汇票", "现金")),
field("contractNo", "关联合同号", "text")),
row(full(req(opts(field("payCategory", "付款类别", "radio"),
"货款", "服务费", "保证金", "预付款", "其他")))),
row(full(req(field("purpose", "付款用途", "textarea"))))
), subTable("payDetails", "付款明细",
col("payeeName", "收款户名", "text", null),
col("payeeBank", "开户行", "text", null),
col("payeeAccount", "账号", "text", null),
col("detailAmount", "金额", "money", 140),
col("invoice", "发票附件", "attachment", 100)));
ObjectNode flow = om.createObjectNode();
ArrayNode nodes = flow.putArray("nodes");
nodes.add(node("start", "发起者", "start"));
nodes.add(node("deptHead", "部门主管", "审批"));
nodes.add(node("financeAudit", "财务审核", "审批"));
nodes.add(node("cashierMaker", "制单出纳", "知会"));
nodes.add(node("cashierReview", "复核出纳", "知会"));
nodes.add(node("financeHead", "财务负责人", "审批"));
nodes.add(node("gm", "总经理", "审批"));
nodes.add(node("cashierPay", "出纳付款", "协同"));
nodes.add(node("end", "结束", "end"));
ArrayNode edges = flow.putArray("edges");
edges.add(edge("start", "deptHead"));
edges.add(edge("deptHead", "financeAudit"));
edges.add(edge("financeAudit", "cashierMaker"));
edges.add(edge("financeAudit", "cashierReview"));
edges.add(edge("cashierMaker", "financeHead"));
edges.add(edge("cashierReview", "financeHead"));
edges.add(edge("financeHead", "gm"));
edges.add(edge("gm", "cashierPay"));
edges.add(edge("cashierPay", "end"));
ArrayNode parallels = flow.putArray("parallels");
ArrayNode p = parallels.addArray();
p.add("cashierMaker");
p.add("cashierReview");
String instructions =
"一、适用范围:本流程适用于供应商货款、服务费、保证金等对外付款申请。\n" +
"二、填报要求:\n" +
" 1. 申请人需完整填写收款单位、付款用途、付款方式及计划付款日期;\n" +
" 2. 付款明细子表须逐行登记收款户名、开户行、账号、金额,并上传发票附件;\n" +
" 3. 关联合同号应如实填写,以便财务核对。\n" +
"三、流程节点说明:\n" +
" 发起者提交后经部门主管、财务审核逐级审批;随后由制单出纳、复核出纳并行\n" +
" 知会;经财务负责人、总经理审批后,由出纳协同完成付款。\n" +
"四、注意事项:付款金额与明细合计须一致,无合同支撑的付款不予受理。";
return tpl("payment-apply", "付款申请", "财务审批", "财务部", "2024-09-28",
instructions, form, flow.toString());
}
private FormTemplate overtime() {
String form = formJson("加班申请单", rows(
row(req(field("applicant", "申请人", "person")), req(field("applyDept", "所属部门", "dept"))),
row(full(req(opts(field("overtimeType", "加班类型", "radio"),
"工作日加班", "休息日加班", "节假日加班")))),
row(req(field("startTime", "开始时间", "datetime")), req(field("endTime", "结束时间", "datetime"))),
row(req(opts(field("compensation", "补偿方式", "select"), "调休", "加班费")),
req(field("totalHours", "合计时长(小时)", "number"))),
row(full(req(field("reason", "加班事由", "textarea"))))
), subTable("overtimeItems", "加班明细",
col("date", "加班日期", "date", 140),
col("fromTime", "开始时间", "datetime", 180),
col("toTime", "结束时间", "datetime", 180),
col("hours", "时长(小时)", "number", 120),
col("workContent", "工作内容", "text", null)));
ObjectNode flow = simpleFlow(new String[][]{
{"start", "发起者", "start"},
{"deptHead", "部门主管", "审批"},
{"hr", "人力资源部", "知会"},
{"leader", "分管领导", "审批"},
{"end", "结束", "end"}
});
String instructions =
"加班申请流程:申请人填写加班类型、起止时间、加班事由及补偿方式后提交,\n" +
"经部门主管审批、人力资源部知会登记后,由分管领导审批生效。加班明细须逐日\n" +
"登记起止时间与时长,调休与加班费不可同时申报。";
return tpl("overtime-apply", "加班申请", "人力资源", "人力资源部", "2024-07-30",
instructions, form, flow.toString());
}
private FormTemplate expenseCond() {
String form = formJson("费用审批单", rows(
row(req(field("applicant", "申请人", "person")), req(field("amount", "金额", "money"))),
row(req(opts(field("expenseType", "费用类型", "select"),
"差旅费", "招待费", "办公费", "培训费", "其他"))),
row(full(req(field("reason", "费用说明", "textarea"))))
));
ObjectNode flow = om.createObjectNode();
ArrayNode nodes = flow.putArray("nodes");
nodes.add(node("start", "发起者", "start"));
nodes.add(node("deptHead", "部门主管", "审批"));
nodes.add(node("amountGate", "金额判断", "审批"));
nodes.add(node("gm", "总经理", "审批"));
nodes.add(node("directPass", "直接通过", "知会"));
nodes.add(node("financeReview", "财务复核", "审批"));
nodes.add(node("end", "结束", "end"));
ArrayNode edges = flow.putArray("edges");
edges.add(edge("start", "deptHead"));
edges.add(edge("deptHead", "amountGate"));
edges.add(edge("amountGate", "gm"));
edges.add(edge("amountGate", "directPass"));
edges.add(edge("gm", "financeReview"));
edges.add(edge("directPass", "financeReview"));
edges.add(edge("financeReview", "end"));
ArrayNode branches = flow.putArray("branches");
ObjectNode br = branches.addObject();
br.put("fromId", "amountGate");
ArrayNode cases = br.putArray("cases");
ObjectNode c1 = cases.addObject();
c1.put("label", "金额 ≥ 1万元");
c1.put("toId", "gm");
ObjectNode c2 = cases.addObject();
c2.put("label", "金额 < 1万元");
c2.put("toId", "directPass");
String instructions =
"费用审批(条件分支演示):部门主管审批后,系统按报销金额自动分流——\n" +
"金额 ≥ 1万元 的走「总经理审批」分支,< 1万元 的直接进入财务复核,\n" +
"两条分支最终汇合到财务复核节点后办结。";
return tpl("expense-cond", "费用审批(条件分支)", "财务审批", "财务部", "2025-03-18",
instructions, form, flow.toString());
}
/**
* 融资申请审批单:金融办初审 → 财务部 → 风控负责人 → 高管/董事长。
* templateId = "financing-apply",供 FinancingController.submitApproval() 调用。
*/
private FormTemplate financingApply() {
String form = formJson("融资申请审批单", rows(
row(req(field("applicant", "经办人", "person")),
req(field("companySubject", "融资主体", "text"))),
row(req(field("financingCode", "融资编号", "text")),
req(field("financingType", "融资品种", "text"))),
row(req(field("lender", "融资机构", "text")),
req(field("amount", "融资金额(元)", "money"))),
row(req(field("rate", "年化利率(%)", "number")),
req(field("termMonths", "期限(月)", "number"))),
row(req(field("repayMethod", "还款方式", "text")),
field("purpose", "资金用途", "text")),
row(full(field("schemeCompareNote", "方案比选说明", "textarea"))),
row(full(field("creditCheckNote", "授信充足性自查", "textarea")))
));
ObjectNode flow = om.createObjectNode();
ArrayNode nodes = flow.putArray("nodes");
nodes.add(node("start", "发起者(金融办)", "start"));
nodes.add(node("financeOfficeReview", "金融办初审", "审批"));
nodes.add(node("financeDept", "财务部审批", "审批"));
nodes.add(node("riskControl", "风控负责人会签", "会签"));
nodes.add(node("gm", "总经理审批", "审批"));
nodes.add(node("chairman", "董事长审批", "审批"));
nodes.add(node("financeNotify", "财务部知会", "知会"));
nodes.add(node("end", "结束", "end"));
ArrayNode edges = flow.putArray("edges");
edges.add(edge("start", "financeOfficeReview"));
edges.add(edge("financeOfficeReview", "financeDept"));
edges.add(edge("financeDept", "riskControl"));
edges.add(edge("riskControl", "gm"));
edges.add(edge("gm", "chairman"));
edges.add(edge("chairman", "financeNotify"));
edges.add(edge("financeNotify", "end"));
String instructions =
"融资申请审批流程:\n" +
"一、适用范围:本流程适用于各类融资申请(银行贷款/融资租赁/债券/票据等)。\n" +
"二、填报要求:\n" +
" 1. 经办人须完整填写融资编号、融资机构、融资金额、年化利率及期限;\n" +
" 2. 方案比选说明一栏需附多机构比选依据(综合成本率对比);\n" +
" 3. 授信充足性自查一栏须确认该机构授信额度剩余足以覆盖本次融资。\n" +
"三、审批节点:\n" +
" 金融办初审 → 财务部审批 → 风控负责人会签 → 总经理 → 董事长 → 财务部知会 → 办结。\n" +
"四、注意事项:\n" +
" 审批通过后,融资台账状态自动由「审批中」推进到「已放款」(到账确认后手动更新)。";
return tpl("financing-apply", "融资申请审批单", "财务审批", "金融办", "2025-01-15",
instructions, form, flow.toString());
}
/**
* 申报服务部 — 年度/季度申报计划审批模板(需求 §1 年度/季度申报计划,Gap 1 [low])。
* RdDeclPlanController 固定引用 templateId="rd-decl-plan-approve";本模板确保种子注册。
*/
private FormTemplate rdDeclPlanApprove() {
String form = formJson("申报计划审批单", rows(
row(req(field("applicant", "经办人", "person")),
req(field("planName", "计划名称", "text"))),
row(req(opts(field("periodType", "计划周期", "radio"), "年度", "季度")),
req(field("planYear", "计划年度", "number"))),
row(req(opts(field("policyCategory", "政策类别", "select"),
"资质类", "资金类", "荣誉类", "人才类")),
field("programName", "申报项目名称", "text")),
row(field("budget", "申报预算(元)", "money"),
field("expectedBenefit", "预期获批金额(元)", "money")),
row(field("planApplyDate", "计划申报日期", "date"),
field("planEndDate", "计划结束日期", "date")),
row(full(field("owner", "负责人", "person"))),
row(full(field("remark", "备注说明", "textarea")))
));
ObjectNode flow = simpleFlow(new String[][]{
{"start", "申报专员(发起)", "start"},
{"deptHead", "申报服务部负责人", "审批"},
{"rdDirector", "创新研发中心主任", "审批"},
{"gm", "分管副总", "审批"},
{"end", "结束", "end"}
});
String instructions =
"申报计划审批流程:\n" +
"一、适用范围:本流程适用于创新研发中心·申报服务部编制的年度/季度政策申报计划。\n" +
"二、填报要求:\n" +
" 1. 计划名称须明确年度(季度)和申报项目,如「2026年高新技术企业认定申报计划」;\n" +
" 2. 申报预算应包含咨询费、审计费、材料费合计;\n" +
" 3. 预期获批金额填写政策奖励或补贴估算值(不含税收减免);\n" +
" 4. 计划申报日期与计划结束日期须在政策申报截止日之前。\n" +
"三、审批节点:\n" +
" 申报专员提交 → 申报服务部负责人审批 → 创新研发中心主任审批 → 分管副总审批 → 办结。\n" +
"四、注意事项:\n" +
" 审批通过后,计划状态自动变为「已批准」,系统同步在政策申报台账创建对应申报记录。";
return tpl("rd-decl-plan-approve", "申报计划审批单", "研发申报", "申报服务部", "2026-01-01",
instructions, form, flow.toString());
}
/**
* 合同法务审批模板(templateId = "legal-contract-review")。
* 由 LegalContractFlowController.submitReview() 引用,补齐审批引擎对接缺口。
* 审批链:业务负责人 → 法务审核 → 财务审核(并行)→ 合规审核 → 高管审批 → 办结。
*/
private FormTemplate legalContractReview() {
String form = formJson("合同法务审批单", rows(
row(req(field("applicant", "发起人", "person")), req(field("applyDept", "发起部门", "dept"))),
row(full(req(field("contractName", "合同名称", "text")))),
row(full(req(field("counterparty", "对方单位", "text")))),
row(req(field("amount", "合同金额", "money")), req(field("signDate", "签订日期", "date"))),
row(req(opts(field("contractType", "合同类型", "select"),
"采购合同", "销售合同", "技术合同", "劳务合同", "保密协议", "NDA", "其他")),
req(opts(field("riskLevel", "风险等级", "radio"), "", "", ""))),
row(full(field("riskPoints", "主要风险点说明", "textarea"))),
row(full(field("remark", "备注", "textarea")))
));
ObjectNode flow = om.createObjectNode();
ArrayNode nodes = flow.putArray("nodes");
nodes.add(node("start", "发起者", "start"));
nodes.add(node("bizLead", "业务负责人", "审批"));
nodes.add(node("legal", "法务审核", "审批"));
nodes.add(node("finance", "财务审核", "知会"));
nodes.add(node("compliance", "合规审核", "审批"));
nodes.add(node("gm", "高管审批", "审批"));
nodes.add(node("end", "结束", "end"));
ArrayNode edges = flow.putArray("edges");
edges.add(edge("start", "bizLead"));
edges.add(edge("bizLead", "legal"));
edges.add(edge("bizLead", "finance"));
edges.add(edge("legal", "compliance"));
edges.add(edge("finance", "compliance"));
edges.add(edge("compliance", "gm"));
edges.add(edge("gm", "end"));
ArrayNode parallels = flow.putArray("parallels");
ArrayNode par = parallels.addArray();
par.add("legal");
par.add("finance");
String instructions =
"合同法务审批流程:\n" +
"一、适用范围:适用于各类型合同(采购/销售/技术/劳务/NDA)签订前的法务合规审批。\n" +
"二、填报要求:\n" +
" 1. 合同名称、对方单位、合同金额、签订日期必填;\n" +
" 2. 合同类型须从下拉列表选择,风险等级(高/中/低)须如实填写;\n" +
" 3. 主要风险点说明(如付款条款、违约责任、适用法律等)须简要描述。\n" +
"三、审批节点:\n" +
" 业务负责人确认 → 法务审核(条款)+ 财务知会(金额)并行 → 合规审核 → 高管审批 → 办结。\n" +
"四、注意事项:高风险合同建议额外附法律意见书;合同正本扫描件须随单上传。";
return tpl("legal-contract-review", "合同法务审批单", "合同管理", "法务风险部", "2026-01-01",
instructions, form, flow.toString());
}
/** Linear flow builder: nodes connected start -> ... -> end in order. */
private ObjectNode simpleFlow(String[][] nodeDefs) {
ObjectNode flow = om.createObjectNode();
ArrayNode nodes = flow.putArray("nodes");
for (String[] nd : nodeDefs) {
nodes.add(node(nd[0], nd[1], nd[2]));
}
ArrayNode edges = flow.putArray("edges");
for (int i = 0; i < nodeDefs.length - 1; i++) {
edges.add(edge(nodeDefs[i][0], nodeDefs[i + 1][0]));
}
return flow;
}
}
@@ -0,0 +1,370 @@
package com.kaidi.oa.seed;
import com.kaidi.oa.domain.EnvDischargePermit;
import com.kaidi.oa.domain.OpsEmergencySupply;
import com.kaidi.oa.domain.WwMsdsFile;
import com.kaidi.oa.domain.WwSamplingTask;
import com.kaidi.oa.repository.EnvDischargePermitRepository;
import com.kaidi.oa.repository.OpsEmergencySupplyRepository;
import com.kaidi.oa.repository.WwMsdsFileRepository;
import com.kaidi.oa.repository.WwSamplingTaskRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.Instant;
/**
* 工业废水运营中心·缺口演示数据(Gap-7 安全环保+Gap-11 跨部门接口)。
*
* 补完以下"活体返回0条/功能在但无数据演示"缺口:
* 1. WwMsdsFileMSDS 化学品安全技术说明书文件库(8种常用危化品)
* 2. OpsEmergencySupply:应急物资台账种子数据(中和剂/吸附棉/围堰三类)
* 3. EnvDischargePermit:排污许可证合规执行台账(4条:废水+废气+噪声各项指标)
* 4. WwSamplingTask:废水采样任务演示数据(覆盖不同状态和超标场景,供 Gap-11 桥接演示)
*/
@Order(9)
@Component
public class WwOpsSeeder implements CommandLineRunner {
private final WwMsdsFileRepository msdsRepo;
private final OpsEmergencySupplyRepository supplyRepo;
private final EnvDischargePermitRepository permitRepo;
private final WwSamplingTaskRepository samplingRepo;
public WwOpsSeeder(WwMsdsFileRepository msdsRepo,
OpsEmergencySupplyRepository supplyRepo,
EnvDischargePermitRepository permitRepo,
WwSamplingTaskRepository samplingRepo) {
this.msdsRepo = msdsRepo;
this.supplyRepo = supplyRepo;
this.permitRepo = permitRepo;
this.samplingRepo = samplingRepo;
}
@Override
public void run(String... args) {
seedMsdsFiles();
seedEmergencySupplies();
seedDischargePermits();
seedSamplingTasks();
}
// ------------------------------------------------------------------ MSDS 文件库
private void seedMsdsFiles() {
if (msdsRepo.count() > 0) return;
msds("MSDS-001", "硫酸", "7664-93-9", "浓硫酸", "腐蚀品", "I级",
"强腐蚀性,接触皮肤/眼睛造成严重灼伤;吸入蒸气损伤呼吸道",
"泄漏用干沙/干土覆盖吸收,以石灰/纯碱中和;皮肤接触立即用大量清水冲洗15分钟",
"化学防护手套+防化服+防酸护目镜+A型防毒面具",
"阴凉干燥处,与碱类/有机物/金属氧化剂严格分开存放,双人双锁库房",
"2024版", "硫酸MSDS_2024版.pdf", "全站", "东方化工", "2024-01-15", "2025-01-15", "王芳");
msds("MSDS-002", "液碱", "1310-73-2", "氢氧化钠/烧碱", "腐蚀品", "II级",
"强腐蚀性碱,吸入粉尘/液雾刺激呼吸道;皮肤接触引起灼伤",
"泄漏用沙土/干土吸收,以稀盐酸/食用醋中和;皮肤接触立即清水冲洗",
"化学防护手套+防护服+防护眼镜",
"密封储存,与酸类/易燃物分开,防潮防雨",
"2024版", "液碱MSDS_2024版.pdf", "全站", "华正化工", "2024-02-10", "2025-02-10", "刘洋");
msds("MSDS-003", "次氯酸钠", "7681-52-9", "NaClO/漂白液", "氧化剂", "III级",
"强氧化性,与有机物接触可能引起燃烧爆炸;挥发氯气刺激呼吸道和眼睛",
"泄漏用碳酸氢钠(小苏打)中和,通风排气;误入眼睛立即用大量清水冲洗",
"橡胶手套+护目镜+防氯口罩",
"阴凉通风密封储存,远离酸类/有机物/还原剂;避免阳光直射",
"2023版", "次氯酸钠MSDS_2023版.pdf", "全站", "科达化学", "2023-08-01", "2024-08-01", "陈静");
msds("MSDS-004", "双氧水", "7722-84-1", "过氧化氢/H2O2(30%)", "氧化剂", "II级",
"强氧化性,高浓度接触皮肤/眼睛引起灼伤;分解产生氧气,高温易爆",
"泄漏立即用大量水稀释冲洗;燃烧/爆炸风险时撤离并通知消防",
"聚乙烯手套+护目镜+防护面罩",
"低温阴凉储存(避免超过30℃),远离金属催化剂/有机物/强酸,使用专用容器",
"2024版", "双氧水MSDS_2024版.pdf", "全站", "天津大沽化工", "2024-03-01", "2025-03-01", "王芳");
msds("MSDS-005", "亚硫酸氢钠", "7631-90-5", "NaHSO3/保险粉", "还原剂", "III级",
"刺激性,吸入粉尘刺激上呼吸道;与酸接触释放SO2毒气",
"泄漏用干沙覆盖,避免与酸接触;皮肤接触用清水冲洗",
"防尘口罩+手套+护目镜",
"密封干燥处存放,远离酸/氧化剂",
"2023版", "亚硫酸氢钠MSDS_2023版.pdf", "水厂1号", "鑫辉化工", "2023-06-01", "2024-06-01", "刘洋");
msds("MSDS-006", "PAC(聚合氯化铝)", "1327-41-9", "聚铝/净水剂", "刺激品", "IV级",
"粉末吸入刺激呼吸道;溶液接触眼睛轻度刺激",
"泄漏清理,防止进入水体;粉末操作戴口罩",
"防尘口罩+防护眼镜",
"干燥处密封存放,防潮",
"2024版", "PAC絮凝剂MSDS_2024版.pdf", "全站", "广州大源化工", "2024-01-01", "2026-01-01", "陈静");
msds("MSDS-007", "PAM(聚丙烯酰胺)", "9003-05-8", "聚丙烯酰胺/助凝剂", "刺激品", "IV级",
"本身毒性低,但单体丙烯酰胺具神经毒性;粉末吸入刺激呼吸道",
"泄漏立即清除,防止地面湿滑;吸入立即转移到新鲜空气处",
"防尘口罩+手套",
"干燥阴凉处密封存放,远离热源",
"2024版", "PAM助凝剂MSDS_2024版.pdf", "全站", "三达新材料", "2024-02-01", "2026-02-01", "王芳");
msds("MSDS-008", "液氯", "7782-50-5", "氯气(液态)", "毒害品", "I级",
"剧毒,低浓度刺激眼/鼻/喉;高浓度引起肺水肿,严重时危及生命",
"立即疏散至上风向;穿戴全套防化服,用碱液(NaOH溶液)喷洒中和泄漏源;紧急联系消防",
"全密闭防化服+自给式空气呼吸器(SCBA)+防化手套",
"专用密闭储罐,设置安全围堰和泄漏检测,远离火源热源,氯气库房单独存放",
"2024版", "液氯MSDS_2024版.pdf", "水厂1号", "中氯工业", "2024-04-01", "2025-04-01", "刘洋");
}
private void msds(String code, String chemName, String casNo, String alias,
String hazardClass, String hazLevel, String healthHazard,
String emergencyAction, String ppe, String storageReq,
String fileVersion, String fileName, String plant,
String supplier, String publishDate, String reviewDate,
String maintainer) {
WwMsdsFile m = new WwMsdsFile();
m.setMsdsCode(code);
m.setChemName(chemName);
m.setCasNo(casNo);
m.setAlias(alias);
m.setHazardClass(hazardClass);
m.setHazLevel(hazLevel);
m.setHealthHazard(healthHazard);
m.setEmergencyAction(emergencyAction);
m.setPpe(ppe);
m.setStorageReq(storageReq);
m.setFileVersion(fileVersion);
m.setFileName(fileName);
m.setApplicablePlant(plant);
m.setSupplier(supplier);
m.setPublishDate(publishDate);
m.setReviewDate(reviewDate);
m.setStatus("有效");
m.setMaintainer(maintainer);
m.setCreatedAt(Instant.now());
m.setUpdatedAt(Instant.now());
msdsRepo.save(m);
}
// ------------------------------------------------------------------ 应急物资台账
private void seedEmergencySupplies() {
if (supplyRepo.count() > 0) return;
// 石灰(中和剂)- 水厂1号
supply("EMS-IN-001", "石灰(中和剂)", "中和剂", "工业级CaO", "水厂1号",
"危化品库A区01号架", "入库", 500d, "kg", 500d, 200d,
"2026-04-01", "2027-04-01", "王芳", "常备中和应急用料");
// 碳酸钠(中和剂)- 水厂1号
supply("EMS-IN-002", "碳酸钠(中和剂)", "中和剂", "工业纯Na2CO3", "水厂1号",
"危化品库A区02号架", "入库", 200d, "kg", 200d, 80d,
"2026-04-10", "2027-04-10", "王芳", "应急中和备用");
// 吸附棉 - 水厂1号
supply("EMS-IN-003", "吸附棉", "吸附棉", "通用型PP吸附棉40cm×50m", "水厂1号",
"应急物资库B区01号架", "入库", 20d, "", 20d, 8d,
"2026-03-15", "2028-03-15", "刘洋", "化学品泄漏应急吸附");
// 吸附棉 - 水厂2号
supply("EMS-IN-004", "吸附棉", "吸附棉", "通用型PP吸附棉40cm×50m", "水厂2号",
"应急物资库B区01号架", "入库", 15d, "", 15d, 6d,
"2026-03-20", "2028-03-20", "陈静", "化学品泄漏应急吸附");
// 围堰 - 水厂1号(出库演示,测试低库存预警)
supply("EMS-IN-005", "围堰", "围堰", "折叠式防污围堰1m*1m*30cm", "水厂1号",
"应急物资库C区", "入库", 6d, "", 6d, 4d,
"2026-02-01", "2029-02-01", "王芳", "液体泄漏围堵");
// 围堰出库(演示当前库存低于安全库存预警)
supply("EMS-OUT-001", "围堰", "围堰", "折叠式防污围堰1m*1m*30cm", "水厂1号",
"应急物资库C区", "领用出库", 4d, "", 2d, 4d,
"2026-05-10", null, "王芳", "应急预案演练领用");
// 防护装备 - 水厂1号
supply("EMS-IN-006", "防化服", "防护装备", "A级全封闭防化服", "水厂1号",
"应急物资库D区", "入库", 4d, "", 4d, 2d,
"2026-01-15", "2028-01-15", "刘洋", "液氯/强酸应急防护");
// 洗眼液 - 全站
supply("EMS-IN-007", "洗眼液", "洗眼站耗材", "洗眼液500ml/瓶", "水厂1号",
"急救箱", "入库", 24d, "", 24d, 12d,
"2026-04-05", "2027-04-05", "陈静", "腐蚀性化学品接触急救");
}
private void supply(String code, String supplyName, String supplyType, String spec,
String plant, String storageLocation, String opType,
Double quantity, String unit, Double currentStock,
Double safetyStock, String opDate, String expiryDate,
String operator, String remark) {
OpsEmergencySupply s = new OpsEmergencySupply();
s.setCode(code);
s.setSupplyName(supplyName);
s.setSupplyType(supplyType);
s.setSpec(spec);
s.setPlant(plant);
s.setStorageLocation(storageLocation);
s.setOpType(opType);
s.setQuantity(quantity);
s.setUnit(unit);
s.setCurrentStock(currentStock);
s.setSafetyStock(safetyStock);
s.setOpDate(opDate);
s.setExpiryDate(expiryDate);
s.setOperator(operator);
s.setRemark(remark);
s.setCreatedAt(Instant.now());
supplyRepo.save(s);
}
// ------------------------------------------------------------------ 排污许可证台账
private void seedDischargePermits() {
if (permitRepo.count() > 0) return;
// 废水综合排放 - COD
permit("PWXK-2024-001", "GWXK-2024-HN-001001", "废水", "DW001-综合废水排放口",
"COD", 50d, "mg/L", "月度在线+季度手工",
"岳阳市生态环境局", "2024-01-01", "2029-12-31", "有效");
// 废水综合排放 - 氨氮
permit("PWXK-2024-002", "GWXK-2024-HN-001001", "废水", "DW001-综合废水排放口",
"氨氮", 5d, "mg/L", "月度在线+季度手工",
"岳阳市生态环境局", "2024-01-01", "2029-12-31", "有效");
// 废水综合排放 - 总磷
permit("PWXK-2024-003", "GWXK-2024-HN-001001", "废水", "DW001-综合废水排放口",
"总磷", 0.5d, "mg/L", "季度手工",
"岳阳市生态环境局", "2024-01-01", "2029-12-31", "有效");
// 废气 - 臭气浓度(即将到期,测试提醒)
permit("PWXK-2024-004", "GWXK-2024-HN-001002", "废气", "DA001-污水处理区无组织废气",
"臭气浓度", 20d, "无量纲", "半年度手工",
"岳阳市生态环境局", "2022-01-01", "2024-12-31", "已过期");
// 噪声
permit("PWXK-2024-005", "GWXK-2024-HN-001003", "噪声", "厂界噪声",
"噪声dB(A)", 65d, "dB(A)", "半年度手工",
"岳阳市生态环境局", "2024-01-01", "2026-06-30", "即将到期");
}
private void permit(String code, String permitNo, String envType, String outlet,
String pollutant, Double limitValue, String unit,
String monitorRequire, String issuer,
String validFrom, String validTo, String status) {
if (permitRepo.existsByPermitNo(permitNo + "-" + pollutant)) return;
EnvDischargePermit p = new EnvDischargePermit();
p.setCode(code);
p.setPermitNo(permitNo);
p.setEnvType(envType);
p.setOutlet(outlet);
p.setPollutant(pollutant);
p.setLimitValue(limitValue);
p.setUnit(unit);
p.setMonitorRequire(monitorRequire);
p.setIssuer(issuer);
p.setValidFrom(validFrom);
p.setValidTo(validTo);
p.setStatus(status);
p.setCreatedAt(Instant.now());
permitRepo.save(p);
}
// ------------------------------------------------------------------ 废水采样任务(Gap-11 桥接演示)
private void seedSamplingTasks() {
if (samplingRepo.count() > 0) return;
// 任务1:达标 - 岳阳恒昌印染来水
sampling("WW-2026-001", "岳阳恒昌印染来水口", "企业来水", 1L,
"岳阳恒昌印染有限公司", "2026-05-08", "2026-05-08 09:30",
"陈静", 86d, 12.5d, null, 0.4d, 7.2d, null,
null, null, null, null, null, null, null,
"GB8978", "达标", null, "已检测", "陈静", "2026-05-08");
// 任务2:超标 - 岳阳恒昌印染来水(COD超标)
sampling("WW-2026-002", "岳阳恒昌印染来水口", "企业来水", 1L,
"岳阳恒昌印染有限公司", "2026-06-08", "2026-06-08 08:45",
"陈静", 132d, 18.6d, null, 0.7d, 6.8d, null,
null, null, null, null, null, null, null,
"GB8978", "超标", "COD", "已检测", "陈静", "2026-06-08");
// 任务3:达标 - 湖南华盛化工来水
sampling("WW-2026-003", "湖南华盛化工来水口", "企业来水", 2L,
"湖南华盛化工股份有限公司", "2026-05-12", "2026-05-12 10:15",
"王芳", 72d, 9.8d, null, 0.3d, 7.5d, null,
0.05d, 0.2d, null, null, null, null, null,
"纳管标准", "达标", null, "已检测", "王芳", "2026-05-12");
// 任务4:预警 - 化工来水(六价铬接近限值)
sampling("WW-2026-004", "湖南华盛化工来水口", "企业来水", 2L,
"湖南华盛化工股份有限公司", "2026-06-12", "2026-06-12 09:00",
"王芳", 95d, 14.2d, null, 0.55d, 7.0d, null,
0.08d, 0.45d, null, null, null, null, null,
"纳管标准", "预警", "六价铬", "已检测", "王芳", "2026-06-12");
// 任务5:超标 - 制药来水(氨氮超标)
sampling("WW-2026-005", "靖州康宁制药来水口", "企业来水", 4L,
"靖州康宁制药有限公司", "2026-05-18", "2026-05-18 11:00",
"刘洋", 148d, 22.4d, null, 0.82d, 6.5d, null,
null, null, null, null, null, null, null,
"GB8978", "超标", "COD;氨氮", "已检测", "刘洋", "2026-05-18");
// 任务6:工艺段 - 好氧池出水
sampling("WW-2026-006", "好氧池出水", "工艺段", null,
null, "2026-06-01", "2026-06-01 14:00",
"陈静", 38d, 3.2d, 8.5d, 0.15d, 7.3d, 4.5d,
null, null, null, null, null, null, null,
"工艺控制值", "达标", null, "已检测", "陈静", "2026-06-01");
// 任务7:最终出水 - 达标排放
sampling("WW-2026-007", "最终出水口", "出水", null,
null, "2026-06-08", "2026-06-08 15:30",
"王芳", 32d, 2.8d, 7.1d, 0.12d, 7.5d, 3.2d,
null, null, null, null, null, null, null,
"GB8978", "达标", null, "已检测", "王芳", "2026-06-08");
// 任务8:待采样(未执行)
sampling("WW-2026-008", "长沙金属表面电镀来水口", "企业来水", 5L,
"长沙金属表面电镀厂", "2026-06-15", null,
"刘洋", null, null, null, null, null, null,
null, null, null, null, null, null, null,
"GB8978", null, null, "待采样", null, null);
}
private void sampling(String sampleCode, String samplePoint, String sampleType,
Long clientId, String clientName, String planDate, String actualTime,
String sampler, Double cod, Double ammonia, Double tn, Double tp,
Double ph, Double ss,
Double cr6, Double totalCr, Double cyanide,
Double phenol, Double nickel, Double copper, Double zinc,
String standard, String judgeResult, String exceedItems,
String status, String analyst, String analyzeDate) {
WwSamplingTask t = new WwSamplingTask();
t.setSampleCode(sampleCode);
t.setSamplePoint(samplePoint);
t.setSampleType(sampleType);
t.setClientId(clientId);
t.setClientName(clientName);
t.setPlanDate(planDate);
t.setActualTime(actualTime);
t.setSampler(sampler);
t.setCodResult(cod);
t.setAmmoniaResult(ammonia);
t.setTnResult(tn);
t.setTpResult(tp);
t.setPhResult(ph);
t.setSsResult(ss);
t.setCr6Result(cr6);
t.setTotalCrResult(totalCr);
t.setCyanideResult(cyanide);
t.setPhenolResult(phenol);
t.setNickelResult(nickel);
t.setCopperResult(copper);
t.setZincResult(zinc);
t.setStandard(standard);
t.setJudgeResult(judgeResult);
t.setExceedItems(exceedItems);
t.setStatus(status);
t.setAnalyst(analyst);
t.setAnalyzeDate(analyzeDate);
t.setCreatedAt(Instant.now());
samplingRepo.save(t);
}
}