diff --git a/fad-hrm/src/main/java/com/ruoyi/hrm/controller/HrmMyApplyController.java b/fad-hrm/src/main/java/com/ruoyi/hrm/controller/HrmMyApplyController.java index f61e362..1ed156c 100644 --- a/fad-hrm/src/main/java/com/ruoyi/hrm/controller/HrmMyApplyController.java +++ b/fad-hrm/src/main/java/com/ruoyi/hrm/controller/HrmMyApplyController.java @@ -1,6 +1,7 @@ package com.ruoyi.hrm.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fasterxml.jackson.databind.ObjectMapper; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.PageQuery; import com.ruoyi.common.core.page.TableDataInfo; @@ -18,8 +19,10 @@ import com.ruoyi.hrm.domain.vo.HrmMyApplyVo; import com.ruoyi.hrm.domain.vo.HrmReimburseReqVo; import com.ruoyi.hrm.domain.vo.HrmSealReqVo; import com.ruoyi.hrm.domain.vo.HrmTravelReqVo; +import com.ruoyi.hrm.domain.HrmFlowInstance; import com.ruoyi.hrm.mapper.HrmAppropriationReqMapper; import com.ruoyi.hrm.mapper.HrmEmployeeMapper; +import com.ruoyi.hrm.mapper.HrmFlowInstanceMapper; import com.ruoyi.hrm.mapper.HrmLeaveReqMapper; import com.ruoyi.hrm.mapper.HrmReimburseReqMapper; import com.ruoyi.hrm.mapper.HrmSealReqMapper; @@ -32,7 +35,9 @@ import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -48,6 +53,8 @@ public class HrmMyApplyController extends BaseController { private final HrmSealReqMapper sealReqMapper; private final HrmReimburseReqMapper reimburseReqMapper; private final HrmAppropriationReqMapper appropriationReqMapper; + private final HrmFlowInstanceMapper flowInstanceMapper; + private final ObjectMapper objectMapper; @GetMapping("/list") public TableDataInfo list(String bizType, String status, String keyword, PageQuery pageQuery) { @@ -80,6 +87,9 @@ public class HrmMyApplyController extends BaseController { all.addAll(mapAppropriation(appropriationReqMapper.selectVoWithProjectList(buildAppropriationBo(emp.getEmpId(), status)), nickName)); } + // 用流程实例状态覆盖业务表状态,避免历史数据状态未同步 + overrideStatusByFlowInstance(all); + if (keyword != null && !keyword.isEmpty()) { String lower = keyword.toLowerCase(); all = all.stream().filter(v -> contains(v, lower)).collect(Collectors.toList()); @@ -93,6 +103,52 @@ public class HrmMyApplyController extends BaseController { return TableDataInfo.build(page); } + private void overrideStatusByFlowInstance(List all) { + if (all == null || all.isEmpty()) return; + Map> bizIdsByType = all.stream() + .filter(v -> v.getBizType() != null && v.getBizId() != null) + .collect(Collectors.groupingBy( + HrmMyApplyVo::getBizType, + Collectors.mapping(HrmMyApplyVo::getBizId, Collectors.toList()) + )); + Map statusByKey = new HashMap<>(); + bizIdsByType.forEach((bizType, bizIds) -> { + if (bizIds.isEmpty()) return; + List insts = flowInstanceMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(HrmFlowInstance::getBizType, bizType) + .in(HrmFlowInstance::getBizId, bizIds) + ); + for (HrmFlowInstance inst : insts) { + if (inst.getStatus() == null) continue; + // 合并多个流程实例:优先终态 (approved/rejected/withdrawn) > 进行中 (running/pending), + // 否则前面遗留的"running"会盖掉新的"approved",导致前端永远显示审批中 + statusByKey.merge(bizType + "_" + inst.getBizId(), inst.getStatus(), + (oldS, newS) -> _statusPriority(newS) > _statusPriority(oldS) ? newS : oldS); + } + }); + for (HrmMyApplyVo v : all) { + String s = statusByKey.get(v.getBizType() + "_" + v.getBizId()); + if (s != null) v.setStatus(s); + } + } + + private int _statusPriority(String s) { + if (s == null) return 0; + switch (s) { + case "approved": + case "done": + return 3; + case "rejected": + case "withdrawn": + return 2; + case "running": + case "pending": + default: + return 1; + } + } + private boolean contains(HrmMyApplyVo v, String lower) { return Objects.toString(v.getTitle(), "").toLowerCase().contains(lower) || Objects.toString(v.getRemark(), "").toLowerCase().contains(lower) @@ -107,13 +163,14 @@ public class HrmMyApplyController extends BaseController { private HrmReimburseReqBo buildReimburseBo(Long empId, String status) { HrmReimburseReqBo bo = new HrmReimburseReqBo(); bo.setEmpId(empId); bo.setStatus(status); return bo; } private HrmAppropriationReqBo buildAppropriationBo(Long empId, String status) { HrmAppropriationReqBo bo = new HrmAppropriationReqBo(); bo.setEmpId(empId); bo.setStatus(status); return bo; } - private List mapLeave(List list, String nickName) { return list.stream().map(v -> toVo("leave", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); } - private List mapTravel(List list, String nickName) { return list.stream().map(v -> toVo("travel", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); } - private List mapSeal(List list, String nickName) { return list.stream().map(v -> toVo("seal", v.getBizId(), v.getEmpId(), nickName, v.getRemark(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); } - private List mapReimburse(List list, String nickName) { return list.stream().map(v -> toVo("reimburse", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); } - private List mapAppropriation(List list, String nickName) { return list.stream().map(v -> toVo("appropriation", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime())).collect(Collectors.toList()); } + private List mapLeave(List list, String nickName) { return list.stream().map(v -> toVo("leave", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime(), v)).collect(Collectors.toList()); } + private List mapTravel(List list, String nickName) { return list.stream().map(v -> toVo("travel", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime(), v)).collect(Collectors.toList()); } + private List mapSeal(List list, String nickName) { return list.stream().map(v -> toVo("seal", v.getBizId(), v.getEmpId(), nickName, v.getPurpose() != null ? v.getPurpose() : v.getRemark(), v.getStatus(), v.getCreateTime(), v)).collect(Collectors.toList()); } + private List mapReimburse(List list, String nickName) { return list.stream().map(v -> toVo("reimburse", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime(), v)).collect(Collectors.toList()); } + private List mapAppropriation(List list, String nickName) { return list.stream().map(v -> toVo("appropriation", v.getBizId(), v.getEmpId(), nickName, v.getReason(), v.getStatus(), v.getCreateTime(), v)).collect(Collectors.toList()); } - private HrmMyApplyVo toVo(String bizType, Long bizId, Long empId, String nickName, String title, String status, java.util.Date createTime) { + @SuppressWarnings("unchecked") + private HrmMyApplyVo toVo(String bizType, Long bizId, Long empId, String nickName, String title, String status, java.util.Date createTime, Object source) { HrmMyApplyVo vo = new HrmMyApplyVo(); vo.setBizType(bizType); vo.setBizId(bizId); @@ -122,6 +179,9 @@ public class HrmMyApplyController extends BaseController { vo.setTitle(title); vo.setStatus(status); vo.setCreateTime(createTime); + if (source != null) { + vo.setBizData(objectMapper.convertValue(source, Map.class)); + } return vo; } } diff --git a/fad-hrm/src/main/java/com/ruoyi/hrm/domain/vo/HrmFlowInstanceVo.java b/fad-hrm/src/main/java/com/ruoyi/hrm/domain/vo/HrmFlowInstanceVo.java index 10825b2..249352b 100644 --- a/fad-hrm/src/main/java/com/ruoyi/hrm/domain/vo/HrmFlowInstanceVo.java +++ b/fad-hrm/src/main/java/com/ruoyi/hrm/domain/vo/HrmFlowInstanceVo.java @@ -6,6 +6,7 @@ import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; import java.util.Date; +import java.util.Map; @Data public class HrmFlowInstanceVo implements Serializable { @@ -41,4 +42,7 @@ public class HrmFlowInstanceVo implements Serializable { private Date createTime; private String updateBy; private Date updateTime; + + /** 业务表回填的数据,用于列表关键信息展示 */ + private Map bizData; } diff --git a/fad-hrm/src/main/java/com/ruoyi/hrm/domain/vo/HrmMyApplyVo.java b/fad-hrm/src/main/java/com/ruoyi/hrm/domain/vo/HrmMyApplyVo.java index 099daa1..ec3241b 100644 --- a/fad-hrm/src/main/java/com/ruoyi/hrm/domain/vo/HrmMyApplyVo.java +++ b/fad-hrm/src/main/java/com/ruoyi/hrm/domain/vo/HrmMyApplyVo.java @@ -4,6 +4,7 @@ import lombok.Data; import java.io.Serializable; import java.util.Date; +import java.util.Map; @Data public class HrmMyApplyVo implements Serializable { @@ -21,4 +22,5 @@ public class HrmMyApplyVo implements Serializable { private Date endTime; private Date actualEndTime; private String remark; + private Map bizData; } diff --git a/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmFlowInstanceServiceImpl.java b/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmFlowInstanceServiceImpl.java index 3bf8f8b..28df45e 100644 --- a/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmFlowInstanceServiceImpl.java +++ b/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmFlowInstanceServiceImpl.java @@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fasterxml.jackson.databind.ObjectMapper; import com.ruoyi.common.core.domain.PageQuery; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.service.UserService; @@ -27,7 +28,9 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @RequiredArgsConstructor @@ -42,6 +45,11 @@ public class HrmFlowInstanceServiceImpl implements IHrmFlowInstanceService { private final UserService userService; private final HrmFlowCcMapper ccMapper; private final HrmTravelReqMapper travelReqMapper; + private final HrmLeaveReqMapper leaveReqMapper; + private final HrmSealReqMapper sealReqMapper; + private final HrmReimburseReqMapper reimburseReqMapper; + private final HrmAppropriationReqMapper appropriationReqMapper; + private final ObjectMapper objectMapper; private final ApplicationEventPublisher eventPublisher; @Override @@ -188,9 +196,55 @@ public class HrmFlowInstanceServiceImpl implements IHrmFlowInstanceService { } } } + fillInstanceBizData(result.getRecords()); return TableDataInfo.build(result); } + @SuppressWarnings("unchecked") + private void fillInstanceBizData(List records) { + if (records == null || records.isEmpty()) return; + Map> bizIdsByType = records.stream() + .filter(v -> v.getBizType() != null && v.getBizId() != null) + .collect(Collectors.groupingBy( + HrmFlowInstanceVo::getBizType, + Collectors.mapping(HrmFlowInstanceVo::getBizId, Collectors.toList()) + )); + Map bizDataMap = new HashMap<>(); + bizIdsByType.forEach((bizType, bizIds) -> { + if (bizIds.isEmpty()) return; + switch (bizType) { + case "leave": + leaveReqMapper.selectBatchIds(bizIds).forEach(d -> + bizDataMap.put("leave_" + d.getBizId(), objectMapper.convertValue(d, Map.class))); + break; + case "travel": + travelReqMapper.selectBatchIds(bizIds).forEach(d -> + bizDataMap.put("travel_" + d.getBizId(), objectMapper.convertValue(d, Map.class))); + break; + case "seal": + sealReqMapper.selectBatchIds(bizIds).forEach(d -> + bizDataMap.put("seal_" + d.getBizId(), objectMapper.convertValue(d, Map.class))); + break; + case "reimburse": + reimburseReqMapper.selectBatchIds(bizIds).forEach(d -> + bizDataMap.put("reimburse_" + d.getBizId(), objectMapper.convertValue(d, Map.class))); + break; + case "appropriation": + appropriationReqMapper.selectBatchIds(bizIds).forEach(d -> + bizDataMap.put("appropriation_" + d.getBizId(), objectMapper.convertValue(d, Map.class))); + break; + default: + break; + } + }); + records.forEach(vo -> { + Object data = bizDataMap.get(vo.getBizType() + "_" + vo.getBizId()); + if (data != null) { + vo.setBizData((Map) data); + } + }); + } + @Override public List queryList(HrmFlowInstanceBo bo) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); diff --git a/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmFlowTaskServiceImpl.java b/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmFlowTaskServiceImpl.java index 9083a74..241a854 100644 --- a/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmFlowTaskServiceImpl.java +++ b/fad-hrm/src/main/java/com/ruoyi/hrm/service/impl/HrmFlowTaskServiceImpl.java @@ -286,6 +286,7 @@ private void fillBizData(List tasks) { sealReqService.stampWithJava(inst.getBizId(), stampBo); } } + bizStatusSyncHelper.setBizApproved(inst.getBizType(), inst.getBizId()); } return true; } diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaAiReviewController.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaAiReviewController.java index b424c0e..1ae8396 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaAiReviewController.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaAiReviewController.java @@ -37,8 +37,9 @@ public class OaAiReviewController extends BaseController { @PostMapping(value = "/analyze", consumes = "multipart/form-data") public R analyze(@RequestParam("file") MultipartFile file, @RequestParam("reviewType") String reviewType, - @RequestParam(value = "position", required = false) String position) { - return R.ok("审核完成", service.analyze(file, reviewType, position)); + @RequestParam(value = "position", required = false) String position, + @RequestParam(value = "requirements", required = false) String requirements) { + return R.ok("审核完成", service.analyze(file, reviewType, position, requirements)); } /** @@ -48,8 +49,9 @@ public class OaAiReviewController extends BaseController { @PostMapping(value = "/analyzeStream", consumes = "multipart/form-data", produces = "text/event-stream;charset=UTF-8") public SseEmitter analyzeStream(@RequestParam("file") MultipartFile file, @RequestParam("reviewType") String reviewType, - @RequestParam(value = "position", required = false) String position) { - return service.analyzeStream(file, reviewType, position); + @RequestParam(value = "position", required = false) String position, + @RequestParam(value = "requirements", required = false) String requirements) { + return service.analyzeStream(file, reviewType, position, requirements); } @GetMapping("/list") @@ -57,7 +59,8 @@ public class OaAiReviewController extends BaseController { return service.queryPageList(bo, pageQuery); } - @GetMapping("/{id}") + // 限定为数字,避免 /analyzeStream 等子路径被 {id} 误匹配(否则 POST 会得到 405) + @GetMapping("/{id:\\d+}") public R getInfo(@NotNull @PathVariable Long id) { return R.ok(service.queryById(id)); } diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaApprovalController.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaApprovalController.java new file mode 100644 index 0000000..e773aba --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/OaApprovalController.java @@ -0,0 +1,98 @@ +package com.ruoyi.oa.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.PageQuery; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.oa.domain.bo.OaApprovalActionBo; +import com.ruoyi.oa.domain.bo.OaApprovalConfigBo; +import com.ruoyi.oa.domain.vo.OaApprovalConfigVo; +import com.ruoyi.oa.domain.vo.OaApprovalInstanceVo; +import com.ruoyi.oa.service.IOaApprovalService; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 通用审批 + */ +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/oa/approval") +public class OaApprovalController extends BaseController { + + private final IOaApprovalService approvalService; + + // ===== 配置 ===== + @GetMapping("/config/list") + public R> configList() { + return R.ok(approvalService.listConfigs()); + } + + @GetMapping("/config/{businessType}") + public R getConfig(@PathVariable String businessType) { + return R.ok(approvalService.getConfig(businessType)); + } + + @PostMapping("/config") + public R saveConfig(@RequestBody OaApprovalConfigBo bo) { + return toAjax(approvalService.saveConfig(bo)); + } + + @DeleteMapping("/config/{id}") + public R deleteConfig(@PathVariable Long id) { + return toAjax(approvalService.deleteConfig(id)); + } + + // ===== 审批 ===== + @PostMapping("/act") + public R act(@Validated @RequestBody OaApprovalActionBo bo) { + return toAjax(approvalService.act(bo)); + } + + @PostMapping("/withdraw/{instanceId}") + public R withdraw(@PathVariable Long instanceId) { + return toAjax(approvalService.withdraw(instanceId)); + } + + // ===== 查询 ===== + @GetMapping("/detail/{instanceId}") + public R detail(@PathVariable Long instanceId) { + return R.ok(approvalService.getDetail(instanceId)); + } + + @GetMapping("/latest") + public R latest(@NotNull @RequestParam String businessType, + @NotNull @RequestParam Long businessId) { + return R.ok(approvalService.getLatestByBusiness(businessType, businessId)); + } + + @GetMapping("/mine/pending") + public TableDataInfo myPending(PageQuery pageQuery, + @RequestParam(required = false) String businessType) { + return approvalService.pageMyPending(pageQuery, businessType); + } + + @GetMapping("/mine/done") + public TableDataInfo myDone(PageQuery pageQuery, + @RequestParam(required = false) String businessType) { + return approvalService.pageMyDone(pageQuery, businessType); + } + + @GetMapping("/mine/submitted") + public TableDataInfo mySubmitted(PageQuery pageQuery, + @RequestParam(required = false) String businessType) { + return approvalService.pageMySubmitted(pageQuery, businessType); + } + + @GetMapping("/list") + public TableDataInfo all(PageQuery pageQuery, + @RequestParam(required = false) String businessType, + @RequestParam(required = false) Integer status) { + return approvalService.pageAll(pageQuery, businessType, status); + } +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaAiReview.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaAiReview.java index a11215b..041ff1f 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaAiReview.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaAiReview.java @@ -35,6 +35,9 @@ public class OaAiReview extends BaseEntity { /** 简历审核的目标岗位 */ private String position; + /** 本次审核的附加要求/审核重点 */ + private String requirements; + /** 简历匹配度评分 0-100(合同为空) */ private Integer matchScore; diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalConfig.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalConfig.java new file mode 100644 index 0000000..d867e70 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalConfig.java @@ -0,0 +1,40 @@ +package com.ruoyi.oa.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 业务审批配置 oa_approval_config + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("oa_approval_config") +public class OaApprovalConfig extends BaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id") + private Long id; + + /** 业务类型 key */ + private String businessType; + + /** 业务名称(展示用) */ + private String businessName; + + /** 审批人 user_id 列表,逗号分隔 */ + private String approverIds; + + /** 1或签 2会签 */ + private Integer signType; + + /** 是否启用:0停用 1启用 */ + private Integer enabled; + + private String remark; + + private Integer delFlag; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalInstance.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalInstance.java new file mode 100644 index 0000000..adfa85d --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalInstance.java @@ -0,0 +1,42 @@ +package com.ruoyi.oa.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 审批单实例 oa_approval_instance + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("oa_approval_instance") +public class OaApprovalInstance extends BaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id") + private Long id; + + private String businessType; + private Long businessId; + private String businessTitle; + + private Long applyUserId; + private String applyUserName; + private Date applyTime; + + /** 快照:提交时的审批人 */ + private String approverIds; + /** 快照:1或签 2会签 */ + private Integer signType; + + /** 0待审 1通过 2驳回 3撤回 */ + private Integer status; + private Date finishTime; + + private Integer delFlag; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalRecord.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalRecord.java new file mode 100644 index 0000000..9ac7cfd --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaApprovalRecord.java @@ -0,0 +1,31 @@ +package com.ruoyi.oa.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 审批操作流水 oa_approval_record + */ +@Data +@TableName("oa_approval_record") +public class OaApprovalRecord implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id") + private Long id; + + private Long instanceId; + private Long approverId; + private String approverName; + + /** 1通过 2驳回 */ + private Integer action; + private String comment; + private Date opTime; + private Date createTime; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaRequirements.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaRequirements.java index 1a0116e..e7c7cd9 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaRequirements.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/OaRequirements.java @@ -45,6 +45,10 @@ public class OaRequirements extends BaseEntity { * 挂接项目 ID,可选 */ private Long projectId; + /** + * 关联物料 ID CSV -> sys_oa_warehouse.id,可选 + */ + private String materialIds; /** * 需求描述 */ diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaApprovalActionBo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaApprovalActionBo.java new file mode 100644 index 0000000..47cf92d --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaApprovalActionBo.java @@ -0,0 +1,21 @@ +package com.ruoyi.oa.domain.bo; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +@Data +public class OaApprovalActionBo implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull + private Long instanceId; + + /** 1通过 2驳回 */ + @NotNull + private Integer action; + + private String comment; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaApprovalConfigBo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaApprovalConfigBo.java new file mode 100644 index 0000000..b33002b --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaApprovalConfigBo.java @@ -0,0 +1,22 @@ +package com.ruoyi.oa.domain.bo; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class OaApprovalConfigBo implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private String businessType; + private String businessName; + /** 审批人 user_id 列表,逗号分隔 */ + private String approverIds; + /** 1或签 2会签 */ + private Integer signType; + /** 0停用 1启用 */ + private Integer enabled; + private String remark; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaRequirementsBo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaRequirementsBo.java index a3f67b8..79771ba 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaRequirementsBo.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/OaRequirementsBo.java @@ -49,6 +49,11 @@ public class OaRequirementsBo extends BaseEntity { */ private Long projectId; + /** + * 关联物料 ID CSV,多个物料用逗号分隔 + */ + private String materialIds; + /** * 需求描述 */ diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaAiReviewVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaAiReviewVo.java index 6c81d32..a47ca42 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaAiReviewVo.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaAiReviewVo.java @@ -20,6 +20,7 @@ public class OaAiReviewVo implements Serializable { private Long ossId; private String fileUrl; private String position; + private String requirements; private Integer matchScore; private String riskLevel; private String summary; diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalConfigVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalConfigVo.java new file mode 100644 index 0000000..2986df6 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalConfigVo.java @@ -0,0 +1,24 @@ +package com.ruoyi.oa.domain.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +public class OaApprovalConfigVo implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private String businessType; + private String businessName; + private String approverIds; + /** 显示用:审批人昵称 列表 */ + private String approverNames; + private Integer signType; + private Integer enabled; + private String remark; + private Date createTime; + private Date updateTime; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalInstanceVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalInstanceVo.java new file mode 100644 index 0000000..0645fff --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalInstanceVo.java @@ -0,0 +1,34 @@ +package com.ruoyi.oa.domain.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +@Data +public class OaApprovalInstanceVo implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private String businessType; + private String businessName; + private Long businessId; + private String businessTitle; + + private Long applyUserId; + private String applyUserName; + private Date applyTime; + + private String approverIds; + private String approverNames; + private Integer signType; + + /** 0待审 1通过 2驳回 3撤回 */ + private Integer status; + private Date finishTime; + + /** 审批流水 */ + private List records; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalRecordVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalRecordVo.java new file mode 100644 index 0000000..da47d44 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaApprovalRecordVo.java @@ -0,0 +1,20 @@ +package com.ruoyi.oa.domain.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +public class OaApprovalRecordVo implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private Long instanceId; + private Long approverId; + private String approverName; + private Integer action; + private String comment; + private Date opTime; +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaRequirementsVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaRequirementsVo.java index 17b1923..96abc16 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaRequirementsVo.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/OaRequirementsVo.java @@ -89,8 +89,29 @@ public class OaRequirementsVo extends BaseEntity { private String ownerNickName; private String projectName; + /** 关联物料 ID CSV */ + private String materialIds; + /** 关联物料明细(service 层 enrich,列表/详情都返回) */ + private java.util.List materials; + + @Data + public static class MaterialItem implements java.io.Serializable { + private Long id; + private String name; + private String model; + private String unit; + private Long inventory; + private String brand; + private String specifications; + } + /** 附件文件列表(已联查 sys_oss,每项形如 "||",逗号分隔) */ private String accessoryFiles; + /** 审批状态:null=未提交审批,0=待审,1=通过,2=驳回,3=撤回 */ + private Integer approvalStatus; + /** 审批单 id(最新一次) */ + private Long approvalInstanceId; + } diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaContractVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaContractVo.java index 264d6f1..5da0d70 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaContractVo.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaContractVo.java @@ -138,4 +138,9 @@ public class SysOaContractVo { @ExcelProperty(value = "备注") private String remark; + /** 审批状态:null=未提交审批,0=待审,1=通过,2=驳回,3=撤回 */ + private Integer approvalStatus; + /** 审批单 id(最新一次) */ + private Long approvalInstanceId; + } diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalConfigMapper.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalConfigMapper.java new file mode 100644 index 0000000..c7f325f --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalConfigMapper.java @@ -0,0 +1,8 @@ +package com.ruoyi.oa.mapper; + +import com.ruoyi.common.core.mapper.BaseMapperPlus; +import com.ruoyi.oa.domain.OaApprovalConfig; +import com.ruoyi.oa.domain.vo.OaApprovalConfigVo; + +public interface OaApprovalConfigMapper extends BaseMapperPlus { +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalInstanceMapper.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalInstanceMapper.java new file mode 100644 index 0000000..41ed321 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalInstanceMapper.java @@ -0,0 +1,8 @@ +package com.ruoyi.oa.mapper; + +import com.ruoyi.common.core.mapper.BaseMapperPlus; +import com.ruoyi.oa.domain.OaApprovalInstance; +import com.ruoyi.oa.domain.vo.OaApprovalInstanceVo; + +public interface OaApprovalInstanceMapper extends BaseMapperPlus { +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalRecordMapper.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalRecordMapper.java new file mode 100644 index 0000000..1496421 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/OaApprovalRecordMapper.java @@ -0,0 +1,8 @@ +package com.ruoyi.oa.mapper; + +import com.ruoyi.common.core.mapper.BaseMapperPlus; +import com.ruoyi.oa.domain.OaApprovalRecord; +import com.ruoyi.oa.domain.vo.OaApprovalRecordVo; + +public interface OaApprovalRecordMapper extends BaseMapperPlus { +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaAiReviewService.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaAiReviewService.java index dbd02bc..f5dae31 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaAiReviewService.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaAiReviewService.java @@ -14,16 +14,17 @@ public interface IOaAiReviewService { /** * 上传合同/简历并进行 AI 审核,落库并返回结果 * - * @param file PDF / Word 文件 - * @param reviewType contract / resume - * @param position 简历审核的目标岗位(合同可空) + * @param file PDF / Word 文件 + * @param reviewType contract / resume + * @param position 简历审核的目标岗位(合同可空) + * @param requirements 附加审核要求/重点(可空) */ - OaAiReviewVo analyze(MultipartFile file, String reviewType, String position); + OaAiReviewVo analyze(MultipartFile file, String reviewType, String position, String requirements); /** * 流式审核:边生成边推送(SSE),结束后落库 */ - SseEmitter analyzeStream(MultipartFile file, String reviewType, String position); + SseEmitter analyzeStream(MultipartFile file, String reviewType, String position, String requirements); TableDataInfo queryPageList(OaAiReviewBo bo, PageQuery pageQuery); diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaApprovalService.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaApprovalService.java new file mode 100644 index 0000000..d8f56f5 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/IOaApprovalService.java @@ -0,0 +1,72 @@ +package com.ruoyi.oa.service; + +import com.ruoyi.common.core.domain.PageQuery; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.oa.domain.bo.OaApprovalActionBo; +import com.ruoyi.oa.domain.bo.OaApprovalConfigBo; +import com.ruoyi.oa.domain.vo.OaApprovalConfigVo; +import com.ruoyi.oa.domain.vo.OaApprovalInstanceVo; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 通用审批 Service + */ +public interface IOaApprovalService { + + // ===== 配置 ===== + List listConfigs(); + + OaApprovalConfigVo getConfig(String businessType); + + Boolean saveConfig(OaApprovalConfigBo bo); + + Boolean deleteConfig(Long id); + + // ===== 业务侧调用 ===== + + /** + * 业务提交触发审批,若已有未结束的实例则直接返回。 + * @return 审批单 id + */ + Long submit(String businessType, Long businessId, String businessTitle); + + /** + * 审批操作(通过/驳回),自动判定或签/会签终结状态。 + */ + Boolean act(OaApprovalActionBo bo); + + /** + * 撤回(仅申请人可在 status=0 时撤回) + */ + Boolean withdraw(Long instanceId); + + /** + * 查询某业务的最新审批单(用于业务详情展示) + */ + OaApprovalInstanceVo getLatestByBusiness(String businessType, Long businessId); + + /** + * 批量查询业务最新审批状态:返回 businessId -> status + */ + Map batchLatestStatus(String businessType, Collection businessIds); + + // ===== 列表 ===== + + /** 我待办(审批人是我,status=0) */ + TableDataInfo pageMyPending(PageQuery pageQuery, String businessType); + + /** 我已办(出现在 record 里) */ + TableDataInfo pageMyDone(PageQuery pageQuery, String businessType); + + /** 我发起的 */ + TableDataInfo pageMySubmitted(PageQuery pageQuery, String businessType); + + /** 全部(管理员) */ + TableDataInfo pageAll(PageQuery pageQuery, String businessType, Integer status); + + /** 单条详情(带流水) */ + OaApprovalInstanceVo getDetail(Long instanceId); +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaAiReviewServiceImpl.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaAiReviewServiceImpl.java index 559cec3..ec508bc 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaAiReviewServiceImpl.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaAiReviewServiceImpl.java @@ -57,8 +57,8 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService { private static final Pattern RISK_PATTERN = Pattern.compile("风险评级[::\\s]*([高中低])"); @Override - public OaAiReviewVo analyze(MultipartFile file, String reviewType, String position) { - Prepared p = prepareSync(file, reviewType, position); + public OaAiReviewVo analyze(MultipartFile file, String reviewType, String position, String requirements) { + Prepared p = prepareSync(file, reviewType, position, requirements); buildPrompt(p); String result = p.images != null ? miMoClient.chatMultimodal(p.system, p.userText, p.images) @@ -70,11 +70,11 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService { } @Override - public SseEmitter analyzeStream(MultipartFile file, String reviewType, String position) { + public SseEmitter analyzeStream(MultipartFile file, String reviewType, String position, String requirements) { // 只在同步阶段做轻量校验 + 读取字节(multipart 必须在请求线程内消费); // 文档解析、渲染、大模型调用全部放到异步线程,任何异常都以 SSE error 事件返回, // 避免在返回 SseEmitter 之前抛异常被全局处理器包成 JSON(前端按流读取会“静默失败”)。 - Prepared p = prepareSync(file, reviewType, position); + Prepared p = prepareSync(file, reviewType, position, requirements); String username = currentUsername(); long timeoutMs = ((miMoProps.getTimeout() == null ? 180 : miMoProps.getTimeout()) + 60) * 1000L; SseEmitter emitter = new SseEmitter(timeoutMs); @@ -129,7 +129,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService { } /** 同步阶段:校验 + 读取字节(不解析,快) */ - private Prepared prepareSync(MultipartFile file, String reviewType, String position) { + private Prepared prepareSync(MultipartFile file, String reviewType, String position, String requirements) { if (file == null || file.isEmpty()) { throw new ServiceException("请上传文件"); } @@ -153,6 +153,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService { Prepared p = new Prepared(); p.reviewType = reviewType; p.position = position; + p.requirements = StringUtils.trimToNull(requirements); p.fileName = fileName; p.bytes = bytes; return p; @@ -160,7 +161,12 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService { /** 解析文档 + 构建提示词(耗时/可能抛异常的部分) */ private void buildPrompt(Prepared p) { - p.system = "contract".equals(p.reviewType) ? contractSystemPrompt() : resumeSystemPrompt(p.position); + String base = "contract".equals(p.reviewType) ? contractSystemPrompt() : resumeSystemPrompt(p.position); + if (StringUtils.isNotBlank(p.requirements)) { + base += "\n\n【本次额外审核要求 / 重点】请在上述固定结构基础上,务必额外重点关注并逐条回应以下要求:\n" + + p.requirements; + } + p.system = base; String text = truncate(DocumentParseUtil.extractText(p.fileName, p.bytes)); if (StringUtils.length(text) >= MIN_TEXT_LEN) { p.userText = "contract".equals(p.reviewType) @@ -189,6 +195,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService { entity.setFileUrl(oss.getUrl()); } entity.setPosition(p.position); + entity.setRequirements(p.requirements); entity.setResultMd(result); entity.setSummary(buildSummary(result)); entity.setModel(miMoProps.getModel()); @@ -242,6 +249,7 @@ public class OaAiReviewServiceImpl implements IOaAiReviewService { private static class Prepared { String reviewType; String position; + String requirements; String fileName; byte[] bytes; String system; diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaApprovalServiceImpl.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaApprovalServiceImpl.java new file mode 100644 index 0000000..614e5cc --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaApprovalServiceImpl.java @@ -0,0 +1,338 @@ +package com.ruoyi.oa.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.ruoyi.common.core.domain.PageQuery; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.helper.LoginHelper; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.oa.domain.OaApprovalConfig; +import com.ruoyi.oa.domain.OaApprovalInstance; +import com.ruoyi.oa.domain.OaApprovalRecord; +import com.ruoyi.oa.domain.bo.OaApprovalActionBo; +import com.ruoyi.oa.domain.bo.OaApprovalConfigBo; +import com.ruoyi.oa.domain.vo.OaApprovalConfigVo; +import com.ruoyi.oa.domain.vo.OaApprovalInstanceVo; +import com.ruoyi.oa.domain.vo.OaApprovalRecordVo; +import com.ruoyi.oa.mapper.OaApprovalConfigMapper; +import com.ruoyi.oa.mapper.OaApprovalInstanceMapper; +import com.ruoyi.oa.mapper.OaApprovalRecordMapper; +import com.ruoyi.oa.service.IOaApprovalService; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.system.service.ISysUserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class OaApprovalServiceImpl implements IOaApprovalService { + + private final OaApprovalConfigMapper configMapper; + private final OaApprovalInstanceMapper instanceMapper; + private final OaApprovalRecordMapper recordMapper; + private final ISysUserService userService; + + // ===================== 配置 ===================== + + @Override + public List listConfigs() { + List list = configMapper.selectVoList( + Wrappers.lambdaQuery().orderByAsc(OaApprovalConfig::getBusinessType)); + list.forEach(this::fillApproverNames); + return list; + } + + @Override + public OaApprovalConfigVo getConfig(String businessType) { + OaApprovalConfig cfg = configMapper.selectOne( + Wrappers.lambdaQuery().eq(OaApprovalConfig::getBusinessType, businessType)); + if (cfg == null) return null; + OaApprovalConfigVo vo = BeanUtil.toBean(cfg, OaApprovalConfigVo.class); + fillApproverNames(vo); + return vo; + } + + @Override + public Boolean saveConfig(OaApprovalConfigBo bo) { + if (StringUtils.isBlank(bo.getBusinessType())) { + throw new ServiceException("业务类型不能为空"); + } + if (StringUtils.isBlank(bo.getApproverIds())) { + throw new ServiceException("审批人不能为空"); + } + if (bo.getSignType() == null) bo.setSignType(1); + if (bo.getEnabled() == null) bo.setEnabled(1); + + OaApprovalConfig exist = configMapper.selectOne( + Wrappers.lambdaQuery().eq(OaApprovalConfig::getBusinessType, bo.getBusinessType())); + OaApprovalConfig entity = BeanUtil.toBean(bo, OaApprovalConfig.class); + if (exist != null) { + entity.setId(exist.getId()); + return configMapper.updateById(entity) > 0; + } + return configMapper.insert(entity) > 0; + } + + @Override + public Boolean deleteConfig(Long id) { + return configMapper.deleteById(id) > 0; + } + + // ===================== 业务侧 ===================== + + @Override + @Transactional(rollbackFor = Exception.class) + public Long submit(String businessType, Long businessId, String businessTitle) { + OaApprovalConfig cfg = configMapper.selectOne( + Wrappers.lambdaQuery().eq(OaApprovalConfig::getBusinessType, businessType)); + if (cfg == null || cfg.getEnabled() != null && cfg.getEnabled() == 0) { + // 未配置 / 未启用:跳过审批,不报错 + return null; + } + if (StringUtils.isBlank(cfg.getApproverIds())) { + throw new ServiceException("业务【" + businessType + "】未配置审批人"); + } + + // 若已存在未结束实例则复用 + OaApprovalInstance pending = instanceMapper.selectOne( + Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getBusinessType, businessType) + .eq(OaApprovalInstance::getBusinessId, businessId) + .eq(OaApprovalInstance::getStatus, 0) + .last("LIMIT 1")); + if (pending != null) return pending.getId(); + + OaApprovalInstance inst = new OaApprovalInstance(); + inst.setBusinessType(businessType); + inst.setBusinessId(businessId); + inst.setBusinessTitle(businessTitle); + inst.setApplyUserId(LoginHelper.getUserId()); + inst.setApplyUserName(LoginHelper.getNickName()); + inst.setApplyTime(new Date()); + inst.setApproverIds(cfg.getApproverIds()); + inst.setSignType(cfg.getSignType()); + inst.setStatus(0); + instanceMapper.insert(inst); + return inst.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean act(OaApprovalActionBo bo) { + OaApprovalInstance inst = instanceMapper.selectById(bo.getInstanceId()); + if (inst == null) throw new ServiceException("审批单不存在"); + if (inst.getStatus() == null || inst.getStatus() != 0) throw new ServiceException("审批已终结"); + + Long me = LoginHelper.getUserId(); + Set approvers = parseIds(inst.getApproverIds()); + if (!approvers.contains(me)) throw new ServiceException("无审批权限"); + + // 防重:同一人对同一单只能操作一次 + Long already = recordMapper.selectCount( + Wrappers.lambdaQuery() + .eq(OaApprovalRecord::getInstanceId, inst.getId()) + .eq(OaApprovalRecord::getApproverId, me)); + if (already != null && already > 0) throw new ServiceException("您已审批过"); + + OaApprovalRecord rec = new OaApprovalRecord(); + rec.setInstanceId(inst.getId()); + rec.setApproverId(me); + rec.setApproverName(LoginHelper.getNickName()); + rec.setAction(bo.getAction()); + rec.setComment(bo.getComment()); + rec.setOpTime(new Date()); + recordMapper.insert(rec); + + // 终结判定 + int finalStatus = decideFinalStatus(inst, bo.getAction(), me, approvers); + if (finalStatus != 0) { + inst.setStatus(finalStatus); + inst.setFinishTime(new Date()); + instanceMapper.updateById(inst); + } + return true; + } + + private int decideFinalStatus(OaApprovalInstance inst, Integer action, Long me, Set approvers) { + // 驳回:无论或签会签都立即驳回 + if (action != null && action == 2) return 2; + // 通过场景 + if (inst.getSignType() != null && inst.getSignType() == 1) { + // 或签:任一人通过即通过 + return 1; + } + // 会签:所有人都通过才通过 + Long passCount = recordMapper.selectCount( + Wrappers.lambdaQuery() + .eq(OaApprovalRecord::getInstanceId, inst.getId()) + .eq(OaApprovalRecord::getAction, 1)); + return passCount != null && passCount >= approvers.size() ? 1 : 0; + } + + @Override + public Boolean withdraw(Long instanceId) { + OaApprovalInstance inst = instanceMapper.selectById(instanceId); + if (inst == null) throw new ServiceException("审批单不存在"); + if (inst.getStatus() == null || inst.getStatus() != 0) throw new ServiceException("已终结,无法撤回"); + if (!Objects.equals(inst.getApplyUserId(), LoginHelper.getUserId())) throw new ServiceException("仅申请人可撤回"); + inst.setStatus(3); + inst.setFinishTime(new Date()); + return instanceMapper.updateById(inst) > 0; + } + + @Override + public OaApprovalInstanceVo getLatestByBusiness(String businessType, Long businessId) { + OaApprovalInstance inst = instanceMapper.selectOne( + Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getBusinessType, businessType) + .eq(OaApprovalInstance::getBusinessId, businessId) + .orderByDesc(OaApprovalInstance::getId) + .last("LIMIT 1")); + if (inst == null) return null; + OaApprovalInstanceVo vo = toInstanceVo(inst); + vo.setRecords(loadRecords(inst.getId())); + return vo; + } + + @Override + public Map batchLatestStatus(String businessType, Collection businessIds) { + if (businessIds == null || businessIds.isEmpty()) return Collections.emptyMap(); + List list = instanceMapper.selectList( + Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getBusinessType, businessType) + .in(OaApprovalInstance::getBusinessId, businessIds) + .orderByAsc(OaApprovalInstance::getId)); + // 后面的覆盖前面的,得到最新 + Map result = new HashMap<>(); + for (OaApprovalInstance i : list) { + result.put(i.getBusinessId(), i.getStatus()); + } + return result; + } + + // ===================== 列表 ===================== + + @Override + public TableDataInfo pageMyPending(PageQuery pageQuery, String businessType) { + Long me = LoginHelper.getUserId(); + LambdaQueryWrapper qw = Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getStatus, 0) + .and(w -> w.like(OaApprovalInstance::getApproverIds, "," + me + ",") + .or().likeRight(OaApprovalInstance::getApproverIds, me + ",") + .or().likeLeft(OaApprovalInstance::getApproverIds, "," + me) + .or().eq(OaApprovalInstance::getApproverIds, String.valueOf(me))) + .eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType) + .orderByDesc(OaApprovalInstance::getId); + Page page = instanceMapper.selectPage(pageQuery.build(), qw); + return buildInstancePage(page); + } + + @Override + public TableDataInfo pageMyDone(PageQuery pageQuery, String businessType) { + Long me = LoginHelper.getUserId(); + List instIds = recordMapper.selectObjs( + Wrappers.lambdaQuery() + .select(OaApprovalRecord::getInstanceId) + .eq(OaApprovalRecord::getApproverId, me)) + .stream().filter(Objects::nonNull).map(o -> Long.parseLong(o.toString())).distinct().collect(Collectors.toList()); + if (instIds.isEmpty()) return TableDataInfo.build(new ArrayList<>()); + LambdaQueryWrapper qw = Wrappers.lambdaQuery() + .in(OaApprovalInstance::getId, instIds) + .eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType) + .orderByDesc(OaApprovalInstance::getId); + Page page = instanceMapper.selectPage(pageQuery.build(), qw); + return buildInstancePage(page); + } + + @Override + public TableDataInfo pageMySubmitted(PageQuery pageQuery, String businessType) { + Long me = LoginHelper.getUserId(); + LambdaQueryWrapper qw = Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getApplyUserId, me) + .eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType) + .orderByDesc(OaApprovalInstance::getId); + Page page = instanceMapper.selectPage(pageQuery.build(), qw); + return buildInstancePage(page); + } + + @Override + public TableDataInfo pageAll(PageQuery pageQuery, String businessType, Integer status) { + LambdaQueryWrapper qw = Wrappers.lambdaQuery() + .eq(StringUtils.isNotBlank(businessType), OaApprovalInstance::getBusinessType, businessType) + .eq(status != null, OaApprovalInstance::getStatus, status) + .orderByDesc(OaApprovalInstance::getId); + Page page = instanceMapper.selectPage(pageQuery.build(), qw); + return buildInstancePage(page); + } + + @Override + public OaApprovalInstanceVo getDetail(Long instanceId) { + OaApprovalInstance inst = instanceMapper.selectById(instanceId); + if (inst == null) return null; + OaApprovalInstanceVo vo = toInstanceVo(inst); + vo.setRecords(loadRecords(instanceId)); + return vo; + } + + // ===================== 内部 ===================== + + private TableDataInfo buildInstancePage(Page page) { + List vos = page.getRecords().stream().map(this::toInstanceVo).collect(Collectors.toList()); + Page p = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); + p.setRecords(vos); + return TableDataInfo.build(p); + } + + private OaApprovalInstanceVo toInstanceVo(OaApprovalInstance inst) { + OaApprovalInstanceVo vo = BeanUtil.toBean(inst, OaApprovalInstanceVo.class); + vo.setApproverNames(resolveNames(inst.getApproverIds())); + OaApprovalConfig cfg = configMapper.selectOne( + Wrappers.lambdaQuery().eq(OaApprovalConfig::getBusinessType, inst.getBusinessType())); + if (cfg != null) vo.setBusinessName(cfg.getBusinessName()); + return vo; + } + + private List loadRecords(Long instanceId) { + return recordMapper.selectVoList( + Wrappers.lambdaQuery() + .eq(OaApprovalRecord::getInstanceId, instanceId) + .orderByAsc(OaApprovalRecord::getOpTime)); + } + + private void fillApproverNames(OaApprovalConfigVo vo) { + vo.setApproverNames(resolveNames(vo.getApproverIds())); + } + + private String resolveNames(String csvIds) { + Set ids = parseIds(csvIds); + if (ids.isEmpty()) return ""; + List names = new ArrayList<>(); + for (Long id : ids) { + try { + SysUser u = userService.selectUserById(id); + names.add(u != null ? u.getNickName() : String.valueOf(id)); + } catch (Exception e) { + names.add(String.valueOf(id)); + } + } + return String.join(",", names); + } + + private Set parseIds(String csv) { + if (StringUtils.isBlank(csv)) return Collections.emptySet(); + Set set = new LinkedHashSet<>(); + for (String s : csv.split(",")) { + String t = s.trim(); + if (t.isEmpty()) continue; + try { set.add(Long.parseLong(t)); } catch (NumberFormatException ignored) {} + } + return set; + } +} diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaRequirementsServiceImpl.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaRequirementsServiceImpl.java index 8c6a631..8455c21 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaRequirementsServiceImpl.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/OaRequirementsServiceImpl.java @@ -14,14 +14,25 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import com.ruoyi.oa.domain.bo.OaRequirementsBo; import com.ruoyi.oa.domain.vo.OaRequirementsVo; +import com.ruoyi.oa.domain.OaApprovalInstance; import com.ruoyi.oa.domain.OaRequirements; +import com.ruoyi.oa.domain.SysOaWarehouse; +import com.ruoyi.oa.mapper.OaApprovalInstanceMapper; import com.ruoyi.oa.mapper.OaRequirementsMapper; +import com.ruoyi.oa.mapper.SysOaWarehouseMapper; +import com.ruoyi.oa.service.IOaApprovalService; import com.ruoyi.oa.service.IOaRequirementsService; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.Collection; +import java.util.stream.Collectors; /** * OA 需求Service业务层处理 @@ -33,16 +44,97 @@ import java.util.Collection; @Service public class OaRequirementsServiceImpl implements IOaRequirementsService { + public static final String BIZ_TYPE = "purchase_req"; + private final OaRequirementsMapper baseMapper; private final OaWarehouseAuditService auditService; + private final SysOaWarehouseMapper warehouseMapper; + + private final IOaApprovalService approvalService; + + private final OaApprovalInstanceMapper approvalInstanceMapper; + /** * 查询OA 需求 */ @Override public OaRequirementsVo queryById(Long requirementId){ - return baseMapper.selectVoById(requirementId); + OaRequirementsVo vo = baseMapper.selectVoById(requirementId); + enrichMaterials(Collections.singletonList(vo)); + enrichApproval(Collections.singletonList(vo)); + return vo; + } + + /** 给 VO 列表注入审批状态(最新一条实例) */ + private void enrichApproval(List list) { + if (list == null || list.isEmpty()) return; + List ids = list.stream().filter(v -> v != null && v.getRequirementId() != null) + .map(OaRequirementsVo::getRequirementId).collect(Collectors.toList()); + if (ids.isEmpty()) return; + List insts = approvalInstanceMapper.selectList( + Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getBusinessType, BIZ_TYPE) + .in(OaApprovalInstance::getBusinessId, ids) + .orderByAsc(OaApprovalInstance::getId)); + Map latest = new HashMap<>(); + for (OaApprovalInstance i : insts) latest.put(i.getBusinessId(), i); + for (OaRequirementsVo v : list) { + if (v == null) continue; + OaApprovalInstance i = latest.get(v.getRequirementId()); + if (i != null) { + v.setApprovalStatus(i.getStatus()); + v.setApprovalInstanceId(i.getId()); + } + } + } + + /** 根据 material_ids(CSV)批量补全物料明细到 VO.materials */ + private void enrichMaterials(List list) { + if (list == null || list.isEmpty()) return; + Set ids = new HashSet<>(); + for (OaRequirementsVo v : list) { + if (v == null) continue; + for (Long id : parseCsv(v.getMaterialIds())) ids.add(id); + } + if (ids.isEmpty()) return; + List wList = warehouseMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper() + .in("id", ids)); + Map wMap = wList.stream() + .collect(Collectors.toMap(SysOaWarehouse::getId, w -> w, (a, b) -> a)); + for (OaRequirementsVo v : list) { + if (v == null) continue; + List mids = parseCsv(v.getMaterialIds()); + if (mids.isEmpty()) continue; + List items = new ArrayList<>(); + for (Long id : mids) { + SysOaWarehouse w = wMap.get(id); + if (w == null) continue; + OaRequirementsVo.MaterialItem it = new OaRequirementsVo.MaterialItem(); + it.setId(w.getId()); + it.setName(w.getName()); + it.setModel(w.getModel()); + it.setUnit(w.getUnit()); + it.setInventory(w.getInventory()); + it.setBrand(w.getBrand()); + it.setSpecifications(w.getSpecifications()); + items.add(it); + } + v.setMaterials(items); + } + } + + private List parseCsv(String csv) { + if (csv == null || csv.isEmpty()) return Collections.emptyList(); + List r = new ArrayList<>(); + for (String s : csv.split(",")) { + String t = s.trim(); + if (t.isEmpty()) continue; + try { r.add(Long.parseLong(t)); } catch (NumberFormatException ignored) {} + } + return r; } /** @@ -52,6 +144,8 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService { public TableDataInfo queryPageList(OaRequirementsBo bo, PageQuery pageQuery) { QueryWrapper lqw = buildQueryWrapper(bo); Page result = baseMapper.selectVoListPage(pageQuery.build(), lqw); + enrichMaterials(result.getRecords()); + enrichApproval(result.getRecords()); return TableDataInfo.build(result); } @@ -61,7 +155,10 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService { @Override public List queryList(OaRequirementsBo bo) { QueryWrapper lqw = buildQueryWrapper(bo); - return baseMapper.selectVoListForExport(lqw); + List list = baseMapper.selectVoListForExport(lqw); + enrichMaterials(list); + enrichApproval(list); + return list; } private QueryWrapper buildQueryWrapper(OaRequirementsBo bo) { @@ -101,6 +198,8 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService { bo.setRequirementId(add.getRequirementId()); auditService.log(OpType.REQ_CREATE, OpType.REF_REQUIREMENT, add.getRequirementId(), "新建采购需求:" + add.getTitle()); + // 触发审批流 + approvalService.submit(BIZ_TYPE, add.getRequirementId(), add.getTitle()); } return flag; } @@ -112,6 +211,18 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService { public Boolean updateByBo(OaRequirementsBo bo) { OaRequirements update = BeanUtil.toBean(bo, OaRequirements.class); validEntityBeforeSave(update); + // 推进到采购中/完成 前需审批通过; + // 若审批未启用/未配置 (无实例),则放行,避免「停用审批反而卡住业务」。 + if (bo.getStatus() != null && (bo.getStatus() == 1 || bo.getStatus() == 2)) { + OaApprovalInstance i = approvalInstanceMapper.selectOne( + Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getBusinessType, BIZ_TYPE) + .eq(OaApprovalInstance::getBusinessId, bo.getRequirementId()) + .orderByDesc(OaApprovalInstance::getId).last("LIMIT 1")); + if (i != null && (i.getStatus() == null || i.getStatus() != 1)) { + throw new com.ruoyi.common.exception.ServiceException("该采购需求尚未审批通过,无法推进状态"); + } + } boolean ok = baseMapper.updateById(update) > 0; if (ok) { String op = OpType.REQ_UPDATE; diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaContractServiceImpl.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaContractServiceImpl.java index a412e5f..ab93950 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaContractServiceImpl.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaContractServiceImpl.java @@ -16,12 +16,19 @@ import org.springframework.stereotype.Service; import com.ruoyi.oa.domain.bo.SysOaContractBo; import com.ruoyi.oa.domain.vo.SysOaContractVo; import com.ruoyi.oa.domain.SysOaContract; +import com.ruoyi.oa.domain.OaApprovalInstance; import com.ruoyi.oa.mapper.SysOaContractMapper; +import com.ruoyi.oa.mapper.OaApprovalInstanceMapper; import com.ruoyi.oa.service.ISysOaContractService; +import com.ruoyi.oa.service.IOaApprovalService; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Collection; +import java.util.stream.Collectors; /** * 合同管理Service业务层处理 @@ -33,16 +40,26 @@ import java.util.Collection; @Service public class SysOaContractServiceImpl implements ISysOaContractService { + public static final String BIZ_TYPE = "contract"; + @Autowired private SysOaContractMapper contractMapper; + @Autowired + private IOaApprovalService approvalService; + + @Autowired + private OaApprovalInstanceMapper approvalInstanceMapper; + /** * 查询合同管理 */ @Override public SysOaContractVo queryById(Long contractId){ - return contractMapper.selectVoById(contractId); + SysOaContractVo vo = contractMapper.selectVoById(contractId); + enrichApproval(Collections.singletonList(vo)); + return vo; } /** @@ -52,6 +69,7 @@ public class SysOaContractServiceImpl implements ISysOaContractService { public TableDataInfo queryPageList(SysOaContractBo bo, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); Page result = contractMapper.selectVoPage(pageQuery.build(), lqw); + enrichApproval(result.getRecords()); return TableDataInfo.build(result); } @@ -61,7 +79,32 @@ public class SysOaContractServiceImpl implements ISysOaContractService { @Override public List queryList(SysOaContractBo bo) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); - return contractMapper.selectVoList(lqw); + List list = contractMapper.selectVoList(lqw); + enrichApproval(list); + return list; + } + + /** 给 VO 列表注入审批状态(最新一条实例) */ + private void enrichApproval(List list) { + if (list == null || list.isEmpty()) return; + List ids = list.stream().filter(v -> v != null && v.getContractId() != null) + .map(SysOaContractVo::getContractId).collect(Collectors.toList()); + if (ids.isEmpty()) return; + List insts = approvalInstanceMapper.selectList( + Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getBusinessType, BIZ_TYPE) + .in(OaApprovalInstance::getBusinessId, ids) + .orderByAsc(OaApprovalInstance::getId)); + Map latest = new HashMap<>(); + for (OaApprovalInstance i : insts) latest.put(i.getBusinessId(), i); + for (SysOaContractVo v : list) { + if (v == null) continue; + OaApprovalInstance i = latest.get(v.getContractId()); + if (i != null) { + v.setApprovalStatus(i.getStatus()); + v.setApprovalInstanceId(i.getId()); + } + } } /** @@ -119,6 +162,8 @@ public class SysOaContractServiceImpl implements ISysOaContractService { boolean flag = contractMapper.insert(add) > 0; if (flag) { bo.setContractId(add.getContractId()); + // 触发审批流(依赖 oa_approval_config 里 contract 配置;未配置/未启用会跳过) + approvalService.submit(BIZ_TYPE, add.getContractId(), add.getContractName()); } return flag; } @@ -146,6 +191,17 @@ public class SysOaContractServiceImpl implements ISysOaContractService { public Boolean updateByBo(SysOaContractBo bo) { SysOaContract update = BeanUtil.toBean(bo, SysOaContract.class); validEntityBeforeSave(update); + // 标记「完结」(1) 前必须审批通过;若审批未启用/未配置 (无实例),放行。 + if (bo.getContractStatus() != null && "1".equals(bo.getContractStatus())) { + OaApprovalInstance i = approvalInstanceMapper.selectOne( + Wrappers.lambdaQuery() + .eq(OaApprovalInstance::getBusinessType, BIZ_TYPE) + .eq(OaApprovalInstance::getBusinessId, bo.getContractId()) + .orderByDesc(OaApprovalInstance::getId).last("LIMIT 1")); + if (i != null && (i.getStatus() == null || i.getStatus() != 1)) { + throw new com.ruoyi.common.exception.ServiceException("该合同尚未审批通过,无法标记为完结"); + } + } return contractMapper.updateById(update) > 0; } diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaWarehouseServiceImpl.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaWarehouseServiceImpl.java index baab89b..e52a354 100644 --- a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaWarehouseServiceImpl.java +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaWarehouseServiceImpl.java @@ -68,6 +68,8 @@ public class SysOaWarehouseServiceImpl implements ISysOaWarehouseService { .like("sow.brand", bo.getName()) .or() .like("sow.model", bo.getName()) + .or() + .like("sow.specifications", bo.getName()) ) .eq(StringUtils.isNotBlank(bo.getModel()), "sow.model", bo.getModel()) .eq(StringUtils.isNotBlank(bo.getBrand()), "sow.brand", bo.getBrand()); diff --git a/ruoyi-ui/src/api/oa/approval.js b/ruoyi-ui/src/api/oa/approval.js new file mode 100644 index 0000000..fcf5d0d --- /dev/null +++ b/ruoyi-ui/src/api/oa/approval.js @@ -0,0 +1,43 @@ +import request from '@/utils/request' + +// ===== 审批配置 ===== +export function listApprovalConfig() { + return request({ url: '/oa/approval/config/list', method: 'get' }) +} +export function getApprovalConfig(businessType) { + return request({ url: '/oa/approval/config/' + businessType, method: 'get' }) +} +export function saveApprovalConfig(data) { + return request({ url: '/oa/approval/config', method: 'post', data }) +} +export function delApprovalConfig(id) { + return request({ url: '/oa/approval/config/' + id, method: 'delete' }) +} + +// ===== 我的审批 ===== +export function listMyPending(query) { + return request({ url: '/oa/approval/mine/pending', method: 'get', params: query }) +} +export function listMyDone(query) { + return request({ url: '/oa/approval/mine/done', method: 'get', params: query }) +} +export function listMySubmitted(query) { + return request({ url: '/oa/approval/mine/submitted', method: 'get', params: query }) +} +export function listAllApproval(query) { + return request({ url: '/oa/approval/list', method: 'get', params: query }) +} + +// ===== 操作 ===== +export function actApproval(data) { + return request({ url: '/oa/approval/act', method: 'post', data }) +} +export function withdrawApproval(instanceId) { + return request({ url: '/oa/approval/withdraw/' + instanceId, method: 'post' }) +} +export function getApprovalDetail(instanceId) { + return request({ url: '/oa/approval/detail/' + instanceId, method: 'get' }) +} +export function getLatestApproval(businessType, businessId) { + return request({ url: '/oa/approval/latest', method: 'get', params: { businessType, businessId } }) +} diff --git a/ruoyi-ui/src/layout/components/Navbar.vue b/ruoyi-ui/src/layout/components/Navbar.vue index 037417d..a09f5a9 100644 --- a/ruoyi-ui/src/layout/components/Navbar.vue +++ b/ruoyi-ui/src/layout/components/Navbar.vue @@ -3,8 +3,8 @@ - +