Compare commits

...

31 Commits

Author SHA1 Message Date
cbabf9c956 'commit' 2025-09-11 21:42:06 +08:00
ec35818b13 'commit' 2025-09-11 21:34:02 +08:00
9349d582b6 'commit' 2025-09-11 21:23:39 +08:00
b821e316ca 'commit' 2025-09-11 21:17:10 +08:00
13730b1e92 'commit' 2025-09-11 20:53:28 +08:00
caca8500b6 'commit' 2025-09-11 20:50:59 +08:00
3e3343fa81 'commit' 2025-09-11 20:47:42 +08:00
649195dfeb 'commit' 2025-09-11 20:41:16 +08:00
8ad6275847 'commit' 2025-09-11 20:28:53 +08:00
6a1a67ac07 'commit' 2025-09-11 20:25:05 +08:00
a941049482 'commit' 2025-09-11 20:22:23 +08:00
fb715c2a83 'commit' 2025-09-11 20:12:57 +08:00
f3b79dc883 Merge branch 'main' of http://10.10.14.176:3000/huanghai/YunNanProject 2025-09-11 15:16:14 +08:00
0f199e5d29 'commit' 2025-09-11 15:16:13 +08:00
e85799be93 Merge branch 'main' of http://10.10.14.176:3000/huanghai/YunNanProject 2025-09-11 15:14:30 +08:00
59ed6b110e 'commit' 2025-09-11 15:14:30 +08:00
ef4c4afafc Merge branch 'main' of http://10.10.14.176:3000/huanghai/YunNanProject 2025-09-11 15:12:33 +08:00
933bb91be7 'commit' 2025-09-11 15:12:33 +08:00
06d1f924f2 'commit' 2025-09-11 15:04:14 +08:00
3998fc722f Merge branch 'main' of http://10.10.14.176:3000/huanghai/YunNanProject 2025-09-11 14:59:21 +08:00
c088fee4c1 'commit' 2025-09-11 14:59:20 +08:00
21dee17af4 'commit' 2025-09-11 14:59:10 +08:00
882c261b17 Merge branch 'main' of http://10.10.14.176:3000/huanghai/YunNanProject 2025-09-11 14:57:38 +08:00
1a99814db1 'commit' 2025-09-11 14:57:37 +08:00
348b076093 Merge branch 'main' of http://10.10.14.176:3000/huanghai/YunNanProject 2025-09-11 14:51:21 +08:00
a783438696 'commit' 2025-09-11 14:51:20 +08:00
321d9b41b4 'commit' 2025-09-11 14:51:12 +08:00
bb17c3214c Merge branch 'main' of http://10.10.14.176:3000/huanghai/YunNanProject 2025-09-11 14:48:36 +08:00
0d2a8cde19 'commit' 2025-09-11 14:48:36 +08:00
c535201e77 'commit' 2025-09-11 14:48:00 +08:00
6112cc1813 'commit' 2025-09-11 14:44:45 +08:00
26 changed files with 1862 additions and 967 deletions

View File

@@ -0,0 +1,67 @@
from fastapi import APIRouter, Query
from Model.EducationDataModel import EducationDataModel
# 创建APIRouter实例
router = APIRouter(prefix="/EducationData", tags=["教育统计数据"])
@router.get("/byYear")
async def get_education_data_by_year(
year: int = Query(default=2023, ge=2015, le=2028,
description="年份: 2015-2028范围内的年份")
):
"""获取指定年份所有学段的教育数据"""
try:
# 调用EducationDataModel的方法获取数据
data = EducationDataModel.get_education_data_by_year(year)
# 返回包含状态和数据的响应
return {
"code": 200,
"message": "success",
"data": data
}
except Exception as e:
# 异常处理
return {
"code": 500,
"message": f"获取数据失败: {str(e)}",
"data": []
}
@router.get("/populationByYear")
async def get_population_data_by_year(
year: int = Query(default=2023, ge=2015, le=2028,
description="年份: 2015-2028范围内的年份")
):
"""获取指定年份的云南省人口数据"""
try:
# 调用EducationDataModel的方法获取人口数据
data = EducationDataModel.get_population_data_by_year(year)
if data:
# 返回包含状态和数据的响应
return {
"code": 200,
"message": "success",
"data": data
}
else:
# 未找到数据的情况
return {
"code": 404,
"message": f"未找到{year}年的云南省人口数据",
"data": None
}
except Exception as e:
# 异常处理
return {
"code": 500,
"message": f"获取数据失败: {str(e)}",
"data": None
}

View File

@@ -1,21 +1,24 @@
from fastapi import APIRouter
from fastapi import APIRouter, Query
from Model.RuYuanZaiYuanCountModel import RuYuanZaiYuanModel
# 创建APIRouter实例
router = APIRouter(prefix="/RuYuanZaiYuan", tags=["大屏展示"])
# 默认的根路由
@router.get("/")
async def root():
return {"message": "Welcome to YunNan Education World!"}
@router.get("/school/preschool/chart")
async def get_preschool_education_chart_config():
return RuYuanZaiYuanModel.generate_preschool_education_config()
@router.get("/school/preschool/inschool/chart")
async def get_preschool_in_school_chart_config():
return RuYuanZaiYuanModel.generate_in_school_education_config()
router = APIRouter(prefix="/RuYuanZaiYuan", tags=["入校、在校人数统计"])
@router.get("/school/chart")
async def get_education_chart_config(
education_stage: str = Query(default="preschool", pattern="^(preschool|primary|junior|senior)$",
description="教育阶段: preschool(学前), primary(小学), junior(初中), senior(高中)")
):
return RuYuanZaiYuanModel.generate_preschool_education_config(education_stage)
@router.get("/school/inschool/chart")
async def get_in_school_chart_config(
education_stage: str = Query(default="preschool", pattern="^(preschool|primary|junior|senior)$",
description="教育阶段: preschool(学前), primary(小学), junior(初中), senior(高中)")
):
return RuYuanZaiYuanModel.generate_in_school_education_config(education_stage)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

350
Model/EducationDataModel.py Normal file
View File

