yue 5 сар өмнө
parent
commit
f1c7e24136
30 өөрчлөгдсөн 19050 нэмэгдсэн , 268 устгасан
  1. 58 0
      SourceCode/IntelligentRailwayCosting/.script/db
  2. 8 8
      SourceCode/IntelligentRailwayCosting/.script/init.sql
  3. 7 7
      SourceCode/IntelligentRailwayCosting/.script/init_sqlserver.sql
  4. 18220 0
      SourceCode/IntelligentRailwayCosting/.script/project_data_test.sql
  5. 21 13
      SourceCode/IntelligentRailwayCosting/app/config.yml
  6. 21 8
      SourceCode/IntelligentRailwayCosting/app/core/configs/app_config.py
  7. 40 12
      SourceCode/IntelligentRailwayCosting/app/core/dtos/excel_parse.py
  8. 7 3
      SourceCode/IntelligentRailwayCosting/app/core/dtos/project_quota.py
  9. 9 7
      SourceCode/IntelligentRailwayCosting/app/core/enum/task.py
  10. 4 4
      SourceCode/IntelligentRailwayCosting/app/core/models/project_quota.py
  11. 3 3
      SourceCode/IntelligentRailwayCosting/app/core/models/project_task.py
  12. 3 0
      SourceCode/IntelligentRailwayCosting/app/executor/__init__.py
  13. 49 31
      SourceCode/IntelligentRailwayCosting/app/executor/task_runner.py
  14. 8 0
      SourceCode/IntelligentRailwayCosting/app/main.py
  15. 2 0
      SourceCode/IntelligentRailwayCosting/app/routes/__init__.py
  16. 135 0
      SourceCode/IntelligentRailwayCosting/app/routes/excel_test.py
  17. 0 1
      SourceCode/IntelligentRailwayCosting/app/routes/project.py
  18. 15 4
      SourceCode/IntelligentRailwayCosting/app/routes/project_task.py
  19. 34 30
      SourceCode/IntelligentRailwayCosting/app/services/project_task.py
  20. 1 1
      SourceCode/IntelligentRailwayCosting/app/stores/project.py
  21. 11 12
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_quota.py
  22. 7 3
      SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_task.py
  23. 92 0
      SourceCode/IntelligentRailwayCosting/app/test/config.yml
  24. 107 0
      SourceCode/IntelligentRailwayCosting/app/test/database_test.py
  25. 1 1
      SourceCode/IntelligentRailwayCosting/app/test/sqlserver_test.py
  26. 1 1
      SourceCode/IntelligentRailwayCosting/app/views/login.py
  27. 6 0
      SourceCode/IntelligentRailwayCosting/app/views/static/base/css/styles.css
  28. 3 2
      SourceCode/IntelligentRailwayCosting/app/views/static/base/js/utils.js
  29. 166 108
      SourceCode/IntelligentRailwayCosting/app/views/static/project/budget_info.js
  30. 11 9
      SourceCode/IntelligentRailwayCosting/app/views/templates/project/budget_info.html

+ 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

+ 8 - 8
SourceCode/IntelligentRailwayCosting/.script/init.sql

