提交
This commit is contained in:
1 parent
10f90d3824
commit
dd6212400c
7 files changed
+365
-56
No files matched your search
@@ -8,6 +8,7 @@
|
|||||||
-- * 「异动生效时间」:学籍异动 = effective_date;换班级 = effective_time(API 按年筛选)
|
-- * 「异动生效时间」:学籍异动 = effective_date;换班级 = effective_time(API 按年筛选)
|
||||||
-- * 「异动类型」:输出教务系统中文名称(见 §异动类型映射)
|
-- * 「异动类型」:输出教务系统中文名称(见 §异动类型映射)
|
||||||
-- * 仅导出已处理:学籍 process_status = 1;换班级 status = '1'
|
-- * 仅导出已处理:学籍 process_status = 1;换班级 status = '1'
|
||||||
|
-- * 去重:同一展示学号 + 登记日 + 异动类型 仅保留 id 最小的一条(学号取 COALESCE(sc,m))
|
||||||
-- * 学籍异动「异动前」:优先 xg_student_msg_his(处理脚本落库快照),否则回退 xg_student_msg
|
-- * 学籍异动「异动前」:优先 xg_student_msg_his(处理脚本落库快照),否则回退 xg_student_msg
|
||||||
-- * 学籍异动「异动后」:取自当前 xg_student_msg(纯学籍异动组织维度通常不变)
|
-- * 学籍异动「异动后」:取自当前 xg_student_msg(纯学籍异动组织维度通常不变)
|
||||||
-- * 末尾「数据来源」「来源记录id」供联调,API 层不对外返回
|
-- * 末尾「数据来源」「来源记录id」供联调,API 层不对外返回
|
||||||
@@ -25,6 +26,32 @@
|
|||||||
DROP VIEW IF EXISTS `vw_jw_student_change_export`;
|
DROP VIEW IF EXISTS `vw_jw_student_change_export`;
|
||||||
|
|
||||||
CREATE VIEW `vw_jw_student_change_export` AS
|
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
|
SELECT
|
||||||
DATE(COALESCE(sc.apply_date, sc.create_time)) AS `提交时间`,
|
DATE(COALESCE(sc.apply_date, sc.create_time)) AS `提交时间`,
|
||||||
(CAST(NULL AS CHAR(32) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `异动生效学期`,
|
(CAST(NULL AS CHAR(32) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `异动生效学期`,
|
||||||
@@ -33,9 +60,9 @@ SELECT
|
|||||||
WHEN 'TUI_XUE' THEN '退学'
|
WHEN 'TUI_XUE' THEN '退学'
|
||||||
WHEN 'XIU_XUE' THEN '休学'
|
WHEN 'XIU_XUE' THEN '休学'
|
||||||
WHEN 'FU_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 `异动类型`,
|
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.`name`, m.`name`) COLLATE utf8mb4_unicode_ci) AS `姓名`,
|
||||||
(COALESCE(mh.grade, m.grade, cm_before.grade) 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 `异动前所在校区`,
|
(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.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(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(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 `异动后班主任姓名`,
|
(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 `新名字`,
|
(CAST(NULL AS CHAR(50) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) AS `新名字`,
|
||||||
('已通过' COLLATE utf8mb4_unicode_ci) AS `审核状态`,
|
('已通过' COLLATE utf8mb4_unicode_ci) AS `审核状态`,
|
||||||
('学籍异动' COLLATE utf8mb4_unicode_ci) AS `数据来源`,
|
('学籍异动' COLLATE utf8mb4_unicode_ci) AS `数据来源`,
|
||||||
sc.id AS `来源记录id`
|
sc.id AS `来源记录id`
|
||||||
FROM xg_student_status_change sc
|
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
|
LEFT JOIN xg_student_msg m
|
||||||
ON m.id = sc.student_id
|
ON m.id = sc.student_id
|
||||||
AND m.deleted = 0
|
AND m.deleted = 0
|
||||||
@@ -67,11 +119,11 @@ LEFT JOIN xg_student_msg_his mh
|
|||||||
LEFT JOIN xg_class_msg cm_before
|
LEFT JOIN xg_class_msg cm_before
|
||||||
ON cm_before.deleted = 0
|
ON cm_before.deleted = 0
|
||||||
AND (cm_before.tenant_id COLLATE utf8mb4_unicode_ci) = (sc.tenant_id COLLATE utf8mb4_unicode_ci)
|
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
|
LEFT JOIN xg_class_msg cm_after
|
||||||
ON cm_after.deleted = 0
|
ON cm_after.deleted = 0
|
||||||
AND (cm_after.tenant_id COLLATE utf8mb4_unicode_ci) = (sc.tenant_id COLLATE utf8mb4_unicode_ci)
|
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
|
LEFT JOIN sys_user su
|
||||||
ON su.user_id = COALESCE(m.class_teacher_id, cm_after.class_teacher_id)
|
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)
|
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
|
WHERE sc.deleted = 0
|
||||||
AND sc.process_status = 1
|
AND sc.process_status = 1
|
||||||
AND (sc.change_type COLLATE utf8mb4_unicode_ci) <> ('BI_YE' COLLATE utf8mb4_unicode_ci)
|
AND (sc.change_type COLLATE utf8mb4_unicode_ci) <> ('BI_YE' COLLATE utf8mb4_unicode_ci)
|
||||||
|
) sc_export
|
||||||
|
GROUP BY `来源记录id`
|
||||||
|
|
||||||
UNION ALL
|
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
|
SELECT
|
||||||
DATE(COALESCE(c.change_time, c.create_time)) AS `提交时间`,
|
DATE(COALESCE(c.change_time, c.create_time)) AS `提交时间`,
|
||||||
(CAST(NULL AS CHAR(32) CHARACTER SET utf8mb4) COLLATE utf8mb4_unicode_ci) 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_college_name COLLATE utf8mb4_unicode_ci) AS `异动后所在院系`,
|
||||||
(c.new_major_name COLLATE utf8mb4_unicode_ci) AS `异动后所在专业`,
|
(c.new_major_name COLLATE utf8mb4_unicode_ci) AS `异动后所在专业`,
|
||||||
nc.school_system 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 `异动后班主任姓名`,
|
(nc.class_teacher_name COLLATE utf8mb4_unicode_ci) AS `异动后班主任姓名`,
|
||||||
(CAST(NULL AS CHAR(50) CHARACTER SET utf8mb4) 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 `审核状态`,
|
||||||
('换班级' COLLATE utf8mb4_unicode_ci) AS `数据来源`,
|
('换班级' COLLATE utf8mb4_unicode_ci) AS `数据来源`,
|
||||||
c.id AS `来源记录id`
|
c.id AS `来源记录id`
|
||||||
FROM xg_student_class_change c
|
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
|
LEFT JOIN xg_class_msg oc
|
||||||
ON oc.id = c.original_class_id
|
ON oc.id = c.original_class_id
|
||||||
AND oc.deleted = 0
|
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.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)
|
AND (su2.tenant_id COLLATE utf8mb4_unicode_ci) = (c.tenant_id COLLATE utf8mb4_unicode_ci)
|
||||||
WHERE c.deleted = 0
|
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)
|
-- 授权:用户 jw_cx 仅查询本视图(需使用具备 GRANT 权限的账号执行,如 root)
|
||||||
|
|||||||
@@ -138,5 +138,6 @@
|
|||||||
| 2026-05-07 | 补充:`vw_jw_student_change_export.sql` 可执行 DDL |
|
| 2026-05-07 | 补充:`vw_jw_student_change_export.sql` 可执行 DDL |
|
||||||
| 2026-06-22 | API 改为按「异动生效时间」自然年筛选;视图输出教务中文异动类型;学籍异动「前」优先读 `xg_student_msg_his` |
|
| 2026-06-22 | API 改为按「异动生效时间」自然年筛选;视图输出教务中文异动类型;学籍异动「前」优先读 `xg_student_msg_his` |
|
||||||
| 2026-06-22 | 毕业(`BI_YE`)记录不同步给教务,视图 WHERE 排除 |
|
| 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**(与现网一致)。
|
- **Python**:开发与生产建议 **3.7.9**(与现网一致)。
|
||||||
- **依赖**:见 `requirements.txt`(Flask 2.0.x + PyMySQL)。
|
- **依赖**:见 `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` 一致 |
|
| `api_key` | 与请求头 `X-Jw-Cx-Key` 一致 |
|
||||||
| `bind_host` / `bind_port` | HTTP 监听(仅直接运行 `python app.py` 时生效) |
|
| `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
|
```http
|
||||||
X-Jw-Cx-Key: <与配置文件中 [api] api_key 相同,默认 JWCX001>
|
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`。
|
**未授权**(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
|
```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" \
|
curl -s -H "X-Jw-Cx-Key: JWCX001" \
|
||||||
"http://127.0.0.1:5050/api/v1/student-changes?year=2026"
|
"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
|
```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_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` 含数据库凭据,请按公司规范控制仓库可见性与部署权限。
|
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;必要时加网络白名单。
|
3. 生产将 **`api_key`** 换为强随机串,并走 HTTPS;必要时加网络白名单。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
教务同步:学生异动视图只读 API(Python 3.7+)
|
教务同步:学生异动 + 学生基本信息 只读 API(Python 3.7+)
|
||||||
|
|
||||||
配置:默认读取与本文件同目录下的 config.ini(INI 格式),路径可用环境变量 JW_CX_CONFIG 覆盖。
|
配置:默认读取与本文件同目录下的 config.ini(INI 格式),路径可用环境变量 JW_CX_CONFIG 覆盖。
|
||||||
校验:请求头 X-Jw-Cx-Key 须与配置文件 [api] api_key 一致。
|
校验:请求头 X-Jw-Cx-Key 须与配置文件 [api] api_key 一致。
|
||||||
@@ -17,16 +17,14 @@ import pymysql
|
|||||||
from flask import Flask, jsonify, request
|
from flask import Flask, jsonify, request
|
||||||
from pymysql.cursors import DictCursor
|
from pymysql.cursors import DictCursor
|
||||||
|
|
||||||
# 与调用方约定的 Header 名(勿随意改动,已写入 README)
|
|
||||||
HEADER_API_KEY = "X-Jw-Cx-Key"
|
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 = "异动生效时间"
|
COL_EFFECTIVE_TIME = "异动生效时间"
|
||||||
|
|
||||||
# 对外返回字段(与教务对接清单一致;不含视图内部列)
|
CHANGE_EXPORT_COLUMNS = (
|
||||||
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__))
|
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
_DEFAULT_CONFIG_PATH = os.path.join(_BASE_DIR, "config.ini")
|
_DEFAULT_CONFIG_PATH = os.path.join(_BASE_DIR, "config.ini")
|
||||||
|
|
||||||
@@ -63,7 +98,6 @@ def _config_path():
|
|||||||
|
|
||||||
|
|
||||||
def _load_config():
|
def _load_config():
|
||||||
"""加载并缓存 INI 配置;缺失文件时给出明确提示。"""
|
|
||||||
global _config_parser
|
global _config_parser
|
||||||
if _config_parser is not None:
|
if _config_parser is not None:
|
||||||
return _config_parser
|
return _config_parser
|
||||||
@@ -78,7 +112,6 @@ def _load_config():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
parser = configparser.ConfigParser()
|
parser = configparser.ConfigParser()
|
||||||
# Python 3.2+:read 支持 encoding
|
|
||||||
read_ok = parser.read(path, encoding="utf-8")
|
read_ok = parser.read(path, encoding="utf-8")
|
||||||
if not read_ok:
|
if not read_ok:
|
||||||
sys.stderr.write("错误:无法读取配置文件 %s\n" % (path,))
|
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():
|
def _default_filter_year():
|
||||||
"""配置文件 default_year;未配置或无效时使用当前自然年。"""
|
|
||||||
raw = _get("api", "default_year", "")
|
raw = _get("api", "default_year", "")
|
||||||
if raw:
|
if raw:
|
||||||
try:
|
try:
|
||||||
@@ -138,10 +208,6 @@ def _default_filter_year():
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_filter_year():
|
def _resolve_filter_year():
|
||||||
"""
|
|
||||||
合并 URL 参数与配置文件:
|
|
||||||
- year:Query `year`,未传则用 [api] default_year 或当前自然年
|
|
||||||
"""
|
|
||||||
default_year = _default_filter_year()
|
default_year = _default_filter_year()
|
||||||
raw_year = request.args.get("year")
|
raw_year = request.args.get("year")
|
||||||
if raw_year is not None and str(raw_year).strip() != "":
|
if raw_year is not None and str(raw_year).strip() != "":
|
||||||
@@ -180,8 +246,26 @@ def _serialize_value(value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _row_to_export_dict(row):
|
def _row_to_export_dict(row, columns):
|
||||||
return {col: _serialize_value(row.get(col)) for col in EXPORT_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():
|
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"])
|
@app.route("/health", methods=["GET"])
|
||||||
def health():
|
def health():
|
||||||
"""存活探测,不做密钥校验,便于负载均衡 / 运维。"""
|
|
||||||
return jsonify({"status": "ok"})
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
|
|
||||||
@@ -213,30 +341,20 @@ def list_student_changes():
|
|||||||
if param_err:
|
if param_err:
|
||||||
return jsonify({"code": 400, "msg": param_err}), 400
|
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 = (
|
col=COL_EFFECTIVE_TIME
|
||||||
"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:
|
try:
|
||||||
conn = _db_connect()
|
rows = _fetch_view_rows(
|
||||||
with conn.cursor() as cur:
|
VIEW_STUDENT_CHANGES,
|
||||||
cur.execute(sql_list, (filter_year,))
|
CHANGE_EXPORT_COLUMNS,
|
||||||
rows = cur.fetchall()
|
where_clause=where_year,
|
||||||
|
params=(filter_year,),
|
||||||
|
)
|
||||||
except pymysql.Error as e:
|
except pymysql.Error as e:
|
||||||
return jsonify({"code": 500, "msg": "数据库查询失败", "detail": str(e)}), 500
|
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(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"code": 200,
|
"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__":
|
if __name__ == "__main__":
|
||||||
_load_config()
|
_load_config()
|
||||||
bind = server_bind()
|
bind = server_bind()
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ database = hzdyjs_xg
|
|||||||
api_key = JWCX001
|
api_key = JWCX001
|
||||||
bind_host = 0.0.0.0
|
bind_host = 0.0.0.0
|
||||||
bind_port = 5050
|
bind_port = 5050
|
||||||
|
page_size = 500
|
||||||
# default_year =
|
# default_year =
|
||||||
@@ -17,5 +17,7 @@ database = hzdyjs_xg
|
|||||||
api_key = JWCX001
|
api_key = JWCX001
|
||||||
bind_host = 0.0.0.0
|
bind_host = 0.0.0.0
|
||||||
bind_port = 5050
|
bind_port = 5050
|
||||||
# 列表接口:默认筛选年份(异动生效时间);请求可带 year 覆盖;留空则使用当前自然年
|
# 学生基本信息接口:默认每页条数;请求可带 page_size 覆盖,范围 1~5000
|
||||||
|
page_size = 500
|
||||||
|
# 异动接口:默认筛选年份(异动生效时间);请求可带 year 覆盖;留空则使用当前自然年
|
||||||
# default_year =
|
# default_year =
|
||||||
@@ -19,4 +19,5 @@ database = hzdyjs_xg
|
|||||||
api_key = JWCX001
|
api_key = JWCX001
|
||||||
bind_host = 0.0.0.0
|
bind_host = 0.0.0.0
|
||||||
bind_port = 5050
|
bind_port = 5050
|
||||||
|
page_size = 500
|
||||||
# default_year =
|
# default_year =
|
||||||
Reference in new issue
Block a user