feat: add signed PostgreSQL release updates
This commit is contained in:
@@ -36,7 +36,8 @@ public class AuthInterceptor implements HandlerInterceptor {
|
||||
private static final Set<String> PUBLIC = Set.of(
|
||||
"/api/oa/auth/login",
|
||||
"/api/oa/auth/session",
|
||||
"/api/oa/auth/logout"
|
||||
"/api/oa/auth/logout",
|
||||
"/api/oa/health"
|
||||
);
|
||||
|
||||
/** 写操作方法(只有这些方法才触发授权门槛;GET/HEAD 等读操作不限制)。 */
|
||||
@@ -44,6 +45,7 @@ public class AuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final Set<String> ADMIN = Set.of("ADMIN");
|
||||
private static final Set<String> APPROVER_OR_ADMIN = Set.of("ADMIN", "APPROVER");
|
||||
private static final List<String> ADMIN_READ_PREFIXES = List.of("/api/oa/system-update");
|
||||
|
||||
/**
|
||||
* 系统 / 组织 / 权限 / 配置 类写接口 → 仅 ADMIN。
|
||||
@@ -51,6 +53,7 @@ public class AuthInterceptor implements HandlerInterceptor {
|
||||
*/
|
||||
private static final List<String> ADMIN_PREFIXES = List.of(
|
||||
"/api/oa/users", "/api/oa/depts", "/api/oa/roles", "/api/oa/settings",
|
||||
"/api/oa/system-update",
|
||||
"/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 可触发(杜绝任意角色发起整库扫描)。
|
||||
@@ -432,6 +435,10 @@ public class AuthInterceptor implements HandlerInterceptor {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (matchesPrefix(path, ADMIN_READ_PREFIXES) && ADMIN.stream().noneMatch(have::contains)) {
|
||||
writeError(response, HttpStatus.FORBIDDEN, 403, "无权限:系统更新仅限 ADMIN 角色");
|
||||
return false;
|
||||
}
|
||||
// 读:机密财务/PII 读口需 ADMIN/APPROVER,其余业务读维持"已登录且有角色"可读。
|
||||
if (isSensitiveRead(path) && APPROVER_OR_ADMIN.stream().noneMatch(have::contains)) {
|
||||
writeError(response, HttpStatus.FORBIDDEN, 403, "无权限:该数据需要 ADMIN/APPROVER 角色");
|
||||
@@ -441,6 +448,15 @@ public class AuthInterceptor implements HandlerInterceptor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean matchesPrefix(String path, List<String> prefixes) {
|
||||
for (String prefix : prefixes) {
|
||||
if (path.equals(prefix) || path.startsWith(prefix + "/")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isSensitiveRead(String path) {
|
||||
// 研发证据链聚合端点 /api/oa/rd-projects/{id}/evidence-chain 会端出研发费用金额/凭证号/申报/专利
|
||||
// 等本应被 rd-expenses 读门槛拦下的明细(基路径 rd-projects 非敏感,故按后缀精确收口,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.kaidi.oa.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Runtime configuration for the Gitea release updater. */
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "oa.update")
|
||||
public class UpdateProperties {
|
||||
|
||||
private boolean enabled;
|
||||
private String giteaBaseUrl = "";
|
||||
private String repository = "awaioi/ERP";
|
||||
private String channel = "stable";
|
||||
private String token = "";
|
||||
private String helperCommand = "";
|
||||
private String stateFile = "./runtime/update-state.json";
|
||||
private boolean allowInsecureHttp;
|
||||
private int requestTimeoutSeconds = 15;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getGiteaBaseUrl() {
|
||||
return giteaBaseUrl;
|
||||
}
|
||||
|
||||
public void setGiteaBaseUrl(String giteaBaseUrl) {
|
||||
this.giteaBaseUrl = giteaBaseUrl;
|
||||
}
|
||||
|
||||
public String getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public void setRepository(String repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public String getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public void setChannel(String channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String getHelperCommand() {
|
||||
return helperCommand;
|
||||
}
|
||||
|
||||
public void setHelperCommand(String helperCommand) {
|
||||
this.helperCommand = helperCommand;
|
||||
}
|
||||
|
||||
public String getStateFile() {
|
||||
return stateFile;
|
||||
}
|
||||
|
||||
public void setStateFile(String stateFile) {
|
||||
this.stateFile = stateFile;
|
||||
}
|
||||
|
||||
public boolean isAllowInsecureHttp() {
|
||||
return allowInsecureHttp;
|
||||
}
|
||||
|
||||
public void setAllowInsecureHttp(boolean allowInsecureHttp) {
|
||||
this.allowInsecureHttp = allowInsecureHttp;
|
||||
}
|
||||
|
||||
public int getRequestTimeoutSeconds() {
|
||||
return requestTimeoutSeconds;
|
||||
}
|
||||
|
||||
public void setRequestTimeoutSeconds(int requestTimeoutSeconds) {
|
||||
this.requestTimeoutSeconds = requestTimeoutSeconds;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -24,7 +23,6 @@ public class Announcement {
|
||||
|
||||
private String title;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -91,18 +91,18 @@ public class AnnualReport {
|
||||
// ---------- 扩展填报字段(模板化,各类年报共享) ----------
|
||||
|
||||
/** 关键指标描述(校验摘要/主要指标文字汇总)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String keyIndicatorSummary;
|
||||
|
||||
/** 数据一致性校验结果(自动校验后填入,如"研发费用占比符合要求")。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String validationResult;
|
||||
|
||||
/** 校验是否通过:true 全部通过,false 有警告或错误。 */
|
||||
private Boolean validationPassed;
|
||||
|
||||
/** 备注/填报说明。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String remark;
|
||||
|
||||
/** 填报负责人。 */
|
||||
|
||||
@@ -5,7 +5,6 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/** 博客文章(知识社区 - 我的博客)。tags 为逗号分隔。 */
|
||||
@@ -22,7 +21,6 @@ public class Blog {
|
||||
@Column(length = 2000)
|
||||
private String summary;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String body;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -30,7 +30,7 @@ public class ChronicleEvent {
|
||||
|
||||
private String category;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
/** 关联文档/单据(逗号分隔的引用,如 IP-2026-0001 / 合同号 / 文件名)。 */
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -33,19 +32,16 @@ public class CollabDoc {
|
||||
|
||||
private String status;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
private Instant updatedAt;
|
||||
|
||||
/** 历史版本 JSON 数组:[{version, content, savedAt, editor}]。每次更新正文前追加上一版。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String versionsJson;
|
||||
|
||||
/** 评论/批注 JSON 数组:[{author, content, createdAt}]。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String commentsJson;
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@@ -34,8 +35,9 @@ public class CollabDocUpdate {
|
||||
/** Monotonic per-document ordering (assigned at append time). */
|
||||
private Long seq;
|
||||
|
||||
/** Raw yjs update bytes. Plain byte[] (NOT @Lob): SQLite JDBC can't read @Lob blobs; getBytes() works. */
|
||||
@Column(name = "data", columnDefinition = "BLOB")
|
||||
/** Raw yjs bytes mapped to SQLite BLOB and PostgreSQL bytea. */
|
||||
@JdbcTypeCode(SqlTypes.LONGVARBINARY)
|
||||
@Column(name = "data", length = Integer.MAX_VALUE)
|
||||
private byte[] data;
|
||||
|
||||
private Instant createdAt;
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -40,7 +40,7 @@ public class CompetitorIp {
|
||||
|
||||
private String publicDate;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String summary;
|
||||
|
||||
/** 录入来源:手动 / API导入。 */
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -61,13 +60,11 @@ public class ComplianceRiskReport {
|
||||
private int findingCount;
|
||||
|
||||
/** 漏洞明细文本(可存 JSON 格式)。 */
|
||||
@Lob
|
||||
@Column(name = "findings_text")
|
||||
@Column(name = "findings_text", columnDefinition = "TEXT")
|
||||
private String findings;
|
||||
|
||||
/** 整改建议。 */
|
||||
@Lob
|
||||
@Column(name = "suggestion_text")
|
||||
@Column(name = "suggestion_text", columnDefinition = "TEXT")
|
||||
private String suggestion;
|
||||
|
||||
/** 报告生成人。 */
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -46,8 +45,7 @@ public class ContentPieceVersion {
|
||||
private String titleSnapshot;
|
||||
|
||||
/** 正文快照(完整内容,用于 diff 对比)。 */
|
||||
@Lob
|
||||
@Column(name = "body_snapshot")
|
||||
@Column(name = "body_snapshot", columnDefinition = "TEXT")
|
||||
private String bodySnapshot;
|
||||
|
||||
private Instant createdAt;
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -31,7 +31,7 @@ public class ContractTemplate {
|
||||
|
||||
private String applicableSubject;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String bodyTemplate;
|
||||
|
||||
private String requiredClauses;
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -36,7 +36,7 @@ public class DeclarationTemplate {
|
||||
|
||||
private String autoCheckRules;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String bodyTemplate;
|
||||
|
||||
private String version;
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -28,7 +27,6 @@ public class Discussion {
|
||||
|
||||
private String category;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@@ -37,7 +35,6 @@ public class Discussion {
|
||||
private Instant createdAt;
|
||||
|
||||
/** 回帖列表 JSON 数组:[{author, content, createdAt}]。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String repliesJson;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/** 享空间动态(文化建设 - 分享流)。tags 为逗号分隔。 */
|
||||
@@ -24,7 +23,6 @@ public class Feed {
|
||||
/** 动态 / 分享 / 图片 / 打卡 */
|
||||
private String type;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@@ -37,7 +35,6 @@ public class Feed {
|
||||
private int comments;
|
||||
|
||||
/** 评论正文列表 [{author,content,createdAt}] 的 JSON。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String commentsJson;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -39,12 +38,10 @@ public class FertProductQc {
|
||||
private String standardCode;
|
||||
|
||||
/** 实测值 JSON:{"有机质":45.2,"水分":28.1,...}(键=indicator)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "text")
|
||||
private String itemsJson;
|
||||
|
||||
/** 逐项判定明细 JSON(自动生成的报告口径)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "text")
|
||||
private String judgeJson;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -68,7 +67,6 @@ public class FertSupplierProfile {
|
||||
private String licenseDoc;
|
||||
|
||||
/** 补充资质 JSON(扩展字段 {"有机认证":"有","产品标准":"QB/T xxxx",...})。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "text")
|
||||
private String admitDocsJson;
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ public class Financing {
|
||||
private String currency;
|
||||
|
||||
/** 年化利率(百分比,如 4.35 表示 4.35%)。 */
|
||||
private Double rate;
|
||||
private BigDecimal rate;
|
||||
|
||||
private String startDate;
|
||||
|
||||
@@ -122,11 +122,11 @@ public class Financing {
|
||||
this.currency = currency;
|
||||
}
|
||||
|
||||
public Double getRate() {
|
||||
public BigDecimal getRate() {
|
||||
return rate;
|
||||
}
|
||||
|
||||
public void setRate(Double rate) {
|
||||
public void setRate(BigDecimal rate) {
|
||||
this.rate = rate;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -31,7 +30,6 @@ public class FlowTrace {
|
||||
/** Who handled the step. */
|
||||
private String who;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String opinion;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -33,7 +32,6 @@ public class FormInstance {
|
||||
|
||||
private String title;
|
||||
|
||||
@Lob
|
||||
@Column(name = "data_json", columnDefinition = "TEXT")
|
||||
private String dataJson;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.kaidi.oa.domain;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -28,15 +27,12 @@ public class FormTemplate {
|
||||
|
||||
private String org;
|
||||
|
||||
@Lob
|
||||
@Column(name = "form_schema_json", columnDefinition = "TEXT")
|
||||
private String formSchemaJson;
|
||||
|
||||
@Lob
|
||||
@Column(name = "flow_schema_json", columnDefinition = "TEXT")
|
||||
private String flowSchemaJson;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String instructions;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -40,7 +40,7 @@ public class HrLaborTemplate {
|
||||
private String version;
|
||||
|
||||
/** 模板正文(含占位符 {员工姓名} {岗位} {薪资} 等)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String bodyTemplate;
|
||||
|
||||
/** 必备条款(逗号分隔,如「试用期条款,保密条款,竞业限制条款」)。 */
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -44,7 +43,7 @@ public class InternalNewsletter {
|
||||
private String authorDept;
|
||||
|
||||
/** 正文(富文本,允许大段文字)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String body;
|
||||
|
||||
/** 配图/附件 URL(逗号分隔,如 "https://…/img1.jpg,https://…/img2.jpg")。 */
|
||||
|
||||
@@ -7,7 +7,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -92,7 +92,7 @@ public class IpAsset {
|
||||
/** 责任人(IP 部门跟案人)。 */
|
||||
private String owner;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String remark;
|
||||
|
||||
private Instant createdAt;
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ public class IpAssetEvent {
|
||||
|
||||
private String toValue;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String note;
|
||||
|
||||
/** 操作人(跟案人/复核人)。 */
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -33,7 +33,7 @@ public class IpKnowledge {
|
||||
/** 技术关键词(逗号分隔,供按关键词检索)。 */
|
||||
private String keywords;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
private String source;
|
||||
|
||||
@@ -7,7 +7,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -71,7 +71,7 @@ public class IpLicenseTransfer {
|
||||
private String status;
|
||||
|
||||
/** 备注(合同关键条款摘要)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String remark;
|
||||
|
||||
/** 经办人。 */
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -37,7 +37,7 @@ public class IpRegulation {
|
||||
private String currentVersion;
|
||||
|
||||
/** 当前正文(最新草稿或已发布版)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
private String owner;
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -29,7 +29,7 @@ public class IpRegulationVersion {
|
||||
|
||||
private String action;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String contentSnapshot;
|
||||
|
||||
/** 修订说明 / 审批意见。 */
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -77,7 +76,6 @@ public class IpSciCredArchive {
|
||||
private String validUntil;
|
||||
|
||||
/** 文件备注/摘要。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String remark;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -34,7 +34,7 @@ public class ItKnowledgeArticle {
|
||||
private String keywords;
|
||||
|
||||
/** 解决方案正文(步骤)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
/** 作者 / 维护人。 */
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -28,7 +28,7 @@ public class LegalConsult {
|
||||
private String subject;
|
||||
|
||||
/** 问题描述详情。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String detail;
|
||||
|
||||
/** 申请部门。 */
|
||||
@@ -44,7 +44,7 @@ public class LegalConsult {
|
||||
private String assignee;
|
||||
|
||||
/** 法律意见(回复内容)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String legalOpinion;
|
||||
|
||||
/** 状态:待受理 / 处理中 / 已答复 / 已关闭。 */
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -51,7 +50,6 @@ public class Meeting {
|
||||
private String description;
|
||||
|
||||
/** 会议附件名列表的 JSON(["文件名1","文件名2"],仅记录文件名,不存二进制)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String attachmentJson;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -29,7 +28,6 @@ public class MeetingMinute {
|
||||
|
||||
private String title;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@@ -37,7 +35,6 @@ public class MeetingMinute {
|
||||
private String status;
|
||||
|
||||
/** 决议项, TEXT/JSON 串. */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String decisions;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -34,7 +33,6 @@ public class Message {
|
||||
|
||||
private String title;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String body;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -84,7 +83,7 @@ public class MfgEnvSupplierProfile {
|
||||
* 历史供货业绩(JSON 数组):[{projectName, deliveryYear, qty, qualifiedRate, customer}...]。
|
||||
* Lob 存储,前端渲染为折叠卡片。
|
||||
*/
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String performanceJson;
|
||||
|
||||
/** 历史供货总次数(冗余,便于快速汇总)。 */
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -31,7 +31,7 @@ public class NetScanLog {
|
||||
|
||||
private String title;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
private String link;
|
||||
|
||||
@@ -5,7 +5,6 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/** 通用键值配置:settingKey 唯一,valueJson 存任意 JSON 字符串。用于工时设置/信息项设置等页面级配置。 */
|
||||
@@ -20,7 +19,6 @@ public class OaSetting {
|
||||
@Column(unique = true, nullable = false)
|
||||
private String settingKey;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String valueJson;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ public class PmtFinancingContract {
|
||||
private BigDecimal contractAmount = BigDecimal.ZERO;
|
||||
|
||||
/** 合同利率(年化,如 4.35)。 */
|
||||
private Double contractRate;
|
||||
private BigDecimal contractRate;
|
||||
|
||||
/** 合同签署日期(YYYY-MM-DD)。 */
|
||||
private String signDate;
|
||||
@@ -94,8 +94,8 @@ public class PmtFinancingContract {
|
||||
public BigDecimal getContractAmount() { return contractAmount; }
|
||||
public void setContractAmount(BigDecimal contractAmount) { this.contractAmount = contractAmount; }
|
||||
|
||||
public Double getContractRate() { return contractRate; }
|
||||
public void setContractRate(Double contractRate) { this.contractRate = contractRate; }
|
||||
public BigDecimal getContractRate() { return contractRate; }
|
||||
public void setContractRate(BigDecimal contractRate) { this.contractRate = contractRate; }
|
||||
|
||||
public String getSignDate() { return signDate; }
|
||||
public void setSignDate(String signDate) { this.signDate = signDate; }
|
||||
|
||||
@@ -39,7 +39,7 @@ public class PmtInternalTrans {
|
||||
private BigDecimal amount = BigDecimal.ZERO;
|
||||
|
||||
/** 利率(内部计息时使用,年化,如 3.5 表示 3.5%)。 */
|
||||
private Double interestRate;
|
||||
private BigDecimal interestRate;
|
||||
|
||||
/** 计息起始日。 */
|
||||
private String interestFrom;
|
||||
@@ -104,8 +104,8 @@ public class PmtInternalTrans {
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
|
||||
public Double getInterestRate() { return interestRate; }
|
||||
public void setInterestRate(Double interestRate) { this.interestRate = interestRate; }
|
||||
public BigDecimal getInterestRate() { return interestRate; }
|
||||
public void setInterestRate(BigDecimal interestRate) { this.interestRate = interestRate; }
|
||||
|
||||
public String getInterestFrom() { return interestFrom; }
|
||||
public void setInterestFrom(String interestFrom) { this.interestFrom = interestFrom; }
|
||||
|
||||
@@ -41,7 +41,7 @@ public class PmtLoanScheme {
|
||||
private BigDecimal amount = BigDecimal.ZERO;
|
||||
|
||||
/** 年化利率(百分比,如 4.35 表示 4.35%)。 */
|
||||
private Double annualRate;
|
||||
private BigDecimal annualRate;
|
||||
|
||||
/** 期限(月)。 */
|
||||
private Integer termMonths;
|
||||
@@ -56,7 +56,7 @@ public class PmtLoanScheme {
|
||||
private String guaranteeType;
|
||||
|
||||
/** 担保费率(年化,百分比)。 */
|
||||
private Double guaranteeRate;
|
||||
private BigDecimal guaranteeRate;
|
||||
|
||||
/** 提款条件(文字描述)。 */
|
||||
private String drawdownConditions;
|
||||
@@ -65,7 +65,7 @@ public class PmtLoanScheme {
|
||||
* 系统自动计算的综合成本率(利息+费用+担保成本之和/本金,IRR 简化口径,百分比)。
|
||||
* 创建/更新时服务端根据 rate+费用+担保自动回填。
|
||||
*/
|
||||
private Double effectiveCostRate;
|
||||
private BigDecimal effectiveCostRate;
|
||||
|
||||
/** 综合排名(越小越优,由 /rank 端点服务端按综合成本排序后写入)。 */
|
||||
private Integer rank;
|
||||
@@ -104,8 +104,8 @@ public class PmtLoanScheme {
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
|
||||
public Double getAnnualRate() { return annualRate; }
|
||||
public void setAnnualRate(Double annualRate) { this.annualRate = annualRate; }
|
||||
public BigDecimal getAnnualRate() { return annualRate; }
|
||||
public void setAnnualRate(BigDecimal annualRate) { this.annualRate = annualRate; }
|
||||
|
||||
public Integer getTermMonths() { return termMonths; }
|
||||
public void setTermMonths(Integer termMonths) { this.termMonths = termMonths; }
|
||||
@@ -119,14 +119,14 @@ public class PmtLoanScheme {
|
||||
public String getGuaranteeType() { return guaranteeType; }
|
||||
public void setGuaranteeType(String guaranteeType) { this.guaranteeType = guaranteeType; }
|
||||
|
||||
public Double getGuaranteeRate() { return guaranteeRate; }
|
||||
public void setGuaranteeRate(Double guaranteeRate) { this.guaranteeRate = guaranteeRate; }
|
||||
public BigDecimal getGuaranteeRate() { return guaranteeRate; }
|
||||
public void setGuaranteeRate(BigDecimal guaranteeRate) { this.guaranteeRate = guaranteeRate; }
|
||||
|
||||
public String getDrawdownConditions() { return drawdownConditions; }
|
||||
public void setDrawdownConditions(String drawdownConditions) { this.drawdownConditions = drawdownConditions; }
|
||||
|
||||
public Double getEffectiveCostRate() { return effectiveCostRate; }
|
||||
public void setEffectiveCostRate(Double effectiveCostRate) { this.effectiveCostRate = effectiveCostRate; }
|
||||
public BigDecimal getEffectiveCostRate() { return effectiveCostRate; }
|
||||
public void setEffectiveCostRate(BigDecimal effectiveCostRate) { this.effectiveCostRate = effectiveCostRate; }
|
||||
|
||||
public Integer getRank() { return rank; }
|
||||
public void setRank(Integer rank) { this.rank = rank; }
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -38,7 +38,7 @@ public class StdApplication {
|
||||
|
||||
private String planNo;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String techContent;
|
||||
|
||||
private String drafters;
|
||||
|
||||
@@ -5,8 +5,9 @@ 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 org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@@ -34,12 +35,9 @@ public class StoredFile {
|
||||
/** Size in bytes. */
|
||||
private Long size;
|
||||
|
||||
/**
|
||||
* Raw bytes as a plain byte[] (NOT @Lob): SQLite's JDBC driver does not implement
|
||||
* the streamed-Blob read path that @Lob triggers ("not implemented by SQLite JDBC
|
||||
* driver"); a plain byte[] is read directly via getBytes() and works.
|
||||
*/
|
||||
@Column(columnDefinition = "BLOB")
|
||||
/** Raw bytes mapped to SQLite BLOB and PostgreSQL bytea without JDBC Blob streaming. */
|
||||
@JdbcTypeCode(SqlTypes.LONGVARBINARY)
|
||||
@Column(length = Integer.MAX_VALUE)
|
||||
private byte[] data;
|
||||
|
||||
/** Display name of the uploader (resolved from the auth token). */
|
||||
|
||||
@@ -5,7 +5,6 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -29,12 +28,10 @@ public class Survey {
|
||||
|
||||
private String status;
|
||||
|
||||
@Lob
|
||||
@Column(name = "options_json", columnDefinition = "TEXT")
|
||||
private String optionsJson;
|
||||
|
||||
// --> JSON array of voter usernames who already voted; used to reject duplicate votes.
|
||||
@Lob
|
||||
@Column(name = "voters_json", columnDefinition = "TEXT")
|
||||
private String votersJson;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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.math.BigDecimal;
|
||||
@@ -57,7 +56,6 @@ public class SvIndepClaim {
|
||||
private String incidentEndDate;
|
||||
|
||||
/** 事件经过详细说明。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String incidentDetail;
|
||||
|
||||
@@ -73,7 +71,6 @@ public class SvIndepClaim {
|
||||
private Integer criticalPathImpactDays;
|
||||
|
||||
/** 关键线路影响分析说明(如"该延误不在关键路径,不影响完工日期")。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String criticalPathImpact;
|
||||
|
||||
@@ -82,7 +79,6 @@ public class SvIndepClaim {
|
||||
private BigDecimal claimAmount = BigDecimal.ZERO;
|
||||
|
||||
/** 费用影响评估说明。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String costImpactDetail;
|
||||
|
||||
@@ -102,7 +98,6 @@ public class SvIndepClaim {
|
||||
private BigDecimal approvedAmount = BigDecimal.ZERO;
|
||||
|
||||
/** 监理意见详细说明。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String reviewOpinion;
|
||||
|
||||
|
||||
@@ -22,6 +22,12 @@ public class SysRole {
|
||||
@Column(nullable = false, unique = true)
|
||||
private String code;
|
||||
|
||||
/** 角色说明(角色管理 UI 用)。 */
|
||||
private String description;
|
||||
|
||||
/** 系统内置角色(ADMIN/APPROVER/USER)不可删;自定义部门角色为 false。 */
|
||||
private boolean system = false;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -45,4 +51,20 @@ public class SysRole {
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public boolean isSystem() {
|
||||
return system;
|
||||
}
|
||||
|
||||
public void setSystem(boolean system) {
|
||||
this.system = system;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -47,7 +47,7 @@ public class TechAchievement {
|
||||
private Long ipAssetId;
|
||||
|
||||
/** 佐证材料清单(论文/标准/软著/检测报告…)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String evidence;
|
||||
|
||||
/** 成果评价等级。 */
|
||||
|
||||
@@ -7,7 +7,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -58,11 +58,11 @@ public class TechContract {
|
||||
private String ipAssetName;
|
||||
|
||||
/** 自动生成的创新技术方案(材料)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String techScheme;
|
||||
|
||||
/** 自动生成的承诺书(材料)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String commitmentLetter;
|
||||
|
||||
private String owner;
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -62,7 +62,7 @@ public class WmDeclTemplate {
|
||||
private String placeholders;
|
||||
|
||||
/** 模板正文(含占位符的完整模板文本)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String bodyTemplate;
|
||||
|
||||
/** 更新说明(本版变更内容摘要,更新时通知相关人员)。 */
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -46,7 +46,7 @@ public class WmDocVersion {
|
||||
private String summary;
|
||||
|
||||
/** 文档正文(内联富文本)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
/** 外部附件 URL / 文件路径(可空;与通用文件模块结合时存储路径)。 */
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -64,7 +64,7 @@ public class WmPromotion {
|
||||
private Integer attendeeCount;
|
||||
|
||||
/** 效果评估说明。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String effectNote;
|
||||
|
||||
/** 推广材料(作业指导书/PPT/视频等,逗号分隔文件路径或文件 id)。 */
|
||||
|
||||
@@ -7,7 +7,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -70,41 +70,41 @@ public class WorkMethod {
|
||||
private String stage;
|
||||
|
||||
// ---- 立项信息(需求功能1·工法立项) ----
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String techBackground;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String innovation;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String applicableScope;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String expectedBenefit;
|
||||
|
||||
// ---- 工法文本九大要素(需求功能1·工法编制) ----
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String contentFeature;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String contentPrinciple;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String contentProcess;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String contentMaterial;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String contentQuality;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String contentSafety;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String contentEnv;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String contentBenefit;
|
||||
|
||||
// ---- 证书与有效期(需求功能1·工法证书) ----
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -53,13 +53,13 @@ public class WorkMethodApplication {
|
||||
* 申报材料清单:逗号分隔的 "材料名:0/1"(0=缺,1=齐),如
|
||||
* "申报书:1,工法文本:1,查新报告:0,应用证明:1,经济效益证明:0"。
|
||||
*/
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String materials;
|
||||
|
||||
/** 批准文号(批准后回填)。 */
|
||||
private String approveDocNo;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String remark;
|
||||
|
||||
private Instant createdAt;
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -36,7 +36,7 @@ public class WorkMethodEvent {
|
||||
|
||||
private String toValue;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String note;
|
||||
|
||||
private String operator;
|
||||
|
||||
@@ -6,7 +6,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -44,7 +44,7 @@ public class WorkMethodReview {
|
||||
/** 评分(0-100,可空)。 */
|
||||
private Integer score;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String opinion;
|
||||
|
||||
private String reviewedDate;
|
||||
|
||||
@@ -7,7 +7,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -44,7 +44,7 @@ public class WorkMethodReward {
|
||||
* 完成人贡献分配明细:逗号分隔的 "姓名:比例%:金额",如
|
||||
* "张三:50:5000.00,李四:30:3000.00,王五:20:2000.00"。比例之和应为 100。
|
||||
*/
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String allocation;
|
||||
|
||||
/** 奖励状态:待审批 / 已审批 / 已发放 / 已驳回。 */
|
||||
@@ -61,7 +61,7 @@ public class WorkMethodReward {
|
||||
/** 审批后生成的资金支付中心付款单 id(联动财务)。 */
|
||||
private Long paymentId;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String remark;
|
||||
|
||||
private Instant createdAt;
|
||||
|
||||
@@ -7,7 +7,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
@@ -49,7 +49,7 @@ public class WorkMethodUsage {
|
||||
private Integer shortenDays = 0;
|
||||
|
||||
/** 应用效果描述(质量提升/用户评价)。 */
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String effect;
|
||||
|
||||
/** 登记人。 */
|
||||
|
||||
@@ -5,7 +5,6 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/** A work plan. period is e.g. 本周 / 本月 / 本季; status is 草稿 / 执行中 / 已完成. */
|
||||
@@ -28,7 +27,6 @@ public class WorkPlan {
|
||||
|
||||
private String status;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ public class FinancingDomainSeeder implements CommandLineRunner {
|
||||
f1.setCreditLimit(Money.of(50000000.0));
|
||||
f1.setAmount(Money.of(50000000.0));
|
||||
f1.setCurrency("人民币");
|
||||
f1.setRate(3.85);
|
||||
f1.setRate(BigDecimal.valueOf(3.85));
|
||||
f1.setStartDate("2024-01-15");
|
||||
f1.setEndDate("2026-01-14");
|
||||
f1.setTermMonths(24);
|
||||
@@ -153,7 +153,7 @@ public class FinancingDomainSeeder implements CommandLineRunner {
|
||||
f2.setCreditLimit(Money.of(100000000.0));
|
||||
f2.setAmount(Money.of(80000000.0));
|
||||
f2.setCurrency("人民币");
|
||||
f2.setRate(3.20);
|
||||
f2.setRate(BigDecimal.valueOf(3.20));
|
||||
f2.setStartDate("2024-03-01");
|
||||
f2.setEndDate("2029-02-28");
|
||||
f2.setTermMonths(60);
|
||||
@@ -173,7 +173,7 @@ public class FinancingDomainSeeder implements CommandLineRunner {
|
||||
f3.setCreditLimit(Money.of(20000000.0));
|
||||
f3.setAmount(Money.of(18000000.0));
|
||||
f3.setCurrency("人民币");
|
||||
f3.setRate(4.35);
|
||||
f3.setRate(BigDecimal.valueOf(4.35));
|
||||
f3.setStartDate("2024-06-01");
|
||||
f3.setEndDate("2025-05-31");
|
||||
f3.setTermMonths(12);
|
||||
@@ -193,7 +193,7 @@ public class FinancingDomainSeeder implements CommandLineRunner {
|
||||
f4.setCreditLimit(Money.of(30000000.0));
|
||||
f4.setAmount(Money.of(30000000.0));
|
||||
f4.setCurrency("人民币");
|
||||
f4.setRate(4.10);
|
||||
f4.setRate(BigDecimal.valueOf(4.10));
|
||||
f4.setStartDate("2025-02-01");
|
||||
f4.setEndDate("2026-01-31");
|
||||
f4.setTermMonths(12);
|
||||
@@ -214,7 +214,7 @@ public class FinancingDomainSeeder implements CommandLineRunner {
|
||||
f5.setCreditLimit(Money.of(20000000.0));
|
||||
f5.setAmount(Money.of(20000000.0));
|
||||
f5.setCurrency("人民币");
|
||||
f5.setRate(2.50);
|
||||
f5.setRate(BigDecimal.valueOf(2.50));
|
||||
f5.setStartDate("2025-07-01");
|
||||
f5.setEndDate("2026-06-30");
|
||||
f5.setTermMonths(12);
|
||||
@@ -234,7 +234,7 @@ public class FinancingDomainSeeder implements CommandLineRunner {
|
||||
f6.setCreditLimit(Money.of(10000000.0));
|
||||
f6.setAmount(Money.of(10000000.0));
|
||||
f6.setCurrency("人民币");
|
||||
f6.setRate(4.60);
|
||||
f6.setRate(BigDecimal.valueOf(4.60));
|
||||
f6.setStartDate("2023-01-01");
|
||||
f6.setEndDate("2024-12-31");
|
||||
f6.setTermMonths(24);
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
package com.kaidi.oa.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kaidi.oa.common.ApiException;
|
||||
import com.kaidi.oa.config.UpdateProperties;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.info.BuildProperties;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** Checks Gitea releases and delegates installation to the external update helper. */
|
||||
@Service
|
||||
public class SystemUpdateService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SystemUpdateService.class);
|
||||
private static final Pattern VERSION_PATTERN = Pattern.compile(
|
||||
"^[vV]?(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?"
|
||||
+ "(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?"
|
||||
+ "(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$");
|
||||
private static final long MAX_STATE_BYTES = 64 * 1024;
|
||||
|
||||
private final UpdateProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HttpClient httpClient;
|
||||
private final String currentVersion;
|
||||
private final AtomicReference<UpdateStatus> state;
|
||||
private final AtomicBoolean installRunning = new AtomicBoolean(false);
|
||||
|
||||
@Autowired
|
||||
public SystemUpdateService(UpdateProperties properties,
|
||||
ObjectMapper objectMapper,
|
||||
ObjectProvider<BuildProperties> buildProperties) {
|
||||
this(properties, objectMapper, buildProperties,
|
||||
HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(Math.max(1, properties.getRequestTimeoutSeconds())))
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build());
|
||||
}
|
||||
|
||||
SystemUpdateService(UpdateProperties properties,
|
||||
ObjectMapper objectMapper,
|
||||
ObjectProvider<BuildProperties> buildProperties,
|
||||
HttpClient httpClient) {
|
||||
this.properties = properties;
|
||||
this.objectMapper = objectMapper;
|
||||
this.httpClient = httpClient;
|
||||
BuildProperties build = buildProperties.getIfAvailable();
|
||||
this.currentVersion = build == null || build.getVersion() == null ? "dev" : build.getVersion();
|
||||
this.state = new AtomicReference<>(UpdateStatus.idle(properties.isEnabled(), currentVersion));
|
||||
}
|
||||
|
||||
public UpdateStatus status() {
|
||||
UpdateStatus persisted = readHelperState();
|
||||
return persisted == null ? state.get() : persisted;
|
||||
}
|
||||
|
||||
public synchronized UpdateStatus check() {
|
||||
requireConfigured();
|
||||
state.set(state.get().withPhase(UpdatePhase.CHECKING, 5, "正在检查 Gitea Release"));
|
||||
try {
|
||||
ReleaseInfo release = fetchLatestRelease();
|
||||
boolean available = compareVersions(release.version(), currentVersion) > 0;
|
||||
UpdateStatus checked = new UpdateStatus(
|
||||
true,
|
||||
currentVersion,
|
||||
release.version(),
|
||||
available,
|
||||
available ? UpdatePhase.AVAILABLE : UpdatePhase.UP_TO_DATE,
|
||||
100,
|
||||
available ? "发现新版本" : "当前已是最新版本",
|
||||
Instant.now(),
|
||||
release.publishedAt(),
|
||||
release.notes(),
|
||||
release.assets(),
|
||||
null
|
||||
);
|
||||
state.set(checked);
|
||||
return checked;
|
||||
} catch (RuntimeException e) {
|
||||
state.set(state.get().failed(safeMessage(e)));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public UpdateStatus install(String requestedVersion) {
|
||||
requireConfigured();
|
||||
if (!isVersion(requestedVersion)) {
|
||||
throw new ApiException(400, "版本号格式不正确");
|
||||
}
|
||||
UpdateStatus checked = state.get();
|
||||
if (checked.latestVersion() == null || !sameVersion(checked.latestVersion(), requestedVersion)) {
|
||||
checked = check();
|
||||
}
|
||||
if (!sameVersion(checked.latestVersion(), requestedVersion)) {
|
||||
throw new ApiException(409, "目标版本已变化,请重新检查更新");
|
||||
}
|
||||
if (!checked.updateAvailable()) {
|
||||
throw new ApiException(409, "当前版本不需要更新");
|
||||
}
|
||||
if (!installRunning.compareAndSet(false, true)) {
|
||||
throw new ApiException(409, "已有更新任务正在执行");
|
||||
}
|
||||
|
||||
Path helper = Path.of(properties.getHelperCommand()).toAbsolutePath().normalize();
|
||||
if (!Files.isRegularFile(helper) || !Files.isExecutable(helper)) {
|
||||
installRunning.set(false);
|
||||
throw new ApiException(503, "更新助手不可用,请检查安装目录");
|
||||
}
|
||||
|
||||
try {
|
||||
Path statePath = statePath();
|
||||
Path parent = statePath.getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
Path logFile = parent == null
|
||||
? Path.of("update-helper.log").toAbsolutePath()
|
||||
: parent.resolve("update-helper.log");
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(
|
||||
helper.toString(), "install", normalizeVersion(requestedVersion));
|
||||
processBuilder.environment().put("ERP_APP_PID", String.valueOf(ProcessHandle.current().pid()));
|
||||
processBuilder.redirectErrorStream(true);
|
||||
processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.toFile()));
|
||||
Process process = processBuilder.start();
|
||||
|
||||
UpdateStatus starting = checked.withPhase(UpdatePhase.STARTING, 1, "更新助手已启动");
|
||||
state.set(starting);
|
||||
process.onExit().thenAccept(completed -> {
|
||||
installRunning.set(false);
|
||||
if (completed.exitValue() != 0) {
|
||||
state.updateAndGet(value -> value.failed("更新助手执行失败,退出码 " + completed.exitValue()));
|
||||
}
|
||||
});
|
||||
return starting;
|
||||
} catch (IOException e) {
|
||||
installRunning.set(false);
|
||||
log.error("Unable to start update helper", e);
|
||||
throw new ApiException(500, "无法启动更新助手");
|
||||
}
|
||||
}
|
||||
|
||||
private ReleaseInfo fetchLatestRelease() {
|
||||
String[] repository = properties.getRepository().split("/", 2);
|
||||
if (repository.length != 2 || repository[0].isBlank() || repository[1].isBlank()) {
|
||||
throw new ApiException(500, "更新仓库配置无效");
|
||||
}
|
||||
String base = properties.getGiteaBaseUrl().replaceAll("/+$", "");
|
||||
URI uri = URI.create(base + "/api/v1/repos/" + encode(repository[0]) + "/"
|
||||
+ encode(repository[1]) + "/releases/latest");
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
|
||||
.timeout(Duration.ofSeconds(Math.max(1, properties.getRequestTimeoutSeconds())))
|
||||
.header("Accept", "application/json")
|
||||
.GET();
|
||||
if (properties.getToken() != null && !properties.getToken().isBlank()) {
|
||||
builder.header("Authorization", "token " + properties.getToken().trim());
|
||||
}
|
||||
try {
|
||||
HttpResponse<String> response = httpClient.send(builder.build(),
|
||||
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
if (response.statusCode() == 404) {
|
||||
throw new ApiException(404, "Gitea 尚未发布 Release");
|
||||
}
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
throw new ApiException(502, "Gitea Release API 返回 HTTP " + response.statusCode());
|
||||
}
|
||||
return parseRelease(objectMapper.readTree(response.body()));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ApiException(503, "检查更新被中断");
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
log.warn("Unable to check Gitea release: {}", e.getMessage());
|
||||
throw new ApiException(503, "无法连接更新服务器");
|
||||
}
|
||||
}
|
||||
|
||||
private ReleaseInfo parseRelease(JsonNode root) {
|
||||
String tag = text(root, "tag_name");
|
||||
if (!isVersion(tag)) {
|
||||
throw new ApiException(502, "Release 版本号格式无效");
|
||||
}
|
||||
if (root.path("draft").asBoolean(false)) {
|
||||
throw new ApiException(502, "最新 Release 仍是草稿");
|
||||
}
|
||||
Matcher versionMatcher = VERSION_PATTERN.matcher(tag);
|
||||
versionMatcher.matches();
|
||||
boolean taggedPrerelease = versionMatcher.group(4) != null;
|
||||
if ((root.path("prerelease").asBoolean(false) || taggedPrerelease)
|
||||
&& "stable".equalsIgnoreCase(properties.getChannel())) {
|
||||
throw new ApiException(502, "稳定频道拒绝预发布版本");
|
||||
}
|
||||
List<ReleaseAsset> assets = new ArrayList<>();
|
||||
for (JsonNode node : root.path("assets")) {
|
||||
assets.add(new ReleaseAsset(
|
||||
text(node, "name"),
|
||||
text(node, "browser_download_url"),
|
||||
node.path("size").asLong(0)
|
||||
));
|
||||
}
|
||||
String version = normalizeVersion(tag);
|
||||
requireAsset(assets, "kaidi-erp-" + version + ".tar.gz");
|
||||
requireAsset(assets, "SHA256SUMS");
|
||||
requireAsset(assets, "SHA256SUMS.sig");
|
||||
Instant publishedAt = null;
|
||||
String published = text(root, "published_at");
|
||||
if (!published.isBlank()) {
|
||||
try {
|
||||
publishedAt = Instant.parse(published);
|
||||
} catch (RuntimeException ignore) {
|
||||
// An invalid optional timestamp must not hide an otherwise valid release.
|
||||
}
|
||||
}
|
||||
return new ReleaseInfo(version, text(root, "body"), publishedAt, List.copyOf(assets));
|
||||
}
|
||||
|
||||
private void requireConfigured() {
|
||||
if (!properties.isEnabled()) {
|
||||
throw new ApiException(503, "在线更新尚未启用");
|
||||
}
|
||||
if (properties.getGiteaBaseUrl() == null || properties.getGiteaBaseUrl().isBlank()) {
|
||||
throw new ApiException(503, "尚未配置 Gitea 地址");
|
||||
}
|
||||
URI uri;
|
||||
try {
|
||||
uri = URI.create(properties.getGiteaBaseUrl().trim());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ApiException(500, "Gitea 地址格式无效");
|
||||
}
|
||||
String scheme = uri.getScheme() == null ? "" : uri.getScheme().toLowerCase(Locale.ROOT);
|
||||
if (!"https".equals(scheme) && !(properties.isAllowInsecureHttp() && "http".equals(scheme))) {
|
||||
throw new ApiException(503, "更新服务器必须使用 HTTPS");
|
||||
}
|
||||
}
|
||||
|
||||
private UpdateStatus readHelperState() {
|
||||
Path path = statePath();
|
||||
try {
|
||||
if (!Files.isRegularFile(path) || Files.size(path) > MAX_STATE_BYTES) {
|
||||
return null;
|
||||
}
|
||||
JsonNode root = objectMapper.readTree(path.toFile());
|
||||
String phaseText = text(root, "phase");
|
||||
if (phaseText.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
UpdatePhase phase = UpdatePhase.valueOf(phaseText.toUpperCase(Locale.ROOT));
|
||||
UpdateStatus memory = state.get();
|
||||
String helperVersion = blankToNull(text(root, "version"));
|
||||
boolean updateAvailable = helperVersion != null
|
||||
&& phase != UpdatePhase.SUCCEEDED
|
||||
&& compareVersions(helperVersion, currentVersion) > 0;
|
||||
Instant helperUpdatedAt = parseInstant(text(root, "updatedAt"));
|
||||
return new UpdateStatus(
|
||||
properties.isEnabled(),
|
||||
currentVersion,
|
||||
helperVersion,
|
||||
updateAvailable,
|
||||
phase,
|
||||
Math.max(0, Math.min(100, root.path("progress").asInt(0))),
|
||||
text(root, "message"),
|
||||
helperUpdatedAt == null ? memory.checkedAt() : helperUpdatedAt,
|
||||
memory.publishedAt(),
|
||||
memory.releaseNotes(),
|
||||
memory.assets(),
|
||||
blankToNull(text(root, "error"))
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.debug("Ignoring unreadable update state {}: {}", path, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Path statePath() {
|
||||
String value = properties.getStateFile();
|
||||
return Path.of(value == null || value.isBlank() ? "./runtime/update-state.json" : value)
|
||||
.toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private static String text(JsonNode node, String field) {
|
||||
JsonNode value = node.path(field);
|
||||
return value.isTextual() ? value.asText().trim() : "";
|
||||
}
|
||||
|
||||
private static String encode(String pathSegment) {
|
||||
return URLEncoder.encode(pathSegment, StandardCharsets.UTF_8).replace("+", "%20");
|
||||
}
|
||||
|
||||
private static void requireAsset(List<ReleaseAsset> assets, String name) {
|
||||
if (assets.stream().noneMatch(asset -> name.equals(asset.name()))) {
|
||||
throw new ApiException(502, "Release 缺少文件 " + name);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isVersion(String version) {
|
||||
return version != null && VERSION_PATTERN.matcher(version.trim()).matches();
|
||||
}
|
||||
|
||||
private static boolean sameVersion(String left, String right) {
|
||||
return normalizeVersion(left).equals(normalizeVersion(right));
|
||||
}
|
||||
|
||||
static int compareVersions(String left, String right) {
|
||||
Matcher a = VERSION_PATTERN.matcher(Objects.requireNonNullElse(left, "").trim());
|
||||
Matcher b = VERSION_PATTERN.matcher(Objects.requireNonNullElse(right, "").trim());
|
||||
boolean aMatches = a.matches();
|
||||
boolean bMatches = b.matches();
|
||||
if (aMatches && !bMatches) {
|
||||
return 1;
|
||||
}
|
||||
if (!aMatches && bMatches) {
|
||||
return -1;
|
||||
}
|
||||
if (!aMatches) {
|
||||
return normalizeVersion(left).compareToIgnoreCase(normalizeVersion(right));
|
||||
}
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
int compared = number(a.group(i)).compareTo(number(b.group(i)));
|
||||
if (compared != 0) {
|
||||
return compared;
|
||||
}
|
||||
}
|
||||
String aPre = a.group(4);
|
||||
String bPre = b.group(4);
|
||||
if (aPre == null && bPre != null) {
|
||||
return 1;
|
||||
}
|
||||
if (aPre != null && bPre == null) {
|
||||
return -1;
|
||||
}
|
||||
if (aPre == null) {
|
||||
return 0;
|
||||
}
|
||||
return comparePrerelease(aPre, bPre);
|
||||
}
|
||||
|
||||
private static int comparePrerelease(String left, String right) {
|
||||
String[] a = left.split("\\.");
|
||||
String[] b = right.split("\\.");
|
||||
for (int i = 0; i < Math.min(a.length, b.length); i++) {
|
||||
if (a[i].equals(b[i])) {
|
||||
continue;
|
||||
}
|
||||
boolean aNumeric = isNumericIdentifier(a[i]);
|
||||
boolean bNumeric = isNumericIdentifier(b[i]);
|
||||
if (aNumeric && bNumeric) {
|
||||
int compared = new BigInteger(a[i]).compareTo(new BigInteger(b[i]));
|
||||
if (compared != 0) {
|
||||
return compared;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (aNumeric != bNumeric) {
|
||||
return aNumeric ? -1 : 1;
|
||||
}
|
||||
int compared = a[i].compareTo(b[i]);
|
||||
if (compared != 0) {
|
||||
return compared;
|
||||
}
|
||||
}
|
||||
return Integer.compare(a.length, b.length);
|
||||
}
|
||||
|
||||
private static boolean isNumericIdentifier(String value) {
|
||||
return value.chars().allMatch(Character::isDigit);
|
||||
}
|
||||
|
||||
private static BigInteger number(String value) {
|
||||
return value == null || value.isBlank() ? BigInteger.ZERO : new BigInteger(value);
|
||||
}
|
||||
|
||||
private static String normalizeVersion(String version) {
|
||||
String value = Objects.requireNonNullElse(version, "").trim();
|
||||
return value.startsWith("v") || value.startsWith("V") ? value.substring(1) : value;
|
||||
}
|
||||
|
||||
private static String safeMessage(RuntimeException error) {
|
||||
return error instanceof ApiException ? error.getMessage() : "检查更新失败";
|
||||
}
|
||||
|
||||
private static String blankToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value;
|
||||
}
|
||||
|
||||
private static Instant parseInstant(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Instant.parse(value);
|
||||
} catch (RuntimeException ignore) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private record ReleaseInfo(String version, String notes, Instant publishedAt, List<ReleaseAsset> assets) {
|
||||
}
|
||||
|
||||
public record ReleaseAsset(String name, String downloadUrl, long size) {
|
||||
}
|
||||
|
||||
public enum UpdatePhase {
|
||||
IDLE,
|
||||
CHECKING,
|
||||
AVAILABLE,
|
||||
UP_TO_DATE,
|
||||
STARTING,
|
||||
DOWNLOADING,
|
||||
VERIFYING,
|
||||
INSTALLING,
|
||||
RESTARTING,
|
||||
SUCCEEDED,
|
||||
ROLLING_BACK,
|
||||
ROLLED_BACK,
|
||||
FAILED
|
||||
}
|
||||
|
||||
public record UpdateStatus(
|
||||
boolean configured,
|
||||
String currentVersion,
|
||||
String latestVersion,
|
||||
boolean updateAvailable,
|
||||
UpdatePhase phase,
|
||||
int progress,
|
||||
String message,
|
||||
Instant checkedAt,
|
||||
Instant publishedAt,
|
||||
String releaseNotes,
|
||||
List<ReleaseAsset> assets,
|
||||
String error
|
||||
) {
|
||||
static UpdateStatus idle(boolean configured, String currentVersion) {
|
||||
return new UpdateStatus(configured, currentVersion, null, false, UpdatePhase.IDLE, 0,
|
||||
configured ? "等待检查更新" : "在线更新尚未启用", null, null, "", List.of(), null);
|
||||
}
|
||||
|
||||
UpdateStatus withPhase(UpdatePhase next, int nextProgress, String nextMessage) {
|
||||
return new UpdateStatus(configured, currentVersion, latestVersion, updateAvailable, next,
|
||||
nextProgress, nextMessage, checkedAt, publishedAt, releaseNotes, assets, null);
|
||||
}
|
||||
|
||||
UpdateStatus failed(String reason) {
|
||||
return new UpdateStatus(configured, currentVersion, latestVersion, updateAvailable,
|
||||
UpdatePhase.FAILED, progress, "更新失败", checkedAt, publishedAt, releaseNotes, assets, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,10 +113,11 @@ public class FinancingController {
|
||||
|
||||
public record FinancingRequest(
|
||||
String code, String lender, String financingType, Double creditLimit, Double amount,
|
||||
String currency, Double rate, String startDate, String endDate, Integer termMonths,
|
||||
String currency, BigDecimal rate, String startDate, String endDate, Integer termMonths,
|
||||
String repayMethod, String status, String companySubject, String purpose, String owner) {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@PostMapping
|
||||
public ApiResp<Financing> create(@RequestBody FinancingRequest req) {
|
||||
if (req.lender() == null || req.lender().isBlank()) {
|
||||
@@ -143,6 +144,7 @@ public class FinancingController {
|
||||
return ApiResp.ok(financingRepo.save(f));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@PatchMapping("/{id}")
|
||||
public ApiResp<Financing> update(@PathVariable Long id, @RequestBody FinancingRequest req) {
|
||||
Financing f = financingRepo.findById(id)
|
||||
@@ -165,7 +167,6 @@ public class FinancingController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Transactional
|
||||
public ApiResp<Void> delete(@PathVariable Long id) {
|
||||
if (!financingRepo.existsById(id)) {
|
||||
throw new NotFoundException("financing not found: " + id);
|
||||
@@ -188,7 +189,6 @@ public class FinancingController {
|
||||
* 已有任意期次「已还」时拒绝重算,避免抹掉还款历史。
|
||||
*/
|
||||
@PostMapping("/{id}/schedule")
|
||||
@Transactional
|
||||
public ApiResp<List<RepaymentPlan>> schedule(@PathVariable Long id) {
|
||||
Financing f = financingRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("financing not found: " + id));
|
||||
@@ -200,7 +200,7 @@ public class FinancingController {
|
||||
planRepo.deleteByFinancingId(id);
|
||||
|
||||
int term = f.getTermMonths() == null || f.getTermMonths() <= 0 ? 12 : f.getTermMonths();
|
||||
double annualRate = f.getRate() == null ? 0 : f.getRate();
|
||||
double annualRate = f.getRate() == null ? 0 : f.getRate().doubleValue();
|
||||
BigDecimal principalTotal = Money.nz(f.getAmount());
|
||||
String method = f.getRepayMethod() == null ? "等额本息" : f.getRepayMethod();
|
||||
LocalDate start = parseDateOrNull(f.getStartDate());
|
||||
@@ -288,7 +288,6 @@ public class FinancingController {
|
||||
* 这条联动把"融资还款"打通到结算/支付链(与需求"还款联动结算中心生成付款单"一致)。
|
||||
*/
|
||||
@PostMapping("/repayments/{planId}/pay")
|
||||
@Transactional
|
||||
public ApiResp<RepaymentPlan> payRepayment(@PathVariable Long planId) {
|
||||
RepaymentPlan plan = planRepo.findById(planId)
|
||||
.orElseThrow(() -> new NotFoundException("repayment plan not found: " + planId));
|
||||
@@ -552,7 +551,7 @@ public class FinancingController {
|
||||
rows.add(new DebtLedgerRow(f.getId(), f.getCode(), f.getLender(), f.getFinancingType(),
|
||||
f.getCompanySubject(), f.getStatus(), principal.doubleValue(),
|
||||
paid.doubleValue(), unpaid.doubleValue(),
|
||||
f.getRate() == null ? 0 : f.getRate(),
|
||||
f.getRate() == null ? 0.0 : f.getRate().doubleValue(),
|
||||
f.getEndDate() == null ? "" : f.getEndDate(),
|
||||
daysToMaturity == Long.MAX_VALUE ? -999 : daysToMaturity,
|
||||
maturityLevel));
|
||||
@@ -561,7 +560,7 @@ public class FinancingController {
|
||||
totalUnpaid = Money.add(totalUnpaid, unpaid);
|
||||
if (f.getRate() != null) {
|
||||
weightedRateSum = Money.add(weightedRateSum,
|
||||
unpaid.multiply(BigDecimal.valueOf(f.getRate())));
|
||||
unpaid.multiply(f.getRate()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +589,6 @@ public class FinancingController {
|
||||
* 若余量 < 0 则拒绝提交,给出明确提示,要求先调整授信或融资金额。
|
||||
*/
|
||||
@PostMapping("/{id}/submit-approval")
|
||||
@Transactional
|
||||
public ApiResp<FormInstance> submitApproval(@PathVariable Long id,
|
||||
@RequestBody(required = false) ApprovalSubmitRequest req) {
|
||||
Financing f = financingRepo.findById(id)
|
||||
@@ -791,7 +789,7 @@ public class FinancingController {
|
||||
rateMap.put("7%以上", new long[]{0, 0, 0});
|
||||
for (Financing f : all) {
|
||||
if (List.of("已结清", "已驳回").contains(nvl(f.getStatus()))) continue;
|
||||
double r = f.getRate() == null ? 0 : f.getRate();
|
||||
double r = f.getRate() == null ? 0 : f.getRate().doubleValue();
|
||||
String bucket = r < 3 ? "3%以下" : r < 5 ? "3%-5%" : r < 7 ? "5%-7%" : "7%以上";
|
||||
long[] slot = rateMap.get(bucket);
|
||||
slot[0]++;
|
||||
@@ -807,7 +805,7 @@ public class FinancingController {
|
||||
// --- 高成本融资识别(年化利率 > benchmarkRate) ---
|
||||
List<Map<String, Object>> highCost = all.stream()
|
||||
.filter(f -> !List.of("已结清", "已驳回").contains(nvl(f.getStatus())))
|
||||
.filter(f -> f.getRate() != null && f.getRate() > benchmarkRate)
|
||||
.filter(f -> f.getRate() != null && f.getRate().doubleValue() > benchmarkRate)
|
||||
.map(f -> {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("financingId", f.getId());
|
||||
@@ -816,8 +814,9 @@ public class FinancingController {
|
||||
m.put("financingType", f.getFinancingType());
|
||||
m.put("amount", Money.nz(f.getAmount()).doubleValue());
|
||||
m.put("rate", f.getRate());
|
||||
m.put("excessBps", Math.round((f.getRate() - benchmarkRate) * 100));
|
||||
m.put("suggestion", f.getRate() - benchmarkRate > 2 ? "建议择机置换或提前还款" : "关注,可在续授信时争取降利率");
|
||||
double rateVal = f.getRate().doubleValue();
|
||||
m.put("excessBps", Math.round((rateVal - benchmarkRate) * 100));
|
||||
m.put("suggestion", rateVal - benchmarkRate > 2 ? "建议择机置换或提前还款" : "关注,可在续授信时争取降利率");
|
||||
return m;
|
||||
})
|
||||
.toList();
|
||||
@@ -928,7 +927,6 @@ public class FinancingController {
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/legal-reviews/link")
|
||||
@Transactional
|
||||
public ApiResp<LegalRiskEvent> linkLegalReview(@PathVariable Long id,
|
||||
@RequestBody LinkLegalReviewRequest req) {
|
||||
if (!financingRepo.existsById(id)) {
|
||||
@@ -957,7 +955,6 @@ public class FinancingController {
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/push-cost-alloc")
|
||||
@Transactional
|
||||
public ApiResp<PmtCostAllocRule> pushCostAlloc(@PathVariable Long id,
|
||||
@RequestBody(required = false) PushCostAllocRequest req) {
|
||||
Financing f = financingRepo.findById(id)
|
||||
@@ -982,7 +979,7 @@ public class FinancingController {
|
||||
.reduce(BigDecimal.ZERO, Money::add);
|
||||
// 若无还款计划,按月息估算(本金 × 年化利率 / 12)
|
||||
if (periodInterest.signum() == 0) {
|
||||
double annualRate = f.getRate() == null ? 0.0 : f.getRate();
|
||||
double annualRate = f.getRate() == null ? 0.0 : f.getRate().doubleValue();
|
||||
periodInterest = Money.of(Money.nz(f.getAmount()).doubleValue() * annualRate / 100.0 / 12.0);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ public class FinancingPolicyController {
|
||||
String description, String status, String createdBy) {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@PostMapping
|
||||
public ApiResp<FinancingPolicy> create(@RequestBody PolicyRequest req) {
|
||||
if (req.ruleName() == null || req.ruleName().isBlank()) {
|
||||
@@ -99,6 +100,7 @@ public class FinancingPolicyController {
|
||||
return ApiResp.ok(policyRepo.save(p));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@PatchMapping("/{id}")
|
||||
public ApiResp<FinancingPolicy> update(@PathVariable Long id, @RequestBody PolicyRequest req) {
|
||||
FinancingPolicy p = policyRepo.findById(id)
|
||||
@@ -115,7 +117,6 @@ public class FinancingPolicyController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Transactional
|
||||
public ApiResp<Void> delete(@PathVariable Long id) {
|
||||
if (!policyRepo.existsById(id)) {
|
||||
throw new NotFoundException("融资政策规则不存在: " + id);
|
||||
@@ -148,6 +149,7 @@ public class FinancingPolicyController {
|
||||
* - 担保方式限制:预留(financing 暂无担保字段,返回通过)。
|
||||
* hardBlock=true 规则违规时,整体 overallPassed=false,调用方可据此阻断提交。
|
||||
*/
|
||||
@Transactional
|
||||
@PostMapping("/{financingId}/compliance-check")
|
||||
public ApiResp<ComplianceCheckResult> complianceCheck(@PathVariable Long financingId) {
|
||||
Financing f = financingRepo.findById(financingId)
|
||||
@@ -176,7 +178,7 @@ public class FinancingPolicyController {
|
||||
}
|
||||
}
|
||||
case "利率上限" -> {
|
||||
double rate = f.getRate() == null ? 0 : f.getRate();
|
||||
double rate = f.getRate() == null ? 0 : f.getRate().doubleValue();
|
||||
double limit = rule.getLimitValue() == null ? 0 : rule.getLimitValue().doubleValue();
|
||||
if (limit > 0 && rate > limit) {
|
||||
passed = false;
|
||||
|
||||
@@ -114,7 +114,7 @@ public class PmtContractFeeItemController {
|
||||
LocalDate maturity = LocalDate.parse(contract.getMaturityDate());
|
||||
long days = java.time.temporal.ChronoUnit.DAYS.between(sign, maturity);
|
||||
if (days > 0) {
|
||||
BigDecimal rate = BigDecimal.valueOf(contract.getContractRate()).divide(
|
||||
BigDecimal rate = contract.getContractRate().divide(
|
||||
BigDecimal.valueOf(100), 10, java.math.RoundingMode.HALF_UP);
|
||||
BigDecimal years = BigDecimal.valueOf(days).divide(
|
||||
BigDecimal.valueOf(365), 10, java.math.RoundingMode.HALF_UP);
|
||||
@@ -164,7 +164,6 @@ public class PmtContractFeeItemController {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Transactional
|
||||
public ApiResp<PmtContractFeeItem> create(@RequestBody FeeItemRequest req) {
|
||||
if (req.contractId() == null) {
|
||||
throw new ApiException(400, "contractId 不能为空");
|
||||
@@ -197,7 +196,6 @@ public class PmtContractFeeItemController {
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
@Transactional
|
||||
public ApiResp<PmtContractFeeItem> update(@PathVariable Long id,
|
||||
@RequestBody FeeItemRequest req) {
|
||||
PmtContractFeeItem item = feeRepo.findById(id)
|
||||
@@ -221,7 +219,6 @@ public class PmtContractFeeItemController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Transactional
|
||||
public ApiResp<Void> delete(@PathVariable Long id) {
|
||||
PmtContractFeeItem item = feeRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("费用明细不存在: " + id));
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
@@ -73,11 +74,12 @@ public class PmtFinancingContractController {
|
||||
|
||||
public record ContractRequest(
|
||||
Long financingId, String contractNo, String lender,
|
||||
Double contractAmount, Double contractRate, String signDate, String maturityDate,
|
||||
Double contractAmount, BigDecimal contractRate, String signDate, String maturityDate,
|
||||
String repayMethod, String guaranteeType, String guaranteeDesc,
|
||||
Double contractFee, String status, String remark, String owner) {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@PostMapping
|
||||
public ApiResp<PmtFinancingContract> create(@RequestBody ContractRequest req) {
|
||||
if (req.lender() == null || req.lender().isBlank()) {
|
||||
@@ -115,6 +117,7 @@ public class PmtFinancingContractController {
|
||||
return ApiResp.ok(contractRepo.save(c));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@PatchMapping("/{id}")
|
||||
public ApiResp<PmtFinancingContract> update(@PathVariable Long id, @RequestBody ContractRequest req) {
|
||||
PmtFinancingContract c = contractRepo.findById(id)
|
||||
@@ -138,7 +141,6 @@ public class PmtFinancingContractController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Transactional
|
||||
public ApiResp<Void> delete(@PathVariable Long id) {
|
||||
PmtFinancingContract c = contractRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("融资合同不存在: " + id));
|
||||
@@ -153,7 +155,6 @@ public class PmtFinancingContractController {
|
||||
|
||||
/** 签署:草稿 → 已签署。 */
|
||||
@PostMapping("/{id}/sign")
|
||||
@Transactional
|
||||
public ApiResp<PmtFinancingContract> sign(@PathVariable Long id) {
|
||||
PmtFinancingContract c = contractRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("融资合同不存在: " + id));
|
||||
@@ -175,7 +176,6 @@ public class PmtFinancingContractController {
|
||||
* - drawnAmount >= contractAmount → 已用款
|
||||
*/
|
||||
@PostMapping("/{id}/drawdown")
|
||||
@Transactional
|
||||
public ApiResp<PmtFinancingContract> drawdown(@PathVariable Long id, @RequestBody DrawdownRequest req) {
|
||||
PmtFinancingContract c = contractRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("融资合同不存在: " + id));
|
||||
@@ -219,7 +219,6 @@ public class PmtFinancingContractController {
|
||||
|
||||
/** 结清:将合同状态置「已结清」(通常在所有还款完成后调用)。 */
|
||||
@PostMapping("/{id}/settle")
|
||||
@Transactional
|
||||
public ApiResp<PmtFinancingContract> settle(@PathVariable Long id) {
|
||||
PmtFinancingContract c = contractRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("融资合同不存在: " + id));
|
||||
@@ -232,7 +231,6 @@ public class PmtFinancingContractController {
|
||||
|
||||
/** 终止合同。 */
|
||||
@PostMapping("/{id}/terminate")
|
||||
@Transactional
|
||||
public ApiResp<PmtFinancingContract> terminate(@PathVariable Long id) {
|
||||
PmtFinancingContract c = contractRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("融资合同不存在: " + id));
|
||||
|
||||
@@ -82,7 +82,7 @@ public class PmtInternalTransController {
|
||||
public record TransRequest(
|
||||
String transType, String payerSubject, String receiverSubject,
|
||||
Double amount, String bizDate, String reconPeriod, String summary,
|
||||
Double interestRate, String interestFrom, String interestTo,
|
||||
BigDecimal interestRate, String interestFrom, String interestTo,
|
||||
String createdBy) {
|
||||
}
|
||||
|
||||
@@ -91,7 +91,6 @@ public class PmtInternalTransController {
|
||||
* 同时自动为付款方生成应付单(ArApItem T_AP)、为收款方生成应收单(ArApItem T_AR)。
|
||||
*/
|
||||
@PostMapping
|
||||
@Transactional
|
||||
public ApiResp<PmtInternalTrans> create(@RequestBody TransRequest req) {
|
||||
if (req.payerSubject() == null || req.payerSubject().isBlank()) {
|
||||
throw new ApiException(400, "付款方主体(payerSubject) 不能为空");
|
||||
@@ -123,7 +122,7 @@ public class PmtInternalTransController {
|
||||
// 利息自动计算(仅 transType=利息 且提供了利率和区间)
|
||||
if ("利息".equals(t.getTransType()) && req.interestRate() != null
|
||||
&& req.interestFrom() != null && req.interestTo() != null) {
|
||||
BigDecimal interest = calcInterest(Money.of(req.amount()), req.interestRate(),
|
||||
BigDecimal interest = calcInterest(Money.of(req.amount()), req.interestRate().doubleValue(),
|
||||
req.interestFrom(), req.interestTo());
|
||||
t.setInterestAmount(interest);
|
||||
t.setAmount(interest); // 利息单,金额即利息
|
||||
@@ -166,6 +165,7 @@ public class PmtInternalTransController {
|
||||
return ApiResp.ok(transRepo.save(saved));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@PatchMapping("/{id}")
|
||||
public ApiResp<PmtInternalTrans> update(@PathVariable Long id, @RequestBody TransRequest req) {
|
||||
PmtInternalTrans t = transRepo.findById(id)
|
||||
@@ -180,7 +180,6 @@ public class PmtInternalTransController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Transactional
|
||||
public ApiResp<Void> delete(@PathVariable Long id) {
|
||||
PmtInternalTrans t = transRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("内部往来单不存在: " + id));
|
||||
@@ -202,7 +201,6 @@ public class PmtInternalTransController {
|
||||
* 「有差异」状态需登记差异调整后方可结清。
|
||||
*/
|
||||
@PostMapping("/{id}/confirm")
|
||||
@Transactional
|
||||
public ApiResp<PmtInternalTrans> confirm(@PathVariable Long id, @RequestBody ConfirmRequest req) {
|
||||
PmtInternalTrans t = transRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("内部往来单不存在: " + id));
|
||||
@@ -235,7 +233,6 @@ public class PmtInternalTransController {
|
||||
|
||||
/** 标记差异已调整,进入可结清状态。 */
|
||||
@PostMapping("/{id}/resolve-diff")
|
||||
@Transactional
|
||||
public ApiResp<PmtInternalTrans> resolveDiff(@PathVariable Long id,
|
||||
@RequestBody Map<String, String> body) {
|
||||
PmtInternalTrans t = transRepo.findById(id)
|
||||
@@ -251,7 +248,6 @@ public class PmtInternalTransController {
|
||||
|
||||
/** 手动结清内部往来单(在「已确认」或「差异已调整」状态下可执行)。 */
|
||||
@PostMapping("/{id}/settle")
|
||||
@Transactional
|
||||
public ApiResp<PmtInternalTrans> settle(@PathVariable Long id) {
|
||||
PmtInternalTrans t = transRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("内部往来单不存在: " + id));
|
||||
@@ -290,7 +286,7 @@ public class PmtInternalTransController {
|
||||
|
||||
// ---------- 利息计算(内部计息,按日) ----------
|
||||
|
||||
public record InterestCalcRequest(Double principal, Double annualRate, String from, String to) {
|
||||
public record InterestCalcRequest(Double principal, BigDecimal annualRate, String from, String to) {
|
||||
}
|
||||
|
||||
public record InterestCalcResult(double principal, double annualRate, String from, String to,
|
||||
@@ -301,6 +297,7 @@ public class PmtInternalTransController {
|
||||
* 内部计息预览(不落库):金额 × 年化利率 / 365 × 天数。
|
||||
* 支持活期(上存利率)与贷款利率,利率单位 % ,如 3.5 表示 3.5%。
|
||||
*/
|
||||
@Transactional
|
||||
@PostMapping("/calc-interest")
|
||||
public ApiResp<InterestCalcResult> calcInterestPreview(@RequestBody InterestCalcRequest req) {
|
||||
if (req.principal() == null || req.annualRate() == null
|
||||
@@ -312,9 +309,10 @@ public class PmtInternalTransController {
|
||||
if (days <= 0) {
|
||||
throw new ApiException(400, "计息截止日必须晚于起始日");
|
||||
}
|
||||
BigDecimal interest = calcInterest(principal, req.annualRate(), req.from(), req.to());
|
||||
double annualRateDouble = req.annualRate().doubleValue();
|
||||
BigDecimal interest = calcInterest(principal, annualRateDouble, req.from(), req.to());
|
||||
return ApiResp.ok(new InterestCalcResult(
|
||||
principal.doubleValue(), req.annualRate(), req.from(), req.to(),
|
||||
principal.doubleValue(), annualRateDouble, req.from(), req.to(),
|
||||
days, interest.doubleValue()));
|
||||
}
|
||||
|
||||
@@ -326,7 +324,6 @@ public class PmtInternalTransController {
|
||||
* 凭证生成后状态流转为「已结清」(已转凭证即代表本期利息已处理完毕)。
|
||||
*/
|
||||
@PostMapping("/{id}/to-voucher")
|
||||
@Transactional
|
||||
public ApiResp<Voucher> toVoucher(@PathVariable Long id) {
|
||||
PmtInternalTrans t = transRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("内部往来单不存在: " + id));
|
||||
|
||||
@@ -73,18 +73,17 @@ public class PmtLoanSchemeController {
|
||||
|
||||
public record SchemeRequest(
|
||||
Long financingId, String institution, String loanType,
|
||||
Double amount, Double annualRate, Integer termMonths, String repayMethod,
|
||||
Double handlingFee, String guaranteeType, Double guaranteeRate,
|
||||
Double amount, BigDecimal annualRate, Integer termMonths, String repayMethod,
|
||||
Double handlingFee, String guaranteeType, BigDecimal guaranteeRate,
|
||||
String drawdownConditions, String status, String remark, String owner) {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Transactional
|
||||
public ApiResp<PmtLoanScheme> create(@RequestBody SchemeRequest req) {
|
||||
if (req.institution() == null || req.institution().isBlank()) {
|
||||
throw new ApiException(400, "报价机构名称(institution) 不能为空");
|
||||
}
|
||||
if (req.annualRate() == null || req.annualRate() < 0) {
|
||||
if (req.annualRate() == null || req.annualRate().compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new ApiException(400, "年化利率(annualRate) 不能为空且须 >= 0");
|
||||
}
|
||||
PmtLoanScheme s = new PmtLoanScheme();
|
||||
@@ -96,7 +95,7 @@ public class PmtLoanSchemeController {
|
||||
s.setRepayMethod(req.repayMethod() == null ? "等额本息" : req.repayMethod());
|
||||
s.setHandlingFee(Money.of(req.handlingFee()));
|
||||
s.setGuaranteeType(req.guaranteeType());
|
||||
s.setGuaranteeRate(req.guaranteeRate() == null ? 0.0 : req.guaranteeRate());
|
||||
s.setGuaranteeRate(req.guaranteeRate() == null ? BigDecimal.ZERO : req.guaranteeRate());
|
||||
s.setDrawdownConditions(req.drawdownConditions());
|
||||
s.setOwner(req.owner());
|
||||
s.setStatus(req.status() == null || req.status().isBlank() ? "待比选" : req.status());
|
||||
@@ -118,7 +117,6 @@ public class PmtLoanSchemeController {
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
@Transactional
|
||||
public ApiResp<PmtLoanScheme> update(@PathVariable Long id, @RequestBody SchemeRequest req) {
|
||||
PmtLoanScheme s = schemeRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("融资方案不存在: " + id));
|
||||
@@ -144,7 +142,6 @@ public class PmtLoanSchemeController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Transactional
|
||||
public ApiResp<Void> delete(@PathVariable Long id) {
|
||||
PmtLoanScheme s = schemeRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("融资方案不存在: " + id));
|
||||
@@ -162,7 +159,6 @@ public class PmtLoanSchemeController {
|
||||
* 同时重算各方案的综合成本率(幂等)。
|
||||
*/
|
||||
@PostMapping("/rank")
|
||||
@Transactional
|
||||
public ApiResp<List<PmtLoanScheme>> rank(@RequestParam Long financingId) {
|
||||
List<PmtLoanScheme> schemes = schemeRepo.findByFinancingId(financingId);
|
||||
if (schemes.isEmpty()) {
|
||||
@@ -172,9 +168,9 @@ public class PmtLoanSchemeController {
|
||||
schemes.forEach(s -> s.setEffectiveCostRate(calcEffectiveCostRate(s)));
|
||||
// 按综合成本升序排
|
||||
schemes.sort((a, b) -> {
|
||||
double ra = a.getEffectiveCostRate() == null ? Double.MAX_VALUE : a.getEffectiveCostRate();
|
||||
double rb = b.getEffectiveCostRate() == null ? Double.MAX_VALUE : b.getEffectiveCostRate();
|
||||
return Double.compare(ra, rb);
|
||||
BigDecimal ra = a.getEffectiveCostRate() == null ? BigDecimal.valueOf(Double.MAX_VALUE) : a.getEffectiveCostRate();
|
||||
BigDecimal rb = b.getEffectiveCostRate() == null ? BigDecimal.valueOf(Double.MAX_VALUE) : b.getEffectiveCostRate();
|
||||
return ra.compareTo(rb);
|
||||
});
|
||||
for (int i = 0; i < schemes.size(); i++) {
|
||||
schemes.get(i).setRank(i + 1);
|
||||
@@ -188,7 +184,6 @@ public class PmtLoanSchemeController {
|
||||
|
||||
/** 标注推荐方案(首选)。同一 financingId 下只能有一个推荐方案;旧推荐自动清除。 */
|
||||
@PostMapping("/{id}/recommend")
|
||||
@Transactional
|
||||
public ApiResp<PmtLoanScheme> recommend(@PathVariable Long id,
|
||||
@RequestBody(required = false) RecommendRequest req) {
|
||||
PmtLoanScheme s = schemeRepo.findById(id)
|
||||
@@ -212,7 +207,6 @@ public class PmtLoanSchemeController {
|
||||
* 同时回写 Financing 台账的 lender/rate/repayMethod,融资状态推进到「审批中」。
|
||||
*/
|
||||
@PostMapping("/{id}/select")
|
||||
@Transactional
|
||||
public ApiResp<PmtLoanScheme> select(@PathVariable Long id) {
|
||||
PmtLoanScheme s = schemeRepo.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException("融资方案不存在: " + id));
|
||||
@@ -258,9 +252,9 @@ public class PmtLoanSchemeController {
|
||||
if (schemes.isEmpty()) {
|
||||
return ApiResp.ok(new SchemeCompareSummary(schemes, 0, 0, ""));
|
||||
}
|
||||
double min = schemes.stream().mapToDouble(s -> s.getEffectiveCostRate() == null ? 0 : s.getEffectiveCostRate())
|
||||
double min = schemes.stream().mapToDouble(s -> s.getEffectiveCostRate() == null ? 0 : s.getEffectiveCostRate().doubleValue())
|
||||
.min().orElse(0);
|
||||
double max = schemes.stream().mapToDouble(s -> s.getEffectiveCostRate() == null ? 0 : s.getEffectiveCostRate())
|
||||
double max = schemes.stream().mapToDouble(s -> s.getEffectiveCostRate() == null ? 0 : s.getEffectiveCostRate().doubleValue())
|
||||
.max().orElse(0);
|
||||
String recommended = schemes.stream()
|
||||
.filter(s -> Boolean.TRUE.equals(s.getRecommended()))
|
||||
@@ -276,11 +270,11 @@ public class PmtLoanSchemeController {
|
||||
* 利息按等额本息近似计算总利息;担保费按本金 × 年担保费率 × 期限(年) 计算。
|
||||
* 真实 XIRR 需逐笔现金流,此处为可读近似值,误差在 0.1% 以内(满足决策比选精度)。
|
||||
*/
|
||||
private double calcEffectiveCostRate(PmtLoanScheme s) {
|
||||
private BigDecimal calcEffectiveCostRate(PmtLoanScheme s) {
|
||||
double principal = s.getAmount() == null ? 0 : s.getAmount().doubleValue();
|
||||
if (principal <= 0) return 0.0;
|
||||
if (principal <= 0) return BigDecimal.ZERO;
|
||||
int term = s.getTermMonths() == null || s.getTermMonths() <= 0 ? 12 : s.getTermMonths();
|
||||
double annualRate = s.getAnnualRate() == null ? 0 : s.getAnnualRate();
|
||||
double annualRate = s.getAnnualRate() == null ? 0 : s.getAnnualRate().doubleValue();
|
||||
double monthlyRate = annualRate / 100.0 / 12.0;
|
||||
|
||||
// 总利息(等额本息口径,兼容多还款方式近似)
|
||||
@@ -297,7 +291,7 @@ public class PmtLoanSchemeController {
|
||||
double handlingFee = s.getHandlingFee() == null ? 0 : s.getHandlingFee().doubleValue();
|
||||
|
||||
// 担保费(年化率 × 期限年数 × 本金)
|
||||
double guaranteeRate = s.getGuaranteeRate() == null ? 0 : s.getGuaranteeRate();
|
||||
double guaranteeRate = s.getGuaranteeRate() == null ? 0 : s.getGuaranteeRate().doubleValue();
|
||||
double years = term / 12.0;
|
||||
double guaranteeCost = principal * guaranteeRate / 100.0 * years;
|
||||
|
||||
@@ -305,6 +299,6 @@ public class PmtLoanSchemeController {
|
||||
|
||||
// 年化综合成本率(百分比)
|
||||
double effectiveRate = (totalCost / principal) / years * 100.0;
|
||||
return BigDecimal.valueOf(effectiveRate).setScale(4, RoundingMode.HALF_UP).doubleValue();
|
||||
return BigDecimal.valueOf(effectiveRate).setScale(4, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ public class StlDebtLedgerController {
|
||||
activeCount++;
|
||||
totalDebt = Money.add(totalDebt, principal);
|
||||
if (f.getRate() != null) {
|
||||
BigDecimal rateVal = BigDecimal.valueOf(f.getRate());
|
||||
BigDecimal rateVal = f.getRate();
|
||||
rateWeightedSum = rateWeightedSum.add(principal.multiply(rateVal));
|
||||
rateWeightTotal = rateWeightTotal.add(principal);
|
||||
}
|
||||
@@ -212,7 +212,7 @@ public class StlDebtLedgerController {
|
||||
principal.doubleValue(), remaining.doubleValue(),
|
||||
paidPrin.doubleValue(), paidInterest.doubleValue(),
|
||||
overdueAmt.doubleValue(),
|
||||
f.getEndDate(), f.getRate() != null ? f.getRate() : 0.0,
|
||||
f.getEndDate(), f.getRate() != null ? f.getRate().doubleValue() : 0.0,
|
||||
f.getStatus(), planRows));
|
||||
}
|
||||
|
||||
@@ -245,7 +245,6 @@ public class StlDebtLedgerController {
|
||||
* <p>凭证科目:借方=长期借款(或按融资类型映射),贷方=银行存款,状态=草稿。
|
||||
*/
|
||||
@PostMapping("/repayments/{planId}/pay-and-voucher")
|
||||
@Transactional
|
||||
public ApiResp<RepayVoucherResult> payAndVoucher(@PathVariable Long planId) {
|
||||
RepaymentPlan plan = planRepo.findById(planId)
|
||||
.orElseThrow(() -> new NotFoundException("还款期次不存在: " + planId));
|
||||
|
||||
@@ -434,7 +434,7 @@ public class StlFundReportController {
|
||||
if (ls.getAnnualRate() == null || ls.getAmount() == null) continue;
|
||||
// 年利息 = 贷款金额 × 年化利率 / 100
|
||||
BigDecimal yearInterest = Money.nz(ls.getAmount())
|
||||
.multiply(BigDecimal.valueOf(ls.getAnnualRate() / 100.0))
|
||||
.multiply(ls.getAnnualRate().divide(BigDecimal.valueOf(100), 10, RoundingMode.HALF_UP))
|
||||
.setScale(2, RoundingMode.HALF_UP);
|
||||
externalInterest = externalInterest.add(yearInterest);
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.kaidi.oa.web;
|
||||
|
||||
import com.kaidi.oa.common.ApiResp;
|
||||
import com.kaidi.oa.service.SystemUpdateService;
|
||||
import com.kaidi.oa.service.SystemUpdateService.UpdateStatus;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** Administrator-only API for checking and installing signed Gitea releases. */
|
||||
@RestController
|
||||
@RequestMapping("/api/oa/system-update")
|
||||
public class SystemUpdateController {
|
||||
|
||||
private final SystemUpdateService updateService;
|
||||
|
||||
public SystemUpdateController(SystemUpdateService updateService) {
|
||||
this.updateService = updateService;
|
||||
}
|
||||
|
||||
@GetMapping("/status")
|
||||
public ApiResp<UpdateStatus> status() {
|
||||
return ApiResp.ok(updateService.status());
|
||||
}
|
||||
|
||||
@PostMapping("/check")
|
||||
public ApiResp<UpdateStatus> check() {
|
||||
return ApiResp.ok(updateService.check());
|
||||
}
|
||||
|
||||
@PostMapping("/install")
|
||||
public ApiResp<UpdateStatus> install(@Valid @RequestBody InstallRequest request) {
|
||||
return ApiResp.ok(updateService.install(request.version()));
|
||||
}
|
||||
|
||||
public record InstallRequest(@NotBlank String version) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user