提交
This commit is contained in:
1 parent
8f99ac7482
commit
f231386592
44 files changed
+800734
-2509
No files matched your search
File diff suppressed because it is too large.
Load diff
+29
@@ -26,16 +26,20 @@ import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.excel.core.DropDownOptions;
|
||||
import org.dromara.common.excel.core.ExcelResult;
|
||||
import org.dromara.common.excel.utils.ExcelUtil;
|
||||
import org.dromara.xg.domain.bo.XgDormCampusBo;
|
||||
import org.dromara.xg.domain.vo.XgClassMsgImportVo;
|
||||
import org.dromara.xg.domain.vo.XgClassMsgVo;
|
||||
import org.dromara.xg.domain.vo.XgSysDeptVo;
|
||||
import org.dromara.xg.domain.vo.XgDormCampusVo;
|
||||
import org.dromara.xg.domain.bo.XgClassMsgBo;
|
||||
import org.dromara.xg.domain.bo.XgClassStatusBo;
|
||||
import org.dromara.xg.domain.bo.XgClassStatusBatchBo;
|
||||
import org.dromara.xg.domain.bo.XgClassCampusBatchBo;
|
||||
import org.dromara.xg.excel.XgClassMsgImportTemplateWriter;
|
||||
import org.dromara.xg.listener.XgClassMsgImportListener;
|
||||
import org.dromara.xg.service.IXgClassMsgService;
|
||||
import org.dromara.xg.service.IXgSysDeptService;
|
||||
import org.dromara.xg.service.IXgDormCampusService;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
|
||||
@@ -54,6 +58,7 @@ public class XgClassMsgController extends BaseController {
|
||||
|
||||
private final IXgClassMsgService xgClassMsgService;
|
||||
private final IXgSysDeptService xgSysDeptService;
|
||||
private final IXgDormCampusService xgDormCampusService;
|
||||
|
||||
/**
|
||||
* 查询班级信息-新版列表
|
||||
@@ -136,6 +141,17 @@ public class XgClassMsgController extends BaseController {
|
||||
return toAjax(xgClassMsgService.updateClassStatusBatch(bo.getIds(), bo.getClassStatus()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置班级所在校区
|
||||
*/
|
||||
// @SaCheckPermission("classMsg:classMsg:edit")
|
||||
@Log(title = "班级信息-新版", businessType = BusinessType.UPDATE)
|
||||
@RepeatSubmit()
|
||||
@PutMapping("/campus/batch")
|
||||
public R<Void> updateCampusBatch(@Validated @RequestBody XgClassCampusBatchBo bo) {
|
||||
return toAjax(xgClassMsgService.updateCampusBatch(bo.getIds(), bo.getCampusId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除班级信息-新版
|
||||
*
|
||||
@@ -237,9 +253,22 @@ public class XgClassMsgController extends BaseController {
|
||||
// 学制常见可选值
|
||||
DropDownOptions schoolSystemOptions = new DropDownOptions(6, Arrays.asList("2", "3", "4", "5", "6"));
|
||||
|
||||
// 所在校区下拉(列 index=10)
|
||||
List<XgDormCampusVo> campusList = xgDormCampusService.queryList(new XgDormCampusBo());
|
||||
List<String> campusNames = new ArrayList<>();
|
||||
if (campusList != null) {
|
||||
for (XgDormCampusVo campus : campusList) {
|
||||
if (StringUtils.isNotBlank(campus.getCampusName())) {
|
||||
campusNames.add(campus.getCampusName());
|
||||
}
|
||||
}
|
||||
}
|
||||
DropDownOptions campusOptions = new DropDownOptions(10, campusNames);
|
||||
|
||||
List<DropDownOptions> options = new ArrayList<>();
|
||||
options.add(collegeToMajor);
|
||||
options.add(schoolSystemOptions);
|
||||
options.add(campusOptions);
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,16 @@ public class XgClassMsg extends TenantEntity {
|
||||
*/
|
||||
private String majorName;
|
||||
|
||||
/**
|
||||
* 所在校区ID,关联xg_dorm_campus.id
|
||||
*/
|
||||
private Long campusId;
|
||||
|
||||
/**
|
||||
* 所在校区名称
|
||||
*/
|
||||
private String campusName;
|
||||
|
||||
/**
|
||||
* 班级代码
|
||||
*/
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package org.dromara.xg.domain.bo;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 班级所在校区批量更新业务对象
|
||||
*/
|
||||
@Data
|
||||
public class XgClassCampusBatchBo {
|
||||
|
||||
/** 主键ID集合(雪花ID) */
|
||||
@NotEmpty(message = "主键ID集合不能为空")
|
||||
private List<Long> ids;
|
||||
|
||||
/** 校区ID,关联 xg_dorm_campus.id */
|
||||
@NotNull(message = "校区不能为空")
|
||||
private Long campusId;
|
||||
}
|
||||
+10
@@ -48,6 +48,16 @@ public class XgClassMsgBo extends BaseEntity {
|
||||
*/
|
||||
private String majorName;
|
||||
|
||||
/**
|
||||
* 所在校区ID,关联xg_dorm_campus.id(选填)
|
||||
*/
|
||||
private Long campusId;
|
||||
|
||||
/**
|
||||
* 所在校区名称
|
||||
*/
|
||||
private String campusName;
|
||||
|
||||
/**
|
||||
* 班级代码
|
||||
*/
|
||||
|
||||
+12
-4
@@ -99,10 +99,18 @@ public class XgClassMsgImportVo implements Serializable {
|
||||
@ExcelProperty(value = "班级教室位置", index = 9)
|
||||
private String classLocation;
|
||||
|
||||
/**
|
||||
* 所在校区(下拉:校区名称,选填)
|
||||
*/
|
||||
@ExcelProperty(value = "所在校区", index = 10)
|
||||
@ColumnWidth(20)
|
||||
@ExcelNotation(value = "选填,从下拉选择校区名称;留空则不写入校区")
|
||||
private String campus;
|
||||
|
||||
/**
|
||||
* 班级状态(字典下拉:xg_class_status)
|
||||
*/
|
||||
@ExcelProperty(value = "班级状态", index = 10, converter = ExcelDictConvert.class)
|
||||
@ExcelProperty(value = "班级状态", index = 11, converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(dictType = "xg_class_status")
|
||||
@NotBlank(message = "班级状态不能为空")
|
||||
private String classStatus;
|
||||
@@ -110,19 +118,19 @@ public class XgClassMsgImportVo implements Serializable {
|
||||
/**
|
||||
* 班主任(选填,填写姓名;仅唯一匹配时写入,不影响班级保存)
|
||||
*/
|
||||
@ExcelProperty(value = "班主任", index = 11)
|
||||
@ExcelProperty(value = "班主任", index = 12)
|
||||
@ExcelNotation(value = "选填,填写姓名。匹配到唯一人员才写入;未匹配或多人同名则不写入,不影响班级导入")
|
||||
private String classTeacherName;
|
||||
|
||||
/**
|
||||
* 班级描述
|
||||
*/
|
||||
@ExcelProperty(value = "班级描述", index = 12)
|
||||
@ExcelProperty(value = "班级描述", index = 13)
|
||||
private String classDescription;
|
||||
|
||||
/**
|
||||
* 备注信息
|
||||
*/
|
||||
@ExcelProperty(value = "备注信息", index = 13)
|
||||
@ExcelProperty(value = "备注信息", index = 14)
|
||||
private String remark;
|
||||
}
|
||||
+11
@@ -59,6 +59,17 @@ public class XgClassMsgVo implements Serializable {
|
||||
@ExcelProperty(value = "专业名称")
|
||||
private String majorName;
|
||||
|
||||
/**
|
||||
* 所在校区ID
|
||||
*/
|
||||
private Long campusId;
|
||||
|
||||
/**
|
||||
* 所在校区名称
|
||||
*/
|
||||
@ExcelProperty(value = "所在校区")
|
||||
private String campusName;
|
||||
|
||||
/**
|
||||
* 班级代码
|
||||
*/
|
||||
|
||||
+29
@@ -19,6 +19,7 @@ import org.dromara.common.excel.core.ExcelListener;
|
||||
import org.dromara.common.excel.core.ExcelResult;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.xg.domain.XgClassMsg;
|
||||
import org.dromara.xg.domain.XgDormCampus;
|
||||
import org.dromara.xg.domain.XgSysDept;
|
||||
import org.dromara.xg.domain.XgSysUser;
|
||||
import org.dromara.xg.domain.bo.XgClassMsgBo;
|
||||
@@ -26,6 +27,7 @@ import org.dromara.xg.domain.bo.XgClassTeacherCounselorRelBo;
|
||||
import org.dromara.xg.domain.vo.XgClassMsgImportVo;
|
||||
import org.dromara.xg.domain.vo.XgSysDeptVo;
|
||||
import org.dromara.xg.mapper.XgClassMsgMapper;
|
||||
import org.dromara.xg.mapper.XgDormCampusMapper;
|
||||
import org.dromara.xg.mapper.XgSysDeptMapper;
|
||||
import org.dromara.xg.mapper.XgSysUserMapper;
|
||||
import org.dromara.xg.service.IXgClassMsgService;
|
||||
@@ -50,6 +52,7 @@ public class XgClassMsgImportListener extends AnalysisEventListener<XgClassMsgIm
|
||||
private final XgSysDeptMapper deptMapper;
|
||||
private final XgClassMsgMapper classMsgMapper;
|
||||
private final XgSysUserMapper sysUserMapper;
|
||||
private final XgDormCampusMapper dormCampusMapper;
|
||||
private final Long operUserId;
|
||||
|
||||
private int successNum = 0;
|
||||
@@ -64,6 +67,7 @@ public class XgClassMsgImportListener extends AnalysisEventListener<XgClassMsgIm
|
||||
this.deptMapper = SpringUtils.getBean(XgSysDeptMapper.class);
|
||||
this.classMsgMapper = SpringUtils.getBean(XgClassMsgMapper.class);
|
||||
this.sysUserMapper = SpringUtils.getBean(XgSysUserMapper.class);
|
||||
this.dormCampusMapper = SpringUtils.getBean(XgDormCampusMapper.class);
|
||||
this.operUserId = LoginHelper.getUserId();
|
||||
}
|
||||
|
||||
@@ -127,6 +131,8 @@ public class XgClassMsgImportListener extends AnalysisEventListener<XgClassMsgIm
|
||||
bo.setRemark(importVo.getRemark());
|
||||
bo.setCreateBy(operUserId);
|
||||
|
||||
resolveAndFillCampus(bo, importVo.getCampus());
|
||||
|
||||
classMsgService.insertByBo(bo);
|
||||
|
||||
String teacherTip = bindClassTeacherIfUnique(bo.getId(), importVo.getClassTeacherName());
|
||||
@@ -201,6 +207,7 @@ public class XgClassMsgImportListener extends AnalysisEventListener<XgClassMsgIm
|
||||
importVo.getGrade(),
|
||||
importVo.getClassStatus(),
|
||||
importVo.getClassLocation(),
|
||||
importVo.getCampus(),
|
||||
importVo.getClassDescription(),
|
||||
importVo.getRemark()
|
||||
) && importVo.getEnrollmentYear() == null
|
||||
@@ -208,6 +215,28 @@ public class XgClassMsgImportListener extends AnalysisEventListener<XgClassMsgIm
|
||||
&& importVo.getIsGraduation() == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按校区名称解析并写入 BO;空则跳过。
|
||||
*/
|
||||
private void resolveAndFillCampus(XgClassMsgBo bo, String campusNameRaw) {
|
||||
String campusName = StringUtils.trim(campusNameRaw);
|
||||
if (StringUtils.isBlank(campusName)) {
|
||||
return;
|
||||
}
|
||||
LambdaQueryWrapper<XgDormCampus> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.eq(XgDormCampus::getCampusName, campusName);
|
||||
List<XgDormCampus> list = dormCampusMapper.selectList(wrapper);
|
||||
if (list == null || list.isEmpty()) {
|
||||
throw new ServiceException("校区不存在:" + campusName);
|
||||
}
|
||||
if (list.size() > 1) {
|
||||
throw new ServiceException("校区名称不唯一:" + campusName);
|
||||
}
|
||||
XgDormCampus campus = list.get(0);
|
||||
bo.setCampusId(campus.getId());
|
||||
bo.setCampusName(campus.getCampusName());
|
||||
}
|
||||
|
||||
private Long resolveCollegeId(String optionValue) {
|
||||
Long id = parseOptionId(optionValue);
|
||||
if (id != null) {
|
||||
|
||||
+9
@@ -83,4 +83,13 @@ public interface IXgClassMsgService {
|
||||
* @return 是否更新成功
|
||||
*/
|
||||
Boolean updateClassStatusBatch(Collection<Long> ids, String classStatus);
|
||||
|
||||
/**
|
||||
* 批量更新班级所在校区
|
||||
*
|
||||
* @param ids 班级ID集合
|
||||
* @param campusId 校区ID
|
||||
* @return 是否更新成功
|
||||
*/
|
||||
Boolean updateCampusBatch(Collection<Long> ids, Long campusId);
|
||||
}
|
||||
+53
-1
@@ -2,8 +2,8 @@ package org.dromara.xg.service.impl;
|
||||
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.system.domain.SysDept;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
@@ -16,9 +16,11 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.dromara.xg.domain.bo.XgClassMsgBo;
|
||||
import org.dromara.xg.domain.vo.XgClassMsgVo;
|
||||
import org.dromara.xg.domain.XgClassMsg;
|
||||
import org.dromara.xg.domain.XgDormCampus;
|
||||
import org.dromara.xg.domain.XgStudentMsg;
|
||||
import org.dromara.xg.domain.XgSysDept;
|
||||
import org.dromara.xg.mapper.XgClassMsgMapper;
|
||||
import org.dromara.xg.mapper.XgDormCampusMapper;
|
||||
import org.dromara.xg.mapper.XgStudentMsgMapper;
|
||||
import org.dromara.xg.mapper.XgSysDeptMapper;
|
||||
import org.dromara.xg.service.IXgClassMsgService;
|
||||
@@ -41,6 +43,7 @@ public class XgClassMsgServiceImpl implements IXgClassMsgService {
|
||||
private final XgClassMsgMapper baseMapper;
|
||||
private final XgSysDeptMapper deptMapper;
|
||||
private final XgStudentMsgMapper studentMsgMapper;
|
||||
private final XgDormCampusMapper dormCampusMapper;
|
||||
|
||||
/**
|
||||
* 查询班级信息-新版
|
||||
@@ -94,9 +97,25 @@ public class XgClassMsgServiceImpl implements IXgClassMsgService {
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getGrade()), XgClassMsg::getGrade, bo.getGrade());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getClassStatus()), XgClassMsg::getClassStatus, bo.getClassStatus());
|
||||
lqw.eq(bo.getClassTeacherId() != null, XgClassMsg::getClassTeacherId, bo.getClassTeacherId());
|
||||
lqw.eq(bo.getCampusId() != null, XgClassMsg::getCampusId, bo.getCampusId());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 campusId 回填 campusName;为空则清空名称
|
||||
*/
|
||||
private void fillCampusFields(XgClassMsgBo bo) {
|
||||
if (bo.getCampusId() == null) {
|
||||
bo.setCampusName(null);
|
||||
return;
|
||||
}
|
||||
XgDormCampus campus = dormCampusMapper.selectById(bo.getCampusId());
|
||||
if (campus == null) {
|
||||
throw new ServiceException("校区不存在或已删除");
|
||||
}
|
||||
bo.setCampusName(campus.getCampusName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增班级信息-新版
|
||||
*
|
||||
@@ -106,6 +125,7 @@ public class XgClassMsgServiceImpl implements IXgClassMsgService {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean insertByBo(XgClassMsgBo bo) {
|
||||
fillCampusFields(bo);
|
||||
XgClassMsg add = MapstructUtils.convert(bo, XgClassMsg.class);
|
||||
validEntityBeforeSave(add);
|
||||
|
||||
@@ -160,6 +180,7 @@ public class XgClassMsgServiceImpl implements IXgClassMsgService {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean updateByBo(XgClassMsgBo bo) {
|
||||
fillCampusFields(bo);
|
||||
XgClassMsg update = MapstructUtils.convert(bo, XgClassMsg.class);
|
||||
validEntityBeforeSave(update);
|
||||
|
||||
@@ -168,6 +189,15 @@ public class XgClassMsgServiceImpl implements IXgClassMsgService {
|
||||
updateWrapper.eq(XgClassMsg::getId, update.getId());
|
||||
boolean result = baseMapper.update(update, updateWrapper) > 0;
|
||||
|
||||
// 显式同步校区字段,支持清空(绕过 update 忽略 null 策略)
|
||||
if (result) {
|
||||
LambdaUpdateWrapper<XgClassMsg> campusUw = Wrappers.lambdaUpdate();
|
||||
campusUw.eq(XgClassMsg::getId, update.getId())
|
||||
.set(XgClassMsg::getCampusId, bo.getCampusId())
|
||||
.set(XgClassMsg::getCampusName, bo.getCampusName());
|
||||
baseMapper.update(null, campusUw);
|
||||
}
|
||||
|
||||
// 更新成功后,同步更新部门表信息
|
||||
if (result) {
|
||||
// 查询部门信息
|
||||
@@ -307,4 +337,26 @@ public class XgClassMsgServiceImpl implements IXgClassMsgService {
|
||||
uw.in(XgClassMsg::getId, ids).set(XgClassMsg::getClassStatus, classStatus);
|
||||
return baseMapper.update(null, uw) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新班级所在校区
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateCampusBatch(Collection<Long> ids, Long campusId) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (campusId == null) {
|
||||
throw new ServiceException("校区不能为空");
|
||||
}
|
||||
XgDormCampus campus = dormCampusMapper.selectById(campusId);
|
||||
if (campus == null) {
|
||||
throw new ServiceException("校区不存在或已删除");
|
||||
}
|
||||
LambdaUpdateWrapper<XgClassMsg> uw = Wrappers.lambdaUpdate();
|
||||
uw.in(XgClassMsg::getId, ids)
|
||||
.set(XgClassMsg::getCampusId, campus.getId())
|
||||
.set(XgClassMsg::getCampusName, campus.getCampusName());
|
||||
return baseMapper.update(null, uw) > 0;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="collegeName" column="college_name"/>
|
||||
<result property="majorId" column="major_id"/>
|
||||
<result property="majorName" column="major_name"/>
|
||||
<result property="campusId" column="campus_id"/>
|
||||
<result property="campusName" column="campus_name"/>
|
||||
<result property="classCode" column="class_code"/>
|
||||
<result property="className" column="class_name"/>
|
||||
<result property="classType" column="class_type"/>
|
||||
@@ -65,6 +67,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<sql id="selectXgClassMsgVo">
|
||||
select c.id, c.college_id, dc.dept_name as college_name, c.major_id, dm.dept_name as major_name,
|
||||
c.campus_id, c.campus_name,
|
||||
c.class_code, c.class_name, c.class_type, c.grade, c.enrollment_year,c.school_system, c.graduation_year,
|
||||
c.training_type, c.degree_type, c.counselor_id, c.counselor_name, c.class_teacher_id,
|
||||
c.class_teacher_name, c.class_advisor_id, c.class_advisor_name, c.class_monitor_id,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# 班级信息「所在校区」Implementation Plan
|
||||
|
||||
> **For agentic workers:** Implement task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 为班级信息增加选填「所在校区」,支持列表筛选、单条维护、批量设置、导入模板下拉与导入解析。
|
||||
|
||||
**Architecture:** `xg_class_msg` 增加 `campus_id` + `campus_name`;增改/批量由服务端按 `xg_dorm_campus` 回填名称;导入按校区名称解析;前端复用 `listDormCampus`。
|
||||
|
||||
**Tech Stack:** Spring Boot 3 + MyBatis-Plus、Vue 3 + Element Plus、FastExcel 下拉
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-01-class-msg-campus-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
| File | Responsibility |
|
||||
|------|----------------|
|
||||
| `docs/数据库脚本/2026-09-01-xg_class_msg-add-campus.sql` | 加列+索引 |
|
||||
| `XgClassMsg.java` / Bo / Vo / ImportVo | 字段与 Excel 列 |
|
||||
| `XgClassCampusBatchBo.java` | 批量请求体 |
|
||||
| `XgClassMsgController` / Service / Impl | 筛选、回填、batch、模板下拉 |
|
||||
| `XgClassMsgImportListener` | 导入解析校区 |
|
||||
| `XgClassMsgMapper.xml` | resultMap + select 补字段 |
|
||||
| `web/.../classMsg/types.ts` + `index.ts` | 类型与 batch API |
|
||||
| `web/.../classMsg/index.vue` | 搜索/列/表单/批量弹窗 |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 数据库
|
||||
|
||||
- [ ] 编写并(在可连库时)执行增量 SQL
|
||||
|
||||
### Task 2: 后端领域模型 + Mapper
|
||||
|
||||
- [ ] Entity/Bo/Vo 加 `campusId`/`campusName`;Vo 导出 Excel 列「所在校区」
|
||||
- [ ] ImportVo 插入「所在校区」列并顺延后续 index
|
||||
- [ ] Mapper.xml 补字段
|
||||
|
||||
### Task 3: 服务与接口
|
||||
|
||||
- [ ] `buildQueryWrapper` 支持 `campusId`
|
||||
- [ ] insert/update 前校验并回填 `campusName`(空则清空)
|
||||
- [ ] `updateCampusBatch` + Controller `PUT /campus/batch`
|
||||
- [ ] importTemplate 增加校区下拉
|
||||
|
||||
### Task 4: 导入监听
|
||||
|
||||
- [ ] 按校区名称解析;空跳过;0/多匹配失败
|
||||
|
||||
### Task 5: 前端
|
||||
|
||||
- [ ] types + `updateClassCampusBatch`
|
||||
- [ ] 搜索、表格列、表单、批量弹窗
|
||||
|
||||
### Task 6: 文档
|
||||
|
||||
- [ ] 更新 spec 状态为已实现;按需补业务/API 说明
|
||||
@@ -0,0 +1,199 @@
|
||||
# 班级信息增加「所在校区」
|
||||
|
||||
> 日期:2026-09-01
|
||||
> 状态:已实现(待库表执行 SQL 后验收)
|
||||
> 方案:A — 班级表直接挂 `campus_id` + `campus_name`
|
||||
> 范围:Web 班级信息模块 + 后端 `classMsg` + 导入模板/导入;不改移动端
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 问题
|
||||
|
||||
班级信息(`xg_class_msg`)目前无校区属性。业务需要标识班级所属校区,数据来源于【校区信息】(`xg_dorm_campus`),并支持历史数据逐步补齐。
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
1. 班级增加「所在校区」(选填)
|
||||
2. 列表可按校区筛选;表格展示校区名称
|
||||
3. 新增/修改可选择校区
|
||||
4. 列表多选后「批量设置校区」
|
||||
5. 导入模板与导入支持校区(下拉 + 按名称解析)
|
||||
|
||||
### 1.3 已确认产品决策
|
||||
|
||||
| 项 | 决策 |
|
||||
|----|------|
|
||||
| 必填性 | 选填,允许为空 |
|
||||
| 存储方案 | A:`campus_id` + `campus_name` 冗余(对齐学院/专业) |
|
||||
| 批量设置 | 列表多选 → 按钮 → 弹窗选一个校区统一写入 |
|
||||
| 导入识别 | 模板下拉填校区名称;导入按名称解析为 ID |
|
||||
| 列表筛选 | 搜索区增加校区下拉 |
|
||||
| 历史补齐 | 主要靠批量设置;导入仅服务新建班级 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据模型
|
||||
|
||||
表 `xg_class_msg` 新增:
|
||||
|
||||
| 字段 | 类型 | 可空 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `campus_id` | `bigint(20)` | YES | 关联 `xg_dorm_campus.id` |
|
||||
| `campus_name` | `varchar(100)` | YES | 冗余校区名称 |
|
||||
|
||||
- 索引:`KEY idx_campus_id (campus_id)`
|
||||
- **不建数据库外键**(与学院/专业一致)
|
||||
- 校区改名/删除:本期不做级联同步;班级保留已写入值;筛选仍按 `campus_id`
|
||||
|
||||
同步改动的类型:
|
||||
|
||||
- 后端:`XgClassMsg`、`XgClassMsgBo`、`XgClassMsgVo`、`XgClassMsgImportVo`
|
||||
- 前端:`ClassMsgVO` / `ClassMsgForm` / `ClassMsgQuery`(`web/src/api/xg/xsjcxx/classMsg/...`)
|
||||
|
||||
增量 SQL 放入 `docs/数据库脚本/`(文件名建议 `2026-09-01-xg_class_msg-add-campus.sql`),实现时同步更新 `docs/设计文档/database/` 中班级表说明(若有)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 接口设计
|
||||
|
||||
### 3.1 扩展现有接口
|
||||
|
||||
| 方法 | 路径 | 变化 |
|
||||
|------|------|------|
|
||||
| GET | `/classMsg/classMsg/list` | Query 支持 `campusId`;行数据含 `campusId`/`campusName` |
|
||||
| GET | `/classMsg/classMsg/{id}` | 详情含校区字段 |
|
||||
| POST | `/classMsg/classMsg` | Body 可选 `campusId`;服务端校验存在后回填 `campusName` |
|
||||
| PUT | `/classMsg/classMsg` | 同上;允许将校区改为空(清空) |
|
||||
| POST | `/classMsg/classMsg/export` | 导出列增加「所在校区」(`campusName`) |
|
||||
| POST | `/classMsg/classMsg/importTemplate` | 模板增加「所在校区」下拉 |
|
||||
| POST | `/classMsg/classMsg/importData` | 解析并写入校区 |
|
||||
|
||||
单条增改校区规则:
|
||||
|
||||
- `campusId` 为空:`campus_id`、`campus_name` 置空
|
||||
- `campusId` 有值:必须在 `xg_dorm_campus` 且未删除;用该记录的 `campus_name` 覆盖冗余字段(不信任前端传的 name)
|
||||
|
||||
### 3.2 新增批量接口
|
||||
|
||||
```
|
||||
PUT /classMsg/classMsg/campus/batch
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"ids": [1, 2, 3],
|
||||
"campusId": 1946815553291304962
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 校验 |
|
||||
|------|------|
|
||||
| `ids` | 非空 |
|
||||
| `campusId` | 非空;必须存在于 `xg_dorm_campus`(未删除) |
|
||||
|
||||
行为:将选中班级的 `campus_id`、`campus_name` 统一更新为该校区。
|
||||
权限:`classMsg:classMsg:edit`(与现有批量状态一致,当前控制器上部分权限注解可能被注释,实现时保持与同模块其他写接口同一风格)。
|
||||
日志:`@Log` 业务类型 UPDATE,标题「班级信息-新版」。
|
||||
|
||||
BO 建议:`XgClassCampusBatchBo`(对齐 `XgClassStatusBatchBo`)。
|
||||
|
||||
校区选项:前端继续使用 `GET /xqxx/dormCampus/list`,不新增选项 API。
|
||||
|
||||
---
|
||||
|
||||
## 4. 前端设计
|
||||
|
||||
页面:`web/src/views/xg/xsjcxx/classmsg/classMsg/index.vue`
|
||||
|
||||
| 区域 | 改动 |
|
||||
|------|------|
|
||||
| 搜索区 | 「所在校区」`el-select`,选项来自 `listDormCampus`,`queryParams.campusId` |
|
||||
| 工具栏 | 「批量设置校区」按钮;未勾选行时禁用(与毕业/暂停同用 `:disabled="multiple"`);`v-hasPermi="['classMsg:classMsg:edit']"` |
|
||||
| 表格 | 列「所在校区」→ `campusName` |
|
||||
| 新增/修改弹窗 | 「所在校区」下拉,选填;变更时同步 `form.campusId` / `form.campusName` |
|
||||
| 批量弹窗 | 仅校区下拉(必选)+ 确认;调用 `updateClassCampusBatch` |
|
||||
|
||||
API 封装:在 `classMsg/index.ts` 增加 `updateClassCampusBatch`,指向 `/classMsg/classMsg/campus/batch`。
|
||||
|
||||
交互:成功后提示并 `getList()`,对齐批量毕业/暂停。
|
||||
|
||||
---
|
||||
|
||||
## 5. 导入 / 模板规则
|
||||
|
||||
### 5.1 模板
|
||||
|
||||
- `XgClassMsgImportVo` 增加字段「所在校区」(选填,无 `@NotBlank`)
|
||||
- 列顺序:插在「班级教室位置」之后;其后「班级状态 / 班主任 / 班级描述 / 备注」index 依次 +1
|
||||
- `buildImportDropDownOptions`:加载全部有效校区名称,注册为该列下拉
|
||||
- 示例行可不填校区;若填写须为真实可解析的校区名
|
||||
|
||||
### 5.2 导入解析(`XgClassMsgImportListener`)
|
||||
|
||||
| 情况 | 行为 |
|
||||
|------|------|
|
||||
| 单元格为空 | 不设置校区,班级仍可导入成功 |
|
||||
| 名称唯一匹配到未删除校区 | 写入 `campusId` + `campusName` |
|
||||
| 名称不存在 / 已删除 | **该行失败**,提示「校区不存在:xxx」 |
|
||||
| 重名校区(极端) | 该行失败,提示「校区名称不唯一:xxx」 |
|
||||
|
||||
说明:现有导入为**仅新增**;班级名重复则失败。历史班级补校区依赖批量设置,**本期不做导入更新已有班级**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 主要改动文件(预估)
|
||||
|
||||
**数据库**
|
||||
|
||||
- 增量 SQL:`xg_class_msg` 加列 + 索引
|
||||
|
||||
**后端**
|
||||
|
||||
- `XgClassMsg.java` / `XgClassMsgBo` / `XgClassMsgVo` / `XgClassMsgImportVo`
|
||||
- `XgClassCampusBatchBo`(新建)
|
||||
- `XgClassMsgController`:list/export/importTemplate/importData + `/campus/batch`
|
||||
- `IXgClassMsgService` / `XgClassMsgServiceImpl`:查询条件、回填名称、批量更新
|
||||
- `XgClassMsgImportListener`:校区解析
|
||||
- `XgClassMsgMapper.xml`:若自定义列清单需补字段
|
||||
|
||||
**前端**
|
||||
|
||||
- `api/.../classMsg/types.ts`、`classMsg/index.ts`
|
||||
- `views/.../classMsg/index.vue`
|
||||
|
||||
**文档**
|
||||
|
||||
- 本 spec;实现后按项目规范同步 API/业务模块文档(若有班级信息专档)
|
||||
|
||||
---
|
||||
|
||||
## 7. 不在本期
|
||||
|
||||
- 批量清空校区
|
||||
- 校区改名/删除后同步班级冗余名
|
||||
- 导入更新已有班级
|
||||
- 移动端班级相关页面
|
||||
- 数据库物理外键
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收要点
|
||||
|
||||
1. 新增班级可不选校区;可选校区后列表显示名称
|
||||
2. 搜索按校区过滤正确(含「空校区」班级不被误筛入有校区条件)
|
||||
3. 多选批量设置后,选中行 `campusId`/`campusName` 一致更新
|
||||
4. 下载模板「所在校区」为下拉;导入空校区成功;错误校区名行失败且有明确提示
|
||||
5. 导出含所在校区列
|
||||
6. 修改班级时可更换或清空校区
|
||||
|
||||
---
|
||||
|
||||
## 9. 测试建议
|
||||
|
||||
- 单测/手工:批量接口非法 `campusId`、空 `ids`
|
||||
- 手工:导入模板下拉可选到西溪/临安等现有校区
|
||||
- 回归:原有批量毕业/暂停/解散、学院专业导入联动不受影响
|
||||
@@ -2,11 +2,32 @@
|
||||
|
||||
**模块名称**:classMsg / 班级信息
|
||||
**基础路径**:`/classMsg/classMsg`
|
||||
**更新时间**:2026-07-22
|
||||
**更新时间**:2026-09-01
|
||||
|
||||
## 模块功能
|
||||
|
||||
班级基础信息的增删改查、状态批量变更,以及 Excel 批量导入(模板含字典下拉、学院-专业联动下拉)。
|
||||
班级基础信息的增删改查、状态批量变更、所在校区维护(含批量设置),以及 Excel 批量导入(模板含字典下拉、学院-专业联动下拉、校区下拉)。
|
||||
|
||||
## 所在校区(2026-09-01)
|
||||
|
||||
- 数据字段:`campusId` / `campusName`(选填,关联【校区信息】`xg_dorm_campus`)
|
||||
- 列表查询支持 `campusId` 筛选
|
||||
- 新增/修改可选择或清空校区;服务端按校区 ID 回填名称
|
||||
- 历史班级补齐:列表多选 →「批量设置校区」
|
||||
|
||||
### 批量设置校区
|
||||
|
||||
- **URL**:`PUT /classMsg/classMsg/campus/batch`
|
||||
- **Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"ids": [1, 2, 3],
|
||||
"campusId": 1946815553291304962
|
||||
}
|
||||
```
|
||||
|
||||
- **说明**:`ids`、`campusId` 均必填;校区须存在且未删除;统一写入 `campus_id` + `campus_name`
|
||||
|
||||
## 导入相关接口
|
||||
|
||||
@@ -28,6 +49,7 @@
|
||||
| 毕业年份 | 公式自动计算 | Excel 公式:`入学年份+学制`;导入时同样按此规则计算 |
|
||||
| 是否已毕业 | 字典下拉 | `is_graduation`(未毕业/已毕业) |
|
||||
| 班级教室位置 | 文本 | 可选 |
|
||||
| 所在校区 | 下拉 | 选填;选项来自【校区信息】校区名称;按名称解析写入 |
|
||||
| 班级状态 | 字典下拉 | `xg_class_status`(正常/暂停/毕业/解散) |
|
||||
| 班主任 | 文本(姓名) | 选填;按教职工姓名精确匹配。唯一命中才写入;未匹配或多人同名则跳过,**不影响班级保存** |
|
||||
| 班级描述 | 文本 | 可选 |
|
||||
@@ -35,8 +57,10 @@
|
||||
|
||||
- **注意**:
|
||||
- 模板首行示例数据以「示例」开头,导入时会自动跳过
|
||||
- 学院、专业、字典项请务必从下拉框选择,避免手填错误
|
||||
- 学院、专业、校区、字典项请务必从下拉框选择,避免手填错误
|
||||
- 所在校区留空则不写入;名称不存在或不唯一则该行失败
|
||||
- 班主任匹配失败不会导致该行班级导入失败,可事后在列表中单独设置
|
||||
- 导入为仅新增;历史班级补校区请用「批量设置校区」
|
||||
|
||||
### 2. 导入班级数据
|
||||
|
||||
@@ -50,8 +74,9 @@
|
||||
2. 字典中文标签自动转换为字典值
|
||||
3. 班级名称作为 `classCode` / `className`,重复则该行失败
|
||||
4. 毕业年份按 `入学年份 + 学制` 自动计算
|
||||
5. 调用既有新增逻辑,同步创建对应 `sys_dept` 班级节点
|
||||
6. 班主任按姓名匹配有效教职工:0 人或多人 → 不写班主任;1 人 → 写入并同步关联/角色
|
||||
5. 可选解析所在校区名称并写入 `campusId`/`campusName`
|
||||
6. 调用既有新增逻辑,同步创建对应 `sys_dept` 班级节点
|
||||
7. 班主任按姓名匹配有效教职工:0 人或多人 → 不写班主任;1 人 → 写入并同步关联/角色
|
||||
|
||||
## 其他已有接口(摘要)
|
||||
|
||||
@@ -64,10 +89,12 @@
|
||||
| 删除 | DELETE | `/classMsg/classMsg/{ids}` |
|
||||
| 更新状态 | PUT | `/classMsg/classMsg/status` |
|
||||
| 批量更新状态 | PUT | `/classMsg/classMsg/status/batch` |
|
||||
| 批量设置校区 | PUT | `/classMsg/classMsg/campus/batch` |
|
||||
| 导出 | POST | `/classMsg/classMsg/export` |
|
||||
| 我作为班主任的班级 | GET | `/classMsg/classMsg/myClassListAsTeacher` |
|
||||
|
||||
## 前端入口
|
||||
|
||||
- 页面:`web/src/views/xg/xsjcxx/classmsg/classMsg/index.vue`
|
||||
- 操作:工具栏「导入」→ 下载模板 / 上传 Excel
|
||||
- 操作:工具栏「导入」→ 下载模板 / 上传 Excel;「批量设置校区」
|
||||
- 数据库脚本:`docs/数据库脚本/2026-09-01-xg_class_msg-add-campus.sql`
|
||||
@@ -0,0 +1,7 @@
|
||||
-- 班级信息增加所在校区字段
|
||||
-- 2026-09-01
|
||||
|
||||
ALTER TABLE `xg_class_msg`
|
||||
ADD COLUMN `campus_id` bigint(20) DEFAULT NULL COMMENT '所在校区ID,关联xg_dorm_campus.id' AFTER `major_name`,
|
||||
ADD COLUMN `campus_name` varchar(100) DEFAULT NULL COMMENT '所在校区名称' AFTER `campus_id`,
|
||||
ADD KEY `idx_campus_id` (`campus_id`);
|
||||
@@ -0,0 +1,228 @@
|
||||
-- ============================================================
|
||||
-- 班级信息:按班级名称批量设置所在校区
|
||||
-- 日期:2026-09-01
|
||||
-- 依赖:先执行 2026-09-01-xg_class_msg-add-campus.sql
|
||||
-- 说明:
|
||||
-- 1. 按 class_name 精确匹配(含「无人机 261」等带空格名称)
|
||||
-- 2. 校区 ID 从 xg_dorm_campus 按名称解析,不写死雪花 ID
|
||||
-- 3. 执行前可用下方「预览」语句核对命中条数与未命中班级
|
||||
-- ============================================================
|
||||
|
||||
-- ---------- 预览:西溪校区将命中多少条 ----------
|
||||
-- SELECT id, class_name, campus_id, campus_name
|
||||
-- FROM xg_class_msg
|
||||
-- WHERE deleted = 0
|
||||
-- AND class_name IN (
|
||||
-- '美术设计241','美术设计242','美术设计251','电气261','电气263','电气264',
|
||||
-- '机器人261','无人机 261','数字制造261','智能制造261','汽修261','机电261','机电263',
|
||||
-- '中药261','中药263','药剂261','生物制药261','健康管理261','园林技术261','园林技术263',
|
||||
-- '环境设计261','农业技术261','西餐运营261','西餐运营263','西餐运营264','中烹261','中烹263',
|
||||
-- '中西点261','中西点263','数字媒体261','数字媒体263','人工智能261','电商261','电商263','电商264','美术设计261',
|
||||
-- '电气211','无人机221','无人机222','电气241','无人机241','电气221','电气231','无人机231',
|
||||
-- '大数据231','大数据241','机器人241','电气251','机器人251','无人机 251','大数据251',
|
||||
-- '药剂211','药剂222','药剂223','医药物流221','药剂242','药剂243','药品营销241',
|
||||
-- '药剂221','药剂231','药剂232','医药物流231','药剂241','药剂251','药剂252','药剂253','药品营销251','健康管理251',
|
||||
-- '西餐运营211','烹饪221','西餐运营222','西餐运营241','中烹241','中西点241',
|
||||
-- '西餐运营221','西餐运营231','西餐运营232','中烹231','中西点231','中西点232',
|
||||
-- '西餐运营242','中烹242','西餐运营251','西餐运营252','中烹251','中烹252','中西点251','中西点252',
|
||||
-- '电子商务242','数字媒体241','电商231','电商232','电子商务241','人工智能241',
|
||||
-- '数字媒体251','人工智能251','网络安全251','电商251','电商252'
|
||||
-- );
|
||||
|
||||
-- ---------- 预览:临安校区将命中多少条 ----------
|
||||
-- SELECT id, class_name, campus_id, campus_name
|
||||
-- FROM xg_class_msg
|
||||
-- WHERE deleted = 0
|
||||
-- AND class_name IN (
|
||||
-- '数字制造211','汽修221','数控221','数控241','数字制造221','数控231','数字制造231',
|
||||
-- '汽修231','汽修232','数字制造241','汽修241','数字制造251','智能制造251','汽修251','产品检测251',
|
||||
-- '中药211','中药221','中药231','中药232','中药241','中药251','中药252',
|
||||
-- '园林211','园林工程221','园林设计221','园林技术241','园林设计241','园林技术221',
|
||||
-- '园林工程231','园林设计231','园林技术231','园林工程241','园林技术251','园林技术252',
|
||||
-- '园林工程251','环境设计251'
|
||||
-- );
|
||||
|
||||
-- ---------- 预览:名单中库里不存在的班级名(两边合并后左连接检查)----------
|
||||
-- 可将下方更新用的名单拆出核对;有结果说明名称不一致(空格/别名等)
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
-- 1) 西溪校区
|
||||
UPDATE xg_class_msg c
|
||||
INNER JOIN xg_dorm_campus camp
|
||||
ON camp.campus_name = '西溪校区'
|
||||
AND camp.deleted = '0'
|
||||
SET
|
||||
c.campus_id = camp.id,
|
||||
c.campus_name = camp.campus_name,
|
||||
c.update_time = NOW()
|
||||
WHERE c.deleted = 0
|
||||
AND c.class_name IN (
|
||||
-- 261 / 美术设计等
|
||||
'美术设计241',
|
||||
'美术设计242',
|
||||
'美术设计251',
|
||||
'电气261',
|
||||
'电气263',
|
||||
'电气264',
|
||||
'机器人261',
|
||||
'无人机 261',
|
||||
'数字制造261',
|
||||
'智能制造261',
|
||||
'汽修261',
|
||||
'机电261',
|
||||
'机电263',
|
||||
'中药261',
|
||||
'中药263',
|
||||
'药剂261',
|
||||
'生物制药261',
|
||||
'健康管理261',
|
||||
'园林技术261',
|
||||
'园林技术263',
|
||||
'环境设计261',
|
||||
'农业技术261',
|
||||
'西餐运营261',
|
||||
'西餐运营263',
|
||||
'西餐运营264',
|
||||
'中烹261',
|
||||
'中烹263',
|
||||
'中西点261',
|
||||
'中西点263',
|
||||
'数字媒体261',
|
||||
'数字媒体263',
|
||||
'人工智能261',
|
||||
'电商261',
|
||||
'电商263',
|
||||
'电商264',
|
||||
'美术设计261',
|
||||
-- 电气 / 无人机 / 大数据 / 机器人
|
||||
'电气211',
|
||||
'无人机221',
|
||||
'无人机222',
|
||||
'电气241',
|
||||
'无人机241',
|
||||
'电气221',
|
||||
'电气231',
|
||||
'无人机231',
|
||||
'大数据231',
|
||||
'大数据241',
|
||||
'机器人241',
|
||||
'电气251',
|
||||
'机器人251',
|
||||
'无人机 251',
|
||||
'大数据251',
|
||||
-- 药剂 / 医药物流 / 健康管理
|
||||
'药剂211',
|
||||
'药剂222',
|
||||
'药剂223',
|
||||
'医药物流221',
|
||||
'药剂242',
|
||||
'药剂243',
|
||||
'药品营销241',
|
||||
'药剂221',
|
||||
'药剂231',
|
||||
'药剂232',
|
||||
'医药物流231',
|
||||
'药剂241',
|
||||
'药剂251',
|
||||
'药剂252',
|
||||
'药剂253',
|
||||
'药品营销251',
|
||||
'健康管理251',
|
||||
-- 西餐 / 中烹 / 中西点
|
||||
'西餐运营211',
|
||||
'烹饪221',
|
||||
'西餐运营222',
|
||||
'西餐运营241',
|
||||
'中烹241',
|
||||
'中西点241',
|
||||
'西餐运营221',
|
||||
'西餐运营231',
|
||||
'西餐运营232',
|
||||
'中烹231',
|
||||
'中西点231',
|
||||
'中西点232',
|
||||
'西餐运营242',
|
||||
'中烹242',
|
||||
'西餐运营251',
|
||||
'西餐运营252',
|
||||
'中烹251',
|
||||
'中烹252',
|
||||
'中西点251',
|
||||
'中西点252',
|
||||
-- 电商 / 数字媒体 / 人工智能
|
||||
'电子商务242',
|
||||
'数字媒体241',
|
||||
'电商231',
|
||||
'电商232',
|
||||
'电子商务241',
|
||||
'人工智能241',
|
||||
'数字媒体251',
|
||||
'人工智能251',
|
||||
'网络安全251',
|
||||
'电商251',
|
||||
'电商252'
|
||||
);
|
||||
|
||||
-- 2) 临安校区
|
||||
UPDATE xg_class_msg c
|
||||
INNER JOIN xg_dorm_campus camp
|
||||
ON camp.campus_name = '临安校区'
|
||||
AND camp.deleted = '0'
|
||||
SET
|
||||
c.campus_id = camp.id,
|
||||
c.campus_name = camp.campus_name,
|
||||
c.update_time = NOW()
|
||||
WHERE c.deleted = 0
|
||||
AND c.class_name IN (
|
||||
-- 数字制造 / 汽修 / 数控
|
||||
'数字制造211',
|
||||
'汽修221',
|
||||
'数控221',
|
||||
'数控241',
|
||||
'数字制造221',
|
||||
'数控231',
|
||||
'数字制造231',
|
||||
'汽修231',
|
||||
'汽修232',
|
||||
'数字制造241',
|
||||
'汽修241',
|
||||
'数字制造251',
|
||||
'智能制造251',
|
||||
'汽修251',
|
||||
'产品检测251',
|
||||
-- 中药
|
||||
'中药211',
|
||||
'中药221',
|
||||
'中药231',
|
||||
'中药232',
|
||||
'中药241',
|
||||
'中药251',
|
||||
'中药252',
|
||||
-- 园林 / 环境设计
|
||||
'园林211',
|
||||
'园林工程221',
|
||||
'园林设计221',
|
||||
'园林技术241',
|
||||
'园林设计241',
|
||||
'园林技术221',
|
||||
'园林工程231',
|
||||
'园林设计231',
|
||||
'园林技术231',
|
||||
'园林工程241',
|
||||
'园林技术251',
|
||||
'园林技术252',
|
||||
'园林工程251',
|
||||
'环境设计251'
|
||||
);
|
||||
|
||||
-- 核对更新结果(按校区汇总)
|
||||
SELECT campus_name, COUNT(*) AS cnt
|
||||
FROM xg_class_msg
|
||||
WHERE deleted = 0
|
||||
AND campus_name IN ('西溪校区', '临安校区')
|
||||
GROUP BY campus_name;
|
||||
|
||||
-- 确认无误后提交;有问题则 ROLLBACK
|
||||
COMMIT;
|
||||
-- ROLLBACK;
|
||||
@@ -0,0 +1,81 @@
|
||||
-- ============================================================
|
||||
-- 补齐未命中的 8 个西溪校区班级(名称变体)
|
||||
-- 原名单未命中:
|
||||
-- 无人机 261 / 无人机 251(多半库中无空格)
|
||||
-- 电商261/263/264/251/252(多半是「电子商务xxx」)
|
||||
-- 西餐运营211(需先确认实际名称)
|
||||
-- ============================================================
|
||||
|
||||
-- ---------- 1) 先查实际名称(务必先跑)----------
|
||||
SELECT id, class_name, campus_id, campus_name
|
||||
FROM xg_class_msg
|
||||
WHERE deleted = 0
|
||||
AND (
|
||||
class_name IN (
|
||||
'无人机261', '无人机 261',
|
||||
'无人机251', '无人机 251',
|
||||
'电商261', '电子商务261',
|
||||
'电商263', '电子商务263',
|
||||
'电商264', '电子商务264',
|
||||
'电商251', '电子商务251',
|
||||
'电商252', '电子商务252',
|
||||
'西餐运营211'
|
||||
)
|
||||
OR class_name LIKE '无人机%261%'
|
||||
OR class_name LIKE '无人机%251%'
|
||||
OR class_name LIKE '%电商%261%'
|
||||
OR class_name LIKE '%电商%263%'
|
||||
OR class_name LIKE '%电商%264%'
|
||||
OR class_name LIKE '%电商%251%'
|
||||
OR class_name LIKE '%电商%252%'
|
||||
OR class_name LIKE '西餐%211%'
|
||||
)
|
||||
ORDER BY class_name;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
-- ---------- 2) 按常见变体补设为西溪校区 ----------
|
||||
UPDATE xg_class_msg c
|
||||
INNER JOIN xg_dorm_campus camp
|
||||
ON camp.campus_name = '西溪校区'
|
||||
AND camp.deleted = '0'
|
||||
SET
|
||||
c.campus_id = camp.id,
|
||||
c.campus_name = camp.campus_name,
|
||||
c.update_time = NOW()
|
||||
WHERE c.deleted = 0
|
||||
AND (
|
||||
-- 去空格
|
||||
c.class_name IN ('无人机261', '无人机251')
|
||||
-- 电商 → 电子商务
|
||||
OR c.class_name IN (
|
||||
'电子商务261', '电子商务263', '电子商务264',
|
||||
'电子商务251', '电子商务252'
|
||||
)
|
||||
-- 若库中本来就是「电商xxx」但上次因其他原因未更新,再兜底一次
|
||||
OR c.class_name IN (
|
||||
'电商261', '电商263', '电商264', '电商251', '电商252',
|
||||
'无人机 261', '无人机 251', '西餐运营211'
|
||||
)
|
||||
);
|
||||
|
||||
-- 核对这批是否已挂上西溪
|
||||
SELECT id, class_name, campus_name
|
||||
FROM xg_class_msg
|
||||
WHERE deleted = 0
|
||||
AND class_name IN (
|
||||
'无人机261', '无人机 261', '无人机251', '无人机 251',
|
||||
'电商261', '电子商务261', '电商263', '电子商务263',
|
||||
'电商264', '电子商务264', '电商251', '电子商务251',
|
||||
'电商252', '电子商务252', '西餐运营211'
|
||||
);
|
||||
|
||||
-- 西溪应接近 99(若 8 个变体都找到);临安仍为 36
|
||||
SELECT campus_name, COUNT(*) AS cnt
|
||||
FROM xg_class_msg
|
||||
WHERE deleted = 0
|
||||
AND campus_name IN ('西溪校区', '临安校区')
|
||||
GROUP BY campus_name;
|
||||
|
||||
COMMIT;
|
||||
-- ROLLBACK;
|
||||
@@ -0,0 +1,194 @@
|
||||
-- =============================================================================
|
||||
-- 教务同步视图生产验证脚本
|
||||
-- 用途:部署 vw_jw_student_export / vw_jw_student_change_export 后执行
|
||||
--
|
||||
-- 用法:
|
||||
-- mysql -h <host> -u root -p hzdyjs_xg < docs/数据库脚本/2026-09-02-verify-jw-export-views.sql
|
||||
--
|
||||
-- 说明:
|
||||
-- * 脚本仅 SELECT,不修改数据
|
||||
-- * 若某段报错,说明对应视图或依赖表/字段异常
|
||||
-- * 「培养层次疑似中文」> 0 表示仍返回中文名称,需核对视图是否已更新
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SELECT '========== 1. 视图是否存在 ==========' AS section;
|
||||
|
||||
SELECT
|
||||
TABLE_NAME AS view_name,
|
||||
TABLE_TYPE AS object_type
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME IN ('vw_jw_student_export', 'vw_jw_student_change_export')
|
||||
ORDER BY TABLE_NAME;
|
||||
|
||||
SELECT '========== 2. 依赖:班级校区字段是否存在 ==========' AS section;
|
||||
|
||||
SELECT
|
||||
COLUMN_NAME,
|
||||
COLUMN_TYPE,
|
||||
COLUMN_COMMENT
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'xg_class_msg'
|
||||
AND COLUMN_NAME IN ('campus_id', 'campus_name')
|
||||
ORDER BY ORDINAL_POSITION;
|
||||
|
||||
SELECT '========== 3. 基础数据量对比 ==========' AS section;
|
||||
|
||||
SELECT 'xg_student_msg(未删除)' AS metric, COUNT(*) AS cnt
|
||||
FROM xg_student_msg WHERE deleted = 0
|
||||
UNION ALL
|
||||
SELECT 'vw_jw_student_export', COUNT(*) FROM vw_jw_student_export
|
||||
UNION ALL
|
||||
SELECT 'vw_jw_student_change_export(全部)', COUNT(*) FROM vw_jw_student_change_export
|
||||
UNION ALL
|
||||
SELECT CONCAT('vw_jw_student_change_export(', YEAR(CURDATE()), '年)'),
|
||||
COUNT(*)
|
||||
FROM vw_jw_student_change_export
|
||||
WHERE YEAR(`异动生效时间`) = YEAR(CURDATE());
|
||||
|
||||
SELECT '========== 4. vw_jw_student_export 关键字段填充率 ==========' AS section;
|
||||
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
SUM(`学号` IS NOT NULL AND TRIM(`学号`) <> '') AS has_student_no,
|
||||
SUM(`班级信息` IS NOT NULL AND TRIM(`班级信息`) <> '') AS has_class,
|
||||
SUM(`所在校区` IS NOT NULL AND TRIM(`所在校区`) <> '') AS has_campus,
|
||||
SUM(`系部信息` IS NOT NULL AND TRIM(`系部信息`) <> '') AS has_college,
|
||||
SUM(`专业信息` IS NOT NULL AND TRIM(`专业信息`) <> '') AS has_major,
|
||||
SUM(`培养层次` IS NOT NULL AND TRIM(`培养层次`) <> '') AS has_pycc,
|
||||
SUM(`培养层次` REGEXP '^[0-5]$') AS pycc_dict_value_ok,
|
||||
SUM(
|
||||
`培养层次` IS NOT NULL
|
||||
AND TRIM(`培养层次`) <> ''
|
||||
AND `培养层次` NOT REGEXP '^[0-5]$'
|
||||
) AS pycc_maybe_chinese_name
|
||||
FROM vw_jw_student_export;
|
||||
|
||||
SELECT '========== 5. vw_jw_student_export 培养层次分布(应为 0~5 字典值)==========' AS section;
|
||||
|
||||
SELECT
|
||||
COALESCE(`培养层次`, '(null)') AS pycc_value,
|
||||
COUNT(*) AS cnt
|
||||
FROM vw_jw_student_export
|
||||
GROUP BY `培养层次`
|
||||
ORDER BY cnt DESC
|
||||
LIMIT 20;
|
||||
|
||||
SELECT '========== 6. vw_jw_student_export 所在校区分布(Top 10)==========' AS section;
|
||||
|
||||
SELECT
|
||||
COALESCE(`所在校区`, '(null)') AS campus,
|
||||
COUNT(*) AS cnt
|
||||
FROM vw_jw_student_export
|
||||
GROUP BY `所在校区`
|
||||
ORDER BY cnt DESC
|
||||
LIMIT 10;
|
||||
|
||||
SELECT '========== 7. vw_jw_student_export 样例(3 条,含组织信息)==========' AS section;
|
||||
|
||||
SELECT
|
||||
`学号`, `姓名`, `班级信息`, `所在校区`, `系部信息`, `专业信息`, `培养层次`, `学制`, `学生当前状态`
|
||||
FROM vw_jw_student_export
|
||||
ORDER BY `学号`
|
||||
LIMIT 3;
|
||||
|
||||
SELECT '========== 8. vw_jw_student_change_export 关键字段填充率 ==========' AS section;
|
||||
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
SUM(`异动类型` IS NOT NULL AND TRIM(`异动类型`) <> '') AS has_change_type,
|
||||
SUM(`异动前所在院系` IS NOT NULL AND TRIM(`异动前所在院系`) <> '') AS has_before_college,
|
||||
SUM(`异动前所在专业` IS NOT NULL AND TRIM(`异动前所在专业`) <> '') AS has_before_major,
|
||||
SUM(`异动前所在班级` IS NOT NULL AND TRIM(`异动前所在班级`) <> '') AS has_before_class,
|
||||
SUM(`异动前所在校区` IS NOT NULL AND TRIM(`异动前所在校区`) <> '') AS has_before_campus,
|
||||
SUM(`异动后所在院系` IS NOT NULL AND TRIM(`异动后所在院系`) <> '') AS has_after_college,
|
||||
SUM(`异动后所在专业` IS NOT NULL AND TRIM(`异动后所在专业`) <> '') AS has_after_major,
|
||||
SUM(`异动后所在班级` IS NOT NULL AND TRIM(`异动后所在班级`) <> '') AS has_after_class,
|
||||
SUM(`异动后所在校区` IS NOT NULL AND TRIM(`异动后所在校区`) <> '') AS has_after_campus
|
||||
FROM vw_jw_student_change_export;
|
||||
|
||||
SELECT '========== 9. vw_jw_student_change_export 异动类型分布 ==========' AS section;
|
||||
|
||||
SELECT
|
||||
`异动类型`,
|
||||
COUNT(*) AS cnt
|
||||
FROM vw_jw_student_change_export
|
||||
GROUP BY `异动类型`
|
||||
ORDER BY cnt DESC;
|
||||
|
||||
SELECT '========== 10. vw_jw_student_change_export 当年记录数(按年)==========' AS section;
|
||||
|
||||
SELECT
|
||||
YEAR(`异动生效时间`) AS effect_year,
|
||||
COUNT(*) AS cnt
|
||||
FROM vw_jw_student_change_export
|
||||
GROUP BY YEAR(`异动生效时间`)
|
||||
ORDER BY effect_year DESC
|
||||
LIMIT 10;
|
||||
|
||||
SELECT '========== 11. vw_jw_student_change_export 样例(3 条)==========' AS section;
|
||||
|
||||
SELECT
|
||||
`提交时间`, `异动生效时间`, `异动类型`, `申请人学号`, `姓名`,
|
||||
`异动前所在校区`, `异动前所在院系`, `异动前所在专业`, `异动前所在班级`,
|
||||
`异动后所在校区`, `异动后所在院系`, `异动后所在专业`, `异动后所在班级`
|
||||
FROM vw_jw_student_change_export
|
||||
ORDER BY `异动生效时间` DESC
|
||||
LIMIT 3;
|
||||
|
||||
SELECT '========== 12. jw_cx 账号视图权限(可选)==========' AS section;
|
||||
|
||||
SELECT
|
||||
GRANTEE,
|
||||
TABLE_NAME,
|
||||
PRIVILEGE_TYPE
|
||||
FROM information_schema.TABLE_PRIVILEGES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME IN ('vw_jw_student_export', 'vw_jw_student_change_export')
|
||||
AND GRANTEE LIKE '%jw_cx%'
|
||||
ORDER BY TABLE_NAME, PRIVILEGE_TYPE;
|
||||
|
||||
SELECT '========== 13. 快速结论(人工查看)==========' AS section;
|
||||
|
||||
SELECT
|
||||
CASE
|
||||
WHEN (
|
||||
SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME IN ('vw_jw_student_export', 'vw_jw_student_change_export')
|
||||
) = 2 THEN 'PASS'
|
||||
ELSE 'FAIL'
|
||||
END AS views_exist,
|
||||
CASE
|
||||
WHEN (
|
||||
SELECT COUNT(*) FROM vw_jw_student_export LIMIT 1
|
||||
) >= 0 THEN 'PASS'
|
||||
ELSE 'FAIL'
|
||||
END AS student_export_queryable,
|
||||
CASE
|
||||
WHEN (
|
||||
SELECT COUNT(*) FROM vw_jw_student_change_export LIMIT 1
|
||||
) >= 0 THEN 'PASS'
|
||||
ELSE 'FAIL'
|
||||
END AS change_export_queryable,
|
||||
CASE
|
||||
WHEN (
|
||||
SELECT COUNT(*) FROM vw_jw_student_export
|
||||
WHERE `培养层次` IS NOT NULL
|
||||
AND TRIM(`培养层次`) <> ''
|
||||
AND `培养层次` NOT REGEXP '^[0-5]$'
|
||||
) = 0 THEN 'PASS'
|
||||
ELSE 'WARN'
|
||||
END AS pycc_is_dict_value,
|
||||
CASE
|
||||
WHEN (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'xg_class_msg'
|
||||
AND COLUMN_NAME = 'campus_id'
|
||||
) = 1 THEN 'PASS'
|
||||
ELSE 'FAIL'
|
||||
END AS class_campus_column_exists;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 学生异动:院系/专业改从 sys_dept 获取,校区改从班级 campus_id 获取
|
||||
-- 与学生基本信息接口 vw_jw_student_export 口径对齐
|
||||
-- 执行后无需重启 API,视图查询即时生效
|
||||
--
|
||||
-- 在项目根目录执行:
|
||||
-- mysql -h <host> -u root -p hzdyjs_xg < docs/设计文档/database/vw_jw_student_change_export.sql
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 学生所在校区改为从班级信息获取(xg_class_msg.campus_id / campus_name)
|
||||
-- 不再通过宿舍分配(xg_dorm_bed_student)推断校区
|
||||
-- 执行后无需重启 API,视图查询即时生效
|
||||
--
|
||||
-- 在项目根目录执行:
|
||||
-- mysql -h <host> -u root -p hzdyjs_xg < docs/设计文档/database/vw_jw_student_export.sql
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 系部/专业名称改为优先通过 college_id、major_id 关联 sys_dept.dept_name 获取
|
||||
-- 班级名称仍通过 class_id 关联 xg_class_msg.class_name
|
||||
-- 执行后无需重启 API,视图查询即时生效
|
||||
--
|
||||
-- 在项目根目录执行:
|
||||
-- mysql -h <host> -u root -p hzdyjs_xg < docs/设计文档/database/vw_jw_student_export.sql
|
||||
@@ -0,0 +1,5 @@
|
||||
-- 培养层次改为返回字典值(class_type),不再输出中文名称
|
||||
-- 执行后无需重启 API,视图查询即时生效
|
||||
--
|
||||
-- 在项目根目录执行:
|
||||
-- mysql -h <host> -u root -p hzdyjs_xg < docs/设计文档/database/vw_jw_student_export.sql
|
||||
@@ -0,0 +1,224 @@
|
||||
-- =============================================================================
|
||||
-- 刷新 xg_student_msg 冗余字段:班级名称、专业名称
|
||||
--
|
||||
-- 背景:老师修改班级/专业后,xg_class_msg、sys_dept 已更新,
|
||||
-- 但 xg_student_msg 上冗余的 class_name、major_name 仍为旧值。
|
||||
--
|
||||
-- 刷新口径(与 vw_jw_student_export 一致):
|
||||
-- * class_name ← 关联班级 xg_class_msg.class_name(按 class_id)
|
||||
-- * major_name ← 优先学生 major_id → sys_dept.dept_name
|
||||
-- 其次班级 major_id → sys_dept.dept_name
|
||||
-- 再回退 xg_class_msg.major_name
|
||||
--
|
||||
-- 用法:
|
||||
-- mysql -h <host> -u root -p hzdyjs_xg < docs/数据库脚本/2026-09-02-xg_student_msg-refresh-class-major-name.sql
|
||||
--
|
||||
-- 建议:先执行「一、预览」确认影响范围,再执行「二、更新」
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 一、预览:即将刷新的记录数
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
SELECT '========== 1. 班级名称不一致(将刷新)==========' AS section;
|
||||
|
||||
SELECT COUNT(*) AS need_refresh_class_name
|
||||
FROM xg_student_msg m
|
||||
INNER JOIN xg_class_msg cm
|
||||
ON cm.deleted = 0
|
||||
AND cm.tenant_id = m.tenant_id
|
||||
AND cm.id = CAST(NULLIF(TRIM(m.class_id), '') AS UNSIGNED)
|
||||
WHERE m.deleted = 0
|
||||
AND NULLIF(TRIM(m.class_id), '') IS NOT NULL
|
||||
AND (
|
||||
m.class_name IS NULL
|
||||
OR TRIM(m.class_name) = ''
|
||||
OR m.class_name <> cm.class_name
|
||||
);
|
||||
|
||||
SELECT '========== 2. 专业名称不一致(将刷新)==========' AS section;
|
||||
|
||||
SELECT COUNT(*) AS need_refresh_major_name
|
||||
FROM xg_student_msg m
|
||||
LEFT JOIN xg_class_msg cm
|
||||
ON cm.deleted = 0
|
||||
AND cm.tenant_id = m.tenant_id
|
||||
AND cm.id = CAST(NULLIF(TRIM(m.class_id), '') AS UNSIGNED)
|
||||
LEFT JOIN sys_dept dm_m
|
||||
ON dm_m.dept_id = CAST(NULLIF(TRIM(m.major_id), '') AS UNSIGNED)
|
||||
AND dm_m.del_flag = '0'
|
||||
AND dm_m.tenant_id = m.tenant_id
|
||||
LEFT JOIN sys_dept dm_cm
|
||||
ON dm_cm.dept_id = cm.major_id
|
||||
AND dm_cm.del_flag = '0'
|
||||
AND dm_cm.tenant_id = m.tenant_id
|
||||
WHERE m.deleted = 0
|
||||
AND COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name) IS NOT NULL
|
||||
AND TRIM(COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name)) <> ''
|
||||
AND (
|
||||
m.major_name IS NULL
|
||||
OR TRIM(m.major_name) = ''
|
||||
OR m.major_name <> COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name)
|
||||
);
|
||||
|
||||
SELECT '========== 3. 不一致样例(班级名称,最多 20 条)==========' AS section;
|
||||
|
||||
SELECT
|
||||
m.id,
|
||||
m.student_no AS 学号,
|
||||
m.name AS 姓名,
|
||||
m.class_id,
|
||||
m.class_name AS 当前班级名称,
|
||||
cm.class_name AS 新班级名称
|
||||
FROM xg_student_msg m
|
||||
INNER JOIN xg_class_msg cm
|
||||
ON cm.deleted = 0
|
||||
AND cm.tenant_id = m.tenant_id
|
||||
AND cm.id = CAST(NULLIF(TRIM(m.class_id), '') AS UNSIGNED)
|
||||
WHERE m.deleted = 0
|
||||
AND NULLIF(TRIM(m.class_id), '') IS NOT NULL
|
||||
AND (
|
||||
m.class_name IS NULL
|
||||
OR TRIM(m.class_name) = ''
|
||||
OR m.class_name <> cm.class_name
|
||||
)
|
||||
ORDER BY m.student_no
|
||||
LIMIT 20;
|
||||
|
||||
SELECT '========== 4. 不一致样例(专业名称,最多 20 条)==========' AS section;
|
||||
|
||||
SELECT
|
||||
m.id,
|
||||
m.student_no AS 学号,
|
||||
m.name AS 姓名,
|
||||
m.major_id,
|
||||
m.major_name AS 当前专业名称,
|
||||
COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name) AS 新专业名称
|
||||
FROM xg_student_msg m
|
||||
LEFT JOIN xg_class_msg cm
|
||||
ON cm.deleted = 0
|
||||
AND cm.tenant_id = m.tenant_id
|
||||
AND cm.id = CAST(NULLIF(TRIM(m.class_id), '') AS UNSIGNED)
|
||||
LEFT JOIN sys_dept dm_m
|
||||
ON dm_m.dept_id = CAST(NULLIF(TRIM(m.major_id), '') AS UNSIGNED)
|
||||
AND dm_m.del_flag = '0'
|
||||
AND dm_m.tenant_id = m.tenant_id
|
||||
LEFT JOIN sys_dept dm_cm
|
||||
ON dm_cm.dept_id = cm.major_id
|
||||
AND dm_cm.del_flag = '0'
|
||||
AND dm_cm.tenant_id = m.tenant_id
|
||||
WHERE m.deleted = 0
|
||||
AND COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name) IS NOT NULL
|
||||
AND TRIM(COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name)) <> ''
|
||||
AND (
|
||||
m.major_name IS NULL
|
||||
OR TRIM(m.major_name) = ''
|
||||
OR m.major_name <> COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name)
|
||||
)
|
||||
ORDER BY m.student_no
|
||||
LIMIT 20;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 二、执行更新(确认预览无误后再执行)
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
-- 2.1 刷新班级名称
|
||||
UPDATE xg_student_msg m
|
||||
INNER JOIN xg_class_msg cm
|
||||
ON cm.deleted = 0
|
||||
AND cm.tenant_id = m.tenant_id
|
||||
AND cm.id = CAST(NULLIF(TRIM(m.class_id), '') AS UNSIGNED)
|
||||
SET
|
||||
m.class_name = cm.class_name,
|
||||
m.update_time = NOW()
|
||||
WHERE m.deleted = 0
|
||||
AND NULLIF(TRIM(m.class_id), '') IS NOT NULL
|
||||
AND (
|
||||
m.class_name IS NULL
|
||||
OR TRIM(m.class_name) = ''
|
||||
OR m.class_name <> cm.class_name
|
||||
);
|
||||
|
||||
SELECT ROW_COUNT() AS updated_class_name_rows;
|
||||
|
||||
-- 2.2 刷新专业名称
|
||||
UPDATE xg_student_msg m
|
||||
LEFT JOIN xg_class_msg cm
|
||||
ON cm.deleted = 0
|
||||
AND cm.tenant_id = m.tenant_id
|
||||
AND cm.id = CAST(NULLIF(TRIM(m.class_id), '') AS UNSIGNED)
|
||||
LEFT JOIN sys_dept dm_m
|
||||
ON dm_m.dept_id = CAST(NULLIF(TRIM(m.major_id), '') AS UNSIGNED)
|
||||
AND dm_m.del_flag = '0'
|
||||
AND dm_m.tenant_id = m.tenant_id
|
||||
LEFT JOIN sys_dept dm_cm
|
||||
ON dm_cm.dept_id = cm.major_id
|
||||
AND dm_cm.del_flag = '0'
|
||||
AND dm_cm.tenant_id = m.tenant_id
|
||||
SET
|
||||
m.major_name = COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name),
|
||||
m.update_time = NOW()
|
||||
WHERE m.deleted = 0
|
||||
AND COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name) IS NOT NULL
|
||||
AND TRIM(COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name)) <> ''
|
||||
AND (
|
||||
m.major_name IS NULL
|
||||
OR TRIM(m.major_name) = ''
|
||||
OR m.major_name <> COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name)
|
||||
);
|
||||
|
||||
SELECT ROW_COUNT() AS updated_major_name_rows;
|
||||
|
||||
-- 确认无误后提交;若要撤销请执行 ROLLBACK;
|
||||
COMMIT;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 三、更新后复核
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
SELECT '========== 5. 更新后仍不一致(应为 0 或接近 0)==========' AS section;
|
||||
|
||||
SELECT
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM xg_student_msg m
|
||||
INNER JOIN xg_class_msg cm
|
||||
ON cm.deleted = 0
|
||||
AND cm.tenant_id = m.tenant_id
|
||||
AND cm.id = CAST(NULLIF(TRIM(m.class_id), '') AS UNSIGNED)
|
||||
WHERE m.deleted = 0
|
||||
AND NULLIF(TRIM(m.class_id), '') IS NOT NULL
|
||||
AND (
|
||||
m.class_name IS NULL
|
||||
OR TRIM(m.class_name) = ''
|
||||
OR m.class_name <> cm.class_name
|
||||
)
|
||||
) AS remain_class_name_mismatch,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM xg_student_msg m
|
||||
LEFT JOIN xg_class_msg cm
|
||||
ON cm.deleted = 0
|
||||
AND cm.tenant_id = m.tenant_id
|
||||
AND cm.id = CAST(NULLIF(TRIM(m.class_id), '') AS UNSIGNED)
|
||||
LEFT JOIN sys_dept dm_m
|
||||
ON dm_m.dept_id = CAST(NULLIF(TRIM(m.major_id), '') AS UNSIGNED)
|
||||
AND dm_m.del_flag = '0'
|
||||
AND dm_m.tenant_id = m.tenant_id
|
||||
LEFT JOIN sys_dept dm_cm
|
||||
ON dm_cm.dept_id = cm.major_id
|
||||
AND dm_cm.del_flag = '0'
|
||||
AND dm_cm.tenant_id = m.tenant_id
|
||||
WHERE m.deleted = 0
|
||||
AND COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name) IS NOT NULL
|
||||
AND TRIM(COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name)) <> ''
|
||||
AND (
|
||||
m.major_name IS NULL
|
||||
OR TRIM(m.major_name) = ''
|
||||
OR m.major_name <> COALESCE(dm_m.dept_name, dm_cm.dept_name, cm.major_name)
|
||||
)
|
||||
) AS remain_major_name_mismatch;
|
||||
@@ -3,7 +3,9 @@
|
||||
-- 说明见:学生学籍异动同步教务系统改造方案.md
|
||||
--
|
||||
-- 约定:
|
||||
-- * 异动前/后「校区」列为 NULL(本系统无校区字段)
|
||||
-- * 异动前/后「校区」:班级 campus_id → xg_dorm_campus.campus_name,再回退班级 campus_name
|
||||
-- * 异动前/后「院系/专业」:优先 college_id、major_id → sys_dept.dept_name,再回退冗余名称
|
||||
-- * 异动前/后「班级」:优先 xg_class_msg.class_name,再回退冗余名称
|
||||
-- * 「异动生效学期」列保留,值为 NULL(教务可接受空值)
|
||||
-- * 「异动生效时间」:学籍异动 = effective_date;换班级 = effective_time(API 按年筛选)
|
||||
-- * 「异动类型」:输出教务系统中文名称(见 §异动类型映射)
|
||||
@@ -65,16 +67,16 @@ SELECT
|
||||
(COALESCE(sc.student_no COLLATE utf8mb4_unicode_ci, m.student_no COLLATE utf8mb4_unicode_ci) COLLATE utf8mb4_unicode_ci) AS `申请人学号`,
|
||||
(COALESCE(mh.`name`, m.`name`) COLLATE utf8mb4_unicode_ci) AS `姓名`,
|
||||
(COALESCE(mh.grade, m.grade, cm_before.grade) COLLATE utf8mb4_unicode_ci) AS `异动前所在年级`,
|
||||
(CAST(NULL AS CHAR(64) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `异动前所在校区`,
|
||||
(COALESCE(mh.college_name, m.college_name, cm_before.college_name) COLLATE utf8mb4_unicode_ci) AS `异动前所在院系`,
|
||||
(COALESCE(mh.major_name, m.major_name, cm_before.major_name) COLLATE utf8mb4_unicode_ci) AS `异动前所在专业`,
|
||||
(COALESCE(mh.class_name, m.class_name, cm_before.class_name) COLLATE utf8mb4_unicode_ci) AS `异动前所在班级`,
|
||||
(COALESCE(campus_before.campus_name, cm_before.campus_name) COLLATE utf8mb4_unicode_ci) AS `异动前所在校区`,
|
||||
(COALESCE(dc_mh.dept_name, dc_cm_before.dept_name, mh.college_name, cm_before.college_name, m.college_name) COLLATE utf8mb4_unicode_ci) AS `异动前所在院系`,
|
||||
(COALESCE(dm_mh.dept_name, dm_cm_before.dept_name, mh.major_name, cm_before.major_name, m.major_name) COLLATE utf8mb4_unicode_ci) AS `异动前所在专业`,
|
||||
(COALESCE(cm_before.class_name, mh.class_name, m.class_name) COLLATE utf8mb4_unicode_ci) AS `异动前所在班级`,
|
||||
COALESCE(cm_before.school_system, mh.school_system, m.school_system) AS `异动前学制`,
|
||||
(CAST(NULL AS CHAR(64) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `异动后所在校区`,
|
||||
(COALESCE(campus_after.campus_name, cm_after.campus_name) COLLATE utf8mb4_unicode_ci) AS `异动后所在校区`,
|
||||
(COALESCE(m.grade, cm_after.grade, mh.grade) COLLATE utf8mb4_unicode_ci) AS `异动后所在年级`,
|
||||
(COALESCE(m.class_name, cm_after.class_name, mh.class_name) COLLATE utf8mb4_unicode_ci) AS `异动后所在班级`,
|
||||
(COALESCE(m.college_name, cm_after.college_name, mh.college_name) COLLATE utf8mb4_unicode_ci) AS `异动后所在院系`,
|
||||
(COALESCE(m.major_name, cm_after.major_name, mh.major_name) COLLATE utf8mb4_unicode_ci) AS `异动后所在专业`,
|
||||
(COALESCE(cm_after.class_name, m.class_name, mh.class_name) COLLATE utf8mb4_unicode_ci) AS `异动后所在班级`,
|
||||
(COALESCE(dc_m.dept_name, dc_cm_after.dept_name, m.college_name, cm_after.college_name, mh.college_name) COLLATE utf8mb4_unicode_ci) AS `异动后所在院系`,
|
||||
(COALESCE(dm_m.dept_name, dm_cm_after.dept_name, m.major_name, cm_after.major_name, mh.major_name) COLLATE utf8mb4_unicode_ci) AS `异动后所在专业`,
|
||||
COALESCE(cm_after.school_system, m.school_system, cm_before.school_system, mh.school_system) AS `异动后学制`,
|
||||
(COALESCE(
|
||||
NULLIF(TRIM(su.user_no COLLATE utf8mb4_unicode_ci), '' COLLATE utf8mb4_unicode_ci),
|
||||
@@ -124,6 +126,44 @@ LEFT JOIN xg_class_msg cm_after
|
||||
ON cm_after.deleted = 0
|
||||
AND (cm_after.tenant_id COLLATE utf8mb4_unicode_ci) = (sc.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
AND cm_after.id = CAST(NULLIF(TRIM(m.class_id COLLATE utf8mb4_unicode_ci), '' COLLATE utf8mb4_unicode_ci) AS UNSIGNED)
|
||||
LEFT JOIN sys_dept dc_mh
|
||||
ON dc_mh.dept_id = CAST(NULLIF(TRIM(mh.college_id), '') AS UNSIGNED)
|
||||
AND (dc_mh.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dc_mh.tenant_id COLLATE utf8mb4_unicode_ci) = (sc.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dm_mh
|
||||
ON dm_mh.dept_id = CAST(NULLIF(TRIM(mh.major_id), '') AS UNSIGNED)
|
||||
AND (dm_mh.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dm_mh.tenant_id COLLATE utf8mb4_unicode_ci) = (sc.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dc_m
|
||||
ON dc_m.dept_id = CAST(NULLIF(TRIM(m.college_id), '') AS UNSIGNED)
|
||||
AND (dc_m.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dc_m.tenant_id COLLATE utf8mb4_unicode_ci) = (sc.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dm_m
|
||||
ON dm_m.dept_id = CAST(NULLIF(TRIM(m.major_id), '') AS UNSIGNED)
|
||||
AND (dm_m.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dm_m.tenant_id COLLATE utf8mb4_unicode_ci) = (sc.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dc_cm_before
|
||||
ON dc_cm_before.dept_id = cm_before.college_id
|
||||
AND (dc_cm_before.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dc_cm_before.tenant_id COLLATE utf8mb4_unicode_ci) = (sc.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dm_cm_before
|
||||
ON dm_cm_before.dept_id = cm_before.major_id
|
||||
AND (dm_cm_before.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dm_cm_before.tenant_id COLLATE utf8mb4_unicode_ci) = (sc.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dc_cm_after
|
||||
ON dc_cm_after.dept_id = cm_after.college_id
|
||||
AND (dc_cm_after.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dc_cm_after.tenant_id COLLATE utf8mb4_unicode_ci) = (sc.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dm_cm_after
|
||||
ON dm_cm_after.dept_id = cm_after.major_id
|
||||
AND (dm_cm_after.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dm_cm_after.tenant_id COLLATE utf8mb4_unicode_ci) = (sc.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN xg_dorm_campus campus_before
|
||||
ON campus_before.id = cm_before.campus_id
|
||||
AND (campus_before.deleted COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN xg_dorm_campus campus_after
|
||||
ON campus_after.id = cm_after.campus_id
|
||||
AND (campus_after.deleted COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_user su
|
||||
ON su.user_id = COALESCE(m.class_teacher_id, cm_after.class_teacher_id)
|
||||
AND (su.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
@@ -170,16 +210,16 @@ SELECT
|
||||
(c.student_no COLLATE utf8mb4_unicode_ci) AS `申请人学号`,
|
||||
(c.student_name COLLATE utf8mb4_unicode_ci) AS `姓名`,
|
||||
(oc.grade COLLATE utf8mb4_unicode_ci) AS `异动前所在年级`,
|
||||
(CAST(NULL AS CHAR(64) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `异动前所在校区`,
|
||||
(c.original_college_name COLLATE utf8mb4_unicode_ci) AS `异动前所在院系`,
|
||||
(c.original_major_name COLLATE utf8mb4_unicode_ci) AS `异动前所在专业`,
|
||||
(c.original_class_name COLLATE utf8mb4_unicode_ci) AS `异动前所在班级`,
|
||||
(COALESCE(campus_oc.campus_name, oc.campus_name) COLLATE utf8mb4_unicode_ci) AS `异动前所在校区`,
|
||||
(COALESCE(dc_oc.dept_name, dc_c_orig.dept_name, oc.college_name, c.original_college_name) COLLATE utf8mb4_unicode_ci) AS `异动前所在院系`,
|
||||
(COALESCE(dm_oc.dept_name, dm_c_orig.dept_name, oc.major_name, c.original_major_name) COLLATE utf8mb4_unicode_ci) AS `异动前所在专业`,
|
||||
(COALESCE(oc.class_name, c.original_class_name) COLLATE utf8mb4_unicode_ci) AS `异动前所在班级`,
|
||||
oc.school_system AS `异动前学制`,
|
||||
(CAST(NULL AS CHAR(64) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `异动后所在校区`,
|
||||
(COALESCE(campus_nc.campus_name, nc.campus_name) COLLATE utf8mb4_unicode_ci) AS `异动后所在校区`,
|
||||
(nc.grade COLLATE utf8mb4_unicode_ci) AS `异动后所在年级`,
|
||||
(c.new_class_name COLLATE utf8mb4_unicode_ci) AS `异动后所在班级`,
|
||||
(c.new_college_name COLLATE utf8mb4_unicode_ci) AS `异动后所在院系`,
|
||||
(c.new_major_name COLLATE utf8mb4_unicode_ci) AS `异动后所在专业`,
|
||||
(COALESCE(nc.class_name, c.new_class_name) COLLATE utf8mb4_unicode_ci) AS `异动后所在班级`,
|
||||
(COALESCE(dc_nc.dept_name, dc_c_new.dept_name, nc.college_name, c.new_college_name) COLLATE utf8mb4_unicode_ci) AS `异动后所在院系`,
|
||||
(COALESCE(dm_nc.dept_name, dm_c_new.dept_name, nc.major_name, c.new_major_name) COLLATE utf8mb4_unicode_ci) AS `异动后所在专业`,
|
||||
nc.school_system AS `异动后学制`,
|
||||
(COALESCE(
|
||||
NULLIF(TRIM(su2.user_no COLLATE utf8mb4_unicode_ci), '' COLLATE utf8mb4_unicode_ci),
|
||||
@@ -212,6 +252,44 @@ LEFT JOIN xg_class_msg nc
|
||||
ON nc.id = c.new_class_id
|
||||
AND nc.deleted = 0
|
||||
AND (nc.tenant_id COLLATE utf8mb4_unicode_ci) = (c.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dc_oc
|
||||
ON dc_oc.dept_id = oc.college_id
|
||||
AND (dc_oc.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dc_oc.tenant_id COLLATE utf8mb4_unicode_ci) = (c.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dm_oc
|
||||
ON dm_oc.dept_id = oc.major_id
|
||||
AND (dm_oc.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dm_oc.tenant_id COLLATE utf8mb4_unicode_ci) = (c.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dc_nc
|
||||
ON dc_nc.dept_id = nc.college_id
|
||||
AND (dc_nc.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dc_nc.tenant_id COLLATE utf8mb4_unicode_ci) = (c.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dm_nc
|
||||
ON dm_nc.dept_id = nc.major_id
|
||||
AND (dm_nc.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dm_nc.tenant_id COLLATE utf8mb4_unicode_ci) = (c.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dc_c_orig
|
||||
ON dc_c_orig.dept_id = c.original_college_id
|
||||
AND (dc_c_orig.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dc_c_orig.tenant_id COLLATE utf8mb4_unicode_ci) = (c.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dm_c_orig
|
||||
ON dm_c_orig.dept_id = c.original_major_id
|
||||
AND (dm_c_orig.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dm_c_orig.tenant_id COLLATE utf8mb4_unicode_ci) = (c.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dc_c_new
|
||||
ON dc_c_new.dept_id = c.new_college_id
|
||||
AND (dc_c_new.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dc_c_new.tenant_id COLLATE utf8mb4_unicode_ci) = (c.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dm_c_new
|
||||
ON dm_c_new.dept_id = c.new_major_id
|
||||
AND (dm_c_new.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dm_c_new.tenant_id COLLATE utf8mb4_unicode_ci) = (c.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN xg_dorm_campus campus_oc
|
||||
ON campus_oc.id = oc.campus_id
|
||||
AND (campus_oc.deleted COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN xg_dorm_campus campus_nc
|
||||
ON campus_nc.id = nc.campus_id
|
||||
AND (campus_nc.deleted COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_user su2
|
||||
ON su2.user_id = nc.class_teacher_id
|
||||
AND (su2.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
--
|
||||
-- 口径摘要:
|
||||
-- * 身份证件类型:有 id_card 则「居民身份证」,否则 NULL
|
||||
-- * 所在校区:有有效宿舍分配则取 xg_dorm_campus.campus_name,否则 NULL
|
||||
-- * 所在校区:学生 class_id → xg_class_msg.campus_id → xg_dorm_campus.campus_name,再回退班级冗余 campus_name
|
||||
-- * 班主任工号:class_teacher_id → sys_user.user_no(空则 user_name)
|
||||
-- * 学生是否在校:默认「在校」;退学(4)、毕业(5) 为「不在校」
|
||||
-- * 异动类型:固定 NULL
|
||||
@@ -13,6 +13,8 @@
|
||||
-- * 性别:0→男,1→女
|
||||
-- * 成员1/2:父亲/母亲及关系、电话
|
||||
-- * 学生当前状态:输出教务中文名称(见对接说明 §状态映射)
|
||||
-- * 培养层次:班级 class_type 字典值(zsjy_education_level),非中文名称
|
||||
-- * 系部/专业:优先 college_id、major_id 关联 sys_dept.dept_name;班级名称取 xg_class_msg.class_name
|
||||
-- =============================================================================
|
||||
|
||||
DROP VIEW IF EXISTS `vw_jw_student_export`;
|
||||
@@ -28,12 +30,12 @@ SELECT
|
||||
ELSE (CAST(NULL AS CHAR(32) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci)
|
||||
END) AS `身份证件类型`,
|
||||
(m.id_card COLLATE utf8mb4_unicode_ci) AS `身份证件号`,
|
||||
(COALESCE(m.class_name, cm.class_name) COLLATE utf8mb4_unicode_ci) AS `班级信息`,
|
||||
(dorm.campus_name COLLATE utf8mb4_unicode_ci) AS `所在校区`,
|
||||
(COALESCE(cm.class_name, m.class_name) COLLATE utf8mb4_unicode_ci) AS `班级信息`,
|
||||
(COALESCE(campus_cm.campus_name, cm.campus_name) COLLATE utf8mb4_unicode_ci) AS `所在校区`,
|
||||
(COALESCE(m.grade, cm.grade) COLLATE utf8mb4_unicode_ci) AS `所在年级`,
|
||||
m.enrollment_year AS `入学年份`,
|
||||
(COALESCE(m.college_name, cm.college_name) COLLATE utf8mb4_unicode_ci) AS `系部信息`,
|
||||
(COALESCE(m.major_name, cm.major_name) COLLATE utf8mb4_unicode_ci) AS `专业信息`,
|
||||
(COALESCE(dc_m.dept_name, dc_cm.dept_name, m.college_name, cm.college_name) COLLATE utf8mb4_unicode_ci) AS `系部信息`,
|
||||
(COALESCE(dm_m.dept_name, dm_cm.dept_name, m.major_name, cm.major_name) COLLATE utf8mb4_unicode_ci) AS `专业信息`,
|
||||
(COALESCE(NULLIF(TRIM(su.user_no), ''), su.user_name) COLLATE utf8mb4_unicode_ci) AS `班主任工号`,
|
||||
(COALESCE(m.class_teacher_name, cm.class_teacher_name) COLLATE utf8mb4_unicode_ci) AS `班主任姓名`,
|
||||
(CASE m.student_status
|
||||
@@ -56,15 +58,7 @@ SELECT
|
||||
ELSE ('在校' COLLATE utf8mb4_unicode_ci)
|
||||
END) AS `学生是否在校`,
|
||||
(CAST(NULL AS CHAR(32) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `异动类型`,
|
||||
(CASE cm.class_type
|
||||
WHEN '0' THEN '三年制中级技工'
|
||||
WHEN '1' THEN '三年制技师'
|
||||
WHEN '2' THEN '四年制高级工'
|
||||
WHEN '3' THEN '五年制高级技工'
|
||||
WHEN '4' THEN '六年制技师'
|
||||
WHEN '5' THEN '中高职一体'
|
||||
ELSE cm.class_type
|
||||
END COLLATE utf8mb4_unicode_ci) AS `培养层次`,
|
||||
(cm.class_type COLLATE utf8mb4_unicode_ci) AS `培养层次`,
|
||||
COALESCE(cm.school_system, m.school_system) AS `学制`,
|
||||
(
|
||||
CASE
|
||||
@@ -159,31 +153,29 @@ LEFT JOIN xg_class_msg cm
|
||||
ON cm.deleted = 0
|
||||
AND (cm.tenant_id COLLATE utf8mb4_unicode_ci) = (m.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
AND cm.id = CAST(NULLIF(TRIM(m.class_id), '') AS UNSIGNED)
|
||||
LEFT JOIN sys_dept dc_m
|
||||
ON dc_m.dept_id = CAST(NULLIF(TRIM(m.college_id), '') AS UNSIGNED)
|
||||
AND (dc_m.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dc_m.tenant_id COLLATE utf8mb4_unicode_ci) = (m.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dm_m
|
||||
ON dm_m.dept_id = CAST(NULLIF(TRIM(m.major_id), '') AS UNSIGNED)
|
||||
AND (dm_m.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dm_m.tenant_id COLLATE utf8mb4_unicode_ci) = (m.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dc_cm
|
||||
ON dc_cm.dept_id = cm.college_id
|
||||
AND (dc_cm.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dc_cm.tenant_id COLLATE utf8mb4_unicode_ci) = (m.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_dept dm_cm
|
||||
ON dm_cm.dept_id = cm.major_id
|
||||
AND (dm_cm.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (dm_cm.tenant_id COLLATE utf8mb4_unicode_ci) = (m.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN xg_dorm_campus campus_cm
|
||||
ON campus_cm.id = cm.campus_id
|
||||
AND (campus_cm.deleted COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN sys_user su
|
||||
ON su.user_id = COALESCE(m.class_teacher_id, cm.class_teacher_id)
|
||||
AND (su.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
AND (su.tenant_id COLLATE utf8mb4_unicode_ci) = (m.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
dbs.student_id,
|
||||
MAX(dc.campus_name) AS campus_name
|
||||
FROM xg_dorm_bed_student dbs
|
||||
INNER JOIN (
|
||||
SELECT student_id, MAX(assign_time) AS max_assign
|
||||
FROM xg_dorm_bed_student
|
||||
WHERE deleted = '0'
|
||||
AND (status COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
GROUP BY student_id
|
||||
) latest
|
||||
ON latest.student_id = dbs.student_id
|
||||
AND dbs.assign_time = latest.max_assign
|
||||
INNER JOIN xg_dorm_campus dc
|
||||
ON dc.id = dbs.campus_id
|
||||
AND (dc.deleted COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
WHERE dbs.deleted = '0'
|
||||
AND (dbs.status COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
|
||||
GROUP BY dbs.student_id
|
||||
) dorm ON dorm.student_id = m.id
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
sc.student_id,
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
| 5 | 申请人学号 | 直接输出 | `student_no` |
|
||||
| 6 | 姓名 | 输出异动当时姓名:换班级表有 `student_name`;学籍异动可 `JOIN xg_student_msg` 取当前 `name`(若需「当时」姓名需后续增强,见 §6) | 业务表 + `xg_student_msg` |
|
||||
| 7 | 异动前所在年级 | 有则输出 | 换班级:`original_`* 无年级时 JOIN 原班级 `xg_class_msg.grade`;学籍异动:见 §6 |
|
||||
| 8 | 异动前所在校区 | **暂不填写,恒为空** | `NULL` |
|
||||
| 9 | 异动前所在院系 | 有则输出 | 换班级 `original_college_name`;学籍异动见 §6 |
|
||||
| 10 | 异动前所在专业 | 有则输出 | 换班级 `original_major_name`;学籍异动见 §6 |
|
||||
| 11 | 异动前所在班级 | 有则输出 | 换班级 `original_class_name`;学籍异动见 §6 |
|
||||
| 8 | 异动前所在校区 | 异动前班级 `campus_id` → `xg_dorm_campus.campus_name`,再回退 `xg_class_msg.campus_name` | `xg_class_msg` + `xg_dorm_campus` |
|
||||
| 9 | 异动前所在院系 | 优先 `college_id` → `sys_dept.dept_name`,再回退冗余名称;换班级用 `original_college_id` / `original_college_name` | 换班级 `xg_student_class_change`;学籍异动见 §6 |
|
||||
| 10 | 异动前所在专业 | 优先 `major_id` → `sys_dept.dept_name`,再回退冗余名称 | 同上 |
|
||||
| 11 | 异动前所在班级 | 优先 `xg_class_msg.class_name`,再回退冗余名称 | 同上 |
|
||||
| 12 | 异动前学制 | 有则输出 | JOIN `xg_class_msg`(原 `class_id`)取 `school_system` |
|
||||
| 13~18 | 异动后所在校区 / 年级 / 班级 / 院系 / 专业 / 学制 | 校区:**空**;其余:换班级用 `new_`* + 新班 `xg_class_msg`;纯学籍异动时教务若仍要求「后」与「前」一致,可用 **COALESCE(后, 前)** 或统一填「前」快照(需与教务确认) | `xg_student_class_change` + `xg_class_msg` / 约定 |
|
||||
| 13~18 | 异动后所在校区 / 年级 / 班级 / 院系 / 专业 / 学制 | 校区:异动后班级 `campus_id` → `xg_dorm_campus`;院系/专业:`sys_dept`;班级:`xg_class_msg`;换班级用 `new_*` + 新班;纯学籍异动「后」取当前学生/班级 | `xg_student_class_change` + `xg_class_msg` / `sys_dept` |
|
||||
| 19 | 异动后班主任工号 | 有则输出 | `class_teacher_id` → `sys_user.user_no`(需业务确认教师工号存于 `user_no`) |
|
||||
| 20 | 异动后班主任姓名 | 有则输出 | `xg_class_msg.class_teacher_name` 或 `xg_student_msg.class_teacher_name` |
|
||||
| 21 | 新名字 | 选填;无独立「改名流水」时建议 **NULL**;若仅用 `former_name`/`name` 推断改名,易产生误判,不推荐作为正式口径 | 暂空或后续扩展表 |
|
||||
|
||||
+798408
-8
File diff suppressed because it is too large.
Load diff
@@ -55,6 +55,15 @@ export function updateClassStatusBatch(data: { ids: Array<string | number>; clas
|
||||
});
|
||||
}
|
||||
|
||||
// 批量设置班级所在校区
|
||||
export function updateClassCampusBatch(data: { ids: Array<string | number>; campusId: string | number }) {
|
||||
return request({
|
||||
url: '/classMsg/classMsg/campus/batch',
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 删除班级信息-新版
|
||||
export function delClassMsg(id: string | number) {
|
||||
return request({
|
||||
|
||||
@@ -14,6 +14,16 @@ export interface ClassMsgVO {
|
||||
*/
|
||||
majorId: string | number;
|
||||
|
||||
/**
|
||||
* 所在校区ID
|
||||
*/
|
||||
campusId?: string | number;
|
||||
|
||||
/**
|
||||
* 所在校区名称
|
||||
*/
|
||||
campusName?: string;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
@@ -76,6 +86,16 @@ export interface ClassMsgForm extends BaseEntity {
|
||||
*/
|
||||
majorName?: string;
|
||||
|
||||
/**
|
||||
* 所在校区ID
|
||||
*/
|
||||
campusId?: string | number;
|
||||
|
||||
/**
|
||||
* 所在校区名称
|
||||
*/
|
||||
campusName?: string;
|
||||
|
||||
/**
|
||||
* 班级代码
|
||||
*/
|
||||
@@ -303,6 +323,11 @@ export interface ClassMsgQuery extends PageQuery {
|
||||
*/
|
||||
majorId?: string | number;
|
||||
|
||||
/**
|
||||
* 所在校区ID
|
||||
*/
|
||||
campusId?: string | number;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface ClassMsgQuery {
|
||||
collegeId?: number | string;
|
||||
/** 专业代码 */
|
||||
majorId?: number | string;
|
||||
/** 所在校区ID */
|
||||
campusId?: number | string;
|
||||
/** 班级名称 */
|
||||
className?: string;
|
||||
/** 班级类型 */
|
||||
@@ -66,6 +68,10 @@ export interface ClassMsgVO {
|
||||
majorId?: string | number;
|
||||
/** 专业名称 */
|
||||
majorName?: string;
|
||||
/** 所在校区ID */
|
||||
campusId?: string | number;
|
||||
/** 所在校区名称 */
|
||||
campusName?: string;
|
||||
/** 班级代码 */
|
||||
classCode?: string;
|
||||
/** 班级名称 */
|
||||
|
||||
@@ -30,6 +30,15 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="所在校区" prop="campusId">
|
||||
<el-select v-model="queryParams.campusId" placeholder="请选择校区" clearable filterable>
|
||||
<el-option v-for="item in campusOptions" :key="item.id" :label="item.campusName" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24" class="search-btn-container">
|
||||
<el-form-item>
|
||||
@@ -91,6 +100,17 @@
|
||||
>解散</el-button
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="OfficeBuilding"
|
||||
:disabled="multiple"
|
||||
@click="handleBatchSetCampus"
|
||||
v-hasPermi="['classMsg:classMsg:edit']"
|
||||
>批量设置校区</el-button
|
||||
>
|
||||
</el-col>
|
||||
<!-- <el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['classMsg:classMsg:export']">导出</el-button>
|
||||
</el-col> -->
|
||||
@@ -105,6 +125,7 @@
|
||||
<el-table-column label="专业代码" align="center" prop="majorId" v-if="false" />
|
||||
<el-table-column label="学院名称" align="center" prop="collegeName" />
|
||||
<el-table-column label="专业名称" align="center" prop="majorName" />
|
||||
<el-table-column label="所在校区" align="center" prop="campusName" min-width="110" />
|
||||
<el-table-column label="班级名称" align="center" prop="className" />
|
||||
<el-table-column label="学历层次" align="center" prop="classType">
|
||||
<template #default="scope">
|
||||
@@ -185,6 +206,11 @@
|
||||
<el-form-item label="专业名称" prop="majorName" v-if="false">
|
||||
<el-input v-model="form.majorName" placeholder="请输入专业名称" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item label="所在校区" prop="campusId">
|
||||
<el-select v-model="form.campusId" placeholder="请选择校区(选填)" clearable filterable style="width: 100%" @change="handleFormCampusChange">
|
||||
<el-option v-for="item in campusOptions" :key="item.id" :label="item.campusName" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="班级代码" prop="classCode" v-if="false">
|
||||
<el-input v-model="form.classCode" placeholder="请输入班级代码" />
|
||||
</el-form-item>
|
||||
@@ -476,6 +502,26 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 批量设置所在校区 -->
|
||||
<el-dialog title="批量设置校区" v-model="campusBatchDialog.visible" width="420px" append-to-body>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="所在校区" required>
|
||||
<el-select v-model="campusBatchDialog.campusId" placeholder="请选择校区" filterable style="width: 100%">
|
||||
<el-option v-for="item in campusOptions" :key="item.id" :label="item.campusName" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="已选班级">
|
||||
<span>{{ ids.length }} 个</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitBatchSetCampus">确 定</el-button>
|
||||
<el-button @click="campusBatchDialog.visible = false">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -487,10 +533,13 @@ import {
|
||||
addClassMsg,
|
||||
updateClassMsg,
|
||||
updateClassStatus,
|
||||
updateClassStatusBatch
|
||||
updateClassStatusBatch,
|
||||
updateClassCampusBatch
|
||||
} from '@/api/xg/xsjcxx/classmsg/classMsg';
|
||||
import { listColleges, collegeTree, listMajorsByCollegeId } from '@/api/xg/xsjcxx/classmsg/college';
|
||||
import { ClassMsgVO, ClassMsgQuery, ClassMsgForm, SysDeptVo } from '@/api/xg/xsjcxx/classmsg/types';
|
||||
import { listDormCampus } from '@/api/xg/xgss/xqxx/dormCampus';
|
||||
import type { DormCampusVO } from '@/api/xg/xgss/xqxx/dormCampus/types';
|
||||
import { ComponentInternalInstance, ref, reactive, toRefs, onMounted, getCurrentInstance, watch } from 'vue';
|
||||
import type { FormInstance as ElFormInstance, UploadFile, UploadInstance } from 'element-plus';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
@@ -571,6 +620,7 @@ const collegeOptions = ref<SysDeptVo[]>([]); // 学院选项
|
||||
const majorOptions = ref<SysDeptVo[]>([]); // 检索时的专业选项
|
||||
const formMajorOptions = ref<SysDeptVo[]>([]); // 表单中的专业选项
|
||||
const collegeTreeData = ref<SysDeptVo[]>([]); // 学院专业树形数据
|
||||
const campusOptions = ref<DormCampusVO[]>([]); // 校区选项
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const classMsgFormRef = ref<ElFormInstance>();
|
||||
@@ -590,12 +640,19 @@ const dialog = reactive<DialogOption>({
|
||||
title: ''
|
||||
});
|
||||
|
||||
const campusBatchDialog = reactive<{ visible: boolean; campusId?: string | number }>({
|
||||
visible: false,
|
||||
campusId: undefined
|
||||
});
|
||||
|
||||
const initFormData: ClassMsgForm = {
|
||||
id: undefined,
|
||||
collegeId: undefined,
|
||||
collegeName: undefined,
|
||||
majorId: undefined,
|
||||
majorName: undefined,
|
||||
campusId: undefined,
|
||||
campusName: undefined,
|
||||
classCode: undefined,
|
||||
className: undefined,
|
||||
classType: '4',
|
||||
@@ -648,6 +705,7 @@ const data = reactive<PageData<ClassMsgForm, ClassMsgQuery>>({
|
||||
pageSize: 10,
|
||||
collegeId: undefined,
|
||||
majorId: undefined,
|
||||
campusId: undefined,
|
||||
className: undefined,
|
||||
classType: undefined,
|
||||
grade: undefined,
|
||||
@@ -686,6 +744,16 @@ const getCollegeList = async () => {
|
||||
collegeOptions.value = res.data;
|
||||
};
|
||||
|
||||
/** 查询校区列表 */
|
||||
const getCampusList = async () => {
|
||||
try {
|
||||
const res = await listDormCampus({ pageNum: 1, pageSize: 1000 });
|
||||
campusOptions.value = (res as any).rows || res.data || [];
|
||||
} catch (e) {
|
||||
campusOptions.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
/** 查询学院专业树形结构 */
|
||||
const getCollegeTree = async () => {
|
||||
const res = await collegeTree();
|
||||
@@ -752,6 +820,17 @@ const handleFormMajorChange = (value: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
/** 表单校区变更:同步名称 */
|
||||
const handleFormCampusChange = (campusId?: string | number | null) => {
|
||||
if (campusId === undefined || campusId === null || campusId === '') {
|
||||
form.value.campusId = undefined;
|
||||
form.value.campusName = undefined;
|
||||
return;
|
||||
}
|
||||
const campus = campusOptions.value.find((item) => String(item.id) === String(campusId));
|
||||
form.value.campusName = campus?.campusName;
|
||||
};
|
||||
|
||||
// 监听班级名称变化,自动同步到班级代码
|
||||
watch(
|
||||
() => form.value.className,
|
||||
@@ -977,6 +1056,33 @@ const handleBatchDissolve = async () => {
|
||||
await handleBatchChangeStatus('4', '解散');
|
||||
};
|
||||
|
||||
/** 打开批量设置校区弹窗 */
|
||||
const handleBatchSetCampus = () => {
|
||||
if (!ids.value || ids.value.length === 0) {
|
||||
proxy?.$modal.msgWarning('请先勾选需要操作的班级');
|
||||
return;
|
||||
}
|
||||
campusBatchDialog.campusId = undefined;
|
||||
campusBatchDialog.visible = true;
|
||||
};
|
||||
|
||||
/** 提交批量设置校区 */
|
||||
const submitBatchSetCampus = async () => {
|
||||
if (campusBatchDialog.campusId === undefined || campusBatchDialog.campusId === null || campusBatchDialog.campusId === '') {
|
||||
proxy?.$modal.msgWarning('请选择校区');
|
||||
return;
|
||||
}
|
||||
buttonLoading.value = true;
|
||||
try {
|
||||
await updateClassCampusBatch({ ids: ids.value, campusId: campusBatchDialog.campusId });
|
||||
proxy?.$modal.msgSuccess('批量设置校区成功');
|
||||
campusBatchDialog.visible = false;
|
||||
await getList();
|
||||
} finally {
|
||||
buttonLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
@@ -1281,6 +1387,7 @@ onMounted(() => {
|
||||
getList();
|
||||
getCollegeList();
|
||||
getCollegeTree();
|
||||
getCampusList();
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
+50
-14
@@ -12,9 +12,11 @@
|
||||
├── data.conf.prod # 生产环境配置(Oracle 目标库)
|
||||
├── create_tables.sh # 创建中间表
|
||||
├── sync_user_to_sso.sh # 主同步脚本
|
||||
├── verify_sync.sh # 同步结果校验
|
||||
├── verify_sync.sh # 同步结果校验(支持 MySQL / Oracle)
|
||||
├── db_helper.py # MySQL 辅助
|
||||
├── oracle_gen_merge.py # 从 MySQL 生成 Oracle MERGE SQL(生产)
|
||||
├── sync_sql/sync_all_mysql.sql # MySQL 同步 SQL
|
||||
├── sync_oracle/merge_all.sql # Oracle MERGE 模板(生产)
|
||||
├── sync_oracle/merge_all.sql # Oracle 说明(实际由 gen 脚本生成)
|
||||
├── 字段映射说明.md
|
||||
└── logs/ # 运行日志(自动创建)
|
||||
```
|
||||
@@ -35,13 +37,34 @@ chmod +x *.sh
|
||||
./verify_sync.sh
|
||||
```
|
||||
|
||||
## 定时运行
|
||||
## 定时运行(推荐:守护进程 + 时间窗,无需 crontab)
|
||||
|
||||
生产配置 `SYNC_WINDOW_ENABLED=true` 时:脚本每小时醒来一次,**仅在 0 点~1 点之间**真正执行同步,其余时段跳过。手动 `--once` / `--full --once` 不受时间窗限制。
|
||||
|
||||
```bash
|
||||
# 守护进程模式,默认每 3600 秒(1小时)同步一次
|
||||
./sync_user_to_sso.sh
|
||||
# 后台常驻(服务器重启后需重新拉起,或配合 systemd)
|
||||
source /etc/profile.d/oracle.sh
|
||||
cd /path/to/用户信息同步统一身份认证
|
||||
nohup ./sync_user_to_sso.sh >> logs/daemon.log 2>&1 &
|
||||
|
||||
# 修改同步频率:编辑 data.conf 中 CYCLE_INTERVAL(秒)
|
||||
# 查看
|
||||
ps -ef | grep sync_user_to_sso | grep -v grep
|
||||
tail -f logs/daemon.log
|
||||
```
|
||||
|
||||
相关配置(`data.conf` / `data.conf.prod`):
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|--------|------|
|
||||
| `CYCLE_INTERVAL` | 醒来间隔秒数,默认 3600 |
|
||||
| `SYNC_WINDOW_ENABLED` | `true` 开启时间窗 |
|
||||
| `SYNC_WINDOW_START_HOUR` | 开始小时(含),默认 0 |
|
||||
| `SYNC_WINDOW_END_HOUR` | 结束小时(不含),默认 1,即 00:00~00:59 |
|
||||
|
||||
也可改用 crontab(可选):
|
||||
|
||||
```bash
|
||||
10 0 * * * source /etc/profile.d/oracle.sh; /path/to/sync_user_to_sso.sh --once >> /path/to/logs/cron.log 2>&1
|
||||
```
|
||||
|
||||
## 生产部署
|
||||
@@ -49,13 +72,24 @@ chmod +x *.sh
|
||||
1. 复制 `data.conf.prod` 为 `data.conf` 或指定 `--config=data.conf.prod`
|
||||
2. 填写源库(校园系统 MySQL)和目标库(认证中间库 Oracle)连接信息
|
||||
3. 目标库使用第三方提供的 `3.在认证中间库建立表.sql` 在 Oracle 中建表
|
||||
4. 确保服务器已安装 `sqlplus`(Oracle 客户端)
|
||||
5. 使用 crontab 或 systemd 定时执行:
|
||||
|
||||
```bash
|
||||
# crontab 示例:每小时执行一次
|
||||
0 * * * * /path/to/用户信息同步统一身份认证/sync_user_to_sso.sh --once >> /path/to/logs/cron.log 2>&1
|
||||
```
|
||||
4. 确保服务器已安装 Instant Client(`basic` + `sqlplus`),并配置:
|
||||
```bash
|
||||
export ORACLE_HOME=/usr/lib/oracle/23/client64
|
||||
export PATH=$ORACLE_HOME/bin:$PATH
|
||||
export LD_LIBRARY_PATH=$ORACLE_HOME/lib:$LD_LIBRARY_PATH
|
||||
```
|
||||
5. 依赖:`python3` + `pymysql`(`pip3 install pymysql`)
|
||||
6. 全量同步一次并校验:
|
||||
```bash
|
||||
source /etc/profile.d/oracle.sh
|
||||
./sync_user_to_sso.sh --full --once
|
||||
./verify_sync.sh
|
||||
```
|
||||
7. 后台常驻(时间窗内每天自动同步一次):
|
||||
```bash
|
||||
source /etc/profile.d/oracle.sh
|
||||
nohup ./sync_user_to_sso.sh >> logs/daemon.log 2>&1 &
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
@@ -65,4 +99,6 @@ chmod +x *.sh
|
||||
| TARGET_DB_* | 目标库(认证中间库,读写) |
|
||||
| TARGET_DB_TYPE | `mysql`(测试)或 `oracle`(生产) |
|
||||
| SYNC_MODE | `full` 全量 / `incremental` 增量 |
|
||||
| CYCLE_INTERVAL | 循环间隔秒数,默认 3600 |
|
||||
| CYCLE_INTERVAL | 守护进程醒来间隔秒数,默认 3600 |
|
||||
| SYNC_WINDOW_ENABLED | 是否仅在指定小时段同步(守护模式) |
|
||||
| SYNC_WINDOW_START_HOUR / END_HOUR | 时间窗,默认 0~1(每天 0 点档) |
|
||||
@@ -32,6 +32,11 @@ BATCH_SIZE=500
|
||||
MAX_RETRY=3
|
||||
RETRY_DELAY=5
|
||||
|
||||
# 守护进程时间窗(研发默认关闭;生产见 data.conf.prod)
|
||||
SYNC_WINDOW_ENABLED=false
|
||||
SYNC_WINDOW_START_HOUR=0
|
||||
SYNC_WINDOW_END_HOUR=1
|
||||
|
||||
# ---------- 日志与锁 ----------
|
||||
LOG_DIR="${SCRIPT_DIR}/logs"
|
||||
LOCK_FILE="${SCRIPT_DIR}/sync_user_to_sso.lock"
|
||||
|
||||
@@ -10,7 +10,7 @@ SOURCE_DB_HOST="127.0.0.1"
|
||||
SOURCE_DB_PORT="3306"
|
||||
SOURCE_DB_NAME="hzdyjs_xg"
|
||||
SOURCE_DB_USER="root"
|
||||
SOURCE_DB_PASS="请填写生产源库密码"
|
||||
SOURCE_DB_PASS="Hzdyjs@Root@123456"
|
||||
|
||||
# ---------- 目标库(认证中间库 Oracle,读写) ----------
|
||||
# 第三方厂家提供:172.16.0.82:1521/zfdb,用户 CHG_RZ
|
||||
@@ -30,6 +30,12 @@ BATCH_SIZE=500
|
||||
MAX_RETRY=3
|
||||
RETRY_DELAY=5
|
||||
|
||||
# 守护进程时间窗(仅 ./sync_user_to_sso.sh 常驻模式生效;--once 不受限)
|
||||
# true=开启;每天 [START, END) 点之间才真正同步,例如 0~1 表示 00:00~00:59
|
||||
SYNC_WINDOW_ENABLED=true
|
||||
SYNC_WINDOW_START_HOUR=0
|
||||
SYNC_WINDOW_END_HOUR=1
|
||||
|
||||
# ---------- 日志与锁 ----------
|
||||
LOG_DIR="${SCRIPT_DIR}/logs"
|
||||
LOCK_FILE="${SCRIPT_DIR}/sync_user_to_sso.lock"
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""根据 hzdyjs_xg.sql 模型生成「学生可提取第三方信息」Excel 清单(单表版)"""
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
OUTPUT = "学生信息第三方提取清单.xlsx"
|
||||
|
||||
HEADER_FILL = PatternFill("solid", fgColor="4472C4")
|
||||
HEADER_FONT = Font(bold=True, color="FFFFFF", size=11)
|
||||
THIN = Side(style="thin")
|
||||
BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
|
||||
WRAP = Alignment(wrap_text=True, vertical="center")
|
||||
CENTER = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
|
||||
# (数据表, 字段名, 中文名称, 数据类型, 说明)
|
||||
FIELDS = [
|
||||
# ── xg_student_msg 学生信息主表 ──
|
||||
("xg_student_msg", "student_no", "学号", "varchar(32)", "学号,唯一标识"),
|
||||
("xg_student_msg", "name", "姓名", "varchar(50)", ""),
|
||||
("xg_student_msg", "name_english", "英文姓名", "varchar(100)", ""),
|
||||
("xg_student_msg", "former_name", "曾用名", "varchar(50)", ""),
|
||||
("xg_student_msg", "gender", "性别", "char(1)", "0-男,1-女"),
|
||||
("xg_student_msg", "birth_date", "出生日期", "date", ""),
|
||||
("xg_student_msg", "nationality", "民族", "varchar(20)", ""),
|
||||
("xg_student_msg", "native_place", "籍贯", "varchar(20)", ""),
|
||||
("xg_student_msg", "political_status", "政治面貌", "varchar(20)", ""),
|
||||
("xg_student_msg", "id_card", "身份证号", "varchar(18)", ""),
|
||||
("xg_student_msg", "photo_url", "照片URL", "varchar(500)", ""),
|
||||
("xg_student_msg", "phone", "手机号码", "varchar(20)", ""),
|
||||
("xg_student_msg", "email", "电子邮箱", "varchar(100)", ""),
|
||||
("xg_student_msg", "qq", "QQ号码", "varchar(20)", ""),
|
||||
("xg_student_msg", "wechat", "微信号", "varchar(50)", ""),
|
||||
("xg_student_msg", "postal_code", "邮政编码", "varchar(10)", ""),
|
||||
("xg_student_msg", "home_address", "家庭地址", "varchar(1000)", ""),
|
||||
("xg_student_msg", "current_address", "现住址", "varchar(1000)", ""),
|
||||
("xg_student_msg", "hukou_location", "户籍所在地", "varchar(1000)", ""),
|
||||
("xg_student_msg", "student_status", "学籍状态", "char(1)", "1-在籍,2-休学,3-复学,4-退学,5-毕业,6-不在籍"),
|
||||
("xg_student_msg", "enrollment_year", "入学年份", "int(4)", ""),
|
||||
("xg_student_msg", "enrollment_date", "入学日期", "date", ""),
|
||||
("xg_student_msg", "admission_category", "录取类别", "varchar(20)", "统招、自主招生、专项计划等"),
|
||||
("xg_student_msg", "school_system", "学制(年)", "int(2)", ""),
|
||||
("xg_student_msg", "academic_level", "学历层次", "varchar(20)", ""),
|
||||
("xg_student_msg", "training_type", "培养方式", "varchar(20)", "全日制、在职、函授、夜大等"),
|
||||
("xg_student_msg", "major_id", "专业ID", "varchar(100)", ""),
|
||||
("xg_student_msg", "major_name", "专业名称", "varchar(100)", ""),
|
||||
("xg_student_msg", "class_id", "班级ID", "varchar(100)", ""),
|
||||
("xg_student_msg", "class_name", "班级名称", "varchar(100)", ""),
|
||||
("xg_student_msg", "grade", "年级", "varchar(10)", ""),
|
||||
("xg_student_msg", "college_id", "学院ID", "varchar(100)", ""),
|
||||
("xg_student_msg", "college_name", "学院名称", "varchar(100)", ""),
|
||||
("xg_student_msg", "counselor_name", "辅导员姓名", "varchar(50)", ""),
|
||||
("xg_student_msg", "class_teacher_name", "班主任姓名", "varchar(50)", ""),
|
||||
("xg_student_msg", "is_boarder", "是否住校", "tinyint(1)", "0-否,1-是"),
|
||||
("xg_student_msg", "education_level", "文化程度", "varchar(20)", ""),
|
||||
("xg_student_msg", "household_register_type", "户口类型", "varchar(20)", "农业、非农业"),
|
||||
("xg_student_msg", "religion", "宗教信仰", "varchar(200)", ""),
|
||||
("xg_student_msg", "source_region", "生源地区", "varchar(100)", "省市县"),
|
||||
("xg_student_msg", "graduate_school", "毕业院校", "varchar(100)", "升学前毕业院校"),
|
||||
("xg_student_msg", "marital_status", "婚姻状况", "varchar(10)", "1未婚、2已婚、3离异、4丧偶"),
|
||||
("xg_student_msg", "emergency_contact_name", "紧急联系人姓名", "varchar(50)", ""),
|
||||
("xg_student_msg", "emergency_contact_phone", "紧急联系人电话", "varchar(20)", ""),
|
||||
("xg_student_msg", "emergency_contact_relation", "紧急联系人关系", "varchar(100)", ""),
|
||||
("xg_student_msg", "is_poor_student", "是否贫困生", "tinyint(1)", "0-否,1-是"),
|
||||
("xg_student_msg", "is_minority", "是否少数民族", "tinyint(1)", "0-否,1-是"),
|
||||
("xg_student_msg", "is_disabled", "是否残疾", "tinyint(1)", "0-否,1-是"),
|
||||
("xg_student_msg", "is_orphan", "是否孤儿", "tinyint(1)", "0-否,1-是"),
|
||||
("xg_student_msg", "is_left_behind_child", "是否留守儿童", "tinyint(1)", "0-否,1-是"),
|
||||
("xg_student_msg", "is_single_child", "是否独生子女", "tinyint(1)", "0-否,1-是"),
|
||||
("xg_student_msg", "is_low_income", "是否低保", "tinyint(1)", "0-否,1-是"),
|
||||
("xg_student_msg", "has_loan", "是否有助学贷款", "tinyint(1)", "0-否,1-是"),
|
||||
("xg_student_msg", "loan_amount", "助学贷款金额", "decimal(10,2)", ""),
|
||||
("xg_student_msg", "father_name", "父亲姓名", "varchar(50)", ""),
|
||||
("xg_student_msg", "father_phone", "父亲电话", "varchar(20)", ""),
|
||||
("xg_student_msg", "father_work_unit", "父亲工作单位", "varchar(1000)", ""),
|
||||
("xg_student_msg", "mother_name", "母亲姓名", "varchar(50)", ""),
|
||||
("xg_student_msg", "mother_phone", "母亲电话", "varchar(20)", ""),
|
||||
("xg_student_msg", "mother_work_unit", "母亲工作单位", "varchar(1000)", ""),
|
||||
("xg_student_msg", "guardian_name", "监护人姓名", "varchar(50)", ""),
|
||||
("xg_student_msg", "guardian_phone", "监护人电话", "varchar(20)", ""),
|
||||
("xg_student_msg", "guardian_relation", "监护人关系", "varchar(20)", ""),
|
||||
("xg_student_msg", "family_income", "家庭年收入", "decimal(12,2)", ""),
|
||||
("xg_student_msg", "per_capita_income", "人均收入", "decimal(12,2)", ""),
|
||||
("xg_student_msg", "income_source", "收入来源", "varchar(100)", ""),
|
||||
("xg_student_msg", "family_economic_status", "家庭经济状况", "varchar(20)", "富裕、一般、困难、特困"),
|
||||
("xg_student_msg", "family_members", "家庭成员数量", "tinyint(2)", ""),
|
||||
("xg_student_msg", "height", "身高(cm)", "decimal(10,2)", ""),
|
||||
("xg_student_msg", "weight", "体重(kg)", "decimal(10,2)", ""),
|
||||
("xg_student_msg", "blood_type", "血型", "varchar(10)", ""),
|
||||
("xg_student_msg", "health_status", "健康状况", "varchar(1000)", ""),
|
||||
("xg_student_msg", "special_condition", "特殊体质或疾病", "varchar(1000)", ""),
|
||||
("xg_student_msg", "medical_history", "病史", "varchar(1000)", ""),
|
||||
("xg_student_msg", "allergies", "过敏史", "varchar(1000)", ""),
|
||||
("xg_student_msg", "disability_type", "残疾类型", "varchar(50)", "1视力、2听力、3智力、9其他"),
|
||||
("xg_student_msg", "disability_level", "残疾等级", "varchar(20)", ""),
|
||||
("xg_student_msg", "bank_card_no", "银行卡号", "varchar(30)", ""),
|
||||
("xg_student_msg", "bank_name", "开户银行", "varchar(100)", ""),
|
||||
("xg_student_msg", "bank_branch", "开户支行", "varchar(100)", ""),
|
||||
("xg_student_msg", "card_holder_name", "持卡人姓名", "varchar(50)", ""),
|
||||
("xg_student_msg", "specialties", "特长爱好", "text", ""),
|
||||
("xg_student_msg", "awards", "获奖情况", "text", ""),
|
||||
("xg_student_msg", "violations", "违纪记录", "text", ""),
|
||||
("xg_student_msg", "social_activities", "社会活动", "text", ""),
|
||||
("xg_student_msg", "dormitory_building", "宿舍楼栋", "varchar(100)", ""),
|
||||
("xg_student_msg", "dormitory_room", "宿舍房间号", "varchar(100)", ""),
|
||||
# ── xg_class_msg 班级信息表 ──
|
||||
("xg_class_msg", "class_code", "班级代码", "varchar(32)", ""),
|
||||
("xg_class_msg", "class_name", "班级名称", "varchar(100)", ""),
|
||||
("xg_class_msg", "college_name", "学院名称", "varchar(100)", ""),
|
||||
("xg_class_msg", "major_name", "专业名称", "varchar(100)", ""),
|
||||
("xg_class_msg", "grade", "年级", "varchar(10)", ""),
|
||||
("xg_class_msg", "enrollment_year", "入学年份", "int(4)", ""),
|
||||
("xg_class_msg", "graduation_year", "毕业年份", "int(4)", ""),
|
||||
("xg_class_msg", "school_system", "学制", "int(11)", ""),
|
||||
("xg_class_msg", "training_type", "培养方式", "varchar(20)", ""),
|
||||
("xg_class_msg", "degree_type", "学位类型", "varchar(20)", ""),
|
||||
("xg_class_msg", "counselor_name", "辅导员姓名", "varchar(50)", ""),
|
||||
("xg_class_msg", "class_teacher_name", "班主任姓名", "varchar(50)", ""),
|
||||
("xg_class_msg", "class_status", "班级状态", "char(1)", "1-正常,2-暂停,3-毕业,4-解散"),
|
||||
# ── xg_dorm_bed_student 宿舍分配表 ──
|
||||
("xg_dorm_bed_student", "student_id", "学生ID", "bigint(20)", "关联 xg_student_msg.id"),
|
||||
("xg_dorm_bed_student", "bed_id", "床位ID", "bigint(20)", ""),
|
||||
("xg_dorm_bed_student", "room_id", "房间ID", "bigint(20)", ""),
|
||||
("xg_dorm_bed_student", "building_id", "宿舍楼ID", "bigint(20)", ""),
|
||||
("xg_dorm_bed_student", "campus_id", "校区ID", "bigint(20)", ""),
|
||||
("xg_dorm_bed_student", "status", "状态", "char(1)", "0-正常,1-已退宿"),
|
||||
("xg_dorm_bed_student", "assign_time", "分配时间", "datetime", ""),
|
||||
# ── xg_student_leave 请假记录表 ──
|
||||
("xg_student_leave", "student_no", "学号", "varchar(20)", ""),
|
||||
("xg_student_leave", "student_name", "学生姓名", "varchar(50)", ""),
|
||||
("xg_student_leave", "class_name", "班级名称", "varchar(100)", ""),
|
||||
("xg_student_leave", "college_name", "学院名称", "varchar(100)", ""),
|
||||
("xg_student_leave", "leave_type_name", "请假类型", "varchar(50)", ""),
|
||||
("xg_student_leave", "start_time", "开始时间", "datetime", ""),
|
||||
("xg_student_leave", "end_time", "结束时间", "datetime", ""),
|
||||
("xg_student_leave", "leave_duration", "请假时长(天)", "decimal(5,2)", ""),
|
||||
("xg_student_leave", "is_leave_school", "是否离校", "char(1)", "0-否,1-是"),
|
||||
("xg_student_leave", "leave_reason", "请假原因", "text", ""),
|
||||
("xg_student_leave", "approval_status", "审批状态", "char(100)", ""),
|
||||
# ── xg_student_status_change 学籍异动表 ──
|
||||
("xg_student_status_change", "student_no", "学号", "varchar(100)", ""),
|
||||
("xg_student_status_change", "change_type", "异动类型", "varchar(20)", "TUI_XUE退学、XIU_XUE休学、FU_XUE复学、BI_YE毕业"),
|
||||
("xg_student_status_change", "apply_date", "申请日期", "datetime", ""),
|
||||
("xg_student_status_change", "effective_date", "生效日期", "datetime", ""),
|
||||
("xg_student_status_change", "reason", "异动原因", "varchar(1000)", ""),
|
||||
# ── xg_student_class_change 换班记录表 ──
|
||||
("xg_student_class_change", "student_no", "学号", "varchar(32)", ""),
|
||||
("xg_student_class_change", "student_name", "学生姓名", "varchar(50)", ""),
|
||||
("xg_student_class_change", "original_class_name", "原班级名称", "varchar(100)", ""),
|
||||
("xg_student_class_change", "new_class_name", "新班级名称", "varchar(100)", ""),
|
||||
("xg_student_class_change", "change_time", "换班时间", "datetime", ""),
|
||||
("xg_student_class_change", "change_reason", "换班原因", "varchar(500)", ""),
|
||||
# ── jcbt_work_study_application 勤工助学申请表 ──
|
||||
("jcbt_work_study_application", "student_number", "学号", "varchar(20)", ""),
|
||||
("jcbt_work_study_application", "student_name", "学生姓名", "varchar(50)", ""),
|
||||
("jcbt_work_study_application", "college_name", "学院名称", "varchar(100)", ""),
|
||||
("jcbt_work_study_application", "major", "专业", "varchar(100)", ""),
|
||||
("jcbt_work_study_application", "grade", "年级", "varchar(10)", ""),
|
||||
("jcbt_work_study_application", "class_name", "班级", "varchar(50)", ""),
|
||||
("jcbt_work_study_application", "phone", "联系电话", "varchar(20)", ""),
|
||||
("jcbt_work_study_application", "id_card", "身份证号", "varchar(18)", ""),
|
||||
("jcbt_work_study_application", "position_name", "岗位名称", "varchar(100)", ""),
|
||||
("jcbt_work_study_application", "status", "申请状态", "varchar(20)", ""),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "学生可提取信息"
|
||||
|
||||
headers = ["序号", "数据表", "字段名", "中文名称", "数据类型", "说明"]
|
||||
ws.append(headers)
|
||||
for cell in ws[1]:
|
||||
cell.fill = HEADER_FILL
|
||||
cell.font = HEADER_FONT
|
||||
cell.alignment = CENTER
|
||||
cell.border = BORDER
|
||||
|
||||
for i, row in enumerate(FIELDS, 1):
|
||||
ws.append([i, *row])
|
||||
for cell in ws[ws.max_row]:
|
||||
cell.alignment = WRAP
|
||||
cell.border = BORDER
|
||||
|
||||
for col in ws.columns:
|
||||
letter = get_column_letter(col[0].column)
|
||||
width = max(len(str(c.value or "")) for c in col)
|
||||
ws.column_dimensions[letter].width = min(max(width + 2, 8), 40)
|
||||
ws.column_dimensions["A"].width = 6
|
||||
ws.column_dimensions["B"].width = 28
|
||||
ws.column_dimensions["F"].width = 40
|
||||
|
||||
ws.freeze_panes = "A2"
|
||||
wb.save(OUTPUT)
|
||||
print(f"已生成: {OUTPUT}(共 {len(FIELDS)} 条)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,394 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""从 MySQL 源库导出数据,生成 Oracle MERGE SQL,供 sqlplus 执行。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, List, Optional, Sequence, Tuple
|
||||
|
||||
try:
|
||||
import pymysql
|
||||
except ImportError:
|
||||
print("ERROR: 需要安装 pymysql: pip install pymysql", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def connect_mysql(host: str, port: int, user: str, password: str, database: str):
|
||||
return pymysql.connect(
|
||||
host=host,
|
||||
port=port,
|
||||
user=user,
|
||||
password=password,
|
||||
database=database,
|
||||
charset="utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
autocommit=True,
|
||||
)
|
||||
|
||||
|
||||
def q(val: Any, max_len: Optional[int] = None) -> str:
|
||||
"""Python 值转 Oracle SQL 字面量。"""
|
||||
if val is None:
|
||||
return "NULL"
|
||||
if isinstance(val, datetime):
|
||||
s = val.strftime("%Y%m%d%H%M%S")
|
||||
else:
|
||||
s = str(val).strip()
|
||||
if s == "":
|
||||
return "NULL"
|
||||
if max_len is not None:
|
||||
s = s[:max_len]
|
||||
s = s.replace("'", "''")
|
||||
return f"'{s}'"
|
||||
|
||||
|
||||
def right10(val: Any) -> Optional[str]:
|
||||
if val is None:
|
||||
return None
|
||||
s = str(val).strip()
|
||||
if not s or s == "0":
|
||||
return None
|
||||
return s[-10:]
|
||||
|
||||
|
||||
def fmt_dt(val: Any, fmt: str = "%Y%m%d%H%M%S") -> Optional[str]:
|
||||
if val is None:
|
||||
return None
|
||||
if isinstance(val, datetime):
|
||||
return val.strftime(fmt)
|
||||
s = str(val).strip()
|
||||
return s or None
|
||||
|
||||
|
||||
def sex_code(val: Any) -> str:
|
||||
s = "" if val is None else str(val)
|
||||
if s == "0":
|
||||
return "1"
|
||||
if s == "1":
|
||||
return "2"
|
||||
return "0"
|
||||
|
||||
|
||||
def write_header(out) -> None:
|
||||
out.write("-- generated by oracle_gen_merge.py\n")
|
||||
out.write("WHENEVER SQLERROR EXIT SQL.SQLCODE\n")
|
||||
out.write("SET ECHO OFF\n")
|
||||
out.write("SET FEEDBACK OFF\n")
|
||||
out.write("SET DEFINE OFF\n")
|
||||
out.write("SET SERVEROUTPUT ON SIZE UNLIMITED\n")
|
||||
out.write("\n")
|
||||
|
||||
|
||||
def commit_batch(out, count: int, label: str) -> None:
|
||||
out.write("COMMIT;\n")
|
||||
out.write(f"PROMPT {label} committed batch, total so far: {count}\n")
|
||||
out.write("\n")
|
||||
|
||||
|
||||
def gen_dept(cur, out, incr_sql: str, batch_size: int) -> int:
|
||||
sql = f"""
|
||||
SELECT
|
||||
d.dept_id,
|
||||
d.dept_name,
|
||||
d.parent_id,
|
||||
d.status,
|
||||
d.del_flag,
|
||||
IFNULL(d.update_time, d.create_time) AS ts
|
||||
FROM sys_dept d
|
||||
WHERE 1=1
|
||||
{incr_sql}
|
||||
"""
|
||||
cur.execute(sql)
|
||||
n = 0
|
||||
for row in cur:
|
||||
dwh = right10(row["dept_id"])
|
||||
if not dwh:
|
||||
continue
|
||||
lsdwh = right10(row["parent_id"])
|
||||
dwyxbs = "1" if row["status"] == "0" and row["del_flag"] == "0" else "0"
|
||||
scbz = "y" if row["del_flag"] == "1" else "n"
|
||||
zhgxsj = fmt_dt(row["ts"])
|
||||
out.write(
|
||||
f"""MERGE INTO GXXX_YXSDWJBSJZL t
|
||||
USING (SELECT {q(dwh, 10)} AS dwh FROM dual) s
|
||||
ON (t.dwh = s.dwh)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
t.dwmc = {q(row['dept_name'], 60)},
|
||||
t.lsdwh = {q(lsdwh, 10)},
|
||||
t.dwyxbs = {q(dwyxbs, 1)},
|
||||
t.zhgxsj = {q(zhgxsj, 20)},
|
||||
t.scbz = {q(scbz, 1)}
|
||||
WHEN NOT MATCHED THEN INSERT (dwh, dwmc, lsdwh, dwyxbs, zhgxsj, scbz)
|
||||
VALUES ({q(dwh, 10)}, {q(row['dept_name'], 60)}, {q(lsdwh, 10)}, {q(dwyxbs, 1)}, {q(zhgxsj, 20)}, {q(scbz, 1)});
|
||||
"""
|
||||
)
|
||||
n += 1
|
||||
if n % batch_size == 0:
|
||||
commit_batch(out, n, "DEPT")
|
||||
if n % batch_size != 0:
|
||||
commit_batch(out, n, "DEPT")
|
||||
out.write(f"PROMPT DEPT done: {n}\n\n")
|
||||
return n
|
||||
|
||||
|
||||
def gen_teacher(cur, out, incr_sql: str, batch_size: int) -> Tuple[int, int]:
|
||||
sql = f"""
|
||||
SELECT
|
||||
u.user_name,
|
||||
u.nick_name,
|
||||
u.dept_id,
|
||||
u.sex,
|
||||
u.status,
|
||||
u.del_flag,
|
||||
u.phonenumber,
|
||||
u.email,
|
||||
IFNULL(u.update_time, u.create_time) AS ts
|
||||
FROM sys_user u
|
||||
WHERE u.account_type = '1'
|
||||
{incr_sql}
|
||||
"""
|
||||
cur.execute(sql)
|
||||
n_base = 0
|
||||
n_tx = 0
|
||||
for row in cur:
|
||||
gh = (row["user_name"] or "").strip()
|
||||
if not gh:
|
||||
continue
|
||||
gh = gh[:20]
|
||||
dwh = right10(row["dept_id"]) or "0"
|
||||
dwh = dwh[:10]
|
||||
xm = (row["nick_name"] or row["user_name"] or "")[:36]
|
||||
xbm = sex_code(row["sex"])
|
||||
dqztm = "11" if row["status"] == "0" else "99"
|
||||
scbz = "y" if row["del_flag"] == "1" else "n"
|
||||
zhgxsj = fmt_dt(row["ts"])
|
||||
yddh = row["phonenumber"] or None
|
||||
dzxx = row["email"] or None
|
||||
|
||||
out.write(
|
||||
f"""MERGE INTO GXJG_JZGJCSJZLB t
|
||||
USING (SELECT {q(gh, 20)} AS gh FROM dual) s
|
||||
ON (t.gh = s.gh)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
t.dwh = {q(dwh, 10)},
|
||||
t.xm = {q(xm, 36)},
|
||||
t.xbm = {q(xbm, 1)},
|
||||
t.dqztm = {q(dqztm, 2)},
|
||||
t.zhgxsj = {q(zhgxsj, 20)},
|
||||
t.scbz = {q(scbz, 1)}
|
||||
WHEN NOT MATCHED THEN INSERT (gh, dwh, xm, xbm, dqztm, zhgxsj, scbz)
|
||||
VALUES ({q(gh, 20)}, {q(dwh, 10)}, {q(xm, 36)}, {q(xbm, 1)}, {q(dqztm, 2)}, {q(zhgxsj, 20)}, {q(scbz, 1)});
|
||||
"""
|
||||
)
|
||||
n_base += 1
|
||||
|
||||
tid = gh[:48]
|
||||
out.write(
|
||||
f"""MERGE INTO GXJG_GRTXFSZL t
|
||||
USING (SELECT {q(tid, 48)} AS id FROM dual) s
|
||||
ON (t.id = s.id)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
t.gh = {q(gh, 20)},
|
||||
t.yddh = {q(yddh, 30)},
|
||||
t.dzxx = {q(dzxx, 40)},
|
||||
t.zhgxsj = {q(zhgxsj, 20)},
|
||||
t.scbz = {q(scbz, 1)}
|
||||
WHEN NOT MATCHED THEN INSERT (id, gh, yddh, dzxx, zhgxsj, scbz)
|
||||
VALUES ({q(tid, 48)}, {q(gh, 20)}, {q(yddh, 30)}, {q(dzxx, 40)}, {q(zhgxsj, 20)}, {q(scbz, 1)});
|
||||
"""
|
||||
)
|
||||
n_tx += 1
|
||||
|
||||
if n_base % batch_size == 0:
|
||||
commit_batch(out, n_base, "TEACHER")
|
||||
|
||||
if n_base % batch_size != 0:
|
||||
commit_batch(out, n_base, "TEACHER")
|
||||
out.write(f"PROMPT TEACHER done: base={n_base} tx={n_tx}\n\n")
|
||||
return n_base, n_tx
|
||||
|
||||
|
||||
def gen_student(cur, out, incr_sql: str, batch_size: int) -> Tuple[int, int]:
|
||||
sql = f"""
|
||||
SELECT
|
||||
s.student_no,
|
||||
s.name,
|
||||
s.name_english,
|
||||
s.former_name,
|
||||
s.gender,
|
||||
s.birth_date,
|
||||
s.native_place,
|
||||
s.id_card,
|
||||
s.political_status,
|
||||
s.blood_type,
|
||||
s.is_single_child,
|
||||
s.college_id,
|
||||
s.class_id,
|
||||
s.student_status,
|
||||
s.deleted,
|
||||
s.postal_code,
|
||||
s.current_address,
|
||||
s.home_address,
|
||||
s.phone,
|
||||
s.email,
|
||||
s.qq,
|
||||
s.wechat,
|
||||
c.class_code,
|
||||
IFNULL(s.update_time, s.create_time) AS ts
|
||||
FROM xg_student_msg s
|
||||
LEFT JOIN xg_class_msg c ON c.id = s.class_id AND c.deleted = 0
|
||||
WHERE 1=1
|
||||
{incr_sql}
|
||||
"""
|
||||
cur.execute(sql)
|
||||
n_base = 0
|
||||
n_tx = 0
|
||||
for row in cur:
|
||||
xh = (row["student_no"] or "").strip()
|
||||
if not xh:
|
||||
continue
|
||||
xh = xh[:20]
|
||||
dwh = str(row["college_id"] or "0")[:20]
|
||||
if not dwh:
|
||||
dwh = "0"
|
||||
szbh = (row["class_code"] or row["class_id"] or "")
|
||||
szbh = str(szbh)[:20] if szbh else None
|
||||
status = "" if row["student_status"] is None else str(row["student_status"])
|
||||
sfzx = "1" if status in ("1", "2", "3") else "0"
|
||||
sfyby = "1" if status == "5" else "0"
|
||||
scbz = "y" if row["deleted"] == 1 else "n"
|
||||
zhgxsj = fmt_dt(row["ts"])
|
||||
csrq = fmt_dt(row["birth_date"], "%Y%m%d")
|
||||
sfdszn = "1" if row["is_single_child"] == 1 else "0"
|
||||
zzmmm = (row["political_status"] or None)
|
||||
if zzmmm is not None:
|
||||
zzmmm = str(zzmmm)[:2]
|
||||
txdz = row["current_address"] or row["home_address"]
|
||||
jstxh = row["qq"] or row["wechat"]
|
||||
|
||||
out.write(
|
||||
f"""MERGE INTO GXXS_XSJBSJZL t
|
||||
USING (SELECT {q(xh, 20)} AS xh FROM dual) s
|
||||
ON (t.xh = s.xh)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
t.xm = {q(row['name'], 60)},
|
||||
t.ywxm = {q(row['name_english'], 60)},
|
||||
t.cym = {q(row['former_name'], 60)},
|
||||
t.xbm = {q(sex_code(row['gender']), 1)},
|
||||
t.csrq = {q(csrq, 10)},
|
||||
t.jg = {q(row['native_place'], 60)},
|
||||
t.sfzjlxm = '1',
|
||||
t.sfzjh = {q(row['id_card'], 20)},
|
||||
t.zzmmm = {q(zzmmm, 2)},
|
||||
t.xxm = {q(row['blood_type'], 30)},
|
||||
t.sfdszn = {q(sfdszn, 1)},
|
||||
t.zhgxsj = {q(zhgxsj, 20)},
|
||||
t.scbz = {q(scbz, 1)},
|
||||
t.dwh = {q(dwh, 20)},
|
||||
t.szbh = {q(szbh, 20)},
|
||||
t.sfzx = {q(sfzx, 10)},
|
||||
t.sfyby = {q(sfyby, 6)}
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
xh, xm, ywxm, cym, xbm, csrq, jg, sfzjlxm, sfzjh, zzmmm, xxm,
|
||||
sfdszn, zhgxsj, scbz, dwh, szbh, sfzx, sfyby
|
||||
) VALUES (
|
||||
{q(xh, 20)}, {q(row['name'], 60)}, {q(row['name_english'], 60)}, {q(row['former_name'], 60)},
|
||||
{q(sex_code(row['gender']), 1)}, {q(csrq, 10)}, {q(row['native_place'], 60)}, '1',
|
||||
{q(row['id_card'], 20)}, {q(zzmmm, 2)}, {q(row['blood_type'], 30)}, {q(sfdszn, 1)},
|
||||
{q(zhgxsj, 20)}, {q(scbz, 1)}, {q(dwh, 20)}, {q(szbh, 20)}, {q(sfzx, 10)}, {q(sfyby, 6)}
|
||||
);
|
||||
"""
|
||||
)
|
||||
n_base += 1
|
||||
|
||||
out.write(
|
||||
f"""MERGE INTO GXXS_GRTXZL t
|
||||
USING (SELECT {q(xh, 20)} AS xh FROM dual) s
|
||||
ON (t.xh = s.xh)
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
t.yzbm = {q(row['postal_code'], 6)},
|
||||
t.txdz = {q(txdz, 180)},
|
||||
t.yddh = {q(row['phone'], 30)},
|
||||
t.dzxx = {q(row['email'], 40)},
|
||||
t.jstxh = {q(jstxh, 40)},
|
||||
t.zhgxsj = {q(zhgxsj, 20)},
|
||||
t.scbz = {q(scbz, 1)}
|
||||
WHEN NOT MATCHED THEN INSERT (xh, yzbm, txdz, yddh, dzxx, jstxh, zhgxsj, scbz)
|
||||
VALUES (
|
||||
{q(xh, 20)}, {q(row['postal_code'], 6)}, {q(txdz, 180)}, {q(row['phone'], 30)},
|
||||
{q(row['email'], 40)}, {q(jstxh, 40)}, {q(zhgxsj, 20)}, {q(scbz, 1)}
|
||||
);
|
||||
"""
|
||||
)
|
||||
n_tx += 1
|
||||
|
||||
if n_base % batch_size == 0:
|
||||
commit_batch(out, n_base, "STUDENT")
|
||||
|
||||
if n_base % batch_size != 0:
|
||||
commit_batch(out, n_base, "STUDENT")
|
||||
out.write(f"PROMPT STUDENT done: base={n_base} tx={n_tx}\n\n")
|
||||
return n_base, n_tx
|
||||
|
||||
|
||||
def build_incr(alias: str, last_sync: str, full: bool) -> str:
|
||||
if full or not last_sync or last_sync.startswith("1970"):
|
||||
return ""
|
||||
return (
|
||||
f"AND ({alias}.update_time > '{last_sync}' "
|
||||
f"OR {alias}.create_time > '{last_sync}')"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="生成 Oracle MERGE SQL")
|
||||
parser.add_argument("--host", required=True)
|
||||
parser.add_argument("--port", default="3306")
|
||||
parser.add_argument("--user", required=True)
|
||||
parser.add_argument("--password", required=True)
|
||||
parser.add_argument("--database", required=True)
|
||||
parser.add_argument("--output", required=True, help="输出 SQL 文件路径")
|
||||
parser.add_argument("--last-sync", default="1970-01-01 00:00:00")
|
||||
parser.add_argument("--full", action="store_true")
|
||||
parser.add_argument("--batch-size", type=int, default=200)
|
||||
args = parser.parse_args()
|
||||
|
||||
out_path = Path(args.output)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
conn = connect_mysql(
|
||||
args.host, int(args.port), args.user, args.password, args.database
|
||||
)
|
||||
try:
|
||||
with conn.cursor() as cur, out_path.open("w", encoding="utf-8") as out:
|
||||
write_header(out)
|
||||
dept_n = gen_dept(
|
||||
cur, out, build_incr("d", args.last_sync, args.full), args.batch_size
|
||||
)
|
||||
tea_n, tea_tx = gen_teacher(
|
||||
cur, out, build_incr("u", args.last_sync, args.full), args.batch_size
|
||||
)
|
||||
stu_n, stu_tx = gen_student(
|
||||
cur, out, build_incr("s", args.last_sync, args.full), args.batch_size
|
||||
)
|
||||
out.write("COMMIT;\n")
|
||||
out.write(
|
||||
f"PROMPT SYNC SUMMARY dept={dept_n} teacher={tea_n}/{tea_tx} "
|
||||
f"student={stu_n}/{stu_tx}\n"
|
||||
)
|
||||
out.write("EXIT;\n")
|
||||
print(
|
||||
f"OK dept={dept_n} teacher={tea_n} teacher_tx={tea_tx} "
|
||||
f"student={stu_n} student_tx={stu_tx} file={out_path}"
|
||||
)
|
||||
return 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,17 +1,19 @@
|
||||
-- ==========================================
|
||||
-- Oracle 目标库 MERGE 同步脚本(生产环境)
|
||||
-- 由 sync_user_to_sso.sh 通过 sqlplus 调用
|
||||
-- 注意:需根据实际导出文件格式调整
|
||||
-- Oracle 目标库 MERGE 同步说明
|
||||
-- ==========================================
|
||||
-- 生产同步不再直接使用本占位文件。
|
||||
-- sync_user_to_sso.sh 会调用 oracle_gen_merge.py:
|
||||
-- 1) 从 MySQL 源库读取部门/教职工/学生
|
||||
-- 2) 生成带 MERGE 语句的临时 SQL
|
||||
-- 3) 通过 sqlplus 执行写入认证中间库
|
||||
--
|
||||
-- 手动调试示例:
|
||||
-- python3 oracle_gen_merge.py \
|
||||
-- --host 127.0.0.1 --port 3306 --user root --password xxx \
|
||||
-- --database hzdyjs_xg --output /tmp/merge.sql --full
|
||||
-- sqlplus user/pass@host:1521/service @/tmp/merge.sql
|
||||
-- ==========================================
|
||||
|
||||
-- 示例:部门 MERGE(生产部署时需配合 sqlldr 或外部表导入)
|
||||
-- 当前为占位模板,待 Oracle 环境连通后完善
|
||||
|
||||
WHENEVER SQLERROR EXIT SQL.SQLCODE
|
||||
SET ECHO OFF
|
||||
SET FEEDBACK ON
|
||||
SET VERIFY OFF
|
||||
|
||||
PROMPT Oracle MERGE sync placeholder - 请根据生产环境完善
|
||||
|
||||
PROMPT Use oracle_gen_merge.py via sync_user_to_sso.sh
|
||||
EXIT;
|
||||
@@ -173,42 +173,47 @@ sync_mysql_target() {
|
||||
sync_oracle_target() {
|
||||
local last_sync
|
||||
last_sync=$(get_last_sync_time)
|
||||
log "INFO" "开始 Oracle 目标库同步,上次同步: $last_sync"
|
||||
local mode_label="增量"
|
||||
if [[ "$FORCE_FULL" == true ]] || [[ "$SYNC_MODE" == "full" ]]; then
|
||||
mode_label="全量"
|
||||
fi
|
||||
log "INFO" "开始 Oracle 目标库同步,模式: ${mode_label},上次同步: $last_sync"
|
||||
|
||||
if ! command -v sqlplus >/dev/null 2>&1; then
|
||||
error_exit "未找到 sqlplus 命令,无法连接 Oracle 目标库"
|
||||
error_exit "未找到 sqlplus 命令,无法连接 Oracle 目标库(请安装 Instant Client 并配置 PATH)"
|
||||
fi
|
||||
|
||||
local export_dir="/tmp/sso_sync_export_$$"
|
||||
mkdir -p "$export_dir"
|
||||
|
||||
# 导出部门
|
||||
source_mysql "
|
||||
SELECT
|
||||
RIGHT(CAST(d.dept_id AS CHAR), 10),
|
||||
REPLACE(IFNULL(d.dept_name,''),'\t',' '),
|
||||
CASE WHEN d.parent_id IS NULL OR d.parent_id=0 THEN '' ELSE RIGHT(CAST(d.parent_id AS CHAR),10) END,
|
||||
CASE WHEN d.status='0' AND d.del_flag='0' THEN '1' ELSE '0' END,
|
||||
DATE_FORMAT(IFNULL(d.update_time,d.create_time),'%Y%m%d%H%i%s'),
|
||||
CASE WHEN d.del_flag='1' THEN 'y' ELSE 'n' END
|
||||
FROM sys_dept d
|
||||
" > "${export_dir}/dept.tsv" || true
|
||||
|
||||
local oracle_script="${SCRIPT_DIR}/sync_oracle/merge_all.sql"
|
||||
if [[ ! -f "$oracle_script" ]]; then
|
||||
error_exit "Oracle MERGE 脚本未找到: $oracle_script"
|
||||
local merge_sql="/tmp/sso_oracle_merge_$$.sql"
|
||||
local gen_cmd=(
|
||||
python3 "${SCRIPT_DIR}/oracle_gen_merge.py"
|
||||
--host "$SOURCE_DB_HOST" --port "$SOURCE_DB_PORT"
|
||||
--user "$SOURCE_DB_USER" --password "$SOURCE_DB_PASS"
|
||||
--database "$SOURCE_DB_NAME"
|
||||
--output "$merge_sql"
|
||||
--last-sync "$last_sync"
|
||||
--batch-size "${BATCH_SIZE:-200}"
|
||||
)
|
||||
if [[ "$FORCE_FULL" == true ]] || [[ "$SYNC_MODE" == "full" ]]; then
|
||||
gen_cmd+=(--full)
|
||||
fi
|
||||
|
||||
local gen_out
|
||||
if ! gen_out=$("${gen_cmd[@]}" 2>&1); then
|
||||
log "ERROR" "生成 Oracle MERGE SQL 失败: $gen_out"
|
||||
rm -f "$merge_sql"
|
||||
return 1
|
||||
fi
|
||||
log "INFO" "已生成 MERGE SQL: $gen_out"
|
||||
|
||||
local conn="${TARGET_DB_USER}/${TARGET_DB_PASS}@${TARGET_ORACLE_CONNECT:-${TARGET_DB_HOST}:${TARGET_DB_PORT}/${TARGET_DB_NAME}}"
|
||||
if sqlplus -S "$conn" @"$oracle_script" "$export_dir" >> "$LOG_FILE" 2>&1; then
|
||||
if sqlplus -S "$conn" @"$merge_sql" >> "$LOG_FILE" 2>&1; then
|
||||
log "INFO" "Oracle 同步完成"
|
||||
save_sync_time
|
||||
rm -rf "$export_dir"
|
||||
rm -f "$merge_sql"
|
||||
return 0
|
||||
else
|
||||
log "ERROR" "Oracle 同步失败,详见日志"
|
||||
log "ERROR" "Oracle 同步失败,详见日志: $LOG_FILE (SQL 文件保留: $merge_sql)"
|
||||
write_errlog "ALL" "Oracle数据同步" "sqlplus执行失败"
|
||||
rm -rf "$export_dir"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -261,12 +266,33 @@ verify_counts() {
|
||||
log "INFO" "源表 xg_student_msg(未删除): ${src_student} 条"
|
||||
}
|
||||
|
||||
# 守护进程时间窗:仅在 [SYNC_WINDOW_START_HOUR, SYNC_WINDOW_END_HOUR) 内执行
|
||||
# 例如 START=0 END=1 表示每天 00:00~00:59;--once / --full 不受此限制
|
||||
in_sync_window() {
|
||||
local enabled="${SYNC_WINDOW_ENABLED:-false}"
|
||||
if [[ "$enabled" != "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local start_h="${SYNC_WINDOW_START_HOUR:-0}"
|
||||
local end_h="${SYNC_WINDOW_END_HOUR:-1}"
|
||||
local now_h
|
||||
now_h=$(date '+%-H' 2>/dev/null || date '+%H' | sed 's/^0//')
|
||||
[[ -z "$now_h" ]] && now_h=0
|
||||
if (( now_h >= start_h && now_h < end_h )); then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
acquire_lock
|
||||
log "INFO" "统一身份认证用户数据同步启动"
|
||||
log "INFO" "源库: ${SOURCE_DB_TYPE} ${SOURCE_DB_USER}@${SOURCE_DB_HOST}:${SOURCE_DB_PORT}/${SOURCE_DB_NAME}"
|
||||
log "INFO" "目标库: ${TARGET_DB_TYPE} ${TARGET_DB_USER}@${TARGET_DB_HOST}:${TARGET_DB_PORT}/${TARGET_DB_NAME}"
|
||||
log "INFO" "同步模式: ${SYNC_MODE},循环间隔: ${CYCLE_INTERVAL}秒"
|
||||
if [[ "${SYNC_WINDOW_ENABLED:-false}" == "true" ]]; then
|
||||
log "INFO" "守护进程时间窗: ${SYNC_WINDOW_START_HOUR:-0}:00 ~ ${SYNC_WINDOW_END_HOUR:-1}:00(仅该时段执行;--once 不受限)"
|
||||
fi
|
||||
|
||||
if [[ "$RUN_ONCE" == true ]]; then
|
||||
run_sync_cycle && verify_counts
|
||||
@@ -274,7 +300,11 @@ main() {
|
||||
fi
|
||||
|
||||
while true; do
|
||||
run_sync_cycle && verify_counts || log "WARN" "本轮同步失败,将在 ${CYCLE_INTERVAL} 秒后重试"
|
||||
if in_sync_window; then
|
||||
run_sync_cycle && verify_counts || log "WARN" "本轮同步失败,将在 ${CYCLE_INTERVAL} 秒后重试"
|
||||
else
|
||||
log "INFO" "当前不在同步时间窗内(${SYNC_WINDOW_START_HOUR:-0}:00~${SYNC_WINDOW_END_HOUR:-1}:00),跳过本轮"
|
||||
fi
|
||||
log "INFO" "等待 ${CYCLE_INTERVAL} 秒后开始下一轮..."
|
||||
sleep "$CYCLE_INTERVAL"
|
||||
done
|
||||
|
||||
@@ -18,6 +18,27 @@ mysql_q() {
|
||||
scalar "$6" 2>/dev/null
|
||||
}
|
||||
|
||||
oracle_counts() {
|
||||
if ! command -v sqlplus >/dev/null 2>&1; then
|
||||
echo "ERROR: 未找到 sqlplus,无法校验 Oracle 目标库" >&2
|
||||
return 1
|
||||
fi
|
||||
local conn="${TARGET_DB_USER}/${TARGET_DB_PASS}@${TARGET_ORACLE_CONNECT:-${TARGET_DB_HOST}:${TARGET_DB_PORT}/${TARGET_DB_NAME}}"
|
||||
sqlplus -S "$conn" <<'EOF'
|
||||
SET PAGESIZE 100
|
||||
SET LINESIZE 200
|
||||
SET HEADING OFF
|
||||
SET FEEDBACK OFF
|
||||
SET VERIFY OFF
|
||||
SELECT 'GXXX_YXSDWJBSJZL=' || COUNT(*) FROM GXXX_YXSDWJBSJZL;
|
||||
SELECT 'GXJG_JZGJCSJZLB=' || COUNT(*) FROM GXJG_JZGJCSJZLB;
|
||||
SELECT 'GXJG_GRTXFSZL=' || COUNT(*) FROM GXJG_GRTXFSZL;
|
||||
SELECT 'GXXS_XSJBSJZL=' || COUNT(*) FROM GXXS_XSJBSJZL;
|
||||
SELECT 'GXXS_GRTXZL=' || COUNT(*) FROM GXXS_GRTXZL;
|
||||
EXIT;
|
||||
EOF
|
||||
}
|
||||
|
||||
echo "========== 统一身份认证同步校验 =========="
|
||||
echo "时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo ""
|
||||
@@ -48,7 +69,6 @@ if [[ "$TARGET_DB_TYPE" == "mysql" ]]; then
|
||||
echo "[目标库] ZFTAL_XTGL_ERRLOG: ${TGT_ERR}"
|
||||
echo ""
|
||||
|
||||
# 抽样对比
|
||||
echo "---------- 抽样数据对比 ----------"
|
||||
python3 "${SCRIPT_DIR}/db_helper.py" \
|
||||
--host "$TARGET_DB_HOST" --port "$TARGET_DB_PORT" \
|
||||
@@ -60,7 +80,6 @@ if [[ "$TARGET_DB_TYPE" == "mysql" ]]; then
|
||||
query "SELECT xh, xm, dwh, szbh, scbz FROM GXXS_XSJBSJZL LIMIT 3" 2>/dev/null
|
||||
|
||||
echo ""
|
||||
# 简单一致性检查
|
||||
PASS=true
|
||||
[[ "$TGT_DEPT" -ge 1 ]] || { echo "WARN: 部门同步数量为0"; PASS=false; }
|
||||
[[ "$TGT_TEACHER" -ge 1 ]] || { echo "WARN: 教职工同步数量为0"; PASS=false; }
|
||||
@@ -75,7 +94,37 @@ if [[ "$TARGET_DB_TYPE" == "mysql" ]]; then
|
||||
echo "校验结果: 未通过"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "$TARGET_DB_TYPE" == "oracle" ]]; then
|
||||
echo "[目标库 Oracle] 记录数:"
|
||||
ORACLE_OUT=$(oracle_counts) || { echo "校验结果: 未通过(sqlplus 失败)"; exit 1; }
|
||||
echo "$ORACLE_OUT"
|
||||
|
||||
TGT_DEPT=$(echo "$ORACLE_OUT" | sed -n 's/.*GXXX_YXSDWJBSJZL=\([0-9][0-9]*\).*/\1/p' | head -1)
|
||||
TGT_TEACHER=$(echo "$ORACLE_OUT" | sed -n 's/.*GXJG_JZGJCSJZLB=\([0-9][0-9]*\).*/\1/p' | head -1)
|
||||
TGT_TEACHER_TX=$(echo "$ORACLE_OUT" | sed -n 's/.*GXJG_GRTXFSZL=\([0-9][0-9]*\).*/\1/p' | head -1)
|
||||
TGT_STUDENT=$(echo "$ORACLE_OUT" | sed -n 's/.*GXXS_XSJBSJZL=\([0-9][0-9]*\).*/\1/p' | head -1)
|
||||
TGT_STUDENT_TX=$(echo "$ORACLE_OUT" | sed -n 's/.*GXXS_GRTXZL=\([0-9][0-9]*\).*/\1/p' | head -1)
|
||||
|
||||
echo ""
|
||||
echo "---------- 与源库对照 ----------"
|
||||
echo "部门: 源=${SRC_DEPT} 目标=${TGT_DEPT:-0}"
|
||||
echo "教职工: 源=${SRC_TEACHER} 目标=${TGT_TEACHER:-0} / 通讯=${TGT_TEACHER_TX:-0}"
|
||||
echo "学生: 源=${SRC_STUDENT} 目标=${TGT_STUDENT:-0} / 通讯=${TGT_STUDENT_TX:-0}"
|
||||
echo ""
|
||||
|
||||
PASS=true
|
||||
[[ "${TGT_DEPT:-0}" -ge 1 ]] || { echo "WARN: 部门同步数量为0"; PASS=false; }
|
||||
[[ "${TGT_TEACHER:-0}" -ge 1 ]] || { echo "WARN: 教职工同步数量为0"; PASS=false; }
|
||||
[[ "${TGT_STUDENT:-0}" -ge 1 ]] || { echo "WARN: 学生同步数量为0"; PASS=false; }
|
||||
|
||||
if [[ "$PASS" == true ]]; then
|
||||
echo "校验结果: 通过(Oracle 目标库已有数据)"
|
||||
exit 0
|
||||
else
|
||||
echo "校验结果: 未通过"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "目标库为 Oracle,请使用 sqlplus 手动校验目标表记录数"
|
||||
exit 0
|
||||
echo "不支持的目标库类型: $TARGET_DB_TYPE"
|
||||
exit 1
|
||||
fi
|
||||
@@ -102,9 +102,9 @@
|
||||
1. **单位号 dwh 长度限制**:第三方表 `dwh` 仅 10 位,而 `sys_dept.dept_id` 为雪花ID(19位),当前取后10位;若对方有编码规范需另行映射。
|
||||
2. **增量同步**:基于源表 `update_time` / `create_time`,状态记录在 `.sync_state`。
|
||||
3. **全量同步**:设置 `SYNC_MODE=full` 或执行 `./sync_user_to_sso.sh --full --once`。
|
||||
4. **生产 Oracle**:修改 `data.conf.prod` 中 `TARGET_DB_TYPE=oracle` 及连接信息,使用 `sqlplus` 执行 MERGE(模板见 `sync_oracle/`)。
|
||||
4. **生产 Oracle**:`TARGET_DB_TYPE=oracle` 时,由 `oracle_gen_merge.py` 从 MySQL 生成 MERGE SQL,再经 `sqlplus` 写入中间库;`verify_sync.sh` 可直接校验 Oracle 表记录数。
|
||||
|
||||
---
|
||||
|
||||
**版本**: v1.0.0
|
||||
**版本**: v1.1.0
|
||||
**更新时间**: 2026-08-24
|
||||
@@ -15,7 +15,7 @@
|
||||
| 教务字段 | 口径 |
|
||||
|----------|------|
|
||||
| 身份证件类型 | 有 `id_card` →「居民身份证」,否则 `null` |
|
||||
| 所在校区 | 有有效宿舍分配(`xg_dorm_bed_student.status=0`)→ `xg_dorm_campus.campus_name`,否则 `null` |
|
||||
| 所在校区 | 学生 `class_id` → `xg_class_msg.campus_id` → `xg_dorm_campus.campus_name`,再回退班级冗余 `campus_name`;无班级或未设置时为 `null` |
|
||||
| 班主任工号 | `class_teacher_id` → `sys_user.user_no`,空则 `user_name` |
|
||||
| 学生是否在校 | 默认「在校」;学籍状态为 **退学(4)、毕业(5)** →「不在校」 |
|
||||
| 异动类型 | 固定 `null` |
|
||||
@@ -26,6 +26,21 @@
|
||||
| 性别 | `0`→「男」,`1`→「女」 |
|
||||
| 成员1 | 父亲姓名/关系「父亲」/ `father_phone` |
|
||||
| 成员2 | 母亲姓名/关系「母亲」/ `mother_phone` |
|
||||
| 培养层次 | 班级 `xg_class_msg.class_type` 字典值(`zsjy_education_level`),**非中文名称** |
|
||||
| 系部信息 | 优先 `college_id` → `sys_dept.dept_name`(学生表,其次班级表),再回退冗余 `college_name` |
|
||||
| 专业信息 | 优先 `major_id` → `sys_dept.dept_name`(学生表,其次班级表),再回退冗余 `major_name` |
|
||||
| 班级信息 | `class_id` → `xg_class_msg.class_name`(优先班级表,再回退学生表冗余字段) |
|
||||
|
||||
## 培养层次字典值(`zsjy_education_level`)
|
||||
|
||||
| 字典值 | 中文名称 |
|
||||
|--------|----------|
|
||||
| 0 | 三年制中级技工 |
|
||||
| 1 | 三年制技师 |
|
||||
| 2 | 四年制高级工 |
|
||||
| 3 | 五年制高级技工 |
|
||||
| 4 | 六年制技师 |
|
||||
| 5 | 中高职一体 |
|
||||
|
||||
## 学生当前状态映射(本系统 → 教务中文名)
|
||||
|
||||
|
||||
@@ -106,8 +106,8 @@ gunicorn -w 2 -b 0.0.0.0:5050 app:app
|
||||
| 异动生效学期 | 本系统无数据,为 `null` |
|
||||
| 异动类型 | **教务中文名称**(退学、休学、复学、转班级) |
|
||||
| 申请人学号 / 姓名 | |
|
||||
| 异动前/后 年级、院系、专业、班级、学制 | 学籍异动「前」优先取处理前快照 |
|
||||
| 异动前/后 所在校区 | 本系统无数据,为 `null` |
|
||||
| 异动前/后 年级、院系、专业、班级、学制 | 院系/专业优先 `sys_dept`;班级优先 `xg_class_msg`;学籍异动「前」优先历史快照 |
|
||||
| 异动前/后 所在校区 | 班级 `campus_id` → `xg_dorm_campus.campus_name`,再回退班级 `campus_name` |
|
||||
| 异动后班主任工号 / 姓名 | |
|
||||
| 新名字 | 本系统无改名流水,为 `null` |
|
||||
| 审核状态 | 固定「已通过」 |
|
||||
|
||||
@@ -144,18 +144,18 @@ while True:
|
||||
| 联系电话 | 否 | |
|
||||
| 身份证件类型 | 否 | 有身份证号时为「居民身份证」,否则 `null` |
|
||||
| 身份证件号 | 否 | |
|
||||
| 班级信息 | 是 | 班级名称 |
|
||||
| 所在校区 | 是 | 有宿舍分配时取校区名,否则 `null` |
|
||||
| 班级信息 | 是 | `class_id` → `xg_class_msg.class_name`(优先班级表,再回退学生表冗余字段) |
|
||||
| 所在校区 | 是 | 学生 `class_id` → 班级 `campus_id` → `xg_dorm_campus.campus_name`,再回退班级 `campus_name`;无班级或未设置时为 `null` |
|
||||
| 所在年级 | 是 | |
|
||||
| 入学年份 | 是 | 整数,如 `2024` |
|
||||
| 系部信息 | 是 | 院系名称 |
|
||||
| 专业信息 | 是 | 专业名称 |
|
||||
| 系部信息 | 是 | `college_id` → `sys_dept.dept_name`(优先学生表 ID,其次班级表 ID,再回退冗余 `college_name`) |
|
||||
| 专业信息 | 是 | `major_id` → `sys_dept.dept_name`(优先学生表 ID,其次班级表 ID,再回退冗余 `major_name`) |
|
||||
| 班主任工号 | 是 | |
|
||||
| 班主任姓名 | 是 | |
|
||||
| 学生当前状态 | 是 | 教务中文名:在读、休学、复学、退学、毕业、结业、停学、其他 |
|
||||
| 学生是否在校 | 是 | 「在校」或「不在校」;退学、毕业为不在校 |
|
||||
| 异动类型 | 否 | 固定 `null` |
|
||||
| 培养层次 | 否 | 如三年制技师、六年制技师等 |
|
||||
| 培养层次 | 否 | 班级 `class_type` 字典值(`zsjy_education_level`):`0` 三年制中级技工、`1` 三年制技师、`2` 四年制高级工、`3` 五年制高级技工、`4` 六年制技师、`5` 中高职一体;无班级或未设置时为 `null` |
|
||||
| 学制 | 是 | 年数,整数 |
|
||||
| 毕业时间 | 是 | **年份**整数;已毕业取异动生效年,否则按入学年+学制推算 |
|
||||
| 毕业学年学期 | 是 | 如 `2025-2026-2`;无法推算时为 `null` |
|
||||
@@ -198,7 +198,7 @@ while True:
|
||||
"学生当前状态": "在读",
|
||||
"学生是否在校": "在校",
|
||||
"异动类型": null,
|
||||
"培养层次": "三年制技师",
|
||||
"培养层次": "1",
|
||||
"学制": 3,
|
||||
"毕业时间": 2027,
|
||||
"毕业学年学期": "2026-2027-2",
|
||||
@@ -260,10 +260,10 @@ X-Jw-Cx-Key: JWCX001
|
||||
| 异动类型 | 教务中文名:**退学、休学、复学、转班级** |
|
||||
| 申请人学号 | |
|
||||
| 姓名 | |
|
||||
| 异动前所在年级 / 院系 / 专业 / 班级 / 学制 | |
|
||||
| 异动前所在校区 | 固定 `null` |
|
||||
| 异动后所在年级 / 班级 / 院系 / 专业 / 学制 | |
|
||||
| 异动后所在校区 | 固定 `null` |
|
||||
| 异动前所在年级 / 院系 / 专业 / 班级 / 学制 | 院系/专业优先 `sys_dept`;班级优先 `xg_class_msg`;再回退快照冗余名称 |
|
||||
| 异动前所在校区 | 异动前班级 `campus_id` → `xg_dorm_campus.campus_name`,再回退班级 `campus_name` |
|
||||
| 异动后所在年级 / 班级 / 院系 / 专业 / 学制 | 同上(异动后取当前学生/新班级) |
|
||||
| 异动后所在校区 | 异动后班级 `campus_id` → `xg_dorm_campus.campus_name`,再回退班级 `campus_name` |
|
||||
| 异动后班主任工号 / 姓名 | |
|
||||
| 新名字 | 固定 `null` |
|
||||
| 审核状态 | 固定「已通过」 |
|
||||
@@ -293,12 +293,12 @@ X-Jw-Cx-Key: JWCX001
|
||||
"申请人学号": "2024010101",
|
||||
"姓名": "张三",
|
||||
"异动前所在年级": "2024",
|
||||
"异动前所在校区": null,
|
||||
"异动前所在校区": "西溪校区",
|
||||
"异动前所在院系": "电气工程系",
|
||||
"异动前所在专业": "电气工程",
|
||||
"异动前所在班级": "24技师1班",
|
||||
"异动前学制": 3,
|
||||
"异动后所在校区": null,
|
||||
"异动后所在校区": "西溪校区",
|
||||
"异动后所在年级": "2024",
|
||||
"异动后所在班级": "24技师1班",
|
||||
"异动后所在院系": "电气工程系",
|
||||
|
||||
@@ -27,7 +27,10 @@
|
||||
|
||||
- 对外字段名:**异动类型**
|
||||
- 字段值:上表「教务系统异动类型名称」列的中文名称(非本库内部编码)
|
||||
- **异动生效学期**、**异动前/后所在校区**:本系统无数据,固定返回 `null`
|
||||
- **异动生效学期**、**新名字**:本系统无数据,固定返回 `null`
|
||||
- **异动前/后所在校区**:班级 `campus_id` → `xg_dorm_campus.campus_name`,再回退班级 `campus_name`
|
||||
- **异动前/后院系/专业**:优先 `college_id`、`major_id` → `sys_dept.dept_name`,再回退冗余名称
|
||||
- **异动前/后班级**:优先 `xg_class_msg.class_name`,再回退冗余名称
|
||||
- **新名字**:本系统无改名流水,固定返回 `null`
|
||||
- **审核状态**:固定返回「已通过」
|
||||
|
||||
|
||||
Reference in new issue
Block a user