@@ -14,13 +14,13 @@ CREATE TABLE IF NOT EXISTS project_task (
     file_path text COMMENT '文件路径',
     collect_status TINYINT NOT NULL DEFAULT 0 COMMENT '采集状态(0:未开始, 1:进行中, 2:已完成, 3:采集失败)',
     collect_time DATETIME COMMENT '采集时间',
-    collect_error VARCHAR(1000) COMMENT '采集错误信息',
+    collect_error VARCHAR(4000) COMMENT '采集错误信息',
     process_status TINYINT NOT NULL DEFAULT 0 COMMENT '处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)',
     process_time DATETIME COMMENT '处理时间',
-    process_error VARCHAR(1000) COMMENT '处理错误信息',
+    process_error VARCHAR(4000) COMMENT '处理错误信息',
     send_status TINYINT NOT NULL DEFAULT 0 COMMENT '发送状态(0:未发送,1:发送中 ,2:已发送, 3:发送失败)',
     send_time DATETIME COMMENT '发送时间',
-    send_error VARCHAR(1000) 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 '删除时间',
@@ -48,17 +48,17 @@ CREATE TABLE IF NOT EXISTS project_quota (
     entry_name VARCHAR(255) COMMENT '工程或费用项目名称',
     units VARCHAR(20) COMMENT '单位',
     amount FLOAT COMMENT '数量',
-    ex_file VARCHAR(50) COMMENT 'excel文件',
+    ex_file VARCHAR(4000) COMMENT 'excel文件',
     ex_cell VARCHAR(50) COMMENT '数量单元格位置,例如"C17"',
-    ex_row VARCHAR(1000) COMMENT '该行内容,由逗号连接多个单元格得到',
+    ex_row VARCHAR(4000) COMMENT '该行内容,由逗号连接多个单元格得到',
     ex_unit VARCHAR(50) COMMENT 'excel中给出的单位',
     ex_amount FLOAT COMMENT 'excel中给出的数量',
     process_status TINYINT NOT NULL DEFAULT 0 COMMENT '处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)',
     process_time DATETIME COMMENT '处理时间',
-    process_error VARCHAR(1000) COMMENT '处理错误信息',
+    process_error VARCHAR(4000) COMMENT '处理错误信息',
     send_status TINYINT NOT NULL DEFAULT 0 COMMENT '发送状态(0:未发送,1:发送中 ,2:已发送, 3:发送失败)',
     send_time DATETIME COMMENT '发送时间',
-    send_error VARCHAR(1000) 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 '删除时间',
@@ -80,7 +80,7 @@ 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_desc VARCHAR(4000) COMMENT '操作描述',
     operation_result TINYINT COMMENT '操作结果(0:失败, 1:成功)',
     operation_module VARCHAR(100) COMMENT '操作模块',
     operation_data TEXT COMMENT '操作数据',

+ 7 - 7
SourceCode/IntelligentRailwayCosting/.script/init_sqlserver.sql

@@ -47,13 +47,13 @@ CREATE TABLE [dbo].[project_task] (
     [file_path] NVARCHAR(MAX) NULL,
     [collect_status] INT NOT NULL DEFAULT 0,
     [collect_time] DATETIME NULL,
-    [collect_error] NVARCHAR(1000) NULL,
+    [collect_error] NVARCHAR(4000) NULL,
     [process_status] INT NOT NULL DEFAULT 0,
     [process_time] DATETIME NULL,
-    [process_error] NVARCHAR(1000) NULL,
+    [process_error] NVARCHAR(4000) NULL,
     [send_status] INT NOT NULL DEFAULT 0,
     [send_time] DATETIME NULL,
-    [send_error] NVARCHAR(1000) NULL,
+    [send_error] NVARCHAR(4000) NULL,
     [is_del] INT NOT NULL DEFAULT 0,
     [deleted_by] NVARCHAR(50) NULL,
     [deleted_at] DATETIME NULL,
@@ -86,17 +86,17 @@ CREATE TABLE [dbo].[project_quota] (
     [entry_name] NVARCHAR(255) NULL,
     [units] NVARCHAR(20) NULL,
     [amount] FLOAT NULL,
-    [ex_file] NVARCHAR(50) NULL,
+    [ex_file] NVARCHAR(4000) NULL,
     [ex_cell] NVARCHAR(50) NULL,
-    [ex_row] NVARCHAR(1000) NULL,
+    [ex_row] NVARCHAR(4000) NULL,
     [ex_unit] NVARCHAR(50) NULL,
     [ex_amount] FLOAT NULL,
     [process_status] INT NOT NULL DEFAULT 0,
     [process_time] DATETIME NULL,
-    [process_error] NVARCHAR(1000) NULL,
+    [process_error] NVARCHAR(4000) NULL,
     [send_status] INT NOT NULL DEFAULT 0,
     [send_time] DATETIME NULL,
-    [send_error] NVARCHAR(1000) NULL,
+    [send_error] NVARCHAR(4000) NULL,
     [is_del] INT NOT NULL DEFAULT 0,
     [deleted_by] NVARCHAR(50) NULL,
     [deleted_at] DATETIME NULL,

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 18220 - 0
SourceCode/IntelligentRailwayCosting/.script/project_data_test.sql


+ 21 - 13
SourceCode/IntelligentRailwayCosting/app/config.yml

@@ -4,7 +4,9 @@ app:
   version: '2020' # 应用版本 2020|2024
   use_version: true
   source_path: './temp_files'
-  collect_api_url: 'http://192.168.0.104:8020/api'
+  task_api_url: 'http://127.0.0.1:5123/api/v1'
+  task_max_projects: 1 # 最大项目数同时运行
+  task_interval: 60  # 秒
 db:
   # SQL Server 配置
   # SQL Server 2008:'{SQL Server}' 或 '{SQL Server Native Client 10.0}'
@@ -16,23 +18,23 @@ db:
   # 打开以后,点开上方"驱动程序"。 就可以看到系统所安装的ODBC驱动程序
   sqlserver_mian_2024:
     driver: '{ODBC Driver 17 for SQL Server}'
-    server: shvber.com,50535
-    username: sa
-    password: Iwb2017
+    server: 192.168.0.81,1433
+    username: iwb
+    password: 123456Qsc
     database: Iwb_RecoData2024
     trusted_connection: false
   sqlserver_mian_2020:
     driver: '{ODBC Driver 17 for SQL Server}'
-    server: shvber.com,50535
-    username: sa
-    password: Iwb2017
+    server: 192.168.0.81,1433
+    username: iwb
+    password: 123456Qsc
     database: Iwb_RecoData2020
     trusted_connection: false
   Iwb_RailwayCosting:
     driver: '{ODBC Driver 17 for SQL Server}'
-    server: shvber.com,50535
-    username: sa
-    password: Iwb2017
+    server: 192.168.0.81,1433
+    username: iwb
+    password: 123456Qsc
     database: iwb_railway_costing_v1
     trusted_connection: false
   Iwb_RecoData2024:
@@ -44,9 +46,12 @@ db:
     trusted_connection: false
   Iwb_RecoData2020:
     driver: '{ODBC Driver 17 for SQL Server}'
-    server: shvber.com,50535
-    username: sa
-    password: Iwb2017
+    # server: shvber.com,50535
+    # username: sa
+    # password: Iwb2017
+    server: 192.168.0.81,1433
+    username: iwb
+    password: 123456Qsc
     database: Iwb_RecoData2020
     trusted_connection: false
   # MySQL 配置
@@ -86,4 +91,7 @@ fastgpt_ai:
     app_02_2024:
       api_url: http://192.168.0.104:8020/api
       api_key: fastgpt-o4CF7Pu1FRTvHjWFqeNcClBS6ApyflNfkBGXo9p51fuBMAX1L0erU8yz8
+logger:
+  file_path: './logs/'
+  level: 'debug'
 

+ 21 - 8
SourceCode/IntelligentRailwayCosting/app/core/configs/app_config.py

@@ -1,10 +1,12 @@
 class AppConfig:
     """应用配置管理类"""
-    _name = None
-    _version = None
-    _use_version = None
-    _source_path = None
-    _collect_api_url = None
+    _name = ""
+    _version = "2020"
+    _use_version = True
+    _source_path = ""
+    _task_api_url = ""
+    _task_max_projects_count = 10
+    _task_interval = 300 # 任务执行间隔时间,单位秒
 
     @property
     def name(self):
@@ -22,8 +24,17 @@ class AppConfig:
     def source_path(self)->str:
         return self._source_path
     @property
-    def collect_api_url(self)->str:
-        return self._collect_api_url
+    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
+
+
 
     def update_config(self, config):
         """更新应用配置
@@ -35,4 +46,6 @@ class AppConfig:
         self._version = config.get('version')
         self._use_version = config.get('use_version',False)
         self._source_path = config.get('source_path','./temp_files')
-        self._collect_api_url = config.get('collect_api_url','')
+        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))

+ 40 - 12
SourceCode/IntelligentRailwayCosting/app/core/dtos/excel_parse.py

@@ -12,8 +12,8 @@ class ExcelParseZgsDto:
     @classmethod
     def from_dto(cls,dto: TotalBudgetInfoDto):
         return cls(
-            zgs_id=dto.zgs_id,
-            zgs_name=dto.zgs_name
+            zgs_id=dto.budget_id,
+            zgs_name=dto.budget_code
         )
     def to_dict(self):
         return {
@@ -36,7 +36,7 @@ class ExcelParseItemDto:
         return cls(
             item_id=dto.item_id,
             item_code=dto.item_code,
-            item_name=dto.item_name
+            item_name=dto.project_name
         )
     def to_dict(self):
         return {
@@ -90,10 +90,10 @@ class ExcelParseDto:
             "project_name":self.project_name,
             "project_stage":self.project_stage,
             "selected_zgs_id":self.selected_zgs_id,
+            "file_excel":[file.to_dict() for file in self.file_excel],
             "zgs_list":[zgs.to_dict() for zgs in self.zgs_list],
             "hierarchy":[item.to_dict() for item in self.hierarchy],
             "components":[item.to_dict() for item in self.components],
-            "file_excel":[file.to_dict() for file in self.file_excel]
         }
 
 class ExcelParseResultDataDto:
@@ -135,6 +135,7 @@ class ExcelParseResultDataDto:
         self.dinge_code = dinge_code
         self.units = units
         self.amount = amount
+        self.ex_file = ex_file_id
         self.ex_file_id = ex_file_id
         self.ex_cell = ex_cell
         self.ex_row = ex_row
@@ -149,23 +150,42 @@ class ExcelParseResultDataDto:
             zgs_code=data.get('zgs_code', ''),
             item_id=data.get('item_id', 0),
             item_code=data.get('item_code', ''),
-            entry_name=data.get('name', ''),
+            entry_name=data.get('entry_name', ''),
             dinge_code=data.get('dinge_code', ''),
             units=data.get('units', ''),
             amount=data.get('amount', 0.0),
-            ex_file_id= base64.b64decode(data.get('ex_file_id', '').encode()).decode() if data.get('ex_file_id', '') else '',
+            ex_file_id= base64.b64decode(data.get('ex_file_id', '').encode('utf-8')).decode('utf-8') 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,
+            "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,
-                 data:list[ExcelParseResultDataDto],
-                 reason:str
+                 result:int=-1,
+                 reason:str="",
+                 data:list[ExcelParseResultDataDto]=None,
     ):
         self.task_id = task_id
         self.result = result  #-1-失败;0-运行中;1-成功
@@ -178,6 +198,14 @@ class ExcelParseResultDto:
         return cls(
             task_id=response.get('task_id', 0),
             result=response.get('result', 0),
-            data=data,
-            reason=response.get('reason', '')
-        )
+            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 []
+        }

+ 7 - 3
SourceCode/IntelligentRailwayCosting/app/core/dtos/project_quota.py

@@ -11,7 +11,7 @@ class ProjectQuotaDto(BaseModel):
     task_id:Optional[int] = None
     project_id: str
     budget_id: int
-    budget_code: str
+    budget_code: Optional[str] =None
     item_id: int
     item_code: str
     quota_code: Optional[str] = None
@@ -23,6 +23,12 @@ class ProjectQuotaDto(BaseModel):
     ex_row: Optional[str] = None
     ex_unit: Optional[str] = None
     ex_amount: Optional[float] = None
+    process_status: int = 0,
+    process_time: Optional[datetime] = None
+    process_error: Optional[str] = None
+    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
@@ -51,8 +57,6 @@ class ProjectQuotaDto(BaseModel):
             ex_amount=model.ex_amount,
         )
 
-
-
     def to_dict(self) -> dict:
         """转换为字典格式"""
         return self.model_dump()

+ 9 - 7
SourceCode/IntelligentRailwayCosting/app/core/enum/task.py

@@ -4,9 +4,9 @@ class TaskStatusEnum(Enum):
     NEW = 0
     WAIT = 1
     PROCESSING = 2
-    SUCCESS = 3
-    CANCELED = 4
-    FAILURE = 5
+    SUCCESS = 200
+    FAILURE = 4
+    CANCELED = 5
     CHANGE = 6
 
 
@@ -25,15 +25,16 @@ class TaskStatusEnum(Enum):
             "运行中": cls.PROCESSING,
             "运行成功": cls.SUCCESS,
             "运行失败": cls.FAILURE,
-            "取消": cls.CANCELED,
+            "取消运行": cls.CANCELED,
             "已修改": cls.CHANGE
         }
 
 class SendStatusEnum(Enum):
     NEW = 0
     PROCESSING = 1
-    SUCCESS = 2
-    FAILURE = 3
+    SUCCESS = 200
+    FAILURE = 2
+    CHANGE = 3
 
     @classmethod
     def get_name(cls, status: int):
@@ -48,5 +49,6 @@ class SendStatusEnum(Enum):
             "未发送": cls.NEW,
             "发送中": cls.PROCESSING,
             "发送成功": cls.SUCCESS,
-            "发送失败": cls.FAILURE
+            "发送失败": cls.FAILURE,
+            "已修改": cls.CHANGE
         }

+ 4 - 4
SourceCode/IntelligentRailwayCosting/app/core/models/project_quota.py

@@ -17,17 +17,17 @@ class ProjectQuotaModel(Base):
     entry_name = Column(String(255), comment='工程或费用项目名称')
     units = Column(String(20), comment='单位')
     amount = Column(Float, comment='数量')
-    ex_file = Column(String(50), comment='excel⽂件')
+    ex_file = Column(String(5000), comment='excel⽂件')
     ex_cell = Column(String(50), comment='数量单元格位置,例如"C17"')
-    ex_row = Column(String(1000), comment='该⾏内容,由逗号连接多个单元格得到')
+    ex_row = Column(String(5000), comment='该⾏内容,由逗号连接多个单元格得到')
     ex_unit = Column(String(50), comment='excel中给出的单位')
     ex_amount = Column(Float, comment='excel中给出的数量')
     process_status = Column(Integer, nullable=False, default=0, comment='处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)')
     process_time = Column(DateTime, comment='处理时间')
-    process_error = Column(String(1000), 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(1000), 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='删除时间')

+ 3 - 3
SourceCode/IntelligentRailwayCosting/app/core/models/project_task.py

@@ -17,13 +17,13 @@ class ProjectTaskModel(Base):
     file_path = Column(Text, comment='文件路径')
     collect_status = Column(Integer, nullable=False, default=0, comment='采集状态(0:未开始, 1:进行中, 2:已完成, 3:采集失败)')
     collect_time = Column(DateTime, comment='采集时间')
-    collect_error = Column(String(1000), comment='采集错误信息')
+    collect_error = Column(String(5000), comment='采集错误信息')
     process_status = Column(Integer, nullable=False, default=0, comment='处理状态(0:未处理,1:处理中, 2:已处理, 3:处理失败)')
     process_time = Column(DateTime, comment='处理时间')
-    process_error = Column(String(1000), 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(1000), 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='删除时间')

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

@@ -3,6 +3,8 @@ from .processor import Processor
 from .sender import Sender
 from core.dtos import ProjectTaskDto, ProjectQuotaDto
 
+def init():
+    TaskRunner.run()
 
 def run_task(task:ProjectTaskDto):
     return TaskRunner.run(task)
@@ -27,6 +29,7 @@ def send_quota(quota:ProjectQuotaDto):
 
 
 __all__=[
+    'init',
     'run_task',
     'cancel_task',
     'send_task',

+ 49 - 31
SourceCode/IntelligentRailwayCosting/app/executor/task_runner.py

@@ -11,7 +11,8 @@ class TaskRunner:
     _is_running = {}
     _task_wait_list = {}
     _running_projects = set()
-    _max_concurrent_projects = 10
+    _max_concurrent_projects = configs.app.task_max_projects_count
+    _task_sleep_interval = configs.app.task_interval
     _logger = utils.get_logger()
     _project_store = ProjectStore()
     _budget_store = BudgetStore()
@@ -31,6 +32,7 @@ class TaskRunner:
                 cls._task_wait_list[project_id] = []
             cls._logger.info(f"添加到待运行队列:{task.task_name}")
             cls._task_wait_list[project_id].append(task)
+            # cls._task_store.update_task_status(task.id,TaskStatusEnum.WAIT.value)
         
         if task and task.project_id in cls._is_running and cls._is_running[task.project_id]:
             return
@@ -43,8 +45,9 @@ class TaskRunner:
                 for project_id in cls._task_wait_list.keys():
                     cls._execute_project_tasks(project_id)
             else:
+                cls._logger.info(f"项目运行队列[{cls._max_concurrent_projects}]已满,等待{cls._task_sleep_interval}秒后同步数据运行")
                 cls._sync_wait_list()
-                sleep(60*10)
+                sleep(cls._task_sleep_interval)
                 return
         else:
             cls._execute_project_tasks(task.project_id)
@@ -80,6 +83,7 @@ class TaskRunner:
             
             # 创建新线程执行任务
             thread = threading.Thread(target=execute_tasks)
+            thread.daemon = True
             thread.start()
         except Exception as e:
             cls._logger.error(f"执行项目任务失败:{project_id}, {str(e)}")
@@ -109,34 +113,42 @@ class TaskRunner:
             return None
         except Exception as e:
             msg = f"同步待运行队列失败,原因:{e}"
-            cls._logger.error(f"同步待运行队列失败,原因:{e}")
+            cls._logger.error(msg)
             return msg
 
     @classmethod
     def cancel(cls, task:ProjectTaskDto):
         try:
             cls._logger.info(f"开始取消运行任务:{task.id}")
-            res = cls._call_api(cls._task_cancel_url,{"task_id":task.id})
-            if res.result==-1:
-                cls._task_store.update_task_status(task.id,TaskStatusEnum.FAILURE.value, res.reason)
-                return res.reason
-            project = cls._project_store.get(task.project_id)
-            if not project:
-                cls._logger.error(f"取消运行任务:{task.id}失败,原因:项目不存在")
-                return "项目不存在"
-            if res.data and len(res.data)>0:
-                cls._insert_data(task,project,res.data)
-            if res.result == 0 :
-                cls._logger.info(f"取消运行任务:{task.id}成功")
+            if task.process_status == TaskStatusEnum.PROCESSING.value:
+                res = cls._call_api(cls._task_cancel_url,{"task_id":task.id})
+                if res.result==-1:
+                    cls._task_store.update_task_status(task.id,TaskStatusEnum.FAILURE.value, res.reason)
+                    return res.reason
+                project = cls._project_store.get(task.project_id)
+                if not project:
+                    cls._logger.error(f"取消运行任务:{task.id}失败,原因:项目不存在")
+                    return "项目不存在"
+                if res.data and len(res.data)>0:
+                    cls._insert_data(task,project,res.data)
+                if res.result == 0 :
+                    cls._logger.info(f"取消运行任务:{task.id}成功")
+                    cls._task_store.update_task_status(task.id, TaskStatusEnum.CANCELED.value)
+                elif res.result == 1:
+                    cls._logger.info(f"取消运行任务失败:{task.id}已运行完成")
+                    cls._task_store.update_task_status(task.id, TaskStatusEnum.SUCCESS.value)
+                    return f"取消失败,任务已运行完成"
+            elif task.process_status == TaskStatusEnum.WAIT.value:
+                cls._task_wait_list[task.project_id].remove(task)
                 cls._task_store.update_task_status(task.id, TaskStatusEnum.CANCELED.value)
-            elif res.result == 1:
-                cls._logger.info(f"取消运行任务失败:{task.id}已运行完成")
-                cls._task_store.update_task_status(task.id, TaskStatusEnum.SUCCESS.value)
-                return f"取消失败,任务已运行完成"
+                cls._logger.info(f"取消运行任务:{task.id}成功")
+            else:
+                cls._logger.info(f"取消运行任务:{task.id}失败,原因:任务状态错误")
+                return "任务状态错误"
             return None
         except Exception as e:
             msg = f"取消运行任务失败,原因:{e}"
-            cls._logger.error(f"取消运行任务失败,原因:{e}")
+            cls._logger.error(msg)
             return msg
 
     @classmethod
@@ -201,7 +213,7 @@ class TaskRunner:
                     cls._query_task(task, project)
         except Exception as e:
             msg = f"任务状态查询失败,原因:{e}"
-            cls._logger.error(f"任务状态查询失败,原因:{e}")
+            cls._logger.error(msg)
             raise Exception(msg)
 
     @classmethod
@@ -217,7 +229,7 @@ class TaskRunner:
             return files, ''
         except Exception as e:
             msg = f"读取文件失败,原因:{e}"
-            cls._logger.error(f"读取文件失败,原因:{e}")
+            cls._logger.error(msg)
             return None,msg
 
     @classmethod
@@ -231,8 +243,8 @@ class TaskRunner:
                 version=configs.app.version or "2020",
                 project_id=task.project_id,
                 project_name=project.project_name,
-                project_stage=project.project_stage,
-                selected_zgs_id=task.selected_zgs_id or 0,
+                project_stage=project.design_stage,
+                selected_zgs_id=task.budget_id or 0,
                 zgs_list=budgets_data,
                 hierarchy=parents_data,
                 components=children_data,
@@ -240,26 +252,30 @@ class TaskRunner:
             )
             return data, ""
         except Exception as e:
-            msg = f"解析文件失败,原因:{e}"
-            cls._logger.error(f"解析文件失败,原因:{e}")
+            msg = f"构建API BODY,原因:{e}"
+            cls._logger.error(msg)
             return None,msg
 
     @classmethod
     def _call_api(cls,api_url,data)->ExcelParseResultDto:
        try:
-            url = f"{configs.app.collect_api_url}{api_url}"
+            url = f"{configs.app.task_api_url}{api_url}"
+            if isinstance(data, ExcelParseDto):
+                data = data.to_dict()
+            cls._logger.debug(f"调用接口:{url},data:{data}")
             response = requests.post(url, headers={"Content-Type": "application/json"}, json=data)
+            cls._logger.debug(f"调用接口返回[{response.status_code}]:{response.text}")
             if response.status_code == 200:
                 result = response.json()
                 result_dto = ExcelParseResultDto.from_dict(result)
-                cls._logger.debug(f"调用接口成功:{result_dto}")
+                cls._logger.debug(f"调用接口成功")
                 return result_dto
             else:
-                cls._logger.error(f"调用接口失败,原因:{response.text}")
+                cls._logger.error("调用接口失败")
                 raise Exception(response.text)
        except Exception as e:
             msg = f"调用接口失败,原因:{e}"
-            cls._logger.error(f"调用接口:{msg}")
+            cls._logger.error(msg)
             raise Exception(msg)
 
     @classmethod
@@ -268,7 +284,9 @@ class TaskRunner:
             cls._logger.debug(f"开始插入数据:{task.task_name}")
 
             for item in data:
+                cls._logger.debug(f"数据:{item.to_dict()}")
                 quota =  ProjectQuotaDto(
+                    task_id=task.id,
                     budget_id=item.zgs_id,
                     budget_code=item.zgs_code,
                     project_id=project.project_id,
@@ -290,7 +308,7 @@ class TaskRunner:
             return True
         except Exception as e:
             msg = f"插入数据失败,原因:{e}"
-            cls._logger.error(f"插入数据失败,原因:{e}")
+            cls._logger.error(msg)
             return False,msg
 
     @classmethod

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

@@ -1,10 +1,18 @@
+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__':

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

@@ -1,4 +1,5 @@
 from routes.auth import auth_api
+from routes.excel_test import test_api
 from routes.log import log_api
 from routes.project import project_api
 from routes.project_task import project_task_api
@@ -12,3 +13,4 @@ def register_api(app):
     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(test_api, url_prefix=f"{url_prefix}/v1")

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

@@ -0,0 +1,135 @@
+from flask import Blueprint, request, jsonify
+import random
+import string
+
+from core.dtos import ExcelParseResultDto, ExcelParseResultDataDto
+
+test_api = Blueprint('test_api', __name__)
+
+test_data_dic={}
+task_count_dic={}
+
+@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)
+
+@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)
+
+@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_list = task_data.get('zgs_list', None)
+    if not selected_zgs_id and zgs_list:
+        selected_zgs = random.choice(zgs_list)
+        selected_zgs_id = selected_zgs.get('id')
+    
+    file_excel = task_data.get('file_excel', None)
+    # 从file_excel随机选择文件
+    ex_file_id = ""
+    if file_excel:
+        file = random.choice(file_excel)
+        ex_file_id = file.get('file_id')
+    
+    # 随机生成各种编码和名称
+    zgs_code = f"ZGS_{''.join(random.choices(string.digits, k=3))}"
+    item_id = random.randint(100, 999)
+    item_code = f"ITEM{''.join(random.choices(string.digits, k=3))}"
+    project_types = ['路基工程', '桥梁工程', '隧道工程', '轨道工程', '站场工程']
+    entry_name = f"{random.choice(project_types)}{random.randint(1,100)}号"
+    dinge_code = f"DE{''.join(random.choices(string.digits, k=3))}"
+    
+    # 随机选择单位和数量
+    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)}"
+    
+    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,
+            dinge_code=dinge_code,
+            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.1:
+            result = -1  # 10%概率失败
+        elif rand < 0.7:
+            result = 0   # 60%概率处理中
+        else:
+            result = 1   # 30%概率成功
+    
+    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()
+
+
+
+

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

@@ -38,7 +38,6 @@ def get_budget_info(project_id:str):
     except Exception as e:
         return ResponseBase.error(f'获取项目概算信息失败:{str(e)}')
 
-
 @project_api.route('/chapter/<project_id>', methods=['POST'])
 @Permission.authorize
 def get_chapter_items(project_id:str):

+ 15 - 4
SourceCode/IntelligentRailwayCosting/app/routes/project_task.py

@@ -61,6 +61,7 @@ def save_task(task_id:int):
         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'
         # 获取上传的文件
@@ -76,14 +77,20 @@ def save_task(task_id:int):
             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:
-            task_service.start_run_task(task.id)
-        return ResponseBase.success(task.to_dict())
+            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:
@@ -146,9 +153,13 @@ def download_file():
 def start_task(task_id:int):
     try:
         msg = task_service.start_run_task(task_id)
-        if msg:
+        if msg == '0':
+            msg = '项目有正在运行的任务,已加入等待列表中'
+        elif msg:
             return ResponseBase.error(msg)
-        return ResponseBase.success('运行成功')
+        else:
+            msg = '运行成功'
+        return ResponseBase.success(msg)
     except Exception as e:
         return ResponseBase.error(f'运行失败:{str(e)}')
 

+ 34 - 30
SourceCode/IntelligentRailwayCosting/app/services/project_task.py

@@ -1,3 +1,4 @@
+from time import sleep
 from typing import Optional
 
 import tools.utils as utils, core.configs as configs, os, threading
@@ -34,9 +35,13 @@ class ProjectTaskService:
 
             try:
                 thread = threading.Thread(target=self._run_task, args=(task,))
+                thread.daemon = True
                 thread.start()
                 if executor.project_is_running(task.project_id):
-                    return '项目有正在运行的任务'
+                    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)}")
@@ -48,8 +53,8 @@ class ProjectTaskService:
         with task_lock:
             try:
                 msg = executor.run_task(task)
-                if not msg:
-                    self.start_send_task(task.id)
+                # 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))
@@ -68,6 +73,7 @@ class ProjectTaskService:
 
             try:
                 thread = threading.Thread(target=self._send_task, args=(task,))
+                thread.daemon = True
                 thread.start()
                 return None
             except Exception as e:
@@ -224,34 +230,35 @@ class ProjectTaskService:
 
     def _process_file_upload(self, task: ProjectTaskDto, files: list) -> str:
         """处理文件上传流程"""
-        task_dir = os.path.join(configs.app.source_path, f'upload_files/{task.get_path()}')
-        os.makedirs(task_dir, exist_ok=True)
+
         self._logger.info(f"保存处理文件,项目ID:{task.project_id},任务ID:{task.id}")
-        delete_old = task.collect_status==0
+        delete_old = task.collect_status == 0
         if delete_old:
-            if os.path.exists(task_dir):
-                delete_paths = []
-                for filename in os.listdir(task_dir):
-                    file_path = os.path.join(task_dir, filename)
-                    if os.path.isfile(file_path):
-                        delete_dir = os.path.join(task_dir, 'delete')
-                        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)
-                if len(delete_paths) > 0:
-                    LogRecordHelper.log_success(OperationType.DELETE, OperationModule.TASK,
-                                          f"删除任务文件:{task.task_name}", utils.to_str(delete_paths))
+            delete_paths=[]
+            for file_path in task.file_path.split(','):
+                if os.path.isfile(file_path):
+                    delete_dir = 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)
+            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
@@ -378,10 +385,7 @@ class ProjectTaskService:
     def cancel_run_task(self, task_id:int):
         task = self.store.get_task_dto(task_id)
         if task:
-            if task.process_status == TaskStatusEnum.PROCESSING:
-                return executor.cancel_task(task)
-            else:
-                return '任务状态不正确'
+            return executor.cancel_task(task)
         else:
             return '没有查询到任务'
 

+ 1 - 1
SourceCode/IntelligentRailwayCosting/app/stores/project.py

@@ -102,5 +102,5 @@ class ProjectStore:
         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).to_dict()
+            return ProjectDto.from_model(data)
 

+ 11 - 12
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_quota.py

@@ -139,23 +139,22 @@ class ProjectQuotaStore:
             quota = ProjectQuotaModel(
                 project_id=quota_dto.project_id,
                 budget_id=quota_dto.budget_id,
+                task_id=quota_dto.task_id,
                 item_id=quota_dto.item_id,
                 item_code=quota_dto.item_code,
                 quota_code=quota_dto.quota_code,
-                project_name=quota_dto.entry_name,
-                unit=quota_dto.unit,
-                project_quantity=quota_dto.project_quantity,
-                project_quantity_input=quota_dto.project_quantity_input,
-                quota_adjustment=quota_dto.quota_adjustment,
-                unit_price=quota_dto.unit_price,
-                total_price=quota_dto.total_price,
-                unit_weight=quota_dto.unit_weight,
-                total_weight=quota_dto.total_weight,
-                labor_cost=quota_dto.labor_cost,
-                process_status=quota_dto.process_status,
+                entry_name=quota_dto.entry_name,
+                units=quota_dto.units,
+                amount=quota_dto.amount,
+                ex_file=quota_dto.ex_file,
+                ex_cell=quota_dto.ex_cell,
+                ex_row=quota_dto.ex_row,
+                ex_unit=quota_dto.ex_unit,
+                ex_amount=quota_dto.ex_amount,
+                process_status=0,
                 process_time=quota_dto.process_time,
                 process_error=quota_dto.process_error,
-                send_status=quota_dto.send_status,
+                send_status=0,
                 send_time=quota_dto.send_time,
                 send_error=quota_dto.send_error,
                 created_by=quota_dto.created_by or self.current_user.username,

+ 7 - 3
SourceCode/IntelligentRailwayCosting/app/stores/railway_costing_sqlserver/project_task.py

@@ -71,7 +71,8 @@ class ProjectTaskStore:
             total_count = query.count()
 
             # 分页
-            query = query.order_by(ProjectTaskModel.created_at.desc())\
+            query = query.order_by(ProjectTaskModel.task_sort.desc())\
+                .order_by(ProjectTaskModel.created_at.desc())\
                 .offset((page - 1) * page_size).limit(page_size)
 
             tasks = query.all()
@@ -134,7 +135,7 @@ class ProjectTaskStore:
             total_count = query.count()
 
             # 分页
-            query = query.order_by(ProjectTaskModel.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
+            query = query.order_by(ProjectTaskModel.created_at.desc()).order_by(ProjectTaskModel.task_sort.desc()).offset((page - 1) * page_size).limit(page_size)
 
             tasks = query.all()
 
@@ -178,9 +179,10 @@ class ProjectTaskStore:
             query = query.filter(
                 and_(
                     ProjectTaskModel.is_del == 0,
-                    ProjectTaskModel.process_status == TaskStatusEnum.PROCESSING.value))
+                    ProjectTaskModel.process_status == TaskStatusEnum.WAIT.value))
             if project_id:
                 query = query.filter(ProjectTaskModel.project_id == project_id)
+            query.order_by(ProjectTaskModel.task_sort.desc())
             tasks = query.all()
             return [ProjectTaskDto.from_model(task) for task in tasks]
     def create_task(self, task_dto: ProjectTaskDto) -> ProjectTaskDto:
@@ -195,6 +197,7 @@ class ProjectTaskStore:
         task = ProjectTaskModel(
             task_name=task_dto.task_name,
             task_desc=task_dto.task_desc,
+            task_sort=task_dto.task_sort,
             project_id=task_dto.project_id,
             budget_id=task_dto.budget_id,
             item_id=task_dto.item_id,
@@ -225,6 +228,7 @@ class ProjectTaskStore:
         with db_helper.sqlserver_session(self._database) as db_session:
             task.task_name = task_dto.task_name
             task.task_desc = task_dto.task_desc
+            task.task_sort = task_dto.task_sort
             task.budget_id = task_dto.budget_id
             # task.project_id = task_dto.project_id
             # task.item_id = task_dto.item_id

+ 92 - 0
SourceCode/IntelligentRailwayCosting/app/test/config.yml

@@ -0,0 +1,92 @@
+app:
+  # 应用名称
+  name: '铁路造价智能化工具'
+  version: '2020' # 应用版本 2020|2024
+  use_version: true
+  source_path: './temp_files'
+  collect_api_url: 'http://127.0.0.1:5123/api/v1'
+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驱动程序
+  sqlserver_mian_2024:
+    driver: '{ODBC Driver 17 for SQL Server}'
+    server: 192.168.0.81,1433
+    username: iwb
+    password: 123456Qsc
+    database: Iwb_RecoData2024
+    trusted_connection: false
+  sqlserver_mian_2020:
+    driver: '{ODBC Driver 17 for SQL Server}'
+    server: 192.168.0.81,1433
+    username: iwb
+    password: 123456Qsc
+    database: Iwb_RecoData2020
+    trusted_connection: false
+  Iwb_RailwayCosting:
+    driver: '{ODBC Driver 17 for SQL Server}'
+    server: 192.168.0.81,1433
+    username: iwb
+    password: 123456Qsc
+    database: iwb_railway_costing_v1
+    trusted_connection: false
+  Iwb_RecoData2024:
+    driver: '{ODBC Driver 17 for SQL Server}'
+    server: shvber.com,50535
+    username: sa
+    password: Iwb2017
+    database: Iwb_RecoData2024
+    trusted_connection: false
+  Iwb_RecoData2020:
+    driver: '{ODBC Driver 17 for SQL Server}'
+    # server: shvber.com,50535
+    # username: sa
+    # password: Iwb2017
+    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
+  # MySQL 示例数据库配置
+  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
+

+ 107 - 0
SourceCode/IntelligentRailwayCosting/app/test/database_test.py

@@ -0,0 +1,107 @@
+import unittest
+import os
+from tools.db_helper.sqlserver_helper import SQLServerHelper
+from sqlalchemy import text
+
+class TestDatabaseCreation(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        """测试类初始化"""
+        # 使用sa账号进行数据库创建测试
+        cls.db_helper = SQLServerHelper()
+        cls.db_helper._default_config.update({
+            'server': '192.168.0.81,1433',
+            'username': 'sa',
+            'password': '13@24QAZwsx',
+            'trusted_connection': 'no',  # 使用SQL认证
+            'driver': 'ODBC Driver 17 for SQL Server'
+        })
+        # 配置连接池参数,增加重试和超时设置
+        cls.db_helper._pool_config.update({
+            'pool_pre_ping': True,
+            'pool_timeout': 60,
+            'pool_recycle': 1800,
+            'connect_args': {
+                'timeout': 60,
+                'driver_connects_timeout': 60,
+                'connect_timeout': 60,
+                'connect_retries': 3,
+                'connect_retry_interval': 10
+            }
+        })
+        # 读取数据库名列表
+        cls.db_names = []
+        db_file_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.script', 'db')
+        with open(db_file_path, 'r') as f:
+            cls.db_names = [line.strip() for line in f if line.strip()]
+        
+        # 读取建表SQL脚本
+        sql_file_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.script', 'project_data_test.sql')
+        with open(sql_file_path, 'r', encoding='utf-8') as f:
+            cls.create_tables_sql = f.read()
+
+    def test_create_databases(self):
+        """测试创建数据库和表"""
+        import time
+        from sqlalchemy.exc import OperationalError, ProgrammingError
+
+        for db_name in self.db_names:
+            max_retries = 3
+            retry_count = 0
+            while retry_count < max_retries:
+                try:
+                    # 创建数据库(使用独立的连接和autocommit模式)
+                    create_db_sql = f"IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = N'{db_name}') CREATE DATABASE [{db_name}]"
+                    engine = self.db_helper.get_engine('master')
+                    conn = engine.connect()
+                    try:
+                        conn.execution_options(isolation_level='AUTOCOMMIT')
+                        conn.execute(text(create_db_sql))
+                        # 等待数据库创建完成,增加重试次数和等待时间
+                        for _ in range(5):
+                            result = conn.scalar(text(f"SELECT COUNT(*) FROM sys.databases WHERE name = N'{db_name}'"))
+                            if result > 0:
+                                break
+                            time.sleep(5)  # 等待5秒后重试
+                    finally:
+                        conn.close()
+                
+                    # 按GO分隔符拆分SQL脚本并逐条执行
+                    # sql_statements = [stmt.strip() for stmt in self.create_tables_sql.split('\nGO\n') if stmt.strip()]
+                    # for sql in sql_statements:
+                    #     try:
+                    #         self.db_helper.execute_non_query(db_name, sql)
+                    #     except Exception as e:
+                    #         print(f"执行SQL语句失败: {str(e)}\nSQL: {sql}")
+                    #         raise
+                    #
+                    # 验证数据库是否创建成功
+                    # result = self.db_helper.execute_scalar('master', f"SELECT COUNT(*) FROM sys.databases WHERE name = N'{db_name}'")
+                    # self.assertEqual(result, 1, f"数据库 {db_name} 创建失败")
+                    #
+                    # # 验证表是否创建成功(以功能锁表为例)
+                    # result = self.db_helper.execute_scalar(db_name, "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'功能锁'")
+                    # self.assertEqual(result, 1, f"数据库 {db_name} 中的表创建失败")
+                    #
+                    print(f"数据库 {db_name} 创建成功")
+                    break  # 成功后跳出重试循环
+                    
+                except (OperationalError, ProgrammingError) as e:
+                    retry_count += 1
+                    if retry_count >= max_retries:
+                        self.fail(f"创建数据库 {db_name} 时发生错误,已重试{max_retries}次: {str(e)}")
+                    print(f"创建数据库 {db_name} 失败,正在进行第{retry_count}次重试...")
+                    time.sleep(5)  # 等待5秒后重试
+                except Exception as e:
+                    self.fail(f"创建数据库 {db_name} 时发生未预期的错误: {str(e)}")
+                finally:
+                    # 确保在每次尝试后释放连接资源
+                    self.db_helper.dispose_all()
+
+    @classmethod
+    def tearDownClass(cls):
+        """测试类清理"""
+        cls.db_helper.dispose_all()
+
+if __name__ == '__main__':
+    unittest.main()

+ 1 - 1
SourceCode/IntelligentRailwayCosting/app/test/sqlserver_test.py

@@ -120,7 +120,7 @@ class TestSQLServerHelper(unittest.TestCase):
             with self.db_helper.session_scope(self.database) as session:
                 result = session.query(TestTable).filter_by(name="test_commit").first()
                 self.assertIsNotNone(result, "事务提交失败")
-                self.assertEqual(result.entry_name, "test_commit")
+                self.assertEqual(result.name, "test_commit")
 
         except Exception as e:
             self.fail(f"会话管理测试失败: {str(e)}")

+ 1 - 1
SourceCode/IntelligentRailwayCosting/app/views/login.py

@@ -12,4 +12,4 @@ def login():
     #     return redirect(url_for('project.index'))
     logout_user()
     UserSession.clear_user()
-    return render_template('account/login.html', app_name=config.app.entry_name)
+    return render_template('account/login.html', app_name=config.app.name)

+ 6 - 0
SourceCode/IntelligentRailwayCosting/app/views/static/base/css/styles.css

@@ -89,6 +89,12 @@ body > .container, body > .container-fluid{
     font-size: 16px;
     color: #666;
 }
+.table-box td .one-line{
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: inline-block;
+}
 .pagination-row{
     width: 100%;
     display: flex;

+ 3 - 2
SourceCode/IntelligentRailwayCosting/app/views/static/base/js/utils.js

@@ -33,9 +33,10 @@ function IwbAjax(opt) {
 	})
 		.then((response) => response.json())
 		.then((data) => {
+			console.log(`[${opt.url}]返回数据:`, data)
 			if (data.success) {
 				if (opt.isAlert) {
-					MsgSuccess('操作成功')
+					MsgSuccess(data.message||'操作成功')
 				}
 				if (opt.table) {
 					IwbTable($(opt.table))
@@ -139,7 +140,7 @@ function IwbTable(table, opts,isReload) {
 					if (col.render) {
 						body_str += `<td>${col.render(row, rows)}</td>`
 					} else {
-						body_str += `<td>${row[col.data] || '-'}</td>`
+						body_str += `<td>${row[col.data]===undefined || row[col.data]===null ? '-':row[col.data]}</td>`
 					}
 				}
 				body_str += '</tr>'

+ 166 - 108
SourceCode/IntelligentRailwayCosting/app/views/static/project/budget_info.js

@@ -18,13 +18,23 @@ const nav_template = `<ul id="nav_tab" class="nav nav-tabs nav-line-tabs nav-lin
 	tab_content_template = `<div class="tab-pane h-100" id="iwb_tab_{0}" role="tabpanel">{1}</div>`,
 	table_add_task_btn_template = `<button type="button" class="task_add_btn btn btn-primary btn-sm" onclick="Add('{0}')">添加任务</button>`,
 	table_add_quota_btn_template = `<button type="button" class="quota_add_btn btn btn-primary btn-sm" onclick="Add_Quota('{0}')">添加定额</button>`,
-	table_collect_select_template = `<select class="form-select form-select-sm me-5" name="collect_status">
-												<option value="">全部采集状态</option>
-												<option value="0">未采集</option>
-												<option value="1">采集中</option>
-												<option value="2">已采集</option>
-												<option value="3">采集失败</option>
-												<option value="4">数据变更</option>
+	table_run_select_template = `<select class="form-select form-select-sm me-5" name="process_status">
+												<option value="">全部运行状态</option>
+												<option value="0">草稿</option>
+												<option value="1">等待运行</option>
+												<option value="2">运行中</option>
+												<option value="200">运行成功</option>
+												<option value="4">取消运行</option>
+												<option value="5">运行失败</option>
+												<!--<option value="4">已修改</option>-->
+											</select>`,
+	table_send_select_template = `<select class="form-select form-select-sm me-5" name="send_status">
+												<option value="">全部发送状态</option>
+												<option value="0">未发送</option>
+												<option value="1">发送中</option>
+												<option value="200">发送成功</option>
+												<option value="2">发送失败</option>
+												<option value="3">数据变更</option>
 											</select>`,
 	table_template = `<div class="table-box table-responsive" data-id="{0}" id="table_box_{0}">
 								<div class="d-flex justify-content-between my-5">
@@ -32,22 +42,6 @@ const nav_template = `<ul id="nav_tab" class="nav nav-tabs nav-line-tabs nav-lin
 									<form class="search-box d-flex">
 										<div class="d-flex">
 											{2}
-<!--											<select class="form-select form-select-sm me-5" name="process_status">-->
-<!--												<option value="">全部处理状态</option>-->
-<!--												<option value="0">未处理</option>-->
-<!--												<option value="1">处理中</option>-->
-<!--												<option value="2">已处理</option>-->
-<!--												<option value="3">处理失败</option>-->
-<!--												<option value="4">数据变更</option>-->
-<!--											</select>-->
-											<select class="form-select form-select-sm me-5" name="send_status">
-												<option value="">全部发送状态</option>
-												<option value="0">未发送</option>
-												<option value="1">发送中</option>
-												<option value="2">已发送</option>
-												<option value="3">发送失败</option>
-												<option value="4">数据变更</option>
-											</select>
 											<input type="text" class="form-control form-control-sm w-200px" placeholder="请输入关键字" name="keyword" />
 										</div>
 										<div class="btn-group ms-5">
@@ -230,7 +224,7 @@ function RenderRightBox(data){
 		$rightBoxHeader.find('.badge').text('任务列表').removeClass('badge-success').addClass('badge-primary')
 		$rightBoxHeader.find('#task_radio').prop("checked",true)
 		const budget_id =0
-		$taskBox.html(table_template.format(budget_id,table_add_task_btn_template.format(budget_id),table_collect_select_template))
+		$taskBox.html(table_template.format(budget_id,table_add_task_btn_template.format(budget_id),table_run_select_template))
 		_taskTable(data)
 	}
 	function _taskTable(data){
@@ -251,8 +245,8 @@ function RenderRightBox(data){
 					},
 					{
 						title: '任务排序',
-						data: 'sort',
-						width: '100',
+						data: 'task_sort',
+						width: '100px',
 					},
 					{
 						title: '概算单元',
@@ -272,7 +266,8 @@ function RenderRightBox(data){
 							const file_paths = row.file_path ? row.file_path.split(',') : []
 							if(file_paths.length){
 								for (let i = 0; i < file_paths.length; i++) {
-									const path = file_paths[i]
+									const path = file_paths[i] || ""
+									if (!path) continue
 									const names = path.split('/')
 									const file_name = names[names.length - 1]
 									str += `<a href="#" onclick="DownloadFile('/api/task/download?filename=${encodeURIComponent(path)}','${file_name}')"  class="link link-info px-2">${file_name}</a>`
@@ -289,21 +284,21 @@ function RenderRightBox(data){
 						width: '180px',
 						render: (row) => {
 							let str = ``
-							if (row.collect_status === 0) {
-								str += `<span class="badge badge-light-primary">未采集</span>`
-							} else if (row.collect_status === 1){
-								str += `<span class="badge badge-light-warning">采集中</span>`
-							} else if (row.collect_status === 2){
-								str += `<span class="badge badge-light-success">采集完成</span>`
-								if (row.send_status === 0) {
-									str += `<span class="badge badge-light-primary ms-3">未发送</span>`
-								} else if (row.send_status === 1){
-									str += `<span class="badge badge-light-warning ms-3">发送中</span>`
-								} else if (row.send_status === 2){
-									str += `<span class="badge badge-light-success ms-3">发送完成</span>`
-								} else if (row.send_status === 3){
-									str += `<span class="badge badge-light-danger ms-3">发送失败</span>`
-								}
+							if (row.process_status === 0) {
+								str += `<span class="badge badge-secondary">草稿</span>`
+							} else if (row.process_status === 1){
+								str += `<span class="badge badge-light-primary">等待运行</span>`
+							} else if (row.process_status === 2){
+								str += `<span class="badge badge-light-info">运行中</span>`
+								// if (row.send_status === 0) {
+								// 	str += `<span class="badge badge-light-primary ms-3">未发送</span>`
+								// } else if (row.send_status === 1){
+								// 	str += `<span class="badge badge-light-warning ms-3">发送中</span>`
+								// } else if (row.send_status === 2){
+								// 	str += `<span class="badge badge-light-success ms-3">发送完成</span>`
+								// } else if (row.send_status === 3){
+								// 	str += `<span class="badge badge-light-danger ms-3">发送失败</span>`
+								// }
 								// if (row.process_status === 0) {
 								// 	str += `<span class="badge badge-light-primary ms-3">未处理</span>`
 								// } else if (row.process_status === 1){
@@ -314,10 +309,12 @@ function RenderRightBox(data){
 								// } else if (row.process_status === 3){
 								// 	str += `<span class="badge badge-light-danger ms-3">处理失败</span>`
 								// }
-							} else if (row.collect_status === 3){
-								str += `<span class="badge badge-light-danger">采集失败</span>`
-							} else if (row.collect_status === 4){
-								str += `<span class="badge badge-light-info">数据变更</span>`
+							} else if (row.process_status === 200){
+								str += `<span class="badge badge-light-success">运行成功</span>`
+							} else if (row.process_status === 4){
+								str += `<span class="badge badge-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="${row.process_error}">运行失败</span>`
+							} else if (row.process_status === 5){
+								str += `<span class="badge badge-warning">取消运行</span>`
 							}
 							return str
 						}
@@ -328,10 +325,13 @@ function RenderRightBox(data){
 						width: '160px',
 						render: (row) => {
 							let str = ``
-							if (row.collect_status === 0) {
-								str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始任务" onclick="StartCollectTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
-							} else if (row.collect_status === 2) {
-								str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新采集" onclick="ReStartCollectTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+							if (row.process_status === 0) {
+								str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始任务" onclick="StarTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+							} else if (row.process_status === 1 || row.process_status === 2) {
+								str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="取消任务" onclick="CancelTask(${row.id})"><i class="ki-duotone ki-cross-square fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+							}
+							else if (row.process_status === 200) {
+								// str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新运行" onclick="ReStarTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
 								// if (row.process_status === 0) {
 								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始处理" onclick="StartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
 								// } else if (row.process_status === 2) {
@@ -342,23 +342,27 @@ function RenderRightBox(data){
 								// } else if (row.process_status === 4) {
 								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新处理" onclick="ReStartProcessTask(${row.id})"><i class="ki-duotone ki-book-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span></i></button>`
 								// }
-								if (row.send_status === 0) {
-									str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始发送" onclick="StartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-								} else if (row.send_status === 2) {
-									str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-								} else if (row.send_status === 3) {
-									str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-								} else if (row.send_status === 4) {
-									str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-								}
-							} else if (row.collect_status === 3) {
-								str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新采集" onclick="ReStartCollectTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
-							}else if (row.collect_status === 4) {
-								str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新采集" onclick="ReStartCollectTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+								// if (row.send_status === 0) {
+								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始发送" onclick="StartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+								// } else if (row.send_status === 2) {
+								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+								// } else if (row.send_status === 3) {
+								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+								// } else if (row.send_status === 4) {
+								// 	str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendTask(${row.id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+								// }
+							} else if (row.process_status === 4) {
+								str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新运行" onclick="ReStarTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+							}else if (row.process_status === 5) {
+								str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新运行" onclick="ReStarTask(${row.id})"><i class="ki-duotone ki-add-notepad fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
+							}
+							if (row.process_status === 0) {
+								str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit(${row.id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
+							} 
+							str+=`<!--<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit(${row.id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>-->`
+							if (row.process_status !== 2 && row.process_status !==1) {
+								str+=`<button type="button" class="btn btn-icon btn-sm btn-light-danger"  data-bs-toggle="tooltip" data-bs-placement="top" title="删除" onclick="Delete(${row.id})"><i class="ki-duotone ki-trash-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
 							}
-							str+=`<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit(${row.id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-							str+=`<button type="button" class="btn btn-icon btn-sm btn-light-danger"  data-bs-toggle="tooltip" data-bs-placement="top" title="删除" onclick="Delete(${row.id})"><i class="ki-duotone ki-trash-square fs-1"><span class="path1"></span><span class="path2"></span><span class="path3"></span><span class="path4"></span></i></button>`
-
 							return str
 						}
 					},
@@ -391,6 +395,7 @@ function RenderRightBox(data){
 	}
 
 }
+
 function BuildBudgetInfo() {
 	IwbAjax_1({
 		url: `/api/project/budget/${project_id}`,
@@ -403,6 +408,7 @@ function BuildBudgetInfo() {
 		},
 	})
 }
+
 function RenderTabs(data){
 	console.log('RenderTabs', data)
 	let str1 = '',
@@ -411,7 +417,7 @@ function RenderTabs(data){
 		for (let i = 0; i < data.length; i++) {
 			const item = data[i]
 			str1 += nav_tab_template.format(item.budget_id, item.budget_code)
-			const tableStr = table_template.format(item.budget_id,table_add_quota_btn_template.format(item.budget_id),"")
+			const tableStr = table_template.format(item.budget_id,table_add_quota_btn_template.format(item.budget_id),table_send_select_template)
 			const tabContent = tab_content_template.format(item.budget_id, tableStr)
 			// console.log('TAB_CONTENT', tabContent)
 			str2 += tabContent
@@ -430,9 +436,8 @@ function RenderTabs(data){
 	})
 	const firstTab = new bootstrap.Tab($tab_btn.eq(0))
 	firstTab.show()
-
-
 }
+
 function RenderQuotaTable(budget_id,data){
 	console.log('RenderQuotaTable', budget_id, data)
 	const $quotaBox = $('#body_box .right-box .box-body .quota')
@@ -443,28 +448,68 @@ function RenderQuotaTable(budget_id,data){
 			{
 				title: '序号',
 				data: 'id',
-				width: '100px',
+				width: '80px',
 			},
 			{
 				title: '工程或费用项目名称',
-				data: 'project_name',
-				width: 'auto',
+				data: 'entry_name',
+				width: '210px',
+				render: (row) => {
+						return `<span class="one-line mw-200px" data-bs-toggle="tooltip" data-bs-placement="top" title="${row.entry_name}" >${row.entry_name}</span>`
+				}
 			},
 			{
 				title: '工程数量',
-				data: 'project_quantity',
+				data: 'amount',
 				width: '100px',
 			},
 			{
 				title: '单位',
-				data: 'unit',
-				width: '100px',
+				data: 'units',
+				width: '80px',
 			},
 			{
 				title: '定额编号',
 				data: 'quota_code',
 				width: '100px',
 			},
+			{
+				title: 'Excel文件',
+				data: 'ex_file',
+				width: '150px',
+				render: (row) => {
+					const path = row.ex_file
+					if (!path) {
+						return '-'
+					}
+					const names = path.split('/')
+					const file_name = names[names.length - 1]
+					return `<span class="one-line mw-150px"><a href="#" onclick="DownloadFile('/api/task/download?filename=${encodeURIComponent(path)}','${file_name}')"  class="link link-info px-2">${file_name}</a></span>`
+				}
+			},
+			{
+				title: 'Excel整行内容',
+				data: 'ex_row',
+				width: 'auto',
+				render: (row) => {
+					return `<span class="one-line w-300px" data-bs-toggle="tooltip" data-bs-placement="top" title="${row.ex_row}" >${row.ex_row}</span>`
+				}
+			},
+			{
+				title: 'Excel数量',
+				data: 'ex_amount',
+				width: '100px',
+			},
+			{
+				title: '数量位置',
+				data: 'ex_cell',
+				width: '80px',
+			},
+			{
+				title: '数量单位',
+				data: 'ex_unit',
+				width: '80px',
+			},
 			{
 				title: '状态',
 				data: 'status',
@@ -486,11 +531,11 @@ function RenderQuotaTable(budget_id,data){
 						str+= `<span class="badge badge-primary ms-3">未发送</span>`
 					}else if (row.send_status === 1){
 						str+= `<span class="badge badge-warning ms-3">发送中</span>`
+					}else if (row.send_status === 200){
+						str+= `<span class="badge badge-success ms-3">发送成功</span>`
 					}else if (row.send_status === 2){
-						str+= `<span class="badge badge-success ms-3">已发送</span>`
-					}else if (row.send_status === 3){
 						str+= `<span class="badge badge-danger ms-3">发送失败</span>`
-					}else if (row.send_status === 4){
+					}else if (row.send_status === 3){
 						str+= `<span class="badge badge-danger ms-3">数据变更</span>`
 					}
 
@@ -515,11 +560,11 @@ function RenderQuotaTable(budget_id,data){
 					// }
 					if (row.send_status === 0) {
 						str += `<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="开始发送" onclick="StartSendQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-					} else if (row.send_status === 2) {
+					} else if (row.send_status === 200) {
 						str += `<button type="button" class="btn btn-icon btn-sm btn-light-warning" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-					} else if (row.send_status === 3) {
+					} else if (row.send_status === 2) {
 						str += `<button type="button" class="btn btn-icon btn-sm btn-light-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
-					} else if (row.send_status === 4) {
+					} else if (row.send_status === 3) {
 						str += `<button type="button" class="btn btn-icon btn-sm btn-light-info" data-bs-toggle="tooltip" data-bs-placement="top" title="重新发送" onclick="ReStartSendQuota(${row.id}, ${budget_id})"><i class="ki-duotone ki-send fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
 					}
 					str+=`<button type="button" class="btn btn-icon btn-sm btn-light-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="编辑" onclick="Edit_Quota(${row.id}, ${budget_id})"><i class="ki-duotone ki-message-edit fs-1"><span class="path1"></span><span class="path2"></span></i></button>`
@@ -533,16 +578,17 @@ function RenderQuotaTable(budget_id,data){
 
 function SetBudgetData($el){
 	const $tableBox = $(`#body_box .right-box`)
-	$el.find('[name="budget_id"').val($tableBox.find('input[name="budget_id"]').val());
 	$el.find('[name="project_id"]').val($tableBox.find('input[name="project_id"]').val());
 	$el.find('[name="item_id"]').val($tableBox.find('input[name="item_id"]').val());
 	$el.find('[name="item_code"]').val($tableBox.find('input[name="item_code"]').val());
 }
+
 function Add() {
 	_fileUploadDropzone.removeAllFiles()
 	AddModal($modal, () => {
 		$modal.find('[name="task_id"]').val('0');
-		$modal.find('[name="budget_id"]').val('0');
+		$modal.find('[name="budget_id"]').val(0);
+		$modal.find('[name="task_sort"]').val(0);
 		$modal.find('#delete_file_box').hide();
 		// $modal.find('[name="delete_file"]').prop('checked',false)
 		SetBudgetData($modal)
@@ -569,13 +615,14 @@ function Edit(id) {
 				$modal.find('[name="item_code"]').val(data.item_code);
                 $modal.find('[name="task_name"]').val(data.task_name);
                 $modal.find('[name="task_desc"]').val(data.task_desc);
+                $modal.find('[name="task_sort"]').val(data.task_sort);
 				// $modal.find('[name="delete_file"]').prop('checked',false)
             }
         })
     })
 }
 
-function SaveProject() {
+function SaveProject(is_submit) {
     const
 		formData = new FormData(),
 		budget_id = $modal.find('[name="budget_id"]').val(),
@@ -590,10 +637,13 @@ function SaveProject() {
 		files = _fileUploadDropzone.getAcceptedFiles();
 	// console.log("FILES",files)
 
-	if(files.length>0){
+	if(files&&files.length>0){
 		files.forEach((file) => {
 			formData.append('files', file)
 		})
+	}else{
+		MsgWarning('文件不能为空,请选择文件')
+		return
 	}
 	formData.append('budget_id', budget_id)
 	formData.append('item_id', item_id)
@@ -603,7 +653,10 @@ function SaveProject() {
 	formData.append('task_name', task_name)
 	formData.append('task_sort', task_sort)
 	formData.append('task_desc', task_desc)
-	formData.append('delete_old', delete_file)
+	// formData.append('delete_old', delete_file)
+	if (is_submit) {
+		formData.append('run_now', 'true')
+	}
 
     IwbAjax({
         url:`/api/task/save/${task_id}`,
@@ -618,27 +671,32 @@ function Delete(id){
 	ConfirmUrl('确定删除吗?',`/api/task/delete/${id}`,`#table_0`)
 }
 
-function StartCollectTask(id){
-	ConfirmUrl('确定开始采集吗?',`/api/task/start_collect/${id}`,`#table_0`)
-}
-function ReStartCollectTask(id){
-	ConfirmUrl('确定重新开始采集吗?',`/api/task/start_collect/${id}`,`#table_0`)
+function StarTask(id){
+	ConfirmUrl('确定开始运行任务吗?',`/api/task/start_task/${id}`,`#table_0`)
 }
 
-function StartProcessTask(id){
-	ConfirmUrl('确定开始处理吗?',`/api/task/start_process/${id}`,`#table_0`)
+function ReStarTask(id){
+	ConfirmUrl('确定重新开始运行任务吗?',`/api/task/start_task/${id}`,`#table_0`)
 }
-
-function ReStartProcessTask(id){
-	ConfirmUrl('确定重新开始处理吗?',`/api/task/start_process/${id}`,`#table_0`)
+function CancelTask(id){
+	ConfirmUrl('确定取消运行任务吗?',`/api/task/cancel_task/${id}`,`#table_0`)
 }
 
-function StartSendTask(id){
-	ConfirmUrl('确定开始发送吗?',`/api/task/start_send/${id}`,`#table_0`)
-}
-function ReStartSendTask(id){
-	ConfirmUrl('确定重新开始发送吗?',`/api/task/start_send/${id}`,`#table_0`)
-}
+
+// function StartProcessTask(id){
+// 	ConfirmUrl('确定开始处理吗?',`/api/task/start_process/${id}`,`#table_0`)
+// }
+//
+// function ReStartProcessTask(id){
+// 	ConfirmUrl('确定重新开始处理吗?',`/api/task/start_process/${id}`,`#table_0`)
+// }
+
+// function StartSendTask(id){
+// 	ConfirmUrl('确定开始发送吗?',`/api/task/start_send/${id}`,`#table_0`)
+// }
+// function ReStartSendTask(id){
+// 	ConfirmUrl('确定重新开始发送吗?',`/api/task/start_send/${id}`,`#table_0`)
+// }
 
 function Add_Quota(budget_id,) {
 	AddModal($modalQuota, () => {
@@ -712,12 +770,12 @@ function Delete_Quota(id,budget_id){
 	ConfirmUrl('确定删除吗?',`/api/quota/delete/${id}`,`#table_${budget_id}`)
 }
 
-function StartProcessQuota(id,budget_id){
-	ConfirmUrl('确定开始处理吗?',`/api/quota/start_process/${id}`,`#table_${budget_id}`)
-}
-function ReStartProcessQuota(id,budget_id){
-	ConfirmUrl('确定重新开始处理吗?',`/api/quota/start_process/${id}`,`#table_${budget_id}`)
-}
+// function StartProcessQuota(id,budget_id){
+// 	ConfirmUrl('确定开始处理吗?',`/api/quota/start_process/${id}`,`#table_${budget_id}`)
+// }
+// function ReStartProcessQuota(id,budget_id){
+// 	ConfirmUrl('确定重新开始处理吗?',`/api/quota/start_process/${id}`,`#table_${budget_id}`)
+// }
 function StartSendQuota(id,budget_id){
 	ConfirmUrl('确定开始发送吗?',`/api/quota/start_send/${id}`,`#table_${budget_id}`)
 }

+ 11 - 9
SourceCode/IntelligentRailwayCosting/app/views/templates/project/budget_info.html

@@ -45,15 +45,16 @@
 							</select>
 						</div>
 						<div class="fv-row form-group mb-3">
-							<label for="task_desc" class="form-label">任务详情</label>
-							<textarea type="text" class="form-control" name="task_desc" id="task_desc" placeholder="请输入"></textarea>
-						</div>
-						<div class="form-check mb-3" id="delete_file_box">
-						 	<input class="form-check-input" type="checkbox" value="" id="delete_file" name="delete_file"/>
-							<label class="form-check-label ms-5 text-primary " for="delete_file">
-								删除原数据文件
-							</label>
+							<label for="task_sort" class="form-label">任务排序</label>
+							<input type="number" class="form-control" name="task_sort" id="task_sort" placeholder="请输入"/>
 						</div>
+
+<!--						<div class="form-check mb-3" id="delete_file_box">-->
+<!--						 	<input class="form-check-input" type="checkbox" value="" id="delete_file" name="delete_file"/>-->
+<!--							<label class="form-check-label ms-5 text-primary " for="delete_file">-->
+<!--								删除原数据文件-->
+<!--							</label>-->
+<!--						</div>-->
 						<div class="fv-row">
 							<div id="file_upload_dropzone">
 								<div class="dropzone">
@@ -88,7 +89,8 @@
 			</div>
 			<div class="modal-footer">
 				<button type="button" class="btn btn-light" data-bs-dismiss="modal">取消</button>
-				<button type="button" class="btn btn-primary" onclick="SaveProject()">保存</button>
+				<button type="button" class="btn btn-success" onclick="SaveProject(true)">提交</button>
+				<button type="button" class="btn btn-primary" onclick="SaveProject()">保存草稿</button>
 			</div>
 		</div>
 	</div>

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно