YueYunyun пре 6 месеци
комит
28fc2e0a73
42 измењених фајлова са 3158 додато и 0 уклоњено
  1. 2 0
      .gitattributes
  2. 116 0
      .gitignore
  3. BIN
      Doc/工程数量1.xlsx
  4. BIN
      Doc/工程数量2.xlsx
  5. 15 0
      SourceCode/DataMiddleware/.vscode/launch.json
  6. 3 0
      SourceCode/DataMiddleware/.vscode/settings.json
  7. 0 0
      SourceCode/DataMiddleware/app/__init__.py
  8. 21 0
      SourceCode/DataMiddleware/app/config.yml
  9. 4 0
      SourceCode/DataMiddleware/app/data_collect/__init__.py
  10. 18 0
      SourceCode/DataMiddleware/app/data_collect/collect.py
  11. 39 0
      SourceCode/DataMiddleware/app/data_process/__init__.py
  12. 158 0
      SourceCode/DataMiddleware/app/data_process/pre_process.py
  13. 21 0
      SourceCode/DataMiddleware/app/data_process/process.py
  14. 72 0
      SourceCode/DataMiddleware/app/data_process/standard_data_process.py
  15. 15 0
      SourceCode/DataMiddleware/app/data_send/__init__.py
  16. 40 0
      SourceCode/DataMiddleware/app/data_send/send.py
  17. 58 0
      SourceCode/DataMiddleware/app/init.sql
  18. 30 0
      SourceCode/DataMiddleware/app/main.py
  19. 0 0
      SourceCode/DataMiddleware/app/models/__init__.py
  20. 61 0
      SourceCode/DataMiddleware/app/models/source_data.py
  21. 22 0
      SourceCode/DataMiddleware/app/models/standard_data.py
  22. 22 0
      SourceCode/DataMiddleware/app/models/standard_update_log.py
  23. 0 0
      SourceCode/DataMiddleware/app/stores/__init__.py
  24. 401 0
      SourceCode/DataMiddleware/app/stores/mysql_store.py
  25. 13 0
      SourceCode/DataMiddleware/app/ui/__init__.py
  26. 141 0
      SourceCode/DataMiddleware/app/ui/source_data_services.py
  27. 106 0
      SourceCode/DataMiddleware/app/ui/source_data_views.py
  28. 160 0
      SourceCode/DataMiddleware/app/ui/static/source_data_list.js
  29. 125 0
      SourceCode/DataMiddleware/app/ui/static/source_item_list.js
  30. 183 0
      SourceCode/DataMiddleware/app/ui/static/styles.css
  31. 21 0
      SourceCode/DataMiddleware/app/ui/static/utils.js
  32. 180 0
      SourceCode/DataMiddleware/app/ui/templates/source_data/source_data_list.html
  33. 149 0
      SourceCode/DataMiddleware/app/ui/templates/source_data/source_item_list.html
  34. 171 0
      SourceCode/DataMiddleware/app/utils/__init__.py
  35. 120 0
      SourceCode/DataMiddleware/app/utils/ai_helper.py
  36. 79 0
      SourceCode/DataMiddleware/app/utils/config_helper.py
  37. 108 0
      SourceCode/DataMiddleware/app/utils/email_helper.py
  38. 169 0
      SourceCode/DataMiddleware/app/utils/file_helper.py
  39. 113 0
      SourceCode/DataMiddleware/app/utils/logger_helper.py
  40. 117 0
      SourceCode/DataMiddleware/app/utils/mysql_helper.py
  41. 75 0
      SourceCode/DataMiddleware/app/utils/string_helper.py
  42. 10 0
      SourceCode/DataMiddleware/requirements.txt

+ 2 - 0
.gitattributes

@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto

+ 116 - 0
.gitignore

@@ -0,0 +1,116 @@
+# ---> Python
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+.venv/
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# ---> VisualStudioCode
+.settings
+
+
+# ---> JetBrains
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
+
+*.iml
+
+## Directory-based project format:
+.idea/
+# if you remove the above rule, at least ignore the following:
+
+# User-specific stuff:
+# .idea/workspace.xml
+# .idea/tasks.xml
+# .idea/dictionaries
+
+# Sensitive or high-churn files:
+# .idea/dataSources.ids
+# .idea/dataSources.xml
+# .idea/sqlDataSources.xml
+# .idea/dynamic.xml
+# .idea/uiDesigner.xml
+
+# Gradle:
+# .idea/gradle.xml
+# .idea/libraries
+
+# Mongo Explorer plugin:
+# .idea/mongoSettings.xml
+
+## File-based project format:
+*.ipr
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+
+doc/data/
+logs/
+temp_files/

BIN
Doc/工程数量1.xlsx


BIN
Doc/工程数量2.xlsx


+ 15 - 0
SourceCode/DataMiddleware/.vscode/launch.json

@@ -0,0 +1,15 @@
+{
+	// 使用 IntelliSense 了解相关属性。
+	// 悬停以查看现有属性的描述。
+	// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
+	"version": "0.2.0",
+	"configurations": [
+		{
+			"name": "main",
+			"type": "debugpy",
+			"request": "launch",
+			"program": "./app/main.py",
+			"console": "integratedTerminal"
+		}
+	]
+}

+ 3 - 0
SourceCode/DataMiddleware/.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+	"cSpell.words": ["dropna", "usecols"]
+}

+ 0 - 0
SourceCode/DataMiddleware/app/__init__.py


+ 21 - 0
SourceCode/DataMiddleware/app/config.yml

@@ -0,0 +1,21 @@
+app:
+  port: 5123
+mysql:
+  host: 192.168.0.81
+  port: 3307
+  db: iwb_data_middleware_dev
+  user: root
+  password: Iwb-2024
+  charset: utf8mb4
+logger:
+  file_path: './logs/'
+  level: 'debug'
+ai:
+  #  url: http://192.168.0.109:7580/api/chat
+  #  model: qwen2.5:7b
+  key: sk-febca8fea4a247f096cedeea9f185520
+  url: https://dashscope.aliyuncs.com/compatible-mode/v1
+  model: qwen-plus
+  max_tokens: 1024
+file:
+  source_path: './temp_files'

+ 4 - 0
SourceCode/DataMiddleware/app/data_collect/__init__.py

@@ -0,0 +1,4 @@
+from data_collect import collect
+
+def collect_data(project_no: str) ->bool:
+   return collect.DataCollect().run(project_no)

+ 18 - 0
SourceCode/DataMiddleware/app/data_collect/collect.py

@@ -0,0 +1,18 @@
+import utils
+# from stores.mysql_store import MysqlStore
+# from models.source_data import SourceData
+
+class DataCollect:
+
+    def __init__(self):
+        self._logger= utils.get_logger()
+
+    def run(self, project_no: str):
+        try:
+            self._logger.info(f"开始采集数据:{project_no}")
+
+            self._logger.info(f"采集数据完成:{project_no}")
+            return True
+        except Exception as e:
+            self._logger.error(f"采集数据失败:{project_no}",{e})
+            return False

+ 39 - 0
SourceCode/DataMiddleware/app/data_process/__init__.py

@@ -0,0 +1,39 @@
+from data_process import pre_process,process, standard_data_process
+
+def process_project(project_no: str, standard_version: str = "") -> bool:
+    """
+    分析处理数据
+    :param project_no: 项目编号
+    :return: None
+    """
+    if _pre_process_data(project_no):
+        if _process_data(project_no):
+            return True
+        else:
+            return False
+    else:
+        return False
+
+
+def _pre_process_data(project_no: str) -> bool:
+    """
+    预处理数据
+    :param project_no: 项目编号
+    :return: None
+    """
+    return pre_process.PreProcess().run(project_no)
+
+def _process_data(project_no: str) -> bool:
+    """
+    处理数据
+    :param project_no: 项目编号
+    :return: None
+    """
+    return process.Process().run(project_no)
+def process_standard_data(version: str) -> None:
+    """
+    处理标准数据
+    :param version: 标准版本
+    :return: None
+    """
+    standard_data_process.StandardDataProcess().run(version)

+ 158 - 0
SourceCode/DataMiddleware/app/data_process/pre_process.py

@@ -0,0 +1,158 @@
+import utils, pandas as pd
+from pathlib import Path
+
+from models.source_data import SourceData, SourceItemData
+from stores.mysql_store import MysqlStore
+
+class PreProcess:
+
+    def __init__(self):
+        self._logger= utils.get_logger()
+        self._store= MysqlStore()
+        self._ai_helper = utils.AiHelper()
+        self._ai_sys_prompt = "从给定信息中提取结构化信息,并返回json压缩成一行的字符串,如果部分信息为空或nan,则该字段返回为空。"
+        self.separate_ai_calls = False
+        self._data={}
+
+    def run(self, project_no: str, separate_ai_calls=True) ->bool:
+        try:
+            project = self._store.query_source_data(project_no)
+            self._logger.info(f"开始预处理项目:{project.project_name}[{project_no}_{project.standard_version}] ")
+            self.separate_ai_calls = separate_ai_calls
+            raw_data = self.read_excels(f"{utils.get_config_value("file.source_path","./temp_files")}/{project_no}/")
+            if self.separate_ai_calls:
+                for filename, sheets in raw_data.items():
+                    excel_data = self.normalize_data({filename: sheets})
+                    self.call_ai(project_no, excel_data)
+            else:
+                excel_data = self.normalize_data(raw_data)
+                self.call_ai(project_no, excel_data)
+            data= self._data[project_no]
+            data.project_name = project.project_name
+            data.standard_version = project.standard_version
+            data.status = project.status
+            self._store.re_insert_source_data(data)
+            del self._data[project_no]
+            self._logger.info(f"结束预处理项目:{project.project_name}[{project_no}_{project.standard_version}] [设备条数:{len(data.items)}]")
+            return True
+        except Exception as e:
+            self._logger.error(f"预处理项目失败:[{project_no}] 错误: {e}")
+            return False
+
+    def read_excels(self, folder_path):
+        path = Path(folder_path)
+        # 验证路径是否存在
+        if not path.exists():
+            raise FileNotFoundError(f"目录不存在: {path.resolve()}")
+
+        # 验证是否是目录
+        if not path.is_dir():
+            raise NotADirectoryError(f"路径不是目录: {path.resolve()}")
+
+        excel_files = list(path.glob('*'))
+
+        if not excel_files:
+            self._logger.warning(f"警告:未找到任何文件")
+        raw_data = {}
+        for file in excel_files:
+            try:
+                # 使用确定的表头行读取整个 Excel 文件
+                # 读取所有sheet
+                sheets = pd.read_excel(file, sheet_name=None)
+                sheet_data = {}
+                for sheet_name, df in sheets.items():
+                    # 读取前几行以确定表头
+                    header_row = self.determine_header_row(df,file.name)
+                    if header_row >= 0:
+                        # 使用确定的表头行重新读取Sheet
+                        df = df.iloc[header_row:]  # 直接使用确定的表头行
+                        df.columns = df.iloc[0]  # 设置表头
+                        df = df[1:]  # 去掉表头行
+                        df.reset_index(drop=True, inplace=True)  # 重置索引
+                    sheet_data[sheet_name] = df
+                raw_data[file.name] = sheet_data
+                self._logger.debug(f"读取 {file.name} 的 {len(sheet_data)} 个sheet")
+            except Exception as e:
+                self._logger.error(f"读取 {file.name} 失败: {e}")
+        return raw_data
+
+    def determine_header_row(self, df,file_name):
+        if any('序号' in str(cell) or 'No.' in str(cell) for cell in df.head()):
+            self._logger.debug(f"{file_name}表头包含“序号”")
+            return -1
+        for i, row in df.iterrows():
+            if any('序号' in str(cell) or 'No.' in str(cell) for cell in row):
+                self._logger.debug(f"{file_name}找到包含“序号”的行:{i}")
+                return i
+        # 如果没有找到包含“序号”的行,默认使用第0行作为表头
+        self._logger.warning(f"{file_name}未找到包含“序号”的行,使用默认的第1行作为表头")
+        return -1
+    @staticmethod
+    def normalize_data(raw_data):
+        excel_data = []
+        for filename, sheets in raw_data.items():
+            for sheet_name, df in sheets.items():
+                # 统一编码/去除空行等
+                df = df.map(lambda x: x.strip() if isinstance(x, str) else x)
+                # df.columns = ['名称', '型号规格','单位']
+                # 去除空行
+                df = df.dropna(how='all')
+                # 去除空行 及 型号规格为空
+                # df = df.dropna(subset=['型号规格'], how='any')
+                excel_data.append({
+                    "filename": filename,
+                    "sheet": sheet_name,
+                    # "data": df.to_dict(orient='records')
+                    "data": df.to_markdown()
+                })
+        return excel_data
+
+    @staticmethod
+    def prompt_template(excel_data):
+        prompt = """
+        请从以下表格数据中提取结构化信息,要求:
+        1. 识别字段类型:数值/文本/日期
+        2. 提取结构化信息:```typescript
+        export interface device { 
+        n: string; //物料名称
+        m: string; //型号规格
+        u:string; //单位
+        }
+        ```
+        3. 返回结构体device的数组的json数组格式,压缩成一行
+        """
+        prompt += "数据记录:\n"
+        for data in excel_data:
+            # for row in data['data']:
+            #     prompt += f"{row}\n"
+            prompt += f"{data['data']}\n"
+        print("AI_PROMPT: " + prompt)
+        return prompt
+
+    def call_ai(self, project_no, excel_data):
+        source_data = SourceData(project_no)
+        if self.separate_ai_calls:
+            # 初始化self._data[project_no],避免在循环中重复初始化
+            for data in excel_data:
+                prompt = self.prompt_template([data])
+                response = self._ai_helper.call_openai(self._ai_sys_prompt, prompt)
+                # 更新数据部分
+                source_data.items.extend(self.format_data(project_no, response["data"]))
+                source_data.completion_tokens += response["completion_tokens"]
+                source_data.prompt_tokens += response["prompt_tokens"]
+                source_data.total_tokens += response["total_tokens"]
+        else:
+            prompt = self.prompt_template(excel_data)
+            response = self._ai_helper.call_openai(self._ai_sys_prompt, prompt)
+            source_data.completion_tokens = response["completion_tokens"]
+            source_data.prompt_tokens = response["prompt_tokens"]
+            source_data.total_tokens = response["total_tokens"]
+            source_data.items = self.format_data(project_no, response["data"])
+        self._data[project_no] = source_data  
+
+    @staticmethod
+    def format_data(project_no,new_data) ->list[SourceItemData]:
+        formatted_data = []
+        for data in new_data:
+            formatted_data.append(SourceItemData(project_no,data["n"],data["m"],data["u"]))
+        return formatted_data

