Przeglądaj źródła

Merge branch '1.0.0' of yue/DataMiddleware into dev

YueYunyun 6 miesięcy temu
rodzic
commit
17e43e99fa
54 zmienionych plików z 3507 dodań i 1905 usunięć
  1. 13 0
      SourceCode/DataMiddleware/.script/cmd/Build_App.run.xml
  2. 12 0
      SourceCode/DataMiddleware/.script/cmd/Run_App.run.xml
  3. 25 0
      SourceCode/DataMiddleware/.script/cmd/app.run.xml
  4. 25 0
      SourceCode/DataMiddleware/.script/cmd/img_spilt.run.xml
  5. 25 0
      SourceCode/DataMiddleware/.script/cmd/pdf_split.run.xml
  6. 135 28
      SourceCode/DataMiddleware/app/ai/fast_gpt.py
  7. 6 4
      SourceCode/DataMiddleware/app/config.yml
  8. 10 9
      SourceCode/DataMiddleware/app/data_process/__init__.py
  9. 137 49
      SourceCode/DataMiddleware/app/data_process/pre_process.py
  10. 76 19
      SourceCode/DataMiddleware/app/data_process/process.py
  11. 0 56
      SourceCode/DataMiddleware/app/data_process/standard_data_process.py
  12. 10 4
      SourceCode/DataMiddleware/app/data_send/__init__.py
  13. 26 17
      SourceCode/DataMiddleware/app/data_send/send.py
  14. 30 35
      SourceCode/DataMiddleware/app/init.sql
  15. 3 8
      SourceCode/DataMiddleware/app/main.py
  16. 57 14
      SourceCode/DataMiddleware/app/models/project_data.py
  17. 0 22
      SourceCode/DataMiddleware/app/models/standard_data.py
  18. 0 22
      SourceCode/DataMiddleware/app/models/standard_update_log.py
  19. 0 449
      SourceCode/DataMiddleware/app/stores/mysql_store.py
  20. 406 0
      SourceCode/DataMiddleware/app/stores/project_store.py
  21. 5 1
      SourceCode/DataMiddleware/app/ui/__init__.py
  22. 0 150
      SourceCode/DataMiddleware/app/ui/project_services.py
  23. 0 182
      SourceCode/DataMiddleware/app/ui/project_views.py
  24. 265 0
      SourceCode/DataMiddleware/app/ui/services_project.py
  25. 0 143
      SourceCode/DataMiddleware/app/ui/static/project_item_list.js
  26. 48 185
      SourceCode/DataMiddleware/app/ui/static/project_list.js
  27. 93 39
      SourceCode/DataMiddleware/app/ui/static/styles.css
  28. 116 0
      SourceCode/DataMiddleware/app/ui/static/sub_project_item_list.js
  29. 195 0
      SourceCode/DataMiddleware/app/ui/static/sub_project_list.js
  30. 63 12
      SourceCode/DataMiddleware/app/ui/static/utils.js
  31. 0 161
      SourceCode/DataMiddleware/app/ui/templates/project/project_item_list.html
  32. 135 247
      SourceCode/DataMiddleware/app/ui/templates/project/project_list.html
  33. 236 0
      SourceCode/DataMiddleware/app/ui/templates/project/sub_project_item_list.html
  34. 321 0
      SourceCode/DataMiddleware/app/ui/templates/project/sub_project_list.html
  35. 240 0
      SourceCode/DataMiddleware/app/ui/views_project.py
  36. 2 0
      SourceCode/DataMiddleware/app/utils/__init__.py
  37. 64 14
      SourceCode/DataMiddleware/app/utils/ai_helper.py
  38. 17 0
      SourceCode/DataMiddleware/app/utils/file_helper.py
  39. 25 4
      SourceCode/DataMiddleware/app/utils/mysql_helper.py
  40. 5 0
      SourceCode/DataMiddleware/docker/.env
  41. 35 0
      SourceCode/DataMiddleware/docker/Dockerfile
  42. 57 0
      SourceCode/DataMiddleware/docker/docker-compose.yml
  43. 7 0
      SourceCode/DataMiddleware/requirements.txt
  44. 3 3
      SourceCode/DataMiddleware/tools/config.yml
  45. 0 0
      SourceCode/DataMiddleware/tools/image_extract/__init__.py
  46. 138 0
      SourceCode/DataMiddleware/tools/image_extract/extract.py
  47. 0 0
      SourceCode/DataMiddleware/tools/img_spilt/__init__.py
  48. 9 0
      SourceCode/DataMiddleware/tools/img_spilt/run.py
  49. 71 0
      SourceCode/DataMiddleware/tools/img_spilt/spilt.py
  50. 22 0
      SourceCode/DataMiddleware/tools/init.sql
  51. 53 0
      SourceCode/DataMiddleware/tools/models/standard_model.py
  52. 0 0
      SourceCode/DataMiddleware/tools/stores/__init__.py
  53. 106 0
      SourceCode/DataMiddleware/tools/stores/mysql_store.py
  54. 180 28
      SourceCode/DataMiddleware/tools/utils/ai_helper.py

+ 13 - 0
SourceCode/DataMiddleware/.script/cmd/Build_App.run.xml

@@ -0,0 +1,13 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="Build_App" type="docker-deploy" factoryName="dockerfile" server-name="104">
+    <deployment type="dockerfile">
+      <settings>
+        <option name="imageTag" value="y_tielu-data-middleware-app:1.0.0" />
+        <option name="buildOnly" value="true" />
+        <option name="contextFolderPath" value="." />
+        <option name="sourceFilePath" value="docker/Dockerfile" />
+      </settings>
+    </deployment>
+    <method v="2" />
+  </configuration>
+</component>

+ 12 - 0
SourceCode/DataMiddleware/.script/cmd/Run_App.run.xml

@@ -0,0 +1,12 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="Run_App" type="docker-deploy" factoryName="docker-compose.yml" server-name="104">
+    <deployment type="docker-compose.yml">
+      <settings>
+        <option name="composeProjectName" value="tielu_dm" />
+        <option name="envFilePath" value="" />
+        <option name="sourceFilePath" value="docker/docker-compose.yml" />
+      </settings>
+    </deployment>
+    <method v="2" />
+  </configuration>
+</component>

+ 25 - 0
SourceCode/DataMiddleware/.script/cmd/app.run.xml

@@ -0,0 +1,25 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="app" type="PythonConfigurationType" factoryName="Python">
+    <module name="DataMiddleware" />
+    <option name="ENV_FILES" value="" />
+    <option name="INTERPRETER_OPTIONS" value="" />
+    <option name="PARENT_ENVS" value="true" />
+    <envs>
+      <env name="PYTHONUNBUFFERED" value="1" />
+    </envs>
+    <option name="SDK_HOME" value="" />
+    <option name="WORKING_DIRECTORY" value="D:\01Work\1012_DataMiddleware\SourceCode\DataMiddleware" />
+    <option name="IS_MODULE_SDK" value="true" />
+    <option name="ADD_CONTENT_ROOTS" value="true" />
+    <option name="ADD_SOURCE_ROOTS" value="false" />
+    <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
+    <option name="SCRIPT_NAME" value="$PROJECT_DIR$/app/main.py" />
+    <option name="PARAMETERS" value="" />
+    <option name="SHOW_COMMAND_LINE" value="false" />
+    <option name="EMULATE_TERMINAL" value="false" />
+    <option name="MODULE_MODE" value="false" />
+    <option name="REDIRECT_INPUT" value="false" />
+    <option name="INPUT_FILE" value="" />
+    <method v="2" />
+  </configuration>
+</component>

+ 25 - 0
SourceCode/DataMiddleware/.script/cmd/img_spilt.run.xml

@@ -0,0 +1,25 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="img_spilt" type="PythonConfigurationType" factoryName="Python">
+    <module name="DataMiddleware" />
+    <option name="ENV_FILES" value="" />
+    <option name="INTERPRETER_OPTIONS" value="" />
+    <option name="PARENT_ENVS" value="true" />
+    <envs>
+      <env name="PYTHONUNBUFFERED" value="1" />
+    </envs>
+    <option name="SDK_HOME" value="" />
+    <option name="WORKING_DIRECTORY" value="D:\01Work\1012_DataMiddleware\SourceCode\DataMiddleware" />
+    <option name="IS_MODULE_SDK" value="true" />
+    <option name="ADD_CONTENT_ROOTS" value="true" />
+    <option name="ADD_SOURCE_ROOTS" value="true" />
+    <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
+    <option name="SCRIPT_NAME" value="$PROJECT_DIR$/tools/img_spilt/run.py" />
+    <option name="PARAMETERS" value="" />
+    <option name="SHOW_COMMAND_LINE" value="false" />
+    <option name="EMULATE_TERMINAL" value="false" />
+    <option name="MODULE_MODE" value="false" />
+    <option name="REDIRECT_INPUT" value="false" />
+    <option name="INPUT_FILE" value="" />
+    <method v="2" />
+  </configuration>
+</component>

+ 25 - 0
SourceCode/DataMiddleware/.script/cmd/pdf_split.run.xml

@@ -0,0 +1,25 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="pdf_split" type="PythonConfigurationType" factoryName="Python">
+    <module name="DataMiddleware" />
+    <option name="ENV_FILES" value="" />
+    <option name="INTERPRETER_OPTIONS" value="" />
+    <option name="PARENT_ENVS" value="true" />
+    <envs>
+      <env name="PYTHONUNBUFFERED" value="1" />
+    </envs>
+    <option name="SDK_HOME" value="" />
+    <option name="WORKING_DIRECTORY" value="D:\01Work\1012_DataMiddleware\SourceCode\DataMiddleware" />
+    <option name="IS_MODULE_SDK" value="true" />
+    <option name="ADD_CONTENT_ROOTS" value="true" />
+    <option name="ADD_SOURCE_ROOTS" value="false" />
+    <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
+    <option name="SCRIPT_NAME" value="$PROJECT_DIR$/tools/pdf_split/main.py" />
+    <option name="PARAMETERS" value="" />
+    <option name="SHOW_COMMAND_LINE" value="false" />
+    <option name="EMULATE_TERMINAL" value="false" />
+    <option name="MODULE_MODE" value="false" />
+    <option name="REDIRECT_INPUT" value="false" />
+    <option name="INPUT_FILE" value="" />
+    <method v="2" />
+  </configuration>
+</component>

+ 135 - 28
SourceCode/DataMiddleware/app/ai/fast_gpt.py

@@ -1,6 +1,5 @@
-from httpx import stream
 
-import utils,json,requests
+import utils,json,requests, os
 import re  # 添加正则表达式库
 
 class FastGPTAi:
@@ -11,19 +10,19 @@ class FastGPTAi:
         self._logger = utils.get_logger()
         self._headers ={}
 
-    def call_ai(self, msg: str) -> json:
+    def call_ai(self, msg: str, api_key: str=None) -> json:
         try:
-            if self._api_key is None:
+            api_key = api_key if api_key else self._api_key
+            if api_key is None:
                 self._logger.error("fastgpt.api_key 没有配置")
                 raise Exception("fastgpt.api_key 没有配置")
 
