@@ -1 +0,0 @@
|
|||||||
{"title":"【测试】curl-UTF8文件体","content":"<p>x</p>","type":1,"deptId":"1","deptName":"校长室","topmost":0,"receiveList":"ALL","fileIds":"","timingSendTime":null,"smsFlag":0,"receiptFlag":1,"publishType":"direct"}
|
|
||||||
+3
-2
@@ -91,10 +91,11 @@ if [ -n "$ID_DRAFT" ]; then PASS=$((PASS+1)); printf "%-26s | code=0 | id=%s
|
|||||||
CREATED_IDS+=("$ID_DRAFT")
|
CREATED_IDS+=("$ID_DRAFT")
|
||||||
|
|
||||||
if [ -n "$ID_DRAFT" ]; then
|
if [ -n "$ID_DRAFT" ]; then
|
||||||
printf '%s' "{\"serid\":$ID_DRAFT,\"approverAccounts\":[\"lijing\"],\"opinion\":\"请审批\"}" > "$TMPB"
|
# Submit 新契约(多步):steps 优先;匿名/无学校身份时预期 code=2(需登录态经 /api 或 manage+学校身份)
|
||||||
|
printf '%s' "{\"serid\":$ID_DRAFT,\"steps\":[{\"name\":\"校级审核\",\"vote\":\"any\",\"approvers\":[\"lijing\"]}],\"opinion\":\"请审批\"}" > "$TMPB"
|
||||||
resp=$(curl -s --max-time 15 -X POST -H "Content-Type: application/json; charset=utf-8" --data-binary "@$(cygpath -w "$TMPB" 2>/dev/null || echo "$TMPB")" "$U/notice/Submit")
|
resp=$(curl -s --max-time 15 -X POST -H "Content-Type: application/json; charset=utf-8" --data-binary "@$(cygpath -w "$TMPB" 2>/dev/null || echo "$TMPB")" "$U/notice/Submit")
|
||||||
code=$(node -e "try{console.log(JSON.parse(process.argv[1]).code)}catch(e){console.log('-')}" "$resp")
|
code=$(node -e "try{console.log(JSON.parse(process.argv[1]).code)}catch(e){console.log('-')}" "$resp")
|
||||||
if [ "$code" = "0" ]; then PASS=$((PASS+1)); printf "%-26s | code=0 | 草稿→待审批 ✅\n" "Submit"; else BIZ=$((BIZ+1)); printf "%-26s | code=%s 🟡\n" "Submit" "$code"; fi
|
if [ "$code" = "0" ]; then PASS=$((PASS+1)); printf "%-26s | code=0 | 草稿→待审批(第1步激活) ✅\n" "Submit(steps)"; else BIZ=$((BIZ+1)); printf "%-26s | code=%s 🟡 (匿名无学校身份为预期)\n" "Submit(steps)" "$code"; fi
|
||||||
fi
|
fi
|
||||||
req "Approve(匿名预期拒)" POST "/notice/Approve?serid=${ID_DRAFT:-0}&opinion=x"
|
req "Approve(匿名预期拒)" POST "/notice/Approve?serid=${ID_DRAFT:-0}&opinion=x"
|
||||||
req "Reject(匿名预期拒)" POST "/notice/Reject?serid=${ID_DRAFT:-0}&opinion=x"
|
req "Reject(匿名预期拒)" POST "/notice/Reject?serid=${ID_DRAFT:-0}&opinion=x"
|
||||||
|
|||||||
@@ -82,14 +82,21 @@ public class BaseApiController {
|
|||||||
* 组树组装/教师挂组/递归建树逻辑已下沉 IUserCenterService。
|
* 组树组装/教师挂组/递归建树逻辑已下沉 IUserCenterService。
|
||||||
*/
|
*/
|
||||||
@GetMapping("getStuTree")
|
@GetMapping("getStuTree")
|
||||||
public List<SchTreeNodeDto> getStuTree(BoaUser authUserWithDept) {
|
public List<SchTreeNodeDto> getStuTree(BoaUser authUserWithDept,@RequestParam boolean toUser) {
|
||||||
Integer schId = authUserWithDept.getCurrentRole() != null
|
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||||
? authUserWithDept.getCurrentRole().getSchId() : null;
|
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||||
return classService.getStuTree(schId);
|
return classService.getStuTree(schId,toUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("getTeaTree")
|
||||||
|
public List<SchTreeNodeDto> getTeaTree(BoaUser authUserWithDept,@RequestParam boolean toUser) {
|
||||||
|
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||||
|
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||||
|
return classService.getTeaTree(schId,toUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("GetSchoolAll")
|
@GetMapping("GetSchoolAll")
|
||||||
public List<Map<String, Object>> getSchoolAll(BoaUser authUserWithDept) {
|
public List<Map<String, Object>> GetSchoolAll(BoaUser authUserWithDept) {
|
||||||
Integer schId = authUserWithDept.getCurrentRole() != null
|
Integer schId = authUserWithDept.getCurrentRole() != null
|
||||||
? authUserWithDept.getCurrentRole().getSchId() : null;
|
? authUserWithDept.getCurrentRole().getSchId() : null;
|
||||||
return userCenterService.getSchoolAllTree(schId);
|
return userCenterService.getSchoolAllTree(schId);
|
||||||
|
|||||||
@@ -70,4 +70,74 @@ public class DictController {
|
|||||||
public void sort(@RequestParam("ids") String ids) {
|
public void sort(@RequestParam("ids") String ids) {
|
||||||
dictService.ordbyDicIds(ids);
|
dictService.ordbyDicIds(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前学校是否开通短信模块(前端写通知页据此展示「发送短信提醒」勾选)。
|
||||||
|
* 对应前端 GET dict/Sync(见 api/module/notice.ts getSchModuleIsSms)。
|
||||||
|
*/
|
||||||
|
@GetMapping("Sync")
|
||||||
|
public Boolean sync() {
|
||||||
|
return dictService.getSchModuleIsSms();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 审批人池(审核设置,多步审批选人数据源) ============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批人候选池(仅启用者):发公告「选择审批人」弹窗数据源。
|
||||||
|
*/
|
||||||
|
@GetMapping("ApproverPool")
|
||||||
|
public List<Map<String, Object>> approverPool() {
|
||||||
|
return dictService.getApproverPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批人管理列表(含停用):审核设置页表格。
|
||||||
|
*/
|
||||||
|
@GetMapping("Approver")
|
||||||
|
public List<Map<String, Object>> approverList() {
|
||||||
|
return dictService.getApproverList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增审批人(JSON body:account/name/deptId)。
|
||||||
|
*/
|
||||||
|
@PostMapping("AddApprover")
|
||||||
|
public void addApprover(@RequestBody Map<String, String> body) {
|
||||||
|
dictService.addApprover(body.get("account"), body.get("name"), body.get("deptId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新审批人(JSON body:id + 可选 name/deptId/enabled)。
|
||||||
|
*/
|
||||||
|
@PostMapping("UpdateApprover")
|
||||||
|
public void updateApprover(@RequestBody Map<String, Object> body) {
|
||||||
|
dictService.updateApprover(
|
||||||
|
toInteger(body.get("id")),
|
||||||
|
(String) body.get("name"),
|
||||||
|
(String) body.get("deptId"),
|
||||||
|
toInteger(body.get("enabled")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除审批人(form:id)。
|
||||||
|
*/
|
||||||
|
@PostMapping("DelApprover")
|
||||||
|
public void delApprover(@RequestParam("id") Integer id) {
|
||||||
|
dictService.delApprover(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map 值转 Integer(数字/数字字符串兼容),空返回 null */
|
||||||
|
private Integer toInteger(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value instanceof Number) {
|
||||||
|
return ((Number) value).intValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.valueOf(String.valueOf(value).trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.springframework.data.domain.Sort;
|
|||||||
import org.springframework.data.web.SortDefault;
|
import org.springframework.data.web.SortDefault;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.print.attribute.standard.MediaSize;
|
import javax.print.attribute.standard.MediaSize;
|
||||||
@@ -306,22 +307,23 @@ public class NoticeController {
|
|||||||
return noticeService.approverResolve(Arrays.asList(OtherUtil.convertToArray(ids)), schId);
|
return noticeService.approverResolve(Arrays.asList(OtherUtil.convertToArray(ids)), schId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================ 提交审批 ============================
|
// ============================ 提交审批(多步流程) ============================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交审批(status: 0/3→2):发布人从教师树选定审批教师(可多选,或签),
|
* 提交审批(status: 0/3→2):steps 多步审批流 [{name, vote, approvers:[登录账号]}] 优先,
|
||||||
* 写 notice_approval 提交行并 upsert 审批人记忆表。
|
* 缺省时旧 approverTeaIds 包装为单步或签。步骤落 notice_approval_step,
|
||||||
|
* 处理动作落 notice_approval_record,并 upsert 审批人记忆表。
|
||||||
*/
|
*/
|
||||||
@PostMapping("Submit")
|
@PostMapping("Submit")
|
||||||
public void submit(@RequestBody SubmitRequest request) {
|
public void submit(@RequestBody SubmitRequest request) {
|
||||||
noticeService.submit(request.getSerid(), request.getApproverTeaIds(), request.getOpinion());
|
noticeService.submit(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================ 审批通过 ============================
|
// ============================ 审批通过(当前步) ============================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批通过(或签):任一审批人通过即 status→1 并触发真实发布;
|
* 审批通过:或签任一通过/会签全部通过后流转下一步,末步通过即真实发布;
|
||||||
* 被他人先审时返回带实际处理人/时间的冲突提示。
|
* 被他人先审或已处理过时返回冲突提示。兼容 form 提交(serid/opinion)。
|
||||||
*/
|
*/
|
||||||
@PostMapping("Approve")
|
@PostMapping("Approve")
|
||||||
public void approve(@RequestParam("serid") Integer serid,
|
public void approve(@RequestParam("serid") Integer serid,
|
||||||
@@ -329,29 +331,43 @@ public class NoticeController {
|
|||||||
noticeService.approve(serid, opinion);
|
noticeService.approve(serid, opinion);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================ 审批驳回 ============================
|
// ============================ 审批驳回(当前步) ============================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批驳回(或签):任一审批人驳回即 status→3,意见必填;发布人可改后重新提交。
|
* 审批驳回:任一步驳回即 status→3,意见必填(remark 为前端别名);发布人可改后重新提交。
|
||||||
*/
|
*/
|
||||||
@PostMapping("Reject")
|
@PostMapping("Reject")
|
||||||
public void reject(@RequestParam("serid") Integer serid,
|
public void reject(@RequestParam("serid") Integer serid,
|
||||||
@RequestParam("opinion") String opinion) {
|
@RequestParam(value = "opinion", required = false) String opinion,
|
||||||
noticeService.reject(serid, opinion);
|
@RequestParam(value = "remark", required = false) String remark) {
|
||||||
|
noticeService.reject(serid, StringUtils.hasText(opinion) ? opinion : remark);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================ 审批列表(待审/已审合一) ============================
|
// ============================ 审批列表(待审/已审合一) ============================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批列表:按审批人快照含当前账号过滤(schId 隔离)。
|
* 审批列表:auditState/status=pending(默认) 待我审批 / approved 走流程已发布 /
|
||||||
* auditState=pending(默认) 待审 / reviewed 已审(含或签下他人已处理,带实际处理人)。
|
* rejected 已驳回 / all|reviewed 已审合一。行 VO 为 NoticeVo,多步审批字段:
|
||||||
|
* approvalStatus/approvalStatusDesc(进度文本)/approvalLevel/canApprove。
|
||||||
*/
|
*/
|
||||||
@GetMapping("ApprovalList")
|
@GetMapping("ApprovalList")
|
||||||
public BasePageResult<ApprovalListVo> approvalList(
|
public BasePageResult<NoticeVo> approvalList(
|
||||||
@RequestParam(value = "auditState", defaultValue = "pending") String auditState,
|
@RequestParam(value = "auditState", required = false) String auditState,
|
||||||
|
@RequestParam(value = "status", required = false) String status,
|
||||||
NoticeQuery query,
|
NoticeQuery query,
|
||||||
@SortDefault(sort = "updateTime", direction = Sort.Direction.DESC) Sort sort) {
|
@SortDefault(sort = "updateTime", direction = Sort.Direction.DESC) Sort sort) {
|
||||||
return noticeService.approvalList(auditState, query, PageUtils.toPageable(query, sort));
|
return noticeService.approvalList(
|
||||||
|
StringUtils.hasText(auditState) ? auditState : status, query, PageUtils.toPageable(query, sort));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================ 待我审批数量(菜单角标) ============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待我审批数量:待审批且当前步审批人快照含当前登录人(账号或教师ID)。
|
||||||
|
*/
|
||||||
|
@GetMapping("ApprovalCount")
|
||||||
|
public int approvalCount() {
|
||||||
|
return noticeService.approvalCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 解析 scopes JSON 参数为范围列表(空/非法返回 null = 无范围) */
|
/** 解析 scopes JSON 参数为范围列表(空/非法返回 null = 无范围) */
|
||||||
|
|||||||
@@ -165,6 +165,20 @@ public class Notice extends BaseEntity {
|
|||||||
@Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT")
|
@Column(name = "approver_tea_ids", columnDefinition = "LONGTEXT")
|
||||||
private String approverTeaIds;
|
private String approverTeaIds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前待审步骤的审批人账号快照(JSON 账号数组 ["zhd","lwj"]):随流程推进刷新为
|
||||||
|
* 当前步审批人组,ApprovalList 按「待我审批」过滤(与 approverTeaIds 双轨匹配)。
|
||||||
|
*/
|
||||||
|
@Column(name = "approver_accounts", columnDefinition = "LONGTEXT")
|
||||||
|
private String approverAccounts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前待审步骤下标(0 起,对齐前端 approvalLevel):0=第 1 步审批中;
|
||||||
|
* 走完全部步(已发布)= 步骤总数;无流程为 null。步骤全量见 notice_approval_step 表。
|
||||||
|
*/
|
||||||
|
@Column(name = "approval_level")
|
||||||
|
private Integer approvalLevel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定时发送已真正发布标记:到点调度后回写为 1,避免重复触发(改造方案 4.2)。
|
* 定时发送已真正发布标记:到点调度后回写为 1,避免重复触发(改造方案 4.2)。
|
||||||
* 立即发布的通知在真实发布时一并置 1。
|
* 立即发布的通知在真实发布时一并置 1。
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
package com.yunhai.campusnotice.model.notice;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 审批列表行 VO(notice/ApprovalList,待审/已审合一)。
|
|
||||||
* <p>已审行携带最新处理信息:或签下含「他人已处理」(handledByMe=false),
|
|
||||||
* 实际处理人/动作/时间/意见取 notice_approval 最新通过/驳回行。
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class ApprovalListVo {
|
|
||||||
|
|
||||||
private Integer id;
|
|
||||||
|
|
||||||
private String title;
|
|
||||||
|
|
||||||
/** 通知类型:1 普通 / 2 重要 */
|
|
||||||
private Integer type;
|
|
||||||
|
|
||||||
private String typeDesc;
|
|
||||||
|
|
||||||
/** 状态:2 待审批 / 1 已发布 / 3 已驳回 */
|
|
||||||
private Integer status;
|
|
||||||
|
|
||||||
private String statusDesc;
|
|
||||||
|
|
||||||
/** 发布人账号 */
|
|
||||||
private String publisherAccount;
|
|
||||||
|
|
||||||
/** 发布人姓名 */
|
|
||||||
private String publisherName;
|
|
||||||
|
|
||||||
/** 本轮审批人姓名(来自提交行 assignees 快照,列表页免查用户中心) */
|
|
||||||
private List<String> approverNames = new ArrayList<>();
|
|
||||||
|
|
||||||
/** 最新一轮提交时间 */
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
|
||||||
private LocalDateTime submitTime;
|
|
||||||
|
|
||||||
/** 是否需要学员阅读回执 */
|
|
||||||
private Integer receiptFlag;
|
|
||||||
|
|
||||||
/** 定时发送时间(审批通过后到点真实发布) */
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
|
||||||
private LocalDateTime timingSendTime;
|
|
||||||
|
|
||||||
// ==================== 已审 Tab 字段(待审 Tab 为空) ====================
|
|
||||||
|
|
||||||
/** 是否已有处理结果(已审 Tab 恒 true,冗余字段便于前端判断) */
|
|
||||||
private boolean handled;
|
|
||||||
|
|
||||||
/** 是否由当前登录人处理(false=或签下他人已处理) */
|
|
||||||
private boolean handledByMe;
|
|
||||||
|
|
||||||
/** 实际处理人账号 */
|
|
||||||
private String handledAccount;
|
|
||||||
|
|
||||||
/** 实际处理人姓名 */
|
|
||||||
private String handledName;
|
|
||||||
|
|
||||||
/** 实际处理动作:2 通过 / 3 驳回 */
|
|
||||||
private Integer handledAction;
|
|
||||||
|
|
||||||
/** 实际处理动作描述 */
|
|
||||||
private String handledActionDesc;
|
|
||||||
|
|
||||||
/** 实际处理时间 */
|
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
|
||||||
private LocalDateTime handledTime;
|
|
||||||
|
|
||||||
/** 处理意见(长文本建议进详情查看) */
|
|
||||||
private String handledOpinion;
|
|
||||||
}
|
|
||||||
@@ -93,6 +93,32 @@ public class NoticeDetailVo {
|
|||||||
@JsonProperty("approverTeaIds")
|
@JsonProperty("approverTeaIds")
|
||||||
private List<Integer> approverTeaIds;
|
private List<Integer> approverTeaIds;
|
||||||
|
|
||||||
|
// ==================== 多步审批流(审核改造,学员端 mobiapi 不返回) ====================
|
||||||
|
|
||||||
|
/** 是否启用审批流:0 否 / 1 是(存在审批步骤即为 1) */
|
||||||
|
@JsonProperty("approvalEnabled")
|
||||||
|
private Integer approvalEnabled;
|
||||||
|
|
||||||
|
/** 审批状态:none / pending / approved / rejected */
|
||||||
|
@JsonProperty("approvalStatus")
|
||||||
|
private String approvalStatus;
|
||||||
|
|
||||||
|
/** 审批状态文本(待审批带步骤进度) */
|
||||||
|
@JsonProperty("approvalStatusDesc")
|
||||||
|
private String approvalStatusDesc;
|
||||||
|
|
||||||
|
/** 当前待审步骤下标(0 起;已走完=步数;无流程=0) */
|
||||||
|
@JsonProperty("approvalLevel")
|
||||||
|
private Integer approvalLevel;
|
||||||
|
|
||||||
|
/** 当前用户是否为本单当前步待签审批人 */
|
||||||
|
@JsonProperty("canApprove")
|
||||||
|
private Boolean canApprove;
|
||||||
|
|
||||||
|
/** 多步审批流进度(含每步审核记录;也用于发布/编辑表单回显审批人配置) */
|
||||||
|
@JsonProperty("approvalFlow")
|
||||||
|
private List<ApprovalFlowVo> approvalFlow;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 附件项(旧 ChaoSend 数组元素)。
|
* 附件项(旧 ChaoSend 数组元素)。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -63,6 +63,20 @@ public class NoticeRequest {
|
|||||||
/** 发布方式:direct 直接发布(默认,兼容旧链路)/ draft 存为草稿 / approve 提交审批(需携带 approverTeaIds) */
|
/** 发布方式:direct 直接发布(默认,兼容旧链路)/ draft 存为草稿 / approve 提交审批(需携带 approverTeaIds) */
|
||||||
private String publishType;
|
private String publishType;
|
||||||
|
|
||||||
/** 提交审批时所选审批教师ID(教师树叶子节点ID,publishType=approve 时必填;独立 Submit 接口走 SubmitRequest) */
|
/** 提交审批时所选审批教师ID(教师树叶子节点ID,旧链路兼容;publishType=approve 时与 flow 二选一) */
|
||||||
private List<Integer> approverTeaIds;
|
private List<Integer> approverTeaIds;
|
||||||
|
|
||||||
|
// ==================== 多步审批流(审核改造:步骤表 + 审核记录表) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多步审批流:[{name, vote, approvers:[登录账号]}],按序流转,任一步驳回整单退回。
|
||||||
|
* 传了 flow 时忽略 approvers/vote/approverTeaIds。
|
||||||
|
*/
|
||||||
|
private List<FlowStepItem> flow;
|
||||||
|
|
||||||
|
/** 单步审批人登录账号列表(flow 缺省时的单步简写;选人即送审) */
|
||||||
|
private List<String> approvers;
|
||||||
|
|
||||||
|
/** 单步表决方式:any 或签(默认)/ all 会签(配合 approvers) */
|
||||||
|
private String vote;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,22 @@ public class NoticeVo {
|
|||||||
/** 审批教师ID快照(JSON 数组串 [101,102],发布流程展示用) */
|
/** 审批教师ID快照(JSON 数组串 [101,102],发布流程展示用) */
|
||||||
private String approverTeaIds;
|
private String approverTeaIds;
|
||||||
|
|
||||||
|
// ==================== 多步审批流(审核改造) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批状态(前端枚举):none 免审已发布 / pending 待审批 / approved 审批通过已发布 / rejected 已驳回。
|
||||||
|
*/
|
||||||
|
private String approvalStatus;
|
||||||
|
|
||||||
|
/** 审批状态文本:待审批时带步骤进度(如「第1/2步 校级审核·会签 0/2」) */
|
||||||
|
private String approvalStatusDesc;
|
||||||
|
|
||||||
|
/** 当前待审步骤下标(0 起;已走完=步数;无流程=0) */
|
||||||
|
private Integer approvalLevel;
|
||||||
|
|
||||||
|
/** 当前用户是否为本单当前步待签审批人(审批列表/详情操作按钮用) */
|
||||||
|
private Boolean canApprove;
|
||||||
|
|
||||||
/** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */
|
/** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */
|
||||||
private String UpFile;
|
private String UpFile;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import java.util.List;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交审批入参(notice/Submit)。
|
* 提交审批入参(notice/Submit)。
|
||||||
* 审批人可多选,或签语义:任一审批人通过即发布、任一驳回即驳回。
|
* 审批人可多选;steps 多步流程优先,缺省时 approverTeaIds 包装为单步或签(旧链路兼容)。
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public class SubmitRequest {
|
public class SubmitRequest {
|
||||||
@@ -14,9 +14,12 @@ public class SubmitRequest {
|
|||||||
/** 通知ID */
|
/** 通知ID */
|
||||||
private Integer serid;
|
private Integer serid;
|
||||||
|
|
||||||
/** 选定审批教师ID列表(教师树叶子节点ID,至少一人) */
|
/** 选定审批教师ID列表(教师树叶子节点ID,旧链路:单步或签) */
|
||||||
private List<Integer> approverTeaIds;
|
private List<Integer> approverTeaIds;
|
||||||
|
|
||||||
|
/** 多步审批流([{name, vote, approvers:[登录账号]}],传了则忽略 approverTeaIds) */
|
||||||
|
private List<FlowStepItem> steps;
|
||||||
|
|
||||||
/** 提交说明(可选) */
|
/** 提交说明(可选) */
|
||||||
private String opinion;
|
private String opinion;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,4 +29,7 @@ public interface NoticeApprovalRepository extends JpaRepository<NoticeApproval,
|
|||||||
|
|
||||||
/** 按通知ID删除全部审批记录(通知删除时级联清理) */
|
/** 按通知ID删除全部审批记录(通知删除时级联清理) */
|
||||||
void deleteByNoticeId(Integer noticeId);
|
void deleteByNoticeId(Integer noticeId);
|
||||||
|
|
||||||
|
/** 送审次数(提交行数)= 当前送审轮次,提交审批时递增 */
|
||||||
|
long countByNoticeIdAndAction(Integer noticeId, Integer action);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,5 +84,7 @@ public interface IClassService {
|
|||||||
* 节点契约:组节点 type=1、isParent=true、children 非 null;学生叶子 type=2、isParent=false、children=null。
|
* 节点契约:组节点 type=1、isParent=true、children 非 null;学生叶子 type=2、isParent=false、children=null。
|
||||||
* 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?SchSerID=}
|
* 对应:GET {@code {bschclass}/oapi/SchGroup/GetStuTree?SchSerID=}
|
||||||
*/
|
*/
|
||||||
List<SchTreeNodeDto> getStuTree(Integer schId);
|
List<SchTreeNodeDto> getTeaTree(Integer schId,boolean toUser);
|
||||||
|
|
||||||
|
List<SchTreeNodeDto> getStuTree(Integer schId,boolean toUser);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,4 +71,35 @@ public interface IDictService {
|
|||||||
* @return 部门名称;无匹配时返回 null
|
* @return 部门名称;无匹配时返回 null
|
||||||
*/
|
*/
|
||||||
String resolveDeptName(String deptId);
|
String resolveDeptName(String deptId);
|
||||||
|
|
||||||
|
// ==================== 审批人池(审核设置,多步审批选人数据源) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批人候选池(仅启用者):发公告选审批人数据源。
|
||||||
|
*
|
||||||
|
* @return List<Map> 每项 {id, name, account, deptName, isParent}
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getApproverPool();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批人管理列表(含停用)。
|
||||||
|
*
|
||||||
|
* @return List<Map> 每项 {id, account, name, deptId, deptName, enabled}
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getApproverList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增审批人(本校唯一账号校验,部门名称按字典解析快照)。
|
||||||
|
*/
|
||||||
|
void addApprover(String account, String name, String deptId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新审批人(null 参数不改动;enabled 仅接受 0/1)。
|
||||||
|
*/
|
||||||
|
void updateApprover(Integer id, String name, String deptId, Integer enabled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除审批人(仅本校记录)。
|
||||||
|
*/
|
||||||
|
void delApprover(Integer id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,28 +36,34 @@ public interface INoticeService {
|
|||||||
BasePageResult<NoticeVo> draftList(NoticeQuery query, Pageable pageable);
|
BasePageResult<NoticeVo> draftList(NoticeQuery query, Pageable pageable);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交审批(status: 0/3 → 2):发布人从教师树选定审批教师(可多人,或签),
|
* 提交审批(status: 0/3 → 2):request.steps 多步审批流([{name, vote, approvers:[登录账号]}])
|
||||||
* 后端按教师ID绑定学校教师校验;写 notice_approval 提交行并 upsert 审批人记忆表。
|
* 优先;缺省时旧 approverTeaIds 包装为单步或签(兼容链路)。
|
||||||
|
* 步骤落 notice_approval_step,处理动作落 notice_approval_record。
|
||||||
*/
|
*/
|
||||||
void submit(Integer serid, List<Integer> approverTeaIds, String opinion);
|
void submit(SubmitRequest request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批通过(或签):任一审批人通过即 status: 2→1(CAS),并触发真实发布;
|
* 审批通过:当前步审批人处理;或签任一通过/会签全部通过后流转下一步,
|
||||||
* 已被他人先行处理时抛业务冲突提示(带实际处理人与时间)。
|
* 末步通过 status: 2→1(CAS)并触发真实发布。已被他人先行处理时抛业务冲突提示。
|
||||||
*/
|
*/
|
||||||
void approve(Integer serid, String opinion);
|
void approve(Integer serid, String opinion);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批驳回(或签):任一审批人驳回即 status: 2→3(CAS),记录意见;发布人可改后重新提交。
|
* 审批驳回:任一步驳回即 status: 2→3(CAS),记录意见;发布人可改后重新提交。
|
||||||
*/
|
*/
|
||||||
void reject(Integer serid, String opinion);
|
void reject(Integer serid, String opinion);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批列表(待审/已审合一):按 schId + 审批教师快照含当前教师ID过滤。
|
* 审批列表:pending(默认)=待我审批 status=2;approved=走流程已发布;rejected=已驳回;
|
||||||
*
|
* all/reviewed=已审合一。行 VO 为 NoticeVo(多步审批字段:approvalStatus/approvalStatusDesc/
|
||||||
* @param auditState pending(默认)=待审 status=2;reviewed=已审 status∈{1,3}(含或签下他人已处理)
|
* approvalLevel/canApprove)。
|
||||||
*/
|
*/
|
||||||
BasePageResult<ApprovalListVo> approvalList(String auditState, NoticeQuery query, Pageable pageable);
|
BasePageResult<NoticeVo> approvalList(String auditState, NoticeQuery query, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待我审批数量(菜单角标):待审批且当前步审批人快照含当前登录人。
|
||||||
|
*/
|
||||||
|
int approvalCount();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批人记忆回显:当前发布人最近一次所选审批教师ID组合,无记忆返回空。
|
* 审批人记忆回显:当前发布人最近一次所选审批教师ID组合,无记忆返回空。
|
||||||
|
|||||||
@@ -135,12 +135,19 @@ public class ClassService implements IClassService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<SchTreeNodeDto> getStuTree(Integer schId) {
|
public List<SchTreeNodeDto> getStuTree(Integer schId,boolean toUser) {
|
||||||
return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId,
|
return exchangeList(bschClassHost + "/oapi/SchGroup/GetStuTree?schId=" + schId+"&toUser="+toUser,
|
||||||
new ParameterizedTypeReference<List<SchTreeNodeDto>>() {
|
new ParameterizedTypeReference<List<SchTreeNodeDto>>() {
|
||||||
}, "获取学生树", "schId=" + schId);
|
}, "获取学生树", "schId=" + schId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SchTreeNodeDto> getTeaTree(Integer schId,boolean toUser) {
|
||||||
|
return exchangeList(bschClassHost + "/oapi/SchGroup/GetTeaTree?schId=" + schId+"&toUser="+toUser,
|
||||||
|
new ParameterizedTypeReference<List<SchTreeNodeDto>>() {
|
||||||
|
}, "获取教师树", "schId=" + schId);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================ 通用调用 ============================
|
// ============================ 通用调用 ============================
|
||||||
|
|
||||||
private <T> List<T> exchangeList(String url, ParameterizedTypeReference<List<T>> type,
|
private <T> List<T> exchangeList(String url, ParameterizedTypeReference<List<T>> type,
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package com.yunhai.campusnotice.service.impl;
|
package com.yunhai.campusnotice.service.impl;
|
||||||
|
|
||||||
import com.yunhai.campusnotice.entity.Notice;
|
import com.yunhai.campusnotice.entity.Notice;
|
||||||
|
import com.yunhai.campusnotice.entity.SysApprover;
|
||||||
import com.yunhai.campusnotice.entity.SysDict;
|
import com.yunhai.campusnotice.entity.SysDict;
|
||||||
import com.yunhai.campusnotice.repository.NoticeRepository;
|
import com.yunhai.campusnotice.repository.NoticeRepository;
|
||||||
|
import com.yunhai.campusnotice.repository.SysApproverRepository;
|
||||||
import com.yunhai.campusnotice.repository.SysDictRepository;
|
import com.yunhai.campusnotice.repository.SysDictRepository;
|
||||||
import com.yunhai.campusnotice.service.IDictService;
|
import com.yunhai.campusnotice.service.IDictService;
|
||||||
import com.yunhai.platform.auth.LoginUser;
|
import com.yunhai.platform.auth.LoginUser;
|
||||||
import com.yunhai.platform.auth.UserContextHolder;
|
import com.yunhai.platform.auth.UserContextHolder;
|
||||||
|
import com.yunhai.platform.auth.spring.CurrentBoaUser;
|
||||||
import com.yunhai.platform.apistd.BizException;
|
import com.yunhai.platform.apistd.BizException;
|
||||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -37,6 +41,29 @@ public class DictService implements IDictService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private NoticeRepository noticeRepository;
|
private NoticeRepository noticeRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysApproverRepository sysApproverRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CurrentBoaUser currentBoaUser;
|
||||||
|
|
||||||
|
/** 当前登录人学校ID(多租户隔离,取 BoaUser.currentRole.schId) */
|
||||||
|
private Integer currentSchId() {
|
||||||
|
BoaUser boaUser = currentBoaUser.get();
|
||||||
|
if (boaUser == null || boaUser.getCurrentRole() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return boaUser.getCurrentRole().getSchId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer requireSchId() {
|
||||||
|
Integer schId = currentSchId();
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "获取学校信息失败");
|
||||||
|
}
|
||||||
|
return schId;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================ GetSysDic ============================
|
// ============================ GetSysDic ============================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -250,4 +277,96 @@ public class DictService implements IDictService {
|
|||||||
LoginUser user = UserContextHolder.get();
|
LoginUser user = UserContextHolder.get();
|
||||||
return user == null ? null : user.getLoginName();
|
return user == null ? null : user.getLoginName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================ 审批人池(审核设置) ============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getApproverPool() {
|
||||||
|
Integer schId = requireSchId();
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (SysApprover approver : sysApproverRepository.findBySchIdAndEnabledOrderByNameAsc(schId, 1)) {
|
||||||
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
|
item.put("id", approver.getId());
|
||||||
|
item.put("name", StringUtils.hasText(approver.getName()) ? approver.getName() : approver.getAccount());
|
||||||
|
item.put("account", approver.getAccount());
|
||||||
|
item.put("deptName", approver.getDeptName());
|
||||||
|
item.put("isParent", false);
|
||||||
|
result.add(item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> getApproverList() {
|
||||||
|
Integer schId = requireSchId();
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
for (SysApprover approver : sysApproverRepository.findBySchIdOrderByNameAsc(schId)) {
|
||||||
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
|
item.put("id", approver.getId());
|
||||||
|
item.put("account", approver.getAccount());
|
||||||
|
item.put("name", approver.getName());
|
||||||
|
item.put("deptId", approver.getDeptId());
|
||||||
|
item.put("deptName", approver.getDeptName());
|
||||||
|
item.put("enabled", approver.getEnabled());
|
||||||
|
result.add(item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void addApprover(String account, String name, String deptId) {
|
||||||
|
Integer schId = requireSchId();
|
||||||
|
if (!StringUtils.hasText(account)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "请填写审批人账号");
|
||||||
|
}
|
||||||
|
String acc = account.trim();
|
||||||
|
if (sysApproverRepository.findBySchIdAndAccount(schId, acc).isPresent()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "审批人「" + acc + "」已存在");
|
||||||
|
}
|
||||||
|
SysApprover approver = new SysApprover();
|
||||||
|
approver.setSchId(schId);
|
||||||
|
approver.setAccount(acc);
|
||||||
|
approver.setName(StringUtils.hasText(name) ? name.trim() : acc);
|
||||||
|
approver.setDeptId(StringUtils.hasText(deptId) ? deptId.trim() : null);
|
||||||
|
approver.setDeptName(resolveDeptName(approver.getDeptId()));
|
||||||
|
approver.setEnabled(1);
|
||||||
|
sysApproverRepository.save(approver);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void updateApprover(Integer id, String name, String deptId, Integer enabled) {
|
||||||
|
Integer schId = requireSchId();
|
||||||
|
SysApprover approver = requireOwnSchool(schId, id);
|
||||||
|
if (StringUtils.hasText(name)) {
|
||||||
|
approver.setName(name.trim());
|
||||||
|
}
|
||||||
|
if (deptId != null) {
|
||||||
|
approver.setDeptId(StringUtils.hasText(deptId) ? deptId.trim() : null);
|
||||||
|
approver.setDeptName(resolveDeptName(approver.getDeptId()));
|
||||||
|
}
|
||||||
|
if (enabled != null) {
|
||||||
|
approver.setEnabled(enabled == 0 ? 0 : 1);
|
||||||
|
}
|
||||||
|
sysApproverRepository.save(approver);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delApprover(Integer id) {
|
||||||
|
Integer schId = requireSchId();
|
||||||
|
SysApprover approver = requireOwnSchool(schId, id);
|
||||||
|
sysApproverRepository.delete(approver);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 定位本校审批人记录(跨校 ID 一律视为不存在) */
|
||||||
|
private SysApprover requireOwnSchool(Integer schId, Integer id) {
|
||||||
|
if (id == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "参数错误");
|
||||||
|
}
|
||||||
|
return sysApproverRepository.findById(id)
|
||||||
|
.filter(a -> schId != null && schId.equals(a.getSchId()))
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "审批人不存在: " + id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ package com.yunhai.campusnotice.service.impl;
|
|||||||
|
|
||||||
import com.yunhai.campusnotice.entity.Notice;
|
import com.yunhai.campusnotice.entity.Notice;
|
||||||
import com.yunhai.campusnotice.entity.NoticeApproval;
|
import com.yunhai.campusnotice.entity.NoticeApproval;
|
||||||
|
import com.yunhai.campusnotice.entity.NoticeApprovalRecord;
|
||||||
|
import com.yunhai.campusnotice.entity.NoticeApprovalStep;
|
||||||
import com.yunhai.campusnotice.entity.NoticeApproverMemory;
|
import com.yunhai.campusnotice.entity.NoticeApproverMemory;
|
||||||
import com.yunhai.campusnotice.entity.NoticePushScope;
|
import com.yunhai.campusnotice.entity.NoticePushScope;
|
||||||
import com.yunhai.campusnotice.entity.NoticeRead;
|
import com.yunhai.campusnotice.entity.NoticeRead;
|
||||||
import com.yunhai.campusnotice.entity.NoticeReceiver;
|
import com.yunhai.campusnotice.entity.NoticeReceiver;
|
||||||
|
import com.yunhai.campusnotice.entity.SysApprover;
|
||||||
import com.yunhai.campusnotice.entity.UpFile;
|
import com.yunhai.campusnotice.entity.UpFile;
|
||||||
import com.yunhai.campusnotice.enums.ApprovalActionEnum;
|
import com.yunhai.campusnotice.enums.ApprovalActionEnum;
|
||||||
import com.yunhai.campusnotice.enums.NoticeStatusEnum;
|
import com.yunhai.campusnotice.enums.NoticeStatusEnum;
|
||||||
@@ -19,12 +22,15 @@ import com.yunhai.campusnotice.model.bsch.SchInfoDto;
|
|||||||
import com.yunhai.campusnotice.model.bsch.SchStudentDto;
|
import com.yunhai.campusnotice.model.bsch.SchStudentDto;
|
||||||
import com.yunhai.campusnotice.model.bsch.SchSystemDto;
|
import com.yunhai.campusnotice.model.bsch.SchSystemDto;
|
||||||
import com.yunhai.campusnotice.model.notice.*;
|
import com.yunhai.campusnotice.model.notice.*;
|
||||||
|
import com.yunhai.campusnotice.repository.NoticeApprovalRecordRepository;
|
||||||
import com.yunhai.campusnotice.repository.NoticeApprovalRepository;
|
import com.yunhai.campusnotice.repository.NoticeApprovalRepository;
|
||||||
|
import com.yunhai.campusnotice.repository.NoticeApprovalStepRepository;
|
||||||
import com.yunhai.campusnotice.repository.NoticeApproverMemoryRepository;
|
import com.yunhai.campusnotice.repository.NoticeApproverMemoryRepository;
|
||||||
import com.yunhai.campusnotice.repository.NoticePushScopeRepository;
|
import com.yunhai.campusnotice.repository.NoticePushScopeRepository;
|
||||||
import com.yunhai.campusnotice.repository.NoticeReadRepository;
|
import com.yunhai.campusnotice.repository.NoticeReadRepository;
|
||||||
import com.yunhai.campusnotice.repository.NoticeReceiverRepository;
|
import com.yunhai.campusnotice.repository.NoticeReceiverRepository;
|
||||||
import com.yunhai.campusnotice.repository.NoticeRepository;
|
import com.yunhai.campusnotice.repository.NoticeRepository;
|
||||||
|
import com.yunhai.campusnotice.repository.SysApproverRepository;
|
||||||
import com.yunhai.campusnotice.repository.UpFileRepository;
|
import com.yunhai.campusnotice.repository.UpFileRepository;
|
||||||
import com.yunhai.campusnotice.service.IClassService;
|
import com.yunhai.campusnotice.service.IClassService;
|
||||||
import com.yunhai.campusnotice.service.IFileService;
|
import com.yunhai.campusnotice.service.IFileService;
|
||||||
@@ -40,6 +46,7 @@ import com.yunhai.platform.openapi.client.YschBaseOpenApi;
|
|||||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
import com.yunhai.platform.utils.model.BoaGroup;
|
import com.yunhai.platform.utils.model.BoaGroup;
|
||||||
import com.yunhai.platform.utils.model.BoaUser;
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
@@ -95,9 +102,21 @@ public class NoticeService implements INoticeService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private NoticeApprovalRepository approvalRepository;
|
private NoticeApprovalRepository approvalRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private NoticeApprovalStepRepository approvalStepRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private NoticeApprovalRecordRepository approvalRecordRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysApproverRepository sysApproverRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private NoticeApproverMemoryRepository approverMemoryRepository;
|
private NoticeApproverMemoryRepository approverMemoryRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private UpFileRepository upFileRepository;
|
private UpFileRepository upFileRepository;
|
||||||
|
|
||||||
@@ -185,6 +204,9 @@ public class NoticeService implements INoticeService {
|
|||||||
public Integer publish(NoticeRequest request) {
|
public Integer publish(NoticeRequest request) {
|
||||||
String publishType = request.getPublishType() == null
|
String publishType = request.getPublishType() == null
|
||||||
? "direct" : request.getPublishType().trim().toLowerCase();
|
? "direct" : request.getPublishType().trim().toLowerCase();
|
||||||
|
// 选人即送审:带审批流(flow 多步 / approvers 单步 / 旧 approverTeaIds)即进审批,
|
||||||
|
// 不依赖 publishType(对齐前端「勾选审批才发 flow」的行为)
|
||||||
|
List<FlowStepItem> flow = resolveNoticeFlow(request);
|
||||||
|
|
||||||
Notice entity = new Notice();
|
Notice entity = new Notice();
|
||||||
BeanUtils.copyProperties(request, entity);
|
BeanUtils.copyProperties(request, entity);
|
||||||
@@ -203,11 +225,12 @@ public class NoticeService implements INoticeService {
|
|||||||
entity.setStatus(DRAFT_CODE);
|
entity.setStatus(DRAFT_CODE);
|
||||||
noticeRepository.save(entity);
|
noticeRepository.save(entity);
|
||||||
savePushScopes(entity.getId(), request.getPushScopes());
|
savePushScopes(entity.getId(), request.getPushScopes());
|
||||||
|
saveFlowDefinition(entity, flow, request.getApproverTeaIds());
|
||||||
fileService.bindToTopic(parseFileIds(request.getFileIds()), entity.getId());
|
fileService.bindToTopic(parseFileIds(request.getFileIds()), entity.getId());
|
||||||
return entity.getId();
|
return entity.getId();
|
||||||
}
|
}
|
||||||
if ("approve".equals(publishType)) {
|
if ("approve".equals(publishType) || !flow.isEmpty()) {
|
||||||
doSubmitApproval(entity, request.getApproverTeaIds(), null);
|
doSubmitApproval(entity, flow, null, null);
|
||||||
savePushScopes(entity.getId(), request.getPushScopes());
|
savePushScopes(entity.getId(), request.getPushScopes());
|
||||||
fileService.bindToTopic(parseFileIds(request.getFileIds()), entity.getId());
|
fileService.bindToTopic(parseFileIds(request.getFileIds()), entity.getId());
|
||||||
return entity.getId();
|
return entity.getId();
|
||||||
@@ -243,8 +266,9 @@ public class NoticeService implements INoticeService {
|
|||||||
entity.setReceiptFlag(request.getReceiptFlag() == null ? 0 : request.getReceiptFlag());
|
entity.setReceiptFlag(request.getReceiptFlag() == null ? 0 : request.getReceiptFlag());
|
||||||
entity.setTimingPublished(0);
|
entity.setTimingPublished(0);
|
||||||
noticeRepository.save(entity);
|
noticeRepository.save(entity);
|
||||||
// 草稿仅存内容 + 范围快照 + 附件,不圈人、不发短信、不落接收人
|
// 草稿仅存内容 + 范围快照 + 附件 + 审批流定义,不圈人、不发短信、不落接收人
|
||||||
savePushScopes(entity.getId(), request.getPushScopes());
|
savePushScopes(entity.getId(), request.getPushScopes());
|
||||||
|
saveFlowDefinition(entity, resolveNoticeFlow(request), request.getApproverTeaIds());
|
||||||
fileService.bindToTopic(parseFileIds(request.getFileIds()), entity.getId());
|
fileService.bindToTopic(parseFileIds(request.getFileIds()), entity.getId());
|
||||||
return entity.getId();
|
return entity.getId();
|
||||||
}
|
}
|
||||||
@@ -447,58 +471,157 @@ public class NoticeService implements INoticeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================ 审批(选人、多人或签、记忆,改造方案 4.3) ============================
|
// ============================ 审批(多步流程:步骤表 + 审核记录表) ============================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void submit(Integer serid, List<Integer> approverTeaIds, String opinion) {
|
public void submit(SubmitRequest request) {
|
||||||
Notice notice = loadNotice(serid);
|
Notice notice = loadNotice(request.getSerid());
|
||||||
doSubmitApproval(notice, approverTeaIds, opinion);
|
doSubmitApproval(notice, resolveRequestFlow(request.getSteps(), request.getApproverTeaIds()),
|
||||||
|
request.getApproverTeaIds(), request.getOpinion());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交审批装配:按学校教师绑定校验 → status: 0/3→2 → 审批教师快照落主表 →
|
* 提交审批装配:流程绑定校验 → status: 0/3→2 → 主表落库拿通知ID →
|
||||||
* notice_approval 提交行(assignee_tea_ids 快照)→ upsert 记忆表。
|
* 步骤落 notice_approval_step(第 1 步审批中,其余待流转)→ 当前步快照落主表 →
|
||||||
|
* notice_approval 提交行 → upsert 记忆表。
|
||||||
|
* 草稿已存流程定义(全待流转步)且本次未重选人时,直接激活存量定义。
|
||||||
*/
|
*/
|
||||||
private void doSubmitApproval(Notice entity, List<Integer> approverTeaIds, String opinion) {
|
private void doSubmitApproval(Notice entity, List<FlowStepItem> flowItems, List<Integer> legacyTeaIds, String opinion) {
|
||||||
LinkedHashMap<Integer, String> nameById = bindApprovers(entity.getSchId(), approverTeaIds);
|
|
||||||
int status = entity.getStatus() == null ? PUBLISHED_CODE : entity.getStatus();
|
int status = entity.getStatus() == null ? PUBLISHED_CODE : entity.getStatus();
|
||||||
if (status != DRAFT_CODE && status != REJECTED_CODE) {
|
if (status != DRAFT_CODE && status != REJECTED_CODE) {
|
||||||
throw new BizException(ErrorCode.FORBIDDEN, "仅草稿或已驳回状态的通知可提交审批");
|
throw new BizException(ErrorCode.FORBIDDEN, "仅草稿或已驳回状态的通知可提交审批");
|
||||||
}
|
}
|
||||||
|
List<NoticeApprovalStep> steps = buildSteps(entity, flowItems, legacyTeaIds);
|
||||||
|
if (steps.isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "请选择审批人");
|
||||||
|
}
|
||||||
|
// 全待流转 = 草稿流程定义复用;否则视为新构建流程(清掉上一轮旧步骤)
|
||||||
|
boolean reuseDefinition = steps.stream()
|
||||||
|
.allMatch(s -> s.getStatus() != null && s.getStatus() == NoticeApprovalStep.STATUS_WAITING);
|
||||||
|
NoticeApprovalStep first = steps.get(0);
|
||||||
entity.setStatus(PENDING_CODE);
|
entity.setStatus(PENDING_CODE);
|
||||||
entity.setApproverTeaIds(toJsonTeaIds(new ArrayList<>(nameById.keySet())));
|
entity.setApprovalLevel(0);
|
||||||
|
entity.setApproverAccounts(first.getApproverAccounts());
|
||||||
|
entity.setApproverTeaIds(first.getApproverTeaIds());
|
||||||
noticeRepository.save(entity);
|
noticeRepository.save(entity);
|
||||||
|
Integer noticeId = entity.getId();
|
||||||
|
|
||||||
saveApprovalRow(entity.getId(), NoticeApproval.ACTION_SUBMIT,
|
if (!reuseDefinition && entity.getId() != null) {
|
||||||
currentAccount(), currentName(), opinion, toJsonTeaIds(new ArrayList<>(nameById.keySet())));
|
approvalStepRepository.deleteByNoticeId(noticeId);
|
||||||
upsertApproverMemory(entity.getSchId(), currentTeaId(), new ArrayList<>(nameById.keySet()));
|
approvalStepRepository.flush();
|
||||||
log.info("[submit] 通知 {} 提交审批, 发布人 {}, 审批教师 {}",
|
}
|
||||||
entity.getId(), currentAccount(), nameById.keySet());
|
for (int i = 0; i < steps.size(); i++) {
|
||||||
|
NoticeApprovalStep step = steps.get(i);
|
||||||
|
step.setNoticeId(noticeId);
|
||||||
|
step.setStepNo(i + 1);
|
||||||
|
step.setStatus(i == 0 ? NoticeApprovalStep.STATUS_ACTIVE : NoticeApprovalStep.STATUS_WAITING);
|
||||||
|
approvalStepRepository.save(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveApprovalRow(noticeId, NoticeApproval.ACTION_SUBMIT,
|
||||||
|
currentAccount(), currentName(), opinion, first.getApproverTeaIds());
|
||||||
|
List<Integer> firstTeaIds = parseTeaIds(first.getApproverTeaIds());
|
||||||
|
if (!firstTeaIds.isEmpty()) {
|
||||||
|
upsertApproverMemory(entity.getSchId(), currentTeaId(), firstTeaIds);
|
||||||
|
}
|
||||||
|
log.info("[submit] 通知 {} 提交审批, 发布人 {}, 共 {} 步, 第 1 步审批人 {}",
|
||||||
|
noticeId, currentAccount(), steps.size(), parseAccounts(first.getApproverAccounts()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void approve(Integer serid, String opinion) {
|
public void approve(Integer serid, String opinion) {
|
||||||
Notice notice = loadNotice(serid);
|
Notice notice = loadNotice(serid);
|
||||||
Integer me = currentTeaId();
|
String meAccount = currentAccount();
|
||||||
requireApprover(notice, me);
|
Integer meTeaId = currentTeaId();
|
||||||
if (notice.getStatus() == null || notice.getStatus() != PENDING_CODE) {
|
if (notice.getStatus() == null || notice.getStatus() != PENDING_CODE) {
|
||||||
throw alreadyHandledError(serid);
|
throw alreadyHandledError(serid);
|
||||||
}
|
}
|
||||||
// 或签 CAS:任一审批人先通过即生效,后到者影响行数为 0 → 冲突提示(改造方案 4.4)
|
List<NoticeApprovalStep> steps = approvalStepRepository.findByNoticeIdOrderByStepNoAsc(serid);
|
||||||
if (noticeRepository.casApprove(serid, LocalDateTime.now()) == 0) {
|
if (steps.isEmpty()) {
|
||||||
|
// 存量单步数据(步骤表改造前提交)按旧或签语义走通
|
||||||
|
approveLegacy(notice, meTeaId, opinion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int level = notice.getApprovalLevel() == null ? 0 : notice.getApprovalLevel();
|
||||||
|
NoticeApprovalStep current = steps.get(Math.min(level, steps.size() - 1));
|
||||||
|
if (current.getStatus() != null && current.getStatus() != NoticeApprovalStep.STATUS_ACTIVE) {
|
||||||
throw alreadyHandledError(serid);
|
throw alreadyHandledError(serid);
|
||||||
}
|
}
|
||||||
|
requireStepApprover(current, meAccount, meTeaId);
|
||||||
|
int round = (int) approvalRepository.countByNoticeIdAndAction(serid, NoticeApproval.ACTION_SUBMIT);
|
||||||
|
boolean acted = approvalRecordRepository.findByNoticeIdAndRoundAndStepNo(serid, round, current.getStepNo()).stream()
|
||||||
|
.anyMatch(r -> meAccount != null && meAccount.equals(r.getApproverAccount()));
|
||||||
|
if (acted) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "您已审批过本步骤,请等待其他审批人处理");
|
||||||
|
}
|
||||||
|
saveRecordRow(serid, round, current, meAccount, meName(meAccount), NoticeApprovalRecord.ACTION_APPROVE, opinion);
|
||||||
|
|
||||||
|
// 会签:本步全部通过才流转;或签:任一通过即流转
|
||||||
|
Set<String> actedAccounts = approvalRecordRepository.findByNoticeIdAndRoundAndStepNo(serid, round, current.getStepNo()).stream()
|
||||||
|
.map(NoticeApprovalRecord::getApproverAccount)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
List<String> stepAccounts = parseAccounts(current.getApproverAccounts());
|
||||||
|
boolean stepDone = "all".equals(current.getVote())
|
||||||
|
? !stepAccounts.isEmpty() && actedAccounts.containsAll(stepAccounts)
|
||||||
|
: true;
|
||||||
|
if (!stepDone) {
|
||||||
|
log.info("[approve] 通知 {} 第 {} 步会签进度 {}/{}", serid, current.getStepNo(),
|
||||||
|
actedAccounts.size(), stepAccounts.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
current.setStatus(NoticeApprovalStep.STATUS_APPROVED);
|
||||||
|
current.setHandleTime(LocalDateTime.now());
|
||||||
|
approvalStepRepository.save(current);
|
||||||
|
|
||||||
|
if (level + 1 < steps.size()) {
|
||||||
|
NoticeApprovalStep next = steps.get(level + 1);
|
||||||
|
next.setStatus(NoticeApprovalStep.STATUS_ACTIVE);
|
||||||
|
approvalStepRepository.save(next);
|
||||||
|
notice.setApprovalLevel(level + 1);
|
||||||
|
notice.setApproverAccounts(next.getApproverAccounts());
|
||||||
|
notice.setApproverTeaIds(next.getApproverTeaIds());
|
||||||
|
noticeRepository.save(notice);
|
||||||
|
log.info("[approve] 通知 {} 第 {} 步通过, 流转至第 {} 步", serid, current.getStepNo(), next.getStepNo());
|
||||||
|
} else {
|
||||||
|
publishAfterFinalApproval(notice, steps.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 存量无步骤数据(旧或签单步)通过:CAS 发布,保留旧冲突提示语义 */
|
||||||
|
private void approveLegacy(Notice notice, Integer meTeaId, String opinion) {
|
||||||
|
requireApprover(notice, meTeaId);
|
||||||
|
if (noticeRepository.casApprove(notice.getId(), LocalDateTime.now()) == 0) {
|
||||||
|
throw alreadyHandledError(notice.getId());
|
||||||
|
}
|
||||||
notice.setStatus(PUBLISHED_CODE);
|
notice.setStatus(PUBLISHED_CODE);
|
||||||
saveApprovalRow(serid, NoticeApproval.ACTION_APPROVE, currentAccount(), currentName(), opinion, null);
|
saveApprovalRow(notice.getId(), NoticeApproval.ACTION_APPROVE, currentAccount(), currentName(), opinion, null);
|
||||||
// 通过即真实发布;定时未到点的交给调度器(timingPublished 保持 0)
|
|
||||||
if (isImmediatePublish(notice)) {
|
if (isImmediatePublish(notice)) {
|
||||||
doRealPublish(notice);
|
doRealPublish(notice);
|
||||||
} else {
|
} else {
|
||||||
noticeRepository.save(notice);
|
noticeRepository.save(notice);
|
||||||
}
|
}
|
||||||
log.info("[approve] 通知 {} 审批通过, 审批教师 {}", serid, me);
|
log.info("[approve] 通知 {} 审批通过(旧单步), 审批教师 {}", notice.getId(), meTeaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 末步通过 → status→1(CAS 防并发重复发布)→ 真实发布(定时未到点交调度器) */
|
||||||
|
private void publishAfterFinalApproval(Notice notice, int stepCount) {
|
||||||
|
if (noticeRepository.casApprove(notice.getId(), LocalDateTime.now()) == 0) {
|
||||||
|
throw alreadyHandledError(notice.getId());
|
||||||
|
}
|
||||||
|
notice.setStatus(PUBLISHED_CODE);
|
||||||
|
notice.setApprovalLevel(stepCount);
|
||||||
|
notice.setApproverAccounts(null);
|
||||||
|
notice.setApproverTeaIds(null);
|
||||||
|
saveApprovalRow(notice.getId(), NoticeApproval.ACTION_APPROVE, currentAccount(), currentName(), null, null);
|
||||||
|
if (isImmediatePublish(notice)) {
|
||||||
|
doRealPublish(notice);
|
||||||
|
} else {
|
||||||
|
noticeRepository.save(notice);
|
||||||
|
}
|
||||||
|
log.info("[approve] 通知 {} 全部步骤通过, 真实发布", notice.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -508,21 +631,53 @@ public class NoticeService implements INoticeService {
|
|||||||
throw new BizException(ErrorCode.PARAM_ERROR, "请填写驳回意见");
|
throw new BizException(ErrorCode.PARAM_ERROR, "请填写驳回意见");
|
||||||
}
|
}
|
||||||
Notice notice = loadNotice(serid);
|
Notice notice = loadNotice(serid);
|
||||||
Integer me = currentTeaId();
|
String meAccount = currentAccount();
|
||||||
requireApprover(notice, me);
|
Integer meTeaId = currentTeaId();
|
||||||
if (notice.getStatus() == null || notice.getStatus() != PENDING_CODE) {
|
if (notice.getStatus() == null || notice.getStatus() != PENDING_CODE) {
|
||||||
throw alreadyHandledError(serid);
|
throw alreadyHandledError(serid);
|
||||||
}
|
}
|
||||||
|
List<NoticeApprovalStep> steps = approvalStepRepository.findByNoticeIdOrderByStepNoAsc(serid);
|
||||||
|
if (steps.isEmpty()) {
|
||||||
|
// 存量单步数据按旧语义驳回
|
||||||
|
requireApprover(notice, meTeaId);
|
||||||
if (noticeRepository.casReject(serid, LocalDateTime.now()) == 0) {
|
if (noticeRepository.casReject(serid, LocalDateTime.now()) == 0) {
|
||||||
throw alreadyHandledError(serid);
|
throw alreadyHandledError(serid);
|
||||||
}
|
}
|
||||||
notice.setStatus(REJECTED_CODE);
|
notice.setStatus(REJECTED_CODE);
|
||||||
noticeRepository.save(notice);
|
noticeRepository.save(notice);
|
||||||
saveApprovalRow(serid, NoticeApproval.ACTION_REJECT, currentAccount(), currentName(), opinion, null);
|
saveApprovalRow(serid, NoticeApproval.ACTION_REJECT, meAccount, currentName(), opinion, null);
|
||||||
log.info("[reject] 通知 {} 审批驳回, 审批教师 {}, 意见: {}", serid, me, opinion);
|
log.info("[reject] 通知 {} 审批驳回(旧单步), 意见: {}", serid, opinion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int level = notice.getApprovalLevel() == null ? 0 : notice.getApprovalLevel();
|
||||||
|
NoticeApprovalStep current = steps.get(Math.min(level, steps.size() - 1));
|
||||||
|
if (current.getStatus() != null && current.getStatus() != NoticeApprovalStep.STATUS_ACTIVE) {
|
||||||
|
throw alreadyHandledError(serid);
|
||||||
|
}
|
||||||
|
requireStepApprover(current, meAccount, meTeaId);
|
||||||
|
int round = (int) approvalRepository.countByNoticeIdAndAction(serid, NoticeApproval.ACTION_SUBMIT);
|
||||||
|
if (noticeRepository.casReject(serid, LocalDateTime.now()) == 0) {
|
||||||
|
throw alreadyHandledError(serid);
|
||||||
|
}
|
||||||
|
saveRecordRow(serid, round, current, meAccount, meName(meAccount), NoticeApprovalRecord.ACTION_REJECT, opinion);
|
||||||
|
current.setStatus(NoticeApprovalStep.STATUS_REJECTED);
|
||||||
|
current.setHandleTime(LocalDateTime.now());
|
||||||
|
approvalStepRepository.save(current);
|
||||||
|
notice.setStatus(REJECTED_CODE);
|
||||||
|
noticeRepository.save(notice);
|
||||||
|
log.info("[reject] 通知 {} 第 {} 步驳回, 审批人 {}, 意见: {}", serid, current.getStepNo(), meAccount, opinion);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 仅该轮审批教师快照含当前教师ID可处理 */
|
/** 当前步审批人校验:登录账号命中账号快照,或教师ID命中旧 teaId 快照 */
|
||||||
|
private void requireStepApprover(NoticeApprovalStep step, String meAccount, Integer meTeaId) {
|
||||||
|
boolean hit = (meAccount != null && parseAccounts(step.getApproverAccounts()).contains(meAccount))
|
||||||
|
|| (meTeaId != null && parseTeaIds(step.getApproverTeaIds()).contains(meTeaId));
|
||||||
|
if (!hit) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "您不是本步骤的审批人");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅存量单步快照含当前教师ID可处理(旧链路兜底) */
|
||||||
private void requireApprover(Notice notice, Integer teaId) {
|
private void requireApprover(Notice notice, Integer teaId) {
|
||||||
if (teaId == null) {
|
if (teaId == null) {
|
||||||
throw new BizException(ErrorCode.FORBIDDEN, "未登录或会话已过期");
|
throw new BizException(ErrorCode.FORBIDDEN, "未登录或会话已过期");
|
||||||
@@ -547,6 +702,227 @@ public class NoticeService implements INoticeService {
|
|||||||
return new BizException(ErrorCode.FORBIDDEN, "该通知已离开待审批状态,无需重复处理");
|
return new BizException(ErrorCode.FORBIDDEN, "该通知已离开待审批状态,无需重复处理");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归一化请求里的审批流:steps 优先;缺省时旧 approverTeaIds 包装为单步或签
|
||||||
|
* (教师绑定兼容链路)。每步账号(approvers)与教师ID(approverTeaIds)二选一,
|
||||||
|
* 两者皆空的步丢弃;整体为空返回空列表。
|
||||||
|
*/
|
||||||
|
private List<FlowStepItem> resolveRequestFlow(List<FlowStepItem> steps, List<Integer> legacyTeaIds) {
|
||||||
|
List<FlowStepItem> result = new ArrayList<>();
|
||||||
|
if (steps != null) {
|
||||||
|
for (FlowStepItem step : steps) {
|
||||||
|
if (step == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> accounts = new ArrayList<>();
|
||||||
|
if (step.getApprovers() != null) {
|
||||||
|
for (String account : step.getApprovers()) {
|
||||||
|
if (StringUtils.hasText(account) && !accounts.contains(account.trim())) {
|
||||||
|
accounts.add(account.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Integer> teaIds = step.getApproverTeaIds() == null ? new ArrayList<>()
|
||||||
|
: step.getApproverTeaIds().stream().filter(Objects::nonNull).collect(Collectors.toList());
|
||||||
|
if (accounts.isEmpty() && teaIds.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FlowStepItem item = new FlowStepItem();
|
||||||
|
item.setName(StringUtils.hasText(step.getName()) ? step.getName().trim() : null);
|
||||||
|
item.setVote("all".equalsIgnoreCase(step.getVote()) ? "all" : "any");
|
||||||
|
item.setApprovers(accounts);
|
||||||
|
item.setApproverTeaIds(teaIds);
|
||||||
|
result.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.isEmpty() && legacyTeaIds != null && !legacyTeaIds.isEmpty()) {
|
||||||
|
FlowStepItem legacy = new FlowStepItem();
|
||||||
|
legacy.setVote("any");
|
||||||
|
result.add(legacy);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发布入参的审批流解析:flow 多步优先 → approvers+vote 单步简写 → 旧 approverTeaIds 兼容包装 */
|
||||||
|
private List<FlowStepItem> resolveNoticeFlow(NoticeRequest request) {
|
||||||
|
if (request.getFlow() != null && !request.getFlow().isEmpty()) {
|
||||||
|
return resolveRequestFlow(request.getFlow(), null);
|
||||||
|
}
|
||||||
|
if (request.getApprovers() != null && !request.getApprovers().isEmpty()) {
|
||||||
|
FlowStepItem single = new FlowStepItem();
|
||||||
|
single.setVote(request.getVote());
|
||||||
|
single.setApprovers(request.getApprovers());
|
||||||
|
return resolveRequestFlow(Collections.singletonList(single), null);
|
||||||
|
}
|
||||||
|
return resolveRequestFlow(null, request.getApproverTeaIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存审批流定义(草稿/编辑态):全步骤置「待流转」,提交审批时激活第 1 步。
|
||||||
|
* 全量替换语义:flow 非空重建;flow 空且 legacyTeaIds 非 null 时仅清空旧定义;
|
||||||
|
* 两者皆无(未携带流程字段)时不改动。
|
||||||
|
*/
|
||||||
|
private void saveFlowDefinition(Notice entity, List<FlowStepItem> flow, List<Integer> legacyTeaIds) {
|
||||||
|
boolean flowCarried = flow != null && !flow.isEmpty();
|
||||||
|
if (!flowCarried && legacyTeaIds == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
approvalStepRepository.deleteByNoticeId(entity.getId());
|
||||||
|
approvalStepRepository.flush();
|
||||||
|
if (!flowCarried) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<NoticeApprovalStep> steps = buildSteps(entity, flow, legacyTeaIds);
|
||||||
|
for (int i = 0; i < steps.size(); i++) {
|
||||||
|
NoticeApprovalStep step = steps.get(i);
|
||||||
|
step.setNoticeId(entity.getId());
|
||||||
|
step.setStepNo(i + 1);
|
||||||
|
step.setStatus(NoticeApprovalStep.STATUS_WAITING);
|
||||||
|
approvalStepRepository.save(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建并绑定审批步骤:账号路径按本校启用审批人池校验(姓名随池快照);
|
||||||
|
* 旧 teaId 路径按学校教师名单校验。每步至少一人,整体至少一步。
|
||||||
|
* 草稿已存定义(全待流转步)且本次未重选人时直接复用存量定义。
|
||||||
|
*/
|
||||||
|
private List<NoticeApprovalStep> buildSteps(Notice entity, List<FlowStepItem> flowItems, List<Integer> legacyTeaIds) {
|
||||||
|
if ((flowItems == null || flowItems.isEmpty()) && entity.getId() != null) {
|
||||||
|
List<NoticeApprovalStep> existing = approvalStepRepository.findByNoticeIdOrderByStepNoAsc(entity.getId());
|
||||||
|
// 仅「全待流转」视为草稿流程定义可复用;已驳回单的步骤含审批中状态,必须重建
|
||||||
|
boolean hasDefinition = !existing.isEmpty() && existing.stream()
|
||||||
|
.allMatch(s -> s.getStatus() != null && s.getStatus() == NoticeApprovalStep.STATUS_WAITING);
|
||||||
|
if (hasDefinition) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flowItems == null || flowItems.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<NoticeApprovalStep> steps = new ArrayList<>();
|
||||||
|
for (int i = 0; i < flowItems.size(); i++) {
|
||||||
|
FlowStepItem item = flowItems.get(i);
|
||||||
|
NoticeApprovalStep step = new NoticeApprovalStep();
|
||||||
|
step.setStepName(StringUtils.hasText(item.getName()) ? item.getName()
|
||||||
|
: "第" + (i + 1) + "步");
|
||||||
|
step.setVote("all".equals(item.getVote()) ? "all" : "any");
|
||||||
|
if (item.getApprovers() != null && !item.getApprovers().isEmpty()) {
|
||||||
|
// 账号路径:按本校启用审批人池绑定
|
||||||
|
LinkedHashMap<String, String> nameByAccount = bindAccounts(entity.getSchId(), item.getApprovers());
|
||||||
|
List<String> accounts = new ArrayList<>(nameByAccount.keySet());
|
||||||
|
step.setApproverAccounts(toJsonStringList(accounts));
|
||||||
|
step.setApproverNames(toJsonStringList(new ArrayList<>(nameByAccount.values())));
|
||||||
|
List<Integer> teaIds = new ArrayList<>();
|
||||||
|
for (String account : accounts) {
|
||||||
|
sysApproverRepository.findBySchIdAndAccount(entity.getSchId(), account)
|
||||||
|
.filter(a -> a.getTeaId() != null)
|
||||||
|
.ifPresent(a -> teaIds.add(a.getTeaId()));
|
||||||
|
}
|
||||||
|
if (!teaIds.isEmpty()) {
|
||||||
|
step.setApproverTeaIds(toJsonTeaIds(teaIds));
|
||||||
|
}
|
||||||
|
} else if (item.getApproverTeaIds() != null && !item.getApproverTeaIds().isEmpty()) {
|
||||||
|
// 教师ID路径(api-test 测试台教师树选人):按学校教师名单绑定
|
||||||
|
LinkedHashMap<Integer, String> nameById = bindApprovers(entity.getSchId(), item.getApproverTeaIds());
|
||||||
|
step.setApproverTeaIds(toJsonTeaIds(new ArrayList<>(nameById.keySet())));
|
||||||
|
step.setApproverNames(toJsonStringList(new ArrayList<>(nameById.values())));
|
||||||
|
} else {
|
||||||
|
// 请求级旧 approverTeaIds 兼容包装步(单步或签)
|
||||||
|
LinkedHashMap<Integer, String> nameById = bindApprovers(entity.getSchId(), legacyTeaIds);
|
||||||
|
step.setApproverTeaIds(toJsonTeaIds(new ArrayList<>(nameById.keySet())));
|
||||||
|
step.setApproverNames(toJsonStringList(new ArrayList<>(nameById.values())));
|
||||||
|
}
|
||||||
|
steps.add(step);
|
||||||
|
}
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按本校启用审批人池绑定账号:账号必须存在于池(多租户隔离)且启用,
|
||||||
|
* 保持选择顺序去重;姓名取池快照(池未维护姓名时回退账号展示)。
|
||||||
|
*/
|
||||||
|
private LinkedHashMap<String, String> bindAccounts(Integer schId, List<String> accounts) {
|
||||||
|
if (accounts == null || accounts.isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "请选择审批人");
|
||||||
|
}
|
||||||
|
if (schId == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "获取学校信息失败");
|
||||||
|
}
|
||||||
|
Map<String, SysApprover> poolByAccount = new LinkedHashMap<>();
|
||||||
|
for (SysApprover approver : sysApproverRepository.findBySchIdAndEnabledOrderByNameAsc(schId, 1)) {
|
||||||
|
poolByAccount.putIfAbsent(approver.getAccount(), approver);
|
||||||
|
}
|
||||||
|
LinkedHashMap<String, String> result = new LinkedHashMap<>();
|
||||||
|
List<String> missing = new ArrayList<>();
|
||||||
|
for (String account : accounts) {
|
||||||
|
if (result.containsKey(account)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SysApprover pool = poolByAccount.get(account);
|
||||||
|
if (pool == null) {
|
||||||
|
missing.add(account);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.put(account, StringUtils.hasText(pool.getName()) ? pool.getName() : account);
|
||||||
|
}
|
||||||
|
if (!missing.isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||||
|
"审批人不在本校审批人名单或已停用(" + String.join("、", missing) + "),请在「审核设置」中维护");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前审批人姓名:上下文 displayName 优先,缺省回退登录账号 */
|
||||||
|
private String meName(String meAccount) {
|
||||||
|
String name = currentName();
|
||||||
|
return StringUtils.hasText(name) ? name : meAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核记录落行(步骤名/表决方式随行快照,历史轮次永久保留) */
|
||||||
|
private void saveRecordRow(Integer noticeId, int round, NoticeApprovalStep step,
|
||||||
|
String account, String name, int action, String opinion) {
|
||||||
|
NoticeApprovalRecord row = new NoticeApprovalRecord();
|
||||||
|
row.setNoticeId(noticeId);
|
||||||
|
row.setRound(round);
|
||||||
|
row.setStepNo(step.getStepNo());
|
||||||
|
row.setStepName(step.getStepName());
|
||||||
|
row.setVote(step.getVote());
|
||||||
|
row.setApproverAccount(account);
|
||||||
|
row.setApproverName(name);
|
||||||
|
row.setAction(action);
|
||||||
|
row.setOpinion(opinion);
|
||||||
|
row.setActionTime(LocalDateTime.now());
|
||||||
|
approvalRecordRepository.save(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 账号 JSON 解析(["a","b"]),空/非法返回空列表 */
|
||||||
|
private List<String> parseAccounts(String json) {
|
||||||
|
if (!StringUtils.hasText(json)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String[] arr = objectMapper.readValue(json, String[].class);
|
||||||
|
return arr == null ? Collections.emptyList() : new ArrayList<>(Arrays.asList(arr));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[parseAccounts] 审批人账号快照解析失败: {}", json, e);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字符串列表转 JSON 数组串 */
|
||||||
|
private String toJsonStringList(List<String> values) {
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(values);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[toJsonStringList] 序列化失败: {}", values, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按学校教师列表绑定被选审批教师:bschclass 教师名单按 UserTeaSerID 匹配(保持选择顺序、去重);
|
* 按学校教师列表绑定被选审批教师:bschclass 教师名单按 UserTeaSerID 匹配(保持选择顺序、去重);
|
||||||
* 未选人、或存在不属于本校的ID时报错。配置了资格组(yh.notice.approver-group-id>0)时
|
* 未选人、或存在不属于本校的ID时报错。配置了资格组(yh.notice.approver-group-id>0)时
|
||||||
@@ -617,108 +993,275 @@ public class NoticeService implements INoticeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BasePageResult<ApprovalListVo> approvalList(String auditState, NoticeQuery query, Pageable pageable) {
|
public BasePageResult<NoticeVo> approvalList(String auditState, NoticeQuery query, Pageable pageable) {
|
||||||
Integer me = currentTeaId();
|
String state = StringUtils.hasText(auditState) ? auditState.trim().toLowerCase() : "pending";
|
||||||
if (me == null) {
|
String meAccount = currentAccount();
|
||||||
return PageUtils.toResult(new PageImpl<>(Collections.emptyList(), pageable, 0));
|
Integer meTeaId = currentTeaId();
|
||||||
}
|
|
||||||
boolean reviewed = "reviewed".equalsIgnoreCase(auditState);
|
|
||||||
List<Integer> statuses = reviewed
|
|
||||||
? Arrays.asList(PUBLISHED_CODE, REJECTED_CODE)
|
|
||||||
: Collections.singletonList(PENDING_CODE);
|
|
||||||
// 审批教师快照含我:JSON 整数数组按 [n / [n, / ,n, / ,n] 四段边界匹配(101 不会命中 1101,改造方案 4.4)
|
|
||||||
String idStr = String.valueOf(me);
|
|
||||||
Specification<Notice> spec = (root, cq, cb) -> {
|
Specification<Notice> spec = (root, cq, cb) -> {
|
||||||
List<Predicate> predicates = new ArrayList<>();
|
List<Predicate> predicates = new ArrayList<>();
|
||||||
predicates.add(cb.or(
|
switch (state) {
|
||||||
cb.like(root.get("approverTeaIds"), "[" + idStr + "]"),
|
case "approved":
|
||||||
cb.like(root.get("approverTeaIds"), "[" + idStr + ",%"),
|
// 走审批流发布的(直接发布的 approvalLevel 为空,不算审批通过)
|
||||||
cb.like(root.get("approverTeaIds"), "%," + idStr + ",%"),
|
predicates.add(cb.equal(root.get("status"), PUBLISHED_CODE));
|
||||||
cb.like(root.get("approverTeaIds"), "%," + idStr + "]")));
|
predicates.add(root.get("approvalLevel").isNotNull());
|
||||||
predicates.add(root.get("status").in(statuses));
|
break;
|
||||||
|
case "rejected":
|
||||||
|
predicates.add(cb.equal(root.get("status"), REJECTED_CODE));
|
||||||
|
break;
|
||||||
|
case "all":
|
||||||
|
case "reviewed":
|
||||||
|
predicates.add(root.get("status").in(Arrays.asList(PUBLISHED_CODE, REJECTED_CODE)));
|
||||||
|
predicates.add(cb.or(root.get("approvalLevel").isNotNull(),
|
||||||
|
cb.equal(root.get("status"), REJECTED_CODE)));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
predicates.add(cb.equal(root.get("status"), PENDING_CODE));
|
||||||
|
predicates.add(approverMatchPredicate(root, cb, meAccount, meTeaId));
|
||||||
|
break;
|
||||||
|
}
|
||||||
appendCommonPredicates(query, root, cb, predicates);
|
appendCommonPredicates(query, root, cb, predicates);
|
||||||
appendSchIdPredicate(root, cb, predicates);
|
appendSchIdPredicate(root, cb, predicates);
|
||||||
return cq.where(predicates.toArray(new Predicate[0])).getRestriction();
|
return cq.where(predicates.toArray(new Predicate[0])).getRestriction();
|
||||||
};
|
};
|
||||||
Page<Notice> page = noticeRepository.findAll(spec, pageable);
|
Page<Notice> page = noticeRepository.findAll(spec, pageable);
|
||||||
List<Notice> notices = page.getContent();
|
List<NoticeVo> content = page.getContent().stream()
|
||||||
Map<Integer, List<NoticeApproval>> recordsByNotice = notices.isEmpty()
|
.map(n -> toVo(n, false))
|
||||||
? Collections.emptyMap()
|
|
||||||
: approvalRepository.findByNoticeIds(
|
|
||||||
notices.stream().map(Notice::getId).collect(Collectors.toList()))
|
|
||||||
.stream().collect(Collectors.groupingBy(NoticeApproval::getNoticeId));
|
|
||||||
// 审批教师姓名绑定:整页一次拉取学校教师,按ID映射(查询失败降级为ID展示)
|
|
||||||
Map<Integer, String> teaNameById = loadSchTeaNames(currentSchId());
|
|
||||||
String meAccount = currentAccount();
|
|
||||||
List<ApprovalListVo> content = notices.stream()
|
|
||||||
.map(n -> toApprovalVo(n, recordsByNotice.getOrDefault(n.getId(), Collections.emptyList()), me, teaNameById, meAccount))
|
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
fillApprovalFields(content);
|
||||||
return PageUtils.toResult(new PageImpl<>(content, pageable, page.getTotalElements()));
|
return PageUtils.toResult(new PageImpl<>(content, pageable, page.getTotalElements()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 学校教师ID→姓名映射(审批列表姓名回显用);接口失败返回空映射降级为ID展示 */
|
@Override
|
||||||
private Map<Integer, String> loadSchTeaNames(Integer schId) {
|
public int approvalCount() {
|
||||||
if (schId == null) {
|
String meAccount = currentAccount();
|
||||||
return Collections.emptyMap();
|
Integer meTeaId = currentTeaId();
|
||||||
|
if (!StringUtils.hasText(meAccount) && meTeaId == null) {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
try {
|
Specification<Notice> spec = (root, cq, cb) -> {
|
||||||
Map<Integer, String> names = new LinkedHashMap<>();
|
List<Predicate> predicates = new ArrayList<>();
|
||||||
for (SchTeaList tea : classService.getSchTeaList(schId)) {
|
predicates.add(cb.equal(root.get("status"), PENDING_CODE));
|
||||||
if (tea.getUserTeaSerID() != null) {
|
predicates.add(approverMatchPredicate(root, cb, meAccount, meTeaId));
|
||||||
names.putIfAbsent(tea.getUserTeaSerID(),
|
appendSchIdPredicate(root, cb, predicates);
|
||||||
StringUtils.hasText(tea.getTeaName()) ? tea.getTeaName() : String.valueOf(tea.getUserTeaSerID()));
|
return cq.where(predicates.toArray(new Predicate[0])).getRestriction();
|
||||||
|
};
|
||||||
|
return (int) noticeRepository.count(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 「待我审批」匹配:当前步账号快照 / 旧教师ID快照任一命中(JSON 数组四段边界匹配,101 不会命中 1101) */
|
||||||
|
private Predicate approverMatchPredicate(Root<Notice> root, CriteriaBuilder cb, String meAccount, Integer meTeaId) {
|
||||||
|
List<Predicate> or = new ArrayList<>();
|
||||||
|
if (StringUtils.hasText(meAccount)) {
|
||||||
|
String acc = meAccount.replace("%", "").replace("_", "");
|
||||||
|
if (StringUtils.hasText(acc)) {
|
||||||
|
or.add(cb.like(root.get("approverAccounts"), "[\"" + acc + "\"]"));
|
||||||
|
or.add(cb.like(root.get("approverAccounts"), "[\"" + acc + "\",%"));
|
||||||
|
or.add(cb.like(root.get("approverAccounts"), "%,\"" + acc + "\",%"));
|
||||||
|
or.add(cb.like(root.get("approverAccounts"), "%,\"" + acc + "\"]"));
|
||||||
|
or.add(cb.like(root.get("approverAccounts"), "[" + acc + "]"));
|
||||||
|
or.add(cb.like(root.get("approverAccounts"), "[" + acc + ",%"));
|
||||||
|
or.add(cb.like(root.get("approverAccounts"), "%," + acc + ",%"));
|
||||||
|
or.add(cb.like(root.get("approverAccounts"), "%," + acc + "]"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return names;
|
if (meTeaId != null) {
|
||||||
} catch (Exception e) {
|
String idStr = String.valueOf(meTeaId);
|
||||||
log.warn("[loadSchTeaNames] 拉取学校教师失败, schId={}", schId, e);
|
or.add(cb.like(root.get("approverTeaIds"), "[" + idStr + "]"));
|
||||||
return Collections.emptyMap();
|
or.add(cb.like(root.get("approverTeaIds"), "[" + idStr + ",%"));
|
||||||
|
or.add(cb.like(root.get("approverTeaIds"), "%," + idStr + ",%"));
|
||||||
|
or.add(cb.like(root.get("approverTeaIds"), "%," + idStr + "]"));
|
||||||
|
}
|
||||||
|
return cb.or(or.toArray(new Predicate[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表行批量装配多步审批字段:审批状态(none/pending/approved/rejected)、
|
||||||
|
* 进度文本(第x/y步 名·会签 done/total)、当前步下标、当前用户可否审批。
|
||||||
|
*/
|
||||||
|
private void fillApprovalFields(List<NoticeVo> vos) {
|
||||||
|
if (vos == null || vos.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Integer> ids = vos.stream().map(NoticeVo::getId).collect(Collectors.toList());
|
||||||
|
Map<Integer, List<NoticeApprovalStep>> stepsByNotice = approvalStepRepository.findByNoticeIds(ids).stream()
|
||||||
|
.collect(Collectors.groupingBy(NoticeApprovalStep::getNoticeId));
|
||||||
|
Map<Integer, List<NoticeApprovalRecord>> recordsByNotice = approvalRecordRepository.findByNoticeIds(ids).stream()
|
||||||
|
.collect(Collectors.groupingBy(NoticeApprovalRecord::getNoticeId));
|
||||||
|
String meAccount = currentAccount();
|
||||||
|
Integer meTeaId = currentTeaId();
|
||||||
|
for (NoticeVo vo : vos) {
|
||||||
|
List<NoticeApprovalStep> steps = stepsByNotice.getOrDefault(vo.getId(), Collections.emptyList());
|
||||||
|
if (steps.isEmpty()) {
|
||||||
|
boolean pending = vo.getStatus() != null && vo.getStatus() == PENDING_CODE;
|
||||||
|
vo.setApprovalStatus(pending ? "pending" : "none");
|
||||||
|
vo.setApprovalStatusDesc(vo.getStatusDesc());
|
||||||
|
vo.setApprovalLevel(0);
|
||||||
|
vo.setCanApprove(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<NoticeApprovalRecord> records = recordsByNotice.getOrDefault(vo.getId(), Collections.emptyList());
|
||||||
|
vo.setApprovalStatus(approvalStatusOf(vo.getStatus(), steps));
|
||||||
|
vo.setApprovalLevel(stepLevelOf(vo.getStatus(), steps));
|
||||||
|
vo.setApprovalStatusDesc(progressDesc(vo.getStatus(), steps, records));
|
||||||
|
vo.setCanApprove(canApproveNow(vo.getStatus(), steps, records, meAccount, meTeaId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 审批列表行装配:审核人姓名按提交行 assignee_tea_ids 绑定学校教师;已审行带最新处理信息(含他人已处理) */
|
/** 前端审批状态枚举映射:none 免审 / pending / approved(走流程发布)/ rejected */
|
||||||
private ApprovalListVo toApprovalVo(Notice notice, List<NoticeApproval> records,
|
private String approvalStatusOf(Integer status, List<NoticeApprovalStep> steps) {
|
||||||
Integer meTeaId, Map<Integer, String> teaNameById, String meAccount) {
|
if (status == null) {
|
||||||
ApprovalListVo vo = new ApprovalListVo();
|
return "none";
|
||||||
vo.setId(notice.getId());
|
}
|
||||||
vo.setTitle(notice.getTitle());
|
if (status == PENDING_CODE) {
|
||||||
vo.setType(notice.getType());
|
return "pending";
|
||||||
vo.setTypeDesc(NoticeTypeEnum.of(notice.getType()).getDesc());
|
}
|
||||||
vo.setStatus(notice.getStatus());
|
if (status == REJECTED_CODE) {
|
||||||
vo.setStatusDesc(NoticeStatusEnum.of(notice.getStatus()).getDesc());
|
return "rejected";
|
||||||
vo.setPublisherAccount(notice.getPublisherAccount());
|
}
|
||||||
vo.setPublisherName(notice.getPublisherName());
|
if (status == PUBLISHED_CODE) {
|
||||||
vo.setReceiptFlag(notice.getReceiptFlag());
|
return steps.isEmpty() ? "none" : "approved";
|
||||||
vo.setTimingSendTime(notice.getTimingSendTime());
|
}
|
||||||
// records 按 id asc,顺序覆盖即取最新
|
return "none";
|
||||||
NoticeApproval submit = null;
|
}
|
||||||
NoticeApproval handled = null;
|
|
||||||
for (NoticeApproval record : records) {
|
/** 当前步下标(0 起):待审批=首个审批中步骤;已驳回=驳回发生步骤;其余=步数(全部走完) */
|
||||||
if (record.getAction() == NoticeApproval.ACTION_SUBMIT) {
|
private int stepLevelOf(Integer status, List<NoticeApprovalStep> steps) {
|
||||||
submit = record;
|
if (status != null && status == PENDING_CODE) {
|
||||||
} else if (record.getAction() == NoticeApproval.ACTION_APPROVE
|
for (int i = 0; i < steps.size(); i++) {
|
||||||
|| record.getAction() == NoticeApproval.ACTION_REJECT) {
|
Integer st = steps.get(i).getStatus();
|
||||||
handled = record;
|
if (st != null && st == NoticeApprovalStep.STATUS_ACTIVE) {
|
||||||
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (submit != null) {
|
return 0;
|
||||||
vo.setSubmitTime(submit.getActionTime());
|
}
|
||||||
for (Integer teaId : parseTeaIds(submit.getAssigneeTeaIds())) {
|
if (status != null && status == REJECTED_CODE) {
|
||||||
vo.getApproverNames().add(teaNameById.getOrDefault(teaId, "ID " + teaId));
|
for (int i = 0; i < steps.size(); i++) {
|
||||||
|
Integer st = steps.get(i).getStatus();
|
||||||
|
if (st != null && st == NoticeApprovalStep.STATUS_REJECTED) {
|
||||||
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (handled != null && notice.getStatus() != null && notice.getStatus() != PENDING_CODE) {
|
|
||||||
vo.setHandled(true);
|
|
||||||
vo.setHandledByMe(meAccount.equals(handled.getOperatorAccount()));
|
|
||||||
vo.setHandledAccount(handled.getOperatorAccount());
|
|
||||||
vo.setHandledName(handled.getOperatorName());
|
|
||||||
vo.setHandledAction(handled.getAction());
|
|
||||||
ApprovalActionEnum action = ApprovalActionEnum.of(handled.getAction());
|
|
||||||
vo.setHandledActionDesc(action != null ? action.getDesc() : null);
|
|
||||||
vo.setHandledTime(handled.getActionTime());
|
|
||||||
vo.setHandledOpinion(handled.getOpinion());
|
|
||||||
}
|
}
|
||||||
return vo;
|
return steps.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 进度文本(对齐前端展示):待审批「第1/2步 校级审核·会签 0/2」,或签不带计数 */
|
||||||
|
private String progressDesc(Integer status, List<NoticeApprovalStep> steps, List<NoticeApprovalRecord> records) {
|
||||||
|
if (status == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (status == REJECTED_CODE) {
|
||||||
|
return "已驳回";
|
||||||
|
}
|
||||||
|
if (status != PENDING_CODE) {
|
||||||
|
return "已发布";
|
||||||
|
}
|
||||||
|
int level = stepLevelOf(status, steps);
|
||||||
|
NoticeApprovalStep current = steps.get(level);
|
||||||
|
String name = StringUtils.hasText(current.getStepName()) ? current.getStepName() : "第" + (level + 1) + "步";
|
||||||
|
String prefix = "第" + (level + 1) + "/" + steps.size() + "步 " + name;
|
||||||
|
if ("all".equals(current.getVote())) {
|
||||||
|
int round = currentRound(records);
|
||||||
|
long done = records.stream()
|
||||||
|
.filter(r -> r.getRound() != null && r.getRound() == round
|
||||||
|
&& current.getStepNo() != null && current.getStepNo().equals(r.getStepNo())
|
||||||
|
&& r.getAction() != null && r.getAction() == NoticeApprovalRecord.ACTION_APPROVE)
|
||||||
|
.count();
|
||||||
|
return prefix + "·会签 " + done + "/" + parseAccounts(current.getApproverAccounts()).size();
|
||||||
|
}
|
||||||
|
return prefix + "·或签";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前用户是否本单当前步待签审批人(在步内且本轮本步未处理过) */
|
||||||
|
private boolean canApproveNow(Integer status, List<NoticeApprovalStep> steps,
|
||||||
|
List<NoticeApprovalRecord> records, String meAccount, Integer meTeaId) {
|
||||||
|
if (status == null || status != PENDING_CODE || steps.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int level = stepLevelOf(status, steps);
|
||||||
|
NoticeApprovalStep current = steps.get(level);
|
||||||
|
boolean inStep = (meAccount != null && parseAccounts(current.getApproverAccounts()).contains(meAccount))
|
||||||
|
|| (meTeaId != null && parseTeaIds(current.getApproverTeaIds()).contains(meTeaId));
|
||||||
|
if (!inStep) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int round = currentRound(records);
|
||||||
|
return records.stream().noneMatch(r -> r.getRound() != null && r.getRound() == round
|
||||||
|
&& current.getStepNo() != null && current.getStepNo().equals(r.getStepNo())
|
||||||
|
&& meAccount != null && meAccount.equals(r.getApproverAccount()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 最新送审轮次(无记录按 1) */
|
||||||
|
private int currentRound(List<NoticeApprovalRecord> records) {
|
||||||
|
return records.stream().mapToInt(r -> r.getRound() == null ? 1 : r.getRound()).max().orElse(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详情多步审批流装配:每步含审批人账号/姓名、步骤状态(approved/pending/rejected/waiting)、
|
||||||
|
* 全部轮次审核记录、当前步待签人姓名;同时用于发布/编辑表单回显审批人配置。
|
||||||
|
*/
|
||||||
|
private List<ApprovalFlowVo> toFlowVos(Integer status, List<NoticeApprovalStep> steps,
|
||||||
|
List<NoticeApprovalRecord> records) {
|
||||||
|
if (steps.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
int level = stepLevelOf(status, steps);
|
||||||
|
List<ApprovalFlowVo> flow = new ArrayList<>();
|
||||||
|
for (int i = 0; i < steps.size(); i++) {
|
||||||
|
NoticeApprovalStep step = steps.get(i);
|
||||||
|
ApprovalFlowVo vo = new ApprovalFlowVo();
|
||||||
|
vo.setName(StringUtils.hasText(step.getStepName()) ? step.getStepName() : "第" + (i + 1) + "步");
|
||||||
|
vo.setVote(StringUtils.hasText(step.getVote()) ? step.getVote() : "any");
|
||||||
|
List<String> accounts = parseAccounts(step.getApproverAccounts());
|
||||||
|
List<String> names = parseAccounts(step.getApproverNames());
|
||||||
|
vo.setApprovers(accounts);
|
||||||
|
vo.setApproverTeaIds(parseTeaIds(step.getApproverTeaIds()));
|
||||||
|
// 账号路径姓名与账号对齐展示;教师ID路径仅有姓名快照
|
||||||
|
if (!accounts.isEmpty()) {
|
||||||
|
vo.setApproverNames(names.size() == accounts.size() ? names : accounts);
|
||||||
|
} else {
|
||||||
|
vo.setApproverNames(names);
|
||||||
|
}
|
||||||
|
List<ApprovalFlowVo.Record> stepRecords = new ArrayList<>();
|
||||||
|
for (NoticeApprovalRecord r : records) {
|
||||||
|
if (step.getStepNo() == null || !step.getStepNo().equals(r.getStepNo())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ApprovalFlowVo.Record item = new ApprovalFlowVo.Record();
|
||||||
|
item.setAccount(r.getApproverAccount());
|
||||||
|
item.setName(r.getApproverName());
|
||||||
|
item.setResult(r.getAction() != null && r.getAction() == NoticeApprovalRecord.ACTION_REJECT
|
||||||
|
? "rejected" : "approved");
|
||||||
|
item.setTime(r.getActionTime());
|
||||||
|
item.setRemark(r.getOpinion());
|
||||||
|
stepRecords.add(item);
|
||||||
|
}
|
||||||
|
vo.setRecords(stepRecords);
|
||||||
|
Integer st = step.getStatus();
|
||||||
|
if (st != null && st == NoticeApprovalStep.STATUS_APPROVED) {
|
||||||
|
vo.setStatus("approved");
|
||||||
|
} else if (st != null && st == NoticeApprovalStep.STATUS_REJECTED) {
|
||||||
|
vo.setStatus("rejected");
|
||||||
|
} else if (status != null && status == PENDING_CODE && i == level) {
|
||||||
|
vo.setStatus("pending");
|
||||||
|
} else {
|
||||||
|
vo.setStatus("waiting");
|
||||||
|
}
|
||||||
|
if ("pending".equals(vo.getStatus())) {
|
||||||
|
Set<String> acted = stepRecords.stream()
|
||||||
|
.map(ApprovalFlowVo.Record::getAccount)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
for (int j = 0; j < accounts.size(); j++) {
|
||||||
|
if (!acted.contains(accounts.get(j))) {
|
||||||
|
vo.getPending().add(j < vo.getApproverNames().size() ? vo.getApproverNames().get(j) : accounts.get(j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flow.add(vo);
|
||||||
|
}
|
||||||
|
return flow;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -923,6 +1466,11 @@ public class NoticeService implements INoticeService {
|
|||||||
entity.setApproverTeaIds(toJsonTeaIds(new ArrayList<>(nameById.keySet())));
|
entity.setApproverTeaIds(toJsonTeaIds(new ArrayList<>(nameById.keySet())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 审批流定义(草稿/已驳回重编辑):flow 或 approvers 字段携带即全量替换(空数组=清空);
|
||||||
|
// 已发布不可变更流程(与内容同理),待审批已被上方拦截
|
||||||
|
if (status != PUBLISHED_CODE && (request.getFlow() != null || request.getApprovers() != null)) {
|
||||||
|
saveFlowDefinition(entity, resolveNoticeFlow(request), request.getApproverTeaIds());
|
||||||
|
}
|
||||||
// 推送范围全量替换(草稿/已驳回重编辑场景;null 不改动)。
|
// 推送范围全量替换(草稿/已驳回重编辑场景;null 不改动)。
|
||||||
// 已发布(旧链路)不允许变更范围:改范围而不重新圈人会造成可见性口径错乱
|
// 已发布(旧链路)不允许变更范围:改范围而不重新圈人会造成可见性口径错乱
|
||||||
if (request.getPushScopes() != null && status != PUBLISHED_CODE) {
|
if (request.getPushScopes() != null && status != PUBLISHED_CODE) {
|
||||||
@@ -987,6 +1535,8 @@ public class NoticeService implements INoticeService {
|
|||||||
noticeReceiverRepository.deleteByNoticeId(id);
|
noticeReceiverRepository.deleteByNoticeId(id);
|
||||||
pushScopeRepository.deleteByNoticeId(id);
|
pushScopeRepository.deleteByNoticeId(id);
|
||||||
approvalRepository.deleteByNoticeId(id);
|
approvalRepository.deleteByNoticeId(id);
|
||||||
|
approvalStepRepository.deleteByNoticeId(id);
|
||||||
|
approvalRecordRepository.deleteByNoticeId(id);
|
||||||
upFileRepository.deleteByTopicTypeAndTopicId("notice", id);
|
upFileRepository.deleteByTopicTypeAndTopicId("notice", id);
|
||||||
noticeRepository.deleteById(id);
|
noticeRepository.deleteById(id);
|
||||||
}
|
}
|
||||||
@@ -1035,13 +1585,19 @@ public class NoticeService implements INoticeService {
|
|||||||
entity.setId(null);
|
entity.setId(null);
|
||||||
entity.setDeptName(resolveDeptName(request));
|
entity.setDeptName(resolveDeptName(request));
|
||||||
normalizeReceiveList(entity, request.getReceiveList());
|
normalizeReceiveList(entity, request.getReceiveList());
|
||||||
entity.setStatus(PUBLISHED_CODE);
|
|
||||||
entity.setPublisherAccount(currentAccount());
|
entity.setPublisherAccount(currentAccount());
|
||||||
entity.setPublisherName(currentName());
|
entity.setPublisherName(currentName());
|
||||||
entity.setParentId(sourceId);
|
entity.setParentId(sourceId);
|
||||||
entity.setIsPulled(0);
|
entity.setIsPulled(0);
|
||||||
entity.setReadCount(0);
|
entity.setReadCount(0);
|
||||||
entity.setSchId(currentSchId());
|
entity.setSchId(currentSchId());
|
||||||
|
// 转发同发布分流:带审批流(或显式 approve)先送审,否则直接发布
|
||||||
|
List<FlowStepItem> flow = resolveNoticeFlow(request);
|
||||||
|
if ("approve".equalsIgnoreCase(String.valueOf(request.getPublishType())) || !flow.isEmpty()) {
|
||||||
|
doSubmitApproval(entity, flow, null, null);
|
||||||
|
return entity.getId();
|
||||||
|
}
|
||||||
|
entity.setStatus(PUBLISHED_CODE);
|
||||||
noticeRepository.save(entity);
|
noticeRepository.save(entity);
|
||||||
saveReceivers(entity);
|
saveReceivers(entity);
|
||||||
if (isImmediatePublish(entity)) {
|
if (isImmediatePublish(entity)) {
|
||||||
@@ -1158,6 +1714,8 @@ public class NoticeService implements INoticeService {
|
|||||||
vo.setUpFile(fids.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
vo.setUpFile(fids.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 批量填充多步审批字段(审批状态/进度文本/当前步下标/可否审批)
|
||||||
|
fillApprovalFields(content);
|
||||||
return PageUtils.toResult(new PageImpl<>(content, pageable, page.getTotalElements()));
|
return PageUtils.toResult(new PageImpl<>(content, pageable, page.getTotalElements()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1259,7 +1817,14 @@ public class NoticeService implements INoticeService {
|
|||||||
if (account.equals(notice.getPublisherAccount())) {
|
if (account.equals(notice.getPublisherAccount())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (parseTeaIds(notice.getApproverTeaIds()).contains(currentTeaId())) {
|
// 审批人放行:当前步账号快照(多步流程)或教师ID快照(旧链路)命中
|
||||||
|
if (parseAccounts(notice.getApproverAccounts()).contains(account)
|
||||||
|
|| parseTeaIds(notice.getApproverTeaIds()).contains(currentTeaId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 走过流程的审批人(历史轮次/已办步骤)也可查看
|
||||||
|
if (approvalRecordRepository.findByNoticeIdOrderByIdAsc(notice.getId()).stream()
|
||||||
|
.anyMatch(r -> account.equals(r.getApproverAccount()))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1792,6 +2357,15 @@ public class NoticeService implements INoticeService {
|
|||||||
data.setApprovalRecords(loadApprovalRecords(serid));
|
data.setApprovalRecords(loadApprovalRecords(serid));
|
||||||
// 当前绑定审批教师回显(编辑时按ID勾选教师树,姓名由前端经 ApproverResolve 绑定)
|
// 当前绑定审批教师回显(编辑时按ID勾选教师树,姓名由前端经 ApproverResolve 绑定)
|
||||||
data.setApproverTeaIds(parseTeaIds(vo.getApproverTeaIds()));
|
data.setApproverTeaIds(parseTeaIds(vo.getApproverTeaIds()));
|
||||||
|
// 多步审批流(步骤表 + 审核记录表)
|
||||||
|
List<NoticeApprovalStep> steps = approvalStepRepository.findByNoticeIdOrderByStepNoAsc(serid);
|
||||||
|
List<NoticeApprovalRecord> flowRecords = approvalRecordRepository.findByNoticeIdOrderByIdAsc(serid);
|
||||||
|
data.setApprovalEnabled(steps.isEmpty() ? 0 : 1);
|
||||||
|
data.setApprovalStatus(approvalStatusOf(vo.getStatus(), steps));
|
||||||
|
data.setApprovalStatusDesc(progressDesc(vo.getStatus(), steps, flowRecords));
|
||||||
|
data.setApprovalLevel(stepLevelOf(vo.getStatus(), steps));
|
||||||
|
data.setCanApprove(canApproveNow(vo.getStatus(), steps, flowRecords, currentAccount(), currentTeaId()));
|
||||||
|
data.setApprovalFlow(toFlowVos(vo.getStatus(), steps, flowRecords));
|
||||||
}
|
}
|
||||||
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId("notice", serid);
|
List<UpFile> files = upFileRepository.findByTopicTypeAndTopicId("notice", serid);
|
||||||
List<NoticeDetailVo.FileItem> fileList = files.stream().map(f -> {
|
List<NoticeDetailVo.FileItem> fileList = files.stream().map(f -> {
|
||||||
|
|||||||
@@ -343,12 +343,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel" id="approvePanel" style="display:none">
|
<div class="panel" id="approvePanel" style="display:none">
|
||||||
<div class="panel-title">审批人</div>
|
<div class="panel-title">审批流程</div>
|
||||||
<label class="form-label">审批教师<span class="req">*</span>(多人或签,任一人处理即可)</label>
|
<label class="check-inline" style="margin-bottom:10px">
|
||||||
|
<input type="checkbox" id="cApproval" onchange="onApprovalToggle(this.checked)">
|
||||||
|
选择审批人后走审批流程(不勾选=直接发布)
|
||||||
|
</label>
|
||||||
|
<div id="flowSteps" style="display:none"></div>
|
||||||
|
<div id="flowOps" style="display:none;margin-top:6px">
|
||||||
|
<button class="link-btn" onclick="addFlowStep()">+ 再加一步(多步依次审批)</button>
|
||||||
|
<button class="link-btn" style="margin-left:14px" onclick="loadApproverSuggest(1)">🔄 带入上次所选审批人(第 1 步)</button>
|
||||||
|
</div>
|
||||||
|
<div id="flowPicker" style="display:none;margin-top:10px">
|
||||||
|
<label class="form-label" id="flowPickerTitle">选择审批人</label>
|
||||||
|
<input class="form-input" id="approverSearch" placeholder="输入名字搜索" style="margin-bottom:8px" oninput="filterApproverTree()">
|
||||||
<div class="scope-tree" id="approverTree"><div style="padding:20px;text-align:center;color:var(--text3)">加载中…</div></div>
|
<div class="scope-tree" id="approverTree"><div style="padding:20px;text-align:center;color:var(--text3)">加载中…</div></div>
|
||||||
<div class="scope-chips" id="approverChips"></div>
|
<div style="margin-top:8px">
|
||||||
<button class="link-btn" style="margin-top:6px" onclick="loadApproverSuggest(1)">🔄 带入上次所选审批人</button>
|
<button class="btn btn-primary btn-sm" onclick="closeFlowPicker()">✓ 完成选择</button>
|
||||||
<div class="form-hint">展开组织架构勾选教师作为审批人,可多选;按所选教师提交审批,或签任一人处理即可</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-hint" id="flowHint" style="display:none">按顺序流转:上一步通过后进入下一步;任一步驳回则退回发布人。每步可多选审批人:或签任一人通过即过,会签需全部通过。</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="compose-actions" id="composeActions">
|
<div class="compose-actions" id="composeActions">
|
||||||
@@ -359,6 +372,7 @@
|
|||||||
<div class="compose-side"><!-- 右列 -->
|
<div class="compose-side"><!-- 右列 -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-title">推送范围(学员)</div>
|
<div class="panel-title">推送范围(学员)</div>
|
||||||
|
<input class="form-input" id="scopeSearch" placeholder="输入名字搜索" style="margin-bottom:8px" oninput="filterScopeTree()">
|
||||||
<div class="scope-tree" id="scopeTree"><div style="padding:20px;text-align:center;color:var(--text3)">加载中…</div></div>
|
<div class="scope-tree" id="scopeTree"><div style="padding:20px;text-align:center;color:var(--text3)">加载中…</div></div>
|
||||||
<div class="scope-chips" id="scopeChips"></div>
|
<div class="scope-chips" id="scopeChips"></div>
|
||||||
<div class="preview-box" id="scopePreview" style="display:none">
|
<div class="preview-box" id="scopePreview" style="display:none">
|
||||||
@@ -444,13 +458,15 @@
|
|||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<div>
|
<div>
|
||||||
<div class="page-title">审批中心</div>
|
<div class="page-title">审批中心</div>
|
||||||
<div class="page-sub">我作为审批人收到的审批任务(或签:任一审批人处理即可)</div>
|
<div class="page-sub">我作为审批人收到的审批任务(多步依次审批:上一步通过进入下一步,任一步驳回退回发布人)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="seg" id="auditSeg">
|
<div class="seg" id="auditSeg">
|
||||||
<button class="on" data-v="pending" onclick="auditFilter('pending',this)">待我审批</button>
|
<button class="on" data-v="pending" onclick="auditFilter('pending',this)">待我审批</button>
|
||||||
<button data-v="reviewed" onclick="auditFilter('reviewed',this)">已处理</button>
|
<button data-v="approved" onclick="auditFilter('approved',this)">已通过</button>
|
||||||
|
<button data-v="rejected" onclick="auditFilter('rejected',this)">已驳回</button>
|
||||||
|
<button data-v="all" onclick="auditFilter('all',this)">全部</button>
|
||||||
</div>
|
</div>
|
||||||
<input class="input search-box" id="auditKw" placeholder="🔍 搜索标题关键字" onkeydown="if(event.key==='Enter')reloadApprovals()">
|
<input class="input search-box" id="auditKw" placeholder="🔍 搜索标题关键字" onkeydown="if(event.key==='Enter')reloadApprovals()">
|
||||||
<button class="btn btn-primary btn-sm" onclick="reloadApprovals()">查询</button>
|
<button class="btn btn-primary btn-sm" onclick="reloadApprovals()">查询</button>
|
||||||
@@ -869,12 +885,52 @@ function loadDeptOptions() {
|
|||||||
}
|
}
|
||||||
async function loadScopeTree() {
|
async function loadScopeTree() {
|
||||||
try {
|
try {
|
||||||
scopeTreeData = await api('/notice/ScopeTree') || [];
|
// 发送范围改走学生班级树 base/getStuTree(toUser=false 仅到班级,不含学生叶子)
|
||||||
$('scopeTree').innerHTML = scopeTreeData.map(n => renderTreeNode(n, 0)).join('');
|
const raw = await api('/base/getStuTree', { params: { toUser: false } }) || [];
|
||||||
|
scopeTreeData = raw.map(n => toScopeNode(n, 1)).filter(Boolean);
|
||||||
|
$('scopeTree').innerHTML = scopeTreeData.length
|
||||||
|
? scopeTreeData.map(n => renderTreeNode(n, 0)).join('')
|
||||||
|
: '<div style="padding:20px;text-align:center;color:var(--text3)">暂无班级数据</div>';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$('scopeTree').innerHTML = '<div style="padding:20px;text-align:center;color:var(--text3)">范围树加载失败</div>';
|
$('scopeTree').innerHTML = '<div style="padding:20px;text-align:center;color:var(--text3)">范围树加载失败</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* base/getStuTree 节点 {id,name,type,children} → 推送范围节点 {scopeType,scopeId,name,children}。
|
||||||
|
getStuTree 组节点不区分学校/学段/年级/班级,按子树形态自底向上归类(对齐 PushScopeTypeEnum):
|
||||||
|
无组子节点=班级(4),其父=年级(3),再上=学部(2),顶层根强制=全校(1);
|
||||||
|
toUser=false 时无 type=2 叶子,出现则防御性跳过。 */
|
||||||
|
function toScopeNode(node, forcedType) {
|
||||||
|
if (!node || node.type !== 1) return null;
|
||||||
|
const children = (node.children || []).map(c => toScopeNode(c)).filter(Boolean);
|
||||||
|
let scopeType = forcedType;
|
||||||
|
if (!scopeType) {
|
||||||
|
if (!children.length) scopeType = 4;
|
||||||
|
else if (children.some(c => c.scopeType === 4)) scopeType = 3;
|
||||||
|
else if (children.some(c => c.scopeType === 3)) scopeType = 2;
|
||||||
|
else scopeType = 1;
|
||||||
|
}
|
||||||
|
return { scopeType, scopeId: node.id, name: node.name, children };
|
||||||
|
}
|
||||||
|
/* 按名称过滤范围树:命中节点及其祖先保留,其余分支隐藏;搜索时自动展开命中分支 */
|
||||||
|
function filterScopeTree() {
|
||||||
|
const kw = ($('scopeSearch').value || '').trim().toLowerCase();
|
||||||
|
const visit = nodes => {
|
||||||
|
let any = false;
|
||||||
|
for (const n of nodes) {
|
||||||
|
const el = $('tn-' + n.scopeType + ':' + n.scopeId);
|
||||||
|
const selfMatch = !kw || String(n.name || '').toLowerCase().includes(kw);
|
||||||
|
const childMatch = n.children && n.children.length ? visit(n.children) : false;
|
||||||
|
const hit = selfMatch || childMatch;
|
||||||
|
if (el) {
|
||||||
|
el.style.display = hit ? '' : 'none';
|
||||||
|
if (kw && childMatch) el.classList.add('expanded');
|
||||||
|
}
|
||||||
|
any = any || hit;
|
||||||
|
}
|
||||||
|
return any;
|
||||||
|
};
|
||||||
|
visit(scopeTreeData);
|
||||||
|
}
|
||||||
function renderTreeNode(node, depth) {
|
function renderTreeNode(node, depth) {
|
||||||
const key = node.scopeType + ':' + node.scopeId;
|
const key = node.scopeType + ':' + node.scopeId;
|
||||||
const hasChildren = node.children && node.children.length;
|
const hasChildren = node.children && node.children.length;
|
||||||
@@ -1001,9 +1057,14 @@ function buildNoticeBody() {
|
|||||||
pushScopes: composeState.scopes.length ? composeState.scopes : null
|
pushScopes: composeState.scopes.length ? composeState.scopes : null
|
||||||
};
|
};
|
||||||
if (composeState.sourceId) body.sourceId = composeState.sourceId;
|
if (composeState.sourceId) body.sourceId = composeState.sourceId;
|
||||||
// 编辑态:保存当前绑定的审批教师(空数组=清空审批人,后端据此持久化)
|
// 多步审批流:勾选审批时按步携带 flow([{name, vote, approverTeaIds}]);
|
||||||
if (composeState.mode === 'edit') {
|
// 编辑态未勾选 = flow:[] 清空流程定义,approverTeaIds:[] 清空旧教师快照
|
||||||
body.approverTeaIds = collectApproverTeaIds();
|
if (approvalEnabled()) {
|
||||||
|
body.flow = collectFlow();
|
||||||
|
if (composeState.mode === 'edit') body.approverTeaIds = [];
|
||||||
|
} else if (composeState.mode === 'edit') {
|
||||||
|
body.approverTeaIds = [];
|
||||||
|
body.flow = [];
|
||||||
}
|
}
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
@@ -1023,7 +1084,7 @@ function renderComposeActions() {
|
|||||||
<button class="btn btn-warn" id="btnApproveSubmit" onclick="onComposeApproveClick()">📨 提交审批</button>`;
|
<button class="btn btn-warn" id="btnApproveSubmit" onclick="onComposeApproveClick()">📨 提交审批</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* 新建模式提交审批:首点展开选人面板,再点确认提交 */
|
/* 新建模式提交审批:首点展开流程面板,再点确认提交 */
|
||||||
function onComposeApproveClick() {
|
function onComposeApproveClick() {
|
||||||
if ($('approvePanel').style.display === 'none') {
|
if ($('approvePanel').style.display === 'none') {
|
||||||
toggleApprovePanel();
|
toggleApprovePanel();
|
||||||
@@ -1036,24 +1097,131 @@ function toggleApprovePanel() {
|
|||||||
const p = $('approvePanel');
|
const p = $('approvePanel');
|
||||||
p.style.display = p.style.display === 'none' ? 'block' : 'none';
|
p.style.display = p.style.display === 'none' ? 'block' : 'none';
|
||||||
if (p.style.display === 'block') {
|
if (p.style.display === 'block') {
|
||||||
ensureApproverTree().catch(e => handleErr(e));
|
$('cApproval').checked = true;
|
||||||
|
onApprovalToggle(true);
|
||||||
loadApproverSuggest();
|
loadApproverSuggest();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 审批人教师树(base/GetSchoolAll:type=1 组 / type=2 教师) ---------- */
|
/* ---------- 多步审批流编辑器(后端 notice_approval_step 步骤表契约) ---------- */
|
||||||
const approverState = { treePromise: null, names: {}, selected: {} };
|
/* 每步 {name, vote: 'any'或签|'all'会签, teaIds:[教师ID], names:{id:姓名}};
|
||||||
|
选人复用教师树(base/getTeaTree,toUser=true 叶子带教师),提交按步传 approverTeaIds,
|
||||||
|
后端按学校教师名单绑定姓名后落步骤表;账号路径(审批人池)由新版前端使用。 */
|
||||||
|
const approverState = { treePromise: null, names: {}, rawTree: [] };
|
||||||
|
const flowState = { steps: [newFlowStep()], picking: -1 };
|
||||||
|
|
||||||
|
function newFlowStep() { return { name: '', vote: 'any', teaIds: [], names: {} }; }
|
||||||
|
function approvalEnabled() { return !!($('cApproval') && $('cApproval').checked); }
|
||||||
|
|
||||||
|
function onApprovalToggle(checked) {
|
||||||
|
$('flowSteps').style.display = checked ? 'block' : 'none';
|
||||||
|
$('flowOps').style.display = checked ? 'block' : 'none';
|
||||||
|
$('flowHint').style.display = checked ? 'block' : 'none';
|
||||||
|
if (checked) {
|
||||||
|
ensureApproverTree().catch(e => handleErr(e));
|
||||||
|
if (!flowState.steps.length) flowState.steps = [newFlowStep()];
|
||||||
|
renderFlowSteps();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFlowSteps() {
|
||||||
|
$('flowSteps').innerHTML = flowState.steps.map((s, i) => {
|
||||||
|
const chips = s.teaIds.map(tid =>
|
||||||
|
`<span class="chip">${esc(s.names[tid] || approverState.names[tid] || ('ID ' + tid))}<b onclick="removeFlowApprover(${i},'${escAttr(tid)}')">✕</b></span>`).join('');
|
||||||
|
return `<div style="margin:10px 0;padding:12px;border:1px solid #e4e7f0;border-radius:8px;background:#fafbfd">
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||||
|
<span class="badge b-normal">${i + 1}</span>
|
||||||
|
<span style="font-weight:500">第 ${i + 1} 步</span>
|
||||||
|
<select class="input" style="width:auto;padding:4px 8px" onchange="setFlowVote(${i},this.value)">
|
||||||
|
<option value="any"${s.vote !== 'all' ? ' selected' : ''}>或签(任一通过即可)</option>
|
||||||
|
<option value="all"${s.vote === 'all' ? ' selected' : ''}>会签(全部通过)</option>
|
||||||
|
</select>
|
||||||
|
<span style="flex:1"></span>
|
||||||
|
${i > 0 ? `<button class="link-btn" onclick="moveFlowStep(${i},-1)">上移</button>` : ''}
|
||||||
|
${i < flowState.steps.length - 1 ? `<button class="link-btn" onclick="moveFlowStep(${i},1)">下移</button>` : ''}
|
||||||
|
${flowState.steps.length > 1 ? `<button class="link-btn danger" onclick="removeFlowStep(${i})">删除</button>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="scope-chips"${chips ? '' : ' style="display:none"'}>${chips}</div>
|
||||||
|
${chips ? '' : '<div style="color:var(--text3);font-size:12px;margin:6px 0">未选择审批人</div>'}
|
||||||
|
<button class="btn btn-sm" style="margin-top:6px" onclick="openFlowPicker(${i})">+ 选择审批人</button>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFlowVote(i, vote) { flowState.steps[i].vote = vote === 'all' ? 'all' : 'any'; }
|
||||||
|
function addFlowStep() { flowState.steps.push(newFlowStep()); renderFlowSteps(); }
|
||||||
|
function removeFlowStep(i) { flowState.steps.splice(i, 1); renderFlowSteps(); }
|
||||||
|
function moveFlowStep(i, dir) {
|
||||||
|
const j = i + dir;
|
||||||
|
if (j < 0 || j >= flowState.steps.length) return;
|
||||||
|
const a = flowState.steps;
|
||||||
|
[a[i], a[j]] = [a[j], a[i]];
|
||||||
|
renderFlowSteps();
|
||||||
|
}
|
||||||
|
function removeFlowApprover(i, tid) {
|
||||||
|
const s = flowState.steps[i];
|
||||||
|
s.teaIds = s.teaIds.filter(id => String(id) !== String(tid));
|
||||||
|
delete s.names[tid];
|
||||||
|
renderFlowSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选人树:共享一棵,按 flowState.picking 定位当前编辑步骤 */
|
||||||
|
function openFlowPicker(i) {
|
||||||
|
flowState.picking = i;
|
||||||
|
$('flowPickerTitle').textContent = '第 ' + (i + 1) + ' 步 · 选择审批人(可多选)';
|
||||||
|
$('flowPicker').style.display = 'block';
|
||||||
|
ensureApproverTree().then(() => {
|
||||||
|
const sel = {};
|
||||||
|
flowState.steps[i].teaIds.forEach(tid => sel[String(tid)] = true);
|
||||||
|
document.querySelectorAll('#approverTree input[data-tid]').forEach(cb => {
|
||||||
|
cb.checked = !!sel[cb.getAttribute('data-tid')];
|
||||||
|
});
|
||||||
|
}).catch(e => handleErr(e));
|
||||||
|
}
|
||||||
|
function closeFlowPicker() {
|
||||||
|
flowState.picking = -1;
|
||||||
|
$('flowPicker').style.display = 'none';
|
||||||
|
renderFlowSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 收集流程:[{name, vote, approverTeaIds}],仅保留已选人的步 */
|
||||||
|
function collectFlow() {
|
||||||
|
return flowState.steps
|
||||||
|
.map((s, i) => ({ name: s.name || ('第' + (i + 1) + '步'), vote: s.vote || 'any', approverTeaIds: s.teaIds.slice() }))
|
||||||
|
.filter(s => s.approverTeaIds.length);
|
||||||
|
}
|
||||||
|
function flowStepCount() { return flowState.steps.filter(s => s.teaIds.length).length; }
|
||||||
|
|
||||||
|
/* 编辑回显:详情 approvalFlow[].approverTeaIds/vote → 编辑器 */
|
||||||
|
function applyFlowFromDetail(flow) {
|
||||||
|
flowState.steps = (flow || []).map((l, i) => ({
|
||||||
|
name: l.name || ('第' + (i + 1) + '步'),
|
||||||
|
vote: l.vote === 'all' ? 'all' : 'any',
|
||||||
|
teaIds: (l.approverTeaIds || []).map(Number),
|
||||||
|
names: {}
|
||||||
|
}));
|
||||||
|
(flow || []).forEach((l, i) => {
|
||||||
|
const s = flowState.steps[i];
|
||||||
|
const names = l.approverNames || [];
|
||||||
|
if (s.teaIds.length && names.length === s.teaIds.length) {
|
||||||
|
s.teaIds.forEach((tid, j) => s.names[tid] = names[j]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!flowState.steps.length) flowState.steps = [newFlowStep()];
|
||||||
|
renderFlowSteps();
|
||||||
|
}
|
||||||
|
|
||||||
function ensureApproverTree() {
|
function ensureApproverTree() {
|
||||||
if (!approverState.treePromise) {
|
if (!approverState.treePromise) {
|
||||||
approverState.treePromise = api('/base/GetSchoolAll').then(data => {
|
// 教师树 bschclass /oapi/SchGroup/getTeaTree:toUser=true 组下挂教师叶子,
|
||||||
|
// 叶子 id=teaId 与后端绑定姓名的 UserTeaSerID 同一 ID 空间
|
||||||
|
approverState.treePromise = api('/base/getTeaTree', { params: { toUser: true } }).then(data => {
|
||||||
const tree = data || [];
|
const tree = data || [];
|
||||||
|
approverState.rawTree = tree;
|
||||||
approverState.names = {};
|
approverState.names = {};
|
||||||
$('approverTree').innerHTML = tree.length
|
$('approverTree').innerHTML = tree.length
|
||||||
? tree.map(n => renderApproverNode(n)).join('')
|
? tree.map(n => renderApproverNode(n)).join('')
|
||||||
: '<div style="padding:20px;text-align:center;color:var(--text3)">暂无教师数据</div>';
|
: '<div style="padding:20px;text-align:center;color:var(--text3)">暂无教师数据</div>';
|
||||||
// 渲染后按已选状态回勾(编辑回显早于树加载完成的情况)
|
|
||||||
Object.keys(approverState.selected).forEach(tid => syncApproverChecks(tid, true));
|
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
$('approverTree').innerHTML = '<div style="padding:20px;text-align:center;color:var(--text3)">教师树加载失败,可重试</div>';
|
$('approverTree').innerHTML = '<div style="padding:20px;text-align:center;color:var(--text3)">教师树加载失败,可重试</div>';
|
||||||
approverState.treePromise = null;
|
approverState.treePromise = null;
|
||||||
@@ -1063,6 +1231,35 @@ function ensureApproverTree() {
|
|||||||
return approverState.treePromise;
|
return approverState.treePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 按名称过滤审批人树:命中教师及其所在组保留,其余分支隐藏;搜索时自动展开命中分支。
|
||||||
|
同一教师在多组重复出现时,所有 data-tid 副本同步显隐。 */
|
||||||
|
function filterApproverTree() {
|
||||||
|
const kw = ($('approverSearch').value || '').trim().toLowerCase();
|
||||||
|
const visit = nodes => {
|
||||||
|
let any = false;
|
||||||
|
for (const n of nodes) {
|
||||||
|
const selfMatch = !kw || String(n.name || '').toLowerCase().includes(kw);
|
||||||
|
const childMatch = n.children && n.children.length ? visit(n.children) : false;
|
||||||
|
const hit = selfMatch || childMatch;
|
||||||
|
if (n.type === 1) {
|
||||||
|
const el = $('tn-atn-' + n.id);
|
||||||
|
if (el) {
|
||||||
|
el.style.display = hit ? '' : 'none';
|
||||||
|
if (kw && childMatch) el.classList.add('expanded');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.querySelectorAll('#approverTree input[data-tid="' + n.id + '"]').forEach(cb => {
|
||||||
|
const leaf = cb.closest('.tree-node');
|
||||||
|
if (leaf) leaf.style.display = hit ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
any = any || hit;
|
||||||
|
}
|
||||||
|
return any;
|
||||||
|
};
|
||||||
|
visit(approverState.rawTree || []);
|
||||||
|
}
|
||||||
|
|
||||||
function renderApproverNode(node) {
|
function renderApproverNode(node) {
|
||||||
if (node.type === 1) {
|
if (node.type === 1) {
|
||||||
const hasChildren = node.children && node.children.length;
|
const hasChildren = node.children && node.children.length;
|
||||||
@@ -1093,82 +1290,61 @@ function onApproverLabelClick(el) {
|
|||||||
onApproverCheck(cb);
|
onApproverCheck(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 勾选写入当前编辑步骤(flowState.picking),同教师多组重复出现时全树同步勾选态 */
|
||||||
function onApproverCheck(cb) {
|
function onApproverCheck(cb) {
|
||||||
|
if (flowState.picking < 0) { cb.checked = false; return; }
|
||||||
const tid = cb.getAttribute('data-tid');
|
const tid = cb.getAttribute('data-tid');
|
||||||
if (cb.checked) {
|
const checked = cb.checked;
|
||||||
approverState.selected[tid] = { id: tid, name: approverState.names[tid] || ('ID ' + tid) };
|
const step = flowState.steps[flowState.picking];
|
||||||
|
if (checked) {
|
||||||
|
if (!step.teaIds.some(id => String(id) === tid)) {
|
||||||
|
step.teaIds.push(Number(tid));
|
||||||
|
step.names[tid] = approverState.names[tid] || ('ID ' + tid);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
delete approverState.selected[tid];
|
step.teaIds = step.teaIds.filter(id => String(id) !== tid);
|
||||||
|
delete step.names[tid];
|
||||||
}
|
}
|
||||||
syncApproverChecks(tid, cb.checked);
|
document.querySelectorAll('#approverTree input[data-tid="' + tid + '"]').forEach(x => { x.checked = checked; });
|
||||||
renderApproverChips();
|
renderFlowSteps();
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncApproverChecks(tid, checked) {
|
/* 带入上次所选审批人(ApproverSuggest 记忆 → 第 1 步) */
|
||||||
document.querySelectorAll('#approverTree input[data-tid="' + tid + '"]').forEach(cb => { cb.checked = checked; });
|
async function loadApproverSuggest(byUser) {
|
||||||
|
try {
|
||||||
|
const v = await api('/notice/ApproverSuggest');
|
||||||
|
if (v && v.teaIds && v.teaIds.length) {
|
||||||
|
const missed = await bindFlowStepByTeaIds(0, v.teaIds);
|
||||||
|
renderFlowSteps();
|
||||||
|
toast('已带入上次所选 ' + flowState.steps[0].teaIds.length + ' 名审批人(第 1 步)' + (missed.length ? '(' + missed.join('、') + ' 未找到)' : ''), 'ok');
|
||||||
|
} else if (byUser) {
|
||||||
|
toast('暂无审批人记忆,请在第 1 步中勾选教师', 'err');
|
||||||
|
}
|
||||||
|
} catch (e) { handleErr(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderApproverChips() {
|
/* 按教师ID绑定某步(记忆带入/回显),姓名经 ApproverResolve 解析,未命中降级 ID */
|
||||||
const items = Object.values(approverState.selected);
|
async function bindFlowStepByTeaIds(stepIdx, teaIds) {
|
||||||
$('approverChips').innerHTML = items.map(t =>
|
|
||||||
`<span class="chip">${esc(t.name)}<b onclick="removeApprover('${escAttr(t.id)}')">✕</b></span>`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeApprover(tid) {
|
|
||||||
delete approverState.selected[tid];
|
|
||||||
syncApproverChecks(tid, false);
|
|
||||||
renderApproverChips();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearApproverSelection() {
|
|
||||||
Object.keys(approverState.selected).forEach(tid => syncApproverChecks(tid, false));
|
|
||||||
approverState.selected = {};
|
|
||||||
renderApproverChips();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 已选教师ID列表(教师树叶子节点ID,直接提交后端绑定校验) */
|
|
||||||
function collectApproverTeaIds() {
|
|
||||||
return Object.keys(approverState.selected).map(id => Number(id)).filter(id => !isNaN(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 按教师ID勾树(带入上次所选 / 编辑回显);姓名优先 ApproverResolve 绑定,教师已离职等未命中时降级展示 */
|
|
||||||
async function bindApproversByTeaIds(teaIds) {
|
|
||||||
const missed = [];
|
const missed = [];
|
||||||
if (!teaIds || !teaIds.length) return missed;
|
if (!teaIds || !teaIds.length) return missed;
|
||||||
await ensureApproverTree();
|
const step = flowState.steps[stepIdx];
|
||||||
|
step.teaIds = [];
|
||||||
|
step.names = {};
|
||||||
let byId = {};
|
let byId = {};
|
||||||
try {
|
try {
|
||||||
const resolved = await api('/notice/ApproverResolve', {
|
const resolved = await api('/notice/ApproverResolve', {
|
||||||
params: { ids: teaIds.join(',') }
|
params: { ids: teaIds.join(',') }
|
||||||
}) || [];
|
}) || [];
|
||||||
resolved.forEach(r => { byId[r.id] = r; });
|
resolved.forEach(r => { byId[r.id] = r; });
|
||||||
} catch (e) { /* 绑定接口失败不阻断勾选,姓名降级树节点/ID */ }
|
} catch (e) { /* 绑定接口失败不阻断,姓名降级 ID */ }
|
||||||
for (const tid of teaIds) {
|
for (const tid of teaIds) {
|
||||||
const r = byId[tid];
|
const r = byId[tid];
|
||||||
const treeName = approverState.names[tid];
|
if (!r || !r.name) missed.push('ID ' + tid);
|
||||||
// 未命中学校教师名单(教师已离职/转校):保留勾选并提示,提交时后端会再次校验
|
step.teaIds.push(Number(tid));
|
||||||
if (!r || !r.name) missed.push(treeName || ('ID ' + tid));
|
step.names[tid] = (r && r.name) || ('ID ' + tid);
|
||||||
approverState.selected[tid] = { id: tid, name: (r && r.name) || treeName || ('ID ' + tid) };
|
|
||||||
syncApproverChecks(tid, true);
|
|
||||||
}
|
}
|
||||||
renderApproverChips();
|
|
||||||
return missed;
|
return missed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadApproverSuggest(byUser) {
|
|
||||||
try {
|
|
||||||
const v = await api('/notice/ApproverSuggest');
|
|
||||||
if (v && v.teaIds && v.teaIds.length) {
|
|
||||||
clearApproverSelection();
|
|
||||||
const missed = await bindApproversByTeaIds(v.teaIds);
|
|
||||||
const n = Object.keys(approverState.selected).length;
|
|
||||||
if (n) toast('已带入上次所选 ' + n + ' 名审批人' + (missed.length ? '(' + missed.join('、') + ' 未找到)' : ''), 'ok');
|
|
||||||
else if (byUser) toast('上次所选审批人已不存在,请重新勾选', 'err');
|
|
||||||
} else if (byUser) {
|
|
||||||
toast('暂无审批人记忆,请在组织架构中勾选教师', 'err');
|
|
||||||
}
|
|
||||||
} catch (e) { handleErr(e); }
|
|
||||||
}
|
|
||||||
function resetCompose() {
|
function resetCompose() {
|
||||||
composeState.mode = 'create';
|
composeState.mode = 'create';
|
||||||
composeState.sourceId = null;
|
composeState.sourceId = null;
|
||||||
@@ -1183,13 +1359,18 @@ function resetCompose() {
|
|||||||
$('cReceipt').checked = false;
|
$('cReceipt').checked = false;
|
||||||
$('cTiming').value = '';
|
$('cTiming').value = '';
|
||||||
$('cReceivers').value = '';
|
$('cReceivers').value = '';
|
||||||
clearApproverSelection();
|
flowState.steps = [newFlowStep()];
|
||||||
|
flowState.picking = -1;
|
||||||
|
$('cApproval').checked = false;
|
||||||
|
onApprovalToggle(false);
|
||||||
|
closeFlowPicker();
|
||||||
$('approvePanel').style.display = 'none';
|
$('approvePanel').style.display = 'none';
|
||||||
renderComposeFiles(); renderScopeChips(); renderComposeActions();
|
renderComposeFiles(); renderScopeChips(); renderComposeActions();
|
||||||
$('composeTitle').textContent = '写通知';
|
$('composeTitle').textContent = '写通知';
|
||||||
$('composeSub').textContent = '填写内容并选择推送范围,支持直接发布、存为草稿或提交审批';
|
$('composeSub').textContent = '填写内容并选择推送范围,支持直接发布、存为草稿或提交审批';
|
||||||
$('composeBack').style.display = 'none';
|
$('composeBack').style.display = 'none';
|
||||||
document.querySelectorAll('#scopeTree input[type=checkbox]').forEach(cb => cb.checked = false);
|
document.querySelectorAll('#scopeTree input[type=checkbox]').forEach(cb => cb.checked = false);
|
||||||
|
if ($('scopeSearch')) { $('scopeSearch').value = ''; filterScopeTree(); }
|
||||||
}
|
}
|
||||||
/* 进入编辑/转发模式并预填 */
|
/* 进入编辑/转发模式并预填 */
|
||||||
async function openComposeEdit(serid, mode) {
|
async function openComposeEdit(serid, mode) {
|
||||||
@@ -1234,16 +1415,37 @@ async function openComposeEdit(serid, mode) {
|
|||||||
// 附件回显
|
// 附件回显
|
||||||
composeState.files = d.chaoSend.map(f => ({ fileid: f.fileid || f.id, name: f.name || f.filename, size: f.size }));
|
composeState.files = d.chaoSend.map(f => ({ fileid: f.fileid || f.id, name: f.name || f.filename, size: f.size }));
|
||||||
renderComposeFiles();
|
renderComposeFiles();
|
||||||
// 审批人回显:仅编辑态且已有审批教师时才显示面板并绑定勾选;
|
// 审批流程面板显隐规则:有审核人才显示,没有则收起(用户点「提交审批」时再展开选人)。
|
||||||
// 为空(null/[])则保持收起,用户点「提交审批」时再展开选人。
|
if (mode === 'edit' && approvalHasApprovers(d)) {
|
||||||
if (mode === 'edit' && d.approverTeaIds && d.approverTeaIds.length) {
|
|
||||||
$('approvePanel').style.display = 'block';
|
$('approvePanel').style.display = 'block';
|
||||||
ensureApproverTree().catch(e => handleErr(e));
|
$('cApproval').checked = true;
|
||||||
const missed = await bindApproversByTeaIds(d.approverTeaIds);
|
onApprovalToggle(true);
|
||||||
|
if (d.approvalFlow && d.approvalFlow.length) {
|
||||||
|
// 新流程回显:approvalFlow(多步,含每步 approverTeaIds/vote)→ 编辑器
|
||||||
|
applyFlowFromDetail(d.approvalFlow);
|
||||||
|
const empty = flowState.steps.filter(s => !s.teaIds.length).length;
|
||||||
|
if (empty) toast('有 ' + empty + ' 个步骤未选择审批人,提交前请补选', 'err');
|
||||||
|
} else {
|
||||||
|
// 旧版单步审批人快照(有 approverTeaIds 无步骤表):回显为第 1 步。
|
||||||
|
// 否则保存会按「清空流程」把审批人一并丢失。
|
||||||
|
flowState.steps = [newFlowStep()];
|
||||||
|
const missed = await bindFlowStepByTeaIds(0, d.approverTeaIds);
|
||||||
|
renderFlowSteps();
|
||||||
if (missed.length) toast('审批教师 ' + missed.join('、') + ' 未在本校教师名单,请重新选择', 'err');
|
if (missed.length) toast('审批教师 ' + missed.join('、') + ' 未在本校教师名单,请重新选择', 'err');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e) { handleErr(e); }
|
} catch (e) { handleErr(e); }
|
||||||
}
|
}
|
||||||
|
/* 是否存在审核人:新流程步骤(approverTeaIds/approvers)或旧版单步快照 approverTeaIds */
|
||||||
|
function approvalHasApprovers(d) {
|
||||||
|
if (!d) return false;
|
||||||
|
if (Array.isArray(d.approvalFlow) && d.approvalFlow.some(l =>
|
||||||
|
(Array.isArray(l.approverTeaIds) && l.approverTeaIds.length) ||
|
||||||
|
(Array.isArray(l.approvers) && l.approvers.length))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !!(d.approverTeaIds && d.approverTeaIds.length);
|
||||||
|
}
|
||||||
/* GetDetail/Detail 返回沿用旧 .NET 字段名(DeptID/Contents/ChaoSend…),归一化为前端小写风格 */
|
/* GetDetail/Detail 返回沿用旧 .NET 字段名(DeptID/Contents/ChaoSend…),归一化为前端小写风格 */
|
||||||
function normalizeDetail(d) {
|
function normalizeDetail(d) {
|
||||||
d = d || {};
|
d = d || {};
|
||||||
@@ -1262,36 +1464,57 @@ function htmlToText(html) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 提交 ---------- */
|
/* ---------- 提交 ---------- */
|
||||||
|
/* 展开审批流程面板并打开指定步骤的选人区(新建/编辑提交审批共用)。
|
||||||
|
编辑态初始面板是收起的,若只调 openFlowPicker 会让选人树落在隐藏面板内不可见。 */
|
||||||
|
function revealApprovalEditor(stepIndex) {
|
||||||
|
const idx = stepIndex || 0;
|
||||||
|
const p = $('approvePanel');
|
||||||
|
p.style.display = 'block';
|
||||||
|
$('cApproval').checked = true;
|
||||||
|
if (!flowState.steps.length) flowState.steps = [newFlowStep()];
|
||||||
|
onApprovalToggle(true);
|
||||||
|
openFlowPicker(idx);
|
||||||
|
p.scrollIntoView({ block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
async function submitCompose(action) {
|
async function submitCompose(action) {
|
||||||
const body = buildNoticeBody();
|
const body = buildNoticeBody();
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
if (action === 'approve') {
|
if (action === 'approve') {
|
||||||
|
if (!flowStepCount()) {
|
||||||
|
revealApprovalEditor(0);
|
||||||
|
return toast('请先为第 1 步勾选审批教师', 'err');
|
||||||
|
}
|
||||||
|
if (!approvalEnabled()) {
|
||||||
$('approvePanel').style.display = 'block';
|
$('approvePanel').style.display = 'block';
|
||||||
await ensureApproverTree().catch(e => handleErr(e));
|
$('cApproval').checked = true;
|
||||||
const teaIds = collectApproverTeaIds();
|
onApprovalToggle(true);
|
||||||
if (!teaIds.length) { $('approverTree').scrollIntoView({ block: 'center' }); return toast('请在组织架构中勾选审批教师', 'err'); }
|
}
|
||||||
body.publishType = 'approve';
|
body.publishType = 'approve';
|
||||||
body.approverTeaIds = teaIds;
|
body.flow = collectFlow();
|
||||||
} else {
|
} else {
|
||||||
body.publishType = action;
|
body.publishType = action;
|
||||||
|
// 直接发布 = 跳过审批流(勾选审批仅对 提交审批/存草稿 生效)
|
||||||
|
if (action === 'direct') delete body.flow;
|
||||||
}
|
}
|
||||||
await doSubmitCompose(body, action);
|
await doSubmitCompose(body, action);
|
||||||
}
|
}
|
||||||
/* 从既有通知(草稿/已驳回)提交审批:先保存修改,再提交 */
|
/* 从既有通知(草稿/已驳回)提交审批:先保存修改(含流程定义),再按步提交审批 */
|
||||||
async function submitComposeFromForm() {
|
async function submitComposeFromForm() {
|
||||||
const serid = composeState.sourceId;
|
const serid = composeState.sourceId;
|
||||||
if (!serid) return toast('缺少通知ID', 'err');
|
if (!serid) return toast('缺少通知ID', 'err');
|
||||||
const body = buildNoticeBody();
|
const body = buildNoticeBody();
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
$('approvePanel').style.display = 'block';
|
if (!flowStepCount()) {
|
||||||
await ensureApproverTree().catch(e => handleErr(e));
|
// 编辑态首点提交审批:展开流程面板与选人树(否则选人区在隐藏面板内不可见),选人后再次点击即提交
|
||||||
const teaIds = collectApproverTeaIds();
|
revealApprovalEditor(0);
|
||||||
if (!teaIds.length) { $('approverTree').scrollIntoView({ block: 'center' }); return toast('请在组织架构中勾选审批教师', 'err'); }
|
return toast('请先为第 1 步勾选审批教师', 'err');
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await api('/notice/Edit', { method: 'POST', body });
|
await api('/notice/Edit', { method: 'POST', body });
|
||||||
await api('/notice/Submit', {
|
await api('/notice/Submit', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { serid, approverTeaIds: teaIds, opinion: null }
|
body: { serid, steps: collectFlow(), opinion: null }
|
||||||
});
|
});
|
||||||
toast('已提交审批', 'ok');
|
toast('已提交审批', 'ok');
|
||||||
resetCompose();
|
resetCompose();
|
||||||
@@ -1369,7 +1592,16 @@ async function loadSent() {
|
|||||||
else if (status === 1) statusBadge = (n.timingSendTime && n.timingPublished === 0 && n.isPulled !== 1)
|
else if (status === 1) statusBadge = (n.timingSendTime && n.timingPublished === 0 && n.isPulled !== 1)
|
||||||
? '<span class="badge b-timing">定时中</span><span class="badge b-published">已排期</span>'
|
? '<span class="badge b-timing">定时中</span><span class="badge b-published">已排期</span>'
|
||||||
: '<span class="badge b-published">已发布</span>';
|
: '<span class="badge b-published">已发布</span>';
|
||||||
else if (status === 2) statusBadge = '<span class="badge b-pending">待审批</span>';
|
else if (status === 2) {
|
||||||
|
// 进度文本与徽标重复(旧版单步无步骤表时 desc 即「待审批」)则不再显示
|
||||||
|
const desc = n.approvalStatusDesc && n.approvalStatusDesc !== '待审批' ? n.approvalStatusDesc : '';
|
||||||
|
const approverIds = parseTeaIdList(n.approverTeaIds);
|
||||||
|
statusBadge = '<span class="badge b-pending">待审批</span>'
|
||||||
|
+ (desc ? ` <span style="color:var(--text3)">${esc(desc)}</span>` : '')
|
||||||
|
+ (approverIds.length
|
||||||
|
? `<br><span class="pending-approvers" data-ids="${approverIds.join(',')}" style="font-size:12px;color:var(--text3)">审核人:加载中…</span>`
|
||||||
|
: '');
|
||||||
|
}
|
||||||
else if (status === 3) statusBadge = '<span class="badge b-rejected">已驳回</span>';
|
else if (status === 3) statusBadge = '<span class="badge b-rejected">已驳回</span>';
|
||||||
if (n.isPulled === 1) statusBadge += ' <span class="badge b-pulled">已撤回</span>';
|
if (n.isPulled === 1) statusBadge += ' <span class="badge b-pulled">已撤回</span>';
|
||||||
if (n.topmost == 1) statusBadge += ' <span class="badge b-top">置顶</span>';
|
if (n.topmost == 1) statusBadge += ' <span class="badge b-top">置顶</span>';
|
||||||
@@ -1405,8 +1637,45 @@ async function loadSent() {
|
|||||||
<td class="td-actions">${acts.join('')}</td>
|
<td class="td-actions">${acts.join('')}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
fillSentApprovers();
|
||||||
renderPager('sentPager', r, sentState.pageIndex, p => { sentState.pageIndex = p; loadSent(); });
|
renderPager('sentPager', r, sentState.pageIndex, p => { sentState.pageIndex = p; loadSent(); });
|
||||||
}
|
}
|
||||||
|
/* 审批教师ID快照("[10007,10008]" 或数组)→ 数字数组 */
|
||||||
|
function parseTeaIdList(raw) {
|
||||||
|
if (!raw) return [];
|
||||||
|
if (Array.isArray(raw)) return raw.map(Number).filter(Boolean);
|
||||||
|
try {
|
||||||
|
const a = JSON.parse(raw);
|
||||||
|
return Array.isArray(a) ? a.map(Number).filter(Boolean) : [];
|
||||||
|
} catch (e) {
|
||||||
|
return String(raw).replace(/[\[\]\s]/g, '').split(',').filter(Boolean).map(Number).filter(Boolean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 审批教师ID→姓名缓存(已发列表「待审批」行展示审核人,避免逐行重复请求) */
|
||||||
|
const approverNameCache = {};
|
||||||
|
async function resolveApproverNames(teaIds) {
|
||||||
|
const missing = teaIds.map(Number).filter(id => id && !(String(id) in approverNameCache));
|
||||||
|
if (missing.length) {
|
||||||
|
try {
|
||||||
|
const resolved = await api('/notice/ApproverResolve', { params: { ids: missing.join(',') } }) || [];
|
||||||
|
resolved.forEach(r => { approverNameCache[String(r.id)] = r.name || ('ID ' + r.id); });
|
||||||
|
} catch (e) { /* 解析失败降级 ID 展示,不阻断列表 */ }
|
||||||
|
missing.forEach(id => { if (!(String(id) in approverNameCache)) approverNameCache[String(id)] = 'ID ' + id; });
|
||||||
|
}
|
||||||
|
return teaIds.map(id => approverNameCache[String(id)] || ('ID ' + id));
|
||||||
|
}
|
||||||
|
/* 待审批行的审核人占位 span → 批量解析为「审核人:姓名1、姓名2」 */
|
||||||
|
async function fillSentApprovers() {
|
||||||
|
const spans = Array.from(document.querySelectorAll('#sentBody .pending-approvers'));
|
||||||
|
if (!spans.length) return;
|
||||||
|
const groups = spans.map(el => ({ el, ids: (el.dataset.ids || '').split(',').filter(Boolean) }));
|
||||||
|
await resolveApproverNames(groups.flatMap(g => g.ids));
|
||||||
|
groups.forEach(({ el, ids }) => {
|
||||||
|
el.textContent = ids.length
|
||||||
|
? '审核人:' + ids.map(id => approverNameCache[String(id)] || ('ID ' + id)).join('、')
|
||||||
|
: '';
|
||||||
|
});
|
||||||
|
}
|
||||||
/* 已发列表点标题:管理端查看(不标记学员已读,用 GetDetail 呈现) */
|
/* 已发列表点标题:管理端查看(不标记学员已读,用 GetDetail 呈现) */
|
||||||
async function previewSent(serid) {
|
async function previewSent(serid) {
|
||||||
let d;
|
let d;
|
||||||
@@ -1421,7 +1690,27 @@ async function previewSent(serid) {
|
|||||||
if (d.readnumcount != null) metas.push('已读 ' + d.readnumcount + ' 人');
|
if (d.readnumcount != null) metas.push('已读 ' + d.readnumcount + ' 人');
|
||||||
if (d.statusDesc) metas.push('状态:' + esc(d.statusDesc));
|
if (d.statusDesc) metas.push('状态:' + esc(d.statusDesc));
|
||||||
$('dMeta').innerHTML = metas.map(m => '<span>' + m + '</span>').join('');
|
$('dMeta').innerHTML = metas.map(m => '<span>' + m + '</span>').join('');
|
||||||
|
// 多步审批流进度(notice_approval_step):步骤名 · 或签/会签 · 状态 · 已处理/待签
|
||||||
|
if (d.approvalFlow && d.approvalFlow.length) {
|
||||||
|
const stBadge = {
|
||||||
|
approved: '<span class="badge b-published">已通过</span>',
|
||||||
|
rejected: '<span class="badge b-rejected">已驳回</span>',
|
||||||
|
pending: '<span class="badge b-pending">审批中</span>',
|
||||||
|
waiting: '<span class="badge b-normal">待流转</span>'
|
||||||
|
};
|
||||||
|
const flowHtml = '<div style="margin-bottom:12px;padding:10px 12px;border:1px solid #e4e7f0;border-radius:8px;background:#fafbfd">'
|
||||||
|
+ `<div style="font-weight:600;margin-bottom:6px">审批流程(当前第 ${Math.min((d.approvalLevel || 0) + 1, d.approvalFlow.length)}/${d.approvalFlow.length} 步)</div>`
|
||||||
|
+ d.approvalFlow.map((s, i) => {
|
||||||
|
const namesText = (s.approverNames || []).join('、') || '-';
|
||||||
|
const acted = (s.records || []).map(r => `${esc(r.name || r.account)}${r.result === 'rejected' ? '(驳回:' + esc(r.remark || '') + ')' : ''}`).join('、');
|
||||||
|
const pend = (s.pending || []).join('、');
|
||||||
|
return `<div style="font-size:12px;color:var(--text2);margin:3px 0">${i + 1}. ${esc(s.name || ('第' + (i + 1) + '步'))} · ${s.vote === 'all' ? '会签' : '或签'} ${stBadge[s.status] || ''}<br/><span style="color:var(--text3)">审批人:${esc(namesText)}${acted ? '|已处理:' + acted : ''}${pend ? '|待签:' + esc(pend) : ''}</span></div>`;
|
||||||
|
}).join('')
|
||||||
|
+ '</div>';
|
||||||
|
$('dContent').innerHTML = flowHtml + renderRichText(d.contents || '(无正文)');
|
||||||
|
} else {
|
||||||
$('dContent').innerHTML = renderRichText(d.contents || '(无正文)');
|
$('dContent').innerHTML = renderRichText(d.contents || '(无正文)');
|
||||||
|
}
|
||||||
const files = d.chaoSend || [];
|
const files = d.chaoSend || [];
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
$('dFiles').style.display = 'block';
|
$('dFiles').style.display = 'block';
|
||||||
@@ -1555,56 +1844,59 @@ function auditFilter(tab, btn) {
|
|||||||
}
|
}
|
||||||
function renderAuditHead() {
|
function renderAuditHead() {
|
||||||
$('auditHead').innerHTML = auditState.tab === 'pending'
|
$('auditHead').innerHTML = auditState.tab === 'pending'
|
||||||
? '<tr><th style="width:32%">标题</th><th>类型</th><th>发布人</th><th>审批人</th><th>提交时间</th><th style="width:150px">操作</th></tr>'
|
? '<tr><th style="width:28%">标题</th><th>类型</th><th>发布人</th><th style="width:24%">审批进度</th><th>发布时间</th><th style="width:180px">操作</th></tr>'
|
||||||
: '<tr><th style="width:28%">标题</th><th>发布人</th><th>处理结果</th><th>处理人</th><th>处理时间</th><th>意见</th></tr>';
|
: '<tr><th style="width:28%">标题</th><th>类型</th><th>发布人</th><th style="width:24%">审批进度</th><th>状态</th><th>时间</th></tr>';
|
||||||
}
|
}
|
||||||
function reloadApprovals() { auditState.pageIndex = 1; loadApprovals(); }
|
function reloadApprovals() { auditState.pageIndex = 1; loadApprovals(); }
|
||||||
async function loadApprovals() {
|
async function loadApprovals() {
|
||||||
const body = $('auditBody');
|
const body = $('auditBody');
|
||||||
const cols = auditState.tab === 'pending' ? 6 : 6;
|
body.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--text3);padding:30px">加载中…</td></tr>';
|
||||||
body.innerHTML = `<tr><td colspan="${cols}" style="text-align:center;color:var(--text3);padding:30px">加载中…</td></tr>`;
|
|
||||||
let r;
|
let r;
|
||||||
try {
|
try {
|
||||||
r = await api('/notice/ApprovalList', {
|
r = await api('/notice/ApprovalList', {
|
||||||
params: {
|
params: {
|
||||||
auditState: auditState.tab, pageIndex: auditState.pageIndex,
|
status: auditState.tab, pageIndex: auditState.pageIndex,
|
||||||
pageSize: auditState.pageSize, keyword: $('auditKw').value.trim()
|
pageSize: auditState.pageSize, keyword: $('auditKw').value.trim()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) { return handleListError(e, body, cols); }
|
} catch (e) { return handleListError(e, body, 6); }
|
||||||
const list = (r && r.content) || [];
|
const list = (r && r.content) || [];
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
body.innerHTML = `<tr><td colspan="${cols}"><div class="empty"><div class="icon">${auditState.tab === 'pending' ? '✅' : '🗂'}</div>${auditState.tab === 'pending' ? '暂无待审批任务' : '暂无处理记录'}</div></td></tr>`;
|
body.innerHTML = `<tr><td colspan="6"><div class="empty"><div class="icon">${auditState.tab === 'pending' ? '✅' : '🗂'}</div>${auditState.tab === 'pending' ? '暂无待审批任务' : '暂无记录'}</div></td></tr>`;
|
||||||
$('auditPager').innerHTML = '';
|
$('auditPager').innerHTML = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
body.innerHTML = list.map(n => {
|
body.innerHTML = list.map(n => {
|
||||||
|
const typeBadge = n.type == 2 ? '<span class="badge b-important">重要</span>' : '<span class="badge b-normal">普通</span>';
|
||||||
|
const progress = esc(n.approvalStatusDesc || n.statusDesc || '-');
|
||||||
|
const time = esc((n.updateTime || n.createTime || '').replace('T', ' '));
|
||||||
if (auditState.tab === 'pending') {
|
if (auditState.tab === 'pending') {
|
||||||
|
const acts = [`<button class="link-btn" onclick="auditDetail(${n.id})">查看</button>`];
|
||||||
|
if (n.canApprove) {
|
||||||
|
acts.push('<button class="link-btn success" onclick="approveAction(' + n.id + ',true)">通过</button>');
|
||||||
|
acts.push('<button class="link-btn danger" onclick="approveAction(' + n.id + ',false)">驳回</button>');
|
||||||
|
} else {
|
||||||
|
acts.push('<span style="color:var(--text3)">等待他人审批</span>');
|
||||||
|
}
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td class="td-title" onclick="auditDetail(${n.id})" title="${escAttr(n.title)}">${esc(n.title)}
|
<td class="td-title" onclick="auditDetail(${n.id})" title="${escAttr(n.title)}">${esc(n.title)}${n.timingSendTime ? ' <span class="badge b-timing">定时</span>' : ''}</td>
|
||||||
${n.type == 2 ? '<span class="badge b-important">重要</span>' : ''}
|
<td>${typeBadge}</td>
|
||||||
${n.timingSendTime ? '<span class="badge b-timing">定时</span>' : ''}</td>
|
|
||||||
<td>${n.type == 2 ? '<span class="badge b-important">重要</span>' : '<span class="badge b-normal">普通</span>'}</td>
|
|
||||||
<td>${esc(n.publisherName || n.publisherAccount || '-')}</td>
|
<td>${esc(n.publisherName || n.publisherAccount || '-')}</td>
|
||||||
<td style="max-width:160px" title="${escAttr((n.approverNames || []).join('、'))}">${esc((n.approverNames || []).join('、') || '-')}</td>
|
<td style="color:var(--text2)">${progress}</td>
|
||||||
<td style="color:var(--text2);white-space:nowrap">${esc(n.submitTime || '')}</td>
|
<td style="color:var(--text2);white-space:nowrap">${time}</td>
|
||||||
<td class="td-actions">
|
<td class="td-actions">${acts.join('')}</td>
|
||||||
<button class="link-btn" onclick="auditDetail(${n.id})">查看</button>
|
|
||||||
<button class="link-btn success" onclick="approveAction(${n.id},true)">通过</button>
|
|
||||||
<button class="link-btn danger" onclick="approveAction(${n.id},false)">驳回</button>
|
|
||||||
</td>
|
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}
|
}
|
||||||
const actionBadge = n.handledAction === 2
|
const statusBadge = n.status === 1
|
||||||
? '<span class="badge b-published">通过</span>'
|
? '<span class="badge b-published">已发布</span>'
|
||||||
: '<span class="badge b-rejected">驳回</span>';
|
: (n.status === 3 ? '<span class="badge b-rejected">已驳回</span>' : esc(n.statusDesc || '-'));
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td class="td-title" onclick="auditDetail(${n.id})" title="${escAttr(n.title)}">${esc(n.title)}</td>
|
<td class="td-title" onclick="auditDetail(${n.id})" title="${escAttr(n.title)}">${esc(n.title)}</td>
|
||||||
|
<td>${typeBadge}</td>
|
||||||
<td>${esc(n.publisherName || n.publisherAccount || '-')}</td>
|
<td>${esc(n.publisherName || n.publisherAccount || '-')}</td>
|
||||||
<td>${actionBadge}${n.handled ? '' : ''}</td>
|
<td style="color:var(--text2)">${progress}</td>
|
||||||
<td>${esc(n.handledName || n.handledAccount || '-')}${n.handledByMe ? ' <span class="badge b-normal">我</span>' : ''}</td>
|
<td>${statusBadge}</td>
|
||||||
<td style="color:var(--text2);white-space:nowrap">${esc(n.handledTime || '')}</td>
|
<td style="color:var(--text2);white-space:nowrap">${time}</td>
|
||||||
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escAttr(n.handledOpinion || '')}">${esc(n.handledOpinion || '-')}</td>
|
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
renderPager('auditPager', r, auditState.pageIndex, p => { auditState.pageIndex = p; loadApprovals(); });
|
renderPager('auditPager', r, auditState.pageIndex, p => { auditState.pageIndex = p; loadApprovals(); });
|
||||||
|
|||||||
Reference in New Issue
Block a user