+ 21 - 0
SourceCode/DataMiddleware/app/data_process/process.py

@@ -0,0 +1,21 @@
+import utils
+
+from stores.mysql_store import MysqlStore
+
+class Process:
+    def __init__(self):
+        self._store= MysqlStore()
+        self._ai_helper = utils.AiHelper()
+        self._ai_sys_prompt = "查询给定数据的标准编号信息,返回压缩的json字符传"
+        self._data={}
+
+    def run(self, project_no: str) ->bool:
+        try:
+            data = self._store.query_source_item_data_list_by_project(project_no)
+            if data is None:
+                return False
+            return True
+        except Exception as e:
+            utils.get_logger().error(f"查询数据失败:{e}")
+            return False
+

+ 72 - 0
SourceCode/DataMiddleware/app/data_process/standard_data_process.py

@@ -0,0 +1,72 @@
+import utils
+from pathlib import Path
+
+from stores.mysql_store import MysqlStore
+import pdfplumber  # 添加对pdfplumber库的导入,用于读取PDF文件
+from pytesseract import image_to_string  # 添加对pytesseract库的导入,用于OCR识别
+from PIL import Image  # 添加对PIL库的导入,用于处理图片
+import io
+import pandas as pd  # 添加对pandas库的导入,用于处理表格数据
+from pdf2image import convert_from_path  # 添加对pdf2image库的导入,用于提取PDF中的图像
+
+class StandardDataProcess:
+    def __init__(self):
+        self._store= MysqlStore()
+        self._ai_helper = utils.AiHelper()
+        self._ai_sys_prompt = "查询给定数据的标准编号信息,返回压缩的json字符传"
+        self._data={}
+
+    def run(self,version:str):
+        path= f"./temp_files/data/v{version}"
+        pdf_data=  self.read_pdf(path)
+        print(pdf_data)
+
+    @staticmethod
+    def read_pdf(folder_path:str):
+        path = Path(folder_path)
+        # 验证路径是否存在
+        if not path.exists():
+            raise FileNotFoundError(f"目录不存在: {path.resolve()}")
+
+        # 验证是否是目录
+        if not path.is_dir():
+            raise NotADirectoryError(f"路径不是目录: {path.resolve()}")
+
+        pdf_files = list(path.glob('*.pdf'))
+
+        if not pdf_files:
+            print(f"警告:未找到任何PDF文件")
+        for file in pdf_files:
+            try:
+                # 读取PDF文件
+                text = ""
+                # 使用pdfplumber提取PDF中的图像
+                with pdfplumber.open(file) as pdf:
+                    for page in pdf.pages:
+                        # 提取页面中的图像
+                        for image in page.images:
+                            # 使用PIL打开图像
+                            img = Image.open(io.BytesIO(image["stream"].get_data()))
+                            # 使用pytesseract进行OCR识别
+                            ocr_text = image_to_string(img, lang='chi_sim')
+                            # 尝试将OCR识别的文本转换为表格
+                            try:
+                                table = pd.read_csv(io.StringIO(ocr_text), sep='\s+', engine='python')
+                                text += table.to_markdown() + "\n"
+                            except Exception as e:
+                                print(f"无法将OCR识别的文本转换为表格: {str(e)}")
+                return text
+            except Exception as e:
+                print(f"Error reading {file}: {str(e)}")
+
+    def prompt_template(data:str):
+        prompt = f"""
+        从下面的表格里,找出电算代号对应的项目名称及单位要求:
+        1. 电算代号是每个表格的第一列数据中的只有数字的,电算代号、项目名称、单位是表格中一列一列的数据。
+        2. 输出压缩成一行的json格式,
+        3. 找出所有的电算代号,不能有遗漏。
+        4. 例子:7 |人工|工日 、1940005|工字钢 Q235-A|kg 输出:[{"e": "7","p": "人工","u": "工日"},{"e": "1940005","p": "工字钢 Q235-A","u": "kg"}]
+        表格数据如下:
+        {data}
+        """
+        return prompt

+ 15 - 0
SourceCode/DataMiddleware/app/data_send/__init__.py

@@ -0,0 +1,15 @@
+from data_send import  send
+
+def send_project_data(project_no: str) -> bool:
+    """
+    发送项目数据到平台
+    :param project_no
+    """
+    return send.DataSend().send_by_project(project_no)
+
+def send_item_data(item_id: int) -> bool:
+    """
+    发送项目数据到平台
+    :param item_id
+    """
+    return send.DataSend().send_by_item_id(item_id)

+ 40 - 0
SourceCode/DataMiddleware/app/data_send/send.py

@@ -0,0 +1,40 @@
+import  utils
+from stores.mysql_store import MysqlStore
+from models.source_data import SourceItemData
+
+
+class DataSend:
+    def __init__(self):
+        self._logger= utils.get_logger()
+        self._store= MysqlStore()
+
+    def send_by_project(self, project_no: str) -> bool:
+        try:
+            self._logger.info(f"开始发送数据,项目:{project_no}")
+            data_list = self._store.query_source_item_data_list_by_project(project_no, with_empty=False)
+            for data in data_list:
+                self.send_data(data)
+            self._logger.info(f"发送数据成功,项目:{project_no} 共发送{len(data_list)}条")
+            return True
+        except Exception as e:
+            self._logger.error(f"发送数据失败,项目:{project_no},错误信息:{e}")
+            return False
+
+    def send_by_item_id(self, item_id: int) -> bool:
+        try:
+            self._logger.info(f"开始发送数据,项目:{item_id}")
+            data = self._store.query_source_item_data_by_id(item_id)
+            if self.send_data(data):
+                self._logger.info(f"发送数据成功,项目:{item_id}")
+                return True
+            return False
+        except Exception as e:
+            self._logger.error(f"发送数据失败,项目:{item_id},错误信息:{e}")
+            return False
+
+    def send_data(self, data: SourceItemData) -> bool:
+        try:
+            self._logger.debug(f"开始远程请求发送数据,数据:{data}" )
+        except Exception as e:
+            self._logger.error(f"发送数据失败,数据:{data},错误信息:{e}")
+            return False

+ 58 - 0
SourceCode/DataMiddleware/app/init.sql

@@ -0,0 +1,58 @@
+-- 创建数据库
+CREATE DATABASE IF NOT EXISTS iwb_data_middleware_dev CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
+USE iwb_data_middleware_dev;
+
+-- 创建 SourceData 表
+CREATE TABLE IF NOT EXISTS source_data (
+    project_no VARCHAR(255) PRIMARY KEY,
+    project_name VARCHAR(255),
+    standard_version VARCHAR(50),
+    status TINYINT DEFAULT 0,
+    completion_tokens INT DEFAULT 0,
+    prompt_tokens INT DEFAULT 0,
+    total_tokens INT DEFAULT 0,
+    is_del TINYINT DEFAULT 0,
+    delete_time DATETIME,
+    update_time DATETIME,
+    create_time DATETIME
+) CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
+
+-- 创建 SourceItemData 表
+CREATE TABLE IF NOT EXISTS source_item_data (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    project_no VARCHAR(255) NOT NULL,
+    device_name VARCHAR(255) NOT NULL,
+    device_model VARCHAR(255) ,
+    device_unit VARCHAR(50) ,
+    device_count FLOAT DEFAULT 0,
+    standard_version VARCHAR(50),
+    standard_no VARCHAR(255) ,
+    remark TEXT,
+    update_time DATETIME 
+) CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
+
+-- 创建 StandardData 表
+CREATE TABLE IF NOT EXISTS standard_data (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    device_name VARCHAR(255),
+    device_model VARCHAR(255),
+    device_unit VARCHAR(50) ,
+    standard_version VARCHAR(50),
+    standard_no VARCHAR(255),
+    create_time DATETIME,
+    update_time DATETIME,
+    remark TEXT
+) CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
+
+-- 创建 StandardUpdateLog 表
+CREATE TABLE IF NOT EXISTS standard_update_log (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    project_no VARCHAR(255) ,
+    device_name VARCHAR(255) ,
+    device_model VARCHAR(255),
+    device_unit VARCHAR(50) ,
+    standard_version VARCHAR(50),
+    new_standard_no VARCHAR(255) ,
+    old_standard_no VARCHAR(255) ,
+    create_time DATETIME 
+) CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;

+ 30 - 0
SourceCode/DataMiddleware/app/main.py

@@ -0,0 +1,30 @@
+import threading
+import utils, ui, data_process
+
+logger = utils.get_logger()
+
+def main():
+    logger.info("程序启动中...")
+    thread = threading.Thread(target=debug_test)
+    thread.start()
+    app = ui.create_app()
+    port = utils.get_config_int("app.port", 5000)
+    app.run(port=port)  # 指定HTTP端口为5000
+
+
+
+
+
+def debug_test():
+    logger.info("调试程序开始")
+    # project_no = "20250224"
+    # data_process.pre_process_data(project_no, "测试","v1")
+    data_process.process_standard_data("1")
+    logger.info("调试程序结束")
+
+
+# 按装订区域中的绿色按钮以运行脚本。
+if __name__ == '__main__':
+    main()
+
+

+ 0 - 0
SourceCode/DataMiddleware/app/models/__init__.py


+ 61 - 0
SourceCode/DataMiddleware/app/models/source_data.py

@@ -0,0 +1,61 @@
+from datetime import datetime
+
+
+class SourceData:
+
+    def __init__(self,
+                 project_no: str,
+                 project_name: str = None,
+                 standard_version: str = None,
+                 status: int = 0,
+                 completion_tokens: int = 0,
+                 prompt_tokens: int = 0,
+                 total_tokens: int = 0,
+                 update_time: datetime = None,
+                 create_time: datetime = None):
+        self.project_no = project_no
+        self.project_name = project_name
+        self.standard_version = standard_version
+        self.status = status
+        self.completion_tokens = completion_tokens
+        self.prompt_tokens = prompt_tokens
+        self.total_tokens = total_tokens
+        self.update_time = update_time
+        self.create_time = create_time
+        self.items = []
+
+    def __str__(self):
+        return f"SourceData(project_no={self.project_no}, project_name={self.project_name}, standard_version={self.standard_version})"
+
+    def set_items(self, items: list):
+        self.items = items
+
+    def add_item(self, item: 'SourceItemData'):
+        self.items.append(item)
+
+
+class SourceItemData:
+    def __init__(self,
+                 project_no: str=None,
+                 device_name: str=None,
+                 device_model: str=None,
+                 device_unit: str = None,
+                 device_count: float = 0,
+                 standard_version: str = None,
+                 standard_no: str = None,
+                 item_id: int = None,
+                 remark: str = None,
+                 update_time: datetime = None):
+        self.id = item_id
+        self.project_no = project_no
+        self.device_name = device_name
+        self.device_model = device_model
+        self.device_unit = device_unit
+        self.device_count = device_count
+        self.standard_no = standard_no
+        self.standard_version = standard_version
+        self.remark = remark
+        self.update_time = update_time
+
+    def __str__(self):
+        return f"SourceItemData(id={self.id}, project_no={self.project_no}, device_name={self.device_name}, device_model={self.device_model}, device_unit={self.device_unit}, device_count={self.device_count}, standard_no={self.standard_no}, standard_version={self.standard_version})"

+ 22 - 0
SourceCode/DataMiddleware/app/models/standard_data.py

@@ -0,0 +1,22 @@
+from datetime import datetime
+
+
+class StandardData:
+
+    def __init__(self,
+                 device_name: str,
+                 device_model: str,
+                 device_unit: str=None,
+                 standard_no: str=None,
+                 standard_version: str=None,
+                 create_time: datetime = None,
+                 update_time: datetime = None,
+                 remark: str = None):
+        self.device_name = device_name
+        self.device_model = device_model
+        self.device_unit = device_unit
+        self.standard_no = standard_no
+        self.standard_version = standard_version
+        self.create_time = create_time
+        self.update_time = update_time
+        self.remark = remark

+ 22 - 0
SourceCode/DataMiddleware/app/models/standard_update_log.py

@@ -0,0 +1,22 @@
+from datetime import datetime
+
+
+class StandardUpdateLog:
+
+    def __init__(self,
+                 project_no: str,
+                 device_name: str = None,
+                 device_model: str = None,
+                 device_unit: str = None,
+                 standard_version: str = None,
+                 new_standard_no: str = None,
+                 old_standard_no: str = None,
+                 create_time: datetime = None):
+        self.project_no = project_no
+        self.device_name = device_name
+        self.device_model = device_model
+        self.device_unit = device_unit
+        self.standard_version = standard_version
+        self.new_standard_no = new_standard_no
+        self.old_standard_no = old_standard_no
+        self.create_time = create_time

+ 0 - 0
SourceCode/DataMiddleware/app/stores/__init__.py


+ 401 - 0
SourceCode/DataMiddleware/app/stores/mysql_store.py

