Bladeren bron

feat:1.接入日志模块

wangkun 1 week geleden
bovenliggende
commit
e6dc3ddf48
49 gewijzigde bestanden met toevoegingen van 4726 en 68 verwijderingen
  1. 101 0
      doc/sql/log_tables.sql
  2. 887 0
      doc/udeServer-api.json
  3. 5 0
      etc/udeserver-api.yaml
  4. 24 9
      go.mod
  5. 116 19
      go.sum
  6. 215 0
      internal/audit/archive.go
  7. 45 0
      internal/audit/async.go
  8. 71 0
      internal/audit/config.go
  9. 63 0
      internal/audit/constants.go
  10. 34 0
      internal/audit/context.go
  11. 169 0
      internal/audit/digest.go
  12. 58 0
      internal/audit/hmac.go
  13. 47 0
      internal/audit/level.go
  14. 37 0
      internal/audit/meta.go
  15. 288 0
      internal/audit/middleware.go
  16. 166 0
      internal/audit/minio.go
  17. 2 0
      internal/config/config.go
  18. 77 4
      internal/handler/routes.go
  19. 30 0
      internal/handler/systemlog/logarchivedownloadhandler.go
  20. 32 0
      internal/handler/systemlog/logarchivehandler.go
  21. 32 0
      internal/handler/systemlog/logarchivelisthandler.go
  22. 32 0
      internal/handler/systemlog/logarchiverestorehandler.go
  23. 32 0
      internal/handler/systemlog/logaudithandler.go
  24. 32 0
      internal/handler/systemlog/logdetailhandler.go
  25. 32 0
      internal/handler/systemlog/loglinkhandler.go
  26. 32 0
      internal/handler/systemlog/loglisthandler.go
  27. 32 0
      internal/handler/systemlog/logrestorelisthandler.go
  28. 82 0
      internal/log/log.go
  29. 1 1
      internal/logic/brushservicelogic.go
  30. 6 8
      internal/logic/listdirchildrenlogic.go
  31. 78 0
      internal/logic/systemlog/convert.go
  32. 109 0
      internal/logic/systemlog/logarchivedownloadlogic.go
  33. 58 0
      internal/logic/systemlog/logarchivelistlogic.go
  34. 73 0
      internal/logic/systemlog/logarchivelogic.go
  35. 188 0
      internal/logic/systemlog/logarchiverestorelogic.go
  36. 107 0
      internal/logic/systemlog/logauditlogic.go
  37. 47 0
      internal/logic/systemlog/logdetaillogic.go
  38. 49 0
      internal/logic/systemlog/loglinklogic.go
  39. 81 0
      internal/logic/systemlog/loglistlogic.go
  40. 61 0
      internal/logic/systemlog/logrestorelistlogic.go
  41. 50 0
      internal/model/tconfigmodel.go
  42. 129 0
      internal/model/tlogarchiverecordmodel.go
  43. 108 0
      internal/model/tlogrestorerecordmodel.go
  44. 313 0
      internal/model/tsystemlogmodel.go
  45. 30 0
      internal/svc/servicecontext.go
  46. 65 0
      internal/task/task.go
  47. 167 16
      internal/types/types.go
  48. 221 11
      udeServer.api
  49. 12 0
      udeserver.go

+ 101 - 0
doc/sql/log_tables.sql

