Browse Source

合并目前代码

qw 7 tháng trước cách đây
mục cha
commit
b3d96fbad0
74 tập tin đã thay đổi với 2451 bổ sung0 xóa
  1. 3 0
      api/.gitignore
  2. 6 0
      api/README.md
  3. BIN
      api/SoilAcidification.db
  4. 442 0
      api/api_db.py
  5. 27 0
      api/app/__init__.py
  6. 9 0
      api/app/config.py
  7. 72 0
      api/app/database_models.py
  8. 269 0
      api/app/model.py
  9. 610 0
      api/app/routes.py
  10. 57 0
      api/app/utils.py
  11. 120 0
      api/model_optimize/GBSTR_filt.py
  12. 50 0
      api/model_optimize/KNN_filt.py
  13. 93 0
      api/model_optimize/RF_default.py
  14. 141 0
      api/model_optimize/RF_filt.py
  15. 148 0
      api/model_optimize/XGBR_filt.py
  16. BIN
      api/model_optimize/data/Acidity_reduce.xlsx
  17. BIN
      api/model_optimize/data/data_filt.xlsx
  18. 177 0
      api/model_optimize/data_increase.py
  19. 123 0
      api/model_optimize/data_increase_5cv_score.py
  20. 70 0
      api/model_optimize/model_predict.py
  21. 26 0
      api/model_optimize/model_saver.py
  22. BIN
      api/model_optimize/pkl/RF_filt.pkl
  23. BIN
      api/model_optimize/pkl/rf_model_1207_1530.pkl
  24. BIN
      api/model_optimize/pkl/rf_model_1214_1008.pkl
  25. BIN
      api/pkl/rf_model_1222_1922.pkl
  26. BIN
      api/pkl/rf_model_1222_1952.pkl
  27. BIN
      api/pkl/rf_model_1222_1954.pkl
  28. BIN
      api/pkl/rf_model_1222_1959.pkl
  29. BIN
      api/pkl/rf_model_1222_2012.pkl
  30. BIN
      api/pkl/rf_model_1225_0129.pkl
  31. BIN
      api/pkl/rf_model_1225_0141.pkl
  32. BIN
      api/pkl/rf_model_1225_0152.pkl
  33. BIN
      api/pkl/rf_model_1229_1916.pkl
  34. BIN
      api/pkl/rf_model_1229_2145.pkl
  35. BIN
      api/pkl/rf_model_1229_2155.pkl
  36. 8 0
      api/run.py
  37. BIN
      api/uploads/6bSQknDrwrRlde1f2539a3bc98595af5e8b8588c05a4.xlsx
  38. BIN
      api/uploads/9mYcsUjQQMaZ9b01e46167ade216795568db3c2d37e8.xlsx
  39. BIN
      api/uploads/A66orP06DVdAdd1616dcb86e932a94e2a99d69745d25.xlsx
  40. BIN
      api/uploads/CK9XvADp0egG1dd962093a2be4c35841ca8b245b1e3b.xlsx
  41. BIN
      api/uploads/FH8sVIA4ULaG1dd962093a2be4c35841ca8b245b1e3b.xlsx
  42. BIN
      api/uploads/JVlQLhd4fkWr1dd962093a2be4c35841ca8b245b1e3b.xlsx
  43. BIN
      api/uploads/NLPOdpDyac0b92585f8b5d6cc17655671f9cc9ceb93a.xlsx
  44. BIN
      api/uploads/VggWySJFG0li1dd962093a2be4c35841ca8b245b1e3b.xlsx
  45. BIN
      api/uploads/WEvCgKQR8ptBd46529682c0a74b35dd76bec86e9f05e.xlsx
  46. BIN
      api/uploads/WeQ6qtM436ise129f6334fd40c9f68ed74586a5cd77c.xlsx
  47. BIN
      api/uploads/ZUmVAEVHQGEK92585f8b5d6cc17655671f9cc9ceb93a.xlsx
  48. BIN
      api/uploads/aENap7i3PtIS92585f8b5d6cc17655671f9cc9ceb93a.xlsx
  49. BIN
      api/uploads/atLyS0lcgvUVdd1616dcb86e932a94e2a99d69745d25.xlsx
  50. BIN
      api/uploads/datasets/dataset_2.xlsx
  51. BIN
      api/uploads/datasets/dataset_29.xlsx
  52. BIN
      api/uploads/datasets/dataset_30.xlsx
  53. BIN
      api/uploads/datasets/dataset_31.xlsx
  54. BIN
      api/uploads/datasets/dataset_32.xlsx
  55. BIN
      api/uploads/datasets/dataset_33.xlsx
  56. BIN
      api/uploads/datasets/dataset_34.xlsx
  57. BIN
      api/uploads/datasets/dataset_35.xlsx
  58. BIN
      api/uploads/datasets/dataset_36.xlsx
  59. BIN
      api/uploads/datasets/dataset_37.xlsx
  60. BIN
      api/uploads/datasets/dataset_38.xlsx
  61. BIN
      api/uploads/datasets/dataset_39.xlsx
  62. BIN
      api/uploads/datasets/dataset_40.xlsx
  63. BIN
      api/uploads/datasets/dataset_41.xlsx
  64. BIN
      api/uploads/datasets/dataset_42.xlsx
  65. BIN
      api/uploads/datasets/dataset_6.xlsx
  66. BIN
      api/uploads/datasets/dataset_8.xlsx
  67. BIN
      api/uploads/gZPXSLyqib3F1c15940d87f5acd40d4a4b44a26b103a.xlsx
  68. BIN
      api/uploads/gc4ziL16anlB92585f8b5d6cc17655671f9cc9ceb93a.xlsx
  69. BIN
      api/uploads/jnZfDmnygaAV1dd962093a2be4c35841ca8b245b1e3b.xlsx
  70. BIN
      api/uploads/l6C9XURMYwyN92585f8b5d6cc17655671f9cc9ceb93a.xlsx
  71. BIN
      api/uploads/n3K10t9nhqFZd46529682c0a74b35dd76bec86e9f05e.xlsx
  72. BIN
      api/uploads/oOYk1nOo3mfD1dd962093a2be4c35841ca8b245b1e3b.xlsx
  73. BIN
      api/uploads/wyrZ1vW7Tw731dd962093a2be4c35841ca8b245b1e3b.xlsx
  74. BIN
      environment.yml

+ 3 - 0
api/.gitignore

@@ -0,0 +1,3 @@
+app/__pycache__
+.idea
+model_optimize/__pycache__

+ 6 - 0
api/README.md

@@ -0,0 +1,6 @@
+# 环境搭建
+
+根据`environment.yml`文件新建conda环境
+~~~ 
+conda env create -f environment.yml
+~~~

BIN
api/SoilAcidification.db


+ 442 - 0
api/api_db.py