@@ -0,0 +1,401 @@
+from datetime import datetime
+
+from utils.mysql_helper import MySQLHelper
+
+from models.source_data import SourceData, SourceItemData
+from models.standard_data import StandardData
+from models.standard_update_log import StandardUpdateLog
+
+
+class MysqlStore:
+
+    def __init__(self):
+        self._db_helper = MySQLHelper()
+
+    def query_source_data_all(self):
+        query = "SELECT project_no,project_name,standard_version,status,create_time FROM source_data WHERE is_del=0"
+        with self._db_helper:
+            data = []
+            result = self._db_helper.execute_query(query)
+            if not result:
+                return data
+            for item in result:
+                data.append(
+                    SourceData(
+                        project_no=item["project_no"],
+                        project_name=item["project_name"],
+                        standard_version=item["standard_version"],
+                        status=item["status"],
+                        create_time=item["create_time"],
+                    ))
+            return data
+
+    def query_source_data_all_paginated(self, page: int, per_page: int, keyword: str = None, status: int = None) -> (list[SourceData], None):
+        offset = (page - 1) * per_page
+        query_count = "SELECT COUNT(*) as count FROM source_data WHERE is_del=%s"
+        query = "SELECT project_no,project_name,standard_version,status,create_time FROM source_data WHERE is_del=0 "
+        params_count = [0]
+        params = []
+        q_1=" AND (project_no LIKE %s OR project_name LIKE %s)"
+        q_2=" AND status=%s"
+        if keyword:
+            query_count += q_1
+            params_count.extend([f"%{keyword}%", f"%{keyword}%"])
+            query += q_1
+            params.extend([f"%{keyword}%", f"%{keyword}%"])
+
+        if status is not None:
+            query_count += q_2
+            params_count.append(status)
+            query += q_2
+            params.append(status)
+
+        query += " ORDER BY status,create_time DESC LIMIT %s OFFSET %s"
+        params.extend([per_page, offset])
+
+        with self._db_helper:
+            result_count = self._db_helper.fetch_one(query_count, tuple(params_count))
+            count = result_count["count"] if result_count else 0
+            result = self._db_helper.execute_query(query, tuple(params))
+            data = []
+            if not result:
+                return [], count
+            for item in result:
+                data.append(
+                    SourceData(
+                        project_no=item["project_no"],
+                        project_name=item["project_name"],
+                        standard_version=item["standard_version"],
+                        status=item["status"],
+                        create_time=item["create_time"],
+                    ))
+        return data, count
+
+    def query_source_data(self, project_no: str ,with_items=False) -> SourceData|None:
+        query = "SELECT project_no,project_name,standard_version,status,create_time FROM source_data WHERE is_del=0 AND project_no = %s"
+        params = (project_no, )
+        query_items = "SELECT id,project_no,device_name,device_model,standard_version,standard_no FROM source_item_data WHERE project_no = %s"
+        with self._db_helper:
+            result = self._db_helper.fetch_one(query, params)
+            if not result:
+                return None
+            data = SourceData(result["project_no"],
+                              result["project_name"],
+                              result["standard_version"],
+                              result["status"],
+                              create_time=result["create_time"])
+            if not with_items:
+                return data
+            items_result = self._db_helper.execute_query(query_items, params)
+            if items_result:
+                for item in items_result:
+                    data.add_item(
+                        SourceItemData(
+                            item_id=item["id"],
+                            project_no=item["project_no"],
+                            device_name=item["device_name"],
+                            device_model=item["device_model"],
+                            standard_version=item["standard_version"],
+                            standard_no=item["standard_no"],
+                        ))
+            return data
+
+    def query_source_item_data_list_by_project_paginated(self, project_no: str, page: int, per_page: int, keyword: str = None) -> (list[SourceItemData], int):
+        offset = (page - 1) * per_page
+        query_count = "SELECT COUNT(*) as count FROM source_item_data WHERE project_no = %s"
+        query = "SELECT id,project_no,device_name,device_model,device_unit,device_count,standard_version,standard_no FROM source_item_data WHERE project_no = %s"
+        params_count = (project_no,)
+        params = (project_no,)
+
+        if keyword:
+            query_count += " AND (device_name LIKE %s OR device_model LIKE %s)"
+            params_count += (f"%{keyword}%", f"%{keyword}%")
+            query += " AND (device_name LIKE %s OR device_model LIKE %s)"
+            params += (f"%{keyword}%", f"%{keyword}%")
+
+        query += " ORDER BY device_name,device_model LIMIT %s OFFSET %s"
+        params += (per_page, offset)
+
+        with self._db_helper:
+            result_count = self._db_helper.fetch_one(query_count, params_count)
+            count = result_count["count"] if result_count else 0
+            result = self._db_helper.execute_query(query, params)
+            data = []
+            if not result:
+                return data, count
+            for item in result:
+                data.append(
+                    SourceItemData(
+                        item_id=item["id"],
+                        project_no=item["project_no"],
+                        device_name=item["device_name"],
+                        device_model=item["device_model"],
+                        device_unit=item["device_unit"],
+                        device_count=item["device_count"],
+                        standard_version=item["standard_version"],
+                        standard_no=item["standard_no"],
+                    ))
+        return data, count
+
+    def query_source_item_data_list_by_project(self, project_no: str, with_empty=True) -> list[SourceItemData]:
+        query = "SELECT id,project_no,device_name,device_model,device_unit,device_count,standard_version,standard_no FROM source_item_data WHERE project_no = %s"
+        query += "" if with_empty else " AND standard_no !='' AND standard_no IS NOT NULL"
+        params = (project_no,)
+        with self._db_helper:
+            result = self._db_helper.execute_query(query, params)
+            data = []
+            if not result:
+                return data
+            for item in result:
+                data.append(
+                    SourceItemData(
+                        item_id=item["id"],
+                        project_no=item["project_no"],
+                        device_name=item["device_name"],
+                        device_model=item["device_model"],
+                        device_unit=item["device_unit"],
+                        device_count=item["device_count"],
+                        standard_version=item["standard_version"],
+                        standard_no=item["standard_no"],
+                    ))
+            return data
+
+    def insert_source_data(self, source_data: SourceData) -> bool:
+        query = "INSERT INTO source_data (project_no,project_name,standard_version,completion_tokens,prompt_tokens,total_tokens,create_time) VALUES (%s,%s,%s,%s,%s,%s,%s)"
+        params = (
+            source_data.project_no,
+            source_data.project_name,
+            source_data.standard_version,
+            source_data.completion_tokens,
+            source_data.prompt_tokens,
+            source_data.total_tokens,
+            datetime.now(),
+        )
+
+        with self._db_helper:
+            self._db_helper.execute_non_query(query, params)
+        self.insert_source_data_item_list(source_data)
+        return True
+
+    def insert_source_data_item_list(self,source_data):
+        if len(source_data.items) <= 0:
+            return
+        query_items = "INSERT INTO source_item_data (project_no,device_name,device_model,device_unit,device_count,standard_version,standard_no,update_time) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)"
+        params_items = []
+        for item in source_data.items:
+            params_items.append((
+                source_data.project_no,
+                item.device_name,
+                item.device_model,
+                item.device_unit,
+                item.device_count,
+                source_data.standard_version,
+                item.standard_no,
+                datetime.now(),
+            ))
+        with self._db_helper:
+            self._db_helper.execute_many(query_items, params_items)
+
+    def update_source_data(self, source_data: SourceData):
+        query = "UPDATE source_data SET project_name = %s, standard_version = %s ,status = %s,update_time = %s WHERE project_no = %s"
+        params = (
+            source_data.project_name,
+            source_data.standard_version,
+            source_data.status,
+            datetime.now(),
+            source_data.project_no,
+        )
+        with self._db_helper:
+            self._db_helper.execute_non_query(query, params)
+
+    def re_insert_source_data(self, source_data: SourceData) -> bool:
+        if not source_data.project_no:
+            return False
+        self.update_source_data(source_data)
+        query = "DELETE FROM source_item_data WHERE project_no = %s"
+        params = (source_data.project_no,)
+        with self._db_helper:
+            self._db_helper.execute_non_query(query, params)
+        self.insert_source_data_item_list(source_data)
+
+    def delete_source_data(self, project_no: str) -> bool:
+        query_1 = "UPDATE source_data SET is_del=1,delete_time=%s WHERE project_no = %s"
+        # query_2 = "DELETE FROM source_item_data WHERE project_no = %s"
+        params = (datetime.now(),project_no, )
+        with self._db_helper:
+            self._db_helper.execute_non_query(query_1, params)
+            # self._db_helper.execute_non_query(query_2, params)
+            return True
+    def query_source_item_data_by_id(self, item_id:int) -> SourceItemData | None:
+        query = "SELECT id,project_no,device_name,device_model,device_unit,device_count,standard_version,standard_no FROM source_item_data WHERE id = %s"
+        params = (item_id,)
+        with self._db_helper:
+            result = self._db_helper.fetch_one(query, params)
+            if not result:
+                return None
+            data = SourceItemData(
+                item_id=result["id"],
+                project_no=result["project_no"],
+                device_name=result["device_name"],
+                device_model=result["device_model"],
+                device_unit=result["device_unit"],
+                device_count=result["device_count"],
+                standard_version=result["standard_version"],
+                standard_no=result["standard_no"],
+                update_time=datetime.now()
+            )
+            return data
+    def query_source_item_data(self, project_no: str,device_name:str, device_model:str) -> SourceItemData| None:
+        query = "SELECT id,project_no,device_name,device_model,device_unit,device_count,standard_version,standard_no FROM source_item_data WHERE project_no = %s AND device_name = %s AND device_model = %s"
+        params = (project_no,device_name, device_model )
+        with self._db_helper:
+            result = self._db_helper.fetch_one(query, params)
+            if not result:
+                return None
+            data = SourceItemData(
+                item_id=result["id"],
+                project_no=result["project_no"],
+                device_name=result["device_name"],
+                device_model=result["device_model"],
+                device_unit=result["device_unit"],
+                device_count=result["device_count"],
+                standard_version=result["standard_version"],
+                standard_no=result["standard_no"],
+            )
+            return data
+    def insert_source_item_data(self, source_item_data: SourceItemData) -> bool:
+        query = "INSERT INTO source_item_data (project_no,device_name,device_model,device_unit,device_count,standard_version,standard_no,update_time) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)"
+        params = (
+            source_item_data.project_no,
+            source_item_data.device_name,
+            source_item_data.device_model,
+            source_item_data.device_unit,
+            source_item_data.device_count,
+            source_item_data.standard_version,
+            source_item_data.standard_no,
+            datetime.now(),
+        )
+        with self._db_helper:
+            self._db_helper.execute_non_query(query, params)
+            return True
+    def update_source_item_data(self, source_item_data: SourceItemData) -> bool:
+        query = "SElECT standard_no FROM source_item_data WHERE id = %s"
+        params = (source_item_data.id,)
+        u_query = "UPDATE source_item_data SET device_unit= %s,device_count= %s,standard_no = %s,update_time = %s WHERE id = %s"
+        u_params = (source_item_data.device_unit, source_item_data.device_count, source_item_data.standard_no, datetime.now(), source_item_data.id)
+        with self._db_helper:
+            result = self._db_helper.fetch_one(query, params)
+            if result:
+
+                self._db_helper.execute_non_query(u_query, u_params)
+                self.insert_or_update_standard_data(
+                    StandardData(device_name=source_item_data.device_name,
+                                 device_model=source_item_data.device_model,
+                                 device_unit=source_item_data.device_unit,
+                                 standard_version=source_item_data.standard_version,
+                                 standard_no=source_item_data.standard_no),
+                    source_item_data.project_no)
+            return True
+    def delete_source_item_data_by_id(self, item_id: int):
+        query = "DELETE FROM source_item_data WHERE id = %s"
+        params = (item_id, )
+        with self._db_helper:
+            self._db_helper.execute_non_query(query, params)
+            return True
+    def delete_source_item_data(self,
+                                  project_no: str,
+                                  device_name: str,
+                                  device_model: str) -> bool:
+        query = "DELETE FROM source_item_data WHERE project_no = %s AND device_name = %s AND device_model = %s"
+        params = (project_no, device_name, device_model)
+        with self._db_helper:
+            self._db_helper.execute_non_query(query, params)
+            return True
+
+
+    def query_standard_data(self, device_name: str,
+                            device_model: str) -> list[StandardData]|None:
+        query = "SELECT device_model,device_name,device_unit,standard_version,standard_no FROM standard_data WHERE device_name = %s AND device_model = %s "
+        params = (device_name, device_model)
+        with self._db_helper:
+            results = self._db_helper.execute_query(query, params)
+            if not results:
+                return None
+            data = []
+            for result in results:
+                data.append(StandardData(
+                device_model=result["device_model"],
+                device_name=result["device_name"],
+                device_unit=result["device_unit"],
+                standard_version=result["standard_version"],
+                standard_no=result["standard_no"],
+            ))
+            return data
+
+    def insert_or_update_standard_data(self,
+                                       standard_data: StandardData,
+                                       project_no: str = None) -> bool:
+        query = "SELECT device_model,device_name,device_unit,standard_version,standard_no FROM standard_data WHERE device_name = %s AND device_model = %s AND standard_version = %s"
+        params = (standard_data.device_name, standard_data.device_model,standard_data.standard_version)
+        i_query = "INSERT INTO standard_data (device_name,device_model,device_unit,standard_version,standard_no,create_time) VALUES (%s,%s,%s,%s,%s,%s)"
+        i_params = (
+            standard_data.device_name,
+            standard_data.device_model,
+            standard_data.device_unit,
+            standard_data.standard_version,
+            standard_data.standard_no,
+            datetime.now(),
+        )
+        u_query = "UPDATE standard_data SET  device_unit=%s, standard_no = %s, update_time = %s WHERE device_name = %s AND device_model = %s AND standard_version = %s"
+        u_params = (
+            standard_data.device_unit,
+            standard_data.standard_no,
+            datetime.now(),
+            standard_data.device_name,
+            standard_data.device_model,
+            standard_data.standard_version,
+        )
+        with self._db_helper:
+            data = self._db_helper.fetch_one(query, params)
+            if data:
+                self._db_helper.execute_non_query(u_query, u_params)
+                if project_no:
+                    self.insert_standard_update_log(
+                        StandardUpdateLog(
+                            project_no=project_no,
+                            device_name=standard_data.device_name,
+                            device_model=standard_data.device_model,
+                            device_unit= standard_data.device_unit,
+                            standard_version=standard_data.standard_version,
+                            new_standard_no=standard_data.standard_no,
+                            old_standard_no=data["standard_no"]))
+            else:
+                self._db_helper.execute_non_query(i_query, i_params)
+            return True
+
+    def query_standard_update_log(self, project_no: str) -> list[StandardUpdateLog]|None:
+        query = "SELECT project_no,device_name,device_model,device_unit,standard_version,new_standard_no,old_standard_no,create_time FROM standard_update_log WHERE project_no = %s"
+        params = (project_no, )
+        with self._db_helper:
+            results = self._db_helper.execute_query(query, params)
+            if not results:
+                return None
+            data = [StandardUpdateLog(**result) for result in results]
+            return data
+
+    def insert_standard_update_log(
+            self, standard_update_log: StandardUpdateLog) -> bool:
+        query = "INSERT INTO standard_update_log (project_no,device_name,device_model,device_unit,standard_version,new_standard_no,old_standard_no,create_time) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)"
+        params = (
+            standard_update_log.project_no,
+            standard_update_log.device_name,
+            standard_update_log.device_model,
+            standard_update_log.device_unit,
+            standard_update_log.standard_version,
+            standard_update_log.new_standard_no,
+            standard_update_log.old_standard_no,
+            datetime.now(),
+        )
+        with self._db_helper:
+            self._db_helper.execute_non_query(query, params)
+            return True

