Pārlūkot izejas kodu

Merge branch 'dev' of yue/IntelligentRailwayCosting into main

YueYunyun 2 mēneši atpakaļ
vecāks
revīzija
b5ff620ce5
100 mainītis faili ar 25553 papildinājumiem un 0 dzēšanām
  1. 2 0
      .gitignore
  2. BIN
      Doc/05.Excel解析.pdf
  3. 43 0
      Doc/Docker部署/01使用mysql数据库/config_mysql.yml
  4. 41 0
      Doc/Docker部署/01使用mysql数据库/docker-compose_mysql.yml
  5. 86 0
      Doc/Docker部署/01使用mysql数据库/init_mysql.sql
  6. 35 0
      Doc/Docker部署/02使用sqlserver数据库/config_sqlserver.yml
  7. 19 0
      Doc/Docker部署/02使用sqlserver数据库/docker-compose_sqlserver.yml
  8. 134 0
      Doc/Docker部署/02使用sqlserver数据库/init_sqlserver.sql
  9. 6 0
      Doc/Docker部署/部署配置修改.txt
  10. 2 0
      SourceCode/IntelligentRailwayCosting/.dockerignore
  11. 12 0
      SourceCode/IntelligentRailwayCosting/.script/cmd/BuildRcBase_1.0.0.run.xml
  12. 13 0
      SourceCode/IntelligentRailwayCosting/.script/cmd/Mysql_BuildRcApp_1.0.4.run.xml
  13. 12 0
      SourceCode/IntelligentRailwayCosting/.script/cmd/Mysql_RunRcApp.run.xml
  14. 13 0
      SourceCode/IntelligentRailwayCosting/.script/cmd/SqlServer_BuildRcApp_1.0.0.run.xml
  15. 12 0
      SourceCode/IntelligentRailwayCosting/.script/cmd/SqlServer_RunRcApp.run.xml
  16. 26 0
      SourceCode/IntelligentRailwayCosting/.script/cmd/main.run.xml
  17. 58 0
      SourceCode/IntelligentRailwayCosting/.script/db
  18. 88 0
      SourceCode/IntelligentRailwayCosting/.script/init_mysql.sql
  19. 136 0
      SourceCode/IntelligentRailwayCosting/.script/init_sqlserver.sql
  20. 18220 0
      SourceCode/IntelligentRailwayCosting/.script/project_data_test.sql
  21. 5 0
      SourceCode/IntelligentRailwayCosting/Docker/.env
  22. 35 0
      SourceCode/IntelligentRailwayCosting/Docker/Dockerfile
  23. 78 0
      SourceCode/IntelligentRailwayCosting/Docker/DockerfileBase
  24. 59 0
      SourceCode/IntelligentRailwayCosting/Docker/docker-compose.yml
  25. 41 0
      SourceCode/IntelligentRailwayCosting/Docker/docker-compose_mysql.yml
  26. 19 0
      SourceCode/IntelligentRailwayCosting/Docker/docker-compose_sqlserver.yml
  27. 0 0
      SourceCode/IntelligentRailwayCosting/app/__init__.py
  28. 17 0
      SourceCode/IntelligentRailwayCosting/app/ai/__init__.py
  29. 123 0
      SourceCode/IntelligentRailwayCosting/app/ai/fastgpt.py
  30. 203 0
      SourceCode/IntelligentRailwayCosting/app/ai/openai.py
  31. 90 0
      SourceCode/IntelligentRailwayCosting/app/config.yml
  32. 0 0
      SourceCode/IntelligentRailwayCosting/app/core/__init__.py
  33. 2 0
      SourceCode/IntelligentRailwayCosting/app/core/api/__init__.py
  34. 73 0
      SourceCode/IntelligentRailwayCosting/app/core/api/response.py
  35. 38 0
      SourceCode/IntelligentRailwayCosting/app/core/api/table_response.py
  36. 16 0
      SourceCode/IntelligentRailwayCosting/app/core/configs/__init__.py
  37. 35 0
      SourceCode/IntelligentRailwayCosting/app/core/configs/ai_config.py
  38. 49 0
      SourceCode/IntelligentRailwayCosting/app/core/configs/ai_fastgpt_config.py
  39. 58 0
      SourceCode/IntelligentRailwayCosting/app/core/configs/app_config.py
  40. 61 0
      SourceCode/IntelligentRailwayCosting/app/core/configs/config.py
  41. 17 0
      SourceCode/IntelligentRailwayCosting/app/core/configs/database_config.py
  42. 14 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/__init__.py
  43. 144 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/chapter.py
  44. 226 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/excel_parse.py
  45. 45 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/log.py
  46. 87 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/project.py
  47. 114 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/project_quota.py
  48. 75 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/project_task.py
  49. 153 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/quota_input.py
  50. 72 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/total_budget_info.py
  51. 130 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/total_budget_item.py
  52. 26 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/tree.py
  53. 33 0
      SourceCode/IntelligentRailwayCosting/app/core/dtos/user.py
  54. 4 0
      SourceCode/IntelligentRailwayCosting/app/core/enum/__init__.py
  55. 36 0
      SourceCode/IntelligentRailwayCosting/app/core/enum/log.py
  56. 55 0
      SourceCode/IntelligentRailwayCosting/app/core/enum/task.py
  57. 2 0
      SourceCode/IntelligentRailwayCosting/app/core/log/__init__.py
  58. 93 0
      SourceCode/IntelligentRailwayCosting/app/core/log/log_record.py
  59. 11 0
      SourceCode/IntelligentRailwayCosting/app/core/models/__init__.py
  60. 46 0
      SourceCode/IntelligentRailwayCosting/app/core/models/chapter.py
  61. 27 0
      SourceCode/IntelligentRailwayCosting/app/core/models/log.py
  62. 43 0
      SourceCode/IntelligentRailwayCosting/app/core/models/project.py
  63. 62 0
      SourceCode/IntelligentRailwayCosting/app/core/models/project_quota.py
  64. 34 0
      SourceCode/IntelligentRailwayCosting/app/core/models/project_task.py
  65. 60 0
      SourceCode/IntelligentRailwayCosting/app/core/models/quota_input.py
  66. 16 0
      SourceCode/IntelligentRailwayCosting/app/core/models/team.py
  67. 40 0
      SourceCode/IntelligentRailwayCosting/app/core/models/total_budget_info.py
  68. 48 0
      SourceCode/IntelligentRailwayCosting/app/core/models/total_budget_item.py
  69. 19 0
      SourceCode/IntelligentRailwayCosting/app/core/models/user.py
  70. 4 0
      SourceCode/IntelligentRailwayCosting/app/core/user_session/__init__.py
  71. 66 0
      SourceCode/IntelligentRailwayCosting/app/core/user_session/current_user.py
  72. 26 0
      SourceCode/IntelligentRailwayCosting/app/core/user_session/permission.py
  73. 90 0
      SourceCode/IntelligentRailwayCosting/app/core/user_session/user_session.py
  74. 37 0
      SourceCode/IntelligentRailwayCosting/app/executor/__init__.py
  75. 301 0
      SourceCode/IntelligentRailwayCosting/app/executor/task_processor.py
  76. 290 0
      SourceCode/IntelligentRailwayCosting/app/executor/task_runner.py
  77. 76 0
      SourceCode/IntelligentRailwayCosting/app/executor/task_sender.py
  78. 48 0
      SourceCode/IntelligentRailwayCosting/app/flask_app/__init__.py
  79. 20 0
      SourceCode/IntelligentRailwayCosting/app/main.py
  80. 19 0
      SourceCode/IntelligentRailwayCosting/app/routes/__init__.py
  81. 77 0
      SourceCode/IntelligentRailwayCosting/app/routes/auth.py
  82. 23 0
      SourceCode/IntelligentRailwayCosting/app/routes/error.py
  83. 159 0
      SourceCode/IntelligentRailwayCosting/app/routes/excel_test.py
  84. 42 0
      SourceCode/IntelligentRailwayCosting/app/routes/log.py
  85. 98 0
      SourceCode/IntelligentRailwayCosting/app/routes/project.py
  86. 156 0
      SourceCode/IntelligentRailwayCosting/app/routes/project_quota.py
  87. 184 0
      SourceCode/IntelligentRailwayCosting/app/routes/project_task.py
  88. 5 0
      SourceCode/IntelligentRailwayCosting/app/services/__init__.py
  89. 72 0
      SourceCode/IntelligentRailwayCosting/app/services/log.py
  90. 211 0
      SourceCode/IntelligentRailwayCosting/app/services/project.py
  91. 450 0
      SourceCode/IntelligentRailwayCosting/app/services/project_quota.py
  92. 431 0
      SourceCode/IntelligentRailwayCosting/app/services/project_task.py
  93. 33 0
      SourceCode/IntelligentRailwayCosting/app/services/user.py
  94. 13 0
      SourceCode/IntelligentRailwayCosting/app/stores/__init__.py
  95. 195 0
      SourceCode/IntelligentRailwayCosting/app/stores/budget.py
  96. 245 0
      SourceCode/IntelligentRailwayCosting/app/stores/chapter.py
  97. 133 0
      SourceCode/IntelligentRailwayCosting/app/stores/project.py
  98. 238 0
      SourceCode/IntelligentRailwayCosting/app/stores/quota_input.py
  99. 3 0
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/__init__.py
  100. 116 0
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/log.py

+ 2 - 0
.gitignore

@@ -114,3 +114,5 @@ crashlytics-build.properties
 doc/data/
 logs/
 temp_files/
+/SourceCode/IntelligentRailwayCosting/SQL
+Doc/DB

BIN
Doc/05.Excel解析.pdf


+ 43 - 0
Doc/Docker部署/01使用mysql数据库/config_mysql.yml

@@ -0,0 +1,43 @@
+app:
+  # 应用名称
+  name: '铁路造价智能化工具'
+  version: '2020' # 应用版本 2020|2024
+  use_version: true
+  source_path: './temp_files'
+  task_api_url: 'http://{host}:{port}/api/v1'
+  task_max_projects:20 # 最大项目数同时运行
+  task_interval: 60  # 秒
+db:
+  sqlserver_mian_2024:
+    driver: '{FreeTDS}'
+    server: 
+    username: 
+    password: 
+    database: RecoData2024
+    trusted_connection: false
+  sqlserver_mian_2020:
+    driver: '{FreeTDS}'
+    server: 
+    username: 
+    password: 
+    database: RecoData2020
+    trusted_connection: false
+    trusted_connection: false
+  # MySQL 配置
+  mysql_main:
+    db: iwb_railway_costing_v1
+    host: railway_costing-mysql
+    port: 3306
+    user: root
+    password: 12345654321
+    charset: utf8mb4
+  iwb_railway_costing_v1:
+    db: iwb_railway_costing_v1
+    host: railway_costing-mysql
+    port: 3306
+    user: root
+    password: 12345654321
+    charset: utf8mb4
+logger:
+  file_path: './logs/'
+  level: 'info'

+ 41 - 0
Doc/Docker部署/01使用mysql数据库/docker-compose_mysql.yml

@@ -0,0 +1,41 @@
+version: '3.8'
+
+services:
+  rc-mysql:
+    image: mysql:8.0.39
+    container_name: railway_costing-mysql
+    environment:
+      - MYSQL_ROOT_PASSWORD=12345654321
+      - TZ=Asia/Shanghai
+    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_mysql.sql:/docker-entrypoint-initdb.d/init.sql # 挂载 init.sql 文件
+    networks:
+      - railway_costing_mysql_v1
+    restart: always
+
+  rc-app:
+    build:
+      context: ../
+      dockerfile: .
+    image: railway_costing-app_mysql:1.0.0
+    container_name: railway_costing-app_mysql
+    depends_on:
+      - rc-mysql
+    environment:
+      - TZ=Asia/Shanghai
+    volumes:
+      - /home/docker/iwb_railway_costing_v1/app/config_mysql.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_mysql_v1
+    ports:
+      - "7011:5123"
+    restart: always
+
+networks:
+  railway_costing_mysql_v1:
+    driver: bridge

+ 86 - 0
Doc/Docker部署/01使用mysql数据库/init_mysql.sql

@@ -0,0 +1,86 @@
+CREATE DATABASE IF NOT EXISTS iwb_railway_costing_v1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
+USE iwb_railway_costing_v1;
+
+-- 创建项目任务表
+CREATE TABLE IF NOT EXISTS project_task (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    task_name VARCHAR(255) NOT NULL COMMENT '任务名称',
+    task_sort INT DEFAULT 0 COMMENT '任务排序',
+    task_desc VARCHAR(1000) COMMENT '任务描述',
+    is_cover TINYINT DEFAULT 0 COMMENT '是否覆盖',
+    project_id VARCHAR(50) NOT NULL COMMENT '项目编号',
+    budget_id INT NOT NULL DEFAULT 0 COMMENT '概算序号',
+    item_id INT NOT NULL COMMENT '条目序号',
+    item_code VARCHAR(255) NOT NULL COMMENT '条目编号',
+    file_path TEXT COMMENT '文件路径',
+    process_status INT NOT NULL DEFAULT 0 COMMENT '处理状态(0:草稿, 1:待运行, 2:运行中, 200:运行成功, 4:运行失败 5:取消运行)',
+    process_time DATETIME COMMENT '处理时间',
+    process_error VARCHAR(4000) COMMENT '处理错误信息',
+    send_status INT NOT NULL DEFAULT 0 COMMENT '推送状态(0:未推送, 1:推送中 ,2:推送成功, 3:推送失败, 4:已修改)',
+    send_time DATETIME COMMENT '推送时间',
+    send_error VARCHAR(4000) COMMENT '推送错误信息',
+    is_del TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除(0:否, 1:是)',
+    deleted_by VARCHAR(50) COMMENT '删除人',
+    deleted_at DATETIME COMMENT '删除时间',
+    created_by VARCHAR(50) COMMENT '创建人',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_by VARCHAR(50) COMMENT '更新人',
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    INDEX idx_project_id (project_id),
+    INDEX idx_budget_id (budget_id),
+    INDEX idx_item_id (item_id),
+    INDEX idx_item_code (item_code),
+    INDEX idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目任务表';
+
+-- 创建项目定额表
+CREATE TABLE IF NOT EXISTS project_quota (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    task_id INT NOT NULL COMMENT '任务编号',
+    project_id VARCHAR(50) NOT NULL COMMENT '项目编号',
+    budget_id INT NOT NULL COMMENT '概算序号',
+    budget_code VARCHAR(50) COMMENT '概算编号',
+    item_id INT NOT NULL COMMENT '条目序号',
+    item_code VARCHAR(255) NOT NULL COMMENT '条目编号',
+    quota_id INT NULL DEFAULT 0 COMMENT '定额序号',
+    quota_code VARCHAR(50) COMMENT '定额编号',
+    entry_name VARCHAR(255) COMMENT '工程或费用项目名称',
+    units VARCHAR(20) COMMENT '单位',
+    amount FLOAT COMMENT '数量',
+    ex_file VARCHAR(4000) COMMENT 'excel文件',
+    ex_cell VARCHAR(50) COMMENT '数量单元格位置',
+    ex_row VARCHAR(4000) COMMENT '该行内容',
+    ex_unit VARCHAR(50) COMMENT 'excel中给出的单位',
+    ex_amount FLOAT COMMENT 'excel中给出的数量',
+    send_status INT NOT NULL DEFAULT 0 COMMENT '推送状态(0:未推送,1:推送中 ,2:已推送, 3:推送失败, 4:已修改)',
+    send_time DATETIME COMMENT '推送时间',
+    send_error VARCHAR(4000) COMMENT '推送错误信息',
+    is_del TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除(0:否, 1:是)',
+    deleted_by VARCHAR(50) COMMENT '删除人',
+    deleted_at DATETIME COMMENT '删除时间',
+    created_by VARCHAR(50) COMMENT '创建人',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_by VARCHAR(50) COMMENT '更新人',
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    INDEX idx_project_id (project_id),
+    INDEX idx_budget_id (budget_id),
+    INDEX idx_item_id (item_id),
+    INDEX idx_item_code (item_code),
+    INDEX idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目定额表';
+
+-- 创建日志表
+CREATE TABLE IF NOT EXISTS sys_log (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    username VARCHAR(255) NOT NULL COMMENT '用户名',
+    operation_type VARCHAR(50) NOT NULL COMMENT '操作类型',
+    operation_desc VARCHAR(1000) COMMENT '操作描述',
+    operation_result TINYINT COMMENT '操作结果',
+    operation_module VARCHAR(100) COMMENT '操作模块',
+    operation_data TEXT COMMENT '操作数据',
+    data_changes TEXT COMMENT '数据变更记录',
+    operation_ip VARCHAR(50) COMMENT '操作IP',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    INDEX idx_username (username),
+    INDEX idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';

+ 35 - 0
Doc/Docker部署/02使用sqlserver数据库/config_sqlserver.yml

@@ -0,0 +1,35 @@
+app:
+  # 应用名称
+  name: '铁路造价智能化工具'
+  version: '2020' # 应用版本 2020|2024
+  use_version: true
+  source_path: './temp_files'
+  task_api_url: 'http://{host}:{port}/api/v1'
+  task_max_projects:20 # 最大项目数同时运行
+  task_interval: 60  # 秒
+db:
+  sqlserver_mian_2024:
+    driver: '{FreeTDS}'
+    server: 
+    username: 
+    password: 
+    database: RecoData2024
+    trusted_connection: false
+  sqlserver_mian_2020:
+    driver: '{FreeTDS}'
+    server: 
+    username: 
+    password: 
+    database: RecoData2020
+    trusted_connection: false
+  Iwb_RailwayCosting:
+    driver: '{FreeTDS}'
+    server: 
+    username: 
+    password: 
+    database: iwb_railway_costing_v1
+    trusted_connection: false
+logger:
+  file_path: './logs/'
+  level: 'info'
+

+ 19 - 0
Doc/Docker部署/02使用sqlserver数据库/docker-compose_sqlserver.yml

@@ -0,0 +1,19 @@
+version: '3.8'
+
+services:
+  rc-app:
+    build:
+      context: ../
+      dockerfile: .
+    image: railway_costing-app_sqlserver:1.0.0
+    container_name: railway_costing-app_sqlserver
+    environment:
+      - TZ=Asia/Shanghai
+      - ACCEPT_EULA=Y
+    volumes:
+      - /home/docker/iwb_railway_costing_v1/app/config_sqlserver.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
+    ports:
+      - "7010:5123"
+    restart: always

+ 134 - 0
Doc/Docker部署/02使用sqlserver数据库/init_sqlserver.sql

@@ -0,0 +1,134 @@
+IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'iwb_railway_costing_v1')
+BEGIN
+    CREATE DATABASE iwb_railway_costing_v1
+END
+GO
+
+USE iwb_railway_costing_v1
+GO
+
+USE iwb_railway_costing_v1
+GO
+
+
+
+/*-- 删除项目任务表
+IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[project_task]') AND type in (N'U'))
+BEGIN
+    DROP TABLE [dbo].[project_task]
+END
+GO*/
+
+-- 创建项目任务表
+IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[project_task]') AND type in (N'U'))
+BEGIN
+CREATE TABLE [dbo].[project_task] (
+    [id] INT IDENTITY(1,1) PRIMARY KEY,
+    [task_name] NVARCHAR(255) NOT NULL,
+    [task_sort] INT DEFAULT 0,
+    [task_desc] NVARCHAR(1000) NULL,
+    [is_cover]  INT DEFAULT 0,
+    [project_id] NVARCHAR(50) NOT NULL,
+    [budget_id] INT DEFAULT 0,
+    [item_id] INT NOT NULL,
+    [item_code] NVARCHAR(255) NOT NULL,
+    [file_path] NVARCHAR(MAX) NULL,
+    [process_status] INT NOT NULL DEFAULT 0,
+    [process_time] DATETIME NULL,
+    [process_error] NVARCHAR(4000) NULL,
+    [send_status] INT NOT NULL DEFAULT 0,
+    [send_time] DATETIME NULL,
+    [send_error] NVARCHAR(4000) NULL,
+    [is_del] INT NOT NULL DEFAULT 0,
+    [deleted_by] NVARCHAR(50) NULL,
+    [deleted_at] DATETIME NULL,
+    [created_by] NVARCHAR(50) NULL,
+    [created_at] DATETIME NOT NULL DEFAULT GETDATE(),
+    [updated_by] NVARCHAR(50) NULL,
+    [updated_at] DATETIME NOT NULL DEFAULT GETDATE()
+)
+
+CREATE INDEX [idx_project_id] ON [dbo].[project_task] ([project_id])
+CREATE INDEX [idx_budget_id] ON [dbo].[project_task] ([budget_id])
+CREATE INDEX [idx_item_id] ON [dbo].[project_task] ([item_id])
+CREATE INDEX [idx_item_code] ON [dbo].[project_task] ([item_code])
+CREATE INDEX [idx_created_at] ON [dbo].[project_task] ([created_at])
+END
+GO
+
+/* -- 删除项目定额表
+   IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[project_quota]') AND type in (N'U'))
+   BEGIN
+       DROP TABLE [dbo].[project_quota]
+   END
+   GO
+*/
+-- 创建项目定额表
+IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[project_quota]') AND type in (N'U'))
+BEGIN
+CREATE TABLE [dbo].[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,
+    [budget_code] NVARCHAR(50) NULL,
+    [item_id] INT NOT NULL,
+    [item_code] NVARCHAR(255) NOT NULL,
+    [quota_id] INT NULL DEFAULT 0,
+    [quota_code] NVARCHAR(50) NULL,
+    [entry_name] NVARCHAR(255) NULL,
+    [units] NVARCHAR(20) NULL,
+    [amount] FLOAT NULL,
+    [ex_file] NVARCHAR(4000) NULL,
+    [ex_cell] NVARCHAR(50) NULL,
+    [ex_row] NVARCHAR(4000) NULL,
+    [ex_unit] NVARCHAR(50) NULL,
+    [ex_amount] FLOAT NULL,
+    [send_status] INT NOT NULL DEFAULT 0,
+    [send_time] DATETIME NULL,
+    [send_error] NVARCHAR(4000) NULL,
+    [is_del] INT NOT NULL DEFAULT 0,
+    [deleted_by] NVARCHAR(50) NULL,
+    [deleted_at] DATETIME NULL,
+    [created_by] NVARCHAR(50) NULL,
+    [created_at] DATETIME NOT NULL DEFAULT GETDATE(),
+    [updated_by] NVARCHAR(50) NULL,
+    [updated_at] DATETIME NOT NULL DEFAULT GETDATE()
+)
+
+CREATE INDEX [idx_project_id] ON [dbo].[project_quota] ([project_id])
+CREATE INDEX [idx_budget_id] ON [dbo].[project_quota] ([budget_id])
+CREATE INDEX [idx_item_id] ON [dbo].[project_quota] ([item_id])
+CREATE INDEX [idx_item_code] ON [dbo].[project_quota] ([item_code])
+CREATE INDEX [idx_created_at] ON [dbo].[project_quota] ([created_at])
+END
+GO
+
+
+/*-- 删除日志表
+IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sys_log]') AND type in (N'U'))
+BEGIN
+    DROP TABLE [dbo].[sys_log]
+END
+GO*/
+
+-- 创建日志表
+IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sys_log]') AND type in (N'U'))
+BEGIN
+CREATE TABLE [dbo].[sys_log] (
+    [id] INT IDENTITY(1,1) PRIMARY KEY,
+    [username] NVARCHAR(255) NOT NULL,
+    [operation_type] NVARCHAR(50) NOT NULL,
+    [operation_desc] NVARCHAR(1000) NULL,
+    [operation_result] INT NULL,
+    [operation_module] NVARCHAR(100) NULL,
+    [operation_data] NVARCHAR(MAX) NULL,
+    [data_changes] NVARCHAR(MAX) NULL,
+    [operation_ip] NVARCHAR(50) NULL,
+    [created_at] DATETIME NOT NULL DEFAULT GETDATE()
+)
+
+CREATE INDEX [idx_username] ON [dbo].[sys_log] ([username])
+CREATE INDEX [idx_created_at] ON [dbo].[sys_log] ([created_at])
+END
+GO

+ 6 - 0
Doc/Docker部署/部署配置修改.txt

@@ -0,0 +1,6 @@
+01  需要修改docker-compose里的挂载目录
+02  需要修改config里的   app.task_api_url  配置 
+03  需要修改config里的  sqlserver 相关的 server,username,password 。
+04  在本地连接sqlserver数据库,可以使用 trusted_connection: true , server: .\{SqlServer实例名称} ,用户名密码留空进行连接(这个只在windows环境{SQL Server}的驱动配置下测试过,docker环境{FreeTDS}驱动未测试)。
+05  如果使用mysql版本进行部署,config里mysql的相关配置可以无需修改。如果修改需和init_mysql.sql脚本里的数据库名及docker-compose的相关配置一起修改。
+

+ 2 - 0
SourceCode/IntelligentRailwayCosting/.dockerignore

@@ -0,0 +1,2 @@
+app/logs/
+app/temp_files/

+ 12 - 0
SourceCode/IntelligentRailwayCosting/.script/cmd/BuildRcBase_1.0.0.run.xml

@@ -0,0 +1,12 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="BuildRcBase_1.0.0" type="docker-deploy" factoryName="dockerfile" server-name="104">
+    <deployment type="dockerfile">
+      <settings>
+        <option name="imageTag" value="railway_costing-base:1.0.0" />
+        <option name="buildOnly" value="true" />
+        <option name="sourceFilePath" value="Docker/DockerfileBase" />
+      </settings>
+    </deployment>
+    <method v="2" />
+  </configuration>
+</component>

+ 13 - 0
SourceCode/IntelligentRailwayCosting/.script/cmd/Mysql_BuildRcApp_1.0.4.run.xml

@@ -0,0 +1,13 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="Mysql_BuildRcApp_1.0.4" type="docker-deploy" factoryName="dockerfile" server-name="104">
+    <deployment type="dockerfile">
+      <settings>
+        <option name="imageTag" value="railway_costing-app_mysql:1.0.4" />
+        <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/IntelligentRailwayCosting/.script/cmd/Mysql_RunRcApp.run.xml

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

+ 13 - 0
SourceCode/IntelligentRailwayCosting/.script/cmd/SqlServer_BuildRcApp_1.0.0.run.xml

@@ -0,0 +1,13 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="SqlServer_BuildRcApp_1.0.0" type="docker-deploy" factoryName="dockerfile" server-name="104">
+    <deployment type="dockerfile">
+      <settings>
+        <option name="imageTag" value="railway_costing-app_sqlserver: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/IntelligentRailwayCosting/.script/cmd/SqlServer_RunRcApp.run.xml

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

+ 26 - 0
SourceCode/IntelligentRailwayCosting/.script/cmd/main.run.xml

@@ -0,0 +1,26 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="main" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
+    <module name="IntelligentRailwayCosting" />
+    <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="SDK_NAME" value="Python 3.13 virtualenv at D:\01Work\1012_IntelligentRailwayCosting\SourceCode\IntelligentRailwayCosting\.venv" />
+    <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/app" />
+    <option name="IS_MODULE_SDK" value="false" />
+    <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$/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>

+ 58 - 0
SourceCode/IntelligentRailwayCosting/.script/db

@@ -0,0 +1,58 @@
+Reco20250212142905300
+Reco20250212143901733
+Reco20250212145354673
+Reco20250212145532610
+Reco20250212151254227
+Reco20250212152142010
+Reco20250212154425690
+Reco20250212155525693
+Reco20250213082445730
+Reco20250213083156220
+Reco20250213083551790
+Reco20250213085520590
+Reco20250213090734543
+Reco20250213102147467
+Reco20250213110849020
+Reco20250213134321337
+Reco20250213154013750
+Reco20250217090314980
+Reco20250217102247743
+Reco20250218095015837
+Reco20250219104943600
+Reco20250219145847603
+Reco20250219151958463
+Reco20250219161443453
+Reco20250219174446497
+Reco20250220153154013
+Reco20250221092016257
+Reco20250221110720233
+Reco20250224103234837
+Reco20250224141641383
+Reco20250224143808677
+Reco20250226102733400
+Reco20250226105618733
+Reco20250226111533787
+Reco20250227104157053
+Reco20250227164416067
+Reco20250228112039957
+Reco20250228151800300
+Reco20250228154316267
+Reco20250228161303020
+Reco20250303083825253
+Reco20250303145336067
+Reco20250304141116160
+Reco20250304144851617
+Reco20250305084949623
+Reco20250305091830840
+Reco20250305095812047
+Reco20250305133449937
+Reco20250305162539237
+Reco20250305163406660
+Reco20250307091333003
+Reco20250307091420493
+Reco20250307194045983
+Reco20250310095826733
+Reco20250310132922860
+Reco20250311183629840
+Reco20250312084954183
+Reco20250313141023987

+ 88 - 0
SourceCode/IntelligentRailwayCosting/.script/init_mysql.sql

@@ -0,0 +1,88 @@
+CREATE DATABASE IF NOT EXISTS iwb_railway_costing_v1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
+USE iwb_railway_costing_v1;
+
+-- 创建项目任务表
+CREATE TABLE IF NOT EXISTS project_task (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    task_name VARCHAR(255) NOT NULL COMMENT '任务名称',
+    task_sort INT DEFAULT 0 COMMENT '任务排序',
+    task_desc VARCHAR(1000) COMMENT '任务描述',
+    is_cover TINYINT DEFAULT 0 COMMENT '是否覆盖',
+    project_id VARCHAR(50) NOT NULL COMMENT '项目编号',
+    budget_id INT NOT NULL DEFAULT 0 COMMENT '概算序号',
+    item_id INT NOT NULL COMMENT '条目序号',
+    item_code VARCHAR(255) NOT NULL COMMENT '条目编号',
+    file_path TEXT COMMENT '文件路径',
+    process_status INT NOT NULL DEFAULT 0 COMMENT '处理状态(0:草稿, 1:待运行, 2:运行中, 200:运行成功, 4:运行失败 5:取消运行)',
+    process_time DATETIME COMMENT '处理时间',
+    process_error VARCHAR(4000) COMMENT '处理错误信息',
+    send_status INT NOT NULL DEFAULT 0 COMMENT '推送状态(0:未推送, 1:推送中 ,2:推送成功, 3:推送失败, 4:已修改)',
+    send_time DATETIME COMMENT '推送时间',
+    send_error VARCHAR(4000) COMMENT '推送错误信息',
+    is_del TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除(0:否, 1:是)',
+    deleted_by VARCHAR(50) COMMENT '删除人',
+    deleted_at DATETIME COMMENT '删除时间',
+    created_by VARCHAR(50) COMMENT '创建人',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_by VARCHAR(50) COMMENT '更新人',
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    INDEX idx_project_id (project_id),
+    INDEX idx_budget_id (budget_id),
+    INDEX idx_item_id (item_id),
+    INDEX idx_item_code (item_code),
+    INDEX idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目任务表';
+
+-- 创建项目定额表
+CREATE TABLE IF NOT EXISTS project_quota (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    task_id INT NOT NULL COMMENT '任务编号',
+    project_id VARCHAR(50) NOT NULL COMMENT '项目编号',
+    budget_id INT NOT NULL COMMENT '概算序号',
+    budget_code VARCHAR(50) COMMENT '概算编号',
+    item_id INT NOT NULL COMMENT '条目序号',
+    item_code VARCHAR(255) NOT NULL COMMENT '条目编号',
+    quota_id INT NULL DEFAULT 0 COMMENT '定额序号',
+    quota_code VARCHAR(50) COMMENT '定额编号',
+    quota_adjustment TEXT COMMENT '定额调整',
+    entry_name VARCHAR(255) COMMENT '工程或费用项目名称',
+    units VARCHAR(20) COMMENT '单位',
+    amount FLOAT COMMENT '数量',
+    ex_file VARCHAR(4000) COMMENT 'excel文件',
+    ex_cell VARCHAR(50) COMMENT '数量单元格位置',
+    ex_row VARCHAR(4000) COMMENT '该行内容',
+    ex_unit VARCHAR(50) COMMENT 'excel中给出的单位',
+    ex_amount FLOAT COMMENT 'excel中给出的数量',
+    is_change INT NOT NULL DEFAULT 1 COMMENT '是否变更(0:未变更,1:已变更)',
+    send_status INT NOT NULL DEFAULT 0 COMMENT '推送状态(0:未推送,1:推送中 ,2:已推送, 3:推送失败, 4:已修改)',
+    send_time DATETIME COMMENT '推送时间',
+    send_error VARCHAR(4000) COMMENT '推送错误信息',
+    is_del TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除(0:否, 1:是)',
+    deleted_by VARCHAR(50) COMMENT '删除人',
+    deleted_at DATETIME COMMENT '删除时间',
+    created_by VARCHAR(50) COMMENT '创建人',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_by VARCHAR(50) COMMENT '更新人',
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    INDEX idx_project_id (project_id),
+    INDEX idx_budget_id (budget_id),
+    INDEX idx_item_id (item_id),
+    INDEX idx_item_code (item_code),
+    INDEX idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='项目定额表';
+
+-- 创建日志表
+CREATE TABLE IF NOT EXISTS sys_log (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    username VARCHAR(255) NOT NULL COMMENT '用户名',
+    operation_type VARCHAR(50) NOT NULL COMMENT '操作类型',
+    operation_desc VARCHAR(1000) COMMENT '操作描述',
+    operation_result TINYINT COMMENT '操作结果',
+    operation_module VARCHAR(100) COMMENT '操作模块',
+    operation_data TEXT COMMENT '操作数据',
+    data_changes TEXT COMMENT '数据变更记录',
+    operation_ip VARCHAR(50) COMMENT '操作IP',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    INDEX idx_username (username),
+    INDEX idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';

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

@@ -0,0 +1,136 @@
+IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'iwb_railway_costing_v1')
+BEGIN
+    CREATE DATABASE iwb_railway_costing_v1
+END
+GO
+
+USE iwb_railway_costing_v1
+GO
+
+USE iwb_railway_costing_v1
+GO
+
+
+
+/*-- 删除项目任务表
+IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[project_task]') AND type in (N'U'))
+BEGIN
+    DROP TABLE [dbo].[project_task]
+END
+GO*/
+
+-- 创建项目任务表
+IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[project_task]') AND type in (N'U'))
+BEGIN
+CREATE TABLE [dbo].[project_task] (
+    [id] INT IDENTITY(1,1) PRIMARY KEY,
+    [task_name] NVARCHAR(255) NOT NULL,
+    [task_sort] INT DEFAULT 0,
+    [task_desc] NVARCHAR(1000) NULL,
+    [is_cover]  INT DEFAULT 0,
+    [project_id] NVARCHAR(50) NOT NULL,
+    [budget_id] INT DEFAULT 0,
+    [item_id] INT NOT NULL,
+    [item_code] NVARCHAR(255) NOT NULL,
+    [file_path] NVARCHAR(MAX) NULL,
+    [process_status] INT NOT NULL DEFAULT 0,
+    [process_time] DATETIME NULL,
+    [process_error] NVARCHAR(4000) NULL,
+    [send_status] INT NOT NULL DEFAULT 0,
+    [send_time] DATETIME NULL,
+    [send_error] NVARCHAR(4000) NULL,
+    [is_del] INT NOT NULL DEFAULT 0,
+    [deleted_by] NVARCHAR(50) NULL,
+    [deleted_at] DATETIME NULL,
+    [created_by] NVARCHAR(50) NULL,
+    [created_at] DATETIME NOT NULL DEFAULT GETDATE(),
+    [updated_by] NVARCHAR(50) NULL,
+    [updated_at] DATETIME NOT NULL DEFAULT GETDATE()
+)
+
+CREATE INDEX [idx_project_id] ON [dbo].[project_task] ([project_id])
+CREATE INDEX [idx_budget_id] ON [dbo].[project_task] ([budget_id])
+CREATE INDEX [idx_item_id] ON [dbo].[project_task] ([item_id])
+CREATE INDEX [idx_item_code] ON [dbo].[project_task] ([item_code])
+CREATE INDEX [idx_created_at] ON [dbo].[project_task] ([created_at])
+END
+GO
+
+/* -- 删除项目定额表
+   IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[project_quota]') AND type in (N'U'))
+   BEGIN
+       DROP TABLE [dbo].[project_quota]
+   END
+   GO
+*/
+-- 创建项目定额表
+IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[project_quota]') AND type in (N'U'))
+BEGIN
+CREATE TABLE [dbo].[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,
+    [budget_code] NVARCHAR(50) NULL,
+    [item_id] INT NOT NULL,
+    [item_code] NVARCHAR(255) NOT NULL,
+    [quota_id] INT NULL DEFAULT 0,
+    [quota_code] NVARCHAR(50) NULL,
+    [quota_adjustment] TEXT NULL,
+    [entry_name] NVARCHAR(255) NULL,
+    [units] NVARCHAR(20) NULL,
+    [amount] FLOAT NULL,
+    [ex_file] NVARCHAR(4000) NULL,
+    [ex_cell] NVARCHAR(50) NULL,
+    [ex_row] NVARCHAR(4000) NULL,
+    [ex_unit] NVARCHAR(50) NULL,
+    [ex_amount] FLOAT NULL,
+    [is_change] INT NOT NULL DEFAULT 1,
+    [send_status] INT NOT NULL DEFAULT 0,
+    [send_time] DATETIME NULL,
+    [send_error] NVARCHAR(4000) NULL,
+    [is_del] INT NOT NULL DEFAULT 0,
+    [deleted_by] NVARCHAR(50) NULL,
+    [deleted_at] DATETIME NULL,
+    [created_by] NVARCHAR(50) NULL,
+    [created_at] DATETIME NOT NULL DEFAULT GETDATE(),
+    [updated_by] NVARCHAR(50) NULL,
+    [updated_at] DATETIME NOT NULL DEFAULT GETDATE()
+)
+
+CREATE INDEX [idx_project_id] ON [dbo].[project_quota] ([project_id])
+CREATE INDEX [idx_budget_id] ON [dbo].[project_quota] ([budget_id])
+CREATE INDEX [idx_item_id] ON [dbo].[project_quota] ([item_id])
+CREATE INDEX [idx_item_code] ON [dbo].[project_quota] ([item_code])
+CREATE INDEX [idx_created_at] ON [dbo].[project_quota] ([created_at])
+END
+GO
+
+
+/*-- 删除日志表
+IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sys_log]') AND type in (N'U'))
+BEGIN
+    DROP TABLE [dbo].[sys_log]
+END
+GO*/
+
+-- 创建日志表
+IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sys_log]') AND type in (N'U'))
+BEGIN
+CREATE TABLE [dbo].[sys_log] (
+    [id] INT IDENTITY(1,1) PRIMARY KEY,
+    [username] NVARCHAR(255) NOT NULL,
+    [operation_type] NVARCHAR(50) NOT NULL,
+    [operation_desc] NVARCHAR(1000) NULL,
+    [operation_result] INT NULL,
+    [operation_module] NVARCHAR(100) NULL,
+    [operation_data] NVARCHAR(MAX) NULL,
+    [data_changes] NVARCHAR(MAX) NULL,
+    [operation_ip] NVARCHAR(50) NULL,
+    [created_at] DATETIME NOT NULL DEFAULT GETDATE()
+)
+
+CREATE INDEX [idx_username] ON [dbo].[sys_log] ([username])
+CREATE INDEX [idx_created_at] ON [dbo].[sys_log] ([created_at])
+END
+GO

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 18220 - 0
SourceCode/IntelligentRailwayCosting/.script/project_data_test.sql


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

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

+ 35 - 0
SourceCode/IntelligentRailwayCosting/Docker/Dockerfile

@@ -0,0 +1,35 @@
+# 本地构建Dockerfile
+# 基于多阶段构建,使用本地资源和基础镜像
+
+# 第一阶段:使用基础镜像
+FROM railway_costing-base:1.0.0 AS base-system
+
+# 第二阶段:Python依赖构建
+FROM base-system AS builder
+
+# 复制项目依赖文件
+COPY requirements.txt .
+
+# 安装Python依赖
+RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
+
+# 第三阶段:最终应用镜像
+FROM base-system AS final
+
+# 设置工作目录
+WORKDIR /app
+
+# 从builder阶段复制已安装的Python包
+COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
+COPY --from=builder /usr/local/bin /usr/local/bin
+
+# 复制应用程序代码
+COPY app/ /app
+
+
+# 暴露端口
+EXPOSE 5123
+
+
+# 启动命令
+CMD ["python", "main.py"]

+ 78 - 0
SourceCode/IntelligentRailwayCosting/Docker/DockerfileBase

@@ -0,0 +1,78 @@
+# 基础镜像构建Dockerfile
+# 包含系统依赖和ODBC组件
+# 优化版本:减少镜像层数,提高构建效率
+
+# 基础系统依赖镜像
+FROM python:3.13-slim
+
+# 创建工作目录
+WORKDIR /app
+
+# 设置环境变量
+ENV FREETDS_VERSION=7.3 \
+    ODBCSYSINI=/etc \
+    ODBCINI=/etc/odbc.ini \
+    DEBIAN_FRONTEND=noninteractive \
+    TZ=Asia/Shanghai
+
+# 配置阿里云APT源并安装基础依赖
+RUN set -eux; \
+    # 配置阿里云APT源 - 提高下载速度
+    echo "deb http://mirrors.aliyun.com/debian bullseye main contrib non-free" > /etc/apt/sources.list && \
+    echo "deb http://mirrors.aliyun.com/debian bullseye-updates main contrib non-free" >> /etc/apt/sources.list && \
+    echo "deb http://mirrors.aliyun.com/debian-security bullseye-security main contrib non-free" >> /etc/apt/sources.list && \
+    # 修改动态生成的APT源配置文件
+    find /etc/apt/sources.list.d/ -type f -exec sed -i 's|http://deb.debian.org|http://mirrors.aliyun.com|g' {} \; && \
+    find /etc/apt/sources.list.d/ -type f -exec sed -i 's|http://security.debian.org|http://mirrors.aliyun.com|g' {} \; && \
+    # 更新源并安装基础系统依赖 - 合并安装所有依赖减少层数
+    apt-get update && \
+    apt-get install -y --no-install-recommends \
+        # 系统基础依赖
+        apt-utils \
+        libc6 \
+        curl \
+        gnupg2 \
+        ca-certificates \
+        # ODBC基础依赖
+        libltdl7 \
+        libodbc1 \
+        libodbcinst2 \
+        odbcinst \
+        odbcinst1debian2 \
+        unixodbc \
+        unixodbc-common \
+        unixodbc-dev \
+        # FreeTDS相关依赖
+        freetds-bin \
+        freetds-dev \
+        tdsodbc && \
+    # 配置FreeTDS - 使用单一配置文件
+    { \
+        echo "[global]"; \
+        echo "    tds version = 7.3"; \
+        echo "    client charset = UTF-8"; \
+        echo "    timeout = 60"; \
+        echo "    connect timeout = 15"; \
+    } > /etc/freetds/freetds.conf && \
+    # 配置ODBC驱动 - 使用单一配置文件
+    { \
+        echo "[FreeTDS]"; \
+        echo "Description = TDS driver (Sybase/MS SQL)"; \
+        echo "Driver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so"; \
+        echo "CPTimeout ="; \
+        echo "CPReuse ="; \
+        echo "UsageCount = 1"; \
+    } > /etc/odbcinst.ini && \
+    # 验证驱动安装 - 确保配置正确
+    if [ ! -f /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so ]; then \
+        echo "ERROR: FreeTDS ODBC driver not found"; \
+        exit 1; \
+    fi && \
+    # 清理临时文件 - 减小镜像大小
+    apt-get clean && \
+    apt-get autoremove -y && \
+    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
+
+# 健康检查 - 验证ODBC配置是否正确
+HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
+    CMD [ "odbcinst", "-q", "-d", "-n", "FreeTDS" ]

+ 59 - 0
SourceCode/IntelligentRailwayCosting/Docker/docker-compose.yml

@@ -0,0 +1,59 @@
+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
+      - ACCEPT_EULA=Y
+      - SQLNCLI_VERSION=10
+    #      - 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

+ 41 - 0
SourceCode/IntelligentRailwayCosting/Docker/docker-compose_mysql.yml

@@ -0,0 +1,41 @@
+version: '3.8'
+
+services:
+  rc-mysql:
+    image: mysql:8.0.39
+    container_name: railway_costing-mysql
+    environment:
+      - MYSQL_ROOT_PASSWORD=12345654321
+      - TZ=Asia/Shanghai
+    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_mysql.sql:/docker-entrypoint-initdb.d/init.sql # 挂载 init.sql 文件
+    networks:
+      - railway_costing_mysql_v1
+    restart: always
+
+  rc-app:
+    build:
+      context: ../
+      dockerfile: .
+    image: railway_costing-app_mysql:1.0.1
+    container_name: railway_costing-app_mysql
+    depends_on:
+      - rc-mysql
+    environment:
+      - TZ=Asia/Shanghai
+    volumes:
+      - /home/docker/iwb_railway_costing_v1/app/config_mysql.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_mysql_v1
+    ports:
+      - "7011:5123"
+    restart: always
+
+networks:
+  railway_costing_mysql_v1:
+    driver: bridge

+ 19 - 0
SourceCode/IntelligentRailwayCosting/Docker/docker-compose_sqlserver.yml

@@ -0,0 +1,19 @@
+version: '3.8'
+
+services:
+  rc-app:
+    build:
+      context: ../
+      dockerfile: .
+    image: railway_costing-app_sqlserver:1.0.0
+    container_name: railway_costing-app_sqlserver
+    environment:
+      - TZ=Asia/Shanghai
+      - ACCEPT_EULA=Y
+    volumes:
+      - /home/docker/iwb_railway_costing_v1/app/config_sqlserver.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
+    ports:
+      - "7010:5123"
+    restart: always

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


+ 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"
+            }
+        }

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

