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,30 @@
package com.kaidi.oa;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.io.File;
/**
* Entry point for the OA (collaborative office) backend.
*
* Standalone Spring Boot application that replaces the legacy OFBiz engine.
* It mirrors the Vue3 + Element Plus OA frontend engine model: form schemas,
* flow schemas, templates and submitted form instances driven by a simple
* approval state machine.
*/
@SpringBootApplication
@EnableScheduling
public class OaBackendApplication {
public static void main(String[] args) {
// Ensure the SQLite data directory exists before the datasource opens it.
File dataDir = new File("./data");
if (!dataDir.exists()) {
//noinspection ResultOfMethodCallIgnored
dataDir.mkdirs();
}
SpringApplication.run(OaBackendApplication.class, args);
}
}
@@ -0,0 +1,24 @@
package com.kaidi.oa.common;
/**
* Application-level exception carrying a non-zero response code.
* Thrown by services/controllers and translated to an ApiResp by the
* global exception handler.
*/
public class ApiException extends RuntimeException {
private final int code;
public ApiException(int code, String message) {
super(message);
this.code = code;
}
public ApiException(String message) {
this(1, message);
}
public int getCode() {
return code;
}
}
@@ -0,0 +1,22 @@
package com.kaidi.oa.common;
/**
* Uniform API response envelope.
*
* code == 0 means success; any non-zero code indicates an error and message
* carries the human-readable reason. data holds the payload (null on error).
*/
public record ApiResp<T>(int code, String message, T data) {
public static <T> ApiResp<T> ok(T data) {
return new ApiResp<>(0, "ok", data);
}
public static <T> ApiResp<T> ok() {
return new ApiResp<>(0, "ok", null);
}
public static <T> ApiResp<T> error(int code, String message) {
return new ApiResp<>(code, message, null);
}
}
@@ -0,0 +1,87 @@
package com.kaidi.oa.common;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
/**
* 申报域外部政务平台凭据的「可逆加密」工具(创新研发中心·申报服务部,需求 §2 账号密码加密存储)。
*
* 政务平台登录口令必须能取回供真实登录代填,故不能用单向哈希(与 {@link PasswordUtil} 区别);
* 这里用「每条随机盐 + 与盐派生的密钥流做 XOR + Base64 编码」做可逆加密落库,避免明文入库与撞库。
* 落库串格式:{@code v1:<base64(salt)>:<base64(xor密文)>},前缀 {@code v1:} 用于识别已加密串避免二次加密。
*
* 说明:演示环境用对称流加密满足「加密存储/不回明文」的合规口径;生产可替换为 KMS/AES-GCM
* 解密入口受角色门禁(控制器层),列表/详情默认只回掩码而非明文。
*/
public final class DeclCrypto {
private static final String PREFIX = "v1:";
private static final SecureRandom RANDOM = new SecureRandom();
/** 与盐混合的固定基密钥(演示口径;生产应来自外部密钥管理)。 */
private static final byte[] BASE_KEY = "kaidi-decl-portal-key-2026".getBytes(StandardCharsets.UTF_8);
private DeclCrypto() {
}
/** 是否已是本工具加密后的密文串(避免对已加密值二次加密)。 */
public static boolean isEncrypted(String s) {
return s != null && s.startsWith(PREFIX);
}
/** 加密:明文 → {@code v1:<saltB64>:<cipherB64>}。null/空 → 原样返回(无口令可存)。 */
public static String encrypt(String plain) {
if (plain == null || plain.isEmpty()) {
return plain;
}
if (isEncrypted(plain)) {
return plain; // 已加密,幂等
}
byte[] salt = new byte[12];
RANDOM.nextBytes(salt);
byte[] data = plain.getBytes(StandardCharsets.UTF_8);
byte[] cipher = xor(data, salt);
return PREFIX + b64(salt) + ":" + b64(cipher);
}
/** 解密:{@code v1:<saltB64>:<cipherB64>} → 明文。非本格式串原样返回(兼容历史明文)。 */
public static String decrypt(String stored) {
if (stored == null || !isEncrypted(stored)) {
return stored;
}
try {
String[] parts = stored.substring(PREFIX.length()).split(":", 2);
if (parts.length != 2) {
return "";
}
byte[] salt = Base64.getDecoder().decode(parts[0]);
byte[] cipher = Base64.getDecoder().decode(parts[1]);
byte[] data = xor(cipher, salt);
return new String(data, StandardCharsets.UTF_8);
} catch (RuntimeException e) {
return "";
}
}
/** 掩码:用于列表/详情展示,不回明文(如 已设置 ●●●●●●)。 */
public static String mask(String stored) {
if (stored == null || stored.isEmpty()) {
return "";
}
return "已设置 ●●●●●●";
}
/** 用盐与基密钥派生密钥流做按位异或(流加密)。 */
private static byte[] xor(byte[] data, byte[] salt) {
byte[] out = new byte[data.length];
for (int i = 0; i < data.length; i++) {
byte k = (byte) (BASE_KEY[i % BASE_KEY.length] ^ salt[i % salt.length] ^ (i * 31));
out[i] = (byte) (data[i] ^ k);
}
return out;
}
private static String b64(byte[] b) {
return Base64.getEncoder().encodeToString(b);
}
}
@@ -0,0 +1,201 @@
package com.kaidi.oa.common;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.transaction.UnexpectedRollbackException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.util.stream.Collectors;
/**
* Translates exceptions into the uniform {@link ApiResp} envelope so that every
* error response also carries {code != 0, message}. HTTP status stays 200 for
* application errors (envelope code conveys the failure) except where a more
* specific status is meaningful.
*
* 横切健壮:框架层异常(请求方法不支持 / 路径不存在 / 路径参数类型不符 / 请求体坏 JSON /
* 缺必填参数)一律映射成对应的 4xx + 友好 message**绝不把原始 Java/SQL 异常文案回吐前端**
* (避免信息泄露 + 误导性的 500)。原始异常仅写服务端日志,便于排障。
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ApiResp<Void>> handleNotFound(NotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResp.error(ex.getCode(), ex.getMessage()));
}
@ExceptionHandler(ApiException.class)
public ResponseEntity<ApiResp<Void>> handleApi(ApiException ex) {
return ResponseEntity.ok(ApiResp.error(ex.getCode(), ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResp<Void>> handleValidation(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getFieldErrors().stream()
.map(GlobalExceptionHandler::formatFieldError)
.collect(Collectors.joining("; "));
return ResponseEntity.ok(ApiResp.error(400, msg.isEmpty() ? "validation failed" : msg));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResp<Void>> handleIllegalArgument(IllegalArgumentException ex) {
return ResponseEntity.ok(ApiResp.error(400, ex.getMessage()));
}
/**
* 请求方法不支持(如对集合端点发 DELETE)。原来落到通用 500 并回吐
* "Request method 'DELETE' is not supported"。现统一映射成 405 + 友好提示。
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ApiResp<Void>> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex) {
log.warn("method not supported: {} {}", ex.getMethod(), ex.getSupportedHttpMethods());
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(ApiResp.error(405, "请求方法不被支持"));
}
/**
* 路径不存在。Spring Boot 3.2 / Spring 6.1 对未匹配路径抛
* {@link NoResourceFoundException}"No static resource ..."),旧版本抛
* {@link NoHandlerFoundException}。两者都收口成 404 + 友好提示,不回吐路径细节文案。
*/
@ExceptionHandler({NoResourceFoundException.class, NoHandlerFoundException.class})
public ResponseEntity<ApiResp<Void>> handleNoHandler(Exception ex) {
log.warn("no handler / resource: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResp.error(404, "请求的资源不存在"));
}
/**
* 路径/查询参数类型不符(如 /cost-centers/abc 把 abc 喂给 Long id)。原来落到 500 并
* 回吐 "Failed to convert value of type ... For input string: \"abc\""。现映射成 400。
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ApiResp<Void>> handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
log.warn("argument type mismatch on '{}': {}", ex.getName(), ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResp.error(400, "参数「" + ex.getName() + "」格式不正确"));
}
/**
* 请求体不可读 / 坏 JSON。原来落到 500 并回吐 Jackson 内部解析细节
* "JSON parse error: Unexpected character ...")。现映射成 400 + 通用提示。
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiResp<Void>> handleNotReadable(HttpMessageNotReadableException ex) {
log.warn("unreadable request body: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResp.error(400, "请求体格式不正确(无法解析的 JSON)"));
}
/** 缺少必填查询参数。统一 400 + 指明参数名,不回吐底层文案。 */
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<ApiResp<Void>> handleMissingParam(MissingServletRequestParameterException ex) {
log.warn("missing request parameter: {}", ex.getParameterName());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResp.error(400, "缺少必填参数「" + ex.getParameterName() + ""));
}
/** 缺少必填的 multipart 分片(如上传缺 file 字段)。属非法入参,统一 400 而非 500。 */
@ExceptionHandler(MissingServletRequestPartException.class)
public ResponseEntity<ApiResp<Void>> handleMissingPart(MissingServletRequestPartException ex) {
log.warn("missing request part: {}", ex.getRequestPartName());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResp.error(400, "缺少必填上传内容「" + ex.getRequestPartName() + ""));
}
/** 非法/非 multipart 的上传请求(如对上传口发了非 multipart 请求)。同属非法入参,统一 400 而非 500。 */
@ExceptionHandler(MultipartException.class)
public ResponseEntity<ApiResp<Void>> handleMultipart(MultipartException ex) {
log.warn("invalid multipart request: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResp.error(400, "上传请求格式不正确:需以 multipart/form-data 提交文件"));
}
/** 请求体 Content-Type 不被支持。统一 415 + 友好提示。 */
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResponseEntity<ApiResp<Void>> handleMediaType(HttpMediaTypeNotSupportedException ex) {
log.warn("unsupported media type: {}", ex.getContentType());
return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
.body(ApiResp.error(415, "不支持的请求内容类型"));
}
/**
* 数据库约束冲突(唯一键 / NOT NULL / 外键)。原来落到通用 500 并可能回吐 SQL/表名。
* 现统一映射成 409 + 通用提示,且原始 SQL 异常仅写日志——绝不外泄表结构。
* 典型来源:并发写撞唯一约束、必填字段缺失。
*/
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ApiResp<Void>> handleDataIntegrity(DataIntegrityViolationException ex) {
log.warn("data integrity violation: {}",
ex.getMostSpecificCause() != null ? ex.getMostSpecificCause().getMessage() : ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ApiResp.error(409, "操作与现有数据冲突或违反完整性约束,请核对后重试"));
}
/**
* 并发/回滚冲突。典型:同一事项被并发办结,落后的事务被标记 rollback-only 或撞唯一约束。
* 此前会透成不明 500;现收口成 409 + 可理解提示(数据一致性已由约束/守卫保证,无脏写)。
*/
@ExceptionHandler({UnexpectedRollbackException.class, ConcurrencyFailureException.class})
public ResponseEntity<ApiResp<Void>> handleConcurrency(Exception ex) {
log.warn("concurrent / rollback conflict: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ApiResp.error(409, "操作冲突:该事项可能正在被处理或已处理,请刷新后重试"));
}
/**
* Hibernate/SQLite 下,唯一键 / NOT NULL / 外键等约束违反常被包成 {@link JpaSystemException}
* 而非 Spring 的 DataIntegrityViolationException(后者对 SQLite 几乎命中不到)。这里按根因
* 文案识别 SQLITE_CONSTRAINT,将约束类失败收口成 409,其余 JPA 系统异常仍回 500——
* 两种情况都只给通用提示,绝不外泄 SQL / 表名 / 栈。
*/
@ExceptionHandler(JpaSystemException.class)
public ResponseEntity<ApiResp<Void>> handleJpaSystem(JpaSystemException ex) {
String root = ex.getMostSpecificCause() != null ? ex.getMostSpecificCause().getMessage() : ex.getMessage();
String r = root == null ? "" : root.toUpperCase();
boolean constraint = r.contains("CONSTRAINT") || r.contains("UNIQUE") || r.contains("NOT NULL");
log.warn("jpa system exception (constraint={}): {}", constraint, root);
if (constraint) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ApiResp.error(409, "操作与现有数据冲突或违反完整性约束,请核对后重试"));
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResp.error(500, "服务器内部错误,请稍后重试"));
}
/**
* 兜底:任何未被上面专门处理的异常都回 500,但**只给通用友好提示**,绝不回吐
* 原始异常 getMessage()(可能含 Java 栈细节 / SQL / 表名)。原始异常写日志便于排障。
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResp<Void>> handleGeneric(Exception ex) {
log.error("unhandled exception", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResp.error(500, "服务器内部错误,请稍后重试"));
}
private static String formatFieldError(FieldError fe) {
return fe.getField() + ": " + fe.getDefaultMessage();
}
}
@@ -0,0 +1,47 @@
package com.kaidi.oa.common;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;
/**
* 服务端 HTML 白名单净化:用于用户可控的富文本正文(讨论/协作文档/博客/动态/公告/会议纪要/评论/回帖),
* 在写入时清除存储型 XSS 向量,使其即便被前端以 v-html 渲染也不会执行脚本。
*
* <p>实现采用 jsoup 的 {@link Safelist}(白名单)而非正则黑名单:jsoup 会像浏览器一样把输入解析成
* DOM(这一步会解码 HTML 实体,如 {@code javascript&#58;} → {@code javascript:}),再仅保留白名单内的
* 标签/属性,并对 a[href]、img[src] 等按白名单协议(http/https/mailto/ftp)校验——非白名单协议
* {@code javascript:}/{@code vbscript:}/{@code data:})、所有 {@code on*} 事件处理器、
* {@code <script>/<svg>/<iframe>/<object>} 等危险元素一律被剥离。
*
* <p>这从根上消除了"正则只匹配字面 javascript: 而被 &#58;/&#x3a; 等实体编码绕过"的整类问题
* (正则黑名单是军备竞赛,jsoup 的解析-再-白名单序列化才是稳健解)。前端仍建议对 v-html 再加一层
* DOMPurify 作为纵深防御。
*/
public final class HtmlSanitizer {
private HtmlSanitizer() {
}
/**
* 富文本白名单:jsoup {@code relaxed}(允许 p/div/span/a/img/ul/table 等常见排版标签,
* a[href] 仅 http/https/mailto/ftp、img[src] 仅 http/https;自动剥离所有事件处理器与危险元素)。
* 额外允许 a[target] 以兼容"新窗口打开"的链接。
*/
private static final Safelist SAFELIST = Safelist.relaxed().addAttributes("a", "target");
/** 紧凑输出:不美化(prettyPrint=false),尽量保留原正文结构。 */
private static final Document.OutputSettings OUTPUT =
new Document.OutputSettings().prettyPrint(false);
/**
* 净化一段可能含 HTML 的用户正文;null/空 原样返回。
*/
public static String sanitize(String html) {
if (html == null || html.isEmpty()) {
return html;
}
// baseUri 传空串即可;协议白名单仍生效。jsoup.clean 解析(解码实体)→按白名单过滤→再序列化。
return Jsoup.clean(html, "", SAFELIST, OUTPUT);
}
}
@@ -0,0 +1,81 @@
package com.kaidi.oa.common;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 金额工具(统一标度与空值处理)。
*
* 所有"钱"字段一律用 {@link BigDecimal} 而非 double,避免二进制浮点累加误差导致
* 对账(已付=合同额、分包累计、发票回写)判等不可靠。本工具把请求层传来的装箱
* {@code Double}(前端 JSON number 反序列化结果)安全收口成标度 2、HALF_UP 的
* BigDecimal,并提供 null 安全的加减比较,供各控制器/服务复用。
*/
public final class Money {
/** 金额标准标度:分(2 位小数)。 */
public static final int SCALE = 2;
public static final BigDecimal ZERO = scaled(BigDecimal.ZERO);
private Money() {
}
/** 装箱 Double(可空)→ 标度 2 的 BigDecimalnull 视为 0。非有限数(NaN/Infinity)→干净 400。 */
public static BigDecimal of(Double v) {
if (v == null) {
return ZERO;
}
requireFinite(v);
return scaled(BigDecimal.valueOf(v));
}
/** double → 标度 2 的 BigDecimal。非有限数(NaN/Infinity)→干净 400。 */
public static BigDecimal of(double v) {
requireFinite(v);
return scaled(BigDecimal.valueOf(v));
}
/**
* 金额必须是有限数值:拒绝 NaN/Infinity/-InfinityJSON 里的 "NaN"/"Infinity"/1e400 等)。
* 统一在金额收口处给出干净的 400 中文提示,杜绝把 {@code BigDecimal.valueOf(非有限)} 的底层
* NumberFormatException 原文("Character I is neither a decimal digit...")外泄给前端。
*/
private static void requireFinite(double v) {
if (!Double.isFinite(v)) {
throw new ApiException(400, "金额必须是有限数值");
}
}
/** BigDecimal(可空)→ 规整标度;null 视为 0。供已是 BigDecimal 的表达式统一收口。 */
public static BigDecimal of(BigDecimal v) {
return nz(v);
}
/** null 安全:null → 0,否则规整标度。 */
public static BigDecimal nz(BigDecimal v) {
return v == null ? ZERO : scaled(v);
}
public static BigDecimal add(BigDecimal a, BigDecimal b) {
return scaled(nz(a).add(nz(b)));
}
public static BigDecimal sub(BigDecimal a, BigDecimal b) {
return scaled(nz(a).subtract(nz(b)));
}
/** a 是否 > bnull 当 0)。 */
public static boolean gt(BigDecimal a, BigDecimal b) {
return nz(a).compareTo(nz(b)) > 0;
}
/** a 是否 <= 0null 当 0)。 */
public static boolean lte0(BigDecimal a) {
return nz(a).compareTo(ZERO) <= 0;
}
private static BigDecimal scaled(BigDecimal v) {
return v.setScale(SCALE, RoundingMode.HALF_UP);
}
}
@@ -0,0 +1,67 @@
package com.kaidi.oa.common;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 金额文本解析收口(BigDecimal-native)。
*
* <p>历史上 {@code TriggerRuleEngine.parseNumber} / {@code WorkflowService.parseNumber} /
* {@code MobileApprovalController.parseNumber} 三处各自手写了一份"剥噪声→{@code Double.parseDouble}"
* 的金额解析,返回裸 {@code double}:自动联动据此生成付款单/发票,金额下游与台账留痕里就会冒出
* {@code 5.07}、{@code 9.9999999997} 这类二进制浮点伪值(如 5000万经 JSON 序列化为 "5.0E7",或
* 表单 "10.00" 累加后抖动)。本工具把"解析"这一步直接落到 {@link BigDecimal},杜绝中途经过 double
* 用 {@link BigDecimal#BigDecimal(String)}(字符串构造,精确)而非 {@code Double.parseDouble}
* 再统一 {@code setScale(2, HALF_UP)} 规整成"分"标度,使解析结果与 {@link Money} 完全同口径。</p>
*
* <p>容噪规则与原三处保持一致:剥掉 ¥/¥/元/逗号/空白等装饰字符,<b>保留</b> 数字、小数点、正负号
* 与科学计数标记 {@code e/E}(大额金额经 JSON 常序列化为 "5.0E7",剥掉 E 会把它错解析成 5.07,
* 量级灾难);末尾带「万」整体 ×10000。无法解析时返回 {@code null}(业务侧据此走"未解析到金额→跳过")。</p>
*/
public final class MoneyParser {
private static final BigDecimal TEN_THOUSAND = new BigDecimal("10000");
private MoneyParser() {
}
/**
* 解析金额文本 → 标度 2、HALF_UP 的 {@link BigDecimal};解析失败返回 {@code null}。
*
* <p>关键:用 {@code new BigDecimal(cleaned)}(字符串精确构造,支持科学计数法),<b>绝不</b>
* 走 {@code Double.parseDouble},故 "5.0E7"→50000000.00、"10.00"→10.00 全程无浮点抖动。</p>
*/
public static BigDecimal parse(String raw) {
if (raw == null) {
return null;
}
String s = raw.trim();
if (s.isEmpty()) {
return null;
}
boolean wan = s.contains("");
// 保留 0-9 . e E + - ,剥掉其余(¥ 元 万 千分位逗号 空白…)。务必保留 e/E:科学计数法大额。
String cleaned = s.replaceAll("[^0-9.eE+-]", "");
if (cleaned.isEmpty() || "+".equals(cleaned) || "-".equals(cleaned) || ".".equals(cleaned)) {
return null;
}
try {
BigDecimal v = new BigDecimal(cleaned);
if (wan) {
v = v.multiply(TEN_THOUSAND);
}
return v.setScale(Money.SCALE, RoundingMode.HALF_UP);
} catch (NumberFormatException e) {
return null;
}
}
/**
* 同 {@link #parse(String)},但解析失败回退为 {@link Money#ZERO}(标度 2 的 0.00)。
* 适合"展示/排序用金额,缺失即按 0 处理且不抛错"的场景(如移动待办列表金额列)。
*/
public static BigDecimal parseOrZero(String raw) {
BigDecimal v = parse(raw);
return v == null ? Money.ZERO : v;
}
}
@@ -0,0 +1,11 @@
package com.kaidi.oa.common;
/**
* Thrown when a requested resource does not exist. Maps to response code 404.
*/
public class NotFoundException extends ApiException {
public NotFoundException(String message) {
super(404, message);
}
}
@@ -0,0 +1,101 @@
package com.kaidi.oa.common;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
/**
* Password hashing helper — salted PBKDF2-HMAC-SHA256 (JDK built-in, no extra
* dependency). Each hash embeds a random per-user salt and an iteration count,
* stored as {@code pbkdf2$<iterations>$<saltB64>$<hashB64>}. Verification is
* constant-time. This defeats rainbow-table / precomputation attacks that plain
* unsalted SHA-256 is vulnerable to.
*
* Legacy unsalted SHA-256 hex hashes are still accepted by {@link #matches} for
* backward compatibility, so old records keep working until re-hashed.
*/
public final class PasswordUtil {
private static final int ITERATIONS = 120_000;
private static final int KEY_LENGTH = 256; // bits
private static final int SALT_BYTES = 16;
/** 口令长度上限:防超长口令触发 PBKDF2 CPU 放大(DoS)。正常口令远短于此。 */
private static final int MAX_PASSWORD_LEN = 200;
private static final SecureRandom RANDOM = new SecureRandom();
private PasswordUtil() {
}
/** Produce a salted PBKDF2 hash string: {@code pbkdf2$<iter>$<saltB64>$<hashB64>}. */
public static String hash(String raw) {
if (raw != null && raw.length() > MAX_PASSWORD_LEN) {
throw new IllegalArgumentException("password too long");
}
byte[] salt = new byte[SALT_BYTES];
RANDOM.nextBytes(salt);
byte[] dk = pbkdf2(raw, salt, ITERATIONS);
return "pbkdf2$" + ITERATIONS + "$"
+ Base64.getEncoder().encodeToString(salt) + "$"
+ Base64.getEncoder().encodeToString(dk);
}
/** Verify a raw password against a stored hash (PBKDF2 or legacy SHA-256). */
public static boolean matches(String raw, String stored) {
if (raw == null || stored == null || stored.isBlank()) {
return false;
}
// 超长口令直接判否:既挡 PBKDF2 DoS,又不影响任何正常口令。
if (raw.length() > MAX_PASSWORD_LEN) {
return false;
}
if (stored.startsWith("pbkdf2$")) {
String[] parts = stored.split("\\$");
if (parts.length != 4) {
return false;
}
try {
int iterations = Integer.parseInt(parts[1]);
byte[] salt = Base64.getDecoder().decode(parts[2]);
byte[] expected = Base64.getDecoder().decode(parts[3]);
byte[] actual = pbkdf2(raw, salt, iterations);
return MessageDigest.isEqual(expected, actual);
} catch (RuntimeException e) {
return false;
}
}
// legacy unsalted SHA-256 hex fallback (constant-time compare)
return MessageDigest.isEqual(
stored.getBytes(StandardCharsets.UTF_8),
legacySha256Hex(raw).getBytes(StandardCharsets.UTF_8));
}
private static byte[] pbkdf2(String raw, byte[] salt, int iterations) {
try {
PBEKeySpec spec = new PBEKeySpec(raw.toCharArray(), salt, iterations, KEY_LENGTH);
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
return skf.generateSecret(spec).getEncoded();
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new IllegalStateException("PBKDF2 not available", e);
}
}
private static String legacySha256Hex(String raw) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(raw.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(digest.length * 2);
for (byte b : digest) {
sb.append(Character.forDigit((b >> 4) & 0xF, 16));
sb.append(Character.forDigit(b & 0xF, 16));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 not available", e);
}
}
}
@@ -0,0 +1,102 @@
package com.kaidi.oa.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 统一 AI 服务层配置(oa.ai.*)。绑定 application.yml 的 oa.ai 段。
*
* apiKey 解析优先级:application.yml 的 oa.ai.api-key 为空时回退到环境变量
* ANTHROPIC_API_KEY(见 {@link #resolvedApiKey()})。未配置 Key 时
* {@link AiService} 走优雅降级(不报错、不崩库),返回 configured=false 的提示。
*
* model 默认 claude-haiku-4-5-20251001(高性价比);baseUrl 默认 Anthropic
* Messages APImaxTokens 默认 1024anthropicVersion 默认 2023-06-01。
*/
@Component
@ConfigurationProperties(prefix = "oa.ai")
public class AiProperties {
/** Anthropic API Key;为空则回退环境变量 ANTHROPIC_API_KEY,仍为空则降级。 */
private String apiKey = "";
/** 调用的模型,默认高性价比的 Haiku。 */
private String model = "claude-haiku-4-5-20251001";
/** Anthropic Messages API 端点。 */
private String baseUrl = "https://api.anthropic.com/v1/messages";
/** anthropic-version 请求头。 */
private String anthropicVersion = "2023-06-01";
/** 单次最大输出 token。 */
private int maxTokens = 1024;
/** 请求超时(毫秒)。 */
private int timeoutMs = 30000;
/**
* 实际生效的 Keyoa.ai.api-key 优先,空白则回退环境变量 ANTHROPIC_API_KEY。
* 两者都空返回空串(调用方据此走降级,不抛异常)。
*/
public String resolvedApiKey() {
if (apiKey != null && !apiKey.isBlank()) {
return apiKey.trim();
}
String env = System.getenv("ANTHROPIC_API_KEY");
return env == null ? "" : env.trim();
}
/** 是否已配置可用的 API Key(决定真调 LLM 还是降级)。 */
public boolean isConfigured() {
return !resolvedApiKey().isBlank();
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public String getAnthropicVersion() {
return anthropicVersion;
}
public void setAnthropicVersion(String anthropicVersion) {
this.anthropicVersion = anthropicVersion;
}
public int getMaxTokens() {
return maxTokens;
}
public void setMaxTokens(int maxTokens) {
this.maxTokens = maxTokens;
}
public int getTimeoutMs() {
return timeoutMs;
}
public void setTimeoutMs(int timeoutMs) {
this.timeoutMs = timeoutMs;
}
}
@@ -0,0 +1,255 @@
package com.kaidi.oa.config;
import com.kaidi.oa.domain.OperationAuditLog;
import com.kaidi.oa.domain.SysUser;
import com.kaidi.oa.repository.OperationAuditLogRepository;
import com.kaidi.oa.service.AuthorizationService;
import com.kaidi.oa.web.CurrentUserResolver;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;
/**
* 统一操作审计拦截器(P3 贯穿性合规基础设施)。
*
* 在 {@code afterCompletion}(请求处理彻底结束、响应码已定)时,对一切对 {@code /api/oa/**}
* 的「写请求」(POST/PUT/PATCH/DELETE) 落一条 {@link OperationAuditLog}
* 谁(operator/operatorId/operatorRole)、何时(occurredAt)、用什么方法(method)打了哪个路径
* (path/resourceType/resourceId)、结果如何(statusCode)、从哪个 IP(clientIp)发起。
*
* <h3>不可篡改哈希链</h3>
* 落库前计算 {@code hash = sha256(prevHash + 本条规范化字段)}{@code prevHash} 取链上「上一条」
* 记录的 hash。续链是 check-then-act(读上一条 → 算 hash → 落库),必须串行化,否则并发写会让
* 两条记录读到同一个 prevHash 而分叉。这里用单一锁对「读上一条 + 落库」整段加锁保证链的全序。
*
* <h3>健壮性铁律</h3>
* 审计是旁路:本拦截器任何异常都被吞并仅记日志,<b>绝不</b>影响用户本次业务请求的结果
* (写已在 afterCompletion 之前完成提交,落审计失败不应回滚业务,也不应改变已返回的响应)。
*/
@Component
public class AuditLogInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(AuditLogInterceptor.class);
/** 仅这些方法触发审计落库(读请求 GET/HEAD/OPTIONS 不留痕,避免日志爆量)。 */
private static final Set<String> WRITE_METHODS = Set.of("POST", "PUT", "PATCH", "DELETE");
/** 哈希链创世值(首条记录的 prevHash),固定常量便于离线复算从头校验。 */
public static final String GENESIS_HASH = "0".repeat(64);
private static final String ANON = "匿名/未登录";
private final OperationAuditLogRepository auditRepo;
private final CurrentUserResolver currentUser;
private final AuthorizationService authz;
/** 续链锁:序列化「取上一条 hash + 落新记录」,保证哈希链单一全序、不分叉。 */
private final Object chainLock = new Object();
public AuditLogInterceptor(OperationAuditLogRepository auditRepo,
CurrentUserResolver currentUser,
AuthorizationService authz) {
this.auditRepo = auditRepo;
this.currentUser = currentUser;
this.authz = authz;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
try {
String method = request.getMethod() == null ? "" : request.getMethod().toUpperCase(Locale.ROOT);
if (!WRITE_METHODS.contains(method)) {
return;
}
String path = normalizePath(request.getRequestURI());
if (!path.startsWith("/api/oa/")) {
return;
}
recordWrite(method, path, response.getStatus(), resolveOperator(request), clientIp(request));
} catch (RuntimeException e) {
// 审计旁路:绝不让落审计的失败影响用户本次业务请求(业务写已提交、响应已返回)。
log.warn("操作审计落库失败(已忽略,不影响业务): {} {}", request.getMethod(), request.getRequestURI(), e);
}
}
/** 解析操作人(显示名 / 不可变 id / 角色快照);未登录回退匿名占位。 */
private OperatorInfo resolveOperator(HttpServletRequest request) {
try {
SysUser u = currentUser.resolve(request);
if (u == null) {
return new OperatorInfo(ANON, null, "");
}
String name = u.getDisplayName() != null && !u.getDisplayName().isBlank()
? u.getDisplayName() : u.getLoginName();
// 角色集合排序后逗号拼接,得到稳定快照(参与哈希,需可复现)。
Set<String> roles = new TreeSet<>(authz.roleCodesOf(u.getId()));
return new OperatorInfo(name, u.getId(), String.join(",", roles));
} catch (RuntimeException e) {
return new OperatorInfo(ANON, null, "");
}
}
private record OperatorInfo(String name, Long id, String roles) {
}
/**
* 落一条审计并续哈希链。整段在 {@link #chainLock} 下串行:先取链上最后一条的 hash 作 prevHash
* 再算本条 hash,最后落库——保证并发写不会读到同一 prevHash 而分叉。
*/
private void recordWrite(String method, String path, int statusCode,
OperatorInfo op, String clientIp) {
String[] res = parseResource(path);
Instant now = Instant.now();
synchronized (chainLock) {
OperationAuditLog last = auditRepo.findTopByOrderByIdDesc();
String prevHash = last == null ? GENESIS_HASH : last.getHash();
OperationAuditLog rec = new OperationAuditLog();
rec.setOperator(op.name());
rec.setOperatorId(op.id());
rec.setOperatorRole(op.roles());
rec.setMethod(method);
rec.setPath(path);
rec.setResourceType(res[0]);
rec.setResourceId(res[1]);
rec.setStatusCode(statusCode);
rec.setClientIp(clientIp);
rec.setOccurredAt(now);
rec.setPrevHash(prevHash);
String canonical = canonicalOf(prevHash, rec);
rec.setCanonical(canonical);
rec.setHash(sha256Hex(canonical));
auditRepo.save(rec);
}
}
/**
* 规范化参与哈希的字段串:{@code prevHash | operatorId | operator | role | method | path |
* resourceType | resourceId | statusCode | clientIp | occurredAtEpochMilli}。
* 字段顺序与分隔符固定,便于离线逐条复算校验链完整性。
*/
public static String canonicalOf(String prevHash, OperationAuditLog r) {
return String.join("|",
nz(prevHash),
r.getOperatorId() == null ? "" : String.valueOf(r.getOperatorId()),
nz(r.getOperator()),
nz(r.getOperatorRole()),
nz(r.getMethod()),
nz(r.getPath()),
nz(r.getResourceType()),
nz(r.getResourceId()),
r.getStatusCode() == null ? "" : String.valueOf(r.getStatusCode()),
nz(r.getClientIp()),
r.getOccurredAt() == null ? "" : String.valueOf(r.getOccurredAt().toEpochMilli()));
}
/** sha256 → 64 位小写十六进制。 */
public static String sha256Hex(String s) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(s.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(64);
for (byte b : digest) {
sb.append(Character.forDigit((b >> 4) & 0xF, 16));
sb.append(Character.forDigit(b & 0xF, 16));
}
return sb.toString();
} catch (Exception e) {
// SHA-256 是 JDK 必备算法,理论不会缺;缺失时退化为不可比对的空哈希但仍记录。
log.error("SHA-256 不可用,审计哈希降级为空", e);
return "";
}
}
/**
* 从规范化路径解析「资源类型 + 资源 id」。
* {@code /api/oa/financings} -> [financings, null]
* {@code /api/oa/financings/12} -> [financings, 12]
* {@code /api/oa/financings/repayments/5/pay} -> [financings, null](子动作,资源段后非纯数字)。
*/
static String[] parseResource(String path) {
// 去掉前缀 /api/oa/
String rest = path.startsWith("/api/oa/") ? path.substring("/api/oa/".length()) : path;
if (rest.isEmpty()) {
return new String[]{"", null};
}
String[] seg = rest.split("/");
String type = seg.length > 0 ? seg[0] : "";
// 第二段若是纯数字/uuid 形态的标识,记为 resourceId;否则视为子路径,不记 id。
String id = null;
if (seg.length > 1 && isIdLike(seg[1])) {
id = seg[1];
}
return new String[]{type, id};
}
private static boolean isIdLike(String s) {
if (s == null || s.isEmpty() || s.length() > 64) {
return false;
}
boolean hasDigit = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean ok = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || c == '-';
if (!ok) {
return false;
}
if (c >= '0' && c <= '9') {
hasDigit = true;
}
}
return hasDigit; // 至少含一位数字,避免把 "alerts"/"cost" 等纯单词误判成 id
}
/** 取发起方 IP:优先 X-Forwarded-For 首段(隧道/代理后),否则 remoteAddr。 */
private static String clientIp(HttpServletRequest request) {
String xff = request.getHeader("X-Forwarded-For");
if (xff != null && !xff.isBlank()) {
int comma = xff.indexOf(',');
return (comma > 0 ? xff.substring(0, comma) : xff).trim();
}
String ip = request.getRemoteAddr();
return ip == null ? "" : ip;
}
/**
* 与 AuthInterceptor 同口径的路径规范化:URL 解码、剥矩阵参数、折叠重复斜杠、去尾斜杠、小写。
* 保证审计记录的 path 与授权判定看到的 path 一致,便于对账「越权尝试」。
*/
static String normalizePath(String uri) {
if (uri == null) {
return "";
}
String p = uri;
try {
p = URLDecoder.decode(p, StandardCharsets.UTF_8);
} catch (RuntimeException ignore) {
// 非法转义序列:保留原串继续规范化。
}
p = p.replaceAll(";[^/]*", "");
p = p.replaceAll("/{2,}", "/");
if (p.length() > 1 && p.endsWith("/")) {
p = p.substring(0, p.length() - 1);
}
return p.toLowerCase(Locale.ROOT);
}
private static String nz(String s) {
return s == null ? "" : s;
}
}
@@ -0,0 +1,535 @@
package com.kaidi.oa.config;
import com.kaidi.oa.domain.SysUser;
import com.kaidi.oa.service.AuthorizationService;
import com.kaidi.oa.web.CurrentUserResolver;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* Requires a valid auth token for every /api/oa/** endpoint except the public
* allowlist (login / session-check / logout). Unauthenticated calls receive a
* 401 in the standard ApiResp shape. This closes the "all endpoints open" hole
* (H10) and is the prerequisite for per-user behaviour (待办 by assignee, etc.).
*
* 在认证之上加一层最小授权:对少数敏感写接口(用户管理 / 资金支付 / 用印)按角色门槛
* 放行,堵住"任意登录用户都能删合同、改任意单据、增删员工"的水平越权面(仅认证不授权)。
* 读接口(GET)一律不限制;门槛只针对增删改(POST/PUT/PATCH/DELETE)。
*/
@Component
public class AuthInterceptor implements HandlerInterceptor {
private final CurrentUserResolver currentUser;
private final AuthorizationService authz;
/** Endpoints that must work without a token. */
private static final Set<String> PUBLIC = Set.of(
"/api/oa/auth/login",
"/api/oa/auth/session",
"/api/oa/auth/logout"
);
/** 写操作方法(只有这些方法才触发授权门槛;GET/HEAD 等读操作不限制)。 */
private static final Set<String> WRITE_METHODS = Set.of("POST", "PUT", "PATCH", "DELETE");
private static final Set<String> ADMIN = Set.of("ADMIN");
private static final Set<String> APPROVER_OR_ADMIN = Set.of("ADMIN", "APPROVER");
/**
* 系统 / 组织 / 权限 / 配置 类写接口 → 仅 ADMIN。
* 改这些会动用户、组织树、角色、全局配置、审批委托、各类模板——管理员职责。
*/
private static final List<String> ADMIN_PREFIXES = List.of(
"/api/oa/users", "/api/oa/depts", "/api/oa/roles", "/api/oa/settings",
"/api/oa/delegations", "/api/oa/form-templates", "/api/oa/declaration-templates",
"/api/oa/contract-templates", "/api/oa/report-definitions", "/api/oa/crawl-sources",
// 全文索引重建(POST /search/reindex)是全库重活,仅 ADMIN 可触发(杜绝任意角色发起整库扫描)。
"/api/oa/search",
// 内部审计项目(问题数/结论)属高完整性记录,写口仅 ADMIN,杜绝被改成"无问题"掩盖违规。
"/api/oa/audit-projects"
);
/**
* 财务 / 合同 / 资金 / 主数据 类写接口 → ADMIN 或 APPROVER。
* 凡能改钱、改合同(含 contract-changes / -milestones 这些会回写合同的旁路)、改账户、
* 改预算/成本/科目、改客商主档、改补贴申报的写口,统一收口到审批人/管理员,杜绝 BFLA 旁路。
*/
private static final List<String> FINANCE_PREFIXES = List.of(
"/api/oa/contracts", "/api/oa/contract-changes", "/api/oa/contract-milestones",
"/api/oa/payments", "/api/oa/invoices", "/api/oa/vouchers", "/api/oa/accounts",
"/api/oa/bank-accounts", "/api/oa/fund-pool", "/api/oa/seals", "/api/oa/seal-uses",
"/api/oa/bids", "/api/oa/budgets", "/api/oa/cost-centers", "/api/oa/standard-costs",
"/api/oa/rd-expenses", "/api/oa/company-subjects", "/api/oa/price-items",
"/api/oa/bom-items", "/api/oa/customers", "/api/oa/suppliers", "/api/oa/policies",
"/api/oa/policy-applications", "/api/oa/declarations", "/api/oa/declaration-steps",
// HR / 个人隐私写口(职业健康、人员证书)也收口到 ADMIN/APPROVER,杜绝任意 USER 伪造/篡改。
"/api/oa/health-records", "/api/oa/personnel-certs",
// 主数据/资产写口补全(穷尽审查发现这些写口此前漏收,任意有角色 USER 可增删改):
// 库存物料、排污客户、专利(知识产权)登记,均收口到 ADMIN/APPROVER,与 customers/suppliers/bom-items 一致。
"/api/oa/inventory", "/api/oa/wastewater-clients", "/api/oa/patents",
// 融资域(金融办):融资台账/还本付息/还款生成付款单,写口含金额,收口 ADMIN/APPROVER。
"/api/oa/financings",
// P0 整域补建(监理计量/结算复核/银行对账/资金计划,写口含金额或资金口径)。
"/api/oa/supervision-projects", "/api/oa/measurement-payments",
"/api/oa/settlements", "/api/oa/reconciliations", "/api/oa/fund-plans",
// P1 财务内核(税务/固定资产/应收应付/费用报销/领料含金额)+ 机构目录(主数据写)。
"/api/oa/tax-filings", "/api/oa/fixed-assets", "/api/oa/ar-ap-items",
"/api/oa/expense-claims", "/api/oa/material-issues", "/api/oa/org-branches",
// P3 外部对接框架:对接系统配置(endpoint/凭证)与同步执行触发,写口收口 ADMIN/APPROVER。
"/api/oa/external-systems", "/api/oa/integrations", "/api/oa/rule-configs",
// 长尾全建W1:23机构新增含金额/资金写口,统一收口 ADMIN/APPROVER。
"/api/oa/admin-dorms", "/api/oa/admin-requisitions", "/api/oa/admin-supplies", "/api/oa/admin-vehicles",
"/api/oa/archive-borrows", "/api/oa/archive-categories", "/api/oa/archive-submissions", "/api/oa/audit-findings",
"/api/oa/bid-bonds", "/api/oa/bid-deposits", "/api/oa/biz-budgets", "/api/oa/biz-plans",
"/api/oa/branch-assets", "/api/oa/branch-budget-lines", "/api/oa/brand-events", "/api/oa/brand-plans",
"/api/oa/brand-projects", "/api/oa/contract-budget-reviews", "/api/oa/csl-projects", "/api/oa/csl-reports",
"/api/oa/csl-timesheets", "/api/oa/culture-awards", "/api/oa/culture-schemes", "/api/oa/culture-stories",
"/api/oa/customer-complaints", "/api/oa/customer-credits", "/api/oa/customer-visits", "/api/oa/design-change-orders",
"/api/oa/design-projects", "/api/oa/design-tasks", "/api/oa/dev-dashboard", "/api/oa/dev-project-budgets",
"/api/oa/dev-projects", "/api/oa/discharge-bills", "/api/oa/discharge-contracts", "/api/oa/ehs-incidents",
"/api/oa/feedstock-batches", "/api/oa/fert-recipes", "/api/oa/food-batches", "/api/oa/fraud-reports",
"/api/oa/hazwaste-manifests", "/api/oa/ip-assets", "/api/oa/it-budgets", "/api/oa/local-resources",
"/api/oa/logistics-budgets", "/api/oa/logistics-dashboard", "/api/oa/maint-orders", "/api/oa/market-budgets",
"/api/oa/market-campaigns", "/api/oa/market-expenses", "/api/oa/market-intels", "/api/oa/mfg-project-costs",
"/api/oa/monitor-exceptions", "/api/oa/monitor-rules", "/api/oa/ops-maint", "/api/oa/production-reports",
"/api/oa/project-budget-lines", "/api/oa/prototype-orders", "/api/oa/qual-certs", "/api/oa/qual-declarations",
"/api/oa/qualifications", "/api/oa/rd-grant-disbursements", "/api/oa/rd-service-agencies", "/api/oa/rectifications",
"/api/oa/service-tickets", "/api/oa/sewage-energy-logs", "/api/oa/sewage-equipments", "/api/oa/sewage-process-runs",
"/api/oa/sewage-sludge-records", "/api/oa/software-licenses", "/api/oa/staff-credentials", "/api/oa/staff-dossiers",
"/api/oa/tech-achievements", "/api/oa/water-exceed-events", "/api/oa/work-method-rewards", "/api/oa/work-methods",
// 市场部/经营部补完缺口新控制器写口:投标报价明细(含金额)/客户联系人PII子表/商机阶段/经营绩效考核,收口 ADMIN/APPROVER。
"/api/oa/crm-bid-quote-details", "/api/oa/crm-customer-contacts",
"/api/oa/crm-opp-stages", "/api/oa/crm-performance",
// 申报服务部补完缺口新控制器写口:审计报告(含财务数据)/申报项目参与台账,收口 ADMIN/APPROVER。
"/api/oa/decl-audit-reports", "/api/oa/decl-project-assignments",
// 办事处请示报告(含涉及金额/多节点上报审批链),写口收口 ADMIN/APPROVER。
"/api/oa/crm-branch-petitions",
// 审计监察部新缺口补完写口:审计报告/证据/风险有效性评估/舞弊预警规则/自动归档,收口 ADMIN/APPROVER。
"/api/oa/audit-reports", "/api/oa/audit-evidence",
"/api/oa/audit-risk-assessments", "/api/oa/fraud-alert-rules", "/api/oa/audit-auto-archive",
// 支付中心/财务部缺口补完写口:科目辅助核算/收入确认/BOM-成本集成/采购预算检查/财务岗位权限矩阵,收口 ADMIN/APPROVER。
"/api/oa/fin-account-aux", "/api/oa/fin-rev-recognitions",
"/api/oa/fin-bom-cost", "/api/oa/fin-procure-budget-check", "/api/oa/fin-account-perms"
);
/**
* 机密财务 / 个人隐私 类**读**接口 → 需 ADMIN/APPROVER。读侧此前对任意登录用户全开,
* 导致银行账户/付款/凭证/资金池/职业健康等机密数据被任意账号横向读取——这里对最敏感的
* 读口补一层角色门槛(其余一般业务读维持登录可读,避免误伤前端大量列表)。
*/
private static final List<String> SENSITIVE_READ_PREFIXES = List.of(
"/api/oa/accounts", "/api/oa/bank-accounts", "/api/oa/payments", "/api/oa/vouchers",
"/api/oa/fund-pool", "/api/oa/health-records", "/api/oa/personnel-certs",
// BI / 数据中心 / 自定义报表 / 预警 聚合读会把付款/资金/合同/证件PII 等汇总端出来,
// 是敏感读门槛的聚合旁路(基路径非敏感却读敏感数据),一并收口到 ADMIN/APPROVER。
"/api/oa/reports", "/api/oa/datacenter", "/api/oa/report-definitions", "/api/oa/alerts",
// 预算 / 成本中心 / 标准成本 / 研发费 含公司财务明细,读侧也收口(写侧已在财务前缀)。
"/api/oa/budgets", "/api/oa/cost-centers", "/api/oa/standard-costs", "/api/oa/rd-expenses",
// 穷尽审查补收的聚合/机密读旁路:
// - search:全文检索把单据正文/合同甲乙方/供应商开户行/统一社会信用代码等明文灌入索引并返回高亮片段,
// 整体旁路了对象级读与敏感读门槛,全局检索收口到 ADMIN/APPROVER
// - automation-logs:审批办结后的财务联动审计流水(含金额与付款/合同指向);
// - suppliers:列表/详情直接吐供应商开户行+银行账号。
"/api/oa/search", "/api/oa/automation-logs", "/api/oa/suppliers",
// 财务读端点族(含金额/对账/资金口径)系统性收口:与已门禁的 payments/vouchers/budgets 同源,
// 此前从这些未收口的读口整体漏出(红队实证:合同实付/已开票、补贴申请/拨付额、研发预算等)。
// 一次性把含金额的业务读全部纳入 ADMIN/APPROVER,封死"敏感财务从旁路读口外泄"的整类。
"/api/oa/contracts", "/api/oa/contract-changes", "/api/oa/contract-milestones",
"/api/oa/invoices", "/api/oa/bids", "/api/oa/opportunities", "/api/oa/opportunity-followups",
"/api/oa/policy-applications", "/api/oa/rd-projects", "/api/oa/price-items",
"/api/oa/bom-items", "/api/oa/company-subjects",
// 评审专家库含姓名+明文联系电话(PII),收口到 ADMIN/APPROVER,杜绝任意员工导出专家名单定向接触/围标。
"/api/oa/experts",
// 通用键值配置(settings)写口已收口 ADMIN,读口本对任意 USER 全开(读写不对称)。settings 是 ADMIN 专管
// 的配置/台账持久化(将来可能存敏感配置),读侧也收口到 ADMIN/APPROVER,消除不对称。
"/api/oa/settings",
// 融资域读侧(融资台账/还款计划/到期预警/成本分析含授信额度与利率,属机密财务)。
"/api/oa/financings",
// P0 整域补建读侧(监理项目/计量支付含金额;结算/对账/资金计划含资金口径;
// 法务合规义务/内控矩阵/诉讼仲裁/法律咨询/风险评估含敏感法律信息;实验室检测任务/报告含检测结论)。
"/api/oa/supervision-projects", "/api/oa/measurement-payments",
"/api/oa/settlements", "/api/oa/reconciliations", "/api/oa/fund-plans",
"/api/oa/compliance-obligations", "/api/oa/control-matrices", "/api/oa/litigation-cases",
"/api/oa/legal-consults", "/api/oa/risk-assessments",
"/api/oa/test-reports", "/api/oa/test-tasks",
// P1 财务内核读侧(税额/资产原值净值/往来金额/报销金额/领料成本,属机密财务)+ MRP/条款比对(读库存/合同条款)。
"/api/oa/tax-filings", "/api/oa/fixed-assets", "/api/oa/ar-ap-items",
"/api/oa/expense-claims", "/api/oa/material-issues", "/api/oa/mrp", "/api/oa/clause-compare",
// P2 跨模块自动取数/看板读侧:申报条件自查(研发费用/科技人员PII/知识产权聚合)、研发费用证据链、
// 财务多维看板(合同额/付款/应收应付/凭证聚合)——属机密财务+PII 聚合读,收口 ADMIN/APPROVER。
"/api/oa/eligibility-checks", "/api/oa/rd-expense-traces", "/api/oa/finance-dashboard",
// P3 贯穿性读侧:操作审计台账(操作人/资源指向/IP)、外部对接配置(endpoint/凭证)、可配置联动规则——属机密配置/审计读。
"/api/oa/operation-audit-logs", "/api/oa/external-systems", "/api/oa/rule-configs",
// 长尾全建W1:23机构新增含金额/机密/PII 聚合读口,统一收口 ADMIN/APPROVER。
"/api/oa/admin-dorms", "/api/oa/admin-requisitions", "/api/oa/admin-supplies", "/api/oa/admin-vehicles",
"/api/oa/admin-visitors", "/api/oa/archive-board", "/api/oa/archive-borrows", "/api/oa/archive-categories",
"/api/oa/archive-submissions", "/api/oa/audit-dashboard", "/api/oa/audit-findings", "/api/oa/bid-bonds",
"/api/oa/bid-decisions", "/api/oa/bid-deposits", "/api/oa/biz-budgets", "/api/oa/biz-plans",
"/api/oa/branch-budget-lines", "/api/oa/branch-pnl", "/api/oa/brand-dashboard", "/api/oa/brand-events",
"/api/oa/brand-plans", "/api/oa/brand-projects", "/api/oa/contract-budget-reviews", "/api/oa/cost-audit",
"/api/oa/csl-dashboard", "/api/oa/csl-projects", "/api/oa/csl-reports", "/api/oa/csl-timesheets",
"/api/oa/culture-assessment", "/api/oa/culture-awards", "/api/oa/culture-schemes", "/api/oa/culture-stories",
"/api/oa/customer-credits", "/api/oa/declaration-dashboard", "/api/oa/design-change-orders", "/api/oa/design-projects",
"/api/oa/design-reviews", "/api/oa/design-tasks", "/api/oa/dev-dashboard", "/api/oa/dev-project-budgets",
"/api/oa/dev-projects", "/api/oa/discharge-bills", "/api/oa/discharge-contracts", "/api/oa/ehs-board",
"/api/oa/ehs-hazards", "/api/oa/ehs-incidents", "/api/oa/engineering-changes", "/api/oa/feedstock-batches",
"/api/oa/ferment-batches", "/api/oa/fert-recipes", "/api/oa/food-batches", "/api/oa/fraud-reports",
"/api/oa/hazwaste-manifests", "/api/oa/ip-assets", "/api/oa/ip-insights", "/api/oa/it-budgets",
"/api/oa/it-tickets", "/api/oa/logistics-budgets", "/api/oa/logistics-dashboard", "/api/oa/maint-orders",
"/api/oa/market-budgets", "/api/oa/market-expenses", "/api/oa/market-intels", "/api/oa/mfg-project-costs",
"/api/oa/monitor-exceptions", "/api/oa/monitor-rules", "/api/oa/ops-cost-allocation", "/api/oa/ops-maint",
"/api/oa/product-requirements", "/api/oa/production-reports", "/api/oa/project-budget-lines", "/api/oa/prototype-orders",
"/api/oa/qc-inspections", "/api/oa/qual-certs", "/api/oa/qual-declarations", "/api/oa/qualifications",
"/api/oa/rd-grant-disbursements", "/api/oa/rd-service-agencies", "/api/oa/rectifications", "/api/oa/service-tickets",
"/api/oa/sewage-energy-logs", "/api/oa/sewage-equipments", "/api/oa/sewage-process-runs", "/api/oa/sewage-sludge-records",
"/api/oa/software-licenses", "/api/oa/staff-credentials", "/api/oa/staff-dossiers", "/api/oa/tech-achievements",
"/api/oa/water-exceed-events", "/api/oa/work-method-dashboard", "/api/oa/work-method-rewards", "/api/oa/work-methods",
// W2 长尾新控制器读口统一收口 ADMIN/APPROVER(演示下admin可读全, 写口已default-deny)。
"/api/oa/announcements", "/api/oa/appdev", "/api/oa/archive-comments", "/api/oa/archive-physical-items",
"/api/oa/archive-vaults", "/api/oa/archives", "/api/oa/auth", "/api/oa/biz",
"/api/oa/canteen-menus", "/api/oa/certifications", "/api/oa/chronicle-events", "/api/oa/competitor-ips",
"/api/oa/completion-acceptances", "/api/oa/content-pieces", "/api/oa/crawl-jobs", "/api/oa/csl-archives",
"/api/oa/csl-experts", "/api/oa/csl-finance", "/api/oa/csl-risks", "/api/oa/csl-tasks",
"/api/oa/culture-cases", "/api/oa/culture-surveys", "/api/oa/data-sync-logs", "/api/oa/decl-acceptances",
"/api/oa/decl-achievements", "/api/oa/decl-evidence-items", "/api/oa/decl-failure-cases", "/api/oa/decl-grant-ledger",
"/api/oa/decl-mobile-queue", "/api/oa/decl-policy-subscriptions", "/api/oa/decl-push-devices", "/api/oa/decl-submissions",
"/api/oa/design-audit", "/api/oa/design-changes", "/api/oa/design-clashes", "/api/oa/design-docs",
"/api/oa/design-drawings", "/api/oa/design-handoffs", "/api/oa/design-interfaces", "/api/oa/design-knowledge",
"/api/oa/designer-capabilities", "/api/oa/documents", "/api/oa/ehs-capas", "/api/oa/ehs-objectives",
"/api/oa/ehs-work-permits", "/api/oa/emergency-drills", "/api/oa/fert-energy-logs", "/api/oa/fert-mps",
"/api/oa/fert-procure-plans", "/api/oa/fert-qc-standards", "/api/oa/fert-sales-orders", "/api/oa/fert-stock-lots",
"/api/oa/folders", "/api/oa/handovers", "/api/oa/hazard-sources", "/api/oa/intel",
"/api/oa/ip-budgets", "/api/oa/ip-knowledge", "/api/oa/ip-regulations", "/api/oa/it-assets",
"/api/oa/it-knowledge", "/api/oa/it-monitors", "/api/oa/lab-costs", "/api/oa/lab-deviations",
"/api/oa/lab-ehs", "/api/oa/lab-elns", "/api/oa/lab-haz-requests", "/api/oa/lab-instrument-maints",
"/api/oa/lab-instruments", "/api/oa/lab-internal-audits", "/api/oa/lab-report-archives", "/api/oa/lab-reservations",
"/api/oa/lab-sample-flow", "/api/oa/lab-samples", "/api/oa/lab-stocktakes", "/api/oa/leader-trips",
"/api/oa/logistics-tickets", "/api/oa/meal-orders", "/api/oa/meeting-action-items", "/api/oa/meeting-rooms",
"/api/oa/meetings", "/api/oa/mfg-deliveries", "/api/oa/mfg-drawings", "/api/oa/mfg-eng-boms",
"/api/oa/mfg-quotes", "/api/oa/mfg-sales-orders", "/api/oa/minutes", "/api/oa/mobile",
"/api/oa/mobile-approvals", "/api/oa/net-scan-tasks", "/api/oa/ops-kpi", "/api/oa/product-certs",
"/api/oa/product-roadmaps", "/api/oa/project-tasks", "/api/oa/projects", "/api/oa/prototype-stocks",
"/api/oa/qhse-dashboard", "/api/oa/qms-audits", "/api/oa/qms-documents", "/api/oa/qms-records",
"/api/oa/rd-eboms", "/api/oa/rd-issues", "/api/oa/rd-maintenance-tasks", "/api/oa/rd-materials",
"/api/oa/rd-purchase-requests", "/api/oa/rd-timesheets", "/api/oa/reagents", "/api/oa/safety-checks",
"/api/oa/safety-facilities", "/api/oa/safety-investments", "/api/oa/safety-responsibilities", "/api/oa/safety-trainings",
"/api/oa/secret-access-logs", "/api/oa/sentiment-records", "/api/oa/std-applications", "/api/oa/supervision-inspections",
"/api/oa/supervision-logs", "/api/oa/supplier-quality-lots", "/api/oa/supplier-samples", "/api/oa/surveys",
"/api/oa/sv-change-claims", "/api/oa/sv-material-inspects", "/api/oa/sv-monthly-reports", "/api/oa/sv-notices",
"/api/oa/sv-progress-plans", "/api/oa/sv-schemes", "/api/oa/tech-contracts", "/api/oa/test-methods",
// 工程监理部深化补建:监理规划/细则(版本受控)含监理机密、资质审查含PII/证书编号、开工令/隐蔽工程含工程敏感数据,统一收口。
"/api/oa/sv-plans", "/api/oa/sv-qualifications", "/api/oa/sv-open-work-orders", "/api/oa/sv-hidden-works",
"/api/oa/water-alert-rules", "/api/oa/water-quality", "/api/oa/work-orders",
// W2-sonnet 新控制器读口统一收口。
"/api/oa/admin-cleaning-schedules", "/api/oa/admin-dorm-repairs", "/api/oa/admin-express", "/api/oa/admin-greenery",
"/api/oa/admin-patrols", "/api/oa/admin-procurement-requests", "/api/oa/admin-receptions", "/api/oa/admin-security-events",
"/api/oa/admin-shuttle-buses", "/api/oa/admin-shuttles", "/api/oa/admin-vehicle-violations", "/api/oa/annual-reports",
"/api/oa/archive-access-logs", "/api/oa/archive-acls", "/api/oa/archive-destructions", "/api/oa/archive-repair-records",
"/api/oa/archive-reservations", "/api/oa/archive-versions", "/api/oa/audit-caats", "/api/oa/audit-notices",
"/api/oa/audit-procedures", "/api/oa/audit-work-papers", "/api/oa/bid-loss-cases", "/api/oa/bom-variance-details",
"/api/oa/brand-assets", "/api/oa/caats", "/api/oa/canteen-sanitations", "/api/oa/compliance-checklists",
"/api/oa/contract-cost-ext", "/api/oa/cost-purchase-orders", "/api/oa/cost-reductions", "/api/oa/crisis-plans",
"/api/oa/crm-analysis", "/api/oa/crm-bid-analysis", "/api/oa/crm-bid-analytics", "/api/oa/crm-biz-analytics",
"/api/oa/crm-competitors", "/api/oa/crm-contract-approvals", "/api/oa/crm-customer-alerts", "/api/oa/crm-doc-receipts",
"/api/oa/crm-petty-cash", "/api/oa/crm-satisfaction", "/api/oa/csl-cross-dept", "/api/oa/csl-report-chapters",
"/api/oa/culture-archives", "/api/oa/culture-materials", "/api/oa/doc-receipts", "/api/oa/economic-audits",
"/api/oa/env-discharge-permits", "/api/oa/event-checkins", "/api/oa/event-guests", "/api/oa/fert-batch-costs",
"/api/oa/fert-gas-emissions", "/api/oa/fert-pack-orders", "/api/oa/fin-bad-debts", "/api/oa/fin-budget-executions",
"/api/oa/fin-cash-journals", "/api/oa/fin-statements", "/api/oa/fin-voucher-templates", "/api/oa/hr-dossier-search",
"/api/oa/hr-event-archive", "/api/oa/hr-labor-contracts", "/api/oa/imprest-funds", "/api/oa/ip-license-transfers",
"/api/oa/it-asset-events", "/api/oa/it-backups", "/api/oa/it-network-devices", "/api/oa/it-sec-events",
"/api/oa/it-vuln-patches", "/api/oa/legal-auth-limits", "/api/oa/legal-opinions", "/api/oa/legal-regulations",
"/api/oa/litigation-firms", "/api/oa/media-contacts", "/api/oa/mfg-production-anomalies", "/api/oa/mfg-purchase-orders",
"/api/oa/mfg-stocktakes", "/api/oa/mfg-warranty-contracts", "/api/oa/occ-hazard-factors", "/api/oa/pmt-bank-relations",
"/api/oa/pmt-financing-contracts", "/api/oa/pmt-inbound-receipts", "/api/oa/pmt-internal-trans", "/api/oa/pmt-loan-schemes",
"/api/oa/ppe-items", "/api/oa/publicity-tasks", "/api/oa/qual-achievements", "/api/oa/qual-exports",
"/api/oa/qual-risk-alerts", "/api/oa/rd-reuse-modules", "/api/oa/rd-weekly-reports", "/api/oa/regulatory-submissions",
"/api/oa/sewage-emergency-plans", "/api/oa/sewage-kpi", "/api/oa/sewage-process-schemes", "/api/oa/sewage-sampling-plans",
"/api/oa/sewage-shift-handovers", "/api/oa/sewage-spare-parts", "/api/oa/wm-decl-plans", "/api/oa/wm-fees",
"/api/oa/wm-gov-subsidies", "/api/oa/wm-promotions", "/api/oa/ww-process-units", "/api/oa/ww-sampling-tasks",
"/api/oa/ww-service-reports",
// 质安部补完缺口新控制器:管理评审/作业场所监测含职业健康敏感数据,统一收口 ADMIN/APPROVER。
"/api/oa/qms-management-reviews", "/api/oa/occ-workplace-monitors",
// 成本控制部补完缺口新控制器:大宗采购比价(含报价/定标金额)、一对多进度款批次(含应付未付/合同金额)。
"/api/oa/cost-procurement-bids", "/api/oa/multi-payment-batches",
// W3 新控制器读口收口。
"/api/oa/archive-acl-aware", "/api/oa/archive-acl-enforce", "/api/oa/archive-compliance", "/api/oa/archive-cross",
"/api/oa/compliance-alert-rules", "/api/oa/cost-adjust-logs", "/api/oa/csl-analytics", "/api/oa/csl-audit-log",
"/api/oa/csl-chapter-comments", "/api/oa/csl-entity-history", "/api/oa/csl-expert-scores", "/api/oa/csl-integral",
"/api/oa/csl-market-analytics", "/api/oa/decl-calendar", "/api/oa/fert-finance-integration", "/api/oa/fert-occ-health",
"/api/oa/fert-process-qc", "/api/oa/fert-raw-material-qc", "/api/oa/fert-solid-waste", "/api/oa/fert-supplier-profiles",
"/api/oa/fert-wastewater", "/api/oa/fert-work-sub-orders", "/api/oa/fin-advances", "/api/oa/fin-budget-adjs",
"/api/oa/fin-cost-calcs", "/api/oa/fin-ledger", "/api/oa/fin-post-loan-checks", "/api/oa/fin-rd-expenses",
"/api/oa/hr-non-compete", "/api/oa/inventory-valuation-configs", "/api/oa/legal-entities", "/api/oa/legal-knowledge",
"/api/oa/legal-risk-assess-tasks", "/api/oa/legal-risk-events", "/api/oa/legal-supplier-blacklist", "/api/oa/legal-trainings",
"/api/oa/mfg-env-contracts", "/api/oa/mfg-inventory-lots", "/api/oa/mfg-mps", "/api/oa/mfg-self-service",
"/api/oa/mfg-wip-costs", "/api/oa/mrp-from-engbom", "/api/oa/rd-decl-plans", "/api/oa/rd-doc-versions", "/api/oa/rd-knowledge",
"/api/oa/rd-mobile", "/api/oa/rd-sci-tech-personnel", "/api/oa/sewage-cost-months", "/api/oa/sewage-env-records",
"/api/oa/sewage-mobile-queues", "/api/oa/sewage-patrol", "/api/oa/sewage-power-sections", "/api/oa/sewage-reagent-stocks",
"/api/oa/sv-archive-transfers", "/api/oa/sv-drawing-issues", "/api/oa/sv-equipment-ledger", "/api/oa/sv-safety-agg",
"/api/oa/wm-cross-link", "/api/oa/wm-decl-packs", "/api/oa/wm-decl-templates", "/api/oa/wm-doc-versions",
// 成本控制部补完缺口新控制器:跨部门联动(含HR费率/审计包/差异分摊/付款闸门,均含金额),收口 ADMIN/APPROVER。
"/api/oa/cost-cross-dept",
// 市场部/经营部补完缺口新控制器:投标报价明细(含金额)/客户联系人PII/商机阶段配置/经营业绩考核(含绩效得分),收口 ADMIN/APPROVER。
"/api/oa/crm-bid-quote-details", "/api/oa/crm-customer-contacts",
"/api/oa/crm-opp-stages", "/api/oa/crm-performance",
// 工业废水运营中心·药剂费用按企业分摊(含成本金额,财务敏感读),收口 ADMIN/APPROVER。
"/api/oa/ops-reagent-cost-alloc",
// 结算中心/支付中心缺口补完新控制器(含金额/名单/预测,属机密财务与风控读),收口 ADMIN/APPROVER。
"/api/oa/pmt-collection", "/api/oa/pmt-cost-alloc",
"/api/oa/pmt-payee-blacklist", "/api/oa/stl-rolling-forecast", "/api/oa/stl-fund-report",
// 内控部/审计监察部缺口补完:年度审计计划/资料请求/档案库/质量复核/跨模块聚合 含审计机密,收口 ADMIN/APPROVER。
"/api/oa/audit-annual-plans", "/api/oa/audit-material-requests",
"/api/oa/audit-archives", "/api/oa/audit-quality-reviews", "/api/oa/audit-data-aggregate",
// W3 resume 全量补完新控制器读口收口 ADMIN/APPROVER。
"/api/oa/admin-vehicle-reports", "/api/oa/branch-fin-statements", "/api/oa/branch-licenses",
"/api/oa/brand-chronicle", "/api/oa/canteen-procurement", "/api/oa/crm-branch-leases",
"/api/oa/crm-daily-reports", "/api/oa/crm-gps-tracks", "/api/oa/culture-material-downloads",
"/api/oa/fin-ar-ap-auto", "/api/oa/fin-asset-inventories", "/api/oa/fin-period-close",
"/api/oa/fin-rd-closeout", "/api/oa/hr-recruit", "/api/oa/internal-newsletters",
"/api/oa/it-account-audit", "/api/oa/it-data-exports", "/api/oa/it-disaster-drills",
"/api/oa/it-procurement", "/api/oa/it-sec-training", "/api/oa/it-sys-logs",
"/api/oa/legal-compliance-certs", "/api/oa/legal-risk-measures", "/api/oa/logistics-suppliers",
"/api/oa/logistics-surveys", "/api/oa/qual-analytics", "/api/oa/qual-year-checks",
"/api/oa/sewage-detection-reports", "/api/oa/sewage-occ-health", "/api/oa/sewage-process-flow",
"/api/oa/staff-self",
// 申报服务部补完缺口新控制器读口:近三年审计报告(含财务数据)收口 ADMIN/APPROVER。
"/api/oa/decl-audit-reports",
// 申报材料协同编制(申报文档绑定+参与人列表,属过程文件/PII 聚合读),收口 ADMIN/APPROVER。
"/api/oa/decl-material-collab",
// W3 成本控制部补完缺口新控制器(含金额/主体/项目层级穿透,财务敏感读),收口 ADMIN/APPROVER。
"/api/oa/cost-ctrl-ext", "/api/oa/supplier-contract-prereq",
// 办事处请示报告(含涉及金额与审批链记录,属敏感读),收口 ADMIN/APPROVER。
"/api/oa/crm-branch-petitions",
// 审计监察部新缺口补完:审计证据/跨模块取数(HR+生产+申报敏感数据),收口 ADMIN/APPROVER。
"/api/oa/audit-evidence", "/api/oa/audit-cross-module",
// 审计监察部 W4 缺口补完:审计师档案(含利益冲突/资质证书)/结算法务聚合(含拆借金额/诉讼记录)。
"/api/oa/auditor-profiles", "/api/oa/audit-settlement-agg",
// 审计报告含审计机密/结论,收口 ADMIN/APPROVER。
"/api/oa/audit-reports",
// 风险管理有效性审计(含风险缺陷评估结论),收口 ADMIN/APPROVER。
"/api/oa/audit-risk-assessments",
// 支付中心/财务部缺口补完:科目辅助核算余额(含科目余额)/收入确认(含合同金额)/
// BOM-成本集成(含成本金额)/采购预算检查(含预算/合同金额)/财务岗位权限矩阵(含权限配置),
// 均含机密财务数据或权限主数据,统一收口 ADMIN/APPROVER。
"/api/oa/fin-account-aux", "/api/oa/fin-rev-recognitions",
"/api/oa/fin-bom-cost", "/api/oa/fin-procure-budget-check", "/api/oa/fin-account-perms",
// W4 可建缺口补完新控制器读口收口 ADMIN/APPROVER。
"/api/oa/archive-hash-records", "/api/oa/bid-knowledge", "/api/oa/compliance-risk-reports",
"/api/oa/crm-follow-scripts", "/api/oa/crm-marketing-dashboard", "/api/oa/crm-mobile-biz",
"/api/oa/design-gantt", "/api/oa/dev-launch-stats", "/api/oa/env-waste-records",
"/api/oa/env-waste-records/exceed", "/api/oa/env-waste-records/stats", "/api/oa/hr-cross-dept",
"/api/oa/hr-interview-questions", "/api/oa/lab-cross-dept", "/api/oa/patent-disclosures",
"/api/oa/patent-kpi", "/api/oa/pmt-internal-trans (already in SENSITIVE_READ_PREFIXES)", "/api/oa/qhse-interface/cross-dept-summary",
"/api/oa/qhse-interface/health-alert", "/api/oa/qhse-reports/env-discharge-report", "/api/oa/qual-elec-licenses",
"/api/oa/rd-ebom-cost-calc", "/api/oa/rd-innovation-points", "/api/oa/rd-test-plans",
"/api/oa/settlements (already in FINANCE_PREFIXES + SENSITIVE_READ_PREFIXES)", "/api/oa/stl-fund-report (already in SENSITIVE_READ_PREFIXES)", "/api/oa/sv-accidents",
"/api/oa/sv-cross-link", "/api/oa/sv-personal-certs", "/api/oa/sv-sequence-inspects",
// 行政/综合部补完缺口新控制器:安全事故台账(含死亡/受伤/直接损失金额,属法规敏感/事故机密);
// 资质延续换证变更(含证书编号/受理回执/历史材料清单,属资质主数据敏感读),统一收口 ADMIN/APPROVER。
"/api/oa/qual-safety-accidents", "/api/oa/qual-cert-changes",
// W5 可建缺口补完新控制器读口收口。
"/api/oa/branch-cost-entries", "/api/oa/branch-monthly-pnl", "/api/oa/compliance-violations",
"/api/oa/cost-ctrl-deep", "/api/oa/cost-estimates", "/api/oa/crm-biz-achiev-link",
"/api/oa/crm-pnl-monthly", "/api/oa/csl-customers", "/api/oa/decl-cross-dept",
"/api/oa/decl-finance-data", "/api/oa/ehs-special-equip", "/api/oa/external-regulations",
"/api/oa/fert-customer-profiles", "/api/oa/fert-raw-material-catalog", "/api/oa/fin-audit-export",
"/api/oa/fin-budget-plans", "/api/oa/fin-period-ext", "/api/oa/financial-guarantees",
"/api/oa/food-batches/waste-rate — 已在 AuthInterceptor SENSITIVE_READ_PREFIXES 中以 /api/oa/food-batches 前缀覆盖,无需额外配置", "/api/oa/ip-infringement-cases", "/api/oa/it-cross-dept",
"/api/oa/it-inspection-reports", "/api/oa/legal-credit-invest", "/api/oa/litigation-documents",
"/api/oa/publish-channels", "/api/oa/qhse-cross-dept-sync", "/api/oa/rd-after-sales-feedback",
"/api/oa/rd-dev-cross-dept", "/api/oa/stl-forex-risks", "/api/oa/stl-fund-plan-submissions",
"/api/oa/stl-internal-settle-rules", "/api/oa/stl-payment-refunds", "GET /api/oa/financings/{id}/legal-reviews — 落入 FINANCE_PREFIXES + SENSITIVE_READ_PREFIXES,需 ADMIN/APPROVER",
"GET /api/oa/legal-risk-events/by-financing/{id} — 落入 SENSITIVE_READ_PREFIXES,需 ADMIN/APPROVER", "POST /api/oa/financings/{id}/legal-reviews/link — FINANCE_PREFIXES 写口,需 ADMIN/APPROVER", "POST /api/oa/financings/{id}/push-cost-alloc — FINANCE_PREFIXES 写口,需 ADMIN/APPROVER",
"admin-ai-actionsAI 行动日志,需加入 AuthInterceptor SELF_SERVICE_WRITE_PREFIXES,见 sharedFileSnippets", "admin-cleaning-supplies(保洁物资台账,默认 APPROVER_OR_ADMIN", "admin-conference-materials(会务物料清单,含物料规划数据,默认 APPROVER_OR_ADMIN",
"admin-conference-staff(现场人员安排含联系电话 PII,默认 APPROVER_OR_ADMIN", "admin-equip-setups(设备调试记录,默认 APPROVER_OR_ADMIN", "admin-recurring-bookings(周期预订规则,默认 APPROVER_OR_ADMIN",
// W6 可建缺口补完新控制器读口收口。
"/api/oa/cost-hr-labor-rates", "/api/oa/cost-integration", "/api/oa/csl-contracts",
"/api/oa/design-external-portal", "/api/oa/expense-claims — 已在 AuthInterceptor FINANCE_PREFIXES + SENSITIVE_READ_PREFIXES 中,新 PATCH /{id}/invoice 自动继承保护,无需额外配置", "/api/oa/fert-wip-stocks",
"/api/oa/ip-archive-search", "/api/oa/ip-free-tags", "/api/oa/it-dlp-policies",
"/api/oa/it-dlp-policies — 含字段级脱敏规则/拦截策略明细(affectedFields/ruleDetail),属安全机密读,需加入 AuthInterceptor.SENSITIVE_READ_PREFIXES", "/api/oa/it-remote-assist", "/api/oa/it-remote-assist — 含授权码(authCode)和申请人联系方式(contact),属安全敏感读,需加入 AuthInterceptor.SENSITIVE_READ_PREFIXES",
"/api/oa/patent-fees", "/api/oa/pmt-contract-fee-items 建议补入 AuthInterceptor FINANCE_PREFIXES(与 pmt-financing-contracts 同级)", "/api/oa/rd-annual-budgets",
"/api/oa/stl-debt-ledger", "/api/oa/wm-subsidy-finance",
// W7 可建/种子缺口补完新控制器读口收口。
"/api/oa/audit-reg-lib", "/api/oa/branch-fin-statements (already in AuthInterceptor SENSITIVE_READ_PREFIXES line 295, covers all sub-paths including /ar-ap-detail)", "/api/oa/cost-hr-labor-rates (already in SENSITIVE_READ_PREFIXES)",
"/api/oa/cost-purchase-orders (already in SENSITIVE_READ_PREFIXES)", "/api/oa/cost-reductions (already in SENSITIVE_READ_PREFIXES)", "/api/oa/csl-timesheets (already in FINANCE_PREFIXES + SENSITIVE_READ_PREFIXES in AuthInterceptor)",
"/api/oa/csl-timesheets — already in FINANCE_PREFIXES + SENSITIVE_READ_PREFIXES (AuthInterceptor lines 96, 180); no change needed", "/api/oa/fert-accounting", "/api/oa/fin-emp-loans",
"/api/oa/financing-policies", "/api/oa/fraud-investigation-notes", "/api/oa/hr-emp-satisfaction",
"/api/oa/it-sec-policy", "/api/oa/legal-cross-dept", "/api/oa/mfg-cost-entries",
"/api/oa/mfg-fqc-reports", "/api/oa/pmt-bank-evals", "/api/oa/sewage-cross-dept-linkages",
"/api/oa/sewage-ext-lab-commissions", "/api/oa/standard-costs (already in SENSITIVE_READ_PREFIXES)", "/api/oa/ww-msds-filesMSDS安全说明书文件库,含危险类别/应急处置/PPE要求等安全敏感信息,建议集成时加入 AuthInterceptor.SENSITIVE_READ_PREFIXES",
"/api/oa/ww-xdept-bridge(跨部门联动桥接,GET /status 为汇总读,POST 写口已由 default-deny 保护,无需额外读收口)"
);
/**
* 自助写白名单(写操作 default-deny 翻转后的"放行档"):仅这些个人/社交/协作/工作流端点,
* 任意已授角色(含普通 USER)可写——它们的"只动自己的"由各控制器内对象级属主校验保证
* favorites/schedule-events/work-plans/blogs/feeds/discussions/collab-docs/files/messages/
* form-instances/tasks 均已加 isOwner/当前办理人/收件人校验;surveys 为投票/公共协作)。
* **白名单之外的一切写口默认需 ADMIN/APPROVER**(见 requiredRolesFor 默认分支),从而系统性封死
* "任意 USER 改/删他人业务记录(商机/会议/审计/监理/资产/项目/主数据…)"的 BFLA 整类,而非逐个打补丁。
*/
private static final List<String> SELF_SERVICE_WRITE_PREFIXES = List.of(
"/api/oa/favorites", "/api/oa/schedule-events", "/api/oa/work-plans",
"/api/oa/blogs", "/api/oa/feeds", "/api/oa/discussions", "/api/oa/collab-docs",
"/api/oa/messages", "/api/oa/files", "/api/oa/form-instances",
"/api/oa/tasks",
// P2AI 助手(/ai/* 个人辅助工具,不读写敏感业务库,未配 Key 时降级无副作用)、
// 移动外勤打卡(mobile-checkins 个人行踪,对象级属主校验在控制器内)——放给任意已授角色。
"/api/oa/ai", "/api/oa/mobile-checkins"
// 注意:surveys 不在此白名单——其 create/update/delete 缺对象级校验,整体收口到 ADMIN/APPROVER
// 仅"投票"动作(POST /surveys/{id}/vote)由 requiredRolesFor 的 /vote 特判放行给任意角色。
);
public AuthInterceptor(CurrentUserResolver currentUser, AuthorizationService authz) {
this.currentUser = currentUser;
this.authz = authz;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// CORS preflight carries no auth header by design -> let it through.
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
// 关键:用与控制器同口径的"规范化路径"(已解码、剥矩阵参数、折叠斜杠、小写)做匹配。
// 直接拿 getRequestURI() 是原始未解码串,会被 /api/oa/%75sers、/api/oa/users;x=1 这类
// 绕过门槛——而 Spring 控制器仍按解码后路径命中,等于授权形同虚设(已实测可越权)。
String path = normalizePath(request.getRequestURI());
if (PUBLIC.contains(path)) {
return true;
}
SysUser user = currentUser.resolve(request);
if (user == null) {
writeError(response, HttpStatus.UNAUTHORIZED, 401, "未登录或会话已过期");
return false;
}
Set<String> have = authz.roleCodesOf(user.getId());
// 默认拒绝(读写皆然):无任何角色的账号一律零权限,封死"建了用户没授权即可随意读写"
// 的无角色越权面(读侧此前对任意登录用户全开是系统性 IDOR 根因之一)。
if (have.isEmpty()) {
writeError(response, HttpStatus.FORBIDDEN, 403, "无权限:账号未分配任何角色");
return false;
}
boolean isWrite = WRITE_METHODS.contains(request.getMethod().toUpperCase(Locale.ROOT));
if (isWrite) {
// 写:ADMIN 前缀→需 ADMIN;自助白名单→任意已授角色即可(对象级属主校验在控制器内);
// **其余一切业务/主数据写默认需 ADMIN/APPROVER**default-deny 翻转,封死任意 USER 越权写他人业务记录)。
Set<String> required = requiredRolesFor(path);
if (required != null && required.stream().noneMatch(have::contains)) {
writeError(response, HttpStatus.FORBIDDEN, 403,
"无权限:该操作需要 " + String.join("/", required));
return false;
}
} else {
// 读:机密财务/PII 读口需 ADMIN/APPROVER,其余业务读维持"已登录且有角色"可读。
if (isSensitiveRead(path) && APPROVER_OR_ADMIN.stream().noneMatch(have::contains)) {
writeError(response, HttpStatus.FORBIDDEN, 403, "无权限:该数据需要 ADMIN/APPROVER 角色");
return false;
}
}
return true;
}
private static boolean isSensitiveRead(String path) {
// 研发证据链聚合端点 /api/oa/rd-projects/{id}/evidence-chain 会端出研发费用金额/凭证号/申报/专利
// 等本应被 rd-expenses 读门槛拦下的明细(基路径 rd-projects 非敏感,故按后缀精确收口,
// 不误伤 RdProjectController 的普通项目登记读)。
if (path.endsWith("/evidence-chain")) {
return true;
}
// 水质超标溯源 /api/oa/water-quality/trace 连表把受保护的排污客户主数据(企业名/行业/日排放量/追偿建议)
// 端出来——基路径 water-quality 非敏感,按后缀精确收口到 ADMIN/APPROVER(同 evidence-chain 旁路)。
if (path.endsWith("/water-quality/trace")) {
return true;
}
for (String p : SENSITIVE_READ_PREFIXES) {
if (path.equals(p) || path.startsWith(p + "/")) {
return true;
}
}
// ★系统性收口:凡写侧受 ADMIN/APPROVER 门禁的财务/主数据/PII 端点(FINANCE_PREFIXES),读侧一律同门禁。
// 杜绝"写口收了、读口漏收"的反复缺口(红队实证:合同/客户/供应商/申报/专利/补贴等读口逐个漏出敏感数据)。
for (String p : FINANCE_PREFIXES) {
if (path.equals(p) || path.startsWith(p + "/")) {
return true;
}
}
return false;
}
/** 规范化路径命中哪一档授权要求;都不命中返回 null(默认档:需任一已授角色)。 */
private static Set<String> requiredRolesFor(String path) {
// 侧写财务副作用端点:基路径非财务,但会写投标/合同/记账等财务实体(如商机转投标、
// 项目阶段推进记账)。按路径特征单独收口到 ADMIN/APPROVER,杜绝绕开财务前缀门槛的旁路写。
if (path.endsWith("/convert-bid")
|| (path.startsWith("/api/oa/projects/") && path.endsWith("/advance"))) {
return APPROVER_OR_ADMIN;
}
// 问卷投票是公共动作:任意已授角色可对任意问卷投票(仅 POST /surveys/{id}/vote
// 不含问卷的创建/修改/删除——那些经默认档收口到 ADMIN/APPROVER)。
if (path.endsWith("/vote")) {
return null;
}
for (String p : ADMIN_PREFIXES) {
if (path.equals(p) || path.startsWith(p + "/")) {
return ADMIN;
}
}
// 自助写白名单:个人/社交/协作/工作流端点放给任意已授角色(对象级属主校验在控制器内)。
for (String p : SELF_SERVICE_WRITE_PREFIXES) {
if (path.equals(p) || path.startsWith(p + "/")) {
return null;
}
}
// FINANCE_PREFIXES 显式归在 ADMIN/APPROVER(与下方默认同档,保留以自文档化财务/主数据写口)。
for (String p : FINANCE_PREFIXES) {
if (path.equals(p) || path.startsWith(p + "/")) {
return APPROVER_OR_ADMIN;
}
}
// ★default-deny 翻转:其余一切业务/主数据写口(商机/会议/审计/监理/IT资产/项目/实验室/各台账…)
// 默认需 ADMIN/APPROVER,普通 USER 一律挡。系统性封死"未显式登记的写口=任意角色可写"的 BFLA 整类。
return APPROVER_OR_ADMIN;
}
/**
* 把请求 URI 规范化到与 Spring 控制器映射一致的口径,杜绝授权前缀匹配被绕过:
* URL 解码(%75→u)、剥除每段矩阵参数(;k=v)、折叠重复斜杠、去尾斜杠、统一小写。
* 解码异常时回退原串(宁可多拦不可漏拦)。
*/
static String normalizePath(String uri) {
if (uri == null) {
return "";
}
String p = uri;
try {
p = URLDecoder.decode(p, StandardCharsets.UTF_8);
} catch (RuntimeException ignore) {
// 非法转义序列:保留原串继续规范化(授权偏保守,不放行可疑请求)。
}
p = p.replaceAll(";[^/]*", ""); // 剥矩阵参数 /users;x=1 -> /users
p = p.replaceAll("/{2,}", "/"); // 折叠重复斜杠 //api -> /api
if (p.length() > 1 && p.endsWith("/")) {
p = p.substring(0, p.length() - 1);
}
return p.toLowerCase(Locale.ROOT);
}
private void writeError(HttpServletResponse response, HttpStatus status, int code, String message) throws Exception {
response.setStatus(status.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"code\":" + code + ",\"message\":\"" + message + "\",\"data\":null}");
}
}
@@ -0,0 +1,58 @@
package com.kaidi.oa.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* CORS + auth-interceptor registration for the OA frontend.
*
* Allows the Vue dev servers and local hosts to call /api/** during development.
* allowedOriginPatterns is used (instead of allowedOrigins) so the 127.0.0.1
* wildcard port pattern is accepted alongside credentials.
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
private final AuditLogInterceptor auditLogInterceptor;
public CorsConfig(AuthInterceptor authInterceptor, AuditLogInterceptor auditLogInterceptor) {
this.authInterceptor = authInterceptor;
this.auditLogInterceptor = auditLogInterceptor;
}
@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
// 审计拦截器先注册:afterCompletion 按注册逆序执行,且 Spring 仅对 preHandle 返回 true 的拦截器
// 回调 afterCompletion;审计 preHandle 恒 true 且排在 Auth 之前,故连「被授权拒绝(401/403)的越权写尝试」
// 也能落操作审计留痕(完整留痕)。
registry.addInterceptor(auditLogInterceptor).addPathPatterns("/api/oa/**");
registry.addInterceptor(authInterceptor).addPathPatterns("/api/oa/**");
}
@Override
public void addCorsMappings(@NonNull CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns(
// any localhost / 127.0.0.1 port (Vite dev defaults to 5175,
// built app is served same-origin); covers dev + preview.
"http://localhost:*",
"http://127.0.0.1:*",
// 公网隧道演示(ngrok / cloudflare)。整站同源经隧道访问本不触发 CORS,
// 这几条只覆盖个别 scheme/host 口径差异。**不再用 "*" 反射任意源**——
// 通配反射 + allowCredentials 是公认反模式(将来改用 cookie 即成 CSRF 通道),
// 收敛到隧道域名精确模式:既能跑演示,又关掉任意源反射面。
"https://*.ngrok-free.dev",
"https://*.ngrok-free.app",
"https://*.ngrok.io",
"https://*.trycloudflare.com"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
@@ -0,0 +1,28 @@
package com.kaidi.oa.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* Shared ObjectMapper bean used by services that read/write the JSON TEXT
* columns (form schema / flow schema / instance data) and by the web layer.
*
* Registers the JavaTime module so java.time.Instant fields serialize as ISO-8601
* strings (not numeric timestamps), and marks the bean @Primary so it is the one
* Spring MVC uses for HTTP message conversion as well.
*/
@Configuration
public class JacksonConfig {
@Bean
@Primary
public ObjectMapper objectMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
}
@@ -0,0 +1,42 @@
package com.kaidi.oa.config;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 全站安全响应头。
*
* 给所有响应统一加:
* - X-Content-Type-Options: nosniff —— 关掉 MIME 嗅探,配合文件下载的类型降级防存储型 XSS;
* - X-Frame-Options: SAMEORIGIN —— 防点击劫持(页面被第三方 iframe 套壳);
* - Referrer-Policy: same-origin —— 跳转外站不泄露内网 URL/带参;
* - X-XSS-Protection: 0 —— 显式关闭旧版浏览器有缺陷的 XSS 审计器(现代做法)。
*
* 不在此处强加严格 CSP:现有 Element Plus SPA 大量内联样式,盲加 CSP 会白屏;CSP 需与前端
* 配合(nonce / hash)单独推进,列为已知后续项。
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SecurityHeadersFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (response instanceof HttpServletResponse resp) {
resp.setHeader("X-Content-Type-Options", "nosniff");
resp.setHeader("X-Frame-Options", "SAMEORIGIN");
resp.setHeader("Referrer-Policy", "same-origin");
resp.setHeader("X-XSS-Protection", "0");
}
chain.doFilter(request, response);
}
}
@@ -0,0 +1,66 @@
package com.kaidi.oa.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* Serves the built Vue SPA from classpath:/static so the whole platform ships as a
* SINGLE deployable jar — running the backend brings the frontend up on the same
* port (no separate vite/static server, no分离部署).
*
* Caching strategy (so frontend updates always reach users):
* - /assets/** are content-hashed (filename changes every build) -> cache hard.
* - index.html + SPA routes -> no-cache, so a rebuilt UI is never served stale.
*
* Any path that is not an existing asset and not an /api/** call falls back to
* index.html so vue-router history mode (deep links / refresh) resolves to the SPA.
* @RestController mappings for /api/** take precedence, so API calls are unaffected.
*/
@Configuration
public class SpaWebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(@NonNull ResourceHandlerRegistry registry) {
// Content-hashed build assets -> safe to cache for a long time.
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/static/assets/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic().immutable());
// index.html, SPA routes, and other root files -> never cache, so a rebuilt
// frontend is picked up immediately; non-asset routes fall back to index.html.
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.noCache())
.resourceChain(true)
.addResolver(new PathResourceResolver() {
@Override
protected Resource getResource(@NonNull String resourcePath, @NonNull Resource location)
throws IOException {
Resource requested = location.createRelative(resourcePath);
if (requested.exists() && requested.isReadable()) {
return requested;
}
// Never hand API calls an HTML page; let them resolve elsewhere / 404.
if (resourcePath.startsWith("api/")) {
return null;
}
// A missing asset-looking path (has an extension) is a real 404.
if (resourcePath.contains(".")) {
return null;
}
// SPA route (no extension) -> index.html (history-mode fallback).
Resource index = new ClassPathResource("/static/index.html");
return index.exists() ? index : null;
}
});
}
}
@@ -0,0 +1,157 @@
package com.kaidi.oa.config;
import com.kaidi.oa.domain.CollabDoc;
import com.kaidi.oa.domain.SysUser;
import com.kaidi.oa.repository.CollabDocRepository;
import com.kaidi.oa.service.AuthService;
import com.kaidi.oa.service.AuthorizationService;
import com.kaidi.oa.web.ws.CollabSocketHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.NonNull;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.net.URI;
import java.util.Arrays;
import java.util.Map;
/**
* Registers the real-time collaboration WebSocket endpoint.
*
* /ws/collab/{docId} relays yjs CRDT updates between clients editing the same
* document. 它在 /api/oa/** 之外,AuthInterceptor 不覆盖它——因此**在握手阶段**自行鉴权:
* 浏览器 WS 无法带自定义头,token 通过查询串 ?token=xxx 传入,经 AuthService.resolve 校验,
* 未认证一律拒绝握手(401)。**并做对象级鉴权**:解析路径里的 docId,仅文档属主(按稳定 ownerId)、
* 参与者(participants 含本人 displayName)或 ADMIN 可握手,杜绝任意登录用户按 docId 枚举读/写他人
* 文档(与 CollabDocController 的对象级口径一致,堵 WS 旁路 BOLA)。同时把来源从
* setAllowedOrigins("*") 收敛到与 HTTP CORS 一致的域名模式,杜绝跨站 WebSocket 劫持(CSWSH)。
*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final CollabSocketHandler collabSocketHandler;
private final AuthService authService;
private final CollabDocRepository collabDocRepo;
private final AuthorizationService authz;
public WebSocketConfig(CollabSocketHandler collabSocketHandler, AuthService authService,
CollabDocRepository collabDocRepo, AuthorizationService authz) {
this.collabSocketHandler = collabSocketHandler;
this.authService = authService;
this.collabDocRepo = collabDocRepo;
this.authz = authz;
}
@Override
public void registerWebSocketHandlers(@NonNull WebSocketHandlerRegistry registry) {
registry.addHandler(collabSocketHandler, "/ws/collab/**")
.addInterceptors(new TokenHandshakeInterceptor(authService, collabDocRepo, authz))
.setAllowedOriginPatterns(
"http://localhost:*", "http://127.0.0.1:*",
"https://*.ngrok-free.dev", "https://*.ngrok-free.app",
"https://*.ngrok.io", "https://*.trycloudflare.com");
}
/** 握手鉴权:先验 token,再按 docId 做对象级访问控制(属主/参与者/ADMIN)。 */
static final class TokenHandshakeInterceptor implements HandshakeInterceptor {
private final AuthService authService;
private final CollabDocRepository collabDocRepo;
private final AuthorizationService authz;
TokenHandshakeInterceptor(AuthService authService, CollabDocRepository collabDocRepo,
AuthorizationService authz) {
this.authService = authService;
this.collabDocRepo = collabDocRepo;
this.authz = authz;
}
@Override
public boolean beforeHandshake(@NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response,
@NonNull WebSocketHandler wsHandler, @NonNull Map<String, Object> attributes) {
// 1) 认证:?token= 经 AuthService 校验,未认证拒绝握手。
String token = tokenFrom(request.getURI());
SysUser su = token == null ? null : authService.resolve(token);
if (su == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return false;
}
// 2) 对象级鉴权:解析 docId,仅属主/参与者/ADMIN 可接入该文档。
Long docId = docIdFrom(request.getURI());
if (docId == null) {
// 路径无有效数字 docId:无法定位文档,拒绝(杜绝绕过对象级校验的非法房间名)。
response.setStatusCode(HttpStatus.FORBIDDEN);
return false;
}
CollabDoc doc = collabDocRepo.findById(docId).orElse(null);
if (doc == null) {
response.setStatusCode(HttpStatus.NOT_FOUND);
return false;
}
if (!canAccess(su, doc)) {
response.setStatusCode(HttpStatus.FORBIDDEN);
return false;
}
return true;
}
/** ADMIN、文档属主(稳定 ownerId 优先、空回退 owner 名),或参与者(participants 含本人 displayName)。 */
private boolean canAccess(SysUser su, CollabDoc doc) {
if (authz.isAdmin(su) || authz.isOwner(su, doc.getOwnerId(), doc.getOwner())) {
return true;
}
String me = su.getDisplayName();
String participants = doc.getParticipants();
if (me == null || me.isBlank() || participants == null || participants.isBlank()) {
return false;
}
// participants 为逗号分隔的 displayName 列表(协作成员)。
return Arrays.stream(participants.split(","))
.map(String::trim)
.anyMatch(me::equals);
}
@Override
public void afterHandshake(@NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response,
@NonNull WebSocketHandler wsHandler, Exception exception) {
// no-op
}
/** 从 /ws/collab/{docId} 路径解析末段为数字 docId;非数字/缺失返回 null。 */
private static Long docIdFrom(URI uri) {
String path = uri.getPath();
if (path == null) {
return null;
}
int slash = path.lastIndexOf('/');
String last = slash >= 0 ? path.substring(slash + 1) : path;
try {
return Long.parseLong(last.trim());
} catch (NumberFormatException e) {
return null;
}
}
private static String tokenFrom(URI uri) {
String q = uri.getQuery();
if (q == null) {
return null;
}
for (String pair : q.split("&")) {
int eq = pair.indexOf('=');
if (eq > 0 && "token".equals(pair.substring(0, eq))) {
String v = pair.substring(eq + 1);
return v.isBlank() ? null : v;
}
}
return null;
}
}
}
@@ -0,0 +1,115 @@
package com.kaidi.oa.domain;
import java.math.BigDecimal;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* An accounting subject (会计科目) for the finance general-ledger center
* (财务总账). code is the subject code (such as 1001 / 1122 / 6001); category is
* one of 资产 / 负债 / 权益 / 成本 / 损益; direction (余额方向) is one of 借 / 贷;
* parentCode is the optional parent code and level is the subject level (级次);
* balance is the closing balance (期末余额).
*/
@Entity
@Table(name = "account")
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
private String name;
private String category;
private String direction;
private String parentCode;
private int level;
private BigDecimal balance = BigDecimal.ZERO;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getDirection() {
return direction;
}
public void setDirection(String direction) {
this.direction = direction;
}
public String getParentCode() {
return parentCode;
}
public void setParentCode(String parentCode) {
this.parentCode = parentCode;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,80 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·门禁出入记录(安保与门禁 PARTIAL 缺口补全)。
* 记录员工工卡/人脸识别门禁进出记录、外来车辆登记。
* recordType: 员工刷卡 / 人脸识别 / 外来车辆 / 大件物品
*/
@Entity
@Table(name = "admin_access_record")
public class AdminAccessRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 记录类型:员工刷卡 / 人脸识别 / 外来车辆 / 大件物品带出。 */
private String recordType;
/** 员工姓名或来访人员/车辆信息。 */
private String personInfo;
/** 员工工号/访客证号/车牌号。 */
private String identifier;
/** 门禁位置/门区(主门/东门/车库/货运入口等)。 */
private String gateLocation;
/** 进/出方向。 */
private String direction;
/** 出入时间。 */
private Instant accessTime;
/** 大件物品描述(recordType=大件物品带出时填写)。 */
private String itemDesc;
/** 关联审批单号(大件物品带出审批)。 */
private String approvalCode;
/** 操作员/门卫姓名。 */
private String guardName;
/** 备注。 */
private String remark;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getRecordType() { return recordType; }
public void setRecordType(String recordType) { this.recordType = recordType; }
public String getPersonInfo() { return personInfo; }
public void setPersonInfo(String personInfo) { this.personInfo = personInfo; }
public String getIdentifier() { return identifier; }
public void setIdentifier(String identifier) { this.identifier = identifier; }
public String getGateLocation() { return gateLocation; }
public void setGateLocation(String gateLocation) { this.gateLocation = gateLocation; }
public String getDirection() { return direction; }
public void setDirection(String direction) { this.direction = direction; }
public Instant getAccessTime() { return accessTime; }
public void setAccessTime(Instant accessTime) { this.accessTime = accessTime; }
public String getItemDesc() { return itemDesc; }
public void setItemDesc(String itemDesc) { this.itemDesc = itemDesc; }
public String getApprovalCode() { return approvalCode; }
public void setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; }
public String getGuardName() { return guardName; }
public void setGuardName(String guardName) { this.guardName = guardName; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,97 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·AI行政动作落库日志。
* 记录"会议纪要AI整理→推送到指定位置"、"多部门周报/月报AI汇总→生成文档"等
* AI工作流动作结果,实现业务闭环(push target 落库)。
*/
@Entity
@Table(name = "admin_ai_action_log")
public class AdminAiActionLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 动作类型:MINUTES_ORGANIZE(纪要整理)/ REPORT_SUMMARY(周月报汇总)/ DOC_GENERATE(公文生成)。 */
@Column(nullable = false)
private String actionType;
/** 来源对象类型:meeting_minute / dept_report / custom。 */
private String sourceType;
/** 来源对象 ID(如 meetingMinute.id)。 */
private Long sourceId;
/** 来源对象标题/描述。 */
private String sourceTitle;
/** 推送目标位置描述(如"行政档案库-2026年Q2纪要"、"各部门周报汇总钉钉群")。 */
@Column(length = 500)
private String pushTarget;
/** 涉及部门(逗号分隔,周报汇总时使用)。 */
@Column(length = 1000)
private String involvedDepts;
/** 报告周期(如 2026-W24、2026-06)。 */
private String reportPeriod;
/** AI生成结果摘要(存储前256字符)。 */
@Column(length = 2000)
private String resultSummary;
/** AI模型。 */
private String aiModel;
/** 执行人(谁触发了此动作)。 */
private String operator;
/** 状态:成功 / 失败 / AI未配置。 */
@Column(nullable = false)
private String actionStatus;
/** 失败原因。 */
@Column(length = 500)
private String errorMsg;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getActionType() { return actionType; }
public void setActionType(String actionType) { this.actionType = actionType; }
public String getSourceType() { return sourceType; }
public void setSourceType(String sourceType) { this.sourceType = sourceType; }
public Long getSourceId() { return sourceId; }
public void setSourceId(Long sourceId) { this.sourceId = sourceId; }
public String getSourceTitle() { return sourceTitle; }
public void setSourceTitle(String sourceTitle) { this.sourceTitle = sourceTitle; }
public String getPushTarget() { return pushTarget; }
public void setPushTarget(String pushTarget) { this.pushTarget = pushTarget; }
public String getInvolvedDepts() { return involvedDepts; }
public void setInvolvedDepts(String involvedDepts) { this.involvedDepts = involvedDepts; }
public String getReportPeriod() { return reportPeriod; }
public void setReportPeriod(String reportPeriod) { this.reportPeriod = reportPeriod; }
public String getResultSummary() { return resultSummary; }
public void setResultSummary(String resultSummary) { this.resultSummary = resultSummary; }
public String getAiModel() { return aiModel; }
public void setAiModel(String aiModel) { this.aiModel = aiModel; }
public String getOperator() { return operator; }
public void setOperator(String operator) { this.operator = operator; }
public String getActionStatus() { return actionStatus; }
public void setActionStatus(String actionStatus) { this.actionStatus = actionStatus; }
public String getErrorMsg() { return errorMsg; }
public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,88 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·监控设备台账(安保与门禁 PARTIAL 缺口补全)。
* 记录监控摄像头/NVR等设备的安装位置、运行状态、巡检记录。
* status: 运行正常 / 故障 / 维修中 / 已停用
*/
@Entity
@Table(name = "admin_cctv_device")
public class AdminCctvDevice {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 设备编号(自动生成)。 */
private String deviceCode;
/** 设备名称。 */
private String deviceName;
/** 设备类型(球机/枪机/半球/NVR/其他)。 */
private String deviceType;
/** 安装位置。 */
private String location;
/** 安装日期(YYYY-MM-DD)。 */
private String installDate;
/** 品牌/型号。 */
private String brandModel;
/** IP 地址。 */
private String ipAddress;
/** 存储时长(天)。 */
private Integer storeDays;
/** 运行状态(运行正常/故障/维修中/已停用)。 */
private String status;
/** 最近巡检日期。 */
private String lastInspectDate;
/** 备注。 */
private String remark;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getDeviceCode() { return deviceCode; }
public void setDeviceCode(String deviceCode) { this.deviceCode = deviceCode; }
public String getDeviceName() { return deviceName; }
public void setDeviceName(String deviceName) { this.deviceName = deviceName; }
public String getDeviceType() { return deviceType; }
public void setDeviceType(String deviceType) { this.deviceType = deviceType; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getInstallDate() { return installDate; }
public void setInstallDate(String installDate) { this.installDate = installDate; }
public String getBrandModel() { return brandModel; }
public void setBrandModel(String brandModel) { this.brandModel = brandModel; }
public String getIpAddress() { return ipAddress; }
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
public Integer getStoreDays() { return storeDays; }
public void setStoreDays(Integer storeDays) { this.storeDays = storeDays; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getLastInspectDate() { return lastInspectDate; }
public void setLastInspectDate(String lastInspectDate) { this.lastInspectDate = lastInspectDate; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,74 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·保洁排班。制定保洁人员每日/每周工作区域与频次,签到打卡,记录完成情况。
* status: 待执行 / 执行中 / 已完成 / 已缺勤
*/
@Entity
@Table(name = "admin_cleaning_schedule")
public class AdminCleaningSchedule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 保洁人员姓名。 */
private String cleaner;
/** 工作区域(办公区/公共卫生间/大厅/停车场等)。 */
private String area;
/** 排班日期(YYYY-MM-DD)。 */
private String scheduleDate;
/** 时间段(上午/下午/全天/其他)。 */
private String shift;
/** 清洁频次(每日/每周/按需)。 */
private String frequency;
/** 签到时间(ISO 时间戳)。 */
private Instant checkinAt;
/** 完成时间(ISO 时间戳)。 */
private Instant completedAt;
/** 完成情况说明。 */
private String completionNote;
/** 状态:待执行 / 执行中 / 已完成 / 已缺勤。 */
private String status;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCleaner() { return cleaner; }
public void setCleaner(String cleaner) { this.cleaner = cleaner; }
public String getArea() { return area; }
public void setArea(String area) { this.area = area; }
public String getScheduleDate() { return scheduleDate; }
public void setScheduleDate(String scheduleDate) { this.scheduleDate = scheduleDate; }
public String getShift() { return shift; }
public void setShift(String shift) { this.shift = shift; }
public String getFrequency() { return frequency; }
public void setFrequency(String frequency) { this.frequency = frequency; }
public Instant getCheckinAt() { return checkinAt; }
public void setCheckinAt(Instant checkinAt) { this.checkinAt = checkinAt; }
public Instant getCompletedAt() { return completedAt; }
public void setCompletedAt(Instant completedAt) { this.completedAt = completedAt; }
public String getCompletionNote() { return completionNote; }
public void setCompletionNote(String completionNote) { this.completionNote = completionNote; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,89 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 行政/办公室·保洁专属清洁物资台账(清洁剂/工具/垃圾桶等)。
* 独立于 AdminSupply 通用物资,聚焦保洁域库存与领用预警,可由保洁排班联动触发领用记录。
*/
@Entity
@Table(name = "admin_cleaning_supply")
public class AdminCleaningSupply {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 物料编号(自动生成,格式 CS-XXXX)。 */
@Column(nullable = false, unique = true)
private String supplyCode;
/** 物料名称(如 84消毒液、拖把头、垃圾袋50L)。 */
@Column(nullable = false)
private String supplyName;
/** 分类:清洁剂 / 清洁工具 / 垃圾桶/袋 / 消耗品 / 防护用品 / 其他。 */
@Column(nullable = false)
private String supplyCategory;
/** 规格(如 5L/桶、10只/包)。 */
private String spec;
/** 单位。 */
private String unitName;
/** 参考单价(BigDecimal)。 */
@Column(precision = 12, scale = 2)
private BigDecimal refPrice;
/** 当前库存数量。 */
private Double onHand;
/** 安全库存(低于此值触发预警)。 */
private Double safetyStock;
/** 存放位置(如 保洁间A、地下室储物间)。 */
private String location;
/** 状态:启用 / 停用。 */
@Column(nullable = false)
private String supplyStatus;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getSupplyCode() { return supplyCode; }
public void setSupplyCode(String supplyCode) { this.supplyCode = supplyCode; }
public String getSupplyName() { return supplyName; }
public void setSupplyName(String supplyName) { this.supplyName = supplyName; }
public String getSupplyCategory() { return supplyCategory; }
public void setSupplyCategory(String supplyCategory) { this.supplyCategory = supplyCategory; }
public String getSpec() { return spec; }
public void setSpec(String spec) { this.spec = spec; }
public String getUnitName() { return unitName; }
public void setUnitName(String unitName) { this.unitName = unitName; }
public BigDecimal getRefPrice() { return refPrice; }
public void setRefPrice(BigDecimal refPrice) { this.refPrice = refPrice; }
public Double getOnHand() { return onHand; }
public void setOnHand(Double onHand) { this.onHand = onHand; }
public Double getSafetyStock() { return safetyStock; }
public void setSafetyStock(Double safetyStock) { this.safetyStock = safetyStock; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getSupplyStatus() { return supplyStatus; }
public void setSupplyStatus(String supplyStatus) { this.supplyStatus = supplyStatus; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,78 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·保洁清洁物资出入库流水(领用登记,可由保洁排班触发)。
*/
@Entity
@Table(name = "admin_cleaning_supply_move")
public class AdminCleaningSupplyMove {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联清洁物资 ID。 */
@Column(nullable = false)
private Long cleaningSupplyId;
/** 物料名称(冗余快照)。 */
private String supplyName;
/** 方向:入库 / 领用。 */
@Column(nullable = false)
private String moveDirection;
/** 数量。 */
@Column(nullable = false)
private Double moveQty;
/** 领用人/入库经手人。 */
private String handler;
/** 关联保洁排班 ID(保洁日程联动领用时填写)。 */
private Long scheduleId;
/** 发生日期(yyyy-MM-dd)。 */
private String moveDate;
/** 入库:供应商;领用:领用区域。 */
private String source;
/** 备注。 */
@Column(length = 500)
private String remark;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getCleaningSupplyId() { return cleaningSupplyId; }
public void setCleaningSupplyId(Long cleaningSupplyId) { this.cleaningSupplyId = cleaningSupplyId; }
public String getSupplyName() { return supplyName; }
public void setSupplyName(String supplyName) { this.supplyName = supplyName; }
public String getMoveDirection() { return moveDirection; }
public void setMoveDirection(String moveDirection) { this.moveDirection = moveDirection; }
public Double getMoveQty() { return moveQty; }
public void setMoveQty(Double moveQty) { this.moveQty = moveQty; }
public String getHandler() { return handler; }
public void setHandler(String handler) { this.handler = handler; }
public Long getScheduleId() { return scheduleId; }
public void setScheduleId(Long scheduleId) { this.scheduleId = scheduleId; }
public String getMoveDate() { return moveDate; }
public void setMoveDate(String moveDate) { this.moveDate = moveDate; }
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,92 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·大型会议物料准备清单(席卡/茶歇/签到表/水/笔记本/宣传品等)。
* 每条记录对应某次会议的某一物料项,可独立登记数量、准备状态与负责人。
*/
@Entity
@Table(name = "admin_conference_material")
public class AdminConferenceMaterial {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联会议 ID(对应 meeting.id,可选,也可为大型活动标题)。 */
private Long meetingId;
/** 活动/会议名称(meetingId 为空时直接填写)。 */
@Column(nullable = false)
private String meetingTitle;
/** 物料类型:席卡 / 茶歇 / 签到表 / 纸笔 / 水 / 宣传品 / 礼品 / 其他。 */
@Column(nullable = false)
private String materialType;
/** 物料名称/规格描述。 */
private String materialName;
/** 计划数量。 */
private Integer planQty;
/** 实际准备数量。 */
private Integer actualQty;
/** 单位(套/份/瓶/本/张)。 */
private String unitName;
/** 负责人。 */
private String handler;
/** 准备状态:待准备 / 准备中 / 已就绪 / 缺货。 */
@Column(nullable = false)
private String prepStatus;
/** 备注(采购来源/注意事项)。 */
@Column(length = 1000)
private String remark;
/** 计划就绪日期(yyyy-MM-dd)。 */
private String readyDate;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getMeetingId() { return meetingId; }
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
public String getMeetingTitle() { return meetingTitle; }
public void setMeetingTitle(String meetingTitle) { this.meetingTitle = meetingTitle; }
public String getMaterialType() { return materialType; }
public void setMaterialType(String materialType) { this.materialType = materialType; }
public String getMaterialName() { return materialName; }
public void setMaterialName(String materialName) { this.materialName = materialName; }
public Integer getPlanQty() { return planQty; }
public void setPlanQty(Integer planQty) { this.planQty = planQty; }
public Integer getActualQty() { return actualQty; }
public void setActualQty(Integer actualQty) { this.actualQty = actualQty; }
public String getUnitName() { return unitName; }
public void setUnitName(String unitName) { this.unitName = unitName; }
public String getHandler() { return handler; }
public void setHandler(String handler) { this.handler = handler; }
public String getPrepStatus() { return prepStatus; }
public void setPrepStatus(String prepStatus) { this.prepStatus = prepStatus; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public String getReadyDate() { return readyDate; }
public void setReadyDate(String readyDate) { this.readyDate = readyDate; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,83 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·大型会议现场服务人员安排。
* 记录每次大型会议的服务岗位分工(迎宾/签到/引导/摄影/速记/茶水/IT支持等)与人员对应关系。
*/
@Entity
@Table(name = "admin_conference_staff")
public class AdminConferenceStaff {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联会议 ID(可选)。 */
private Long meetingId;
/** 会议/活动名称。 */
@Column(nullable = false)
private String meetingTitle;
/** 服务岗位:迎宾 / 签到 / 引导 / 摄影 / 速记 / 茶水 / IT支持 / 主持协助 / 其他。 */
@Column(nullable = false)
private String roleType;
/** 负责人姓名。 */
@Column(nullable = false)
private String staffName;
/** 所属部门。 */
private String dept;
/** 联系电话。 */
private String phone;
/** 服务时间段(如 09:00-12:00)。 */
private String serviceTime;
/** 到岗状态:待到岗 / 已到岗 / 缺勤。 */
@Column(nullable = false)
private String arrivalStatus;
/** 备注。 */
@Column(length = 500)
private String remark;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getMeetingId() { return meetingId; }
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
public String getMeetingTitle() { return meetingTitle; }
public void setMeetingTitle(String meetingTitle) { this.meetingTitle = meetingTitle; }
public String getRoleType() { return roleType; }
public void setRoleType(String roleType) { this.roleType = roleType; }
public String getStaffName() { return staffName; }
public void setStaffName(String staffName) { this.staffName = staffName; }
public String getDept() { return dept; }
public void setDept(String dept) { this.dept = dept; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getServiceTime() { return serviceTime; }
public void setServiceTime(String serviceTime) { this.serviceTime = serviceTime; }
public String getArrivalStatus() { return arrivalStatus; }
public void setArrivalStatus(String arrivalStatus) { this.arrivalStatus = arrivalStatus; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,163 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·公文模板库。
*
* 状态流转:DRAFT(草稿)→ REVIEW(审核中)→ APPROVED(已发布)→ ARCHIVED(已归档)。
* 支持按公文类型(通知/请示/报告/函等)、适用场景、责任部门分类管理。
* 版本号字段支持模板修订记录。
*
* 解决 GAP 2 low:「无专属公文模板管理(正式模板库/审批流+公文归档)」。
*/
@Entity
@Table(name = "admin_doc_template")
public class AdminDocTemplate {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 模板名称,如「关于XXX的通知」。 */
@Column(nullable = false, length = 200)
private String templateName;
/** 公文类型:通知/公告/请示/报告/函/会议纪要/批复/决定/规定/办法/合同/协议/其他。 */
@Column(nullable = false, length = 50)
private String docCategory;
/** 适用场景描述,如「跨部门协调事项通知」。 */
@Column(length = 500)
private String scenario;
/** 责任部门,如「行政办公室」。 */
@Column(length = 100)
private String ownerDept;
/** 模板正文内容(含占位符,如【标题】【日期】【责任人】)。 */
@Column(columnDefinition = "TEXT")
private String templateContent;
/** 格式说明(纸张/字体/页边距/格式要求)。 */
@Column(length = 1000)
private String formatSpec;
/** 使用说明。 */
@Column(length = 1000)
private String usage;
/** 版本号,如 v1.0/v2.1。 */
@Column(nullable = false, length = 20)
private String version;
/**
* 状态:DRAFT(草稿)/ REVIEW(审核中)/ APPROVED(已发布)/ ARCHIVED(已归档)。
* 注意:status 是 SQLite 保留字,映射到物理列 doc_status。
*/
@Column(name = "doc_status", nullable = false, length = 20)
private String status;
/** 创建人(displayName)。 */
@Column(length = 100)
private String createdBy;
/** 审核人(displayName)。 */
@Column(length = 100)
private String reviewedBy;
/** 审核意见。 */
@Column(length = 500)
private String reviewComment;
/** 发布人(displayName)。 */
@Column(length = 100)
private String publishedBy;
/** 发布时间。 */
private Instant publishedAt;
/** 归档原因。 */
@Column(length = 500)
private String archiveReason;
/** 归档时间。 */
private Instant archivedAt;
/** 使用次数(每次基于本模板起草时+1)。 */
private int usageCount;
private Instant createdAt;
private Instant updatedAt;
// ----------------------------------------------------------------
// Getters & Setters
// ----------------------------------------------------------------
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTemplateName() { return templateName; }
public void setTemplateName(String templateName) { this.templateName = templateName; }
public String getDocCategory() { return docCategory; }
public void setDocCategory(String docCategory) { this.docCategory = docCategory; }
public String getScenario() { return scenario; }
public void setScenario(String scenario) { this.scenario = scenario; }
public String getOwnerDept() { return ownerDept; }
public void setOwnerDept(String ownerDept) { this.ownerDept = ownerDept; }
public String getTemplateContent() { return templateContent; }
public void setTemplateContent(String templateContent) { this.templateContent = templateContent; }
public String getFormatSpec() { return formatSpec; }
public void setFormatSpec(String formatSpec) { this.formatSpec = formatSpec; }
public String getUsage() { return usage; }
public void setUsage(String usage) { this.usage = usage; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
public String getReviewedBy() { return reviewedBy; }
public void setReviewedBy(String reviewedBy) { this.reviewedBy = reviewedBy; }
public String getReviewComment() { return reviewComment; }
public void setReviewComment(String reviewComment) { this.reviewComment = reviewComment; }
public String getPublishedBy() { return publishedBy; }
public void setPublishedBy(String publishedBy) { this.publishedBy = publishedBy; }
public Instant getPublishedAt() { return publishedAt; }
public void setPublishedAt(Instant publishedAt) { this.publishedAt = publishedAt; }
public String getArchiveReason() { return archiveReason; }
public void setArchiveReason(String archiveReason) { this.archiveReason = archiveReason; }
public Instant getArchivedAt() { return archivedAt; }
public void setArchivedAt(Instant archivedAt) { this.archivedAt = archivedAt; }
public int getUsageCount() { return usageCount; }
public void setUsageCount(int usageCount) { this.usageCount = usageCount; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,179 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 宿舍入住/退宿申请(行政/办公室·宿舍管理)。员工在线申请入住(入住时间、预计退宿),
* 审批通过后分配房间/床位(占用 +1);退宿时释放床位(占用 -1)并提示检查设施、结清水电费。
*
* 状态机:待审批 → 已入住 → 已退宿 / 已驳回。assign 分配房间占床,checkout 退宿释床。
*/
@Entity
@Table(name = "admin_dorm_checkin")
public class AdminDormCheckin {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
/** 入住员工。 */
private String employee;
private String dept;
/** 联系电话。 */
private String phone;
private String checkinDate;
/** 预计退宿日期。 */
private String expectCheckout;
/** 实际退宿日期(退宿时回填)。 */
private String actualCheckout;
/** 分配的房间 id(审批分配后回填)。 */
private Long roomId;
private String building;
private String roomNo;
/** 待审批 / 已入住 / 已退宿 / 已驳回。 */
private String status;
private String approver;
/** 退宿设施检查结论。 */
private String checkoutNote;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getEmployee() {
return employee;
}
public void setEmployee(String employee) {
this.employee = employee;
}
public String getDept() {
return dept;
}
public void setDept(String dept) {
this.dept = dept;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getCheckinDate() {
return checkinDate;
}
public void setCheckinDate(String checkinDate) {
this.checkinDate = checkinDate;
}
public String getExpectCheckout() {
return expectCheckout;
}
public void setExpectCheckout(String expectCheckout) {
this.expectCheckout = expectCheckout;
}
public String getActualCheckout() {
return actualCheckout;
}
public void setActualCheckout(String actualCheckout) {
this.actualCheckout = actualCheckout;
}
public Long getRoomId() {
return roomId;
}
public void setRoomId(Long roomId) {
this.roomId = roomId;
}
public String getBuilding() {
return building;
}
public void setBuilding(String building) {
this.building = building;
}
public String getRoomNo() {
return roomNo;
}
public void setRoomNo(String roomNo) {
this.roomNo = roomNo;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getApprover() {
return approver;
}
public void setApprover(String approver) {
this.approver = approver;
}
public String getCheckoutNote() {
return checkoutNote;
}
public void setCheckoutNote(String checkoutNote) {
this.checkoutNote = checkoutNote;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,110 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·宿舍定期巡检。记录卫生/安全巡检评分、隐患、整改跟踪。
* status: 待巡检 / 巡检中 / 待整改 / 整改完成 / 已关闭
*/
@Entity
@Table(name = "admin_dorm_inspection")
public class AdminDormInspection {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 巡检单号,自动生成。 */
private String code;
/** 巡检类型(卫生巡检/安全巡检)。 */
private String inspectionType;
/** 巡检楼栋(null 表示全楼)。 */
private String building;
/** 巡检人。 */
private String inspector;
/** 计划巡检日期(YYYY-MM-DD)。 */
private String planDate;
/** 实际巡检日期(YYYY-MM-DD)。 */
private String actualDate;
/** 卫生评分(0-100)。 */
private Integer score;
/** 发现隐患描述(多条逗号分隔或 JSON)。 */
private String hazards;
/** 照片(逗号分隔 URL)。 */
private String photos;
private String grade;
/** 整改要求。 */
private String rectifyRequirement;
/** 整改截止日期(YYYY-MM-DD)。 */
private String rectifyDeadline;
/** 整改人。 */
private String rectifyBy;
/** 整改说明。 */
private String rectifyNote;
/** 整改完成日期(YYYY-MM-DD)。 */
private String rectifiedDate;
/** 状态:待巡检 / 巡检中 / 待整改 / 整改完成 / 已关闭。 */
private String status;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getInspectionType() { return inspectionType; }
public void setInspectionType(String inspectionType) { this.inspectionType = inspectionType; }
public String getBuilding() { return building; }
public void setBuilding(String building) { this.building = building; }
public String getInspector() { return inspector; }
public void setInspector(String inspector) { this.inspector = inspector; }
public String getPlanDate() { return planDate; }
public void setPlanDate(String planDate) { this.planDate = planDate; }
public String getActualDate() { return actualDate; }
public void setActualDate(String actualDate) { this.actualDate = actualDate; }
public Integer getScore() { return score; }
public void setScore(Integer score) { this.score = score; }
public String getHazards() { return hazards; }
public void setHazards(String hazards) { this.hazards = hazards; }
public String getGrade() { return grade; }
public void setGrade(String grade) { this.grade = grade; }
public String getPhotos() { return photos; }
public void setPhotos(String photos) { this.photos = photos; }
public String getRectifyRequirement() { return rectifyRequirement; }
public void setRectifyRequirement(String rectifyRequirement) { this.rectifyRequirement = rectifyRequirement; }
public String getRectifyDeadline() { return rectifyDeadline; }
public void setRectifyDeadline(String rectifyDeadline) { this.rectifyDeadline = rectifyDeadline; }
public String getRectifyBy() { return rectifyBy; }
public void setRectifyBy(String rectifyBy) { this.rectifyBy = rectifyBy; }
public String getRectifyNote() { return rectifyNote; }
public void setRectifyNote(String rectifyNote) { this.rectifyNote = rectifyNote; }
public String getRectifiedDate() { return rectifiedDate; }
public void setRectifiedDate(String rectifiedDate) { this.rectifiedDate = rectifiedDate; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,107 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·宿舍维修报修。住宿员工在线报修故障 → 派工至维修人员 → 维修确认闭环。
* status: 待派工 / 维修中 / 已完成 / 已取消
*/
@Entity
@Table(name = "admin_dorm_repair")
public class AdminDormRepair {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 报修单号,自动生成。 */
private String code;
/** 关联宿舍房间 ID。 */
private Long roomId;
/** 宿舍楼(冗余,便于展示)。 */
private String building;
/** 房间号(冗余)。 */
private String roomNo;
/** 报修人姓名。 */
private String reporter;
/** 故障类型(水电/门窗/空调/热水器/其他)。 */
private String faultType;
/** 故障描述。 */
private String description;
/** 照片附件(逗号分隔 URL)。 */
private String photos;
/** 紧急程度(紧急/一般/低)。 */
private String urgency;
/** 派工维修人员。 */
private String assignedTo;
/** 派工时间。 */
private Instant assignedAt;
/** 维修说明。 */
private String repairNote;
/** 完成时间。 */
private Instant completedAt;
/** 报修人满意度评价(5星制,0=未评)。 */
private Integer satisfaction;
/** 状态:待派工 / 维修中 / 已完成 / 已取消。 */
private String status;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public Long getRoomId() { return roomId; }
public void setRoomId(Long roomId) { this.roomId = roomId; }
public String getBuilding() { return building; }
public void setBuilding(String building) { this.building = building; }
public String getRoomNo() { return roomNo; }
public void setRoomNo(String roomNo) { this.roomNo = roomNo; }
public String getReporter() { return reporter; }
public void setReporter(String reporter) { this.reporter = reporter; }
public String getFaultType() { return faultType; }
public void setFaultType(String faultType) { this.faultType = faultType; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getPhotos() { return photos; }
public void setPhotos(String photos) { this.photos = photos; }
public String getUrgency() { return urgency; }
public void setUrgency(String urgency) { this.urgency = urgency; }
public String getAssignedTo() { return assignedTo; }
public void setAssignedTo(String assignedTo) { this.assignedTo = assignedTo; }
public Instant getAssignedAt() { return assignedAt; }
public void setAssignedAt(Instant assignedAt) { this.assignedAt = assignedAt; }
public String getRepairNote() { return repairNote; }
public void setRepairNote(String repairNote) { this.repairNote = repairNote; }
public Instant getCompletedAt() { return completedAt; }
public void setCompletedAt(Instant completedAt) { this.completedAt = completedAt; }
public Integer getSatisfaction() { return satisfaction; }
public void setSatisfaction(Integer satisfaction) { this.satisfaction = satisfaction; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,116 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 宿舍资源台账(行政/办公室·宿舍管理)。记录宿舍楼、房间号、床位总数、设施、当前入住数 occupied。
* occupied 由入住申请审批分配 / 退宿({@link AdminDormCheckin})在控制器内级联自动增减。
* status 据 occupied 与 beds 自动派生展示:空置(0)/ 部分入住 / 已满。
*/
@Entity
@Table(name = "admin_dorm_room")
public class AdminDormRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String building;
private String roomNo;
/** 床位总数。 */
private Integer beds = 0;
/** 当前已入住人数(级联维护)。 */
private Integer occupied = 0;
/** 设施(空调、热水器等)。 */
private String facilities;
/** 房间用途/性别(男/女/混住)。 */
private String roomType;
/** 启用 / 停用(停用不再分配)。 */
private String status;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getBuilding() {
return building;
}
public void setBuilding(String building) {
this.building = building;
}
public String getRoomNo() {
return roomNo;
}
public void setRoomNo(String roomNo) {
this.roomNo = roomNo;
}
public Integer getBeds() {
return beds;
}
public void setBeds(Integer beds) {
this.beds = beds;
}
public Integer getOccupied() {
return occupied;
}
public void setOccupied(Integer occupied) {
this.occupied = occupied;
}
public String getFacilities() {
return facilities;
}
public void setFacilities(String facilities) {
this.facilities = facilities;
}
public String getRoomType() {
return roomType;
}
public void setRoomType(String roomType) {
this.roomType = roomType;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,223 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 宿舍水电费账单(行政/办公室·宿舍管理)。每月录入水/电表起止读数与单价,
* 系统自动算用量与费用:电费 =(elecEnd-elecStart)×elecPrice,水费 同理,合计 totalAmount。
* settled 标记是否已结清(从工资扣除或在线缴费后置已结清)。
* 读数为 double(用量口径),单价/费用为金额(BigDecimal)。
*/
@Entity
@Table(name = "admin_dorm_utility")
public class AdminDormUtility {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long roomId;
private String building;
private String roomNo;
/** 账期 YYYY-MM。 */
private String period;
private Double elecStart = 0d;
private Double elecEnd = 0d;
/** 电单价(元/度)。 */
private BigDecimal elecPrice = BigDecimal.ZERO;
private Double waterStart = 0d;
private Double waterEnd = 0d;
/** 水单价(元/吨)。 */
private BigDecimal waterPrice = BigDecimal.ZERO;
/** 用电量(度,自动算)。 */
private Double elecUsage = 0d;
/** 用水量(吨,自动算)。 */
private Double waterUsage = 0d;
/** 电费(自动算)。 */
private BigDecimal elecAmount = BigDecimal.ZERO;
/** 水费(自动算)。 */
private BigDecimal waterAmount = BigDecimal.ZERO;
/** 合计费用(自动算)。 */
private BigDecimal totalAmount = BigDecimal.ZERO;
/** 结算方式:工资扣除 / 在线缴费。 */
private String settleMethod;
/** 是否已结清。 */
private Boolean settled = Boolean.FALSE;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getRoomId() {
return roomId;
}
public void setRoomId(Long roomId) {
this.roomId = roomId;
}
public String getBuilding() {
return building;
}
public void setBuilding(String building) {
this.building = building;
}
public String getRoomNo() {
return roomNo;
}
public void setRoomNo(String roomNo) {
this.roomNo = roomNo;
}
public String getPeriod() {
return period;
}
public void setPeriod(String period) {
this.period = period;
}
public Double getElecStart() {
return elecStart;
}
public void setElecStart(Double elecStart) {
this.elecStart = elecStart;
}
public Double getElecEnd() {
return elecEnd;
}
public void setElecEnd(Double elecEnd) {
this.elecEnd = elecEnd;
}
public BigDecimal getElecPrice() {
return elecPrice;
}
public void setElecPrice(BigDecimal elecPrice) {
this.elecPrice = elecPrice;
}
public Double getWaterStart() {
return waterStart;
}
public void setWaterStart(Double waterStart) {
this.waterStart = waterStart;
}
public Double getWaterEnd() {
return waterEnd;
}
public void setWaterEnd(Double waterEnd) {
this.waterEnd = waterEnd;
}
public BigDecimal getWaterPrice() {
return waterPrice;
}
public void setWaterPrice(BigDecimal waterPrice) {
this.waterPrice = waterPrice;
}
public Double getElecUsage() {
return elecUsage;
}
public void setElecUsage(Double elecUsage) {
this.elecUsage = elecUsage;
}
public Double getWaterUsage() {
return waterUsage;
}
public void setWaterUsage(Double waterUsage) {
this.waterUsage = waterUsage;
}
public BigDecimal getElecAmount() {
return elecAmount;
}
public void setElecAmount(BigDecimal elecAmount) {
this.elecAmount = elecAmount;
}
public BigDecimal getWaterAmount() {
return waterAmount;
}
public void setWaterAmount(BigDecimal waterAmount) {
this.waterAmount = waterAmount;
}
public BigDecimal getTotalAmount() {
return totalAmount;
}
public void setTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
}
public String getSettleMethod() {
return settleMethod;
}
public void setSettleMethod(String settleMethod) {
this.settleMethod = settleMethod;
}
public Boolean getSettled() {
return settled;
}
public void setSettled(Boolean settled) {
this.settled = settled;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,88 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·大型会议设备调试记录(投影仪/音响/麦克风/视频会议终端等)。
* 记录每次会议前设备调试结果,异常时留存处理过程。
*/
@Entity
@Table(name = "admin_equip_setup")
public class AdminEquipSetup {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联会议 ID(可选)。 */
private Long meetingId;
/** 会议/活动名称。 */
@Column(nullable = false)
private String meetingTitle;
/** 设备类型:投影仪 / 音响 / 麦克风 / 视频终端 / 屏幕 / 网络 / 灯光 / 其他。 */
@Column(nullable = false)
private String equipType;
/** 设备型号/编号。 */
private String equipModel;
/** 调试人员。 */
private String technician;
/** 调试日期(yyyy-MM-dd)。 */
private String setupDate;
/** 调试结果:正常 / 异常 / 待处理。 */
@Column(nullable = false)
private String setupResult;
/** 异常描述(调试结果=异常时必填)。 */
@Column(length = 2000)
private String issueDesc;
/** 处理措施。 */
@Column(length = 2000)
private String resolution;
/** 最终状态:正常 / 故障未修 / 已更换备机。 */
private String finalStatus;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getMeetingId() { return meetingId; }
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
public String getMeetingTitle() { return meetingTitle; }
public void setMeetingTitle(String meetingTitle) { this.meetingTitle = meetingTitle; }
public String getEquipType() { return equipType; }
public void setEquipType(String equipType) { this.equipType = equipType; }
public String getEquipModel() { return equipModel; }
public void setEquipModel(String equipModel) { this.equipModel = equipModel; }
public String getTechnician() { return technician; }
public void setTechnician(String technician) { this.technician = technician; }
public String getSetupDate() { return setupDate; }
public void setSetupDate(String setupDate) { this.setupDate = setupDate; }
public String getSetupResult() { return setupResult; }
public void setSetupResult(String setupResult) { this.setupResult = setupResult; }
public String getIssueDesc() { return issueDesc; }
public void setIssueDesc(String issueDesc) { this.issueDesc = issueDesc; }
public String getResolution() { return resolution; }
public void setResolution(String resolution) { this.resolution = resolution; }
public String getFinalStatus() { return finalStatus; }
public void setFinalStatus(String finalStatus) { this.finalStatus = finalStatus; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,137 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 行政/办公室·快递收发台账(收件 + 寄件二合一)。
* 收件:门卫登记收件人/快递公司/单号 → 自动通知收件人取件。
* 寄件:员工在线申请 → 审批(选填)→ 打印面单 → 费用归集部门。
* direction: 收件 / 寄件
* status(收件): 待取件 / 已取件 / 已退回
* status(寄件): 草稿 / 待审批 / 已批准 / 已寄出 / 已驳回
*/
@Entity
@Table(name = "admin_express")
public class AdminExpress {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 快递单号(登记单号)。 */
private String code;
/** 方向:收件 / 寄件。 */
private String direction;
/** 快递公司(顺丰/EMS/圆通/中通/京东/其他)。 */
private String carrier;
/** 运单号(快递公司单号)。 */
private String trackingNo;
/** 收件人(收件场景:目标员工;寄件场景:对方收件人)。 */
private String recipient;
/** 收件人部门。 */
private String recipientDept;
/** 收件人联系电话。 */
private String recipientPhone;
/** 收件地址(寄件用)。 */
private String recipientAddress;
/** 寄件人(寄件场景:申请员工)。 */
private String sender;
/** 寄件人部门。 */
private String senderDept;
/** 物品描述。 */
private String itemDesc;
/** 重量(kg,寄件用)。 */
private Double weight;
/** 快递费用(元,寄件用)。 */
private BigDecimal freightCost;
/** 费用归集部门(寄件)。 */
private String costDept;
/** 登记人/门卫(收件场景)。 */
private String registrar;
/** 登记/签收时间。 */
private Instant registeredAt;
/** 取件/寄出时间。 */
private Instant pickedAt;
/** 通知发送时间(收件到件通知)。 */
private Instant notifiedAt;
/** 备注。 */
private String remark;
/** 状态(见类注释)。 */
private String status;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getDirection() { return direction; }
public void setDirection(String direction) { this.direction = direction; }
public String getCarrier() { return carrier; }
public void setCarrier(String carrier) { this.carrier = carrier; }
public String getTrackingNo() { return trackingNo; }
public void setTrackingNo(String trackingNo) { this.trackingNo = trackingNo; }
public String getRecipient() { return recipient; }
public void setRecipient(String recipient) { this.recipient = recipient; }
public String getRecipientDept() { return recipientDept; }
public void setRecipientDept(String recipientDept) { this.recipientDept = recipientDept; }
public String getRecipientPhone() { return recipientPhone; }
public void setRecipientPhone(String recipientPhone) { this.recipientPhone = recipientPhone; }
public String getRecipientAddress() { return recipientAddress; }
public void setRecipientAddress(String recipientAddress) { this.recipientAddress = recipientAddress; }
public String getSender() { return sender; }
public void setSender(String sender) { this.sender = sender; }
public String getSenderDept() { return senderDept; }
public void setSenderDept(String senderDept) { this.senderDept = senderDept; }
public String getItemDesc() { return itemDesc; }
public void setItemDesc(String itemDesc) { this.itemDesc = itemDesc; }
public Double getWeight() { return weight; }
public void setWeight(Double weight) { this.weight = weight; }
public BigDecimal getFreightCost() { return freightCost; }
public void setFreightCost(BigDecimal freightCost) { this.freightCost = freightCost; }
public String getCostDept() { return costDept; }
public void setCostDept(String costDept) { this.costDept = costDept; }
public String getRegistrar() { return registrar; }
public void setRegistrar(String registrar) { this.registrar = registrar; }
public Instant getRegisteredAt() { return registeredAt; }
public void setRegisteredAt(Instant registeredAt) { this.registeredAt = registeredAt; }
public Instant getPickedAt() { return pickedAt; }
public void setPickedAt(Instant pickedAt) { this.pickedAt = pickedAt; }
public Instant getNotifiedAt() { return notifiedAt; }
public void setNotifiedAt(Instant notifiedAt) { this.notifiedAt = notifiedAt; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,100 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·录像调阅申请(安保与门禁 PARTIAL 缺口补全)。
* 调阅申请 → 审批 → 已调阅 三步审批流闭环。
* status: 待审批 / 已批准 / 已拒绝 / 已调阅
*/
@Entity
@Table(name = "admin_footage_request")
public class AdminFootageRequest {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 申请编号(自动生成)。 */
private String code;
/** 申请人。 */
private String applicant;
/** 申请人部门。 */
private String dept;
/** 申请用途(事故调查/纠纷取证/安全稽查/其他)。 */
private String purpose;
/** 涉及设备(关联 AdminCctvDevice.deviceCode 或自由填写)。 */
private String deviceDesc;
/** 调阅时间段 - 开始(YYYY-MM-DD HH:mm)。 */
private String footageStart;
/** 调阅时间段 - 结束(YYYY-MM-DD HH:mm)。 */
private String footageEnd;
/** 申请日期(YYYY-MM-DD)。 */
private String applyDate;
/** 审批人。 */
private String approver;
/** 审批意见。 */
private String approvalComment;
/** 审批时间。 */
private Instant approvalTime;
/** 实际调阅时间。 */
private Instant viewTime;
/** 调阅操作员(负责提供录像的人员)。 */
private String operator;
/** 状态:待审批 / 已批准 / 已拒绝 / 已调阅。 */
private String status;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getApplicant() { return applicant; }
public void setApplicant(String applicant) { this.applicant = applicant; }
public String getDept() { return dept; }
public void setDept(String dept) { this.dept = dept; }
public String getPurpose() { return purpose; }
public void setPurpose(String purpose) { this.purpose = purpose; }
public String getDeviceDesc() { return deviceDesc; }
public void setDeviceDesc(String deviceDesc) { this.deviceDesc = deviceDesc; }
public String getFootageStart() { return footageStart; }
public void setFootageStart(String footageStart) { this.footageStart = footageStart; }
public String getFootageEnd() { return footageEnd; }
public void setFootageEnd(String footageEnd) { this.footageEnd = footageEnd; }
public String getApplyDate() { return applyDate; }
public void setApplyDate(String applyDate) { this.applyDate = applyDate; }
public String getApprover() { return approver; }
public void setApprover(String approver) { this.approver = approver; }
public String getApprovalComment() { return approvalComment; }
public void setApprovalComment(String approvalComment) { this.approvalComment = approvalComment; }
public Instant getApprovalTime() { return approvalTime; }
public void setApprovalTime(Instant approvalTime) { this.approvalTime = approvalTime; }
public Instant getViewTime() { return viewTime; }
public void setViewTime(Instant viewTime) { this.viewTime = viewTime; }
public String getOperator() { return operator; }
public void setOperator(String operator) { this.operator = operator; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,107 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·绿化养护台账。记录绿植租摆、浇水/施肥/修剪计划、病虫害处理。
* status: 正常 / 待养护 / 病虫害处理中 / 已报废
*/
@Entity
@Table(name = "admin_greenery")
public class AdminGreenery {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 植物名称。 */
private String plantName;
/** 摆放位置(区域/楼层/具体位置)。 */
private String location;
/** 来源(自购/租摆)。 */
private String source;
/** 租摆供应商。 */
private String supplier;
/** 租摆开始日期(YYYY-MM-DD)。 */
private String rentalStart;
/** 租摆到期日期(YYYY-MM-DD)。 */
private String rentalEnd;
/** 浇水频率(如:每天/隔天/每周)。 */
private String waterFreq;
/** 最近浇水日期(YYYY-MM-DD)。 */
private String lastWatered;
/** 施肥频率(如:每月/每季度)。 */
private String fertFreq;
/** 最近施肥日期(YYYY-MM-DD)。 */
private String lastFertilized;
/** 最近修剪日期(YYYY-MM-DD)。 */
private String lastPruned;
/** 病虫害描述。 */
private String pestNote;
/** 病虫害处理记录。 */
private String pestHandleNote;
/** 养护负责人。 */
private String maintainedBy;
/** 状态:正常 / 待养护 / 病虫害处理中 / 已报废。 */
private String status;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getPlantName() { return plantName; }
public void setPlantName(String plantName) { this.plantName = plantName; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
public String getSupplier() { return supplier; }
public void setSupplier(String supplier) { this.supplier = supplier; }
public String getRentalStart() { return rentalStart; }
public void setRentalStart(String rentalStart) { this.rentalStart = rentalStart; }
public String getRentalEnd() { return rentalEnd; }
public void setRentalEnd(String rentalEnd) { this.rentalEnd = rentalEnd; }
public String getWaterFreq() { return waterFreq; }
public void setWaterFreq(String waterFreq) { this.waterFreq = waterFreq; }
public String getLastWatered() { return lastWatered; }
public void setLastWatered(String lastWatered) { this.lastWatered = lastWatered; }
public String getFertFreq() { return fertFreq; }
public void setFertFreq(String fertFreq) { this.fertFreq = fertFreq; }
public String getLastFertilized() { return lastFertilized; }
public void setLastFertilized(String lastFertilized) { this.lastFertilized = lastFertilized; }
public String getLastPruned() { return lastPruned; }
public void setLastPruned(String lastPruned) { this.lastPruned = lastPruned; }
public String getPestNote() { return pestNote; }
public void setPestNote(String pestNote) { this.pestNote = pestNote; }
public String getPestHandleNote() { return pestHandleNote; }
public void setPestHandleNote(String pestHandleNote) { this.pestHandleNote = pestHandleNote; }
public String getMaintainedBy() { return maintainedBy; }
public void setMaintainedBy(String maintainedBy) { this.maintainedBy = maintainedBy; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,92 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·卫生巡检。管理人员巡检办公区/公共区域卫生,打分、拍照上传,不合格项立即整改跟踪。
* status: 待整改 / 整改中 / 已关闭
*/
@Entity
@Table(name = "admin_hygiene_inspection")
public class AdminHygieneInspection {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 巡检人。 */
private String inspector;
/** 巡检区域。 */
private String area;
/** 巡检日期(YYYY-MM-DD)。 */
private String inspectDate;
/** 卫生评分(0-100)。 */
private Integer score;
/** 评级(优秀/良好/一般/不合格)。 */
private String grade;
/** 不合格项描述(可多项,JSON 字符串或换行分隔)。 */
private String issues;
/** 照片(逗号分隔 URL)。 */
private String photos;
/** 整改负责人。 */
private String rectifyBy;
/** 整改截止日期(YYYY-MM-DD)。 */
private String rectifyDeadline;
/** 整改说明。 */
private String rectifyNote;
/** 整改完成日期。 */
private String rectifiedDate;
/** 状态:待整改 / 整改中 / 已关闭。 */
private String status;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getInspector() { return inspector; }
public void setInspector(String inspector) { this.inspector = inspector; }
public String getArea() { return area; }
public void setArea(String area) { this.area = area; }
public String getInspectDate() { return inspectDate; }
public void setInspectDate(String inspectDate) { this.inspectDate = inspectDate; }
public Integer getScore() { return score; }
public void setScore(Integer score) { this.score = score; }
public String getGrade() { return grade; }
public void setGrade(String grade) { this.grade = grade; }
public String getIssues() { return issues; }
public void setIssues(String issues) { this.issues = issues; }
public String getPhotos() { return photos; }
public void setPhotos(String photos) { this.photos = photos; }
public String getRectifyBy() { return rectifyBy; }
public void setRectifyBy(String rectifyBy) { this.rectifyBy = rectifyBy; }
public String getRectifyDeadline() { return rectifyDeadline; }
public void setRectifyDeadline(String rectifyDeadline) { this.rectifyDeadline = rectifyDeadline; }
public String getRectifyNote() { return rectifyNote; }
public void setRectifyNote(String rectifyNote) { this.rectifyNote = rectifyNote; }
public String getRectifiedDate() { return rectifiedDate; }
public void setRectifiedDate(String rectifiedDate) { this.rectifiedDate = rectifiedDate; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,153 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·公文模板台账(需求 §2 日常行政工作优化·专属公文模板管理)。
*
* 支持正式模板库管理:通知/请示/报告/决定/纪要/批复/函/规定等公文类型;
* 模板版本控制(versionNo)、审批流标识(approvalFlowId 关联 WorkflowDef);
* 公文归档路径记录(archivePath)。
*
* status:草稿 / 已发布 / 已归档 / 已废止
*
* 表名 admin_official_doc_tpl,零撞名、无保留字列。
*/
@Entity
@Table(name = "admin_official_doc_tpl")
public class AdminOfficialDocTemplate {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 模板编号(自动生成,如 TPL-20260615-001 */
@Column(name = "tpl_no")
private String tplNo;
/** 模板名称(如:公司通知模板、向上级请示模板) */
private String tplName;
/** 公文类型:通知 / 请示 / 报告 / 决定 / 纪要 / 批复 / 函 / 规定 / 其他 */
@Column(name = "doc_type")
private String docType;
/** 适用场景描述 */
@Column(name = "use_case")
private String useCase;
/** 模板正文内容(含占位符,如 {{受文单位}} {{正文}} {{落款}} */
@Column(name = "body_content", length = 8000)
private String bodyContent;
/** 版本号(如 v1.0 / v2.0 */
@Column(name = "version_no")
private String versionNo;
/** 关联审批流 ID(引用 WorkflowDef 或字符串标识) */
@Column(name = "approval_flow_id")
private String approvalFlowId;
/** 归档路径/编号(公文发布后的归档位置标识) */
@Column(name = "archive_path")
private String archivePath;
/** 制定人 */
@Column(name = "created_by")
private String createdBy;
/** 审核人 */
@Column(name = "reviewed_by")
private String reviewedBy;
/** 发布人 */
@Column(name = "published_by")
private String publishedBy;
/** 发布日期 */
@Column(name = "published_date")
private String publishedDate;
/** 废止日期 */
@Column(name = "obsolete_date")
private String obsoleteDate;
/** 是否需要 AI 辅助起草(0=否,1=是) */
@Column(name = "ai_draft_enabled")
private Integer aiDraftEnabled;
/** 状态:草稿 / 已发布 / 已归档 / 已废止 */
private String status;
/** 备注 */
private String remark;
private Instant createdAt;
private Instant updatedAt;
// --- getters & setters ---
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTplNo() { return tplNo; }
public void setTplNo(String tplNo) { this.tplNo = tplNo; }
public String getTplName() { return tplName; }
public void setTplName(String tplName) { this.tplName = tplName; }
public String getDocType() { return docType; }
public void setDocType(String docType) { this.docType = docType; }
public String getUseCase() { return useCase; }
public void setUseCase(String useCase) { this.useCase = useCase; }
public String getBodyContent() { return bodyContent; }
public void setBodyContent(String bodyContent) { this.bodyContent = bodyContent; }
public String getVersionNo() { return versionNo; }
public void setVersionNo(String versionNo) { this.versionNo = versionNo; }
public String getApprovalFlowId() { return approvalFlowId; }
public void setApprovalFlowId(String approvalFlowId) { this.approvalFlowId = approvalFlowId; }
public String getArchivePath() { return archivePath; }
public void setArchivePath(String archivePath) { this.archivePath = archivePath; }
public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
public String getReviewedBy() { return reviewedBy; }
public void setReviewedBy(String reviewedBy) { this.reviewedBy = reviewedBy; }
public String getPublishedBy() { return publishedBy; }
public void setPublishedBy(String publishedBy) { this.publishedBy = publishedBy; }
public String getPublishedDate() { return publishedDate; }
public void setPublishedDate(String publishedDate) { this.publishedDate = publishedDate; }
public String getObsoleteDate() { return obsoleteDate; }
public void setObsoleteDate(String obsoleteDate) { this.obsoleteDate = obsoleteDate; }
public Integer getAiDraftEnabled() { return aiDraftEnabled; }
public void setAiDraftEnabled(Integer aiDraftEnabled) { this.aiDraftEnabled = aiDraftEnabled; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,78 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·安保巡逻记录。巡逻人员扫码签到,记录巡逻时间、路线,异常情况拍照上报。
*/
@Entity
@Table(name = "admin_patrol_log")
public class AdminPatrolLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 巡逻人员。 */
private String guard;
/** 巡逻日期(YYYY-MM-DD)。 */
private String patrolDate;
/** 班次(白班/夜班/全天)。 */
private String shift;
/** 巡更点 ID(关联 AdminPatrolPoint)。 */
private Long pointId;
/** 巡更点名称(冗余)。 */
private String pointName;
/** 签到时间(ISO 时间戳)。 */
private Instant checkinAt;
/** 是否有异常(true=有异常)。 */
private Boolean hasAnomaly;
/** 异常描述。 */
private String anomalyDesc;
/** 异常照片(逗号分隔 URL)。 */
private String anomalyPhotos;
/** 处理结果。 */
private String handleResult;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getGuard() { return guard; }
public void setGuard(String guard) { this.guard = guard; }
public String getPatrolDate() { return patrolDate; }
public void setPatrolDate(String patrolDate) { this.patrolDate = patrolDate; }
public String getShift() { return shift; }
public void setShift(String shift) { this.shift = shift; }
public Long getPointId() { return pointId; }
public void setPointId(Long pointId) { this.pointId = pointId; }
public String getPointName() { return pointName; }
public void setPointName(String pointName) { this.pointName = pointName; }
public Instant getCheckinAt() { return checkinAt; }
public void setCheckinAt(Instant checkinAt) { this.checkinAt = checkinAt; }
public Boolean getHasAnomaly() { return hasAnomaly; }
public void setHasAnomaly(Boolean hasAnomaly) { this.hasAnomaly = hasAnomaly; }
public String getAnomalyDesc() { return anomalyDesc; }
public void setAnomalyDesc(String anomalyDesc) { this.anomalyDesc = anomalyDesc; }
public String getAnomalyPhotos() { return anomalyPhotos; }
public void setAnomalyPhotos(String anomalyPhotos) { this.anomalyPhotos = anomalyPhotos; }
public String getHandleResult() { return handleResult; }
public void setHandleResult(String handleResult) { this.handleResult = handleResult; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,53 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·安保巡更点台账。电子巡更点二维码/NFC 设置,供巡逻签到使用。
*/
@Entity
@Table(name = "admin_patrol_point")
public class AdminPatrolPoint {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 巡更点名称。 */
private String name;
/** 位置描述(楼层/区域)。 */
private String location;
/** 标识码(二维码/NFC 内容,通常是 UUID 或编码)。 */
private String code;
/** 标识类型(QR/NFC)。 */
private String codeType;
/** 状态(启用/停用)。 */
private String status;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getCodeType() { return codeType; }
public void setCodeType(String codeType) { this.codeType = codeType; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,113 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·非生产性物资采购申请(物资采购链)。
* 各部门在线提交采购申请 → 系统汇总生成采购计划 → 审批 → 审批通过后入库。
* status: 草稿 → 待审批 → 已批准 → 采购中 → 已入库 / 已驳回
*/
@Entity
@Table(name = "admin_procurement_request")
public class AdminProcurementRequest {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 申请单号,自动生成。 */
private String code;
/** 申请部门。 */
private String dept;
/** 申请人。 */
private String applicant;
/** 物资名称(可关联 AdminSupply.name)。 */
private String supplyName;
/** 物资品类。 */
private String category;
/** 规格/型号。 */
private String spec;
/** 单位。 */
private String unit;
/** 申请数量。 */
private Double qty;
/** 预算金额(元)。 */
private java.math.BigDecimal budgetAmount;
/** 用途说明。 */
private String purpose;
/** 期望到货日期(YYYY-MM-DD)。 */
private String expectDate;
/** 状态:草稿 / 待审批 / 已批准 / 采购中 / 已入库 / 已驳回。 */
private String status;
/** 审批意见。 */
private String approveOpinion;
/** 审批人。 */
private String approver;
/** 审批时间。 */
private Instant approvedAt;
/** 关联的入库批次号(审批通过入库后回填)。 */
private String batchNo;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getDept() { return dept; }
public void setDept(String dept) { this.dept = dept; }
public String getApplicant() { return applicant; }
public void setApplicant(String applicant) { this.applicant = applicant; }
public String getSupplyName() { return supplyName; }
public void setSupplyName(String supplyName) { this.supplyName = supplyName; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getSpec() { return spec; }
public void setSpec(String spec) { this.spec = spec; }
public String getUnit() { return unit; }
public void setUnit(String unit) { this.unit = unit; }
public Double getQty() { return qty; }
public void setQty(Double qty) { this.qty = qty; }
public java.math.BigDecimal getBudgetAmount() { return budgetAmount; }
public void setBudgetAmount(java.math.BigDecimal budgetAmount) { this.budgetAmount = budgetAmount; }
public String getPurpose() { return purpose; }
public void setPurpose(String purpose) { this.purpose = purpose; }
public String getExpectDate() { return expectDate; }
public void setExpectDate(String expectDate) { this.expectDate = expectDate; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getApproveOpinion() { return approveOpinion; }
public void setApproveOpinion(String approveOpinion) { this.approveOpinion = approveOpinion; }
public String getApprover() { return approver; }
public void setApprover(String approver) { this.approver = approver; }
public Instant getApprovedAt() { return approvedAt; }
public void setApprovedAt(Instant approvedAt) { this.approvedAt = approvedAt; }
public String getBatchNo() { return batchNo; }
public void setBatchNo(String batchNo) { this.batchNo = batchNo; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,114 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·培训题库题目(培训学习AI题库补全缺口)。
* 支持按岗位/课程管理题目;可由AI生成后人工复核入库,
* 也可手动录入。类型:单选/多选/判断/简答。
* status: 待审 / 已启用 / 已停用
*/
@Entity
@Table(name = "admin_quiz_question")
public class AdminQuizQuestion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联培训课程ID(可选,为null表示通用题库)。 */
private Long courseId;
/** 适用岗位(逗号分隔;"全员"表示所有岗位)。 */
private String targetPosition;
/** 题目类型:单选/多选/判断/简答。 */
@Column(name = "question_type")
private String questionType;
/** 题目内容。 */
@Column(name = "question_text", length = 2000)
private String questionText;
/** 选项A。 */
@Column(name = "option_a")
private String optionA;
/** 选项B。 */
@Column(name = "option_b")
private String optionB;
/** 选项C。 */
@Column(name = "option_c")
private String optionC;
/** 选项D。 */
@Column(name = "option_d")
private String optionD;
/** 正确答案(单选:A/B/C/D,多选:AB等,判断:true/false,简答:参考答案文本)。 */
@Column(name = "correct_answer", length = 2000)
private String correctAnswer;
/** 题目解析/解释。 */
@Column(name = "explanation", length = 2000)
private String explanation;
/** 难度:简单/中等/困难。 */
private String difficulty;
/** 分值。 */
private Integer score;
/** 来源:手动录入 / AI生成。 */
@Column(name = "question_source")
private String questionSource;
/** 状态:待审 / 已启用 / 已停用。 */
private String status;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getCourseId() { return courseId; }
public void setCourseId(Long courseId) { this.courseId = courseId; }
public String getTargetPosition() { return targetPosition; }
public void setTargetPosition(String targetPosition) { this.targetPosition = targetPosition; }
public String getQuestionType() { return questionType; }
public void setQuestionType(String questionType) { this.questionType = questionType; }
public String getQuestionText() { return questionText; }
public void setQuestionText(String questionText) { this.questionText = questionText; }
public String getOptionA() { return optionA; }
public void setOptionA(String optionA) { this.optionA = optionA; }
public String getOptionB() { return optionB; }
public void setOptionB(String optionB) { this.optionB = optionB; }
public String getOptionC() { return optionC; }
public void setOptionC(String optionC) { this.optionC = optionC; }
public String getOptionD() { return optionD; }
public void setOptionD(String optionD) { this.optionD = optionD; }
public String getCorrectAnswer() { return correctAnswer; }
public void setCorrectAnswer(String correctAnswer) { this.correctAnswer = correctAnswer; }
public String getExplanation() { return explanation; }
public void setExplanation(String explanation) { this.explanation = explanation; }
public String getDifficulty() { return difficulty; }
public void setDifficulty(String difficulty) { this.difficulty = difficulty; }
public Integer getScore() { return score; }
public void setScore(Integer score) { this.score = score; }
public String getQuestionSource() { return questionSource; }
public void setQuestionSource(String questionSource) { this.questionSource = questionSource; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,104 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 物资领用定额(行政/办公室·领用定额控制)。按岗位/部门 × 物资设置每月领用上限
* (如每人每月 2 支笔)。领用申请时按 (post|dept, supplyId) 匹配定额,结合本月累计已领量
* 判断是否超定额。monthlyLimit 为月度上限(数量口径,double)。
*/
@Entity
@Table(name = "admin_quota")
public class AdminQuota {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long supplyId;
private String supplyName;
/** 适用岗位(与部门二选一或叠加;空表示不限岗位)。 */
private String post;
/** 适用部门(空表示不限部门)。 */
private String dept;
/** 月度领用上限(数量口径)。 */
private Double monthlyLimit = 0d;
private String remark;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getSupplyId() {
return supplyId;
}
public void setSupplyId(Long supplyId) {
this.supplyId = supplyId;
}
public String getSupplyName() {
return supplyName;
}
public void setSupplyName(String supplyName) {
this.supplyName = supplyName;
}
public String getPost() {
return post;
}
public void setPost(String post) {
this.post = post;
}
public String getDept() {
return dept;
}
public void setDept(String dept) {
this.dept = dept;
}
public Double getMonthlyLimit() {
return monthlyLimit;
}
public void setMonthlyLimit(Double monthlyLimit) {
this.monthlyLimit = monthlyLimit;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,159 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 行政/办公室·接待申请。部门申请接待客户/访客(级别/人数/行程/餐饮住宿),
* 后勤部安排车辆/餐饮/会议室/礼品,记录接待费用,分摊至对应部门/项目。
* status: 草稿 / 待审批 / 已批准 / 接待中 / 已完成 / 已驳回
*/
@Entity
@Table(name = "admin_reception")
public class AdminReception {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 接待单号,自动生成。 */
private String code;
/** 申请部门。 */
private String dept;
/** 申请人。 */
private String applicant;
/** 接待对象/客户名称。 */
private String guestName;
/** 接待级别(VIP/一般/内部)。 */
private String level;
/** 接待人数。 */
private Integer guestCount;
/** 接待目的/行程说明。 */
private String purpose;
/** 接待开始日期(YYYY-MM-DD)。 */
private String startDate;
/** 接待结束日期(YYYY-MM-DD)。 */
private String endDate;
/** 是否需要餐饮。 */
private Boolean needDining;
/** 是否需要住宿。 */
private Boolean needAccommodation;
/** 是否需要用车。 */
private Boolean needVehicle;
/** 是否需要礼品。 */
private Boolean needGift;
/** 是否需要会议室。 */
private Boolean needMeetingRoom;
/** 关联会议室预订 ID。 */
private Long meetingId;
/** 备注/特殊要求。 */
private String remark;
/** 安排说明(后勤填写)。 */
private String arrangement;
/** 审批意见。 */
private String approveOpinion;
/** 餐饮费用。 */
private BigDecimal diningCost;
/** 住宿费用。 */
private BigDecimal accommodationCost;
/** 礼品费用。 */
private BigDecimal giftCost;
/** 其他费用。 */
private BigDecimal otherCost;
/** 总费用(自动累计)。 */
private BigDecimal totalCost;
/** 费用归集项目/部门。 */
private String costCenter;
/** 状态:草稿 / 待审批 / 已批准 / 接待中 / 已完成 / 已驳回。 */
private String status;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getDept() { return dept; }
public void setDept(String dept) { this.dept = dept; }
public String getApplicant() { return applicant; }
public void setApplicant(String applicant) { this.applicant = applicant; }
public String getGuestName() { return guestName; }
public void setGuestName(String guestName) { this.guestName = guestName; }
public String getLevel() { return level; }
public void setLevel(String level) { this.level = level; }
public Integer getGuestCount() { return guestCount; }
public void setGuestCount(Integer guestCount) { this.guestCount = guestCount; }
public String getPurpose() { return purpose; }
public void setPurpose(String purpose) { this.purpose = purpose; }
public String getStartDate() { return startDate; }
public void setStartDate(String startDate) { this.startDate = startDate; }
public String getEndDate() { return endDate; }
public void setEndDate(String endDate) { this.endDate = endDate; }
public Boolean getNeedDining() { return needDining; }
public void setNeedDining(Boolean needDining) { this.needDining = needDining; }
public Boolean getNeedAccommodation() { return needAccommodation; }
public void setNeedAccommodation(Boolean needAccommodation) { this.needAccommodation = needAccommodation; }
public Boolean getNeedVehicle() { return needVehicle; }
public void setNeedVehicle(Boolean needVehicle) { this.needVehicle = needVehicle; }
public Boolean getNeedGift() { return needGift; }
public void setNeedGift(Boolean needGift) { this.needGift = needGift; }
public Boolean getNeedMeetingRoom() { return needMeetingRoom; }
public void setNeedMeetingRoom(Boolean needMeetingRoom) { this.needMeetingRoom = needMeetingRoom; }
public Long getMeetingId() { return meetingId; }
public void setMeetingId(Long meetingId) { this.meetingId = meetingId; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public String getArrangement() { return arrangement; }
public void setArrangement(String arrangement) { this.arrangement = arrangement; }
public String getApproveOpinion() { return approveOpinion; }
public void setApproveOpinion(String approveOpinion) { this.approveOpinion = approveOpinion; }
public BigDecimal getDiningCost() { return diningCost; }
public void setDiningCost(BigDecimal diningCost) { this.diningCost = diningCost; }
public BigDecimal getAccommodationCost() { return accommodationCost; }
public void setAccommodationCost(BigDecimal accommodationCost) { this.accommodationCost = accommodationCost; }
public BigDecimal getGiftCost() { return giftCost; }
public void setGiftCost(BigDecimal giftCost) { this.giftCost = giftCost; }
public BigDecimal getOtherCost() { return otherCost; }
public void setOtherCost(BigDecimal otherCost) { this.otherCost = otherCost; }
public BigDecimal getTotalCost() { return totalCost; }
public void setTotalCost(BigDecimal totalCost) { this.totalCost = totalCost; }
public String getCostCenter() { return costCenter; }
public void setCostCenter(String costCenter) { this.costCenter = costCenter; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,112 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·周期会议预订规则。
* 存储周期规则(每天/每周N/每月第N天),由调度 Job 自动展开为具体的 Meeting 记录。
* status: 生效中 / 已暂停 / 已结束
*/
@Entity
@Table(name = "admin_recurring_booking")
public class AdminRecurringBooking {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 周期规则名称(如"周一例会")。 */
@Column(nullable = false)
private String ruleName;
/** 会议主题模板(实际生成时追加日期)。 */
@Column(nullable = false)
private String subject;
/** 会议室 ID。 */
private Long roomId;
/** 组织者。 */
private String organizer;
/** 参会人(逗号分隔)。 */
@Column(length = 2000)
private String attendees;
/**
* 周期类型:DAILY(每天)/ WEEKLY(每周,需指定 weekDays/ MONTHLY(每月,需指定 monthDay)。
*/
@Column(nullable = false)
private String recurType;
/**
* WEEKLY 时:1-7 逗号分隔(1=周一,7=周日),如 "1,3,5" 表示周一三五。
* MONTHLY 时:月中的第几天(1-28),如 "15" 表示每月15日。
*/
private String recurParam;
/** 每次会议开始时间(HH:mm),如 "09:00"。 */
@Column(nullable = false)
private String startTime;
/** 每次会议结束时间(HH:mm),如 "10:00"。 */
@Column(nullable = false)
private String endTime;
/** 周期生效开始日期(yyyy-MM-dd)。 */
@Column(nullable = false)
private String effectFrom;
/** 周期结束日期(yyyy-MM-ddnull=长期有效)。 */
private String effectTo;
/** 最后一次自动展开到的日期(yyyy-MM-dd)。 */
private String lastExpandedDate;
/** 状态:生效中 / 已暂停 / 已结束。 */
@Column(nullable = false)
private String bookingStatus;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getRuleName() { return ruleName; }
public void setRuleName(String ruleName) { this.ruleName = ruleName; }
public String getSubject() { return subject; }
public void setSubject(String subject) { this.subject = subject; }
public Long getRoomId() { return roomId; }
public void setRoomId(Long roomId) { this.roomId = roomId; }
public String getOrganizer() { return organizer; }
public void setOrganizer(String organizer) { this.organizer = organizer; }
public String getAttendees() { return attendees; }
public void setAttendees(String attendees) { this.attendees = attendees; }
public String getRecurType() { return recurType; }
public void setRecurType(String recurType) { this.recurType = recurType; }
public String getRecurParam() { return recurParam; }
public void setRecurParam(String recurParam) { this.recurParam = recurParam; }
public String getStartTime() { return startTime; }
public void setStartTime(String startTime) { this.startTime = startTime; }
public String getEndTime() { return endTime; }
public void setEndTime(String endTime) { this.endTime = endTime; }
public String getEffectFrom() { return effectFrom; }
public void setEffectFrom(String effectFrom) { this.effectFrom = effectFrom; }
public String getEffectTo() { return effectTo; }
public void setEffectTo(String effectTo) { this.effectTo = effectTo; }
public String getLastExpandedDate() { return lastExpandedDate; }
public void setLastExpandedDate(String lastExpandedDate) { this.lastExpandedDate = lastExpandedDate; }
public String getBookingStatus() { return bookingStatus; }
public void setBookingStatus(String bookingStatus) { this.bookingStatus = bookingStatus; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,225 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 物资领用申请单(行政/办公室·领用定额控制)。员工按岗位/部门提交领用申请,
* 系统按 {@link AdminQuota} 自动判断是否超定额:未超 → 直接「已通过」;
* 超定额 → 进入「待审批」需特别审批,审批通过后才可出库。
* status 流转:待审批 → 已通过 / 已驳回 → 已出库。overQuota 标记本单是否超定额。
* qty 为申请数量(数量口径,double)。
*/
@Entity
@Table(name = "admin_requisition")
public class AdminRequisition {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
private Long supplyId;
private String supplyName;
private String unit;
/** 申请数量。 */
private Double qty = 0d;
/** 申请人。 */
private String applicant;
/** 领用部门(费用分摊归集维度)。 */
private String dept;
/** 岗位(定额按岗位/部门设置)。 */
private String post;
/** 用途。 */
private String purpose;
/** 本单是否超定额。 */
private Boolean overQuota = Boolean.FALSE;
/** 本月该领用人对该物资的累计已领量(提交时快照,便于审批参考)。 */
private Double monthUsed = 0d;
/** 适用定额(提交时快照)。 */
private Double quotaLimit = 0d;
/** 待审批 / 已通过 / 已驳回 / 已出库。 */
private String status;
/** 审批人。 */
private String approver;
/** 审批意见。 */
private String approveComment;
/** 出库流水 id(出库后回填)。 */
private Long stockMoveId;
private String applyDate;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Long getSupplyId() {
return supplyId;
}
public void setSupplyId(Long supplyId) {
this.supplyId = supplyId;
}
public String getSupplyName() {
return supplyName;
}
public void setSupplyName(String supplyName) {
this.supplyName = supplyName;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public Double getQty() {
return qty;
}
public void setQty(Double qty) {
this.qty = qty;
}
public String getApplicant() {
return applicant;
}
public void setApplicant(String applicant) {
this.applicant = applicant;
}
public String getDept() {
return dept;
}
public void setDept(String dept) {
this.dept = dept;
}
public String getPost() {
return post;
}
public void setPost(String post) {
this.post = post;
}
public String getPurpose() {
return purpose;
}
public void setPurpose(String purpose) {
this.purpose = purpose;
}
public Boolean getOverQuota() {
return overQuota;
}
public void setOverQuota(Boolean overQuota) {
this.overQuota = overQuota;
}
public Double getMonthUsed() {
return monthUsed;
}
public void setMonthUsed(Double monthUsed) {
this.monthUsed = monthUsed;
}
public Double getQuotaLimit() {
return quotaLimit;
}
public void setQuotaLimit(Double quotaLimit) {
this.quotaLimit = quotaLimit;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getApprover() {
return approver;
}
public void setApprover(String approver) {
this.approver = approver;
}
public String getApproveComment() {
return approveComment;
}
public void setApproveComment(String approveComment) {
this.approveComment = approveComment;
}
public Long getStockMoveId() {
return stockMoveId;
}
public void setStockMoveId(Long stockMoveId) {
this.stockMoveId = stockMoveId;
}
public String getApplyDate() {
return applyDate;
}
public void setApplyDate(String applyDate) {
this.applyDate = applyDate;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,102 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·安保事件(安保事件台账)。记录偷盗、打架、消防等安全事件的处理过程闭环。
* status: 已上报 / 处理中 / 已结案
*/
@Entity
@Table(name = "admin_security_event")
public class AdminSecurityEvent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 事件编号,自动生成。 */
private String code;
/** 事件类型(盗窃/斗殴/消防/违规/其他)。 */
private String eventType;
/** 发生时间(YYYY-MM-DD HH:mm)。 */
private String eventTime;
/** 发生地点。 */
private String location;
/** 事件描述。 */
private String description;
/** 当事人(逗号分隔多人)。 */
private String parties;
/** 照片/证据(逗号分隔 URL)。 */
private String evidencePhotos;
/** 上报人。 */
private String reporter;
/** 处理人。 */
private String handler;
/** 处理措施。 */
private String handleMeasures;
/** 是否上报公安(true=已报警)。 */
private Boolean policeReported;
/** 结案说明。 */
private String closureNote;
/** 结案日期(YYYY-MM-DD)。 */
private String closureDate;
/** 状态:已上报 / 处理中 / 已结案。 */
private String status;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getEventType() { return eventType; }
public void setEventType(String eventType) { this.eventType = eventType; }
public String getEventTime() { return eventTime; }
public void setEventTime(String eventTime) { this.eventTime = eventTime; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getParties() { return parties; }
public void setParties(String parties) { this.parties = parties; }
public String getEvidencePhotos() { return evidencePhotos; }
public void setEvidencePhotos(String evidencePhotos) { this.evidencePhotos = evidencePhotos; }
public String getReporter() { return reporter; }
public void setReporter(String reporter) { this.reporter = reporter; }
public String getHandler() { return handler; }
public void setHandler(String handler) { this.handler = handler; }
public String getHandleMeasures() { return handleMeasures; }
public void setHandleMeasures(String handleMeasures) { this.handleMeasures = handleMeasures; }
public Boolean getPoliceReported() { return policeReported; }
public void setPoliceReported(Boolean policeReported) { this.policeReported = policeReported; }
public String getClosureNote() { return closureNote; }
public void setClosureNote(String closureNote) { this.closureNote = closureNote; }
public String getClosureDate() { return closureDate; }
public void setClosureDate(String closureDate) { this.closureDate = closureDate; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,97 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·通勤班车台账。记录班车路线、司机、车辆、排班计划。
* status: 运营中 / 暂停 / 停运
*/
@Entity
@Table(name = "admin_shuttle_bus")
public class AdminShuttleBus {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 路线名称。 */
private String routeName;
/** 起点站。 */
private String startStop;
/** 终点站。 */
private String endStop;
/** 途经站点(JSON 数组字符串)。 */
private String stops;
/** 发车时间(如 07:30)。 */
private String departureTime;
/** 返程时间(如 18:00)。 */
private String returnTime;
/** 关联车牌(来自 AdminVehicle)。 */
private String vehiclePlate;
/** 司机姓名。 */
private String driver;
/** 座位数。 */
private Integer seats;
/** 当前预约人数。 */
private Integer bookedCount;
/** 运行日(工作日/周一至六/每天/自定义)。 */
private String runDays;
/** 状态:运营中 / 暂停 / 停运。 */
private String status;
/** 备注。 */
private String remark;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getRouteName() { return routeName; }
public void setRouteName(String routeName) { this.routeName = routeName; }
public String getStartStop() { return startStop; }
public void setStartStop(String startStop) { this.startStop = startStop; }
public String getEndStop() { return endStop; }
public void setEndStop(String endStop) { this.endStop = endStop; }
public String getStops() { return stops; }
public void setStops(String stops) { this.stops = stops; }
public String getDepartureTime() { return departureTime; }
public void setDepartureTime(String departureTime) { this.departureTime = departureTime; }
public String getReturnTime() { return returnTime; }
public void setReturnTime(String returnTime) { this.returnTime = returnTime; }
public String getVehiclePlate() { return vehiclePlate; }
public void setVehiclePlate(String vehiclePlate) { this.vehiclePlate = vehiclePlate; }
public String getDriver() { return driver; }
public void setDriver(String driver) { this.driver = driver; }
public Integer getSeats() { return seats; }
public void setSeats(Integer seats) { this.seats = seats; }
public Integer getBookedCount() { return bookedCount; }
public void setBookedCount(Integer bookedCount) { this.bookedCount = bookedCount; }
public String getRunDays() { return runDays; }
public void setRunDays(String runDays) { this.runDays = runDays; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,64 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·通勤班车乘客预约(乘客统计)。
* status: 已预约 / 已乘坐 / 已取消
*/
@Entity
@Table(name = "admin_shuttle_passenger")
public class AdminShuttlePassenger {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联班车 ID。 */
private Long shuttleId;
/** 乘客姓名。 */
private String passenger;
/** 乘客部门。 */
private String dept;
/** 乘坐日期(YYYY-MM-DD)。 */
private String rideDate;
/** 上下行(去/回)。 */
private String direction;
/** 上车站点。 */
private String boardStop;
/** 状态:已预约 / 已乘坐 / 已取消。 */
private String status;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getShuttleId() { return shuttleId; }
public void setShuttleId(Long shuttleId) { this.shuttleId = shuttleId; }
public String getPassenger() { return passenger; }
public void setPassenger(String passenger) { this.passenger = passenger; }
public String getDept() { return dept; }
public void setDept(String dept) { this.dept = dept; }
public String getRideDate() { return rideDate; }
public void setRideDate(String rideDate) { this.rideDate = rideDate; }
public String getDirection() { return direction; }
public void setDirection(String direction) { this.direction = direction; }
public String getBoardStop() { return boardStop; }
public void setBoardStop(String boardStop) { this.boardStop = boardStop; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,204 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 物资库存流水(一次入库或一次出库领用)。direction 为 入库 / 出库。
* 入库登记数量、金额、供应商;出库领用登记领用部门 / 项目 / 领用人,用于费用分摊归集。
* 每条流水在控制器内级联调整 {@link AdminSupply#getOnHand()}(入库 +qty,出库 -qty)。
* qty 为数量口径(double),amount 为金额口径(BigDecimal,入库总金额)。
*/
@Entity
@Table(name = "admin_stock_move")
public class AdminStockMove {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long supplyId;
/** 冗余物资编号/名称,便于流水列表展示。 */
private String supplyCode;
private String supplyName;
/** 入库 / 出库。 */
private String direction;
/** 数量(数量口径)。 */
private Double qty = 0d;
/** 入库总金额(出库时一般为 0)。 */
private BigDecimal amount = BigDecimal.ZERO;
/** 入库供应商。 */
private String supplier;
/** 出库领用部门(费用分摊归集维度)。 */
private String dept;
/** 出库归集项目(可空)。 */
private String project;
/** 领用人 / 经办人。 */
private String handler;
/** 批次/有效期(批次管理物资)。 */
private String batchNo;
private String expireDate;
/** 发生日期(YYYY-MM-DD),用于月度分摊归集。 */
private String moveDate;
/** 关联领用申请单 id(由超定额审批通过的领用单出库时回填)。 */
private Long requisitionId;
private String remark;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getSupplyId() {
return supplyId;
}
public void setSupplyId(Long supplyId) {
this.supplyId = supplyId;
}
public String getSupplyCode() {
return supplyCode;
}
public void setSupplyCode(String supplyCode) {
this.supplyCode = supplyCode;
}
public String getSupplyName() {
return supplyName;
}
public void setSupplyName(String supplyName) {
this.supplyName = supplyName;
}
public String getDirection() {
return direction;
}
public void setDirection(String direction) {
this.direction = direction;
}
public Double getQty() {
return qty;
}
public void setQty(Double qty) {
this.qty = qty;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getSupplier() {
return supplier;
}
public void setSupplier(String supplier) {
this.supplier = supplier;
}
public String getDept() {
return dept;
}
public void setDept(String dept) {
this.dept = dept;
}
public String getProject() {
return project;
}
public void setProject(String project) {
this.project = project;
}
public String getHandler() {
return handler;
}
public void setHandler(String handler) {
this.handler = handler;
}
public String getBatchNo() {
return batchNo;
}
public void setBatchNo(String batchNo) {
this.batchNo = batchNo;
}
public String getExpireDate() {
return expireDate;
}
public void setExpireDate(String expireDate) {
this.expireDate = expireDate;
}
public String getMoveDate() {
return moveDate;
}
public void setMoveDate(String moveDate) {
this.moveDate = moveDate;
}
public Long getRequisitionId() {
return requisitionId;
}
public void setRequisitionId(Long requisitionId) {
this.requisitionId = requisitionId;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,164 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 非生产性物资档案(行政/办公室)。一条 AdminSupply 代表一种可领用的物资:
* category 为 办公用品 / 劳保用品 / 清洁用品 / 低值易耗品 之一;记录规格、单位、参考单价、
* 安全库存与当前结存量 onHand。onHand 由入库 / 出库领用({@link AdminStockMove})在
* 控制器内级联自动累减,低于 safetyStock 触发低库存预警。物资数量口径为 double(数量非金额),
* 参考单价 refPrice 为金额(BigDecimal)。
*/
@Entity
@Table(name = "admin_supply")
public class AdminSupply {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
/** 物资名称。 */
private String name;
/** 分类:办公用品 / 劳保用品 / 清洁用品 / 低值易耗品。 */
private String category;
/** 规格(如 A4 70g、9 号牛皮纸袋)。 */
private String spec;
/** 计量单位(支 / 包 / 个 / 瓶)。 */
private String unit;
/** 参考单价(金额)。 */
private BigDecimal refPrice = BigDecimal.ZERO;
/** 安全库存(数量口径)。低于此值触发低库存预警。 */
private Double safetyStock = 0d;
/** 当前结存量(数量口径),由入库/出库级联维护。 */
private Double onHand = 0d;
/** 是否批次管理(如清洁剂有效期)。 */
private Boolean batchManaged = Boolean.FALSE;
/** 默认归集部门(领用未指定时回退)。 */
private String defaultDept;
/** 启用 / 停用。 */
private String status;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getSpec() {
return spec;
}
public void setSpec(String spec) {
this.spec = spec;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public BigDecimal getRefPrice() {
return refPrice;
}
public void setRefPrice(BigDecimal refPrice) {
this.refPrice = refPrice;
}
public Double getSafetyStock() {
return safetyStock;
}
public void setSafetyStock(Double safetyStock) {
this.safetyStock = safetyStock;
}
public Double getOnHand() {
return onHand;
}
public void setOnHand(Double onHand) {
this.onHand = onHand;
}
public Boolean getBatchManaged() {
return batchManaged;
}
public void setBatchManaged(Boolean batchManaged) {
this.batchManaged = batchManaged;
}
public String getDefaultDept() {
return defaultDept;
}
public void setDefaultDept(String defaultDept) {
this.defaultDept = defaultDept;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,118 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·培训课程(培训学习 PARTIAL 缺口补全)。
* 支持岗位定制化学习路径:按目标岗位设置课程,学员按岗位自动匹配。
* status: 计划中 / 报名中 / 进行中 / 已完成 / 已取消
*/
@Entity
@Table(name = "admin_training_course")
public class AdminTrainingCourse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 课程编号(自动生成)。 */
private String courseCode;
/** 课程名称。 */
private String title;
/** 培训类型(新员工入职/岗位技能/安全规范/管理提升/法律合规/其他)。 */
private String trainingType;
/** 目标岗位(适用岗位,逗号分隔;"全员"表示所有人)。 */
private String targetPosition;
/** 目标部门(逗号分隔;"全部"表示所有部门)。 */
private String targetDept;
/** 讲师/培训师。 */
private String lecturer;
/** 培训场地/方式(线下+地点 或 线上平台名)。 */
private String venue;
/** 培训日期(YYYY-MM-DD)。 */
private String trainDate;
/** 培训时长(小时)。 */
private Double durationHours;
/** 报名截止日期(YYYY-MM-DD)。 */
private String enrollDeadline;
/** 学习材料描述/链接(逗号分隔多份)。 */
private String materials;
/** 考核方式(笔试/线上答题/问卷/无)。 */
private String assessMethod;
/** 合格分数线(0-100,0表示无需考核)。 */
private Integer passScore;
/** 参训人数上限(0=不限)。 */
private Integer capacityLimit;
/** 已报名人数(统计字段,随报名更新)。 */
private Integer enrolledCount;
/** 培训小结(完成后填写)。 */
private String summary;
/** 状态:计划中 / 报名中 / 进行中 / 已完成 / 已取消。 */
private String status;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCourseCode() { return courseCode; }
public void setCourseCode(String courseCode) { this.courseCode = courseCode; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getTrainingType() { return trainingType; }
public void setTrainingType(String trainingType) { this.trainingType = trainingType; }
public String getTargetPosition() { return targetPosition; }
public void setTargetPosition(String targetPosition) { this.targetPosition = targetPosition; }
public String getTargetDept() { return targetDept; }
public void setTargetDept(String targetDept) { this.targetDept = targetDept; }
public String getLecturer() { return lecturer; }
public void setLecturer(String lecturer) { this.lecturer = lecturer; }
public String getVenue() { return venue; }
public void setVenue(String venue) { this.venue = venue; }
public String getTrainDate() { return trainDate; }
public void setTrainDate(String trainDate) { this.trainDate = trainDate; }
public Double getDurationHours() { return durationHours; }
public void setDurationHours(Double durationHours) { this.durationHours = durationHours; }
public String getEnrollDeadline() { return enrollDeadline; }
public void setEnrollDeadline(String enrollDeadline) { this.enrollDeadline = enrollDeadline; }
public String getMaterials() { return materials; }
public void setMaterials(String materials) { this.materials = materials; }
public String getAssessMethod() { return assessMethod; }
public void setAssessMethod(String assessMethod) { this.assessMethod = assessMethod; }
public Integer getPassScore() { return passScore; }
public void setPassScore(Integer passScore) { this.passScore = passScore; }
public Integer getCapacityLimit() { return capacityLimit; }
public void setCapacityLimit(Integer capacityLimit) { this.capacityLimit = capacityLimit; }
public Integer getEnrolledCount() { return enrolledCount; }
public void setEnrolledCount(Integer enrolledCount) { this.enrolledCount = enrolledCount; }
public String getSummary() { return summary; }
public void setSummary(String summary) { this.summary = summary; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,95 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 行政/办公室·培训报名与考核明细(培训学习 PARTIAL 缺口补全)。
* 一条记录 = 一名学员对一次培训课程的报名+签到+考核状态。
* status: 已报名 / 已签到 / 已考核 / 未通过 / 已通过
*/
@Entity
@Table(name = "admin_training_enrollment")
public class AdminTrainingEnrollment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联培训课程 ID。 */
private Long courseId;
/** 学员姓名。 */
private String traineeName;
/** 学员工号。 */
private String traineeNo;
/** 学员部门。 */
private String dept;
/** 学员岗位。 */
private String position;
/** 报名日期(YYYY-MM-DD)。 */
private String enrollDate;
/** 是否签到出席。 */
private boolean signedIn;
/** 签到时间。 */
private Instant signInTime;
/** 学习进度(0-100,线上课程用;线下课统一0或100)。 */
private Integer progressPct;
/** 考核得分。 */
private Integer score;
/** 是否通过(按课程及格分自动判定)。 */
private boolean passed;
/** 状态:已报名 / 已签到 / 已考核 / 未通过 / 已通过。 */
private String status;
/** 备注。 */
private String remark;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getCourseId() { return courseId; }
public void setCourseId(Long courseId) { this.courseId = courseId; }
public String getTraineeName() { return traineeName; }
public void setTraineeName(String traineeName) { this.traineeName = traineeName; }
public String getTraineeNo() { return traineeNo; }
public void setTraineeNo(String traineeNo) { this.traineeNo = traineeNo; }
public String getDept() { return dept; }
public void setDept(String dept) { this.dept = dept; }
public String getPosition() { return position; }
public void setPosition(String position) { this.position = position; }
public String getEnrollDate() { return enrollDate; }
public void setEnrollDate(String enrollDate) { this.enrollDate = enrollDate; }
public boolean isSignedIn() { return signedIn; }
public void setSignedIn(boolean signedIn) { this.signedIn = signedIn; }
public Instant getSignInTime() { return signInTime; }
public void setSignInTime(Instant signInTime) { this.signInTime = signInTime; }
public Integer getProgressPct() { return progressPct; }
public void setProgressPct(Integer progressPct) { this.progressPct = progressPct; }
public Integer getScore() { return score; }
public void setScore(Integer score) { this.score = score; }
public boolean isPassed() { return passed; }
public void setPassed(boolean passed) { this.passed = passed; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,169 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 车辆档案(行政/办公室·车辆管理)。记录车牌、品牌车型、座位数、购置日期、
* 保险到期 / 年检到期 / 下次保养日期(用于到期分级预警),以及当前里程表读数。
* usage 为 公务 / 通勤 之一;status 为 可用 / 出车中 / 维保 / 停用。
* 用车申请({@link AdminVehicleDispatch})派车时把车置「出车中」,出车记录归还后置「可用」。
*/
@Entity
@Table(name = "admin_vehicle")
public class AdminVehicle {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String plate;
private String brand;
private String model;
/** 用途:公务 / 通勤。 */
private String usage;
private Integer seats;
private String purchaseDate;
/** 保险到期日(YYYY-MM-DD)。 */
private String insuranceDue;
/** 年检到期日(YYYY-MM-DD)。 */
private String inspectionDue;
/** 下次保养日期(YYYY-MM-DD)。 */
private String maintenanceDue;
/** 当前里程表读数(公里)。 */
private Double odometer = 0d;
/** 可用 / 出车中 / 维保 / 停用。 */
private String status;
/** 默认驾驶员 / 责任人。 */
private String driver;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPlate() {
return plate;
}
public void setPlate(String plate) {
this.plate = plate;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getUsage() {
return usage;
}
public void setUsage(String usage) {
this.usage = usage;
}
public Integer getSeats() {
return seats;
}
public void setSeats(Integer seats) {
this.seats = seats;
}
public String getPurchaseDate() {
return purchaseDate;
}
public void setPurchaseDate(String purchaseDate) {
this.purchaseDate = purchaseDate;
}
public String getInsuranceDue() {
return insuranceDue;
}
public void setInsuranceDue(String insuranceDue) {
this.insuranceDue = insuranceDue;
}
public String getInspectionDue() {
return inspectionDue;
}
public void setInspectionDue(String inspectionDue) {
this.inspectionDue = inspectionDue;
}
public String getMaintenanceDue() {
return maintenanceDue;
}
public void setMaintenanceDue(String maintenanceDue) {
this.maintenanceDue = maintenanceDue;
}
public Double getOdometer() {
return odometer;
}
public void setOdometer(Double odometer) {
this.odometer = odometer;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getDriver() {
return driver;
}
public void setDriver(String driver) {
this.driver = driver;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,280 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 用车申请与派车单(行政/办公室·车辆管理)。员工提交用车申请(时间区间、起止点、事由、乘客数),
* 调度员派车——派车时系统按时间区间做冲突检测(同一车辆同时段不可重复派车),紧急用车走绿色通道。
*
* 状态机:待派车 → 已派车 → 已出车 → 已归还 / 已取消。派车占用车辆(车置「出车中」),
* 归还时回填实际里程/油耗/费用并自动算百公里油耗、车辆复位「可用」。
* 费用 tollFee/parkingFee/fuelCost 为金额(BigDecimal)。
*/
@Entity
@Table(name = "admin_vehicle_dispatch")
public class AdminVehicleDispatch {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
/** 申请人。 */
private String applicant;
private String dept;
/** 用车事由。 */
private String reason;
private String fromPlace;
private String toPlace;
/** 用车开始时间(YYYY-MM-DD HH:mm 或 YYYY-MM-DD)。 */
private String startTime;
/** 用车结束时间。 */
private String endTime;
private Integer passengers;
/** 是否紧急用车(绿色通道)。 */
private Boolean urgent = Boolean.FALSE;
/** 派定车辆 id(派车后回填)。 */
private Long vehicleId;
private String vehiclePlate;
/** 指派驾驶员。 */
private String driver;
/** 待派车 / 已派车 / 已出车 / 已归还 / 已取消。 */
private String status;
// ---- 出车记录(归还时回填)----
/** 出车起始里程。 */
private Double startOdometer;
/** 归还里程。 */
private Double endOdometer;
/** 行驶里程(归还时自动算 endOdometer-startOdometer)。 */
private Double mileage = 0d;
/** 油耗(升)。 */
private Double fuelLiters = 0d;
/** 百公里油耗(自动算 fuelLiters/mileage*100)。 */
private Double fuelPer100km = 0d;
private BigDecimal fuelCost = BigDecimal.ZERO;
private BigDecimal tollFee = BigDecimal.ZERO;
private BigDecimal parkingFee = BigDecimal.ZERO;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getApplicant() {
return applicant;
}
public void setApplicant(String applicant) {
this.applicant = applicant;
}
public String getDept() {
return dept;
}
public void setDept(String dept) {
this.dept = dept;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public String getFromPlace() {
return fromPlace;
}
public void setFromPlace(String fromPlace) {
this.fromPlace = fromPlace;
}
public String getToPlace() {
return toPlace;
}
public void setToPlace(String toPlace) {
this.toPlace = toPlace;
}
public String getStartTime() {
return startTime;
}
public void setStartTime(String startTime) {
this.startTime = startTime;
}
public String getEndTime() {
return endTime;
}
public void setEndTime(String endTime) {
this.endTime = endTime;
}
public Integer getPassengers() {
return passengers;
}
public void setPassengers(Integer passengers) {
this.passengers = passengers;
}
public Boolean getUrgent() {
return urgent;
}
public void setUrgent(Boolean urgent) {
this.urgent = urgent;
}
public Long getVehicleId() {
return vehicleId;
}
public void setVehicleId(Long vehicleId) {
this.vehicleId = vehicleId;
}
public String getVehiclePlate() {
return vehiclePlate;
}
public void setVehiclePlate(String vehiclePlate) {
this.vehiclePlate = vehiclePlate;
}
public String getDriver() {
return driver;
}
public void setDriver(String driver) {
this.driver = driver;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Double getStartOdometer() {
return startOdometer;
}
public void setStartOdometer(Double startOdometer) {
this.startOdometer = startOdometer;
}
public Double getEndOdometer() {
return endOdometer;
}
public void setEndOdometer(Double endOdometer) {
this.endOdometer = endOdometer;
}
public Double getMileage() {
return mileage;
}
public void setMileage(Double mileage) {
this.mileage = mileage;
}
public Double getFuelLiters() {
return fuelLiters;
}
public void setFuelLiters(Double fuelLiters) {
this.fuelLiters = fuelLiters;
}
public Double getFuelPer100km() {
return fuelPer100km;
}
public void setFuelPer100km(Double fuelPer100km) {
this.fuelPer100km = fuelPer100km;
}
public BigDecimal getFuelCost() {
return fuelCost;
}
public void setFuelCost(BigDecimal fuelCost) {
this.fuelCost = fuelCost;
}
public BigDecimal getTollFee() {
return tollFee;
}
public void setTollFee(BigDecimal tollFee) {
this.tollFee = tollFee;
}
public BigDecimal getParkingFee() {
return parkingFee;
}
public void setParkingFee(BigDecimal parkingFee) {
this.parkingFee = parkingFee;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,93 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 行政/办公室·车辆违章记录。记录违章时间/地点/类型/罚款,通知责任人(驾驶员)并跟踪处理。
* status: 未处理 / 处理中 / 已缴罚款 / 已销记
*/
@Entity
@Table(name = "admin_vehicle_violation")
public class AdminVehicleViolation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联车辆 ID。 */
private Long vehicleId;
/** 车牌号(冗余,便于展示)。 */
private String plate;
/** 驾驶员(责任人)。 */
private String driver;
/** 违章日期(YYYY-MM-DD)。 */
private String violationDate;
/** 违章地点。 */
private String location;
/** 违章类型(超速/闯红灯/违停/不系安全带/其他)。 */
private String violationType;
/** 违章描述。 */
private String description;
/** 罚款金额(元)。 */
private BigDecimal fine;
/** 扣分数量。 */
private Integer deductPoints;
/** 通知责任人时间。 */
private Instant notifiedAt;
/** 处理说明。 */
private String handleNote;
/** 状态:未处理 / 处理中 / 已缴罚款 / 已销记。 */
private String status;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getVehicleId() { return vehicleId; }
public void setVehicleId(Long vehicleId) { this.vehicleId = vehicleId; }
public String getPlate() { return plate; }
public void setPlate(String plate) { this.plate = plate; }
public String getDriver() { return driver; }
public void setDriver(String driver) { this.driver = driver; }
public String getViolationDate() { return violationDate; }
public void setViolationDate(String violationDate) { this.violationDate = violationDate; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getViolationType() { return violationType; }
public void setViolationType(String violationType) { this.violationType = violationType; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public BigDecimal getFine() { return fine; }
public void setFine(BigDecimal fine) { this.fine = fine; }
public Integer getDeductPoints() { return deductPoints; }
public void setDeductPoints(Integer deductPoints) { this.deductPoints = deductPoints; }
public Instant getNotifiedAt() { return notifiedAt; }
public void setNotifiedAt(Instant notifiedAt) { this.notifiedAt = notifiedAt; }
public String getHandleNote() { return handleNote; }
public void setHandleNote(String handleNote) { this.handleNote = handleNote; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,204 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 访客预约与门禁记录(行政/办公室·安保与门禁)。访客在线预约(姓名、单位、被访人、来访时间)
* → 被访人确认(生成访客通行码 passCode)→ 门卫扫码放行(签到)→ 离开签退。
*
* 状态机:待确认 → 已确认(生成通行码)→ 已入场(签到)→ 已离场 / 已拒绝 / 已过期。
* passCode 仅在确认后生成,用于门卫核验。
*/
@Entity
@Table(name = "admin_visitor")
public class AdminVisitor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
private String visitorName;
/** 访客单位。 */
private String company;
/** 身份证号(登记用)。 */
private String idCard;
private String phone;
/** 被访人。 */
private String host;
/** 被访部门。 */
private String hostDept;
/** 来访事由。 */
private String reason;
/** 预约来访时间(YYYY-MM-DD HH:mm 或 YYYY-MM-DD)。 */
private String visitTime;
private Integer companions;
/** 待确认 / 已确认 / 已入场 / 已离场 / 已拒绝 / 已过期。 */
private String status;
/** 通行码(确认后生成)。 */
private String passCode;
/** 实际签到时间(门卫扫码)。 */
private String checkinTime;
/** 实际签退时间。 */
private String checkoutTime;
/** 确认人/拒绝人备注。 */
private String hostNote;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getVisitorName() {
return visitorName;
}
public void setVisitorName(String visitorName) {
this.visitorName = visitorName;
}
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
public String getIdCard() {
return idCard;
}
public void setIdCard(String idCard) {
this.idCard = idCard;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getHostDept() {
return hostDept;
}
public void setHostDept(String hostDept) {
this.hostDept = hostDept;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public String getVisitTime() {
return visitTime;
}
public void setVisitTime(String visitTime) {
this.visitTime = visitTime;
}
public Integer getCompanions() {
return companions;
}
public void setCompanions(Integer companions) {
this.companions = companions;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getPassCode() {
return passCode;
}
public void setPassCode(String passCode) {
this.passCode = passCode;
}
public String getCheckinTime() {
return checkinTime;
}
public void setCheckinTime(String checkinTime) {
this.checkinTime = checkinTime;
}
public String getCheckoutTime() {
return checkoutTime;
}
public void setCheckoutTime(String checkoutTime) {
this.checkoutTime = checkoutTime;
}
public String getHostNote() {
return hostNote;
}
public void setHostNote(String hostNote) {
this.hostNote = hostNote;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,105 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
import java.time.Instant;
/** News / announcement post. category is one of: 新闻 / 公告. */
@Entity
@Table(name = "announcement")
public class Announcement {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 新闻 / 公告 */
private String category;
private String title;
@Lob
@Column(columnDefinition = "TEXT")
private String content;
private String author;
@Column(name = "is_top")
private boolean top;
private Instant publishedAt;
/** 发布范围(逗号分隔:全体员工 / 各部门)。 */
@Column(length = 1000)
private String publishRange;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public boolean isTop() {
return top;
}
public void setTop(boolean top) {
this.top = top;
}
public Instant getPublishedAt() {
return publishedAt;
}
public void setPublishedAt(Instant publishedAt) {
this.publishedAt = publishedAt;
}
public String getPublishRange() {
return publishRange;
}
public void setPublishRange(String publishRange) {
this.publishRange = publishRange;
}
}
@@ -0,0 +1,199 @@
package com.kaidi.oa.domain;
import java.math.BigDecimal;
import java.time.Instant;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
/**
* 科技年报填报(知识产权部,需求功能12「年报与统计填报辅助」— 补深 LOGIC_GAP)。
*
* 支持多类型年报模板(工程勘察设计年报/经开区统计年报/火炬报表/高企/专精特新等),
* 系统自动从 RdExpense/Patent/RdProject/PersonnelCert 四仓抽取基础数据预填关键指标;
* 填报数据校验:研发费用占比一致性(与项目数匹配),人员工时总量范围校验;
* 支持历年对比:同一 reportType 按 fiscalYear 查近几年对比;
* 状态机:草稿 → 自动预填 → 填报中 → 内部审核 → 已提交 → 已反馈。
*
* 关键指标:rdExpenseAmount(研发费用)、staffRdCount(科技人员数)、patentCount(有效专利数)、
* revenueAmount(营收,手填)、rdExpenseRate(研发投入占比,自动算)。
*/
@Entity
@Table(name = "annual_report")
public class AnnualReport {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 报表编号,系统自动生成(AR-2026-001)。 */
private String code;
/** 报表名称(如"2025年度火炬统计年报")。 */
private String reportName;
/**
* 报表类型:高企认定 / 专精特新 / 火炬报表 / 经开区统计 / 工程勘察设计年报 / 科技计划 / 其他。
*/
private String reportType;
/** 填报对象(科技局 / 统计局 / 税务局 / 高企认定 / 工程学会 等)。 */
private String reportTarget;
/** 填报年度(四位整数)。 */
private Integer fiscalYear;
/** 截止日期(YYYY-MM-DD)。 */
private String deadline;
/** 状态:草稿 / 填报中 / 内部审核 / 已提交 / 已反馈。 */
private String status;
// ---------- 自动抽取并预填的基础指标 ----------
/** 营收金额(手填,单位元)。 */
private BigDecimal revenueAmount = BigDecimal.ZERO;
/** 研发费用(从 RdExpense 自动归集,单位元)。 */
private BigDecimal rdExpenseAmount = BigDecimal.ZERO;
/** 研发投入占比(rdExpenseAmount/revenueAmount,自动算)。 */
private BigDecimal rdExpenseRate = BigDecimal.ZERO;
/** 科技人员数(从 PersonnelCert 自动统计)。 */
private Integer staffRdCount;
/** 企业总人员数(手填或 HR 同步)。 */
private Integer staffTotalCount;
/** 科技人员占比(自动算)。 */
private BigDecimal staffRdRate = BigDecimal.ZERO;
/** 有效专利数(从 Patent status=授权维持 自动统计)。 */
private Integer patentCount;
/** 发明专利数(专利类型=发明,授权维持)。 */
private Integer inventionPatentCount;
/** 研发项目数(从 RdProject 自动统计,按年度)。 */
private Integer rdProjectCount;
/** 科技成果数(从 TechAchievement 自动统计)。 */
private Integer techAchievementCount;
/** 技术合同金额(从 TechContract 自动统计,单位元)。 */
private BigDecimal techContractAmount = BigDecimal.ZERO;
// ---------- 扩展填报字段(模板化,各类年报共享) ----------
/** 关键指标描述(校验摘要/主要指标文字汇总)。 */
@Lob
private String keyIndicatorSummary;
/** 数据一致性校验结果(自动校验后填入,如"研发费用占比符合要求")。 */
@Lob
private String validationResult;
/** 校验是否通过:true 全部通过,false 有警告或错误。 */
private Boolean validationPassed;
/** 备注/填报说明。 */
@Lob
private String remark;
/** 填报负责人。 */
private String owner;
/** 审核意见。 */
private String reviewNote;
private Instant createdAt;
private Instant updatedAt;
// ---------- getters/setters ----------
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getReportName() { return reportName; }
public void setReportName(String reportName) { this.reportName = reportName; }
public String getReportType() { return reportType; }
public void setReportType(String reportType) { this.reportType = reportType; }
public String getReportTarget() { return reportTarget; }
public void setReportTarget(String reportTarget) { this.reportTarget = reportTarget; }
public Integer getFiscalYear() { return fiscalYear; }
public void setFiscalYear(Integer fiscalYear) { this.fiscalYear = fiscalYear; }
public String getDeadline() { return deadline; }
public void setDeadline(String deadline) { this.deadline = deadline; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public BigDecimal getRevenueAmount() { return revenueAmount; }
public void setRevenueAmount(BigDecimal revenueAmount) { this.revenueAmount = revenueAmount; }
public BigDecimal getRdExpenseAmount() { return rdExpenseAmount; }
public void setRdExpenseAmount(BigDecimal rdExpenseAmount) { this.rdExpenseAmount = rdExpenseAmount; }
public BigDecimal getRdExpenseRate() { return rdExpenseRate; }
public void setRdExpenseRate(BigDecimal rdExpenseRate) { this.rdExpenseRate = rdExpenseRate; }
public Integer getStaffRdCount() { return staffRdCount; }
public void setStaffRdCount(Integer staffRdCount) { this.staffRdCount = staffRdCount; }
public Integer getStaffTotalCount() { return staffTotalCount; }
public void setStaffTotalCount(Integer staffTotalCount) { this.staffTotalCount = staffTotalCount; }
public BigDecimal getStaffRdRate() { return staffRdRate; }
public void setStaffRdRate(BigDecimal staffRdRate) { this.staffRdRate = staffRdRate; }
public Integer getPatentCount() { return patentCount; }
public void setPatentCount(Integer patentCount) { this.patentCount = patentCount; }
public Integer getInventionPatentCount() { return inventionPatentCount; }
public void setInventionPatentCount(Integer inventionPatentCount) { this.inventionPatentCount = inventionPatentCount; }
public Integer getRdProjectCount() { return rdProjectCount; }
public void setRdProjectCount(Integer rdProjectCount) { this.rdProjectCount = rdProjectCount; }
public Integer getTechAchievementCount() { return techAchievementCount; }
public void setTechAchievementCount(Integer techAchievementCount) { this.techAchievementCount = techAchievementCount; }
public BigDecimal getTechContractAmount() { return techContractAmount; }
public void setTechContractAmount(BigDecimal techContractAmount) { this.techContractAmount = techContractAmount; }
public String getKeyIndicatorSummary() { return keyIndicatorSummary; }
public void setKeyIndicatorSummary(String keyIndicatorSummary) { this.keyIndicatorSummary = keyIndicatorSummary; }
public String getValidationResult() { return validationResult; }
public void setValidationResult(String validationResult) { this.validationResult = validationResult; }
public Boolean getValidationPassed() { return validationPassed; }
public void setValidationPassed(Boolean validationPassed) { this.validationPassed = validationPassed; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public String getOwner() { return owner; }
public void setOwner(String owner) { this.owner = owner; }
public String getReviewNote() { return reviewNote; }
public void setReviewNote(String reviewNote) { this.reviewNote = reviewNote; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,200 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 应收/应付款项(财务管理·往来核算)。一条 ArApItem 代表一笔往来账:
* arApType 类型(应收 / 应付);partyName 往来单位(客户/供应商);
* relatedRef 关联合同号或发票号;amount 款项金额;writtenOff 已核销;
* unwrittenOff 未核销(= 金额 - 已核销,服务端维护);status 流转
* 未核销 → 部分核销 → 已结清(全核销即「已结清」);dueDate 账期;overdue 是否逾期(按账期)。
*/
@Entity
@Table(name = "ar_ap_item")
public class ArApItem {
public static final String T_AR = "应收";
public static final String T_AP = "应付";
public static final String S_OPEN = "未核销";
public static final String S_PARTIAL = "部分核销";
public static final String S_SETTLED = "已结清";
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
/** 类型:应收 / 应付。 */
private String arApType;
/** 往来单位(客户 / 供应商)。 */
private String partyName;
/** 关联合同号或发票号。 */
private String relatedRef;
/** 关联合同 id(可空)。 */
private Long contractId;
/** 关联发票 id(可空)。 */
private Long invoiceId;
/** 款项金额。 */
private BigDecimal amount = BigDecimal.ZERO;
/** 已核销金额。 */
private BigDecimal writtenOff = BigDecimal.ZERO;
/** 未核销金额(= 金额 - 已核销)。 */
private BigDecimal unwrittenOff = BigDecimal.ZERO;
/** 未核销 / 部分核销 / 已结清。 */
private String status;
private String dueDate;
private String companySubject;
private String owner;
private String remark;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getArApType() {
return arApType;
}
public void setArApType(String arApType) {
this.arApType = arApType;
}
public String getPartyName() {
return partyName;
}
public void setPartyName(String partyName) {
this.partyName = partyName;
}
public String getRelatedRef() {
return relatedRef;
}
public void setRelatedRef(String relatedRef) {
this.relatedRef = relatedRef;
}
public Long getContractId() {
return contractId;
}
public void setContractId(Long contractId) {
this.contractId = contractId;
}
public Long getInvoiceId() {
return invoiceId;
}
public void setInvoiceId(Long invoiceId) {
this.invoiceId = invoiceId;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public BigDecimal getWrittenOff() {
return writtenOff;
}
public void setWrittenOff(BigDecimal writtenOff) {
this.writtenOff = writtenOff;
}
public BigDecimal getUnwrittenOff() {
return unwrittenOff;
}
public void setUnwrittenOff(BigDecimal unwrittenOff) {
this.unwrittenOff = unwrittenOff;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getDueDate() {
return dueDate;
}
public void setDueDate(String dueDate) {
this.dueDate = dueDate;
}
public String getCompanySubject() {
return companySubject;
}
public void setCompanySubject(String companySubject) {
this.companySubject = companySubject;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,186 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* A unified archive / knowledge-base entry. category is one of 项目档案 /
* 合同档案 / 申报成果 / 制度文件 / 会议纪要 / 技术资料; sourceType is one of
* 合同 / 项目 / 申报 / 会议 / 手动. sourceId optionally links the entry back to
* the originating business record. fileSize is in bytes; tags is a
* comma-separated keyword list used for keyword search. accessLevel is one
* of 公开 / 内部 / 机密 and controls visibility; fileUrl is a preview link to
* the underlying file.
*/
@Entity
@Table(name = "archive")
public class Archive {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String category;
private String title;
private String sourceType;
private Long sourceId;
private String fileName;
private String fileType;
private long fileSize;
private String uploader;
private String archiveDate;
private String tags;
private String summary;
private String accessLevel;
private String fileUrl;
/** Optional link to the real stored binary (StoredFile.id) for online preview/download. */
private Long storedFileId;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getSourceType() {
return sourceType;
}
public void setSourceType(String sourceType) {
this.sourceType = sourceType;
}
public Long getSourceId() {
return sourceId;
}
public void setSourceId(Long sourceId) {
this.sourceId = sourceId;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getFileType() {
return fileType;
}
public void setFileType(String fileType) {
this.fileType = fileType;
}
public long getFileSize() {
return fileSize;
}
public void setFileSize(long fileSize) {
this.fileSize = fileSize;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public String getArchiveDate() {
return archiveDate;
}
public void setArchiveDate(String archiveDate) {
this.archiveDate = archiveDate;
}
public String getTags() {
return tags;
}
public void setTags(String tags) {
this.tags = tags;
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
public String getAccessLevel() {
return accessLevel;
}
public void setAccessLevel(String accessLevel) {
this.accessLevel = accessLevel;
}
public String getFileUrl() {
return fileUrl;
}
public void setFileUrl(String fileUrl) {
this.fileUrl = fileUrl;
}
public Long getStoredFileId() {
return storedFileId;
}
public void setStoredFileId(Long storedFileId) {
this.storedFileId = storedFileId;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,132 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 档案访问日志(资料室·Module 6「访问日志」+ Module 7「合规审计支持」)。所有对档案的访问
* (查询/预览/下载/打印/修改/删除/授权变更)均落一条日志,记录操作人/时间/IP/操作类型。
* 补审计「访问日志、权限变更记录、销毁记录维度审计」缺口(满足审计追溯要求)。
*
* <p>action 操作类型:查询 / 预览 / 下载 / 打印 / 修改 / 删除 / 授权变更 / 销毁。
* archiveId 关联档案(可空,分类级操作时为空)。日志只追加不修改,供 Module 7 四维审计报告取数。
*/
@Entity
@Table(name = "archive_access_log")
public class ArchiveAccessLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联档案 id(可空)。 */
private Long archiveId;
/** 档案标题(冗余)。 */
private String archiveTitle;
/** 操作类型:查询 / 预览 / 下载 / 打印 / 修改 / 删除 / 授权变更 / 销毁。 */
private String action;
/** 操作人。 */
private String operator;
/** 操作人部门。 */
private String operatorDept;
/** 来源 IP。 */
private String ip;
/** 操作详情/备注。 */
private String detail;
/** 操作时间(YYYY-MM-DD HH:mm:ss)。 */
private String occurredAt;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getArchiveId() {
return archiveId;
}
public void setArchiveId(Long archiveId) {
this.archiveId = archiveId;
}
public String getArchiveTitle() {
return archiveTitle;
}
public void setArchiveTitle(String archiveTitle) {
this.archiveTitle = archiveTitle;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getOperator() {
return operator;
}
public void setOperator(String operator) {
this.operator = operator;
}
public String getOperatorDept() {
return operatorDept;
}
public void setOperatorDept(String operatorDept) {
this.operatorDept = operatorDept;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
public String getOccurredAt() {
return occurredAt;
}
public void setOccurredAt(String occurredAt) {
this.occurredAt = occurredAt;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,225 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 档案细粒度授权(资料室·Module 6「细粒度权限」)。一条 ArchiveAcl 为「某授权主体」对
* 「某档案 / 某分类(文件夹)」的一组操作位授权(可见/预览/下载/打印/改/删/授权),支持按
* 部门 / 岗位 / 项目组 / 个人动态授权。补审计「细粒度权限无、ArchiveController 仅密级粗粒度」缺口。
*
* <p>授权主体 granteeType:部门 / 岗位 / 项目组 / 个人;granteeKey 为对应的部门名/岗位名/项目组名/人名。
* 授权目标 targetType:档案 / 分类;targetId 为 Archive.id 或 ArchiveCategory.id(分类即文件夹粒度)。
* 七个操作位为独立布尔:canView/canPreview/canDownload/canPrint/canEdit/canDelete/canGrant。
* expireDate 为授权到期日(可空表示长期),过期由查询/看板判定失效。
*/
@Entity
@Table(name = "archive_acl")
public class ArchiveAcl {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 授权主体类型:部门 / 岗位 / 项目组 / 个人。 */
private String granteeType;
/** 授权主体标识(部门名/岗位名/项目组名/人名)。 */
private String granteeKey;
/** 授权目标类型:档案 / 分类。 */
private String targetType;
/** 授权目标 idArchive.id 或 ArchiveCategory.id)。 */
private Long targetId;
/** 冗余目标名(便于列表展示)。 */
private String targetName;
private Boolean canView = Boolean.TRUE;
private Boolean canPreview = Boolean.TRUE;
private Boolean canDownload = Boolean.FALSE;
private Boolean canPrint = Boolean.FALSE;
private Boolean canEdit = Boolean.FALSE;
private Boolean canDelete = Boolean.FALSE;
private Boolean canGrant = Boolean.FALSE;
/** 授权到期日(YYYY-MM-DD,可空=长期)。 */
private String expireDate;
/** 授权人。 */
private String grantedBy;
/** 授权日期。 */
private String grantDate;
/** 是否启用(撤销授权时置 false 而非删除,留痕)。 */
private Boolean enabled = Boolean.TRUE;
private String remark;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getGranteeType() {
return granteeType;
}
public void setGranteeType(String granteeType) {
this.granteeType = granteeType;
}
public String getGranteeKey() {
return granteeKey;
}
public void setGranteeKey(String granteeKey) {
this.granteeKey = granteeKey;
}
public String getTargetType() {
return targetType;
}
public void setTargetType(String targetType) {
this.targetType = targetType;
}
public Long getTargetId() {
return targetId;
}
public void setTargetId(Long targetId) {
this.targetId = targetId;
}
public String getTargetName() {
return targetName;
}
public void setTargetName(String targetName) {
this.targetName = targetName;
}
public Boolean getCanView() {
return canView;
}
public void setCanView(Boolean canView) {
this.canView = canView;
}
public Boolean getCanPreview() {
return canPreview;
}
public void setCanPreview(Boolean canPreview) {
this.canPreview = canPreview;
}
public Boolean getCanDownload() {
return canDownload;
}
public void setCanDownload(Boolean canDownload) {
this.canDownload = canDownload;
}
public Boolean getCanPrint() {
return canPrint;
}
public void setCanPrint(Boolean canPrint) {
this.canPrint = canPrint;
}
public Boolean getCanEdit() {
return canEdit;
}
public void setCanEdit(Boolean canEdit) {
this.canEdit = canEdit;
}
public Boolean getCanDelete() {
return canDelete;
}
public void setCanDelete(Boolean canDelete) {
this.canDelete = canDelete;
}
public Boolean getCanGrant() {
return canGrant;
}
public void setCanGrant(Boolean canGrant) {
this.canGrant = canGrant;
}
public String getExpireDate() {
return expireDate;
}
public void setExpireDate(String expireDate) {
this.expireDate = expireDate;
}
public String getGrantedBy() {
return grantedBy;
}
public void setGrantedBy(String grantedBy) {
this.grantedBy = grantedBy;
}
public String getGrantDate() {
return grantDate;
}
public void setGrantDate(String grantDate) {
this.grantDate = grantDate;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,167 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 资料室电子档案自动备份执行日志。
*
* 每次 @Scheduled 备份调度运行后追加一条记录(成功/失败/跳过),
* 作为「自动备份无真实调度执行」缺口的真实落地证明,
* 替代 ArchiveComplianceController.backup-config 仅返回静态配置的现状。
*
* 状态:运行中 / 成功 / 失败 / 跳过。
* backupType:增量 / 全量。
*/
@Entity
@Table(name = "archive_backup_log")
public class ArchiveBackupLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 备份类型:增量 / 全量。 */
private String backupType;
/** 备份状态:运行中 / 成功 / 失败 / 跳过。 */
private String backupStatus;
/** 触发方式:定时 / 手动。 */
private String triggerMode;
/** 备份开始时间(ISO 字符串,便于 SQLite 存储与前端展示)。 */
private String startedAt;
/** 备份完成时间。 */
private String finishedAt;
/** 耗时秒数(完成后填写)。 */
private Long durationSeconds;
/** 备份文件路径(本地快照路径或 S3 前缀)。 */
private String backupPath;
/** 备份文件大小(字节,-1 表示未知)。 */
private Long fileSizeBytes;
/** 备份档案条数(实际扫描档案总数)。 */
private Long archiveCount;
/** 操作结果详细说明(成功摘要 / 失败原因)。 */
private String resultMsg;
/** 执行节点(本机 hostname,多活场景区分节点)。 */
private String executorNode;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getBackupType() {
return backupType;
}
public void setBackupType(String backupType) {
this.backupType = backupType;
}
public String getBackupStatus() {
return backupStatus;
}
public void setBackupStatus(String backupStatus) {
this.backupStatus = backupStatus;
}
public String getTriggerMode() {
return triggerMode;
}
public void setTriggerMode(String triggerMode) {
this.triggerMode = triggerMode;
}
public String getStartedAt() {
return startedAt;
}
public void setStartedAt(String startedAt) {
this.startedAt = startedAt;
}
public String getFinishedAt() {
return finishedAt;
}
public void setFinishedAt(String finishedAt) {
this.finishedAt = finishedAt;
}
public Long getDurationSeconds() {
return durationSeconds;
}
public void setDurationSeconds(Long durationSeconds) {
this.durationSeconds = durationSeconds;
}
public String getBackupPath() {
return backupPath;
}
public void setBackupPath(String backupPath) {
this.backupPath = backupPath;
}
public Long getFileSizeBytes() {
return fileSizeBytes;
}
public void setFileSizeBytes(Long fileSizeBytes) {
this.fileSizeBytes = fileSizeBytes;
}
public Long getArchiveCount() {
return archiveCount;
}
public void setArchiveCount(Long archiveCount) {
this.archiveCount = archiveCount;
}
public String getResultMsg() {
return resultMsg;
}
public void setResultMsg(String resultMsg) {
this.resultMsg = resultMsg;
}
public String getExecutorNode() {
return executorNode;
}
public void setExecutorNode(String executorNode) {
this.executorNode = executorNode;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,249 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 档案借阅(资料室·借阅与利用 + 保密分级审批)。员工在线申请借阅某份正式档案
* {@link Archive}),按档案密级走分级审批,获批后借出,按期归还,逾期催还。状态机:
*
* <pre>
* 待审批 ──批准──▶ 已批准 ──借出──▶ 借阅中 ──归还──▶ 已归还
* │ │
* └──驳回──▶ 已驳回 └─(超期未还)─▶ 借阅中(overdue 标记)
* </pre>
*
* borrowType 为 在线预览 / 电子下载 / 实体借出;电子类(预览/下载)在「借出」时不占实体,
* 实体借出需登记预计归还日 dueDate,逾期由看板/预警按 dueDate 与今天比对标红催还。
* approvalLevel 按档案密级派生(公开/内部→部门级,秘密/机密→资料室主任级,绝密→高管级),
* 仅作展示与审批留痕分级提示。
*/
@Entity
@Table(name = "archive_borrow")
public class ArchiveBorrow {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 借阅单号(BY-流水)。 */
private String code;
/** 被借阅的正式档案 id{@link Archive})。 */
private Long archiveId;
/** 冗余档案标题。 */
private String archiveTitle;
/** 冗余档案密级(决定审批分级)。 */
private String secLevel;
/** 借阅人。 */
private String borrower;
/** 借阅人部门。 */
private String borrowerDept;
/** 借阅方式:在线预览 / 电子下载 / 实体借出。 */
private String borrowType;
/** 借阅目的。 */
private String purpose;
/** 申请日期。 */
private String applyDate;
/** 预计归还日(实体借出/下载有效期,YYYY-MM-DD)。 */
private String dueDate;
/** 实际归还日。 */
private String returnDate;
/** 审批分级(部门级 / 资料室主任级 / 高管级),按密级派生。 */
private String approvalLevel;
/** 状态:待审批 / 已批准 / 借阅中 / 已归还 / 已驳回。 */
private String status;
/** 审批人。 */
private String approver;
/** 审批意见。 */
private String approveOpinion;
/** 归还检查结果(完好 / 缺页 / 污损…)。 */
private String returnCheck;
/** 续借次数(每次续借 +1,由 renew 端点维护)。 */
private int renewCount;
/** 最近一次续借后的新到期日(YYYY-MM-DD);同步写入 dueDate。 */
private String renewedDueDate;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Long getArchiveId() {
return archiveId;
}
public void setArchiveId(Long archiveId) {
this.archiveId = archiveId;
}
public String getArchiveTitle() {
return archiveTitle;
}
public void setArchiveTitle(String archiveTitle) {
this.archiveTitle = archiveTitle;
}
public String getSecLevel() {
return secLevel;
}
public void setSecLevel(String secLevel) {
this.secLevel = secLevel;
}
public String getBorrower() {
return borrower;
}
public void setBorrower(String borrower) {
this.borrower = borrower;
}
public String getBorrowerDept() {
return borrowerDept;
}
public void setBorrowerDept(String borrowerDept) {
this.borrowerDept = borrowerDept;
}
public String getBorrowType() {
return borrowType;
}
public void setBorrowType(String borrowType) {
this.borrowType = borrowType;
}
public String getPurpose() {
return purpose;
}
public void setPurpose(String purpose) {
this.purpose = purpose;
}
public String getApplyDate() {
return applyDate;
}
public void setApplyDate(String applyDate) {
this.applyDate = applyDate;
}
public String getDueDate() {
return dueDate;
}
public void setDueDate(String dueDate) {
this.dueDate = dueDate;
}
public String getReturnDate() {
return returnDate;
}
public void setReturnDate(String returnDate) {
this.returnDate = returnDate;
}
public String getApprovalLevel() {
return approvalLevel;
}
public void setApprovalLevel(String approvalLevel) {
this.approvalLevel = approvalLevel;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getApprover() {
return approver;
}
public void setApprover(String approver) {
this.approver = approver;
}
public String getApproveOpinion() {
return approveOpinion;
}
public void setApproveOpinion(String approveOpinion) {
this.approveOpinion = approveOpinion;
}
public String getReturnCheck() {
return returnCheck;
}
public void setReturnCheck(String returnCheck) {
this.returnCheck = returnCheck;
}
public int getRenewCount() {
return renewCount;
}
public void setRenewCount(int renewCount) {
this.renewCount = renewCount;
}
public String getRenewedDueDate() {
return renewedDueDate;
}
public void setRenewedDueDate(String renewedDueDate) {
this.renewedDueDate = renewedDueDate;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,148 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 档案分类(资料室·分类体系 + 保存期限规则)。一条 ArchiveCategory 代表一个可自定义的
* 档案类别(行政档案 / 人事档案 / 财务档案 / 项目档案 / 合同档案 / 图纸档案 /
* 知识产权档案 / 质量档案 / 设备档案…),支持多级(parentId 指向上级类别,顶级为空)。
*
* <p>每个类别可挂保存期限规则:retentionType 为 永久 / 定期;retentionYears 为定期年限
* (10 / 30 / 5 等,永久时忽略)。归档时系统按「形成日期 + retentionYears」自动算到期日,
* 到期前进入鉴定销毁预警。codePrefix 为该类别归档编号前缀(如 XM 表示项目档案),
* 归档审核通过时按「codePrefix-年度-流水号」生成唯一档案编号。
*
* <p>metaFields 为该类别要求的自定义元数据字段(逗号分隔,如「项目编号,起止日期,投资额」),
* 仅作归档申请表单的提示用途,不强约束。defaultSecLevel 为该类别建议密级
* (公开 / 内部 / 秘密 / 机密 / 绝密)。
*/
@Entity
@Table(name = "archive_category")
public class ArchiveCategory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 类别名称(如 项目档案 / 合同档案)。 */
private String name;
/** 上级类别 id(多级分类,顶级为空)。 */
private Long parentId;
/** 归档编号前缀(如 XM / HT / TZ)。 */
private String codePrefix;
/** 保存期限类型:永久 / 定期。 */
private String retentionType;
/** 定期年限(年;永久时忽略)。 */
private Integer retentionYears;
/** 该类别要求的自定义元数据字段(逗号分隔,仅提示)。 */
private String metaFields;
/** 建议密级:公开 / 内部 / 秘密 / 机密 / 绝密。 */
private String defaultSecLevel;
/** 是否启用(停用后不在归档申请下拉中出现)。 */
private Boolean enabled = Boolean.TRUE;
private String remark;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
this.parentId = parentId;
}
public String getCodePrefix() {
return codePrefix;
}
public void setCodePrefix(String codePrefix) {
this.codePrefix = codePrefix;
}
public String getRetentionType() {
return retentionType;
}
public void setRetentionType(String retentionType) {
this.retentionType = retentionType;
}
public Integer getRetentionYears() {
return retentionYears;
}
public void setRetentionYears(Integer retentionYears) {
this.retentionYears = retentionYears;
}
public String getMetaFields() {
return metaFields;
}
public void setMetaFields(String metaFields) {
this.metaFields = metaFields;
}
public String getDefaultSecLevel() {
return defaultSecLevel;
}
public void setDefaultSecLevel(String defaultSecLevel) {
this.defaultSecLevel = defaultSecLevel;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,71 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* A comment attached to an archive entry. archiveId links the comment to the
* archive record it annotates; author is the commenter name and content the
* comment body. createdAt is set on creation.
*/
@Entity
@Table(name = "archive_comment")
public class ArchiveComment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long archiveId;
private String author;
private String content;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getArchiveId() {
return archiveId;
}
public void setArchiveId(Long archiveId) {
this.archiveId = archiveId;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,283 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 档案销毁鉴定单(资料室·Module 4「销毁管理」)。保存期满的档案进入销毁鉴定流程,
* 经鉴定 + 审批后执行销毁(实体拍照/视频留证,电子彻底/逻辑删除),系统留存销毁证明。
* 补审计「销毁鉴定审批流程 + 实体销毁拍照视频留证 + 电子彻底/逻辑删除 + 销毁证明」全缺缺口。
*
* <p>状态机(双重把关:先鉴定再审批,审批后才可执行销毁,杜绝误销):
* <pre>
* 待鉴定 ──鉴定(appraise: 留存/销毁)──┬─[留存]─▶ 已鉴定·留存(回写延长保存期, 终态)
* └─[销毁]─▶ 待审批
* 待审批 ──审批通过(approve)──▶ 已批准 ──执行(execute: 录留证)──▶ 已销毁(终态)
* 待审批 ──审批驳回(reject)──▶ 已驳回(终态)
* </pre>
*
* <p>disposalMode 销毁方式:实体粉碎 / 实体焚毁 / 电子彻底删除 / 电子逻辑删除。
* proofUrl 为拍照/视频留证链接,certNo 为系统生成的销毁证明编号。targetItemId 关联被销毁实体盒。
*/
@Entity
@Table(name = "archive_destruction")
public class ArchiveDestruction {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 销毁单号(XH-流水)。 */
private String code;
/** 被销毁档案标题(冗余)。 */
private String archiveTitle;
/** 关联正式档案 id{@link Archive},可空)。 */
private Long archiveId;
/** 关联实体盒 id{@link ArchivePhysicalItem},可空,实体销毁时回写盒状态)。 */
private Long targetItemId;
/** 密级。 */
private String secLevel;
/** 保存到期日(YYYY-MM-DD,纳入销毁理由)。 */
private String retentionDueDate;
/** 销毁理由(保存期满 / 重复件 / 失去保存价值…)。 */
private String reason;
/** 销毁方式:实体粉碎 / 实体焚毁 / 电子彻底删除 / 电子逻辑删除。 */
private String disposalMode;
/** 状态:待鉴定 / 已鉴定·留存 / 待审批 / 已批准 / 已销毁 / 已驳回。 */
private String status;
/** 申请人。 */
private String applicant;
/** 申请日期。 */
private String applyDate;
/** 鉴定人。 */
private String appraiser;
/** 鉴定结论:留存 / 销毁。 */
private String appraisalResult;
/** 鉴定意见。 */
private String appraisalOpinion;
/** 鉴定为留存时延长的保存到期日(回写)。 */
private String extendedDueDate;
/** 审批人。 */
private String approver;
/** 审批意见。 */
private String approveOpinion;
/** 执行人(实际销毁操作人)。 */
private String executor;
/** 执行日期。 */
private String executeDate;
/** 拍照/视频留证链接。 */
private String proofUrl;
/** 系统生成的销毁证明编号。 */
private String certNo;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getArchiveTitle() {
return archiveTitle;
}
public void setArchiveTitle(String archiveTitle) {
this.archiveTitle = archiveTitle;
}
public Long getArchiveId() {
return archiveId;
}
public void setArchiveId(Long archiveId) {
this.archiveId = archiveId;
}
public Long getTargetItemId() {
return targetItemId;
}
public void setTargetItemId(Long targetItemId) {
this.targetItemId = targetItemId;
}
public String getSecLevel() {
return secLevel;
}
public void setSecLevel(String secLevel) {
this.secLevel = secLevel;
}
public String getRetentionDueDate() {
return retentionDueDate;
}
public void setRetentionDueDate(String retentionDueDate) {
this.retentionDueDate = retentionDueDate;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public String getDisposalMode() {
return disposalMode;
}
public void setDisposalMode(String disposalMode) {
this.disposalMode = disposalMode;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getApplicant() {
return applicant;
}
public void setApplicant(String applicant) {
this.applicant = applicant;
}
public String getApplyDate() {
return applyDate;
}
public void setApplyDate(String applyDate) {
this.applyDate = applyDate;
}
public String getAppraiser() {
return appraiser;
}
public void setAppraiser(String appraiser) {
this.appraiser = appraiser;
}
public String getAppraisalResult() {
return appraisalResult;
}
public void setAppraisalResult(String appraisalResult) {
this.appraisalResult = appraisalResult;
}
public String getAppraisalOpinion() {
return appraisalOpinion;
}
public void setAppraisalOpinion(String appraisalOpinion) {
this.appraisalOpinion = appraisalOpinion;
}
public String getExtendedDueDate() {
return extendedDueDate;
}
public void setExtendedDueDate(String extendedDueDate) {
this.extendedDueDate = extendedDueDate;
}
public String getApprover() {
return approver;
}
public void setApprover(String approver) {
this.approver = approver;
}
public String getApproveOpinion() {
return approveOpinion;
}
public void setApproveOpinion(String approveOpinion) {
this.approveOpinion = approveOpinion;
}
public String getExecutor() {
return executor;
}
public void setExecutor(String executor) {
this.executor = executor;
}
public String getExecuteDate() {
return executeDate;
}
public void setExecuteDate(String executeDate) {
this.executeDate = executeDate;
}
public String getProofUrl() {
return proofUrl;
}
public void setProofUrl(String proofUrl) {
this.proofUrl = proofUrl;
}
public String getCertNo() {
return certNo;
}
public void setCertNo(String certNo) {
this.certNo = certNo;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,256 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 资料室灾难恢复演练记录(档案系统 Module 7「合规与审计支持」)。
*
* 需求:「灾难恢复演练记录表/端点缺失」——本实体补全该缺口,
* 支持定期演练记录的创建、状态跟踪(计划中/演练中/已完成/演练失败)
* 及恢复时间/恢复点目标(RTO/RPO)实测记录,满足合规审计要求。
*
* 状态机:
* 计划中 ──开始演练──▶ 演练中 ──完成──▶ 已完成
* └────失败──▶ 演练失败
*/
@Entity
@Table(name = "archive_drill_record")
public class ArchiveDrillRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 演练编号(DR-年度-流水)。 */
private String drillCode;
/** 演练名称(如「年度档案系统全量恢复演练」)。 */
private String drillName;
/** 演练类型:全量恢复 / 增量恢复 / 单档案恢复 / 异地切换。 */
private String drillType;
/** 演练状态:计划中 / 演练中 / 已完成 / 演练失败。 */
private String drillStatus;
/** 计划演练日期(YYYY-MM-DD)。 */
private String plannedDate;
/** 实际开始时间。 */
private String actualStartTime;
/** 实际结束时间。 */
private String actualEndTime;
/** 演练负责人。 */
private String leader;
/** 参与人员(逗号分隔)。 */
private String participants;
/** 依赖备份批次/路径(演练所用备份的标识)。 */
private String backupRef;
/** 实测 RTO(恢复时间目标,分钟)。 */
private Integer actualRtoMinutes;
/** 实测 RPO(恢复点目标,数据丢失分钟数)。 */
private Integer actualRpoMinutes;
/** 目标 RTO(分钟,合规要求)。 */
private Integer targetRtoMinutes;
/** 目标 RPO(分钟,合规要求)。 */
private Integer targetRpoMinutes;
/** 是否达标(实测 RTO/RPO 均满足目标)。 */
private Boolean passFlag;
/** 演练结果摘要(成功/失败原因、数据完整性、问题描述)。 */
private String resultSummary;
/** 改进措施(演练中发现问题的整改建议)。 */
private String improvements;
/** 演练记录附件 URL(如录屏/截图/报告 PDF)。 */
private String evidenceUrl;
/** 下次演练计划日期。 */
private String nextDrillDate;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getDrillCode() {
return drillCode;
}
public void setDrillCode(String drillCode) {
this.drillCode = drillCode;
}
public String getDrillName() {
return drillName;
}
public void setDrillName(String drillName) {
this.drillName = drillName;
}
public String getDrillType() {
return drillType;
}
public void setDrillType(String drillType) {
this.drillType = drillType;
}
public String getDrillStatus() {
return drillStatus;
}
public void setDrillStatus(String drillStatus) {
this.drillStatus = drillStatus;
}
public String getPlannedDate() {
return plannedDate;
}
public void setPlannedDate(String plannedDate) {
this.plannedDate = plannedDate;
}
public String getActualStartTime() {
return actualStartTime;
}
public void setActualStartTime(String actualStartTime) {
this.actualStartTime = actualStartTime;
}
public String getActualEndTime() {
return actualEndTime;
}
public void setActualEndTime(String actualEndTime) {
this.actualEndTime = actualEndTime;
}
public String getLeader() {
return leader;
}
public void setLeader(String leader) {
this.leader = leader;
}
public String getParticipants() {
return participants;
}
public void setParticipants(String participants) {
this.participants = participants;
}
public String getBackupRef() {
return backupRef;
}
public void setBackupRef(String backupRef) {
this.backupRef = backupRef;
}
public Integer getActualRtoMinutes() {
return actualRtoMinutes;
}
public void setActualRtoMinutes(Integer actualRtoMinutes) {
this.actualRtoMinutes = actualRtoMinutes;
}
public Integer getActualRpoMinutes() {
return actualRpoMinutes;
}
public void setActualRpoMinutes(Integer actualRpoMinutes) {
this.actualRpoMinutes = actualRpoMinutes;
}
public Integer getTargetRtoMinutes() {
return targetRtoMinutes;
}
public void setTargetRtoMinutes(Integer targetRtoMinutes) {
this.targetRtoMinutes = targetRtoMinutes;
}
public Integer getTargetRpoMinutes() {
return targetRpoMinutes;
}
public void setTargetRpoMinutes(Integer targetRpoMinutes) {
this.targetRpoMinutes = targetRpoMinutes;
}
public Boolean getPassFlag() {
return passFlag;
}
public void setPassFlag(Boolean passFlag) {
this.passFlag = passFlag;
}
public String getResultSummary() {
return resultSummary;
}
public void setResultSummary(String resultSummary) {
this.resultSummary = resultSummary;
}
public String getImprovements() {
return improvements;
}
public void setImprovements(String improvements) {
this.improvements = improvements;
}
public String getEvidenceUrl() {
return evidenceUrl;
}
public void setEvidenceUrl(String evidenceUrl) {
this.evidenceUrl = evidenceUrl;
}
public String getNextDrillDate() {
return nextDrillDate;
}
public void setNextDrillDate(String nextDrillDate) {
this.nextDrillDate = nextDrillDate;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,110 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 资料室·电子档案哈希存证(防篡改)。
*
* 满足需求 16:「重要电子档案计算哈希值存证,防止篡改」。
* 每次对档案计算 SHA-256 并记录在此表;后续可随时对比重算值与存档值是否一致。
*
* algorithm:哈希算法,默认 SHA-256。
* hashValue:十六进制摘要字符串(64位)。
* sourceMANUAL(人工触发)/ SCHEDULED(定时自动)。
* verifyStatusUNVERIFIED / OK / TAMPERED(篡改告警)。
*/
@Entity
@Table(name = "archive_hash_record")
public class ArchiveHashRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联档案 idArchive.id)。 */
private Long archiveId;
/** 档案标题(冗余,便于展示)。 */
private String archiveTitle;
/** 哈希算法,默认 SHA-256。 */
private String algorithm;
/** 十六进制摘要。 */
@Column(name = "hash_val", length = 256)
private String hashValue;
/** 档案文件 URL(计算时取自 Archive.fileUrl,用于再次拉取校验)。 */
@Column(name = "src_url")
private String srcUrl;
/** 文件大小(bytes),与 hashValue 一起作为存证基础。 */
private Long fileSize;
/** 触发来源:MANUAL / SCHEDULED。 */
private String source;
/** 操作人(触发者 displayName)。 */
private String operator;
/**
* 验证状态:UNVERIFIED(未校验)/ OK(与上次一致)/ TAMPERED(检测到篡改)。
* 通过 /archive-hash-records/{id}/verify 端点更新。
*/
private String verifyStatus;
/** 最近一次核验时间。 */
private Instant verifiedAt;
/** 核验时的重算 hash(与 hashValue 比对)。 */
@Column(name = "verify_hash", length = 256)
private String verifyHash;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getArchiveId() { return archiveId; }
public void setArchiveId(Long archiveId) { this.archiveId = archiveId; }
public String getArchiveTitle() { return archiveTitle; }
public void setArchiveTitle(String archiveTitle) { this.archiveTitle = archiveTitle; }
public String getAlgorithm() { return algorithm; }
public void setAlgorithm(String algorithm) { this.algorithm = algorithm; }
public String getHashValue() { return hashValue; }
public void setHashValue(String hashValue) { this.hashValue = hashValue; }
public String getSrcUrl() { return srcUrl; }
public void setSrcUrl(String srcUrl) { this.srcUrl = srcUrl; }
public Long getFileSize() { return fileSize; }
public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
public String getOperator() { return operator; }
public void setOperator(String operator) { this.operator = operator; }
public String getVerifyStatus() { return verifyStatus; }
public void setVerifyStatus(String verifyStatus) { this.verifyStatus = verifyStatus; }
public Instant getVerifiedAt() { return verifiedAt; }
public void setVerifiedAt(Instant verifiedAt) { this.verifiedAt = verifiedAt; }
public String getVerifyHash() { return verifyHash; }
public void setVerifyHash(String verifyHash) { this.verifyHash = verifyHash; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,260 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 实体档案盒/卷(资料室·实体档案管理)。一条 ArchivePhysicalItem 代表一个实体档案盒(卷),
* 是实体档案上架定位与借阅出入库的最小载体。补审计 Module 4「入库上架(扫码录位/移动端上架)」
* 与「档案盒/卷管理(盒内文件清单 + 盒号位置绑定)」缺口。
*
* <p>状态机:
* <pre>
* 待上架 ──上架(scanShelve, 录入库房/排/列/层 + 绑定条码)──▶ 在库
* 在库 ──出库(checkOut, 借阅出库)──▶ 出库中 ──入库(checkIn, 归还入库)──▶ 在库
* 在库 ──销毁(linkDestruction)──▶ 已销毁
* </pre>
*
* <p>位置六段定位:vaultId(虚拟档案库) + roomNo(库房号) + rackNo(密集架排号) + colNo(列号)
* + rowNo(层号) + boxNo(盒号)。barcode 为条形码/二维码/RFID 标签内容,扫码可秒级定位。
* itemsJson 为盒内文件清单(JSON 数组,每项含 archiveId/title/pages),实现「同一项目多份文件合并入盒」。
*/
@Entity
@Table(name = "archive_physical_item")
public class ArchivePhysicalItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 盒/卷编号(PB-流水)。 */
private String code;
/** 盒/卷题名(如「2026 凯迪大厦项目竣工验收卷」)。 */
private String title;
/** 所属虚拟档案库 id{@link ArchiveVault},可空)。 */
private Long vaultId;
/** 冗余档案库名(便于列表展示)。 */
private String vaultName;
/** 关联档案类别名(如 项目档案 / 合同档案)。 */
private String categoryName;
/** 密级(公开/内部/秘密/机密/绝密)。 */
private String secLevel;
/** 条码/二维码/RFID 标签内容(上架时绑定,扫码定位用)。 */
private String barcode;
/** 库房号。 */
private String roomNo;
/** 密集架排号。 */
private String rackNo;
/** 列号。 */
private String colNo;
/** 层号。 */
private String rowNo;
/** 盒号。 */
private String boxNo;
/** 盒内文件清单 JSON(数组,每项 {archiveId,title,pages})。 */
private String itemsJson;
/** 盒内文件份数(itemsJson 长度冗余,便于统计)。 */
private Integer itemCount;
/** 状态:待上架 / 在库 / 出库中 / 已销毁。 */
private String status;
/** 上架人。 */
private String shelvedBy;
/** 上架日期(YYYY-MM-DD)。 */
private String shelvedDate;
/** 关联销毁单 id(销毁后回填,{@link ArchiveDestruction})。 */
private Long destructionId;
private String remark;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Long getVaultId() {
return vaultId;
}
public void setVaultId(Long vaultId) {
this.vaultId = vaultId;
}
public String getVaultName() {
return vaultName;
}
public void setVaultName(String vaultName) {
this.vaultName = vaultName;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public String getSecLevel() {
return secLevel;
}
public void setSecLevel(String secLevel) {
this.secLevel = secLevel;
}
public String getBarcode() {
return barcode;
}
public void setBarcode(String barcode) {
this.barcode = barcode;
}
public String getRoomNo() {
return roomNo;
}
public void setRoomNo(String roomNo) {
this.roomNo = roomNo;
}
public String getRackNo() {
return rackNo;
}
public void setRackNo(String rackNo) {
this.rackNo = rackNo;
}
public String getColNo() {
return colNo;
}
public void setColNo(String colNo) {
this.colNo = colNo;
}
public String getRowNo() {
return rowNo;
}
public void setRowNo(String rowNo) {
this.rowNo = rowNo;
}
public String getBoxNo() {
return boxNo;
}
public void setBoxNo(String boxNo) {
this.boxNo = boxNo;
}
public String getItemsJson() {
return itemsJson;
}
public void setItemsJson(String itemsJson) {
this.itemsJson = itemsJson;
}
public Integer getItemCount() {
return itemCount;
}
public void setItemCount(Integer itemCount) {
this.itemCount = itemCount;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getShelvedBy() {
return shelvedBy;
}
public void setShelvedBy(String shelvedBy) {
this.shelvedBy = shelvedBy;
}
public String getShelvedDate() {
return shelvedDate;
}
public void setShelvedDate(String shelvedDate) {
this.shelvedDate = shelvedDate;
}
public Long getDestructionId() {
return destructionId;
}
public void setDestructionId(Long destructionId) {
this.destructionId = destructionId;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,193 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 档案修复与保护记录(资料室·Module 4「档案修复与保护」)。记录实体档案破损、褪色、虫蛀等
* 损坏情况与修复过程;未修复且达预警条件的进入褪色/虫蛀预警。补审计「档案破损修复记录与
* 褪色虫蛀预警」全缺缺口。
*
* <p>状态机:
* <pre>
* 待处理(登记损坏) ──开始修复(startRepair)──▶ 修复中 ──完成(finishRepair, 录修复人/方法/日期)──▶ 已修复
* 待处理 / 修复中 ──标记报废(scrap)──▶ 已报废
* </pre>
*
* <p>damageType 损坏类型:破损 / 褪色 / 虫蛀 / 霉变 / 水渍 / 缺页。severity 严重度:轻微/中度/严重。
* 「严重」且「待处理」的记录由看板纳入预警(褪色/虫蛀严重需尽快修复)。
*/
@Entity
@Table(name = "archive_repair_record")
public class ArchiveRepairRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 记录单号(XF-流水)。 */
private String code;
/** 关联实体盒 id{@link ArchivePhysicalItem},可空)。 */
private Long itemId;
/** 档案/盒标题(冗余)。 */
private String archiveTitle;
/** 损坏类型:破损 / 褪色 / 虫蛀 / 霉变 / 水渍 / 缺页。 */
private String damageType;
/** 严重度:轻微 / 中度 / 严重。 */
private String severity;
/** 损坏描述。 */
private String damageDesc;
/** 状态:待处理 / 修复中 / 已修复 / 已报废。 */
private String status;
/** 发现人。 */
private String reporter;
/** 登记日期。 */
private String reportDate;
/** 修复人。 */
private String repairer;
/** 修复方法(如 托裱 / 去酸 / 杀虫熏蒸 / 数字化抢救)。 */
private String repairMethod;
/** 修复完成日期。 */
private String repairDate;
/** 修复结果说明。 */
private String repairResult;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Long getItemId() {
return itemId;
}
public void setItemId(Long itemId) {
this.itemId = itemId;
}
public String getArchiveTitle() {
return archiveTitle;
}
public void setArchiveTitle(String archiveTitle) {
this.archiveTitle = archiveTitle;
}
public String getDamageType() {
return damageType;
}
public void setDamageType(String damageType) {
this.damageType = damageType;
}
public String getSeverity() {
return severity;
}
public void setSeverity(String severity) {
this.severity = severity;
}
public String getDamageDesc() {
return damageDesc;
}
public void setDamageDesc(String damageDesc) {
this.damageDesc = damageDesc;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getReporter() {
return reporter;
}
public void setReporter(String reporter) {
this.reporter = reporter;
}
public String getReportDate() {
return reportDate;
}
public void setReportDate(String reportDate) {
this.reportDate = reportDate;
}
public String getRepairer() {
return repairer;
}
public void setRepairer(String repairer) {
this.repairer = repairer;
}
public String getRepairMethod() {
return repairMethod;
}
public void setRepairMethod(String repairMethod) {
this.repairMethod = repairMethod;
}
public String getRepairDate() {
return repairDate;
}
public void setRepairDate(String repairDate) {
this.repairDate = repairDate;
}
public String getRepairResult() {
return repairResult;
}
public void setRepairResult(String repairResult) {
this.repairResult = repairResult;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,159 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 档案预约(资料室·Module 5「预约与续借」)。档案被借出时,其他用户可预约;档案归还后
* 按预约队列先后通知可借。补审计「预约与续借完全缺、ArchiveBorrow 无 reserve/renew 字段」缺口
* 的预约侧(续借在 ArchiveBorrow 上以 renewCount/续借 动作补深)。
*
* <p>状态机:
* <pre>
* 排队中 ──(档案归还后通知)──▶ 可借 ──(预约人发起借阅)──▶ 已转借阅(终态)
* 排队中 / 可借 ──取消(cancel)──▶ 已取消(终态)
* </pre>
*
* queueNo 为同一档案下的预约排队序号(先到先得)。
*/
@Entity
@Table(name = "archive_reservation")
public class ArchiveReservation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 预约单号(YY-流水)。 */
private String code;
/** 预约的档案 id{@link Archive})。 */
private Long archiveId;
/** 档案标题(冗余)。 */
private String archiveTitle;
/** 预约人。 */
private String requester;
/** 预约人部门。 */
private String requesterDept;
/** 预约目的。 */
private String purpose;
/** 排队序号(同档案先到先得)。 */
private Integer queueNo;
/** 状态:排队中 / 可借 / 已转借阅 / 已取消。 */
private String status;
/** 预约日期。 */
private String reserveDate;
/** 转为可借的通知日期。 */
private String notifyDate;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Long getArchiveId() {
return archiveId;
}
public void setArchiveId(Long archiveId) {
this.archiveId = archiveId;
}
public String getArchiveTitle() {
return archiveTitle;
}
public void setArchiveTitle(String archiveTitle) {
this.archiveTitle = archiveTitle;
}
public String getRequester() {
return requester;
}
public void setRequester(String requester) {
this.requester = requester;
}
public String getRequesterDept() {
return requesterDept;
}
public void setRequesterDept(String requesterDept) {
this.requesterDept = requesterDept;
}
public String getPurpose() {
return purpose;
}
public void setPurpose(String purpose) {
this.purpose = purpose;
}
public Integer getQueueNo() {
return queueNo;
}
public void setQueueNo(Integer queueNo) {
this.queueNo = queueNo;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getReserveDate() {
return reserveDate;
}
public void setReserveDate(String reserveDate) {
this.reserveDate = reserveDate;
}
public String getNotifyDate() {
return notifyDate;
}
public void setNotifyDate(String notifyDate) {
this.notifyDate = notifyDate;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,248 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 归档申请(资料室·在线归档 + 归档审核 + 归档编号生成)。业务部门在线提交归档申请,
* 资料室管理员审核格式 / 元数据 / 密级。状态机:
*
* <pre>
* 待审核 ──通过──▶ 已归档(自动生成档案编号 + 落一条正式 Archive + 算保存到期日)
* │
* └──退回──▶ 已退回(填退回意见,业务部门改后可重新提交→回到 待审核)
* </pre>
*
* archiveCode 在审核通过时由「类别前缀-年度-流水号」自动生成(与 {@link Archive} 打通:
* 通过后自动落一条正式档案记录,archiveId 回填)。retentionDueDate 按类别保存期限规则
* (形成日期 formedDate + 保留年限)自动算出,永久类别为空。
*/
@Entity
@Table(name = "archive_submission")
public class ArchiveSubmission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 资料名称(必填)。 */
private String title;
/** 档案类别 id(关联 {@link ArchiveCategory})。 */
private Long categoryId;
/** 冗余类别名(便于列表/审计展示)。 */
private String categoryName;
/** 来源部门。 */
private String sourceDept;
/** 形成日期(YYYY-MM-DD,用于算保存到期日)。 */
private String formedDate;
/** 归档范围(如 全宗 / 案卷级 / 文件级)。 */
private String archiveScope;
/** 保密等级:公开 / 内部 / 秘密 / 机密 / 绝密。 */
private String secLevel;
/** 文件类型(PDF / CAD / DOC…)。 */
private String fileType;
/** 自定义元数据 JSON 文本(前端按类别 metaFields 拼装,原样存储)。 */
private String metaJson;
/** 申请人。 */
private String applicant;
/** 申请日期。 */
private String applyDate;
/** 状态:待审核 / 已归档 / 已退回。 */
private String status;
/** 审核人。 */
private String reviewer;
/** 审核 / 退回意见。 */
private String reviewOpinion;
/** 审核日期。 */
private String reviewDate;
/** 审核通过自动生成的档案编号(类别前缀-年度-流水号)。 */
private String archiveCode;
/** 审核通过后落地的正式档案 id{@link Archive})。 */
private Long archiveId;
/** 保存到期日(YYYY-MM-DD;永久档案为空)。审核通过时按类别保留年限算出。 */
private String retentionDueDate;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Long getCategoryId() {
return categoryId;
}
public void setCategoryId(Long categoryId) {
this.categoryId = categoryId;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public String getSourceDept() {
return sourceDept;
}
public void setSourceDept(String sourceDept) {
this.sourceDept = sourceDept;
}
public String getFormedDate() {
return formedDate;
}
public void setFormedDate(String formedDate) {
this.formedDate = formedDate;
}
public String getArchiveScope() {
return archiveScope;
}
public void setArchiveScope(String archiveScope) {
this.archiveScope = archiveScope;
}
public String getSecLevel() {
return secLevel;
}
public void setSecLevel(String secLevel) {
this.secLevel = secLevel;
}
public String getFileType() {
return fileType;
}
public void setFileType(String fileType) {
this.fileType = fileType;
}
public String getMetaJson() {
return metaJson;
}
public void setMetaJson(String metaJson) {
this.metaJson = metaJson;
}
public String getApplicant() {
return applicant;
}
public void setApplicant(String applicant) {
this.applicant = applicant;
}
public String getApplyDate() {
return applyDate;
}
public void setApplyDate(String applyDate) {
this.applyDate = applyDate;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getReviewer() {
return reviewer;
}
public void setReviewer(String reviewer) {
this.reviewer = reviewer;
}
public String getReviewOpinion() {
return reviewOpinion;
}
public void setReviewOpinion(String reviewOpinion) {
this.reviewOpinion = reviewOpinion;
}
public String getReviewDate() {
return reviewDate;
}
public void setReviewDate(String reviewDate) {
this.reviewDate = reviewDate;
}
public String getArchiveCode() {
return archiveCode;
}
public void setArchiveCode(String archiveCode) {
this.archiveCode = archiveCode;
}
public Long getArchiveId() {
return archiveId;
}
public void setArchiveId(Long archiveId) {
this.archiveId = archiveId;
}
public String getRetentionDueDate() {
return retentionDueDate;
}
public void setRetentionDueDate(String retentionDueDate) {
this.retentionDueDate = retentionDueDate;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,175 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 虚拟档案库(资料室·Module 1「档案库管理」)。一条 ArchiveVault 对应一个实体库房 / 密集架 /
* 电子存储区,支持按年度、部门、保密等级、保存期限等维度建库。补审计「无虚拟档案库实体/接口」缺口。
*
* <p>storeType 区分载体:实体库房 / 密集架 / 电子存储区 / 云存储。实体类记 roomNo(库房号),
* 电子/云类记 cloudId(云存储 ID / 存储路径),打通需求「电子档案记录存储路径或云存储ID」。
* 维度字段(year/dept/secLevel/retentionScope)用于按维度建库与归档时落库匹配。
*/
@Entity
@Table(name = "archive_vault")
public class ArchiveVault {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 档案库编号(VL-流水)。 */
private String code;
/** 档案库名称(如「2026 项目档案密集架 A 区」)。 */
private String name;
/** 载体类型:实体库房 / 密集架 / 电子存储区 / 云存储。 */
private String storeType;
/** 实体库房号(实体/密集架类填)。 */
private String roomNo;
/** 云存储 ID / 存储路径(电子/云类填)。 */
private String cloudId;
/** 建库维度·年度(如 2026,可空表示不限)。 */
private String dimYear;
/** 建库维度·部门。 */
private String dimDept;
/** 建库维度·保密等级上限(该库可容纳的最高密级)。 */
private String dimSecLevel;
/** 建库维度·保存期限范围(永久 / 30年 / 10年 / 5年)。 */
private String dimRetention;
/** 容量(盒/卷上限,0 表示不限)。 */
private Integer capacity;
/** 是否启用。 */
private Boolean enabled = Boolean.TRUE;
private String remark;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getStoreType() {
return storeType;
}
public void setStoreType(String storeType) {
this.storeType = storeType;
}
public String getRoomNo() {
return roomNo;
}
public void setRoomNo(String roomNo) {
this.roomNo = roomNo;
}
public String getCloudId() {
return cloudId;
}
public void setCloudId(String cloudId) {
this.cloudId = cloudId;
}
public String getDimYear() {
return dimYear;
}
public void setDimYear(String dimYear) {
this.dimYear = dimYear;
}
public String getDimDept() {
return dimDept;
}
public void setDimDept(String dimDept) {
this.dimDept = dimDept;
}
public String getDimSecLevel() {
return dimSecLevel;
}
public void setDimSecLevel(String dimSecLevel) {
this.dimSecLevel = dimSecLevel;
}
public String getDimRetention() {
return dimRetention;
}
public void setDimRetention(String dimRetention) {
this.dimRetention = dimRetention;
}
public Integer getCapacity() {
return capacity;
}
public void setCapacity(Integer capacity) {
this.capacity = capacity;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,155 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 档案版本(资料室·Module 3「版本控制」)。同一档案可存在多个版本(如图纸 V1.0、V2.0),
* 系统记录版本历史与变更说明;当前有效版本标记为「现行」,旧版本标记为「作废」但保留。
* 补审计「Archive 无 version/现行/作废字段、DocumentController 恒置 setVersion(1) 无版本历史」缺口。
*
* <p>每条 ArchiveVersion 隶属一个 ArchivearchiveId)。versionNo 形如 V1.0 / V2.0
* isCurrent 标记现行版本(同一 archiveId 下仅一条为 true,升级新版时自动把旧现行版置作废)。
* status:现行 / 作废。changeNote 为本次版本的变更说明。
*/
@Entity
@Table(name = "archive_version")
public class ArchiveVersion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 所属档案 id{@link Archive})。 */
private Long archiveId;
/** 档案标题(冗余)。 */
private String archiveTitle;
/** 版本号(V1.0 / V2.0…)。 */
private String versionNo;
/** 是否现行版本(同一档案下唯一)。 */
private Boolean isCurrent = Boolean.FALSE;
/** 状态:现行 / 作废。 */
private String status;
/** 变更说明。 */
private String changeNote;
/** 关联存储文件 idStoredFile.id,可空)。 */
private Long storedFileId;
/** 文件名。 */
private String fileName;
/** 上传/制版人。 */
private String author;
/** 版本日期(YYYY-MM-DD)。 */
private String versionDate;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getArchiveId() {
return archiveId;
}
public void setArchiveId(Long archiveId) {
this.archiveId = archiveId;
}
public String getArchiveTitle() {
return archiveTitle;
}
public void setArchiveTitle(String archiveTitle) {
this.archiveTitle = archiveTitle;
}
public String getVersionNo() {
return versionNo;
}
public void setVersionNo(String versionNo) {
this.versionNo = versionNo;
}
public Boolean getIsCurrent() {
return isCurrent;
}
public void setIsCurrent(Boolean isCurrent) {
this.isCurrent = isCurrent;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getChangeNote() {
return changeNote;
}
public void setChangeNote(String changeNote) {
this.changeNote = changeNote;
}
public Long getStoredFileId() {
return storedFileId;
}
public void setStoredFileId(Long storedFileId) {
this.storedFileId = storedFileId;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getVersionDate() {
return versionDate;
}
public void setVersionDate(String versionDate) {
this.versionDate = versionDate;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,101 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 年度审计计划(内控部·审计监察部·第1模块)。
*
* 每年由审计监察部编制,经审批后作为全年审计工作指引。
* 计划项涵盖财务审计/内控审计/经济责任审计/专项审计/IT审计等。
*
* - planNo 计划编号,自动生成 AP-yyyy-序号;
* - year 计划年度;
* - auditType 审计类型:财务审计 / 内控审计 / 经济责任审计 / 专项审计 / IT审计;
* - auditee 被审计单位/部门;
* - riskLevel 风险等级:高 / 中 / 低;
* - importance 重要性:重要 / 一般;
* - basis 列入依据(风险评估结果/管理层要求/法规要求);
* - plannedStartDate 计划开始日期 YYYY-MM-DD
* - plannedEndDate 计划结束日期;
* - leadAuditor 项目负责人;
* - teamMembers 审计组成员(逗号分隔);
* - budget 审计项目预算(差旅费+外聘专家费等),BigDecimal
* - status 状态:草稿 → 待审批 → 已批准 → 实施中 → 已完成;
* - approver 审批人;
* - approvedAt 审批时间;
* - linkedProjectId 关联的 audit_project 记录(立项后回写);
* - notes 备注。
*/
@Entity
@Table(name = "audit_annual_plan")
public class AuditAnnualPlan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String planNo;
private String year;
private String auditType;
private String auditee;
private String riskLevel;
private String importance;
private String basis;
private String plannedStartDate;
private String plannedEndDate;
private String leadAuditor;
private String teamMembers;
private BigDecimal budget = BigDecimal.ZERO;
private String status;
private String approver;
private Instant approvedAt;
private Long linkedProjectId;
private String notes;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getPlanNo() { return planNo; }
public void setPlanNo(String planNo) { this.planNo = planNo; }
public String getYear() { return year; }
public void setYear(String year) { this.year = year; }
public String getAuditType() { return auditType; }
public void setAuditType(String auditType) { this.auditType = auditType; }
public String getAuditee() { return auditee; }
public void setAuditee(String auditee) { this.auditee = auditee; }
public String getRiskLevel() { return riskLevel; }
public void setRiskLevel(String riskLevel) { this.riskLevel = riskLevel; }
public String getImportance() { return importance; }
public void setImportance(String importance) { this.importance = importance; }
public String getBasis() { return basis; }
public void setBasis(String basis) { this.basis = basis; }
public String getPlannedStartDate() { return plannedStartDate; }
public void setPlannedStartDate(String plannedStartDate) { this.plannedStartDate = plannedStartDate; }
public String getPlannedEndDate() { return plannedEndDate; }
public void setPlannedEndDate(String plannedEndDate) { this.plannedEndDate = plannedEndDate; }
public String getLeadAuditor() { return leadAuditor; }
public void setLeadAuditor(String leadAuditor) { this.leadAuditor = leadAuditor; }
public String getTeamMembers() { return teamMembers; }
public void setTeamMembers(String teamMembers) { this.teamMembers = teamMembers; }
public BigDecimal getBudget() { return budget; }
public void setBudget(BigDecimal budget) { this.budget = budget; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getApprover() { return approver; }
public void setApprover(String approver) { this.approver = approver; }
public Instant getApprovedAt() { return approvedAt; }
public void setApprovedAt(Instant approvedAt) { this.approvedAt = approvedAt; }
public Long getLinkedProjectId() { return linkedProjectId; }
public void setLinkedProjectId(Long linkedProjectId) { this.linkedProjectId = linkedProjectId; }
public String getNotes() { return notes; }
public void setNotes(String notes) { this.notes = notes; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,101 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 审计档案库(内控部·审计监察部·第9模块)。
*
* 统一审计档案归档实体:底稿/报告/证据/整改记录按年度/项目类型分类归档。
* 每条 AuditArchive 代表一个归档记录(可归档多类文件)。
*
* - archiveNo 档案编号,自动生成 AC-yyyy-序号;
* - projectId 关联审计项目(可选,专项归档可无项目);
* - projectName 冗余项目名;
* - year 档案归属年度;
* - archiveType 档案类型:底稿 / 报告 / 证据 / 整改记录 / 法规制度 / 知识总结;
* - auditType 审计类型(来自关联项目);
* - auditee 被审计单位;
* - title 档案标题;
* - description 档案描述/摘要;
* - fileRef 文件路径/ID
* - fileHash 防篡改哈希值;
* - tags 标签(逗号分隔,便于检索);
* - keywords 关键词(供简易全文检索);
* - status 状态:归档中 / 已归档 / 已销毁;
* - archiver 归档人;
* - archivedAt 归档时间;
* - retentionYear 保存年限(年,-1=永久);
* - createdAt 创建时间。
*/
@Entity
@Table(name = "audit_archive")
public class AuditArchive {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String archiveNo;
private Long projectId;
private String projectName;
private String year;
private String archiveType;
private String auditType;
private String auditee;
private String title;
private String description;
private String fileRef;
private String fileHash;
private String tags;
private String keywords;
private String status;
private String archiver;
private Instant archivedAt;
private int retentionYear;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getArchiveNo() { return archiveNo; }
public void setArchiveNo(String archiveNo) { this.archiveNo = archiveNo; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getYear() { return year; }
public void setYear(String year) { this.year = year; }
public String getArchiveType() { return archiveType; }
public void setArchiveType(String archiveType) { this.archiveType = archiveType; }
public String getAuditType() { return auditType; }
public void setAuditType(String auditType) { this.auditType = auditType; }
public String getAuditee() { return auditee; }
public void setAuditee(String auditee) { this.auditee = auditee; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getFileRef() { return fileRef; }
public void setFileRef(String fileRef) { this.fileRef = fileRef; }
public String getFileHash() { return fileHash; }
public void setFileHash(String fileHash) { this.fileHash = fileHash; }
public String getTags() { return tags; }
public void setTags(String tags) { this.tags = tags; }
public String getKeywords() { return keywords; }
public void setKeywords(String keywords) { this.keywords = keywords; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getArchiver() { return archiver; }
public void setArchiver(String archiver) { this.archiver = archiver; }
public Instant getArchivedAt() { return archivedAt; }
public void setArchivedAt(Instant archivedAt) { this.archivedAt = archivedAt; }
public int getRetentionYear() { return retentionYear; }
public void setRetentionYear(int retentionYear) { this.retentionYear = retentionYear; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,84 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 连续审计日志(内控部·审计监察部·CAATs·连续审计 @Scheduled 结果存储)。
*
* 每次调度执行后,将异常分析摘要写入本表留存:
* - runDate 运行日期(YYYY-MM-DD);
* - analysisPeriod 分析周期描述(如 "近1日");
* - module 分析模块(付款/费用/合同/库存 等);
* - anomalyCount 本次发现异常条目数;
* - totalAmount 涉及金额合计(BigDecimal);
* - detail 摘要详情(JSON 字符串或多行文本,前端展开显示);
* - riskLevel 风险等级:低/中/高;
* - handled 是否已处置(false=待审查)。
*/
@Entity
@Table(name = "audit_continuous_log")
public class AuditContinuousLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 调度运行日期(YYYY-MM-DD)。 */
private String runDate;
/** 分析周期描述(近1日 / 近7日)。 */
private String analysisPeriod;
/** 分析模块:付款 / 费用报销 / 合同 / 综合。 */
@Column(name = "analysis_module")
private String module;
/** 本次发现异常条目数0 = 无异常 */
private int anomalyCount;
/** 涉及金额合计(BigDecimal)。 */
@Column(name = "total_amount")
private BigDecimal totalAmount = BigDecimal.ZERO;
/** 异常摘要详情(可多行或 JSON)。 */
@Column(columnDefinition = "TEXT")
private String detail;
/** 风险等级:低 / 中 / 高。 */
private String riskLevel;
/** 是否已处置(false=待审查,true=已处置)。 */
@Column(name = "audit_handled")
private boolean handled = false;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getRunDate() { return runDate; }
public void setRunDate(String runDate) { this.runDate = runDate; }
public String getAnalysisPeriod() { return analysisPeriod; }
public void setAnalysisPeriod(String analysisPeriod) { this.analysisPeriod = analysisPeriod; }
public String getModule() { return module; }
public void setModule(String module) { this.module = module; }
public int getAnomalyCount() { return anomalyCount; }
public void setAnomalyCount(int anomalyCount) { this.anomalyCount = anomalyCount; }
public BigDecimal getTotalAmount() { return totalAmount; }
public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; }
public String getDetail() { return detail; }
public void setDetail(String detail) { this.detail = detail; }
public String getRiskLevel() { return riskLevel; }
public void setRiskLevel(String riskLevel) { this.riskLevel = riskLevel; }
public boolean isHandled() { return handled; }
public void setHandled(boolean handled) { this.handled = handled; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,146 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 审计证据库(内控部·审计监察部·模块2)。
*
* 上传并管理证据文件(扫描件/截图/数据导出文件),关联底稿。
* 支持证据分类、标签、关键词检索(补全 PARTIAL 缺口:证据分类/标签/全文检索)。
* 关键证据哈希防篡改。
*
* category: 财务凭证 / 合同文件 / 实物照片 / 系统截图 / 数据导出 / 会议记录 / 访谈记录 / 其他
*/
@Entity
@Table(name = "audit_evidence")
public class AuditEvidence {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联审计项目 id。 */
private Long projectId;
/** 冗余项目名称。 */
private String projectName;
/** 关联底稿 id(可空)。 */
private Long workPaperId;
/** 关联审计发现 id(可空)。 */
private Long findingId;
/** 证据编号 AE-yyyy-序号。 */
private String evidenceNo;
/** 证据标题/描述。 */
private String title;
/** 证据类别:财务凭证 / 合同文件 / 实物照片 / 系统截图 / 数据导出 / 会议记录 / 访谈记录 / 其他。 */
private String category;
/** 标签(逗号分隔,供检索)。 */
private String tags;
/** 关键词(供全文检索匹配)。 */
@Column(length = 2048)
private String keywords;
/** 文件路径/引用(通过 FileController 上传后记录路径)。 */
private String fileRef;
/** 文件名。 */
private String fileName;
/** 文件类型(pdf/jpg/png/xlsx/docx等)。 */
private String fileType;
/** 防篡改哈希值(SHA-256,关键证据必填)。 */
private String fileHash;
/** 是否关键证据(关键证据加水印/哈希校验)。 */
private boolean keyEvidence;
/** 水印文字(关键证据)。 */
private String watermark;
/** 证据说明(解释证据内容与审计结论的关联)。 */
@Column(length = 2048)
private String description;
/** 收集人。 */
private String collectedBy;
/** 收集日期。 */
private String collectDate;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public Long getWorkPaperId() { return workPaperId; }
public void setWorkPaperId(Long workPaperId) { this.workPaperId = workPaperId; }
public Long getFindingId() { return findingId; }
public void setFindingId(Long findingId) { this.findingId = findingId; }
public String getEvidenceNo() { return evidenceNo; }
public void setEvidenceNo(String evidenceNo) { this.evidenceNo = evidenceNo; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getTags() { return tags; }
public void setTags(String tags) { this.tags = tags; }
public String getKeywords() { return keywords; }
public void setKeywords(String keywords) { this.keywords = keywords; }
public String getFileRef() { return fileRef; }
public void setFileRef(String fileRef) { this.fileRef = fileRef; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public String getFileType() { return fileType; }
public void setFileType(String fileType) { this.fileType = fileType; }
public String getFileHash() { return fileHash; }
public void setFileHash(String fileHash) { this.fileHash = fileHash; }
public boolean isKeyEvidence() { return keyEvidence; }
public void setKeyEvidence(boolean keyEvidence) { this.keyEvidence = keyEvidence; }
public String getWatermark() { return watermark; }
public void setWatermark(String watermark) { this.watermark = watermark; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getCollectedBy() { return collectedBy; }
public void setCollectedBy(String collectedBy) { this.collectedBy = collectedBy; }
public String getCollectDate() { return collectDate; }
public void setCollectDate(String collectDate) { this.collectDate = collectDate; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,214 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 审计发现 / 问题登记(内控部·审计监察部)。一条 AuditFinding 代表一次审计项目中登记的
* 一个问题:findingNo 为系统自动生成的问题编号(AF-yyyy-序号),关联 {@link AuditProject}。
*
* severity 风险等级为 高 / 中 / 低 之一;amountInvolved 为涉及金额(BigDecimal)。
* status 流转 待沟通 → 已确认 → 整改中 → 已整改 / 不采纳:
* - 待沟通:刚登记,待与被审计单位沟通确认;
* - 已确认:被审计单位确认后进入整改流程,此时自动派生一条 {@link Rectification} 整改任务;
* - 整改中:整改任务进行中(由整改任务状态回写);
* - 已整改:整改验证通过(由整改任务回写);
* - 不采纳:被审计单位不认可且复核维持,问题闭环不整改。
*
* 涉及金额、违反制度、事实依据为审计底稿核心要素;rootCause 供问题根源分析归并同类问题。
*/
@Entity
@Table(name = "audit_finding")
public class AuditFinding {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 自动生成的问题编号 AF-yyyy-序号。 */
private String findingNo;
/** 关联审计项目 id。 */
private Long projectId;
/** 冗余项目名/被审计单位,便于台账与整改页直接展示,免每行回查。 */
private String projectName;
private String auditee;
/** 问题描述。 */
private String title;
/** 事实依据(审计底稿要素)。 */
private String factBasis;
/** 违反的制度 / 法规。 */
private String violatedRule;
/** 涉及金额。 */
private BigDecimal amountInvolved = BigDecimal.ZERO;
/** 风险等级:高 / 中 / 低。 */
private String severity;
/** 问题类型(用于根源分析归并:采购循环 / 资金循环 / 费用报销 / 内控缺陷 / 合规 等)。 */
private String category;
/** 根源(问题根源分析填写,识别系统性缺陷)。 */
private String rootCause;
/** 待沟通 / 已确认 / 整改中 / 已整改 / 不采纳。 */
private String status;
/** 被审计单位反馈意见(沟通确认环节)。 */
private String auditeeFeedback;
/** 派生的整改任务 id(已确认后生成,便于双向跳转)。 */
private Long rectificationId;
private String auditor;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFindingNo() {
return findingNo;
}
public void setFindingNo(String findingNo) {
this.findingNo = findingNo;
}
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public String getAuditee() {
return auditee;
}
public void setAuditee(String auditee) {
this.auditee = auditee;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getFactBasis() {
return factBasis;
}
public void setFactBasis(String factBasis) {
this.factBasis = factBasis;
}
public String getViolatedRule() {
return violatedRule;
}
public void setViolatedRule(String violatedRule) {
this.violatedRule = violatedRule;
}
public BigDecimal getAmountInvolved() {
return amountInvolved;
}
public void setAmountInvolved(BigDecimal amountInvolved) {
this.amountInvolved = amountInvolved;
}
public String getSeverity() {
return severity;
}
public void setSeverity(String severity) {
this.severity = severity;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getRootCause() {
return rootCause;
}
public void setRootCause(String rootCause) {
this.rootCause = rootCause;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getAuditeeFeedback() {
return auditeeFeedback;
}
public void setAuditeeFeedback(String auditeeFeedback) {
this.auditeeFeedback = auditeeFeedback;
}
public Long getRectificationId() {
return rectificationId;
}
public void setRectificationId(Long rectificationId) {
this.rectificationId = rectificationId;
}
public String getAuditor() {
return auditor;
}
public void setAuditor(String auditor) {
this.auditor = auditor;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,97 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 审计资料清单请求(内控部·审计监察部·第2模块)。
*
* 审计人员向被审计单位发出资料收集请求,系统跟踪每项资料的提供状态。
* 一个审计项目可发出多条资料清单请求(按资料类别分批发出)。
*
* - requestNo 请求编号,自动生成 MR-yyyy-序号;
* - projectId 关联审计项目;
* - projectName 冗余项目名;
* - auditee 被审计单位;
* - itemName 资料名称(如:2024年度财务报表/采购合同台账/...);
* - itemType 资料类型:报表 / 凭证 / 合同 / 制度 / 其他;
* - description 详细描述(格式要求/期间范围等);
* - dueDate 要求提供日期 YYYY-MM-DD
* - status 状态:待提供 → 已提供 → 验收中 → 已验收 / 已退回;
* - fileRef 上传文件引用(被审计单位上传后记录路径/ID);
* - fileHash 防篡改哈希值(SHA-256,文件上传时系统计算);
* - submittedAt 被审计单位提交时间;
* - verifiedAt 审计人员验收时间;
* - verifier 验收人;
* - rejectReason 退回原因;
* - requester 请求发出人(审计人员);
* - createdAt 创建时间。
*/
@Entity
@Table(name = "audit_material_request")
public class AuditMaterialRequest {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String requestNo;
private Long projectId;
private String projectName;
private String auditee;
private String itemName;
private String itemType;
private String description;
private String dueDate;
private String status;
private String fileRef;
private String fileHash;
private Instant submittedAt;
private Instant verifiedAt;
private String verifier;
private String rejectReason;
private String requester;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getRequestNo() { return requestNo; }
public void setRequestNo(String requestNo) { this.requestNo = requestNo; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getAuditee() { return auditee; }
public void setAuditee(String auditee) { this.auditee = auditee; }
public String getItemName() { return itemName; }
public void setItemName(String itemName) { this.itemName = itemName; }
public String getItemType() { return itemType; }
public void setItemType(String itemType) { this.itemType = itemType; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getDueDate() { return dueDate; }
public void setDueDate(String dueDate) { this.dueDate = dueDate; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getFileRef() { return fileRef; }
public void setFileRef(String fileRef) { this.fileRef = fileRef; }
public String getFileHash() { return fileHash; }
public void setFileHash(String fileHash) { this.fileHash = fileHash; }
public Instant getSubmittedAt() { return submittedAt; }
public void setSubmittedAt(Instant submittedAt) { this.submittedAt = submittedAt; }
public Instant getVerifiedAt() { return verifiedAt; }
public void setVerifiedAt(Instant verifiedAt) { this.verifiedAt = verifiedAt; }
public String getVerifier() { return verifier; }
public void setVerifier(String verifier) { this.verifier = verifier; }
public String getRejectReason() { return rejectReason; }
public void setRejectReason(String rejectReason) { this.rejectReason = rejectReason; }
public String getRequester() { return requester; }
public void setRequester(String requester) { this.requester = requester; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,111 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 审计通知书(内控部·审计监察部·审计项目实施管理)。
* 关联 {@link AuditProject},记录向被审计单位发出审计通知的全过程:
* - 生成通知书(noticeNo 自动编号 AN-yyyy-序号);
* - 被审计单位在线签收确认(signedAt 不为空且 signedBy 有值);
* - status: 待签收 → 已签收 / 已撤回。
*/
@Entity
@Table(name = "audit_notice")
public class AuditNotice {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 自动生成的通知书编号 AN-yyyy-序号。 */
private String noticeNo;
/** 关联审计项目 id。 */
private Long projectId;
/** 冗余项目名称,便于列表展示。 */
private String projectName;
/** 被审计单位名称。 */
private String auditee;
/** 审计期间。 */
private String auditPeriod;
/** 审计目的与范围(通知书正文摘要)。 */
private String purpose;
/** 拟开始日期。 */
private String plannedStartDate;
/** 主审 / 签发人。 */
private String issuer;
/** 签发日期。 */
private String issueDate;
/** 待签收 / 已签收 / 已撤回。 */
private String status;
/** 被审计单位签收人。 */
private String signedBy;
/** 签收时间。 */
private Instant signedAt;
/** 签收备注(如有异议)。 */
private String signNote;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getNoticeNo() { return noticeNo; }
public void setNoticeNo(String noticeNo) { this.noticeNo = noticeNo; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getAuditee() { return auditee; }
public void setAuditee(String auditee) { this.auditee = auditee; }
public String getAuditPeriod() { return auditPeriod; }
public void setAuditPeriod(String auditPeriod) { this.auditPeriod = auditPeriod; }
public String getPurpose() { return purpose; }
public void setPurpose(String purpose) { this.purpose = purpose; }
public String getPlannedStartDate() { return plannedStartDate; }
public void setPlannedStartDate(String plannedStartDate) { this.plannedStartDate = plannedStartDate; }
public String getIssuer() { return issuer; }
public void setIssuer(String issuer) { this.issuer = issuer; }
public String getIssueDate() { return issueDate; }
public void setIssueDate(String issueDate) { this.issueDate = issueDate; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getSignedBy() { return signedBy; }
public void setSignedBy(String signedBy) { this.signedBy = signedBy; }
public Instant getSignedAt() { return signedAt; }
public void setSignedAt(Instant signedAt) { this.signedAt = signedAt; }
public String getSignNote() { return signNote; }
public void setSignNote(String signNote) { this.signNote = signNote; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,105 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 审计程序库(内控部·审计监察部·审计档案与知识库 · 审计程序库)。
* 维护各业务循环的标准审计程序,新审计项目可引用复用,提高效率。
*
* businessCycle 业务循环:采购循环 / 销售循环 / 资金循环 / 存货循环 / 固定资产循环 / 薪酬循环 / 合规循环 / IT审计 / 其他;
* auditObjective 审计目标(完整性/准确性/合规性/存在性等);
* procedures 标准审计步骤(可为多行文本,详细描述测试程序与抽样方法);
* samplingMethod 抽样方法(随机/判断/属性/变量/其他);
* isTemplate 是否为模板程序(模板不可修改,仅可复制使用)。
*/
@Entity
@Table(name = "audit_procedure")
public class AuditProcedure {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 程序编号 AP-yyyy-序号。 */
private String procedureNo;
/** 程序名称。 */
private String name;
/** 业务循环:采购/销售/资金/存货/固定资产/薪酬/合规/IT审计/其他。 */
private String businessCycle;
/** 审计目标(完整性/准确性/合规性/存在性/切割/计价/呈报)。 */
private String auditObjective;
/** 标准审计程序步骤(多行文本)。 */
@Column(length = 8192)
private String procedures;
/** 抽样方法:随机 / 判断 / 属性 / 变量 / 系统 / 不抽样。 */
private String samplingMethod;
/** 参考法规/准则(如《内部审计准则》第X号、《企业内部控制基本规范》等)。 */
@Column(name = "reference_links", length = 1024)
private String references;
/** 是否为内置模板(true=仅可引用/复制,不可直接修改)。 */
private boolean isTemplate;
/** 维护人。 */
private String maintainedBy;
/** 最后更新日期。 */
private String lastUpdatedDate;
/** 使用次数(被新项目引用的次数,便于排序/推荐)。 */
private int usageCount;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getProcedureNo() { return procedureNo; }
public void setProcedureNo(String procedureNo) { this.procedureNo = procedureNo; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getBusinessCycle() { return businessCycle; }
public void setBusinessCycle(String businessCycle) { this.businessCycle = businessCycle; }
public String getAuditObjective() { return auditObjective; }
public void setAuditObjective(String auditObjective) { this.auditObjective = auditObjective; }
public String getProcedures() { return procedures; }
public void setProcedures(String procedures) { this.procedures = procedures; }
public String getSamplingMethod() { return samplingMethod; }
public void setSamplingMethod(String samplingMethod) { this.samplingMethod = samplingMethod; }
public String getReferences() { return references; }
public void setReferences(String references) { this.references = references; }
public boolean isTemplate() { return isTemplate; }
public void setTemplate(boolean template) { isTemplate = template; }
public String getMaintainedBy() { return maintainedBy; }
public void setMaintainedBy(String maintainedBy) { this.maintainedBy = maintainedBy; }
public String getLastUpdatedDate() { return lastUpdatedDate; }
public void setLastUpdatedDate(String lastUpdatedDate) { this.lastUpdatedDate = lastUpdatedDate; }
public int getUsageCount() { return usageCount; }
public void setUsageCount(int usageCount) { this.usageCount = usageCount; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,111 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* An audit project (审计项目) run by the audit and supervision center. auditType
* is one of 财务审计 / 工程审计 / 合规审计 / 内控评价 / 经济责任审计; status is one
* of 计划 / 实施中 / 报告 / 整改 / 已结. findings holds the count of issues found.
*/
@Entity
@Table(name = "audit_project")
public class AuditProject {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String auditType;
private String auditee;
private String leadAuditor;
private String period;
private int findings;
private String status;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuditType() {
return auditType;
}
public void setAuditType(String auditType) {
this.auditType = auditType;
}
public String getAuditee() {
return auditee;
}
public void setAuditee(String auditee) {
this.auditee = auditee;
}
public String getLeadAuditor() {
return leadAuditor;
}
public void setLeadAuditor(String leadAuditor) {
this.leadAuditor = leadAuditor;
}
public String getPeriod() {
return period;
}
public void setPeriod(String period) {
this.period = period;
}
public int getFindings() {
return findings;
}
public void setFindings(int findings) {
this.findings = findings;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,104 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 审计质量复核(内控部·审计监察部·第10模块)。
*
* 审计项目完成后,指定独立复核人进行质量检查,包括:
* - 审计程序执行情况;
* - 证据充分性;
* - 底稿规范性。
* 复核结论记录在此实体。
*
* - reviewNo 复核编号,自动生成 QR-yyyy-序号;
* - projectId 关联审计项目;
* - projectName 冗余项目名;
* - reviewer 独立复核人(须非项目组成员);
* - reviewDate 复核日期 YYYY-MM-DD
* - procedureScore 程序执行评分(0-100);
* - evidenceScore 证据充分性评分(0-100);
* - workpaperScore 底稿规范性评分(0-100);
* - overallScore 综合评分(自动加权平均);
* - overallResult 复核结论:优良 / 合格 / 需改进 / 不合格;
* - procedureComment 程序执行意见;
* - evidenceComment 证据充分性意见;
* - workpaperComment 底稿规范性意见;
* - overallComment 综合复核意见;
* - improvementRequired 是否需要整改(true=需);
* - improvementItems 需改进事项(换行分隔);
* - status 状态:草稿 → 复核中 → 已完成;
* - createdAt 创建时间。
*/
@Entity
@Table(name = "audit_quality_review")
public class AuditQualityReview {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String reviewNo;
private Long projectId;
private String projectName;
private String reviewer;
private String reviewDate;
private int procedureScore;
private int evidenceScore;
private int workpaperScore;
private int overallScore;
private String overallResult;
private String procedureComment;
private String evidenceComment;
private String workpaperComment;
private String overallComment;
private boolean improvementRequired;
private String improvementItems;
private String status;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getReviewNo() { return reviewNo; }
public void setReviewNo(String reviewNo) { this.reviewNo = reviewNo; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getReviewer() { return reviewer; }
public void setReviewer(String reviewer) { this.reviewer = reviewer; }
public String getReviewDate() { return reviewDate; }
public void setReviewDate(String reviewDate) { this.reviewDate = reviewDate; }
public int getProcedureScore() { return procedureScore; }
public void setProcedureScore(int procedureScore) { this.procedureScore = procedureScore; }
public int getEvidenceScore() { return evidenceScore; }
public void setEvidenceScore(int evidenceScore) { this.evidenceScore = evidenceScore; }
public int getWorkpaperScore() { return workpaperScore; }
public void setWorkpaperScore(int workpaperScore) { this.workpaperScore = workpaperScore; }
public int getOverallScore() { return overallScore; }
public void setOverallScore(int overallScore) { this.overallScore = overallScore; }
public String getOverallResult() { return overallResult; }
public void setOverallResult(String overallResult) { this.overallResult = overallResult; }
public String getProcedureComment() { return procedureComment; }
public void setProcedureComment(String procedureComment) { this.procedureComment = procedureComment; }
public String getEvidenceComment() { return evidenceComment; }
public void setEvidenceComment(String evidenceComment) { this.evidenceComment = evidenceComment; }
public String getWorkpaperComment() { return workpaperComment; }
public void setWorkpaperComment(String workpaperComment) { this.workpaperComment = workpaperComment; }
public String getOverallComment() { return overallComment; }
public void setOverallComment(String overallComment) { this.overallComment = overallComment; }
public boolean isImprovementRequired() { return improvementRequired; }
public void setImprovementRequired(boolean improvementRequired) { this.improvementRequired = improvementRequired; }
public String getImprovementItems() { return improvementItems; }
public void setImprovementItems(String improvementItems) { this.improvementItems = improvementItems; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,164 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 审计报告(内控部·审计监察部·模块2)。
*
* 基于审计底稿和问题清单自动生成审计报告,支持自定义报告模板,
* 报告经审批后发送至管理层和被审计单位。
*
* 状态机:草稿 → 待审批 → 已审批 → 已发送
* reportNo 格式:AR-yyyy-序号
*/
@Entity
@Table(name = "audit_report")
public class AuditReport {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 报告编号 AR-yyyy-序号。 */
private String reportNo;
/** 关联审计项目 id。 */
private Long projectId;
/** 冗余项目名称。 */
private String projectName;
/** 被审计单位。 */
private String auditee;
/** 审计类型。 */
private String auditType;
/** 审计期间。 */
private String auditPeriod;
/** 报告标题。 */
private String title;
/** 报告模板名称。 */
private String templateName;
/** 审计结论(简要摘要)。 */
@Column(length = 4096)
private String conclusion;
/** 主要审计发现(自动从 AuditFinding 聚合或手动填写)。 */
@Column(length = 8192)
private String majorFindings;
/** 整改建议。 */
@Column(length = 4096)
private String recommendations;
/** 关联的问题编号列表(逗号分隔,如 AF-2026-001,AF-2026-002)。 */
@Column(length = 2048)
private String findingNos;
/** 关联的底稿编号列表(逗号分隔)。 */
@Column(length = 2048)
private String paperNos;
/** 报告编制人。 */
private String preparedBy;
/** 审批人。 */
private String approver;
/** 审批意见。 */
@Column(length = 1024)
private String approveComment;
/** 审批时间。 */
private Instant approvedAt;
/** 发送时间。 */
private Instant sentAt;
/** 接收人(管理层+被审计单位,逗号分隔)。 */
private String recipients;
/**
* 状态:草稿 / 待审批 / 已审批 / 已发送。
*/
private String status;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getReportNo() { return reportNo; }
public void setReportNo(String reportNo) { this.reportNo = reportNo; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getAuditee() { return auditee; }
public void setAuditee(String auditee) { this.auditee = auditee; }
public String getAuditType() { return auditType; }
public void setAuditType(String auditType) { this.auditType = auditType; }
public String getAuditPeriod() { return auditPeriod; }
public void setAuditPeriod(String auditPeriod) { this.auditPeriod = auditPeriod; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getTemplateName() { return templateName; }
public void setTemplateName(String templateName) { this.templateName = templateName; }
public String getConclusion() { return conclusion; }
public void setConclusion(String conclusion) { this.conclusion = conclusion; }
public String getMajorFindings() { return majorFindings; }
public void setMajorFindings(String majorFindings) { this.majorFindings = majorFindings; }
public String getRecommendations() { return recommendations; }
public void setRecommendations(String recommendations) { this.recommendations = recommendations; }
public String getFindingNos() { return findingNos; }
public void setFindingNos(String findingNos) { this.findingNos = findingNos; }
public String getPaperNos() { return paperNos; }
public void setPaperNos(String paperNos) { this.paperNos = paperNos; }
public String getPreparedBy() { return preparedBy; }
public void setPreparedBy(String preparedBy) { this.preparedBy = preparedBy; }
public String getApprover() { return approver; }
public void setApprover(String approver) { this.approver = approver; }
public String getApproveComment() { return approveComment; }
public void setApproveComment(String approveComment) { this.approveComment = approveComment; }
public Instant getApprovedAt() { return approvedAt; }
public void setApprovedAt(Instant approvedAt) { this.approvedAt = approvedAt; }
public Instant getSentAt() { return sentAt; }
public void setSentAt(Instant sentAt) { this.sentAt = sentAt; }
public String getRecipients() { return recipients; }
public void setRecipients(String recipients) { this.recipients = recipients; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,144 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 风险管理有效性审计评估(内控部·审计监察部·模块4)。
*
* 补全 PARTIAL 缺口:审计人员评估风险应对措施有效性,记录评估结论;
* 发现风险管理缺陷时自动生成 AuditFinding 问题(通过 AuditRiskAuditController 联动)。
*
* 关联 RiskAssessment(法务合规风险库)中的风险条目,进行审计侧有效性评估。
* status: 待评估 / 评估中 / 有效 / 部分有效 / 无效(→ 自动生成问题)
*/
@Entity
@Table(name = "audit_risk_assessment")
public class AuditRiskAssessment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 评估编号 ARA-yyyy-序号。 */
private String assessNo;
/** 关联审计项目 id。 */
private Long projectId;
/** 冗余项目名称。 */
private String projectName;
/** 关联风险库中的风险 idlegal_risk_assessment)。 */
private Long riskId;
/** 冗余风险名称/描述。 */
private String riskName;
/** 风险类别(冗余自风险库)。 */
private String riskCategory;
/** 风险等级(来自风险库:低/中/高/重大)。 */
private String riskLevel;
/** 责任部门(冗余自风险库)。 */
private String responsibleDept;
/** 原定应对措施(冗余自风险库)。 */
@Column(length = 2048)
private String originalMeasure;
/** 审计评估结论:有效 / 部分有效 / 无效。 */
private String effectiveness;
/** 评估依据/发现的缺陷描述。 */
@Column(length = 4096)
private String assessmentDetail;
/** 整改建议。 */
@Column(length = 2048)
private String recommendation;
/** 评估人(审计员)。 */
private String assessor;
/** 评估日期(YYYY-MM-DD)。 */
private String assessDate;
/**
* 状态:待评估 / 评估中 / 有效 / 部分有效 / 无效。
* "无效"时自动生成 AuditFindingfindingId 回填。
*/
private String status;
/** 联动生成的审计发现 id(无效时自动派生)。 */
private Long findingId;
/** 联动生成的审计发现编号(冗余)。 */
private String findingNo;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getAssessNo() { return assessNo; }
public void setAssessNo(String assessNo) { this.assessNo = assessNo; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public Long getRiskId() { return riskId; }
public void setRiskId(Long riskId) { this.riskId = riskId; }
public String getRiskName() { return riskName; }
public void setRiskName(String riskName) { this.riskName = riskName; }
public String getRiskCategory() { return riskCategory; }
public void setRiskCategory(String riskCategory) { this.riskCategory = riskCategory; }
public String getRiskLevel() { return riskLevel; }
public void setRiskLevel(String riskLevel) { this.riskLevel = riskLevel; }
public String getResponsibleDept() { return responsibleDept; }
public void setResponsibleDept(String responsibleDept) { this.responsibleDept = responsibleDept; }
public String getOriginalMeasure() { return originalMeasure; }
public void setOriginalMeasure(String originalMeasure) { this.originalMeasure = originalMeasure; }
public String getEffectiveness() { return effectiveness; }
public void setEffectiveness(String effectiveness) { this.effectiveness = effectiveness; }
public String getAssessmentDetail() { return assessmentDetail; }
public void setAssessmentDetail(String assessmentDetail) { this.assessmentDetail = assessmentDetail; }
public String getRecommendation() { return recommendation; }
public void setRecommendation(String recommendation) { this.recommendation = recommendation; }
public String getAssessor() { return assessor; }
public void setAssessor(String assessor) { this.assessor = assessor; }
public String getAssessDate() { return assessDate; }
public void setAssessDate(String assessDate) { this.assessDate = assessDate; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Long getFindingId() { return findingId; }
public void setFindingId(Long findingId) { this.findingId = findingId; }
public String getFindingNo() { return findingNo; }
public void setFindingNo(String findingNo) { this.findingNo = findingNo; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,144 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 审计工作底稿(内控部·审计监察部·审计项目实施管理)。
* 每条底稿关联 {@link AuditProject},支持文字/表格/附件内容,
* 版本受控(version 自增)、修改留痕(lastModifiedAt/lastModifiedBy),
* 审计组长复核状态机(草稿→待复核→复核通过/退回)。
*
* 关键流程:
* - 审计员 POST 新建底稿(草稿);
* - PATCH /{id}/submit-review 提交审计组长复核(→ 待复核);
* - PATCH /{id}/review 组长复核:通过(→ 复核通过)/ 退回(→ 草稿,退回意见填 reviewComment);
* - PATCH /{id}/revise 修改底稿(版本号自增,旧版本记录快照摘要到 versionHistory),
* 状态回草稿,需重新提交复核。
*/
@Entity
@Table(name = "audit_work_paper")
public class AuditWorkPaper {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 自动编号 WP-yyyy-序号。 */
private String paperNo;
/** 关联审计项目 id。 */
private Long projectId;
/** 冗余项目名称,便于列表展示。 */
private String projectName;
/** 底稿标题。 */
private String title;
/** 底稿类型(如:采购循环测试/资金循环测试/内控评估/固定资产盘点/其他)。 */
private String paperType;
/** 编制人。 */
private String preparedBy;
/** 底稿内容(文字 + 嵌入表格说明,附件通过 FileController 关联)。 */
@Column(length = 8192)
private String content;
/** 关联审计发现 id(可空,底稿可先建,后关联发现)。 */
private Long findingId;
/**
* 当前版本号,从 1 开始。PATCH /revise 时自增,旧版快照摘要追加到 versionHistory。
*/
private int version;
/**
* 版本历史快照:格式 "V1: 标题(修改者, 时间)\nV2: ..." 简易追加留痕。
* 完整历史如需查看可接 FileController 附件或 Archive 归档。
*/
@Column(length = 4096)
private String versionHistory;
/** 草稿 / 待复核 / 复核通过 / 退回。 */
private String status;
/** 复核人(审计组长)。 */
private String reviewer;
/** 复核意见(通过/退回说明)。 */
@Column(length = 2048)
private String reviewComment;
/** 复核时间。 */
private Instant reviewedAt;
/** 最后修改人。 */
private String lastModifiedBy;
/** 最后修改时间(修改留痕)。 */
private Instant lastModifiedAt;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getPaperNo() { return paperNo; }
public void setPaperNo(String paperNo) { this.paperNo = paperNo; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getPaperType() { return paperType; }
public void setPaperType(String paperType) { this.paperType = paperType; }
public String getPreparedBy() { return preparedBy; }
public void setPreparedBy(String preparedBy) { this.preparedBy = preparedBy; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public Long getFindingId() { return findingId; }
public void setFindingId(Long findingId) { this.findingId = findingId; }
public int getVersion() { return version; }
public void setVersion(int version) { this.version = version; }
public String getVersionHistory() { return versionHistory; }
public void setVersionHistory(String versionHistory) { this.versionHistory = versionHistory; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getReviewer() { return reviewer; }
public void setReviewer(String reviewer) { this.reviewer = reviewer; }
public String getReviewComment() { return reviewComment; }
public void setReviewComment(String reviewComment) { this.reviewComment = reviewComment; }
public Instant getReviewedAt() { return reviewedAt; }
public void setReviewedAt(Instant reviewedAt) { this.reviewedAt = reviewedAt; }
public String getLastModifiedBy() { return lastModifiedBy; }
public void setLastModifiedBy(String lastModifiedBy) { this.lastModifiedBy = lastModifiedBy; }
public Instant getLastModifiedAt() { return lastModifiedAt; }
public void setLastModifiedAt(Instant lastModifiedAt) { this.lastModifiedAt = lastModifiedAt; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,131 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 审计人员资质专长档案(内控部·审计监察部·模块1 审计计划与资源管理)。
*
* 补全 PARTIAL 缺口:
* 「仅靠 teamMembers 字符串存储,无可查资质库;审计人员资质/专长/可用时间无独立实体」。
*
* 字段说明:
* - employeeNo 工号(可关联 HR 模块)
* - name 姓名(唯一)
* - auditorType 审计师类型:内部审计师 / 外部专家 / 实习审计师
* - specialties 专长方向(逗号分隔):财务审计/内控审计/IT审计/工程审计/舞弊调查 等
* - certifications 资质证书(逗号分隔):CPA/CIA/CISA/CFE/注册造价师 等
* - availableFrom 可用起始日(YYYY-MM-DD
* - availableTo 可用截止日(YYYY-MM-DD
* - workloadPct 当前工作负荷百分比(0-100);100=已满,<100=可接项目
* - currentProject 当前承接项目编号(可空)
* - interestConflictUnits 曾任职/有亲属关系的被审计单位(逗号分隔,独立性校验用)
* - contactEmail 联系邮箱
* - status 在职 / 停用
* - remark 备注
*/
@Entity
@Table(name = "audit_auditor_profile")
public class AuditorProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 工号(唯一)。 */
@Column(name = "employee_no", unique = true)
private String employeeNo;
/** 姓名。 */
private String name;
/** 审计师类型:内部审计师 / 外部专家 / 实习审计师。 */
private String auditorType;
/** 专长方向(逗号分隔)。 */
private String specialties;
/** 资质证书(逗号分隔)。 */
private String certifications;
/** 可用起始日(YYYY-MM-DD)。 */
private String availableFrom;
/** 可用截止日(YYYY-MM-DD)。 */
private String availableTo;
/** 当前工作负荷百分比(0-100)。 */
private Integer workloadPct;
/** 当前承接项目编号(可空)。 */
private String currentProject;
/**
* 曾任职/有亲属关系的被审计单位(逗号分隔)。
* 独立性校验:若派往与此字段有重叠的被审计单位,系统提示利益冲突。
*/
@Column(name = "interest_conflict_units")
private String interestConflictUnits;
/** 联系邮箱。 */
private String contactEmail;
/** 在职 / 停用。 */
private String status;
/** 备注。 */
private String remark;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getEmployeeNo() { return employeeNo; }
public void setEmployeeNo(String employeeNo) { this.employeeNo = employeeNo; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getAuditorType() { return auditorType; }
public void setAuditorType(String auditorType) { this.auditorType = auditorType; }
public String getSpecialties() { return specialties; }
public void setSpecialties(String specialties) { this.specialties = specialties; }
public String getCertifications() { return certifications; }
public void setCertifications(String certifications) { this.certifications = certifications; }
public String getAvailableFrom() { return availableFrom; }
public void setAvailableFrom(String availableFrom) { this.availableFrom = availableFrom; }
public String getAvailableTo() { return availableTo; }
public void setAvailableTo(String availableTo) { this.availableTo = availableTo; }
public Integer getWorkloadPct() { return workloadPct; }
public void setWorkloadPct(Integer workloadPct) { this.workloadPct = workloadPct; }
public String getCurrentProject() { return currentProject; }
public void setCurrentProject(String currentProject) { this.currentProject = currentProject; }
public String getInterestConflictUnits() { return interestConflictUnits; }
public void setInterestConflictUnits(String interestConflictUnits) { this.interestConflictUnits = interestConflictUnits; }
public String getContactEmail() { return contactEmail; }
public void setContactEmail(String contactEmail) { this.contactEmail = contactEmail; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,130 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.time.Instant;
/**
* H16 流程办结自动联动台账 (automation_log). Each row records one downstream
* effect fired by the workflow engine when a form instance reached 办结
* (TriggerRuleEngine). Together with the (instanceId, ruleKey) idempotency key
* this guarantees a rule fires at most once per instance even on a replayed
* 办结 transition.
*
* action examples: 合同生效 / 生成待付付款单 / 回写合同已付款 / 新建项目 / 新建用印台账 / 新建供应商。
* targetType is the downstream entity table (contract / payment / project /
* seal_use / supplier); targetId is the affected row's id (may be null when the
* rule was skipped because no matching主数据 was found). detail carries a
* human-readable note; status (when present in detail) distinguishes 成功/跳过.
*/
@Entity
@Table(name = "automation_log",
uniqueConstraints = @UniqueConstraint(
name = "uk_automation_instance_rule",
columnNames = { "instanceId", "ruleKey" }))
public class AutomationLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** The办结 form instance that fired this rule. */
private Long instanceId;
/** Snapshot of the instance title at trigger time (for ledger display). */
private String instanceTitle;
/** Stable rule identifier — half of the (instanceId, ruleKey) idempotency key. */
private String ruleKey;
/** What the engine did downstream (合同生效 / 生成待付付款单 / 新建项目 …). */
private String action;
/** Downstream entity table: contract / payment / project / seal_use / supplier. */
private String targetType;
/** Affected downstream row id (null when skipped — no matching主数据). */
private Long targetId;
/** Human-readable detail / outcome note. */
private String detail;
private Instant firedAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getInstanceId() {
return instanceId;
}
public void setInstanceId(Long instanceId) {
this.instanceId = instanceId;
}
public String getInstanceTitle() {
return instanceTitle;
}
public void setInstanceTitle(String instanceTitle) {
this.instanceTitle = instanceTitle;
}
public String getRuleKey() {
return ruleKey;
}
public void setRuleKey(String ruleKey) {
this.ruleKey = ruleKey;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getTargetType() {
return targetType;
}
public void setTargetType(String targetType) {
this.targetType = targetType;
}
public Long getTargetId() {
return targetId;
}
public void setTargetId(Long targetId) {
this.targetId = targetId;
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
public Instant getFiredAt() {
return firedAt;
}
public void setFiredAt(Instant firedAt) {
this.firedAt = firedAt;
}
}
@@ -0,0 +1,192 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
/**
* 银行账户台账(支付中心·银行账户与资金池管理)。
* 统一管理集团及各分子公司在各家银行开立的所有账户(基本户/一般户/专户/外币户/保证金户)。
* currency 默认人民币。status 正常/冻结/销户。
* balance/frozenAmount/inTransitAmount 由每日余额同步或手工录入更新。
* minReserve 最低备付金,低于此值系统自动预警。
*/
@Entity
@Table(name = "bank_account")
public class BankAccount {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String accountName;
private String bankName;
private String accountNo;
private String companySubject;
private String currency;
private String purpose;
/** 账户类型:基本户 / 一般户 / 专户 / 外币户 / 保证金户。 */
private String accountType;
/** 账户状态:正常 / 冻结 / 销户。 */
private String status;
/** 授权人员(逗号分隔)。 */
private String authPersonnel;
/** 开户日期(YYYY-MM-DD)。 */
private String openDate;
/** 可用余额(元)。 */
private BigDecimal balance = BigDecimal.ZERO;
/** 冻结金额(元)。 */
private BigDecimal frozenAmount = BigDecimal.ZERO;
/** 在途资金(元)。 */
private BigDecimal inTransitAmount = BigDecimal.ZERO;
/** 最低备付金(元,低于此值预警)。 */
private BigDecimal minReserve = BigDecimal.ZERO;
/** 余额最后同步时间(YYYY-MM-DD HH:mm)。 */
private String lastSyncTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public String getBankName() {
return bankName;
}
public void setBankName(String bankName) {
this.bankName = bankName;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public String getCompanySubject() {
return companySubject;
}
public void setCompanySubject(String companySubject) {
this.companySubject = companySubject;
}
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
public String getPurpose() {
return purpose;
}
public void setPurpose(String purpose) {
this.purpose = purpose;
}
public String getAccountType() {
return accountType;
}
public void setAccountType(String accountType) {
this.accountType = accountType;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getAuthPersonnel() {
return authPersonnel;
}
public void setAuthPersonnel(String authPersonnel) {
this.authPersonnel = authPersonnel;
}
public String getOpenDate() {
return openDate;
}
public void setOpenDate(String openDate) {
this.openDate = openDate;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public BigDecimal getFrozenAmount() {
return frozenAmount;
}
public void setFrozenAmount(BigDecimal frozenAmount) {
this.frozenAmount = frozenAmount;
}
public BigDecimal getInTransitAmount() {
return inTransitAmount;
}
public void setInTransitAmount(BigDecimal inTransitAmount) {
this.inTransitAmount = inTransitAmount;
}
public BigDecimal getMinReserve() {
return minReserve;
}
public void setMinReserve(BigDecimal minReserve) {
this.minReserve = minReserve;
}
public String getLastSyncTime() {
return lastSyncTime;
}
public void setLastSyncTime(String lastSyncTime) {
this.lastSyncTime = lastSyncTime;
}
}
@@ -0,0 +1,173 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 银行对账单(结算中心·银行对账)。一条 BankReconciliation 代表某账户某对账日的一次对账:
* bankBalance 为银行流水余额(来自银行对账单 / 银企直连),bookBalance 为账面余额;
* diff 为差额(银行余额 - 账面余额,由后端 {@link com.kaidi.oa.common.Money} 自动算并落库);
* status 为 未对平 / 已对平(diff 为 0 时即对平)。
* 银企直连自动取数为外部对接项,本表先支持手工 / 导入登记。
*/
@Entity
@Table(name = "stl_bank_reconciliation")
public class BankReconciliation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
/** 银行账户名称 / 户名。 */
private String bankAccount;
/** 银行账号。 */
private String bankAccountNo;
/** 开户行。 */
private String bankName;
/** 对账日(YYYY-MM-DD)。 */
private String reconcileDate;
/** 银行流水余额。 */
private BigDecimal bankBalance = BigDecimal.ZERO;
/** 账面余额。 */
private BigDecimal bookBalance = BigDecimal.ZERO;
/** 差额 = 银行余额 - 账面余额(后端自动算)。 */
private BigDecimal diff = BigDecimal.ZERO;
/** 未对平 / 已对平。 */
private String status;
/** 差异说明 / 调节项备注。 */
private String remark;
private String companySubject;
private String owner;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getBankAccount() {
return bankAccount;
}
public void setBankAccount(String bankAccount) {
this.bankAccount = bankAccount;
}
public String getBankAccountNo() {
return bankAccountNo;
}
public void setBankAccountNo(String bankAccountNo) {
this.bankAccountNo = bankAccountNo;
}
public String getBankName() {
return bankName;
}
public void setBankName(String bankName) {
this.bankName = bankName;
}
public String getReconcileDate() {
return reconcileDate;
}
public void setReconcileDate(String reconcileDate) {
this.reconcileDate = reconcileDate;
}
public BigDecimal getBankBalance() {
return bankBalance;
}
public void setBankBalance(BigDecimal bankBalance) {
this.bankBalance = bankBalance;
}
public BigDecimal getBookBalance() {
return bookBalance;
}
public void setBookBalance(BigDecimal bookBalance) {
this.bookBalance = bookBalance;
}
public BigDecimal getDiff() {
return diff;
}
public void setDiff(BigDecimal diff) {
this.diff = diff;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getCompanySubject() {
return companySubject;
}
public void setCompanySubject(String companySubject) {
this.companySubject = companySubject;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,212 @@
package com.kaidi.oa.domain;
import java.math.BigDecimal;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* A bidding project (投标项目) covering the full tender lifecycle.
* projectType is one of 房建 / 市政 / 公路 / EPC / 咨询 / 环保设备 / 污水运营;
* bidMethod is one of 公开招标 / 邀请招标 / 竞争性谈判; status is one of
* 投标准备 / 已投标 / 中标 / 未中标 / 废标. May link to a CRM opportunity
* (opportunityId) before winning, and to a project (projectId) after winning.
*/
@Entity
@Table(name = "bid")
public class Bid {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String projectName;
private String bidNo;
private String tenderee;
private String projectType;
private BigDecimal controlPrice = BigDecimal.ZERO;
private BigDecimal bidAmount = BigDecimal.ZERO;
private BigDecimal deposit = BigDecimal.ZERO;
private String bidMethod;
private String status;
private String bidDate;
private String openDate;
private Long opportunityId;
private Long projectId;
private String owner;
private String orgUnit;
/**
* 招标文件附件 ID(关联 StoredFile.id),办事处上传招标文件后直接回填到投标记录,
* 解决"上传招标文件后只能分开引用"的缺口。物理列名 tender_file_id 规避 SQLite
* 保留字冲突。
*/
@Column(name = "tender_file_id")
private Long tenderFileId;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public String getBidNo() {
return bidNo;
}
public void setBidNo(String bidNo) {
this.bidNo = bidNo;
}
public String getTenderee() {
return tenderee;
}
public void setTenderee(String tenderee) {
this.tenderee = tenderee;
}
public String getProjectType() {
return projectType;
}
public void setProjectType(String projectType) {
this.projectType = projectType;
}
public BigDecimal getControlPrice() {
return controlPrice;
}
public void setControlPrice(BigDecimal controlPrice) {
this.controlPrice = controlPrice;
}
public BigDecimal getBidAmount() {
return bidAmount;
}
public void setBidAmount(BigDecimal bidAmount) {
this.bidAmount = bidAmount;
}
public BigDecimal getDeposit() {
return deposit;
}
public void setDeposit(BigDecimal deposit) {
this.deposit = deposit;
}
public String getBidMethod() {
return bidMethod;
}
public void setBidMethod(String bidMethod) {
this.bidMethod = bidMethod;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getBidDate() {
return bidDate;
}
public void setBidDate(String bidDate) {
this.bidDate = bidDate;
}
public String getOpenDate() {
return openDate;
}
public void setOpenDate(String openDate) {
this.openDate = openDate;
}
public Long getOpportunityId() {
return opportunityId;
}
public void setOpportunityId(Long opportunityId) {
this.opportunityId = opportunityId;
}
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getOrgUnit() {
return orgUnit;
}
public void setOrgUnit(String orgUnit) {
this.orgUnit = orgUnit;
}
public Long getTenderFileId() {
return tenderFileId;
}
public void setTenderFileId(Long tenderFileId) {
this.tenderFileId = tenderFileId;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,169 @@
package com.kaidi.oa.domain;
import java.math.BigDecimal;
import java.time.Instant;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* 投标保证金台账(招投标与合同管理:记录投标保证金缴纳与退还)。
* 状态机 status: 待缴纳 → 已缴纳 → 待退还 → 已退还;或 已缴纳/待退还 → 已没收。
* 关联投标项目(bidId);占用资金 = 处于 已缴纳/待退还 状态的保证金累计(控制器聚合)。
* expectRefundDate 预计退还日,退还超期由控制器按 ChronoUnit 算逾期天数并预警。
*/
@Entity
@Table(name = "bid_bond")
public class BidBond {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联投标项目。 */
private Long bidId;
private String projectName;
/** 招标人/收款方。 */
private String payee;
private BigDecimal amount = BigDecimal.ZERO;
/** 缴纳方式:电汇 / 保函 / 保险 / 现金。 */
private String payMethod;
/** 待缴纳 / 已缴纳 / 待退还 / 已退还 / 已没收。 */
private String status;
private String payDate;
/** 预计退还日,YYYY-MM-DD。 */
private String expectRefundDate;
/** 实际退还日,YYYY-MM-DD。 */
private String refundDate;
private String owner;
private String orgUnit;
private String remark;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getBidId() {
return bidId;
}
public void setBidId(Long bidId) {
this.bidId = bidId;
}
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public String getPayee() {
return payee;
}
public void setPayee(String payee) {
this.payee = payee;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getPayMethod() {
return payMethod;
}
public void setPayMethod(String payMethod) {
this.payMethod = payMethod;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getPayDate() {
return payDate;
}
public void setPayDate(String payDate) {
this.payDate = payDate;
}
public String getExpectRefundDate() {
return expectRefundDate;
}
public void setExpectRefundDate(String expectRefundDate) {
this.expectRefundDate = expectRefundDate;
}
public String getRefundDate() {
return refundDate;
}
public void setRefundDate(String refundDate) {
this.refundDate = refundDate;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getOrgUnit() {
return orgUnit;
}
public void setOrgUnit(String orgUnit) {
this.orgUnit = orgUnit;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,183 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 投标决策会(需求 §3「投标决策与任务分配」)。一次投标(bidId)对应一场决策会,
* 技术/商务/成本/法务在线评审,记录结论。
* decision:投标 / 不投标(弃标);
* status:待决策 → 已决策(决策通过后由控制器自动生成投标任务清单 BidTask)。
* 各维度意见以独立字段留痕(技术/商务/成本/法务),attendees 记录参会人。
*/
@Entity
@Table(name = "bid_decision")
public class BidDecision {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联投标。 */
private Long bidId;
private String bidNo;
private String projectName;
/** 决策会日期(YYYY-MM-DD)。 */
private String meetingDate;
/** 参会人(逗号分隔)。 */
private String attendees;
/** 技术评审意见。 */
private String techOpinion;
/** 商务评审意见。 */
private String bizOpinion;
/** 成本评审意见。 */
private String costOpinion;
/** 法务评审意见。 */
private String legalOpinion;
/** 投标 / 不投标。 */
private String decision;
/** 待决策 / 已决策。 */
private String status;
/** 已生成任务清单(避免重复生成)。 */
private Boolean tasksGenerated = Boolean.FALSE;
private String owner;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getBidId() {
return bidId;
}
public void setBidId(Long bidId) {
this.bidId = bidId;
}
public String getBidNo() {
return bidNo;
}
public void setBidNo(String bidNo) {
this.bidNo = bidNo;
}
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public String getMeetingDate() {
return meetingDate;
}
public void setMeetingDate(String meetingDate) {
this.meetingDate = meetingDate;
}
public String getAttendees() {
return attendees;
}
public void setAttendees(String attendees) {
this.attendees = attendees;
}
public String getTechOpinion() {
return techOpinion;
}
public void setTechOpinion(String techOpinion) {
this.techOpinion = techOpinion;
}
public String getBizOpinion() {
return bizOpinion;
}
public void setBizOpinion(String bizOpinion) {
this.bizOpinion = bizOpinion;
}
public String getCostOpinion() {
return costOpinion;
}
public void setCostOpinion(String costOpinion) {
this.costOpinion = costOpinion;
}
public String getLegalOpinion() {
return legalOpinion;
}
public void setLegalOpinion(String legalOpinion) {
this.legalOpinion = legalOpinion;
}
public String getDecision() {
return decision;
}
public void setDecision(String decision) {
this.decision = decision;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Boolean getTasksGenerated() {
return tasksGenerated;
}
public void setTasksGenerated(Boolean tasksGenerated) {
this.tasksGenerated = tasksGenerated;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,192 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
/**
* 投标保证金台账(需求 §3「投标保证金管理」)。每笔保证金对应一次投标(bidId)。
* 生命周期 status:待缴纳 → 已缴纳 → 已退还(中标转履约保证金 / 未中标全额退)/ 逾期未退。
* payMethod:银行转账 / 银行保函 / 电子保函 / 现金。
* dueRefundDate 为约定退还日;逾期未退由预警接口分级催办。
* 缴纳时在资金支付中心生成一张「投标保证金」付款单(paymentId),退还时回写 refundDate。
*/
@Entity
@Table(name = "bid_deposit")
public class BidDeposit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
/** 关联投标。 */
private Long bidId;
private String bidNo;
private String projectName;
/** 招标人 / 收款机构。 */
private String tenderee;
private BigDecimal amount = BigDecimal.ZERO;
/** 银行转账 / 银行保函 / 电子保函 / 现金。 */
private String payMethod;
/** 待缴纳 / 已缴纳 / 已退还 / 逾期未退。 */
private String status;
/** 缴纳日期(YYYY-MM-DD)。 */
private String payDate;
/** 约定退还日期(YYYY-MM-DD),用于逾期催办分级。 */
private String dueRefundDate;
/** 实际退还日期(YYYY-MM-DD)。 */
private String refundDate;
/** 缴纳时生成的资金支付中心付款单 id(联动结算/支付链)。 */
private Long paymentId;
private String owner;
private String remark;
private Instant createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Long getBidId() {
return bidId;
}
public void setBidId(Long bidId) {
this.bidId = bidId;
}
public String getBidNo() {
return bidNo;
}
public void setBidNo(String bidNo) {
this.bidNo = bidNo;
}
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public String getTenderee() {
return tenderee;
}
public void setTenderee(String tenderee) {
this.tenderee = tenderee;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getPayMethod() {
return payMethod;
}
public void setPayMethod(String payMethod) {
this.payMethod = payMethod;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getPayDate() {
return payDate;
}
public void setPayDate(String payDate) {
this.payDate = payDate;
}
public String getDueRefundDate() {
return dueRefundDate;
}
public void setDueRefundDate(String dueRefundDate) {
this.dueRefundDate = dueRefundDate;
}
public String getRefundDate() {
return refundDate;
}
public void setRefundDate(String refundDate) {
this.refundDate = refundDate;
}
public Long getPaymentId() {
return paymentId;
}
public void setPaymentId(Long paymentId) {
this.paymentId = paymentId;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,105 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 投标文件版本协同台账(需求 §3「投标文件编制」)。
* 支持版本控制、在线批注、多人协同编制商务标/技术标/报价单。
*
* docPart: 商务标 / 技术标 / 报价单 / 资质材料 / 其他
* status: 草稿 / 审核中 / 已定稿 / 已驳回
*/
@Entity
@Table(name = "bid_doc_version")
public class BidDocVersion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 关联投标 bid.id。 */
private Long bidId;
/** 文件组成部分:商务标 / 技术标 / 报价单 / 资质材料 / 其他。 */
private String docPart;
/** 版本号(v1, v2, v1.1 等)。 */
private String version;
/** 文件标题。 */
private String docTitle;
/** 文件引用(上传后获得的 StoredFile 引用 id 或路径)。 */
private String fileRef;
/** 在线批注内容(JSON 字符串或纯文本摘要)。 */
private String annotations;
/** 参与协同编辑人员(逗号分隔 displayName)。 */
private String collaborators;
/** 草稿 / 审核中 / 已定稿 / 已驳回。 */
private String status;
/** 审核意见。 */
private String reviewComment;
/** 提交人。 */
private String submitter;
/** 提交时间。 */
private Instant submittedAt;
/** 定稿时间(status=已定稿时写入)。 */
private Instant finalizedAt;
private Instant createdAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getBidId() { return bidId; }
public void setBidId(Long bidId) { this.bidId = bidId; }
public String getDocPart() { return docPart; }
public void setDocPart(String docPart) { this.docPart = docPart; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public String getDocTitle() { return docTitle; }
public void setDocTitle(String docTitle) { this.docTitle = docTitle; }
public String getFileRef() { return fileRef; }
public void setFileRef(String fileRef) { this.fileRef = fileRef; }
public String getAnnotations() { return annotations; }
public void setAnnotations(String annotations) { this.annotations = annotations; }
public String getCollaborators() { return collaborators; }
public void setCollaborators(String collaborators) { this.collaborators = collaborators; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getReviewComment() { return reviewComment; }
public void setReviewComment(String reviewComment) { this.reviewComment = reviewComment; }
public String getSubmitter() { return submitter; }
public void setSubmitter(String submitter) { this.submitter = submitter; }
public Instant getSubmittedAt() { return submittedAt; }
public void setSubmittedAt(Instant submittedAt) { this.submittedAt = submittedAt; }
public Instant getFinalizedAt() { return finalizedAt; }
public void setFinalizedAt(Instant finalizedAt) { this.finalizedAt = finalizedAt; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
@@ -0,0 +1,137 @@
package com.kaidi.oa.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
/**
* 投标知识库条目(咨询可研院·招投标管理·知识库补全)。
*
* 覆盖需求:
* - 归档过往投标文件(bidFileRef);
* - 报价策略(quoteStrategy);
* - 评审记录(reviewNotes);
* - 按项目类型/投标阶段/标签多维检索;
* - 与 BidbidId)、OpportunityopportunityId)、CslProjectcslProjectId)显式外键关联。
*
* entryType:标书范本 / 报价策略 / 评审记录 / 竞情分析 / 合规要点 / 其他。
* phase:商机 / 投标准备 / 已投标 / 结果归档。
*/
@Entity
@Table(name = "bid_knowledge_entry")
public class BidKnowledgeEntry {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 知识条目标题。 */
private String title;
/** 条目类型:标书范本 / 报价策略 / 评审记录 / 竞情分析 / 合规要点 / 其他。 */
private String entryType;
/** 投标阶段:商机 / 投标准备 / 已投标 / 结果归档。 */
private String phase;
/** 关联的项目类型(与 Bid.projectType 一致)。 */
private String projectType;
/** 专业领域:市政 / 交通 / 水利 / 建筑 / 环境 / 综合。 */
private String field;
/** 适用地区 / 区域。 */
private String region;
/** 标签(逗号分隔,便于多维检索)。 */
private String tags;
/** 报价策略摘要(entryType=报价策略时填写)。 */
@Column(length = 4000)
private String quoteStrategy;
/** 评审记录(entryType=评审记录时填写)。 */
@Column(length = 4000)
private String reviewNotes;
/** 归档投标文件路径/引用(entryType=标书范本时填写)。 */
@Column(length = 1000)
private String bidFileRef;
/** 正文摘要 / 知识内容。 */
@Column(length = 8000)
private String content;
/** 关联投标 IDbid 表)。 */
private Long bidId;
/** 关联商机 IDopportunity 表)。 */
private Long opportunityId;
/** 关联咨询项目 IDcsl_project 表)。 */
private Long cslProjectId;
/** 质量评分(1-5,录入者自评或评审打分)。 */
private Integer qualityScore;
/** 录入人。 */
private String creator;
/** 是否公开(公共知识库 vs. 项目私有)。 */
private Boolean isPublic;
/** 浏览次数(被引用/查看)。 */
private Integer viewCount;
private Instant createdAt;
private Instant updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getEntryType() { return entryType; }
public void setEntryType(String entryType) { this.entryType = entryType; }
public String getPhase() { return phase; }
public void setPhase(String phase) { this.phase = phase; }
public String getProjectType() { return projectType; }
public void setProjectType(String projectType) { this.projectType = projectType; }
public String getField() { return field; }
public void setField(String field) { this.field = field; }
public String getRegion() { return region; }
public void setRegion(String region) { this.region = region; }
public String getTags() { return tags; }
public void setTags(String tags) { this.tags = tags; }
public String getQuoteStrategy() { return quoteStrategy; }
public void setQuoteStrategy(String quoteStrategy) { this.quoteStrategy = quoteStrategy; }
public String getReviewNotes() { return reviewNotes; }
public void setReviewNotes(String reviewNotes) { this.reviewNotes = reviewNotes; }
public String getBidFileRef() { return bidFileRef; }
public void setBidFileRef(String bidFileRef) { this.bidFileRef = bidFileRef; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public Long getBidId() { return bidId; }
public void setBidId(Long bidId) { this.bidId = bidId; }
public Long getOpportunityId() { return opportunityId; }
public void setOpportunityId(Long opportunityId) { this.opportunityId = opportunityId; }
public Long getCslProjectId() { return cslProjectId; }
public void setCslProjectId(Long cslProjectId) { this.cslProjectId = cslProjectId; }
public Integer getQualityScore() { return qualityScore; }
public void setQualityScore(Integer qualityScore) { this.qualityScore = qualityScore; }
public String getCreator() { return creator; }
public void setCreator(String creator) { this.creator = creator; }
public Boolean getIsPublic() { return isPublic; }
public void setIsPublic(Boolean isPublic) { this.isPublic = isPublic; }
public Integer getViewCount() { return viewCount; }
public void setViewCount(Integer viewCount) { this.viewCount = viewCount; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}

Some files were not shown because too many files have changed in this diff Show More