584 lines
26 KiB
Python
584 lines
26 KiB
Python
"""
|
||
高考AI服务 v3 — 数据库驱动推荐 + 概率计算器
|
||
"""
|
||
import sqlite3, os, json, re, uuid
|
||
from flask import Flask, request, jsonify, send_file
|
||
from datetime import datetime
|
||
import urllib.request
|
||
import urllib.error
|
||
|
||
app = Flask(__name__)
|
||
|
||
# ===== 配置 =====
|
||
DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions"
|
||
DEEPSEEK_API_KEY = os.environ.get('DEEPSEEK_API_KEY', '')
|
||
DB_PATH = os.environ.get('GAOKAO_DB', '/www/wwwroot/gaokao/gaokao_henan.db')
|
||
STATIC_DIR = "/www/wwwroot/gaokao"
|
||
|
||
SESSIONS = {}
|
||
|
||
# ===== SYSTEM PROMPT =====
|
||
SYSTEM_PROMPT = """你是一个专业、温暖的高考志愿填报助手,专门为河南省考生服务。
|
||
|
||
【重要说明】
|
||
- 当前年份:2026年,2025年河南高考投档已完成,以下数据是最有价值的参考。
|
||
- 科目改革:2025年起河南新高考采用「3+1+2」模式,物理/历史分开招生。
|
||
- 2025年河南分数线:物理类特控535/本科427;历史类特控552/本科471;专科185。
|
||
|
||
【数据来源】
|
||
你有真实的院校和专业录取数据(2022-2025年),包括:
|
||
- 院校最低分 / 最低位次(按选科分组)
|
||
- 专业录取分
|
||
- 招生计划数
|
||
- 一分一段表(2025年)
|
||
当考生提供分数时,你应该结合这些真实数据给出推荐,而非凭空估算。
|
||
|
||
【推荐输出格式】(每组9项,缺一不可)
|
||
1. 院校名称:(选科要求)| 分数 | 位次 | 批次线差
|
||
2. 推荐理由:为什么适合考生(具体、个性化)
|
||
3. 报考策略:志愿梯度建议(冲/稳/保的具体填法)
|
||
4. 推荐专业:该院校的王牌专业及考生意向专业的录取情况说明
|
||
5. 城市实况:校区所在城市的气候、交通、经济发展水平(1-2句)
|
||
6. 食堂餐饮:食堂数量、菜系丰富度、消费水平(1-2句)
|
||
7. 住宿条件:宿舍几人间、有无空调热水、翻新情况(1-2句)
|
||
8. 就读体验:学风氛围、升学/就业资源、学术氛围(1-2句)
|
||
9. 风险提示:该院校/专业需要特别注意的风险(招生批次、专业分流、选科限制等,1-2句)
|
||
|
||
【分组规则】
|
||
- 冲:考生分数比学校最低分低0~15分(踩线,热档志愿)
|
||
- 稳:考生分数比学校最低分高0~20分(志愿组合核心区)
|
||
- 保:考生分数比学校最低分高20分以上(安全垫)
|
||
概率参考:冲≈30%、稳≈65%、保≈90%
|
||
|
||
【注意事项】
|
||
1. 优先参考2025年投档数据,2024年数据作为趋势参考。
|
||
2. 选科要求必须匹配,否则该院校不适合该考生。
|
||
3. 新高考「1+2」选科组合直接影响可报院校范围,务必确认选科是否满足。
|
||
4. 对于临床医学等长学制专业,提醒考生关注学制年限。
|
||
5. 对话风格:直接给出推荐,不要反问,不要列举选项,不要说"以下是参考"。
|
||
"""
|
||
|
||
# ===== 学费辅助 =====
|
||
def get_school_tuition(school_name, cur):
|
||
"""查询学校的学费信息,优先从 tuition 列取,兜底用 tuition_data 模块"""
|
||
# 优先从 DB 字段取
|
||
row = cur.execute(
|
||
"SELECT tuition FROM schools WHERE school_name=? AND tuition IS NOT NULL AND tuition!='' LIMIT 1",
|
||
(school_name,)
|
||
).fetchone()
|
||
if row and row[0]:
|
||
try:
|
||
import json as _json
|
||
return _json.loads(row[0])
|
||
except Exception:
|
||
pass
|
||
# 兜底:用 tuition_data 模块(内嵌,不依赖外部文件)
|
||
return _get_tuition_fallback(school_name)
|
||
|
||
def _get_tuition_fallback(school_name):
|
||
"""内嵌版学费计算逻辑(与 tuition_data.py 保持一致)"""
|
||
import json as _json
|
||
sn = school_name or ""
|
||
# 精确匹配表
|
||
EXACT = {
|
||
"郑州大学": ("普通类(理科)", 5000),
|
||
"郑州大学国际学院": ("国际学院", 25000),
|
||
"郑州大学(中外合作)": ("中外合作办学(普通专业)", 18000),
|
||
"郑州大学医学院": ("医学类", 5500),
|
||
"河南大学": ("普通类(文科)", 4400),
|
||
"河南大学(中外合作)": ("中外合作办学(普通专业)", 18000),
|
||
"河南师范大学": ("普通类(文科)", 4400),
|
||
"河南师范大学(中外合作)": ("中外合作办学(普通专业)", 18000),
|
||
"河南开封科技传媒学院": ("独立学院(普通专业)", 12000),
|
||
"河南大学民生学院": ("独立学院(普通专业)", 12000),
|
||
"河南师范大学新联学院": ("独立学院(普通专业)", 12000),
|
||
"中原工学院信息商务学院": ("独立学院(普通专业)", 12000),
|
||
"新乡医学院三全学院": ("独立学院(医学类)", 13000),
|
||
"河南理工大学万方科技学院": ("独立学院(普通专业)", 12000),
|
||
"郑州大学西亚斯国际学院": ("中外合作办学(普通专业)", 18000),
|
||
"河南科技大学": ("普通类(理科)", 5000),
|
||
"河南理工大学": ("普通类(理科)", 5000),
|
||
"河南农业大学": ("农林类", 4000),
|
||
"河南工业大学": ("普通类(理科)", 5000),
|
||
"华北水利水电大学": ("普通类(理科)", 5000),
|
||
"郑州轻工业大学": ("普通类(理科)", 5000),
|
||
"中原工学院": ("普通类(理科)", 5000),
|
||
"河南财经政法大学": ("普通类(文科)", 4400),
|
||
"郑州航空工业管理学院": ("普通类(文科)", 4400),
|
||
"河南中医药大学": ("医学类", 5500),
|
||
"新乡医学院": ("医学类", 5500),
|
||
"河南警察学院": ("普通类(文科)", 4400),
|
||
"郑州师范学院": ("普通类(文科)", 4400),
|
||
"洛阳师范学院": ("普通类(文科)", 4400),
|
||
"信阳师范学院": ("普通类(文科)", 4400),
|
||
"南阳师范学院": ("普通类(文科)", 4400),
|
||
"商丘师范学院": ("普通类(文科)", 4400),
|
||
"安阳师范学院": ("普通类(文科)", 4400),
|
||
"周口师范学院": ("普通类(文科)", 4400),
|
||
"许昌学院": ("普通类(文科)", 4400),
|
||
"洛阳理工学院": ("普通类(理科)", 5000),
|
||
"河南工程学院": ("普通类(理科)", 5000),
|
||
"南阳理工学院": ("普通类(理科)", 5000),
|
||
"河南城建学院": ("普通类(理科)", 5000),
|
||
"平顶山学院": ("普通类(文科)", 4400),
|
||
"河南牧业经济学院": ("农林类", 4000),
|
||
"黄河科技学院": ("独立学院(普通专业)", 12000),
|
||
"商丘学院": ("独立学院(普通专业)", 12000),
|
||
"郑州科技学院": ("独立学院(普通专业)", 12000),
|
||
"郑州升达经贸管理学院": ("独立学院(普通专业)", 12000),
|
||
"新乡工程学院": ("独立学院(普通专业)", 12000),
|
||
}
|
||
if sn in EXACT:
|
||
cat, amt = EXACT[sn]
|
||
return {"category": cat, "annual": amt, "remark": f"{cat} | 河南省发改委标准", "source": "fallback"}
|
||
# 关键字匹配
|
||
kw_joint = ["中外合作", "国际学院", "合作办学", "香港", "台湾"]
|
||
kw_private = ["独立学院", "民生学院", "信息商务学院", "应用技术"]
|
||
kw_arts = ["艺术", "音乐", "美术", "设计", "舞蹈", "传媒", "戏剧", "影视"]
|
||
kw_medical = ["医学", "药学", "护理", "临床", "口腔", "中医", "医科", "医大", "军医"]
|
||
kw_agri = ["农业", "林业", "园艺", "畜牧", "兽医", "水产"]
|
||
for kw in kw_joint:
|
||
if kw in sn:
|
||
if any(a in sn for a in kw_arts):
|
||
return {"category": "中外合作办学(艺术/医学类)", "annual": 22000, "remark": "中外合作办学(艺术/医学类) | 河南省发改委标准", "source": "fallback"}
|
||
return {"category": "中外合作办学(普通专业)", "annual": 18000, "remark": "中外合作办学 | 河南省发改委标准", "source": "fallback"}
|
||
for kw in kw_private:
|
||
if kw in sn:
|
||
if any(a in sn for a in kw_arts):
|
||
return {"category": "独立学院(艺术类)", "annual": 15000, "remark": "独立学院(艺术类) | 河南省发改委标准", "source": "fallback"}
|
||
return {"category": "独立学院(普通专业)", "annual": 12000, "remark": "独立学院 | 河南省发改委标准", "source": "fallback"}
|
||
for kw in kw_arts:
|
||
if kw in sn:
|
||
return {"category": "艺术类", "annual": 8000, "remark": "艺术类 | 河南省发改委标准", "source": "fallback"}
|
||
for kw in kw_medical:
|
||
if kw in sn:
|
||
return {"category": "医学类", "annual": 5500, "remark": "医学类 | 河南省发改委标准", "source": "fallback"}
|
||
for kw in kw_agri:
|
||
if kw in sn:
|
||
return {"category": "农林类", "annual": 4000, "remark": "农林类 | 河南省发改委标准", "source": "fallback"}
|
||
if "师范" in sn:
|
||
return {"category": "普通类(文科)", "annual": 4400, "remark": "师范类 | 河南省发改委标准", "source": "fallback"}
|
||
if "体育" in sn:
|
||
return {"category": "体育类", "annual": 5000, "remark": "体育类 | 河南省发改委标准", "source": "fallback"}
|
||
# 高职/专科
|
||
if any(k in sn for k in ["职业", "技术", "专科", "高职"]):
|
||
return {"category": "高职(普通专业)", "annual": 4200, "remark": "高职 | 河南省发改委标准", "source": "fallback"}
|
||
# 默认
|
||
return {"category": "普通类(理科)", "annual": 5000, "remark": "公办本科 | 河南省发改委标准", "source": "fallback"}
|
||
|
||
|
||
def get_db():
|
||
return sqlite3.connect(DB_PATH)
|
||
|
||
def score_to_rank(conn, subject, score):
|
||
cur = conn.execute(
|
||
'SELECT rank FROM yiyi WHERE year=2025 AND subject=? AND score<=? ORDER BY score DESC LIMIT 1',
|
||
(subject, score))
|
||
r = cur.fetchone()
|
||
return r[0] if r else None
|
||
|
||
def recommend(score, subject, batch='本科批', top_n=4):
|
||
conn = get_db()
|
||
student_rank = score_to_rank(conn, subject, score)
|
||
if not student_rank:
|
||
conn.close()
|
||
return None
|
||
|
||
cur = conn.execute('''
|
||
SELECT school_name, subject_req, min_score, min_rank, batch_diff
|
||
FROM schools
|
||
WHERE year=2025 AND subject=? AND batch=? AND min_rank IS NOT NULL AND min_score IS NOT NULL
|
||
ORDER BY min_score DESC
|
||
''', (subject, batch))
|
||
|
||
chong, wen, bao = [], [], []
|
||
for row in cur.fetchall():
|
||
sch_name, req, sch_score, sch_rank, diff = row
|
||
score_gap = sch_score - score
|
||
entry = {'学校': sch_name, '选科': req, '分数': sch_score, '位次': sch_rank, '线差': diff}
|
||
if 0 <= score_gap <= 15:
|
||
chong.append(entry)
|
||
elif -20 <= score_gap < 0:
|
||
wen.append(entry)
|
||
elif score_gap < -20:
|
||
bao.append(entry)
|
||
|
||
def dedup(items):
|
||
seen = {}
|
||
for it in items:
|
||
key = (it['学校'], it['分数'])
|
||
if key not in seen:
|
||
seen[key] = it
|
||
return list(seen.values())
|
||
|
||
chong, wen, bao = dedup(chong), dedup(wen), dedup(bao)
|
||
chong.sort(key=lambda x: x['分数'], reverse=True)
|
||
wen.sort(key=lambda x: x['分数'], reverse=True)
|
||
bao.sort(key=lambda x: x['分数'])
|
||
|
||
conn.close()
|
||
return {'student_rank': student_rank, 'student_score': score,
|
||
'subject': subject, 'batch': batch,
|
||
'chong': chong[:top_n], 'wen': wen[:top_n], 'bao': bao[:top_n]}
|
||
|
||
def format_recommend(r):
|
||
if not r:
|
||
return "(数据库暂无该分数段推荐数据,请结合往年经验分析)"
|
||
parts = []
|
||
parts.append(f"【数据库实时推荐】考生{r['student_score']}分(位次{r['student_rank']},{r['subject']}类)推荐如下:")
|
||
for label, items in [('冲志愿(考生分数比学校最低分低0-15分,风险较高)', r['chong']),
|
||
('稳志愿(考生分数比学校最低分高0-20分,稳妥之选)', r['wen']),
|
||
('保志愿(考生分数比学校最低分高20+,安全垫充足)', r['bao'])]:
|
||
parts.append(f"\n{label}")
|
||
for it in items:
|
||
parts.append(f" - {it['学校']}({it['选科']})| {it['分数']}分 | 位次{it['位次']} | 批次线差{it['线差']}")
|
||
return '\n'.join(parts)
|
||
|
||
def call_deepseek(messages, timeout=90):
|
||
data = json.dumps({
|
||
"model": "deepseek-v4-flash",
|
||
"messages": messages,
|
||
"max_tokens": 2500,
|
||
"temperature": 0.3,
|
||
}).encode("utf-8")
|
||
req = urllib.request.Request(
|
||
DEEPSEEK_API_URL, data=data,
|
||
headers={"Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json"},
|
||
method="POST"
|
||
)
|
||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||
result = json.loads(resp.read().decode("utf-8"))
|
||
return result["choices"][0]["message"]["content"]
|
||
|
||
def extract_score_subject(text):
|
||
m = re.search(r'(\d{2,3})\s*[分]', text)
|
||
score = int(m.group(1)) if m else None
|
||
subject = '物理类' if any(k in text for k in ['物理', '理科', '物理类']) else \
|
||
'历史类' if any(k in text for k in ['历史', '文科', '历史类']) else None
|
||
return score, subject
|
||
|
||
def build_reply(user_message, history_msgs):
|
||
score, subject = extract_score_subject(user_message)
|
||
db_context = ""
|
||
if score and subject:
|
||
r = recommend(score, subject)
|
||
if r:
|
||
db_context = "\n\n" + format_recommend(r)
|
||
prompt = SYSTEM_PROMPT + db_context
|
||
messages = [{"role": "system", "content": prompt}]
|
||
for h in history_msgs[-10:]:
|
||
messages.append({"role": "user", "content": h.get("user", "")})
|
||
if h.get("assistant"):
|
||
messages.append({"role": "assistant", "content": h.get("assistant")})
|
||
messages.append({"role": "user", "content": user_message})
|
||
return call_deepseek(messages), bool(db_context)
|
||
|
||
# ===== 路由 =====
|
||
|
||
@app.route("/health")
|
||
def health():
|
||
return jsonify({"status": "ok", "time": datetime.now().isoformat()})
|
||
|
||
@app.route("/chat", methods=["GET", "POST"])
|
||
@app.route("/ai-tool-chat", methods=["GET", "POST"])
|
||
def chat():
|
||
if request.method == "GET":
|
||
return send_file(f"{STATIC_DIR}/ai-chat.html")
|
||
body = request.get_json() or {}
|
||
user_message = body.get("message", "")
|
||
if not user_message:
|
||
for m in body.get("messages", []):
|
||
if isinstance(m, dict) and m.get("role") == "user":
|
||
user_message = m.get("content", "")
|
||
break
|
||
history = body.get("history", [])
|
||
try:
|
||
reply, db_used = build_reply(user_message, history)
|
||
except Exception as e:
|
||
return jsonify({"error": str(e), "reply": f"错误:{e}"}), 500
|
||
return jsonify({"reply": reply, "db_used": db_used})
|
||
|
||
@app.route("/api/chat/start", methods=["POST"])
|
||
def chat_start():
|
||
sid = str(uuid.uuid4())
|
||
SESSIONS[sid] = []
|
||
welcome = ("您好!我是高考志愿填报 AI 助手 🎓\n\n"
|
||
"请告诉我以下信息,我来帮你推荐院校:\n\n"
|
||
"1. **高考分数**(如:600分)\n"
|
||
"2. **科类**(物理类 / 历史类)\n"
|
||
"3. **想学的专业**(如:计算机、医学、法律等)\n\n"
|
||
"您也可以简单说:*物理类 600 分想学计算机*,我会直接给出推荐方案。")
|
||
SESSIONS[sid].append({"role": "assistant", "content": welcome})
|
||
return jsonify({"session_id": sid, "message": welcome})
|
||
|
||
@app.route("/api/chat/respond", methods=["POST"])
|
||
def chat_respond():
|
||
body = request.get_json() or {}
|
||
sid = body.get("session_id", "")
|
||
user_message = body.get("message", "")
|
||
if sid not in SESSIONS:
|
||
return jsonify({"error": "会话已过期,请刷新页面重新开始。"}), 400
|
||
history = SESSIONS[sid]
|
||
history.append({"role": "user", "content": user_message})
|
||
try:
|
||
reply, db_used = build_reply(user_message, history)
|
||
except Exception as e:
|
||
SESSIONS[sid].append({"role": "assistant", "content": f"抱歉,出了点问题:{e}"})
|
||
return jsonify({"message": f"抱歉,出了点问题:{e}", "done": False}), 200
|
||
history.append({"role": "assistant", "content": reply})
|
||
return jsonify({"message": reply, "done": False, "db_used": db_used})
|
||
|
||
@app.route("/api/prob", methods=["POST"])
|
||
def prob_calc():
|
||
body = request.get_json() or {}
|
||
score = int(body.get('score', 0))
|
||
subject = body.get('subject', '物理类')
|
||
# 表单科目映射到数据库yiyi表科目
|
||
subject_map = {'文科': '历史类', '理科': '物理类'}
|
||
subject = subject_map.get(subject, subject)
|
||
major = body.get('major', '不限')
|
||
region = body.get('region', '不限')
|
||
|
||
conn = get_db()
|
||
student_rank = score_to_rank(conn, subject, score)
|
||
if not student_rank:
|
||
conn.close()
|
||
return jsonify({'error': '该分数超出数据库范围'}), 400
|
||
|
||
# 专业/地区过滤:先从 majors 表查出符合条件的学校列表
|
||
allowed_schools = None
|
||
if major != '不限' or region != '不限':
|
||
# 城市→省份映射(majors表只有省份字段)
|
||
city_to_province = {'郑州':'河南','开封':'河南','洛阳':'河南','新乡':'河南',
|
||
'南阳':'河南','安阳':'河南','焦作':'河南','平顶山':'河南',
|
||
'信阳':'河南','周口':'河南','驻马店':'河南','许昌':'河南',
|
||
'漯河':'河南','三门峡':'河南','商丘':'河南','鹤壁':'河南',
|
||
'济源':'河南','省内':'河南','河南':'河南'}
|
||
prov = city_to_province.get(region, region) if region != '不限' else None
|
||
filter_clauses = []
|
||
params = []
|
||
if major != '不限':
|
||
kw_map = {'计算机':'计算机','电子信息':'电子信息','临床医学':'临床医学',
|
||
'经济金融':'经济','法学':'法学','师范':'师范'}
|
||
kw = kw_map.get(major, major)
|
||
filter_clauses.append("major LIKE ?")
|
||
params.append('%' + kw + '%')
|
||
if prov and prov != '不限':
|
||
filter_clauses.append("location = ?")
|
||
params.append(prov)
|
||
if filter_clauses:
|
||
cur2 = conn.execute(
|
||
"SELECT DISTINCT school_name FROM majors WHERE year=2025 AND subject=? AND " + " AND ".join(filter_clauses),
|
||
[subject] + params)
|
||
allowed_schools = set(r[0] for r in cur2.fetchall())
|
||
|
||
cur = conn.execute(
|
||
"SELECT school_name, subject_req, min_score, min_rank, batch_diff "
|
||
"FROM schools WHERE year=2025 AND subject=? AND batch='本科批' "
|
||
"AND min_rank IS NOT NULL AND min_score IS NOT NULL "
|
||
"ORDER BY min_score DESC",
|
||
(subject,))
|
||
rows = cur.fetchall()
|
||
conn.close()
|
||
|
||
chong, wen, bao = [], [], []
|
||
for row in rows:
|
||
sch_name, req, sch_score, sch_rank, diff = row
|
||
if allowed_schools is not None and sch_name not in allowed_schools:
|
||
continue
|
||
gap = sch_score - score
|
||
entry = {'school': sch_name, 'req': req, 'score': sch_score,
|
||
'rank': sch_rank, 'diff': diff}
|
||
if 0 <= gap <= 15:
|
||
chong.append(entry)
|
||
elif -20 <= gap < 0:
|
||
wen.append(entry)
|
||
elif gap < -20:
|
||
bao.append(entry)
|
||
|
||
def top(items, n=6, reverse=True):
|
||
return sorted(items, key=lambda x: x['score'], reverse=reverse)[:n]
|
||
|
||
def with_prob(items, score):
|
||
result = []
|
||
for it in items:
|
||
score_gap = it['score'] - score # 学校最低分 - 考生分数,正=冲,负=保
|
||
# 概率基于分差:直接反映风险,摆脱 rank 换算失真
|
||
if score_gap >= 15:
|
||
prob = 15 + min(15, score_gap - 15) * 1.0 # 15-30%
|
||
elif score_gap >= 0:
|
||
prob = 15 + score_gap * 1.0 # 冲区 15-30%
|
||
elif score_gap >= -20:
|
||
prob = 30 + (-score_gap) * 1.75 # 稳区 30-65%
|
||
else:
|
||
prob = 65 + min(25, -score_gap - 20) * 0.5 # 保区 65-90%
|
||
prob = min(95, max(5, prob))
|
||
|
||
tui = _get_tuition_fallback(it['school'])
|
||
|
||
result.append({
|
||
**it,
|
||
'prob': round(prob, 1),
|
||
'tuition_annual': tui.get('annual', 5000),
|
||
'tuition_category': tui.get('category', '普通类(理科)'),
|
||
'tuition_source': tui.get('source', 'fallback'),
|
||
})
|
||
return result
|
||
|
||
return jsonify({
|
||
'score': score, 'subject': subject,
|
||
'rank': student_rank,
|
||
'major_filter': major if major != '不限' else None,
|
||
'region_filter': region if region != '不限' else None,
|
||
'chong': with_prob(top(chong), score),
|
||
'wen': with_prob(top(wen), score),
|
||
'bao': with_prob(top(bao), score),
|
||
})
|
||
|
||
@app.route("/api/school/search", methods=["GET"])
|
||
def school_search():
|
||
q = request.args.get("q", "").strip()
|
||
subject = request.args.get("subject", "物理类")
|
||
if not q:
|
||
return jsonify({"schools": []})
|
||
cur = get_db().cursor()
|
||
rows = cur.execute("""
|
||
SELECT school_name, subject, min_score, min_rank, plan_count, plan_type, subject_req, subject_group
|
||
FROM schools
|
||
WHERE school_name LIKE ? AND subject = ?
|
||
ORDER BY min_score DESC LIMIT 20
|
||
""", ("%" + q + "%", subject)).fetchall()
|
||
schools = []
|
||
for r in rows:
|
||
tui = get_school_tuition(r[0], cur)
|
||
schools.append({
|
||
"school": r[0],
|
||
"subject": r[1],
|
||
"score": r[2],
|
||
"rank": r[3] or 0,
|
||
"num": r[4],
|
||
"type": r[5] or "普通类",
|
||
"req": r[6] or "",
|
||
"group": r[7] or "",
|
||
"tuition_annual": tui.get("annual", 5000),
|
||
"tuition_category": tui.get("category", "普通类(理科)"),
|
||
})
|
||
return jsonify({"schools": schools})
|
||
|
||
@app.route("/api/major/search", methods=["GET"])
|
||
def major_search():
|
||
school = request.args.get("school", "").strip()
|
||
major = request.args.get("major", "").strip()
|
||
subject = request.args.get("subject", "")
|
||
year = request.args.get("year", "2025")
|
||
batch = request.args.get("batch", "")
|
||
limit = min(int(request.args.get("limit", 50)), 100)
|
||
offset = int(request.args.get("offset", 0))
|
||
|
||
if not school and not major:
|
||
return jsonify({"majors": []})
|
||
|
||
cur = get_db().cursor()
|
||
conditions = []
|
||
params = []
|
||
|
||
if school:
|
||
conditions.append("school_name LIKE ?")
|
||
params.append("%" + school + "%")
|
||
if major:
|
||
conditions.append("major LIKE ?")
|
||
params.append("%" + major + "%")
|
||
if subject:
|
||
conditions.append("subject = ?")
|
||
params.append(subject)
|
||
if year:
|
||
conditions.append("year = ?")
|
||
params.append(year)
|
||
if batch:
|
||
conditions.append("batch = ?")
|
||
params.append(batch)
|
||
|
||
where = " AND ".join(conditions) if conditions else "1=1"
|
||
params.extend([limit, offset])
|
||
|
||
rows = cur.execute(f"""
|
||
SELECT school_name, major, major_code, subject_group, subject_req,
|
||
plan_count, min_score, min_rank, location, school_type,
|
||
is_985, is_211, subject, batch, year
|
||
FROM majors
|
||
WHERE {where}
|
||
ORDER BY min_score DESC
|
||
LIMIT ? OFFSET ?
|
||
""", params).fetchall()
|
||
|
||
majors = []
|
||
for r in rows:
|
||
majors.append({
|
||
"school": r[0],
|
||
"major": r[1],
|
||
"major_code": r[2] or "",
|
||
"group": r[3] or "",
|
||
"req": r[4] or "",
|
||
"plan": r[5] or 0,
|
||
"score": r[6] or 0,
|
||
"rank": r[7] or 0,
|
||
"location": r[8] or "",
|
||
"school_type": r[9] or "",
|
||
"is_985": bool(r[10]) if r[10] is not None else False,
|
||
"is_211": bool(r[11]) if r[11] is not None else False,
|
||
"subject": r[12],
|
||
"batch": r[13],
|
||
"year": r[14]
|
||
})
|
||
return jsonify({"majors": majors, "total": len(majors)})
|
||
|
||
@app.route("/api/recommend", methods=["POST"])
|
||
def api_recommend():
|
||
"""AI智能推荐 - 整合数据库查询+生成推荐方案 summary"""
|
||
body = request.get_json() or {}
|
||
score = int(body.get('score', 0))
|
||
category = body.get('category', '物理类')
|
||
batch = body.get('batch', '本科批')
|
||
|
||
if not score or score < 100 or score > 800:
|
||
return jsonify({"error": "请输入有效的分数(100-800)"}), 400
|
||
|
||
# 调用现有的 prob 计算获取推荐数据
|
||
from flask import make_response
|
||
try:
|
||
# 复用 prob_calc 的逻辑,但不返回它的 JSON 直接用
|
||
orig_result = prob_calc()
|
||
prob_data = orig_result.get_json()
|
||
except Exception as e:
|
||
return jsonify({"error": f"推荐服务暂时不可用:{e}"}), 500
|
||
|
||
chong = prob_data.get('chong', [])
|
||
wen = prob_data.get('wen', [])
|
||
bao = prob_data.get('bao', [])
|
||
|
||
total = len(chong) + len(wen) + len(bao)
|
||
subject_name = "物理类" if category in ["物理类", "理科"] else "历史类" if category in ["历史类", "文科"] else category
|
||
|
||
summary = (f"根据您提供的 {score} 分({subject_name}),数据库为您推荐了 {total} 所院校。"
|
||
f"其中「冲」志愿 {len(chong)} 所、「稳」志愿 {len(wen)} 所、「保」志愿 {len(bao)} 所。"
|
||
f"建议优先考虑稳志愿区间院校,风险相对可控。")
|
||
|
||
tips = [
|
||
f"您的分数 {score} 分,建议填报时保持「冲-稳-保」的梯度策略。",
|
||
f"冲志愿:分数低于学校录取线 0~15 分,风险较高,适合对专业有明确意向的考生。",
|
||
f"稳志愿:分数高于学校录取线 0~20 分,是最核心的志愿组合区。",
|
||
f"保志愿:分数高于学校录取线 20 分以上,建议填报 1-2 所确保录取。",
|
||
f"最终填报请参考《河南省2026年普通高校招生志愿填报指南》和各高校招生章程。",
|
||
]
|
||
|
||
return jsonify({
|
||
"summary": summary,
|
||
"chong": chong[:6],
|
||
"wen": wen[:6],
|
||
"bao": bao[:6],
|
||
"tips": tips,
|
||
})
|
||
|
||
if __name__ == "__main__":
|
||
app.run(host="0.0.0.0", port=8080, threaded=True)
|