@@ -0,0 +1,203 @@
+import re, json
+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

+ 90 - 0
SourceCode/IntelligentRailwayCosting/app/config.yml

@@ -0,0 +1,90 @@
+app:
+  # 应用名称
+  name: '铁路造价智能化工具'
+  version: '2020' # 应用版本 2020|2024
+  use_version: true
+  source_path: './temp_files'
+  task_api_url: 'http://127.0.0.1:5123/api/v1'
+  task_max_projects: 5 # 最大项目数同时运行
+  task_interval: 60  # 秒
+db:
+  # SQL Server 配置
+  # SQL Server 2008:'{SQL Server}' 或 '{SQL Server Native Client 10.0}'
+  # SQL Server 2016:'{ODBC Driver 13 for SQL Server}'
+  # SQL Server 2020:'{ODBC Driver 17 for SQL Server}'
+  # SQL Server 2022:'{ODBC Driver 18 for SQL Server}'
+  # 在Windows系统的ODBC数据源管理器中查看已安装的驱动程序,选择相应的驱动名称
+  # 在开始菜单的列表里面找到"Windows管理工具"打开, 然后点开里面的"ODBC数据源"。
+  # 打开以后,点开上方"驱动程序"。 就可以看到系统所安装的ODBC驱动程序
+  # 在docker容器中运行,将配置改为  driver: '{FreeTDS}'
+  #
+  sqlserver_mian_2024:
+    server: 192.168.0.81:1433
+    username: iwb
+    password: 123456Qsc
+    database: Iwb_RecoData2024
+    trusted_connection: false
+  sqlserver_mian_2020:
+    server: 192.168.0.81:1433
+    username: iwb
+    password: 123456Qsc
+    database: Iwb_RecoData2020
+    trusted_connection: false
+  Iwb_RailwayCosting:
+    server: 192.168.0.81:1433
+    username: iwb
+    password: 123456Qsc
+    database: iwb_railway_costing_v1
+    trusted_connection: false
+  Iwb_RecoData2024:
+    server: 192.168.0.81:1433
+    username: iwb
+    password: 123456Qsc
+    database: Iwb_RecoData2024
+    trusted_connection: false
+  Iwb_RecoData2020:
+    server: 192.168.0.81:1433
+    username: iwb
+    password: 123456Qsc
+    database: Iwb_RecoData2020
+    trusted_connection: false
+  # MySQL 配置
+  mysql_main:
+    db: iwb_railway_costing_v1
+    host: 192.168.0.81
+    port: 3307
+    user: root
+    password: Iwb-2024
+    charset: utf8mb4
+  iwb_railway_costing_v1:
+    db: iwb_railway_costing_v1
+    host: 192.168.0.81
+    port: 3307
+    user: root
+    password: Iwb-2024
+    charset: utf8mb4
+ai:
+  api_key: sk-febca8fea4a247f096cedeea9f185520
+  api_url: https://dashscope.aliyuncs.com/compatible-mode/v1
+  model: qwen-max
+  max_tokens: 1024
+fastgpt_ai:
+  api_url: http://192.168.0.104:8020/api
+  api_key: fastgpt-o4CF7Pu1FRTvHjWFqeNcClBS6ApyflNfkBGXo9p51fuBMAX1L0erU8yz8
+  apps:
+    knowledge_01:
+      api_url: http://192.168.0.104:8020/api
+      api_key: fastgpt-o4CF7Pu1FRTvHjWFqeNcClBS6ApyflNfkBGXo9p51fuBMAX1L0erU8yz8
+    app_02_2020:
+      api_url: http://192.168.0.104:8020/api
+      api_key: fastgpt-o4CF7Pu1FRTvHjWFqeNcClBS6ApyflNfkBGXo9p51fuBMAX1L0erU8yz8
+    app_01_2024:
+      api_url: http://192.168.0.104:8020/api
+      api_key: fastgpt-o4CF7Pu1FRTvHjWFqeNcClBS6ApyflNfkBGXo9p51fuBMAX1L0erU8yz8
+    app_02_2024:
+      api_url: http://192.168.0.104:8020/api
+      api_key: fastgpt-o4CF7Pu1FRTvHjWFqeNcClBS6ApyflNfkBGXo9p51fuBMAX1L0erU8yz8
+logger:
+  file_path: './logs/'
+  level: 'debug'
+

+ 0 - 0
SourceCode/IntelligentRailwayCosting/app/core/__init__.py


+ 2 - 0
SourceCode/IntelligentRailwayCosting/app/core/api/__init__.py

@@ -0,0 +1,2 @@
+from .response import ResponseBase
+from .table_response import TableResponse

+ 73 - 0
SourceCode/IntelligentRailwayCosting/app/core/api/response.py

@@ -0,0 +1,73 @@
+from typing import Dict, Any, Optional
+
+from flask import jsonify, Response, make_response
+
+
+class ResponseBase:
+    """统一的API响应结构"""
+
+    @staticmethod
+    def _set_response_headers(response: Response) -> Response:
+        """设置响应头
+        Args:
+            response: Flask响应对象
+        Returns:
+            Response: 设置好响应头的Flask响应对象
+        """
+        response.headers["Content-Type"] = "application/json; charset=utf-8"
+        return response
+
+    @staticmethod
+    def success(data: Optional[Any] = None, message: str = "操作成功") -> Response:
+        """成功响应
+        Args:
+            data: 响应数据
+            message: 响应消息
+        Returns:
+            Dict: 统一的成功响应格式
+        """
+        response = {"success": True, "code": 200, "message": message}
+        if data is not None:
+            response["data"] = data
+        response = make_response(jsonify(response))
+        return ResponseBase._set_response_headers(response)
+
+    @staticmethod
+    def error(
+        message: str = "操作失败", code: int = 400, data: Optional[Any] = None
+    ) -> Response:
+        """错误响应
+        Args:
+            message: 错误消息
+            code: 错误码
+            data: 错误详细信息
+        Returns:
+            Dict: 统一的错误响应格式
+        """
+        response = {"success": False, "code": code, "message": message}
+        if data is not None:
+            response["data"] = data
+        response = make_response(jsonify(response))
+        return ResponseBase._set_response_headers(response)
+
+    @staticmethod
+    def json_response(
+        success: bool = True,
+        code: int = 200,
+        message: str = "",
+        data: Optional[Any] = None,
+    ) -> Response:
+        """自定义响应
+        Args:
+            success: 是否成功
+            code: 状态码
+            message: 响应消息
+            data: 响应数据
+        Returns:
+            Dict: 统一的响应格式
+        """
+        response = {"success": success, "code": code, "message": message}
+        if data is not None:
+            response["data"] = data
+        response = make_response(jsonify(response))
+        return ResponseBase._set_response_headers(response)

+ 38 - 0
SourceCode/IntelligentRailwayCosting/app/core/api/table_response.py

@@ -0,0 +1,38 @@
+from typing import Dict, List
+
+from flask import Response
+
+from .response import ResponseBase
+
+
+class TableResponse(ResponseBase):
+    """表格数据响应结构"""
+
+    @staticmethod
+    def success(
+        rows: List[Dict] = None, total: int = 0, message: str = "操作成功"
+    ) -> Response:
+        """表格数据成功响应
+        Args:
+            rows: 表格数据行列表
+            total: 数据总条数
+            message: 响应消息
+        Returns:
+            Dict: 统一的表格数据响应格式
+        """
+        return ResponseBase.success(
+            data={"rows": rows or [], "total": total}, message=message
+        )
+
+    @staticmethod
+    def error(message: str = "操作失败", code: int = 400, **kwargs) -> Response:
+        """表格数据错误响应
+        Args:
+            message: 错误消息
+            code: 错误码
+        Returns:
+            Response: 统一的错误响应格式
+            :param code:
+            :param message:
+        """
+        return ResponseBase.error(message=message, code=code)

+ 16 - 0
SourceCode/IntelligentRailwayCosting/app/core/configs/__init__.py

@@ -0,0 +1,16 @@
+import os
+from .config import config
+
+# 导出配置实例和配置对象
+__all__ = ['config', 'app', 'database', 'ai', 'fastgpt_ai']
+
+_path = os.environ.get("CONFIG_PATH",  "config.yml")
+
+# 初始化配置
+config.load_config(_path)
+# 导出配置对象的快捷方式
+app = config.app
+database = config.database
+ai= config.ai
+fastgpt_ai = config.fastgpt_ai
+

+ 35 - 0
SourceCode/IntelligentRailwayCosting/app/core/configs/ai_config.py

@@ -0,0 +1,35 @@
+class AIConfig:
+    """AI应用配置实体类"""
+
+    def __init__(self):
+        self._api_url = None
+        self._api_key = None
+        self._model = None
+        self._max_tokens = None
+
+    @property
+    def api_url(self):
+        return self._api_url
+
+    @property
+    def api_key(self):
+        return self._api_key
+
+    @property
+    def model(self):
+        return self._model
+
+    @property
+    def max_tokens(self):
+        return self._max_tokens
+
+    def update_config(self, config):
+        """更新AI应用配置
+
+        Args:
+            config: AI应用配置字典
+        """
+        self._api_url = config.get("api_url", "")
+        self._api_key = config.get("api_key", "")
+        self._model = config.get("model", "")
+        self._max_tokens = config.get("max_tokens", "")

+ 49 - 0
SourceCode/IntelligentRailwayCosting/app/core/configs/ai_fastgpt_config.py

@@ -0,0 +1,49 @@
+from core.configs.ai_config import AIConfig
+
+
+class FastGPTAIConfig:
+    """FastGPT AI配置管理类"""
+
+    _api_url = None
+    _api_key = None
+    _apps = {}
+
+    @property
+    def api_url(self):
+        return self._api_url
+
+    @property
+    def api_key(self):
+        return self._api_key
+
+    @property
+    def apps(self):
+        return self._apps
+
+    def __getitem__(self, app_key: str):
+        """支持字典式访问应用配置
+
+        Args:
+            app_key: 应用配置键名
+
+        Returns:
+            AIConfig: 应用配置实例
+        """
+        return self._apps.get(app_key)
+
+    def update_config(self, config):
+        """更新FastGPT AI配置
+
+        Args:
+            config: FastGPT AI配置字典
+        """
+        self._api_url = config.get("api_url", "")
+        self._api_key = config.get("api_key", "")
+        # 更新apps配置
+        apps_config = config.get("apps", {})
+        # 更新apps配置
+        self._apps = {}
+        for app_key, app_config in apps_config.items():
+            app = AIConfig()
+            app.update_config(app_config)
+            self._apps[app_key] = app

+ 58 - 0
SourceCode/IntelligentRailwayCosting/app/core/configs/app_config.py

@@ -0,0 +1,58 @@
+class AppConfig:
+    """应用配置管理类"""
+
+    _name = ""
+    _version = "2020"
+    _use_version = True
+    _source_path = ""
+    _task_api_url = ""
+    _task_max_projects_count = 10
+    _task_interval = 300  # 任务执行间隔时间,单位秒
+    _ai_flag = "_AI"
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def version(self):
+        return self._version or "2020"
+
+    @property
+    def use_version(self) -> bool:
+        return self._use_version
+
+    @property
+    def source_path(self) -> str:
+        return self._source_path
+
+    @property
+    def task_api_url(self) -> str:
+        return self._task_api_url
+
+    @property
+    def task_max_projects_count(self) -> int:
+        return self._task_max_projects_count
+
+    @property
+    def task_interval(self) -> int:
+        return self._task_interval
+
+    @property
+    def ai_flag(self) -> str:
+        return self._ai_flag
+
+    def update_config(self, config):
+        """更新应用配置
+
+        Args:
+            config: 应用配置字典
+        """
+        self._name = config.get("name", "")
+        self._version = config.get("version", "2000")
+        self._use_version = config.get("use_version", False)
+        self._source_path = config.get("source_path", "./temp_files")
+        self._task_api_url = config.get("task_api_url", "")
+        self._task_max_projects_count = int(config.get("task_max_projects", 10))
+        self._task_interval = int(config.get("task_interval", 300))
+        self._ai_flag = config.get("ai_flag", "[AI]")

+ 61 - 0
SourceCode/IntelligentRailwayCosting/app/core/configs/config.py

@@ -0,0 +1,61 @@
+
+import os
+import yaml
+from .app_config import AppConfig
+from .database_config import DatabaseConfig
+from .ai_config import AIConfig
+from .ai_fastgpt_config import FastGPTAIConfig
+
+class Config:
+    """配置管理类"""
+    _instance = None
+    _config = None
+    app = None
+    database = None
+    ai = None
+    fastgpt_ai = None
+
+    def __new__(cls):
+        if cls._instance is None:
+            cls._instance = super().__new__(cls)
+            cls.app = AppConfig()
+            cls.database = DatabaseConfig()
+            cls.ai = AIConfig()
+            cls.fastgpt_ai = FastGPTAIConfig()
+        return cls._instance
+
+    @classmethod
+    def load_config(cls, path=None):
+        """加载配置文件
+        
+        Args:
+            path: 配置文件路径,如果为None则使用默认路径
+        """
+        if path is None:
+            # path = os.environ.get("CONFIG_PATH", os.path.join(os.path.dirname(__file__), "../..", "config.yml"))
+            path = os.environ.get("CONFIG_PATH","config.yml")
+
+        with open(path, 'r', encoding='utf-8') as f:
+            cls._config = yaml.safe_load(f)
+            # 初始化应用配置
+            cls.app.update_config(cls._config.get('app', {}))
+            # 初始化数据库配置
+            cls.database.update_config(cls._config.get('db', {}))
+            # 初始化AI配置
+            cls.ai.update_config(cls._config.get('ai', {}))
+            # 初始化FastGPT AI配置
+            cls.fastgpt_ai.update_config(cls._config.get('fastgpt_ai', {}))
+
+    @classmethod
+    def reload(cls, path=None):
+        """重新加载配置
+        Args:
+            path: 配置文件路径,如果为None则使用默认路径
+        """
+        cls._config = None
+        cls.load_config(path)
+
+# 创建全局配置实例
+config = Config()
+
+  

+ 17 - 0
SourceCode/IntelligentRailwayCosting/app/core/configs/database_config.py

@@ -0,0 +1,17 @@
+class DatabaseConfig:
+    """数据库配置管理类"""
+    _configs = {}
+
+    def __getitem__(self, key):
+        return self._configs.get(key, {})
+
+    def get(self, key, default=None):
+        return self._configs.get(key, default)
+
+    def update_config(self, configs):
+        """更新数据库配置
+        
+        Args:
+            configs: 数据库配置字典
+        """
+        self._configs = configs

+ 14 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/__init__.py

@@ -0,0 +1,14 @@
+from .log import LogDto
+from .project_quota import ProjectQuotaDto
+from .project_task import ProjectTaskDto
+
+from .user import UserDto
+from .project import ProjectDto
+from .total_budget_info import TotalBudgetInfoDto
+from .total_budget_item import TotalBudgetItemDto
+from .chapter import ChapterDto
+from .quota_input import QuotaInputDto
+
+from .excel_parse import ExcelParseDto,ExcelParseZgsDto,ExcelParseItemDto,ExcelParseFileDto,ExcelParseResultDataDto,ExcelParseResultDto
+
+

+ 144 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/chapter.py

