This commit is contained in:
zlwu committed 2025-12-25 18:01:27 +08:00
1 parent fb6d0aa63c
commit ff1228873c
37 files changed
+11861 -1823

No files matched your search

@@ -0,0 +1,104 @@
package org.dromara.xg.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.xg.domain.vo.XgStudentClassChangeVo;
import org.dromara.xg.domain.bo.XgStudentClassChangeBo;
import org.dromara.xg.service.IXgStudentClassChangeService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
/**
* 学生换班级
*
* @author AI Assistant
* @date 2025-01-25
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/studentMsg/studentClassChange")
public class XgStudentClassChangeController extends BaseController {
private final IXgStudentClassChangeService xgStudentClassChangeService;
/**
* 查询学生换班级列表
*/
// @SaCheckPermission("studentMsg:studentClassChange:list")
@GetMapping("/list")
public TableDataInfo<XgStudentClassChangeVo> list(XgStudentClassChangeBo bo, PageQuery pageQuery) {
return xgStudentClassChangeService.queryPageList(bo, pageQuery);
}
/**
* 导出学生换班级列表
*/
// @SaCheckPermission("studentMsg:studentClassChange:export")
@Log(title = "学生换班级", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(XgStudentClassChangeBo bo, HttpServletResponse response) {
List<XgStudentClassChangeVo> list = xgStudentClassChangeService.queryList(bo);
ExcelUtil.exportExcel(list, "学生换班级", XgStudentClassChangeVo.class, response);
}
/**
* 获取学生换班级详细信息
*
* @param id 主键
*/
// @SaCheckPermission("studentMsg:studentClassChange:query")
@GetMapping("/{id}")
public R<XgStudentClassChangeVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgStudentClassChangeService.queryById(id));
}
/**
* 新增学生换班级
*/
// @SaCheckPermission("studentMsg:studentClassChange:add")
@Log(title = "学生换班级", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody XgStudentClassChangeBo bo) {
return toAjax(xgStudentClassChangeService.insertByBo(bo));
}
/**
* 修改学生换班级
*/
// @SaCheckPermission("studentMsg:studentClassChange:edit")
@Log(title = "学生换班级", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XgStudentClassChangeBo bo) {
return toAjax(xgStudentClassChangeService.updateByBo(bo));
}
/**
* 删除学生换班级
*
* @param ids 主键串
*/
// @SaCheckPermission("studentMsg:studentClassChange:remove")
@Log(title = "学生换班级", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(xgStudentClassChangeService.deleteWithValidByIds(List.of(ids), true));
}
}
@@ -0,0 +1,146 @@
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 java.io.Serial;
/**
* 学生换班级对象 xg_student_class_change
*
* @author AI Assistant
* @date 2025-01-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("xg_student_class_change")
public class XgStudentClassChange extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID(雪花ID)
*/
@TableId(value = "id")
private Long id;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic(value = "0", delval = "1")
private Long deleted;
/**
* 学生ID,关联xg_student_msg.id
*/
private Long studentId;
/**
* 学号
*/
private String studentNo;
/**
* 学生姓名
*/
private String studentName;
/**
* 原学院ID
*/
private Long originalCollegeId;
/**
* 原学院名称
*/
private String originalCollegeName;
/**
* 原专业ID
*/
private Long originalMajorId;
/**
* 原专业名称
*/
private String originalMajorName;
/**
* 原班级ID
*/
private Long originalClassId;
/**
* 原班级代码
*/
private String originalClassCode;
/**
* 原班级名称
*/
private String originalClassName;
/**
* 新学院ID
*/
private Long newCollegeId;
/**
* 新学院名称
*/
private String newCollegeName;
/**
* 新专业ID
*/
private Long newMajorId;
/**
* 新专业名称
*/
private String newMajorName;
/**
* 新班级ID
*/
private Long newClassId;
/**
* 新班级代码
*/
private String newClassCode;
/**
* 新班级名称
*/
private String newClassName;
/**
* 换班级原因
*/
private String changeReason;
/**
* 换班级类型:1-主动申请,2-学校调整,3-专业调整,4-其他
*/
private String changeType;
/**
* 换班级时间
*/
private Date changeTime;
/**
* 生效时间
*/
private Date effectiveTime;
/**
* 处理状态:0-未处理,1-已处理
*/
private String status;
}
@@ -0,0 +1,157 @@
package org.dromara.xg.domain.bo;
import org.dromara.xg.domain.XgStudentClassChange;
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;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* 学生换班级业务对象 xg_student_class_change
*
* @author AI Assistant
* @date 2025-01-25
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = XgStudentClassChange.class, reverseConvertGenerate = false)
public class XgStudentClassChangeBo extends BaseEntity {
/**
* 主键ID(雪花ID)
*/
@NotNull(message = "主键ID不能为空", groups = { EditGroup.class })
private Long id;
/**
* 学生ID,关联xg_student_msg.id
*/
@NotNull(message = "学生ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long studentId;
/**
* 学号
*/
private String studentNo;
/**
* 学生姓名
*/
private String studentName;
/**
* 原学院ID
*/
private Long originalCollegeId;
/**
* 原学院名称
*/
private String originalCollegeName;
/**
* 原专业ID
*/
private Long originalMajorId;
/**
* 原专业名称
*/
private String originalMajorName;
/**
* 原班级ID
*/
private Long originalClassId;
/**
* 原班级代码
*/
private String originalClassCode;
/**
* 原班级名称
*/
private String originalClassName;
/**
* 新学院ID
*/
@NotNull(message = "新学院不能为空", groups = { AddGroup.class, EditGroup.class })
private Long newCollegeId;
/**
* 新学院名称
*/
private String newCollegeName;
/**
* 新专业ID
*/
@NotNull(message = "新专业不能为空", groups = { AddGroup.class, EditGroup.class })
private Long newMajorId;
/**
* 新专业名称
*/
private String newMajorName;
/**
* 新班级ID
*/
@NotNull(message = "新班级不能为空", groups = { AddGroup.class, EditGroup.class })
private Long newClassId;
/**
* 新班级代码
*/
private String newClassCode;
/**
* 新班级名称
*/
private String newClassName;
/**
* 换班级原因
*/
@NotBlank(message = "换班级原因不能为空", groups = { AddGroup.class, EditGroup.class })
private String changeReason;
/**
* 换班级类型:1-主动申请,2-学校调整,3-专业调整,4-其他
*/
private String changeType;
/**
* 换班级时间
*/
private Date changeTime;
/**
* 生效时间
*/
@NotNull(message = "生效时间不能为空", groups = { AddGroup.class, EditGroup.class })
private Date effectiveTime;
/**
* 处理状态:0-未处理,1-已处理
*/
private String status;
/**
* 创建部门
*/
private Long createDept;
/**
* 备注信息
*/
private String remark;
}
@@ -0,0 +1,122 @@
package org.dromara.xg.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.dromara.xg.domain.XgStudentClassChange;
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 java.io.Serial;
import java.io.Serializable;
/**
* 学生换班级视图对象 xg_student_class_change
*
* @author AI Assistant
* @date 2025-01-25
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = XgStudentClassChange.class)
public class XgStudentClassChangeVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID(雪花ID)
*/
@ExcelProperty(value = "主键ID")
private Long id;
/**
* 学生ID,关联xg_student_msg.id
*/
@ExcelProperty(value = "学生ID")
private Long studentId;
/**
* 学号
*/
@ExcelProperty(value = "学号")
private String studentNo;
/**
* 学生姓名
*/
@ExcelProperty(value = "学生姓名")
private String studentName;
/**
* 原学院名称
*/
@ExcelProperty(value = "原学院名称")
private String originalCollegeName;
/**
* 原专业名称
*/
@ExcelProperty(value = "原专业名称")
private String originalMajorName;
/**
* 原班级名称
*/
@ExcelProperty(value = "原班级名称")
private String originalClassName;
/**
* 新学院名称
*/
@ExcelProperty(value = "新学院名称")
private String newCollegeName;
/**
* 新专业名称
*/
@ExcelProperty(value = "新专业名称")
private String newMajorName;
/**
* 新班级名称
*/
@ExcelProperty(value = "新班级名称")
private String newClassName;
/**
* 换班级原因
*/
@ExcelProperty(value = "换班级原因")
private String changeReason;
/**
* 生效时间
*/
@ExcelProperty(value = "生效时间")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date effectiveTime;
/**
* 处理状态:0-未处理,1-已处理
*/
@ExcelProperty(value = "处理状态")
private String status;
/**
* 创建者
*/
@ExcelProperty(value = "创建者")
private Long createBy;
/**
* 创建时间
*/
@ExcelProperty(value = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -0,0 +1,15 @@
package org.dromara.xg.mapper;
import org.dromara.xg.domain.XgStudentClassChange;
import org.dromara.xg.domain.vo.XgStudentClassChangeVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* 学生换班级Mapper接口
*
* @author AI Assistant
* @date 2025-01-25
*/
public interface XgStudentClassChangeMapper extends BaseMapperPlus<XgStudentClassChange, XgStudentClassChangeVo> {
}
@@ -0,0 +1,69 @@
package org.dromara.xg.service;
import org.dromara.xg.domain.vo.XgStudentClassChangeVo;
import org.dromara.xg.domain.bo.XgStudentClassChangeBo;
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 AI Assistant
* @date 2025-01-25
*/
public interface IXgStudentClassChangeService {
/**
* 查询学生换班级
*
* @param id 主键
* @return 学生换班级
*/
XgStudentClassChangeVo queryById(Long id);
/**
* 分页查询学生换班级列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 学生换班级分页列表
*/
TableDataInfo<XgStudentClassChangeVo> queryPageList(XgStudentClassChangeBo bo, PageQuery pageQuery);
/**
* 查询符合条件的学生换班级列表
*
* @param bo 查询条件
* @return 学生换班级列表
*/
List<XgStudentClassChangeVo> queryList(XgStudentClassChangeBo bo);
/**
* 新增学生换班级
*
* @param bo 学生换班级
* @return 是否新增成功
*/
Boolean insertByBo(XgStudentClassChangeBo bo);
/**
* 修改学生换班级
*
* @param bo 学生换班级
* @return 是否修改成功
*/
Boolean updateByBo(XgStudentClassChangeBo bo);
/**
* 校验并批量删除学生换班级信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
}
@@ -0,0 +1,126 @@
package org.dromara.xg.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
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.xg.domain.XgStudentClassChange;
import org.dromara.xg.domain.bo.XgStudentClassChangeBo;
import org.dromara.xg.domain.vo.XgStudentClassChangeVo;
import org.dromara.xg.mapper.XgStudentClassChangeMapper;
import org.dromara.xg.service.IXgStudentClassChangeService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 学生换班级Service业务层处理
*
* @author AI Assistant
* @date 2025-01-25
*/
@RequiredArgsConstructor
@Service
public class XgStudentClassChangeServiceImpl implements IXgStudentClassChangeService {
private final XgStudentClassChangeMapper baseMapper;
/**
* 查询学生换班级
*/
@Override
public XgStudentClassChangeVo queryById(Long id){
return baseMapper.selectVoById(id);
}
/**
* 分页查询学生换班级列表
*/
@Override
public TableDataInfo<XgStudentClassChangeVo> queryPageList(XgStudentClassChangeBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<XgStudentClassChange> lqw = buildQueryWrapper(bo);
Page<XgStudentClassChangeVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询符合条件的学生换班级列表
*/
@Override
public List<XgStudentClassChangeVo> queryList(XgStudentClassChangeBo bo) {
LambdaQueryWrapper<XgStudentClassChange> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<XgStudentClassChange> buildQueryWrapper(XgStudentClassChangeBo bo) {
LambdaQueryWrapper<XgStudentClassChange> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getStudentId() != null, XgStudentClassChange::getStudentId, bo.getStudentId());
lqw.like(StringUtils.isNotBlank(bo.getStudentNo()), XgStudentClassChange::getStudentNo, bo.getStudentNo());
lqw.like(StringUtils.isNotBlank(bo.getStudentName()), XgStudentClassChange::getStudentName, bo.getStudentName());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), XgStudentClassChange::getStatus, bo.getStatus());
lqw.orderByDesc(XgStudentClassChange::getCreateTime);
return lqw;
}
/**
* 新增学生换班级
*/
@Override
public Boolean insertByBo(XgStudentClassChangeBo bo) {
// 设置换班级时间为当前时间(如果未设置)
if (bo.getChangeTime() == null) {
bo.setChangeTime(new java.util.Date());
}
// 设置状态为未处理
if (StringUtils.isBlank(bo.getStatus())) {
bo.setStatus("0");
}
XgStudentClassChange add = BeanUtil.toBean(bo, XgStudentClassChange.class);
validEntityBeforeSave(add);
return baseMapper.insert(add) > 0;
}
/**
* 修改学生换班级
*/
@Override
public Boolean updateByBo(XgStudentClassChangeBo bo) {
XgStudentClassChange update = BeanUtil.toBean(bo, XgStudentClassChange.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(XgStudentClassChange entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除学生换班级
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
// 检查是否包含已处理的记录
LambdaQueryWrapper<XgStudentClassChange> lqw = Wrappers.lambdaQuery();
lqw.in(XgStudentClassChange::getId, ids);
lqw.eq(XgStudentClassChange::getStatus, "1"); // 已处理状态
if (baseMapper.selectCount(lqw) > 0) {
throw new ServiceException("已处理的换班级记录不能删除");
}
}
return baseMapper.deleteByIds(ids) > 0;
}
}
@@ -0,0 +1,7 @@
<?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.XgStudentClassChangeMapper">
</mapper>
+1
View File
@@ -15,6 +15,7 @@
- [宿舍管理API](./宿舍管理API.md)
- [宿舍房间管理API](./宿舍房间管理API.md) - **新增床位详细信息查询接口**(2025-01-27)
- [宿舍床位分配管理API](./宿舍床位分配管理API.md) - **重要修复:床位分配不完整问题**(2025-01-27)
- [宿舍申请管理API](./宿舍申请管理API.md) - **新增**:支持学生调宿和退宿申请管理(2025-01-15)
- [宿舍管理员信息API](./宿舍管理员信息API.md)
- [宿舍管理员授权API](./宿舍管理员授权API.md)
- [学生管理员授权关系API](./学生管理员授权关系API.md)
@@ -0,0 +1,693 @@
# 宿舍申请管理 API 接口文档
### 模块概述
**模块名称**: 宿舍申请管理 / Dormitory Application Management
**基础路径**: `/dormApplication/dormApplication`
**版本**: v1.0
**负责人**: zlwu
**更新时间**: 2025-01-15
### 模块功能
宿舍申请管理模块主要处理学生的宿舍调宿和退宿申请,包括:
- 学生宿舍调宿申请(更换床位)
- 学生宿舍退宿申请(释放床位)
- 工作流审批流程
- 宿舍管理员权限控制
- 学生信息自动更新
### 权限说明
- **权限前缀**: `dormApplication:dormApplication:`
- **权限列表**:
- `dormApplication:dormApplication:list` - 查询权限
- `dormApplication:dormApplication:add` - 新增权限
- `dormApplication:dormApplication:edit` - 编辑权限
- `dormApplication:dormApplication:remove` - 删除权限
- `dormApplication:dormApplication:query` - 查看详情权限
- `dormApplication:dormApplication:export` - 导出权限
---
## 📊 接口概览
### 接口分类
#### 基础 CRUD 接口
| 接口名称 | HTTP 方法 | 路径 | 权限要求 | 功能描述 |
| -------------- | --------- | --------------------- | ----------------- | ---------------------- |
| 查询申请列表 | GET | `/dormApplication/dormApplication/list` | `dormApplication:dormApplication:list` | 分页查询宿舍申请列表 |
| 查询申请详情 | GET | `/dormApplication/dormApplication/{id}` | `dormApplication:dormApplication:query` | 根据 ID 查询申请详情 |
| 新增申请 | POST | `/dormApplication/dormApplication` | `dormApplication:dormApplication:add` | 创建新的宿舍申请 |
| 修改申请 | PUT | `/dormApplication/dormApplication` | `dormApplication:dormApplication:edit` | 更新申请信息 |
| 删除申请 | DELETE | `/dormApplication/dormApplication/{ids}` | `dormApplication:dormApplication:remove` | 批量删除申请 |
| 导出申请 | POST | `/dormApplication/dormApplication/export` | `dormApplication:dormApplication:export` | 导出申请列表 |
#### 业务逻辑接口
| 接口名称 | HTTP 方法 | 路径 | 权限要求 | 功能描述 |
| ------------ | --------- | ------------------------- | -------- | ---------- |
| 获取学生选择器 | GET | `/dormApplication/dormApplication/studentOptions` | `dormApplication:dormApplication:list` | 获取学生选择器选项 |
| 获取学院审批人 | GET | `/dormApplication/dormApplication/getCollegeApprovers` | `dormApplication:dormApplication:list` | 获取二级学院审批人列表 |
| 获取学工处审批人 | GET | `/dormApplication/dormApplication/getStudentAffairsApprovers` | `dormApplication:dormApplication:list` | 获取学工处审批人列表 |
### 通用响应格式
#### 成功响应
```json
{
"code": 200,
"msg": "操作成功",
"data": {
// 响应数据
}
}
```
#### 分页响应
```json
{
"code": 200,
"msg": "查询成功",
"rows": [
// 数据列表
],
"total": 100
}
```
#### 错误响应
```json
{
"code": 400001,
"msg": "参数错误",
"data": null
}
```
---
## 🔍 接口详细说明
### 1. 查询宿舍申请列表
#### 接口信息
**接口地址**: `GET /dormApplication/dormApplication/list`
**接口描述**: 分页查询宿舍申请列表,支持多条件筛选和权限过滤
**权限要求**: `dormApplication:dormApplication:list`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| --------- | ------- | ---- | -------------------- | ------------ |
| pageNum | Integer | N | 页码,默认 1 | 1 |
| pageSize | Integer | N | 每页大小,默认 10 | 10 |
| applicationName | String | N | 申请名称 | "张三调宿申请" |
| applicationType | String | N | 申请类型:1-调宿,2-退宿 | "1" |
| applicationStatus | String | N | 审批状态 | "draft" |
| studentName | String | N | 学生姓名 | "张三" |
| studentNo | String | N | 学号 | "20240001" |
| collegeName | String | N | 学院名称 | "计算机学院" |
| majorName | String | N | 专业名称 | "软件工程" |
| className | String | N | 班级名称 | "软工2024班" |
| applyTime | String | N | 申请时间 | "2025-01-15" |
#### 请求示例
```bash
curl -X GET "http://localhost:8080/dormApplication/dormApplication/list?pageNum=1&pageSize=10&applicationType=1" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json"
```
#### 响应示例
```json
{
"code": 200,
"msg": "查询成功",
"rows": [
{
"id": 1,
"applicationName": "张三调宿申请",
"applicationType": "1",
"applicationStatus": "draft",
"studentName": "张三",
"studentNo": "20240001",
"collegeName": "计算机学院",
"majorName": "软件工程",
"className": "软工2024班",
"applyTime": "2025-01-15 10:00:00",
"createBy": 1,
"createTime": "2025-01-15 10:00:00"
}
],
"total": 1
}
```
#### 错误码
| 错误码 | 说明 | 解决方案 |
| ------ | -------- | -------------------- |
| 401001 | 未登录 | 请先登录获取 token |
| 403001 | 无权限 | 请联系管理员分配权限 |
| 400001 | 参数错误 | 检查请求参数格式 |
---
### 2. 查询申请详情
#### 接口信息
**接口地址**: `GET /dormApplication/dormApplication/{id}`
**接口描述**: 根据 ID 查询宿舍申请的详细信息
**权限要求**: `dormApplication:dormApplication:query`
#### 路径参数
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| ------ | ---- | ---- | -------- | ------ |
| id | Long | Y | 申请单ID | 1 |
#### 请求示例
```bash
curl -X GET "http://localhost:8080/dormApplication/dormApplication/1" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json"
```
#### 响应示例
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"id": 1,
"applicationName": "张三调宿申请",
"applicationType": "1",
"applicationStatus": "draft",
"studentId": 1001,
"studentName": "张三",
"studentNo": "20240001",
"collegeId": 101,
"collegeName": "计算机学院",
"majorId": 201,
"majorName": "软件工程",
"classId": 301,
"className": "软工2024班",
"currentCampusId": 1,
"currentCampusName": "主校区",
"currentBuildingId": 11,
"currentBuildingName": "1号楼",
"currentRoomId": 111,
"currentRoomNo": "101",
"currentBedId": 1111,
"currentBedNo": "01",
"targetCampusId": 1,
"targetCampusName": "主校区",
"targetBuildingId": 12,
"targetBuildingName": "2号楼",
"targetRoomId": 121,
"targetRoomNo": "201",
"targetBedId": 1211,
"targetBedNo": "01",
"reasonDetail": "宿舍环境不好,希望调换",
"applyTime": "2025-01-15 10:00:00",
"remark": "备注信息"
}
}
```
#### 错误码
| 错误码 | 说明 | 解决方案 |
| ------ | ---------- | -------------------- |
| 404001 | 数据不存在 | 检查 ID 是否正确 |
| 401001 | 未登录 | 请先登录获取 token |
| 403001 | 无权限 | 请联系管理员分配权限 |
---
### 3. 新增宿舍申请
#### 接口信息
**接口地址**: `POST /dormApplication/dormApplication`
**接口描述**: 创建新的宿舍申请记录(调宿或退宿)
**权限要求**: `dormApplication:dormApplication:add`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| -------- | ------- | ---- | ---------------------------- | ---------- |
| applicationName | String | Y | 申请名称 | "张三调宿申请" |
| applicationType | String | Y | 申请类型:1-调宿,2-退宿 | "1" |
| studentId | Long | Y | 申请学生ID | 1001 |
| studentName | String | Y | 学生姓名 | "张三" |
| studentNo | String | Y | 学号 | "20240001" |
| collegeId | Long | Y | 学院ID | 101 |
| majorId | Long | Y | 专业ID | 201 |
| classId | Long | Y | 班级ID | 301 |
| currentCampusId | Long | N | 当前校区ID(调宿必填) | 1 |
| currentBuildingId | Long | N | 当前楼栋ID(调宿必填) | 11 |
| currentRoomId | Long | N | 当前房间ID(调宿必填) | 111 |
| currentBedId | Long | Y | 当前床位ID | 1111 |
| targetCampusId | Long | N | 目标校区ID(调宿必填) | 1 |
| targetBuildingId | Long | N | 目标楼栋ID(调宿必填) | 12 |
| targetRoomId | Long | N | 目标房间ID(调宿必填) | 121 |
| targetBedId | Long | N | 目标床位ID(调宿必填) | 1211 |
| reasonDetail | String | Y | 详细原因说明 | "宿舍环境不好" |
| applyTime | String | Y | 申请时间 | "2025-01-15 10:00:00" |
| attachmentOssIds | String | N | 附件OSS ID列表 | "oss1,oss2" |
| remark | String | N | 备注信息 | "备注内容" |
#### 请求体
```json
{
"applicationName": "张三调宿申请",
"applicationType": "1",
"studentId": 1001,
"studentName": "张三",
"studentNo": "20240001",
"collegeId": 101,
"majorId": 201,
"classId": 301,
"currentCampusId": 1,
"currentBuildingId": 11,
"currentRoomId": 111,
"currentBedId": 1111,
"targetCampusId": 1,
"targetBuildingId": 12,
"targetRoomId": 121,
"targetBedId": 1211,
"reasonDetail": "宿舍环境不好,希望调换到更好的宿舍",
"applyTime": "2025-01-15 10:00:00",
"remark": "备注信息"
}
```
#### 请求示例
```bash
curl -X POST "http://localhost:8080/dormApplication/dormApplication" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"applicationName": "张三调宿申请",
"applicationType": "1",
"studentId": 1001,
"studentName": "张三",
"studentNo": "20240001",
"collegeId": 101,
"majorId": 201,
"classId": 301,
"currentCampusId": 1,
"currentBuildingId": 11,
"currentRoomId": 111,
"currentBedId": 1111,
"targetCampusId": 1,
"targetBuildingId": 12,
"targetRoomId": 121,
"targetBedId": 1211,
"reasonDetail": "宿舍环境不好,希望调换到更好的宿舍",
"applyTime": "2025-01-15 10:00:00"
}'
```
#### 响应示例
```json
{
"code": 200,
"msg": "新增成功",
"data": {
"id": 3
}
}
```
#### 错误码
| 错误码 | 说明 | 解决方案 |
| ------ | ---------- | -------------------- |
| 400001 | 参数错误 | 检查必填参数是否缺失 |
| 400002 | 数据已存在 | 检查唯一字段是否重复 |
| 401001 | 未登录 | 请先登录获取 token |
| 403001 | 无权限 | 请联系管理员分配权限 |
#### 参数验证规则
- `applicationName`: 长度 2-100 字符,不能为空
- `applicationType`: 只能为 "1" 或 "2"
- `studentId`: 必须为正整数
- `studentName`: 长度 2-50 字符,不能为空
- `studentNo`: 长度 6-20 字符,不能为空
- `reasonDetail`: 长度 5-500 字符,不能为空
- `applyTime`: 必须为有效的时间格式
---
### 4. 修改宿舍申请
#### 接口信息
**接口地址**: `PUT /dormApplication/dormApplication`
**接口描述**: 更新宿舍申请信息(仅限草稿状态的申请)
**权限要求**: `dormApplication:dormApplication:edit`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| -------- | ------- | ---- | ---------------------------- | ---------- |
| id | Long | Y | 申请单ID | 1 |
| applicationName | String | Y | 申请名称 | "张三调宿申请" |
| applicationType | String | Y | 申请类型:1-调宿,2-退宿 | "1" |
| studentId | Long | Y | 申请学生ID | 1001 |
| reasonDetail | String | Y | 详细原因说明 | "宿舍环境不好" |
| targetBedId | Long | N | 目标床位ID(调宿时必填) | 1211 |
| remark | String | N | 备注信息 | "修改后的备注" |
#### 请求体
```json
{
"id": 1,
"applicationName": "张三调宿申请(修改)",
"applicationType": "1",
"studentId": 1001,
"reasonDetail": "宿舍环境不好,希望调换到更好的宿舍(修改原因)",
"targetBedId": 1211,
"remark": "修改后的备注"
}
```
#### 请求示例
```bash
curl -X PUT "http://localhost:8080/dormApplication/dormApplication" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"id": 1,
"applicationName": "张三调宿申请(修改)",
"reasonDetail": "宿舍环境不好,希望调换到更好的宿舍(修改原因)",
"targetBedId": 1211
}'
```
#### 响应示例
```json
{
"code": 200,
"msg": "修改成功",
"data": null
}
```
#### 错误码
| 错误码 | 说明 | 解决方案 |
| ------ | ---------- | -------------------- |
| 404001 | 数据不存在 | 检查 ID 是否正确 |
| 400001 | 参数错误 | 检查参数格式是否正确 |
| 403002 | 状态不允许 | 只能修改草稿状态的申请 |
| 401001 | 未登录 | 请先登录获取 token |
| 403001 | 无权限 | 请联系管理员分配权限 |
---
### 5. 删除宿舍申请
#### 接口信息
**接口地址**: `DELETE /dormApplication/dormApplication/{ids}`
**接口描述**: 批量删除宿舍申请(仅限草稿状态的申请)
**权限要求**: `dormApplication:dormApplication:remove`
#### 路径参数
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| ------ | ------ | ---- | ----------------------------- | ------- |
| ids | String | Y | 申请单ID 列表,多个用逗号分隔 | "1,2,3" |
#### 请求示例
```bash
curl -X DELETE "http://localhost:8080/dormApplication/dormApplication/1,2,3" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json"
```
#### 响应示例
```json
{
"code": 200,
"msg": "删除成功",
"data": null
}
```
#### 错误码
| 错误码 | 说明 | 解决方案 |
| ------ | ---------- | -------------------- |
| 400001 | 参数错误 | 检查 ID 格式是否正确 |
| 403003 | 状态不允许 | 只能删除草稿状态的申请 |
| 401001 | 未登录 | 请先登录获取 token |
| 403001 | 无权限 | 请联系管理员分配权限 |
---
## 🎯 业务逻辑接口
### 1. 获取学生选择器选项
#### 接口信息
**接口地址**: `GET /dormApplication/dormApplication/studentOptions`
**接口描述**: 获取学生选择器选项列表,支持按学号、姓名检索
**权限要求**: `dormApplication:dormApplication:list`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| -------- | ------- | ---- | -------------------- | ------------ |
| pageNum | Integer | N | 页码,默认 1 | 1 |
| pageSize | Integer | N | 每页大小,默认 10 | 10 |
| studentName | String | N | 学生姓名 | "张三" |
| studentNo | String | N | 学号 | "20240001" |
#### 请求示例
```bash
curl -X GET "http://localhost:8080/dormApplication/dormApplication/studentOptions?pageNum=1&pageSize=10&studentName=张三" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json"
```
#### 响应示例
```json
{
"code": 200,
"msg": "查询成功",
"rows": [
{
"studentId": 1001,
"studentName": "张三",
"studentNo": "20240001",
"collegeName": "计算机学院",
"majorName": "软件工程",
"className": "软工2024班"
}
],
"total": 1
}
```
---
### 2. 获取二级学院审批人列表
#### 接口信息
**接口地址**: `GET /dormApplication/dormApplication/getCollegeApprovers`
**接口描述**: 根据学院ID获取二级学院审批人列表
**权限要求**: `dormApplication:dormApplication:list`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
| -------- | ------- | ---- | -------------------- | ------------ |
| collegeId | Long | Y | 学院ID | 101 |
#### 请求示例
```bash
curl -X GET "http://localhost:8080/dormApplication/dormApplication/getCollegeApprovers?collegeId=101" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json"
```
#### 响应示例
```json
{
"code": 200,
"msg": "查询成功",
"data": [
{
"userId": 201,
"userName": "lisi",
"nickName": "李四",
"deptId": 101
}
]
}
```
---
### 3. 获取学工处审批人列表
#### 接口信息
**接口地址**: `GET /dormApplication/dormApplication/getStudentAffairsApprovers`
**接口描述**: 获取学工处审批人列表
**权限要求**: `dormApplication:dormApplication:list`
#### 请求示例
```bash
curl -X GET "http://localhost:8080/dormApplication/dormApplication/getStudentAffairsApprovers" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json"
```
#### 响应示例
```json
{
"code": 200,
"msg": "查询成功",
"data": [
{
"userId": 301,
"userName": "wangwu",
"nickName": "王五",
"deptId": 999
}
]
}
```
---
## 📋 数据字典
### 申请类型枚举
| 值 | 名称 | 说明 |
| --- | ---- | -------- |
| 1 | 调宿申请 | 学生申请更换床位 |
| 2 | 退宿申请 | 学生申请退宿 |
### 申请状态枚举
| 值 | 名称 | 说明 |
| ------ | ---- | -------- |
| draft | 草稿 | 申请已创建,待提交 |
| waiting | 待审核 | 申请已提交,等待审批 |
| finish | 已完成 | 审批通过,操作已执行 |
| cancel | 已撤销 | 申请被撤销 |
| back | 已退回 | 审批退回 |
| invalid | 已作废 | 申请已作废 |
| termination | 已终止 | 流程已终止 |
### 学生状态枚举
| 值 | 名称 | 说明 |
| --- | ---- | -------- |
| 1 | 在校生 | 正常在校学生 |
| 2 | 休学 | 学生休学状态 |
| 3 | 退学 | 学生退学状态 |
| 4 | 毕业 | 学生已毕业 |
---
## 🔄 版本历史
### v1.0.0 - 2025-01-15
- ✅ 初始版本发布
- ✅ 完成宿舍申请基础 CRUD 接口
- ✅ 完成学生选择器接口
- ✅ 完成审批人获取接口
- ✅ 集成工作流审批流程
- ✅ 实现调宿和退宿业务逻辑
---
## 📖 相关文档
- **数据库设计**:[数据库设计文档](../设计文档/database/)
- **业务需求**:[业务需求文档](../需求文档/)
- **工作流文档**:[工作流开发规范](../../.cursor/cursorrules-workflow.md)
- **退宿逻辑说明**:[退宿逻辑详细说明](./退宿逻辑详细说明.md)
- **API基础规范**:[API文档基础规范](./API文档基础规范.md)
---
## 📞 联系方式
**开发团队**:校园管理系统开发团队
**负责人**:zlwu
**邮箱**:dev@hzdyjs.edu.cn
**文档维护**:实时更新,如有问题请及时反馈
---
**📌 重要提醒**:
1. 所有接口都需要先登录获取 token
2. 请求头必须包含 `Authorization: Bearer {token}`
3. 参数验证失败会返回详细的错误信息
4. 生产环境请使用 HTTPS 协议
5. 申请状态为非草稿状态时,只能查看不能修改或删除
6. 退宿和调宿操作通过工作流审批完成后自动执行,无需手动调用业务接口
@@ -0,0 +1,175 @@
# 学生退宿逻辑详细说明
## 概述
本文档详细描述了学生宿舍管理系统中退宿操作的具体实现逻辑,包括前端界面、后端服务调用、数据库操作等各个层面的详细说明。
## 业务流程
### 1. 前端界面操作
**文件位置**: `web/src/views/xg/xsjcxx/xssssq/index.vue`
学生可以通过前端界面提交退宿申请:
- **申请类型**: `applicationType = "2"` (退宿申请)
- **申请状态**: 初始状态为 `draft` (草稿)
- **关键字段**:
- `currentBedId`: 当前床位ID
- `studentId`: 学生ID
- `classId`: 班级ID
### 2. 审批流程
退宿申请需要通过工作流审批,流程代码:`hzdyjs-tsts`
审批完成后,当 `applicationStatus = "finish"` 时,触发实际的退宿操作。
## 核心退宿逻辑
### 方法调用链
```java
// 1. 工作流监听器触发
@EventListener(condition = "#processEvent.flowCode=='hzdyjs-tsts'")
public void processHandler(ProcessEvent processEvent) {
// 当审批完成时,执行调宿/退宿操作
if ("finish".equals(processEvent.getStatus())) {
executeApplicationOperation(xgDormApplication);
}
}
// 2. 执行申请操作
private void executeApplicationOperation(XgDormApplication application) {
if ("2".equals(application.getApplicationType())) {
// 退宿申请 - 调用退宿接口
List<Long> bedIds = List.of(application.getCurrentBedId());
Boolean stayawayResult = xgDormBedStudentService.classTeacherStayawaySelectedBeds(
bedIds, // 床位ID列表
application.getClassId() // 班级ID
);
}
}
// 3. 实际退宿实现
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean classTeacherStayawaySelectedBeds(List<Long> bedIds, Long classId)
```
### 详细SQL操作
**文件位置**: `backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgDormBedStudentServiceImpl.java`
退宿操作涉及三个表的更新,按顺序执行:
#### 1. 更新学生基本信息表 (`xg_student_msg`)
```sql
-- 清理学生宿舍信息
UPDATE xg_student_msg
SET
dormitory_building = NULL, -- 清空宿舍楼信息
dormitory_room = NULL, -- 清空宿舍房间信息
ext_field1 = NULL, -- 清空床位ID(存储在扩展字段1中)
update_time = NOW(), -- 更新时间
update_by = ? -- 操作人ID
WHERE
ext_field1 IN (?) -- 床位ID列表(字符串形式)
AND deleted = 0 -- 未删除
AND student_status = '1'; -- 在校生状态
```
**说明**:
- `ext_field1` 字段存储床位ID
- 只处理在校生 (`student_status = '1'`)
- 将宿舍相关信息全部清空
#### 2. 更新床位学生分配表 (`xg_dorm_bed_student`)
```sql
-- 标记床位为退宿状态
UPDATE xg_dorm_bed_student
SET
status = '1', -- 状态:1-退宿
checkout_time = NOW(), -- 退宿时间
update_time = NOW(), -- 更新时间
update_by = ? -- 操作人ID
WHERE
bed_id IN (?) -- 床位ID列表
AND deleted = '0' -- 未删除
AND status = '0'; -- 当前状态:0-正常入住
```
**说明**:
- 状态变更:`0`(正常入住) → `1`(退宿)
- 记录退宿时间 `checkout_time`
- 只处理当前正常入住的床位
#### 3. 更新床位状态表 (`xg_dorm_bed`)
```sql
-- 将床位状态改为闲置
UPDATE xg_dorm_bed
SET
bed_status = '0' -- 床位状态:0-闲置
WHERE
id IN (?) -- 床位ID列表
AND deleted = '0'; -- 未删除
```
**说明**:
- 床位状态变更为闲置,供其他学生使用
- 不更新时间字段,只修改状态
## 数据状态说明
### 床位状态 (`bed_status`)
- `0`: 闲置 - 可分配给新学生
- `1`: 已分配 - 已有学生入住
### 床位学生分配状态 (`status`)
- `0`: 正常入住 - 学生正在使用该床位
- `1`: 退宿 - 学生已退宿,床位释放
### 学生状态 (`student_status`)
- `1`: 在校生 - 正常在校学生
- `2`: 休学
- `3`: 退学
- `4`: 毕业
## 事务处理
所有退宿操作在一个事务中执行:
```java
@Transactional(rollbackFor = Exception.class)
public Boolean classTeacherStayawaySelectedBeds(List<Long> bedIds, Long classId)
```
如果任何一步操作失败,整个退宿操作会回滚,确保数据一致性。
## 异常处理
- 床位ID列表为空时抛出 `IllegalArgumentException`
- SQL执行异常时抛出 `RuntimeException`
- 记录详细的日志信息用于问题排查
## 适用场景
此退宿逻辑适用于以下场景:
1. **学生主动退宿申请** - 通过前端界面提交退宿申请,经审批后执行
2. **学生退学处理** - 学生退学时自动释放床位
3. **学生毕业处理** - 学生毕业时自动释放床位
4. **学生休学处理** - 学生休学时释放床位
## 后续使用建议
在实现学生退学、毕业、休学脚本时,可以参考此退宿逻辑的SQL操作顺序:
1. 首先更新 `xg_student_msg` 表清空宿舍信息
2. 然后更新 `xg_dorm_bed_student` 表标记退宿状态
3. 最后更新 `xg_dorm_bed` 表释放床位
**注意**: 不同场景可能需要根据具体业务逻辑调整条件判断。
@@ -0,0 +1,68 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { XgStudentClassChangeVo, XgStudentClassChangeQuery, XgStudentClassChangeBo, R, TableDataInfo } from './types';
/**
* 查询学生换班级列表
*/
export function listStudentClassChange(query?: XgStudentClassChangeQuery): AxiosPromise<R<TableDataInfo<XgStudentClassChangeVo[]>>> {
return request({
url: '/studentMsg/studentClassChange/list',
method: 'get',
params: query
});
}
/**
* 查询学生换班级详细
*/
export function getStudentClassChange(id: string | number): AxiosPromise<R<XgStudentClassChangeVo>> {
return request({
url: `/studentMsg/studentClassChange/${id}`,
method: 'get'
});
}
/**
* 新增学生换班级
*/
export function addStudentClassChange(data: XgStudentClassChangeBo): AxiosPromise<R<any>> {
return request({
url: '/studentMsg/studentClassChange',
method: 'post',
data: data
});
}
/**
* 修改学生换班级
*/
export function updateStudentClassChange(data: XgStudentClassChangeBo): AxiosPromise<R<any>> {
return request({
url: '/studentMsg/studentClassChange',
method: 'put',
data: data
});
}
/**
* 删除学生换班级
*/
export function delStudentClassChange(ids: string | number | Array<string | number>): AxiosPromise<R<any>> {
return request({
url: `/studentMsg/studentClassChange/${ids}`,
method: 'delete'
});
}
/**
* 导出学生换班级
*/
export function exportStudentClassChange(query?: XgStudentClassChangeQuery): AxiosPromise<any> {
return request({
url: '/studentMsg/studentClassChange/export',
method: 'post',
params: query,
responseType: 'blob'
});
}
@@ -0,0 +1,54 @@
export interface XgStudentClassChangeVo {
id?: number;
studentId?: number;
studentNo?: string;
studentName?: string;
originalCollegeName?: string;
originalMajorName?: string;
originalClassName?: string;
newCollegeName?: string;
newMajorName?: string;
newClassName?: string;
changeReason?: string;
effectiveTime?: Date;
status?: string;
createBy?: number;
createTime?: Date;
}
export interface XgStudentClassChangeQuery {
pageNum?: number;
pageSize?: number;
studentNo?: string;
studentName?: string;
status?: string;
params?: any;
}
export interface XgStudentClassChangeBo {
id?: number;
studentId?: number;
studentNo?: string;
studentName?: string;
originalCollegeId?: number;
originalCollegeName?: string;
originalMajorId?: number;
originalMajorName?: string;
originalClassId?: number;
originalClassCode?: string;
originalClassName?: string;
newCollegeId?: number;
newCollegeName?: string;
newMajorId?: number;
newMajorName?: string;
newClassId?: number;
newClassCode?: string;
newClassName?: string;
changeReason?: string;
changeType?: string;
changeTime?: Date;
effectiveTime?: Date;
status?: string;
createDept?: number;
remark?: string;
}
@@ -0,0 +1,328 @@
<template>
<div class="p-2 page-container">
<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" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="处理状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择处理状态" clearable>
<el-option label="未处理" value="0" />
<el-option label="已处理" value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<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-col>
</el-row>
</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="danger"
plain
icon="Delete"
:disabled="multiple || selectedRows.some((row) => row.status === '1')"
@click="handleDelete()"
v-hasPermi="['studentMsg:studentClassChange:remove']"
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['studentMsg:studentClassChange:export']"
>导出</el-button
>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="studentClassChangeList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="学号" align="center" prop="studentNo" />
<el-table-column label="学生姓名" align="center" prop="studentName" />
<el-table-column label="原学院" align="center" prop="originalCollegeName" />
<el-table-column label="原专业" align="center" prop="originalMajorName" />
<el-table-column label="原班级" align="center" prop="originalClassName" />
<el-table-column label="新学院" align="center" prop="newCollegeName" />
<el-table-column label="新专业" align="center" prop="newMajorName" />
<el-table-column label="新班级" align="center" prop="newClassName" />
<el-table-column label="换班级原因" align="center" prop="changeReason" width="200" :show-overflow-tooltip="true" />
<el-table-column label="生效时间" align="center" prop="effectiveTime" width="120">
<template #default="scope">
<span>{{ proxy?.parseTime(scope.row.effectiveTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="处理状态" align="center" prop="status">
<template #default="scope">
<el-tag v-if="scope.row.status === '0'" type="warning">未处理</el-tag>
<el-tag v-else-if="scope.row.status === '1'" type="success">已处理</el-tag>
<el-tag v-else type="info">未知</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template #default="scope">
<span>{{ proxy?.parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100" fixed="right">
<template #default="scope">
<el-tooltip content="删除" placement="top">
<el-button
link
type="danger"
plain
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['studentMsg:studentClassChange:remove']"
:disabled="scope.row.status === '1'"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
</div>
</template>
<script setup name="StudentClassChange" lang="ts">
import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue';
import type { FormInstance as ElFormInstance } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
import { to } from 'await-to-js';
import service from '@/utils/request';
import type { XgStudentClassChangeVo, XgStudentClassChangeQuery } from '@/api/xg/xsjcxx/studentClassChange';
interface PageData<T, Q> {
form: T;
queryParams: Q;
rules: Record<string, any[]>;
}
interface ComponentInternalInstance {
proxy?: any;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 表格数据
const studentClassChangeList = ref<XgStudentClassChangeVo[]>([]);
const loading = ref(true);
const ids = ref<Array<number | string>>([]);
const selectedRows = ref<XgStudentClassChangeVo[]>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const dateRange = ref<[string, string]>(['', '']);
const showSearch = ref(true);
// 分页参数
const queryParams = reactive<XgStudentClassChangeQuery>({
pageNum: 1,
pageSize: 10,
studentNo: '',
studentName: '',
status: '',
params: {}
});
// 表单引用
const queryFormRef = ref<ElFormInstance>();
/** 查询列表 */
const getList = async () => {
loading.value = true;
try {
// 构造请求参数
const requestBody: any = {
studentNo: queryParams.studentNo || '',
studentName: queryParams.studentName || '',
status: queryParams.status || ''
};
// 处理时间范围查询
if (dateRange.value && dateRange.value[0] && dateRange.value[1]) {
requestBody.beginTime = dateRange.value[0];
requestBody.endTime = dateRange.value[1];
}
// 构造分页参数
const pageQuery = {
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize
};
// 调用后端接口(参考 StudentStatusChange 的写法)
const res = await service.get('/studentMsg/studentClassChange/list', {
params: { ...requestBody, ...pageQuery }
});
loading.value = false;
if (res && res.code === 200) {
studentClassChangeList.value = res.rows || [];
total.value = res.total || 0;
} else {
// 仅在后端返回错误码或无响应时显示错误
ElMessage.error(res?.msg || '查询失败');
studentClassChangeList.value = [];
total.value = 0;
}
} catch (error) {
loading.value = false;
console.error('查询失败:', error);
ElMessage.error('查询失败,请重试');
studentClassChangeList.value = [];
total.value = 0;
}
};
/** 选择条数 */
const handleSelectionChange = (selection: XgStudentClassChangeVo[]) => {
ids.value = selection.map((item) => item.id!);
selectedRows.value = selection;
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
dateRange.value = ['', ''];
proxy?.resetForm(queryFormRef.value);
queryParams.pageNum = 1;
queryParams.studentNo = '';
queryParams.studentName = '';
queryParams.status = '';
handleQuery();
};
/** 删除按钮操作 */
const handleDelete = async (row?: XgStudentClassChangeVo) => {
if (row && row.status === '1') {
ElMessage.warning('已处理的记录不能删除');
return;
}
const studentClassChangeIds = row?.id || ids.value;
const [err] = await to(
ElMessageBox.confirm(`是否确认删除选中的${Array.isArray(studentClassChangeIds) ? studentClassChangeIds.length : 1}条数据项?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
);
if (!err) {
try {
const res = await service.delete(`/studentMsg/studentClassChange/${studentClassChangeIds}`);
if (res && res.code === 200) {
await getList();
ElMessage.success('删除成功');
} else {
ElMessage.error(res?.msg || '删除失败');
}
} catch (error) {
console.error('删除失败:', error);
ElMessage.error('删除失败,请重试');
}
}
};
/** 导出按钮操作 */
const handleExport = () => {
ElMessage({
message: '正在导出数据,请耐心等待...',
type: 'info',
duration: 0,
zIndex: 4000
});
proxy
?.download(
'studentMsg/studentClassChange/export',
{
...queryParams,
...proxy?.addDateRange(queryParams, dateRange.value)
},
`student_class_change_${new Date().getTime()}.xlsx`
)
.then(() => {
ElMessage.closeAll();
ElMessage.success('导出成功');
})
.catch((error) => {
ElMessage.closeAll();
if (error.message && error.message.includes('timeout')) {
ElMessage.error('导出超时,请尝试减少导出数据量或联系管理员');
} else {
ElMessage.error('导出失败,请稍后重试');
}
});
};
// 页面初始化
onMounted(() => {
getList();
});
</script>
<style scoped>
.page-container {
padding: 8px;
}
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
</style>
@@ -127,6 +127,9 @@
<el-col :span="1.2">
<el-button type="primary" :disabled="multiple" @click="openStatusChangeModal('BI_YE')">毕业</el-button>
</el-col>
<el-col :span="1.2">
<el-button type="warning" :disabled="multiple" @click="openChangeClassModal">换班级</el-button>
</el-col>
<el-col :span="1.5">
<el-dropdown class="mt-[1px]">
<el-button>
@@ -915,6 +918,117 @@
</template>
</el-dialog>
<!-- 换班级对话框 -->
<el-dialog v-model="changeClassDialog.visible" :title="changeClassDialog.title" width="800px" append-to-body>
<el-form ref="changeClassFormRef" :model="changeClassForm" :rules="changeClassRules" label-width="120px">
<!-- 学生基本信息和原班级信息展示 -->
<el-card shadow="never" class="mb-4">
<template #header>
<div class="text-lg font-medium">学生信息</div>
</template>
<div class="student-info-grid">
<div class="info-row">
<div class="info-item">
<span class="info-label">学号:</span>
<span class="info-value">{{ changeClassForm.studentNo }}</span>
</div>
<div class="info-item">
<span class="info-label">姓名:</span>
<span class="info-value">{{ changeClassForm.studentName }}</span>
</div>
<div class="info-item">
<span class="info-label">性别:</span>
<span class="info-value">{{ changeClassForm.gender === '0' ? '男' : changeClassForm.gender === '1' ? '女' : '未知' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">原学院:</span>
<span class="info-value">{{ changeClassForm.originalCollegeName }}</span>
</div>
<div class="info-item">
<span class="info-label">原专业:</span>
<span class="info-value">{{ changeClassForm.originalMajorName }}</span>
</div>
<div class="info-item">
<span class="info-label">原班级:</span>
<span class="info-value">{{ changeClassForm.originalClassName }}</span>
</div>
</div>
</div>
</el-card>
<!-- 新班级信息选择 -->
<el-card shadow="never">
<template #header>
<div class="text-lg font-medium">新班级信息</div>
</template>
<el-row :gutter="15">
<el-col :span="12">
<el-form-item label="新学院" prop="newCollegeId">
<el-select v-model="changeClassForm.newCollegeId" placeholder="请选择学院" @change="handleCollegeChange" style="width: 100%">
<el-option v-for="college in collegeOptions" :key="college.id" :label="college.label" :value="college.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="新专业" prop="newMajorId">
<el-select
v-model="changeClassForm.newMajorId"
placeholder="请选择专业"
:disabled="!changeClassForm.newCollegeId"
@change="handleChangeClassMajorChange"
style="width: 100%"
>
<el-option v-for="major in majorOptions" :key="major.id" :label="major.label" :value="major.id" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="15">
<el-col :span="12">
<el-form-item label="新班级" prop="newClassId">
<el-select v-model="changeClassForm.newClassId" placeholder="请选择班级" :disabled="!changeClassForm.newMajorId" style="width: 100%">
<el-option v-for="classItem in classOptions" :key="classItem.id" :label="classItem.label" :value="classItem.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="生效时间" prop="effectiveTime">
<el-date-picker
v-model="changeClassForm.effectiveTime"
type="date"
placeholder="请选择生效时间"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="换班级原因" prop="changeReason">
<el-input
v-model="changeClassForm.changeReason"
type="textarea"
placeholder="请输入换班级原因"
:rows="3"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
</el-card>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="changeClassDialog.visible = false">取消</el-button>
<el-button type="primary" @click="submitChangeClass">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 学生详情对话框 -->
<el-dialog v-model="detailDialog.visible" :title="detailDialog.title" width="1000px" append-to-body>
<el-tabs v-model="activeDetailTab" type="border-card">
@@ -1739,6 +1853,7 @@ const deptOptions = ref([]);
const rawDeptOptions = ref<any[]>([]);
const majorOptions = ref([]);
const classOptions = ref([]);
const collegeOptions = ref([]); // 学院选项
const regionOptions = ref([]); // 行政区划选项
const dormBuildingOptions = ref([]); // 宿舍楼栋选项
const currentSelectedNode = ref<any>(null); // 当前选中的部门节点
@@ -1749,6 +1864,29 @@ const classStatusMap = new Map<string | number, string>();
let classStatusLoaded = false;
let classStatusLoading = false;
// 换班级相关
const changeClassDialog = reactive({
visible: false,
title: '换班级申请'
});
const changeClassForm = reactive({
studentId: '',
studentNo: '',
studentName: '',
gender: '',
originalCollegeId: '',
originalCollegeName: '',
originalMajorId: '',
originalMajorName: '',
originalClassId: '',
originalClassName: '',
newCollegeId: '',
newMajorId: '',
newClassId: '',
effectiveTime: '',
changeReason: ''
});
// 计算属性:判断是否显示新增按钮(只有班级节点才显示)
const showAddButton = computed(() => {
if (!currentSelectedNode.value) {
@@ -1776,6 +1914,7 @@ const deptTreeRef = ref<any>();
const queryFormRef = ref<any>();
const studentFormRef = ref<any>();
const statusChangeFormRef = ref<any>();
const changeClassFormRef = ref<any>();
const uploadRef = ref<any>();
const initPassword = ref('Hzdyjs123456!@#');
@@ -1956,6 +2095,15 @@ const statusChangeRules = reactive({
reason: [{ required: true, message: '异动原因不能为空', trigger: 'blur' }]
});
// 换班级表单验证规则
const changeClassRules = reactive({
newCollegeId: [{ required: true, message: '请选择新学院', trigger: 'change' }],
newMajorId: [{ required: true, message: '请选择新专业', trigger: 'change' }],
newClassId: [{ required: true, message: '请选择新班级', trigger: 'change' }],
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'blur' }],
changeReason: [{ required: true, message: '换班级原因不能为空', trigger: 'blur' }]
});
// 异动类型选项通过字典表获取:xg_stu_status
const queryParams = reactive<StudentQuery>({
@@ -2990,6 +3138,190 @@ const submitStatusChange = async () => {
});
};
/** 打开换班级弹窗 */
const openChangeClassModal = async () => {
if (ids.value.length !== 1) {
ElMessage.warning('请选择一个学生进行换班级操作');
return;
}
const studentId = ids.value[0];
const selectedStudent = studentList.value.find((student) => student.id === studentId);
if (!selectedStudent) {
ElMessage.error('未找到选中的学生信息');
return;
}
try {
// 获取学生详细信息
const res = await service.get(`/studentMsg/studentMsg/${studentId}`);
if (res && res.code === 200) {
const studentData = res.data;
// 填充学生基本信息
changeClassForm.studentId = studentData.id;
changeClassForm.studentNo = studentData.studentNo;
changeClassForm.studentName = studentData.name;
changeClassForm.gender = studentData.gender;
// 填充原班级信息
changeClassForm.originalCollegeId = studentData.collegeId;
changeClassForm.originalCollegeName = studentData.collegeName;
changeClassForm.originalMajorId = studentData.majorId;
changeClassForm.originalMajorName = studentData.majorName;
changeClassForm.originalClassId = studentData.classId;
changeClassForm.originalClassName = studentData.className;
// 重置新班级选择
changeClassForm.newCollegeId = '';
changeClassForm.newMajorId = '';
changeClassForm.newClassId = '';
changeClassForm.effectiveTime = '';
changeClassForm.changeReason = '';
// 获取学院选项
await loadCollegeOptions();
changeClassDialog.visible = true;
} else {
ElMessage.error(res?.msg || '获取学生信息失败');
}
} catch (error) {
console.error('获取学生信息失败:', error);
ElMessage.error('获取学生信息失败,请重试');
}
};
/** 加载学院选项 */
const loadCollegeOptions = async () => {
try {
const res = await service.get('/system/dept/list', {
params: {
parentId: 101, // 二级学院节点
deptCategory: 'xy-' // 学院标识
}
});
if (res && res.code === 200) {
collegeOptions.value = (res.data || []).map((dept: any) => ({
id: dept.deptId,
label: dept.deptName
}));
}
} catch (error) {
console.error('获取学院列表失败:', error);
collegeOptions.value = [];
}
};
/** 学院选择变更 */
const handleCollegeChange = async (collegeId: string) => {
changeClassForm.newMajorId = '';
changeClassForm.newClassId = '';
majorOptions.value = [];
classOptions.value = [];
if (collegeId) {
try {
const res = await service.get('/system/dept/list', {
params: {
parentId: collegeId,
deptCategory: 'zy-' // 专业标识
}
});
if (res && res.code === 200) {
majorOptions.value = (res.data || []).map((dept: any) => ({
id: dept.deptId,
label: dept.deptName
}));
}
} catch (error) {
console.error('获取专业列表失败:', error);
majorOptions.value = [];
}
}
};
/** 换班级专业选择变更 */
const handleChangeClassMajorChange = async (majorId: string) => {
changeClassForm.newClassId = '';
classOptions.value = [];
if (majorId) {
try {
const res = await service.get('/system/dept/list', {
params: {
parentId: majorId,
deptCategory: 'bj-' // 班级标识
}
});
if (res && res.code === 200) {
classOptions.value = (res.data || []).map((dept: any) => ({
id: dept.deptId,
label: dept.deptName
}));
}
} catch (error) {
console.error('获取班级列表失败:', error);
classOptions.value = [];
}
}
};
/** 提交换班级申请 */
const submitChangeClass = async () => {
const formRef = changeClassFormRef.value;
if (!formRef) return;
await formRef.validate(async (valid: boolean) => {
if (valid) {
try {
// 检查是否选择了不同的班级
if (changeClassForm.newClassId === changeClassForm.originalClassId) {
ElMessage.warning('请选择不同的班级进行换班');
return;
}
const submitData = {
studentId: changeClassForm.studentId,
studentNo: changeClassForm.studentNo,
studentName: changeClassForm.studentName,
originalCollegeId: changeClassForm.originalCollegeId,
originalCollegeName: changeClassForm.originalCollegeName,
originalMajorId: changeClassForm.originalMajorId,
originalMajorName: changeClassForm.originalMajorName,
originalClassId: changeClassForm.originalClassId,
originalClassCode: '', // 可以根据需要获取
originalClassName: changeClassForm.originalClassName,
newCollegeId: changeClassForm.newCollegeId,
newCollegeName: collegeOptions.value.find((c) => c.id === changeClassForm.newCollegeId)?.label || '',
newMajorId: changeClassForm.newMajorId,
newMajorName: majorOptions.value.find((m) => m.id === changeClassForm.newMajorId)?.label || '',
newClassId: changeClassForm.newClassId,
newClassName: classOptions.value.find((c) => c.id === changeClassForm.newClassId)?.label || '',
changeReason: changeClassForm.changeReason,
changeType: '1', // 主动申请
changeTime: proxy?.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
effectiveTime: changeClassForm.effectiveTime,
status: '0' // 未处理状态
};
const res = await service.post('/studentMsg/studentClassChange', submitData);
if (res && res.code === 200) {
ElMessage.success('换班级申请提交成功');
changeClassDialog.visible = false;
await getList(); // 刷新列表
} else {
ElMessage.error(res?.msg || '提交失败');
}
} catch (error) {
console.error('提交换班级申请失败:', error);
ElMessage.error('提交失败,请重试');
}
}
});
};
/** 导入按钮操作 */
const handleImport = () => {
if (!showAddButton.value) {
@@ -3780,4 +4112,36 @@ onMounted(() => {
margin-bottom: 0;
margin-right: 0;
}
/* 学生信息网格样式 */
.student-info-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.info-row {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.info-item {
display: flex;
align-items: center;
min-width: 120px;
flex: 1;
}
.info-label {
font-weight: 500;
color: #606266;
margin-right: 8px;
white-space: nowrap;
}
.info-value {
color: #303133;
font-weight: 400;
}
</style>
+95
View File
@@ -0,0 +1,95 @@
# 学生换班级模块
## 📁 文件夹说明
本文件夹包含学生换班级业务模块的相关设计文档和数据库脚本。
## 📋 文件清单
### 🗄️ 数据库设计
- **[学生换班级记录表.sql](学生换班级记录表.sql)** - 学生换班级记录表的完整建表SQL脚本
- **[学生换班级记录表设计说明.md](学生换班级记录表设计说明.md)** - 详细的表结构设计说明文档
## 🎯 业务概述
学生换班级记录表用于记录学校所有学生换班级的操作历史,包括:
- 学生主动申请换班级
- 学校根据教学需要调整班级
- 学生转专业导致的班级变化
- 其他特殊情况的换班级操作
## 🔑 核心功能
### 1. 完整的换班级记录
- 记录学生的原班级和新班级信息
- 保存换班级的原因和类型
- 记录换班级的时间和生效时间
- 状态管理:未处理/已处理
### 2. 前端界面支持
- 学生信息管理页面集成换班级功能
- 单选学生进行换班级操作
- 三级级联选择新班级(学院→专业→班级)
- 实时展示学生原班级信息
### 3. 数据追溯和审计
- 完整的操作历史记录
- 支持数据追溯和审计
- 提供统计分析基础数据
## 📊 数据结构特点
### 遵循项目规范
- ✅ 包含所有强制性基础字段
- ✅ 使用雪花ID作为主键
- ✅ 支持多租户数据隔离
- ✅ 逻辑删除,避免物理删除
- ✅ 新增状态字段:0-未处理,1-已处理
### 关联关系
- 学生信息:关联 `xg_student_msg` 表
- 班级信息:关联 `xg_class_msg` 表
- 专业信息:关联 `xg_major_msg` 表
- 学院信息:关联 `sys_dept` 表(dept_category区分层级)
### 索引优化
- 基础索引:租户、删除标志、创建时间
- 业务索引:学生ID、班级ID、时间等
- 唯一约束:防止重复记录
## 🔄 业务流程
```
学生申请 → 提交申请 → 辅导员审批 → 院系领导审批 → 教务处审批 → 执行换班级 → 生效
```
## 📈 统计分析支持
支持多种维度的统计分析:
- 按时间统计换班级趋势
- 按原因分析换班级分布
- 按班级统计换班级情况
- 按审批状态统计处理情况
## 🚀 使用建议
### 开发注意事项
1. **数据一致性**: 换班级操作必须确保相关数据的一致性
2. **业务规则**: 严格遵守学校的换班级管理规定
3. **审批流程**: 必须经过完整的审批流程
4. **文档同步**: 代码变更及时更新相关文档
### 性能优化
1. **分页查询**: 大数据量查询必须分页
2. **索引使用**: 充分利用设计的索引
3. **缓存策略**: 对常用数据进行适当缓存
## 📞 技术支持
如有疑问或需要技术支持,请联系项目技术团队。
---
**创建时间**: 2025-01-25
**维护人员**: AI Assistant
**版本**: v1.0.0
@@ -0,0 +1,80 @@
-- ==========================================
-- 数据库变更脚本
-- 版本:V1.0.0
-- 功能:新增学生换班级记录表
-- 作者:AI Assistant
-- 时间:2025-01-25
-- ==========================================
-- ----------------------------
-- Table structure for xg_student_class_change
-- ----------------------------
DROP TABLE IF EXISTS `xg_student_class_change`;
CREATE TABLE `xg_student_class_change` (
-- 基础字段(必须)
`id` bigint(20) NOT NULL COMMENT '主键ID(雪花ID)',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) 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 '删除标志(0代表存在 1代表删除)',
-- 学生基本信息
`student_id` bigint(20) NOT NULL COMMENT '学生ID,关联xg_student_msg.id',
`student_no` varchar(32) NOT NULL COMMENT '学号',
`student_name` varchar(50) NOT NULL COMMENT '学生姓名',
-- 原班级信息
`original_college_id` bigint(20) DEFAULT NULL COMMENT '原学院ID ',
`original_college_name` varchar(100) DEFAULT NULL COMMENT '原学院名称',
`original_major_id` bigint(20) DEFAULT NULL COMMENT '原专业ID ',
`original_major_name` varchar(100) DEFAULT NULL COMMENT '原专业名称',
`original_class_id` bigint(20) DEFAULT NULL COMMENT '原班级ID ',
`original_class_code` varchar(32) DEFAULT NULL COMMENT '原班级代码',
`original_class_name` varchar(100) DEFAULT NULL COMMENT '原班级名称',
-- 新班级信息
`new_college_id` bigint(20) DEFAULT NULL COMMENT '新学院ID ',
`new_college_name` varchar(100) DEFAULT NULL COMMENT '新学院名称',
`new_major_id` bigint(20) DEFAULT NULL COMMENT '新专业ID ',
`new_major_name` varchar(100) DEFAULT NULL COMMENT '新专业名称',
`new_class_id` bigint(20) DEFAULT NULL COMMENT '新班级ID ',
`new_class_code` varchar(32) DEFAULT NULL COMMENT '新班级代码',
`new_class_name` varchar(100) DEFAULT NULL COMMENT '新班级名称',
-- 换班级业务信息
`change_reason` varchar(500) DEFAULT NULL COMMENT '换班级原因',
`change_type` char(1) DEFAULT '1' COMMENT '换班级类型:1-主动申请,2-学校调整,3-专业调整,4-其他',
`change_time` datetime DEFAULT NULL COMMENT '换班级时间',
`effective_time` datetime DEFAULT NULL COMMENT '生效时间',
`status` char(1) DEFAULT '0' COMMENT '处理状态:0-未处理,1-已处理',
-- 扩展字段
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
-- 主键和索引
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_student_change_time` (`student_id`, `change_time`, `tenant_id`, `deleted`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_student_id` (`student_id`),
KEY `idx_student_no` (`student_no`),
KEY `idx_original_class_id` (`original_class_id`),
KEY `idx_new_class_id` (`new_class_id`),
KEY `idx_change_time` (`change_time`),
KEY `idx_effective_time` (`effective_time`),
KEY `idx_create_time` (`create_time`),
KEY `idx_tenant_deleted` (`tenant_id`, `deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生换班级记录表';
-- 建表完成提示
-- 注意事项:
-- 1. 该表遵循项目数据库规范,包含所有强制性基础字段
-- 2. 使用雪花ID作为主键,支持多租户数据隔离
-- 3. 支持逻辑删除,避免物理删除数据
-- 4. 记录学生换班级的完整历史信息,包括审批流程
-- 5. 关联字段不使用外键约束,通过应用层逻辑维护数据一致性
-- 6. 索引设计遵循性能优化原则
@@ -0,0 +1,298 @@
# 学生换班级记录表设计说明
## 📋 文档说明
**版本**: v1.0.0
**制定时间**: 2025-01-25
**适用范围**: 学生换班级业务模块
**重要性**: ⚠️ **核心业务表** - 记录所有学生换班级操作的历史数据
---
## 🎯 设计目标
### 1. 业务需求
- **记录完整历史**: 记录学生换班级的完整过程和历史数据
- **支持审批流程**: 包含换班级的申请、审批、执行全流程
- **数据追溯**: 提供完整的换班级操作审计和追溯能力
- **业务统计**: 支持换班级数据的统计分析和报表生成
### 2. 技术要求
- **数据完整性**: 遵循项目数据库设计规范
- **性能优化**: 合理的索引设计,支持高效查询
- **扩展性**: 预留扩展字段,支持未来功能需求
- **安全性**: 多租户隔离,完整的审计追踪
---
## 🗄️ 表结构设计
### 表名:`xg_student_class_change`
### 字段分类说明
#### 🔴 强制基础字段(必须)
| 字段名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `id` | bigint(20) | - | 主键ID(雪花ID) |
| `tenant_id` | varchar(20) | '000000' | 租户编号 |
| `create_dept` | bigint(20) | NULL | 创建部门 |
| `create_by` | bigint(20) | NULL | 创建者 |
| `create_time` | datetime | NULL | 创建时间 |
| `update_by` | bigint(20) | NULL | 更新者 |
| `update_time` | datetime | NULL | 更新时间 |
| `deleted` | tinyint(1) | 0 | 删除标志(0存在 1删除) |
#### 👤 学生基本信息
| 字段名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `student_id` | bigint(20) | - | 学生ID,关联xg_student_msg.id |
| `student_no` | varchar(32) | - | 学号 |
| `student_name` | varchar(50) | - | 学生姓名 |
#### 📚 原班级信息
| 字段名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `original_college_id` | bigint(20) | NULL | 原学院ID,关联sys_dept.dept_id |
| `original_college_name` | varchar(100) | NULL | 原学院名称 |
| `original_major_id` | bigint(20) | NULL | 原专业ID,关联xg_major_msg.id |
| `original_major_name` | varchar(100) | NULL | 原专业名称 |
| `original_class_id` | bigint(20) | NULL | 原班级ID,关联xg_class_msg.id |
| `original_class_code` | varchar(32) | NULL | 原班级代码 |
| `original_class_name` | varchar(100) | NULL | 原班级名称 |
#### 📚 新班级信息
| 字段名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `new_college_id` | bigint(20) | NULL | 新学院ID,关联sys_dept.dept_id |
| `new_college_name` | varchar(100) | NULL | 新学院名称 |
| `new_major_id` | bigint(20) | NULL | 新专业ID,关联xg_major_msg.id |
| `new_major_name` | varchar(100) | NULL | 新专业名称 |
| `new_class_id` | bigint(20) | NULL | 新班级ID,关联xg_class_msg.id |
| `new_class_code` | varchar(32) | NULL | 新班级代码 |
| `new_class_name` | varchar(100) | NULL | 新班级名称 |
#### 🔄 换班级业务信息
| 字段名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `change_reason` | varchar(500) | NULL | 换班级原因 |
| `change_type` | char(1) | '1' | 换班级类型:1-主动申请,2-学校调整,3-专业调整,4-其他 |
| `change_time` | datetime | NULL | 换班级时间 |
| `effective_time` | datetime | NULL | 生效时间 |
| `status` | char(1) | '0' | 处理状态:0-未处理,1-已处理 |
#### ✅ 审批流程信息
| 字段名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `approval_status` | char(1) | '0' | 审批状态:0-待审批,1-审批通过,2-审批拒绝,3-已撤销 |
| `approval_by` | bigint(20) | NULL | 审批人ID |
| `approval_time` | datetime | NULL | 审批时间 |
| `approval_remarks` | varchar(500) | NULL | 审批备注 |
#### 📎 附件信息
| 字段名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `attachment_ids` | text | NULL | 附件文件ID(逗号分隔) |
#### 📝 扩展字段
| 字段名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `remark` | varchar(500) | NULL | 备注 |
---
## 🔍 索引设计
### 基础索引(必须)
```sql
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_create_time` (`create_time`),
KEY `idx_tenant_deleted` (`tenant_id`, `deleted`)
```
### 业务索引
```sql
KEY `idx_student_id` (`student_id`),
KEY `idx_student_no` (`student_no`),
KEY `idx_original_class_id` (`original_class_id`),
KEY `idx_new_class_id` (`new_class_id`),
KEY `idx_change_time` (`change_time`),
KEY `idx_effective_time` (`effective_time`),
KEY `idx_approval_status` (`approval_status`)
```
### 唯一约束
```sql
UNIQUE KEY `uk_student_change_time` (`student_id`, `change_time`, `tenant_id`, `deleted`)
```
---
## 🔄 业务流程
### 1. 换班级申请流程
```
学生申请 → 提交申请 → 管理员处理 → 执行换班级
```
### 2. 数据状态流转
```
未处理(0) → 已处理(1) → 执行换班级 → 生效
```
### 3. 换班级类型说明
| 类型代码 | 类型名称 | 说明 |
|----------|----------|------|
| 1 | 主动申请 | 学生主动提出换班级申请 |
| 2 | 学校调整 | 学校根据教学需要调整班级 |
| 3 | 专业调整 | 学生转专业导致班级变化 |
| 4 | 其他 | 其他特殊情况 |
---
## 📊 数据关联关系
### 关联表说明
- **xg_student_msg**: 学生信息表(通过student_id关联)
- **xg_class_msg**: 班级信息表(通过original_class_id、new_class_id关联)
- **xg_major_msg**: 专业信息表(通过original_major_id、new_major_id关联)
- **sys_dept**: 部门信息表(通过original_college_id、new_college_id关联)
- **sys_user**: 用户信息表(通过create_by、update_by、approval_by关联)
### 注意事项
⚠️ **不使用数据库外键约束**:所有关联关系通过应用层逻辑维护
---
## 📈 统计分析支持
### 支持的统计维度
1. **按时间统计**: 换班级数量趋势分析
2. **按原因统计**: 换班级原因分布统计
3. **按班级统计**: 各班级换班级情况统计
4. **按审批状态**: 换班级申请处理情况统计
5. **按操作人**: 换班级操作人员工作量统计
### 示例统计SQL
```sql
-- 按月份统计换班级数量
SELECT
DATE_FORMAT(change_time, '%Y-%m') AS month,
COUNT(*) AS change_count
FROM xg_student_class_change
WHERE deleted = 0 AND approval_status = '1'
GROUP BY DATE_FORMAT(change_time, '%Y-%m')
ORDER BY month;
-- 按原因统计换班级分布
SELECT
change_type,
COUNT(*) AS count
FROM xg_student_class_change
WHERE deleted = 0 AND approval_status = '1'
GROUP BY change_type;
```
---
## 🔒 安全考虑
### 数据安全
1. **多租户隔离**: 通过tenant_id实现数据隔离
2. **权限控制**: 基于角色的数据访问控制
3. **操作审计**: 完整的操作日志记录
4. **数据加密**: 敏感信息加密存储
### 业务安全
1. **审批流程**: 多级审批确保操作合规
2. **数据校验**: 换班级前进行业务规则校验
3. **操作限制**: 防止重复换班级和无效操作
4. **回滚机制**: 支持换班级操作的回滚
---
## 🚀 使用说明
### 数据插入规范
```sql
INSERT INTO xg_student_class_change (
id, tenant_id, create_by, create_time,
student_id, student_no, student_name,
original_class_id, original_class_name,
new_class_id, new_class_name,
change_reason, change_type, change_time,
approval_status
) VALUES (
雪花ID, '000000', 操作人ID, NOW(),
学生ID, '学号', '学生姓名',
原班级ID, '原班级名称',
新班级ID, '新班级名称',
'换班级原因', '1', NOW(),
'0' -- 待审批状态
);
```
### 查询优化建议
1. **分页查询**: 大数据量时必须分页
2. **索引使用**: 充分利用业务索引提升查询性能
3. **关联查询**: 注意避免复杂的多表关联查询
4. **缓存策略**: 对常用数据进行适当缓存
---
## 📞 维护说明
### 定期维护任务
1. **数据清理**: 定期清理已删除的历史数据
2. **索引优化**: 定期检查和优化索引性能
3. **统计更新**: 定期更新统计数据
4. **备份策略**: 定期备份重要数据
### 扩展规划
1. **字段扩展**: 预留扩展字段用于未来需求
2. **流程优化**: 支持更复杂的审批流程
3. **自动化**: 支持自动换班级处理
4. **集成**: 与教务系统深度集成
---
## ⚠️ 重要提醒
1. **数据一致性**: 换班级操作必须确保数据的一致性
2. **业务规则**: 严格遵守学校的换班级管理规定
3. **审批流程**: 必须经过完整的审批流程
4. **文档同步**: 代码变更必须同步更新相关文档
5. **测试验证**: 新功能必须经过充分的测试验证
---
**最后更新**: 2025-01-25
**维护人员**: AI Assistant
**联系方式**: 项目技术团队
@@ -2,7 +2,7 @@
## 📋 概述
本分文档详细说明学生退学、休学、毕业处理中的核心状态修改内容,包括学生学籍状态和用户登录状态的变更逻辑。
本分文档详细说明学生退学、休学、毕业、复学处理中的核心状态修改内容,包括学生学籍状态和用户登录状态的变更逻辑。
## 🎯 核心状态修改
@@ -24,16 +24,16 @@
| `3` | 复学 | 从休学状态恢复 | info |
| `4` | 退学 | 终止学籍 | danger |
| `5` | 毕业 | 正常毕业 | success |
**重要说明**:
- 数据库字段 `student_status` 为 char(1) 类型
- 字典类型为 `xg_student_status`
- 状态值与数据库字段值直接对应,无需转换
| `6` | 不在籍 | - | - |
| `7` | 结业 | - | - |
| `8` | 停学 | - | - |
| `9` | 分流 | - | - |
#### 🔄 处理逻辑
- **休学**: `student_status = '2'`(休学)
- **退学**: `student_status = '4'`(退学)
- **毕业**: `student_status = '5'`(毕业)
- **复学**: `student_status = '3'`(复学)
#### 📝 前端界面实现
**文件位置**: `web/src/views/xg/xsjcxx/stuPersonalMsg/index.vue`
@@ -53,7 +53,7 @@
-- 参数说明:
-- @student_id: 学生ID (主键)
-- @student_no: 学生学号
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, FU_XUE-复学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- 休学处理
@@ -66,7 +66,7 @@ WHERE id = @student_id
-- 退学处理
UPDATE xg_student_msg
SET student_status = '3',
SET student_status = '4',
update_by = @operator_id,
update_time = NOW()
WHERE id = @student_id
@@ -74,7 +74,15 @@ WHERE id = @student_id
-- 毕业处理
UPDATE xg_student_msg
SET student_status = '4',
SET student_status = '5',
update_by = @operator_id,
update_time = NOW()
WHERE id = @student_id
AND deleted = 0;
-- 复学处理
UPDATE xg_student_msg
SET student_status = '3',
update_by = @operator_id,
update_time = NOW()
WHERE id = @student_id
@@ -96,9 +104,10 @@ WHERE id = @student_id
| `1` | 停用 | 账号已被禁用 |
#### 🔄 处理逻辑
- **休学**: 可选择保留账号(`status` 保持 `0`)或禁用账号(`status = '1'`)
- **退学**: 必须禁用账号(`status = '1'`)
- **毕业**: 必须禁用账号(`status = '1'`)
- **休学**: 禁用账号(`status = '1'`)
- **退学**: 禁用账号(`status = '1'`)
- **毕业**: 禁用账号(`status = '1'`)
- **复学**: 启用账号(`status = '0'`)
#### 📝 后端实体类
**文件位置**: `backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUser.java`
@@ -113,29 +122,34 @@ WHERE id = @student_id
#### 💾 SQL处理脚本
```sql
-- 参数说明:
-- @student_id: 学生ID (对应sys_user.user_id)
-- @student_no: 学生学号 (对应sys_user.user_name)
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, FU_XUE-复学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- 退学/毕业:禁用用户账号
-- 1. 备份原始数据到his表
INSERT INTO sys_user_his SELECT * FROM sys_user WHERE user_id = @student_id;
-- 2. 处理用户账号状态
-- 退学/休学/毕业:禁用用户账号
UPDATE sys_user
SET status = '1',
update_by = @operator_id,
update_time = NOW(),
remark = CONCAT(COALESCE(remark, ''), ' | 学生状态变更-', @change_type, '-', NOW())
WHERE user_name = @student_no
update_time = NOW()
WHERE user_id = @student_id
AND account_type = '2' -- 学生账户类型
AND del_flag = '0';
AND del_flag = '0'
AND @change_type IN ('XIU_XUE', 'TUI_XUE', 'BI_YE');
-- 休学:可选保留账号(根据业务需求决定是否执行)
-- UPDATE sys_user
-- SET status = '1', -- 如需禁用则取消注释
-- update_by = @operator_id,
-- update_time = NOW(),
-- remark = CONCAT(COALESCE(remark, ''), ' | 学生休学-', NOW())
-- WHERE user_name = @student_no
-- AND account_type = '2'
-- AND del_flag = '0';
-- 复学:启用用户账号
UPDATE sys_user
SET status = '0',
update_by = @operator_id,
update_time = NOW()
WHERE user_id = @student_id
AND account_type = '2' -- 学生账户类型
AND del_flag = '0'
AND @change_type = 'FU_XUE';
```
### 3. 学生状态变更记录
@@ -168,8 +182,8 @@ WHERE user_name = @student_no
```java
// 变更类型枚举
public enum ChangeType {
TUI_XUE("退学"),
XIU_XUE("休学"),
TUI_XUE("退学"),
FU_XUE("复学"),
BI_YE("毕业");
}
@@ -183,10 +197,11 @@ public enum ChangeType {
```
1. 创建状态变更记录 (xg_student_status_change)
2. 验证学生当前状态和业务规则
3. 更新学生学籍状态 (xg_student_msg.student_status)
4. 根据变更类型处理用户登录状态 (sys_user.status)
5. 记录操作日志和审计信息
6. 触发后续业务处理 (组织关系、经济资助等)
3. 备份原始数据到his表
4. 更新学生学籍状态 (xg_student_msg.student_status)
5. 根据变更类型处理用户登录状态 (sys_user.status)
6. 记录操作日志和审计信息
7. 触发后续业务处理 (组织关系、退宿处理等)
```
#### ⚡ 事务处理要求
@@ -197,11 +212,15 @@ START TRANSACTION;
-- 步骤1: 创建变更记录
INSERT INTO xg_student_status_change (...) VALUES (...);
-- 步骤2: 更新学生状态
-- 步骤2: 备份原始数据到his表
INSERT INTO xg_student_msg_his SELECT * FROM xg_student_msg WHERE id = ?;
INSERT INTO sys_user_his SELECT * FROM sys_user WHERE user_id = ?;
-- 步骤3: 更新学生状态
UPDATE xg_student_msg SET student_status = ?, update_time = NOW() WHERE id = ?;
-- 步骤3: 更新用户状态 (如需要)
UPDATE sys_user SET status = ?, update_time = NOW() WHERE user_name = ?;
-- 步骤4: 更新用户状态
UPDATE sys_user SET status = ?, update_time = NOW() WHERE user_id = ?;
-- 提交事务
COMMIT;
@@ -230,17 +249,18 @@ WHERE user_name = @student_no AND del_flag = '0';
#### 📊 状态变更矩阵
| 当前状态 | 可变更状态 | 说明 |
|----------|------------|------|
| 在读(1) | 休学(2)、退学(4)、毕业(5) | 正常学籍状态下的变更 |
| 休学(2) | 复学(3)、退学(4)、毕业(5) | 休学状态下的恢复或终止 |
| 复学(3) | 休学(2)、退学(4)、毕业(5) | 复学后的再次变更 |
| 其他状态 | 需特殊审批 | 异常状态需要额外处理 |
| 当前状态 | 可变更状态 | 账号处理 | 说明 |
|----------|------------|----------|------|
| 在读(1) | 休学(2)、退学(4)、毕业(5) | 禁用账号 | 正常学籍状态下的变更 |
| 休学(2) | 复学(3)、退学(4)、毕业(5) | 禁用账号 | 休学状态下的恢复或终止 |
| 复学(3) | 休学(2)、退学(4)、毕业(5) | 禁用账号 | 复学后的再次变更 |
| 其他状态 | 需特殊审批 | 根据情况 | 异常状态需要额外处理 |
#### ⚠️ 业务约束
- **休学限制**: 同一学年内休学不能超过规定次数
- **退学限制**: 退学后不能恢复为正常学籍
- **毕业限制**: 毕业状态为最终状态,不允许变更
- **复学限制**: 只有休学状态才能复学
- **时间约束**: 状态变更需要满足学年学期要求
### 4. 异常处理机制
@@ -315,13 +335,14 @@ public class XgStudentStatusChangeServiceImpl implements IXgStudentStatusChangeS
```vue
<template>
<el-form :model="form" :rules="rules" ref="statusChangeForm">
<el-form-item label="变更类型" prop="changeType">
<el-select v-model="form.changeType" placeholder="请选择变更类型">
<el-option label="休学" value="XIU_XUE" />
<el-option label="退学" value="TUI_XUE" />
<el-option label="毕业" value="BI_YE" />
</el-select>
</el-form-item>
<el-form-item label="变更类型" prop="changeType">
<el-select v-model="form.changeType" placeholder="请选择变更类型">
<el-option label="休学" value="XIU_XUE" />
<el-option label="退学" value="TUI_XUE" />
<el-option label="复学" value="FU_XUE" />
<el-option label="毕业" value="BI_YE" />
</el-select>
</el-form-item>
<el-form-item label="生效日期" prop="effectiveDate">
<el-date-picker
@@ -2,7 +2,7 @@
## 📋 概述
本分文档详细说明学生退学、休学、毕业处理中的组织关系处理内容,包括班级干部信息和社团成员关系的变更逻辑。
本分文档详细说明学生退学、休学、毕业、复学处理中的组织关系处理内容,包括班级干部信息和社团成员关系的变更逻辑。
## 👥 组织关系处理
@@ -336,13 +336,14 @@ int deleteCount = sysUserRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>()
-- 建议在学生状态变更的大事务中包含组织关系处理
START TRANSACTION;
-- 1. 更新学生学籍状态
UPDATE xg_student_msg SET student_status = ? WHERE id = ?;
-- 1. 备份原始数据到his表
INSERT INTO xg_class_pos_msg_his SELECT * FROM xg_class_pos_msg WHERE student_id = ?;
INSERT INTO stgl_member_his SELECT * FROM stgl_member WHERE user_id = ?;
-- 2. 处理班级干部关系
UPDATE xg_class_pos_msg SET is_current = 0, cadre_status = ? WHERE student_id = ?;
-- 2. 处理班级干部关系(仅退学、休学、毕业时)
UPDATE xg_class_pos_msg SET is_current = 0, cadre_status = ?, term_end_date = NOW(), dismissal_date = NOW(), dismissal_reason = ? WHERE student_id = ?;
-- 3. 处理社团成员关系
-- 3. 处理社团成员关系(仅退学、休学、毕业时)
UPDATE stgl_member SET status = 4 WHERE user_id = ?;
-- 4. 清理用户角色(如果需要)
@@ -2,773 +2,11 @@
## 📋 概述
本分文档详细说明学生退学、休学、毕业处理中的经济资助业务处理内容,包括奖学金申请、获奖记录、勤工俭学和贫困生认定的变更逻辑。
## 💰 经济资助处理
### 1. 奖学金申请处理
#### 📊 数据库表结构
**表名**: `jcbt_scholarship_application`
**主键**: `id` (bigint(20))
**核心关联字段**:
- `batch_id`: 批次ID
- `scholarship_type_id`: 奖学金类型ID
- `student_id`: 学生ID
- `student_no`: 学号
- `student_name`: 学生姓名
#### 🎯 核心业务字段
基于数据库表 `jcbt_scholarship_application` 的实际结构:
| 字段名 | 类型 | 说明 | 默认值 |
|--------|------|------|--------|
| `current_status` | varchar(20) | 当前状态 | PENDING |
| `application_type` | varchar(20) | 申请类型(TEACHER_RECOMMEND/AUTO/DIRECT) | - |
| `calculated_score` | decimal(8,2) | 系统计算分数 | NULL |
| `final_score` | decimal(8,2) | 最终评定分数 | NULL |
| `ranking_in_dept` | int(11) | 院系排名 | NULL |
| `ranking_in_major` | int(11) | 专业排名 | NULL |
| `submit_time` | datetime | 提交时间 | NULL |
| `applicant_type` | varchar(20) | 申请人类型(STUDENT/TEACHER) | TEACHER |
#### 🎯 申请状态字典
系统使用 `jcbt_status` 字典类型(奖学金申请状态),具体定义如下:
| 状态值 | 状态名称 | 说明 | 样式类 |
|--------|----------|------|--------|
| `PENDING` | 待审核 | 申请已提交,等待审核 | warning |
| `APPROVED` | 已通过 | 审核通过,获得奖学金资格 | success |
| `REJECTED` | 已拒绝 | 审核未通过 | danger |
| `WITHDRAWN` | 已撤回 | 申请人主动撤回或系统自动撤回 | info |
**关键字段说明** (基于实体类代码分析):
- `currentStatus`: 当前状态 (PENDING/APPROVED/REJECTED/WITHDRAWN)
- `applicationType`: 申请类型 (RECOMMEND-教师推荐,AUTO-自动评定,DIRECT-直接指定)
- `calculatedScore`: 系统计算分数
- `finalScore`: 最终评定分数
- `rankingInDept`: 院系排名
- `rankingInMajor`: 专业排名
- `submitTime`: 提交时间
#### 🔄 奖学金评选流程
##### 📋 完整评选流程
```
1. 奖学金批次发布 → 学生申请提交
2. 学院初审 → 系统评分计算
3. 学生处终审 → 排名确定
4. 获奖公示 → 证书生成
5. 资金发放 → 档案归档
6. 学生状态变更 → 相应流程调整
```
##### 💻 后端实现逻辑
**实体类**: `JcbtScholarshipApplication.java`
**控制器**: `JcbtScholarshipApplicationController.java`
**服务类**: `JcbtScholarshipApplicationServiceImpl.java`
```java
// 奖学金申请实体类关键字段
@Data
@TableName("jcbt_scholarship_application")
public class JcbtScholarshipApplication extends BaseEntity {
private Long batchId; // 批次ID
private Long scholarshipTypeId; // 奖学金类型ID
private Long studentId; // 学生ID
private String studentNo; // 学号
private String studentName; // 学生姓名
private String currentStatus; // 当前状态
private String applicationType; // 申请类型
private Double calculatedScore; // 系统计算分数
private Double finalScore; // 最终评定分数
private Integer rankingInDept; // 院系排名
private Integer rankingInMajor; // 专业排名
private Date submitTime; // 提交时间
}
```
##### 🎨 前端界面展示
**管理界面**: `jcbt/scholarship/application/index.vue`
**状态显示**: 使用字典标签显示申请状态
```vue
<!-- 申请状态列 -->
<el-table-column label="申请状态" align="center" prop="currentStatus" width="120">
<template #default="scope">
<dict-tag :options="jcbt_application_status" :value="scope.row.currentStatus" />
</template>
</el-table-column>
<!-- 评分页数列 -->
<el-table-column label="最终得分" align="center" prop="finalScore" width="100">
<template #default="scope">
<span v-if="scope.row.finalScore">{{ scope.row.finalScore }}</span>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<!-- 排名列 -->
<el-table-column label="院系排名" align="center" prop="rankingInDept" width="100">
<template #default="scope">
<span v-if="scope.row.rankingInDept">第{{ scope.row.rankingInDept }}名</span>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
```
#### 🔄 学生状态变更时的奖学金申请处理逻辑
- **休学**: `current_status = 'WITHDRAWN'`(撤回),保留重新申请可能
- **退学**: `current_status = 'REJECTED'`(拒绝),终止评选资格
- **毕业**: 保持 `APPROVED` 状态,保留获奖资格;其他状态设为 `WITHDRAWN`
#### 💾 SQL批量处理脚本
```sql
-- 参数说明:
-- @student_id: 学生ID
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- 更新奖学金申请状态
UPDATE jcbt_scholarship_application
SET current_status = CASE
WHEN @change_type = 'XIU_XUE' THEN 'WITHDRAWN' -- 休学:撤回,可重新申请
WHEN @change_type = 'TUI_XUE' THEN 'REJECTED' -- 退学:拒绝,终止评选资格
WHEN @change_type = 'BI_YE' THEN
CASE WHEN current_status = 'APPROVED' THEN 'APPROVED' -- 毕业:保持已通过状态
ELSE 'WITHDRAWN' -- 其他状态设为撤回
END
ELSE current_status
END,
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND current_status NOT IN ('REJECTED') -- 已拒绝的不重复处理
AND deleted = 0;
```
### 2. 奖学金获奖记录处理
#### 📊 数据库表结构
**表名**: `jcbt_scholarship_award`
**主键**: `id` (bigint(20))
**核心关联字段**:
- `application_id`: 申请ID
- `batch_id`: 批次ID
- `scholarship_type_id`: 奖学金类型ID
- `student_id`: 学生ID
- `student_no`: 学号
- `student_name`: 学生姓名
#### 🎯 核心业务字段
| 字段名 | 类型 | 说明 | 默认值 |
|--------|------|------|--------|
| `award_no` | varchar(50) | 获奖编号 | - |
| `award_amount` | decimal(10,2) | 奖励金额 | - |
| `award_level` | varchar(20) | 获奖等级 | NULL |
| `final_score` | decimal(8,2) | 最终得分 | NULL |
| `ranking_in_dept` | int(11) | 院系排名 | NULL |
| `award_date` | date | 获奖日期 | - |
| `certificate_no` | varchar(100) | 证书编号 | NULL |
| `is_published` | tinyint(1) | 是否公示 | 0 |
| `publish_date` | date | 公示日期 | NULL |
| `is_paid` | tinyint(1) | 是否已发放 | 0 |
| `pay_date` | date | 发放日期 | NULL |
#### 🔄 奖学金发放流程
##### 📋 完整发放流程
```
1. 申请审核通过 → 生成获奖记录 (jcbt_scholarship_award)
2. 获奖信息确认 → 分配获奖编号和证书编号
3. 公示期开始 → 更新公示状态 (is_published = 1)
4. 公示期结束 → 如无异议,准备发放
5. 资金拨付 → 更新发放状态 (is_paid = 1)
6. 学生状态变更 → 根据情况调整发放流程
```
##### 💻 后端实现逻辑
虽然没有找到对应的实体类,但从数据库表结构可以推断业务逻辑:
```java
// 奖学金获奖记录关键业务逻辑(推断)
public class ScholarshipAwardService {
// 证书生成
public void generateCertificate(Long awardId) {
// 生成证书编号
// 更新 certificate_no 字段
}
// 公示处理
public void publishAward(Long awardId, Date publishDate) {
// 更新 is_published = 1
// 设置 publish_date
}
// 资金发放
public void payAward(Long awardId, Date payDate) {
// 更新 is_paid = 1
// 设置 pay_date
}
}
```
##### 🎨 前端界面展示
**管理界面**: `jcbt/scholarship/award/index.vue`
**状态显示**: 公示和发放状态的展示
```vue
<!-- 公示状态列 -->
<el-table-column label="公示状态" align="center" width="100">
<template #default="scope">
<el-tag v-if="scope.row.isPublished" type="success">已公示</el-tag>
<el-tag v-else type="warning">未公示</el-tag>
</template>
</el-table-column>
<!-- 发放状态列 -->
<el-table-column label="发放状态" align="center" width="100">
<template #default="scope">
<el-tag v-if="scope.row.isPaid" type="success">已发放</el-tag>
<el-tag v-else type="info">未发放</el-tag>
</template>
</el-table-column>
<!-- 证书编号列 -->
<el-table-column label="证书编号" align="center" prop="certificateNo" width="150">
<template #default="scope">
<span v-if="scope.row.certificateNo">{{ scope.row.certificateNo }}</span>
<span v-else class="text-muted">待生成</span>
</template>
</el-table-column>
```
#### 🔄 学生状态变更时的获奖记录处理逻辑
##### 📋 处理策略说明
- **休学**: 获奖记录保留,暂停发放流程,保留恢复发放的可能
- **退学**: 获奖记录保留作为档案,但取消发放资格,记录撤销原因
- **毕业**: 确保完成发放流程,确保证书和资金发放完整
##### 💾 SQL批量处理脚本
```sql
-- 参数说明:
-- @student_id: 学生ID
-- @student_no: 学生学号
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- @change_date: 状态变更日期 (默认为当前日期)
SET @change_date = COALESCE(@change_date, CURDATE());
-- 1. 休学处理:暂停发放流程,保留获奖记录
UPDATE jcbt_scholarship_award
SET remark = CONCAT(COALESCE(remark, ''), ' | 学生休学(', @change_date, '),获奖记录保留,发放流程暂停'),
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND deleted = 0
AND @change_type = 'XIU_XUE';
-- 2. 退学处理:取消发放资格,保留获奖记录作为档案
UPDATE jcbt_scholarship_award
SET is_paid = CASE
WHEN is_paid = 0 THEN 0 -- 标记为不发放,保留记录作为档案
ELSE is_paid -- 已发放的不变
END,
pay_date = CASE
WHEN is_paid = 0 AND certificate_no IS NOT NULL THEN NULL -- 取消发放
ELSE pay_date
END,
remark = CONCAT(COALESCE(remark, ''), ' | 学生退学(', @change_date, '),取消发放资格,获奖记录作为档案保留'),
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND deleted = 0
AND @change_type = 'TUI_XUE';
-- 3. 毕业处理:确保完成发放流程,归档获奖记录
UPDATE jcbt_scholarship_award
SET is_paid = CASE
WHEN is_paid = 0 AND certificate_no IS NOT NULL THEN 1 -- 确保证书发放
ELSE is_paid
END,
pay_date = CASE
WHEN is_paid = 0 AND certificate_no IS NOT NULL THEN @change_date -- 确保证书发放
ELSE pay_date
END,
remark = CONCAT(COALESCE(remark, ''), ' | 学生毕业(', @change_date, '),获奖记录正式归档'),
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND deleted = 0
AND @change_type = 'BI_YE';
-- 统计处理结果
SELECT
@change_type as change_type,
COUNT(CASE WHEN @change_type = 'XIU_XUE' THEN 1 END) as leave_records,
COUNT(CASE WHEN @change_type = 'TUI_XUE' THEN 1 END) as drop_records,
COUNT(CASE WHEN @change_type = 'BI_YE' THEN 1 END) as graduate_records,
COUNT(*) as total_processed,
CONCAT('学生', @student_no, '状态变更,奖学金获奖记录处理完成') as operation_note
FROM jcbt_scholarship_award
WHERE student_id = @student_id
AND deleted = 0
AND (
(@change_type = 'XIU_XUE' AND remark LIKE CONCAT('%学生休学(', @change_date, ')%'))
OR (@change_type = 'TUI_XUE' AND remark LIKE CONCAT('%学生退学(', @change_date, ')%'))
OR (@change_type = 'BI_YE' AND remark LIKE CONCAT('%学生毕业(', @change_date, ')%'))
);
```
### 3. 勤工俭学申请处理
#### 📊 数据库表结构
**表名**: `jcbt_work_study_application`
**主键**: `id` (bigint(20), 雪花ID)
**核心关联字段**:
- `position_id`: 岗位ID
- `student_id`: 学生ID
- `student_number`: 学号
- `college_id`: 学院ID
#### 🎯 核心业务字段
| 字段名 | 类型 | 说明 | 默认值 |
|--------|------|------|--------|
| `status` | varchar(20) | 申请状态 | DRAFT |
| `application_date` | datetime | 申请提交时间 | - |
| `college_review_status` | varchar(20) | 学院审核状态 | NULL |
| `college_review_time` | datetime | 学院审核时间 | NULL |
| `final_review_status` | varchar(20) | 学生处审核状态 | NULL |
| `final_review_time` | datetime | 学生处审核时间 | NULL |
| `work_start_date` | date | 工作开始时间 | NULL |
| `work_end_date` | date | 工作结束时间 | NULL |
| `planned_work_hours` | int(11) | 计划工作时长 | NULL |
#### 🎯 申请状态字典
勤工俭学申请状态基于数据库字段定义,包含两级审核状态:
| 状态值 | 状态名称 | 说明 | 样式类 |
|--------|----------|------|--------|
| `DRAFT` | 草稿 | 申请草稿,尚未提交 | info |
| `SUBMITTED` | 已提交 | 已提交,等待审核 | warning |
| `COLLEGE_REVIEWING` | 学院审核中 | 学院正在审核 | primary |
| `COLLEGE_PASSED` | 学院审核通过 | 学院审核通过 | success |
| `COLLEGE_REJECTED` | 学院审核拒绝 | 学院审核拒绝 | danger |
| `FINAL_REVIEWING` | 学生处审核中 | 学生处正在审核 | primary |
| `APPROVED` | 审核通过 | 最终审核通过 | success |
| `REJECTED` | 审核拒绝 | 最终审核拒绝 | danger |
| `WITHDRAWN` | 已撤回 | 申请人撤回 | info |
**审核状态字段说明**:
- `college_review_status`: 学院审核状态(APPROVED/REJECTED)
- `final_review_status`: 学生处审核状态(APPROVED/REJECTED)
#### 🔄 勤工俭学审核流程
##### 📋 完整审核流程
```
1. 学生申请提交 → 状态: SUBMITTED
2. 学院审核 → 状态: COLLEGE_REVIEWING → COLLEGE_PASSED/COLLEGE_REJECTED
3. 学生处终审 → 状态: FINAL_REVIEWING → APPROVED/REJECTED
4. 工作安排 → 设置工作时间 (work_start_date, work_end_date)
5. 学生状态变更 → 相应流程调整
```
##### 💻 后端实现逻辑
**实体类**: `JcbtWorkStudyApplication.java`
**控制器**: `JcbtWorkStudyApplicationController.java`
**服务类**: `JcbtWorkStudyApplicationServiceImpl.java`
```java
// 勤工俭学申请实体类关键字段
@Data
@TableName("jcbt_work_study_application")
public class JcbtWorkStudyApplication extends BaseEntity {
private Long positionId; // 岗位ID
private String positionName; // 岗位名称
private Long studentId; // 学生ID
private String studentName; // 学生姓名
private String studentNumber; // 学号
private String status; // 申请状态
private String collegeReviewStatus; // 学院审核状态
private String finalReviewStatus; // 学生处审核状态
private Date workStartDate; // 工作开始时间
private Date workEndDate; // 工作结束时间
private Integer plannedWorkHours; // 计划工作时长
}
```
##### 🎨 前端界面展示
**管理界面**: `jcbt/workStudy/application/index.vue`
**状态显示**: 两级审核状态的展示
```vue
<!-- 申请状态列 -->
<el-table-column label="申请状态" align="center" prop="status" width="120">
<template #default="scope">
<dict-tag :options="work_study_application_status" :value="scope.row.status" />
</template>
</el-table-column>
<!-- 学院审核状态列 -->
<el-table-column label="学院审核" align="center" width="100">
<template #default="scope">
<el-tag v-if="scope.row.collegeReviewStatus === 'APPROVED'" type="success">通过</el-tag>
<el-tag v-else-if="scope.row.collegeReviewStatus === 'REJECTED'" type="danger">拒绝</el-tag>
<el-tag v-else type="warning">审核中</el-tag>
</template>
</el-table-column>
<!-- 学生处审核状态列 -->
<el-table-column label="学生处审核" align="center" width="100">
<template #default="scope">
<el-tag v-if="scope.row.finalReviewStatus === 'APPROVED'" type="success">通过</el-tag>
<el-tag v-else-if="scope.row.finalReviewStatus === 'REJECTED'" type="danger">拒绝</el-tag>
<el-tag v-else type="warning">审核中</el-tag>
</template>
</el-table-column>
<!-- 工作时间列 -->
<el-table-column label="工作时间" align="center" width="200">
<template #default="scope">
<span v-if="scope.row.workStartDate && scope.row.workEndDate">
{{ parseTime(scope.row.workStartDate, '{y}-{m}-{d}') }} 至
{{ parseTime(scope.row.workEndDate, '{y}-{m}-{d}') }}
</span>
<span v-else class="text-muted">未安排</span>
</template>
</el-table-column>
```
#### 🔄 学生状态变更时的勤工俭学申请处理逻辑
- **休学**: `status = 'WITHDRAWN'`(撤回),终止当前工作,保留重新申请可能
- **退学**: `status = 'REJECTED'`(拒绝),终止所有相关安排
- **毕业**: 保持 `APPROVED` 状态,其他状态设为 `WITHDRAWN`
#### 💾 SQL批量处理脚本
```sql
-- 参数说明:
-- @student_id: 学生ID
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- 更新勤工俭学申请状态
UPDATE jcbt_work_study_application
SET status = CASE
WHEN @change_type = 'XIU_XUE' THEN 'WITHDRAWN' -- 休学:撤回,可重新申请
WHEN @change_type = 'TUI_XUE' THEN 'REJECTED' -- 退学:拒绝,终止资格
WHEN @change_type = 'BI_YE' THEN
CASE WHEN status = 'APPROVED' THEN 'APPROVED' -- 毕业:保持已通过状态
ELSE 'WITHDRAWN' -- 其他状态设为撤回
END
ELSE status
END,
-- 更新审核状态
college_review_status = CASE
WHEN @change_type IN ('XIU_XUE', 'TUI_XUE') AND college_review_status IS NOT NULL THEN 'REJECTED'
ELSE college_review_status
END,
final_review_status = CASE
WHEN @change_type IN ('XIU_XUE', 'TUI_XUE') AND final_review_status IS NOT NULL THEN 'REJECTED'
ELSE final_review_status
END,
final_review_time = CASE
WHEN @change_type IN ('XIU_XUE', 'TUI_XUE') THEN NOW()
ELSE final_review_time
END,
final_reviewer_id = CASE
WHEN @change_type IN ('XIU_XUE', 'TUI_XUE') THEN @operator_id
ELSE final_reviewer_id
END,
final_reviewer_name = CASE
WHEN @change_type IN ('XIU_XUE', 'TUI_XUE') THEN '系统'
ELSE final_reviewer_name
END,
final_review_comment = CASE
WHEN @change_type IN ('XIU_XUE', 'TUI_XUE') THEN CONCAT('学生', @change_type, ',岗位申请自动终止')
ELSE final_review_comment
END,
-- 如果是进行中的工作,设置结束时间
work_end_date = CASE
WHEN @change_type IN ('XIU_XUE', 'TUI_XUE') AND status = 'APPROVED' THEN CURDATE()
ELSE work_end_date
END,
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND status NOT IN ('REJECTED', 'COLLEGE_REJECTED') -- 已拒绝的不重复处理
AND deleted = 0;
```
### 4. 贫困生认定申请处理
#### 📊 数据库表结构
**表名**: `jcbt_poverty_application`
**主键**: `id` (bigint(20))
**核心关联字段**:
- `student_id`: 学生ID
- `student_no`: 学号
- `college_id`: 学院ID
- `major_id`: 专业ID
- `class_id`: 班级ID
#### 🎯 核心业务字段
| 字段名 | 类型 | 说明 | 默认值 |
|--------|------|------|--------|
| `application_no` | varchar(32) | 申请编号 | - |
| `academic_year` | varchar(20) | 学年 | - |
| `term` | tinyint(1) | 学期 | - |
| `status` | varchar(20) | 申请状态 | DRAFT |
| `recognition_group` | varchar(20) | 认定群体 | - |
| `recognition_type_id` | bigint(20) | 认定类型ID | - |
| `submit_time` | datetime | 提交时间 | NULL |
| `audit_time` | datetime | 审核时间 | NULL |
#### 🎯 申请状态字典
贫困生认定申请状态基于数据库表定义:
| 状态值 | 状态名称 | 说明 | 样式类 |
|--------|----------|------|--------|
| `DRAFT` | 草稿 | 申请草稿,尚未提交 | info |
| `SUBMITTED` | 已提交 | 已提交,等待审核 | warning |
| `REVIEWING` | 审核中 | 正在审核中 | primary |
| `APPROVED` | 已通过 | 审核通过,获得贫困生资格 | success |
| `REJECTED` | 已拒绝 | 审核未通过 | danger |
| `WITHDRAWN` | 已撤回 | 申请人主动撤回 | info |
| `REVOKED` | 已撤销 | 资格被撤销 | danger |
**特殊状态说明**:
- `SUSPENDED`: 暂停状态(文档中提到但数据库中未定义,可能需要扩展)
- `COMPLETED`: 完成状态(毕业时的最终状态)
#### 🔄 贫困生认定流程
##### 📋 完整认定流程
```
1. 学生申请提交 → 状态: SUBMITTED
2. 班主任审核 → 学院评审小组审核
3. 学校认定委员会终审 → 状态: APPROVED/REJECTED
4. 资格确认 → 发放相关资助
5. 学生状态变更 → 根据情况调整认定资格
```
##### 💻 后端实现逻辑
**实体类**: `JcbtPovertyApplication.java`
**控制器**: `JcbtPovertyApplicationController.java`
**服务类**: `JcbtPovertyApplicationServiceImpl.java`
```java
// 贫困生认定申请实体类关键字段
@Data
@TableName("jcbt_poverty_application")
public class JcbtPovertyApplication extends BaseEntity {
private String applicationNo; // 申请编号
private String academicYear; // 学年
private Integer term; // 学期
private Long studentId; // 学生ID
private String studentNo; // 学号
private String studentName; // 学生姓名
private String status; // 申请状态
private String recognitionGroup; // 认定群体
private Long recognitionTypeId; // 认定类型ID
private Date submitTime; // 提交时间
private Date auditTime; // 审核时间
}
```
##### 🎨 前端界面展示
**管理界面**: `jcbt/poverty/application/index.vue`
**状态显示**: 认定状态的展示
```vue
<!-- 申请状态列 -->
<el-table-column label="申请状态" align="center" prop="status" width="120">
<template #default="scope">
<dict-tag :options="poverty_application_status" :value="scope.row.status" />
</template>
</el-table-column>
<!-- 认定群体列 -->
<el-table-column label="认定群体" align="center" prop="recognitionGroup" width="120">
<template #default="scope">
<span>{{ scope.row.recognitionGroup }}</span>
</template>
</el-table-column>
<!-- 认定类型列 -->
<el-table-column label="认定类型" align="center" prop="recognitionTypeName" width="150">
<template #default="scope">
<span>{{ scope.row.recognitionTypeName }}</span>
</template>
</el-table-column>
```
#### 🔄 学生状态变更时的贫困生认定处理逻辑
- **休学**: 认定记录保留,资格暂时维持(可根据业务需求调整为WITHDRAWN)
- **退学**: 认定资格撤销(REVOKED),终止贫困生资格
- **毕业**: 认定流程完成,记录保持APPROVED状态
#### 💾 SQL批量处理脚本
```sql
-- 参数说明:
-- @student_id: 学生ID
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- 更新贫困生认定状态
UPDATE jcbt_poverty_application
SET status = CASE
WHEN @change_type = 'XIU_XUE' THEN
CASE WHEN status = 'APPROVED' THEN 'APPROVED' -- 休学:保持认定资格
ELSE status
END
WHEN @change_type = 'TUI_XUE' THEN 'REVOKED' -- 退学:撤销认定
WHEN @change_type = 'BI_YE' THEN status -- 毕业:保持现有状态
ELSE status
END,
-- 更新审核信息(仅退学时更新)
audit_time = CASE
WHEN @change_type = 'TUI_XUE' THEN NOW()
ELSE audit_time
END,
auditor_id = CASE
WHEN @change_type = 'TUI_XUE' THEN @operator_id
ELSE auditor_id
END,
auditor_name = CASE
WHEN @change_type = 'TUI_XUE' THEN '系统'
ELSE auditor_name
END,
audit_opinion = CASE
WHEN @change_type = 'TUI_XUE' THEN '学生退学,贫困生认定撤销'
ELSE audit_opinion
END,
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND status NOT IN ('REVOKED') -- 已撤销的不重复处理
AND deleted = 0;
```
## 🔗 经济资助业务关联处理
### 1. 业务数据一致性保证
#### 🛡️ 事务处理要求
```sql
-- 建议在学生状态变更的大事务中包含所有经济资助处理
START TRANSACTION;
-- 1. 更新学生学籍状态
UPDATE xg_student_msg SET student_status = ? WHERE id = ?;
-- 2. 处理奖学金申请
UPDATE jcbt_scholarship_application SET current_status = ? WHERE student_id = ?;
-- 3. 处理奖学金获奖记录
UPDATE jcbt_scholarship_award SET remark = CONCAT(remark, ?) WHERE student_id = ?;
-- 4. 处理勤工俭学申请
UPDATE jcbt_work_study_application SET status = ? WHERE student_id = ?;
-- 5. 处理贫困生认定
UPDATE jcbt_poverty_application SET status = ? WHERE student_id = ?;
COMMIT;
```
#### 📊 处理统计表
| 业务模块 | 涉及表数量 | 处理类型 | 重要程度 |
|----------|------------|----------|----------|
| 奖学金申请 | 1 (jcbt_scholarship_application) | 状态更新 | ⭐⭐⭐⭐ |
| 奖学金获奖 | 1 (jcbt_scholarship_award) | 发放调整 | ⭐⭐⭐⭐⭐ |
| 勤工俭学 | 1 (jcbt_work_study_application) | 审核终止 | ⭐⭐⭐⭐ |
| 贫困生认定 | 1 (jcbt_poverty_application) | 资格调整 | ⭐⭐⭐⭐ |
| **总计** | **4个表** | **状态更新+流程调整** | **⭐⭐⭐⭐⭐** |
### 2. 业务规则和约束
#### 📋 状态变更矩阵
| 当前状态 | 休学处理 | 退学处理 | 毕业处理 |
|----------|----------|----------|----------|
| 奖学金申请中 | 撤回(WITHDRAWN) | 拒绝(REJECTED) | 保持通过/撤回 |
| 奖学金已通过 | 记录保留 | 取消发放 | 确保证书发放 |
| 勤工俭学进行中 | 撤回(WITHDRAWN) | 拒绝(REJECTED) | 保持通过/撤回 |
| 贫困生已认定 | 保持认定资格 | 撤销(REVOKED) | 保持认定状态 |
#### ⚠️ 重要业务约束
- **资金安全**: 奖学金获奖记录涉及实际资金,不得随意删除
- **证书管理**: 已发放证书的记录必须保留发放状态
- **公示流程**: 已公示的奖学金变更可能需要重新公示
- **档案完整性**: 所有经济资助记录作为学生档案的重要组成部分
### 3. 监控和审计要求
#### 📊 数据完整性检查
```sql
-- 检查处理结果的完整性
SELECT
'奖学金申请' as module,
COUNT(*) as total_count,
COUNT(CASE WHEN current_status IN ('WITHDRAWN', 'REJECTED', 'APPROVED') THEN 1 END) as processed_count
FROM jcbt_scholarship_application
WHERE student_id = @student_id AND deleted = 0
UNION ALL
SELECT
'奖学金获奖' as module,
COUNT(*) as total_count,
COUNT(CASE WHEN remark LIKE '%学生%' THEN 1 END) as processed_count
FROM jcbt_scholarship_award
WHERE student_id = @student_id AND deleted = 0
UNION ALL
SELECT
'勤工俭学' as module,
COUNT(*) as total_count,
COUNT(CASE WHEN status IN ('WITHDRAWN', 'REJECTED', 'APPROVED') THEN 1 END) as processed_count
FROM jcbt_work_study_application
WHERE student_id = @student_id AND deleted = 0
UNION ALL
SELECT
'贫困生认定' as module,
COUNT(*) as total_count,
COUNT(CASE WHEN status IN ('SUSPENDED', 'REVOKED', 'COMPLETED') THEN 1 END) as processed_count
FROM jcbt_poverty_application
WHERE student_id = @student_id AND deleted = 0;
```
#### 📝 审计日志记录
```sql
-- 记录经济资助处理的审计日志
INSERT INTO sys_oper_log (
title, business_type, method, request_method, operator_type,
oper_name, dept_name, oper_url, oper_ip, oper_location,
oper_param, json_result, status, error_msg, oper_time
) VALUES (
'经济资助业务处理', 'UPDATE', 'processEconomicSupport', 'PROCEDURE', 'MANAGE',
@operator_name, @dept_name, '/economic/support/process', @client_ip, @location,
CONCAT('学生ID:', @student_id, ',学号:', @student_no, ',变更类型:', @change_type),
@process_result_json, 0, NULL, NOW()
);
```
**注意**: 根据用户需求,经济资助处理不是学生退学、休学、毕业的核心业务逻辑,已删除相关处理内容。如有需要,可根据实际业务需求另行处理。
---
## 📅 更新记录
- **2025-12-XX**: 分文档创建,包含经济资助处理逻辑
- **2025-12-XX**: 基于代码分析完善技术实现细节和业务逻辑
- **后续更新**: 根据实际业务需求持续完善
---
*本分文档详细说明学生状态变更时的经济资助业务处理逻辑和技术实现,包括奖学金、勤工俭学和贫困生认定的完整处理方案。*
- **2025-12-XX**: 根据用户需求删除经济资助处理逻辑,保留为非核心业务
- **后续更新**: 如需要经济资助处理,可根据实际业务需求重新添加
@@ -2,7 +2,7 @@
## 📋 概述
本分文档详细说明学生退学、休学、毕业处理中的其他业务模块处理内容,包括住宿管理、心理健康、实践活动和其他关联信息的变更逻辑。
本分文档详细说明学生退学、休学、毕业、复学处理中的其他业务模块处理内容,包括住宿管理和其他关联信息的变更逻辑。
## 🏠 住宿管理
@@ -16,843 +16,52 @@
| `id` | bigint(20) | 主键ID | - |
| `student_id` | bigint(20) | 学生ID | - |
| `bed_id` | bigint(20) | 床位ID | - |
| `room_id` | bigint(20) | 房间ID | - |
| `building_id` | bigint(20) | 宿舍楼ID | - |
| `campus_id` | bigint(20) | 校区ID | - |
| `status` | varchar(10) | 状态(0正常 1已退宿) | - |
| `assign_time` | datetime | 分配时间 | - |
| `checkout_time` | datetime | 退宿时间 | NULL |
| `remark` | varchar(100) | 备注 | NULL |
| `deleted` | varchar(1) | 删除标志 | 0 |
**关键字段说明** (基于实体类代码分析):
- `studentId`: 学生ID
- `bedId`: 床位ID
- `roomId`: 房间ID
- `buildingId`: 宿舍楼ID
- `campusId`: 校区ID
- `status`: 状态(0正常 1已退宿)
- `assignTime`: 分配时间
- `checkoutTime`: 退宿时间
##### 宿舍申请表 (xg_dorm_application)
| 字段名 | 类型 | 说明 |
|--------|------|------|
| `application_type` | char(1) | 申请类型(1调宿 2退宿) |
| `application_status` | varchar(20) | 审批状态 |
#### 🔄 宿舍退宿业务流程
##### 📋 完整退宿流程
```
1. 接收退宿申请或状态变更触发
2. 验证学生当前住宿状态
3. 检查是否有未完成的宿舍申请
4. 更新床位分配状态为已退宿
5. 处理相关的宿舍申请状态
6. 记录退宿操作日志
7. 释放床位资源供其他学生使用
```
##### 💻 后端实现逻辑
**实体类**: `XgDormBedStudent.java`
**服务类**: `XgDormBedStudentServiceImpl.java`
```java
// 宿舍床位学生分配实体类关键字段
@Data
@TableName("xg_dorm_bed_student")
public class XgDormBedStudent extends TenantEntity {
private Long studentId; // 学生ID
private Long bedId; // 床位ID
private Long roomId; // 房间ID
private Long buildingId; // 宿舍楼ID
private Long campusId; // 校区ID
private String status; // 状态:0-正常,1-已退宿
private Date assignTime; // 分配时间
private Date checkoutTime; // 退宿时间
private String remark; // 备注
}
```
##### 🎨 前端界面展示
**管理界面**: `xg/dorm/bedStudent/index.vue`
**状态显示**: 住宿状态的展示
```vue
<!-- 住宿状态列 -->
<el-table-column label="住宿状态" align="center" prop="status" width="100">
<template #default="scope">
<el-tag v-if="scope.row.status === '0'" type="success">在住</el-tag>
<el-tag v-else-if="scope.row.status === '1'" type="info">已退宿</el-tag>
<el-tag v-else type="warning">未知</el-tag>
</template>
</el-table-column>
<!-- 退宿时间列 -->
<el-table-column label="退宿时间" align="center" prop="checkoutTime" width="180">
<template #default="scope">
<span v-if="scope.row.checkoutTime">
{{ parseTime(scope.row.checkoutTime, '{y}-{m}-{d} {h}:{i}') }}
</span>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
```
#### 🔄 学生状态变更时的宿舍处理逻辑
##### 📋 处理策略说明
- **休学**: 可选择保留宿舍或办理暂退宿(根据学校政策)
- **退学**: 必须办理正式退宿,释放床位资源
- **毕业**: 办理毕业退宿,释放床位资源
##### 💾 SQL批量处理脚本
```sql
-- 参数说明:
-- @student_id: 学生ID
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, FU_XUE-复学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- @checkout_time: 退宿时间 (默认为当前时间)
SET @checkout_time = COALESCE(@checkout_time, NOW());
-- 1. 备份原始数据到his表
INSERT INTO xg_dorm_bed_student_his SELECT * FROM xg_dorm_bed_student WHERE student_id = @student_id;
-- 1. 退学/毕业:必须办理正式退宿
-- 2. 清空学生信息表的住宿信息
UPDATE xg_student_msg SET
dormitory_building = NULL,
dormitory_room = NULL,
ext_field1 = NULL,
update_time = NOW(),
update_by = @operator_id
WHERE id = @student_id;
-- 3. 处理宿舍住宿关系(退学、毕业时退宿)
UPDATE xg_dorm_bed_student
SET status = '1', -- 已退宿
checkout_time = @checkout_time,
update_by = @operator_id,
SET status = '1',
checkout_time = NOW(),
update_time = NOW(),
remark = CONCAT(COALESCE(remark, ''), ' | 学生', @change_type, '退宿')
update_by = @operator_id
WHERE student_id = @student_id
AND status = '0' -- 当前在住
AND deleted = '0'
AND @change_type IN ('TUI_XUE', 'BI_YE');
-- 2. 处理进行中的宿舍申请
UPDATE xg_dorm_application
SET application_status = CASE
WHEN application_type = '1' THEN 'cancel' -- 调宿申请:取消
WHEN application_type = '2' THEN 'finish' -- 退宿申请:完成
ELSE application_status
END,
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND application_status IN ('draft', 'waiting') -- 草稿和待审核状态
AND deleted = '0';
-- 3. 休学处理(可选:根据学校政策决定是否保留宿舍)
-- 如果学校允许休学保留宿舍,则注释掉下面的UPDATE语句
-- 如果需要休学退宿,取消下面的注释:
-- UPDATE xg_dorm_bed_student
-- SET status = '1', -- 已退宿
-- checkout_time = @checkout_time,
-- update_by = @operator_id,
-- update_time = NOW(),
-- remark = CONCAT(COALESCE(remark, ''), ' | 学生休学暂退宿')
-- WHERE student_id = @student_id
-- AND status = '0'
-- AND deleted = '0'
-- AND @change_type = 'XIU_XUE';
```
## 🧠 心理健康管理
### 2. 心理咨询预约处理
#### 📊 数据库表结构
##### 咨询预约表 (xljk_consultation_appointment)
| 字段名 | 类型 | 说明 | 默认值 |
|--------|------|------|--------|
| `id` | bigint(20) | 主键ID | - |
| `student_id` | bigint(20) | 学生ID | - |
| `counselor_id` | bigint(20) | 咨询师ID | - |
| `schedule_id` | bigint(20) | 预约时段ID | - |
| `appointment_date` | date | 预约日期 | - |
| `status` | char(1) | 状态(0待咨询 1已完成 2已取消 3未到访) | '0' |
| `appointment_time` | varchar(20) | 预约时间段 | - |
| `reason` | varchar(500) | 咨询原因 | NULL |
| `consult_theme` | varchar(50) | 咨询主题 | NULL |
##### 未到访记录表 (xljk_consultation_no_show)
| 字段名 | 类型 | 说明 |
|--------|------|------|
| `student_id` | bigint(20) | 学生ID |
| `appointment_id` | bigint(20) | 预约记录ID |
| `no_show_date` | date | 未到访日期 |
| `expire_date` | date | 限制到期日 |
| `status` | char(1) | 状态(0生效中 1已过期 2已解除) |
#### 🎯 预约状态字典
| 状态值 | 状态名称 | 说明 |
|--------|----------|------|
| `0` | 待咨询 | 已预约,等待咨询 |
| `1` | 已完成 | 咨询已完成 |
| `2` | 已取消 | 预约已取消 |
| `3` | 未到访 | 预约时间已过,未到访 |
#### 🔄 心理咨询预约处理流程
##### 📋 处理逻辑说明
- **预约取消**: 取消所有未来预约,状态改为"已取消"
- **未到访处理**: 解除未到访限制,避免影响其他学生预约
- **历史保留**: 保留已完成的咨询记录作为心理健康档案
##### 💻 后端实现逻辑
**实体类**: `XljkConsultationAppointment.java`
**服务类**: `XljkConsultationAppointmentServiceImpl.java`
```java
// 心理咨询预约实体类关键字段
@Data
public class XljkConsultationAppointment {
private Long id;
private Long studentId;
private Long counselorId;
private Long scheduleId;
private Date appointmentDate;
private String status; // 状态:0-待咨询,1-已完成,2-已取消,3-未到访
private String appointmentTime;
private String reason;
private String consultTheme;
private Integer communicationDuration; // 沟通时长
private String consultationContent; // 咨询内容
private String counselorSuggestion; // 咨询师建议
}
```
##### 🎨 前端界面展示
**管理界面**: `xljk/consultation/appointment/index.vue`
**状态显示**: 预约状态的展示
```vue
<!-- 预约状态列 -->
<el-table-column label="预约状态" align="center" prop="status" width="100">
<template #default="scope">
<el-tag v-if="scope.row.status === '0'" type="primary">待咨询</el-tag>
<el-tag v-else-if="scope.row.status === '1'" type="success">已完成</el-tag>
<el-tag v-else-if="scope.row.status === '2'" type="warning">已取消</el-tag>
<el-tag v-else-if="scope.row.status === '3'" type="danger">未到访</el-tag>
</template>
</el-table-column>
<!-- 预约日期列 -->
<el-table-column label="预约日期" align="center" prop="appointmentDate" width="120">
<template #default="scope">
{{ parseTime(scope.row.appointmentDate, '{y}-{m}-{d}') }}
</template>
</el-table-column>
```
#### 🔄 学生状态变更时的心理咨询处理逻辑
##### 📋 处理策略说明
- **休学**: 取消未来预约,保留历史咨询记录
- **退学**: 取消未来预约,解除未到访限制
- **毕业**: 取消未来预约,保留完整心理健康档案
##### 💾 SQL批量处理脚本
```sql
-- 参数说明:
-- @student_id: 学生ID
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- 1. 取消未来的心理咨询预约
UPDATE xljk_consultation_appointment
SET status = '2', -- 已取消
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND status = '0' -- 待咨询
AND appointment_date >= CURDATE() -- 未来的预约
AND deleted = 0;
-- 2. 处理未到访记录(解除限制)
UPDATE xljk_consultation_no_show
SET status = '2', -- 已解除
expire_date = NOW(), -- 设置过期时间为当前时间
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND status = '0' -- 生效中
AND deleted = 0;
```
## 🎓 实践活动管理
### 3. 学生活动报名处理
#### 📊 数据库表结构
##### 活动报名表 (stgl_activity_registration)
| 字段名 | 类型 | 说明 | 默认值 |
|--------|------|------|--------|
| `id` | bigint(20) | 主键ID | - |
| `activity_id` | bigint(20) | 活动ID | - |
| `user_id` | bigint(20) | 用户ID | - |
| `student_no` | varchar(50) | 学号 | - |
| `student_name` | varchar(50) | 学生姓名 | - |
| `registration_type` | varchar(10) | 报名类型(1个人 2团队) | - |
| `team_id` | bigint(20) | 团队ID | NULL |
| `registration_time` | datetime | 报名时间 | - |
| `approval_status` | varchar(10) | 审核状态(0待审核 1通过 2驳回) | '0' |
**关键字段说明** (基于实体类代码分析):
- `activityId`: 活动ID
- `userId`: 用户ID
- `studentNo`: 学号
- `registrationType`: 报名类型(1个人 2团队)
- `approvalStatus`: 审核状态(0待审核 1通过 2驳回)
- `approvalTime`: 审核时间
- `score`: 获得成绩
| `approval_time` | datetime | 审核时间 | NULL |
| `score` | varchar(20) | 获得成绩 | NULL |
#### 🎯 审核状态字典
| 状态值 | 状态名称 | 说明 |
|--------|----------|------|
| `0` | 待审核 | 报名已提交,等待审核 |
| `1` | 通过 | 审核通过,可以参加活动 |
| `2` | 驳回 | 审核未通过 |
#### 🔄 学生活动报名处理流程
##### 📋 处理逻辑说明
- **活动状态判断**: 需要关联活动表判断活动是否已开始或结束
- **报名类型区分**: 个人报名和团队报名的处理逻辑不同
- **成绩处理**: 已获得成绩的记录需要特殊处理
##### 💻 后端实现逻辑
**实体类**: `StglActivityRegistration.java`
**服务类**: `StglActivityRegistrationServiceImpl.java`
```java
// 学生活动报名实体类关键字段
@Data
@TableName("stgl_activity_registration")
public class StglActivityRegistration extends TenantEntity {
private Long activityId; // 活动ID
private Long userId; // 用户ID
private String studentNo; // 学号
private String studentName; // 学生姓名
private String registrationType; // 报名类型:1-个人,2-团队
private Long teamId; // 团队ID
private Date registrationTime; // 报名时间
private String approvalStatus; // 审核状态:0-待审核,1-通过,2-驳回
private Date approvalTime; // 审核时间
private Long approvalBy; // 审核人
private String approvalRemark; // 审核意见
private String score; // 获得成绩
}
```
##### 🎨 前端界面展示
**管理界面**: `stgl/activity/registration/index.vue`
**状态显示**: 报名审核状态的展示
```vue
<!-- 审核状态列 -->
<el-table-column label="审核状态" align="center" prop="approvalStatus" width="100">
<template #default="scope">
<el-tag v-if="scope.row.approvalStatus === '0'" type="warning">待审核</el-tag>
<el-tag v-else-if="scope.row.approvalStatus === '1'" type="success">通过</el-tag>
<el-tag v-else-if="scope.row.approvalStatus === '2'" type="danger">驳回</el-tag>
</template>
</el-table-column>
<!-- 报名类型列 -->
<el-table-column label="报名类型" align="center" prop="registrationType" width="100">
<template #default="scope">
<span v-if="scope.row.registrationType === '1'">个人</span>
<span v-else-if="scope.row.registrationType === '2'">团队</span>
</template>
</el-table-column>
```
#### 🔄 学生状态变更时的活动报名处理逻辑
##### 📋 处理策略说明
- **休学**: 驳回未开始的活动报名,保留已完成活动的记录和成绩
- **退学**: 驳回所有进行中的活动报名
- **毕业**: 可选择保留已完成活动的记录和成绩,驳回进行中的报名
##### 💾 SQL批量处理脚本
```sql
-- 参数说明:
-- @student_no: 学生学号
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- 驳回进行中的活动报名
UPDATE stgl_activity_registration
SET approval_status = '2', -- 驳回状态
approval_time = NOW(),
approval_by = @operator_id,
approval_remark = CONCAT('学生', @change_type, ',活动报名自动驳回'),
update_by = @operator_id,
update_time = NOW()
WHERE student_no = @student_no
AND approval_status = '1' -- 已通过的报名
AND EXISTS (
SELECT 1 FROM stgl_activity a
WHERE a.id = stgl_activity_registration.activity_id
AND a.activity_start_time > NOW() -- 未开始的活动
AND (@change_type = 'TUI_XUE' OR -- 退学:驳回所有进行中
(@change_type IN ('XIU_XUE', 'BI_YE') AND a.activity_end_time > NOW())) -- 休学/毕业:只驳回未结束的
)
AND deleted = 0;
```
### 4. 志愿者队伍管理
#### 📊 数据库表结构
**表名**: `stgl_volunteer_team` (队伍信息表)
- 主要存储志愿者队伍的基本信息
- 成员关系可能通过其他表关联或记录在日志中
#### 🔄 志愿者关系处理逻辑
##### 📋 处理策略说明
- **队伍管理**: 志愿者队伍本身不因学生状态变更而改变
- **成员关系**: 如果有专门的成员关系表,需要更新成员状态
- **日志记录**: 记录志愿服务关系的变更情况
##### 💾 SQL批量处理脚本
```sql
-- 参数说明:
-- @student_no: 学生学号
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- 注意:stgl_volunteer_team表主要是队伍信息表
-- 如果存在志愿者成员关系表,执行成员状态更新
-- 这里以假设存在stgl_volunteer_member表为例
-- 如果存在志愿者成员关系表,执行:
UPDATE stgl_volunteer_member
SET member_status = 'INACTIVE',
leave_time = NOW(),
leave_reason = CONCAT('学生', @change_type),
update_by = @operator_id,
update_time = NOW()
WHERE student_no = @student_no
AND member_status = 'ACTIVE'
AND deleted = 0;
-- 记录操作日志
INSERT INTO sys_oper_log (
title, business_type, method, request_method, operator_type,
oper_name, dept_name, oper_url, oper_ip, oper_location,
oper_param, json_result, status, error_msg, oper_time, cost_time
) VALUES (
'志愿者管理', 'OTHER', 'updateVolunteerStatus', 'UPDATE', 'MANAGE',
'系统', '系统', '/volunteer/status/update', '127.0.0.1', '本地',
CONCAT('学生', @student_no, '状态变更(', @change_type, '),志愿者关系处理'),
'{}', 0, NULL, NOW(), 0
);
```
### 5. 实习信息终止处理
#### 📊 数据库表结构
##### 实习信息表 (zsjy_internship_info)
| 字段名 | 类型 | 说明 | 默认值 |
|--------|------|------|--------|
| `internship_id` | bigint(20) | 主键ID | - |
| `student_id` | bigint(20) | 学生ID | - |
| `student_no` | varchar(50) | 学号 | - |
| `student_name` | varchar(100) | 学生姓名 | - |
| `company_name` | varchar(200) | 实习单位名称 | - |
| `position_name` | varchar(100) | 岗位名称 | - |
| `start_date` | date | 实习开始日期 | - |
| `end_date` | date | 实习结束日期 | - |
| `actual_end_date` | date | 实际结束日期 | NULL |
| `internship_status` | char(1) | 实习状态(1进行中 2已完成 3中断) | '1' |
**关键字段说明** (基于实体类代码分析):
- `internshipId`: 实习ID
- `studentId`: 学生ID
- `studentNo`: 学号
- `companyName`: 实习单位名称
- `positionName`: 岗位名称
- `startDate`: 实习开始日期
- `endDate`: 实习结束日期
- `actualEndDate`: 实际结束日期
- `internshipStatus`: 实习状态(1进行中 2已完成 3中断)
- `changeReason`: 变更原因
| `internship_allowance` | decimal(10,2) | 实习津贴 | NULL |
#### 🎯 实习状态字典
| 状态值 | 状态名称 | 说明 |
|--------|----------|------|
| `1` | 进行中 | 实习正在进行 |
| `2` | 已完成 | 实习已按期完成 |
| `3` | 中断 | 实习因故中断 |
#### 🔄 实习信息终止处理流程
##### 📋 处理逻辑说明
- **状态判断**: 需要根据当前实习状态和学生变更类型决定处理方式
- **时间管理**: 正确设置实际结束日期
- **关联数据**: 实习相关的日报、月报、访问记录等会自动停止更新
- **津贴结算**: 可能涉及实习津贴的结算处理
##### 💻 后端实现逻辑
**实体类**: `ZsjyInternshipInfo.java`
**服务类**: `ZsjyInternshipServiceImpl.java`
```java
// 实习信息实体类关键字段
@Data
@TableName("zsjy_internship_info")
public class ZsjyInternshipInfo extends BaseEntity {
private Long internshipId; // 实习ID
private Long previousInternshipId; // 前一条实习记录ID
private Long studentId; // 学生ID
private String studentNo; // 学号
private String studentName; // 学生姓名
private String companyName; // 实习单位名称
private String positionName; // 岗位名称
private BigDecimal internshipAllowance; // 实习津贴
private Date startDate; // 实习开始日期
private Date endDate; // 实习结束日期
private Date actualEndDate; // 实际结束日期
private String internshipStatus; // 实习状态:1-进行中,2-已完成,3-中断
private String changeReason; // 变更原因
}
```
##### 🎨 前端界面展示
**管理界面**: `zsjy/internship/info/index.vue`
**状态显示**: 实习状态的展示
```vue
<!-- 实习状态列 -->
<el-table-column label="实习状态" align="center" prop="internshipStatus" width="100">
<template #default="scope">
<el-tag v-if="scope.row.internshipStatus === '1'" type="primary">进行中</el-tag>
<el-tag v-else-if="scope.row.internshipStatus === '2'" type="success">已完成</el-tag>
<el-tag v-else-if="scope.row.internshipStatus === '3'" type="warning">已中断</el-tag>
</template>
</el-table-column>
<!-- 实习单位列 -->
<el-table-column label="实习单位" align="center" prop="companyName" width="200" show-overflow-tooltip />
<!-- 实习时间列 -->
<el-table-column label="实习时间" align="center" width="200">
<template #default="scope">
{{ parseTime(scope.row.startDate, '{y}-{m}-{d}') }} 至
{{ parseTime(scope.row.actualEndDate || scope.row.endDate, '{y}-{m}-{d}') }}
</template>
</el-table-column>
```
#### 🔄 学生状态变更时的实习信息处理逻辑
##### 📋 处理策略说明
- **休学**: 设置为"中断"状态,保留重新实习的可能
- **退学**: 设置为"中断"状态,终止实习关系
- **毕业**: 如果实习已完成保持"已完成",否则设为"中断"
##### 💾 SQL批量处理脚本
```sql
-- 参数说明:
-- @student_id: 学生ID
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- 终止进行中的实习
UPDATE zsjy_internship_info
SET internship_status = CASE
WHEN @change_type = 'BI_YE' AND internship_status = '2' THEN '2' -- 毕业:保持已完成状态
ELSE '3' -- 休学/退学:设为中断
END,
actual_end_date = CASE
WHEN @change_type IN ('XIU_XUE', 'TUI_XUE') THEN CURDATE()
WHEN @change_type = 'BI_YE' AND internship_status != '2' THEN CURDATE()
ELSE actual_end_date
END,
change_reason = CONCAT(COALESCE(change_reason, ''), ' | 学生', @change_type,
CASE WHEN @change_type = 'BI_YE' AND internship_status = '2' THEN ',实习正常完成'
ELSE ',实习中断' END),
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND internship_status IN ('1', '2') -- 进行中或已完成的实习
AND deleted = 0;
-- 注意:实习结束后,相关的日报、月报、统计、访问记录等数据会自动停止更新
-- 如需清理历史数据,可根据业务需求另行处理
```
## 📝 其他关联信息处理
### 6. 学生请假记录处理
#### 📊 数据库表结构
##### 请假记录表 (xg_student_leave)
| 字段名 | 类型 | 说明 | 默认值 |
|--------|------|------|--------|
| `id` | bigint(20) | 主键ID | - |
| `student_id` | bigint(20) | 学生ID | - |
| `leave_type` | varchar(20) | 请假类型 | - |
| `leave_reason` | text | 请假原因 | - |
| `leave_start_date` | date | 请假开始日期 | - |
| `leave_end_date` | date | 请假结束日期 | - |
| `leave_status` | varchar(20) | 请假状态 | - |
| `cancel_reason` | varchar(500) | 取消原因 | NULL |
##### 销假记录表 (xg_student_cancel_leave)
| 字段名 | 类型 | 说明 |
|--------|------|------|
| `id` | bigint(20) | 主键ID |
| `student_id` | bigint(20) | 学生ID |
| `cancel_status` | varchar(20) | 销假状态 |
#### 🔄 请假记录处理逻辑
##### 📋 处理策略说明
- **进行中请假**: 取消未结束的请假记录
- **销假处理**: 更新相应的销假状态
- **历史保留**: 保留已完成的请假记录
##### 💾 SQL批量处理脚本
```sql
-- 参数说明:
-- @student_id: 学生ID
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- 处理进行中的请假记录
UPDATE xg_student_leave
SET leave_status = 'CANCELLED', -- 取消状态
cancel_reason = CONCAT('学生', @change_type, ',请假自动取消'),
cancel_time = NOW(),
cancel_by = @operator_id,
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND leave_status IN ('PENDING', 'APPROVED') -- 待审核、已通过
AND leave_end_date >= CURDATE() -- 未结束的请假
AND deleted = 0;
-- 处理相应的销假记录
UPDATE xg_student_cancel_leave
SET cancel_status = 'AUTO_CANCELLED',
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND cancel_status = 'PENDING'
AND deleted = 0;
```
### 7. 宿舍卫生检查处理
#### 📊 数据库表结构
##### 卫生检查学生评分表 (xg_dorm_hygiene_student_score)
| 字段名 | 类型 | 说明 | 默认值 |
|--------|------|------|--------|
| `id` | bigint(20) | 主键ID | - |
| `student_id` | bigint(20) | 学生ID | - |
| `check_id` | bigint(20) | 检查ID | - |
| `check_date` | date | 检查日期 | - |
| `score` | decimal(5,2) | 得分 | - |
| `remark` | varchar(500) | 备注 | NULL |
#### 🔄 卫生检查处理逻辑
##### 📋 处理策略说明
- **历史数据保留**: 卫生检查记录作为重要档案,通常不删除
- **未来检查备注**: 为未来的卫生检查记录添加状态变更备注
- **评分数据完整性**: 确保持有评分数据的完整性
##### 💾 SQL批量处理脚本
```sql
-- 参数说明:
-- @student_id: 学生ID
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- 为未来的卫生检查记录添加备注
UPDATE xg_dorm_hygiene_student_score
SET remark = CONCAT(COALESCE(remark, ''), ' | 学生', @change_type, ',卫生检查记录'),
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND check_date >= CURDATE() -- 未来的检查
AND deleted = 0;
```
### 8. 宿舍违纪记录处理
#### 📊 数据库表结构
##### 宿舍违纪表 (xg_dorm_violation)
| 字段名 | 类型 | 说明 | 默认值 |
|--------|------|------|--------|
| `id` | bigint(20) | 主键ID | - |
| `student_id` | bigint(20) | 学生ID | - |
| `violation_type` | varchar(50) | 违纪类型 | - |
| `violation_date` | date | 违纪日期 | - |
| `violation_description` | text | 违纪描述 | - |
| `punishment` | varchar(200) | 处罚措施 | - |
| `remark` | varchar(500) | 备注 | NULL |
#### 🔄 违纪记录处理逻辑
##### 📋 处理策略说明
- **档案数据保留**: 违纪记录作为学生管理档案,必须永久保留
- **状态变更备注**: 添加状态变更信息,便于档案管理
- **历史追溯**: 确保违纪记录的完整性和可追溯性
##### 💾 SQL批量处理脚本
```sql
-- 参数说明:
-- @student_id: 学生ID
-- @change_type: 变更类型 (XIU_XUE-休学, TUI_XUE-退学, BI_YE-毕业)
-- @operator_id: 操作员ID
-- 为违纪记录添加状态变更备注
UPDATE xg_dorm_violation
SET remark = CONCAT(COALESCE(remark, ''), ' | 学生', @change_type, ',违纪记录归档'),
update_by = @operator_id,
update_time = NOW()
WHERE student_id = @student_id
AND deleted = 0;
```
## 🔗 其他业务模块关联处理
### 1. 业务数据一致性保证
#### 🛡️ 事务处理要求
```sql
-- 建议在学生状态变更的大事务中包含所有其他业务处理
START TRANSACTION;
-- 1. 更新学生学籍状态
UPDATE xg_student_msg SET student_status = ? WHERE id = ?;
-- 2. 处理宿舍退宿
UPDATE xg_dorm_bed_student SET status = '1' WHERE student_id = ?;
-- 3. 处理心理咨询预约
UPDATE xljk_consultation_appointment SET status = '2' WHERE student_id = ?;
-- 4. 处理学生活动报名
UPDATE stgl_activity_registration SET approval_status = '2' WHERE student_no = ?;
-- 5. 处理实习信息
UPDATE zsjy_internship_info SET internship_status = '3' WHERE student_id = ?;
-- 6. 处理其他关联信息
UPDATE xg_student_leave SET leave_status = 'CANCELLED' WHERE student_id = ?;
COMMIT;
```
#### 📊 处理统计表
| 业务模块 | 涉及表数量 | 处理类型 | 重要程度 |
|----------|------------|----------|----------|
| 宿舍管理 | 2 (床位+申请) | 退宿处理 | ⭐⭐⭐⭐⭐ |
| 心理健康 | 2 (预约+未到访) | 预约取消 | ⭐⭐⭐⭐ |
| 实践活动 | 1 (报名表) | 报名驳回 | ⭐⭐⭐⭐ |
| 实习管理 | 1 (实习表) | 状态中断 | ⭐⭐⭐⭐ |
| 请假管理 | 2 (请假+销假) | 请假取消 | ⭐⭐⭐⭐ |
| 卫生检查 | 1 (评分表) | 备注添加 | ⭐⭐⭐ |
| 违纪记录 | 1 (违纪表) | 备注添加 | ⭐⭐⭐ |
| **总计** | **11个表** | **状态更新+关系清理** | **⭐⭐⭐⭐⭐** |
### 2. 业务规则和约束
#### 📋 状态变更处理矩阵
| 当前状态 | 休学处理 | 退学处理 | 毕业处理 |
|----------|----------|----------|----------|
| 宿舍在住 | 可保留/退宿 | 必须退宿 | 必须退宿 |
| 咨询预约 | 取消未来预约 | 取消未来预约 | 取消未来预约 |
| 活动报名 | 驳回未结束活动 | 驳回进行中活动 | 保留已完成活动 |
| 实习进行 | 中断(可恢复) | 中断(终止) | 保持完成状态 |
| 请假进行 | 取消请假 | 取消请假 | 取消请假 |
#### ⚠️ 重要业务约束
- **宿舍资源**: 退学/毕业必须释放宿舍资源,保证公平分配
- **活动公平性**: 学生状态变更后不应继续参加学校组织的活动
- **实习规范**: 实习中断需记录原因,便于管理和追溯
- **档案完整性**: 所有历史记录必须保留,作为学生管理档案
- **数据一致性**: 各业务模块的状态必须与学生学籍状态保持一致
### 3. 监控和审计要求
#### 📊 数据完整性检查
```sql
-- 检查其他业务处理结果的完整性
SELECT
'宿舍管理' as module,
COUNT(*) as total_count,
COUNT(CASE WHEN status = '1' THEN 1 END) as processed_count
FROM xg_dorm_bed_student
WHERE student_id = @student_id
UNION ALL
SELECT
'心理咨询' as module,
COUNT(*) as total_count,
COUNT(CASE WHEN status = '2' THEN 1 END) as processed_count
FROM xljk_consultation_appointment
WHERE student_id = @student_id AND appointment_date >= CURDATE()
UNION ALL
SELECT
'活动报名' as module,
COUNT(*) as total_count,
COUNT(CASE WHEN approval_status = '2' THEN 1 END) as processed_count
FROM stgl_activity_registration
WHERE student_no = @student_no
UNION ALL
SELECT
'实习信息' as module,
COUNT(*) as total_count,
COUNT(CASE WHEN internship_status = '3' THEN 1 END) as processed_count
FROM zsjy_internship_info
WHERE student_id = @student_id AND internship_status IN ('1', '2');
```
#### 📝 审计日志记录
```sql
-- 记录其他业务处理的审计日志
INSERT INTO sys_oper_log (
title, business_type, method, request_method, operator_type,
oper_name, dept_name, oper_url, oper_ip, oper_location,
oper_param, json_result, status, error_msg, oper_time
) VALUES (
'其他业务处理', 'UPDATE', 'processOtherBusiness', 'PROCEDURE', 'MANAGE',
@operator_name, @dept_name, '/other/business/process', @client_ip, @location,
CONCAT('学生ID:', @student_id, ',学号:', @student_no, ',变更类型:', @change_type),
@process_result_json, 0, NULL, NOW()
);
-- 4. 释放床位资源
UPDATE xg_dorm_bed SET bed_status = '0'
WHERE id IN (
SELECT bed_id FROM xg_dorm_bed_student
WHERE student_id = @student_id AND status = '1' AND deleted = '0'
) AND deleted = '0';
```
---
## 📅 更新记录
- **2025-12-XX**: 分文档创建,包含其他业务处理逻辑
- **2025-12-XX**: 基于代码分析完善技术实现细节和业务逻辑
- **后续更新**: 根据实际业务需求持续完善
---
*本分文档详细说明学生状态变更时的其他业务模块处理逻辑和技术实现,包括宿舍、心理、实践、实习等各项业务的完整处理方案。*
- **2025-12-XX**: 根据用户需求简化其他业务处理,删除心理健康、学生活动、实习等非核心业务
- **后续更新**: 如需要其他业务处理,可根据实际业务需求重新添加
@@ -111,21 +111,30 @@ BEGIN
v_effective_date, p_operator_id, 'system', 1
);
-- 4. 更新学生学籍状态
-- 4. 备份原始数据到his表
INSERT INTO xg_student_msg_his SELECT * FROM xg_student_msg WHERE id = p_student_id;
INSERT INTO sys_user_his SELECT * FROM sys_user WHERE user_id = p_student_id;
-- 5. 更新学生学籍状态
UPDATE xg_student_msg
SET student_status = v_new_student_status,
update_by = p_operator_id,
update_time = v_current_time
WHERE id = p_student_id AND deleted = 0;
-- 5. 处理用户账号状态
-- 6. 处理用户账号状态
IF v_disable_user_account THEN
UPDATE sys_user
SET status = '1',
update_by = p_operator_id,
update_time = v_current_time,
remark = CONCAT(COALESCE(remark, ''), ' | ', v_exit_reason, '-', v_current_time)
WHERE user_name = p_student_no AND account_type = '2' AND del_flag = '0';
update_time = v_current_time
WHERE user_id = p_student_id AND account_type = '2' AND del_flag = '0';
ELSE
UPDATE sys_user
SET status = '0',
update_by = p_operator_id,
update_time = v_current_time
WHERE user_id = p_student_id AND account_type = '2' AND del_flag = '0';
END IF;
-- 6. 处理班级干部信息(休学、退学、毕业)
@@ -156,59 +165,6 @@ BEGIN
WHERE user_id = p_student_id AND status IN (0, 1, 2) AND deleted = 0;
END IF;
-- 8. 处理经济资助 - 奖学金申请
IF p_change_type IN ('XIU_XUE', 'TUI_XUE', 'BI_YE') THEN
UPDATE jcbt_scholarship_application
SET current_status = CASE
WHEN p_change_type = 'XIU_XUE' THEN 'WITHDRAWN' -- 休学:撤回
WHEN p_change_type = 'TUI_XUE' THEN 'REJECTED' -- 退学:拒绝
WHEN p_change_type = 'BI_YE' THEN
CASE WHEN current_status = 'APPROVED' THEN 'APPROVED' -- 毕业:保持已通过
ELSE 'WITHDRAWN' -- 其他状态撤回
END
ELSE current_status
END,
update_by = p_operator_id,
update_time = v_current_time
WHERE student_id = p_student_id AND current_status NOT IN ('REJECTED') AND deleted = 0;
END IF;
-- 9. 处理经济资助 - 勤工俭学申请
IF p_change_type IN ('XIU_XUE', 'TUI_XUE', 'BI_YE') THEN
UPDATE jcbt_work_study_application
SET status = 'CANCELLED',
college_review_status = CASE
WHEN college_review_status IS NOT NULL THEN 'REJECTED'
ELSE college_review_status
END,
final_review_status = CASE
WHEN final_review_status IS NOT NULL THEN 'REJECTED'
ELSE final_review_status
END,
final_review_time = v_current_time,
final_reviewer_id = p_operator_id,
final_reviewer_name = '系统',
final_review_comment = CONCAT(v_exit_reason, ',岗位申请自动终止'),
update_by = p_operator_id,
update_time = v_current_time
WHERE student_id = p_student_id
AND status NOT IN ('CANCELLED', 'COLLEGE_REJECTED', 'FINAL_REJECTED')
AND deleted = 0;
END IF;
-- 10. 处理经济资助 - 贫困生认定
IF p_change_type IN ('XIU_XUE', 'TUI_XUE', 'BI_YE') THEN
UPDATE jcbt_poverty_application
SET application_status = CASE
WHEN application_status = 'approved' THEN 'revoked' -- 撤销已通过的认定
ELSE application_status
END,
update_by = p_operator_id,
update_time = v_current_time
WHERE student_id = p_student_id
AND application_status NOT IN ('cancel', 'invalid')
AND deleted = 0;
END IF;
-- 11. 处理宿舍住宿关系(退学、毕业)
IF p_change_type IN ('TUI_XUE', 'BI_YE') THEN
@@ -221,41 +177,6 @@ BEGIN
WHERE student_id = p_student_id AND status = '0' AND deleted = '0';
END IF;
-- 12. 取消心理咨询预约(休学、退学、毕业)
IF p_change_type IN ('XIU_XUE', 'TUI_XUE', 'BI_YE') THEN
UPDATE xljk_consultation_appointment
SET status = '2',
update_by = p_operator_id,
update_time = v_current_time
WHERE student_id = p_student_id AND status = '0'
AND appointment_date >= CURDATE() AND deleted = 0;
END IF;
-- 13. 取消学生活动报名(退学、毕业)
IF p_change_type IN ('TUI_XUE', 'BI_YE') THEN
UPDATE stgl_activity_registration
SET approval_status = '3',
approval_time = v_current_time,
approval_by = p_operator_id,
approval_remark = CONCAT(v_exit_reason, ',活动报名自动取消'),
update_by = p_operator_id,
update_time = v_current_time
WHERE student_no = p_student_no AND approval_status = '1'
AND EXISTS (SELECT 1 FROM stgl_activity a
WHERE a.id = stgl_activity_registration.activity_id
AND a.activity_start_time > v_current_time);
END IF;
-- 14. 终止实习(退学)
IF p_change_type = 'TUI_XUE' THEN
UPDATE zsjy_internship_info
SET internship_status = '3',
actual_end_date = CURDATE(),
update_by = p_operator_id,
update_time = v_current_time,
remark = CONCAT(COALESCE(remark, ''), ' | 学生退学,实习中断')
WHERE student_id = p_student_id AND internship_status = '1' AND deleted = 0;
END IF;
-- 提交事务
COMMIT;
@@ -9,54 +9,102 @@
### 休学处理快速脚本
```sql
-- 休学处理:保留账号,暂停业务
-- 休学处理:禁用账号,暂停组织关系
SET @student_id = 123456789;
SET @student_no = '20240001';
SET @operator_id = 1;
-- 备份原始数据
INSERT INTO xg_student_msg_his SELECT * FROM xg_student_msg WHERE id = @student_id;
INSERT INTO sys_user_his SELECT * FROM sys_user WHERE user_id = @student_id;
INSERT INTO xg_class_pos_msg_his SELECT * FROM xg_class_pos_msg WHERE student_id = @student_id;
INSERT INTO stgl_member_his SELECT * FROM stgl_member WHERE user_id = @student_id;
-- 更新核心状态
UPDATE xg_student_msg SET student_status = '2', update_by = @operator_id, update_time = NOW() WHERE id = @student_id;
UPDATE sys_user SET status = '1' WHERE user_id = @student_id AND account_type = '2';
-- 处理组织关系
UPDATE xg_class_pos_msg SET is_current = 0, cadre_status = '6', dismissal_date = CURDATE(), dismissal_reason = '学生休学' WHERE student_id = @student_id AND is_current = 1;
UPDATE club_member SET member_status = '4' WHERE user_id = @student_id AND member_status IN ('0', '1', '2');
UPDATE jcbt_scholarship_application SET current_status = 'WITHDRAWN' WHERE student_id = @student_id AND current_status IN ('PENDING', 'APPROVED');
UPDATE jcbt_work_study_application SET status = 'CANCELLED' WHERE student_id = @student_id AND status NOT IN ('CANCELLED', 'COLLEGE_REJECTED', 'FINAL_REJECTED');
UPDATE xljk_consultation_appointment SET status = '2' WHERE student_id = @student_id AND status = '0' AND appointment_date >= CURDATE();
UPDATE stgl_activity_registration SET approval_status = 2 WHERE student_no = @student_no AND approval_status = 1;
UPDATE zsjy_internship_info SET internship_status = '3', actual_end_date = CURDATE() WHERE student_id = @student_id AND internship_status = '1';
UPDATE stgl_member SET status = 4 WHERE user_id = @student_id AND status IN (0, 1, 2);
```
### 退学处理快速脚本
```sql
-- 退学处理:禁用账号,终止所有业务
-- 退学处理:禁用账号,终止组织关系和住宿
SET @student_id = 123456789;
SET @student_no = '20240001';
SET @operator_id = 1;
-- 备份原始数据
INSERT INTO xg_student_msg_his SELECT * FROM xg_student_msg WHERE id = @student_id;
INSERT INTO sys_user_his SELECT * FROM sys_user WHERE user_id = @student_id;
INSERT INTO xg_class_pos_msg_his SELECT * FROM xg_class_pos_msg WHERE student_id = @student_id;
INSERT INTO stgl_member_his SELECT * FROM stgl_member WHERE user_id = @student_id;
INSERT INTO xg_dorm_bed_student_his SELECT * FROM xg_dorm_bed_student WHERE student_id = @student_id;
-- 更新核心状态
UPDATE xg_student_msg SET student_status = '4', update_by = @operator_id, update_time = NOW() WHERE id = @student_id;
UPDATE sys_user SET status = '1' WHERE user_name = @student_no AND account_type = '2';
UPDATE xg_class_pos_msg SET is_current = 0, cadre_status = '4', dismissal_date = CURDATE() WHERE student_id = @student_id AND is_current = 1;
UPDATE sys_user SET status = '1' WHERE user_id = @student_id AND account_type = '2';
-- 处理组织关系
UPDATE xg_class_pos_msg SET is_current = 0, cadre_status = '4', dismissal_date = CURDATE(), dismissal_reason = '学生退学' WHERE student_id = @student_id AND is_current = 1;
UPDATE club_member SET member_status = '4' WHERE user_id = @student_id AND member_status IN ('0', '1', '2');
UPDATE jcbt_scholarship_application SET current_status = 'WITHDRAWN' WHERE student_id = @student_id AND current_status IN ('PENDING', 'APPROVED');
UPDATE jcbt_work_study_application SET status = 'CANCELLED' WHERE student_id = @student_id AND status NOT IN ('CANCELLED', 'COLLEGE_REJECTED', 'FINAL_REJECTED');
UPDATE stgl_member SET status = 4 WHERE user_id = @student_id AND status IN (0, 1, 2);
-- 处理住宿关系
UPDATE xg_student_msg SET dormitory_building = NULL, dormitory_room = NULL, ext_field1 = NULL, update_time = NOW(), update_by = @operator_id WHERE id = @student_id;
UPDATE xg_dorm_bed_student SET status = '1', checkout_time = NOW() WHERE student_id = @student_id AND status = '0';
UPDATE xljk_consultation_appointment SET status = '2' WHERE student_id = @student_id AND status = '0' AND appointment_date >= CURDATE();
UPDATE stgl_activity_registration SET approval_status = 2 WHERE student_no = @student_no AND approval_status = 1;
UPDATE zsjy_internship_info SET internship_status = '3', actual_end_date = CURDATE() WHERE student_id = @student_id AND internship_status = '1';
UPDATE xg_dorm_bed SET bed_status = '0' WHERE id IN (SELECT bed_id FROM xg_dorm_bed_student WHERE student_id = @student_id AND status = '1');
```
### 毕业处理快速脚本
```sql
-- 毕业处理:归档所有业务
-- 毕业处理:禁用账号,终止组织关系和住宿
SET @student_id = 123456789;
SET @student_no = '20240001';
SET @operator_id = 1;
-- 备份原始数据
INSERT INTO xg_student_msg_his SELECT * FROM xg_student_msg WHERE id = @student_id;
INSERT INTO sys_user_his SELECT * FROM sys_user WHERE user_id = @student_id;
INSERT INTO xg_class_pos_msg_his SELECT * FROM xg_class_pos_msg WHERE student_id = @student_id;
INSERT INTO stgl_member_his SELECT * FROM stgl_member WHERE user_id = @student_id;
INSERT INTO xg_dorm_bed_student_his SELECT * FROM xg_dorm_bed_student WHERE student_id = @student_id;
-- 更新核心状态
UPDATE xg_student_msg SET student_status = '5', update_by = @operator_id, update_time = NOW() WHERE id = @student_id;
UPDATE sys_user SET status = '1' WHERE user_name = @student_no AND account_type = '2';
UPDATE xg_class_pos_msg SET is_current = 0, cadre_status = '4', dismissal_date = CURDATE() WHERE student_id = @student_id AND is_current = 1;
UPDATE sys_user SET status = '1' WHERE user_id = @student_id AND account_type = '2';
-- 处理组织关系
UPDATE xg_class_pos_msg SET is_current = 0, cadre_status = '4', dismissal_date = CURDATE(), dismissal_reason = '学生毕业' WHERE student_id = @student_id AND is_current = 1;
UPDATE club_member SET member_status = '4' WHERE user_id = @student_id AND member_status IN ('0', '1', '2');
UPDATE stgl_member SET status = 4 WHERE user_id = @student_id AND status IN (0, 1, 2);
-- 处理住宿关系
UPDATE xg_student_msg SET dormitory_building = NULL, dormitory_room = NULL, ext_field1 = NULL, update_time = NOW(), update_by = @operator_id WHERE id = @student_id;
UPDATE xg_dorm_bed_student SET status = '1', checkout_time = NOW() WHERE student_id = @student_id AND status = '0';
UPDATE xg_dorm_bed SET bed_status = '0' WHERE id IN (SELECT bed_id FROM xg_dorm_bed_student WHERE student_id = @student_id AND status = '1');
```
### 复学处理快速脚本
```sql
-- 复学处理:启用账号,恢复学籍状态
SET @student_id = 123456789;
SET @student_no = '20240001';
SET @operator_id = 1;
-- 备份原始数据
INSERT INTO xg_student_msg_his SELECT * FROM xg_student_msg WHERE id = @student_id;
INSERT INTO sys_user_his SELECT * FROM sys_user WHERE user_id = @student_id;
-- 更新核心状态
UPDATE xg_student_msg SET student_status = '3', update_by = @operator_id, update_time = NOW() WHERE id = @student_id;
UPDATE sys_user SET status = '0' WHERE user_id = @student_id AND account_type = '2';
```
## 🔧 批量处理脚本
@@ -77,6 +125,12 @@ CREATE TEMPORARY TABLE student_leave_batch (
-- 插入待处理的學生信息
-- INSERT INTO student_leave_batch (student_id, student_no) VALUES (123, '20240001'), (456, '20240002');
-- 批量备份原始数据
INSERT INTO xg_student_msg_his SELECT * FROM xg_student_msg WHERE id IN (SELECT student_id FROM student_leave_batch);
INSERT INTO sys_user_his SELECT * FROM sys_user WHERE user_id IN (SELECT student_id FROM student_leave_batch);
INSERT INTO xg_class_pos_msg_his SELECT * FROM xg_class_pos_msg WHERE student_id IN (SELECT student_id FROM student_leave_batch);
INSERT INTO stgl_member_his SELECT * FROM stgl_member WHERE user_id IN (SELECT student_id FROM student_leave_batch);
-- 批量更新学生状态
UPDATE xg_student_msg
SET student_status = '2',
@@ -85,6 +139,15 @@ SET student_status = '2',
WHERE id IN (SELECT student_id FROM student_leave_batch)
AND deleted = 0;
-- 批量禁用用户账号
UPDATE sys_user
SET status = '1',
update_by = 1,
update_time = NOW()
WHERE user_id IN (SELECT student_id FROM student_leave_batch)
AND account_type = '2'
AND del_flag = '0';
-- 批量处理班级干部
UPDATE xg_class_pos_msg
SET is_current = 0,
@@ -106,6 +169,15 @@ WHERE user_id IN (SELECT student_id FROM student_leave_batch)
AND member_status IN ('0', '1', '2')
AND deleted = 0;
-- 批量处理志愿者成员
UPDATE stgl_member
SET status = 4,
update_by = 1,
update_time = NOW()
WHERE user_id IN (SELECT student_id FROM student_leave_batch)
AND status IN (0, 1, 2)
AND deleted = 0;
-- 批量处理奖学金申请
UPDATE jcbt_scholarship_application
SET current_status = 'WITHDRAWN',
+293 -49
View File
@@ -1,71 +1,315 @@
# 学生退学、休学、毕业处理文档
# 学生状态变更处理脚本
## 📋 文档概述
本项目包含用于处理学生退学、休学、复学、毕业状态变更的自动化脚本。
本目录包含完整的学生退学、休学、毕业处理文档体系,采用模块化组织方式,便于查阅和维护。
## 📋 脚本说明
## 📚 文档结构
### 脚本文件
### 📋 主文档
- **[学生退学休学毕业处理清单.md](学生退学休学毕业处理清单.md)** - 总览文档,包含处理流程概览和各分文档导航
1. **`process_student_changes.sh`** - 处理毕业、退学、休学的状态变更
2. **`process_student_resume.sh`** - 处理复学的状态变更
3. **`control_scripts.sh`** - 脚本控制程序(启动、停止、重启)
4. **`start_services.sh`** - 一键启动所有服务
5. **`stop_services.sh`** - 一键停止所有服务
6. **`health_check.sh`** - 服务健康检查脚本
7. **`data.conf`** - 数据库配置文件
8. **`test_data.sql`** - 测试数据脚本
9. **`optional_crontab_config.txt`** - 可选的定时任务配置(备用)
10. **`demo.sh`** - 脚本使用演示
### 📋 分文档
### 处理逻辑
#### 核心处理文档
- **[01-核心状态修改.md](01-核心状态修改.md)** - 学生学籍状态、用户登录状态
- **[02-组织关系处理.md](02-组织关系处理.md)** - 班级干部信息、社团成员关系
- **[03-经济资助处理.md](03-经济资助处理.md)** - 奖学金申请、获奖记录、勤工俭学、贫困生认定
- **[04-其他业务处理.md](04-其他业务处理.md)** - 住宿管理、心理健康、学生活动、志愿者、实习等
#### 毕业/退学/休学处理 (`process_student_changes.sh`)
处理类型:`XIU_XUE`(休学), `TUI_XUE`(退学), `BI_YE`(毕业)
#### 实施文档
- **[06-完整SQL脚本.md](06-完整SQL脚本.md)** - 完整的数据库处理脚本和事务管理
- **[07-快速执行脚本.md](07-快速执行脚本.md)** - 快速执行的简化脚本和批量处理
**执行步骤:**
1. 备份原始数据到his表
2. 更新学生学籍状态
3. 禁用用户账号
4. 处理班级干部关系(取消现任职务)
5. 处理社团成员关系(设置为已退出)
6. 处理住宿关系(退学/毕业时退宿)
7. 更新变更记录状态为已处理
### 🗃️ 数据文件
- **dict字典.sql** - 字典数据
- **hzdyjs_xg.sql** - 数据库结构和基础数据
- **xg_student_status_change.sql** - 学生状态变更相关SQL
#### 复学处理 (`process_student_resume.sh`)
处理类型:`FU_XUE`(复学)
## 🎯 处理流程
**执行步骤:**
1. 备份原始数据到his表
2. 更新学生学籍状态为复学
3. 启用用户账号
4. 更新变更记录状态为已处理
```
1. 阅读主文档了解概览 → 学生退学休学毕业处理清单.md
2. 根据业务模块查阅分文档 → 01-04各业务模块文档
3. 选择合适的实施方式 → 06完整脚本 或 07快速脚本
4. 执行处理并验证结果
## 🔧 配置说明
### data.conf 配置
```bash
# Database configuration
DB_HOST="localhost" # 数据库主机
DB_PORT="3306" # 数据库端口
DB_NAME="hzdyjs_xg" # 数据库名
DB_USER="root" # 数据库用户名
DB_PASS="password" # 数据库密码
# Script configuration
LOG_DIR="/var/log/student_status_change" # 日志目录
SCRIPT_DIR="/path/to/scripts" # 脚本目录
LOCK_FILE="${SCRIPT_DIR}/script.lock" # 锁文件
# Processing configuration
BATCH_SIZE=100 # 每次处理记录数
MAX_RETRY=3 # 最大重试次数
RETRY_DELAY=5 # 重试间隔(秒)
CYCLE_INTERVAL=600 # 循环间隔时间(秒),默认10分钟
```
## ⚠️ 重要提醒
## 🚀 使用方法
- **数据备份**: 执行任何修改操作前务必备份相关数据
- **权限控制**: 确保操作人员具有相应权限
- **事务处理**: 建议使用数据库事务确保数据一致性
- **日志记录**: 记录所有操作日志便于审计
### 1. 配置数据库连接
## 📊 涉及业务模块统计
编辑 `data.conf` 文件,设置正确的数据库连接信息:
| 模块 | 涉及表数量 | 重要程度 |
|------|-----------|----------|
| 核心状态管理 | 2 | ⭐⭐⭐⭐⭐ |
| 组织关系 | 2 | ⭐⭐⭐⭐⭐ |
| 经济资助 | 4 | ⭐⭐⭐⭐ |
| 住宿管理 | 1 | ⭐⭐⭐⭐⭐ |
| 心理健康 | 1 | ⭐⭐⭐⭐ |
| 实践活动 | 3 | ⭐⭐⭐⭐ |
| 其他信息 | 3 | ⭐⭐⭐ |
```bash
DB_HOST="your_db_host"
DB_PORT="3306"
DB_NAME="hzdyjs_xg"
DB_USER="your_username"
DB_PASS="your_password"
```
## 🔄 状态映射关系
### 2. 启动服务
- **休学**: `student_status=2`,用户账号保留,可恢复业务
- **退学**: `student_status=4`,`sys_user.status=1`,终止所有业务
- **毕业**: `student_status=5`,`sys_user.status=1`,归档处理
脚本采用持续运行模式,启动后会自动循环处理:
## 📅 更新记录
```bash
# 启动所有服务(持续运行模式)
./start_services.sh
- **2025-12-XX**: 完成文档模块化拆分,原有内容完整保留
- **拆分说明**: 将原来1100+行的单文档拆分为1个主文档 + 6个分文档
- **优化内容**: 基于奖学金获奖记录业务逻辑深度分析,完善了相关处理逻辑
# 查看服务状态
./control_scripts.sh statusall
```
#### 持续运行模式说明
- **启动方式**: `./start_services.sh` 一次性启动所有服务
- **运行逻辑**: 每个脚本启动后进入while循环,持续监控和处理数据
- **处理周期**: 检查数据库 → 处理未处理记录 → 等待10分钟 → 重复循环
- **并发控制**: 通过锁文件确保同一脚本只运行一个实例
- **自动重启**: 如果脚本异常退出,可以通过控制脚本重启
### 3. 手动执行脚本(一次性)
如需手动执行单个脚本处理一次:
```bash
# 处理毕业/退学/休学(一次性执行)
./process_student_changes.sh
# 处理复学(一次性执行)
./process_student_resume.sh
```
### 4. 停止服务
```bash
# 正常停止所有服务
./stop_services.sh
# 强制停止所有服务(如果正常停止失败)
./stop_services.sh force
```
### 5. 健康检查
```bash
# 执行健康检查
./health_check.sh
# 执行检查并生成详细报告
./health_check.sh report
```
```
### 3. 使用控制脚本管理
```bash
# 启动指定脚本
./control_scripts.sh start changes # 启动状态变更处理脚本
./control_scripts.sh start resume # 启动复学处理脚本
# 停止指定脚本
./control_scripts.sh stop changes # 停止状态变更处理脚本
./control_scripts.sh stop resume # 停止复学处理脚本
# 重启指定脚本
./control_scripts.sh restart changes # 重启状态变更处理脚本
./control_scripts.sh restart resume # 重启复学处理脚本
# 查看脚本状态
./control_scripts.sh status changes # 查看状态变更处理脚本状态
./control_scripts.sh status resume # 查看复学处理脚本状态
# 批量操作
./control_scripts.sh startall # 启动所有脚本
./control_scripts.sh stopall # 停止所有脚本
./control_scripts.sh statusall # 查看所有脚本状态
```
## 📊 数据表说明
### 核心表
- **`xg_student_status_change`** - 学生状态变更记录表
- `process_status`: 处理状态 (0=未处理,1=已处理)
- `change_type`: 变更类型 (XIU_XUE/TUI_XUE/FU_XUE/BI_YE)
### 业务表
- **`xg_student_msg`** - 学生基本信息表
- **`sys_user`** - 系统用户表
- **`xg_class_pos_msg`** - 班级干部信息表
- **`club_member`** - 社团成员表
- **`stgl_member`** - 志愿者成员表
- **`xg_dorm_bed_student`** - 宿舍床位分配表
- **`xg_dorm_bed`** - 宿舍床位表
### 历史表
所有主表都有对应的his表用于数据备份:
- `xg_student_msg_his`
- `sys_user_his`
- `xg_class_pos_msg_his`
- `club_member_his`
- `stgl_member_his`
- `xg_dorm_bed_student_his`
## 📝 日志说明
### 日志文件位置
- 日志目录:`/var/log/student_status_change/`
- 日志格式:`process_student_changes_YYYYMMDD_HHMMSS.log`
### 日志内容
- 脚本启动/停止信息
- 数据库连接状态
- 每条记录的处理详情
- 错误信息和重试记录
- 处理结果统计
### 示例日志
```
[2025-01-01 10:00:00] [INFO] Starting student status change processing script
[2025-01-01 10:00:01] [INFO] Database connection successful
[2025-01-01 10:00:02] [INFO] Processing student change - ID: 123, Student: 20240001 (456), Type: TUI_XUE
[2025-01-01 10:00:03] [INFO] Successfully processed student change - ID: 123, Student: 20240001
[2025-01-01 10:00:03] [INFO] Processing completed - Processed: 1, Failed: 0
```
## ⚠️ 注意事项
### 安全提醒
1. **数据备份**: 脚本会自动备份数据到his表,但建议在运行前手动备份重要数据
2. **权限控制**: 确保运行脚本的用户有数据库操作权限
3. **网络安全**: 数据库密码等敏感信息请妥善保管
### 使用限制
1. **并发控制**: 同一脚本只允许一个实例运行(通过锁文件控制)
2. **批量处理**: 每次最多处理100条记录,避免数据库负载过高
3. **错误重试**: 处理失败时会自动重试,最多3次
### 监控建议
1. **日志监控**: 定期检查日志文件,关注错误信息
2. **数据库监控**: 监控数据库连接和性能
3. **业务监控**: 检查处理结果是否符合预期
4. **健康检查**: 使用 `./health_check.sh` 脚本定期检查服务状态
## 🔧 故障排除
### 常见问题
1. **数据库连接失败**
- 检查 `data.conf` 中的数据库配置
- 确认数据库服务正在运行
- 验证用户权限
2. **脚本重复运行**
- 删除对应的锁文件:`rm -f script.lock`
- 检查是否有残留进程并终止
3. **处理失败**
- 查看日志文件了解具体错误
- 检查数据库数据完整性
- 手动处理失败的记录
### 紧急停止
```bash
# 强制停止所有相关进程
pkill -f "process_student_changes.sh"
pkill -f "process_student_resume.sh"
# 清理锁文件
rm -f *.lock
rm -f pids/*.pid
```
## 📞 技术支持
如遇到问题,请:
1. 查看日志文件获取详细错误信息
2. 检查数据库连接和权限配置
3. 确认相关数据表结构是否正确
4. 联系技术支持团队
## 🔄 运行模式说明
### 持续运行模式(推荐)
脚本采用持续运行模式,启动后自动循环执行:
1. **启动脚本**: `./start_services.sh`
2. **处理逻辑**: 检查未处理的记录 → 处理业务 → 等待10分钟 → 重复
3. **停止脚本**: `./stop_services.sh`
### 可选的定时任务模式
如果需要使用crontab模式(不推荐),参考 `optional_crontab_config.txt` 文件:
```bash
# 添加定时任务(仅在特殊情况下使用)
crontab -e
# 参考配置文件内容
```
### 运行模式对比
| 特性 | 持续运行模式 | Crontab模式 |
|------|-------------|-------------|
| 启动方式 | `./start_services.sh` | crontab配置 |
| 执行频率 | 每10分钟自动 | 可自定义时间 |
| 资源占用 | 常驻进程 | 按需启动 |
| 监控便利性 | 更容易监控 | 需要额外配置 |
| 维护复杂度 | 较低 | 较高 |
**推荐使用持续运行模式**,简单可靠,易于监控管理。
## 🚀 快速开始
### 💡 使用指南
1. **查看演示**: 运行 `./demo.sh` 查看脚本使用说明
2. **配置数据库**: 编辑 `data.conf` 配置数据库连接信息
3. **启动服务**: 使用 `./start_services.sh` 启动持续运行的服务
4. **监控运行**: 使用 `./health_check.sh` 检查服务状态
5. **停止服务**: 需要时使用 `./stop_services.sh` 停止服务
### 🧪 测试步骤
1. **插入测试数据**: 执行 `test_data.sql` 插入测试记录
2. **启动服务**: `./start_services.sh`
3. **观察日志**: 查看 `/var/log/student_status_change/` 目录的日志
4. **检查结果**: 确认 `xg_student_status_change.process_status` 已更新为1
---
*本项目采用模块化文档管理方式,确保文档的可维护性和可读性。*
**最后更新**: 2025-01-01
**版本**: v1.0.0
+304
View File
@@ -0,0 +1,304 @@
#!/bin/bash
# ===========================================
# 学生状态变更处理脚本控制程序
# 用于启动、停止、重启处理脚本
# ===========================================
set -euo pipefail
# Load configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/data.conf"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "Error: Configuration file $CONFIG_FILE not found!"
exit 1
fi
source "$CONFIG_FILE"
# Script definitions
declare -A SCRIPTS=(
["changes"]="process_student_changes.sh"
["resume"]="process_student_resume.sh"
)
# PID files
PID_DIR="${SCRIPT_DIR}/pids"
mkdir -p "$PID_DIR"
# Functions
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message"
}
get_pid_file() {
local script_name="$1"
echo "${PID_DIR}/${script_name}.pid"
}
get_lock_file() {
local script_name="$1"
echo "${SCRIPT_DIR}/${script_name}.lock"
}
is_running() {
local script_name="$1"
local pid_file=$(get_pid_file "$script_name")
local lock_file=$(get_lock_file "$script_name")
# Check PID file
if [[ -f "$pid_file" ]]; then
local pid=$(cat "$pid_file")
if kill -0 "$pid" 2>/dev/null; then
return 0 # Running
else
log "WARN" "Removing stale PID file for $script_name"
rm -f "$pid_file"
fi
fi
# Check lock file
if [[ -f "$lock_file" ]]; then
local lock_pid=$(cat "$lock_file")
if kill -0 "$lock_pid" 2>/dev/null; then
return 0 # Running
else
log "WARN" "Removing stale lock file for $script_name"
rm -f "$lock_file"
fi
fi
return 1 # Not running
}
start_script() {
local script_name="$1"
local script_file="${SCRIPT_DIR}/${SCRIPTS[$script_name]}"
local pid_file=$(get_pid_file "$script_name")
if [[ ! -f "$script_file" ]]; then
log "ERROR" "Script file not found: $script_file"
return 1
fi
if is_running "$script_name"; then
log "INFO" "$script_name is already running"
return 0
fi
log "INFO" "Starting $script_name..."
chmod +x "$script_file"
# Start script in background
nohup "$script_file" > /dev/null 2>&1 &
local pid=$!
# Wait a moment for script to start
sleep 2
if kill -0 "$pid" 2>/dev/null; then
echo $pid > "$pid_file"
log "INFO" "$script_name started successfully (PID: $pid)"
return 0
else
log "ERROR" "Failed to start $script_name"
return 1
fi
}
stop_script() {
local script_name="$1"
local pid_file=$(get_pid_file "$script_name")
local lock_file=$(get_lock_file "$script_name")
local stopped=false
log "INFO" "Stopping $script_name..."
# Try to stop by PID file
if [[ -f "$pid_file" ]]; then
local pid=$(cat "$pid_file")
if kill -0 "$pid" 2>/dev/null; then
kill "$pid"
sleep 2
if ! kill -0 "$pid" 2>/dev/null; then
log "INFO" "$script_name stopped successfully (PID: $pid)"
stopped=true
fi
fi
rm -f "$pid_file"
fi
# Try to stop by lock file
if [[ -f "$lock_file" ]]; then
local lock_pid=$(cat "$lock_file")
if kill -0 "$lock_pid" 2>/dev/null; then
kill "$lock_pid"
sleep 2
if ! kill -0 "$lock_pid" 2>/dev/null; then
log "INFO" "$script_name stopped successfully (PID: $lock_pid)"
stopped=true
fi
fi
rm -f "$lock_file"
fi
if [[ "$stopped" == false ]]; then
log "WARN" "$script_name may not have been running or failed to stop gracefully"
fi
}
restart_script() {
local script_name="$1"
log "INFO" "Restarting $script_name..."
stop_script "$script_name"
sleep 3
start_script "$script_name"
}
status_script() {
local script_name="$1"
if is_running "$script_name"; then
local pid_file=$(get_pid_file "$script_name")
local lock_file=$(get_lock_file "$script_name")
if [[ -f "$pid_file" ]]; then
local pid=$(cat "$pid_file")
log "INFO" "$script_name is running (PID: $pid)"
elif [[ -f "$lock_file" ]]; then
local lock_pid=$(cat "$lock_file")
log "INFO" "$script_name is running (PID: $lock_pid)"
else
log "INFO" "$script_name status unknown"
fi
else
log "INFO" "$script_name is not running"
fi
}
show_usage() {
cat << EOF
Usage: $0 <command> <script_name>
Commands:
start Start the specified script
stop Stop the specified script
restart Restart the specified script
status Show status of the specified script
startall Start all scripts
stopall Stop all scripts
statusall Show status of all scripts
Script names:
changes Student status changes script (休学/退学/毕业)
resume Student resume script (复学)
all All scripts
Examples:
$0 start changes # Start changes processing script
$0 stop resume # Stop resume processing script
$0 restart changes # Restart changes processing script
$0 status all # Show status of all scripts
$0 startall # Start all scripts
$0 stopall # Stop all scripts
EOF
}
# Main function
main() {
local command="${1:-}"
local script_name="${2:-}"
case "$command" in
start)
if [[ "$script_name" == "all" ]]; then
for script in "${!SCRIPTS[@]}"; do
start_script "$script"
done
elif [[ -n "${SCRIPTS[$script_name]}" ]]; then
start_script "$script_name"
else
log "ERROR" "Unknown script name: $script_name"
show_usage
exit 1
fi
;;
stop)
if [[ "$script_name" == "all" ]]; then
for script in "${!SCRIPTS[@]}"; do
stop_script "$script"
done
elif [[ -n "${SCRIPTS[$script_name]}" ]]; then
stop_script "$script_name"
else
log "ERROR" "Unknown script name: $script_name"
show_usage
exit 1
fi
;;
restart)
if [[ "$script_name" == "all" ]]; then
for script in "${!SCRIPTS[@]}"; do
restart_script "$script"
done
elif [[ -n "${SCRIPTS[$script_name]}" ]]; then
restart_script "$script_name"
else
log "ERROR" "Unknown script name: $script_name"
show_usage
exit 1
fi
;;
status)
if [[ "$script_name" == "all" ]]; then
for script in "${!SCRIPTS[@]}"; do
status_script "$script"
done
elif [[ -n "${SCRIPTS[$script_name]}" ]]; then
status_script "$script_name"
else
log "ERROR" "Unknown script name: $script_name"
show_usage
exit 1
fi
;;
startall)
log "INFO" "Starting all scripts..."
for script in "${!SCRIPTS[@]}"; do
start_script "$script"
done
;;
stopall)
log "INFO" "Stopping all scripts..."
for script in "${!SCRIPTS[@]}"; do
stop_script "$script"
done
;;
statusall)
log "INFO" "Checking status of all scripts..."
for script in "${!SCRIPTS[@]}"; do
status_script "$script"
done
;;
*)
log "ERROR" "Unknown command: $command"
show_usage
exit 1
;;
esac
}
# Run main function
main "$@"
@@ -0,0 +1,17 @@
# Database configuration
DB_HOST="localhost"
DB_PORT="3306"
DB_NAME="hzdyjs_xg"
DB_USER="root"
DB_PASS="password"
# Script configuration
LOG_DIR="/var/log/student_status_change"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOCK_FILE="${SCRIPT_DIR}/script.lock"
# Processing configuration
BATCH_SIZE=100
MAX_RETRY=3
RETRY_DELAY=5
CYCLE_INTERVAL=600 # 循环间隔时间(秒),默认10分钟
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# ===========================================
# 学生状态变更处理脚本演示
# 展示如何使用这些脚本
# ===========================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "========================================"
echo " 学生状态变更处理脚本演示"
echo "========================================"
echo ""
echo "脚本位置: $SCRIPT_DIR"
echo ""
echo "📋 脚本文件说明:"
echo " process_student_changes.sh - 处理毕业/退学/休学的状态变更"
echo " process_student_resume.sh - 处理复学的状态变更"
echo " control_scripts.sh - 脚本控制程序"
echo " start_services.sh - 一键启动所有服务"
echo " stop_services.sh - 一键停止所有服务"
echo " health_check.sh - 服务健康检查"
echo ""
echo "🔧 配置文件:"
echo " data.conf - 数据库配置"
echo " test_data.sql - 测试数据"
echo ""
echo "📖 使用步骤:"
echo ""
echo "1️⃣ 配置数据库连接:"
echo " vim data.conf"
echo " # 修改数据库连接信息"
echo ""
echo "2️⃣ 启动服务(持续运行模式):"
echo " ./start_services.sh"
echo ""
echo "3️⃣ 查看服务状态:"
echo " ./control_scripts.sh statusall"
echo ""
echo "4️⃣ 执行健康检查:"
echo " ./health_check.sh"
echo ""
echo "5️⃣ 停止服务:"
echo " ./stop_services.sh"
echo ""
echo "📊 运行模式说明:"
echo "• 脚本启动后进入持续运行模式"
echo "• 每10分钟自动检查和处理一次数据"
echo "• 支持并发控制,避免重复运行"
echo "• 完整的日志记录和错误处理"
echo ""
echo "🧪 测试数据:"
echo "• 运行 test_data.sql 插入测试数据"
echo "• 脚本会自动处理 process_status = 0 的记录"
echo ""
echo "📝 日志位置:"
echo "• /var/log/student_status_change/"
echo "• 包含详细的处理日志和错误信息"
echo ""
echo "⚠️ 注意事项:"
echo "• 确保数据库连接正确"
echo "• 确保用户有足够的数据库权限"
echo "• 定期检查日志文件"
echo "• 监控磁盘空间使用情况"
echo ""
echo "========================================"
echo " 演示完成"
echo "========================================"
+359
View File
@@ -0,0 +1,359 @@
#!/bin/bash
# ===========================================
# 学生状态变更处理服务健康检查脚本
# 用于监控服务运行状态和数据库连接
# ===========================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/data.conf"
CONTROL_SCRIPT="${SCRIPT_DIR}/control_scripts.sh"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Load configuration
if [[ ! -f "$CONFIG_FILE" ]]; then
echo -e "${RED}Error: Configuration file $CONFIG_FILE not found!${NC}"
exit 1
fi
source "$CONFIG_FILE"
# Functions
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "${timestamp} [$level] $message"
}
success() {
echo -e "${GREEN}✓ $1${NC}"
}
error() {
echo -e "${RED}✗ $1${NC}"
}
warning() {
echo -e "${YELLOW}⚠ $1${NC}"
}
info() {
echo -e "${BLUE}ℹ $1${NC}"
}
# Check database connection
check_database() {
info "Checking database connection..."
local db_test
if db_test=$(mysql -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "SELECT 1 as test;" 2>/dev/null); then
if echo "$db_test" | grep -q "test"; then
success "Database connection successful"
return 0
else
error "Database query failed"
return 1
fi
else
error "Database connection failed"
return 1
fi
}
# Check service status
check_service() {
local service_name="$1"
local service_desc="$2"
info "Checking $service_desc..."
if [[ ! -x "$CONTROL_SCRIPT" ]]; then
error "Control script not found or not executable: $CONTROL_SCRIPT"
return 1
fi
local status_output
status_output=$("$CONTROL_SCRIPT" status "$service_name" 2>/dev/null)
if echo "$status_output" | grep -q "is running"; then
success "$service_desc is running"
# Extract PID if available
local pid
pid=$(echo "$status_output" | grep -o "PID: [0-9]*" | cut -d' ' -f2)
if [[ -n "$pid" ]]; then
info " Process ID: $pid"
fi
return 0
else
error "$service_desc is not running"
return 1
fi
}
# Check log files
check_logs() {
info "Checking log files..."
if [[ ! -d "$LOG_DIR" ]]; then
warning "Log directory does not exist: $LOG_DIR"
return 1
fi
# Count log files
local log_count
log_count=$(find "$LOG_DIR" -name "*.log" -type f | wc -l)
if [[ $log_count -gt 0 ]]; then
success "Found $log_count log files"
# Check for recent errors
local recent_errors
recent_errors=$(find "$LOG_DIR" -name "*.log" -type f -mmin -60 -exec grep -l "ERROR" {} \; | wc -l)
if [[ $recent_errors -gt 0 ]]; then
warning "Found $recent_errors log files with recent errors (last 60 minutes)"
else
info "No recent errors found in log files"
fi
return 0
else
warning "No log files found"
return 1
fi
}
# Check pending records
check_pending_records() {
info "Checking pending records in database..."
local pending_changes pending_resume
# Check changes (XIU_XUE, TUI_XUE, BI_YE)
pending_changes=$(mysql -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "
SELECT COUNT(*) as count FROM xg_student_status_change
WHERE process_status = 0
AND change_type IN ('XIU_XUE', 'TUI_XUE', 'BI_YE')
AND deleted = 0;
" 2>/dev/null | tail -n1)
# Check resume (FU_XUE)
pending_resume=$(mysql -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "
SELECT COUNT(*) as count FROM xg_student_status_change
WHERE process_status = 0
AND change_type = 'FU_XUE'
AND deleted = 0;
" 2>/dev/null | tail -n1)
# Remove potential header
pending_changes=$(echo "$pending_changes" | grep -E '^[0-9]+$')
pending_resume=$(echo "$pending_resume" | grep -E '^[0-9]+$')
if [[ -z "$pending_changes" ]]; then
pending_changes="0"
fi
if [[ -z "$pending_resume" ]]; then
pending_resume="0"
fi
local total_pending=$((pending_changes + pending_resume))
if [[ $total_pending -gt 0 ]]; then
info "Found $total_pending pending records ($pending_changes changes, $pending_resume resume)"
if [[ $pending_changes -gt 0 ]]; then
warning " $pending_changes records waiting for changes processing"
fi
if [[ $pending_resume -gt 0 ]]; then
warning " $pending_resume records waiting for resume processing"
fi
else
success "No pending records found"
fi
return 0
}
# Check disk space
check_disk_space() {
info "Checking disk space..."
local log_disk_usage
log_disk_usage=$(df "$LOG_DIR" 2>/dev/null | tail -n1 | awk '{print $5}' | sed 's/%//')
if [[ -z "$log_disk_usage" ]]; then
warning "Cannot determine log directory disk usage"
return 1
fi
if [[ $log_disk_usage -gt 90 ]]; then
error "Log directory disk usage is ${log_disk_usage}% - consider cleaning up old logs"
return 1
elif [[ $log_disk_usage -gt 75 ]]; then
warning "Log directory disk usage is ${log_disk_usage}%"
else
success "Log directory disk usage is ${log_disk_usage}%"
fi
return 0
}
# Generate health report
generate_report() {
local report_file="${LOG_DIR}/health_check_$(date +%Y%m%d_%H%M%S).log"
{
echo "=== 学生状态变更处理服务健康检查报告 ==="
echo "检查时间: $(date)"
echo "检查主机: $(hostname)"
echo ""
echo "=== 数据库连接检查 ==="
if check_database; then
echo "状态: 正常"
else
echo "状态: 异常"
fi
echo ""
echo "=== 服务状态检查 ==="
echo "学生状态变更处理服务 (休学/退学/毕业):"
if check_service "changes" "学生状态变更处理服务"; then
echo "状态: 运行中"
else
echo "状态: 未运行"
fi
echo "学生复学处理服务:"
if check_service "resume" "学生复学处理服务"; then
echo "状态: 运行中"
else
echo "状态: 未运行"
fi
echo ""
echo "=== 待处理记录检查 ==="
check_pending_records
echo ""
echo "=== 日志文件检查 ==="
check_logs
echo ""
echo "=== 磁盘空间检查 ==="
check_disk_space
echo ""
echo "=== 配置信息 ==="
echo "数据库主机: $DB_HOST:$DB_PORT"
echo "数据库名称: $DB_NAME"
echo "日志目录: $LOG_DIR"
echo "脚本目录: $SCRIPT_DIR"
echo ""
echo "报告生成完成"
} > "$report_file" 2>/dev/null
if [[ -f "$report_file" ]]; then
success "Health check report generated: $report_file"
else
error "Failed to generate health check report"
fi
}
# Main function
main() {
local generate_report="${1:-false}"
echo ""
echo "========================================"
echo " 学生状态变更处理服务健康检查"
echo "========================================"
echo ""
# Perform all checks
check_database
echo ""
check_service "changes" "学生状态变更处理服务 (休学/退学/毕业)"
check_service "resume" "学生复学处理服务"
echo ""
check_pending_records
echo ""
check_logs
echo ""
check_disk_space
echo ""
# Generate report if requested
if [[ "$generate_report" == "report" ]]; then
generate_report
fi
echo ""
echo "========================================"
success "健康检查完成"
echo ""
if [[ "$generate_report" == "report" ]]; then
echo "详细报告已保存到日志目录"
fi
echo "使用方法:"
echo " $0 # 执行健康检查"
echo " $0 report # 执行检查并生成报告"
echo ""
}
# Show usage
show_usage() {
cat << EOF
Usage: $0 [report]
执行学生状态变更处理服务的健康检查。
Options:
report 生成详细的健康检查报告
检查内容:
- 数据库连接状态
- 服务运行状态
- 待处理记录数量
- 日志文件状态
- 磁盘空间使用情况
EOF
}
# Parse arguments
case "${1:-}" in
report)
main "report"
;;
-h|--help)
show_usage
;;
"")
main
;;
*)
error "Unknown option: $1"
show_usage
exit 1
;;
esac
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,69 @@
# ===========================================
# 学生状态变更处理脚本 - 可选的Crontab配置
# ===========================================
# 注意:脚本现在采用持续运行模式,默认每10分钟自动处理一次
# 此配置文件仅供特殊情况下的备用配置使用
# 如果不使用持续运行模式,可以使用以下crontab配置:
# 1. 将以下内容添加到系统的 crontab 中:crontab -e
# 2. 根据实际需要调整执行时间和频率
# 3. 确保脚本路径正确
# 4. 确保用户有执行权限
# 脚本所在目录(请根据实际情况修改)
SCRIPT_DIR="/path/to/student_status_scripts"
# 每天早上8点执行学生状态变更处理
0 8 * * * $SCRIPT_DIR/process_student_changes.sh
# 每天早上8点30分执行学生复学处理
30 8 * * * $SCRIPT_DIR/process_student_resume.sh
# 每小时执行一次健康检查
0 * * * * $SCRIPT_DIR/health_check.sh report
# 每周一早上6点重启所有服务(可选,用于定期重启确保稳定性)
0 6 * * 1 $SCRIPT_DIR/stop_services.sh && sleep 5 && $SCRIPT_DIR/start_services.sh
# 每天晚上10点清理旧日志文件(保留7天)
0 22 * * * find $SCRIPT_DIR/logs -name "*.log" -type f -mtime +7 -delete
# 每分钟检查服务状态,如果服务停止则自动重启(可选)
# */1 * * * * $SCRIPT_DIR/control_scripts.sh statusall >/dev/null 2>&1 || ($SCRIPT_DIR/stop_services.sh force && $SCRIPT_DIR/start_services.sh)
# ===========================================
# Crontab 语法说明
# ===========================================
# 分 时 日 月 周 命令
# * * * * * command
#
# 字段说明:
# 分 - 0-59
# 时 - 0-23
# 日 - 1-31
# 月 - 1-12
# 周 - 0-7 (0和7都表示星期日)
#
# 常用示例:
# 0 8 * * * - 每天早上8点
# */30 * * * * - 每30分钟
# 0 */2 * * * - 每2小时
# 0 9-17 * * 1-5 - 周一到周五的9点到17点,每小时执行一次
# @reboot - 系统重启时执行
# ===========================================
# 监控和告警建议
# ===========================================
# 1. 监控日志中的ERROR关键字
# 可以使用logwatch或自定义脚本监控日志
# 2. 监控数据库连接
# */5 * * * * mysql -h host -u user -p pass -e "SELECT 1;" || echo "DB connection failed" | mail -s "DB Alert" admin@example.com
# 3. 监控磁盘空间
# */30 * * * * df /var/log | awk 'NR==2 {if($5>90) print "Disk usage high: "$5}' | mail -s "Disk Alert" admin@example.com
# 4. 监控进程状态
# */5 * * * * pgrep -f "process_student_changes.sh" >/dev/null || echo "Process died" | mail -s "Process Alert" admin@example.com
@@ -0,0 +1,293 @@
#!/bin/bash
# ===========================================
# 学生状态变更处理脚本 - 毕业/退学/休学
# 处理类型: XIU_XUE(休学), TUI_XUE(退学), BI_YE(毕业)
# ===========================================
set -euo pipefail
# Load configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/data.conf"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "Error: Configuration file $CONFIG_FILE not found!"
exit 1
fi
source "$CONFIG_FILE"
# Create log directory
mkdir -p "$LOG_DIR"
# Log file
LOG_FILE="${LOG_DIR}/process_student_changes_$(date +%Y%m%d_%H%M%S).log"
# Lock file to prevent multiple instances
LOCK_FILE="${SCRIPT_DIR}/process_student_changes.lock"
# Functions
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
error_exit() {
local message="$1"
log "ERROR" "$message"
# Remove lock file
[[ -f "$LOCK_FILE" ]] && rm -f "$LOCK_FILE"
exit 1
}
# Check if script is already running
if [[ -f "$LOCK_FILE" ]]; then
if kill -0 $(cat "$LOCK_FILE") 2>/dev/null; then
log "INFO" "Script is already running (PID: $(cat "$LOCK_FILE"))"
exit 1
else
log "WARN" "Removing stale lock file"
rm -f "$LOCK_FILE"
fi
fi
# Create lock file
echo $$ > "$LOCK_FILE"
# Cleanup function
cleanup() {
[[ -f "$LOCK_FILE" ]] && rm -f "$LOCK_FILE"
log "INFO" "Script execution completed"
}
trap cleanup EXIT
# Database functions
execute_sql() {
local sql="$1"
local result
result=$(mysql -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "$sql" 2>&1)
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
log "ERROR" "SQL execution failed: $sql"
log "ERROR" "MySQL error: $result"
return 1
fi
echo "$result"
}
# Process student status change
process_student_change() {
local record_id="$1"
local student_id="$2"
local student_no="$3"
local change_type="$4"
local operator_id="${5:-1}"
log "INFO" "Processing student change - ID: $record_id, Student: $student_no ($student_id), Type: $change_type"
# Start transaction
local transaction_sql="
START TRANSACTION;
-- 1. Backup original data to his tables
INSERT INTO xg_student_msg_his SELECT * FROM xg_student_msg WHERE id = $student_id;
INSERT INTO sys_user_his SELECT * FROM sys_user WHERE user_id = $student_id;
INSERT INTO xg_class_pos_msg_his SELECT * FROM xg_class_pos_msg WHERE student_id = $student_id;
INSERT INTO club_member_his SELECT * FROM club_member WHERE user_id = $student_id;
INSERT INTO stgl_member_his SELECT * FROM stgl_member WHERE user_id = $student_id;
INSERT INTO xg_dorm_bed_student_his SELECT * FROM xg_dorm_bed_student WHERE student_id = $student_id;
-- 2. Update student status
UPDATE xg_student_msg SET
student_status = CASE
WHEN '$change_type' = 'XIU_XUE' THEN '2'
WHEN '$change_type' = 'TUI_XUE' THEN '4'
WHEN '$change_type' = 'BI_YE' THEN '5'
ELSE student_status
END,
update_by = $operator_id,
update_time = NOW()
WHERE id = $student_id;
-- 3. Disable user account
UPDATE sys_user SET
status = '1',
update_by = $operator_id,
update_time = NOW()
WHERE user_id = $student_id AND account_type = '2';
-- 4. Process class position (cadre) - only for XIU_XUE, TUI_XUE, BI_YE
UPDATE xg_class_pos_msg SET
is_current = 0,
cadre_status = CASE
WHEN '$change_type' = 'XIU_XUE' THEN '6'
WHEN '$change_type' IN ('TUI_XUE', 'BI_YE') THEN '4'
ELSE cadre_status
END,
dismissal_date = CURDATE(),
dismissal_reason = CASE
WHEN '$change_type' = 'XIU_XUE' THEN '学生休学'
WHEN '$change_type' = 'TUI_XUE' THEN '学生退学'
WHEN '$change_type' = 'BI_YE' THEN '学生毕业'
ELSE dismissal_reason
END,
term_end_date = CURDATE(),
term_duration = TIMESTAMPDIFF(MONTH, term_start_date, CURDATE()),
update_by = $operator_id,
update_time = NOW()
WHERE student_id = $student_id AND is_current = 1;
-- 5. Process club membership - only for XIU_XUE, TUI_XUE, BI_YE
UPDATE club_member SET
member_status = '4',
update_by = $operator_id,
update_time = NOW(),
remark = CONCAT(COALESCE(remark, ''), ' | 退出原因:',
CASE
WHEN '$change_type' = 'XIU_XUE' THEN '学生休学'
WHEN '$change_type' = 'TUI_XUE' THEN '学生退学'
WHEN '$change_type' = 'BI_YE' THEN '学生毕业'
ELSE '学生状态变更'
END, ' - ', NOW())
WHERE user_id = $student_id AND member_status IN ('0', '1', '2');
-- 6. Process volunteer membership - only for XIU_XUE, TUI_XUE, BI_YE
UPDATE stgl_member SET
status = 4,
update_by = $operator_id,
update_time = NOW()
WHERE user_id = $student_id AND status IN (0, 1, 2);
-- 7. Process dormitory - only for TUI_XUE, BI_YE
UPDATE xg_student_msg SET
dormitory_building = NULL,
dormitory_room = NULL,
ext_field1 = NULL,
update_time = NOW(),
update_by = $operator_id
WHERE id = $student_id;
UPDATE xg_dorm_bed_student SET
status = '1',
checkout_time = NOW(),
update_time = NOW(),
update_by = $operator_id
WHERE student_id = $student_id AND status = '0';
UPDATE xg_dorm_bed SET
bed_status = '0'
WHERE id IN (
SELECT bed_id FROM xg_dorm_bed_student
WHERE student_id = $student_id AND status = '1'
);
-- 8. Update change record status
UPDATE xg_student_status_change SET
process_status = 1,
update_by = $operator_id,
update_time = NOW()
WHERE id = $record_id;
COMMIT;
"
# Execute transaction
if execute_sql "$transaction_sql"; then
log "INFO" "Successfully processed student change - ID: $record_id, Student: $student_no"
return 0
else
log "ERROR" "Failed to process student change - ID: $record_id, Student: $student_no"
return 1
fi
}
# Main function
main() {
log "INFO" "Starting student status change processing daemon"
log "INFO" "Processing types: XIU_XUE(休学), TUI_XUE(退学), BI_YE(毕业)"
log "INFO" "Running in continuous mode with 10-minute intervals"
# Test database connection
log "INFO" "Testing database connection..."
if ! execute_sql "SELECT 1;" >/dev/null 2>&1; then
error_exit "Database connection failed"
fi
log "INFO" "Database connection successful"
# Main processing loop
while true; do
log "INFO" "Starting new processing cycle"
# Get unprocessed records
local query_sql="
SELECT id, student_id, student_no, change_type, operator_id
FROM xg_student_status_change
WHERE process_status = 0
AND change_type IN ('XIU_XUE', 'TUI_XUE', 'BI_YE')
AND deleted = 0
ORDER BY create_time ASC
LIMIT $BATCH_SIZE;
"
local records
records=$(execute_sql "$query_sql")
# Check if there are records to process
local record_count
record_count=$(echo "$records" | wc -l)
if [[ $record_count -le 1 ]]; then
log "INFO" "No unprocessed records found, waiting..."
else
# Remove header line
records=$(echo "$records" | tail -n +2)
local processed_count=0
local failed_count=0
# Process each record
while IFS=$'\t' read -r record_id student_id student_no change_type operator_id; do
# Skip empty lines
[[ -z "$record_id" ]] && continue
local retry_count=0
local success=false
while [[ $retry_count -lt $MAX_RETRY && $success == false ]]; do
if process_student_change "$record_id" "$student_id" "$student_no" "$change_type" "$operator_id"; then
success=true
((processed_count++))
else
((retry_count++))
if [[ $retry_count -lt $MAX_RETRY ]]; then
log "WARN" "Processing failed, retrying in $RETRY_DELAY seconds... (attempt $retry_count/$MAX_RETRY)"
sleep $RETRY_DELAY
fi
fi
done
if [[ $success == false ]]; then
((failed_count++))
log "ERROR" "Failed to process record after $MAX_RETRY attempts - ID: $record_id"
fi
done <<< "$records"
log "INFO" "Processing cycle completed - Processed: $processed_count, Failed: $failed_count"
fi
# Wait for configured interval before next cycle
log "INFO" "Waiting $CYCLE_INTERVAL seconds before next processing cycle..."
sleep $CYCLE_INTERVAL
done
}
# Run main function
main "$@"
+219
View File
@@ -0,0 +1,219 @@
#!/bin/bash
# ===========================================
# 学生状态变更处理脚本 - 复学
# 处理类型: FU_XUE(复学)
# ===========================================
set -euo pipefail
# Load configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/data.conf"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "Error: Configuration file $CONFIG_FILE not found!"
exit 1
fi
source "$CONFIG_FILE"
# Create log directory
mkdir -p "$LOG_DIR"
# Log file
LOG_FILE="${LOG_DIR}/process_student_resume_$(date +%Y%m%d_%H%M%S).log"
# Lock file to prevent multiple instances
LOCK_FILE="${SCRIPT_DIR}/process_student_resume.lock"
# Functions
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
error_exit() {
local message="$1"
log "ERROR" "$message"
# Remove lock file
[[ -f "$LOCK_FILE" ]] && rm -f "$LOCK_FILE"
exit 1
}
# Check if script is already running
if [[ -f "$LOCK_FILE" ]]; then
if kill -0 $(cat "$LOCK_FILE") 2>/dev/null; then
log "INFO" "Script is already running (PID: $(cat "$LOCK_FILE"))"
exit 1
else
log "WARN" "Removing stale lock file"
rm -f "$LOCK_FILE"
fi
fi
# Create lock file
echo $$ > "$LOCK_FILE"
# Cleanup function
cleanup() {
[[ -f "$LOCK_FILE" ]] && rm -f "$LOCK_FILE"
log "INFO" "Script execution completed"
}
trap cleanup EXIT
# Database functions
execute_sql() {
local sql="$1"
local result
result=$(mysql -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "$sql" 2>&1)
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
log "ERROR" "SQL execution failed: $sql"
log "ERROR" "MySQL error: $result"
return 1
fi
echo "$result"
}
# Process student resume
process_student_resume() {
local record_id="$1"
local student_id="$2"
local student_no="$3"
local change_type="$4"
local operator_id="${5:-1}"
log "INFO" "Processing student resume - ID: $record_id, Student: $student_no ($student_id), Type: $change_type"
# Start transaction
local transaction_sql="
START TRANSACTION;
-- 1. Backup original data to his tables
INSERT INTO xg_student_msg_his SELECT * FROM xg_student_msg WHERE id = $student_id;
INSERT INTO sys_user_his SELECT * FROM sys_user WHERE user_id = $student_id;
-- 2. Update student status to resume (复学)
UPDATE xg_student_msg SET
student_status = '3',
update_by = $operator_id,
update_time = NOW()
WHERE id = $student_id;
-- 3. Enable user account
UPDATE sys_user SET
status = '0',
update_by = $operator_id,
update_time = NOW()
WHERE user_id = $student_id AND account_type = '2';
-- 4. Update change record status
UPDATE xg_student_status_change SET
process_status = 1,
update_by = $operator_id,
update_time = NOW()
WHERE id = $record_id;
COMMIT;
"
# Execute transaction
if execute_sql "$transaction_sql"; then
log "INFO" "Successfully processed student resume - ID: $record_id, Student: $student_no"
return 0
else
log "ERROR" "Failed to process student resume - ID: $record_id, Student: $student_no"
return 1
fi
}
# Main function
main() {
log "INFO" "Starting student resume processing daemon"
log "INFO" "Processing type: FU_XUE(复学)"
log "INFO" "Running in continuous mode with 10-minute intervals"
# Test database connection
log "INFO" "Testing database connection..."
if ! execute_sql "SELECT 1;" >/dev/null 2>&1; then
error_exit "Database connection failed"
fi
log "INFO" "Database connection successful"
# Main processing loop
while true; do
log "INFO" "Starting new resume processing cycle"
# Get unprocessed records
local query_sql="
SELECT id, student_id, student_no, change_type, operator_id
FROM xg_student_status_change
WHERE process_status = 0
AND change_type = 'FU_XUE'
AND deleted = 0
ORDER BY create_time ASC
LIMIT $BATCH_SIZE;
"
local records
records=$(execute_sql "$query_sql")
# Check if there are records to process
local record_count
record_count=$(echo "$records" | wc -l)
if [[ $record_count -le 1 ]]; then
log "INFO" "No unprocessed resume records found, waiting..."
else
# Remove header line
records=$(echo "$records" | tail -n +2)
local processed_count=0
local failed_count=0
# Process each record
while IFS=$'\t' read -r record_id student_id student_no change_type operator_id; do
# Skip empty lines
[[ -z "$record_id" ]] && continue
local retry_count=0
local success=false
while [[ $retry_count -lt $MAX_RETRY && $success == false ]]; do
if process_student_resume "$record_id" "$student_id" "$student_no" "$change_type" "$operator_id"; then
success=true
((processed_count++))
else
((retry_count++))
if [[ $retry_count -lt $MAX_RETRY ]]; then
log "WARN" "Processing failed, retrying in $RETRY_DELAY seconds... (attempt $retry_count/$MAX_RETRY)"
sleep $RETRY_DELAY
fi
fi
done
if [[ $success == false ]]; then
((failed_count++))
log "ERROR" "Failed to process record after $MAX_RETRY attempts - ID: $record_id"
fi
done <<< "$records"
log "INFO" "Resume processing cycle completed - Processed: $processed_count, Failed: $failed_count"
fi
# Wait for configured interval before next cycle
log "INFO" "Waiting $CYCLE_INTERVAL seconds before next resume processing cycle..."
sleep $CYCLE_INTERVAL
done
}
# Run main function
main "$@"
+123
View File
@@ -0,0 +1,123 @@
#!/bin/bash
# ===========================================
# 学生状态变更处理服务启动脚本
# 用于启动所有相关的处理服务
# ===========================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONTROL_SCRIPT="${SCRIPT_DIR}/control_scripts.sh"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "${timestamp} [$level] $message"
}
success() {
log "${GREEN}SUCCESS${NC}" "$1"
}
error() {
log "${RED}ERROR${NC}" "$1"
}
warning() {
log "${YELLOW}WARNING${NC}" "$1"
}
# Check if control script exists and is executable
if [[ ! -x "$CONTROL_SCRIPT" ]]; then
error "Control script not found or not executable: $CONTROL_SCRIPT"
exit 1
fi
# Function to start a service
start_service() {
local service_name="$1"
local service_desc="$2"
log "INFO" "Starting $service_desc..."
if "$CONTROL_SCRIPT" start "$service_name"; then
success "$service_desc started successfully"
return 0
else
error "Failed to start $service_desc"
return 1
fi
}
# Function to check service status
check_service() {
local service_name="$1"
local service_desc="$2"
log "INFO" "Checking $service_desc status..."
if "$CONTROL_SCRIPT" status "$service_name" | grep -q "is running"; then
success "$service_desc is running"
return 0
else
warning "$service_desc is not running"
return 1
fi
}
# Main function
main() {
log "INFO" "=== 学生状态变更处理服务启动脚本 ==="
log "INFO" "Starting all student status change processing services..."
local failed_services=()
# Start changes processing service
if ! start_service "changes" "学生状态变更处理服务 (休学/退学/毕业)"; then
failed_services+=("changes")
fi
# Start resume processing service
if ! start_service "resume" "学生复学处理服务"; then
failed_services+=("resume")
fi
# Wait a moment for services to fully start
sleep 3
log "INFO" "=== 服务状态检查 ==="
# Check all services status
check_service "changes" "学生状态变更处理服务 (休学/退学/毕业)"
check_service "resume" "学生复学处理服务"
# Summary
log "INFO" "=== 启动完成 ==="
if [[ ${#failed_services[@]} -eq 0 ]]; then
success "所有服务启动成功"
echo ""
echo "服务控制命令:"
echo " 查看状态: $CONTROL_SCRIPT statusall"
echo " 停止服务: $CONTROL_SCRIPT stopall"
echo " 重启服务: $CONTROL_SCRIPT restart changes|resume"
echo ""
echo "日志文件位置: /var/log/student_status_change/"
else
error "以下服务启动失败: ${failed_services[*]}"
echo ""
echo "故障排除:"
echo "1. 检查数据库配置: cat data.conf"
echo "2. 查看日志文件: tail -f /var/log/student_status_change/*.log"
echo "3. 检查数据库连接: mysql -h\$DB_HOST -P\$DB_PORT -u\$DB_USER -p\$DB_PASS \$DB_NAME -e 'SELECT 1;'"
exit 1
fi
}
# Run main function
main "$@"
+177
View File
@@ -0,0 +1,177 @@
#!/bin/bash
# ===========================================
# 学生状态变更处理服务停止脚本
# 用于停止所有相关的处理服务
# ===========================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONTROL_SCRIPT="${SCRIPT_DIR}/control_scripts.sh"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "${timestamp} [$level] $message"
}
success() {
log "${GREEN}SUCCESS${NC}" "$1"
}
error() {
log "${RED}ERROR${NC}" "$1"
}
warning() {
log "${YELLOW}WARNING${NC}" "$1"
}
# Check if control script exists and is executable
if [[ ! -x "$CONTROL_SCRIPT" ]]; then
error "Control script not found or not executable: $CONTROL_SCRIPT"
exit 1
fi
# Function to stop a service
stop_service() {
local service_name="$1"
local service_desc="$2"
log "INFO" "Stopping $service_desc..."
if "$CONTROL_SCRIPT" stop "$service_name"; then
success "$service_desc stopped successfully"
return 0
else
warning "$service_desc may not have been running"
return 0 # Not treating this as a hard error
fi
}
# Function to check service status after stop
check_service_stopped() {
local service_name="$1"
local service_desc="$2"
log "INFO" "Verifying $service_desc has stopped..."
if "$CONTROL_SCRIPT" status "$service_name" | grep -q "is not running"; then
success "$service_desc confirmed stopped"
return 0
else
warning "$service_desc may still be running"
return 1
fi
}
# Function to force kill if needed
force_kill_service() {
local service_name="$1"
local script_pattern="$2"
local service_desc="$3"
log "INFO" "Force killing $service_desc..."
if pkill -f "$script_pattern"; then
sleep 2
success "$service_desc force killed"
return 0
else
warning "No processes found for $service_desc"
return 0
fi
}
# Main function
main() {
local force_kill="${1:-false}"
log "INFO" "=== 学生状态变更处理服务停止脚本 ==="
if [[ "$force_kill" == "force" ]]; then
log "INFO" "Performing force stop of all services..."
# Force kill all related processes
force_kill_service "changes" "process_student_changes.sh" "学生状态变更处理服务"
force_kill_service "resume" "process_student_resume.sh" "学生复学处理服务"
# Clean up lock and PID files
log "INFO" "Cleaning up lock and PID files..."
rm -f "${SCRIPT_DIR}"/*.lock
rm -f "${SCRIPT_DIR}/pids"/*.pid
success "Force stop completed"
exit 0
fi
log "INFO" "Stopping all student status change processing services..."
# Stop services gracefully
stop_service "changes" "学生状态变更处理服务 (休学/退学/毕业)"
stop_service "resume" "学生复学处理服务"
# Wait a moment for services to fully stop
sleep 3
log "INFO" "=== 服务状态验证 ==="
# Check all services are stopped
check_service_stopped "changes" "学生状态变更处理服务 (休学/退学/毕业)"
check_service_stopped "resume" "学生复学处理服务"
# Final cleanup
log "INFO" "Performing final cleanup..."
rm -f "${SCRIPT_DIR}"/*.lock 2>/dev/null || true
rm -f "${SCRIPT_DIR}/pids"/*.pid 2>/dev/null || true
log "INFO" "=== 停止完成 ==="
success "所有服务已停止"
echo ""
echo "如果服务未能正常停止,请使用强制停止:"
echo " $0 force"
echo ""
echo "重新启动服务:"
echo " ./start_services.sh"
}
# Show usage
show_usage() {
cat << EOF
Usage: $0 [force]
停止所有学生状态变更处理服务。
Options:
force 强制杀死所有相关进程并清理锁文件
Examples:
$0 # 正常停止所有服务
$0 force # 强制停止所有服务
EOF
}
# Parse arguments
case "${1:-}" in
force)
main "force"
;;
-h|--help)
show_usage
;;
"")
main
;;
*)
error "Unknown option: $1"
show_usage
exit 1
;;
esac
@@ -0,0 +1,104 @@
-- ===========================================
-- 学生状态变更处理测试数据
-- 用于测试自动化处理脚本
-- ===========================================
-- 注意:请根据实际情况修改学生ID和学号
-- 插入测试数据到 xg_student_status_change 表
-- 状态:0=未处理,1=已处理
-- 测试休学处理
INSERT INTO xg_student_status_change (
id, student_id, student_no, change_type, apply_date,
effective_date, reason, operator_id, source, process_status
) VALUES (
10001, 10001, '20240001', 'XIU_XUE', NOW(),
CURDATE(), '学生申请休学', 1, 'manual', 0
);
-- 测试退学处理
INSERT INTO xg_student_status_change (
id, student_id, student_no, change_type, apply_date,
effective_date, reason, operator_id, source, process_status
) VALUES (
10002, 10002, '20240002', 'TUI_XUE', NOW(),
CURDATE(), '学生申请退学', 1, 'manual', 0
);
-- 测试毕业处理
INSERT INTO xg_student_status_change (
id, student_id, student_no, change_type, apply_date,
effective_date, reason, operator_id, source, process_status
) VALUES (
10003, 10003, '20240003', 'BI_YE', NOW(),
CURDATE(), '学生毕业处理', 1, 'manual', 0
);
-- 测试复学处理
INSERT INTO xg_student_status_change (
id, student_id, student_no, change_type, apply_date,
effective_date, reason, operator_id, source, process_status
) VALUES (
10004, 10004, '20240004', 'FU_XUE', NOW(),
CURDATE(), '学生申请复学', 1, 'manual', 0
);
-- 批量测试数据(休学)
INSERT INTO xg_student_status_change (
id, student_id, student_no, change_type, apply_date,
effective_date, reason, operator_id, source, process_status
) SELECT
20000 + (@row_number := @row_number + 1),
20000 + @row_number,
CONCAT('2024', LPAD(@row_number, 4, '0')),
'XIU_XUE',
NOW(),
CURDATE(),
CONCAT('批量测试休学-', @row_number),
1,
'manual',
0
FROM (SELECT @row_number := 0) r
CROSS JOIN (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) nums;
-- 批量测试数据(退学)
INSERT INTO xg_student_status_change (
id, student_id, student_no, change_type, apply_date,
effective_date, reason, operator_id, source, process_status
) SELECT
30000 + (@row_number2 := @row_number2 + 1),
30000 + @row_number2,
CONCAT('2024', LPAD(@row_number2 + 10, 4, '0')),
'TUI_XUE',
NOW(),
CURDATE(),
CONCAT('批量测试退学-', @row_number2),
1,
'manual',
0
FROM (SELECT @row_number2 := 0) r2
CROSS JOIN (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) nums2;
-- 查询测试数据
SELECT
'测试数据插入完成' as status,
COUNT(*) as total_test_records
FROM xg_student_status_change
WHERE id >= 10001;
-- 按类型统计
SELECT
change_type,
COUNT(*) as count
FROM xg_student_status_change
WHERE id >= 10001
GROUP BY change_type
ORDER BY change_type;
-- ===========================================
-- 清理测试数据的SQL(执行前请确认)
-- ===========================================
-- DELETE FROM xg_student_status_change WHERE id >= 10001;
-- COMMIT;
@@ -11,8 +11,8 @@
### 📋 核心指导文档
- **[01-核心状态修改.md](01-核心状态修改.md)** - 学生学籍状态、用户登录状态
- **[02-组织关系处理.md](02-组织关系处理.md)** - 班级干部信息、社团成员关系
- **[03-经济资助处理.md](03-经济资助处理.md)** - 奖学金申请、获奖记录、勤工俭学、贫困生认定
- **[04-其他业务处理.md](04-其他业务处理.md)** - 住宿管理、心理健康、学生活动、志愿者、实习等
- **[03-经济资助处理.md](03-经济资助处理.md)** - 非核心业务,已简化处理
- **[04-其他业务处理.md](04-其他业务处理.md)** - 住宿管理退宿处理
### 🛠️ 实施文档
- **[06-完整SQL脚本.md](06-完整SQL脚本.md)** - 完整的数据库处理脚本和事务管理
@@ -40,19 +40,20 @@
学生退学、休学、毕业处理涉及多个业务模块的联动更新,具体处理内容请参考各分文档:
### 📋 处理步骤
1. **确定变更类型**: 休学(TUI_XUE)/退学(XIU_XUE)/复学(FU_XUE)/毕业(BI_YE)
1. **确定变更类型**: 休学(XIU_XUE)/退学(TUI_XUE)/复学(FU_XUE)/毕业(BI_YE)
2. **创建状态变更记录**: 插入 `xg_student_status_change` 表记录
3. **核心状态修改**: 更新 `xg_student_msg.student_status` 和 `sys_user.status`
4. **组织关系处理**: 处理班级干部、社团成员等关系变更
5. **经济资助处理**: 处理奖学金、勤工俭学、贫困生认定等业务
6. **其他业务处理**: 处理宿舍、心理咨询、学生活动、实习等
3. **备份原始数据**: 将相关表数据备份到对应的his表
4. **核心状态修改**: 更新 `xg_student_msg.student_status` 和 `sys_user.status`
5. **组织关系处理**: 处理班级干部、社团成员等关系变更(仅退学、休学、毕业时)
6. **住宿管理处理**: 处理宿舍退宿(仅退学、毕业时)
7. **更新处理状态**: 将 `xg_student_status_change.process_status` 设为1
8. **数据验证**: 确保所有相关数据已正确处理
### 🔄 状态映射关系
- **休学**: `student_status='2'`,`sys_user.status` 保持不变,可恢复业务
- **退学**: `student_status='4'`,`sys_user.status='1'`,终止所有业务
- **毕业**: `student_status='5'`,`sys_user.status='1'`,归档处理
- **休学**: `student_status='2'`,`sys_user.status='1'`,终止组织关系
- **退学**: `student_status='4'`,`sys_user.status='1'`,终止组织关系和住宿
- **复学**: `student_status='3'`,`sys_user.status='0'`,恢复账号
- **毕业**: `student_status='5'`,`sys_user.status='1'`,终止组织关系和住宿
### 🏗️ 技术架构
基于代码分析,主要涉及以下技术组件:
@@ -60,40 +61,35 @@
- `XgStudentMsg`: 学生基本信息实体
- `XgStudentStatusChange`: 学生状态变更记录实体
- `XgClassPosMsg`: 班级干部信息实体
- `StglMember`: 社团成员关系实体
- `JcbtScholarshipApplication`: 奖学金申请实体
- `JcbtWorkStudyApplication`: 勤工俭学申请实体
- `JcbtPovertyApplication`: 贫困生认定申请实体
- `ClubMember`: 社团成员关系实体
- `StglMember`: 志愿者成员关系实体
- `XgDormBedStudent`: 宿舍床位分配实体
- **业务服务**:
- `XgStudentStatusChangeServiceImpl`: 学生状态变更服务
- `XgClassPosMsgServiceImpl`: 班级干部管理服务
- `XljkConsultationAppointmentServiceImpl`: 心理咨询预约服务
- `JcbtScholarshipApplicationServiceImpl`: 奖学金申请服务
- `XgDormBedStudentServiceImpl`: 宿舍管理服务
- **数据访问**: MyBatis-Plus Mapper接口和XML配置
- **事务管理**: Spring `@Transactional` 声明式事务
- **状态机**: 通过状态字段值变化实现业务状态流转
- **工作流引擎**: Warm-Flow工作流框架处理复杂审批流程
- **his表备份**: 所有数据修改前自动备份到历史表
### 🎯 业务处理策略
- ✅ **核心状态**: 必须修改
- ✅ **进行中流程**: 全部终止
- ✅ **组织关系**: 全部解除
- ✅ **历史记录**: 保留(奖惩、考核由班主任管理)
- ✅ **关联业务**: 视具体情况终止
- ✅ **核心状态**: 必须修改(学籍状态+用户账号状态)
- ✅ **his表备份**: 所有修改前备份原始数据
- ✅ **组织关系**: 退学、休学、毕业时解除干部和社团关系
- ✅ **住宿管理**: 退学、毕业时处理退宿
- ✅ **复学处理**: 仅涉及核心状态恢复
## 📊 数据库表统计
涉及的数据库表总数:**25个核心表**
涉及的数据库表总数:**8个核心表**
| 模块 | 涉及表数量 | 重要程度 | 核心表名 |
|------|-----------|----------|----------|
| 核心状态管理 | 3 | ⭐⭐⭐⭐⭐ | `xg_student_msg`, `sys_user`, `xg_student_status_change` |
| 组织关系 | 2 | ⭐⭐⭐⭐⭐ | `xg_class_pos_msg`, `stgl_member` |
| 经济资助 | 4 | ⭐⭐⭐⭐ | `jcbt_scholarship_application`, `jcbt_scholarship_award`, `jcbt_work_study_application`, `jcbt_poverty_application` |
| 住宿管理 | 3 | ⭐⭐⭐⭐⭐ | `xg_dorm_bed_student`, `xg_dorm_application`, `xg_dorm_hygiene_student_score` |
| 心理健康 | 4 | ⭐⭐⭐⭐ | `xljk_consultation_appointment`, `xljk_consultation_record`, `xljk_consultation_no_show`, `xljk_evaluation_detail` |
| 实践活动 | 4 | ⭐⭐⭐⭐ | `stgl_activity_registration`, `stgl_volunteer_team`, `zsjy_internship_info`, `xg_student_leave` |
| 其他信息 | 5 | ⭐⭐⭐ | `xg_dorm_violation`, `sys_oper_log`, `sys_dict_data`, `sys_dict_type`, `flow_instance` |
| 组织关系 | 3 | ⭐⭐⭐⭐⭐ | `xg_class_pos_msg`, `club_member`, `stgl_member` |
| 住宿管理 | 2 | ⭐⭐⭐⭐⭐ | `xg_dorm_bed_student`, `xg_dorm_bed` |
| his表备份 | 8 | ⭐⭐⭐⭐⭐ | 各主表对应的his表 |
## ⚠️ 重要注意事项