@@ -0,0 +1,442 @@
+import os
+import sqlite3
+from flask import Flask, jsonify, request, send_file, g
+from werkzeug.utils import secure_filename
+from flask_cors import CORS
+import pandas as pd
+from io import BytesIO
+import logging
+
+app = Flask(__name__)
+
+# 设置数据库文件和上传文件夹的路径
+DATABASE = 'SoilAcidification.db'
+UPLOAD_FOLDER = 'uploads'
+app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
+os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
+
+# 跨源资源共享(CORS)配置,允许跨域请求
+CORS(app)
+
+# 设置日志
+logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+# 创建一个数据库连接池
+def get_db():
+    db = getattr(g, '_database', None)
+    if db is None:
+        db = g._database = sqlite3.connect(DATABASE)
+    return db
+
+# 获取列名的函数
+def get_column_names(table_name):
+    db = get_db()
+    cur = db.cursor()
+    cur.execute(f"PRAGMA table_info({table_name});")
+    columns = [column[1] for column in cur.fetchall()]
+    db.close()
+    return columns
+
+@app.after_request
+def add_headers(response):
+    response.headers['Content-Type'] = 'application/json; charset=utf-8'
+    return response
+
+@app.route('/')
+def index():
+    return 'Hello, World!'
+
+# 有这个函数来获取列名
+def get_column_names(table_name):
+    """
+    根据表名获取对应的列名(字段)。
+    """
+    if table_name == 'current_reflux':
+        # 返回列名映射
+        return ['OM', 'CL', 'CEC', 'H_plus', 'HN', 'Al3_plus', 'Free_alumina', 'Free_iron_oxides', 'Delta_pH'], \
+               ["有机质含量", "土壤粘粒重量", "阳离子交换量", "氢离子含量", "硝态氮含量", "铝离子含量", "游离氧化铝", "游离氧化铁", "酸碱差值"]
+    elif table_name == 'current_reduce':
+        # 返回列名映射
+        return ['Q_over_b', 'pH', 'OM', 'CL', 'H', 'Al'], \
+               ["比值", "酸碱度", "有机质含量", "氯离子含量", "氢离子含量", "铝离子含量"]
+    else:
+        return [], []  # 不支持的表名,返回空列表
+
+
+@app.route('/download_template', methods=['GET'])
+def download_template():
+        """
+        根据表名生成模板文件(Excel 或 CSV),并返回下载。
+        :return: 返回模板文件
+        """
+        table_name = request.args.get('table')  # 从请求参数中获取表名
+        if not table_name:
+            return jsonify({'error': '表名参数缺失'}), 400
+
+        # 获取表的列名
+        try:
+            column_names = get_column_names(table_name)
+        except sqlite3.Error as e:
+            return jsonify({'error': f'获取列名失败: {str(e)}'}), 400
+
+        if not column_names:
+            return jsonify({'error': f'未找到表 {table_name} 或列名为空'}), 400
+
+        # 根据表名映射列名(可自定义标题)
+        if table_name == 'current_reflux':
+            column_titles = ["有机质含量", "土壤粘粒重量", "阳离子交换量", "氢离子含量", "硝态氮含量", "铝离子含量",
+                             "游离氧化铝", "游离氧化铁", "酸碱差值"]
+        elif table_name == 'current_reduce':
+            column_titles = ["比值", "酸碱度", "有机质含量", "氯离子含量", "氢离子含量1", "铝离子含量1"]
+        else:
+            return jsonify({'error': f'不支持的表名 {table_name}'}), 400
+
+        # 使用 pandas 创建一个空的 DataFrame,列名为自定义的标题
+        df = pd.DataFrame(columns=column_titles)
+
+        # 根据需求选择模板格式:Excel 或 CSV
+        file_format = request.args.get('format', 'excel').lower()
+        if file_format == 'csv':
+            # 将 DataFrame 写入内存中的 CSV 文件
+            output = BytesIO()
+            df.to_csv(output, index=False, encoding='utf-8')
+            output.seek(0)  # 返回文件开头
+            return send_file(output, as_attachment=True, download_name=f'{table_name}_template.csv',
+                             mimetype='text/csv')
+
+        else:
+            # 默认生成 Excel 文件
+            output = BytesIO()
+            df.to_excel(output, index=False, engine='openpyxl')
+            output.seek(0)  # 返回文件开头
+            return send_file(output, as_attachment=True, download_name=f'{table_name}_template.xlsx',
+                             mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+
+# 模板下载接口
+@app.route('/import_data', methods=['POST'])
+def import_data():
+    """
+    导入数据接口,根据表名将 Excel 或 CSV 数据导入到对应的数据库表中。
+    """
+    # 检查是否有文件
+    if 'file' not in request.files:
+        logging.error("请求中没有文件")
+        return jsonify({'success': False, 'message': '文件缺失'}), 400
+
+    file = request.files['file']
+    table_name = request.form.get('table')
+
+    # 检查表名参数
+    if not table_name:
+        logging.error("缺少表名参数")
+        return jsonify({'success': False, 'message': '缺少表名参数'}), 400
+
+    # 检查文件是否为空
+    if file.filename == '':
+        logging.error("文件为空")
+        return jsonify({'success': False, 'message': '未选择文件'}), 400
+
+    # 获取表的字段映射
+    db_columns, display_titles = get_column_names(table_name)
+
+    # 校验是否支持该表名
+    if not db_columns:
+        logging.error(f"不支持的表名: {table_name}")
+        return jsonify({'success': False, 'message': f'不支持的表名: {table_name}'}), 400
+
+    logging.info(f"表名 {table_name} 对应的字段: {db_columns}")
+
+    # 中文列名到数据库列名的映射
+    column_name_mapping = {
+        "有机质含量": "OM",
+        "土壤粘粒重量": "CL",
+        "阳离子交换量": "CEC",
+        "氢离子含量": "H_plus",
+        "硝态氮含量": "HN",
+        "铝离子含量": "Al3_plus",
+        "游离氧化铝": "Free_alumina",
+        "游离氧化铁": "Free_iron_oxides",
+        "酸碱差值": "Delta_pH",
+        "比值": "Q_over_b",
+        "酸碱度": "pH",
+        "氯离子含量": "CL",
+        "氢离子含量1": "H",
+        "铝离子含量1": "Al"
+    }
+
+    try:
+        # 保存上传文件到临时目录
+        temp_path = os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(file.filename))
+        logging.info(f"正在保存文件到临时路径: {temp_path}")
+        file.save(temp_path)
+
+        # 读取文件内容(支持 Excel 和 CSV 格式)
+        if file.filename.endswith('.xlsx'):
+            logging.info(f"读取 Excel 文件: {file.filename}")
+            df = pd.read_excel(temp_path)
+        elif file.filename.endswith('.csv'):
+            logging.info(f"读取 CSV 文件: {file.filename}")
+            df = pd.read_csv(temp_path)
+        else:
+            logging.error("仅支持 Excel 和 CSV 文件")
+            return jsonify({'success': False, 'message': '仅支持 Excel 和 CSV 文件'}), 400
+
+        logging.info(f"文件读取成功,列名: {df.columns.tolist()}")
+
+        # 1. 移除“序号”列(如果存在)
+        if '序号' in df.columns:
+            df = df.drop(columns=['序号'])
+
+        # 2. 将文件中的列名(中文)转换为数据库列名(英文)
+        df.columns = [column_name_mapping.get(col, col) for col in df.columns]
+
+        logging.info(f"转换后的列名: {df.columns.tolist()}")
+
+        # 校验文件的列是否与数据库表字段匹配
+        if not set(db_columns).issubset(set(df.columns)):
+            logging.error(f"文件列名与数据库表不匹配. 文件列: {df.columns.tolist()}, 期望列: {db_columns}")
+            return jsonify({'success': False, 'message': '文件列名与数据库表不匹配'}), 400
+
+        logging.info("开始清洗数据:移除空行并按需要格式化")
+        # 数据清洗:移除空行并按需要格式化
+        df_cleaned = df[db_columns].dropna()
+        logging.info(f"清洗后数据行数: {len(df_cleaned)}")
+
+        # 插入数据到数据库
+        conn = get_db()
+        logging.info(f"开始将数据插入到表 {table_name} 中")
+
+        # 使用事务确保操作的一致性
+        with conn:
+            df_cleaned.to_sql(table_name, conn, if_exists='append', index=False)
+
+        logging.info(f"数据成功插入到表 {table_name} 中,插入行数: {len(df_cleaned)}")
+
+        # 删除临时文件
+        os.remove(temp_path)
+        logging.info(f"临时文件 {temp_path} 删除成功")
+
+        return jsonify({'success': True, 'message': '数据导入成功'}), 200
+
+    except Exception as e:
+        logging.error(f"导入失败: {e}", exc_info=True)  # 捕获堆栈信息,方便调试
+        return jsonify({'success': False, 'message': f'导入失败: {str(e)}'}), 500
+
+
+# 导出数据接口
+@app.route('/export_data', methods=['GET'])
+def export_data():
+    """
+    根据表名从数据库导出数据,并返回 Excel 或 CSV 格式的文件
+    :return: 返回文件
+    """
+    # 获取表名和文件格式
+    table_name = request.args.get('table')
+    if not table_name:
+        return jsonify({'error': '缺少表名参数'}), 400
+
+    file_format = request.args.get('format', 'excel').lower()  # 默认生成 Excel 文件
+
+    # 获取数据
+    conn = get_db()
+    query = f"SELECT * FROM {table_name};"  # 查询表中的所有数据
+    df = pd.read_sql(query, conn)
+    conn.close()
+
+    if file_format == 'csv':
+        # 将数据保存为 CSV 格式
+        output = BytesIO()
+        df.to_csv(output, index=False)
+        output.seek(0)
+        return send_file(output, as_attachment=True, download_name=f'{table_name}_data.csv',
+                         mimetype='text/csv')
+
+    else:
+        # 默认生成 Excel 格式
+        output = BytesIO()
+        df.to_excel(output, index=False, engine='openpyxl')
+        output.seek(0)
+        return send_file(output, as_attachment=True, download_name=f'{table_name}_data.xlsx',
+                         mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+
+# 添加记录
+@app.route('/add_item', methods=['POST'])
+def add_item():
+    db = get_db()
+    try:
+        # 确保请求体是JSON格式
+        data = request.get_json()
+        if not data:
+            raise ValueError("No JSON data provided")
+
+        table_name = data.get('table')
+        item_data = data.get('item')
+
+        if not table_name or not item_data:
+            return jsonify({'error': 'Missing table name or item data'}), 400
+
+        cur = db.cursor()
+
+        # 动态构建 SQL 语句
+        columns = ', '.join(item_data.keys())
+        placeholders = ', '.join(['?'] * len(item_data))
+        sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
+        cur.execute(sql, tuple(item_data.values()))
+        db.commit()
+
+        return jsonify({'success': True, 'message': 'Item added successfully'}), 201
+
+    except ValueError as e:
+        return jsonify({'error': str(e)}), 400
+    except KeyError as e:
+        return jsonify({'error': f'Missing data field: {e}'}), 400
+    except sqlite3.IntegrityError as e:
+        return jsonify({'error': 'Database integrity error', 'details': str(e)}), 409
+    except sqlite3.Error as e:
+        return jsonify({'error': 'Database error', 'details': str(e)}), 500
+    finally:
+        db.close()
+
+# 删除记录
+@app.route('/delete_item', methods=['POST'])
+def delete_item():
+    data = request.get_json()
+    table_name = data.get('table')
+    condition = data.get('condition')
+
+    # 日志:检查是否收到正确的参数
+    logging.debug(f"Received data: table={table_name}, condition={condition}")
+
+    if not table_name or not condition:
+        logging.error("缺少表名或条件参数")
+        return jsonify({"success": False, "message": "缺少表名或条件参数"}), 400
+
+    try:
+        key, value = condition.split('=')
+    except ValueError:
+        logging.error(f"条件格式错误,received condition: {condition}")
+        return jsonify({"success": False, "message": "条件格式错误,应为 'key=value'"}), 400
+
+    db = get_db()
+    cur = db.cursor()
+
+    # 日志:确认开始删除操作
+    logging.debug(f"Attempting to delete from {table_name} where {key} = {value}")
+
+    try:
+        cur.execute(f"DELETE FROM {table_name} WHERE {key} = ?", (value,))
+        db.commit()
+
+        # 日志:删除成功
+        logging.info(f"Record deleted from {table_name} where {key} = {value}")
+        return jsonify({"success": True, "message": "记录删除成功"}), 200
+    except sqlite3.Error as e:
+        db.rollback()
+        # 日志:捕获删除失败的错误
+        logging.error(f"Failed to delete from {table_name} where {key} = {value}. Error: {e}")
+        return jsonify({"success": False, "message": f"删除失败: {e}"}), 400
+
+# 更新记录
+@app.route('/update_item', methods=['PUT'])
+def update_record():
+    data = request.get_json()
+
+    if not data or 'table' not in data or 'item' not in data:
+        return jsonify({"success": False, "message": "请求数据不完整"}), 400
+
+    table_name = data['table']
+    item = data['item']
+
+    if not item or next(iter(item.keys())) is None:
+        return jsonify({"success": False, "message": "记录数据为空"}), 400
+
+    id_key = next(iter(item.keys()))
+    record_id = item[id_key]
+    updates = {key: value for key, value in item.items() if key != id_key}
+
+    db = get_db()
+    cur = db.cursor()
+
+    try:
+        record_id = int(record_id)
+    except ValueError:
+        return jsonify({"success": False, "message": "ID 必须是整数"}), 400
+
+    parameters = list(updates.values()) + [record_id]
+    set_clause = ','.join([f"{k} = ?" for k in updates.keys()])
+    sql = f"UPDATE {table_name} SET {set_clause} WHERE {id_key} = ?"
+
+    try:
+        cur.execute(sql, parameters)
+        db.commit()
+        if cur.rowcount == 0:
+            return jsonify({"success": False, "message": "未找到要更新的记录"}), 404
+        return jsonify({"success": True, "message": "数据更新成功"}), 200
+    except sqlite3.Error as e:
+        db.rollback()
+        return jsonify({"success": False, "message": f"更新失败: {e}"}), 400
+
+# 查询记录
+@app.route('/search/record', methods=['GET'])
+def sql_search():
+    try:
+        data = request.get_json()
+        sql_table = data['table']
+        record_id = data['id']
+
+        db = get_db()
+        cur = db.cursor()
+
+        sql = f"SELECT * FROM {sql_table} WHERE id = ?"
+        cur.execute(sql, (record_id,))
+
+        rows = cur.fetchall()
+        column_names = [desc[0] for desc in cur.description]
+
+        if not rows:
+            return jsonify({'error': '未查找到对应数据。'}), 400
+
+        results = []
+        for row in rows:
+            result = {column_names[i]: row[i] for i in range(len(row))}
+            results.append(result)
+
+        cur.close()
+        db.close()
+
+        return jsonify(results), 200
+
+    except sqlite3.Error as e:
+        return jsonify({'error': str(e)}), 400
+    except KeyError as e:
+        return jsonify({'error': f'缺少必要的数据字段: {e}'}), 400
+
+# 提供表格数据
+@app.route('/tables', methods=['POST'])
+def get_table():
+    data = request.get_json()
+    table_name = data.get('table')
+
+    if not table_name:
+        return jsonify({'error': '需要表名'}), 400
+
+    db = get_db()
+    try:
+        cur = db.cursor()
+        cur.execute(f"SELECT * FROM {table_name}")
+        rows = cur.fetchall()
+
+        if not rows:
+            return jsonify({'error': f'表 {table_name} 为空或不存在'}), 400
+
+        headers = [description[0] for description in cur.description]
+        return jsonify(rows=rows, headers=headers), 200
+    except sqlite3.Error as e:
+        return jsonify({'error': str(e)}), 400
+    finally:
+        db.close()
+
+if __name__ == '__main__':
+    app.run(debug=True)

+ 27 - 0
api/app/__init__.py

@@ -0,0 +1,27 @@
+from flask import Flask
+from flask_cors import CORS
+from . import config
+from flask_sqlalchemy import SQLAlchemy
+from flask_migrate import Migrate
+import logging
+
+# 创建 SQLAlchemy 全局实例
+db = SQLAlchemy()
+
+# 创建并配置 Flask 应用
+def create_app():
+    app = Flask(__name__)
+    CORS(app)
+    # 进行初始配置,加载配置文件等
+    app.config.from_object(config.Configs)
+    app.logger.setLevel(logging.DEBUG)
+    # 初始化 SQLAlchemy
+    db.init_app(app)
+
+    # 初始化 Flask-Migrate
+    migrate = Migrate(app, db)
+
+    # 导入路由
+    from . import routes
+    app.register_blueprint(routes.bp)
+    return app

+ 9 - 0
api/app/config.py

@@ -0,0 +1,9 @@
+import os
+
+class Configs:
+    SECRET_KEY = 'your_secret_key'
+    DEBUG = True
+    MODEL_PATH = 'model_optimize/pkl/RF_filt.pkl'
+    DATABASE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'SoilAcidification.db')
+    SQLALCHEMY_DATABASE_URI = f'sqlite:///{DATABASE}'
+    UPLOAD_FOLDER = 'uploads/datasets'

+ 72 - 0
api/app/database_models.py

@@ -0,0 +1,72 @@
+from typing import List, Optional
+
+from sqlalchemy import Column, Float, ForeignKey, Integer, String, TIMESTAMP, Table, Text, text
+from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
+import datetime
+
+class Base(DeclarativeBase):
+    pass
+
+
+class Datasets(Base):
+    __tablename__ = 'Datasets'
+
+    Dataset_name: Mapped[str] = mapped_column(Text)
+    Row_count: Mapped[int] = mapped_column(Integer)
+    Dataset_type: Mapped[str] = mapped_column(Text)
+    Dataset_ID: Mapped[Optional[int]] = mapped_column(Integer, primary_key=True)
+    Dataset_description: Mapped[Optional[str]] = mapped_column(Text)
+    Uploaded_at: Mapped[Optional[datetime.datetime]] = mapped_column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'))
+    Status: Mapped[Optional[str]] = mapped_column(Text, server_default=text("'pending'"))
+
+
+class Models(Base):
+    __tablename__ = 'Models'
+
+    ModelID: Mapped[Optional[int]] = mapped_column(Integer, primary_key=True)
+    Model_name: Mapped[str] = mapped_column(Text, nullable=False)
+    Model_type: Mapped[str] = mapped_column(Text, nullable=False)
+    Created_at: Mapped[Optional[datetime.datetime]] = mapped_column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'))
+    Description: Mapped[Optional[str]] = mapped_column(Text)
+    DatasetID: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('Datasets.Dataset_ID'))
+    ModelFilePath: Mapped[Optional[str]] = mapped_column(Text)
+    Data_type: Mapped[Optional[str]] = mapped_column(Text)  # 新增字段
+
+    ModelParameters: Mapped[List['ModelParameters']] = relationship('ModelParameters', back_populates='Models_')
+
+class CurrentReduce(Base):
+    __tablename__ = 'current_reduce'
+
+    id: Mapped[int] = mapped_column(Integer, primary_key=True)
+    Q_over_b: Mapped[float] = mapped_column(Float)
+    pH: Mapped[float] = mapped_column(Float)
+    OM: Mapped[float] = mapped_column(Float)
+    CL: Mapped[float] = mapped_column(Float)
+    H: Mapped[float] = mapped_column(Float)
+    Al: Mapped[float] = mapped_column(Float)
+
+
+class CurrentReflux(Base):
+    __tablename__ = 'current_reflux'
+
+    id: Mapped[int] = mapped_column(Integer, primary_key=True)
+    OM: Mapped[float] = mapped_column(Float)
+    CL: Mapped[float] = mapped_column(Float)
+    CEC: Mapped[float] = mapped_column(Float)
+    H_plus: Mapped[float] = mapped_column(Float)
+    HN: Mapped[float] = mapped_column(Float)
+    Al3_plus: Mapped[float] = mapped_column(Float)
+    Free_alumina: Mapped[float] = mapped_column(Float)
+    Free_iron_oxides: Mapped[float] = mapped_column(Float)
+    Delta_pH: Mapped[float] = mapped_column(Float)
+
+
+class ModelParameters(Base):
+    __tablename__ = 'ModelParameters'
+
+    ParamID: Mapped[int] = mapped_column(Integer, primary_key=True)
+    ModelID: Mapped[int] = mapped_column(ForeignKey('Models.ModelID'))
+    ParamName: Mapped[str] = mapped_column(Text)
+    ParamValue: Mapped[str] = mapped_column(Text)
+
+    Models_: Mapped['Models'] = relationship('Models', back_populates='ModelParameters')

+ 269 - 0
api/app/model.py

@@ -0,0 +1,269 @@
+import datetime
+import os
+import pickle
+import pandas as pd
+from flask_sqlalchemy.session import Session
+from sklearn.ensemble import RandomForestRegressor
+from sklearn.model_selection import train_test_split, cross_val_score
+from sqlalchemy import text
+
+from .database_models import Models, Datasets
+
+
+
+# 加载模型
+def load_model(model_name):
+    file_path = f'model_optimize/pkl/{model_name}.pkl'
+    with open(file_path, 'rb') as f:
+        return pickle.load(f)
+
+
+# 模型预测
+def predict(input_data: pd.DataFrame, model_name):
+    # 初始化模型
+    model = load_model(model_name)  # 根据指定的模型名加载模型
+    predictions = model.predict(input_data)
+    return predictions.tolist()
+
+
+
+def train_and_save_model(session, model_type, model_name, model_description, data_type, dataset_id=None):
+    if not dataset_id:
+        # 直接创建新的数据集并复制数据
+        dataset_id = save_current_dataset(session, data_type)
+
+    # 从新复制的数据集表中加载数据
+    dataset_table_name = f"dataset_{dataset_id}"
+    dataset = pd.read_sql_table(dataset_table_name, session.bind)
+
+    if dataset.empty:
+        raise ValueError(f"Dataset {dataset_id} is empty or not found.")
+
+    # 数据准备
+    X = dataset.iloc[:, :-1]
+    y = dataset.iloc[:, -1]
+
+    # 训练模型
+    model = train_model_by_type(X, y, model_type)
+
+    # 保存模型到数据库
+    save_model(session, model, model_name, model_type, model_description, dataset_id, data_type)
+
+
+# # 保存模型参数
+    # save_model_parameters(model, saved_model.ModelID)
+
+    # # 计算评估指标(如MSE)
+    # y_pred = model.predict(X)
+    # mse = mean_squared_error(y, y_pred)
+    #
+    # return saved_model, mse
+
+def save_current_dataset(session, data_type):
+    """
+    创建一个新的数据集条目,并复制对应的数据类型表的数据。
+
+    Args:
+    session (Session): SQLAlchemy session对象。
+    data_type (str): 数据集的类型,如 'reduce' 或 'reflux'。
+
+    Returns:
+    int: 新保存的数据集的ID。
+    """
+    # 创建一个新的数据集条目
+    new_dataset = Datasets(
+        Dataset_name=f"{data_type}_dataset_{datetime.datetime.now():%Y%m%d_%H%M%S}",  # 使用当前时间戳生成独特的名称
+        Dataset_description=f"Automatically generated dataset for type {data_type}",
+        Row_count=0,  # 初始行数为0,将在复制数据后更新
+        Status='pending',  # 初始状态为pending
+        Dataset_type=data_type
+    )
+
+    # 添加到数据库并提交以获取ID
+    session.add(new_dataset)
+    session.flush()  # flush用于立即执行SQL并获取ID,但不提交事务
+
+    # 获取新数据集的ID
+    dataset_id = new_dataset.Dataset_ID
+
+    # 复制数据到新表
+    source_table = data_type_table_mapping(data_type)  # 假设有函数映射数据类型到表名
+    new_table_name = f"dataset_{dataset_id}"
+    copy_table_sql = f"CREATE TABLE {new_table_name} AS SELECT * FROM {source_table};"
+    session.execute(text(copy_table_sql))
+
+    # 更新新数据集的状态和行数
+    update_sql = f"UPDATE datasets SET status='processed', row_count=(SELECT count(*) FROM {new_table_name}) WHERE dataset_id={dataset_id};"
+    session.execute(text(update_sql))
+
+    session.commit()
+
+    return dataset_id
+
+def data_type_table_mapping(data_type):
+    """映射数据类型到对应的数据库表名"""
+    if data_type == 'reduce':
+        return 'current_reduce'
+    elif data_type == 'reflux':
+        return 'current_reflux'
+    else:
+        raise ValueError("Invalid data type provided.")
+
+
+def train_model_by_type(X, y, model_type):
+    # 划分数据集
+    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
+
+    if model_type == 'RandomForest':
+        # 随机森林的参数优化
+        return train_random_forest(X_train, y_train)
+    elif model_type == 'XGBR':
+        # XGBoost的参数优化
+        return train_xgboost(X_train, y_train)
+    elif model_type == 'GBSTR':
+        # 梯度提升树的参数优化
+        return train_gradient_boosting(X_train, y_train)
+    else:
+        raise ValueError(f"Unsupported model type: {model_type}")
+
+
+def train_random_forest(X_train, y_train):
+    best_score = 0
+    best_n_estimators = None
+    best_max_depth = None
+    random_state = 43
+
+    # 筛选最佳的树的数量
+    for n_estimators in range(1, 20, 1):
+        model = RandomForestRegressor(n_estimators=n_estimators, random_state=random_state)
+        score = cross_val_score(model, X_train, y_train, cv=5).mean()
+        if score > best_score:
+            best_score = score
+            best_n_estimators = n_estimators
+
+    print(f"Best number of trees: {best_n_estimators}, Score: {best_score}")
+
+    # 在找到的最佳树的数量基础上,筛选最佳的最大深度
+    best_score = 0  # 重置最佳得分,为最大深度优化做准备
+    for max_depth in range(1, 30, 1):
+        model = RandomForestRegressor(n_estimators=best_n_estimators, max_depth=max_depth, random_state=random_state)
+        score = cross_val_score(model, X_train, y_train, cv=5).mean()
+        if score > best_score:
+            best_score = score
+            best_max_depth = max_depth
+
+    print(f"Best max depth: {best_max_depth}, Score: {best_score}")
+
+    # 使用最佳的树的数量和最大深度训练最终模型
+    best_model = RandomForestRegressor(n_estimators=best_n_estimators, max_depth=best_max_depth,
+                                       random_state=random_state)
+    best_model.fit(X_train, y_train)
+
+    return best_model
+
+
+def train_xgboost(X_train, y_train, X_test, y_test):
+    # XGBoost训练过程
+    # (将类似上面的代码添加到这里)
+    pass
+
+
+def train_gradient_boosting(X_train, y_train, X_test, y_test):
+    # 梯度提升树训练过程
+    # (将类似上面的代码添加到这里)
+    pass
+
+def save_model(session, model, model_name, model_type, model_description, dataset_id, data_type, custom_path='pkl'):
+    """
+    保存模型到数据库,并将模型文件保存到磁盘。
+
+    :param session: 数据库会话
+    :param model: 要保存的模型对象
+    :param model_name: 模型的名称
+    :param model_type: 模型的类型
+    :param model_description: 模型的描述信息
+    :param dataset_id: 数据集ID
+    :param custom_path: 保存模型的路径
+    :return: 返回保存的模型文件路径
+    """
+    # 根据模型类型设置文件名前缀
+    prefix_dict = {
+        'RandomForest': 'rf_model_',
+        'XGBRegressor': 'xgbr_model_',
+        'GBSTRegressor': 'gbstr_model_'
+    }
+    prefix = prefix_dict.get(model_type, 'default_model_')  # 如果model_type不在字典中,默认前缀
+
+    try:
+        # 确保路径存在
+        os.makedirs(custom_path, exist_ok=True)
+
+        # 获取当前时间戳(格式:月日时分)
+        timestamp = datetime.datetime.now().strftime('%m%d_%H%M')
+
+        # 拼接完整的文件名
+        file_name = os.path.join(custom_path, f'{prefix}{timestamp}.pkl')
+
+        # 保存模型到文件
+        with open(file_name, 'wb') as f:
+            pickle.dump(model, f)
+        print(f"模型已保存为: {file_name}")
+
+        # 创建模型数据库记录
+        new_model = Models(
+            Model_name=model_name,
+            Model_type=model_type,
+            Description=model_description,
+            DatasetID=dataset_id,
+            Created_at=datetime.datetime.now(),
+            ModelFilePath=file_name,
+            Data_type=data_type
+        )
+
+        # 添加记录到数据库
+        session.add(new_model)
+        session.commit()
+
+        # 返回文件路径
+        return file_name
+
+    except Exception as e:
+        session.rollback()
+        print(f"Error saving model: {str(e)}")
+        raise e  # 显式抛出异常供调用者处理
+
+
+
+if __name__ == '__main__':
+    # 反酸模型预测
+    # 测试 predict 函数
+    input_data = pd.DataFrame([{
+        "organic_matter": 5.2,
+        "chloride": 3.1,
+        "cec": 25.6,
+        "h_concentration": 0.5,
+        "hn": 12.4,
+        "al_concentration": 0.8,
+        "free_alumina": 1.2,
+        "free_iron": 0.9,
+        "delta_ph": -0.2
+    }])
+
+    model_name = 'RF_filt'
+
+    Acid_reflux_result = predict(input_data, model_name)
+    print("Acid_reflux_result:", Acid_reflux_result)  # 预测结果
+
+    # 降酸模型预测
+    # 测试 predict 函数
+    input_data = pd.DataFrame([{
+        "pH": 5.2,
+        "OM": 3.1,
+        "CL": 25.6,
+        "H": 0.5,
+        "Al": 12.4
+    }])
+
+    model_name = 'rf_model_1214_1008'
+    Acid_reduce_result = predict(input_data, model_name)
+    print("Acid_reduce_result:", Acid_reduce_result)  # 预测结果

+ 610 - 0
api/app/routes.py

@@ -0,0 +1,610 @@
+import sqlite3
+
+from flask import Blueprint, request, jsonify, current_app
+from .model import predict, train_and_save_model
+import pandas as pd
+from . import db  # 从 app 包导入 db 实例
+from sqlalchemy.engine.reflection import Inspector
+from .database_models import Models, ModelParameters, Datasets, CurrentReduce, CurrentReflux
+import os
+from .utils import create_dynamic_table, allowed_file
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.schema import MetaData, Table
+import logging
+from sqlalchemy import text, select
+
+# 配置日志
+logging.basicConfig(level=logging.DEBUG)
+logger = logging.getLogger(__name__)
+# 创建蓝图 (Blueprint),用于分离路由
+bp = Blueprint('routes', __name__)
+
+def infer_column_types(df):
+    type_map = {
+        'object': 'str',
+        'int64': 'int',
+        'float64': 'float',
+        'datetime64[ns]': 'datetime'  # 适应Pandas datetime类型
+    }
+    # 提取列和其数据类型
+    return {col: type_map.get(str(df[col].dtype), 'str') for col in df.columns}
+
+
+@bp.route('/upload-dataset', methods=['POST'])
+def upload_dataset():
+    try:
+        if 'file' not in request.files:
+            return jsonify({'error': 'No file part'}), 400
+        file = request.files['file']
+        if file.filename == '' or not allowed_file(file.filename):
+            return jsonify({'error': 'No selected file or invalid file type'}), 400
+
+        dataset_name = request.form.get('dataset_name')
+        dataset_description = request.form.get('dataset_description', 'No description provided')
+        dataset_type = request.form.get('dataset_type')
+        if not dataset_type:
+            return jsonify({'error': 'Dataset type is required'}), 400
+
+        # 创建 sessionmaker 实例
+        Session = sessionmaker(bind=db.engine)
+        session = Session()
+        new_dataset = Datasets(
+            Dataset_name=dataset_name,
+            Dataset_description=dataset_description,
+            Row_count=0,
+            Status='pending',
+            Dataset_type=dataset_type
+        )
+        session.add(new_dataset)
+        session.commit()
+
+        unique_filename = f"dataset_{new_dataset.Dataset_ID}.xlsx"
+        upload_folder = current_app.config['UPLOAD_FOLDER']
+        file_path = os.path.join(upload_folder, unique_filename)
+        file.save(file_path)
+
+        dataset_df = pd.read_excel(file_path)
+        new_dataset.Row_count = len(dataset_df)
+        new_dataset.Status = 'processed'
+        session.commit()
+
+        # 清理列名
+        dataset_df = clean_column_names(dataset_df)
+        # 重命名 DataFrame 列以匹配模型字段
+        dataset_df = rename_columns_for_model(dataset_df, dataset_type)
+
+        column_types = infer_column_types(dataset_df)
+        dynamic_table_class = create_dynamic_table(new_dataset.Dataset_ID, column_types)
+        insert_data_into_dynamic_table(session, dataset_df, dynamic_table_class)
+
+        # 根据 dataset_type 决定插入到哪个已有表
+        if dataset_type == 'reduce':
+            insert_data_into_existing_table(session, dataset_df, CurrentReduce)
+        elif dataset_type == 'reflux':
+            insert_data_into_existing_table(session, dataset_df, CurrentReflux)
+
+        session.commit()
+
+        return jsonify({
+            'message': f'Dataset {dataset_name} uploaded successfully!',
+            'dataset_id': new_dataset.Dataset_ID,
+            'filename': unique_filename
+        }), 201
+
+    except Exception as e:
+        if session:
+            session.rollback()
+        logging.error('Failed to process the dataset upload:', exc_info=True)
+        return jsonify({'error': str(e)}), 500
+    finally:
+        session.close()
+
+@bp.route('/train-and-save-model', methods=['POST'])
+def train_and_save_model_endpoint():
+    # 创建 sessionmaker 实例
+    Session = sessionmaker(bind=db.engine)
+    session = Session()
+
+    # 从请求中解析参数
+    data = request.get_json()
+    model_type = data.get('model_type')
+    model_name = data.get('model_name')
+    model_description = data.get('model_description')
+    data_type = data.get('data_type')
+    dataset_id = data.get('dataset_id', None)  # 默认为 None,如果未提供
+
+    try:
+        # 调用训练和保存模型的函数
+        result = train_and_save_model(session, model_type, model_name, model_description, data_type, dataset_id)
+
+        # 返回成功响应
+        return jsonify({'message': 'Model trained and saved successfully', 'result': result}), 200
+
+    except Exception as e:
+        session.rollback()
+        logging.error('Failed to process the dataset upload:', exc_info=True)
+        return jsonify({'error': 'Failed to train and save model', 'message': str(e)}), 500
+    finally:
+        session.close()
+
+
+def clean_column_names(dataframe):
+    # Strip whitespace and replace non-breaking spaces and other non-printable characters
+    dataframe.columns = [col.strip().replace('\xa0', '') for col in dataframe.columns]
+    return dataframe
+
+
+def rename_columns_for_model(dataframe, dataset_type):
+    if dataset_type == 'reduce':
+        rename_map = {
+            '1/b': 'Q_over_b',
+            'pH': 'pH',
+            'OM': 'OM',
+            'CL': 'CL',
+            'H': 'H',
+            'Al': 'Al'
+        }
+    elif dataset_type == 'reflux':
+        rename_map = {
+            'OM g/kg': 'OM',
+            'CL g/kg': 'CL',
+            'CEC cmol/kg': 'CEC',
+            'H+ cmol/kg': 'H_plus',
+            'HN mg/kg': 'HN',
+            'Al3+cmol/kg': 'Al3_plus',
+            'Free alumina g/kg': 'Free_alumina',
+            'Free iron oxides g/kg': 'Free_iron_oxides',
+            'ΔpH': 'Delta_pH'
+        }
+
+    # 使用 rename() 方法更新列名
+    dataframe = dataframe.rename(columns=rename_map)
+    return dataframe
+
+
+def insert_data_into_existing_table(session, dataframe, model_class):
+    """Insert data from a DataFrame into an existing SQLAlchemy model table."""
+    for index, row in dataframe.iterrows():
+        record = model_class(**row.to_dict())
+        session.add(record)
+
+def insert_data_into_dynamic_table(session, dataset_df, dynamic_table_class):
+    for _, row in dataset_df.iterrows():
+        record_data = row.to_dict()
+        session.execute(dynamic_table_class.__table__.insert(), [record_data])
+
+def insert_data_by_type(session, dataset_df, dataset_type):
+    if dataset_type == 'reduce':
+        for _, row in dataset_df.iterrows():
+            record = CurrentReduce(**row.to_dict())
+            session.add(record)
+    elif dataset_type == 'reflux':
+        for _, row in dataset_df.iterrows():
+            record = CurrentReflux(**row.to_dict())
+            session.add(record)
+
+
+def get_current_data(session, data_type):
+    # 根据数据类型选择相应的表模型
+    if data_type == 'reduce':
+        model = CurrentReduce
+    elif data_type == 'reflux':
+        model = CurrentReflux
+    else:
+        raise ValueError("Invalid data type provided. Choose 'reduce' or 'reflux'.")
+
+    # 从数据库中查询所有记录
+    result = session.execute(select(model))
+
+    # 将结果转换为DataFrame
+    dataframe = pd.DataFrame([dict(row) for row in result])
+    return dataframe
+
+def get_dataset_by_id(session, dataset_id):
+    # 动态获取表的元数据
+    metadata = MetaData(bind=session.bind)
+    dataset_table = Table(dataset_id, metadata, autoload=True, autoload_with=session.bind)
+
+    # 从数据库中查询整个表的数据
+    query = select(dataset_table)
+    result = session.execute(query).fetchall()
+
+    # 检查是否有数据返回
+    if not result:
+        raise ValueError(f"No data found for dataset {dataset_id}.")
+
+    # 将结果转换为DataFrame
+    dataframe = pd.DataFrame(result, columns=[column.name for column in dataset_table.columns])
+
+    return dataframe
+
+
+@bp.route('/delete-dataset/<int:dataset_id>', methods=['DELETE'])
+def delete_dataset(dataset_id):
+    # 创建 sessionmaker 实例
+    Session = sessionmaker(bind=db.engine)
+    session = Session()
+    try:
+        # 查询数据集
+        dataset = session.query(Datasets).filter_by(Dataset_ID=dataset_id).first()
+        if not dataset:
+            return jsonify({'error': 'Dataset not found'}), 404
+
+        # 删除文件
+        filename = f"dataset_{dataset.Dataset_ID}.xlsx"
+        file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
+        if os.path.exists(file_path):
+            os.remove(file_path)
+
+        # 删除数据表
+        table_name = f"dataset_{dataset.Dataset_ID}"
+        session.execute(text(f"DROP TABLE IF EXISTS {table_name}"))
+
+        # 删除数据集记录
+        session.delete(dataset)
+        session.commit()
+
+        return jsonify({'message': 'Dataset deleted successfully'}), 200
+
+    except Exception as e:
+        session.rollback()
+        logging.error(f'Failed to delete dataset {dataset_id}:', exc_info=True)
+        return jsonify({'error': str(e)}), 500
+    finally:
+        session.close()
+
+
+@bp.route('/tables', methods=['GET'])
+def list_tables():
+    engine = db.engine  # 使用 db 实例的 engine
+    inspector = Inspector.from_engine(engine)  # 创建 Inspector 对象
+    table_names = inspector.get_table_names()  # 获取所有表名
+    return jsonify(table_names)  # 以 JSON 形式返回表名列表
+
+
+@bp.route('/models/<int:model_id>', methods=['GET'])
+def get_model(model_id):
+    try:
+        model = Models.query.filter_by(ModelID=model_id).first()
+        if model:
+            return jsonify({
+                'ModelID': model.ModelID,
+                'ModelName': model.ModelName,
+                'ModelType': model.ModelType,
+                'CreatedAt': model.CreatedAt.strftime('%Y-%m-%d %H:%M:%S'),
+                'Description': model.Description
+            })
+        else:
+            return jsonify({'message': 'Model not found'}), 404
+    except Exception as e:
+        return jsonify({'error': 'Internal server error', 'message': str(e)}), 500
+
+
+@bp.route('/models', methods=['GET'])
+def get_all_models():
+    try:
+        models = Models.query.all()  # 获取所有模型数据
+        if models:
+            result = [
+                {
+                    'ModelID': model.ModelID,
+                    'ModelName': model.ModelName,
+                    'ModelType': model.ModelType,
+                    'CreatedAt': model.CreatedAt.strftime('%Y-%m-%d %H:%M:%S'),
+                    'Description': model.Description
+                }
+                for model in models
+            ]
+            return jsonify(result)
+        else:
+            return jsonify({'message': 'No models found'}), 404
+    except Exception as e:
+        return jsonify({'error': 'Internal server error', 'message': str(e)}), 500
+
+
+@bp.route('/model-parameters', methods=['GET'])
+def get_all_model_parameters():
+    try:
+        parameters = ModelParameters.query.all()  # 获取所有参数数据
+        if parameters:
+            result = [
+                {
+                    'ParamID': param.ParamID,
+                    'ModelID': param.ModelID,
+                    'ParamName': param.ParamName,
+                    'ParamValue': param.ParamValue
+                }
+                for param in parameters
+            ]
+            return jsonify(result)
+        else:
+            return jsonify({'message': 'No parameters found'}), 404
+    except Exception as e:
+        return jsonify({'error': 'Internal server error', 'message': str(e)}), 500
+
+
+@bp.route('/models/<int:model_id>/parameters', methods=['GET'])
+def get_model_parameters(model_id):
+    try:
+        model = Models.query.filter_by(ModelID=model_id).first()
+        if model:
+            # 获取该模型的所有参数
+            parameters = [
+                {
+                    'ParamID': param.ParamID,
+                    'ParamName': param.ParamName,
+                    'ParamValue': param.ParamValue
+                }
+                for param in model.parameters
+            ]
+            
+            # 返回模型参数信息
+            return jsonify({
+                'ModelID': model.ModelID,
+                'ModelName': model.ModelName,
+                'ModelType': model.ModelType,
+                'CreatedAt': model.CreatedAt.strftime('%Y-%m-%d %H:%M:%S'),
+                'Description': model.Description,
+                'Parameters': parameters
+            })
+        else:
+            return jsonify({'message': 'Model not found'}), 404
+    except Exception as e:
+        return jsonify({'error': 'Internal server error', 'message': str(e)}), 500
+
+
+@bp.route('/predict', methods=['POST'])
+def predict_route():
+    try:
+        data = request.get_json()
+        model_name = data.get('model_name')  # 提取模型名称
+        parameters = data.get('parameters', {})  # 提取所有参数
+
+        input_data = pd.DataFrame([parameters])  # 转换参数为DataFrame
+        predictions = predict(input_data, model_name)  # 调用预测函数
+        return jsonify({'predictions': predictions}), 200
+    except Exception as e:
+        return jsonify({'error': str(e)}), 400
+
+
+# 定义添加数据库记录的 API 接口
+@bp.route('/add_item', methods=['POST'])
+def add_item():
+    """
+    接收 JSON 格式的请求体,包含表名和要插入的数据。
+    尝试将数据插入到指定的表中。
+    :return:
+    """
+    try:
+        # 确保请求体是JSON格式
+        data = request.get_json()
+        if not data:
+            raise ValueError("No JSON data provided")
+
+        table_name = data.get('table')
+        item_data = data.get('item')
+
+        if not table_name or not item_data:
+            return jsonify({'error': 'Missing table name or item data'}), 400
+        cur = db.cursor()
+
+        # 动态构建 SQL 语句
+        columns = ', '.join(item_data.keys())
+        placeholders = ', '.join(['?'] * len(item_data))
+        sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
+        cur.execute(sql, tuple(item_data.values()))
+        db.commit()
+
+        # 返回更详细的成功响应
+        return jsonify({'success': True, 'message': 'Item added successfully'}), 201
+
+    except ValueError as e:
+        return jsonify({'error': str(e)}), 400
+    except KeyError as e:
+        return jsonify({'error': f'Missing data field: {e}'}), 400
+    except sqlite3.IntegrityError as e:
+        # 处理例如唯一性约束违反等数据库完整性错误
+        return jsonify({'error': 'Database integrity error', 'details': str(e)}), 409
+    except sqlite3.Error as e:
+        # 处理其他数据库错误
+        return jsonify({'error': 'Database error', 'details': str(e)}), 500
+    finally:
+        db.close()
+
+
+# 定义删除数据库记录的 API 接口
+@bp.route('/delete_item', methods=['POST'])
+def delete_item():
+    data = request.get_json()
+    table_name = data.get('table')
+    condition = data.get('condition')
+
+    # 检查表名和条件是否提供
+    if not table_name or not condition:
+        return jsonify({
+            "success": False,
+            "message": "缺少表名或条件参数"
+        }), 400
+
+    # 尝试从条件字符串中分离键和值
+    try:
+        key, value = condition.split('=')
+    except ValueError:
+        return jsonify({
+            "success": False,
+            "message": "条件格式错误,应为 'key=value'"
+        }), 400
+
+    cur = db.cursor()
+
+    try:
+        # 执行删除操作
+        cur.execute(f"DELETE FROM {table_name} WHERE {key} = ?", (value,))
+        db.commit()
+        # 如果没有错误发生,返回成功响应
+        return jsonify({
+            "success": True,
+            "message": "记录删除成功"
+        }), 200
+    except sqlite3.Error as e:
+        # 发生错误,回滚事务
+        db.rollback()
+        # 返回失败响应,并包含错误信息
+        return jsonify({
+            "success": False,
+            "message": f"删除失败: {e}"
+        }), 400
+
+
+# 定义修改数据库记录的 API 接口
+@bp.route('/update_item', methods=['PUT'])
+def update_record():
+    data = request.get_json()
+
+    # 检查必要的数据是否提供
+    if not data or 'table' not in data or 'item' not in data:
+        return jsonify({
+            "success": False,
+            "message": "请求数据不完整"
+        }), 400
+
+    table_name = data['table']
+    item = data['item']
+
+    # 假设 item 的第一个元素是 ID
+    if not item or next(iter(item.keys())) is None:
+        return jsonify({
+            "success": False,
+            "message": "记录数据为空"
+        }), 400
+
+    # 获取 ID 和其他字段值
+    id_key = next(iter(item.keys()))
+    record_id = item[id_key]
+    updates = {key: value for key, value in item.items() if key != id_key}  # 排除 ID
+
+    cur = db.cursor()
+
+    try:
+        record_id = int(record_id)  # 确保 ID 是整数
+    except ValueError:
+        return jsonify({
+            "success": False,
+            "message": "ID 必须是整数"
+        }), 400
+
+    # 准备参数列表,包括更新的值和 ID
+    parameters = list(updates.values()) + [record_id]
+
+    # 执行更新操作
+    set_clause = ','.join([f"{k} = ?" for k in updates.keys()])
+    sql = f"UPDATE {table_name} SET {set_clause} WHERE {id_key} = ?"
+    try:
+        cur.execute(sql, parameters)
+        db.commit()
+        if cur.rowcount == 0:
+            return jsonify({
+                "success": False,
+                "message": "未找到要更新的记录"
+            }), 404
+        return jsonify({
+            "success": True,
+            "message": "数据更新成功"
+        }), 200
+    except sqlite3.Error as e:
+        db.rollback()
+        return jsonify({
+            "success": False,
+            "message": f"更新失败: {e}"
+        }), 400
+
+
+# 定义查询数据库记录的 API 接口
+@bp.route('/search/record', methods=['GET'])
+def sql_search():
+    """
+    接收 JSON 格式的请求体,包含表名和要查询的 ID。
+    尝试查询指定 ID 的记录并返回结果。
+    :return:
+    """
+    try:
+        data = request.get_json()
+
+        # 表名
+        sql_table = data['table']
+
+        # 要搜索的 ID
+        Id = data['id']
+
+        # 连接到数据库
+        cur = db.cursor()
+
+        # 构造查询语句
+        sql = f"SELECT * FROM {sql_table} WHERE id = ?"
+
+        # 执行查询
+        cur.execute(sql, (Id,))
+
+        # 获取查询结果
+        rows = cur.fetchall()
+        column_names = [desc[0] for desc in cur.description]
+
+        # 检查是否有结果
+        if not rows:
+            return jsonify({'error': '未查找到对应数据。'}), 400
+
+        # 构造响应数据
+        results = []
+        for row in rows:
+            result = {column_names[i]: row[i] for i in range(len(row))}
+            results.append(result)
+
+        # 关闭游标和数据库连接
+        cur.close()
+        db.close()
+
+        # 返回 JSON 响应
+        return jsonify(results), 200
+
+    except sqlite3.Error as e:
+        # 如果发生数据库错误,返回错误信息
+        return jsonify({'error': str(e)}), 400
+    except KeyError as e:
+        # 如果请求数据中缺少必要的键,返回错误信息
+        return jsonify({'error': f'缺少必要的数据字段: {e}'}), 400
+
+
+# 定义提供数据库列表,用于展示表格的 API 接口
+@bp.route('/table', methods=['POST'])
+def get_table():
+    data = request.get_json()
+    table_name = data.get('table')
+    if not table_name:
+        return jsonify({'error': '需要表名'}), 400
+
+    try:
+        # 创建 sessionmaker 实例
+        Session = sessionmaker(bind=db.engine)
+        session = Session()
+
+        # 动态获取表的元数据
+        metadata = MetaData()
+        table = Table(table_name, metadata, autoload_with=db.engine)
+
+        # 从数据库中查询所有记录
+        query = select(table)
+        result = session.execute(query).fetchall()
+
+        # 将结果转换为列表字典形式
+        rows = [dict(zip([column.name for column in table.columns], row)) for row in result]
+
+        # 获取列名
+        headers = [column.name for column in table.columns]
+
+        return jsonify(rows=rows, headers=headers), 200
+
+    except Exception as e:
+        return jsonify({'error': str(e)}), 400
+    finally:
+        # 关闭 session
+        session.close()

