This commit is contained in:
zlwu committed 2026-01-19 13:52:00 +08:00
1 parent 8b6d1e5be8
commit 9e6cccf846
23 files changed
+2658 -67

No files matched your search

@@ -0,0 +1,111 @@
package org.dromara.xg.controller;
import java.util.List;
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.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
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.xg.domain.vo.XgConductScoreAuditVo;
import org.dromara.xg.domain.bo.XgConductScoreAuditQueryBo;
import org.dromara.xg.domain.bo.XgConductScoreAuditApproveBo;
import org.dromara.xg.domain.bo.XgConductScoreAuditRejectBo;
import org.dromara.xg.domain.bo.XgConductScoreAuditBatchBo;
import org.dromara.xg.service.IXgConductScoreAuditService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* 操行分审核
*
* @author Lion Li
* @date 2025-01-16
*/
@Validated
@RequiredArgsConstructor
@Slf4j
@RestController
@RequestMapping("/xg/conduct-score-audit")
public class XgConductScoreAuditController extends BaseController {
private final IXgConductScoreAuditService xgConductScoreAuditService;
/**
* 查询操行分审核列表
*/
// @SaCheckPermission("xg:conductScoreAudit:list")
@GetMapping("/list")
public TableDataInfo<XgConductScoreAuditVo> list(XgConductScoreAuditQueryBo bo, PageQuery pageQuery) {
return xgConductScoreAuditService.queryPageList(bo, pageQuery);
}
/**
* 获取操行分审核详细信息
*
* @param id 主键
*/
// @SaCheckPermission("xg:conductScoreAudit:query")
@GetMapping("/{id}")
public R<XgConductScoreAuditVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgConductScoreAuditService.queryById(id));
}
/**
* 审核通过
*/
// @SaCheckPermission("xg:conductScoreAudit:approve")
@Log(title = "操行分审核", businessType = BusinessType.UPDATE)
@PostMapping("/approve/{id}")
public R<Void> approve(@NotNull(message = "审核ID不能为空") @PathVariable Long id,
@Validated @RequestBody XgConductScoreAuditApproveBo bo) {
return toAjax(xgConductScoreAuditService.approveAudit(id, bo));
}
/**
* 审核驳回
*/
// @SaCheckPermission("xg:conductScoreAudit:reject")
@Log(title = "操行分审核", businessType = BusinessType.UPDATE)
@PostMapping("/reject/{id}")
public R<Void> reject(@NotNull(message = "审核ID不能为空") @PathVariable Long id,
@Validated @RequestBody XgConductScoreAuditRejectBo bo) {
return toAjax(xgConductScoreAuditService.rejectAudit(id, bo));
}
/**
* 批量审核通过
*/
// @SaCheckPermission("xg:conductScoreAudit:batchApprove")
@Log(title = "操行分审核", businessType = BusinessType.UPDATE)
@PostMapping("/batch-approve")
public R<Void> batchApprove(@Validated @RequestBody XgConductScoreAuditBatchBo bo) {
return toAjax(xgConductScoreAuditService.batchApproveAudit(bo));
}
/**
* 批量审核驳回
*/
// @SaCheckPermission("xg:conductScoreAudit:batchReject")
@Log(title = "操行分审核", businessType = BusinessType.UPDATE)
@PostMapping("/batch-reject")
public R<Void> batchReject(@Validated @RequestBody XgConductScoreAuditBatchBo bo) {
return toAjax(xgConductScoreAuditService.batchRejectAudit(bo));
}
/**
* 删除操行分审核
*/
// @SaCheckPermission("xg:conductScoreAudit:remove")
@Log(title = "操行分审核", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(xgConductScoreAuditService.deleteWithValidByIds(List.of(ids), true));
}
}
@@ -29,6 +29,7 @@ import org.dromara.xg.domain.vo.XgConductScoreSemesterAverageExportVo;
import org.dromara.xg.domain.bo.XgConductScoreBo;
import org.dromara.xg.domain.bo.XgConductScoreQueryBo;
import org.dromara.xg.domain.bo.XgConductScoreBatchBo;
import org.dromara.xg.domain.bo.XgConductScoreBatchUpdateBo;
import org.dromara.xg.domain.vo.XgConductScoreBatchResultVo;
import org.dromara.xg.domain.vo.XgConductScoreClassSummaryVo;
import org.dromara.xg.domain.vo.XgConductScoreSemesterAverageVo;
@@ -105,6 +106,17 @@ public class XgConductScoreController extends BaseController {
return R.ok(xgConductScoreService.batchInsertConductScores(bo));
}
/**
* 批量提交修改审核
*/
// @SaCheckPermission("xg:conductScore:batchUpdate")
@RepeatSubmit()
@Log(title = "学生操行分", businessType = BusinessType.UPDATE)
@PostMapping("/batch-update")
public R<XgConductScoreBatchResultVo> batchUpdate(@Validated @RequestBody XgConductScoreBatchUpdateBo bo) {
return R.ok(xgConductScoreService.batchUpdateConductScores(bo));
}
/**
* 查询学生操行分列表
*/
@@ -0,0 +1,89 @@
package org.dromara.xg.domain;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import java.math.BigDecimal;
import java.util.Date;
/**
* 操行分审核对象 xg_conduct_score_audit
*
* @author Lion Li
* @date 2025-01-16
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class XgConductScoreAudit extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 主键ID */
private Long id;
/** 租户ID */
private String tenantId;
/** 学生ID */
private Long studentId;
/** 学年 */
private Integer academicYear;
/** 学期:1-第一学期,2-第二学期 */
private Integer semester;
/** 月份 */
private Integer month;
/** 班级ID */
private Long classId;
/** 原分数 */
private BigDecimal oldScore;
/** 原备注 */
private String oldRemark;
/** 新分数 */
private BigDecimal newScore;
/** 新备注 */
private String newRemark;
/** 申请人ID */
private Long applyUserId;
/** 申请人姓名 */
private String applyUserName;
/** 申请时间 */
private Date applyTime;
/** 申请理由 */
private String applyReason;
/** 审核状态:0-待审核,1-审核通过,2-审核驳回 */
private Integer auditStatus;
/** 审核人ID */
private Long auditUserId;
/** 审核人姓名 */
private String auditUserName;
/** 审核时间 */
private Date auditTime;
/** 审核备注 */
private String auditRemark;
/** 删除标志 */
private Integer deleted;
/** 创建部门(审核表不需要此字段) */
@TableField(exist = false)
private Long createDept;
}
@@ -0,0 +1,24 @@
package org.dromara.xg.domain.bo;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
/**
* 操行分审核审批业务对象
*
* @author Lion Li
* @date 2025-01-16
*/
@Data
public class XgConductScoreAuditApproveBo {
private static final long serialVersionUID = 1L;
/**
* 审核备注
*/
@Size(max = 200, message = "审核备注不能超过200个字符")
private String auditRemark;
}
@@ -0,0 +1,32 @@
package org.dromara.xg.domain.bo;
import lombok.Data;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import java.util.List;
/**
* 操行分批量审核业务对象
*
* @author Lion Li
* @date 2025-01-16
*/
@Data
public class XgConductScoreAuditBatchBo {
private static final long serialVersionUID = 1L;
/**
* 审核记录ID列表
*/
@NotEmpty(message = "审核记录ID不能为空")
private List<Long> ids;
/**
* 审核备注
*/
@Size(max = 200, message = "审核备注不能超过200个字符")
private String auditRemark;
}
@@ -0,0 +1,95 @@
package org.dromara.xg.domain.bo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
/**
* 操行分审核查询业务对象
*
* @author Lion Li
* @date 2025-01-16
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class XgConductScoreAuditQueryBo extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/**
* 学生ID
*/
private Long studentId;
/**
* 学生姓名
*/
private String studentName;
/**
* 学号
*/
private String studentNo;
/**
* 班级ID
*/
private Long classId;
/**
* 班级名称
*/
private String className;
/**
* 申请人ID
*/
private Long applyUserId;
/**
* 申请人姓名
*/
private String applyUserName;
/**
* 审核状态:0-待审核,1-审核通过,2-审核驳回
*/
private Integer auditStatus;
/**
* 审核人ID
*/
private Long auditUserId;
/**
* 审核人姓名
*/
private String auditUserName;
/**
* 申请开始时间
*/
private String applyBeginTime;
/**
* 申请结束时间
*/
private String applyEndTime;
/**
* 审核开始时间
*/
private String auditBeginTime;
/**
* 审核结束时间
*/
private String auditEndTime;
}
@@ -0,0 +1,24 @@
package org.dromara.xg.domain.bo;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
/**
* 操行分审核驳回业务对象
*
* @author Lion Li
* @date 2025-01-16
*/
@Data
public class XgConductScoreAuditRejectBo {
private static final long serialVersionUID = 1L;
/**
* 审核备注
*/
@Size(max = 200, message = "审核备注不能超过200个字符")
private String auditRemark;
}
@@ -0,0 +1,50 @@
package org.dromara.xg.domain.bo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.util.List;
import jakarta.validation.constraints.NotNull;
/**
* 学生操行分批量修改审核业务对象
*
* @author Lion Li
* @date 2025-01-16
*/
@Data
@Accessors(chain = true)
public class XgConductScoreBatchUpdateBo {
private static final long serialVersionUID = 1L;
/**
* 学年
*/
@NotNull(message = "学年不能为空")
private Integer academicYear;
/**
* 学期
*/
@NotNull(message = "学期不能为空")
private Integer semester;
/**
* 月份
*/
@NotNull(message = "月份不能为空")
private Integer month;
/**
* 班级ID(前端传递字符串避免精度问题)
*/
private String classId;
/**
* 修改分数列表
*/
private List<XgConductScoreBatchUpdateItemBo> scores;
}
@@ -0,0 +1,39 @@
package org.dromara.xg.domain.bo;
import lombok.Data;
import lombok.experimental.Accessors;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
/**
* 学生操行分批量修改审核项业务对象
*
* @author Lion Li
* @date 2025-01-16
*/
@Data
@Accessors(chain = true)
public class XgConductScoreBatchUpdateItemBo {
private static final long serialVersionUID = 1L;
/**
* 学生ID
*/
@NotNull(message = "学生ID不能为空")
private Long studentId;
/**
* 新分数
*/
@NotNull(message = "分数不能为空")
@DecimalMin(value = "0", message = "分数不能小于0")
@DecimalMax(value = "100", message = "分数不能大于100")
private BigDecimal score;
/**
* 新备注
*/
private String remark;
}
@@ -0,0 +1,166 @@
package org.dromara.xg.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.io.Serial;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import org.dromara.xg.domain.XgConductScoreAudit;
import io.github.linpeilie.annotations.AutoMapper;
/**
* 操行分审核视图对象
*
* @author Lion Li
* @date 2025-01-16
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = XgConductScoreAudit.class)
public class XgConductScoreAuditVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
private Long id;
/**
* 租户ID
*/
private String tenantId;
/**
* 学生ID
*/
private Long studentId;
/**
* 学生姓名
*/
private String studentName;
/**
* 学号
*/
private String studentNo;
/**
* 学年
*/
private Integer academicYear;
/**
* 学期
*/
private Integer semester;
/**
* 月份
*/
private Integer month;
/**
* 班级ID
*/
private Long classId;
/**
* 班级名称
*/
private String className;
/**
* 原分数
*/
private BigDecimal oldScore;
/**
* 原备注
*/
private String oldRemark;
/**
* 新分数
*/
private BigDecimal newScore;
/**
* 新备注
*/
private String newRemark;
/**
* 申请人ID
*/
private Long applyUserId;
/**
* 申请人姓名
*/
private String applyUserName;
/**
* 申请时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date applyTime;
/**
* 申请理由
*/
private String applyReason;
/**
* 审核状态:0-待审核,1-审核通过,2-审核驳回
*/
private Integer auditStatus;
/**
* 审核人ID
*/
private Long auditUserId;
/**
* 审核人姓名
*/
private String auditUserName;
/**
* 审核时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date auditTime;
/**
* 审核备注
*/
private String auditRemark;
/**
* 创建者
*/
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;
}
@@ -71,7 +71,8 @@ public class XgConductScoreImportListener extends AnalysisEventListener<XgConduc
List<XgStudentMsgVo> students = studentMsgService.queryList(queryBo);
if (students == null || students.isEmpty()) {
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、学号 ").append(importVo.getStudentNo()).append(" 不存在");
failureMsg.append("<br/>").append(failureNum).append("、学号 ").append(importVo.getStudentNo())
.append(" 不存在");
return;
}
XgStudentMsgVo student = students.get(0);
@@ -80,34 +81,70 @@ public class XgConductScoreImportListener extends AnalysisEventListener<XgConduc
if (!importVo.getStudentName().equals(student.getName())) {
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、学号 ").append(importVo.getStudentNo())
.append(" 姓名不匹配,Excel中:").append(importVo.getStudentName())
.append(",系统中:").append(student.getName());
.append(" 姓名不匹配,Excel中:").append(importVo.getStudentName())
.append(",系统中:").append(student.getName());
return;
}
// 4. 验证班级权限(如果指定了班级)
if (StringUtils.isNotBlank(classId) && !classId.equals(student.getClassId())) {
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、学号 ").append(importVo.getStudentNo()).append(" 不属于指定班级");
failureMsg.append("<br/>").append(failureNum).append("、学号 ").append(importVo.getStudentNo())
.append(" 不属于指定班级");
return;
}
// 5. 检查是否已有记录
XgConductScoreVo existingRecord = conductScoreService.selectByStudentAndTime(
student.getId(), academicYear, semester, month);
student.getId(), academicYear, semester, month);
if (existingRecord != null) {
// 更新现有记录
XgConductScoreBo updateBo = new XgConductScoreBo();
updateBo.setId(existingRecord.getId());
updateBo.setScore(java.math.BigDecimal.valueOf(importVo.getScore()));
updateBo.setRemark(importVo.getRemark());
updateBo.setUpdateBy(operUserId);
conductScoreService.updateByBo(updateBo);
// 检查数据是否发生变化
boolean scoreChanged = !java.math.BigDecimal.valueOf(importVo.getScore())
.equals(existingRecord.getScore());
boolean remarkChanged = !StringUtils.equals(StringUtils.trim(importVo.getRemark()),
StringUtils.trim(existingRecord.getRemark()));
successNum++;
successMsg.append("<br/>").append(successNum).append("、学号 ").append(importVo.getStudentNo())
.append(" 更新成功(覆盖原有分数 ").append(existingRecord.getScore()).append(" → ").append(importVo.getScore()).append(")");
if (scoreChanged || remarkChanged) {
// 数据有变化:提交审核
try {
// 构造审核数据
org.dromara.xg.domain.bo.XgConductScoreBatchUpdateItemBo updateItem = new org.dromara.xg.domain.bo.XgConductScoreBatchUpdateItemBo()
.setStudentId(student.getId())
.setScore(java.math.BigDecimal.valueOf(importVo.getScore()))
.setRemark(importVo.getRemark());
org.dromara.xg.domain.bo.XgConductScoreBatchUpdateBo batchUpdateBo = new org.dromara.xg.domain.bo.XgConductScoreBatchUpdateBo()
.setAcademicYear(academicYear)
.setSemester(semester)
.setMonth(month)
.setClassId(student.getClassId())
.setScores(java.util.Arrays.asList(updateItem));
// 调用审核接口
conductScoreService.batchUpdateConductScores(batchUpdateBo);
successNum++;
successMsg.append("<br/>").append(successNum).append("、学号 ").append(importVo.getStudentNo())
.append(" 提交审核(数据有变化)");
} catch (Exception e) {
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、学号 ").append(importVo.getStudentNo())
.append(" 提交审核失败:").append(e.getMessage());
}
} else {
// 数据无变化:直接更新
XgConductScoreBo updateBo = new XgConductScoreBo();
updateBo.setId(existingRecord.getId());
updateBo.setScore(java.math.BigDecimal.valueOf(importVo.getScore()));
updateBo.setRemark(importVo.getRemark());
updateBo.setUpdateBy(operUserId);
conductScoreService.updateByBo(updateBo);
successNum++;
successMsg.append("<br/>").append(successNum).append("、学号 ").append(importVo.getStudentNo())
.append(" 更新成功(数据无变化)");
}
} else {
// 插入新记录
XgConductScoreBo insertBo = new XgConductScoreBo();
@@ -124,7 +161,8 @@ public class XgConductScoreImportListener extends AnalysisEventListener<XgConduc
conductScoreService.insertByBo(insertBo);
successNum++;
successMsg.append("<br/>").append(successNum).append("、学号 ").append(importVo.getStudentNo()).append(" 导入成功");
successMsg.append("<br/>").append(successNum).append("、学号 ").append(importVo.getStudentNo())
.append(" 导入成功");
}
} catch (Exception e) {
@@ -197,4 +235,4 @@ public class XgConductScoreImportListener extends AnalysisEventListener<XgConduc
}
};
}
}
}
@@ -0,0 +1,51 @@
package org.dromara.xg.mapper;
import org.dromara.xg.domain.XgConductScoreAudit;
import org.dromara.xg.domain.bo.XgConductScoreAuditQueryBo;
import org.dromara.xg.domain.vo.XgConductScoreAuditVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 操行分审核Mapper接口
*
* @author Lion Li
* @date 2025-01-16
*/
public interface IXgConductScoreAuditMapper extends BaseMapperPlus<XgConductScoreAudit, XgConductScoreAuditVo> {
/**
* 查询操行分审核列表(包含关联数据)
*
* @param queryBo 查询条件
* @return 操行分审核列表
*/
List<XgConductScoreAuditVo> selectXgConductScoreAuditList(XgConductScoreAuditQueryBo queryBo);
/**
* 根据审核ID列表批量更新审核状态
*
* @param ids 审核ID列表
* @param auditStatus 审核状态
* @param auditUserId 审核人ID
* @param auditUserName 审核人姓名
* @param auditRemark 审核备注
* @return 影响行数
*/
int updateBatchAuditStatus(@Param("ids") List<Long> ids,
@Param("auditStatus") Integer auditStatus,
@Param("auditUserId") Long auditUserId,
@Param("auditUserName") String auditUserName,
@Param("auditRemark") String auditRemark);
/**
* 根据审核ID将数据同步到正式表
*
* @param auditId 审核ID
* @return 影响行数
*/
int syncToFormalTable(@Param("auditId") Long auditId);
}
@@ -0,0 +1,115 @@
package org.dromara.xg.service;
import org.dromara.xg.domain.XgConductScoreAudit;
import org.dromara.xg.domain.vo.XgConductScoreAuditVo;
import org.dromara.xg.domain.bo.XgConductScoreAuditQueryBo;
import org.dromara.xg.domain.bo.XgConductScoreAuditApproveBo;
import org.dromara.xg.domain.bo.XgConductScoreAuditRejectBo;
import org.dromara.xg.domain.bo.XgConductScoreAuditBatchBo;
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 Lion Li
* @date 2025-01-16
*/
public interface IXgConductScoreAuditService {
/**
* 查询操行分审核
*
* @param id 主键
* @return 操行分审核
*/
XgConductScoreAuditVo queryById(Long id);
/**
* 分页查询操行分审核列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 操行分审核分页列表
*/
TableDataInfo<XgConductScoreAuditVo> queryPageList(XgConductScoreAuditQueryBo bo, PageQuery pageQuery);
/**
* 查询符合条件的操行分审核列表
*
* @param bo 查询条件
* @return 操行分审核列表
*/
List<XgConductScoreAuditVo> queryList(XgConductScoreAuditQueryBo bo);
/**
* 新增操行分审核
*
* @param bo 操行分审核
* @return 结果
*/
Boolean insertByBo(Object bo);
/**
* 修改操行分审核
*
* @param bo 操行分审核
* @return 结果
*/
Boolean updateByBo(Object bo);
/**
* 校验并批量删除操行分审核信息
*
* @param ids 待删除的主键集合
* @param isValid 是否校验,true-删除前校验,false-直接删除
* @return 结果
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 审核通过
*
* @param id 审核ID
* @param bo 审核参数
* @return 结果
*/
Boolean approveAudit(Long id, XgConductScoreAuditApproveBo bo);
/**
* 审核驳回
*
* @param id 审核ID
* @param bo 审核参数
* @return 结果
*/
Boolean rejectAudit(Long id, XgConductScoreAuditRejectBo bo);
/**
* 批量审核通过
*
* @param bo 批量审核参数
* @return 结果
*/
Boolean batchApproveAudit(XgConductScoreAuditBatchBo bo);
/**
* 批量审核驳回
*
* @param bo 批量审核参数
* @return 结果
*/
Boolean batchRejectAudit(XgConductScoreAuditBatchBo bo);
/**
* 批量提交修改审核
*
* @param auditList 审核记录列表
* @return 结果
*/
Boolean batchInsertAudits(List<XgConductScoreAudit> auditList);
}
@@ -4,6 +4,7 @@ import org.dromara.xg.domain.vo.XgConductScoreVo;
import org.dromara.xg.domain.bo.XgConductScoreBo;
import org.dromara.xg.domain.bo.XgConductScoreQueryBo;
import org.dromara.xg.domain.bo.XgConductScoreBatchBo;
import org.dromara.xg.domain.bo.XgConductScoreBatchUpdateBo;
import org.dromara.xg.domain.vo.XgConductScoreBatchResultVo;
import org.dromara.xg.domain.vo.XgConductScoreClassSummaryVo;
import org.dromara.xg.domain.vo.XgConductScoreSemesterAverageVo;
@@ -101,6 +102,14 @@ public interface IXgConductScoreService {
*/
XgConductScoreBatchResultVo batchInsertConductScores(XgConductScoreBatchBo bo);
/**
* 批量提交修改审核
*
* @param bo 批量修改审核参数
* @return 审核结果
*/
XgConductScoreBatchResultVo batchUpdateConductScores(XgConductScoreBatchUpdateBo bo);
/**
* 获取班主任负责的班级学生列表
*
@@ -0,0 +1,328 @@
package org.dromara.xg.service.impl;
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.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.xg.domain.XgConductScoreAudit;
import org.dromara.xg.domain.bo.XgConductScoreAuditQueryBo;
import org.dromara.xg.domain.bo.XgConductScoreAuditApproveBo;
import org.dromara.xg.domain.bo.XgConductScoreAuditRejectBo;
import org.dromara.xg.domain.bo.XgConductScoreAuditBatchBo;
import org.dromara.xg.domain.vo.XgConductScoreAuditVo;
import org.dromara.xg.mapper.IXgConductScoreAuditMapper;
import org.dromara.xg.service.IXgConductScoreAuditService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* 操行分审核Service业务层处理
*
* @author Lion Li
* @date 2025-01-16
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class XgConductScoreAuditServiceImpl implements IXgConductScoreAuditService {
private final IXgConductScoreAuditMapper xgConductScoreAuditMapper;
/**
* 查询操行分审核
*
* @param id 主键
* @return 操行分审核
*/
@Override
public XgConductScoreAuditVo queryById(Long id) {
return xgConductScoreAuditMapper.selectVoById(id);
}
/**
* 分页查询操行分审核列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 操行分审核分页列表
*/
@Override
public TableDataInfo<XgConductScoreAuditVo> queryPageList(XgConductScoreAuditQueryBo bo, PageQuery pageQuery) {
// 设置租户ID
if (StringUtils.isBlank(bo.getTenantId())) {
bo.setTenantId(TenantHelper.getTenantId());
}
// 设置默认审核状态为待审核
if (bo.getAuditStatus() == null) {
bo.setAuditStatus(0);
}
// 使用自定义查询方法,支持关联查询
List<XgConductScoreAuditVo> list = xgConductScoreAuditMapper.selectXgConductScoreAuditList(bo);
// 手动分页(因为使用了自定义SQL查询)
int total = list.size();
int startIndex = (pageQuery.getPageNum() - 1) * pageQuery.getPageSize();
int endIndex = Math.min(startIndex + pageQuery.getPageSize(), total);
List<XgConductScoreAuditVo> pageList = new ArrayList<>();
if (startIndex < total) {
pageList = list.subList(startIndex, endIndex);
}
return new TableDataInfo<>(pageList, total);
}
/**
* 查询符合条件的操行分审核列表
*
* @param bo 查询条件
* @return 操行分审核列表
*/
@Override
public List<XgConductScoreAuditVo> queryList(XgConductScoreAuditQueryBo bo) {
return xgConductScoreAuditMapper.selectXgConductScoreAuditList(bo);
}
/**
* 新增操行分审核
*
* @param bo 操行分审核
* @return 结果
*/
@Override
public Boolean insertByBo(Object bo) {
XgConductScoreAudit audit = MapstructUtils.convert(bo, XgConductScoreAudit.class);
validEntityBeforeSave(audit);
return xgConductScoreAuditMapper.insert(audit) > 0;
}
/**
* 修改操行分审核
*
* @param bo 操行分审核
* @return 结果
*/
@Override
public Boolean updateByBo(Object bo) {
XgConductScoreAudit audit = MapstructUtils.convert(bo, XgConductScoreAudit.class);
validEntityBeforeSave(audit);
return xgConductScoreAuditMapper.updateById(audit) > 0;
}
/**
* 校验并批量删除操行分审核信息
*
* @param ids 待删除的主键集合
* @param isValid 是否校验,true-删除前校验,false-直接删除
* @return 结果
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// 只能删除待审核状态的记录
LambdaQueryWrapper<XgConductScoreAudit> lqw = Wrappers.lambdaQuery();
lqw.in(XgConductScoreAudit::getId, ids);
lqw.ne(XgConductScoreAudit::getAuditStatus, 0);
if (xgConductScoreAuditMapper.exists(lqw)) {
throw new ServiceException("只能删除待审核状态的记录");
}
}
return xgConductScoreAuditMapper.deleteBatchIds(ids) > 0;
}
/**
* 审核通过
*
* @param id 审核ID
* @param bo 审核参数
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean approveAudit(Long id, XgConductScoreAuditApproveBo bo) {
// 1. 更新审核记录状态
XgConductScoreAudit audit = new XgConductScoreAudit();
audit.setId(id);
audit.setAuditStatus(1); // 审核通过
audit.setAuditUserId(LoginHelper.getUserId());
audit.setAuditUserName(LoginHelper.getUsername());
audit.setAuditTime(new Date());
audit.setAuditRemark(bo.getAuditRemark());
audit.setUpdateBy(LoginHelper.getUserId());
audit.setUpdateTime(new Date());
int updateCount = xgConductScoreAuditMapper.updateById(audit);
if (updateCount <= 0) {
throw new ServiceException("审核记录不存在或状态已变更");
}
// 2. 同步数据到正式表
int syncCount = xgConductScoreAuditMapper.syncToFormalTable(id);
if (syncCount <= 0) {
throw new ServiceException("同步数据到正式表失败");
}
log.info("审核通过成功,审核ID: {}, 同步记录数: {}", id, syncCount);
return true;
}
/**
* 审核驳回
*
* @param id 审核ID
* @param bo 审核参数
* @return 结果
*/
@Override
public Boolean rejectAudit(Long id, XgConductScoreAuditRejectBo bo) {
XgConductScoreAudit audit = new XgConductScoreAudit();
audit.setId(id);
audit.setAuditStatus(2); // 审核驳回
audit.setAuditUserId(LoginHelper.getUserId());
audit.setAuditUserName(LoginHelper.getUsername());
audit.setAuditTime(new Date());
audit.setAuditRemark(bo.getAuditRemark());
audit.setUpdateBy(LoginHelper.getUserId());
audit.setUpdateTime(new Date());
int updateCount = xgConductScoreAuditMapper.updateById(audit);
if (updateCount <= 0) {
throw new ServiceException("审核记录不存在或状态已变更");
}
log.info("审核驳回成功,审核ID: {}", id);
return true;
}
/**
* 批量审核通过
*
* @param bo 批量审核参数
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean batchApproveAudit(XgConductScoreAuditBatchBo bo) {
Long auditUserId = LoginHelper.getUserId();
String auditUserName = LoginHelper.getUsername();
String auditRemark = bo.getAuditRemark();
// 批量更新审核状态
int updateCount = xgConductScoreAuditMapper.updateBatchAuditStatus(
bo.getIds(), 1, auditUserId, auditUserName, auditRemark);
if (updateCount != bo.getIds().size()) {
throw new ServiceException("部分审核记录不存在或状态已变更");
}
// 批量同步数据到正式表
for (Long auditId : bo.getIds()) {
int syncCount = xgConductScoreAuditMapper.syncToFormalTable(auditId);
if (syncCount <= 0) {
throw new ServiceException("同步数据到正式表失败,审核ID: " + auditId);
}
}
log.info("批量审核通过成功,审核ID数量: {}", bo.getIds().size());
return true;
}
/**
* 批量审核驳回
*
* @param bo 批量审核参数
* @return 结果
*/
@Override
public Boolean batchRejectAudit(XgConductScoreAuditBatchBo bo) {
Long auditUserId = LoginHelper.getUserId();
String auditUserName = LoginHelper.getUsername();
String auditRemark = bo.getAuditRemark();
int updateCount = xgConductScoreAuditMapper.updateBatchAuditStatus(
bo.getIds(), 2, auditUserId, auditUserName, auditRemark);
if (updateCount != bo.getIds().size()) {
throw new ServiceException("部分审核记录不存在或状态已变更");
}
log.info("批量审核驳回成功,审核ID数量: {}", bo.getIds().size());
return true;
}
/**
* 批量提交修改审核
*
* @param auditList 审核记录列表
* @return 结果
*/
@Override
public Boolean batchInsertAudits(List<XgConductScoreAudit> auditList) {
if (auditList == null || auditList.isEmpty()) {
return true;
}
// 批量验证
for (XgConductScoreAudit audit : auditList) {
validEntityBeforeSave(audit);
}
// 批量插入
xgConductScoreAuditMapper.insertBatch(auditList);
return true;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(XgConductScoreAudit entity) {
// 基础字段校验
if (entity.getStudentId() == null) {
throw new ServiceException("学生ID不能为空");
}
if (entity.getAcademicYear() == null) {
throw new ServiceException("学年不能为空");
}
if (entity.getSemester() == null) {
throw new ServiceException("学期不能为空");
}
if (entity.getMonth() == null) {
throw new ServiceException("月份不能为空");
}
if (entity.getClassId() == null) {
throw new ServiceException("班级ID不能为空");
}
if (entity.getNewScore() == null) {
throw new ServiceException("新分数不能为空");
}
if (entity.getApplyUserId() == null) {
throw new ServiceException("申请人ID不能为空");
}
if (StringUtils.isBlank(entity.getApplyUserName())) {
throw new ServiceException("申请人姓名不能为空");
}
// 分数范围校验
if (entity.getNewScore().compareTo(BigDecimal.ZERO) < 0 ||
entity.getNewScore().compareTo(new BigDecimal("100")) > 0) {
throw new ServiceException("分数必须在0-100之间");
}
}
}
@@ -17,10 +17,12 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.xg.domain.XgConductScore;
import org.dromara.xg.domain.XgConductScoreAudit;
import org.dromara.xg.domain.XgClassMsg;
import org.dromara.xg.domain.XgStudentManager;
import org.dromara.xg.domain.XgStudentManagerSource;
import org.dromara.xg.domain.bo.XgConductScoreBatchBo;
import org.dromara.xg.domain.bo.XgConductScoreBatchUpdateBo;
import org.dromara.xg.domain.bo.XgConductScoreQueryBo;
import org.dromara.xg.domain.bo.XgConductScoreBo;
import org.dromara.xg.domain.vo.XgConductScoreBatchResultVo;
@@ -38,6 +40,7 @@ import org.dromara.xg.mapper.XgClassMsgMapper;
import org.dromara.xg.mapper.XgStudentManagerMapper;
import org.dromara.xg.mapper.XgStudentManagerSourceMapper;
import org.dromara.xg.service.IXgConductScoreService;
import org.dromara.xg.service.IXgConductScoreAuditService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -69,6 +72,7 @@ public class XgConductScoreServiceImpl implements IXgConductScoreService {
private final XgStudentManagerSourceMapper xgStudentManagerSourceMapper;
private final XgClassMsgMapper xgClassMsgMapper;
private final SysDeptMapper sysDeptMapper;
private final IXgConductScoreAuditService auditService;
/**
* 查询学生操行分
@@ -383,6 +387,112 @@ public class XgConductScoreServiceImpl implements IXgConductScoreService {
return result;
}
/**
* 批量提交修改审核
*
* @param bo 批量修改审核参数
* @return 审核结果
*/
@Override
public XgConductScoreBatchResultVo batchUpdateConductScores(XgConductScoreBatchUpdateBo bo) {
Long teacherId = LoginHelper.getUserId();
String tenantId = TenantHelper.getTenantId();
// 将String类型的classId转换为Long
Long classId = Long.valueOf(bo.getClassId());
// 校验班主任权限
if (!checkTeacherPermission(classId, teacherId)) {
throw new ServiceException("您不是该班级的班主任,无权修改操行分");
}
List<String> failList = new ArrayList<>();
int successCount = 0;
int failCount = 0;
List<XgConductScoreAudit> auditList = new ArrayList<>();
for (var item : bo.getScores()) {
try {
// 校验分数范围
if (item.getScore().compareTo(BigDecimal.ZERO) < 0
|| item.getScore().compareTo(new BigDecimal("100.00")) > 0) {
failList.add("学生ID:" + item.getStudentId() + " - 分数范围错误");
failCount++;
continue;
}
// 检查学生是否已存在当月记录
if (!existsByStudentAndMonth(item.getStudentId(), bo.getAcademicYear(), bo.getSemester(), bo.getMonth(),
tenantId)) {
failList.add("学生ID:" + item.getStudentId() + " - 当月无历史分数记录,无法提交修改审核");
failCount++;
continue;
}
// 查询现有的分数记录,获取原分数和备注
XgConductScoreVo existingRecord = selectByStudentAndTime(item.getStudentId(),
bo.getAcademicYear(), bo.getSemester(), bo.getMonth());
if (existingRecord == null) {
failList.add("学生ID:" + item.getStudentId() + " - 未找到历史记录");
failCount++;
continue;
}
// 检查是否有实际的修改(分数或备注不同)
boolean hasScoreChange = !item.getScore().equals(existingRecord.getScore());
boolean hasRemarkChange = !ObjectUtils.equals(item.getRemark(), existingRecord.getRemark());
if (!hasScoreChange && !hasRemarkChange) {
failList.add("学生ID:" + item.getStudentId() + " - 无实际修改内容");
failCount++;
continue;
}
// 创建审核记录
XgConductScoreAudit audit = new XgConductScoreAudit();
audit.setTenantId(tenantId);
audit.setStudentId(item.getStudentId());
audit.setAcademicYear(bo.getAcademicYear());
audit.setSemester(bo.getSemester());
audit.setMonth(bo.getMonth());
audit.setClassId(classId);
audit.setOldScore(existingRecord.getScore());
audit.setOldRemark(existingRecord.getRemark());
audit.setNewScore(item.getScore());
audit.setNewRemark(item.getRemark());
audit.setApplyUserId(teacherId);
audit.setApplyUserName(LoginHelper.getUsername());
audit.setApplyTime(new Date());
audit.setApplyReason("班主任修改操行分");
audit.setAuditStatus(0); // 待审核
audit.setCreateBy(teacherId);
audit.setCreateTime(new Date());
auditList.add(audit);
successCount++;
} catch (Exception e) {
log.error("处理学生操行分修改审核失败,学生ID: {}", item.getStudentId(), e);
failList.add("学生ID:" + item.getStudentId() + " - 处理失败: " + e.getMessage());
failCount++;
}
}
// 批量提交审核
if (!auditList.isEmpty()) {
auditService.batchInsertAudits(auditList);
}
XgConductScoreBatchResultVo result = new XgConductScoreBatchResultVo();
result.setSuccessCount(successCount);
result.setFailCount(failCount);
result.setFailList(failList);
return result;
}
/**
* 获取班主任负责的班级学生列表
*
@@ -0,0 +1,150 @@
<?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.IXgConductScoreAuditMapper">
<resultMap type="org.dromara.xg.domain.vo.XgConductScoreAuditVo" id="XgConductScoreAuditResult">
<result property="id" column="id"/>
<result property="tenantId" column="tenant_id"/>
<result property="studentId" column="student_id"/>
<result property="studentName" column="student_name"/>
<result property="studentNo" column="student_no"/>
<result property="academicYear" column="academic_year"/>
<result property="semester" column="semester"/>
<result property="month" column="month"/>
<result property="classId" column="class_id"/>
<result property="className" column="class_name"/>
<result property="oldScore" column="old_score"/>
<result property="oldRemark" column="old_remark"/>
<result property="newScore" column="new_score"/>
<result property="newRemark" column="new_remark"/>
<result property="applyUserId" column="apply_user_id"/>
<result property="applyUserName" column="apply_user_name"/>
<result property="applyTime" column="apply_time"/>
<result property="applyReason" column="apply_reason"/>
<result property="auditStatus" column="audit_status"/>
<result property="auditUserId" column="audit_user_id"/>
<result property="auditUserName" column="audit_user_name"/>
<result property="auditTime" column="audit_time"/>
<result property="auditRemark" column="audit_remark"/>
<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>
<!-- 查询审核列表(包含关联数据) -->
<select id="selectXgConductScoreAuditList" parameterType="org.dromara.xg.domain.bo.XgConductScoreAuditQueryBo" resultMap="XgConductScoreAuditResult">
SELECT
a.id,
a.tenant_id,
a.student_id,
sm.name AS student_name,
sm.student_no,
a.academic_year,
a.semester,
a.month,
a.class_id,
cm.class_name,
a.old_score,
a.old_remark,
a.new_score,
a.new_remark,
a.apply_user_id,
a.apply_user_name,
a.apply_time,
a.apply_reason,
a.audit_status,
a.audit_user_id,
a.audit_user_name,
a.audit_time,
a.audit_remark,
a.create_by,
a.create_time,
a.update_by,
a.update_time
FROM xg_conduct_score_audit a
LEFT JOIN xg_student_msg sm ON a.student_id = sm.id AND sm.deleted = 0
LEFT JOIN xg_class_msg cm ON a.class_id = cm.id AND cm.deleted = 0
<where>
a.deleted = 0
<if test="tenantId != null">
AND a.tenant_id = #{tenantId}
</if>
<if test="studentId != null">
AND a.student_id = #{studentId}
</if>
<if test="studentName != null and studentName != ''">
AND sm.name LIKE CONCAT('%', #{studentName}, '%')
</if>
<if test="studentNo != null and studentNo != ''">
AND sm.student_no LIKE CONCAT('%', #{studentNo}, '%')
</if>
<if test="classId != null">
AND a.class_id = #{classId}
</if>
<if test="className != null and className != ''">
AND cm.class_name LIKE CONCAT('%', #{className}, '%')
</if>
<if test="applyUserId != null">
AND a.apply_user_id = #{applyUserId}
</if>
<if test="applyUserName != null and applyUserName != ''">
AND a.apply_user_name LIKE CONCAT('%', #{applyUserName}, '%')
</if>
<if test="auditStatus != null">
AND a.audit_status = #{auditStatus}
</if>
<if test="auditUserId != null">
AND a.audit_user_id = #{auditUserId}
</if>
<if test="auditUserName != null and auditUserName != ''">
AND a.audit_user_name LIKE CONCAT('%', #{auditUserName}, '%')
</if>
<if test="applyBeginTime != null and applyEndTime != null">
AND a.apply_time BETWEEN #{applyBeginTime} AND #{applyEndTime}
</if>
<if test="auditBeginTime != null and auditEndTime != null">
AND a.audit_time BETWEEN #{auditBeginTime} AND #{auditEndTime}
</if>
</where>
ORDER BY a.apply_time DESC
</select>
<!-- 批量更新审核状态 -->
<update id="updateBatchAuditStatus">
UPDATE xg_conduct_score_audit
SET
audit_status = #{auditStatus},
audit_user_id = #{auditUserId},
audit_user_name = #{auditUserName},
audit_time = NOW(),
audit_remark = #{auditRemark},
update_by = #{auditUserId},
update_time = NOW()
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<!-- 同步审核数据到正式表 -->
<update id="syncToFormalTable">
UPDATE xg_conduct_score cs
INNER JOIN xg_conduct_score_audit audit ON
cs.student_id = audit.student_id
AND cs.academic_year = audit.academic_year
AND cs.semester = audit.semester
AND cs.month = audit.month
AND cs.tenant_id = audit.tenant_id
AND cs.deleted = 0
SET
cs.score = audit.new_score,
cs.remark = audit.new_remark,
cs.update_by = audit.audit_user_id,
cs.update_time = NOW()
WHERE audit.id = #{auditId}
</update>
</mapper>
@@ -0,0 +1,66 @@
-- ----------------------------
-- 操行分审核表
-- ----------------------------
DROP TABLE IF EXISTS `xg_conduct_score_audit`;
CREATE TABLE `xg_conduct_score_audit` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户ID',
`student_id` bigint(20) NOT NULL COMMENT '学生ID',
`academic_year` int(11) NOT NULL COMMENT '学年',
`semester` tinyint(1) NOT NULL COMMENT '学期:1-第一学期,2-第二学期',
`month` tinyint(2) NOT NULL COMMENT '月份',
`class_id` bigint(20) NOT NULL COMMENT '班级ID',
`old_score` decimal(5,2) DEFAULT NULL COMMENT '原分数',
`old_remark` varchar(500) DEFAULT NULL COMMENT '原备注',
`new_score` decimal(5,2) NOT NULL COMMENT '新分数',
`new_remark` varchar(500) DEFAULT NULL COMMENT '新备注',
`apply_user_id` bigint(20) NOT NULL COMMENT '申请人ID',
`apply_user_name` varchar(50) NOT NULL COMMENT '申请人姓名',
`apply_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间',
`apply_reason` varchar(200) DEFAULT NULL COMMENT '申请理由',
`audit_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '审核状态:0-待审核,1-审核通过,2-审核驳回',
`audit_user_id` bigint(20) DEFAULT NULL COMMENT '审核人ID',
`audit_user_name` varchar(50) DEFAULT NULL COMMENT '审核人姓名',
`audit_time` datetime DEFAULT NULL COMMENT '审核时间',
`audit_remark` varchar(200) DEFAULT NULL COMMENT '审核备注',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_period` (`student_id`,`academic_year`,`semester`,`month`,`deleted`),
KEY `idx_class_period` (`class_id`,`academic_year`,`semester`,`month`),
KEY `idx_audit_status` (`audit_status`,`apply_time`),
KEY `idx_apply_user` (`apply_user_id`,`apply_time`),
KEY `idx_audit_user` (`audit_user_id`,`audit_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='操行分审核表';
-- ----------------------------
-- 为审核表添加必要的索引和约束
-- ----------------------------
ALTER TABLE `xg_conduct_score_audit`
ADD CONSTRAINT `fk_audit_student` FOREIGN KEY (`student_id`) REFERENCES `xg_student_manager` (`student_id`) ON DELETE CASCADE,
ADD CONSTRAINT `fk_audit_class` FOREIGN KEY (`class_id`) REFERENCES `xg_class_msg` (`class_id`) ON DELETE CASCADE,
ADD CONSTRAINT `fk_audit_apply_user` FOREIGN KEY (`apply_user_id`) REFERENCES `sys_user` (`user_id`),
ADD CONSTRAINT `fk_audit_user` FOREIGN KEY (`audit_user_id`) REFERENCES `sys_user` (`user_id`);
-- ----------------------------
-- 初始化审核表数据(可选)
-- ----------------------------
-- 将现有操行分数据标记为已审核状态(如果需要)
-- INSERT INTO xg_conduct_score_audit (
-- tenant_id, student_id, academic_year, semester, month, class_id,
-- old_score, old_remark, new_score, new_remark,
-- apply_user_id, apply_user_name, apply_time, apply_reason,
-- audit_status, audit_user_id, audit_user_name, audit_time, audit_remark,
-- create_by, create_time, update_by, update_time, deleted
-- )
-- SELECT
-- s.tenant_id, s.student_id, s.academic_year, s.semester, s.month, s.class_id,
-- NULL as old_score, NULL as old_remark, s.score as new_score, s.remark as new_remark,
-- s.class_teacher_id as apply_user_id, '系统导入' as apply_user_name, s.create_time as apply_time, '系统初始化' as apply_reason,
-- 1 as audit_status, s.class_teacher_id as audit_user_id, '系统导入' as audit_user_name, s.create_time as audit_time, '系统初始化审核' as audit_remark,
-- s.create_by, s.create_time, s.update_by, s.update_time, s.deleted
-- FROM xg_conduct_score s
-- WHERE s.deleted = 0;
+9
View File
@@ -70,6 +70,15 @@ export function conductScoreBatchAdd(data: any) {
});
}
// 批量提交操行分修改审核
export function conductScoreBatchUpdate(data: any) {
return request({
url: '/xg/conduct-score/batch-update',
method: 'post',
data: data
});
}
// 获取班主任负责的班级学生列表
export function conductScoreClassStudents(query: any) {
return request({
+72
View File
@@ -0,0 +1,72 @@
import request from '@/utils/request';
// 查询操行分审核列表
export function conductScoreAuditList(query: any) {
return request({
url: '/xg/conduct-score-audit/list',
method: 'get',
params: query
});
}
// 查询操行分审核详细
export function conductScoreAuditInfo(id: any) {
return request({
url: '/xg/conduct-score-audit/' + id,
method: 'get'
});
}
// 审核通过
export function conductScoreAuditApprove(id: any, data: any) {
return request({
url: '/xg/conduct-score-audit/approve/' + id,
method: 'post',
data: data
});
}
// 审核驳回
export function conductScoreAuditReject(id: any, data: any) {
return request({
url: '/xg/conduct-score-audit/reject/' + id,
method: 'post',
data: data
});
}
// 批量审核通过
export function conductScoreAuditBatchApprove(data: any) {
return request({
url: '/xg/conduct-score-audit/batch-approve',
method: 'post',
data: data
});
}
// 批量审核驳回
export function conductScoreAuditBatchReject(data: any) {
return request({
url: '/xg/conduct-score-audit/batch-reject',
method: 'post',
data: data
});
}
// 导出操行分审核
export function conductScoreAuditExport(query: any) {
return request({
url: '/xg/conduct-score-audit/export',
method: 'post',
params: query
});
}
// 获取审核统计
export function conductScoreAuditStatistics(query: any) {
return request({
url: '/xg/conduct-score-audit/statistics',
method: 'get',
params: query
});
}
@@ -0,0 +1,394 @@
<template>
<el-dialog v-model="visible" :title="dialogTitle" width="600px" append-to-body :before-close="handleClose">
<div class="audit-dialog-content">
<!-- 审核详情 -->
<div v-if="dialogMode === 'view'" class="audit-detail">
<div class="detail-section">
<h4>学生信息</h4>
<div class="info-grid">
<div class="info-item">
<label>学生姓名:</label>
<span>{{ auditData.studentName }}</span>
</div>
<div class="info-item">
<label>学号:</label>
<span>{{ auditData.studentNo }}</span>
</div>
<div class="info-item">
<label>班级:</label>
<span>{{ auditData.className }}</span>
</div>
<div class="info-item">
<label>学年学期:</label>
<span>{{ auditData.academicYear - 1 }}-{{ auditData.academicYear }}学年 {{ auditData.semester === 1 ? '第一学期' : '第二学期' }} {{ auditData.month }}月</span>
</div>
</div>
</div>
<div class="detail-section">
<h4>分数信息</h4>
<div class="score-comparison">
<div class="score-item old-score">
<label>原分数:</label>
<span class="score-value">{{ auditData.oldScore || 0 }}</span>
</div>
<div class="score-arrow">→</div>
<div class="score-item new-score">
<label>新分数:</label>
<span class="score-value">{{ auditData.newScore || 0 }}</span>
</div>
<div class="score-change" v-if="auditData.newScore != null && auditData.oldScore != null">
<el-tag :type="auditData.newScore > auditData.oldScore ? 'success' : auditData.newScore < auditData.oldScore ? 'danger' : 'info'" size="small">
{{ auditData.newScore > auditData.oldScore ? '+' : '' }}{{ auditData.newScore - auditData.oldScore }}
</el-tag>
</div>
</div>
<div class="remark-section" v-if="auditData.oldRemark || auditData.newRemark">
<div class="remark-item" v-if="auditData.oldRemark">
<label>原备注:</label>
<span>{{ auditData.oldRemark }}</span>
</div>
<div class="remark-item" v-if="auditData.newRemark">
<label>新备注:</label>
<span>{{ auditData.newRemark }}</span>
</div>
</div>
</div>
<div class="detail-section">
<h4>审核信息</h4>
<div class="info-grid">
<div class="info-item">
<label>申请人:</label>
<span>{{ auditData.applyUserName }}</span>
</div>
<div class="info-item">
<label>申请时间:</label>
<span>{{ parseTime(auditData.applyTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</div>
<div class="info-item" v-if="auditData.applyReason">
<label>申请理由:</label>
<span>{{ auditData.applyReason }}</span>
</div>
<div class="info-item">
<label>审核状态:</label>
<el-tag :type="getAuditStatusType(auditData.auditStatus)" size="small">
{{ getAuditStatusLabel(auditData.auditStatus) }}
</el-tag>
</div>
<div class="info-item" v-if="auditData.auditUserName">
<label>审核人:</label>
<span>{{ auditData.auditUserName }}</span>
</div>
<div class="info-item" v-if="auditData.auditTime">
<label>审核时间:</label>
<span>{{ parseTime(auditData.auditTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</div>
<div class="info-item" v-if="auditData.auditRemark" style="grid-column: 1 / -1;">
<label>审核意见:</label>
<span>{{ auditData.auditRemark }}</span>
</div>
</div>
</div>
</div>
<!-- 审核操作 -->
<div v-else-if="dialogMode === 'audit'" class="audit-form">
<div class="audit-info">
<div class="student-info">
<strong>{{ auditData.studentName }} ({{ auditData.studentNo }})</strong>
<span class="class-info">{{ auditData.className }}</span>
</div>
<div class="score-change-info">
分数修改:{{ auditData.oldScore }} → <strong>{{ auditData.newScore }}</strong>
<el-tag :type="auditData.newScore > auditData.oldScore ? 'success' : auditData.newScore < auditData.oldScore ? 'danger' : 'info'" size="small" style="margin-left: 10px;">
{{ auditData.newScore > auditData.oldScore ? '+' : '' }}{{ auditData.newScore - auditData.oldScore }}
</el-tag>
</div>
<div class="remark-info" v-if="auditData.newRemark">
新备注:{{ auditData.newRemark }}
</div>
</div>
<el-divider />
<el-form :model="auditForm" label-width="80px">
<el-form-item label="审核意见" prop="auditRemark">
<el-input
v-model="auditForm.auditRemark"
type="textarea"
:rows="3"
placeholder="请输入审核意见(可选)"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<template v-if="dialogMode === 'audit'">
<el-button type="danger" @click="handleReject" :loading="submitting">驳回</el-button>
<el-button type="success" @click="handleApprove" :loading="submitting">通过</el-button>
</template>
<template v-else>
<el-button type="primary" @click="handleClose">确定</el-button>
</template>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="AuditDialog">
import { ref, reactive, getCurrentInstance } from 'vue';
import ElMessage from 'element-plus/es/components/message/index';
// API接口
import { conductScoreAuditApprove, conductScoreAuditReject } from '@/api/xg/conductScoreAudit';
// 获取当前实例
const { proxy } = getCurrentInstance() as any;
// 定义事件
const emit = defineEmits(['success']);
// 响应式数据
const visible = ref(false);
const dialogMode = ref<'view' | 'audit'>('view');
const dialogTitle = ref('审核详情');
const auditData = ref<any>({});
const auditForm = reactive({
auditRemark: ''
});
const submitting = ref(false);
// 方法
const openDialog = (data: any, mode: 'view' | 'audit' = 'view') => {
auditData.value = data;
dialogMode.value = mode;
if (mode === 'view') {
dialogTitle.value = '审核详情';
} else {
dialogTitle.value = '审核操行分修改';
}
auditForm.auditRemark = '';
visible.value = true;
};
const handleClose = () => {
visible.value = false;
};
const handleApprove = async () => {
await performAudit('approve');
};
const handleReject = async () => {
await performAudit('reject');
};
const performAudit = async (action: 'approve' | 'reject') => {
if (!auditData.value) return;
submitting.value = true;
try {
const auditRemark = auditForm.auditRemark.trim();
if (action === 'approve') {
await conductScoreAuditApprove(auditData.value.id, { auditRemark });
ElMessage.success('审核通过成功');
} else {
await conductScoreAuditReject(auditData.value.id, { auditRemark });
ElMessage.success('审核驳回成功');
}
emit('success');
handleClose();
} catch (error) {
ElMessage.error(`${action === 'approve' ? '审核通过' : '审核驳回'}失败`);
} finally {
submitting.value = false;
}
};
// 状态标签方法
const getAuditStatusType = (status: number) => {
switch (status) {
case 0: return 'warning';
case 1: return 'success';
case 2: return 'danger';
default: return 'info';
}
};
const getAuditStatusLabel = (status: number) => {
switch (status) {
case 0: return '待审核';
case 1: return '审核通过';
case 2: return '审核驳回';
default: return '未知';
}
};
// 暴露方法
defineExpose({
openDialog
});
</script>
<style scoped>
.audit-dialog-content {
max-height: 70vh;
overflow-y: auto;
}
.detail-section {
margin-bottom: 24px;
}
.detail-section h4 {
margin: 0 0 12px 0;
color: #303133;
font-size: 16px;
font-weight: 500;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 12px;
}
.info-item {
display: flex;
align-items: center;
padding: 8px 0;
}
.info-item label {
min-width: 80px;
color: #606266;
font-weight: 500;
}
.info-item span {
color: #303133;
}
.score-comparison {
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.score-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.score-item label {
font-size: 14px;
color: #909399;
}
.score-value {
font-size: 24px;
font-weight: 600;
color: #303133;
}
.old-score .score-value {
color: #909399;
}
.new-score .score-value {
color: #409eff;
}
.score-arrow {
margin: 0 20px;
font-size: 20px;
color: #c0c4cc;
font-weight: bold;
}
.score-change {
margin-left: 16px;
}
.remark-section {
margin-top: 16px;
}
.remark-item {
display: flex;
margin-bottom: 8px;
}
.remark-item label {
min-width: 60px;
color: #606266;
font-weight: 500;
flex-shrink: 0;
}
.remark-item span {
color: #303133;
line-height: 1.5;
}
.audit-form {
padding: 0;
}
.audit-info {
margin-bottom: 16px;
}
.student-info {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.student-info strong {
font-size: 16px;
color: #303133;
}
.class-info {
color: #909399;
font-size: 14px;
}
.score-change-info {
color: #606266;
margin-bottom: 8px;
}
.remark-info {
color: #606266;
font-size: 14px;
}
.dialog-footer {
text-align: right;
}
.dialog-footer .el-button {
margin-left: 8px;
}
</style>
@@ -0,0 +1,381 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="审核状态" prop="auditStatus">
<el-select v-model="queryParams.auditStatus" placeholder="审核状态" clearable>
<el-option label="待审核" :value="0" />
<el-option label="审核通过" :value="1" />
<el-option label="审核驳回" :value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Check"
:disabled="!hasSelected"
@click="handleBatchApprove"
v-hasPermi="['xg:conductScoreAudit:batchApprove']"
>
批量通过
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Close"
:disabled="!hasSelected"
@click="handleBatchReject"
v-hasPermi="['xg:conductScoreAudit:batchReject']"
>
批量驳回
</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table
v-loading="loading"
border
:data="auditList || []"
row-key="id"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="学年" align="center" prop="academicYear" width="100">
<template #default="scope">
{{ scope.row && scope.row.academicYear ? scope.row.academicYear - 1 + '-' + scope.row.academicYear + '学年' : '-' }}
</template>
</el-table-column>
<el-table-column label="学期" align="center" prop="semester" width="80">
<template #default="scope">
{{ scope.row.semester === 1 ? '第一学期' : '第二学期' }}
</template>
</el-table-column>
<el-table-column label="月份" align="center" prop="month" width="60">
<template #default="scope"> {{ scope.row.month }}月 </template>
</el-table-column>
<el-table-column label="学生姓名" align="center" prop="studentName" :show-overflow-tooltip="true" />
<el-table-column label="学号" align="center" prop="studentNo" width="120" :show-overflow-tooltip="true" />
<el-table-column label="班级" align="center" prop="className" :show-overflow-tooltip="true" width="150" />
<el-table-column label="原分数" align="center" prop="oldScore" width="80" />
<el-table-column label="原备注" align="center" prop="oldRemark" :show-overflow-tooltip="true" width="120">
<template #default="scope">
<span>{{ scope.row.oldRemark || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="新分数" align="center" prop="newScore" width="80">
<template #default="scope">
<span class="text-primary font-medium">{{ scope.row.newScore }}</span>
</template>
</el-table-column>
<el-table-column label="新备注" align="center" prop="newRemark" :show-overflow-tooltip="true" width="120">
<template #default="scope">
<span>{{ scope.row.newRemark || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="分数变化" align="center" width="100">
<template #default="scope">
<el-tag
v-if="scope.row && scope.row.newScore != null && scope.row.oldScore != null"
:type="scope.row.newScore > scope.row.oldScore ? 'success' : scope.row.newScore < scope.row.oldScore ? 'danger' : 'info'"
size="small"
>
{{ scope.row.newScore > scope.row.oldScore ? '+' : '' }}{{ scope.row.newScore - scope.row.oldScore }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="申请人" align="center" prop="applyUserName" width="100" />
<el-table-column label="申请时间" align="center" prop="applyTime" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.applyTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="审核状态" align="center" prop="auditStatus" width="100">
<template #default="scope">
<el-tag v-if="scope.row && scope.row.auditStatus != null" :type="getAuditStatusType(scope.row.auditStatus)" size="small">
{{ getAuditStatusLabel(scope.row.auditStatus) }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="审核人" align="center" prop="auditUserName" width="100" />
<el-table-column label="审核时间" align="center" prop="auditTime" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.auditTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<el-tooltip v-if="scope.row && scope.row.auditStatus === 0" content="审核通过" placement="top">
<el-button link type="success" icon="Check" @click="handleApprove(scope.row)" v-hasPermi="['xg:conductScoreAudit:approve']"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row && scope.row.auditStatus === 0" content="审核驳回" placement="top">
<el-button link type="danger" icon="Close" @click="handleReject(scope.row)" v-hasPermi="['xg:conductScoreAudit:reject']"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row" content="查看详情" placement="top">
<el-button link type="primary" icon="View" @click="handleViewDetail(scope.row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
</el-card>
<!-- 审核弹窗 -->
<AuditDialog ref="auditDialogRef" @success="getList" />
</div>
</template>
<script setup lang="ts" name="ConductScoreAudit">
import { ref, reactive, onMounted, computed, getCurrentInstance, nextTick } from 'vue';
import ElMessage from 'element-plus/es/components/message/index';
import ElMessageBox from 'element-plus/es/components/message-box/index';
import { ArrowDown } from '@element-plus/icons-vue';
// API接口
import {
conductScoreAuditList,
conductScoreAuditApprove,
conductScoreAuditReject,
conductScoreAuditBatchApprove,
conductScoreAuditBatchReject
} from '@/api/xg/conductScoreAudit';
// 组件
import AuditDialog from './components/AuditDialog.vue';
// 获取当前实例
const { proxy } = getCurrentInstance() as any;
// 响应式数据
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
studentName: '',
studentNo: '',
auditStatus: 0 // 默认显示待审核
});
// 数据列表
const auditList = ref([]);
const loading = ref(true); // 初始设置为true,避免DOM渲染问题
const total = ref(0);
// 选择相关
const selectedAudits = ref([]);
// 搜索显示控制
const showSearch = ref(true);
// 表单引用
const queryFormRef = ref();
const auditDialogRef = ref();
// 选项数据
// 计算属性
const hasSelected = computed(() => selectedAudits.value && selectedAudits.value.length > 0);
// 方法
const handleQuery = () => {
queryParams.pageNum = 1;
getList();
};
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.auditStatus = 0; // 重置为待审核
handleQuery();
};
const getList = async () => {
loading.value = true;
try {
const response = await conductScoreAuditList(queryParams);
if (response.code === 200) {
// 确保数据总是数组格式,避免DOM渲染问题
auditList.value = Array.isArray(response.rows) ? response.rows : [];
total.value = response.total || 0;
} else {
auditList.value = [];
total.value = 0;
}
} catch (error) {
console.error('获取审核列表失败:', error);
auditList.value = [];
total.value = 0;
} finally {
// 使用nextTick确保DOM更新完成
nextTick(() => {
loading.value = false;
});
}
};
const handleSelectionChange = (selection) => {
selectedAudits.value = selection;
};
const handleSortChange = ({ prop, order }) => {
queryParams.orderByColumn = prop;
queryParams.isAsc = order === 'ascending' ? 'asc' : 'desc';
getList();
};
// 单个审核操作
const handleApprove = async (row) => {
ElMessageBox.confirm(`确定要通过 ${row.studentName} 的分数修改申请吗?\n新分数:${row.newScore}`, '审核确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'success'
}).then(async () => {
try {
await conductScoreAuditApprove(row.id, { auditRemark: '审核通过' });
ElMessage.success('审核通过成功');
getList();
} catch (error) {
ElMessage.error('审核失败');
}
});
};
const handleReject = async (row) => {
ElMessageBox.confirm(`确定要驳回 ${row.studentName} 的分数修改申请吗?`, '审核确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await conductScoreAuditReject(row.id, { auditRemark: '审核驳回' });
ElMessage.success('审核驳回成功');
getList();
} catch (error) {
ElMessage.error('审核失败');
}
});
};
// 批量审核操作
const handleBatchApprove = async () => {
if (!selectedAudits.value || selectedAudits.value.length === 0) {
ElMessage.warning('请先选择要审核的记录');
return;
}
ElMessageBox.confirm(`确定要批量通过 ${selectedAudits.value.length} 条审核申请吗?`, '批量审核确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'success'
}).then(async () => {
try {
const ids = selectedAudits.value.map((item) => item.id);
await conductScoreAuditBatchApprove({ ids, auditRemark: '批量审核通过' });
ElMessage.success('批量审核通过成功');
selectedAudits.value = [];
getList();
} catch (error) {
ElMessage.error('批量审核失败');
}
});
};
const handleBatchReject = async () => {
if (!selectedAudits.value || selectedAudits.value.length === 0) {
ElMessage.warning('请先选择要审核的记录');
return;
}
ElMessageBox.confirm(`确定要批量驳回 ${selectedAudits.value.length} 条审核申请吗?`, '批量审核确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const ids = selectedAudits.value.map((item) => item.id);
await conductScoreAuditBatchReject({ ids, auditRemark: '批量审核驳回' });
ElMessage.success('批量审核驳回成功');
selectedAudits.value = [];
getList();
} catch (error) {
ElMessage.error('批量审核失败');
}
});
};
// 查看详情
const handleViewDetail = (row) => {
auditDialogRef.value?.openDialog(row, 'view');
};
// 状态标签方法
const getAuditStatusType = (status) => {
switch (status) {
case 0:
return 'warning';
case 1:
return 'success';
case 2:
return 'danger';
default:
return 'info';
}
};
const getAuditStatusLabel = (status) => {
switch (status) {
case 0:
return '待审核';
case 1:
return '审核通过';
case 2:
return '审核驳回';
default:
return '未知';
}
};
// 生命周期
onMounted(() => {
// 确保组件完全加载后再获取数据
nextTick(() => {
getList();
});
});
</script>
<style scoped>
.text-primary {
color: #409eff;
}
.font-medium {
font-weight: 500;
}
</style>
@@ -290,11 +290,9 @@
<el-input v-model="scope.row.remark" size="small" placeholder="输入备注(可选)" maxlength="500" show-word-limit />
</template>
</el-table-column>
<el-table-column v-if="columns[6].visible" key="status" label="状态" align="center" width="100">
<el-table-column v-if="columns[6].visible" key="status" label="状态" align="center" width="120">
<template #default="scope">
<el-tag v-if="scope.row.existingScore" type="success" size="small">已录入</el-tag>
<el-tag v-else-if="scope.row.score" type="warning" size="small">待保存</el-tag>
<el-tag v-else type="info" size="small">未录入</el-tag>
<el-tag :type="getStudentStatusTagType(scope.row)" size="small">{{ getStudentStatus(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column v-if="columns[7].visible" key="lastScore" label="上月分数" align="center" width="100">
@@ -330,7 +328,7 @@ import { ArrowDown, InfoFilled } from '@element-plus/icons-vue';
import { globalHeaders } from '@/utils/request';
// API接口
import { conductScoreClassStudents, conductScoreBatchAdd } from '@/api/xg/conductScore';
import { conductScoreClassStudents, conductScoreBatchAdd, conductScoreBatchUpdate } from '@/api/xg/conductScore';
// 获取当前实例
const { proxy } = getCurrentInstance() as any;
@@ -506,6 +504,55 @@ const getStudentStatusType = (status) => {
}
};
// 获取学生录入状态
const getStudentStatus = (student) => {
const hasExistingScore = student.existingScore !== null && student.existingScore !== undefined;
const hasCurrentScore = student.score !== null && student.score !== undefined;
if (!hasExistingScore && !hasCurrentScore) {
return '未录入';
}
if (!hasExistingScore && hasCurrentScore) {
return '待保存';
}
if (hasExistingScore && !hasCurrentScore) {
return '已录入';
}
if (hasExistingScore && hasCurrentScore) {
// 比较分数和备注是否变化
const scoreChanged = Number(student.score) !== Number(student.existingScore.score);
const remarkChanged = String(student.remark || '').trim() !== String(student.existingScore.remark || '').trim();
if (scoreChanged || remarkChanged) {
return '待审核';
} else {
return '已录入';
}
}
return '未录入';
};
// 获取学生录入状态的标签类型
const getStudentStatusTagType = (student) => {
const status = getStudentStatus(student);
switch (status) {
case '未录入':
return 'info';
case '待保存':
return 'primary';
case '已录入':
return 'success';
case '待审核':
return 'warning';
default:
return 'info';
}
};
// 方法
const handleQuery = () => {
getList();
@@ -623,7 +670,7 @@ const getList = async () => {
// 批量操作方法
const handleBatchInput = () => {
// 打开批量录入弹窗(弹窗中输入分数和备注,保存时应用到所选学生)
// 批量录入:根据分数变化情况智能处理
console.log('handleBatchInput selectedStudents:', selectedStudents.value);
if (!selectedStudents.value || selectedStudents.value.length === 0) {
ElMessage.warning('请先选择要批量录入的学生');
@@ -634,30 +681,108 @@ const handleBatchInput = () => {
batchModalForm.score = null;
batchModalForm.remark = '';
batchModalVisible.value = true;
// debug info for users to see modal opening
ElMessage.info('已打开批量录入弹窗');
// 显示操作提示
ElMessage.info(`已选择 ${selectedStudents.value.length} 个学生进行批量录入。系统将根据分数变化情况自动判断是否需要审核。`);
};
const handleSaveAll = () => {
const validStudents = filteredStudents.value.filter(
(student) => student.score !== null && student.score !== undefined && student.existingScore === null
);
// 获取所有有分值的学生
const studentsWithScore = filteredStudents.value.filter((student) => student.score !== null && student.score !== undefined);
if (validStudents.length === 0) {
if (studentsWithScore.length === 0) {
ElMessage.warning('没有需要保存的数据');
return;
}
ElMessageBox.confirm(`确定要保存当前班级的 ${validStudents.length} 个学生的操行分吗?`, '保存确认', {
// 区分需要审核和不需要审核的学生
const directSaveStudents = []; // 直接保存的学生
const auditStudents = []; // 需要审核的学生
studentsWithScore.forEach((student) => {
const hasExistingScore = student.existingScore !== null && student.existingScore !== undefined;
if (!hasExistingScore) {
// 第一次录入:直接保存
directSaveStudents.push(student);
} else {
// 有历史记录:检查分数和备注是否都相同
// 确保数据类型一致进行比较
const currentScore = Number(student.score);
const originalScore = Number(student.existingScore.score);
const currentRemark = String(student.remark || '').trim();
const originalRemark = String(student.existingScore.remark || '').trim();
const scoreChanged = currentScore !== originalScore;
const remarkChanged = currentRemark !== originalRemark;
// 调试信息:检查每个学生的比较结果
console.log(`学生 ${student.studentName}:`, {
studentId: student.studentId,
hasExistingScore,
currentScore,
originalScore,
scoreChanged,
currentRemark: `"${currentRemark}"`,
originalRemark: `"${originalRemark}"`,
remarkChanged,
existingScore: student.existingScore,
studentRemark: student.remark,
existingScoreRemark: student.existingScore?.remark
});
if (!scoreChanged && !remarkChanged) {
// 分数和备注都没有变化:直接保存
directSaveStudents.push(student);
} else {
// 分数或备注发生了变化:需要审核
console.log(`学生 ${student.studentName} 需要审核,原因:`, {
scoreChanged,
remarkChanged,
reason: scoreChanged ? '分数变化' : remarkChanged ? '备注变化' : '未知'
});
auditStudents.push(student);
}
}
});
if (directSaveStudents.length === 0 && auditStudents.length === 0) {
ElMessage.warning('没有需要保存的数据');
return;
}
// 构建确认消息
let confirmMessage = '确定要';
const actions = [];
if (directSaveStudents.length > 0) {
actions.push(`保存 ${directSaveStudents.length} 个学生`);
}
if (auditStudents.length > 0) {
actions.push(`提交 ${auditStudents.length} 个修改学生审核`);
}
confirmMessage += actions.join(' 和 ') + '的操行分吗?';
if (auditStudents.length > 0) {
confirmMessage += '\n\n注意:分数或备注发生变化的学生需要管理员审核才能生效。';
}
ElMessageBox.confirm(confirmMessage, '保存确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
type: auditStudents.length > 0 ? 'warning' : 'success',
dangerouslyUseHTMLString: true
}).then(() => {
saveBatchData(validStudents);
// 分批处理直接保存和审核
if (directSaveStudents.length > 0) {
saveBatchData(directSaveStudents, 'add');
}
if (auditStudents.length > 0) {
saveBatchData(auditStudents, 'update');
}
});
};
const saveBatchData = async (students) => {
const saveBatchData = async (students, operationType = 'add') => {
try {
const batchData = {
academicYear: queryParams.academicYear,
@@ -671,18 +796,32 @@ const saveBatchData = async (students) => {
}))
};
const response = await conductScoreBatchAdd(batchData);
if (response.code === 200) {
ElMessage.success(`成功保存 ${response.data.successCount} 条记录`);
if (response.data.failCount > 0) {
ElMessage.warning(`失败 ${response.data.failCount} 条记录`);
let response;
if (operationType === 'update') {
// 修改操作:提交审核
response = await conductScoreBatchUpdate(batchData);
if (response.code === 200) {
ElMessage.success(`成功提交 ${response.data.successCount} 条审核申请`);
if (response.data.failCount > 0) {
ElMessage.warning(`失败 ${response.data.failCount} 条记录`);
}
}
} else {
// 新增操作:直接保存
response = await conductScoreBatchAdd(batchData);
if (response.code === 200) {
ElMessage.success(`成功保存 ${response.data.successCount} 条记录`);
if (response.data.failCount > 0) {
ElMessage.warning(`失败 ${response.data.failCount} 条记录`);
}
}
getList();
}
getList();
} catch (error) {
console.error('保存数据失败:', error);
ElMessage.error('保存失败');
const operationName = operationType === 'update' ? '提交审核' : '保存';
ElMessage.error(`${operationName}失败`);
}
};
@@ -703,7 +842,7 @@ const importUpload = reactive({
});
const confirmBatchModalSave = () => {
// 批量录入弹窗填写的分数/备注将应用到所有已勾选的学生(包含已录入的,视为覆盖)
// 批量录入弹窗:根据分数变化情况决定是否需要审核
const targets = selectedStudents.value || [];
if (targets.length === 0) {
@@ -717,15 +856,66 @@ const confirmBatchModalSave = () => {
return;
}
const studentsToSave = targets.map((stu) => ({
studentId: stu.studentId,
score: batchModalForm.score,
remark: batchModalForm.remark || ''
}));
// 区分需要审核和不需要审核的学生
const directSaveStudents = [];
const auditStudents = [];
targets.forEach((stu) => {
const hasExistingScore = stu.existingScore !== null && stu.existingScore !== undefined;
if (!hasExistingScore) {
// 第一次录入:直接保存
directSaveStudents.push(stu);
} else {
// 有历史记录:检查分数和备注是否都相同
const currentScore = Number(batchModalForm.score);
const originalScore = Number(stu.existingScore.score);
const currentRemark = String(batchModalForm.remark || '').trim();
const originalRemark = String(stu.existingScore.remark || '').trim();
const scoreChanged = currentScore !== originalScore;
const remarkChanged = currentRemark !== originalRemark;
if (!scoreChanged && !remarkChanged) {
// 分数和备注都没有变化:直接保存
directSaveStudents.push(stu);
} else {
// 分数或备注发生了变化:需要审核
auditStudents.push(stu);
}
}
});
batchModalVisible.value = false;
// 保存时会覆盖已有分数(后端应支持更新)
saveBatchData(studentsToSave);
// 分批处理
if (directSaveStudents.length > 0) {
const directSaveData = directSaveStudents.map((stu) => ({
studentId: stu.studentId,
score: batchModalForm.score,
remark: batchModalForm.remark || ''
}));
saveBatchData(directSaveData, 'add');
}
if (auditStudents.length > 0) {
const auditData = auditStudents.map((stu) => ({
studentId: stu.studentId,
score: batchModalForm.score,
remark: batchModalForm.remark || ''
}));
saveBatchData(auditData, 'update');
}
// 显示操作结果
let message = '批量操作完成:';
if (directSaveStudents.length > 0) {
message += `保存 ${directSaveStudents.length} 个学生`;
}
if (auditStudents.length > 0) {
message += `${directSaveStudents.length > 0 ? ',' : ''}提交 ${auditStudents.length} 个修改学生审核`;
}
ElMessage.success(message);
};
const handleRefresh = () => {
@@ -733,30 +923,66 @@ const handleRefresh = () => {
};
const handleSingleInput = (row) => {
// 单条保存:支持覆盖已录入的分数。根据是否已有分数,调整提示文案。
// 单条保存:根据分数和备注变化情况决定是否需要审核
if (row.score === null || row.score === undefined) {
ElMessage.warning('请先输入分数');
return;
}
const confirmMessage = row.existingScore
? `该学生已有分数,确定要覆盖 ${row.studentName} 的分数为 ${row.score} 吗?`
: `确定保存 ${row.studentName} 的分数 ${row.score} 吗?`;
const hasExistingScore = row.existingScore !== null && row.existingScore !== undefined;
ElMessageBox.confirm(confirmMessage, '保存确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 传入简化对象以避免多余字段
saveBatchData([
{
studentId: row.studentId,
score: row.score,
remark: row.remark || ''
}
]);
});
if (!hasExistingScore) {
// 第一次录入:直接保存
const confirmMessage = `确定保存 ${row.studentName} 的分数 ${row.score} 吗?`;
ElMessageBox.confirm(confirmMessage, '保存确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'success'
}).then(() => {
saveBatchData(
[
{
studentId: row.studentId,
score: row.score,
remark: row.remark || ''
}
],
'add'
);
});
} else {
// 有历史记录:检查分数和备注是否都相同
const currentScore = Number(row.score);
const originalScore = Number(row.existingScore.score);
const currentRemark = String(row.remark || '').trim();
const originalRemark = String(row.existingScore.remark || '').trim();
const scoreChanged = currentScore !== originalScore;
const remarkChanged = currentRemark !== originalRemark;
const needsAudit = scoreChanged || remarkChanged;
const operationName = needsAudit ? '修改审核' : '保存';
const confirmMessage = needsAudit
? `该学生${scoreChanged ? '分数' : ''}${scoreChanged && remarkChanged ? '和' : ''}${remarkChanged ? '备注' : ''}发生变化,修改后需要管理员审核。确定要提交 ${row.studentName} 的修改吗?`
: `该学生信息没有变化,确定要保存吗?`;
ElMessageBox.confirm(confirmMessage, `${operationName}确认`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: needsAudit ? 'warning' : 'success'
}).then(() => {
saveBatchData(
[
{
studentId: row.studentId,
score: row.score,
remark: row.remark || ''
}
],
needsAudit ? 'update' : 'add'
);
});
}
};
const handleSelectionChange = (selection) => {