@@ -0,0 +1,350 @@
import os
import json
class EducationDataModel:
# 定义支持的教育阶段映射
EDUCATION_STAGES = {
'preschool': '学前',
'primary': '小学',
'junior': '初中',
'senior': '高中',
'vocational': '中职'
}
# 获取脚本所在目录的绝对路径
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 使用绝对路径定义数据目录和文件
DATA_DIR = os.path.join(BASE_DIR, '../Data')
SCHOOL_COUNT_FILE = os.path.join(DATA_DIR, 'SchoolCount.json')
TEACHER_COUNT_FILE = os.path.join(DATA_DIR, 'TeacherCount.json')
ENROLLMENT_COUNT_FILE = os.path.join(DATA_DIR, 'ZaiXiaoShengCount.json')
ADMISSION_COUNT_FILE = os.path.join(DATA_DIR, 'ZhaoShengCount.json')
POPULATION_FILE = os.path.join(DATA_DIR, 'RenKou.json') # 新增:人口数据文件路径
@staticmethod
def load_json_file(file_path):
"""加载JSON文件数据"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"加载文件 {file_path} 失败: {e}")
return None
@staticmethod
def get_province_data(data):
"""从数据中提取云南省的数据"""
if not data or not isinstance(data, list):
return None
# 查找云南省的数据
for item in data:
if item.get('area_name') == '云南省':
return item
print("未找到云南省的数据")
return None
@staticmethod
def get_school_count_by_year_and_stage(year, stage):
"""获取指定年份和学段的学校总数"""
school_data = EducationDataModel.load_json_file(EducationDataModel.SCHOOL_COUNT_FILE)
province_data = EducationDataModel.get_province_data(school_data)
if not province_data:
return 0
# 根据学段获取对应的学校数据字段名
stage_field_mapping = {
'preschool': 'preschool_schools',
'primary': 'primary_schools',
'junior': 'junior_schools',
'senior': 'senior_schools',
'vocational': 'vocational_schools'
}
field_name = stage_field_mapping.get(stage)
if not field_name or field_name not in province_data:
print(f"学段 {stage} 对应的学校数据字段不存在")
return 0
year_data = province_data[field_name].get(str(year))
if not year_data:
print(f"年份 {year} 的学校数据不存在")
return 0
# 计算学校总数 - 处理vocational_schools的特殊格式
try:
# 对于vocational学段数据可能是直接的数值而不是字典
if stage == 'vocational' and isinstance(year_data, int):
return year_data
# 对于其他学段或正常字典格式
elif isinstance(year_data, dict):
if 'urban' in year_data and 'town' in year_data and 'rural' in year_data:
total_schools = year_data.get('urban', 0) + year_data.get('town', 0) + year_data.get('rural', 0)
return total_schools
elif 'total' in year_data:
# 如果没有分区数据但有total字段使用total
return year_data.get('total', 0)
else:
print(f"年份 {year} 的学校数据缺少必要的字段")
return 0
else:
print(f"年份 {year} 的学校数据格式不正确")
return 0
except Exception as e:
print(f"计算学校总数时出错: {e}")
return 0
@staticmethod
def get_teacher_count_by_year_and_stage(year, stage):
"""获取指定年份和学段的教职工总数"""
teacher_data = EducationDataModel.load_json_file(EducationDataModel.TEACHER_COUNT_FILE)
province_data = EducationDataModel.get_province_data(teacher_data)
if not province_data:
return 0
# 根据学段获取对应的教职工数据字段名
stage_field_mapping = {
'preschool': 'preschool_teachers',
'primary': 'primary_teachers',
'junior': 'junior_teachers',
'senior': 'senior_teachers',
'vocational': 'vocational_teachers'
}
field_name = stage_field_mapping.get(stage)
if not field_name or field_name not in province_data:
print(f"学段 {stage} 对应的教职工数据字段不存在")
return 0
year_data = province_data[field_name].get(str(year))
if not year_data or not isinstance(year_data, dict):
print(f"年份 {year} 的教职工数据不存在或格式不正确")
return 0
# 优先使用total_staff如果为0或不存在则返回教师总数
try:
total_staff = year_data.get('total_staff', 0)
if total_staff == 0:
total_staff = year_data.get('total_teacher', 0)
return total_staff
except Exception as e:
print(f"获取教职工总数时出错: {e}")
return 0
@staticmethod
def get_enrollment_count_by_year_and_stage(year, stage):
"""获取指定年份和学段的在校生总数"""
enrollment_data = EducationDataModel.load_json_file(EducationDataModel.ENROLLMENT_COUNT_FILE)
province_data = EducationDataModel.get_province_data(enrollment_data)
if not province_data or 'student_data' not in province_data:
print("未找到学生数据")
return 0
stage_data = province_data['student_data'].get(stage)
if not stage_data:
print(f"学段 {stage} 的学生数据不存在")
return 0
year_data = stage_data.get(str(year))
if not year_data or not isinstance(year_data, dict):
print(f"年份 {year} 的学生数据不存在或格式不正确")
return 0
# 返回在校生总数 - 处理vocational数据只有total字段的情况
try:
if 'urban' in year_data and 'town' in year_data and 'rural' in year_data:
# 通过累加城区、镇区、乡村数据计算总数
total_enrollment = year_data.get('urban', 0) + year_data.get('town', 0) + year_data.get('rural', 0)
return total_enrollment
elif 'total' in year_data and stage == 'vocational':
# 对于vocational学段如果只有total字段使用total
return year_data.get('total', 0)
else:
# 对于其他学段,如果没有分区数据,直接报错
print(f"年份 {year} 的学生数据缺少城区、镇区或乡村字段")
return 0
except Exception as e:
print(f"获取在校生总数时出错: {e}")
return 0
@staticmethod
def get_admission_count_by_year_and_stage(year, stage):
"""获取指定年份和学段的招生总数"""
admission_data = EducationDataModel.load_json_file(EducationDataModel.ADMISSION_COUNT_FILE)
province_data = EducationDataModel.get_province_data(admission_data)
if not province_data or 'education_data' not in province_data:
print("未找到教育数据")
return 0
stage_data = province_data['education_data'].get(stage)
if not stage_data:
print(f"学段 {stage} 的教育数据不存在")
return 0
year_data = stage_data.get(str(year))
if not year_data or not isinstance(year_data, dict):
print(f"年份 {year} 的教育数据不存在或格式不正确")
return 0
# 返回招生总数 - 处理vocational数据只有total字段的情况
try:
if 'urban' in year_data and 'town' in year_data and 'rural' in year_data:
# 通过累加城区、镇区、乡村数据计算总数
total_admission = year_data.get('urban', 0) + year_data.get('town', 0) + year_data.get('rural', 0)
return total_admission
elif 'total' in year_data and stage == 'vocational':
# 对于vocational学段如果只有total字段使用total
return year_data.get('total', 0)
else:
# 对于其他学段,如果没有分区数据,直接报错
print(f"年份 {year} 的教育数据缺少城区、镇区或乡村字段")
return 0
except Exception as e:
print(f"获取招生总数时出错: {e}")
return 0
@staticmethod
def get_education_data_by_year(year):
"""获取指定年份所有学段的教育数据"""
result = []
for stage_code, stage_name in EducationDataModel.EDUCATION_STAGES.items():
# 获取各类数据(增加异常处理)
try:
school_count = EducationDataModel.get_school_count_by_year_and_stage(year, stage_code)
teacher_count = EducationDataModel.get_teacher_count_by_year_and_stage(year, stage_code)
enrollment_count = EducationDataModel.get_enrollment_count_by_year_and_stage(year, stage_code)
admission_count = EducationDataModel.get_admission_count_by_year_and_stage(year, stage_code)
# 转换为万人单位
teacher_count_10k = round(teacher_count / 10000, 2) if teacher_count else 0
enrollment_count_10k = round(enrollment_count / 10000, 2) if enrollment_count else 0
admission_count_10k = round(admission_count / 10000, 2) if admission_count else 0
# 添加到结果列表
result.append({
'education_stage': stage_name,
'school_count': school_count,
'teacher_count_10k': teacher_count_10k,
'enrollment_count_10k': enrollment_count_10k,
'admission_count_10k': admission_count_10k
})
except Exception as e:
print(f"处理学段 {stage_name} 时出错: {e}")
# 出错时添加一个默认的空数据条目
result.append({
'education_stage': stage_name,
'school_count': 0,
'teacher_count_10k': 0,
'enrollment_count_10k': 0,
'admission_count_10k': 0
})
return result
# 新增:人口数据相关方法
@staticmethod
def get_population_data_by_year(year):
"""获取指定年份的云南省人口数据"""
# 加载人口数据文件
population_data = EducationDataModel.load_json_file(EducationDataModel.POPULATION_FILE)
# 获取云南省数据
yunnan_data = EducationDataModel.get_province_data(population_data)
if not yunnan_data:
print(f"未获取到云南省{year}年的人口数据")
return None
year_str = str(year)
result = {
'year': year,
'total_population': yunnan_data.get('total_population', {}).get(year_str, 0), # 万人
'birth_population': yunnan_data.get('birth_population', {}).get(year_str, 0), # 原数据单位为个
'urban_population': yunnan_data.get('urban_population', {}).get(year_str, 0), # 万人
'rural_population': yunnan_data.get('rural_population', {}).get(year_str, 0), # 万人
'urbanization_rate': yunnan_data.get('urbanization_rate', {}).get(year_str, 0) # %
}
# 将新生人口转换为万人(如果是整数)
if isinstance(result['birth_population'], int):
result['birth_population'] = round(result['birth_population'] / 10000, 1)
return result
@staticmethod
def print_population_data(population_data):
"""打印人口数据"""
if not population_data:
print("没有可打印的人口数据")
return
year = population_data.get('year', '未知')
print(f"云南省 {year}年人口概览")
print("-------------------")
print(f"总人口 {population_data.get('total_population', 0):,} 万人")
print(f"新生人口 {population_data.get('birth_population', 0)} 万人")
print(f"城镇人口 {population_data.get('urban_population', 0):,} 万人")
print(f"乡村人口 {population_data.get('rural_population', 0):,} 万人")
print(f"城镇化率 {population_data.get('urbanization_rate', 0):.2f} %")
# 验证数据一致性
total = population_data.get('total_population', 0)
urban_rural_sum = population_data.get('urban_population', 0) + population_data.get('rural_population', 0)
if abs(total - urban_rural_sum) > 0.01:
print("\n注意:总人口与城镇人口+乡村人口存在差异")
print(f"差异值: {abs(total - urban_rural_sum):.2f} 万人")
@staticmethod
def print_education_data(data, year):
"""打印教育数据"""
print(f"\n===== {year}年云南省各学段教育数据统计(单位:学校总数-个,其余-万人)=====")
print(f"{'学段':<8}{'学校总数':<10}{'教职工总数':<12}{'在校生总数':<12}{'招生总数':<12}")
print("=" * 60)
for item in data:
print(f"{item['education_stage']:<8}{item['school_count']:<10}{item['teacher_count_10k']:<12.2f}{item['enrollment_count_10k']:<12.2f}{item['admission_count_10k']:<12.2f}")
@staticmethod
def main(year=2023):
"""主函数"""
try:
print(f"正在获取 {year} 年数据...")
print(f"数据目录: {EducationDataModel.DATA_DIR}")
# 检查数据文件是否存在
for file_path in [EducationDataModel.SCHOOL_COUNT_FILE,
EducationDataModel.TEACHER_COUNT_FILE,
EducationDataModel.ENROLLMENT_COUNT_FILE,
EducationDataModel.ADMISSION_COUNT_FILE,
EducationDataModel.POPULATION_FILE]: # 新增:检查人口数据文件
if os.path.exists(file_path):
print(f"找到数据文件: {file_path}")
else:
print(f"警告:数据文件不存在: {file_path}")
# 获取并打印教育数据
education_data = EducationDataModel.get_education_data_by_year(year)
if education_data:
EducationDataModel.print_education_data(education_data, year)
else:
print("未获取到任何教育数据")
# 新增:获取并打印人口数据
print("\n" + "=" * 50)
population_data = EducationDataModel.get_population_data_by_year(year)
EducationDataModel.print_population_data(population_data)
except Exception as e:
print(f"获取数据时发生错误: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
EducationDataModel.main()

View File

@@ -1,14 +1,15 @@
import json
from pyecharts import options as opts
from pyecharts.charts import Bar, Line
from pyecharts.globals import CurrentConfig
from Config.Config import ONLINE_HOST
CurrentConfig.ONLINE_HOST = ONLINE_HOST
class RuYuanZaiYuanModel:
# 定义支持的教育阶段映射
EDUCATION_STAGES = {
'preschool': '学前',
'primary': '小学',
'junior': '初中',
'senior': '高中'
}
@staticmethod
def load_student_data():
try:
@@ -26,52 +27,70 @@ class RuYuanZaiYuanModel:
return [], []
@staticmethod
def generate_preschool_education_config():
# 获取学前教育相关数据
# enrollment_data, in_school_data = RuYuanZaiYuanModel.load_student_data()
#
# # 提取云南省级数据
# yunnan_enroll = next((item for item in enrollment_data if item["area_name"] == "云南省"), None)
#
# if not yunnan_enroll:
# return {}
#
# # 提取年份数据(2015-2024)
# years = [str(year) for year in range(2015, 2025)]
#
# # 构建学前教育数据
# urban_data = [] # 城区数据
# town_data = [] # 镇区数据
# rural_data = [] # 乡村数据
# total_enroll = [] # 总入园数
#
# for year in years:
# enroll_data = yunnan_enroll["education_data"]["preschool"].get(year, {})
# urban_data.append(enroll_data.get("urban", 0) / 10000) # 转换为万人
# town_data.append(enroll_data.get("town", 0) / 10000) # 转换为万人
# rural_data.append(enroll_data.get("rural", 0) / 10000) # 转换为万人
# # 计算总和作为总入园数而非使用文件中的total字段
# calculated_total = enroll_data.get("urban", 0) + enroll_data.get("town", 0) + enroll_data.get("rural", 0)
# total_enroll.append(calculated_total / 10000) # 转换为万人
def generate_preschool_education_config(education_stage='preschool'):
# 验证教育阶段参数
if education_stage not in RuYuanZaiYuanModel.EDUCATION_STAGES:
education_stage = 'preschool' # 默认使用学前
xAxis = ["2019", "2020", "2021", "2022", "2023", "2024", "2025", "2026"]
series_data_1 = [2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 135.6]
series_data_2 = [2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6]
series_data_3 = [2.4, 5.2, 8.0, 25.4, 27.7, 73.7, 155.6]
series_data_4 = [2.0, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3]
line_data = [2.0, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3]
data = {"xAxis_data": xAxis,
"series_data_1": series_data_1,
"series_data_2": series_data_2,
"series_data_3": series_data_3,
"series_data_4": series_data_4,
"line_data": line_data
}
# 获取学前教育相关数据
enrollment_data, in_school_data = RuYuanZaiYuanModel.load_student_data()
# 提取云南省级数据
yunnan_enroll = next((item for item in enrollment_data if item["area_name"] == "云南省"), None)
if not yunnan_enroll:
return {}
# # 构建学前教育数据
urban_data = [] # 城区数据
town_data = [] # 镇区数据
rural_data = [] # 乡村数据
total_enroll = [] # 总人数
# 提取年份数据(2015-2024)
years = [str(year) for year in range(2015, 2025)]
for year in years:
# 使用传入的教育阶段参数
enroll_data = yunnan_enroll["education_data"].get(education_stage, {}).get(year, {})
urban_data.append(enroll_data.get("urban", 0) / 10000) # 转换为万人
town_data.append(enroll_data.get("town", 0) / 10000) # 转换为万人
rural_data.append(enroll_data.get("rural", 0) / 10000) # 转换为万人
# 计算总和作为总人数
calculated_total = enroll_data.get("urban", 0) + enroll_data.get("town", 0) + enroll_data.get("rural", 0)
total_enroll.append(calculated_total / 10000) # 转换为万人
# 添加2022年基数的粉色折线
base_year = "2022"
# 找到2022年在years中的索引位置
base_index = years.index(base_year) if base_year in years else 0
# 获取2022年的总人数作为基数
base_value = total_enroll[base_index] if base_index < len(total_enroll) else 0
# 创建2022年基数折线数据2022-2024年
base_2022_line = []
for i, year in enumerate(years):
# 只在2022年及之后显示基数线
if i >= base_index:
base_2022_line.append(base_value)
else:
base_2022_line.append(None) # 2022年之前不显示
data = {
"xAxis_data": years,
"series_data_0": urban_data, # 城区
"series_data_1": town_data, # 镇区
"series_data_2": rural_data, # 乡村
"series_data_3": total_enroll, # 总人数
"series_data_4": base_2022_line, # 2022年基数
"education_stage": RuYuanZaiYuanModel.EDUCATION_STAGES.get(education_stage, '学前') # 添加教育阶段名称
}
return data
@staticmethod
def generate_in_school_education_config():
# 获取学前教育相关数据
def generate_in_school_education_config(education_stage='preschool'):
# 验证教育阶段参数
if education_stage not in RuYuanZaiYuanModel.EDUCATION_STAGES:
education_stage = 'preschool' # 默认使用学前
# 获取在校生相关数据
enrollment_data, in_school_data = RuYuanZaiYuanModel.load_student_data()
# 提取云南省级数据
@@ -83,15 +102,15 @@ class RuYuanZaiYuanModel:
# 提取年份数据(2015-2024)
years = [str(year) for year in range(2015, 2025)]
# 构建学前教育数据
# 构建数据
urban_data = [] # 城区数据
town_data = [] # 镇区数据
rural_data = [] # 乡村数据
total_in_school = [] # 总在园
total_in_school = [] # 总
for year in years:
# 将education_data改为student_data
in_school_year_data = yunnan_in_school["student_data"]["preschool"].get(year, {})
# 使用传入的教育阶段参数
in_school_year_data = yunnan_in_school["student_data"].get(education_stage, {}).get(year, {})
# 先获取原始数据
urban_val = in_school_year_data.get("urban", 0)
@@ -111,7 +130,7 @@ class RuYuanZaiYuanModel:
base_year = "2022"
# 找到2022年在years中的索引位置
base_index = years.index(base_year) if base_year in years else 0
# 获取2022年的总在园数作为基数
# 获取2022年的总数作为基数
base_value = total_in_school[base_index] if base_index < len(total_in_school) else 0
# 创建2022年基数折线数据2022-2024年
base_2022_line = []
@@ -122,137 +141,14 @@ class RuYuanZaiYuanModel:
else:
base_2022_line.append(None) # 2022年之前不显示
# 构建ECharts配置
option = {
"tooltip": {
"trigger": "axis",
"axisPointer": {
"type": "cross",
"crossStyle": {"color": "#999"}
},
"textStyle": {"color": "#333"},
"backgroundColor": "rgba(255, 255, 255, 0.8)",
"borderColor": "rgba(0, 0, 0, 0.2)",
"borderWidth": 1
},
"legend": {
"data": ["城区", "镇区", "乡村", "总在园数", "2022年基数(万人)"],
"top": 30,
"textStyle": {"color": "#333"},
"icon": "roundRect",
"itemWidth": 12,
"itemHeight": 12
},
"xAxis": [
{
"type": "category",
"data": years,
"axisPointer": {"type": "shadow"},
"axisLabel": {"color": "#333"},
"axisLine": {"lineStyle": {"color": "#333"}}
}
],
"yAxis": [
{
"type": "value",
"name": "人数",
"min": 0,
"max": 200, # 将左侧Y轴最大值从100修改为200万
"interval": 40, # 调整间隔为40万确保坐标轴上有5个主要刻度(0,40,80,120,160,200)
"axisLabel": {
"formatter": "{value} 万人",
"color": "#333"
},
"axisLine": {"lineStyle": {"color": "#333"}},
"splitLine": {"lineStyle": {"color": "rgba(0,0,0,0.1)"}}
},
{
"type": "value",
"name": "总在园数",
"min": 0,
"max": 320, # 将右侧Y轴最大值从160扩大一倍到320万
"interval": 60, # 调整间隔为60万
"position": "right",
"nameLocation": "end",
"nameGap": 30,
"axisLabel": {
"formatter": "{value} 万人",
"color": "#00a859" # 改为绿色
},
"axisLine": {"show": True, "lineStyle": {"color": "#00a859"}}, # 改为绿色
"axisTick": {"show": True},
"splitLine": {"show": False}
}
],
"series": [
{
"name": "城区",
"type": "bar",
"data": urban_data,
"itemStyle": {
"color": "#5470c6",
"borderRadius": [6, 6, 0, 0]
},
"barWidth": "25%",
"barGap": "-10%"
},
{
"name": "镇区",
"type": "bar",
"data": town_data,
"itemStyle": {
"color": "#91cc75",
"borderRadius": [6, 6, 0, 0]
},
"barWidth": "25%",
"barGap": "-10%"
},
{
"name": "乡村",
"type": "bar",
"data": rural_data,
"itemStyle": {
"color": "#fac858",
"borderRadius": [6, 6, 0, 0]
},
"barWidth": "25%",
"barGap": "-10%"
},
{
"name": "总在园数",
"type": "line",
"yAxisIndex": 1,
"data": total_in_school,
"lineStyle": {"color": "#00a859", "width": 3}, # 改为绿色
"symbol": "circle",
"symbolSize": 8,
"itemStyle": {"color": "#00a859"}, # 改为绿色
"emphasis": {
"focus": "series"
},
"z": 10 # 确保折线图显示在最上层
},
# 添加2022年基数的粉色折线
{
"name": "2022年基数(万人)",
"type": "line",
"yAxisIndex": 1,
"data": base_2022_line,
"lineStyle": {"color": "#ff9e9e", "width": 2, "type": "solid"},
"symbol": "circle",
"symbolSize": 6,
"itemStyle": {"color": "#ff9e9e"},
"emphasis": {
"focus": "series"
},
"z": 5 # 确保折线图显示在柱状图之上,但在总在园数折线之下
}
],
"grid": {
"left": "3%",
"right": "15%", # 稍微增加右侧边距确保Y轴标签不被截断
"bottom": "3%",
"containLabel": True
}
data = {
"xAxis_data": years,
"series_data_0": urban_data,
"series_data_1": town_data,
"series_data_2": rural_data,
"series_data_3": total_in_school,
"series_data_4": base_2022_line,
"education_stage": RuYuanZaiYuanModel.EDUCATION_STAGES.get(education_stage, '学前') # 添加教育阶段名称
}
return option
return data

Binary file not shown.

View File

@@ -4,7 +4,7 @@ from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
import uvicorn
from Controller.RuYuanZaiYuanCountController import router as ruyuanZaiYuan_router
from Controller.EducationDataController import router as educationData_router
# 创建 FastAPI 应用实例
app = FastAPI(title="云南教育决策研究服务系统", description="云南省教育数据分析和可视化平台")
@@ -14,6 +14,7 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
# 包含大屏展示路由
app.include_router(ruyuanZaiYuan_router)
app.include_router(educationData_router)
# 主程序入口
if __name__ == "__main__":
# 启动 FastAPI 应用,监听 8100 端口

View File

@@ -0,0 +1,274 @@
import json
import os
from datetime import datetime
class PopulationStatistics:
def __init__(self, data_file):
"""初始化人口统计类,加载数据文件"""
self.data_file = data_file
self.data = self.load_data()
self.years = list(range(2015, 2025)) # 2015-2024年
def load_data(self):
"""加载人口数据JSON文件"""
try:
with open(self.data_file, 'r', encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
print(f"错误:找不到数据文件 {self.data_file}")
return []
except json.JSONDecodeError:
print(f"错误:数据文件 {self.data_file} 格式不正确")
return []
def get_province_data(self):
"""获取云南省全省数据"""
for item in self.data:
if item['area_name'] == '云南省' and item['area_code'] == '530000000':
return item
return None
def get_city_data(self, city_name):
"""获取指定城市的数据"""
for item in self.data:
if item['area_name'] == city_name:
return item
return None
def get_all_cities(self):
"""获取所有地市级城市列表"""
cities = []
# 地市级城市的area_code格式为53XX00000
for item in self.data:
if len(item['area_code']) == 9 and item['area_code'][-5:] == '00000' and item['area_code'] != '530000000':
cities.append(item)
return cities
def calculate_total_population_by_year(self, level='province'):
"""按年份计算总人口
level: 'province'(全省), 'city'(昆明市), 'all_cities'(所有地级市)
"""
result = {}
if level == 'province':
province_data = self.get_province_data()
if province_data:
result = province_data['total_population']
elif level == 'city':
city_data = self.get_city_data('昆明市')
if city_data:
result = city_data['total_population']
elif level == 'all_cities':
cities = self.get_all_cities()
for year in self.years:
total = 0
for city in cities:
if str(year) in city['total_population']:
total += city['total_population'][str(year)]
result[str(year)] = total
return result
def calculate_urbanization_trend(self, area_name='云南省'):
"""计算指定地区的城镇化率趋势"""
area_data = None
if area_name == '云南省':
area_data = self.get_province_data()
else:
area_data = self.get_city_data(area_name)
if area_data and 'urbanization_rate' in area_data:
return area_data['urbanization_rate']
return {}
def compare_city_populations(self, year=2023):
"""比较指定年份各城市的人口数量"""
cities = self.get_all_cities()
result = {}
for city in cities:
if str(year) in city['total_population']:
result[city['area_name']] = city['total_population'][str(year)]
# 按人口数量排序
return dict(sorted(result.items(), key=lambda x: x[1], reverse=True))
def calculate_population_growth(self, area_name='云南省', start_year=2020, end_year=2023):
"""计算指定地区在指定时间段内的人口增长率"""
area_data = None
if area_name == '云南省':
area_data = self.get_province_data()
else:
area_data = self.get_city_data(area_name)
if not area_data:
return None
start_pop = area_data['total_population'].get(str(start_year), 0)
end_pop = area_data['total_population'].get(str(end_year), 0)
if start_pop == 0:
return None
growth_rate = ((end_pop - start_pop) / start_pop) * 100
return growth_rate
def analyze_birth_rate(self, area_name='云南省'):
"""分析指定地区的出生率趋势"""
area_data = None
if area_name == '云南省':
area_data = self.get_province_data()
else:
area_data = self.get_city_data(area_name)
if not area_data or 'birth_population' not in area_data or 'total_population' not in area_data:
return {}
birth_rates = {}
for year in self.years:
year_str = str(year)
if year_str in area_data['birth_population'] and year_str in area_data['total_population']:
birth_pop = area_data['birth_population'][year_str]
total_pop = area_data['total_population'][year_str]
# 注意:总人口单位是万人,出生人口单位是个,需要统一单位
if total_pop > 0 and birth_pop > 0:
birth_rate = (birth_pop / (total_pop * 10000)) * 1000 # 千分比
birth_rates[year] = birth_rate
return birth_rates
def save_results_to_file(self, results, filename):
"""将统计结果保存到文件"""
# 创建结果目录
result_dir = os.path.join(os.path.dirname(self.data_file), '../Results')
os.makedirs(result_dir, exist_ok=True)
# 生成带时间戳的文件名
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
full_filename = f"{filename}_{timestamp}.txt"
full_path = os.path.join(result_dir, full_filename)
# 保存结果
with open(full_path, 'w', encoding='utf-8') as f:
f.write(f"人口统计结果 - 生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
for key, value in results.items():
if isinstance(value, dict):
f.write(f"{key}:\n")
for k, v in value.items():
f.write(f" {k}: {v}\n")
else:
f.write(f"{key}: {value}\n")
print(f"统计结果已保存至:{full_path}")
return full_path
# 主函数演示如何使用这个类
if __name__ == "__main__":
# 数据文件路径
data_file = os.path.join(os.path.dirname(__file__), '../Data/RenKou.json')
# 创建统计类实例
stats = PopulationStatistics(data_file)
# 检查数据是否成功加载
if not stats.data:
print("无法加载数据,程序退出。")
exit(1)
# 主菜单
print("云南省人口数据统计分析工具")
print("========================")
while True:
print("\n请选择要执行的统计分析:")
print("1. 查看云南省历年总人口统计")
print("2. 查看昆明市历年总人口统计")
print("3. 分析云南省城镇化率趋势")
print("4. 分析昆明市城镇化率趋势")
print("5. 比较2023年各城市人口数量")
print("6. 计算云南省2020-2023年人口增长率")
print("7. 分析云南省出生率趋势")
print("8. 分析昆明市出生率趋势")
print("9. 导出所有统计结果")
print("0. 退出程序")
choice = input("请输入您的选择0-9")
if choice == '0':
print("感谢使用,再见!")
break
elif choice == '1':
result = stats.calculate_total_population_by_year('province')
print("\n云南省历年总人口统计(单位:万人):")
for year, population in result.items():
print(f"{year}年: {population}")
elif choice == '2':
result = stats.calculate_total_population_by_year('city')
print("\n昆明市历年总人口统计(单位:万人):")
for year, population in result.items():
print(f"{year}年: {population}")
elif choice == '3':
result = stats.calculate_urbanization_trend('云南省')
print("\n云南省历年城镇化率(单位:%")
for year, rate in result.items():
print(f"{year}年: {rate}")
elif choice == '4':
result = stats.calculate_urbanization_trend('昆明市')
print("\n昆明市历年城镇化率(单位:%")
for year, rate in result.items():
print(f"{year}年: {rate}")
elif choice == '5':
result = stats.compare_city_populations(2023)
print("\n2023年各城市人口数量排名单位万人")
for i, (city, population) in enumerate(result.items(), 1):
print(f"{i}. {city}: {population}")
elif choice == '6':
rate = stats.calculate_population_growth('云南省', 2020, 2023)
if rate is not None:
print(f"\n云南省2020-2023年人口增长率: {rate:.2f}%")
else:
print("\n无法计算人口增长率")
elif choice == '7':
result = stats.analyze_birth_rate('云南省')
print("\n云南省历年出生率(单位:‰):")
for year, rate in result.items():
print(f"{year}年: {rate:.2f}")
elif choice == '8':
result = stats.analyze_birth_rate('昆明市')
print("\n昆明市历年出生率(单位:‰):")
for year, rate in result.items():
print(f"{year}年: {rate:.2f}")
elif choice == '9':
# 收集所有统计结果
all_results = {
"云南省历年总人口统计(万人)": stats.calculate_total_population_by_year('province'),
"昆明市历年总人口统计(万人)": stats.calculate_total_population_by_year('city'),
"云南省历年城镇化率(%": stats.calculate_urbanization_trend('云南省'),
"昆明市历年城镇化率(%": stats.calculate_urbanization_trend('昆明市'),
"2023年各城市人口数量排名万人": stats.compare_city_populations(2023),
"云南省2020-2023年人口增长率%": stats.calculate_population_growth('云南省', 2020, 2023),
"云南省历年出生率(‰)": stats.analyze_birth_rate('云南省'),
"昆明市历年出生率(‰)": stats.analyze_birth_rate('昆明市')
}
# 保存结果到文件
file_path = stats.save_results_to_file(all_results, "population_statistics")
print(f"\n所有统计结果已保存至文件:{file_path}")
else:
print("\n无效的选择,请重新输入。")
# 等待用户按Enter键继续
input("\n按Enter键继续...")

View File

@@ -0,0 +1,52 @@
import json
import os
# 获取脚本所在目录
script_dir = os.path.dirname(os.path.abspath(__file__))
# 构建数据文件的绝对路径
file_path = os.path.join(script_dir, '../Data/RenKou.json')
# 读取JSON数据
with open(file_path, 'r', encoding='utf-8') as f:
population_data = json.load(f)
# 查找云南省的数据
for area in population_data:
if area.get('area_name') == '云南省':
yunnan_data = area
break
# 提取2023年的人口数据
year = '2023'
# 总人口(万人)
total_population = yunnan_data.get('total_population', {}).get(year, 0)
# 新生人口(万人,注意原数据是整数,需要转换为万人)
birth_population = yunnan_data.get('birth_population', {}).get(year, 0)
# 转换为万人(如果是整数)
if isinstance(birth_population, int):
birth_population = round(birth_population / 10000, 1)
# 城镇人口(万人)
urban_population = yunnan_data.get('urban_population', {}).get(year, 0)
# 乡村人口(万人)
rural_population = yunnan_data.get('rural_population', {}).get(year, 0)
# 城镇化率(%
urbanization_rate = yunnan_data.get('urbanization_rate', {}).get(year, 0)
# 打印结果
print("云南省 2023年人口概览")
print("-------------------")
print(f"总人口 {total_population:,} 万人")
print(f"新生人口 {birth_population} 万人")
print(f"城镇人口 {urban_population:,} 万人")
print(f"乡村人口 {rural_population:,} 万人")
print(f"城镇化率 {urbanization_rate:.2f} %")
# 验证数据一致性
if abs(total_population - (urban_population + rural_population)) > 0.01:
print("\n注意:总人口与城镇人口+乡村人口存在差异")
print(f"差异值: {abs(total_population - (urban_population + rural_population)):.2f} 万人")

View File

@@ -0,0 +1,166 @@
import requests
import json
import time
# API基础URL
BASE_URL = "http://localhost:8100/RuYuanZaiYuan"
EDUCATION_DATA_BASE_URL = "http://localhost:8100/EducationData"
# 支持的教育阶段
test_education_stages = [
("preschool", "学前"),
("primary", "小学"),
("junior", "初中"),
("senior", "高中")
]
# 测试年份
test_years = [2023, 2022, 2024]
def test_school_chart_interface():
"""测试获取教育阶段图表数据接口"""
print("\n===== 测试 /school/chart 接口 =====")
for stage_code, stage_name in test_education_stages:
url = f"{BASE_URL}/school/chart?education_stage={stage_code}"
print(f"\n测试 {stage_name}({stage_code}) 数据:")
try:
response = requests.get(url)
response.raise_for_status() # 如果状态码不是200抛出异常
data = response.json()
print(f"响应状态码: {response.status_code}")
print(f"响应数据: {json.dumps(data, ensure_ascii=False, indent=2)}")
# 验证返回数据的基本结构
assert "xAxis_data" in data, "返回数据缺少xAxis_data字段"
assert "series_data_0" in data, "返回数据缺少series_data_0字段(城区数据)"
assert "series_data_1" in data, "返回数据缺少series_data_1字段(镇区数据)"
assert "series_data_2" in data, "返回数据缺少series_data_2字段(乡村数据)"
assert "series_data_3" in data, "返回数据缺少series_data_3字段(总人数数据)"
assert "series_data_4" in data, "返回数据缺少series_data_4字段(2022年基数数据)"
assert "education_stage" in data, "返回数据缺少education_stage字段"
assert data["education_stage"] == stage_name, f"教育阶段名称不匹配,期望: {stage_name},实际: {data['education_stage']}"
print(f"{stage_name} 数据验证通过")
except requests.exceptions.RequestException as e:
print(f"❌ 请求失败: {e}")
except AssertionError as e:
print(f"❌ 数据验证失败: {e}")
# 添加短暂延迟,避免请求过于频繁
time.sleep(0.5)
# @router.get("/school/inschool/chart")
# async def get_in_school_chart_config(
# education_stage: str = Query(default="preschool", pattern="^(preschool|primary|junior|senior)$",
# description="教育阶段: preschool(学前), primary(小学), junior(初中), senior(高中)")
# ):
# return RuYuanZaiYuanModel.generate_in_school_education_config(education_stage)
#
def test_education_data_by_year():
"""测试获取指定年份教育数据接口"""
print("\n===== 测试 /EducationData/byYear 接口 =====")
# 测试不同年份的数据
for year in test_years:
url = f"{EDUCATION_DATA_BASE_URL}/byYear?year={year}"
print(f"\n测试 {year} 年数据:")
try:
response = requests.get(url)
response.raise_for_status() # 如果状态码不是200抛出异常
data = response.json()
print(f"响应状态码: {response.status_code}")
print(f"响应数据: {json.dumps(data, ensure_ascii=False, indent=2)}")
# 验证返回数据的基本结构
assert "code" in data, "返回数据缺少code字段"
assert "message" in data, "返回数据缺少message字段"
assert "data" in data, "返回数据缺少data字段"
assert data["code"] == 200, f"状态码不匹配,期望: 200实际: {data['code']}"
assert isinstance(data["data"], list), "data字段应为列表类型"
# 验证数据内容
if data["data"]:
# 检查每个数据项的结构
for item in data["data"]:
assert "education_stage" in item, "数据项缺少education_stage字段"
assert "school_count" in item, "数据项缺少school_count字段"
assert "teacher_count_10k" in item, "数据项缺少teacher_count_10k字段"
assert "enrollment_count_10k" in item, "数据项缺少enrollment_count_10k字段"
assert "admission_count_10k" in item, "数据项缺少admission_count_10k字段"
# 检查数值类型
assert isinstance(item["school_count"], (int, float)), "school_count应为数值类型"
assert isinstance(item["teacher_count_10k"], (int, float)), "teacher_count_10k应为数值类型"
assert isinstance(item["enrollment_count_10k"], (int, float)), "enrollment_count_10k应为数值类型"
assert isinstance(item["admission_count_10k"], (int, float)), "admission_count_10k应为数值类型"
print(f"{year} 年数据验证通过,共包含 {len(data['data'])} 个教育阶段的数据")
else:
print(f"⚠️ {year} 年未返回数据")
except requests.exceptions.RequestException as e:
print(f"❌ 请求失败: {e}")
except AssertionError as e:
print(f"❌ 数据验证失败: {e}")
# 添加短暂延迟,避免请求过于频繁
time.sleep(0.5)
# 测试边界值
print("\n===== 测试边界值 =====")
# 测试最小值年份
min_year = 2015
url = f"{EDUCATION_DATA_BASE_URL}/byYear?year={min_year}"
print(f"\n测试最小值年份 {min_year}:")
try:
response = requests.get(url)
response.raise_for_status()
print(f"响应状态码: {response.status_code}")
print(f"✅ 最小值年份测试通过")
except requests.exceptions.RequestException as e:
print(f"❌ 请求失败: {e}")
# 测试最大值年份
max_year = 2028
url = f"{EDUCATION_DATA_BASE_URL}/byYear?year={max_year}"
print(f"\n测试最大值年份 {max_year}:")
try:
response = requests.get(url)
response.raise_for_status()
print(f"响应状态码: {response.status_code}")
print(f"✅ 最大值年份测试通过")
except requests.exceptions.RequestException as e:
print(f"❌ 请求失败: {e}")
# 测试超出范围的年份
invalid_year = 2030
url = f"{EDUCATION_DATA_BASE_URL}/byYear?year={invalid_year}"
print(f"\n测试超出范围的年份 {invalid_year}:")
try:
response = requests.get(url)
if response.status_code == 422: # FastAPI的验证错误状态码
print(f"响应状态码: {response.status_code}")
print(f"✅ 超出范围年份正确返回验证错误")
else:
print(f"❌ 期望状态码422实际: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"❌ 请求失败: {e}")
if __name__ == "__main__":
# 测试原有接口
# test_school_chart_interface()
# 测试新添加的教育数据接口
test_education_data_by_year()

View File

@@ -0,0 +1,124 @@
import requests
import json
import time
# API基础URL
EDUCATION_DATA_BASE_URL = "http://localhost:8100/EducationData"
# 测试年份
test_years = [2023, 2022, 2024]
def test_population_data_by_year():
"""测试获取指定年份人口数据接口"""
print("\n===== 测试 /EducationData/populationByYear 接口 =====")
# 测试不同年份的数据
for year in test_years:
url = f"{EDUCATION_DATA_BASE_URL}/populationByYear?year={year}"
print(f"\n测试 {year} 年人口数据:")
try:
response = requests.get(url)
response.raise_for_status() # 如果状态码不是200抛出异常
data = response.json()
print(f"响应状态码: {response.status_code}")
print(f"响应数据: {json.dumps(data, ensure_ascii=False, indent=2)}")
# 验证返回数据的基本结构
assert "code" in data, "返回数据缺少code字段"
assert "message" in data, "返回数据缺少message字段"
assert "data" in data, "返回数据缺少data字段"
assert data["code"] == 200, f"状态码不匹配,期望: 200实际: {data['code']}"
# 验证数据内容
if data["data"]:
# 检查数据项的结构
population_data = data["data"]
assert "year" in population_data, "数据缺少year字段"
assert "total_population" in population_data, "数据缺少total_population字段"
assert "birth_population" in population_data, "数据缺少birth_population字段"
assert "urban_population" in population_data, "数据缺少urban_population字段"
assert "rural_population" in population_data, "数据缺少rural_population字段"
assert "urbanization_rate" in population_data, "数据缺少urbanization_rate字段"
# 检查数值类型
assert isinstance(population_data["year"], int), "year应为整数类型"
assert isinstance(population_data["total_population"], (int, float)), "total_population应为数值类型"
assert isinstance(population_data["birth_population"], (int, float)), "birth_population应为数值类型"
assert isinstance(population_data["urban_population"], (int, float)), "urban_population应为数值类型"
assert isinstance(population_data["rural_population"], (int, float)), "rural_population应为数值类型"
assert isinstance(population_data["urbanization_rate"], (int, float)), "urbanization_rate应为数值类型"
# 检查年份是否匹配
assert population_data["year"] == year, f"年份不匹配,期望: {year},实际: {population_data['year']}"
print(f"{year} 年人口数据验证通过")
else:
print(f"⚠️ {year} 年未返回人口数据")
except requests.exceptions.RequestException as e:
print(f"❌ 请求失败: {e}")
except AssertionError as e:
print(f"❌ 数据验证失败: {e}")
# 添加短暂延迟,避免请求过于频繁
time.sleep(0.5)
# 测试边界值
print("\n===== 测试边界值 =====")
# 测试最小值年份
min_year = 2015
url = f"{EDUCATION_DATA_BASE_URL}/populationByYear?year={min_year}"
print(f"\n测试最小值年份 {min_year}:")
try:
response = requests.get(url)
response.raise_for_status()
data = response.json()
print(f"响应状态码: {response.status_code}")
if data["code"] == 200 or data["code"] == 404:
print(f"✅ 最小值年份测试通过")
else:
print(f"❌ 状态码异常: {data['code']}")
except requests.exceptions.RequestException as e:
print(f"❌ 请求失败: {e}")
# 测试最大值年份
max_year = 2028
url = f"{EDUCATION_DATA_BASE_URL}/populationByYear?year={max_year}"
print(f"\n测试最大值年份 {max_year}:")
try:
response = requests.get(url)
response.raise_for_status()
data = response.json()
print(f"响应状态码: {response.status_code}")
if data["code"] == 200 or data["code"] == 404:
print(f"✅ 最大值年份测试通过")
else:
print(f"❌ 状态码异常: {data['code']}")
except requests.exceptions.RequestException as e:
print(f"❌ 请求失败: {e}")
# 测试超出范围的年份
invalid_year = 2030
url = f"{EDUCATION_DATA_BASE_URL}/populationByYear?year={invalid_year}"
print(f"\n测试超出范围的年份 {invalid_year}:")
try:
response = requests.get(url)
if response.status_code == 422: # FastAPI的验证错误状态码
print(f"响应状态码: {response.status_code}")
print(f"✅ 超出范围年份正确返回验证错误")
else:
data = response.json()
print(f"响应状态码: {response.status_code}")
print(f"响应数据: {json.dumps(data, ensure_ascii=False, indent=2)}")
print(f"❌ 期望状态码422实际: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"❌ 请求失败: {e}")
if __name__ == "__main__":
# 测试新添加的人口数据接口
test_population_data_by_year()

View File

@@ -74,7 +74,7 @@ def main():
},
# 初中教育
'junior_high_schools': {
'junior_schools': {
'years': [2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024],
'columns': [
{'year': 2015, 'urban': 'CF', 'town': 'CG', 'rural': 'CH'},
@@ -92,7 +92,7 @@ def main():
},
# 普通高中教育
'senior_high_schools': {
'senior_schools': {
'years': [2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024],
'columns': [
{'year': 2015, 'urban': 'DT', 'town': 'DU', 'rural': 'DV'},

View File

@@ -53,7 +53,7 @@ EDUCATION_STAGES = {
{'year': 2024, 'total_staff': 'EZ', 'urban_staff': 'FA', 'town_staff': 'FB', 'rural_staff': 'FC', 'total_teacher': 'FD', 'urban_teacher': 'FE', 'town_teacher': 'FF', 'rural_teacher': 'FG'}
]
},
'junior_high_teachers': {
'junior_teachers': {
'years': [2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024],
'columns': [
{'year': 2015, 'total_staff': 'FH', 'urban_staff': 'FI', 'town_staff': 'FJ', 'rural_staff': 'FK', 'total_teacher': 'FL', 'urban_teacher': 'FM', 'town_teacher': 'FN', 'rural_teacher': 'FO'},
@@ -68,7 +68,7 @@ EDUCATION_STAGES = {
{'year': 2024, 'total_staff': 'IB', 'urban_staff': 'IC', 'town_staff': 'ID', 'rural_staff': 'IE', 'total_teacher': 'IF', 'urban_teacher': 'IG', 'town_teacher': 'IH', 'rural_teacher': 'II'}
]
},
'senior_high_teachers': {
'senior_teachers': {
'years': [2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024],
'columns': [
{'year': 2015, 'total_staff': 'IJ', 'urban_staff': 'IK', 'town_staff': 'IL', 'rural_staff': 'IM', 'total_teacher': 'IN', 'urban_teacher': 'IO', 'town_teacher': 'IP', 'rural_teacher': 'IQ'},

View File

@@ -1,135 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>云南省学前教育数据</title>
<link rel="stylesheet" href="css/style.css">
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="js/jquery-3.7.1.min.js"></script>
<style>
.chart-controls {
margin: 20px 0;
text-align: center;
}
.btn-group {
display: inline-flex;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.btn {
padding: 10px 20px;
border: none;
background: #f0f0f0;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.btn.active {
background: #409eff;
color: white;
}
.btn:hover:not(.active) {
background: #e0e0e0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
</style>
</head>
<body>
<div class="container">
<h1>学前教育发展规模预测</h1>
<div class="chart-controls">
<div class="btn-group">
<button class="btn active" data-type="enroll">入园数总量</button>
<button class="btn" data-type="inschool">在园数总量</button>
</div>
</div>
<div id="preschoolChart" style="width: 100%; height: 500px;"></div>
<div id="message" class="message"></div>
</div>
<script>
// 初始化图表
var preschoolChart = echarts.init(document.getElementById('preschoolChart'));
var chartData = null;
var currentType = 'enroll';
// 显示加载状态
function showLoading() {
preschoolChart.showLoading();
}
// 隐藏加载状态
function hideLoading() {
preschoolChart.hideLoading();
}
// 显示错误信息
function showError(message) {
$('#message').text('错误: ' + message);
console.error(message);
}
// 渲染图表
function renderChart() {
if (!chartData) return;
var option = JSON.parse(JSON.stringify(chartData));
// 根据用户要求,现在显示所有系列数据,不需要过滤
preschoolChart.setOption(option);
}
// 加载学前教育数据
function loadPreschoolData() {
showLoading();
// 根据当前类型加载不同数据
var url = currentType === 'enroll' ?
'/RuYuanZaiYuan/school/preschool/chart' :
'/RuYuanZaiYuan/school/preschool/inschool/chart';
$.getJSON(url)
.done(function(data) {
chartData = data;
renderChart();
})
.fail(function(jqXHR, textStatus, errorThrown) {
showError(errorThrown || '获取数据失败,请检查接口是否正常');
})
.always(function() {
hideLoading();
});
}
// 按钮点击事件
$(document).ready(function() {
loadPreschoolData();
$('.btn').click(function() {
$('.btn').removeClass('active');
$(this).addClass('active');
currentType = $(this).data('type');
loadPreschoolData(); // 点击按钮后重新加载数据,而不是只渲染
});
// 窗口大小变化时调整图表
$(window).resize(function() {
preschoolChart.resize();
});
});
</script>
</body>
</html>

View File

@@ -166,8 +166,8 @@
</li>
</ul>
</div>
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="https://gcore.jsdelivr.net/npm/echarts@6.0.0/dist/echarts.min.js"></script>
<script type="text/javascript" src="js/jquery-3.7.1.min.js"></script>
<script type="text/javascript" src="js/echarts.min.js"></script>
<script language="JavaScript" src="js/index.js"></script>
<script language="JavaScript" src="js/data/index.js"></script>
</body>

View File

@@ -36,43 +36,108 @@ option_1_1 = {
name: "城区",
type: "bar",
data: [],
tooltip: { valueFormatter: (v) => v + " 万人" },
itemStyle: { borderRadius: [6, 6, 0, 0] },
},
{
name: "镇区",
type: "bar",
data: [ ],
data: [],
tooltip: { valueFormatter: (v) => v + " 万人" },
itemStyle: { borderRadius: [6, 6, 0, 0] },
},
{
name: "乡村",
type: "bar",
data: [ ],
data: [],
tooltip: { valueFormatter: (v) => v + " 万人" },
itemStyle: { borderRadius: [6, 6, 0, 0] },
},
{
name: "总入园数",
type: "line",
yAxisIndex: 1,
data: [
],
data: [],
tooltip: { valueFormatter: (v) => v + " 万人" },
},
{
name: "2022年基数",
type: "line",
yAxisIndex: 1,
data: [
null,
null,
null,
null,
null,
null,
null,
105.6714,
105.6714,
105.6714,
],
data: [],
tooltip: { valueFormatter: (v) => typeof v === "number" ? v + " 万人" : v },
},
],
};
option_1_2 = {
grid: { left: 0, right: 0, top: 40, bottom: 10, containLabel: true },
textStyle: { color: "#fff" },
tooltip: {
trigger: "axis",
axisPointer: { type: "cross", crossStyle: { color: "#999" } },
textStyle: { color: "#fff" },
backgroundColor: "rgba(96,98,102,0.8)",
borderColor: "rgba(255,255,255,0.3)",
borderWidth: 1,
},
legend: {
data: ["城区", "镇区", "乡村", "总在园数", "2022年基数"],
top: 0,
textStyle: { color: "#fff" },
icon: "roundRect",
itemWidth: 12,
itemHeight: 12,
},
xAxis: [
{
type: "category",
data: [],
axisPointer: { type: "shadow" },
axisLine: { lineStyle: { color: "#fff" } },
axisLabel: { color: "#fff" },
nameTextStyle: { color: "#fff" },
},
],
yAxis: [
{ type: "value", axisLabel: { formatter: "{value}", color: "#fff" } },
{ type: "value", axisLabel: { formatter: "{value}", color: "#fff" } },
],
series: [
{
name: "城区",
type: "bar",
data: [],
tooltip: { valueFormatter: (v) => v + " 万人" },
itemStyle: { borderRadius: [6, 6, 0, 0] },
},
{
name: "镇区",
type: "bar",
data: [],
tooltip: { valueFormatter: (v) => v + " 万人" },
itemStyle: { borderRadius: [6, 6, 0, 0] },
},
{
name: "乡村",
type: "bar",
data: [],
tooltip: { valueFormatter: (v) => v + " 万人" },
itemStyle: { borderRadius: [6, 6, 0, 0] },
},
{
name: "总在园数",
type: "line",
yAxisIndex: 1,
data: [],
tooltip: { valueFormatter: (v) => v + " 万人" },
},
{
name: "2022年基数",
type: "line",
yAxisIndex: 1,
data: [],
tooltip: { valueFormatter: (v) => typeof v === "number" ? v + " 万人" : v },
},
],
};

45
static/js/echarts.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -16,11 +16,11 @@ $(window).load(function () {
});
$(function () {
echarts_1();
echarts_1_1();
echarts_2();
// echarts_3();
function echarts_1() {
function echarts_1_1() {
var myChart = echarts.init(document.getElementById("echarts01"));
$.ajax({
@@ -29,14 +29,12 @@ $(function () {
dataType: "json",
url: "/RuYuanZaiYuan/school/preschool/chart",
success: function (res) {
option_1_1.series.forEach((seriesItem, index) => {
seriesItem.tooltip = { valueFormatter: (v) => v + " 万人" };
});
option_1_1.xAxis[0].data = res.xAxis_data;
option_1_1.series[0].data = res.series_data_1;
option_1_1.series[1].data = res.series_data_2;
option_1_1.series[2].data = res.series_data_3;
option_1_1.series[3].data = res.series_data_4;
option_1_1.series[0].data = res.series_data_0;
option_1_1.series[1].data = res.series_data_1;
option_1_1.series[2].data = res.series_data_2;
option_1_1.series[3].data = res.series_data_3;
option_1_1.series[4].data = res.series_data_4;
myChart.setOption(option_1_1);
window.addEventListener("resize", function () {
myChart.resize();
@@ -282,18 +280,12 @@ $(function () {
$(this).siblings().removeClass("active");
// 添加active类到当前点击的元素
$(this).addClass("active");
// 获取当前选中的标签数据
var selectedTab = $(this).data("tab");
// 这里可以添加根据标签切换内容的逻辑
console.log("切换到: " + selectedTab);
// 示例:根据标签切换图表数据
switch (selectedTab) {
case "入园数":
// 加载入园数数据
echarts_1();
echarts_1_1();
break;
case "在园数":
echarts_3();

5
static/js/jquery.js vendored

File diff suppressed because one or more lines are too long