@@ -0,0 +1,144 @@
+from typing import Optional
+from pydantic import BaseModel
+from ..models.chapter import ChapterModel
+from ..models.total_budget_item import TotalBudgetItemModel
+
+
+class ChapterDto(BaseModel):
+    # 章节表字段
+    item_id: int
+    item_code: Optional[str] = None
+    parent_code: Optional[str] = None
+    default_first_page: Optional[str] = None
+    chapter: Optional[str] = None
+    section: Optional[str] = None
+    project_name: Optional[str] = None
+    unit: Optional[str] = None
+    # is_locked: Optional[str] = None
+    # unit2: Optional[str] = None
+    item_type: Optional[str] = None
+    # project_code: Optional[str] = None
+    # cooperation_fee_code: Optional[str] = None
+    # formula_code: Optional[str] = None
+    # material_price_diff_code: Optional[str] = None
+    # calculation_formula: Optional[str] = None
+    # selected_labor_cost: Optional[int] = None
+    # shift_labor_cost: Optional[int] = None
+    # rate_scheme: Optional[str] = None
+    # transport_scheme: Optional[str] = None
+    # summary_method: Optional[str] = None
+    # cumulative_coefficient: Optional[float] = None
+    # display_range: Optional[str] = None
+    # cost_category: Optional[str] = None
+    # auto_calculate_quantity: Optional[bool] = None
+    # comprehensive_display: Optional[bool] = None
+    # list_code: Optional[str] = None
+    # sub_division_feature: Optional[str] = None
+    # quantity_calculation_rule: Optional[str] = None
+    # work_content: Optional[str] = None
+    # note: Optional[str] = None
+    # seat_count: Optional[int] = None
+    # quota_book_number: Optional[str] = None
+    # quota_section_number: Optional[str] = None
+    # professional_name: Optional[str] = None
+    # tax_category: Optional[str] = None
+
+    # 总概算条目字段
+    budget_id: Optional[int] = None
+
+    # project_quantity1: Optional[float] = None
+    # project_quantity2: Optional[float] = None
+    # budget_value: Optional[float] = None
+    # budget_index1: Optional[float] = None
+    # budget_index2: Optional[float] = None
+    # construction_cost: Optional[float] = None
+    # installation_cost: Optional[float] = None
+    # equipment_cost: Optional[float] = None
+    # other_cost: Optional[float] = None
+    # transport_unit_price: Optional[float] = None
+    # parameter_adjustment: Optional[str] = None
+    # project_quantity1_input: Optional[str] = None
+    # project_quantity2_input: Optional[str] = None
+    # installation_sub_item: Optional[str] = None
+    # tax_rate: Optional[float] = None
+    # bridge_type: Optional[str] = None
+    # electricity_price_category: Optional[int] = None
+    # tax: Optional[float] = None
+
+    @classmethod
+    def from_model(
+        cls,
+        chapter_model: ChapterModel,
+        budget_item_model: Optional[TotalBudgetItemModel] = None,
+    ) -> "ChapterDto":
+        """从数据库模型创建DTO对象"""
+        dto = cls(
+            # 章节表字段
+            item_id=chapter_model.item_id,
+            item_code=chapter_model.item_code,
+            chapter=chapter_model.chapter,
+            section=chapter_model.section,
+            project_name=chapter_model.project_name,
+            unit=chapter_model.unit,
+            # is_locked=chapter_model.is_locked,
+            # unit2=chapter_model.unit2,
+            item_type=chapter_model.item_type,
+            # project_code=chapter_model.project_code,
+            # cooperation_fee_code=chapter_model.cooperation_fee_code,
+            # formula_code=chapter_model.formula_code,
+            # material_price_diff_code=chapter_model.material_price_diff_code,
+            # calculation_formula=chapter_model.calculation_formula,
+            # selected_labor_cost=chapter_model.selected_labor_cost,
+            # shift_labor_cost=chapter_model.shift_labor_cost,
+            # rate_scheme=chapter_model.rate_scheme,
+            # transport_scheme=chapter_model.transport_scheme,
+            # summary_method=chapter_model.summary_method,
+            # cumulative_coefficient=chapter_model.cumulative_coefficient,
+            # display_range=chapter_model.display_range,
+            # cost_category=chapter_model.cost_category,
+            # auto_calculate_quantity=chapter_model.auto_calculate_quantity,
+            # comprehensive_display=chapter_model.comprehensive_display,
+            # list_code=chapter_model.list_code,
+            # sub_division_feature=chapter_model.sub_division_feature,
+            # quantity_calculation_rule=chapter_model.quantity_calculation_rule,
+            # work_content=chapter_model.work_content,
+            # note=chapter_model.note,
+            # seat_count=chapter_model.seat_count,
+            # quota_book_number=chapter_model.quota_book_number,
+            # quota_section_number=chapter_model.quota_section_number,
+            # professional_name=chapter_model.professional_name,
+            # tax_category=chapter_model.tax_category
+        )
+        if "default_first_page" in chapter_model:
+            dto.default_first_page = chapter_model.default_first_page
+
+        # 如果提供了总概算条目模型,则添加相关字段
+        if budget_item_model:
+            dto.budget_id = budget_item_model.budget_id
+            # dto.project_quantity1 = budget_item_model.project_quantity1
+            # dto.project_quantity2 = budget_item_model.project_quantity2
+            # dto.budget_value = budget_item_model.budget_value
+            # dto.budget_index1 = budget_item_model.budget_index1
+            # dto.budget_index2 = budget_item_model.budget_index2
+            # dto.construction_cost = budget_item_model.construction_cost
+            # dto.installation_cost = budget_item_model.installation_cost
+            # dto.equipment_cost = budget_item_model.equipment_cost
+            # dto.other_cost = budget_item_model.other_cost
+            # dto.transport_unit_price = budget_item_model.transport_unit_price
+            # dto.parameter_adjustment = budget_item_model.parameter_adjustment
+            # dto.project_quantity1_input = budget_item_model.project_quantity1_input
+            # dto.project_quantity2_input = budget_item_model.project_quantity2_input
+            # dto.installation_sub_item = budget_item_model.installation_sub_item
+            # dto.tax_rate = budget_item_model.tax_rate
+            # dto.bridge_type = budget_item_model.bridge_type
+            # dto.electricity_price_category = budget_item_model.electricity_price_category
+            # dto.tax = budget_item_model.tax
+
+        return dto
+
+    def to_dict(self) -> dict:
+        """转换为字典格式"""
+        return self.model_dump()
+
+    class Config:
+        from_attributes = True

+ 226 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/excel_parse.py

@@ -0,0 +1,226 @@
+from core.dtos import TotalBudgetInfoDto, ChapterDto
+import base64
+
+
+class ExcelParseZgsDto:
+    def __init__(self, zgs_id: int, zgs_code: str):
+        self.zgs_id = zgs_id
+        self.zgs_code = zgs_code
+
+    @classmethod
+    def from_dto(cls, dto: TotalBudgetInfoDto):
+        return cls(zgs_id=dto.budget_id, zgs_code=dto.budget_code)
+
+    def to_dict(self):
+        return {"zgs_id": self.zgs_id, "zgs_code": self.zgs_code}
+
+
+class ExcelParseItemDto:
+    def __init__(self, item_id: int, item_code: str, item_name: str):
+        self.item_id = item_id
+        self.item_code = item_code
+        self.item_name = item_name
+
+    @classmethod
+    def from_dto(cls, dto: ChapterDto):
+        return cls(
+            item_id=dto.item_id, item_code=dto.item_code, item_name=dto.project_name
+        )
+
+    def to_dict(self):
+        return {
+            "item_id": self.item_id,
+            "item_code": self.item_code,
+            "item_name": self.item_name,
+        }
+
+
+class ExcelParseFileDto:
+    def __init__(self, file_id: str, content: str):
+        self.file_id = file_id
+        # 从file_id路径中获取文件类型
+        self.file_type = file_id.split(".")[-1]
+        self.content = content
+
+    def to_dict(self):
+        return {
+            "file_id": base64.b64encode(self.file_id.encode()).decode(),
+            "file_type": self.file_type,
+            "content": self.content,
+        }
+
+
+class ExcelParseDto:
+    def __init__(
+        self,
+        task_id: int,
+        version: str,
+        project_id: str,
+        project_name: str,
+        project_stage: str,
+        selected_zgs_id: int,
+        zgs_list: list[ExcelParseZgsDto],
+        selected_chapter: ExcelParseItemDto,
+        # hierarchy: list[ExcelParseItemDto],
+        # components: list[ExcelParseItemDto],
+        files: list[ExcelParseFileDto],
+    ):
+        self.task_id = task_id
+        self.version = version
+        self.project_id = project_id
+        self.project_name = project_name
+        self.project_stage = project_stage
+        self.selected_zgs_id = -1 if selected_zgs_id == 0 else selected_zgs_id
+        self.zgs_list = zgs_list
+        self.selected_chapter = selected_chapter
+        # self.hierarchy = hierarchy
+        # self.components = components
+        self.files = files
+
+    def to_dict(self):
+        data = {
+            "task_id": self.task_id,
+            "version": self.version,
+            "project_id": self.project_id,
+            "project_name": self.project_name,
+            "project_stage": self.project_stage,
+            "selected_zgs_id": self.selected_zgs_id,
+            "files": [file.to_dict() for file in self.files],
+            "zgs_list": [zgs.to_dict() for zgs in self.zgs_list],
+            "selected_chapter": self.selected_chapter.to_dict(),
+            # "hierarchy": [item.to_dict() for item in self.hierarchy],
+            # "components": [item.to_dict() for item in self.components],
+        }
+        return data
+
+
+class ExcelParseResultDataDto:
+    # {
+    #     "zgs_id": 总概算id, // int
+    #     "zgs_code": 概算编号,
+    #     "item_id": 条⽬序号, // int
+    #     "item_code": 条⽬编码,
+    #     "dinge_code": 定额编号,
+    #     "entry_name": ⼯程或费⽤项⽬名称,来⾃于定额表,
+    #     "units": 单位,
+    #     "amount": 数量, // number
+    #     "target_id": ⽤户数据库中条⽬的id,-1表示没有, // int
+    #     "ex_file_id": excel⽂件id, // str
+    #     "ex_cell": 数量单元格位置,例如 "C17",
+    #     "ex_row": 该⾏内容,由逗号连接多个单元格得到,
+    #     "ex_unit": excel中给出的单位,
+    #     "ex_amount": excel中给出的数量, // number
+    # }
+    def __init__(
+        self,
+        zgs_id: int,  # 总概算id
+        zgs_code: str,  # 总概算编号
+        item_id: int,  # 条⽬序号
+        item_code: str,  # 条⽬编码
+        entry_name: str,  # ⼯程或费⽤项⽬名称,来⾃于定额表,
+        dinge_code: str,  # 定额编号,
+        dinge_adjust: str,  # 定额调整,
+        units: str,  # 单位,
+        amount: float,  # 数量,
+        target_id: int,  # ⽤户数据库中条⽬的id,-1表示没有,
+        ex_file_id: str,  # excel⽂件id,
+        ex_cell: str,  # 数量单元格位置,例如 "C17",
+        ex_row: str,  # 该⾏内容,由逗号连接多个单元格得到,
+        ex_unit: str,  # excel中给出的单位,
+        ex_amount: float,  # excel中给出的数量,
+    ):
+        self.zgs_id = zgs_id
+        self.zgs_code = zgs_code
+        self.item_id = item_id
+        self.item_code = item_code
+        self.entry_name = entry_name
+        self.dinge_code = dinge_code
+        self.dinge_adjust = dinge_adjust
+        self.units = units
+        self.amount = amount
+        self.target_id = target_id
+        self.ex_file = ex_file_id
+        self.ex_file_id = ex_file_id
+        self.ex_cell = ex_cell
+        self.ex_row = ex_row
+        self.ex_unit = ex_unit
+        self.ex_amount = ex_amount
+
+    @classmethod
+    def from_dict(cls, data: dict):
+        return cls(
+            zgs_id=data.get("zgs_id", 0),
+            zgs_code=data.get("zgs_code", ""),
+            item_id=data.get("item_id", 0),
+            item_code=data.get("item_code", ""),
+            entry_name=data.get("entry_name", ""),
+            dinge_code=data.get("dinge_code", ""),
+            dinge_adjust=data.get("dinge_adjust", ""),
+            units=data.get("units", ""),
+            amount=data.get("amount", 0.0),
+            target_id=data.get("target_id", -1),
+            ex_file_id=(
+                base64.b64decode(data.get("ex_file_id", "").encode()).decode()
+                if data.get("ex_file_id", "")
+                else ""
+            ),
+            ex_cell=data.get("ex_cell", ""),
+            ex_row=data.get("ex_row", ""),
+            ex_unit=data.get("ex_unit", ""),
+            ex_amount=data.get("ex_amount", 0.0),
+        )
+
+    def to_dict(self):
+        return {
+            "zgs_id": self.zgs_id,
+            "zgs_code": self.zgs_code,
+            "item_id": self.item_id,
+            "item_code": self.item_code,
+            "entry_name": self.entry_name,
+            "dinge_code": self.dinge_code,
+            "dinge_adjust": self.dinge_adjust,
+            "target_id": self.target_id,
+            "units": self.units,
+            "amount": self.amount,
+            "ex_file": self.ex_file_id,
+            # "ex_file_id": base64.b64encode(self.ex_file_id.encode('utf-8')).decode('utf-8') if self.ex_file_id else self.ex_file_id,
+            "ex_file_id": self.ex_file_id,
+            "ex_cell": self.ex_cell,
+            "ex_row": self.ex_row,
+            "ex_unit": self.ex_unit,
+            "ex_amount": self.ex_amount,
+        }
+
+
+class ExcelParseResultDto:
+    def __init__(
+        self,
+        task_id: int,
+        result: int = -1,
+        reason: str = "",
+        data: list[ExcelParseResultDataDto] = None,
+    ):
+        self.task_id = task_id
+        self.result = result  # -1-失败;0-运行中;1-成功
+        self.data = data
+        self.reason = reason
+
+    @classmethod
+    def from_dict(cls, response: dict):
+        data = [
+            ExcelParseResultDataDto.from_dict(item) for item in response.get("data", [])
+        ]
+        return cls(
+            task_id=response.get("task_id", 0),
+            result=response.get("result", 0),
+            reason=response.get("reason", ""),
+            data=data,
+        )
+
+    def to_dict(self):
+        return {
+            "task_id": self.task_id,
+            "result": self.result,
+            "reason": self.reason,
+            "data": [item.to_dict() for item in self.data] if self.data else [],
+        }

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

@@ -0,0 +1,45 @@
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime
+from ..models import LogModel
+
+class LogDto(BaseModel):
+    """日志信息DTO"""
+    id: Optional[int] = None
+    username: str
+    operation_type: str
+    operation_desc: str
+    operation_result: int
+    operation_module: str
+    operation_data: Optional[str] = None
+    data_changes: Optional[str] = None
+    operation_ip: str
+    created_at: datetime
+
+    @classmethod
+    def from_model(cls, model: LogModel) -> 'LogDto':
+        """从数据库模型创建DTO对象"""
+        return cls(
+            id=model.id,
+            username=model.username,
+            operation_type=model.operation_type,
+            operation_desc=model.operation_desc,
+            operation_result=model.operation_result,
+            operation_module=model.operation_module,
+            operation_data=model.operation_data,
+            data_changes=model.data_changes,
+            operation_ip=model.operation_ip,
+            created_at=model.created_at
+        )
+
+    def to_dict(self) -> dict:
+        """转换为字典格式"""
+        data = self.model_dump()
+
+        if self.created_at:
+            data['created_at'] = self.created_at.strftime('%Y-%m-%d %H:%M:%S')
+        return data
+
+    class Config:
+        from_attributes = True
+ 

+ 87 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/project.py

@@ -0,0 +1,87 @@
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime
+from ..models import ProjectModel
+
+class ProjectDto(BaseModel):
+    """项目信息DTO"""
+    project_id: Optional[str] = None
+    project_name: str
+    project_manager: Optional[str] = None
+    design_stage: Optional[str] = None
+    project_desc: Optional[str] = None
+    short_name: Optional[str] = None
+    project_type: Optional[str] = None
+    project_version: Optional[str] = None
+    unit: Optional[str] = None
+    create_time: Optional[datetime] = None
+    # compilation_method: Optional[str] = None
+    # compilation_scope: Optional[str] = None
+    # total_engineering: Optional[float] = None
+    # total_budget: Optional[float] = None
+    # budget_index: Optional[float] = None
+    # standard_quota: Optional[str] = None
+    # train_transport_standard: Optional[str] = None
+    # material_library: Optional[str] = None
+    # work_shift_library: Optional[str] = None
+    # equipment_library: Optional[str] = None
+    # review_status: Optional[int] = None
+    # years_to_construction: Optional[int] = None
+    # project_password: Optional[str] = None
+    # railway_grade: Optional[str] = None
+    # main_line_count: Optional[int] = None
+    # traction_type: Optional[str] = None
+    # blocking_mode: Optional[str] = None
+    # station_count: Optional[str] = None
+    # target_speed: Optional[int] = None
+    # print_compilation_review: Optional[bool] = None
+    # unit_conversion: Optional[bool] = None
+    # completion_status: Optional[str] = None
+
+    @classmethod
+    def from_model(cls, model: ProjectModel) -> 'ProjectDto':
+        """从数据库模型创建DTO对象"""
+        return cls(
+            project_id=model.project_id,
+            project_name=model.project_name,
+            project_manager=model.project_manager,
+            design_stage=model.design_stage,
+            # project_desc=model.project_description,
+            short_name=model.short_name,
+            project_type=model.project_type,
+            project_version=model.project_version,
+            unit=model.unit,
+            create_time=model.create_time,
+            # compilation_method=model.compilation_method,
+            # compilation_scope=model.compilation_scope,
+            # total_engineering=model.total_engineering,
+            # total_budget=model.total_budget,
+            # budget_index=model.budget_index,
+            # standard_quota=model.standard_quota,
+            # train_transport_standard=model.train_transport_standard,
+            # material_library=model.material_library,
+            # work_shift_library=model.work_shift_library,
+            # equipment_library=model.equipment_library,
+            # review_status=model.review_status,
+            # years_to_construction=model.years_to_construction,
+            # project_password=model.project_password,
+            # railway_grade=model.railway_grade,
+            # main_line_count=model.main_line_count,
+            # traction_type=model.traction_type,
+            # blocking_mode=model.blocking_mode,
+            # station_count=model.station_count,
+            # target_speed=model.target_speed,
+            # print_compilation_review=model.print_compilation_review,
+            # unit_conversion=model.unit_conversion,
+            # completion_status=model.completion_status
+        )
+
+    def to_dict(self) -> dict:
+        """转换为字典格式"""
+        data = self.model_dump()
+        if self.create_time:
+            data['create_time'] = self.create_time.strftime('%Y-%m-%d %H:%M:%S')
+        return data
+
+    class Config:
+        from_attributes = True

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

@@ -0,0 +1,114 @@
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime
+
+from core.models import ProjectQuotaModel
+
+
+class ProjectQuotaDto(BaseModel):
+    """项目定额DTO"""
+
+    id: Optional[int] = None
+    task_id: Optional[int] = None
+    project_id: str
+    budget_id: int
+    budget_code: Optional[str] = None
+    item_id: int
+    item_code: str
+    quota_id: Optional[int] = 0
+    quota_code: Optional[str] = None
+    quota_adjustment: Optional[str] = None
+    entry_name: Optional[str] = None
+    units: Optional[str] = None
+    amount: Optional[float] = None
+    ex_file: Optional[str] = None
+    ex_cell: Optional[str] = None
+    ex_row: Optional[str] = None
+    ex_unit: Optional[str] = None
+    ex_amount: Optional[float] = None
+    is_change: int = 1
+    send_status: int = 0
+    send_time: Optional[datetime] = None
+    send_error: Optional[str] = None
+    created_at: Optional[datetime] = None
+    created_by: Optional[str] = None
+    updated_at: Optional[datetime] = None
+    updated_by: Optional[str] = None
+
+    @classmethod
+    def from_model(cls, model: ProjectQuotaModel) -> "ProjectQuotaDto":
+        """从数据库模型创建DTO对象"""
+        return cls(
+            id=model.id,
+            task_id=model.task_id,
+            project_id=model.project_id,
+            budget_id=model.budget_id,
+            budget_code=model.budget_code,
+            item_id=model.item_id,
+            item_code=model.item_code,
+            quota_id=model.quota_id,
+            quota_code=model.quota_code,
+            quota_adjustment=model.quota_adjustment,
+            entry_name=model.entry_name,
+            units=model.units,
+            amount=model.amount,
+            ex_file=model.ex_file,
+            ex_cell=model.ex_cell,
+            ex_row=model.ex_row,
+            ex_unit=model.ex_unit,
+            ex_amount=model.ex_amount,
+            is_change=model.is_change,
+            send_status=model.send_status,
+            send_time=model.send_time,
+            send_error=model.send_error,
+            created_at=model.created_at,
+            created_by=model.created_by,
+            updated_at=model.updated_at,
+            updated_by=model.updated_by,
+        )
+
+    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", ""),
+            units=data.get("u", ""),
+        )
+
+    def to_ai_dict(self):
+        return {
+            "i": self.id,
+            "n": self.entry_name,
+            "u": self.unit,
+            "q": self.project_quantity,
+        }
+
+    class Config:
+        from_attributes = True

+ 75 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/project_task.py

@@ -0,0 +1,75 @@
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime
+from ..models import ProjectTaskModel
+
+class ProjectTaskDto(BaseModel):
+    """项目任务DTO"""
+    id: Optional[int] = None
+    task_name: str
+    task_sort: int = 0
+    task_desc: Optional[str] = None
+    is_cover: Optional[int] = 0
+    project_id: str
+    budget_id: Optional[int] = 0
+    item_id: int
+    item_code: Optional[str] = None
+    file_path: Optional[str] = None
+    process_status: Optional[int] = 0
+    process_time: Optional[datetime] = None
+    process_error: Optional[str] = None
+    send_status: Optional[int] = 0
+    send_time: Optional[datetime] = None
+    send_error: Optional[str] = None
+    is_del: Optional[int] = 0
+    deleted_by: Optional[str] = None
+    deleted_at: Optional[datetime] = None
+    created_by: Optional[str] = None
+    created_at: Optional[datetime] = None
+    updated_by: Optional[str] = None
+    updated_at: Optional[datetime] = None
+
+    @classmethod
+    def from_model(cls, model: ProjectTaskModel) -> 'ProjectTaskDto':
+        """从数据库模型创建DTO对象"""
+        return cls(
+            id=model.id,
+            task_name=model.task_name,
+            task_sort=model.task_sort,
+            task_desc=model.task_desc,
+            is_cover=model.is_cover,
+            project_id=model.project_id,
+            budget_id=model.budget_id,
+            item_id=model.item_id,
+            item_code=model.item_code,
+            file_path=model.file_path,
+            process_status=model.process_status,
+            process_time=model.process_time,
+            process_error=model.process_error,
+            send_status=model.send_status,
+            send_time=model.send_time,
+            send_error=model.send_error,
+            is_del=model.is_del,
+            deleted_by=model.deleted_by,
+            deleted_at=model.deleted_at,
+            created_by=model.created_by,
+            created_at=model.created_at,
+            updated_by=model.updated_by,
+            updated_at=model.updated_at
+        )
+
+    @classmethod
+    def from_dict(cls, data: dict) -> 'ProjectTaskDto':
+        """从字典创建DTO对象"""
+        # if 'budget_id' in data and 'budget_id' not in data:
+        #     data['budget_id'] = data.pop('budget_id')
+        return cls(**data)
+
+    def to_dict(self) -> dict:
+        """转换为字典格式"""
+        return self.model_dump()
+
+    def get_path(self):
+        return f"{self.project_id}_{self.item_id}({self.item_code})_{self.id}/{datetime.now().strftime('%Y%m%d%H%M%S')}"
+    class Config:
+        from_attributes = True

+ 153 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/quota_input.py

@@ -0,0 +1,153 @@
+from pydantic import BaseModel
+from typing import Optional
+from core.models.quota_input import QuotaInputModel
+from core.dtos.project_quota import ProjectQuotaDto
+
+
+class QuotaInputDto(BaseModel):
+    """定额输入DTO"""
+
+    quota_id: Optional[int] = None
+    budget_id: int
+    item_id: int
+    quota_code: str
+    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
+    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
+    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
+
+    @classmethod
+    def from_model(cls, model: QuotaInputModel) -> "QuotaInputDto":
+        """从数据库模型创建DTO对象"""
+        return cls(
+            quota_id=model.quota_id,
+            budget_id=model.budget_id,
+            item_id=model.item_id,
+            quota_code=model.quota_code,
+            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,
+            unit_price=model.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,
+            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
+        )
+
+    @classmethod
+    def from_quota_dto(cls, quota_dto: ProjectQuotaDto):
+        return cls(
+            quota_id=quota_dto.quota_id,
+            budget_id=quota_dto.budget_id,
+            item_id=quota_dto.item_id,
+            quota_code=quota_dto.quota_code,
+            sequence_number=1000,
+            project_name=quota_dto.entry_name,
+            unit=quota_dto.units,
+            project_quantity=quota_dto.amount,
+            project_quantity_input=str(quota_dto.amount),
+            quota_adjustment=quota_dto.quota_adjustment,
+            unit_price=0.1,
+            # compilation_unit_price=quota_dto.compilation_unit_price,
+            # total_price=quota_dto.total_price,
+            # compilation_total_price=quota_dto.compilation_total_price,
+            unit_weight=0.1,
+            # 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()
+
+    class Config:
+        from_attributes = True

+ 72 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/total_budget_info.py

@@ -0,0 +1,72 @@
+from pydantic import BaseModel
+from typing import Optional
+from ..models.total_budget_info import TotalBudgetInfoModel
+
+class TotalBudgetInfoDto(BaseModel):
+    """总概算信息DTO"""
+
+    budget_id: Optional[int] = None
+    budget_code: str
+    # compilation_scope: Optional[str] = None
+    # project_quantity: float
+    # unit: str
+    # budget_value: Optional[float] = None
+    # budget_index: Optional[float] = None
+    # price_diff_coefficient: str
+    # price_diff_area: str
+    # ending_scheme: str
+    # material_cost_scheme: str
+    # mechanical_cost_scheme: str
+    # equipment_cost_scheme: str
+    # labor_cost_scheme: str
+    # compilation_status: int
+    # train_interference_count: Optional[int] = None
+    # train_interference_10_count: Optional[int] = None
+    # deduct_supplied_materials: Optional[int] = None
+    # auto_calculate_quantity: Optional[bool] = None
+    # mechanical_depreciation_adjustment: Optional[float] = None
+    # construction_supervision_group: Optional[int] = None
+    # construction_management_group: Optional[int] = None
+    # survey_group: Optional[int] = None
+    # design_group: Optional[int] = None
+    # compilation_scope_group: Optional[int] = None
+    # enable_total_budget_group: Optional[int] = None
+
+    @classmethod
+    def from_model(cls, model: TotalBudgetInfoModel) -> 'TotalBudgetInfoDto':
+        """从数据库模型创建DTO对象"""
+        return cls(
+            budget_id=model.budget_id,
+            budget_code=model.budget_code,
+            # compilation_scope=model.compilation_scope,
+            # project_quantity=model.project_quantity,
+            # unit=model.unit,
+            # budget_value=model.budget_value,
+            # budget_index=model.budget_index,
+            # price_diff_coefficient=model.price_diff_coefficient,
+            # price_diff_area=model.price_diff_area,
+            # ending_scheme=model.ending_scheme,
+            # material_cost_scheme=model.material_cost_scheme,
+            # mechanical_cost_scheme=model.mechanical_cost_scheme,
+            # equipment_cost_scheme=model.equipment_cost_scheme,
+            # labor_cost_scheme=model.labor_cost_scheme,
+            # compilation_status=model.compilation_status,
+            # train_interference_count=model.train_interference_count,
+            # train_interference_10_count=model.train_interference_10_count,
+            # deduct_supplied_materials=model.deduct_supplied_materials,
+            # auto_calculate_quantity=model.auto_calculate_quantity,
+            # mechanical_depreciation_adjustment=model.mechanical_depreciation_adjustment,
+            # construction_supervision_group=model.construction_supervision_group,
+            # construction_management_group=model.construction_management_group,
+            # survey_group=model.survey_group,
+            # design_group=model.design_group,
+            # compilation_scope_group=model.compilation_scope_group,
+            # enable_total_budget_group=model.enable_total_budget_group
+        )
+
+    def to_dict(self) -> dict:
+        """转换为字典格式"""
+        return self.model_dump()
+
+    class Config:
+        from_attributes = True

+ 130 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/total_budget_item.py

@@ -0,0 +1,130 @@
+from pydantic import BaseModel
+from typing import Optional
+from ..models.total_budget_item import TotalBudgetItemModel
+
+
+class TotalBudgetItemDto(BaseModel):
+    """总概算条目DTO"""
+
+    budget_id: int
+    item_id: int
+    # project_quantity1: Optional[float] = None
+    # project_quantity2: Optional[float] = None
+    # budget_value: Optional[float] = None
+    # budget_index1: Optional[float] = None
+    # budget_index2: Optional[float] = None
+    # construction_cost: Optional[float] = None
+    # installation_cost: Optional[float] = None
+    # equipment_cost: Optional[float] = None
+    # other_cost: Optional[float] = None
+    # selected_labor_cost: Optional[int] = None
+    # shift_labor_cost: Optional[int] = None
+    # rate_scheme: Optional[str] = None
+    # formula_code: Optional[str] = None
+    # transport_scheme: Optional[str] = None
+    # transport_unit_price: Optional[float] = None
+    # parameter_adjustment: Optional[str] = None
+    # calculation_formula: Optional[str] = None
+    # unit1: Optional[str] = None
+    # unit2: Optional[str] = None
+    # project_quantity1_input: Optional[str] = None
+    # project_quantity2_input: Optional[str] = None
+    # seat_count: Optional[int] = None
+    # installation_sub_item: Optional[str] = None
+    # cooperation_fee_code: Optional[str] = None
+    # tax_category: Optional[str] = None
+    # tax_rate: Optional[float] = None
+    # bridge_type: Optional[str] = None
+    # electricity_price_category: Optional[int] = None
+    # tax: Optional[float] = None
+    # 从ChapterModel添加的字段
+    item_code: Optional[str] = None
+    chapter: Optional[str] = None
+    section: Optional[str] = None
+    project_name: Optional[str] = None
+    unit: Optional[str] = None
+    item_type: Optional[str] = None
+
+    # project_code: Optional[str] = None
+    # material_price_diff_code: Optional[str] = None
+    # summary_method: Optional[str] = None
+    # cumulative_coefficient: Optional[float] = None
+    # display_range: Optional[str] = None
+    # cost_category: Optional[str] = None
+    # auto_calculate_quantity: Optional[bool] = None
+    # comprehensive_display: Optional[bool] = None
+    # list_code: Optional[str] = None
+    # sub_division_feature: Optional[str] = None
+    # quantity_calculation_rule: Optional[str] = None
+    # work_content: Optional[str] = None
+    # note: Optional[str] = None
+    # quota_book_number: Optional[str] = None
+    # quota_section_number: Optional[str] = None
+    # professional_name: Optional[str] = None
+
+    @classmethod
+    def from_model(cls, model) -> "TotalBudgetItemDto":
+        """从数据库模型创建DTO对象"""
+        return cls(
+            budget_id=model.budget_id,
+            item_id=model.item_id,
+            # project_quantity1=model.project_quantity1,
+            # project_quantity2=model.project_quantity2,
+            # budget_value=model.budget_value,
+            # budget_index1=model.budget_index1,
+            # budget_index2=model.budget_index2,
+            # construction_cost=model.construction_cost,
+            # installation_cost=model.installation_cost,
+            # equipment_cost=model.equipment_cost,
+            # other_cost=model.other_cost,
+            # selected_labor_cost=model.selected_labor_cost,
+            # shift_labor_cost=model.shift_labor_cost,
+            # rate_scheme=model.rate_scheme,
+            # formula_code=model.formula_code,
+            # transport_scheme=model.transport_scheme,
+            # transport_unit_price=model.transport_unit_price,
+            # parameter_adjustment=model.parameter_adjustment,
+            # calculation_formula=model.calculation_formula,
+            # unit1=model.unit1,
+            # unit2=model.unit2,
+            # project_quantity1_input=model.project_quantity1_input,
+            # project_quantity2_input=model.project_quantity2_input,
+            # seat_count=model.seat_count,
+            # installation_sub_item=model.installation_sub_item,
+            # cooperation_fee_code=model.cooperation_fee_code,
+            # tax_category=model.tax_category,
+            # tax_rate=model.tax_rate,
+            # bridge_type=model.bridge_type,
+            # electricity_price_category=model.electricity_price_category,
+            # tax=model.tax,
+            # 从ChapterModel添加的字段映射
+            item_code=model.item_code,
+            chapter=model.chapter,
+            section=model.section,
+            project_name=model.project_name,
+            unit=model.unit,
+            item_type=model.item_type,
+            # project_code=model.chapter.project_code if model.chapter else None,
+            # material_price_diff_code=model.chapter.material_price_diff_code if model.chapter else None,
+            # summary_method=model.chapter.summary_method if model.chapter else None,
+            # cumulative_coefficient=model.chapter.cumulative_coefficient if model.chapter else None,
+            # display_range=model.chapter.display_range if model.chapter else None,
+            # cost_category=model.chapter.cost_category if model.chapter else None,
+            # auto_calculate_quantity=model.chapter.auto_calculate_quantity if model.chapter else None,
+            # comprehensive_display=model.chapter.comprehensive_display if model.chapter else None,
+            # list_code=model.chapter.list_code if model.chapter else None,
+            # sub_division_feature=model.chapter.sub_division_feature if model.chapter else None,
+            # quantity_calculation_rule=model.chapter.quantity_calculation_rule if model.chapter else None,
+            # work_content=model.chapter.work_content if model.chapter else None,
+            # note=model.chapter.note if model.chapter else None,
+            # quota_book_number=model.chapter.quota_book_number if model.chapter else None,
+            # quota_section_number=model.chapter.quota_section_number if model.chapter else None,
+            # professional_name=model.chapter.professional_name if model.chapter else None
+        )
+
+    def to_dict(self) -> dict:
+        """转换为字典格式"""
+        return self.model_dump()
+
+    class Config:
+        from_attributes = True

+ 26 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/tree.py

@@ -0,0 +1,26 @@
+
+
+class TreeDto:
+    id: str = None
+    text: str =None
+    children: bool = False
+
+    def __init__(self, node_id: str, parent: str,text: str, children: bool=False,data: dict=None):
+        self.id = node_id
+        self.parent = parent
+        self.text = text
+        self.children = children
+        self.icon = ('fa fa-folder text-primary' if children else 'fa fa-file text-warning') + ' fs-3'
+        self.data = data
+
+
+    def to_dict(self):
+        data = {
+            "id": self.id,
+            "parent": self.parent,
+            "text": self.text,
+            "children": self.children,
+            "icon": self.icon,
+            "data": self.data._asdict() if hasattr(self.data, '_asdict') else self.data
+        }
+        return data

+ 33 - 0
SourceCode/IntelligentRailwayCosting/app/core/dtos/user.py

@@ -0,0 +1,33 @@
+from pydantic import BaseModel
+from typing import Optional
+from ..models import UserModel
+
+class UserDto(BaseModel):
+    """用户信息DTO"""
+    id: Optional[int] = None
+    username: str
+    specialty: Optional[str] = None
+    order_number: Optional[int] = None
+    item_range: Optional[str] = None
+    project_supplement: Optional[int] = None
+    auth_supplement_quota: Optional[int] = None
+
+    @classmethod
+    def from_model(cls, model: UserModel) -> 'UserDto':
+        """从数据库模型创建DTO对象"""
+        return cls(
+            id=model.id,
+            username=model.username,
+            specialty=model.specialty,
+            order_number=model.order_number,
+            item_range=model.item_range,
+            project_supplement=model.project_supplement,
+            auth_supplement_quota=model.auth_supplement_quota
+        )
+
+    def to_dict(self) -> dict:
+        """转换为字典格式"""
+        return self.model_dump()
+
+    class Config:
+        from_attributes = True

+ 4 - 0
SourceCode/IntelligentRailwayCosting/app/core/enum/__init__.py

@@ -0,0 +1,4 @@
+from .log import OperationType,OperationModule,OperationResult
+from .task import TaskStatusEnum,SendStatusEnum
+
+

+ 36 - 0
SourceCode/IntelligentRailwayCosting/app/core/enum/log.py

@@ -0,0 +1,36 @@
+from enum import Enum
+
+class OperationResult(Enum):
+    SUCCESS = 0
+    FAILURE = 1
+
+    @staticmethod
+    def get_name(status: int):
+        return OperationResult.get_dict().get(status, "未知")
+
+    @staticmethod
+    def get_dict() -> dict:
+        """获取状态值和对应的中文名称的字典映射"""
+        return {
+            OperationResult.SUCCESS.value: "成功",
+            OperationResult.FAILURE.value: "失败",
+        }
+
+
+class OperationType(Enum):
+    """操作类型枚举"""
+    LOGIN = "登录"
+    LOGOUT = "注销"
+    CREATE = "新增"
+    UPDATE = "修改"
+    DELETE = "删除"
+    PROCESS_TASK = "开始任务"
+    PROCESS = "处理数据"
+    SEND = "发送数据"
+
+class OperationModule(Enum):
+    """操作模块枚举"""
+    ACCOUNT = "账户"
+    PROJECT = "项目"
+    TASK = "任务"
+    QUOTA = "定额输入"

+ 55 - 0
SourceCode/IntelligentRailwayCosting/app/core/enum/task.py

@@ -0,0 +1,55 @@
+from enum import Enum
+
+
+class TaskStatusEnum(Enum):
+    NEW = 0
+    WAIT = 1
+    PROCESSING = 2
+    SUCCESS = 200
+    FAILURE = 4
+    CANCELED = 5
+    CHANGE = 6
+
+    @classmethod
+    def get_name(cls, status: int):
+        for k, v in cls.get_dict().items():
+            if v == status:
+                return k
+        return None
+
+    @classmethod
+    def get_dict(cls):
+        return {
+            "草稿": cls.NEW,
+            "等待运行": cls.WAIT,
+            "运行中": cls.PROCESSING,
+            "运行成功": cls.SUCCESS,
+            "运行失败": cls.FAILURE,
+            "取消运行": cls.CANCELED,
+            "已修改": cls.CHANGE,
+        }
+
+
+class SendStatusEnum(Enum):
+    NEW = 0
+    PROCESSING = 1
+    SUCCESS = 200
+    FAILURE = 2
+    CHANGE = 3
+
+    @classmethod
+    def get_name(cls, status: int):
+        for k, v in cls.get_dict().items():
+            if v == status:
+                return k
+        return None
+
+    @classmethod
+    def get_dict(cls):
+        return {
+            "未推送": cls.NEW,
+            "推送中": cls.PROCESSING,
+            "推送成功": cls.SUCCESS,
+            "推送失败": cls.FAILURE,
+            "已修改": cls.CHANGE,
+        }

+ 2 - 0
SourceCode/IntelligentRailwayCosting/app/core/log/__init__.py

@@ -0,0 +1,2 @@
+from .log_record import LogRecordHelper
+

+ 93 - 0
SourceCode/IntelligentRailwayCosting/app/core/log/log_record.py

@@ -0,0 +1,93 @@
+from flask import request, session
+from typing import Optional
+from core.enum import OperationType,OperationModule,OperationResult
+
+class LogRecordHelper:
+    _log_service = None
+
+    @staticmethod
+    def get_log_service():
+        if LogRecordHelper._log_service is None:
+            from services import LogService
+            LogRecordHelper._log_service = LogService()
+        return LogRecordHelper._log_service
+
+    @staticmethod
+    def get_client_ip() -> str:
+        """获取客户端IP地址"""
+        if request:
+            if 'X-Forwarded-For' in request.headers:
+                return request.headers['X-Forwarded-For'].split(',')[0]
+            return request.remote_addr
+        return ''
+
+    @staticmethod
+    def log_success(operation_type: OperationType,
+                    operation_module: Optional[OperationModule],
+                    operation_desc: str,
+                    operation_data: Optional[str] = None,
+                    data_changes: Optional[str] = None,
+                    username: Optional[str] = None)-> None:
+        LogRecordHelper.add_log(
+            operation_type=operation_type,
+            operation_desc=operation_desc,
+            operation_module=operation_module,
+            operation_result=OperationResult.SUCCESS,
+            operation_data=operation_data,
+            data_changes=data_changes,
+            username=username
+        )
+
+    @staticmethod
+    def log_fail(operation_type: OperationType,
+                 operation_module: Optional[OperationModule],
+                 operation_desc: str,
+                 operation_data: Optional[str] = None,
+                 data_changes: Optional[str] = None,
+                 username: Optional[str] = None)-> None:
+        LogRecordHelper.add_log(
+            operation_type=operation_type,
+            operation_desc=operation_desc,
+            operation_module=operation_module,
+            operation_result=OperationResult.FAILURE,
+            operation_data=operation_data,
+            data_changes=data_changes,
+            username=username
+        )
+
+    @staticmethod
+    def add_log(operation_type: OperationType,
+                operation_desc: str,
+                operation_module: Optional[OperationModule],
+                operation_result: OperationResult,
+                operation_data: Optional[str] = None,
+                data_changes: Optional[str] = None,
+                username: Optional[str] = None) -> None:
+        """添加操作日志
+
+        Args:
+            operation_type: 操作类型(枚举值)
+            operation_desc: 操作描述
+            operation_module: 操作模块(枚举值,可选)
+            operation_result: 操作结果(0:失败, 1:成功)
+            operation_data: 操作数据(可选)
+            data_changes: 数据变更记录(可选)
+            username: 用户名(可选,默认从session获取)
+        """
+        log_service = LogRecordHelper.get_log_service()
+        log_service.add_operation_log(
+            username=username or session.get('username'),
+            operation_type=operation_type.value,
+            operation_desc=operation_desc,
+            operation_module=operation_module.value if operation_module else None,
+            operation_result=operation_result.value,
+            operation_data=operation_data,
+            operation_ip=LogRecordHelper.get_client_ip(),
+            data_changes=data_changes
+        )
+
+
+
+
+
+

+ 11 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/__init__.py

@@ -0,0 +1,11 @@
+from .log import LogModel
+from .project_quota import ProjectQuotaModel
+from .project_task import ProjectTaskModel
+
+from .user import UserModel
+from .team import TeamModel
+from .project import ProjectModel
+from .total_budget_info import TotalBudgetInfoModel
+from .total_budget_item import TotalBudgetItemModel
+from .chapter import ChapterModel
+from .quota_input import QuotaInputModel

+ 46 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/chapter.py

@@ -0,0 +1,46 @@
+from sqlalchemy import Column, String, Integer, Float, Text, Boolean
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+class ChapterModel(Base):
+    __tablename__ = '章节表'
+
+    item_id = Column('条目序号', Integer, primary_key=True, autoincrement=True)
+    item_code = Column('条目编号', String(255))
+    default_first_page = Column('默认首页', String(1))
+    chapter = Column('章别', String(50))
+    section = Column('节号', String(50))
+    project_name = Column('工程或费用项目名称', String(255))
+    unit = Column('单位', String(20))
+    is_locked = Column('锁定', String(1))
+    unit2 = Column('单位2', String(20))
+    item_type = Column('条目类型', String(50))
+    project_code = Column('工程代码', String(50))
+    cooperation_fee_code = Column('配合费代码', String(50))
+    formula_code = Column('公式代码', String(50))
+    material_price_diff_code = Column('材料价差号', String(50))
+    calculation_formula = Column('计算公式', Text)
+    selected_labor_cost = Column('选用工费', Integer)
+    shift_labor_cost = Column('台班工费', Integer)
+    rate_scheme = Column('费率方案', String(50))
+    transport_scheme = Column('运输方案', String(50))
+    summary_method = Column('汇总方式', String(2))
+    cumulative_coefficient = Column('累计系数', Float)
+    display_range = Column('显示范围', String(50))
+    cost_category = Column('费用类别', String(10))
+    auto_calculate_quantity = Column('自动计算工程量', Boolean)
+    comprehensive_display = Column('综合显示', Boolean)
+    list_code = Column('清单编码', String(255))
+    sub_division_feature = Column('子目划分特征', String(50))
+    quantity_calculation_rule = Column('工程量计算规则', Text)
+    work_content = Column('工作内容', Text)
+    note = Column('附注', Text)
+    seat_count = Column('座数', Integer)
+    quota_book_number = Column('定额书号', String(100))
+    quota_section_number = Column('定额节号', String(100))
+    professional_name = Column('专业名称', String(50))
+    tax_category = Column('税金类别', String(50))
+
+    def __repr__(self):
+        return f"<Chapter(item_id={self.item_id}, item_code='{self.item_code}')>"

+ 27 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/log.py

@@ -0,0 +1,27 @@
+from sqlalchemy import Column, Integer, String, Text, DateTime, Index
+from sqlalchemy.ext.declarative import declarative_base
+from datetime import datetime
+
+Base = declarative_base()
+
+class LogModel(Base):
+    __tablename__ = 'sys_log'
+
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    username = Column(String(255), nullable=False, comment='用户名')
+    operation_type = Column(String(50), nullable=False, comment='操作类型')
+    operation_desc = Column(String(1000), comment='操作描述')
+    operation_result = Column(Integer, comment='操作结果(0:失败, 1:成功)')
+    operation_module = Column(String(100), comment='操作模块')
+    operation_data = Column(Text, comment='操作数据')
+    data_changes = Column(Text, comment='数据变更记录')
+    operation_ip = Column(String(50), comment='操作IP')
+    created_at = Column(DateTime, nullable=False, default=datetime.now, comment='创建时间')
+
+    __table_args__ = (
+        Index('idx_username', 'username'),
+        Index('idx_created_at', 'created_at')
+    )
+
+    def __repr__(self):
+        return f"<Log(id='{self.id}', username='{self.username}', operation_type='{self.operation_type}')"

+ 43 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/project.py

@@ -0,0 +1,43 @@
+from sqlalchemy import Column, String, Integer, Float, DateTime, Boolean, Text
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+class ProjectModel(Base):
+    __tablename__ = '项目信息'
+
+    project_id = Column('项目编号', String(30), primary_key=True)
+    compilation_method = Column('编制办法文号', String(50))
+    project_name = Column('建设项目名称', String(255))
+    short_name = Column('简称', String(10))
+    design_stage = Column('设计阶段', String(50))
+    compilation_scope = Column('编制范围', String(255))
+    total_engineering = Column('工程总量', Float)
+    unit = Column('单位', String(20))
+    project_manager = Column('项目负责人', String(20))
+    total_budget = Column('概算总值', Float)
+    budget_index = Column('概算指标', Float)
+    standard_quota = Column('标准定额应用', Text)
+    train_transport_standard = Column('火车运输标准', String(50))
+    project_version = Column('项目版本号', String(50))
+    create_time = Column('创建时间', DateTime)
+    material_library = Column('材料库', String(50))
+    work_shift_library = Column('台班库', String(50))
+    equipment_library = Column('设备库', String(50))
+    review_status = Column('审查状态', Integer)
+    years_to_construction = Column('编制年至开工年年限', Integer)
+    project_password = Column('项目密码', String(10))
+    railway_grade = Column('铁路等级', String(10))
+    main_line_count = Column('正线数目', Integer)
+    traction_type = Column('牵引种类', String(10))
+    blocking_mode = Column('闭塞方式', String(10))
+    # station_count = Column('车站数量', String(50))
+    project_description = Column('项目简介', Text)
+    target_speed = Column('速度目标值', Integer)
+    print_compilation_review = Column('打印编制复核', Boolean)
+    project_type = Column('项目类型', String(20))
+    unit_conversion = Column('单位换算', Boolean)
+    # completion_status = Column('完成状态', String(10))
+
+    def __repr__(self):
+        return f"<Project(project_id='{self.project_id}', project_name='{self.project_name}')>"

+ 62 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/project_quota.py

@@ -0,0 +1,62 @@
+from sqlalchemy import Column, Integer, String, DateTime, Float
+from sqlalchemy.sql import func
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class ProjectQuotaModel(Base):
+    __tablename__ = "project_quota"
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    task_id = Column(Integer, nullable=False, comment="任务编号")
+    project_id = Column(String(50), nullable=False, comment="项目编号")
+    budget_id = Column(Integer, nullable=False, comment="概算序号")
+    budget_code = Column(String(50), comment="概算编号")
+    item_id = Column(Integer, nullable=False, comment="条目序号")
+    item_code = Column(String(255), nullable=False, comment="条目编号")
+    quota_id = Column(Integer, nullable=False, default=0, comment="定额序号")
+    quota_code = Column(String(50), comment="定额编号")
+    quota_adjustment = Column(String(50), comment="定额调整")
+    entry_name = Column(String(255), comment="工程或费用项目名称")
+    units = Column(String(20), comment="单位")
+    amount = Column(Float, comment="数量")
+    ex_file = Column(String(5000), comment="excel⽂件")
+    ex_cell = Column(String(50), comment='数量单元格位置,例如"C17"')
+    ex_row = Column(String(5000), comment="该⾏内容,由逗号连接多个单元格得到")
+    ex_unit = Column(String(50), comment="excel中给出的单位")
+    ex_amount = Column(Float, comment="excel中给出的数量")
+    is_change = Column(
+        Integer,
+        nullable=False,
+        default=1,
+        comment="是否变更(0:未变更,1:已变更)",
+    )
+    send_status = Column(
+        Integer,
+        nullable=False,
+        default=0,
+        comment="发送状态(0:未发送,1:发送中 ,2:已发送, 3:发送失败)",
+    )
+    send_time = Column(DateTime, comment="发送时间")
+    send_error = Column(String(5000), comment="发送错误信息")
+    is_del = Column(Integer, nullable=False, default=0, comment="是否删除(0:否, 1:是)")
+    deleted_by = Column(String(50), comment="删除人")
+    deleted_at = Column(DateTime, comment="删除时间")
+    created_by = Column(String(50), comment="创建人")
+    created_at = Column(
+        DateTime,
+        nullable=False,
+        server_default=func.current_timestamp(),
+        comment="创建时间",
+    )
+    updated_by = Column(String(50), comment="更新人")
+    updated_at = Column(
+        DateTime,
+        nullable=False,
+        server_default=func.current_timestamp(),
+        server_onupdate=func.current_timestamp(),
+        comment="更新时间",
+    )
+
+    def __repr__(self):
+        return f"<ProjectQuota(id='{self.id}', project_id='{self.project_id}')>"

+ 34 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/project_task.py

@@ -0,0 +1,34 @@
+from sqlalchemy import Column, Integer, String, DateTime,  Text
+from sqlalchemy.sql import func
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+class ProjectTaskModel(Base):
+    __tablename__ = 'project_task'
+
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    task_name = Column(String(255), nullable=False, comment='任务名称')
+    task_sort = Column(Integer, nullable=False, default=0, comment='任务排序')
+    task_desc = Column(String(1000), comment='任务描述')
+    is_cover = Column(Integer, nullable=False, default=0, comment='是否覆盖(0:不覆盖, 1:覆盖)')
+    project_id = Column(String(50), nullable=False, comment='项目编号')
+    budget_id = Column(Integer, comment='概算序号')
+    item_id = Column(Integer, nullable=False, comment='条目序号')
+    item_code = Column(String(255), nullable=False, comment='条目编号')
+    file_path = Column(Text, comment='文件路径')
+    process_status = Column(Integer, nullable=False, default=0, comment='处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)')
+    process_time = Column(DateTime, comment='处理时间')
+    process_error = Column(String(5000), comment='处理错误信息')
+    send_status = Column(Integer, nullable=False, default=0, comment='发送状态(0:未发送,1:发送中 ,2:已发送, 3:发送失败)')
+    send_time = Column(DateTime, comment='发送时间')
+    send_error = Column(String(5000), comment='发送错误信息')
+    is_del = Column(Integer, nullable=False, default=0, comment='是否删除(0:否, 1:是)')
+    deleted_by = Column(String(50), comment='删除人')
+    deleted_at = Column(DateTime, comment='删除时间')
+    created_by = Column(String(50), comment='创建人')
+    created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), comment='创建时间')
+    updated_by = Column(String(50), comment='更新人')
+    updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), server_onupdate=func.current_timestamp(), comment='更新时间')
+
+    def __repr__(self):
+        return f"<ProjectTask(id='{self.id}', task_name='{self.task_name}')>"

+ 60 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/quota_input.py

@@ -0,0 +1,60 @@
+from sqlalchemy import Column, String, Integer, Float, Text
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class QuotaInputModel(Base):
+    __tablename__ = "定额输入"
+
+    quota_id = Column("定额序号", Integer, primary_key=True, autoincrement=True)
+    # budget_id = Column('总概算序号', Integer, ForeignKey('总概算信息.总概算序号'), nullable=False)
+    # item_id = Column('条目序号', Integer, ForeignKey('章节表.条目序号'), nullable=False)
+    budget_id = Column("总概算序号", Integer, nullable=False)
+    item_id = Column("条目序号", Integer, nullable=False)
+    quota_code = Column("定额编号", String(255), nullable=False)
+    sequence_number = Column("顺号", Integer)
+    project_name = Column("工程或费用项目名称", String(255))
+    unit = Column("单位", String(20))
+    project_quantity = Column("工程数量", Float)
+    project_quantity_input = Column("工程数量输入", Text)
+    quota_adjustment = Column("定额调整", Text)
+    unit_price = Column("单价", Float)
+    compilation_unit_price = Column("编制期单价", Float)
+    total_price = Column("合价", Float)
+    compilation_total_price = Column("编制期合价", Float)
+    unit_weight = Column("单重", Float)
+    total_weight = Column("合重", Float)
+    labor_cost = Column("人工费", Float)
+    compilation_labor_cost = Column("编制期人工费", Float)
+    material_cost = Column("材料费", Float)
+    compilation_material_cost = Column("编制期材料费", Float)
+    deduct_material_cost = Column("扣料费", Float)
+    compilation_deduct_material_cost = Column("编制期扣料费", Float)
+    mechanical_cost = Column("机械使用费", Float)
+    compilation_mechanical_cost = Column("编制期机械使用费", Float)
+    equipment_cost = Column("设备费", Float)
+    compilation_equipment_cost = Column("编制期设备费", Float)
+    transport_cost = Column("运杂费", Float)
+    compilation_transport_cost = Column("编制期运杂费", Float)
+    quota_workday = Column("定额工日", Float)
+    total_workday = Column("工日合计", Float)
+    workday_salary = Column("工日工资", Float)
+    compilation_workday_salary = Column("编制期工日工资", Float)
+    quota_mechanical_workday = Column("定额机械工日", Float)
+    total_mechanical_workday = Column("机械工合计", Float)
+    mechanical_workday_salary = Column("机械工日工资", Float)
+    compilation_mechanical_workday_salary = Column("编制期机械工日工资", Float)
+    compiler = Column("编制人", String(50))
+    modify_date = Column("修改日期", String(50))
+    quota_consumption = Column("定额消耗", Text)
+    basic_quota = Column("基本定额", String(255))
+
+    # quota_comprehensive_unit_price = Column('定额综合单价', Float)
+    # quota_comprehensive_total_price = Column('定额综合合价', Float)
+
+    # budget_info = relationship('TotalBudgetInfoModel')
+    # chapter = relationship('ChapterModel')
+
+    def __repr__(self):
+        return f"<QuotaInput(quota_id={self.quota_id}, quota_code='{self.quota_code}')>"

+ 16 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/team.py

@@ -0,0 +1,16 @@
+from sqlalchemy import Column, String, Integer, Text
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+class TeamModel(Base):
+    __tablename__ = '团队人员'
+
+    project_id = Column('项目编号', String(30), primary_key=True)
+    name = Column('姓名', String(50), primary_key=True)
+    operation_permission = Column('操作权限', String(2000))
+    item_code = Column('条目编号', Text)
+    compilation_status = Column('编制状态', Integer)
+
+    def __repr__(self):
+        return f"<Team(project_id='{self.project_id}', name='{self.name}')>"

+ 40 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/total_budget_info.py

@@ -0,0 +1,40 @@
+from sqlalchemy import Column, String, Integer, Float, Boolean
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class TotalBudgetInfoModel(Base):
+    __tablename__ = "总概算信息"
+
+    budget_id = Column("总概算序号", Integer, primary_key=True, autoincrement=True)
+    budget_code = Column("总概算编号", String(50), nullable=False)
+    compilation_scope = Column("编制范围", String(255))
+    project_quantity = Column("工程数量", Float, nullable=False)
+    unit = Column("单位", String(20), nullable=False)
+    budget_value = Column("概算价值", Float)
+    budget_index = Column("概算指标", Float)
+    price_diff_coefficient = Column("价差系数", String(50), nullable=False)
+    price_diff_area = Column("价差区号", String(20), nullable=False)
+    ending_scheme = Column("结尾方案", String(50), nullable=False)
+    material_cost_scheme = Column("材料费方案", String(50), nullable=False)
+    mechanical_cost_scheme = Column("机械费方案", String(50), nullable=False)
+    equipment_cost_scheme = Column("设备费方案", String(50), nullable=False)
+    labor_cost_scheme = Column("工费方案", String(50), nullable=False)
+    compilation_status = Column("编制状态", Integer, nullable=False)
+    train_interference_count = Column("行车干扰次数", Integer)
+    train_interference_10_count = Column("行干10号工次数", Integer)
+    deduct_supplied_materials = Column("扣甲供料", Integer)
+    auto_calculate_quantity = Column("是否自动计算工程量", Boolean)
+    mechanical_depreciation_adjustment = Column("机械折旧费调差系数", Float)
+    construction_supervision_group = Column("施工监理分组", Integer)
+    construction_management_group = Column("建设管理分组", Integer)
+    survey_group = Column("勘察分组", Integer)
+    design_group = Column("设计分组", Integer)
+    compilation_scope_group = Column("编制范围分组", Integer)
+    enable_total_budget_group = Column("启用总概算分组", Integer)
+
+    # items = relationship('TotalBudgetItemModel', back_populates='budget_info', lazy='dynamic')
+
+    def __repr__(self):
+        return f"<TotalBudgetInfo(budget_id={self.budget_id}, budget_code='{self.budget_code}')>"

+ 48 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/total_budget_item.py

@@ -0,0 +1,48 @@
+from sqlalchemy import Column, String, Integer, Float, Text
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+
+class TotalBudgetItemModel(Base):
+    __tablename__ = "总概算条目"
+
+    # budget_id = Column('总概算序号', Integer, ForeignKey('总概算信息.总概算序号'), primary_key=True)
+    # item_id = Column('条目序号', Integer, ForeignKey('章节表.条目序号'), primary_key=True)
+    budget_id = Column("总概算序号", Integer, primary_key=True)
+    item_id = Column("条目序号", Integer, primary_key=True)
+    project_quantity1 = Column("工程数量1", Float)
+    project_quantity2 = Column("工程数量2", Float)
+    budget_value = Column("概算价值", Float)
+    budget_index1 = Column("概算指标1", Float)
+    budget_index2 = Column("概算指标2", Float)
+    construction_cost = Column("建筑工程费", Float)
+    installation_cost = Column("安装工程费", Float)
+    equipment_cost = Column("设备工器具", Float)
+    other_cost = Column("其他费", Float)
+    selected_labor_cost = Column("选用工费", Integer)
+    shift_labor_cost = Column("台班工费", Integer)
+    rate_scheme = Column("费率方案", String(50))
+    formula_code = Column("公式代码", String(50))
+    transport_scheme = Column("运输方案", String(50))
+    transport_unit_price = Column("运输单价", Float)
+    parameter_adjustment = Column("参数调整", Text)
+    calculation_formula = Column("计算公式", Text)
+    unit1 = Column("单位1", String(20))
+    unit2 = Column("单位2", String(20))
+    project_quantity1_input = Column("工程数量1输入", String(100))
+    project_quantity2_input = Column("工程数量2输入", String(100))
+    seat_count = Column("座数", Integer)
+    installation_sub_item = Column("安装子目", String(500))
+    cooperation_fee_code = Column("配合费代码", String(50))
+    tax_category = Column("税金类别", String(50))
+    tax_rate = Column("税率", Float)
+    bridge_type = Column("桥梁类型", String(50))
+    electricity_price_category = Column("电价分类", Integer)
+    tax = Column("税金", Float)
+
+    # budget_info = relationship('TotalBudgetInfoModel', back_populates='items')
+    # chapter = relationship('ChapterModel')
+
+    def __repr__(self):
+        return f"<TotalBudgetItem(budget_id={self.budget_id}, item_id={self.item_id})>"

+ 19 - 0
SourceCode/IntelligentRailwayCosting/app/core/models/user.py

@@ -0,0 +1,19 @@
+from sqlalchemy import Column, Integer, String, Text
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+class UserModel(Base):
+    __tablename__ = '系统用户'
+
+    id = Column('序号', Integer, primary_key=True)
+    order_number = Column('顺号', Integer, nullable=False)
+    username = Column('用户名称', String(20))
+    password = Column('用户密码', String(20))
+    specialty = Column('专业名称', String(50))
+    auth_supplement_quota = Column('授权补充定额', Integer)
+    item_range = Column('条目范围30', Text)
+    project_supplement = Column('项目补充', Integer)
+
+    def __repr__(self):
+        return f"<User(username='{self.username}', specialty='{self.specialty}')"

+ 4 - 0
SourceCode/IntelligentRailwayCosting/app/core/user_session/__init__.py

@@ -0,0 +1,4 @@
+from .user_session import UserSession
+from .current_user import CurrentUser
+from .permission import Permission
+

+ 66 - 0
SourceCode/IntelligentRailwayCosting/app/core/user_session/current_user.py