+ 13 - 0
SourceCode/DataMiddleware/app/ui/__init__.py

@@ -0,0 +1,13 @@
+import os,utils
+from flask import Flask
+
+def create_app():
+    # current_dir = os.path.dirname(os.path.abspath(__file__))
+    # template_path = os.path.join(current_dir, 'templates')
+    # web_app = Flask(__name__, template_folder=template_path)
+    web_app = Flask(__name__)
+    # 注册蓝图或其他初始化操作
+    from .source_data_views import source_data as source_data_blueprint
+    web_app.register_blueprint(source_data_blueprint)
+
+    return web_app

+ 141 - 0
SourceCode/DataMiddleware/app/ui/source_data_services.py

@@ -0,0 +1,141 @@
+import threading
+
+from models.source_data import SourceData, SourceItemData
+from stores.mysql_store import MysqlStore
+import data_collect,data_process,data_send
+
+
+class SourceDataService:
+
+    def __init__(self):
+        self._store = MysqlStore()
+
+    def get_all_source_data_paginated(self, page: int, per_page: int, keyword: str = None, status: int = None) ->(list[SourceData], int):
+        source_data_list, total_count = self._store.query_source_data_all_paginated(page, per_page, keyword, status)
+        return source_data_list, total_count
+
+    def get_all_source_data(self) ->list[SourceData]:
+        source_data_list = self._store.query_source_data_all()
+        return source_data_list
+
+    def get_source_data_by_project_no(self,project_no:str) ->SourceData:
+        source_data = self._store.query_source_data(project_no)
+        return source_data
+
+    def add_source_data(self,source_data:SourceData):
+        self._store.insert_source_data(source_data)
+    def update_source_data(self,source_data:SourceData):
+        self._store.update_source_data(source_data)
+
+    def start_collect_data(self, project_no:str)->(bool,str):
+        data = self._store.query_source_data(project_no)
+        if data:
+            if data.status == 21:
+                return False, '项目正采集数据中'
+            if data.status == 31:
+                return False, '项目已采集数据'
+            if data.status == 22:
+                return False, '项目正在分析处理中'
+            if data.status == 32:
+                return False, '项目分析处理完成'
+            data.status = 21
+            self._store.update_source_data(data)
+            #TODO 启动采集数据
+            thread = threading.Thread(target=self._collect_project, args=(data,))
+            thread.start()
+            return True, ''
+        else:
+            return False, '项目不存在'
+
+    def _collect_project(self,project:SourceData):
+        if  data_collect.collect_data(project.project_no):
+            project.status = 31
+            self._store.update_source_data(project)
+        else:
+            project.status = 11
+            self._store.update_source_data(project)
+    def start_process_data(self, project_no: str) -> (bool, str):
+        data = self._store.query_source_data(project_no)
+        if data:
+            if data.status == 0:
+                return False, '项目还未采集数据'
+            if data.status == 21:
+                return False, '项目正在采集数据中'
+            if data.status == 22:
+                return False, '项目正在分析处理中'
+            if data.status == 32:
+                return False, '项目分析处理完成'
+            data.status = 22
+            self._store.update_source_data(data)
+            thread = threading.Thread(target = self._process_and_send_project, args=(data,))
+            thread.start()
+            return True, ''
+        else:
+            return False, '项目不存在'
+
+    def _process_and_send_project(self, project: SourceData) :
+        # 启动分析处理
+        if data_process.process_project(project.project_no):
+            # 更新远程数据
+            data_send.send_project_data(project.project_no)
+            project.status = 32
+            self._store.update_source_data(project)
+            return True, ''
+        else:
+            project.status = 12
+            self._store.update_source_data(project)
+
+    def delete_source_data(self,project_no:str):
+        source_data = self._store.query_source_data(project_no)
+        if source_data:
+            self._store.delete_source_data(project_no)
+            return True
+        else:
+            return False
+
+    def get_source_item_data_list_by_project_paginated(self, project_no: str, page: int, per_page: int, keyword: str = None) -> (list[SourceItemData], int):
+        return self._store.query_source_item_data_list_by_project_paginated(project_no, page, per_page, keyword)
+    def get_source_item_list_by_project_no(self, project_no:str) ->list[SourceItemData]:
+        return  self._store.query_source_item_data_list_by_project(project_no)
+
+    def get_source_item_by_id(self, item_id: int):
+        return self._store.query_source_item_data_by_id(item_id)
+
+    def get_source_item(self, project_no:str, device_name:str, device_model:str):
+        return self._store.query_source_item_data(project_no, device_name, device_model)
+
+    def add_source_item(self,item:SourceItemData):
+        source_item = self._store.query_source_item_data(item.project_no, item.device_name, item.device_model)
+        if source_item:
+            return False
+        else:
+            project = self._store.query_source_data(item.project_no)
+            source_item = SourceItemData(
+                project_no=item.project_no,
+                device_name=item.device_name,
+                device_model=item.device_model,
+                device_unit=item.device_unit,
+                standard_version=project.standard_version,
+                standard_no=item.standard_no,
+            )
+            self._store.insert_source_item_data(source_item)
+            return True
+    def update_source_item(self, item:SourceItemData):
+        source_item = self._store.query_source_item_data_by_id(item.id)
+        if source_item:
+            source_item.standard_no = item.standard_no
+            self._store.update_source_item_data(source_item)
+            #TODO 更新标准数据库
+
+            #TODO 更新远程数据
+
+            return True
+        else:
+            return False
+    def delete_source_item(self,item_id:int):
+        source_item = self._store.query_source_item_data_by_id(item_id)
+        if source_item:
+            self._store.delete_source_item_data_by_id(source_item.id)
+            return True
+        else:
+            return False

+ 106 - 0
SourceCode/DataMiddleware/app/ui/source_data_views.py

@@ -0,0 +1,106 @@
+from flask import render_template, url_for, redirect, request, jsonify
+from flask import Blueprint
+import os
+
+from .source_data_services import SourceDataService
+from models.source_data import SourceData,SourceItemData
+
+SourceDataService = SourceDataService()  # 确保 SourceDataService 是一个实例
+
+# 创建一个名为 'source_data' 的蓝图
+source_data = Blueprint('source_data', __name__, template_folder='templates/source_data')
+
+@source_data.route('/')
+def index():
+    return "Hello, World!"
+
+@source_data.route('/source_data_list')
+def source_data_list():
+    keyword = request.args.get('k', '', type=str)
+    page = request.args.get('p', 1, type=int)
+    per_page = request.args.get('pp', 15, type=int)
+    status = request.args.get('s',-1, type=int)
+    data_list, total_count = SourceDataService.get_all_source_data_paginated(page, per_page, keyword,None if status==-1 else status)  # 传递 status 参数
+    return render_template('source_data_list.html', source_data_list=data_list, keyword=keyword, page=page, per_page=per_page, total_count=total_count, status=status)  # 传递 status 参数到模板
+
+@source_data.route('/source_item_list/<project_no>')
+def source_item_list(project_no):
+    keyword = request.args.get('k', '', type=str)
+    page = request.args.get('p', 1, type=int)
+    per_page = request.args.get('pp', 15, type=int)
+    source_data = SourceDataService.get_source_data_by_project_no(project_no)
+    source_items, total_count = SourceDataService.get_source_item_data_list_by_project_paginated(project_no, page, per_page, keyword)
+    return render_template('source_item_list.html', source_data=source_data, source_items=source_items, keyword=keyword, page=page, per_page=per_page, total_count=total_count)
+
+@source_data.route('/add_source_data', methods=['POST'])
+def add_source_data():
+    data = request.get_json()
+    project_no = data.get('project_no')
+    if not project_no:
+        return jsonify({'success': False, 'error': 'Project No 不能为空'}), 400
+    project_name = data.get('project_name')
+    standard_version = data.get('standard_version')
+    source_data= SourceData(project_no=project_no, project_name=project_name, standard_version=standard_version)
+    SourceDataService.add_source_data(source_data)
+    return jsonify({'success': True})
+
+@source_data.route('/update_source_data', methods=['POST'])
+def update_source_data():
+    data = request.get_json()
+    project_no = data.get('project_no')
+    if not project_no:
+        return jsonify({'success': False, 'error': 'Project No 不能为空'}), 400
+    project_name= data.get('project_name')
+    standard_version= data.get('standard_version')
+    source_data= SourceData(project_no=project_no, project_name=project_name, standard_version=standard_version)
+    SourceDataService.update_source_data(source_data)
+    return jsonify({'success': True})
+
+@source_data.route('/collect_source_data/<project_no>', methods=['POST'])
+def collect_source_data(project_no):
+    result, err_msg = SourceDataService.start_collect_data(project_no)
+    return jsonify({'success': result, 'error': err_msg})
+
+@source_data.route('/process_source_data/<project_no>', methods=['POST'])
+def process_source_data(project_no):
+    result, err_msg = SourceDataService.start_process_data(project_no)
+    return jsonify({'success': result, 'error': err_msg})
+
+@source_data.route('/delete_source_data/<project_no>', methods=['POST'])
+def delete_source_data(project_no):
+    SourceDataService.delete_source_data(project_no)  # 使用实例方法
+    return redirect(url_for('source_data.source_data_list'))
+
+@source_data.route('/add_source_item', methods=['POST'])
+def add_source_item():
+    data = request.get_json()
+    project_no = data.get('project_no')
+    device_name = data.get('device_name')
+    device_model = data.get('device_model')
+    device_unit = data.get('device_unit')
+    standard_no = data.get('standard_no')
+    new_item = SourceItemData(project_no=project_no, device_name=device_name, device_model=device_model, device_unit=device_unit, standard_no=standard_no)
+    item_id = SourceDataService.add_source_item(new_item)
+    return jsonify({'success': True, 'id': item_id})
+
+@source_data.route('/update_data_item', methods=['POST'])
+def update_data_item():
+    data = request.get_json()
+    item_id = data.get('id')
+    if not item_id:
+        return jsonify({'success': False, 'error': 'ID 不能为空'}), 400
+    standard_no = data.get('standard_no')
+    if not standard_no:
+        return jsonify({'success': False, 'error': 'Standard No is missing'}), 400
+    item = SourceItemData(item_id=int(item_id), standard_no=standard_no)
+    SourceDataService.update_source_item(item)
+    return jsonify({'success': True})
+
+@source_data.route('/delete_source_item/<item_id>', methods=['POST'])
+def delete_source_item_route(item_id):
+    source_item = SourceDataService.get_source_item_by_id(item_id)
+    SourceDataService.delete_source_item(item_id)
+    return redirect(url_for('source_data.source_item_list', project_no=source_item.project_no))
+
+
+

+ 160 - 0
SourceCode/DataMiddleware/app/ui/static/source_data_list.js

@@ -0,0 +1,160 @@
+function addNewData(){
+    const table = document.querySelector('table.table tbody');
+    const rows = table.querySelectorAll('tr.edit-mode');
+    rows.forEach(row => row.classList.remove('edit-mode'));
+    const newRow = document.createElement('tr');
+    newRow.classList.add('edit-mode');
+    newRow.innerHTML = `
+        <td class="editable project_no"><span class="edit"><input type="text" class="form-control" placeholder="项目编号"></span></td>
+        <td class="editable project_name"><span class="edit"><input type="text" class="form-control" placeholder="项目名称"></span></td>
+        <td class="editable standard_version"><span class="edit"><select class="form-control"><option value="1" {% if source_data.standard_version == 1 %}selected{% endif %}>旧版</option><option value="2" {% if source_data.standard_version == 2 %}selected{% endif %}>新版</option></select></td>
+        <td class="editable "><span class="edit">新建</span></td>
+        <td class="editable tool">
+            <span class="show">
+                
+            </span>
+            <span class="edit">
+                <button class="btn btn-success" onclick="saveDataCreate(this.parentNode.parentNode.parentNode)">确定</button>
+                <button class="btn btn-warning" onclick="cancelNewChanges(this.parentNode.parentNode.parentNode)">取消</button>
+            </span>
+        </td>
+    `;
+    table.insertBefore(newRow, table.firstChild);
+
+}
+function saveDataCreate(row){
+    const project_no = row.querySelector('td.project_no .form-control').value;
+    if (project_no === '') {
+        alert('项目编号')
+    }
+    const name = row.querySelector('td.project_name .form-control').value;
+    if (name === '') {
+        alert('名称不能为空');
+        return;
+    }
+    version = row.querySelector('td.standard_version .form-control').value;
+    if (version === '') {
+        alert('版本号不能为空');
+        return;
+    }
+    fetch(`/add_source_data`, {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({project_no: project_no, project_name: name,standard_version:version })
+    }).then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                alert('添加成功');
+                window.location.href = '/source_data_list';
+            }else{
+                alert('添加失败:'+data.error);
+            }
+        });
+}
+
+function saveDataEdit(row,project_no){
+    const name = row.querySelector('.project_name .form-control').value;
+    const version = row.querySelector('.standard_version .form-control').value;
+    if (name === '') {
+        alert('名称不能为空');
+        return;
+    }
+    if (version === '') {
+        alert('版本号不能为空');
+        return;
+    }
+    fetch(`/update_source_data`, {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({project_no: project_no, project_name: name,standard_version:version })
+    }).then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                alert('保存成功');
+                window.location.reload();
+            } else {
+                alert('保存失败:'+data.error);
+            }
+        })
+        .catch(error => {
+            console.error('保存失败:', error);
+            alert('保存失败');
+        });
+
+}
+
+function confirmCollectData(project_no) {
+    if (confirm('确定要开始采集数据吗?')){
+        fetch(`/collect_source_data/${project_no}`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            }
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                alert('操作成功');
+                window.location.reload();
+            } else {
+                alert('采集失败:'+data.error);
+            }
+        })
+        .catch(error=> {})
+    }
+}
+
+function confirmProcessData(project_no) {
+    if (confirm('确定要开始分析处理数据吗?')){
+        fetch(`/process_source_data/${project_no}`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            }
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                alert('操作成功');
+                window.location.reload();
+            } else {
+                alert('分析处理失败:'+data.error);
+            }
+        })
+        .catch(error=> {})
+    }
+}
+
+
+function confirmDataDelete(project_no) {
+    if (confirm('确定要删除该项目吗?')) {
+        fetch(`/delete_source_data/${project_no}`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            }
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                alert('删除成功');
+                // 删除对应的行
+                const row = document.querySelector(`tr[data-id="${itemId}"]`);
+                if (row) {
+                    row.remove();
+                }
+            } else {
+                alert('删除失败:'+data.error);
+            }
+        })
+        .catch(error => {
+            console.error('删除失败:', error);
+            alert('删除失败');
+        });
+    }
+}
+

