YueYunyun 3 mesi fa
parent
commit
a806cc0444
21 ha cambiato i file con 992 aggiunte e 97 eliminazioni
  1. 106 0
      SourceCode/IntelligentRailwayCosting/.script/init_sqlserver.sql
  2. 5 0
      SourceCode/IntelligentRailwayCosting/Docker/.env
  3. 35 0
      SourceCode/IntelligentRailwayCosting/Docker/Dockerfile
  4. 57 0
      SourceCode/IntelligentRailwayCosting/Docker/docker-compose.yml
  5. 17 0
      SourceCode/IntelligentRailwayCosting/app/ai/__init__.py
  6. 123 0
      SourceCode/IntelligentRailwayCosting/app/ai/fastgpt.py
  7. 170 0
      SourceCode/IntelligentRailwayCosting/app/ai/openai.py
  8. 2 2
      SourceCode/IntelligentRailwayCosting/app/config.yml
  9. 4 4
      SourceCode/IntelligentRailwayCosting/app/core/configs/app_config.py
  10. 45 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/project_quota.py
  11. 111 63
      SourceCode/IntelligentRailwayCosting/app/core/dtos/quota_input.py
  12. 1 0
      SourceCode/IntelligentRailwayCosting/app/executor/collector.py
  13. 47 7
      SourceCode/IntelligentRailwayCosting/app/executor/processor.py
  14. 5 3
      SourceCode/IntelligentRailwayCosting/app/executor/sender.py
  15. 1 1
      SourceCode/IntelligentRailwayCosting/app/main.py
  16. 0 10
      SourceCode/IntelligentRailwayCosting/app/routes/project.py
  17. 1 0
      SourceCode/IntelligentRailwayCosting/app/stores/__init__.py
  18. 21 1
      SourceCode/IntelligentRailwayCosting/app/stores/project_quota.py
  19. 232 0
      SourceCode/IntelligentRailwayCosting/app/stores/quota_input.py
  20. 1 1
      SourceCode/IntelligentRailwayCosting/app/tools/db_helper/sqlserver_helper.py
  21. 8 5
      SourceCode/IntelligentRailwayCosting/app/views/static/project/budget_info.js

+ 106 - 0
SourceCode/IntelligentRailwayCosting/.script/init_sqlserver.sql

@@ -0,0 +1,106 @@
+-- 创建数据库
+CREATE DATABASE iwb_railway_costing_v1;
+GO
+
+USE iwb_railway_costing_v1;
+GO
+
+-- 创建项目任务表
+CREATE TABLE project_task (
+    id INT IDENTITY(1,1) PRIMARY KEY,
+    task_name NVARCHAR(255) NOT NULL, -- 任务名称
+    task_desc NVARCHAR(1000), -- 任务描述
+    project_id NVARCHAR(50) NOT NULL, -- 项目编号
+    budget_id INT NOT NULL, -- 概算序号
+    item_id INT NOT NULL, -- 条目序号
+    item_code NVARCHAR(255) NOT NULL, -- 条目编号
+    file_path TEXT, -- 文件路径
+    collect_status TINYINT NOT NULL DEFAULT 0, -- 采集状态(0:未开始, 1:进行中, 2:已完成, 3:采集失败)
+    collect_time DATETIME, -- 采集时间
+    collect_error TEXT, -- 采集错误信息
+    process_status TINYINT NOT NULL DEFAULT 0, -- 处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)
+    process_time DATETIME, -- 处理时间
+    process_error TEXT, -- 处理错误信息
+    send_status TINYINT NOT NULL DEFAULT 0, -- 发送状态(0:未发送,1:发送中 ,2:已发送, 3:发送失败)
+    send_time DATETIME, -- 发送时间
+    send_error TEXT, -- 发送错误信息
+    is_del TINYINT NOT NULL DEFAULT 0, -- 是否删除(0:否, 1:是)
+    deleted_by NVARCHAR(50), -- 删除人
+    deleted_at DATETIME, -- 删除时间
+    created_by NVARCHAR(50), -- 创建人
+    created_at DATETIME , -- 创建时间
+    updated_by NVARCHAR(50), -- 更新人
+    updated_at DATETIME -- 更新时间
+);
+GO
+
+-- 创建索引
+CREATE INDEX idx_project_id ON project_task (project_id);
+CREATE INDEX idx_budget_id ON project_task (budget_id);
+CREATE INDEX idx_item_id ON project_task (item_id);
+CREATE INDEX idx_item_code ON project_task (item_code);
+CREATE INDEX idx_created_at ON project_task (created_at);
+GO
+
+-- 创建项目定额表
+CREATE TABLE project_quota (
+    id INT IDENTITY(1,1) PRIMARY KEY,
+    task_id INT NOT NULL, -- 任务编号
+    project_id NVARCHAR(50) NOT NULL, -- 项目编号
+    budget_id INT NOT NULL, -- 概算序号
+    item_id INT NOT NULL, -- 条目序号
+    item_code NVARCHAR(255) NOT NULL, -- 条目编号
+    quota_code NVARCHAR(50), -- 定额编号
+    project_name NVARCHAR(255), -- 工程或费用项目名称
+    unit NVARCHAR(20), -- 单位
+    project_quantity FLOAT, -- 工程数量
+    project_quantity_input NVARCHAR(1000), -- 工程数量输入
+    quota_adjustment NVARCHAR(1000), -- 定额调整
+    unit_price FLOAT, -- 单价
+    total_price FLOAT, -- 合价
+    unit_weight FLOAT, -- 单重
+    total_weight FLOAT, -- 合重
+    labor_cost FLOAT, -- 人工费
+    process_status TINYINT NOT NULL DEFAULT 0, -- 处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)
+    process_time DATETIME, -- 处理时间
+    process_error TEXT, -- 处理错误信息
+    send_status TINYINT NOT NULL DEFAULT 0, -- 发送状态(0:未发送,1:发送中 ,2:已发送, 3:发送失败)
+    send_time DATETIME, -- 发送时间
+    send_error TEXT, -- 发送错误信息
+    is_del TINYINT NOT NULL DEFAULT 0, -- 是否删除(0:否, 1:是)
+    deleted_by NVARCHAR(50), -- 删除人
+    deleted_at DATETIME, -- 删除时间
+    created_by NVARCHAR(50), -- 创建人
+    created_at DATETIME , -- 创建时间
+    updated_by NVARCHAR(50), -- 更新人
+    updated_at DATETIME  -- 更新时间
+);
+GO
+
+-- 创建索引
+CREATE INDEX idx_project_id ON project_quota (project_id);
+CREATE INDEX idx_budget_id ON project_quota (budget_id);
+CREATE INDEX idx_item_id ON project_quota (item_id);
+CREATE INDEX idx_item_code ON project_quota (item_code);
+CREATE INDEX idx_created_at ON project_quota (created_at);
+GO
+
+-- 创建日志表
+CREATE TABLE sys_log (
+    id INT IDENTITY(1,1) PRIMARY KEY,
+    username NVARCHAR(255) NOT NULL, -- 用户名
+    operation_type NVARCHAR(50) NOT NULL, -- 操作类型
+    operation_desc NVARCHAR(1000), -- 操作描述
+    operation_result TINYINT, -- 操作结果(0:失败, 1:成功)
+    operation_module NVARCHAR(100), -- 操作模块
+    operation_data NVARCHAR(MAX), -- 操作数据
+    data_changes NVARCHAR(MAX), -- 数据变更记录
+    operation_ip NVARCHAR(50), -- 操作IP
+    created_at DATETIME NOT NULL DEFAULT GETDATE() -- 创建时间
+);
+GO
+
+-- 创建索引
+CREATE INDEX idx_username ON sys_log (username);
+CREATE INDEX idx_created_at ON sys_log (created_at);
+GO