@@ -0,0 +1,66 @@
+from dataclasses import dataclass
+from typing import Optional
+from flask_login import UserMixin
+
+
+@dataclass
+class CurrentUser(UserMixin):
+    """当前用户信息结构体"""
+
+    _user_id: Optional[int] = None
+    _username: Optional[str] = None
+    _item_range: Optional[str] = None
+    _specialty: Optional[str] = None
+
+    # _auth_supplement_quota: Optional[str] = None
+    # _project_supplement: Optional[str] = None
+
+    def __init__(
+        self,
+        user_id: Optional[int] = None,
+        username: Optional[str] = None,
+        item_range: Optional[str] = None,
+        specialty: Optional[str] = None,
+    ):
+        self._user_id = user_id
+        self._username = username
+        self._item_range = item_range
+        self._specialty = specialty
+
+    def get_id(self):
+        return self.user_id
+
+    @property
+    def user_id(self):
+        """实现Flask-Login要求的get_id方法"""
+        return str(self._user_id) if self._user_id else None
+
+    @property
+    def username(self):
+        return self._username
+
+    @property
+    def item_range(self):
+        return self._item_range
+
+    @property
+    def specialty(self):
+        return self._specialty
+
+    @property
+    def is_authenticated(self) -> bool:
+        """检查用户是否已认证
+
+        Returns:
+            bool: 如果用户已认证返回True,否则返回False
+        """
+        return self.user_id is not None and self.username is not None
+
+    @property
+    def is_admin(self) -> bool:
+        """检查用户是否为超级管理员
+
+        Returns:
+            bool: 如果用户是超级管理员返回True,否则返回False
+        """
+        return self.username == "admin"

+ 26 - 0
SourceCode/IntelligentRailwayCosting/app/core/user_session/permission.py

@@ -0,0 +1,26 @@
+from flask import redirect, url_for, request
+from functools import wraps
+from .user_session import UserSession
+
+class Permission:
+    @staticmethod
+    def authorize(f):
+        @wraps(f)
+        def decorated_function(*args, **kwargs):
+            current_user = UserSession.get_current_user()
+            if not current_user.is_authenticated:
+                return redirect(url_for('auth.login', next=request.url))
+            return f(*args, **kwargs)
+
+        return decorated_function
+
+    # @staticmethod
+    # def edit(f):
+    #     @wraps(f)
+    #     def decorated_function(*args, **kwargs):
+    #         current_user = UserSession.get_current_user()
+    #         if not current_user.is_authenticated:
+    #             return redirect(url_for('auth.login', next=request.url))
+    #         if not current_user.is_admin:
+    #             return redirect(url_for('auth.login', next=request.url))
+    #         return f(*args, **kwargs)

+ 90 - 0
SourceCode/IntelligentRailwayCosting/app/core/user_session/user_session.py

@@ -0,0 +1,90 @@
+from flask import session, has_request_context
+from typing import Optional
+from .current_user import CurrentUser
+from core.dtos import UserDto
+
+
+class UserSession:
+    """用户会话管理类"""
+
+    @staticmethod
+    def set_user(user: UserDto) -> None:
+        session['user_id'] = user.id
+        session['username'] = user.username
+        session['item_range'] = user.item_range
+        session['specialty'] = user.specialty
+
+    @staticmethod
+    def get_current_user() -> CurrentUser:
+        """获取当前登录用户信息
+
+        Returns:
+            CurrentUser: 返回当前用户信息结构体
+        """
+        if not has_request_context():
+            return CurrentUser(
+                user_id=None,
+                username=None,
+                item_range=None,
+                specialty=None
+            )
+        return CurrentUser(
+            user_id=session.get('user_id'),
+            username=session.get('username'),
+            item_range=session.get('item_range'),
+            specialty=session.get('specialty'),
+            )
+
+    @staticmethod
+    def get_current_username() -> Optional[str]:
+        """获取当前登录用户名
+
+        Returns:
+            Optional[str]: 返回用户名,未登录则返回None
+        """
+        return session.get('username')
+    
+    @staticmethod
+    def get_current_user_id() -> Optional[int]:
+        """获取当前登录用户ID
+
+        Returns:
+            Optional[int]: 返回用户ID,未登录则返回None
+        """
+        return session.get('user_id')
+    
+    @staticmethod
+    def get_current_name() -> Optional[str]:
+        """获取当前登录用户姓名
+
+        Returns:
+            Optional[str]: 返回用户姓名,未登录则返回None
+        """
+        return session.get('name')
+
+    
+    @staticmethod
+    def clear_user() -> None:
+        """清除用户登录状态"""
+        session.pop('user_id', None)
+        session.pop('username', None)
+        session.pop('user', None)
+
+    @staticmethod
+    def is_logged_in() -> bool:
+        """检查用户是否已登录
+
+        Returns:
+            bool: 如果用户已登录返回True,否则返回False
+        """
+        return 'user_id' in session and 'username' in session
+    
+    @staticmethod
+    def is_admin() -> bool:
+        """检查当前用户是否为管理员
+
+        Returns:
+            bool: 如果当前用户是管理员返回True,否则返回False
+        """
+        return session.get('username') == 'admin'
+    

+ 37 - 0
SourceCode/IntelligentRailwayCosting/app/executor/__init__.py

@@ -0,0 +1,37 @@
+from .task_runner import TaskRunner
+from .task_sender import TaskSender
+from core.dtos import ProjectTaskDto, ProjectQuotaDto
+
+
+def init():
+    TaskRunner.init()
+
+
+def run_task(task: ProjectTaskDto):
+    return TaskRunner.run_task(task)
+
+
+def cancel_task(task: ProjectTaskDto):
+    return TaskRunner.cancel(task)
+
+
+def project_is_running(project_id: str):
+    return TaskRunner.get_project_running_state(project_id)
+
+
+def send_task(task: ProjectTaskDto):
+    return TaskSender().send_task(task)
+
+
+def send_quota(quota: ProjectQuotaDto):
+    return TaskSender().send_quota(quota)
+
+
+__all__ = [
+    "init",
+    "run_task",
+    "cancel_task",
+    "send_task",
+    "send_quota",
+    "project_is_running",
+]

+ 301 - 0
SourceCode/IntelligentRailwayCosting/app/executor/task_processor.py

@@ -0,0 +1,301 @@
+import requests, tools.utils as utils, core.configs as configs
+from core.dtos import (
+    ProjectDto,
+    ProjectTaskDto,
+    ProjectQuotaDto,
+    ChapterDto,
+    TotalBudgetInfoDto,
+    ExcelParseResultDataDto,
+)
+from core.enum import TaskStatusEnum
+from stores import (
+    ProjectStore,
+    ProjectTaskStore,
+    ProjectQuotaStore,
+    ChapterStore,
+    BudgetStore,
+)
+from core.dtos import (
+    ExcelParseDto,
+    ExcelParseZgsDto,
+    ExcelParseItemDto,
+    ExcelParseFileDto,
+    ExcelParseResultDto,
+)
+
+
+class TaskProcessor:
+    def __init__(self):
+        self._logger = utils.get_logger()
+        self._project_store = ProjectStore()
+        self._budget_store = BudgetStore()
+        self._chapter_store = ChapterStore()
+        self._task_store = ProjectTaskStore()
+        self._quota_store = ProjectQuotaStore()
+        self._task_submit_url = "/task_submit"
+        self._task_status_url = "/task_status"
+        self._task_cancel_url = "/cancel_task"
+
+    def submit_task(self, task: ProjectTaskDto):
+        try:
+            self._logger.info(f"开始运行任务:{task.task_name}")
+            self._task_store.update_task_status(
+                task.id, TaskStatusEnum.PROCESSING.value
+            )
+            if not task.file_path:
+                raise Exception("任务文件不存在")
+            files, msg = self._read_files(task.file_path)
+            if not files or len(files) == 0:
+                raise Exception(msg)
+            project = self._project_store.get(task.project_id)
+            if not project:
+                raise Exception("项目不存在")
+            budget_models = self._budget_store.get_budget_info(task.project_id)
+            budgets = [
+                TotalBudgetInfoDto.from_model(budget) for budget in budget_models
+            ]
+            chapter = self._chapter_store.get_chapter_item_by_item_code(
+                task.project_id, task.item_code
+            )
+            # parents = self._chapter_store.get_all_parents_chapter_items(
+            #     task.project_id, task.item_code
+            # )
+            # children = self._chapter_store.get_all_children_chapter_items(
+            #     task.project_id, task.item_code
+            # )
+            # data, msg = self._build_api_body(
+            #     task, project, budgets, parents, children, files
+            # )
+            data, msg = self._build_api_body(task, project, budgets, chapter, files)
+            if not data:
+                raise Exception(msg)
+            res = self._call_api(self._task_submit_url, data)
+
+            if res.result == -1:
+                self._task_store.update_task_status(
+                    task.id, TaskStatusEnum.FAILURE.value, res.reason
+                )
+                return res.reason
+
+            if res.data and len(res.data) > 0:
+                self._insert_data(task, project, res.data)
+            if res.result == 1:
+                self._logger.debug(f"运行任务:{task.task_name}完成")
+                self._task_store.update_task_status(
+                    task.id, TaskStatusEnum.SUCCESS.value
+                )
+            else:
+                self._logger.debug(f"运行任务:{task.task_name}请求中,等待结果")
+                self.query_task(task, project)
+            return None
+        except Exception as e:
+            msg = f"任务运行失败,原因:{e}"
+            self._logger.error(f"运行任务:{task.task_name}, {msg}")
+            self._task_store.update_task_status(
+                task.id, TaskStatusEnum.FAILURE.value, msg
+            )
+            return msg
+
+    def cancel_task(self, task: ProjectTaskDto):
+        try:
+            self._logger.info(f"开始取消运行任务:{task.id}")
+            if task.process_status == TaskStatusEnum.PROCESSING.value:
+                res = self._call_api(self._task_cancel_url, {"task_id": task.id})
+                if res.result == -1:
+                    self._task_store.update_task_status(
+                        task.id, TaskStatusEnum.FAILURE.value, res.reason
+                    )
+                    return res.reason
+                project = self._project_store.get(task.project_id)
+                if not project:
+                    self._logger.error(f"取消运行任务:{task.id}失败,原因:项目不存在")
+                    return "项目不存在"
+                if res.data and len(res.data) > 0:
+                    self._insert_data(task, project, res.data)
+                if res.result == 0:
+                    self._logger.info(f"取消运行任务:{task.id}成功")
+                    # self._task_store.update_task_status(task.id, TaskStatusEnum.CANCELED.value)
+                    return None
+                elif res.result == 1:
+                    self._logger.error(f"取消运行任务失败:{task.id}已运行完成")
+                    self._task_store.update_task_status(
+                        task.id, TaskStatusEnum.SUCCESS.value
+                    )
+                    return "取消失败,任务已运行完成"
+            else:
+                self._logger.error(f"取消运行任务:{task.id}失败,原因:任务状态错误")
+                return "任务状态错误"
+        except Exception as e:
+            msg = f"取消运行任务失败,原因:{e}"
+            self._logger.error(msg)
+            return msg
+
+    def query_task(self, task: ProjectTaskDto, project: ProjectDto = None):
+        try:
+            import time
+
+            while True:
+                time.sleep(30)
+                res = self._call_api(self._task_status_url, {"task_id": task.id})
+                if res.result == -1:
+                    self._task_store.update_task_status(
+                        task.id, TaskStatusEnum.FAILURE.value, res.reason
+                    )
+                    return res.reason
+                if res.data and len(res.data) > 0:
+                    if not project:
+                        project = self._project_store.get(task.project_id)
+                    self._insert_data(task, project, res.data)
+                if res.result == 1:
+                    self._logger.debug(f"运行任务:{task.task_name}完成")
+                    self._task_store.update_task_status(
+                        task.id, TaskStatusEnum.SUCCESS.value
+                    )
+                    break
+                else:
+                    self._logger.debug(f"运行任务:{task.task_name}请求中,等待结果")
+                    self.query_task(task, project)
+        except Exception as e:
+            msg = f"任务状态查询失败,原因:{e}"
+            self._logger.error(msg)
+            raise Exception(msg)
+
+    def _read_files(self, paths: str) -> (list[ExcelParseFileDto], str):
+        try:
+            files = []
+            self._logger.debug(f"开始读取文件:{paths}")
+            path_list = paths.split(",")
+            for path in path_list:
+                file = utils.encode_file(path)
+                files.append(ExcelParseFileDto(file_id=path, content=file))
+            self._logger.debug(f"读取文件完成:{paths}")
+            return files, ""
+        except Exception as e:
+            msg = f"读取文件失败,原因:{e}"
+            self._logger.error(msg)
+            return None, msg
+
+    def _build_api_body(
+        self,
+        task: ProjectTaskDto,
+        project: ProjectDto,
+        budgets: list[TotalBudgetInfoDto],
+        chapter: ChapterDto,
+        # parents: list[ChapterDto],
+        # children: list[ChapterDto],
+        files: list[ExcelParseFileDto],
+    ):
+        try:
+            budgets_data = [ExcelParseZgsDto.from_dto(budget) for budget in budgets]
+            # parents_data = [ExcelParseItemDto.from_dto(parent) for parent in parents]
+            # children_data = [ExcelParseItemDto.from_dto(child) for child in children]
+            chapter_data = ExcelParseItemDto.from_dto(chapter)
+            data = ExcelParseDto(
+                task_id=task.id or 0,
+                version=configs.app.version or "2020",
+                project_id=task.project_id,
+                project_name=project.project_name,
+                project_stage=project.design_stage,
+                selected_zgs_id=task.budget_id or 0,
+                zgs_list=budgets_data,
+                selected_chapter=chapter_data,
+                # hierarchy=parents_data,
+                # components=children_data,
+                files=files,
+            )
+            return data, ""
+        except Exception as e:
+            msg = f"构建API BODY,原因:{e}"
+            self._logger.error(msg)
+            return None, msg
+
+    def _call_api(self, api_url, data) -> ExcelParseResultDto:
+        try:
+            url = f"{configs.app.task_api_url}{api_url}"
+            if isinstance(data, ExcelParseDto):
+                data = data.to_dict()
+            self._logger.debug(f"调用接口:{url},data:{data}")
+            response = requests.post(
+                url, headers={"Content-Type": "application/json"}, json=data
+            )
+            self._logger.debug(f"调用接口返回[{response.status_code}]:{response.text}")
+            if response.status_code == 200:
+                result = response.json()
+                result_dto = ExcelParseResultDto.from_dict(result)
+                self._logger.debug(f"调用接口成功")
+                return result_dto
+            else:
+                self._logger.error("调用接口失败")
+                raise Exception(response.text)
+        except Exception as e:
+            msg = f"调用接口失败,原因:{e}"
+            self._logger.error(msg)
+            raise Exception(msg)
+
+    def _insert_data(
+        self,
+        task: ProjectTaskDto,
+        project: ProjectDto,
+        data: list[ExcelParseResultDataDto],
+    ):
+        try:
+            self._logger.debug(f"开始插入数据:{task.task_name}")
+            for item in data:
+                self._logger.debug(f"数据:{item.to_dict()}")
+                quota_dto = (
+                    self._quota_store.get_quota_by_quota_input(
+                        project.project_id, task.budget_id, item.target_id
+                    )
+                    if item.target_id > 0
+                    else None
+                )
+                if quota_dto:
+                    self._logger.debug(
+                        f"更新数据[{item.target_id}]:{item.item_id}/{item.item_code} {item.dinge_code}"
+                    )
+                    quota_dto.item_code = item.item_code
+                    quota_dto.item_id = item.item_id
+                    quota_dto.quota_id = item.target_id if item.target_id > 0 else 0
+                    quota_dto.quota_code = item.dinge_code
+                    quota_dto.quota_adjustment = item.dinge_adjust
+                    quota_dto.entry_name = item.entry_name
+                    quota_dto.units = item.units
+                    quota_dto.amount = item.amount
+                    quota_dto.ex_file = item.ex_file_id
+                    quota_dto.ex_cell = item.ex_cell
+                    quota_dto.ex_row = item.ex_row
+                    quota_dto.ex_unit = item.ex_unit
+                    quota_dto.ex_amount = item.ex_amount
+                    quota_dto.send_error = None
+                    self._quota_store.update_quota(quota_dto)
+                else:
+                    self._logger.debug(
+                        f"新增数据[{item.target_id}]:{item.item_id}/{item.item_code} {item.dinge_code}"
+                    )
+                    quota_dto = ProjectQuotaDto(
+                        task_id=task.id,
+                        budget_id=item.zgs_id,
+                        budget_code=item.zgs_code,
+                        project_id=project.project_id,
+                        item_code=item.item_code,
+                        item_id=item.item_id,
+                        quota_id=item.target_id if item.target_id > 0 else 0,
+                        quota_code=item.dinge_code,
+                        quota_adjustment=item.dinge_adjust,
+                        entry_name=item.entry_name,
+                        units=item.units,
+                        amount=item.amount,
+                        ex_file=item.ex_file_id,
+                        ex_cell=item.ex_cell,
+                        ex_row=item.ex_row,
+                        ex_unit=item.ex_unit,
+                        ex_amount=item.ex_amount,
+                        created_by=task.created_by,
+                    )
+                    self._quota_store.create_quota(quota_dto)
+            self._logger.debug(f"插入数据完成:{task.task_name}")
+            return True
+        except Exception as e:
+            msg = f"插入数据失败,原因:{e}"
+            self._logger.error(msg)
+            return False, msg

+ 290 - 0
SourceCode/IntelligentRailwayCosting/app/executor/task_runner.py

@@ -0,0 +1,290 @@
+from time import sleep
+import threading
+
+import tools.utils as utils, core.configs as configs
+from core.dtos import ProjectTaskDto
+from core.enum import TaskStatusEnum
+from executor.task_processor import TaskProcessor
+from stores import ProjectStore, ProjectTaskStore
+
+
+class TaskRunner:
+    _is_running = {}
+    _task_wait_list = {}
+    _process_task_wait_list = {}
+    _running_projects = set()
+    _max_concurrent_projects = configs.app.task_max_projects_count
+    _task_sleep_interval = configs.app.task_interval
+    _logger = utils.get_logger()
+    _project_store = ProjectStore()
+    _task_store = ProjectTaskStore()
+    _lock = threading.Lock()
+    _task_processor = None
+    _task_empty_wait_count = 0
+    _task_empty_wait_max_count = 5
+
+    @classmethod
+    def run_task(cls, task: ProjectTaskDto):
+        project_id = task.project_id
+        # 检查项目是否已有任务在运行
+        if project_id in cls._is_running and cls._is_running[project_id]:
+            cls._add_task_to_wait_list(
+                task,
+                f"项目已有任务在运行[{task.project_id}],任务[{task.task_name}]将等待",
+            )
+            return
+
+        # 如果项目没有任务在运行,但并发项目数已满且该项目不在运行集合中
+        if (
+            len(cls._running_projects) >= cls._max_concurrent_projects
+            and project_id not in cls._running_projects
+        ):
+            cls._add_task_to_wait_list(
+                task,
+                f"项目运行队列[{cls._max_concurrent_projects}]已满,任务[{task.task_name}]将等待",
+            )
+            return
+
+        # 可以执行的新任务
+        cls._add_task_to_wait_list(
+            task, f"添加到待运行队列[{task.project_id}]:{task.task_name}"
+        )
+        cls._execute_project_tasks(project_id)
+        return
+
+    @classmethod
+    def cancel(cls, task: ProjectTaskDto):
+        try:
+            cls._logger.info(f"开始取消运行任务[{task.project_id}]:{task.id}")
+            if task.process_status == TaskStatusEnum.PROCESSING.value:
+                task_processor = cls._get_task_processor()
+                result = task_processor.cancel_task(task)
+                return result
+            elif task.process_status == TaskStatusEnum.WAIT.value:
+                cls._task_wait_list[task.project_id].remove(task)
+                cls._logger.info(f"取消运行任务[{task.project_id}]:{task.id}成功")
+            else:
+                cls._logger.info(
+                    f"取消运行任务[{task.project_id}]:{task.id}失败,原因:任务状态错误"
+                )
+                return "任务状态错误"
+            return None
+        except Exception as e:
+            msg = f"取消运行任务失败[{task.project_id}],原因:{e}"
+            cls._logger.error(msg)
+            return msg
+
+    @classmethod
+    def get_project_running_state(cls, project_id: str) -> bool:
+        return project_id in cls._is_running and cls._is_running[project_id]
+
+    @classmethod
+    def init(cls):
+        running_tasks = cls._task_store.get_tasks_by_status(
+            TaskStatusEnum.PROCESSING.value
+        )
+        cls._logger.info(
+            f"初始化正在运行的任务。正在运行中的任务数量:[{len(running_tasks)}]"
+        )
+        if len(running_tasks) > 0:
+            for task in running_tasks:
+                cls._execute_processing_task(task)
+        cls._logger.info(f"初始化正在运行的任务完成。")
+        cls._sync_wait_list()
+
+    @classmethod
+    def _run(cls):
+        if len(cls._running_projects) >= cls._max_concurrent_projects:
+            cls._logger.info(f"【运行等待队列中的任务】: 项目运行队列已满。")
+            return
+        cls._logger.info(f"【运行等待队列中的任务】")
+        if cls._task_wait_list and len(cls._task_wait_list) > 0:
+            # 遍历等待队列中的项目
+            for project_id in list(cls._task_wait_list.keys()):
+                # 只有当项目没有任务在运行且并发项目数未满时才执行
+                if (
+                    project_id not in cls._is_running or not cls._is_running[project_id]
+                ) and len(cls._running_projects) < cls._max_concurrent_projects:
+                    cls._execute_project_tasks(project_id)
+        else:
+            cls._logger.info(f"暂无任务在等待队列中,等待同步")
+            cls._sync_wait_list()
+
+    @classmethod
+    def _get_task_processor(cls):
+        if cls._task_processor is None:
+            cls._task_processor = TaskProcessor()
+        return cls._task_processor
+
+    @classmethod
+    def _ensure_wait_list(cls, project_id: str) -> None:
+        """确保项目的等待队列已初始化"""
+        if project_id not in cls._task_wait_list:
+            cls._task_wait_list[project_id] = []
+
+    @classmethod
+    def _add_task_to_wait_list(
+        cls, task: ProjectTaskDto, log_message: str = None
+    ) -> None:
+        """将任务添加到等待队列并更新状态"""
+        project_id = task.project_id
+        cls._ensure_wait_list(project_id)
+        if log_message:
+            cls._logger.info(log_message)
+        cls._task_wait_list[project_id].append(task)
+
+    @classmethod
+    def _update_project_running_status(
+        cls, project_id: str, is_running: bool = False
+    ) -> None:
+        """更新项目运行状态"""
+        with cls._lock:
+            cls._is_running[project_id] = is_running
+            if is_running:
+                cls._running_projects.add(project_id)
+            elif project_id in cls._running_projects:
+                cls._running_projects.remove(project_id)
+
+            # 如果项目没有等待任务,从等待列表中移除
+            if (
+                not is_running
+                and project_id in cls._task_wait_list
+                and len(cls._task_wait_list[project_id]) == 0
+            ):
+                del cls._task_wait_list[project_id]
+
+    @classmethod
+    def _can_execute_project(cls, project_id: str) -> bool:
+        """检查项目是否可以执行"""
+        # 如果项目已在运行中,直接返回False
+        if project_id in cls._is_running and cls._is_running[project_id]:
+            return False
+        # 如果项目不在运行集合中且并发数已满,直接返回False
+        if (
+            project_id not in cls._running_projects
+            and len(cls._running_projects) >= cls._max_concurrent_projects
+        ):
+            return False
+        return True
+
+    @classmethod
+    def _execute_project_tasks(cls, project_id: str):
+        try:
+            # 检查项目是否可以执行
+            with cls._lock:
+                if not cls._can_execute_project(project_id):
+                    return
+                cls._is_running[project_id] = True
+                cls._running_projects.add(project_id)
+
+            def execute_tasks():
+                try:
+                    cls._logger.debug(f"开始执行项目[{project_id}]")
+                    while (
+                        project_id in cls._task_wait_list
+                        and len(cls._task_wait_list[project_id]) > 0
+                    ):
+                        current_task = cls._task_wait_list[project_id].pop(0)
+                        try:
+                            task_processor = cls._get_task_processor()
+                            cls._logger.debug(
+                                f"开始执行任务[{current_task.project_id}]:{current_task.id}/{current_task.task_name}"
+                            )
+                            task_processor.submit_task(current_task)
+                        except Exception as ex:
+                            cls._logger.error(
+                                f"运行任务失败[{current_task.project_id}]:{current_task.id}/{current_task.task_name}, {str(ex)}"
+                            )
+                finally:
+                    cls._update_project_running_status(project_id, False)
+                    cls._run()
+
+            # 创建新线程执行任务
+            thread = threading.Thread(target=execute_tasks)
+            thread.daemon = True
+            thread.start()
+        except Exception as e:
+            cls._logger.error(f"执行项目任务失败:{project_id}, {str(e)}")
+            cls._update_project_running_status(project_id, False)
+            cls._run()
+
+    @classmethod
+    def _execute_processing_task(cls, task: ProjectTaskDto):
+        # 检查项目是否可以执行
+        project_id = task.project_id
+        try:
+            with cls._lock:
+                if project_id not in cls._process_task_wait_list:
+                    cls._process_task_wait_list[project_id] = []
+                cls._is_running[project_id] = True
+                cls._running_projects.add(project_id)
+                cls._process_task_wait_list[project_id].append(task)
+                cls._logger.debug(f"任务[{task.id}]添加到等待队列")
+
+            def execute_process_tasks():
+                try:
+                    while (
+                        project_id in cls._process_task_wait_list
+                        and len(cls._process_task_wait_list[project_id]) > 0
+                    ):
+                        current_task = cls._process_task_wait_list[project_id].pop(0)
+                        try:
+                            cls._logger.debug(
+                                f"开始执行运行中任务[{current_task.project_id}]:{current_task.id}/{current_task.task_name}"
+                            )
+                            task_processor = cls._get_task_processor()
+                            task_processor.query_task(current_task)
+                        except Exception as ex:
+                            cls._logger.error(
+                                f"运行任务失败[{current_task.project_id}]:{current_task.task_name}, {str(ex)}"
+                            )
+                finally:
+                    cls._update_project_running_status(project_id, False)
+                    cls._run()
+
+            # 创建新线程执行任务
+            thread = threading.Thread(target=execute_process_tasks)
+            thread.daemon = True
+            thread.start()
+        except Exception as e:
+            cls._logger.error(
+                f"执行项目[{project_id}]任务失败:{task.id}/{task.task_name}, {str(e)}"
+            )
+            cls._update_project_running_status(project_id, False)
+            cls._run()
+
+    @classmethod
+    def _sync_wait_list(cls, project_id: str = None) -> int:
+        try:
+            tasks = cls._task_store.get_wait_tasks(project_id)
+            for task in tasks:
+                cls._ensure_wait_list(task.project_id)
+                if task not in cls._task_wait_list[task.project_id]:
+                    cls._task_wait_list[task.project_id].append(task)
+            total_tasks = sum(len(tasks) for tasks in cls._task_wait_list.values())
+            if total_tasks > 0:
+                if len(cls._running_projects) < cls._max_concurrent_projects:
+                    cls._logger.debug(f"同步待运行队列,同步{total_tasks}条数据,运行")
+                    cls._task_empty_wait_count = 0
+                    cls._run()
+                else:
+                    cls._logger.debug(
+                        f"同步待运行队列,同步{total_tasks}条数据。暂无空闲线程,稍后运行"
+                    )
+            else:
+                cls._task_empty_wait_count += 1
+                if cls._task_empty_wait_count < cls._task_empty_wait_max_count:
+                    cls._logger.info(
+                        f"同步待运行队列,无新增数据,等待{cls._task_sleep_interval}秒后同步数据运行,已连续 {cls._task_empty_wait_count} 次无新增数据"
+                    )
+                    sleep(cls._task_sleep_interval)
+                    cls._sync_wait_list()
+                else:
+                    cls._logger.info(
+                        f"同步待运行队列,无新增数据,等待{cls._task_sleep_interval}秒后同步数据运行,已连续 {cls._task_empty_wait_count} 次无新增数据,已超过最大等待次数,停止运行"
+                    )
+            return total_tasks
+        except Exception as e:
+            msg = f"同步待运行队列失败,原因:{e}"
+            cls._logger.error(msg)
+            return 0

+ 76 - 0
SourceCode/IntelligentRailwayCosting/app/executor/task_sender.py

@@ -0,0 +1,76 @@
+import tools.utils as utils, core.configs as configs
+from core.dtos import ProjectTaskDto, ProjectQuotaDto, QuotaInputDto
+from core.enum import SendStatusEnum
+from stores import ProjectQuotaStore, ProjectTaskStore, QuotaInputStore
+
+
+class TaskSender:
+    def __init__(self):
+        self._logger = utils.get_logger()
+        self._task_store = ProjectTaskStore()
+        self._quota_store = ProjectQuotaStore()
+        self._quota_input_store = QuotaInputStore()
+
+    def send_task(self, task: ProjectTaskDto):
+        try:
+            self._logger.info(f"开始推送任务:{task.task_name}")
+            self._task_store.update_send_status(task.id, SendStatusEnum.SUCCESS.value)
+
+            error_count = 0
+            data_list = self._quota_store.get_quotas_by_task_id(task.id, True)
+            for data in data_list:
+                msg = self._quota_store.update_send_status(
+                    data.id, SendStatusEnum.SUCCESS.value
+                )
+                if msg:
+                    error_count += 1
+                    continue
+
+            self._task_store.update_send_status(task.id, SendStatusEnum.SUCCESS.value)
+            self._logger.info(
+                f"推送任务:{task.task_name}完成,{error_count}项错误/共{len(data_list)}项"
+            )
+            return None
+        except Exception as e:
+            msg = f"任务推送失败,原因:{e}"
+            self._logger.error(f"推送任务:{task.task_name},{msg}")
+            self._task_store.update_send_status(
+                task.id, SendStatusEnum.FAILURE.value, msg
+            )
+            return msg
+
+    def send_quota(self, quota: ProjectQuotaDto):
+        try:
+            self._logger.debug(f"开始推送定额输入[{quota.id}]")
+            self._quota_store.update_send_status(quota.id, SendStatusEnum.SUCCESS.value)
+            quota_input = QuotaInputDto.from_quota_dto(quota)
+            quota_input.project_name = f"{quota_input.project_name}{configs.app.ai_flag if quota.is_change else ''}"
+            self._save_quota(quota_input, quota.project_id)
+            # self._quota_store.update_send_status(quota.id, SendStatusEnum.SUCCESS.value)
+            self._logger.debug(f"推送定额输入[{quota.id}]完成")
+            return None
+        except Exception as e:
+            msg = f"定额输入[{quota.id}]推送失败,原因:{e}"
+            self._logger.error(msg)
+            self._quota_store.update_send_status(
+                quota.id, SendStatusEnum.FAILURE.value, msg
+            )
+            return msg
+
+    def _save_quota(self, quota: QuotaInputDto, project_id: str):
+        try:
+            # data = self._quota_input_store.get_quota(project_id, quota.budget_id,quota.item_id,quota.quota_code)
+            if quota.quota_id and quota.quota_id > 0:
+                self._logger.debug(
+                    f"修改定额输入[{quota.quota_id}]:{quota.project_name} {quota.quota_code} {quota.quota_id}"
+                )
+                self._quota_input_store.update_quota(project_id, quota)
+            else:
+                self._quota_input_store.create_quota(project_id, quota)
+                self._logger.debug(
+                    f"新增定额输入[{quota.quota_id}]:{quota.project_name} {quota.quota_code} {quota.quota_id}"
+                )
+        except Exception as e:
+            msg = f"保存定额[{quota.quota_id}]输入失败,原因:{e}"
+            self._logger.error(msg)
+            raise Exception(msg)

