This commit is contained in:
weizhong committed 2025-09-08 15:12:40 +08:00
commit 465c284d4a
244 files changed
+28906 -5725

No files matched your search

+2 -1
View File
@@ -326,4 +326,5 @@ docs/变更管理/
查找界面中涉及到的权限字符.md
docs/部署文档/nginx配置/hzdyjsweb.camasort.com.conf
首页完善的提示词.md
scripts/
scripts/
学生宿舍卫生检查、学生违纪记录统计.md
+10
View File
@@ -85,6 +85,16 @@
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>prod-local</id>
<properties>
<!-- 生产环境本地配置 -->
<profiles.active>prod-local</profiles.active>
<logging.level>warn</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
@@ -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<Void> 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<Void> executeApproval(@Validated @RequestBody CwApprovalActionBo bo) {
return toAjax(projectApprovalService.executeApproval(bo));
}
/**
* 获取我的待审批列表
*/
@SaCheckPermission("cw:projectApproval:query")
@GetMapping("/pending")
public TableDataInfo<CwApprovalRecordVo> getMyPendingApprovals(PageQuery pageQuery) {
return projectApprovalService.getMyPendingApprovals(pageQuery);
}
/**
* 获取我的已审批列表
*/
@SaCheckPermission("cw:projectApproval:query")
@GetMapping("/processed")
public TableDataInfo<CwApprovalRecordVo> getMyProcessedApprovals(PageQuery pageQuery) {
return projectApprovalService.getMyProcessedApprovals(pageQuery);
}
/**
* 查询审批流程列表
*/
@SaCheckPermission("cw:projectApproval:query")
@GetMapping("/list")
public TableDataInfo<CwProjectApprovalProcessVo> list(PageQuery pageQuery) {
return projectApprovalService.queryProcessList(pageQuery);
}
/**
* 获取审批流程详情
*/
@SaCheckPermission("cw:projectApproval:query")
@GetMapping("/{id}")
public R<CwProjectApprovalProcessVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(projectApprovalService.getProcessDetail(id));
}
/**
* 获取审批记录列表
*/
@SaCheckPermission("cw:projectApproval:query")
@GetMapping("/records/{applicationId}")
public R<List<CwApprovalRecordVo>> getApprovalRecords(@PathVariable Long applicationId) {
return R.ok(projectApprovalService.getApprovalRecords(applicationId));
}
/**
* 终止审批流程
*/
@SaCheckPermission("cw:projectApproval:terminate")
@Log(title = "终止审批流程", businessType = BusinessType.UPDATE)
@DeleteMapping("/{ids}")
public R<Void> terminate(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(projectApprovalService.terminateProcess(List.of(ids)));
}
/**
* 审批流程验证
*/
@SaCheckPermission("cw:projectApproval:query")
@PostMapping("/validate")
public R<String> 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<Object> 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", "已拒绝")
)
));
}
}
@@ -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<CwProjectAuditorVo> 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<CwProjectAuditorVo> list = cwProjectAuditorService.queryList(bo);
ExcelUtil.exportExcel(list, "项目审核人配置", CwProjectAuditorVo.class, response);
}
/**
* 获取项目审核人配置详细信息
*/
@SaCheckPermission("cw:projectAuditor:query")
@GetMapping("/{id}")
public R<CwProjectAuditorVo> 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<Void> 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<Void> 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<Void> 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<Void> batchAdd(@RequestBody BatchAddRequest request) {
return toAjax(cwProjectAuditorService.batchAddAuditors(
request.getUserIds(),
LoginHelper.getUserId(),
LoginHelper.getUsername(),
request.getRemark()
));
}
/**
* 获取可用审核人列表
*/
@SaCheckPermission("cw:projectAuditor:list")
@GetMapping("/available")
public R<List<CwProjectAuditorVo>> getAvailableAuditors() {
return R.ok(cwProjectAuditorService.getAvailableAuditors());
}
/**
* 根据部门ID获取用户列表
*/
@SaCheckPermission("cw:projectAuditor:list")
@GetMapping("/deptUsers/{deptId}")
public R<List<Object>> 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<Void> updateStatus(@RequestBody StatusUpdateRequest request) {
return toAjax(cwProjectAuditorService.updateStatus(request.getIds(), request.getStatus()));
}
/**
* 我配置的审核人
*/
@SaCheckPermission("cw:projectAuditor:list")
@GetMapping("/myConfig")
public TableDataInfo<CwProjectAuditorVo> getMyConfig(CwProjectAuditorBo bo, PageQuery pageQuery) {
bo.setConfigBy(LoginHelper.getUserId());
return cwProjectAuditorService.queryPageList(bo, pageQuery);
}
/**
* 获取审核人选项(用于下拉选择)
*/
@SaCheckPermission("cw:projectAuditor:list")
@GetMapping("/options")
public R<List<CwProjectAuditorVo>> getAuditorOptions() {
return R.ok(cwProjectAuditorService.getAvailableAuditors());
}
/**
* 批量启用
*/
@SaCheckPermission("cw:projectAuditor:edit")
@Log(title = "批量启用审核人", businessType = BusinessType.UPDATE)
@PutMapping("/enable/{ids}")
public R<Void> 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<Void> disable(@PathVariable Long[] ids) {
return toAjax(cwProjectAuditorService.updateStatus(List.of(ids), "0"));
}
/**
* 获取审核人统计数据
*/
@SaCheckPermission("cw:projectAuditor:list")
@GetMapping("/statistics")
public R<Object> getStatistics() {
return R.ok(cwProjectAuditorService.getStatistics());
}
/**
* 批量添加请求对象
*/
public static class BatchAddRequest {
private List<Long> userIds;
private String remark;
public List<Long> getUserIds() {
return userIds;
}
public void setUserIds(List<Long> userIds) {
this.userIds = userIds;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}
/**
* 状态更新请求对象
*/
public static class StatusUpdateRequest {
private List<Long> ids;
private String status;
public List<Long> getIds() {
return ids;
}
public void setIds(List<Long> ids) {
this.ids = ids;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
}
@@ -124,4 +124,19 @@ public class CwApprovalRecord extends TenantEntity implements Serializable {
*/
private String configSnapshot;
}
/**
* 流程ID
*/
private Long processId;
/**
* 自定义节点名称
*/
private String customNodeName;
/**
* 是否自定义流程
*/
private Boolean isCustomProcess;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
}
@@ -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<Long> auditorIds;
/**
* 流程名称
*/
@NotBlank(message = "流程名称不能为空")
private String processName;
/**
* 提交人ID
*/
@NotNull(message = "提交人ID不能为空")
private Long submitterId;
/**
* 提交人姓名
*/
@NotBlank(message = "提交人姓名不能为空")
private String submitterName;
}
@@ -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;
}
@@ -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<Long> userIds;
/**
* 配置人ID
*/
@NotNull(message = "配置人ID不能为空")
private Long configBy;
/**
* 配置人姓名
*/
@NotBlank(message = "配置人姓名不能为空")
private String configByName;
/**
* 备注说明
*/
private String remark;
}
@@ -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;
}
@@ -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;
}
/**
* 总节点数(从流程表获取)
*/
@ExcelProperty(value = "总节点数")
private Integer totalNodes;
/**
* 当前节点数(从流程表获取)
*/
@ExcelProperty(value = "当前节点数")
private Integer currentNode;
/**
* 流程名称(从流程表获取)
*/
@ExcelProperty(value = "流程名称")
private String processName;
/**
* 流程状态(从流程表获取)
*/
@ExcelProperty(value = "流程状态")
private String processStatus;
}
@@ -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;
}
@@ -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;
}
@@ -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<CwProjectApprovalProcess, CwProjectApprovalProcessVo> {
}
@@ -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<CwProjectAuditor, CwProjectAuditorVo> {
/**
* 根据部门ID查询可用审核人
*/
List<CwProjectAuditorVo> selectAuditorsByDeptId(@Param("deptId") Long deptId);
/**
* 查询审核人统计信息
*/
List<CwProjectAuditorVo> selectAuditorStatistics();
/**
* 查询完整的用户信息(包含部门信息)用于批量添加
*/
List<UserWithDeptInfo> selectUsersWithDeptInfo(@Param("userIds") List<Long> userIds);
}
@@ -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<CwApprovalRecord> records);
/**
* 修改审批记录
*/
@@ -190,4 +196,18 @@ public interface ICwApprovalRecordService {
*/
List<Map<String, Object>> getAvailableApprovers(Long applicationId);
/**
* 创建自定义审批流程
* 根据用户选择的审批人列表创建自定义审批流程
*
* @param applicationId 申报ID
* @param applicationNo 申报编号
* @param auditorIds 审批人ID列表(按审批顺序排列)
* @param submitterId 提交人ID
* @param submitterName 提交人名称
* @return 是否创建成功
*/
Boolean createCustomApprovalProcess(Long applicationId, String applicationNo,
List<Long> auditorIds, Long submitterId, String submitterName);
}
@@ -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<CwProjectApprovalProcessVo> queryPageList(CwProjectApprovalProcessVo vo, PageQuery pageQuery);
/**
* 查询项目审批流程列表
*/
List<CwProjectApprovalProcessVo> queryList(CwProjectApprovalProcessVo vo);
/**
* 根据申报ID查询审批流程
*/
CwProjectApprovalProcessVo queryByApplicationId(Long applicationId);
/**
* 根据申报编号查询审批流程
*/
CwProjectApprovalProcessVo queryByApplicationNo(String applicationNo);
/**
* 查询我提交的审批流程
*/
List<CwProjectApprovalProcessVo> queryMyProcesses(Long submitterId);
/**
* 查询进行中的审批流程
*/
List<CwProjectApprovalProcessVo> queryRunningProcesses();
/**
* 查询超时的审批流程
*/
List<CwProjectApprovalProcessVo> queryOvertimeProcesses(Integer hours);
/**
* 启动自定义审批流程
*/
Boolean startCustomApprovalProcess(CwCustomApprovalBo bo);
/**
* 终止审批流程
*/
Boolean terminateProcess(Long id, String reason);
/**
* 校验并批量删除项目审批流程信息
*/
Boolean deleteWithValidByIds(Collection<Long> 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<Map<String, Object>> queryProcessStatistics(Long submitterId);
/**
* 导出数据
*/
List<CwProjectApprovalProcessVo> exportList(CwProjectApprovalProcessVo vo);
}
@@ -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<CwApprovalRecordVo> getMyPendingApprovals(PageQuery pageQuery);
/**
* 获取我的已审批列表
*/
TableDataInfo<CwApprovalRecordVo> getMyProcessedApprovals(PageQuery pageQuery);
/**
* 查询审批流程列表
*/
TableDataInfo<CwProjectApprovalProcessVo> queryProcessList(PageQuery pageQuery);
/**
* 获取审批流程详情
*/
CwProjectApprovalProcessVo getProcessDetail(Long processId);
/**
* 获取审批记录列表
*/
List<CwApprovalRecordVo> getApprovalRecords(Long applicationId);
/**
* 终止审批流程
*/
Boolean terminateProcess(List<Long> processIds);
}
@@ -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<CwProjectAuditorVo> queryPageList(CwProjectAuditorBo bo, PageQuery pageQuery);
/**
* 查询项目审核人配置列表
*/
List<CwProjectAuditorVo> queryList(CwProjectAuditorBo bo);
/**
* 新增项目审核人配置
*/
Boolean insertByBo(CwProjectAuditorBo bo);
/**
* 修改项目审核人配置
*/
Boolean updateByBo(CwProjectAuditorBo bo);
/**
* 校验并批量删除项目审核人配置信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 批量添加审核人(从部门用户中选择)
*/
Boolean batchAddAuditors(List<Long> userIds, Long configBy, String configByName, String remark);
/**
* 获取可用审核人列表
*/
List<CwProjectAuditorVo> getAvailableAuditors();
/**
* 根据部门ID获取用户列表
*/
List<Object> getDeptUsers(Long deptId);
/**
* 批量更新状态
*/
Boolean updateStatus(List<Long> ids, String status);
/**
* 获取审核人统计数据
*/
Object getStatistics();
/**
* 根据用户ID查询项目审核人配置
*/
CwProjectAuditorVo queryByUserId(Long userId);
}
@@ -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<CwApprovalRecord> 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<CwApprovalRecord> currentQuery = Wrappers.<CwApprovalRecord>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<CwApprovalRecord> nextQuery = Wrappers.<CwApprovalRecord>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<CwApprovalRecord> updateWrapper = Wrappers.<CwApprovalRecord>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<Long> 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<CwProjectAuditorVo> validAuditors = validateAuditors(auditorIds);
if (validAuditors.size() != auditorIds.size()) {
throw new RuntimeException("包含无效的审批人,请重新选择");
}
// 3. 创建自定义审批记录
List<CwApprovalRecord> 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<CwProjectAuditorVo> validateAuditors(List<Long> auditorIds) {
List<CwProjectAuditorVo> 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;
}
}
@@ -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());
}
/**
* 生成申报编号
*/
@@ -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<CwProjectApprovalProcessVo> queryPageList(CwProjectApprovalProcessVo vo, PageQuery pageQuery) {
LambdaQueryWrapper<CwProjectApprovalProcess> lqw = buildQueryWrapper(vo);
Page<CwProjectApprovalProcessVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询项目审批流程列表
*/
@Override
public List<CwProjectApprovalProcessVo> queryList(CwProjectApprovalProcessVo vo) {
LambdaQueryWrapper<CwProjectApprovalProcess> lqw = buildQueryWrapper(vo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<CwProjectApprovalProcess> buildQueryWrapper(CwProjectApprovalProcessVo vo) {
LambdaQueryWrapper<CwProjectApprovalProcess> 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<CwProjectApprovalProcess> 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<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 根据申报ID查询审批流程
*/
@Override
public CwProjectApprovalProcessVo queryByApplicationId(Long applicationId) {
LambdaQueryWrapper<CwProjectApprovalProcess> wrapper = Wrappers.lambdaQuery();
wrapper.eq(CwProjectApprovalProcess::getApplicationId, applicationId);
return baseMapper.selectVoOne(wrapper);
}
/**
* 根据申报编号查询审批流程
*/
@Override
public CwProjectApprovalProcessVo queryByApplicationNo(String applicationNo) {
LambdaQueryWrapper<CwProjectApprovalProcess> wrapper = Wrappers.lambdaQuery();
wrapper.eq(CwProjectApprovalProcess::getApplicationNo, applicationNo);
return baseMapper.selectVoOne(wrapper);
}
/**
* 查询我提交的流程
*/
@Override
public List<CwProjectApprovalProcessVo> queryMyProcesses(Long submitterId) {
LambdaQueryWrapper<CwProjectApprovalProcess> wrapper = Wrappers.lambdaQuery();
wrapper.eq(CwProjectApprovalProcess::getSubmitterId, submitterId);
wrapper.orderByDesc(CwProjectApprovalProcess::getCreateTime);
return baseMapper.selectVoList(wrapper);
}
/**
* 查询运行中的流程
*/
@Override
public List<CwProjectApprovalProcessVo> queryRunningProcesses() {
LambdaQueryWrapper<CwProjectApprovalProcess> wrapper = Wrappers.lambdaQuery();
wrapper.eq(CwProjectApprovalProcess::getProcessStatus, "running");
wrapper.orderByDesc(CwProjectApprovalProcess::getCreateTime);
return baseMapper.selectVoList(wrapper);
}
/**
* 查询超时流程
*/
@Override
public List<CwProjectApprovalProcessVo> queryOvertimeProcesses(Integer hours) {
// 这里可以根据需要实现超时查询逻辑
LambdaQueryWrapper<CwProjectApprovalProcess> 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<Map<String, Object>> queryProcessStatistics(Long submitterId) {
// 这里可以根据需要实现统计逻辑
return List.of(Map.of(
"totalProcesses", 0,
"runningProcesses", 0,
"completedProcesses", 0,
"terminatedProcesses", 0
));
}
/**
* 导出数据
*/
@Override
public List<CwProjectApprovalProcessVo> exportList(CwProjectApprovalProcessVo vo) {
return queryList(vo);
}
}
@@ -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<CwApprovalRecordVo> getMyPendingApprovals(PageQuery pageQuery) {
try {
Long userId = LoginHelper.getUserId();
log.info("获取用户{}的待审批列表", userId);
// 🔧 修复:调用现有的审批记录服务查询方法
List<CwApprovalRecordVo> 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<CwApprovalRecordVo> pagedList = start < total ?
list.subList(start, end) : new ArrayList<>();
log.info("✅ 查询待审批列表成功:总数={}, 当前页={}, 返回数量={}",
total, pageQuery.getPageNum(), pagedList.size());
// 🔧 修复:使用正确的TableDataInfo构造方法
TableDataInfo<CwApprovalRecordVo> result = new TableDataInfo<>(pagedList, total);
return result;
} catch (Exception e) {
log.error("获取待审批列表失败: {}", e.getMessage(), e);
return TableDataInfo.build(new ArrayList<>());
}
}
/**
* 获取我的已审批列表
*/
@Override
public TableDataInfo<CwApprovalRecordVo> getMyProcessedApprovals(PageQuery pageQuery) {
try {
Long userId = LoginHelper.getUserId();
log.info("获取用户{}的已审批列表", userId);
// 🔧 修复:直接查询已审批记录,然后手动分页
List<CwApprovalRecordVo> 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<CwApprovalRecordVo> pagedList = start < total ?
allList.subList(start, end) : new ArrayList<>();
log.info("✅ 查询已审批列表成功:总数={}, 当前页={}, 返回数量={}",
total, pageQuery.getPageNum(), pagedList.size());
// 🔧 修复:使用正确的TableDataInfo构造方法
TableDataInfo<CwApprovalRecordVo> result = new TableDataInfo<>(pagedList, total);
return result;
} catch (Exception e) {
log.error("获取已审批列表失败: {}", e.getMessage(), e);
return TableDataInfo.build(new ArrayList<>());
}
}
/**
* 查询审批流程列表
*/
@Override
public TableDataInfo<CwProjectApprovalProcessVo> queryProcessList(PageQuery pageQuery) {
try {
log.info("查询审批流程列表");
// 这里应该查询具体的流程数据
// 由于缺少具体的业务逻辑,这里返回空列表
// 实际项目中需要根据具体业务需求实现
List<CwProjectApprovalProcessVo> 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<CwApprovalRecordVo> 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<Long> processIds) {
try {
log.info("终止审批流程: {}", processIds);
// 这里应该实现具体的流程终止逻辑
// 由于缺少具体的业务逻辑,这里返回成功状态
// 实际项目中需要根据具体业务需求实现
return true;
} catch (Exception e) {
log.error("终止审批流程失败: {}", e.getMessage(), e);
return false;
}
}
}
@@ -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<CwProjectAuditorVo> queryPageList(CwProjectAuditorBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<CwProjectAuditor> lqw = buildQueryWrapper(bo);
Page<CwProjectAuditorVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询项目审核人配置列表
*/
@Override
public List<CwProjectAuditorVo> queryList(CwProjectAuditorBo bo) {
LambdaQueryWrapper<CwProjectAuditor> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<CwProjectAuditor> buildQueryWrapper(CwProjectAuditorBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<CwProjectAuditor> 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<CwProjectAuditor> 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<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 批量添加审核人
*/
@Override
@Transactional
public Boolean batchAddAuditors(List<Long> userIds, Long configBy, String configByName, String remark) {
if (userIds == null || userIds.isEmpty()) {
return false;
}
// 1. 获取完整的用户信息(包含部门信息)
List<UserWithDeptInfo> users = baseMapper.selectUsersWithDeptInfo(userIds);
if (users.isEmpty()) {
return false;
}
// 2. 过滤已存在的审核人
List<Long> existingUserIds = baseMapper.selectList(Wrappers.<CwProjectAuditor>lambdaQuery()
.in(CwProjectAuditor::getUserId, userIds))
.stream().map(CwProjectAuditor::getUserId).collect(Collectors.toList());
// 3. 构建新增审核人列表
List<CwProjectAuditor> 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<CwProjectAuditorVo> getAvailableAuditors() {
LambdaQueryWrapper<CwProjectAuditor> wrapper = Wrappers.lambdaQuery();
wrapper.eq(CwProjectAuditor::getStatus, "1"); // 只查询正常状态的审核人
wrapper.orderByAsc(CwProjectAuditor::getDeptId, CwProjectAuditor::getUserName);
return baseMapper.selectVoList(wrapper);
}
/**
* 根据部门ID获取用户列表
*/
@Override
public List<Object> getDeptUsers(Long deptId) {
try {
// 参数验证
if (deptId == null || deptId <= 0) {
log.warn("无效的部门ID: {}", deptId);
return Collections.emptyList();
}
// 调用系统用户服务获取部门用户
List<SysUserVo> users = sysUserService.selectUserListByDept(deptId);
if (users == null || users.isEmpty()) {
log.info("部门 {} 下没有找到用户", deptId);
return Collections.emptyList();
}
return users.stream().map(user -> {
Map<String, Object> 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<Long> ids, String status) {
if (ids == null || ids.isEmpty()) {
return false;
}
LambdaQueryWrapper<CwProjectAuditor> 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.<CwProjectAuditor>lambdaQuery()
.eq(CwProjectAuditor::getDeleted, 0));
// 获取正常状态审核人数
long activeAuditors = baseMapper.selectCount(Wrappers.<CwProjectAuditor>lambdaQuery()
.eq(CwProjectAuditor::getDeleted, 0)
.eq(CwProjectAuditor::getStatus, "1"));
// 获取停用状态审核人数
long inactiveAuditors = baseMapper.selectCount(Wrappers.<CwProjectAuditor>lambdaQuery()
.eq(CwProjectAuditor::getDeleted, 0)
.eq(CwProjectAuditor::getStatus, "0"));
// 获取部门数量(去重)
List<CwProjectAuditor> auditors = baseMapper.selectList(Wrappers.<CwProjectAuditor>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.<CwProjectAuditor>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.<CwProjectAuditor>lambdaQuery()
.eq(CwProjectAuditor::getDeleted, 0)
.ge(CwProjectAuditor::getCreateTime, startOfMonth)
.le(CwProjectAuditor::getCreateTime, endOfMonth));
// 获取我配置的审核人数
long myConfigured = baseMapper.selectCount(Wrappers.<CwProjectAuditor>lambdaQuery()
.eq(CwProjectAuditor::getDeleted, 0)
.eq(CwProjectAuditor::getConfigBy, LoginHelper.getUserId()));
// 获取最近活跃数(近7天有更新的)
LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7);
long recentActive = baseMapper.selectCount(Wrappers.<CwProjectAuditor>lambdaQuery()
.eq(CwProjectAuditor::getDeleted, 0)
.ge(CwProjectAuditor::getUpdateTime, sevenDaysAgo));
// 构建统计结果
Map<String, Object> 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<CwProjectAuditor> wrapper = Wrappers.<CwProjectAuditor>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);
}
}
@@ -72,6 +72,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="approvalStatus" column="approval_status" />
<result property="submitTime" column="submit_time" />
<result property="approvalTime" column="approval_time" />
<!-- 流程相关字段 -->
<result property="totalNodes" column="total_nodes" />
<result property="currentNode" column="current_node" />
<result property="processName" column="process_name" />
<result property="processStatus" column="process_status" />
</resultMap>
<sql id="selectCwApprovalRecordVo">
@@ -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
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.cw.mapper.CwProjectApprovalProcessMapper">
<resultMap type="org.dromara.cw.domain.vo.CwProjectApprovalProcessVo" id="CwProjectApprovalProcessResult">
<result property="id" column="id" />
<result property="applicationId" column="application_id" />
<result property="applicationNo" column="application_no" />
<result property="processName" column="process_name" />
<result property="processStatus" column="process_status" />
<result property="totalNodes" column="total_nodes" />
<result property="currentNode" column="current_node" />
<result property="startTime" column="start_time" />
<result property="endTime" column="end_time" />
<result property="submitterId" column="submitter_id" />
<result property="submitterName" column="submitter_name" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectCwProjectApprovalProcessVo">
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
</sql>
<select id="selectProcessList" parameterType="org.dromara.cw.domain.CwProjectApprovalProcess" resultMap="CwProjectApprovalProcessResult">
<include refid="selectCwProjectApprovalProcessVo"/>
<where>
p.deleted = 0
<if test="applicationId != null"> and p.application_id = #{applicationId}</if>
<if test="applicationNo != null and applicationNo != ''"> and p.application_no like concat('%', #{applicationNo}, '%')</if>
<if test="processName != null and processName != ''"> and p.process_name like concat('%', #{processName}, '%')</if>
<if test="processStatus != null and processStatus != ''"> and p.process_status = #{processStatus}</if>
<if test="submitterId != null"> and p.submitter_id = #{submitterId}</if>
<if test="submitterName != null and submitterName != ''"> and p.submitter_name like concat('%', #{submitterName}, '%')</if>
<if test="params.beginCreateTime != null and params.beginCreateTime != ''"> and p.create_time &gt;= #{params.beginCreateTime}</if>
<if test="params.endCreateTime != null and params.endCreateTime != ''"> and p.create_time &lt;= #{params.endCreateTime}</if>
<if test="params.beginStartTime != null and params.beginStartTime != ''"> and p.start_time &gt;= #{params.beginStartTime}</if>
<if test="params.endStartTime != null and params.endStartTime != ''"> and p.start_time &lt;= #{params.endStartTime}</if>
</where>
order by p.create_time desc
</select>
<select id="selectByApplicationId" resultMap="CwProjectApprovalProcessResult">
<include refid="selectCwProjectApprovalProcessVo"/>
where p.application_id = #{applicationId} and p.deleted = 0
</select>
<select id="selectByApplicationNo" resultMap="CwProjectApprovalProcessResult">
<include refid="selectCwProjectApprovalProcessVo"/>
where p.application_no = #{applicationNo} and p.deleted = 0
</select>
<select id="selectMyProcesses" resultMap="CwProjectApprovalProcessResult">
<include refid="selectCwProjectApprovalProcessVo"/>
where p.submitter_id = #{submitterId} and p.deleted = 0
order by p.create_time desc
</select>
<select id="selectRunningProcesses" resultMap="CwProjectApprovalProcessResult">
<include refid="selectCwProjectApprovalProcessVo"/>
where p.process_status = 'running' and p.deleted = 0
order by p.start_time
</select>
<select id="selectOvertimeProcesses" resultMap="CwProjectApprovalProcessResult">
<include refid="selectCwProjectApprovalProcessVo"/>
where p.process_status = 'running'
and p.start_time &lt; date_sub(now(), interval #{hours} hour)
and p.deleted = 0
order by p.start_time
</select>
<select id="selectProcessStatistics" resultType="map">
select
p.process_status as status,
count(*) as count
from cw_project_approval_process p
where p.deleted = 0
<if test="submitterId != null"> and p.submitter_id = #{submitterId}</if>
group by p.process_status
order by count desc
</select>
<update id="updateCurrentNode">
update cw_project_approval_process
set current_node = #{currentNode}, update_by = #{updateBy}, update_time = now()
where id = #{id} and deleted = 0
</update>
<update id="updateProcessStatus">
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
</update>
</mapper>
@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.cw.mapper.CwProjectAuditorMapper">
<resultMap type="org.dromara.cw.domain.vo.CwProjectAuditorVo" id="CwProjectAuditorResult">
<result property="id" column="id" />
<result property="userId" column="user_id" />
<result property="userName" column="user_name" />
<result property="nickName" column="nick_name" />
<result property="deptId" column="dept_id" />
<result property="deptName" column="dept_name" />
<result property="phoneNumber" column="phone_number" />
<result property="email" column="email" />
<result property="status" column="status" />
<result property="remark" column="remark" />
<result property="configBy" column="config_by" />
<result property="configByName" column="config_by_name" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectCwProjectAuditorVo">
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
</sql>
<select id="selectAuditorList" parameterType="org.dromara.cw.domain.CwProjectAuditor" resultMap="CwProjectAuditorResult">
<include refid="selectCwProjectAuditorVo"/>
<where>
a.deleted = 0
<if test="userId != null"> and a.user_id = #{userId}</if>
<if test="userName != null and userName != ''"> and a.user_name like concat('%', #{userName}, '%')</if>
<if test="nickName != null and nickName != ''"> and a.nick_name like concat('%', #{nickName}, '%')</if>
<if test="deptId != null"> and a.dept_id = #{deptId}</if>
<if test="deptName != null and deptName != ''"> and a.dept_name like concat('%', #{deptName}, '%')</if>
<if test="status != null and status != ''"> and a.status = #{status}</if>
<if test="configBy != null"> and a.config_by = #{configBy}</if>
<if test="configByName != null and configByName != ''"> and a.config_by_name like concat('%', #{configByName}, '%')</if>
<if test="params.beginCreateTime != null and params.beginCreateTime != ''"> and a.create_time &gt;= #{params.beginCreateTime}</if>
<if test="params.endCreateTime != null and params.endCreateTime != ''"> and a.create_time &lt;= #{params.endCreateTime}</if>
</where>
order by a.create_time desc
</select>
<select id="selectAuditorsByDeptId" resultMap="CwProjectAuditorResult">
<include refid="selectCwProjectAuditorVo"/>
where a.dept_id = #{deptId} and a.status = '1' and a.deleted = 0
order by a.create_time desc
</select>
<select id="selectAvailableAuditors" resultMap="CwProjectAuditorResult">
<include refid="selectCwProjectAuditorVo"/>
where a.status = '1' and a.deleted = 0
order by a.dept_id, a.create_time desc
</select>
<select id="selectMyConfigAuditors" resultMap="CwProjectAuditorResult">
<include refid="selectCwProjectAuditorVo"/>
where a.config_by = #{configBy} and a.deleted = 0
order by a.create_time desc
</select>
<select id="selectAuditorsByUserIds" resultMap="CwProjectAuditorResult">
<include refid="selectCwProjectAuditorVo"/>
where a.user_id in
<foreach item="userId" collection="userIds" open="(" separator="," close=")">
#{userId}
</foreach>
and a.status = '1' and a.deleted = 0
order by a.create_time desc
</select>
<select id="selectAuditorStatistics" resultType="map">
select
d.dept_name,
count(*) as total_count,
count(case when a.status = '1' then 1 end) as active_count,
count(case when a.status = '0' then 1 end) as inactive_count
from cw_project_auditor a
left join sys_dept d on a.dept_id = d.dept_id
where a.deleted = 0
<if test="deptId != null"> and a.dept_id = #{deptId}</if>
<if test="status != null and status != ''"> and a.status = #{status}</if>
group by a.dept_id, d.dept_name
order by total_count desc
</select>
<select id="selectByUserId" resultMap="CwProjectAuditorResult">
<include refid="selectCwProjectAuditorVo"/>
where a.user_id = #{userId} and a.deleted = 0
</select>
<update id="batchUpdateStatus">
update cw_project_auditor
set status = #{status}, update_by = #{updateBy}, update_time = now()
where id in
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
and deleted = 0
</update>
<!-- 查询完整的用户信息(包含部门信息)用于批量添加 -->
<select id="selectUsersWithDeptInfo" resultType="org.dromara.cw.domain.UserWithDeptInfo">
select
u.user_id as userId,
u.user_name as userName,
u.nick_name as nickName,
u.dept_id as deptId,
d.dept_name as deptName,
u.phonenumber as phonenumber,
u.email as email,
u.status as status
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
where u.status = '0'
and u.user_id in
<foreach item="userId" collection="userIds" open="(" separator="," close=")">
#{userId}
</foreach>
order by u.dept_id, u.user_name
</select>
</mapper>
@@ -297,7 +297,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
@Override
public List<SysUserVo> selectUserByIds(List<Long> userIds, Long deptId) {
return baseMapper.selectUserList(new LambdaQueryWrapper<SysUser>()
.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));
+7
View File
@@ -104,6 +104,13 @@
<artifactId>ruoyi-workflow</artifactId>
</dependency>
<!-- 学生考核模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-xskh</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>
@@ -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")
@@ -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<BuildingTreeSelectVo> treeData = xgDormBuildingService.getBuildingTreeData();
return R.ok(treeData);
}
/**
* 查询宿舍楼信息简化列表
* 用于简化界面显示,隐藏宿舍区层级,直接显示校区→宿舍楼结构
*/
@SaCheckPermission("sslxx:dormBuilding:list")
@GetMapping("/simplifiedList")
public TableDataInfo<XgDormBuildingSimplifiedVo> getSimplifiedList(XgDormBuildingBo bo, PageQuery pageQuery) {
return xgDormBuildingService.querySimplifiedPageList(bo, pageQuery);
}
/**
* 查询宿舍楼信息简化列表(不分页)
* 用于下拉选择等场景
*/
@SaCheckPermission("sslxx:dormBuilding:list")
@GetMapping("/simplifiedListAll")
public R<List<XgDormBuildingSimplifiedVo>> getSimplifiedListAll(XgDormBuildingBo bo) {
List<XgDormBuildingSimplifiedVo> list = xgDormBuildingService.querySimplifiedList(bo);
return R.ok(list);
}
}
@@ -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<List<DormBuildingTreeVo>> getDormBuildingTree(DormBuildingTreeBo bo) {
List<DormBuildingTreeVo> treeData = xgDormBuildingTreeService.getDormBuildingTree(bo);
public R<List<DormBuildingTreeSimplifiedVo>> getDormBuildingTree(DormBuildingTreeBo bo) {
List<DormBuildingTreeSimplifiedVo> treeData = xgDormBuildingTreeService.getSimplifiedDormBuildingTree(bo);
return R.ok(treeData);
}
/**
* 获取所有宿舍楼树形结构数据(不考虑权限)
* 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择
* 校区-宿舍楼二级结构,校区不可选,只有宿舍楼可以选择
*/
@SaCheckPermission("xgss:dormBuildingTree:list")
@GetMapping("/listAll")
public R<List<DormBuildingTreeVo>> getAllDormBuildingTree(DormBuildingTreeBo bo) {
public R<List<DormBuildingTreeSimplifiedVo>> getAllDormBuildingTree(DormBuildingTreeBo bo) {
List<DormBuildingTreeSimplifiedVo> treeData = xgDormBuildingTreeService.getAllSimplifiedDormBuildingTree(bo);
return R.ok(treeData);
}
/**
* 获取宿舍楼树形结构完整数据(包含宿舍区层级)
* 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择
* 注意:此接口仅用于特殊需求,一般情况请使用/list接口
*/
@SaCheckPermission("xgss:dormBuildingTree:list")
@GetMapping("/fullList")
public R<List<DormBuildingTreeVo>> getFullDormBuildingTree(DormBuildingTreeBo bo) {
List<DormBuildingTreeVo> treeData = xgDormBuildingTreeService.getDormBuildingTree(bo);
return R.ok(treeData);
}
/**
* 获取所有宿舍楼树形结构完整数据(不考虑权限,包含宿舍区层级)
* 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择
* 注意:此接口仅用于特殊需求,一般情况请使用/listAll接口
*/
@SaCheckPermission("xgss:dormBuildingTree:list")
@GetMapping("/fullListAll")
public R<List<DormBuildingTreeVo>> getAllFullDormBuildingTree(DormBuildingTreeBo bo) {
List<DormBuildingTreeVo> treeData = xgDormBuildingTreeService.getAllDormBuildingTree(bo);
return R.ok(treeData);
}
/**
* 获取宿舍楼树形结构简化数据
* 校区-宿舍楼二级结构,隐藏宿舍区层级,校区不可选,只有宿舍楼可以选择
*/
@SaCheckPermission("xgss:dormBuildingTree:list")
@GetMapping("/simplifiedList")
public R<List<DormBuildingTreeSimplifiedVo>> getSimplifiedDormBuildingTree(DormBuildingTreeBo bo) {
List<DormBuildingTreeSimplifiedVo> treeData = xgDormBuildingTreeService.getSimplifiedDormBuildingTree(bo);
return R.ok(treeData);
}
/**
* 获取所有宿舍楼树形结构简化数据(不考虑权限)
* 校区-宿舍楼二级结构,隐藏宿舍区层级,校区不可选,只有宿舍楼可以选择
*/
@SaCheckPermission("xgss:dormBuildingTree:list")
@GetMapping("/simplifiedListAll")
public R<List<DormBuildingTreeSimplifiedVo>> getAllSimplifiedDormBuildingTree(DormBuildingTreeBo bo) {
List<DormBuildingTreeSimplifiedVo> treeData = xgDormBuildingTreeService.getAllSimplifiedDormBuildingTree(bo);
return R.ok(treeData);
}
}
@@ -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<Long> 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<Void> 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<List<XgHygieneCheckViolationVo>> getViolations(
@NotNull(message = "卫生检查ID不能为空") @PathVariable Long hygieneCheckId) {
List<XgHygieneCheckViolationVo> violations = xgDormHygieneCheckService
.getViolationsByHygieneCheckId(hygieneCheckId);
return R.ok(violations);
}
}
@@ -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<String, Object> 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);
}
/**
* 获取宿舍房间信息详细信息
*
@@ -111,4 +111,15 @@ public class XgHolidayStayController extends BaseController {
public TableDataInfo<StudentOptionVo> getStudentOptions(StudentOptionBo bo, PageQuery pageQuery) {
return xgHolidayStayService.getStudentOptions(bo, pageQuery);
}
/**
* 查询单人留宿有效数据
* 获取所有有效的假期留宿数据,筛选出在同一时间段内该宿舍只有一个学生留宿的记录
*/
@SaCheckPermission("holidayStay:holidayStay:list")
@GetMapping("/singlePersonValidStay")
public R<List<XgHolidayStayVo>> getSinglePersonValidStay() {
List<XgHolidayStayVo> result = xgHolidayStayService.querySinglePersonValidStay();
return R.ok(result);
}
}
@@ -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<Void> 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<Map<Long, XgStudentLeaveVo>> queryByCheckDate(
@RequestParam String studentIds,
@RequestParam String checkDate) {
try {
List<Long> studentIdList = List.of(studentIds.split(","))
.stream()
.map(String::trim)
.map(Long::valueOf)
.toList();
LocalDateTime checkDateTime = LocalDateTime.parse(checkDate.replace(" ", "T"));
Map<Long, XgStudentLeaveVo> result = xgStudentLeaveService.queryStudentLeavesByCheckDate(studentIdList,
checkDateTime);
return R.ok(result);
} catch (Exception e) {
return R.fail("参数格式错误:" + e.getMessage());
}
}
}
@@ -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<XgStudentStatisticsVo> 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<List<XgCascadeOptionVo>> getColleges() {
return R.ok(studentStatisticsService.getColleges());
}
/**
* 根据学院ID获取专业列表
*/
@SaCheckPermission("xg:studentStatistics:query")
@GetMapping("/majors/{collegeId}")
public R<List<XgCascadeOptionVo>> getMajorsByCollegeId(
@NotNull(message = "学院ID不能为空") @PathVariable Long collegeId) {
return R.ok(studentStatisticsService.getMajorsByCollegeId(collegeId));
}
/**
* 根据专业ID获取班级列表
*/
@SaCheckPermission("xg:studentStatistics:query")
@GetMapping("/classes/{majorId}")
public R<List<XgCascadeOptionVo>> getClassesByMajorId(
@NotNull(message = "专业ID不能为空") @PathVariable Long majorId) {
return R.ok(studentStatisticsService.getClassesByMajorId(majorId));
}
}
@@ -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;
}
@@ -94,7 +94,7 @@ public class XgDormHygieneStudentScore extends TenantEntity {
private String bedCode;
/**
* 学生个人得分(0-100分)
* 学生个人扣分(0-100分)
*/
private Double studentScore;
@@ -67,6 +67,11 @@ public class XgDormRoom extends TenantEntity {
*/
private String roomStatus;
/**
* 排序号
*/
private Long sort;
/**
* 删除标志(0代表存在 1代表删除)
*/
@@ -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;
}
@@ -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;
/**
@@ -147,5 +147,14 @@ public class XgDormHygieneCheckBo extends BaseEntity {
*/
private String checkRemarks;
/**
* 违纪类型ID,关联xskh_discipline_type.id
*/
private Long disciplineTypeId;
/**
* 违纪类型名称
*/
private String disciplineTypeName;
}
@@ -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<XgDormHygieneStudentScoreBo> studentScores;
/**
* 违纪记录数据列表
*/
@Valid
private List<XgViolationDataBo> violationData;
// 基础字段(用于VO类映射)
private Long createBy;
@@ -166,4 +182,4 @@ public class XgDormHygieneCheckCompleteBo {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<DormBuildingTreeSimplifiedVo> 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<DormBuildingTreeSimplifiedVo> flattenAreaChildren(List<DormBuildingTreeVo> 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();
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<StatisticsChartData> chartData;
/**
* 明细数据
*/
private List<StatisticsDetailData> 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;
}
}
@@ -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<XgHygieneCheckViolation, XgHygieneCheckViolationVo> {
/**
* 根据卫生检查ID查询违纪记录(包含详情)
*
* @param hygieneCheckId 卫生检查ID
* @return 违纪记录列表(包含详情)
*/
List<XgHygieneCheckViolationVo> selectViolationsByHygieneCheckId(@Param("hygieneCheckId") Long hygieneCheckId);
}
@@ -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<XgStudentStatisticsVo.StatisticsChartData> getCollegeStatistics(@Param("bo") XgStudentStatisticsBo bo);
/**
* 按学院维度统计明细数据
*/
List<XgStudentStatisticsVo.StatisticsDetailData> getCollegeDetailStatistics(@Param("bo") XgStudentStatisticsBo bo);
/**
* 按专业维度统计图表数据
*/
List<XgStudentStatisticsVo.StatisticsChartData> getMajorStatistics(@Param("bo") XgStudentStatisticsBo bo);
/**
* 按专业维度统计明细数据
*/
List<XgStudentStatisticsVo.StatisticsDetailData> getMajorDetailStatistics(@Param("bo") XgStudentStatisticsBo bo);
/**
* 按班级维度统计图表数据
*/
List<XgStudentStatisticsVo.StatisticsChartData> getClassStatistics(@Param("bo") XgStudentStatisticsBo bo);
/**
* 按班级维度统计明细数据
*/
List<XgStudentStatisticsVo.StatisticsDetailData> getClassDetailStatistics(@Param("bo") XgStudentStatisticsBo bo);
/**
* 获取二级学院列表
*/
List<XgCascadeOptionVo> getColleges();
/**
* 根据学院ID获取专业列表
*/
List<XgCascadeOptionVo> getMajorsByCollegeId(@Param("collegeId") Long collegeId);
/**
* 根据专业ID获取班级列表
*/
List<XgCascadeOptionVo> getClassesByMajorId(@Param("majorId") Long majorId);
}
@@ -107,7 +107,7 @@ public interface IXgDormBedStudentService {
/**
* 获取当前用户二级学院的宿舍楼树形结构
* 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择
* 校区-宿舍楼二级结构,校区不可选,只有宿舍楼可以选择
*
* @return 宿舍楼树形结构数据
*/
@@ -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<BuildingTreeSelectVo> getBuildingTreeData();
/**
* 分页查询宿舍楼信息简化列表
* 用于简化界面显示,隐藏宿舍区层级,直接显示校区→宿舍楼结构
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 宿舍楼信息简化分页列表
*/
TableDataInfo<XgDormBuildingSimplifiedVo> querySimplifiedPageList(XgDormBuildingBo bo, PageQuery pageQuery);
/**
* 查询符合条件的宿舍楼信息简化列表
* 用于简化界面显示,隐藏宿舍区层级,直接显示校区→宿舍楼结构
*
* @param bo 查询条件
* @return 宿舍楼信息简化列表
*/
List<XgDormBuildingSimplifiedVo> querySimplifiedList(XgDormBuildingBo bo);
}
@@ -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<DormBuildingTreeVo> getAllDormBuildingTree(DormBuildingTreeBo bo);
/**
* 获取宿舍楼树形结构简化数据
* 校区-宿舍楼二级结构,隐藏宿舍区层级
*
* @param bo 查询参数
* @return 简化树形结构数据
*/
List<DormBuildingTreeSimplifiedVo> getSimplifiedDormBuildingTree(DormBuildingTreeBo bo);
/**
* 获取所有宿舍楼树形结构简化数据(不考虑权限)
* 校区-宿舍楼二级结构,隐藏宿舍区层级
*
* @param bo 查询参数
* @return 简化树形结构数据
*/
List<DormBuildingTreeSimplifiedVo> getAllSimplifiedDormBuildingTree(DormBuildingTreeBo bo);
}
@@ -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<XgDormHygieneCheckVo> 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<XgHygieneCheckViolationVo> getViolationsByHygieneCheckId(Long hygieneCheckId);
}
@@ -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<Long> ids, Boolean isValid);
/**
* 导出住宿清单(宿舍卡片式布局)
*
* @param params 导出参数
* @param response HTTP响应对象
*/
void exportAccommodationList(Map<String, Object> params, HttpServletResponse response);
}
@@ -77,4 +77,12 @@ public interface IXgHolidayStayService {
*/
TableDataInfo<StudentOptionVo> getStudentOptions(StudentOptionBo bo, PageQuery pageQuery);
/**
* 查询单人留宿有效数据
* 获取所有有效的假期留宿数据,筛选出在同一时间段内该宿舍只有一个学生留宿的记录
*
* @return 单人留宿有效数据列表
*/
List<XgHolidayStayVo> querySinglePersonValidStay();
}
@@ -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<XgHygieneCheckViolationVo> queryPageList(XgHygieneCheckViolationBo bo, PageQuery pageQuery);
/**
* 查询符合条件的宿舍卫生检查违纪记录列表
*
* @param bo 查询条件
* @return 宿舍卫生检查违纪记录列表
*/
List<XgHygieneCheckViolationVo> 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<Long> ids, Boolean isValid);
/**
* 批量新增宿舍卫生检查违纪记录
*
* @param bos 违纪记录列表
* @return 是否新增成功
*/
Boolean insertBatch(List<XgHygieneCheckViolationBo> bos);
/**
* 根据卫生检查ID查询违纪记录列表
*
* @param hygieneCheckId 卫生检查ID
* @return 违纪记录列表
*/
List<XgHygieneCheckViolationVo> queryByHygieneCheckId(Long hygieneCheckId);
/**
* 根据卫生检查ID删除违纪关联记录
*
* @param hygieneCheckId 卫生检查ID
* @return 是否删除成功
*/
Boolean deleteByHygieneCheckId(Long hygieneCheckId);
}
@@ -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<Long> ids, Boolean isValid);
/**
* 根据学生ID列表和检查日期查询学生请假信息
*
* @param studentIds 学生ID列表
* @param checkDate 检查日期
* @return 学生ID -> 请假信息的映射
*/
Map<Long, XgStudentLeaveVo> queryStudentLeavesByCheckDate(List<Long> studentIds, LocalDateTime checkDate);
}
@@ -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<XgCascadeOptionVo> getColleges();
/**
* 根据学院ID获取专业列表
*
* @param collegeId 学院ID
* @return 专业列表
*/
List<XgCascadeOptionVo> getMajorsByCollegeId(Long collegeId);
/**
* 根据专业ID获取班级列表
*
* @param majorId 专业ID
* @return 班级列表
*/
List<XgCascadeOptionVo> getClassesByMajorId(Long majorId);
}
@@ -983,7 +983,8 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
Long roomId = entry.getKey();
List<Long> 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<XgDormCampus> 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<DormBuildingTreeVo> buildSimplifiedTreeStructure(List<XgDormCampus> campuses,
List<XgDormArea> areas,
List<XgDormBuilding> buildings) {
// 构建校区节点(第一层)
Map<Long, DormBuildingTreeVo> 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());
}
/**
* 获取班主任管理的班级宿舍楼树
* 通过当前用户所在的班级,获取该班级分配的宿舍楼
@@ -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.<XgDormCampus>lambdaQuery()
.orderByAsc(XgDormCampus::getSortOrder, XgDormCampus::getId));
// 2. 获取所有宿舍区数据
List<XgDormArea> areaList = areaMapper.selectList(
Wrappers.<XgDormArea>lambdaQuery()
.orderByAsc(XgDormArea::getSortOrder, XgDormArea::getId));
// 3. 获取所有宿舍楼数据
// 2. 获取所有宿舍楼数据
List<XgDormBuilding> buildingList = baseMapper.selectList(
Wrappers.<XgDormBuilding>lambdaQuery()
.orderByAsc(XgDormBuilding::getSortOrder, XgDormBuilding::getId));
// 4. 构建树形结构
// 3. 构建树形结构
List<BuildingTreeSelectVo> 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<XgDormArea> campusAreas = areaList.stream()
.filter(area -> area.getCampusId().equals(campus.getId()))
// 检查该校区下是否有宿舍楼
List<XgDormBuilding> 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<XgDormBuilding> 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<XgDormBuildingSimplifiedVo> querySimplifiedPageList(XgDormBuildingBo bo, PageQuery pageQuery) {
// 先查询完整的分页数据
TableDataInfo<XgDormBuildingVo> fullPageList = queryPageList(bo, pageQuery);
// 转换为简化版VO
List<XgDormBuildingSimplifiedVo> simplifiedList = fullPageList.getRows().stream()
.map(XgDormBuildingSimplifiedVo::fromFullVo)
.collect(Collectors.toList());
// 创建简化版分页结果
TableDataInfo<XgDormBuildingSimplifiedVo> simplifiedPageList = new TableDataInfo<>();
simplifiedPageList.setRows(simplifiedList);
simplifiedPageList.setTotal(fullPageList.getTotal());
return simplifiedPageList;
}
/**
* 查询符合条件的宿舍楼信息简化列表
* 用于简化界面显示,隐藏宿舍区层级,直接显示校区→宿舍楼结构
*
* @param bo 查询条件
* @return 宿舍楼信息简化列表
*/
@Override
public List<XgDormBuildingSimplifiedVo> querySimplifiedList(XgDormBuildingBo bo) {
// 先查询完整的数据
List<XgDormBuildingVo> fullList = queryList(bo);
// 转换为简化版VO
return fullList.stream()
.map(XgDormBuildingSimplifiedVo::fromFullVo)
.collect(Collectors.toList());
}
}
@@ -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<DormBuildingTreeSimplifiedVo> getSimplifiedDormBuildingTree(DormBuildingTreeBo bo) {
// 先获取完整的树形结构数据
List<DormBuildingTreeVo> fullTreeData = getDormBuildingTree(bo);
// 转换为简化版树形结构
return fullTreeData.stream()
.map(DormBuildingTreeSimplifiedVo::fromFullTreeVo)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* 获取所有宿舍楼树形结构简化数据(不考虑权限)
* 校区-宿舍楼二级结构,隐藏宿舍区层级
*
* @param bo 查询参数
* @return 简化树形结构数据
*/
@Override
public List<DormBuildingTreeSimplifiedVo> getAllSimplifiedDormBuildingTree(DormBuildingTreeBo bo) {
// 先获取完整的树形结构数据(不考虑权限)
List<DormBuildingTreeVo> fullTreeData = getAllDormBuildingTree(bo);
// 转换为简化版树形结构
return fullTreeData.stream()
.map(DormBuildingTreeSimplifiedVo::fromFullTreeVo)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
@@ -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<Long> ids, Boolean isValid) {
if (isValid) {
// TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteByIds(ids) > 0;
try {
// 逐个处理每个卫生检查记录的级联删除
for (Long checkId : ids) {
log.info("开始级联删除宿舍卫生检查记录,检查ID: {}", checkId);
// 1. 查询并删除关联的违纪记录
List<XgHygieneCheckViolationVo> 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<String, Object> 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<String, Object> getStudentDetailInfo(Long studentId) {
try {
var studentInfo = xgStudentMsgService.queryById(studentId);
if (studentInfo != null) {
Map<String, Object> 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<XgDormHygieneStudentScore> 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<XgViolationDataBo> newViolationData) {
try {
// 1. 查询数据库中现有的违纪记录
List<XgHygieneCheckViolationVo> existingViolations = getViolationsByHygieneCheckId(hygieneCheckId);
log.info("数据库中现有违纪记录数量: {}", existingViolations.size());
// 2. 收集现有记录的违纪记录ID(disciplineRecordId)
Set<Long> existingDisciplineIds = existingViolations.stream()
.map(XgHygieneCheckViolationVo::getDisciplineRecordId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
log.info("现有违纪记录ID集合: {}", existingDisciplineIds);
// 3. 收集新数据中的违纪记录ID(优先使用disciplineRecordId字段,兼容旧版本的id字段)
Set<Long> 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<Long> 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<XgHygieneCheckViolation> 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<XgHygieneCheckViolationVo> getViolationsByHygieneCheckId(Long hygieneCheckId) {
if (hygieneCheckId == null) {
log.warn("卫生检查ID为空,无法查询违纪记录");
return List.of();
}
try {
// 使用自定义查询方法,关联查询违纪详情
List<XgHygieneCheckViolationVo> 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();
}
}
}
@@ -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<XgDormRoom> buildQueryWrapper(XgDormRoomBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<XgDormRoom> 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<String, Object> 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<String, Object> accommodationData = objectMapper.readValue(accommodationDataJson, Map.class);
@SuppressWarnings("unchecked")
Map<String, Object> buildingInfo = (Map<String, Object>) accommodationData.get("buildingInfo");
@SuppressWarnings("unchecked")
List<Map<String, Object>> floorDataList = (List<Map<String, Object>>) 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<String, Object> floorData : floorDataList) {
Integer floorNum = (Integer) floorData.get("floorNum");
@SuppressWarnings("unchecked")
List<Map<String, Object>> rooms = (List<Map<String, Object>>) 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<Map<String, Object>> 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<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> beds = (List<Map<String, Object>>) 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<String, Object> room = roomGroup.get(roomIndex);
@SuppressWarnings("unchecked")
List<Map<String, Object>> beds = (List<Map<String, Object>>) room.get("beds");
if (beds != null && bedIndex < beds.size()) {
Map<String, Object> 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;
}
}
@@ -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<Long> 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<StudentOptionVo> result = baseMapper.selectStudentOptions(pageQuery.build(), bo);
return TableDataInfo.build(result);
}
/**
* 查询单人留宿有效数据
* 获取所有有效的假期留宿数据,筛选出在同一时间段内该宿舍只有一个学生留宿的记录
*
* @return 单人留宿有效数据列表
*/
@Override
public List<XgHolidayStayVo> querySinglePersonValidStay() {
// 1. 获取所有有效的假期留宿数据
XgHolidayStayBo bo = new XgHolidayStayBo();
Map<String, Object> 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<XgHolidayStayVo> validStayList = queryList(bo);
log.info("查询到有效的假期留宿数据总数: {}", validStayList.size());
if (validStayList.isEmpty()) {
return new ArrayList<>();
}
// 2. 按房间分组,筛选出单人留宿的数据
List<XgHolidayStayVo> singlePersonStayList = new ArrayList<>();
// 按房间ID分组
Map<Long, List<XgHolidayStayVo>> roomGroupMap = validStayList.stream()
.filter(stay -> stay.getRoomId() != null)
.collect(Collectors.groupingBy(XgHolidayStayVo::getRoomId));
log.info("按房间分组后,涉及房间数量: {}", roomGroupMap.size());
// 遍历每个房间的留宿数据
for (Map.Entry<Long, List<XgHolidayStayVo>> entry : roomGroupMap.entrySet()) {
Long roomId = entry.getKey();
List<XgHolidayStayVo> roomStayList = entry.getValue();
log.debug("处理房间ID: {}, 该房间留宿人数: {}", roomId, roomStayList.size());
// 3. 对于每个房间,检查是否存在单人留宿的时间段
List<XgHolidayStayVo> 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<XgHolidayStayVo> findSinglePersonPeriodsInRoom(List<XgHolidayStayVo> roomStayList) {
List<XgHolidayStayVo> 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<Long, List<XgHolidayStayVo>> studentStaysMap = roomStayList.stream()
.collect(Collectors.groupingBy(XgHolidayStayVo::getStudentId));
// 为每个学生创建时间段
Map<Long, List<TimeSegment>> studentTimeSegments = new HashMap<>();
for (Map.Entry<Long, List<XgHolidayStayVo>> entry : studentStaysMap.entrySet()) {
Long studentId = entry.getKey();
List<XgHolidayStayVo> stays = entry.getValue();
List<TimeSegment> 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<TimeSegment> mergedSegments = mergeTimeSegments(segments);
studentTimeSegments.put(studentId, mergedSegments);
}
// 收集所有关键时间点
Set<Date> keyTimePoints = new TreeSet<>();
for (List<TimeSegment> segments : studentTimeSegments.values()) {
for (TimeSegment segment : segments) {
keyTimePoints.add(segment.getStartTime());
keyTimePoints.add(segment.getEndTime());
}
}
List<Date> 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<Long> studentsInSegment = new ArrayList<>();
Map<Long, XgHolidayStayVo> studentStayMap = new HashMap<>();
for (Map.Entry<Long, List<TimeSegment>> entry : studentTimeSegments.entrySet()) {
Long studentId = entry.getKey();
List<TimeSegment> 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<String> getDatesBetween(Date startDate, Date endDate, SimpleDateFormat dateFormat) {
List<String> 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<String> 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<TimeSegment> mergeTimeSegments(List<TimeSegment> segments) {
if (segments.isEmpty()) {
return new ArrayList<>();
}
// 按开始时间排序
segments.sort(Comparator.comparing(TimeSegment::getStartTime));
List<TimeSegment> 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<XgHolidayStayVo> mergeConsecutiveSinglePersonStays(List<XgHolidayStayVo> singlePersonStays) {
if (singlePersonStays.isEmpty()) {
return new ArrayList<>();
}
// 按学生ID和开始时间分组排序
Map<Long, List<XgHolidayStayVo>> studentStaysMap = singlePersonStays.stream()
.collect(Collectors.groupingBy(XgHolidayStayVo::getStudentId));
List<XgHolidayStayVo> mergedStays = new ArrayList<>();
for (Map.Entry<Long, List<XgHolidayStayVo>> entry : studentStaysMap.entrySet()) {
List<XgHolidayStayVo> 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;
}
}
}
@@ -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<XgHygieneCheckViolationVo> queryPageList(XgHygieneCheckViolationBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<XgHygieneCheckViolation> lqw = buildQueryWrapper(bo);
Page<XgHygieneCheckViolationVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询符合条件的宿舍卫生检查违纪记录列表
*
* @param bo 查询条件
* @return 宿舍卫生检查违纪记录列表
*/
@Override
public List<XgHygieneCheckViolationVo> queryList(XgHygieneCheckViolationBo bo) {
LambdaQueryWrapper<XgHygieneCheckViolation> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<XgHygieneCheckViolation> buildQueryWrapper(XgHygieneCheckViolationBo bo) {
LambdaQueryWrapper<XgHygieneCheckViolation> 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<Long> ids, Boolean isValid) {
if (isValid) {
// TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 批量新增宿舍卫生检查违纪记录
*
* @param bos 违纪记录列表
* @return 是否新增成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean insertBatch(List<XgHygieneCheckViolationBo> bos) {
if (bos == null || bos.isEmpty()) {
return true;
}
List<XgHygieneCheckViolation> entities = MapstructUtils.convert(bos, XgHygieneCheckViolation.class);
for (XgHygieneCheckViolation entity : entities) {
validEntityBeforeSave(entity);
}
return baseMapper.insertBatch(entities);
}
/**
* 根据卫生检查ID查询违纪记录列表
*
* @param hygieneCheckId 卫生检查ID
* @return 违纪记录列表
*/
@Override
public List<XgHygieneCheckViolationVo> queryByHygieneCheckId(Long hygieneCheckId) {
LambdaQueryWrapper<XgHygieneCheckViolation> lqw = Wrappers.lambdaQuery();
lqw.eq(XgHygieneCheckViolation::getHygieneCheckId, hygieneCheckId);
return baseMapper.selectVoList(lqw);
}
/**
* 根据卫生检查ID删除违纪关联记录
*
* @param hygieneCheckId 卫生检查ID
* @return 是否删除成功
*/
@Override
public Boolean deleteByHygieneCheckId(Long hygieneCheckId) {
LambdaQueryWrapper<XgHygieneCheckViolation> lqw = Wrappers.lambdaQuery();
lqw.eq(XgHygieneCheckViolation::getHygieneCheckId, hygieneCheckId);
return baseMapper.delete(lqw) > 0;
}
}
@@ -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<Long, XgStudentLeaveVo> queryStudentLeavesByCheckDate(List<Long> studentIds, LocalDateTime checkDate) {
if (studentIds == null || studentIds.isEmpty() || checkDate == null) {
return Map.of();
}
LambdaQueryWrapper<XgStudentLeave> 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<XgStudentLeave> leaveList = baseMapper.selectList(lqw);
// 转换为VO并按学生ID分组,每个学生只取最新的一条请假记录
return leaveList.stream()
.collect(Collectors.toMap(
XgStudentLeave::getStudentId,
leave -> MapstructUtils.convert(leave, XgStudentLeaveVo.class),
(existing, replacement) -> existing // 保留第一条(最新的)
));
}
}
@@ -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<XgStudentStatisticsVo.StatisticsDetailData> 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<XgCascadeOptionVo> getColleges() {
return statisticsMapper.getColleges();
}
@Override
public List<XgCascadeOptionVo> getMajorsByCollegeId(Long collegeId) {
return statisticsMapper.getMajorsByCollegeId(collegeId);
}
@Override
public List<XgCascadeOptionVo> 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<XgStudentStatisticsVo.StatisticsChartData> convertDetailToChartData(
List<XgStudentStatisticsVo.StatisticsDetailData> 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());
}
}
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.xg.mapper.XgHygieneCheckViolationMapper">
<resultMap type="org.dromara.xg.domain.vo.XgHygieneCheckViolationVo" id="XgHygieneCheckViolationResult">
<result property="id" column="id" />
<result property="hygieneCheckId" column="hygiene_check_id" />
<result property="disciplineRecordId" column="discipline_record_id" />
<result property="studentId" column="student_id" />
<result property="studentNo" column="student_no" />
<result property="studentName" column="student_name" />
<result property="disciplineTypeId" column="discipline_type_id" />
<result property="disciplineTypeName" column="discipline_type_name" />
<result property="violationSource" column="violation_source" />
<result property="status" column="status" />
<!-- 违纪详情字段 -->
<result property="disciplineDate" column="discipline_date" />
<result property="disciplineReason" column="discipline_reason" />
<result property="disciplineContent" column="discipline_content" />
<result property="scoreDeduction" column="score_deduction" />
<result property="attachmentOssIds" column="attachment_oss_ids" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectXgHygieneCheckViolationVo">
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
</sql>
<select id="selectVoList" parameterType="com.baomidou.mybatisplus.core.conditions.Wrapper" resultMap="XgHygieneCheckViolationResult">
<include refid="selectXgHygieneCheckViolationVo"/>
<where>
hcv.deleted = 0
${ew.customSqlSegment}
</where>
ORDER BY hcv.create_time DESC
</select>
<select id="selectVoById" parameterType="Long" resultMap="XgHygieneCheckViolationResult">
<include refid="selectXgHygieneCheckViolationVo"/>
WHERE hcv.id = #{id} AND hcv.deleted = 0
</select>
<!-- 根据卫生检查ID查询违纪记录(包含详情) -->
<select id="selectViolationsByHygieneCheckId" parameterType="Long" resultMap="XgHygieneCheckViolationResult">
<include refid="selectXgHygieneCheckViolationVo"/>
WHERE hcv.hygiene_check_id = #{hygieneCheckId}
AND hcv.deleted = 0
ORDER BY hcv.create_time DESC
</select>
</mapper>
@@ -0,0 +1,504 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.xg.mapper.XgStudentStatisticsMapper">
<!-- 按学院维度统计图表数据 -->
<select id="getCollegeStatistics" resultType="org.dromara.xg.domain.vo.XgStudentStatisticsVo$StatisticsChartData">
SELECT
d.dept_name as name,
COALESCE(dorm_stats.dormDeductionCount, 0) as dormDeductionCount,
COALESCE(dorm_violation_stats.dormViolationCount, 0) as dormViolationCount,
COALESCE(dorm_violation_stats.dormViolationDeduction, 0) as dormViolationDeduction,
COALESCE(teaching_violation_stats.teachingViolationCount, 0) as teachingViolationCount,
COALESCE(teaching_violation_stats.teachingViolationDeduction, 0) as teachingViolationDeduction
FROM sys_dept d
LEFT JOIN (
-- 宿舍卫生检查扣分统计
SELECT
c.college_id,
COUNT(h.id) as dormDeductionCount
FROM xg_dorm_hygiene_check h
INNER JOIN xg_class_msg c ON h.room_id IN (
SELECT DISTINCT bs.room_id
FROM xg_dorm_bed_student bs
INNER JOIN xg_student_msg s ON bs.student_id = s.id
WHERE s.class_id = c.id AND s.deleted = 0
)
WHERE h.deleted = 0
AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
AND h.total_score > 0
GROUP BY c.college_id
) dorm_stats ON d.dept_id = dorm_stats.college_id
LEFT JOIN (
-- 宿舍区违纪记录统计
SELECT
dr.college_id,
COUNT(dr.id) as dormViolationCount,
COALESCE(SUM(dr.score_deduction), 0) as dormViolationDeduction
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '1'
AND dr.status = '1'
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
GROUP BY dr.college_id
) dorm_violation_stats ON d.dept_id = dorm_violation_stats.college_id
LEFT JOIN (
-- 教学区违纪记录统计
SELECT
dr.college_id,
COUNT(dr.id) as teachingViolationCount,
COALESCE(SUM(dr.score_deduction), 0) as teachingViolationDeduction
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '2'
AND dr.status = '1'
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
GROUP BY dr.college_id
) teaching_violation_stats ON d.dept_id = teaching_violation_stats.college_id
WHERE d.parent_id = 101 AND d.del_flag = '0'
ORDER BY d.order_num ASC, d.dept_name ASC
</select>
<!-- 按学院维度统计明细数据 -->
<select id="getCollegeDetailStatistics" resultType="org.dromara.xg.domain.vo.XgStudentStatisticsVo$StatisticsDetailData">
SELECT
d.dept_id as id,
d.dept_name as name,
'' as parentName,
COALESCE(dorm_stats.dormDeductionCount, 0) as dormDeductionCount,
COALESCE(dorm_violation_stats.dormViolationCount, 0) as dormViolationCount,
COALESCE(dorm_violation_stats.dormViolationDeduction, 0) as dormViolationDeduction,
COALESCE(teaching_violation_stats.teachingViolationCount, 0) as teachingViolationCount,
COALESCE(teaching_violation_stats.teachingViolationDeduction, 0) as teachingViolationDeduction,
COALESCE(dorm_violation_stats.dormViolationDeduction, 0) + COALESCE(teaching_violation_stats.teachingViolationDeduction, 0) as totalDeduction
FROM sys_dept d
LEFT JOIN (
-- 宿舍卫生检查扣分统计
SELECT
c.college_id,
COUNT(h.id) as dormDeductionCount
FROM xg_dorm_hygiene_check h
INNER JOIN xg_class_msg c ON h.room_id IN (
SELECT DISTINCT bs.room_id
FROM xg_dorm_bed_student bs
INNER JOIN xg_student_msg s ON bs.student_id = s.id
WHERE s.class_id = c.id AND s.deleted = 0
)
WHERE h.deleted = 0
AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
AND h.total_score > 0
GROUP BY c.college_id
) dorm_stats ON d.dept_id = dorm_stats.college_id
LEFT JOIN (
-- 宿舍区违纪记录统计
SELECT
dr.college_id,
COUNT(dr.id) as dormViolationCount,
COALESCE(SUM(dr.score_deduction), 0) as dormViolationDeduction
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '1'
AND dr.status = '1'
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
GROUP BY dr.college_id
) dorm_violation_stats ON d.dept_id = dorm_violation_stats.college_id
LEFT JOIN (
-- 教学区违纪记录统计
SELECT
dr.college_id,
COUNT(dr.id) as teachingViolationCount,
COALESCE(SUM(dr.score_deduction), 0) as teachingViolationDeduction
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '2'
AND dr.status = '1'
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
GROUP BY dr.college_id
) teaching_violation_stats ON d.dept_id = teaching_violation_stats.college_id
WHERE d.parent_id = 101 AND d.del_flag = '0'
ORDER BY d.order_num ASC, d.dept_name ASC
</select>
<!-- 按专业维度统计图表数据 -->
<select id="getMajorStatistics" resultType="org.dromara.xg.domain.vo.XgStudentStatisticsVo$StatisticsChartData">
SELECT
d.dept_name as name,
COALESCE(dorm_stats.dormDeductionCount, 0) as dormDeductionCount,
COALESCE(dorm_violation_stats.dormViolationCount, 0) as dormViolationCount,
COALESCE(dorm_violation_stats.dormViolationDeduction, 0) as dormViolationDeduction,
COALESCE(teaching_violation_stats.teachingViolationCount, 0) as teachingViolationCount,
COALESCE(teaching_violation_stats.teachingViolationDeduction, 0) as teachingViolationDeduction
FROM sys_dept d
LEFT JOIN (
-- 宿舍卫生检查扣分统计
SELECT
c.major_id,
COUNT(h.id) as dormDeductionCount
FROM xg_dorm_hygiene_check h
INNER JOIN xg_class_msg c ON h.room_id IN (
SELECT DISTINCT bs.room_id
FROM xg_dorm_bed_student bs
INNER JOIN xg_student_msg s ON bs.student_id = s.id
WHERE s.class_id = c.id AND s.deleted = 0
)
WHERE h.deleted = 0
AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
AND h.total_score > 0
AND c.college_id = #{bo.collegeId}
GROUP BY c.major_id
) dorm_stats ON d.dept_id = dorm_stats.major_id
LEFT JOIN (
-- 宿舍区违纪记录统计
SELECT
dr.major_id,
COUNT(dr.id) as dormViolationCount,
COALESCE(SUM(dr.score_deduction), 0) as dormViolationDeduction
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '1'
AND dr.status = '1'
AND dr.college_id = #{bo.collegeId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
GROUP BY dr.major_id
) dorm_violation_stats ON d.dept_id = dorm_violation_stats.major_id
LEFT JOIN (
-- 教学区违纪记录统计
SELECT
dr.major_id,
COUNT(dr.id) as teachingViolationCount,
COALESCE(SUM(dr.score_deduction), 0) as teachingViolationDeduction
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '2'
AND dr.status = '1'
AND dr.college_id = #{bo.collegeId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
GROUP BY dr.major_id
) teaching_violation_stats ON d.dept_id = teaching_violation_stats.major_id
WHERE d.parent_id = #{bo.collegeId} AND d.dept_category LIKE 'zy-%' AND d.del_flag = '0'
ORDER BY d.order_num ASC, d.dept_name ASC
</select>
<!-- 按专业维度统计明细数据 -->
<select id="getMajorDetailStatistics" resultType="org.dromara.xg.domain.vo.XgStudentStatisticsVo$StatisticsDetailData">
SELECT
d.dept_id as id,
d.dept_name as name,
p.dept_name as parentName,
COALESCE(dorm_stats.dormDeductionCount, 0) as dormDeductionCount,
COALESCE(dorm_violation_stats.dormViolationCount, 0) as dormViolationCount,
COALESCE(dorm_violation_stats.dormViolationDeduction, 0) as dormViolationDeduction,
COALESCE(teaching_violation_stats.teachingViolationCount, 0) as teachingViolationCount,
COALESCE(teaching_violation_stats.teachingViolationDeduction, 0) as teachingViolationDeduction,
COALESCE(dorm_violation_stats.dormViolationDeduction, 0) + COALESCE(teaching_violation_stats.teachingViolationDeduction, 0) as totalDeduction
FROM sys_dept d
INNER JOIN sys_dept p ON d.parent_id = p.dept_id
LEFT JOIN (
-- 宿舍卫生检查扣分统计
SELECT
c.major_id,
COUNT(h.id) as dormDeductionCount
FROM xg_dorm_hygiene_check h
INNER JOIN xg_class_msg c ON h.room_id IN (
SELECT DISTINCT bs.room_id
FROM xg_dorm_bed_student bs
INNER JOIN xg_student_msg s ON bs.student_id = s.id
WHERE s.class_id = c.id AND s.deleted = 0
)
WHERE h.deleted = 0
AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
AND h.total_score > 0
AND c.college_id = #{bo.collegeId}
GROUP BY c.major_id
) dorm_stats ON d.dept_id = dorm_stats.major_id
LEFT JOIN (
-- 宿舍区违纪记录统计
SELECT
dr.major_id,
COUNT(dr.id) as dormViolationCount,
COALESCE(SUM(dr.score_deduction), 0) as dormViolationDeduction
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '1'
AND dr.status = '1'
AND dr.college_id = #{bo.collegeId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
GROUP BY dr.major_id
) dorm_violation_stats ON d.dept_id = dorm_violation_stats.major_id
LEFT JOIN (
-- 教学区违纪记录统计
SELECT
dr.major_id,
COUNT(dr.id) as teachingViolationCount,
COALESCE(SUM(dr.score_deduction), 0) as teachingViolationDeduction
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '2'
AND dr.status = '1'
AND dr.college_id = #{bo.collegeId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
GROUP BY dr.major_id
) teaching_violation_stats ON d.dept_id = teaching_violation_stats.major_id
WHERE d.parent_id = #{bo.collegeId} AND d.dept_category LIKE 'zy-%' AND d.del_flag = '0'
ORDER BY d.order_num ASC, d.dept_name ASC
</select>
<!-- 按班级维度统计图表数据 -->
<select id="getClassStatistics" resultType="org.dromara.xg.domain.vo.XgStudentStatisticsVo$StatisticsChartData">
SELECT
c.class_name as name,
(SELECT COUNT(h.id)
FROM xg_dorm_hygiene_check h
WHERE h.deleted = 0
AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
AND h.total_score > 0
AND h.room_id IN (
SELECT DISTINCT bs.room_id
FROM xg_dorm_bed_student bs
INNER JOIN xg_student_msg s ON bs.student_id = s.id
WHERE s.class_id = c.id AND s.deleted = 0
)
) as dormDeductionCount,
(SELECT COUNT(dr.id)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '1'
AND dr.status = '1'
AND dr.class_id = c.id
AND dr.college_id = #{bo.collegeId}
AND dr.major_id = #{bo.majorId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) as dormViolationCount,
(SELECT COALESCE(SUM(dr.score_deduction), 0)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '1'
AND dr.status = '1'
AND dr.class_id = c.id
AND dr.college_id = #{bo.collegeId}
AND dr.major_id = #{bo.majorId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) as dormViolationDeduction,
(SELECT COUNT(dr.id)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '2'
AND dr.status = '1'
AND dr.class_id = c.id
AND dr.college_id = #{bo.collegeId}
AND dr.major_id = #{bo.majorId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) as teachingViolationCount,
(SELECT COALESCE(SUM(dr.score_deduction), 0)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '2'
AND dr.status = '1'
AND dr.class_id = c.id
AND dr.college_id = #{bo.collegeId}
AND dr.major_id = #{bo.majorId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) as teachingViolationDeduction
FROM xg_class_msg c
WHERE c.college_id = #{bo.collegeId} AND c.major_id = #{bo.majorId} AND c.deleted = 0
ORDER BY c.class_name ASC
</select>
<!-- 按班级维度统计明细数据 -->
<select id="getClassDetailStatistics" resultType="org.dromara.xg.domain.vo.XgStudentStatisticsVo$StatisticsDetailData">
<choose>
<when test="bo.classId != null">
<!-- 班级详情统计 -->
SELECT
c.id as id,
c.class_name as name,
CONCAT(c.college_name, '-', c.major_name) as parentName,
(SELECT COUNT(h.id)
FROM xg_dorm_hygiene_check h
WHERE h.deleted = 0
AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
AND h.total_score > 0
AND h.room_id IN (
SELECT DISTINCT bs.room_id
FROM xg_dorm_bed_student bs
INNER JOIN xg_student_msg s ON bs.student_id = s.id
WHERE s.class_id = c.id AND s.deleted = 0
)
) as dormDeductionCount,
(SELECT COUNT(dr.id)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '1'
AND dr.status = '1'
AND dr.class_id = #{bo.classId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) as dormViolationCount,
(SELECT COALESCE(SUM(dr.score_deduction), 0)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '1'
AND dr.status = '1'
AND dr.class_id = #{bo.classId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) as dormViolationDeduction,
(SELECT COUNT(dr.id)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '2'
AND dr.status = '1'
AND dr.class_id = #{bo.classId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) as teachingViolationCount,
(SELECT COALESCE(SUM(dr.score_deduction), 0)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '2'
AND dr.status = '1'
AND dr.class_id = #{bo.classId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) as teachingViolationDeduction,
((SELECT COALESCE(SUM(dr.score_deduction), 0)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '1'
AND dr.status = '1'
AND dr.class_id = #{bo.classId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) +
(SELECT COALESCE(SUM(dr.score_deduction), 0)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '2'
AND dr.status = '1'
AND dr.class_id = #{bo.classId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
)) as totalDeduction
FROM xg_class_msg c
WHERE c.id = #{bo.classId} AND c.deleted = 0
</when>
<otherwise>
<!-- 班级列表统计 -->
SELECT
c.id as id,
c.class_name as name,
c.major_name as parentName,
(SELECT COUNT(h.id)
FROM xg_dorm_hygiene_check h
WHERE h.deleted = 0
AND h.check_date BETWEEN #{bo.startTime} AND #{bo.endTime}
AND h.total_score > 0
AND h.room_id IN (
SELECT DISTINCT bs.room_id
FROM xg_dorm_bed_student bs
INNER JOIN xg_student_msg s ON bs.student_id = s.id
WHERE s.class_id = c.id AND s.deleted = 0
)
) as dormDeductionCount,
(SELECT COUNT(dr.id)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '1'
AND dr.status = '1'
AND dr.class_id = c.id
AND dr.college_id = #{bo.collegeId}
AND dr.major_id = #{bo.majorId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) as dormViolationCount,
(SELECT COALESCE(SUM(dr.score_deduction), 0)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '1'
AND dr.status = '1'
AND dr.class_id = c.id
AND dr.college_id = #{bo.collegeId}
AND dr.major_id = #{bo.majorId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) as dormViolationDeduction,
(SELECT COUNT(dr.id)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '2'
AND dr.status = '1'
AND dr.class_id = c.id
AND dr.college_id = #{bo.collegeId}
AND dr.major_id = #{bo.majorId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) as teachingViolationCount,
(SELECT COALESCE(SUM(dr.score_deduction), 0)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '2'
AND dr.status = '1'
AND dr.class_id = c.id
AND dr.college_id = #{bo.collegeId}
AND dr.major_id = #{bo.majorId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) as teachingViolationDeduction,
((SELECT COALESCE(SUM(dr.score_deduction), 0)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '1'
AND dr.status = '1'
AND dr.class_id = c.id
AND dr.college_id = #{bo.collegeId}
AND dr.major_id = #{bo.majorId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
) +
(SELECT COALESCE(SUM(dr.score_deduction), 0)
FROM xskh_discipline_record dr
WHERE dr.deleted = 0
AND dr.violation_source = '2'
AND dr.status = '1'
AND dr.class_id = c.id
AND dr.college_id = #{bo.collegeId}
AND dr.major_id = #{bo.majorId}
AND dr.discipline_date BETWEEN #{bo.startTime} AND #{bo.endTime}
)) as totalDeduction
FROM xg_class_msg c
WHERE c.college_id = #{bo.collegeId} AND c.major_id = #{bo.majorId} AND c.deleted = 0
ORDER BY c.class_name ASC
</otherwise>
</choose>
</select>
<!-- 获取二级学院列表 -->
<select id="getColleges" resultType="org.dromara.xg.domain.vo.XgCascadeOptionVo">
SELECT
dept_id as id,
dept_name as label,
CAST(dept_id AS CHAR) as value,
order_num as sort
FROM sys_dept
WHERE parent_id = 101 AND del_flag = '0'
ORDER BY order_num ASC, dept_name ASC
</select>
<!-- 根据学院ID获取专业列表 -->
<select id="getMajorsByCollegeId" resultType="org.dromara.xg.domain.vo.XgCascadeOptionVo">
SELECT
dept_id as id,
dept_name as label,
CAST(dept_id AS CHAR) as value,
order_num as sort
FROM sys_dept
WHERE parent_id = #{collegeId}
AND dept_category LIKE 'zy-%'
AND del_flag = '0'
ORDER BY order_num ASC, dept_name ASC
</select>
<!-- 根据专业ID获取班级列表 -->
<select id="getClassesByMajorId" resultType="org.dromara.xg.domain.vo.XgCascadeOptionVo">
SELECT
id,
class_name as label,
CAST(id AS CHAR) as value,
0 as sort
FROM xg_class_msg
WHERE major_id = #{majorId} AND deleted = 0
ORDER BY class_name ASC
</select>
</mapper>
@@ -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<Void> 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<XskhDisciplineRevokeVo> getClassTeacherReviewedRevokeApplications(XskhDisciplineRevokeBo bo,
PageQuery pageQuery) {
return xskhDisciplineRevokeService.queryClassTeacherReviewedPageList(bo, pageQuery);
}
/**
* 班主任审核撤销申请
*/
@SaCheckPermission("disciplineRevoke:classTeacher:approve")
@Log(title = "班主任审核撤销申请", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/classTeacherApprove")
public R<Void> classTeacherApproveRevoke(@Validated(EditGroup.class) @RequestBody XskhDisciplineRevokeBo bo) {
return toAjax(xskhDisciplineRevokeService.classTeacherApproveRevoke(bo));
@PutMapping("/teacherApprove")
public R<Void> teacherApproveRevoke(@Validated(EditGroup.class) @RequestBody XskhDisciplineRevokeBo bo) {
return toAjax(xskhDisciplineRevokeService.teacherApproveRevoke(bo));
}
/**
* 查询学工处待审核的撤销申请列表(学工处专用)
*/
@SaCheckPermission("disciplineRevoke:studentAffairs:list")
@GetMapping("/studentAffairsList")
public TableDataInfo<XskhDisciplineRevokeVo> getStudentAffairsRevokeApplications(XskhDisciplineRevokeBo bo,
PageQuery pageQuery) {
return xskhDisciplineRevokeService.queryStudentAffairsPageList(bo, pageQuery);
}
/**
* 学工处审核撤销申请
*/
@SaCheckPermission("disciplineRevoke:studentAffairs:approve")
@Log(title = "学工处审核撤销申请", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/studentAffairsApprove")
public R<Void> studentAffairsApproveRevoke(@Validated(EditGroup.class) @RequestBody XskhDisciplineRevokeBo bo) {
return toAjax(xskhDisciplineRevokeService.studentAffairsApproveRevoke(bo));
}
/**
* 查询学工处已审核的撤销申请列表(学工处专用)
*/
@SaCheckPermission("disciplineRevoke:studentAffairs:list")
@GetMapping("/studentAffairsReviewedList")
public TableDataInfo<XskhDisciplineRevokeVo> getStudentAffairsReviewedRevokeApplications(XskhDisciplineRevokeBo bo,
PageQuery pageQuery) {
return xskhDisciplineRevokeService.queryStudentAffairsReviewedPageList(bo, pageQuery);
}
}
@@ -63,8 +63,7 @@ public class XskhDisciplineTypeController extends BaseController {
*/
@SaCheckPermission("disciplineType:disciplineType:query")
@GetMapping("/{id}")
public R<XskhDisciplineTypeVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
public R<XskhDisciplineTypeVo> 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<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
public R<Void> 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<List<XskhDisciplineTypeVo>> listByAreaType(@RequestParam String areaType,
@RequestParam(required = false) String status) {
XskhDisciplineTypeBo bo = new XskhDisciplineTypeBo();
bo.setAreaType(areaType);
if (status != null) {
bo.setStatus(status);
}
List<XskhDisciplineTypeVo> list = xskhDisciplineTypeService.queryList(bo);
return R.ok(list);
}
}
@@ -110,6 +110,11 @@ public class XskhDisciplineRecord extends TenantEntity {
*/
private String status;
/**
* 违纪来源(1-宿舍卫生检查 2-教学区违纪)
*/
private String violationSource;
/**
* 附件OSS ID列表,逗号分隔
*/
@@ -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;
@@ -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;
}
@@ -42,6 +42,16 @@ public class XskhRewardType extends TenantEntity {
*/
private String description;
/**
* 是否均化(0-否 1-是)
*/
private String isEqualized;
/**
* 均化阈值
*/
private java.math.BigDecimal equalizedThreshold;
/**
* 状态(0-正常 1-停用)
*/
@@ -128,6 +128,11 @@ public class XskhDisciplineRecordBo extends BaseEntity {
*/
private String status;
/**
* 违纪来源(1-宿舍卫生检查 2-教学区违纪)
*/
private String violationSource;
/**
* 附件OSS ID列表,逗号分隔
*/
@@ -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; // 更新时间
}
@@ -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;
}
@@ -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-停用)
*/
@@ -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;
/**
@@ -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;
/**
@@ -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;
}
@@ -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;
}
@@ -19,46 +19,54 @@ import java.util.Map;
*/
public interface XskhDisciplineRecordMapper extends BaseMapperPlus<XskhDisciplineRecord, XskhDisciplineRecordVo> {
/**
* 查询学生选项列表(用于下拉框)
*
* @param keyword 搜索关键词(学号或姓名)
* @return 学生选项列表
*/
List<XskhStudentOptionVo> selectStudentOptions(String keyword);
/**
* 查询学生选项列表(用于下拉框)
*
* @param keyword 搜索关键词(学号或姓名)
* @return 学生选项列表
*/
List<XskhStudentOptionVo> selectStudentOptions(String keyword);
/**
* 查询班主任班级的违纪处分记录列表(班主任专用)
*
* @param page 分页参数
* @param bo 查询条件
* @param classTeacherId 班主任ID
* @return 分页查询结果
*/
Page<XskhDisciplineRecordVo> selectClassTeacherVoPage(@Param("page") Page<XskhDisciplineRecordVo> page,
@Param("bo") XskhDisciplineRecordBo bo,
@Param("classTeacherId") Long classTeacherId);
/**
* 查询班主任班级的违纪处分记录列表(班主任专用)
*
* @param page 分页参数
* @param bo 查询条件
* @param classTeacherId 班主任ID
* @return 分页查询结果
*/
Page<XskhDisciplineRecordVo> selectClassTeacherVoPage(@Param("page") Page<XskhDisciplineRecordVo> page,
@Param("bo") XskhDisciplineRecordBo bo,
@Param("classTeacherId") Long classTeacherId);
/**
* 获取指标关联的违纪处分类型和奖励类型
*
* @param indicatorId 指标ID
* @return 包含disciplineTypeIds和rewardTypeIds的Map
*/
Map<String, String> selectIndicatorTypes(@Param("indicatorId") Long indicatorId);
/**
* 获取指标关联的违纪处分类型和奖励类型
*
* @param indicatorId 指标ID
* @return 包含disciplineTypeIds和rewardTypeIds的Map
*/
Map<String, String> selectIndicatorTypes(@Param("indicatorId") Long indicatorId);
/**
* 获取学生在指定时间范围内的违纪处分记录
*
* @param studentId 学生ID
* @param disciplineTypeIds 违纪处分类型ID列表
* @param startTime 开始时间
* @param endTime 结束时间
* @return 违纪处分记录列表
*/
List<XskhDisciplineRecordVo> selectStudentDisciplineRecords(@Param("studentId") Long studentId,
@Param("disciplineTypeIds") List<Long> disciplineTypeIds,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 获取学生在指定时间范围内的违纪处分记录
*
* @param studentId 学生ID
* @param disciplineTypeIds 违纪处分类型ID列表
* @param startTime 开始时间
* @param endTime 结束时间
* @return 违纪处分记录列表
*/
List<XskhDisciplineRecordVo> selectStudentDisciplineRecords(@Param("studentId") Long studentId,
@Param("disciplineTypeIds") List<Long> disciplineTypeIds,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/**
* 根据违纪记录ID逻辑删除宿舍卫生检查违纪关联记录
*
* @param disciplineRecordId 违纪记录ID
* @return 删除的记录数量
*/
int deleteHygieneCheckViolationByDisciplineRecordId(@Param("disciplineRecordId") Long disciplineRecordId);
}
@@ -15,24 +15,58 @@ import org.dromara.xskh.domain.vo.XskhDisciplineRevokeVo;
*/
public interface XskhDisciplineRevokeMapper extends BaseMapperPlus<XskhDisciplineRevoke, XskhDisciplineRevokeVo> {
/**
* 根据违纪记录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<XskhDisciplineRevokeVo> selectClassTeacherVoPage(@Param("page") Page<XskhDisciplineRevokeVo> page,
@Param("bo") XskhDisciplineRevokeBo bo,
@Param("classTeacherId") Long classTeacherId);
/**
* 查询班主任班级的违纪撤销申请列表(班主任专用)
*
* @param page 分页参数
* @param bo 查询条件
* @param classTeacherId 班主任ID
* @return 分页查询结果
*/
Page<XskhDisciplineRevokeVo> selectClassTeacherVoPage(@Param("page") Page<XskhDisciplineRevokeVo> page,
@Param("bo") XskhDisciplineRevokeBo bo,
@Param("classTeacherId") Long classTeacherId);
/**
* 查询学工处待审核的违纪撤销申请列表(学工处专用)
*
* @param page 分页参数
* @param bo 查询条件
* @return 分页查询结果
*/
Page<XskhDisciplineRevokeVo> selectStudentAffairsVoPage(@Param("page") Page<XskhDisciplineRevokeVo> page,
@Param("bo") XskhDisciplineRevokeBo bo);
/**
* 查询学工处已审核的违纪撤销申请列表(学工处专用)
*
* @param page 分页参数
* @param bo 查询条件
* @return 分页查询结果
*/
Page<XskhDisciplineRevokeVo> selectStudentAffairsReviewedVoPage(
@Param("page") Page<XskhDisciplineRevokeVo> page,
@Param("bo") XskhDisciplineRevokeBo bo);
/**
* 查询班主任已审核的违纪撤销申请列表(根据审核人查询历史记录)
*
* @param page 分页参数
* @param bo 查询条件
* @param classTeacherId 班主任ID
* @return 分页查询结果
*/
Page<XskhDisciplineRevokeVo> selectClassTeacherReviewedVoPage(
@Param("page") Page<XskhDisciplineRevokeVo> page,
@Param("bo") XskhDisciplineRevokeBo bo,
@Param("classTeacherId") Long classTeacherId);
}
@@ -61,19 +61,36 @@ public interface IXskhDisciplineRevokeService {
*/
Boolean applyRevoke(XskhDisciplineRevokeBo bo);
/**
* 审核撤销申请
*/
Boolean approveRevoke(XskhDisciplineRevokeBo bo);
/**
* 查询班主任班级的违纪撤销申请列表(班主任专用)
*/
TableDataInfo<XskhDisciplineRevokeVo> queryClassTeacherPageList(XskhDisciplineRevokeBo bo, PageQuery pageQuery);
/**
* 查询班主任已审核的违纪撤销申请列表(根据审核人查询历史记录)
*/
TableDataInfo<XskhDisciplineRevokeVo> queryClassTeacherReviewedPageList(XskhDisciplineRevokeBo bo,
PageQuery pageQuery);
/**
* 班主任审核撤销申请
*/
Boolean classTeacherApproveRevoke(XskhDisciplineRevokeBo bo);
Boolean teacherApproveRevoke(XskhDisciplineRevokeBo bo);
/**
* 查询学工处待审核的撤销申请列表(学工处专用)
*/
TableDataInfo<XskhDisciplineRevokeVo> queryStudentAffairsPageList(XskhDisciplineRevokeBo bo, PageQuery pageQuery);
/**
* 学工处审核撤销申请
*/
Boolean studentAffairsApproveRevoke(XskhDisciplineRevokeBo bo);
/**
* 查询学工处已审核的撤销申请列表(学工处专用)
*/
TableDataInfo<XskhDisciplineRevokeVo> queryStudentAffairsReviewedPageList(XskhDisciplineRevokeBo bo,
PageQuery pageQuery);
}
Loaded 100 of 244 files, more files were not shown because too many files have changed in this diff. Show more