+ 5 - 0
SourceCode/IntelligentRailwayCosting/Docker/.env

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

+ 35 - 0
SourceCode/IntelligentRailwayCosting/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/IntelligentRailwayCosting/Docker/docker-compose.yml

@@ -0,0 +1,57 @@
+version: '3.8'
+
+services:
+  rc-mysql:
+    image: mysql:8.0.39
+    container_name: railway_costing-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/iwb_railway_costing_v1/mysql/log:/var/log/mysql
+      - /home/docker/iwb_railway_costing_v1/mysql/data:/var/lib/mysql
+      - /etc/localtime:/etc/localtime:ro
+      - /home/docker/iwb_railway_costing_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:
+      - railway_costing_v1
+    restart: always
+
+  rc-app:
+    build:
+      context: ../
+      dockerfile: .
+    image: railway_costing-app:1.0.0
+    container_name: railway_costing-app
+    depends_on:
+      - rc-mysql
+    environment:
+      - TZ=Asia/Shanghai
+      - APP_MYSQL__HOST=railway_costing-mysql
+      - APP_MYSQL__PORT=3306
+      - APP_MYSQL__DB=${MYSQL_DATABASE}
+      - APP_MYSQL__USER=${MYSQL_USER}
+      - APP_MYSQL__PASSWORD=${MYSQL_PASSWORD}
+    volumes:
+      - /home/docker/iwb_railway_costing_v1/app/config.yml:/app/config.yml
+      - /home/docker/iwb_railway_costing_v1/app/logs:/app/logs
+      - /home/docker/iwb_railway_costing_v1/app/temp_files:/app/temp_files
+    networks:
+      - railway_costing_v1
+    ports:
+       - "7010:5123"
+    restart: always
+
+networks:
+  railway_costing_v1:
+    driver: bridge

+ 17 - 0
SourceCode/IntelligentRailwayCosting/app/ai/__init__.py

@@ -0,0 +1,17 @@
+from .fastgpt import FastGPTAi
+
+
+def call_fastgpt_ai(msg: str, api_key: str=None, api_url: str=None):
+    return FastGPTAi().call_ai(msg, api_key, api_url)
+def call_fastgpt_ai_with_file(file_path, msg: str,api_key: str=None, api_url: str=None):
+    return FastGPTAi().call_ai_with_file(file_path, msg,api_key, api_url)
+
+def call_fastgpt_ai_with_image(image_path, msg: str,api_key: str=None, api_url: str=None):
+    return FastGPTAi().call_ai_with_image(image_path, msg,api_key, api_url)
+
+__all__= [
+    'FastGPTAi',
+    'call_fastgpt_ai',
+    'call_fastgpt_ai_with_file',
+    'call_fastgpt_ai_with_image'
+]

+ 123 - 0
SourceCode/IntelligentRailwayCosting/app/ai/fastgpt.py