+ 48 - 0
SourceCode/IntelligentRailwayCosting/app/flask_app/__init__.py

@@ -0,0 +1,48 @@
+import logging, tools.utils as utils
+from flask import Flask, render_template
+from routes.auth import login_manager
+from routes import register_api
+from views import register_views
+
+
+def create_app():
+    app = Flask(__name__, static_folder="../views/static")
+    app.secret_key = "1qwe2iwb3vber"
+    app.config["JSON_AS_ASCII"] = False
+    app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024  # 限制上传文件大小为16MB
+    app.config["UPLOAD_EXTENSIONS"] = [
+        ".jpg",
+        ".png",
+        ".pdf",
+        ".doc",
+        ".docx",
+        ".xls",
+        ".xlsx",
+    ]  # 允许的文件类型
+    app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024
+    app.config["PRESERVE_CONTEXT_ON_EXCEPTION"] = False
+    app.config["REQUEST_BODY_LIMIT"] = "16MB"
+
+    # 注册所有蓝图
+    register_api(app)
+    register_views(app)
+    login_manager.init_app(app)
+
+    if utils.get_logger_level() == logging.DEBUG:
+        for rule in app.url_map.iter_rules():
+            # route = {
+            #     "endpoint": rule.endpoint,
+            #     "methods": sorted(rule.methods),
+            #     "path": str(rule),
+            # }
+            print(
+                f"URL [{str(rule)}] ==> endpoint: {rule.endpoint}, methods: {sorted(rule.methods)}"
+            )
+    else:
+        log = logging.getLogger("werkzeug")
+        log.setLevel(logging.ERROR)
+
+    return app
+
+
+__all__ = ["create_app"]

+ 20 - 0
SourceCode/IntelligentRailwayCosting/app/main.py

@@ -0,0 +1,20 @@
+import executor, threading
+from flask_app import create_app
+import tools.utils as utils
+
+logger = utils.get_logger()
+
+
+def main():
+    logger.info("程序启动")
+    app = create_app()
+
+    thread = threading.Thread(target=executor.init)
+    thread.daemon = True
+    thread.start()
+
+    app.run(host="0.0.0.0", port=5123)  # 指定HTTP端口为5123
+
+
+if __name__ == "__main__":
+    main()

+ 19 - 0
SourceCode/IntelligentRailwayCosting/app/routes/__init__.py

@@ -0,0 +1,19 @@
+from routes.auth import auth_api
+from routes.excel_test import excel_test_api
+from routes.log import log_api
+from routes.project import project_api
+from routes.project_task import project_task_api
+from routes.project_quota import project_quota_api
+from routes.error import error_api
+
+
+def register_api(app):
+    url_prefix = "/api"
+    # API蓝图注册
+    app.register_blueprint(auth_api, url_prefix=f"{url_prefix}/auth")
+    app.register_blueprint(log_api, url_prefix=f"{url_prefix}/log")
+    app.register_blueprint(project_api, url_prefix=f"{url_prefix}/project")
+    app.register_blueprint(project_task_api, url_prefix=f"{url_prefix}/task")
+    app.register_blueprint(project_quota_api, url_prefix=f"{url_prefix}/quota")
+    app.register_blueprint(excel_test_api, url_prefix=f"{url_prefix}/v1")
+    app.register_blueprint(error_api, url_prefix=f"{url_prefix}/error")

+ 77 - 0
SourceCode/IntelligentRailwayCosting/app/routes/auth.py

@@ -0,0 +1,77 @@
+from flask import Blueprint, request, jsonify, session
+from flask_login import LoginManager, login_user, logout_user
+
+import tools.utils as utils
+from core.log import LogRecordHelper
+from core.enum import OperationType, OperationModule
+from core.api import ResponseBase
+from core.user_session import UserSession, CurrentUser
+from services import UserService
+
+auth_api = Blueprint("auth_api", __name__)
+user_service = UserService()
+
+login_manager = LoginManager()
+login_manager.login_view = "auth_api.login"
+login_manager.login_message = "请先登录"
+
+logger = utils.get_logger()
+
+
+@login_manager.user_loader
+def load_user(user_id):
+    try:
+        user = user_service.get_user_by_id(user_id)
+        if user:
+            return CurrentUser(
+                user_id=user.id,
+                username=user.username,
+                item_range=user.item_range,
+                specialty=user.specialty,
+            )
+    except Exception as e:
+        logger.error(f"获取当前用户失败: {e}")
+    current_user = UserSession.get_current_user()
+    return current_user
+
+
+@auth_api.route("/login", methods=["POST"])
+def login():
+    username = request.json.get("username")
+    password = request.json.get("password")
+
+    # 调用UserService进行用户验证
+    user, msg = user_service.authenticate_user(username, password)
+    if user:
+        # 创建CurrentUser对象并登录
+        current_user = CurrentUser(user_id=user.id)
+        login_user(current_user)
+        UserSession.set_user(user)
+        # 记录日志
+        LogRecordHelper.log_success(
+            OperationType.LOGIN,
+            OperationModule.ACCOUNT,
+            "用户登录成功",
+            username=user.username,
+        )
+
+        return ResponseBase.success({"user_id": user.id, "username": user.username})
+    else:
+        # 登录失败,返回错误信息
+        return ResponseBase.error(msg)
+
+
+@auth_api.route("/logout", methods=["POST"])
+def logout():
+    username = session.get("username")
+    if username:
+        logout_user()
+        UserSession.clear_user()
+        LogRecordHelper.log_success(
+            OperationType.LOGIN,
+            OperationModule.ACCOUNT,
+            "用户退出登录",
+            username=username,
+        )
+        return jsonify(ResponseBase.success())
+    return jsonify(ResponseBase.error("用户未登录"))

+ 23 - 0
SourceCode/IntelligentRailwayCosting/app/routes/error.py

@@ -0,0 +1,23 @@
+from flask import Blueprint, redirect, url_for
+from core.api import ResponseBase
+
+error_api = Blueprint('error_api', __name__)
+
+
+@error_api.route('/redirect/<error_message>', methods=['GET'])
+def redirect_to_error(error_message: str):
+    """
+    API错误重定向到全局错误页面
+    """
+    return redirect(url_for('error.error_with_message', error_message=error_message))
+
+
+def handle_api_error(e: Exception, module_name: str = ''):
+    """
+    统一处理API错误
+    :param e: 异常
+    :param module_name: 模块名称
+    :return: API错误响应
+    """
+    error_message = f'{module_name}操作失败:{str(e)}'
+    return ResponseBase.error(error_message)

+ 159 - 0
SourceCode/IntelligentRailwayCosting/app/routes/excel_test.py

@@ -0,0 +1,159 @@
+from flask import Blueprint, request, jsonify
+import random
+import string
+
+from core.dtos import ExcelParseResultDto, ExcelParseResultDataDto, ChapterDto
+from stores import ChapterStore
+
+excel_test_api = Blueprint("test_api", __name__)
+
+test_data_dic = {}
+task_count_dic = {}
+chapter_store = ChapterStore()
+
+
+@excel_test_api.route("/task_submit", methods=["POST"])
+def test_submit():
+    data = request.get_json()
+    # print("[task_submit] 接受数据:",end="")
+    # print(data)
+    task_id = int(data.get("task_id"))
+    test_data_dic[task_id] = data
+    result = build_result(task_id)
+    return jsonify(result)
+
+
+@excel_test_api.route("/task_status", methods=["POST"])
+def test_query():
+    data = request.get_json()
+    # print("[task_status] 接受数据:",end="")
+    # print(data)
+    task_id = int(data.get("task_id"))
+    result = build_result(task_id)
+    return jsonify(result)
+
+
+@excel_test_api.route("/cancel_task", methods=["POST"])
+def test_cancel():
+    data = request.get_json()
+    # print("[cancel_task] 接受数据:",end="")
+    # print(data)
+    task_id = int(data.get("task_id"))
+    result = build_result(task_id)
+    return jsonify(result)
+
+
+def build_test_data(task_data):
+    # 从task_data获取selected_zgs_id,如果没有则从zgs_list随机选择
+    selected_zgs_id = task_data.get("selected_zgs_id", None) if task_data else 1
+    zgs_code = ""
+    zgs_list = task_data.get("zgs_list", None)
+    if selected_zgs_id == -1 and zgs_list:
+        selected_zgs = random.choice(zgs_list)
+        selected_zgs_id = selected_zgs.get("zgs_id")
+        zgs_code = selected_zgs.get("zgs_name")
+    elif zgs_list:
+        # 修改: 使用列表推导式进行过滤
+        selected_zgs = [x for x in zgs_list if x.get("zgs_id") == selected_zgs_id]
+        if not selected_zgs:
+            selected_zgs = random.choice(zgs_list)
+        else:
+            selected_zgs = selected_zgs[0]  # 取第一个匹配项
+        zgs_code = selected_zgs.get("zgs_name")
+
+    file_excel = task_data.get("files", None)
+    # 从file_excel随机选择文件
+    ex_file_id = ""
+    if file_excel:
+        file = random.choice(file_excel)
+        ex_file_id = file.get("file_id")
+    project_id = task_data.get("project_id", None)
+    selected_chapter = task_data.get("selected_chapter", None)
+    item_id = 0
+    item_code = ""
+    entry_name = ""
+    if project_id and selected_chapter:
+        selected_item_code = selected_chapter.get("item_code", "")
+        items = chapter_store.get_all_children_chapter_items(
+            project_id, selected_item_code
+        )
+        item_data = random.choice(items)
+        item = ChapterDto.from_model(item_data)
+        item_id = item.item_id
+        item_code = item.item_code
+        entry_name = item.project_name
+    dinge_code = f"TY-{''.join(random.choices(string.digits, k=3))}"
+    # dinge_code = ""
+    # 随机选择单位和数量
+    units_list = ["个", "米", "千米", "平方米", "立方米", "吨", "件"]
+    units = random.choice(units_list)
+    amount = round(random.uniform(1, 1000), 2)
+
+    # 随机生成单元格位置
+    col = random.choice(string.ascii_uppercase)
+    row = random.randint(1, 100)
+    ex_cell = f"{col}{row}"
+
+    # 生成更丰富的行数据
+    remarks = ["按图施工", "特殊工艺", "现场测量", "质量要求高", "工期紧张"]
+    ex_row = f"{entry_name},{amount},{units},{random.choice(remarks)}"
+    target_id = -1
+    dinge_adjust = ""
+    if task_data.get("task_id") == 1:
+        target_id = random.randint(-1, 15)
+        dinge_adjust = "1"
+    return ExcelParseResultDataDto(
+        zgs_id=selected_zgs_id if selected_zgs_id else 1,
+        zgs_code=zgs_code,
+        item_id=item_id,
+        item_code=item_code,
+        entry_name=entry_name,
+        target_id=target_id,
+        dinge_code=dinge_code,
+        dinge_adjust=dinge_adjust,
+        units=units,
+        amount=amount,
+        ex_file_id=ex_file_id,
+        ex_cell=ex_cell,
+        ex_row=ex_row,
+        ex_unit=units,
+        ex_amount=amount,
+    )
+
+
+def build_result(task_id: int):
+    data = test_data_dic.get(task_id)
+    test_data = []
+
+    # 更新任务运行次数
+    count = task_count_dic.get(task_id, 0) + 1
+    task_count_dic[task_id] = count
+
+    # 根据运行次数和概率分布确定状态
+    if count >= 3:
+        result = 1  # 运行超过5次必定成功
+    else:
+        rand = random.random()
+        if rand < 0.05:
+            result = -1  # 5%概率失败
+        elif rand < 0.8:
+            result = 0  # 85%概率处理中
+        else:
+            result = 1  # 10%概率成功
+
+    if data and result != -1:
+        # 当result为1(成功)时,有80%概率生成数据
+        if result == 1:
+            if random.random() < 0.8:  # 80%概率生成数据
+                for i in range(random.randint(3, 5)):
+                    test_data.append(build_test_data(data))
+        else:  # result为0(处理中)时,一定生成数据
+            for i in range(random.randint(3, 5)):
+                test_data.append(build_test_data(data))
+
+    status_msg = {-1: "EXCEL远程解析失败", 0: "EXCEL正在解析", 1: "EXCEL解析完成"}.get(
+        result, "未知状态"
+    )
+
+    result = ExcelParseResultDto(task_id, result, status_msg, test_data)
+    return result.to_dict()

+ 42 - 0
SourceCode/IntelligentRailwayCosting/app/routes/log.py

@@ -0,0 +1,42 @@
+from flask import Blueprint, request
+from core.user_session import Permission
+from core.api import ResponseBase, TableResponse
+from services import LogService
+
+log_api = Blueprint("log_api", __name__)
+log_service = LogService()
+
+
+@log_api.route("/list", methods=["POST"])
+@Permission.authorize
+def get_page_list():
+    try:
+        data = request.get_json()
+        page = int(data.get("pageNum", 1))
+        per_page = int(data.get("pageSize", 10))
+        username = data.get("username")
+        operation_type = data.get("operationType")
+        operation_module = data.get("operationModule")
+        operation_result = data.get("operationResult")
+        operation_result = int(operation_result) if operation_result else None
+        start_date = None
+        end_date = None
+        date = data.get("date")
+        if date:
+            date = date.split(" - ")
+            start_date = date[0]
+            end_date = date[1]
+
+        logs, total_count = log_service.get_logs_paginated(
+            page,
+            per_page,
+            username,
+            operation_type,
+            operation_module,
+            operation_result,
+            start_date,
+            end_date,
+        )
+        return TableResponse.success(logs, total_count)
+    except Exception as e:
+        return ResponseBase.error(f"获取日志失败:{str(e)}")

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

@@ -0,0 +1,98 @@
+from flask import Blueprint, request
+from core.user_session import Permission
+from core.api import ResponseBase, TableResponse
+from services import ProjectService, ProjectTaskService
+
+project_api = Blueprint("project_api", __name__)
+project_srvice = ProjectService()
+task_service = ProjectTaskService()
+
+
+@project_api.route("/list", methods=["POST"])
+@Permission.authorize
+def get_page_list():
+    try:
+        data = request.get_json()
+        page = int(data.get("pageNum", 1))
+        per_page = int(data.get("pageSize", 10))
+        keyword = data.get("keyword")
+        start_date = None
+        end_date = None
+        date = data.get("date")
+        if date:
+            date = date.split(" - ")
+            start_date = date[0]
+            end_date = date[1]
+
+        projects, total_count = project_srvice.get_projects_paginated(
+            page, per_page, keyword, start_date, end_date
+        )
+        return TableResponse.success(projects, total_count)
+    except Exception as e:
+        from routes.error import handle_api_error
+
+        return handle_api_error(e, "项目列表")
+
+
+@project_api.route("/budget/<project_id>", methods=["POST"])
+@Permission.authorize
+def get_budget_info(project_id: str):
+    try:
+        data, msg = project_srvice.get_budget_info(project_id)
+        if not data:
+            return ResponseBase.error(msg)
+        return ResponseBase.success(data)
+    except Exception as e:
+        from routes.error import handle_api_error
+
+        return handle_api_error(e, "项目概算信息")
+
+
+@project_api.route("/chapter/<project_id>", methods=["POST"])
+@Permission.authorize
+def get_chapter_items(project_id: str):
+    code = request.args.get("c", None)
+    task_id = request.args.get("t", 0)
+    item_code = code if code else None
+    if not item_code and task_id != 0:
+        task = task_service.get_task(task_id)
+        item_code = task.item_code if task else None
+    try:
+        # 任务页面第一次请求
+        task_first = True if task_id != 0 and not code else False
+        data, msg = project_srvice.get_chapter_items(project_id, item_code, task_first)
+        if not data:
+            return ResponseBase.error(msg)
+        return ResponseBase.success(data)
+    except Exception as e:
+        from routes.error import handle_api_error
+
+        return handle_api_error(e, "项目章节条目")
+
+
+@project_api.route("/task_chapters/<int:task_id>", methods=["POST"])
+@Permission.authorize
+def get_all_chapters(task_id: int):
+    try:
+        task = task_service.get_task(task_id)
+        data, msg = project_srvice.get_task_chapters(task.project_id, task.item_code)
+        if not data:
+            return ResponseBase.error(msg)
+        return ResponseBase.success(data)
+    except Exception as e:
+        from routes.error import handle_api_error
+
+        return handle_api_error(e, "项目章节条目")
+
+
+# @project_api.route('/budget-item/<budget_id>/<project_id>', methods=['POST'])
+# @Permission.authorize
+# def get_budget_items(budget_id:str,project_id:str):
+#     item_code = request.args.get('c', None)
+#     try:
+#         data,msg = project_srvice.get_budget_items(budget_id, project_id, item_code)
+#         if not data:
+#             return ResponseBase.error(msg)
+#         return ResponseBase.success(data)
+#     except Exception as e:
+#         return ResponseBase.error(f'获取项目概算子条目失败:{str(e)}')

+ 156 - 0
SourceCode/IntelligentRailwayCosting/app/routes/project_quota.py

@@ -0,0 +1,156 @@
+from flask import Blueprint, request
+
+from core.dtos import ProjectQuotaDto
+from core.user_session import Permission
+from core.api import ResponseBase, TableResponse
+from services import ProjectQuotaService
+
+project_quota_api = Blueprint("project_quota_api", __name__)
+quota_service = ProjectQuotaService()
+
+
+@project_quota_api.route(
+    "/list/<int:budget_id>/<project_id>/<item_code>", methods=["POST"]
+)
+@Permission.authorize
+def get_page_list(budget_id: int, project_id: str, item_code: str):
+    try:
+        data = request.get_json()
+        page = int(data.get("pageNum", 1))
+        per_page = int(data.get("pageSize", 10))
+        keyword = data.get("keyword")
+        send_status = int(data.get("send_status")) if data.get("send_status") else None
+        data, count = quota_service.get_quotas_paginated(
+            budget_id, project_id, item_code, page, per_page, keyword, send_status
+        )
+        return TableResponse.success(data, count)
+    except Exception as e:
+        return ResponseBase.error(f"获取定额条目列表失败:{str(e)}")
+
+
+@project_quota_api.route(
+    "/list/task/<int:task_id>/<int:budget_id>/<project_id>/<item_code>",
+    methods=["POST"],
+)
+@Permission.authorize
+def get_quotas_by_task_paginated(
+        task_id: int, budget_id: int, project_id: str, item_code: str
+):
+    try:
+        data = request.get_json()
+        page = int(data.get("pageNum", 1))
+        per_page = int(data.get("pageSize", 10))
+        keyword = data.get("keyword")
+        send_status = int(data.get("send_status")) if data.get("send_status") else None
+        data, count = quota_service.get_quotas_by_task_paginated(
+            task_id,
+            budget_id,
+            project_id,
+            item_code,
+            page,
+            per_page,
+            keyword,
+            send_status,
+        )
+        return TableResponse.success(data, count)
+    except Exception as e:
+        return ResponseBase.error(f"获取任务定额条目列表失败:{str(e)}")
+
+
+@project_quota_api.route("/get/<int:quota_id>", methods=["POST"])
+@Permission.authorize
+def get_quota(quota_id: int):
+    try:
+        data = quota_service.get_quota_dto(quota_id)
+        return ResponseBase.success(data.to_dict())
+    except Exception as e:
+        return ResponseBase.error(f"获取定额条目失败:{str(e)}")
+
+
+@project_quota_api.route("/save", methods=["POST"])
+@Permission.authorize
+def save_quota():
+    try:
+        data = request.get_json()
+        quota_dto = ProjectQuotaDto(**data)
+        quota_dto = quota_service.save_quota(quota_dto)
+        run_now = data.get("run_now", "false") == "true"
+        if run_now:
+            is_cover = data.get("is_cover", "false") == "true"
+            quota_service.start_send(quota_dto.id, is_cover)
+        return ResponseBase.success(quota_dto.to_dict())
+    except Exception as e:
+        return ResponseBase.error(f"保存定额条目失败:{str(e)}")
+
+
+@project_quota_api.route("/edit", methods=["POST"])
+@Permission.authorize
+def edit_by_key():
+    try:
+        data = request.get_json()
+        id = data.get("id")
+        if not id:
+            return ResponseBase.error(f"未能获取到定额条目ID")
+        key = data.get("key")
+        value = data.get("val")
+        msg = quota_service.edit_by_key(id, key, value)
+        if msg:
+            return ResponseBase.error(msg)
+        return ResponseBase.success()
+    except Exception as e:
+        return ResponseBase.error(f"编辑定额条目失败:{str(e)}")
+
+
+@project_quota_api.route("/edit_quota_code_batch", methods=["POST"])
+@Permission.authorize
+def edit_quota_code_batch():
+    try:
+        data = request.get_json()
+        ids = data.get("ids", "")
+        quota_code = data.get("quota_code", "")
+        msg = quota_service.edit_quota_code_batch(ids, quota_code)
+        if msg:
+            return ResponseBase.error(msg)
+        return ResponseBase.success()
+    except Exception as e:
+        return ResponseBase.error(f"批量修改定额编号失败:{str(e)}")
+
+
+@project_quota_api.route("/delete/<int:quota_id>", methods=["POST"])
+@Permission.authorize
+def delete_quota(quota_id: int):
+    try:
+        quota_service.delete_quota(quota_id)
+        return ResponseBase.success()
+    except Exception as e:
+        return ResponseBase.error(f"删除定额条目失败:{str(e)}")
+
+
+@project_quota_api.route("/start_send", methods=["POST"])
+@Permission.authorize
+def start_send():
+    try:
+        data = request.get_json()
+        ids = data.get("ids", "")
+        is_cover = data.get("is_cover", "false") == "true"
+        msg = quota_service.start_send_by_ids(ids, is_cover)
+        if msg:
+            return ResponseBase.error(msg)
+        return ResponseBase.success()
+    except Exception as e:
+        return ResponseBase.error(f"启动定额条目失败:{str(e)}")
+
+
+@project_quota_api.route("/save_change_chapter/<project_id>", methods=["POST"])
+@Permission.authorize
+def save_change_chapter(project_id: str):
+    try:
+        data = request.get_json()
+        ids = data.get("ids", "")
+        new_id = data.get("new_id", "")
+        msg = quota_service.save_change_chapter(project_id, ids, new_id)
+        if msg:
+            return ResponseBase.error(msg)
+        return ResponseBase.success()
+    except Exception as e:
+        return ResponseBase.error(f"变更章节失败:{str(e)}")

+ 184 - 0
SourceCode/IntelligentRailwayCosting/app/routes/project_task.py

@@ -0,0 +1,184 @@
+from flask import Blueprint, request, send_from_directory
+import os
+from core.dtos import ProjectTaskDto
+from core.user_session import Permission
+from core.api import ResponseBase, TableResponse
+from services import ProjectTaskService
+
+project_task_api = Blueprint("project_task_api", __name__)
+task_service = ProjectTaskService()
+
+
+@project_task_api.route("/list/<project_id>/<item_code>", methods=["POST"])
+@Permission.authorize
+def get_page_list(project_id: str, item_code: str):
+    try:
+        data = request.get_json()
+        page = int(data.get("pageNum", 1))
+        per_page = int(data.get("pageSize", 10))
+        keyword = data.get("keyword")
+        process_status = (
+            int(data.get("process_status")) if data.get("process_status") else None
+        )
+        send_status = int(data.get("send_status")) if data.get("send_status") else None
+        task, total_count = task_service.get_tasks_paginated(
+            project_id, item_code, page, per_page, keyword, process_status, send_status
+        )
+        return TableResponse.success(task, total_count)
+    except Exception as e:
+        return ResponseBase.error(f"获取任务列表失败:{str(e)}")
+
+
+@project_task_api.route("/get/<int:task_id>", methods=["POST"])
+@Permission.authorize
+def get_task(task_id: int):
+    try:
+        task = task_service.get_task_dto(task_id)
+        return ResponseBase.success(task.to_dict())
+    except Exception as e:
+        return ResponseBase.error(f"获取项目失败:{str(e)}")
+
+
+@project_task_api.route("/save/<int:task_id>", methods=["POST"])
+@Permission.authorize
+def save_task(task_id: int):
+    try:
+        # 从请求中获取表单数据
+        form_data = request.form.to_dict()
+        budget_id = int(form_data.get("budget_id")) if form_data.get("budget_id") else 0
+        item_id = int(form_data.get("item_id")) if form_data.get("item_id") else None
+        project_id = form_data.get("project_id")
+        item_code = form_data.get("item_code")
+        task_name = form_data.get("task_name")
+        task_desc = form_data.get("task_desc")
+        task_sort = int(form_data.get("task_sort")) if form_data.get("task_sort") else 0
+        run_now = form_data.get("run_now") == "true"
+        # delete_old = form_data.get('delete_old', 'false').lower() == 'true'
+        # 获取上传的文件
+        files = request.files.getlist("files")
+        # 验证必要参数
+        if not all([project_id, task_name]):
+            return ResponseBase.error("缺少必要参数:project_id、task_name")
+        # 构建任务DTO
+        task_dto = ProjectTaskDto(
+            item_id=item_id,
+            budget_id=budget_id,
+            project_id=project_id,
+            item_code=item_code,
+            task_name=task_name,
+            task_desc=task_desc,
+            task_sort=task_sort,
+            file_path=None,
+        )
+
+        # 保存任务
+        task = task_service.save_task(task_id, task_dto, files)
+        msg = ""
+        if run_now:
+            msg = task_service.start_run_task(task.id)
+            if msg == "0":
+                msg = "项目有正在运行的任务,已加入等待列表中"
+            elif msg:
+                return ResponseBase.error(msg)
+        return ResponseBase.success(task.to_dict(), msg)
+    except ValueError as ve:
+        return ResponseBase.error(f"参数格式错误:{str(ve)}")
+    except Exception as e:
+        return ResponseBase.error(f"保存任务失败:{str(e)}")
+
+
+@project_task_api.route("/delete/<int:task_id>", methods=["POST"])
+@Permission.authorize
+def delete_task(task_id: int):
+    try:
+        task = task_service.delete_task(task_id)
+        return ResponseBase.success(task)
+    except Exception as e:
+        return ResponseBase.error(f"删除任务失败:{str(e)}")
+
+
+@project_task_api.route("/download", methods=["POST"])
+@Permission.authorize
+def download_file():
+    filename = request.args.get("filename", type=str)
+    if not filename:
+        return ResponseBase.error("文件名不能为空")
+    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 ResponseBase.error("项目目录不存在")
+        full_path = os.path.join(upload_folder, safe_filename)
+        if not os.path.exists(full_path):
+            return ResponseBase.error("文件不存在")
+    except Exception as e:
+        return ResponseBase.error(f"非法文件路径{str(e)}")
+    return send_from_directory(
+        upload_folder.replace("\\", "/"), safe_filename, as_attachment=True
+    )
+
+
+# @project_task_api.route('/start_collect/<int:task_id>', methods=['POST'])
+# @Permission.authorize
+# def start_collect(task_id:int):
+#     try:
+#         msg = task_service.start_run_task(task_id)
+#         if msg:
+#             return ResponseBase.error(msg)
+#         return ResponseBase.success('启动采集成功')
+#     except Exception as e:
+#         return ResponseBase.error(f'启动采集失败:{str(e)}')
+
+# @project_task_api.route('/start_process/<int:task_id>', methods=['POST'])
+# @Permission.authorize
+# def start_process(task_id:int):
+#     try:
+#         msg = task_service.start_process(task_id)
+#         if msg:
+#             return ResponseBase.error(msg)
+#         return ResponseBase.success('启动处理成功')
+#     except Exception as e:
+#         return ResponseBase.error(f'启动处理失败:{str(e)}')
+
+
+@project_task_api.route("/start_task/<int:task_id>", methods=["POST"])
+@Permission.authorize
+def start_task(task_id: int):
+    try:
+        msg = task_service.start_run_task(task_id)
+        if msg == "0":
+            msg = "项目有正在运行的任务,已加入等待列表中"
+        elif msg:
+            return ResponseBase.error(msg)
+        else:
+            msg = "运行成功"
+        return ResponseBase.success(message=msg)
+    except Exception as e:
+        return ResponseBase.error(f"运行失败:{str(e)}")
+
+
+@project_task_api.route("/cancel_task/<int:task_id>", methods=["POST"])
+@Permission.authorize
+def cancel_task(task_id: int):
+    try:
+        msg = task_service.cancel_run_task(task_id)
+        if msg:
+            return ResponseBase.error(msg)
+        return ResponseBase.success(message="启动采集成功")
+    except Exception as e:
+        return ResponseBase.error(f"启动采集失败:{str(e)}")
+
+
+@project_task_api.route("/start_send/<int:task_id>", methods=["POST"])
+@Permission.authorize
+def start_send(task_id: int):
+    try:
+        msg = task_service.start_send_task(task_id)
+        if msg:
+            return ResponseBase.error(msg)
+        return ResponseBase.success(message="启动发送成功")
+    except Exception as e:
+        return ResponseBase.error(f"启动发送失败:{str(e)}")

+ 5 - 0
SourceCode/IntelligentRailwayCosting/app/services/__init__.py

@@ -0,0 +1,5 @@
+from .user import UserService
+from .log import LogService
+from .project import ProjectService
+from .project_quota import ProjectQuotaService
+from .project_task import ProjectTaskService

+ 72 - 0
SourceCode/IntelligentRailwayCosting/app/services/log.py