+ 57 - 0
api/app/utils.py

@@ -0,0 +1,57 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy import Column, Integer, String, Float, DateTime
+from sqlalchemy import create_engine
+import uuid
+from datetime import datetime, timezone
+
+Base = declarative_base()
+
+def create_dynamic_table(dataset_id, columns):
+    """动态创建数据表"""
+    # 动态构建列
+    dynamic_columns = {
+        'id': Column(Integer, primary_key=True, autoincrement=True)  # 为每个表添加一个主键
+    }
+
+    # 根据 columns 字典动态创建字段
+    for col_name, col_type in columns.items():
+        if col_type == 'str':
+            dynamic_columns[col_name] = Column(String(255))
+        elif col_type == 'int':
+            dynamic_columns[col_name] = Column(Integer)
+        elif col_type == 'float':
+            dynamic_columns[col_name] = Column(Float)
+        elif col_type == 'datetime':
+            dynamic_columns[col_name] = Column(DateTime)
+
+    # 动态生成模型类,表名使用 dataset_{dataset_id}
+    table_name = f"dataset_{dataset_id}"
+
+    # 在生成的类中添加 `__tablename__`
+    dynamic_columns['__tablename__'] = table_name
+
+    # 动态创建类
+    dynamic_class = type(table_name, (Base,), dynamic_columns)
+
+    # 打印调试信息
+    print("table_name:", table_name)
+    print("dynamic_columns:", dynamic_columns)
+
+    # 创建数据库引擎
+    engine = create_engine('sqlite:///SoilAcidification.db')  # 这里需要替换为你的数据库引擎
+    Base.metadata.create_all(engine)  # 创建所有表格
+
+    return dynamic_class
+
+# 判断文件类型是否允许
+def allowed_file(filename):
+    ALLOWED_EXTENSIONS = {'xlsx', 'xls'}
+    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
+
+# 生成唯一文件名
+def generate_unique_filename(filename):
+    # 获取文件的扩展名
+    ext = filename.rsplit('.', 1)[1].lower()
+    # 使用 UUID 和当前时间戳生成唯一文件名(使用 UTC 时区)
+    unique_filename = f"{uuid.uuid4().hex}_{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}.{ext}"
+    return unique_filename

+ 120 - 0
api/model_optimize/GBSTR_filt.py

@@ -0,0 +1,120 @@
+from sklearn.ensemble import GradientBoostingRegressor as GBSTR
+from sklearn.model_selection import cross_val_score,cross_val_predict
+from sklearn.model_selection import train_test_split
+from sklearn.preprocessing import StandardScaler
+from sklearn.metrics import mean_squared_error
+import matplotlib.pyplot as plt
+import numpy as np
+import pandas as pd
+
+# 导入数据
+data=pd.read_excel('model_optimize\data\data_filt.xlsx')
+x = data.iloc[:,1:10]
+y = data.iloc[:,-1]
+
+# 为 x 赋予列名
+x.columns = [
+    'organic_matter',        # OM g/kg
+    'chloride',              # CL g/kg
+    'cec',                   # CEC cmol/kg
+    'h_concentration',       # H+ cmol/kg
+    'hn',                    # HN mg/kg
+    'al_concentration',      # Al3+ cmol/kg
+    'free_alumina',          # Free alumina g/kg
+    'free_iron',             # Free iron oxides g/kg
+    'delta_ph'               # ΔpH
+]
+
+y.name = 'target_ph'
+
+Xtrain, Xtest, Ytrain, Ytest = train_test_split(x, y, test_size=0.2, random_state=42)
+
+score_5cv_all = []
+for i in np.arange(0.01, 0.5, 0.01):
+    GBST = GBSTR(learning_rate=i)
+    score_5cv = cross_val_score(GBST, Xtrain, Ytrain, cv=5).mean()
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+n_lr_5cv = np.arange(0.01,0.5,0.01)[score_5cv_all.index(score_max_5cv)]
+print(
+      "最大5cv得分:{}".format(score_max_5cv),
+      "lr_5cv:{}".format(n_lr_5cv))
+
+# 随机种子
+score_5cv_all = []
+for i in range(0, 200, 1):
+    GBST =GBSTR(learning_rate=n_lr_5cv
+              ,random_state=i)
+    score_5cv =cross_val_score(GBST, Xtrain, Ytrain, cv=5).mean()
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+random_state_5cv = range(0, 200)[score_5cv_all.index(max(score_5cv_all))]
+print(
+      "最大5cv得分:{}".format(score_max_5cv),
+      "random_state_5cv:{}".format(random_state_5cv))
+
+#随机树数目
+score_5cv_all = []
+for i in range(1, 400, 1):
+    GBST = GBSTR(n_estimators=i,
+               learning_rate=n_lr_5cv,
+               random_state=random_state_5cv)
+    score_5cv = cross_val_score(GBST, Xtrain, Ytrain, cv=5).mean()
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+n_est_5cv = range(1,400)[score_5cv_all.index(score_max_5cv)]
+print(
+      "最大5cv得分:{}".format(score_max_5cv),
+      "n_est_5cv:{}".format(n_est_5cv))
+
+
+score_5cv_all = []
+for i in range(1, 300, 1):
+    GBST = GBSTR(n_estimators=n_est_5cv,
+               learning_rate=n_lr_5cv, 
+               random_state=random_state_5cv, 
+               max_depth=i)
+    score_5cv = cross_val_score(GBST, Xtrain, Ytrain, cv=5).mean()
+    CV_predictions = cross_val_predict(GBST, Xtrain, Ytrain, cv=5)
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+max_depth_5cv = range(1,300)[score_5cv_all.index(score_max_5cv)]
+print(
+      "最大5cv得分:{}".format(score_max_5cv),
+      "max_depth_5cv:{}".format(max_depth_5cv))
+
+
+score_5cv_all = []
+for i in np.arange(0.01,1.0,0.05):
+    GBST = GBSTR(n_estimators=n_est_5cv,
+               learning_rate=n_lr_5cv, 
+               random_state=random_state_5cv,
+               max_depth=max_depth_5cv,
+               alpha=i)
+    score_5cv = cross_val_score(GBST, Xtrain, Ytrain, cv=5).mean()
+    CV_predictions = cross_val_predict(GBST, Xtrain, Ytrain, cv=5)
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+max_alpha_5cv =  np.arange(0.01,1.0,0.05)[score_5cv_all.index(score_max_5cv)]
+print(
+      "最大5cv得分:{}".format(score_max_5cv),
+      "alpha_5cv:{}".format(max_alpha_5cv))
+
+# 固定参数
+GBST = GBSTR(learning_rate=n_lr_5cv,n_estimators=n_est_5cv,random_state=random_state_5cv,max_depth=max_depth_5cv,alpha = max_alpha_5cv)
+CV_score = cross_val_score(GBST, Xtrain, Ytrain, cv=5).mean()
+CV_predictions = cross_val_predict(GBST, Xtrain, Ytrain, cv=5)
+rmse1 = np.sqrt(mean_squared_error(Ytrain,CV_predictions))
+regressor = GBST.fit(Xtrain, Ytrain)
+test_predictions = regressor.predict(Xtest)
+score_test = regressor.score(Xtest,Ytest)
+rmse2 = np.sqrt(mean_squared_error(Ytest,test_predictions))
+print("5cv:",CV_score)
+print("rmse_5CV",rmse1)
+print("test:",score_test)
+print("rmse_test",rmse2)