@@ -0,0 +1,101 @@
+-- ============================================================
+-- 审计日志模块建表脚本(移植自 sdf-serve,ude 库)
+-- 执行目标: 10.10.10.30:3306 / ude
+-- 执行前提: SHOW TABLES 确认以下 4 张表不存在
+-- ============================================================
+
+-- 1. 系统链路与业务日志表(审计中间件每次请求写一条 type=1)
+CREATE TABLE IF NOT EXISTS `t_system_log` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增主键',
+  `iden` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '日志编号',
+  `trace_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '全链路唯一Trace ID',
+  `span_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '本次跨度唯一Span ID',
+  `parent_span_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '父SpanID,用于构建调用树',
+  `type` tinyint(1) NOT NULL COMMENT '日志类型:1.系统日志;2.接口日志',
+  `service_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '所属服务或模块名',
+  `uri_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '接口请求路径',
+  `http_method` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '请求方法:GET/POST/PUT/DELETE',
+  `method` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '请求业务方法',
+  `level` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '日志级别:1-DEBUG,2-INFO,3-WARN,4-ERROR',
+  `app_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '调用接口应用标识',
+  `user_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '系统用户ID',
+  `user_name` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '系统用户名',
+  `client_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '客户端IP',
+  `request_header` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '请求头(脱敏后)',
+  `request_body_digest` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '请求体SM3摘要',
+  `request_body_file_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '请求体原文FileKey(MinIO)',
+  `response_code` int NOT NULL COMMENT '响应业务码(ude恒100000/100xxx,取body业务码)',
+  `response_body_digest` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '响应体SM3摘要',
+  `response_body_file_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '响应体原文FileKey(MinIO)',
+  `error_message` text COLLATE utf8mb4_unicode_ci COMMENT '错误提示,仅非100000时填',
+  `duration_ms` int NOT NULL COMMENT '本Span耗时(毫秒)',
+  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '写入时间',
+  `h_mac` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'HMAC-SM3防篡改签名值',
+  `audit_status` tinyint(1) NOT NULL COMMENT '审计状态:1.未审计;2.审计通过;3.审计异常',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `idx_iden` (`iden`) USING BTREE,
+  KEY `idx_trace` (`trace_id`),
+  KEY `idx_parent_span` (`parent_span_id`),
+  KEY `idx_service_method` (`service_name`,`method`),
+  KEY `idx_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统链路与业务日志表';
+
+-- 2. 动态配置表(热加载:MinIO参数/记录级别/归档阈值/HMAC密钥)
+CREATE TABLE IF NOT EXISTS `t_config` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `config_item` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '配置项',
+  `config_value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '配置值',
+  `edit_flag` tinyint(1) NOT NULL COMMENT '是否允许编辑,1是不可编辑configItem,2是所有均可编辑',
+  `description` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述',
+  `updated_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最后更新人',
+  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '配置更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `idx_config_item` (`config_item`) COMMENT '配置项唯一索引'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='配置表';
+
+-- 3. 日志归档记录表
+CREATE TABLE IF NOT EXISTS `t_log_archive_record` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `archive_name` varchar(100) NOT NULL COMMENT '归档任务名',
+  `record_count` int NOT NULL COMMENT '归档记录数',
+  `archive_file_key` varchar(255) NOT NULL COMMENT '归档SQL文件FileKey(MinIO)',
+  `archive_mode` tinyint(1) NOT NULL COMMENT '归档方式:1-手动,2-自动',
+  `batch_size` int NOT NULL DEFAULT '10000' COMMENT '本次归档的阈值N',
+  `archived_by` varchar(50) NOT NULL COMMENT '归档人账号',
+  `archived_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '归档时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_archive_by_time` (`archived_by`,`archived_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='日志归档记录表';
+
+-- 4. 日志恢复记录表
+CREATE TABLE IF NOT EXISTS `t_log_restore_record` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `archive_id` bigint NOT NULL COMMENT '归档记录ID',
+  `restore_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '恢复状态:1-成功,2-失败,3-处理中',
+  `restore_message` varchar(255) DEFAULT NULL COMMENT '恢复说明或错误原因',
+  `restored_by` varchar(50) NOT NULL COMMENT '恢复人账号',
+  `restored_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '恢复时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_restore_by_time` (`restored_by`,`restored_at`),
+  KEY `idx_archive_id` (`archive_id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='日志恢复记录表';
+
+-- ============================================================
+-- t_config 初始配置行(8 项)
+-- 注意:
+--   * file_* 四项暂用 sdf 同套 MinIO(10.10.10.25:9020/桶test)的现值,
+--     桶名改为 ude 专有;确认后可随时 UPDATE(服务每个请求热加载,改完立即生效)。
+--   * file_use_ssl = '1' 启用TLS,'2' 停用(sdf 语义)。
+--   * log_record_level = 日志入库门槛,t_config 缺失/非法时服务端自动退化为 info。
+--   * log_hmac_secret 留空仍可运行(空串作为密钥计算,审计链不中断),
+--     但防篡改强度有限;建议尽快填入一段随机字符串(32~64位)。
+-- ============================================================
+INSERT INTO t_config (`config_item`, `config_value`, `edit_flag`, `description`, `updated_by`) VALUES
+  ('file_endpoint',    '10.10.10.25:9020', 1, '文件服务器地址(MinIO endpoint)', 'ude'),
+  ('file_bucket',      'ude',              1, '文件bucket',                     'ude'),
+  ('file_access_key',  'minio_bxM8k5',     1, '文件accessKey(与sdf共用,确认后替换)', 'ude'),
+  ('file_secret_key',  'minio_mP6zC4',     1, '文件secretKey(与sdf共用,确认后替换)', 'ude'),
+  ('file_use_ssl',     '2',                1, '是否使用加密链接(1启用,2停用)',   'ude'),
+  ('log_record_level', 'info',             1, '日志记录级别:debug/info/warn/error', 'ude'),
+  ('archive_threshold','100',              1, '日志归档阈值(条)',               'ude'),
+  ('log_hmac_secret',  '',                 1, '日志防篡改Hmac密钥(建议填随机串)', 'ude');

+ 887 - 0
doc/udeServer-api.json

@@ -0,0 +1,887 @@
+{
+  "swagger": "2.0",
+  "info": {
+    "title": "",
+    "version": ""
+  },
+  "host": "127.0.0.1:9993",
+  "schemes": [
+    "http"
+  ],
+  "consumes": [
+    "application/json"
+  ],
+  "produces": [
+    "application/json"
+  ],
+  "paths": {
+    "/api/v1/db/ping": {
+      "get": {
+        "operationId": "DbPingHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/DbPingResponse"
+            }
+          }
+        },
+        "tags": [
+          "udeServer-api"
+        ]
+      }
+    },
+    "/api/v1/directories": {
+      "get": {
+        "operationId": "ListDirectoriesHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/DirectoryListResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "page",
+            "in": "query",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "name": "pageSize",
+            "in": "query",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ],
+        "consumes": [
+          "multipart/form-data"
+        ]
+      },
+      "post": {
+        "operationId": "CreateDirectoryHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/DirectoryResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateDirectoryReq"
+            }
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      }
+    },
+    "/api/v1/directories/{id}": {
+      "get": {
+        "operationId": "GetDirectoryHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/DirectoryResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      },
+      "delete": {
+        "operationId": "DeleteDirectoryHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/DeleteResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "required": true,
+            "type": "string"
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/DirectoryIdReq"
+            }
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      },
+      "put": {
+        "operationId": "UpdateDirectoryHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/DirectoryResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "required": true,
+            "type": "string"
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateDirectoryReq"
+            }
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      }
+    },
+    "/api/v1/from/{name}": {
+      "get": {
+        "operationId": "UdeServerHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/Response"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "name",
+            "in": "path",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      }
+    },
+    "/api/v1/keys/create": {
+      "post": {
+        "operationId": "CreateKeyHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/KeyResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateKeyReq"
+            }
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      }
+    },
+    "/api/v1/keys/delete": {
+      "post": {
+        "operationId": "DeleteKeyHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/DeleteResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/KeyIdReq"
+            }
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      }
+    },
+    "/api/v1/keys/detail": {
+      "post": {
+        "operationId": "GetKeyHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/KeyResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/KeyIdReq"
+            }
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      }
+    },
+    "/api/v1/keys/list": {
+      "post": {
+        "operationId": "ListKeysHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/KeyListResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/ListKeysReq"
+            }
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      }
+    },
+    "/api/v1/keys/update": {
+      "post": {
+        "operationId": "UpdateKeyHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/KeyResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateKeyReq"
+            }
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      }
+    },
+    "/api/v1/services": {
+      "get": {
+        "operationId": "ListServicesHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/ServiceListResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "page",
+            "in": "query",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          },
+          {
+            "name": "pageSize",
+            "in": "query",
+            "required": false,
+            "type": "integer",
+            "format": "int64"
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ],
+        "consumes": [
+          "multipart/form-data"
+        ]
+      },
+      "post": {
+        "operationId": "CreateServiceHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/ServiceResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/CreateServiceReq"
+            }
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      }
+    },
+    "/api/v1/services/{id}": {
+      "get": {
+        "operationId": "GetServiceHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/ServiceResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      },
+      "delete": {
+        "operationId": "DeleteServiceHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/DeleteResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "required": true,
+            "type": "string"
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/ServiceIdReq"
+            }
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      },
+      "put": {
+        "operationId": "UpdateServiceHandler",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/ServiceResp"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "required": true,
+            "type": "string"
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/UpdateServiceReq"
+            }
+          }
+        ],
+        "tags": [
+          "udeServer-api"
+        ]
+      }
+    }
+  },
+  "definitions": {
+    "CreateDirectoryReq": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "serviceId": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "directory": {
+          "type": "string"
+        }
+      },
+      "title": "CreateDirectoryReq",
+      "required": [
+        "id"
+      ]
+    },
+    "CreateKeyReq": {
+      "type": "object",
+      "properties": {
+        "name": {
+          "type": "string"
+        }
+      },
+      "title": "CreateKeyReq",
+      "required": [
+        "name"
+      ]
+    },
+    "CreateServiceReq": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "name": {
+          "type": "string"
+        },
+        "ip": {
+          "type": "string"
+        },
+        "port": {
+          "type": "string"
+        },
+        "iden": {
+          "type": "string"
+        },
+        "keyIden": {
+          "type": "string"
+        },
+        "keyAlgo": {
+          "type": "string"
+        },
+        "status": {
+          "type": "string"
+        }
+      },
+      "title": "CreateServiceReq",
+      "required": [
+        "id"
+      ]
+    },
+    "DbPingResponse": {
+      "type": "object",
+      "properties": {
+        "ok": {
+          "type": "boolean",
+          "format": "boolean"
+        },
+        "version": {
+          "type": "string"
+        },
+        "database": {
+          "type": "string"
+        }
+      },
+      "title": "DbPingResponse",
+      "required": [
+        "ok",
+        "version",
+        "database"
+      ]
+    },
+    "DeleteResp": {
+      "type": "object",
+      "properties": {
+        "ok": {
+          "type": "boolean",
+          "format": "boolean"
+        }
+      },
+      "title": "DeleteResp",
+      "required": [
+        "ok"
+      ]
+    },
+    "DirectoryIdReq": {
+      "type": "object",
+      "title": "DirectoryIdReq"
+    },
+    "DirectoryListResp": {
+      "type": "object",
+      "properties": {
+        "list": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/DirectoryResp"
+          }
+        },
+        "total": {
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "title": "DirectoryListResp",
+      "required": [
+        "list",
+        "total"
+      ]
+    },
+    "DirectoryResp": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "serviceId": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "directory": {
+          "type": "string"
+        }
+      },
+      "title": "DirectoryResp",
+      "required": [
+        "id",
+        "serviceId",
+        "directory"
+      ]
+    },
+    "KeyIdReq": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "title": "KeyIdReq",
+      "required": [
+        "id"
+      ]
+    },
+    "KeyListResp": {
+      "type": "object",
+      "properties": {
+        "list": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/KeyResp"
+          }
+        },
+        "total": {
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "title": "KeyListResp",
+      "required": [
+        "list",
+        "total"
+      ]
+    },
+    "KeyResp": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "iden": {
+          "type": "string"
+        },
+        "name": {
+          "type": "string"
+        },
+        "content": {
+          "type": "string"
+        },
+        "status": {
+          "type": "string"
+        },
+        "createAt": {
+          "type": "string"
+        }
+      },
+      "title": "KeyResp",
+      "required": [
+        "id",
+        "iden",
+        "name",
+        "content",
+        "status",
+        "createAt"
+      ]
+    },
+    "ListDirectoryReq": {
+      "type": "object",
+      "properties": {
+        "page": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "pageSize": {
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "title": "ListDirectoryReq"
+    },
+    "ListKeysReq": {
+      "type": "object",
+      "properties": {
+        "page": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "pageSize": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "name": {
+          "type": "string"
+        },
+        "status": {
+          "type": "string"
+        }
+      },
+      "title": "ListKeysReq"
+    },
+    "ListServiceReq": {
+      "type": "object",
+      "properties": {
+        "page": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "pageSize": {
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "title": "ListServiceReq"
+    },
+    "Request": {
+      "type": "object",
+      "title": "Request"
+    },
+    "Response": {
+      "type": "object",
+      "properties": {
+        "message": {
+          "type": "string"
+        }
+      },
+      "title": "Response",
+      "required": [
+        "message"
+      ]
+    },
+    "ServiceIdReq": {
+      "type": "object",
+      "title": "ServiceIdReq"
+    },
+    "ServiceListResp": {
+      "type": "object",
+      "properties": {
+        "list": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/ServiceResp"
+          }
+        },
+        "total": {
+          "type": "integer",
+          "format": "int64"
+        }
+      },
+      "title": "ServiceListResp",
+      "required": [
+        "list",
+        "total"
+      ]
+    },
+    "ServiceResp": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "name": {
+          "type": "string"
+        },
+        "ip": {
+          "type": "string"
+        },
+        "port": {
+          "type": "string"
+        },
+        "iden": {
+          "type": "string"
+        },
+        "keyIden": {
+          "type": "string"
+        },
+        "keyAlgo": {
+          "type": "string"
+        },
+        "status": {
+          "type": "string"
+        },
+        "createAt": {
+          "type": "string"
+        }
+      },
+      "title": "ServiceResp",
+      "required": [
+        "id",
+        "name",
+        "ip",
+        "port",
+        "iden",
+        "keyIden",
+        "keyAlgo",
+        "status",
+        "createAt"
+      ]
+    },
+    "UpdateDirectoryReq": {
+      "type": "object",
+      "properties": {
+        "serviceId": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "directory": {
+          "type": "string"
+        }
+      },
+      "title": "UpdateDirectoryReq"
+    },
+    "UpdateKeyReq": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "integer",
+          "format": "int64"
+        },
+        "status": {
+          "type": "string"
+        }
+      },
+      "title": "UpdateKeyReq",
+      "required": [
+        "id",
+        "status"
+      ]
+    },
+    "UpdateServiceReq": {
+      "type": "object",
+      "properties": {
+        "name": {
+          "type": "string"
+        },
+        "ip": {
+          "type": "string"
+        },
+        "port": {
+          "type": "string"
+        },
+        "iden": {
+          "type": "string"
+        },
+        "keyIden": {
+          "type": "string"
+        },
+        "keyAlgo": {
+          "type": "string"
+        },
+        "status": {
+          "type": "string"
+        }
+      },
+      "title": "UpdateServiceReq"
+    }
+  },
+  "securityDefinitions": {
+    "apiKey": {
+      "type": "apiKey",
+      "description": "Enter JWT Bearer token **_only_**",
+      "name": "Authorization",
+      "in": "header"
+    }
+  }
+}

+ 5 - 0
etc/udeserver-api.yaml

@@ -2,6 +2,11 @@ Name: udeServer-api
 Host: 0.0.0.0
 Port: 9993
 
+Log:
+  Mode: console
+  Encoding: plain
+  Level: info
+
 MySQL:
   DataSource: "ude:H3G8xzfYpAASYFxF@tcp(10.10.10.30:3306)/ude?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai"
 

+ 24 - 9
go.mod

@@ -1,10 +1,14 @@
 module udeServer
 
-go 1.25.10
+go 1.26.0
 
 require (
 	github.com/google/uuid v1.6.0
+	github.com/minio/minio-go/v7 v7.0.93
+	github.com/robfig/cron/v3 v3.0.1
+	github.com/tjfoc/gmsm v1.4.1
 	github.com/zeromicro/go-zero v1.10.3
+	go.uber.org/zap v1.27.0
 )
 
 require (
@@ -12,25 +16,34 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cenkalti/backoff/v5 v5.0.3 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
-	github.com/fatih/color v1.18.0 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/fatih/color v1.19.0 // indirect
+	github.com/go-ini/ini v1.67.0 // indirect
 	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-sql-driver/mysql v1.10.0 // indirect
+	github.com/goccy/go-json v0.10.5 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
 	github.com/grafana/pyroscope-go v1.3.0 // indirect
 	github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
-	github.com/klauspost/compress v1.18.6 // indirect
-	github.com/mattn/go-colorable v0.1.13 // indirect
-	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/klauspost/compress v1.19.2 // indirect
+	github.com/klauspost/cpuid/v2 v2.4.0 // indirect
+	github.com/mattn/go-colorable v0.1.15 // indirect
+	github.com/mattn/go-isatty v0.0.22 // indirect
+	github.com/minio/crc64nvme v1.1.1 // indirect
+	github.com/minio/md5-simd v1.1.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/openzipkin/zipkin-go v0.4.3 // indirect
 	github.com/pelletier/go-toml/v2 v2.4.3 // indirect
+	github.com/philhofer/fwd v1.2.0 // indirect
 	github.com/prometheus/client_golang v1.23.2 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
 	github.com/prometheus/common v0.66.1 // indirect
 	github.com/prometheus/procfs v0.16.1 // indirect
+	github.com/rs/xid v1.6.0 // indirect
 	github.com/spaolacci/murmur3 v1.1.0 // indirect
+	github.com/tinylib/msgp v1.6.4 // indirect
 	github.com/titanous/json5 v1.0.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
 	go.opentelemetry.io/otel v1.40.0 // indirect
@@ -44,13 +57,15 @@ require (
 	go.opentelemetry.io/otel/trace v1.40.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.9.0 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
+	go.uber.org/multierr v1.10.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.2 // indirect
-	golang.org/x/net v0.50.0 // indirect
-	golang.org/x/sys v0.41.0 // indirect
-	golang.org/x/text v0.34.0 // indirect
+	golang.org/x/crypto v0.57.0 // indirect
+	golang.org/x/net v0.60.0 // indirect
+	golang.org/x/sys v0.48.0 // indirect
+	golang.org/x/text v0.42.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
 	google.golang.org/grpc v1.80.0 // indirect
-	google.golang.org/protobuf v1.36.11 // indirect
+	google.golang.org/protobuf v1.36.12 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )

+ 116 - 19
go.sum

@@ -1,17 +1,29 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
 filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
 github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
 github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
-github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
+github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
+github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
+github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -19,10 +31,27 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
 github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
 github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -35,31 +64,42 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZ
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
 github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
 github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
-github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
-github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
+github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
+github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
+github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
+github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
+github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
+github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
+github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
+github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
+github.com/minio/minio-go/v7 v7.0.93 h1:lAB4QJp8Nq3vDMOU0eKgMuyBiEGMNlXQ5Glc8qAxqSU=
+github.com/minio/minio-go/v7 v7.0.93/go.mod h1:71t2CqDt3ThzESgZUlU1rBN54mksGGlkLcFgguDnnAc=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
 github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
 github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
 github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
+github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
 github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
 github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
 github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
@@ -68,16 +108,24 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
 github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
 github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
+github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
+github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
 github.com/titanous/json5 v1.0.0 h1:hJf8Su1d9NuI/ffpxgxQfxh/UiBFZX7bMPid0rIL/7s=
 github.com/titanous/json5 v1.0.0/go.mod h1:7JH1M8/LHKc6cyP5o5g3CSaRj+mBrIimTxzpvmckH8c=
+github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
+github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
 github.com/zeromicro/go-zero v1.10.3 h1:fm4+jUuUF77IWtFeAyf2xVoBRcgEpF1NZJUqTvZ3dw0=
 github.com/zeromicro/go-zero v1.10.3/go.mod h1:Gnac2bT/JGb9Ja79wchssVeYtJxuWWzL98DuLH11kds=
 go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
@@ -108,26 +156,73 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
 go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
+go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
 go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
-golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
-golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
-golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
-golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
+golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
+golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
+golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
+golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
 gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
 google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
 google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
-google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
-google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
+google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -139,5 +234,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM=
 k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=

+ 215 - 0
internal/audit/archive.go

@@ -0,0 +1,215 @@
+package audit
+
+import (
+	"bufio"
+	"context"
+	"database/sql"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"udeServer/internal/model"
+
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+
+// LogArchive builds a temporary SQL dump file with one INSERT statement per
+// log entry and returns the temp file together with the MinIO object key.
+func LogArchive(logs []*model.TSystemLog, archiveName string) (*os.File, string, error) {
+	tmpDir := os.TempDir()
+	filename := fmt.Sprintf("%s_%d.sql", archiveName, time.Now().UnixNano())
+	tmpPath := filepath.Join(tmpDir, filename)
+	tmpFile, err := os.Create(tmpPath)
+	objectKey := fmt.Sprintf("%s/%s", ObjectDirArchives, filename)
+	if err != nil {
+		return nil, "", fmt.Errorf("create temp file failed: %w", err)
+	}
+	defer func(tmpFile *os.File) {
+		_ = tmpFile.Close()
+	}(tmpFile)
+
+	for _, entry := range logs {
+		stmt := getLogStmt(entry)
+		if _, err := tmpFile.WriteString(stmt); err != nil {
+			return nil, "", fmt.Errorf("writing to file failed: %w", err)
+		}
+	}
+
+	return tmpFile, objectKey, nil
+}
+
+// getLogStmt renders one INSERT statement for the entry. The table is left
+// unqualified so restoring works against whatever schema the DSN points to.
+func getLogStmt(entry *model.TSystemLog) string {
+	return fmt.Sprintf(
+		"INSERT INTO `t_system_log` ("+
+			"`id`, `iden`, `trace_id`, `span_id`, `parent_span_id`, "+
+			"`type`, `service_name`, "+
+			"`uri_path`, `http_method`, `method`, `level`, "+
+			"`app_key`, `user_id`, `user_name`, `client_ip`, "+
+			"`request_header`, `request_body_digest`, `request_body_file_key`, "+
+			"`response_code`, `response_body_digest`, `response_body_file_key`, "+
+			"`error_message`, `duration_ms`, "+
+			"`created_at`, `h_mac`, `audit_status`) VALUES ("+
+			"%d, '%s', '%s', '%s', %s, "+
+			"%d, '%s', "+
+			"%s, %s, '%s', '%s', "+
+			"%s, %s, %s, %s, "+
+			"%s, '%s', '%s', "+
+			"%d, '%s', '%s', "+
+			"%s, %d, "+
+			"'%s', '%s', %d);\n",
+		entry.Id, entry.Iden, entry.TraceId, entry.SpanId, nullOrQuotedString(entry.ParentSpanId),
+		entry.Type, entry.ServiceName,
+		nullOrQuotedString(entry.UriPath), nullOrQuotedString(entry.HttpMethod), entry.Method, entry.Level,
+		nullOrQuotedString(entry.AppKey), nullOrQuotedString(entry.UserId), nullOrQuotedString(entry.UserName), nullOrQuotedString(entry.ClientIp),
+		nullOrQuotedString(entry.RequestHeader), entry.RequestBodyDigest, entry.RequestBodyFileKey,
+		entry.ResponseCode, entry.ResponseBodyDigest, entry.ResponseBodyFileKey,
+		nullOrQuotedString(entry.ErrorMessage), entry.DurationMs,
+		entry.CreatedAt.Format("2006-01-02 15:04:05"), entry.HMac, entry.AuditStatus,
+	)
+}
+
+func nullOrQuotedString(ns sql.NullString) string {
+	if !ns.Valid {
+		return "NULL"
+	}
+	s := ns.String
+	if len(s) > 1 && s[0] == '\'' && s[len(s)-1] == '\'' {
+		return s
+	}
+	// 转义单引号为两个单引号,防止SQL注入或语法错误
+	safeStr := strings.ReplaceAll(s, "'", "''")
+	return "'" + safeStr + "'"
+}
+
+// RestoreSQLStream streams the SQL dump file back into the database,
+// executing batches of statements through execFunc.
+func RestoreSQLStream(ctx context.Context, filePath string, batchSize int, execFunc func(context.Context, []string) error) error {
+	// 打开文件
+	file, err := os.Open(filePath)
+	if err != nil {
+		return err
+	}
+	defer func(file *os.File) {
+		_ = file.Close()
+	}(file)
+
+	reader := bufio.NewReader(file)
+	var buffer strings.Builder
+	var batch []string
+
+	for {
+		line, err := reader.ReadString('\n')
+		if err != nil && err != io.EOF {
+			return err
+		}
+
+		buffer.WriteString(line)
+
+		if strings.HasSuffix(strings.TrimSpace(line), ";") {
+			stmt := strings.TrimSpace(buffer.String())
+			if stmt != "" {
+				batch = append(batch, stmt)
+			}
+			buffer.Reset()
+		}
+
+		// 到达批量大小时,执行一次批量插入
+		if len(batch) >= batchSize {
+			if err := execFunc(ctx, batch); err != nil {
+				return err
+			}
+			// 清空数组
+			batch = batch[:0]
+		}
+
+		if err == io.EOF {
+			break
+		}
+	}
+
+	// 补全插入剩下的数据
+	if len(batch) > 0 {
+		return execFunc(ctx, batch)
+	}
+
+	return nil
+}
+
+// ArchiveLogsToStorageAndRecord uploads the log dump to MinIO and, inside
+// one transaction, deletes the archived rows and records the archive run.
+// The MinIO client is guarded: when it is not loaded the caller should not
+// invoke this function (checked by the business logic).
+func ArchiveLogsToStorageAndRecord(
+	ctx context.Context,
+	logs []*model.TSystemLog,
+	archiveName string,
+	threshold int64,
+	mode int64,
+	minioClient *MinioClient,
+	dbConn sqlx.SqlConn,
+) (int, error) {
+	// 构建归档文件
+	tmpFile, objectKey, err := LogArchive(logs, archiveName)
+	if err != nil {
+		return 0, fmt.Errorf("日志归档文件生成失败: %w", err)
+	}
+
+	// 删除临时文件
+	defer func(tmpFile *os.File) {
+		_ = os.Remove(tmpFile.Name())
+	}(tmpFile)
+
+	// 上传
+	fileKey, err := minioClient.UploadFile(ctx, tmpFile.Name(), objectKey)
+	if err != nil {
+		return 0, fmt.Errorf("归档文件上传失败: %w", err)
+	}
+
+	// 提取 ID
+	var ids []int64
+	for _, entry := range logs {
+		ids = append(ids, entry.Id)
+	}
+
+	const deleteBatchSize = 1000
+
+	// 入库事务
+	err = dbConn.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error {
+		// 删除日志
+		for i := 0; i < len(ids); i += deleteBatchSize {
+			end := i + deleteBatchSize
+			if end > len(ids) {
+				end = len(ids)
+			}
+			batchIds := ids[i:end]
+			placeholders := strings.Repeat("?,", len(batchIds))
+			placeholders = strings.TrimSuffix(placeholders, ",")
+
+			query := fmt.Sprintf("DELETE FROM t_system_log WHERE id IN (%s)", placeholders)
+
+			args := make([]interface{}, len(batchIds))
+			for j, v := range batchIds {
+				args[j] = v
+			}
+
+			if _, err := session.ExecCtx(ctx, query, args...); err != nil {
+				return err
+			}
+		}
+
+		// 插入记录
+		archivedBy := operatorAdmin
+		archivedAt := time.Now()
+		_, err := session.ExecCtx(ctx,
+			"INSERT INTO t_log_archive_record (archive_name, record_count, archive_file_key, archive_mode, batch_size, archived_by, archived_at) VALUES (?,?,?,?,?,?,?)",
+			archiveName, int64(len(logs)), fileKey, mode, threshold, archivedBy, archivedAt)
+		return err
+	})
+
+	return len(logs), err
+}

+ 45 - 0
internal/audit/async.go

@@ -0,0 +1,45 @@
+package audit
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"os"
+
+	udeModel "udeServer/internal/model"
+)
+
+// AsyncInsertLog inserts the log entry in a background goroutine. When the
+// insert fails, the entry is appended to log_fallback.jsonl so the audit
+// chain never silently loses data.
+func AsyncInsertLog(logModel udeModel.TSystemLogModel, entry *udeModel.TSystemLog) {
+	go func(e *udeModel.TSystemLog) {
+		defer func() {
+			if rec := recover(); rec != nil {
+				fmt.Printf("panic in log insert: %v\n", rec)
+			}
+		}()
+		if _, insErr := logModel.Insert(context.Background(), e); insErr != nil {
+			fmt.Printf("system log insert failed: %v\n", insErr)
+			data, mErr := json.Marshal(e)
+			if mErr != nil {
+				fmt.Printf("marshal fallback log error: %v\n", mErr)
+				return
+			}
+			// JSON Line格式
+			f, fErr := os.OpenFile("log_fallback.jsonl", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+			if fErr != nil {
+				fmt.Printf("open fallback file error: %v\n", fErr)
+				return
+			}
+			defer func() {
+				if cErr := f.Close(); cErr != nil {
+					fmt.Printf("close fallback file error: %v\n", cErr)
+				}
+			}()
+			if _, wErr := f.WriteString(string(data) + "\n"); wErr != nil {
+				fmt.Printf("write fallback log error: %v\n", wErr)
+			}
+		}
+	}(entry)
+}

+ 71 - 0
internal/audit/config.go

@@ -0,0 +1,71 @@
+package audit
+
+import (
+	"context"
+	"strconv"
+
+	"udeServer/internal/model"
+)
+
+// FileConfigManager holds the MinIO connection settings loaded from t_config.
+type FileConfigManager struct {
+	endpoint  string
+	bucket    string
+	accessKey string
+	secretKey string
+	useSSL    bool
+}
+
+// LogConfigManager holds the audit logging settings loaded from t_config.
+type LogConfigManager struct {
+	LogRecordLevel   string
+	ArchiveThreshold int64
+	HmacSecret       string
+}
+
+// Reload reads the whole t_config table and extracts the audit-related
+// settings. It never fails hard: a missing table or empty rows simply
+// yields zero-valued managers (the callers degrade gracefully).
+func Reload(configModel model.TConfigModel) (fileConfig FileConfigManager, logConfig LogConfigManager) {
+	configList, err := configModel.FindAll(context.Background())
+	if err != nil {
+		return
+	}
+
+	var useSSLStr string
+	handlers := map[string]func(string){
+		ItemFileEndpoint:   func(v string) { fileConfig.endpoint = v },
+		ItemFileBucket:     func(v string) { fileConfig.bucket = v },
+		ItemFileAccessKey:  func(v string) { fileConfig.accessKey = v },
+		ItemFileSecretKey:  func(v string) { fileConfig.secretKey = v },
+		ItemFileUseSsl:     func(v string) { useSSLStr = v },
+		ItemLogRecordLevel: func(v string) { logConfig.LogRecordLevel = v },
+		ItemArchiveThresh: func(v string) {
+			count, _ := strconv.ParseInt(v, 10, 64)
+			logConfig.ArchiveThreshold = count
+		},
+		ItemLogHmacSecret: func(v string) { logConfig.HmacSecret = v },
+	}
+
+	for _, v := range configList {
+		if handler, ok := handlers[v.ConfigItem]; ok {
+			handler(v.ConfigValue)
+		}
+	}
+	fileConfig.useSSL = useSSLStr == "1"
+	return
+}
+
+// GetGlobalConfig reloads the dynamic configuration and (re)builds the
+// global MinIO client when needed. It returns the currently loaded client
+// (nil when configuration is incomplete/failed) and the log settings.
+func GetGlobalConfig(configModel model.TConfigModel) (*MinioClient, *LogConfigManager) {
+	fileConfig, logConfig := Reload(configModel)
+	ReloadMinioClient(fileConfig)
+
+	return GetMinioClient(), &LogConfigManager{
+		LogRecordLevel:   logConfig.LogRecordLevel,
+		ArchiveThreshold: logConfig.ArchiveThreshold,
+		HmacSecret:       logConfig.HmacSecret,
+	}
+}

+ 63 - 0
internal/audit/constants.go

@@ -0,0 +1,63 @@
+// Package audit ports the sdf-serve audit-logging chain to udeServer:
+// trace/span context helpers, SM3 digests, MinIO raw-body storage,
+// HMAC-SM3 tamper evidence, the HTTP audit middleware (type=1 system log)
+// and the archive/restore machinery.
+package audit
+
+// Context keys for the trace/span fallbacks (go-zero trace comes from OTel).
+const (
+	TraceKey = "trace_id"
+	SpanKey  = "span_id"
+)
+
+// Log types: 1. system log (HTTP requests); 2. interface log (device calls,
+// reserved, currently unused in udeServer).
+const (
+	SystemLog    int64 = 1
+	InterfaceLog int64 = 2
+)
+
+// Audit states of a stored log entry.
+const (
+	UnAudited      int64 = 1 // 未审计
+	AuditedPassed  int64 = 2 // 审计通过
+	AuditedAnomaly int64 = 3 // 审计异常
+)
+
+// Restore states of an archive-restore run.
+const (
+	RestoreSuccess    int64 = 1 // 成功
+	RestoreFail       int64 = 2 // 失败
+	RestoreProcessing int64 = 3 // 处理中
+)
+
+// Archive modes.
+const (
+	ManualArchive int64 = 1 // 手动归档
+	AutoArchive   int64 = 2 // 自动归档
+)
+
+// Dynamic configuration item names stored in t_config.
+const (
+	ItemFileEndpoint   = "file_endpoint"
+	ItemFileBucket     = "file_bucket"
+	ItemFileAccessKey  = "file_access_key"
+	ItemFileSecretKey  = "file_secret_key"
+	ItemFileUseSsl     = "file_use_ssl"
+	ItemLogRecordLevel = "log_record_level"
+	ItemArchiveThresh  = "archive_threshold"
+	ItemLogHmacSecret  = "log_hmac_secret"
+)
+
+// MinIO object paths.
+const (
+	ObjectDirLogs     = "logs"
+	ObjectDirArchives = "archives"
+)
+
+// HmacAlgo identifies the MAC algorithm family used for the tamper chain.
+const HmacAlgo = "HmacSM3"
+
+// operatorAdmin is used as archived_by/restored_by when the request carries
+// no user identity (udeServer has no auth headers yet).
+const operatorAdmin = "admin"

+ 34 - 0
internal/audit/context.go

@@ -0,0 +1,34 @@
+package audit
+
+import (
+	"context"
+	"strings"
+
+	"github.com/google/uuid"
+	"github.com/zeromicro/go-zero/core/trace"
+)
+
+// EnsureTraceAndSpanID resolves the trace/span ids from go-zero's OTel
+// context, generating UUID fallbacks (stored in ctx) when empty. It also
+// returns the caller identity from the RequestMeta, mirroring sdf-serve.
+func EnsureTraceAndSpanID(ctx context.Context) (context.Context, string, string, string, string, string) {
+	traceID := trace.TraceIDFromContext(ctx)
+	spanID := trace.SpanIDFromContext(ctx)
+
+	if traceID == "" {
+		traceID = strings.ReplaceAll(uuid.NewString(), "-", "")
+		ctx = context.WithValue(ctx, TraceKey, traceID)
+	}
+	if spanID == "" {
+		spanID = strings.ReplaceAll(uuid.NewString(), "-", "")
+		ctx = context.WithValue(ctx, SpanKey, spanID)
+	}
+
+	requestMeta := GetRequestMeta(ctx)
+	return ctx, traceID, spanID, requestMeta.UserId, requestMeta.UserName, requestMeta.ApiKey
+}
+
+// NewUUID returns a uuid string without dashes.
+func NewUUID() string {
+	return strings.ReplaceAll(uuid.NewString(), "-", "")
+}

+ 169 - 0
internal/audit/digest.go

@@ -0,0 +1,169 @@
+package audit
+
+import (
+	"bytes"
+	"context"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"io"
+	"mime"
+	"mime/multipart"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/tjfoc/gmsm/sm3"
+)
+
+// UploadAndDigest computes the SM3 digest of the payload and uploads the
+// raw bytes to MinIO under logs/{traceID}/{spanID}/. When the MinIO client
+// is not loaded (no/invalid file config) the upload is skipped and only the
+// digest is returned with an empty file key, so the audit chain keeps
+// working without MinIO.
+// Supported content types: application/json and multipart/form-data.
+func UploadAndDigest(data []byte, traceID, spanID, label string, minioClient *MinioClient, contentType string) (string, string, error) {
+	// 1.计算摘要
+	h := sm3.New()
+	h.Write(data)
+	digest := hex.EncodeToString(h.Sum(nil))
+
+	// 2.构造标签、文件名、文件上传路径
+	safeLabel := strings.Trim(label, "[]")
+	baseName := fmt.Sprintf("%s_%d", safeLabel, time.Now().UnixNano())
+	objectDir := fmt.Sprintf("%s/%s/%s", ObjectDirLogs, traceID, spanID)
+
+	// MinIO 未就绪时只返回摘要
+	if minioClient == nil {
+		return digest, "", nil
+	}
+
+	// 空 body(典型如 GET 请求)无需上传原文
+	if len(data) == 0 {
+		return digest, "", nil
+	}
+
+	// 3.判断请求头类型
+	switch {
+	case strings.HasPrefix(contentType, "application/json"):
+		// application/json
+		fileName := baseName + ".json"
+		objectKey := fmt.Sprintf("%s/%s", objectDir, fileName)
+
+		fileKey, err := writeAndUploadTempFile(minioClient, data, fileName, objectKey)
+		if err != nil {
+			return "", "", fmt.Errorf("minio upload file failed: %w", err)
+		}
+		return digest, fileKey, nil
+	case strings.HasPrefix(contentType, "multipart/form-data"):
+		// multipart/form-data
+		boundary := extractBoundary(contentType)
+		if boundary == "" {
+			return "", "", fmt.Errorf("invalid multipart content-type: %s", contentType)
+		}
+		reader := multipart.NewReader(bytes.NewReader(data), boundary)
+
+		fields := make(map[string]string)
+		fileKeys := make(map[string]string)
+
+		for {
+			part, err := reader.NextPart()
+			if err == io.EOF {
+				break
+			}
+			if err != nil {
+				return "", "", fmt.Errorf("read multipart part failed: %w", err)
+			}
+
+			partData, err := io.ReadAll(part)
+			if err != nil {
+				return "", "", fmt.Errorf("read part data failed: %w", err)
+			}
+
+			if part.FileName() == "" {
+				// 普通字段
+				fields[part.FormName()] = string(partData)
+				continue
+			}
+
+			// 文件字段
+			objectKey := fmt.Sprintf("%s/%s_%s", objectDir, baseName, part.FileName())
+			fileKey, err := writeAndUploadTempFile(minioClient, partData, part.FileName(), objectKey)
+			if err != nil {
+				return "", "", fmt.Errorf("upload file part failed: %w", err)
+			}
+			fileKeys[part.FormName()] = fileKey
+		}
+
+		// 保存字段 JSON
+		fieldsFile := baseName + "_fields.json"
+		fieldsKey := fmt.Sprintf("%s/%s", objectDir, fieldsFile)
+
+		fieldBytes, err := json.MarshalIndent(fields, "", "  ")
+		if err != nil {
+			return "", "", fmt.Errorf("marshal fields failed: %w", err)
+		}
+		if _, err := writeAndUploadTempFile(minioClient, fieldBytes, fieldsFile, fieldsKey); err != nil {
+			return "", "", fmt.Errorf("upload fields failed: %w", err)
+		}
+
+		// 保存元数据 JSON
+		metaJson := map[string]interface{}{
+			"digest":   digest,
+			"trace_id": traceID,
+			"span_id":  spanID,
+			"files":    fileKeys,
+			"fields":   fieldsKey,
+		}
+
+		metaFile := baseName + "_meta.json"
+		metaKey := fmt.Sprintf("%s/%s", objectDir, metaFile)
+
+		metaBytes, err := json.MarshalIndent(metaJson, "", "  ")
+		if err != nil {
+			return "", "", fmt.Errorf("marshal meta failed: %w", err)
+		}
+		if _, err := writeAndUploadTempFile(minioClient, metaBytes, metaFile, metaKey); err != nil {
+			return "", "", fmt.Errorf("upload meta failed: %w", err)
+		}
+
+		return digest, metaKey, nil
+	default:
+		return "", "", fmt.Errorf("unsupported content type: %s", contentType)
+	}
+}
+
+// extractBoundary extracts the boundary from a Content-Type header value.
+func extractBoundary(contentType string) string {
+	_, params, err := mime.ParseMediaType(contentType)
+	if err != nil {
+		return ""
+	}
+	return params["boundary"]
+}
+
+// writeAndUploadTempFile writes data to a temp file, uploads it, and
+// removes the temp file afterwards.
+func writeAndUploadTempFile(minioClient *MinioClient, data []byte, tmpName, objectKey string) (string, error) {
+	tmpFile, err := os.CreateTemp("", tmpName)
+	if err != nil {
+		return "", fmt.Errorf("create temp file failed: %w", err)
+	}
+	defer func() {
+		_ = tmpFile.Close()
+		_ = os.Remove(tmpFile.Name())
+	}()
+
+	if _, err := tmpFile.Write(data); err != nil {
+		return "", fmt.Errorf("write temp file failed: %w", err)
+	}
+	if err := tmpFile.Close(); err != nil {
+		return "", fmt.Errorf("close temp file failed: %w", err)
+	}
+
+	fileKey, err := minioClient.UploadFile(context.Background(), tmpFile.Name(), objectKey)
+	if err != nil {
+		return "", fmt.Errorf("minio upload failed: %w", err)
+	}
+	return fileKey, nil
+}

+ 58 - 0
internal/audit/hmac.go

@@ -0,0 +1,58 @@
+package audit
+
+import (
+	"crypto/hmac"
+	"encoding/hex"
+	"encoding/json"
+
+	udeModel "udeServer/internal/model"
+
+	"github.com/tjfoc/gmsm/sm3"
+)
+
+// CalculateHMAC computes the HMAC-SM3 of the log entry (without the
+// audit-state / HMAC / timestamp / id fields) with the given secret key.
+func CalculateHMAC(entry *udeModel.TSystemLog, secretKey string, macBytes []byte) (string, error) {
+	// 解析log对象为字节数组,并清空旧的Hmac
+	data, err := generateHmacBytes(entry)
+	if err != nil {
+		return "", err
+	}
+
+	// 计算Hmac
+	mac := hmac.New(sm3.New, []byte(secretKey))
+	mac.Write(data)
+
+	// 写入复用缓冲区,只清空长度,不清空容量
+	entryHmac := mac.Sum(macBytes[:0])
+
+	return hex.EncodeToString(entryHmac), nil
+}
+
+// generateHmacBytes marshals the entry and strips the fields excluded from
+// the MAC computation. NOTE: TSystemLog has no json tags on purpose, so the
+// marshalled keys equal the Go field names and this exclusion list stays in
+// sync with the struct definition.
+func generateHmacBytes(entry any) ([]byte, error) {
+	data, err := json.Marshal(entry)
+	if err != nil {
+		return nil, err
+	}
+
+	var m map[string]interface{}
+	if err = json.Unmarshal(data, &m); err != nil {
+		return nil, err
+	}
+
+	// 移除Hmac无关字段
+	delete(m, "AuditStatus")
+	delete(m, "HMac")
+	delete(m, "CreatedAt")
+	delete(m, "Id")
+
+	signContent, err := json.Marshal(m)
+	if err != nil {
+		return nil, err
+	}
+	return signContent, nil
+}

+ 47 - 0
internal/audit/level.go

@@ -0,0 +1,47 @@
+package audit
+
+import (
+	"strings"
+
+	"udeServer/internal/response"
+)
+
+// ChooseLevelByBusinessCode maps a unified-response business code to a log
+// level. There is no gap for 300000-500000 like in sdf-serve because
+// udeServer always answers with HTTP 200 and encodes errors in the body.
+// Unknown codes are treated as warnings except the server-error code.
+func ChooseLevelByBusinessCode(code int64) string {
+	switch code {
+	case response.CodeOK:
+		return "INFO"
+	case response.CodeServerError:
+		return "ERROR"
+	default:
+		// 100001 参数错误 / 100002 不存在 以及未来新增的业务错误码
+		return "WARN"
+	}
+}
+
+// SaveToDBByLogLevel decides whether a log entry is persisted: the stored
+// entry is kept when its level is at or above the configured level. An
+// empty or unrecognized configured level falls back to INFO so that the
+// audit chain keeps working before t_config is populated.
+func SaveToDBByLogLevel(configLevel, actualLevel string) bool {
+	// DEBUG < INFO < WARN < ERROR
+	levelOrder := map[string]int{
+		"DEBUG": 1,
+		"INFO":  2,
+		"WARN":  3,
+		"ERROR": 4,
+	}
+
+	effective := strings.ToUpper(strings.TrimSpace(configLevel))
+	if _, ok := levelOrder[effective]; !ok {
+		effective = "INFO"
+	}
+	actual, ok := levelOrder[actualLevel]
+	if !ok {
+		return false
+	}
+	return levelOrder[effective] <= actual
+}

+ 37 - 0
internal/audit/meta.go

@@ -0,0 +1,37 @@
+package audit
+
+import (
+	"context"
+)
+
+// RequestMeta carries per-request caller identity parsed from HTTP headers.
+// udeServer has no auth layer yet, so the fields stay empty unless a future
+// middleware fills them in.
+type RequestMeta struct {
+	UserId    string
+	UserName  string
+	UserToken string
+	ApiKey    string
+	ApiSecret string
+	ApiHmac   string
+}
+
+type ctxKeyRequestMeta struct{}
+
+// RequestMetaKey is the context key of RequestMeta.
+var RequestMetaKey = ctxKeyRequestMeta{}
+
+// WithRequestMeta stores RequestMeta in ctx.
+func WithRequestMeta(ctx context.Context, m RequestMeta) context.Context {
+	return context.WithValue(ctx, RequestMetaKey, m)
+}
+
+// GetRequestMeta returns the RequestMeta carried by ctx, or the zero value.
+func GetRequestMeta(ctx context.Context) RequestMeta {
+	if v := ctx.Value(RequestMetaKey); v != nil {
+		if m, ok := v.(RequestMeta); ok {
+			return m
+		}
+	}
+	return RequestMeta{}
+}

+ 288 - 0
internal/audit/middleware.go

@@ -0,0 +1,288 @@
+package audit
+
+import (
+	"bytes"
+	"database/sql"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"strings"
+	"time"
+
+	"udeServer/internal/log"
+	udeModel "udeServer/internal/model"
+	"udeServer/internal/response"
+)
+
+// 捕获响应
+type wrappedResponseWriter struct {
+	http.ResponseWriter
+	statusCode int
+	body       *bytes.Buffer
+}
+
+func newWrappedResponseWriter(w http.ResponseWriter) *wrappedResponseWriter {
+	return &wrappedResponseWriter{
+		ResponseWriter: w,
+		statusCode:     http.StatusOK,
+		body:           bytes.NewBuffer(nil),
+	}
+}
+
+// Write re-records the response body (buffered for the audit entry).
+func (w *wrappedResponseWriter) Write(b []byte) (int, error) {
+	w.body.Write(b)
+	return w.ResponseWriter.Write(b)
+}
+
+// WriteHeader re-records the status code.
+func (w *wrappedResponseWriter) WriteHeader(code int) {
+	w.statusCode = code
+	w.ResponseWriter.WriteHeader(code)
+}
+
+// genericResp parses the unified response envelope.
+type genericResp struct {
+	Code    int             `json:"code"`
+	Message string          `json:"message"`
+	Data    json.RawMessage `json:"data"`
+}
+
+// skipLogPaths silences self-generation noise: the log management endpoints
+// would otherwise audit every entry they produce, and the archive download
+// response is a huge binary blob with no audit value.
+var skipLogPaths = map[string]bool{
+	"/api/v1/systemLog/list":      true,
+	"/api/v1/systemLog/audit":     true,
+	"/api/v1/systemLog/link":      true,
+	"/api/v1/logRestore/list":     true,
+	"/api/v1/logArchive/list":     true,
+	"/api/v1/logArchive/download": true,
+}
+
+// LogMiddleware audits every HTTP request: it captures request/response
+// bodies, computes digests, uploads the raw bodies to MinIO, builds a
+// t_system_log record guarded by an HMAC-SM3 signature and stores it
+// asynchronously. Deleted per sdf-serve build: the entry is built from the
+// unified response envelope (udeServer answers HTTP 200 always, business
+// codes live in the body), so ResponseCode records the body code.
+type LogMiddleware struct {
+}
+
+func NewLogMiddleware() *LogMiddleware {
+	return &LogMiddleware{}
+}
+
+// Handle is the middleware closure, constructed with the system-log and
+// config models plus the service name of this deployment.
+func (m *LogMiddleware) Handle(logModel udeModel.TSystemLogModel, configModel udeModel.TConfigModel, serviceName string) func(next http.HandlerFunc) http.HandlerFunc {
+	return func(next http.HandlerFunc) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			if skipLogPaths[r.URL.Path] {
+				next.ServeHTTP(w, r)
+				return
+			}
+			// 1.获取TraceId和SpanId以及请求元数据
+			ctx, traceID, spanID, userID, userName, _ := EnsureTraceAndSpanID(r.Context())
+
+			// 2.读请求体
+			reqBody, _ := io.ReadAll(r.Body)
+			r.Body = io.NopCloser(bytes.NewBuffer(reqBody))
+			reqHeader := filterRequestHeader(r.Header)
+			log.RecordDebug(ctx, "请求头:%v", reqHeader)
+
+			// 3.捕获响应
+			rw := newWrappedResponseWriter(w)
+			start := time.Now()
+			next.ServeHTTP(rw, r.WithContext(ctx))
+			duration := time.Since(start).Milliseconds()
+
+			// 4.解析响应统一封装
+			respBody := rw.body.Bytes()
+			var resp genericResp
+			httpCode := int64(response.CodeServerError)
+			message := ""
+			parseFailed := false
+			if err := json.Unmarshal(respBody, &resp); err != nil {
+				parseFailed = true
+				log.RecordWarn(ctx, "[LogHandle] 无法解析响应 JSON, Path:%s, Status:%d, Raw:%s",
+					r.URL.Path, rw.statusCode, string(respBody))
+			} else {
+				httpCode = int64(resp.Code)
+				message = resp.Message
+				if resp.Code != response.CodeOK {
+					log.RecordError(ctx, fmt.Errorf("[Request-Fail] Path=%s Code=%d Msg=%q",
+						r.URL.Path, resp.Code, resp.Message))
+				} else {
+					log.RecordInfo(ctx, "[Request-Success] Path=%s Code=%d Msg=%s Data:%+v",
+						r.URL.Path, resp.Code, resp.Message, string(resp.Data))
+				}
+			}
+
+			// 5. 上传 & 摘要
+			client, logConfig := GetGlobalConfig(configModel)
+			reqDigestStr, reqUrl, reqErr := UploadAndDigest(reqBody, traceID, spanID, "[Http-Request]", client, r.Header.Get("Content-Type"))
+			if reqErr != nil {
+				log.RecordError(ctx, reqErr)
+			}
+			respDigestStr, respUrl, respErr := UploadAndDigest(respBody, traceID, spanID, "[Http-Response]", client, "application/json")
+			if respErr != nil {
+				log.RecordError(ctx, respErr)
+			}
+
+			// 6.构造日志对象
+			entry := &udeModel.TSystemLog{
+				Iden:                NewUUID(),
+				TraceId:             traceID,
+				SpanId:              spanID,
+				ParentSpanId:        sql.NullString{String: "", Valid: false}, //顶层链路父SpanId为空时填写null
+				Type:                SystemLog,
+				ServiceName:         serviceName,
+				UriPath:             sql.NullString{String: r.URL.Path, Valid: true},
+				HttpMethod:          sql.NullString{String: r.Method, Valid: true},
+				Method:              deriveHandlerName(r.URL.Path),
+				Level:               ChooseLevelByBusinessCode(httpCode),
+				AppKey:              sql.NullString{Valid: false},
+				UserId:              sql.NullString{String: userID, Valid: userID != ""},
+				UserName:            sql.NullString{String: userName, Valid: userName != ""},
+				ClientIp:            sql.NullString{String: GetClientIPPreferPublic(r), Valid: true},
+				RequestHeader:       reqHeader,
+				RequestBodyDigest:   reqDigestStr,
+				RequestBodyFileKey:  reqUrl,
+				ResponseCode:        httpCode,
+				ResponseBodyDigest:  respDigestStr,
+				ResponseBodyFileKey: respUrl,
+				DurationMs:          duration,
+				CreatedAt:           time.Now(),
+				AuditStatus:         UnAudited,
+			}
+
+			// 7.填充ErrMsg
+			switch {
+			case parseFailed:
+				entry.ErrorMessage = sql.NullString{String: "Invalid response format", Valid: true}
+			case resp.Code != response.CodeOK:
+				entry.ErrorMessage = sql.NullString{String: message, Valid: true}
+			default:
+				entry.ErrorMessage = sql.NullString{Valid: false}
+			}
+
+			// 8.填充Hmac值,复用缓冲区;密钥为空时用空串计算,保持列非空
+			macBuffer := make([]byte, 0, 32)
+			if mac, hErr := CalculateHMAC(entry, logConfig.HmacSecret, macBuffer); hErr == nil {
+				entry.HMac = mac
+			} else {
+				log.RecordError(ctx, hErr)
+			}
+
+			// 9.按配置库中 log_record_level 过滤后异步入库
+			save := SaveToDBByLogLevel(logConfig.LogRecordLevel, entry.Level)
+			if save {
+				go AsyncInsertLog(logModel, entry)
+			}
+		}
+	}
+}
+
+// deriveHandlerName derives the business handler name from the request path
+// (last path segment + "Handler").
+func deriveHandlerName(path string) string {
+	clean := strings.Trim(path, "/")
+	parts := strings.Split(clean, "/")
+	if len(parts) == 0 {
+		return ""
+	}
+	last := parts[len(parts)-1]
+	return last + "Handler"
+}
+
+var keepHeaders = map[string]bool{
+	"content-type": true,
+	// x-api-key/x-user-id 等 udeServer 还没有鉴权体系,暂无注入来源
+}
+
+// filterRequestHeader keeps only whitelisted request headers.
+func filterRequestHeader(header map[string][]string) sql.NullString {
+	filtered := make(map[string][]string)
+
+	for k, v := range header {
+		if keepHeaders[strings.ToLower(k)] {
+			filtered[k] = v
+		}
+	}
+
+	if headerBytes, err := json.Marshal(filtered); err == nil {
+		return sql.NullString{
+			String: string(headerBytes),
+			Valid:  true,
+		}
+	}
+
+	return sql.NullString{Valid: false}
+}
+
+// GetClientIPPreferPublic extracts the client IP, preferring public IPs.
+func GetClientIPPreferPublic(r *http.Request) string {
+	ip := getClientIP(r, true)
+	if ip == "" {
+		ip = getClientIP(r, false)
+	}
+	return ip
+}
+
+// getClientIP resolves the client IP honouring proxy headers, optionally
+// hiding private addresses.
+func getClientIP(r *http.Request, excludePrivate bool) string {
+	// 1. 从 X-Forwarded-For 获取第一个真实IP
+	xff := r.Header.Get("X-Forwarded-For")
+	if xff != "" {
+		parts := strings.Split(xff, ",")
+		for _, ip := range parts {
+			ip = strings.TrimSpace(ip)
+			if ip != "" && (!excludePrivate || isPublicIP(ip)) {
+				return ip
+			}
+		}
+	}
+
+	// 2. 从 X-Real-IP 获取反向代理地址
+	if ip := strings.TrimSpace(r.Header.Get("X-Real-IP")); ip != "" {
+		if !excludePrivate || isPublicIP(ip) {
+			return ip
+		}
+	}
+
+	// 3. 从 RemoteAddr 获取代理地址
+	ip, _, err := net.SplitHostPort(r.RemoteAddr)
+	if err == nil && (!excludePrivate || isPublicIP(ip)) {
+		return ip
+	}
+
+	return ""
+}
+
+// isPublicIP reports whether the IP address is public/external.
+func isPublicIP(ip string) bool {
+	parsedIP := net.ParseIP(ip)
+	if parsedIP == nil {
+		return false
+	}
+
+	privateBlocks := []*net.IPNet{
+		// IPv4 私有地址段
+		{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)},
+		{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)},
+		{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)},
+		// IPv6 loopback
+		{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)},
+	}
+
+	for _, block := range privateBlocks {
+		if block.Contains(parsedIP) {
+			return false
+		}
+	}
+	return true
+}

+ 166 - 0
internal/audit/minio.go

@@ -0,0 +1,166 @@
+package audit
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"sync/atomic"
+	"time"
+
+	"github.com/minio/minio-go/v7"
+	"github.com/minio/minio-go/v7/pkg/credentials"
+)
+
+var (
+	minioClient atomic.Value
+	// clientSignature caches the config signature of the loaded client so a
+	// hot reload that keeps the same endpoint/keys/bucket reuses the client
+	// instead of rebuilding it on every request.
+	clientSignature atomic.Value
+)
+
+// MinioClient wraps a minio client together with the target bucket.
+type MinioClient struct {
+	Client     *minio.Client
+	BucketName string
+}
+
+// GetMinioClient atomically reads the global MinioClient (nil if not set).
+func GetMinioClient() *MinioClient {
+	v := minioClient.Load()
+	if v == nil {
+		return nil
+	}
+	return v.(*MinioClient)
+}
+
+// SetMinioClient atomically writes the global MinioClient.
+func SetMinioClient(c *MinioClient) {
+	minioClient.Store(c)
+}
+
+// newMinioClient builds a MinioClient and makes sure the bucket exists.
+func newMinioClient(endPoint, accessKey, secretKey, bucketName string, useSSl bool) (*MinioClient, error) {
+	client, err := minio.New(endPoint, &minio.Options{
+		Creds:  credentials.NewStaticV4(accessKey, secretKey, ""),
+		Secure: useSSl,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	ctx := context.Background()
+	exists, err := client.BucketExists(ctx, bucketName)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		if err := client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}); err != nil {
+			return nil, err
+		}
+	}
+
+	return &MinioClient{
+		Client:     client,
+		BucketName: bucketName,
+	}, nil
+}
+
+// ReloadMinioClient rebuilds the global client when the file configuration
+// changed. An empty configuration (before t_config has been populated) or a
+// failed connection keeps the previously loaded client untouched.
+func ReloadMinioClient(fileConfig FileConfigManager) {
+	if fileConfig.endpoint == "" || fileConfig.bucket == "" ||
+		fileConfig.accessKey == "" || fileConfig.secretKey == "" {
+		return
+	}
+
+	signature := fmt.Sprintf("%s|%s|%s|%s|%t",
+		fileConfig.endpoint, fileConfig.accessKey, fileConfig.secretKey, fileConfig.bucket, fileConfig.useSSL)
+	if stored, ok := clientSignature.Load().(string); ok && stored == signature {
+		return // configuration unchanged, keep the loaded client
+	}
+
+	client, err := newMinioClient(fileConfig.endpoint, fileConfig.accessKey, fileConfig.secretKey, fileConfig.bucket, fileConfig.useSSL)
+	if err != nil {
+		fmt.Printf("Minio client reload failed: %v, keep previous client\n", err)
+		return
+	}
+
+	clientSignature.Store(signature)
+	SetMinioClient(client)
+}
+
+// UploadFile uploads a local file and returns its object key.
+func (m *MinioClient) UploadFile(ctx context.Context, localFilePath, objectName string) (string, error) {
+	file, err := os.Open(localFilePath)
+	if err != nil {
+		return "", err
+	}
+	defer func() {
+		_ = file.Close()
+	}()
+
+	fileStat, err := file.Stat()
+	if err != nil {
+		return "", err
+	}
+
+	uploadInfo, err := m.Client.PutObject(ctx, m.BucketName, objectName, file, fileStat.Size(), minio.PutObjectOptions{})
+	if err != nil {
+		return "", err
+	}
+
+	return uploadInfo.Key, nil
+}
+
+// DownloadFile downloads an object into a local temp file and returns the
+// temp file path.
+func (m *MinioClient) DownloadFile(ctx context.Context, fileKey string) (string, error) {
+	fileName := filepath.Base(fileKey)
+	if fileName == "" || fileName == "." || fileName == "/" {
+		return "", fmt.Errorf("无效的FileKey:%s", fileKey)
+	}
+
+	tmpFile, err := os.CreateTemp("", fmt.Sprintf("log_restore_*_%s", fileName))
+	if err != nil {
+		return "", fmt.Errorf("创建临时文件失败: %w", err)
+	}
+	defer func() {
+		_ = tmpFile.Close()
+	}()
+
+	object, err := m.Client.GetObject(ctx, m.BucketName, fileKey, minio.GetObjectOptions{})
+	if err != nil {
+		return "", fmt.Errorf("从 MinIO 获取对象失败: %w", err)
+	}
+	defer object.Close()
+
+	if _, err = io.Copy(tmpFile, object); err != nil {
+		return "", fmt.Errorf("写入临时文件失败: %w", err)
+	}
+
+	return tmpFile.Name(), nil
+}
+
+// GetTempDownloadURL returns a presigned private-bucket download URL.
+func (m *MinioClient) GetTempDownloadURL(ctx context.Context, fileKey string, expire int32) (string, error) {
+	url, err := m.Client.PresignedGetObject(ctx, m.BucketName, fileKey, time.Duration(expire)*time.Second, nil)
+	if err != nil {
+		return "", err
+	}
+	return url.String(), nil
+}
+
+// RemoveFile deletes an object from the bucket.
+func (m *MinioClient) RemoveFile(ctx context.Context, fileKey string) error {
+	if fileKey == "" {
+		return fmt.Errorf("无效的FileKey:%s", fileKey)
+	}
+	if err := m.Client.RemoveObject(ctx, m.BucketName, fileKey, minio.RemoveObjectOptions{}); err != nil {
+		return fmt.Errorf("remove object failed: %w", err)
+	}
+	return nil
+}

+ 2 - 0
internal/config/config.go

@@ -12,6 +12,8 @@ type Config struct {
 	rest.RestConf
 	MySQL sqlx.SqlConf
 	Mount MountConf
+	// 注意: rest.RestConf 内嵌 service.ServiceConf 自带 Log logx.LogConf
+	// (yaml 的 Log: 段,经匿名内嵌提升为 c.Log),供 internal/log 使用。
 }
 type MountConf struct {
 	BaseDir string `json:",default=/var/ude/mnt"`

+ 77 - 4
internal/handler/routes.go

@@ -6,6 +6,7 @@ package handler
 import (
 	"net/http"
 
+	systemlog "udeServer/internal/handler/systemlog"
 	"udeServer/internal/svc"
 
 	"github.com/zeromicro/go-zero/rest"
@@ -44,6 +45,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
 				Path:    "/directories/:id",
 				Handler: DeleteDirectoryHandler(serverCtx),
 			},
+			{
+				Method:  http.MethodGet,
+				Path:    "/dirs/children",
+				Handler: ListDirChildrenHandler(serverCtx),
+			},
 			{
 				Method:  http.MethodGet,
 				Path:    "/from/:name",
@@ -104,12 +110,79 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
 				Path:    "/services/:id/brush",
 				Handler: BrushServiceHandler(serverCtx),
 			},
+		},
+		rest.WithPrefix("/api/v1"),
+	)
+
+	server.AddRoutes(
+		[]rest.Route{
 			{
-				Method:  http.MethodGet,
-				Path:    "/dirs/children",
-				Handler: ListDirChildrenHandler(serverCtx),
+				// 日志归档
+				Method:  http.MethodPost,
+				Path:    "/archive",
+				Handler: systemlog.LogArchiveHandler(serverCtx),
+			},
+			{
+				// 日志审计(校验Hmac防篡改)
+				Method:  http.MethodPost,
+				Path:    "/audit",
+				Handler: systemlog.LogAuditHandler(serverCtx),
+			},
+			{
+				// 日志详情
+				Method:  http.MethodPost,
+				Path:    "/detail",
+				Handler: systemlog.LogDetailHandler(serverCtx),
+			},
+			{
+				// 查看完整链路
+				Method:  http.MethodPost,
+				Path:    "/link",
+				Handler: systemlog.LogLinkHandler(serverCtx),
+			},
+			{
+				// 日志分页列表
+				Method:  http.MethodPost,
+				Path:    "/list",
+				Handler: systemlog.LogListHandler(serverCtx),
 			},
 		},
-		rest.WithPrefix("/api/v1"),
+		rest.WithPrefix("/api/v1/systemLog"),
+	)
+
+	server.AddRoutes(
+		[]rest.Route{
+			{
+				// 归档打包下载(zip)
+				Method:  http.MethodPost,
+				Path:    "/download",
+				Handler: systemlog.LogArchiveDownloadHandler(serverCtx),
+			},
+			{
+				// 归档列表
+				Method:  http.MethodPost,
+				Path:    "/list",
+				Handler: systemlog.LogArchiveListHandler(serverCtx),
+			},
+			{
+				// 归档恢复
+				Method:  http.MethodPost,
+				Path:    "/restore",
+				Handler: systemlog.LogArchiveRestoreHandler(serverCtx),
+			},
+		},
+		rest.WithPrefix("/api/v1/logArchive"),
+	)
+
+	server.AddRoutes(
+		[]rest.Route{
+			{
+				// 恢复记录列表
+				Method:  http.MethodPost,
+				Path:    "/list",
+				Handler: systemlog.LogRestoreListHandler(serverCtx),
+			},
+		},
+		rest.WithPrefix("/api/v1/logRestore"),
 	)
 }

+ 30 - 0
internal/handler/systemlog/logarchivedownloadhandler.go

@@ -0,0 +1,30 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/systemlog"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 归档打包下载(zip)
+func LogArchiveDownloadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.LogArchiveDownloadReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := systemlog.NewLogArchiveDownloadLogic(r.Context(), svcCtx)
+		// zip 二进制直接写入 w,不调用 httpx.OkJsonCtx(统一封装会破坏 zip 流)
+		if err := l.LogArchiveDownload(w, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		}
+	}
+}

+ 32 - 0
internal/handler/systemlog/logarchivehandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/systemlog"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 日志归档
+func LogArchiveHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.SystemLogArchiveReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := systemlog.NewLogArchiveLogic(r.Context(), svcCtx)
+		resp, err := l.LogArchive(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/systemlog/logarchivelisthandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/systemlog"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 归档列表
+func LogArchiveListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.LogArchiveListReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := systemlog.NewLogArchiveListLogic(r.Context(), svcCtx)
+		resp, err := l.LogArchiveList(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/systemlog/logarchiverestorehandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/systemlog"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 归档恢复
+func LogArchiveRestoreHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.LogArchiveRestoreReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := systemlog.NewLogArchiveRestoreLogic(r.Context(), svcCtx)
+		resp, err := l.LogArchiveRestore(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/systemlog/logaudithandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/systemlog"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 日志审计(校验Hmac防篡改)
+func LogAuditHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.SystemLogAuditReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := systemlog.NewLogAuditLogic(r.Context(), svcCtx)
+		resp, err := l.LogAudit(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/systemlog/logdetailhandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/systemlog"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 日志详情
+func LogDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.SystemLogDetailReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := systemlog.NewLogDetailLogic(r.Context(), svcCtx)
+		resp, err := l.LogDetail(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/systemlog/loglinkhandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/systemlog"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 查看完整链路
+func LogLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.SystemLogLinkReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := systemlog.NewLogLinkLogic(r.Context(), svcCtx)
+		resp, err := l.LogLink(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/systemlog/loglisthandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/systemlog"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 日志分页列表
+func LogListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.SystemLogListReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := systemlog.NewLogListLogic(r.Context(), svcCtx)
+		resp, err := l.LogList(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 32 - 0
internal/handler/systemlog/logrestorelisthandler.go

@@ -0,0 +1,32 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic/systemlog"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// 恢复记录列表
+func LogRestoreListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.LogRestoreListReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := systemlog.NewLogRestoreListLogic(r.Context(), svcCtx)
+		resp, err := l.LogRestoreList(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 82 - 0
internal/log/log.go

@@ -0,0 +1,82 @@
+// Package log provides the colored console logger setup and the business
+// Record* helpers, ported from sdf-serve pkg/common/log.
+package log
+
+import (
+	"context"
+	stdErrors "errors"
+	"fmt"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/response"
+
+	"github.com/zeromicro/go-zero/core/logx"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// SetupLoggerWithColor replaces the global zap logger with a colored
+// development configuration. It only takes effect when Log.Mode is "console";
+// otherwise go-zero keeps its default logging behaviour.
+func SetupLoggerWithColor(conf logx.LogConf) {
+	if conf.Mode != "console" {
+		return
+	}
+
+	cfg := zap.NewDevelopmentConfig()
+	cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
+	cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
+	cfg.Level = zap.NewAtomicLevelAt(getZapLevel(conf.Level))
+
+	logger, err := cfg.Build()
+	if err != nil {
+		panic(err)
+	}
+	zap.ReplaceGlobals(logger)
+}
+
+func getZapLevel(level string) zapcore.Level {
+	switch level {
+	case "debug":
+		return zapcore.DebugLevel
+	case "info":
+		return zapcore.InfoLevel
+	case "warn":
+		return zapcore.WarnLevel
+	case "error":
+		return zapcore.ErrorLevel
+	default:
+		return zapcore.InfoLevel
+	}
+}
+
+// RecordError logs an error. For HTTP-aware errors (apierr.Error) the mapped
+// business code is reported as well; anything else is logged as a system
+// error with its full cause.
+func RecordError(ctx context.Context, err error) {
+	var apiErr *apierr.Error
+	if stdErrors.As(err, &apiErr) {
+		logx.WithContext(ctx).Errorf("【BUSINESS_ERROR】Status:%d, Code:%d, Msg:%s",
+			apiErr.Status, response.Code(err), apiErr.Msg)
+	} else {
+		logx.WithContext(ctx).Errorf("【SYSTEM_ERROR】Cause: %+v", err)
+	}
+}
+
+// RecordInfo logs ordinary business information.
+func RecordInfo(ctx context.Context, format string, args ...interface{}) {
+	logx.WithContext(ctx).Infof("【BUSINESS_INFO】"+format, args...)
+}
+
+// RecordWarn logs issues worth attention that do not necessarily alert.
+// go-zero logx has no Warn level, so warnings go through the global zap
+// logger (same as sdf-serve); the ctx parameter stays for call-site
+// compatibility.
+func RecordWarn(ctx context.Context, format string, args ...interface{}) {
+	zap.L().Warn(fmt.Sprintf("【BUSINESS_WARN】"+format, args...))
+}
+
+// RecordDebug logs development-time debug information.
+func RecordDebug(ctx context.Context, format string, args ...interface{}) {
+	logx.WithContext(ctx).Debugf("【BUSINESS_DEBUG】"+format, args...)
+}

+ 1 - 1
internal/logic/brushservicelogic.go

@@ -37,4 +37,4 @@ func (l *BrushServiceLogic) BrushService(req *types.ServiceIdReq) (*types.Delete
 
 	l.Infof("brush config dispatched, service id: %d", req.Id)
 	return &types.DeleteResp{Ok: true}, nil
-}
+}

+ 6 - 8
internal/logic/listdirchildrenlogic.go

@@ -12,7 +12,6 @@ package logic
 import (
 	"context"
 	"encoding/json"
-	"fmt"
 	"io"
 	"net"
 	"net/http"
@@ -131,20 +130,20 @@ func (l *ListDirChildrenLogic) fetchRemoteList(ip, port, dir string) (*remoteLis
 
 	httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
 	if err != nil {
-		return nil, apierr.BadRequest(fmt.Sprintf("构造远程请求失败: %v", err))
+		return nil, apierr.BadRequest("构造远程请求失败: %v", err)
 	}
 
 	httpResp, err := http.DefaultClient.Do(httpReq)
 	if err != nil {
 		l.Errorf("call remote /list %s failed: %v", u.String(), err)
-		return nil, apierr.BadRequest(fmt.Sprintf("调用远程接口失败: %v", err))
+		return nil, apierr.BadRequest("调用远程接口失败: %v", err)
 	}
 	defer httpResp.Body.Close()
 
 	// 保护性限制:一个目录条目正常远小于 8MB
 	body, readErr := io.ReadAll(io.LimitReader(httpResp.Body, 8<<20))
 	if readErr != nil {
-		return nil, apierr.BadRequest(fmt.Sprintf("读取远程响应失败: %v", readErr))
+		return nil, apierr.BadRequest("读取远程响应失败: %v", readErr)
 	}
 
 	switch httpResp.StatusCode {
@@ -152,17 +151,16 @@ func (l *ListDirChildrenLogic) fetchRemoteList(ip, port, dir string) (*remoteLis
 		// ok,继续解析
 	case http.StatusBadRequest:
 		// 远程 400 自带中文说明,原样透传给前端
-		return nil, apierr.BadRequest(extractRemoteMessage(body))
+		return nil, apierr.BadRequest("%s", extractRemoteMessage(body))
 	case http.StatusMethodNotAllowed:
 		return nil, apierr.BadRequest("远程服务拒绝了 GET 请求")
 	default:
-		return nil, apierr.BadRequest(fmt.Sprintf(
-			"远程接口异常(%d): %s", httpResp.StatusCode, extractRemoteMessage(body)))
+		return nil, apierr.BadRequest("远程接口异常(%d): %s", httpResp.StatusCode, extractRemoteMessage(body))
 	}
 
 	var out remoteListResp
 	if err := json.Unmarshal(body, &out); err != nil {
-		return nil, apierr.BadRequest(fmt.Sprintf("解析远程响应失败: %v", err))
+		return nil, apierr.BadRequest("解析远程响应失败: %v", err)
 	}
 	return &out, nil
 }

+ 78 - 0
internal/logic/systemlog/convert.go

@@ -0,0 +1,78 @@
+package systemlog
+
+import (
+	udeModel "udeServer/internal/model"
+	"udeServer/internal/types"
+)
+
+// ConvertToSystemLog maps the model row to the API response type.
+func ConvertToSystemLog(v *udeModel.TSystemLog) types.SystemLog {
+	return types.SystemLog{
+		Id:                  v.Id,
+		Iden:                v.Iden,
+		TraceId:             v.TraceId,
+		SpanId:              v.SpanId,
+		ParentSpanId:        v.ParentSpanId.String,
+		Type:                int(v.Type),
+		ServiceName:         v.ServiceName,
+		UriPath:             v.UriPath.String,
+		HttpMethod:          v.HttpMethod.String,
+		Method:              v.Method,
+		Level:               v.Level,
+		AppKey:              v.AppKey.String,
+		UserId:              v.UserId.String,
+		UserName:            v.UserName.String,
+		ClientIp:            v.ClientIp.String,
+		RequestHeader:       v.RequestHeader.String,
+		RequestBodyDigest:   v.RequestBodyDigest,
+		RequestBodyFileKey:  v.RequestBodyFileKey,
+		ResponseCode:        v.ResponseCode,
+		ResponseBodyDigest:  v.ResponseBodyDigest,
+		ResponseBodyFileKey: v.ResponseBodyFileKey,
+		ErrorMsg:            v.ErrorMessage.String,
+		DurationMs:          v.DurationMs,
+		CreatedAt:           v.CreatedAt.Format("2006-01-02 15:04:05"),
+		Hmac:                v.HMac,
+		AuditStatus:         int(v.AuditStatus),
+	}
+}
+
+// ConvertToLogArchive maps the model row to the API response type.
+func ConvertToLogArchive(v *udeModel.TLogArchiveRecord) types.LogArchive {
+	return types.LogArchive{
+		Id:             v.Id,
+		ArchiveName:    v.ArchiveName,
+		RecordCount:    v.RecordCount,
+		ArchiveFileKey: v.ArchiveFileKey,
+		ArchiveMode:    int(v.ArchiveMode),
+		BatchSize:      int(v.BatchSize),
+		ArchivedBy:     v.ArchivedBy,
+		ArchivedAt:     v.ArchivedAt.Format("2006-01-02 15:04:05"),
+	}
+}
+
+// ConvertToLogRestore maps the model row to the API response type.
+func ConvertToLogRestore(v *udeModel.TLogRestoreRecord) types.LogRestore {
+	return types.LogRestore{
+		Id:             v.Id,
+		ArchiveId:      v.ArchiveId,
+		RestoreStatus:  int(v.RestoreStatus),
+		RestoreMessage: v.RestoreMessage.String,
+		RestoredBy:     v.RestoredBy,
+		RestoredAt:     v.RestoredAt.Format("2006-01-02 15:04:05"),
+	}
+}
+
+// normalizePage clamps the paging parameters (default 1/10, max 100).
+func normalizePage(page, pageSize int64) (int64, int64) {
+	if page < 1 {
+		page = 1
+	}
+	if pageSize < 10 {
+		pageSize = 10
+	}
+	if pageSize > 100 {
+		pageSize = 100
+	}
+	return page, pageSize
+}

+ 109 - 0
internal/logic/systemlog/logarchivedownloadlogic.go

@@ -0,0 +1,109 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"archive/zip"
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/log"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type LogArchiveDownloadLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 归档打包下载(zip)
+func NewLogArchiveDownloadLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogArchiveDownloadLogic {
+	return &LogArchiveDownloadLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+// LogArchiveDownload streams the selected archive files back as one zip.
+// The response is written to w directly (no unified envelope); the handler
+// therefore must not call httpx.OkJsonCtx afterwards.
+func (l *LogArchiveDownloadLogic) LogArchiveDownload(w http.ResponseWriter, req *types.LogArchiveDownloadReq) error {
+	if len(req.Ids) == 0 {
+		return apierr.BadRequest("ids 不能为空")
+	}
+	if l.svcCtx.MinioClient == nil {
+		return apierr.New(500, "MinIO客户端未加载,无法下载归档(检查t_config的file_*配置)")
+	}
+
+	log.RecordDebug(l.ctx, "开始下载Ids:%+v的归档!", req.Ids)
+
+	list, err := l.svcCtx.LogArchiveRecordModel.FindByIds(l.ctx, req.Ids)
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return apierr.New(500, "查询归档记录失败")
+	}
+	if len(list) == 0 {
+		return apierr.New(404, "归档记录不存在")
+	}
+
+	w.Header().Set("Content-Type", "application/zip")
+	w.Header().Set("Content-Disposition", "attachment; filename=archives.zip")
+
+	zipWriter := zip.NewWriter(w)
+	defer func() {
+		_ = zipWriter.Close()
+	}()
+
+	for i := range list {
+		v := &list[i]
+		filePath, err := l.svcCtx.MinioClient.DownloadFile(l.ctx, v.ArchiveFileKey)
+		if err != nil || filePath == "" {
+			log.RecordError(l.ctx, fmt.Errorf("下载归档文件失败[ID:%d]: %v", v.Id, err))
+			return apierr.New(500, "下载归档文件失败")
+		}
+		if err := copyArchiveIntoZip(zipWriter, filePath, v.ArchiveFileKey); err != nil {
+			log.RecordError(l.ctx, err)
+		}
+	}
+
+	log.RecordDebug(l.ctx, "Ids:%+v的归档下载成功!", req.Ids)
+	return nil
+}
+
+// copyArchiveIntoZip adds one archive file as a zip entry (temp file removed
+// afterwards).
+func copyArchiveIntoZip(zipWriter *zip.Writer, filePath, archiveFileKey string) error {
+	// 1.删除临时文件
+	defer func() {
+		_ = os.Remove(filePath)
+	}()
+	// 2.打开文件
+	file, err := os.Open(filePath)
+	if err != nil {
+		return fmt.Errorf("打开文件失败[%s]: %w", archiveFileKey, err)
+	}
+	defer func() {
+		_ = file.Close()
+	}()
+	// 3.文件打包
+	zipFileName := filepath.Base(archiveFileKey)
+	zipEntryWriter, err := zipWriter.Create(zipFileName)
+	if err != nil {
+		return fmt.Errorf("创建ZIP条目失败[%s]: %w", archiveFileKey, err)
+	}
+	if _, err := io.Copy(zipEntryWriter, file); err != nil {
+		return fmt.Errorf("写入ZIP条目失败[%s]: %w", archiveFileKey, err)
+	}
+	return nil
+}

+ 58 - 0
internal/logic/systemlog/logarchivelistlogic.go

@@ -0,0 +1,58 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"context"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/log"
+	udeModel "udeServer/internal/model"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type LogArchiveListLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 归档列表
+func NewLogArchiveListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogArchiveListLogic {
+	return &LogArchiveListLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+// LogArchiveList pages the archive records with optional filters.
+func (l *LogArchiveListLogic) LogArchiveList(req *types.LogArchiveListReq) (*types.LogArchiveListResp, error) {
+	page, pageSize := normalizePage(req.Page, req.PageSize)
+
+	f := udeModel.LogArchiveFilter{
+		ArchiveName:       req.ArchiveName,
+		ArchivedBy:        req.ArchivedBy,
+		ArchivedStartTime: req.ArchivedStartTime,
+		ArchivedEndTime:   req.ArchivedEndTime,
+	}
+
+	list, total, err := l.svcCtx.LogArchiveRecordModel.FindPage(l.ctx, page, pageSize, f)
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "查询归档分页列表失败")
+	}
+
+	logArchives := make([]types.LogArchive, len(list))
+	for i := range list {
+		logArchives[i] = ConvertToLogArchive(&list[i])
+	}
+	return &types.LogArchiveListResp{
+		Total:       total,
+		LogArchives: logArchives,
+	}, nil
+}

+ 73 - 0
internal/logic/systemlog/logarchivelogic.go

@@ -0,0 +1,73 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"context"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/audit"
+	"udeServer/internal/log"
+	udeModel "udeServer/internal/model"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type LogArchiveLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 日志归档
+func NewLogArchiveLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogArchiveLogic {
+	return &LogArchiveLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+// LogArchive dumps the selected logs (by ids and/or created_at range) into
+// a SQL file stored in MinIO, then removes them from t_system_log inside
+// one transaction and books an archive record.
+func (l *LogArchiveLogic) LogArchive(req *types.SystemLogArchiveReq) (*types.SystemLogArchiveResp, error) {
+	if req.Name == "" {
+		return nil, apierr.BadRequest("name(归档任务名称)不能为空")
+	}
+	if (req.StartDate != "" && req.EndDate == "") || (req.StartDate == "" && req.EndDate != "") {
+		return nil, apierr.BadRequest("startDate/endDate 必须成对传入")
+	}
+	if l.svcCtx.MinioClient == nil {
+		return nil, apierr.New(500, "MinIO客户端未加载,无法归档(检查t_config的file_*配置)")
+	}
+
+	log.RecordDebug(l.ctx, "开始进行Name:%s的日志归档!", req.Name)
+
+	list, err := l.svcCtx.SystemLogModel.FindForArchive(l.ctx, req.Ids, req.StartDate, req.EndDate)
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "查询待归档日志失败")
+	}
+	if len(list) == 0 {
+		return nil, apierr.BadRequest("未查询到待归档日志(检查ids/日期区间)")
+	}
+
+	entries := make([]*udeModel.TSystemLog, len(list))
+	for i := range list {
+		entries[i] = &list[i]
+	}
+	count, err := audit.ArchiveLogsToStorageAndRecord(l.ctx, entries, req.Name,
+		l.svcCtx.LogConfigManager.ArchiveThreshold, audit.ManualArchive,
+		l.svcCtx.MinioClient, l.svcCtx.DB)
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, err.Error())
+	}
+
+	log.RecordDebug(l.ctx, "日志归档成功!Count:%d", count)
+	return &types.SystemLogArchiveResp{Count: count}, nil
+}

+ 188 - 0
internal/logic/systemlog/logarchiverestorelogic.go

@@ -0,0 +1,188 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"os"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/audit"
+	"udeServer/internal/log"
+	udeModel "udeServer/internal/model"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+
+type LogArchiveRestoreLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 归档恢复
+func NewLogArchiveRestoreLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogArchiveRestoreLogic {
+	return &LogArchiveRestoreLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+// restoreResult carries the outcome of one restore goroutine.
+type restoreResult struct {
+	RecordId int64
+	Success  bool
+	ErrMsg   string
+}
+
+// LogArchiveRestore launches the restore of the selected archive runs in
+// background goroutines (bounded to 5 concurrent) and answers immediately;
+// progress is tracked through t_log_restore_record.
+func (l *LogArchiveRestoreLogic) LogArchiveRestore(req *types.LogArchiveRestoreReq) (*types.LogArchiveRestoreResp, error) {
+	if len(req.Ids) == 0 {
+		return nil, apierr.BadRequest("ids 不能为空")
+	}
+	if l.svcCtx.MinioClient == nil {
+		return nil, apierr.New(500, "MinIO客户端未加载,无法恢复(检查t_config的file_*配置)")
+	}
+
+	log.RecordDebug(l.ctx, "开始日志归档恢复!")
+
+	list, err := l.svcCtx.LogArchiveRecordModel.FindByIds(l.ctx, req.Ids)
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "查询归档记录失败")
+	}
+	if len(list) == 0 {
+		return nil, apierr.New(404, "归档记录不存在")
+	}
+
+	operator := operatorFromContext(l.ctx)
+
+	maxConcurrent := 5
+	// 同时可支持5个协程的信号量通道,满位后阻塞
+	sem := make(chan struct{}, maxConcurrent)
+	// 为每一个归档日志记录一个结果
+	resultChan := make(chan restoreResult, len(list))
+
+	for i := range list {
+		logArchiveRecord := &list[i]
+		// 占位,满位后阻塞
+		sem <- struct{}{}
+		go func(r *udeModel.TLogArchiveRecord) {
+			defer func() {
+				<-sem
+				if rec := recover(); rec != nil {
+					log.RecordError(l.ctx, fmt.Errorf("[%d]的归档恢复Panic!原因:%v", r.Id, rec))
+				}
+			}()
+			// 恢复执行在后台协程,不携带 HTTP 请求上下文
+			resultChan <- runRestore(context.Background(), l.svcCtx, r, operator)
+		}(logArchiveRecord)
+	}
+
+	// 后台汇总结果日志
+	go func() {
+		var success, failed int
+		for i := 0; i < len(list); i++ {
+			r := <-resultChan
+			if r.Success {
+				success++
+			} else {
+				failed++
+				log.RecordDebug(l.ctx, "[%d]的归档恢复失败:%s", r.RecordId, r.ErrMsg)
+			}
+		}
+		log.RecordDebug(l.ctx, "归档恢复任务全部完成,总数:%d,成功:%d,失败:%d", len(list), success, failed)
+	}()
+
+	msg := "归档恢复任务已启动,正在后台处理中,请稍候刷新恢复记录查看进度!"
+	log.RecordDebug(l.ctx, "%s", msg)
+	return &types.LogArchiveRestoreResp{RestoreMsg: msg}, nil
+}
+
+// operatorFromContext resolves the operator name (no auth layer -> admin).
+func operatorFromContext(ctx context.Context) string {
+	name := audit.GetRequestMeta(ctx).UserName
+	if name == "" {
+		return "admin"
+	}
+	return name
+}
+
+// runRestore executes one archive restore run and books its record row.
+func runRestore(localCtx context.Context, svcCtx *svc.ServiceContext, r *udeModel.TLogArchiveRecord, operator string) restoreResult {
+	result := restoreResult{RecordId: r.Id}
+
+	// 1.构造恢复记录
+	logRestoreRecord := &udeModel.TLogRestoreRecord{
+		ArchiveId:      r.Id,
+		RestoreStatus:  audit.RestoreProcessing,
+		RestoreMessage: sql.NullString{Valid: false},
+		RestoredBy:     operator,
+	}
+	insertResult, err := svcCtx.LogRestoreRecordModel.Insert(localCtx, logRestoreRecord)
+	if err != nil {
+		log.RecordError(localCtx, err)
+		result.Success, result.ErrMsg = false, "新增恢复记录失败:"+err.Error()
+		return result
+	}
+	recordId, err := insertResult.LastInsertId()
+	if err != nil {
+		log.RecordError(localCtx, err)
+		result.Success, result.ErrMsg = false, "获取恢复记录ID失败:"+err.Error()
+		return result
+	}
+
+	// 状态更新辅助函数
+	update := func(status int64, message sql.NullString) {
+		if uErr := svcCtx.LogRestoreRecordModel.UpdateResult(localCtx, recordId, status, message); uErr != nil {
+			log.RecordError(localCtx, fmt.Errorf("更新[%d]的恢复记录状态失败!原因:%v", recordId, uErr))
+		}
+	}
+	fail := func(errMsg string) {
+		result.Success, result.ErrMsg = false, errMsg
+		update(audit.RestoreFail, sql.NullString{String: errMsg, Valid: true})
+	}
+
+	// 2.下载归档文件
+	filePath, err := svcCtx.MinioClient.DownloadFile(localCtx, r.ArchiveFileKey)
+	if err != nil || filePath == "" {
+		fail(fmt.Sprintf("下载归档文件失败:%v", err))
+		return result
+	}
+
+	// 3.流式恢复(每批100条语句一个事务)
+	err = audit.RestoreSQLStream(localCtx, filePath, 100, func(ctx context.Context, batch []string) error {
+		return svcCtx.DB.TransactCtx(localCtx, func(ctx context.Context, session sqlx.Session) error {
+			for _, stmt := range batch {
+				if _, err := session.ExecCtx(ctx, stmt); err != nil {
+					return err
+				}
+			}
+			return nil
+		})
+	})
+
+	// 4.清理下载的临时文件
+	if rErr := os.Remove(filePath); rErr != nil {
+		log.RecordDebug(localCtx, "删除临时文件失败(%s):%v", filePath, rErr)
+	}
+
+	// 5.更新恢复记录状态
+	if err != nil {
+		fail("恢复执行失败:" + err.Error())
+		return result
+	}
+
+	result.Success, result.ErrMsg = true, ""
+	update(audit.RestoreSuccess, sql.NullString{Valid: false})
+	return result
+}

+ 107 - 0
internal/logic/systemlog/logauditlogic.go

@@ -0,0 +1,107 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"context"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/audit"
+	"udeServer/internal/log"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+
+type LogAuditLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 日志审计(校验Hmac防篡改)
+func NewLogAuditLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogAuditLogic {
+	return &LogAuditLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+// LogAudit recomputes the HMAC of the selected entries and marks each one
+// as audited-passed or audited-anomaly.
+func (l *LogAuditLogic) LogAudit(req *types.SystemLogAuditReq) (*types.SystemLogAuditResp, error) {
+	if len(req.Idens) == 0 {
+		return nil, apierr.BadRequest("idens 不能为空")
+	}
+
+	list, err := l.svcCtx.SystemLogModel.FindByIdens(l.ctx, req.Idens)
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "查询日志失败")
+	}
+
+	// HMAC 密钥与写入时一致(来自 t_config.log_hmac_secret)
+	_, logConfig := audit.GetGlobalConfig(l.svcCtx.ConfigModel)
+
+	var invalidCount, validCount int64
+	auditList := make([]types.AuditResult, 0, len(list))
+
+	for i := range list {
+		v := &list[i]
+		auditResult := types.AuditResult{LogIden: v.Iden}
+		macBuffer := make([]byte, 0, 32)
+
+		mac, hErr := audit.CalculateHMAC(v, logConfig.HmacSecret, macBuffer)
+		switch {
+		case hErr != nil:
+			log.RecordError(l.ctx, hErr)
+			auditResult.Valid = false
+			v.AuditStatus = audit.AuditedAnomaly
+			auditResult.Reason = "HMAC计算失败"
+		case v.HMac == mac:
+			auditResult.Valid = true
+			v.AuditStatus = audit.AuditedPassed
+			validCount++
+			auditList = append(auditList, auditResult)
+			continue
+		default:
+			auditResult.Valid = false
+			v.AuditStatus = audit.AuditedAnomaly
+			auditResult.Reason = "HMAC不匹配,日志被篡改!"
+			invalidCount++
+			auditList = append(auditList, auditResult)
+			continue
+		}
+		invalidCount++
+		auditList = append(auditList, auditResult)
+	}
+
+	// 事务内逐条更新审计状态
+	err = l.svcCtx.DB.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
+		for i := range list {
+			if _, err := session.ExecCtx(ctx,
+				"update t_system_log set audit_status = ? where id = ?",
+				list[i].AuditStatus, list[i].Id); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "更新审计状态失败")
+	}
+
+	resp := &types.SystemLogAuditResp{
+		AuditResults: auditList,
+		TotalCount:   int64(len(auditList)),
+		ValidCount:   validCount,
+		InValidCount: invalidCount,
+	}
+	log.RecordDebug(l.ctx, "日志审计完成!")
+	return resp, nil
+}

+ 47 - 0
internal/logic/systemlog/logdetaillogic.go

@@ -0,0 +1,47 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"context"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/log"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type LogDetailLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 日志详情
+func NewLogDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogDetailLogic {
+	return &LogDetailLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+// LogDetail returns one log entry by its iden.
+func (l *LogDetailLogic) LogDetail(req *types.SystemLogDetailReq) (*types.SystemLogDetailResp, error) {
+	if req.Iden == "" {
+		return nil, apierr.BadRequest("iden 不能为空")
+	}
+
+	entry, err := l.svcCtx.SystemLogModel.FindOneByIden(l.ctx, req.Iden)
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(404, "日志 iden="+req.Iden+" 不存在")
+	}
+
+	return &types.SystemLogDetailResp{
+		SystemLog: ConvertToSystemLog(entry),
+	}, nil
+}

+ 49 - 0
internal/logic/systemlog/loglinklogic.go

@@ -0,0 +1,49 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"context"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/log"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type LogLinkLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 查看完整链路
+func NewLogLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogLinkLogic {
+	return &LogLinkLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+// LogLink returns every log entry of one trace, ordered for display.
+func (l *LogLinkLogic) LogLink(req *types.SystemLogLinkReq) (*types.SystemLogLinkResp, error) {
+	if req.TraceId == "" {
+		return nil, apierr.BadRequest("traceId 不能为空")
+	}
+
+	list, err := l.svcCtx.SystemLogModel.FindByTraceId(l.ctx, req.TraceId)
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "查询日志链路失败")
+	}
+
+	logs := make([]types.SystemLog, len(list))
+	for i := range list {
+		logs[i] = ConvertToSystemLog(&list[i])
+	}
+	return &types.SystemLogLinkResp{LogLinks: logs}, nil
+}

+ 81 - 0
internal/logic/systemlog/loglistlogic.go

@@ -0,0 +1,81 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"context"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/audit"
+	"udeServer/internal/log"
+	udeModel "udeServer/internal/model"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type LogListLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 日志分页列表
+func NewLogListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogListLogic {
+	return &LogListLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+// LogList queries the audit log with paging and optional filters.
+func (l *LogListLogic) LogList(req *types.SystemLogListReq) (*types.SystemLogListResp, error) {
+	page, pageSize := normalizePage(req.Page, req.PageSize)
+
+	f := udeModel.SystemLogFilter{
+		Iden:             req.Iden,
+		UserName:         req.UserName,
+		AppKey:           req.AppKey,
+		UriPath:          req.UriPath,
+		HttpMethod:       req.HttpMethod,
+		CreatedStartTime: req.CreatedStartTime,
+		CreatedEndTime:   req.CreatedEndTime,
+		ClientIp:         req.ClientIp,
+		ErrorMsg:         req.ErrorMsg,
+	}
+	// 日志类型:合法取值才过滤(与 sdf-serve 行为一致,非法类型视为不传)
+	if req.Type == audit.SystemLog || req.Type == audit.InterfaceLog {
+		f.Type = req.Type
+		f.TypeValid = true
+	}
+	// 审计状态:"1"未审计/"2"通过/"3"异常,空白视为未传
+	switch req.AuditStatus {
+	case "":
+	case "1":
+		f.AuditStatus = audit.UnAudited
+	case "2":
+		f.AuditStatus = audit.AuditedPassed
+	case "3":
+		f.AuditStatus = audit.AuditedAnomaly
+	default:
+		return nil, apierr.BadRequest("auditStatus must be \"1\" (未审计) or \"2\" (通过) or \"3\" (异常)")
+	}
+
+	list, total, err := l.svcCtx.SystemLogModel.FindPage(l.ctx, page, pageSize, f)
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "查询日志分页列表失败")
+	}
+
+	logs := make([]types.SystemLog, len(list))
+	for i := range list {
+		logs[i] = ConvertToSystemLog(&list[i])
+	}
+	return &types.SystemLogListResp{
+		Total: total,
+		Logs:  logs,
+	}, nil
+}

+ 61 - 0
internal/logic/systemlog/logrestorelistlogic.go

@@ -0,0 +1,61 @@
+// Code scaffolded by goctl. Safe to edit.
+// goctl 1.10.2
+
+package systemlog
+
+import (
+	"context"
+
+	"udeServer/internal/apierr"
+	"udeServer/internal/audit"
+	"udeServer/internal/log"
+	udeModel "udeServer/internal/model"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+
+	"github.com/zeromicro/go-zero/core/logx"
+)
+
+type LogRestoreListLogic struct {
+	logx.Logger
+	ctx    context.Context
+	svcCtx *svc.ServiceContext
+}
+
+// 恢复记录列表
+func NewLogRestoreListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogRestoreListLogic {
+	return &LogRestoreListLogic{
+		Logger: logx.WithContext(ctx),
+		ctx:    ctx,
+		svcCtx: svcCtx,
+	}
+}
+
+// LogRestoreList pages the restore records with optional filters.
+func (l *LogRestoreListLogic) LogRestoreList(req *types.LogRestoreListReq) (*types.LogRestoreListResp, error) {
+	page, pageSize := normalizePage(req.Page, req.PageSize)
+
+	f := udeModel.LogRestoreFilter{
+		RestoreMessage: req.RestoreMessage,
+	}
+	// 恢复状态:1成功/2失败/3恢复中,非法取值视为不传(与 sdf 行为一致)
+	switch req.RestoreStatus {
+	case audit.RestoreSuccess, audit.RestoreFail, audit.RestoreProcessing:
+		f.RestoreStatus = req.RestoreStatus
+	}
+
+	list, total, err := l.svcCtx.LogRestoreRecordModel.FindPage(l.ctx, page, pageSize, f)
+	if err != nil {
+		log.RecordError(l.ctx, err)
+		return nil, apierr.New(500, "查询恢复记录列表失败")
+	}
+
+	logRestores := make([]types.LogRestore, len(list))
+	for i := range list {
+		logRestores[i] = ConvertToLogRestore(&list[i])
+	}
+	return &types.LogRestoreListResp{
+		Total:       total,
+		LogRestores: logRestores,
+	}, nil
+}

+ 50 - 0
internal/model/tconfigmodel.go

@@ -0,0 +1,50 @@
+package model
+
+import (
+	"context"
+	"time"
+
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+
+// TConfig is one dynamic configuration row. Hot-reloadable audit settings
+// (MinIO endpoint/keys, log level, archive threshold, HMAC secret) are
+// stored here and read back on every request by the audit middleware.
+type TConfig struct {
+	Id          int64     `db:"id"`           // 主键
+	ConfigItem  string    `db:"config_item"`  // 配置项
+	ConfigValue string    `db:"config_value"` // 配置值
+	EditFlag    int64     `db:"edit_flag"`    // 是否允许编辑,1是不可编辑configItem,2是所有均可编辑
+	Description string    `db:"description"`  // 描述
+	UpdatedBy   string    `db:"updated_by"`   // 最后更新人
+	UpdatedAt   time.Time `db:"updated_at"`   // 配置更新时间
+}
+
+type TConfigModel interface {
+	// FindAll returns every configuration row ordered by id.
+	FindAll(ctx context.Context) ([]TConfig, error)
+}
+
+type customTConfigModel struct {
+	conn  sqlx.SqlConn
+	table string
+}
+
+var _ TConfigModel = (*customTConfigModel)(nil)
+
+// NewTConfigModel returns the model for the t_config table.
+func NewTConfigModel(conn sqlx.SqlConn) TConfigModel {
+	return &customTConfigModel{
+		conn:  conn,
+		table: "`t_config`",
+	}
+}
+
+func (m *customTConfigModel) FindAll(ctx context.Context) ([]TConfig, error) {
+	var resp []TConfig
+	query := "select `id`, `config_item`, `config_value`, `edit_flag`, `description`, `updated_by`, `updated_at` from t_config order by `id` asc"
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}

+ 129 - 0
internal/model/tlogarchiverecordmodel.go

@@ -0,0 +1,129 @@
+package model
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+
+// TLogArchiveRecord is one archive run: the dumped rows are stored as a SQL
+// file in MinIO (ArchiveFileKey) and then removed from t_system_log.
+type TLogArchiveRecord struct {
+	Id             int64     `db:"id"`               // 主键
+	ArchiveName    string    `db:"archive_name"`     // 归档任务名
+	RecordCount    int64     `db:"record_count"`     // 归档记录数
+	ArchiveFileKey string    `db:"archive_file_key"` // 归档SQL文件FileKey
+	ArchiveMode    int64     `db:"archive_mode"`     // 归档方式:1-手动,2-自动
+	BatchSize      int64     `db:"batch_size"`       // 本次归档的阈值N
+	ArchivedBy     string    `db:"archived_by"`      // 归档人账号
+	ArchivedAt     time.Time `db:"archived_at"`      // 归档时间
+}
+
+// LogArchiveFilter mirrors LogArchiveListReq; empty fields = no filtering.
+type LogArchiveFilter struct {
+	ArchiveName       string
+	ArchivedBy        string
+	ArchivedStartTime string
+	ArchivedEndTime   string
+}
+
+type TLogArchiveRecordModel interface {
+	// FindPage lists archive runs with optional filters, newest first.
+	FindPage(ctx context.Context, page, pageSize int64, f LogArchiveFilter) ([]TLogArchiveRecord, int64, error)
+	// FindByIds returns the archive runs with any of the given ids.
+	FindByIds(ctx context.Context, ids []int64) ([]TLogArchiveRecord, error)
+	// CountAll returns the total number of archive runs.
+	CountAll(ctx context.Context) (int64, error)
+}
+
+type customTLogArchiveRecordModel struct {
+	conn  sqlx.SqlConn
+	table string
+}
+
+var _ TLogArchiveRecordModel = (*customTLogArchiveRecordModel)(nil)
+
+// NewTLogArchiveRecordModel returns the model for t_log_archive_record.
+func NewTLogArchiveRecordModel(conn sqlx.SqlConn) TLogArchiveRecordModel {
+	return &customTLogArchiveRecordModel{
+		conn:  conn,
+		table: "`t_log_archive_record`",
+	}
+}
+
+func (m *customTLogArchiveRecordModel) FindPage(ctx context.Context, page, pageSize int64, f LogArchiveFilter) ([]TLogArchiveRecord, int64, error) {
+	var conds []string
+	var args []any
+
+	if f.ArchiveName != "" {
+		conds = append(conds, "`archive_name` like ? escape '\\\\'")
+		args = append(args, "%"+escapeLike(f.ArchiveName)+"%")
+	}
+	if f.ArchivedBy != "" {
+		conds = append(conds, "`archived_by` like ? escape '\\\\'")
+		args = append(args, "%"+escapeLike(f.ArchivedBy)+"%")
+	}
+	if f.ArchivedStartTime != "" {
+		conds = append(conds, "`archived_at` >= ?")
+		args = append(args, f.ArchivedStartTime)
+	}
+	if f.ArchivedEndTime != "" {
+		conds = append(conds, "`archived_at` <= ?")
+		args = append(args, f.ArchivedEndTime)
+	}
+
+	where := ""
+	if len(conds) > 0 {
+		where = " where " + strings.Join(conds, " and ")
+	}
+
+	var total int64
+	if err := m.conn.QueryRowCtx(ctx, &total, fmt.Sprintf("select count(*) from %s%s", m.table, where), args...); err != nil {
+		return nil, 0, err
+	}
+
+	if total == 0 {
+		return nil, 0, nil
+	}
+
+	offset := (page - 1) * pageSize
+	args = append(args, pageSize, offset)
+	query := fmt.Sprintf(
+		"select `id`, `archive_name`, `record_count`, `archive_file_key`, `archive_mode`, `batch_size`, `archived_by`, `archived_at` from %s%s order by `archived_at` desc limit ? offset ?",
+		m.table, where)
+
+	var resp []TLogArchiveRecord
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, args...); err != nil {
+		return nil, 0, err
+	}
+
+	return resp, total, nil
+}
+
+func (m *customTLogArchiveRecordModel) FindByIds(ctx context.Context, ids []int64) ([]TLogArchiveRecord, error) {
+	placeholders := strings.TrimSuffix(strings.Repeat("?,", len(ids)), ",")
+	args := make([]any, len(ids))
+	for i, v := range ids {
+		args[i] = v
+	}
+
+	var resp []TLogArchiveRecord
+	query := fmt.Sprintf(
+		"select `id`, `archive_name`, `record_count`, `archive_file_key`, `archive_mode`, `batch_size`, `archived_by`, `archived_at` from %s where `id` in (%s)",
+		m.table, placeholders)
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, args...); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+func (m *customTLogArchiveRecordModel) CountAll(ctx context.Context) (int64, error) {
+	var total int64
+	if err := m.conn.QueryRowCtx(ctx, &total, fmt.Sprintf("select count(*) from %s", m.table)); err != nil {
+		return 0, err
+	}
+	return total, nil
+}

+ 108 - 0
internal/model/tlogrestorerecordmodel.go

@@ -0,0 +1,108 @@
+package model
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+
+// TLogRestoreRecord is one archive-restore run booked against the archive
+// it restores.
+type TLogRestoreRecord struct {
+	Id             int64          `db:"id"`              // 主键
+	ArchiveId      int64          `db:"archive_id"`      // 归档记录ID
+	RestoreStatus  int64          `db:"restore_status"`  // 恢复状态:1-成功,2-失败,3-处理中
+	RestoreMessage sql.NullString `db:"restore_message"` // 恢复说明或错误原因
+	RestoredBy     string         `db:"restored_by"`     // 恢复人账号
+	RestoredAt     time.Time      `db:"restored_at"`     // 恢复时间
+}
+
+// LogRestoreFilter mirrors LogRestoreReq; empty fields = no filtering.
+type LogRestoreFilter struct {
+	RestoreStatus  int64
+	RestoreMessage string
+}
+
+type TLogRestoreRecordModel interface {
+	// Insert books a new restore run and returns the insert result.
+	Insert(ctx context.Context, data *TLogRestoreRecord) (sql.Result, error)
+	// UpdateResult updates status and message of a restore run.
+	UpdateResult(ctx context.Context, id int64, status int64, message sql.NullString) error
+	// FindPage lists restore runs with optional filters, newest first.
+	FindPage(ctx context.Context, page, pageSize int64, f LogRestoreFilter) ([]TLogRestoreRecord, int64, error)
+}
+
+type customTLogRestoreRecordModel struct {
+	conn  sqlx.SqlConn
+	table string
+}
+
+var _ TLogRestoreRecordModel = (*customTLogRestoreRecordModel)(nil)
+
+// NewTLogRestoreRecordModel returns the model for t_log_restore_record.
+func NewTLogRestoreRecordModel(conn sqlx.SqlConn) TLogRestoreRecordModel {
+	return &customTLogRestoreRecordModel{
+		conn:  conn,
+		table: "`t_log_restore_record`",
+	}
+}
+
+func (m *customTLogRestoreRecordModel) Insert(ctx context.Context, data *TLogRestoreRecord) (sql.Result, error) {
+	query := "insert into t_log_restore_record (`archive_id`, `restore_status`, `restore_message`, `restored_by`) values (?, ?, ?, ?)"
+	ret, err := m.conn.ExecCtx(ctx, query,
+		data.ArchiveId, data.RestoreStatus, data.RestoreMessage, data.RestoredBy)
+	return ret, err
+}
+
+func (m *customTLogRestoreRecordModel) UpdateResult(ctx context.Context, id int64, status int64, message sql.NullString) error {
+	query := "update t_log_restore_record set `restore_status` = ?, `restore_message` = ? where `id` = ?"
+	if _, err := m.conn.ExecCtx(ctx, query, status, message, id); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (m *customTLogRestoreRecordModel) FindPage(ctx context.Context, page, pageSize int64, f LogRestoreFilter) ([]TLogRestoreRecord, int64, error) {
+	var conds []string
+	var args []any
+
+	if f.RestoreStatus != 0 {
+		conds = append(conds, "`restore_status` = ?")
+		args = append(args, f.RestoreStatus)
+	}
+	if f.RestoreMessage != "" {
+		conds = append(conds, "`restore_message` like ? escape '\\\\'")
+		args = append(args, "%"+escapeLike(f.RestoreMessage)+"%")
+	}
+
+	where := ""
+	if len(conds) > 0 {
+		where = " where " + strings.Join(conds, " and ")
+	}
+
+	var total int64
+	if err := m.conn.QueryRowCtx(ctx, &total, fmt.Sprintf("select count(*) from %s%s", m.table, where), args...); err != nil {
+		return nil, 0, err
+	}
+
+	if total == 0 {
+		return nil, 0, nil
+	}
+
+	offset := (page - 1) * pageSize
+	args = append(args, pageSize, offset)
+	query := fmt.Sprintf(
+		"select `id`, `archive_id`, `restore_status`, `restore_message`, `restored_by`, `restored_at` from %s%s order by `restored_at` desc limit ? offset ?",
+		m.table, where)
+
+	var resp []TLogRestoreRecord
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, args...); err != nil {
+		return nil, 0, err
+	}
+
+	return resp, total, nil
+}

+ 313 - 0
internal/model/tsystemlogmodel.go

@@ -0,0 +1,313 @@
+package model
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/zeromicro/go-zero/core/stores/sqlx"
+)
+
+// TSystemLog is the audit log entry of one request/operation. It has no
+// json tags on purpose: the HMAC-SM3 chain signs the marshalled Go field
+// names, which must stay stable (see internal/audit/hmac.go).
+type TSystemLog struct {
+	Id                  int64          `db:"id"`                     // 自增主键
+	Iden                string         `db:"iden"`                   // 日志编号
+	TraceId             string         `db:"trace_id"`               // 全链路唯一Trace ID
+	SpanId              string         `db:"span_id"`                // 本次跨度唯一Span ID
+	ParentSpanId        sql.NullString `db:"parent_span_id"`         // 父SpanID,用于构建调用树
+	Type                int64          `db:"type"`                   // 日志类型:1.系统日志;2.接口日志
+	ServiceName         string         `db:"service_name"`           // 所属服务或模块名
+	UriPath             sql.NullString `db:"uri_path"`               // 接口请求路径
+	HttpMethod          sql.NullString `db:"http_method"`            // 请求方法:GET/POST/PUT/DELETE
+	Method              string         `db:"method"`                 // 请求业务方法
+	Level               string         `db:"level"`                  // 日志级别:DEBUG/INFO/WARN/ERROR
+	AppKey              sql.NullString `db:"app_key"`                // 调用接口应用标识
+	UserId              sql.NullString `db:"user_id"`                // 系统用户ID
+	UserName            sql.NullString `db:"user_name"`              // 系统用户名
+	ClientIp            sql.NullString `db:"client_ip"`              // 客户端IP
+	RequestHeader       sql.NullString `db:"request_header"`         // 请求头,脱敏后
+	RequestBodyDigest   string         `db:"request_body_digest"`    // 请求体摘要(SM3)
+	RequestBodyFileKey  string         `db:"request_body_file_key"`  // 请求体FileKey
+	ResponseCode        int64          `db:"response_code"`          // 响应业务码
+	ResponseBodyDigest  string         `db:"response_body_digest"`   // 响应体摘要(SM3)
+	ResponseBodyFileKey string         `db:"response_body_file_key"` // 响应体FileKey
+	ErrorMessage        sql.NullString `db:"error_message"`          // 错误提示,仅 ERROR 时填
+	DurationMs          int64          `db:"duration_ms"`            // 本 Span 耗时(毫秒)
+	CreatedAt           time.Time      `db:"created_at"`             // 写入时间
+	HMac                string         `db:"h_mac"`                  // Hmac值
+	AuditStatus         int64          `db:"audit_status"`           // 审计状态:1.未审计;2.审计通过;3.审计异常
+}
+
+// tSystemLogRows is the full column list.
+var tSystemLogRows = strings.Join([]string{
+	"`id`", "`iden`", "`trace_id`", "`span_id`", "`parent_span_id`", "`type`",
+	"`service_name`", "`uri_path`", "`http_method`", "`method`", "`level`",
+	"`app_key`", "`user_id`", "`user_name`", "`client_ip`", "`request_header`",
+	"`request_body_digest`", "`request_body_file_key`", "`response_code`",
+	"`response_body_digest`", "`response_body_file_key`", "`error_message`",
+	"`duration_ms`", "`created_at`", "`h_mac`", "`audit_status`",
+}, ",")
+
+// SystemLogFilter mirrors SystemLogListReq; empty fields mean no filtering.
+// TypeValid marks that Type was provided (so type=0 can be an explicit
+// invalid request instead of "unfiltered").
+type SystemLogFilter struct {
+	Iden             string
+	UserName         string
+	AppKey           string
+	UriPath          string
+	HttpMethod       string
+	CreatedStartTime string
+	CreatedEndTime   string
+	ClientIp         string
+	ErrorMsg         string
+	AuditStatus      int64
+	Type             int64
+	TypeValid        bool
+}
+
+type TSystemLogModel interface {
+	// Insert stores a new audit entry; id and created_at are db generated
+	// (created_at default CURRENT_TIMESTAMP, same as sdf-serve).
+	Insert(ctx context.Context, data *TSystemLog) (sql.Result, error)
+	// FindOneByIden returns the entry with the given iden.
+	FindOneByIden(ctx context.Context, iden string) (*TSystemLog, error)
+	// FindPage lists entries with optional filters and returns the page and
+	// the total count, newest first (created_at DESC).
+	FindPage(ctx context.Context, page, pageSize int64, f SystemLogFilter) ([]TSystemLog, int64, error)
+	// FindByTraceId returns every entry of one trace, system-log first.
+	FindByTraceId(ctx context.Context, traceId string) ([]TSystemLog, error)
+	// FindByIdens returns the entries whose iden is contained in idens.
+	FindByIdens(ctx context.Context, idens []string) ([]TSystemLog, error)
+	// FindForArchive returns entries selected by the id list and/or the
+	// created_at range (closed interval), ordered by created_at.
+	FindForArchive(ctx context.Context, ids []int64, start, end string) ([]TSystemLog, error)
+	// FindAllWithLimit returns up to threshold oldest entries for the
+	// automatic archive job.
+	FindAllWithLimit(ctx context.Context, threshold int) ([]*TSystemLog, error)
+	// DeleteByIds batch-deletes rows by id; meant to run inside a tx.
+	DeleteByIds(ctx context.Context, session sqlx.Session, ids []int64) error
+}
+
+type customTSystemLogModel struct {
+	conn  sqlx.SqlConn
+	table string
+}
+
+var _ TSystemLogModel = (*customTSystemLogModel)(nil)
+
+// NewTSystemLogModel returns the model for the t_system_log table.
+func NewTSystemLogModel(conn sqlx.SqlConn) TSystemLogModel {
+	return &customTSystemLogModel{
+		conn:  conn,
+		table: "`t_system_log`",
+	}
+}
+
+// tSystemLogInsertColumns is the insert column list; id and created_at are
+// generated by the database.
+func tSystemLogInsertColumns() string {
+	return strings.Join([]string{
+		"`iden`", "`trace_id`", "`span_id`", "`parent_span_id`", "`type`",
+		"`service_name`", "`uri_path`", "`http_method`", "`method`", "`level`",
+		"`app_key`", "`user_id`", "`user_name`", "`client_ip`", "`request_header`",
+		"`request_body_digest`", "`request_body_file_key`", "`response_code`",
+		"`response_body_digest`", "`response_body_file_key`", "`error_message`",
+		"`duration_ms`", "`h_mac`", "`audit_status`",
+	}, ",")
+}
+
+func (m *customTSystemLogModel) Insert(ctx context.Context, data *TSystemLog) (sql.Result, error) {
+	query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+		m.table, tSystemLogInsertColumns())
+	ret, err := m.conn.ExecCtx(ctx, query,
+		data.Iden, data.TraceId, data.SpanId, data.ParentSpanId, data.Type,
+		data.ServiceName, data.UriPath, data.HttpMethod, data.Method, data.Level,
+		data.AppKey, data.UserId, data.UserName, data.ClientIp, data.RequestHeader,
+		data.RequestBodyDigest, data.RequestBodyFileKey, data.ResponseCode,
+		data.ResponseBodyDigest, data.ResponseBodyFileKey, data.ErrorMessage,
+		data.DurationMs, data.HMac, data.AuditStatus)
+	return ret, err
+}
+
+func (m *customTSystemLogModel) FindOneByIden(ctx context.Context, iden string) (*TSystemLog, error) {
+	var resp TSystemLog
+	query := fmt.Sprintf("select %s from %s where `iden` = ? limit 1", tSystemLogRows, m.table)
+	err := m.conn.QueryRowCtx(ctx, &resp, query, iden)
+	switch err {
+	case nil:
+		return &resp, nil
+	case sqlx.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}
+
+func (m *customTSystemLogModel) FindPage(ctx context.Context, page, pageSize int64, f SystemLogFilter) ([]TSystemLog, int64, error) {
+	var conds []string
+	var args []any
+
+	if f.Iden != "" {
+		conds = append(conds, "`iden` = ?")
+		args = append(args, f.Iden)
+	}
+	if f.UserName != "" {
+		conds = append(conds, "`user_name` like ? escape '\\\\'")
+		args = append(args, "%"+escapeLike(f.UserName)+"%")
+	}
+	if f.AppKey != "" {
+		conds = append(conds, "`app_key` = ?")
+		args = append(args, f.AppKey)
+	}
+	if f.CreatedStartTime != "" {
+		conds = append(conds, "`created_at` >= ?")
+		args = append(args, f.CreatedStartTime)
+	}
+	if f.CreatedEndTime != "" {
+		conds = append(conds, "`created_at` <= ?")
+		args = append(args, f.CreatedEndTime)
+	}
+	if f.UriPath != "" {
+		conds = append(conds, "`uri_path` = ?")
+		args = append(args, f.UriPath)
+	}
+	if f.HttpMethod != "" {
+		conds = append(conds, "`http_method` = ?")
+		args = append(args, f.HttpMethod)
+	}
+	if f.ClientIp != "" {
+		conds = append(conds, "`client_ip` = ?")
+		args = append(args, f.ClientIp)
+	}
+	if f.ErrorMsg != "" {
+		conds = append(conds, "`error_message` like ? escape '\\\\'")
+		args = append(args, "%"+escapeLike(f.ErrorMsg)+"%")
+	}
+	if f.AuditStatus != 0 {
+		conds = append(conds, "`audit_status` = ?")
+		args = append(args, f.AuditStatus)
+	}
+	if f.TypeValid {
+		conds = append(conds, "`type` = ?")
+		args = append(args, f.Type)
+	}
+
+	where := ""
+	if len(conds) > 0 {
+		where = " where " + strings.Join(conds, " and ")
+	}
+
+	var total int64
+	if err := m.conn.QueryRowCtx(ctx, &total, fmt.Sprintf("select count(*) from %s%s", m.table, where), args...); err != nil {
+		return nil, 0, err
+	}
+
+	if total == 0 {
+		return nil, 0, nil
+	}
+
+	offset := (page - 1) * pageSize
+	args = append(args, pageSize, offset)
+	query := fmt.Sprintf("select %s from %s%s order by `created_at` desc limit ? offset ?", tSystemLogRows, m.table, where)
+
+	var resp []TSystemLog
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, args...); err != nil {
+		return nil, 0, err
+	}
+
+	return resp, total, nil
+}
+
+func (m *customTSystemLogModel) FindByTraceId(ctx context.Context, traceId string) ([]TSystemLog, error) {
+	var resp []TSystemLog
+	query := fmt.Sprintf(
+		"select %s from %s where `trace_id` = ? order by case `type` when 1 then 0 when 2 then 1 else 2 end, `id` asc",
+		tSystemLogRows, m.table)
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, traceId); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+func (m *customTSystemLogModel) FindByIdens(ctx context.Context, idens []string) ([]TSystemLog, error) {
+	placeholders := strings.TrimSuffix(strings.Repeat("?,", len(idens)), ",")
+	args := make([]any, len(idens))
+	for i, v := range idens {
+		args[i] = v
+	}
+	var resp []TSystemLog
+	query := fmt.Sprintf("select %s from %s where `iden` in (%s)", tSystemLogRows, m.table, placeholders)
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, args...); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+func (m *customTSystemLogModel) FindForArchive(ctx context.Context, ids []int64, start, end string) ([]TSystemLog, error) {
+	var conds []string
+	var args []any
+
+	if len(ids) > 0 {
+		placeholders := strings.TrimSuffix(strings.Repeat("?,", len(ids)), ",")
+		conds = append(conds, "`id` in ("+placeholders+")")
+		for _, v := range ids {
+			args = append(args, v)
+		}
+	}
+	if start != "" && end != "" {
+		conds = append(conds, "`created_at` >= ? and `created_at` <= ?")
+		args = append(args, start, end)
+	}
+
+	where := ""
+	if len(conds) > 0 {
+		where = " where " + strings.Join(conds, " and ")
+	}
+
+	var resp []TSystemLog
+	query := fmt.Sprintf("select %s from %s%s order by `created_at` asc", tSystemLogRows, m.table, where)
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, args...); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+func (m *customTSystemLogModel) FindAllWithLimit(ctx context.Context, threshold int) ([]*TSystemLog, error) {
+	var resp []*TSystemLog
+	query := fmt.Sprintf("select %s from %s order by `created_at` asc limit ?", tSystemLogRows, m.table)
+	if err := m.conn.QueryRowsCtx(ctx, &resp, query, threshold); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+func (m *customTSystemLogModel) DeleteByIds(ctx context.Context, session sqlx.Session, ids []int64) error {
+	const deleteBatchSize = 1000
+
+	for i := 0; i < len(ids); i += deleteBatchSize {
+		end := i + deleteBatchSize
+		if end > len(ids) {
+			end = len(ids)
+		}
+		batchIds := ids[i:end]
+		placeholders := strings.TrimSuffix(strings.Repeat("?,", len(batchIds)), ",")
+
+		query := fmt.Sprintf("delete from t_system_log where id in (%s)", placeholders)
+
+		args := make([]interface{}, len(batchIds))
+		for j, v := range batchIds {
+			args[j] = v
+		}
+
+		if _, err := session.ExecCtx(ctx, query, args...); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}

+ 30 - 0
internal/svc/servicecontext.go

@@ -4,10 +4,13 @@
 package svc
 
 import (
+	"udeServer/internal/audit"
 	"udeServer/internal/config"
 	"udeServer/internal/model"
 
+	"github.com/zeromicro/go-zero/core/logx"
 	"github.com/zeromicro/go-zero/core/stores/sqlx"
+	"github.com/zeromicro/go-zero/rest"
 )
 
 type ServiceContext struct {
@@ -16,15 +19,42 @@ type ServiceContext struct {
 	DirectoryModel model.TDirectoryModel
 	KeyModel       model.TKeyModel
 	ServiceModel   model.TServiceModel
+
+	// 审计日志(移植自 sdf-serve)
+	SystemLogModel        model.TSystemLogModel
+	ConfigModel           model.TConfigModel
+	LogArchiveRecordModel model.TLogArchiveRecordModel
+	LogRestoreRecordModel model.TLogRestoreRecordModel
+	MinioClient           *audit.MinioClient
+	LogConfigManager      *audit.LogConfigManager
+	LogMiddleware         rest.Middleware
 }
 
 func NewServiceContext(c config.Config) *ServiceContext {
 	conn := sqlx.MustNewConn(c.MySQL)
+	configModel := model.NewTConfigModel(conn)
+	systemLogModel := model.NewTSystemLogModel(conn)
+
+	// 动态配置 + MinIO 客户端;t_config 未建/缺行时降级运行(client 保持 nil)
+	minioClient, logConfig := audit.GetGlobalConfig(configModel)
+	logx.Infof("audit logging initialized: minio=%t logLevel=%q hmacSecretSet=%t",
+		minioClient != nil, logConfig.LogRecordLevel, logConfig.HmacSecret != "")
+
+	logMiddleware := audit.NewLogMiddleware().Handle(systemLogModel, configModel, c.Name)
+
 	return &ServiceContext{
 		Config:         c,
 		DB:             conn,
 		DirectoryModel: model.NewTDirectoryModel(conn),
 		KeyModel:       model.NewTKeyModel(conn),
 		ServiceModel:   model.NewTServiceModel(conn),
+
+		SystemLogModel:        systemLogModel,
+		ConfigModel:           configModel,
+		LogArchiveRecordModel: model.NewTLogArchiveRecordModel(conn),
+		LogRestoreRecordModel: model.NewTLogRestoreRecordModel(conn),
+		MinioClient:           minioClient,
+		LogConfigManager:      logConfig,
+		LogMiddleware:         logMiddleware,
 	}
 }

+ 65 - 0
internal/task/task.go

@@ -0,0 +1,65 @@
+// Package task hosts the scheduled jobs ported from sdf-serve. The daily
+// log auto-archive job moves oldest audit logs into MinIO when they exceed
+// the archive_threshold configured in t_config.
+package task
+
+import (
+	"context"
+
+	"udeServer/internal/audit"
+	"udeServer/internal/log"
+	"udeServer/internal/svc"
+
+	"github.com/robfig/cron/v3"
+)
+
+// StartScheduler registers and starts the cron scheduler.
+func StartScheduler(ctx *svc.ServiceContext) {
+	c := cron.New()
+
+	// 每天 00:30 执行自动归档
+	_, err := c.AddFunc("30 0 * * *", func() {
+		log.RecordInfo(context.Background(), "[Ude-Task]开始自动归档日志!")
+		LogAutoArchive(ctx)
+	})
+	if err != nil {
+		log.RecordError(context.Background(), err)
+	}
+
+	log.RecordInfo(context.Background(), "定时任务启动中...")
+	c.Start()
+}
+
+// LogAutoArchive archives oldest logs while they exceed the threshold.
+func LogAutoArchive(svcCtx *svc.ServiceContext) {
+	ctx := context.Background()
+	threshold := svcCtx.LogConfigManager.ArchiveThreshold
+	if threshold <= 0 {
+		log.RecordInfo(ctx, "[Task-LogArchive]归档阈值未配置(t_config.archive_threshold),跳过自动归档!")
+		return
+	}
+	if svcCtx.MinioClient == nil {
+		log.RecordWarn(ctx, "[Task-LogArchive]MinIO客户端未加载(检查t_config的file_*配置),跳过自动归档!")
+		return
+	}
+	archiveName := "system_auto_archive"
+
+	for {
+		list, err := svcCtx.SystemLogModel.FindAllWithLimit(ctx, int(threshold))
+		if err != nil {
+			log.RecordError(ctx, err)
+			return
+		}
+		if int64(len(list)) < threshold {
+			log.RecordInfo(ctx, "[Task-LogArchive]当前日志%d条,未到达归档阈值%d,跳过自动归档!", len(list), threshold)
+			break
+		}
+
+		_, err = audit.ArchiveLogsToStorageAndRecord(ctx, list, archiveName, threshold, audit.AutoArchive, svcCtx.MinioClient, svcCtx.DB)
+		if err != nil {
+			log.RecordError(ctx, err)
+			return
+		}
+		log.RecordInfo(ctx, "[Task-LogArchive]自动归档:%d条日志成功!", len(list))
+	}
+}

+ 167 - 16
internal/types/types.go

@@ -3,6 +3,12 @@
 
 package types
 
+type AuditResult struct {
+	LogIden string `json:"logIden"`
+	Valid   bool   `json:"valid"`
+	Reason  string `json:"reason"`
+}
+
 type CreateDirectoryReq struct {
 	Id        int64  `json:"id,optional"`
 	ServiceId int64  `json:"serviceId,optional"`
@@ -35,6 +41,10 @@ type DeleteResp struct {
 	Ok bool `json:"ok"`
 }
 
+type DirChildrenResp struct {
+	Nodes []LocalDirNode `json:"nodes"`
+}
+
 type DirectoryIdReq struct {
 	Id int64 `path:"id"`
 }
@@ -68,6 +78,12 @@ type KeyResp struct {
 	CreateAt string `json:"createAt"`
 }
 
+type ListDirChildrenReq struct {
+	Ip   string `form:"ip"`   // 目标 Linux 主机 IP
+	Port string `form:"port"` // 目标主机 /api/v1 端口
+	Path string `form:"path"` // 空或 "/" 表示根目录
+}
+
 type ListDirectoryReq struct {
 	Page     int64 `form:"page,optional"`
 	PageSize int64 `form:"pageSize,optional"`
@@ -85,30 +101,77 @@ type ListServiceReq struct {
 	PageSize int64 `form:"pageSize,optional"`
 }
 
-type Request struct {
-	Name string `path:"name,options=you|me"`
+type LocalDirNode struct {
+	Name        string `json:"name"`        // 目录名,如 "usr"
+	Path        string `json:"path"`        // 完整绝对路径,如 "/usr"
+	IsDir       bool   `json:"isDir"`       // 恒 true
+	HasChildren bool   `json:"hasChildren"` // 是否含子目录(前端据此决定是否显示展开箭头)
 }
 
-type Response struct {
-	Message string `json:"message"`
+type LogArchive struct {
+	Id             int64  `json:"id"`
+	ArchiveName    string `json:"archiveName"`
+	RecordCount    int64  `json:"recordCount"`
+	ArchiveFileKey string `json:"archiveFileKey"`
+	ArchiveMode    int    `json:"archiveMode"` // 归档模式:1.手动,2.自动
+	BatchSize      int    `json:"batchSize"`
+	ArchivedBy     string `json:"archivedBy"`
+	ArchivedAt     string `json:"archivedAt"`
 }
 
-// 本地目录懒加载(udeServer 所在 Linux 主机文件系统,只列目录不列文件)
-type ListDirChildrenReq struct {
-	Ip   string `form:"ip"` // 目标 Linux 主机 IP
-	Port string `form:"port"`
-	Path string `form:"path"` // 空或 "/" 表示根目录
+type LogArchiveDownloadReq struct {
+	Ids []int64 `json:"ids"`
 }
 
-type LocalDirNode struct {
-	Name        string `json:"name"`        // 目录名,如 "usr"
-	Path        string `json:"path"`        // 完整绝对路径,如 "/usr"
-	IsDir       bool   `json:"isDir"`       // 恒 true
-	HasChildren bool   `json:"hasChildren"` // 是否含子目录(前端据此决定是否显示展开箭头)
+type LogArchiveListReq struct {
+	ArchiveName       string `json:"archiveName,optional"`
+	ArchivedBy        string `json:"archivedBy,optional"`
+	ArchivedStartTime string `json:"archivedStartTime,optional"`
+	ArchivedEndTime   string `json:"archivedEndTime,optional"`
+	Page              int64  `json:"page,optional"`
+	PageSize          int64  `json:"pageSize,optional"`
 }
 
-type DirChildrenResp struct {
-	Nodes []LocalDirNode `json:"nodes"`
+type LogArchiveListResp struct {
+	Total       int64        `json:"total"`
+	LogArchives []LogArchive `json:"logArchives"`
+}
+
+type LogArchiveRestoreReq struct {
+	Ids []int64 `json:"ids"`
+}
+
+type LogArchiveRestoreResp struct {
+	RestoreMsg string `json:"restoreMsg"`
+}
+
+type LogRestore struct {
+	Id             int64  `json:"id"`
+	ArchiveId      int64  `json:"archiveId"`
+	RestoreStatus  int    `json:"restoreStatus"` // 1.成功,2.失败,3.恢复中
+	RestoreMessage string `json:"restoreMessage"`
+	RestoredBy     string `json:"restoredBy"`
+	RestoredAt     string `json:"restoredAt"`
+}
+
+type LogRestoreListReq struct {
+	RestoreStatus  int64  `json:"restoreStatus,optional"`
+	RestoreMessage string `json:"restoreMessage,optional"`
+	Page           int64  `json:"page,optional"`
+	PageSize       int64  `json:"pageSize,optional"`
+}
+
+type LogRestoreListResp struct {
+	Total       int64        `json:"total"`
+	LogRestores []LogRestore `json:"logRestores"`
+}
+
+type Request struct {
+	Name string `path:"name,options=you|me"`
+}
+
+type Response struct {
+	Message string `json:"message"`
 }
 
 type ServiceIdReq struct {
@@ -132,6 +195,94 @@ type ServiceResp struct {
 	CreateAt string `json:"createAt"`
 }
 
+type SystemLog struct {
+	Id                  int64  `json:"id"`                  // 主键
+	Iden                string `json:"iden"`                // 日志标识
+	TraceId             string `json:"traceId"`             // 全链路TraceId
+	SpanId              string `json:"spanId"`              // 子链路SpanId
+	ParentSpanId        string `json:"parentSpanId"`        // 父SpanId
+	Type                int    `json:"type"`                // 日志类型:1.系统日志;2.接口日志
+	ServiceName         string `json:"serviceName"`         // 所属服务名称/模块名称
+	UriPath             string `json:"uriPath"`             // 接口请求路径
+	HttpMethod          string `json:"httpMethod"`          // 请求方法
+	Method              string `json:"method"`              // 业务方法名
+	Level               string `json:"level"`               // 日志级别
+	AppKey              string `json:"appKey"`              // 调用接口应用标识
+	UserId              string `json:"userId"`              // 系统用户ID
+	UserName            string `json:"userName"`            // 系统用户名
+	ClientIp            string `json:"clientIp"`            // 客户端IP
+	RequestHeader       string `json:"requestHeader"`       // 请求头
+	RequestBodyDigest   string `json:"requestBodyDigest"`   // 请求体摘要
+	RequestBodyFileKey  string `json:"requestBodyFileKey"`  // 请求体FileKey
+	ResponseCode        int64  `json:"responseCode"`        // 响应业务码
+	ResponseBodyDigest  string `json:"responseBodyDigest"`  // 响应体摘要
+	ResponseBodyFileKey string `json:"responseBodyFileKey"` // 响应体FileKey
+	ErrorMsg            string `json:"errorMsg"`            // 错误信息
+	DurationMs          int64  `json:"durationMs"`          // 请求耗时
+	CreatedAt           string `json:"createdAt"`           // 日志写入时间
+	Hmac                string `json:"hmac"`                // Hmac值
+	AuditStatus         int    `json:"auditStatus"`         // 审计状态:1.未审计;2.审计通过;3.审计异常
+}
+
+type SystemLogArchiveReq struct {
+	Name      string  `json:"name"`               // 归档任务名称
+	Ids       []int64 `json:"ids,optional"`       // 日志ID集合
+	StartDate string  `json:"startDate,optional"` // 归档开始日期
+	EndDate   string  `json:"endDate,optional"`   // 归档结束日期
+}
+
+type SystemLogArchiveResp struct {
+	Count int `json:"count"`
+}
+
+type SystemLogAuditReq struct {
+	Idens []string `json:"idens"`
+}
+
+type SystemLogAuditResp struct {
+	TotalCount   int64         `json:"totalCount"`
+	ValidCount   int64         `json:"validCount"`
+	InValidCount int64         `json:"inValidCount"`
+	AuditResults []AuditResult `json:"auditResults"`
+}
+
+type SystemLogDetailReq struct {
+	Iden string `json:"iden"`
+}
+
+type SystemLogDetailResp struct {
+	SystemLog
+}
+
+type SystemLogLinkReq struct {
+	TraceId string `json:"traceId"`
+}
+
+type SystemLogLinkResp struct {
+	LogLinks []SystemLog `json:"logLinks"`
+}
+
+type SystemLogListReq struct {
+	Iden             string `json:"iden,optional"`             // 日志标识
+	Type             int64  `json:"type,optional"`             // 日志类型
+	UserName         string `json:"userName,optional"`         // 用户名称
+	AppKey           string `json:"appKey,optional"`           // 应用标识
+	UriPath          string `json:"uriPath,optional"`          // 接口请求路径
+	HttpMethod       string `json:"httpMethod,optional"`       // 请求方式
+	CreatedStartTime string `json:"createdStartTime,optional"` // 写入开始时间
+	CreatedEndTime   string `json:"createdEndTime,optional"`   // 写入结束时间
+	ClientIp         string `json:"clientIp,optional"`         // 客户端IP
+	ErrorMsg         string `json:"errorMsg,optional"`         // 错误信息
+	AuditStatus      string `json:"auditStatus,optional"`      // 审计状态
+	Page             int64  `json:"page,optional"`
+	PageSize         int64  `json:"pageSize,optional"`
+}
+
+type SystemLogListResp struct {
+	Total int64       `json:"total"`
+	Logs  []SystemLog `json:"logs"`
+}
+
 type UpdateDirectoryReq struct {
 	Id        int64  `path:"id"`
 	ServiceId int64  `json:"serviceId,optional"`

+ 221 - 11
udeServer.api

@@ -85,14 +85,14 @@ type KeyListResp {
 }
 
 type CreateServiceReq {
-	Id      int64    `json:"id,optional"`
-	Name    string   `json:"name,optional"`
-	Ip      string   `json:"ip,optional"`
-	Port    string   `json:"port,optional"`
-	Iden    string   `json:"iden,optional"`
-	KeyIden string   `json:"keyIden,optional"`
-	KeyAlgo string   `json:"keyAlgo,optional"`
-	Status  string   `json:"status,optional"`
+	Id          int64    `json:"id,optional"`
+	Name        string   `json:"name,optional"`
+	Ip          string   `json:"ip,optional"`
+	Port        string   `json:"port,optional"`
+	Iden        string   `json:"iden,optional"`
+	KeyIden     string   `json:"keyIden,optional"`
+	KeyAlgo     string   `json:"keyAlgo,optional"`
+	Status      string   `json:"status,optional"`
 	Directories []string `json:"directories,optional"`
 }
 
@@ -135,13 +135,15 @@ type ServiceListResp {
 
 // 本地目录懒加载(udeServer 所在 Linux 主机文件系统,只列目录不列文件)
 type ListDirChildrenReq {
+	Ip   string `form:"ip"` // 目标 Linux 主机 IP
+	Port string `form:"port"` // 目标主机 /api/v1 端口
 	Path string `form:"path"` // 空或 "/" 表示根目录
 }
 
 type LocalDirNode {
-	Name        string `json:"name"`        // 目录名,如 "usr"
-	Path        string `json:"path"`        // 完整绝对路径,如 "/usr"
-	IsDir       bool   `json:"isDir"`       // 恒 true
+	Name        string `json:"name"` // 目录名,如 "usr"
+	Path        string `json:"path"` // 完整绝对路径,如 "/usr"
+	IsDir       bool   `json:"isDir"` // 恒 true
 	HasChildren bool   `json:"hasChildren"` // 是否含子目录(前端据此决定是否显示展开箭头)
 }
 
@@ -149,7 +151,215 @@ type DirChildrenResp {
 	Nodes []LocalDirNode `json:"nodes"`
 }
 
+// ===== 审计日志(移植自 sdf-serve systemLog 模块)=====
+type SystemLog {
+	Id                  int64  `json:"id"` // 主键
+	Iden                string `json:"iden"` // 日志标识
+	TraceId             string `json:"traceId"` // 全链路TraceId
+	SpanId              string `json:"spanId"` // 子链路SpanId
+	ParentSpanId        string `json:"parentSpanId"` // 父SpanId
+	Type                int    `json:"type"` // 日志类型:1.系统日志;2.接口日志
+	ServiceName         string `json:"serviceName"` // 所属服务名称/模块名称
+	UriPath             string `json:"uriPath"` // 接口请求路径
+	HttpMethod          string `json:"httpMethod"` // 请求方法
+	Method              string `json:"method"` // 业务方法名
+	Level               string `json:"level"` // 日志级别
+	AppKey              string `json:"appKey"` // 调用接口应用标识
+	UserId              string `json:"userId"` // 系统用户ID
+	UserName            string `json:"userName"` // 系统用户名
+	ClientIp            string `json:"clientIp"` // 客户端IP
+	RequestHeader       string `json:"requestHeader"` // 请求头
+	RequestBodyDigest   string `json:"requestBodyDigest"` // 请求体摘要
+	RequestBodyFileKey  string `json:"requestBodyFileKey"` // 请求体FileKey
+	ResponseCode        int64  `json:"responseCode"` // 响应业务码
+	ResponseBodyDigest  string `json:"responseBodyDigest"` // 响应体摘要
+	ResponseBodyFileKey string `json:"responseBodyFileKey"` // 响应体FileKey
+	ErrorMsg            string `json:"errorMsg"` // 错误信息
+	DurationMs          int64  `json:"durationMs"` // 请求耗时
+	CreatedAt           string `json:"createdAt"` // 日志写入时间
+	Hmac                string `json:"hmac"` // Hmac值
+	AuditStatus         int    `json:"auditStatus"` // 审计状态:1.未审计;2.审计通过;3.审计异常
+}
+
+type SystemLogListReq {
+	Iden             string `json:"iden,optional"` // 日志标识
+	Type             int64  `json:"type,optional"` // 日志类型
+	UserName         string `json:"userName,optional"` // 用户名称
+	AppKey           string `json:"appKey,optional"` // 应用标识
+	UriPath          string `json:"uriPath,optional"` // 接口请求路径
+	HttpMethod       string `json:"httpMethod,optional"` // 请求方式
+	CreatedStartTime string `json:"createdStartTime,optional"` // 写入开始时间
+	CreatedEndTime   string `json:"createdEndTime,optional"` // 写入结束时间
+	ClientIp         string `json:"clientIp,optional"` // 客户端IP
+	ErrorMsg         string `json:"errorMsg,optional"` // 错误信息
+	AuditStatus      string `json:"auditStatus,optional"` // 审计状态
+	Page             int64  `json:"page,optional"`
+	PageSize         int64  `json:"pageSize,optional"`
+}
+
+type SystemLogListResp {
+	Total int64       `json:"total"`
+	Logs  []SystemLog `json:"logs"`
+}
+
+type SystemLogDetailReq {
+	Iden string `json:"iden"`
+}
+
+type SystemLogDetailResp {
+	SystemLog
+}
+
+type SystemLogAuditReq {
+	Idens []string `json:"idens"`
+}
+
+type AuditResult {
+	LogIden string `json:"logIden"`
+	Valid   bool   `json:"valid"`
+	Reason  string `json:"reason"`
+}
+
+type SystemLogAuditResp {
+	TotalCount   int64         `json:"totalCount"`
+	ValidCount   int64         `json:"validCount"`
+	InValidCount int64         `json:"inValidCount"`
+	AuditResults []AuditResult `json:"auditResults"`
+}
+
+type SystemLogArchiveReq {
+	Name      string  `json:"name"` // 归档任务名称
+	Ids       []int64 `json:"ids,optional"` // 日志ID集合
+	StartDate string  `json:"startDate,optional"` // 归档开始日期
+	EndDate   string  `json:"endDate,optional"` // 归档结束日期
+}
+
+type SystemLogArchiveResp {
+	Count int `json:"count"`
+}
+
+type SystemLogLinkReq {
+	TraceId string `json:"traceId"`
+}
+
+type SystemLogLinkResp {
+	LogLinks []SystemLog `json:"logLinks"`
+}
+
+type LogArchive {
+	Id             int64  `json:"id"`
+	ArchiveName    string `json:"archiveName"`
+	RecordCount    int64  `json:"recordCount"`
+	ArchiveFileKey string `json:"archiveFileKey"`
+	ArchiveMode    int    `json:"archiveMode"` // 归档模式:1.手动,2.自动
+	BatchSize      int    `json:"batchSize"`
+	ArchivedBy     string `json:"archivedBy"`
+	ArchivedAt     string `json:"archivedAt"`
+}
+
+type LogArchiveListReq {
+	ArchiveName       string `json:"archiveName,optional"`
+	ArchivedBy        string `json:"archivedBy,optional"`
+	ArchivedStartTime string `json:"archivedStartTime,optional"`
+	ArchivedEndTime   string `json:"archivedEndTime,optional"`
+	Page              int64  `json:"page,optional"`
+	PageSize          int64  `json:"pageSize,optional"`
+}
+
+type LogArchiveListResp {
+	Total       int64        `json:"total"`
+	LogArchives []LogArchive `json:"logArchives"`
+}
+
+type LogArchiveDownloadReq {
+	Ids []int64 `json:"ids"`
+}
 
+type LogArchiveRestoreReq {
+	Ids []int64 `json:"ids"`
+}
+
+type LogArchiveRestoreResp {
+	RestoreMsg string `json:"restoreMsg"`
+}
+
+type LogRestore {
+	Id             int64  `json:"id"`
+	ArchiveId      int64  `json:"archiveId"`
+	RestoreStatus  int    `json:"restoreStatus"` // 1.成功,2.失败,3.恢复中
+	RestoreMessage string `json:"restoreMessage"`
+	RestoredBy     string `json:"restoredBy"`
+	RestoredAt     string `json:"restoredAt"`
+}
+
+type LogRestoreListReq {
+	RestoreStatus  int64  `json:"restoreStatus,optional"`
+	RestoreMessage string `json:"restoreMessage,optional"`
+	Page           int64  `json:"page,optional"`
+	PageSize       int64  `json:"pageSize,optional"`
+}
+
+type LogRestoreListResp {
+	Total       int64        `json:"total"`
+	LogRestores []LogRestore `json:"logRestores"`
+}
+
+@server (
+	prefix: /api/v1/systemlog
+	group:  systemlog
+)
+// 审计日志管理模块
+service udeServer-api {
+	@doc "日志分页列表"
+	@handler LogListHandler
+	post /list (SystemLogListReq) returns (SystemLogListResp)
+
+	@doc "日志详情"
+	@handler LogDetailHandler
+	post /detail (SystemLogDetailReq) returns (SystemLogDetailResp)
+
+	@doc "日志审计(校验Hmac防篡改)"
+	@handler LogAuditHandler
+	post /audit (SystemLogAuditReq) returns (SystemLogAuditResp)
+
+	@doc "日志归档"
+	@handler LogArchiveHandler
+	post /archive (SystemLogArchiveReq) returns (SystemLogArchiveResp)
+
+	@doc "查看完整链路"
+	@handler LogLinkHandler
+	post /link (SystemLogLinkReq) returns (SystemLogLinkResp)
+}
+
+@server (
+	prefix: /api/v1/logarchive
+	group:  systemlog
+)
+// 归档管理模块
+service udeServer-api {
+	@doc "归档列表"
+	@handler LogArchiveListHandler
+	post /list (LogArchiveListReq) returns (LogArchiveListResp)
+
+	@doc "归档打包下载(zip)"
+	@handler LogArchiveDownloadHandler
+	post /download (LogArchiveDownloadReq)
+
+	@doc "归档恢复"
+	@handler LogArchiveRestoreHandler
+	post /restore (LogArchiveRestoreReq) returns (LogArchiveRestoreResp)
+}
+
+@server (
+	prefix: /api/v1/logrestore
+	group:  systemlog
+)
+// 恢复记录模块
+service udeServer-api {
+	@doc "恢复记录列表"
+	@handler LogRestoreListHandler
+	post /list (LogRestoreListReq) returns (LogRestoreListResp)
+}
 
 @server (
 	prefix: /api/v1

+ 12 - 0
udeserver.go

@@ -9,8 +9,10 @@ import (
 
 	"udeServer/internal/config"
 	"udeServer/internal/handler"
+	"udeServer/internal/log"
 	"udeServer/internal/response"
 	"udeServer/internal/svc"
+	"udeServer/internal/task"
 
 	"github.com/zeromicro/go-zero/core/conf"
 	"github.com/zeromicro/go-zero/rest"
@@ -24,6 +26,9 @@ func main() {
 	var c config.Config
 	conf.MustLoad(*configFile, &c)
 
+	// 彩色控制台日志(Log.Mode=console 时生效)
+	log.SetupLoggerWithColor(c.Log)
+
 	// 所有成功/失败响应统一封装为 {"code":100000/100xxx,"message":...,"data":...},
 	// HTTP 状态恒为 200;必须在 server.Start 之前注册。
 	response.Register()
@@ -33,8 +38,15 @@ func main() {
 	defer server.Stop()
 
 	ctx := svc.NewServiceContext(c)
+
+	// 注册全局审计日志中间件(type=1 系统日志;在路由注册前 Use)
+	server.Use(ctx.LogMiddleware)
+
 	handler.RegisterHandlers(server, ctx)
 
+	// 注册定时任务(每日 00:30 自动归档)
+	go task.StartScheduler(ctx)
+
 	fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
 	server.Start()
 }