+ 125 - 0
SourceCode/DataMiddleware/app/ui/static/source_item_list.js

@@ -0,0 +1,125 @@
+function addNewItem(project_no) {
+    const table = document.querySelector('table.table tbody');
+    const rows = table.querySelectorAll('tr.edit-mode');
+    rows.forEach(row => row.classList.remove('edit-mode'));
+
+    const newRow = document.createElement('tr');
+    newRow.classList.add('edit-mode');
+    newRow.innerHTML = `
+        <td class="editable name"><span class="edit"><input type="text" class="form-control" placeholder="名称"> </span></td>
+        <td class="editable model"><span class="edit"><input type="text" class="form-control" placeholder="规格型号"> </span></td>
+        <td class="editable unit"><span class="edit"><input type="text" class="form-control" placeholder="单位"> </span></td>
+        <td class="editable standard_no"><span class="edit"><input type="text" class="form-control" placeholder="标准编号"></span>
+        </td>
+        <td class="editable tool">
+            <span class="show"></span>
+            <span class="edit">
+                <button class="btn btn-success" onclick="saveItemCreate(this.parentNode.parentNode.parentNode,'${project_no}')">确定</button>
+                <button class="btn btn-warning" onclick="cancelNewChanges(this.parentNode.parentNode.parentNode)">取消</button>
+            </span>
+        </td>
+    `;
+    table.insertBefore(newRow, table.firstChild);
+}
+
+function saveItemCreate(row,project_no) {
+    const deviceName = row.querySelector('td.name input').value;
+    const deviceModel = row.querySelector('td.model input').value;
+    const deviceUnit = row.querySelector('td.unit input').value;
+    const standardNo = row.querySelector('td.standard_no input').value;
+    if (deviceName === '') {
+        alert('名称不能为空');
+        return;
+    }
+    if (deviceModel === '') {
+        alert('规格型号不能为空');
+        return;
+    }
+    if (deviceUnit === '') {
+        alert('单位不能为空');
+        return;
+    }
+    if (standardNo === '') {
+        alert('标准编号不能为空');
+        return;
+    }
+
+    fetch(`/add_source_item`, {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({project_no:project_no,device_name: deviceName, device_model: deviceModel, device_unit: deviceUnit, standard_no: standardNo })
+    }).then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                alert('添加成功');
+                window.location.href = `/source_item_list/${project_no}`;
+            } else {
+                alert('添加失败:'+data.error);
+            }
+        })
+        .catch(error => {
+            console.error('添加失败:', error);
+            alert('添加失败');
+        });
+}
+
+function saveItemEdit(row, itemId) {
+    const standardNo = row.querySelector('.standard_no input').value;
+    if (standardNo === '') {
+        alert('标准编号不能为空');
+        return;
+    }
+    fetch(`/update_data_item`, {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({id: itemId, standard_no: standardNo })
+    }).then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                alert('保存成功');
+                row.classList.remove('edit-mode');
+                row.querySelector('.standard_no .show').textContent = standardNo;
+                // window.location.reload();
+            } else {
+                alert('保存失败:'+data.error);
+            }
+        })
+        .catch(error => {
+            console.error('保存失败:', error);
+            alert('保存失败');
+        });
+}
+
+
+function confirmItemDelete(itemId) {
+    if (confirm('确定要删除该设备吗?')) {
+        fetch(`/delete_source_item/${itemId}`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            }
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                alert('删除成功');
+                // 删除对应的行
+                const row = document.querySelector(`tr[data-id="${itemId}"]`);
+                if (row) {
+                    row.remove();
+                }
+            } else {
+                alert('删除失败:'+data.error);
+            }
+        })
+        .catch(error => {
+            console.error('删除失败:', error);
+            alert('删除失败');
+        });
+    }
+}
+

+ 183 - 0
SourceCode/DataMiddleware/app/ui/static/styles.css

@@ -0,0 +1,183 @@
+/* styles.css */
+h1,h2,h3,h4,h5,h6{
+    margin: 0;
+}
+.btn {
+    margin: 0 5px;
+    padding: 4px 15px;
+    background-color: #f2f2f2;
+    border: 1px solid #ccc;
+    cursor: pointer;
+    border-radius: 4px;
+    outline: none;
+}
+.btn-large{
+    padding: 6px 18px;
+}
+.btn:hover{
+    background-image: linear-gradient(to bottom, rgba(0,0,0,0.05), rgba(0,0,0,0.05));
+}
+.btn.btn-info{
+    background-color: #2196F3;
+    color: white;
+    border: 1px solid #2196F3;
+}
+.btn.btn-warning{
+    background-color: #ff9800;
+    color: white;
+    border: 1px solid #ff9800;
+}
+.btn.btn-danger{
+    background-color: #f44336;
+    color: white;
+    border: 1px solid #f44336;
+}
+.btn.btn-success{
+    background-color: #4CAF50;
+    color: white;
+    border: 1px solid #4CAF50;
+}
+.label{
+    margin: 0 5px;
+    padding: 3px 10px;
+    font-size: 12px;
+    color: #666;
+    background-color: #f2f2f2;
+    border: 1px solid #ccc;
+    border-radius: 15px;
+}
+.label.label-info{
+    background-color: #2196F3;
+    color: white;
+    border: 1px solid #2196F3;
+}
+.label.label-warning{
+    background-color: #ff9800;
+    color: white;
+    border: 1px solid #ff9800;
+}
+.label.label-danger{
+    background-color: #f44336;
+    color: white;
+    border: 1px solid #f44336;
+}
+.label.label-success{
+    background-color: #4CAF50;
+    color: white;
+    border: 1px solid #4CAF50;
+}
+.form-control{
+    width:  240px;
+    padding: 6px 10px;
+    outline: none;
+    border: 1px solid #2196F3;
+    border-radius: 3px;
+    margin: 0 5px;
+}
+.box{
+    margin: 20px 20px;
+}
+.box_header{
+    margin-bottom: 10px;
+    display: flex;
+    justify-content: space-between;
+    background-color: #f9f9f9;
+    padding: 10px;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+}
+.box_header dl{
+    display: flex;
+    align-items: center;
+    margin: 0 10px;
+}
+.box_header dl dt{
+    font-weight: bolder;
+    color: #333;
+}
+.box_header dl dd{
+    margin-left: 10px;
+    color: #555;
+}
+
+.box_body{
+    margin-top: 10px;
+}
+table {
+    width: 100%;
+    border-collapse: collapse;
+}
+th, td {
+    border: 1px solid #ddd;
+    padding: 8px;
+    text-align: center;
+}
+th {
+    background-color: #f2f2f2;
+}
+tr:hover {
+    background-color: #f5f5f5;
+}
+   
+td.editable .edit{
+    display: none;
+}
+.edit-mode .editable .show{
+    display: none;
+}
+.edit-mode .editable .edit{
+    width: 100%;
+    display: flex;
+    justify-content: center;
+}
+.edit-mode .editable .form-control {
+    width: 80%;
+    max-width: 300px;
+    padding: 5px 10px;
+    border: 1px solid #4CAF50;
+}
+
+.pagination {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 20px 0px 0;
+}
+.pagination .pagination-links{
+    display: flex;
+}
+
+.pagination .page {
+    height: 40px;
+    min-width: 20px;
+    padding: 0 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    text-decoration: none;
+    color: black;
+    border: 1px solid #ddd;
+    margin: 0 4px;
+    border-radius: 4px;
+    background-color: #eee;
+}
+.pagination .pagination-links .page.page-link{
+    background-color: white;
+    cursor: pointer;
+}
+.pagination .pagination-links .page.page-link:hover {
+    background-color: #ddd;
+}
+
+.pagination .pagination-links .page.page-link.active {
+    background-color: #2196F3;
+    color: white;
+    border: 1px solid #2196F3;
+}
+
+.pagination .pagination-info {
+    font-size: 14px;
+    text-align: center;
+    color: #3c3c3c;
+
+}

+ 21 - 0
SourceCode/DataMiddleware/app/ui/static/utils.js

@@ -0,0 +1,21 @@
+function goTo(url) {
+    window.location.href = url;
+}
+
+function toggleEditMode(row) {
+    const table = document.querySelector('table.table tbody');
+    const rows = table.querySelectorAll('tr.edit-mode');
+    rows.forEach(row => row.classList.remove('edit-mode'));
+    let inputs = row.querySelectorAll('td .editable input');
+    for (let i = 0; i < inputs.length; i++) {
+        inputs[i].value = "";
+    }
+    row.classList.toggle('edit-mode');
+}
+function cancelChanges(row) {
+    row.classList.remove('edit-mode');
+}
+
+function cancelNewChanges(row) {
+    row.remove();
+}

+ 180 - 0
SourceCode/DataMiddleware/app/ui/templates/source_data/source_data_list.html

