This commit is contained in:
zlwu committed 2026-07-03 11:25:45 +08:00
1 parent 10f90d3824
commit dd6212400c
7 files changed
+364 -55

No files matched your search

@@ -8,6 +8,7 @@
-- * 「异动生效时间」:学籍异动 = effective_date;换班级 = effective_time(API 按年筛选)
-- * 「异动类型」:输出教务系统中文名称(见 §异动类型映射)
-- * 仅导出已处理:学籍 process_status = 1;换班级 status = '1'
-- * 去重:同一展示学号 + 登记日 + 异动类型 仅保留 id 最小的一条(学号取 COALESCE(sc,m))
-- * 学籍异动「异动前」:优先 xg_student_msg_his(处理脚本落库快照),否则回退 xg_student_msg
-- * 学籍异动「异动后」:取自当前 xg_student_msg(纯学籍异动组织维度通常不变)
-- * 末尾「数据来源」「来源记录id」供联调,API 层不对外返回
@@ -25,6 +26,32 @@
DROP VIEW IF EXISTS `vw_jw_student_change_export`;
CREATE VIEW `vw_jw_student_change_export` AS
SELECT
ANY_VALUE(`提交时间`) AS `提交时间`,
ANY_VALUE(`异动生效学期`) AS `异动生效学期`,
ANY_VALUE(`异动生效时间`) AS `异动生效时间`,
ANY_VALUE(`异动类型`) AS `异动类型`,
ANY_VALUE(`申请人学号`) AS `申请人学号`,
ANY_VALUE(`姓名`) AS `姓名`,
ANY_VALUE(`异动前所在年级`) AS `异动前所在年级`,
ANY_VALUE(`异动前所在校区`) AS `异动前所在校区`,
ANY_VALUE(`异动前所在院系`) AS `异动前所在院系`,
ANY_VALUE(`异动前所在专业`) AS `异动前所在专业`,
ANY_VALUE(`异动前所在班级`) AS `异动前所在班级`,
ANY_VALUE(`异动前学制`) AS `异动前学制`,
ANY_VALUE(`异动后所在校区`) AS `异动后所在校区`,
ANY_VALUE(`异动后所在年级`) AS `异动后所在年级`,
ANY_VALUE(`异动后所在班级`) AS `异动后所在班级`,
ANY_VALUE(`异动后所在院系`) AS `异动后所在院系`,
ANY_VALUE(`异动后所在专业`) AS `异动后所在专业`,
ANY_VALUE(`异动后学制`) AS `异动后学制`,
ANY_VALUE(`异动后班主任工号`) AS `异动后班主任工号`,
ANY_VALUE(`异动后班主任姓名`) AS `异动后班主任姓名`,
ANY_VALUE(`新名字`) AS `新名字`,
ANY_VALUE(`审核状态`) AS `审核状态`,
ANY_VALUE(`数据来源`) AS `数据来源`,
`来源记录id`
FROM (
SELECT
DATE(COALESCE(sc.apply_date, sc.create_time)) AS `提交时间`,
(CAST(NULL AS CHAR(32) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `异动生效学期`,
@@ -33,9 +60,9 @@ SELECT
WHEN 'TUI_XUE' THEN '退学'
WHEN 'XIU_XUE' THEN '休学'
WHEN 'FU_XUE' THEN '复学'
ELSE COALESCE(sc.change_type, '')
ELSE COALESCE(sc.change_type COLLATE utf8mb4_unicode_ci, '' COLLATE utf8mb4_unicode_ci)
END COLLATE utf8mb4_unicode_ci) AS `异动类型`,
(COALESCE(sc.student_no, m.student_no) COLLATE utf8mb4_unicode_ci) AS `申请人学号`,
(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 `异动前所在校区`,
@@ -49,13 +76,38 @@ SELECT
(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.school_system, m.school_system, cm_before.school_system, mh.school_system) AS `异动后学制`,
(COALESCE(NULLIF(TRIM(su.user_no), ''), su.user_name) COLLATE utf8mb4_unicode_ci) AS `异动后班主任工号`,
(COALESCE(
NULLIF(TRIM(su.user_no COLLATE utf8mb4_unicode_ci), '' COLLATE utf8mb4_unicode_ci),
su.user_name COLLATE utf8mb4_unicode_ci
) COLLATE utf8mb4_unicode_ci) AS `异动后班主任工号`,
(COALESCE(m.class_teacher_name, cm_after.class_teacher_name, mh.class_teacher_name) COLLATE utf8mb4_unicode_ci) AS `异动后班主任姓名`,
(CAST(NULL AS CHAR(50) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `新名字`,
('已通过' COLLATE utf8mb4_unicode_ci) AS `审核状态`,
('学籍异动' COLLATE utf8mb4_unicode_ci) AS `数据来源`,
sc.id AS `来源记录id`
FROM xg_student_status_change sc
INNER JOIN (
SELECT MIN(sc2.id) AS id
FROM xg_student_status_change sc2
LEFT JOIN xg_student_msg m2
ON m2.id = sc2.student_id
AND m2.deleted = 0
AND (m2.tenant_id COLLATE utf8mb4_unicode_ci) = (sc2.tenant_id COLLATE utf8mb4_unicode_ci)
WHERE sc2.deleted = 0
AND sc2.process_status = 1
AND sc2.effective_date IS NOT NULL
AND (sc2.change_type COLLATE utf8mb4_unicode_ci) <> ('BI_YE' COLLATE utf8mb4_unicode_ci)
GROUP BY
COALESCE(
NULLIF(
TRIM(COALESCE(sc2.student_no COLLATE utf8mb4_unicode_ci, m2.student_no COLLATE utf8mb4_unicode_ci)),
'' COLLATE utf8mb4_unicode_ci
),
(CONCAT('__sid__', sc2.student_id) COLLATE utf8mb4_unicode_ci)
),
(sc2.change_type COLLATE utf8mb4_unicode_ci),
DATE(COALESCE(sc2.apply_date, sc2.create_time))
) sc_dedup ON sc.id = sc_dedup.id
LEFT JOIN xg_student_msg m
ON m.id = sc.student_id
AND m.deleted = 0
@@ -67,11 +119,11 @@ LEFT JOIN xg_student_msg_his mh
LEFT JOIN xg_class_msg cm_before
ON cm_before.deleted = 0
AND (cm_before.tenant_id COLLATE utf8mb4_unicode_ci) = (sc.tenant_id COLLATE utf8mb4_unicode_ci)
AND cm_before.id = CAST(NULLIF(TRIM(COALESCE(mh.class_id, m.class_id)), '') AS UNSIGNED)
AND cm_before.id = CAST(NULLIF(TRIM(COALESCE(mh.class_id, m.class_id) COLLATE utf8mb4_unicode_ci), '' COLLATE utf8mb4_unicode_ci) AS UNSIGNED)
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), '') AS UNSIGNED)
AND cm_after.id = CAST(NULLIF(TRIM(m.class_id COLLATE utf8mb4_unicode_ci), '' COLLATE utf8mb4_unicode_ci) AS UNSIGNED)
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)
@@ -79,9 +131,37 @@ LEFT JOIN sys_user su
WHERE sc.deleted = 0
AND sc.process_status = 1
AND (sc.change_type COLLATE utf8mb4_unicode_ci) <> ('BI_YE' COLLATE utf8mb4_unicode_ci)
) sc_export
GROUP BY `来源记录id`
UNION ALL
SELECT
ANY_VALUE(`提交时间`) AS `提交时间`,
ANY_VALUE(`异动生效学期`) AS `异动生效学期`,
ANY_VALUE(`异动生效时间`) AS `异动生效时间`,
ANY_VALUE(`异动类型`) AS `异动类型`,
ANY_VALUE(`申请人学号`) AS `申请人学号`,
ANY_VALUE(`姓名`) AS `姓名`,
ANY_VALUE(`异动前所在年级`) AS `异动前所在年级`,
ANY_VALUE(`异动前所在校区`) AS `异动前所在校区`,
ANY_VALUE(`异动前所在院系`) AS `异动前所在院系`,
ANY_VALUE(`异动前所在专业`) AS `异动前所在专业`,
ANY_VALUE(`异动前所在班级`) AS `异动前所在班级`,
ANY_VALUE(`异动前学制`) AS `异动前学制`,
ANY_VALUE(`异动后所在校区`) AS `异动后所在校区`,
ANY_VALUE(`异动后所在年级`) AS `异动后所在年级`,
ANY_VALUE(`异动后所在班级`) AS `异动后所在班级`,
ANY_VALUE(`异动后所在院系`) AS `异动后所在院系`,
ANY_VALUE(`异动后所在专业`) AS `异动后所在专业`,
ANY_VALUE(`异动后学制`) AS `异动后学制`,
ANY_VALUE(`异动后班主任工号`) AS `异动后班主任工号`,
ANY_VALUE(`异动后班主任姓名`) AS `异动后班主任姓名`,
ANY_VALUE(`新名字`) AS `新名字`,
ANY_VALUE(`审核状态`) AS `审核状态`,
ANY_VALUE(`数据来源`) AS `数据来源`,
`来源记录id`
FROM (
SELECT
DATE(COALESCE(c.change_time, c.create_time)) AS `提交时间`,
(CAST(NULL AS CHAR(32) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `异动生效学期`,
@@ -101,13 +181,29 @@ SELECT
(c.new_college_name COLLATE utf8mb4_unicode_ci) AS `异动后所在院系`,
(c.new_major_name COLLATE utf8mb4_unicode_ci) AS `异动后所在专业`,
nc.school_system AS `异动后学制`,
(COALESCE(NULLIF(TRIM(su2.user_no), ''), su2.user_name) COLLATE utf8mb4_unicode_ci) AS `异动后班主任工号`,
(COALESCE(
NULLIF(TRIM(su2.user_no COLLATE utf8mb4_unicode_ci), '' COLLATE utf8mb4_unicode_ci),
su2.user_name COLLATE utf8mb4_unicode_ci
) COLLATE utf8mb4_unicode_ci) AS `异动后班主任工号`,
(nc.class_teacher_name COLLATE utf8mb4_unicode_ci) AS `异动后班主任姓名`,
(CAST(NULL AS CHAR(50) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `新名字`,
('已通过' COLLATE utf8mb4_unicode_ci) AS `审核状态`,
('换班级' COLLATE utf8mb4_unicode_ci) AS `数据来源`,
c.id AS `来源记录id`
FROM xg_student_class_change c
INNER JOIN (
SELECT MIN(id) AS id
FROM xg_student_class_change
WHERE deleted = 0
AND effective_time IS NOT NULL
AND (`status` COLLATE utf8mb4_unicode_ci) = ('1' COLLATE utf8mb4_unicode_ci)
GROUP BY
COALESCE(
NULLIF(TRIM(student_no COLLATE utf8mb4_unicode_ci), '' COLLATE utf8mb4_unicode_ci),
(CONCAT('__sid__', student_id) COLLATE utf8mb4_unicode_ci)
),
DATE(COALESCE(change_time, create_time))
) c_dedup ON c.id = c_dedup.id
LEFT JOIN xg_class_msg oc
ON oc.id = c.original_class_id
AND oc.deleted = 0
@@ -121,7 +217,9 @@ LEFT JOIN sys_user su2
AND (su2.del_flag COLLATE utf8mb4_unicode_ci) = ('0' COLLATE utf8mb4_unicode_ci)
AND (su2.tenant_id COLLATE utf8mb4_unicode_ci) = (c.tenant_id COLLATE utf8mb4_unicode_ci)
WHERE c.deleted = 0
AND (c.`status` COLLATE utf8mb4_unicode_ci) = ('1' COLLATE utf8mb4_unicode_ci);
AND (c.`status` COLLATE utf8mb4_unicode_ci) = ('1' COLLATE utf8mb4_unicode_ci)
) c_export
GROUP BY `来源记录id`;
-- -----------------------------------------------------------------------------
-- 授权:用户 jw_cx 仅查询本视图(需使用具备 GRANT 权限的账号执行,如 root)
@@ -138,5 +138,6 @@
| 2026-05-07 | 补充:`vw_jw_student_change_export.sql` 可执行 DDL |
| 2026-06-22 | API 改为按「异动生效时间」自然年筛选;视图输出教务中文异动类型;学籍异动「前」优先读 `xg_student_msg_his` |
| 2026-06-22 | 毕业(`BI_YE`)记录不同步给教务,视图 WHERE 排除 |
| 2026-07-03 | 视图去重:同一学号 + 登记日 + 异动类型仅保留 `MIN(id)` 一条(按学号分组,非 student_id) |
@@ -1,10 +1,14 @@
# 学生异动视图只读 API
# 教务同步只读 API(学生异动 + 学生基本信息)
从 MySQL 视图 `vw_jw_student_change_export` 拉取数据,供教务或其它系统调用。
供教务或其它系统调用,**对外接口说明见 [`接口调用说明.md`](./接口调用说明.md)**。
- **学生异动**:视图 `vw_jw_student_change_export` → `GET /api/v1/student-changes`
- **学生基本信息**:视图 `vw_jw_student_export` → `GET /api/v1/students`
- **Python**:开发与生产建议 **3.7.9**(与现网一致)。
- **依赖**:见 `requirements.txt`(Flask 2.0.x + PyMySQL)。
- **异动类型映射**:见上级目录 [`异动类型映射说明.md`](../异动类型映射说明.md)
- **异动类型映射**:见 [`异动类型映射说明.md`](../异动类型映射说明.md)
- **学生基本信息口径**:见 [`学生基本信息对接说明.md`](../../跟教务系统同步学生基础信息/学生基本信息对接说明.md)
## 配置文件与环境对应关系
@@ -59,11 +63,12 @@ python app.py
|----|------|
| `api_key` | 与请求头 `X-Jw-Cx-Key` 一致 |
| `bind_host` / `bind_port` | HTTP 监听(仅直接运行 `python app.py` 时生效) |
| `default_year` | **默认筛选年份**(按视图列「异动生效时间」);未配置则使用当前自然年;请求可带 `year` 覆盖 |
| `default_year` | 异动接口默认筛选年份(`year` 未传时);未配置则当前自然年 |
| `page_size` | 学生基本信息接口默认每页条数;请求可带 `page_size` 覆盖,范围 1~5000 |
## 密钥校验
所有 **`/api/v1/student-changes`** 请求必须在 HTTP Header 中携带:
所有 **`/api/v1/*`** 业务接口必须在 HTTP Header 中携带:
```http
X-Jw-Cx-Key: <与配置文件中 [api] api_key 相同,默认 JWCX001>
@@ -117,28 +122,79 @@ gunicorn -w 2 -b 0.0.0.0:5050 app:app
**未授权**(HTTP 401):未带或错误的 `X-Jw-Cx-Key`。
### GET `/api/v1/students`
**数据范围**:全部未逻辑删除的学生(`xg_student_msg.deleted = 0`),**分页返回**。
| Query | 说明 | 未传时的默认值 |
|--------|------|------------------|
| `page` | 页码,从 1 开始 | `1` |
| `page_size` | 每页条数,有效范围 1~5000 | `[api] page_size`(默认 500) |
成功响应元数据:
| JSON 字段 | 说明 |
|-----------|------|
| `total` | 符合条件的记录总数 |
| `page` | 当前页码 |
| `page_size` | 本页大小 |
| `total_pages` | 总页数(`ceil(total / page_size)`) |
字段口径见 [`学生基本信息对接说明.md`](../../跟教务系统同步学生基础信息/学生基本信息对接说明.md)。
**拉取全量示例**(教务侧循环):
```python
page = 1
while True:
resp = get(f"/api/v1/students?page={page}&page_size=500")
# 处理 resp["data"]
if page >= resp["total_pages"]:
break
page += 1
```
## 调用示例
```bash
# 拉取当前自然年(如 2026)全部异动
curl -s -H "X-Jw-Cx-Key: JWCX001" \
"http://127.0.0.1:5050/api/v1/student-changes"
# 指定 2026 年
# 异动:当前自然年全部
curl -s -H "X-Jw-Cx-Key: JWCX001" \
"http://127.0.0.1:5050/api/v1/student-changes?year=2026"
# 学生基本信息:分页(第 1 页,默认每页 500 条)
curl -s -H "X-Jw-Cx-Key: JWCX001" \
"http://127.0.0.1:5050/api/v1/students?page=1"
# 指定每页 1000 条、第 2 页
curl -s -H "X-Jw-Cx-Key: JWCX001" \
"http://127.0.0.1:5050/api/v1/students?page=2&page_size=1000"
```
## 部署前:重建视图
代码更新后,请在目标库执行:
```bash
mysql -h <host> -u root -p hzdyjs_xg < docs/设计文档/database/vw_jw_student_change_export.sql
mysql -h <host> -u root -p hzdyjs_xg < docs/设计文档/database/vw_jw_student_export.sql
```
## 联调测试脚本
仅调用两个业务接口(学生基本信息、学生异动)并打印结果:
```bash
cd 跟教务系统同步学生异动信息/api_service
# 生产
python test_jw_cx_api.py --prod
# 若 Nginx 带路径前缀
python test_jw_cx_api.py --base-url http://web.hzdyjsxy.cn:18081/jw-cx-api --year 2026
```
脚本仅依赖 Python 标准库;详见 `test_jw_cx_api.py --help`。
## 安全说明
1. `config.dev.ini` / `config.prod.ini` 含数据库凭据,请按公司规范控制仓库可见性与部署权限。
2. 建议数据库侧使用**仅对视图** `vw_jw_student_change_export` 有 `SELECT` 的专用账号,与后端 `root` 分离。
2. 建议数据库侧使用**仅对视图** `vw_jw_student_change_export`、`vw_jw_student_export` 有 `SELECT` 的专用账号,与后端 `root` 分离。
3. 生产将 **`api_key`** 换为强随机串,并走 HTTPS;必要时加网络白名单。
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
教务同步:学生异动视图只读 API(Python 3.7+)
教务同步:学生异动 + 学生基本信息 只读 API(Python 3.7+)
配置:默认读取与本文件同目录下的 config.ini(INI 格式),路径可用环境变量 JW_CX_CONFIG 覆盖。
校验:请求头 X-Jw-Cx-Key 须与配置文件 [api] api_key 一致。
@@ -17,16 +17,14 @@ import pymysql
from flask import Flask, jsonify, request
from pymysql.cursors import DictCursor
# 与调用方约定的 Header 名(勿随意改动,已写入 README)
HEADER_API_KEY = "X-Jw-Cx-Key"
VIEW_NAME = "vw_jw_student_change_export"
VIEW_STUDENT_CHANGES = "vw_jw_student_change_export"
VIEW_STUDENT_BASE = "vw_jw_student_export"
# 视图列名:API 按「异动生效时间」所在自然年筛选
COL_EFFECTIVE_TIME = "异动生效时间"
# 对外返回字段(与教务对接清单一致;不含视图内部列)
EXPORT_COLUMNS = (
CHANGE_EXPORT_COLUMNS = (
"提交时间",
"异动生效学期",
"异动类型",
@@ -50,6 +48,43 @@ EXPORT_COLUMNS = (
"审核状态",
)
STUDENT_EXPORT_COLUMNS = (
"姓名",
"学号",
"联系电话",
"身份证件类型",
"身份证件号",
"班级信息",
"所在校区",
"所在年级",
"入学年份",
"系部信息",
"专业信息",
"班主任工号",
"班主任姓名",
"学生当前状态",
"学生是否在校",
"异动类型",
"培养层次",
"学制",
"毕业时间",
"毕业学年学期",
"学习经历",
"何年何月-何年何月",
"在何地学习",
"性别",
"籍贯地",
"在校照片",
"出生日期",
"家庭现住址",
"成员1姓名",
"成员2姓名",
"成员1关系",
"成员2关系",
"成员1联系电话",
"成员2联系电话",
)
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
_DEFAULT_CONFIG_PATH = os.path.join(_BASE_DIR, "config.ini")
@@ -63,7 +98,6 @@ def _config_path():
def _load_config():
"""加载并缓存 INI 配置;缺失文件时给出明确提示。"""
global _config_parser
if _config_parser is not None:
return _config_parser
@@ -78,7 +112,6 @@ def _load_config():
sys.exit(1)
parser = configparser.ConfigParser()
# Python 3.2+:read 支持 encoding
read_ok = parser.read(path, encoding="utf-8")
if not read_ok:
sys.stderr.write("错误:无法读取配置文件 %s\n" % (path,))
@@ -124,8 +157,45 @@ def server_bind():
}
def _default_page_size():
page_size = _get_int("api", "page_size", 500)
if page_size < 1:
page_size = 1
if page_size > 5000:
page_size = 5000
return page_size
def _clamp_int(value, lo, hi):
v = int(value)
if v < lo:
return lo
if v > hi:
return hi
return v
def _resolve_paging():
"""解析 page / page_size;未传 page_size 则用配置 [api] page_size。"""
try:
page = int(request.args.get("page", "1"))
except (ValueError, TypeError):
return None, None, "page 必须为整数"
if page < 1:
page = 1
page_size = _default_page_size()
raw_ps = request.args.get("page_size")
if raw_ps is not None and str(raw_ps).strip() != "":
try:
page_size = _clamp_int(raw_ps, 1, 5000)
except (ValueError, TypeError):
return None, None, "page_size 必须为整数"
return page, page_size, None
def _default_filter_year():
"""配置文件 default_year;未配置或无效时使用当前自然年。"""
raw = _get("api", "default_year", "")
if raw:
try:
@@ -138,10 +208,6 @@ def _default_filter_year():
def _resolve_filter_year():
"""
合并 URL 参数与配置文件:
- year:Query `year`,未传则用 [api] default_year 或当前自然年
"""
default_year = _default_filter_year()
raw_year = request.args.get("year")
if raw_year is not None and str(raw_year).strip() != "":
@@ -180,8 +246,26 @@ def _serialize_value(value):
return value
def _row_to_export_dict(row):
return {col: _serialize_value(row.get(col)) for col in EXPORT_COLUMNS}
def _row_to_export_dict(row, columns):
return {col: _serialize_value(row.get(col)) for col in columns}
def _dedupe_student_changes(rows, columns):
"""同一学号 + 异动类型 + 提交时间 仅保留首条(兜底,防视图 JOIN 放大或学号在关联表)。"""
seen = set()
out = []
for row in rows:
item = _row_to_export_dict(row, columns)
key = (
item.get("申请人学号"),
item.get("异动类型"),
item.get("提交时间"),
)
if key in seen:
continue
seen.add(key)
out.append(item)
return out
def _db_connect():
@@ -198,9 +282,53 @@ def _db_connect():
)
def _fetch_view_page(view_name, columns, page, page_size, where_clause="", params=()):
"""分页查询视图,返回 (rows, total)。"""
select_cols = ", ".join("`%s`" % col for col in columns)
sql_count = "SELECT COUNT(*) AS cnt FROM `{view}` {where}".format(
view=view_name, where=where_clause
)
offset = (page - 1) * page_size
sql_list = (
"SELECT {cols} FROM `{view}` {where} "
"ORDER BY `来源记录id` ASC LIMIT %s OFFSET %s"
).format(cols=select_cols, view=view_name, where=where_clause)
conn = None
try:
conn = _db_connect()
with conn.cursor() as cur:
cur.execute(sql_count, params)
total = int(cur.fetchone()["cnt"])
cur.execute(sql_list, params + (page_size, offset))
rows = cur.fetchall()
return rows, total
finally:
if conn is not None:
conn.close()
def _fetch_view_rows(view_name, columns, where_clause="", params=(), order_by="`来源记录id` ASC"):
select_cols = ", ".join("`%s`" % col for col in columns)
sql = "SELECT {cols} FROM `{view}` {where} ORDER BY {order_by}".format(
cols=select_cols,
view=view_name,
where=where_clause,
order_by=order_by,
)
conn = None
try:
conn = _db_connect()
with conn.cursor() as cur:
cur.execute(sql, params)
return cur.fetchall()
finally:
if conn is not None:
conn.close()
@app.route("/health", methods=["GET"])
def health():
"""存活探测,不做密钥校验,便于负载均衡 / 运维。"""
return jsonify({"status": "ok"})
@@ -213,30 +341,20 @@ def list_student_changes():
if param_err:
return jsonify({"code": 400, "msg": param_err}), 400
select_cols = ", ".join("`%s`" % col for col in EXPORT_COLUMNS)
where_year = (
"WHERE `{col}` IS NOT NULL AND YEAR(`{col}`) = %s".format(
where_year = "WHERE `{col}` IS NOT NULL AND YEAR(`{col}`) = %s".format(
col=COL_EFFECTIVE_TIME
)
)
sql_list = (
"SELECT {cols} FROM `{view}` {where_clause} "
"ORDER BY `来源记录id` ASC"
).format(cols=select_cols, view=VIEW_NAME, where_clause=where_year)
conn = None
try:
conn = _db_connect()
with conn.cursor() as cur:
cur.execute(sql_list, (filter_year,))
rows = cur.fetchall()
rows = _fetch_view_rows(
VIEW_STUDENT_CHANGES,
CHANGE_EXPORT_COLUMNS,
where_clause=where_year,
params=(filter_year,),
)
except pymysql.Error as e:
return jsonify({"code": 500, "msg": "数据库查询失败", "detail": str(e)}), 500
finally:
if conn is not None:
conn.close()
data = [_row_to_export_dict(r) for r in rows]
data = _dedupe_student_changes(rows, CHANGE_EXPORT_COLUMNS)
return jsonify(
{
"code": 200,
@@ -249,6 +367,38 @@ def list_student_changes():
)
@app.route("/api/v1/students", methods=["GET"])
def list_students():
"""分页返回未删除学生基本信息。"""
if not _check_api_key():
return jsonify({"code": 401, "msg": "无效的密钥或未提供请求头 " + HEADER_API_KEY}), 401
page, page_size, param_err = _resolve_paging()
if param_err:
return jsonify({"code": 400, "msg": param_err}), 400
try:
rows, total = _fetch_view_page(
VIEW_STUDENT_BASE, STUDENT_EXPORT_COLUMNS, page, page_size
)
except pymysql.Error as e:
return jsonify({"code": 500, "msg": "数据库查询失败", "detail": str(e)}), 500
data = [_row_to_export_dict(r, STUDENT_EXPORT_COLUMNS) for r in rows]
total_pages = (total + page_size - 1) // page_size if total > 0 else 0
return jsonify(
{
"code": 200,
"msg": "success",
"data": data,
"total": total,
"page": page,
"page_size": page_size,
"total_pages": total_pages,
}
)
if __name__ == "__main__":
_load_config()
bind = server_bind()
@@ -18,4 +18,5 @@ database = hzdyjs_xg
api_key = JWCX001
bind_host = 0.0.0.0
bind_port = 5050
page_size = 500
# default_year =
@@ -17,5 +17,7 @@ database = hzdyjs_xg
api_key = JWCX001
bind_host = 0.0.0.0
bind_port = 5050
# 列表接口:默认筛选年份(异动生效时间);请求可带 year 覆盖;留空则使用当前自然年
# 学生基本信息接口:默认每页条数;请求可带 page_size 覆盖,范围 1~5000
page_size = 500
# 异动接口:默认筛选年份(异动生效时间);请求可带 year 覆盖;留空则使用当前自然年
# default_year =
@@ -19,4 +19,5 @@ database = hzdyjs_xg
api_key = JWCX001
bind_host = 0.0.0.0
bind_port = 5050
page_size = 500
# default_year =