@@ -0,0 +1,72 @@
+from datetime import datetime, timedelta
+from core.dtos import LogDto
+from stores import LogStore
+
+
+class LogService:
+    def __init__(self):
+        self.log_store = LogStore()
+        pass
+
+    def get_logs_paginated(
+            self,
+            page: int,
+            page_size: int,
+            username: str,
+            operation_type: str,
+            operation_module: str,
+            operation_result: int,
+            start_time: str,
+            end_time: str,
+    ):
+        # 处理开始时间和结束时间
+        start_datetime = None
+        end_datetime = None
+        try:
+            if start_time:
+                start_datetime = datetime.strptime(start_time, "%Y-%m-%d").replace(
+                    hour=0, minute=0, second=0
+                )
+            if end_time:
+                end_datetime = datetime.strptime(end_time, "%Y-%m-%d").replace(
+                    hour=0, minute=0, second=0
+                )
+                # 使用timedelta来处理结束时间加一天
+                end_datetime = end_datetime + timedelta(days=1)
+        except ValueError as e:
+            raise ValueError(f"日期格式错误,请使用YYYY-MM-DD格式: {str(e)}")
+        data = self.log_store.query_logs_paginated(
+            page,
+            page_size,
+            username,
+            operation_type,
+            operation_module,
+            operation_result,
+            start_datetime,
+            end_datetime,
+        )
+        return [
+            LogDto.from_model(item).to_dict() for item in data.get("data", [])
+        ], data.get("total", 0)
+
+    def add_operation_log(
+            self,
+            username: str,
+            operation_type: str,
+            operation_module: str,
+            operation_desc: str,
+            operation_result: int,
+            operation_data: str = None,
+            data_changes: str = None,
+            operation_ip: str = None,
+    ):
+        self.log_store.insert_log(
+            username,
+            operation_type,
+            operation_desc,
+            operation_result,
+            operation_module,
+            operation_data,
+            data_changes,
+            operation_ip,
+        )

+ 211 - 0
SourceCode/IntelligentRailwayCosting/app/services/project.py

@@ -0,0 +1,211 @@
+from typing import Optional
+from datetime import datetime, timedelta
+
+from core.dtos import TotalBudgetInfoDto, TotalBudgetItemDto, ChapterDto
+from core.dtos.project import ProjectDto
+from core.dtos.tree import TreeDto
+from core.user_session import UserSession
+from stores import ProjectStore, BudgetStore, ChapterStore
+
+
+class ProjectService:
+
+    def __init__(self):
+        self._project_store = ProjectStore()
+        self._budget_store = BudgetStore()
+        self._chapter_store = ChapterStore()
+
+    def get_projects_paginated(
+        self,
+        page: int,
+        page_size: int,
+        keyword: Optional[str] = None,
+        start_time: Optional[str] = None,
+        end_time: Optional[str] = None,
+        can_edit: Optional[int] = 0,
+    ):
+
+        # 处理开始时间和结束时间
+        start_datetime = None
+        end_datetime = None
+        try:
+            if start_time:
+                start_datetime = datetime.strptime(start_time, "%Y-%m-%d").replace(
+                    hour=0, minute=0, second=0
+                )
+            if end_time:
+                end_datetime = datetime.strptime(end_time, "%Y-%m-%d").replace(
+                    hour=0, minute=0, second=0
+                )
+                # 使用timedelta来处理结束时间加一天
+                end_datetime = end_datetime + timedelta(days=1)
+        except ValueError as e:
+            raise ValueError(f"日期格式错误,请使用YYYY-MM-DD格式: {str(e)}")
+
+        data = self._project_store.get_user_projects_paginated(
+            page, page_size, keyword, start_datetime, end_datetime, can_edit
+        )
+        return [
+            ProjectDto.from_model(item).to_dict() for item in data.get("data", [])
+        ], data.get("total", 0)
+
+    def get_budget_info(self, project_id: str):
+        msg = self._check_project_db_exit(project_id)
+        if msg:
+            return None, msg
+        data = self._budget_store.get_budget_info(project_id)
+        return [TotalBudgetInfoDto.from_model(item).to_dict() for item in data], ""
+
+    def get_top_budget_items(self, budget_id: int, project_id: str):
+        msg = self._check_project_db_exit(project_id)
+        if msg:
+            return None, msg
+        items = self._budget_store.get_top_budget_items(project_id, budget_id)
+        return [TotalBudgetItemDto.from_model(item).to_dict() for item in items], ""
+
+    def get_task_chapters(self, project_id: str, item_code: str):
+        msg = self._check_project_db_exit(project_id)
+        if msg:
+            return None, msg
+        data = self._chapter_store.get_task_chapter_items(project_id, item_code)
+        data_list = []
+        for item in data:
+            text = (
+                f"第{item.chapter}章、{item.project_name}"
+                if item.chapter
+                else (
+                    f"{item.section}  {item.project_name}"
+                    if item.section
+                    else item.project_name
+                )
+            )
+            parent = (
+                None
+                if item.item_code == item_code
+                else self.get_parent_code(item.item_code)
+            )
+            data_list.append(
+                TreeDto(item.item_code, parent, text, False, item).to_dict()
+            )
+
+        return data_list, ""
+
+    @staticmethod
+    def get_parent_code(item_code: str):
+        if not item_code or len(item_code) == 2:
+            return None
+        if len(item_code) == 4:
+            return item_code[:2]
+        else:
+            return item_code[: len(item_code) - 3]
+
+    def get_chapter_items(self, project_id: str, item_code: str, task_first: bool):
+        msg = self._check_project_db_exit(project_id)
+        if msg:
+            return None, msg
+        data_list = []
+        if not item_code:
+            team_item_code = None
+            current_user = UserSession.get_current_user()
+            if not current_user.is_admin:
+                team_item_code_str = self._project_store.get_team_project_item_code(
+                    project_id, current_user.username
+                )
+                if team_item_code_str:
+                    team_item_code = (
+                        None
+                        if team_item_code_str == "None"
+                        or team_item_code_str == "0"
+                        or team_item_code_str == ""
+                        else team_item_code_str.split(",")
+                    )
+            items = self._chapter_store.get_top_chapter_items(
+                project_id, team_item_code
+            )
+        else:
+            items = self._chapter_store.get_child_chapter_items(
+                project_id, item_code, task_first
+            )
+        parent = "#"
+        if item_code and not task_first:
+            item = self._chapter_store.get_chapter_item_by_item_code(
+                project_id, item_code
+            )
+            parent = item.item_id
+        for item in items:
+            text = (
+                f"第{item.chapter}章、{item.project_name}"
+                if item.chapter
+                else (
+                    f"{item.section}  {item.project_name}"
+                    if item.section
+                    else item.project_name
+                )
+            )
+            data_list.append(
+                TreeDto(
+                    item.item_id, parent, text, item.children_count > 0, item
+                ).to_dict()
+            )
+        return data_list, ""
+
+    def get_budget_items(self, budget_id: int, project_id: str, item_code: str):
+        if not budget_id:
+            return None, "budget_id不能为空"
+        msg = self._check_project_db_exit(project_id)
+        if msg:
+            return None, msg
+        data_list = []
+        if not item_code:
+            team_item_code = None
+            current_user = UserSession.get_current_user()
+            if not current_user.is_admin:
+                team_item_code_str = self._project_store.get_team_project_item_code(
+                    project_id, current_user.username
+                )
+                if team_item_code_str:
+                    team_item_code = (
+                        None
+                        if team_item_code_str == "None"
+                        or team_item_code_str == "0"
+                        or team_item_code_str == ""
+                        else team_item_code_str.split(",")
+                    )
+            items = self._budget_store.get_top_budget_items(
+                project_id, budget_id, team_item_code
+            )
+        else:
+            items = self._budget_store.get_child_budget_items(
+                project_id, budget_id, item_code
+            )
+        parent = "#"
+        if item_code:
+            item = self._budget_store.get_budget_item_by_item_code(
+                project_id, budget_id, item_code
+            )
+            parent = item.item_id
+        for item in items:
+            text = (
+                f"第{item.chapter}章、{item.project_name}"
+                if item.chapter
+                else (
+                    f"{item.section}  {item.project_name}"
+                    if item.section
+                    else item.project_name
+                )
+            )
+            data_list.append(
+                TreeDto(
+                    item.item_id, parent, text, item.children_count > 0, item
+                ).to_dict()
+            )
+        return data_list, ""
+
+    def _check_project_db_exit(self, project_id: str):
+        if not project_id:
+            return "project_id不能为空"
+        if not self._project_store.get(project_id):
+            return "项目不存在"
+        if not project_id.startswith("Reco"):
+            return "项目id格式错误"
+        return None

+ 450 - 0
SourceCode/IntelligentRailwayCosting/app/services/project_quota.py

@@ -0,0 +1,450 @@
+from typing import Optional
+
+import tools.utils as utils
+from core.dtos import ProjectQuotaDto
+from core.models import ProjectQuotaModel
+from stores import ProjectQuotaStore, ChapterStore
+from core.log.log_record import LogRecordHelper
+from core.enum import OperationModule, OperationType, SendStatusEnum
+
+import executor
+
+
+class ProjectQuotaService:
+    def __init__(self):
+        self.store = ProjectQuotaStore()
+        self.chapter_store = ChapterStore()
+        self._logger = utils.get_logger()
+
+    def get_quotas_paginated(
+            self,
+            budget_id: int,
+            project_id: str,
+            item_code: str,
+            page: int = 1,
+            page_size: int = 10,
+            keyword: Optional[str] = None,
+            send_status: Optional[int] = None,
+    ):
+        """获取项目定额列表
+
+        Args:
+            budget_id: 概算序号
+            project_id: 项目编号
+            item_code: 条目编号
+            page: 页码
+            page_size: 每页数量
+            keyword: 关键字
+            send_status: 发送状态
+
+        Returns:
+            dict: 包含总数和定额列表的字典
+        """
+        try:
+            data = self.store.get_quotas_paginated(
+                budget_id=budget_id,
+                project_id=project_id,
+                item_code=item_code,
+                page=page,
+                page_size=page_size,
+                keyword=keyword,
+                send_status=send_status,
+            )
+            return [
+                ProjectQuotaDto.from_model(quota).to_dict()
+                for quota in data.get("data", [])
+            ], data.get("total", 0)
+
+        except Exception as e:
+            # 记录错误日志
+            print(f"获取项目定额列表失败: {str(e)}")
+            raise
+
+    def get_quotas_by_task_paginated(
+            self,
+            task_id: int,
+            budget_id,
+            project_id,
+            item_code,
+            page: int = 1,
+            page_size: int = 10,
+            keyword: Optional[str] = None,
+            send_status: Optional[int] = None,
+    ):
+        try:
+            data = self.store.get_quotas_by_task_paginated(
+                task_id=task_id,
+                budget_id=budget_id,
+                project_id=project_id,
+                item_code=item_code,
+                page=page,
+                page_size=page_size,
+                send_status=send_status,
+                keyword=keyword,
+            )
+            return [
+                ProjectQuotaDto.from_model(quota).to_dict()
+                for quota in data.get("data", [])
+            ], data.get("total", 0)
+        except Exception as e:
+            self._logger.error(f"获取任务项目定额列表失败: {str(e)}")
+            raise
+
+    def get_quota_dto(self, quota_id: int):
+        try:
+            return self.store.get_quota_dto(quota_id)
+        except Exception as e:
+            self._logger.error(f"获取定额条目DTO失败: {str(e)}")
+            raise
+
+    def get_quota(self, quota_id: int) -> Optional[ProjectQuotaModel]:
+        """获取单个项目定额
+
+        Args:
+            quota_id: 定额ID
+
+        Returns:
+            Optional[ProjectQuotaDto]: 项目定额DTO对象
+        """
+        try:
+            return self.store.get_quota(quota_id)
+        except Exception as e:
+            self._logger.error(f"获取定额条目失败: {str(e)}")
+            raise
+
+    def save_quota(self, quota_dto: ProjectQuotaDto) -> Optional[ProjectQuotaDto]:
+        """保存定额"""
+        try:
+            # 业务验证
+            if quota_dto.id == 0:
+                quota_dto = self.create_quota(quota_dto)
+            else:
+                quota_dto = self.update_quota(quota_dto)
+                if quota_dto.send_status != SendStatusEnum.NEW.value:
+                    self.update_send_status(quota_dto.id, SendStatusEnum.CHANGE.value)
+            return quota_dto
+        except Exception as e:
+            self._logger.error(f"保存定额条目失败: {str(e)}")
+            raise
+
+    def create_quota(self, quota_dto: ProjectQuotaDto) -> ProjectQuotaDto:
+        """创建项目定额
+
+        Args:
+            quota_dto: 定额DTO对象
+
+        Returns:
+            ProjectQuotaDto: 创建后的定额DTO对象
+        """
+        try:
+            # 业务验证
+            if not quota_dto.project_id or not quota_dto.budget_id:
+                raise ValueError("项目编号和概算序号不能为空")
+            LogRecordHelper.log_success(
+                OperationType.CREATE,
+                OperationModule.QUOTA,
+                f"新增定额条目: {quota_dto.entry_name}",
+                utils.to_str(quota_dto.to_dict()),
+            )
+            return self.store.create_quota(quota_dto)
+        except Exception as e:
+            self._logger.error(f"创建项目定额失败: {str(e)}")
+            LogRecordHelper.log_fail(
+                OperationType.CREATE,
+                OperationModule.QUOTA,
+                f"新增定额条目失败: {str(e)}",
+                utils.to_str(quota_dto.to_dict()),
+            )
+            raise
+
+    def update_quota(self, quota_dto: ProjectQuotaDto) -> Optional[ProjectQuotaDto]:
+        """更新项目定额
+
+        Args:
+            quota_dto: 定额DTO对象
+
+        Returns:
+            Optional[ProjectQuotaDto]: 更新后的定额DTO对象
+        """
+        try:
+            # 业务验证
+            if not quota_dto.id:
+                raise ValueError("定额ID不能为空")
+            quota = self.get_quota_dto(quota_dto.id)
+            log_data = utils.to_str(quota.to_dict())
+            if quota:
+                quota_dto.id = quota.id
+                quota_dto.project_id = quota.project_id
+                quota_dto.budget_id = quota.budget_id
+                quota_dto.item_id = quota.item_id
+                quota_dto.item_code = quota.item_code
+                quota_dto.quota_id = quota.quota_id
+                quota_dto.ex_file = quota.ex_file
+                quota_dto.ex_row = quota.ex_row
+                quota_dto.ex_cell = quota.ex_cell
+                quota_dto.ex_unit = quota.ex_unit
+                quota_dto.ex_amount = quota.ex_amount
+
+            # self.update_process_status(quota_dto.id,4)
+            LogRecordHelper.log_success(
+                OperationType.UPDATE,
+                OperationModule.QUOTA,
+                f"修改定额条目: {quota_dto.entry_name}",
+                log_data,
+                utils.to_str(quota_dto.to_dict()),
+            )
+            return self.store.update_quota(quota_dto)
+        except Exception as e:
+            self._logger.error(f"更新项目定额失败: {str(e)}")
+            LogRecordHelper.log_fail(
+                OperationType.UPDATE,
+                OperationModule.QUOTA,
+                f"修改定额条目失败: {str(e)}",
+                utils.to_str(quota_dto.to_dict()),
+            )
+            raise
+
+    def edit_by_key(self, id: int, key: str, value: str):
+        try:
+            # 业务验证
+            # dict_data = {"id": id, key: value}
+
+            # quota_dto = ProjectQuotaDto(**dict_data)
+            quota_dto = self.get_quota_dto(id)
+            if not quota_dto:
+                raise ValueError("未查询到定额条目")
+            log_data = utils.to_str(quota_dto.to_dict())
+            setattr(quota_dto, key, value)
+            if quota_dto.is_change == 0 and key == "entry_name":
+                quota_dto.is_change = 0
+            else:
+                quota_dto.is_change = 1
+            # self.update_process_status(quota_dto.id,4)
+            LogRecordHelper.log_success(
+                OperationType.UPDATE,
+                OperationModule.QUOTA,
+                f"修改定额条目[{key}]: {value}",
+                log_data,
+                utils.to_str({"id": id, key: value}),
+            )
+            self.store.update_quota(quota_dto)
+            return None
+        except Exception as e:
+            self._logger.error(f"更新项目定额失败[{key}]: {value}, {str(e)}")
+            LogRecordHelper.log_fail(
+                OperationType.UPDATE,
+                OperationModule.QUOTA,
+                f"修改定额条目失败[{key}]: {value}, {str(e)}",
+                utils.to_str({"id": id, key: value}),
+            )
+            raise
+
+    def edit_quota_code_batch(self, ids: str, quota_code: str):
+        try:
+            if not ids:
+                return "请选择要变更的章节"
+            ids = ids.split(",")
+            msg = ""
+            for id in ids:
+                try:
+                    quota_dto = self.get_quota_dto(int(id))
+                    if not quota_dto:
+                        msg += f"{id}不存在;"
+                        continue
+                    quota_dto.quota_code = quota_code
+                    quota_dto.is_change = 1
+                    self.store.update_quota(quota_dto)
+                except Exception as e:
+                    msg += f"{id}: {str(e)};"
+            LogRecordHelper.log_fail(
+                OperationType.UPDATE,
+                OperationModule.QUOTA,
+                f"批量修改定额: {ids}",
+                utils.to_str({"ids": ids, "quota_code": quota_code}),
+            )
+            return None if msg == "" else msg
+        except Exception as e:
+            msg = f"批量修改定额失败: {str(e)}"
+            self._logger.error(msg)
+            LogRecordHelper.log_fail(
+                OperationType.UPDATE,
+                OperationModule.QUOTA,
+                f"批量修改定额失败: {str(e)}",
+                utils.to_str({"ids": ids, "quota_code": quota_code}),
+            )
+            return msg
+
+    def delete_quota(self, quota_id: int) -> bool:
+        """删除项目定额
+
+        Args:
+            quota_id: 定额ID
+
+        Returns:
+            bool: 删除是否成功
+        """
+        try:
+            flag = self.store.delete_quota(quota_id)
+            LogRecordHelper.log_success(
+                OperationType.DELETE,
+                OperationModule.QUOTA,
+                f"删除定额条目: {quota_id}",
+                f"{quota_id}",
+            )
+            return flag
+        except Exception as e:
+            self._logger.error(f"删除项目定额失败: {str(e)}")
+            LogRecordHelper.log_fail(
+                OperationType.DELETE,
+                OperationModule.QUOTA,
+                f"删除定额条目失败: {quota_id}",
+                f"{quota_id}",
+            )
+            raise
+
+    def save_change_chapter(self, project_id, ids, new_id):
+        try:
+            if not ids:
+                return "请选择要变更的章节"
+            chapter = self.chapter_store.get_chapter_dto(project_id, new_id)
+            if not chapter:
+                return "章节不存在"
+            ids_arr = ids.split(",")
+
+            err = ""
+            for id_str in ids_arr:
+                try:
+                    self.store.update_quota_chapter(
+                        int(id_str), chapter.item_id, chapter.item_code
+                    )
+                except Exception as e:
+                    err += str(e) + ";"
+                    continue
+            if err:
+                LogRecordHelper.log_fail(
+                    OperationType.UPDATE,
+                    OperationModule.QUOTA,
+                    f"修改定额条目章节失败: {err}",
+                    ids,
+                )
+                return err
+            LogRecordHelper.log_success(
+                OperationType.UPDATE,
+                OperationModule.QUOTA,
+                f"修改定额条目章节",
+                ids,
+            )
+            return None
+        except Exception as e:
+            msg = f"章节变更失败: {str(e)}"
+            self._logger.error(msg)
+            LogRecordHelper.log_fail(
+                OperationType.UPDATE,
+                OperationModule.QUOTA,
+                f"修改定额条目章节失败: {str(e)}",
+                ids,
+            )
+            return msg
+
+    def update_send_status(self, quota_id: int, status: int, err: str = None) -> bool:
+        """更新发送状态
+
+        Args:
+            quota_id: 定额ID
+            status: 状态值
+            err: 错误信息
+
+        Returns:
+            bool: 更新是否成功
+        """
+        try:
+            return self.store.update_send_status(quota_id, status, err)
+        except Exception as e:
+            self._logger.error(f"更新项目定额发送状态失败: {str(e)}")
+            raise
+
+    # def start_process(self, quota_id: int) -> Optional[str]:
+    #     """启动处理"""
+    #     quota = self.get_quota_dto(quota_id)
+    #     if quota:
+    #         self.update_process_status(quota_id, 1)
+    #         thread = threading.Thread(target=self._process_quota, args=(quota,))
+    #         thread.start()
+    #     else:
+    #         return "定额条目没有查询到"
+    #
+    # def _process_quota(self, quota: ProjectQuotaDto):
+    #     try:
+    #        msg = executor.process_quota(quota)
+    #        if not msg:
+    #            self.start_send(quota.id)
+    #     except Exception as e:
+    #         self._logger.error(f"处理定额条目失败: {str(e)}")
+    #         self.update_process_status(quota.id, 3, str(e))
+    #         raise
+
+    def start_send_by_ids(self, ids: str, is_cover: bool = False):
+        try:
+            id_list = ids.split(",")
+            err = ""
+            for _id in id_list:
+                msg = self.start_send(int(_id), is_cover, False)
+                if msg:
+                    err += f"{msg}[{_id}],"
+            if err:
+                LogRecordHelper.log_fail(
+                    OperationType.SEND,
+                    OperationModule.QUOTA,
+                    f"批量推送定额条目,部分失败: {err}",
+                    ids,
+                )
+            LogRecordHelper.log_success(
+                OperationType.SEND,
+                OperationModule.QUOTA,
+                f"批量推送定额条目",
+                ids,
+            )
+            return err
+        except Exception as e:
+            LogRecordHelper.log_fail(
+                OperationType.SEND,
+                OperationModule.QUOTA,
+                f"批量推送定额条目失败: {str(e)}",
+                ids,
+            )
+            self._logger.error(f"批量启动定额条目发送失败: {str(e)}")
+            raise
+
+    def start_send(
+            self, _id: int, is_cover: bool = False, is_log: bool = True
+    ) -> Optional[str]:
+        """启动发送"""
+        quota = self.get_quota_dto(_id)
+        if quota:
+            self.update_send_status(_id, SendStatusEnum.SUCCESS.value)
+            if not is_cover:
+                quota.quota_id = 0
+            # thread = threading.Thread(target=self._send_quota, args=(quota,))
+            # thread.start()
+            if is_log:
+                LogRecordHelper.log_success(
+                    OperationType.SEND,
+                    OperationModule.QUOTA,
+                    f"推送定额条目",
+                    f"ID:{_id},是否覆盖:{is_cover}",
+                )
+            if self._send_quota(quota):
+                return None
+            else:
+                return f"{_id}发送失败;"
+        else:
+            return "定额条目没有查询到"
+
+    def _send_quota(self, quota: ProjectQuotaDto):
+        try:
+            executor.send_quota(quota)
+            return True
+        except Exception as e:
+            self._logger.error(f"发送定额条目失败: {str(e)}")
+            self.update_send_status(quota.id, 3, str(e))
+            return False

+ 431 - 0
SourceCode/IntelligentRailwayCosting/app/services/project_task.py

@@ -0,0 +1,431 @@
+from typing import Optional
+
+import tools.utils as utils, core.configs as configs, os, threading
+from core.log.log_record import LogRecordHelper
+from core.enum import OperationModule, OperationType, TaskStatusEnum, SendStatusEnum
+from core.dtos import ProjectTaskDto
+from core.models import ProjectTaskModel
+from stores import ProjectTaskStore
+import executor
+
+
+class ProjectTaskService:
+    def __init__(self):
+        self.store = ProjectTaskStore()
+        self._logger = utils.get_logger()
+        self._task_locks = {}
+        self._lock = threading.Lock()
+
+    def _get_task_lock(self, task_id: int) -> threading.Lock:
+        with self._lock:
+            if task_id not in self._task_locks:
+                self._task_locks[task_id] = threading.Lock()
+            return self._task_locks[task_id]
+
+    def start_run_task(self, task_id: int):
+        task_lock = self._get_task_lock(task_id)
+        with task_lock:
+            task = self.store.get_task_dto(task_id)
+            if not task:
+                return "没有查询到任务"
+            if not task.file_path or task.file_path.strip() == "":
+                return "没有上传文件"
+            if task.process_status == TaskStatusEnum.PROCESSING:
+                return "正在运行中"
+
+            try:
+                thread = threading.Thread(target=self._run_task, args=(task,))
+                thread.daemon = True
+                thread.start()
+                LogRecordHelper.log_success(
+                    OperationType.PROCESS,
+                    OperationModule.TASK,
+                    f"开始运行:{task.task_name}",
+                    task.id,
+                )
+                if executor.project_is_running(task.project_id):
+                    self.update_process_status(task_id, TaskStatusEnum.WAIT.value, "")
+                    return "0"
+                else:
+                    self.update_process_status(
+                        task_id, TaskStatusEnum.PROCESSING.value, ""
+                    )
+                return None
+            except Exception as e:
+                self._logger.error(f"启动任务失败: {str(e)}")
+                self.update_process_status(
+                    task_id, TaskStatusEnum.FAILURE.value, str(e)
+                )
+                LogRecordHelper.log_fail(
+                    OperationType.PROCESS,
+                    OperationModule.TASK,
+                    f"开始运行失败:{task.task_name}",
+                    task.id,
+                )
+                raise
+
+    def _run_task(self, task: ProjectTaskDto):
+        task_lock = self._get_task_lock(task.id)
+        with task_lock:
+            try:
+                executor.run_task(task)
+                # if not msg:
+                #     self.start_send_task(task.id)
+            except Exception as e:
+                self._logger.error(f"运行项目任务失败: {str(e)}")
+                self.update_process_status(
+                    task.id, TaskStatusEnum.FAILURE.value, str(e)
+                )
+                raise
+
+    def start_send_task(self, task_id: int):
+        task_lock = self._get_task_lock(task_id)
+        with task_lock:
+            task = self.store.get_task_dto(task_id)
+            if not task:
+                return "没有查询到任务"
+            if task.process_status != TaskStatusEnum.SUCCESS.value:
+                return "还未处理完成"
+            if task.send_status == SendStatusEnum.PROCESSING.value:
+                return "正在发送中"
+
+            try:
+                thread = threading.Thread(target=self._send_task, args=(task,))
+                thread.daemon = True
+                thread.start()
+                LogRecordHelper.log_success(
+                    OperationType.SEND,
+                    OperationModule.TASK,
+                    f"开始发送:{task.task_name}",
+                    task.id,
+                )
+                return None
+            except Exception as e:
+                self._logger.error(f"启动发送任务失败: {str(e)}")
+                self.update_send_status(task_id, TaskStatusEnum.FAILURE.value, str(e))
+                LogRecordHelper.log_fail(
+                    OperationType.SEND,
+                    OperationModule.TASK,
+                    f"开始发送失败:{task.task_name}",
+                    task.id,
+                )
+                raise
+
+    def _send_task(self, task: ProjectTaskDto):
+        task_lock = self._get_task_lock(task.id)
+        with task_lock:
+            try:
+                executor.send_task(task)
+            except Exception as e:
+                self._logger.error(f"发送项目任务失败: {str(e)}")
+                self.update_send_status(task.id, TaskStatusEnum.FAILURE.value, str(e))
+                raise
+
+    def get_tasks_paginated(
+        self,
+        project_id: str,
+        item_code: str,
+        page: int = 1,
+        page_size: int = 10,
+        keyword: Optional[str] = None,
+        process_status: Optional[int] = None,
+        send_status: Optional[int] = None,
+    ):
+        """获取项目任务列表
+
+        Args:
+            project_id: 项目编号
+            item_code: 条目编号
+            page: 页码
+            page_size: 每页数量
+            keyword: 关键字
+            process_status: 处理状态
+            send_status: 发送状态
+
+        Returns:
+            dict: 包含总数和任务列表的字典
+        """
+        try:
+            data = self.store.get_tasks_paginated(
+                project_id=project_id,
+                item_code=item_code,
+                page=page,
+                page_size=page_size,
+                keyword=keyword,
+                process_status=process_status,
+                send_status=send_status,
+            )
+            return [
+                ProjectTaskDto.from_model(task).to_dict()
+                for task in data.get("data", [])
+            ], data.get("total", 0)
+        except Exception as e:
+            self._logger.error(f"获取项目任务列表失败: {str(e)}")
+            raise
+
+    def get_task(self, task_id: int) -> Optional[ProjectTaskModel]:
+        """获取单个项目任务
+
+        Args:
+            task_id: 任务ID
+
+        Returns:
+            Optional[ProjectTaskModel]: 项目任务
+        """
+        try:
+            task = self.store.get_task(task_id)
+            return task
+        except Exception as e:
+            self._logger.error(f"获取项目任务失败: {str(e)}")
+            raise
+
+    def get_task_dto(self, task_id: int):
+        try:
+            task = self.store.get_task_dto(task_id)
+            return task
+        except Exception as e:
+            self._logger.error(f"获取项目任务失败: {str(e)}")
+            raise
+
+    def save_task(self, task_id: int, task_dto: ProjectTaskDto, files: list):
+        log_data = ""
+        if task_id == 0:
+            task = self.store.create_task(task_dto)
+        else:
+            task = self.get_task_dto(task_id)
+            if task.process_status != TaskStatusEnum.NEW.value:
+                raise Exception("项目任务提交处理过,不能再修改。")
+            log_data = utils.to_str(task.to_dict())
+            if task is None:
+                LogRecordHelper.log_fail(
+                    OperationType.UPDATE,
+                    OperationModule.TASK,
+                    f"修改任务失败 错误:任务[{task_id}]不存在",
+                )
+                raise Exception("项目任务不存在")
+            task_dto.id = task_id
+            task = self.store.update_task(task_dto)
+        try:
+            if task:
+                paths = self._process_file_upload(task, files)
+                if paths != task.file_path:
+                    task = self.store.update_task_files(task.id, paths)
+                if task_id == 0:
+                    LogRecordHelper.log_success(
+                        OperationType.CREATE,
+                        OperationModule.TASK,
+                        f"创建任务成功 任务:{task.task_name}",
+                        utils.to_str(task.to_dict()),
+                    )
+                else:
+                    LogRecordHelper.log_success(
+                        OperationType.UPDATE,
+                        OperationModule.TASK,
+                        f"修改任务成功",
+                        log_data,
+                        utils.to_str(task.to_dict()),
+                    )
+            else:
+                if task_id == 0:
+                    LogRecordHelper.log_fail(
+                        OperationType.CREATE,
+                        OperationModule.TASK,
+                        f"创建任务失败",
+                        utils.to_str(task_dto.to_dict()),
+                    )
+                else:
+                    LogRecordHelper.log_fail(
+                        OperationType.UPDATE,
+                        OperationModule.TASK,
+                        f"修改任务失败",
+                        log_data,
+                    )
+            return task
+        except ValueError as ve:
+            LogRecordHelper.log_fail(
+                OperationType.UPDATE,
+                OperationModule.TASK,
+                f"{'修改' if task_id == 0 else '添加'}任务失败 错误:{task_dto.task_name} {str(ve)}",
+                utils.to_str(task_dto.to_dict()),
+            )
+            raise ve
+        except Exception as e:
+            LogRecordHelper.log_fail(
+                OperationType.UPDATE,
+                OperationModule.TASK,
+                f"{'修改' if task_id == 0 else '添加'}任务失败 错误:{task_dto.task_name} {str(e)}",
+                utils.to_str(task_dto.to_dict()),
+            )
+            raise e
+
+    def _process_file_upload(self, task: ProjectTaskDto, files: list) -> str:
+        """处理文件上传流程"""
+
+        self._logger.info(f"保存处理文件,项目ID:{task.project_id},任务ID:{task.id}")
+        delete_old = task.process_status == 0
+        if delete_old and task.file_path:
+            delete_paths = []
+            for file_path in task.file_path.split(","):
+                if os.path.isfile(file_path):
+                    delete_dir = os.path.dirname(file_path).replace(
+                        "upload_files/", "delete_files/"
+                    )
+                    os.makedirs(delete_dir, exist_ok=True)
+                    # 处理文件名冲突
+                    base_name = os.path.basename(file_path)
+                    target_path = os.path.join(delete_dir, base_name)
+                    counter = 1
+                    while os.path.exists(target_path):
+                        name, ext = os.path.splitext(base_name)
+                        target_path = os.path.join(delete_dir, f"{name}_{counter}{ext}")
+                        counter += 1
+                    os.rename(file_path, target_path)
+                    delete_paths.append(target_path)
+                    # 删除空目录
+                    original_dir = os.path.dirname(file_path)
+                    if not os.listdir(original_dir):
+                        os.rmdir(original_dir)
+            if len(delete_paths) > 0:
+                LogRecordHelper.log_success(
+                    OperationType.DELETE,
+                    OperationModule.TASK,
+                    f"删除任务文件:{task.task_name}",
+                    utils.to_str(delete_paths),
+                )
+
+        # file_paths = [] if delete_old or not task.file_path else task.file_path.split(',')
+
+        file_paths = []
+        if files and len(files) > 0:
+            task_dir = os.path.join(
+                configs.app.source_path, f"upload_files/{task.get_path()}"
+            )
+            os.makedirs(task_dir, exist_ok=True)
+            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(task_dir, file.filename)
+                file_path = file_path.replace("\\", "/")
+                file.save(file_path)
+                file_paths.append(file_path)
+        return ",".join(file_paths)
+
+    def _create_task(self, task_dto: ProjectTaskDto) -> ProjectTaskDto:
+        """创建项目任务
+
+        Args:
+            task_dto: 任务DTO对象
+
+        Returns:
+            ProjectTaskDto: 创建后的任务DTO对象
+        """
+        try:
+            # 业务验证
+            if not task_dto.project_id or not task_dto.budget_id:
+                raise ValueError("项目编号和概算序号不能为空")
+            if not task_dto.task_name:
+                raise ValueError("任务名称不能为空")
+
+            return self.store.create_task(task_dto)
+        except Exception as e:
+            self._logger.error(f"创建项目任务失败: {str(e)}")
+            raise
+
+    def _update_task(self, task_dto: ProjectTaskDto) -> Optional[ProjectTaskDto]:
+        """更新项目任务
+
+        Args:
+            task_dto: 任务DTO对象
+
+        Returns:
+            Optional[ProjectTaskDto]: 更新后的任务DTO对象
+        """
+        try:
+            # 业务验证
+            if not task_dto.id:
+                raise ValueError("任务ID不能为空")
+            if not task_dto.task_name:
+                raise ValueError("任务名称不能为空")
+
+            return self.store.update_task(task_dto)
+        except Exception as e:
+            self._logger.error(f"更新项目任务失败: {str(e)}")
+            raise
+
+    def delete_task(self, task_id: int) -> bool:
+        """删除项目任务
+
+        Args:
+            task_id: 任务ID
+        Returns:
+            bool: 删除是否成功
+        """
+        try:
+            task = self.store.get_task(task_id)
+            if task.process_status == TaskStatusEnum.PROCESSING.value:
+                raise Exception("任务正在进行中,请先取消任务")
+            return self.store.delete_task(task_id)
+        except Exception as e:
+            self._logger.error(f"删除项目任务失败: {str(e)}")
+            raise
+
+    def update_process_status(self, task_id: int, status: int, err: str = None) -> bool:
+        """更新处理状态
+
+        Args:
+            task_id: 任务ID
+            status: 状态值
+            err: 错误信息
+
+        Returns:
+            bool: 更新是否成功
+        """
+        try:
+            return self.store.update_process_status(task_id, status, err)
+        except Exception as e:
+            self._logger.error(f"更新项目任务处理状态失败: {str(e)}")
+            raise
+
+    def update_send_status(self, task_id: int, status: int, err: str = None) -> bool:
+        """更新发送状态
+
+        Args:
+            task_id: 任务ID
+            status: 状态值
+            err: 错误信息
+
+        Returns:
+            bool: 更新是否成功
+        """
+        try:
+            return self.store.update_send_status(task_id, status, err)
+        except Exception as e:
+            self._logger.error(f"更新项目任务发送状态失败: {str(e)}")
+            raise
+
+    def cancel_run_task(self, task_id: int):
+        task = self.store.get_task_dto(task_id)
+        if task:
+            msg = executor.cancel_task(task)
+            if not msg:
+                self.store.update_task_status(task.id, TaskStatusEnum.CANCELED.value)
+                LogRecordHelper.log_success(
+                    OperationType.PROCESS,
+                    OperationModule.TASK,
+                    f"取消运行:{task.task_name}",
+                    task.id,
+                )
+            else:
+                LogRecordHelper.log_fail(
+                    OperationType.PROCESS,
+                    OperationModule.TASK,
+                    f"取消运行失败:{task.task_name}。 {msg}",
+                    task.id,
+                )
+                return msg
+        else:
+            return "没有查询到任务"

