2
0

2 Commity dfd5f361fa ... 8f471dff4b

Autor SHA1 Správa Dátum
  李旭明 8f471dff4b Merge branch 'master' of http://47.99.164.66:3000/hzdyjs-xg/all-project 11 mesiacov pred
  李旭明 03c80657ac lxm 11 mesiacov pred
23 zmenil súbory, kde vykonal 2234 pridanie a 139 odobranie
  1. 62 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/config/AnnualReviewConfig.java
  2. 111 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityAuditController.java
  3. 4 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityController.java
  4. 10 2
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglActivity.java
  5. 91 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglActivityAudit.java
  6. 61 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglActivityApprovalBo.java
  7. 2 2
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglActivityBo.java
  8. 106 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglActivityAuditVo.java
  9. 2 2
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglActivityVo.java
  10. 42 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglActivityAuditMapper.java
  11. 94 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglActivityAuditService.java
  12. 4 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglActivityService.java
  13. 388 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityAuditServiceImpl.java
  14. 112 2
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityServiceImpl.java
  15. 1 1
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubAnnualReviewServiceImpl.java
  16. 97 16
      backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubPermissionServiceImpl.java
  17. 7 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/resources/application-stgl.properties
  18. 82 0
      backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityAuditMapper.xml
  19. 131 0
      web/src/api/stgl/activity/audit.ts
  20. 4 0
      web/src/api/stgl/activity/index.ts
  21. 310 0
      web/src/views/stgl/activity/components/ApprovalDialog.vue
  22. 323 0
      web/src/views/stgl/activity/components/ApprovalProcessDialog.vue
  23. 190 114
      web/src/views/stgl/activity/index.vue

+ 62 - 0
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/config/AnnualReviewConfig.java

@@ -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());
+    }
+}

+ 111 - 0
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityAuditController.java

@@ -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));
+    }
+}
+

+ 4 - 0
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglActivityController.java

@@ -156,4 +156,8 @@ public class StglActivityController extends BaseController {
         return R.ok(stglActivityService.getActivityRatings(id));
     }
 
+    // 注意:提交审批和撤回审批功能已改为通过修改接口(edit)更新活动状态实现
+    // 前端调用 updateStglActivity 修改状态为8(已提交)或0(草稿)
+    // 后端在 updateByBo 方法中监听状态变化,自动创建或删除审批记录
+
 } 

+ 10 - 2
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglActivity.java

@@ -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-志愿者队伍)

+ 91 - 0
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglActivityAudit.java

@@ -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;
+}
+

+ 61 - 0
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglActivityApprovalBo.java

@@ -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;
+    }
+}
+

+ 2 - 2
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglActivityBo.java

@@ -97,9 +97,9 @@ public class StglActivityBo extends BaseEntity {
     private String relatedDeptNames;
 
     /**
-     * 关联社团ID
+     * 关联社团ID(String类型以避免雪花ID精度丢失)
      */
-    private Long relatedClubId;
+    private String relatedClubId;
 
     /**
      * 关联类型 (1: 社团, 2: 志愿者队伍)

+ 106 - 0
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglActivityAuditVo.java

@@ -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;
+}
+

+ 2 - 2
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglActivityVo.java

@@ -110,9 +110,9 @@ public class StglActivityVo implements Serializable {
     private String relatedDeptNames;
 
     /**
-     * 关联社团ID
+     * 关联社团ID(String类型以避免雪花ID精度丢失)
      */
-    private Long relatedClubId;
+    private String relatedClubId;
 
     /**
      * 关联类型(1-社团 2-志愿者队伍)

+ 42 - 0
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglActivityAuditMapper.java

@@ -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);
+}
+

+ 94 - 0
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglActivityAuditService.java

@@ -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);
+}
+

+ 4 - 0
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglActivityService.java

@@ -123,4 +123,8 @@ public interface IStglActivityService extends IService<StglActivity> {
      */
     List<ActivityRatingVo> getActivityRatings(Long activityId);
 
+    // 注意:提交审批和撤回审批功能已改为通过 updateByBo 方法监听状态变化实现
+    // 前端调用 updateStglActivity 修改状态为8(已提交)或0(草稿)
+    // 后端在 updateByBo 方法中自动创建或删除审批记录
+
 } 

+ 388 - 0
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityAuditServiceImpl.java

@@ -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"; // 进行中
+        }
+    }
+}
+

+ 112 - 2
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityServiceImpl.java

@@ -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 方法中,通过监听状态变化自动处理审批记录
 } 

+ 1 - 1
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubAnnualReviewServiceImpl.java

@@ -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表示志愿团队)
         
         // 设置时间范围查询条件(查询活动开始时间在该年度内的活动)

+ 97 - 16
backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubPermissionServiceImpl.java