+ 50 - 0
api/model_optimize/KNN_filt.py

@@ -0,0 +1,50 @@
+# 导入常用基本包
+import os
+import pandas as pd
+import numpy as np
+from PIL import Image
+import matplotlib.pyplot as plt
+import matplotlib
+from mpl_toolkits.mplot3d import Axes3D 
+import matplotlib.cm as cm
+
+# 机器学习相关库
+from sklearn.ensemble import RandomForestRegressor
+from sklearn.model_selection import train_test_split, cross_val_score, cross_val_predict, GridSearchCV
+from sklearn.preprocessing import StandardScaler, MinMaxScaler
+from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error, accuracy_score, log_loss, roc_auc_score
+
+# 导入XGBoost
+from xgboost import XGBRegressor as XGBR
+
+# 其他工具
+import pickle
+from pathlib import Path
+
+# 导入数据
+data=pd.read_excel('model_optimize\data\data_filt.xlsx')
+x = data.iloc[:,1:10]
+y = data.iloc[:,-1]
+
+# 为 x 赋予列名
+x.columns = [
+    'organic_matter',        # OM g/kg
+    'chloride',              # CL g/kg
+    'cec',                   # CEC cmol/kg
+    'h_concentration',       # H+ cmol/kg
+    'hn',                    # HN mg/kg
+    'al_concentration',      # Al3+ cmol/kg
+    'free_alumina',          # Free alumina g/kg
+    'free_iron',             # Free iron oxides g/kg
+    'delta_ph'               # ΔpH
+]
+
+y.name = 'target_ph'
+
+Xtrain, Xtest, Ytrain, Ytest = train_test_split(x, y, test_size=0.2, random_state=42)
+
+from sklearn.neighbors import KNeighborsRegressor
+KNN_model = KNeighborsRegressor(n_neighbors=10)
+r2_score=cross_val_score(KNN_model,Xtrain,Ytrain,cv=5)
+print(r2_score)
+print(r2_score.mean())

+ 93 - 0
api/model_optimize/RF_default.py

@@ -0,0 +1,93 @@
+## 导入常用基本包
+import os
+import pandas as pd
+import numpy as np
+from PIL import Image
+from model_saver import save_model
+
+# 机器学习模型导入
+from sklearn.ensemble import RandomForestRegressor
+from sklearn.model_selection import cross_val_score,cross_val_predict
+from sklearn.model_selection import train_test_split
+from sklearn.metrics import mean_squared_error
+
+## 导入常用辅助函数
+from sklearn.model_selection import train_test_split
+from sklearn.model_selection import GridSearchCV
+from sklearn.model_selection import cross_val_score
+from sklearn.model_selection import cross_val_predict
+
+## 导入数据处理函数
+from sklearn.preprocessing import StandardScaler
+from sklearn.preprocessing import MinMaxScaler
+
+## 导入评分函数
+from sklearn.metrics import r2_score
+from sklearn.metrics import mean_squared_error 
+from sklearn.metrics import mean_absolute_error
+from sklearn.metrics import accuracy_score
+from sklearn.metrics import log_loss
+from sklearn.metrics import roc_auc_score
+
+import pickle
+import pandas as pd
+import numpy as np
+from sklearn.metrics import mean_squared_error
+from pathlib import Path
+
+# 导入数据
+data=pd.read_excel('model_optimize\data\data_filt.xlsx')
+x = data.iloc[:,1:10]
+y = data.iloc[:,-1]
+
+# 为 x 赋予列名
+x.columns = [
+    'organic_matter',        # OM g/kg
+    'chloride',              # CL g/kg
+    'cec',                   # CEC cmol/kg
+    'h_concentration',       # H+ cmol/kg
+    'hn',                    # HN mg/kg
+    'al_concentration',      # Al3+ cmol/kg
+    'free_alumina',          # Free alumina g/kg
+    'free_iron',             # Free iron oxides g/kg
+    'delta_ph'               # ΔpH
+]
+
+y.name = 'target_ph'
+
+Xtrain, Xtest, Ytrain, Ytest = train_test_split(x, y, test_size=0.2, random_state=42)
+
+## 原始模型
+model_path = Path('model_optimize\pkl\RF_filt.pkl')
+# 确保路径存在
+if model_path.exists():
+    with open(model_path, 'rb') as f:
+        rfc = pickle.load(f)
+
+CV_score = cross_val_score(rfc, Xtrain, Ytrain, cv=5).mean()
+CV_predictions = cross_val_predict(rfc, Xtrain, Ytrain, cv=5)
+rmse1 = np.sqrt(mean_squared_error(Ytrain,CV_predictions))
+regressor = rfc.fit(Xtrain, Ytrain)
+test_predictions = regressor.predict(Xtest)
+score_test = regressor.score(Xtest,Ytest)
+rmse2 = np.sqrt(mean_squared_error(Ytest,test_predictions))
+print("5cv_score:",CV_score)
+print("rmse_5CV",rmse1)
+print("R2_test:",score_test)
+print("rmse_test",rmse2)
+
+# 指定参数
+# 确定参数进行训练
+rfc = RandomForestRegressor(random_state=1)
+CV_score = cross_val_score(rfc, Xtrain, Ytrain, cv=5).mean()
+CV_predictions = cross_val_predict(rfc, Xtrain, Ytrain, cv=5)
+rmse1 = np.sqrt(mean_squared_error(Ytrain,CV_predictions))
+regressor = rfc.fit(Xtrain, Ytrain)
+test_predictions = regressor.predict(Xtest)
+score_test = regressor.score(Xtest,Ytest)
+rmse2 = np.sqrt(mean_squared_error(Ytest,test_predictions))
+print("5cv:",CV_score)
+print("rmse_5CV",rmse1)
+print("R2_test:",score_test)
+print("rmse_test",rmse2)
+

+ 141 - 0
api/model_optimize/RF_filt.py

@@ -0,0 +1,141 @@
+# '''
+# 模型筛选
+# '''
+
+## 导入常用基本包
+import os
+import pandas as pd
+import numpy as np
+from PIL import Image
+from model_saver import save_model
+
+# 机器学习模型导入
+from sklearn.ensemble import RandomForestRegressor
+from sklearn.model_selection import cross_val_score,cross_val_predict
+from sklearn.model_selection import train_test_split
+from sklearn.metrics import mean_squared_error
+
+## 导入常用辅助函数
+from sklearn.model_selection import train_test_split
+from sklearn.model_selection import GridSearchCV
+from sklearn.model_selection import cross_val_score
+from sklearn.model_selection import cross_val_predict
+
+## 导入数据处理函数
+from sklearn.preprocessing import StandardScaler
+from sklearn.preprocessing import MinMaxScaler
+
+## 导入评分函数
+from sklearn.metrics import r2_score
+from sklearn.metrics import mean_squared_error 
+from sklearn.metrics import mean_absolute_error
+from sklearn.metrics import accuracy_score
+from sklearn.metrics import log_loss
+from sklearn.metrics import roc_auc_score
+
+
+
+# ## 土壤反酸筛选数据
+# data=pd.read_excel('model_optimize\data\data_filt.xlsx')   
+
+# x = data.iloc[:,1:10]
+# y = data.iloc[:,-1]
+
+# # 为 x 赋予列名
+# x.columns = [
+#     'organic_matter',        # OM g/kg
+#     'chloride',              # CL g/kg
+#     'cec',                   # CEC cmol/kg
+#     'h_concentration',       # H+ cmol/kg
+#     'hn',                    # HN mg/kg
+#     'al_concentration',      # Al3+ cmol/kg
+#     'free_alumina',          # Free alumina g/kg
+#     'free_iron',             # Free iron oxides g/kg
+#     'delta_ph'               # ΔpH
+# ]
+
+# y.name = 'target_ph'
+
+## 精准降酸数据
+data=pd.read_excel('model_optimize\data\Acidity_reduce.xlsx')
+
+x = data.iloc[:,1:]
+y = data.iloc[:,0]
+# 为 x 赋予列名
+x.columns = [
+    'pH',        
+    'OM',              
+    'CL', 
+    'H',
+    'Al'
+]
+y.name = 'target'
+
+
+Xtrain, Xtest, Ytrain, Ytest = train_test_split(x, y, test_size=0.2, random_state=42)
+
+# 筛选随机种子
+score_5cv_all = []
+for i in range(0, 200, 1):
+    rfc =RandomForestRegressor(random_state=i)
+    score_5cv =cross_val_score(rfc, Xtrain, Ytrain, cv=5).mean()
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+
+random_state_5cv = range(0, 200)[score_5cv_all.index(max(score_5cv_all))] # 5cv最大得分对应的随机种子
+
+print("最大5cv得分:{}".format(score_max_5cv),
+      "random_5cv:{}".format(random_state_5cv))
+
+
+# 筛选随机树数目
+score_5cv_all = []
+for i in range(1, 400, 1):
+    rfc = RandomForestRegressor(n_estimators=i,
+        random_state=random_state_5cv)
+    score_5cv = cross_val_score(rfc, Xtrain, Ytrain, cv=5).mean()
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+n_est_5cv = range(1,400)[score_5cv_all.index(score_max_5cv)]    # 5cv最大得分对应的树数目
+
+print("最大5cv得分:{}".format(score_max_5cv),
+      "n_est_5cv:{}".format(n_est_5cv))  # 5cv最大得分对应的树数目??
+score_test_all = []
+
+
+# 筛选最大深度
+score_5cv_all = []
+for i in range(1, 300, 1):
+    rfc = RandomForestRegressor(n_estimators=n_est_5cv
+                                , random_state=random_state_5cv
+                                , max_depth=i)
+    score_5cv = cross_val_score(rfc, Xtrain, Ytrain, cv=5).mean()
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+max_depth_5cv = range(1,300)[score_5cv_all.index(score_max_5cv)]    
+print(
+      "最大5cv得分:{}".format(score_max_5cv),
+      "max_depth_5cv:{}".format(max_depth_5cv))      
+
+# 确定参数进行训练
+rfc = RandomForestRegressor(n_estimators=n_est_5cv,random_state=random_state_5cv,max_depth=max_depth_5cv)
+CV_score = cross_val_score(rfc, Xtrain, Ytrain, cv=5).mean()
+CV_predictions = cross_val_predict(rfc, Xtrain, Ytrain, cv=5)
+rmse1 = np.sqrt(mean_squared_error(Ytrain,CV_predictions))
+regressor = rfc.fit(Xtrain, Ytrain)
+test_predictions = regressor.predict(Xtest)
+score_test = regressor.score(Xtest,Ytest)
+rmse2 = np.sqrt(mean_squared_error(Ytest,test_predictions))
+print("5cv:",CV_score)
+print("rmse_5CV",rmse1)
+print("test:",score_test)
+print("rmse_test",rmse2)
+
+# 保存训练好的模型
+custom_path='model_optimize\pkl'            # 模型保存路径
+prefix='rf_model_'                          # 模型文件名前缀
+save_model(rfc, custom_path, prefix)
+

+ 148 - 0
api/model_optimize/XGBR_filt.py