+ 33 - 0
SourceCode/IntelligentRailwayCosting/app/services/user.py

@@ -0,0 +1,33 @@
+from stores import UserStore
+from core.dtos import UserDto
+
+
+class UserService:
+    def __init__(self):
+        self.user_store = UserStore()
+
+    def get_user_by_id(self, user_id: int) -> UserDto | None:
+        user = self.user_store.get_user_by_id(user_id)
+        if user:
+            return UserDto.from_model(user)
+        return None
+
+    def get_user_by_username(self, username: str) -> UserDto | None:
+        user = self.user_store.get_user_by_username(username)
+        if user:
+            return UserDto.from_model(user)
+        return None
+
+    def authenticate_user(self, username: str, password: str):
+        user = self.user_store.get_user_by_username(username)
+        if not user:
+            return None, "用户不存在"
+        user = self.user_store.authenticate_user(username, password)
+        if user:
+            return UserDto.from_model(user), ""
+        return None, "密码错误"
+
+    def get_all_users(self) -> list[UserDto]:
+        users = self.user_store.get_all_users()
+        user_list = [UserDto.from_model(user) for user in users]
+        return user_list

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

@@ -0,0 +1,13 @@
+from stores.railway_costing_mysql import LogStore, ProjectQuotaStore, ProjectTaskStore
+
+# from stores.railway_costing_sqlserver import (
+#     LogStore,
+#     ProjectQuotaStore,
+#     ProjectTaskStore,
+# )
+
+from .user import UserStore
+from .project import ProjectStore
+from .budget import BudgetStore
+from .chapter import ChapterStore
+from .quota_input import QuotaInputStore

+ 195 - 0
SourceCode/IntelligentRailwayCosting/app/stores/budget.py

@@ -0,0 +1,195 @@
+from sqlalchemy.orm import aliased
+from sqlalchemy import and_, or_, func
+
+from core.models import TotalBudgetInfoModel, TotalBudgetItemModel, ChapterModel
+from tools import db_helper
+
+
+class BudgetStore:
+    def __init__(self):
+        self._database = None
+        self._db_session = None
+        pass
+
+    def get_budget_info(self, project_id: str):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            budgets = db_session.query(TotalBudgetInfoModel).all()
+            if budgets is None:
+                return None
+            return budgets
+
+    def get_budget_items(self, project_id: str, budget_id: int):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            if budget_id != 0:
+                budget_items = db_session.query(TotalBudgetItemModel).all()
+            else:
+                budget_items = (
+                    db_session.query(TotalBudgetItemModel)
+                    .filter(TotalBudgetItemModel.budget_id == budget_id)
+                    .all()
+                )
+            if budget_items is None:
+                return None
+            return budget_items
+
+    def get_budget_item_by_item_code(
+            self, project_id: str, budget_id: int, item_code: str
+    ):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            budget = (
+                db_session.query(
+                    TotalBudgetItemModel.budget_id,
+                    TotalBudgetItemModel.item_id,
+                    ChapterModel.item_code,
+                    ChapterModel.chapter,
+                    ChapterModel.section,
+                    ChapterModel.project_name,
+                    ChapterModel.item_type,
+                    ChapterModel.unit,
+                )
+                .join(
+                    ChapterModel, ChapterModel.item_id == TotalBudgetItemModel.item_id
+                )
+                .filter(
+                    and_(
+                        TotalBudgetItemModel.budget_id == budget_id,
+                        ChapterModel.item_code == item_code,
+                    )
+                )
+                .first()
+            )
+            if budget is None:
+                return None
+            return budget
+
+    def _build_children_count_subquery(self, model_class):
+        # 创建父节点和子节点的别名
+        parent = aliased(model_class, name="parent")
+        child = aliased(model_class, name="child")
+
+        # 子查询:计算每个节点的直接子节点数量
+        return (
+            self.db_session.query(
+                parent.item_code.label("parent_code"),
+                func.count(child.item_code).label("child_count"),
+            )
+            .outerjoin(
+                child,
+                or_(
+                    # 匹配形如01-01的格式
+                    child.item_code.like(parent.item_code + "-__"),
+                    # 匹配形如0101的格式
+                    child.item_code.like(parent.item_code + "__"),
+                ),
+            )
+            .group_by(parent.item_code)
+            .subquery()
+        )
+
+    def _build_budget_items_query(self, budget_id: int):
+        # 子查询:计算每个节点的直接子节点数量
+        children_count = self._build_children_count_subquery(ChapterModel)
+
+        return (
+            self.db_session.query(
+                TotalBudgetItemModel.budget_id,
+                TotalBudgetItemModel.item_id,
+                ChapterModel.item_code,
+                ChapterModel.chapter,
+                ChapterModel.section,
+                ChapterModel.project_name,
+                ChapterModel.item_type,
+                ChapterModel.unit,
+                func.coalesce(children_count.c.child_count, 0).label("children_count"),
+            )
+            .join(ChapterModel, ChapterModel.item_id == TotalBudgetItemModel.item_id)
+            .outerjoin(
+                children_count, children_count.c.parent_code == ChapterModel.item_code
+            )
+            .filter(TotalBudgetItemModel.budget_id == budget_id)
+            .distinct()
+        )
+
+    def get_top_budget_items(
+            self, project_id: str, budget_id: int, item_code: list[str] = None
+    ):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            query = self._build_budget_items_query(budget_id)
+
+            if item_code:
+                query = query.filter(ChapterModel.item_code.in_(item_code))
+            else:
+                query = query.filter(ChapterModel.item_code.like("__")).filter(
+                    ChapterModel.chapter.is_not(None)
+                )
+            query = query.order_by(ChapterModel.item_code)
+            items = query.all()
+            return items
+
+    def get_child_budget_items(
+            self, project_id: str, budget_id: int, parent_item_code: str
+    ):
+        # 构建子节点的模式:支持两种格式
+        # 1. 父级编号后跟-和两位数字(如:01-01)
+        # 2. 父级编号直接跟两位数字(如:0101)
+        pattern_with_dash = f"{parent_item_code}-__"
+        pattern_without_dash = f"{parent_item_code}__"
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            query = (
+                self._build_budget_items_query(budget_id)
+                .filter(
+                    or_(
+                        ChapterModel.item_code.like(pattern_with_dash),
+                        ChapterModel.item_code.like(pattern_without_dash),
+                    )
+                )
+                .order_by(ChapterModel.item_code)
+            )
+            items = query.all()
+            return items
+
+    def get_all_budget_items_not_children(
+            self, project_id: str, budget_id: int, item_code: str
+    ):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            # 添加叶子节点过滤条件,使用复用的子查询方法
+            children_count = self._build_children_count_subquery(ChapterModel)
+            query = (
+                self.db_session.query(
+                    TotalBudgetItemModel.budget_id,
+                    TotalBudgetItemModel.item_id,
+                    ChapterModel.item_code,
+                    ChapterModel.chapter,
+                    ChapterModel.section,
+                    ChapterModel.project_name,
+                    ChapterModel.item_type,
+                    ChapterModel.unit,
+                    func.coalesce(children_count.c.child_count, 0).label(
+                        "children_count"
+                    ),
+                )
+                .join(
+                    ChapterModel, ChapterModel.item_id == TotalBudgetItemModel.item_id
+                )
+                .outerjoin(
+                    children_count,
+                    children_count.c.parent_code == ChapterModel.item_code,
+                )
+                .filter(TotalBudgetItemModel.budget_id == budget_id)
+                .distinct()
+            )
+
+            query = query.filter(func.coalesce(children_count.c.child_count, 0) == 0)
+            # 如果指定了item_code,添加前缀匹配条件
+            if item_code:
+                pattern_with_dash = f"{item_code}-%"
+                query = query.filter(ChapterModel.item_code.like(pattern_with_dash))
+            query = query.order_by(ChapterModel.item_code)
+            items = query.all()
+            return items

+ 245 - 0
SourceCode/IntelligentRailwayCosting/app/stores/chapter.py

@@ -0,0 +1,245 @@
+from sqlalchemy.orm import aliased
+from sqlalchemy import or_, func, and_
+
+from core.dtos import ChapterDto
+from core.models import ChapterModel
+from tools import db_helper
+
+
+class ChapterStore:
+    def __init__(self):
+        self._database = None
+        self._db_session = None
+
+    def get_chapter(self, project_id: str, item_id: int):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            chapter = (
+                db_session.query(
+                    ChapterModel.item_id,
+                    ChapterModel.item_code,
+                    ChapterModel.chapter,
+                    ChapterModel.section,
+                    ChapterModel.project_name,
+                    ChapterModel.item_type,
+                    ChapterModel.unit,
+                )
+                .filter(ChapterModel.item_id == item_id)
+                .first()
+            )
+            if chapter is None:
+                return None
+            return chapter
+
+    def get_chapter_dto(self, project_id: str, item_id: int):
+        chapter = self.get_chapter(project_id, item_id)
+        if chapter is None:
+            return None
+        return ChapterDto.from_model(chapter)
+
+    def get_chapter_item_by_item_code(self, project_id: str, item_code: str):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            chapter = (
+                db_session.query(
+                    ChapterModel.item_id,
+                    ChapterModel.item_code,
+                    ChapterModel.chapter,
+                    ChapterModel.section,
+                    ChapterModel.project_name,
+                    ChapterModel.item_type,
+                    ChapterModel.unit,
+                )
+                .filter(ChapterModel.item_code == item_code)
+                .first()
+            )
+            if chapter is None:
+                return None
+            return chapter
+
+    def _build_children_count_subquery(self, model_class):
+        # 创建父节点和子节点的别名
+        parent = aliased(model_class, name="parent")
+        child = aliased(model_class, name="child")
+
+        # 子查询:计算每个节点的直接子节点数量
+        return (
+            self.db_session.query(
+                parent.item_code.label("parent_code"),
+                func.count(child.item_code).label("child_count"),
+            )
+            .outerjoin(
+                child,
+                or_(
+                    # 匹配形如01-01的格式
+                    child.item_code.like(parent.item_code + "-__"),
+                    # 匹配形如0101的格式
+                    child.item_code.like(parent.item_code + "__"),
+                ),
+            )
+            .group_by(parent.item_code)
+            .subquery()
+        )
+
+    def _build_chapter_items_query(self):
+        # 子查询:计算每个节点的直接子节点数量
+        children_count = self._build_children_count_subquery(ChapterModel)
+
+        return (
+            self.db_session.query(
+                ChapterModel.item_id,
+                ChapterModel.item_code,
+                ChapterModel.chapter,
+                ChapterModel.section,
+                ChapterModel.project_name,
+                ChapterModel.item_type,
+                ChapterModel.unit,
+                func.coalesce(children_count.c.child_count, 0).label("children_count"),
+            )
+            .outerjoin(
+                children_count, children_count.c.parent_code == ChapterModel.item_code
+            )
+            .distinct()
+        )
+
+    def get_task_chapter_items(self, project_id: str, item_code: str):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            query = (
+                self.db_session.query(
+                    ChapterModel.item_id,
+                    ChapterModel.item_code,
+                    ChapterModel.chapter,
+                    ChapterModel.section,
+                    ChapterModel.project_name,
+                    ChapterModel.item_type,
+                    ChapterModel.unit,
+                )
+                .filter(
+                    and_(
+                        ChapterModel.item_code != "0",
+                        ChapterModel.item_code.like(f"{item_code}%"),
+                    ),
+                )
+                .order_by(ChapterModel.item_code)
+            )
+            items = query.all()
+            return items
+
+    def get_top_chapter_items(self, project_id: str, item_code: list[str] = None):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            query = self._build_chapter_items_query()
+
+            if item_code:
+                query = query.filter(ChapterModel.item_code.in_(item_code))
+            else:
+                query = query.filter(ChapterModel.item_code.like("__")).filter(
+                    ChapterModel.chapter.is_not(None)
+                )
+            query = query.order_by(ChapterModel.item_code)
+            items = query.all()
+            return items
+
+    def get_child_chapter_items(
+        self, project_id: str, parent_item_code: str, task_first: bool
+    ):
+        # 构建子节点的模式:支持两种格式
+        # 1. 父级编号后跟-和两位数字(如:01-01)
+        # 2. 父级编号直接跟两位数字(如:0101)
+        pattern_with_dash = f"{parent_item_code}-__"
+        pattern_without_dash = f"{parent_item_code}__"
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            query = self._build_chapter_items_query()
+            query = (
+                query.filter(ChapterModel.item_code == parent_item_code)
+                if task_first
+                else query.filter(
+                    or_(
+                        ChapterModel.item_code.like(pattern_with_dash),
+                        ChapterModel.item_code.like(pattern_without_dash),
+                    )
+                )
+            )
+            query = query.order_by(ChapterModel.item_code)
+            items = query.all()
+            return items
+
+    def get_all_children_chapter_items(self, project_id: str, item_code: str):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            query = (
+                self._build_chapter_items_query()
+                .filter(ChapterModel.item_code.like(f"{item_code}%"))
+                .order_by(ChapterModel.item_code)
+            )
+            items = query.all()
+            return items
+
+    def get_all_parents_chapter_items(self, project_id: str, item_code: str):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            # 获取所有可能的父节点编码
+            parent_codes = []
+            current_code = item_code
+
+            # 按'-'分割item_code
+            parts = current_code.split("-")
+
+            # 处理第一个部分(如'0101')
+            first_part = parts[0]
+            for i in range(2, len(first_part) + 1, 2):
+                parent_codes.append(first_part[:i])
+
+            # 逐步拼接其他部分构建完整父节点列表
+            current_parent = first_part
+            for i in range(1, len(parts)):
+                current_parent = current_parent + "-" + parts[i]
+                parent_codes.append(current_parent)
+
+            if not parent_codes:
+                return []
+            print(parent_codes)
+            # 查询所有父节点
+            query = (
+                self._build_chapter_items_query()
+                .filter(ChapterModel.item_code.in_(parent_codes))
+                .order_by(ChapterModel.item_code)
+            )
+            items = query.all()
+            return items
+
+    def get_all_chapter_items_not_children(self, project_id: str, item_code: str):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as self.db_session:
+            # 添加叶子节点过滤条件,使用复用的子查询方法
+            children_count = self._build_children_count_subquery(ChapterModel)
+            query = (
+                self.db_session.query(
+                    ChapterModel.item_id,
+                    ChapterModel.item_code,
+                    ChapterModel.chapter,
+                    ChapterModel.section,
+                    ChapterModel.project_name,
+                    ChapterModel.item_type,
+                    ChapterModel.unit,
+                    func.coalesce(children_count.c.child_count, 0).label(
+                        "children_count"
+                    ),
+                )
+                .outerjoin(
+                    children_count,
+                    children_count.c.parent_code == ChapterModel.item_code,
+                )
+                .distinct()
+            )
+
+            query = query.filter(func.coalesce(children_count.c.child_count, 0) == 0)
+            # 如果指定了item_code,添加前缀匹配条件
+            if item_code:
+                pattern_with_dash = f"{item_code}-%"
+                query = query.filter(ChapterModel.item_code.like(pattern_with_dash))
+            query = query.order_by(ChapterModel.item_code)
+            items = query.all()
+            return items

+ 133 - 0
SourceCode/IntelligentRailwayCosting/app/stores/project.py

@@ -0,0 +1,133 @@
+from sqlalchemy import and_, or_
+from datetime import datetime
+from typing import Optional
+
+import tools.db_helper as db_helper
+from core import configs
+from core.dtos import ProjectDto
+from core.models.project import ProjectModel
+from core.models.team import TeamModel
+from core.user_session import UserSession
+
+
+class ProjectStore:
+    def __init__(self):
+        self._database = (
+            f"sqlserver_mian_{configs.app.version}"
+            if configs.app.use_version
+            else "sqlserver_mian"
+        )
+
+    def get_user_projects_paginated(
+        self,
+        page: int = 1,
+        page_size: int = 10,
+        keyword: Optional[str] = None,
+        start_time: Optional[datetime] = None,
+        end_time: Optional[datetime] = None,
+        can_edit: Optional[int] = 0,
+    ):
+        """
+        分页查询用户有权限的项目列表
+
+        Args:
+            page: 页码,从1开始
+            page_size: 每页数量
+            keyword: 关键字(模糊查询)
+            start_time: 开始时间
+            end_time: 结束时间
+            can_edit: 是否只显示可编辑的项目
+
+        Returns:
+            Tuple[total_count, projects]
+        """
+
+        # 构建基础查询
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            query = db_session.query(
+                ProjectModel.project_id,
+                ProjectModel.project_name,
+                ProjectModel.project_manager,
+                ProjectModel.design_stage,
+                # ProjectModel.project_description,
+                ProjectModel.short_name,
+                ProjectModel.project_version,
+                ProjectModel.project_type,
+                ProjectModel.unit,
+                ProjectModel.create_time,
+            ).distinct()
+            user = UserSession.get_current_user()
+            if not user.is_admin:
+                query = query.outerjoin(
+                    TeamModel, ProjectModel.project_id == TeamModel.project_id
+                )
+                if can_edit:
+                    query = query.filter(
+                        or_(
+                            ProjectModel.project_manager == user.username,
+                            and_(
+                                TeamModel.name == user.username,
+                                TeamModel.compilation_status == can_edit,
+                            ),
+                        )
+                    )
+                else:
+                    query = query.filter(
+                        or_(
+                            ProjectModel.project_manager == user.username,
+                            TeamModel.name == user.username,
+                        )
+                    )
+
+            # 添加过滤条件
+            if keyword:
+                query = query.filter(
+                    or_(
+                        ProjectModel.project_id.like(f"%{keyword}%"),
+                        ProjectModel.project_name.like(f"%{keyword}%"),
+                        ProjectModel.project_manager.like(f"%{keyword}%"),
+                        # ProjectModel.project_description.like(f'%{keyword}%'),
+                        ProjectModel.short_name.like(f"%{keyword}%"),
+                    )
+                )
+
+            if start_time:
+                query = query.filter(ProjectModel.create_time >= start_time)
+
+            if end_time:
+                query = query.filter(ProjectModel.create_time < end_time)
+
+            # 获取总记录数和数据
+            total_count = query.count()
+            projects = (
+                query.order_by(ProjectModel.create_time.desc())
+                .offset((page - 1) * page_size)
+                .limit(page_size)
+                .all()
+            )
+
+            return {"total": total_count, "data": projects}
+
+    def get_team_project_item_code(self, project_id: str, user_name: str):
+        with db_helper.sqlserver_query_session(self._database) as session:
+            db_session = session
+            data = (
+                db_session.query(TeamModel.item_code)
+                .filter(
+                    and_(
+                        TeamModel.project_id == project_id, TeamModel.name == user_name
+                    )
+                )
+                .first()
+            )
+            return data[0] if data else None
+
+    def get(self, project_id: str):
+        with db_helper.sqlserver_query_session(self._database) as session:
+            db_session = session
+            data = (
+                db_session.query(ProjectModel)
+                .filter(ProjectModel.project_id == project_id)
+                .first()
+            )
+            return ProjectDto.from_model(data) if data else None

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

@@ -0,0 +1,238 @@
+from sqlalchemy import and_, or_
+from datetime import datetime
+from typing import Optional
+
+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,
+        project_id: str,
+        budget_id: int,
+        item_id: int,
+        page: int = 1,
+        page_size: int = 10,
+        keyword: Optional[str] = None,
+    ):
+        """分页查询定额输入列表
+
+        Args:
+            page: 页码,从1开始
+            page_size: 每页数量
+            project_id: 总概算序号
+            budget_id: 总概算序号
+            item_id: 条目序号
+            keyword: 关键字
+
+        Returns:
+        """
+        self._database = project_id
+        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(self, project_id: str, budget_id: int, item_id: int, quota_code: str):
+        self._database = project_id
+        with db_helper.sqlserver_query_session(self._database) as db_session:
+            model = (
+                db_session.query(QuotaInputModel)
+                .filter(
+                    QuotaInputModel.budget_id == budget_id,
+                    QuotaInputModel.item_id == item_id,
+                    QuotaInputModel.quota_code == quota_code,
+                )
+                .first()
+            )
+            if model is None:
+                return None
+            return QuotaInputDto.from_model(model)
+
+    def create_quota(self, project_id: str, dto: QuotaInputDto) -> QuotaInputDto:
+        """创建定额输入
+
+        Args:
+            project_id
+            dto: 定额输入DTO
+
+        Returns:
+            QuotaInputDto
+        """
+        self._database = project_id
+        with db_helper.sqlserver_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,
+                # 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().strftime("%Y/%m/%d %H:%M:%S"),
+                # quota_consumption=dto.quota_consumption,0
+                # 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, project_id: str, dto: QuotaInputDto
+    ) -> Optional[QuotaInputDto]:
+        """更新定额输入
+
+        Args:
+            project_id:
+            dto: 定额输入DTO
+
+        Returns:
+            QuotaInputDto or None
+        """
+        self._database = project_id
+        with db_helper.sqlserver_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 = str(dto.project_quantity)
+            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 = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
+            # 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.merge(model)
+
+            return QuotaInputDto.from_model(model)
+
+    def delete_quota(self, quota_id: int) -> bool:
+        """删除定额输入
+
+        Args:
+            quota_id: 定额序号
+
+        Returns:
+            bool
+        """
+        with db_helper.sqlserver_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

+ 3 - 0
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/__init__.py

@@ -0,0 +1,3 @@
+from .project_task import ProjectTaskStore
+from .project_quota import ProjectQuotaStore
+from .log import LogStore

+ 116 - 0
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_mysql/log.py

@@ -0,0 +1,116 @@
+from typing import List, Optional, Dict, Any
+from datetime import datetime
+from sqlalchemy import and_
+import tools.db_helper as db_helper
+
+from core.models import LogModel
+
+
+class LogStore:
+    def __init__(self):
+        # self._database= None
+        self._database = "iwb_railway_costing_v1"
+
+    def query_logs_paginated(
+        self,
+        page: int = 1,
+        page_size: int = 10,
+        username: Optional[str] = None,
+        operation_type: Optional[str] = None,
+        operation_module: Optional[str] = None,
+        operation_result: Optional[int] = None,
+        start_time: Optional[datetime] = None,
+        end_time: Optional[datetime] = None,
+    ) -> Dict[str, Any]:
+        """
+        分页查询日志记录
+        :param page: 页码
+        :param page_size: 每页记录数
+        :param username: 用户名(支持模糊查询)
+        :param operation_type: 操作类型
+        :param operation_module: 操作模块
+        :param operation_result: 操作结果
+        :param start_time: 开始时间
+        :param end_time: 结束时间
+        :return: 包含总记录数和日志列表的字典
+        """
+        with db_helper.mysql_query_session(self._database) as db_session:
+            query = db_session.query(LogModel)
+            # 构建查询条件
+            conditions = []
+            if username:
+                conditions.append(LogModel.username.like(f"%{username}%"))
+            if operation_type:
+                conditions.append(LogModel.operation_type == operation_type)
+            if operation_module:
+                conditions.append(LogModel.operation_module == operation_module)
+            if operation_result is not None:
+                conditions.append(LogModel.operation_result == operation_result)
+            if start_time:
+                conditions.append(LogModel.created_at >= start_time)
+            if end_time:
+                conditions.append(LogModel.created_at < end_time)
+
+            if conditions:
+                query = query.filter(and_(*conditions))
+
+            # 计算总记录数
+            total = query.count()
+
+            # 分页并按创建时间倒序排序
+            logs = (
+                query.order_by(LogModel.created_at.desc())
+                .offset((page - 1) * page_size)
+                .limit(page_size)
+                .all()
+            )
+
+            return {"total": total, "data": logs}
+
+    def insert_log(
+        self,
+        username: str,
+        operation_type: str,
+        operation_desc: Optional[str] = None,
+        operation_result: Optional[int] = None,
+        operation_module: Optional[str] = None,
+        operation_data: Optional[str] = None,
+        data_changes: Optional[str] = None,
+        operation_ip: Optional[str] = None,
+    ) -> LogModel:
+        """
+        插入单条日志记录
+        :param username: 用户名
+        :param operation_type: 操作类型
+        :param operation_desc: 操作描述
+        :param operation_result: 操作结果
+        :param operation_module: 操作模块
+        :param operation_data: 操作数据
+        :param data_changes: 数据变更记录
+        :param operation_ip: 操作IP
+        :return: 创建的日志记录
+        """
+        log = LogModel(
+            username=username,
+            operation_type=operation_type,
+            operation_desc=operation_desc,
+            operation_result=operation_result,
+            operation_module=operation_module,
+            operation_data=operation_data,
+            data_changes=data_changes,
+            operation_ip=operation_ip,
+        )
+        with db_helper.mysql_session(self._database) as db_session:
+            db_session.add(log)
+            return log
+
+    def batch_insert_logs(self, logs: List[Dict[str, Any]]) -> List[LogModel]:
+        """
+        批量插入日志记录
+        :param logs: 日志记录列表
+        :return: 创建的日志记录列表
+        """
+        log_models = [LogModel(**log) for log in logs]
+        with db_helper.mysql_session(self._database) as db_session:
+            db_session.add_all(log_models)
+            return log_models

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels