提交
This commit is contained in:
1 parent
dd6212400c
commit
f3869785ff
9 files changed
+870
-3
No files matched your search
+1
-1
@@ -68,7 +68,7 @@ Mall4J 高质量Java商城系统 - https://www.mall4j.com/cn/?statId=11 <br>
|
||||
| 数据库主键 | 采用 雪花ID 基于时间戳的 有序增长 唯一ID 再也不用为分库分表 数据合并主键冲突重复而发愁 | 采用 数据库自增ID 支持数据量有限 不支持多数据源主键唯一 |
|
||||
| WebSocket协议 | 基于 Spring 封装的 WebSocket 协议 扩展了Token鉴权与分布式会话同步 不再只是基于单机的废物 | 无 |
|
||||
| SSE推送 | 采用 Spring SSE 实现 扩展了Token鉴权与分布式会话同步 | 无 |
|
||||
| 序列化 | 采用 Jackson Spring官方内置序列化 靠谱!!! | 采用 fastjson bugjson 远近闻名 |
|
||||
| 序列化 | 采用 Jackson Spring官方内置序列化 靠谱!!!(JustAuth 传递依赖已统一升级为 Fastjson2 1.x 兼容包 2.0.61) | 采用 fastjson bugjson 远近闻名 |
|
||||
| 分布式幂等 | 参考美团GTIS防重系统简化实现(细节可看文档) | 手动编写注解基于aop实现 |
|
||||
| 分布式锁 | 采用 Lock4j 底层基于 Redisson | 无 |
|
||||
| 分布式任务调度 | 采用 SnailJob 天生支持分布式 统一的管理中心 支持多种数据库 支持分片重试DAG任务流等 | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造 |
|
||||
|
||||
+2
-2
@@ -43,8 +43,8 @@
|
||||
<aws.sdk.version>2.28.22</aws.sdk.version>
|
||||
<!-- SMS 配置 -->
|
||||
<sms4j.version>3.3.4</sms4j.version>
|
||||
<!-- 限制框架中的fastjson版本 -->
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
<!-- JustAuth 传递依赖 fastjson:升级为 Fastjson2 的 1.x 兼容包,规避 1.x 反序列化风险 -->
|
||||
<fastjson.version>2.0.61</fastjson.version>
|
||||
<!-- 面向运行时的D-ORM依赖 -->
|
||||
<anyline.version>8.7.2-20250101</anyline.version>
|
||||
<!-- 工作流配置 -->
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Fastjson 1.2.83 漏洞整改(升级 Fastjson2 兼容包)
|
||||
|
||||
## 背景
|
||||
|
||||
客户通报 Fastjson `<= 1.2.83` 存在远程代码执行风险,要求排查整改。建议升级至 Fastjson2。
|
||||
|
||||
## 排查结论
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 业务序列化 | 使用 Jackson(`ruoyi-common-json`),业务代码未直接调用 Fastjson |
|
||||
| 引入路径 | `JustAuth 1.16.7`(`ruoyi-common-social`)传递依赖 `com.alibaba:fastjson:1.2.83` |
|
||||
| 影响功能 | 第三方社交登录/绑定、微信小程序登录(JustAuth) |
|
||||
|
||||
## 整改方案
|
||||
|
||||
采用 **Fastjson2 的 1.x 兼容包**(坐标仍为 `com.alibaba:fastjson`,版本切到 `2.0.x`),无需修改 JustAuth 调用代码。
|
||||
|
||||
修改位置:`backend/pom.xml`
|
||||
|
||||
```xml
|
||||
<!-- JustAuth 传递依赖 fastjson:升级为 Fastjson2 的 1.x 兼容包,规避 1.x 反序列化风险 -->
|
||||
<fastjson.version>2.0.61</fastjson.version>
|
||||
```
|
||||
|
||||
`dependencyManagement` 中已统一锁定该版本,JustAuth 传递依赖会解析为:
|
||||
|
||||
```
|
||||
JustAuth
|
||||
└─ com.alibaba:fastjson:2.0.61
|
||||
└─ com.alibaba.fastjson2:fastjson2-extension:2.0.61
|
||||
└─ com.alibaba.fastjson2:fastjson2:2.0.61
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
mvn -pl ruoyi-admin -am dependency:tree -Dincludes=com.alibaba:fastjson,com.alibaba.fastjson2:fastjson2
|
||||
```
|
||||
|
||||
确认结果中为 `com.alibaba:fastjson:jar:2.0.61`,且无 `1.2.83`。
|
||||
|
||||
## 回归建议
|
||||
|
||||
1. 账号密码登录(应不受影响)
|
||||
2. 实际启用的第三方登录/绑定(如有)
|
||||
3. 微信小程序登录(如有启用)
|
||||
|
||||
## 变更日期
|
||||
|
||||
2026-07-22
|
||||
@@ -0,0 +1,203 @@
|
||||
-- =============================================================================
|
||||
-- 学生基本信息 → 教务对接宽表视图
|
||||
-- 说明见:跟教务系统同步学生基础信息/学生基本信息对接说明.md
|
||||
--
|
||||
-- 口径摘要:
|
||||
-- * 身份证件类型:有 id_card 则「居民身份证」,否则 NULL
|
||||
-- * 所在校区:有有效宿舍分配则取 xg_dorm_campus.campus_name,否则 NULL
|
||||
-- * 班主任工号:class_teacher_id → sys_user.user_no(空则 user_name)
|
||||
-- * 学生是否在校:默认「在校」;退学(4)、毕业(5) 为「不在校」
|
||||
-- * 异动类型:固定 NULL
|
||||
-- * 毕业时间/毕业学年学期:在籍按 入学年份+学制 推算;已毕业(5)取 BI_YE 异动 effective_date
|
||||
-- * 学习经历/何年何月/在何地学习/在校照片:固定 NULL
|
||||
-- * 性别:0→男,1→女
|
||||
-- * 成员1/2:父亲/母亲及关系、电话
|
||||
-- * 学生当前状态:输出教务中文名称(见对接说明 §状态映射)
|
||||
-- =============================================================================
|
||||
|
||||
DROP VIEW IF EXISTS `vw_jw_student_export`;
|
||||
|
||||
CREATE VIEW `vw_jw_student_export` AS
|
||||
SELECT
|
||||
(m.`name` COLLATE utf8mb4_unicode_ci) AS `姓名`,
|
||||
(m.student_no COLLATE utf8mb4_unicode_ci) AS `学号`,
|
||||
(m.phone COLLATE utf8mb4_unicode_ci) AS `联系电话`,
|
||||
(CASE
|
||||
WHEN NULLIF(TRIM(m.id_card), '') IS NOT NULL
|
||||
THEN ('居民身份证' COLLATE utf8mb4_unicode_ci)
|
||||
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(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(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
|
||||
WHEN '1' THEN '在读'
|
||||
WHEN '2' THEN '休学'
|
||||
WHEN '3' THEN '复学'
|
||||
WHEN '4' THEN '退学'
|
||||
WHEN '5' THEN '毕业'
|
||||
WHEN '6' THEN '其他'
|
||||
WHEN '7' THEN '结业'
|
||||
WHEN '8' THEN '停学'
|
||||
WHEN '9' THEN '其他'
|
||||
ELSE COALESCE(m.student_status, '')
|
||||
END COLLATE utf8mb4_unicode_ci) AS `学生当前状态`,
|
||||
(CASE
|
||||
WHEN (m.student_status COLLATE utf8mb4_unicode_ci) IN (
|
||||
('4' COLLATE utf8mb4_unicode_ci),
|
||||
('5' COLLATE utf8mb4_unicode_ci)
|
||||
) THEN ('不在校' COLLATE utf8mb4_unicode_ci)
|
||||
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 `培养层次`,
|
||||
COALESCE(cm.school_system, m.school_system) AS `学制`,
|
||||
(
|
||||
CASE
|
||||
WHEN (m.student_status COLLATE utf8mb4_unicode_ci) = ('5' COLLATE utf8mb4_unicode_ci)
|
||||
AND grad_sc.effective_date IS NOT NULL
|
||||
THEN YEAR(grad_sc.effective_date)
|
||||
WHEN m.enrollment_year IS NOT NULL
|
||||
AND COALESCE(cm.school_system, m.school_system) IS NOT NULL
|
||||
THEN m.enrollment_year + COALESCE(cm.school_system, m.school_system)
|
||||
WHEN cm.graduation_year IS NOT NULL
|
||||
THEN cm.graduation_year
|
||||
ELSE NULL
|
||||
END
|
||||
) AS `毕业时间`,
|
||||
(
|
||||
CASE
|
||||
WHEN (
|
||||
CASE
|
||||
WHEN (m.student_status COLLATE utf8mb4_unicode_ci) = ('5' COLLATE utf8mb4_unicode_ci)
|
||||
AND grad_sc.effective_date IS NOT NULL
|
||||
THEN YEAR(grad_sc.effective_date)
|
||||
WHEN m.enrollment_year IS NOT NULL
|
||||
AND COALESCE(cm.school_system, m.school_system) IS NOT NULL
|
||||
THEN m.enrollment_year + COALESCE(cm.school_system, m.school_system)
|
||||
WHEN cm.graduation_year IS NOT NULL
|
||||
THEN cm.graduation_year
|
||||
ELSE NULL
|
||||
END
|
||||
) IS NOT NULL
|
||||
THEN CONCAT(
|
||||
(
|
||||
CASE
|
||||
WHEN (m.student_status COLLATE utf8mb4_unicode_ci) = ('5' COLLATE utf8mb4_unicode_ci)
|
||||
AND grad_sc.effective_date IS NOT NULL
|
||||
THEN YEAR(grad_sc.effective_date)
|
||||
WHEN m.enrollment_year IS NOT NULL
|
||||
AND COALESCE(cm.school_system, m.school_system) IS NOT NULL
|
||||
THEN m.enrollment_year + COALESCE(cm.school_system, m.school_system)
|
||||
WHEN cm.graduation_year IS NOT NULL
|
||||
THEN cm.graduation_year
|
||||
ELSE NULL
|
||||
END
|
||||
) - 1,
|
||||
'-',
|
||||
(
|
||||
CASE
|
||||
WHEN (m.student_status COLLATE utf8mb4_unicode_ci) = ('5' COLLATE utf8mb4_unicode_ci)
|
||||
AND grad_sc.effective_date IS NOT NULL
|
||||
THEN YEAR(grad_sc.effective_date)
|
||||
WHEN m.enrollment_year IS NOT NULL
|
||||
AND COALESCE(cm.school_system, m.school_system) IS NOT NULL
|
||||
THEN m.enrollment_year + COALESCE(cm.school_system, m.school_system)
|
||||
WHEN cm.graduation_year IS NOT NULL
|
||||
THEN cm.graduation_year
|
||||
ELSE NULL
|
||||
END
|
||||
),
|
||||
'-2'
|
||||
)
|
||||
ELSE NULL
|
||||
END
|
||||
) AS `毕业学年学期`,
|
||||
(CAST(NULL AS CHAR(500) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `学习经历`,
|
||||
(CAST(NULL AS CHAR(128) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `何年何月-何年何月`,
|
||||
(CAST(NULL AS CHAR(256) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `在何地学习`,
|
||||
(CASE m.gender
|
||||
WHEN '0' THEN '男'
|
||||
WHEN '1' THEN '女'
|
||||
ELSE NULL
|
||||
END COLLATE utf8mb4_unicode_ci) AS `性别`,
|
||||
(m.native_place COLLATE utf8mb4_unicode_ci) AS `籍贯地`,
|
||||
(CAST(NULL AS CHAR(500) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `在校照片`,
|
||||
DATE(m.birth_date) AS `出生日期`,
|
||||
(COALESCE(NULLIF(TRIM(m.home_address), ''), m.current_address) COLLATE utf8mb4_unicode_ci) AS `家庭现住址`,
|
||||
(m.father_name COLLATE utf8mb4_unicode_ci) AS `成员1姓名`,
|
||||
(m.mother_name COLLATE utf8mb4_unicode_ci) AS `成员2姓名`,
|
||||
(CASE
|
||||
WHEN NULLIF(TRIM(m.father_name), '') IS NOT NULL
|
||||
THEN ('父亲' COLLATE utf8mb4_unicode_ci)
|
||||
ELSE (CAST(NULL AS CHAR(16) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci)
|
||||
END) AS `成员1关系`,
|
||||
(CASE
|
||||
WHEN NULLIF(TRIM(m.mother_name), '') IS NOT NULL
|
||||
THEN ('母亲' COLLATE utf8mb4_unicode_ci)
|
||||
ELSE (CAST(NULL AS CHAR(16) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci)
|
||||
END) AS `成员2关系`,
|
||||
(m.father_phone COLLATE utf8mb4_unicode_ci) AS `成员1联系电话`,
|
||||
(m.mother_phone COLLATE utf8mb4_unicode_ci) AS `成员2联系电话`,
|
||||
m.id AS `来源记录id`
|
||||
FROM xg_student_msg m
|
||||
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_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,
|
||||
MAX(sc.effective_date) AS effective_date
|
||||
FROM xg_student_status_change sc
|
||||
WHERE sc.deleted = 0
|
||||
AND sc.process_status = 1
|
||||
AND (sc.change_type COLLATE utf8mb4_unicode_ci) = ('BI_YE' COLLATE utf8mb4_unicode_ci)
|
||||
GROUP BY sc.student_id
|
||||
) grad_sc ON grad_sc.student_id = m.id
|
||||
WHERE m.deleted = 0;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 授权:用户 jw_cx 仅查询本视图(需使用具备 GRANT 权限的账号执行,如 root)
|
||||
-- -----------------------------------------------------------------------------
|
||||
GRANT SELECT ON `hzdyjs_xg`.`vw_jw_student_export` TO 'jw_cx'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
@@ -0,0 +1,78 @@
|
||||
# 学生基本信息 — 教务对接说明
|
||||
|
||||
## 接口
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 路径 | `GET /api/v1/students` |
|
||||
| 鉴权 | Header `X-Jw-Cx-Key` |
|
||||
| 数据范围 | 全部 `xg_student_msg.deleted = 0` 的学生,**分页返回** |
|
||||
| 视图 DDL | `docs/设计文档/database/vw_jw_student_export.sql` |
|
||||
| 服务代码 | `跟教务系统同步学生异动信息/api_service/app.py`(与异动接口同进程) |
|
||||
|
||||
## 字段口径(已确认)
|
||||
|
||||
| 教务字段 | 口径 |
|
||||
|----------|------|
|
||||
| 身份证件类型 | 有 `id_card` →「居民身份证」,否则 `null` |
|
||||
| 所在校区 | 有有效宿舍分配(`xg_dorm_bed_student.status=0`)→ `xg_dorm_campus.campus_name`,否则 `null` |
|
||||
| 班主任工号 | `class_teacher_id` → `sys_user.user_no`,空则 `user_name` |
|
||||
| 学生是否在校 | 默认「在校」;学籍状态为 **退学(4)、毕业(5)** →「不在校」 |
|
||||
| 异动类型 | 固定 `null` |
|
||||
| 毕业时间 | 年份整数。已毕业(5):取 `BI_YE` 已处理异动的 `effective_date` 年份;否则 `入学年份 + 学制`(或班级 `graduation_year`) |
|
||||
| 毕业学年学期 | `{毕业年-1}-{毕业年}-2`,例如 `2025-2026-2`;无法推算则为 `null` |
|
||||
| 学习经历 / 何年何月-何年何月 / 在何地学习 | 固定 `null` |
|
||||
| 在校照片 | 固定 `null` |
|
||||
| 性别 | `0`→「男」,`1`→「女」 |
|
||||
| 成员1 | 父亲姓名/关系「父亲」/ `father_phone` |
|
||||
| 成员2 | 母亲姓名/关系「母亲」/ `mother_phone` |
|
||||
|
||||
## 学生当前状态映射(本系统 → 教务中文名)
|
||||
|
||||
| 本系统 `student_status` | 本系统标签 | 接口输出(教务中文名) |
|
||||
|-------------------------|------------|------------------------|
|
||||
| 1 | 在读 | 在读 |
|
||||
| 2 | 休学 | 休学 |
|
||||
| 3 | 复学 | 复学 |
|
||||
| 4 | 退学 | 退学 |
|
||||
| 5 | 毕业 | 毕业 |
|
||||
| 6 | 不在籍 | 其他 |
|
||||
| 7 | 结业 | 结业 |
|
||||
| 8 | 停学 | 停学 |
|
||||
| 9 | 分流 | 其他 |
|
||||
|
||||
教务附件2 字典值(供联调参考,接口当前输出**中文名称**):
|
||||
|
||||
| 教务状态名称 | 字典值 |
|
||||
|--------------|--------|
|
||||
| 在读 | XSDQZT016 |
|
||||
| 休学 | XSDQZT015 |
|
||||
| 退学 | XSDQZT014 |
|
||||
| 停学 | XSDQZT013 |
|
||||
| 复学 | XSDQZT012 |
|
||||
| 毕业 | XSDQZT010 |
|
||||
| 结业 | XSDQZT009 |
|
||||
| 其他 | XSDQZT007 |
|
||||
|
||||
## 部署
|
||||
|
||||
```bash
|
||||
# 1. 建视图
|
||||
mysql -h <host> -u root -p hzdyjs_xg < docs/设计文档/database/vw_jw_student_export.sql
|
||||
|
||||
# 2. 重启 API(与异动接口相同进程)
|
||||
export JW_CX_CONFIG=/path/to/config.prod.ini
|
||||
python app.py
|
||||
|
||||
# 3. 调用示例
|
||||
curl -s -H "X-Jw-Cx-Key: JWCX001" \
|
||||
"http://127.0.0.1:5050/api/v1/students?page=1&page_size=500"
|
||||
```
|
||||
|
||||
响应含 `total`(总条数)、`page`、`page_size`、`total_pages`,教务可按页循环直至 `page >= total_pages`。
|
||||
|
||||
## 与异动接口关系
|
||||
|
||||
- 异动接口:`/api/v1/student-changes?year=2026`(按异动生效年份)
|
||||
- 学生基本信息:`/api/v1/students?page=1&page_size=500`(分页,含 total / total_pages)
|
||||
- 毕业时间在学生接口中仍可使用;**毕业记录不再通过异动接口同步**(异动接口已排除 `BI_YE`)
|
||||
Binary file not shown.
Binary file not shown.
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
教务同步只读 API 联调脚本(Python 3.7+,仅标准库)
|
||||
|
||||
调用两个业务接口并打印成功结果:
|
||||
- GET /api/v1/students 学生基本信息(第 1 页)
|
||||
- GET /api/v1/student-changes 学生异动(按年)
|
||||
|
||||
用法:
|
||||
python test_jw_cx_api.py --prod
|
||||
python test_jw_cx_api.py --base-url http://web.hzdyjsxy.cn:18081/jw-cx-api --year 2026
|
||||
|
||||
环境变量:JW_CX_BASE_URL、JW_CX_API_KEY
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
HEADER_API_KEY = "X-Jw-Cx-Key"
|
||||
DEFAULT_BASE_URL = "http://127.0.0.1:5050"
|
||||
PROD_BASE_URL = "http://web.hzdyjsxy.cn:18081"
|
||||
DEFAULT_API_KEY = "JWCX001"
|
||||
|
||||
|
||||
def _get_json(url, api_key, timeout=120):
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
req.add_header(HEADER_API_KEY, api_key)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
body = resp.read().decode("utf-8")
|
||||
return resp.status, json.loads(body)
|
||||
|
||||
|
||||
def _join_url(base, path):
|
||||
return base.rstrip("/") + "/" + path.lstrip("/")
|
||||
|
||||
|
||||
def fetch_students(base_url, api_key, page=1, page_size=10):
|
||||
qs = urllib.parse.urlencode({"page": page, "page_size": page_size})
|
||||
url = _join_url(base_url, "/api/v1/students?" + qs)
|
||||
status, data = _get_json(url, api_key)
|
||||
if status != 200 or data.get("code") != 200:
|
||||
raise RuntimeError("学生基本信息接口失败: status=%s body=%s" % (status, data))
|
||||
return data
|
||||
|
||||
|
||||
def fetch_student_changes(base_url, api_key, year):
|
||||
qs = urllib.parse.urlencode({"year": year})
|
||||
url = _join_url(base_url, "/api/v1/student-changes?" + qs)
|
||||
status, data = _get_json(url, api_key)
|
||||
if status != 200 or data.get("code") != 200:
|
||||
raise RuntimeError("学生异动接口失败: status=%s body=%s" % (status, data))
|
||||
return data
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="教务同步 API:获取学生信息与学生异动")
|
||||
parser.add_argument("--base-url", default=None, help="API 根地址")
|
||||
parser.add_argument("--prod", action="store_true", help="使用 %s" % PROD_BASE_URL)
|
||||
parser.add_argument(
|
||||
"--api-key",
|
||||
default=os.environ.get("JW_CX_API_KEY", DEFAULT_API_KEY),
|
||||
help="请求头 %s" % HEADER_API_KEY,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--year",
|
||||
type=int,
|
||||
default=None,
|
||||
help="异动筛选年份,默认当前自然年",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--page-size",
|
||||
type=int,
|
||||
default=10,
|
||||
help="学生基本信息每页条数,默认 10",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.base_url:
|
||||
base_url = args.base_url
|
||||
elif args.prod:
|
||||
base_url = PROD_BASE_URL
|
||||
else:
|
||||
base_url = os.environ.get("JW_CX_BASE_URL", DEFAULT_BASE_URL)
|
||||
|
||||
year = args.year if args.year is not None else time.localtime().tm_year
|
||||
|
||||
print("BASE_URL: %s" % base_url)
|
||||
print("")
|
||||
|
||||
try:
|
||||
students = fetch_students(base_url, args.api_key, page=1, page_size=args.page_size)
|
||||
print("=== 学生基本信息 GET /api/v1/students ===")
|
||||
print(
|
||||
"total=%s page=%s/%s 本页=%s条"
|
||||
% (
|
||||
students.get("total"),
|
||||
students.get("page"),
|
||||
students.get("total_pages"),
|
||||
len(students.get("data") or []),
|
||||
)
|
||||
)
|
||||
if students.get("data"):
|
||||
print("首条:", json.dumps(students["data"][0], ensure_ascii=False, indent=2))
|
||||
print("")
|
||||
|
||||
changes = fetch_student_changes(base_url, args.api_key, year)
|
||||
print("=== 学生异动 GET /api/v1/student-changes?year=%s ===" % year)
|
||||
print("total=%s filter_year=%s" % (changes.get("total"), changes.get("filter_year")))
|
||||
if changes.get("data"):
|
||||
print("首条:", json.dumps(changes["data"][0], ensure_ascii=False, indent=2))
|
||||
elif changes.get("total") == 0:
|
||||
print("(当年无异动记录)")
|
||||
|
||||
except (urllib.error.URLError, urllib.error.HTTPError, RuntimeError, ValueError) as e:
|
||||
print("调用失败: %s" % e, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("")
|
||||
print("调用成功")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,402 @@
|
||||
# 教务系统对接 — 学工数据只读 API 调用说明
|
||||
|
||||
本文档面向**教务系统**开发/联调人员,说明如何调用学工系统提供的学生基本信息、学生学籍异动两类只读接口。
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 协议 | HTTP GET |
|
||||
| 数据格式 | JSON,`Content-Type: application/json` |
|
||||
| 字符编码 | UTF-8 |
|
||||
| 鉴权方式 | 请求头 `X-Jw-Cx-Key`(由学工方分配) |
|
||||
| 接口数量 | 2 个业务接口 + 1 个健康检查 |
|
||||
|
||||
| 接口 | 路径 | 用途 |
|
||||
|------|------|------|
|
||||
| 学生基本信息 | `GET /api/v1/students` | 分页拉取全部在库学生快照 |
|
||||
| 学生学籍异动 | `GET /api/v1/student-changes` | 按自然年拉取已办结异动 |
|
||||
| 健康检查 | `GET /health` | 探测服务是否存活(无需密钥) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 访问地址
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| Base URL | `http://web.hzdyjsxy.cn:18081/jw-cx-api` |
|
||||
|
||||
完整示例:
|
||||
|
||||
```
|
||||
http://web.hzdyjsxy.cn:18081/jw-cx-api/api/v1/students?page=1
|
||||
http://web.hzdyjsxy.cn:18081/jw-cx-api/api/v1/student-changes?year=2026
|
||||
http://web.hzdyjsxy.cn:18081/jw-cx-api/health
|
||||
```
|
||||
|
||||
> 若域名、端口或路径前缀有调整,以学工方最新通知为准。
|
||||
|
||||
---
|
||||
|
||||
## 3. 鉴权
|
||||
|
||||
除 `/health` 外,所有业务接口必须在 **HTTP Header** 中携带:
|
||||
|
||||
```http
|
||||
X-Jw-Cx-Key: <学工方分配的密钥>
|
||||
```
|
||||
|
||||
| 情况 | HTTP 状态 | 响应示例 |
|
||||
|------|-----------|----------|
|
||||
| 未携带或密钥错误 | 401 | `{"code":401,"msg":"无效的密钥或未提供请求头 X-Jw-Cx-Key"}` |
|
||||
| 参数错误 | 400 | `{"code":400,"msg":"..."}` |
|
||||
| 成功 | 200 | `{"code":200,"msg":"success",...}` |
|
||||
| 服务端异常 | 500 | `{"code":500,"msg":"数据库查询失败",...}` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 统一响应结构
|
||||
|
||||
### 4.1 成功
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": [ /* 记录数组 */ ],
|
||||
"total": 100
|
||||
}
|
||||
```
|
||||
|
||||
各接口还会在顶层返回分页或筛选元数据(见下文)。
|
||||
|
||||
### 4.2 字段值约定
|
||||
|
||||
- 日期:`YYYY-MM-DD`(如 `"2026-04-21"`)
|
||||
- 日期时间:`YYYY-MM-DD HH:mm:ss`
|
||||
- 无数据:JSON `null`(不是空字符串 `""`)
|
||||
- 异动类型、学生当前状态等枚举:**中文名称**(非内部代码)
|
||||
|
||||
---
|
||||
|
||||
## 5. 接口一:学生基本信息
|
||||
|
||||
### 5.1 请求
|
||||
|
||||
```
|
||||
GET /api/v1/students
|
||||
```
|
||||
|
||||
**Query 参数**
|
||||
|
||||
| 参数 | 必填 | 说明 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| `page` | 否 | 页码,从 1 开始 | `1` |
|
||||
| `page_size` | 否 | 每页条数,范围 1~5000 | `500` |
|
||||
|
||||
**示例**
|
||||
|
||||
```http
|
||||
GET /api/v1/students?page=1&page_size=500
|
||||
X-Jw-Cx-Key: JWCX001
|
||||
```
|
||||
|
||||
### 5.2 响应元数据
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `total` | 学生总条数 |
|
||||
| `page` | 当前页码 |
|
||||
| `page_size` | 本页大小 |
|
||||
| `total_pages` | 总页数 |
|
||||
|
||||
### 5.3 拉取全量建议流程
|
||||
|
||||
学生数量可能达到数万条,**必须分页循环**:
|
||||
|
||||
```
|
||||
1. 请求 page=1,读取 total、total_pages
|
||||
2. 处理 data 数组
|
||||
3. page 递增,直到 page >= total_pages
|
||||
```
|
||||
|
||||
伪代码:
|
||||
|
||||
```python
|
||||
page = 1
|
||||
page_size = 500
|
||||
while True:
|
||||
resp = GET(f"/api/v1/students?page={page}&page_size={page_size}")
|
||||
# 入库或处理 resp["data"]
|
||||
if page >= resp["total_pages"]:
|
||||
break
|
||||
page += 1
|
||||
```
|
||||
|
||||
### 5.4 `data` 每条记录字段
|
||||
|
||||
| 字段名 | 必填(教务) | 说明 |
|
||||
|--------|--------------|------|
|
||||
| 姓名 | 是 | |
|
||||
| 学号 | 是 | |
|
||||
| 联系电话 | 否 | |
|
||||
| 身份证件类型 | 否 | 有身份证号时为「居民身份证」,否则 `null` |
|
||||
| 身份证件号 | 否 | |
|
||||
| 班级信息 | 是 | 班级名称 |
|
||||
| 所在校区 | 是 | 有宿舍分配时取校区名,否则 `null` |
|
||||
| 所在年级 | 是 | |
|
||||
| 入学年份 | 是 | 整数,如 `2024` |
|
||||
| 系部信息 | 是 | 院系名称 |
|
||||
| 专业信息 | 是 | 专业名称 |
|
||||
| 班主任工号 | 是 | |
|
||||
| 班主任姓名 | 是 | |
|
||||
| 学生当前状态 | 是 | 教务中文名:在读、休学、复学、退学、毕业、结业、停学、其他 |
|
||||
| 学生是否在校 | 是 | 「在校」或「不在校」;退学、毕业为不在校 |
|
||||
| 异动类型 | 否 | 固定 `null` |
|
||||
| 培养层次 | 否 | 如三年制技师、六年制技师等 |
|
||||
| 学制 | 是 | 年数,整数 |
|
||||
| 毕业时间 | 是 | **年份**整数;已毕业取异动生效年,否则按入学年+学制推算 |
|
||||
| 毕业学年学期 | 是 | 如 `2025-2026-2`;无法推算时为 `null` |
|
||||
| 学习经历 | 否 | 固定 `null` |
|
||||
| 何年何月-何年何月 | 否 | 固定 `null` |
|
||||
| 在何地学习 | 否 | 固定 `null` |
|
||||
| 性别 | 是 | 「男」或「女」 |
|
||||
| 籍贯地 | 否 | |
|
||||
| 在校照片 | 否 | 固定 `null` |
|
||||
| 出生日期 | 否 | `YYYY-MM-DD` |
|
||||
| 家庭现住址 | 否 | |
|
||||
| 成员1姓名 | 否 | 父亲姓名 |
|
||||
| 成员2姓名 | 否 | 母亲姓名 |
|
||||
| 成员1关系 | 否 | 有父亲姓名时为「父亲」 |
|
||||
| 成员2关系 | 否 | 有母亲姓名时为「母亲」 |
|
||||
| 成员1联系电话 | 否 | 父亲电话 |
|
||||
| 成员2联系电话 | 否 | 母亲电话 |
|
||||
|
||||
### 5.5 响应示例(节选)
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{
|
||||
"姓名": "张三",
|
||||
"学号": "2024010101",
|
||||
"联系电话": "13800138000",
|
||||
"身份证件类型": "居民身份证",
|
||||
"身份证件号": "3301************",
|
||||
"班级信息": "24技师1班",
|
||||
"所在校区": "西溪校区",
|
||||
"所在年级": "2024",
|
||||
"入学年份": 2024,
|
||||
"系部信息": "电气工程系",
|
||||
"专业信息": "电气工程",
|
||||
"班主任工号": "T001",
|
||||
"班主任姓名": "李老师",
|
||||
"学生当前状态": "在读",
|
||||
"学生是否在校": "在校",
|
||||
"异动类型": null,
|
||||
"培养层次": "三年制技师",
|
||||
"学制": 3,
|
||||
"毕业时间": 2027,
|
||||
"毕业学年学期": "2026-2027-2",
|
||||
"性别": "男"
|
||||
}
|
||||
],
|
||||
"total": 12500,
|
||||
"page": 1,
|
||||
"page_size": 500,
|
||||
"total_pages": 25
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 接口二:学生学籍异动
|
||||
|
||||
### 6.1 请求
|
||||
|
||||
```
|
||||
GET /api/v1/student-changes
|
||||
```
|
||||
|
||||
**Query 参数**
|
||||
|
||||
| 参数 | 必填 | 说明 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| `year` | 否 | 按「异动生效时间」所在**自然年**筛选,范围 2000~2100 | 当前自然年 |
|
||||
|
||||
**示例**
|
||||
|
||||
```http
|
||||
GET /api/v1/student-changes?year=2026
|
||||
X-Jw-Cx-Key: JWCX001
|
||||
```
|
||||
|
||||
### 6.2 数据范围说明
|
||||
|
||||
- 仅返回 **已办结** 记录(学籍异动 `process_status=1`,换班级 `status=1`)
|
||||
- 按 **`异动生效时间` 的年份** 过滤(非提交时间)
|
||||
- **不分页**,一次返回该年全部记录(数据量通常远小于学生全量)
|
||||
- **不包含毕业(BI_YE)** 记录
|
||||
- **去重**:同一学号、同一登记日(`提交时间`)、同一异动类型仅保留一条;视图与 API 双层去重
|
||||
|
||||
### 6.3 响应元数据
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `total` | 本次返回条数(等于 `data` 长度) |
|
||||
| `filter_year` | 实际筛选年份 |
|
||||
| `filter_by_column` | 固定为 `异动生效时间` |
|
||||
|
||||
### 6.4 `data` 每条记录字段
|
||||
|
||||
| 字段名 | 说明 |
|
||||
|--------|------|
|
||||
| 提交时间 | `YYYY-MM-DD` |
|
||||
| 异动生效学期 | 固定 `null`(本系统无学年学期) |
|
||||
| 异动类型 | 教务中文名:**退学、休学、复学、转班级** |
|
||||
| 申请人学号 | |
|
||||
| 姓名 | |
|
||||
| 异动前所在年级 / 院系 / 专业 / 班级 / 学制 | |
|
||||
| 异动前所在校区 | 固定 `null` |
|
||||
| 异动后所在年级 / 班级 / 院系 / 专业 / 学制 | |
|
||||
| 异动后所在校区 | 固定 `null` |
|
||||
| 异动后班主任工号 / 姓名 | |
|
||||
| 新名字 | 固定 `null` |
|
||||
| 审核状态 | 固定「已通过」 |
|
||||
|
||||
### 6.5 异动类型说明
|
||||
|
||||
| 接口返回值 | 含义 |
|
||||
|------------|------|
|
||||
| 退学 | 学籍异动-退学 |
|
||||
| 休学 | 学籍异动-休学 |
|
||||
| 复学 | 学籍异动-复学 |
|
||||
| 转班级 | 换班级记录 |
|
||||
|
||||
> 本系统**不同步**毕业异动;毕业相关信息请从「学生基本信息」接口获取。
|
||||
|
||||
### 6.6 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{
|
||||
"提交时间": "2026-03-15",
|
||||
"异动生效学期": null,
|
||||
"异动类型": "休学",
|
||||
"申请人学号": "2024010101",
|
||||
"姓名": "张三",
|
||||
"异动前所在年级": "2024",
|
||||
"异动前所在校区": null,
|
||||
"异动前所在院系": "电气工程系",
|
||||
"异动前所在专业": "电气工程",
|
||||
"异动前所在班级": "24技师1班",
|
||||
"异动前学制": 3,
|
||||
"异动后所在校区": null,
|
||||
"异动后所在年级": "2024",
|
||||
"异动后所在班级": "24技师1班",
|
||||
"异动后所在院系": "电气工程系",
|
||||
"异动后所在专业": "电气工程",
|
||||
"异动后学制": 3,
|
||||
"异动后班主任工号": "T001",
|
||||
"异动后班主任姓名": "李老师",
|
||||
"新名字": null,
|
||||
"审核状态": "已通过"
|
||||
}
|
||||
],
|
||||
"total": 12,
|
||||
"filter_year": 2026,
|
||||
"filter_by_column": "异动生效时间"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 调用示例
|
||||
|
||||
### 7.1 cURL
|
||||
|
||||
```bash
|
||||
# 健康检查
|
||||
curl -s "http://web.hzdyjsxy.cn:18081/jw-cx-api/health"
|
||||
|
||||
# 学生基本信息(第 1 页)
|
||||
curl -s -H "X-Jw-Cx-Key: JWCX001" \
|
||||
"http://web.hzdyjsxy.cn:18081/jw-cx-api/api/v1/students?page=1&page_size=500"
|
||||
|
||||
# 学生异动(2026 年)
|
||||
curl -s -H "X-Jw-Cx-Key: JWCX001" \
|
||||
"http://web.hzdyjsxy.cn:18081/jw-cx-api/api/v1/student-changes?year=2026"
|
||||
```
|
||||
|
||||
### 7.2 Postman
|
||||
|
||||
1. Method 选 **GET**
|
||||
2. URL 填入完整地址(含 Query)
|
||||
3. **Headers** 增加一行:
|
||||
- Key: `X-Jw-Cx-Key`
|
||||
- Value: 学工分配的密钥
|
||||
4. 点击 Send
|
||||
|
||||
### 7.3 Python(标准库)
|
||||
|
||||
项目内提供联调脚本:
|
||||
|
||||
```bash
|
||||
cd 跟教务系统同步学生异动信息/api_service
|
||||
python test_jw_cx_api.py --base-url http://web.hzdyjsxy.cn:18081/jw-cx-api --year 2026
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 联调检查清单
|
||||
|
||||
| 步骤 | 操作 |
|
||||
|------|------|
|
||||
| 1 | 访问 `/health` 返回 `{"status":"ok"}` |
|
||||
| 2 | 不带密钥访问业务接口,应返回 401 |
|
||||
| 3 | 带正确密钥访问 `/api/v1/students?page=1`,`code=200` 且含 `total`、`total_pages` |
|
||||
| 4 | 循环分页直至拉取全部学生 |
|
||||
| 5 | 带密钥访问 `/api/v1/student-changes?year=2026`,核对异动类型中文名 |
|
||||
| 6 | 确认 `null` 字段(校区、异动生效学期等)教务侧可接受 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 常见问题
|
||||
|
||||
**Q:为什么 80 端口访问 404?**
|
||||
A:公网 API 走 **18081** 端口及 `/jw-cx-api` 路径,请使用本文 2.1 节地址。
|
||||
|
||||
**Q:学生接口为什么要分页?**
|
||||
A:全量学生可能数万条,单次响应过大易超时;请按 `total_pages` 逐页拉取。
|
||||
|
||||
**Q:异动接口为何没有毕业?**
|
||||
A:按对接约定,毕业不同步至异动接口;毕业状态与毕业时间见学生基本信息接口。
|
||||
|
||||
**Q:「复学并换班级」会有几条异动?**
|
||||
A:可能同时存在「复学」「转班级」两条记录,请按学号+提交时间处理。
|
||||
|
||||
**Q:密钥如何更换?**
|
||||
A:由学工方在服务端配置 `[api] api_key` 后通知教务更新 Header。
|
||||
|
||||
---
|
||||
|
||||
## 10. 相关文档
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [README.md](./README.md) | 服务部署与运维(学工侧) |
|
||||
| [../异动类型映射说明.md](../异动类型映射说明.md) | 异动类型详细映射 |
|
||||
| [../../跟教务系统同步学生基础信息/学生基本信息对接说明.md](../../跟教务系统同步学生基础信息/学生基本信息对接说明.md) | 学生字段口径与状态映射 |
|
||||
| [test_jw_cx_api.py](./test_jw_cx_api.py) | 联调测试脚本 |
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:2026-07-02
|
||||
**维护**:学工系统开发组(接口变更时请同步更新本文档)
|
||||
Reference in new issue
Block a user