This commit is contained in:
李旭明 committed 2025-11-25 15:16:56 +08:00
1 parent aca0a8636a
commit 03c80657ac
23 files changed
+2236 -141

No files matched your search

@@ -0,0 +1,62 @@
package org.dromara.stgl.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 年审配置类
*
* @author Cursor AI
* @date 2025-01-15
*/
@Data
@Component
@ConfigurationProperties(prefix = "stgl.club.annual-review")
@PropertySource("classpath:application-stgl.properties")
public class AnnualReviewConfig {
/**
* 项目上线时间(字符串格式:yyyy-MM-dd HH:mm:ss)
*/
private String projectLaunchDate = "2025-01-01 00:00:00";
/**
* 历史数据宽限期(年)
*/
private int gracePeriodYears = 1;
/**
* 年审窗口期天数
*/
private int windowDays = 15;
/**
* 获取项目上线时间的Date对象
*
* @return 项目上线时间
*/
public Date getProjectLaunchDateTime() {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(projectLaunchDate);
} catch (ParseException e) {
throw new RuntimeException("项目上线时间格式错误: " + projectLaunchDate, e);
}
}
/**
* 是否为历史社团
*
* @param establishDate 社团成立时间
* @return true-历史社团,false-新成立社团
*/
public boolean isHistoricalClub(Date establishDate) {
return establishDate.before(getProjectLaunchDateTime());
}
}
@@ -0,0 +1,111 @@
package org.dromara.stgl.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.web.core.BaseController;
import org.dromara.stgl.domain.StglActivityAudit;
import org.dromara.stgl.domain.bo.StglActivityApprovalBo;
import org.dromara.stgl.domain.vo.StglActivityAuditVo;
import org.dromara.stgl.service.IStglActivityAuditService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
/**
* 活动审批记录
*
* @author ruoyi
* @date 2025-01-09
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/stgl/activityAudit")
public class StglActivityAuditController extends BaseController {
private final IStglActivityAuditService stglActivityAuditService;
/**
* 查询活动审批记录详细
*/
@SaCheckPermission("stgl:activity:query")
@GetMapping("/{id}")
public R<StglActivityAudit> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(stglActivityAuditService.queryById(id));
}
/**
* 根据活动ID查询审批记录
*/
@SaCheckPermission("stgl:activity:query")
@GetMapping("/byActivity/{activityId}")
public R<List<StglActivityAuditVo>> getByActivityId(@NotNull(message = "活动ID不能为空")
@PathVariable Long activityId) {
return R.ok(stglActivityAuditService.queryByActivityId(activityId));
}
/**
* 根据审批人ID查询待审批记录
*/
@SaCheckPermission("stgl:activity:query")
@GetMapping("/pending/{approverId}")
public R<List<StglActivityAudit>> getPendingByApproverId(@NotNull(message = "审批人ID不能为空")
@PathVariable Long approverId) {
return R.ok(stglActivityAuditService.queryPendingByApproverId(approverId));
}
/**
* 新增活动审批记录
*/
@SaCheckPermission("stgl:activity:add")
@Log(title = "活动审批记录", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody StglActivityAudit stglActivityAudit) {
return toAjax(stglActivityAuditService.insertByBo(stglActivityAudit));
}
/**
* 修改活动审批记录
*/
@SaCheckPermission("stgl:activity:edit")
@Log(title = "活动审批记录", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody StglActivityAudit stglActivityAudit) {
return toAjax(stglActivityAuditService.updateByBo(stglActivityAudit));
}
/**
* 删除活动审批记录
*/
@SaCheckPermission("stgl:activity:remove")
@Log(title = "活动审批记录", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(stglActivityAuditService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 审批活动
*/
@SaCheckPermission("stgl:activity:approve")
@Log(title = "活动审批", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/approve")
public R<Void> approve(@Validated @RequestBody StglActivityApprovalBo approvalBo) {
return toAjax(stglActivityAuditService.approveActivity(approvalBo));
}
}
@@ -156,4 +156,8 @@ public class StglActivityController extends BaseController {
return R.ok(stglActivityService.getActivityRatings(id));
}
// 注意:提交审批和撤回审批功能已改为通过修改接口(edit)更新活动状态实现
// 前端调用 updateStglActivity 修改状态为8(已提交)或0(草稿)
// 后端在 updateByBo 方法中监听状态变化,自动创建或删除审批记录
}
@@ -3,6 +3,7 @@ package org.dromara.stgl.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
@@ -32,6 +33,13 @@ public class StglActivity extends TenantEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic
@TableField("deleted")
private Integer deleted;
/**
* 活动名称
*/
@@ -107,10 +115,10 @@ public class StglActivity extends TenantEntity {
private String relatedDeptNames;
/**
* 关联社团ID
* 关联社团ID(String类型以避免雪花ID精度丢失)
*/
@TableField("related_club_id")
private Long relatedClubId;
private String relatedClubId;
/**
* 关联类型(1-社团 2-志愿者队伍)
@@ -0,0 +1,91 @@
package org.dromara.stgl.domain;
import org.dromara.common.tenant.core.TenantEntity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.math.BigDecimal;
import java.util.Date;
/**
* 活动审批记录对象 stgl_activity_audit
*
* @author ruoyi
* @date 2025-01-09
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("stgl_activity_audit")
public class StglActivityAudit extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic
private Integer deleted;
/**
* 活动ID
*/
private Long activityId;
/**
* 节点代码(advisor_approve/dept_approve/school_approve)
*/
private String nodeCode;
/**
* 节点名称(指导老师审批/部门审批/校级审批)
*/
private String nodeName;
/**
* 节点顺序(1.1,1.2,2.1,2.2等)
*/
private BigDecimal nodeOrder;
/**
* 节点状态(pending/waiting/approved)
*/
@TableField("audit_status")
private String nodeStatus;
/**
* 审批人ID
*/
private Long approverId;
/**
* 审批人姓名
*/
private String approverName;
/**
* 审批结果(通过/不通过)
*/
private String approveResult;
/**
* 审批意见
*/
@TableField("audit_opinion")
private String approveOpinion;
/**
* 审批时间
*/
@TableField("audit_time")
private Date approveTime;
}
@@ -0,0 +1,61 @@
package org.dromara.stgl.domain.bo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 活动审批请求业务对象
*
* @author ruoyi
* @date 2025-01-09
*/
@Data
public class StglActivityApprovalBo {
/**
* 审批记录ID
*/
@NotNull(message = "审批记录ID不能为空")
private Long auditId;
/**
* 审批结果
*/
@NotBlank(message = "审批结果不能为空")
private String approveResult;
/**
* 审批意见
*/
private String approveOpinion;
/**
* 下一步审核人列表
*/
private List<NextAuditor> nextAuditors;
/**
* 下一步审核人信息
*/
@Data
public static class NextAuditor {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
}
}
@@ -97,9 +97,9 @@ public class StglActivityBo extends BaseEntity {
private String relatedDeptNames;
/**
* 关联社团ID
* 关联社团ID(String类型以避免雪花ID精度丢失)
*/
private Long relatedClubId;
private String relatedClubId;
/**
* 关联类型 (1: 社团, 2: 志愿者队伍)
@@ -0,0 +1,106 @@
package org.dromara.stgl.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 活动审批记录视图对象 stgl_activity_audit
*
* @author ruoyi
* @date 2025-01-09
*/
@Data
public class StglActivityAuditVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
private Long id;
/**
* 活动ID
*/
private Long activityId;
/**
* 活动名称
*/
private String activityName;
/**
* 节点代码(advisor_approve/dept_approve/school_approve)
*/
private String nodeCode;
/**
* 节点名称(指导老师审批/部门审批/校级审批)
*/
private String nodeName;
/**
* 节点顺序(1.1,1.2,2.1,2.2等)
*/
private BigDecimal nodeOrder;
/**
* 节点状态(pending/waiting/approved)
*/
private String nodeStatus;
/**
* 审批人ID
*/
private Long approverId;
/**
* 审批人姓名
*/
private String approverName;
/**
* 审批结果(通过/不通过)
*/
private String approveResult;
/**
* 审批意见
*/
private String approveOpinion;
/**
* 审批时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date approveTime;
/**
* 创建者
*/
private Long createBy;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 更新者
*/
private Long updateBy;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -110,9 +110,9 @@ public class StglActivityVo implements Serializable {
private String relatedDeptNames;
/**
* 关联社团ID
* 关联社团ID(String类型以避免雪花ID精度丢失)
*/
private Long relatedClubId;
private String relatedClubId;
/**
* 关联类型(1-社团 2-志愿者队伍)
@@ -0,0 +1,42 @@
package org.dromara.stgl.mapper;
import org.dromara.stgl.domain.StglActivityAudit;
import org.dromara.stgl.domain.vo.StglActivityAuditVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import java.util.List;
/**
* 活动审批记录Mapper接口
*
* @author ruoyi
* @date 2025-01-09
*/
public interface StglActivityAuditMapper extends BaseMapperPlus<StglActivityAudit, StglActivityAudit> {
/**
* 根据活动ID查询审批记录
*
* @param activityId 活动ID
* @return 审批记录列表
*/
List<StglActivityAuditVo> selectByActivityId(Long activityId);
/**
* 根据活动ID和节点代码查询审批记录
*
* @param activityId 活动ID
* @param nodeCode 节点代码
* @return 审批记录列表
*/
List<StglActivityAudit> selectByActivityIdAndNodeCode(Long activityId, String nodeCode);
/**
* 根据审批人ID查询待审批记录
*
* @param approverId 审批人ID
* @return 审批记录列表
*/
List<StglActivityAudit> selectPendingByApproverId(Long approverId);
}
@@ -0,0 +1,94 @@
package org.dromara.stgl.service;
import org.dromara.stgl.domain.StglActivityAudit;
import org.dromara.stgl.domain.bo.StglActivityApprovalBo;
import org.dromara.stgl.domain.vo.StglActivityAuditVo;
import java.util.Collection;
import java.util.List;
/**
* 活动审批记录Service接口
*
* @author ruoyi
* @date 2025-01-09
*/
public interface IStglActivityAuditService {
/**
* 查询活动审批记录
*
* @param id 主键ID
* @return 审批记录
*/
StglActivityAudit queryById(Long id);
/**
* 根据活动ID查询审批记录
*
* @param activityId 活动ID
* @return 审批记录列表
*/
List<StglActivityAuditVo> queryByActivityId(Long activityId);
/**
* 根据活动ID和节点代码查询审批记录
*
* @param activityId 活动ID
* @param nodeCode 节点代码
* @return 审批记录列表
*/
List<StglActivityAudit> queryByActivityIdAndNodeCode(Long activityId, String nodeCode);
/**
* 根据审批人ID查询待审批记录
*
* @param approverId 审批人ID
* @return 审批记录列表
*/
List<StglActivityAudit> queryPendingByApproverId(Long approverId);
/**
* 新增活动审批记录
*
* @param bo 审批记录业务对象
* @return 是否成功
*/
Boolean insertByBo(StglActivityAudit bo);
/**
* 修改活动审批记录
*
* @param bo 审批记录业务对象
* @return 是否成功
*/
Boolean updateByBo(StglActivityAudit bo);
/**
* 批量删除活动审批记录
*
* @param ids 主键ID列表
* @param isValid 是否进行有效性校验
* @return 是否成功
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 创建指导老师审批记录
*
* @param activityId 活动ID
* @param advisorIds 指导老师ID列表
* @param advisorNames 指导老师姓名列表
* @return 是否成功
*/
Boolean createAdvisorApprovalRecords(Long activityId, List<Long> advisorIds, List<String> advisorNames);
/**
* 审批活动(支持动态审核人选择)
*
* @param approvalBo 审批业务对象
* @return 是否成功
*/
Boolean approveActivity(StglActivityApprovalBo approvalBo);
}
@@ -123,4 +123,8 @@ public interface IStglActivityService extends IService<StglActivity> {
*/
List<ActivityRatingVo> getActivityRatings(Long activityId);
// 注意:提交审批和撤回审批功能已改为通过 updateByBo 方法监听状态变化实现
// 前端调用 updateStglActivity 修改状态为8(已提交)或0(草稿)
// 后端在 updateByBo 方法中自动创建或删除审批记录
}
@@ -0,0 +1,388 @@
package org.dromara.stgl.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.stgl.domain.StglActivity;
import org.dromara.stgl.domain.StglActivityAudit;
import org.dromara.stgl.domain.bo.StglActivityApprovalBo;
import org.dromara.stgl.domain.vo.StglActivityAuditVo;
import org.dromara.stgl.mapper.StglActivityAuditMapper;
import org.dromara.stgl.mapper.StglActivityMapper;
import org.dromara.stgl.service.IStglActivityAuditService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* 活动审批记录Service业务层处理
*
* @author ruoyi
* @date 2025-01-09
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class StglActivityAuditServiceImpl implements IStglActivityAuditService {
private final StglActivityAuditMapper baseMapper;
private final StglActivityMapper activityMapper;
/**
* 查询活动审批记录
*/
@Override
public StglActivityAudit queryById(Long id) {
return baseMapper.selectById(id);
}
/**
* 根据活动ID查询审批记录
*/
@Override
public List<StglActivityAuditVo> queryByActivityId(Long activityId) {
return baseMapper.selectByActivityId(activityId);
}
/**
* 根据活动ID和节点代码查询审批记录
*/
@Override
public List<StglActivityAudit> queryByActivityIdAndNodeCode(Long activityId, String nodeCode) {
return baseMapper.selectByActivityIdAndNodeCode(activityId, nodeCode);
}
/**
* 根据审批人ID查询待审批记录
*/
@Override
public List<StglActivityAudit> queryPendingByApproverId(Long approverId) {
return baseMapper.selectPendingByApproverId(approverId);
}
/**
* 新增活动审批记录
*/
@Override
public Boolean insertByBo(StglActivityAudit bo) {
StglActivityAudit add = BeanUtil.toBean(bo, StglActivityAudit.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改活动审批记录
*/
@Override
public Boolean updateByBo(StglActivityAudit bo) {
StglActivityAudit update = BeanUtil.toBean(bo, StglActivityAudit.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(StglActivityAudit entity) {
// TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除活动审批记录
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteByIds(ids) > 0;
}
/**
* 创建指导老师审批记录
*/
@Override
@Transactional
public Boolean createAdvisorApprovalRecords(Long activityId, List<Long> advisorIds, List<String> advisorNames) {
if (ObjectUtil.isEmpty(advisorIds) || ObjectUtil.isEmpty(advisorNames) || advisorIds.size() != advisorNames.size()) {
return false;
}
// 删除已存在的指导老师审批记录
LambdaQueryWrapper<StglActivityAudit> wrapper = Wrappers.lambdaQuery();
wrapper.eq(StglActivityAudit::getActivityId, activityId)
.eq(StglActivityAudit::getNodeCode, "advisor_approve");
baseMapper.delete(wrapper);
// 创建新的指导老师审批记录
for (int i = 0; i < advisorIds.size(); i++) {
StglActivityAudit audit = new StglActivityAudit();
audit.setActivityId(activityId);
audit.setNodeCode("advisor_approve");
audit.setNodeName("指导老师审批");
// 设置节点顺序:单个指导老师为1.0,多个指导老师为1.1, 1.2, 1.3...
if (advisorIds.size() == 1) {
audit.setNodeOrder(new BigDecimal("1.0"));
} else {
audit.setNodeOrder(new BigDecimal("1." + (i + 1)));
}
// 设置节点状态:第一个为pending,其他为waiting
audit.setNodeStatus(i == 0 ? "pending" : "waiting");
audit.setApproverId(advisorIds.get(i));
audit.setApproverName(advisorNames.get(i));
baseMapper.insert(audit);
}
return true;
}
/**
* 审批活动(支持动态审核人选择)
*/
@Override
@Transactional
public Boolean approveActivity(StglActivityApprovalBo approvalBo) {
// 1. 获取当前审批记录
StglActivityAudit currentAudit = baseMapper.selectById(approvalBo.getAuditId());
if (currentAudit == null || !"pending".equals(currentAudit.getNodeStatus())) {
return false;
}
// 2. 更新当前审批记录
currentAudit.setNodeStatus("approved");
currentAudit.setApproveResult(approvalBo.getApproveResult());
currentAudit.setApproveOpinion(approvalBo.getApproveOpinion());
currentAudit.setApproveTime(new Date());
baseMapper.updateById(currentAudit);
// 3. 如果审批不通过,直接更新活动状态为7(已驳回)
if ("不通过".equals(approvalBo.getApproveResult())) {
StglActivity activity = activityMapper.selectById(currentAudit.getActivityId());
if (activity != null) {
LambdaUpdateWrapper<StglActivity> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.eq(StglActivity::getId, currentAudit.getActivityId())
.set(StglActivity::getActivityStatus, "7");
activityMapper.update(null, updateWrapper);
}
return true;
}
// 4. 如果审批通过,检查是否有下一个待审批的同环节审批人
List<StglActivityAudit> samePhaseAudits = baseMapper.selectByActivityIdAndNodeCode(
currentAudit.getActivityId(), currentAudit.getNodeCode());
// 查找下一个waiting状态的审批记录
StglActivityAudit nextAudit = null;
for (StglActivityAudit audit : samePhaseAudits) {
if ("waiting".equals(audit.getNodeStatus()) &&
audit.getNodeOrder().compareTo(currentAudit.getNodeOrder()) > 0) {
if (nextAudit == null || audit.getNodeOrder().compareTo(nextAudit.getNodeOrder()) < 0) {
nextAudit = audit;
}
}
}
// 5. 如果有下一个审批人,将其状态改为pending
if (nextAudit != null) {
nextAudit.setNodeStatus("pending");
baseMapper.updateById(nextAudit);
} else {
// 6. 如果同环节所有审批人都审批通过,检查是否是第一个审批节点
boolean allApproved = samePhaseAudits.stream()
.allMatch(audit -> "approved".equals(audit.getNodeStatus()) && "通过".equals(audit.getApproveResult()));
if (allApproved) {
// 只有第一个审批节点(node_order=1.1 或 1.0)通过时才设置为9-审核中
if (isFirstApprovalNode(currentAudit)) {
StglActivity activity = activityMapper.selectById(currentAudit.getActivityId());
if (activity != null) {
LambdaUpdateWrapper<StglActivity> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.eq(StglActivity::getId, currentAudit.getActivityId())
.set(StglActivity::getActivityStatus, "9");
activityMapper.update(null, updateWrapper);
log.info("第一个审批节点通过,活动状态更新为9-审核中,活动ID: {}", activity.getId());
}
}
// 如果有指定下一步审核人,创建新的审批记录
if (approvalBo.getNextAuditors() != null && !approvalBo.getNextAuditors().isEmpty()) {
createNextStepAuditRecords(currentAudit.getActivityId(), approvalBo.getNextAuditors());
} else if (isLastApprovalNode(currentAudit, samePhaseAudits)) {
// 如果是最后一个节点且没有下一步审核人,根据活动时间判断最终状态
StglActivity activity = activityMapper.selectById(currentAudit.getActivityId());
String finalStatus = determineFinalStatus(activity);
LambdaUpdateWrapper<StglActivity> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.eq(StglActivity::getId, currentAudit.getActivityId())
.set(StglActivity::getActivityStatus, finalStatus);
activityMapper.update(null, updateWrapper);
log.info("最后审批节点通过,活动状态更新为: {}, 活动ID: {}", finalStatus, activity.getId());
}
}
}
return true;
}
/**
* 创建下一步审批记录
*/
private void createNextStepAuditRecords(Long activityId, List<StglActivityApprovalBo.NextAuditor> nextAuditors) {
// 查询已有的审批记录,确定下一个审批层级
List<StglActivityAuditVo> existingAudits = baseMapper.selectByActivityId(activityId);
// 确定下一个审批层级
int nextLevel = determineNextApprovalLevel(existingAudits);
for (int i = 0; i < nextAuditors.size(); i++) {
StglActivityApprovalBo.NextAuditor auditor = nextAuditors.get(i);
StglActivityAudit nextAudit = new StglActivityAudit();
nextAudit.setActivityId(activityId);
nextAudit.setNodeCode(getNodeCodeByLevel(nextLevel));
nextAudit.setNodeName(getNodeNameByLevel(nextLevel));
// 根据层级和顺序设置node_order
if (nextAuditors.size() == 1) {
// 单个审核人为 X.0
nextAudit.setNodeOrder(new BigDecimal(nextLevel + ".0"));
} else {
// 多个审核人为 X.1, X.2, X.3...
nextAudit.setNodeOrder(new BigDecimal(nextLevel + "." + (i + 1)));
}
nextAudit.setApproverId(auditor.getUserId());
nextAudit.setApproverName(auditor.getNickName() != null ? auditor.getNickName() : auditor.getUserName());
nextAudit.setNodeStatus(i == 0 ? "pending" : "waiting"); // 第一个为pending,其余为waiting
nextAudit.setCreateTime(new Date());
baseMapper.insert(nextAudit);
}
}
/**
* 确定下一个审批层级
*/
private int determineNextApprovalLevel(List<StglActivityAuditVo> existingAudits) {
int maxLevel = 1; // 默认从指导老师审批层级开始
for (StglActivityAuditVo audit : existingAudits) {
BigDecimal nodeOrder = audit.getNodeOrder();
if (nodeOrder != null) {
int level = nodeOrder.intValue(); // 获取整数部分作为层级
if (level > maxLevel) {
maxLevel = level;
}
}
}
return maxLevel + 1; // 下一个层级
}
/**
* 根据层级获取节点代码
*/
private String getNodeCodeByLevel(int level) {
switch (level) {
case 1:
return "advisor_approve";
case 2:
return "dept_approve";
case 3:
return "school_approve";
default:
return "dept_approve"; // 默认为部门审批
}
}
/**
* 根据层级获取节点名称
*/
private String getNodeNameByLevel(int level) {
switch (level) {
case 1:
return "指导老师审批";
case 2:
return "部门审批";
case 3:
return "校级审批";
default:
return "部门审批"; // 默认为部门审批
}
}
/**
* 判断是否为第一个审批节点(node_order=1.1 或 1.0)
*/
private boolean isFirstApprovalNode(StglActivityAudit currentAudit) {
if (currentAudit == null || currentAudit.getNodeOrder() == null) {
return false;
}
BigDecimal nodeOrder = currentAudit.getNodeOrder();
// 检查node_order是否为1.1或1.0
return nodeOrder.compareTo(new BigDecimal("1.0")) == 0 ||
nodeOrder.compareTo(new BigDecimal("1.1")) == 0;
}
/**
* 判断是否为最后一个审批节点
*/
private boolean isLastApprovalNode(StglActivityAudit currentAudit, List<StglActivityAudit> allAudits) {
if (currentAudit == null || currentAudit.getNodeOrder() == null || allAudits == null || allAudits.isEmpty()) {
return false;
}
// 获取所有审批记录中最大的nodeOrder
BigDecimal maxNodeOrder = allAudits.stream()
.map(StglActivityAudit::getNodeOrder)
.filter(order -> order != null)
.max(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO);
// 当前节点的nodeOrder等于最大nodeOrder时,表示是最后一个节点
boolean isLast = currentAudit.getNodeOrder().compareTo(maxNodeOrder) == 0;
log.info("最后节点判断: 当前节点={}, nodeOrder={}, 最大nodeOrder={}, 是否最后节点={}",
currentAudit.getNodeName(), currentAudit.getNodeOrder(), maxNodeOrder, isLast);
return isLast;
}
/**
* 根据活动时间判断最终状态
*/
private String determineFinalStatus(StglActivity activity) {
if (activity == null) {
return "1"; // 默认未开始
}
Date now = new Date();
Date startTime = activity.getStartTime();
Date endTime = activity.getEndTime();
if (startTime != null && now.before(startTime)) {
return "1"; // 未开始
} else if (endTime != null && now.after(endTime)) {
return "5"; // 已结束
} else {
return "4"; // 进行中
}
}
}
@@ -17,13 +17,17 @@ import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.stgl.domain.StglActivity;
import org.dromara.stgl.domain.StglActivityAudit;
import org.dromara.stgl.domain.bo.StglActivityBo;
import org.dromara.stgl.domain.vo.StglActivityVo;
import org.dromara.stgl.domain.vo.ActivityRatingVo;
import org.dromara.stgl.mapper.StglActivityMapper;
import org.dromara.stgl.mapper.StglActivityAuditMapper;
import org.dromara.stgl.mapper.StglActivityRatingMapper;
import org.dromara.stgl.mapper.StglMemberMapper;
import org.dromara.stgl.service.IStglActivityService;
import org.dromara.stgl.service.IStglActivityAuditService;
import org.dromara.stgl.service.IStglClubService;
import org.dromara.stgl.utils.AdvisorRoleAssigner;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -69,6 +73,9 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
private final StglActivityRatingMapper stglActivityRatingMapper;
private final StglMemberMapper stglMemberMapper;
private final ISysUserService sysUserService;
private final IStglActivityAuditService activityAuditService;
private final StglActivityAuditMapper activityAuditMapper;
private final IStglClubService clubService;
/**
* 查询活动信息
@@ -237,7 +244,7 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
lqw.ge(ObjectUtil.isNotNull(bo.getStartTime()), StglActivity::getStartTime, bo.getStartTime());
lqw.le(ObjectUtil.isNotNull(bo.getEndTime()), StglActivity::getEndTime, bo.getEndTime());
lqw.like(StringUtils.isNotBlank(bo.getLocation()), StglActivity::getLocation, bo.getLocation());
lqw.eq(ObjectUtil.isNotNull(bo.getRelatedClubId()), StglActivity::getRelatedClubId, bo.getRelatedClubId());
lqw.eq(StringUtils.isNotBlank(bo.getRelatedClubId()), StglActivity::getRelatedClubId, bo.getRelatedClubId());
lqw.like(StringUtils.isNotBlank(bo.getOrganizerName()), StglActivity::getOrganizerName, bo.getOrganizerName());
lqw.eq(ObjectUtil.isNotNull(bo.getRegistrationType()), StglActivity::getRegistrationType, bo.getRegistrationType());
lqw.ge(ObjectUtil.isNotNull(bo.getRegistrationStart()), StglActivity::getRegistrationStart, bo.getRegistrationStart());
@@ -256,6 +263,7 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
@Transactional(rollbackFor = Exception.class)
public Boolean insertByBo(StglActivityBo bo) {
StglActivity add = MapstructUtils.convert(bo, StglActivity.class);
validEntityBeforeSave(add);
// 处理多选部门数据
@@ -288,12 +296,13 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(StglActivityBo bo) {
StglActivity update = MapstructUtils.convert(bo, StglActivity.class);
validEntityBeforeSave(update);
// 处理多选部门数据
handleMultiDeptData(bo, update);
// 获取更新前的活动信息,用于比较指导老师变化
// 获取更新前的活动信息,用于比较指导老师变化和状态变化
StglActivity oldActivity = null;
if (update.getId() != null) {
oldActivity = baseMapper.selectById(update.getId());
@@ -304,6 +313,104 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
if (updateResult) {
// 处理指导老师角色分配
handleAdvisorRoleAssignment(oldActivity, update);
// 监听状态变化:如果状态从0(草稿)变为8(已提交),自动创建审批记录
if (oldActivity != null &&
"0".equals(oldActivity.getActivityStatus()) &&
"8".equals(update.getActivityStatus())) {
log.info("检测到活动状态从草稿变为已提交,自动创建审批记录 - activityId: {}", update.getId());
// 获取活动关联的社团信息
if (StringUtils.isBlank(update.getRelatedClubId())) {
throw new RuntimeException("活动未关联社团,无法提交审批");
}
log.info("查询关联社团信息 - clubId: {}", update.getRelatedClubId());
// 将String类型的clubId转换为Long类型进行查询
Long clubId;
try {
clubId = Long.valueOf(update.getRelatedClubId());
} catch (NumberFormatException e) {
log.error("关联社团ID格式错误: {}", update.getRelatedClubId(), e);
throw new RuntimeException("关联社团ID格式错误: " + update.getRelatedClubId());
}
StglClubVo clubVo = clubService.queryById(clubId);
if (clubVo == null) {
log.error("社团不存在或已被删除 - clubId: {}", update.getRelatedClubId());
throw new RuntimeException("活动关联的社团不存在或已被删除(ID: " + update.getRelatedClubId() + ")。请检查社团数据或重新选择关联社团。");
}
// 从社团中提取指导老师信息
List<Long> advisorIds = new ArrayList<>();
List<String> advisorNames = new ArrayList<>();
if (StringUtils.isNotBlank(clubVo.getAdvisorsJson())) {
// 解析社团指导老师JSON数据
List<Map<String, Object>> advisors =
JsonUtils.parseObject(clubVo.getAdvisorsJson(),
new TypeReference<List<Map<String, Object>>>() {});
if (advisors != null) {
for (Map<String, Object> advisor : advisors) {
// 安全地转换userId,支持String和Number类型
Long userId = null;
Object userIdObj = advisor.get("userId");
if (userIdObj != null) {
if (userIdObj instanceof Number) {
userId = ((Number) userIdObj).longValue();
} else if (userIdObj instanceof String) {
try {
userId = Long.valueOf((String) userIdObj);
} catch (NumberFormatException e) {
log.warn("无法解析指导老师userId: {}", userIdObj, e);
continue;
}
}
}
String name = (String) advisor.get("name");
if (userId != null && StringUtils.isNotBlank(name)) {
advisorIds.add(userId);
advisorNames.add(name);
}
}
}
}
if (advisorIds.isEmpty()) {
throw new RuntimeException("活动关联的社团没有配置指导老师,无法提交审批");
}
// 创建指导老师审批记录
activityAuditService.createAdvisorApprovalRecords(
update.getId(), advisorIds, advisorNames);
log.info("自动创建审批记录成功 - activityId: {}, advisorCount: {}",
update.getId(), advisorIds.size());
}
// 监听状态变化:如果状态从8(已提交)变为0(草稿),自动删除审批记录
if (oldActivity != null &&
"8".equals(oldActivity.getActivityStatus()) &&
"0".equals(update.getActivityStatus())) {
log.info("检测到活动状态从已提交变为草稿,自动删除审批记录 - activityId: {}", update.getId());
// 删除所有审批记录
LambdaQueryWrapper<StglActivityAudit> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.eq(StglActivityAudit::getActivityId, update.getId());
List<StglActivityAudit> audits = activityAuditMapper.selectList(queryWrapper);
if (!audits.isEmpty()) {
activityAuditService.deleteWithValidByIds(
audits.stream().map(StglActivityAudit::getId).collect(Collectors.toList()),
false);
log.info("自动删除审批记录成功 - activityId: {}, auditCount: {}",
update.getId(), audits.size());
}
}
}
return updateResult;
@@ -908,4 +1015,7 @@ public class StglActivityServiceImpl extends ServiceImpl<StglActivityMapper, Stg
entity.setRelatedDeptNames(null);
}
}
// 注意:submitActivityForApproval 和 withdrawActivityApproval 方法已废弃
// 功能已整合到 updateByBo 方法中,通过监听状态变化自动处理审批记录
}
@@ -705,7 +705,7 @@ public class StglClubAnnualReviewServiceImpl implements IStglClubAnnualReviewSer
// 构建查询条件
StglActivityBo bo = new StglActivityBo();
bo.setRelatedClubId(clubId); // 设置社团ID
bo.setRelatedClubId(String.valueOf(clubId)); // 设置社团ID(Long转String避免精度丢失)
bo.setRelatedType(1); // 关联类型为社团(1表示社团,2表示志愿团队)
// 设置时间范围查询条件(查询活动开始时间在该年度内的活动)
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.stgl.config.AnnualReviewConfig;
import org.dromara.stgl.domain.StglClub;
import org.dromara.stgl.domain.StglClubAnnualReview;
import org.dromara.stgl.domain.vo.ClubActivityPermissionResult;
@@ -29,11 +30,7 @@ public class StglClubPermissionServiceImpl implements IStglClubPermissionService
private final StglClubMapper clubMapper;
private final StglClubAnnualReviewMapper annualReviewMapper;
/**
* 年审窗口期天数(15天)
*/
private static final int ANNUAL_REVIEW_WINDOW_DAYS = 15;
private final AnnualReviewConfig annualReviewConfig;
/**
* 检查社团活动权限(实时计算,精确到秒级)
@@ -57,7 +54,7 @@ public class StglClubPermissionServiceImpl implements IStglClubPermissionService
// 2. 计算关键时间点(精确到秒)
Date annualReviewStartTime = calculateAnnualReviewStartTime(establishDate, currentTime);
Date annualReviewDeadlineTime = addDays(annualReviewStartTime, ANNUAL_REVIEW_WINDOW_DAYS);
Date annualReviewDeadlineTime = addDays(annualReviewStartTime, annualReviewConfig.getWindowDays());
log.debug("社团ID: {}, 成立时间: {}, 年审开始时间: {}, 年审截止时间: {}, 当前时间: {}",
clubId, establishDate, annualReviewStartTime, annualReviewDeadlineTime, currentTime);
@@ -164,7 +161,7 @@ public class StglClubPermissionServiceImpl implements IStglClubPermissionService
Date currentTime = new Date();
Date annualReviewStartTime = calculateAnnualReviewStartTime(clubInfo.getEstablishDate(), currentTime);
Date annualReviewDeadlineTime = addDays(annualReviewStartTime, ANNUAL_REVIEW_WINDOW_DAYS);
Date annualReviewDeadlineTime = addDays(annualReviewStartTime, annualReviewConfig.getWindowDays());
// 如果当前不在年审窗口期,返回false
if (currentTime.before(annualReviewStartTime) || currentTime.after(annualReviewDeadlineTime)) {
@@ -175,30 +172,114 @@ public class StglClubPermissionServiceImpl implements IStglClubPermissionService
}
/**
* 计算年审开始时间(精确到具体时刻)
* 计算年审开始时间(支持历史社团处理)
*
* @param establishDate 社团成立时间
* @param currentDate 当前时间
* @return 今年的年审开始时间
* @return 下一次年审开始时间
*/
private Date calculateAnnualReviewStartTime(Date establishDate, Date currentDate) {
// 1. 区分历史社团和新成立社团
if (annualReviewConfig.isHistoricalClub(establishDate)) {
return handleHistoricalClub(establishDate, currentDate);
} else {
return handleNewClub(establishDate, currentDate);
}
}
/**
* 处理历史社团的年审时间计算
* 历史社团统一从项目上线后的第N年开始年审
*/
private Date handleHistoricalClub(Date establishDate, Date currentDate) {
Date projectLaunchDate = annualReviewConfig.getProjectLaunchDateTime();
Calendar launchCal = Calendar.getInstance();
launchCal.setTime(projectLaunchDate);
Calendar establishCal = Calendar.getInstance();
establishCal.setTime(establishDate);
Calendar currentCal = Calendar.getInstance();
currentCal.setTime(currentDate);
// 构造今年同月同日的时间,保持原有的时分秒毫秒
// 构造年审时间:项目上线年份 + 宽限期 + 社团成立的月日时分秒
Calendar annualReviewCal = Calendar.getInstance();
annualReviewCal.set(Calendar.YEAR, currentCal.get(Calendar.YEAR));
annualReviewCal.set(Calendar.YEAR, launchCal.get(Calendar.YEAR) + annualReviewConfig.getGracePeriodYears());
annualReviewCal.set(Calendar.MONTH, establishCal.get(Calendar.MONTH));
annualReviewCal.set(Calendar.DAY_OF_MONTH, establishCal.get(Calendar.DAY_OF_MONTH));
annualReviewCal.set(Calendar.HOUR_OF_DAY, establishCal.get(Calendar.HOUR_OF_DAY));
annualReviewCal.set(Calendar.MINUTE, establishCal.get(Calendar.MINUTE));
annualReviewCal.set(Calendar.SECOND, establishCal.get(Calendar.SECOND));
annualReviewCal.set(Calendar.MILLISECOND, establishCal.get(Calendar.MILLISECOND));
Date firstAnnualReviewTime = annualReviewCal.getTime();
log.debug("历史社团年审计算 - 成立时间: {}, 项目上线: {}, 第1次年审: {}, 当前时间: {}",
establishDate, projectLaunchDate, firstAnnualReviewTime, currentDate);
// 如果第一次年审时间还没到,返回第一次年审时间
if (currentDate.before(firstAnnualReviewTime)) {
return firstAnnualReviewTime;
}
// 如果已经过了第一次年审时间,计算后续的年审时间
return calculateNextAnnualReviewAfterFirst(establishDate, currentDate, firstAnnualReviewTime);
}
return annualReviewCal.getTime();
/**
* 处理新成立社团的年审时间计算
* 新社团:成立满1年后开始年审
*/
private Date handleNewClub(Date establishDate, Date currentDate) {
// 第一次年审时间 = 成立时间 + 1年
Calendar firstAnnualReviewCal = Calendar.getInstance();
firstAnnualReviewCal.setTime(establishDate);
firstAnnualReviewCal.add(Calendar.YEAR, 1);
Date firstAnnualReviewTime = firstAnnualReviewCal.getTime();
log.debug("新成立社团年审计算 - 成立时间: {}, 第1次年审: {}, 当前时间: {}",
establishDate, firstAnnualReviewTime, currentDate);
// 如果第一次年审时间还没到,返回第一次年审时间
if (currentDate.before(firstAnnualReviewTime)) {
return firstAnnualReviewTime;
}
// 如果已经过了第一次年审时间,计算后续的年审时间
return calculateNextAnnualReviewAfterFirst(establishDate, currentDate, firstAnnualReviewTime);
}
/**
* 计算第一次年审之后的年审时间
*/
private Date calculateNextAnnualReviewAfterFirst(Date establishDate, Date currentDate, Date firstAnnualReviewTime) {
Calendar currentCal = Calendar.getInstance();
currentCal.setTime(currentDate);
Calendar establishCal = Calendar.getInstance();
establishCal.setTime(establishDate);
// 构造今年的年审时间
Calendar thisYearAnnualReviewCal = Calendar.getInstance();
thisYearAnnualReviewCal.set(Calendar.YEAR, currentCal.get(Calendar.YEAR));
thisYearAnnualReviewCal.set(Calendar.MONTH, establishCal.get(Calendar.MONTH));
thisYearAnnualReviewCal.set(Calendar.DAY_OF_MONTH, establishCal.get(Calendar.DAY_OF_MONTH));
thisYearAnnualReviewCal.set(Calendar.HOUR_OF_DAY, establishCal.get(Calendar.HOUR_OF_DAY));
thisYearAnnualReviewCal.set(Calendar.MINUTE, establishCal.get(Calendar.MINUTE));
thisYearAnnualReviewCal.set(Calendar.SECOND, establishCal.get(Calendar.SECOND));
thisYearAnnualReviewCal.set(Calendar.MILLISECOND, establishCal.get(Calendar.MILLISECOND));
Date thisYearAnnualReview = thisYearAnnualReviewCal.getTime();
log.debug("计算后续年审时间 - 今年年审: {}, 当前时间: {}", thisYearAnnualReview, currentDate);
// 如果今年的年审时间还没到,返回今年的
if (currentDate.before(thisYearAnnualReview)) {
return thisYearAnnualReview;
}
// 如果今年的年审时间已经过了,返回明年的
thisYearAnnualReviewCal.add(Calendar.YEAR, 1);
return thisYearAnnualReviewCal.getTime();
}
/**
@@ -0,0 +1,7 @@
# 社团管理模块独立配置
# 此配置仅适用于stgl社团管理模块
# 社团年审配置
stgl.club.annual-review.project-launch-date=2025-10-15 00:00:00
stgl.club.annual-review.grace-period-years=1
stgl.club.annual-review.window-days=15
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.stgl.mapper.StglActivityAuditMapper">
<resultMap type="org.dromara.stgl.domain.StglActivityAudit" id="StglActivityAuditResult">
<result property="id" column="id" />
<result property="tenantId" column="tenant_id" />
<result property="createDept" column="create_dept" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="deleted" column="deleted" />
<result property="activityId" column="activity_id" />
<result property="nodeCode" column="node_code" />
<result property="nodeName" column="node_name" />
<result property="nodeOrder" column="node_order" />
<result property="nodeStatus" column="audit_status" />
<result property="approverId" column="approver_id" />
<result property="approverName" column="approver_name" />
<result property="approveResult" column="approve_result" />
<result property="approveOpinion" column="audit_opinion" />
<result property="approveTime" column="audit_time" />
</resultMap>
<resultMap type="org.dromara.stgl.domain.vo.StglActivityAuditVo" id="StglActivityAuditVoResult">
<result property="id" column="id" />
<result property="activityId" column="activity_id" />
<result property="activityName" column="activity_name" />
<result property="nodeCode" column="node_code" />
<result property="nodeName" column="node_name" />
<result property="nodeOrder" column="node_order" />
<result property="nodeStatus" column="audit_status" />
<result property="approverId" column="approver_id" />
<result property="approverName" column="approver_name" />
<result property="approveResult" column="approve_result" />
<result property="approveOpinion" column="audit_opinion" />
<result property="approveTime" column="audit_time" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectStglActivityAuditVo">
select id, tenant_id, create_dept, create_by, create_time, update_by, update_time, deleted,
activity_id, node_code, node_name, node_order, audit_status,
approver_id, approver_name, approve_result, audit_opinion, audit_time
from stgl_activity_audit
</sql>
<!-- 根据活动ID查询审批记录(包含活动名称) -->
<select id="selectByActivityId" parameterType="Long" resultMap="StglActivityAuditVoResult">
select
a.id, a.activity_id, a.node_code, a.node_name, a.node_order, a.audit_status,
a.approver_id, a.approver_name, a.approve_result, a.audit_opinion, a.audit_time,
a.create_by, a.create_time, a.update_by, a.update_time,
act.activity_name
from stgl_activity_audit a
left join stgl_activity act on a.activity_id = act.id
where a.activity_id = #{activityId} and a.deleted = 0
order by a.node_order
</select>
<!-- 根据活动ID和节点代码查询审批记录 -->
<select id="selectByActivityIdAndNodeCode" resultMap="StglActivityAuditResult">
<include refid="selectStglActivityAuditVo"/>
where activity_id = #{activityId} and node_code = #{nodeCode} and deleted = 0
order by node_order
</select>
<!-- 根据审批人ID查询待审批记录 -->
<select id="selectPendingByApproverId" parameterType="Long" resultMap="StglActivityAuditResult">
<include refid="selectStglActivityAuditVo"/>
where approver_id = #{approverId} and audit_status = 'pending' and deleted = 0
order by create_time desc
</select>
</mapper>
+131
View File
@@ -0,0 +1,131 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
/**
* 活动审批记录VO
*/
export interface ActivityAuditVO {
id?: number;
activityId?: number;
activityName?: string;
nodeCode?: string;
nodeName?: string;
nodeOrder?: number;
nodeStatus?: string;
approverId?: number;
approverName?: string;
approveResult?: string;
approveOpinion?: string;
approveTime?: string;
createBy?: number;
createTime?: string;
updateBy?: number;
updateTime?: string;
}
/**
* 活动审批请求参数
*/
export interface ActivityApprovalForm {
auditId: number;
approveResult: string;
approveOpinion?: string;
nextAuditors?: NextAuditor[];
}
/**
* 下一步审核人信息
*/
export interface NextAuditor {
userId: number;
userName: string;
nickName: string;
}
/**
* 查询活动审批记录详细
* @param id 审批记录主键
* @returns 审批记录详情
*/
export const getActivityAudit = (id: string | number): AxiosPromise<ActivityAuditVO> => {
return request({
url: '/stgl/activityAudit/' + id,
method: 'get'
});
};
/**
* 根据活动ID查询审批记录
* @param activityId 活动ID
* @returns 审批记录列表
*/
export const getActivityAuditsByActivityId = (activityId: string | number): AxiosPromise<ActivityAuditVO[]> => {
return request({
url: '/stgl/activityAudit/byActivity/' + activityId,
method: 'get'
});
};
/**
* 根据审批人ID查询待审批记录
* @param approverId 审批人ID
* @returns 审批记录列表
*/
export const getPendingActivityAudits = (approverId: string | number): AxiosPromise<ActivityAuditVO[]> => {
return request({
url: '/stgl/activityAudit/pending/' + approverId,
method: 'get'
});
};
/**
* 审批活动
* @param data 审批参数
* @returns 操作结果
*/
export const approveActivity = (data: ActivityApprovalForm) => {
return request({
url: '/stgl/activityAudit/approve',
method: 'post',
data: data
});
};
/**
* 新增活动审批记录
* @param data 审批记录信息
* @returns 操作结果
*/
export const addActivityAudit = (data: ActivityAuditVO) => {
return request({
url: '/stgl/activityAudit',
method: 'post',
data: data
});
};
/**
* 修改活动审批记录
* @param data 审批记录信息
* @returns 操作结果
*/
export const updateActivityAudit = (data: ActivityAuditVO) => {
return request({
url: '/stgl/activityAudit',
method: 'put',
data: data
});
};
/**
* 删除活动审批记录
* @param id 审批记录主键
* @returns 操作结果
*/
export const delActivityAudit = (id: string | number | Array<string | number>) => {
return request({
url: '/stgl/activityAudit/' + id,
method: 'delete'
});
};
+4
View File
@@ -146,3 +146,7 @@ export const getActivityRatings = (activityId: string | number): AxiosPromise<Ac
method: 'get'
});
};
// 注意:submitActivityForApproval 和 withdrawActivityApproval 方法已废弃
// 现在直接使用 updateStglActivity 修改活动状态为8(已提交)或0(草稿)
// 后端会在 updateByBo 方法中监听状态变化,自动创建或删除审批记录
@@ -0,0 +1,310 @@
<template>
<el-dialog
title="活动审批"
v-model="visible"
width="600px"
append-to-body
:before-close="handleClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="活动名称:">
<span>{{ activityInfo?.activityName || '加载中...' }}</span>
</el-form-item>
<el-form-item label="活动类型:">
<span>{{ activityInfo?.activityType || '加载中...' }}</span>
</el-form-item>
<el-form-item label="审批节点:">
<span>{{ auditRecord?.nodeName || '加载中...' }}</span>
</el-form-item>
<el-form-item label="审批结果:" prop="approveResult">
<el-radio-group v-model="form.approveResult" @change="handleApproveResultChange">
<el-radio value="通过">通过</el-radio>
<el-radio value="不通过">不通过</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审批意见:" prop="approveOpinion">
<el-input
v-model="form.approveOpinion"
type="textarea"
:rows="4"
placeholder="请填写审批意见"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 审批流程选择对话框 -->
<ApprovalProcessDialog
v-model:visible="showApprovalProcessDialog"
:next-node-name="getNextNodeName()"
@confirm="handleApprovalProcessConfirm"
/>
</template>
<script setup lang="ts">
import { approveActivity, getActivityAudit, getActivityAuditsByActivityId } from '@/api/stgl/activity/audit';
import { getStglActivity } from '@/api/stgl/activity';
import ApprovalProcessDialog from './ApprovalProcessDialog.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emit = defineEmits<{
success: [];
}>();
const visible = ref(false);
const loading = ref(false);
const formRef = ref<ElFormInstance>();
const form = ref({
auditId: '',
approveResult: '',
approveOpinion: ''
});
const activityInfo = ref<any>(null);
const auditRecord = ref<any>(null);
const allAuditRecords = ref<any[]>([]);
const showApprovalProcessDialog = ref(false);
const rules = reactive<ElFormRules>({
approveResult: [
{ required: true, message: '请选择审批结果', trigger: 'change' }
],
approveOpinion: [
{ required: true, message: '请填写审批意见', trigger: 'blur' },
{ max: 500, message: '审批意见不能超过500个字符', trigger: 'blur' }
]
});
/** 打开弹窗 */
const open = async (auditId: string | number, activityId: string | number) => {
visible.value = true;
form.value.auditId = auditId.toString();
// 获取活动信息用于显示
try {
const res = await getStglActivity(activityId);
activityInfo.value = res.data;
} catch (error) {
console.error('获取活动信息失败:', error);
}
// 获取审批记录信息用于显示节点名称
try {
const auditRes = await getActivityAudit(auditId);
auditRecord.value = auditRes.data;
} catch (error) {
console.error('获取审批记录信息失败:', error);
}
// 获取当前活动的所有审批记录,用于判断是否是本环节最后一个节点
try {
const allAuditsRes = await getActivityAuditsByActivityId(activityId);
allAuditRecords.value = allAuditsRes.data || [];
} catch (error) {
console.error('获取所有审批记录失败:', error);
}
// 重置表单
resetForm();
};
/** 重置表单 */
const resetForm = () => {
form.value.approveResult = '';
form.value.approveOpinion = '';
formRef.value?.clearValidate();
};
/** 审批结果变化处理 */
const handleApproveResultChange = (value: string) => {
// 审批结果变化时的处理逻辑(如需要)
};
/** 关闭弹窗 */
const handleClose = () => {
visible.value = false;
resetForm();
activityInfo.value = null;
auditRecord.value = null;
allAuditRecords.value = [];
};
/** 提交审批 */
const handleSubmit = async () => {
if (!formRef.value) return;
const valid = await formRef.value.validate();
if (!valid) return;
// 如果是校级审批的最后一个节点,直接结束流程,不弹出审批流程选择对话框
if (form.value.approveResult === '通过' && isSchoolLevelLastNode()) {
console.log('校级审批最后节点,直接结束流程');
await submitApproval();
return;
}
// 如果审批通过,且当前节点是本环节的最后一个节点,弹出审批流程选择对话框
if (form.value.approveResult === '通过' && isLastNodeInCurrentPhase()) {
showApprovalProcessDialog.value = true;
return;
}
// 直接提交审批(审批不通过或不是本环节最后一个节点的情况)
await submitApproval();
};
/** 判断当前节点是否是本环节的最后一个节点 */
const isLastNodeInCurrentPhase = () => {
if (!auditRecord.value || !allAuditRecords.value.length) {
return false;
}
const currentNodeCode = auditRecord.value.nodeCode;
const currentNodeOrder = Number(auditRecord.value.nodeOrder);
// 获取同一环节的所有节点
const samePhaseNodes = allAuditRecords.value.filter(record =>
record.nodeCode === currentNodeCode
);
if (samePhaseNodes.length === 0) {
return false;
}
// 找出同一环节中最大的nodeOrder
const maxNodeOrder = Math.max(...samePhaseNodes.map(node => Number(node.nodeOrder)));
// 当前节点的nodeOrder等于同环节最大nodeOrder时,表示是本环节的最后一个节点
const isLast = currentNodeOrder === maxNodeOrder;
console.log('判断是否是本环节最后节点:', {
currentNodeCode,
currentNodeOrder,
maxNodeOrder,
samePhaseNodesCount: samePhaseNodes.length,
isLast
});
return isLast;
};
/** 判断当前是否是校级审批的最后一个节点 */
const isSchoolLevelLastNode = () => {
if (!auditRecord.value) {
return false;
}
const currentNodeName = auditRecord.value.nodeName;
const currentNodeOrder = Number(auditRecord.value.nodeOrder);
// 检查是否是校级审批节点且node_order是3.X格式
const isSchoolLevelNode = currentNodeName === '校级审批';
const isNodeOrder3X = currentNodeOrder >= 3 && currentNodeOrder < 4;
console.log('校级审批最后节点判断:', {
currentNodeName,
currentNodeOrder,
isSchoolLevelNode,
isNodeOrder3X,
isLastInPhase: isLastNodeInCurrentPhase()
});
// 校级审批 + node_order=3.X + 是本环节最后一个节点
return isSchoolLevelNode && isNodeOrder3X && isLastNodeInCurrentPhase();
};
/** 获取下一步节点名称 */
const getNextNodeName = () => {
if (!auditRecord.value) return '';
const currentNodeCode = auditRecord.value.nodeCode;
// 根据当前节点代码推断下一步节点名称
if (currentNodeCode === 'advisor_approve') {
return '二级学院审批';
} else if (currentNodeCode === 'dept_approve') {
return '校级审批';
}
return '下一步审核';
};
/** 审批流程选择确认 */
const handleApprovalProcessConfirm = async (nextAuditors: any[]) => {
await submitApproval(nextAuditors);
};
/** 提交审批请求 */
const submitApproval = async (nextAuditors?: any[]) => {
loading.value = true;
try {
// 判断是否是校级审批最后节点
const isSchoolLevelFinalNode = isSchoolLevelLastNode();
// 准备审批数据
const params: any = {
auditId: form.value.auditId, // 保持字符串格式,避免精度丢失
approveResult: form.value.approveResult,
approveOpinion: form.value.approveOpinion
};
// 校级审批最后节点时不设置下一步审核人,表示流程结束
// 其他情况:只有审批通过且是本环节最后一个节点时,才包含下一步审核人信息
if (!isSchoolLevelFinalNode && nextAuditors && nextAuditors.length > 0) {
params.nextAuditors = nextAuditors;
}
console.log('提交审批数据:', {
审批结果: params.approveResult,
是否本环节最后节点: isLastNodeInCurrentPhase(),
是否校级审批最后节点: isSchoolLevelFinalNode,
下一步审核人数量: params.nextAuditors?.length || 0,
流程状态: isSchoolLevelFinalNode ? '流程结束' : '继续流转'
});
await approveActivity(params);
if (isSchoolLevelFinalNode) {
proxy?.$modal.msgSuccess('审批成功,活动审批流程已完成!');
} else {
proxy?.$modal.msgSuccess('审批成功');
}
handleClose();
emit('success');
} catch (error) {
console.error('审批失败:', error);
} finally {
loading.value = false;
}
};
defineExpose({
open
});
</script>
<style scoped lang="scss">
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
@@ -0,0 +1,323 @@
<template>
<el-dialog
title="选择审批流程"
v-model="visible"
width="70%"
append-to-body
destroy-on-close
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div class="approval-dialog-content">
<el-alert
:title="`请选择${props.nextNodeName}审核人`"
description="活动审批提交后将按照您选择的审批人员顺序进行审批,请仔细选择审核人员。"
type="info"
:closable="false"
show-icon
style="margin-bottom: 20px"
/>
<!-- 下一步审批节点提示 -->
<div class="next-node-info">
<div class="node-label">
<el-icon><Document /></el-icon>
下一步审批节点:{{ props.nextNodeName }}
</div>
</div>
<!-- 审核人选择区域 -->
<div class="auditor-selection-section">
<div class="custom-process-config">
<el-form-item label="选择审核人" required>
<div class="auditor-selector">
<div class="selector-header">
<el-button type="primary" icon="Plus" @click="openAuditorSelector">添加审核人</el-button>
<el-text v-if="selectedAuditors.length > 0" type="info" size="small">
已选择 {{ selectedAuditors.length }} 个审核人,将按添加顺序进行审批
</el-text>
</div>
<!-- 已选择的审核人列表 -->
<div v-if="selectedAuditors.length > 0" class="selected-auditors">
<div class="auditor-flow">
<div v-for="(auditor, index) in selectedAuditors" :key="auditor.userId" class="auditor-node">
<div class="node-content">
<span class="node-index">{{ index + 1 }}</span>
<div class="auditor-info">
<div class="auditor-name">{{ auditor.nickName || auditor.userName }}</div>
<div class="auditor-dept">{{ auditor.deptName || '未知部门' }}</div>
</div>
<el-button type="danger" icon="Delete" size="small" circle @click="removeAuditor(auditor)" />
</div>
<div v-if="index < selectedAuditors.length - 1" class="flow-arrow">
<el-icon><ArrowRight /></el-icon>
</div>
</div>
</div>
<!-- 流程预览说明 -->
<div class="flow-description">
<el-alert type="info" :closable="false" show-icon>
<template #title>
<span>审批流程预览:{{ props.nextNodeName }}将按上述顺序进行逐级审批,每个节点审批通过后自动流转到下一节点</span>
</template>
</el-alert>
</div>
</div>
<!-- 空状态提示 -->
<div v-else class="empty-state">
<el-empty description="暂未选择审核人">
<el-button type="primary" @click="openAuditorSelector">选择审核人</el-button>
</el-empty>
</div>
</div>
</el-form-item>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:loading="submitting"
:disabled="selectedAuditors.length === 0"
>
<el-icon><Check /></el-icon>
确认提交 ({{ selectedAuditors.length }} 个审核人)
</el-button>
</div>
</template>
</el-dialog>
<!-- 审核人选择器 -->
<AuditorSelector
ref="auditorSelectorRef"
@confirm="handleAuditorsSelected"
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { Check, User, UserFilled, Document, Plus, ArrowRight, Delete } from '@element-plus/icons-vue';
import AuditorSelector from '@/views/stgl/club/info/annualReview/auditorSelector/index.vue';
interface Props {
visible: boolean;
nextNodeName?: string; // 下一步审批节点名称
}
interface Emits {
(e: 'update:visible', value: boolean): void;
(e: 'confirm', auditors: any[]): void;
}
const props = withDefaults(defineProps<Props>(), {
nextNodeName: '部门审批'
});
const emit = defineEmits<Emits>();
const visible = ref(props.visible);
const submitting = ref(false);
const selectedAuditors = ref<any[]>([]);
const auditorSelectorRef = ref<InstanceType<typeof AuditorSelector>>();
// 监听visible变化
watch(() => props.visible, (newVal) => {
visible.value = newVal;
if (newVal) {
selectedAuditors.value = [];
}
});
// 监听内部visible变化
watch(visible, (newVal) => {
emit('update:visible', newVal);
});
// 打开审核人选择器
const openAuditorSelector = () => {
auditorSelectorRef.value?.open(selectedAuditors.value);
};
// 审核人选择确认
const handleAuditorsSelected = (auditors: any[]) => {
selectedAuditors.value = [...auditors];
};
// 移除审核人
const removeAuditor = (auditor: any) => {
const index = selectedAuditors.value.findIndex(item => item.userId === auditor.userId);
if (index > -1) {
selectedAuditors.value.splice(index, 1);
}
};
// 取消操作
const handleCancel = () => {
visible.value = false;
ElMessage.info('已取消提交');
};
// 确认提交
const handleConfirm = async () => {
try {
if (selectedAuditors.value.length === 0) {
ElMessage.warning('请至少选择一个审核人');
return;
}
submitting.value = true;
// 触发确认事件,返回审核人列表
emit('confirm', selectedAuditors.value);
visible.value = false;
ElMessage.success('审批流程配置成功');
} catch (error) {
console.error('确认审批流程失败:', error);
ElMessage.error('确认审批流程失败');
} finally {
submitting.value = false;
}
};
// 暴露方法供父组件调用
defineExpose({
open: (preSelectedAuditors?: any[]) => {
visible.value = true;
selectedAuditors.value = [...(preSelectedAuditors || [])];
}
});
</script>
<style scoped>
.approval-dialog-content {
max-height: 500px;
}
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.next-node-info {
margin-bottom: 20px;
padding: 12px 16px;
background-color: #f0f9ff;
border: 1px solid #d1ecf1;
border-radius: 6px;
}
.node-label {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
color: #0c63e4;
font-size: 14px;
}
.auditor-selection-section {
margin-bottom: 20px;
}
.custom-process-config {
padding: 24px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #fafbfc;
}
.auditor-selector {
width: 100%;
}
.selector-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.selected-auditors {
margin-top: 20px;
}
.auditor-flow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
.auditor-node {
display: flex;
align-items: center;
gap: 12px;
}
.node-content {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: white;
border: 1px solid #e4e7ed;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.node-index {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: #409eff;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: 600;
}
.auditor-info {
flex: 1;
}
.auditor-name {
font-size: 14px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 2px;
}
.auditor-dept {
font-size: 12px;
color: #909399;
}
.flow-arrow {
display: flex;
align-items: center;
color: #409eff;
font-size: 16px;
}
.flow-description {
margin-top: 16px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
}
</style>
+192 -116
View File
@@ -117,12 +117,20 @@
!single && selectedActivity && getOriginalActivityStatus(selectedActivity) === '0' && hasActivityOperationPermission(selectedActivity)
"
>
<el-button type="success" icon="Upload" @click="handleSubmit()" v-hasPermi="['stgl:activity:edit']">提交</el-button>
<el-button type="success" icon="Upload" @click="handleSubmitApproval()" v-hasPermi="['stgl:activity:edit']">提交审批</el-button>
</el-col>
<el-col
:span="1.5"
v-if="
!single && selectedActivity && getOriginalActivityStatus(selectedActivity) === '2' && hasActivityOperationPermission(selectedActivity)
!single && selectedActivity && getOriginalActivityStatus(selectedActivity) === '8' && hasActivityOperationPermission(selectedActivity)
"
>
<el-button type="info" icon="RefreshLeft" @click="handleWithdrawApproval()" v-hasPermi="['stgl:activity:edit']">撤回审批</el-button>
</el-col>
<el-col
:span="1.5"
v-if="
!single && selectedActivity && ['8', '9'].includes(getOriginalActivityStatus(selectedActivity) || '') && canApproveActivity(selectedActivity)
"
>
<el-button type="warning" icon="Check" @click="handleApprove()" v-hasPermi="['stgl:activity:approve']">审批</el-button>
@@ -283,7 +291,7 @@
></el-button>
</el-tooltip>
<el-tooltip
content="提交"
content="提交审批"
placement="top"
v-if="getOriginalActivityStatus(scope.row) === '0' && hasActivityOperationPermission(scope.row)"
>
@@ -293,14 +301,29 @@
icon="Upload"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleSubmit(scope.row)"
@click="handleSubmitApproval(scope.row)"
v-hasPermi="['stgl:activity:edit']"
></el-button>
</el-tooltip>
<el-tooltip
content="撤回审批"
placement="top"
v-if="getOriginalActivityStatus(scope.row) === '8' && hasActivityOperationPermission(scope.row)"
>
<el-button
link
type="info"
icon="RefreshLeft"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleWithdrawApproval(scope.row)"
v-hasPermi="['stgl:activity:edit']"
></el-button>
</el-tooltip>
<el-tooltip
content="审批"
placement="top"
v-if="getOriginalActivityStatus(scope.row) === '2' && hasActivityOperationPermission(scope.row)"
v-if="['8', '9'].includes(getOriginalActivityStatus(scope.row) || '') && canApproveActivity(scope.row)"
>
<el-button
link
@@ -661,29 +684,8 @@
ref="membersDialogRef"
/>
<!-- 审批对话框 -->
<el-dialog v-model="approveDialog.visible" title="审批活动" width="500px" append-to-body>
<el-form ref="approveFormRef" :model="approveForm" label-width="100px">
<el-form-item label="活动名称">
<el-input v-model="approveForm.activityName" readonly />
</el-form-item>
<el-form-item label="审批结果" prop="approvalResult">
<el-radio-group v-model="approveForm.approvalResult">
<el-radio label="approve">通过</el-radio>
<el-radio label="reject">驳回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="驳回原因" prop="rejectReason" v-if="approveForm.approvalResult === 'reject'">
<el-input v-model="approveForm.rejectReason" type="textarea" placeholder="请输入驳回原因" :rows="3" :maxlength="200" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="approveDialog.visible = false">取消</el-button>
<el-button type="primary" @click="confirmApprove" :loading="approveLoading">确认审批</el-button>
</div>
</template>
</el-dialog>
<!-- 新的审批对话框组件 -->
<ApprovalDialog ref="approvalDialogRef" @success="handleApprovalSuccess" />
</div>
</template>
@@ -712,9 +714,11 @@ import ActivityRatingView from './components/ActivityRatingView.vue';
import ActivityDetailView from './components/ActivityDetailView.vue';
import ActivityMembersDialog from './components/ActivityMembersDialog.vue';
import ActivityRegistrationView from './components/ActivityRegistrationView.vue'; // Added import
import ApprovalDialog from './components/ApprovalDialog.vue';
import { AdvisorVO } from '@/components/AdvisorManager/index.vue';
import { useUserStore } from '@/store/modules/user';
import { checkClubActivityPermission, type ClubActivityPermissionResult } from '@/api/stgl/clubPermission';
import { getPendingActivityAudits } from '@/api/stgl/activity/audit';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
@@ -752,8 +756,8 @@ const queryFormRef = ref<ElFormInstance>();
const activityFormRef = ref<ElFormInstance>();
const copyFormRef = ref<ElFormInstance>();
const summaryFormRef = ref<ElFormInstance>();
const approveFormRef = ref<ElFormInstance>();
const membersDialogRef = ref();
const approvalDialogRef = ref<InstanceType<typeof ApprovalDialog>>();
const dialog = reactive<DialogOption>({
visible: false,
@@ -780,23 +784,11 @@ const membersDialog = reactive<DialogOption>({
title: ''
});
// 审批对话框
const approveDialog = reactive<DialogOption>({
visible: false,
title: ''
});
// 审批表单
const approveForm = reactive({
id: undefined as string | number | undefined,
activityName: '',
approvalResult: 'approve' as 'approve' | 'reject',
rejectReason: ''
});
// 待审批活动缓存(用于权限判断)
const pendingAuditsCache = ref<Map<number, any>>(new Map());
const copyLoading = ref(false);
const summaryLoading = ref(false);
const approveLoading = ref(false);
// 关联社团禁用状态控制
const isRelatedClubDisabled = ref(false);
@@ -1005,12 +997,6 @@ const reset = () => {
// 重置关联社团禁用状态
isRelatedClubDisabled.value = false;
// 重置审批表单
approveForm.id = undefined;
approveForm.activityName = '';
approveForm.approvalResult = 'approve';
approveForm.rejectReason = '';
// 更新formKey以强制重新渲染组件
formKey.value++;
@@ -1689,6 +1675,12 @@ const handleRelatedDeptsChange = (depts: DeptOption[] | null) => {
}
};
/** 查询表单 - 关联部门变化处理 */
const handleQueryRelatedDeptChange = (dept: DeptOption | null) => {
queryParams.value.relatedDept = dept;
queryParams.value.relatedDeptId = dept?.deptId;
};
/** 关联社团变化处理 */
const handleRelatedClubChange = (club: ClubOption | null) => {
form.value.relatedClub = club;
@@ -1776,18 +1768,135 @@ const handleSubmit = async (row?: StglActivityVO) => {
}
};
/** 审批按钮操作 */
/** 提交活动审批 */
const handleSubmitApproval = async (row?: StglActivityVO) => {
const activity = row || selectedActivity.value;
if (!activity) return;
// 调试信息:打印关联社团ID
console.log('🔍 提交审批 - 活动信息:', {
activityId: activity.id,
activityName: activity.activityName,
relatedClubId: activity.relatedClubId,
relatedClubIdType: typeof activity.relatedClubId,
relatedClubName: activity.relatedClubName
});
// 前置检查:必须关联社团
if (!activity.relatedClubId) {
proxy?.$modal.msgError('该活动未关联社团,无法提交审批。请先编辑活动并选择关联社团。');
return;
}
try {
await proxy?.$modal.confirm(`是否确认提交活动"${activity.activityName}"进行审批?`);
// 发送必填字段 + 业务必需字段(满足后端验证和业务逻辑)
// 注意:relatedClubId 保持字符串类型,避免雪花ID精度丢失
// 后端 Bo 已修改为接受 String 类型,在 Service 层转换为 Long
const requestData = {
id: activity.id,
activityName: activity.activityName,
activityType: activity.activityType,
startTime: activity.startTime,
endTime: activity.endTime,
relatedClubId: String(activity.relatedClubId), // 保持字符串类型
activityStatus: '8' // 8-已提交审批
};
console.log('📤 发送审批请求:', requestData);
await updateStglActivity(requestData);
proxy?.$modal.msgSuccess('提交审批成功');
await getList();
// 重新加载待审批权限
await loadPendingAudits();
} catch (error) {
if (error !== 'cancel') {
console.error('❌ 提交审批失败:', error);
proxy?.$modal.msgError('提交审批失败');
}
}
};
/** 撤回活动审批 */
const handleWithdrawApproval = async (row?: StglActivityVO) => {
const activity = row || selectedActivity.value;
if (!activity) return;
try {
await proxy?.$modal.confirm(`是否确认撤回活动"${activity.activityName}"的审批?`);
// 发送必填字段(满足后端EditGroup验证)
await updateStglActivity({
id: activity.id,
activityName: activity.activityName,
activityType: activity.activityType,
startTime: activity.startTime,
endTime: activity.endTime,
activityStatus: '0' // 0-草稿
});
proxy?.$modal.msgSuccess('撤回审批成功');
await getList();
// 重新加载待审批权限
await loadPendingAudits();
} catch (error) {
if (error !== 'cancel') {
console.error('撤回审批失败:', error);
proxy?.$modal.msgError('撤回审批失败');
}
}
};
/** 审批活动 */
const handleApprove = async (row?: StglActivityVO) => {
const activity = row || selectedActivity.value;
if (!activity) return;
// 显示审批对话框
approveDialog.visible = true;
approveDialog.title = `审批活动 - ${activity.activityName}`;
approveForm.id = activity.id;
approveForm.activityName = activity.activityName || '';
approveForm.approvalResult = 'approve';
approveForm.rejectReason = '';
// 从缓存中获取审批记录
const auditRecord = pendingAuditsCache.value.get(Number(activity.id));
if (!auditRecord) {
proxy?.$modal.msgWarning('无法获取审批记录信息');
return;
}
// 打开新的审批对话框
approvalDialogRef.value?.open(auditRecord.id, activity.id);
};
/** 审批成功回调 */
const handleApprovalSuccess = async () => {
await getList();
// 重新加载待审批权限
await loadPendingAudits();
};
/** 判断是否可以审批活动 */
const canApproveActivity = (activity: StglActivityVO | null): boolean => {
if (!activity) return false;
return pendingAuditsCache.value.has(Number(activity.id));
};
/** 加载待审批权限 */
const loadPendingAudits = async () => {
try {
const userId = userStore.userId;
const res = await getPendingActivityAudits(userId);
const audits = res.data || [];
// 构建待审批缓存
const cache = new Map<number, any>();
audits.forEach((audit: any) => {
cache.set(Number(audit.activityId), audit);
});
pendingAuditsCache.value = cache;
console.log('待审批活动缓存:', pendingAuditsCache.value);
} catch (error) {
console.error('加载待审批权限失败:', error);
}
};
/** 取消活动按钮操作 */
@@ -1815,63 +1924,6 @@ const handleCancel = async (row?: StglActivityVO) => {
}
};
/** 确认审批 */
const confirmApprove = async () => {
if (!approveFormRef.value) return;
approveLoading.value = true;
try {
const activity = activityList.value.find((item) => item.id === approveForm.id);
if (!activity) {
proxy?.$modal.msgError('活动信息不存在');
return;
}
let newStatus: string;
let successMessage: string;
if (approveForm.approvalResult === 'approve') {
// 审批通过,根据时间比较确定状态
const now = new Date();
const startTime = new Date(activity.startTime);
const endTime = new Date(activity.endTime);
if (now < startTime) {
newStatus = '3'; // 已通过(活动还未开始)
} else if (now >= startTime && now <= endTime) {
newStatus = '4'; // 进行中(活动正在进行)
} else {
newStatus = '5'; // 已结束(活动已结束)
}
successMessage = '审批通过';
} else {
// 审批驳回
if (!approveForm.rejectReason.trim()) {
proxy?.$modal.msgWarning('请输入驳回原因');
return;
}
newStatus = '7'; // 已驳回
successMessage = '审批驳回';
}
// 调用后端接口更新活动状态
// 传递完整的活动数据,避免后端验证失败
await updateStglActivity({
...activity,
activityStatus: newStatus
});
proxy?.$modal.msgSuccess(successMessage);
approveDialog.visible = false;
await getList();
} catch (error) {
console.error('审批失败:', error);
proxy?.$modal.msgError('审批失败');
} finally {
approveLoading.value = false;
}
};
/** 格式化日期 */
const formatDate = (dateStr: string): string => {
if (!dateStr) return '';
@@ -1989,6 +2041,28 @@ const getOriginalActivityStatus = (activity: StglActivityVO | null): string => {
return activity.originalActivityStatus || activity.activityStatus || '';
};
// 监听对话框打开,在新增模式下自动设置报名开始时间为当前时间
watch(
() => dialog.visible,
(isVisible, wasVisible) => {
// 当对话框从关闭变为打开,且是新增模式(无ID)时,自动设置报名开始时间
if (isVisible && !wasVisible && !form.value.id) {
nextTick(() => {
// 获取当前时间并格式化为 YYYY-MM-DD HH:mm:ss
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
form.value.registrationStart = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
});
}
}
);
// 监听活动开始时间变化,自动更新报名结束时间
watch(
() => form.value.startTime,
@@ -2019,9 +2093,11 @@ watch(
onMounted(() => {
initUrlParams();
getList();
// 加载待审批权限
loadPendingAudits();
// 如果有关联社团,检查权限
if (queryParams.value.relatedClub?.relatedClubId && queryParams.value.relatedClub?.relatedType === 1) {
checkClubPermission(Number(queryParams.value.relatedClub.relatedClubId));
checkClubPermission(queryParams.value.relatedClub.relatedClubId);
}
});
@@ -2030,7 +2106,7 @@ watch(
() => queryParams.value.relatedClub,
(newRelatedClub) => {
if (newRelatedClub?.relatedClubId && newRelatedClub.relatedType === 1) {
checkClubPermission(Number(newRelatedClub.relatedClubId));
checkClubPermission(newRelatedClub.relatedClubId);
} else {
clubPermissionInfo.value = null;
}