@@ -0,0 +1,148 @@
+# 导入常用基本包
+import os
+import pandas as pd
+import numpy as np
+from PIL import Image
+import matplotlib.pyplot as plt
+import matplotlib
+from mpl_toolkits.mplot3d import Axes3D 
+import matplotlib.cm as cm
+
+# 机器学习相关库
+from sklearn.ensemble import RandomForestRegressor
+from sklearn.model_selection import train_test_split, cross_val_score, cross_val_predict, GridSearchCV
+from sklearn.preprocessing import StandardScaler, MinMaxScaler
+from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error, accuracy_score, log_loss, roc_auc_score
+
+# 导入XGBoost
+from xgboost import XGBRegressor as XGBR
+
+# 其他工具
+import pickle
+from pathlib import Path
+
+# 导入数据
+data=pd.read_excel('model_optimize\data\data_filt.xlsx')
+x = data.iloc[:,1:10]
+y = data.iloc[:,-1]
+
+# 为 x 赋予列名
+x.columns = [
+    'organic_matter',        # OM g/kg
+    'chloride',              # CL g/kg
+    'cec',                   # CEC cmol/kg
+    'h_concentration',       # H+ cmol/kg
+    'hn',                    # HN mg/kg
+    'al_concentration',      # Al3+ cmol/kg
+    'free_alumina',          # Free alumina g/kg
+    'free_iron',             # Free iron oxides g/kg
+    'delta_ph'               # ΔpH
+]
+
+y.name = 'target_ph'
+
+Xtrain, Xtest, Ytrain, Ytest = train_test_split(x, y, test_size=0.2, random_state=42)
+
+# 
+score_5cv_all = []
+for i in np.arange(0.01, 0.5, 0.01):
+    XGB = XGBR(learning_rate=i)
+    score_5cv = cross_val_score(XGB, Xtrain, Ytrain, cv=5).mean()
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+n_lr_5cv = np.arange(0.01,0.5,0.01)[score_5cv_all.index(score_max_5cv)]
+print(
+      "最大5cv得分:{}".format(score_max_5cv),
+      "n_lr_5cv:{}".format(n_lr_5cv))
+
+# 随机种子
+score_5cv_all = []
+for i in range(0, 200, 1):
+    XGB =XGBR(learning_rate=n_lr_5cv
+              ,random_state=i)
+    score_5cv =cross_val_score(XGB, Xtrain, Ytrain, cv=5).mean()
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+random_state_5cv = range(0, 200)[score_5cv_all.index(max(score_5cv_all))]
+print(
+      "最大5cv得分:{}".format(score_max_5cv),
+      "random_5cv:{}".format(random_state_5cv))
+
+#随机树数目
+score_5cv_all = []
+for i in range(1, 400, 1):
+    XGB = XGBR(n_estimators=i,
+               learning_rate=n_lr_5cv,
+               random_state=random_state_5cv)
+    score_5cv = cross_val_score(XGB, Xtrain, Ytrain, cv=5).mean()
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+n_est_5cv = range(1,400)[score_5cv_all.index(score_max_5cv)]
+
+print(
+      "最大5cv得分:{}".format(score_max_5cv),
+      "n_est_5cv:{}".format(n_est_5cv))
+
+score_5cv_all = []
+for i in range(1, 300, 1):
+    XGB = XGBR(n_estimators=n_est_5cv,
+               learning_rate=n_lr_5cv, 
+               random_state=random_state_5cv, 
+               max_depth=i)
+    score_5cv = cross_val_score(XGB, Xtrain, Ytrain, cv=5).mean()
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+max_depth_5cv = range(1,300)[score_5cv_all.index(score_max_5cv)]
+print(
+      "最大5cv得分:{}".format(score_max_5cv),
+      "max_depth_5cv:{}".format(max_depth_5cv))
+
+score_5cv_all = []
+for i in np.arange(0,5,0.05):
+    XGB = XGBR(n_estimators=n_est_5cv,
+               learning_rate=n_lr_5cv,
+               random_state=random_state_5cv,
+               max_depth=max_depth_5cv,
+               gamma= i)
+    score_5cv = cross_val_score(XGB, Xtrain, Ytrain, cv=5).mean()
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+max_gamma_5cv =  np.arange(0,5,0.05)[score_5cv_all.index(score_max_5cv)]
+print(
+      "最大5cv得分:{}".format(score_max_5cv),
+      "gamma_5cv:{}".format(max_gamma_5cv))
+
+score_5cv_all = []
+for i in np.arange(0,5,0.05):
+    XGB = XGBR(n_estimators=n_est_5cv,
+               learning_rate=n_lr_5cv, 
+               random_state=random_state_5cv,
+               max_depth=max_depth_5cv,
+               gamma=max_gamma_5cv,
+               alpha=i)
+    score_5cv = cross_val_score(XGB, Xtrain, Ytrain, cv=5).mean()
+    score_5cv_all.append(score_5cv)
+    pass
+score_max_5cv = max(score_5cv_all)
+max_alpha_5cv =  np.arange(0,5,0.05)[score_5cv_all.index(score_max_5cv)]
+print(
+      "最大5cv得分:{}".format(score_max_5cv),
+      "alpha_5cv:{}".format(max_alpha_5cv))
+
+XGB = XGBR(learning_rate=n_lr_5cv,n_estimators=n_est_5cv,random_state=random_state_5cv,max_depth=max_depth_5cv,gamma =max_gamma_5cv,alpha = max_alpha_5cv)
+CV_score = cross_val_score(XGB, Xtrain, Ytrain, cv=5).mean()
+CV_predictions = cross_val_predict(XGB, Xtrain, Ytrain, cv=5)
+rmse1 = np.sqrt(mean_squared_error(Ytrain,CV_predictions))
+regressor = XGB.fit(Xtrain, Ytrain)
+test_predictions = regressor.predict(Xtest)
+score_test = regressor.score(Xtest,Ytest)
+rmse2 = np.sqrt(mean_squared_error(Ytest,test_predictions))
+print("5cv:",CV_score)
+print("rmse_5CV",rmse1)
+print("test:",score_test)
+print("rmse_test",rmse2)

BIN
api/model_optimize/data/Acidity_reduce.xlsx


BIN
api/model_optimize/data/data_filt.xlsx


+ 177 - 0
api/model_optimize/data_increase.py

@@ -0,0 +1,177 @@
+# 导入常用包
+import os
+import pandas as pd
+import numpy as np
+from PIL import Image
+from model_saver import save_model
+
+# 机器学习模型导入
+from sklearn.ensemble import RandomForestRegressor
+from sklearn.model_selection import train_test_split
+from sklearn.metrics import mean_squared_error
+from sklearn.ensemble import GradientBoostingRegressor as GBSTR
+from sklearn.neighbors import KNeighborsRegressor
+from xgboost import XGBRegressor as XGBR
+
+# 导入数据处理函数
+from sklearn.preprocessing import StandardScaler
+from sklearn.preprocessing import MinMaxScaler
+
+# 导入评分函数
+from sklearn.metrics import r2_score
+from sklearn.metrics import mean_squared_error 
+from sklearn.metrics import mean_absolute_error
+from sklearn.metrics import accuracy_score
+from sklearn.metrics import log_loss
+from sklearn.metrics import roc_auc_score
+
+import pickle
+import pandas as pd
+import numpy as np
+from sklearn.metrics import mean_squared_error
+from pathlib import Path
+
+## 土壤反酸筛选数据
+data=pd.read_excel('model_optimize\data\data_filt.xlsx')   
+
+x = data.iloc[:,1:10]
+print(x)
+# x = data.iloc[:,1:9]
+y = data.iloc[:,-1]
+
+y1 = data.iloc[:,-2]
+y2 = data.iloc[:,-1]
+# 绝对值
+y = abs(y1 - y2)
+
+print(y)
+
+# 为 x 赋予列名
+x.columns = [
+    'organic_matter',        # OM g/kg
+    'chloride',              # CL g/kg
+    'cec',                   # CEC cmol/kg
+    'h_concentration',       # H+ cmol/kg
+    'hn',                    # HN mg/kg
+    'al_concentration',      # Al3+ cmol/kg
+    'free_alumina',          # Free alumina g/kg
+    'free_iron',             # Free iron oxides g/kg
+    'delta_ph'               # ΔpH
+]
+
+y.name = 'target_ph'
+
+# ## 精准降酸数据
+# data=pd.read_excel('model_optimize\data\Acidity_reduce.xlsx')
+
+# x = data.iloc[:,1:]
+# y = data.iloc[:,0]
+# # 为 x 赋予列名
+# x.columns = [
+#     'pH',        
+#     'OM',              
+#     'CL', 
+#     'H',
+#     'Al'
+# ]
+# y.name = 'target'
+
+
+## 数据集划分
+Xtrain, Xtest, Ytrain, Ytest = train_test_split(x, y, test_size=0.2, random_state=42)
+
+## 模型
+# 随机森林回归模型
+rfc = RandomForestRegressor(random_state=1)
+# XGBR
+XGB = XGBR(random_state=1)
+# GBSTR
+GBST = GBSTR(random_state=1)
+# KNN
+KNN = KNeighborsRegressor(n_neighbors=4)
+
+# 增量训练:每次增加10%的训练数据
+increment = 0.1  # 每次增加的比例
+train_sizes = np.arange(0.1, 1.1, increment)  # 从0.1到1.0的训练集大小
+r2_scores_rfc = []  
+r2_scores_xgb = []  
+r2_scores_gbst = []  
+r2_scores_knn = []  # 用来记录KNN的r2_score
+
+
+# 对于每种训练集大小,训练模型并记录r2_score
+for size in train_sizes:
+    # 计算当前训练集的大小
+    current_size = int(size * len(Xtrain))
+    
+    # RandomForestRegressor
+    rfc.fit(Xtrain[:current_size], Ytrain[:current_size])
+    # XGBRegressor
+    XGB.fit(Xtrain[:current_size], Ytrain[:current_size])
+    # GBST
+    GBST.fit(Xtrain[:current_size], Ytrain[:current_size])
+    # KNN
+    KNN.fit(Xtrain[:current_size], Ytrain[:current_size])
+
+    # r2_score
+    y_pred_rfc = rfc.predict(Xtest)
+    score_rfc = r2_score(Ytest, y_pred_rfc)
+    r2_scores_rfc.append(score_rfc)
+    
+    # XGBRegressor的r2_score
+    y_pred_xgb = XGB.predict(Xtest)
+    score_xgb = r2_score(Ytest, y_pred_xgb)
+    r2_scores_xgb.append(score_xgb)
+
+    # GBST的r2_score
+    y_pred_gbst = GBST.predict(Xtest)
+    score_gbst = r2_score(Ytest, y_pred_gbst)
+    r2_scores_gbst.append(score_gbst)
+
+    # KNN的r2_score
+    y_pred_knn = KNN.predict(Xtest)
+    score_knn = r2_score(Ytest, y_pred_knn)
+    r2_scores_knn.append(score_knn)
+
+    # 输出当前的训练进度与r2评分
+    print(f"Training with {size * 100:.2f}% of the data:")
+    print(f"  - Random Forest R2 score: {score_rfc}")
+    print(f"  - XGB R2 score: {score_xgb}")
+    print(f"  - GBST R2 score: {score_gbst}")
+    print(f"  - KNN R2 score: {score_knn}")
+
+# 绘制R2评分随训练数据大小变化的图形
+import matplotlib.pyplot as plt
+
+plt.plot(train_sizes * 100, r2_scores_rfc, marker='o', label='Random Forest')
+plt.plot(train_sizes * 100, r2_scores_xgb, marker='x', label='XGBoost')
+plt.plot(train_sizes * 100, r2_scores_gbst, marker='s', label='Gradient Boosting')
+# plt.plot(train_sizes * 100, r2_scores_knn, marker='^', label='KNN')
+
+
+plt.xlabel('Training data size (%)')
+plt.ylabel('R2 Score')
+plt.title('Model Performance with Incremental Data')
+plt.legend()
+plt.grid(True)
+plt.show()
+
+
+# 选择后半部分数据
+# import matplotlib.pyplot as plt
+
+# # 选择后半部分数据
+# half_index = len(train_sizes) // 2
+
+# # 绘制后半部分的数据
+# plt.plot(train_sizes[half_index:] * 100, r2_scores_rfc[half_index:], marker='o', label='Random Forest')
+# plt.plot(train_sizes[half_index:] * 100, r2_scores_xgb[half_index:], marker='x', label='XGBoost')
+# plt.plot(train_sizes[half_index:] * 100, r2_scores_gbst[half_index:], marker='s', label='Gradient Boosting')
+# plt.plot(train_sizes[half_index:] * 100, r2_scores_knn[half_index:], marker='^', label='KNN')
+
+# plt.xlabel('Training data size (%)')
+# plt.ylabel('R2 Score')
+# plt.title('Model Performance with Incremental Data (Second Half)')
+# plt.legend()
+# plt.grid(True)
+# plt.show()

+ 123 - 0
api/model_optimize/data_increase_5cv_score.py