@@ -0,0 +1,123 @@
+
+import re ,json,requests, os
+import tools.utils as utils, core.configs as configs
+
+class FastGPTAi:
+
+    def __init__(self, api_key: str=None, api_url: str=None):
+        self._api_key = api_key or configs.fastgpt_ai.api_key
+        self._api_url = api_url or configs.fastgpt_ai.api_url
+        self._logger = utils.get_logger()
+        self._headers ={}
+
+    def call_ai(self, msg: str, api_key: str=None, api_url: str=None) -> json:
+        self._logger.info("调用 fastgpt 的AI接口")
+        try:
+            self._check_api(api_key, api_url)
+            url = f"{self._api_url}/v1/chat/completions"
+            headers = self._build_header()
+            data = self._build_data([{"role": "user","content": msg}])
+            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, api_url: str=None) -> json:
+        self._logger.info("调用 fastgpt 的AI_Image接口")
+        try:
+            self._check_api(api_key, api_url)
+            url = f"{self._api_url}/v1/chat/completions"
+            headers = self._build_header()
+            base64_str = utils.encode_file(image_path)
+            data = self._build_data([{"role": "user","content": [{"type": "text","text": msg },{"type": "image_url","image_url": {"url": base64_str}}]}])
+            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, api_url: str=None) -> json:
+        self._logger.info("调用 fastgpt 的AI_File接口")
+        try:
+            self._check_api(api_key, api_url)
+            url = f"{self._api_url}/v1/chat/completions"
+            headers = self._build_header()
+            file_name = os.path.basename(file_path)
+            base64_str = utils.encode_file(file_path)
+            data = self._build_data([{"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}")
+                    result_data = {
+                        'data': data,
+                        'raw_content': content
+                    }
+                except json.JSONDecodeError as e:
+                    self._logger.error(f"JSON解析失败,尝试清理特殊字符: {e}")
+                    # 尝试清理特殊字符后重新解析
+                    content = re.sub(r'[“”‘’\x00-\x1f]', '', content)
+                    try:
+                         result_data = {
+                            'data':json.loads(content),
+                            'raw_content': content
+                        }
+                    except json.JSONDecodeError:
+                         self._logger.error("最终JSON解析失败,返回原始内容")
+                         result_data = {
+                            'data': None,
+                            'raw_content': content
+                        }
+                return result_data
+            return None
+        else:
+            error_msg = f"Error: {response.status_code} - {response.text}"
+            self._logger.error(error_msg)
+            return None
+
+    def _check_api(self, api_key: str, api_url: str):
+        self._api_key = api_key if api_key else self._api_key
+        self._api_url = api_url if api_url else self._api_url
+        if api_key is None:
+            self._logger.error("fastgpt.api_key 没有配置")
+            raise Exception("fastgpt.api_key 没有配置")
+        if api_url is None:
+            self._logger.error("fastgpt.api_url 没有配置")
+            raise Exception("fastgpt.api_url 没有配置")
+
+    def _build_header(self):
+        return {
+            "Authorization": f"Bearer {self._api_key}",
+            "Content-Type": "application/json"
+        }
+
+    @staticmethod
+    def _build_data(message:list[dict]):
+        return {
+            "stream": False,
+            "detail": False,
+            "messages": message,
+            "response_format": {
+                "type": "json_object"
+            }
+        }

+ 170 - 0
SourceCode/IntelligentRailwayCosting/app/ai/openai.py

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

+ 2 - 2
SourceCode/IntelligentRailwayCosting/app/config.yml

@@ -2,7 +2,7 @@ app:
   # 应用名称
   name: '铁路造价智能化工具'
   version: '2024' # 应用版本 2020|2024
-  user_version: true
+  use_version: true
   collect_api_url: 'http://192.168.0.104:8020/api'
 db:
   # SQL Server 配置
@@ -66,7 +66,7 @@ fastgpt_ai:
   api_url: http://192.168.0.104:8020/api
   api_key: fastgpt-o4CF7Pu1FRTvHjWFqeNcClBS6ApyflNfkBGXo9p51fuBMAX1L0erU8yz8
   apps:
-    app_01_2020:
+    knowledge_01:
       api_url: http://192.168.0.104:8020/api
       api_key: fastgpt-o4CF7Pu1FRTvHjWFqeNcClBS6ApyflNfkBGXo9p51fuBMAX1L0erU8yz8
     app_02_2020:

+ 4 - 4
SourceCode/IntelligentRailwayCosting/app/core/configs/app_config.py

@@ -2,7 +2,7 @@ class AppConfig:
     """应用配置管理类"""
     _name = None
     _version = None
-    _user_version = None
+    _use_version = None
     _collect_api_url = None
 
     @property
@@ -14,8 +14,8 @@ class AppConfig:
         return self._version
 
     @property
-    def user_version(self)->bool:
-        return self._user_version
+    def use_version(self)->bool:
+        return self._use_version
 
     @property
     def collect_api_url(self)->str:
@@ -29,5 +29,5 @@ class AppConfig:
         """
         self._name = config.get('name')
         self._version = config.get('version')
-        self._user_version = config.get('user_version')
+        self._use_version = config.get('use_version')
         self._collect_api_url = config.get('collect_api_url')

+ 45 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/project_quota.py

@@ -72,9 +72,54 @@ class ProjectQuotaDto(BaseModel):
             updated_at=model.updated_at
         )
 
+
+
     def to_dict(self) -> dict:
         """转换为字典格式"""
         return self.model_dump()
 
+    @classmethod
+    def ai_prompt_struct(cls):
+
+        return """```typescript
+        export interface input_struct {  //输入数据结构体
+            i: // ID
+            n: string; //工程或费用项目名称
+            u:string; //单位
+            q: float; //数量
+        }
+        export interface item { //需返回的结构体
+            i: int; // ID 与提供的ID保持一致
+            q: string; // 定额编号
+            u:string; // 定额单位
+            p: float; // 定额单价
+            tp: float; // 总价
+        }
+        ```
+        """
+
+    @classmethod
+    def from_ai_dict(cls,data: dict):
+        return cls(
+            project_id="",
+            budget_id=0,
+            item_id=0,
+            item_code="",
+            id=data.get("i",0),
+            quota_code=data.get("q",""),
+            unit=data.get("u",""),
+            unit_price=data.get("p",0),
+            total_price=data.get("tp",0)
+        )
+
+    def to_ai_dict(self):
+        return {
+            "i": self.id,
+            "n": self.project_name,
+            "u": self.unit,
+            "q": self.project_quantity,
+        }
+
+
     class Config:
         from_attributes = True

+ 111 - 63
SourceCode/IntelligentRailwayCosting/app/core/dtos/quota_input.py

@@ -1,6 +1,7 @@
 from pydantic import BaseModel
 from typing import Optional
-from ..models.quota_input import QuotaInputModel
+from core.models.quota_input import QuotaInputModel
+from core.dtos.project_quota import ProjectQuotaDto
 
 class QuotaInputDto(BaseModel):
     """定额输入DTO"""
@@ -8,44 +9,44 @@ class QuotaInputDto(BaseModel):
     budget_id: int
     item_id: int
     quota_code: str
-    sequence_number: Optional[int] = None
+    # sequence_number: Optional[int] = None
     project_name: Optional[str] = None
     unit: Optional[str] = None
     project_quantity: Optional[float] = None
-    project_quantity_input: Optional[str] = None
-    quota_adjustment: Optional[str] = None
+    # project_quantity_input: Optional[str] = None
+    # quota_adjustment: Optional[str] = None
     unit_price: Optional[float] = None
-    compilation_unit_price: Optional[float] = None
+    # compilation_unit_price: Optional[float] = None
     total_price: Optional[float] = None
-    compilation_total_price: Optional[float] = None
-    unit_weight: Optional[float] = None
-    total_weight: Optional[float] = None
-    labor_cost: Optional[float] = None
-    compilation_labor_cost: Optional[float] = None
-    material_cost: Optional[float] = None
-    compilation_material_cost: Optional[float] = None
-    deduct_material_cost: Optional[float] = None
-    compilation_deduct_material_cost: Optional[float] = None
-    mechanical_cost: Optional[float] = None
-    compilation_mechanical_cost: Optional[float] = None
-    equipment_cost: Optional[float] = None
-    compilation_equipment_cost: Optional[float] = None
-    transport_cost: Optional[float] = None
-    compilation_transport_cost: Optional[float] = None
-    quota_workday: Optional[float] = None
-    total_workday: Optional[float] = None
-    workday_salary: Optional[float] = None
-    compilation_workday_salary: Optional[float] = None
-    quota_mechanical_workday: Optional[float] = None
-    total_mechanical_workday: Optional[float] = None
-    mechanical_workday_salary: Optional[float] = None
-    compilation_mechanical_workday_salary: Optional[float] = None
+    # compilation_total_price: Optional[float] = None
+    # unit_weight: Optional[float] = None
+    # total_weight: Optional[float] = None
+    # labor_cost: Optional[float] = None
+    # compilation_labor_cost: Optional[float] = None
+    # material_cost: Optional[float] = None
+    # compilation_material_cost: Optional[float] = None
+    # deduct_material_cost: Optional[float] = None
+    # compilation_deduct_material_cost: Optional[float] = None
+    # mechanical_cost: Optional[float] = None
+    # compilation_mechanical_cost: Optional[float] = None
+    # equipment_cost: Optional[float] = None
+    # compilation_equipment_cost: Optional[float] = None
+    # transport_cost: Optional[float] = None
+    # compilation_transport_cost: Optional[float] = None
+    # quota_workday: Optional[float] = None
+    # total_workday: Optional[float] = None
+    # workday_salary: Optional[float] = None
+    # compilation_workday_salary: Optional[float] = None
+    # quota_mechanical_workday: Optional[float] = None
+    # total_mechanical_workday: Optional[float] = None
+    # mechanical_workday_salary: Optional[float] = None
+    # compilation_mechanical_workday_salary: Optional[float] = None
     compiler: Optional[str] = None
     modify_date: Optional[str] = None
-    quota_consumption: Optional[str] = None
-    basic_quota: Optional[str] = None
-    quota_comprehensive_unit_price: Optional[float] = None
-    quota_comprehensive_total_price: Optional[float] = None
+    # quota_consumption: Optional[str] = None
+    # basic_quota: Optional[str] = None
+    # quota_comprehensive_unit_price: Optional[float] = None
+    # quota_comprehensive_total_price: Optional[float] = None
 
     @classmethod
     def from_model(cls, model: QuotaInputModel) -> 'QuotaInputDto':
@@ -55,46 +56,93 @@ class QuotaInputDto(BaseModel):
             budget_id=model.budget_id,
             item_id=model.item_id,
             quota_code=model.quota_code,
-            sequence_number=model.sequence_number,
+            # sequence_number=model.sequence_number,
             project_name=model.project_name,
             unit=model.unit,
             project_quantity=model.project_quantity,
-            project_quantity_input=model.project_quantity_input,
-            quota_adjustment=model.quota_adjustment,
+            # project_quantity_input=model.project_quantity_input,
+            # quota_adjustment=model.quota_adjustment,
             unit_price=model.unit_price,
-            compilation_unit_price=model.compilation_unit_price,
+            # compilation_unit_price=model.compilation_unit_price,
             total_price=model.total_price,
-            compilation_total_price=model.compilation_total_price,
-            unit_weight=model.unit_weight,
-            total_weight=model.total_weight,
-            labor_cost=model.labor_cost,
-            compilation_labor_cost=model.compilation_labor_cost,
-            material_cost=model.material_cost,
-            compilation_material_cost=model.compilation_material_cost,
-            deduct_material_cost=model.deduct_material_cost,
-            compilation_deduct_material_cost=model.compilation_deduct_material_cost,
-            mechanical_cost=model.mechanical_cost,
-            compilation_mechanical_cost=model.compilation_mechanical_cost,
-            equipment_cost=model.equipment_cost,
-            compilation_equipment_cost=model.compilation_equipment_cost,
-            transport_cost=model.transport_cost,
-            compilation_transport_cost=model.compilation_transport_cost,
-            quota_workday=model.quota_workday,
-            total_workday=model.total_workday,
-            workday_salary=model.workday_salary,
-            compilation_workday_salary=model.compilation_workday_salary,
-            quota_mechanical_workday=model.quota_mechanical_workday,
-            total_mechanical_workday=model.total_mechanical_workday,
-            mechanical_workday_salary=model.mechanical_workday_salary,
-            compilation_mechanical_workday_salary=model.compilation_mechanical_workday_salary,
+            # compilation_total_price=model.compilation_total_price,
+            # unit_weight=model.unit_weight,
+            # total_weight=model.total_weight,
+            # labor_cost=model.labor_cost,
+            # compilation_labor_cost=model.compilation_labor_cost,
+            # material_cost=model.material_cost,
+            # compilation_material_cost=model.compilation_material_cost,
+            # deduct_material_cost=model.deduct_material_cost,
+            # compilation_deduct_material_cost=model.compilation_deduct_material_cost,
+            # mechanical_cost=model.mechanical_cost,
+            # compilation_mechanical_cost=model.compilation_mechanical_cost,
+            # equipment_cost=model.equipment_cost,
+            # compilation_equipment_cost=model.compilation_equipment_cost,
+            # transport_cost=model.transport_cost,
+            # compilation_transport_cost=model.compilation_transport_cost,
+            # quota_workday=model.quota_workday,
+            # total_workday=model.total_workday,
+            # workday_salary=model.workday_salary,
+            # compilation_workday_salary=model.compilation_workday_salary,
+            # quota_mechanical_workday=model.quota_mechanical_workday,
+            # total_mechanical_workday=model.total_mechanical_workday,
+            # mechanical_workday_salary=model.mechanical_workday_salary,
+            # compilation_mechanical_workday_salary=model.compilation_mechanical_workday_salary,
             compiler=model.compiler,
             modify_date=model.modify_date,
-            quota_consumption=model.quota_consumption,
-            basic_quota=model.basic_quota,
-            quota_comprehensive_unit_price=model.quota_comprehensive_unit_price,
-            quota_comprehensive_total_price=model.quota_comprehensive_total_price
+            # quota_consumption=model.quota_consumption,
+            # basic_quota=model.basic_quota,
+            # quota_comprehensive_unit_price=model.quota_comprehensive_unit_price,
+            # quota_comprehensive_total_price=model.quota_comprehensive_total_price
         )
 
+    @classmethod
+    def from_quota_dto(cls, quota_dto: ProjectQuotaDto):
+        return cls(
+            budget_id=quota_dto.budget_id,
+            item_id=quota_dto.item_id,
+            quota_code=quota_dto.quota_code,
+            # sequence_number=quota_dto.sequence_number,
+            project_name=quota_dto.project_name,
+            unit=quota_dto.unit,
+            project_quantity=quota_dto.project_quantity,
+            # project_quantity_input=quota_dto.project_quantity_input,
+            # quota_adjustment=quota_dto.quota_adjustment,
+            unit_price=quota_dto.unit_price,
+            # compilation_unit_price=quota_dto.compilation_unit_price,
+            total_price=quota_dto.total_price,
+            # compilation_total_price=quota_dto.compilation_total_price,
+            # unit_weight=quota_dto.unit_weight,
+            # total_weight=quota_dto.total_weight,
+            # labor_cost=quota_dto.labor_cost,
+            # compilation_labor_cost=quota_dto.compilation_labor_cost,
+            # material_cost=quota_dto.material_cost,
+            # compilation_material_cost=quota_dto.compilation_material_cost,
+            # deduct_material_cost=quota_dto.deduct_material_cost,
+            # compilation_deduct_material_cost=quota_dto.compilation_deduct_material_cost,
+            # mechanical_cost=quota_dto.mechanical_cost,
+            # compilation_mechanical_cost=quota_dto.compilation_mechanical_cost,
+            # equipment_cost=quota_dto.equipment_cost,
+            # compilation_equipment_cost=quota_dto.compilation_equipment_cost,
+            # transport_cost=quota_dto.transport_cost,
+            # compilation_transport_cost=quota_dto.compilation_transport_cost,
+            # quota_workday=quota_dto.quota_workday,
+            # total_workday=quota_dto.total_workday,
+            # workday_salary=quota_dto.workday_salary,
+            # compilation_workday_salary=quota_dto.compilation_workday_salary,
+            # quota_mechanical_workday=quota_dto.quota_mechanical_workday,
+            # total_mechanical_workday=quota_dto.total_mechanical_workday,
+            # mechanical_workday_salary=quota_dto.mechanical_workday_salary,
+            # compilation_mechanical_workday_salary=quota_dto.compilation_mechanical_workday_salary,
+            compiler=quota_dto.created_by,
+            # quota_consumption=quota_dto.quota_consumption,
+            # basic_quota=quota_dto.basic_quota,
+            # quota_comprehensive_unit_price=quota_dto.quota_comprehensive_unit_price,
+            # quota_comprehensive_total_price=quota_dto.quota_comprehensive_total_price
+        )
+
+
+
     def to_dict(self) -> dict:
         """转换为字典格式"""
         return self.model_dump()

+ 1 - 0
SourceCode/IntelligentRailwayCosting/app/executor/collector.py

@@ -91,6 +91,7 @@ class Collector:
                     project_quantity_input=item['project_quantity_input'],
                     unit=item['unit'],
                     unit_weight=item['unit_weight'],
+                    created_by=task.created_by,
                 )
                 self._quota_store.create_quota(quota)
             self._logger.debug(f"插入数据完成:{task.task_name}")

+ 47 - 7
SourceCode/IntelligentRailwayCosting/app/executor/processor.py

@@ -1,5 +1,4 @@
-
-import tools.utils as utils
+import tools.utils as utils, core.configs as configs, ai
 from core.dtos import ProjectTaskDto, ProjectQuotaDto
 from stores import ProjectTaskStore,ProjectQuotaStore
 
@@ -32,11 +31,14 @@ class Processor:
 
     def process_quota(self,quota:ProjectQuotaDto):
         try:
-            self._logger.info(f"开始处理定额:{quota.id}")
+            self._logger.debug(f"开始处理定额:{quota.id}")
             self._quota_store.update_process_status(quota.id,1)
-
+            data,msg = self._call_ai(quota)
+            if not data:
+                raise Exception(msg)
+            self._update_quota(data)
             self._quota_store.update_process_status(quota.id,2)
-            self._logger.info(f"处理定额:{quota.id}完成")
+            self._logger.debug(f"处理定额:{quota.id}完成")
             return None
         except Exception as e:
             msg = f"定额处理失败,原因:{e}"
@@ -44,5 +46,43 @@ class Processor:
             self._quota_store.update_process_status(quota.id,3, msg)
             return msg
 
-    def _call_ai(self):
-        pass
+    def _call_ai(self,quota:ProjectQuotaDto):
+        try:
+            self._logger.debug(f"开始调用AI:{quota.id}")
+            app = configs.fastgpt_ai.apps.get(f"knowledge_01_{configs.app.version}" if configs.app.use_version else "knowledge_01")
+            if not app:
+                return None, "未找到AI应用"
+            data = ai.call_fastgpt_ai(self._build_prompt(quota), app.api_key, app.api_url)
+            if not data or not data.get("data") or not data.get("data").get("i"):
+                return None, "AI返回数据为空"
+            self._logger.debug(f"调用AI:{quota.id}完成")
+            return data.get("data"),''
+        except Exception as e:
+            msg = f"调用AI失败,原因:{e}"
+            self._logger.error(f"调用AI:{quota.id},{msg}")
+            return None,msg
+
+    def _build_prompt(self,quota:ProjectQuotaDto):
+        text=f"""
+        请分析提供的json数据,要求:
+            1. 根据工程或费用项目名称、数量、单位查找计算出定额编号
+            2. 结构体信息:{ProjectQuotaDto.ai_prompt_struct()}
+            3. 返回结构体item的json字符串,压缩成一行。
+            4. 输入数据如下:
+        """
+        text += utils.to_str(quota.to_ai_dict())
+        self._logger.debug(f"AI提示词:\n{text}")
+        return text
+
+    def _update_quota(self,data:dict):
+        quota_id = data.get("i","")
+        try:
+            self._logger.debug(f"开始更新定额编号:{quota_id}")
+            quota = ProjectQuotaDto.from_ai_dict(data)
+            self._quota_store.update_quota_code(quota)
+            self._logger.debug(f"更新定额编号:{quota.id}完成")
+            return None
+        except Exception as e:
+            msg = f"更新定额编号失败,原因:{e}"
+            self._logger.error(f"更新定额:{quota_id},{msg}")
+            return msg

+ 5 - 3
SourceCode/IntelligentRailwayCosting/app/executor/sender.py

@@ -1,6 +1,6 @@
 import tools.utils as utils
-from core.dtos import ProjectTaskDto, ProjectQuotaDto
-from stores import ProjectQuotaStore, ProjectTaskStore
+from core.dtos import ProjectTaskDto, ProjectQuotaDto, QuotaInputDto
+from stores import ProjectQuotaStore, ProjectTaskStore,QuotaInputStore
 
 
 class Sender:
@@ -8,6 +8,7 @@ class Sender:
         self._logger = utils.get_logger()
         self._task_store = ProjectTaskStore()
         self._quota_store = ProjectQuotaStore()
+        self._quota_input_store = QuotaInputStore()
 
     def send(self,task:ProjectTaskDto):
         try:
@@ -36,7 +37,8 @@ class Sender:
         try:
             self._logger.info(f"开始发送定额:{quota.id}")
             self._quota_store.update_send_status(quota.id, 1)
-
+            quota_input = QuotaInputDto.from_quota_dto(quota)
+            self._quota_input_store.create_quota(quota_input)
             self._quota_store.update_send_status(quota.id, 2)
             self._logger.info(f"发送定额:{quota.id}完成")
             return None

+ 1 - 1
SourceCode/IntelligentRailwayCosting/app/main.py

@@ -5,7 +5,7 @@ logger = utils.get_logger()
 def main():
     logger.info("程序启动")
     app = create_app()
-    app.run(host='0.0.0.0', port=5124)  # 指定HTTP端口为5124
+    app.run(host='0.0.0.0', port=5123)  # 指定HTTP端口为5123
 
 if __name__ == '__main__':
     main()

+ 0 - 10
SourceCode/IntelligentRailwayCosting/app/routes/project.py

@@ -39,16 +39,6 @@ def get_budget_info(project_id:str):
     except Exception as e:
         return ResponseBase.error(f'获取项目概算信息失败:{str(e)}')
 
-@project_api.route('/budget-item/top/<int:budget_id>/<project_id>', methods=['POST'])
-@Permission.authorize
-def get_budget_top_items(budget_id:int,project_id:str):
-    try:
-        data,msg = project_srvice.get_top_budget_items(budget_id, project_id)
-        if not data:
-            return ResponseBase.error(msg)
-        return ResponseBase.success(data)
-    except Exception as e:
-        return ResponseBase.error(f'获取项目概算条目失败:{str(e)}')
 
 @project_api.route('/budget-item/<budget_id>/<project_id>', methods=['POST'])
 @Permission.authorize

+ 1 - 0
SourceCode/IntelligentRailwayCosting/app/stores/__init__.py

@@ -5,4 +5,5 @@ from .project_task import ProjectTaskStore
 from .user import UserStore
 from .project import ProjectStore
 from .budget import BudgetStore
+from .quota_input import QuotaInputStore
 

+ 21 - 1
SourceCode/IntelligentRailwayCosting/app/stores/project_quota.py

@@ -157,7 +157,7 @@ class ProjectQuotaStore:
                 send_status=quota_dto.send_status,
                 send_time=quota_dto.send_time,
                 send_error=quota_dto.send_error,
-                created_by=self.current_user.username,
+                created_by=quota_dto.created_by or self.current_user.username,
                 created_at=datetime.now(),
             )
 
@@ -257,3 +257,23 @@ class ProjectQuotaStore:
             quota = db_session.merge(quota)
             return True
 
+    def update_quota_code(self, quota_dto: ProjectQuotaDto):
+        """更新定额编号
+        Args:
+            quota_dto: 定额DTO
+        Returns:
+            bool
+        """
+        quota = self.get_quota(quota_dto.id)
+        if not quota:
+            return False
+        with db_helper.mysql_session(self._database) as db_session:
+            quota.quota_code = quota_dto.quota_code
+            quota.unit = quota_dto.unit
+            quota.unit_price = quota_dto.unit_price
+            quota.total_price = quota_dto.total_price
+
+            quota.updated_by = self.current_user.username
+            quota.updated_at = datetime.now()
+            db_session.merge(quota)
+            return True

+ 232 - 0
SourceCode/IntelligentRailwayCosting/app/stores/quota_input.py

@@ -0,0 +1,232 @@
+from sqlalchemy import and_, or_
+from datetime import datetime
+from typing import Optional, List, Tuple
+
+import tools.db_helper as db_helper
+from core.dtos import QuotaInputDto
+from core.models import QuotaInputModel
+from core.user_session import UserSession
+
+class QuotaInputStore:
+    def __init__(self):
+        self._database = None
+        self._current_user = None
+
+    @property
+    def current_user(self):
+        if self._current_user is None:
+            self._current_user = UserSession.get_current_user()
+        return self._current_user
+
+    def get_quotas_paginated(
+        self,
+        budget_id: int,
+        item_id: int,
+        page: int = 1,
+        page_size: int = 10,
+        keyword: Optional[str] = None,
+    ) :
+        """分页查询定额输入列表
+
+        Args:
+            page: 页码,从1开始
+            page_size: 每页数量
+            budget_id: 总概算序号
+            item_id: 条目序号
+            keyword: 关键字
+
+        Returns:
+        """
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            query = db_session.query(QuotaInputModel)
+
+            # 构建查询条件
+            conditions = [
+                QuotaInputModel.budget_id == budget_id,
+                QuotaInputModel.item_id == item_id
+            ]
+
+            if keyword:
+                conditions.append(
+                    or_(
+                        QuotaInputModel.quota_code.like(f'%{keyword}%'),
+                        QuotaInputModel.project_name.like(f'%{keyword}%')
+                    )
+                )
+
+            query = query.filter(and_(*conditions))
+
+            # 获取总数
+            total_count = query.count()
+
+            # 分页
+            query = query.offset((page - 1) * page_size).limit(page_size)
+
+            # 转换为DTO
+            quotas = [QuotaInputDto.from_model(model) for model in query.all()]
+
+            return {
+                'total': total_count,
+                'data': quotas
+            }
+
+    def get_quota_by_id(self, quota_id: int) -> Optional[QuotaInputDto]:
+        """根据ID获取定额输入
+
+        Args:
+            quota_id: 定额序号
+
+        Returns:
+            QuotaInputDto or None
+        """
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            model = db_session.query(QuotaInputModel).filter(
+                QuotaInputModel.quota_id == quota_id
+            ).first()
+
+            if model is None:
+                return None
+
+            return QuotaInputDto.from_model(model)
+
+    def create_quota(self, dto: QuotaInputDto) -> QuotaInputDto:
+        """创建定额输入
+
+        Args:
+            dto: 定额输入DTO
+
+        Returns:
+            QuotaInputDto
+        """
+        with db_helper.mysql_transaction_session(self._database) as db_session:
+            model = QuotaInputModel(
+                budget_id=dto.budget_id,
+                item_id=dto.item_id,
+                quota_code=dto.quota_code,
+                # sequence_number=dto.sequence_number,
+                project_name=dto.project_name,
+                unit=dto.unit,
+                project_quantity=dto.project_quantity,
+                # project_quantity_input=dto.project_quantity_input,
+                # quota_adjustment=dto.quota_adjustment,
+                unit_price=dto.unit_price,
+                # compilation_unit_price=dto.compilation_unit_price,
+                total_price=dto.total_price,
+                # compilation_total_price=dto.compilation_total_price,
+                # unit_weight=dto.unit_weight,
+                # total_weight=dto.total_weight,
+                # labor_cost=dto.labor_cost,
+                # compilation_labor_cost=dto.compilation_labor_cost,
+                # material_cost=dto.material_cost,
+                # compilation_material_cost=dto.compilation_material_cost,
+                # deduct_material_cost=dto.deduct_material_cost,
+                # compilation_deduct_material_cost=dto.compilation_deduct_material_cost,
+                # mechanical_cost=dto.mechanical_cost,
+                # compilation_mechanical_cost=dto.compilation_mechanical_cost,
+                # equipment_cost=dto.equipment_cost,
+                # compilation_equipment_cost=dto.compilation_equipment_cost,
+                # transport_cost=dto.transport_cost,
+                # compilation_transport_cost=dto.compilation_transport_cost,
+                # quota_workday=dto.quota_workday,
+                # total_workday=dto.total_workday,
+                # workday_salary=dto.workday_salary,
+                # compilation_workday_salary=dto.compilation_workday_salary,
+                # quota_mechanical_workday=dto.quota_mechanical_workday,
+                # total_mechanical_workday=dto.total_mechanical_workday,
+                # mechanical_workday_salary=dto.mechanical_workday_salary,
+                # compilation_mechanical_workday_salary=dto.compilation_mechanical_workday_salary,
+                compiler=dto.compiler,
+                modify_date=datetime.now(),
+                # quota_consumption=dto.quota_consumption,
+                # basic_quota=dto.basic_quota,
+                # quota_comprehensive_unit_price=dto.quota_comprehensive_unit_price,
+                # quota_comprehensive_total_price=dto.quota_comprehensive_total_price
+            )
+
+            db_session.add(model)
+            db_session.flush()
+
+            return QuotaInputDto.from_model(model)
+
+    def update_quota(self, dto: QuotaInputDto) -> Optional[QuotaInputDto]:
+        """更新定额输入
+
+        Args:
+            dto: 定额输入DTO
+
+        Returns:
+            QuotaInputDto or None
+        """
+        with db_helper.mysql_transaction_session(self._database) as db_session:
+            model = db_session.query(QuotaInputModel).filter(
+                QuotaInputModel.quota_id == dto.quota_id
+            ).first()
+
+            if model is None:
+                return None
+
+            model.budget_id = dto.budget_id
+            model.item_id = dto.item_id
+            model.quota_code = dto.quota_code
+            # model.sequence_number = dto.sequence_number
+            model.project_name = dto.project_name
+            model.unit = dto.unit
+            model.project_quantity = dto.project_quantity
+            # model.project_quantity_input = dto.project_quantity_input
+            # model.quota_adjustment = dto.quota_adjustment
+            model.unit_price = dto.unit_price
+            # model.compilation_unit_price = dto.compilation_unit_price
+            model.total_price = dto.total_price
+            # model.compilation_total_price = dto.compilation_total_price
+            # model.unit_weight = dto.unit_weight
+            # model.total_weight = dto.total_weight
+            # model.labor_cost = dto.labor_cost
+            # model.compilation_labor_cost = dto.compilation_labor_cost
+            # model.material_cost = dto.material_cost
+            # model.compilation_material_cost = dto.compilation_material_cost
+            # model.deduct_material_cost = dto.deduct_material_cost
+            # model.compilation_deduct_material_cost = dto.compilation_deduct_material_cost
+            # model.mechanical_cost = dto.mechanical_cost
+            # model.compilation_mechanical_cost = dto.compilation_mechanical_cost
+            # model.equipment_cost = dto.equipment_cost
+            # model.compilation_equipment_cost = dto.compilation_equipment_cost
+            # model.transport_cost = dto.transport_cost
+            # model.compilation_transport_cost = dto.compilation_transport_cost
+            # model.quota_workday = dto.quota_workday
+            # model.total_workday = dto.total_workday
+            # model.workday_salary = dto.workday_salary
+            # model.compilation_workday_salary = dto.compilation_workday_salary
+            # model.quota_mechanical_workday = dto.quota_mechanical_workday
+            # model.total_mechanical_workday = dto.total_mechanical_workday
+            # model.mechanical_workday_salary = dto.mechanical_workday_salary
+            # model.compilation_mechanical_workday_salary = dto.compilation_mechanical_workday_salary
+            # model.compiler = dto.compiler
+            model.modify_date = dto.modify_date
+            # model.quota_consumption = dto.quota_consumption
+            # model.basic_quota = dto.basic_quota
+            # model.quota_comprehensive_unit_price = dto.quota_comprehensive_unit_price
+            # model.quota_comprehensive_total_price = dto.quota_comprehensive_total_price
+
+            db_session.flush()
+
+            return QuotaInputDto.from_model(model)
+
+    def delete_quota(self, quota_id: int) -> bool:
+        """删除定额输入
+
+        Args:
+            quota_id: 定额序号
+
+        Returns:
+            bool
+        """
+        with db_helper.mysql_transaction_session(self._database) as db_session:
+            model = db_session.query(QuotaInputModel).filter(
+                QuotaInputModel.quota_id == quota_id
+            ).first()
+
+            if model is None:
+                return False
+
+            db_session.delete(model)
+            return True

+ 1 - 1
SourceCode/IntelligentRailwayCosting/app/tools/db_helper/sqlserver_helper.py

@@ -35,7 +35,7 @@ class SQLServerHelper(DBHelper):
             }
         }
 
-        self.main_database_name = f"sqlserver_mian_{configs.app.version}" if configs.app.user_version else "sqlserver_mian"
+        self.main_database_name = f"sqlserver_mian_{configs.app.version}" if configs.app.use_version else "sqlserver_mian"
     def _build_connection_string(self, database: str, config: Optional[Dict[str, str]] = None) -> str:
         """构建连接字符串"""
         conn_config = self._default_config.copy()

+ 8 - 5
SourceCode/IntelligentRailwayCosting/app/views/static/project/budget_info.js

@@ -9,7 +9,7 @@ const nav_tab_template = `
 	tab_content_template = `<div class="tab-pane h-100 fade" id="iwb_tab_{0}" role="tabpanel">{1}</div>`,
 	table_template = `<div class="d-flex flex-row h-100 project-box">
 									<div class="flex-row-auto h-100 left-box">
-										<div class="tree-dom h-100 overflow-auto" id="js-tree_{0}"></div>
+										<div class="tree-dom w-300px h-100 overflow-auto" id="js-tree_{0}"></div>
 									</div>
 									<div class="flex-row-fluid right-box">
 										<div class="table-box table-responsive" id="table_box_{0}" style="display: none">
@@ -183,6 +183,9 @@ function GetBudgetItems(id) {
 			themes: {
 				responsive: false,
 			},
+			strings:{
+				'Loading ...': '加载中...',
+			},
 			check_callback: true,
 			data: function (node, callback) {
 				// console.log('TREE_NODE', node)
@@ -362,8 +365,8 @@ function RenderTabCondent(data) {
 							}else if (row.collect_status === 4) {
 								str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新采集" onclick="ReStartCollectTask(${row.id}, ${data.budget_id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
 							}
-							str+=`<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit(${row.id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-							str+=`<button type="button" class="btn btn-icon btn-sm btn-light-danger"  data-bs-toggle="tooltip" data-bs-placement="top" title="删除" onclick="Delete(${row.id})"><i class="ki-duotone ki-trash-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+							str+=`<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit(${row.id}, ${data.budget_id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+							str+=`<button type="button" class="btn btn-icon btn-sm btn-light-danger"  data-bs-toggle="tooltip" data-bs-placement="top" title="删除" onclick="Delete(${row.id}, ${data.budget_id})"><i class="ki-duotone ki-trash-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
 
 							return str
 						}
@@ -459,8 +462,8 @@ function RenderTabCondent(data) {
 						} else if (row.process_status === 4) {
 							str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessQuota(${row.id}, ${data.budget_id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
 						}
-						str+=`<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit_Quota(${row.id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-						str+=`<button type="button" class="btn btn-icon btn-sm btn-light-danger"  data-bs-toggle="tooltip" data-bs-placement="top" title="删除" onclick="Delete_Quota(${row.id})"><i class="ki-duotone ki-trash-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+						str+=`<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit_Quota(${row.id}, ${data.budget_id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+						str+=`<button type="button" class="btn btn-icon btn-sm btn-light-danger"  data-bs-toggle="tooltip" data-bs-placement="top" title="删除" onclick="Delete_Quota(${row.id}, ${data.budget_id})"><i class="ki-duotone ki-trash-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
 						return str
 					}
 				},