diff --git a/.gitignore b/.gitignore index fd624ea9..03ef4a45 100644 --- a/.gitignore +++ b/.gitignore @@ -326,4 +326,5 @@ docs/变更管理/ 查找界面中涉及到的权限字符.md docs/部署文档/nginx配置/hzdyjsweb.camasort.com.conf 首页完善的提示词.md -scripts/ \ No newline at end of file +scripts/ +学生宿舍卫生检查、学生违纪记录统计.md diff --git a/backend/pom.xml b/backend/pom.xml index 3f903412..261234a4 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -85,6 +85,16 @@ true + + prod-local + + + prod-local + warn + ruoyi + 123456 + + prod diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectApprovalController.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectApprovalController.java new file mode 100644 index 00000000..b4e7c626 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectApprovalController.java @@ -0,0 +1,156 @@ +package org.dromara.cw.controller; + +import java.util.List; + +import lombok.RequiredArgsConstructor; +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.cw.domain.bo.CwCustomApprovalBo; +import org.dromara.cw.domain.bo.CwApprovalActionBo; +import org.dromara.cw.domain.vo.CwProjectApprovalProcessVo; +import org.dromara.cw.domain.vo.CwApprovalRecordVo; +import org.dromara.cw.service.ICwProjectApprovalService; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.common.log.enums.BusinessType; + +/** + * 项目审批流程管理 + * + * @author ruoyi + * @date 2025-01-03 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/cw/projectApproval") +public class CwProjectApprovalController extends BaseController { + + private final ICwProjectApprovalService projectApprovalService; + + /** + * 启动自定义审批流程 + */ + @SaCheckPermission("cw:projectApproval:start") + @Log(title = "启动审批流程", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping("/start") + public R startApprovalProcess(@Validated @RequestBody CwCustomApprovalBo bo) { + // 设置提交人信息 + bo.setSubmitterId(LoginHelper.getUserId()); + bo.setSubmitterName(LoginHelper.getUsername()); + return toAjax(projectApprovalService.startCustomApprovalProcess(bo)); + } + + /** + * 执行审批操作 + */ + @SaCheckPermission("cw:projectApproval:approve") + @Log(title = "执行审批操作", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping("/approve") + public R executeApproval(@Validated @RequestBody CwApprovalActionBo bo) { + return toAjax(projectApprovalService.executeApproval(bo)); + } + + /** + * 获取我的待审批列表 + */ + @SaCheckPermission("cw:projectApproval:query") + @GetMapping("/pending") + public TableDataInfo getMyPendingApprovals(PageQuery pageQuery) { + return projectApprovalService.getMyPendingApprovals(pageQuery); + } + + /** + * 获取我的已审批列表 + */ + @SaCheckPermission("cw:projectApproval:query") + @GetMapping("/processed") + public TableDataInfo getMyProcessedApprovals(PageQuery pageQuery) { + return projectApprovalService.getMyProcessedApprovals(pageQuery); + } + + /** + * 查询审批流程列表 + */ + @SaCheckPermission("cw:projectApproval:query") + @GetMapping("/list") + public TableDataInfo list(PageQuery pageQuery) { + return projectApprovalService.queryProcessList(pageQuery); + } + + /** + * 获取审批流程详情 + */ + @SaCheckPermission("cw:projectApproval:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") + @PathVariable Long id) { + return R.ok(projectApprovalService.getProcessDetail(id)); + } + + /** + * 获取审批记录列表 + */ + @SaCheckPermission("cw:projectApproval:query") + @GetMapping("/records/{applicationId}") + public R> getApprovalRecords(@PathVariable Long applicationId) { + return R.ok(projectApprovalService.getApprovalRecords(applicationId)); + } + + /** + * 终止审批流程 + */ + @SaCheckPermission("cw:projectApproval:terminate") + @Log(title = "终止审批流程", businessType = BusinessType.UPDATE) + @DeleteMapping("/{ids}") + public R terminate(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] ids) { + return toAjax(projectApprovalService.terminateProcess(List.of(ids))); + } + + /** + * 审批流程验证 + */ + @SaCheckPermission("cw:projectApproval:query") + @PostMapping("/validate") + public R validateApprovalProcess(@RequestBody CwCustomApprovalBo bo) { + // 简单验证逻辑 + if (bo.getAuditorIds() == null || bo.getAuditorIds().isEmpty()) { + return R.fail("请选择审核人"); + } + if (bo.getAuditorIds().size() > 10) { + return R.fail("审核人数量不能超过10个"); + } + return R.ok("验证通过"); + } + + /** + * 审批选项查询(用于下拉选择) + */ + @SaCheckPermission("cw:projectApproval:query") + @GetMapping("/options") + public R getApprovalOptions() { + // 返回一些选项数据,比如审批动作选项等 + return R.ok(java.util.Map.of( + "actions", java.util.List.of( + java.util.Map.of("value", "approve", "label", "通过"), + java.util.Map.of("value", "reject", "label", "拒绝") + ), + "statuses", java.util.List.of( + java.util.Map.of("value", "waiting", "label", "等待审批"), + java.util.Map.of("value", "approved", "label", "已通过"), + java.util.Map.of("value", "rejected", "label", "已拒绝") + ) + )); + } + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectAuditorController.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectAuditorController.java new file mode 100644 index 00000000..5d5cb767 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/controller/CwProjectAuditorController.java @@ -0,0 +1,251 @@ +package org.dromara.cw.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.excel.utils.ExcelUtil; +import org.dromara.cw.domain.vo.CwProjectAuditorVo; +import org.dromara.cw.domain.bo.CwProjectAuditorBo; +import org.dromara.cw.service.ICwProjectAuditorService; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.common.log.enums.BusinessType; + +/** + * 项目审核人配置 + * + * @author ruoyi + * @date 2025-01-03 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/cw/projectAuditor") +public class CwProjectAuditorController extends BaseController { + + private final ICwProjectAuditorService cwProjectAuditorService; + + /** + * 查询项目审核人配置列表 + */ + @SaCheckPermission("cw:projectAuditor:list") + @GetMapping("/list") + public TableDataInfo list(CwProjectAuditorBo bo, PageQuery pageQuery) { + return cwProjectAuditorService.queryPageList(bo, pageQuery); + } + + /** + * 导出项目审核人配置列表 + */ + @SaCheckPermission("cw:projectAuditor:export") + @Log(title = "项目审核人配置", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(CwProjectAuditorBo bo, HttpServletResponse response) { + List list = cwProjectAuditorService.queryList(bo); + ExcelUtil.exportExcel(list, "项目审核人配置", CwProjectAuditorVo.class, response); + } + + /** + * 获取项目审核人配置详细信息 + */ + @SaCheckPermission("cw:projectAuditor:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") + @PathVariable Long id) { + return R.ok(cwProjectAuditorService.queryById(id)); + } + + /** + * 新增项目审核人配置 + */ + @SaCheckPermission("cw:projectAuditor:add") + @Log(title = "项目审核人配置", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody CwProjectAuditorBo bo) { + // 设置配置人信息 + bo.setConfigBy(LoginHelper.getUserId()); + bo.setConfigByName(LoginHelper.getUsername()); + return toAjax(cwProjectAuditorService.insertByBo(bo)); + } + + /** + * 修改项目审核人配置 + */ + @SaCheckPermission("cw:projectAuditor:edit") + @Log(title = "项目审核人配置", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody CwProjectAuditorBo bo) { + return toAjax(cwProjectAuditorService.updateByBo(bo)); + } + + /** + * 删除项目审核人配置 + */ + @SaCheckPermission("cw:projectAuditor:remove") + @Log(title = "项目审核人配置", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public R remove(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] ids) { + return toAjax(cwProjectAuditorService.deleteWithValidByIds(List.of(ids), true)); + } + + /** + * 批量添加审核人 + */ + @SaCheckPermission("cw:projectAuditor:add") + @Log(title = "批量添加审核人", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping("/batchAdd") + public R batchAdd(@RequestBody BatchAddRequest request) { + return toAjax(cwProjectAuditorService.batchAddAuditors( + request.getUserIds(), + LoginHelper.getUserId(), + LoginHelper.getUsername(), + request.getRemark() + )); + } + + /** + * 获取可用审核人列表 + */ + @SaCheckPermission("cw:projectAuditor:list") + @GetMapping("/available") + public R> getAvailableAuditors() { + return R.ok(cwProjectAuditorService.getAvailableAuditors()); + } + + /** + * 根据部门ID获取用户列表 + */ + @SaCheckPermission("cw:projectAuditor:list") + @GetMapping("/deptUsers/{deptId}") + public R> getDeptUsers(@PathVariable Long deptId) { + // 参数验证 + if (deptId == null || deptId <= 0) { + return R.fail("部门ID参数无效"); + } + return R.ok(cwProjectAuditorService.getDeptUsers(deptId)); + } + + /** + * 批量更新状态 + */ + @SaCheckPermission("cw:projectAuditor:edit") + @Log(title = "批量更新审核人状态", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping("/status") + public R updateStatus(@RequestBody StatusUpdateRequest request) { + return toAjax(cwProjectAuditorService.updateStatus(request.getIds(), request.getStatus())); + } + + /** + * 我配置的审核人 + */ + @SaCheckPermission("cw:projectAuditor:list") + @GetMapping("/myConfig") + public TableDataInfo getMyConfig(CwProjectAuditorBo bo, PageQuery pageQuery) { + bo.setConfigBy(LoginHelper.getUserId()); + return cwProjectAuditorService.queryPageList(bo, pageQuery); + } + + /** + * 获取审核人选项(用于下拉选择) + */ + @SaCheckPermission("cw:projectAuditor:list") + @GetMapping("/options") + public R> getAuditorOptions() { + return R.ok(cwProjectAuditorService.getAvailableAuditors()); + } + + /** + * 批量启用 + */ + @SaCheckPermission("cw:projectAuditor:edit") + @Log(title = "批量启用审核人", businessType = BusinessType.UPDATE) + @PutMapping("/enable/{ids}") + public R enable(@PathVariable Long[] ids) { + return toAjax(cwProjectAuditorService.updateStatus(List.of(ids), "1")); + } + + /** + * 批量停用 + */ + @SaCheckPermission("cw:projectAuditor:edit") + @Log(title = "批量停用审核人", businessType = BusinessType.UPDATE) + @PutMapping("/disable/{ids}") + public R disable(@PathVariable Long[] ids) { + return toAjax(cwProjectAuditorService.updateStatus(List.of(ids), "0")); + } + + /** + * 获取审核人统计数据 + */ + @SaCheckPermission("cw:projectAuditor:list") + @GetMapping("/statistics") + public R getStatistics() { + return R.ok(cwProjectAuditorService.getStatistics()); + } + + /** + * 批量添加请求对象 + */ + public static class BatchAddRequest { + private List userIds; + private String remark; + + public List getUserIds() { + return userIds; + } + + public void setUserIds(List userIds) { + this.userIds = userIds; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + } + + /** + * 状态更新请求对象 + */ + public static class StatusUpdateRequest { + private List ids; + private String status; + + public List getIds() { + return ids; + } + + public void setIds(List ids) { + this.ids = ids; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + } + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwApprovalRecord.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwApprovalRecord.java index 064ebc40..67f4e2dc 100644 --- a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwApprovalRecord.java +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwApprovalRecord.java @@ -124,4 +124,19 @@ public class CwApprovalRecord extends TenantEntity implements Serializable { */ private String configSnapshot; -} \ No newline at end of file + /** + * 流程ID + */ + private Long processId; + + /** + * 自定义节点名称 + */ + private String customNodeName; + + /** + * 是否自定义流程 + */ + private Boolean isCustomProcess; + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectApprovalProcess.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectApprovalProcess.java new file mode 100644 index 00000000..3ba5c393 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectApprovalProcess.java @@ -0,0 +1,82 @@ +package org.dromara.cw.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.tenant.core.TenantEntity; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 项目审批流程对象 cw_project_approval_process + * + * @author ruoyi + * @date 2025-01-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("cw_project_approval_process") +public class CwProjectApprovalProcess extends TenantEntity implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID(雪花ID) + */ + @TableId(value = "id") + private Long id; + + /** + * 申报ID + */ + private Long applicationId; + + /** + * 申报编号 + */ + private String applicationNo; + + /** + * 流程名称 + */ + private String processName; + + /** + * 流程状态(waiting-等待开始 running-进行中 completed-已完成 terminated-已终止) + */ + private String processStatus; + + /** + * 总审批节点数 + */ + private Integer totalNodes; + + /** + * 当前审批节点 + */ + private Integer currentNode; + + /** + * 流程开始时间 + */ + private Date startTime; + + /** + * 流程结束时间 + */ + private Date endTime; + + /** + * 提交人ID + */ + private Long submitterId; + + /** + * 提交人姓名 + */ + private String submitterName; + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectAuditor.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectAuditor.java new file mode 100644 index 00000000..b8fb06ee --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/CwProjectAuditor.java @@ -0,0 +1,92 @@ +package org.dromara.cw.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.tenant.core.TenantEntity; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 项目审核人配置对象 cw_project_auditor + * + * @author ruoyi + * @date 2025-01-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("cw_project_auditor") +public class CwProjectAuditor extends TenantEntity implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID(雪花ID) + */ + @TableId(value = "id") + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户姓名 + */ + private String userName; + + /** + * 用户昵称 + */ + private String nickName; + + /** + * 所属部门ID + */ + private Long deptId; + + /** + * 所属部门名称 + */ + private String deptName; + + /** + * 联系电话 + */ + private String phoneNumber; + + /** + * 邮箱地址 + */ + private String email; + + /** + * 审核人状态(1正常 0停用) + */ + private String status; + + /** + * 备注说明 + */ + private String remark; + + /** + * 配置人ID + */ + private Long configBy; + + /** + * 配置人姓名 + */ + private String configByName; + + /** + * 删除标志(0代表存在 1代表删除) + */ + @TableLogic + private Integer deleted; + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/UserWithDeptInfo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/UserWithDeptInfo.java new file mode 100644 index 00000000..217676a6 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/UserWithDeptInfo.java @@ -0,0 +1,39 @@ +package org.dromara.cw.domain; + +import lombok.Data; + +/** + * 用户和部门完整信息 + * 用于批量添加审核人时获取完整的用户数据 + * + * @author ruoyi + * @date 2025-01-03 + */ +@Data +public class UserWithDeptInfo { + + /** 用户ID */ + private Long userId; + + /** 用户账号 */ + private String userName; + + /** 用户昵称 */ + private String nickName; + + /** 部门ID */ + private Long deptId; + + /** 部门名称 */ + private String deptName; + + /** 手机号码 */ + private String phonenumber; + + /** 用户邮箱 */ + private String email; + + /** 帐号状态(1正常 0停用) */ + private String status; + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwApprovalActionBo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwApprovalActionBo.java new file mode 100644 index 00000000..aeb6c89d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwApprovalActionBo.java @@ -0,0 +1,33 @@ +package org.dromara.cw.domain.bo; + +import lombok.Data; +import jakarta.validation.constraints.*; + +/** + * 审批操作业务对象 + * + * @author ruoyi + * @date 2025-01-03 + */ +@Data +public class CwApprovalActionBo { + + /** + * 审批记录ID + */ + @NotNull(message = "审批记录ID不能为空") + private Long recordId; + + /** + * 审批操作(approve-通过 reject-拒绝) + */ + @NotBlank(message = "审批操作不能为空") + private String action; + + /** + * 审批意见 + */ + @NotBlank(message = "审批意见不能为空") + private String opinion; + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwApprovalRecordBo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwApprovalRecordBo.java index 1428356d..b3105f5c 100644 --- a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwApprovalRecordBo.java +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwApprovalRecordBo.java @@ -132,13 +132,18 @@ public class CwApprovalRecordBo extends BaseEntity { private String configSnapshot; // ========== 查询条件字段 ========== - + /** * 部门ID查询条件(用于部门审批页面过滤) * 支持具体部门ID或特殊值 'current'(当前用户部门) */ private String deptId; + /** + * 部门ID(Long类型,用于数据库操作) + */ + private Long deptIdLong; + /** * 项目名称(查询条件) */ @@ -164,4 +169,4 @@ public class CwApprovalRecordBo extends BaseEntity { */ private String approvalStatus; -} \ No newline at end of file +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwCustomApprovalBo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwCustomApprovalBo.java new file mode 100644 index 00000000..dc245b05 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwCustomApprovalBo.java @@ -0,0 +1,52 @@ +package org.dromara.cw.domain.bo; + +import lombok.Data; +import jakarta.validation.constraints.*; +import java.util.List; + +/** + * 自定义审批流程业务对象 + * + * @author ruoyi + * @date 2025-01-03 + */ +@Data +public class CwCustomApprovalBo { + + /** + * 申报ID + */ + @NotNull(message = "申报ID不能为空") + private Long applicationId; + + /** + * 申报编号 + */ + @NotBlank(message = "申报编号不能为空") + private String applicationNo; + + /** + * 审核人ID列表 + */ + @NotEmpty(message = "审核人列表不能为空") + private List auditorIds; + + /** + * 流程名称 + */ + @NotBlank(message = "流程名称不能为空") + private String processName; + + /** + * 提交人ID + */ + @NotNull(message = "提交人ID不能为空") + private Long submitterId; + + /** + * 提交人姓名 + */ + @NotBlank(message = "提交人姓名不能为空") + private String submitterName; + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectApprovalProcessBo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectApprovalProcessBo.java new file mode 100644 index 00000000..44b85fed --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectApprovalProcessBo.java @@ -0,0 +1,88 @@ +package org.dromara.cw.domain.bo; + +import org.dromara.cw.domain.CwProjectApprovalProcess; +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.util.Date; + +/** + * 项目审批流程业务对象 cw_project_approval_process + * + * @author ruoyi + * @date 2025-01-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = CwProjectApprovalProcess.class, reverseConvertGenerate = false) +public class CwProjectApprovalProcessBo extends BaseEntity { + + /** + * 主键ID(雪花ID) + */ + @NotNull(message = "主键ID不能为空", groups = { EditGroup.class }) + private Long id; + + /** + * 申报ID + */ + @NotNull(message = "申报ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long applicationId; + + /** + * 申报编号 + */ + @NotBlank(message = "申报编号不能为空", groups = { AddGroup.class, EditGroup.class }) + private String applicationNo; + + /** + * 流程名称 + */ + @NotBlank(message = "流程名称不能为空", groups = { AddGroup.class, EditGroup.class }) + private String processName; + + /** + * 流程状态(waiting-等待开始 running-进行中 completed-已完成 terminated-已终止) + */ + @NotBlank(message = "流程状态不能为空", groups = { AddGroup.class, EditGroup.class }) + private String processStatus; + + /** + * 总审批节点数 + */ + @NotNull(message = "总审批节点数不能为空", groups = { AddGroup.class, EditGroup.class }) + private Integer totalNodes; + + /** + * 当前审批节点 + */ + private Integer currentNode; + + /** + * 流程开始时间 + */ + private Date startTime; + + /** + * 流程结束时间 + */ + private Date endTime; + + /** + * 提交人ID + */ + @NotNull(message = "提交人ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long submitterId; + + /** + * 提交人姓名 + */ + @NotBlank(message = "提交人姓名不能为空", groups = { AddGroup.class, EditGroup.class }) + private String submitterName; + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectAuditorBatchBo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectAuditorBatchBo.java new file mode 100644 index 00000000..d4bfebd7 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectAuditorBatchBo.java @@ -0,0 +1,39 @@ +package org.dromara.cw.domain.bo; + +import lombok.Data; +import jakarta.validation.constraints.*; +import java.util.List; + +/** + * 批量添加审核人业务对象 + * + * @author ruoyi + * @date 2025-01-03 + */ +@Data +public class CwProjectAuditorBatchBo { + + /** + * 用户ID列表 + */ + @NotEmpty(message = "用户ID列表不能为空") + private List userIds; + + /** + * 配置人ID + */ + @NotNull(message = "配置人ID不能为空") + private Long configBy; + + /** + * 配置人姓名 + */ + @NotBlank(message = "配置人姓名不能为空") + private String configByName; + + /** + * 备注说明 + */ + private String remark; + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectAuditorBo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectAuditorBo.java new file mode 100644 index 00000000..6001304e --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/bo/CwProjectAuditorBo.java @@ -0,0 +1,91 @@ +package org.dromara.cw.domain.bo; + +import org.dromara.cw.domain.CwProjectAuditor; +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.util.List; + +/** + * 项目审核人配置业务对象 cw_project_auditor + * + * @author ruoyi + * @date 2025-01-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = CwProjectAuditor.class, reverseConvertGenerate = false) +public class CwProjectAuditorBo extends BaseEntity { + + /** + * 主键ID(雪花ID) + */ + @NotNull(message = "主键ID不能为空", groups = { EditGroup.class }) + private Long id; + + /** + * 用户ID + */ + @NotNull(message = "用户ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long userId; + + /** + * 用户姓名 + */ + @NotBlank(message = "用户姓名不能为空", groups = { AddGroup.class, EditGroup.class }) + private String userName; + + /** + * 用户昵称 + */ + private String nickName; + + /** + * 所属部门ID + */ + @NotNull(message = "所属部门ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long deptId; + + /** + * 所属部门名称 + */ + @NotBlank(message = "所属部门名称不能为空", groups = { AddGroup.class, EditGroup.class }) + private String deptName; + + /** + * 联系电话 + */ + private String phoneNumber; + + /** + * 邮箱地址 + */ + private String email; + + /** + * 审核人状态(1正常 0停用) + */ + @NotBlank(message = "审核人状态不能为空", groups = { AddGroup.class, EditGroup.class }) + private String status; + + /** + * 备注说明 + */ + private String remark; + + /** + * 配置人ID + */ + private Long configBy; + + /** + * 配置人姓名 + */ + private String configByName; + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwApprovalRecordVo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwApprovalRecordVo.java index 3feba644..0ea24363 100644 --- a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwApprovalRecordVo.java +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwApprovalRecordVo.java @@ -172,37 +172,37 @@ public class CwApprovalRecordVo implements Serializable { private Date updateTime; // ================ 项目申报相关字段(通过关联查询获取) ================ - + /** * 项目名称 */ @ExcelProperty(value = "项目名称") private String projectName; - + /** * 申报人姓名 */ @ExcelProperty(value = "申报人姓名") private String applicantName; - + /** * 申报部门名称 */ @ExcelProperty(value = "申报部门") private String deptName; - + /** * 总投资(万元) */ @ExcelProperty(value = "总投资(万元)") private java.math.BigDecimal totalInvestment; - + /** * 项目负责人 */ - @ExcelProperty(value = "项目负责人") + @ExcelProperty(value = "项目负责人") private String principal; - + /** * 部门名称(完整) */ @@ -213,23 +213,47 @@ public class CwApprovalRecordVo implements Serializable { */ @ExcelProperty(value = "当前审批人") private String currentApproverName; - + /** * 审批状态(前端表格需要) */ @ExcelProperty(value = "审批状态") private String approvalStatus; - + /** * 提交时间(前端表格需要) */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date submitTime; - + /** * 审批时间(前端表格需要) */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date approvalTime; -} \ No newline at end of file + /** + * 总节点数(从流程表获取) + */ + @ExcelProperty(value = "总节点数") + private Integer totalNodes; + + /** + * 当前节点数(从流程表获取) + */ + @ExcelProperty(value = "当前节点数") + private Integer currentNode; + + /** + * 流程名称(从流程表获取) + */ + @ExcelProperty(value = "流程名称") + private String processName; + + /** + * 流程状态(从流程表获取) + */ + @ExcelProperty(value = "流程状态") + private String processStatus; + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectApprovalProcessVo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectApprovalProcessVo.java new file mode 100644 index 00000000..e5e39e8b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectApprovalProcessVo.java @@ -0,0 +1,121 @@ +package org.dromara.cw.domain.vo; + +import org.dromara.cw.domain.CwProjectApprovalProcess; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import org.dromara.common.excel.annotation.ExcelDictFormat; +import org.dromara.common.excel.convert.ExcelDictConvert; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 项目审批流程视图对象 cw_project_approval_process + * + * @author ruoyi + * @date 2025-01-03 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = CwProjectApprovalProcess.class) +public class CwProjectApprovalProcessVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID(雪花ID) + */ + @ExcelProperty(value = "主键ID") + private Long id; + + /** + * 申报ID + */ + @ExcelProperty(value = "申报ID") + private Long applicationId; + + /** + * 申报编号 + */ + @ExcelProperty(value = "申报编号") + private String applicationNo; + + /** + * 流程名称 + */ + @ExcelProperty(value = "流程名称") + private String processName; + + /** + * 流程状态(waiting-等待开始 running-进行中 completed-已完成 terminated-已终止) + */ + @ExcelProperty(value = "流程状态", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "cw_process_status") + private String processStatus; + + /** + * 总审批节点数 + */ + @ExcelProperty(value = "总审批节点数") + private Integer totalNodes; + + /** + * 当前审批节点 + */ + @ExcelProperty(value = "当前审批节点") + private Integer currentNode; + + /** + * 流程开始时间 + */ + @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; + + /** + * 提交人ID + */ + @ExcelProperty(value = "提交人ID") + private Long submitterId; + + /** + * 提交人姓名 + */ + @ExcelProperty(value = "提交人姓名") + private String submitterName; + + /** + * 创建者 + */ + 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; + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectAuditorVo.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectAuditorVo.java new file mode 100644 index 00000000..cdd6e72d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/domain/vo/CwProjectAuditorVo.java @@ -0,0 +1,125 @@ +package org.dromara.cw.domain.vo; + +import org.dromara.cw.domain.CwProjectAuditor; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import org.dromara.common.excel.annotation.ExcelDictFormat; +import org.dromara.common.excel.convert.ExcelDictConvert; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 项目审核人配置视图对象 cw_project_auditor + * + * @author ruoyi + * @date 2025-01-03 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = CwProjectAuditor.class) +public class CwProjectAuditorVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID(雪花ID) + */ + @ExcelProperty(value = "主键ID") + private Long id; + + /** + * 用户ID + */ + @ExcelProperty(value = "用户ID") + private Long userId; + + /** + * 用户姓名 + */ + @ExcelProperty(value = "用户姓名") + private String userName; + + /** + * 用户昵称 + */ + @ExcelProperty(value = "用户昵称") + private String nickName; + + /** + * 所属部门ID + */ + @ExcelProperty(value = "所属部门ID") + private Long deptId; + + /** + * 所属部门名称 + */ + @ExcelProperty(value = "所属部门名称") + private String deptName; + + /** + * 联系电话 + */ + @ExcelProperty(value = "联系电话") + private String phoneNumber; + + /** + * 邮箱地址 + */ + @ExcelProperty(value = "邮箱地址") + private String email; + + /** + * 审核人状态(1正常 0停用) + */ + @ExcelProperty(value = "审核人状态", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "sys_normal_disable") + private String status; + + /** + * 备注说明 + */ + @ExcelProperty(value = "备注说明") + private String remark; + + /** + * 配置人ID + */ + @ExcelProperty(value = "配置人ID") + private Long configBy; + + /** + * 配置人姓名 + */ + @ExcelProperty(value = "配置人姓名") + private String configByName; + + /** + * 创建者 + */ + 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; + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectApprovalProcessMapper.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectApprovalProcessMapper.java new file mode 100644 index 00000000..a9262c90 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectApprovalProcessMapper.java @@ -0,0 +1,15 @@ +package org.dromara.cw.mapper; + +import org.dromara.cw.domain.CwProjectApprovalProcess; +import org.dromara.cw.domain.vo.CwProjectApprovalProcessVo; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; + +/** + * 项目审批流程Mapper接口 + * + * @author ruoyi + * @date 2025-01-03 + */ +public interface CwProjectApprovalProcessMapper extends BaseMapperPlus { + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectAuditorMapper.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectAuditorMapper.java new file mode 100644 index 00000000..277706ec --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/mapper/CwProjectAuditorMapper.java @@ -0,0 +1,34 @@ +package org.dromara.cw.mapper; + +import org.dromara.cw.domain.CwProjectAuditor; +import org.dromara.cw.domain.UserWithDeptInfo; +import org.dromara.cw.domain.vo.CwProjectAuditorVo; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 项目审核人配置Mapper接口 + * + * @author ruoyi + * @date 2025-01-03 + */ +public interface CwProjectAuditorMapper extends BaseMapperPlus { + + /** + * 根据部门ID查询可用审核人 + */ + List selectAuditorsByDeptId(@Param("deptId") Long deptId); + + /** + * 查询审核人统计信息 + */ + List selectAuditorStatistics(); + + /** + * 查询完整的用户信息(包含部门信息)用于批量添加 + */ + List selectUsersWithDeptInfo(@Param("userIds") List userIds); + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwApprovalRecordService.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwApprovalRecordService.java index 5064c532..1ca9db32 100644 --- a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwApprovalRecordService.java +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwApprovalRecordService.java @@ -2,6 +2,7 @@ package org.dromara.cw.service; import org.dromara.cw.domain.bo.CwApprovalRecordBo; import org.dromara.cw.domain.vo.CwApprovalRecordVo; +import org.dromara.cw.domain.CwApprovalRecord; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.PageQuery; @@ -119,6 +120,11 @@ public interface ICwApprovalRecordService { */ Boolean insertByBo(CwApprovalRecordBo bo); + /** + * 批量新增审批记录 + */ + Boolean batchInsert(List records); + /** * 修改审批记录 */ @@ -190,4 +196,18 @@ public interface ICwApprovalRecordService { */ List> getAvailableApprovers(Long applicationId); + /** + * 创建自定义审批流程 + * 根据用户选择的审批人列表创建自定义审批流程 + * + * @param applicationId 申报ID + * @param applicationNo 申报编号 + * @param auditorIds 审批人ID列表(按审批顺序排列) + * @param submitterId 提交人ID + * @param submitterName 提交人名称 + * @return 是否创建成功 + */ + Boolean createCustomApprovalProcess(Long applicationId, String applicationNo, + List auditorIds, Long submitterId, String submitterName); + } diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectApprovalProcessService.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectApprovalProcessService.java new file mode 100644 index 00000000..cf76f459 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectApprovalProcessService.java @@ -0,0 +1,110 @@ +package org.dromara.cw.service; + +import org.dromara.cw.domain.vo.CwProjectApprovalProcessVo; +import org.dromara.cw.domain.bo.CwCustomApprovalBo; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 项目审批流程Service接口 + * + * @author ruoyi + * @date 2025-01-03 + */ +public interface ICwProjectApprovalProcessService { + + /** + * 查询项目审批流程 + */ + CwProjectApprovalProcessVo queryById(Long id); + + /** + * 查询项目审批流程列表 + */ + TableDataInfo queryPageList(CwProjectApprovalProcessVo vo, PageQuery pageQuery); + + /** + * 查询项目审批流程列表 + */ + List queryList(CwProjectApprovalProcessVo vo); + + /** + * 根据申报ID查询审批流程 + */ + CwProjectApprovalProcessVo queryByApplicationId(Long applicationId); + + /** + * 根据申报编号查询审批流程 + */ + CwProjectApprovalProcessVo queryByApplicationNo(String applicationNo); + + /** + * 查询我提交的审批流程 + */ + List queryMyProcesses(Long submitterId); + + /** + * 查询进行中的审批流程 + */ + List queryRunningProcesses(); + + /** + * 查询超时的审批流程 + */ + List queryOvertimeProcesses(Integer hours); + + /** + * 启动自定义审批流程 + */ + Boolean startCustomApprovalProcess(CwCustomApprovalBo bo); + + /** + * 终止审批流程 + */ + Boolean terminateProcess(Long id, String reason); + + /** + * 校验并批量删除项目审批流程信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 推进流程到下一节点 + */ + Boolean advanceToNextNode(Long processId); + + /** + * 完成审批流程 + */ + Boolean completeProcess(Long processId); + + /** + * 更新流程当前节点 + */ + Boolean updateCurrentNode(Long id, Integer currentNode); + + /** + * 更新流程状态 + */ + Boolean updateProcessStatus(Long id, String status); + + /** + * 验证审批流程配置 + */ + Boolean validateApprovalProcess(CwCustomApprovalBo bo); + + /** + * 统计审批流程状态 + */ + List> queryProcessStatistics(Long submitterId); + + /** + * 导出数据 + */ + List exportList(CwProjectApprovalProcessVo vo); + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectApprovalService.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectApprovalService.java new file mode 100644 index 00000000..14fc48dd --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectApprovalService.java @@ -0,0 +1,60 @@ +package org.dromara.cw.service; + +import org.dromara.cw.domain.bo.CwCustomApprovalBo; +import org.dromara.cw.domain.bo.CwApprovalActionBo; +import org.dromara.cw.domain.vo.CwProjectApprovalProcessVo; +import org.dromara.cw.domain.vo.CwApprovalRecordVo; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.mybatis.core.page.PageQuery; + +import java.util.List; + +/** + * 项目审批流程Service接口 + * + * @author ruoyi + * @date 2025-01-03 + */ +public interface ICwProjectApprovalService { + + /** + * 启动自定义审批流程 + */ + Boolean startCustomApprovalProcess(CwCustomApprovalBo bo); + + /** + * 执行审批操作 + */ + Boolean executeApproval(CwApprovalActionBo bo); + + /** + * 获取我的待审批列表 + */ + TableDataInfo getMyPendingApprovals(PageQuery pageQuery); + + /** + * 获取我的已审批列表 + */ + TableDataInfo getMyProcessedApprovals(PageQuery pageQuery); + + /** + * 查询审批流程列表 + */ + TableDataInfo queryProcessList(PageQuery pageQuery); + + /** + * 获取审批流程详情 + */ + CwProjectApprovalProcessVo getProcessDetail(Long processId); + + /** + * 获取审批记录列表 + */ + List getApprovalRecords(Long applicationId); + + /** + * 终止审批流程 + */ + Boolean terminateProcess(List processIds); + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectAuditorService.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectAuditorService.java new file mode 100644 index 00000000..b53ad4f2 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/ICwProjectAuditorService.java @@ -0,0 +1,79 @@ +package org.dromara.cw.service; + +import org.dromara.cw.domain.vo.CwProjectAuditorVo; +import org.dromara.cw.domain.bo.CwProjectAuditorBo; +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 ruoyi + * @date 2025-01-03 + */ +public interface ICwProjectAuditorService { + + /** + * 查询项目审核人配置 + */ + CwProjectAuditorVo queryById(Long id); + + /** + * 查询项目审核人配置列表 + */ + TableDataInfo queryPageList(CwProjectAuditorBo bo, PageQuery pageQuery); + + /** + * 查询项目审核人配置列表 + */ + List queryList(CwProjectAuditorBo bo); + + /** + * 新增项目审核人配置 + */ + Boolean insertByBo(CwProjectAuditorBo bo); + + /** + * 修改项目审核人配置 + */ + Boolean updateByBo(CwProjectAuditorBo bo); + + /** + * 校验并批量删除项目审核人配置信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 批量添加审核人(从部门用户中选择) + */ + Boolean batchAddAuditors(List userIds, Long configBy, String configByName, String remark); + + /** + * 获取可用审核人列表 + */ + List getAvailableAuditors(); + + /** + * 根据部门ID获取用户列表 + */ + List getDeptUsers(Long deptId); + + /** + * 批量更新状态 + */ + Boolean updateStatus(List ids, String status); + + /** + * 获取审核人统计数据 + */ + Object getStatistics(); + + /** + * 根据用户ID查询项目审核人配置 + */ + CwProjectAuditorVo queryByUserId(Long userId); + +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwApprovalRecordServiceImpl.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwApprovalRecordServiceImpl.java index 84f2b634..c12b5ff1 100644 --- a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwApprovalRecordServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwApprovalRecordServiceImpl.java @@ -5,13 +5,12 @@ 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.mybatis.helper.DataPermissionHelper; -import org.dromara.common.redis.utils.SequenceUtils; import org.dromara.common.satoken.utils.LoginHelper; -import java.time.Duration; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,8 +24,10 @@ import org.dromara.cw.domain.CwApprovalRecord; import org.dromara.cw.mapper.CwApprovalRecordMapper; import org.dromara.cw.service.ICwApprovalRecordService; import org.dromara.cw.service.ICwProjectApplicationService; +import org.dromara.cw.service.ICwProjectAuditorService; import org.dromara.cw.mapper.CwProjectApplicationMapper; import org.dromara.cw.domain.CwProjectApplication; +import org.dromara.cw.domain.vo.CwProjectAuditorVo; import org.dromara.system.service.ISysUserService; import org.dromara.system.service.ISysDeptService; import org.dromara.system.domain.vo.SysUserVo; @@ -54,6 +55,7 @@ public class CwApprovalRecordServiceImpl implements ICwApprovalRecordService { private final CwProjectApplicationMapper applicationMapper; private final ISysUserService sysUserService; private final ISysDeptService sysDeptService; + private final ICwProjectAuditorService projectAuditorService; /** * 根据主键查询审批记录 @@ -502,6 +504,17 @@ public class CwApprovalRecordServiceImpl implements ICwApprovalRecordService { return flag; } + /** + * 批量新增审批记录 + */ + @Override + public Boolean batchInsert(List records) { + if (records == null || records.isEmpty()) { + return false; + } + return baseMapper.batchInsert(records) > 0; + } + /** * 修改审批记录 */ @@ -678,8 +691,8 @@ public class CwApprovalRecordServiceImpl implements ICwApprovalRecordService { Long currentDeptId, String currentTenantId) { CwApprovalRecord record = new CwApprovalRecord(); - // 生成唯一ID(批量插入需要手动生成) - record.setId(SequenceUtils.nextId("approval_record", Duration.ofDays(1))); + // 移除手动ID设置,使用MyBatis-Plus自动生成雪花ID(符合项目规范) + // record.setId() - 让MyBatis-Plus自动生成,避免主键冲突 // 设置申报相关信息 record.setApplicationId(applicationId); @@ -979,27 +992,110 @@ public class CwApprovalRecordServiceImpl implements ICwApprovalRecordService { } private Boolean updateApplicationStatusAndProcess(Long applicationId, String nodeCode, String approveAction) { - if ("approve".equals(approveAction)) { - // 审批通过,启动下一个节点或完成审批 - if ("dept_approve".equals(nodeCode)) { - // 部门审批通过,启动财务审批 - return startNextApprovalNode(applicationId, "finance_approve"); - } else if ("finance_approve".equals(nodeCode)) { - // 财务审批通过,启动领导审批 - return startNextApprovalNode(applicationId, "leader_approve"); - } else if ("leader_approve".equals(nodeCode)) { - // 领导审批通过,整个审批流程完成 - return projectApplicationService.updateApprovalStatus(applicationId, "approved"); - } - } else if ("reject".equals(approveAction)) { - // 审批拒绝,整个流程结束 - return projectApplicationService.updateApprovalStatus(applicationId, "rejected"); - } else if ("return".equals(approveAction)) { - // 退回修改,申报状态改为已退回 - return projectApplicationService.updateApprovalStatus(applicationId, "returned"); - } + try { + // 获取当前审批记录 + LambdaQueryWrapper currentQuery = Wrappers.lambdaQuery() + .eq(CwApprovalRecord::getApplicationId, applicationId) + .eq(CwApprovalRecord::getNodeCode, nodeCode); + CwApprovalRecord currentRecord = baseMapper.selectOne(currentQuery); - return true; + if (currentRecord == null) { + log.error("❌ 当前审批记录不存在:applicationId={}, nodeCode={}", applicationId, nodeCode); + return false; + } + + if ("approve".equals(approveAction)) { + // 🔧 基于node_order的顺序审批逻辑 + log.info("✅ 审批通过,查找下一级审批:applicationId={}, currentOrder={}", + applicationId, currentRecord.getNodeOrder()); + + // 查找下一级审批记录(node_order + 1) + LambdaQueryWrapper nextQuery = Wrappers.lambdaQuery() + .eq(CwApprovalRecord::getApplicationId, applicationId) + .eq(CwApprovalRecord::getNodeOrder, currentRecord.getNodeOrder() + 1); + CwApprovalRecord nextRecord = baseMapper.selectOne(nextQuery); + + if (nextRecord != null) { + // 存在下一级审批,激活下一级 + log.info("📋 激活下一级审批:nextRecordId={}, nodeOrder={}, nodeCode={}, currentStatus={}", + nextRecord.getId(), nextRecord.getNodeOrder(), nextRecord.getNodeCode(), nextRecord.getNodeStatus()); + + // 使用条件更新,确保只有在特定状态下才能激活 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate() + .eq(CwApprovalRecord::getId, nextRecord.getId()) + .eq(CwApprovalRecord::getApplicationId, applicationId) + .ne(CwApprovalRecord::getNodeStatus, "waiting") // 避免重复激活已经是waiting状态的记录 + .ne(CwApprovalRecord::getNodeStatus, "approved") // 避免激活已完成的记录 + .ne(CwApprovalRecord::getNodeStatus, "rejected") // 避免激活已拒绝的记录 + .set(CwApprovalRecord::getNodeStatus, "waiting") + .set(CwApprovalRecord::getStartTime, new Date()) // 设置开始时间 + .set(CwApprovalRecord::getUpdateTime, new Date()) + .set(CwApprovalRecord::getUpdateBy, LoginHelper.getUserId()); + + int updateResult = baseMapper.update(null, updateWrapper); + + if (updateResult <= 0) { + log.error("❌ 激活下一级审批失败: applicationId={}, nextRecordId={}, nodeOrder={}, nodeCode={}", + applicationId, nextRecord.getId(), nextRecord.getNodeOrder(), nextRecord.getNodeCode()); + + // 重新查询记录状态,分析失败原因 + CwApprovalRecord checkRecord = baseMapper.selectById(nextRecord.getId()); + if (checkRecord == null) { + log.error("❌ 下一级审批记录已不存在: recordId={}", nextRecord.getId()); + } else { + log.error("❌ 下一级审批记录状态不符合激活条件: recordId={}, currentStatus={}, 限制条件=不能是[waiting,approved,rejected]", + nextRecord.getId(), checkRecord.getNodeStatus()); + } + return false; + } + + log.info("✅ 下一级审批已激活成功: recordId={}, newStatus=waiting", nextRecord.getId()); + return true; + } else { + // 没有下一级审批,整个流程完成 + log.info("🎉 所有审批完成,更新申报状态为approved: applicationId={}, currentOrder={}", + applicationId, currentRecord.getNodeOrder()); + + boolean statusUpdateResult = projectApplicationService.updateApprovalStatus(applicationId, "approved"); + if (!statusUpdateResult) { + log.error("❌ 更新申报状态为approved失败: applicationId={}", applicationId); + return false; + } + + log.info("✅ 申报状态已成功更新为approved: applicationId={}", applicationId); + return true; + } + + } else if ("reject".equals(approveAction)) { + // 审批拒绝,整个流程结束 + log.info("❌ 审批拒绝,更新申报状态为rejected: applicationId={}", applicationId); + boolean rejectResult = projectApplicationService.updateApprovalStatus(applicationId, "rejected"); + if (!rejectResult) { + log.error("❌ 更新申报状态为rejected失败: applicationId={}", applicationId); + return false; + } + log.info("✅ 申报状态已更新为rejected: applicationId={}", applicationId); + return true; + + } else if ("return".equals(approveAction)) { + // 退回修改,申报状态改为已退回,保留历史审批记录 + log.info("↩️ 审批退回,更新申报状态为returned: applicationId={}", applicationId); + boolean returnResult = projectApplicationService.updateApprovalStatus(applicationId, "returned"); + if (!returnResult) { + log.error("❌ 更新申报状态为returned失败: applicationId={}", applicationId); + return false; + } + log.info("✅ 申报状态已更新为returned: applicationId={}", applicationId); + return true; + } + + return true; + + } catch (Exception e) { + log.error("❌ 更新申报状态和流程失败:applicationId={}, nodeCode={}, action={}, error={}", + applicationId, nodeCode, approveAction, e.getMessage(), e); + return false; + } } private Boolean startNextApprovalNode(Long applicationId, String nextNodeCode) { @@ -1450,4 +1546,122 @@ public class CwApprovalRecordServiceImpl implements ICwApprovalRecordService { throw new RuntimeException("指定院领导审批人失败:" + e.getMessage()); } } + + /** + * 创建自定义审批流程 + * 根据用户选择的审批人列表创建自定义审批流程 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean createCustomApprovalProcess(Long applicationId, String applicationNo, + List auditorIds, Long submitterId, String submitterName) { + + log.info("🚀 创建自定义审批流程:applicationId={}, auditorIds={}", applicationId, auditorIds); + + try { + // 1. 验证参数 + if (applicationId == null) { + throw new RuntimeException("申报ID不能为空"); + } + if (auditorIds == null || auditorIds.isEmpty()) { + throw new RuntimeException("审批人列表不能为空"); + } + + // 2. 验证审批人是否有效 + List validAuditors = validateAuditors(auditorIds); + if (validAuditors.size() != auditorIds.size()) { + throw new RuntimeException("包含无效的审批人,请重新选择"); + } + + // 3. 创建自定义审批记录 + List records = new ArrayList<>(); + + for (int i = 0; i < auditorIds.size(); i++) { + Long auditorId = auditorIds.get(i); + CwProjectAuditorVo auditor = validAuditors.get(i); + + CwApprovalRecord record = new CwApprovalRecord(); + // 移除手动ID设置,使用MyBatis-Plus自动生成雪花ID(符合项目规范) + // record.setId() - 让MyBatis-Plus自动生成,避免主键冲突 + record.setApplicationId(applicationId); + record.setApplicationNo(applicationNo); + record.setNodeCode("custom_node_" + (i + 1)); + record.setNodeName("自定义审批节点" + (i + 1)); + record.setNodeOrder(i + 1); + record.setApproverId(auditorId); + record.setApproverName(auditor.getUserName()); + record.setNodeStatus(i == 0 ? "waiting" : "pending"); // 第一个waiting,其他pending + + // 设置基础字段 + Date now = new Date(); + record.setCreateBy(submitterId); + record.setCreateTime(now); + record.setUpdateBy(submitterId); + record.setUpdateTime(now); + record.setTenantId(LoginHelper.getTenantId()); + record.setIsDelegate(false); + record.setIsOvertime(false); + + // 🔧 修复:设置startTime字段(数据库NOT NULL约束) + if (i == 0) { + // 第一个节点:waiting状态,立即开始 + record.setStartTime(now); + log.info("设置自定义审批第一个节点开始时间:{} -> {}", record.getNodeCode(), now); + } else { + // 后续节点:pending状态,也需要设置startTime满足数据库约束 + record.setStartTime(now); + log.info("设置自定义审批后续节点开始时间:{} -> {} (满足数据库约束)", record.getNodeCode(), now); + } + + records.add(record); + } + + // 4. 批量插入审批记录 + boolean result = false; + if (!records.isEmpty()) { + for (CwApprovalRecord record : records) { + result = baseMapper.insert(record) > 0; + if (!result) { + throw new RuntimeException("创建审批记录失败"); + } + } + } + + // 5. 同步更新项目申报状态为"审批中" + // 这是关键步骤:启动审批流程的同时提交申报 + ICwProjectApplicationService applicationService = SpringUtils.getBean(ICwProjectApplicationService.class); + boolean statusUpdateResult = applicationService.updateApprovalStatus(applicationId, "approving"); + if (!statusUpdateResult) { + throw new RuntimeException("更新申报状态失败,审批流程创建失败"); + } + + log.info("✅ 自定义审批流程创建成功:共创建{}个审批节点", records.size()); + log.info("✅ 申报状态已更新为审批中:applicationId={}", applicationId); + return result; + + } catch (Exception e) { + log.error("❌ 创建自定义审批流程失败:{}", e.getMessage(), e); + throw new RuntimeException("创建审批流程失败:" + e.getMessage()); + } + } + + /** + * 验证审批人列表 + */ + private List validateAuditors(List auditorIds) { + List validAuditors = new ArrayList<>(); + + for (Long auditorId : auditorIds) { + CwProjectAuditorVo auditor = projectAuditorService.queryByUserId(auditorId); + if (auditor == null) { + throw new RuntimeException("审批人不存在:" + auditorId); + } + if (!"1".equals(auditor.getStatus())) { + throw new RuntimeException("审批人已停用:" + auditor.getUserName()); + } + validAuditors.add(auditor); + } + + return validAuditors; + } } diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectApplicationServiceImpl.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectApplicationServiceImpl.java index a6d59fba..4fcaf271 100644 --- a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectApplicationServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectApplicationServiceImpl.java @@ -19,8 +19,8 @@ import org.dromara.cw.domain.CwProjectApplication; import org.dromara.cw.mapper.CwProjectApplicationMapper; import org.dromara.cw.service.ICwProjectApplicationService; import org.dromara.cw.service.ICwPerformanceIndicatorService; -import org.dromara.cw.event.ApprovalProcessCreatedEvent; -import org.springframework.context.ApplicationEventPublisher; +import org.dromara.cw.service.ICwApprovalRecordService; +import org.dromara.common.core.utils.SpringUtils; import java.text.SimpleDateFormat; import java.util.*; @@ -41,7 +41,6 @@ public class CwProjectApplicationServiceImpl implements ICwProjectApplicationSer private final CwProjectApplicationMapper baseMapper; private final ICwPerformanceIndicatorService performanceIndicatorService; - private final ApplicationEventPublisher eventPublisher; /** * 查询项目预算申报 @@ -584,30 +583,34 @@ public class CwProjectApplicationServiceImpl implements ICwProjectApplicationSer } if (updateResult) { - // 异步发布审批流程创建事件(避免阻塞主流程) + // 直接同步创建审批流程(确保事务一致性) try { - log.info("🚀 准备异步发布审批流程创建事件:applicationId={}, applicationNo={}", id, application.getApplicationNo()); + log.info("🚀 开始同步创建审批流程:applicationId={}, applicationNo={}", id, application.getApplicationNo()); - // 在主线程中收集用户上下文信息(异步线程中无法获取) + // 收集用户上下文信息 Long currentUserId = LoginHelper.getUserId(); String currentUserName = LoginHelper.getUsername(); Long currentDeptId = LoginHelper.getDeptId(); String currentTenantId = LoginHelper.getTenantId(); - log.info("📋 收集用户上下文:userId={}, userName={}, deptId={}, tenantId={}", + log.info("📋 用户上下文:userId={}, userName={}, deptId={}, tenantId={}", currentUserId, currentUserName, currentDeptId, currentTenantId); - ApprovalProcessCreatedEvent event = new ApprovalProcessCreatedEvent( - this, id, application.getApplicationNo(), + // 使用SpringUtils动态获取服务,避免循环依赖 + ICwApprovalRecordService approvalRecordService = SpringUtils.getBean(ICwApprovalRecordService.class); + Boolean createResult = approvalRecordService.createApprovalProcess( + id, application.getApplicationNo(), currentUserId, currentUserName, currentDeptId, currentTenantId); - log.info("📤 异步发布事件(不阻塞主流程)..."); - eventPublisher.publishEvent(event); - log.info("✅ 审批流程创建事件发布完成(异步处理中):applicationId={}, applicationNo={}", - id, application.getApplicationNo()); + if (createResult) { + log.info("✅ 审批流程创建成功:applicationId={}, applicationNo={}", id, application.getApplicationNo()); + } else { + log.error("❌ 审批流程创建失败:applicationId={}, applicationNo={}", id, application.getApplicationNo()); + throw new RuntimeException("审批流程创建失败"); + } } catch (Exception e) { - log.error("💥 发布审批流程创建事件失败:applicationId={}, error={}", id, e.getMessage(), e); - // 事件发布失败不影响提交,审批流程会在后台异步创建 + log.error("💥 创建审批流程异常:applicationId={}, error={}", id, e.getMessage(), e); + throw new RuntimeException("创建审批流程失败:" + e.getMessage(), e); } } else { log.error("❌ 申报提交失败,数据库更新返回false:applicationId={}", id); @@ -667,16 +670,66 @@ public class CwProjectApplicationServiceImpl implements ICwProjectApplicationSer throw new RuntimeException("申报记录不存在"); } + // 验证状态转换的合法性 + String currentStatus = application.getApprovalStatus(); + log.info("更新申报状态:applicationId={}, {} -> {}", id, currentStatus, approvalStatus); + + // 当从草稿状态提交到审批中时,进行数据完整性验证和字段设置 + if ("draft".equals(currentStatus) && "approving".equals(approvalStatus)) { + // 验证申报数据完整性 + validateApplicationForSubmission(application); + + // 设置提交时间(第一次提交) + if (application.getSubmitTime() == null) { + application.setSubmitTime(new Date()); + log.info("设置申报提交时间:applicationId={}", id); + } + + // 生成申报编号(如果还没有) + if (StringUtils.isBlank(application.getApplicationNo())) { + String applicationNo = generateApplicationNo(); + application.setApplicationNo(applicationNo); + log.info("生成申报编号:applicationId={}, applicationNo={}", id, applicationNo); + } + + log.info("✅ 申报正式提交:从草稿状态提交到审批中"); + } + + // 更新审批状态 application.setApprovalStatus(approvalStatus); // 如果是最终通过或拒绝状态,设置审批完成时间 if ("approved".equals(approvalStatus) || "rejected".equals(approvalStatus)) { application.setApproveTime(new Date()); + log.info("设置审批完成时间:applicationId={}, status={}", id, approvalStatus); } return baseMapper.updateById(application) > 0; } + /** + * 验证申报数据完整性(提交时验证) + */ + private void validateApplicationForSubmission(CwProjectApplication application) { + if (StringUtils.isBlank(application.getProjectName())) { + throw new RuntimeException("项目名称不能为空"); + } + if (StringUtils.isBlank(application.getPrincipal())) { + throw new RuntimeException("项目负责人不能为空"); + } + if (application.getTotalInvestment() == null || application.getTotalInvestment().compareTo(BigDecimal.ZERO) <= 0) { + throw new RuntimeException("项目总投资必须大于0"); + } + if (application.getYear() == null) { + throw new RuntimeException("预算年度不能为空"); + } + if (StringUtils.isBlank(application.getApplicationReason())) { + throw new RuntimeException("项目申请原因不能为空"); + } + + log.info("✅ 申报数据完整性验证通过:applicationId={}", application.getId()); + } + /** * 生成申报编号 */ diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectApprovalProcessServiceImpl.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectApprovalProcessServiceImpl.java new file mode 100644 index 00000000..a5eab539 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectApprovalProcessServiceImpl.java @@ -0,0 +1,289 @@ +package org.dromara.cw.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.cw.domain.bo.CwProjectApprovalProcessBo; +import org.dromara.cw.domain.bo.CwCustomApprovalBo; +import org.dromara.cw.domain.vo.CwProjectApprovalProcessVo; +import org.dromara.cw.domain.CwProjectApprovalProcess; +import org.dromara.cw.mapper.CwProjectApprovalProcessMapper; +import org.dromara.cw.service.ICwProjectApprovalProcessService; +import org.dromara.common.satoken.utils.LoginHelper; + +import java.util.List; +import java.util.Map; +import java.util.Collection; +import java.util.Date; + +/** + * 项目审批流程Service业务层处理 + * + * @author ruoyi + * @date 2025-01-03 + */ +@RequiredArgsConstructor +@Service +public class CwProjectApprovalProcessServiceImpl implements ICwProjectApprovalProcessService { + + private final CwProjectApprovalProcessMapper baseMapper; + + /** + * 查询项目审批流程 + */ + @Override + public CwProjectApprovalProcessVo queryById(Long id){ + return baseMapper.selectVoById(id); + } + + /** + * 查询项目审批流程列表 + */ + @Override + public TableDataInfo queryPageList(CwProjectApprovalProcessVo vo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(vo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询项目审批流程列表 + */ + @Override + public List queryList(CwProjectApprovalProcessVo vo) { + LambdaQueryWrapper lqw = buildQueryWrapper(vo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(CwProjectApprovalProcessVo vo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(vo.getApplicationId() != null, CwProjectApprovalProcess::getApplicationId, vo.getApplicationId()); + lqw.like(StringUtils.isNotBlank(vo.getApplicationNo()), CwProjectApprovalProcess::getApplicationNo, vo.getApplicationNo()); + lqw.like(StringUtils.isNotBlank(vo.getProcessName()), CwProjectApprovalProcess::getProcessName, vo.getProcessName()); + lqw.eq(StringUtils.isNotBlank(vo.getProcessStatus()), CwProjectApprovalProcess::getProcessStatus, vo.getProcessStatus()); + lqw.eq(vo.getTotalNodes() != null, CwProjectApprovalProcess::getTotalNodes, vo.getTotalNodes()); + lqw.eq(vo.getCurrentNode() != null, CwProjectApprovalProcess::getCurrentNode, vo.getCurrentNode()); + lqw.eq(vo.getSubmitterId() != null, CwProjectApprovalProcess::getSubmitterId, vo.getSubmitterId()); + lqw.like(StringUtils.isNotBlank(vo.getSubmitterName()), CwProjectApprovalProcess::getSubmitterName, vo.getSubmitterName()); + return lqw; + } + + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(CwProjectApprovalProcess entity){ + // 检查申报ID是否已存在流程 + if (entity.getApplicationId() != null) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CwProjectApprovalProcess::getApplicationId, entity.getApplicationId()); + if (entity.getId() != null) { + wrapper.ne(CwProjectApprovalProcess::getId, entity.getId()); + } + if (baseMapper.exists(wrapper)) { + throw new RuntimeException("该申报已存在审批流程"); + } + } + } + + /** + * 批量删除项目审批流程 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if(isValid){ + //TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 根据申报ID查询审批流程 + */ + @Override + public CwProjectApprovalProcessVo queryByApplicationId(Long applicationId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CwProjectApprovalProcess::getApplicationId, applicationId); + return baseMapper.selectVoOne(wrapper); + } + + /** + * 根据申报编号查询审批流程 + */ + @Override + public CwProjectApprovalProcessVo queryByApplicationNo(String applicationNo) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CwProjectApprovalProcess::getApplicationNo, applicationNo); + return baseMapper.selectVoOne(wrapper); + } + + /** + * 查询我提交的流程 + */ + @Override + public List queryMyProcesses(Long submitterId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CwProjectApprovalProcess::getSubmitterId, submitterId); + wrapper.orderByDesc(CwProjectApprovalProcess::getCreateTime); + return baseMapper.selectVoList(wrapper); + } + + /** + * 查询运行中的流程 + */ + @Override + public List queryRunningProcesses() { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CwProjectApprovalProcess::getProcessStatus, "running"); + wrapper.orderByDesc(CwProjectApprovalProcess::getCreateTime); + return baseMapper.selectVoList(wrapper); + } + + /** + * 查询超时流程 + */ + @Override + public List queryOvertimeProcesses(Integer hours) { + // 这里可以根据需要实现超时查询逻辑 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CwProjectApprovalProcess::getProcessStatus, "running"); + wrapper.orderByDesc(CwProjectApprovalProcess::getCreateTime); + return baseMapper.selectVoList(wrapper); + } + + /** + * 启动自定义审批流程 + */ + @Override + @Transactional + public Boolean startCustomApprovalProcess(CwCustomApprovalBo bo) { + try { + // 1. 创建审批流程 + CwProjectApprovalProcess process = new CwProjectApprovalProcess(); + process.setApplicationId(bo.getApplicationId()); + process.setApplicationNo(bo.getApplicationNo()); + process.setProcessName(bo.getProcessName()); + process.setProcessStatus("running"); + process.setTotalNodes(bo.getAuditorIds().size()); + process.setCurrentNode(1); + process.setStartTime(new Date()); + process.setSubmitterId(bo.getSubmitterId()); + process.setSubmitterName(bo.getSubmitterName()); + + boolean result = baseMapper.insert(process) > 0; + + // TODO: 这里可以创建对应的审批记录 + // 由于CwApprovalRecord相关的实现比较复杂,这里先简化处理 + + return result; + } catch (Exception e) { + throw new RuntimeException("启动审批流程失败:" + e.getMessage()); + } + } + + + /** + * 终止审批流程 + */ + @Override + @Transactional + public Boolean terminateProcess(Long id, String reason) { + CwProjectApprovalProcess update = new CwProjectApprovalProcess(); + update.setId(id); + update.setProcessStatus("terminated"); + update.setEndTime(new Date()); + update.setUpdateBy(LoginHelper.getUserId()); + return baseMapper.updateById(update) > 0; + } + + /** + * 推进流程到下一节点 + */ + @Override + public Boolean advanceToNextNode(Long processId) { + CwProjectApprovalProcess process = baseMapper.selectById(processId); + if (process != null && process.getCurrentNode() < process.getTotalNodes()) { + return updateCurrentNode(processId, process.getCurrentNode() + 1); + } + return false; + } + + /** + * 完成审批流程 + */ + @Override + public Boolean completeProcess(Long processId) { + return updateProcessStatus(processId, "completed"); + } + + /** + * 更新流程当前节点 + */ + @Override + public Boolean updateCurrentNode(Long id, Integer currentNode) { + CwProjectApprovalProcess update = new CwProjectApprovalProcess(); + update.setId(id); + update.setCurrentNode(currentNode); + update.setUpdateBy(LoginHelper.getUserId()); + return baseMapper.updateById(update) > 0; + } + + /** + * 更新流程状态 + */ + @Override + public Boolean updateProcessStatus(Long id, String status) { + CwProjectApprovalProcess update = new CwProjectApprovalProcess(); + update.setId(id); + update.setProcessStatus(status); + update.setUpdateBy(LoginHelper.getUserId()); + if ("completed".equals(status) || "terminated".equals(status)) { + update.setEndTime(new Date()); + } + return baseMapper.updateById(update) > 0; + } + + /** + * 验证审批流程配置 + */ + @Override + public Boolean validateApprovalProcess(CwCustomApprovalBo bo) { + // 验证基本参数 + if (bo.getApplicationId() == null || + StringUtils.isBlank(bo.getApplicationNo()) || + bo.getAuditorIds() == null || bo.getAuditorIds().isEmpty()) { + return false; + } + // TODO: 可以添加更多验证逻辑 + return true; + } + + /** + * 统计审批流程状态 + */ + @Override + public List> queryProcessStatistics(Long submitterId) { + // 这里可以根据需要实现统计逻辑 + return List.of(Map.of( + "totalProcesses", 0, + "runningProcesses", 0, + "completedProcesses", 0, + "terminatedProcesses", 0 + )); + } + + /** + * 导出数据 + */ + @Override + public List exportList(CwProjectApprovalProcessVo vo) { + return queryList(vo); + } +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectApprovalServiceImpl.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectApprovalServiceImpl.java new file mode 100644 index 00000000..42d49899 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectApprovalServiceImpl.java @@ -0,0 +1,258 @@ +package org.dromara.cw.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.dromara.cw.domain.bo.CwCustomApprovalBo; +import org.dromara.cw.domain.bo.CwApprovalActionBo; +import org.dromara.cw.domain.vo.CwProjectApprovalProcessVo; +import org.dromara.cw.domain.vo.CwApprovalRecordVo; +import org.dromara.cw.service.ICwProjectApprovalService; +import org.dromara.cw.service.ICwProjectApprovalProcessService; +import org.dromara.cw.service.ICwApprovalRecordService; +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.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.dromara.cw.domain.CwApprovalRecord; +import cn.hutool.core.bean.BeanUtil; + +import java.util.List; +import java.util.ArrayList; + +/** + * 项目审批流程Service业务层处理 + * + * @author ruoyi + * @date 2025-01-03 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class CwProjectApprovalServiceImpl implements ICwProjectApprovalService { + + private final ICwProjectApprovalProcessService processService; + private final ICwApprovalRecordService recordService; + + /** + * 启动自定义审批流程 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean startCustomApprovalProcess(CwCustomApprovalBo bo) { + try { + log.info("🚀 启动自定义审批流程: applicationId={}, auditorIds={}", + bo.getApplicationId(), bo.getAuditorIds()); + + // 1. 验证参数 + if (bo.getApplicationId() == null) { + throw new RuntimeException("申报ID不能为空"); + } + if (bo.getAuditorIds() == null || bo.getAuditorIds().isEmpty()) { + throw new RuntimeException("审批人列表不能为空"); + } + + // 2. 调用审批记录服务创建自定义审批流程 + Boolean result = recordService.createCustomApprovalProcess( + bo.getApplicationId(), + bo.getApplicationNo(), + bo.getAuditorIds(), + bo.getSubmitterId(), + bo.getSubmitterName() + ); + + if (result) { + log.info("✅ 自定义审批流程启动成功: applicationId={}", bo.getApplicationId()); + } else { + log.error("❌ 自定义审批流程启动失败: applicationId={}", bo.getApplicationId()); + } + + return result; + + } catch (Exception e) { + log.error("❌ 启动自定义审批流程失败: applicationId={}, error={}", + bo.getApplicationId(), e.getMessage(), e); + throw new RuntimeException("启动审批流程失败:" + e.getMessage()); + } + } + + /** + * 执行审批操作 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean executeApproval(CwApprovalActionBo bo) { + try { + log.info("执行审批操作: recordId={}, action={}, opinion={}", + bo.getRecordId(), bo.getAction(), bo.getOpinion()); + + // 🔧 修复:先通过recordId获取审批记录信息 + CwApprovalRecordVo record = recordService.queryById(bo.getRecordId()); + if (record == null) { + throw new RuntimeException("审批记录不存在:" + bo.getRecordId()); + } + + // 🔧 修复:调用现有的审批记录服务执行审批 + Boolean result = recordService.processApproval( + record.getApplicationId(), + record.getNodeCode(), + bo.getAction(), + bo.getOpinion() + ); + + log.info("✅ 审批操作执行完成:recordId={}, action={}, result={}", + bo.getRecordId(), bo.getAction(), result); + + return result; + } catch (Exception e) { + log.error("执行审批操作失败: recordId={}, action={}, error={}", + bo.getRecordId(), bo.getAction(), e.getMessage(), e); + throw new RuntimeException("审批操作失败:" + e.getMessage(), e); + } + } + + /** + * 获取我的待审批列表 + */ + @Override + public TableDataInfo getMyPendingApprovals(PageQuery pageQuery) { + try { + Long userId = LoginHelper.getUserId(); + log.info("获取用户{}的待审批列表", userId); + + // 🔧 修复:调用现有的审批记录服务查询方法 + List list = recordService.queryPendingByApproverId(userId); + + // 应用分页 + int total = list.size(); + int start = Math.max(0, (pageQuery.getPageNum() - 1) * pageQuery.getPageSize()); + int end = Math.min(start + pageQuery.getPageSize(), total); + + List pagedList = start < total ? + list.subList(start, end) : new ArrayList<>(); + + log.info("✅ 查询待审批列表成功:总数={}, 当前页={}, 返回数量={}", + total, pageQuery.getPageNum(), pagedList.size()); + + // 🔧 修复:使用正确的TableDataInfo构造方法 + TableDataInfo result = new TableDataInfo<>(pagedList, total); + return result; + } catch (Exception e) { + log.error("获取待审批列表失败: {}", e.getMessage(), e); + return TableDataInfo.build(new ArrayList<>()); + } + } + + /** + * 获取我的已审批列表 + */ + @Override + public TableDataInfo getMyProcessedApprovals(PageQuery pageQuery) { + try { + Long userId = LoginHelper.getUserId(); + log.info("获取用户{}的已审批列表", userId); + + // 🔧 修复:直接查询已审批记录,然后手动分页 + List allList = recordService.queryApprovedByCurrentUser(); + + // 应用分页 + int total = allList.size(); + int start = Math.max(0, (pageQuery.getPageNum() - 1) * pageQuery.getPageSize()); + int end = Math.min(start + pageQuery.getPageSize(), total); + + List pagedList = start < total ? + allList.subList(start, end) : new ArrayList<>(); + + log.info("✅ 查询已审批列表成功:总数={}, 当前页={}, 返回数量={}", + total, pageQuery.getPageNum(), pagedList.size()); + + // 🔧 修复:使用正确的TableDataInfo构造方法 + TableDataInfo result = new TableDataInfo<>(pagedList, total); + return result; + } catch (Exception e) { + log.error("获取已审批列表失败: {}", e.getMessage(), e); + return TableDataInfo.build(new ArrayList<>()); + } + } + + /** + * 查询审批流程列表 + */ + @Override + public TableDataInfo queryProcessList(PageQuery pageQuery) { + try { + log.info("查询审批流程列表"); + + // 这里应该查询具体的流程数据 + // 由于缺少具体的业务逻辑,这里返回空列表 + // 实际项目中需要根据具体业务需求实现 + + List list = new ArrayList<>(); + return TableDataInfo.build(list); + } catch (Exception e) { + log.error("查询审批流程列表失败: {}", e.getMessage(), e); + return TableDataInfo.build(new ArrayList<>()); + } + } + + /** + * 获取审批流程详情 + */ + @Override + public CwProjectApprovalProcessVo getProcessDetail(Long processId) { + try { + log.info("获取审批流程详情: {}", processId); + + // 这里应该查询具体的流程详情 + // 由于缺少具体的业务逻辑,这里返回null + // 实际项目中需要根据具体业务需求实现 + + return null; + } catch (Exception e) { + log.error("获取审批流程详情失败: {}", e.getMessage(), e); + return null; + } + } + + /** + * 获取审批记录列表 + */ + @Override + public List getApprovalRecords(Long applicationId) { + try { + log.info("获取审批记录列表: {}", applicationId); + + // 这里应该查询具体的审批记录 + // 由于缺少具体的业务逻辑,这里返回空列表 + // 实际项目中需要根据具体业务需求实现 + + return new ArrayList<>(); + } catch (Exception e) { + log.error("获取审批记录列表失败: {}", e.getMessage(), e); + return new ArrayList<>(); + } + } + + /** + * 终止审批流程 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean terminateProcess(List processIds) { + try { + log.info("终止审批流程: {}", processIds); + + // 这里应该实现具体的流程终止逻辑 + // 由于缺少具体的业务逻辑,这里返回成功状态 + // 实际项目中需要根据具体业务需求实现 + + return true; + } catch (Exception e) { + log.error("终止审批流程失败: {}", e.getMessage(), e); + return false; + } + } +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectAuditorServiceImpl.java b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectAuditorServiceImpl.java new file mode 100644 index 00000000..42c6e73d --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/java/org/dromara/cw/service/impl/CwProjectAuditorServiceImpl.java @@ -0,0 +1,344 @@ +package org.dromara.cw.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 lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.dromara.cw.domain.bo.CwProjectAuditorBo; +import org.dromara.cw.domain.vo.CwProjectAuditorVo; +import org.dromara.cw.domain.CwProjectAuditor; +import org.dromara.cw.domain.UserWithDeptInfo; +import org.dromara.cw.mapper.CwProjectAuditorMapper; +import org.dromara.cw.service.ICwProjectAuditorService; +import org.dromara.system.service.ISysUserService; +import org.dromara.system.domain.vo.SysUserVo; +import org.dromara.common.satoken.utils.LoginHelper; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Collection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.stream.Collectors; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.TemporalAdjusters; + +/** + * 项目审核人配置Service业务层处理 + * + * @author ruoyi + * @date 2025-01-03 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class CwProjectAuditorServiceImpl implements ICwProjectAuditorService { + + private final CwProjectAuditorMapper baseMapper; + private final ISysUserService sysUserService; + + /** + * 查询项目审核人配置 + */ + @Override + public CwProjectAuditorVo queryById(Long id){ + return baseMapper.selectVoById(id); + } + + /** + * 查询项目审核人配置列表 + */ + @Override + public TableDataInfo queryPageList(CwProjectAuditorBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询项目审核人配置列表 + */ + @Override + public List queryList(CwProjectAuditorBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(CwProjectAuditorBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(bo.getUserId() != null, CwProjectAuditor::getUserId, bo.getUserId()); + lqw.like(StringUtils.isNotBlank(bo.getUserName()), CwProjectAuditor::getUserName, bo.getUserName()); + lqw.like(StringUtils.isNotBlank(bo.getNickName()), CwProjectAuditor::getNickName, bo.getNickName()); + lqw.eq(bo.getDeptId() != null, CwProjectAuditor::getDeptId, bo.getDeptId()); + lqw.like(StringUtils.isNotBlank(bo.getDeptName()), CwProjectAuditor::getDeptName, bo.getDeptName()); + lqw.eq(StringUtils.isNotBlank(bo.getPhoneNumber()), CwProjectAuditor::getPhoneNumber, bo.getPhoneNumber()); + lqw.eq(StringUtils.isNotBlank(bo.getEmail()), CwProjectAuditor::getEmail, bo.getEmail()); + lqw.eq(StringUtils.isNotBlank(bo.getStatus()), CwProjectAuditor::getStatus, bo.getStatus()); + lqw.eq(bo.getConfigBy() != null, CwProjectAuditor::getConfigBy, bo.getConfigBy()); + lqw.like(StringUtils.isNotBlank(bo.getConfigByName()), CwProjectAuditor::getConfigByName, bo.getConfigByName()); + return lqw; + } + + /** + * 新增项目审核人配置 + */ + @Override + public Boolean insertByBo(CwProjectAuditorBo bo) { + CwProjectAuditor add = MapstructUtils.convert(bo, CwProjectAuditor.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改项目审核人配置 + */ + @Override + public Boolean updateByBo(CwProjectAuditorBo bo) { + CwProjectAuditor update = MapstructUtils.convert(bo, CwProjectAuditor.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(CwProjectAuditor entity){ + // 检查用户是否已存在 + if (entity.getUserId() != null) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CwProjectAuditor::getUserId, entity.getUserId()); + if (entity.getId() != null) { + wrapper.ne(CwProjectAuditor::getId, entity.getId()); + } + if (baseMapper.exists(wrapper)) { + throw new RuntimeException("该用户已经是审核人,不能重复添加"); + } + } + } + + /** + * 批量删除项目审核人配置 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if(isValid){ + //TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 批量添加审核人 + */ + @Override + @Transactional + public Boolean batchAddAuditors(List userIds, Long configBy, String configByName, String remark) { + if (userIds == null || userIds.isEmpty()) { + return false; + } + + // 1. 获取完整的用户信息(包含部门信息) + List users = baseMapper.selectUsersWithDeptInfo(userIds); + if (users.isEmpty()) { + return false; + } + + // 2. 过滤已存在的审核人 + List existingUserIds = baseMapper.selectList(Wrappers.lambdaQuery() + .in(CwProjectAuditor::getUserId, userIds)) + .stream().map(CwProjectAuditor::getUserId).collect(Collectors.toList()); + + // 3. 构建新增审核人列表 + List auditors = new ArrayList<>(); + for (UserWithDeptInfo user : users) { + if (!existingUserIds.contains(user.getUserId())) { + CwProjectAuditor auditor = new CwProjectAuditor(); + auditor.setUserId(user.getUserId()); + auditor.setUserName(user.getUserName()); + auditor.setNickName(user.getNickName()); + auditor.setDeptId(user.getDeptId()); + auditor.setDeptName(user.getDeptName() != null ? user.getDeptName() : ""); + auditor.setPhoneNumber(user.getPhonenumber()); + auditor.setEmail(user.getEmail()); + auditor.setStatus("1"); // 默认正常状态 + auditor.setConfigBy(configBy); + auditor.setConfigByName(configByName); + auditor.setRemark(remark); + auditors.add(auditor); + } + } + + // 4. 批量保存 + if (!auditors.isEmpty()) { + return baseMapper.insertBatch(auditors); + } + return true; + } + + /** + * 获取可用的审核人列表 + */ + @Override + public List getAvailableAuditors() { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CwProjectAuditor::getStatus, "1"); // 只查询正常状态的审核人 + wrapper.orderByAsc(CwProjectAuditor::getDeptId, CwProjectAuditor::getUserName); + return baseMapper.selectVoList(wrapper); + } + + /** + * 根据部门ID获取用户列表 + */ + @Override + public List getDeptUsers(Long deptId) { + try { + // 参数验证 + if (deptId == null || deptId <= 0) { + log.warn("无效的部门ID: {}", deptId); + return Collections.emptyList(); + } + + // 调用系统用户服务获取部门用户 + List users = sysUserService.selectUserListByDept(deptId); + if (users == null || users.isEmpty()) { + log.info("部门 {} 下没有找到用户", deptId); + return Collections.emptyList(); + } + + return users.stream().map(user -> { + Map userMap = Map.of( + "userId", user.getUserId(), + "userName", user.getUserName(), + "nickName", user.getNickName() != null ? user.getNickName() : user.getUserName(), + "deptId", user.getDeptId(), + "deptName", user.getDeptName() != null ? user.getDeptName() : "", + "phonenumber", user.getPhonenumber() != null ? user.getPhonenumber() : "", + "email", user.getEmail() != null ? user.getEmail() : "" + ); + return (Object) userMap; + }).collect(Collectors.toList()); + } catch (Exception e) { + log.error("获取部门 {} 的用户列表失败: {}", deptId, e.getMessage(), e); + return Collections.emptyList(); + } + } + + /** + * 批量更新审核人状态 + */ + @Override + public Boolean updateStatus(List ids, String status) { + if (ids == null || ids.isEmpty()) { + return false; + } + + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(CwProjectAuditor::getId, ids); + + CwProjectAuditor update = new CwProjectAuditor(); + update.setStatus(status); + update.setUpdateBy(LoginHelper.getUserId()); + + return baseMapper.update(update, wrapper) > 0; + } + + @Override + public Object getStatistics() { + // 获取总审核人数 + long totalAuditors = baseMapper.selectCount(Wrappers.lambdaQuery() + .eq(CwProjectAuditor::getDeleted, 0)); + + // 获取正常状态审核人数 + long activeAuditors = baseMapper.selectCount(Wrappers.lambdaQuery() + .eq(CwProjectAuditor::getDeleted, 0) + .eq(CwProjectAuditor::getStatus, "1")); + + // 获取停用状态审核人数 + long inactiveAuditors = baseMapper.selectCount(Wrappers.lambdaQuery() + .eq(CwProjectAuditor::getDeleted, 0) + .eq(CwProjectAuditor::getStatus, "0")); + + // 获取部门数量(去重) + List auditors = baseMapper.selectList(Wrappers.lambdaQuery() + .eq(CwProjectAuditor::getDeleted, 0) + .select(CwProjectAuditor::getDeptId)); + long totalDepts = auditors.stream().map(CwProjectAuditor::getDeptId).distinct().count(); + + // 获取今日新增 + LocalDateTime startOfDay = LocalDateTime.now().with(LocalTime.MIN); + LocalDateTime endOfDay = LocalDateTime.now().with(LocalTime.MAX); + long todayAdded = baseMapper.selectCount(Wrappers.lambdaQuery() + .eq(CwProjectAuditor::getDeleted, 0) + .ge(CwProjectAuditor::getCreateTime, startOfDay) + .le(CwProjectAuditor::getCreateTime, endOfDay)); + + // 获取本月新增 + LocalDateTime startOfMonth = LocalDateTime.now().with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN); + LocalDateTime endOfMonth = LocalDateTime.now().with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX); + long monthlyAdded = baseMapper.selectCount(Wrappers.lambdaQuery() + .eq(CwProjectAuditor::getDeleted, 0) + .ge(CwProjectAuditor::getCreateTime, startOfMonth) + .le(CwProjectAuditor::getCreateTime, endOfMonth)); + + // 获取我配置的审核人数 + long myConfigured = baseMapper.selectCount(Wrappers.lambdaQuery() + .eq(CwProjectAuditor::getDeleted, 0) + .eq(CwProjectAuditor::getConfigBy, LoginHelper.getUserId())); + + // 获取最近活跃数(近7天有更新的) + LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7); + long recentActive = baseMapper.selectCount(Wrappers.lambdaQuery() + .eq(CwProjectAuditor::getDeleted, 0) + .ge(CwProjectAuditor::getUpdateTime, sevenDaysAgo)); + + // 构建统计结果 + Map statistics = new HashMap<>(); + statistics.put("totalAuditors", totalAuditors); + statistics.put("activeAuditors", activeAuditors); + statistics.put("inactiveAuditors", inactiveAuditors); + statistics.put("totalDepts", totalDepts); + statistics.put("todayAdded", todayAdded); + statistics.put("monthlyAdded", monthlyAdded); + statistics.put("myConfigured", myConfigured); + statistics.put("recentActive", recentActive); + + return statistics; + } + + /** + * 根据用户ID查询项目审核人配置 + */ + @Override + public CwProjectAuditorVo queryByUserId(Long userId) { + if (userId == null) { + return null; + } + + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(CwProjectAuditor::getUserId, userId) + .eq(CwProjectAuditor::getDeleted, 0) + .orderByDesc(CwProjectAuditor::getCreateTime) + .last("LIMIT 1"); + + CwProjectAuditor entity = baseMapper.selectOne(wrapper); + if (entity == null) { + return null; + } + + return MapstructUtils.convert(entity, CwProjectAuditorVo.class); + } +} diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwApprovalRecordMapper.xml b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwApprovalRecordMapper.xml index 11e1c691..43b4f9b6 100644 --- a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwApprovalRecordMapper.xml +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwApprovalRecordMapper.xml @@ -72,6 +72,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + @@ -90,7 +95,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ar.approver_name as current_approver_name, ar.node_status as approval_status, pa.create_time as submit_time, - ar.end_time as approval_time + ar.end_time as approval_time, + -- 流程相关字段(通过审批记录计算) + (SELECT COUNT(DISTINCT ar2.node_order) + FROM cw_approval_record ar2 + WHERE ar2.application_id = ar.application_id + AND ar2.deleted = 0) as total_nodes, + ar.node_order as current_node, + '项目审批' as process_name, + (CASE + WHEN EXISTS(SELECT 1 FROM cw_approval_record ar3 + WHERE ar3.application_id = ar.application_id + AND ar3.node_status = 'rejected' + AND ar3.deleted = 0) THEN 'rejected' + WHEN NOT EXISTS(SELECT 1 FROM cw_approval_record ar4 + WHERE ar4.application_id = ar.application_id + AND ar4.node_status IN ('waiting', 'processing') + AND ar4.deleted = 0) THEN 'completed' + ELSE 'processing' + END) as process_status from cw_approval_record ar left join cw_project_application pa on ar.application_id = pa.id left join sys_user u on pa.create_by = u.user_id diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwProjectApprovalProcessMapper.xml b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwProjectApprovalProcessMapper.xml new file mode 100644 index 00000000..469047cb --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwProjectApprovalProcessMapper.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + select + p.id, p.application_id, p.application_no, p.process_name, p.process_status, + p.total_nodes, p.current_node, p.start_time, p.end_time, + p.submitter_id, p.submitter_name, + p.create_by, p.create_time, p.update_by, p.update_time, + a.project_name, + u.nick_name as submitter_nick_name + from cw_project_approval_process p + left join cw_project_application a on p.application_id = a.id + left join sys_user u on p.submitter_id = u.user_id + + + + + + + + + + + + + + + + + + update cw_project_approval_process + set current_node = #{currentNode}, update_by = #{updateBy}, update_time = now() + where id = #{id} and deleted = 0 + + + + update cw_project_approval_process + set process_status = #{status}, + end_time = case when #{status} in ('completed', 'terminated') then now() else end_time end, + update_by = #{updateBy}, + update_time = now() + where id = #{id} and deleted = 0 + + + diff --git a/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwProjectAuditorMapper.xml b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwProjectAuditorMapper.xml new file mode 100644 index 00000000..fb8efb03 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-cw/src/main/resources/mapper/CwProjectAuditorMapper.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + select + a.id, a.user_id, a.user_name, a.nick_name, a.dept_id, a.dept_name, + a.phone_number, a.email, a.status, a.remark, a.config_by, a.config_by_name, + a.create_by, a.create_time, a.update_by, a.update_time, + u.user_name as sys_user_name, + d.dept_name as sys_dept_name + from cw_project_auditor a + left join sys_user u on a.user_id = u.user_id + left join sys_dept d on a.dept_id = d.dept_id + + + + + + + + + + + + + + + + + + update cw_project_auditor + set status = #{status}, update_by = #{updateBy}, update_time = now() + where id in + + #{id} + + and deleted = 0 + + + + + + diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java index a322e3d0..f03931a6 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java @@ -297,7 +297,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService { @Override public List selectUserByIds(List userIds, Long deptId) { return baseMapper.selectUserList(new LambdaQueryWrapper() - .select(SysUser::getUserId, SysUser::getUserName, SysUser::getNickName) + .select(SysUser::getUserId, SysUser::getUserName, SysUser::getNickName, SysUser::getPhonenumber) .eq(SysUser::getStatus, SystemConstants.NORMAL) .eq(ObjectUtil.isNotNull(deptId), SysUser::getDeptId, deptId) .in(CollUtil.isNotEmpty(userIds), SysUser::getUserId, userIds)); diff --git a/backend/ruoyi-modules/ruoyi-xg/pom.xml b/backend/ruoyi-modules/ruoyi-xg/pom.xml index e41186bf..a2987fb7 100644 --- a/backend/ruoyi-modules/ruoyi-xg/pom.xml +++ b/backend/ruoyi-modules/ruoyi-xg/pom.xml @@ -104,6 +104,13 @@ ruoyi-workflow + + + org.dromara + ruoyi-xskh + ${revision} + + diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBedStudentController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBedStudentController.java index 7de403d4..2c02ae9f 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBedStudentController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBedStudentController.java @@ -157,7 +157,7 @@ public class XgDormBedStudentController extends BaseController { /** * 获取当前用户二级学院的宿舍楼树形结构 - * 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择 + * 校区-宿舍楼二级结构,校区不可选,只有宿舍楼可以选择 */ @SaCheckPermission("dormBedStudent:dormBedStudent:list") @GetMapping("/currentUserCollegeDormTree") @@ -168,7 +168,7 @@ public class XgDormBedStudentController extends BaseController { /** * 获取班主任管理的班级宿舍楼树形结构 - * 只显示班主任管理班级分配到的宿舍楼 + * 校区-宿舍楼二级结构,只显示班主任管理班级分配到的宿舍楼 */ @SaCheckPermission("dormBedStudent:dormBedStudent:list") @GetMapping("/getClassTeacherDormTree") diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBuildingController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBuildingController.java index 30420e62..9d1b519b 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBuildingController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBuildingController.java @@ -18,6 +18,7 @@ import org.dromara.common.core.validate.EditGroup; import org.dromara.common.log.enums.BusinessType; import org.dromara.common.excel.utils.ExcelUtil; import org.dromara.xg.domain.vo.XgDormBuildingVo; +import org.dromara.xg.domain.vo.XgDormBuildingSimplifiedVo; import org.dromara.xg.domain.bo.XgDormBuildingBo; import org.dromara.xg.domain.vo.BuildingTreeSelectVo; import org.dromara.xg.service.IXgDormBuildingService; @@ -104,7 +105,7 @@ public class XgDormBuildingController extends BaseController { /** * 获取宿舍楼树形结构数据 - * 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择 + * 校区-宿舍楼二级结构,校区不可选,只有宿舍楼可以选择 */ @SaCheckPermission("sslxx:dormBuilding:list") @GetMapping("/buildingTree") @@ -112,4 +113,25 @@ public class XgDormBuildingController extends BaseController { List treeData = xgDormBuildingService.getBuildingTreeData(); return R.ok(treeData); } + + /** + * 查询宿舍楼信息简化列表 + * 用于简化界面显示,隐藏宿舍区层级,直接显示校区→宿舍楼结构 + */ + @SaCheckPermission("sslxx:dormBuilding:list") + @GetMapping("/simplifiedList") + public TableDataInfo getSimplifiedList(XgDormBuildingBo bo, PageQuery pageQuery) { + return xgDormBuildingService.querySimplifiedPageList(bo, pageQuery); + } + + /** + * 查询宿舍楼信息简化列表(不分页) + * 用于下拉选择等场景 + */ + @SaCheckPermission("sslxx:dormBuilding:list") + @GetMapping("/simplifiedListAll") + public R> getSimplifiedListAll(XgDormBuildingBo bo) { + List list = xgDormBuildingService.querySimplifiedList(bo); + return R.ok(list); + } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBuildingTreeController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBuildingTreeController.java index 01f01f94..a1cf2805 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBuildingTreeController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormBuildingTreeController.java @@ -9,6 +9,7 @@ import org.springframework.validation.annotation.Validated; import org.dromara.common.web.core.BaseController; import org.dromara.common.core.domain.R; import org.dromara.xg.domain.vo.DormBuildingTreeVo; +import org.dromara.xg.domain.vo.DormBuildingTreeSimplifiedVo; import org.dromara.xg.domain.bo.DormBuildingTreeBo; import org.dromara.xg.service.IXgDormBuildingTreeService; @@ -28,24 +29,70 @@ public class XgDormBuildingTreeController extends BaseController { /** * 获取宿舍楼树形结构数据 - * 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择 + * 校区-宿舍楼二级结构,校区不可选,只有宿舍楼可以选择 */ @SaCheckPermission("xgss:dormBuildingTree:list") @GetMapping("/list") - public R> getDormBuildingTree(DormBuildingTreeBo bo) { - List treeData = xgDormBuildingTreeService.getDormBuildingTree(bo); + public R> getDormBuildingTree(DormBuildingTreeBo bo) { + List treeData = xgDormBuildingTreeService.getSimplifiedDormBuildingTree(bo); return R.ok(treeData); } /** * 获取所有宿舍楼树形结构数据(不考虑权限) - * 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择 + * 校区-宿舍楼二级结构,校区不可选,只有宿舍楼可以选择 */ @SaCheckPermission("xgss:dormBuildingTree:list") @GetMapping("/listAll") - public R> getAllDormBuildingTree(DormBuildingTreeBo bo) { + public R> getAllDormBuildingTree(DormBuildingTreeBo bo) { + List treeData = xgDormBuildingTreeService.getAllSimplifiedDormBuildingTree(bo); + return R.ok(treeData); + } + + /** + * 获取宿舍楼树形结构完整数据(包含宿舍区层级) + * 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择 + * 注意:此接口仅用于特殊需求,一般情况请使用/list接口 + */ + @SaCheckPermission("xgss:dormBuildingTree:list") + @GetMapping("/fullList") + public R> getFullDormBuildingTree(DormBuildingTreeBo bo) { + List treeData = xgDormBuildingTreeService.getDormBuildingTree(bo); + return R.ok(treeData); + } + + /** + * 获取所有宿舍楼树形结构完整数据(不考虑权限,包含宿舍区层级) + * 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择 + * 注意:此接口仅用于特殊需求,一般情况请使用/listAll接口 + */ + @SaCheckPermission("xgss:dormBuildingTree:list") + @GetMapping("/fullListAll") + public R> getAllFullDormBuildingTree(DormBuildingTreeBo bo) { List treeData = xgDormBuildingTreeService.getAllDormBuildingTree(bo); return R.ok(treeData); } + /** + * 获取宿舍楼树形结构简化数据 + * 校区-宿舍楼二级结构,隐藏宿舍区层级,校区不可选,只有宿舍楼可以选择 + */ + @SaCheckPermission("xgss:dormBuildingTree:list") + @GetMapping("/simplifiedList") + public R> getSimplifiedDormBuildingTree(DormBuildingTreeBo bo) { + List treeData = xgDormBuildingTreeService.getSimplifiedDormBuildingTree(bo); + return R.ok(treeData); + } + + /** + * 获取所有宿舍楼树形结构简化数据(不考虑权限) + * 校区-宿舍楼二级结构,隐藏宿舍区层级,校区不可选,只有宿舍楼可以选择 + */ + @SaCheckPermission("xgss:dormBuildingTree:list") + @GetMapping("/simplifiedListAll") + public R> getAllSimplifiedDormBuildingTree(DormBuildingTreeBo bo) { + List treeData = xgDormBuildingTreeService.getAllSimplifiedDormBuildingTree(bo); + return R.ok(treeData); + } + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormHygieneCheckController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormHygieneCheckController.java index c5e203d0..a5c319ce 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormHygieneCheckController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormHygieneCheckController.java @@ -23,6 +23,7 @@ import org.dromara.xg.domain.bo.XgDormHygieneCheckBo; import org.dromara.xg.domain.bo.XgDormHygieneCheckCompleteBo; import org.dromara.xg.service.IXgDormHygieneCheckService; import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.xg.domain.vo.XgHygieneCheckViolationVo; /** * 宿舍卫生检查 @@ -164,4 +165,46 @@ public class XgDormHygieneCheckController extends BaseController { return xgDormHygieneCheckService.queryPageListByStudentId(studentId, bo, pageQuery); } + /** + * 保存完整的宿舍卫生检查数据(包含学生评分和违纪记录) + */ + @SaCheckPermission("dormHygieneCheck:dormHygieneCheck:add") + @Log(title = "宿舍卫生检查", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping("/saveCompleteData") + public R saveCompleteData(@Validated(AddGroup.class) @RequestBody XgDormHygieneCheckCompleteBo bo) { + Long hygieneCheckId = xgDormHygieneCheckService.saveCompleteData(bo); + if (hygieneCheckId != null) { + return R.ok(hygieneCheckId); + } else { + return R.fail("保存宿舍卫生检查数据失败"); + } + } + + /** + * 更新完整的宿舍卫生检查数据(包含学生评分和违纪记录) + */ + @SaCheckPermission("dormHygieneCheck:dormHygieneCheck:edit") + @Log(title = "宿舍卫生检查", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping("/updateCompleteData") + public R updateCompleteData(@Validated(EditGroup.class) @RequestBody XgDormHygieneCheckCompleteBo bo) { + return toAjax(xgDormHygieneCheckService.updateCompleteData(bo)); + } + + /** + * 根据卫生检查ID查询相关的违纪记录 + * + * @param hygieneCheckId 卫生检查ID + * @return 违纪记录列表 + */ + @SaCheckPermission("dormHygieneCheck:dormHygieneCheck:query") + @GetMapping("/getViolations/{hygieneCheckId}") + public R> getViolations( + @NotNull(message = "卫生检查ID不能为空") @PathVariable Long hygieneCheckId) { + List violations = xgDormHygieneCheckService + .getViolationsByHygieneCheckId(hygieneCheckId); + return R.ok(violations); + } + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRoomController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRoomController.java index a6a8337d..78437195 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRoomController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgDormRoomController.java @@ -1,6 +1,7 @@ package org.dromara.xg.controller; import java.util.List; +import java.util.HashMap; import lombok.RequiredArgsConstructor; import jakarta.servlet.http.HttpServletResponse; @@ -21,6 +22,7 @@ import org.dromara.xg.domain.vo.XgDormRoomVo; import org.dromara.xg.domain.bo.XgDormRoomBo; import org.dromara.xg.service.IXgDormRoomService; import org.dromara.common.mybatis.core.page.TableDataInfo; +import java.util.Map; /** * 宿舍房间信息 @@ -77,6 +79,33 @@ public class XgDormRoomController extends BaseController { ExcelUtil.exportExcel(list, "宿舍房间信息", XgDormRoomVo.class, response); } + /** + * 导出住宿清单(宿舍卡片式布局) + */ + @SaCheckPermission("xgss:dormRoom:export") + @Log(title = "住宿清单", businessType = BusinessType.EXPORT) + @PostMapping("/exportAccommodationList") + public void exportAccommodationList( + @RequestParam(required = false) Long buildingId, + @RequestParam(required = false) String buildingName, + @RequestParam(required = false) String buildingType, + @RequestParam(required = false) String accommodationData, + HttpServletResponse response) { + + // 构建参数Map + Map params = new HashMap<>(); + if (buildingId != null) + params.put("buildingId", buildingId); + if (buildingName != null) + params.put("buildingName", buildingName); + if (buildingType != null) + params.put("buildingType", buildingType); + if (accommodationData != null) + params.put("accommodationData", accommodationData); + + xgDormRoomService.exportAccommodationList(params, response); + } + /** * 获取宿舍房间信息详细信息 * diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgHolidayStayController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgHolidayStayController.java index cf1e6914..f3851c56 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgHolidayStayController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgHolidayStayController.java @@ -111,4 +111,15 @@ public class XgHolidayStayController extends BaseController { public TableDataInfo getStudentOptions(StudentOptionBo bo, PageQuery pageQuery) { return xgHolidayStayService.getStudentOptions(bo, pageQuery); } + + /** + * 查询单人留宿有效数据 + * 获取所有有效的假期留宿数据,筛选出在同一时间段内该宿舍只有一个学生留宿的记录 + */ + @SaCheckPermission("holidayStay:holidayStay:list") + @GetMapping("/singlePersonValidStay") + public R> getSinglePersonValidStay() { + List result = xgHolidayStayService.querySinglePersonValidStay(); + return R.ok(result); + } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentLeaveController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentLeaveController.java index e2d446f2..e453a992 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentLeaveController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentLeaveController.java @@ -1,6 +1,8 @@ package org.dromara.xg.controller; +import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import jakarta.servlet.http.HttpServletResponse; @@ -101,4 +103,30 @@ public class XgStudentLeaveController extends BaseController { public R remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) { return toAjax(xgStudentLeaveService.deleteWithValidByIds(List.of(ids), true)); } + + /** + * 根据学生ID列表和检查日期查询学生请假信息 + * + * @param studentIds 学生ID列表(逗号分隔) + * @param checkDate 检查日期 + */ + @SaCheckPermission("studentLeave:studentLeave:query") + @GetMapping("/queryByCheckDate") + public R> queryByCheckDate( + @RequestParam String studentIds, + @RequestParam String checkDate) { + try { + List studentIdList = List.of(studentIds.split(",")) + .stream() + .map(String::trim) + .map(Long::valueOf) + .toList(); + LocalDateTime checkDateTime = LocalDateTime.parse(checkDate.replace(" ", "T")); + Map result = xgStudentLeaveService.queryStudentLeavesByCheckDate(studentIdList, + checkDateTime); + return R.ok(result); + } catch (Exception e) { + return R.fail("参数格式错误:" + e.getMessage()); + } + } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentStatisticsController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentStatisticsController.java new file mode 100644 index 00000000..a6de5c51 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentStatisticsController.java @@ -0,0 +1,82 @@ +package org.dromara.xg.controller; + +import java.util.List; + +import lombok.RequiredArgsConstructor; +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.log.annotation.Log; +import org.dromara.common.web.core.BaseController; +import org.dromara.common.core.domain.R; +import org.dromara.common.log.enums.BusinessType; +import org.dromara.xg.domain.bo.XgStudentStatisticsBo; +import org.dromara.xg.domain.vo.XgStudentStatisticsVo; +import org.dromara.xg.domain.vo.XgCascadeOptionVo; +import org.dromara.xg.service.IXgStudentStatisticsService; + +/** + * 学生宿舍卫生检查、违纪记录统计 + * + * @author zlwu + * @date 2025-01-14 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/xg/studentStatistics") +public class XgStudentStatisticsController extends BaseController { + + private final IXgStudentStatisticsService studentStatisticsService; + + /** + * 查询学生宿舍卫生检查、违纪记录统计数据 + */ + @SaCheckPermission("xg:studentStatistics:query") + @PostMapping("/statistics") + public R getStatistics(@Validated @RequestBody XgStudentStatisticsBo bo) { + return R.ok(studentStatisticsService.getStatistics(bo)); + } + + /** + * 导出学生宿舍卫生检查、违纪记录统计数据 + */ + @SaCheckPermission("xg:studentStatistics:export") + @Log(title = "学生统计数据", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void exportStatistics(@Validated @RequestBody XgStudentStatisticsBo bo, + jakarta.servlet.http.HttpServletResponse response) { + studentStatisticsService.exportStatistics(bo, response); + } + + /** + * 获取二级学院列表 + */ + @SaCheckPermission("xg:studentStatistics:query") + @GetMapping("/colleges") + public R> getColleges() { + return R.ok(studentStatisticsService.getColleges()); + } + + /** + * 根据学院ID获取专业列表 + */ + @SaCheckPermission("xg:studentStatistics:query") + @GetMapping("/majors/{collegeId}") + public R> getMajorsByCollegeId( + @NotNull(message = "学院ID不能为空") @PathVariable Long collegeId) { + return R.ok(studentStatisticsService.getMajorsByCollegeId(collegeId)); + } + + /** + * 根据专业ID获取班级列表 + */ + @SaCheckPermission("xg:studentStatistics:query") + @GetMapping("/classes/{majorId}") + public R> getClassesByMajorId( + @NotNull(message = "专业ID不能为空") @PathVariable Long majorId) { + return R.ok(studentStatisticsService.getClassesByMajorId(majorId)); + } + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormHygieneCheck.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormHygieneCheck.java index 87fa3502..c183d0dd 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormHygieneCheck.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormHygieneCheck.java @@ -111,7 +111,7 @@ public class XgDormHygieneCheck extends TenantEntity { private String campusName; /** - * 宿舍总分(0-100分) + * 宿舍扣分(0-100分) */ private Double totalScore; @@ -140,4 +140,14 @@ public class XgDormHygieneCheck extends TenantEntity { */ private String checkRemarks; + /** + * 违纪类型ID,关联xskh_discipline_type.id + */ + private Long disciplineTypeId; + + /** + * 违纪类型名称 + */ + private String disciplineTypeName; + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormHygieneStudentScore.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormHygieneStudentScore.java index 4bfc2c15..57c02d0d 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormHygieneStudentScore.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormHygieneStudentScore.java @@ -94,7 +94,7 @@ public class XgDormHygieneStudentScore extends TenantEntity { private String bedCode; /** - * 学生个人得分(0-100分) + * 学生个人扣分(0-100分) */ private Double studentScore; diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormRoom.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormRoom.java index 1346dc08..4dd7000c 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormRoom.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgDormRoom.java @@ -67,6 +67,11 @@ public class XgDormRoom extends TenantEntity { */ private String roomStatus; + /** + * 排序号 + */ + private Long sort; + /** * 删除标志(0代表存在 1代表删除) */ diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgHygieneCheckViolation.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgHygieneCheckViolation.java new file mode 100644 index 00000000..0a7d8d39 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/XgHygieneCheckViolation.java @@ -0,0 +1,83 @@ +package org.dromara.xg.domain; + +import org.dromara.common.tenant.core.TenantEntity; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.math.BigDecimal; + +import java.io.Serial; + +/** + * 宿舍卫生检查违纪关联对象 xg_hygiene_check_violation + * + * @author 吴自龙 + * @date 2025-01-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("xg_hygiene_check_violation") +public class XgHygieneCheckViolation extends TenantEntity { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id") + private Long id; + + /** + * 宿舍卫生检查ID,关联xg_dorm_hygiene_check.id + */ + private Long hygieneCheckId; + + /** + * 违纪记录ID,关联xskh_discipline_record.id + */ + private Long disciplineRecordId; + + /** + * 学生ID,关联xg_student_msg.id + */ + private Long studentId; + + /** + * 学号 + */ + private String studentNo; + + /** + * 学生姓名 + */ + private String studentName; + + /** + * 违纪类型ID,关联xskh_discipline_type.id + */ + private Long disciplineTypeId; + + /** + * 违纪类型名称 + */ + private String disciplineTypeName; + + /** + * 违纪来源(1宿舍卫生检查 2其他) + */ + private String violationSource; + + /** + * 状态(1生效 2已撤销) + */ + private String status; + + /** + * 删除标志(0代表存在 1代表删除) + */ + @TableLogic(value = "0", delval = "1") + private Long deleted; + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormBuildingBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormBuildingBo.java index 499f6725..503e3944 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormBuildingBo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormBuildingBo.java @@ -41,7 +41,8 @@ public class XgDormBuildingBo extends BaseEntity { /** * 所属宿舍区ID */ - @NotNull(message = "所属宿舍区ID不能为空", groups = { AddGroup.class, EditGroup.class }) + // @NotNull(message = "所属宿舍区ID不能为空", groups = { AddGroup.class, EditGroup.class + // }) // 宿舍区轻量化改造:移除必填验证 private Long areaId; /** diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckBo.java index ca9e945d..5912f3f9 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckBo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckBo.java @@ -147,5 +147,14 @@ public class XgDormHygieneCheckBo extends BaseEntity { */ private String checkRemarks; + /** + * 违纪类型ID,关联xskh_discipline_type.id + */ + private Long disciplineTypeId; + + /** + * 违纪类型名称 + */ + private String disciplineTypeName; } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckCompleteBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckCompleteBo.java index 3a84a6a1..18d2202f 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckCompleteBo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormHygieneCheckCompleteBo.java @@ -149,6 +149,16 @@ public class XgDormHygieneCheckCompleteBo { */ private String checkRemarks; + /** + * 违纪类型ID,关联xskh_discipline_type.id + */ + private Long disciplineTypeId; + + /** + * 违纪类型名称 + */ + private String disciplineTypeName; + /** * 学生评分数据列表 */ @@ -156,6 +166,12 @@ public class XgDormHygieneCheckCompleteBo { @NotEmpty(message = "学生评分数据不能为空", groups = { AddGroup.class, EditGroup.class }) private List studentScores; + /** + * 违纪记录数据列表 + */ + @Valid + private List violationData; + // 基础字段(用于VO类映射) private Long createBy; @@ -166,4 +182,4 @@ public class XgDormHygieneCheckCompleteBo { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updateTime; -} \ No newline at end of file +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormRoomBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormRoomBo.java index c870cba6..a0eedb5d 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormRoomBo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgDormRoomBo.java @@ -70,6 +70,11 @@ public class XgDormRoomBo extends BaseEntity { */ private String roomStatus; + /** + * 排序号 + */ + private Long sort; + /** * 删除标志(0代表存在 1代表删除) */ @@ -80,5 +85,4 @@ public class XgDormRoomBo extends BaseEntity { */ private String remark; - } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgHygieneCheckViolationBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgHygieneCheckViolationBo.java new file mode 100644 index 00000000..9c466d26 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgHygieneCheckViolationBo.java @@ -0,0 +1,106 @@ +package org.dromara.xg.domain.bo; + +import org.dromara.xg.domain.XgHygieneCheckViolation; +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; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * 宿舍卫生检查违纪关联业务对象 xg_hygiene_check_violation + * + * @author 吴自龙 + * @date 2025-01-03 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = XgHygieneCheckViolation.class, reverseConvertGenerate = false) +public class XgHygieneCheckViolationBo extends BaseEntity { + + /** + * 主键ID + */ + private Long id; + + /** + * 宿舍卫生检查ID,关联xg_dorm_hygiene_check.id + */ + private Long hygieneCheckId; + + /** + * 违纪记录ID,关联xskh_discipline_record.id + */ + private Long disciplineRecordId; + + /** + * 学生ID,关联xg_student_msg.id + */ + @NotNull(message = "学生ID不能为空") + private Long studentId; + + /** + * 学号 + */ + @NotBlank(message = "学号不能为空") + private String studentNo; + + /** + * 学生姓名 + */ + @NotBlank(message = "学生姓名不能为空") + private String studentName; + + /** + * 违纪类型ID,关联xskh_discipline_type.id + */ + @NotNull(message = "违纪类型ID不能为空") + private Long disciplineTypeId; + + /** + * 违纪类型名称 + */ + @NotBlank(message = "违纪类型名称不能为空") + private String disciplineTypeName; + + /** + * 违纪来源(1宿舍卫生检查 2其他) + */ + private String violationSource; + + /** + * 状态(1生效 2已撤销) + */ + private String status; + + /** + * 分页大小 + */ + @JsonIgnore + private Integer pageSize; + + /** + * 当前页数 + */ + @JsonIgnore + private Integer pageNum; + + /** + * 排序列 + */ + @JsonIgnore + private String orderByColumn; + + /** + * 排序的方向desc或者asc + */ + @JsonIgnore + private String isAsc; + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgStudentStatisticsBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgStudentStatisticsBo.java new file mode 100644 index 00000000..8b6d3f91 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgStudentStatisticsBo.java @@ -0,0 +1,48 @@ +package org.dromara.xg.domain.bo; + +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import jakarta.validation.constraints.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.time.LocalDateTime; + +/** + * 学生宿舍卫生检查、违纪记录统计查询业务对象 + * + * @author zlwu + * @date 2025-01-14 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class XgStudentStatisticsBo extends BaseEntity { + + /** + * 统计开始时间 + */ + @NotNull(message = "统计开始时间不能为空") + private LocalDateTime startTime; + + /** + * 统计结束时间 + */ + @NotNull(message = "统计结束时间不能为空") + private LocalDateTime endTime; + + /** + * 二级学院ID + */ + private Long collegeId; + + /** + * 专业ID + */ + private Long majorId; + + /** + * 班级ID + */ + private Long classId; + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgViolationDataBo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgViolationDataBo.java new file mode 100644 index 00000000..7ab4228b --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgViolationDataBo.java @@ -0,0 +1,125 @@ +package org.dromara.xg.domain.bo; + +import lombok.Data; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * 违纪数据业务对象(用于前端传输) + * + * @author 吴自龙 + * @date 2025-01-03 + */ +@Data +public class XgViolationDataBo { + + /** + * 违纪记录ID(用于更新操作,新增时为null) + */ + private Long id; + + /** + * 违纪记录ID,关联xskh_discipline_record.id(用于更新违纪记录) + */ + private Long disciplineRecordId; + + /** + * 学生ID,关联xg_student_msg.id + */ + @NotNull(message = "学生ID不能为空") + private Long studentId; + + /** + * 学号 + */ + @NotBlank(message = "学号不能为空") + private String studentNo; + + /** + * 学生姓名 + */ + @NotBlank(message = "学生姓名不能为空") + private String studentName; + + /** + * 班级ID + */ + private Long classId; + + /** + * 班级名称 + */ + private String className; + + /** + * 专业ID + */ + private Long majorId; + + /** + * 专业名称 + */ + private String majorName; + + /** + * 学院ID + */ + private Long collegeId; + + /** + * 学院名称 + */ + private String collegeName; + + /** + * 违纪类型ID,关联xskh_discipline_type.id + */ + @NotNull(message = "违纪类型ID不能为空") + private Long disciplineTypeId; + + /** + * 违纪类型名称 + */ + @NotBlank(message = "违纪类型名称不能为空") + private String disciplineTypeName; + + /** + * 违纪日期 + */ + private String disciplineDate; + + /** + * 违纪原因 + */ + @NotBlank(message = "违纪原因不能为空") + private String disciplineReason; + + /** + * 违纪内容 + */ + private String disciplineContent; + + /** + * 扣分分值 + */ + private BigDecimal scoreDeduction; + + /** + * 违纪来源(1宿舍卫生检查 2其他) + */ + private String violationSource; + + /** + * 状态(1生效 2已撤销) + */ + private String status; + + /** + * 附件OSS ID列表,逗号分隔 + */ + private String attachmentOssIds; +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormBuildingTreeSimplifiedVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormBuildingTreeSimplifiedVo.java new file mode 100644 index 00000000..8d9ac7f4 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/DormBuildingTreeSimplifiedVo.java @@ -0,0 +1,104 @@ +package org.dromara.xg.domain.vo; + +import java.io.Serializable; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +/** + * 宿舍楼树形结构简化VO + * 用于简化界面显示,只显示校区→宿舍楼的二级结构,隐藏宿舍区层级 + * + * @author wuzilong + * @date 2025-01-03 + */ +@Data +public class DormBuildingTreeSimplifiedVo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 节点ID + */ + private Long id; + + /** + * 节点名称 + */ + private String label; + + /** + * 节点类型 (campus:校区, building:宿舍楼) + * 注意:简化版本中移除了 area:宿舍区 类型 + */ + private String type; + + /** + * 是否禁用节点(校区节点禁用点击,宿舍楼节点可点击) + */ + private Boolean disabled; + + /** + * 宿舍楼层数(仅宿舍楼节点有此字段) + */ + private Integer buildingFloors; + + /** + * 宿舍楼类型(1男生楼 2女生楼 3混合楼)(仅宿舍楼节点有此字段) + */ + private String buildingType; + + /** + * 子节点 + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List children; + + /** + * 从完整树形VO转换为简化树形VO + * 将三级结构(校区→宿舍区→宿舍楼)转换为二级结构(校区→宿舍楼) + */ + public static DormBuildingTreeSimplifiedVo fromFullTreeVo(DormBuildingTreeVo fullTreeVo) { + if (fullTreeVo == null) { + return null; + } + + DormBuildingTreeSimplifiedVo simplified = new DormBuildingTreeSimplifiedVo(); + simplified.setId(fullTreeVo.getId()); + simplified.setLabel(fullTreeVo.getLabel()); + simplified.setBuildingFloors(fullTreeVo.getBuildingFloors()); + simplified.setBuildingType(fullTreeVo.getBuildingType()); + + // 根据节点类型进行转换 + if ("campus".equals(fullTreeVo.getType())) { + // 校区节点:保持不变 + simplified.setType("campus"); + simplified.setDisabled(true); + // 处理子节点:跳过宿舍区层级,直接提取宿舍楼 + simplified.setChildren(flattenAreaChildren(fullTreeVo.getChildren())); + } else if ("building".equals(fullTreeVo.getType())) { + // 宿舍楼节点:保持不变 + simplified.setType("building"); + simplified.setDisabled(false); + } + + return simplified; + } + + /** + * 扁平化宿舍区子节点,将宿舍区下的宿舍楼直接提升为校区的子节点 + */ + private static List flattenAreaChildren(List areaNodes) { + if (areaNodes == null || areaNodes.isEmpty()) { + return null; + } + + return areaNodes.stream() + .filter(node -> "area".equals(node.getType())) // 找到宿舍区节点 + .flatMap(areaNode -> areaNode.getChildren() != null ? areaNode.getChildren().stream() + : java.util.stream.Stream.empty()) // 提取宿舍楼 + .filter(buildingNode -> "building".equals(buildingNode.getType())) // 确保是宿舍楼节点 + .map(DormBuildingTreeSimplifiedVo::fromFullTreeVo) // 转换为简化VO + .toList(); + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgCascadeOptionVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgCascadeOptionVo.java new file mode 100644 index 00000000..9752cdd4 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgCascadeOptionVo.java @@ -0,0 +1,34 @@ +package org.dromara.xg.domain.vo; + +import lombok.Data; + +/** + * 级联选择器选项视图对象 + * + * @author zlwu + * @date 2025-01-14 + */ +@Data +public class XgCascadeOptionVo { + + /** + * ID + */ + private Long id; + + /** + * 显示名称 + */ + private String label; + + /** + * 值 + */ + private String value; + + /** + * 排序 + */ + private Integer sort; + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormBuildingSimplifiedVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormBuildingSimplifiedVo.java new file mode 100644 index 00000000..dccc0607 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormBuildingSimplifiedVo.java @@ -0,0 +1,103 @@ +package org.dromara.xg.domain.vo; + +import org.dromara.xg.domain.XgDormBuilding; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import org.dromara.common.excel.annotation.ExcelDictFormat; +import org.dromara.common.excel.convert.ExcelDictConvert; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.springframework.beans.BeanUtils; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 宿舍楼信息简化视图对象 xg_dorm_building + * 用于简化界面显示,隐藏宿舍区层级,直接显示校区→宿舍楼结构 + * + * @author zlwu + * @date 2025-01-03 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = XgDormBuilding.class) +public class XgDormBuildingSimplifiedVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @ExcelProperty(value = "主键ID") + private Long id; + + /** + * 宿舍楼编码 + */ + @ExcelProperty(value = "宿舍楼编码") + private String buildingCode; + + /** + * 宿舍楼名称 + */ + @ExcelProperty(value = "宿舍楼名称") + private String buildingName; + + /** + * 所属校区ID + */ + @ExcelProperty(value = "所属校区ID") + private Long campusId; + + /** + * 校区名称 + */ + @ExcelProperty(value = "校区名称") + private String campusName; + + /** + * 宿舍楼类型(1男生楼 2女生楼 3混合楼) + */ + @ExcelProperty(value = "宿舍楼类型(1男生楼 2女生楼 3混合楼)", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "dorm_type") + private String buildingType; + + /** + * 宿舍楼描述 + */ + @ExcelProperty(value = "宿舍楼描述") + private String buildingDescription; + + /** + * 排序号 + */ + @ExcelProperty(value = "排序号") + private Long sortOrder; + + /** + * 总楼层数 + */ + @ExcelProperty(value = "总楼层数") + private Integer floors; + + // 基础字段 + private Long createBy; // 创建者 + private Date createTime; // 创建时间 + private Long updateBy; // 更新者 + private Date updateTime; // 更新时间 + + /** + * 从完整VO转换为简化VO + */ + public static XgDormBuildingSimplifiedVo fromFullVo(XgDormBuildingVo fullVo) { + if (fullVo == null) { + return null; + } + XgDormBuildingSimplifiedVo simplified = new XgDormBuildingSimplifiedVo(); + BeanUtils.copyProperties(fullVo, simplified); + return simplified; + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckSimplifiedVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckSimplifiedVo.java new file mode 100644 index 00000000..51b8d632 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckSimplifiedVo.java @@ -0,0 +1,183 @@ +package org.dromara.xg.domain.vo; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.dromara.xg.domain.XgDormHygieneCheck; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import org.dromara.common.excel.annotation.ExcelDictFormat; +import org.dromara.common.excel.convert.ExcelDictConvert; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.springframework.beans.BeanUtils; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 宿舍卫生检查简化视图对象 xg_dorm_hygiene_check + * 用于简化界面显示,隐藏宿舍区层级,直接显示校区→宿舍楼→房间结构 + * + * @author zlwu + * @date 2025-01-03 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = XgDormHygieneCheck.class) +public class XgDormHygieneCheckSimplifiedVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @ExcelProperty(value = "主键ID") + private Long id; + + /** + * 删除标志(0代表存在 1代表删除) + */ + @ExcelProperty(value = "删除标志", converter = ExcelDictConvert.class) + @ExcelDictFormat(readConverterExp = "0=代表存在,1=代表删除") + private Long deleted; + + /** + * 检查标题 + */ + @ExcelProperty(value = "检查标题") + private String checkTitle; + + /** + * 检查日期 + */ + @ExcelProperty(value = "检查日期") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date checkDate; + + /** + * 检查员ID,关联sys_user.id + */ + @ExcelProperty(value = "检查员ID,关联sys_user.id") + private Long checkerId; + + /** + * 检查员姓名 + */ + @ExcelProperty(value = "检查员姓名") + private String checkerName; + + /** + * 检查类型(1日常检查 2专项检查 3突击检查 4复查) + */ + @ExcelProperty(value = "检查类型", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "xg_dorm_hygiene_check_type") + private String checkType; + + /** + * 检查级别(1宿管员 2学院 3学校) + */ + @ExcelProperty(value = "检查级别", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "xg_dorm_hygiene_check_level") + private String checkLevel; + + /** + * 宿舍房间ID,关联xg_dorm_room.id + */ + @ExcelProperty(value = "宿舍房间ID,关联xg_dorm_room.id") + private Long roomId; + + /** + * 房间编号 + */ + @ExcelProperty(value = "房间编号") + private String roomCode; + + /** + * 房间名称 + */ + @ExcelProperty(value = "房间名称") + private String roomName; + + /** + * 宿舍楼ID,关联xg_dorm_building.id + */ + @ExcelProperty(value = "宿舍楼ID,关联xg_dorm_building.id") + private Long buildingId; + + /** + * 宿舍楼名称 + */ + @ExcelProperty(value = "宿舍楼名称") + private String buildingName; + + /** + * 校区ID,关联xg_dorm_campus.id + */ + @ExcelProperty(value = "校区ID,关联xg_dorm_campus.id") + private Long campusId; + + /** + * 校区名称 + */ + @ExcelProperty(value = "校区名称") + private String campusName; + + /** + * 宿舍总分(0-100分) + */ + @ExcelProperty(value = "宿舍总分", converter = ExcelDictConvert.class) + @ExcelDictFormat(readConverterExp = "0=-100分") + private Double totalScore; + + /** + * 卫生等级(A优秀 B良好 C合格 D不合格) + */ + @ExcelProperty(value = "卫生等级", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "xg_dorm_hygiene_grade") + private String hygieneGrade; + + /** + * 附件OSS ID列表,逗号分隔 + */ + @ExcelProperty(value = "附件OSS ID列表,逗号分隔") + private String attachmentIds; + + /** + * 检查总结 + */ + @ExcelProperty(value = "检查总结") + private String checkSummary; + + /** + * 整改建议 + */ + @ExcelProperty(value = "整改建议") + private String improvementSuggestions; + + /** + * 检查备注 + */ + @ExcelProperty(value = "检查备注") + private String checkRemarks; + + // 基础字段 + 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; // 更新时间 + + /** + * 从完整VO转换为简化VO + */ + public static XgDormHygieneCheckSimplifiedVo fromFullVo(XgDormHygieneCheckVo fullVo) { + if (fullVo == null) { + return null; + } + XgDormHygieneCheckSimplifiedVo simplified = new XgDormHygieneCheckSimplifiedVo(); + BeanUtils.copyProperties(fullVo, simplified); + return simplified; + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckVo.java index 43ba5278..bd50c81c 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckVo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormHygieneCheckVo.java @@ -14,8 +14,6 @@ import java.io.Serial; import java.io.Serializable; import java.util.Date; - - /** * 宿舍卫生检查视图对象 xg_dorm_hygiene_check * @@ -173,5 +171,16 @@ public class XgDormHygieneCheckVo implements Serializable { @ExcelProperty(value = "检查备注") private String checkRemarks; + /** + * 违纪类型ID,关联xskh_discipline_type.id + */ + @ExcelProperty(value = "违纪类型ID") + private Long disciplineTypeId; + + /** + * 违纪类型名称 + */ + @ExcelProperty(value = "违纪类型名称") + private String disciplineTypeName; } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormRoomVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormRoomVo.java index aae4aabc..f71cd261 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormRoomVo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgDormRoomVo.java @@ -11,8 +11,7 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; import java.util.Date; - - +import com.fasterxml.jackson.annotation.JsonFormat; /** * 宿舍房间信息视图对象 xg_dorm_room @@ -82,6 +81,12 @@ public class XgDormRoomVo implements Serializable { @ExcelProperty(value = "房间状态(0可用 1维修中 2停用)") private String roomStatus; + /** + * 排序号 + */ + @ExcelProperty(value = "排序号") + private Long sort; + /** * 删除标志(0代表存在 1代表删除) */ @@ -94,5 +99,26 @@ public class XgDormRoomVo implements Serializable { @ExcelProperty(value = "备注") private String remark; + /** + * 创建者 + */ + 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; } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgHolidayStayVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgHolidayStayVo.java index c7887d40..c6bc2dc6 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgHolidayStayVo.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgHolidayStayVo.java @@ -14,8 +14,6 @@ import java.io.Serial; import java.io.Serializable; import java.util.Date; - - /** * 假期留宿管理视图对象 xg_holiday_stay * @@ -39,8 +37,6 @@ public class XgHolidayStayVo implements Serializable { /** * 删除标志(0代表存在 1代表删除) */ - @ExcelProperty(value = "删除标志", converter = ExcelDictConvert.class) - @ExcelDictFormat(readConverterExp = "0=代表存在,1=代表删除") private Long deleted; /** @@ -179,8 +175,7 @@ public class XgHolidayStayVo implements Serializable { /** * 留宿原因(必填) */ - @ExcelProperty(value = "留宿原因", converter = ExcelDictConvert.class) - @ExcelDictFormat(readConverterExp = "必=填") + @ExcelProperty(value = "留宿原因") private String stayReason; /** @@ -218,26 +213,21 @@ public class XgHolidayStayVo implements Serializable { /** * 附件OSS文件ID,多个用逗号分隔(申请材料、证明文件等) */ - @ExcelProperty(value = "附件OSS文件ID,多个用逗号分隔", converter = ExcelDictConvert.class) - @ExcelDictFormat(readConverterExp = "申=请材料、证明文件等") private String attachmentOssIds; /** * 扩展字段1 */ - @ExcelProperty(value = "扩展字段1") private String extField1; /** * 扩展字段2 */ - @ExcelProperty(value = "扩展字段2") private String extField2; /** * 扩展字段3 */ - @ExcelProperty(value = "扩展字段3") private String extField3; /** @@ -246,5 +236,4 @@ public class XgHolidayStayVo implements Serializable { @ExcelProperty(value = "备注信息") private String remark; - } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgHygieneCheckViolationVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgHygieneCheckViolationVo.java new file mode 100644 index 00000000..cd6a9dc6 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgHygieneCheckViolationVo.java @@ -0,0 +1,126 @@ +package org.dromara.xg.domain.vo; + +import org.dromara.xg.domain.XgHygieneCheckViolation; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import java.math.BigDecimal; +import java.util.Date; +import java.time.LocalDate; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 宿舍卫生检查违纪关联视图对象 xg_hygiene_check_violation + * + * @author 吴自龙 + * @date 2025-01-03 + */ +@Data +@AutoMapper(target = XgHygieneCheckViolation.class) +public class XgHygieneCheckViolationVo { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + private Long id; + + /** + * 宿舍卫生检查ID + */ + private Long hygieneCheckId; + + /** + * 违纪记录ID + */ + private Long disciplineRecordId; + + /** + * 学生ID + */ + private Long studentId; + + /** + * 学号 + */ + private String studentNo; + + /** + * 学生姓名 + */ + private String studentName; + + /** + * 违纪类型ID + */ + private Long disciplineTypeId; + + /** + * 违纪类型名称 + */ + private String disciplineTypeName; + + /** + * 违纪来源 + */ + private String violationSource; + + /** + * 状态 + */ + private String status; + + // ============ 新增违纪详情字段 ============ + + /** + * 违纪处分日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate disciplineDate; + + /** + * 违纪处分原因 + */ + private String disciplineReason; + + /** + * 违纪处分内容 + */ + private String disciplineContent; + + /** + * 扣分分值 + */ + private BigDecimal scoreDeduction; + + /** + * 附件OSS ID列表,逗号分隔 + */ + private String attachmentOssIds; + + /** + * 创建者 + */ + 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; + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgStudentStatisticsVo.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgStudentStatisticsVo.java new file mode 100644 index 00000000..a8a30042 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/vo/XgStudentStatisticsVo.java @@ -0,0 +1,126 @@ +package org.dromara.xg.domain.vo; + +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; +import java.util.List; + +/** + * 学生宿舍卫生检查、违纪记录统计视图对象 + * + * @author zlwu + * @date 2025-01-14 + */ +@Data +public class XgStudentStatisticsVo { + + /** + * 统计维度 (college-学院, major-专业, class-班级, classDetail-班级详情) + */ + private String dimension; + + /** + * 统计图表数据 + */ + private List chartData; + + /** + * 明细数据 + */ + private List detailData; + + /** + * 图表数据内部类 + */ + @Data + public static class StatisticsChartData { + /** + * 名称 (学院名、专业名、班级名) + */ + private String name; + + /** + * 宿舍扣分总数 + */ + private Integer dormDeductionCount; + + /** + * 宿舍违纪记录总数 + */ + private Integer dormViolationCount; + + /** + * 宿舍违纪记录扣分总数 + */ + private Double dormViolationDeduction; + + /** + * 教学区违纪记录总数 + */ + private Integer teachingViolationCount; + + /** + * 教学区违纪记录扣分总数 + */ + private Double teachingViolationDeduction; + } + + /** + * 明细数据内部类 + */ + @Data + public static class StatisticsDetailData { + /** + * ID (不导出到Excel) + */ + private Long id; + + /** + * 名称 + */ + @ExcelProperty(value = "名称") + private String name; + + /** + * 上级名称(如学院下的专业名称显示学院名) + */ + @ExcelProperty(value = "上级") + private String parentName; + + /** + * 宿舍扣分总数 + */ + @ExcelProperty(value = "宿舍扣分总数") + private Integer dormDeductionCount; + + /** + * 宿舍违纪记录总数 + */ + @ExcelProperty(value = "宿舍违纪记录总数") + private Integer dormViolationCount; + + /** + * 宿舍违纪记录扣分总数 + */ + @ExcelProperty(value = "宿舍违纪记录扣分总数") + private Double dormViolationDeduction; + + /** + * 教学区违纪记录总数 + */ + @ExcelProperty(value = "教学区违纪记录总数") + private Integer teachingViolationCount; + + /** + * 教学区违纪记录扣分总数 + */ + @ExcelProperty(value = "教学区违纪记录扣分总数") + private Double teachingViolationDeduction; + + /** + * 合计扣分 + */ + @ExcelProperty(value = "违纪合计扣分") + private Double totalDeduction; + } + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgHygieneCheckViolationMapper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgHygieneCheckViolationMapper.java new file mode 100644 index 00000000..8c07c95a --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgHygieneCheckViolationMapper.java @@ -0,0 +1,26 @@ +package org.dromara.xg.mapper; + +import org.dromara.xg.domain.XgHygieneCheckViolation; +import org.dromara.xg.domain.vo.XgHygieneCheckViolationVo; +import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +/** + * 宿舍卫生检查违纪记录Mapper接口 + * + * @author zlwu + * @date 2025-01-29 + */ +public interface XgHygieneCheckViolationMapper + extends BaseMapperPlus { + + /** + * 根据卫生检查ID查询违纪记录(包含详情) + * + * @param hygieneCheckId 卫生检查ID + * @return 违纪记录列表(包含详情) + */ + List selectViolationsByHygieneCheckId(@Param("hygieneCheckId") Long hygieneCheckId); + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentStatisticsMapper.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentStatisticsMapper.java new file mode 100644 index 00000000..d9ad2a16 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentStatisticsMapper.java @@ -0,0 +1,63 @@ +package org.dromara.xg.mapper; + +import org.apache.ibatis.annotations.Param; +import org.dromara.xg.domain.bo.XgStudentStatisticsBo; +import org.dromara.xg.domain.vo.XgStudentStatisticsVo; +import org.dromara.xg.domain.vo.XgCascadeOptionVo; + +import java.util.List; + +/** + * 学生宿舍卫生检查、违纪记录统计Mapper接口 + * + * @author zlwu + * @date 2025-01-14 + */ +public interface XgStudentStatisticsMapper { + + /** + * 按学院维度统计图表数据 + */ + List getCollegeStatistics(@Param("bo") XgStudentStatisticsBo bo); + + /** + * 按学院维度统计明细数据 + */ + List getCollegeDetailStatistics(@Param("bo") XgStudentStatisticsBo bo); + + /** + * 按专业维度统计图表数据 + */ + List getMajorStatistics(@Param("bo") XgStudentStatisticsBo bo); + + /** + * 按专业维度统计明细数据 + */ + List getMajorDetailStatistics(@Param("bo") XgStudentStatisticsBo bo); + + /** + * 按班级维度统计图表数据 + */ + List getClassStatistics(@Param("bo") XgStudentStatisticsBo bo); + + /** + * 按班级维度统计明细数据 + */ + List getClassDetailStatistics(@Param("bo") XgStudentStatisticsBo bo); + + /** + * 获取二级学院列表 + */ + List getColleges(); + + /** + * 根据学院ID获取专业列表 + */ + List getMajorsByCollegeId(@Param("collegeId") Long collegeId); + + /** + * 根据专业ID获取班级列表 + */ + List getClassesByMajorId(@Param("majorId") Long majorId); + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBedStudentService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBedStudentService.java index bc972308..92c7523c 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBedStudentService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBedStudentService.java @@ -107,7 +107,7 @@ public interface IXgDormBedStudentService { /** * 获取当前用户二级学院的宿舍楼树形结构 - * 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择 + * 校区-宿舍楼二级结构,校区不可选,只有宿舍楼可以选择 * * @return 宿舍楼树形结构数据 */ diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBuildingService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBuildingService.java index d15bdb61..d1203a18 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBuildingService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBuildingService.java @@ -1,6 +1,7 @@ package org.dromara.xg.service; import org.dromara.xg.domain.vo.XgDormBuildingVo; +import org.dromara.xg.domain.vo.XgDormBuildingSimplifiedVo; import org.dromara.xg.domain.bo.XgDormBuildingBo; import org.dromara.xg.domain.vo.BuildingTreeSelectVo; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -74,4 +75,23 @@ public interface IXgDormBuildingService { * @return 树形结构数据 */ List getBuildingTreeData(); + + /** + * 分页查询宿舍楼信息简化列表 + * 用于简化界面显示,隐藏宿舍区层级,直接显示校区→宿舍楼结构 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 宿舍楼信息简化分页列表 + */ + TableDataInfo querySimplifiedPageList(XgDormBuildingBo bo, PageQuery pageQuery); + + /** + * 查询符合条件的宿舍楼信息简化列表 + * 用于简化界面显示,隐藏宿舍区层级,直接显示校区→宿舍楼结构 + * + * @param bo 查询条件 + * @return 宿舍楼信息简化列表 + */ + List querySimplifiedList(XgDormBuildingBo bo); } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBuildingTreeService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBuildingTreeService.java index 8aea57c5..8f7c86ce 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBuildingTreeService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormBuildingTreeService.java @@ -2,6 +2,7 @@ package org.dromara.xg.service; import java.util.List; import org.dromara.xg.domain.vo.DormBuildingTreeVo; +import org.dromara.xg.domain.vo.DormBuildingTreeSimplifiedVo; import org.dromara.xg.domain.bo.DormBuildingTreeBo; /** @@ -28,4 +29,22 @@ public interface IXgDormBuildingTreeService { */ List getAllDormBuildingTree(DormBuildingTreeBo bo); + /** + * 获取宿舍楼树形结构简化数据 + * 校区-宿舍楼二级结构,隐藏宿舍区层级 + * + * @param bo 查询参数 + * @return 简化树形结构数据 + */ + List getSimplifiedDormBuildingTree(DormBuildingTreeBo bo); + + /** + * 获取所有宿舍楼树形结构简化数据(不考虑权限) + * 校区-宿舍楼二级结构,隐藏宿舍区层级 + * + * @param bo 查询参数 + * @return 简化树形结构数据 + */ + List getAllSimplifiedDormBuildingTree(DormBuildingTreeBo bo); + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormHygieneCheckService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormHygieneCheckService.java index fc056139..099301cf 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormHygieneCheckService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormHygieneCheckService.java @@ -2,6 +2,7 @@ package org.dromara.xg.service; import org.dromara.xg.domain.vo.DormHygieneCheckExportVo; import org.dromara.xg.domain.vo.XgDormHygieneCheckVo; +import org.dromara.xg.domain.vo.XgHygieneCheckViolationVo; import org.dromara.xg.domain.bo.XgDormHygieneCheckBo; import org.dromara.xg.domain.bo.XgDormHygieneCheckCompleteBo; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -111,4 +112,28 @@ public interface IXgDormHygieneCheckService { TableDataInfo queryPageListByStudentId(Long studentId, XgDormHygieneCheckBo bo, PageQuery pageQuery); + /** + * 保存完整的宿舍卫生检查数据(包含学生评分和违纪记录) + * + * @param bo 完整的宿舍卫生检查数据 + * @return 检查记录ID,保存失败返回null + */ + Long saveCompleteData(XgDormHygieneCheckCompleteBo bo); + + /** + * 更新完整的宿舍卫生检查数据(包含学生评分和违纪记录) + * + * @param bo 完整的宿舍卫生检查数据 + * @return 是否更新成功 + */ + Boolean updateCompleteData(XgDormHygieneCheckCompleteBo bo); + + /** + * 根据卫生检查ID查询相关的违纪记录 + * + * @param hygieneCheckId 卫生检查ID + * @return 违纪记录列表 + */ + List getViolationsByHygieneCheckId(Long hygieneCheckId); + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRoomService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRoomService.java index f5b8cd41..4e64135f 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRoomService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgDormRoomService.java @@ -7,6 +7,8 @@ import org.dromara.common.mybatis.core.page.PageQuery; import java.util.Collection; import java.util.List; +import java.util.Map; +import jakarta.servlet.http.HttpServletResponse; /** * 宿舍房间信息Service接口 @@ -84,4 +86,12 @@ public interface IXgDormRoomService { * @return 是否删除成功 */ Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 导出住宿清单(宿舍卡片式布局) + * + * @param params 导出参数 + * @param response HTTP响应对象 + */ + void exportAccommodationList(Map params, HttpServletResponse response); } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgHolidayStayService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgHolidayStayService.java index 449b5842..90662e6d 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgHolidayStayService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgHolidayStayService.java @@ -77,4 +77,12 @@ public interface IXgHolidayStayService { */ TableDataInfo getStudentOptions(StudentOptionBo bo, PageQuery pageQuery); + /** + * 查询单人留宿有效数据 + * 获取所有有效的假期留宿数据,筛选出在同一时间段内该宿舍只有一个学生留宿的记录 + * + * @return 单人留宿有效数据列表 + */ + List querySinglePersonValidStay(); + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgHygieneCheckViolationService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgHygieneCheckViolationService.java new file mode 100644 index 00000000..ef343bb8 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgHygieneCheckViolationService.java @@ -0,0 +1,93 @@ +package org.dromara.xg.service; + +import org.dromara.xg.domain.vo.XgHygieneCheckViolationVo; +import org.dromara.xg.domain.bo.XgHygieneCheckViolationBo; +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 zlwu + * @date 2025-01-29 + */ +public interface IXgHygieneCheckViolationService { + + /** + * 查询宿舍卫生检查违纪记录 + * + * @param id 主键 + * @return 宿舍卫生检查违纪记录 + */ + XgHygieneCheckViolationVo queryById(Long id); + + /** + * 分页查询宿舍卫生检查违纪记录列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 宿舍卫生检查违纪记录分页列表 + */ + TableDataInfo queryPageList(XgHygieneCheckViolationBo bo, PageQuery pageQuery); + + /** + * 查询符合条件的宿舍卫生检查违纪记录列表 + * + * @param bo 查询条件 + * @return 宿舍卫生检查违纪记录列表 + */ + List queryList(XgHygieneCheckViolationBo bo); + + /** + * 新增宿舍卫生检查违纪记录 + * + * @param bo 宿舍卫生检查违纪记录 + * @return 是否新增成功 + */ + Boolean insertByBo(XgHygieneCheckViolationBo bo); + + /** + * 修改宿舍卫生检查违纪记录 + * + * @param bo 宿舍卫生检查违纪记录 + * @return 是否修改成功 + */ + Boolean updateByBo(XgHygieneCheckViolationBo bo); + + /** + * 校验并批量删除宿舍卫生检查违纪记录信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 批量新增宿舍卫生检查违纪记录 + * + * @param bos 违纪记录列表 + * @return 是否新增成功 + */ + Boolean insertBatch(List bos); + + /** + * 根据卫生检查ID查询违纪记录列表 + * + * @param hygieneCheckId 卫生检查ID + * @return 违纪记录列表 + */ + List queryByHygieneCheckId(Long hygieneCheckId); + + /** + * 根据卫生检查ID删除违纪关联记录 + * + * @param hygieneCheckId 卫生检查ID + * @return 是否删除成功 + */ + Boolean deleteByHygieneCheckId(Long hygieneCheckId); + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentLeaveService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentLeaveService.java index 125b19e2..1c877494 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentLeaveService.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentLeaveService.java @@ -5,8 +5,10 @@ import org.dromara.xg.domain.bo.XgStudentLeaveBo; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.PageQuery; +import java.time.LocalDateTime; import java.util.Collection; import java.util.List; +import java.util.Map; /** * 学生请假 - 记录学生的请假申请、审批和销假信息Service接口 @@ -65,4 +67,13 @@ public interface IXgStudentLeaveService { * @return 是否删除成功 */ Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 根据学生ID列表和检查日期查询学生请假信息 + * + * @param studentIds 学生ID列表 + * @param checkDate 检查日期 + * @return 学生ID -> 请假信息的映射 + */ + Map queryStudentLeavesByCheckDate(List studentIds, LocalDateTime checkDate); } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentStatisticsService.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentStatisticsService.java new file mode 100644 index 00000000..14059e07 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentStatisticsService.java @@ -0,0 +1,56 @@ +package org.dromara.xg.service; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import org.dromara.xg.domain.bo.XgStudentStatisticsBo; +import org.dromara.xg.domain.vo.XgStudentStatisticsVo; +import org.dromara.xg.domain.vo.XgCascadeOptionVo; + +/** + * 学生宿舍卫生检查、违纪记录统计Service接口 + * + * @author zlwu + * @date 2025-01-14 + */ +public interface IXgStudentStatisticsService { + + /** + * 获取统计数据 + * + * @param bo 查询条件 + * @return 统计结果 + */ + XgStudentStatisticsVo getStatistics(XgStudentStatisticsBo bo); + + /** + * 导出统计数据 + * + * @param bo 查询条件 + * @param response HTTP响应 + */ + void exportStatistics(XgStudentStatisticsBo bo, HttpServletResponse response); + + /** + * 获取二级学院列表 + * + * @return 学院列表 + */ + List getColleges(); + + /** + * 根据学院ID获取专业列表 + * + * @param collegeId 学院ID + * @return 专业列表 + */ + List getMajorsByCollegeId(Long collegeId); + + /** + * 根据专业ID获取班级列表 + * + * @param majorId 专业ID + * @return 班级列表 + */ + List getClassesByMajorId(Long majorId); + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java index 232f1775..fa0226b2 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java @@ -983,7 +983,8 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { Long roomId = entry.getKey(); List roomUnallocatedBeds = entry.getValue(); String roomCode = roomCodeMap.get(roomId); - log.error("房间{}({})有{}个床位未分配:{}", roomCode, roomId, roomUnallocatedBeds.size(), roomUnallocatedBeds); + log.error("房间{}({})有{}个床位未分配:{}", roomCode, roomId, roomUnallocatedBeds.size(), + roomUnallocatedBeds); } throw new RuntimeException("床位分配不完整,存在" + (totalExpectedBeds - totalAllocatedBeds) + "个床位未分配"); @@ -1008,7 +1009,7 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { /** * 获取当前用户二级学院的宿舍楼树形结构 - * 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择 + * 校区-宿舍楼二级结构,校区不可选,只有宿舍楼可以选择 * * @return 宿舍楼树形结构数据 */ @@ -1159,8 +1160,8 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { .orderByAsc(XgDormCampus::getSortOrder, XgDormCampus::getId); List campuses = xgDormCampusMapper.selectList(campusWrapper); - // 构建树形结构 - return buildTreeStructure(campuses, areas, buildings); + // 构建简化版树形结构(校区-宿舍楼,去除宿舍区层级) + return buildSimplifiedTreeStructure(campuses, areas, buildings); } catch (Exception e) { log.error("构建宿舍楼树形结构失败", e); @@ -1232,6 +1233,60 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService { return new ArrayList<>(campusMap.values()); } + /** + * 构建简化树形结构:校区-宿舍楼(去除宿舍区层级) + */ + private List buildSimplifiedTreeStructure(List campuses, + List areas, + List buildings) { + + // 构建校区节点(第一层) + Map campusMap = new LinkedHashMap<>(); + for (XgDormCampus campus : campuses) { + // 检查该校区下是否有宿舍楼(通过宿舍区关联) + boolean hasBuildings = buildings.stream().anyMatch(building -> areas.stream().anyMatch( + area -> area.getId().equals(building.getAreaId()) && area.getCampusId().equals(campus.getId()))); + + if (hasBuildings) { + DormBuildingTreeVo campusNode = new DormBuildingTreeVo(); + campusNode.setId(campus.getId()); + campusNode.setLabel(campus.getCampusName()); + campusNode.setType("campus"); + campusNode.setDisabled(true); // 校区节点禁止点击 + campusNode.setChildren(new ArrayList<>()); + campusMap.put(campus.getId(), campusNode); + } + } + + // 直接构建宿舍楼节点(第二层),跳过宿舍区 + for (XgDormBuilding building : buildings) { + // 通过宿舍区找到对应的校区 + XgDormArea area = areas.stream() + .filter(a -> a.getId().equals(building.getAreaId())) + .findFirst() + .orElse(null); + + if (area != null) { + DormBuildingTreeVo buildingNode = new DormBuildingTreeVo(); + buildingNode.setId(building.getId()); + buildingNode.setLabel(building.getBuildingName()); + buildingNode.setType("building"); + buildingNode.setDisabled(false); // 宿舍楼节点可以点击 + buildingNode.setBuildingFloors(building.getFloors()); + buildingNode.setBuildingType(building.getBuildingType()); + buildingNode.setChildren(new ArrayList<>()); + + // 直接添加到校区下,跳过宿舍区层级 + DormBuildingTreeVo campusNode = campusMap.get(area.getCampusId()); + if (campusNode != null) { + campusNode.getChildren().add(buildingNode); + } + } + } + + return new ArrayList<>(campusMap.values()); + } + /** * 获取班主任管理的班级宿舍楼树 * 通过当前用户所在的班级,获取该班级分配的宿舍楼 diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBuildingServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBuildingServiceImpl.java index 3cca3511..07d9e674 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBuildingServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBuildingServiceImpl.java @@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.dromara.xg.domain.bo.XgDormBuildingBo; import org.dromara.xg.domain.vo.XgDormBuildingVo; +import org.dromara.xg.domain.vo.XgDormBuildingSimplifiedVo; import org.dromara.xg.domain.XgDormBuilding; import org.dromara.xg.domain.XgDormArea; import org.dromara.xg.domain.XgDormCampus; @@ -145,7 +146,7 @@ public class XgDormBuildingServiceImpl implements IXgDormBuildingService { /** * 获取宿舍楼树形结构数据 - * 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择 + * 校区-宿舍楼二级结构,校区不可选,只有宿舍楼可以选择 * * @return 宿舍楼树形结构数据 */ @@ -156,59 +157,41 @@ public class XgDormBuildingServiceImpl implements IXgDormBuildingService { Wrappers.lambdaQuery() .orderByAsc(XgDormCampus::getSortOrder, XgDormCampus::getId)); - // 2. 获取所有宿舍区数据 - List areaList = areaMapper.selectList( - Wrappers.lambdaQuery() - .orderByAsc(XgDormArea::getSortOrder, XgDormArea::getId)); - - // 3. 获取所有宿舍楼数据 + // 2. 获取所有宿舍楼数据 List buildingList = baseMapper.selectList( Wrappers.lambdaQuery() .orderByAsc(XgDormBuilding::getSortOrder, XgDormBuilding::getId)); - // 4. 构建树形结构 + // 3. 构建树形结构 List treeList = new ArrayList<>(); - // 5. 先构建校区节点 + // 4. 先构建校区节点 for (XgDormCampus campus : campusList) { - BuildingTreeSelectVo campusNode = new BuildingTreeSelectVo(); - campusNode.setId(campus.getId()); - campusNode.setLabel(campus.getCampusName()); - campusNode.setNodeType("campus"); - campusNode.setDisabled(true); // 校区不可选 - campusNode.setChildren(new ArrayList<>()); - - // 设置创建者和更新者字段 - campusNode.setCreateBy(campus.getCreateBy()); - campusNode.setCreateTime(campus.getCreateTime()); - campusNode.setUpdateBy(campus.getUpdateBy()); - campusNode.setUpdateTime(campus.getUpdateTime()); - - // 添加该校区下的宿舍区 - List campusAreas = areaList.stream() - .filter(area -> area.getCampusId().equals(campus.getId())) + // 检查该校区下是否有宿舍楼 + List campusBuildings = buildingList.stream() + .filter(building -> { + // 通过宿舍区找到校区 + XgDormArea area = areaMapper.selectById(building.getAreaId()); + return area != null && area.getCampusId().equals(campus.getId()); + }) .collect(Collectors.toList()); - for (XgDormArea area : campusAreas) { - BuildingTreeSelectVo areaNode = new BuildingTreeSelectVo(); - areaNode.setId(area.getId()); - areaNode.setLabel(area.getAreaName()); - areaNode.setNodeType("area"); - areaNode.setDisabled(true); // 宿舍区不可选 - areaNode.setChildren(new ArrayList<>()); + if (!campusBuildings.isEmpty()) { + BuildingTreeSelectVo campusNode = new BuildingTreeSelectVo(); + campusNode.setId(campus.getId()); + campusNode.setLabel(campus.getCampusName()); + campusNode.setNodeType("campus"); + campusNode.setDisabled(true); // 校区不可选 + campusNode.setChildren(new ArrayList<>()); // 设置创建者和更新者字段 - areaNode.setCreateBy(area.getCreateBy()); - areaNode.setCreateTime(area.getCreateTime()); - areaNode.setUpdateBy(area.getUpdateBy()); - areaNode.setUpdateTime(area.getUpdateTime()); + campusNode.setCreateBy(campus.getCreateBy()); + campusNode.setCreateTime(campus.getCreateTime()); + campusNode.setUpdateBy(campus.getUpdateBy()); + campusNode.setUpdateTime(campus.getUpdateTime()); - // 添加该宿舍区下的宿舍楼 - List areaBuildings = buildingList.stream() - .filter(building -> building.getAreaId().equals(area.getId())) - .collect(Collectors.toList()); - - for (XgDormBuilding building : areaBuildings) { + // 直接添加该校区下的宿舍楼,跳过宿舍区层级 + for (XgDormBuilding building : campusBuildings) { BuildingTreeSelectVo buildingNode = new BuildingTreeSelectVo(); buildingNode.setId(building.getId()); buildingNode.setLabel(building.getBuildingName()); @@ -222,15 +205,57 @@ public class XgDormBuildingServiceImpl implements IXgDormBuildingService { buildingNode.setUpdateBy(building.getUpdateBy()); buildingNode.setUpdateTime(building.getUpdateTime()); - areaNode.getChildren().add(buildingNode); + campusNode.getChildren().add(buildingNode); } - campusNode.getChildren().add(areaNode); + treeList.add(campusNode); } - - treeList.add(campusNode); } return treeList; } + + /** + * 分页查询宿舍楼信息简化列表 + * 用于简化界面显示,隐藏宿舍区层级,直接显示校区→宿舍楼结构 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 宿舍楼信息简化分页列表 + */ + @Override + public TableDataInfo querySimplifiedPageList(XgDormBuildingBo bo, PageQuery pageQuery) { + // 先查询完整的分页数据 + TableDataInfo fullPageList = queryPageList(bo, pageQuery); + + // 转换为简化版VO + List simplifiedList = fullPageList.getRows().stream() + .map(XgDormBuildingSimplifiedVo::fromFullVo) + .collect(Collectors.toList()); + + // 创建简化版分页结果 + TableDataInfo simplifiedPageList = new TableDataInfo<>(); + simplifiedPageList.setRows(simplifiedList); + simplifiedPageList.setTotal(fullPageList.getTotal()); + + return simplifiedPageList; + } + + /** + * 查询符合条件的宿舍楼信息简化列表 + * 用于简化界面显示,隐藏宿舍区层级,直接显示校区→宿舍楼结构 + * + * @param bo 查询条件 + * @return 宿舍楼信息简化列表 + */ + @Override + public List querySimplifiedList(XgDormBuildingBo bo) { + // 先查询完整的数据 + List fullList = queryList(bo); + + // 转换为简化版VO + return fullList.stream() + .map(XgDormBuildingSimplifiedVo::fromFullVo) + .collect(Collectors.toList()); + } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBuildingTreeServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBuildingTreeServiceImpl.java index 46cd2244..c96263fb 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBuildingTreeServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBuildingTreeServiceImpl.java @@ -8,6 +8,7 @@ import lombok.RequiredArgsConstructor; import org.dromara.xg.service.IXgDormBuildingTreeService; import org.dromara.xg.mapper.XgDormBuildingTreeMapper; import org.dromara.xg.domain.vo.DormBuildingTreeVo; +import org.dromara.xg.domain.vo.DormBuildingTreeSimplifiedVo; import org.dromara.xg.domain.vo.DormBuildingInfoVo; import org.dromara.xg.domain.bo.DormBuildingTreeBo; import org.dromara.common.satoken.utils.LoginHelper; @@ -231,4 +232,42 @@ public class XgDormBuildingTreeServiceImpl implements IXgDormBuildingTreeService return new ArrayList<>(campusMap.values()); } + /** + * 获取宿舍楼树形结构简化数据 + * 校区-宿舍楼二级结构,隐藏宿舍区层级 + * + * @param bo 查询参数 + * @return 简化树形结构数据 + */ + @Override + public List getSimplifiedDormBuildingTree(DormBuildingTreeBo bo) { + // 先获取完整的树形结构数据 + List fullTreeData = getDormBuildingTree(bo); + + // 转换为简化版树形结构 + return fullTreeData.stream() + .map(DormBuildingTreeSimplifiedVo::fromFullTreeVo) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * 获取所有宿舍楼树形结构简化数据(不考虑权限) + * 校区-宿舍楼二级结构,隐藏宿舍区层级 + * + * @param bo 查询参数 + * @return 简化树形结构数据 + */ + @Override + public List getAllSimplifiedDormBuildingTree(DormBuildingTreeBo bo) { + // 先获取完整的树形结构数据(不考虑权限) + List fullTreeData = getAllDormBuildingTree(bo); + + // 转换为简化版树形结构 + return fullTreeData.stream() + .map(DormBuildingTreeSimplifiedVo::fromFullTreeVo) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckServiceImpl.java index f74536ad..d06141ed 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckServiceImpl.java @@ -14,6 +14,8 @@ import org.springframework.transaction.annotation.Transactional; import org.dromara.xg.domain.bo.XgDormHygieneCheckBo; import org.dromara.xg.domain.bo.XgDormHygieneCheckCompleteBo; import org.dromara.xg.domain.bo.XgDormHygieneStudentScoreBo; +import org.dromara.xg.domain.bo.XgHygieneCheckViolationBo; +import org.dromara.xg.domain.bo.XgViolationDataBo; import org.dromara.xg.domain.vo.DormHygieneCheckExportVo; import org.dromara.xg.domain.vo.XgDormHygieneCheckVo; import org.dromara.xg.domain.XgDormHygieneCheck; @@ -22,11 +24,23 @@ import org.dromara.xg.mapper.XgDormHygieneCheckMapper; import org.dromara.xg.mapper.XgDormHygieneStudentScoreMapper; import org.dromara.xg.service.IXgDormHygieneCheckService; import org.dromara.xg.service.IXgStudentMsgService; +import org.dromara.xg.service.IXgHygieneCheckViolationService; +import org.dromara.xg.service.IXgDormHygieneStudentScoreService; +import org.dromara.xg.mapper.XgHygieneCheckViolationMapper; import org.dromara.system.service.ISysDictDataService; +import org.springframework.context.ApplicationContext; import java.util.List; import java.util.Map; import java.util.Collection; +import java.util.HashMap; +import java.util.Set; +import java.util.HashSet; +import java.util.Objects; +import java.util.stream.Collectors; +import org.dromara.xg.domain.vo.XgHygieneCheckViolationVo; +import org.dromara.xg.domain.XgHygieneCheckViolation; +import java.util.Collections; /** * 宿舍卫生检查Service业务层处理 @@ -43,6 +57,10 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService private final XgDormHygieneStudentScoreMapper studentScoreMapper; private final IXgStudentMsgService xgStudentMsgService; private final ISysDictDataService sysDictDataService; + private final IXgHygieneCheckViolationService violationService; + private final IXgDormHygieneStudentScoreService studentScoreService; + private final XgHygieneCheckViolationMapper violationMapper; + private final ApplicationContext applicationContext; /** * 查询宿舍卫生检查 @@ -154,18 +172,60 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService } /** - * 校验并批量删除宿舍卫生检查信息 + * 校验并批量删除宿舍卫生检查信息(级联删除四个表) * * @param ids 待删除的主键集合 * @param isValid 是否进行有效性校验 * @return 是否删除成功 */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { if (isValid) { // TODO 做一些业务上的校验,判断是否需要校验 } - return baseMapper.deleteByIds(ids) > 0; + + try { + // 逐个处理每个卫生检查记录的级联删除 + for (Long checkId : ids) { + log.info("开始级联删除宿舍卫生检查记录,检查ID: {}", checkId); + + // 1. 查询并删除关联的违纪记录 + List violations = violationService.queryByHygieneCheckId(checkId); + if (!violations.isEmpty()) { + log.info("发现 {} 条违纪关联记录需要删除,检查ID: {}", violations.size(), checkId); + + // 1.1 删除统一违纪记录 (xskh_discipline_record) + for (XgHygieneCheckViolationVo violation : violations) { + if (violation.getDisciplineRecordId() != null) { + deleteDisciplineRecord(violation.getDisciplineRecordId()); + } + } + + // 1.2 删除违纪关联记录 (xg_hygiene_check_violation) + violationService.deleteByHygieneCheckId(checkId); + log.info("删除违纪关联记录完成,检查ID: {}", checkId); + } + + // 2. 删除学生评分记录 (xg_dorm_hygiene_student_score) + Boolean studentScoreDeleted = studentScoreService.deleteByHygieneCheckId(checkId); + if (studentScoreDeleted) { + log.info("删除学生评分记录完成,检查ID: {}", checkId); + } + } + + // 3. 删除主检查记录 (xg_dorm_hygiene_check) + Boolean mainDeleted = baseMapper.deleteByIds(ids) > 0; + if (mainDeleted) { + log.info("删除宿舍卫生检查主记录完成,删除数量: {}", ids.size()); + } + + return mainDeleted; + + } catch (Exception e) { + log.error("级联删除宿舍卫生检查记录失败,检查IDs: {}, 错误: {}", ids, e.getMessage(), e); + throw new RuntimeException("删除宿舍卫生检查记录失败: " + e.getMessage(), e); + } } /** @@ -574,4 +634,711 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService return "1".equals(isResponsiblePerson) ? "是" : "否"; } + /** + * 保存完整的宿舍卫生检查数据(包含学生评分和违纪记录) + * + * @param bo 完整的宿舍卫生检查数据 + * @return 检查记录ID,保存失败返回null + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveCompleteData(XgDormHygieneCheckCompleteBo bo) { + try { + // 1. 转换并保存主表数据 + XgDormHygieneCheck add = MapstructUtils.convert(bo, XgDormHygieneCheck.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + log.info("保存宿舍卫生检查主表数据: {}", add); + + if (!flag) { + log.error("保存宿舍卫生检查主表数据失败"); + return null; + } + + Long hygieneCheckId = add.getId(); + + // 2. 保存学生评分数据 + if (bo.getStudentScores() != null && !bo.getStudentScores().isEmpty()) { + for (XgDormHygieneStudentScoreBo studentScoreBo : bo.getStudentScores()) { + XgDormHygieneStudentScore studentScore = MapstructUtils.convert(studentScoreBo, + XgDormHygieneStudentScore.class); + studentScore.setHygieneCheckId(hygieneCheckId); + + // 根据学生ID查询学生完整信息,补充学院、专业、班级信息 + if (studentScoreBo.getStudentId() != null) { + try { + var studentInfo = xgStudentMsgService.queryById(studentScoreBo.getStudentId()); + if (studentInfo != null) { + // 补充学院信息 + if (studentScore.getCollegeId() == null + && StringUtils.isNotBlank(studentInfo.getCollegeId())) { + try { + studentScore.setCollegeId(Long.valueOf(studentInfo.getCollegeId())); + } catch (NumberFormatException e) { + log.warn("学院ID转换失败,学生ID: {}, 学院ID: {}", studentScoreBo.getStudentId(), + studentInfo.getCollegeId()); + } + } + if (StringUtils.isBlank(studentScore.getCollegeName())) { + studentScore.setCollegeName(studentInfo.getCollegeName()); + } + + // 补充专业信息 + if (studentScore.getMajorId() == null + && StringUtils.isNotBlank(studentInfo.getMajorId())) { + try { + studentScore.setMajorId(Long.valueOf(studentInfo.getMajorId())); + } catch (NumberFormatException e) { + log.warn("专业ID转换失败,学生ID: {}, 专业ID: {}", studentScoreBo.getStudentId(), + studentInfo.getMajorId()); + } + } + if (StringUtils.isBlank(studentScore.getMajorName())) { + studentScore.setMajorName(studentInfo.getMajorName()); + } + + // 补充班级信息 + if (studentScore.getClassId() == null + && StringUtils.isNotBlank(studentInfo.getClassId())) { + try { + studentScore.setClassId(Long.valueOf(studentInfo.getClassId())); + } catch (NumberFormatException e) { + log.warn("班级ID转换失败,学生ID: {}, 班级ID: {}", studentScoreBo.getStudentId(), + studentInfo.getClassId()); + } + } + if (StringUtils.isBlank(studentScore.getClassName())) { + studentScore.setClassName(studentInfo.getClassName()); + } + + log.info("补充学生信息 - 学生ID: {}, 学院: {}, 专业: {}, 班级: {}", + studentScoreBo.getStudentId(), + studentInfo.getCollegeName(), + studentInfo.getMajorName(), + studentInfo.getClassName()); + } + } catch (Exception e) { + log.warn("查询学生信息失败,学生ID: {}, 错误: {}", studentScoreBo.getStudentId(), e.getMessage()); + } + } + + log.info("保存学生评分数据: {}", studentScore); + studentScoreMapper.insert(studentScore); + } + } + + // 3. 保存违纪记录数据(兼容架构:优先保存到统一表,失败时保存关联关系) + if (bo.getViolationData() != null && !bo.getViolationData().isEmpty()) { + for (XgViolationDataBo violationBo : bo.getViolationData()) { + try { + log.info("处理违纪记录数据: {}", violationBo); + + // 3.1 尝试创建违纪记录到统一的xskh_discipline_record表 + Long disciplineRecordId = createDisciplineRecord(violationBo); + + // 3.2 仅当违纪记录创建成功时才创建关联记录 + if (disciplineRecordId != null) { + XgHygieneCheckViolationBo relationBo = new XgHygieneCheckViolationBo(); + relationBo.setHygieneCheckId(hygieneCheckId); + relationBo.setDisciplineRecordId(disciplineRecordId); + relationBo.setStudentId(violationBo.getStudentId()); + relationBo.setStudentNo(violationBo.getStudentNo()); + relationBo.setStudentName(violationBo.getStudentName()); + relationBo.setDisciplineTypeId(violationBo.getDisciplineTypeId()); + relationBo.setDisciplineTypeName(violationBo.getDisciplineTypeName()); + relationBo.setViolationSource("1"); // 1-宿舍卫生检查 + relationBo.setStatus("1"); // 1-生效 + + boolean relationResult = violationService.insertByBo(relationBo); + if (!relationResult) { + log.warn("创建卫生检查违纪关联记录失败,学生ID: {}", violationBo.getStudentId()); + } else { + log.info("创建卫生检查违纪关联记录成功,学生ID: {},违纪记录ID: {}", + violationBo.getStudentId(), disciplineRecordId); + } + } else { + log.warn("违纪记录创建失败,跳过关联记录创建,学生ID: {}", violationBo.getStudentId()); + } + } catch (Exception e) { + log.error("处理违纪数据失败,学生ID: {}, 错误: {}", violationBo.getStudentId(), e.getMessage(), e); + throw new RuntimeException( + "处理违纪数据失败,学生ID: " + violationBo.getStudentId() + ", 错误: " + e.getMessage(), e); + } + } + } + + log.info("完整保存宿舍卫生检查数据成功,检查ID: {}", hygieneCheckId); + return hygieneCheckId; + + } catch (Exception e) { + log.error("保存宿舍卫生检查数据失败", e); + throw new RuntimeException("保存宿舍卫生检查数据失败: " + e.getMessage(), e); + } + } + + /** + * 创建学生违纪记录 + * + * @param violationBo 违纪数据 + * @return 违纪记录ID + */ + private Long createDisciplineRecord(XgViolationDataBo violationBo) { + try { + // 通过接口类型动态获取IXskhDisciplineRecordService,避免硬依赖 + Class serviceInterface = Class.forName("org.dromara.xskh.service.IXskhDisciplineRecordService"); + Object disciplineRecordService = applicationContext.getBean(serviceInterface); + + if (disciplineRecordService != null) { + // 创建XskhDisciplineRecordBo对象 + Object disciplineRecordBo = createDisciplineRecordBo(violationBo); + if (disciplineRecordBo != null) { + // 反射调用insertByBo方法 - 使用具体的Bo类型而不是Object + Class disciplineRecordBoClass = Class + .forName("org.dromara.xskh.domain.bo.XskhDisciplineRecordBo"); + var insertMethod = disciplineRecordService.getClass().getMethod("insertByBo", + disciplineRecordBoClass); + Boolean result = (Boolean) insertMethod.invoke(disciplineRecordService, disciplineRecordBo); + + if (result) { + // 获取插入后的ID + var field = disciplineRecordBo.getClass().getDeclaredField("id"); + field.setAccessible(true); + Long id = (Long) field.get(disciplineRecordBo); + log.info("创建学生违纪记录成功,学生ID: {}, 违纪记录ID: {}", violationBo.getStudentId(), id); + return id; + } + } + } + } catch (ClassNotFoundException e) { + log.warn("学生考核管理模块(xskh)未启用,跳过违纪记录创建"); + } catch (Exception e) { + log.error("创建学生违纪记录失败: {}", e.getMessage(), e); + } + return null; + } + + /** + * 创建学生违纪记录Bo对象 + * + * @param violationBo 违纪数据 + * @return 学生违纪记录Bo对象 + */ + private Object createDisciplineRecordBo(XgViolationDataBo violationBo) { + try { + // 通过反射创建XskhDisciplineRecordBo对象 + Class disciplineRecordBoClass = Class.forName("org.dromara.xskh.domain.bo.XskhDisciplineRecordBo"); + Object disciplineRecordBo = disciplineRecordBoClass.getDeclaredConstructor().newInstance(); + + // 设置基本字段 + setFieldValue(disciplineRecordBo, "studentId", violationBo.getStudentId()); + setFieldValue(disciplineRecordBo, "studentNo", violationBo.getStudentNo()); + setFieldValue(disciplineRecordBo, "studentName", violationBo.getStudentName()); + + // 修复:确保collegeId、majorId、classId不为null + // 如果ID为空,尝试通过学生ID查询完整信息 + Long collegeId = violationBo.getCollegeId(); + Long majorId = violationBo.getMajorId(); + Long classId = violationBo.getClassId(); + String collegeName = violationBo.getCollegeName(); + String majorName = violationBo.getMajorName(); + String className = violationBo.getClassName(); + + // 如果ID字段为空,尝试从学生信息中获取 + if ((collegeId == null || collegeId == 0) && violationBo.getStudentId() != null) { + try { + // 查询学生详细信息以获取缺失的ID字段 + Map studentDetail = getStudentDetailInfo(violationBo.getStudentId()); + if (studentDetail != null) { + // 修复:安全地获取Long类型的ID字段 + Object collegeIdObj = studentDetail.get("collegeId"); + Object majorIdObj = studentDetail.get("majorId"); + Object classIdObj = studentDetail.get("classId"); + + collegeId = (collegeIdObj instanceof Long) ? (Long) collegeIdObj + : (collegeIdObj != null ? Long.valueOf(collegeIdObj.toString()) : null); + majorId = (majorIdObj instanceof Long) ? (Long) majorIdObj + : (majorIdObj != null ? Long.valueOf(majorIdObj.toString()) : null); + classId = (classIdObj instanceof Long) ? (Long) classIdObj + : (classIdObj != null ? Long.valueOf(classIdObj.toString()) : null); + + // 如果名称为空,也从查询结果中获取 + if (StringUtils.isBlank(collegeName)) { + collegeName = (String) studentDetail.getOrDefault("collegeName", "未知学院"); + } + if (StringUtils.isBlank(majorName)) { + majorName = (String) studentDetail.getOrDefault("majorName", "未知专业"); + } + if (StringUtils.isBlank(className)) { + className = (String) studentDetail.getOrDefault("className", "未知班级"); + } + + log.info("从学生信息中补充ID字段,学生ID: {}, 学院ID: {}, 专业ID: {}, 班级ID: {}", + violationBo.getStudentId(), collegeId, majorId, classId); + } + } catch (Exception e) { + log.warn("查询学生详细信息失败,使用默认值,学生ID: {}, 错误: {}", violationBo.getStudentId(), e.getMessage()); + } + } + + setFieldValue(disciplineRecordBo, "collegeId", collegeId != null ? collegeId : 0L); + setFieldValue(disciplineRecordBo, "collegeName", + StringUtils.isNotBlank(collegeName) ? collegeName : "未知学院"); + setFieldValue(disciplineRecordBo, "majorId", majorId != null ? majorId : 0L); + setFieldValue(disciplineRecordBo, "majorName", + StringUtils.isNotBlank(majorName) ? majorName : "未知专业"); + setFieldValue(disciplineRecordBo, "classId", classId != null ? classId : 0L); + setFieldValue(disciplineRecordBo, "className", + StringUtils.isNotBlank(className) ? className : "未知班级"); + + setFieldValue(disciplineRecordBo, "disciplineTypeId", violationBo.getDisciplineTypeId()); + setFieldValue(disciplineRecordBo, "disciplineTypeName", violationBo.getDisciplineTypeName()); + + // 设置违纪日期 + if (StringUtils.isNotBlank(violationBo.getDisciplineDate())) { + setFieldValue(disciplineRecordBo, "disciplineDate", + java.time.LocalDate.parse(violationBo.getDisciplineDate())); + } else { + setFieldValue(disciplineRecordBo, "disciplineDate", java.time.LocalDate.now()); + } + + // 设置违纪原因和内容 + setFieldValue(disciplineRecordBo, "disciplineReason", violationBo.getDisciplineReason()); + setFieldValue(disciplineRecordBo, "disciplineContent", violationBo.getDisciplineContent()); + + // 设置扣分分值 + if (violationBo.getScoreDeduction() != null) { + setFieldValue(disciplineRecordBo, "scoreDeduction", violationBo.getScoreDeduction()); + } + + // 设置状态和违纪来源 + setFieldValue(disciplineRecordBo, "status", + StringUtils.isNotBlank(violationBo.getStatus()) ? violationBo.getStatus() : "1"); + setFieldValue(disciplineRecordBo, "violationSource", "1"); // 1-宿舍卫生检查 + + // 设置附件信息(如果有) + if (StringUtils.isNotBlank(violationBo.getAttachmentOssIds())) { + setFieldValue(disciplineRecordBo, "attachmentOssIds", violationBo.getAttachmentOssIds()); + } + + log.info("创建学生违纪记录Bo对象成功,学生ID: {}, 违纪类型: {}", + violationBo.getStudentId(), violationBo.getDisciplineTypeName()); + + return disciplineRecordBo; + + } catch (Exception e) { + log.error("创建学生违纪记录Bo对象失败: {}", e.getMessage(), e); + return null; + } + } + + /** + * 通过反射设置对象字段值 + * + * @param target 目标对象 + * @param fieldName 字段名 + * @param value 字段值 + */ + private void setFieldValue(Object target, String fieldName, Object value) { + try { + if (value == null) { + return; + } + + var field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } catch (Exception e) { + log.warn("设置字段值失败,字段名: {}, 值: {}, 错误: {}", fieldName, value, e.getMessage()); + } + } + + /** + * 获取学生详细信息(包含学院、专业、班级ID) + * + * @param studentId 学生ID + * @return 学生详细信息Map + */ + private Map getStudentDetailInfo(Long studentId) { + try { + var studentInfo = xgStudentMsgService.queryById(studentId); + if (studentInfo != null) { + Map detailMap = new HashMap<>(); + + // 修复:安全地将String类型的ID转换为Long类型 + detailMap.put("collegeId", parseStringToLong(studentInfo.getCollegeId())); + detailMap.put("collegeName", studentInfo.getCollegeName()); + detailMap.put("majorId", parseStringToLong(studentInfo.getMajorId())); + detailMap.put("majorName", studentInfo.getMajorName()); + detailMap.put("classId", parseStringToLong(studentInfo.getClassId())); + detailMap.put("className", studentInfo.getClassName()); + + log.debug("获取学生详细信息成功,学生ID: {}, 学院ID: {}, 专业ID: {}, 班级ID: {}", + studentId, detailMap.get("collegeId"), detailMap.get("majorId"), detailMap.get("classId")); + + return detailMap; + } + } catch (Exception e) { + log.warn("查询学生详细信息失败,学生ID: {}", studentId, e); + } + return null; + } + + /** + * 安全地将String类型转换为Long类型 + * + * @param str 字符串值 + * @return Long值,转换失败时返回null + */ + private Long parseStringToLong(String str) { + if (StringUtils.isBlank(str)) { + return null; + } + try { + return Long.valueOf(str.trim()); + } catch (NumberFormatException e) { + log.warn("字符串转Long失败,原值: {}", str); + return null; + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean updateCompleteData(XgDormHygieneCheckCompleteBo bo) { + try { + // 1. 更新主表数据 + XgDormHygieneCheck update = MapstructUtils.convert(bo, XgDormHygieneCheck.class); + validEntityBeforeSave(update); + boolean flag = baseMapper.updateById(update) > 0; + log.info("更新宿舍卫生检查主表数据: {}", update); + + if (!flag) { + log.error("更新宿舍卫生检查主表数据失败"); + return false; + } + + Long hygieneCheckId = update.getId(); + + // 2. 更新学生评分数据 - 先删除旧数据,再插入新数据 + LambdaQueryWrapper scoreWrapper = Wrappers.lambdaQuery(); + scoreWrapper.eq(XgDormHygieneStudentScore::getHygieneCheckId, hygieneCheckId); + studentScoreMapper.delete(scoreWrapper); + log.info("删除旧的学生评分数据,检查ID: {}", hygieneCheckId); + + if (bo.getStudentScores() != null && !bo.getStudentScores().isEmpty()) { + for (XgDormHygieneStudentScoreBo studentScoreBo : bo.getStudentScores()) { + XgDormHygieneStudentScore studentScore = MapstructUtils.convert(studentScoreBo, + XgDormHygieneStudentScore.class); + studentScore.setHygieneCheckId(hygieneCheckId); + studentScore.setId(null); // 确保是新插入 + + // 根据学生ID查询学生完整信息,补充学院、专业、班级信息 + if (studentScoreBo.getStudentId() != null) { + try { + var studentInfo = xgStudentMsgService.queryById(studentScoreBo.getStudentId()); + if (studentInfo != null) { + // 补充学院信息 + if (studentScore.getCollegeId() == null + && StringUtils.isNotBlank(studentInfo.getCollegeId())) { + try { + studentScore.setCollegeId(Long.valueOf(studentInfo.getCollegeId())); + } catch (NumberFormatException e) { + log.warn("学院ID转换失败,学生ID: {}, 学院ID: {}", studentScoreBo.getStudentId(), + studentInfo.getCollegeId()); + } + } + if (StringUtils.isBlank(studentScore.getCollegeName())) { + studentScore.setCollegeName(studentInfo.getCollegeName()); + } + + // 补充专业信息 + if (studentScore.getMajorId() == null + && StringUtils.isNotBlank(studentInfo.getMajorId())) { + try { + studentScore.setMajorId(Long.valueOf(studentInfo.getMajorId())); + } catch (NumberFormatException e) { + log.warn("专业ID转换失败,学生ID: {}, 专业ID: {}", studentScoreBo.getStudentId(), + studentInfo.getMajorId()); + } + } + if (StringUtils.isBlank(studentScore.getMajorName())) { + studentScore.setMajorName(studentInfo.getMajorName()); + } + + // 补充班级信息 + if (studentScore.getClassId() == null + && StringUtils.isNotBlank(studentInfo.getClassId())) { + try { + studentScore.setClassId(Long.valueOf(studentInfo.getClassId())); + } catch (NumberFormatException e) { + log.warn("班级ID转换失败,学生ID: {}, 班级ID: {}", studentScoreBo.getStudentId(), + studentInfo.getClassId()); + } + } + if (StringUtils.isBlank(studentScore.getClassName())) { + studentScore.setClassName(studentInfo.getClassName()); + } + + log.info("补充学生信息 - 学生ID: {}, 学院: {}, 专业: {}, 班级: {}", + studentScoreBo.getStudentId(), + studentInfo.getCollegeName(), + studentInfo.getMajorName(), + studentInfo.getClassName()); + } + } catch (Exception e) { + log.warn("查询学生信息失败,学生ID: {}, 错误: {}", studentScoreBo.getStudentId(), e.getMessage()); + } + } + + log.info("保存学生评分数据: {}", studentScore); + studentScoreMapper.insert(studentScore); + } + } + + // 3. 处理违纪记录数据 - 完整的增删改逻辑 + handleViolationRecords(hygieneCheckId, bo.getViolationData()); + + log.info("完整更新宿舍卫生检查数据成功,检查ID: {}", hygieneCheckId); + return true; + + } catch (Exception e) { + log.error("更新宿舍卫生检查数据失败", e); + throw new RuntimeException("更新宿舍卫生检查数据失败: " + e.getMessage(), e); + } + } + + /** + * 处理违纪记录数据 - 完整的增删改逻辑 + * + * @param hygieneCheckId 卫生检查ID + * @param newViolationData 新的违纪记录数据列表 + */ + private void handleViolationRecords(Long hygieneCheckId, List newViolationData) { + try { + // 1. 查询数据库中现有的违纪记录 + List existingViolations = getViolationsByHygieneCheckId(hygieneCheckId); + log.info("数据库中现有违纪记录数量: {}", existingViolations.size()); + + // 2. 收集现有记录的违纪记录ID(disciplineRecordId) + Set existingDisciplineIds = existingViolations.stream() + .map(XgHygieneCheckViolationVo::getDisciplineRecordId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + log.info("现有违纪记录ID集合: {}", existingDisciplineIds); + + // 3. 收集新数据中的违纪记录ID(优先使用disciplineRecordId字段,兼容旧版本的id字段) + Set newDisciplineIds = new HashSet<>(); + if (newViolationData != null) { + newDisciplineIds = newViolationData.stream() + .map(violationBo -> { + // 优先使用disciplineRecordId字段,如果为空则使用id字段(向后兼容) + return violationBo.getDisciplineRecordId() != null + ? violationBo.getDisciplineRecordId() + : violationBo.getId(); + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + log.info("新数据中的违纪记录ID集合: {}", newDisciplineIds); + + // 4. 找出需要删除的记录(在数据库中存在,但不在新数据中) + Set toDeleteIds = new HashSet<>(existingDisciplineIds); + toDeleteIds.removeAll(newDisciplineIds); + log.info("需要删除的违纪记录ID集合: {}", toDeleteIds); + + // 5. 删除不再存在的记录 + for (Long deleteId : toDeleteIds) { + deleteViolationRecord(hygieneCheckId, deleteId); + } + + // 6. 处理新数据中的记录(新增或更新) + if (newViolationData != null && !newViolationData.isEmpty()) { + for (XgViolationDataBo violationBo : newViolationData) { + try { + log.info("处理违纪记录数据: {}", violationBo); + + // 如果是已存在的违纪记录(有违纪记录ID且在现有记录中),更新统一表中的记录 + Long currentDisciplineRecordId = violationBo.getDisciplineRecordId() != null + ? violationBo.getDisciplineRecordId() + : violationBo.getId(); + if (currentDisciplineRecordId != null + && existingDisciplineIds.contains(currentDisciplineRecordId)) { + log.info("更新现有违纪记录,违纪记录ID: {}", currentDisciplineRecordId); + updateDisciplineRecord(violationBo); + } else { + // 新增违纪记录 + log.info("新增违纪记录,学生ID: {}", violationBo.getStudentId()); + Long disciplineRecordId = createDisciplineRecord(violationBo); + + if (disciplineRecordId != null && violationService != null) { + XgHygieneCheckViolationBo relationBo = new XgHygieneCheckViolationBo(); + relationBo.setHygieneCheckId(hygieneCheckId); + relationBo.setDisciplineRecordId(disciplineRecordId); + relationBo.setStudentId(violationBo.getStudentId()); + relationBo.setStudentNo(violationBo.getStudentNo()); + relationBo.setStudentName(violationBo.getStudentName()); + relationBo.setDisciplineTypeId(violationBo.getDisciplineTypeId()); + relationBo.setDisciplineTypeName(violationBo.getDisciplineTypeName()); + relationBo.setViolationSource("1"); // 1-宿舍卫生检查 + relationBo.setStatus("1"); // 1-生效 + + boolean relationResult = violationService.insertByBo(relationBo); + if (!relationResult) { + log.warn("创建卫生检查违纪关联记录失败,学生ID: {}", violationBo.getStudentId()); + } else { + log.info("创建卫生检查违纪关联记录成功,学生ID: {},违纪记录ID: {}", + violationBo.getStudentId(), disciplineRecordId); + } + } + } + } catch (Exception e) { + log.error("处理违纪数据失败,学生ID: {}, 错误: {}", violationBo.getStudentId(), e.getMessage(), e); + throw new RuntimeException( + "处理违纪数据失败,学生ID: " + violationBo.getStudentId() + ", 错误: " + e.getMessage(), e); + } + } + } + + log.info("违纪记录处理完成,卫生检查ID: {}", hygieneCheckId); + + } catch (Exception e) { + log.error("处理违纪记录数据失败,卫生检查ID: {}, 错误: {}", hygieneCheckId, e.getMessage(), e); + throw new RuntimeException("处理违纪记录数据失败: " + e.getMessage(), e); + } + } + + /** + * 删除违纪记录(同时删除关联表和统一表中的记录) + * + * @param hygieneCheckId 卫生检查ID + * @param disciplineRecordId 违纪记录ID + */ + private void deleteViolationRecord(Long hygieneCheckId, Long disciplineRecordId) { + try { + log.info("开始逻辑删除违纪记录,卫生检查ID: {}, 违纪记录ID: {}", hygieneCheckId, disciplineRecordId); + + // 1. 逻辑删除关联表中的记录 (xg_hygiene_check_violation) + if (violationMapper != null) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(XgHygieneCheckViolation::getHygieneCheckId, hygieneCheckId) + .eq(XgHygieneCheckViolation::getDisciplineRecordId, disciplineRecordId); + + // 使用逻辑删除 + int deleteCount = violationMapper.delete(wrapper); + if (deleteCount > 0) { + log.info("逻辑删除卫生检查违纪关联记录成功,违纪记录ID: {}, 删除数量: {}", disciplineRecordId, deleteCount); + } else { + log.warn("逻辑删除卫生检查违纪关联记录失败,违纪记录ID: {}", disciplineRecordId); + } + } + + // 2. 逻辑删除统一表中的记录 (xskh_discipline_record) + deleteDisciplineRecord(disciplineRecordId); + + } catch (Exception e) { + log.error("逻辑删除违纪记录失败,违纪记录ID: {}, 错误: {}", disciplineRecordId, e.getMessage(), e); + throw new RuntimeException("逻辑删除违纪记录失败: " + e.getMessage(), e); + } + } + + /** + * 逻辑删除统一表中的违纪记录 + * + * @param disciplineRecordId 违纪记录ID + */ + private void deleteDisciplineRecord(Long disciplineRecordId) { + try { + // 通过接口类型动态获取IXskhDisciplineRecordService,避免硬依赖 + Class serviceInterface = Class.forName("org.dromara.xskh.service.IXskhDisciplineRecordService"); + Object disciplineRecordService = applicationContext.getBean(serviceInterface); + + if (disciplineRecordService != null) { + // 使用deleteWithValidByIds方法进行逻辑删除 + var deleteMethod = disciplineRecordService.getClass().getMethod("deleteWithValidByIds", + Collection.class, Boolean.class); + Boolean result = (Boolean) deleteMethod.invoke(disciplineRecordService, + Collections.singletonList(disciplineRecordId), false); + + if (result) { + log.info("逻辑删除统一违纪记录成功,违纪记录ID: {}", disciplineRecordId); + } else { + log.error("逻辑删除统一违纪记录失败,违纪记录ID: {}", disciplineRecordId); + } + } else { + log.error("未找到违纪记录服务,无法删除统一违纪记录,违纪记录ID: {}", disciplineRecordId); + } + + } catch (ClassNotFoundException e) { + log.warn("学生考核管理模块(xskh)未启用,跳过统一违纪记录删除"); + } catch (Exception e) { + log.error("逻辑删除统一违纪记录失败,违纪记录ID: {}, 错误: {}", disciplineRecordId, e.getMessage(), e); + throw new RuntimeException("逻辑删除统一违纪记录失败: " + e.getMessage(), e); + } + } + + /** + * 更新学生违纪记录 + * + * @param violationBo 违纪数据 + */ + private void updateDisciplineRecord(XgViolationDataBo violationBo) { + try { + // 通过接口类型动态获取IXskhDisciplineRecordService,避免硬依赖 + Class serviceInterface = Class.forName("org.dromara.xskh.service.IXskhDisciplineRecordService"); + Object disciplineRecordService = applicationContext.getBean(serviceInterface); + + if (disciplineRecordService != null) { + // 创建XskhDisciplineRecordBo对象 + Object disciplineRecordBo = createDisciplineRecordBo(violationBo); + if (disciplineRecordBo != null) { + // 设置ID用于更新 + setFieldValue(disciplineRecordBo, "id", violationBo.getId()); + + // 反射调用updateByBo方法 + Class disciplineRecordBoClass = Class + .forName("org.dromara.xskh.domain.bo.XskhDisciplineRecordBo"); + var updateMethod = disciplineRecordService.getClass().getMethod("updateByBo", + disciplineRecordBoClass); + Boolean result = (Boolean) updateMethod.invoke(disciplineRecordService, disciplineRecordBo); + + if (result) { + log.info("更新学生违纪记录成功,学生ID: {}, 违纪记录ID: {}", + violationBo.getStudentId(), violationBo.getId()); + } else { + log.warn("更新学生违纪记录失败,学生ID: {}", violationBo.getStudentId()); + } + } + } + } catch (ClassNotFoundException e) { + log.warn("学生考核管理模块(xskh)未启用,跳过违纪记录更新"); + } catch (Exception e) { + log.error("更新学生违纪记录失败: {}", e.getMessage(), e); + } + } + + /** + * 根据卫生检查ID查询相关的违纪记录(包含详情) + * + * @param hygieneCheckId 卫生检查ID + * @return 违纪记录列表(包含详情) + */ + @Override + public List getViolationsByHygieneCheckId(Long hygieneCheckId) { + if (hygieneCheckId == null) { + log.warn("卫生检查ID为空,无法查询违纪记录"); + return List.of(); + } + + try { + // 使用自定义查询方法,关联查询违纪详情 + List violations = violationMapper + .selectViolationsByHygieneCheckId(hygieneCheckId); + + log.info("卫生检查ID: {} 查询到 {} 条违纪记录(包含详情)", hygieneCheckId, violations.size()); + return violations; + } catch (Exception e) { + log.error("查询卫生检查违纪记录失败,检查ID: {}, 错误: {}", hygieneCheckId, e.getMessage(), e); + return List.of(); + } + } + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRoomServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRoomServiceImpl.java index dddcfed0..36502a5e 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRoomServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRoomServiceImpl.java @@ -26,6 +26,24 @@ import java.util.Map; import java.util.Collection; import java.util.Set; import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFCell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.util.CellRangeAddress; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.dromara.xg.domain.XgDormBed; +import org.dromara.xg.mapper.XgDormBedMapper; +import java.io.IOException; +import java.io.OutputStream; /** * 宿舍房间信息Service业务层处理 @@ -41,6 +59,7 @@ public class XgDormRoomServiceImpl implements IXgDormRoomService { private final XgDormRoomMapper baseMapper; private final XgDormBedStudentMapper xgDormBedStudentMapper; private final XgSysDeptMapper xgSysDeptMapper; + private final XgDormBedMapper xgDormBedMapper; /** * 查询宿舍房间信息 @@ -144,8 +163,9 @@ public class XgDormRoomServiceImpl implements IXgDormRoomService { private LambdaQueryWrapper buildQueryWrapper(XgDormRoomBo bo) { Map params = bo.getParams(); LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.orderByAsc(XgDormRoom::getFloorNum); // 优先按楼层排序 - lqw.orderByAsc(XgDormRoom::getRoomName); // 其次按房间名称排序 + lqw.orderByAsc(XgDormRoom::getFloorNum); // 第一优先级:按楼层升序 + lqw.orderByAsc(XgDormRoom::getSort); // 第二优先级:按排序号升序 + lqw.orderByAsc(XgDormRoom::getRoomName); // 第三优先级:按房间名称升序 lqw.like(StringUtils.isNotBlank(bo.getRoomCode()), XgDormRoom::getRoomCode, bo.getRoomCode()); lqw.like(StringUtils.isNotBlank(bo.getRoomName()), XgDormRoom::getRoomName, bo.getRoomName()); lqw.eq(bo.getBuildingId() != null, XgDormRoom::getBuildingId, bo.getBuildingId()); @@ -154,6 +174,7 @@ public class XgDormRoomServiceImpl implements IXgDormRoomService { lqw.eq(bo.getActualBeds() != null, XgDormRoom::getActualBeds, bo.getActualBeds()); lqw.eq(bo.getRoomArea() != null, XgDormRoom::getRoomArea, bo.getRoomArea()); lqw.eq(StringUtils.isNotBlank(bo.getRoomStatus()), XgDormRoom::getRoomStatus, bo.getRoomStatus()); + lqw.eq(bo.getSort() != null, XgDormRoom::getSort, bo.getSort()); lqw.eq(StringUtils.isNotBlank(bo.getDeleted()), XgDormRoom::getDeleted, bo.getDeleted()); return lqw; } @@ -310,4 +331,265 @@ public class XgDormRoomServiceImpl implements IXgDormRoomService { return Set.of(); } } + + /** + * 导出住宿清单(宿舍卡片式布局) + * + * @param params 导出参数 + * @param response HTTP响应对象 + */ + @Override + public void exportAccommodationList(Map params, HttpServletResponse response) { + try { + // 解析参数 + Long buildingId = Long.valueOf(params.get("buildingId").toString()); + String buildingName = params.get("buildingName").toString(); + String buildingType = params.get("buildingType").toString(); + String buildingTypeText = "1".equals(buildingType) ? "男生楼" : "女生楼"; + + // 解析JSON字符串 + String accommodationDataJson = params.get("accommodationData").toString(); + ObjectMapper objectMapper = new ObjectMapper(); + @SuppressWarnings("unchecked") + Map accommodationData = objectMapper.readValue(accommodationDataJson, Map.class); + + @SuppressWarnings("unchecked") + Map buildingInfo = (Map) accommodationData.get("buildingInfo"); + + @SuppressWarnings("unchecked") + List> floorDataList = (List>) accommodationData.get("floorData"); + + // 创建Excel工作簿 + try (XSSFWorkbook workbook = new XSSFWorkbook()) { + XSSFSheet sheet = workbook.createSheet(buildingName + "住宿清单"); + + // 创建样式 + CellStyle titleStyle = createTitleStyle(workbook); + CellStyle headerStyle = createHeaderStyle(workbook); + CellStyle dataStyle = createDataStyle(workbook); + CellStyle roomNameStyle = createRoomNameStyle(workbook); + + int currentRow = 0; + + // 添加标题 + XSSFRow titleRow = sheet.createRow(currentRow++); + XSSFCell titleCell = titleRow.createCell(0); + titleCell.setCellValue(buildingName + " " + buildingTypeText + " 住宿清单"); + titleCell.setCellStyle(titleStyle); + sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 19)); // 合并20列 + currentRow++; // 空行 + + // 处理每个楼层 + for (Map floorData : floorDataList) { + Integer floorNum = (Integer) floorData.get("floorNum"); + @SuppressWarnings("unchecked") + List> rooms = (List>) floorData.get("rooms"); + + if (rooms == null || rooms.isEmpty()) { + continue; + } + + // 添加楼层标题 + XSSFRow floorRow = sheet.createRow(currentRow++); + XSSFCell floorCell = floorRow.createCell(0); + floorCell.setCellValue("第" + floorNum + "层"); + floorCell.setCellStyle(headerStyle); + sheet.addMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, 0, 19)); + currentRow++; // 空行 + + // 按4个宿舍一行进行分组处理 + for (int i = 0; i < rooms.size(); i += 4) { + int endIndex = Math.min(i + 4, rooms.size()); + List> roomGroup = rooms.subList(i, endIndex); + + // 处理当前组的宿舍 + currentRow = processRoomGroup(sheet, roomGroup, currentRow, roomNameStyle, headerStyle, + dataStyle); + currentRow += 2; // 组间空行 + } + } + + // 设置列宽 (4个宿舍,每个宿舍4列,宿舍间3个空列,共19列) + for (int i = 0; i < 20; i++) { + sheet.setColumnWidth(i, 15 * 256); // 15个字符宽度 + } + + // 设置响应头 + String fileName = buildingName + "_住宿清单_" + System.currentTimeMillis() + ".xlsx"; + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", + "attachment; filename=\"" + new String(fileName.getBytes("UTF-8"), "ISO-8859-1") + "\""); + + // 输出Excel文件 + try (OutputStream outputStream = response.getOutputStream()) { + workbook.write(outputStream); + outputStream.flush(); + } + } + + } catch (Exception e) { + log.error("导出住宿清单失败", e); + throw new RuntimeException("导出住宿清单失败", e); + } + } + + /** + * 处理一组宿舍(最多4个) + */ + private int processRoomGroup(XSSFSheet sheet, List> roomGroup, int startRow, + CellStyle roomNameStyle, CellStyle headerStyle, CellStyle dataStyle) { + int currentRow = startRow; + + // 宿舍名称行 + XSSFRow roomNameRow = sheet.createRow(currentRow++); + for (int i = 0; i < roomGroup.size(); i++) { + Map room = roomGroup.get(i); + String roomName = (String) room.get("roomName"); + + // 每个宿舍占4列,宿舍之间增加1列间隔 (i * 5) + int startCol = i * 5; + + // 为整个合并区域的所有单元格创建并设置样式 + for (int col = startCol; col <= startCol + 3; col++) { + XSSFCell cell = roomNameRow.createCell(col); + cell.setCellStyle(roomNameStyle); + if (col == startCol) { + cell.setCellValue(roomName); // 只在第一个单元格设置值 + } + } + + sheet.addMergedRegion(new CellRangeAddress(currentRow - 1, currentRow - 1, startCol, startCol + 3)); + } + + // 表头行 + XSSFRow headerRow = sheet.createRow(currentRow++); + String[] headers = { "床位", "姓名", "班级", "备注" }; + for (int i = 0; i < roomGroup.size(); i++) { + for (int j = 0; j < headers.length; j++) { + // 每个宿舍占4列,宿舍之间增加1列间隔 (i * 5) + XSSFCell headerCell = headerRow.createCell(i * 5 + j); + headerCell.setCellValue(headers[j]); + headerCell.setCellStyle(headerStyle); + } + } + + // 计算最大床位数 + int maxBeds = roomGroup.stream() + .mapToInt(room -> { + @SuppressWarnings("unchecked") + List> beds = (List>) room.get("beds"); + return beds != null ? beds.size() : 0; + }) + .max() + .orElse(0); + + // 床位数据行 + for (int bedIndex = 0; bedIndex < maxBeds; bedIndex++) { + XSSFRow dataRow = sheet.createRow(currentRow++); + + for (int roomIndex = 0; roomIndex < roomGroup.size(); roomIndex++) { + Map room = roomGroup.get(roomIndex); + @SuppressWarnings("unchecked") + List> beds = (List>) room.get("beds"); + + if (beds != null && bedIndex < beds.size()) { + Map bed = beds.get(bedIndex); + + // 每个宿舍占4列,宿舍之间增加1列间隔 (roomIndex * 5) + // 床位 + XSSFCell bedNameCell = dataRow.createCell(roomIndex * 5); + bedNameCell.setCellValue((String) bed.get("bedName")); + bedNameCell.setCellStyle(dataStyle); + + // 姓名 + XSSFCell studentNameCell = dataRow.createCell(roomIndex * 5 + 1); + studentNameCell.setCellValue((String) bed.get("studentName")); + studentNameCell.setCellStyle(dataStyle); + + // 班级 + XSSFCell classNameCell = dataRow.createCell(roomIndex * 5 + 2); + classNameCell.setCellValue((String) bed.get("className")); + classNameCell.setCellStyle(dataStyle); + + // 备注 + XSSFCell remarkCell = dataRow.createCell(roomIndex * 5 + 3); + remarkCell.setCellValue((String) bed.get("bedRemark")); + remarkCell.setCellStyle(dataStyle); + } else { + // 空床位 + for (int j = 0; j < 4; j++) { + XSSFCell emptyCell = dataRow.createCell(roomIndex * 5 + j); + emptyCell.setCellValue(""); + emptyCell.setCellStyle(dataStyle); + } + } + } + } + + return currentRow; + } + + /** + * 创建标题样式 + */ + private CellStyle createTitleStyle(XSSFWorkbook workbook) { + CellStyle style = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setBold(true); + font.setFontHeightInPoints((short) 16); + style.setFont(font); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + return style; + } + + /** + * 创建表头样式 + */ + private CellStyle createHeaderStyle(XSSFWorkbook workbook) { + CellStyle style = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setBold(true); + font.setFontHeightInPoints((short) 12); + style.setFont(font); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderTop(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + return style; + } + + /** + * 创建数据样式 + */ + private CellStyle createDataStyle(XSSFWorkbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderTop(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + return style; + } + + /** + * 创建宿舍名称样式 + */ + private CellStyle createRoomNameStyle(XSSFWorkbook workbook) { + CellStyle style = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setBold(true); + font.setFontHeightInPoints((short) 14); + style.setFont(font); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderTop(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + return style; + } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java index 93f8229e..fee36299 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHolidayStayServiceImpl.java @@ -21,6 +21,16 @@ import org.dromara.xg.service.IXgHolidayStayService; import java.util.List; import java.util.Map; import java.util.Collection; +import java.util.ArrayList; +import java.util.stream.Collectors; +import java.util.Set; +import java.util.TreeSet; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Calendar; +import java.text.SimpleDateFormat; +import java.util.Date; +import org.springframework.beans.BeanUtils; /** * 假期留宿管理Service业务层处理 @@ -90,15 +100,42 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService { lqw.like(StringUtils.isNotBlank(bo.getCampusName()), XgHolidayStay::getCampusName, bo.getCampusName()); lqw.eq(bo.getAreaId() != null, XgHolidayStay::getAreaId, bo.getAreaId()); lqw.like(StringUtils.isNotBlank(bo.getAreaName()), XgHolidayStay::getAreaName, bo.getAreaName()); - lqw.eq(bo.getBuildingId() != null, XgHolidayStay::getBuildingId, bo.getBuildingId()); + + // 楼栋ID过滤逻辑:支持单个楼栋ID和多个楼栋ID的查询 + if (bo.getBuildingId() != null) { + // 单个楼栋ID查询 + lqw.eq(XgHolidayStay::getBuildingId, bo.getBuildingId()); + } else if (params != null && params.containsKey("buildingIds")) { + // 多个楼栋ID查询,从params中获取buildingIds(逗号分隔的字符串) + String buildingIdsStr = (String) params.get("buildingIds"); + if (StringUtils.isNotBlank(buildingIdsStr)) { + String[] buildingIdArray = buildingIdsStr.split(","); + if (buildingIdArray.length > 0) { + List buildingIds = new ArrayList<>(); + for (String buildingIdStr : buildingIdArray) { + try { + Long buildingId = Long.parseLong(buildingIdStr.trim()); + buildingIds.add(buildingId); + } catch (NumberFormatException e) { + log.warn("无效的楼栋ID: {}", buildingIdStr); + } + } + if (!buildingIds.isEmpty()) { + lqw.in(XgHolidayStay::getBuildingId, buildingIds); + log.debug("添加多楼栋ID过滤条件: {}", buildingIds); + } + } + } + } + lqw.like(StringUtils.isNotBlank(bo.getBuildingName()), XgHolidayStay::getBuildingName, bo.getBuildingName()); lqw.eq(bo.getRoomId() != null, XgHolidayStay::getRoomId, bo.getRoomId()); lqw.like(StringUtils.isNotBlank(bo.getRoomName()), XgHolidayStay::getRoomName, bo.getRoomName()); lqw.eq(bo.getBedId() != null, XgHolidayStay::getBedId, bo.getBedId()); lqw.like(StringUtils.isNotBlank(bo.getBedName()), XgHolidayStay::getBedName, bo.getBedName()); lqw.eq(StringUtils.isNotBlank(bo.getHolidayType()), XgHolidayStay::getHolidayType, bo.getHolidayType()); - lqw.ge(bo.getApplyStartDate() != null, XgHolidayStay::getApplyStartDate, bo.getApplyStartDate()); - lqw.le(bo.getApplyEndDate() != null, XgHolidayStay::getApplyEndDate, bo.getApplyEndDate()); + lqw.eq(bo.getApplyStartDate() != null, XgHolidayStay::getApplyStartDate, bo.getApplyStartDate()); + lqw.eq(bo.getApplyEndDate() != null, XgHolidayStay::getApplyEndDate, bo.getApplyEndDate()); lqw.like(StringUtils.isNotBlank(bo.getStayReason()), XgHolidayStay::getStayReason, bo.getStayReason()); lqw.eq(StringUtils.isNotBlank(bo.getStayReasonType()), XgHolidayStay::getStayReasonType, bo.getStayReasonType()); @@ -110,9 +147,12 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService { lqw.eq(StringUtils.isNotBlank(bo.getParentConsent()), XgHolidayStay::getParentConsent, bo.getParentConsent()); lqw.like(StringUtils.isNotBlank(bo.getAttachmentOssIds()), XgHolidayStay::getAttachmentOssIds, bo.getAttachmentOssIds()); - lqw.eq(StringUtils.isNotBlank(bo.getExtField1()), XgHolidayStay::getExtField1, bo.getExtField1()); - lqw.eq(StringUtils.isNotBlank(bo.getExtField2()), XgHolidayStay::getExtField2, bo.getExtField2()); - lqw.eq(StringUtils.isNotBlank(bo.getExtField3()), XgHolidayStay::getExtField3, bo.getExtField3()); + lqw.like(StringUtils.isNotBlank(bo.getExtField1()), XgHolidayStay::getExtField1, bo.getExtField1()); + lqw.like(StringUtils.isNotBlank(bo.getExtField2()), XgHolidayStay::getExtField2, bo.getExtField2()); + lqw.like(StringUtils.isNotBlank(bo.getExtField3()), XgHolidayStay::getExtField3, bo.getExtField3()); + + // 添加创建人过滤条件 - 实现班主任权限控制,只能查看自己创建的数据 + lqw.eq(bo.getCreateBy() != null, XgHolidayStay::getCreateBy, bo.getCreateBy()); // 添加状态过滤逻辑 addStatusFilter(lqw, bo); @@ -122,7 +162,7 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService { /** * 添加状态过滤条件 - * + * * @param lqw 查询条件构造器 * @param bo 查询参数对象 */ @@ -134,17 +174,19 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService { if (StringUtils.isNotBlank(statusType) && StringUtils.isNotBlank(currentTime)) { if ("valid".equals(statusType)) { - // 有效状态:结束时间 >= 当前时间 AND 结束时间不为空 + // 有效状态:当前时间还没有超过留宿结束时间(包括预约状态) + // 条件:applyEndDate >= currentTime AND applyEndDate IS NOT NULL lqw.ge(XgHolidayStay::getApplyEndDate, currentTime) .isNotNull(XgHolidayStay::getApplyEndDate); - log.debug("添加有效状态过滤条件: apply_end_date >= {} AND apply_end_date IS NOT NULL", currentTime); + log.debug("添加有效状态过滤条件: apply_end_date >= {} AND end_date is not null", currentTime); } else if ("invalid".equals(statusType)) { - // 失效状态:结束时间 < 当前时间 OR 结束时间为空 + // 失效状态:当前时间已超过留宿结束时间 + // 条件:applyEndDate < currentTime OR applyEndDate IS NULL lqw.and(wrapper -> wrapper .lt(XgHolidayStay::getApplyEndDate, currentTime) .or() .isNull(XgHolidayStay::getApplyEndDate)); - log.debug("添加失效状态过滤条件: apply_end_date < {} OR apply_end_date IS NULL", currentTime); + log.debug("添加失效状态过滤条件: apply_end_date < {} OR end_date is null", currentTime); } } } @@ -214,4 +256,503 @@ public class XgHolidayStayServiceImpl implements IXgHolidayStayService { Page result = baseMapper.selectStudentOptions(pageQuery.build(), bo); return TableDataInfo.build(result); } + + /** + * 查询单人留宿有效数据 + * 获取所有有效的假期留宿数据,筛选出在同一时间段内该宿舍只有一个学生留宿的记录 + * + * @return 单人留宿有效数据列表 + */ + @Override + public List querySinglePersonValidStay() { + // 1. 获取所有有效的假期留宿数据 + XgHolidayStayBo bo = new XgHolidayStayBo(); + Map params = bo.getParams(); + if (params == null) { + params = new HashMap<>(); + bo.setParams(params); + } + + // 设置查询有效状态的留宿数据 + String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); + params.put("statusType", "valid"); + params.put("currentTime", currentTime); + + List validStayList = queryList(bo); + log.info("查询到有效的假期留宿数据总数: {}", validStayList.size()); + + if (validStayList.isEmpty()) { + return new ArrayList<>(); + } + + // 2. 按房间分组,筛选出单人留宿的数据 + List singlePersonStayList = new ArrayList<>(); + + // 按房间ID分组 + Map> roomGroupMap = validStayList.stream() + .filter(stay -> stay.getRoomId() != null) + .collect(Collectors.groupingBy(XgHolidayStayVo::getRoomId)); + + log.info("按房间分组后,涉及房间数量: {}", roomGroupMap.size()); + + // 遍历每个房间的留宿数据 + for (Map.Entry> entry : roomGroupMap.entrySet()) { + Long roomId = entry.getKey(); + List roomStayList = entry.getValue(); + + log.debug("处理房间ID: {}, 该房间留宿人数: {}", roomId, roomStayList.size()); + + // 3. 对于每个房间,检查是否存在单人留宿的时间段 + List singlePersonInRoom = findSinglePersonPeriodsInRoom(roomStayList); + singlePersonStayList.addAll(singlePersonInRoom); + } + + log.info("筛选出单人留宿有效数据总数: {}", singlePersonStayList.size()); + + // 按ID倒序排序(作为时间排序的替代) + return singlePersonStayList.stream() + .sorted((a, b) -> { + if (a.getId() == null && b.getId() == null) + return 0; + if (a.getId() == null) + return 1; + if (b.getId() == null) + return -1; + return b.getId().compareTo(a.getId()); + }) + .collect(Collectors.toList()); + } + + /** + * 检查房间内是否存在单人留宿的时间段 + * + * @param roomStayList 同一房间内的留宿数据列表 + * @return 单人留宿的数据列表 + */ + private List findSinglePersonPeriodsInRoom(List roomStayList) { + List singlePersonList = new ArrayList<>(); + + if (roomStayList == null || roomStayList.isEmpty()) { + return singlePersonList; + } + + // 如果房间内只有一个人留宿,直接返回 + if (roomStayList.size() == 1) { + singlePersonList.add(roomStayList.get(0)); + log.debug("房间 {} 只有一个人留宿: {}", roomStayList.get(0).getRoomName(), roomStayList.get(0).getStudentName()); + return singlePersonList; + } + + // 按学生分组,处理每个学生的多个留宿记录 + Map> studentStaysMap = roomStayList.stream() + .collect(Collectors.groupingBy(XgHolidayStayVo::getStudentId)); + + // 为每个学生创建时间段 + Map> studentTimeSegments = new HashMap<>(); + for (Map.Entry> entry : studentStaysMap.entrySet()) { + Long studentId = entry.getKey(); + List stays = entry.getValue(); + + List segments = new ArrayList<>(); + for (XgHolidayStayVo stay : stays) { + if (stay.getApplyStartDate() != null && stay.getApplyEndDate() != null) { + segments.add(new TimeSegment(stay.getApplyStartDate(), stay.getApplyEndDate(), studentId, stay)); + } + } + + // 合并同一学生的重叠时间段 + List mergedSegments = mergeTimeSegments(segments); + studentTimeSegments.put(studentId, mergedSegments); + } + + // 收集所有关键时间点 + Set keyTimePoints = new TreeSet<>(); + for (List segments : studentTimeSegments.values()) { + for (TimeSegment segment : segments) { + keyTimePoints.add(segment.getStartTime()); + keyTimePoints.add(segment.getEndTime()); + } + } + + List sortedTimePoints = new ArrayList<>(keyTimePoints); + log.debug("房间 {} 关键时间点数量: {}", roomStayList.get(0).getRoomName(), sortedTimePoints.size()); + + // 分析每个时间段内的留宿人数 + for (int i = 0; i < sortedTimePoints.size() - 1; i++) { + Date segmentStart = sortedTimePoints.get(i); + Date segmentEnd = sortedTimePoints.get(i + 1); + + // 找出在这个时间段内留宿的所有学生 + List studentsInSegment = new ArrayList<>(); + Map studentStayMap = new HashMap<>(); + + for (Map.Entry> entry : studentTimeSegments.entrySet()) { + Long studentId = entry.getKey(); + List segments = entry.getValue(); + + for (TimeSegment segment : segments) { + // 检查时间段是否重叠(注意边界条件) + if (segment.getStartTime().compareTo(segmentEnd) < 0 && + segment.getEndTime().compareTo(segmentStart) > 0) { + studentsInSegment.add(studentId); + studentStayMap.put(studentId, segment.getOriginalStay()); + break; // 一个学生在同一时间段内只能有一个记录 + } + } + } + + // 如果只有一个学生,这是单人留宿时间段 + if (studentsInSegment.size() == 1) { + Long singleStudentId = studentsInSegment.get(0); + XgHolidayStayVo originalStay = studentStayMap.get(singleStudentId); + + // 创建单人留宿记录 + XgHolidayStayVo singlePersonStay = createSinglePersonStayFromSegment( + originalStay, segmentStart, segmentEnd); + singlePersonList.add(singlePersonStay); + + log.debug("找到单人留宿时间段: {} ({} - {}) 学生: {}", + originalStay.getRoomName(), + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(segmentStart), + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(segmentEnd), + originalStay.getStudentName()); + } + } + + // 合并同一学生的连续单人留宿时间段 + return mergeConsecutiveSinglePersonStays(singlePersonList); + } + + /** + * 获取两个日期之间的所有日期(包含起始和结束日期) + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param dateFormat 日期格式化器 + * @return 日期字符串列表 + */ + private List getDatesBetween(Date startDate, Date endDate, SimpleDateFormat dateFormat) { + List dates = new ArrayList<>(); + + if (startDate == null || endDate == null) { + return dates; + } + + Calendar calendar = Calendar.getInstance(); + calendar.setTime(startDate); + + // 只取日期部分,忽略时间 + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + + Calendar endCalendar = Calendar.getInstance(); + endCalendar.setTime(endDate); + endCalendar.set(Calendar.HOUR_OF_DAY, 0); + endCalendar.set(Calendar.MINUTE, 0); + endCalendar.set(Calendar.SECOND, 0); + endCalendar.set(Calendar.MILLISECOND, 0); + + // 生成日期列表 + while (!calendar.after(endCalendar)) { + dates.add(dateFormat.format(calendar.getTime())); + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + + return dates; + } + + /** + * 创建单人留宿记录,调整时间范围为单人留宿的日期范围 + * + * @param originalStay 原始留宿记录 + * @param singlePersonDates 单人留宿的日期列表 + * @param dateFormat 日期格式化器 + * @return 调整后的单人留宿记录 + */ + private XgHolidayStayVo createSinglePersonStayRecord(XgHolidayStayVo originalStay, List singlePersonDates, + SimpleDateFormat dateFormat) { + if (singlePersonDates.isEmpty()) { + return originalStay; + } + + try { + // 排序日期列表 + singlePersonDates.sort(String::compareTo); + + // 获取单人留宿的开始和结束日期 + String startDateStr = singlePersonDates.get(0); + String endDateStr = singlePersonDates.get(singlePersonDates.size() - 1); + + // 克隆原始记录 + XgHolidayStayVo singlePersonStay = new XgHolidayStayVo(); + BeanUtils.copyProperties(originalStay, singlePersonStay); + + // 调整时间范围为单人留宿的日期范围 + Date adjustedStartDate = dateFormat.parse(startDateStr); + Date adjustedEndDate = dateFormat.parse(endDateStr); + + // 结束日期设置为当天的23:59:59 + Calendar endCalendar = Calendar.getInstance(); + endCalendar.setTime(adjustedEndDate); + endCalendar.set(Calendar.HOUR_OF_DAY, 23); + endCalendar.set(Calendar.MINUTE, 59); + endCalendar.set(Calendar.SECOND, 59); + adjustedEndDate = endCalendar.getTime(); + + singlePersonStay.setApplyStartDate(adjustedStartDate); + singlePersonStay.setApplyEndDate(adjustedEndDate); + + // 在备注中添加单人留宿信息 + String originalRemark = singlePersonStay.getRemark() == null ? "" : singlePersonStay.getRemark(); + String singlePersonInfo = String.format("【单人留宿】%s天(%s至%s)", + singlePersonDates.size(), startDateStr, endDateStr); + singlePersonStay + .setRemark(originalRemark.isEmpty() ? singlePersonInfo : originalRemark + " " + singlePersonInfo); + + log.debug("为学生 {} 创建单人留宿记录: {} 天 ({} - {})", + singlePersonStay.getStudentName(), singlePersonDates.size(), startDateStr, endDateStr); + + return singlePersonStay; + } catch (Exception e) { + log.error("创建单人留宿记录失败", e); + return originalStay; + } + } + + /** + * 检查两个留宿时间段是否重叠 + * + * @param stay1 留宿记录1 + * @param stay2 留宿记录2 + * @return 是否重叠 + */ + private boolean isTimeOverlap(XgHolidayStayVo stay1, XgHolidayStayVo stay2) { + if (stay1.getApplyStartDate() == null || stay1.getApplyEndDate() == null || + stay2.getApplyStartDate() == null || stay2.getApplyEndDate() == null) { + return false; + } + + Date start1 = stay1.getApplyStartDate(); + Date end1 = stay1.getApplyEndDate(); + Date start2 = stay2.getApplyStartDate(); + Date end2 = stay2.getApplyEndDate(); + + // 检查时间段是否重叠:两个时间段重叠当且仅当 start1 <= end2 && start2 <= end1 + boolean overlap = start1.compareTo(end2) <= 0 && start2.compareTo(end1) <= 0; + + if (overlap) { + log.debug("时间段重叠: {} ({} - {}) 与 {} ({} - {})", + stay1.getStudentName(), start1, end1, + stay2.getStudentName(), start2, end2); + } + + return overlap; + } + + /** + * 合并同一学生的重叠时间段 + */ + private List mergeTimeSegments(List segments) { + if (segments.isEmpty()) { + return new ArrayList<>(); + } + + // 按开始时间排序 + segments.sort(Comparator.comparing(TimeSegment::getStartTime)); + + List merged = new ArrayList<>(); + TimeSegment current = segments.get(0); + + for (int i = 1; i < segments.size(); i++) { + TimeSegment next = segments.get(i); + + // 如果时间段重叠或相邻,合并它们 + if (current.getEndTime().compareTo(next.getStartTime()) >= 0) { + // 扩展当前时间段 + if (next.getEndTime().compareTo(current.getEndTime()) > 0) { + current = new TimeSegment( + current.getStartTime(), + next.getEndTime(), + current.getStudentId(), + current.getOriginalStay() + ); + } + } else { + // 不重叠,添加当前时间段到结果中 + merged.add(current); + current = next; + } + } + + // 添加最后一个时间段 + merged.add(current); + return merged; + } + + /** + * 从时间段创建单人留宿记录 + */ + private XgHolidayStayVo createSinglePersonStayFromSegment(XgHolidayStayVo template, Date startTime, Date endTime) { + XgHolidayStayVo singlePersonStay = new XgHolidayStayVo(); + + // 复制基本信息 + BeanUtils.copyProperties(template, singlePersonStay); + + // 设置ID为null,表示这是计算出来的记录 + singlePersonStay.setId(null); + + // 设置单人留宿的时间段 + singlePersonStay.setApplyStartDate(startTime); + singlePersonStay.setApplyEndDate(endTime); + + // 计算单人留宿天数 + long timeDiff = endTime.getTime() - startTime.getTime(); + long days = Math.max(1, timeDiff / (24 * 60 * 60 * 1000) + 1); // 至少1天 + + // 设置备注信息 + String originalRemark = template.getRemark() != null ? template.getRemark() : ""; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String singlePersonRemark = String.format("单人留宿%d天 (%s至%s)", + days, + sdf.format(startTime), + sdf.format(endTime) + ); + + if (!originalRemark.isEmpty()) { + singlePersonStay.setRemark(originalRemark + " | " + singlePersonRemark); + } else { + singlePersonStay.setRemark(singlePersonRemark); + } + + return singlePersonStay; + } + + /** + * 合并同一学生的连续单人留宿时间段 + */ + private List mergeConsecutiveSinglePersonStays(List singlePersonStays) { + if (singlePersonStays.isEmpty()) { + return new ArrayList<>(); + } + + // 按学生ID和开始时间分组排序 + Map> studentStaysMap = singlePersonStays.stream() + .collect(Collectors.groupingBy(XgHolidayStayVo::getStudentId)); + + List mergedStays = new ArrayList<>(); + + for (Map.Entry> entry : studentStaysMap.entrySet()) { + List stays = entry.getValue(); + + // 按开始时间排序 + stays.sort(Comparator.comparing(XgHolidayStayVo::getApplyStartDate)); + + XgHolidayStayVo current = stays.get(0); + + for (int i = 1; i < stays.size(); i++) { + XgHolidayStayVo next = stays.get(i); + + // 如果时间段连续或重叠,合并它们 + if (current.getApplyEndDate().compareTo(next.getApplyStartDate()) >= 0) { + // 扩展当前时间段 + if (next.getApplyEndDate().compareTo(current.getApplyEndDate()) > 0) { + current.setApplyEndDate(next.getApplyEndDate()); + } + + // 更新备注信息 + updateMergedRemark(current, next); + } else { + // 不连续,添加当前时间段到结果中 + mergedStays.add(current); + current = next; + } + } + + // 添加最后一个时间段 + mergedStays.add(current); + } + + return mergedStays; + } + + /** + * 更新合并后的备注信息 + */ + private void updateMergedRemark(XgHolidayStayVo current, XgHolidayStayVo next) { + // 重新计算合并后的天数 + long timeDiff = current.getApplyEndDate().getTime() - current.getApplyStartDate().getTime(); + long totalDays = Math.max(1, timeDiff / (24 * 60 * 60 * 1000) + 1); + + // 更新备注 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String mergedRemark = String.format("单人留宿%d天 (%s至%s)", + totalDays, + sdf.format(current.getApplyStartDate()), + sdf.format(current.getApplyEndDate()) + ); + + // 保留原始备注的非单人留宿信息 + String originalPart = extractOriginalRemark(current.getRemark()); + if (!originalPart.isEmpty()) { + current.setRemark(originalPart + " | " + mergedRemark); + } else { + current.setRemark(mergedRemark); + } + } + + /** + * 提取原始备注信息(去除单人留宿相关的备注) + */ + private String extractOriginalRemark(String remark) { + if (remark == null || remark.isEmpty()) { + return ""; + } + + // 如果包含单人留宿标记,提取原始部分 + if (remark.contains(" | 单人留宿")) { + return remark.split(" \\| 单人留宿")[0].trim(); + } + + // 如果以单人留宿开头,返回空 + if (remark.startsWith("单人留宿")) { + return ""; + } + + return remark; + } + + /** + * 时间段内部类 + */ + private static class TimeSegment { + private Date startTime; + private Date endTime; + private Long studentId; + private XgHolidayStayVo originalStay; + + public TimeSegment(Date startTime, Date endTime, Long studentId, XgHolidayStayVo originalStay) { + this.startTime = startTime; + this.endTime = endTime; + this.studentId = studentId; + this.originalStay = originalStay; + } + + public Date getStartTime() { + return startTime; + } + + public Date getEndTime() { + return endTime; + } + + public Long getStudentId() { + return studentId; + } + + public XgHolidayStayVo getOriginalStay() { + return originalStay; + } + } } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHygieneCheckViolationServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHygieneCheckViolationServiceImpl.java new file mode 100644 index 00000000..7a683f91 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgHygieneCheckViolationServiceImpl.java @@ -0,0 +1,193 @@ +package org.dromara.xg.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 lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.dromara.xg.domain.bo.XgHygieneCheckViolationBo; +import org.dromara.xg.domain.vo.XgHygieneCheckViolationVo; +import org.dromara.xg.domain.XgHygieneCheckViolation; +import org.dromara.xg.mapper.XgHygieneCheckViolationMapper; +import org.dromara.xg.service.IXgHygieneCheckViolationService; + +import java.util.List; +import java.util.Collection; + +/** + * 宿舍卫生检查违纪记录Service业务层处理 + * + * @author zlwu + * @date 2025-01-29 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class XgHygieneCheckViolationServiceImpl implements IXgHygieneCheckViolationService { + + private final XgHygieneCheckViolationMapper baseMapper; + + /** + * 查询宿舍卫生检查违纪记录 + * + * @param id 主键 + * @return 宿舍卫生检查违纪记录 + */ + @Override + public XgHygieneCheckViolationVo queryById(Long id) { + return baseMapper.selectVoById(id); + } + + /** + * 分页查询宿舍卫生检查违纪记录列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 宿舍卫生检查违纪记录分页列表 + */ + @Override + public TableDataInfo queryPageList(XgHygieneCheckViolationBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询符合条件的宿舍卫生检查违纪记录列表 + * + * @param bo 查询条件 + * @return 宿舍卫生检查违纪记录列表 + */ + @Override + public List queryList(XgHygieneCheckViolationBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(XgHygieneCheckViolationBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(bo.getHygieneCheckId() != null, XgHygieneCheckViolation::getHygieneCheckId, bo.getHygieneCheckId()); + lqw.eq(bo.getStudentId() != null, XgHygieneCheckViolation::getStudentId, bo.getStudentId()); + lqw.like(StringUtils.isNotBlank(bo.getStudentNo()), XgHygieneCheckViolation::getStudentNo, bo.getStudentNo()); + lqw.like(StringUtils.isNotBlank(bo.getStudentName()), XgHygieneCheckViolation::getStudentName, + bo.getStudentName()); + lqw.eq(bo.getDisciplineTypeId() != null, XgHygieneCheckViolation::getDisciplineTypeId, + bo.getDisciplineTypeId()); + lqw.like(StringUtils.isNotBlank(bo.getDisciplineTypeName()), XgHygieneCheckViolation::getDisciplineTypeName, + bo.getDisciplineTypeName()); + lqw.eq(StringUtils.isNotBlank(bo.getViolationSource()), XgHygieneCheckViolation::getViolationSource, + bo.getViolationSource()); + lqw.eq(StringUtils.isNotBlank(bo.getStatus()), XgHygieneCheckViolation::getStatus, bo.getStatus()); + return lqw; + } + + /** + * 新增宿舍卫生检查违纪记录 + * + * @param bo 宿舍卫生检查违纪记录 + * @return 是否新增成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean insertByBo(XgHygieneCheckViolationBo bo) { + XgHygieneCheckViolation add = MapstructUtils.convert(bo, XgHygieneCheckViolation.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改宿舍卫生检查违纪记录 + * + * @param bo 宿舍卫生检查违纪记录 + * @return 是否修改成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean updateByBo(XgHygieneCheckViolationBo bo) { + XgHygieneCheckViolation update = MapstructUtils.convert(bo, XgHygieneCheckViolation.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + * + * @param entity 实体类数据 + */ + private void validEntityBeforeSave(XgHygieneCheckViolation entity) { + // TODO 做一些数据校验,如唯一约束 + } + + /** + * 校验并批量删除宿舍卫生检查违纪记录信息 + * + * @param ids 待删除的主键集合 + * @param isValid 是否进行有效性校验 + * @return 是否删除成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 批量新增宿舍卫生检查违纪记录 + * + * @param bos 违纪记录列表 + * @return 是否新增成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean insertBatch(List bos) { + if (bos == null || bos.isEmpty()) { + return true; + } + + List entities = MapstructUtils.convert(bos, XgHygieneCheckViolation.class); + for (XgHygieneCheckViolation entity : entities) { + validEntityBeforeSave(entity); + } + + return baseMapper.insertBatch(entities); + } + + /** + * 根据卫生检查ID查询违纪记录列表 + * + * @param hygieneCheckId 卫生检查ID + * @return 违纪记录列表 + */ + @Override + public List queryByHygieneCheckId(Long hygieneCheckId) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(XgHygieneCheckViolation::getHygieneCheckId, hygieneCheckId); + return baseMapper.selectVoList(lqw); + } + + /** + * 根据卫生检查ID删除违纪关联记录 + * + * @param hygieneCheckId 卫生检查ID + * @return 是否删除成功 + */ + @Override + public Boolean deleteByHygieneCheckId(Long hygieneCheckId) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(XgHygieneCheckViolation::getHygieneCheckId, hygieneCheckId); + return baseMapper.delete(lqw) > 0; + } +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java index 93f6cb80..b38d24c7 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java @@ -51,6 +51,7 @@ import org.dromara.xg.domain.XgStudentManager; import org.dromara.xg.domain.XgStudentManagerSource; import org.dromara.xg.domain.XgClassMsg; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.Collection; @@ -582,4 +583,37 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService { baseMapper.updateById(xgStudentLeave); } + /** + * 根据学生ID列表和检查日期查询学生请假信息 + * + * @param studentIds 学生ID列表 + * @param checkDate 检查日期 + * @return 学生ID -> 请假信息的映射 + */ + @Override + public Map queryStudentLeavesByCheckDate(List studentIds, LocalDateTime checkDate) { + if (studentIds == null || studentIds.isEmpty() || checkDate == null) { + return Map.of(); + } + + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.in(XgStudentLeave::getStudentId, studentIds); + // 查询审核状态为"waiting"(待审核)或"finish"(已完成)的请假记录 + lqw.in(XgStudentLeave::getApprovalStatus, "waiting", "finish"); + // 检查日期在请假时间段内 + lqw.le(XgStudentLeave::getStartTime, checkDate); + lqw.ge(XgStudentLeave::getEndTime, checkDate); + lqw.orderByDesc(XgStudentLeave::getCreateTime); + + List leaveList = baseMapper.selectList(lqw); + + // 转换为VO并按学生ID分组,每个学生只取最新的一条请假记录 + return leaveList.stream() + .collect(Collectors.toMap( + XgStudentLeave::getStudentId, + leave -> MapstructUtils.convert(leave, XgStudentLeaveVo.class), + (existing, replacement) -> existing // 保留第一条(最新的) + )); + } + } diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentStatisticsServiceImpl.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentStatisticsServiceImpl.java new file mode 100644 index 00000000..7c11c5a3 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentStatisticsServiceImpl.java @@ -0,0 +1,117 @@ +package org.dromara.xg.service.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import jakarta.servlet.http.HttpServletResponse; +import org.dromara.common.excel.utils.ExcelUtil; +import org.dromara.xg.service.IXgStudentStatisticsService; +import org.dromara.xg.domain.bo.XgStudentStatisticsBo; +import org.dromara.xg.domain.vo.XgStudentStatisticsVo; +import org.dromara.xg.domain.vo.XgCascadeOptionVo; +import org.dromara.xg.mapper.XgStudentStatisticsMapper; + +import java.util.List; +import java.util.Map; + +/** + * 学生宿舍卫生检查、违纪记录统计Service业务层处理 + * + * @author zlwu + * @date 2025-01-14 + */ +@RequiredArgsConstructor +@Service +public class XgStudentStatisticsServiceImpl implements IXgStudentStatisticsService { + + private final XgStudentStatisticsMapper statisticsMapper; + + @Override + public XgStudentStatisticsVo getStatistics(XgStudentStatisticsBo bo) { + XgStudentStatisticsVo result = new XgStudentStatisticsVo(); + + // 确定统计维度 + String dimension = determineDimension(bo); + result.setDimension(dimension); + + // 获取图表数据和明细数据 + switch (dimension) { + case "college": + result.setChartData(statisticsMapper.getCollegeStatistics(bo)); + result.setDetailData(statisticsMapper.getCollegeDetailStatistics(bo)); + break; + case "major": + result.setChartData(statisticsMapper.getMajorStatistics(bo)); + result.setDetailData(statisticsMapper.getMajorDetailStatistics(bo)); + break; + case "class": + result.setChartData(statisticsMapper.getClassStatistics(bo)); + result.setDetailData(statisticsMapper.getClassDetailStatistics(bo)); + break; + case "classDetail": + // 班级详情统计:图表数据和明细数据都只显示指定班级 + List detailData = statisticsMapper + .getClassDetailStatistics(bo); + result.setDetailData(detailData); + // 将明细数据转换为图表数据格式 + result.setChartData(convertDetailToChartData(detailData)); + break; + } + + return result; + } + + @Override + public void exportStatistics(XgStudentStatisticsBo bo, HttpServletResponse response) { + XgStudentStatisticsVo statistics = getStatistics(bo); + ExcelUtil.exportExcel(statistics.getDetailData(), "学生统计数据", XgStudentStatisticsVo.StatisticsDetailData.class, + response); + } + + @Override + public List getColleges() { + return statisticsMapper.getColleges(); + } + + @Override + public List getMajorsByCollegeId(Long collegeId) { + return statisticsMapper.getMajorsByCollegeId(collegeId); + } + + @Override + public List getClassesByMajorId(Long majorId) { + return statisticsMapper.getClassesByMajorId(majorId); + } + + /** + * 确定统计维度 + */ + private String determineDimension(XgStudentStatisticsBo bo) { + if (bo.getClassId() != null) { + return "classDetail"; + } else if (bo.getMajorId() != null) { + return "class"; + } else if (bo.getCollegeId() != null) { + return "major"; + } else { + return "college"; + } + } + + /** + * 将明细数据转换为图表数据格式 + */ + private List convertDetailToChartData( + List detailData) { + return detailData.stream().map(detail -> { + XgStudentStatisticsVo.StatisticsChartData chartData = new XgStudentStatisticsVo.StatisticsChartData(); + chartData.setName(detail.getName()); + chartData.setDormDeductionCount(detail.getDormDeductionCount()); + chartData.setDormViolationCount(detail.getDormViolationCount()); + chartData.setDormViolationDeduction(detail.getDormViolationDeduction()); + chartData.setTeachingViolationCount(detail.getTeachingViolationCount()); + chartData.setTeachingViolationDeduction(detail.getTeachingViolationDeduction()); + return chartData; + }).collect(java.util.stream.Collectors.toList()); + } + +} diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgHygieneCheckViolationMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgHygieneCheckViolationMapper.xml new file mode 100644 index 00000000..d2a96137 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/dormHygieneCheck/XgHygieneCheckViolationMapper.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT + hcv.id, + hcv.hygiene_check_id, + hcv.discipline_record_id, + hcv.student_id, + hcv.student_no, + hcv.student_name, + hcv.discipline_type_id, + hcv.discipline_type_name, + hcv.violation_source, + hcv.status, + hcv.create_by, + hcv.create_time, + hcv.update_by, + hcv.update_time, + -- 关联违纪详情 + dr.discipline_date, + dr.discipline_reason, + dr.discipline_content, + dr.score_deduction, + dr.attachment_oss_ids + FROM xg_hygiene_check_violation hcv + LEFT JOIN xskh_discipline_record dr ON hcv.discipline_record_id = dr.id AND dr.deleted = 0 + + + + + + + + + + diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgStudentStatisticsMapper.xml b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgStudentStatisticsMapper.xml new file mode 100644 index 00000000..3585dcf8 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgStudentStatisticsMapper.xml @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/controller/XskhDisciplineRevokeController.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/controller/XskhDisciplineRevokeController.java index 5739870a..0f140299 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/controller/XskhDisciplineRevokeController.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/controller/XskhDisciplineRevokeController.java @@ -134,17 +134,6 @@ public class XskhDisciplineRevokeController extends BaseController { return toAjax(xskhDisciplineRevokeService.applyRevoke(bo)); } - /** - * 审核撤销申请 - */ - @SaCheckPermission("disciplineRevoke:disciplineRevoke:approve") - @Log(title = "审核撤销申请", businessType = BusinessType.UPDATE) - @RepeatSubmit() - @PutMapping("/approve") - public R approveRevoke(@Validated(EditGroup.class) @RequestBody XskhDisciplineRevokeBo bo) { - return toAjax(xskhDisciplineRevokeService.approveRevoke(bo)); - } - /** * 查询班主任班级的违纪撤销申请列表(班主任专用) */ @@ -155,15 +144,56 @@ public class XskhDisciplineRevokeController extends BaseController { return xskhDisciplineRevokeService.queryClassTeacherPageList(bo, pageQuery); } + /** + * 查询班主任已审核的违纪撤销申请列表(根据审核人查询历史记录) + */ + @SaCheckPermission("disciplineRevoke:classTeacher:list") + @GetMapping("/classTeacherReviewedList") + public TableDataInfo getClassTeacherReviewedRevokeApplications(XskhDisciplineRevokeBo bo, + PageQuery pageQuery) { + return xskhDisciplineRevokeService.queryClassTeacherReviewedPageList(bo, pageQuery); + } + /** * 班主任审核撤销申请 */ @SaCheckPermission("disciplineRevoke:classTeacher:approve") @Log(title = "班主任审核撤销申请", businessType = BusinessType.UPDATE) @RepeatSubmit() - @PutMapping("/classTeacherApprove") - public R classTeacherApproveRevoke(@Validated(EditGroup.class) @RequestBody XskhDisciplineRevokeBo bo) { - return toAjax(xskhDisciplineRevokeService.classTeacherApproveRevoke(bo)); + @PutMapping("/teacherApprove") + public R teacherApproveRevoke(@Validated(EditGroup.class) @RequestBody XskhDisciplineRevokeBo bo) { + return toAjax(xskhDisciplineRevokeService.teacherApproveRevoke(bo)); + } + + /** + * 查询学工处待审核的撤销申请列表(学工处专用) + */ + @SaCheckPermission("disciplineRevoke:studentAffairs:list") + @GetMapping("/studentAffairsList") + public TableDataInfo getStudentAffairsRevokeApplications(XskhDisciplineRevokeBo bo, + PageQuery pageQuery) { + return xskhDisciplineRevokeService.queryStudentAffairsPageList(bo, pageQuery); + } + + /** + * 学工处审核撤销申请 + */ + @SaCheckPermission("disciplineRevoke:studentAffairs:approve") + @Log(title = "学工处审核撤销申请", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping("/studentAffairsApprove") + public R studentAffairsApproveRevoke(@Validated(EditGroup.class) @RequestBody XskhDisciplineRevokeBo bo) { + return toAjax(xskhDisciplineRevokeService.studentAffairsApproveRevoke(bo)); + } + + /** + * 查询学工处已审核的撤销申请列表(学工处专用) + */ + @SaCheckPermission("disciplineRevoke:studentAffairs:list") + @GetMapping("/studentAffairsReviewedList") + public TableDataInfo getStudentAffairsReviewedRevokeApplications(XskhDisciplineRevokeBo bo, + PageQuery pageQuery) { + return xskhDisciplineRevokeService.queryStudentAffairsReviewedPageList(bo, pageQuery); } } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/controller/XskhDisciplineTypeController.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/controller/XskhDisciplineTypeController.java index 4a345895..d89f777b 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/controller/XskhDisciplineTypeController.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/controller/XskhDisciplineTypeController.java @@ -63,8 +63,7 @@ public class XskhDisciplineTypeController extends BaseController { */ @SaCheckPermission("disciplineType:disciplineType:query") @GetMapping("/{id}") - public R getInfo(@NotNull(message = "主键不能为空") - @PathVariable Long id) { + public R getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) { return R.ok(xskhDisciplineTypeService.queryById(id)); } @@ -98,8 +97,27 @@ public class XskhDisciplineTypeController extends BaseController { @SaCheckPermission("disciplineType:disciplineType:remove") @Log(title = "学生违纪处分类型", businessType = BusinessType.DELETE) @DeleteMapping("/{ids}") - public R remove(@NotEmpty(message = "主键不能为空") - @PathVariable Long[] ids) { + public R remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) { return toAjax(xskhDisciplineTypeService.deleteWithValidByIds(List.of(ids), true)); } + + /** + * 根据区域类型查询学生违纪处分类型列表 + * + * @param areaType 区域类型(1-宿舍区违纪 2-教学区违纪) + * @param status 状态(0-正常 1-停用) + * @return 学生违纪处分类型列表 + */ + @SaCheckPermission("disciplineType:disciplineType:list") + @GetMapping("/listByAreaType") + public R> listByAreaType(@RequestParam String areaType, + @RequestParam(required = false) String status) { + XskhDisciplineTypeBo bo = new XskhDisciplineTypeBo(); + bo.setAreaType(areaType); + if (status != null) { + bo.setStatus(status); + } + List list = xskhDisciplineTypeService.queryList(bo); + return R.ok(list); + } } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhDisciplineRecord.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhDisciplineRecord.java index 83ccbd65..cbf51b08 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhDisciplineRecord.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhDisciplineRecord.java @@ -110,6 +110,11 @@ public class XskhDisciplineRecord extends TenantEntity { */ private String status; + /** + * 违纪来源(1-宿舍卫生检查 2-教学区违纪) + */ + private String violationSource; + /** * 附件OSS ID列表,逗号分隔 */ diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhDisciplineRevoke.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhDisciplineRevoke.java index 7a010b6e..e19d6aa2 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhDisciplineRevoke.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhDisciplineRevoke.java @@ -59,22 +59,37 @@ public class XskhDisciplineRevoke extends TenantEntity { private Date applyTime; /** - * 状态(1-待审核 2-审核通过 3-审核拒绝) + * 状态(1-待班主任审核 2-待学工处审核 3-审核通过 4-班主任审核拒绝 5-学工处审核拒绝) */ private String status; /** - * 审核人ID + * 班主任审核人ID + */ + private Long teacherApprovalBy; + + /** + * 班主任审核时间 + */ + private Date teacherApprovalTime; + + /** + * 班主任审核备注 + */ + private String teacherApprovalRemark; + + /** + * 学工处审核人ID */ private Long approvalBy; /** - * 审核时间 + * 学工处审核时间 */ private Date approvalTime; /** - * 审核备注 + * 学工处审核备注 */ private String approvalRemark; diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhDisciplineType.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhDisciplineType.java index 62342129..610a4a9b 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhDisciplineType.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhDisciplineType.java @@ -42,6 +42,31 @@ public class XskhDisciplineType extends TenantEntity { */ private String typeCategory; + /** + * 区域类型(1-宿舍区违纪 2-教学区违纪) + */ + private String areaType; + + /** + * 最低扣分 + */ + private java.math.BigDecimal minScore; + + /** + * 最高扣分 + */ + private java.math.BigDecimal maxScore; + + /** + * 是否均化(0-否 1-是) + */ + private String isEqualized; + + /** + * 均化阈值 + */ + private java.math.BigDecimal equalizedThreshold; + /** * 类型说明 */ @@ -63,5 +88,4 @@ public class XskhDisciplineType extends TenantEntity { @TableLogic(value = "0", delval = "1") private Long deleted; - } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhRewardType.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhRewardType.java index 94eb5c16..bc521373 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhRewardType.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhRewardType.java @@ -42,6 +42,16 @@ public class XskhRewardType extends TenantEntity { */ private String description; + /** + * 是否均化(0-否 1-是) + */ + private String isEqualized; + + /** + * 均化阈值 + */ + private java.math.BigDecimal equalizedThreshold; + /** * 状态(0-正常 1-停用) */ diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhDisciplineRecordBo.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhDisciplineRecordBo.java index 5b26582e..eb567fa8 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhDisciplineRecordBo.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhDisciplineRecordBo.java @@ -128,6 +128,11 @@ public class XskhDisciplineRecordBo extends BaseEntity { */ private String status; + /** + * 违纪来源(1-宿舍卫生检查 2-教学区违纪) + */ + private String violationSource; + /** * 附件OSS ID列表,逗号分隔 */ diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhDisciplineRevokeBo.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhDisciplineRevokeBo.java index 7a161fc1..0e598b9d 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhDisciplineRevokeBo.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhDisciplineRevokeBo.java @@ -68,24 +68,48 @@ public class XskhDisciplineRevokeBo extends BaseEntity { private Date applyTime; /** - * 状态(1-待审核 2-审核通过 3-审核拒绝) + * 状态(1-待班主任审核 2-待学工处审核 3-审核通过 4-班主任审核拒绝 5-学工处审核拒绝) */ private String status; /** - * 审核人ID + * 班主任审核人ID + */ + private Long teacherApprovalBy; + + /** + * 班主任审核时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date teacherApprovalTime; + + /** + * 班主任审核备注 + */ + private String teacherApprovalRemark; + + /** + * 学工处审核人ID */ private Long approvalBy; /** - * 审核时间 + * 学工处审核时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date approvalTime; /** - * 审核备注 + * 学工处审核备注 */ private String approvalRemark; + // VO类基础字段规范(避免MyBatis映射错误) + 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; // 更新时间 + } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhDisciplineTypeBo.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhDisciplineTypeBo.java index 763f1ee2..c8190fa5 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhDisciplineTypeBo.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhDisciplineTypeBo.java @@ -8,6 +8,7 @@ import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; import lombok.EqualsAndHashCode; import jakarta.validation.constraints.*; +import java.math.BigDecimal; /** * 学生违纪处分类型业务对象 xskh_discipline_type @@ -44,6 +45,39 @@ public class XskhDisciplineTypeBo extends BaseEntity { @NotBlank(message = "类型分类(1-违纪类型 2-处分类型)不能为空", groups = { AddGroup.class, EditGroup.class }) private String typeCategory; + /** + * 区域类型(1-宿舍区违纪 2-教学区违纪) + */ + @NotBlank(message = "区域类型不能为空", groups = { AddGroup.class, EditGroup.class }) + private String areaType; + + /** + * 最低扣分 + */ + @NotNull(message = "最低扣分不能为空", groups = { AddGroup.class, EditGroup.class }) + @DecimalMin(value = "0.00", message = "最低扣分不能小于0", groups = { AddGroup.class, EditGroup.class }) + @DecimalMax(value = "100.00", message = "最低扣分不能大于100", groups = { AddGroup.class, EditGroup.class }) + private java.math.BigDecimal minScore; + + /** + * 最高扣分 + */ + @NotNull(message = "最高扣分不能为空", groups = { AddGroup.class, EditGroup.class }) + @DecimalMin(value = "0.00", message = "最高扣分不能小于0", groups = { AddGroup.class, EditGroup.class }) + @DecimalMax(value = "100.00", message = "最高扣分不能大于100", groups = { AddGroup.class, EditGroup.class }) + private java.math.BigDecimal maxScore; + + /** + * 是否均化(0-否 1-是) + */ + private String isEqualized; + + /** + * 均化阈值 + */ + @DecimalMin(value = "0.00", message = "均化阈值不能小于0", groups = { AddGroup.class, EditGroup.class }) + private java.math.BigDecimal equalizedThreshold; + /** * 类型说明 */ @@ -64,5 +98,4 @@ public class XskhDisciplineTypeBo extends BaseEntity { */ private Long deleted; - } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhRewardTypeBo.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhRewardTypeBo.java index 134ec50b..27bdfb66 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhRewardTypeBo.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhRewardTypeBo.java @@ -43,6 +43,17 @@ public class XskhRewardTypeBo extends BaseEntity { */ private String description; + /** + * 是否均化(0-否 1-是) + */ + private String isEqualized; + + /** + * 均化阈值 + */ + @DecimalMin(value = "0.00", message = "均化阈值不能小于0", groups = { AddGroup.class, EditGroup.class }) + private java.math.BigDecimal equalizedThreshold; + /** * 状态(0-正常 1-停用) */ diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhDisciplineRecordVo.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhDisciplineRecordVo.java index 6020b84e..319d6a13 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhDisciplineRecordVo.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhDisciplineRecordVo.java @@ -131,11 +131,20 @@ public class XskhDisciplineRecordVo implements Serializable { * 状态(1-生效 2-已撤销) */ @ExcelProperty(value = "状态", converter = ExcelDictConvert.class) - @ExcelDictFormat(dictType = "student_discipline_status") + @ExcelDictFormat(dictType = "xskh_discipline_status") private String status; + + /** + * 违纪来源(1-宿舍卫生检查 2-教学区违纪) + */ + @ExcelProperty(value = "违纪来源", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "xskh_violation_source") + private String violationSource; + /** * 附件OSS ID列表,逗号分隔 */ + @ExcelProperty(value = "附件OSS ID列表") private String attachmentOssIds; /** diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhDisciplineRevokeVo.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhDisciplineRevokeVo.java index 17ae3452..5591c7f8 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhDisciplineRevokeVo.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhDisciplineRevokeVo.java @@ -90,35 +90,60 @@ public class XskhDisciplineRevokeVo implements Serializable { private Date applyTime; /** - * 状态(1-待审核 2-审核通过 3-审核拒绝) + * 状态(1-待班主任审核 2-待学工处审核 3-审核通过 4-班主任审核拒绝 5-学工处审核拒绝) */ @ExcelProperty(value = "状态", converter = ExcelDictConvert.class) @ExcelDictFormat(dictType = "student_revoke_status") private String status; /** - * 审核人ID + * 班主任审核人ID */ - @ExcelProperty(value = "审核人ID") + @ExcelProperty(value = "班主任审核人ID") + private Long teacherApprovalBy; + + /** + * 班主任审核人姓名(关联查询) + */ + @ExcelProperty(value = "班主任审核人姓名") + private String teacherApprovalByName; + + /** + * 班主任审核时间 + */ + @ExcelProperty(value = "班主任审核时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date teacherApprovalTime; + + /** + * 班主任审核备注 + */ + @ExcelProperty(value = "班主任审核备注") + private String teacherApprovalRemark; + + /** + * 学工处审核人ID + */ + @ExcelProperty(value = "学工处审核人ID") private Long approvalBy; /** - * 审核人姓名(关联查询) + * 学工处审核人姓名(关联查询) */ - @ExcelProperty(value = "审核人姓名") + @ExcelProperty(value = "学工处审核人姓名") private String approvalByName; /** - * 审核时间 + * 学工处审核时间 */ - @ExcelProperty(value = "审核时间") + @ExcelProperty(value = "学工处审核时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date approvalTime; /** - * 审核备注 + * 学工处审核备注 */ - @ExcelProperty(value = "审核备注") + @ExcelProperty(value = "学工处审核备注") private String approvalRemark; /** diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhDisciplineTypeVo.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhDisciplineTypeVo.java index 426425f9..3d9fb5f9 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhDisciplineTypeVo.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhDisciplineTypeVo.java @@ -12,8 +12,6 @@ import java.io.Serial; import java.io.Serializable; import java.util.Date; - - /** * 学生违纪处分类型视图对象 xskh_discipline_type * @@ -53,6 +51,38 @@ public class XskhDisciplineTypeVo implements Serializable { @ExcelDictFormat(dictType = "student_discipline_category") private String typeCategory; + /** + * 区域类型(1-宿舍区违纪 2-教学区违纪) + */ + @ExcelProperty(value = "区域类型", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "discipline_area_type") + private String areaType; + + /** + * 最低扣分 + */ + @ExcelProperty(value = "最低扣分") + private java.math.BigDecimal minScore; + + /** + * 最高扣分 + */ + @ExcelProperty(value = "最高扣分") + private java.math.BigDecimal maxScore; + + /** + * 是否均化(0-否 1-是) + */ + @ExcelProperty(value = "是否均化", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "sys_yes_no") + private String isEqualized; + + /** + * 均化阈值 + */ + @ExcelProperty(value = "均化阈值") + private java.math.BigDecimal equalizedThreshold; + /** * 类型说明 */ @@ -78,5 +108,4 @@ public class XskhDisciplineTypeVo implements Serializable { @ExcelProperty(value = "删除标志(0-未删除 1-已删除)") private Long deleted; - } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhRewardTypeVo.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhRewardTypeVo.java index fd17fdbd..d644a9ab 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhRewardTypeVo.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhRewardTypeVo.java @@ -12,8 +12,6 @@ import java.io.Serial; import java.io.Serializable; import java.util.Date; - - /** * 学生奖励类型视图对象 xskh_reward_type * @@ -52,6 +50,19 @@ public class XskhRewardTypeVo implements Serializable { @ExcelProperty(value = "类型说明") private String description; + /** + * 是否均化(0-否 1-是) + */ + @ExcelProperty(value = "是否均化", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "sys_yes_no") + private String isEqualized; + + /** + * 均化阈值 + */ + @ExcelProperty(value = "均化阈值") + private java.math.BigDecimal equalizedThreshold; + /** * 状态(0-正常 1-停用) */ @@ -71,5 +82,4 @@ public class XskhRewardTypeVo implements Serializable { @ExcelProperty(value = "删除标志(0-未删除 1-已删除)") private Long deleted; - } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhDisciplineRecordMapper.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhDisciplineRecordMapper.java index fe285aaa..3557020d 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhDisciplineRecordMapper.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhDisciplineRecordMapper.java @@ -19,46 +19,54 @@ import java.util.Map; */ public interface XskhDisciplineRecordMapper extends BaseMapperPlus { - /** - * 查询学生选项列表(用于下拉框) - * - * @param keyword 搜索关键词(学号或姓名) - * @return 学生选项列表 - */ - List selectStudentOptions(String keyword); + /** + * 查询学生选项列表(用于下拉框) + * + * @param keyword 搜索关键词(学号或姓名) + * @return 学生选项列表 + */ + List selectStudentOptions(String keyword); - /** - * 查询班主任班级的违纪处分记录列表(班主任专用) - * - * @param page 分页参数 - * @param bo 查询条件 - * @param classTeacherId 班主任ID - * @return 分页查询结果 - */ - Page selectClassTeacherVoPage(@Param("page") Page page, - @Param("bo") XskhDisciplineRecordBo bo, - @Param("classTeacherId") Long classTeacherId); + /** + * 查询班主任班级的违纪处分记录列表(班主任专用) + * + * @param page 分页参数 + * @param bo 查询条件 + * @param classTeacherId 班主任ID + * @return 分页查询结果 + */ + Page selectClassTeacherVoPage(@Param("page") Page page, + @Param("bo") XskhDisciplineRecordBo bo, + @Param("classTeacherId") Long classTeacherId); - /** - * 获取指标关联的违纪处分类型和奖励类型 - * - * @param indicatorId 指标ID - * @return 包含disciplineTypeIds和rewardTypeIds的Map - */ - Map selectIndicatorTypes(@Param("indicatorId") Long indicatorId); + /** + * 获取指标关联的违纪处分类型和奖励类型 + * + * @param indicatorId 指标ID + * @return 包含disciplineTypeIds和rewardTypeIds的Map + */ + Map selectIndicatorTypes(@Param("indicatorId") Long indicatorId); - /** - * 获取学生在指定时间范围内的违纪处分记录 - * - * @param studentId 学生ID - * @param disciplineTypeIds 违纪处分类型ID列表 - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 违纪处分记录列表 - */ - List selectStudentDisciplineRecords(@Param("studentId") Long studentId, - @Param("disciplineTypeIds") List disciplineTypeIds, - @Param("startTime") String startTime, - @Param("endTime") String endTime); + /** + * 获取学生在指定时间范围内的违纪处分记录 + * + * @param studentId 学生ID + * @param disciplineTypeIds 违纪处分类型ID列表 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 违纪处分记录列表 + */ + List selectStudentDisciplineRecords(@Param("studentId") Long studentId, + @Param("disciplineTypeIds") List disciplineTypeIds, + @Param("startTime") String startTime, + @Param("endTime") String endTime); + + /** + * 根据违纪记录ID逻辑删除宿舍卫生检查违纪关联记录 + * + * @param disciplineRecordId 违纪记录ID + * @return 删除的记录数量 + */ + int deleteHygieneCheckViolationByDisciplineRecordId(@Param("disciplineRecordId") Long disciplineRecordId); } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhDisciplineRevokeMapper.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhDisciplineRevokeMapper.java index 236bba85..2659cd96 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhDisciplineRevokeMapper.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/mapper/XskhDisciplineRevokeMapper.java @@ -15,24 +15,58 @@ import org.dromara.xskh.domain.vo.XskhDisciplineRevokeVo; */ public interface XskhDisciplineRevokeMapper extends BaseMapperPlus { - /** - * 根据违纪记录ID查询撤销申请记录 - * - * @param disciplineRecordId 违纪记录ID - * @return 撤销申请记录 - */ - XskhDisciplineRevokeVo selectByDisciplineRecordId(Long disciplineRecordId); + /** + * 根据违纪记录ID查询撤销申请记录 + * + * @param disciplineRecordId 违纪记录ID + * @return 撤销申请记录 + */ + XskhDisciplineRevokeVo selectByDisciplineRecordId(Long disciplineRecordId); - /** - * 查询班主任班级的违纪撤销申请列表(班主任专用) - * - * @param page 分页参数 - * @param bo 查询条件 - * @param classTeacherId 班主任ID - * @return 分页查询结果 - */ - Page selectClassTeacherVoPage(@Param("page") Page page, - @Param("bo") XskhDisciplineRevokeBo bo, - @Param("classTeacherId") Long classTeacherId); + /** + * 查询班主任班级的违纪撤销申请列表(班主任专用) + * + * @param page 分页参数 + * @param bo 查询条件 + * @param classTeacherId 班主任ID + * @return 分页查询结果 + */ + Page selectClassTeacherVoPage(@Param("page") Page page, + @Param("bo") XskhDisciplineRevokeBo bo, + @Param("classTeacherId") Long classTeacherId); + + /** + * 查询学工处待审核的违纪撤销申请列表(学工处专用) + * + * @param page 分页参数 + * @param bo 查询条件 + * @return 分页查询结果 + */ + Page selectStudentAffairsVoPage(@Param("page") Page page, + @Param("bo") XskhDisciplineRevokeBo bo); + + /** + * 查询学工处已审核的违纪撤销申请列表(学工处专用) + * + * @param page 分页参数 + * @param bo 查询条件 + * @return 分页查询结果 + */ + Page selectStudentAffairsReviewedVoPage( + @Param("page") Page page, + @Param("bo") XskhDisciplineRevokeBo bo); + + /** + * 查询班主任已审核的违纪撤销申请列表(根据审核人查询历史记录) + * + * @param page 分页参数 + * @param bo 查询条件 + * @param classTeacherId 班主任ID + * @return 分页查询结果 + */ + Page selectClassTeacherReviewedVoPage( + @Param("page") Page page, + @Param("bo") XskhDisciplineRevokeBo bo, + @Param("classTeacherId") Long classTeacherId); } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/IXskhDisciplineRevokeService.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/IXskhDisciplineRevokeService.java index f35b1bd8..9d845c3f 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/IXskhDisciplineRevokeService.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/IXskhDisciplineRevokeService.java @@ -61,19 +61,36 @@ public interface IXskhDisciplineRevokeService { */ Boolean applyRevoke(XskhDisciplineRevokeBo bo); - /** - * 审核撤销申请 - */ - Boolean approveRevoke(XskhDisciplineRevokeBo bo); - /** * 查询班主任班级的违纪撤销申请列表(班主任专用) */ TableDataInfo queryClassTeacherPageList(XskhDisciplineRevokeBo bo, PageQuery pageQuery); + /** + * 查询班主任已审核的违纪撤销申请列表(根据审核人查询历史记录) + */ + TableDataInfo queryClassTeacherReviewedPageList(XskhDisciplineRevokeBo bo, + PageQuery pageQuery); + /** * 班主任审核撤销申请 */ - Boolean classTeacherApproveRevoke(XskhDisciplineRevokeBo bo); + Boolean teacherApproveRevoke(XskhDisciplineRevokeBo bo); + + /** + * 查询学工处待审核的撤销申请列表(学工处专用) + */ + TableDataInfo queryStudentAffairsPageList(XskhDisciplineRevokeBo bo, PageQuery pageQuery); + + /** + * 学工处审核撤销申请 + */ + Boolean studentAffairsApproveRevoke(XskhDisciplineRevokeBo bo); + + /** + * 查询学工处已审核的撤销申请列表(学工处专用) + */ + TableDataInfo queryStudentAffairsReviewedPageList(XskhDisciplineRevokeBo bo, + PageQuery pageQuery); } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineRecordServiceImpl.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineRecordServiceImpl.java index d7cba428..9f12aaf5 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineRecordServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineRecordServiceImpl.java @@ -17,6 +17,8 @@ import org.dromara.xskh.domain.vo.XskhStudentOptionVo; import org.dromara.xskh.mapper.XskhDisciplineRecordMapper; import org.dromara.xskh.service.IXskhDisciplineRecordService; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import lombok.extern.slf4j.Slf4j; import java.util.Collection; import java.util.List; @@ -30,6 +32,7 @@ import java.util.stream.Collectors; * @author ruoyi * @date 2025-01-03 */ +@Slf4j @RequiredArgsConstructor @Service public class XskhDisciplineRecordServiceImpl implements IXskhDisciplineRecordService { @@ -132,10 +135,28 @@ public class XskhDisciplineRecordServiceImpl implements IXskhDisciplineRecordSer * 批量删除学生违纪处分记录 */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { if (isValid) { // TODO 做一些业务上的校验,判断是否需要校验 } + + // 在删除违纪记录前,先删除相关的宿舍卫生检查违纪关联记录 + for (Long id : ids) { + try { + // 查询违纪记录,判断是否为宿舍卫生检查来源 + XskhDisciplineRecordVo disciplineRecord = baseMapper.selectVoById(id); + if (disciplineRecord != null && "1".equals(disciplineRecord.getViolationSource())) { + // 删除宿舍卫生检查违纪关联记录 + int deletedCount = baseMapper.deleteHygieneCheckViolationByDisciplineRecordId(id); + log.info("删除违纪记录ID: {} 对应的宿舍卫生检查违纪关联记录,删除数量: {}", id, deletedCount); + } + } catch (Exception e) { + log.error("删除违纪记录ID: {} 对应的宿舍卫生检查违纪关联记录失败: {}", id, e.getMessage(), e); + throw new RuntimeException("删除宿舍卫生检查违纪关联记录失败", e); + } + } + return baseMapper.deleteBatchIds(ids) > 0; } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineRevokeServiceImpl.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineRevokeServiceImpl.java index 755167c6..06a483d5 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineRevokeServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineRevokeServiceImpl.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Service; import java.util.Collection; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -197,46 +198,53 @@ public class XskhDisciplineRevokeServiceImpl implements IXskhDisciplineRevokeSer bo.setStudentId(currentUserId); bo.setStudentName(disciplineRecord.getStudentName()); bo.setDisciplineRecordId(bo.getDisciplineRecordId()); - bo.setStatus("1"); // 待审核 + bo.setStatus("1"); // 待班主任审核 bo.setApplyTime(new Date()); return insertByBo(bo); } /** - * 审核撤销申请 + * 班主任审核撤销申请 */ @Override - public Boolean approveRevoke(XskhDisciplineRevokeBo bo) { - // 获取当前登录用户ID作为审核人 + public Boolean teacherApproveRevoke(XskhDisciplineRevokeBo bo) { + // 获取当前登录用户ID作为班主任审核人 Long currentUserId = LoginHelper.getUserId(); - bo.setApprovalBy(currentUserId); - bo.setApprovalTime(new Date()); + bo.setTeacherApprovalBy(currentUserId); + bo.setTeacherApprovalTime(new Date()); + + // 根据审核结果更新状态 + if ("2".equals(bo.getStatus())) { + // 班主任审核通过,状态改为待学工处审核 + } else if ("4".equals(bo.getStatus())) { + // 班主任审核拒绝,流程结束 + } return updateByBo(bo); } /** - * 查询班主任班级的违纪撤销申请列表(班主任专用) + * 查询学工处待审核的撤销申请列表(学工处专用) */ @Override - public TableDataInfo queryClassTeacherPageList(XskhDisciplineRevokeBo bo, + public TableDataInfo queryStudentAffairsPageList(XskhDisciplineRevokeBo bo, PageQuery pageQuery) { - // 获取当前登录用户ID(班主任ID) - Long currentUserId = LoginHelper.getUserId(); + // 只查询状态为2(待学工处审核)的申请 + bo.setStatus("2"); - // 调用mapper中的专用查询方法 - Page result = baseMapper.selectClassTeacherVoPage(pageQuery.build(), bo, currentUserId); + // 调用mapper中的学工处专用查询方法 + Page result = baseMapper.selectStudentAffairsVoPage(pageQuery.build(), bo); return TableDataInfo.build(result); } /** - * 班主任审核撤销申请 + * 学工处审核撤销申请 */ @Override - public Boolean classTeacherApproveRevoke(XskhDisciplineRevokeBo bo) { - // 获取当前登录用户ID作为审核人 + public Boolean studentAffairsApproveRevoke(XskhDisciplineRevokeBo bo) { + // 获取当前登录用户ID作为学工处审核人 Long currentUserId = LoginHelper.getUserId(); bo.setApprovalBy(currentUserId); @@ -245,8 +253,8 @@ public class XskhDisciplineRevokeServiceImpl implements IXskhDisciplineRevokeSer // 更新撤销申请状态 Boolean updateResult = updateByBo(bo); - // 如果撤销申请审批通过(状态为2),则同步更新违纪记录状态为已撤销 - if (updateResult && "2".equals(bo.getStatus())) { + // 如果学工处审核通过(状态为3),则同步更新违纪记录状态为已撤销 + if (updateResult && "3".equals(bo.getStatus())) { // 根据撤销申请ID查询完整信息以获取违纪记录ID XskhDisciplineRevokeVo revokeInfo = queryById(bo.getId()); if (revokeInfo != null && revokeInfo.getDisciplineRecordId() != null) { @@ -258,4 +266,58 @@ public class XskhDisciplineRevokeServiceImpl implements IXskhDisciplineRevokeSer return updateResult; } + /** + * 查询班主任班级的违纪撤销申请列表(班主任专用) + */ + @Override + public TableDataInfo queryClassTeacherPageList(XskhDisciplineRevokeBo bo, + PageQuery pageQuery) { + // 获取当前登录用户ID(班主任ID) + Long currentUserId = LoginHelper.getUserId(); + + // 只查询状态为1(待班主任审核)的申请 + bo.setStatus("1"); + + // 调用mapper中的专用查询方法 + Page result = baseMapper.selectClassTeacherVoPage(pageQuery.build(), bo, currentUserId); + return TableDataInfo.build(result); + } + + /** + * 查询班主任已审核的违纪撤销申请列表(根据审核人查询历史记录) + */ + @Override + public TableDataInfo queryClassTeacherReviewedPageList(XskhDisciplineRevokeBo bo, + PageQuery pageQuery) { + // 获取当前登录用户ID(班主任ID) + Long currentUserId = LoginHelper.getUserId(); + + // 调用mapper中的专用查询方法,通过teacherApprovalBy字段查询历史记录 + Page result = baseMapper.selectClassTeacherReviewedVoPage(pageQuery.build(), bo, + currentUserId); + return TableDataInfo.build(result); + } + + /** + * 查询学工处已审核的撤销申请列表(学工处专用) + */ + @Override + public TableDataInfo queryStudentAffairsReviewedPageList(XskhDisciplineRevokeBo bo, + PageQuery pageQuery) { + // 如果没有指定状态,则查询所有已审核状态(3-审核通过,5-审核拒绝) + if (StringUtils.isBlank(bo.getStatus())) { + // 通过params传递状态列表 + if (bo.getParams() == null) { + bo.setParams(new HashMap<>()); + } + if (!bo.getParams().containsKey("statusList")) { + bo.getParams().put("statusList", "3,5"); + } + } + + // 调用mapper中的学工处已审核查询方法 + Page result = baseMapper.selectStudentAffairsReviewedVoPage(pageQuery.build(), bo); + return TableDataInfo.build(result); + } + } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineTypeServiceImpl.java b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineTypeServiceImpl.java index fa2bf457..5b86731d 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineTypeServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhDisciplineTypeServiceImpl.java @@ -40,7 +40,7 @@ public class XskhDisciplineTypeServiceImpl implements IXskhDisciplineTypeService * @return 学生违纪处分类型 */ @Override - public XskhDisciplineTypeVo queryById(Long id){ + public XskhDisciplineTypeVo queryById(Long id) { return baseMapper.selectVoById(id); } @@ -73,10 +73,11 @@ public class XskhDisciplineTypeServiceImpl implements IXskhDisciplineTypeService private LambdaQueryWrapper buildQueryWrapper(XskhDisciplineTypeBo bo) { Map params = bo.getParams(); LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.orderByAsc(XskhDisciplineType::getId); + lqw.orderByAsc(XskhDisciplineType::getSortOrder, XskhDisciplineType::getId); lqw.eq(StringUtils.isNotBlank(bo.getTypeCode()), XskhDisciplineType::getTypeCode, bo.getTypeCode()); lqw.like(StringUtils.isNotBlank(bo.getTypeName()), XskhDisciplineType::getTypeName, bo.getTypeName()); lqw.eq(StringUtils.isNotBlank(bo.getTypeCategory()), XskhDisciplineType::getTypeCategory, bo.getTypeCategory()); + lqw.eq(StringUtils.isNotBlank(bo.getAreaType()), XskhDisciplineType::getAreaType, bo.getAreaType()); lqw.eq(StringUtils.isNotBlank(bo.getStatus()), XskhDisciplineType::getStatus, bo.getStatus()); lqw.eq(bo.getSortOrder() != null, XskhDisciplineType::getSortOrder, bo.getSortOrder()); lqw.eq(bo.getDeleted() != null, XskhDisciplineType::getDeleted, bo.getDeleted()); @@ -91,7 +92,16 @@ public class XskhDisciplineTypeServiceImpl implements IXskhDisciplineTypeService */ @Override public Boolean insertByBo(XskhDisciplineTypeBo bo) { + // 添加调试日志 + log.info("接收到的BO参数: typeCode={}, typeName={}, areaType={}, minScore={}, maxScore={}", + bo.getTypeCode(), bo.getTypeName(), bo.getAreaType(), bo.getMinScore(), bo.getMaxScore()); + XskhDisciplineType add = MapstructUtils.convert(bo, XskhDisciplineType.class); + + // 添加映射后的调试日志 + log.info("映射后的Entity参数: typeCode={}, typeName={}, areaType={}, minScore={}, maxScore={}", + add.getTypeCode(), add.getTypeName(), add.getAreaType(), add.getMinScore(), add.getMaxScore()); + validEntityBeforeSave(add); boolean flag = baseMapper.insert(add) > 0; if (flag) { @@ -116,8 +126,26 @@ public class XskhDisciplineTypeServiceImpl implements IXskhDisciplineTypeService /** * 保存前的数据校验 */ - private void validEntityBeforeSave(XskhDisciplineType entity){ - //TODO 做一些数据校验,如唯一约束 + private void validEntityBeforeSave(XskhDisciplineType entity) { + // 校验类型编码唯一性 + if (StringUtils.isNotBlank(entity.getTypeCode())) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(XskhDisciplineType::getTypeCode, entity.getTypeCode()); + if (entity.getId() != null) { + lqw.ne(XskhDisciplineType::getId, entity.getId()); + } + boolean exists = baseMapper.exists(lqw); + if (exists) { + throw new RuntimeException("类型编码已存在:" + entity.getTypeCode()); + } + } + + // 校验扣分范围 + if (entity.getMinScore() != null && entity.getMaxScore() != null) { + if (entity.getMinScore().compareTo(entity.getMaxScore()) > 0) { + throw new RuntimeException("最低扣分不能大于最高扣分"); + } + } } /** @@ -129,8 +157,8 @@ public class XskhDisciplineTypeServiceImpl implements IXskhDisciplineTypeService */ @Override public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { - if(isValid){ - //TODO 做一些业务上的校验,判断是否需要校验 + if (isValid) { + // TODO 做一些业务上的校验,判断是否需要校验 } return baseMapper.deleteByIds(ids) > 0; } diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/XskhDisciplineRecordMapper.xml b/backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/XskhDisciplineRecordMapper.xml index 0994c996..9713a923 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/XskhDisciplineRecordMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/XskhDisciplineRecordMapper.xml @@ -22,6 +22,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -45,7 +46,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select id, student_id, student_no, student_name, college_id, college_name, major_id, major_name, class_id, class_name, discipline_type_id, discipline_type_name, discipline_date, discipline_reason, discipline_content, score_deduction, status, attachment_oss_ids, create_by, create_time, create_dept, update_by, update_time, deleted, tenant_id from xskh_discipline_record + select id, student_id, student_no, student_name, college_id, college_name, major_id, major_name, class_id, class_name, discipline_type_id, discipline_type_name, discipline_date, discipline_reason, discipline_content, score_deduction, status, violation_source, attachment_oss_ids, create_by, create_time, create_dept, update_by, update_time, deleted, tenant_id from xskh_discipline_record + + + update xg_hygiene_check_violation + set deleted = 1, + update_time = now() + where discipline_record_id = #{disciplineRecordId} + and deleted = 0 + + diff --git a/backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/XskhDisciplineRevokeMapper.xml b/backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/XskhDisciplineRevokeMapper.xml index 8bb249b0..5e874576 100644 --- a/backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/XskhDisciplineRevokeMapper.xml +++ b/backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/XskhDisciplineRevokeMapper.xml @@ -13,6 +13,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + @@ -26,17 +29,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select id, discipline_record_id, student_id, student_name, revoke_reason, revoke_evidence, apply_time, status, approval_by, approval_time, approval_remark, create_by, create_time, create_dept, update_by, update_time, deleted, tenant_id from xskh_discipline_revoke + select id, discipline_record_id, student_id, student_name, revoke_reason, revoke_evidence, apply_time, status, + teacher_approval_by, teacher_approval_time, teacher_approval_remark, + approval_by, approval_time, approval_remark, + create_by, create_time, create_dept, update_by, update_time, deleted, tenant_id + from xskh_discipline_revoke select dr.id, dr.discipline_record_id, dr.student_id, dr.student_name, dr.revoke_reason, - dr.revoke_evidence, dr.apply_time, dr.status, dr.approval_by, dr.approval_time, - dr.approval_remark, dr.create_by, dr.create_time, dr.create_dept, + dr.revoke_evidence, dr.apply_time, dr.status, + dr.teacher_approval_by, dr.teacher_approval_time, dr.teacher_approval_remark, + dr.approval_by, dr.approval_time, dr.approval_remark, + dr.create_by, dr.create_time, dr.create_dept, dr.update_by, dr.update_time, dr.deleted, dr.tenant_id, xdr.student_no, xdr.discipline_type_name, xdr.discipline_date, - u.nick_name as approval_by_name + tu.nick_name as teacher_approval_by_name, + au.nick_name as approval_by_name from xskh_discipline_revoke dr inner join xskh_discipline_record xdr on dr.discipline_record_id = xdr.id - left join sys_user u on dr.approval_by = u.user_id and u.del_flag = '0' and u.tenant_id = '000000' + left join sys_user tu on dr.teacher_approval_by = tu.user_id and tu.del_flag = '0' and tu.tenant_id = '000000' + left join sys_user au on dr.approval_by = au.user_id and au.del_flag = '0' and au.tenant_id = '000000' where dr.deleted = 0 and dr.student_id in ( select sm.id @@ -75,4 +90,93 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by dr.apply_time desc + + + + + + diff --git a/docs/开发文档/API/API文档总览.md b/docs/开发文档/API/API文档总览.md index f66d441b..5d93d987 100644 --- a/docs/开发文档/API/API文档总览.md +++ b/docs/开发文档/API/API文档总览.md @@ -19,6 +19,7 @@ - [宿舍管理员授权API](./宿舍管理员授权API.md) - [学生管理员授权关系API](./学生管理员授权关系API.md) - [学生销假管理API](./学生销假管理API.md) +- [假期留宿管理API](./假期留宿管理API.md) - **重要修复:单人留宿计算逻辑**(2025-01-03) ### 1. 用户认证登录 diff --git a/docs/开发文档/API/宿舍卫生检查管理API.md b/docs/开发文档/API/宿舍卫生检查管理API.md index a3ae778d..b91b1190 100644 --- a/docs/开发文档/API/宿舍卫生检查管理API.md +++ b/docs/开发文档/API/宿舍卫生检查管理API.md @@ -329,8 +329,269 @@ public interface DormHygieneStudentScoreMapper extends BaseMapperPlus { + // 验证宿舍总分范围 + if (form.totalScore !== null && form.totalScore !== undefined && form.totalScore !== '') { + // 处理非0分情况 + const score = parseFloat(form.totalScore.toString()); + if (isNaN(score) || score < 0 || score > 100) { + ElMessage.warning('宿舍总分必须在0-100分之间'); + form.totalScore = 0; + return; + } + // 自动为所有学生设置相同的个人得分(包括0分) + if (roomStudents.value.length > 0) { + roomStudents.value.forEach((student: any) => { + student.student_score = score; + }); + ElMessage.success(`已自动为${roomStudents.value.length}名学生设置个人得分:${score}分`); + } + } else if (form.totalScore === 0 || form.totalScore === '0') { + // 特别处理0分的情况 + if (roomStudents.value.length > 0) { + roomStudents.value.forEach((student: any) => { + student.student_score = 0; + }); + ElMessage.success(`已自动为${roomStudents.value.length}名学生设置个人得分:0分`); + } + } +}; +``` + +#### 修复问题三:房间排序问题 +**问题现象:** 选择宿舍楼名称后,房间名称按照sort序号进行排序返回,但因为每层房间的sort都从0开始编码,导致返回数据把每个楼层的第一个房间先返回。 + +**解决方案:** 修改后端`XgDormRoomServiceImpl.java`中的`buildQueryWrapper`方法,调整排序逻辑为先按楼层再按sort排序。 + +```java +// 修改前 +lqw.orderByAsc(XgDormRoom::getSort); // 第一优先级:按排序号升序 +lqw.orderByAsc(XgDormRoom::getFloorNum); // 第二优先级:按楼层升序 + +// 修改后 +lqw.orderByAsc(XgDormRoom::getFloorNum); // 第一优先级:按楼层升序 +lqw.orderByAsc(XgDormRoom::getSort); // 第二优先级:按排序号升序 +``` + +#### 修复效果 +1. **问题一**:新增时个人扣分直接显示0,用户体验更好 +2. **问题二**:宿舍扣分与个人扣分完全同步,包括0分的情况 +3. **问题三**:房间列表按楼层顺序显示,每层内部按sort排序,符合用户期望 + +#### 涉及文件 +- `web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckDialog.vue` +- `backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormRoomServiceImpl.java` + +--- + +## 违纪记录更新修复 (2025-01-03) + +### 问题描述 + +在调用 `updateCompleteData` 接口更新宿舍卫生检查数据时,即使违纪记录入参中包含有效的 `disciplineRecordId`,系统仍然执行了"逻辑删除旧记录 + 新增新记录"的操作,而不是直接更新现有记录。 + +### 根本原因 + +**数据字段映射错误**: +- `XgViolationDataBo` 中的 `id` 字段被误认为是违纪记录ID (`disciplineRecordId`) +- 实际上 `id` 字段是关联表 `xg_hygiene_check_violation` 的主键ID +- 真正的违纪记录ID应该通过 `disciplineRecordId` 字段传递 + +### 修复方案 + +#### 1. 扩展 XgViolationDataBo 字段 + +**文件**: `backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/domain/bo/XgViolationDataBo.java` + +```java +/** + * 违纪记录ID(用于更新操作,新增时为null) + */ +private Long id; + +/** + * 违纪记录ID,关联xskh_discipline_record.id(用于更新违纪记录) + */ +private Long disciplineRecordId; +``` + +#### 2. 修复 handleViolationRecords 方法逻辑 + +**文件**: `backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormHygieneCheckServiceImpl.java` + +**修复前**: +```java +newDisciplineIds = newViolationData.stream() + .map(XgViolationDataBo::getId) // 错误:使用关联表ID + .filter(Objects::nonNull) + .collect(Collectors.toSet()); +``` + +**修复后**: +```java +newDisciplineIds = newViolationData.stream() + .map(violationBo -> { + // 优先使用disciplineRecordId字段,如果为空则使用id字段(向后兼容) + return violationBo.getDisciplineRecordId() != null + ? violationBo.getDisciplineRecordId() + : violationBo.getId(); + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); +``` + +#### 3. 修复判断逻辑 + +**修复前**: +```java +if (violationBo.getId() != null && existingDisciplineIds.contains(violationBo.getId())) { + // 更新逻辑 +} +``` + +**修复后**: +```java +Long currentDisciplineRecordId = violationBo.getDisciplineRecordId() != null + ? violationBo.getDisciplineRecordId() + : violationBo.getId(); +if (currentDisciplineRecordId != null && existingDisciplineIds.contains(currentDisciplineRecordId)) { + // 更新逻辑 +} +``` + +### 正确的入参格式 + +```json +{ + "violationData": [ + { + "id": "1964301377652428802", // 关联表ID (可选) + "disciplineRecordId": "1964301377522405377", // 违纪记录ID (用于更新判断) + "studentId": "1949114038914949121", + "studentNo": "2025DQ21121", + "studentName": "梁文清", + "disciplineTypeId": "1963793291531026434", + "disciplineTypeName": "地面未打扫", + "disciplineReason": "未按要求打扫地面", + "disciplineContent": "地面有明显垃圾", + "scoreDeduction": "2.00", + // ... 其他字段 + } + ] +} +``` + +### 修复效果 + +1. **准确识别更新操作**:系统能正确识别哪些违纪记录需要更新,哪些需要新增 +2. **避免重复数据**:不再错误地删除现有记录并新增重复记录 +3. **向后兼容**:保持对旧版本数据格式的兼容性 +4. **数据完整性**:确保违纪记录的历史数据完整性 + +### 测试验证 + +使用修复后的接口,当入参包含 `disciplineRecordId` 时: +- ✅ 系统识别为更新操作,直接更新现有违纪记录 +- ✅ 不再执行逻辑删除 + 新增的错误流程 +- ✅ 数据库中不会产生重复的违纪记录 \ No newline at end of file diff --git a/docs/开发文档/API/宿舍房间管理API.md b/docs/开发文档/API/宿舍房间管理API.md index b60ce95e..b26aec8f 100644 --- a/docs/开发文档/API/宿舍房间管理API.md +++ b/docs/开发文档/API/宿舍房间管理API.md @@ -302,6 +302,7 @@ xgss:dormRoom:list | floorNum | Integer | 否 | 楼层号,精确查询 | | roomType | String | 否 | 房间类型(1标准间 2单人间 3套间) | | roomStatus | String | 否 | 房间状态(0正常 1维修中 2停用) | +| sort | Integer | 否 | 排序号,精确查询 | #### 返回结果 @@ -320,6 +321,7 @@ xgss:dormRoom:list "actualBeds": 4, "roomArea": 20.5, "roomStatus": "0", + "sort": 0, "remark": "标准四人间", "createBy": 1, "createTime": "2025-07-22 09:15:30", @@ -337,6 +339,7 @@ xgss:dormRoom:list "actualBeds": 4, "roomArea": 20.5, "roomStatus": "0", + "sort": 0, "remark": "标准四人间", "createBy": 1, "createTime": "2025-07-22 09:15:30", @@ -363,6 +366,7 @@ xgss:dormRoom:list | actualBeds | Integer | 实际床位数 | | roomArea | Double | 房间面积(平方米) | | roomStatus | String | 房间状态(0正常 1维修中 2停用) | +| sort | Integer | 排序号,数字越小排序越靠前 | | remark | String | 备注 | | createBy | Long | 创建者ID | | createTime | Date | 创建时间 | @@ -371,11 +375,17 @@ xgss:dormRoom:list #### 排序规则 -房间列表按照以下顺序排序: -1. 优先按楼层号(floorNum)升序排列 -2. 其次按房间名称(roomName)升序排列 +房间列表按照以下优先级顺序排序: +1. **第一优先级**:按排序号(sort)升序排列 +2. **第二优先级**:按楼层号(floorNum)升序排列 +3. **第三优先级**:按房间名称(roomName)升序排列 -这确保了在宿舍管理系统中房间按照从低楼层到高楼层的顺序展示,相同楼层的房间按照名称顺序排列,提供更直观的浏览体验。 +**排序逻辑说明**: +- **自定义排序**:`sort` 字段允许管理员为房间设置自定义排序,数字越小排序越靠前 +- **默认排序**:当 `sort` 值相同或为0时,按楼层和房间名称的自然顺序排列 +- **灵活管理**:支持同一楼层内房间的个性化排序需求 + +这确保了宿舍管理系统既能保持传统的楼层顺序展示,又能支持管理员根据实际需要进行个性化排序调整。 #### 错误码 diff --git a/docs/开发文档/API/宿舍楼树形结构API.md b/docs/开发文档/API/宿舍楼树形结构API.md index 3a2384a1..de43ee96 100644 --- a/docs/开发文档/API/宿舍楼树形结构API.md +++ b/docs/开发文档/API/宿舍楼树形结构API.md @@ -2,11 +2,13 @@ ## 接口说明 -本文档描述了获取宿舍楼树形结构数据的API接口,该接口返回校区-宿舍区-宿舍楼的三级树形结构,其中校区和宿舍区节点不可选,只有宿舍楼节点可以选择。 +本文档描述了获取宿舍楼树形结构数据的API接口。**重要变更**:根据业务需求调整,主要接口已从 "校区-宿舍区-宿舍楼" 三级结构改为 "校区-宿舍楼" 二级结构,隐藏宿舍区层级,简化用户操作。 ## 接口定义 -### 获取宿舍楼树形结构 +### 🔄 主要接口(二级结构) + +#### 1. 获取宿舍楼树形结构(推荐使用) **请求URL**:`/sslxx/dormBuilding/buildingTree` @@ -14,6 +16,8 @@ **权限标识**:`sslxx:dormBuilding:list` +**结构说明**:校区-宿舍楼二级结构,校区不可选,只有宿舍楼可以选择 + **请求参数**:无 **响应示例**: @@ -30,24 +34,16 @@ "disabled": true, "children": [ { - "id": 101, - "label": "东区宿舍区", - "nodeType": "area", - "disabled": true, - "children": [ - { - "id": 1001, - "label": "1号楼", - "nodeType": "building", - "disabled": false - }, - { - "id": 1002, - "label": "2号楼", - "nodeType": "building", - "disabled": false - } - ] + "id": 1001, + "label": "1号楼", + "nodeType": "building", + "disabled": false + }, + { + "id": 1002, + "label": "2号楼", + "nodeType": "building", + "disabled": false } ] }, @@ -58,22 +54,92 @@ "disabled": true, "children": [ { - "id": 201, - "label": "西区宿舍区", - "nodeType": "area", + "id": 2001, + "label": "3号楼", + "nodeType": "building", + "disabled": false + }, + { + "id": 2002, + "label": "4号楼", + "nodeType": "building", + "disabled": false + } + ] + } + ] +} +``` + +#### 2. 获取宿舍楼树形结构(基于权限) + +**请求URL**:`/xgss/dormBuildingTree/list` + +**请求方式**:`GET` + +**权限标识**:`xgss:dormBuildingTree:list` + +**结构说明**:校区-宿舍楼二级结构,根据当前用户权限过滤 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 描述 | +|-----------|--------|------|----------------| +| campusId | Long | 否 | 校区ID过滤条件 | +| buildingName | String | 否 | 宿舍楼名称过滤条件 | + +**响应格式**:与上述接口相同 + +#### 3. 获取所有宿舍楼树形结构(不考虑权限) + +**请求URL**:`/xgss/dormBuildingTree/listAll` + +**请求方式**:`GET` + +**权限标识**:`xgss:dormBuildingTree:list` + +**结构说明**:校区-宿舍楼二级结构,返回所有数据,不进行权限过滤 + +**响应格式**:与上述接口相同 + +### 📋 特殊用途接口(三级结构) + +#### 4. 获取完整宿舍楼树形结构(包含宿舍区) + +**请求URL**:`/xgss/dormBuildingTree/fullList` + +**请求方式**:`GET` + +**权限标识**:`xgss:dormBuildingTree:list` + +**结构说明**:校区-宿舍区-宿舍楼三级结构,仅用于特殊需求 + +**响应示例**: + +```json +{ + "code": 200, + "msg": "操作成功", + "data": [ + { + "id": 1, + "label": "东校区", + "type": "campus", + "disabled": true, + "children": [ + { + "id": 101, + "label": "东区宿舍区", + "type": "area", "disabled": true, "children": [ { - "id": 2001, - "label": "3号楼", - "nodeType": "building", - "disabled": false - }, - { - "id": 2002, - "label": "4号楼", - "nodeType": "building", - "disabled": false + "id": 1001, + "label": "1号楼", + "type": "building", + "disabled": false, + "buildingFloors": 6, + "buildingType": "1" } ] } @@ -85,7 +151,7 @@ ## 数据结构 -### BuildingTreeSelectVo +### BuildingTreeSelectVo(二级结构) 树形结构节点对象: @@ -93,7 +159,7 @@ |------------|-------------------|----------------------------------------| | id | Long | 节点ID | | label | String | 节点名称 | -| nodeType | String | 节点类型:campus(校区)、area(宿舍区)、building(宿舍楼) | +| nodeType | String | 节点类型:campus(校区)、building(宿舍楼) | | disabled | Boolean | 是否禁用选择 | | children | List | 子节点列表 | | createBy | Long | 创建者 | @@ -101,6 +167,20 @@ | updateBy | Long | 更新者 | | updateTime | Date | 更新时间 | +### DormBuildingTreeVo(完整结构) + +完整三级结构节点对象: + +| 字段名 | 类型 | 描述 | +|-----------------|-------------------|----------------------------------------| +| id | Long | 节点ID | +| label | String | 节点名称 | +| type | String | 节点类型:campus(校区)、area(宿舍区)、building(宿舍楼) | +| disabled | Boolean | 是否禁用选择 | +| children | List | 子节点列表 | +| buildingFloors | Integer | 宿舍楼层数(仅宿舍楼节点) | +| buildingType | String | 宿舍楼类型(仅宿舍楼节点) | + ## 使用方法 ### 前端使用示例 @@ -114,7 +194,7 @@ import type { BuildingTreeSelectVo } from '@/api/sslxx/dormBuilding/types'; // 在组件的setup函数中 const treeData = ref([]); -// 加载宿舍楼树形数据 +// 加载宿舍楼树形数据(推荐使用二级结构) const loadBuildingTree = async () => { try { const res = await getBuildingTree(); @@ -155,20 +235,34 @@ loadBuildingTree(); ## 特殊说明 -1. 接口返回的树形结构中,校区和宿舍区节点已设置 `disabled=true`,前端无需额外处理即可实现只能选择宿舍楼节点的效果 -2. 使用 `check-strictly=true` 可以确保树形选择器的选中状态不会级联影响父子节点 -3. 该接口已支持管理员分配宿舍楼管理范围的业务需求 +### ⚠️ 重要变更说明 + +1. **主要接口结构调整**:为简化用户操作,去除宿舍区概念,主要接口已改为二级结构 +2. **向后兼容**:保留了 `fullList` 接口供需要完整三级结构的特殊场景使用 +3. **业务影响**:前端界面将不再显示宿舍区层级,用户直接在校区下选择宿舍楼 + +### 📋 使用建议 + +1. **推荐使用二级结构接口**:适用于绝大多数业务场景 +2. **校区节点设置为不可选**:确保用户只能选择宿舍楼 +3. **使用权限过滤接口**:根据用户权限显示相应的宿舍楼范围 ## 相关API接口 - [宿舍管理员授权API](./宿舍管理员授权API.md) +- [宿舍床位分配管理API](./宿舍床位分配管理API.md) ## 数据库关系 - `xg_dorm_building` 表通过 `campus_id` 字段关联 `xg_dorm_campus` 表 -- `xg_dorm_building` 表通过 `area_id` 字段关联 `xg_dorm_area` 表 +- `xg_dorm_building` 表通过 `area_id` 字段关联 `xg_dorm_area` 表(数据库保留,UI隐藏) - `xg_dorm_area` 表通过 `campus_id` 字段关联 `xg_dorm_campus` 表 +## 版本历史 + +- **2025-01-29**: 接口结构重大调整,主要接口从三级改为二级结构 +- **2025-07-20**: 初始版本,三级结构 + ## 最后更新时间 -2025-07-20 \ No newline at end of file +2025-01-29 \ No newline at end of file diff --git a/docs/设计文档/database/hzdyjs_xg0820.sql b/docs/设计文档/database/hzdyjs_xg0820.sql index 21c308f0..05034694 100644 --- a/docs/设计文档/database/hzdyjs_xg0820.sql +++ b/docs/设计文档/database/hzdyjs_xg0820.sql @@ -4376,11 +4376,13 @@ CREATE TABLE `xg_dorm_hygiene_check` ( `area_name` varchar(100) DEFAULT NULL COMMENT '宿舍区名称', `campus_id` bigint(20) DEFAULT NULL COMMENT '校区ID,关联xg_dorm_campus.id', `campus_name` varchar(100) DEFAULT NULL COMMENT '校区名称', - `total_score` decimal(5,2) DEFAULT NULL COMMENT '宿舍总分(0-100分)', + `total_score` decimal(5,2) DEFAULT NULL COMMENT '宿舍扣分(0-100分)', `hygiene_grade` char(1) DEFAULT NULL COMMENT '卫生等级(A优秀 B良好 C合格 D不合格)', `attachment_ids` text COMMENT '附件OSS ID列表,逗号分隔', `check_summary` text COMMENT '检查总结', `improvement_suggestions` text COMMENT '整改建议', + `discipline_type_id` bigint(20) DEFAULT NULL COMMENT '违纪类型ID,关联xskh_discipline_type.id', + `discipline_type_name` varchar(100) DEFAULT NULL COMMENT '违纪类型名称', `check_remarks` varchar(500) DEFAULT NULL COMMENT '检查备注', PRIMARY KEY (`id`) USING BTREE, KEY `idx_tenant_id` (`tenant_id`), @@ -4394,6 +4396,7 @@ CREATE TABLE `xg_dorm_hygiene_check` ( KEY `idx_hygiene_grade` (`hygiene_grade`), KEY `idx_check_type` (`check_type`), KEY `idx_create_time` (`create_time`), + KEY `idx_discipline_type_id` (`discipline_type_id`), KEY `idx_tenant_deleted` (`tenant_id`,`deleted`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宿舍卫生检查表'; @@ -4422,7 +4425,7 @@ CREATE TABLE `xg_dorm_hygiene_student_score` ( `college_name` varchar(100) DEFAULT NULL COMMENT '学院名称', `bed_id` bigint(20) DEFAULT NULL COMMENT '床位ID,关联xg_dorm_bed.id', `bed_code` varchar(50) DEFAULT NULL COMMENT '床位编号', - `student_score` decimal(5,2) DEFAULT NULL COMMENT '学生个人得分(0-100分)', + `student_score` decimal(5,2) DEFAULT NULL COMMENT '学生个人扣分(0-100分)', `attendance_status` char(1) DEFAULT '1' COMMENT '出勤状态(1在寝 2外出 3请假 4其他)', `student_grade` char(1) DEFAULT NULL COMMENT '学生评级(A优秀 B良好 C合格 D不合格)', `is_responsible_person` tinyint(1) DEFAULT '0' COMMENT '是否为寝室长(0否 1是)', @@ -4531,6 +4534,7 @@ CREATE TABLE `xg_dorm_room` ( `actual_beds` int(11) DEFAULT '4' COMMENT '实际床位数', `room_area` decimal(10,2) DEFAULT NULL COMMENT '房间面积(平方米)', `room_status` char(1) DEFAULT '0' COMMENT '房间状态(0可用 1维修中 2停用)', + `sort` int(11) DEFAULT '0' COMMENT '排序号', `deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)', `tenant_id` bigint(20) DEFAULT '0' COMMENT '租户编号', `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', diff --git a/docs/设计文档/database/学生考核管理/学生考核管理数据库模型.sql b/docs/设计文档/database/学生考核管理/学生考核管理数据库模型.sql index 92f54d1b..0b913d09 100644 --- a/docs/设计文档/database/学生考核管理/学生考核管理数据库模型.sql +++ b/docs/设计文档/database/学生考核管理/学生考核管理数据库模型.sql @@ -12,6 +12,11 @@ CREATE TABLE `xskh_discipline_type` ( `type_name` varchar(100) NOT NULL COMMENT '类型名称', `type_category` char(1) NOT NULL COMMENT '类型分类(1-违纪类型 2-处分类型)', `remark` varchar(500) DEFAULT NULL COMMENT '类型说明', + `area_type` char(1) DEFAULT '1' COMMENT '区域类型(1-宿舍区违纪 2-教学区违纪)' , + `min_score` decimal(5,2) DEFAULT 0.00 COMMENT '最低扣分', + `max_score` decimal(5,2) DEFAULT 100.00 COMMENT '最高扣分', + `is_equalized` char(1) DEFAULT '0' COMMENT '是否均化(0-否 1-是)', + `equalized_threshold` decimal(10,2) DEFAULT NULL COMMENT '均化阈值', `status` char(1) DEFAULT '0' COMMENT '状态(0-正常 1-停用)', `sort_order` int(4) DEFAULT 0 COMMENT '排序号', `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', @@ -27,6 +32,39 @@ CREATE TABLE `xskh_discipline_type` ( KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='学生违纪处分类型表'; +-- 1. 宿舍卫生检查违纪关联表(关联宿舍检查与违纪记录) +DROP TABLE IF EXISTS `xg_hygiene_check_violation`; +CREATE TABLE `xg_hygiene_check_violation` ( + `id` bigint(20) NOT NULL COMMENT '主键ID', + `tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号', + `hygiene_check_id` bigint(20) NOT NULL COMMENT '宿舍卫生检查ID,关联xg_dorm_hygiene_check.id', + `discipline_record_id` bigint(20) NOT NULL COMMENT '违纪记录ID,关联xskh_discipline_record.id', + `student_id` bigint(20) NOT NULL COMMENT '学生ID,关联xg_student_msg.id', + `student_no` varchar(20) NOT NULL COMMENT '学号', + `student_name` varchar(50) NOT NULL COMMENT '学生姓名', + `discipline_type_id` bigint(20) NOT NULL COMMENT '违纪类型ID,关联xskh_discipline_type.id', + `discipline_type_name` varchar(100) NOT NULL COMMENT '违纪类型名称', + `violation_source` char(1) DEFAULT '1' COMMENT '违纪来源(1宿舍卫生检查 2其他)', + `status` char(1) DEFAULT '1' COMMENT '状态(1生效 2已撤销)', + `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `deleted` tinyint(1) DEFAULT 0 COMMENT '删除标志(0-未删除 1-已删除)', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_tenant_id` (`tenant_id`), + KEY `idx_hygiene_check_id` (`hygiene_check_id`), + KEY `idx_discipline_record_id` (`discipline_record_id`), + KEY `idx_student_id` (`student_id`), + KEY `idx_student_no` (`student_no`), + KEY `idx_discipline_type_id` (`discipline_type_id`), + KEY `idx_violation_source` (`violation_source`), + KEY `idx_status` (`status`), + KEY `idx_deleted` (`deleted`), + KEY `idx_tenant_deleted` (`tenant_id`,`deleted`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宿舍卫生检查违纪关联表'; + -- 2. 学生违纪处分记录表 DROP TABLE IF EXISTS `xskh_discipline_record`; CREATE TABLE `xskh_discipline_record` ( @@ -47,6 +85,7 @@ CREATE TABLE `xskh_discipline_record` ( `discipline_content` varchar(500) DEFAULT NULL COMMENT '违纪处分内容', `score_deduction` decimal(5,2) DEFAULT 0.00 COMMENT '扣分分值', `status` char(1) DEFAULT '1' COMMENT '状态(1-生效 2-已撤销)', + `violation_source` char(1) DEFAULT '2' COMMENT '违纪来源(1-宿舍卫生检查 2-教学区违纪)', `attachment_oss_ids` varchar(1000) DEFAULT NULL COMMENT '附件OSS文件ID,多个用逗号分隔(申请材料、证明文件等)', `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', `create_time` datetime DEFAULT NULL COMMENT '创建时间', @@ -73,10 +112,15 @@ CREATE TABLE `xskh_discipline_revoke` ( `revoke_reason` varchar(500) NOT NULL COMMENT '撤销申请原因', `revoke_evidence` varchar(500) DEFAULT NULL COMMENT '撤销证明材料,附件的oss_id,多个用逗号分隔', `apply_time` datetime NOT NULL COMMENT '申请时间', - `status` char(1) DEFAULT '1' COMMENT '状态(1-待审核 2-审核通过 3-审核拒绝)', - `approval_by` bigint(20) DEFAULT NULL COMMENT '审核人ID', - `approval_time` datetime DEFAULT NULL COMMENT '审核时间', - `approval_remark` varchar(500) DEFAULT NULL COMMENT '审核备注', + `status` char(1) DEFAULT '1' COMMENT '状态(1-待班主任审核 2-待学工处审核 3-审核通过 4-班主任审核拒绝 5-学工处审核拒绝)', + -- 班主任审核信息 + `teacher_approval_by` bigint(20) DEFAULT NULL COMMENT '班主任审核人ID', + `teacher_approval_time` datetime DEFAULT NULL COMMENT '班主任审核时间', + `teacher_approval_remark` varchar(500) DEFAULT NULL COMMENT '班主任审核备注', + -- 学工处审核信息(保留原有字段名,语义调整为学工处审核) + `approval_by` bigint(20) DEFAULT NULL COMMENT '学工处审核人ID', + `approval_time` datetime DEFAULT NULL COMMENT '学工处审核时间', + `approval_remark` varchar(500) DEFAULT NULL COMMENT '学工处审核备注', `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', @@ -87,7 +131,9 @@ CREATE TABLE `xskh_discipline_revoke` ( PRIMARY KEY (`id`), KEY `idx_discipline_record_id` (`discipline_record_id`), KEY `idx_student_id` (`student_id`), - KEY `idx_status` (`status`) + KEY `idx_status` (`status`), + KEY `idx_teacher_approval_by` (`teacher_approval_by`), + KEY `idx_approval_by` (`approval_by`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='违纪处分撤销申请审批表'; -- 4. 奖励类型表 @@ -97,6 +143,8 @@ CREATE TABLE `xskh_reward_type` ( `type_code` varchar(50) NOT NULL COMMENT '类型编码', `type_name` varchar(100) NOT NULL COMMENT '类型名称', `description` varchar(500) DEFAULT NULL COMMENT '类型说明', + `is_equalized` char(1) DEFAULT '0' COMMENT '是否均化(0-否 1-是)', + `equalized_threshold` decimal(10,2) DEFAULT NULL COMMENT '均化阈值', `status` char(1) DEFAULT '0' COMMENT '状态(0-正常 1-停用)', `sort_order` int(4) DEFAULT 0 COMMENT '排序号', `create_by` bigint(20) DEFAULT NULL COMMENT '创建者', diff --git a/docs/问题解决方案/学生考核指标indicator_type_name字段问题修复.md b/docs/问题解决方案/学生考核指标indicator_type_name字段问题修复.md deleted file mode 100644 index 40017513..00000000 --- a/docs/问题解决方案/学生考核指标indicator_type_name字段问题修复.md +++ /dev/null @@ -1,211 +0,0 @@ -# 学生考核指标indicator_type_name字段问题修复 - -## 问题描述 - -在新增学生考核指标时,出现数据库插入错误: - -``` -Field 'indicator_type_name' doesn't have a default value -``` - -## 问题分析 - -### 1. 根本原因 -- 数据库表 `xskh_assessment_indicator` 中的 `indicator_type_name` 字段设置为 `NOT NULL` -- 前端传递的数据中只有 `indicatorTypeId`,没有 `indicatorTypeName` -- 后端Service层在插入数据时没有自动设置 `indicatorTypeName` 字段的值 - -### 2. 字段映射问题 -- 前端使用 `indicatorDescription` 字段,但后端数据库字段是 `description` -- 为了保持前后端代码和数据库字段的一致性,统一使用 `description` 字段名 - -## 解决方案 - -### 1. 后端Service层修复 - -**文件**: `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhAssessmentIndicatorServiceImpl.java` - -#### 1.1 添加依赖注入 -```java -private final XskhAssessmentIndicatorTypeMapper indicatorTypeMapper; -``` - -#### 1.2 修改insertByBo方法 -```java -@Override -@Transactional(rollbackFor = Exception.class) -public Boolean insertByBo(XskhAssessmentIndicatorBo bo) { - XskhAssessmentIndicator add = MapstructUtils.convert(bo, XskhAssessmentIndicator.class); - - // 根据指标类型ID设置指标类型名称 - if (add.getIndicatorTypeId() != null) { - XskhAssessmentIndicatorType indicatorType = indicatorTypeMapper.selectById(add.getIndicatorTypeId()); - if (indicatorType != null) { - add.setIndicatorTypeName(indicatorType.getTypeName()); - } - } - - validEntityBeforeSave(add); - boolean flag = baseMapper.insert(add) > 0; - if (flag) { - bo.setId(add.getId()); - // 保存关联关系 - saveIndicatorRelations(add.getId(), bo.getDisciplineTypeIds(), bo.getRewardTypeIds()); - } - return flag; -} -``` - -#### 1.3 修改updateByBo方法 -```java -@Override -@Transactional(rollbackFor = Exception.class) -public Boolean updateByBo(XskhAssessmentIndicatorBo bo) { - XskhAssessmentIndicator update = MapstructUtils.convert(bo, XskhAssessmentIndicator.class); - - // 根据指标类型ID设置指标类型名称 - if (update.getIndicatorTypeId() != null) { - XskhAssessmentIndicatorType indicatorType = indicatorTypeMapper.selectById(update.getIndicatorTypeId()); - if (indicatorType != null) { - update.setIndicatorTypeName(indicatorType.getTypeName()); - } - } - - validEntityBeforeSave(update); - boolean flag = baseMapper.updateById(update) > 0; - if (flag) { - // 更新关联关系 - saveIndicatorRelations(update.getId(), bo.getDisciplineTypeIds(), bo.getRewardTypeIds()); - } - return flag; -} -``` - -### 2. 字段映射修复 - -为了保持前后端代码和数据库字段的一致性,统一使用 `description` 字段名: - -#### 2.1 前端类型定义修复 -**文件**: `web/src/api/xskh/assessmentIndicator/types.ts` - -将所有接口中的 `indicatorDescription` 字段改为 `description`: -- `XskhAssessmentIndicatorBo` -- `XskhAssessmentIndicatorVo` -- `XskhAssessmentIndicatorForm` - -#### 2.2 前端表单修复 -**文件**: `web/src/views/xskh/assessmentIndicator/AssessmentIndicatorForm.vue` - -- 表单字段名:`indicatorDescription` → `description` -- 表单验证规则:`indicatorDescription` → `description` -- 初始数据:`indicatorDescription` → `description` - -### 3. Mapper XML修复 - -**文件**: `backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/xskh/XskhAssessmentIndicatorMapper.xml` - -添加缺失的 `createDept` 字段映射: - -```xml - -``` - -### 4. 前端CSS修复 - -**文件**: `web/src/views/xskh/assessmentIndicator/AssessmentIndicatorForm.vue` - -修复CSS语法错误: - -```css -/* 固定多选框高度,防止动态变化 */ -.fixed-height-select :deep(.el-select__tags) { - max-height: 32px; - overflow: hidden; -} - -.fixed-height-select :deep(.el-select__tags-text) { - max-width: 120px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.fixed-height-select :deep(.el-select__collapse-tags) { - max-width: 100%; -} -``` - -## 测试验证 - -### 1. 创建测试脚本 -**文件**: `test/学生考核指标API测试脚本.py` - -包含以下测试用例: -- 登录认证 -- 新增学生考核指标 -- 查询指标列表 -- 查询指标详情 -- 修改指标 - -### 2. 测试数据 -```json -{ - "indicatorCode": "zbbm01", - "indicatorName": "学生指标01", - "description": "学生指标01指标说明", - "indicatorTypeId": "1958131304973221889", - "maxScore": 2.1, - "status": "0", - "sortOrder": 0, - "disciplineTypeIds": [ - "1958135628856049666", - "1958135753561096193" - ], - "rewardTypeIds": [ - "1958135350199074817", - "1958135400207761409" - ] -} -``` - -## 修复效果 - -### 1. 问题解决 -- ✅ 解决了 `indicator_type_name` 字段缺失默认值的问题 -- ✅ 统一了前后端字段命名,使用 `description` 字段 -- ✅ 修复了CSS语法错误 -- ✅ 完善了Mapper XML字段映射 - -### 2. 功能增强 -- ✅ 自动根据指标类型ID设置指标类型名称 -- ✅ 统一前后端字段命名,提高代码一致性 -- ✅ 完善了错误处理和日志记录 - -### 3. 代码质量 -- ✅ 遵循项目开发规范 -- ✅ 添加了完整的注释说明 -- ✅ 提供了测试脚本验证功能 - -## 注意事项 - -1. **数据库字段约束**: 确保数据库表设计合理,避免不必要的NOT NULL约束 -2. **字段命名一致性**: 前后端字段名应保持一致,避免不必要的映射转换 -3. **自动填充**: 对于关联字段,应该在Service层自动填充,而不是依赖前端传递 -4. **测试验证**: 每次修复后都应该进行完整的测试验证 - -## 相关文件 - -- `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhAssessmentIndicatorServiceImpl.java` -- `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhAssessmentIndicatorBo.java` -- `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/XskhAssessmentIndicator.java` -- `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhAssessmentIndicatorVo.java` -- `backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/xskh/XskhAssessmentIndicatorMapper.xml` -- `web/src/api/xskh/assessmentIndicator/types.ts` -- `web/src/views/xskh/assessmentIndicator/AssessmentIndicatorForm.vue` -- `test/学生考核指标API测试脚本.py` - ---- - -**修复时间**: 2025-01-03 -**修复人员**: AI Assistant -**验证状态**: 待验证 \ No newline at end of file diff --git a/docs/问题解决方案/学生考核指标类型createDept字段问题修复.md b/docs/问题解决方案/学生考核指标类型createDept字段问题修复.md deleted file mode 100644 index c27ed59a..00000000 --- a/docs/问题解决方案/学生考核指标类型createDept字段问题修复.md +++ /dev/null @@ -1,162 +0,0 @@ -# 学生考核指标类型createDept字段问题修复 - -## 问题描述 - -在 `XskhAssessmentIndicatorTypeController.java` 类的 `add` 方法执行后,数据库中查看数据时发现 `create_dept` 字段(创建人的部门ID)没有插入数据,显示为 NULL。 - -## 问题分析 - -通过代码分析,发现问题的根本原因是: - -### 1. VO类缺少createDept字段 -**文件**: `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhAssessmentIndicatorTypeVo.java` - -**问题**: VO类中缺少 `createDept` 字段定义,导致字段映射不完整。 - -### 2. BO类缺少createDept字段 -**文件**: `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhAssessmentIndicatorTypeBo.java` - -**问题**: BO类中缺少 `createDept` 字段定义,影响数据传输。 - -### 3. Mapper XML缺少字段映射 -**文件**: `backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/xskh/XskhAssessmentIndicatorTypeMapper.xml` - -**问题**: resultMap中缺少 `createDept` 字段的映射配置,导致数据库字段无法正确映射到实体属性。 - -## 解决方案 - -### 1. 修复VO类 -在 `XskhAssessmentIndicatorTypeVo.java` 中添加 `createDept` 字段: - -```java -/** - * 创建部门 - */ -private Long createDept; -``` - -### 2. 修复BO类 -在 `XskhAssessmentIndicatorTypeBo.java` 中添加基础字段: - -```java -/** - * 创建部门 - */ -private Long createDept; - -/** - * 创建者 - */ -private Long createBy; - -/** - * 创建时间 - */ -private Date createTime; - -/** - * 更新者 - */ -private Long updateBy; - -/** - * 更新时间 - */ -private Date updateTime; -``` - -### 3. 修复Mapper XML -在 `XskhAssessmentIndicatorTypeMapper.xml` 的两个resultMap中添加字段映射: - -```xml - -``` - -### 4. 增强Service实现 -在 `XskhAssessmentIndicatorTypeServiceImpl.java` 中添加调试日志和手动设置逻辑: - -```java -// 调试日志:检查当前登录用户信息 -try { - Long userId = LoginHelper.getUserId(); - Long deptId = LoginHelper.getDeptId(); - log.info("🔍 调试信息 - 当前登录用户ID: {}, 部门ID: {}", userId, deptId); - - // 手动设置createDept字段,确保有值 - if (add.getCreateDept() == null && deptId != null) { - add.setCreateDept(deptId); - log.info("✅ 手动设置createDept字段: {}", deptId); - } else { - log.info("⚠️ createDept字段状态: {}", add.getCreateDept()); - } -} catch (Exception e) { - log.warn("⚠️ 获取登录用户信息失败: {}", e.getMessage()); -} -``` - -## 技术原理 - -### MyBatis-Plus字段填充机制 -系统使用 MyBatis-Plus 的 `@TableField(fill = FieldFill.INSERT)` 注解来自动填充字段: - -```java -@TableField(fill = FieldFill.INSERT) -private Long createDept; -``` - -### 字段填充处理器 -`InjectionMetaObjectHandler` 负责在插入时自动填充字段: - -```java -baseEntity.setCreateDept(ObjectUtils.notNull(baseEntity.getCreateDept(), loginUser.getDeptId())); -``` - -### 字段映射关系 -- 数据库字段: `create_dept` -- 实体属性: `createDept` -- 映射配置: `` - -## 验证方法 - -### 1. 创建测试脚本 -创建 `test/学生考核指标类型API测试脚本.py` 来验证修复效果。 - -### 2. 测试步骤 -1. 登录系统获取token -2. 调用新增API创建测试数据 -3. 调用查询API检查createDept字段 -4. 验证字段是否正确填充 - -### 3. 预期结果 -- createDept字段应该包含当前登录用户的部门ID -- 不再显示为NULL - -## 预防措施 - -### 1. 规范检查 -- 所有VO类必须包含四个基础字段:`createBy`、`createTime`、`updateBy`、`updateTime` -- 新增 `createDept` 字段作为第五个基础字段 -- Mapper XML必须包含完整的字段映射 - -### 2. 代码模板 -创建标准的代码模板,确保新开发的模块不会遗漏基础字段。 - -### 3. 自动化测试 -建立自动化测试机制,定期检查字段映射的完整性。 - -## 相关文件 - -### 修改的文件 -1. `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhAssessmentIndicatorTypeVo.java` -2. `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/bo/XskhAssessmentIndicatorTypeBo.java` -3. `backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/xskh/XskhAssessmentIndicatorTypeMapper.xml` -4. `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhAssessmentIndicatorTypeServiceImpl.java` - -### 新增的文件 -1. `test/学生考核指标类型API测试脚本.py` - -## 总结 - -通过修复VO类、BO类、Mapper XML的字段映射问题,并增强Service实现的调试功能,成功解决了 `create_dept` 字段不填充的问题。这个问题的根本原因是字段映射不完整,导致MyBatis-Plus的自动填充机制无法正常工作。 - -修复后的系统能够正确记录创建人的部门信息,为后续的数据分析和权限控制提供了基础支持。 \ No newline at end of file diff --git a/docs/问题解决方案/学生考核时间格式修复报告.md b/docs/问题解决方案/学生考核时间格式修复报告.md deleted file mode 100644 index 660475f8..00000000 --- a/docs/问题解决方案/学生考核时间格式修复报告.md +++ /dev/null @@ -1,218 +0,0 @@ -# 学生考核时间格式修复报告 - -## 📋 问题描述 - -### 问题现象 -在学生考核管理系统中,点击保存按钮调用接口 `http://localhost/dev-api/assessment/approval/approve` 时,返回的数据中 `bonusApprovalList` 和 `bonusList` 的时间字段显示为不标准的格式: - -```json -{ - "bonusApprovalList": [ - { - "createTime": "2025/8/21 上午10:52", - "updateTime": "2025/8/21 上午10:52" - } - ] -} -``` - -### 问题分析 -经过代码分析,发现问题出现在以下两个VO类文件中: - -1. **XskhAssessmentBonusApprovalVo.java** - 附加分审核VO类 -2. **XskhAssessmentSelfScoreVo.java** - 自评得分VO类 - -这两个文件中的 `createTime` 和 `updateTime` 字段被错误地定义为 `String` 类型,但使用了 `@JsonFormat` 注解,导致时间格式不正确。 - -## 🔧 修复方案 - -### 修复文件列表 - -#### 1. XskhAssessmentBonusApprovalVo.java -**文件路径**: `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhAssessmentBonusApprovalVo.java` - -**修复内容**: -```java -// 修复前 -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private String createTime; - -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private String updateTime; - -// 修复后 -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private Date createTime; - -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private Date updateTime; -``` - -#### 2. XskhAssessmentSelfScoreVo.java -**文件路径**: `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhAssessmentSelfScoreVo.java` - -**修复内容**: -```java -// 修复前 -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private String createTime; - -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private String updateTime; - -// 修复后 -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private Date createTime; - -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private Date updateTime; -``` - -### 修复原理 - -1. **类型一致性**: 将时间字段类型从 `String` 改为 `Date`,与实体类保持一致 -2. **JSON格式化**: 使用 `@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")` 注解确保时间以标准格式输出 -3. **遵循项目规范**: 符合项目后端开发规范中关于VO类基础字段的要求 - -## 📊 修复效果 - -### 修复前 -```json -{ - "bonusApprovalList": [ - { - "createTime": "2025/8/21 上午10:52", - "updateTime": "2025/8/21 上午10:52" - } - ] -} -``` - -### 修复后 -```json -{ - "bonusApprovalList": [ - { - "createTime": "2025-08-21 10:52:40", - "updateTime": "2025-08-21 10:52:40" - } - ] -} -``` - -## 🧪 验证方法 - -### 1. 代码验证 -- ✅ 检查VO类文件中的时间字段类型 -- ✅ 确认 `@JsonFormat` 注解正确使用 -- ✅ 验证字段类型与实体类一致 - -### 2. 功能验证 -创建了验证脚本 `test/学生考核时间格式修复验证.py`,用于: -- 登录系统获取token -- 调用考核审核接口 -- 检查返回数据中的时间格式 -- 验证时间字段是否为标准格式 - -### 3. 测试步骤 -```bash -# 1. 启动后端服务 -cd backend -mvn spring-boot:run - -# 2. 运行验证脚本 -cd test -python 学生考核时间格式修复验证.py -``` - -## 📚 相关规范 - -### 项目后端开发规范 -根据 `.cursor/cursorrules-backend.md` 中的规范: - -#### VO类基础字段规范(强制性) -```java -/** - * 创建者 - */ -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; -``` - -#### 字段使用注解规范 -- 时间字段必须使用JSON格式化注解 -- 字段类型必须统一:createBy/updateBy 为 Long,createTime/updateTime 为 Date -- 时间字段必须使用 `@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")` 注解 - -## 🔍 预防措施 - -### 1. 代码审查 -- 新增VO类时必须检查基础字段类型 -- 确保时间字段使用正确的类型和注解 - -### 2. 自动化检查 -- 在CI/CD流程中添加代码规范检查 -- 使用静态代码分析工具检查字段类型 - -### 3. 文档更新 -- 更新项目开发规范文档 -- 在团队中推广VO类开发标准 - -## 📝 总结 - -本次修复解决了学生考核管理系统中时间格式显示不正确的问题,主要修复了: - -1. **2个VO类文件**中的时间字段类型错误 -2. **4个时间字段**的类型从String改为Date -3. **确保JSON格式化注解**正确使用 -4. **修复Service实现类**中的类型转换错误 - -### 🔧 修复详情 - -#### 第一阶段:VO类修复 -- `XskhAssessmentBonusApprovalVo.java` - 修复createTime和updateTime字段类型 -- `XskhAssessmentSelfScoreVo.java` - 修复createTime和updateTime字段类型 - -#### 第二阶段:Service实现类修复(优化方案) -- `XskhAssessmentApplicationApprovalServiceImpl.java` - 优化时间字段处理逻辑 -- **根本解决方案**:修改SQL查询使用ResultMap而不是Map类型 -- **移除复杂的手动类型转换**:让MyBatis自动处理类型映射 -- **简化代码**:从50+行手动转换代码简化为1行直接赋值 - -### 🚨 编译错误解决 - -在修复VO类后,编译时出现了类型转换错误: -``` -不兼容的类型: java.lang.String无法转换为java.util.Date -``` - -**解决方案**: -1. **修改SQL查询**:使用ResultMap替代resultType="java.util.Map" -2. **添加ResultMap定义**:`XskhAssessmentSelfScoreWithIndicatorResult` -3. **修改Mapper接口**:返回类型从`List`改为`List` -4. **简化Service代码**:移除复杂的手动类型转换,直接使用MyBatis自动映射 - -修复后,系统返回的时间格式将符合标准格式 `yyyy-MM-dd HH:mm:ss`,提升了数据的一致性和用户体验。 - ---- - -**修复人员**: AI Assistant -**修复时间**: 2025-01-03 -**验证状态**: ✅ 已修复 -**影响范围**: 学生考核管理系统时间显示 \ No newline at end of file diff --git a/docs/问题解决方案/学生考核申请ID精度丢失修复报告.md b/docs/问题解决方案/学生考核申请ID精度丢失修复报告.md deleted file mode 100644 index 300c0c18..00000000 --- a/docs/问题解决方案/学生考核申请ID精度丢失修复报告.md +++ /dev/null @@ -1,246 +0,0 @@ -# 学生考核申请ID精度丢失修复报告 - -## 问题描述 - -### 错误现象 -在学生考核申请界面保存时,ID等字段丢失精度,导致存到数据库都为 `1958180338534527000`,后几位都是 `000`。 - -### 问题分析 -1. **根本原因**: JavaScript 大整数精度丢失问题 - - JavaScript 的 `Number.MAX_SAFE_INTEGER` 为 `9007199254740991` - - 雪花ID通常超过这个范围,导致精度丢失 - - 使用 `parseInt()` 转换大整数字符串时,超出安全范围的数字会被截断 - -2. **问题位置**: `safeConvertId` 函数 - - 原函数使用 `parseInt()` 将字符串ID转换为数字 - - 对于大整数(如雪花ID),转换后精度丢失 - -3. **影响范围**: 所有ID字段 - - `id`: 主键ID - - `studentId`: 学生ID - - `collegeId`: 学院ID - - `majorId`: 专业ID - - `classId`: 班级ID - - `scheduleId`: 考核计划ID - -## 修复方案 - -### 1. 修复 `safeConvertId` 函数 - -**文件**: `web/src/views/xskh/assessmentApplication/AssessmentApplicationForm.vue` - -**修复前**: -```typescript -const safeConvertId = (id: string | number | undefined): number | undefined => { - if (id === undefined || id === null) return undefined; - if (typeof id === 'number') return id; - if (typeof id === 'string') { - // 对于大整数,使用parseInt而不是Number,并检查是否超出安全范围 - const num = parseInt(id, 10); - if (isNaN(num)) return undefined; - // 检查是否超出JavaScript安全整数范围 - if (num > Number.MAX_SAFE_INTEGER) { - console.warn('ID超出JavaScript安全整数范围,可能导致精度丢失:', id); - } - return num; - } - return undefined; -}; -``` - -**修复后**: -```typescript -const safeConvertId = (id: string | number | undefined): string | undefined => { - if (id === undefined || id === null) return undefined; - // 始终将ID转换为字符串,避免JavaScript大整数精度丢失 - return String(id); -}; -``` - -### 2. 修复要点 - -#### 2.1 类型安全 -- **返回类型**: 从 `number | undefined` 改为 `string | undefined` -- **处理逻辑**: 始终使用 `String()` 转换,保持原始精度 -- **兼容性**: 后端API支持 `string | number` 类型,无需修改 - -#### 2.2 精度保持 -- **字符串转换**: 使用 `String()` 而不是 `parseInt()` -- **精度完整**: 保持雪花ID的完整精度 -- **无截断**: 避免大整数被截断为 `000` - -#### 2.3 项目一致性 -- **标准做法**: 与项目中其他模块的处理方式保持一致 -- **最佳实践**: 参考项目中已有的成功案例 - -## 技术原理 - -### JavaScript 大整数精度问题 - -#### 1. 安全整数范围 -```javascript -Number.MAX_SAFE_INTEGER = 9007199254740991 // 2^53 - 1 -Number.MIN_SAFE_INTEGER = -9007199254740991 // -(2^53 - 1) -``` - -#### 2. 雪花ID特点 -- **长度**: 通常为19位数字 -- **范围**: 远超JavaScript安全整数范围 -- **精度**: 需要保持完整精度 - -#### 3. 转换对比 -```javascript -// 错误的转换方式(精度丢失) -const bigId = "1958180338534527123"; -const wrongResult = parseInt(bigId); // 1958180338534527000 (精度丢失) - -// 正确的转换方式(保持精度) -const correctResult = String(bigId); // "1958180338534527123" (完整精度) -``` - -### 后端支持 - -#### 1. API接口支持 -```typescript -// 所有ID参数都支持 string | number 类型 -export function getAssessmentApplication(id: string | number) { - return request({ - url: '/assessment/application/' + id, - method: 'get' - }); -} -``` - -#### 2. 类型定义支持 -```typescript -export interface XskhAssessmentApplicationApprovalBo { - id?: string | number; - studentId?: string | number; - collegeId?: string | number; - majorId?: string | number; - classId?: string | number; - scheduleId?: string | number; - // ... 其他字段 -} -``` - -#### 3. 后端处理 -- **Spring Boot**: 自动处理字符串到Long的转换 -- **MyBatis-Plus**: 支持字符串类型的ID参数 -- **数据库**: bigint类型可以正确处理字符串输入 - -## 验证测试 - -### 1. 功能测试 - -#### 1.1 新增申请测试 -- **测试步骤**: - 1. 打开学生考核申请界面 - 2. 选择考核计划 - 3. 填写申请信息 - 4. 点击保存 -- **预期结果**: ID字段保持完整精度,数据库记录正确 - -#### 1.2 编辑申请测试 -- **测试步骤**: - 1. 打开已有申请记录 - 2. 修改申请信息 - 3. 点击保存 -- **预期结果**: ID字段保持完整精度,更新成功 - -#### 1.3 提交申请测试 -- **测试步骤**: - 1. 打开申请记录 - 2. 点击提交 -- **预期结果**: ID字段保持完整精度,提交成功 - -### 2. 数据验证 - -#### 2.1 前端数据验证 -```javascript -// 验证ID精度 -const testId = "1958180338534527123"; -const convertedId = safeConvertId(testId); -console.log('转换前:', testId); -console.log('转换后:', convertedId); -console.log('精度保持:', testId === convertedId); // 应该为 true -``` - -#### 2.2 后端数据验证 -```sql --- 查询保存的记录,验证ID精度 -SELECT id, student_id, college_id, major_id, class_id, schedule_id -FROM xskh_assessment_application_approval -WHERE id = '1958180338534527123'; -``` - -### 3. 边界测试 - -#### 3.1 大整数测试 -- **测试数据**: 19位雪花ID -- **预期结果**: 完整精度保持 - -#### 3.2 小整数测试 -- **测试数据**: 普通整数ID -- **预期结果**: 正常转换 - -#### 3.3 空值测试 -- **测试数据**: undefined, null -- **预期结果**: 返回 undefined - -## 影响范围 - -### 1. 直接影响 -- ✅ **学生考核申请模块**: 修复完成 -- ✅ **ID精度保持**: 所有ID字段完整精度 -- ✅ **数据一致性**: 前后端数据一致 - -### 2. 间接影响 -- ✅ **API接口**: 无需修改,已支持字符串类型 -- ✅ **数据库**: 无需修改,bigint类型支持字符串输入 -- ✅ **其他模块**: 无影响 - -### 3. 兼容性 -- ✅ **向后兼容**: 不影响现有功能 -- ✅ **向前兼容**: 支持新的ID格式 -- ✅ **跨平台**: 所有平台一致 - -## 最佳实践总结 - -### 1. 大整数处理原则 -```typescript -// ✅ 正确做法:保持字符串类型 -const safeConvertId = (id: string | number | undefined): string | undefined => { - if (id === undefined || id === null) return undefined; - return String(id); -}; - -// ❌ 错误做法:转换为数字类型 -const wrongConvertId = (id: string | number | undefined): number | undefined => { - return parseInt(String(id), 10); // 可能导致精度丢失 -}; -``` - -### 2. 项目统一规范 -- **ID字段**: 始终使用 `string | number` 类型 -- **转换函数**: 保持字符串格式,避免数字转换 -- **API接口**: 支持字符串类型参数 -- **类型定义**: 明确定义支持的类型 - -### 3. 测试验证 -- **功能测试**: 验证保存、编辑、提交功能 -- **数据验证**: 检查数据库记录精度 -- **边界测试**: 测试各种ID格式 -- **兼容性测试**: 确保向后兼容 - -## 总结 - -通过本次修复,成功解决了学生考核申请界面ID精度丢失的问题: - -1. **问题根除**: 修复了JavaScript大整数精度丢失的根本原因 -2. **精度保持**: 所有ID字段现在保持完整精度 -3. **数据一致**: 前后端数据完全一致 -4. **标准统一**: 与项目其他模块的处理方式保持一致 -5. **向后兼容**: 不影响现有功能和数据 - -修复方案简单有效,符合项目的最佳实践,为后续类似问题提供了标准解决方案。 \ No newline at end of file diff --git a/docs/问题解决方案/学生考核申请LocalDateTime类型转换错误修复报告.md b/docs/问题解决方案/学生考核申请LocalDateTime类型转换错误修复报告.md deleted file mode 100644 index 24a340d2..00000000 --- a/docs/问题解决方案/学生考核申请LocalDateTime类型转换错误修复报告.md +++ /dev/null @@ -1,180 +0,0 @@ -# 学生考核申请LocalDateTime类型转换错误修复报告 - -## 问题描述 - -**时间**: 2025-01-03 -**接口**: `GET /assessment/application/student/{studentId}/schedule/{scheduleId}` -**错误信息**: -``` -{ - "code": 500, - "msg": "class java.time.LocalDateTime cannot be cast to class java.lang.String (java.time.LocalDateTime and java.lang.String are in module java.base of loader 'bootstrap')", - "data": null -} -``` - -## 问题分析 - -### 错误原因 -在 `XskhAssessmentApplicationApprovalServiceImpl.queryById()` 方法中,第83-84行存在类型转换错误: - -```java -// 错误的代码 -selfScoreVo.setCreateTime((String) scoreMap.get("createTime")); -selfScoreVo.setUpdateTime((String) scoreMap.get("updateTime")); -``` - -### 根本原因 -1. **数据库字段类型**: `create_time` 和 `update_time` 字段在数据库中定义为 `datetime` 类型 -2. **MyBatis查询返回**: 通过 `selectSelfScoresWithIndicatorInfo` 查询时,MyBatis可能返回 `LocalDateTime` 类型 -3. **强制类型转换**: 代码中直接强制转换为 `String` 类型,导致 `ClassCastException` - -### 影响范围 -- 学生考核申请详情查询接口 -- 学生考核申请表单页面无法正常加载历史数据 - -## 解决方案 - -### 1. 修复Service实现类中的类型转换 - -**文件**: `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhAssessmentApplicationApprovalServiceImpl.java` - -**修改内容**: -```java -// 修复前 -selfScoreVo.setCreateTime((String) scoreMap.get("createTime")); -selfScoreVo.setUpdateTime((String) scoreMap.get("updateTime")); - -// 修复后 -Object createTimeObj = scoreMap.get("createTime"); -if (createTimeObj != null) { - selfScoreVo.setCreateTime(createTimeObj.toString()); -} -Object updateTimeObj = scoreMap.get("updateTime"); -if (updateTimeObj != null) { - selfScoreVo.setUpdateTime(updateTimeObj.toString()); -} -``` - -### 2. 添加JSON格式化注解 - -**文件**: `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhAssessmentSelfScoreVo.java` - -**修改内容**: -```java -import com.fasterxml.jackson.annotation.JsonFormat; - -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private String createTime; - -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private String updateTime; -``` - -**文件**: `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhAssessmentBonusApprovalVo.java` - -**修改内容**: -```java -import com.fasterxml.jackson.annotation.JsonFormat; - -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private String createTime; - -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private String updateTime; -``` - -**文件**: `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhAssessmentApplicationApprovalVo.java` - -**修改内容**: -```java -import com.fasterxml.jackson.annotation.JsonFormat; - -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private Date submitTime; - -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private Date teacherApprovalTime; - -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private Date createTime; - -@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") -private Date updateTime; -``` - -## 修复效果 - -### 修复前 -- 接口返回500错误 -- 前端无法正常显示考核申请详情 -- 用户无法查看历史申请记录 - -### 修复后 -- 接口正常返回数据 -- 前端可以正常显示考核申请详情 -- 时间字段格式化为标准格式 `yyyy-MM-dd HH:mm:ss` -- 支持查看历史申请记录 - -## 技术要点 - -### 1. 类型安全处理 -- 使用 `Object` 类型接收查询结果 -- 通过 `toString()` 方法安全转换 -- 添加空值检查避免NPE - -### 2. JSON序列化优化 -- 添加 `@JsonFormat` 注解统一时间格式 -- 确保前后端时间格式一致 -- 提升API响应质量 - -### 3. 数据库字段映射 -- 保持与BaseEntity基类的一致性 -- 遵循项目的时间字段处理规范 -- 避免类型转换异常 - -## 验证方法 - -### 1. 接口测试 -```bash -# 测试学生考核申请查询接口 -GET /assessment/application/student/1/schedule/1958180338534526978 -``` - -### 2. 前端验证 -- 打开学生考核申请表单页面 -- 验证历史申请记录正常显示 -- 检查时间字段格式是否正确 - -### 3. 日志检查 -- 查看后端日志确认无类型转换错误 -- 验证SQL查询正常执行 -- 确认数据正确返回 - -## 预防措施 - -### 1. 代码规范 -- 避免直接强制类型转换 -- 使用安全的类型转换方法 -- 添加适当的空值检查 - -### 2. 测试覆盖 -- 添加单元测试覆盖时间字段处理 -- 集成测试验证API响应格式 -- 前端测试确保UI正常显示 - -### 3. 文档维护 -- 更新API文档说明时间字段格式 -- 记录VO类字段类型定义 -- 维护问题解决方案文档 - -## 总结 - -本次修复解决了学生考核申请模块中的LocalDateTime类型转换错误,通过安全的类型处理和JSON格式化注解,确保了接口的稳定性和数据格式的一致性。修复后的代码更加健壮,能够正确处理各种时间类型的数据,提升了用户体验。 - ---- - -**修复人员**: AI Assistant -**修复时间**: 2025-01-03 -**影响模块**: 学生考核管理模块 -**测试状态**: 待验证 \ No newline at end of file diff --git a/docs/问题解决方案/学生考核申请更新主键冲突修复报告.md b/docs/问题解决方案/学生考核申请更新主键冲突修复报告.md deleted file mode 100644 index b5174da1..00000000 --- a/docs/问题解决方案/学生考核申请更新主键冲突修复报告.md +++ /dev/null @@ -1,144 +0,0 @@ -# 学生考核申请更新主键冲突修复报告 - -## 问题描述 - -### 现象 -在考核申请表单中,对已有数据进行修改后点击保存按钮,调用PUT接口更新数据时出现主键冲突错误: - -``` -Duplicate entry '1958434137320771585' for key 'PRIMARY' -``` - -### 错误详情 -- **接口**: `PUT /assessment/application` -- **错误类型**: `java.sql.SQLIntegrityConstraintViolationException` -- **错误位置**: `XskhAssessmentSelfScoreMapper.insert` -- **根本原因**: 更新操作误用插入逻辑 - -## 问题分析 - -### 根本原因 -在 `XskhAssessmentApplicationApprovalServiceImpl.updateSelfScores()` 和 `updateBonusApprovals()` 方法中,更新逻辑存在严重缺陷: - -**原有错误逻辑**: -1. 删除所有关联的子表记录 -2. 重新插入新的记录 - -**问题所在**: -- 前端传递的数据包含已存在记录的ID -- 后端删除后重新插入时,由于ID已存在导致主键冲突 -- 这种"删除重建"的方式会丢失原有的创建时间、创建人等审计信息 - -### 影响范围 -- 学生考核自评得分表 (`xskh_assessment_self_score`) -- 附加分申请表 (`xskh_assessment_bonus_approval`) - -## 解决方案 - -### 修复策略 -采用"智能更新"策略,根据记录ID是否存在决定执行更新或插入操作: - -```java -// 如果ID存在,则更新;否则新增 -if (selfScore.getId() != null) { - // 更新现有记录 - selfScoreMapper.updateById(selfScore); -} else { - // 新增记录 - selfScoreMapper.insert(selfScore); -} -``` - -### 具体修改 - -#### 1. 修复自评得分更新逻辑 -**文件**: `XskhAssessmentApplicationApprovalServiceImpl.java` -**方法**: `updateSelfScores()` - -```java -private void updateSelfScores(Long applicationId, List selfScoreList) { - if (selfScoreList != null && !selfScoreList.isEmpty()) { - for (XskhAssessmentSelfScoreBo selfScoreBo : selfScoreList) { - XskhAssessmentSelfScore selfScore = MapstructUtils.convert(selfScoreBo, XskhAssessmentSelfScore.class); - selfScore.setApplicationId(applicationId); - - // 如果ID存在,则更新;否则新增 - if (selfScore.getId() != null) { - // 更新现有记录 - selfScoreMapper.updateById(selfScore); - } else { - // 新增记录 - selfScoreMapper.insert(selfScore); - } - } - } -} -``` - -#### 2. 修复附加分申请更新逻辑 -**文件**: `XskhAssessmentApplicationApprovalServiceImpl.java` -**方法**: `updateBonusApprovals()` - -```java -private void updateBonusApprovals(Long applicationId, List bonusApprovalList) { - if (bonusApprovalList != null && !bonusApprovalList.isEmpty()) { - for (XskhAssessmentBonusApprovalBo bonusApprovalBo : bonusApprovalList) { - XskhAssessmentBonusApproval bonusApproval = MapstructUtils.convert(bonusApprovalBo, - XskhAssessmentBonusApproval.class); - bonusApproval.setApplicationId(applicationId); - - // 如果ID存在,则更新;否则新增 - if (bonusApproval.getId() != null) { - // 更新现有记录 - bonusApprovalMapper.updateById(bonusApproval); - } else { - // 新增记录,设置默认状态 - bonusApproval.setStatus("1"); // 默认待审核状态 - bonusApprovalMapper.insert(bonusApproval); - } - } - } -} -``` - -## 修复效果 - -### 优势 -1. **保留审计信息**: 不再删除重建,保留原有的创建时间、创建人等字段 -2. **避免主键冲突**: 根据ID存在性智能选择更新或插入操作 -3. **数据完整性**: 确保数据的连续性和完整性 -4. **性能优化**: 避免不必要的删除和插入操作 - -### 测试验证 -- ✅ 新增考核申请:正常插入新记录 -- ✅ 修改考核申请:正常更新现有记录 -- ✅ 混合操作:同时包含新增和修改的记录 -- ✅ 数据完整性:审计字段正确保留 - -## 经验总结 - -### 开发规范 -1. **更新操作原则**: 避免"删除重建"模式,优先使用"智能更新" -2. **ID处理策略**: 根据ID存在性决定操作类型 -3. **审计字段保护**: 更新时注意保护创建时间、创建人等审计信息 -4. **事务完整性**: 确保关联数据的更新在同一个事务中 - -### 代码审查要点 -- 检查更新逻辑是否采用"删除重建"模式 -- 验证是否正确处理已存在记录的ID -- 确认审计字段的保留策略 -- 测试混合操作场景(新增+修改) - -## 相关文件 - -- **修复文件**: `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/service/impl/XskhAssessmentApplicationApprovalServiceImpl.java` -- **涉及表**: - - `xskh_assessment_self_score` (学生考核自评得分表) - - `xskh_assessment_bonus_approval` (附加分申请表) -- **相关接口**: `PUT /assessment/application` - ---- - -**修复时间**: 2025-08-21 -**修复人员**: AI Assistant -**状态**: 已完成 \ No newline at end of file diff --git a/docs/问题解决方案/学生考核自评得分查询字段错误修复报告.md b/docs/问题解决方案/学生考核自评得分查询字段错误修复报告.md deleted file mode 100644 index 40a3fbbb..00000000 --- a/docs/问题解决方案/学生考核自评得分查询字段错误修复报告.md +++ /dev/null @@ -1,153 +0,0 @@ -# 学生考核自评得分查询字段错误修复报告 - -## 问题描述 - -**错误时间**: 2025-08-21 11:53:24 -**错误接口**: `/assessment/approval/1958361581653856257` -**错误类型**: SQL语法错误 - 未知字段 - -### 错误详情 -``` -org.springframework.jdbc.BadSqlGrammarException: -Unknown column 'i.indicator_description' in 'field list' -``` - -### 错误SQL -```sql -SELECT s.id, s.application_id, s.indicator_id, s.indicator_name, - i.indicator_code, i.indicator_description, s.indicator_type, - s.max_score, s.self_score, s.self_remark, s.teacher_score, - s.teacher_remark, s.create_by, s.create_time, s.update_by, s.update_time -FROM xskh_assessment_self_score s -LEFT JOIN xskh_assessment_indicator i ON s.indicator_id = i.id AND i.tenant_id = '000000' -WHERE s.application_id = ? AND s.deleted = 0 AND s.tenant_id = '000000' -ORDER BY s.create_time ASC -``` - -## 问题分析 - -### 根本原因 -1. **字段名不匹配**: SQL查询中使用了 `i.indicator_description`,但数据库表中实际字段名为 `description` -2. **数据库表结构**: `xskh_assessment_indicator` 表中的字段名是 `description`,不是 `indicator_description` - -### 数据库表结构确认 -```sql --- xskh_assessment_indicator 表结构 -CREATE TABLE `xskh_assessment_indicator` ( - `id` bigint(20) NOT NULL COMMENT '主键ID', - `indicator_code` varchar(50) NOT NULL COMMENT '指标编码', - `indicator_name` varchar(100) NOT NULL COMMENT '指标名称', - `indicator_type_id` bigint(20) NOT NULL COMMENT '指标类型ID', - `indicator_type_name` varchar(100) NOT NULL COMMENT '指标类型名称', - `description` varchar(500) DEFAULT NULL COMMENT '指标说明', -- 正确字段名 - `max_score` decimal(5,2) NOT NULL COMMENT '最高分值', - -- ... 其他字段 -); -``` - -## 修复方案 - -### 修复文件 -- **文件路径**: `backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/xskh/XskhAssessmentSelfScoreMapper.xml` -- **修复方法**: 将SQL查询中的字段名从 `i.indicator_description` 改为 `i.description` - -### 字段命名统一优化 -为了保持前后端字段命名的一致性,同时进行了以下优化: - -1. **VO类字段统一**: 将 `indicatorDescription` 改为 `description` -2. **Mapper映射统一**: 更新ResultMap中的字段映射 -3. **相关文件同步**: 修改了所有相关的Mapper文件和VO类 - -### 具体修复内容 - -#### 1. 修复ResultMap映射 -```xml - - - - - -``` - -#### 2. 修复SQL查询语句 -```xml - -SELECT - s.id, s.application_id, s.indicator_id, s.indicator_name, - i.indicator_code, i.indicator_description, s.indicator_type, - -- ... 其他字段 - - -SELECT - s.id, s.application_id, s.indicator_id, s.indicator_name, - i.indicator_code, i.description, s.indicator_type, - -- ... 其他字段 -``` - -### 修复后的完整SQL -```sql -SELECT - s.id, - s.application_id, - s.indicator_id, - s.indicator_name, - i.indicator_code, - i.description, -- 修复:使用正确的字段名 - s.indicator_type, - s.max_score, - s.self_score, - s.self_remark, - s.teacher_score, - s.teacher_remark, - s.create_by, - s.create_time, - s.update_by, - s.update_time -FROM xskh_assessment_self_score s -LEFT JOIN xskh_assessment_indicator i ON s.indicator_id = i.id AND i.tenant_id = '000000' -WHERE s.application_id = #{applicationId} -AND s.deleted = 0 -AND s.tenant_id = '000000' -ORDER BY s.create_time ASC -``` - -## 验证结果 - -### 修复验证 -1. ✅ **字段映射正确**: ResultMap中的 `description` 字段正确映射到数据库的 `description` 字段 -2. ✅ **SQL语法正确**: 查询语句使用正确的字段名,符合数据库表结构 -3. ✅ **VO类兼容**: `XskhAssessmentSelfScoreVo` 类中已正确定义 `description` 字段 -4. ✅ **字段命名统一**: 前后端字段名保持一致,避免混淆 - -### 测试建议 -1. **接口测试**: 重新调用 `/assessment/approval/{id}` 接口,确认不再出现字段错误 -2. **数据验证**: 确认返回的指标说明数据正确显示 -3. **功能测试**: 测试学生考核审核功能,确认自评得分查询正常 - -## 经验总结 - -### 开发规范提醒 -1. **字段命名一致性**: SQL查询中的字段名必须与数据库表结构完全一致 -2. **数据库设计规范**: 遵循项目统一的字段命名规范,避免混淆 -3. **代码审查**: 在开发过程中注意检查SQL语句的字段名正确性 - -### 预防措施 -1. **数据库文档**: 及时更新数据库设计文档,确保字段名准确 -2. **代码规范**: 建立SQL编写规范,要求字段名与数据库表结构保持一致 -3. **测试覆盖**: 增加SQL语法检查的自动化测试 - -## 相关文件 - -- **数据库设计**: `docs/设计文档/database/学生考核管理/学生考核管理数据库模型.sql` -- **Mapper文件**: - - `backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/xskh/XskhAssessmentSelfScoreMapper.xml` - - `backend/ruoyi-modules/ruoyi-xskh/src/main/resources/mapper/xskh/XskhAssessmentPlanMapper.xml` -- **VO类**: - - `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhAssessmentSelfScoreVo.java` - - `backend/ruoyi-modules/ruoyi-xskh/src/main/java/org/dromara/xskh/domain/vo/XskhAssessmentPlanIndicatorVo.java` - ---- - -**修复人员**: Cursor AI -**修复时间**: 2025-08-21 -**修复状态**: ✅ 已完成 \ No newline at end of file diff --git a/mobile/src/pages/enrollment/index.vue b/mobile/src/pages/enrollment/index.vue index 4c4a86a4..843a370c 100644 --- a/mobile/src/pages/enrollment/index.vue +++ b/mobile/src/pages/enrollment/index.vue @@ -1,1694 +1,1765 @@ diff --git a/web/package.json b/web/package.json index 68f51ada..5e29e2d3 100644 --- a/web/package.json +++ b/web/package.json @@ -34,7 +34,7 @@ "await-to-js": "3.0.0", "axios": "1.8.4", "crypto-js": "4.2.0", - "echarts": "5.6.0", + "echarts": "^5.6.0", "element-plus": "2.9.8", "file-saver": "2.0.5", "highlight.js": "11.9.0", diff --git a/web/src/api/bzrkh/assessmentApproval/index.ts b/web/src/api/bzrkh/assessmentApproval/index.ts index 133d9224..fb6a0087 100644 --- a/web/src/api/bzrkh/assessmentApproval/index.ts +++ b/web/src/api/bzrkh/assessmentApproval/index.ts @@ -1,6 +1,12 @@ import request from '@/utils/request'; import { AxiosPromise } from 'axios'; -import { AssessmentApprovalQuery, AssessmentApprovalVO, AssessmentApprovalDetailVO, AssessmentApprovalSubmitBO, EvaluationLevelStatsVO } from './types'; +import { + AssessmentApprovalQuery, + AssessmentApprovalVO, + AssessmentApprovalDetailVO, + AssessmentApprovalSubmitBO, + EvaluationLevelStatsVO +} from './types'; /** * 查询班主任考核审核列表 diff --git a/web/src/api/bzrkh/assessmentApproval/types.ts b/web/src/api/bzrkh/assessmentApproval/types.ts index ce8c6dbe..8d06ba56 100644 --- a/web/src/api/bzrkh/assessmentApproval/types.ts +++ b/web/src/api/bzrkh/assessmentApproval/types.ts @@ -224,11 +224,12 @@ export interface AssessmentApprovalSpecialWorkSubmitVO { * 评价等级统计数据 */ export interface EvaluationLevelStatsVO { - totalTeachers: number; // 总班主任数量 + totalTeachers: number; // 总班主任数量 scoreRankings: ScoreRankingVO[]; // 分数排名列表 - evaluationLevelRules: { // 评价等级规则 - aLevelMaxPercentage: number; // A等最大占比(如20%) - passScore: number; // 及格分数(如60分) + evaluationLevelRules: { + // 评价等级规则 + aLevelMaxPercentage: number; // A等最大占比(如20%) + passScore: number; // 及格分数(如60分) }; } @@ -236,8 +237,8 @@ export interface EvaluationLevelStatsVO { * 分数排名VO */ export interface ScoreRankingVO { - score: number; // 考核得分 - ranking: number; // 排名(从1开始) - percentage: number; // 百分比排名(前X%) + score: number; // 考核得分 + ranking: number; // 排名(从1开始) + percentage: number; // 百分比排名(前X%) evaluatedUserId?: string | number; // 被评价用户ID(可选,用于标识当前用户) } diff --git a/web/src/api/cw/projectApproval.ts b/web/src/api/cw/projectApproval.ts new file mode 100644 index 00000000..e83a4cec --- /dev/null +++ b/web/src/api/cw/projectApproval.ts @@ -0,0 +1,217 @@ +import request from '@/utils/request'; +import type { AxiosPromise } from 'axios'; +import type { PageQuery, TableDataInfo } from '@/api/types'; + +/** + * 项目审批流程查询参数 + */ +export interface CwProjectApprovalQuery extends PageQuery { + applicationNo?: string; + projectName?: string; + processStatus?: string; + submitterName?: string; + startTime?: string[]; +} + +/** + * 项目审批流程VO + */ +export interface CwProjectApprovalProcessVO { + id: string | number; + applicationId: string | number; + applicationNo: string; + processName: string; + processStatus: string; + totalNodes: number; + currentNode: number; + startTime: string; + endTime: string; + submitterId: string | number; + submitterName: string; + createTime: string; + updateTime: string; +} + +/** + * 审批记录VO + */ +export interface CwApprovalRecordVO { + id: string | number; + applicationId: string | number; + applicationNo: string; + processId: string | number; + nodeCode: string; + nodeName: string; + customNodeName: string; + nodeOrder: number; + approverId: string | number; + approverName: string; + nodeStatus: string; + approveAction: string; + approveOpinion: string; + startTime: string; + endTime: string; + isCustomProcess: boolean; + createTime: string; + updateTime: string; +} + +/** + * 自定义审批流程启动请求 + */ +export interface CwCustomApprovalRequest { + applicationId: string | number; + applicationNo: string; + auditorIds: Array; + processName: string; + submitterId: string | number; + submitterName: string; +} + +/** + * 审批操作请求 + */ +export interface CwApprovalActionRequest { + recordId: string | number; + action: string; // approve | reject + opinion: string; +} + +/** + * 查询项目审批流程列表 + */ +export function listProjectApproval(query: CwProjectApprovalQuery): AxiosPromise> { + return request({ + url: '/cw/projectApproval/list', + method: 'get', + params: query + }); +} + +/** + * 获取项目审批流程详细信息 + */ +export function getProjectApproval(id: string | number): AxiosPromise { + return request({ + url: '/cw/projectApproval/' + id, + method: 'get' + }); +} + +/** + * 启动自定义审批流程 + */ +export function startCustomApprovalProcess(data: CwCustomApprovalRequest): AxiosPromise { + return request({ + url: '/cw/projectApproval/start', + method: 'post', + data: data + }); +} + +/** + * 执行审批操作(统一接口) + */ +export function approveProcess(data: CwApprovalActionRequest): AxiosPromise { + return request({ + url: '/cw/projectApproval/approve', + method: 'put', + data: data + }); +} + +/** + * 打回申请(统一接口) + */ +export function rejectProcess(data: CwApprovalActionRequest): AxiosPromise { + return request({ + url: '/cw/projectApproval/approve', + method: 'put', + data: data + }); +} + +/** + * 终止审批流程 + */ +export function terminateProcess(id: string | number): AxiosPromise { + return request({ + url: '/cw/projectApproval/' + id, + method: 'delete' + }); +} + +/** + * 获取我的待审批列表 + */ +export function getMyPendingApprovals(query?: CwProjectApprovalQuery): AxiosPromise { + return request({ + url: '/cw/projectApproval/pending', + method: 'get', + params: query + }); +} + +/** + * 获取我的已审批列表 + */ +export function getMyProcessedApprovals(query?: CwProjectApprovalQuery): AxiosPromise { + return request({ + url: '/cw/projectApproval/processed', + method: 'get', + params: query + }); +} + +/** + * 获取审批记录 + */ +export function getApprovalRecords(applicationId: string | number): AxiosPromise { + return request({ + url: '/cw/projectApproval/records/' + applicationId, + method: 'get' + }); +} + +/** + * 验证审批流程 + */ +export function validateApprovalProcess(data: CwCustomApprovalRequest): AxiosPromise { + return request({ + url: '/cw/projectApproval/validate', + method: 'post', + data: data + }); +} + +/** + * 获取审批流程统计数据 + */ +export function getApprovalStatistics(): AxiosPromise { + return request({ + url: '/cw/projectApproval/statistics', + method: 'get' + }); +} + +/** + * 导出项目审批流程 + */ +export function exportProjectApproval(query: CwProjectApprovalQuery): AxiosPromise { + return request({ + url: '/cw/projectApproval/export', + method: 'post', + data: query, + responseType: 'blob' + }); +} + +/** + * 获取可用审核人选项 + */ +export function getAuditorOptions(): AxiosPromise { + return request({ + url: '/cw/projectAuditor/options', + method: 'get' + }); +} diff --git a/web/src/api/cw/projectAuditor.ts b/web/src/api/cw/projectAuditor.ts new file mode 100644 index 00000000..9f4a1027 --- /dev/null +++ b/web/src/api/cw/projectAuditor.ts @@ -0,0 +1,274 @@ +import request from '@/utils/request'; +import { AxiosPromise } from 'axios'; + +/** + * 项目审核人配置VO对象 + */ +export interface CwProjectAuditorVO { + id: string | number; + userId: string | number; + userName: string; + nickName: string; + deptId: string | number; + deptName: string; + phoneNumber: string; + email: string; + status: string; + remark: string; + configBy: string | number; + configByName: string; + createTime: string; + updateTime: string; + createBy: string | number; + updateBy: string | number; +} + +/** + * 项目审核人配置表单对象 + */ +export interface CwProjectAuditorForm { + id?: string | number; + userId: string | number; + userName: string; + nickName: string; + deptId: string | number; + deptName: string; + phoneNumber?: string; + email?: string; + status: string; + remark?: string; + configBy?: string | number; + configByName?: string; +} + +/** + * 项目审核人配置查询对象 + */ +export interface CwProjectAuditorQuery { + pageNum: number; + pageSize: number; + userName?: string; + nickName?: string; + deptId?: string | number; + status?: string; + configBy?: string | number; + createTime?: string[]; +} + +/** + * 批量添加请求对象 + */ +export interface BatchAddRequest { + userIds: Array; + remark?: string; +} + +/** + * 批量添加审核人详细请求对象(包含完整用户信息) + */ +export interface BatchAddDetailRequest { + users: Array<{ + userId: string | number; + userName: string; + nickName: string; + deptId: string | number; + deptName: string; + phoneNumber: string; // 注意:这里使用驼峰命名,确保与数据库字段映射 + email: string; + }>; + remark?: string; +} + +/** + * 状态更新请求对象 + */ +export interface StatusUpdateRequest { + ids: Array; + status: string; +} + +/** + * 部门用户对象 + */ +export interface DeptUserVO { + userId: string | number; + userName: string; + nickName: string; + deptId: string | number; + deptName: string; + phonenumber: string; + email: string; + status: string; +} + +/** + * 查询项目审核人配置列表 + */ +export function listProjectAuditor(query: CwProjectAuditorQuery): AxiosPromise<{ rows: CwProjectAuditorVO[]; total: number }> { + return request({ + url: '/cw/projectAuditor/list', + method: 'get', + params: query + }); +} + +/** + * 查询项目审核人配置详细 + */ +export function getProjectAuditor(id: string | number): AxiosPromise { + return request({ + url: '/cw/projectAuditor/' + id, + method: 'get' + }); +} + +/** + * 新增项目审核人配置 + */ +export function addProjectAuditor(data: CwProjectAuditorForm): AxiosPromise { + return request({ + url: '/cw/projectAuditor', + method: 'post', + data: data + }); +} + +/** + * 修改项目审核人配置 + */ +export function updateProjectAuditor(data: CwProjectAuditorForm): AxiosPromise { + return request({ + url: '/cw/projectAuditor', + method: 'put', + data: data + }); +} + +/** + * 删除项目审核人配置 + */ +export function delProjectAuditor(ids: Array): AxiosPromise { + return request({ + url: '/cw/projectAuditor/' + ids.join(','), + method: 'delete' + }); +} + +/** + * 批量添加审核人 + */ +export function batchAddProjectAuditor(data: BatchAddRequest): AxiosPromise { + return request({ + url: '/cw/projectAuditor/batchAdd', + method: 'post', + data: data + }); +} + +// 注释掉:后端暂不支持带完整用户信息的批量添加接口 +// 使用 batchAddProjectAuditor 接口,后端会自动查询用户完整信息 +/** + * 批量添加审核人(带完整用户信息)- 已弃用 + * 改用 batchAddProjectAuditor,后端会自动获取用户完整信息 + */ +// export function batchAddProjectAuditorWithDetails(data: BatchAddDetailRequest): AxiosPromise { +// return request({ +// url: '/cw/projectAuditor/batchAddWithDetails', +// method: 'post', +// data: data +// }); +// } + +/** + * 获取可用审核人列表 + */ +export function listAvailableAuditors(): AxiosPromise { + return request({ + url: '/cw/projectAuditor/available', + method: 'get' + }); +} + +/** + * 根据部门ID获取用户列表 + */ +export function listUserByDeptId(deptId: string | number): AxiosPromise { + return request({ + url: '/cw/projectAuditor/deptUsers/' + deptId, + method: 'get' + }); +} + +/** + * 批量更新状态 + */ +export function updateAuditorStatus(data: StatusUpdateRequest): AxiosPromise { + return request({ + url: '/cw/projectAuditor/status', + method: 'put', + data: data + }); +} + +/** + * 我配置的审核人 + */ +export function listMyConfigAuditors(query: CwProjectAuditorQuery): AxiosPromise<{ rows: CwProjectAuditorVO[]; total: number }> { + return request({ + url: '/cw/projectAuditor/myConfig', + method: 'get', + params: query + }); +} + +/** + * 获取审核人选项(用于下拉选择) + */ +export function listAuditorOptions(): AxiosPromise { + return request({ + url: '/cw/projectAuditor/options', + method: 'get' + }); +} + +/** + * 批量启用 + */ +export function enableAuditors(ids: Array): AxiosPromise { + return request({ + url: '/cw/projectAuditor/enable/' + ids.join(','), + method: 'put' + }); +} + +/** + * 批量停用 + */ +export function disableAuditors(ids: Array): AxiosPromise { + return request({ + url: '/cw/projectAuditor/disable/' + ids.join(','), + method: 'put' + }); +} + +/** + * 导出项目审核人配置 + */ +export function exportProjectAuditor(query: CwProjectAuditorQuery): AxiosPromise { + return request({ + url: '/cw/projectAuditor/export', + method: 'post', + data: query, + responseType: 'blob' + }); +} + +/** + * 获取审核人统计数据 + */ +export function getAuditorStatistics(): AxiosPromise { + return request({ + url: '/cw/projectAuditor/statistics', + method: 'get' + }); +} diff --git a/web/src/api/cw/types.ts b/web/src/api/cw/types.ts index 11267e70..dfec69c2 100644 --- a/web/src/api/cw/types.ts +++ b/web/src/api/cw/types.ts @@ -203,3 +203,183 @@ export interface StatusDistribution { count: number; percentage: number; } + +// ==================== 项目审批人配置相关类型 ==================== + +// 项目审批人配置查询对象 +export interface CwProjectAuditorQuery { + pageNum?: number; + pageSize?: number; + userName?: string; + nickName?: string; + deptId?: string | number; + status?: string; + configTime?: [string, string]; + configBy?: string | number; +} + +// 项目审批人配置视图对象 +export interface CwProjectAuditorVO { + id?: string | number; + userId?: string | number; + userName?: string; + nickName?: string; + deptId?: string | number; + deptName?: string; + phoneNumber?: string; + email?: string; + status?: string; + remark?: string; + configBy?: string | number; + configByName?: string; + createBy?: string | number; + createTime?: string; + updateBy?: string | number; + updateTime?: string; +} + +// 项目审批人配置表单对象 +export interface CwProjectAuditorForm { + id?: string | number; + userId?: string | number; + userName?: string; + nickName?: string; + deptId?: string | number; + deptName?: string; + phoneNumber?: string; + email?: string; + status?: string; + remark?: string; +} + +// 批量添加审批人业务对象 +export interface CwProjectAuditorBatchBo { + userIds: Array; + configBy: string | number; + configByName: string; + remark?: string; +} + +// ==================== 项目审批流程相关类型 ==================== + +// 审批流程查询对象 +export interface CwProjectApprovalProcessQuery { + pageNum?: number; + pageSize?: number; + applicationNo?: string; + processName?: string; + processStatus?: string; + submitterId?: string | number; + submitterName?: string; + startTime?: [string, string]; + endTime?: [string, string]; +} + +// 审批流程视图对象 +export interface CwProjectApprovalProcessVO { + id?: string | number; + applicationId?: string | number; + applicationNo?: string; + processName?: string; + processStatus?: string; + totalNodes?: number; + currentNode?: number; + startTime?: string; + endTime?: string; + submitterId?: string | number; + submitterName?: string; + createBy?: string | number; + createTime?: string; + updateBy?: string | number; + updateTime?: string; +} + +// 审批流程表单对象 +export interface CwProjectApprovalProcessForm { + id?: string | number; + applicationId?: string | number; + applicationNo?: string; + processName?: string; + processStatus?: string; + totalNodes?: number; + currentNode?: number; + startTime?: string; + endTime?: string; + submitterId?: string | number; + submitterName?: string; +} + +// 审批记录查询对象 +export interface CwApprovalRecordQuery { + pageNum?: number; + pageSize?: number; + applicationId?: string | number; + applicationNo?: string; + processId?: string | number; + approverId?: string | number; + nodeStatus?: string; + approveAction?: string; + isCustomProcess?: boolean; + startTime?: [string, string]; + endTime?: [string, string]; +} + +// 审批记录视图对象 +export interface CwApprovalRecordVO { + id?: string | number; + applicationId?: string | number; + applicationNo?: string; + processId?: string | number; + nodeCode?: string; + nodeName?: string; + customNodeName?: string; + nodeOrder?: number; + approverId?: string | number; + approverName?: string; + nodeStatus?: string; + approveAction?: string; + approveOpinion?: string; + startTime?: string; + endTime?: string; + isCustomProcess?: boolean; + createBy?: string | number; + createTime?: string; + updateBy?: string | number; + updateTime?: string; + // 关联的申请信息 + projectName?: string; + submitterName?: string; + totalInvestment?: number; +} + +// 自定义审批业务对象 +export interface CwCustomApprovalBo { + applicationId: string | number; + auditorIds: Array; + processName?: string; + submitterId?: string | number; + submitterName?: string; +} + +// 审批操作业务对象 +export interface CwApprovalActionBo { + recordId: string | number; + action: string; // 'approve' | 'reject' + opinion: string; + approverId?: string | number; + approverName?: string; +} + +// ===================================================== +// 扩展审批记录视图对象 +// ===================================================== +export interface CwApprovalRecordExtendedVO extends CwApprovalRecordVO { + processId?: string | number; + customNodeName?: string; + isCustomProcess?: boolean; + + // 关联项目信息(用于显示) + projectName?: string; + totalInvestment?: number; + applicantName?: string; +} diff --git a/web/src/api/stgl/activityteaminfo/index.ts b/web/src/api/stgl/activityteaminfo/index.ts index 8cd32b24..f2046618 100644 --- a/web/src/api/stgl/activityteaminfo/index.ts +++ b/web/src/api/stgl/activityteaminfo/index.ts @@ -59,4 +59,4 @@ export function getTeamsByActivityId(activityId: string | number) { url: '/stgl/activityteaminfo/activity/' + activityId, method: 'get' }); -} \ No newline at end of file +} diff --git a/web/src/api/stgl/activityteaminfo/types.ts b/web/src/api/stgl/activityteaminfo/types.ts index 39dad5a4..1f0a8445 100644 --- a/web/src/api/stgl/activityteaminfo/types.ts +++ b/web/src/api/stgl/activityteaminfo/types.ts @@ -238,4 +238,4 @@ export interface StglActivityTeamInfoVo { * 更新时间 */ updateTime?: string; -} \ No newline at end of file +} diff --git a/web/src/api/stgl/stclass/index.ts b/web/src/api/stgl/stclass/index.ts index 6dd6a53f..aad3ff60 100644 --- a/web/src/api/stgl/stclass/index.ts +++ b/web/src/api/stgl/stclass/index.ts @@ -61,4 +61,4 @@ export function exportStglClass(query: StglClassQuery) { method: 'get', params: query }); -} \ No newline at end of file +} diff --git a/web/src/api/stgl/stclass/types.ts b/web/src/api/stgl/stclass/types.ts index b075ad4a..68ef621a 100644 --- a/web/src/api/stgl/stclass/types.ts +++ b/web/src/api/stgl/stclass/types.ts @@ -299,4 +299,4 @@ export interface StglClassDetailVO { * 年级 */ grade: string; -} \ No newline at end of file +} diff --git a/web/src/api/system/dept/index.ts b/web/src/api/system/dept/index.ts index 61775067..55ed5ffb 100644 --- a/web/src/api/system/dept/index.ts +++ b/web/src/api/system/dept/index.ts @@ -71,3 +71,11 @@ export const delDept = (deptId: number | string) => { method: 'delete' }); }; + +// 查询部门选项列表 (实际调用 list 接口) +export const listDeptOptions = (): AxiosPromise => { + return request({ + url: '/system/dept/list', + method: 'get' + }); +}; diff --git a/web/src/api/types.ts b/web/src/api/types.ts index 411e3aa3..dc72b2ef 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -67,6 +67,14 @@ export interface R { data: T; } +/** + * 分页查询基础接口 + */ +export interface PageQuery { + pageNum?: number; + pageSize?: number; +} + /** * 分页数据信息 */ diff --git a/web/src/api/xg/studentStatistics.ts b/web/src/api/xg/studentStatistics.ts new file mode 100644 index 00000000..6531e785 --- /dev/null +++ b/web/src/api/xg/studentStatistics.ts @@ -0,0 +1,86 @@ +import request from '@/utils/request' + +export interface StudentStatisticsBo { + startTime: string + endTime: string + collegeId?: number + majorId?: number + classId?: number +} + +export interface StatisticsChartData { + name: string + dormDeductionCount: number + dormViolationCount: number + dormViolationDeduction: number + teachingViolationCount: number + teachingViolationDeduction: number +} + +export interface StatisticsDetailData { + id: number + name: string + parentName: string + dormDeductionCount: number + dormViolationCount: number + dormViolationDeduction: number + teachingViolationCount: number + teachingViolationDeduction: number + totalDeduction: number +} + +export interface StudentStatisticsVo { + dimension: string + chartData: StatisticsChartData[] + detailData: StatisticsDetailData[] +} + +export interface CascadeOptionVo { + id: number + label: string + value: string + sort: number +} + +// 获取统计数据 +export function getStatistics(data: StudentStatisticsBo) { + return request({ + url: '/xg/studentStatistics/statistics', + method: 'post', + data: data + }) +} + +// 导出统计数据 +export function exportStatistics(data: StudentStatisticsBo) { + return request({ + url: '/xg/studentStatistics/export', + method: 'post', + data: data, + responseType: 'blob' + }) +} + +// 获取二级学院列表 +export function getColleges() { + return request({ + url: '/xg/studentStatistics/colleges', + method: 'get' + }) +} + +// 根据学院ID获取专业列表 +export function getMajorsByCollegeId(collegeId: number) { + return request({ + url: `/xg/studentStatistics/majors/${collegeId}`, + method: 'get' + }) +} + +// 根据专业ID获取班级列表 +export function getClassesByMajorId(majorId: number) { + return request({ + url: `/xg/studentStatistics/classes/${majorId}`, + method: 'get' + }) +} diff --git a/web/src/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/index.ts b/web/src/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/index.ts index 5847317e..c182c13c 100644 --- a/web/src/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/index.ts +++ b/web/src/api/xg/xgss/dormHygieneCheck/dormHygieneCheck/index.ts @@ -1,5 +1,6 @@ import request from '@/utils/request'; import { AxiosPromise } from 'axios'; +import { TableDataInfo } from '@/api/types'; import { DormHygieneCheckVO, DormHygieneCheckForm, @@ -16,7 +17,7 @@ import { * @returns {*} */ -export const listDormHygieneCheck = (query?: DormHygieneCheckQuery): AxiosPromise => { +export const listDormHygieneCheck = (query?: DormHygieneCheckQuery): AxiosPromise> => { return request({ url: '/dormHygieneCheck/dormHygieneCheck/list', method: 'get', diff --git a/web/src/api/xg/xgss/dormRoom/types.ts b/web/src/api/xg/xgss/dormRoom/types.ts index 2ce0a6f9..4d8bdd5a 100644 --- a/web/src/api/xg/xgss/dormRoom/types.ts +++ b/web/src/api/xg/xgss/dormRoom/types.ts @@ -49,6 +49,11 @@ export interface DormRoomVO { */ roomStatus: string; + /** + * 排序号 + */ + sort: number; + /** * 删除标志(0代表存在 1代表删除) */ @@ -106,6 +111,11 @@ export interface DormRoomForm extends BaseEntity { */ roomStatus?: string; + /** + * 排序号 + */ + sort?: number; + /** * 删除标志(0代表存在 1代表删除) */ @@ -158,6 +168,11 @@ export interface DormRoomQuery extends PageQuery { */ roomStatus?: string; + /** + * 排序号 + */ + sort?: number; + /** * 删除标志(0代表存在 1代表删除) */ diff --git a/web/src/api/xg/xgss/holidayStay/holidayStay/index-sg.ts b/web/src/api/xg/xgss/holidayStay/holidayStay/index-sg.ts new file mode 100644 index 00000000..d759cec3 --- /dev/null +++ b/web/src/api/xg/xgss/holidayStay/holidayStay/index-sg.ts @@ -0,0 +1,156 @@ +import request from '@/utils/request'; +import { AxiosPromise } from 'axios'; +import { HolidayStayVO, HolidayStayForm, HolidayStayQuery, StudentOptionVO, StudentOptionQuery } from '@/api/xg/xgss/holidayStay/holidayStay/types-sg'; + +/** + * 查询假期留宿管理列表 + * @param query + * @returns {*} + */ + +export const listHolidayStay = (query?: HolidayStayQuery): AxiosPromise => { + return request({ + url: '/holidayStay/holidayStay/list', + method: 'get', + params: query + }); +}; + +/** + * 查询假期留宿管理详细 + * @param id + */ +export const getHolidayStay = (id: string | number): AxiosPromise => { + return request({ + url: '/holidayStay/holidayStay/' + id, + method: 'get' + }); +}; + +/** + * 新增假期留宿管理 + * @param data + */ +export const addHolidayStay = (data: HolidayStayForm) => { + return request({ + url: '/holidayStay/holidayStay', + method: 'post', + data: data + }); +}; + +/** + * 修改假期留宿管理 + * @param data + */ +export const updateHolidayStay = (data: HolidayStayForm) => { + return request({ + url: '/holidayStay/holidayStay', + method: 'put', + data: data + }); +}; + +/** + * 删除假期留宿管理 + * @param id + */ +export const delHolidayStay = (id: string | number | Array) => { + return request({ + url: '/holidayStay/holidayStay/' + id, + method: 'delete' + }); +}; + +/** + * 获取学生选择器选项列表(支持按学号、姓名检索) + * @param query 查询参数 + * @returns 学生选项列表 + */ +export const getStudentOptions = ( + query?: StudentOptionQuery +): AxiosPromise<{ + total: number; + rows: StudentOptionVO[]; +}> => { + return request({ + url: '/holidayStay/holidayStay/studentOptions', + method: 'get', + params: query + }); +}; + +/** + * 查询单人留宿有效数据 + * 获取所有有效的假期留宿数据,筛选出在同一时间段内该宿舍只有一个学生留宿的记录 + * @returns 单人留宿有效数据列表 + */ +export const getSinglePersonValidStay = (): AxiosPromise => { + return request({ + url: '/holidayStay/holidayStay/singlePersonValidStay', + method: 'get' + }); +}; + +/** + * 获取校区选项列表 + * @returns 校区选项列表 + */ +export const getCampusOptions = (): AxiosPromise => { + return request({ + url: '/xqxx/dormCampus/list', + method: 'get' + }); +}; + +/** + * 获取指定校区的宿舍区选项列表 + * @param campusId 校区ID + * @returns 宿舍区选项列表 + */ +export const getAreaOptions = (campusId?: string | number): AxiosPromise => { + return request({ + url: '/ssqxx/dormArea/list', + method: 'get', + params: campusId ? { campusId } : undefined + }); +}; + +/** + * 获取指定宿舍区的宿舍楼选项列表 + * @param areaId 宿舍区ID + * @returns 宿舍楼选项列表 + */ +export const getBuildingOptions = (areaId?: string | number): AxiosPromise => { + return request({ + url: '/sslxx/dormBuilding/list', + method: 'get', + params: areaId ? { areaId } : undefined + }); +}; + +/** + * 获取指定宿舍楼的房间选项列表 + * @param buildingId 宿舍楼ID + * @returns 房间选项列表 + */ +export const getRoomOptions = (buildingId?: string | number): AxiosPromise => { + return request({ + url: '/xgss/dormRoom/list', + method: 'get', + params: buildingId ? { buildingId } : undefined + }); +}; + +/** + * 获取指定房间的床位选项列表 + * @param roomId 房间ID + * @returns 床位选项列表 + */ +export const getBedOptions = (roomId?: string | number): AxiosPromise => { + return request({ + url: '/dormbed/dormBed/list', + method: 'get', + params: roomId ? { roomId } : undefined + }); +}; diff --git a/web/src/api/xg/xgss/holidayStay/holidayStay/types-sg.ts b/web/src/api/xg/xgss/holidayStay/holidayStay/types-sg.ts new file mode 100644 index 00000000..75c01781 --- /dev/null +++ b/web/src/api/xg/xgss/holidayStay/holidayStay/types-sg.ts @@ -0,0 +1,751 @@ +export interface HolidayStayVO { + /** + * 主键ID + */ + id: string | number; + + /** + * 删除标志(0代表存在 1代表删除) + */ + deleted: number; + + /** + * 学生ID,关联xg_student_msg.id + */ + studentId: string | number; + + /** + * 学生姓名 + */ + studentName: string; + + /** + * 学号 + */ + studentNo: string; + + /** + * 班级ID + */ + classId: string | number; + + /** + * 班级名称 + */ + className: string; + + /** + * 专业ID + */ + majorId: string | number; + + /** + * 专业名称 + */ + majorName: string; + + /** + * 学院ID + */ + collegeId: string | number; + + /** + * 学院名称 + */ + collegeName: string; + + /** + * 校区ID,关联xg_dorm_campus.id + */ + campusId: string | number; + + /** + * 校区名称 + */ + campusName: string; + + /** + * 宿舍区ID,关联xg_dorm_area.id + */ + areaId: string | number; + + /** + * 宿舍区名称 + */ + areaName: string; + + /** + * 宿舍楼ID,关联xg_dorm_building.id + */ + buildingId: string | number; + + /** + * 宿舍楼名称 + */ + buildingName: string; + + /** + * 房间ID,关联xg_dorm_room.id + */ + roomId: string | number; + + /** + * 房间名称 + */ + roomName: string; + + /** + * 床位ID,关联xg_dorm_bed.id + */ + bedId: string | number; + + /** + * 床位名称 + */ + bedName: string; + + /** + * 假期类型(寒假、暑假、春节假期、国庆假期等) + */ + holidayType: string | number; + + /** + * 申请留宿开始日期 + */ + applyStartDate: string; + + /** + * 申请留宿结束日期 + */ + applyEndDate: string; + + /** + * 留宿原因(必填) + */ + stayReason: string; + + /** + * 留宿原因类型(学习、实习、家庭困难、其他) + */ + stayReasonType: string; + + /** + * 学生联系电话 + */ + studentPhone: string; + + /** + * 紧急联系人姓名 + */ + emergencyContact: string; + + /** + * 紧急联系人电话 + */ + emergencyPhone: string; + + /** + * 家长同意(0否 1是) + */ + parentConsent: string; + + /** + * 附件OSS文件ID,多个用逗号分隔(申请材料、证明文件等) + */ + attachmentOssIds: string | number; + + /** + * 扩展字段1 + */ + extField1: string; + + /** + * 扩展字段2 + */ + extField2: string; + + /** + * 扩展字段3 + */ + extField3: string; + + /** + * 备注信息 + */ + remark: string; +} + +export interface HolidayStayForm extends BaseEntity { + /** + * 主键ID + */ + id?: string | number; + + /** + * 删除标志(0代表存在 1代表删除) + */ + deleted?: number; + + /** + * 学生ID,关联xg_student_msg.id + */ + studentId?: string | number; + + /** + * 学生姓名 + */ + studentName?: string; + + /** + * 学号 + */ + studentNo?: string; + + /** + * 班级ID + */ + classId?: string | number; + + /** + * 班级名称 + */ + className?: string; + + /** + * 专业ID + */ + majorId?: string | number; + + /** + * 专业名称 + */ + majorName?: string; + + /** + * 学院ID + */ + collegeId?: string | number; + + /** + * 学院名称 + */ + collegeName?: string; + + /** + * 校区ID,关联xg_dorm_campus.id + */ + campusId?: string | number; + + /** + * 校区名称 + */ + campusName?: string; + + /** + * 宿舍区ID,关联xg_dorm_area.id + */ + areaId?: string | number; + + /** + * 宿舍区名称 + */ + areaName?: string; + + /** + * 宿舍楼ID,关联xg_dorm_building.id + */ + buildingId?: string | number; + + /** + * 宿舍楼名称 + */ + buildingName?: string; + + /** + * 房间ID,关联xg_dorm_room.id + */ + roomId?: string | number; + + /** + * 房间名称 + */ + roomName?: string; + + /** + * 床位ID,关联xg_dorm_bed.id + */ + bedId?: string | number; + + /** + * 床位名称 + */ + bedName?: string; + + /** + * 假期类型(寒假、暑假、春节假期、国庆假期等) + */ + holidayType?: string | number; + + /** + * 申请留宿开始日期 + */ + applyStartDate?: string; + + /** + * 申请留宿结束日期 + */ + applyEndDate?: string; + + /** + * 留宿原因(必填) + */ + stayReason?: string; + + /** + * 留宿原因类型(学习、实习、家庭困难、其他) + */ + stayReasonType?: string; + + /** + * 学生联系电话 + */ + studentPhone?: string; + + /** + * 紧急联系人姓名 + */ + emergencyContact?: string; + + /** + * 紧急联系人电话 + */ + emergencyPhone?: string; + + /** + * 家长同意(0否 1是) + */ + parentConsent?: string; + + /** + * 附件OSS文件ID,多个用逗号分隔(申请材料、证明文件等) + */ + attachmentOssIds?: string | number; + + /** + * 扩展字段1 + */ + extField1?: string; + + /** + * 扩展字段2 + */ + extField2?: string; + + /** + * 扩展字段3 + */ + extField3?: string; + + /** + * 备注信息 + */ + remark?: string; +} + +export interface HolidayStayQuery extends PageQuery { + /** + * 删除标志(0代表存在 1代表删除) + */ + deleted?: number; + + /** + * 学生ID,关联xg_student_msg.id + */ + studentId?: string | number; + + /** + * 学生姓名 + */ + studentName?: string; + + /** + * 学号 + */ + studentNo?: string; + + /** + * 班级ID + */ + classId?: string | number; + + /** + * 班级名称 + */ + className?: string; + + /** + * 专业ID + */ + majorId?: string | number; + + /** + * 专业名称 + */ + majorName?: string; + + /** + * 学院ID + */ + collegeId?: string | number; + + /** + * 学院名称 + */ + collegeName?: string; + + /** + * 校区ID,关联xg_dorm_campus.id + */ + campusId?: string | number; + + /** + * 校区名称 + */ + campusName?: string; + + /** + * 宿舍区ID,关联xg_dorm_area.id + */ + areaId?: string | number; + + /** + * 宿舍区名称 + */ + areaName?: string; + + /** + * 宿舍楼ID,关联xg_dorm_building.id + */ + buildingId?: string | number; + + /** + * 宿舍楼名称 + */ + buildingName?: string; + + /** + * 房间ID,关联xg_dorm_room.id + */ + roomId?: string | number; + + /** + * 房间名称 + */ + roomName?: string; + + /** + * 床位ID,关联xg_dorm_bed.id + */ + bedId?: string | number; + + /** + * 床位名称 + */ + bedName?: string; + + /** + * 假期类型(寒假、暑假、春节假期、国庆假期等) + */ + holidayType?: string | number; + + /** + * 申请留宿开始日期 + */ + applyStartDate?: string; + + /** + * 申请留宿结束日期 + */ + applyEndDate?: string; + + /** + * 留宿原因(必填) + */ + stayReason?: string; + + /** + * 留宿原因类型(学习、实习、家庭困难、其他) + */ + stayReasonType?: string; + + /** + * 学生联系电话 + */ + studentPhone?: string; + + /** + * 紧急联系人姓名 + */ + emergencyContact?: string; + + /** + * 紧急联系人电话 + */ + emergencyPhone?: string; + + /** + * 家长同意(0否 1是) + */ + parentConsent?: string; + + /** + * 附件OSS文件ID,多个用逗号分隔(申请材料、证明文件等) + */ + attachmentOssIds?: string | number; + + /** + * 扩展字段1 + */ + extField1?: string; + + /** + * 扩展字段2 + */ + extField2?: string; + + /** + * 扩展字段3 + */ + extField3?: string; + + /** + * 状态过滤(valid-有效,invalid-失效) + */ + status?: string; + + /** + * 日期范围参数 + */ + params?: any; +} + +/** + * 学生选择器选项 + */ +export interface StudentOptionVO { + /** + * 学生ID + */ + id: string | number; + + /** + * 学号 + */ + studentNo: string; + + /** + * 学生姓名 + */ + name: string; + + /** + * 性别:0-男,1-女 + */ + gender: string; + + /** + * 学院ID + */ + collegeId?: string | number; + + /** + * 学院名称 + */ + collegeName?: string; + + /** + * 专业ID + */ + majorId?: string | number; + + /** + * 专业名称 + */ + majorName?: string; + + /** + * 班级ID + */ + classId?: string | number; + + /** + * 班级名称 + */ + className?: string; + + /** + * 学生联系电话 + */ + phone?: string; + + /** + * 宿舍信息 + */ + dormInfo?: { + campusId?: string | number; + campusName?: string; + areaId?: string | number; + areaName?: string; + buildingId?: string | number; + buildingName?: string; + roomId?: string | number; + roomName?: string; + bedId?: string | number; + bedName?: string; + }; +} + +/** + * 学生选择器查询参数 + */ +export interface StudentOptionQuery { + /** + * 搜索关键字(学号或姓名) + */ + keyword?: string; + + /** + * 页码 + */ + pageNum?: number; + + /** + * 每页大小 + */ + pageSize?: number; +} + +/** + * 校区选项 + */ +export interface CampusOptionVO { + /** + * 校区ID + */ + id: string | number; + + /** + * 校区名称 + */ + campusName: string; + + /** + * 校区编码 + */ + campusCode?: string; +} + +/** + * 宿舍区选项 + */ +export interface AreaOptionVO { + /** + * 宿舍区ID + */ + id: string | number; + + /** + * 宿舍区名称 + */ + areaName: string; + + /** + * 校区ID + */ + campusId: string | number; + + /** + * 校区名称 + */ + campusName?: string; +} + +/** + * 宿舍楼选项 + */ +export interface BuildingOptionVO { + /** + * 宿舍楼ID + */ + id: string | number; + + /** + * 宿舍楼名称 + */ + buildingName: string; + + /** + * 宿舍区ID + */ + areaId: string | number; + + /** + * 宿舍区名称 + */ + areaName?: string; +} + +/** + * 房间选项 + */ +export interface RoomOptionVO { + /** + * 房间ID + */ + id: string | number; + + /** + * 房间名称 + */ + roomName: string; + + /** + * 宿舍楼ID + */ + buildingId: string | number; + + /** + * 宿舍楼名称 + */ + buildingName?: string; +} + +/** + * 床位选项 + */ +export interface BedOptionVO { + /** + * 床位ID + */ + id: string | number; + + /** + * 床位名称 + */ + bedName: string; + + /** + * 房间ID + */ + roomId: string | number; + + /** + * 房间名称 + */ + roomName?: string; + + /** + * 床位状态(0-空闲,1-已分配,2-已入住) + */ + bedStatus?: string; +} diff --git a/web/src/api/xskh/assessmentApplication/assessmentApplication.ts b/web/src/api/xskh/assessmentApplication/assessmentApplication.ts index 07a88751..88d997ca 100644 --- a/web/src/api/xskh/assessmentApplication/assessmentApplication.ts +++ b/web/src/api/xskh/assessmentApplication/assessmentApplication.ts @@ -1,5 +1,10 @@ import request from '@/utils/request'; -import { XskhAssessmentApplicationApprovalQuery, XskhAssessmentApplicationApprovalBo, XskhAssessmentApplicationApprovalVo, AvailableScheduleOption } from './types'; +import { + XskhAssessmentApplicationApprovalQuery, + XskhAssessmentApplicationApprovalBo, + XskhAssessmentApplicationApprovalVo, + AvailableScheduleOption +} from './types'; // 查询学生考核申请列表 export function listAssessmentApplication(query: XskhAssessmentApplicationApprovalQuery) { @@ -91,4 +96,4 @@ export function getIndicatorsByScheduleId(scheduleId: string | number) { url: `/assessment/application/indicators/${scheduleId}`, method: 'get' }); -} \ No newline at end of file +} diff --git a/web/src/api/xskh/assessmentApproval/assessmentApproval.ts b/web/src/api/xskh/assessmentApproval/assessmentApproval.ts index d3d25f68..46ed6b03 100644 --- a/web/src/api/xskh/assessmentApproval/assessmentApproval.ts +++ b/web/src/api/xskh/assessmentApproval/assessmentApproval.ts @@ -71,4 +71,4 @@ export function getAvailableAssessmentSchedules() { url: '/assessment/approval/available', method: 'get' }); -} \ No newline at end of file +} diff --git a/web/src/api/xskh/assessmentIndicator/types.ts b/web/src/api/xskh/assessmentIndicator/types.ts index 8a3c894c..b2c0fb9d 100644 --- a/web/src/api/xskh/assessmentIndicator/types.ts +++ b/web/src/api/xskh/assessmentIndicator/types.ts @@ -138,4 +138,4 @@ export interface RewardTypeOption { typeCode: string; /** 类型名称 */ typeName: string; -} \ No newline at end of file +} diff --git a/web/src/api/xskh/assessmentIndicatorType/assessmentIndicatorType.ts b/web/src/api/xskh/assessmentIndicatorType/assessmentIndicatorType.ts index 4e1ccf5d..9065fcfd 100644 --- a/web/src/api/xskh/assessmentIndicatorType/assessmentIndicatorType.ts +++ b/web/src/api/xskh/assessmentIndicatorType/assessmentIndicatorType.ts @@ -1,7 +1,12 @@ import request from '@/utils/request'; import { AxiosPromise } from 'axios'; import { TableDataInfo, R } from '@/api/types'; -import { XskhAssessmentIndicatorTypeVo, XskhAssessmentIndicatorTypeBo, XskhAssessmentIndicatorTypeQuery, XskhAssessmentIndicatorTypeForm } from './types'; +import { + XskhAssessmentIndicatorTypeVo, + XskhAssessmentIndicatorTypeBo, + XskhAssessmentIndicatorTypeQuery, + XskhAssessmentIndicatorTypeForm +} from './types'; /** * 查询学生考核指标类型列表 @@ -75,4 +80,4 @@ export function getEnabledAssessmentIndicatorTypes(): AxiosPromise> { url: '/assessment/plan/enabled', method: 'get' }); -} \ No newline at end of file +} diff --git a/web/src/api/xskh/assessmentSchedule/types.ts b/web/src/api/xskh/assessmentSchedule/types.ts index 73212fde..49ac3b92 100644 --- a/web/src/api/xskh/assessmentSchedule/types.ts +++ b/web/src/api/xskh/assessmentSchedule/types.ts @@ -142,4 +142,4 @@ export interface AssessmentPlanOption { planName: string; /** 方案说明 */ planDescription: string; -} \ No newline at end of file +} diff --git a/web/src/api/xskh/disciplineRevoke/disciplineRevoke.ts b/web/src/api/xskh/disciplineRevoke/disciplineRevoke.ts index b545c849..6138d21b 100644 --- a/web/src/api/xskh/disciplineRevoke/disciplineRevoke.ts +++ b/web/src/api/xskh/disciplineRevoke/disciplineRevoke.ts @@ -98,12 +98,50 @@ export const applyDisciplineRevoke = (data: DisciplineRevokeForm) => { }; /** - * 审核撤销申请 + * 查询班主任待审核的撤销申请列表 + * @param query + * @returns {*} + */ +export const listClassTeacherDisciplineRevoke = (query?: DisciplineRevokeQuery): AxiosPromise => { + return request({ + url: '/disciplineRevoke/disciplineRevoke/classTeacherList', + method: 'get', + params: query + }); +}; + +/** + * 班主任审核撤销申请 * @param data */ -export const approveDisciplineRevoke = (data: DisciplineRevokeForm) => { +export const teacherApproveDisciplineRevoke = (data: DisciplineRevokeForm) => { return request({ - url: '/disciplineRevoke/disciplineRevoke/approve', + url: '/disciplineRevoke/disciplineRevoke/teacherApprove', + method: 'put', + data: data + }); +}; + +/** + * 查询学工处待审核的撤销申请列表 + * @param query + * @returns {*} + */ +export const listStudentAffairsDisciplineRevoke = (query?: DisciplineRevokeQuery): AxiosPromise => { + return request({ + url: '/disciplineRevoke/disciplineRevoke/studentAffairsList', + method: 'get', + params: query + }); +}; + +/** + * 学工处审核撤销申请 + * @param data + */ +export const studentAffairsApproveDisciplineRevoke = (data: DisciplineRevokeForm) => { + return request({ + url: '/disciplineRevoke/disciplineRevoke/studentAffairsApprove', method: 'put', data: data }); diff --git a/web/src/api/xskh/disciplineRevoke/types.ts b/web/src/api/xskh/disciplineRevoke/types.ts index d24c8cbf..68138c9d 100644 --- a/web/src/api/xskh/disciplineRevoke/types.ts +++ b/web/src/api/xskh/disciplineRevoke/types.ts @@ -37,27 +37,47 @@ export interface DisciplineRevokeVO { applyTime: string; /** - * 状态(1-待审核 2-审核通过 3-审核拒绝) + * 状态(1-待班主任审核 2-待学工处审核 3-审核通过 4-班主任审核拒绝 5-学工处审核拒绝) */ status: string; /** - * 审核人ID + * 班主任审核人ID + */ + teacherApprovalBy: string | number; + + /** + * 班主任审核人姓名 + */ + teacherApprovalByName: string; + + /** + * 班主任审核时间 + */ + teacherApprovalTime: string; + + /** + * 班主任审核备注 + */ + teacherApprovalRemark: string; + + /** + * 学工处审核人ID */ approvalBy: string | number; /** - * 审核人姓名 + * 学工处审核人姓名 */ approvalByName: string; /** - * 审核时间 + * 学工处审核时间 */ approvalTime: string; /** - * 审核备注 + * 学工处审核备注 */ approvalRemark: string; @@ -80,6 +100,22 @@ export interface DisciplineRevokeVO { * 更新时间 */ updateTime: string; + + // 扩展字段(用于关联查询显示) + /** + * 学号 + */ + studentNo?: string; + + /** + * 违纪类型名称 + */ + disciplineTypeName?: string; + + /** + * 违纪日期 + */ + disciplineDate?: string; } export interface DisciplineRevokeForm { @@ -119,22 +155,37 @@ export interface DisciplineRevokeForm { applyTime?: string; /** - * 状态(1-待审核 2-审核通过 3-审核拒绝) + * 状态(1-待班主任审核 2-待学工处审核 3-审核通过 4-班主任审核拒绝 5-学工处审核拒绝) */ status?: string; /** - * 审核人ID + * 班主任审核人ID + */ + teacherApprovalBy?: string | number; + + /** + * 班主任审核时间 + */ + teacherApprovalTime?: string; + + /** + * 班主任审核备注 + */ + teacherApprovalRemark?: string; + + /** + * 学工处审核人ID */ approvalBy?: string | number; /** - * 审核时间 + * 学工处审核时间 */ approvalTime?: string; /** - * 审核备注 + * 学工处审核备注 */ approvalRemark?: string; } @@ -155,6 +206,11 @@ export interface DisciplineRevokeQuery extends PageQuery { */ studentName?: string; + /** + * 学号 + */ + studentNo?: string; + /** * 撤销申请原因 */ @@ -166,25 +222,30 @@ export interface DisciplineRevokeQuery extends PageQuery { applyTime?: string; /** - * 状态(1-待审核 2-审核通过 3-审核拒绝) + * 状态(1-待班主任审核 2-待学工处审核 3-审核通过 4-班主任审核拒绝 5-学工处审核拒绝) */ status?: string; /** - * 审核人ID + * 班主任审核人ID + */ + teacherApprovalBy?: string | number; + + /** + * 班主任审核时间 + */ + teacherApprovalTime?: string; + + /** + * 学工处审核人ID */ approvalBy?: string | number; /** - * 审核时间 + * 学工处审核时间 */ approvalTime?: string; - /** - * 审核备注 - */ - approvalRemark?: string; - /** * 其他参数 */ diff --git a/web/src/api/xskh/disciplineType/disciplineType/types.ts b/web/src/api/xskh/disciplineType/disciplineType/types.ts index 874f0b75..be6edc8f 100644 --- a/web/src/api/xskh/disciplineType/disciplineType/types.ts +++ b/web/src/api/xskh/disciplineType/disciplineType/types.ts @@ -19,6 +19,31 @@ export interface DisciplineTypeVO { */ typeCategory: string; + /** + * 区域类型(1-宿舍区违纪 2-教学区违纪) + */ + areaType: string; + + /** + * 最低扣分 + */ + minScore: number; + + /** + * 最高扣分 + */ + maxScore: number; + + /** + * 是否均化(0-否 1-是) + */ + isEqualized: string; + + /** + * 均化阈值 + */ + equalizedThreshold: number; + /** * 类型说明 */ @@ -61,6 +86,31 @@ export interface DisciplineTypeForm extends BaseEntity { */ typeCategory?: string; + /** + * 区域类型(1-宿舍区违纪 2-教学区违纪) + */ + areaType?: string; + + /** + * 最低扣分 + */ + minScore?: number; + + /** + * 最高扣分 + */ + maxScore?: number; + + /** + * 是否均化(0-否 1-是) + */ + isEqualized?: string; + + /** + * 均化阈值 + */ + equalizedThreshold?: number; + /** * 类型说明 */ @@ -98,6 +148,11 @@ export interface DisciplineTypeQuery extends PageQuery { */ typeCategory?: string; + /** + * 区域类型(1-宿舍区违纪 2-教学区违纪) + */ + areaType?: string; + /** * 状态(0-正常 1-停用) */ diff --git a/web/src/api/xskh/rewardType/rewardType/types.ts b/web/src/api/xskh/rewardType/rewardType/types.ts index cab8b0d8..28fbabf1 100644 --- a/web/src/api/xskh/rewardType/rewardType/types.ts +++ b/web/src/api/xskh/rewardType/rewardType/types.ts @@ -19,6 +19,16 @@ export interface RewardTypeVO { */ description: string; + /** + * 是否均化(0-否 1-是) + */ + isEqualized: string; + + /** + * 均化阈值 + */ + equalizedThreshold: number; + /** * 状态(0-正常 1-停用) */ @@ -56,6 +66,16 @@ export interface RewardTypeForm extends BaseEntity { */ description?: string; + /** + * 是否均化(0-否 1-是) + */ + isEqualized?: string; + + /** + * 均化阈值 + */ + equalizedThreshold?: number; + /** * 状态(0-正常 1-停用) */ @@ -88,6 +108,16 @@ export interface RewardTypeQuery extends PageQuery { */ description?: string; + /** + * 是否均化(0-否 1-是) + */ + isEqualized?: string; + + /** + * 均化阈值 + */ + equalizedThreshold?: number; + /** * 状态(0-正常 1-停用) */ diff --git a/web/src/components/AdvisorManager/index.vue b/web/src/components/AdvisorManager/index.vue index 62105d79..64ab3eb2 100644 --- a/web/src/components/AdvisorManager/index.vue +++ b/web/src/components/AdvisorManager/index.vue @@ -15,9 +15,9 @@ - @@ -119,7 +119,7 @@ const removeAdvisor = (index: number) => { // 监听teacher选择变化,自动填充相关信息 const handleTeacherChange = (advisor: AdvisorVO, teacher: TeacherVO | null) => { console.log('handleTeacherChange called with teacher:', teacher); - + if (teacher) { // 自动填充老师信息 advisor.name = teacher.nickName || ''; @@ -127,7 +127,7 @@ const handleTeacherChange = (advisor: AdvisorVO, teacher: TeacherVO | null) => { advisor.dept = teacher.deptName || ''; advisor.email = teacher.email || '暂无邮箱'; advisor.userId = teacher.userId?.toString() || ''; - + console.log('Updated advisor:', { name: advisor.name, phone: advisor.phone, diff --git a/web/src/components/ClubSelector/index.vue b/web/src/components/ClubSelector/index.vue index 4ee98f48..b635f3c7 100644 --- a/web/src/components/ClubSelector/index.vue +++ b/web/src/components/ClubSelector/index.vue @@ -14,7 +14,12 @@ value-key="uniqueKey" style="width: 100%" > - +
{{ option.relatedClubName }} {{ getTypeLabel(option.relatedType) }} @@ -98,9 +103,13 @@ const selectedOption = ref(null); // 监听 modelValue 变化 import { watch } from 'vue'; -watch(() => props.modelValue, (newValue) => { - console.log('ClubSelector - modelValue changed:', newValue); -}, { immediate: true }); +watch( + () => props.modelValue, + (newValue) => { + console.log('ClubSelector - modelValue changed:', newValue); + }, + { immediate: true } +); // 格式化选项标签 const formatOptionLabel = (option: ClubOption): string => { @@ -172,7 +181,7 @@ const loadOptions = async () => { uniqueKey: `${item.relatedType}-${item.relatedClubId}` })); options.value = [...clubOptions.value, ...volunteerOptions.value]; - + console.log('ClubSelector - 加载选项数据完成:', { clubCount: clubOptions.value.length, volunteerCount: volunteerOptions.value.length, diff --git a/web/src/views/bzrkh/assessmentApproval/approvalDialog.vue b/web/src/views/bzrkh/assessmentApproval/approvalDialog.vue index 8a5b93c9..e75ea8d7 100644 --- a/web/src/views/bzrkh/assessmentApproval/approvalDialog.vue +++ b/web/src/views/bzrkh/assessmentApproval/approvalDialog.vue @@ -267,11 +267,11 @@ - @@ -457,7 +457,7 @@ const loadApprovalDetail = async () => { if (approvalDetail.value?.routineWorkIndicators && Array.isArray(approvalDetail.value.routineWorkIndicators)) { routineWorkScores.value = approvalDetail.value.routineWorkIndicators.map((item) => { const mappedItem = { - ...item, + ...item, // 确保数值字段不为空 selfScore: Number(item.selfScore) || 0, superiorScore: Number(item.superiorScore) || Number(item.selfScore) || 0, // 默认使用自评分数 @@ -517,7 +517,7 @@ const loadApprovalDetail = async () => { if (approvalDetail.value?.specialWorkIndicators && Array.isArray(approvalDetail.value.specialWorkIndicators)) { specialWorkScores.value = approvalDetail.value.specialWorkIndicators.map((item) => { const mappedItem = { - ...item, + ...item, // 自评分数字段处理 - 兼容不同字段名,确保数值类型 score: Number(item.score || item.selfScore) || 0, selfScore: Number(item.selfScore || item.score) || 0, @@ -557,18 +557,17 @@ const loadApprovalDetail = async () => { approvalForm.auditComment = ''; console.log('=== 审批详情数据加载完成 ==='); - + // 验证数据完整性 if (routineWorkScores.value.length === 0) { console.warn('⚠️ 警告:班主任常规工作数据为空,可能存在数据问题'); proxy?.$modal.msgWarning('班主任常规工作数据为空,请检查数据是否正确'); } - + if (specialWorkScores.value.length === 0) { console.warn('⚠️ 警告:特色工作数据为空,可能存在数据问题'); proxy?.$modal.msgWarning('特色工作数据为空,请检查数据是否正确'); } - } catch (error) { console.error('加载审批详情失败:', error); proxy?.$modal.msgError('加载审批详情失败'); diff --git a/web/src/views/bzrkh/assessmentApproval/viewDialog.vue b/web/src/views/bzrkh/assessmentApproval/viewDialog.vue index 2fba8e33..d519e764 100644 --- a/web/src/views/bzrkh/assessmentApproval/viewDialog.vue +++ b/web/src/views/bzrkh/assessmentApproval/viewDialog.vue @@ -241,15 +241,13 @@ -
+
@@ -259,9 +257,7 @@
- - {{ calculateMonthlyScore().toFixed(2) }}分 - + {{ calculateMonthlyScore().toFixed(2) }}分 {{ parseTime(currentRow?.createTime, '{y}-{m}-{d} {h}:{i}:{s}') || '-' }} @@ -310,9 +306,10 @@ const getEvaluationLevelOptions = (): DictDataOption[] => { console.log('✅ 使用数据库字典数据:', bmzr_evaluation_level.value); return bmzr_evaluation_level.value; } - + // 字典数据未加载或为空时的处理 - if (visible.value) { // 只在弹窗打开时输出警告 + if (visible.value) { + // 只在弹窗打开时输出警告 console.warn('⚠️ 字典数据为空,使用备用数据,当前状态:', { 弹窗是否打开: visible.value, 字典对象存在: !!bmzr_evaluation_level.value, @@ -321,7 +318,7 @@ const getEvaluationLevelOptions = (): DictDataOption[] => { 字典数据内容: bmzr_evaluation_level.value }); } - + return fallbackEvaluationLevelOptions.value; }; @@ -363,10 +360,10 @@ const dialogTitle = computed(() => { const isDictDataMissing = computed(() => { // 如果还没有尝试加载字典数据,不显示警告 if (!visible.value) return false; - + // 如果字典数据已经加载成功,不显示警告 if (isDictLoaded.value) return false; - + // 只有在弹窗打开且字典数据为空时才显示警告 return !bmzr_evaluation_level.value || bmzr_evaluation_level.value.length === 0; }); @@ -434,7 +431,7 @@ const calculateEvaluationLevel = () => { console.log('💯 考核得分:', score); console.log('📈 是否有统计数据:', !!evaluationStats.value); console.log('👤 当前用户ID:', currentRow.value?.evaluatedUserId); - + if (evaluationStats.value) { console.log('📊 统计数据详情:', { 总教师数: evaluationStats.value.totalTeachers, @@ -443,15 +440,17 @@ const calculateEvaluationLevel = () => { 及格分数: evaluationStats.value.evaluationLevelRules?.passScore }); } - + let level = ''; let calculationMethod = ''; - + // 如果有统计数据且用户ID存在,使用严格的占比规则 - if (evaluationStats.value && - evaluationStats.value.totalTeachers > 0 && - evaluationStats.value.scoreRankings.length > 0 && - currentRow.value?.evaluatedUserId) { + if ( + evaluationStats.value && + evaluationStats.value.totalTeachers > 0 && + evaluationStats.value.scoreRankings.length > 0 && + currentRow.value?.evaluatedUserId + ) { level = calculateEvaluationLevelByStats(score, currentRow.value.evaluatedUserId); calculationMethod = '📈 严格占比规则评级'; } else { @@ -464,17 +463,17 @@ const calculateEvaluationLevel = () => { level = 'B'; // B等(合格):60-85分 } calculationMethod = '🔢 简化分数评级'; - + // 输出使用简化评级的原因 const reasons = []; if (!evaluationStats.value) reasons.push('统计数据为空'); if (evaluationStats.value?.totalTeachers === 0) reasons.push('总教师数为0'); if (evaluationStats.value?.scoreRankings?.length === 0) reasons.push('排名数据为空'); if (!currentRow.value?.evaluatedUserId) reasons.push('用户ID缺失'); - + console.warn('⚠️ 使用简化评级的原因:', reasons.join('、')); } - + console.log('🎯 计算方法:', calculationMethod); console.log('📋 最终评价等级:', level); console.log('=== 📊 评价等级计算调试结束 ==='); @@ -486,32 +485,30 @@ const calculateEvaluationLevelByStats = (score: number, evaluatedUserId: string const stats = evaluationStats.value; if (!stats) { console.warn('统计数据不可用,使用简单评级'); - return score < 60 ? 'C' : (score >= 85 ? 'A' : 'B'); + return score < 60 ? 'C' : score >= 85 ? 'A' : 'B'; } - + const { totalTeachers, scoreRankings, evaluationLevelRules } = stats; const { aLevelMaxPercentage, passScore } = evaluationLevelRules; - + // C等:不及格 if (score < passScore) { return 'C'; } - + // 查找当前用户的排名 - const currentRanking = scoreRankings.find(item => - item.evaluatedUserId === evaluatedUserId || item.score === score - ); - + const currentRanking = scoreRankings.find((item) => item.evaluatedUserId === evaluatedUserId || item.score === score); + if (!currentRanking) { console.warn('找不到当前用户的排名信息,使用简单评级'); - return score < 60 ? 'C' : (score >= 85 ? 'A' : 'B'); + return score < 60 ? 'C' : score >= 85 ? 'A' : 'B'; } - + // A等:及格且排名在前20%内 if (score >= passScore && currentRanking.percentage <= aLevelMaxPercentage) { return 'A'; } - + // B等:其余及格的 return 'B'; }; @@ -525,28 +522,28 @@ const open = async (row: AssessmentApprovalVO) => { currentRow.value = row; visible.value = true; - + // 检查字典数据加载状态 console.log('🔍 打开弹窗时字典数据状态:', { 字典对象存在: !!bmzr_evaluation_level.value, 字典数据长度: bmzr_evaluation_level.value?.length || 0, 字典数据内容: bmzr_evaluation_level.value }); - + // 等待字典数据加载完成(最多等待2秒) let waitCount = 0; while ((!bmzr_evaluation_level.value || bmzr_evaluation_level.value.length === 0) && waitCount < 20) { - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); waitCount++; } - + if (bmzr_evaluation_level.value && bmzr_evaluation_level.value.length > 0) { console.log('✅ 字典数据加载完成:', bmzr_evaluation_level.value); isDictLoaded.value = true; } else { console.warn('⚠️ 字典数据加载超时,使用备用数据'); } - + await loadViewDetail(); }; @@ -577,11 +574,15 @@ const loadViewDetail = async () => { if (statsRes.status === 'fulfilled') { evaluationStats.value = statsRes.value.data; console.log('✅ 评价等级统计数据加载成功:', evaluationStats.value); - + // 检查统计数据的完整性 if (!evaluationStats.value.totalTeachers || evaluationStats.value.scoreRankings.length === 0) { - console.warn('⚠️ 统计数据不完整,总教师数:', evaluationStats.value.totalTeachers, - '排名数据条数:', evaluationStats.value.scoreRankings.length); + console.warn( + '⚠️ 统计数据不完整,总教师数:', + evaluationStats.value.totalTeachers, + '排名数据条数:', + evaluationStats.value.scoreRankings.length + ); proxy?.$modal.msgWarning('统计数据不完整,可能影响等级评定准确性'); } } else { @@ -696,18 +697,17 @@ const loadViewDetail = async () => { approvalData.auditComment = ''; console.log('=== 查看详情数据加载完成 ==='); - + // 验证数据完整性 if (routineWorkScores.value.length === 0) { console.warn('⚠️ 警告:班主任常规工作数据为空,可能存在数据问题'); proxy?.$modal.msgWarning('班主任常规工作数据为空,请检查数据是否正确'); } - + if (specialWorkScores.value.length === 0) { console.warn('⚠️ 警告:特色工作数据为空,可能存在数据问题'); proxy?.$modal.msgWarning('特色工作数据为空,请检查数据是否正确'); } - } catch (error) { console.error('加载查看详情失败:', error); proxy?.$modal.msgError('加载查看详情失败'); @@ -724,8 +724,8 @@ const handleClose = () => { routineWorkScores.value = []; specialWorkScores.value = []; evaluationStats.value = undefined; - isDictLoaded.value = false; // 重置字典加载状态 - + isDictLoaded.value = false; // 重置字典加载状态 + Object.assign(managementEffectivenessData, { classDeduction: 0, classDeductionHalf: 0, @@ -802,7 +802,7 @@ defineExpose({ max-height: 150px; overflow-y: auto; } - + .el-table__row { height: auto; } diff --git a/web/src/views/bzrkh/assessmentIndicatorType/index.vue b/web/src/views/bzrkh/assessmentIndicatorType/index.vue index 01bf2e01..43f12139 100644 --- a/web/src/views/bzrkh/assessmentIndicatorType/index.vue +++ b/web/src/views/bzrkh/assessmentIndicatorType/index.vue @@ -7,12 +7,22 @@ - + - + @@ -194,7 +204,7 @@ const handleQuery = () => { pageNum: queryParams.value.pageNum, pageSize: queryParams.value.pageSize }); - + queryParams.value.pageNum = 1; getList(); }; @@ -202,24 +212,26 @@ const handleQuery = () => { /** 查询班主任考核指标类型列表 */ const getList = async () => { loading.value = true; - + // 构建查询参数,正确处理空字符串和undefined/null值 const queryData = { pageNum: queryParams.value.pageNum, pageSize: queryParams.value.pageSize, // 修复:正确处理空字符串,只过滤undefined和null - ...(queryParams.value.indicatorTypeCode !== undefined && queryParams.value.indicatorTypeCode !== null && { indicatorTypeCode: queryParams.value.indicatorTypeCode }), - ...(queryParams.value.indicatorTypeName !== undefined && queryParams.value.indicatorTypeName !== null && { indicatorTypeName: queryParams.value.indicatorTypeName }), + ...(queryParams.value.indicatorTypeCode !== undefined && + queryParams.value.indicatorTypeCode !== null && { indicatorTypeCode: queryParams.value.indicatorTypeCode }), + ...(queryParams.value.indicatorTypeName !== undefined && + queryParams.value.indicatorTypeName !== null && { indicatorTypeName: queryParams.value.indicatorTypeName }), ...(queryParams.value.status !== undefined && queryParams.value.status !== null && { status: queryParams.value.status }) }; - + console.log('📋 发送查询请求,参数:', queryData); console.log('🔍 原始查询参数:', { indicatorTypeCode: `"${queryParams.value.indicatorTypeCode}"`, indicatorTypeName: `"${queryParams.value.indicatorTypeName}"`, status: `"${queryParams.value.status}"` }); - + try { const res = await listAssessmentIndicatorType(queryData); console.log('📊 查询结果:', { @@ -227,7 +239,7 @@ const getList = async () => { dataCount: res.rows?.length, firstItem: res.rows?.[0] }); - + assessmentIndicatorTypeList.value = res.rows; total.value = res.total; } catch (error) { @@ -253,23 +265,23 @@ const reset = () => { /** 重置按钮操作 */ const resetQuery = () => { console.log('🔄 重置查询参数'); - + // 重置表单字段 queryFormRef.value?.resetFields(); - + // 手动重置查询参数对象,确保清空所有搜索条件 queryParams.value.indicatorTypeCode = undefined; queryParams.value.indicatorTypeName = undefined; queryParams.value.status = undefined; queryParams.value.pageNum = 1; - + console.log('🔄 重置后的查询参数:', { indicatorTypeCode: queryParams.value.indicatorTypeCode, indicatorTypeName: queryParams.value.indicatorTypeName, status: queryParams.value.status, pageNum: queryParams.value.pageNum }); - + // 重新查询 getList(); }; @@ -349,7 +361,7 @@ onMounted(() => { pageNum: queryParams.value.pageNum, pageSize: queryParams.value.pageSize }); - + getList(); }); diff --git a/web/src/views/bzrkh/assessmentPlan/index.vue b/web/src/views/bzrkh/assessmentPlan/index.vue index 1e1f55d1..7eee5bb4 100644 --- a/web/src/views/bzrkh/assessmentPlan/index.vue +++ b/web/src/views/bzrkh/assessmentPlan/index.vue @@ -454,7 +454,7 @@ const submitForm = () => { /** 删除按钮操作 */ const handleDelete = async (row?: AssessmentPlanVO) => { const _ids = row?.id || ids.value; - + let confirmMessage = ''; if (row) { const semesterText = row.semester === '1' ? '上学期' : '下学期'; @@ -462,7 +462,7 @@ const handleDelete = async (row?: AssessmentPlanVO) => { } else { confirmMessage = `是否确认删除选中的${ids.value.length}条考核计划?`; } - + await proxy?.$modal.confirm(confirmMessage).then(async () => { await delAssessmentPlan(_ids).then(() => { getList(); diff --git a/web/src/views/bzrkh/assessmentScheme/index.vue b/web/src/views/bzrkh/assessmentScheme/index.vue index 4a025634..54492165 100644 --- a/web/src/views/bzrkh/assessmentScheme/index.vue +++ b/web/src/views/bzrkh/assessmentScheme/index.vue @@ -382,14 +382,14 @@ const submitForm = () => { /** 删除按钮操作 */ const handleDelete = async (row?: BmzrAssessmentSchemeVO) => { const _ids = row?.id || ids.value; - + let confirmMessage = ''; if (row) { confirmMessage = `是否确认删除考核方案"${row.schemeName}"(${row.academicYear}学年第${row.semester}学期)?`; } else { confirmMessage = `是否确认删除选中的${ids.value.length}条考核方案?`; } - + try { await proxy?.$modal.confirm(confirmMessage); await delBmzrAssessmentScheme(_ids); diff --git a/web/src/views/bzrkh/teacherAssessment/index.vue b/web/src/views/bzrkh/teacherAssessment/index.vue index 491eef44..b350f94f 100644 --- a/web/src/views/bzrkh/teacherAssessment/index.vue +++ b/web/src/views/bzrkh/teacherAssessment/index.vue @@ -95,14 +95,14 @@ @@ -155,7 +155,7 @@ - +
@@ -241,13 +241,13 @@ const getTeacherClasses = async () => { queryParams.value.planName = undefined; // 确保搜索条件被重置 queryParams.value.pageNum = 1; queryParams.value.classId = activeTab.value; - + console.log('🔄 初始化完成:', { activeTab: activeTab.value, classId: queryParams.value.classId, planName: queryParams.value.planName }); - + // 等待activeTab设置完成后再获取列表 await nextTick(); await getList(); @@ -270,9 +270,9 @@ const getList = async () => { teacherId: userStore.userId, // 确保包含teacherId planStatus: '1' // 确保包含planStatus }; - + console.log('📋 查询参数:', queryData); - + const res = await listTeacherAssessmentPlans(queryData); console.log('📊 查询结果:', { total: res.total, @@ -280,7 +280,7 @@ const getList = async () => { firstItemStatus: res.rows?.[0]?.auditStatus, firstItemSubmitted: res.rows?.[0]?.submitted }); - + assessmentPlanList.value = res.rows || []; total.value = res.total || 0; } catch (error) { @@ -298,18 +298,18 @@ const handleTabChange = async (tabName: string) => { activeTab: activeTab.value, oldPlanName: queryParams.value.planName }); - + // 重置分页和搜索条件,设置新的班级ID queryParams.value.pageNum = 1; queryParams.value.planName = undefined; // 重置搜索条件,避免标签页间共享 queryParams.value.classId = tabName; - + console.log('🔄 参数设置完成:', { classId: queryParams.value.classId, activeTab: activeTab.value, planName: queryParams.value.planName }); - + // 等待参数设置完成后再查询 await nextTick(); getList(); @@ -332,20 +332,20 @@ const handleQuery = () => { /** 重置按钮操作 */ const resetQuery = () => { console.log('🔄 开始重置查询参数'); - + try { // 保存当前班级ID和计划状态(这些不应该被重置) const currentClassId = queryParams.value.classId; const currentPlanStatus = queryParams.value.planStatus; - + // 手动重置查询参数到初始状态 queryParams.value.planName = undefined; queryParams.value.pageNum = 1; - + // 恢复不应该被重置的参数 queryParams.value.classId = currentClassId; queryParams.value.planStatus = currentPlanStatus; - + console.log('🔄 重置后的查询参数:', { planName: queryParams.value.planName, pageNum: queryParams.value.pageNum, @@ -353,7 +353,7 @@ const resetQuery = () => { planStatus: queryParams.value.planStatus, teacherId: userStore.userId }); - + // 等待DOM更新完成后再刷新数据 nextTick(() => { console.log('🔄 开始强制刷新数据'); @@ -367,8 +367,6 @@ const resetQuery = () => { } }; - - /** 排序触发事件 */ const sortChange = (column: any) => { const { prop, order } = column; diff --git a/web/src/views/bzrkh/teacherAssessment/submitDialog.vue b/web/src/views/bzrkh/teacherAssessment/submitDialog.vue index 4c4da1c5..a19bc6e9 100644 --- a/web/src/views/bzrkh/teacherAssessment/submitDialog.vue +++ b/web/src/views/bzrkh/teacherAssessment/submitDialog.vue @@ -192,7 +192,7 @@ 特色工作(加20分)
- + @@ -210,7 +210,15 @@ - + @@ -231,7 +239,15 @@
- +
@@ -252,7 +268,15 @@ - + @@ -273,15 +297,23 @@ - + - - + + @@ -323,7 +355,9 @@ @@ -560,7 +594,7 @@ const calculateMonthlyScore = () => { // 检查是否有实际评分数据(避免空白状态显示50分) const hasAnyScore = routineWorkTotal > 0 || deductionHalf > 0 || specialWorkTotal > 0; - + if (!hasAnyScore || totalCoefficient === 0) { return 0; // 没有评分数据时显示0 } @@ -569,7 +603,7 @@ const calculateMonthlyScore = () => { const classDeductionMaxScore = 50; // 修正后的考核得分公式 = 总系数(K)* ((班主任常规工作自评分之和 + 班级扣分项满分值) - 按班级扣分50%计的分值 + 特色工作总分值) - return totalCoefficient * ((routineWorkTotal + classDeductionMaxScore) - deductionHalf + specialWorkTotal); + return totalCoefficient * (routineWorkTotal + classDeductionMaxScore - deductionHalf + specialWorkTotal); }; /** 打开弹窗 */ @@ -1071,7 +1105,7 @@ defineExpose({ font-weight: 400; text-align: center; } - + /* 覆盖small尺寸的字体大小,与特色工作输入框保持一致 */ .el-input-number--small.score-input .el-input-number__input { font-size: 14px !important; diff --git a/web/src/views/bzrkh/teacherAssessment/viewDialog.vue b/web/src/views/bzrkh/teacherAssessment/viewDialog.vue index 8c91e595..267a3e6b 100644 --- a/web/src/views/bzrkh/teacherAssessment/viewDialog.vue +++ b/web/src/views/bzrkh/teacherAssessment/viewDialog.vue @@ -55,9 +55,7 @@ {{ getCurrentClassDetail()?.totalStudentCount || 0 }}人 - {{ - formatNumber(getCurrentCoefficient()?.studentCountCoefficient, 3) - }} + {{ formatNumber(getCurrentCoefficient()?.studentCountCoefficient, 3) }} @@ -70,9 +68,7 @@ {{ getCurrentClassDetail()?.boardingStudentCount || 0 }}人 - {{ - formatNumber(getCurrentCoefficient()?.boardingCoefficient, 3) - }} + {{ formatNumber(getCurrentCoefficient()?.boardingCoefficient, 3) }} @@ -165,10 +161,10 @@ - @@ -176,12 +172,12 @@ - + - + @@ -280,17 +276,17 @@ - + - + - + @@ -307,15 +303,13 @@ -
+
@@ -325,16 +319,14 @@
- - {{ calculateMonthlyScore().toFixed(2) }}分 - + {{ calculateMonthlyScore().toFixed(2) }}分 {{ parseTime(approvalDetail?.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') || '-' }} - + @@ -434,11 +426,7 @@ import { InfoFilled, WarningFilled } from '@element-plus/icons-vue'; import { getAssessmentSubmitDetail, getClassStudentRecords } from '@/api/bzrkh/teacherAssessment'; import { getAssessmentApprovalDetail, getEvaluationLevelStats } from '@/api/bzrkh/assessmentApproval'; -import { - AssessmentDetailVO, - AssessmentIndicatorVO, - ClassStudentRecordsVO -} from '@/api/bzrkh/teacherAssessment/types'; +import { AssessmentDetailVO, AssessmentIndicatorVO, ClassStudentRecordsVO } from '@/api/bzrkh/teacherAssessment/types'; import { AssessmentApprovalDetailVO, AssessmentApprovalIndicatorVO, @@ -471,9 +459,10 @@ const getEvaluationLevelOptions = (): DictDataOption[] => { console.log('✅ 使用数据库字典数据:', bmzr_evaluation_level.value); return bmzr_evaluation_level.value; } - + // 字典数据未加载或为空时的处理 - if (visible.value) { // 只在弹窗打开时输出警告 + if (visible.value) { + // 只在弹窗打开时输出警告 console.warn('⚠️ 字典数据为空,使用备用数据,当前状态:', { 弹窗是否打开: visible.value, 字典对象存在: !!bmzr_evaluation_level.value, @@ -482,7 +471,7 @@ const getEvaluationLevelOptions = (): DictDataOption[] => { 字典数据内容: bmzr_evaluation_level.value }); } - + return fallbackEvaluationLevelOptions.value; }; @@ -511,10 +500,10 @@ const isDictLoaded = ref(false); const isDictDataMissing = computed(() => { // 如果还没有尝试加载字典数据,不显示警告 if (!visible.value) return false; - + // 如果字典数据已经加载成功,不显示警告 if (isDictLoaded.value) return false; - + // 只有在弹窗打开且字典数据为空时才显示警告 return !bmzr_evaluation_level.value || bmzr_evaluation_level.value.length === 0; }); @@ -556,16 +545,16 @@ const specialWorkForm = reactive({ totalScore: 0 }); - // 考核得分表单(只读) - const monthlyScoreForm = reactive({ - overallComment: '' - }); +// 考核得分表单(只读) +const monthlyScoreForm = reactive({ + overallComment: '' +}); - // 审批模式下的考核得分数据 - const approvalMonthlyScoreData = reactive({ - selfOverallComment: '', - superiorOverallComment: '' - }); +// 审批模式下的考核得分数据 +const approvalMonthlyScoreData = reactive({ + selfOverallComment: '', + superiorOverallComment: '' +}); // 学生违纪记录和奖励记录弹窗相关 const studentRecordsVisible = ref(false); @@ -666,7 +655,7 @@ const calculateMonthlyScore = () => { // 检查是否有实际评分数据(避免空白状态显示50分) const hasAnyScore = routineWorkTotal > 0 || deductionHalf > 0 || specialWorkTotal > 0; - + if (!hasAnyScore || totalCoefficient === 0) { return 0; // 没有评分数据时显示0 } @@ -675,7 +664,7 @@ const calculateMonthlyScore = () => { const classDeductionMaxScore = 50; // 修正后的考核得分公式 = 总系数(K)* ((班主任常规工作自评分之和 + 班级扣分项满分值) - 按班级扣分50%计的分值 + 特色工作总分值) - return totalCoefficient * ((routineWorkTotal + classDeductionMaxScore) - deductionHalf + specialWorkTotal); + return totalCoefficient * (routineWorkTotal + classDeductionMaxScore - deductionHalf + specialWorkTotal); } }; @@ -685,7 +674,7 @@ const calculateEvaluationLevel = () => { console.log('=== 📊 评价等级计算调试 ==='); console.log('💯 考核得分:', score); console.log('📈 是否有统计数据:', !!evaluationStats.value); - + if (evaluationStats.value) { console.log('📊 统计数据详情:', { 总教师数: evaluationStats.value.totalTeachers, @@ -694,14 +683,12 @@ const calculateEvaluationLevel = () => { 及格分数: evaluationStats.value.evaluationLevelRules?.passScore }); } - + let level = ''; let calculationMethod = ''; - + // 如果有统计数据,使用严格的占比规则 - if (evaluationStats.value && - evaluationStats.value.totalTeachers > 0 && - evaluationStats.value.scoreRankings.length > 0) { + if (evaluationStats.value && evaluationStats.value.totalTeachers > 0 && evaluationStats.value.scoreRankings.length > 0) { level = calculateEvaluationLevelByStats(score); calculationMethod = '📈 严格占比规则评级'; } else { @@ -714,16 +701,16 @@ const calculateEvaluationLevel = () => { level = 'B'; // B等(合格):60-85分 } calculationMethod = '🔢 简化分数评级'; - + // 输出使用简化评级的原因 const reasons = []; if (!evaluationStats.value) reasons.push('统计数据为空'); if (evaluationStats.value?.totalTeachers === 0) reasons.push('总教师数为0'); if (evaluationStats.value?.scoreRankings?.length === 0) reasons.push('排名数据为空'); - + console.warn('⚠️ 使用简化评级的原因:', reasons.join('、')); } - + console.log('🎯 计算方法:', calculationMethod); console.log('📋 最终评价等级:', level); console.log('=== 📊 评价等级计算调试结束 ==='); @@ -735,30 +722,30 @@ const calculateEvaluationLevelByStats = (score: number) => { const stats = evaluationStats.value; if (!stats) { console.warn('统计数据不可用,使用简单评级'); - return score < 60 ? 'C' : (score >= 85 ? 'A' : 'B'); + return score < 60 ? 'C' : score >= 85 ? 'A' : 'B'; } - + const { totalTeachers, scoreRankings, evaluationLevelRules } = stats; const { aLevelMaxPercentage, passScore } = evaluationLevelRules; - + // C等:不及格 if (score < passScore) { return 'C'; } - + // 查找当前分数的排名 - const currentRanking = scoreRankings.find(item => item.score === score); - + const currentRanking = scoreRankings.find((item) => item.score === score); + if (!currentRanking) { console.warn('找不到当前分数的排名信息,使用简单评级'); - return score < 60 ? 'C' : (score >= 85 ? 'A' : 'B'); + return score < 60 ? 'C' : score >= 85 ? 'A' : 'B'; } - + // A等:及格且排名在前20%内 if (score >= passScore && currentRanking.percentage <= aLevelMaxPercentage) { return 'A'; } - + // B等:其余及格的 return 'B'; }; @@ -783,7 +770,7 @@ const open = async (auditStatus?: string) => { routineWorkScores.value = []; approvalRoutineWorkScores.value = []; approvalSpecialWorkScores.value = []; - + Object.assign(managementEffectivenessForm, { classDeduction: 0, classDeductionHalf: 0, @@ -816,7 +803,7 @@ const open = async (auditStatus?: string) => { }); visible.value = true; - + // 检查字典数据加载状态 console.log('🔍 打开弹窗时字典数据状态:', { 字典对象存在: !!bmzr_evaluation_level.value, @@ -824,21 +811,21 @@ const open = async (auditStatus?: string) => { 字典加载状态: isDictLoaded.value, 字典数据内容: bmzr_evaluation_level.value }); - + // 等待字典数据加载完成(最多等待2秒) let waitCount = 0; while ((!bmzr_evaluation_level.value || bmzr_evaluation_level.value.length === 0) && waitCount < 20) { - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); waitCount++; } - + if (bmzr_evaluation_level.value && bmzr_evaluation_level.value.length > 0) { console.log('✅ 字典数据加载完成:', bmzr_evaluation_level.value); isDictLoaded.value = true; } else { console.warn('⚠️ 字典数据加载超时,使用备用数据'); } - + // 根据显示模式加载不同的数据 if (viewMode.value === 'approval') { await loadApprovalDetail(); @@ -854,7 +841,7 @@ const loadApprovalDetail = async () => { // 从userStore获取当前用户ID作为被评价用户ID const userStore = useUserStore(); const evaluatedUserId = userStore.userId; - + if (!evaluatedUserId) { proxy?.$modal.msgError('获取用户信息失败'); return; @@ -925,7 +912,6 @@ const loadApprovalDetail = async () => { } console.log('=== 审批详情数据加载完成 ==='); - } catch (error) { console.error('加载审批详情失败:', error); proxy?.$modal.msgError('加载审批详情失败,请检查网络连接或刷新页面重试'); @@ -981,7 +967,7 @@ const loadAssessmentDetail = async () => { break; } }); - + // 计算特色工作总分 specialWorkForm.totalScore = (specialWorkForm.score1 || 0) + (specialWorkForm.score2 || 0) + (specialWorkForm.score3 || 0) + (specialWorkForm.score4 || 0); @@ -1033,8 +1019,8 @@ const handleClose = () => { approvalRoutineWorkScores.value = []; approvalSpecialWorkScores.value = []; evaluationStats.value = undefined; - isDictLoaded.value = false; // 重置字典加载状态 - + isDictLoaded.value = false; // 重置字典加载状态 + Object.assign(managementEffectivenessForm, { classDeduction: 0, classDeductionHalf: 0, @@ -1128,13 +1114,13 @@ defineExpose({ .total-score { font-size: 16px; font-weight: 600; - color: #409EFF; + color: #409eff; } .monthly-score { font-size: 18px; font-weight: 600; - color: #67C23A; + color: #67c23a; } /* 表格样式 */ @@ -1183,7 +1169,7 @@ defineExpose({ max-height: 150px; overflow-y: auto; } - + .el-table__row { height: auto; } @@ -1280,4 +1266,4 @@ defineExpose({ padding: 16px; } } - \ No newline at end of file + diff --git a/web/src/views/cw/README.md b/web/src/views/cw/README.md index f00dc84b..e46fe239 100644 --- a/web/src/views/cw/README.md +++ b/web/src/views/cw/README.md @@ -1,55 +1,213 @@ -# 财务管理模块 - 前端界面 +# 财务管理模块 (CW) - 项目申报审批功能 -## 📋 模块说明 +## 🎯 功能概述 -- **模块名称**: 财务管理 (Financial Management) -- **模块简称**: cw -- **对应后端**: backend/ruoyi-modules/ruoyi-cw/ +本次开发实现了项目申报管理模块的审批流程变更,从固定的三级审批改为支持用户自定义的灵活审批流程。 ## 📁 目录结构 -此目录用于存放财务管理模块的所有前端界面组件,包括: +``` +web/src/views/cw/ +├── projectAuditor/ # 项目审批人配置模块 +│ ├── index.vue # 审批人配置主页面 +│ └── README.md # 功能说明文档 +├── projectApproval/ # 项目审批模块 +│ ├── index.vue # 审批流程管理页面(待实现) +│ ├── pending.vue # 我的待审批页面 +│ ├── components/ +│ │ └── ApprovalProcessSelector.vue # 审批流程选择组件 +│ └── README.md # 功能说明文档(待创建) +├── application/ # 项目申报模块(已修改) +│ ├── form.vue # 申报表单(已集成审批流程选择) +│ └── ... # 其他文件保持不变 +└── README.md # 总体说明文档(本文件) +``` -### 💰 会计核算 +``` +web/src/api/cw/ +├── projectAuditor.ts # 审批人配置API +├── projectApproval.ts # 项目审批API +└── types.ts # TypeScript类型定义(已扩展) +``` -- 科目管理 -- 凭证管理 -- 账簿管理 +## 🚀 核心功能 -### 📊 预算管理 +### 1. 项目审批人配置管理 (`projectAuditor/`) -- 预算编制 -- 预算执行 -- 预算分析 +- **审批人池管理**: 从系统用户中选择并配置审批人员 +- **批量添加**: 通过部门树选择用户,支持批量添加审批人 +- **跨部门选择**: 支持在不同部门间选择用户,选择状态会自动保持 +- **信息维护**: 修改审批人联系信息、状态管理 +- **权限控制**: 基于RBAC的细粒度权限控制 -### 💳 资产管理 +### 2. 项目审批流程管理 (`projectApproval/`) -- 固定资产 -- 流动资产 -- 资产盘点 +- **流程监控**: 查看所有审批流程状态和进度(待实现) +- **流程详情**: 查看审批记录和时间轴展示(待实现) +- **流程终止**: 管理员可终止进行中的审批流程(待实现) -### 📈 财务报表 +### 3. 我的待审批 (`projectApproval/pending.vue`) -- 资产负债表 -- 损益表 -- 现金流量表 +- **待审批列表**: 显示当前用户需要审批的项目 +- **项目详情**: 查看项目完整信息和审批历史 +- **审批操作**: 支持通过/拒绝,必填审批意见 +- **批量审批**: 支持批量通过或拒绝多个项目(待实现) -### 💸 费用管理 +### 4. 审批流程选择组件 (`components/ApprovalProcessSelector.vue`) -- 费用申请 -- 费用审批 -- 费用报销 +- **自定义流程配置**: 支持用户从审批人池中选择审核人 +- **可视化流程预览**: 直观展示审批顺序和流程走向 +- **审核人管理**: 支持添加、删除、排序审核人 +- **流程验证**: 验证审批流程的合理性和完整性 -## 🎯 开发规范 +### 5. 项目申报集成 (`application/form.vue` - 已修改) -- 遵循Vue 3 + TypeScript + Element Plus开发规范 -- 组件命名采用PascalCase -- 文件命名采用kebab-case -- 统一使用Composition API +- **流程选择对话框**: 在提交时弹出审批流程选择界面 +- **数据保存**: 先保存申报数据,再配置审批流程 +- **流程启动**: 自动启动配置的审批流程 +- **用户体验**: 平滑的提交流程,清晰的操作提示 -## 📝 注意事项 +## 📋 实现特色 -- 新增页面需要配置对应的路由 -- API接口统一调用@/api/cw/目录下的接口 -- 权限控制使用v-hasPermi指令,权限编码格式:cw:功能:操作 -- 财务数据需要特别注意精度和安全性 +### 🔧 技术特色 + +1. **Vue 3.5.13 Composition API**: 使用最新的组合式API,代码结构清晰 +2. **TypeScript支持**: 完整的类型定义,提供良好的开发体验 +3. **Element Plus UI**: 统一的UI组件库,符合后勤管理模块设计规范 +4. **响应式设计**: 支持移动端和桌面端,适配不同屏幕尺寸 +5. **组件化开发**: 高度复用的组件设计,易于维护和扩展 + +### 🎨 用户体验特色 + +1. **跨部门选择**: 审批人选择支持在多个部门间切换,保持选择状态 +2. **可视化流程**: 审批流程以图形化方式展示,直观易懂 +3. **实时验证**: 表单验证和流程验证,及时提供反馈 +4. **操作引导**: 清晰的操作提示和帮助信息 +5. **状态恢复**: 切换部门后自动恢复之前的选择状态 + +### 🚀 性能特色 + +1. **按需加载**: 组件和API按需加载,优化性能 +2. **智能缓存**: 合理的数据缓存策略,减少重复请求 +3. **批量操作**: 支持批量添加、删除等操作,提升效率 +4. **异步处理**: 异步API调用,不阻塞用户界面 + +## 🔄 核心流程 + +### 审批人配置流程 + +``` +管理员登录 → 进入审批人配置 → 选择部门 → 批量选择用户 → 确认添加 → 配置完成 +``` + +### 项目申报审批流程 + +``` +填写申报信息 → 点击提交 → 选择审批流程 → 配置审核人 → 确认提交 → 流程启动 +``` + +### 审批执行流程 + +``` +审核人收到通知 → 查看待审批列表 → 查看项目详情 → 执行审批操作 → 填写意见 → 提交审批 +``` + +## 🛠️ 后端支持 + +### 数据库设计 + +1. **cw_project_auditor**: 项目审核人配置表 +2. **cw_project_approval_process**: 项目审批流程表 +3. **cw_approval_record**: 审批记录表(已扩展支持自定义流程) + +### API接口 + +1. **审核人配置**: 16个标准API接口(CRUD + 业务功能 + 辅助功能) +2. **审批流程**: 10个核心API接口(流程管理 + 审批执行) +3. **权限控制**: 基于Sa-Token的权限管理 + +### 数据安全 + +1. **多租户隔离**: 严格的数据隔离机制 +2. **权限控制**: 细粒度的功能权限控制 +3. **操作日志**: 完整的操作记录和审计 + +## 📊 技术规范 + +### 前端规范 + +- **组件命名**: PascalCase,清晰的语义化命名 +- **文件组织**: 按功能模块组织,层次分明 +- **样式管理**: Scoped CSS,避免样式冲突 +- **状态管理**: Reactive响应式状态,简洁高效 + +### 后端规范 + +- **接口设计**: RESTful风格,统一响应格式 +- **数据验证**: Bean Validation参数校验 +- **异常处理**: 统一的异常处理机制 +- **事务管理**: 合理的事务边界控制 + +### 代码质量 + +- **TypeScript**: 完整的类型检查 +- **ESLint**: 代码风格检查 +- **Prettier**: 代码格式化 +- **单元测试**: 核心逻辑单元测试(待实现) + +## 🎯 后续优化 + +### 功能扩展 + +1. **审批流程管理页面**: 完整的流程监控和管理界面 +2. **批量审批功能**: 支持批量处理多个项目 +3. **审批历史查询**: 完整的审批记录查询功能 +4. **移动端支持**: 基于uni-app的移动端审批应用 + +### 性能优化 + +1. **虚拟滚动**: 大数据量列表的性能优化 +2. **缓存策略**: 更智能的数据缓存机制 +3. **懒加载**: 按需加载组件和数据 +4. **CDN集成**: 静态资源CDN加速 + +### 用户体验 + +1. **快捷操作**: 键盘快捷键支持 +2. **拖拽排序**: 审核人拖拽排序功能 +3. **消息通知**: 实时消息推送 +4. **离线支持**: PWA离线功能支持 + +## 🚀 部署说明 + +### 开发环境 + +1. 确保Node.js 20.15.x+,npm 10.7.x+ +2. 安装依赖:`npm install` +3. 启动开发服务器:`npm run dev` +4. 访问:http://localhost + +### 生产环境 + +1. 构建项目:`npm run build` +2. 部署到Nginx或其他Web服务器 +3. 配置反向代理到后端API +4. 确保数据库脚本已执行 + +### 数据库部署 + +1. 执行数据库脚本:`docs/数据库修复脚本.sql` +2. 配置权限菜单和角色权限 +3. 创建测试用户和审核人数据 + +## 📞 技术支持 + +如有任何技术问题或建议,请联系开发团队。 + +--- + +**🎯 目标达成**: 成功实现了从固定三级审批到灵活自定义审批流程的完整改造,提供了良好的用户体验和技术架构! + +**📝 维护日期**: 2025-01-03 +**📁 文件**: 完整的前后端实现 + 数据库脚本 + API文档 +**🔄 状态**: 核心功能完成,待进一步测试和优化 diff --git a/web/src/views/cw/application/form.vue b/web/src/views/cw/application/form.vue index e0f0a504..dd1a5636 100644 --- a/web/src/views/cw/application/form.vue +++ b/web/src/views/cw/application/form.vue @@ -215,53 +215,56 @@ @@ -480,17 +483,59 @@
+ + + +
+ + + +
+ + +
+ + diff --git a/web/src/views/cw/projectApproval/index.vue b/web/src/views/cw/projectApproval/index.vue new file mode 100644 index 00000000..4f24de19 --- /dev/null +++ b/web/src/views/cw/projectApproval/index.vue @@ -0,0 +1,1093 @@ + + + + + diff --git a/web/src/views/cw/projectApproval/pending.vue b/web/src/views/cw/projectApproval/pending.vue new file mode 100644 index 00000000..5e484174 --- /dev/null +++ b/web/src/views/cw/projectApproval/pending.vue @@ -0,0 +1,403 @@ + + + + + diff --git a/web/src/views/cw/projectAuditor/BUG_FIX_REPORT.md b/web/src/views/cw/projectAuditor/BUG_FIX_REPORT.md new file mode 100644 index 00000000..4504242b --- /dev/null +++ b/web/src/views/cw/projectAuditor/BUG_FIX_REPORT.md @@ -0,0 +1,168 @@ +# 项目审批人批量添加功能Bug修复报告 + +## 🐛 问题描述 + +用户反馈:批量添加审批人时,选择用户后点击提交,页面提示添加成功,但后台数据库中没有新增数据。 + +## 🔍 问题分析 + +### 根本原因 + +1. **前端使用模拟API函数**:`index.vue` 中第354-378行定义了模拟的API函数,而不是真实的API调用 +2. **缺少正确的API导入**:没有导入真实的 `batchAddProjectAuditor` 等API函数 +3. **数据格式不匹配**:前端传递的数据格式与后端期望格式存在差异 + +### 问题代码 + +```javascript +// 错误:使用模拟函数 +const batchAddProjectAuditor = (data: any) => { + return Promise.resolve() // 直接返回成功,不实际调用后端 +} +``` + +## 🔧 修复方案 + +### 1. 导入真实API函数 + +```javascript +import { + listProjectAuditor, + getProjectAuditor, + updateProjectAuditor, + delProjectAuditor, + batchAddProjectAuditor +} from '@/api/cw/projectAuditor' +import type { CwProjectAuditorVO, CwProjectAuditorForm, CwProjectAuditorQuery, CwProjectAuditorBatchBo } from '@/api/cw/types' +``` + +### 2. 修复数据类型定义 + +```typescript +// 修正数据类型 +const auditorList = ref([]) +const queryParams = ref({...}) +const editDialog = reactive({ + form: {} as CwProjectAuditorForm +}) +``` + +### 3. 修复批量添加数据格式 + +```javascript +const confirmBatchAdd = async () => { + const userIds = batchDialog.selectedUsers.map((user) => user.userId as number) + const data: CwProjectAuditorBatchBo = { + userIds: userIds, + remark: '批量添加' + } + + const response = await batchAddProjectAuditor(data) + // ... 处理响应 +} +``` + +### 4. 修复后端数据解析 + +```javascript +const response = await listProjectAuditor(queryParams.value); +// 后端返回的是TableDataInfo格式 +auditorList.value = response.data?.rows || []; +total.value = response.data?.total || 0; +``` + +## ✅ 修复内容清单 + +### 前端修复 + +- [x] 移除模拟API函数,导入真实API +- [x] 修复TypeScript类型定义 +- [x] 修复批量添加数据格式 +- [x] 修复后端响应数据解析 +- [x] 改进错误处理和用户反馈 +- [x] 修复按钮事件类型问题 + +### 后端验证 + +- [x] 确认后端API接口正常工作 +- [x] 确认数据库表结构正确 +- [x] 确认权限控制配置 + +## 🧪 测试验证 + +### 测试用例 + +1. **批量添加功能测试** + + - 选择部门 + - 选择多个用户 + - 点击确认添加 + - 验证数据库是否成功插入数据 + +2. **查询列表功能测试** + + - 验证API调用正常 + - 验证数据格式解析正确 + - 验证分页功能 + +3. **错误处理测试** + - 测试网络错误情况 + - 测试用户已存在情况 + - 测试权限不足情况 + +### 测试脚本 + +提供了 `test-api.js` 文件用于功能验证测试。 + +## 📋 技术规范遵循 + +### 符合项目规范 + +- ✅ **前端开发规范**:符合Vue 3.5.13 + TypeScript + Element Plus架构 +- ✅ **API接口规范**:使用统一的request工具和错误处理 +- ✅ **UI设计规范**:继承后勤管理模块的样式风格和颜色主题 +- ✅ **数据库规范**:使用雪花ID、逻辑删除、多租户等标准字段 +- ✅ **权限控制**:使用@SaCheckPermission注解控制接口权限 + +### 代码质量 + +- ✅ **类型安全**:完整的TypeScript类型定义 +- ✅ **错误处理**:完善的异常捕获和用户提示 +- ✅ **用户体验**:优化的加载状态和反馈信息 +- ✅ **响应式设计**:保持左右布局在不同屏幕下的表现 + +## 🚀 部署说明 + +### 验证步骤 + +1. 确保后端服务正常运行 +2. 检查数据库表 `cw_project_auditor` 是否存在 +3. 验证用户权限配置 `cw:projectAuditor:add` +4. 测试前端页面功能 + +### 预期结果 + +- 批量添加功能正常工作 +- 数据成功写入数据库 +- 页面显示准确的成功/失败提示 +- 列表数据正确刷新 + +## 📝 修复日志 + +| 时间 | 修复内容 | 影响范围 | +| ---------- | ---------------- | ------------------ | +| 2025-01-03 | 修复API调用问题 | 批量添加功能 | +| 2025-01-03 | 修复数据格式问题 | 前后端数据传递 | +| 2025-01-03 | 修复类型定义问题 | TypeScript类型安全 | +| 2025-01-03 | 修复响应解析问题 | 列表查询功能 | + +## 🎯 总结 + +本次修复解决了批量添加审批人功能的根本问题,确保: + +1. **功能正常**:批量添加能够真实地向后端发送请求并保存数据 +2. **类型安全**:完整的TypeScript类型定义和检查 +3. **用户体验**:准确的成功/失败提示和数据刷新 +4. **代码质量**:符合项目开发规范和最佳实践 + +修复后的功能完全符合需求分析文档的要求,并遵循项目的前后端开发规范。 diff --git a/web/src/views/cw/projectAuditor/README.md b/web/src/views/cw/projectAuditor/README.md new file mode 100644 index 00000000..40e24670 --- /dev/null +++ b/web/src/views/cw/projectAuditor/README.md @@ -0,0 +1,63 @@ +# 项目审批人配置模块 + +## 功能概述 + +项目审批人配置模块用于管理项目申报审批流程中的审批人员池,支持从系统用户中选择并配置审批人员,为自定义审批流程提供人员支持。 + +## 主要功能 + +### 1. 审批人列表管理 + +- 查询审批人列表(支持分页) +- 按用户姓名、部门、状态等条件筛选 +- 显示审批人基本信息(姓名、部门、联系方式等) +- 支持审批人状态管理(正常/停用) + +### 2. 批量添加审批人 + +- 通过部门树选择部门 +- 展示部门下的可用用户列表 +- 支持多选用户批量添加为审批人 +- 自动过滤已存在的审批人 + +### 3. 审批人信息维护 + +- 修改审批人联系信息 +- 更新审批人状态 +- 添加备注信息 +- 删除审批人配置 + +## 文件结构 + +``` +web/src/views/cw/projectAuditor/ +├── index.vue # 主页面组件 +└── README.md # 功能说明文档 +``` + +## API接口 + +使用的主要API接口: + +- `listProjectAuditor` - 查询审批人列表 +- `getProjectAuditor` - 获取审批人详情 +- `updateProjectAuditor` - 更新审批人信息 +- `delProjectAuditor` - 删除审批人 +- `batchAddProjectAuditor` - 批量添加审批人 +- `getDeptUsers` - 获取部门用户列表 + +## 权限控制 + +页面功能对应的权限标识: + +- `cw:projectAuditor:query` - 查询权限 +- `cw:projectAuditor:add` - 新增权限 +- `cw:projectAuditor:edit` - 编辑权限 +- `cw:projectAuditor:remove` - 删除权限 + +## 技术实现 + +- 基于Vue 3 Composition API +- 使用Element Plus UI组件库 +- 集成项目标准的权限控制和国际化 +- 遵循项目统一的代码规范和样式风格 diff --git a/web/src/views/cw/projectAuditor/debug.vue b/web/src/views/cw/projectAuditor/debug.vue new file mode 100644 index 00000000..eb337eab --- /dev/null +++ b/web/src/views/cw/projectAuditor/debug.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/web/src/views/cw/projectAuditor/index.vue b/web/src/views/cw/projectAuditor/index.vue new file mode 100644 index 00000000..6b374e03 --- /dev/null +++ b/web/src/views/cw/projectAuditor/index.vue @@ -0,0 +1,1021 @@ + + + + + diff --git a/web/src/views/cw/projectAuditor/test.vue b/web/src/views/cw/projectAuditor/test.vue new file mode 100644 index 00000000..39bb97e6 --- /dev/null +++ b/web/src/views/cw/projectAuditor/test.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/web/src/views/jcbt/poverty/Application/index.vue b/web/src/views/jcbt/poverty/Application/index.vue index 7f2ac4a8..8b34ce59 100644 --- a/web/src/views/jcbt/poverty/Application/index.vue +++ b/web/src/views/jcbt/poverty/Application/index.vue @@ -148,7 +148,7 @@ - + 搜索 重置 @@ -167,10 +167,14 @@ 新增申请 - 修改 + 修改 - 删除 + 删除 导出 diff --git a/web/src/views/jcbt/poverty/review/index.vue b/web/src/views/jcbt/poverty/review/index.vue index d81c9867..451e9a8c 100644 --- a/web/src/views/jcbt/poverty/review/index.vue +++ b/web/src/views/jcbt/poverty/review/index.vue @@ -164,7 +164,14 @@ - + 批量驳回 diff --git a/web/src/views/jcbt/scholarship/application/index.vue b/web/src/views/jcbt/scholarship/application/index.vue index a93c8611..af5c3fd2 100644 --- a/web/src/views/jcbt/scholarship/application/index.vue +++ b/web/src/views/jcbt/scholarship/application/index.vue @@ -44,12 +44,7 @@ - + @@ -68,9 +63,9 @@ - + - + 搜索 重置 @@ -89,14 +84,9 @@ 班主任批量申请 - 修改 + 修改 删除 + >删除 diff --git a/web/src/views/jcbt/scholarship/batch/index.vue b/web/src/views/jcbt/scholarship/batch/index.vue index d119d8fb..6801c9da 100644 --- a/web/src/views/jcbt/scholarship/batch/index.vue +++ b/web/src/views/jcbt/scholarship/batch/index.vue @@ -76,7 +76,7 @@ - + 搜索 重置 @@ -153,33 +153,17 @@ 新增 - 修改 + 修改 - 删除 + 删除 - 导出 + 导出 @@ -254,22 +238,10 @@ @@ -178,18 +156,45 @@ @@ -977,13 +982,13 @@ onMounted(() => { } /* 表格卡片布局 */ - .table-card { - margin-top: 0; - } +.table-card { + margin-top: 0; +} - .table-card :deep(.el-card__body) { - padding: 0px 20px 20px 20px !important; - } +.table-card :deep(.el-card__body) { + padding: 0px 20px 20px 20px !important; +} /* 表格容器 - 支持横向滚动 */ .table-container { @@ -1013,29 +1018,29 @@ onMounted(() => { } /* Webkit 浏览器的横向滚动条样式 */ - .scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar { +.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar { height: 12px !important; /* 增加滚动条高度 */ width: 12px !important; - } +} - .scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-track { +.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-track { background: #f0f0f0 !important; border-radius: 6px !important; border: 1px solid #e4e7ed !important; /* 添加边框使其更明显 */ - } +} - .scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb { +.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb { background: #409eff !important; /* 使用主题色,更明显 */ border-radius: 6px !important; border: 2px solid #f0f0f0 !important; /* 添加白色边框 */ min-height: 20px !important; /* 确保最小高度 */ - } +} - .scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:hover { +.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:hover { background: #337ecc !important; /* 悬停时更深的蓝色 */ - } +} - .scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:active { +.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:active { background: #2b6cb0 !important; /* 点击时的颜色 */ } @@ -1065,13 +1070,13 @@ onMounted(() => { } /* 分页容器样式 */ - .pagination-container { - display: flex; - justify-content: flex-end; - padding: 16px 20px 0 0 !important; - background-color: #fff; - margin-top: 0px !important; - } +.pagination-container { + display: flex; + justify-content: flex-end; + padding: 16px 20px 0 0 !important; + background-color: #fff; + margin-top: 0px !important; +} /* 移除按钮栏底部边框 */ .button-card :deep(.el-card__header) { diff --git a/web/src/views/jcbt/scholarship/review/index.vue b/web/src/views/jcbt/scholarship/review/index.vue index 9d8b5300..4c8c5af3 100644 --- a/web/src/views/jcbt/scholarship/review/index.vue +++ b/web/src/views/jcbt/scholarship/review/index.vue @@ -140,7 +140,7 @@ - + 搜索 重置 diff --git a/web/src/views/jcbt/scholarship/type/index.vue b/web/src/views/jcbt/scholarship/type/index.vue index 7b859ee7..0e543e0d 100644 --- a/web/src/views/jcbt/scholarship/type/index.vue +++ b/web/src/views/jcbt/scholarship/type/index.vue @@ -69,9 +69,9 @@ - + - + 搜索 重置 @@ -88,36 +88,20 @@ 新增 - + - 修改 - + 修改 + - 删除 - + 删除 + - 导出 - + 导出 + @@ -180,15 +164,15 @@ @@ -646,19 +630,21 @@ const getExclusiveRelationText = (scholarshipTypeId: string | number): string => } // 异步获取并缓存 - getExclusiveRelationDisplay(scholarshipTypeId).then(response => { - exclusiveRelationCache.value.set(scholarshipTypeId, response.data); - }).catch(error => { - console.warn('获取互斥关系显示文本失败:', error); - exclusiveRelationCache.value.set(scholarshipTypeId, '互斥(无)'); - }); + getExclusiveRelationDisplay(scholarshipTypeId) + .then((response) => { + exclusiveRelationCache.value.set(scholarshipTypeId, response.data); + }) + .catch((error) => { + console.warn('获取互斥关系显示文本失败:', error); + exclusiveRelationCache.value.set(scholarshipTypeId, '互斥(无)'); + }); return '加载中...'; }; /** 预加载互斥关系显示文本 */ const preloadExclusiveRelations = async () => { - const customTypes = scholarshipTypeList.value.filter(item => item.compatibilityType === 'CUSTOM'); + const customTypes = scholarshipTypeList.value.filter((item) => item.compatibilityType === 'CUSTOM'); for (const type of customTypes) { if (type.id && !exclusiveRelationCache.value.has(type.id)) { @@ -967,13 +953,13 @@ onMounted(() => { } /* 表格卡片布局 */ - .table-card { - margin-top: 0; - } +.table-card { + margin-top: 0; +} - .table-card :deep(.el-card__body) { - padding: 0px 20px 20px 20px !important; - } +.table-card :deep(.el-card__body) { + padding: 0px 20px 20px 20px !important; +} /* 表格容器 - 支持横向滚动 */ .table-container { @@ -1003,29 +989,29 @@ onMounted(() => { } /* Webkit 浏览器的横向滚动条样式 */ - .scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar { +.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar { height: 12px !important; /* 增加滚动条高度 */ width: 12px !important; - } +} - .scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-track { +.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-track { background: #f0f0f0 !important; border-radius: 6px !important; border: 1px solid #e4e7ed !important; /* 添加边框使其更明显 */ - } +} - .scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb { +.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb { background: #409eff !important; /* 使用主题色,更明显 */ border-radius: 6px !important; border: 2px solid #f0f0f0 !important; /* 添加白色边框 */ min-height: 20px !important; /* 确保最小高度 */ - } +} - .scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:hover { +.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:hover { background: #337ecc !important; /* 悬停时更深的蓝色 */ - } +} - .scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:active { +.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:active { background: #2b6cb0 !important; /* 点击时的颜色 */ } @@ -1055,13 +1041,13 @@ onMounted(() => { } /* 分页容器样式 */ - .pagination-container { - display: flex; - justify-content: flex-end; - padding: 16px 20px 0 0 !important; - background-color: #fff; - margin-top: 0px !important; - } +.pagination-container { + display: flex; + justify-content: flex-end; + padding: 16px 20px 0 0 !important; + background-color: #fff; + margin-top: 0px !important; +} /* 移除按钮栏底部边框 */ .button-card :deep(.el-card__header) { @@ -1074,23 +1060,23 @@ onMounted(() => { color: #e74c3c; } - /* 对话框样式 */ - .dialog-footer { - display: flex; - justify-content: flex-end; - gap: 10px; +/* 对话框样式 */ +.dialog-footer { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.dialog-form { + .el-form-item { + margin-bottom: 18px; } - .dialog-form { - .el-form-item { - margin-bottom: 18px; - } - - .form-tip { - font-size: 12px; - color: #909399; - margin-top: 4px; - line-height: 1.4; + .form-tip { + font-size: 12px; + color: #909399; + margin-top: 4px; + line-height: 1.4; } } diff --git a/web/src/views/rs/index.vue b/web/src/views/rs/index.vue index 33646f58..dc3363b1 100644 --- a/web/src/views/rs/index.vue +++ b/web/src/views/rs/index.vue @@ -227,7 +227,9 @@ - - + + - + @@ -634,7 +634,6 @@ const resetRecordQuery = () => { } }; - // 查看申请详情 const handleViewApplication = async (row: OverseasApplicationVO) => { try { @@ -821,8 +820,6 @@ const getApprovalStatusTagType = (status: string): 'success' | 'warning' | 'dang }; return typeMap[status] || 'info'; }; - - \ No newline at end of file + diff --git a/web/src/views/stgl/activity/components/ActivityRatingView.vue b/web/src/views/stgl/activity/components/ActivityRatingView.vue index 0f4f083d..d046c14a 100644 --- a/web/src/views/stgl/activity/components/ActivityRatingView.vue +++ b/web/src/views/stgl/activity/components/ActivityRatingView.vue @@ -6,7 +6,7 @@
- + @@ -18,7 +18,7 @@
- +