From de3ed43b6d51e4ead5ee50c06cd99be3e46c3b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=97=AD=E6=98=8E?= Date: Tue, 9 Sep 2025 11:35:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B9=B4=E5=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StglClubAdvisorHistoryController.java | 147 +++ .../StglClubAnnualReviewController.java | 213 ++++ .../StglClubPermissionController.java | 200 ++++ .../org/dromara/stgl/domain/StglActivity.java | 24 + .../stgl/domain/StglClubAdvisorHistory.java | 75 ++ .../stgl/domain/StglClubAnnualReview.java | 130 +++ .../domain/StglClubAnnualReviewActivity.java | 94 ++ .../domain/StglClubAnnualReviewMember.java | 69 ++ .../stgl/domain/bo/StglActivityBo.java | 21 +- .../domain/bo/StglClubAdvisorHistoryBo.java | 85 ++ .../domain/bo/StglClubAnnualReviewBo.java | 163 +++ .../vo/ClubActivityPermissionResult.java | 95 ++ .../dromara/stgl/domain/vo/MyActivityVo.java | 10 + .../stgl/domain/vo/StglActivityVo.java | 21 + .../dromara/stgl/domain/vo/StglClassVo.java | 15 + .../domain/vo/StglClubAdvisorHistoryVo.java | 119 ++ .../vo/StglClubAnnualReviewActivityVo.java | 111 ++ .../domain/vo/StglClubAnnualReviewVo.java | 156 +++ .../mapper/StglClubAdvisorHistoryMapper.java | 54 + .../StglClubAnnualReviewActivityMapper.java | 27 + .../mapper/StglClubAnnualReviewMapper.java | 15 + .../StglClubAnnualReviewMemberMapper.java | 27 + .../IStglClubAdvisorHistoryService.java | 130 +++ .../service/IStglClubAnnualReviewService.java | 89 ++ .../service/IStglClubPermissionService.java | 55 + .../service/impl/StglActivityServiceImpl.java | 53 +- .../StglClubAdvisorHistoryServiceImpl.java | 205 ++++ .../impl/StglClubAnnualReviewServiceImpl.java | 664 +++++++++++ .../impl/StglClubNotificationScheduler.java | 263 +++++ .../impl/StglClubPermissionServiceImpl.java | 240 ++++ .../service/impl/StglClubServiceImpl.java | 107 ++ .../main/resources/mapper/StglMyMapper.xml | 2 + .../mapper/stgl/StglActivityMapper.xml | 108 +- .../resources/mapper/stgl/StglClassMapper.xml | 137 +++ .../stgl/StglClubAdvisorHistoryMapper.xml | 68 ++ .../StglClubAnnualReviewActivityMapper.xml | 44 + .../stgl/StglClubAnnualReviewMapper.xml | 66 ++ .../stgl/StglClubAnnualReviewMemberMapper.xml | 40 + .../database/社团管理/社团管理数据库设计.md | 3 + web/src/api/stgl/activity/types.ts | 60 + .../api/stgl/activityregisterhall/types.ts | 20 + web/src/api/stgl/club/advisor-history.ts | 276 +++++ web/src/api/stgl/club/annualReview/index.ts | 222 ++++ web/src/api/stgl/club/annualReview/type.ts | 323 ++++++ web/src/api/stgl/clubPermission.ts | 93 ++ web/src/api/stgl/my/types.ts | 4 + web/src/api/stgl/stclass/types.ts | 15 + .../components/AdvisorHistoryDialog/index.vue | 399 +++++++ web/src/components/AdvisorManager/index.vue | 96 +- .../DeptSelector/MultiDeptSelector.vue | 136 +++ web/src/components/DeptSelector/index.vue | 100 ++ web/src/components/FileUpload/index.vue | 19 +- web/src/components/TeacherSelector/index.vue | 3 + .../components/ActivityDetailView.vue | 68 +- web/src/views/stgl/activity/hall/index.vue | 92 +- web/src/views/stgl/activity/index.vue | 505 ++++++-- .../stgl/club/info/annualReview/detail.vue | 527 +++++++++ .../stgl/club/info/annualReview/form.vue | 1014 +++++++++++++++++ .../stgl/club/info/annualReview/index.vue | 386 +++++++ .../club/info/components/JoinedMembersTab.vue | 11 +- web/src/views/stgl/club/info/index.vue | 252 ++-- web/src/views/stgl/my/activity/index.vue | 233 +++- web/src/views/stgl/stclass/index.vue | 53 +- 63 files changed, 8724 insertions(+), 328 deletions(-) create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubAdvisorHistoryController.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubAnnualReviewController.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubPermissionController.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAdvisorHistory.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAnnualReview.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAnnualReviewActivity.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAnnualReviewMember.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglClubAdvisorHistoryBo.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglClubAnnualReviewBo.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/ClubActivityPermissionResult.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClubAdvisorHistoryVo.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClubAnnualReviewActivityVo.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClubAnnualReviewVo.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAdvisorHistoryMapper.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAnnualReviewActivityMapper.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAnnualReviewMapper.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAnnualReviewMemberMapper.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglClubAdvisorHistoryService.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglClubAnnualReviewService.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglClubPermissionService.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubAdvisorHistoryServiceImpl.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubAnnualReviewServiceImpl.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubNotificationScheduler.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubPermissionServiceImpl.java create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAdvisorHistoryMapper.xml create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAnnualReviewActivityMapper.xml create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAnnualReviewMapper.xml create mode 100644 backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAnnualReviewMemberMapper.xml create mode 100644 web/src/api/stgl/club/advisor-history.ts create mode 100644 web/src/api/stgl/club/annualReview/index.ts create mode 100644 web/src/api/stgl/club/annualReview/type.ts create mode 100644 web/src/api/stgl/clubPermission.ts create mode 100644 web/src/components/AdvisorHistoryDialog/index.vue create mode 100644 web/src/components/DeptSelector/MultiDeptSelector.vue create mode 100644 web/src/components/DeptSelector/index.vue create mode 100644 web/src/views/stgl/club/info/annualReview/detail.vue create mode 100644 web/src/views/stgl/club/info/annualReview/form.vue create mode 100644 web/src/views/stgl/club/info/annualReview/index.vue diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubAdvisorHistoryController.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubAdvisorHistoryController.java new file mode 100644 index 00000000..42bb8e1d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubAdvisorHistoryController.java @@ -0,0 +1,147 @@ +package org.dromara.stgl.controller; + +import java.util.List; + +import lombok.RequiredArgsConstructor; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.mybatis.core.page.PageQuery; +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.log.enums.BusinessType; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.stgl.domain.vo.StglClubAdvisorHistoryVo; +import org.dromara.stgl.domain.bo.StglClubAdvisorHistoryBo; +import org.dromara.stgl.service.IStglClubAdvisorHistoryService; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; + +/** + * 社团指导老师任职历史管理 + * + * @author Cursor AI + * @date 2025-01-03 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/stgl/club/advisor/history") +@Tag(name = "社团指导老师任职历史管理", description = "社团指导老师任职历史管理") +public class StglClubAdvisorHistoryController extends BaseController { + + private final IStglClubAdvisorHistoryService stglClubAdvisorHistoryService; + + /** + * 查询社团指导老师任职历史列表 + */ + @Operation(summary = "查询社团指导老师任职历史列表") + @SaCheckPermission("stgl:club:query") + @GetMapping("/list") + public TableDataInfo list(StglClubAdvisorHistoryBo bo, PageQuery pageQuery) { + return stglClubAdvisorHistoryService.queryPageList(bo, pageQuery); + } + + /** + * 导出社团指导老师任职历史列表 + */ + @Operation(summary = "导出社团指导老师任职历史列表") + @SaCheckPermission("stgl:club:export") + @Log(title = "社团指导老师任职历史", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(StglClubAdvisorHistoryBo bo, HttpServletResponse response) { + List list = stglClubAdvisorHistoryService.queryList(bo); + ExcelUtil.exportExcel(list, "社团指导老师任职历史", StglClubAdvisorHistoryVo.class, response); + } + + /** + * 获取社团指导老师任职历史详细信息 + */ + @Operation(summary = "获取社团指导老师任职历史详细信息") + @SaCheckPermission("stgl:club:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { + return R.ok(stglClubAdvisorHistoryService.queryById(id)); + } + + /** + * 查询指定社团的当前在任指导老师 + */ + @Operation(summary = "查询指定社团的当前在任指导老师") + @SaCheckPermission("stgl:club:query") + @GetMapping("/current/{clubId}") + @Parameter(name = "clubId", description = "社团ID", required = true) + public R> getCurrentAdvisors(@PathVariable Long clubId) { + List list = stglClubAdvisorHistoryService.getCurrentAdvisorsByClubId(clubId); + return R.ok(list); + } + + /** + * 查询指定社团的历史指导老师(已离任) + */ + @Operation(summary = "查询指定社团的历史指导老师") + @SaCheckPermission("stgl:club:query") + @GetMapping("/history/{clubId}") + @Parameter(name = "clubId", description = "社团ID", required = true) + public R> getHistoryAdvisors(@PathVariable Long clubId) { + List list = stglClubAdvisorHistoryService.getHistoryAdvisorsByClubId(clubId); + return R.ok(list); + } + + /** + * 查询指定社团的所有指导老师(包含当前和历史) + */ + @Operation(summary = "查询指定社团的所有指导老师") + @SaCheckPermission("stgl:club:query") + @GetMapping("/all/{clubId}") + @Parameter(name = "clubId", description = "社团ID", required = true) + public R> getAllAdvisors(@PathVariable Long clubId) { + List list = stglClubAdvisorHistoryService.getAllAdvisorsByClubId(clubId); + return R.ok(list); + } + + /** + * 新增社团指导老师任职历史 + */ + @Operation(summary = "新增社团指导老师任职历史") + @SaCheckPermission("stgl:club:add") + @Log(title = "社团指导老师任职历史", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody StglClubAdvisorHistoryBo bo) { + return toAjax(stglClubAdvisorHistoryService.insertByBo(bo)); + } + + /** + * 修改社团指导老师任职历史 + */ + @Operation(summary = "修改社团指导老师任职历史") + @SaCheckPermission("stgl:club:edit") + @Log(title = "社团指导老师任职历史", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody StglClubAdvisorHistoryBo bo) { + return toAjax(stglClubAdvisorHistoryService.updateByBo(bo)); + } + + /** + * 删除社团指导老师任职历史 + */ + @Operation(summary = "删除社团指导老师任职历史") + @SaCheckPermission("stgl:club:remove") + @Log(title = "社团指导老师任职历史", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public R remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) { + return toAjax(stglClubAdvisorHistoryService.deleteWithValidByIds(List.of(ids), true)); + } + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubAnnualReviewController.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubAnnualReviewController.java new file mode 100644 index 00000000..437086b7 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubAnnualReviewController.java @@ -0,0 +1,213 @@ +package org.dromara.stgl.controller; + +import java.util.List; + +import lombok.RequiredArgsConstructor; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.dromara.common.idempotent.annotation.RepeatSubmit; +import org.dromara.common.log.annotation.Log; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.mybatis.core.page.PageQuery; +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.log.enums.BusinessType; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.stgl.domain.vo.StglClubAnnualReviewVo; +import org.dromara.stgl.domain.bo.StglClubAnnualReviewBo; +import org.dromara.stgl.service.IStglClubAnnualReviewService; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; + +/** + * 社团年审记录管理 + * + * @author Cursor AI + * @date 2025-01-15 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/stgl/club/annualReview") +@Tag(name = "社团年审记录管理", description = "社团年审记录管理") +public class StglClubAnnualReviewController extends BaseController { + + private final IStglClubAnnualReviewService stglClubAnnualReviewService; + + /** + * 查询社团年审记录列表 + */ + @Operation(summary = "查询社团年审记录列表") + @SaCheckPermission("stgl:clubAnnualReview:list") + @GetMapping("/list") + public TableDataInfo list(StglClubAnnualReviewBo bo, PageQuery pageQuery) { + return stglClubAnnualReviewService.queryPageList(bo, pageQuery); + } + + /** + * 查询我的社团年审记录列表 + */ + @Operation(summary = "查询我的社团年审记录列表") + @SaCheckPermission("stgl:clubAnnualReview:myList") + @GetMapping("/myList") + public TableDataInfo myList(StglClubAnnualReviewBo bo, PageQuery pageQuery) { + return stglClubAnnualReviewService.queryMyPageList(bo, pageQuery); + } + + /** + * 导出社团年审记录列表 + */ + @Operation(summary = "导出社团年审记录列表") + @SaCheckPermission("stgl:clubAnnualReview:export") + @Log(title = "社团年审记录", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(StglClubAnnualReviewBo bo, HttpServletResponse response) { + List list = stglClubAnnualReviewService.queryList(bo); + ExcelUtil.exportExcel(list, "社团年审记录", StglClubAnnualReviewVo.class, response); + } + + /** + * 获取社团年审记录详细信息 + */ + @Operation(summary = "获取社团年审记录详细信息") + @SaCheckPermission("stgl:clubAnnualReview:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") + @PathVariable Long id) { + return R.ok(stglClubAnnualReviewService.queryById(id)); + } + + /** + * 新增社团年审记录 + */ + @Operation(summary = "新增社团年审记录") + @SaCheckPermission("stgl:clubAnnualReview:add") + @Log(title = "社团年审记录", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody StglClubAnnualReviewBo bo) { + return toAjax(stglClubAnnualReviewService.insertByBo(bo)); + } + + /** + * 修改社团年审记录 + */ + @Operation(summary = "修改社团年审记录") + @SaCheckPermission("stgl:clubAnnualReview:edit") + @Log(title = "社团年审记录", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody StglClubAnnualReviewBo bo) { + return toAjax(stglClubAnnualReviewService.updateByBo(bo)); + } + + /** + * 删除社团年审记录 + */ + @Operation(summary = "删除社团年审记录") + @SaCheckPermission("stgl:clubAnnualReview:remove") + @Log(title = "社团年审记录", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public R remove(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] ids) { + return toAjax(stglClubAnnualReviewService.deleteWithValidByIds(List.of(ids), true)); + } + + /** + * 提交年审记录 + */ + @Operation(summary = "提交年审记录") + @SaCheckPermission("stgl:clubAnnualReview:submit") + @Log(title = "社团年审记录", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/submit/{id}") + public R submit(@NotNull(message = "主键不能为空") + @PathVariable Long id) { + return toAjax(stglClubAnnualReviewService.submitReview(id)); + } + + /** + * 撤回年审记录 + */ + @Operation(summary = "撤回年审记录") + @SaCheckPermission("stgl:clubAnnualReview:withdraw") + @Log(title = "社团年审记录", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/withdraw/{id}") + public R withdraw(@NotNull(message = "主键不能为空") + @PathVariable Long id) { + return toAjax(stglClubAnnualReviewService.withdrawReview(id)); + } + + /** + * 审核年审记录 + */ + @Operation(summary = "审核年审记录") + @SaCheckPermission("stgl:clubAnnualReview:audit") + @Log(title = "社团年审记录", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/audit") + public R audit(@RequestParam @NotNull(message = "年审记录ID不能为空") Long id, + @RequestParam @NotBlank(message = "审核结果不能为空") String auditResult, + @RequestParam String auditOpinion, + @RequestParam @NotBlank(message = "审核类型不能为空") String auditType) { + return toAjax(stglClubAnnualReviewService.auditReview(id, auditResult, auditOpinion, auditType)); + } + + /** + * 检查是否可以创建年审记录 + */ + @Operation(summary = "检查是否可以创建年审记录") + @SaCheckPermission("stgl:clubAnnualReview:check") + @GetMapping("/check") + public R checkCanCreate(@RequestParam @NotNull(message = "社团ID不能为空") Long clubId, + @RequestParam @NotNull(message = "年审年度不能为空") Integer reviewYear) { + return R.ok(stglClubAnnualReviewService.checkCanCreateReview(clubId, reviewYear)); + } + + /** + * 获取年审状态选项 + */ + @Operation(summary = "获取年审状态选项") + @GetMapping("/statusOptions") + public R> getStatusOptions() { + List options = List.of("0", "1", "2", "3", "4"); + return R.ok(options); + } + + /** + * 根据社团ID和年审年度获取该年度的活动数据 + */ + @Operation(summary = "根据社团ID和年审年度获取该年度的活动数据") + @Parameters({ + @Parameter(name = "clubId", description = "社团ID", required = true), + @Parameter(name = "reviewYear", description = "年审年度", required = true) + }) + @SaCheckPermission("stgl:clubAnnualReview:query") + @GetMapping("/clubActivities") + public R> getClubActivities( + @RequestParam @NotNull(message = "社团ID不能为空") Long clubId, + @RequestParam @NotNull(message = "年审年度不能为空") Integer reviewYear) { + return R.ok(stglClubAnnualReviewService.getClubActivitiesByReviewYear(clubId, reviewYear)); + } + + /** + * 根据年审记录ID获取对应的活动数据 + */ + @Operation(summary = "根据年审记录ID获取对应的活动数据") + @Parameter(name = "id", description = "年审记录ID", required = true) + @SaCheckPermission("stgl:clubAnnualReview:query") + @GetMapping("/{id}/activities") + public R> getAnnualReviewActivities( + @NotNull(message = "年审记录ID不能为空") @PathVariable Long id) { + return R.ok(stglClubAnnualReviewService.getAnnualReviewActivities(id)); + } + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubPermissionController.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubPermissionController.java new file mode 100644 index 00000000..4c2565c9 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/controller/StglClubPermissionController.java @@ -0,0 +1,200 @@ +package org.dromara.stgl.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.domain.R; +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.vo.ClubActivityPermissionResult; +import org.dromara.stgl.service.IStglClubPermissionService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 社团权限管理Controller + * + * @author Cursor AI + * @date 2025-01-15 + */ +@Slf4j +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/stgl/club/permission") +public class StglClubPermissionController extends BaseController { + + private final IStglClubPermissionService clubPermissionService; + + /** + * 检查社团活动权限 + * 前端每次进入活动管理页面都会调用这个接口 + * + * @param clubId 社团ID + * @return 权限检查结果 + */ + @SaCheckPermission("stgl:club:list") + @GetMapping("/checkActivityPermission/{clubId}") + @Log(title = "检查社团活动权限", businessType = BusinessType.OTHER) + public R checkActivityPermission(@PathVariable Long clubId) { + log.info("检查社团活动权限请求,clubId: {}", clubId); + + if (clubId == null || clubId <= 0) { + return R.fail("社团ID不能为空"); + } + + try { + ClubActivityPermissionResult result = clubPermissionService.checkClubActivityPermission(clubId); + log.info("社团ID: {} 权限检查结果: {}", clubId, result.getStatus()); + return R.ok(result); + } catch (Exception e) { + log.error("检查社团活动权限失败,clubId: {}", clubId, e); + return R.fail("检查权限失败: " + e.getMessage()); + } + } + + /** + * 批量检查多个社团的权限状态 + * 用于列表页面显示多个社团的权限状态 + * + * @param clubIds 社团ID列表 + * @return 权限检查结果映射 + */ + @SaCheckPermission("stgl:club:list") + @PostMapping("/batchCheckPermission") + @Log(title = "批量检查社团权限", businessType = BusinessType.OTHER) + public R> batchCheckPermission(@RequestBody List clubIds) { + log.info("批量检查社团权限请求,社团数量: {}", clubIds != null ? clubIds.size() : 0); + + if (clubIds == null || clubIds.isEmpty()) { + return R.fail("社团ID列表不能为空"); + } + + if (clubIds.size() > 100) { + return R.fail("批量检查数量不能超过100个"); + } + + try { + Map results = + clubPermissionService.batchCheckClubActivityPermission(clubIds); + log.info("批量权限检查完成,处理社团数量: {}", results.size()); + return R.ok(results); + } catch (Exception e) { + log.error("批量检查社团权限失败,clubIds: {}", clubIds, e); + return R.fail("批量检查权限失败: " + e.getMessage()); + } + } + + /** + * 检查社团是否在年审窗口期内 + * + * @param clubId 社团ID + * @return 是否在年审窗口期 + */ + @SaCheckPermission("stgl:club:list") + @GetMapping("/isInAnnualReviewWindow/{clubId}") + public R isInAnnualReviewWindow(@PathVariable Long clubId) { + log.debug("检查社团是否在年审窗口期,clubId: {}", clubId); + + if (clubId == null || clubId <= 0) { + return R.fail("社团ID不能为空"); + } + + try { + boolean inWindow = clubPermissionService.isInAnnualReviewWindow(clubId); + return R.ok(inWindow); + } catch (Exception e) { + log.error("检查年审窗口期失败,clubId: {}", clubId, e); + return R.fail("检查失败: " + e.getMessage()); + } + } + + /** + * 获取社团距离下次年审的天数 + * + * @param clubId 社团ID + * @return 距离年审的天数 + */ + @SaCheckPermission("stgl:club:list") + @GetMapping("/getDaysUntilNextAnnualReview/{clubId}") + public R getDaysUntilNextAnnualReview(@PathVariable Long clubId) { + log.debug("获取社团距离年审天数,clubId: {}", clubId); + + if (clubId == null || clubId <= 0) { + return R.fail("社团ID不能为空"); + } + + try { + long days = clubPermissionService.getDaysUntilNextAnnualReview(clubId); + return R.ok(days); + } catch (Exception e) { + log.error("获取距离年审天数失败,clubId: {}", clubId, e); + return R.fail("获取失败: " + e.getMessage()); + } + } + + /** + * 检查社团是否已在当前年审期内提交年审 + * + * @param clubId 社团ID + * @return 是否已提交年审 + */ + @SaCheckPermission("stgl:club:list") + @GetMapping("/hasSubmittedCurrentYearAnnualReview/{clubId}") + public R hasSubmittedCurrentYearAnnualReview(@PathVariable Long clubId) { + log.debug("检查社团是否已提交年审,clubId: {}", clubId); + + if (clubId == null || clubId <= 0) { + return R.fail("社团ID不能为空"); + } + + try { + boolean hasSubmitted = clubPermissionService.hasSubmittedCurrentYearAnnualReview(clubId); + return R.ok(hasSubmitted); + } catch (Exception e) { + log.error("检查年审提交状态失败,clubId: {}", clubId, e); + return R.fail("检查失败: " + e.getMessage()); + } + } + + /** + * 获取社团权限详细信息(包含所有相关信息的综合接口) + * + * @param clubId 社团ID + * @return 权限详细信息 + */ + @SaCheckPermission("stgl:club:list") + @GetMapping("/getPermissionDetails/{clubId}") + @Log(title = "获取社团权限详情", businessType = BusinessType.OTHER) + public R> getPermissionDetails(@PathVariable Long clubId) { + log.info("获取社团权限详情,clubId: {}", clubId); + + if (clubId == null || clubId <= 0) { + return R.fail("社团ID不能为空"); + } + + try { + ClubActivityPermissionResult permissionResult = + clubPermissionService.checkClubActivityPermission(clubId); + boolean inWindow = clubPermissionService.isInAnnualReviewWindow(clubId); + long daysUntilNext = clubPermissionService.getDaysUntilNextAnnualReview(clubId); + boolean hasSubmitted = clubPermissionService.hasSubmittedCurrentYearAnnualReview(clubId); + + Map details = Map.of( + "permissionResult", permissionResult, + "isInAnnualReviewWindow", inWindow, + "daysUntilNextAnnualReview", daysUntilNext, + "hasSubmittedCurrentYearAnnualReview", hasSubmitted + ); + + return R.ok(details); + } catch (Exception e) { + log.error("获取社团权限详情失败,clubId: {}", clubId, e); + return R.fail("获取详情失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglActivity.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglActivity.java index f3525a71..2ed27089 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglActivity.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglActivity.java @@ -82,6 +82,30 @@ public class StglActivity extends TenantEntity { @TableField("poster_url") private String posterUrl; + /** + * 关联部门ID + */ + @TableField("related_dept_id") + private Long relatedDeptId; + + /** + * 关联部门名称 + */ + @TableField("related_dept_name") + private String relatedDeptName; + + /** + * 关联部门ID列表(JSON格式) + */ + @TableField("related_dept_ids") + private String relatedDeptIds; + + /** + * 关联部门名称列表(逗号分隔) + */ + @TableField("related_dept_names") + private String relatedDeptNames; + /** * 关联社团ID */ diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAdvisorHistory.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAdvisorHistory.java new file mode 100644 index 00000000..e7ea1698 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAdvisorHistory.java @@ -0,0 +1,75 @@ +package org.dromara.stgl.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import org.dromara.common.tenant.core.TenantEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 社团指导老师任职历史对象 stgl_club_advisor_history + * + * @author Cursor AI + * @date 2025-01-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("stgl_club_advisor_history") +public class StglClubAdvisorHistory extends TenantEntity { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id") + private Long id; + + /** + * 社团ID + */ + private Long clubId; + + /** + * 指导老师姓名 + */ + private String advisorName; + + /** + * 指导老师部门 + */ + private String advisorDept; + + /** + * 指导老师电话 + */ + private String advisorPhone; + + /** + * 指导老师邮箱 + */ + private String advisorEmail; + + /** + * 指导老师用户ID + */ + private Long advisorUserId; + + /** + * 在任状态:1-当前在任,0-历史(已离任) + */ + private Integer status; + + /** + * 上任时间 + */ + private Date startTime; + + /** + * 离任时间 + */ + private Date endTime; + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAnnualReview.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAnnualReview.java new file mode 100644 index 00000000..8c426740 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAnnualReview.java @@ -0,0 +1,130 @@ +package org.dromara.stgl.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import org.dromara.common.tenant.core.TenantEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 社团年审记录对象 stgl_club_annual_review + * + * @author Cursor AI + * @date 2025-01-15 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("stgl_club_annual_review") +public class StglClubAnnualReview extends TenantEntity { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id") + private Long id; + + /** + * 社团ID + */ + private Long clubId; + + /** + * 年审年度 + */ + private Integer reviewYear; + + /** + * 年审开始日期 + */ + private Date reviewStartDate; + + /** + * 年审结束日期 + */ + private Date reviewEndDate; + + /** + * 年审时间 + */ + private Date reviewTime; + + /** + * 社团名称 + */ + private String clubName; + + /** + * 成立日期 + */ + private Date establishDate; + + /** + * 社长姓名 + */ + private String presidentName; + + /** + * 社团类型 + */ + private String clubType; + + /** + * 指导老师姓名 + */ + private String advisorName; + + /** + * 指导老师部门 + */ + private String advisorDept; + + /** + * 是否有章程 + */ + private String hasCharter; + + /** + * 章程文件URL + */ + private String charterUrl; + + /** + * 是否有资金 + */ + private String hasFunds; + + /** + * 资金数额 + */ + private BigDecimal fundAmount; + + /** + * 成员数量 + */ + private Integer memberCount; + + /** + * 状态(0-草稿 1-已提交 2-审核中 3-审核通过 4-审核不通过) + */ + private String status; + + /** + * 指导老师意见 + */ + private String advisorOpinion; + + /** + * 学院意见 + */ + private String collegeOpinion; + + /** + * 团委意见 + */ + private String youthLeagueOpinion; +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAnnualReviewActivity.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAnnualReviewActivity.java new file mode 100644 index 00000000..8ce08d5b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAnnualReviewActivity.java @@ -0,0 +1,94 @@ +package org.dromara.stgl.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.common.tenant.core.TenantEntity; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.io.Serializable; +import java.util.Date; + +/** + * 社团年审记录活动对象 stgl_club_annual_review_activity + * + * @author Lion Li + * @date 2024-12-31 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("stgl_club_annual_review_activity") +@Schema(description = "社团年审记录活动") +public class StglClubAnnualReviewActivity extends TenantEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id") + @Schema(description = "主键ID") + private Long id; + + /** + * 年审记录ID + */ + @Schema(description = "年审记录ID") + private Long annualReviewId; + + /** + * 活动名称 + */ + @Schema(description = "活动名称") + private String activityName; + + /** + * 活动时间 + */ + @Schema(description = "活动时间") + private Date activityTime; + + /** + * 活动开始时间 + */ + @Schema(description = "活动开始时间") + private Date activityStartTime; + + /** + * 活动结束时间 + */ + @Schema(description = "活动结束时间") + private Date activityEndTime; + + /** + * 活动地点 + */ + @Schema(description = "活动地点") + private String activityLocation; + + /** + * 活动类型 + */ + @Schema(description = "活动类型") + private String activityType; + + /** + * 参与人数 + */ + @Schema(description = "参与人数") + private Integer participantCount; + + /** + * 活动描述 + */ + @Schema(description = "活动描述") + private String activityDescription; + + /** + * 删除标志(0代表存在 1代表删除) + */ + @TableLogic(value = "0", delval = "1") + private Integer deleted; + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAnnualReviewMember.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAnnualReviewMember.java new file mode 100644 index 00000000..74b5dc98 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglClubAnnualReviewMember.java @@ -0,0 +1,69 @@ +package org.dromara.stgl.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.common.tenant.core.TenantEntity; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.io.Serializable; + +/** + * 社团年审记录成员对象 stgl_club_annual_review_member + * + * @author Lion Li + * @date 2024-12-31 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("stgl_club_annual_review_member") +@Schema(description = "社团年审记录成员") +public class StglClubAnnualReviewMember extends TenantEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id") + @Schema(description = "主键ID") + private Long id; + + /** + * 年审记录ID + */ + @Schema(description = "年审记录ID") + private Long annualReviewId; + + /** + * 成员姓名 + */ + @Schema(description = "成员姓名") + private String memberName; + + /** + * 班级 + */ + @Schema(description = "班级") + private String className; + + /** + * 职务 + */ + @Schema(description = "职务") + private String role; + + /** + * 联系方式 + */ + @Schema(description = "联系方式") + private String contactInfo; + + /** + * 删除标志(0代表存在 1代表删除) + */ + @TableLogic(value = "0", delval = "1") + private Integer deleted; + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglActivityBo.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglActivityBo.java index 4273c1a2..df878a3d 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglActivityBo.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglActivityBo.java @@ -76,6 +76,26 @@ public class StglActivityBo extends BaseEntity { */ private String posterUrl; + /** + * 关联部门ID + */ + private Long relatedDeptId; + + /** + * 关联部门名称 + */ + private String relatedDeptName; + + /** + * 关联部门ID列表(JSON格式) + */ + private String relatedDeptIds; + + /** + * 关联部门名称列表(逗号分隔) + */ + private String relatedDeptNames; + /** * 关联社团ID */ @@ -89,7 +109,6 @@ public class StglActivityBo extends BaseEntity { /** * 主办方名称 */ - @NotBlank(message = "主办方名称不能为空", groups = { AddGroup.class, EditGroup.class }) private String organizerName; /** diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglClubAdvisorHistoryBo.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglClubAdvisorHistoryBo.java new file mode 100644 index 00000000..6a941d09 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglClubAdvisorHistoryBo.java @@ -0,0 +1,85 @@ +package org.dromara.stgl.domain.bo; + +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import jakarta.validation.constraints.*; +import org.dromara.stgl.domain.StglClubAdvisorHistory; + +import java.util.Date; + +/** + * 社团指导老师任职历史业务对象 stgl_club_advisor_history + * + * @author Cursor AI + * @date 2025-01-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = StglClubAdvisorHistory.class, reverseConvertGenerate = false) +public class StglClubAdvisorHistoryBo extends BaseEntity { + + /** + * 主键ID + */ + @NotNull(message = "主键ID不能为空", groups = { EditGroup.class }) + private Long id; + + /** + * 社团ID + */ + @NotNull(message = "社团ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long clubId; + + /** + * 指导老师姓名 + */ + @NotBlank(message = "指导老师姓名不能为空", groups = { AddGroup.class, EditGroup.class }) + @Size(max = 100, message = "指导老师姓名长度不能超过100个字符") + private String advisorName; + + /** + * 指导老师部门 + */ + @Size(max = 200, message = "指导老师部门长度不能超过200个字符") + private String advisorDept; + + /** + * 指导老师电话 + */ + @Size(max = 20, message = "指导老师电话长度不能超过20个字符") + private String advisorPhone; + + /** + * 指导老师邮箱 + */ + @Size(max = 100, message = "指导老师邮箱长度不能超过100个字符") + @Email(message = "指导老师邮箱格式不正确") + private String advisorEmail; + + /** + * 指导老师用户ID + */ + private Long advisorUserId; + + /** + * 在任状态:1-当前在任,0-历史(已离任) + */ + @NotNull(message = "在任状态不能为空", groups = { AddGroup.class, EditGroup.class }) + private Integer status; + + /** + * 上任时间 + */ + @NotNull(message = "上任时间不能为空", groups = { AddGroup.class, EditGroup.class }) + private Date startTime; + + /** + * 离任时间 + */ + private Date endTime; + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglClubAnnualReviewBo.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglClubAnnualReviewBo.java new file mode 100644 index 00000000..1036893d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/bo/StglClubAnnualReviewBo.java @@ -0,0 +1,163 @@ +package org.dromara.stgl.domain.bo; + +import org.dromara.stgl.domain.StglClubAnnualReview; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import jakarta.validation.constraints.*; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 社团年审记录业务对象 StglClubAnnualReviewBo + * + * @author Cursor AI + * @date 2025-01-15 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = StglClubAnnualReview.class, reverseConvertGenerate = false) +public class StglClubAnnualReviewBo extends BaseEntity { + + /** + * 主键ID + */ + @NotNull(message = "主键ID不能为空", groups = { EditGroup.class }) + private Long id; + + /** + * 社团ID + */ + @NotNull(message = "社团ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long clubId; + + /** + * 年审年度 + */ + @NotNull(message = "年审年度不能为空", groups = { AddGroup.class, EditGroup.class }) + @Min(value = 2000, message = "年审年度不能小于2000年") + @Max(value = 9999, message = "年审年度不能大于9999年") + private Integer reviewYear; + + /** + * 年审开始日期 + */ + @NotNull(message = "年审开始日期不能为空", groups = { AddGroup.class, EditGroup.class }) + private Date reviewStartDate; + + /** + * 年审结束日期 + */ + @NotNull(message = "年审结束日期不能为空", groups = { AddGroup.class, EditGroup.class }) + private Date reviewEndDate; + + /** + * 年审时间 + */ + @NotNull(message = "年审时间不能为空", groups = { AddGroup.class, EditGroup.class }) + private Date reviewTime; + + /** + * 社团名称 + */ + @NotBlank(message = "社团名称不能为空", groups = { AddGroup.class, EditGroup.class }) + @Size(max = 100, message = "社团名称长度不能超过100个字符") + private String clubName; + + /** + * 成立日期 + */ + private Date establishDate; + + /** + * 社长姓名 + */ + @Size(max = 50, message = "社长姓名长度不能超过50个字符") + private String presidentName; + + /** + * 社团类型 + */ + @Size(max = 20, message = "社团类型长度不能超过20个字符") + private String clubType; + + /** + * 指导老师姓名 + */ + @Size(max = 50, message = "指导老师姓名长度不能超过50个字符") + private String advisorName; + + /** + * 指导老师部门 + */ + @Size(max = 100, message = "指导老师部门长度不能超过100个字符") + private String advisorDept; + + /** + * 是否有章程 + */ + @Pattern(regexp = "^[YN]$", message = "是否有章程必须是Y或N") + private String hasCharter; + + /** + * 章程文件URL + */ + @Size(max = 500, message = "章程文件URL长度不能超过500个字符") + private String charterUrl; + + /** + * 是否有资金 + */ + @Pattern(regexp = "^[YN]$", message = "是否有资金必须是Y或N") + private String hasFunds; + + /** + * 资金数额 + */ + @DecimalMin(value = "0.00", message = "资金数额不能小于0") + @DecimalMax(value = "99999999.99", message = "资金数额不能超过99999999.99") + private BigDecimal fundAmount; + + /** + * 成员数量 + */ + @Min(value = 0, message = "成员数量不能小于0") + @Max(value = 9999, message = "成员数量不能超过9999") + private Integer memberCount; + + /** + * 状态(0-草稿 1-已提交 2-审核中 3-审核通过 4-审核不通过) + */ + @Pattern(regexp = "^[0-4]$", message = "状态值必须是0-4之间的数字") + private String status; + + /** + * 指导老师意见 + */ + private String advisorOpinion; + + /** + * 学院意见 + */ + private String collegeOpinion; + + /** + * 团委意见 + */ + private String youthLeagueOpinion; + + /** + * 成员信息JSON(前端传入) + */ + private String membersJson; + + /** + * 活动信息JSON(前端传入) + */ + private String activitiesJson; + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/ClubActivityPermissionResult.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/ClubActivityPermissionResult.java new file mode 100644 index 00000000..0dd91173 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/ClubActivityPermissionResult.java @@ -0,0 +1,95 @@ +package org.dromara.stgl.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; + +/** + * 社团活动权限检查结果 + * + * @author Cursor AI + * @date 2025-01-15 + */ +@Data +public class ClubActivityPermissionResult { + + /** + * 是否可以发起活动 + */ + private Boolean canCreateActivity; + + /** + * 权限状态 + * NORMAL - 正常状态 + * TEMPORARILY_LIMITED - 临时限制(15天内可恢复) + * PERMANENTLY_LIMITED - 永久限制(超过15天) + */ + private String status; + + /** + * 状态描述信息 + */ + private String message; + + /** + * 年审截止时间(仅临时限制时有值) + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date deadlineTime; + + /** + * 剩余小时数(仅临时限制时有值) + */ + private Long remainingHours; + + /** + * 剩余天数(仅临时限制时有值) + */ + private Long remainingDays; + + /** + * 年审开始时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date annualReviewStartTime; + + /** + * 创建正常状态结果 + */ + public static ClubActivityPermissionResult allowed() { + ClubActivityPermissionResult result = new ClubActivityPermissionResult(); + result.setCanCreateActivity(true); + result.setStatus("NORMAL"); + result.setMessage("权限正常,可以发起活动"); + return result; + } + + /** + * 创建临时限制状态结果 + */ + public static ClubActivityPermissionResult temporarilyDenied(String message, Date deadlineTime, + Date annualReviewStartTime, + Long remainingHours, Long remainingDays) { + ClubActivityPermissionResult result = new ClubActivityPermissionResult(); + result.setCanCreateActivity(false); + result.setStatus("TEMPORARILY_LIMITED"); + result.setMessage(message); + result.setDeadlineTime(deadlineTime); + result.setAnnualReviewStartTime(annualReviewStartTime); + result.setRemainingHours(remainingHours); + result.setRemainingDays(remainingDays); + return result; + } + + /** + * 创建永久限制状态结果 + */ + public static ClubActivityPermissionResult permanentlyDenied(String message) { + ClubActivityPermissionResult result = new ClubActivityPermissionResult(); + result.setCanCreateActivity(false); + result.setStatus("PERMANENTLY_LIMITED"); + result.setMessage(message); + return result; + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/MyActivityVo.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/MyActivityVo.java index 82ac300e..280aa9f8 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/MyActivityVo.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/MyActivityVo.java @@ -65,6 +65,16 @@ public class MyActivityVo implements Serializable { */ private String organizerName; + /** + * 关联部门名称 + */ + private String relatedDeptName; + + /** + * 关联部门名称列表 + */ + private String relatedDeptNames; + /** * 关联类型(1-社团 2-志愿者队伍) */ diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglActivityVo.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglActivityVo.java index ccb9848b..a4e7ca65 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglActivityVo.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglActivityVo.java @@ -88,6 +88,27 @@ public class StglActivityVo implements Serializable { */ private String posterUrl; + /** + * 关联部门ID + */ + private Long relatedDeptId; + + /** + * 关联部门名称 + */ + @ExcelProperty(value = "关联部门") + private String relatedDeptName; + + /** + * 关联部门ID列表(JSON格式) + */ + private String relatedDeptIds; + + /** + * 关联部门名称列表(逗号分隔) + */ + private String relatedDeptNames; + /** * 关联社团ID */ diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClassVo.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClassVo.java index 75788a93..c8f12e24 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClassVo.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClassVo.java @@ -65,6 +65,21 @@ public class StglClassVo implements Serializable { */ private Integer activityCount; + /** + * 今日活动名称(审核通过且时间为今天的活动) + */ + private String todayActivityName; + + /** + * 今日活动开始时间 + */ + private String todayActivityStartTime; + + /** + * 今日活动结束时间 + */ + private String todayActivityEndTime; + /** * 创建者 */ diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClubAdvisorHistoryVo.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClubAdvisorHistoryVo.java new file mode 100644 index 00000000..3ee6a933 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClubAdvisorHistoryVo.java @@ -0,0 +1,119 @@ +package org.dromara.stgl.domain.vo; + +import org.dromara.stgl.domain.StglClubAdvisorHistory; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 社团指导老师任职历史视图对象 stgl_club_advisor_history + * + * @author Cursor AI + * @date 2025-01-03 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = StglClubAdvisorHistory.class) +public class StglClubAdvisorHistoryVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @ExcelProperty(value = "主键ID") + private Long id; + + /** + * 社团ID + */ + @ExcelProperty(value = "社团ID") + private Long clubId; + + /** + * 社团名称 + */ + @ExcelProperty(value = "社团名称") + private String clubName; + + /** + * 指导老师姓名 + */ + @ExcelProperty(value = "指导老师姓名") + private String advisorName; + + /** + * 指导老师部门 + */ + @ExcelProperty(value = "指导老师部门") + private String advisorDept; + + /** + * 指导老师电话 + */ + @ExcelProperty(value = "指导老师电话") + private String advisorPhone; + + /** + * 指导老师邮箱 + */ + @ExcelProperty(value = "指导老师邮箱") + private String advisorEmail; + + /** + * 指导老师用户ID + */ + @ExcelProperty(value = "指导老师用户ID") + private Long advisorUserId; + + /** + * 在任状态:1-当前在任,0-历史(已离任) + */ + @ExcelProperty(value = "在任状态", converter = org.dromara.common.excel.convert.ExcelDictConvert.class) + @org.dromara.common.excel.annotation.ExcelDictFormat(dictType = "sys_normal_disable") + private Integer status; + + /** + * 上任时间 + */ + @ExcelProperty(value = "上任时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + + /** + * 离任时间 + */ + @ExcelProperty(value = "离任时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + /** + * 创建者 + */ + 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; + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClubAnnualReviewActivityVo.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClubAnnualReviewActivityVo.java new file mode 100644 index 00000000..a8c73bdd --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClubAnnualReviewActivityVo.java @@ -0,0 +1,111 @@ +package org.dromara.stgl.domain.vo; + +import org.dromara.stgl.domain.StglClubAnnualReviewActivity; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 社团年审记录活动视图对象 StglClubAnnualReviewActivityVo + * + * @author Cursor AI + * @date 2025-01-15 + */ +@Data +@AutoMapper(target = StglClubAnnualReviewActivity.class) +public class StglClubAnnualReviewActivityVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + private Long id; + + /** + * 年审记录ID + */ + private Long annualReviewId; + + /** + * 活动名称 + */ + private String activityName; + + /** + * 活动时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date activityTime; + + /** + * 活动开始时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date activityStartTime; + + /** + * 活动结束时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date activityEndTime; + + /** + * 活动地点 + */ + private String activityLocation; + + /** + * 活动类型 + */ + private String activityType; + + /** + * 参与人数 + */ + private Integer participantCount; + + /** + * 活动描述 + */ + private String activityDescription; + + /** + * 创建者 + */ + 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; + + // 为了兼容前端,添加一些别名字段 + /** + * 地点(别名) + */ + public String getLocation() { + return this.activityLocation; + } + + public void setLocation(String location) { + this.activityLocation = location; + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClubAnnualReviewVo.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClubAnnualReviewVo.java new file mode 100644 index 00000000..3b962b9b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/vo/StglClubAnnualReviewVo.java @@ -0,0 +1,156 @@ +package org.dromara.stgl.domain.vo; + +import org.dromara.stgl.domain.StglClubAnnualReview; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 社团年审记录视图对象 StglClubAnnualReviewVo + * + * @author Cursor AI + * @date 2025-01-15 + */ +@Data +@AutoMapper(target = StglClubAnnualReview.class) +public class StglClubAnnualReviewVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + private Long id; + + /** + * 社团ID + */ + private Long clubId; + + /** + * 年审年度 + */ + private Integer reviewYear; + + /** + * 年审开始日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date reviewStartDate; + + /** + * 年审结束日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date reviewEndDate; + + /** + * 年审时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date reviewTime; + + /** + * 社团名称 + */ + private String clubName; + + /** + * 成立日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date establishDate; + + /** + * 社长姓名 + */ + private String presidentName; + + /** + * 社团类型 + */ + private String clubType; + + /** + * 指导老师姓名 + */ + private String advisorName; + + /** + * 指导老师部门 + */ + private String advisorDept; + + /** + * 是否有章程 + */ + private String hasCharter; + + /** + * 章程文件URL + */ + private String charterUrl; + + /** + * 是否有资金 + */ + private String hasFunds; + + /** + * 资金数额 + */ + private BigDecimal fundAmount; + + /** + * 成员数量 + */ + private Integer memberCount; + + /** + * 状态(0-草稿 1-已提交 2-审核中 3-审核通过 4-审核不通过) + */ + private String status; + + /** + * 指导老师意见 + */ + private String advisorOpinion; + + /** + * 学院意见 + */ + private String collegeOpinion; + + /** + * 团委意见 + */ + private String youthLeagueOpinion; + + /** + * 成员信息JSON(用于前端显示) + */ + private String membersJson; + + /** + * 活动信息JSON(用于前端显示) + */ + private String activitiesJson; + + // 继承的基础字段 + 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; + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAdvisorHistoryMapper.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAdvisorHistoryMapper.java new file mode 100644 index 00000000..0ec99d83 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAdvisorHistoryMapper.java @@ -0,0 +1,54 @@ +package org.dromara.stgl.mapper; + +import org.dromara.stgl.domain.StglClubAdvisorHistory; +import org.dromara.stgl.domain.vo.StglClubAdvisorHistoryVo; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 社团指导老师任职历史Mapper接口 + * + * @author Cursor AI + * @date 2025-01-03 + */ +public interface StglClubAdvisorHistoryMapper extends BaseMapperPlus { + + /** + * 查询指定社团的当前在任指导老师 + * + * @param clubId 社团ID + * @return 当前在任指导老师列表 + */ + List selectCurrentAdvisorsByClubId(@Param("clubId") Long clubId); + + /** + * 查询指定社团的历史指导老师(已离任) + * + * @param clubId 社团ID + * @return 历史指导老师列表 + */ + List selectHistoryAdvisorsByClubId(@Param("clubId") Long clubId); + + /** + * 查询指定社团的所有指导老师(包含当前和历史) + * + * @param clubId 社团ID + * @return 所有指导老师列表 + */ + List selectAllAdvisorsByClubId(@Param("clubId") Long clubId); + + /** + * 批量设置指导老师为历史状态(离任) + * + * @param clubId 社团ID + * @param advisorNames 指导老师姓名列表 + * @param endTime 离任时间 + * @return 更新记录数 + */ + int batchSetHistoryStatus(@Param("clubId") Long clubId, + @Param("advisorNames") List advisorNames, + @Param("endTime") java.util.Date endTime); + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAnnualReviewActivityMapper.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAnnualReviewActivityMapper.java new file mode 100644 index 00000000..d5be4bdd --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAnnualReviewActivityMapper.java @@ -0,0 +1,27 @@ +package org.dromara.stgl.mapper; + +import org.dromara.stgl.domain.StglClubAnnualReviewActivity; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 社团年审记录活动Mapper接口 + * + * @author Lion Li + * @date 2024-12-31 + */ +public interface StglClubAnnualReviewActivityMapper extends BaseMapperPlus { + + /** + * 根据年审记录ID查询活动列表 + */ + List selectActivitiesByAnnualReviewId(@Param("annualReviewId") Long annualReviewId); + + /** + * 根据年审记录ID删除活动 + */ + int deleteActivitiesByAnnualReviewId(@Param("annualReviewId") Long annualReviewId); + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAnnualReviewMapper.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAnnualReviewMapper.java new file mode 100644 index 00000000..8e7514ab --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAnnualReviewMapper.java @@ -0,0 +1,15 @@ +package org.dromara.stgl.mapper; + +import org.dromara.stgl.domain.StglClubAnnualReview; +import org.dromara.stgl.domain.vo.StglClubAnnualReviewVo; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; + +/** + * 社团年审记录Mapper接口 + * + * @author Cursor AI + * @date 2025-01-15 + */ +public interface StglClubAnnualReviewMapper extends BaseMapperPlus { + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAnnualReviewMemberMapper.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAnnualReviewMemberMapper.java new file mode 100644 index 00000000..5348662f --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/mapper/StglClubAnnualReviewMemberMapper.java @@ -0,0 +1,27 @@ +package org.dromara.stgl.mapper; + +import org.dromara.stgl.domain.StglClubAnnualReviewMember; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 社团年审记录成员Mapper接口 + * + * @author Lion Li + * @date 2024-12-31 + */ +public interface StglClubAnnualReviewMemberMapper extends BaseMapperPlus { + + /** + * 根据年审记录ID查询成员列表 + */ + List selectMembersByAnnualReviewId(@Param("annualReviewId") Long annualReviewId); + + /** + * 根据年审记录ID删除成员 + */ + int deleteMembersByAnnualReviewId(@Param("annualReviewId") Long annualReviewId); + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglClubAdvisorHistoryService.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglClubAdvisorHistoryService.java new file mode 100644 index 00000000..33704d76 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglClubAdvisorHistoryService.java @@ -0,0 +1,130 @@ +package org.dromara.stgl.service; + +import org.dromara.stgl.domain.vo.StglClubAdvisorHistoryVo; +import org.dromara.stgl.domain.bo.StglClubAdvisorHistoryBo; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * 社团指导老师任职历史Service接口 + * + * @author Cursor AI + * @date 2025-01-03 + */ +public interface IStglClubAdvisorHistoryService { + + /** + * 查询社团指导老师任职历史 + */ + StglClubAdvisorHistoryVo queryById(Long id); + + /** + * 查询社团指导老师任职历史列表 + */ + TableDataInfo queryPageList(StglClubAdvisorHistoryBo bo, PageQuery pageQuery); + + /** + * 查询社团指导老师任职历史列表 + */ + List queryList(StglClubAdvisorHistoryBo bo); + + /** + * 新增社团指导老师任职历史 + */ + Boolean insertByBo(StglClubAdvisorHistoryBo bo); + + /** + * 修改社团指导老师任职历史 + */ + Boolean updateByBo(StglClubAdvisorHistoryBo bo); + + /** + * 校验并批量删除社团指导老师任职历史信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 查询指定社团的当前在任指导老师 + * + * @param clubId 社团ID + * @return 当前在任指导老师列表 + */ + List getCurrentAdvisorsByClubId(Long clubId); + + /** + * 查询指定社团的历史指导老师(已离任) + * + * @param clubId 社团ID + * @return 历史指导老师列表 + */ + List getHistoryAdvisorsByClubId(Long clubId); + + /** + * 查询指定社团的所有指导老师(包含当前和历史) + * + * @param clubId 社团ID + * @return 所有指导老师列表 + */ + List getAllAdvisorsByClubId(Long clubId); + + /** + * 更新社团指导老师历史记录 + * 对比新旧指导老师列表,处理新增和离任 + * + * @param clubId 社团ID + * @param newAdvisors 新的指导老师列表 + */ + void updateClubAdvisors(Long clubId, List newAdvisors); + + /** + * 更新社团指导老师历史记录(指定开始时间) + * 对比新旧指导老师列表,处理新增和离任 + * + * @param clubId 社团ID + * @param newAdvisors 新的指导老师列表 + * @param startTime 新指导老师的上任时间(如社团成立日期) + */ + void updateClubAdvisors(Long clubId, List newAdvisors, java.util.Date startTime); + + /** + * 指导老师信息 + */ + class AdvisorInfo { + private String name; + private String dept; + private String phone; + private String email; + private Long userId; + + // 构造方法 + public AdvisorInfo() {} + + public AdvisorInfo(String name, String dept, String phone, String email, Long userId) { + this.name = name; + this.dept = dept; + this.phone = phone; + this.email = email; + this.userId = userId; + } + + // getter和setter方法 + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getDept() { return dept; } + public void setDept(String dept) { this.dept = dept; } + + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + } + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglClubAnnualReviewService.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglClubAnnualReviewService.java new file mode 100644 index 00000000..fcf91f1b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglClubAnnualReviewService.java @@ -0,0 +1,89 @@ +package org.dromara.stgl.service; + +import org.dromara.stgl.domain.vo.StglClubAnnualReviewVo; +import org.dromara.stgl.domain.bo.StglClubAnnualReviewBo; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * 社团年审记录Service接口 + * + * @author Cursor AI + * @date 2025-01-15 + */ +public interface IStglClubAnnualReviewService { + + /** + * 查询社团年审记录 + */ + StglClubAnnualReviewVo queryById(Long id); + + /** + * 查询社团年审记录列表 + */ + TableDataInfo queryPageList(StglClubAnnualReviewBo bo, PageQuery pageQuery); + + /** + * 查询社团年审记录列表 + */ + List queryList(StglClubAnnualReviewBo bo); + + /** + * 新增社团年审记录 + */ + Boolean insertByBo(StglClubAnnualReviewBo bo); + + /** + * 修改社团年审记录 + */ + Boolean updateByBo(StglClubAnnualReviewBo bo); + + /** + * 校验并批量删除社团年审记录信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 提交年审记录 + */ + Boolean submitReview(Long id); + + /** + * 撤回年审记录 + */ + Boolean withdrawReview(Long id); + + /** + * 审核年审记录 + */ + Boolean auditReview(Long id, String auditResult, String auditOpinion, String auditType); + + /** + * 获取当前用户的年审记录列表 + */ + TableDataInfo queryMyPageList(StglClubAnnualReviewBo bo, PageQuery pageQuery); + + /** + * 检查是否可以创建年审记录 + */ + Boolean checkCanCreateReview(Long clubId, Integer reviewYear); + + /** + * 根据社团ID和成立日期获取一年内的活动数据 + * @param clubId 社团ID + * @param establishDate 成立日期字符串 (yyyy-MM-dd格式) + * @return 活动列表 + */ + List getClubActivitiesByReviewYear(Long clubId, Integer reviewYear); + + /** + * 根据年审记录ID获取对应的活动数据 + * @param annualReviewId 年审记录ID + * @return 年审活动列表 + */ + List getAnnualReviewActivities(Long annualReviewId); + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglClubPermissionService.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglClubPermissionService.java new file mode 100644 index 00000000..adae1847 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/IStglClubPermissionService.java @@ -0,0 +1,55 @@ +package org.dromara.stgl.service; + +import org.dromara.stgl.domain.vo.ClubActivityPermissionResult; + +import java.util.List; +import java.util.Map; + +/** + * 社团权限管理Service接口 + * + * @author Cursor AI + * @date 2025-01-15 + */ +public interface IStglClubPermissionService { + + /** + * 检查社团活动权限(实时计算) + * + * @param clubId 社团ID + * @return 权限检查结果 + */ + ClubActivityPermissionResult checkClubActivityPermission(Long clubId); + + /** + * 批量检查社团活动权限 + * + * @param clubIds 社团ID列表 + * @return 权限检查结果映射(key: clubId, value: 权限结果) + */ + Map batchCheckClubActivityPermission(List clubIds); + + /** + * 检查社团是否在年审窗口期内 + * + * @param clubId 社团ID + * @return true-在窗口期内,false-不在窗口期内 + */ + boolean isInAnnualReviewWindow(Long clubId); + + /** + * 获取社团距离下次年审的天数 + * + * @param clubId 社团ID + * @return 距离年审的天数,负数表示已过期 + */ + long getDaysUntilNextAnnualReview(Long clubId); + + /** + * 检查社团是否已在当前年审期内提交年审 + * + * @param clubId 社团ID + * @return true-已提交,false-未提交 + */ + boolean hasSubmittedCurrentYearAnnualReview(Long clubId); +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityServiceImpl.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityServiceImpl.java index a49dd247..c14459b9 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglActivityServiceImpl.java @@ -129,7 +129,7 @@ public class StglActivityServiceImpl extends ServiceImpl params = new HashMap<>(); params.put("registrationStatusList", Arrays.asList("0", "1")); @@ -258,6 +258,9 @@ public class StglActivityServiceImpl extends ServiceImpl deptIds = JsonUtils.parseArray(bo.getRelatedDeptIds(), Long.class); + + if (deptIds != null && !deptIds.isEmpty()) { + // 设置多选部门ID列表(JSON格式) + entity.setRelatedDeptIds(JsonUtils.toJsonString(deptIds)); + + // 设置第一个部门作为主要关联部门(保持兼容性) + entity.setRelatedDeptId(deptIds.get(0)); + + // 如果有多选部门名称,直接使用 + if (StringUtils.isNotBlank(bo.getRelatedDeptNames())) { + entity.setRelatedDeptNames(bo.getRelatedDeptNames()); + + // 设置第一个部门名称作为主要关联部门名称 + String[] deptNames = bo.getRelatedDeptNames().split(","); + if (deptNames.length > 0) { + entity.setRelatedDeptName(deptNames[0].trim()); + } + } + + log.info("处理多选部门数据 - 活动ID: {}, 部门ID列表: {}, 部门名称列表: {}", + entity.getId(), entity.getRelatedDeptIds(), entity.getRelatedDeptNames()); + } + } catch (Exception e) { + log.error("解析多选部门数据失败: {}", bo.getRelatedDeptIds(), e); + // 解析失败时清空多选字段 + entity.setRelatedDeptIds(null); + entity.setRelatedDeptNames(null); + } + } else { + // 没有多选部门数据,清空相关字段 + entity.setRelatedDeptIds(null); + entity.setRelatedDeptNames(null); + } + } } \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubAdvisorHistoryServiceImpl.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubAdvisorHistoryServiceImpl.java new file mode 100644 index 00000000..9a3fd4ba --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubAdvisorHistoryServiceImpl.java @@ -0,0 +1,205 @@ +package org.dromara.stgl.service.impl; + +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.dromara.stgl.domain.bo.StglClubAdvisorHistoryBo; +import org.dromara.stgl.domain.vo.StglClubAdvisorHistoryVo; +import org.dromara.stgl.domain.StglClubAdvisorHistory; +import org.dromara.stgl.mapper.StglClubAdvisorHistoryMapper; +import org.dromara.stgl.service.IStglClubAdvisorHistoryService; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 社团指导老师任职历史Service业务层处理 + * + * @author Cursor AI + * @date 2025-01-03 + */ +@RequiredArgsConstructor +@Service +public class StglClubAdvisorHistoryServiceImpl implements IStglClubAdvisorHistoryService { + + private final StglClubAdvisorHistoryMapper baseMapper; + + /** + * 查询社团指导老师任职历史 + */ + @Override + public StglClubAdvisorHistoryVo queryById(Long id){ + return baseMapper.selectVoById(id); + } + + /** + * 查询社团指导老师任职历史列表 + */ + @Override + public TableDataInfo queryPageList(StglClubAdvisorHistoryBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询社团指导老师任职历史列表 + */ + @Override + public List queryList(StglClubAdvisorHistoryBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(StglClubAdvisorHistoryBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(bo.getClubId() != null, StglClubAdvisorHistory::getClubId, bo.getClubId()); + lqw.like(StringUtils.isNotBlank(bo.getAdvisorName()), StglClubAdvisorHistory::getAdvisorName, bo.getAdvisorName()); + lqw.like(StringUtils.isNotBlank(bo.getAdvisorDept()), StglClubAdvisorHistory::getAdvisorDept, bo.getAdvisorDept()); + lqw.eq(StringUtils.isNotBlank(bo.getAdvisorPhone()), StglClubAdvisorHistory::getAdvisorPhone, bo.getAdvisorPhone()); + lqw.like(StringUtils.isNotBlank(bo.getAdvisorEmail()), StglClubAdvisorHistory::getAdvisorEmail, bo.getAdvisorEmail()); + lqw.eq(bo.getAdvisorUserId() != null, StglClubAdvisorHistory::getAdvisorUserId, bo.getAdvisorUserId()); + lqw.eq(bo.getStatus() != null, StglClubAdvisorHistory::getStatus, bo.getStatus()); + lqw.eq(bo.getStartTime() != null, StglClubAdvisorHistory::getStartTime, bo.getStartTime()); + lqw.eq(bo.getEndTime() != null, StglClubAdvisorHistory::getEndTime, bo.getEndTime()); + return lqw; + } + + /** + * 新增社团指导老师任职历史 + */ + @Override + public Boolean insertByBo(StglClubAdvisorHistoryBo bo) { + StglClubAdvisorHistory add = MapstructUtils.convert(bo, StglClubAdvisorHistory.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改社团指导老师任职历史 + */ + @Override + public Boolean updateByBo(StglClubAdvisorHistoryBo bo) { + StglClubAdvisorHistory update = MapstructUtils.convert(bo, StglClubAdvisorHistory.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(StglClubAdvisorHistory entity){ + // TODO 做一些数据校验,如唯一约束 + } + + /** + * 批量删除社团指导老师任职历史 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if(isValid){ + // TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 查询指定社团的当前在任指导老师 + */ + @Override + public List getCurrentAdvisorsByClubId(Long clubId) { + return baseMapper.selectCurrentAdvisorsByClubId(clubId); + } + + /** + * 查询指定社团的历史指导老师(已离任) + */ + @Override + public List getHistoryAdvisorsByClubId(Long clubId) { + return baseMapper.selectHistoryAdvisorsByClubId(clubId); + } + + /** + * 查询指定社团的所有指导老师(包含当前和历史) + */ + @Override + public List getAllAdvisorsByClubId(Long clubId) { + return baseMapper.selectAllAdvisorsByClubId(clubId); + } + + /** + * 更新社团指导老师历史记录 + * 对比新旧指导老师列表,处理新增和离任 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateClubAdvisors(Long clubId, List newAdvisors) { + updateClubAdvisors(clubId, newAdvisors, new Date()); + } + + /** + * 更新社团指导老师历史记录(指定开始时间) + * 对比新旧指导老师列表,处理新增和离任 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateClubAdvisors(Long clubId, List newAdvisors, Date startTime) { + Date currentDate = new Date(); + + // 1. 获取当前指导老师列表 + List currentRecords = getCurrentAdvisorsByClubId(clubId); + + // 2. 建立姓名映射 + Set currentNames = currentRecords.stream() + .map(StglClubAdvisorHistoryVo::getAdvisorName) + .collect(Collectors.toSet()); + + Set newNames = newAdvisors.stream() + .map(AdvisorInfo::getName) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + // 3. 处理被移除的指导老师 + List removedNames = currentNames.stream() + .filter(name -> !newNames.contains(name)) + .collect(Collectors.toList()); + + if (!removedNames.isEmpty()) { + baseMapper.batchSetHistoryStatus(clubId, removedNames, currentDate); + } + + // 4. 处理新增的指导老师 + for (AdvisorInfo newAdvisor : newAdvisors) { + if (StringUtils.isNotBlank(newAdvisor.getName()) && + !currentNames.contains(newAdvisor.getName())) { + + StglClubAdvisorHistory newRecord = new StglClubAdvisorHistory(); + newRecord.setClubId(clubId); + newRecord.setAdvisorName(newAdvisor.getName()); + newRecord.setAdvisorDept(newAdvisor.getDept()); + newRecord.setAdvisorPhone(newAdvisor.getPhone()); + newRecord.setAdvisorEmail(newAdvisor.getEmail()); + newRecord.setAdvisorUserId(newAdvisor.getUserId()); + newRecord.setStatus(1); + // 使用传入的开始时间,而不是当前时间 + newRecord.setStartTime(startTime != null ? startTime : currentDate); + newRecord.setEndTime(null); + + baseMapper.insert(newRecord); + } + } + } + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubAnnualReviewServiceImpl.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubAnnualReviewServiceImpl.java new file mode 100644 index 00000000..dd4d0d54 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubAnnualReviewServiceImpl.java @@ -0,0 +1,664 @@ +package org.dromara.stgl.service.impl; + +import org.dromara.common.core.utils.MapstructUtils; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.satoken.utils.LoginHelper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import cn.hutool.core.bean.BeanUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.dromara.stgl.domain.bo.StglClubAnnualReviewBo; +import org.dromara.stgl.domain.vo.StglClubAnnualReviewVo; +import org.dromara.stgl.domain.StglClubAnnualReview; +import org.dromara.stgl.domain.StglClubAnnualReviewMember; +import org.dromara.stgl.domain.StglClubAnnualReviewActivity; +import org.dromara.stgl.mapper.StglClubAnnualReviewMapper; +import org.dromara.stgl.mapper.StglClubAnnualReviewMemberMapper; +import org.dromara.stgl.mapper.StglClubAnnualReviewActivityMapper; +import org.dromara.stgl.service.IStglClubAnnualReviewService; +import org.dromara.stgl.service.IStglActivityService; +import org.dromara.stgl.service.IStglClubService; +import org.dromara.stgl.domain.bo.StglActivityBo; +import org.dromara.stgl.domain.vo.StglClubVo; + +import java.util.List; +import java.util.Map; +import java.util.Collection; +import java.util.Set; +import java.util.HashSet; +import java.util.ArrayList; +import java.util.stream.Collectors; +import java.util.Date; +import java.text.SimpleDateFormat; +import java.text.ParseException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * 社团年审记录Service业务层处理 + * + * @author Cursor AI + * @date 2025-01-15 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class StglClubAnnualReviewServiceImpl implements IStglClubAnnualReviewService { + + private final StglClubAnnualReviewMapper baseMapper; + private final StglClubAnnualReviewMemberMapper memberMapper; + private final StglClubAnnualReviewActivityMapper activityMapper; + private final IStglActivityService stglActivityService; + private final IStglClubService stglClubService; + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * 查询社团年审记录 + */ + @Override + public StglClubAnnualReviewVo queryById(Long id){ + StglClubAnnualReviewVo vo = baseMapper.selectVoById(id); + if (vo == null) { + log.warn("未找到年审记录,ID: {}", id); + return null; + } + + try { + // 查询关联的成员数据 + List members = memberMapper.selectMembersByAnnualReviewId(id); + + // 查询关联的活动数据 + List activities = activityMapper.selectActivitiesByAnnualReviewId(id); + + // 将成员和活动数据转为JSON格式返回给前端 + if (!members.isEmpty()) { + // 转换成员数据格式,匹配前端期望的字段名 + List> membersList = new ArrayList<>(); + for (StglClubAnnualReviewMember member : members) { + Map memberMap = new HashMap<>(); + memberMap.put("name", member.getMemberName()); + memberMap.put("className", member.getClassName()); + memberMap.put("role", member.getRole()); + memberMap.put("phone", member.getContactInfo()); + membersList.add(memberMap); + } + vo.setMembersJson(objectMapper.writeValueAsString(membersList)); + } else { + vo.setMembersJson("[]"); // 设置空数组而不是null + } + + if (!activities.isEmpty()) { + // 转换活动数据格式,匹配前端期望的字段名 + List> activitiesList = new ArrayList<>(); + for (StglClubAnnualReviewActivity activity : activities) { + Map activityMap = new HashMap<>(); + activityMap.put("activityName", activity.getActivityName()); + activityMap.put("location", activity.getActivityLocation()); + activityMap.put("participantCount", activity.getParticipantCount()); + + // 格式化活动时间显示 - 优先使用新的开始/结束时间字段 + if (activity.getActivityStartTime() != null) { + activityMap.put("activityStartTime", activity.getActivityStartTime().toString()); + } + if (activity.getActivityEndTime() != null) { + activityMap.put("activityEndTime", activity.getActivityEndTime().toString()); + } + // 保持向后兼容 + if (activity.getActivityTime() != null) { + activityMap.put("activityDate", activity.getActivityTime().toString()); + } else { + activityMap.put("activityDate", ""); + } + + activitiesList.add(activityMap); + } + vo.setActivitiesJson(objectMapper.writeValueAsString(activitiesList)); + } else { + vo.setActivitiesJson("[]"); // 设置空数组而不是null + } + + } catch (Exception e) { + log.error("处理年审记录关联数据失败,ID: {}", id, e); + // 确保JSON字段不为null + if (vo.getMembersJson() == null) { + vo.setMembersJson("[]"); + } + if (vo.getActivitiesJson() == null) { + vo.setActivitiesJson("[]"); + } + } + + return vo; + } + + /** + * 查询社团年审记录列表 + */ + @Override + public TableDataInfo queryPageList(StglClubAnnualReviewBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询社团年审记录列表 + */ + @Override + public List queryList(StglClubAnnualReviewBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(StglClubAnnualReviewBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(bo.getClubId() != null, StglClubAnnualReview::getClubId, bo.getClubId()); + lqw.eq(bo.getReviewYear() != null, StglClubAnnualReview::getReviewYear, bo.getReviewYear()); + lqw.between(params.get("beginReviewStartDate") != null && params.get("endReviewStartDate") != null, + StglClubAnnualReview::getReviewStartDate, params.get("beginReviewStartDate"), params.get("endReviewStartDate")); + lqw.between(params.get("beginReviewEndDate") != null && params.get("endReviewEndDate") != null, + StglClubAnnualReview::getReviewEndDate, params.get("beginReviewEndDate"), params.get("endReviewEndDate")); + lqw.like(StringUtils.isNotBlank(bo.getClubName()), StglClubAnnualReview::getClubName, bo.getClubName()); + lqw.eq(StringUtils.isNotBlank(bo.getClubType()), StglClubAnnualReview::getClubType, bo.getClubType()); + lqw.eq(StringUtils.isNotBlank(bo.getStatus()), StglClubAnnualReview::getStatus, bo.getStatus()); + lqw.orderByDesc(StglClubAnnualReview::getCreateTime); + return lqw; + } + + /** + * 新增社团年审记录 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean insertByBo(StglClubAnnualReviewBo bo) { + // 检查是否可以创建年审记录 + if (!checkCanCreateReview(bo.getClubId(), bo.getReviewYear())) { + throw new RuntimeException("该社团在该年度已存在年审记录"); + } + + StglClubAnnualReview add = MapstructUtils.convert(bo, StglClubAnnualReview.class); + // 设置默认状态为草稿 + if (StringUtils.isBlank(add.getStatus())) { + add.setStatus("0"); + } + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + + // 保存成员数据 + saveMembers(add.getId(), bo.getMembersJson()); + + // 保存活动数据 + saveActivities(add.getId(), bo.getActivitiesJson()); + } + return flag; + } + + /** + * 修改社团年审记录 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean updateByBo(StglClubAnnualReviewBo bo) { + StglClubAnnualReview update = MapstructUtils.convert(bo, StglClubAnnualReview.class); + validEntityBeforeSave(update); + boolean flag = baseMapper.updateById(update) > 0; + + if (flag) { + // 保存成员数据 + saveMembers(bo.getId(), bo.getMembersJson()); + + // 保存活动数据 + saveActivities(bo.getId(), bo.getActivitiesJson()); + } + + return flag; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(StglClubAnnualReview entity){ + // 可以在此处进行额外的业务校验 + if (entity.getReviewStartDate() != null && entity.getReviewEndDate() != null) { + if (entity.getReviewStartDate().after(entity.getReviewEndDate())) { + throw new RuntimeException("年审开始日期不能晚于结束日期"); + } + } + + // 自动填充社长信息(如果社长姓名为空且有社团ID) + if (entity.getClubId() != null && StringUtils.isBlank(entity.getPresidentName())) { + StglClubVo clubVo = stglClubService.queryById(entity.getClubId()); + if (clubVo != null) { + entity.setPresidentName(clubVo.getPresidentName()); + } + } + } + + /** + * 保存成员数据 - 优化版本:精确删除而非全量重建 + */ + private void saveMembers(Long annualReviewId, String membersJson) { + if (StringUtils.isBlank(membersJson)) { + return; + } + try { + // 1. 获取现有成员数据 + List existingMembers = + memberMapper.selectMembersByAnnualReviewId(annualReviewId); + + // 2. 解析前端提交的数据 + List> newMembersList = objectMapper.readValue(membersJson, + new TypeReference>>() {}); + + // 3. 构建要保留的成员标识集合 + Set keepMemberKeys = newMembersList.stream() + .map(this::generateMemberKey) + .collect(Collectors.toSet()); + + // 4. 只逻辑删除不在保留列表中的成员 + for (StglClubAnnualReviewMember existingMember : existingMembers) { + String existingKey = generateMemberKey(existingMember); + if (!keepMemberKeys.contains(existingKey)) { + // 逻辑删除该成员 + memberMapper.deleteById(existingMember.getId()); + log.info("删除年审成员记录: annualReviewId={}, memberName={}, className={}", + annualReviewId, existingMember.getMemberName(), existingMember.getClassName()); + } + } + + // 5. 新增不存在的成员(如果有的话 - 虽然当前页面只支持删除) + Set existingMemberKeys = existingMembers.stream() + .map(this::generateMemberKey) + .collect(Collectors.toSet()); + + for (Map memberData : newMembersList) { + String memberKey = generateMemberKey(memberData); + if (!existingMemberKeys.contains(memberKey)) { + // 新增成员 + StglClubAnnualReviewMember member = new StglClubAnnualReviewMember(); + member.setAnnualReviewId(annualReviewId); + member.setMemberName((String) memberData.get("name")); + member.setClassName((String) memberData.get("className")); + member.setRole((String) memberData.get("role")); + member.setContactInfo((String) memberData.get("phone")); + + // 注意:userId字段已从数据库表中删除,不再设置 + + memberMapper.insert(member); + log.info("新增年审成员记录: annualReviewId={}, memberName={}, className={}", + annualReviewId, member.getMemberName(), member.getClassName()); + } + } + + } catch (Exception e) { + log.error("保存成员数据失败: annualReviewId={}", annualReviewId, e); + throw new RuntimeException("保存成员数据失败: " + e.getMessage()); + } + } + + /** + * 生成成员唯一标识 - 优先使用userId,其次使用姓名+班级组合 + */ + private String generateMemberKey(Map memberData) { + // 如果有user_id,优先使用 + Object userId = memberData.get("userId"); + if (userId != null && StringUtils.isNotBlank(userId.toString())) { + return "user_" + userId.toString(); + } + // 否则使用姓名+班级的组合 + String name = (String) memberData.get("name"); + String className = (String) memberData.get("className"); + return (name != null ? name : "") + "_" + (className != null ? className : ""); + } + + /** + * 生成成员唯一标识 - 实体对象版本 + */ + private String generateMemberKey(StglClubAnnualReviewMember member) { + // userId字段已删除,直接使用姓名+班级的组合 + String name = member.getMemberName(); + String className = member.getClassName(); + return (name != null ? name : "") + "_" + (className != null ? className : ""); + } + + /** + * 保存活动数据 - 基于ID的精确匹配版本 + */ + private void saveActivities(Long annualReviewId, String activitiesJson) { + if (StringUtils.isBlank(activitiesJson)) { + return; + } + try { + // 1. 获取现有活动数据 + List existingActivities = + activityMapper.selectActivitiesByAnnualReviewId(annualReviewId); + + // 2. 解析前端提交的数据 + List> newActivitiesList = objectMapper.readValue(activitiesJson, + new TypeReference>>() {}); + + // 3. 构建要保留的活动ID集合(只包含有ID的活动,即数据库中已存在的) + Set keepActivityIds = new HashSet<>(); + List> newActivities = new ArrayList<>(); + + for (Map activity : newActivitiesList) { + Object idObj = activity.get("id"); + if (idObj != null) { + // 现有活动,添加到保留ID集合 + Long activityId = Long.valueOf(idObj.toString()); + keepActivityIds.add(activityId); + } else { + // 新活动,添加到新增列表 + newActivities.add(activity); + } + } + + // 4. 删除不在保留列表中的活动 + for (StglClubAnnualReviewActivity existingActivity : existingActivities) { + if (!keepActivityIds.contains(existingActivity.getId())) { + // 逻辑删除该活动 + activityMapper.deleteById(existingActivity.getId()); + } + } + + // 5. 新增活动 + for (Map activityData : newActivities) { + StglClubAnnualReviewActivity activity = new StglClubAnnualReviewActivity(); + activity.setAnnualReviewId(annualReviewId); + activity.setActivityName((String) activityData.get("activityName")); + activity.setActivityLocation((String) activityData.get("location")); + activity.setParticipantCount(activityData.get("participantCount") != null ? + Integer.parseInt(activityData.get("participantCount").toString()) : 0); + + // 处理时间字段 + parseAndSetActivityTimes(activity, activityData); + + activityMapper.insert(activity); + } + + } catch (Exception e) { + log.error("保存活动数据失败: annualReviewId={}", annualReviewId, e); + throw new RuntimeException("保存活动数据失败: " + e.getMessage()); + } + } + + /** + * 解析并设置活动时间 + */ + private void parseAndSetActivityTimes(StglClubAnnualReviewActivity activity, Map activityData) { + // 处理活动日期 (保持兼容性) + if (activityData.get("activityDate") != null) { + String dateStr = activityData.get("activityDate").toString(); + if (StringUtils.isNotBlank(dateStr)) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date activityDate = sdf.parse(dateStr); + activity.setActivityTime(activityDate); + } catch (ParseException e) { + log.warn("解析活动时间失败: {}", dateStr, e); + activity.setActivityTime(null); + } + } + } + + // 处理活动开始时间 + if (activityData.get("activityStartTime") != null) { + String startTimeStr = activityData.get("activityStartTime").toString(); + if (StringUtils.isNotBlank(startTimeStr)) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date startTime = sdf.parse(startTimeStr); + activity.setActivityStartTime(startTime); + } catch (ParseException e) { + try { + SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + Date startTime = sdf2.parse(startTimeStr); + activity.setActivityStartTime(startTime); + } catch (ParseException e2) { + log.warn("解析活动开始时间失败: {}", startTimeStr, e2); + activity.setActivityStartTime(null); + } + } + } + } + + // 处理活动结束时间 + if (activityData.get("activityEndTime") != null) { + String endTimeStr = activityData.get("activityEndTime").toString(); + if (StringUtils.isNotBlank(endTimeStr)) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date endTime = sdf.parse(endTimeStr); + activity.setActivityEndTime(endTime); + } catch (ParseException e) { + try { + SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + Date endTime = sdf2.parse(endTimeStr); + activity.setActivityEndTime(endTime); + } catch (ParseException e2) { + log.warn("解析活动结束时间失败: {}", endTimeStr, e2); + activity.setActivityEndTime(null); + } + } + } + } + } + + /** + * 生成活动唯一标识 - 优先使用activityId,其次使用活动名称+时间+地点组合 + */ + private String generateActivityKey(Map activityData) { + // 如果有activity_id,优先使用 + Object activityId = activityData.get("activityId"); + if (activityId != null && StringUtils.isNotBlank(activityId.toString())) { + return "activity_" + activityId.toString(); + } + // 否则使用活动名称+时间+地点的组合 + String name = (String) activityData.get("activityName"); + String location = (String) activityData.get("location"); + String time = ""; + + // 优先使用开始时间,其次使用活动日期 + if (activityData.get("activityStartTime") != null) { + time = activityData.get("activityStartTime").toString(); + } else if (activityData.get("activityDate") != null) { + time = activityData.get("activityDate").toString(); + } + + return (name != null ? name : "") + "_" + time + "_" + (location != null ? location : ""); + } + + /** + * 生成活动唯一标识 - 实体对象版本 + */ + private String generateActivityKey(StglClubAnnualReviewActivity activity) { + // activityId字段已删除,直接使用活动名称+时间+地点的组合 + String name = activity.getActivityName(); + String location = activity.getActivityLocation(); + String time = ""; + + // 优先使用开始时间,其次使用活动时间 + if (activity.getActivityStartTime() != null) { + time = activity.getActivityStartTime().toString(); + } else if (activity.getActivityTime() != null) { + time = activity.getActivityTime().toString(); + } + + return (name != null ? name : "") + "_" + time + "_" + (location != null ? location : ""); + } + + /** + * 批量删除社团年审记录 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if(isValid){ + // 做一些业务上的校验,判断是否需要校验 + for (Long id : ids) { + StglClubAnnualReviewVo vo = queryById(id); + if (vo != null && !"0".equals(vo.getStatus())) { + throw new RuntimeException("只能删除草稿状态的年审记录"); + } + } + } + + // 删除关联的成员和活动数据 + for (Long id : ids) { + memberMapper.deleteMembersByAnnualReviewId(id); + activityMapper.deleteActivitiesByAnnualReviewId(id); + } + + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 提交年审记录 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean submitReview(Long id) { + StglClubAnnualReview entity = baseMapper.selectById(id); + if (entity == null) { + throw new RuntimeException("年审记录不存在"); + } + if (!"0".equals(entity.getStatus())) { + throw new RuntimeException("只能提交草稿状态的年审记录"); + } + + entity.setStatus("1"); + entity.setReviewTime(new Date()); + return baseMapper.updateById(entity) > 0; + } + + /** + * 撤回年审记录 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean withdrawReview(Long id) { + StglClubAnnualReview entity = baseMapper.selectById(id); + if (entity == null) { + throw new RuntimeException("年审记录不存在"); + } + if (!"1".equals(entity.getStatus())) { + throw new RuntimeException("只能撤回已提交状态的年审记录"); + } + + entity.setStatus("0"); + return baseMapper.updateById(entity) > 0; + } + + /** + * 审核年审记录 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean auditReview(Long id, String auditResult, String auditOpinion, String auditType) { + StglClubAnnualReview entity = baseMapper.selectById(id); + if (entity == null) { + throw new RuntimeException("年审记录不存在"); + } + + // 根据审核类型设置意见 + switch (auditType) { + case "advisor": + entity.setAdvisorOpinion(auditOpinion); + break; + case "college": + entity.setCollegeOpinion(auditOpinion); + break; + case "youth_league": + entity.setYouthLeagueOpinion(auditOpinion); + break; + default: + throw new RuntimeException("无效的审核类型"); + } + + // 设置审核结果状态 + entity.setStatus("3".equals(auditResult) ? "3" : "4"); + + return baseMapper.updateById(entity) > 0; + } + + /** + * 获取当前用户的年审记录列表 + */ + @Override + public TableDataInfo queryMyPageList(StglClubAnnualReviewBo bo, PageQuery pageQuery) { + // 这里需要根据实际业务逻辑,通过用户ID查询其所属社团的年审记录 + // 暂时使用相同的查询逻辑,实际应该加上用户权限过滤 + return queryPageList(bo, pageQuery); + } + + /** + * 检查是否可以创建年审记录 + */ + @Override + public Boolean checkCanCreateReview(Long clubId, Integer reviewYear) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(StglClubAnnualReview::getClubId, clubId); + lqw.eq(StglClubAnnualReview::getReviewYear, reviewYear); + return baseMapper.selectCount(lqw) == 0; + } + + /** + * 根据社团ID和年审年度获取该年度的活动数据 + * @param clubId 社团ID + * @param reviewYear 年审年度 (例如:2024) + * @return 活动列表 - 只包含已通过、进行中、已结束的活动 + */ + @Override + public List getClubActivitiesByReviewYear(Long clubId, Integer reviewYear) { + // 计算年审年度的时间范围:年审年度的1月1日到12月31日 + LocalDate startDate = LocalDate.of(reviewYear, 1, 1); + LocalDate endDate = LocalDate.of(reviewYear, 12, 31); + + // 构建查询条件 + StglActivityBo bo = new StglActivityBo(); + bo.setRelatedClubId(clubId); // 设置社团ID + bo.setRelatedType(1); // 关联类型为社团(1表示社团,2表示志愿团队) + + // 设置时间范围查询条件(查询活动开始时间在该年度内的活动) + bo.setStartTime(java.sql.Date.valueOf(startDate)); + bo.setEndTime(java.sql.Date.valueOf(endDate)); + + // 查询活动列表 + List allActivities = stglActivityService.queryList(bo); + + // 过滤活动状态:只保留已通过(3)、进行中(4)、已结束(5)的活动 + return allActivities.stream() + .filter(activity -> { + String status = activity.getActivityStatus(); + return "3".equals(status) || "4".equals(status) || "5".equals(status); + }) + .collect(java.util.stream.Collectors.toList()); + } + + /** + * 根据年审记录ID获取对应的活动数据 + */ + @Override + public List getAnnualReviewActivities(Long annualReviewId) { + LambdaQueryWrapper lqw = new LambdaQueryWrapper<>(); + lqw.eq(StglClubAnnualReviewActivity::getAnnualReviewId, annualReviewId); + lqw.orderByAsc(StglClubAnnualReviewActivity::getActivityStartTime); + + List activities = activityMapper.selectList(lqw); + return BeanUtil.copyToList(activities, org.dromara.stgl.domain.vo.StglClubAnnualReviewActivityVo.class); + } + +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubNotificationScheduler.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubNotificationScheduler.java new file mode 100644 index 00000000..363eda2f --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubNotificationScheduler.java @@ -0,0 +1,263 @@ +package org.dromara.stgl.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.stgl.domain.StglClub; +import org.dromara.stgl.domain.vo.ClubActivityPermissionResult; +import org.dromara.stgl.mapper.StglClubMapper; +import org.dromara.stgl.service.IStglClubPermissionService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * 社团通知定时任务 + * 仅用于发送年审提醒通知,不管理权限状态 + * + * @author Cursor AI + * @date 2025-01-15 + */ +@Slf4j +@RequiredArgsConstructor +@Component +@ConditionalOnProperty(name = "stgl.club.notification.enabled", havingValue = "true", matchIfMissing = true) +public class StglClubNotificationScheduler { + + private final StglClubMapper clubMapper; + private final IStglClubPermissionService clubPermissionService; + + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + /** + * 每天早上8点发送年审相关通知 + * 通过实时权限检查确定通知类型,不存储状态 + */ + @Scheduled(cron = "0 0 8 * * ?") + public void sendDailyAnnualReviewReminders() { + log.info("开始执行社团年审通知任务"); + + try { + Date today = new Date(); + + // 获取所有正常状态的社团 + List allClubs = getAllActiveClubs(); + log.info("获取到活跃社团数量: {}", allClubs.size()); + + int todayLimitedCount = 0; + int urgentReminderCount = 0; + int permanentlyLimitedCount = 0; + + for (StglClub club : allClubs) { + try { + // 实时检查权限状态 + ClubActivityPermissionResult permission = + clubPermissionService.checkClubActivityPermission(club.getId()); + + switch (permission.getStatus()) { + case "TEMPORARILY_LIMITED": + handleTemporarilyLimitedClub(club, permission); + if (isFirstDayOfRestriction(club, today)) { + todayLimitedCount++; + } + if (isUrgentReminder(permission)) { + urgentReminderCount++; + } + break; + + case "PERMANENTLY_LIMITED": + if (isFirstDayOfPermanentRestriction(club, today)) { + handlePermanentlyLimitedClub(club); + permanentlyLimitedCount++; + } + break; + + case "NORMAL": + // 正常状态无需通知 + break; + + default: + log.warn("未知的权限状态: {}, 社团ID: {}", permission.getStatus(), club.getId()); + } + } catch (Exception e) { + log.error("处理社团通知失败,社团ID: {}", club.getId(), e); + } + } + + log.info("社团年审通知任务完成 - 今日开始限制: {}, 紧急提醒: {}, 永久限制: {}", + todayLimitedCount, urgentReminderCount, permanentlyLimitedCount); + + } catch (Exception e) { + log.error("执行社团年审通知任务失败", e); + } + } + + /** + * 处理临时限制状态的社团 + */ + private void handleTemporarilyLimitedClub(StglClub club, ClubActivityPermissionResult permission) { + if (permission.getRemainingDays() != null) { + if (permission.getRemainingDays() <= 3) { + // 剩余3天内,发送紧急提醒 + sendUrgentAnnualReviewReminder(club, permission); + } else if (permission.getRemainingDays() >= 12) { + // 刚开始限制(剩余12天以上),发送开始通知 + sendAnnualReviewStartNotification(club, permission); + } + } + } + + /** + * 处理永久限制状态的社团 + */ + private void handlePermanentlyLimitedClub(StglClub club) { + sendPermanentRestrictionNotification(club); + } + + /** + * 发送年审开始通知 + */ + private void sendAnnualReviewStartNotification(StglClub club, ClubActivityPermissionResult permission) { + String message = String.format( + "【年审提醒】尊敬的%s社团,您的社团从今天开始进入年审期,活动发起功能已限制。" + + "请在%d天内完成年审提交,截止时间:%s。" + + "提醒:超过期限将永久限制活动发起功能。", + club.getClubName(), + permission.getRemainingDays(), + dateFormat.format(permission.getDeadlineTime()) + ); + + log.info("发送年审开始通知 - 社团: {}, 剩余天数: {}", club.getClubName(), permission.getRemainingDays()); + sendNotificationToClub(club, "年审开始通知", message); + } + + /** + * 发送紧急年审提醒 + */ + private void sendUrgentAnnualReviewReminder(StglClub club, ClubActivityPermissionResult permission) { + String message = String.format( + "【紧急提醒】%s社团,您的年审即将到期!" + + "仅剩%d天%d小时,截止时间:%s。" + + "请立即提交年审,否则将永久限制活动发起功能!", + club.getClubName(), + permission.getRemainingDays(), + permission.getRemainingHours() % 24, + dateFormat.format(permission.getDeadlineTime()) + ); + + log.warn("发送紧急年审提醒 - 社团: {}, 剩余: {}天{}小时", + club.getClubName(), permission.getRemainingDays(), permission.getRemainingHours() % 24); + sendNotificationToClub(club, "紧急年审提醒", message); + } + + /** + * 发送永久限制通知 + */ + private void sendPermanentRestrictionNotification(StglClub club) { + String message = String.format( + "【权限限制】%s社团,很遗憾通知您,由于超过年审提交期限," + + "您的社团活动发起功能已被永久限制。即使现在补交年审也无法恢复权限。" + + "如有疑问,请联系管理员。", + club.getClubName() + ); + + log.warn("发送永久限制通知 - 社团: {}", club.getClubName()); + sendNotificationToClub(club, "活动权限永久限制", message); + } + + /** + * 发送通知到社团(实际发送逻辑) + */ + private void sendNotificationToClub(StglClub club, String title, String message) { + try { + // TODO: 集成实际的通知系统 + // 可以是站内信、邮件、短信等 + log.info("通知发送 - 社团: {}, 标题: {}, 内容: {}", club.getClubName(), title, message); + + // 示例:记录通知日志 + recordNotificationLog(club.getId(), title, message); + } catch (Exception e) { + log.error("发送通知失败 - 社团: {}, 标题: {}", club.getClubName(), title, e); + } + } + + /** + * 记录通知日志 + */ + private void recordNotificationLog(Long clubId, String title, String message) { + // TODO: 将通知记录保存到数据库,用于审计和跟踪 + log.debug("记录通知日志 - 社团ID: {}, 标题: {}", clubId, title); + } + + /** + * 获取所有活跃的社团 + */ + private List getAllActiveClubs() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(StglClub::getStatus, 1) // 1-正常状态 + .isNotNull(StglClub::getEstablishDate); // 必须有成立日期 + return clubMapper.selectList(wrapper); + } + + /** + * 判断是否是限制的第一天 + */ + private boolean isFirstDayOfRestriction(StglClub club, Date today) { + try { + ClubActivityPermissionResult permission = + clubPermissionService.checkClubActivityPermission(club.getId()); + + if (!"TEMPORARILY_LIMITED".equals(permission.getStatus())) { + return false; + } + + // 如果剩余天数接近15天,说明是刚开始限制 + return permission.getRemainingDays() != null && permission.getRemainingDays() >= 14; + } catch (Exception e) { + log.error("判断限制第一天失败,社团ID: {}", club.getId(), e); + return false; + } + } + + /** + * 判断是否需要紧急提醒 + */ + private boolean isUrgentReminder(ClubActivityPermissionResult permission) { + return permission.getRemainingDays() != null && permission.getRemainingDays() <= 3; + } + + /** + * 判断是否是永久限制的第一天 + */ + private boolean isFirstDayOfPermanentRestriction(StglClub club, Date today) { + try { + // 检查昨天的状态是否还是临时限制 + Date yesterday = new Date(today.getTime() - TimeUnit.DAYS.toMillis(1)); + + // 通过计算年审截止时间来判断 + // 如果今天刚好是截止后的第一天,就发送通知 + Date establishDate = club.getEstablishDate(); + if (establishDate == null) return false; + + Calendar cal = Calendar.getInstance(); + cal.setTime(establishDate); + cal.set(Calendar.YEAR, Calendar.getInstance().get(Calendar.YEAR)); + cal.add(Calendar.DAY_OF_MONTH, 15); + + Date deadlineDate = cal.getTime(); + + // 如果今天是截止日期后的1-2天内,认为是刚刚永久限制 + long daysSinceDeadline = TimeUnit.MILLISECONDS.toDays( + today.getTime() - deadlineDate.getTime()); + + return daysSinceDeadline >= 0 && daysSinceDeadline <= 1; + } catch (Exception e) { + log.error("判断永久限制第一天失败,社团ID: {}", club.getId(), e); + return false; + } + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubPermissionServiceImpl.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubPermissionServiceImpl.java new file mode 100644 index 00000000..1c75a121 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubPermissionServiceImpl.java @@ -0,0 +1,240 @@ +package org.dromara.stgl.service.impl; + +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.domain.StglClub; +import org.dromara.stgl.domain.StglClubAnnualReview; +import org.dromara.stgl.domain.vo.ClubActivityPermissionResult; +import org.dromara.stgl.mapper.StglClubMapper; +import org.dromara.stgl.mapper.StglClubAnnualReviewMapper; +import org.dromara.stgl.service.IStglClubPermissionService; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 社团权限管理Service业务层处理 + * + * @author Cursor AI + * @date 2025-01-15 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class StglClubPermissionServiceImpl implements IStglClubPermissionService { + + private final StglClubMapper clubMapper; + private final StglClubAnnualReviewMapper annualReviewMapper; + + /** + * 年审窗口期天数(15天) + */ + private static final int ANNUAL_REVIEW_WINDOW_DAYS = 15; + + /** + * 检查社团活动权限(实时计算,精确到秒级) + */ + @Override + public ClubActivityPermissionResult checkClubActivityPermission(Long clubId) { + log.debug("开始检查社团权限,clubId: {}", clubId); + + // 1. 获取社团基础信息 + StglClub clubInfo = clubMapper.selectById(clubId); + if (clubInfo == null) { + throw new ServiceException("社团不存在,ID: " + clubId); + } + + Date establishDate = clubInfo.getEstablishDate(); + if (establishDate == null) { + throw new ServiceException("社团成立日期不能为空"); + } + + Date currentTime = new Date(); // 当前精确时间(到毫秒) + + // 2. 计算关键时间点(精确到秒) + Date annualReviewStartTime = calculateAnnualReviewStartTime(establishDate, currentTime); + Date annualReviewDeadlineTime = addDays(annualReviewStartTime, ANNUAL_REVIEW_WINDOW_DAYS); + + log.debug("社团ID: {}, 成立时间: {}, 年审开始时间: {}, 年审截止时间: {}, 当前时间: {}", + clubId, establishDate, annualReviewStartTime, annualReviewDeadlineTime, currentTime); + + // 3. 时间段判断(精确比较) + if (currentTime.before(annualReviewStartTime)) { + // 未到年审时间,正常状态 + log.debug("社团ID: {} 未到年审时间,状态正常", clubId); + return ClubActivityPermissionResult.allowed(); + } + + if (currentTime.after(annualReviewDeadlineTime)) { + // 超过15天期限,永久限制 + log.debug("社团ID: {} 已超过年审期限,永久限制", clubId); + return ClubActivityPermissionResult.permanentlyDenied( + "已超过年审提交期限(15天),无法发起活动"); + } + + // 4. 在15天窗口期内,检查年审提交情况(精确查询) + boolean hasValidSubmission = checkAnnualReviewSubmissionWithPrecision( + clubId, annualReviewStartTime, currentTime); + + if (hasValidSubmission) { + log.debug("社团ID: {} 已在窗口期内提交年审,权限恢复", clubId); + return ClubActivityPermissionResult.allowed(); + } else { + // 计算剩余时间 + long remainingMillis = annualReviewDeadlineTime.getTime() - currentTime.getTime(); + long remainingHours = TimeUnit.MILLISECONDS.toHours(remainingMillis); + long remainingDays = TimeUnit.MILLISECONDS.toDays(remainingMillis); + + String message = String.format("需要先提交年审,剩余时间:%d天%d小时", + remainingDays, remainingHours % 24); + + log.debug("社团ID: {} 在年审窗口期内但未提交,剩余{}天{}小时", + clubId, remainingDays, remainingHours % 24); + + return ClubActivityPermissionResult.temporarilyDenied( + message, annualReviewDeadlineTime, annualReviewStartTime, + remainingHours, remainingDays); + } + } + + /** + * 批量检查社团活动权限 + */ + @Override + public Map batchCheckClubActivityPermission(List clubIds) { + if (clubIds == null || clubIds.isEmpty()) { + return new HashMap<>(); + } + + return clubIds.stream() + .collect(Collectors.toMap( + clubId -> clubId, + this::checkClubActivityPermission, + (existing, replacement) -> existing + )); + } + + /** + * 检查社团是否在年审窗口期内 + */ + @Override + public boolean isInAnnualReviewWindow(Long clubId) { + ClubActivityPermissionResult result = checkClubActivityPermission(clubId); + return "TEMPORARILY_LIMITED".equals(result.getStatus()); + } + + /** + * 获取社团距离下次年审的天数 + */ + @Override + public long getDaysUntilNextAnnualReview(Long clubId) { + StglClub clubInfo = clubMapper.selectById(clubId); + if (clubInfo == null || clubInfo.getEstablishDate() == null) { + return -1; + } + + Date currentTime = new Date(); + Date nextAnnualReviewTime = calculateAnnualReviewStartTime(clubInfo.getEstablishDate(), currentTime); + + // 如果已经过了今年的年审时间,计算明年的 + if (currentTime.after(nextAnnualReviewTime)) { + Calendar cal = Calendar.getInstance(); + cal.setTime(nextAnnualReviewTime); + cal.add(Calendar.YEAR, 1); + nextAnnualReviewTime = cal.getTime(); + } + + long diffMillis = nextAnnualReviewTime.getTime() - currentTime.getTime(); + return TimeUnit.MILLISECONDS.toDays(diffMillis); + } + + /** + * 检查社团是否已在当前年审期内提交年审 + */ + @Override + public boolean hasSubmittedCurrentYearAnnualReview(Long clubId) { + StglClub clubInfo = clubMapper.selectById(clubId); + if (clubInfo == null || clubInfo.getEstablishDate() == null) { + return false; + } + + Date currentTime = new Date(); + Date annualReviewStartTime = calculateAnnualReviewStartTime(clubInfo.getEstablishDate(), currentTime); + Date annualReviewDeadlineTime = addDays(annualReviewStartTime, ANNUAL_REVIEW_WINDOW_DAYS); + + // 如果当前不在年审窗口期,返回false + if (currentTime.before(annualReviewStartTime) || currentTime.after(annualReviewDeadlineTime)) { + return false; + } + + return checkAnnualReviewSubmissionWithPrecision(clubId, annualReviewStartTime, currentTime); + } + + /** + * 计算年审开始时间(精确到具体时刻) + * + * @param establishDate 社团成立时间 + * @param currentDate 当前时间 + * @return 今年的年审开始时间 + */ + private Date calculateAnnualReviewStartTime(Date establishDate, Date currentDate) { + 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.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)); + + return annualReviewCal.getTime(); + } + + /** + * 精确检查年审提交情况(关键改进 - 支持秒级精度) + * + * @param clubId 社团ID + * @param windowStartTime 窗口开始时间 + * @param queryTime 查询时间点 + * @return 是否已提交年审 + */ + private boolean checkAnnualReviewSubmissionWithPrecision(Long clubId, Date windowStartTime, Date queryTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(StglClubAnnualReview::getClubId, clubId) + .ge(StglClubAnnualReview::getCreateTime, windowStartTime) // 大于等于窗口开始时间 + .le(StglClubAnnualReview::getCreateTime, queryTime) // 小于等于当前查询时间 + .ne(StglClubAnnualReview::getStatus, "0"); // 非草稿状态 + + long count = annualReviewMapper.selectCount(wrapper); + + log.debug("年审提交检查 - 社团ID: {}, 窗口开始: {}, 查询时间: {}, 提交记录数: {}", + clubId, windowStartTime, queryTime, count); + + return count > 0; + } + + /** + * 日期加天数工具方法 + * + * @param date 原始日期 + * @param days 要添加的天数 + * @return 新日期 + */ + private Date addDays(Date date, int days) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(Calendar.DAY_OF_MONTH, days); + return cal.getTime(); + } +} \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubServiceImpl.java b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubServiceImpl.java index 70ef2fac..0d18877f 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/service/impl/StglClubServiceImpl.java @@ -17,6 +17,7 @@ import org.dromara.stgl.domain.StglMember; import org.dromara.stgl.mapper.StglClubMapper; import org.dromara.stgl.mapper.StglMemberMapper; import org.dromara.stgl.service.IStglClubService; +import org.dromara.stgl.service.IStglClubAdvisorHistoryService; import org.dromara.stgl.utils.AdvisorRoleAssigner; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.json.utils.JsonUtils; @@ -71,6 +72,8 @@ public class StglClubServiceImpl implements IStglClubService { private final AdvisorRoleAssigner advisorRoleAssigner; // 添加用户角色Mapper private final SysUserRoleMapper sysUserRoleMapper; + // 添加指导老师历史记录Service + private final IStglClubAdvisorHistoryService advisorHistoryService; /** * 查询社团信息 @@ -342,6 +345,8 @@ public class StglClubServiceImpl implements IStglClubService { assignAdvisorRoles(add); // 为社长分配角色 assignPresidentRole(add); + // 创建指导老师历史记录 + createAdvisorHistoryRecords(add); } return flag; } @@ -372,6 +377,8 @@ public class StglClubServiceImpl implements IStglClubService { syncVicePresidentsInMembers(update); // 处理指导老师角色分配 handleAdvisorRoleAssignment(oldClub, update); + // 更新指导老师历史记录 + updateAdvisorHistoryRecords(oldClub, update); } return updateResult; @@ -1347,4 +1354,104 @@ public class StglClubServiceImpl implements IStglClubService { } } + /** + * 创建指导老师历史记录(新增社团时) + * + * @param club 社团信息 + */ + private void createAdvisorHistoryRecords(StglClub club) { + try { + List advisors = parseAdvisorsFromClub(club); + if (!advisors.isEmpty()) { + // 新建社团时,指导老师的上任时间应该是社团成立日期,而不是当前时间 + Date startTime = club.getEstablishDate() != null ? club.getEstablishDate() : new Date(); + advisorHistoryService.updateClubAdvisors(club.getId(), advisors, startTime); + log.info("为社团 {} 创建指导老师历史记录成功,指导老师数量: {},上任时间: {}", + club.getClubName(), advisors.size(), startTime); + } + } catch (Exception e) { + log.error("为社团 {} 创建指导老师历史记录失败", club.getClubName(), e); + // 不抛出异常,避免影响主流程 + } + } + + /** + * 更新指导老师历史记录(更新社团时) + * + * @param oldClub 旧的社团信息 + * @param newClub 新的社团信息 + */ + private void updateAdvisorHistoryRecords(StglClub oldClub, StglClub newClub) { + try { + List advisors = parseAdvisorsFromClub(newClub); + advisorHistoryService.updateClubAdvisors(newClub.getId(), advisors); + log.info("为社团 {} 更新指导老师历史记录成功,指导老师数量: {}", newClub.getClubName(), advisors.size()); + } catch (Exception e) { + log.error("为社团 {} 更新指导老师历史记录失败", newClub.getClubName(), e); + // 不抛出异常,避免影响主流程 + } + } + + /** + * 从社团信息中解析指导老师列表 + * + * @param club 社团信息 + * @return 指导老师列表 + */ + private List parseAdvisorsFromClub(StglClub club) { + List advisors = new ArrayList<>(); + + // 优先从JSON字段解析 + if (StringUtils.isNotBlank(club.getAdvisorsJson())) { + try { + ObjectMapper mapper = new ObjectMapper(); + List> advisorMaps = mapper.readValue( + club.getAdvisorsJson(), + new TypeReference>>() {} + ); + + for (Map advisorMap : advisorMaps) { + String name = (String) advisorMap.get("name"); + String dept = (String) advisorMap.get("dept"); + String phone = (String) advisorMap.get("phone"); + String email = (String) advisorMap.get("email"); + Long userId = null; + + // 尝试获取用户ID + Object userIdObj = advisorMap.get("userId"); + if (userIdObj != null) { + if (userIdObj instanceof Number) { + userId = ((Number) userIdObj).longValue(); + } else if (userIdObj instanceof String) { + try { + userId = Long.parseLong((String) userIdObj); + } catch (NumberFormatException e) { + // 忽略解析错误 + } + } + } + + if (StringUtils.isNotBlank(name)) { + advisors.add(new IStglClubAdvisorHistoryService.AdvisorInfo(name, dept, phone, email, userId)); + } + } + } catch (Exception e) { + log.warn("解析指导老师JSON数据失败: {}", e.getMessage()); + } + } + + // 如果JSON解析失败或为空,回退到单一字段 + if (advisors.isEmpty() && StringUtils.isNotBlank(club.getAdvisorName())) { + advisors.add(new IStglClubAdvisorHistoryService.AdvisorInfo( + club.getAdvisorName(), + club.getAdvisorDept(), + club.getAdvisorPhone(), + club.getAdvisorEmail(), + null + )); + } + + return advisors; + } + } \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/StglMyMapper.xml b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/StglMyMapper.xml index 2df4193b..9715a314 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/StglMyMapper.xml +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/StglMyMapper.xml @@ -159,6 +159,8 @@ a.end_time, a.location, a.organizer_name, + a.related_dept_name, + a.related_dept_names, CAST(a.related_type AS CHAR) as related_type, CASE WHEN a.related_type = 1 THEN c.club_name diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml index f94ee3f5..03af3451 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml @@ -14,6 +14,10 @@ + + + + @@ -55,22 +59,25 @@ SELECT sa.id, sa.activity_name, sa.activity_type, sa.activity_theme, sa.start_time, sa.end_time, sa.location, sa.description, - sa.poster_url, sa.related_club_id, sa.related_type, sa.organizer_name, sa.organizer_type, + sa.poster_url, sa.related_dept_id, sa.related_dept_name, sa.related_dept_ids, sa.related_dept_names, + sa.related_club_id, sa.related_type, sa.organizer_name, sa.organizer_type, sa.organizer_contact, sa.organizer_phone, sa.co_organizers, sa.instructors, sa.registration_type, sa.registration_start, sa.registration_end, - -- 动态计算报名状态:优先根据活动状态,再根据报名时间范围 + -- 动态计算报名状态:只有审批通过的活动才根据时间计算报名状态 CASE - -- 首先根据活动状态强制设置(优先级最高) - WHEN sa.activity_status = '0' THEN '0' -- 草稿 → 未开始(固定) - WHEN sa.activity_status = '1' THEN '0' -- 未开始 → 未开始(固定) - WHEN sa.activity_status = '2' THEN '0' -- 待审批 → 未开始(固定) - WHEN sa.activity_status = '5' THEN '2' -- 已结束 → 报名结束(固定) - WHEN sa.activity_status = '6' THEN '3' -- 已取消 → 已关闭(固定) - WHEN sa.activity_status = '7' THEN '0' -- 已驳回 → 未开始(固定) + -- 草稿、待审批、已驳回状态不映射报名状态,保持为空 + WHEN sa.activity_status IN ('0', '2', '7') THEN NULL - -- 然后根据活动状态和报名时间判断 - WHEN sa.activity_status = '3' OR sa.activity_status = '4' THEN + -- 已取消的活动设置为已关闭 + WHEN sa.activity_status = '6' THEN '3' -- 已取消 → 已关闭(固定) + + -- 只有审批通过的活动(已通过、进行中、已结束)才根据时间计算报名状态 + WHEN sa.activity_status IN ('3', '4', '5') THEN CASE + -- 已结束的活动强制设置报名结束 + WHEN sa.activity_status = '5' THEN '2' -- 已结束 → 报名结束(固定) + + -- 已通过和进行中的活动根据报名时间判断 WHEN sa.registration_start IS NULL OR sa.registration_end IS NULL THEN sa.registration_status = sa.registration_start AND NOW() <= sa.registration_end THEN '1']]> @@ -119,9 +126,9 @@ END ELSE NULL END AS related_club_name, - -- 统计报名人数(子查询) + -- 统计报名人数(子查询)- 只统计审核通过的报名记录 (SELECT COUNT(*) FROM stgl_activity_registration sar - WHERE sar.activity_id = sa.id AND sar.deleted = 0) AS registration_count, + WHERE sar.activity_id = sa.id AND sar.deleted = 0 AND sar.approval_status = 1) AS registration_count, -- 统计平均评分(子查询) (SELECT ROUND(AVG(rating_score), 1) FROM stgl_activity_rating sar2 WHERE sar2.activity_id = sa.id AND sar2.deleted = 0) AS average_rating @@ -156,17 +163,19 @@ AND ( CASE - -- 首先根据活动状态强制设置(优先级最高) - WHEN sa.activity_status = '0' THEN '0' -- 草稿 → 未开始(固定) - WHEN sa.activity_status = '1' THEN '0' -- 未开始 → 未开始(固定) - WHEN sa.activity_status = '2' THEN '0' -- 待审批 → 未开始(固定) - WHEN sa.activity_status = '5' THEN '2' -- 已结束 → 报名结束(固定) - WHEN sa.activity_status = '6' THEN '3' -- 已取消 → 已关闭(固定) - WHEN sa.activity_status = '7' THEN '0' -- 已驳回 → 未开始(固定) + -- 草稿、待审批、已驳回状态不映射报名状态,保持为空 + WHEN sa.activity_status IN ('0', '2', '7') THEN NULL - -- 然后根据活动状态和报名时间判断 - WHEN sa.activity_status = '3' OR sa.activity_status = '4' THEN + -- 已取消的活动设置为已关闭 + WHEN sa.activity_status = '6' THEN '3' -- 已取消 → 已关闭(固定) + + -- 只有审批通过的活动(已通过、进行中、已结束)才根据时间计算报名状态 + WHEN sa.activity_status IN ('3', '4', '5') THEN CASE + -- 已结束的活动强制设置报名结束 + WHEN sa.activity_status = '5' THEN '2' -- 已结束 → 报名结束(固定) + + -- 已通过和进行中的活动根据报名时间判断 WHEN sa.registration_start IS NULL OR sa.registration_end IS NULL THEN sa.registration_status = sa.registration_start AND NOW() <= sa.registration_end THEN '1']]> @@ -191,6 +200,9 @@ AND sa.registration_end <= #{registrationEnd} + + AND sa.related_dept_id = #{relatedDeptId} + AND sa.related_club_id = #{relatedClubId} @@ -258,17 +270,19 @@ AND ( CASE - -- 首先根据活动状态强制设置(优先级最高) - WHEN sa.activity_status = '0' THEN '0' -- 草稿 → 未开始(固定) - WHEN sa.activity_status = '1' THEN '0' -- 未开始 → 未开始(固定) - WHEN sa.activity_status = '2' THEN '0' -- 待审批 → 未开始(固定) - WHEN sa.activity_status = '5' THEN '2' -- 已结束 → 报名结束(固定) - WHEN sa.activity_status = '6' THEN '3' -- 已取消 → 已关闭(固定) - WHEN sa.activity_status = '7' THEN '0' -- 已驳回 → 未开始(固定) + -- 草稿、待审批、已驳回状态不映射报名状态,保持为空 + WHEN sa.activity_status IN ('0', '2', '7') THEN NULL - -- 然后根据活动状态和报名时间判断 - WHEN sa.activity_status = '3' OR sa.activity_status = '4' THEN + -- 已取消的活动设置为已关闭 + WHEN sa.activity_status = '6' THEN '3' -- 已取消 → 已关闭(固定) + + -- 只有审批通过的活动(已通过、进行中、已结束)才根据时间计算报名状态 + WHEN sa.activity_status IN ('3', '4', '5') THEN CASE + -- 已结束的活动强制设置报名结束 + WHEN sa.activity_status = '5' THEN '2' -- 已结束 → 报名结束(固定) + + -- 已通过和进行中的活动根据报名时间判断 WHEN sa.registration_start IS NULL OR sa.registration_end IS NULL THEN sa.registration_status = sa.registration_start AND NOW() <= sa.registration_end THEN '1']]> @@ -285,17 +299,19 @@ AND ( CASE - -- 首先根据活动状态强制设置(优先级最高) - WHEN sa.activity_status = '0' THEN '0' -- 草稿 → 未开始(固定) - WHEN sa.activity_status = '1' THEN '0' -- 未开始 → 未开始(固定) - WHEN sa.activity_status = '2' THEN '0' -- 待审批 → 未开始(固定) - WHEN sa.activity_status = '5' THEN '2' -- 已结束 → 报名结束(固定) - WHEN sa.activity_status = '6' THEN '3' -- 已取消 → 已关闭(固定) - WHEN sa.activity_status = '7' THEN '0' -- 已驳回 → 未开始(固定) + -- 草稿、待审批、已驳回状态不映射报名状态,保持为空 + WHEN sa.activity_status IN ('0', '2', '7') THEN NULL - -- 然后根据活动状态和报名时间判断 - WHEN sa.activity_status = '3' OR sa.activity_status = '4' THEN + -- 已取消的活动设置为已关闭 + WHEN sa.activity_status = '6' THEN '3' -- 已取消 → 已关闭(固定) + + -- 只有审批通过的活动(已通过、进行中、已结束)才根据时间计算报名状态 + WHEN sa.activity_status IN ('3', '4', '5') THEN CASE + -- 已结束的活动强制设置报名结束 + WHEN sa.activity_status = '5' THEN '2' -- 已结束 → 报名结束(固定) + + -- 已通过和进行中的活动根据报名时间判断 WHEN sa.registration_start IS NULL OR sa.registration_end IS NULL THEN sa.registration_status = sa.registration_start AND NOW() <= sa.registration_end THEN '1']]> @@ -323,6 +339,12 @@ AND sa.tenant_id = #{activity.tenantId} + + AND sa.related_dept_id = #{activity.relatedDeptId} + + + AND sa.related_club_id = #{activity.relatedClubId} + AND ( @@ -383,7 +405,8 @@ INSERT INTO stgl_activity ( id, tenant_id, create_dept, create_by, create_time, update_by, update_time, activity_name, activity_type, activity_theme, start_time, end_time, location, - description, poster_url, related_club_id, organizer_name, organizer_type, + description, poster_url, related_dept_id, related_dept_name, related_dept_ids, related_dept_names, + related_club_id, organizer_name, organizer_type, organizer_contact, organizer_phone, co_organizers, instructors, registration_type, registration_start, registration_end, registration_status, max_participants, max_teams, team_size_min, team_size_max, need_approval, @@ -394,7 +417,8 @@ #{newActivity.createBy}, #{newActivity.createTime}, #{newActivity.updateBy}, #{newActivity.updateTime}, #{newActivity.activityName}, activity_type, activity_theme, #{newActivity.startTime}, #{newActivity.endTime}, location, - description, poster_url, related_club_id, organizer_name, organizer_type, + description, poster_url, related_dept_id, related_dept_name, related_dept_ids, related_dept_names, + related_club_id, organizer_name, organizer_type, organizer_contact, organizer_phone, co_organizers, instructors, registration_type, #{newActivity.registrationStart}, #{newActivity.registrationEnd}, #{newActivity.registrationStatus}, max_participants, max_teams, team_size_min, team_size_max, need_approval, diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClassMapper.xml b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClassMapper.xml index 06ebe6c7..4e353556 100644 --- a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClassMapper.xml +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClassMapper.xml @@ -14,6 +14,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + @@ -52,6 +55,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" COALESCE(club_stats.club_count, 0) as club_count, COALESCE(volunteer_stats.volunteer_team_count, 0) as volunteer_team_count, COALESCE(activity_stats.activity_count, 0) as activity_count, + today_activity_stats.activity_name as today_activity_name, + today_activity_stats.start_time as today_activity_start_time, + today_activity_stats.end_time as today_activity_end_time, s.create_by, s.create_time, s.update_by, @@ -60,6 +66,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" s.deleted FROM xg_student_msg s LEFT JOIN xg_class_msg c ON s.class_id = c.id + AND c.tenant_id = '000000' LEFT JOIN ( SELECT m.user_id, @@ -68,6 +75,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" WHERE m.member_type = 1 AND m.deleted = 0 AND m.status = 2 + AND m.tenant_id = '000000' GROUP BY m.user_id ) club_stats ON s.id = club_stats.user_id LEFT JOIN ( @@ -78,6 +86,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" WHERE m.member_type = 2 AND m.deleted = 0 AND m.status = 2 + AND m.tenant_id = '000000' GROUP BY m.user_id ) volunteer_stats ON s.id = volunteer_stats.user_id LEFT JOIN ( @@ -125,9 +134,132 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND user_id != '' GROUP BY user_id ) activity_stats ON s.id = activity_stats.user_id + LEFT JOIN ( + -- 查询学生今日活动信息(审核通过且时间为今天的活动,只取最早的一个) + -- 使用兼容MySQL 5.7的方式,通过MIN函数和相关子查询实现 + SELECT + ta.user_id, + ta.activity_name, + DATE_FORMAT(ta.start_time, '%H:%i') as start_time, + DATE_FORMAT(ta.end_time, '%H:%i') as end_time + FROM ( + -- 个人报名的今日活动 + SELECT + ar.user_id, + a.activity_name, + a.start_time, + a.end_time + FROM stgl_activity_registration ar + INNER JOIN stgl_activity a ON ar.activity_id = a.id + AND ar.tenant_id = '000000' + AND a.tenant_id = '000000' + WHERE ar.deleted = 0 + AND ar.approval_status = 1 + AND ar.tenant_id = '000000' + AND a.deleted = 0 + AND DATE(a.start_time) = CURDATE() -- 今日活动 + AND a.tenant_id = '000000' + + UNION + + -- 团队报名的今日活动(通过团队成员信息) + SELECT + JSON_UNQUOTE(JSON_EXTRACT(ati.team_members, CONCAT('$[', numbers.n, '].userId'))) as user_id, + a.activity_name, + a.start_time, + a.end_time + FROM stgl_activity_team_info ati + CROSS JOIN ( + SELECT 0 as n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION + SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION + SELECT 10 UNION SELECT 11 UNION SELECT 12 UNION SELECT 13 UNION SELECT 14 UNION + SELECT 15 UNION SELECT 16 UNION SELECT 17 UNION SELECT 18 UNION SELECT 19 + ) numbers + INNER JOIN stgl_activity a ON ati.activity_id = a.id + AND ati.tenant_id = '000000' + AND a.tenant_id = '000000' + WHERE ati.deleted = 0 + AND ati.tenant_id = '000000' + AND a.deleted = 0 + AND DATE(a.start_time) = CURDATE() -- 今日活动 + AND a.tenant_id = '000000' + AND JSON_UNQUOTE(JSON_EXTRACT(ati.team_members, CONCAT('$[', numbers.n, '].userId'))) IS NOT NULL + AND JSON_UNQUOTE(JSON_EXTRACT(ati.team_members, CONCAT('$[', numbers.n, '].userId'))) != '' + AND EXISTS ( + SELECT 1 FROM stgl_activity_registration team_ar + WHERE team_ar.team_id = ati.id + AND team_ar.deleted = 0 + AND team_ar.approval_status = 1 + AND team_ar.registration_type = 2 + AND team_ar.tenant_id = '000000' + AND team_ar.tenant_id = '000000' + ) + AND ati.tenant_id = '000000' + ) ta + WHERE ta.user_id IS NOT NULL + AND ta.user_id != '' + -- 只取每个用户最早的活动(通过相关子查询实现,兼容MySQL 5.7) + AND ta.start_time = ( + SELECT MIN(ta2.start_time) + FROM ( + -- 个人报名的今日活动 + SELECT + ar2.user_id, + a2.start_time + FROM stgl_activity_registration ar2 + INNER JOIN stgl_activity a2 ON ar2.activity_id = a2.id + AND ar2.tenant_id = '000000' + AND a2.tenant_id = '000000' + WHERE ar2.deleted = 0 + AND ar2.approval_status = 1 + AND ar2.tenant_id = '000000' + AND a2.deleted = 0 + AND DATE(a2.start_time) = CURDATE() + AND a2.tenant_id = '000000' + + UNION + + -- 团队报名的今日活动 + SELECT + JSON_UNQUOTE(JSON_EXTRACT(ati2.team_members, CONCAT('$[', numbers2.n, '].userId'))) as user_id, + a2.start_time + FROM stgl_activity_team_info ati2 + CROSS JOIN ( + SELECT 0 as n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION + SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION + SELECT 10 UNION SELECT 11 UNION SELECT 12 UNION SELECT 13 UNION SELECT 14 UNION + SELECT 15 UNION SELECT 16 UNION SELECT 17 UNION SELECT 18 UNION SELECT 19 + ) numbers2 + INNER JOIN stgl_activity a2 ON ati2.activity_id = a2.id + AND ati2.tenant_id = '000000' + AND a2.tenant_id = '000000' + WHERE ati2.deleted = 0 + AND ati2.tenant_id = '000000' + AND a2.deleted = 0 + AND DATE(a2.start_time) = CURDATE() + AND a2.tenant_id = '000000' + AND JSON_UNQUOTE(JSON_EXTRACT(ati2.team_members, CONCAT('$[', numbers2.n, '].userId'))) IS NOT NULL + AND JSON_UNQUOTE(JSON_EXTRACT(ati2.team_members, CONCAT('$[', numbers2.n, '].userId'))) != '' + AND EXISTS ( + SELECT 1 FROM stgl_activity_registration team_ar2 + WHERE team_ar2.team_id = ati2.id + AND team_ar2.deleted = 0 + AND team_ar2.approval_status = 1 + AND team_ar2.registration_type = 2 + AND team_ar2.tenant_id = '000000' + AND team_ar2.tenant_id = '000000' + ) + AND ati2.tenant_id = '000000' + ) ta2 + WHERE ta2.user_id = ta.user_id + AND ta2.user_id IS NOT NULL + AND ta2.user_id != '' + ) + ) today_activity_stats ON s.id = today_activity_stats.user_id s.deleted = 0 AND s.student_status = '1' + AND s.tenant_id = '000000' AND s.class_id = #{classId} @@ -151,6 +283,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" s.deleted = 0 AND s.student_status = '1' + AND s.tenant_id = '000000' AND s.class_id = #{classId} @@ -185,7 +318,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND m.member_type = 1 AND m.deleted = 0 AND m.status = 2 + AND m.tenant_id = '000000' AND c.deleted = 0 + AND c.tenant_id = '000000' ORDER BY c.create_time DESC @@ -209,7 +344,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND m.member_type = 2 AND m.deleted = 0 AND m.status = 2 + AND m.tenant_id = '000000' AND vt.deleted = 0 + AND vt.tenant_id = '000000' ORDER BY vt.create_time DESC diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAdvisorHistoryMapper.xml b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAdvisorHistoryMapper.xml new file mode 100644 index 00000000..5d4b1dfc --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAdvisorHistoryMapper.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + select h.id, h.club_id, h.advisor_name, h.advisor_dept, h.advisor_phone, h.advisor_email, + h.advisor_user_id, h.status, h.start_time, h.end_time, + h.create_by, h.create_time, h.update_by, h.update_time, + c.club_name + from stgl_club_advisor_history h + left join stgl_club c on h.club_id = c.id + + + + + + + + + + + + + + update stgl_club_advisor_history + set status = 0, end_time = #{endTime}, update_time = now() + where club_id = #{clubId} and status = 1 and deleted = 0 + + and advisor_name in + + #{name} + + + + + \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAnnualReviewActivityMapper.xml b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAnnualReviewActivityMapper.xml new file mode 100644 index 00000000..6545ffd6 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAnnualReviewActivityMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + select id, tenant_id, create_dept, create_by, create_time, update_by, update_time, deleted, annual_review_id, activity_name, activity_time, activity_start_time, activity_end_time, activity_location, activity_type, participant_count, activity_description from stgl_club_annual_review_activity + + + + + + + + update stgl_club_annual_review_activity set deleted = 1 + where annual_review_id = #{annualReviewId} and deleted = 0 + + + \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAnnualReviewMapper.xml b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAnnualReviewMapper.xml new file mode 100644 index 00000000..8152d445 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAnnualReviewMapper.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select + ar.id, ar.club_id, ar.review_year, ar.review_start_date, ar.review_end_date, ar.review_time, + ar.club_name, ar.establish_date, ar.club_type, ar.advisor_name, ar.advisor_dept, + ar.has_charter, ar.charter_url, ar.has_funds, ar.fund_amount, ar.member_count, + ar.status, ar.advisor_opinion, ar.college_opinion, ar.youth_league_opinion, + ar.members_json, ar.activities_json, + ar.create_by, ar.create_time, ar.update_by, ar.update_time, ar.tenant_id, + c.president_name + from stgl_club_annual_review ar + left join stgl_club c on ar.club_id = c.id + + + + + + + + + \ No newline at end of file diff --git a/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAnnualReviewMemberMapper.xml b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAnnualReviewMemberMapper.xml new file mode 100644 index 00000000..e61337a7 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglClubAnnualReviewMemberMapper.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + select id, tenant_id, create_dept, create_by, create_time, update_by, update_time, deleted, annual_review_id, member_name, class_name, role, contact_info from stgl_club_annual_review_member + + + + + + + + update stgl_club_annual_review_member set deleted = 1 + where annual_review_id = #{annualReviewId} and deleted = 0 + + + \ No newline at end of file diff --git a/docs/设计文档/database/社团管理/社团管理数据库设计.md b/docs/设计文档/database/社团管理/社团管理数据库设计.md index aae58e51..a6c38e26 100644 --- a/docs/设计文档/database/社团管理/社团管理数据库设计.md +++ b/docs/设计文档/database/社团管理/社团管理数据库设计.md @@ -190,6 +190,8 @@ CREATE TABLE `stgl_activity` ( `location` varchar(200) COMMENT '活动地点', `description` text COMMENT '活动简介(1000字以内)', `poster_url` varchar(500) COMMENT '活动海报路径', + `related_dept_id` bigint(20) COMMENT '关联部门ID', + `related_dept_name` varchar(100) COMMENT '关联部门名称', `related_club_id` bigint(20) COMMENT '关联社团ID', -- 主办承办方信息 @@ -225,6 +227,7 @@ CREATE TABLE `stgl_activity` ( KEY `idx_end_time` (`end_time`), KEY `idx_activity_status` (`activity_status`), KEY `idx_registration_status` (`registration_status`), + KEY `idx_related_dept_id` (`related_dept_id`), KEY `idx_related_club_id` (`related_club_id`), KEY `idx_create_time` (`create_time`), KEY `idx_tenant_deleted` (`tenant_id`, `deleted`) diff --git a/web/src/api/stgl/activity/types.ts b/web/src/api/stgl/activity/types.ts index e5640174..2b40215c 100644 --- a/web/src/api/stgl/activity/types.ts +++ b/web/src/api/stgl/activity/types.ts @@ -44,6 +44,26 @@ export interface StglActivityVO { */ posterUrl?: string; + /** + * 关联部门ID(多个部门时用逗号分隔) + */ + relatedDeptId?: string | number; + + /** + * 关联部门ID列表 + */ + relatedDeptIds?: (string | number)[]; + + /** + * 关联部门名称(多个部门时用逗号分隔) + */ + relatedDeptName?: string; + + /** + * 关联部门名称列表 + */ + relatedDeptNames?: string[]; + /** * 关联社团ID */ @@ -272,6 +292,26 @@ export interface StglActivityForm { */ posterUrl?: string; + /** + * 关联部门ID(多个部门时用逗号分隔) + */ + relatedDeptId?: string | number; + + /** + * 关联部门ID列表 + */ + relatedDeptIds?: (string | number)[]; + + /** + * 关联部门对象(用于选择器绑定) + */ + relatedDept?: DeptOption | null; + + /** + * 关联部门对象列表(多选时使用) + */ + relatedDepts?: DeptOption[] | null; + /** * 关联社团ID */ @@ -454,6 +494,11 @@ export interface StglActivityQuery extends PageQuery { */ location?: string; + /** + * 关联部门ID + */ + relatedDeptId?: string | number; + /** * 关联社团ID */ @@ -505,6 +550,21 @@ export interface StglActivityQuery extends PageQuery { params?: any; } +/** + * 部门选择器选项 + */ +export interface DeptOption { + /** + * 部门ID + */ + deptId: string | number; + + /** + * 部门名称 + */ + deptName: string; +} + /** * 社团/志愿者队伍选择器选项 */ diff --git a/web/src/api/stgl/activityregisterhall/types.ts b/web/src/api/stgl/activityregisterhall/types.ts index a4acf966..6500ba7c 100644 --- a/web/src/api/stgl/activityregisterhall/types.ts +++ b/web/src/api/stgl/activityregisterhall/types.ts @@ -45,6 +45,26 @@ export interface StglActivityVo { */ posterUrl: string; + /** + * 关联部门ID(多个部门时用逗号分隔) + */ + relatedDeptId?: string | number; + + /** + * 关联部门ID列表 + */ + relatedDeptIds?: (string | number)[]; + + /** + * 关联部门名称(多个部门时用逗号分隔) + */ + relatedDeptName?: string; + + /** + * 关联部门名称列表 + */ + relatedDeptNames?: string[]; + /** * 关联社团ID */ diff --git a/web/src/api/stgl/club/advisor-history.ts b/web/src/api/stgl/club/advisor-history.ts new file mode 100644 index 00000000..143cfc0e --- /dev/null +++ b/web/src/api/stgl/club/advisor-history.ts @@ -0,0 +1,276 @@ +import request from '@/utils/request'; +import { AxiosPromise } from 'axios'; + +export interface StglClubAdvisorHistoryVO { + /** + * 主键ID + */ + id?: string | number; + + /** + * 社团ID + */ + clubId?: string | number; + + /** + * 社团名称 + */ + clubName?: string; + + /** + * 指导老师姓名 + */ + advisorName?: string; + + /** + * 指导老师部门 + */ + advisorDept?: string; + + /** + * 指导老师电话 + */ + advisorPhone?: string; + + /** + * 指导老师邮箱 + */ + advisorEmail?: string; + + /** + * 指导老师用户ID + */ + advisorUserId?: string | number; + + /** + * 在任状态:1-当前在任,0-历史(已离任) + */ + status?: number; + + /** + * 上任时间 + */ + startTime?: string; + + /** + * 离任时间 + */ + endTime?: string; + + /** + * 创建者 + */ + createBy?: string | number; + + /** + * 创建时间 + */ + createTime?: string; + + /** + * 更新者 + */ + updateBy?: string | number; + + /** + * 更新时间 + */ + updateTime?: string; +} + +export interface StglClubAdvisorHistoryForm extends BaseEntity { + /** + * 主键ID + */ + id?: string | number; + + /** + * 社团ID + */ + clubId?: string | number; + + /** + * 指导老师姓名 + */ + advisorName?: string; + + /** + * 指导老师部门 + */ + advisorDept?: string; + + /** + * 指导老师电话 + */ + advisorPhone?: string; + + /** + * 指导老师邮箱 + */ + advisorEmail?: string; + + /** + * 指导老师用户ID + */ + advisorUserId?: string | number; + + /** + * 在任状态:1-当前在任,0-历史(已离任) + */ + status?: number; + + /** + * 上任时间 + */ + startTime?: string; + + /** + * 离任时间 + */ + endTime?: string; +} + +export interface StglClubAdvisorHistoryQuery extends PageQuery { + /** + * 社团ID + */ + clubId?: string | number; + + /** + * 指导老师姓名 + */ + advisorName?: string; + + /** + * 指导老师部门 + */ + advisorDept?: string; + + /** + * 指导老师电话 + */ + advisorPhone?: string; + + /** + * 指导老师邮箱 + */ + advisorEmail?: string; + + /** + * 指导老师用户ID + */ + advisorUserId?: string | number; + + /** + * 在任状态:1-当前在任,0-历史(已离任) + */ + status?: number; + + /** + * 上任时间 + */ + startTime?: string; + + /** + * 离任时间 + */ + endTime?: string; + + /** + * 日期范围参数 + */ + params?: any; +} + +/** + * 查询社团指导老师任职历史列表 + * @param query + * @returns {*} + */ + +export const listStglClubAdvisorHistory = (query?: StglClubAdvisorHistoryQuery): AxiosPromise => { + return request({ + url: '/stgl/club/advisor/history/list', + method: 'get', + params: query + }); +}; + +/** + * 查询社团指导老师任职历史详细 + * @param id + */ +export const getStglClubAdvisorHistory = (id: string | number): AxiosPromise => { + return request({ + url: '/stgl/club/advisor/history/' + id, + method: 'get' + }); +}; + +/** + * 查询指定社团的当前在任指导老师 + * @param clubId + */ +export const getCurrentAdvisors = (clubId: string | number): AxiosPromise => { + return request({ + url: '/stgl/club/advisor/history/current/' + clubId, + method: 'get' + }); +}; + +/** + * 查询指定社团的历史指导老师(已离任) + * @param clubId + */ +export const getHistoryAdvisors = (clubId: string | number): AxiosPromise => { + return request({ + url: '/stgl/club/advisor/history/history/' + clubId, + method: 'get' + }); +}; + +/** + * 查询指定社团的所有指导老师(包含当前和历史) + * @param clubId + */ +export const getAllAdvisors = (clubId: string | number): AxiosPromise => { + return request({ + url: '/stgl/club/advisor/history/all/' + clubId, + method: 'get' + }); +}; + +/** + * 新增社团指导老师任职历史 + * @param data + */ +export const addStglClubAdvisorHistory = (data: StglClubAdvisorHistoryForm) => { + return request({ + url: '/stgl/club/advisor/history', + method: 'post', + data: data + }); +}; + +/** + * 修改社团指导老师任职历史 + * @param data + */ +export const updateStglClubAdvisorHistory = (data: StglClubAdvisorHistoryForm) => { + return request({ + url: '/stgl/club/advisor/history', + method: 'put', + data: data + }); +}; + +/** + * 删除社团指导老师任职历史 + * @param id + */ +export const delStglClubAdvisorHistory = (id: string | number | Array) => { + return request({ + url: '/stgl/club/advisor/history/' + id, + method: 'delete' + }); +}; \ No newline at end of file diff --git a/web/src/api/stgl/club/annualReview/index.ts b/web/src/api/stgl/club/annualReview/index.ts new file mode 100644 index 00000000..3951066c --- /dev/null +++ b/web/src/api/stgl/club/annualReview/index.ts @@ -0,0 +1,222 @@ +import request from '@/utils/request'; +import { AxiosPromise } from 'axios'; +import { StglClubAnnualReviewVO, StglClubAnnualReviewForm, StglClubAnnualReviewQuery } from './type'; + +/** + * 查询社团年审记录列表 + * @param query + * @returns {*} + */ +export const listStglClubAnnualReview = (query?: StglClubAnnualReviewQuery): AxiosPromise => { + return request({ + url: '/stgl/club/annualReview/list', + method: 'get', + params: query, + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 查询我的社团年审记录列表 + * @param query + * @returns {*} + */ +export const listMyStglClubAnnualReview = (query?: StglClubAnnualReviewQuery): AxiosPromise => { + return request({ + url: '/stgl/club/annualReview/myList', + method: 'get', + params: query, + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 查询社团年审记录详细 + * @param id + */ +export const getStglClubAnnualReview = (id: string | number): AxiosPromise => { + return request({ + url: '/stgl/club/annualReview/' + id, + method: 'get', + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 新增社团年审记录 + * @param data + */ +export const addStglClubAnnualReview = (data: StglClubAnnualReviewForm) => { + return request({ + url: '/stgl/club/annualReview', + method: 'post', + data: data, + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 修改社团年审记录 + * @param data + */ +export const updateStglClubAnnualReview = (data: StglClubAnnualReviewForm) => { + return request({ + url: '/stgl/club/annualReview', + method: 'put', + data: data, + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 删除社团年审记录 + * @param id + */ +export const delStglClubAnnualReview = (id: string | number | Array) => { + return request({ + url: '/stgl/club/annualReview/' + id, + method: 'delete', + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 提交年审记录 + * @param id + */ +export const submitStglClubAnnualReview = (id: string | number) => { + return request({ + url: '/stgl/club/annualReview/submit/' + id, + method: 'post', + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 撤回年审记录 + * @param id + */ +export const withdrawStglClubAnnualReview = (id: string | number) => { + return request({ + url: '/stgl/club/annualReview/withdraw/' + id, + method: 'post', + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 审核年审记录 + * @param params + */ +export const auditStglClubAnnualReview = (params: { + id: string | number; + auditResult: string; + auditOpinion?: string; + auditType: string; +}) => { + return request({ + url: '/stgl/club/annualReview/audit', + method: 'post', + params: params, + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 检查是否可以创建年审记录 + * @param clubId + * @param reviewYear + */ +export const checkCanCreateStglClubAnnualReview = (clubId: string | number, reviewYear: number): AxiosPromise => { + return request({ + url: '/stgl/club/annualReview/check', + method: 'get', + params: { + clubId, + reviewYear + }, + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 获取年审状态选项 + */ +export const getAnnualReviewStatusOptions = (): AxiosPromise => { + return request({ + url: '/stgl/club/annualReview/statusOptions', + method: 'get', + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 导出社团年审记录 + * @param query + */ +export const exportStglClubAnnualReview = (query?: StglClubAnnualReviewQuery) => { + return request({ + url: '/stgl/club/annualReview/export', + method: 'post', + data: query, + responseType: 'blob', + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 根据社团ID和年审年度获取该年度的活动数据 + * @param clubId 社团ID + * @param reviewYear 年审年度 (例如:2024) + */ +export const getClubActivitiesByReviewYear = (clubId: string | number, reviewYear: number) => { + return request({ + url: '/stgl/club/annualReview/clubActivities', + method: 'get', + params: { + clubId, + reviewYear + }, + headers: { + isEncrypt: 'true' + } + }); +}; + +/** + * 获取年审记录的活动数据(从stgl_club_annual_review_activity表中获取) + * @param annualReviewId 年审记录ID + */ +export const getAnnualReviewActivities = (annualReviewId: string | number) => { + return request({ + url: '/stgl/club/annualReview/' + annualReviewId + '/activities', + method: 'get', + headers: { + isEncrypt: 'true' + } + }); +}; \ No newline at end of file diff --git a/web/src/api/stgl/club/annualReview/type.ts b/web/src/api/stgl/club/annualReview/type.ts new file mode 100644 index 00000000..c70b94d3 --- /dev/null +++ b/web/src/api/stgl/club/annualReview/type.ts @@ -0,0 +1,323 @@ +export interface StglClubAnnualReviewVO { + /** + * 主键ID + */ + id: string | number; + + /** + * 社团ID + */ + clubId: string | number; + + /** + * 年审年度 + */ + reviewYear: number; + + /** + * 年审开始日期 + */ + reviewStartDate: string; + + /** + * 年审结束日期 + */ + reviewEndDate: string; + + /** + * 年审时间 + */ + reviewTime: string; + + /** + * 社团名称 + */ + clubName: string; + + /** + * 成立日期 + */ + establishDate: string; + + /** + * 社长姓名 + */ + presidentName: string; + + /** + * 社团类型 + */ + clubType: string; + + /** + * 指导老师姓名 + */ + advisorName: string; + + /** + * 指导老师部门 + */ + advisorDept: string; + + /** + * 是否有章程 + */ + hasCharter: string; + + /** + * 章程文件URL + */ + charterUrl: string; + + /** + * 是否有资金 + */ + hasFunds: string; + + /** + * 资金数额 + */ + fundAmount: number; + + /** + * 成员数量 + */ + memberCount: number; + + /** + * 状态(0-草稿 1-已提交 2-审核中 3-审核通过 4-审核不通过) + */ + status: string; + + /** + * 指导老师意见 + */ + advisorOpinion: string; + + /** + * 学院意见 + */ + collegeOpinion: string; + + /** + * 团委意见 + */ + youthLeagueOpinion: string; + + /** + * 创建者 + */ + createBy: string | number; + + /** + * 创建时间 + */ + createTime: string; + + /** + * 更新者 + */ + updateBy: string | number; + + /** + * 更新时间 + */ + updateTime: string; +} + +export interface StglClubAnnualReviewForm { + /** + * 主键ID + */ + id?: string | number; + + /** + * 社团ID + */ + clubId: string | number; + + /** + * 年审年度 + */ + reviewYear: number; + + /** + * 年审开始日期 + */ + reviewStartDate: string; + + /** + * 年审结束日期 + */ + reviewEndDate: string; + + /** + * 年审时间 + */ + reviewTime: string; + + /** + * 社团名称 + */ + clubName: string; + + /** + * 成立日期 + */ + establishDate?: string; + + /** + * 社团类型 + */ + clubType?: string; + + /** + * 指导老师姓名 + */ + advisorName?: string; + + /** + * 指导老师部门 + */ + advisorDept?: string; + + /** + * 是否有章程 + */ + hasCharter?: string; + + /** + * 章程文件URL + */ + charterUrl?: string; + + /** + * 是否有资金 + */ + hasFunds?: string; + + /** + * 资金数额 + */ + fundAmount?: number; + + /** + * 成员数量 + */ + memberCount?: number; + + /** + * 状态(0-草稿 1-已提交 2-审核中 3-审核通过 4-审核不通过) + */ + status?: string; + + /** + * 指导老师意见 + */ + advisorOpinion?: string; + + /** + * 学院意见 + */ + collegeOpinion?: string; + + /** + * 团委意见 + */ + youthLeagueOpinion?: string; + + /** + * 成员信息JSON + */ + membersJson?: string; + + /** + * 活动信息JSON + */ + activitiesJson?: string; +} + +export interface StglClubAnnualReviewQuery extends PageQuery { + /** + * 社团ID + */ + clubId?: string | number; + + /** + * 年审年度 + */ + reviewYear?: number; + + /** + * 年审开始日期范围 + */ + beginReviewStartDate?: string; + endReviewStartDate?: string; + + /** + * 年审结束日期范围 + */ + beginReviewEndDate?: string; + endReviewEndDate?: string; + + /** + * 社团名称 + */ + clubName?: string; + + /** + * 社团类型 + */ + clubType?: string; + + /** + * 状态(0-草稿 1-已提交 2-审核中 3-审核通过 4-审核不通过) + */ + status?: string; + + /** + * 其他查询参数 + */ + params?: { [key: string]: any }; +} + +/** + * 分页查询接口 + */ +export interface PageQuery { + /** + * 分页大小 + */ + pageSize?: number; + /** + * 当前页数 + */ + pageNum?: number; + /** + * 排序列 + */ + orderByColumn?: string; + /** + * 排序的方向desc或者asc + */ + isAsc?: string; +} + +/** + * 年审状态选项 + */ +export const ANNUAL_REVIEW_STATUS_OPTIONS = [ + { label: '草稿', value: '0' }, + { label: '已提交', value: '1' }, + { label: '审核中', value: '2' }, + { label: '审核通过', value: '3' }, + { label: '审核不通过', value: '4' } +]; + +/** + * 是否选项 + */ +export const YES_NO_OPTIONS = [ + { label: '是', value: 'Y' }, + { label: '否', value: 'N' } +]; \ No newline at end of file diff --git a/web/src/api/stgl/clubPermission.ts b/web/src/api/stgl/clubPermission.ts new file mode 100644 index 00000000..c4d5464a --- /dev/null +++ b/web/src/api/stgl/clubPermission.ts @@ -0,0 +1,93 @@ +import request from '@/utils/request' + +// 社团活动权限检查结果类型定义 +export interface ClubActivityPermissionResult { + canCreateActivity: boolean + status: 'NORMAL' | 'TEMPORARILY_LIMITED' | 'PERMANENTLY_LIMITED' + message: string + deadlineTime?: string + remainingHours?: number + remainingDays?: number + annualReviewStartTime?: string +} + +// 权限详情类型定义 +export interface ClubPermissionDetails { + permissionResult: ClubActivityPermissionResult + isInAnnualReviewWindow: boolean + daysUntilNextAnnualReview: number + hasSubmittedCurrentYearAnnualReview: boolean +} + +/** + * 检查社团活动权限 + * @param clubId 社团ID (支持字符串和数字类型,避免大整数精度丢失) + * @returns 权限检查结果 + */ +export function checkClubActivityPermission(clubId: number | string) { + return request({ + url: `/stgl/club/permission/checkActivityPermission/${clubId}`, + method: 'get' + }) +} + +/** + * 批量检查多个社团的权限状态 + * @param clubIds 社团ID列表 + * @returns 权限检查结果映射 + */ +export function batchCheckClubPermission(clubIds: number[]) { + return request>({ + url: '/stgl/club/permission/batchCheckPermission', + method: 'post', + data: clubIds + }) +} + +/** + * 检查社团是否在年审窗口期内 + * @param clubId 社团ID + * @returns 是否在年审窗口期 + */ +export function isInAnnualReviewWindow(clubId: number) { + return request({ + url: `/stgl/club/permission/isInAnnualReviewWindow/${clubId}`, + method: 'get' + }) +} + +/** + * 获取社团距离下次年审的天数 + * @param clubId 社团ID + * @returns 距离年审的天数 + */ +export function getDaysUntilNextAnnualReview(clubId: number) { + return request({ + url: `/stgl/club/permission/getDaysUntilNextAnnualReview/${clubId}`, + method: 'get' + }) +} + +/** + * 检查社团是否已在当前年审期内提交年审 + * @param clubId 社团ID + * @returns 是否已提交年审 + */ +export function hasSubmittedCurrentYearAnnualReview(clubId: number) { + return request({ + url: `/stgl/club/permission/hasSubmittedCurrentYearAnnualReview/${clubId}`, + method: 'get' + }) +} + +/** + * 获取社团权限详细信息(综合接口) + * @param clubId 社团ID + * @returns 权限详细信息 + */ +export function getClubPermissionDetails(clubId: number) { + return request({ + url: `/stgl/club/permission/getPermissionDetails/${clubId}`, + method: 'get' + }) +} \ No newline at end of file diff --git a/web/src/api/stgl/my/types.ts b/web/src/api/stgl/my/types.ts index 056375f0..7c2a61bc 100644 --- a/web/src/api/stgl/my/types.ts +++ b/web/src/api/stgl/my/types.ts @@ -97,6 +97,10 @@ export interface MyActivityVO { undertakerName?: string; /** 关联类型(1-社团 2-志愿者队伍) */ relatedType?: number; + /** 关联部门名称 */ + relatedDeptName?: string; + /** 关联部门名称列表 */ + relatedDeptNames?: string; /** 关联组织名称(社团名称或志愿者团队名称) */ organizationName?: string; /** 指导老师 */ diff --git a/web/src/api/stgl/stclass/types.ts b/web/src/api/stgl/stclass/types.ts index b075ad4a..a92bb195 100644 --- a/web/src/api/stgl/stclass/types.ts +++ b/web/src/api/stgl/stclass/types.ts @@ -74,6 +74,21 @@ export interface StglClassVO { * 参与活动数量 */ activityCount: number; + + /** + * 今日活动名称(审核通过且时间为今天的活动) + */ + todayActivityName?: string; + + /** + * 今日活动开始时间 + */ + todayActivityStartTime?: string; + + /** + * 今日活动结束时间 + */ + todayActivityEndTime?: string; } /** diff --git a/web/src/components/AdvisorHistoryDialog/index.vue b/web/src/components/AdvisorHistoryDialog/index.vue new file mode 100644 index 00000000..44c905a8 --- /dev/null +++ b/web/src/components/AdvisorHistoryDialog/index.vue @@ -0,0 +1,399 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/AdvisorManager/index.vue b/web/src/components/AdvisorManager/index.vue index 62105d79..f5e869e0 100644 --- a/web/src/components/AdvisorManager/index.vue +++ b/web/src/components/AdvisorManager/index.vue @@ -64,10 +64,14 @@ export interface AdvisorVO { dept: string; email: string; userId?: string; + startTime?: string; // 上任时间 + endTime?: string; // 离任时间 } interface Props { modelValue: AdvisorVO[]; + establishDate?: string; // 社团成立日期 + isNewClub?: boolean; // 是否为新建社团 } interface Emits { @@ -78,42 +82,74 @@ interface Emits { const props = defineProps(); const emit = defineEmits(); -const advisors = ref([...props.modelValue]); +const advisors = ref([]); -// 监听props变化 +// 使用computed来避免双重监听导致的递归更新 +const advisorsData = computed({ + get: () => props.modelValue || [], + set: (newValue: AdvisorVO[]) => { + emit('update:modelValue', newValue); + emit('change', newValue); + } +}); + +// 初始化数据 +advisors.value = advisorsData.value.map((item) => ({ ...item })); + +// 只监听props变化来同步内部数据 watch( () => props.modelValue, (newValue) => { - advisors.value = newValue ? newValue.map((item) => ({ ...item })) : []; + console.log('AdvisorManager: props.modelValue 变化:', newValue); + if (newValue) { + advisors.value = newValue.map((item) => ({ ...item })); + console.log('AdvisorManager: 设置后的 advisors.value:', advisors.value); + } else { + advisors.value = []; + } }, - { deep: true } -); - -// 监听内部数据变化,同步到父组件 -watch( - advisors, - (newValue) => { - emit('update:modelValue', newValue); - emit('change', newValue); - }, - { deep: true } + { deep: true, immediate: true } ); // 添加指导老师 const addAdvisor = () => { - advisors.value.push({ + // 智能设置上任时间: + // 1. 如果是新建社团的首任指导老师且有社团成立日期,使用成立日期 + // 2. 否则使用当前日期 + let startTime = ''; + const isFirstAdvisor = advisors.value.length === 0; + const isNewClub = props.isNewClub !== false; // 默认认为是新建社团,除非明确传入false + + if (isFirstAdvisor && isNewClub && props.establishDate) { + startTime = props.establishDate; + console.log('AdvisorManager: 新建社团的首任指导老师,上任时间设置为社团成立日期:', startTime); + } else { + const today = new Date(); + startTime = today.getFullYear() + '-' + + String(today.getMonth() + 1).padStart(2, '0') + '-' + + String(today.getDate()).padStart(2, '0'); + console.log('AdvisorManager: 后续新增指导老师,上任时间设置为当前日期:', startTime); + } + + const newAdvisors = [...advisors.value, { teacher: null, name: '', phone: '', dept: '', email: '', - userId: '' - }); + userId: '', + startTime: startTime, + endTime: undefined + }]; + advisors.value = newAdvisors; + advisorsData.value = newAdvisors; }; // 删除指导老师 const removeAdvisor = (index: number) => { - advisors.value.splice(index, 1); + const newAdvisors = advisors.value.filter((_, i) => i !== index); + advisors.value = newAdvisors; + advisorsData.value = newAdvisors; }; // 监听teacher选择变化,自动填充相关信息 @@ -121,28 +157,46 @@ const handleTeacherChange = (advisor: AdvisorVO, teacher: TeacherVO | null) => { console.log('handleTeacherChange called with teacher:', teacher); if (teacher) { - // 自动填充老师信息 + // 自动填充老师信息,保留时间字段 + const originalStartTime = advisor.startTime; + const originalEndTime = advisor.endTime; + + advisor.teacher = teacher; advisor.name = teacher.nickName || ''; advisor.phone = teacher.phonenumber || '暂无电话'; advisor.dept = teacher.deptName || ''; advisor.email = teacher.email || '暂无邮箱'; advisor.userId = teacher.userId?.toString() || ''; + advisor.startTime = originalStartTime; // 保留原有的上任时间 + advisor.endTime = originalEndTime; // 保留原有的离任时间 console.log('Updated advisor:', { name: advisor.name, phone: advisor.phone, dept: advisor.dept, email: advisor.email, - userId: advisor.userId + userId: advisor.userId, + startTime: advisor.startTime, + endTime: advisor.endTime }); } else { - // 清空选择时重置所有信息 + // 清空选择时重置所有信息,但保留时间字段 + const originalStartTime = advisor.startTime; + const originalEndTime = advisor.endTime; + + advisor.teacher = null; advisor.name = ''; advisor.phone = ''; advisor.dept = ''; advisor.email = ''; advisor.userId = ''; + advisor.startTime = originalStartTime; // 保留原有的上任时间 + advisor.endTime = originalEndTime; // 保留原有的离任时间 } + + // 触发更新 + const newAdvisors = [...advisors.value]; + advisorsData.value = newAdvisors; }; // 移除有问题的深度监听,改为在TeacherSelector的change事件中处理 diff --git a/web/src/components/DeptSelector/MultiDeptSelector.vue b/web/src/components/DeptSelector/MultiDeptSelector.vue new file mode 100644 index 00000000..7ed5e1fc --- /dev/null +++ b/web/src/components/DeptSelector/MultiDeptSelector.vue @@ -0,0 +1,136 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/DeptSelector/index.vue b/web/src/components/DeptSelector/index.vue new file mode 100644 index 00000000..00156bb7 --- /dev/null +++ b/web/src/components/DeptSelector/index.vue @@ -0,0 +1,100 @@ + + + \ No newline at end of file diff --git a/web/src/components/FileUpload/index.vue b/web/src/components/FileUpload/index.vue index ae2a2205..91aacb65 100644 --- a/web/src/components/FileUpload/index.vue +++ b/web/src/components/FileUpload/index.vue @@ -38,7 +38,7 @@
删除 - 下载 + 下载
@@ -155,7 +155,7 @@ const handleDownloadAttachment = async (file: any) => { }); // 由于axios响应拦截器对blob类型响应直接返回blob数据 // 这里response直接就是blob对象,而不是包含data属性的响应对象 - const blob = response; + const blob = response as unknown as Blob; // 验证响应数据 if (!blob) { @@ -306,4 +306,19 @@ const listToString = (list: any[], separator?: string) => { .ele-upload-list__item-content-action .el-link { margin-right: 10px; } + +.ele-upload-list__item-content-action { + margin-left: 15px; +} + +.ele-upload-list__item-content-action .el-button--primary.is-link { + border: none !important; + padding: 0 !important; +} + +.ele-upload-list__item-content-action .el-button--danger.is-link { + border: none !important; + padding: 0 !important; + margin-right: 10px; +} diff --git a/web/src/components/TeacherSelector/index.vue b/web/src/components/TeacherSelector/index.vue index 21583990..9f0ca9df 100644 --- a/web/src/components/TeacherSelector/index.vue +++ b/web/src/components/TeacherSelector/index.vue @@ -157,10 +157,13 @@ const treeProps = { watch( () => props.modelValue, (newValue) => { + console.log('TeacherSelector: props.modelValue 变化:', newValue); if (newValue) { displayValue.value = newValue.nickName; + console.log('TeacherSelector: 设置 displayValue 为:', displayValue.value); } else { displayValue.value = ''; + console.log('TeacherSelector: 清空 displayValue'); } }, { immediate: true } diff --git a/web/src/views/stgl/activity/components/ActivityDetailView.vue b/web/src/views/stgl/activity/components/ActivityDetailView.vue index ad70a5eb..07b6218c 100644 --- a/web/src/views/stgl/activity/components/ActivityDetailView.vue +++ b/web/src/views/stgl/activity/components/ActivityDetailView.vue @@ -14,10 +14,24 @@ {{ activity.location || '暂无' }} {{ activity.startTime || '暂无' }} {{ activity.endTime || '暂无' }} + +
+ + {{ deptName }} + +
+ 暂无 +
+ {{ activity.relatedClubName || '暂无' }} - {{ activity.relatedClubName || '暂无' }} @@ -36,14 +50,17 @@ + + + @@ -235,6 +255,37 @@ const instructorList = computed(() => { } }); +// 获取关联部门名称列表 +const getRelatedDeptNames = (activity: StglActivityVO | StglActivityVo): string[] => { + // 优先检查relatedDeptNames字段(多部门支持) + if ('relatedDeptNames' in activity && activity.relatedDeptNames) { + // 如果是数组类型,直接使用 + if (Array.isArray(activity.relatedDeptNames)) { + return activity.relatedDeptNames.filter(name => name); + } + // 如果是字符串类型(逗号分隔),进行分割 + const deptNamesStr = activity.relatedDeptNames as unknown as string; + if (typeof deptNamesStr === 'string' && deptNamesStr.includes(',')) { + return deptNamesStr.split(',').map(name => name.trim()).filter(name => name); + } + // 如果是单个字符串 + if (typeof deptNamesStr === 'string' && deptNamesStr.trim()) { + return [deptNamesStr.trim()]; + } + } + + // 兼容旧的单部门字段 + if (activity.relatedDeptName) { + if (activity.relatedDeptName.includes(',')) { + return activity.relatedDeptName.split(',').map(name => name.trim()).filter(name => name); + } else { + return [activity.relatedDeptName]; + } + } + + return []; +}; + // 加载附件信息 const loadAttachments = async () => { // 重置附件列表 @@ -543,6 +594,20 @@ onMounted(() => { margin-bottom: 20px; } +/* 部门列表显示样式 */ +.dept-list { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; +} + +.dept-tag { + margin: 0; + font-size: 12px; + border-radius: 4px; +} + /* 附件显示区域样式 */ .attachment-list { display: flex; @@ -641,3 +706,4 @@ onMounted(() => { } } + diff --git a/web/src/views/stgl/activity/hall/index.vue b/web/src/views/stgl/activity/hall/index.vue index 76df9275..08ce600f 100644 --- a/web/src/views/stgl/activity/hall/index.vue +++ b/web/src/views/stgl/activity/hall/index.vue @@ -18,8 +18,8 @@ - - + + @@ -61,7 +61,23 @@ - + + + - + @@ -648,6 +682,7 @@ import { StglClubVO, StglClubQuery, StglClubForm } from '@/api/stgl/club/types'; import StudentSelector from '@/components/StudentSelector/index.vue'; import AdvisorManager from '@/components/AdvisorManager/index.vue'; import MemberManagementDialog from './components/MemberManagementDialog.vue'; +import AdvisorHistoryDialog from '@/components/AdvisorHistoryDialog/index.vue'; import { StudentVO } from '@/api/xg/student/types'; import { AdvisorVO } from '@/components/AdvisorManager/index.vue'; import { Document } from '@element-plus/icons-vue'; @@ -711,6 +746,14 @@ const memberDialog = reactive({ const currentClub = ref(null); const memberActiveTab = ref('joined'); +// 指导老师历史记录相关 +const advisorHistoryDialog = reactive({ + visible: false, + title: '' +}); + +const currentAdvisorClub = ref(null); + interface TransferForm { clubId?: string | number; oldPresidentName?: string; @@ -850,12 +893,7 @@ const getList = async () => { const res = await listStglClub(queryParams.value); - // 🔥 调试信息:检查返回的数据结构 - console.log('getList: 获取到的社团列表数据:', res); - if (res.rows && res.rows.length > 0) { - console.log('getList: 第一条社团数据详情:', res.rows[0]); - console.log('getList: 第一条社团的advisorsJson字段:', res.rows[0].advisorsJson); - } + // 处理列表数据:将状态字段从数字类型转换为字符串类型,确保字典组件正常显示 if (res.rows && Array.isArray(res.rows)) { @@ -927,10 +965,8 @@ const handleAdd = () => { const handleUpdate = async (row?: StglClubVO) => { reset(); const _id = row?.id || ids.value[0]; - console.log('handleUpdate: 开始获取社团数据,ID:', _id); const res = await getStglClub(_id); - console.log('handleUpdate: API响应数据:', res.data); // 处理经费来源数据:如果是字符串则分割为数组 if (res.data.fundingSources) { @@ -964,37 +1000,30 @@ const handleUpdate = async (row?: StglClubVO) => { } // 处理副社长信息:优先从JSON字段读取,如果没有则使用单一字段 - console.log('handleUpdate: 副社长原始数据:', { - vicePresidentsJson: res.data.vicePresidentsJson, - vicePresidentName: res.data.vicePresidentName, - vicePresidentStudentNo: res.data.vicePresidentStudentNo, - vicePresidentDept: res.data.vicePresidentDept, - vicePresidentMajor: res.data.vicePresidentMajor, - vicePresidentClass: res.data.vicePresidentClass, - vicePresidentPhone: res.data.vicePresidentPhone - }); // 处理指导老师信息:优先从JSON字段读取,如果没有则使用单一字段 - // 🔥 优先从JSON字段读取指导老师数据 + let advisorsData = []; if (res.data.advisorsJson) { try { - (res.data as any).advisors = JSON.parse(res.data.advisorsJson); - console.log('handleUpdate: 从JSON字段读取的指导老师数据:', (res.data as any).advisors); + advisorsData = JSON.parse(res.data.advisorsJson); } catch (error) { console.warn('解析指导老师JSON数据失败:', error); - (res.data as any).advisors = []; + advisorsData = []; } } else if (res.data.advisorName) { - // 回退到单一字段 - (res.data as any).advisors = [ + // 回退到单一字段,构建完整的teacher对象 + advisorsData = [ { name: res.data.advisorName, dept: res.data.advisorDept, phone: res.data.advisorPhone || '', email: res.data.advisorEmail || '', + userId: '', teacher: { + userId: null, + userName: '', nickName: res.data.advisorName, deptName: res.data.advisorDept, phonenumber: res.data.advisorPhone || '', @@ -1002,13 +1031,15 @@ const handleUpdate = async (row?: StglClubVO) => { } } ]; - } else { - (res.data as any).advisors = []; } + // 先设置其他数据 Object.assign(form.value, res.data); - console.log('handleUpdate: 表单数据设置后:', form.value); - console.log('handleUpdate: 表单中的副社长数据:', form.value.vicePresidents); + + // 然后单独设置指导老师数据,避免触发 watch 时的冲突 + nextTick(() => { + form.value.advisors = advisorsData; + }); dialog.visible = true; dialog.title = '修改社团信息'; @@ -1042,15 +1073,9 @@ const submitForm = () => { - // 🔥 新增:同时保存JSON字段和兼容性字段 + // 同时保存JSON字段和兼容性字段 submitData.advisorsJson = JSON.stringify(submitData.advisors || []); - // 调试日志:检查指导老师数据 - console.log('提交数据 - advisors:', submitData.advisors); - console.log('提交数据 - advisorName:', submitData.advisorName); - console.log('提交数据 - advisorDept:', submitData.advisorDept); - console.log('完整提交数据:', submitData); - if (form.value.id) { await updateStglClub(submitData).finally(() => (buttonLoading.value = false)); } else { @@ -1111,16 +1136,10 @@ const handleView = async (row: StglClubVO) => { - // 🔥 处理指导老师信息:优先从JSON字段读取,如果没有则使用单一字段 - console.log('handleView: 指导老师原始数据:', { - advisorsJson: viewData.advisorsJson, - advisorName: viewData.advisorName - }); - + // 处理指导老师信息:优先从JSON字段读取,如果没有则使用单一字段 if (viewData.advisorsJson) { try { (viewData as any).advisors = JSON.parse(viewData.advisorsJson); - console.log('handleView: 从JSON字段读取的指导老师数据:', (viewData as any).advisors); } catch (error) { console.warn('解析指导老师JSON数据失败:', error); (viewData as any).advisors = []; @@ -1145,10 +1164,6 @@ const handleView = async (row: StglClubVO) => { (viewData as any).advisors = []; } - console.log('handleView: 处理后的数据:', { - advisors: (viewData as any).advisors - }); - Object.assign(viewForm, viewData); viewDialog.visible = true; viewDialog.title = '查看社团信息'; @@ -1212,16 +1227,12 @@ const getSourceTagType = (value: string) => { /** 获取指导老师姓名列表 */ const getAdvisorNames = (row: any): string[] => { - console.log('getAdvisorNames: 处理行数据:', row); - // 优先从JSON字段读取指导老师数据 if (row.advisorsJson) { try { const advisors = JSON.parse(row.advisorsJson); - console.log('getAdvisorNames: 从JSON字段解析的指导老师数据:', advisors); if (Array.isArray(advisors)) { const names = advisors.map((advisor: any) => advisor.name || advisor.teacher?.nickName || '').filter((name) => name); - console.log('getAdvisorNames: 从JSON提取的姓名列表:', names); return names; } } catch (error) { @@ -1231,11 +1242,9 @@ const getAdvisorNames = (row: any): string[] => { // 回退到单一字段 if (row.advisorName) { - console.log('getAdvisorNames: 从单一字段获取指导老师:', row.advisorName); return [row.advisorName]; } - console.log('getAdvisorNames: 没有找到指导老师数据'); return []; }; @@ -1265,107 +1274,91 @@ const handlePresidentChange = (student: StudentVO | null) => { -// 防止递归更新的锁 -const syncLock = ref(false); +// 防止递归更新的标志 +const syncingAdvisors = ref(false); -/** 指导老师列表变化处理 */ -const handleAdvisorsChange = (advisors: AdvisorVO[]) => { - // 添加调试日志 - console.log('handleAdvisorsChange 被调用,advisors:', advisors); - - // 添加防护锁,避免递归更新 - if (syncLock.value) return; - - syncLock.value = true; - nextTick(() => { - // 同步指导老师数据到表单 - form.value.advisors = advisors; - - // 为了兼容旧版本,同步第一个指导老师的信息到旧字段 - if (advisors.length > 0) { - const firstAdvisor = advisors[0]; - form.value.advisorName = firstAdvisor.name || firstAdvisor.teacher?.nickName; - form.value.advisorDept = firstAdvisor.dept || firstAdvisor.teacher?.deptName; - form.value.advisorPhone = firstAdvisor.phone || firstAdvisor.teacher?.phonenumber; - form.value.advisorEmail = firstAdvisor.email || firstAdvisor.teacher?.email; - } else { - form.value.advisorName = undefined; - form.value.advisorDept = undefined; - form.value.advisorPhone = undefined; - form.value.advisorEmail = undefined; - } - syncLock.value = false; - }); -}; +/** 监听指导老师数据变化,同步兼容性字段 */ +watch( + () => form.value.advisors, + (advisors: AdvisorVO[]) => { + // 防止递归更新 + if (syncingAdvisors.value) return; + + syncingAdvisors.value = true; + nextTick(() => { + try { + // 为了兼容旧版本,同步第一个指导老师的信息到旧字段 + if (advisors && advisors.length > 0) { + const firstAdvisor = advisors[0]; + form.value.advisorName = firstAdvisor.name || firstAdvisor.teacher?.nickName; + form.value.advisorDept = firstAdvisor.dept || firstAdvisor.teacher?.deptName; + form.value.advisorPhone = firstAdvisor.phone || firstAdvisor.teacher?.phonenumber; + form.value.advisorEmail = firstAdvisor.email || firstAdvisor.teacher?.email; + } else { + form.value.advisorName = undefined; + form.value.advisorDept = undefined; + form.value.advisorPhone = undefined; + form.value.advisorEmail = undefined; + } + } finally { + syncingAdvisors.value = false; + } + }); + }, + { deep: true } +); /** 检查用户是否为指定社团的社长(无论是否为创建者) */ const checkIsPresident = async (clubId: string | number): Promise => { try { - console.log('🔍 checkIsPresident: 开始检查社长权限, clubId:', clubId); const res = await checkPresidentRole(clubId); - console.log('🔍 checkIsPresident: API返回结果:', res); const isPresident = res.data === true; - console.log('🔍 checkIsPresident: 最终结果:', isPresident); return isPresident; } catch (error) { - console.error('❌ checkIsPresident: 检查社长权限失败:', error); + console.error('checkIsPresident: 检查社长权限失败:', error); return false; } }; /** 批量检查社长权限(检查用户是否为各社团的社长,无论是否为创建者) */ const checkBatchPresidentRoles = async (clubs: StglClubVO[]) => { - console.log('🔍 checkBatchPresidentRoles: 开始批量检查权限, clubs数量:', clubs.length); const checks = clubs.map(async (club) => { const isPresident = await checkIsPresident(club.id!); presidentRoles.value[club.id!] = isPresident; - console.log(`🔍 checkBatchPresidentRoles: 社团${club.id}(${club.clubName}) 权限检查结果:`, isPresident); return { clubId: club.id!, isPresident }; }); await Promise.all(checks); - console.log('🔍 checkBatchPresidentRoles: 批量权限检查完成, presidentRoles:', presidentRoles.value); }; /** 判断是否显示转让按钮 */ const shouldShowTransferButton = (club: StglClubVO): boolean => { - console.log('🔍 shouldShowTransferButton: 开始判断转让按钮显示, club:', club); - // 1. 检查基础权限 const hasEditPermission = proxy?.$auth.hasPermi('stgl:club:edit'); - console.log('🔍 shouldShowTransferButton: 基础edit权限检查:', hasEditPermission); if (!hasEditPermission) { - console.log('❌ shouldShowTransferButton: 无基础edit权限,返回false'); return false; } // 2. 检查角色权限 - 如果有stgl_president角色,则需要进一步检查是否为该社团社长 const hasPresidentRole = proxy?.$auth.hasRole('stgl_president'); - console.log('🔍 shouldShowTransferButton: stgl_president角色检查:', hasPresidentRole); if (hasPresidentRole) { // 如果权限检查还未完成,返回false(避免按钮闪现) const isPresident = presidentRoles.value[club.id!]; - console.log('🔍 shouldShowTransferButton: 社团社长权限检查结果:', isPresident, 'presidentRoles:', presidentRoles.value); if (isPresident === undefined) { - console.log('⏳ shouldShowTransferButton: 权限检查未完成,返回false避免闪现'); return false; } // 社长可以看到转让按钮(无论是否为创建者) const result = isPresident === true; - console.log('🔍 shouldShowTransferButton: stgl_president角色最终结果:', result); return result; } // 3. 如果是管理员角色,显示转让按钮(但转让操作仍需社长权限验证) const hasAdminRole = proxy?.$auth.hasRoleOr(['admin', 'common']); - console.log('🔍 shouldShowTransferButton: 管理员角色检查:', hasAdminRole); if (hasAdminRole) { - console.log('✅ shouldShowTransferButton: 管理员角色,返回true'); return true; } - console.log('❌ shouldShowTransferButton: 无任何有效角色,返回false'); return false; }; @@ -1473,7 +1466,6 @@ const handleActivityManagement = (row: StglClubVO) => { /** 成员管理按钮操作 */ const handleMemberManagement = (row: StglClubVO) => { - console.log('handleMemberManagement: 打开成员管理弹窗, row:', row); currentClub.value = row; memberDialog.visible = true; memberDialog.title = `${row.clubName} - 成员管理`; @@ -1482,7 +1474,6 @@ const handleMemberManagement = (row: StglClubVO) => { /** 处理成员数量变化 */ const handleMemberCountChanged = (newCount: number) => { - console.log('handleMemberCountChanged: 成员数量变化:', newCount, '当前社团:', currentClub.value); if (currentClub.value) { // 更新当前社团的成员数量 currentClub.value.memberCount = newCount; @@ -1491,11 +1482,40 @@ const handleMemberCountChanged = (newCount: number) => { const clubIndex = clubList.value.findIndex(club => club.id === currentClub.value?.id); if (clubIndex !== -1) { clubList.value[clubIndex].memberCount = newCount; - console.log('handleMemberCountChanged: 已更新列表中的成员数量:', clubList.value[clubIndex]); } } }; +/** 指导老师历史记录按钮操作 */ +const handleAdvisorHistory = (row: StglClubVO) => { + currentAdvisorClub.value = row; + advisorHistoryDialog.visible = true; + advisorHistoryDialog.title = `${row.clubName} - 指导老师历史记录`; +}; + +/** 工具栏指导老师按钮操作 */ +const handleAdvisorHistoryFromToolbar = () => { + const selectedClub = clubList.value.find((c) => ids.value.includes(c.id!)); + if (selectedClub) { + handleAdvisorHistory(selectedClub); + } +}; + +/** 年审管理按钮操作 */ +const handleAnnualReview = () => { + const selectedClub = clubList.value.find((c) => ids.value.includes(c.id!)); + if (selectedClub) { + router.push({ + path: '/stgl/club/info/annualReview/index', + query: { + clubId: selectedClub.id, + clubName: selectedClub.clubName, + from: 'club' + } + }); + } +}; + onMounted(() => { getList(); }); diff --git a/web/src/views/stgl/my/activity/index.vue b/web/src/views/stgl/my/activity/index.vue index 1c6269aa..29f7d92e 100644 --- a/web/src/views/stgl/my/activity/index.vue +++ b/web/src/views/stgl/my/activity/index.vue @@ -77,8 +77,26 @@ - - + + + + + + - + - + - + + + + + + +
@@ -452,4 +470,33 @@ onMounted(() => { .activity-item:last-child { margin-bottom: 0; } + +/* 今日活动和活动时间样式 */ +.activity-name { + color: #409eff; + font-weight: 500; + line-height: 1.4; + word-break: break-word; +} + +.activity-time { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + line-height: 1.3; +} + +.time-start, +.time-end { + color: #606266; + font-size: 13px; + font-weight: 500; +} + +.time-separator { + color: #909399; + font-size: 12px; + margin: 0 4px; +} \ No newline at end of file