@@ -0,0 +1,78 @@
|
||||
package com.yunhai.campusnotice.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.UniqueConstraint;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 通知审核记录表 notice_approval_record。
|
||||
* <p>
|
||||
* 每位审批人在每一步的通过/驳回动作各落一行(会签逐步留痕),含步骤名/表决方式快照,
|
||||
* 驳回重提产生新一轮(round 递增),历史轮次永久保留。唯一约束防同一人同轮同步重复处理。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "notice_approval_record",
|
||||
indexes = @Index(name = "idx_approval_record_notice", columnList = "notice_id"),
|
||||
uniqueConstraints = @UniqueConstraint(
|
||||
name = "uk_approval_record_round_step_account",
|
||||
columnNames = {"notice_id", "round", "step_no", "approver_account"}))
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class NoticeApprovalRecord extends BaseEntity {
|
||||
|
||||
/** 审批动作:1 通过 / 2 驳回 */
|
||||
public static final int ACTION_APPROVE = 1;
|
||||
public static final int ACTION_REJECT = 2;
|
||||
|
||||
/** 通知ID(关联 notice.id) */
|
||||
@Column(name = "notice_id", nullable = false)
|
||||
private Integer noticeId;
|
||||
|
||||
/** 送审轮次(1 起,每次提交审批递增) */
|
||||
@Column(name = "round", nullable = false)
|
||||
private Integer round;
|
||||
|
||||
/** 步骤序号(1 起) */
|
||||
@Column(name = "step_no", nullable = false)
|
||||
private Integer stepNo;
|
||||
|
||||
/** 步骤名称快照 */
|
||||
@Column(name = "step_name", length = 64)
|
||||
private String stepName;
|
||||
|
||||
/** 步骤表决方式快照:any 或签 / all 会签 */
|
||||
@Column(name = "vote", length = 8)
|
||||
private String vote;
|
||||
|
||||
/** 审批人登录账号 */
|
||||
@Column(name = "approver_account", length = 64, nullable = false)
|
||||
private String approverAccount;
|
||||
|
||||
/** 审批人姓名 */
|
||||
@Column(name = "approver_name", length = 64)
|
||||
private String approverName;
|
||||
|
||||
/** 审批人同步教师ID(用户中心 teaId,与 notice_approval_step.approver_tea_ids 同一 ID 空间;v15 新增) */
|
||||
@Column(name = "approver_tea_id")
|
||||
private Integer approverTeaId;
|
||||
|
||||
/** 审批动作:1 通过 / 2 驳回 */
|
||||
@Column(name = "action", nullable = false)
|
||||
private Integer action;
|
||||
|
||||
/** 审批意见 */
|
||||
@Column(name = "opinion", length = 1000)
|
||||
private String opinion;
|
||||
|
||||
/** 操作时间 */
|
||||
@Column(name = "action_time")
|
||||
private LocalDateTime actionTime;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.yunhai.campusnotice.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 通知审批步骤表 notice_approval_step。
|
||||
* <p>
|
||||
* 一次送审的有序多步流程快照:每步一个审批人组(会签=全部通过 / 或签=任一通过),
|
||||
* 依次流转,任一步驳回整单退回发布人。驳回重提时旧步骤行删除重建(当前流程定义),
|
||||
* 历史处理痕迹由 {@link NoticeApprovalRecord}(含步骤名/表决方式快照)永久保留。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "notice_approval_step",
|
||||
indexes = @Index(name = "idx_approval_step_notice", columnList = "notice_id"))
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class NoticeApprovalStep extends BaseEntity {
|
||||
|
||||
/** 步骤状态:0 待流转 / 1 审批中 / 2 已通过 / 3 已驳回 */
|
||||
public static final int STATUS_WAITING = 0;
|
||||
public static final int STATUS_ACTIVE = 1;
|
||||
public static final int STATUS_APPROVED = 2;
|
||||
public static final int STATUS_REJECTED = 3;
|
||||
|
||||
/** 通知ID(关联 notice.id) */
|
||||
@Column(name = "notice_id", nullable = false)
|
||||
private Integer noticeId;
|
||||
|
||||
/** 步骤序号(1 起,按流转顺序) */
|
||||
@Column(name = "step_no", nullable = false)
|
||||
private Integer stepNo;
|
||||
|
||||
/** 步骤名称(如「校级审核」,前端未填时按「第N步」兜底) */
|
||||
@Column(name = "step_name", length = 64)
|
||||
private String stepName;
|
||||
|
||||
/** 表决方式:any 或签(任一通过)/ all 会签(全部通过) */
|
||||
@Column(name = "vote", length = 8, nullable = false)
|
||||
private String vote = "any";
|
||||
|
||||
/** 审批人登录账号快照 JSON ["zhd","lwj"] */
|
||||
@Column(name = "approver_accounts", columnDefinition = "LONGTEXT")
|
||||
private String approverAccounts;
|
||||
|
||||
/** 审批人教师ID快照 JSON [101,102](旧 teaId 链路兼容,账号不可得时允许为空) */
|
||||
@Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT")
|
||||
private String approverTeaIds;
|
||||
|
||||
/** 审批人姓名快照 JSON(提交时点姓名,展示不受后续改名影响) */
|
||||
@Column(name = "approver_names", columnDefinition = "LONGTEXT")
|
||||
private String approverNames;
|
||||
|
||||
/** 状态:0 待流转 / 1 审批中 / 2 已通过 / 3 已驳回 */
|
||||
@Column(name = "status", nullable = false)
|
||||
private Integer status = STATUS_WAITING;
|
||||
|
||||
/** 步骤办结时间(通过/驳回落定时间) */
|
||||
@Column(name = "handle_time")
|
||||
private LocalDateTime handleTime;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.yunhai.campusnotice.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.UniqueConstraint;
|
||||
|
||||
/**
|
||||
* 审批人池 sys_approver(审核设置页维护,发布选人数据源)。
|
||||
* <p>
|
||||
* 按学校维护可被选为审批人的名单(登录账号为身份主键),停用后不出现在
|
||||
* 「发公告」审批人候选池,已有流程不受影响。Submit 时按本校启用名单校验,
|
||||
* 防止表单传入池外账号。
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "sys_approver",
|
||||
uniqueConstraints = @UniqueConstraint(
|
||||
name = "uk_approver_sch_account",
|
||||
columnNames = {"sch_id", "account"}))
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
|
||||
public class SysApprover extends BaseEntity {
|
||||
|
||||
/** 学校ID(多租户隔离) */
|
||||
@Column(name = "sch_id", nullable = false)
|
||||
private Integer schId;
|
||||
|
||||
/** 登录账号(审批身份主键,Approve/Reject 按当前登录账号匹配) */
|
||||
@Column(name = "account", length = 64, nullable = false)
|
||||
private String account;
|
||||
|
||||
/** 姓名 */
|
||||
@Column(name = "name", length = 64)
|
||||
private String name;
|
||||
|
||||
/** 所属部门ID(发文部门字典 sys_dict) */
|
||||
@Column(name = "dept_id", length = 64)
|
||||
private String deptId;
|
||||
|
||||
/** 所属部门名称快照 */
|
||||
@Column(name = "dept_name", length = 128)
|
||||
private String deptName;
|
||||
|
||||
/** 教师用户ID(用户中心,兼容旧 teaId 链路,可空) */
|
||||
@Column(name = "tea_id")
|
||||
private Integer teaId;
|
||||
|
||||
/** 状态:0 停用 / 1 启用 */
|
||||
@Column(name = "enabled", nullable = false)
|
||||
private Integer enabled = 1;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.yunhai.campusnotice.job;
|
||||
|
||||
import com.yunhai.campusnotice.service.INoticeService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 历史审批身份回填(v15 一次性任务,默认不执行)。
|
||||
*
|
||||
* <p>背景:平台 {@code DefaultLoginUserProvider} 曾把登录名同时当作 displayName 落库,
|
||||
* 历史审批轨迹/审核记录里 {@code operator_name / approver_name / publisher_name} 全是登录名,
|
||||
* 且留痕表没有教师ID列。v15 已修写入链并补列,本任务负责把存量数据一次性换过来。</p>
|
||||
*
|
||||
* <p>触发方式(二选一,跑完即可去掉):</p>
|
||||
* <ul>
|
||||
* <li>IDEA 运行配置 VM options:{@code -Dcampus-notice.backfill-approver-identity=true}</li>
|
||||
* <li>环境变量:{@code CAMPUS_NOTICE_BACKFILL_APPROVER_IDENTITY=true}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>幂等:只改「姓名为空/等于登录名」与「teaId 为 NULL」的行,重复执行无副作用;
|
||||
* devtools 热重启时重复执行同样安全。</p>
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "campus-notice.backfill-approver-identity", havingValue = "true")
|
||||
public class ApproverIdentityBackfillRunner implements ApplicationRunner {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApproverIdentityBackfillRunner.class);
|
||||
|
||||
@Autowired
|
||||
private INoticeService noticeService;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
try {
|
||||
Map<String, Integer> stat = noticeService.backfillApproverIdentity();
|
||||
log.info("[ApproverIdentityBackfill] 回填完成: {}", stat);
|
||||
} catch (Exception e) {
|
||||
// 回填失败不阻断应用启动;修正数据源(用户中心可达)后可再次执行
|
||||
log.error("[ApproverIdentityBackfill] 回填失败,请检查用户中心连通性后重跑", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 多步审批流步骤 VO(详情 approvalFlow / 编辑回显,对齐前端 ApprovalFlowStep 契约)。
|
||||
* <p>
|
||||
* status:approved 已通过 / pending 审批中 / rejected 已驳回 / waiting 待流转;
|
||||
* records 为该步全部审核动作(会签多人逐步留痕);pending 为当前步尚未处理姓名。
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.ALWAYS)
|
||||
public class ApprovalFlowVo {
|
||||
|
||||
/** 步骤名称 */
|
||||
private String name;
|
||||
|
||||
/** 表决方式:any 或签 / all 会签 */
|
||||
private String vote = "any";
|
||||
|
||||
/** 审批人账号(编辑回显用) */
|
||||
private List<String> approvers = new ArrayList<>();
|
||||
|
||||
/** 审批人教师ID(教师树选人回显用,如 api-test 测试台) */
|
||||
private List<Integer> approverTeaIds = new ArrayList<>();
|
||||
|
||||
/** 审批人姓名(展示用,与 approvers 顺序一致) */
|
||||
private List<String> approverNames = new ArrayList<>();
|
||||
|
||||
/** 步骤状态:approved / pending / rejected / waiting */
|
||||
private String status;
|
||||
|
||||
/** 该步审核记录 */
|
||||
private List<Record> records = new ArrayList<>();
|
||||
|
||||
/** 当前步待签人姓名(仅审批中步骤返回,其余为空列表) */
|
||||
private List<String> pending = new ArrayList<>();
|
||||
|
||||
/** 单步审核动作 */
|
||||
@Data
|
||||
public static class Record {
|
||||
|
||||
/** 审批人账号 */
|
||||
private String account;
|
||||
|
||||
/** 审批人姓名 */
|
||||
private String name;
|
||||
|
||||
/** 结果:approved 通过 / rejected 驳回 */
|
||||
private String result;
|
||||
|
||||
/** 操作时间(yyyy-MM-dd HH:mm:ss) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime time;
|
||||
|
||||
/** 审批意见 */
|
||||
private String remark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 审批中心标签页统计数字(v13-E3)。
|
||||
* <p>
|
||||
* 口径与 GET /api/notice/ApprovalList 各 tab 的默认过滤一致(不含 keyword 搜索条件):
|
||||
* pending=待审批(当前步审批人快照含当前登录人)/ approved=走审批流已发布 /
|
||||
* rejected=已驳回 / all=已通过+已驳回合一。
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalTabCountVo {
|
||||
|
||||
/** 待审批(需我处理)条数 */
|
||||
private int pending;
|
||||
|
||||
/** 已通过(走流程发布)条数 */
|
||||
private int approved;
|
||||
|
||||
/** 已驳回条数 */
|
||||
private int rejected;
|
||||
|
||||
/** 全部(已审合一)条数 */
|
||||
private int all;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审批流程步骤入参(NoticeRequest.flow / SubmitRequest.steps 元素)。
|
||||
* <p>
|
||||
* 前端「发公告」多步审批流编辑器产出:每步一个审批人组(登录账号列表),
|
||||
* vote=any 或签(任一通过进入下一步)/ all 会签(全部通过进入下一步);
|
||||
* name 可缺省,后端按「第N步」兜底。
|
||||
*/
|
||||
@Data
|
||||
public class FlowStepItem {
|
||||
|
||||
/** 步骤名称(可空,缺省「第N步」) */
|
||||
private String name;
|
||||
|
||||
/** 表决方式:any 或签(默认)/ all 会签 */
|
||||
private String vote = "any";
|
||||
|
||||
/** 审批人登录账号列表(与审批人池配合;api-test 测试台走教师树时用 approverTeaIds) */
|
||||
private List<String> approvers = new ArrayList<>();
|
||||
|
||||
/** 审批人教师ID列表(教师树叶子节点ID;与 approvers 二选一,按学校教师名单绑定) */
|
||||
private List<Integer> approverTeaIds;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.yunhai.campusnotice.model.notice;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 「已收到」三 tab 统计数字(v17-B1)。
|
||||
* <p>
|
||||
* 口径 = {@code GET /api/notice/List} 在<b>同一</b> keyword / type / deptId / 时间范围过滤下,
|
||||
* 分别取 readType=0(全部)、1(未读)、2(已读)的总数;readType 不作为入参(它就是被统计的三个桶),
|
||||
* 因此恒满足 {@code all = unread + read}。
|
||||
* <p>
|
||||
* 与 {@link ApprovalTabCountVo}(v13,审批中心四 tab)同范式:三者都来自列表谓词,不另立统计 SQL。
|
||||
*/
|
||||
@Data
|
||||
public class ReadTabCountVo {
|
||||
|
||||
/** 全部(可见已收公告总数) */
|
||||
private int all;
|
||||
|
||||
/** 未读(可见且无本人阅读记录) */
|
||||
private int unread;
|
||||
|
||||
/** 已读(可见且已有本人阅读记录) */
|
||||
private int read;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.NoticeApprovalRecord;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知审核记录仓储。
|
||||
*/
|
||||
public interface NoticeApprovalRecordRepository extends JpaRepository<NoticeApprovalRecord, Integer> {
|
||||
|
||||
/** 某通知全量审核记录(按落库顺序,跨轮次时间线) */
|
||||
List<NoticeApprovalRecord> findByNoticeIdOrderByIdAsc(Integer noticeId);
|
||||
|
||||
/** 批量查 N 个通知的审核记录(列表页批量装配) */
|
||||
@Query("select r from NoticeApprovalRecord r where r.noticeId in :noticeIds order by r.id asc")
|
||||
List<NoticeApprovalRecord> findByNoticeIds(@Param("noticeIds") Collection<Integer> noticeIds);
|
||||
|
||||
/** 某轮某步已处理记录(会签进度判定) */
|
||||
List<NoticeApprovalRecord> findByNoticeIdAndRoundAndStepNo(Integer noticeId, Integer round, Integer stepNo);
|
||||
|
||||
/** 按通知ID删除全部审核记录(通知删除时级联清理) */
|
||||
void deleteByNoticeId(Integer noticeId);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.NoticeApprovalStep;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通知审批步骤仓储。
|
||||
*/
|
||||
public interface NoticeApprovalStepRepository extends JpaRepository<NoticeApprovalStep, Integer> {
|
||||
|
||||
/** 某通知全部步骤(按序号升序) */
|
||||
List<NoticeApprovalStep> findByNoticeIdOrderByStepNoAsc(Integer noticeId);
|
||||
|
||||
/** 批量查 N 个通知的步骤(列表页批量装配) */
|
||||
@Query("select s from NoticeApprovalStep s where s.noticeId in :noticeIds order by s.noticeId asc, s.stepNo asc")
|
||||
List<NoticeApprovalStep> findByNoticeIds(@Param("noticeIds") Collection<Integer> noticeIds);
|
||||
|
||||
/** 某通知某步 */
|
||||
NoticeApprovalStep findByNoticeIdAndStepNo(Integer noticeId, Integer stepNo);
|
||||
|
||||
/** 删除某通知全部步骤(重新送审流程重置 / 通知删除级联) */
|
||||
@Modifying
|
||||
void deleteByNoticeId(Integer noticeId);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.yunhai.campusnotice.repository;
|
||||
|
||||
import com.yunhai.campusnotice.entity.SysApprover;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 审批人池仓储(审核设置页维护,发布选人数据源)。
|
||||
*/
|
||||
public interface SysApproverRepository extends JpaRepository<SysApprover, Integer> {
|
||||
|
||||
/** 某学校全部审批人(管理页,按姓名排序) */
|
||||
List<SysApprover> findBySchIdOrderByNameAsc(Integer schId);
|
||||
|
||||
/** 某学校启用中的审批人(发布选人候选池) */
|
||||
List<SysApprover> findBySchIdAndEnabledOrderByNameAsc(Integer schId, Integer enabled);
|
||||
|
||||
/** 按学校+账号定位(送审绑定校验) */
|
||||
Optional<SysApprover> findBySchIdAndAccount(Integer schId, String account);
|
||||
}
|
||||
Reference in New Issue
Block a user