@@ -0,0 +1,180 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>项目列表</title>
+    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
+    <script src="{{ url_for('static', filename='utils.js') }}"></script>
+    <script src="{{ url_for('static', filename='source_data_list.js') }}"></script>
+    <script>
+        function searchData(){
+            var keyword = document.getElementById('search_input').value;
+            var status = document.getElementById('status_select').value;
+            window.location.href = '{{ url_for('source_data.source_data_list') }}?k=' + keyword + '&s=' + status;
+        }
+        function reSearchData(){
+            window.location.href = '{{ url_for('source_data.source_data_list') }}';
+        }
+    </script>
+</head>
+<body>
+    <div class="box">
+        <div class="box_header">
+            <div><h3>项目列表</h3></div>
+        </div>
+        <div class="box_header">
+            <div class="btn_box">
+                <button type="button" class="btn btn-success btn-large" onclick="addNewData()">添加</button>
+            </div>
+            <div class="search_box">
+                <select id="status_select" class="form-control" style="width: 100px;">
+                    <option value="-1" {% if status == -1 %}selected{% endif %}>所有状态</option>
+                    <option value="0" {% if status == 0 %}selected{% endif %}>新建</option>
+                    <option value="32" {% if status == 32 %}selected{% endif %}>分析完成</option>
+                    <option value="31" {% if status == 31 %}selected{% endif %}>采集完成</option>
+                    <option value="21" {% if status == 21 %}selected{% endif %}>采集中...</option>
+                    <option value="22" {% if status == 22 %}selected{% endif %}>分析中...</option>
+                    <option value="11" {% if status == 11 %}selected{% endif %}>采集失败</option>
+                    <option value="12" {% if status == 12 %}selected{% endif %}>分析失败</option>
+                </select>
+                <input type="text" id="search_input" class="form-control" placeholder="请输入查询关键字" value="{{ keyword }}">
+                <button type="button" class="btn btn-info btn-large" onclick="searchData()">查询</button>
+                <button type="button" class="btn btn-warning btn-large" onclick="reSearchData()">重置</button>
+            </div>
+        </div>
+        <div class="box-body">
+            <table class="table">
+                <thead>
+                    <tr>
+                        <th width="20%">项目编号</th>
+                        <th width="">项目名称</th>
+                        <th width="20%">标准版本</th>
+                        <th width="150px">状态</th>
+                        <th width="260px">操作</th>
+                    </tr>
+                </thead>
+                <tbody>
+                {% if source_data_list %}
+                    {% for source_data in source_data_list %}
+                        <tr>
+                            <td class="project_no">{{ source_data.project_no }}</td>
+                            <td class="editable project_name">
+                                <span class="show">{{ source_data.project_name if source_data.project_name else '-' }}</span>
+                                <span class="edit">
+                                    <input type="text" class="form-control" value="{{ source_data.project_name if source_data.project_name else '' }}">
+                                </span>
+                            </td>
+                            <td class="editable standard_version">
+                                <span class="show">
+                                    {% if source_data.standard_version == '1' %}
+                                    <span class="label label-warning">旧版</span>
+                                    {% elif source_data.standard_version == '2' %}
+                                    <span class="label label-info">新版</span>
+                                    {% endif %}
+                                </span>
+                                <span class="edit">
+                                    <select class="form-control" >
+                                        <option value="1" {% if source_data.standard_version == '1' %}selected{% endif %}>旧版</option>
+                                        <option value="2" {% if source_data.standard_version == '2' %}selected{% endif %}>新版</option>
+                                    </select>
+                                </span>
+                            </td>
+                            <td>
+                                <span class="status">
+                                    {% if source_data.status == 0 %}
+                                    <span class="label label-default">新建</span>
+                                    {% elif source_data.status == 21 %}
+                                    <span class="label label-warning">采集中...</span>
+                                    {% elif source_data.status == 31 %}
+                                    <span class="label label-info">采集完成</span>
+                                    {% elif source_data.status == 22 %}
+                                    <span class="label label-warning">分析中...</span>
+                                    {% elif source_data.status == 32 %}
+                                    <span class="label label-success">分析完成</span>
+                                    {% elif source_data.status == 11 %}
+                                    <span class="label label-danger">采集失败</span>
+                                    {% elif source_data.status == 12 %}
+                                    <span class="label label-danger">分析失败</span>
+                                    {% endif %}
+                                </span>
+                            </td>
+                            <td class="editable">
+                                <span class="show">
+                                    {% if source_data.status == 0 %}
+                                    <button class="btn btn-success" type="button" onclick="confirmCollectData('{{ source_data.project_no }}')">采集数据</button>
+                                    <button class="btn btn-info" onclick="toggleEditMode(this.parentNode.parentNode.parentNode)">编辑</button>
+                                    {% elif source_data.status == 31 %}
+                                    <button class="btn btn-success" type="button" onclick="confirmProcessData('{{ source_data.project_no }}')">分析数据</button>
+                                    {% elif source_data.status == 32 %}
+                                    <button class="btn btn-info" type="button" onclick="goTo('{{ url_for('source_data.source_item_list', project_no=source_data.project_no) }}')">详情</button>
+                                    {% elif source_data.status == 11 %}
+                                    <button class="btn btn-warning" type="button" onclick="confirmCollectData('{{ source_data.project_no }}')">重新采集</button>
+                                    {% elif source_data.status == 12 %}
+                                    <button class="btn btn-warning" type="button" onclick="confirmProcessData('{{ source_data.project_no }}')">重新分析</button>
+                                    {% endif %}
+                                    {% if source_data.status != 21  and source_data.status != 22 %}
+                                    <button class="btn btn-danger" onclick="confirmDataDelete('{{ source_data.project_no }}')">删除</button>
+                                    {% endif %}
+                                </span>
+                                <span class="edit">
+                                    <button class="btn btn-success" onclick="saveDataEdit(this.parentNode.parentNode.parentNode,'{{ source_data.project_no }}')">确定</button>
+                                    <button class="btn btn-warning" onclick="cancelChanges(this.parentNode.parentNode.parentNode)">取消</button>
+                                </span>
+                            </td>
+                        </tr>
+                    {% endfor %}
+                {% else %}
+                    <tr>
+                        <td colspan="15">没有找到项目数据</td>
+                    </tr>
+                {% endif %}
+                </tbody>
+            </table>
+             <div class="pagination">
+            <div class="pagination-info">
+                {% set total_pages = (total_count|int + per_page|int - 1)//per_page %}
+               <span class="page">
+                    总共 {{ total_count }} 条数据,每页 {{ per_page }} 条,当前第 {{ page }} 页 / 共 {{ total_pages }} 页
+               </span>
+            </div>
+            <div class="pagination-links">
+                {% if page > 1 %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_data_list', k=keyword, s=status, p=1, pp=per_page) }}">首页</a>
+                {% endif %}
+                {% if page > 1 %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_data_list', k=keyword, p=page-1, pp=per_page) }}">上一页</a>
+                {% endif %}
+                {% set start_page = [1, page - 2]|max %}
+                {% set end_page = [total_pages, page + 2]|min %}
+                {% if start_page > 1 %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_data_list', k=keyword, s=status, p=1, pp=per_page) }}">1</a>
+                    {% if start_page > 2 %}
+                        <span class="page">...</span>
+                    {% endif %}
+                {% endif %}
+                {% for p in range(start_page, end_page + 1) %}
+                    {% if p == page %}
+                    <a class="page page-link active" href="{{ url_for('source_data.source_data_list', k=keyword, p=p, pp=per_page) }}" >{{ p }}</a>
+                    {% else %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_data_list', k=keyword, p=p, pp=per_page) }}" >{{ p }}</a>
+                    {% endif %}
+                {% endfor %}
+                {% if end_page < total_pages %}
+                    {% if end_page < total_pages - 1 %}
+                        <span class="page">...</span>
+                    {% endif %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_data_list', k=keyword, p=total_pages, pp=per_page) }}">{{ total_pages }}</a>
+                {% endif %}
+                {% if page < total_pages %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_data_list', k=keyword, p=page+1, pp=per_page) }}">下一页</a>
+                {% endif %}
+                {% if page < total_pages %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_data_list', k=keyword, p=total_pages, pp=per_page) }}">末页</a>
+                {% endif %}
+            </div>
+        </div>
+        </div>
+    </div>
+</body>
+</html>

+ 149 - 0
SourceCode/DataMiddleware/app/ui/templates/source_data/source_item_list.html

@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>设备列表</title>
+    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
+    <style>
+        .box_header .project_info{
+            display: flex;
+            justify-content: left;
+            width: 60%;
+        }
+
+    </style>
+    <script src="{{ url_for('static', filename='utils.js') }}"></script>
+    <script src="{{ url_for('static', filename='source_item_list.js') }}"></script>
+    <script>
+         function searchData(){
+            var keyword = document.getElementById('search_input').value;
+            window.location.href = '{{ url_for('source_data.source_item_list', project_no=source_data.project_no) }}?k=' + keyword;
+        }
+        function reSearchData(){
+            window.location.href = '{{ url_for('source_data.source_item_list', project_no=source_data.project_no) }}';
+        }
+    </script>
+</head>
+<body>
+<div class="box">
+    <div class="box_header">
+        <div class="project_info">
+            <button class="btn btn-info btn-large" onclick="goTo('{{ url_for('source_data.source_data_list') }}')">返回</button>
+            <h3 style="margin-left:20px">设备列表</h3>
+            <span style="margin:0 20px">|</span>
+            <dl>
+                <dt>项目编号:</dt>
+                <dd>{{ source_data.project_no }}</dd>
+            </dl>
+            <dl>
+                <dt>项目名称:</dt>
+                <dd>{{ source_data.project_name }}</dd>
+            </dl>
+            <dl>
+                <dt>标准版本:</dt>
+                <dd>{{ source_data.standard_version }}</dd>
+            </dl>
+        </div>
+    </div>
+    <div class="box_header">
+        <div class="btn_box">
+            <button class="btn btn-success btn-large" onclick="addNewItem('{{ source_data.project_no}}')">添加</button>
+        </div>
+        <div class="search_box">
+            <input type="text" id="search_input" class="form-control" placeholder="请输入查询关键字" value="{{ keyword }}">
+            <button type="button" class="btn btn-info btn-large" onclick="searchData()">查询</button>
+            <button type="button" class="btn btn-warning btn-large" onclick="reSearchData()">重置</button>
+        </div>
+    </div>
+
+    <div class="box_body">
+        <table class="table">
+            <thead>
+                <tr>
+                    <th width="25%">名称</th>
+                    <th>规格型号</th>
+                    <th width="8%">单位</th>
+                    <th width="18%">标准编号</th>
+                    <th width="180px">操作</th>
+                </tr>
+            </thead>
+            <tbody>
+                {% if source_items %}
+                    {% for item in source_items %}
+                        <tr>
+                            <td class="name">{{ item.device_name }}</td>
+                            <td class="model">{{ item.device_model }}</td>
+                            <td class="unit">{{ item.device_unit if item.device_unit else '-' }}</td>
+                            <td class="editable standard_no">
+                                <span class="show">{{ item.standard_no if item.standard_no else '-' }}</span>
+                                <span class="edit">
+                                    <input type="text" class="form-control" value="{{ item.standard_no if item.standard_no else '' }}">
+                                </span>
+                            </td>
+                            <td class="editable tool">
+                                <span class="show">
+                                    <button class="btn btn-info" onclick="toggleEditMode(this.parentNode.parentNode.parentNode)">编辑</button>
+                                    <button class="btn btn-danger" onclick="confirmItemDelete('{{ item.id }}')">删除</button>
+                                </span>
+                                <span class="edit">
+                                    <button class="btn btn-success" onclick="saveItemEdit(this.parentNode.parentNode.parentNode, '{{ item.id }}')">保存</button>
+                                    <button class="btn btn-warning" onclick="cancelChanges(this.parentNode.parentNode.parentNode)">取消</button>
+                                </span>
+                            </td>
+                        </tr>
+                    {% endfor %}
+                {% else %}
+                    <tr>
+                        <td colspan="15">没有找到设备数据</td>
+                    </tr>
+                {% endif %}
+            </tbody>
+        </table>
+        <div class="pagination">
+            <div class="pagination-info">
+                {% set total_pages = (total_count|int + per_page|int - 1)//per_page %}
+               <span class="page">
+                    总共 {{ total_count }} 条数据,每页 {{ per_page }} 条,当前第 {{ page }} 页 / 共 {{ total_pages }} 页
+               </span>
+            </div>
+            <div class="pagination-links">
+                {% if page > 1 %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_item_list', project_no=source_data.project_no, k=keyword, p=1, pp=per_page) }}">首页</a>
+                {% endif %}
+                {% if page > 1 %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_item_list', project_no=source_data.project_no, k=keyword, p=page-1, pp=per_page) }}">上一页</a>
+                {% endif %}
+                {% set start_page = [1, page - 2]|max %}
+                {% set end_page = [total_pages, page + 2]|min %}
+                {% if start_page > 1 %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_item_list', project_no=source_data.project_no, k=keyword, p=1, pp=per_page) }}">1</a>
+                    {% if start_page > 2 %}
+                        <span class="page">...</span>
+                    {% endif %}
+                {% endif %}
+                {% for p in range(start_page, end_page + 1) %}
+                    {% if p == page %}
+                    <a class="page page-link active" href="{{ url_for('source_data.source_item_list', project_no=source_data.project_no, k=keyword, p=p, pp=per_page) }}" >{{ p }}</a>
+                    {% else %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_item_list', project_no=source_data.project_no, k=keyword, p=p, pp=per_page) }}" >{{ p }}</a>
+                    {% endif %}
+                {% endfor %}
+                {% if end_page < total_pages %}
+                    {% if end_page < total_pages - 1 %}
+                        <span class="page">...</span>
+                    {% endif %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_item_list', project_no=source_data.project_no, k=keyword, p=total_pages, pp=per_page) }}">{{ total_pages }}</a>
+                {% endif %}
+                {% if page < total_pages %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_item_list', project_no=source_data.project_no, k=keyword, p=page+1, pp=per_page) }}">下一页</a>
+                {% endif %}
+                {% if page < total_pages %}
+                    <a class="page page-link" href="{{ url_for('source_data.source_item_list', project_no=source_data.project_no, k=keyword, p=total_pages, pp=per_page) }}">末页</a>
+                {% endif %}
+            </div>
+        </div>
+    </div>
+</div>
+</body>
+</html>

+ 171 - 0
SourceCode/DataMiddleware/app/utils/__init__.py

@@ -0,0 +1,171 @@
+"""
+utils/__init__.py
+
+该模块初始化文件,导入了多个辅助工具类,并定义了一系列便捷函数,用于日志记录、配置管理、文件操作、字符串处理和邮件发送等功能。
+"""
+import json
+
+from utils.ai_helper import AiHelper
+from utils.config_helper import ConfigHelper
+from utils.email_helper import EmailHelper
+from utils.file_helper import FileHelper
+from utils.logger_helper import LoggerHelper
+from utils.string_helper import StringHelper
+
+
+def get_logger():
+    """
+    获取日志记录器实例。
+
+    该函数通过调用LoggerHelper类的静态方法get_logger()来获取一个日志记录器实例。
+    主要用于需要记录日志的位置,通过该函数获取日志记录器实例,然后进行日志记录。
+    这样做可以保持日志记录的一致性和集中管理。
+
+    :return: Logger实例,用于记录日志。
+    """
+    return LoggerHelper.get_logger()
+
+
+def clean_log_file(day: int):
+    """
+    清理指定天数之前的日志文件。
+
+    :param day: 整数,表示清理多少天前的日志文件。
+    """
+    LoggerHelper.clean_log_file(day)
+
+
+def get_config():
+    """
+    获取配置管理器实例。
+
+    该函数返回一个ConfigHelper实例,用于读取和管理应用程序的配置信息。
+
+    :return: ConfigHelper实例,用于配置管理。
+    """
+    return ConfigHelper()
+
+
+def reload_config():
+    """
+    重新加载配置文件。
+
+    该函数会重新加载配置文件中的内容,适用于配置文件发生更改后需要重新加载的情况。
+    """
+    get_config().load_config()
+
+
+def get_config_value(key: str, default: str = None):
+    """
+    获取配置项的值。
+
+    :param key: 字符串,配置项的键。
+    :param default: 字符串,默认值(可选)。
+    :return: 配置项的值,如果不存在则返回默认值。
+    """
+    return get_config().get(key, default)
+
+
+def get_config_int(key: str, default: int = None):
+    """
+    获取配置项的整数值。
+
+    :param key: 字符串,配置项的键。
+    :param default: 整数,默认值(可选)。
+    :return: 配置项的整数值,如果不存在则返回默认值。
+    """
+    return get_config().get_int(key, default)
+
+
+def get_config_bool(key: str):
+    """
+    获取配置项的布尔值。
+
+    :param key: 字符串,配置项的键。
+    :return: 配置项的布尔值。
+    """
+    return get_config().get_bool(key)
+
+
+def download_remote_file(file_url: str, file_name: str) -> str:
+    """
+    下载远程文件并保存到本地。
+
+    :param file_url: 字符串,远程文件的URL。
+    :param file_name: 字符串,保存到本地的文件名。
+    :return: 字符串,下载后的文件路径。
+    """
+    return FileHelper().download_remote_file(file_url, file_name)
+
+
+def clean_attach_file(day: int):
+    """
+    清理指定天数之前的附件文件。
+
+    :param day: 整数,表示清理多少天前的附件文件。
+    """
+    FileHelper().clean_attach_file(day)
+
+
+def save_report_excel(data, file_name: str = None) -> str:
+    """
+    保存报表数据到Excel文件。
+
+    :param data: 列表,报表数据。
+    :param file_name: 字符串,保存的文件名(可选)。
+    :return: 字符串,保存的文件路径。
+    """
+    return FileHelper().save_report_excel(data, file_name)
+
+
+def clean_report_file(day: int):
+    """
+    清理指定天数之前的报表文件。
+
+    :param day: 整数,表示清理多少天前的报表文件。
+    """
+    FileHelper().clean_report_file(day)
+
+
+def to_array(s: str, split: str = ",") -> list[str]:
+    """
+    将字符串按指定分隔符拆分为数组。
+
+    :param s: 字符串,待拆分的字符串。
+    :param split: 字符串,分隔符。
+    :return: 列表,拆分后的数组。
+    """
+    return StringHelper.to_array(s, split)
+
+
+def call_openai(system_prompt: str, user_prompt: str) -> json:
+    """
+    调用OpenAI API进行对话。
+
+    :param system_prompt: 字符串,系统提示信息。
+    :param user_prompt: 字符串,用户输入的提示信息。
+    :return: JSON对象,API返回的结果。
+    """
+    return AiHelper().call_openai(system_prompt, user_prompt)
+
+
+def send_email(
+    to_addr: str,
+    subject: str,
+    body: str,
+    body_is_html: bool = True,
+    attachment_paths: str = None,
+) -> bool:
+    """
+    发送电子邮件。
+
+    :param to_addr: 字符串,收件人地址。
+    :param subject: 字符串,邮件主题。
+    :param body: 字符串,邮件正文。
+    :param body_is_html: 布尔值,是否为HTML格式,默认为True。
+    :param attachment_paths: 字符串,附件路径(可选)。
+    :return: 布尔值,表示邮件是否发送成功。
+    """
+    return EmailHelper().send_email(
+        to_addr, subject, body, body_is_html, attachment_paths
+    )

+ 120 - 0
SourceCode/DataMiddleware/app/utils/ai_helper.py

@@ -0,0 +1,120 @@
+import json
+import re
+
+import utils
+from openai import OpenAI
+
+
+class AiHelper:
+
+    _ai_api_key = None
+    _ai_api_url = None
+    _ai_max_tokens = 150
+
+    def __init__(self, api_url: str=None, api_key: str=None, api_model: str=None):
+        self._ai_api_url = api_url if api_url else utils.get_config_value("ai.url")
+        self._ai_api_key = api_key if api_key else utils.get_config_value("ai.key")
+        self._api_model = api_model if api_model else utils.get_config_value("ai.model")
+        max_tokens = utils.get_config_value("ai.max_tokens")
+        if max_tokens:
+            self._ai_max_tokens = int(max_tokens)
+
+    def call_openai(self, system_prompt: str, user_prompt: str,api_url: str=None,api_key: str=None,api_model: str=None) -> json:
+        if api_url:
+            self._ai_api_url = api_url
+        if api_key:
+            self._ai_api_key = api_key
+        if api_model:
+            self._api_model = api_model
+        if self._ai_api_key is None:
+            raise Exception("AI API key 没有配置")
+        if self._ai_api_url is None:
+            raise Exception("AI API url 没有配置")
+        if self._api_model is None:
+            raise Exception("AI API model 没有配置")
+
+        utils.get_logger().info(f"调用AI API ==> Url:{self._ai_api_url},Model:{self._api_model}")
+
+        client = OpenAI(api_key=self._ai_api_key, base_url=self._ai_api_url)
+        completion = client.chat.completions.create(
+            model=self._api_model,
+            messages=[
+                {
+                    "role": "system",
+                    "content": system_prompt,
+                },
+                {
+                    "role": "user",
+                    "content": user_prompt,
+                },
+            ],
+            stream=False,
+            temperature=0.7,
+            response_format={"type": "json_object"},
+            # max_tokens=self._ai_max_tokens,
+        )
+        try:
+            response = completion.model_dump_json()
+            result = {}
+            response_json = json.loads(response)
+            res_str = self._extract_message_content(response_json)
+            result_data = self._parse_response(res_str, True)
+            if result_data:
+                result["data"] = result_data
+                usage = response_json["usage"]
+                result["completion_tokens"] = usage.get("completion_tokens", 0)
+                result["prompt_tokens"] = usage.get("prompt_tokens", 0)
+                result["total_tokens"] = usage.get("total_tokens", 0)
+                utils.get_logger().info(f"AI Process JSON: {result}")
+            else:
+                utils.get_logger().info(f"AI Response: {response}")
+            return result
+        except Exception as e:
+            raise Exception(f"解析 AI 响应错误: {e}")
+
+    @staticmethod
+    def _extract_message_content(response_json: dict) -> str:
+        utils.get_logger().info(f"AI Response JSON: {response_json}")
+        if "choices" in response_json and len(response_json["choices"]) > 0:
+            choice = response_json["choices"][0]
+            message_content = choice.get("message", {}).get("content", "")
+        elif "message" in response_json:
+            message_content = response_json["message"].get("content", "")
+        else:
+            raise Exception("AI 响应中未找到有效的 choices 或 message 数据")
+
+        # 移除多余的 ```json 和 ```
+        if message_content.startswith("```json") and message_content.endswith(
+                "```"):
+            message_content = message_content[6:-3]
+
+        # 去除开头的 'n' 字符
+        if message_content.startswith("n"):
+            message_content = message_content[1:]
+        # 移除无效的转义字符和时间戳前缀
+        message_content = re.sub(r"\\[0-9]{2}", "",
+                                 message_content)  # 移除 \32 等无效转义字符
+        message_content = re.sub(r"\d{4}-\d{2}-\dT\d{2}:\d{2}:\d{2}\.\d+Z", "",
+                                 message_content)  # 移除时间戳
+        message_content = message_content.strip()  # 去除首尾空白字符
+
+        # 替换所有的反斜杠
+        message_content = message_content.replace("\\", "")
+
+        return message_content
+
+    def _parse_response(self, response: str, first=True) -> json:
+        # utils.get_logger().info(f"AI Response JSON STR: {response}")
+        try:
+            data = json.loads(response)
+            return data
+
+        except json.JSONDecodeError as e:
+            if first:
+                utils.get_logger().error(f"JSON 解析错误,去除部分特殊字符重新解析一次: {e}")
+                # 替换中文引号为空
+                message_content = re.sub(r"[“”]", "", response)  # 替换双引号
+                message_content = re.sub(r"[‘’]", "", message_content)  # 替换单引号
+                return self._parse_response(message_content, False)
+            else:
+                raise Exception(f"解析 AI 响应错误: {response} {e}")

+ 79 - 0
SourceCode/DataMiddleware/app/utils/config_helper.py

@@ -0,0 +1,79 @@
+import os
+
+import yaml
+
+
+class ConfigHelper:
+    _instance = None
+
+    # 默认配置文件路径
+    default_config_path = os.path.join(os.path.dirname(__file__), "..", "config.yml")
+
+    # 类变量存储加载的配置
+    _config = None
+    _path = None
+
+    def __new__(cls, *args, **kwargs):
+        if not cls._instance:
+            cls._instance = super(ConfigHelper, cls).__new__(cls)
+        return cls._instance
+
+    def load_config(self, path=None):
+        if self._config is None:
+            if not path:
+                # print(f"使用默认配置文件:{self.default_config_path}")
+                self._path = self.default_config_path
+            else:
+                self._path = path
+            if not os.path.exists(self._path):
+                raise FileNotFoundError(f"没有找到文件或目录:'{self._path}'")
+        with open(self._path, "r", encoding="utf-8") as file:
+            self._config = yaml.safe_load(file)
+        # 合并环境变量配置
+        self._merge_env_vars()
+        # print(f"加载的配置文件内容:{self._config}")
+        return self._config
+
+    def _merge_env_vars(self, env_prefix="APP_"):  # 环境变量前缀为 APP_
+        for key, value in os.environ.items():
+            if key.startswith(env_prefix):
+                config_key = key[len(env_prefix) :].lower()
+                self._set_nested_key(self._config, config_key.split("__"), value)
+
+    def _set_nested_key(self, config, keys, value):
+        if len(keys) > 1:
+            if keys[0] not in config or not isinstance(config[keys[0]], dict):
+                config[keys[0]] = {}
+            self._set_nested_key(config[keys[0]], keys[1:], value)
+        else:
+            config[keys[0]] = value
+
+    def get(self, key: str, default: str = None):
+        if self._config is None:
+            self.load_config(self._path)
+        keys = key.split(".")
+        config = self._config
+        for k in keys:
+            if isinstance(config, dict) and k in config:
+                config = config[k]
+            else:
+                return default
+        return config
+
+    def get_bool(self, key: str) -> bool:
+        val = str(self.get(key, "0"))
+        return True if val.lower() == "true" or val == "1" else False
+
+    def get_int(self, key: str, default: int = 0) -> int:
+        val = self.get(key)
+        if not val:
+            return default
+        try:
+            return int(val)
+        except ValueError:
+            return default
+
+    def get_all(self):
+        if self._config is None:
+            self.load_config(self._path)
+        return self._config

+ 108 - 0
SourceCode/DataMiddleware/app/utils/email_helper.py

@@ -0,0 +1,108 @@
+import mimetypes
+import os
+import smtplib
+import utils
+from email import encoders
+from email.header import Header
+from email.mime.base import MIMEBase
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+
+class EmailHelper:
+
+    def __init__(self):
+        self.smtp_server = utils.get_config_value("email.smtp_server")
+        self.port = utils.get_config_value("email.smtp_port")
+        self.username = utils.get_config_value("email.smtp_user")
+        self.password = utils.get_config_value("email.smtp_password")
+        self.from_email = utils.get_config_value("email.from_email")
+        # print(
+        #     f"server:{self.smtp_server},port:{self.port},username:{self.username},password:{self.password},from_email:{self.from_email}"
+        # )
+
+    def send_email(
+        self,
+        to_addr: str,
+        subject: str,
+        body: str,
+        body_is_html: bool = True,
+        attachment_paths: str = None,
+    ):
+        msg = MIMEMultipart()
+        msg["From"] = self.from_email
+        msg["To"] = ", ".join(to_addr.split(","))
+        msg["Subject"] = subject
+
+        # 根据 body_is_html 参数设置 MIMEText 类型
+        if body_is_html:
+            msg.attach(MIMEText(body, "html", "utf-8"))
+        else:
+            msg.attach(MIMEText(body, "plain", "utf-8"))
+
+        if attachment_paths:
+            attachment_arr = utils.to_array(attachment_paths)
+            for attachment_path in attachment_arr:
+                self._attach_file(msg, attachment_path)
+
+        try:
+            with smtplib.SMTP_SSL(
+                self.smtp_server, port=self.port, timeout=10
+            ) as server:
+                # server.starttls()
+                server.login(self.username, self.password)
+                # 将 to_addr 字符串通过 split(',') 分割成列表,传递给 sendmail
+                server.sendmail(self.from_email, to_addr.split(","), msg.as_string())
+            utils.get_logger().info(f"邮件发送成功:{to_addr}")
+            return True
+        except smtplib.SMTPAuthenticationError:
+            utils.get_logger().error("SMTP 认证失败")
+        except smtplib.SMTPServerDisconnected:
+            utils.get_logger().error("SMTP 服务器断开连接")
+        except smtplib.SMTPException as e:
+            utils.get_logger().error(f"SMTP 异常: {e}")
+        except Exception as e:
+            utils.get_logger().error(f"邮件发送失败:{to_addr} {e}")
+            return False
+
+    @staticmethod
+    def _attach_file(msg: MIMEMultipart, attachment_path: str):
+        if not os.path.isfile(attachment_path):
+            utils.get_logger().error(f"文件 {attachment_path} 不存在。")
+            return
+
+        file_size = os.path.getsize(attachment_path)
+        max_size = 1024 * 8192  # 8MB
+
+        if file_size > max_size:
+            utils.get_logger().error(
+                f"文件 {attachment_path} 大小超过限制 ({file_size} bytes > {max_size} bytes),不添加附件。"
+            )
+            return
+
+        # 根据文件名后缀获取 MIME 类型
+        content_type, _ = mimetypes.guess_type(attachment_path)
+        if content_type is None:
+            content_type = "application/octet-stream"  # 默认类型
+        main_type, sub_type = content_type.split("/", 1)
+
+        with open(attachment_path, "rb") as attachment:
+            # part = MIMEBase('application', 'octet-stream')
+            part = MIMEBase(main_type, sub_type)
+            part.set_payload(attachment.read(max_size))
+            # 获取文件名并去除第一个 @ 字符前面的部分
+            name = os.path.basename(attachment_path)
+            at_index = name.find("@")
+            if at_index != -1:
+                name = name[at_index + 1 :]
+
+            # 使用 RFC 2047 编码文件名
+            encoded_name = Header(name, "utf-8").encode()
+            part.add_header(
+                "Content-Disposition", f"attachment; filename= {encoded_name}"
+            )
+            # part.add_header("Content-ID", "<0>")
+            # part.add_header("X-Attachment-Id", "0")
+            encoders.encode_base64(part)
+            msg.attach(part)
+            utils.get_logger().info(f"添加附件 {name} {attachment_path} 到邮件中。")

+ 169 - 0
SourceCode/DataMiddleware/app/utils/file_helper.py

@@ -0,0 +1,169 @@
+import os
+import shutil
+import utils
+from datetime import datetime, timedelta
+from urllib.parse import urlparse
+
+import pandas as pd
+import requests
+
+
+class FileHelper:
+
+    DEFAULT_ATTACH_PATH = "./temp_files/attaches/"
+    DEFAULT_REPORT_PATH = "./temp_files/reoport/"
+
+    def __init__(self):
+        attach_path = utils.get_config_value(
+            "save.attach_file_path", self.DEFAULT_ATTACH_PATH
+        )
+        attach_path = attach_path.replace("\\", "/")
+        attach_path = attach_path.replace("//", "/")
+        self._attach_file_path = attach_path
+        report_path = utils.get_config_value(
+            "save.report_file_path", self.DEFAULT_REPORT_PATH
+        )
+        report_path = report_path.replace("\\", "/")
+        report_path = report_path.replace("//", "/")
+        self._report_file_path = report_path
+
+    def download_remote_file(self, file_url: str, file_name: str) -> str | None:
+        utils.get_logger().info(f"下载远程文件: {file_url}  文件名:{file_name}")
+        current_timestamp = datetime.now().strftime("%H%M%S%f")[:-3]  # 取前三位毫秒
+        file_name = f"{current_timestamp}@{file_name}"
+        file_path = os.path.join(
+            self._attach_file_path, f'{datetime.now().strftime("%Y-%m-%d")}'
+        )
+        if not os.path.exists(file_path):
+            os.makedirs(file_path)
+        path = os.path.join(file_path, file_name)
+        path = path.replace("\\", "/")
+        path = path.replace("//", "/")
+        # 10个不同的 User-Agent
+        user_agents = [
+            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
+            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Safari/605.1.15",
+            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
+            "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0",
+            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/91.0.4472.124 Safari/605.1.15",
+            "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0",
+            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/91.0.864.59 Safari/537.36",
+            "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1",
+            "Mozilla/5.0 (iPad; CPU OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1",
+            "Mozilla/5.0 (Linux; Android 11; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Mobile Safari/537.36",
+        ]
+
+        # 根据文件名长度选择一个 User-Agent
+        ua_index = len(file_name) % len(user_agents)
+        # 解析 file_url 获取 Referer
+        parsed_url = urlparse(file_url)
+        referer = f"{parsed_url.scheme}://{parsed_url.netloc}/".replace(
+            "//download.", "//www."
+        )
+        headers = {
+            "User-Agent": user_agents[ua_index],
+            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
+            "Accept-Encoding": "gzip, deflate, br",
+            "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7",
+            "Referer": referer,
+        }
+
+        try:
+            response = requests.get(file_url, headers=headers, allow_redirects=True)
+            response.raise_for_status()
+            with open(path, "wb") as f:
+                f.write(response.content)
+            utils.get_logger().info(f"文件下载成功: {file_name}")
+            return path
+        except requests.exceptions.HTTPError as http_err:
+            utils.get_logger().error(f"HTTP 错误: {http_err}")
+        except Exception as e:
+            utils.get_logger().error(f"文件下载失败: {file_name}。Exception: {e}")
+            return None
+
+    def clean_attach_file(self, day: int) -> None:
+        try:
+            current_time = datetime.now()
+            cutoff_time = current_time - timedelta(days=day)
+            for root, dirs, _ in os.walk(self._attach_file_path):
+                for dir_name in dirs:
+                    path = os.path.join(root, dir_name)
+                    dir_path = (
+                        str(path).replace(self._attach_file_path, "").replace("\\", "/")
+                    )
+                    if dir_path.count("/") > 0:
+                        continue
+                    try:
+                        dir_date = datetime.strptime(dir_path, "%Y-%m-%d")
+                        if dir_date < cutoff_time:
+                            try:
+                                shutil.rmtree(path)
+                                utils.get_logger().info(
+                                    f"  删除目录及其内容: {dir_path}"
+                                )
+                            except PermissionError:
+                                utils.get_logger().error(
+                                    f"  权限错误,无法删除目录: {dir_path}"
+                                )
+                            except Exception as e:
+                                utils.get_logger().error(
+                                    f"  删除目录失败: {dir_path}。Exception: {e}"
+                                )
+                    except ValueError:
+                        # 如果目录名称不符合 %Y-%m/%d 格式,跳过
+                        continue
+        except Exception as e:
+            utils.get_logger().error(f"attach 文件清理失败。Exception: {e}")
+
+    def save_report_excel(self, data, file_name: str = None) -> str:
+        try:
+            df = pd.DataFrame(data)
+            file_path = os.path.join(
+                self._report_file_path, f'{datetime.now().strftime("%Y-%m-%d")}'
+            )
+            if not os.path.exists(file_path):
+                os.makedirs(file_path)
+            file_name = f"{file_name}_{datetime.now().strftime('%H%M%S')}.xlsx"
+            path = os.path.join(file_path, file_name)
+            path = path.replace("\\", "/")
+            path = path.replace("//", "/")
+            df.to_excel(path, index=False)
+            utils.get_logger().debug(f"Report报存成功: {file_name}")
+            return path
+        except Exception as e:
+            utils.get_logger().error(f"保存 Report Excel 文件失败。Exception: {e}")
+            return ""
+
+    def clean_report_file(self, day: int) -> None:
+        try:
+            current_time = datetime.now()
+            cutoff_time = current_time - timedelta(days=day)
+            for root, dirs, _ in os.walk(self._report_file_path):
+                for dir_name in dirs:
+                    path = os.path.join(root, dir_name)
+                    dir_path = (
+                        str(path).replace(self._report_file_path, "").replace("\\", "/")
+                    )
+                    if dir_path.count("/") > 0:
+                        continue
+                    try:
+                        dir_date = datetime.strptime(dir_path, "%Y-%m-%d")
+                        if dir_date < cutoff_time:
+                            try:
+                                shutil.rmtree(path)
+                                utils.get_logger().info(
+                                    f"  Report 删除目录及其内容: {dir_path}"
+                                )
+                            except PermissionError:
+                                utils.get_logger().error(
+                                    f"  Report 权限错误,无法删除目录: {dir_path}"
+                                )
+                            except Exception as e:
+                                utils.get_logger().error(
+                                    f"  Report 删除目录失败: {dir_path}。Exception: {e}"
+                                )
+                    except ValueError:
+                        # 如果目录名称不符合 %Y-%m/%d 格式,跳过
+                        continue
+        except Exception as e:
+            utils.get_logger().error(f"Report 文件清理失败。Exception: {e}")

+ 113 - 0
SourceCode/DataMiddleware/app/utils/logger_helper.py

@@ -0,0 +1,113 @@
+import logging
+import os
+from datetime import datetime
+from logging.handlers import TimedRotatingFileHandler
+
+from utils.config_helper import ConfigHelper
+
+
+class LoggerHelper:
+    """
+    日志辅助类,用于创建和提供日志记录器实例
+    该类实现了单例模式,确保在整个应用程序中只有一个日志记录器实例被创建和使用
+    """
+
+    _instance = None
+    config = ConfigHelper()
+    _log_file_name = f"{config.get("logger.file_name", "log")}.log"
+    _log_file_path = config.get("logger.file_path", "./logs")
+    _log_level_string = config.get("logger.level", "INFO")
+
+    def __new__(cls, *args, **kwargs):
+        """
+        实现单例模式,确保日志记录器仅被创建一次
+        如果尚未创建实例,则创建并初始化日志记录器
+        """
+        if not cls._instance:
+            cls._instance = super(LoggerHelper, cls).__new__(cls, *args, **kwargs)
+            try:
+                cls._instance._initialize_logger()
+            except Exception as e:
+                raise Exception(f"配置logger出错: {e}")
+        return cls._instance
+
+    @property
+    def logger(self):
+        return self._logger
+
+    def _initialize_logger(self):
+        """
+        初始化日志记录器,包括设置日志级别、创建处理器和格式化器,并将它们组合起来
+        """
+        log_level = self._get_log_level()
+        self._logger = logging.getLogger("app_logger")
+        self._logger.setLevel(log_level)
+
+        if not os.path.exists(self._log_file_path):
+            os.makedirs(self._log_file_path)
+
+        # 创建按日期分割的文件处理器
+        file_handler = TimedRotatingFileHandler(
+            os.path.join(self._log_file_path, self._log_file_name),
+            when="midnight",
+            interval=1,
+            backupCount=7,
+            encoding="utf-8",
+        )
+        file_handler.setLevel(log_level)
+
+        # 创建控制台处理器
+        console_handler = logging.StreamHandler()
+        console_handler.setLevel(logging.DEBUG)
+
+        # 创建格式化器
+        formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
+
+        # 将格式化器添加到处理器
+        file_handler.setFormatter(formatter)
+        console_handler.setFormatter(formatter)
+
+        # 将处理器添加到日志记录器
+        self._logger.addHandler(file_handler)
+        self._logger.addHandler(console_handler)
+
+    def _get_log_level(self):
+        try:
+            # 尝试将字符串转换为 logging 模块中的日志级别常量
+            log_level = getattr(logging, self._log_level_string.upper())
+            if not isinstance(log_level, int):
+                raise ValueError
+            return log_level
+        except (AttributeError, ValueError):
+            raise ValueError(
+                f"配置logger出错: Unknown level: '{self._log_level_string}'"
+            )
+
+    @classmethod
+    def get_logger(cls):
+        """
+        提供初始化后的日志记录器实例
+        :return: 初始化后的日志记录器实例
+        """
+        if not cls._instance:
+            cls._instance = cls()
+        return cls._instance._logger
+
+    @classmethod
+    def clean_log_file(cls, day: int):
+        if not os.path.exists(cls._log_file_path):
+            return
+        for filename in os.listdir(cls._log_file_path):
+            if filename != cls._log_file_name and filename.startswith(
+                cls._log_file_name
+            ):
+                try:
+                    file_path = os.path.join(cls._log_file_path, filename)
+                    file_time = datetime.strptime(
+                        filename.replace(f"{cls._log_file_name}.", ""), "%Y-%m-%d"
+                    )
+                    if (datetime.now() - file_time).days > day:
+                        os.remove(file_path)
+                        cls.get_logger().info(f"  删除日志文件: {file_path}")
+                except Exception as e:
+                    cls.get_logger().error(f"删除日志文件出错: {filename} {e}")

+ 117 - 0
SourceCode/DataMiddleware/app/utils/mysql_helper.py

@@ -0,0 +1,117 @@
+import pymysql
+import utils
+from pymysql.cursors import DictCursor
+
+
+class MySQLHelper:
+
+    def __init__(self):
+        try:
+            self.host = utils.get_config_value("mysql.host")
+            self.user = utils.get_config_value("mysql.user")
+            self.password = utils.get_config_value("mysql.password")
+            self.db = utils.get_config_value("mysql.db")
+            self.port = int(utils.get_config_value("mysql.port"))
+            self.charset = utils.get_config_value("mysql.charset")
+            self.connection = None
+        except Exception as e:
+            utils.get_logger().error(f"加载数据库配置文件失败: {e}")
+
+    def connect(self):
+        try:
+            self.connection = pymysql.connect(
+                host=self.host,
+                user=self.user,
+                password=self.password,
+                db=self.db,
+                port=self.port,
+                charset=self.charset,
+                cursorclass=DictCursor,
+            )
+            # utils.get_logger().info(f"成功连接到数据库:{self.db}。")
+        except pymysql.MySQLError as e:
+            utils.get_logger().error(
+                f"数据库连接失败: {self.host}:{self.port} {self.db}"
+            )
+            self.connection = None  # 确保连接失败时设置为 None
+            raise Exception(f"连接数据库失败: {e}")
+
+    def disconnect(self):
+        if self.connection and self.connection.open:
+            self.connection.close()
+            # utils.get_logger().info("数据库连接已关闭。")
+
+    def execute_query(self, query, params=None):
+        try:
+            with self.connection.cursor() as cursor:
+                cursor.execute(query, params)
+                result = cursor.fetchall()
+                return result
+        except pymysql.MySQLError as e:
+            utils.get_logger().error(f"执行查询时出错:{e}")
+            return None
+
+    def execute_non_query(self, query, params=None):
+        if isinstance(params, list) and all(isinstance(p, tuple) for p in params):
+            self.execute_many(query, params)
+        elif isinstance(params, tuple):
+            self.execute(query, params)
+        else:
+            self.execute(query, (params,))
+
+    def execute(self, query, params=None):
+        try:
+            with self.connection.cursor() as cursor:
+                cursor.execute(query, params)
+                self.connection.commit()
+        except pymysql.MySQLError as e:
+            utils.get_logger().error(f"执行非查询时出错:{e}")
+            self.connection.rollback()
+
+    def execute_many(self, query, params: list):
+        if isinstance(params, list) and all(isinstance(p, tuple) for p in params):
+            try:
+                with self.connection.cursor() as cursor:
+                    cursor.executemany(query, params)
+                    self.connection.commit()
+            except pymysql.MySQLError as e:
+                utils.get_logger().error(f"执行非查询时出错:{e}")
+                self.connection.rollback()
+        else:
+            raise ValueError("参数必须是元组列表")
+
+    def fetch_one(self, query, params=None):
+        try:
+            with self.connection.cursor() as cursor:
+                cursor.execute(query, params)
+                result = cursor.fetchone()
+                return result
+        except pymysql.MySQLError as e:
+            utils.get_logger().error(f"获取一条记录时出错:{e}")
+            return None
+
+    def __enter__(self):
+        """
+        当进入上下文时自动调用此方法。
+        它负责建立连接,并将当前实例返回,以便在上下文中使用。
+
+        :return: 返回实例本身,以便在上下文中使用。
+        """
+
+        self.connect()  # 建立连接
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        """
+        当退出上下文时自动调用此方法。
+        无论上下文中的代码是否完成或因异常退出,此方法都会被调用,以确保断开连接。
+
+        :param exc_type: 异常类型, 如果没有异常则为None。
+        :param exc_value: 异常值, 如果没有异常则为None。
+        :param traceback: 异常的traceback对象, 如果没有异常则为None。
+        """
+        if exc_type:
+            utils.get_logger().error(
+                f"数据库发生异常,断开连接。异常类型:{exc_type}, 异常值:{exc_value} traceback: {traceback}"
+            )
+        self.disconnect()  # 断开连接

+ 75 - 0
SourceCode/DataMiddleware/app/utils/string_helper.py

@@ -0,0 +1,75 @@
+class StringHelper:
+
+    @staticmethod
+    def check_empty(s: str, default: str) -> str:
+        """
+        检查字符串是否为空
+        """
+        if s:
+            return s
+        return default
+
+    @staticmethod
+    def to_array(s: str, sep: str = ",") -> list[str]:
+        """
+        将字符串按指定分隔符分割成数组。
+
+        :param s: 要分割的字符串。
+        :param sep: 分隔符,默认为逗号。
+        :return: 分割后的字符串数组。
+        """
+        if not s:
+            return []
+        if sep == ",":
+            s = s.replace(",", ",")
+        return s.split(sep)
+
+    @staticmethod
+    def e_startswith(s: str, prefix: str) -> str:
+        """
+        检查字符串是否以特定前缀开头,如果没有则补全。
+
+        :param s: 要检查的字符串。
+        :param prefix: 前缀。
+        :return: 如果字符串以指定前缀开头,返回原字符串;否则返回补全后的字符串。
+        """
+        if not s.startswith(prefix):
+            return prefix + s
+        return s
+
+    @staticmethod
+    def e_endswith(s: str, suffix: str) -> str:
+        """
+        检查字符串是否以特定后缀结尾,如果没有则补全。
+
+        :param s: 要检查的字符串。
+        :param suffix: 后缀。
+        :return: 如果字符串以指定后缀结尾,返回原字符串;否则返回补全后的字符串。
+        """
+        if not s.endswith(suffix):
+            return s + suffix
+        return s
+
+    @staticmethod
+    def split_and_clean(s: str, sep: str = ",") -> list[str]:
+        """
+        将字符串按指定分隔符分割并去除空字符串。
+
+        :param s: 要分割的字符串。
+        :param sep: 分隔符,默认为逗号。
+        :return: 分割后的字符串数组,去除空字符串。
+        """
+        if not s:
+            return []
+        parts = StringHelper.to_array(s, sep)
+        return [part.strip() for part in parts if part.strip()]
+
+    @staticmethod
+    def remove_extra_spaces(s: str) -> str:
+        """
+        将字符串中的多个连续空格替换为单个空格。
+
+        :param s: 要处理的字符串。
+        :return: 替换后的字符串。
+        """
+        return " ".join(s.split())

+ 10 - 0
SourceCode/DataMiddleware/requirements.txt

@@ -0,0 +1,10 @@
+PyMySQL==1.1.1
+PyYAML==6.0.2
+Requests==2.32.3
+openai==1.58.1
+pandas~=2.2.3
+pdfplumber~=0.11.5
+pytesseract~=0.3.13
+pillow~=11.1.0
+Flask~=3.1.0
+pdf2image~=1.17.0