diff --git a/backend/README.md b/backend/README.md index 5d6dc9c0..199ac325 100644 --- a/backend/README.md +++ b/backend/README.md @@ -68,7 +68,7 @@ Mall4J 高质量Java商城系统 - https://www.mall4j.com/cn/?statId=11
| 数据库主键 | 采用 雪花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 基于数据库锁性能差 集群需要做很多配置与改造 | diff --git a/backend/pom.xml b/backend/pom.xml index 261234a4..417a8c33 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -43,8 +43,8 @@ 2.28.22 3.3.4 - - 1.2.83 + + 2.0.61 8.7.2-20250101 diff --git a/docs/技术问题解决记录/Fastjson1.2.83漏洞整改-升级Fastjson2兼容包.md b/docs/技术问题解决记录/Fastjson1.2.83漏洞整改-升级Fastjson2兼容包.md new file mode 100644 index 00000000..5da03810 --- /dev/null +++ b/docs/技术问题解决记录/Fastjson1.2.83漏洞整改-升级Fastjson2兼容包.md @@ -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 + +2.0.61 +``` + +`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 diff --git a/docs/设计文档/database/vw_jw_student_export.sql b/docs/设计文档/database/vw_jw_student_export.sql new file mode 100644 index 00000000..52ecd432 --- /dev/null +++ b/docs/设计文档/database/vw_jw_student_export.sql @@ -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; diff --git a/跟教务系统同步学生基础信息/学生基本信息对接说明.md b/跟教务系统同步学生基础信息/学生基本信息对接说明.md new file mode 100644 index 00000000..2940056f --- /dev/null +++ b/跟教务系统同步学生基础信息/学生基本信息对接说明.md @@ -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 -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`) diff --git a/跟教务系统同步学生基础信息/学生当前状态(附件2).xlsx b/跟教务系统同步学生基础信息/学生当前状态(附件2).xlsx new file mode 100644 index 00000000..461d1b23 Binary files /dev/null and b/跟教务系统同步学生基础信息/学生当前状态(附件2).xlsx differ diff --git a/跟教务系统同步学生基础信息/第一技师学生基本信息.docx b/跟教务系统同步学生基础信息/第一技师学生基本信息.docx new file mode 100644 index 00000000..8956958e Binary files /dev/null and b/跟教务系统同步学生基础信息/第一技师学生基本信息.docx differ diff --git a/跟教务系统同步学生异动信息/api_service/test_jw_cx_api.py b/跟教务系统同步学生异动信息/api_service/test_jw_cx_api.py new file mode 100755 index 00000000..39e7fb74 --- /dev/null +++ b/跟教务系统同步学生异动信息/api_service/test_jw_cx_api.py @@ -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() diff --git a/跟教务系统同步学生异动信息/api_service/学生信息接口和学生异动接口调用说明.md b/跟教务系统同步学生异动信息/api_service/学生信息接口和学生异动接口调用说明.md new file mode 100644 index 00000000..15fd3b2f --- /dev/null +++ b/跟教务系统同步学生异动信息/api_service/学生信息接口和学生异动接口调用说明.md @@ -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 +**维护**:学工系统开发组(接口变更时请同步更新本文档)