@@ -0,0 +1,123 @@
+# 导入常用包
+import os
+import pandas as pd
+import numpy as np
+from PIL import Image
+from model_saver import save_model
+
+# 机器学习模型导入
+from sklearn.ensemble import RandomForestRegressor
+from sklearn.model_selection import train_test_split
+from sklearn.metrics import mean_squared_error
+from sklearn.ensemble import GradientBoostingRegressor as GBSTR
+from sklearn.neighbors import KNeighborsRegressor
+from xgboost import XGBRegressor as XGBR
+
+
+
+# 导入数据处理函数
+from sklearn.preprocessing import StandardScaler
+from sklearn.preprocessing import MinMaxScaler
+
+# 导入评分函数
+from sklearn.metrics import r2_score
+from sklearn.metrics import mean_squared_error 
+from sklearn.metrics import mean_absolute_error
+from sklearn.metrics import accuracy_score
+from sklearn.metrics import log_loss
+from sklearn.metrics import roc_auc_score
+from sklearn.model_selection import cross_val_score
+
+import pickle
+import pandas as pd
+import numpy as np
+from sklearn.metrics import mean_squared_error
+from pathlib import Path
+
+
+
+# 导入数据
+data = pd.read_excel('model_optimize/data/data_filt.xlsx')
+x = data.iloc[:, 1:10]
+y = data.iloc[:, -1]
+
+# 为 x 赋予列名
+x.columns = [
+    'organic_matter',        # OM g/kg
+    'chloride',              # CL g/kg
+    'cec',                   # CEC cmol/kg
+    'h_concentration',       # H+ cmol/kg
+    'hn',                    # HN mg/kg
+    'al_concentration',      # Al3+ cmol/kg
+    'free_alumina',          # Free alumina g/kg
+    'free_iron',             # Free iron oxides g/kg
+    'delta_ph'               # ΔpH
+]
+
+y.name = 'target_ph'
+
+Xtrain, Xtest, Ytrain, Ytest = train_test_split(x, y, test_size=0.2, random_state=42)
+
+## 模型
+# 随机森林回归模型
+rfc = RandomForestRegressor(random_state=1)
+# XGBR
+XGB = XGBR(random_state=1)
+# GBSTR
+GBST = GBSTR(random_state=1)
+# KNN
+KNN = KNeighborsRegressor(n_neighbors=5)
+
+# 增量训练:每次增加10%的训练数据
+increment = 0.1  # 每次增加的比例
+train_sizes = np.arange(0.1, 1.1, increment)  # 从0.1到1.0的训练集大小
+# 记录各模型交叉验证评分
+cv5_scores_rfc = []  
+cv5_scores_xgb = []  
+cv5_scores_gbst = []  
+cv5_scores_knn = []  
+
+
+# 对于每种训练集大小,训练模型并记录r2_score
+for size in train_sizes:
+    # 计算当前训练集的大小
+    current_size = int(size * len(Xtrain))
+    
+    # 交叉验证评分
+    score_rfc = cross_val_score(rfc, Xtrain, Ytrain, cv=5).mean()
+    cv5_scores_rfc.append(score_rfc)
+
+    # XGBRegressor的交叉验证评分
+    score_xgb = cross_val_score(XGB, Xtrain, Ytrain, cv=5).mean()
+    cv5_scores_xgb.append(score_xgb)
+
+    # GBST的交叉验证评分
+    score_gbst = cross_val_score(GBST, Xtrain, Ytrain, cv=5).mean()
+    cv5_scores_gbst.append(score_gbst)
+
+    # KNN的交叉验证评分
+    score_knn = cross_val_score(KNN, Xtrain, Ytrain, cv=5).mean()
+    cv5_scores_knn.append(score_knn)
+
+    # 输出当前的训练进度与评分
+    print(f"Training with {size * 100:.2f}% of the data:")
+    print(f"  - Random Forest CV5 score: {score_rfc}")
+    print(f"  - XGB CV5 score: {score_xgb}")
+    print(f"  - GBST CV5 score: {score_gbst}")
+    print(f"  - KNN CV5 score: {score_knn}")
+
+# 绘制R2评分随训练数据大小变化的图形
+import matplotlib.pyplot as plt
+
+plt.plot(train_sizes * 100, cv5_scores_rfc, marker='o', label='Random Forest')
+plt.plot(train_sizes * 100, cv5_scores_xgb, marker='x', label='XGBoost')
+plt.plot(train_sizes * 100, cv5_scores_gbst, marker='s', label='Gradient Boosting')
+plt.plot(train_sizes * 100, cv5_scores_knn, marker='^', label='KNN')
+
+
+plt.xlabel('Training data size (%)')
+plt.ylabel('CV5 Score')
+plt.title('Model Performance with Incremental Data')
+plt.legend()
+plt.grid(True)
+plt.show()

+ 70 - 0
api/model_optimize/model_predict.py

@@ -0,0 +1,70 @@
+import pickle
+import pandas as pd
+import numpy as np
+from sklearn.metrics import mean_squared_error
+from pathlib import Path
+
+model_path = Path('model_optimize\pkl\RF_filt.pkl')
+# 确保路径存在
+if model_path.exists():
+    with open(model_path, 'rb') as f:
+        rfc = pickle.load(f)
+
+
+# 读取数据
+data_path = Path('model_optimize\data\data_filt.xlsx')
+data=pd.read_excel(data_path)
+
+
+x = data.iloc[:,1:10]
+y = data.iloc[:,-1]
+
+# 转换列名
+x.columns = [
+    'organic_matter',        # OM g/kg
+    'chloride',              # CL g/kg
+    'cec',                   # CEC cmol/kg
+    'h_concentration',       # H+ cmol/kg
+    'hn',                    # HN mg/kg
+    'al_concentration',      # Al3+ cmol/kg
+    'free_alumina',          # Free alumina g/kg
+    'free_iron',             # Free iron oxides g/kg
+    'delta_ph'               # ΔpH
+]
+
+
+# 预测
+y_pred = rfc.predict(x)
+
+# y 与 y_pred 的对比
+print('y:',y)
+print('y_pred:',y_pred)
+# 计算预测误差
+errors = y - y_pred
+
+# 图示
+import matplotlib.pyplot as plt
+
+# 绘制散点图
+plt.figure(figsize=(10, 6))
+plt.scatter(y, y_pred, color='blue', label='Predictions', alpha=0.5)
+plt.plot([y.min(), y.max()], [y.min(), y.max()], color='red', lw=2, label='Perfect fit')  # 理想的完美拟合线
+plt.xlabel('True Values')
+plt.ylabel('Predicted Values')
+plt.title('True vs Predicted Values')
+plt.legend()
+plt.show()
+
+# 绘制误差的直方图
+plt.figure(figsize=(10, 6))
+plt.hist(errors, bins=20, edgecolor='black', color='lightblue')
+plt.axvline(x=0, color='red', linestyle='--', lw=2, label='Zero Error Line')  # 添加零误差线
+plt.xlabel('Prediction Error')
+plt.ylabel('Frequency')
+plt.title('Distribution of Prediction Errors')
+plt.legend()
+plt.show()
+
+# 评分
+rmse = np.sqrt(mean_squared_error(y,y_pred))
+print("rmse",rmse)

+ 26 - 0
api/model_optimize/model_saver.py

@@ -0,0 +1,26 @@
+import pickle
+import datetime
+import os
+
+def save_model(model, custom_path='D:/suan/Code_suan/', prefix='my_model_'):
+    """
+    将模型保存为一个文件,文件名包括时间戳,防止覆盖。
+    
+    :param model: 训练好的模型(例如 RandomForestRegressor)
+    :param custom_path: 保存模型的路径,默认是 'D:/suan/Code_suan/'
+    :param prefix: 文件名前缀,默认是 'my_model_'
+    """
+    # 确保路径存在
+    os.makedirs(custom_path, exist_ok=True)
+    
+    # 获取当前时间戳(格式:月日时分)
+    timestamp = datetime.datetime.now().strftime('%m%d_%H%M')
+
+    # 拼接完整的文件名
+    file_name = os.path.join(custom_path, f'{prefix}{timestamp}.pkl')
+
+    # 保存模型
+    with open(file_name, 'wb') as f:
+        pickle.dump(model, f)
+
+    print(f"模型已保存为: {file_name}")

BIN
api/model_optimize/pkl/RF_filt.pkl


BIN
api/model_optimize/pkl/rf_model_1207_1530.pkl


BIN
api/model_optimize/pkl/rf_model_1214_1008.pkl


BIN
api/pkl/rf_model_1222_1922.pkl


BIN
api/pkl/rf_model_1222_1952.pkl


BIN
api/pkl/rf_model_1222_1954.pkl


BIN
api/pkl/rf_model_1222_1959.pkl


BIN
api/pkl/rf_model_1222_2012.pkl


BIN
api/pkl/rf_model_1225_0129.pkl


BIN
api/pkl/rf_model_1225_0141.pkl


BIN
api/pkl/rf_model_1225_0152.pkl


BIN
api/pkl/rf_model_1229_1916.pkl


BIN
api/pkl/rf_model_1229_2145.pkl


BIN
api/pkl/rf_model_1229_2155.pkl


+ 8 - 0
api/run.py

@@ -0,0 +1,8 @@
+from app import create_app
+import os
+# 创建 Flask 应用
+app = create_app()
+
+# 启动服务器
+if __name__ == '__main__':
+    app.run(debug=True)

BIN
api/uploads/6bSQknDrwrRlde1f2539a3bc98595af5e8b8588c05a4.xlsx


BIN
api/uploads/9mYcsUjQQMaZ9b01e46167ade216795568db3c2d37e8.xlsx


BIN
api/uploads/A66orP06DVdAdd1616dcb86e932a94e2a99d69745d25.xlsx


BIN
api/uploads/CK9XvADp0egG1dd962093a2be4c35841ca8b245b1e3b.xlsx


BIN
api/uploads/FH8sVIA4ULaG1dd962093a2be4c35841ca8b245b1e3b.xlsx


BIN
api/uploads/JVlQLhd4fkWr1dd962093a2be4c35841ca8b245b1e3b.xlsx


BIN
api/uploads/NLPOdpDyac0b92585f8b5d6cc17655671f9cc9ceb93a.xlsx


BIN
api/uploads/VggWySJFG0li1dd962093a2be4c35841ca8b245b1e3b.xlsx


BIN
api/uploads/WEvCgKQR8ptBd46529682c0a74b35dd76bec86e9f05e.xlsx


BIN
api/uploads/WeQ6qtM436ise129f6334fd40c9f68ed74586a5cd77c.xlsx


BIN
api/uploads/ZUmVAEVHQGEK92585f8b5d6cc17655671f9cc9ceb93a.xlsx


BIN
api/uploads/aENap7i3PtIS92585f8b5d6cc17655671f9cc9ceb93a.xlsx


BIN
api/uploads/atLyS0lcgvUVdd1616dcb86e932a94e2a99d69745d25.xlsx


BIN
api/uploads/datasets/dataset_2.xlsx


BIN
api/uploads/datasets/dataset_29.xlsx


BIN
api/uploads/datasets/dataset_30.xlsx


BIN
api/uploads/datasets/dataset_31.xlsx


BIN
api/uploads/datasets/dataset_32.xlsx


BIN
api/uploads/datasets/dataset_33.xlsx


BIN
api/uploads/datasets/dataset_34.xlsx


BIN
api/uploads/datasets/dataset_35.xlsx


BIN
api/uploads/datasets/dataset_36.xlsx


BIN
api/uploads/datasets/dataset_37.xlsx


BIN
api/uploads/datasets/dataset_38.xlsx


BIN
api/uploads/datasets/dataset_39.xlsx


BIN
api/uploads/datasets/dataset_40.xlsx


BIN
api/uploads/datasets/dataset_41.xlsx


BIN
api/uploads/datasets/dataset_42.xlsx


BIN
api/uploads/datasets/dataset_6.xlsx


BIN
api/uploads/datasets/dataset_8.xlsx


BIN
api/uploads/gZPXSLyqib3F1c15940d87f5acd40d4a4b44a26b103a.xlsx


BIN
api/uploads/gc4ziL16anlB92585f8b5d6cc17655671f9cc9ceb93a.xlsx


BIN
api/uploads/jnZfDmnygaAV1dd962093a2be4c35841ca8b245b1e3b.xlsx


BIN
api/uploads/l6C9XURMYwyN92585f8b5d6cc17655671f9cc9ceb93a.xlsx


BIN
api/uploads/n3K10t9nhqFZd46529682c0a74b35dd76bec86e9f05e.xlsx


BIN
api/uploads/oOYk1nOo3mfD1dd962093a2be4c35841ca8b245b1e3b.xlsx


BIN
api/uploads/wyrZ1vW7Tw731dd962093a2be4c35841ca8b245b1e3b.xlsx


BIN
environment.yml