@@ -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();
     }
 
     /**

+ 7 - 0
backend/ruoyi-modules/ruoyi-stgl/src/main/resources/application-stgl.properties

@@ -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

+ 82 - 0
backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityAuditMapper.xml

@@ -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 - 0
web/src/api/stgl/activity/audit.ts

@@ -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 - 0
web/src/api/stgl/activity/index.ts

@@ -146,3 +146,7 @@ export const getActivityRatings = (activityId: string | number): AxiosPromise<Ac
     method: 'get'
   });
 };
+
+// 注意:submitActivityForApproval 和 withdrawActivityApproval 方法已废弃
+// 现在直接使用 updateStglActivity 修改活动状态为8(已提交)或0(草稿)
+// 后端会在 updateByBo 方法中监听状态变化,自动创建或删除审批记录

+ 310 - 0
web/src/views/stgl/activity/components/ApprovalDialog.vue

@@ -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>
+

+ 323 - 0
web/src/views/stgl/activity/components/ApprovalProcessDialog.vue

@@ -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>
+

+ 190 - 114
web/src/views/stgl/activity/index.vue

@@ -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,99 +1768,159 @@ const handleSubmit = async (row?: StglActivityVO) => {
   }
 };
 
-/** 审批按钮操作 */
-const handleApprove = async (row?: StglActivityVO) => {
+/** 提交活动审批 */
+const handleSubmitApproval = 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 = '';
+  // 调试信息:打印关联社团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 handleCancel = async (row?: StglActivityVO) => {
+/** 撤回活动审批 */
+const handleWithdrawApproval = async (row?: StglActivityVO) => {
   const activity = row || selectedActivity.value;
   if (!activity) return;
 
   try {
-    await proxy?.$modal.confirm(`是否确认取消活动"${activity.activityName}"?取消后将无法恢复。`);
-
-    // 调用后端接口更新活动状态为6(已取消)
-    // 传递完整的活动数据,避免后端验证失败
+    await proxy?.$modal.confirm(`是否确认撤回活动"${activity.activityName}"的审批?`);
+    
+    // 发送必填字段(满足后端EditGroup验证)
     await updateStglActivity({
-      ...activity,
-      activityStatus: '6'
+      id: activity.id,
+      activityName: activity.activityName,
+      activityType: activity.activityType,
+      startTime: activity.startTime,
+      endTime: activity.endTime,
+      activityStatus: '0'  // 0-草稿
     });
-
-    proxy?.$modal.msgSuccess('取消活动成功');
+    
+    proxy?.$modal.msgSuccess('撤回审批成功');
     await getList();
+    // 重新加载待审批权限
+    await loadPendingAudits();
   } catch (error) {
     if (error !== 'cancel') {
-      console.error('取消活动失败:', error);
-      proxy?.$modal.msgError('取消活动失败');
+      console.error('撤回审批失败:', error);
+      proxy?.$modal.msgError('撤回审批失败');
     }
   }
 };
 
-/** 确认审批 */
-const confirmApprove = async () => {
-  if (!approveFormRef.value) return;
+/** 审批活动 */
+const handleApprove = async (row?: StglActivityVO) => {
+  const activity = row || selectedActivity.value;
+  if (!activity) return;
 
-  approveLoading.value = true;
-  try {
-    const activity = activityList.value.find((item) => item.id === approveForm.id);
-    if (!activity) {
-      proxy?.$modal.msgError('活动信息不存在');
-      return;
-    }
+  // 从缓存中获取审批记录
+  const auditRecord = pendingAuditsCache.value.get(Number(activity.id));
+  if (!auditRecord) {
+    proxy?.$modal.msgWarning('无法获取审批记录信息');
+    return;
+  }
 
-    let newStatus: string;
-    let successMessage: string;
+  // 打开新的审批对话框
+  approvalDialogRef.value?.open(auditRecord.id, activity.id);
+};
 
-    if (approveForm.approvalResult === 'approve') {
-      // 审批通过,根据时间比较确定状态
-      const now = new Date();
-      const startTime = new Date(activity.startTime);
-      const endTime = new Date(activity.endTime);
+/** 审批成功回调 */
+const handleApprovalSuccess = async () => {
+  await getList();
+  // 重新加载待审批权限
+  await loadPendingAudits();
+};
 
-      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 = '审批驳回';
-    }
+/** 判断是否可以审批活动 */
+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);
+  }
+};
 
-    // 调用后端接口更新活动状态
+/** 取消活动按钮操作 */
+const handleCancel = async (row?: StglActivityVO) => {
+  const activity = row || selectedActivity.value;
+  if (!activity) return;
+
+  try {
+    await proxy?.$modal.confirm(`是否确认取消活动"${activity.activityName}"?取消后将无法恢复。`);
+
+    // 调用后端接口更新活动状态为6(已取消)
     // 传递完整的活动数据,避免后端验证失败
     await updateStglActivity({
       ...activity,
-      activityStatus: newStatus
+      activityStatus: '6'
     });
 
-    proxy?.$modal.msgSuccess(successMessage);
-    approveDialog.visible = false;
+    proxy?.$modal.msgSuccess('取消活动成功');
     await getList();
   } catch (error) {
-    console.error('审批失败:', error);
-    proxy?.$modal.msgError('审批失败');
-  } finally {
-    approveLoading.value = false;
+    if (error !== 'cancel') {
+      console.error('取消活动失败:', error);
+      proxy?.$modal.msgError('取消活动失败');
+    }
   }
 };
 
@@ -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;
     }