Forráskód Böngészése

添加了模型训练页面和可视化页面

qw 5 hónapja
szülő
commit
3c48e97f87
29 módosított fájl, 285 hozzáadás és 118 törlés
  1. BIN
      api/SoilAcidification.db
  2. 265 109
      api/api_db.py
  3. 20 9
      api/app/routes.py
  4. BIN
      api/pkl/rf_model_1225_0129.pkl
  5. BIN
      api/pkl/rf_model_1225_0141.pkl
  6. BIN
      api/pkl/rf_model_1225_0152.pkl
  7. BIN
      api/pkl/rf_model_1229_1916.pkl
  8. BIN
      api/pkl/rf_model_1229_2145.pkl
  9. BIN
      api/pkl/rf_model_1229_2155.pkl
  10. BIN
      api/uploads/6bSQknDrwrRlde1f2539a3bc98595af5e8b8588c05a4.xlsx
  11. BIN
      api/uploads/9mYcsUjQQMaZ9b01e46167ade216795568db3c2d37e8.xlsx
  12. BIN
      api/uploads/A66orP06DVdAdd1616dcb86e932a94e2a99d69745d25.xlsx
  13. BIN
      api/uploads/CK9XvADp0egG1dd962093a2be4c35841ca8b245b1e3b.xlsx
  14. BIN
      api/uploads/FH8sVIA4ULaG1dd962093a2be4c35841ca8b245b1e3b.xlsx
  15. BIN
      api/uploads/JVlQLhd4fkWr1dd962093a2be4c35841ca8b245b1e3b.xlsx
  16. BIN
      api/uploads/NLPOdpDyac0b92585f8b5d6cc17655671f9cc9ceb93a.xlsx
  17. BIN
      api/uploads/VggWySJFG0li1dd962093a2be4c35841ca8b245b1e3b.xlsx
  18. BIN
      api/uploads/WEvCgKQR8ptBd46529682c0a74b35dd76bec86e9f05e.xlsx
  19. BIN
      api/uploads/WeQ6qtM436ise129f6334fd40c9f68ed74586a5cd77c.xlsx
  20. BIN
      api/uploads/ZUmVAEVHQGEK92585f8b5d6cc17655671f9cc9ceb93a.xlsx
  21. BIN
      api/uploads/aENap7i3PtIS92585f8b5d6cc17655671f9cc9ceb93a.xlsx
  22. BIN
      api/uploads/atLyS0lcgvUVdd1616dcb86e932a94e2a99d69745d25.xlsx
  23. BIN
      api/uploads/gZPXSLyqib3F1c15940d87f5acd40d4a4b44a26b103a.xlsx
  24. BIN
      api/uploads/gc4ziL16anlB92585f8b5d6cc17655671f9cc9ceb93a.xlsx
  25. BIN
      api/uploads/jnZfDmnygaAV1dd962093a2be4c35841ca8b245b1e3b.xlsx
  26. BIN
      api/uploads/l6C9XURMYwyN92585f8b5d6cc17655671f9cc9ceb93a.xlsx
  27. BIN
      api/uploads/n3K10t9nhqFZd46529682c0a74b35dd76bec86e9f05e.xlsx
  28. BIN
      api/uploads/oOYk1nOo3mfD1dd962093a2be4c35841ca8b245b1e3b.xlsx
  29. BIN
      api/uploads/wyrZ1vW7Tw731dd962093a2be4c35841ca8b245b1e3b.xlsx

BIN
api/SoilAcidification.db


+ 265 - 109
api/api_db.py

@@ -1,15 +1,11 @@
 import os
 import sqlite3
-
-from flask import Flask, jsonify, request
-from flask import g
+from flask import Flask, jsonify, request, send_file, g
+from werkzeug.utils import secure_filename
 from flask_cors import CORS
-
-# 定义应用的状态码文档
-"""
-状态码 200 成功
-状态码 400 失败
-"""
+import pandas as pd
+from io import BytesIO
+import logging
 
 app = Flask(__name__)
 
@@ -17,13 +13,15 @@ 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():
@@ -32,27 +30,240 @@ def get_db():
         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
 
