This commit is contained in:
zlwu committed 2026-07-22 09:51:31 +08:00
1 parent dd6212400c
commit f3869785ff
9 files changed
+870 -3

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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`)
@@ -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
**维护**:学工系统开发组(接口变更时请同步更新本文档)