-            self._headers = {
-                "Authorization": f"Bearer {self._api_key}",
+            headers = {
+                "Authorization": f"Bearer {api_key}",
                 "Content-Type": "application/json"
             }
             url = f"{self._api_url}/v1/chat/completions"
             data = {
-                # "model":"",
                 "stream": False,
                 "detail": False,
                 "messages": [
@@ -37,27 +36,135 @@ class FastGPTAi:
                 }
             }
 
-            response = requests.post(url, headers=self._headers, json=data)
-            if response.status_code == 200:
-                result = response.json()
-                self._logger.info(f"Response: {result}")
-                content = result.get("choices", [{}])[0].get("message", {}).get("content", "")
-                if content:
-                    # 使用正则表达式去除 content 前后的特定字符串
-                    content = re.sub(r'^```json\n', '', content.strip())
-                    content = re.sub(r'\n```$', '', content.strip())
-                    try:
-                        data = json.loads(content)
-                        self._logger.info(f"Response_JSON: {data}")
-                        return data
-                    except json.JSONDecodeError as e:
-                        self._logger.error(f"Failed to decode JSON: {e}")
-                        return None
-                return None
-            else:
-                error_msg = f"Error: {response.status_code} - {response.text}"
-                self._logger.error(error_msg)
-                return None
+            return self.process_response(data, headers, url)
+        except Exception as e:
+            self._logger.error(f"Error: {str(e)}")
+            return None
+
+    def call_ai_with_image(self,image_path, msg: str,api_key: str=None) -> json:
+        self._logger.info("调用 fastgpt 的AI_Image接口")
+        try:
+            if not os.path.exists(image_path):
+                utils.get_logger().error(f"图片文件不存在: {image_path}")
+                raise Exception(f"图片文件不存在: {image_path}")
+            api_key = api_key if api_key else  self._api_key
+            if api_key is None:
+                self._logger.error("fastgpt.api_key 没有配置")
+                raise Exception("fastgpt.api_key 没有配置")
+
+            headers = {
+                "Authorization": f"Bearer {api_key}",
+                "Content-Type": "application/json"
+            }
+            url = f"{self._api_url}/v1/chat/completions"
+            base64_str = utils.encode_file(image_path)
+            data = {
+                # "model":"",
+                "stream": False,
+                "detail": False,
+                "messages": [
+                    {
+                        "role": "user",
+                        "content": [
+                            {
+                                "type": "text",
+                                "text": msg
+                            },
+                            {
+                                "type": "image_url",
+                                "image_url": {
+                                    "url": base64_str
+                                }
+                            }
+                        ]
+                    }
+                ],
+                "response_format": {
+                    "type": "json_object"
+                }
+            }
+
+            return self.process_response(data, headers, url)
         except Exception as e:
             self._logger.error(f"Error: {str(e)}")
             return None
+
+    def call_ai_with_file(self,file_path, msg: str,api_key: str=None) -> json:
+        self._logger.info("调用 fastgpt 的AI_File接口")
+        try:
+            if not os.path.exists(file_path):
+                utils.get_logger().error(f"文件不存在: {file_path}")
+                raise Exception(f"文件不存在: {file_path}")
+            api_key = api_key if api_key else  self._api_key
+            if api_key is None:
+                self._logger.error("fastgpt.api_key 没有配置")
+                raise Exception("fastgpt.api_key 没有配置")
+
+            headers = {
+                "Authorization": f"Bearer {api_key}",
+                "Content-Type": "application/json"
+            }
+            url = f"{self._api_url}/v1/chat/completions"
+            file_name = os.path.basename(file_path)
+            base64_str = utils.encode_file(file_path)
+            data = {
+                # "model":"",
+                "stream": False,
+                "detail": False,
+                "messages": [
+                    {
+                        "role": "user",
+                        "content": [
+                            {
+                                "type": "text",
+                                "text": msg
+                            },
+                            {
+                                "type": "file_url",
+                                "name": file_name,
+                                "url": base64_str
+                            }
+                        ]
+                    }
+                ]
+            }
+
+            res= self.process_response(data, headers, url)
+            if res:
+                return res
+            return None
+        except Exception as e:
+            self._logger.error(f"Error: {str(e)}")
+            return None
+
+    def process_response(self, data, headers, url)->json:
+        response = requests.post(url, headers=headers, json=data)
+        if response.status_code == 200:
+            result = response.json()
+            self._logger.info(f"Response: {result}")
+            content = result.get("choices", [{}])[0].get("message", {}).get("content", "")
+            if content:
+                if "```json" in content:
+                    # 使用正则表达式提取代码块内容
+                    code_block_match = re.search(r'```(json)?(.*?)```', content, re.DOTALL)
+                    if code_block_match:
+                        content = code_block_match.group(2).strip()
+                    content = content.strip()
+                try:
+                    data = json.loads(content)
+                    self._logger.info(f"Response_JSON: {data}")
+                    return data
+                except json.JSONDecodeError as e:
+                    self._logger.error(f"JSON解析失败,尝试清理特殊字符: {e}")
+                    # 尝试清理特殊字符后重新解析
+                    content = re.sub(r'[“”‘’\x00-\x1f]', '', content)
+                    try:
+                        return json.loads(content)
+                    except json.JSONDecodeError:
+                        self._logger.error("最终JSON解析失败,返回原始内容")
+                        return {'raw_content': content}
+            return None
+        else:
+            error_msg = f"Error: {response.status_code} - {response.text}"
+            self._logger.error(error_msg)
+            return None

+ 6 - 4
SourceCode/DataMiddleware/app/config.yml

@@ -1,9 +1,8 @@
-app:
-  port: 5123
 mysql:
   host: 192.168.0.81
   port: 3307
-  db: iwb_data_middleware_dev
+#  db: iwb_data_middleware_dev
+  db: iwb_data_middleware_v1_1
   user: root
   password: Iwb-2024
   charset: utf8mb4
@@ -25,6 +24,9 @@ anythingLLm:
   workspace: datamiddleware
   thread: fd218d56-8717-4519-a6e0-09c64b732091
 fastgpt:
-  api_key: fastgpt-wdJruTQNjqeH7oRdf54Ilb12pHAspEdW7Io3hcsuaifx5U1OOFzW8Qrc
+#  api_key: fastgpt-wdJruTQNjqeH7oRdf54Ilb12pHAspEdW7Io3hcsuaifx5U1OOFzW8Qrc
+  api_key_pre_process: fastgpt-o4CF7Pu1FRTvHjWFqeNcClBS6ApyflNfkBGXo9p51fuBMAX1L0erU8yz8
+  api_key_process: fastgpt-wdJruTQNjqeH7oRdf54Ilb12pHAspEdW7Io3hcsuaifx5U1OOFzW8Qrc
+  api_key: fastgpt-o4CF7Pu1FRTvHjWFqeNcClBS6ApyflNfkBGXo9p51fuBMAX1L0erU8yz8
   api_url: http://192.168.0.104:8020/api
   app_id: 67c6be7d686fc1d3f0cc1cce

+ 10 - 9
SourceCode/DataMiddleware/app/data_process/__init__.py

@@ -1,8 +1,8 @@
-from data_process import pre_process,process, standard_data_process
-from models.project_data import ProjectModel
+from data_process import pre_process,process
+from models.project_data import SubProjectModel,SubProjectItemModel
 
 
-def process_project(project: ProjectModel) -> bool:
+def process_project(project: SubProjectModel) -> bool:
     """
     分析处理数据
     :param project: 项目
@@ -17,7 +17,7 @@ def process_project(project: ProjectModel) -> bool:
         return False
 
 
-def _pre_process_data(project: ProjectModel) -> bool:
+def _pre_process_data(project: SubProjectModel) -> bool:
     """
     预处理数据
     :param project: 项目
@@ -25,17 +25,18 @@ def _pre_process_data(project: ProjectModel) -> bool:
     """
     return pre_process.PreProcess().run(project)
 
-def process_data(project: ProjectModel) -> bool:
+def process_data(project: SubProjectModel) -> bool:
     """
     处理数据
     :param project: 项目编号
     :return: None
     """
     return process.Process().run(project)
-def process_standard_data(version: str) -> None:
+
+def process_item(project_item: SubProjectItemModel) -> bool:
     """
-    处理标准数据
-    :param version: 标准版本
+    处理数据
+    :param project_item: 项目明细
     :return: None
     """
-    standard_data_process.StandardDataProcess().run(version)
+    return process.Process().process_item(project_item)

+ 137 - 49
SourceCode/DataMiddleware/app/data_process/pre_process.py

@@ -1,44 +1,79 @@
-import utils, pandas as pd
+import utils, pandas as pd, os, json
 from pathlib import Path
 
-from models.project_data import ProjectModel, ProjectItemModel
-from stores.mysql_store import MysqlStore
+from models.project_data import SubProjectModel, SubProjectItemModel
+from stores.project_store import ProjectStore
+from ai.fast_gpt import FastGPTAi
 
 class PreProcess:
 
     def __init__(self):
         self._logger= utils.get_logger()
-        self._store= MysqlStore()
-        self._ai_helper = utils.AiHelper()
+        self._project_store= ProjectStore()
+        # self._ai = utils.AiHelper()
+        self._ai = FastGPTAi()
         self._ai_sys_prompt = "从给定信息中提取结构化信息,并返回json压缩成一行的字符串,如果部分信息为空或nan,则该字段返回为空。"
         self.separate_ai_calls = False
         self._data={}
 
-    def run(self, project: ProjectModel, separate_ai_calls=True) ->bool:
+    def run(self, project: SubProjectModel, separate_ai_calls=True) ->bool:
         try:
-            self._logger.info(f"开始预处理项目:{project.project_name}[{project.project_no}_{project.standard_version}] ")
-            self._store.update_project_status(project.id,21)
+            if not project.file_paths:
+                return False
+            self._logger.info(f"开始预处理项目:{project.sub_project_name}[{project.project_id}_{project.standard_version}] ")
+            self._project_store.update_sub_project_status(project.id, 21)
             self.separate_ai_calls = separate_ai_calls
-            raw_data = self.read_excels(f"{utils.get_config_value("file.source_path","./temp_files")}/project/{project.project_no}/")
+            res_data = []
+            raw_data = self.read_excels(project.file_paths)
             if self.separate_ai_calls:
                 for filename, sheets in raw_data.items():
                     excel_data = self.normalize_data({filename: sheets})
-                    self.call_ai(project.id, excel_data)
+                    data = self.call_ai(project.id, excel_data)
+                    res_data.extend(data)
             else:
                 excel_data = self.normalize_data(raw_data)
-                self.call_ai(project.id, excel_data)
-            project.items = self._data[project.id].items
-            self._store.re_insert_project(project)
-            del self._data[project.id]
-            self._logger.info(f"结束预处理项目:{project.project_name}[{project.project_no}_{project.standard_version}] [设备条数:{len(project.items)}]")
-            self._store.update_project_status(project.id,31)
+                res_data = self.call_ai(project.id, excel_data)
+            project.items = res_data
+            self._project_store.re_insert_sub_project(project)
+            self._logger.info(f"结束预处理项目:{project.sub_project_name}[{project.project_id}_{project.standard_version}] [设备条数:{len(project.items)}]")
+            self._project_store.update_sub_project_status(project.id, 31)
             return True
         except Exception as e:
-            self._logger.error(f"预处理项目失败:[{project.project_no}] 错误: {e}")
-            self._store.update_project_status(project.id,11)
+            self._logger.error(f"预处理项目失败:[{project.project_id}] 错误: {e}")
+            self._project_store.update_sub_project_status(project.id, 11)
             return False
 
-    def read_excels(self, folder_path):
+    def read_excels(self, paths):
+        file_paths = paths.split(',')
+        raw_data = {}
+        for file in file_paths:
+            try:
+                file = Path(file)  # 将 file 转换为 Path 对象
+                if not file.exists():
+                    self._logger.error(f"文件不存在:{file}")
+                    continue
+                # 使用确定的表头行读取整个 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 read_excels_1(self, folder_path):
         path = Path(folder_path)
         # 验证路径是否存在
         if not path.exists():
@@ -61,7 +96,7 @@ class PreProcess:
                 sheet_data = {}
                 for sheet_name, df in sheets.items():
                     # 读取前几行以确定表头
-                    header_row = self.determine_header_row(df,file.name)
+                    header_row = self.determine_header_row(df, file.name)
                     if header_row >= 0:
                         # 使用确定的表头行重新读取Sheet
                         df = df.iloc[header_row:]  # 直接使用确定的表头行
@@ -130,34 +165,18 @@ class PreProcess:
         return prompt
 
     def call_ai(self, project_id:int, excel_data):
-        project = ProjectModel(project_id=project_id)
-        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)
-                # response = {'data': [{'n': '阻燃型导线穿管敷设', 'm': 'WDZB1-BYJ-750V-2.5mm2', 'u': '米', 'c': 900.0}, {'n': '阻燃型导线穿管敷设', 'm': 'WDZB1-BYJ-750V-4mm2', 'u': '米', 'c': 800.0}, {'n': '耐火型导线穿管敷设', 'm': 'WDZB1N-BYJ-750V-2.5mm2', 'u': '米', 'c': 200.0}, {'n': '防火堵料', 'm': '', 'u': '公斤', 'c': 10.0}, {'n': '防火漆', 'm': '10kg/', '': '桶', 'c': 2.0}, {'n': '镀锌钢管', 'm': 'SC20', 'u': '米', 'c': 580.0}, {'n': '接地线', 'm': '热浸镀锌扁钢25x4', 'u': '米', 'c': 50.0}, {'n': '局部等电位端子箱', 'm': '', 'u': '个', 'c': 2.0}, {'n': '双联单控照明开关', 'm': '~250V 10A', 'u': '个', 'c': 4.0}, {'n': '密闭双联单控照明开关', 'm': '~250V 10A', 'u': '个', 'c': 4.0}, {'n': '配合空调室外机移位', 'm': '', 'u': '项', 'c': 1.0}, {'n': '应急照明灯', 'm': '220V,10W', 'u': '套', 'c': 4.0}, {'n': '门禁', 'm': '', 'u': '套', 'c': 4.0}, {'n': '配电线路改移', 'm': '开槽、移点位等', 'u': '项', 'c': 1.0}, {'n': '烘手器插座', 'm': '220V,10A,密闭型', 'u': '个', 'c': 2.0}], 'completion_tokens': 439, 'prompt_tokens': 766, 'total_tokens': 1205}
-                # 更新数据部分
-                project.items.extend(self.format_data(project_id, response["data"]))
-                project.completion_tokens += response["completion_tokens"]
-                project.prompt_tokens += response["prompt_tokens"]
-                project.total_tokens += response["total_tokens"]
-        else:
-            prompt = self.prompt_template(excel_data)
-            response = self._ai_helper.call_openai(self._ai_sys_prompt, prompt)
-            project.completion_tokens = response["completion_tokens"]
-            project.prompt_tokens = response["prompt_tokens"]
-            project.total_tokens = response["total_tokens"]
-            project.items = self.format_data(project_id, response["data"])
-        self._data[project_id] = project
-
-
-    def format_data(self,project_id,new_data) ->list[ProjectItemModel]:
-        formatted_data = []
-        for data in new_data:
+        api_key = utils.get_config_value("fastgpt.api_key_pre_process")
+        prompt = self.prompt_template(excel_data)
+        json_data = self._ai.call_ai(prompt, api_key)
+        data = self.format_data(project_id, json_data)
+        return data
+
+    def format_data(self, project_id, data_list) ->list[SubProjectItemModel]:
+        formatted_list = []
+        for data in data_list:
             try:
-                item = ProjectItemModel(
-                    project_id=project_id,
+                item = SubProjectItemModel(
+                    sub_project_id=project_id,
                     device_name="",
                     device_model="",
                     device_unit="",
@@ -170,8 +189,77 @@ class PreProcess:
                     item.device_unit = data['u']
                 if data.get('c'):
                     item.device_count = data['c']
-                formatted_data.append(item)
+                formatted_list.append(item)
             except Exception as e:
                 self._logger.error(f"格式化数据时出错: {data} {e}")
+        self._logger.debug(f"格式化数据数量: {len(formatted_list)}")
+        return formatted_list
 
-        return formatted_data
+    def run_1(self, sub_project: SubProjectModel) ->bool:
+        self._logger.info(f"开始预处理项目:{sub_project.sub_project_name}[{sub_project.project_id}_{sub_project.standard_version}] ")
+        base_path=  utils.get_config_value("file.source_path", "./temp_files")
+        folder_path= os.path.join(base_path, f'{sub_project.get_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"警告:未找到任何文件")
+            raise FileNotFoundError(f"未找到任何文件")
+        try:
+            self._project_store.update_sub_project_status(sub_project.id, 21)
+            items =[]
+            for file in excel_files:
+                file_path = str(file.resolve())
+                data = self.call_ai_with_files(sub_project.id, file_path)
+                if data:
+                    items.extend(data)
+            sub_project.items = items
+            self._project_store.re_insert_sub_project(sub_project)
+            self._logger.info(
+                f"结束预处理项目:{sub_project.sub_project_name}[{sub_project.project_id}_{sub_project.standard_version}] [设备条数:{len(sub_project.items)}]")
+            self._project_store.update_sub_project_status(sub_project.id, 31)
+            return True
+        except Exception as e:
+            self._logger.error(f"项目:{sub_project.project_id} 文件处理失败: {e}")
+            self._project_store.update_sub_project_status(sub_project.id, 11)
+            return False
+
+
+    def call_ai_with_files(self, sub_id:int,file_paths:str) ->json:
+        try:
+            prompt = """从上传的表格数据中提取信息,要求:
+                       1. 识别字段类型:数值/文本/日期
+                       2. 提取信息结构体:```typescript
+                           type item { 
+                               n: string; //物料名称
+                               m: string; //型号规格
+                               u:string; //单位
+                               c: float; //数量,数量多列的话要求和
+                           }
+                       ```
+                       3. 返回压缩成一行的item数组的json字符串
+                       """
+            api_key = utils.get_config_value("fastgpt.api_key_pre_process")
+            data = self._ai.call_ai_with_file(file_paths, prompt, api_key)
+            if isinstance(data, str):
+                data = json.loads(data)
+            res_data = self.format_data(sub_id, data)
+            return res_data
+        except Exception as e:
+            self._logger.error(f"文件处理失败: {e}")
+            return ""
+
+    @staticmethod
+    def check_file_type(file_name:str) ->bool:
+        file_type = file_name.split('.')[-1]
+        if file_type not in ['xlsx','xls']:
+            return False
+        return True

+ 76 - 19
SourceCode/DataMiddleware/app/data_process/process.py

@@ -1,33 +1,88 @@
 import utils,json
 
-from stores.mysql_store import MysqlStore
-from models.project_data import ProjectModel, ProjectItemModel
+from stores.project_store import ProjectStore
+from models.project_data import SubProjectModel, SubProjectItemModel
 from ai.fast_gpt import FastGPTAi
 
 
 class Process:
     def __init__(self):
         self._logger= utils.get_logger()
-        self._store= MysqlStore()
+        self._project_store= ProjectStore()
         self._ai = FastGPTAi()
         self._data={}
 
-    def run(self, project: ProjectModel) ->bool:
+    def run(self, sub_project: SubProjectModel) ->bool:
         try:
-            self._logger.info(f"开始处理数据:{project.project_no}")
-            self._store.update_project_status(project.id,22)
-            project_items = self._store.query_project_items_by_project(project.id)
-            text = self.prompt_template(project,project_items)
-            items = self.call_ai_process(text)
-            self._store.update_project_item_standard_no_batch(items)
-            self._store.update_project_status(project.id,12)
-            self._logger.info(f"处理数据完成:{project.project_no}")
+            self._logger.debug(f"开始处理数据:{sub_project.sub_project_name}[{sub_project.id}]")
+            self._project_store.update_sub_project_status(sub_project.id, 22)
+            project_items = self._project_store.query_sub_project_items_by_project(sub_project.id)
+            for project_item in project_items:
+                self.process_item(project_item)
+            self._project_store.update_sub_project_status(sub_project.id, 32)
+            self._logger.info(f"处理数据完成:{sub_project.sub_project_name}[{sub_project.id}]")
             return True
         except Exception as e:
             self._logger.error(f"处理数据失败:{e}")
-            self._store.update_project_status(project.id,12)
+            self._project_store.update_sub_project_status(sub_project.id, 12)
             return False
 
+    def process_item(self, sub_project_item: SubProjectItemModel) ->bool:
+        try:
+            self._logger.debug(f"开始处理明细:{sub_project_item.device_name}{sub_project_item.device_model}[{sub_project_item.id}]")
+            self._project_store.update_sub_project_item_process_status(sub_project_item.id, 1)
+            data = self.call_ai(sub_project_item)
+            self._project_store.update_sub_project_item_standard_no(sub_project_item.id, data.standard_no)
+            return True
+        except Exception as e:
+            self._logger.error(f"处理数据明细:{e}")
+            self._project_store.update_sub_project_item_process_status(sub_project_item.id, 3)
+            return False
+
+
+    def call_ai(self, item: SubProjectItemModel) -> SubProjectItemModel:
+        try:
+            self._logger.info(f"开始处理数据:{item.id}")
+            msg = """
+            请分析提供的json数据,要求:
+            1. 根据工作内容、类型、项目名称、规格型号、数量、单位查找计算出定额编号
+            3. 提供的数据结构化信息:```typescript
+            export interface item { 
+            i: // ID
+            n: string; //物料名称
+            m: string; //型号规格
+            u:string; //单位
+            c: float; //数量
+            }
+            ```
+            4. 需返回的结构体:```typescript
+            export interface item { 
+            i: int; // ID 与提供的ID保持一致
+            s: string; // 定额编号
+            }
+            ```
+            5. 返回结构体item的json字符串,压缩成一行。
+            6. 数据如下:
+                """
+            msg += json.dumps(item.to_ai_json())
+            api_key = utils.get_config_value("fastgpt.api_key_process")
+            self._logger.info(f"开始调用AI:\n {msg}")
+            json_data = self._ai.call_ai(msg, api_key)
+            self._logger.info(f"AI返回结果:{json_data}")
+            if not json_data:
+                raise Exception("AI返回结果为空")
+            # 检查 json_data 是否为数组,并取第一项
+            if isinstance(json_data, list):
+                json_data = json_data[0]
+            data = SubProjectItemModel(
+                item_id=json_data["i"],
+                standard_no=json_data["s"]
+            )
+            return data
+        except Exception as e:
+            self._logger.error(f"AI调用失败:{e}")
+            raise Exception(f"AI调用失败:{e}")
+
     @staticmethod
     def prompt_template(project, items) ->str:
         text = f"""
@@ -53,25 +108,27 @@ class Process:
         5. 返回结构体item的数组的json数组格式,压缩成一行。
         6. 数据如下:
         """
-        item_ai_json = ProjectItemModel.list_to_ai_json(items)
+        item_ai_json = SubProjectItemModel.list_to_ai_json(items)
         text += json.dumps(item_ai_json,ensure_ascii=False, separators=(',', ':'))
         return text
 
-    def call_ai_process(self, message:str) ->list[ProjectItemModel]:
+    def call_ai_batch(self, message:str) ->list[SubProjectItemModel]:
         try:
-
+            api_key = utils.get_config_value("fastgpt.api_key_process")
             self._logger.info(f"开始调用AI:\n {message}")
-            json_data = self._ai.call_ai(message)
+            json_data = self._ai.call_ai(message,api_key)
             self._logger.info(f"AI返回结果:{json_data}")
+            if not json_data:
+                raise  Exception("AI返回结果为空")
             data=[]
             for item in json_data:
-                data.append(ProjectItemModel(
+                data.append(SubProjectItemModel(
                     item_id=item["i"],
                     standard_no=item["s"]
                 ))
             return data
         except Exception as e:
             self._logger.error(f"AI调用失败:{e}")
-            return []
+            raise Exception(f"AI调用失败:{e}")
 
 

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

@@ -1,56 +0,0 @@
-import utils
-from pathlib import Path
-
-from stores.mysql_store import MysqlStore
-from models.standard_data import StandardModel
-import pandas as pd  # 添加对pandas库的导入,用于处理表格数据
-
-class StandardDataProcess:
-    def __init__(self):
-        self._logger= utils.get_logger()
-        self._store= MysqlStore()
-        self._ai_helper = utils.AiHelper()
-        self._ai_sys_prompt = "查询给定数据的标准编号信息,返回压缩的json字符传"
-
-    def run(self,version:str):
-        path= f"./temp_files/data/v{version}"
-        data = self.read_excels(path)
-        self.insert_data(version,data)
-
-    def read_excels(self, 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()}")
-
-        excel_files = list(path.glob('.csv'))
-        for file in excel_files:
-            try:
-                # 读取CSV文件
-                df = pd.read_csv(file)
-                data =  df.to_dict(orient='records')
-                return data
-            except Exception as e:
-                self._logger.error(f"读取数据失败 {file}: {str(e)}")
-
-    def insert_data(self,version:str,data):
-        for row in data:
-            standard_no = row['电算代号']
-            if not standard_no:
-                continue
-            name = row['项目名称']
-            unit = row['单位']
-            item = self._store.query_standard_data_by_no(row['电算代号'],version)
-            if item:
-                item.standard_version=version
-                item.standard_name=name
-                item.device_model=None
-                item.device_unit=unit
-                self._store.update_standard_data(item)
-            else:
-                item= StandardModel(device_name=name, device_unit=unit, standard_no=standard_no, standard_version=version)
-            self._store.insert_standard_data(item)

+ 10 - 4
SourceCode/DataMiddleware/app/data_send/__init__.py

@@ -1,17 +1,23 @@
 from data_send import  send
-from models.project_data import ProjectModel
+from models.project_data import SubProjectModel,SubProjectItemModel
 
 
-def send_project_data(project: ProjectModel) -> bool:
+def send_project_data(project: SubProjectModel) -> bool:
     """
     发送项目数据到平台
     :param project
     """
     return send.DataSend().send_by_project(project)
 
-def send_item_data(item_id: int) -> bool:
+def send_item_data_by_id(item_id: int) -> bool:
     """
     发送项目数据到平台
     :param item_id
     """
-    return send.DataSend().send_by_item_id(item_id)
+    return send.DataSend().send_by_item_id(item_id)
+def send_item_data(item: SubProjectItemModel) -> bool:
+    """
+    发送项目数据到平台
+    :param item
+    """
+    return send.DataSend().send_by_item(item)

+ 26 - 17
SourceCode/DataMiddleware/app/data_send/send.py

@@ -1,44 +1,53 @@
 import  utils
-from stores.mysql_store import MysqlStore
-from models.project_data import ProjectModel, ProjectItemModel
+from stores.project_store import ProjectStore
+from models.project_data import SubProjectModel, SubProjectItemModel
 
 
 class DataSend:
     def __init__(self):
         self._logger= utils.get_logger()
-        self._store= MysqlStore()
+        self._project_store= ProjectStore()
 
-    def send_by_project(self, project: ProjectModel) -> bool:
+    def send_by_project(self, sub_project: SubProjectModel) -> bool:
         try:
 
-            self._logger.info(f"开始发送数据,项目:{project.project_no}")
-            self._store.update_project_status(project.id,23)
-            data_list = self._store.query_project_items_by_project(project.id, with_empty=False)
+            self._logger.info(f"开始发送数据,项目:{sub_project.sub_project_name}[{sub_project.project_id}_{sub_project.id}]")
+            self._project_store.update_sub_project_status(sub_project.id, 23)
+            data_list = self._project_store.query_sub_project_items_by_project(sub_project.id, with_empty=False)
             for data in data_list:
                 self.send_data(data)
-            self._store.update_project_status(project.id,33)
-            self._logger.info(f"发送数据成功,项目:{project.project_no} 共发送{len(data_list)}条")
+            self._project_store.update_sub_project_status(sub_project.id, 33)
+            self._logger.info(f"发送数据成功,项目:{sub_project.sub_project_name}[{sub_project.project_id}_{sub_project.id}] 共发送{len(data_list)}条")
             return True
         except Exception as e:
-            self._logger.error(f"发送数据失败,项目:{project.project_no},错误信息:{e}")
-            self._store.update_project_status(project.id,13)
+            self._logger.error(f"发送数据失败,项目:{sub_project.sub_project_name}[{sub_project.project_id}_{sub_project.id}],错误信息:{e}")
+            self._project_store.update_sub_project_status(sub_project.id, 13)
             return False
 
     def send_by_item_id(self, item_id: int) -> bool:
+        data = self._project_store.query_sub_project_item_by_id(item_id)
+        if data:
+            return self.send_by_item(data)
+        return False
+
+    def send_by_item(self, item: SubProjectItemModel) -> bool:
         try:
-            self._logger.info(f"开始发送数据,项目:{item_id}")
-            data = self._store.query_project_item_by_id(item_id)
-            if self.send_data(data):
-                self._logger.info(f"发送数据成功,项目:{item_id}")
+            self._logger.info(f"开始发送数据,项目:{item.id}")
+            if self.send_data(item):
+                self._logger.info(f"发送数据成功,项目:{item.id}")
                 return True
             return False
         except Exception as e:
-            self._logger.error(f"发送数据失败,项目:{item_id},错误信息:{e}")
+            self._logger.error(f"发送数据失败,项目:{item.id},错误信息:{e}")
             return False
 
-    def send_data(self, data: ProjectItemModel) -> bool:
+    def send_data(self, data: SubProjectItemModel) -> bool:
         try:
             self._logger.debug(f"开始远程请求发送数据,数据:{data}" )
+            self._project_store.update_sub_project_item_send_status(data.id, 1)
+
+            self._project_store.update_sub_project_item_send_status(data.id, 2)
         except Exception as e:
             self._logger.error(f"发送数据失败,数据:{data},错误信息:{e}")
+            self._project_store.update_sub_project_item_send_status(data.id, 3)
             return False

+ 30 - 35
SourceCode/DataMiddleware/app/init.sql

@@ -1,62 +1,57 @@
 -- 创建数据库
-CREATE DATABASE IF NOT EXISTS iwb_data_middleware_dev CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
-USE iwb_data_middleware_dev;
+CREATE DATABASE IF NOT EXISTS iwb_data_middleware_v1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
+USE iwb_data_middleware_v1;
 
--- 创建 SourceData
+-- 创建 sub_project
 CREATE TABLE IF NOT EXISTS project (
     id INT AUTO_INCREMENT PRIMARY KEY,
-    project_no VARCHAR(255) NOT NULL,
-    INDEX idx_project_no (project_no),
     project_name VARCHAR(255),
+    description VARCHAR(1000),
+    is_del TINYINT DEFAULT 0,
+    delete_by VARCHAR(255),
+    delete_time DATETIME,
+    update_by VARCHAR(255),
+    update_time DATETIME,
+    create_by VARCHAR(255),
+    create_time DATETIME
+) CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
+
+-- 创建 sub_project 表
+CREATE TABLE IF NOT EXISTS sub_project (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    project_id int,
+    INDEX idx_project_id (project_id),
+    sub_project_name VARCHAR(255),
     work_catalog VARCHAR(255),
     work_content VARCHAR(255),
     standard_version VARCHAR(50),
+    file_paths TEXT,
     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_by VARCHAR(255),
     delete_time DATETIME,
+    update_by VARCHAR(255),
     update_time DATETIME,
+    create_by VARCHAR(255),
     create_time DATETIME
 ) CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
 
--- 创建 SourceItemData 表
-CREATE TABLE IF NOT EXISTS project_item (
+-- 创建 project_item
+CREATE TABLE IF NOT EXISTS sub_project_item (
     id INT AUTO_INCREMENT PRIMARY KEY,
     project_id int NOT NULL,
+    sub_project_id int 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) ,
+    process_status TINYINT DEFAULT 0,
+    process_time DATETIME,
+    send_status TINYINT DEFAULT 0,
+    send_time DATETIME,
     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_id int,
-    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;

+ 3 - 8
SourceCode/DataMiddleware/app/main.py

@@ -5,15 +5,10 @@ logger = utils.get_logger()
 
 def main():
     logger.info("程序启动中...")
-    thread = threading.Thread(target=debug_test)
-    thread.start()
+    # 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
-
-
-
-
+    app.run(host='0.0.0.0',port=5123)  # 指定HTTP端口为5123
 
 def debug_test():
     logger.info("调试程序开始")

+ 57 - 14
SourceCode/DataMiddleware/app/models/project_data.py

@@ -1,71 +1,114 @@
 from datetime import datetime
 
-
 class ProjectModel:
-
     def __init__(self,
-                 project_no: str= None,
                  project_name: str = None,
+                 description:str = None,
+                 project_id: int = None,
+                 delete_by:str = None,
+                 delete_time: datetime = None,
+                 create_by: str = None,
+                 create_time: datetime = None,
+                 update_by: str = None,
+                 update_time: datetime = None,
+                 ):
+        self.id = project_id
+        self.project_name = project_name
+        self.description = description
+        self.delete_by = delete_by
+        self.delete_time = delete_time
+        self.create_by = create_by
+        self.create_time = create_time
+        self.update_by = update_by
+        self.update_time = update_time
+
+class SubProjectModel:
+
+    def __init__(self,
+                 project_id: int = None,
+                 sub_project_name: str = None,
                  work_catalog: str = None,
                  work_content: str = None,
                  standard_version: str = None,
+                 file_paths: str = None,
                  status: int = 0,
                  completion_tokens: int = 0,
                  prompt_tokens: int = 0,
                  total_tokens: int = 0,
-                 update_time: datetime = None,
+                 sub_id: int = None,
+                 delete_by: str = None,
+                 delete_time: datetime = None,
+                 create_by: str = None,
                  create_time: datetime = None,
-                 project_id: int = None,
+                 update_by: str = None,
+                 update_time: datetime = None,
                  ):
-        self.id = project_id
-        self.project_no = project_no
-        self.project_name = project_name
+        self.id = sub_id
+        self.project_id = project_id
+        self.sub_project_name = sub_project_name
         self.work_catalog = work_catalog
         self.work_content = work_content
         self.standard_version = standard_version
+        self.file_paths = file_paths
         self.status = status
         self.completion_tokens = completion_tokens
         self.prompt_tokens = prompt_tokens
         self.total_tokens = total_tokens
-        self.update_time = update_time
+        self.delete_by = delete_by
+        self.delete_time = delete_time
+        self.create_by = create_by
         self.create_time = create_time
+        self.update_by = update_by
+        self.update_time = update_time
         self.items = []
 
     def __str__(self):
-        return f"SourceData(project_no={self.project_no}, project_name={self.project_name}, standard_version={self.standard_version})"
+        return f"SubProjectModel(id={self.id}, project_no={self.project_id}, project_name={self.sub_project_name}, work_catalog={self.work_catalog}, work_content={self.work_content}, standard_version={self.standard_version}, status={self.status}, completion_tokens={self.completion_tokens}, prompt_tokens={self.prompt_tokens}, total_tokens={self.total_tokens}, update_time={self.update_time}, create_time={self.create_time})"
 
+    def get_path(self):
+        return f"project/p{self.project_id}/s{self.id}"
     def set_items(self, items: list):
         self.items = items
 
-    def add_item(self, item: 'ProjectItemModel'):
+    def add_item(self, item: 'SubProjectItemModel'):
         self.items.append(item)
 
 
-class ProjectItemModel:
+class SubProjectItemModel:
     def __init__(self,
-                 project_id: int=None,
+                 project_id: int = None,
+                 sub_project_id: int=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,
+                 process_status: int = 0,
+                 process_time: datetime = None,
+                 send_status: int = 0,
+                 send_time: datetime = None,
                  item_id: int = None,
                  remark: str = None,
                  update_time: datetime = None):
         self.id = item_id
         self.project_id = project_id
+        self.sub_project_id = sub_project_id
         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.process_status = process_status
+        self.process_time = process_time
+        self.send_status = send_status
+        self.send_time = send_time
         self.remark = remark
         self.update_time = update_time
 
     def __str__(self):
-        return f"SourceItemData(id={self.id}, project_no={self.project_id}, 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})"
+        return f"ProjectItemModel(id={self.id}, project_no={self.sub_project_id}, 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})"
 
     def to_ai_json(self):
         return {

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

@@ -1,22 +0,0 @@
-from datetime import datetime
-
-
-class StandardModel:
-
-    def __init__(self,
-                 device_name: str,
-                 device_model: str=None,
-                 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

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

@@ -1,22 +0,0 @@
-from datetime import datetime
-
-
-class StandardUpdateLogModel:
-
-    def __init__(self,
-                 project_id: int,
-                 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_id = project_id
-        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 - 449
SourceCode/DataMiddleware/app/stores/mysql_store.py

@@ -1,449 +0,0 @@
-from datetime import datetime
-
-from utils.mysql_helper import MySQLHelper
-
-from models.project_data import ProjectModel, ProjectItemModel
-from models.standard_data import StandardModel
-from models.standard_update_log import StandardUpdateLogModel
-
-
-class MysqlStore:
-
-    def __init__(self):
-        self._db_helper = MySQLHelper()
-
-    def query_project_all(self):
-        query = "SELECT project_no,project_name,work_catalog,work_content,standard_version,status,create_time FROM project 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(
-                    ProjectModel(
-                        project_id=item["id"],
-                        project_no=item["project_no"],
-                        project_name=item["project_name"],
-                        work_catalog=item["work_catalog"],
-                        work_content=item["work_content"],
-                        standard_version=item["standard_version"],
-                        status=item["status"],
-                        create_time=item["create_time"],
-                    ))
-            return data
-    def query_project_all_paginated(self, page: int, per_page: int, keyword: str = None, status: int = None) -> (list[ProjectModel], None):
-        offset = (page - 1) * per_page
-        query_count = "SELECT COUNT(*) as count FROM project WHERE is_del=%s"
-        query = "SELECT id,project_no,project_name,work_catalog,work_content,standard_version,status,create_time FROM project 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(
-                    ProjectModel(
-                        project_id=item["id"],
-                        project_no=item["project_no"],
-                        project_name=item["project_name"],
-                        work_catalog=item["work_catalog"],
-                        work_content=item["work_content"],
-                        standard_version=item["standard_version"],
-                        status=item["status"],
-                        create_time=item["create_time"],
-                    ))
-        return data, count
-    def query_project(self, project_id: int, with_items=False) -> ProjectModel | None:
-        query = "SELECT id,project_no,project_name,work_catalog,work_content,standard_version,status,create_time FROM project WHERE is_del=0 AND id = %s"
-        params = (project_id,)
-        query_items = "SELECT id,project_id,device_name,device_model,standard_version,standard_no FROM project_item WHERE project_id = %s"
-        with self._db_helper:
-            result = self._db_helper.fetch_one(query, params)
-            if not result:
-                return None
-            data = ProjectModel(result["project_no"],
-                                result["project_name"],
-                                result["work_catalog"],
-                                result["work_content"],
-                                result["standard_version"],
-                                result["status"],
-                                project_id=result["id"],
-                                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(
-                        ProjectItemModel(
-                            item_id=item["id"],
-                            project_id=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_project_items_by_project_paginated(self, project_id: int, page: int, per_page: int, keyword: str = None) -> (list[ProjectItemModel], int):
-        offset = (page - 1) * per_page
-        query_count = "SELECT COUNT(*) as count FROM project_item WHERE project_id = %s"
-        query = "SELECT id,project_id,device_name,device_model,device_unit,device_count,standard_version,standard_no FROM project_item WHERE project_id = %s"
-        params_count = (project_id,)
-        params = (project_id,)
-
-        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(
-                    ProjectItemModel(
-                        item_id=item["id"],
-                        project_id=item["project_id"],
-                        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_project_items_by_project(self, project_id: int, with_empty=True) -> list[ProjectItemModel]:
-        query = "SELECT id,project_id,device_name,device_model,device_unit,device_count,standard_version,standard_no FROM project_item WHERE project_id = %s"
-        query += "" if with_empty else " AND standard_no !='' AND standard_no IS NOT NULL"
-        params = (project_id,)
-        with self._db_helper:
-            result = self._db_helper.execute_query(query, params)
-            data = []
-            if not result:
-                return data
-            for item in result:
-                data.append(
-                    ProjectItemModel(
-                        item_id=item["id"],
-                        project_id=item["project_id"],
-                        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_project(self, project: ProjectModel) -> bool:
-        query = "INSERT INTO project (project_no,project_name,work_catalog,work_content,standard_version,completion_tokens,prompt_tokens,total_tokens,create_time) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)"
-        params = (
-            project.project_no,
-            project.project_name,
-            project.work_catalog,
-            project.work_content,
-            project.standard_version,
-            project.completion_tokens,
-            project.prompt_tokens,
-            project.total_tokens,
-            datetime.now(),
-        )
-
-        with self._db_helper:
-            self._db_helper.execute_non_query(query, params)
-        self.insert_project_item_list(project)
-        return True
-    def insert_project_item_list(self, project):
-        if len(project.items) <= 0:
-            return
-        query_items = "INSERT INTO project_item (project_id,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 project.items:
-            params_items.append((
-                project.id,
-                item.device_name,
-                item.device_model,
-                item.device_unit,
-                item.device_count,
-                project.standard_version,
-                item.standard_no,
-                datetime.now(),
-            ))
-        with self._db_helper:
-            self._db_helper.execute_many(query_items, params_items)
-    def update_project(self, project: ProjectModel):
-        query = "UPDATE project SET project_name = %s, work_catalog = %s, work_content = %s, standard_version = %s ,status = %s,update_time = %s WHERE id = %s"
-        params = (
-            project.project_name,
-            project.work_catalog,
-            project.work_content,
-            project.standard_version,
-            project.status,
-            datetime.now(),
-            project.id,
-        )
-        with self._db_helper:
-            self._db_helper.execute_non_query(query, params)
-    def update_project_status(self,project_id:int, status:int):
-        query = "UPDATE project SET status = %s,update_time = %s WHERE id = %s"
-        params = (status, datetime.now(), project_id,)
-        with self._db_helper:
-            self._db_helper.execute_non_query(query, params)
-    def re_insert_project(self, project: ProjectModel) -> bool:
-        if not project.id:
-            return False
-        self.update_project(project)
-        query = "DELETE FROM project_item WHERE project_id = %s"
-        params = (project.id,)
-        with self._db_helper:
-            self._db_helper.execute_non_query(query, params)
-        self.insert_project_item_list(project)
-    def delete_project(self, project_id: int) -> bool:
-        query_1 = "UPDATE project SET is_del=1,delete_time=%s WHERE id = %s"
-        # query_2 = "DELETE FROM project_item WHERE project_no = %s"
-        params = (datetime.now(), project_id,)
-        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_project_item_by_id(self, item_id:int) -> ProjectItemModel | None:
-        query = "SELECT id,project_id,device_name,device_model,device_unit,device_count,standard_version,standard_no FROM project_item WHERE id = %s"
-        params = (item_id,)
-        with self._db_helper:
-            result = self._db_helper.fetch_one(query, params)
-            if not result:
-                return None
-            data = ProjectItemModel(
-                item_id=result["id"],
-                project_id=result["project_id"],
-                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_project_item(self, project_id: int, device_name:str, device_model:str) -> ProjectItemModel | None:
-        query = "SELECT id,project_id,device_name,device_model,device_unit,device_count,standard_version,standard_no FROM project_item WHERE project_id = %s AND device_name = %s AND device_model = %s"
-        params = (project_id,device_name, device_model )
-        with self._db_helper:
-            result = self._db_helper.fetch_one(query, params)
-            if not result:
-                return None
-            data = ProjectItemModel(
-                item_id=result["id"],
-                project_id=result["project_id"],
-                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_project_item(self, project_item: ProjectItemModel) -> bool:
-        query = "INSERT INTO project_item (project_id,device_name,device_model,device_unit,device_count,standard_version,standard_no,update_time) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)"
-        params = (
-            project_item.project_id,
-            project_item.device_name,
-            project_item.device_model,
-            project_item.device_unit,
-            project_item.device_count,
-            project_item.standard_version,
-            project_item.standard_no,
-            datetime.now(),
-        )
-        with self._db_helper:
-            self._db_helper.execute_non_query(query, params)
-            return True
-    def update_project_data(self, project_item: ProjectItemModel) -> bool:
-        query = "SElECT standard_no FROM project_item WHERE id = %s"
-        params = (project_item.id,)
-        u_query = "UPDATE project_item SET device_name= %s,device_model= %s,device_unit= %s,device_count= %s,standard_no = %s,update_time = %s WHERE id = %s"
-        u_params = (project_item.device_name, project_item.device_model,project_item.device_unit, project_item.device_count, project_item.standard_no, datetime.now(), project_item.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(
-                    StandardModel(device_name=project_item.device_name,
-                                  device_model=project_item.device_model,
-                                  device_unit=project_item.device_unit,
-                                  standard_version=project_item.standard_version,
-                                  standard_no=project_item.standard_no),
-                    project_item.project_id)
-            return True
-    def delete_project_item_by_id(self, item_id: int):
-        query = "DELETE FROM project_item WHERE id = %s"
-        params = (item_id, )
-        with self._db_helper:
-            self._db_helper.execute_non_query(query, params)
-            return True
-
-    def update_project_item_standard_no_batch(self,items:list[ProjectItemModel]):
-        data =datetime.now()
-        for item in items:
-            query = "UPDATE project_item SET standard_no = %s,update_time = %s WHERE id = %s"
-            params = (item.standard_no, data, item.id)
-            with self._db_helper:
-                self._db_helper.execute_non_query(query, params)
-
-    def query_standard_data_by_no(self, standard_no: str,version:str):
-        query = "SELECT device_model,device_name,device_unit,standard_version,standard_no FROM standard_data WHERE standard_no = %s AND standard_version = %s"
-        params = (standard_no,version,)
-        with self._db_helper:
-            result = self._db_helper.fetch_one(query, params)
-            if not result:
-                return None
-            data = StandardModel(
-                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 query_standard_data(self, device_name: str,
-                            device_model: str) -> list[StandardModel] | 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(StandardModel(
-                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_standard_data(self, standard_data: StandardModel) -> bool:
-        query = "INSERT INTO standard_data (device_name,device_model,device_unit,standard_version,standard_no,create_time) VALUES (%s,%s,%s,%s,%s,%s)"
-        params = (
-            standard_data.device_name,
-            standard_data.device_model,
-            standard_data.device_unit,
-            standard_data.standard_version,
-            standard_data.standard_no,
-            datetime.now(),
-        )
-        with self._db_helper:
-            self._db_helper.execute_non_query(query, params)
-            return True
-
-    def update_standard_data(self, standard_data: StandardModel) -> bool:
-        query = "UPDATE standard_data SET device_name = %s, device_model = %s, device_unit= %s WHERE standard_no = %s AND standard_version = %s"
-        params = (
-            standard_data.device_name,
-            standard_data.device_model,
-            standard_data.device_unit,
-            standard_data.standard_no,
-            standard_data.standard_version,
-        )
-        with self._db_helper:
-            self._db_helper.execute_non_query(query, params)
-            return True
-
-    def insert_or_update_standard_data(self,
-                                       standard_data: StandardModel,
-                                       project_id: int = 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_id:
-                    self.insert_standard_update_log(
-                        StandardUpdateLogModel(
-                            project_id=project_id,
-                            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_id: int) -> list[StandardUpdateLogModel] | None:
-        query = "SELECT project_id,device_name,device_model,device_unit,standard_version,new_standard_no,old_standard_no,create_time FROM standard_update_log WHERE project_id = %s"
-        params = (project_id, )
-        with self._db_helper:
-            results = self._db_helper.execute_query(query, params)
-            if not results:
-                return None
-            data = [StandardUpdateLogModel(**result) for result in results]
-            return data
-
-    def insert_standard_update_log(
-            self, standard_update_log: StandardUpdateLogModel) -> bool:
-        query = "INSERT INTO standard_update_log (project_id,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_id,
-            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

+ 406 - 0
SourceCode/DataMiddleware/app/stores/project_store.py

@@ -0,0 +1,406 @@
+from datetime import datetime
+
+from utils.mysql_helper import MySQLHelper
+
+from models.project_data import ProjectModel, SubProjectModel, SubProjectItemModel
+
+
+class ProjectStore:
+
+    def __init__(self):
+        self._db_helper = MySQLHelper()
+
+    def query_project_all_paginated(self, page: int, per_page: int, keyword: str = None):
+        offset = (page - 1) * per_page
+        sql_count = "SELECT COUNT(*) as count FROM project WHERE is_del=%s"
+        sql = "SELECT id,project_name,description,delete_by,delete_time,create_by,create_time,update_by,update_time FROM project WHERE is_del=%s"
+        params_count = (0,)
+        params = (0,)
+        if keyword:
+            sql_count += " AND (project_name LIKE %s)"
+            sql += " AND (project_name LIKE %s)"
+            params_count += (f"%{keyword}%",)
+            params += (f"%{keyword}%",)
+        sql += " ORDER BY create_time DESC LIMIT %s OFFSET %s"
+        params += (per_page, offset)
+        with self._db_helper:
+            result_count = self._db_helper.fetch_one(sql_count, params_count)
+            count = result_count["count"] if result_count else 0
+            result = self._db_helper.execute_query(sql, params)
+            data = []
+            if not result:
+                return data, count
+            for item in result:
+                data.append(
+                    ProjectModel(
+                        project_id=item["id"],
+                        project_name=item["project_name"],
+                        description=item["description"],
+                        delete_by=item["delete_by"],
+                        delete_time=item["delete_time"],
+                        create_by=item["create_by"],
+                        create_time=item["create_time"],
+                        update_by=item["update_by"],
+                        update_time=item["update_time"],
+                    ))
+            return data, count
+
+    def query_project_by_id(self, project_id: int) -> ProjectModel | None:
+        sql = "SELECT id,project_name,description,delete_by,delete_time,create_by,create_time,update_by,update_time FROM project WHERE is_del=0 AND id = %s"
+        params = (project_id,)
+        with self._db_helper:
+            result = self._db_helper.fetch_one(sql, params)
+            if not result:
+                return None
+            return ProjectModel(
+                project_id=result["id"],
+                project_name=result["project_name"],
+                description=result["description"],
+                delete_by=result["delete_by"],
+                delete_time=result["delete_time"],
+                create_by=result["create_by"],
+                create_time=result["create_time"],
+                update_by=result["update_by"],
+                update_time=result["update_time"],
+            )
+    def insert_project(self, project_data: ProjectModel):
+        sql = "INSERT INTO project (project_name,description,create_by,create_time,update_by,update_time) VALUES (%s,%s,%s,%s,%s,%s)"
+        params = (project_data.project_name,project_data.description, project_data.create_by, datetime.now(), project_data.update_by, datetime.now())
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)
+    def update_project(self, project_data: ProjectModel):
+        sql = "UPDATE project SET project_name=%s,description=%s,update_by=%s,update_time=%s WHERE id=%s"
+        params = (project_data.project_name, project_data.description,project_data.update_by, datetime.now(), project_data.id)
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)
+    def delete_project(self, project_id: int,delete_by: str=""):
+        sql = "UPDATE project SET is_del=1,project.delete_by=%s,delete_time=%s WHERE id=%s"
+        params = (delete_by, datetime.now(), project_id)
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)
+    def query_sub_project_all(self):
+        sql = "SELECT id,project_id,sub_project_name,work_catalog,work_content,standard_version,status,file_paths,create_time FROM sub_project WHERE is_del=0"
+        with self._db_helper:
+            data = []
+            result = self._db_helper.execute_query(sql)
+            if not result:
+                return data
+            for item in result:
+                data.append(
+                    SubProjectModel(
+                        sub_id=item["id"],
+                        project_id=item["project_id"],
+                        sub_project_name=item["sub_project_name"],
+                        work_catalog=item["work_catalog"],
+                        work_content=item["work_content"],
+                        standard_version=item["standard_version"],
+                        status=item["status"],
+                        file_paths=item["file_paths"],
+                        create_time=item["create_time"],
+                    ))
+            return data
+    def query_sub_project_all_paginated(self, project_id: int, page: int, per_page: int, keyword: str = None, status: int = None) -> (list[SubProjectModel], None):
+        offset = (page - 1) * per_page
+        sql_count = "SELECT COUNT(*) as count FROM sub_project WHERE is_del=0 AND project_id=%s"
+        sql = "SELECT id,project_id,sub_project_name,work_catalog,work_content,standard_version,status,file_paths,create_time FROM sub_project WHERE is_del=0 AND project_id=%s"
+        params_count = (project_id,)
+        params =  (project_id,)
+        q_1=" AND (sub_project_name LIKE %s)"
+        q_2=" AND status=%s"
+        if keyword:
+            sql_count += q_1
+            params_count += (f"%{keyword}%",)
+            sql += q_1
+            params += (f"%{keyword}%",)
+
+        if status is not None:
+            sql_count += q_2
+            params_count += (status,)
+            sql += q_2
+            params += (status,)
+
+        sql += " ORDER BY status,create_time DESC LIMIT %s OFFSET %s"
+        params += (per_page, offset)
+
+        with self._db_helper:
+            result_count = self._db_helper.fetch_one(sql_count, params_count)
+            count = result_count["count"] if result_count else 0
+            result = self._db_helper.execute_query(sql, params)
+            data = []
+            if not result:
+                return [], count
+            for item in result:
+                data.append(
+                    SubProjectModel(
+                        sub_id=item["id"],
+                        project_id=item["project_id"],
+                        sub_project_name=item["sub_project_name"],
+                        work_catalog=item["work_catalog"],
+                        work_content=item["work_content"],
+                        standard_version=item["standard_version"],
+                        status=item["status"],
+                        file_paths=item["file_paths"],
+                        create_time=item["create_time"],
+                    ))
+        return data, count
+    def query_sub_project(self, sub_project_id: int, with_items=False) -> SubProjectModel | None:
+        sql = "SELECT id,project_id,sub_project_name,work_catalog,work_content,standard_version,status,file_paths,create_time FROM sub_project WHERE is_del=0 AND id = %s"
+        params = (sub_project_id,)
+        sql_items = "SELECT id,project_id,sub_project_id,device_name,device_model,standard_version,standard_no,process_status,process_time,send_status,send_time FROM sub_project_item WHERE sub_project_id = %s"
+        with self._db_helper:
+            result = self._db_helper.fetch_one(sql, params)
+            if not result:
+                return None
+            data = SubProjectModel(
+                        project_id=result["project_id"],
+                        sub_project_name=result["sub_project_name"],
+                        work_catalog=result["work_catalog"],
+                        work_content=result["work_content"],
+                        standard_version=result["standard_version"],
+                        status=result["status"],
+                        file_paths=result["file_paths"],
+                        sub_id=result["id"],
+                        create_time=result["create_time"])
+            if not with_items:
+                return data
+            items_result = self._db_helper.execute_query(sql_items, params)
+            if items_result:
+                for item in items_result:
+                    data.add_item(
+                        SubProjectItemModel(
+                            item_id=item["id"],
+                            project_id=item["project_id"],
+                            sub_project_id=item["sub_project_id"],
+                            device_name=item["device_name"],
+                            device_model=item["device_model"],
+                            standard_version=item["standard_version"],
+                            standard_no=item["standard_no"],
+                            process_status=item["process_status"],
+                            process_time=item["process_time"],
+                            send_status=item["send_status"],
+                            send_time=item["send_time"],
+                        ))
+            return data
+    def query_sub_project_items_by_project_paginated(self, sub_project_id: int, page: int, per_page: int, keyword: str = None, process_status: int = None,send_status: int = None) -> (list[SubProjectItemModel], int):
+        offset = (page - 1) * per_page
+        sql_count = "SELECT COUNT(*) as count FROM sub_project_item WHERE sub_project_id = %s"
+        sql = "SELECT id,project_id,sub_project_id,device_name,device_model,device_unit,device_count,standard_version,standard_no,process_status,process_time,send_status,send_time FROM sub_project_item WHERE sub_project_id = %s"
+        params_count = (sub_project_id,)
+        params = (sub_project_id,)
+
+        if keyword:
+            sql_count += " AND (device_name LIKE %s OR device_model LIKE %s)"
+            params_count += (f"%{keyword}%", f"%{keyword}%")
+            sql += " AND (device_name LIKE %s OR device_model LIKE %s)"
+            params += (f"%{keyword}%", f"%{keyword}%")
+        if process_status is not None:
+            sql_count += " AND process_status=%s"
+            params_count += (process_status,)
+            sql += " AND process_status=%s"
+            params += (process_status,)
+        if send_status is not None:
+            sql_count += " AND send_status=%s"
+            params_count += (send_status,)
+            sql += " AND send_status=%s"
+            params += (send_status,)
+
+        sql += " 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(sql_count, params_count)
+            count = result_count["count"] if result_count else 0
+            result = self._db_helper.execute_query(sql, params)
+            data = []
+            if not result:
+                return data, count
+            for item in result:
+                data.append(
+                    SubProjectItemModel(
+                        item_id=item["id"],
+                        project_id=item["project_id"],
+                        sub_project_id=item["sub_project_id"],
+                        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"],
+                        process_status=item["process_status"],
+                        process_time=item["process_time"],
+                        send_status=item["send_status"],
+                        send_time=item["send_time"],
+                    ))
+        return data, count
+    def query_sub_project_items_by_project(self, sub_project_id: int, with_empty=True) -> list[SubProjectItemModel]:
+        sql = "SELECT id,project_id,sub_project_id,device_name,device_model,device_unit,device_count,standard_version,standard_no FROM sub_project_item WHERE sub_project_id = %s"
+        sql += "" if with_empty else " AND standard_no !='' AND standard_no IS NOT NULL"
+        params = (sub_project_id,)
+        with self._db_helper:
+            result = self._db_helper.execute_query(sql, params)
+            data = []
+            if not result:
+                return data
+            for item in result:
+                data.append(
+                    SubProjectItemModel(
+                        item_id=item["id"],
+                        project_id=item["project_id"],
+                        sub_project_id=item["sub_project_id"],
+                        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_sub_project(self, sub_project: SubProjectModel) -> int:
+        sql = "INSERT INTO sub_project (project_id,sub_project_name,work_catalog,work_content,standard_version,file_paths,create_time,create_by) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)"
+        params = (
+            sub_project.project_id,
+            sub_project.sub_project_name,
+            sub_project.work_catalog,
+            sub_project.work_content,
+            sub_project.standard_version,
+            sub_project.file_paths,
+            datetime.now(),
+            sub_project.create_by,
+        )
+        with self._db_helper:
+            new_id = self._db_helper.execute_non_query(sql, params)
+        self.insert_sub_project_item_list(sub_project)
+        return new_id
+    def insert_sub_project_item_list(self, sub_project):
+        if len(sub_project.items) <= 0:
+            return
+        sql = "INSERT INTO sub_project_item (project_id,sub_project_id,device_name,device_model,device_unit,device_count,standard_version,standard_no,update_time) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)"
+        params_items = []
+        for item in sub_project.items:
+            params_items.append((
+                sub_project.project_id,
+                sub_project.id,
+                item.device_name,
+                item.device_model,
+                item.device_unit,
+                item.device_count,
+                sub_project.standard_version,
+                item.standard_no,
+                datetime.now(),
+            ))
+        with self._db_helper:
+            self._db_helper.execute_many(sql, params_items)
+    def update_sub_project(self, sub_project: SubProjectModel):
+        sql = "UPDATE sub_project SET sub_project_name = %s, work_catalog = %s, work_content = %s, standard_version = %s ,status = %s,update_time = %s WHERE id = %s"
+        params = (
+            sub_project.sub_project_name,
+            sub_project.work_catalog,
+            sub_project.work_content,
+            sub_project.standard_version,
+            sub_project.status,
+            datetime.now(),
+            sub_project.id,
+        )
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)
+    def update_sub_project_file_path(self, sub_project_id: int, file_paths: str):
+        sql = "UPDATE sub_project SET file_paths = %s WHERE id = %s"
+        params = (file_paths, sub_project_id,)
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)
+    def update_sub_project_status(self, sub_project_id:int, status:int):
+        sql = "UPDATE sub_project SET status = %s,update_time = %s WHERE id = %s"
+        params = (status, datetime.now(), sub_project_id,)
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)
+    def re_insert_sub_project(self, sub_project: SubProjectModel) -> bool:
+        if not sub_project.id:
+            return False
+        self.update_sub_project(sub_project)
+        sql = "DELETE FROM sub_project_item WHERE sub_project_id = %s"
+        params = (sub_project.id,)
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)
+        self.insert_sub_project_item_list(sub_project)
+    def delete_sub_project(self, sub_project_id: int, delete_by: str="") -> bool:
+        sql = "UPDATE sub_project SET is_del=1,delete_by=%s,delete_time=%s WHERE id = %s"
+        params = (delete_by, datetime.now(), sub_project_id,)
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)
+            return True
+    def query_sub_project_item_by_id(self, item_id:int) -> SubProjectItemModel | None:
+        sql = "SELECT id,project_id,sub_project_id,device_name,device_model,device_unit,device_count,standard_version,standard_no,process_status,process_time,send_status,send_time FROM sub_project_item WHERE id = %s"
+        params = (item_id,)
+        with self._db_helper:
+            result = self._db_helper.fetch_one(sql, params)
+            if not result:
+                return None
+            data = SubProjectItemModel(
+                item_id=result["id"],
+                project_id=result["project_id"],
+                sub_project_id=result["project_id"],
+                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"],
+                process_status=result["process_status"],
+                process_time=result["process_time"],
+                send_status=result["send_status"],
+                send_time=result["send_time"],
+                update_time=datetime.now()
+            )
+            return data
+    def insert_sub_project_item(self, project_item: SubProjectItemModel):
+        sql = "INSERT INTO sub_project_item (project_id,sub_project_id,device_name,device_model,device_unit,device_count,standard_version,standard_no,process_status,send_status,update_time) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
+        params = (
+            project_item.project_id,
+            project_item.sub_project_id,
+            project_item.device_name,
+            project_item.device_model,
+            project_item.device_unit,
+            project_item.device_count,
+            project_item.standard_version,
+            project_item.standard_no,
+            project_item.process_status,
+            project_item.send_status,
+            datetime.now(),
+        )
+        with self._db_helper:
+           return self._db_helper.execute_non_query(sql, params)
+    def update_sub_project_item(self, project_item: SubProjectItemModel) -> bool:
+        sql = "UPDATE sub_project_item SET device_name= %s,device_model= %s,device_unit= %s,device_count= %s,standard_no = %s,update_time = %s WHERE id = %s"
+        params = (
+            project_item.device_name,
+            project_item.device_model,
+            project_item.device_unit,
+            project_item.device_count,
+            project_item.standard_no,
+            datetime.now(),
+            project_item.id
+        )
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)
+            return True
+    def delete_sub_project_item_by_id(self, item_id: int):
+        sql = "DELETE FROM sub_project_item WHERE id = %s"
+        params = (item_id, )
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)
+            return True
+    def update_sub_project_item_standard_no(self, item_id:int, standard_no:str):
+        sql = "UPDATE sub_project_item SET standard_no=%s,process_status = 2,process_time = %s,update_time=%s WHERE id = %s"
+        params = (standard_no, datetime.now(), datetime.now(), item_id,)
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)
+    def update_sub_project_item_process_status(self, item_id:int, status:int):
+        sql = "UPDATE sub_project_item SET process_status = %s,process_time = %s WHERE id = %s"
+        params = (status, datetime.now(), item_id,)
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)
+    def update_sub_project_item_send_status(self, item_id:int, status:int):
+        sql = "UPDATE sub_project_item SET send_status = %s,send_time = %s WHERE id = %s"
+        params = (status, datetime.now(), item_id,)
+        with self._db_helper:
+            self._db_helper.execute_non_query(sql, params)

+ 5 - 1
SourceCode/DataMiddleware/app/ui/__init__.py

@@ -1,13 +1,17 @@
 import os,utils
 from flask import Flask
 
+def basename_filter(path):
+    return os.path.basename(path)
+
 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 .project_views import project as project_blueprint
+    from .views_project import project as project_blueprint
     web_app.register_blueprint(project_blueprint)
+    web_app.jinja_env.filters['basename'] = basename_filter
 
     return web_app

+ 0 - 150
SourceCode/DataMiddleware/app/ui/project_services.py

@@ -1,150 +0,0 @@
-import threading,utils
-
-from models.project_data import ProjectModel, ProjectItemModel
-from stores.mysql_store import MysqlStore
-import data_collect,data_process,data_send
-
-
-class SourceDataService:
-
-    def __init__(self):
-        self._store = MysqlStore()
-        self._logger = utils.get_logger()
-
-    def get_all_project_paginated(self, page: int, per_page: int, keyword: str = None, status: int = None) ->(list[ProjectModel], int):
-        project_list, total_count = self._store.query_project_all_paginated(page, per_page, keyword, status)
-        return project_list, total_count
-
-    def get_all_project(self) ->list[ProjectModel]:
-        project_list = self._store.query_project_all()
-        return project_list
-
-    def get_project_by_id(self, project_id:int) ->ProjectModel:
-        project = self._store.query_project(project_id)
-        return project
-
-    def add_project(self, project:ProjectModel):
-        self._store.insert_project(project)
-
-
-    def update_project(self, project:ProjectModel):
-        self._store.update_project(project)
-
-    def start_task(self, project_id: int) -> (bool, str):
-        data = self._store.query_project(project_id)
-        if data:
-            if data.status == 21 or  data.status == 31:
-                return False, '正在采集数据中'
-            if data.status == 22 or data.status == 32:
-                return False, '正在分析处理中'
-            if data.status == 32 or data.status == 33:
-                return False, '正在上传数据中'
-            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: ProjectModel) :
-        # 启动分析处理
-        if data_process.process_project(project):
-            # 更新远程数据
-            data_send.send_project_data(project)
-            self._store.update_project_status(project.id,5)
-
-    def process_project(self, project_id:int):
-        data = self._store.query_project(project_id)
-        if data:
-            if data.status == 21 or data.status == 31:
-                return False, '正在采集数据中'
-            if data.status == 22 or data.status == 32:
-                return False, '正在分析处理中'
-            if data.status == 32 or data.status == 33:
-                return False, '正在上传数据中'
-            thread = threading.Thread(target=self._process_project, args=(data,))
-            thread.start()
-            return True, ''
-        else:
-            return False, '项目不存在'
-
-    def _process_project(self, project: ProjectModel):
-        if data_process.process_data(project):
-            return True, ''
-        else:
-            self._logger.error(f"分析处理失败:{project.project_no}")
-        return False, '分析处理失败'
-    def start_send_data(self, project_id: int) -> (bool, str):
-        data = self._store.query_project(project_id)
-        if data:
-            if data.status == 21 or data.status == 31:
-                return False, '正在采集数据中'
-            if data.status == 22 or data.status == 32:
-                return False, '正在分析处理中'
-            if data.status == 32 or data.status == 33:
-                return False, '正在上传数据中'
-            thread = threading.Thread(target=self._send_project_data, args=(data,))
-            thread.start()
-
-
-    def _send_project_data(self, project: ProjectModel):
-        if data_send.send_project_data(project):
-            self._store.update_project_status(project.id,5)
-            return True, ''
-        else:
-            return False, '上传数据失败'
-
-    def delete_project(self, project_id:int):
-        project = self._store.query_project(project_id)
-        if project:
-            self._store.delete_project(project_id)
-            return True,''
-        else:
-            return False, '项目不存在'
-
-    def get_project_item_list_by_project_paginated(self, project_id: int, page: int, per_page: int, keyword: str = None) -> (list[ProjectItemModel], int):
-        return self._store.query_project_items_by_project_paginated(project_id, page, per_page, keyword)
-    def get_project_item_list_by_project_id(self, project_id:int) ->list[ProjectItemModel]:
-        return  self._store.query_project_items_by_project(project_id)
-
-    def get_project_item_by_id(self, item_id: int):
-        return self._store.query_project_item_by_id(item_id)
-
-    def add_project_item(self, item:ProjectItemModel):
-        project_item = self._store.query_project_item(item.project_id, item.device_name, item.device_model)
-        if project_item:
-            return False
-        else:
-            project = self._store.query_project(item.project_id)
-            project_item = ProjectItemModel(
-                project_id=item.project_id,
-                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_project_item(project_item)
-            return True
-    def update_project_item(self, item:ProjectItemModel):
-        project_item = self._store.query_project_item_by_id(item.id)
-        if project_item:
-            project_item.device_name = item.device_name
-            project_item.device_model = item.device_model
-            project_item.device_unit = item.device_unit
-            project_item.device_count = item.device_count
-            project_item.standard_no = item.standard_no
-            self._store.update_project_data(project_item)
-            #TODO 更新标准数据库
-
-            #TODO 更新远程数据
-
-            return True
-        else:
-            return False
-    def delete_project_item(self, item_id:int):
-        project_item = self._store.query_project_item_by_id(item_id)
-        if project_item:
-            self._store.delete_project_item_by_id(project_item.id)
-            return True
-        else:
-            return False

+ 0 - 182
SourceCode/DataMiddleware/app/ui/project_views.py

@@ -1,182 +0,0 @@
-from flask import render_template, url_for, redirect, request, jsonify
-from flask import Blueprint
-import os,utils
-
-from .project_services import SourceDataService
-from models.project_data import ProjectModel,ProjectItemModel
-
-SourceDataService = SourceDataService()  # 确保 SourceDataService 是一个实例
-
-# 创建一个名为 'project' 的蓝图
-project = Blueprint('project', __name__, template_folder='templates/project')
-
-@project.route('/')
-def index():
-    return "Hello, World!"
-
-@project.route('/project_list')
-def project_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_project_paginated(page, per_page, keyword, None if status == -1 else status)  # 传递 status 参数
-    return render_template('project_list.html', project_list=data_list, keyword=keyword, page=page, per_page=per_page, total_count=total_count, status=status)  # 传递 status 参数到模板
-
-@project.route('/project_item_list/<project_id>')
-def project_item_list(project_id:int):
-    keyword = request.args.get('k', '', type=str)
-    page = request.args.get('p', 1, type=int)
-    per_page = request.args.get('pp', 15, type=int)
-    project_data= SourceDataService.get_project_by_id(project_id)
-    project_items, total_count = SourceDataService.get_project_item_list_by_project_paginated(project_id, page, per_page, keyword)
-    return render_template('project_item_list.html', project=project_data, project_items=project_items, keyword=keyword, page=page, per_page=per_page, total_count=total_count)
-
-@project.route('/save_project', methods=['POST'])
-def save_project():
-    project_id = request.form.get('id')
-    project_no = request.form.get('project_no')
-    if not project_no:
-        return jsonify({'success': False, 'error': 'ProjectNo不能为空'}), 400
-    project_name = request.form.get('project_name')
-    standard_version = request.form.get('standard_version')
-    work_catalog = request.form.get('work_catalog')
-    work_content = request.form.get('work_content')
-    delete_old_data = request.form.get('delete_old_data') == 'true'
-    # 处理多文件上传
-    project_files = request.files.getlist('project_files')
-    project_data = None
-    if project_id:
-        project_data = SourceDataService.get_project_by_id(int(project_id))
-    if len(project_files)<=0 and project_id and delete_old_data:
-        return jsonify({'success': False, 'error': '请上传项目数据文件'}), 400
-    elif project_files:
-        try:
-            base_path = os.path.abspath(utils.get_config_value("file.source_path","./temp_files"))
-            if not os.path.exists(base_path):
-                os.makedirs(base_path)
-            # 如果选择删除旧数据且目录存在,则删除目录下的所有文件
-            if delete_old_data and project_data :
-                del_path = os.path.join(base_path, f"project/{project_data.project_no}")
-                if os.path.exists(del_path):
-                    for filename in os.listdir(del_path):
-                        file_path = os.path.join(del_path, filename)
-                        try:
-                            if os.path.isfile(file_path):
-                                os.remove(file_path)
-                        except Exception as e:
-                            return jsonify({'success': False, 'error': f'删除旧文件失败:{str(e)}'}), 500
-            path = os.path.join(base_path, f"project/{project_no}")
-            if not os.path.exists(path):
-                os.makedirs(path)
-            for project_file in project_files:
-                filename = project_file.filename
-                if filename and '.' in filename:
-                    file_ext = filename.rsplit('.', 1)[1].lower()
-                    if file_ext not in {'xlsx', 'xls', 'csv'}:
-                        return jsonify({'success': False, 'error': f'不支持的文件格式:{filename},请上传xlsx,xls,csv文件'}), 400
-                    file_path = os.path.join(path, filename)
-                    project_file.save(file_path)
-                    
-        except PermissionError:
-            return jsonify({'success': False, 'error': '文件保存失败:权限不足,请确保应用程序具有写入权限'}), 403
-        except Exception as e:
-            return jsonify({'success': False, 'error': f'文件保存失败:{str(e)}'}), 500
-    elif not project_id:
-        return jsonify({'success': False, 'error': '请上传项目数据文件'}), 400
-
-    if project_data:
-        project_data.project_no = project_no
-        project_data.project_name = project_name
-        project_data.standard_version = standard_version
-        project_data.work_catalog = work_catalog
-        project_data.work_content = work_content
-        SourceDataService.update_project(project_data)
-    else:
-        project_data = ProjectModel(project_no=project_no,
-                                    project_name=project_name,
-                                    standard_version=standard_version,
-                                    work_catalog=work_catalog,
-                                    work_content=work_content,
-                                    status=0)
-        SourceDataService.add_project(project_data)
-    return jsonify({'success': True})
-
-@project.route('/update_project', methods=['POST'])
-def update_project():
-    req = request.get_json()
-    project_id = req.get('id')
-    project_no = req.get('project_no')
-    project_name= req.get('project_name')
-    standard_version= req.get('standard_version')
-    work_catalog = req.get('work_catalog')
-    work_content = req.get('work_content')
-    project_data = SourceDataService.get_project_by_id(project_id)
-    if not project_data:
-        return jsonify({'success': False, 'error': '项目不存在'}), 404
-    data= ProjectModel(project_id=project_id,
-                       project_no=project_no,
-                       project_name=project_name,
-                       standard_version=standard_version,
-                       work_catalog=work_catalog,
-                       work_content=work_content,
-                       status=project_data.status)
-    SourceDataService.update_project(data)
-    return jsonify({'success': True})
-
-@project.route('/start_project_task/<project_id>', methods=['POST'])
-def start_project_task(project_id:int):
-    result, err_msg = SourceDataService.start_task(project_id)
-    return jsonify({'success': result, 'error': err_msg})
-
-@project.route('/process_project/<project_id>', methods=['POST'])
-def process_project(project_id:int):
-    result, err_msg = SourceDataService.process_project(project_id)
-    return jsonify({'success': result, 'error': err_msg})
-@project.route('/send_project/<project_id>', methods=['POST'])
-def send_project(project_id:int):
-    result, err_msg = SourceDataService.start_send_data(project_id)
-    return jsonify({'success': result, 'error': err_msg})
-@project.route('/delete_project/<project_id>', methods=['POST'])
-def delete_project(project_id:int):
-    result, err_msg =  SourceDataService.delete_project(project_id)
-    return jsonify({'success': result, 'error': err_msg})
-
-@project.route('/add_project_item', methods=['POST'])
-def add_project_item():
-    data = request.get_json()
-    project_id = data.get('project_id')
-    device_name = data.get('device_name')
-    device_model = data.get('device_model')
-    device_unit = data.get('device_unit')
-    device_count = data.get('device_count')
-    standard_no = data.get('standard_no')
-    new_item = ProjectItemModel(project_id=project_id, device_name=device_name, device_model=device_model, device_unit=device_unit, standard_no=standard_no)
-    item_id = SourceDataService.add_project_item(new_item)
-    return jsonify({'success': True, 'id': item_id})
-
-@project.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
-    device_name = data.get('device_name')
-    device_model = data.get('device_model')
-    device_unit = data.get('device_unit')
-    device_count = data.get('device_count')
-    standard_no = data.get('standard_no')
-    # if not standard_no:
-    #     return jsonify({'success': False, 'error': 'Standard No is missing'}), 400
-    item =ProjectItemModel(item_id=item_id,project_id=project_id, device_name=device_name, device_model=device_model, device_unit=device_unit, standard_no=standard_no)
-    SourceDataService.update_project_item(item)
-    return jsonify({'success': True})
-
-@project.route('/delete_project_item/<item_id>', methods=['POST'])
-def delete_project_item_route(item_id):
-    project_item = SourceDataService.get_project_item_by_id(item_id)
-    SourceDataService.delete_project_item(item_id)
-    return redirect(url_for('project.project_item_list', project_no=project_item.project_id))
-
-
-

+ 265 - 0
SourceCode/DataMiddleware/app/ui/services_project.py

@@ -0,0 +1,265 @@
+import threading,utils,os
+
+from models.project_data import ProjectModel,SubProjectModel, SubProjectItemModel
+from stores.project_store import ProjectStore
+import data_process,data_send
+
+
+class ProjectService:
+
+    def __init__(self):
+        self._project_store = ProjectStore()
+        self._logger = utils.get_logger()
+    def get_all_project_paginated(self, page: int, per_page: int, keyword: str = None) ->(list[ProjectModel], int):
+        sub_project_list, total_count = self._project_store.query_project_all_paginated(page, per_page, keyword)
+        return sub_project_list, total_count
+
+    def get_project_by_id(self ,project_id:int):
+        project = self._project_store.query_project_by_id(project_id)
+        return project
+
+    def add_project(self, project:ProjectModel):
+        try:
+            project.create_by = 'admin' if project.create_by is None else project.create_by
+            self._project_store.insert_project(project)
+            return True, ''
+        except Exception as e:
+            self._logger.error(f"添加项目失败:{e}")
+            return False, '添加项目失败'
+    def update_project(self, project:ProjectModel):
+        project = self._project_store.query_project_by_id(project.id)
+        if project:
+            self._project_store.update_project(project)
+            return True, ''
+        else:
+            return False, '项目不存在'
+    def delete_project(self, project_id:int):
+        project = self._project_store.query_project_by_id(project_id)
+        if project:
+            self._project_store.delete_project(project_id)
+            return True,''
+        else:
+            return False, '项目不存在'
+
+    def get_all_sub_project_paginated(self, project_id: int, page: int, per_page: int, keyword: str = None, status: int = None) ->(list[SubProjectModel], int):
+        sub_project_list, total_count = self._project_store.query_sub_project_all_paginated(project_id, page, per_page, keyword, status)
+        return sub_project_list, total_count
+
+    def get_all_sub_project(self) ->list[SubProjectModel]:
+        sub_project_list = self._project_store.query_sub_project_all()
+        return sub_project_list
+
+    def get_sub_project_by_id(self, sub_project_id:int) ->SubProjectModel:
+        project = self._project_store.query_sub_project(sub_project_id)
+        return project
+
+    def save_sub_project(self, sub_project:SubProjectModel, project_files: list, delete_old: bool):
+        if sub_project.id:
+            sub_data = self._project_store.query_sub_project(sub_project.id)
+            if not sub_data:
+                return False, '子项目不存在'
+            sub_data.sub_project_name = sub_project.sub_project_name
+            sub_data.standard_version = sub_project.standard_version
+            sub_data.work_catalog = sub_project.work_catalog
+            sub_data.work_content = sub_project.work_content
+            sub_data.status = 0
+            self.update_sub_project(sub_data)
+        else:
+            sub_data = SubProjectModel(
+                project_id=sub_project.project_id,
+                sub_project_name=sub_project.sub_project_name,
+                standard_version=sub_project.standard_version,
+                work_catalog=sub_project.work_catalog,
+                work_content=sub_project.work_content,
+                file_paths="",
+                status=0
+            )
+            new_id =  self.add_sub_project(sub_data)
+            sub_data.id = new_id
+        try:
+            # 处理文件上传逻辑
+            if project_files:
+                file_paths = self._process_file_upload(sub_data, project_files, delete_old)
+                self.update_sub_project_file_paths(sub_data.id, file_paths)
+            elif delete_old:
+                return False,"请上传项目数据文件"
+            return True, ''
+        except ValueError as ve:
+            return False,str(ve)
+        except Exception as e:
+            return False,f'服务器错误: {str(e)}'
+
+    def _process_file_upload(self,sub_project: SubProjectModel, files: list,  delete_old: bool) -> str:
+        """处理文件上传流程"""
+        base_path = utils.get_config_value('file.source_path', './temp_files')
+        sub_project_dir = os.path.join(base_path, f'{sub_project.get_path()}')
+        os.makedirs(sub_project_dir, exist_ok=True)
+        self._logger.info(f"保存处理文件,项目ID:{sub_project.project_id},子项目ID:{sub_project.id}")
+        if delete_old:
+            if os.path.exists(sub_project_dir):
+                for filename in os.listdir(sub_project_dir):
+                    file_path = os.path.join(sub_project_dir, filename)
+                    if os.path.isfile(file_path):
+                        os.remove(file_path)
+        file_paths =[] if delete_old or not sub_project.file_paths else sub_project.file_paths.split(',')
+        for file in files:
+            if not file.filename:
+                continue
+            allowed_ext = {'xlsx', 'xls', 'csv'}
+            ext = file.filename.rsplit('.', 1)[-1].lower()
+            if ext not in allowed_ext:
+                continue
+            file_path = os.path.join(sub_project_dir, file.filename)
+            file_path = file_path.replace('\\', '/')
+            file.save(file_path)
+            file_paths.append(file_path)
+        return ','.join(file_paths)
+
+    def add_sub_project(self, sub_project:SubProjectModel):
+       return self._project_store.insert_sub_project(sub_project)
+
+    def update_sub_project(self, sub_project:SubProjectModel):
+        self._project_store.update_sub_project(sub_project)
+
+    def update_sub_project_file_paths(self, sub_project_id: int, paths: str):
+        self._project_store.update_sub_project_file_path(sub_project_id, paths)
+    def start_sub_project_task(self, sub_project_id: int) -> (bool, str):
+        data = self._project_store.query_sub_project(sub_project_id)
+        if data:
+            if data.status == 21 or  data.status == 31:
+                return False, '正在采集数据中'
+            if data.status == 22 or data.status == 32:
+                return False, '正在分析处理中'
+            if data.status == 32 or data.status == 33:
+                return False, '正在上传数据中'
+            thread = threading.Thread(target = self._process_and_send_sub_project, args=(data,))
+            thread.start()
+            return True, ''
+        else:
+            return False, '项目不存在'
+    def _process_and_send_sub_project(self, sub_project: SubProjectModel) :
+        # 启动分析处理
+        if data_process.process_project(sub_project):
+            # 更新远程数据
+            data_send.send_project_data(sub_project)
+            self._project_store.update_sub_project_status(sub_project.id, 5)
+
+    def process_sub_project(self, sub_project_id:int):
+        data = self._project_store.query_sub_project(sub_project_id)
+        if data:
+            if data.status == 21 or data.status == 31:
+                return False, '正在采集数据中'
+            if data.status == 22 or data.status == 32:
+                return False, '正在分析处理中'
+            if data.status == 32 or data.status == 33:
+                return False, '正在上传数据中'
+            thread = threading.Thread(target=self._process_sub_project, args=(data,))
+            thread.start()
+            return True, ''
+        else:
+            return False, '项目不存在'
+
+    def _process_sub_project(self, sub_project: SubProjectModel):
+        if data_process.process_data(sub_project):
+            return True, ''
+        else:
+            self._logger.error(f"分析处理失败:{sub_project.sub_project_name}")
+        return False, '分析处理失败'
+    def start_send_sub_project(self, sub_project_id: int) -> (bool, str):
+        data = self._project_store.query_sub_project(sub_project_id)
+        if data:
+            if data.status == 21 or data.status == 31:
+                return False, '正在采集数据中'
+            if data.status == 22 or data.status == 32:
+                return False, '正在分析处理中'
+            if data.status == 32 or data.status == 33:
+                return False, '正在上传数据中'
+            thread = threading.Thread(target=self._send_sub_project_data, args=(data,))
+            thread.start()
+
+    def _send_sub_project_data(self, project: SubProjectModel):
+        if data_send.send_project_data(project):
+            self._project_store.update_sub_project_status(project.id, 5)
+            return True, ''
+        else:
+            return False, '上传数据失败'
+
+    def delete_sub_project(self, sub_project_id:int):
+        project = self._project_store.query_sub_project(sub_project_id)
+        if project:
+            self._project_store.delete_sub_project(sub_project_id)
+            return True,''
+        else:
+            return False, '项目不存在'
+
+    def get_sub_project_item_list_by_sub_project_paginated(self, project_id: int, page: int, per_page: int, keyword: str = None, process_status:int = None, send_status:int=None) -> (list[SubProjectItemModel], int):
+        return self._project_store.query_sub_project_items_by_project_paginated(project_id, page, per_page, keyword, process_status, send_status)
+    def get_sub_project_item_list_by_sub_project_id(self, project_id:int) ->list[SubProjectItemModel]:
+        return  self._project_store.query_sub_project_items_by_project(project_id)
+
+    def get_sub_project_item_by_id(self, item_id: int):
+        return self._project_store.query_sub_project_item_by_id(item_id)
+
+    def add_sub_project_item(self, item:SubProjectItemModel):
+        project = self._project_store.query_sub_project(item.sub_project_id)
+        project_item = SubProjectItemModel(
+            project_id=project.project_id,
+            sub_project_id=item.sub_project_id,
+            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,
+        )
+        return self._project_store.insert_sub_project_item(project_item)
+
+    def update_sub_project_item(self, item:SubProjectItemModel):
+        project_item = self._project_store.query_sub_project_item_by_id(item.id)
+        if project_item:
+            project_item.device_name = item.device_name
+            project_item.device_model = item.device_model
+            project_item.device_unit = item.device_unit
+            project_item.device_count = float(item.device_count)
+            project_item.standard_no = item.standard_no
+            self._project_store.update_sub_project_item(project_item)
+            return True
+        else:
+            return False
+    def delete_sub_project_item(self, item_id:int):
+        project_item = self._project_store.query_sub_project_item_by_id(item_id)
+        if project_item:
+            self._project_store.delete_sub_project_item_by_id(project_item.id)
+            return True
+        else:
+            return False
+
+    def start_process_item(self, item_id:int):
+        project_item = self._project_store.query_sub_project_item_by_id(item_id)
+        if not project_item:
+            return False, '项目不存在'
+        thread = threading.Thread(target=self._process_sub_project_item, args=(project_item,))
+        thread.start()
+        return True,""
+
+    def _process_sub_project_item(self, sub_project_item:SubProjectItemModel):
+        if data_process.process_item(sub_project_item):
+            return data_send.send_item_data(sub_project_item),""
+        else:
+            self._logger.error(f"分析处理失败:{sub_project_item.device_name}")
+            return False, '分析处理失败'
+
+    def start_send_item(self, item_id:int):
+        project_item = self._project_store.query_sub_project_item_by_id(item_id)
+        if not project_item:
+            return False, '项目不存在'
+        thread = threading.Thread(target=self._send_sub_project_item, args=(project_item,))
+        thread.start()
+        return True,""
+
+    def _send_sub_project_item(self, sub_project_item:SubProjectItemModel):
+        if data_send.send_item_data(sub_project_item):
+            return True, ''
+        else:
+            self._logger.error(f"上传失败:{sub_project_item.device_name}")
+            return False, '上传数据失败'
+

+ 0 - 143
SourceCode/DataMiddleware/app/ui/static/project_item_list.js

@@ -1,143 +0,0 @@
-function addNewItem(project_id) {
-	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 count"><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_id}')">确定</button>
-                <button class="btn btn-warning" onclick="cancelNewChanges(this.parentNode.parentNode.parentNode)">取消</button>
-            </span>
-        </td>
-    `
-	table.insertBefore(newRow, table.firstChild)
-}
-
-function saveItemCreate(row, project_id) {
-	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 deviceCount = row.querySelector('td.count input').value
-	const standardNo = row.querySelector('td.standard_no input').value
-	if (deviceName === '') {
-		alert('名称不能为空')
-		return
-	}
-	if (deviceModel === '') {
-		alert('规格型号不能为空')
-		return
-	}
-	if (deviceCount === '') {
-		alert('数量不能为空')
-		return
-	}
-	if (deviceUnit === '') {
-		alert('单位不能为空')
-		return
-	}
-	// if (standardNo === '') {
-	//     alert('标准编号不能为空');
-	//     return;
-	// }
-
-	fetch(`/add_project_item`, {
-		method: 'POST',
-		headers: {
-			'Content-Type': 'application/json',
-		},
-		body: JSON.stringify({ project_id: project_id, device_name: deviceName, device_model: deviceModel, device_unit: deviceUnit, device_count: deviceCount, standard_no: standardNo }),
-	})
-		.then((response) => response.json())
-		.then((data) => {
-			if (data.success) {
-				alert('添加成功')
-				window.location.href = `/project_item_list/${project_id}`
-			} else {
-				alert('添加失败:' + data.error)
-			}
-		})
-		.catch((error) => {
-			console.error('添加失败:', error)
-			alert('添加失败')
-		})
-}
-
-function saveItemEdit(row, itemId) {
-	const deviceName = row.querySelector('td.name input').value
-	const deviceModel = row.querySelector('td.model input').value
-	const standardNo = row.querySelector('.standard_no input').value
-	const deviceUnit = row.querySelector('.unit input').value
-	const deviceCount = row.querySelector('.count input').value
-	// if (standardNo === '') {
-	//     alert('标准编号不能为空');
-	//     return;
-	// }
-	if (deviceCount === '') {
-		alert('数量不能为空')
-		return
-	}
-	if (deviceUnit === '') {
-		alert('单位不能为空')
-		return
-	}
-	fetch(`/update_data_item`, {
-		method: 'POST',
-		headers: {
-			'Content-Type': 'application/json',
-		},
-		body: JSON.stringify({ id: itemId, device_name: deviceName, device_model: deviceModel, standard_no: standardNo, device_count: deviceCount, device_unit: deviceUnit }),
-	})
-		.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_project_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('删除失败')
-			})
-	}
-}

+ 48 - 185
SourceCode/DataMiddleware/app/ui/static/project_list.js

@@ -1,212 +1,75 @@
 function addProject() {
-	const editBox = document.querySelector('div.edit-box')
-	const inputs = editBox.querySelectorAll('.form-control')
+	const modal = document.querySelector('#modal-project')
+	const inputs = modal.querySelectorAll('.form-control')
 	inputs.forEach((input) => {
 		input.value = ''
 	})
-	// 移除已存在的delete_old_data复选框
-	const existingCheckbox = document.getElementById('delete_old_data')
-	if (existingCheckbox) {
-		existingCheckbox.parentNode.remove()
-	}
-	const file = document.getElementById('new_project_file').parentNode
-	file.innerHTML = `<label for="new_project_file">项目数据:</label><input type="file" id="new_project_file" class="form-control" accept=".xlsx,.xls,.csv" multiple>`
-
-	editBox.classList.add('show')
-}
-
-function updateProject(row, id) {
-	document.getElementById('project_id').value = id
-	document.getElementById('new_project_no').value = row.querySelector('.project_no .form-control').value
-	document.getElementById('new_project_name').value = row.querySelector('.project_name .form-control').value
-	const file = document.getElementById('new_project_file').parentNode
-	file.innerHTML = `<label for="new_project_file">项目数据:</label><input type="file" id="new_project_file" class="form-control" accept=".xlsx,.xls,.csv" multiple>`
-	// 移除已存在的delete_old_data复选框
-	const existingCheckbox = document.getElementById('delete_old_data')
-	if (existingCheckbox) {
-		existingCheckbox.parentNode.remove()
-	}
-	const checkboxDiv = document.createElement('div')
-	checkboxDiv.innerHTML = `<label for="delete_old_data">删除原数据:</label><input type="checkbox" id="delete_old_data" class="form-control" style="width: auto;flex: 0;">`
-	file.parentNode.insertBefore(checkboxDiv, file.nextSibling)
-	document.getElementById('new_work_catalog').value = row.querySelector('.work_catalog .form-control').value
-	document.getElementById('new_work_content').value = row.querySelector('.work_content .form-control').value
-	document.getElementById('new_standard_version').value = row.querySelector('.standard_version .form-control').value
-	const editBox = document.querySelector('div.edit-box')
-	editBox.classList.add('show')
-}
-function saveProject() {
-	const id = document.getElementById('project_id').value
-	const no = document.getElementById('new_project_no').value
-	const name = document.getElementById('new_project_name').value
-	const files = document.getElementById('new_project_file').files
-	const catalog = document.getElementById('new_work_catalog').value
-	const content = document.getElementById('new_work_content').value
-	const version = document.getElementById('new_standard_version').value
-
-	if (no === '') {
-		alert('项目编号不能为空')
-		return
-	}
-	if (name === '') {
-		alert('项目名称不能为空')
-		return
-	}
-	if (files.length === 0 && !id) {
-		alert('请选择项目数据文件')
-		return
-	}
-	if (catalog === '') {
-		alert('工作目录不能为空')
-		return
-	}
-	if (content === '') {
-		alert('工作内容不能为空')
-		return
-	}
-
-	const formData = new FormData()
-	formData.append('id', id)
-	formData.append('project_no', no)
-	formData.append('project_name', name)
-	for (let i = 0; i < files.length; i++) {
-		formData.append('project_files', files[i])
-	}
-	formData.append('work_catalog', catalog)
-	formData.append('work_content', content)
-	formData.append('standard_version', version)
-	if (document.getElementById('delete_old_data')){
-		formData.append('delete_old_data', document.getElementById('delete_old_data').checked)
-	}
-
-	fetch('/save_project', {
-		method: 'POST',
-		body: formData,
-	})
-		.then((response) => response.json())
-		.then((data) => {
-			if (data.success) {
-				alert('保存成功')
-				window.location.reload()
-			} else {
-				alert('保存失败:' + data.error)
-			}
-		})
-		.catch((error) => {
-			console.error('保存失败:', error)
-			alert('保存失败')
-		})
-}
-function cancelAdd() {
-	const editBox = document.querySelector('div.edit-box')
-	editBox.classList.remove('show')
-}
-
-function saveProjectUpdate(row, id) {
-	const no = row.querySelector('.project_no .form-control').value
-	const name = row.querySelector('.project_name .form-control').value
-	const work_catalog = row.querySelector('.work_catalog .form-control').value
-	const work_content = row.querySelector('.work_content .form-control').value
-	const version = row.querySelector('.standard_version .form-control').value
-	if (no === '') {
-		alert('名称不能为空')
-		return
-	}
-	if (name === '') {
-		alert('名称不能为空')
-		return
-	}
-	if (version === '') {
-		alert('版本不能为空')
-		return
-	}
-	fetch('/update_project', {
-		method: 'POST',
-		headers: {
-			'Content-Type': 'application/json',
-		},
-		body: JSON.stringify({
-			id: id,
-			project_no: no,
-			project_name: name,
-			work_catalog: work_catalog,
-			work_content: work_content,
-			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('更新失败')
-		})
+	show_add_moadl(modal)
 }
-
-function confirmStartTask(id){
-	_confirm('确定要开始任务吗?',`/start_project_task/${id}`)
+function editProject(projectId) {
+	const row = document.querySelector(`tr[data-id='${projectId}']`)
+	const modal = document.querySelector('#modal-project')
+	modal.querySelector('#project_id').value = projectId
+	modal.querySelector('#project_name').value = row.querySelector('.project-name .form-control').value
+	modal.querySelector('#project_desc').value = row.querySelector('.project-desc .form-control').value
+	show_edit_moadl(modal)
 }
 
-function confirmReStartTask(id){
-	_confirm('确定要重新采集数据吗?',`/start_project_task/${id}`)
-}
-
-function confirmReProcessData(id){
-	_confirm('确定要重新处理数据吗?',`/process_project/${id}`)
-}
-
-function confirmSendData(id) {
-	_confirm('确定要开始上传数据吗?',`/send_project/${id}`)
-}
+function saveProject() {
+	const projectId = document.getElementById('project_id').value
+	const projectName = document.getElementById('project_name').value
+	const projectDesc = document.getElementById('project_desc').value
 
-function _confirm(text,url){
-	if (confirm(text)) {
-		fetch(url, {
+	try {
+		fetch('/save_project', {
 			method: 'POST',
 			headers: {
 				'Content-Type': 'application/json',
 			},
+			body: JSON.stringify({
+				project_id: projectId,
+				project_name: projectName,
+				project_desc: projectDesc,
+			}),
 		})
 			.then((response) => response.json())
 			.then((data) => {
 				if (data.success) {
-					alert('操作成功')
+					alert('保存成功')
 					window.location.reload()
 				} else {
-					alert('操作失败:' + data.error)
+					alert('保存失败:' + data.error)
 				}
 			})
-			.catch((error) => {})
+			.catch((error) => {
+				alert('保存失败')
+			})
+	} catch (e) {
+		alert('网络请求异常')
 	}
 }
 
-
-function confirmProjectDelete(id) {
+function confirmDelete(projectId) {
 	if (confirm('确定要删除该项目吗?')) {
-		fetch(`/delete_project/${id}`, {
-			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) => {
-				console.error('删除失败:', error)
-				alert('删除失败')
+		try {
+			fetch(`/delete_project/${projectId}`, {
+				method: 'POST',
 			})
+				.then((response) => response.json())
+				.then((data) => {
+					if (data.success) {
+						alert('删除成功')
+						window.location.reload()
+					} else {
+						alert('删除失败:' + data.error)
+					}
+				})
+				.catch((error) => {
+					console.error('删除失败:', error)
+					alert('删除失败')
+				})
+		} catch (e) {
+			alert('网络请求异常')
+		}
 	}
 }

+ 93 - 39
SourceCode/DataMiddleware/app/ui/static/styles.css

@@ -76,13 +76,21 @@ h6 {
 	color: white;
 	border: 1px solid #4caf50;
 }
+.hide,
+.hidden {
+	display: none;
+}
 .form-control {
-	width: 240px;
-	padding: 6px 10px;
+	padding: 8px 12px;
 	outline: none;
-	border: 1px solid #2196f3;
-	border-radius: 3px;
-	margin: 0 5px;
+	border: 1px solid #ddd;
+	border-radius: 6px;
+	transition: border-color 0.3s ease;
+}
+
+.form-control:focus {
+	border-color: #2196f3;
+	box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
 }
 .box {
 	margin: 20px 20px;
@@ -97,6 +105,12 @@ h6 {
 	border: 1px solid #ddd;
 	border-radius: 4px;
 }
+.box_header .project_info {
+	display: flex;
+	justify-content: left;
+	align-items: center;
+	width: 60%;
+}
 .box_header .box_title {
 	font-size: 18px;
 	font-weight: bolder;
@@ -142,10 +156,6 @@ th {
 tr:hover {
 	background-color: #f5f5f5;
 }
-
-td.editable .edit {
-	display: none;
-}
 .edit-mode .editable .show {
 	display: none;
 }
@@ -160,7 +170,6 @@ td.editable .edit {
 	padding: 5px 10px;
 	border: 1px solid #4caf50;
 }
-
 .pagination {
 	display: flex;
 	justify-content: space-between;
@@ -170,7 +179,6 @@ td.editable .edit {
 .pagination .pagination-links {
 	display: flex;
 }
-
 .pagination .page {
 	height: 40px;
 	min-width: 40px;
@@ -192,65 +200,111 @@ td.editable .edit {
 .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;
 }
-
-.edit-box {
-	position: absolute;
-	background: #f5f5f5;
-	z-index: 1000;
+.modal {
+}
+.modal {
+	position: fixed;
+	opacity: 0;
+	transition: all 0.3s ease;
+	top: 0;
+	left: 0;
 	width: 100%;
-	border: 1px solid #ddd;
-	box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+	height: 100%;
 	display: none;
+	align-items: center;
+	justify-content: center;
+	background: rgba(0, 0, 0, 0.2);
+}
+
+.modal.active {
+	opacity: 1;
+	display: flex;
+}
+.modal .modal-dialog {
+	background: white;
+	padding: 0;
+	border-radius: 8px;
+	box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+	z-index: 1000;
+	width: 600px;
+	overflow: hidden;
 }
-.edit-box.show {
-	display: block;
-	margin-top: -15px;
+.modal .close-btn {
+	font-size: 20px;
+	color: #666;
+	cursor: pointer;
+}
+.modal .close-btn:hover {
+	color: #333;
+	transform: scale(1.1);
 }
 
-.edit-box .form-group {
+.modal-content {
 	display: flex;
 	flex-direction: column;
-	gap: 15px;
-	padding: 15px;
-	width: 60%;
-	margin: 0 auto;
 }
 
-.edit-box .form-group > div {
+.modal-header {
+	padding: 16px 24px;
+	border-bottom: 1px solid #eee;
 	display: flex;
+	justify-content: space-between;
 	align-items: center;
-	gap: 10px;
 }
 
-.edit-box .form-group label {
-	min-width: 80px;
+.modal-header .modal-title {
+	font-weight: 600;
+	font-size: 18px;
+	margin: 0;
 }
 
-.edit-box .form-group .form-control {
-	flex: 1;
+.modal-body {
+	padding: 20px 24px;
+}
+
+.modal-footer {
+	padding: 16px 24px;
+	border-top: 1px solid #eee;
+	display: flex;
+	gap: 12px;
+	justify-content: center;
 }
 
-.edit-box .form-group textarea.form-control {
+.form-group {
+	display: flex;
+	flex-direction: column;
+	gap: 20px;
+	padding: 15px 0;
+	width: 100%;
+}
+.form-group > div {
+	display: flex;
+	align-items: flex-start;
+	gap: 16px;
+}
+.form-group label {
+	min-width: 80px;
+}
+.form-group .form-control {
+	flex: 1;
+}
+.form-group textarea.form-control {
 	height: 60px;
 	resize: vertical;
 }
-
-.edit-box .form-group textarea.form-control.large {
+.form-group textarea.form-control.large {
 	height: 80px;
 }
-
-.edit-box .form-group .button-group {
+.form-group .button-group {
 	justify-content: center;
 }

+ 116 - 0
SourceCode/DataMiddleware/app/ui/static/sub_project_item_list.js

@@ -0,0 +1,116 @@
+function addSubProjectItem(){
+	const modal = document.querySelector('#modal-sub_project_item')
+	const inputs = modal.querySelectorAll('.form-control')
+	inputs.forEach((input) => {
+		input.value = ''
+	})
+	show_add_moadl(modal)
+}
+function editSubProjectItem(id) {
+	const row = document.querySelector(`tr[data-id='${id}']`)
+	const modal = document.querySelector('#modal-sub_project_item')
+	modal.querySelector('#item_id').value = id
+	modal.querySelector('#device_name').value = row.querySelector('.device_name .form-control').value
+	modal.querySelector('#device_model').value = row.querySelector('.device_model .form-control').value
+	modal.querySelector('#device_count').value = row.querySelector('.device_count .form-control').value
+	modal.querySelector('#device_unit').value = row.querySelector('.device_unit .form-control').value
+	modal.querySelector('#standard_no').value = row.querySelector('.standard_no .form-control').value
+
+	show_edit_moadl(modal)
+}
+function saveSubProjectItem(sub_id) {
+	const modal = document.querySelector('#modal-sub_project_item')
+	const id = modal.querySelector('#item_id').value
+	const deviceName = modal.querySelector('#device_name').value
+	const deviceModel = modal.querySelector('#device_model').value
+	const deviceUnit = modal.querySelector('#device_unit').value
+	const deviceCount = modal.querySelector('#device_count').value
+	const standardNo = modal.querySelector('#standard_no').value
+	const isQuery = modal.querySelector('#is_query').checked
+	if (deviceName === '') {
+		alert('名称不能为空')
+		return
+	}
+	if (deviceModel === '') {
+		alert('规格型号不能为空')
+		return
+	}
+	if (deviceCount === '') {
+		alert('数量不能为空')
+		return
+	}
+	if (deviceUnit === '') {
+		alert('单位不能为空')
+		return
+	}
+	// if (standardNo === '') {
+	//     alert('标准编号不能为空');
+	//     return;
+	// }
+
+	fetch(`/save_sub_project_item`, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'application/json',
+		},
+		body: JSON.stringify({
+			id: id,
+			sub_id: sub_id,
+			device_name: deviceName,
+			device_model: deviceModel,
+			device_unit: deviceUnit,
+			device_count: deviceCount,
+			standard_no: standardNo ,
+			is_query: isQuery
+		}),
+	})
+		.then((response) => response.json())
+		.then((data) => {
+			if (data.success) {
+				alert('保存成功')
+				window.location.href = `/project_item_list/${sub_id}`
+			} else {
+				alert('保存失败:' + data.error)
+			}
+		})
+		.catch((error) => {
+			console.error('保存失败:', error)
+		})
+}
+
+function deleteSubProjectItem(itemId) {
+	if (confirm('确定要删除该设备吗?')) {
+		fetch(`/delete_sub_project_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('删除失败')
+			})
+	}
+}
+
+function startProcessItem(itemId){
+	_confirm('确定要开始处理吗?', `/start_process_item/${itemId}`)
+}
+function startSendItem(itemId){
+	_confirm('确定要开始发送吗?',`/start_send_item/${itemId}`)
+}
+
+

+ 195 - 0
SourceCode/DataMiddleware/app/ui/static/sub_project_list.js

@@ -0,0 +1,195 @@
+const files = new Set()
+function addSubProject() {
+	const modal = document.querySelector('#modal-sub_project')
+	const inputs = modal.querySelectorAll('.form-control')
+	inputs.forEach((input) => {
+		input.value = ''
+	})
+	modal.querySelector('#delete_old_data').checked = false
+	modal.querySelector('#delete_old_data').parentNode.style.display = 'none'
+	modal.querySelector('#sub_project_file').value = ''
+	modal.querySelector('.file-list').innerHTML = ''
+	files.clear()
+	show_add_moadl(modal)
+}
+function updateSubProject(row, id) {
+	const modal = document.querySelector('#modal-sub_project')
+	modal.querySelector('#sub_id').value = id
+	modal.querySelector('#sub_project_name').value = row.querySelector('.sub_project_name .form-control').value
+	modal.querySelector('#work_catalog').value = row.querySelector('.work_catalog .form-control').value
+	// modal.querySelector('#work_content').value = row.querySelector('.work_content .form-control').value
+	modal.querySelector('#delete_old_data').checked = false
+	modal.querySelector('#delete_old_data').parentNode.style.display = 'block'
+	modal.querySelector('#sub_project_file').value = ''
+	modal.querySelector('.file-list').innerHTML = ''
+	files.clear()
+	show_edit_moadl(modal)
+}
+function saveSubProject() {
+	const id = document.getElementById('sub_id').value
+	const project_id = document.getElementById('project_id').value
+	const name = document.getElementById('sub_project_name').value
+	const catalog = document.getElementById('work_catalog').value
+	// const content = document.getElementById('work_content').value
+	// const version = document.getElementById('new_standard_version').value
+	const content = ''
+	const version = '2'
+
+	if (name === '') {
+		alert('工程名称不能为空')
+		return
+	}
+	if (files.length === 0 && !id) {
+		alert('请选择工程数据文件')
+		return
+	}
+	// if (catalog === '') {
+	// 	alert('工作目录不能为空')
+	// 	return
+	// }
+	// if (content === '') {
+	// 	alert('工作内容不能为空')
+	// 	return
+	// }
+
+	const formData = new FormData()
+	formData.append('id', id)
+	formData.append('project_id', project_id)
+	formData.append('sub_project_name', name)
+	files.forEach((file) => {
+		formData.append('project_files', file)
+	})
+	formData.append('work_catalog', catalog)
+	formData.append('work_content', content)
+	formData.append('standard_version', version)
+	if (document.getElementById('delete_old_data')) {
+		formData.append('delete_old_data', document.getElementById('delete_old_data').checked)
+	}
+
+	fetch('/save_sub_project', {
+		method: 'POST',
+		body: formData,
+	})
+		.then((response) => response.json())
+		.then((data) => {
+			if (data.success) {
+				alert('保存成功')
+				window.location.reload()
+			} else {
+				alert('保存失败:' + data.error)
+			}
+		})
+		.catch((error) => {
+			console.error('保存失败:', error)
+			alert('保存失败')
+		})
+}
+
+function startSubProjectTask(id) {
+	_confirm('确定要开始任务吗?', `/start_sub_project_task/${id}`)
+}
+
+function reStartSubProjectTask(id) {
+	_confirm('确定要重新采集Excel的数据吗?', `/start_sub_project_task/${id}`)
+}
+
+function reStartProcessSubProjectTask(id) {
+	_confirm('确定要重新处理数据吗?', `/start_process_sub_project/${id}`)
+}
+
+function reStartSendSubProjectTask(id) {
+	_confirm('确定要开始上传数据吗?', `/start_send_sub_project/${id}`)
+}
+
+function deleteSubProject(id) {
+	if (confirm('确定要删除该工程吗?')) {
+		fetch(`/delete_sub_project/${id}`, {
+			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) => {
+				console.error('删除失败:', error)
+				alert('删除失败')
+			})
+	}
+}
+
+function handleDragOver(e) {
+	e.preventDefault()
+	e.stopPropagation()
+	const modal = document.querySelector('#modal-sub_project')
+	modal.querySelector('.drag-drop-box').classList.add('dragover')
+}
+
+function handleDragLeave(e) {
+	e.preventDefault()
+	e.stopPropagation()
+	const modal = document.querySelector('#modal-sub_project')
+	modal.querySelector('.drag-drop-box').classList.remove('dragover')
+}
+
+function handleDrop(e) {
+	e.preventDefault()
+	e.stopPropagation()
+	const modal = document.querySelector('#modal-sub_project')
+	modal.querySelector('.drag-drop-box').classList.remove('dragover')
+	const files = e.dataTransfer.files
+	handleFiles(files)
+}
+
+function handleFileSelect(e) {
+	const files = e.target.files
+	handleFiles(files)
+}
+
+function handleFiles(newFiles) {
+	const fileList = document.getElementById('fileList')
+	const existingFiles = new Set()
+	fileList.querySelectorAll('.file-item span:first-child').forEach((item) => {
+		existingFiles.add(item.textContent)
+	})
+
+	Array.from(newFiles).forEach((file) => {
+		if (existingFiles.has(file.name)) {
+			alert('文件' + file.name + '已存在,请勿重复添加!')
+			return
+		}
+		existingFiles.add(file.name)
+		fileList.appendChild(createFileItem(file))
+		files.add(file)
+	})
+	document.getElementById('sub_project_file').value = ''
+}
+function createFileItem(file) {
+	const item = document.createElement('div')
+	item.className = 'file-item'
+	item.innerHTML = `
+			<span>${file.name}</span>
+			<span class="del" onclick="removeFileItem(this)">×</span>
+	`
+	return item
+}
+
+function removeFileItem(element) {
+	const fileItem = element.closest('.file-item')
+	fileItem.remove()
+	// 清除文件输入框的值
+	document.getElementById('sub_project_file').value = ''
+}
+
+document.addEventListener('DOMContentLoaded', function () {
+	document.getElementById('dragDropArea').addEventListener('click', function () {
+		document.getElementById('sub_project_file').click()
+	})
+})

+ 63 - 12
SourceCode/DataMiddleware/app/ui/static/utils.js

@@ -1,21 +1,72 @@
 function goTo(url) {
-    window.location.href = url;
+	window.location.href = url
+}
+function show_add_moadl(modal) {
+	modal = modal || '.modal'
+	if (typeof modal == 'string') {
+		modal = document.querySelector(`${modal}`)
+	}
+	modal.querySelector('.modal-header .modal-title span').innerHTML = '添加'
+	show_modal(modal)
+}
+function show_edit_moadl(modal) {
+	modal = modal || '.modal'
+	if (typeof modal == 'string') {
+		modal = document.querySelector(`${modal}`)
+	}
+	modal.querySelector('.modal-header .modal-title span').innerHTML = '修改'
+	show_modal(modal)
+}
+function show_modal(modal) {
+	modal = modal || '.modal'
+	if (typeof modal == 'string') {
+		modal = document.querySelector(`${modal}`)
+	}
+	modal.classList.add('active')
+}
+function hide_modal(modal) {
+	modal = modal || '.modal'
+	if (typeof modal == 'string') {
+		modal = document.querySelector(`${modal}`)
+	}
+	modal.classList.remove('active')
 }
 
 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');
+	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');
+	row.classList.remove('edit-mode')
 }
 
 function cancelNewChanges(row) {
-    row.remove();
-}
+	row.remove()
+}
+
+function _confirm(text, url) {
+	if (confirm(text)) {
+		fetch(url, {
+			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) => {})
+	}
+}

+ 0 - 161
SourceCode/DataMiddleware/app/ui/templates/project/project_item_list.html

@@ -1,161 +0,0 @@
-<!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;
-				align-items: center;
-				width: 60%;
-			}
-		</style>
-		<script src="{{ url_for('static', filename='utils.js') }}"></script>
-		<script src="{{ url_for('static', filename='project_item_list.js') }}"></script>
-		<script>
-			function searchData() {
-				var keyword = document.getElementById('search_input').value
-				window.location.href = `{{ url_for('project.project_item_list', project_id=project.id) }}?k=` + keyword
-			}
-			function reSearchData() {
-				window.location.href = `{{ url_for('project.project_item_list', project_id=project.id) }}`
-			}
-		</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('project.project_list') }}`)">返回</button>
-					<h3 class="box_title">设备列表</h3>
-					<span class="separator">|</span>
-					<dl>
-						<dt>项目编号:</dt>
-						<dd>{{ project.project_no }}</dd>
-					</dl>
-					<dl>
-						<dt>项目名称:</dt>
-						<dd>{{ project.project_name }}</dd>
-					</dl>
-					<dl>
-						<dt>标准版本:</dt>
-						<dd>
-							{% if project.standard_version == '1' %}
-							<span class="label label-warning">旧版</span>
-							{% elif project.standard_version == '2' %}
-							<span class="label label-info">新版</span>
-							{% endif %}
-						</dd>
-					</dl>
-				</div>
-			</div>
-			<div class="box_header">
-				<div class="btn_box">
-					<button class="btn btn-success btn-large" onclick="addNewItem('{{ project.id}}')">添加</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="200px">数量</th>
-							<th width="8%">单位</th>
-							<th width="15%">定额编号</th>
-							<th width="180px">操作</th>
-						</tr>
-					</thead>
-					<tbody>
-						{% if project_items %} {% for item in project_items %}
-						<tr>
-							<td class="editable name">
-								<span class="show">{{ item.device_name if item.device_name else '-' }}</span>
-								<span class="edit">
-									<input type="text" class="form-control" title="设备名称" value="{{ item.device_name if item.device_name else '' }}" />
-								</span>
-							</td>
-							<td class="editable model">
-								<span class="show">{{ item.device_model if item.device_model else '-' }}</span>
-								<span class="edit">
-									<input type="text" class="form-control" title="设备规格型号" value="{{ item.device_model if item.device_model else '' }}" />
-								</span>
-							</td>
-							<td class="editable count">
-								<span class="show">{{ item.device_count if item.device_count else '-' }}</span>
-								<span class="edit">
-									<input type="text" class="form-control" title="设备数量" value="{{ item.device_count if item.device_count else '' }}" />
-								</span>
-							</td>
-							<td class="editable unit">
-								<span class="show">{{ item.device_unit if item.device_unit else '-' }}</span>
-								<span class="edit">
-									<input type="text" class="form-control" title="设备单位" value="{{ item.device_unit if item.device_unit else '' }}" />
-								</span>
-							</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" title="设备定额编号" 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('project.project_item_list', project_id=project.id, k=keyword, p=1, pp=per_page) }}">首页</a>
-						{% endif %} {% if page > 1 %}
-						<a class="page page-link" href="{{ url_for('project.project_item_list', project_id=project.id, 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('project.project_item_list', project_id=project.id, 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('project.project_item_list', project_id=project.id, k=keyword, p=p, pp=per_page) }}">{{ p }}</a>
-						{% else %}
-						<a class="page page-link" href="{{ url_for('project.project_item_list', project_id=project.id, 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('project.project_item_list', project_id=project.id, k=keyword, p=total_pages, pp=per_page) }}">{{ total_pages }}</a>
-						{% endif %} {% if page < total_pages %}
-						<a class="page page-link" href="{{ url_for('project.project_item_list', project_id=project.id, k=keyword, p=page+1, pp=per_page) }}">下一页</a>
-						{% endif %} {% if page < total_pages %}
-						<a class="page page-link" href="{{ url_for('project.project_item_list', project_id=project.id, k=keyword, p=total_pages, pp=per_page) }}">末页</a>
-						{% endif %}
-					</div>
-				</div>
-			</div>
-		</div>
-	</body>
-</html>

+ 135 - 247
SourceCode/DataMiddleware/app/ui/templates/project/project_list.html

@@ -1,249 +1,137 @@
 <!DOCTYPE html>
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
 <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='project_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('project.project_list') }}?k=${keyword}&s=${status}`;
-        }
-        function reSearchData(){
-            window.location.href = `{{ url_for('project.project_list') }}`;
-        }
-    </script>
-</head>
-<body>
-    <div class="box">
-        <div class="box_header">
-            <div><h3 class="box_title">项目列表</h3></div>
-        </div>
-        <div class="box_header">
-            <div class="btn_box">
-                <button type="button" class="btn btn-success btn-large" onclick="addProject()">添加</button>
-            </div>
-            <div class="search_box">
-                <select id="status_select" class="form-control form-select" title="状态选择">
-                    <option value="-1" {% if status == -1 %}selected{% endif %}>所有状态</option>
-                    <option value="0" {% if status == 0 %}selected{% endif %}>新建</option>
-                    <option value="5" {% if status == 5 %}selected{% endif %}>完成</option>
-                     <option value="11" {% if status == 11 %}selected{% endif %}>采集失败</option>
-                    <option value="12" {% if status == 12 %}selected{% endif %}>分析失败</option>
-                    <option value="13" {% if status == 13 %}selected{% endif %}>上传失败</option>
-                     <option value="21" {% if status == 21 %}selected{% endif %}>采集中...</option>
-                    <option value="22" {% if status == 22 %}selected{% endif %}>分析中...</option>
-                    <option value="23" {% if status == 23 %}selected{% endif %}>上传中...</option>
-                    <option value="31" {% if status == 31 %}selected{% endif %}>采集完成</option>
-                    <option value="32" {% if status == 32 %}selected{% endif %}>分析完成</option>
-                    <option value="33" {% if status == 32 %}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="edit-box">
-            <div class="form-group">
-                <input type="hidden" id="project_id" class="form-control">
-                <div>
-                    <label for="new_project_no">项目编号:</label>
-                    <input type="text" id="new_project_no" class="form-control" placeholder="项目编号">
-                </div>
-                <div>
-                    <label for="new_project_name">项目名称:</label>
-                    <input type="text" id="new_project_name" class="form-control" placeholder="项目名称">
-                </div>
-                <div>
-                    <label for="new_project_file">项目数据:</label>
-                    <input type="file" id="new_project_file" class="form-control" accept=".xlsx,.xls,.csv" multiple>
-                </div>
-                <div></div>
-                <div>
-                    <label for="new_work_catalog">工作目录:</label>
-                    <textarea id="new_work_catalog" class="form-control" placeholder="工作目录"></textarea>
-                </div>
-                <div>
-                    <label for="new_work_content">工作内容:</label>
-                    <textarea id="new_work_content" class="form-control large" placeholder="工作内容"></textarea>
-                </div>
-                <div>
-                    <label for="new_standard_version">标准版本:</label>
-                    <select id="new_standard_version" class="form-control">
-                        <option value="1">旧版</option>
-                        <option value="2">新版</option>
-                    </select>
-                </div>
-                <div class="button-group">
-                    <button type="button" class="btn btn-success btn-large" onclick="saveProject()">保存</button>
-                    <button type="button" class="btn btn-warning btn-large" onclick="cancelAdd()">关闭</button>
-                </div>
-            </div>
-        </div>
-        <div class="box-body">
-            <table class="table">
-                <thead>
-                    <tr>
-                        <th width="200px">项目编号</th>
-                        <th width="280px">项目名称</th>
-                        <th width="15%">工作目录</th>
-                        <th width="">工作内容</th>
-                        <th width="120px">标准版本</th>
-                        <th width="120px">状态</th>
-                        <th width="260px">操作</th>
-                    </tr>
-                </thead>
-                <tbody>
-                {% if project_list %}
-                    {% for project in project_list %}
-                        <tr>
-                            <td class="editable project_no">
-                                <span class="show">{{ project.project_no if project.project_no else '-' }}</span>
-                                <span class="edit">
-                                    <input type="text" class="form-control" title="项目编号" name="project_no" value="{{ project.project_no if project.project_no else '' }}">
-                                </span>
-                            </td>
-                            <td class="editable project_name">
-                                <span class="show">{{ project.project_name if project.project_name else '-' }}</span>
-                                <span class="edit">
-                                    <input type="text" class="form-control" title="项目名称" name="project_name" value="{{ project.project_name if project.project_name else '' }}">
-                                </span>
-                            </td>
-                            <td class="editable work_catalog">
-                                <span class="show">{{ project.work_catalog if project.work_catalog else '-' }}</span>
-                                <span class="edit">
-                                    <input type="text" class="form-control" title="工作目录" name="work_catalog" value="{{ project.work_catalog if project.work_catalog else '' }}">
-                                </span>
-                            </td>
-                            <td class="editable work_content">
-                                <span class="show">{{ project.work_content if project.work_content else '-' }}</span>
-                                <span class="edit">
-                                    <input type="text" class="form-control" title="工作内容" value="{{ project.work_content if project.work_content else '' }}">
-                                </span>
-                            </td>
-                            <td class="editable standard_version">
-                                <span class="show">
-                                    {% if project.standard_version == '1' %}
-                                    <span class="label label-warning">旧版</span>
-                                    {% elif project.standard_version == '2' %}
-                                    <span class="label label-info">新版</span>
-                                    {% endif %}
-                                </span>
-                                <span class="edit">
-                                    <select class="form-control" title="标准版本">
-                                        <option value="1" {% if project.standard_version == '1' %}selected{% endif %}>旧版</option>
-                                        <option value="2" {% if project.standard_version == '2' %}selected{% endif %}>新版</option>
-                                    </select>
-                                </span>
-                            </td>
-                            <td>
-                                <span class="status">
-                                    {% if project.status == 0 %}
-                                    <span class="label label-default">新建</span>
-                                    {% elif project.status == 21 %}
-                                    <span class="label label-warning">采集中...</span>
-                                    {% elif project.status == 22 %}
-                                    <span class="label label-warning">分析中...</span>
-                                    {% elif project.status == 23 %}
-                                    <span class="label label-warning">上传中...</span>
-                                    {% elif project.status == 31 %}
-                                    <span class="label label-info">采集完成</span>
-                                    {% elif project.status == 32 %}
-                                    <span class="label label-info">分析完成</span>
-                                    {% elif project.status == 33 %}
-                                    <span class="label label-info">上传完成</span>
-                                    {% elif project.status == 11 %}
-                                    <span class="label label-danger">采集失败</span>
-                                    {% elif project.status == 12 %}
-                                    <span class="label label-danger">分析失败</span>
-                                    {% elif project.status == 13 %}
-                                    <span class="label label-danger">上传失败</span>
-                                    {% elif project.status == 5 %}
-                                    <span class="label label-success">完成</span>
-                                    {% endif %}
-                                </span>
-                            </td>
-                            <td class="editable">
-                                <span class="show">
-                                    {% if project.status == 0 %}
-                                    <button class="btn btn-success" type="button" onclick="confirmStartTask('{{ project.id }}')">开始</button>
-                                    <button class="btn btn-info" onclick="updateProject(this.parentNode.parentNode.parentNode,'{{ project.id }}')">编辑</button>
-                                    {% elif project.status == 11 %}
-                                    <button class="btn btn-success" type="button" onclick="confirmReStartTask('{{ project.id }}')">重新采集</button>
-                                    <button class="btn btn-info" onclick="updateProject(this.parentNode.parentNode.parentNode,'{{ project.id }}')">编辑</button>
-                                    {% elif project.status == 12 %}
-                                    <button class="btn btn-success" type="button" onclick="confirmReProcessData('{{ project.id }}')">重新处理</button>
-                                    {% elif project.status == 13 %}
-                                    <button class="btn btn-success" type="button" onclick="confirmSendData('{{ project.id }}')">重新发送</button>
-                                    {% elif project.status == 5 or project.status == 32 or project.status == 13 or project.status == 23 or project.status == 33 %}
-                                    <button class="btn btn-info" type="button" onclick="goTo(`{{ url_for('project.project_item_list', project_id=project.id) }}`)">详情</button>
-                                    {% endif %}
-                                    {% if project.status != 21  and project.status != 22 %}
-                                    <button class="btn btn-danger" onclick="confirmProjectDelete('{{ project.id }}')">删除</button>
-                                    {% endif %}
-                                </span>
-                                <span class="edit">
-                                    <button class="btn btn-success" onclick="saveProjectUpdate(this.parentNode.parentNode.parentNode,'{{ project.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('project.project_list', k=keyword, s=status, p=1, pp=per_page) }}">首页</a>
-                {% endif %}
-                {% if page > 1 %}
-                    <a class="page page-link" href="{{ url_for('project.project_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('project.project_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('project.project_list', k=keyword, p=p, pp=per_page) }}" >{{ p }}</a>
-                    {% else %}
-                    <a class="page page-link" href="{{ url_for('project.project_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('project.project_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('project.project_list', k=keyword, p=page+1, pp=per_page) }}">下一页</a>
-                {% endif %}
-                {% if page < total_pages %}
-                    <a class="page page-link" href="{{ url_for('project.project_list', k=keyword, p=total_pages, pp=per_page) }}">末页</a>
-                {% endif %}
-            </div>
-        </div>
-        </div>
-    </div>
-</body>
-</html>
+	<head>
+		<meta charset="UTF-8" />
+		<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='project_list.js') }}"></script>
+		<script>
+			function searchData() {
+				const keyword = document.getElementById('search_input').value
+				window.location.href = `{{ url_for('project.main_project_list') }}?k=${keyword}`
+			}
+			function reSearchData() {
+				window.location.href = `{{ url_for('project.main_project_list') }}`
+			}
+			function jumpToPage(page) {
+				window.location.href = `{{ url_for('project.main_project_list') }}?k={{keyword}}&pp={{per_page}}&p=${page}`
+			}
+		</script>
+	</head>
+	<body>
+		<div class="box">
+			<div class="box_header">
+				<h3 class="box_title">主项目列表</h3>
+			</div>
+			<div class="box_header">
+				<div class="btn_box">
+					<button type="button" class="btn btn-success btn-large" onclick="addProject()">新建项目</button>
+				</div>
+				<div class="search_box">
+					<input type="text" title="" 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="120px">项目ID</th>
+							<th width="350px">项目名称</th>
+							<th>项目简介</th>
+							<th width="300px">操作</th>
+						</tr>
+					</thead>
+					<tbody>
+					{% if  project_list %}
+						{% for project in project_list %}
+						<tr data-id="{{project.id}}" data-p="{{project}}">
+							<td class="project-name">
+								{{ project.id }}
+							</td>
+							<td class="project-name">
+								{{ project.project_name }}
+								<input type="hidden" class="form-control" value="{{project.project_name}}"/>
+							</td>
+							<td class="project-desc">
+								{{ project.description }}
+								<input type="hidden" class="form-control" value="{{project.description}}"/>
+							</td>
+							<td>
+								<button class="btn btn-info" onclick="goTo(`{{ url_for('project.sub_project_list', project_id=project.id) }}`)">子项目管理</button>
+								<button class="btn btn-warning" onclick="editProject('{{ project.id }}')">编辑</button>
+								<button class="btn btn-danger" onclick="confirmDelete('{{ project.id }}')">删除</button>
+							</td>
+						</tr>
+						{% endfor %}
+					{% else %}
+						<tr>
+							<td colspan="34">暂无数据</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 %}
+						<span class="page page-link" onclick="jumpToPage(1)">首页</span>
+						{% endif %} {% if page > 1 %}
+						<span class="page page-link" onclick="jumpToPage(`{{page - 1}}`)">上一页</span>
+						{% endif %} {% set start_page = [1, page - 2]|max %} {% set end_page = [total_pages, page + 2]|min %} {% if start_page > 1 %}
+						<span class="page page-link" onclick="jumpToPage(1)">1</span>
+						{% if start_page > 2 %}
+						<span class="page">...</span>
+						{% endif %} {% endif %} {% for p in range(start_page, end_page + 1) %} {% if p == page %}
+						<span class="page page-link active" onclick="jumpToPage(`{{ p }}`)">{{ p }}</span>
+						{% else %}
+						<span class="page page-link" onclick="jumpToPage(`{{ p }}`)">{{ p }}</span>
+						{% endif %} {% endfor %} {% if end_page < total_pages %} {% if end_page < total_pages - 1 %}
+						<span class="page">...</span>
+						{% endif %}
+						<span class="page page-link" onclick="jumpToPage(`{{ total_pages }}`)">{{ total_pages }}</span>
+						{% endif %} {% if page < total_pages %}
+						<span class="page page-link" onclick="jumpToPage(`{{ page+1 }}`)">下一页</span>
+						{% endif %} {% if page < total_pages %}
+						<span class="page page-link" onclick="jumpToPage(`{{ total_pages }}`)">末页</span>
+						{% endif %}
+					</div>
+				</div>
+			</div>
+		</div>
+		<div class="modal" id="modal-project">
+			<div class="modal-dialog">
+			<div class="modal-content">
+				<div class="modal-header">
+					<h3 class="modal-title"><span></span>项目</h3>
+					<span class="close-btn" onclick="hide_modal('#modal-project')">&times;</span>
+				</div>
+				<div class="modal-body">
+					<div class="form-group">
+						<input type="hidden" id="project_id" class="form-control" />
+						<div>
+							<label for="project_name">项目名称:</label>
+							<input type="text" id="project_name" class="form-control" placeholder="项目名称" required />
+						</div>
+						<div>
+							<label for="project_desc">项目简介:</label>
+							<textarea id="project_desc" class="form-control large" placeholder="项目简介"></textarea>
+						</div>
+					</div>
+				</div>
+				<div class="modal-footer">
+					<button class="btn btn-success btn-large" onclick="saveProject()">保存</button>
+					<button class="btn btn-warning btn-large" onclick="hide_modal()">取消</button>
+				</div>
+			</div>
+			</div>
+		</div>
+	</body>
+</html>

+ 236 - 0
SourceCode/DataMiddleware/app/ui/templates/project/sub_project_item_list.html

@@ -0,0 +1,236 @@
+<!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>
+
+		</style>
+		<script src="{{ url_for('static', filename='utils.js') }}"></script>
+		<script src="{{ url_for('static', filename='sub_project_item_list.js') }}"></script>
+		<script>
+			function searchData() {
+				const process_status = document.getElementById('process_status_select').value
+				const send_status = document.getElementById('send_status_select').value
+				const keyword = document.getElementById('search_input').value
+				window.location.href = `{{ url_for('project.project_item_list', sub_project_id=sub_project.id) }}?k=${keyword}&p_s=${process_status}&s_s=${send_status}`
+			}
+			function reSearchData() {
+				window.location.href = `{{ url_for('project.project_item_list', sub_project_id=sub_project.id) }}`
+			}
+			function jumpToPage(page) {
+				window.location.href = `{{ url_for('project.project_item_list', sub_project_id=sub_project.id) }}?p_s={{process_status}}&s_s={{send_status}}&k={{keyword}}&pp={{per_page}}&p=${page}`
+			}
+		</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('project.sub_project_list',project_id=sub_project.project_id) }}`)">返回</button>
+					<h3 class="box_title">工程明细列表</h3>
+					<span class="separator">|</span>
+					<dl>
+						<dt>工程名称:</dt>
+						<dd>{{ sub_project.sub_project_name }}</dd>
+					</dl>
+					<!--<dl>
+						<dt>标准版本:</dt>
+						<dd>
+							{% if sub_project.standard_version == '1' %}
+							<span class="label label-warning">旧版</span>
+							{% elif sub_project.standard_version == '2' %}
+							<span class="label label-info">新版</span>
+							{% endif %}
+						</dd>
+					</dl>-->
+				</div>
+			</div>
+			<div class="box_header">
+				<div class="btn_box">
+					<button class="btn btn-success btn-large" onclick="addSubProjectItem()">新建工程明细</button>
+				</div>
+				<div class="search_box">
+					<select id="process_status_select" class="form-control form-select" title="处理状态选择">
+						<option value="-1" {% if process_status == -1 %}selected{% endif %}>请选择处理状态</option>
+						<option value="2" {% if process_status == 2 %}selected{% endif %}>已处理</option>
+						<option value="3" {% if process_status == 3 %}selected{% endif %}>处理失败</option>
+						<option value="0" {% if process_status == 0 %}selected{% endif %}>未处理</option>
+						<option value="1" {% if process_status == 1 %}selected{% endif %}>处理中</option>
+					</select>
+					<select id="send_status_select" class="form-control form-select" title="推送状态选择">
+						<option value="-1" {% if send_status == -1 %}selected{% endif %}>请选择推送状态</option>
+						<option value="2" {% if send_status == 2 %}selected{% endif %}>已推送</option>
+						<option value="3" {% if send_status == 3 %}selected{% endif %}>推送失败</option>
+						<option value="0" {% if send_status == 0 %}selected{% endif %}>未推送</option>
+						<option value="1" {% if send_status == 1 %}selected{% endif %}>推送中</option>
+					</select>
+					<input type="text" id="search_input" title="" 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="300px">名称</th>
+							<th>规格型号</th>
+							<th width="150px">数量</th>
+							<th width="120px">单位</th>
+							<th width="120">定额编号</th>
+							<th width="120px">处理状态</th>
+							<th width="120px">发送状态</th>
+							<th width="360px">操作</th>
+						</tr>
+					</thead>
+					<tbody>
+						{% if items %} {% for item in items %}
+						<tr data-id="{{item.id}}">
+							<td class="device_name">
+								{{ item.device_name if item.device_name else '-' }}
+								<input type="hidden" class="form-control" title="设备名称" value="{{ item.device_name if item.device_name else '' }}" />
+							</td>
+							<td class="device_model">
+								{{ item.device_model if item.device_model else '-' }}
+								<input type="hidden" class="form-control" title="设备规格型号" value="{{ item.device_model if item.device_model else '' }}" />
+							</td>
+							<td class="device_count">
+								{{ item.device_count if item.device_count else '-' }}
+								<input type="hidden" class="form-control" title="设备数量" value="{{ item.device_count if item.device_count else '' }}" />
+							</td>
+							<td class="device_unit">
+								{{ item.device_unit if item.device_unit else '-' }}
+								<input type="hidden" class="form-control" title="设备单位" value="{{ item.device_unit if item.device_unit else '' }}" />
+							</td>
+							<td class="standard_no">
+								{{ item.standard_no if item.standard_no else '-' }}
+								<input type="hidden" class="form-control" title="设备定额编号" value="{{ item.standard_no if item.standard_no else '' }}" />
+							</td>
+							<td class="process_status">
+								{% if item.process_status == 1 %}
+								<span class="label label-warning">处理中...</span>
+								{% elif item.process_status == 2 %}
+								<span class="label label-success">已处理</span>
+								{% elif item.process_status == 3 %}
+								<span class="label label-danger">处理失败</span>
+								{% else %}
+								<span class="label label-default">未处理</span>
+								{% endif %}
+							</td>
+							<td class="send_status">
+								{% if item.send_status == 1 %}
+								<span class="label label-warning">推送中...</span>
+								{% elif item.send_status == 2 %}
+								<span class="label label-success">已推送</span>
+								{% elif item.send_status == 3 %}
+								<span class="label label-danger">推送失败</span>
+								{% else %}
+								<span class="label label-default">未推送</span>
+								{% endif %}
+							</td>
+							<td class="tool">
+								{% if item.process_status == 0 %}
+								<button class="btn btn-success" onclick="startProcessItem('{{ item.id }}')">开始处理</button>
+								{% elif item.process_status == 2 %}
+								<button class="btn btn-warning" onclick="startProcessItem('{{ item.id }}')">重新处理</button>
+								{% elif item.process_status == 3 %}
+								<button class="btn btn-danger" onclick="startProcessItem('{{ item.id }}')">重新处理</button>
+								{% endif %}
+								{% if item.send_status == 0 and item.process_status != 1 %}
+								<button class="btn btn-success" onclick="startSendItem('{{ item.id }}')">开始发送</button>
+								{% elif item.send_status == 2 and item.process_status != 1 %}
+								<button class="btn btn-warning" onclick="startSendItem('{{ item.id }}')">重新发送</button>
+								{% elif item.send_status == 3 and item.process_status != 1 %}
+								<button class="btn btn-danger" onclick="startSendItem('{{ item.id }}')">重新发送</button>
+								{% endif %}
+								<button class="btn btn-info" onclick="editSubProjectItem('{{ item.id }}')">编辑</button>
+								<button class="btn btn-danger" onclick="deleteSubProjectItem('{{ item.id }}')">删除</button>
+							</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 %}
+						<span class="page page-link" onclick="jumpToPage(1)">首页</span>
+						{% endif %} {% if page > 1 %}
+						<span class="page page-link" onclick="jumpToPage(`{{page - 1}}`)">上一页</span>
+						{% endif %} {% set start_page = [1, page - 2]|max %} {% set end_page = [total_pages, page + 2]|min %} {% if start_page > 1 %}
+						<span class="page page-link" onclick="jumpToPage(1)">1</span>
+						{% if start_page > 2 %}
+						<span class="page">...</span>
+						{% endif %} {% endif %} {% for p in range(start_page, end_page + 1) %} {% if p == page %}
+						<span class="page page-link active" onclick="jumpToPage(`{{ p }}`)">{{ p }}</span>
+						{% else %}
+						<span class="page page-link" onclick="jumpToPage(`{{ p }}`)">{{ p }}</span>
+						{% endif %} {% endfor %} {% if end_page < total_pages %} {% if end_page < total_pages - 1 %}
+						<span class="page">...</span>
+						{% endif %}
+						<span class="page page-link" onclick="jumpToPage(`{{ total_pages }}`)">{{ total_pages }}</span>
+						{% endif %} {% if page < total_pages %}
+						<span class="page page-link" onclick="jumpToPage(`{{ page+1 }}`)">下一页</span>
+						{% endif %} {% if page < total_pages %}
+						<span class="page page-link" onclick="jumpToPage(`{{ total_pages }}`)">末页</span>
+						{% endif %}
+					</div>
+				</div>
+			</div>
+		</div>
+	<div class="modal" id="modal-sub_project_item">
+	<div class="modal-dialog">
+		<div class="modal-content">
+			<div class="modal-header">
+				<h3 class="modal-title"><span></span>工程明细</h3>
+				<span class="close-btn" onclick="hide_modal('#modal-sub_project_item')">&times;</span>
+			</div>
+			<div class="modal-body">
+				<div class="form-group">
+					<input type="hidden" id="item_id">
+					<div>
+						<label>设备名称:</label>
+						<input type="text" title="" id="device_name" class="form-control" placeholder="请输入设备名称">
+					</div>
+					<div>
+						<label>规格型号:</label>
+						<input type="text" title="" id="device_model" class="form-control" placeholder="请输入规格型号">
+					</div>
+					<div>
+						<label>数量:</label>
+						<input type="text" title="" id="device_count" class="form-control" placeholder="请输入数量">
+					</div>
+					<div>
+						<label>单位:</label>
+						<input type="text" title="" id="device_unit" class="form-control" placeholder="请输入单位">
+					</div>
+					<div>
+						<label>定额编号:</label>
+						<input type="text" title="" id="standard_no" class="form-control" placeholder="请输入定额编号">
+					</div>
+					<div style="display: block;">
+                        <label for="is_query">是否查询知识库:</label>
+                        <input type="checkbox" id="is_query" class="form-control" style="width: auto;flex: 0;">
+                    </div>
+				</div>
+			</div>
+			<div class="modal-footer">
+				<button class="btn btn-success" onclick="saveSubProjectItem('{{sub_project.id}}')">保存</button>
+				<button class="btn btn-warning" onclick="hide_modal('#modal-sub_project_item')">取消</button>
+			</div>
+		</div>
+	</div>
+</div>
+</body>
+</html>

+ 321 - 0
SourceCode/DataMiddleware/app/ui/templates/project/sub_project_list.html

@@ -0,0 +1,321 @@
+<!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='sub_project_list.js') }}"></script>
+    <style>
+        .file-upload-box{
+            width: 100%;
+            display: flex;
+            flex-direction: column;
+        }
+        .drag-drop-box {
+            border: 2px dashed #ccc;
+            border-radius: 4px;
+            padding: 20px;
+            text-align: center;
+            cursor: pointer;
+            transition: border-color 0.3s;
+            color: #666;
+        }
+        .drag-drop-box.dragover {
+            border-color: #2196F3;
+            background-color: rgba(33, 150, 243, 0.1);
+        }
+         .file-list:empty{
+             display: none;
+         }
+        .file-list {
+            display: flex;
+            flex-wrap: wrap;
+            margin-top: 10px;
+        }
+        .file-item {
+            display: flex;
+            align-items: center;
+            background-color: #f5f5f5;
+            padding:5px 8px;
+            border-radius: 5px;
+            margin: 5px 8px;
+
+        }
+        .file-item span{
+            color: #2196f3;
+        }
+        .file-item span.del {
+            margin-left: 5px;
+            cursor: pointer;
+            color: #eeeeee;
+            background: #2196f3;
+            border-radius: 50%;
+            width: 15px;
+            height: 15px;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+        }
+        td .file{
+            width: auto;
+            margin: 0 5px;
+            color: #2196f3;
+            text-decoration: none;
+        }
+        td .file:hover{
+            text-underline: #2196f3;
+            text-decoration: underline;
+        }
+    </style>
+    <script>
+        function searchData(){
+            const keyword = document.getElementById('search_input').value;
+            const status = document.getElementById('status_select').value;
+            window.location.href = `{{ url_for('project.sub_project_list',project_id=project.id) }}?k=${keyword}&s=${status}`;
+        }
+        function reSearchData(){
+            window.location.href = `{{ url_for('project.sub_project_list',project_id=project.id) }}`;
+        }
+        function jumpToPage(page) {
+            window.location.href = `{{ url_for('project.sub_project_list', project_id=project.id) }}?s={{status}}&k={{keyword}}&pp={{per_page}}&p=${page}`
+        }
+        function detail(id){
+            window.location.href = `{{  url_for('project.project_item_list',sub_project_id='') }}${id}`
+        }
+    </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('project.main_project_list') }}`)">返回</button>
+                <h3 class="box_title">工程列表</h3>
+                <span class="separator">|</span>
+                <dl>
+                    <dt>工程名称:</dt>
+                    <dd>{{ project.project_name }}</dd>
+                </dl>
+            </div>
+        </div>
+        <div class="box_header">
+            <div class="btn_box">
+                <button type="button" class="btn btn-success btn-large" onclick="addSubProject()">新建工程</button>
+            </div>
+            <div class="search_box">
+                <select id="status_select" class="form-control form-select" title="状态选择">
+                    <option value="-1" {% if status == -1 %}selected{% endif %}>所有状态</option>
+                    <option value="0" {% if status == 0 %}selected{% endif %}>新建</option>
+                    <option value="5" {% if status == 5 %}selected{% endif %}>完成</option>
+                     <option value="11" {% if status == 11 %}selected{% endif %}>采集失败</option>
+                    <option value="12" {% if status == 12 %}selected{% endif %}>分析失败</option>
+                    <option value="13" {% if status == 13 %}selected{% endif %}>上传失败</option>
+                     <option value="21" {% if status == 21 %}selected{% endif %}>采集中...</option>
+                    <option value="22" {% if status == 22 %}selected{% endif %}>分析中...</option>
+                    <option value="23" {% if status == 23 %}selected{% endif %}>上传中...</option>
+                    <option value="31" {% if status == 31 %}selected{% endif %}>采集完成</option>
+                    <option value="32" {% if status == 32 %}selected{% endif %}>分析完成</option>
+                    <option value="33" {% if status == 32 %}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="330px">工程名称</th>
+                        <th width="25%">工程分类</th>
+                        <th width="">文件</th>
+<!--                        <th width="120px">标准版本</th>-->
+                        <th width="120px">状态</th>
+                        <th width="340px">操作</th>
+                    </tr>
+                </thead>
+                <tbody>
+                {% if sub_project_list %}
+                    {% for item in sub_project_list %}
+                        <tr data-id="{{item.id}}">
+                            <td class="sub_project_name">
+                                {{ item.sub_project_name if item.sub_project_name else '-' }}
+                                <input type="hidden" class="form-control" title="工程名称" name="sub_project_name" value="{{ item.sub_project_name if item.sub_project_name else '' }}">
+                            </td>
+                            <td class="work_catalog">
+                                {{ item.work_catalog if item.work_catalog else '-' }}
+                                <input type="hidden" class="form-control" title="工作目录" name="work_catalog" value="{{ item.work_catalog if item.work_catalog else '' }}">
+                            </td>
+                            <td class="">
+                                {% if item.file_paths %}
+                                {% for file in item.file_paths.split(",") %}
+                                <a class="file" href="{{ url_for('project.download_file') }}?f={{file}}&p={{item.project_id}}&sp={{item.id}}" download>{{ file|basename }}</a>
+                                 {% endfor %}
+                                 {% endif %}
+                            </td>
+<!--                            <td class="work_content">
+                                {{ item.work_content if item.work_content else '-' }}
+                                <input type="hidden" class="form-control" title="工作内容" value="{{ item.work_content if item.work_content else '' }}">
+                            </td>-->
+<!--                            <td class="standard_version">
+                                {% if item.standard_version == '1' %}
+                                    <span class="label label-warning">旧版</span>
+                                    {% elif item.standard_version == '2' %}
+                                    <span class="label label-info">新版</span>
+                                    {% endif %}
+                                <select class="form-control hide" title="标准版本">
+                                    <option value="1" {% if item.standard_version == '1' %}selected{% endif %}>旧版</option>
+                                    <option value="2" {% if item.standard_version == '2' %}selected{% endif %}>新版</option>
+                                </select>
+                            </td>-->
+                            <td>
+                                <span class="status">
+                                    {% if item.status == 0 %}
+                                    <span class="label label-default">新建</span>
+                                    {% elif item.status == 21 %}
+                                    <span class="label label-warning">采集中...</span>
+                                    {% elif item.status == 22 %}
+                                    <span class="label label-warning">分析中...</span>
+                                    {% elif item.status == 23 %}
+                                    <span class="label label-warning">上传中...</span>
+                                    {% elif item.status == 31 %}
+                                    <span class="label label-info">采集完成</span>
+                                    {% elif item.status == 32 %}
+                                    <span class="label label-info">分析完成</span>
+                                    {% elif item.status == 33 %}
+                                    <span class="label label-info">上传完成</span>
+                                    {% elif item.status == 11 %}
+                                    <span class="label label-danger">采集失败</span>
+                                    {% elif item.status == 12 %}
+                                    <span class="label label-danger">分析失败</span>
+                                    {% elif item.status == 13 %}
+                                    <span class="label label-danger">上传失败</span>
+                                    {% elif item.status == 5 %}
+                                    <span class="label label-success">完成</span>
+                                    {% endif %}
+                                </span>
+                            </td>
+                            <td>
+                            {% if item.status == 0 %}
+                                <button class="btn btn-success" type="button" onclick="startSubProjectTask('{{ item.id }}')">开始任务</button>
+                                {% elif item.status == 11 %}
+                                <button class="btn btn-success" type="button" onclick="reStartSubProjectTask('{{ item.id }}')">重新采集</button>
+                                {% elif item.status == 12 %}
+                                <button class="btn btn-success" type="button" onclick="reStartProcessSubProjectTask('{{ item.id }}')">重新处理</button>
+                                {% elif item.status == 13 %}
+                                <button class="btn btn-success" type="button" onclick="reStartSendSubProjectTask('{{ item.id }}')">重新发送</button>
+                                {% endif %}
+                                <button class="btn btn-info" type="button" onclick="detail('{{ item.id }}')">详情</button>
+                                {% if item.status != 21  and item.status != 22 %}
+                                <button class="btn btn-info" onclick="updateSubProject(this.parentNode.parentNode.parentNode,'{{ item.id }}')">编辑</button>
+                                <button class="btn btn-danger" onclick="deleteSubProject('{{ item.id }}')">删除</button>
+                                {% endif %}
+                            </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 %}
+                    <span class="page page-link" onclick="jumpToPage(1)">首页</span>
+                    {% endif %} {% if page > 1 %}
+                    <span class="page page-link" onclick="jumpToPage(`{{page - 1}}`)">上一页</span>
+                    {% endif %} {% set start_page = [1, page - 2]|max %} {% set end_page = [total_pages, page + 2]|min %} {% if start_page > 1 %}
+                    <span class="page page-link" onclick="jumpToPage(1)">1</span>
+                    {% if start_page > 2 %}
+                    <span class="page">...</span>
+                    {% endif %} {% endif %} {% for p in range(start_page, end_page + 1) %} {% if p == page %}
+                    <span class="page page-link active" onclick="jumpToPage(`{{ p }}`)">{{ p }}</span>
+                    {% else %}
+                    <span class="page page-link" onclick="jumpToPage(`{{ p }}`)">{{ p }}</span>
+                    {% endif %} {% endfor %} {% if end_page < total_pages %} {% if end_page < total_pages - 1 %}
+                    <span class="page">...</span>
+                    {% endif %}
+                    <span class="page page-link" onclick="jumpToPage(`{{ total_pages }}`)">{{ total_pages }}</span>
+                    {% endif %} {% if page < total_pages %}
+                    <span class="page page-link" onclick="jumpToPage(`{{ page+1 }}`)">下一页</span>
+                    {% endif %} {% if page < total_pages %}
+                    <span class="page page-link" onclick="jumpToPage(`{{ total_pages }}`)">末页</span>
+                    {% endif %}
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="modal" id="modal-sub_project" 
+    ondragover="handleDragOver(event)"
+    ondragleave="handleDragLeave(event)"
+    ondrop="handleDrop(event)">
+        <div class="modal-dialog">
+			<div class="modal-content">
+				<div class="modal-header">
+					<h3 class="modal-title"><span></span>工程</h3>
+					<span class="close-btn" onclick="hide_modal('#modal-sub_project')">&times;</span>
+				</div>
+				<div class="modal-body">
+					<div class="form-group">
+                    <input type="hidden" id="sub_id" class="form-control">
+                    <input type="hidden" id="project_id" value="{{project.id}}">
+                    <div>
+                        <label for="sub_project_name">工程名称:</label>
+                        <input type="text" id="sub_project_name" class="form-control" placeholder="工程名称">
+                    </div>
+                    <div class="file-upload-area">
+                        <label>工程数据:</label>
+                        <div class="file-upload-box">
+                            <div class="drag-drop-box" id="dragDropArea"
+                             >
+                            <div class="drag-text">
+                                <span>点击选择文件或拖拽文件至此</span>
+                                <input type="file" id="sub_project_file" class="form-control" accept=".xlsx,.xls,.csv" multiple
+                                    style="display:none"
+                                    onchange="handleFileSelect(event)">
+                            </div>
+                        </div>
+
+                        <div class="file-list" id="fileList"></div>
+                        </div>
+                    </div>
+                    <div>
+                        <label for="delete_old_data">删除原数据:</label>
+                        <input type="checkbox" id="delete_old_data" class="form-control" style="width: auto;flex: 0;">
+                    </div>
+                    <div>
+                        <label for="work_catalog">工程类别:</label>
+                        <input type="text" id="work_catalog" class="form-control" placeholder="工作目录"></input>
+                    </div>
+                  <!--  <div>
+                        <label for="work_content">工程内容:</label>
+                        <textarea id="work_content" class="form-control large" placeholder="工作内容"></textarea>
+                    </div>-->
+                    <!--   <div>
+                       <label for="standard_version">标准版本:</label>
+                       <select id="standard_version" class="form-control">
+                           <option value="1">旧版</option>
+                           <option value="2">新版</option>
+                       </select>
+                   </div>-->
+
+            </div>
+            </div>
+				<div class="modal-footer">
+					<button class="btn btn-success btn-large" onclick="saveSubProject()">保存</button>
+					<button class="btn btn-warning btn-large" onclick="hide_modal()">取消</button>
+				</div>
+			</div>
+        </div>
+    </div>
+</body>
+</html>

+ 240 - 0
SourceCode/DataMiddleware/app/ui/views_project.py

@@ -0,0 +1,240 @@
+from flask import Blueprint, render_template, request, redirect, url_for, jsonify, send_from_directory
+import os, utils
+
+def basename_filter(path):
+    return os.path.basename(path)
+
+from .services_project import ProjectService
+from models.project_data import ProjectModel, SubProjectModel,SubProjectItemModel
+
+ProjectService = ProjectService()  # 确保 SourceDataService 是一个实例
+
+# 创建一个名为 'project' 的蓝图
+project = Blueprint('project', __name__, template_folder='templates/project')
+
+@project.route('/')
+def index():
+    return redirect(url_for('project.main_project_list'))
+
+@project.route('/main_project_list')
+def main_project_list():
+    page = request.args.get('p', 1, type=int)
+    per_page = request.args.get('pp', 10, type=int)
+    keyword = request.args.get('k', '')
+    projects, total_count = ProjectService.get_all_project_paginated(page, per_page, keyword)
+    return render_template('project_list.html', project_list=projects, keyword=keyword, page=page, per_page=per_page, total_count=total_count)
+
+@project.route('/save_project', methods=['POST'])
+def save_project():
+    try:
+        data = request.get_json()
+        project_id = data.get('project_id')
+        project_name = data.get('project_name')
+        project_desc = data.get('project_desc')
+        if not project_name:
+            return jsonify({'success': False, 'error': '项目编号和名称不能为空'}), 400
+        if project_id:
+            project_data = ProjectService.get_project_by_id(int(project_id))
+            if not project_data:
+                return jsonify({'success': False, 'error': '项目编号不存在'}), 400
+            project_data.project_name = project_name
+            project_data.description = project_desc
+            result, msg = ProjectService.update_project(project_data)
+        else:
+            project_data = ProjectModel(project_name=project_name, description=project_desc)
+            result, msg = ProjectService.add_project(project_data)
+        if not result:
+            return jsonify({'success': False, 'error': msg}), 400
+        return jsonify({'success': True})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)}), 400
+
+@project.route('/delete_project/<int:project_id>', methods=['POST'])
+def delete_project(project_id:int):
+    try:
+       success, message = ProjectService.delete_project(project_id)
+       if success:
+           return jsonify({'success': True}), 200
+       return jsonify({'success': False, 'error': message}), 400
+    except Exception as e:
+       return jsonify({'success': False, 'error': f'删除项目失败{str(e)}'}), 400
+
+
+@project.route('/download')
+def download_file():
+    filename = request.args.get('f', type=str)
+    if not filename:
+        return jsonify({'success': False, 'error': '文件名不能为空'}), 400
+    try:
+        # 安全处理文件名
+        pure_filename = os.path.basename(filename)
+        safe_filename = os.path.basename(pure_filename)
+        path = filename.replace(safe_filename, '')
+        upload_folder = os.path.abspath(os.path.join(os.getcwd(),path))
+        if not os.path.exists(upload_folder):
+            return jsonify({'success': False, 'error': '项目目录不存在'}), 404
+        full_path = os.path.join(upload_folder, safe_filename)
+        if not os.path.exists(full_path):
+            return jsonify({'success': False, 'error': '文件不存在'}), 404
+    except Exception as e:
+        utils.get_logger().error(f'路径构建失败:{str(e)}')
+        return jsonify({'success': False, 'error': f'非法文件路径{str(e)}'}), 400
+    return send_from_directory(upload_folder.replace('\\', '/'), safe_filename, as_attachment=True)
+@project.route('/sub_project_list/<int:project_id>')
+def sub_project_list(project_id:int):
+    project_data = ProjectService.get_project_by_id(project_id)
+    keyword = request.args.get('k', '', type=str)
+    page = request.args.get('p', 1, type=int)
+    per_page = request.args.get('pp', 10, type=int)
+    status = request.args.get('s',-1, type=int)
+    data_list, total_count = ProjectService.get_all_sub_project_paginated(project_id, page, per_page, keyword, None if status == -1 else status)  # 传递 status 参数
+    return render_template('sub_project_list.html', project=project_data ,sub_project_list=data_list, keyword=keyword, page=page, per_page=per_page, total_count=total_count, status=status)  # 传递 status 参数到模板
+
+
+@project.route('/save_sub_project', methods=['POST'])
+def save_sub_project():
+    """处理子项目保存请求,包含文件上传和数据处理"""
+    try:
+        # 获取表单数据
+        sub_id = request.form.get('id', type=int)
+        project_id = request.form.get('project_id', type=int)
+        sub_project_name = request.form.get('sub_project_name', '')
+        standard_version = request.form.get('standard_version', '')
+        work_catalog = request.form.get('work_catalog', '')
+        work_content = request.form.get('work_content', '')
+        delete_old = request.form.get('delete_old_data', 'false') == 'true'
+        project_files = request.files.getlist('project_files')
+        # 获取或创建子项目对象
+        project_data = SubProjectModel(
+            sub_id=sub_id,
+            project_id=project_id,
+            sub_project_name=sub_project_name,
+            standard_version=standard_version,
+            work_catalog=work_catalog,
+            work_content=work_content,
+        )
+        res, msg = ProjectService.save_sub_project(project_data, project_files, delete_old)
+        if res:
+            return jsonify({'success': True}), 200
+        return jsonify({'success': False, 'error': msg}), 400
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)}), 400
+
+@project.route('/update_sub_project', methods=['POST'])
+def update_sub_project():
+    try:
+        req = request.get_json()
+        sub_project_id = req.get('id')
+        project_name = req.get('project_name')
+        standard_version = req.get('standard_version')
+        work_catalog = req.get('work_catalog')
+        work_content = req.get('work_content')
+        project_data = ProjectService.get_sub_project_by_id(sub_project_id)
+        if not project_data:
+            return jsonify({'success': False, 'error': '项目不存在'}), 404
+        data = SubProjectModel(sub_id=sub_project_id,
+                               sub_project_name=project_name,
+                               standard_version=standard_version,
+                               work_catalog=work_catalog,
+                               work_content=work_content,
+                               status=project_data.status)
+        ProjectService.update_sub_project(data)
+        return jsonify({'success': True})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)}), 400
+@project.route('/start_sub_project_task/<project_id>', methods=['POST'])
+def start_sub_project_task(project_id:int):
+   try:
+       result, err_msg = ProjectService.start_sub_project_task(project_id)
+       return jsonify({'success': result, 'error': err_msg})
+   except Exception as e:
+       return jsonify({'success': False, 'error': str(e)})
+
+@project.route('/start_process_sub_project/<project_id>', methods=['POST'])
+def start_process_sub_project(project_id:int):
+    try:
+        result, err_msg = ProjectService.process_sub_project(project_id)
+        return jsonify({'success': result, 'error': err_msg})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})
+@project.route('/start_send_sub_project/<project_id>', methods=['POST'])
+def start_send_sub_project(project_id:int):
+    try:
+        result, err_msg = ProjectService.start_send_sub_project(project_id)
+        return jsonify({'success': result, 'error': err_msg})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})
+@project.route('/delete_sub_project/<project_id>', methods=['POST'])
+def delete_sub_project(project_id:int):
+    try:
+        result, err_msg =  ProjectService.delete_sub_project(project_id)
+        return jsonify({'success': result, 'error': err_msg})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})
+
+
+@project.route('/project_item_list/<sub_project_id>')
+def project_item_list(sub_project_id:int):
+    process_status = request.args.get('p_s', -1, type=int)
+    send_status = request.args.get('s_s', -1, type=int)
+    keyword = request.args.get('k', '', type=str)
+    page = request.args.get('p', 1, type=int)
+    per_page = request.args.get('pp', 10, type=int)
+    sub_project_data= ProjectService.get_sub_project_by_id(sub_project_id)
+    sub_items, total_count = ProjectService.get_sub_project_item_list_by_sub_project_paginated(sub_project_id, page, per_page, keyword, process_status if process_status != -1 else None, send_status if send_status != -1 else None)
+    return render_template('sub_project_item_list.html', sub_project=sub_project_data,items=sub_items, process_status=process_status, send_status=send_status, keyword=keyword, page=page, per_page=per_page, total_count=total_count)
+
+@project.route('/save_sub_project_item', methods=['POST'])
+def save_sub_project_item():
+    try:
+        data = request.get_json()
+        item_id = data.get('id')
+        sub_id = data.get('sub_id')
+        device_name = data.get('device_name')
+        device_model = data.get('device_model')
+        device_unit = data.get('device_unit')
+        device_count = data.get('device_count')
+        standard_no = data.get('standard_no')
+        is_query =  data.get('is_query') == True or data.get('is_query') == 'true'
+        if  item_id:
+            item_data = ProjectService.get_sub_project_item_by_id(item_id)
+            # 更新项目
+            item_data.device_name = device_name
+            item_data.device_model = device_model
+            item_data.device_unit = device_unit
+            item_data.device_count = float(device_count)
+            item_data.standard_no = standard_no
+            ProjectService.update_sub_project_item(item_data)
+        else:
+            sub_project = ProjectService.get_sub_project_by_id(sub_id)
+            if not sub_project:
+                return jsonify({'success': False, 'error': '项目工程不存在'}), 404
+            new_item = SubProjectItemModel(project_id=sub_project.project_id,sub_project_id=sub_id, device_name=device_name,device_count=float(device_count), device_model=device_model, device_unit=device_unit, standard_no=standard_no)
+            item_id = ProjectService.add_sub_project_item(new_item)
+        if is_query:
+            ProjectService.start_process_item(item_id)
+
+        return jsonify({'success': True})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)}), 400
+@project.route('/delete_sub_project_item/<item_id>', methods=['POST'])
+def delete_sub_project_item(item_id):
+    try:
+        ProjectService.delete_sub_project_item(item_id)
+        return jsonify({'success': True})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)}), 400
+@project.route('/start_process_item/<item_id>', methods=['POST'])
+def start_process_item(item_id:int):
+    try:
+        result, err_msg = ProjectService.start_process_item(item_id)
+        return jsonify({'success': result, 'error': err_msg})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})
+@project.route('/start_send_item/<item_id>', methods=['POST'])
+def start_send_item(item_id:int):
+    try:
+        result, err_msg = ProjectService.start_send_item(item_id)
+        return jsonify({'success': result, 'error': err_msg})
+    except Exception as e:
+        return jsonify({'success': False, 'error': str(e)})

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

@@ -126,6 +126,8 @@ def clean_report_file(day: int):
     """
     FileHelper().clean_report_file(day)
 
+def encode_file(path: str):
+    return FileHelper.encode_file(path)
 
 def to_array(s: str, split: str = ",") -> list[str]:
     """

+ 64 - 14
SourceCode/DataMiddleware/app/utils/ai_helper.py

@@ -3,7 +3,7 @@ import re
 
 import utils
 from openai import OpenAI
-
+from pathlib import Path
 
 class AiHelper:
 
@@ -20,19 +20,7 @@ class AiHelper:
             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 没有配置")
-
+        self.check_api(api_key, api_model, api_url)
         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)
@@ -72,6 +60,20 @@ class AiHelper:
         except Exception as e:
             raise Exception(f"解析 AI 响应错误: {e}")
 
+    def check_api(self, api_key, api_model, api_url):
+        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 没有配置")
+
     @staticmethod
     def _extract_message_content(response_json: dict) -> str:
         utils.get_logger().info(f"AI Response JSON: {response_json}")
@@ -118,3 +120,51 @@ class AiHelper:
                 return self._parse_response(message_content, False)
             else:
                 raise Exception(f"解析 AI 响应错误: {response} {e}")
+
+
+    def call_openai_with_image(self, image_path,system_prompt: str, user_prompt: str, api_url: str=None,api_key: str=None,api_model: str=None) -> json:
+        pass
+
+    def call_openai_with_file(self, file_path,system_prompt: str, user_prompt: str, api_url: str=None,api_key: str=None,api_model: str=None)->json:
+        self.check_api(api_key, api_model, api_url)
+        utils.get_logger().info(f"调用AI API File==> Url:{self._ai_api_url},Model:{self._api_model}")
+
+        client = OpenAI(api_key=self._ai_api_key, base_url=self._ai_api_url)
+        file_object = client.files.create(    file=Path(file_path),purpose='file-extract',)
+        completion = client.chat.completions.create(
+            model=self._api_model,
+            messages=[
+                {
+                    "role": "system",
+                    # "content": system_prompt,
+                    'content': f'fileid://{file_object.id}'
+                },
+                {
+                    "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}")
+        pass

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

@@ -6,6 +6,8 @@ from urllib.parse import urlparse
 
 import pandas as pd
 import requests
+import mimetypes
+import base64
 
 
 class FileHelper:
@@ -167,3 +169,18 @@ class FileHelper:
                         continue
         except Exception as e:
             utils.get_logger().error(f"Report 文件清理失败。Exception: {e}")
+
+    @staticmethod
+    def encode_file(file_path: str):
+        if not os.path.exists(file_path):
+            utils.get_logger().error(f"文件不存在: {file_path}")
+            raise FileNotFoundError(f"文件不存在: {file_path}")
+        # 根据文件扩展名获取 MIME 类型
+        mime_type, _ = mimetypes.guess_type(file_path)
+        if mime_type is None:
+            mime_type = 'image/jpeg'  # 默认使用 jpeg 类型
+        # 将图片编码为 base64 字符串
+        with open(file_path, "rb") as image_file:
+            encoded_string = base64.b64encode(image_file.read())
+            base64_str = encoded_string.decode("utf-8")
+            return f"data:{mime_type};base64,{base64_str}"

+ 25 - 4
SourceCode/DataMiddleware/app/utils/mysql_helper.py

@@ -51,22 +51,43 @@ class MySQLHelper:
             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()
+
+    # 修改 execute_non_query 使其返回ID
     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)
+            return self.execute_many(query, params)
         elif isinstance(params, tuple):
-            self.execute(query, params)
+            return self.execute(query, params)
         else:
-            self.execute(query, (params,))
-
+            return self.execute(query, (params,))
     def execute(self, query, params=None):
         try:
             with self.connection.cursor() as cursor:
                 cursor.execute(query, params)
                 self.connection.commit()
+                return cursor.lastrowid  # 新增返回自增ID
         except pymysql.MySQLError as e:
             utils.get_logger().error(f"执行非查询时出错:{e}")
             self.connection.rollback()
+            return None
+
+
 
     def execute_many(self, query, params: list):
         if isinstance(params, list) and all(isinstance(p, tuple) for p in params):

+ 5 - 0
SourceCode/DataMiddleware/docker/.env

@@ -0,0 +1,5 @@
+MYSQL_ROOT_PASSWORD=123456qwertyu
+MYSQL_DATABASE=iwb_data_middleware_v1
+MYSQL_USER=iwb_data
+MYSQL_PASSWORD=123456iwb
+MYSQL_PORT=3536

+ 35 - 0
SourceCode/DataMiddleware/docker/Dockerfile

@@ -0,0 +1,35 @@
+# 第一阶段:构建
+# 使用官方的 Python 基础镜像
+FROM python:3.13-slim AS builder
+
+RUN mkdir /app
+
+WORKDIR /app
+# 明确指定 requirements.txt 的路径
+COPY requirements.txt .
+# 安装项目依赖
+RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
+# 在 builder 阶段添加调试命令
+# RUN pip freeze > installed-packages.txt
+
+# 复制项目文件到工作目录
+COPY app/ /app
+
+# 将/etc/localtime链接到上海时区文件
+RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
+
+# 第二阶段:运行
+FROM python:3.13-slim
+
+WORKDIR /app
+COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
+COPY --from=builder /app /app
+
+# 暴露端口(如果有需要)
+EXPOSE 5123
+
+# 设置环境变量(如果有需要)
+# ENV MY_VARIABLE=value
+
+# 运行项目
+CMD ["python", "main.py"]

+ 57 - 0
SourceCode/DataMiddleware/docker/docker-compose.yml

@@ -0,0 +1,57 @@
+version: '3.8'
+
+services:
+  tl-mysql:
+    image: mysql:8.0.39
+    container_name: y_tielu-data-middleware-mysql
+    environment:
+      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
+      - MYSQL_DATABASE=${MYSQL_DATABASE}
+      - MYSQL_USER=${MYSQL_USER}
+      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
+      - TZ=Asia/Shanghai
+      # - MYSQL_DEFAULT_AUTHENTICATION_PLUGIN=mysql_native_password
+    volumes:
+      - /home/docker/teilu_data_middleware_v1/mysql/log:/var/log/mysql
+      - /home/docker/teilu_data_middleware_v1/mysql/data:/var/lib/mysql
+      - /etc/localtime:/etc/localtime:ro
+      - /home/docker/teilu_data_middleware_v1/app/init.sql:/docker-entrypoint-initdb.d/init.sql # 挂载 init.sql 文件
+      # - ./.dev/mysql5.7/log:/var/log/mysql
+      # - ./.dev/mysql5.7/data:/var/lib/mysql
+      # - ./.dev/mysql8.0.39/log:/var/log/mysql
+      # - ./.dev/mysql8.0.39/data:/var/lib/mysql
+      # - ./init.sql:/docker-entrypoint-initdb.d/init.sql
+    ports:
+      - '${MYSQL_PORT}:3306'
+    networks:
+      - tielu-data-middleware-net
+    restart: always
+
+  tl-app:
+    build:
+      context: ../
+      dockerfile: .
+    image: y_tielu-data-middleware-app:1.0.0
+    container_name: y_tielu-data-middleware-app
+    depends_on:
+      - tl-mysql
+    environment:
+      - TZ=Asia/Shanghai
+      - APP_MYSQL__HOST=y_tielu-data-middleware-mysql
+      - APP_MYSQL__PORT=3306
+      - APP_MYSQL__DB=${MYSQL_DATABASE}
+      - APP_MYSQL__USER=${MYSQL_USER}
+      - APP_MYSQL__PASSWORD=${MYSQL_PASSWORD}
+    volumes:
+      - /home/docker/teilu_data_middleware_v1/app/config.yml:/app/config.yml
+      - /home/docker/teilu_data_middleware_v1/app/logs:/app/logs
+      - /home/docker/teilu_data_middleware_v1/app/temp_files:/app/temp_files
+    networks:
+      - tielu-data-middleware-net
+    ports:
+       - "7010:5123"
+    restart: always
+
+networks:
+  tielu-data-middleware-net:
+    driver: bridge

+ 7 - 0
SourceCode/DataMiddleware/requirements.txt

@@ -8,3 +8,10 @@ PyPDF2~=3.0.1
 pillow~=11.1.0
 pathlib~=1.0.1
 PyMuPDF~=1.25.3
+cryptography==41.0.4
+openpyxl== 3.1.5
+tabulate==0.9.0
+
+
+pyxnat~=1.6.3
+httplib2~=0.22.0

+ 3 - 3
SourceCode/DataMiddleware/tools/config.yml

@@ -1,7 +1,7 @@
 mysql:
   host: 192.168.0.81
   port: 3307
-  db: iwb_data_tielu_pdf_dev
+  db: iwb_data_tielu_standard_dev
   user: root
   password: Iwb-2024
   charset: utf8mb4
@@ -20,5 +20,5 @@ ai:
   max_tokens: 10240
 
 fastgpt:
-  url: http://192.168.0.104:8020/api/v1/chat/completions
-  key: fastgpt-pzXtKVjkBU8NW8MUqZ7WnEfqK3m8qP6wmDdfcBgOaK2PZDekoHM1
+  api_url: http://192.168.0.104:8020/api
+  api_key: fastgpt-rSTjfs9BPv6KHUnqtz9vWHNSiPqJneOeBYDtMxqgsu6JgW2trJC7

+ 0 - 0
SourceCode/DataMiddleware/tools/image_extract/__init__.py


+ 138 - 0
SourceCode/DataMiddleware/tools/image_extract/extract.py

@@ -0,0 +1,138 @@
+import csv, json, tools.utils as utils, os
+
+from tools.stores.mysql_store import MysqlStore
+from tools.models.standard_model import StandardModel
+
+
+class ImageExtractor:
+    def __init__(self):
+        self._logger = utils.get_logger()
+        self._db_store = MysqlStore()
+        self._base_path = "./temp_files/images/output"
+        self._complete_path=""
+        self._ai = utils.AiHelper()
+        self._err_files=[]
+        self._file_name = ""
+        self._sys_prompt = "请提取图片中的表格,用json格式输出。"
+        self._user_prompt = """提取表格信息,要求:
+        1. 提取结构化信息:```typescript
+        type item { 
+        a: string; //书号
+        b: string; //定额编号
+        c:string; //定额名称
+        d: string; //工作内容
+        e: string; //单位
+        f: string; //基本定额
+        g: float; //基价(元)
+        h: float; //单重(t)
+        i: float; //工费
+        j: float; //料费
+        k: float; //机费
+        l: string; //主材
+        }
+        ```
+        2. 提取的文字中间的空格需要保留,数据没有就留空
+        3. 确保符号提取准确,例如 kg,m²,m³,直径符号∅等
+        4. 返回压缩成一行的item数组的json字符串
+        """
+    def extract(self,file_name: str):
+        self._file_name = file_name
+        self._err_files =[]
+        path = f"{self._base_path}/img/{self._file_name}/"
+        self._complete_path = f"{self._base_path}/img_complete/{self._file_name}/"
+        os.makedirs(self._complete_path , exist_ok=True)
+        try:
+            self._logger.info(f"开始处理目录: {path}")
+            # 确保目录存在
+            if not os.path.exists(path):
+                self._logger.error(f"目录不存在: {path}")
+                return
+            # 遍历目录下的所有文件
+            for root, dirs, files in os.walk(path):
+                for file in files:
+                    # 检查是否为图片文件
+                    if file.lower().endswith(('.png', '.jpg', '.jpeg')):
+                        image_path = os.path.join(root, file)
+                        self.extract_image(image_path)
+
+            self._logger.info(f"目录处理完成: {path}")
+            if len(self._err_files)>0:
+                self._logger.error(f"----【处理图片失败】-----: {self._err_files}")
+        except Exception as e:
+            self._logger.error(f"处理目录失败 {path}: {e}")
+    def extract_image(self, image_path: str) -> None:
+        try:
+            self._logger.info(f"开始处理图片: {image_path}")
+            # content = self._ai.call_openai_with_image(image_path,self._sys_prompt,self._user_prompt,api_model="qwen2.5-vl-72b-instruct")
+            api_key= utils.get_config_value("fastgpt.api_key")
+            content = self._ai.call_fastgpt_ai_with_image(image_path,self._user_prompt,api_key)
+            self.save_to_db(content)
+            # 保存成功后移动文件到已处理目录
+            os.rename(image_path, os.path.join(self._complete_path,os.path.basename(image_path)))
+            self._logger.info(f"图片处理完成: {image_path}")
+        except Exception as e:
+            self._err_files.append(image_path)
+            self._logger.error(f"处理图片失败 {image_path}: {e}")
+
+    def save_to_db(self, data_list: str|list) -> None:
+        try:
+            self._logger.info(f"开始保存图片内到数据库:{data_list}")
+            if isinstance(data_list,str):
+                data_list = json.loads(data_list)
+            for item in data_list:
+                try :
+                    standard = StandardModel(
+                        book_number=item['a'],
+                        quota_number=item['b'],
+                        quota_name=item['c'],
+                        work_content=item['d'],
+                        unit=item['e'],
+                        basic_quota=item['f'],
+                        base_price=item['g'],
+                        unit_weight=item['h'],
+                        labor_cost=item['i'],
+                        material_cost=item['j'],
+                        machine_cost=item['k'],
+                        main_material=item['l']
+                    )
+                    if not self._db_store.insert_standard(standard):
+                        self._logger.error(f"保存数据到数据库失败: {item}")
+                except Exception as e:
+                    self._logger.error(f"保存图片内容失败: {e}")
+                    continue
+        except Exception as e:
+            self._logger.error(f"保存图片内容失败: {e}")
+
+    def export(self):
+        try:
+            self._logger.info(f"开始导出数据库数据")
+            data = self._db_store.query_standard_group_by_book()
+            for k, v in data.items():
+                # 数据保存为 csv
+                csv_file = f"{self._base_path}/csv/{k}.csv"
+                # 确保目录存在
+                os.makedirs(os.path.dirname(csv_file), exist_ok=True)
+                with open(csv_file, 'w', newline='', encoding='utf-8-sig') as f:
+                    writer = csv.writer(f)
+                    writer.writerow(['书号', '定额编号', '定额名称', '工作内容', '单位', '基本定额', '基价(元)', '单重(t)', '工费', '料费', '机费', '主材'])
+                    for item in v:
+                        # 将 StandardModel 对象的属性提取出来,构造成一个列表
+                        row = [
+                            item.book_number,
+                            item.quota_number,
+                            item.quota_name,
+                            item.work_content,
+                            item.unit,
+                            item.basic_quota,
+                            item.base_price,
+                            item.unit_weight,
+                            item.labor_cost,
+                            item.material_cost,
+                            item.machine_cost,
+                            item.main_material
+                        ]
+                        writer.writerow(row)
+            self._logger.info(f"成功导出数据库数据")
+            return data
+        except Exception as e:
+            self._logger.error(f"导出数据库数据失败: {e}")

+ 0 - 0
SourceCode/DataMiddleware/tools/img_spilt/__init__.py


+ 9 - 0
SourceCode/DataMiddleware/tools/img_spilt/run.py

@@ -0,0 +1,9 @@
+from  tools.img_spilt.spilt import ImageSpilt
+from tools.image_extract.extract import ImageExtractor
+
+if __name__ == '__main__':
+    file_name ="01"
+    # ImageSpilt().split(file_name)
+    ImageExtractor().extract(file_name)
+    # ImageExtractor().extract_image("./temp_files/images/output/img/01/part_1.png")
+    ImageExtractor().export()

+ 71 - 0
SourceCode/DataMiddleware/tools/img_spilt/spilt.py

@@ -0,0 +1,71 @@
+import tools.utils as utils
+
+from PIL import Image
+import os
+
+
+class ImageSpilt:
+
+    def __init__(self,header_height:int=30,row_height:int=23,row_count:int=10):
+
+        self._logger = utils.get_logger()
+
+        self._header_height = header_height
+
+        self._row_height = row_height
+
+        self._row_count = row_count
+
+    def split(self,image_name:str, output_dir:str=None):
+
+        self._logger.info(f"开始处理图片{image_name}")
+        if output_dir is None:
+            output_dir = f"./temp_files/images/output/img/{image_name}"
+        # 确保输出目录存在
+        os.makedirs(output_dir, exist_ok=True)
+
+        image_path = f"./temp_files/images/source/{image_name}.png"
+        # 打开并读取图片
+        image = Image.open(image_path)
+        width, height = image.size
+        self._logger.info(f"图片尺寸为{width}x{height}")
+        # 提取表头部分
+        header = image.crop((0, 0, width, self._header_height))
+        # 计算总行数
+        content_height = height - self._header_height
+        total_rows = content_height // self._row_height
+        
+        # 计算需要生成的图片数量
+        image_count = (total_rows + self._row_count - 1) // self._row_count
+        self._logger.info(f"总行数:{total_rows},需要生成{image_count}张图片")
+
+        # 按图片切割
+        for img_index in range(image_count):
+            # 计算当前图片包含的行范围
+            start_row = img_index * self._row_count
+            end_row = min((img_index + 1) * self._row_count, total_rows)
+            current_rows = end_row - start_row
+
+            # 创建新图片(表头高度 + 当前行数 * 行高)
+            new_image = Image.new('RGB', (width, self._header_height + current_rows * self._row_height))
+            
+            # 粘贴表头
+            new_image.paste(header, (0, 0))
+            
+            # 粘贴每一行内容
+            for row in range(current_rows):
+                # 计算原图中的行位置
+                src_start_y = self._header_height + (start_row + row) * self._row_height
+                src_end_y = src_start_y + self._row_height
+                # 提取行内容
+                row_image = image.crop((0, src_start_y, width, src_end_y))
+                # 粘贴到新图片中
+                new_image.paste(row_image, (0, self._header_height + row * self._row_height))
+
+            # 保存切割后的图片
+            output_path = os.path.join(output_dir, f"part_{img_index + 1}.png")
+            new_image.save(output_path)
+            self._logger.info(f"已保存第{img_index + 1}张图片(包含{current_rows}行): {output_path}")
+
+        self._logger.info(f"图片处理完成,共生成{image_count}张图片")
+        

+ 22 - 0
SourceCode/DataMiddleware/tools/init.sql

@@ -1,6 +1,28 @@
+CREATE DATABASE IF NOT EXISTS iwb_data_tielu_standard_dev CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
+USE iwb_data_tielu_standard_dev;
+
+CREATE TABLE standard_data (
+    `book_number` VARCHAR(20) COMMENT '书号',
+    `quota_number` VARCHAR(20) COMMENT '定额编号',
+    `quota_name` VARCHAR(200) COMMENT '定额名称',
+    `work_content` TEXT COMMENT '工作内容',
+    `unit` VARCHAR(10) COMMENT '单位',
+    `basic_quota` VARCHAR(50) COMMENT '基本定额',
+    `base_price` DECIMAL(12,2) COMMENT '基价(元)',
+    `unit_weight` DECIMAL(12,2) COMMENT '单重(t)',
+    `labor_cost` DECIMAL(12,2) COMMENT '工费',
+    `material_cost` DECIMAL(12,2) COMMENT '料费',
+    `machine_cost` DECIMAL(12,2) COMMENT '机费',
+    `main_material` VARCHAR(100) COMMENT '主材',
+    `created_at`  datetime COMMENT '创建时间',
+    `updated_at`  datetime COMMENT '更新时间',
+    PRIMARY KEY (`book_number`, `quota_number`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标准数据表';
+
 -- 创建数据库
 CREATE DATABASE IF NOT EXISTS iwb_data_tielu_pdf_dev CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
 USE iwb_data_tielu_pdf_dev;
+
 -- 创建标准表
 CREATE TABLE IF NOT EXISTS pdf_standard (
     id INT AUTO_INCREMENT PRIMARY KEY,

+ 53 - 0
SourceCode/DataMiddleware/tools/models/standard_model.py

@@ -0,0 +1,53 @@
+from datetime import datetime
+
+class StandardModel:
+    def __init__(self,
+                 book_number: str = None,
+                 quota_number: str = None,
+                 quota_name: str = None,
+                 work_content: str = None,
+                 unit: str = None,
+                 basic_quota: str = None,
+                 base_price: str = None,
+                 unit_weight: str = None,
+                 labor_cost: str = None,
+                 material_cost: str = None,
+                 machine_cost: str = None,
+                 main_material: str = None,
+                 data_id: int = None,
+                 created_at: datetime = None,
+                 updated_at: datetime = None):
+        self.id = data_id
+        self.book_number = book_number
+        self.quota_number = quota_number
+        self.quota_name = quota_name
+        self.work_content = work_content
+        self.unit = unit
+        self.basic_quota = basic_quota
+        self.base_price = base_price
+        self.unit_weight = unit_weight
+        self.labor_cost = labor_cost
+        self.material_cost = material_cost
+        self.machine_cost = machine_cost
+        self.main_material = main_material
+        self.created_at = created_at or datetime.now()
+        self.updated_at = updated_at or datetime.now()
+
+    def to_dict(self) -> dict:
+        return {
+            'id': self.id,
+            'book_number': self.book_number,
+            'quota_number': self.quota_number,
+            'quota_name': self.quota_name,
+            'work_content': self.work_content,
+            'unit': self.unit,
+            'basic_quota': self.basic_quota,
+            'base_price': self.base_price,
+            'unit_weight': self.unit_weight,
+            'labor_cost': self.labor_cost,
+            'material_cost': self.material_cost,
+            'machine_cost': self.machine_cost,
+            'main_material': self.main_material,
+            'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
+            'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None
+        }

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


+ 106 - 0
SourceCode/DataMiddleware/tools/stores/mysql_store.py

@@ -0,0 +1,106 @@
+from datetime import datetime
+
+from httplib2.auth import params
+from pymupdf.utils import insert_text
+
+import tools.utils as utils
+from tools.utils.mysql_helper import MySQLHelper
+from tools.models.standard_model import StandardModel
+
+class MysqlStore:
+    def __init__(self):
+        self._db_helper = MySQLHelper()
+        self._logger = utils.get_logger()
+
+    def insert_standard(self, data: StandardModel) -> bool:
+        try:
+            sql = """
+                INSERT INTO standard_data (book_number, quota_number, quota_name, work_content, unit, basic_quota, base_price, unit_weight, labor_cost, material_cost, machine_cost, main_material, created_at)
+                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
+            """
+            with self._db_helper as db:
+                db.execute(sql, (
+                    data.book_number,
+                    data.quota_number,
+                    data.quota_name,
+                    data.work_content,
+                    data.unit,
+                    data.basic_quota,
+                    data.base_price,
+                    data.unit_weight,
+                    data.labor_cost,
+                    data.material_cost,
+                    data.machine_cost,
+                    data.main_material,
+                    datetime.now()
+                ))
+            return True
+        except Exception as e:
+            self._logger.error(f"Error inserting standard: {str(e)}")
+            return False
+    def insert_standard_batch(self, data_list: list[StandardModel]) -> bool:
+        try:
+            sql = """
+                INSERT INTO standard_data (book_number, quota_number, quota_name, work_content, unit, basic_quota, base_price, unit_weight, labor_cost, material_cost, machine_cost, main_material, created_at)
+                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
+            """
+            insert_params = [(
+                data.book_number,
+                data.quota_number,
+                data.quota_name,
+                data.work_content,
+                data.unit,
+                data.basic_quota,
+                data.base_price,
+                data.unit_weight,
+                data.labor_cost,
+                data.material_cost,
+                data.machine_cost,
+                data.main_material,
+                datetime.now()
+            ) for data in data_list]
+            self._db_helper.execute_many(sql, insert_params)
+        except Exception as e:
+            self._logger.error(f"Error inserting standard batch: {str(e)}")
+            return False
+    def query_standard_group_by_book(self):
+        sql = """
+            WITH grouped_data AS (
+                SELECT DISTINCT book_number
+                FROM standard_data
+            )
+            SELECT id, book_number, quota_number, quota_name, work_content, unit, basic_quota, base_price, unit_weight, labor_cost, material_cost, machine_cost, main_material
+            FROM standard_data sd
+            INNER JOIN grouped_data gd ON sd.book_number = gd.book_number
+            ORDER BY sd.book_number, sd.quota_number
+        """
+        with self._db_helper:
+            result = self._db_helper.execute_query(sql)
+            if not result:
+                return {}
+            
+            # 按book_number分组并转换为StandardModel
+            grouped_data = {}
+            for row in result:
+                book_number = row['book_number']
+                if book_number not in grouped_data:
+                    grouped_data[book_number] = []
+                
+                standard = StandardModel(
+                    book_number=row['book_number'],
+                    quota_number=row['quota_number'],
+                    quota_name=row['quota_name'],
+                    work_content=row['work_content'],
+                    unit=row['unit'],
+                    basic_quota=row['basic_quota'],
+                    base_price=row['base_price'],
+                    unit_weight=row['unit_weight'],
+                    labor_cost=row['labor_cost'],
+                    material_cost=row['material_cost'],
+                    machine_cost=row['machine_cost'],
+                    main_material=row['main_material'],
+                    data_id=row['id']
+                )
+                grouped_data[book_number].append(standard)
+            
+            return grouped_data

+ 180 - 28
SourceCode/DataMiddleware/tools/utils/ai_helper.py

@@ -1,5 +1,4 @@
-import json
-import re
+import json, os, re, requests
 
 import tools.utils as utils
 from tools.utils.file_helper import encode_image
@@ -13,29 +12,19 @@ class AiHelper:
     _ai_max_tokens = 150
 
     def __init__(self, api_url: str=None, api_key: str=None, api_model: str=None):
+        self._logger = utils.get_logger()
         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")
+        self._fastgpt_api_key = utils.get_config_value("fastgpt.api_key")
+        self._fastgpt_api_url = utils.get_config_value("fastgpt.api_url")
         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 没有配置")
-
+        self.check_api(api_key, api_model, api_url)
         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,
@@ -73,6 +62,20 @@ class AiHelper:
         except Exception as e:
             raise Exception(f"解析 AI 响应错误: {e}")
 
+    def check_api(self, api_key, api_model, api_url):
+        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 没有配置")
+
     @staticmethod
     def _extract_message_content(response_json: dict) -> str:
         utils.get_logger().info(f"AI Response JSON: {response_json}")
@@ -120,21 +123,47 @@ class AiHelper:
             else:
                 raise Exception(f"解析 AI 响应错误: {response} {e}")
 
+    def call_openai_with_image(self, image_path,system_prompt: str, user_prompt: str, api_url: str=None,api_key: str=None,api_model: str=None) -> json:
+        try:
+            self.check_api(api_key, api_model, api_url)
+            utils.get_logger().info(f"调用AI API IMAGE==> Url:{self._ai_api_url},Model:{self._api_model} {image_path}")
+            client = OpenAI(api_key=self._ai_api_key, base_url=self._ai_api_url)
+            if not os.path.exists(image_path):
+                utils.get_logger().error(f"图片文件不存在: {image_path}")
+                raise Exception(f"图片文件不存在: {image_path}")
+            base64_str = encode_image(image_path)
+            response = client.chat.completions.create(
+                model=self._api_model,
+                messages=[
+                    {
+                    "role": "system",
+                    "content": system_prompt,
+                    },
+                    {
+                        "role": "user",
+                        "content": [
+                            {"type": "text",
+                                "text": f"{user_prompt}"},
+                            {
+                                "type": "image_url",
+                                "image_url": {
+                                    "url": base64_str
+                                }
+                            }
+                        ]
+                    }
+                ],
+                response_format={"type": "json_object"},
+                timeout=600
+            )
+            return response.choices[0].message.content
+        except Exception as e:
+            raise Exception(f"调用 AI 错误: {e}")
+
     def analyze_image_with_ai(self,image_path, api_url: str=None,api_key: str=None,api_model: str=None):
         """调用OpenAI的API分析图片内容"""
-        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 没有配置")
         try:
+            self.check_api(api_key, api_model, api_url)
             client = OpenAI(api_key=self._ai_api_key, base_url=self._ai_api_url)
             base64_str = encode_image(image_path)
             response = client.chat.completions.create(
@@ -160,3 +189,126 @@ class AiHelper:
         except Exception as e:
             print(f"调用AI接口时出错: {e}")
         return ''
+
+
+    def call_fastgpt_ai(self, msg: str,api_key: str=None) -> json:
+        self._logger.info("调用fastgpt的AI接口")
+        try:
+            if api_key is not None:
+                self._fastgpt_api_key = api_key
+            if self._fastgpt_api_key is None:
+                self._logger.error("fastgpt.api_key 没有配置")
+                raise Exception("fastgpt.api_key 没有配置")
+
+            headers = {
+                "Authorization": f"Bearer {self._fastgpt_api_key}",
+                "Content-Type": "application/json"
+            }
+            url = f"{self._fastgpt_api_url}/v1/chat/completions"
+            data = {
+                # "model":"",
+                "stream": False,
+                "detail": False,
+                "messages": [
+                    {
+                        "role": "user",
+                        "content": msg
+                    }
+                ],
+                "response_format": {
+                    "type": "json_object"
+                }
+            }
+
+            response = requests.post(url, headers=headers, json=data)
+            if response.status_code == 200:
+                result = response.json()
+                self._logger.info(f"Response: {result}")
+                content = result.get("choices", [{}])[0].get("message", {}).get("content", "")
+                if content:
+                    # 使用正则表达式去除 content 前后的特定字符串
+                    content = re.sub(r'^```json\n', '', content.strip())
+                    content = re.sub(r'\n```$', '', content.strip())
+                    try:
+                        data = json.loads(content)
+                        self._logger.info(f"Response_JSON: {data}")
+                        return data
+                    except json.JSONDecodeError as e:
+                        self._logger.error(f"Failed to decode JSON: {e}")
+                        return None
+                return None
+            else:
+                error_msg = f"Error: {response.status_code} - {response.text}"
+                self._logger.error(error_msg)
+                return None
+        except Exception as e:
+            self._logger.error(f"Error: {str(e)}")
+            return None
+    def call_fastgpt_ai_with_image(self,image_path, msg: str,api_key: str=None) -> json:
+        self._logger.info("调用fastgpt的AI_Image接口")
+        try:
+            if not os.path.exists(image_path):
+                utils.get_logger().error(f"图片文件不存在: {image_path}")
+                raise Exception(f"图片文件不存在: {image_path}")
+            if api_key is not None:
+                self._fastgpt_api_key = api_key
+            if self._fastgpt_api_key is None:
+                self._logger.error("fastgpt.api_key 没有配置")
+                raise Exception("fastgpt.api_key 没有配置")
+
+            headers = {
+                "Authorization": f"Bearer {self._fastgpt_api_key}",
+                "Content-Type": "application/json"
+            }
+            url = f"{self._fastgpt_api_url}/v1/chat/completions"
+            base64_str = encode_image(image_path)
+            data = {
+                # "model":"",
+                "stream": False,
+                "detail": False,
+                "messages": [
+                    {
+                        "role": "user",
+                        "content": [
+                            {
+                                "type": "text",
+                                "text": msg
+                            },
+                            {
+                                "type": "image_url",
+                                "image_url": {
+                                    "url": base64_str
+                                }
+                            }
+                        ]
+                    }
+                ],
+                "response_format": {
+                    "type": "json_object"
+                }
+            }
+
+            response = requests.post(url, headers=headers, json=data)
+            if response.status_code == 200:
+                result = response.json()
+                self._logger.info(f"Response: {result}")
+                content = result.get("choices", [{}])[0].get("message", {}).get("content", "")
+                if content:
+                    # 使用正则表达式去除 content 前后的特定字符串
+                    content = re.sub(r'^```json\n', '', content.strip())
+                    content = re.sub(r'\n```$', '', content.strip())
+                    try:
+                        data = json.loads(content)
+                        self._logger.info(f"Response_JSON: {data}")
+                        return data
+                    except json.JSONDecodeError as e:
+                        self._logger.error(f"Failed to decode JSON: {e}")
+                        return None
+                return None
+            else:
+                error_msg = f"Error: {response.status_code} - {response.text}"
+                self._logger.error(error_msg)
+                return None
+        except Exception as e:
+            self._logger.error(f"Error: {str(e)}")
+            return None