-# 定义添加数据库记录的 API 接口
-@app.route('/add_item', methods=['POST'])
-def add_item():
+    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():
     """
-    接收 JSON 格式的请求体,包含表名和要插入的数据。
-    尝试将数据插入到指定的表中。
-    :return:
+    根据表名从数据库导出数据,并返回 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格式
@@ -65,6 +276,7 @@ def add_item():
 
         if not table_name or not item_data:
             return jsonify({'error': 'Missing table name or item data'}), 400
+
         cur = db.cursor()
 
         # 动态构建 SQL 语句
@@ -74,7 +286,6 @@ def add_item():
         cur.execute(sql, tuple(item_data.values()))
         db.commit()
 
-        # 返回更详细的成功响应
         return jsonify({'success': True, 'message': 'Item added successfully'}), 201
 
     except ValueError as e:
@@ -82,185 +293,132 @@ def add_item():
     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 接口
+# 删除记录
 @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:
-        return jsonify({
-            "success": False,
-            "message": "缺少表名或条件参数"
-        }), 400
+        logging.error("缺少表名或条件参数")
+        return jsonify({"success": False, "message": "缺少表名或条件参数"}), 400
 
-    # 尝试从条件字符串中分离键和值
     try:
         key, value = condition.split('=')
     except ValueError:
-        return jsonify({
-            "success": False,
-            "message": "条件格式错误,应为 'key=value'"
-        }), 400
+        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()
-        # 如果没有错误发生,返回成功响应
-        return jsonify({
-            "success": True,
-            "message": "记录删除成功"
-        }), 200
+
+        # 日志:删除成功
+        logging.info(f"Record deleted from {table_name} where {key} = {value}")
+        return jsonify({"success": True, "message": "记录删除成功"}), 200
     except sqlite3.Error as e:
-        # 发生错误,回滚事务
         db.rollback()
-        # 返回失败响应,并包含错误信息
-        return jsonify({
-            "success": False,
-            "message": f"删除失败: {e}"
-        }), 400
+        # 日志:捕获删除失败的错误
+        logging.error(f"Failed to delete from {table_name} where {key} = {value}. Error: {e}")
+        return jsonify({"success": False, "message": f"删除失败: {e}"}), 400
 
-
-# 定义修改数据库记录的 API 接口
+# 更新记录
 @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
+        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
+        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
+    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)  # 确保 ID 是整数
+        record_id = int(record_id)
     except ValueError:
-        return jsonify({
-            "success": False,
-            "message": "ID 必须是整数"
-        }), 400
+        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
+            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
-
+        return jsonify({"success": False, "message": f"更新失败: {e}"}), 400
 
-# 定义查询数据库记录的 API 接口
+# 查询记录
 @app.route('/search/record', methods=['GET'])
 def sql_search():
-    """
-    接收 JSON 格式的请求体,包含表名和要查询的 ID。
-    尝试查询指定 ID 的记录并返回结果。
-    :return:
-    """
     try:
         data = request.get_json()
-
-        # 表名
         sql_table = data['table']
+        record_id = data['id']
 
-        # 要搜索的 ID
-        Id = data['id']
-
-        # 连接到数据库
         db = get_db()
         cur = db.cursor()
 
-        # 构造查询语句
         sql = f"SELECT * FROM {sql_table} WHERE id = ?"
+        cur.execute(sql, (record_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 接口
+# 提供表格数据
 @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
 
@@ -271,16 +429,14 @@ def get_table():
         rows = cur.fetchall()
 
         if not rows:
-            return jsonify({'error': '表为空或不存在'}), 400
+            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)

+ 20 - 9
api/app/routes.py

@@ -575,7 +575,7 @@ def sql_search():
 
 
 # 定义提供数据库列表,用于展示表格的 API 接口
-@bp.route('/tables', methods=['POST'])
+@bp.route('/table', methods=['POST'])
 def get_table():
     data = request.get_json()
     table_name = data.get('table')
@@ -583,17 +583,28 @@ def get_table():
         return jsonify({'error': '需要表名'}), 400
 
     try:
-        cur = db.cursor()
-        cur.execute(f"SELECT * FROM {table_name}")
-        rows = cur.fetchall()
+        # 创建 sessionmaker 实例
+        Session = sessionmaker(bind=db.engine)
+        session = Session()
 
-        if not rows:
-            return jsonify({'error': '表为空或不存在'}), 400
+        # 动态获取表的元数据
+        metadata = MetaData()
+        table = Table(table_name, metadata, autoload_with=db.engine)
+
+        # 从数据库中查询所有记录
+        query = select(table)
+        result = session.execute(query).fetchall()
 
-        headers = [description[0] for description in cur.description]
+        # 将结果转换为列表字典形式
+        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 sqlite3.Error as e:
+
+    except Exception as e:
         return jsonify({'error': str(e)}), 400
     finally:
-        db.close()
+        # 关闭 session
+        session.close()

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


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/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