Procházet zdrojové kódy

fix:临时提交,添加AI配置,解决部分故障录波的问题

Jerry Liang před 3 měsíci
rodič
revize
87d214fe41

+ 507 - 0
.kiro/rules/coding-standards.md

@@ -0,0 +1,507 @@
+# FranklinWH-ToolBox AI 编程规范与操作规范
+
+## 项目概览
+
+本项目是 FranklinWH 内部工具箱平台,采用 monorepo 结构(pnpm workspace),包含前端 Web 应用和后端 API 服务。
+
+- **前端**:Vue 3 + TypeScript + Vite (rolldown-vite)
+- **后端**:Go 1.25+ + Fiber v2
+- **包管理**:pnpm(前端)、Go Modules(后端)
+
+---
+
+## 一、项目结构规范
+
+### 前端目录结构(根目录)
+
+```
+src/
+├── assets/          # 静态资源(CSS、图片、数据文件)
+├── components/      # 全局通用组件
+├── composable/      # Vue Composables(可组合函数)
+├── event/           # 全局事件系统(EventEmitter)
+├── injection/       # Vue provide/inject 相关
+├── layout/          # 布局组件(header、aside、footer、main)
+├── router/          # 路由配置、守卫、辅助函数
+├── service/         # API 请求层(按业务模块分目录)
+├── stores/          # Pinia 状态管理
+├── theme/           # 主题配置(light/dark/common)
+├── types/           # TypeScript 类型声明
+├── typings/         # 全局类型扩展(window.d.ts、vue-router.d.ts)
+├── utils/           # 工具函数
+├── views/           # 页面视图组件(按业务模块分目录)
+├── App.vue          # 根组件
+└── main.ts          # 入口文件
+```
+
+### 后端目录结构(service/)
+
+```
+service/
+├── cmd/             # 脚本(部署、构建、日志查看)
+├── cache/           # 缓存层(Redis)
+├── config/          # 配置加载(Viper)
+├── controllers/     # 控制器层(请求处理、参数校验)
+├── database/        # 数据库初始化(MySQL、PostgreSQL、MongoDB)
+├── domain/          # 领域模型/实体定义
+├── dto/             # 数据传输对象
+├── docs/            # Swagger 文档
+├── function/        # 功能模块(文件处理等)
+├── jobs/            # 后台定时任务
+├── logger/          # 日志配置(Zap)
+├── middleware/      # 中间件(认证等)
+├── models/          # 数据库模型(GORM)
+├── networkEvents/   # 网络事件(Evio)
+├── repositories/    # 数据访问层(MySQL/PgSQL/MongoDB/Redis/MQTT/API)
+├── routes/          # 路由注册
+├── scheduler/       # 定时调度器(Cron)
+├── services/        # 业务逻辑层
+├── task/            # 异步任务队列(Asynq)
+├── test/            # 测试文件
+└── utils/           # 工具包
+```
+
+---
+
+## 二、前端编码规范
+
+### 2.1 通用格式
+
+| 项目     | 规范       |
+| -------- | ---------- |
+| 缩进     | 2 空格     |
+| 分号     | 不使用分号 |
+| 引号     | 单引号     |
+| 行宽     | 100 字符   |
+| 行尾     | LF         |
+| 尾逗号   | ES5 风格   |
+| 文件末尾 | 保留空行   |
+
+### 2.2 TypeScript
+
+- 启用严格模式(`strict: true`)
+- 使用路径别名 `@/` 指向 `src/`
+- 优先使用 `interface` 定义对象类型,`type` 用于联合类型/工具类型
+- 避免 `any`,必要时使用 `unknown` 并进行类型收窄
+- 导出类型时使用 `export type` / `export interface`
+
+### 2.3 Vue 组件
+
+```vue
+<template>
+  <!-- 使用 UnoCSS 原子类 -->
+  <div class="flex items-center gap-4 p-4">
+    <NButton @click="handleClick">{{ displayTitle }}</NButton>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 1. 第三方库导入
+import { NButton } from 'naive-ui'
+import { ref, computed, onMounted } from 'vue'
+
+// 2. 内部模块导入
+import { useXxxStore } from '@/stores/xxx'
+import type { SomeType } from '@/service/xxx/interface'
+
+// 3. 组件选项
+defineOptions({
+  name: 'ComponentName',
+})
+
+// 4. Props & Emits
+const props = defineProps<{
+  title: string
+  count?: number
+}>()
+
+const emit = defineEmits<{
+  update: [value: string]
+}>()
+
+// 5. 响应式状态
+const loading = ref(false)
+
+// 6. 计算属性
+const displayTitle = computed(() => `${props.title} (${props.count ?? 0})`)
+
+// 7. 方法
+function handleClick() {
+  emit('update', 'new value')
+}
+
+// 8. 生命周期
+onMounted(() => {
+  // ...
+})
+</script>
+```
+
+**关键约定**:
+
+- 使用 `<script setup lang="ts">` + Composition API
+- 使用 `defineOptions({ name: 'XxxPage' })` 定义组件名
+- Props 使用 `defineProps<T>()` 泛型语法
+- Emits 使用 `defineEmits<T>()` 泛型语法
+- 不在 template 中使用复杂表达式,提取为 computed 或方法
+- 模板中组件使用 PascalCase(如 `<NButton>`)
+
+### 2.4 Pinia Store
+
+```typescript
+import { useStorage } from '@vueuse/core'
+import { acceptHMRUpdate, defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+
+export const useXxxStore = defineStore('xxxStore', () => {
+  // 持久化状态使用 useStorage
+  const data = useStorage<DataType>('storageKey', defaultValue)
+
+  // 非持久化状态使用 ref
+  const loading = ref(false)
+
+  // Getters 使用 computed
+  const isEmpty = computed(() => data.value.length === 0)
+
+  // Actions 使用普通函数
+  function reset() {
+    data.value = defaultValue
+  }
+
+  return {
+    data,
+    loading,
+    isEmpty,
+    reset,
+  }
+})
+
+// HMR 支持(必须)
+if (import.meta.hot) {
+  import.meta.hot.accept(acceptHMRUpdate(useXxxStore, import.meta.hot))
+}
+```
+
+### 2.5 Service 层(API 请求)
+
+每个业务模块在 `src/service/` 下创建独立目录,包含:
+
+- `index.ts` — API 函数导出
+- `interface.ts` — 请求/响应类型定义
+
+```typescript
+// src/service/moduleName/interface.ts
+export interface ModuleItem {
+  id: string
+  name: string
+  createdAt: string
+}
+
+// src/service/moduleName/index.ts
+import http from '@/service/serverReq'
+import type { ModuleItem } from './interface'
+
+const API_List = {
+  list: '/moduleName/list',
+  detail: '/moduleName/detail',
+}
+
+export const getModuleList = async (page: number, size: number): Promise<ModuleItem[]> => {
+  return await http.get<ModuleItem[]>(API_List.list, {
+    params: { page, size },
+  })
+}
+
+export const getModuleDetail = async (id: string): Promise<ModuleItem> => {
+  return await http.get<ModuleItem>(API_List.detail, {
+    params: { id },
+  })
+}
+```
+
+### 2.6 样式规范
+
+- **优先使用 UnoCSS 原子类**,避免自定义 CSS
+- 暗色模式使用 `dark:` 前缀变体
+- 响应式使用 `sm:` / `md:` / `lg:` / `xl:` 前缀
+- CSS 变量引用使用 `var(--xxx)` 语法(如 `bg-[var(--card-color)]`)
+- 图标使用 UnoCSS Icons(如 `i-ph:arrow-up`、`i-mdi:close`)
+- 过渡动画使用项目自定义 `transition-[property]-duration` 规则
+
+### 2.7 路由
+
+- 路由记录定义在 `src/router/record.ts`
+- 懒加载页面组件:`component: () => import('@/views/xxx/index.vue')`
+- 页面视图目录使用 kebab-case 命名(如 `site-info-manage`)
+- 路由 name 使用 camelCase(如 `siteInfoManage`)
+
+---
+
+## 三、后端编码规范(Go)
+
+### 3.1 分层职责
+
+| 层级         | 职责                           | 命名规范            |
+| ------------ | ------------------------------ | ------------------- |
+| routes       | 路由注册                       | `xxxRoutes.go`      |
+| controllers  | 请求处理、参数校验、响应格式化 | `xxxControllers.go` |
+| services     | 业务逻辑编排                   | `xxxServices.go`    |
+| repositories | 数据访问(DB/Cache/API/MQTT)  | 按数据源分子目录    |
+| domain       | 领域实体结构定义               | `xxxDomain.go`      |
+| dto          | 请求/响应数据传输对象          | `xxxDTO.go`         |
+
+### 3.2 路由文件
+
+```go
+package routes
+
+import (
+    "fwh-toolbox-server/controllers"
+    "github.com/gofiber/fiber/v2"
+)
+
+func XxxSetup(api fiber.Router) {
+    api.Get("/xxx/list", controllers.GetXxxList)
+    api.Post("/xxx/create", controllers.CreateXxx)
+}
+```
+
+### 3.3 Controller
+
+```go
+package controllers
+
+import (
+    "fwh-toolbox-server/services"
+    "github.com/gofiber/fiber/v2"
+    "go.uber.org/zap"
+)
+
+func GetXxxList(c *fiber.Ctx) error {
+    page := c.QueryInt("page", 1)
+    size := c.QueryInt("size", 20)
+
+    data, err := services.GetXxxList(uint(page), uint(size))
+    if err != nil {
+        zap.L().Error("GetXxxList failed.", zap.String("err", err.Error()))
+        return Error(c, "Get xxx list failed.")
+    }
+    return Success(c, data)
+}
+```
+
+**关键约定**:
+
+- 使用统一响应函数:`Success`、`SuccessPage`、`Fail`、`Error`、`NotFound`、`Unauthorized`、`Forbidden`
+- 参数校验在 Controller 层完成
+- 错误日志使用 `zap.L()` 结构化日志
+- 业务错误返回中文/英文描述,不暴露内部错误细节
+
+### 3.4 Service
+
+```go
+package services
+
+import (
+    "fwh-toolbox-server/repositories"
+    "go.uber.org/zap"
+)
+
+func GetXxxList(page, size uint) (*SomeResult, error) {
+    data, err := repositories.GetXxxList(page, size)
+    if err != nil {
+        zap.L().Error("repositories.GetXxxList failed.",
+            zap.String("err", err.Error()),
+            zap.Uint("page", page),
+            zap.Uint("size", size),
+        )
+        return nil, err
+    }
+    // 业务逻辑处理...
+    return result, nil
+}
+```
+
+### 3.5 日志规范
+
+- 使用 `go.uber.org/zap` 结构化日志
+- 日志级别:`Debug` < `Info` < `Warn` < `Error` < `Fatal`
+- 错误日志必须包含错误信息和关键上下文参数
+- 格式:`zap.L().Error("描述", zap.String("key", value), ...)`
+
+### 3.6 配置管理
+
+- 使用 Viper 读取配置(支持 `.env` 文件 + 环境变量)
+- 配置结构体定义在 `config/config.go`
+- 密码等敏感信息通过环境变量注入,不硬编码
+
+---
+
+## 四、通用开发规范
+
+### 4.1 命名规范
+
+| 类型      | 前端                                               | 后端                                  |
+| --------- | -------------------------------------------------- | ------------------------------------- |
+| 文件名    | camelCase.ts / kebab-case.vue                      | camelCase.go                          |
+| 变量      | camelCase                                          | camelCase                             |
+| 常量      | UPPER_SNAKE_CASE 或 camelCase                      | UPPER_SNAKE_CASE 或 camelCase         |
+| 函数      | camelCase                                          | PascalCase(导出)/ camelCase(私有) |
+| 类型/接口 | PascalCase                                         | PascalCase                            |
+| 组件      | PascalCase(.vue 文件名可 index.vue 或 camelCase) | —                                     |
+| CSS 类    | UnoCSS 原子类 / kebab-case                         | —                                     |
+| 路由路径  | kebab-case                                         | camelCase                             |
+| 目录名    | kebab-case(views)/ camelCase(service、stores)  | camelCase                             |
+
+### 4.2 Git 提交规范
+
+```
+<type>(<scope>): <subject>
+
+type: feat | fix | docs | style | refactor | perf | test | chore | ci
+scope: 可选,影响范围(如 frontend, backend, router, store)
+subject: 简短描述,不超过 50 字符
+```
+
+### 4.3 注释规范
+
+- 前端:重要逻辑使用 `//` 行注释,中文或英文均可
+- 后端:Controller 函数头部使用 `// 功能描述` 注释
+- 路由文件头部注释 API 清单:
+
+```go
+/*
+ * Module Name API List
+ * /module/action1 - GET 描述
+ * /module/action2 - POST 描述
+ */
+```
+
+---
+
+## 五、工具链与命令
+
+### 前端命令
+
+| 命令                | 用途                     |
+| ------------------- | ------------------------ |
+| `pnpm dev`          | 启动开发服务器           |
+| `pnpm build`        | 类型检查 + 生产构建      |
+| `pnpm test:unit`    | 运行 Vitest 单元测试     |
+| `pnpm test:e2e`     | 运行 Playwright E2E 测试 |
+| `pnpm lint`         | ESLint 检查              |
+| `pnpm lint:fix`     | ESLint 自动修复          |
+| `pnpm format:check` | Prettier 格式检查        |
+| `pnpm format:fix`   | Prettier 自动格式化      |
+
+### 后端命令
+
+| 命令                             | 用途                             |
+| -------------------------------- | -------------------------------- |
+| `go run main.go`                 | 启动服务(在 service/ 目录执行) |
+| `go build -o fwh-toolbox-server` | 编译(在 service/ 目录执行)     |
+| `go test ./...`                  | 运行测试                         |
+| `./cmd/deploy.sh`                | 部署脚本(在 service/ 目录执行) |
+
+### ESLint 配置要点
+
+- 基于 `@antfu/eslint-config`
+- 禁用的规则:`perfectionist/sort-imports`、`vue/block-order`、`no-console`(开发期)
+- 强制 Vue SFC 使用 `<script lang="ts">` 或 `<script lang="tsx">`
+- 启用 UnoCSS、Vitest、Playwright 插件支持
+
+---
+
+## 六、AI 操作规范
+
+### 6.1 代码生成
+
+- 生成前端代码时,遵循本项目的 Composition API + `<script setup>` 风格
+- 使用 Naive UI 组件库,不引入其他 UI 框架
+- 样式优先使用 UnoCSS 原子类
+- API 请求使用 `@/service/serverReq` 中封装的 `http` 实例
+- 新页面按 `src/views/module-name/index.vue` 结构创建
+- 新 API 模块按 `src/service/moduleName/{index.ts, interface.ts}` 结构创建
+- Store 使用 setup function 风格 + HMR 热更新支持
+- 后端新增接口遵循 routes → controllers → services → repositories 分层
+
+### 6.2 代码修改
+
+- 修改前先读取相关文件,理解现有实现
+- 保持与周围代码一致的风格
+- 不随意删除注释掉的代码(可能是有意保留的备选方案)
+- 修改前需详细说明修改或创建的内容,获得同意后再写入文件
+- 修改后确保通过 `pnpm lint` 检查
+
+### 6.3 依赖管理
+
+- 前端使用 pnpm 安装依赖,锁定具体版本
+- 后端使用 `go get` 添加依赖
+- 不引入与现有技术栈冲突的库(如不使用 Axios、Element UI、Tailwind CSS)
+- 工具函数优先使用 `es-toolkit`(替代 lodash)
+- 日期处理使用 `dayjs`
+- 状态持久化使用 `@vueuse/core` 的 `useStorage`
+
+### 6.4 类型安全
+
+- 所有新增 API 响应必须定义 TypeScript 接口
+- 后端 DTO 结构体必须添加 JSON tag
+- 避免使用 `any`、`interface{}`(Go 中优先使用泛型或具体类型)
+
+### 6.5 测试
+
+- 单元测试文件放在 `src/**/__tests__/` 下
+- 使用 Vitest 编写测试,测试文件后缀 `.test.ts`
+- E2E 测试放在 `e2e/` 目录,使用 Playwright
+- 后端测试放在 `service/test/` 目录
+
+### 6.6 安全注意事项
+
+- 不在代码中硬编码密钥、Token、密码
+- `.env` 文件中的敏感配置不应被提交到仓库
+- API 请求自动附带 Authorization header(通过拦截器)
+- 后端使用 middleware 进行认证校验
+
+### 6.7 文件操作
+
+- 编辑前端文件时,工作目录为项目根目录
+- 编辑后端文件时,注意文件在 `service/` 子目录内
+- Go 模块路径为 `fwh-toolbox-server`
+- Go import 使用完整模块路径(如 `fwh-toolbox-server/controllers`)
+
+---
+
+## 七、技术栈速查
+
+### 前端核心依赖
+
+| 库            | 用途       | 版本要求 |
+| ------------- | ---------- | -------- |
+| Vue 3         | 框架       | ^3.5     |
+| Naive UI      | UI 组件库  | ^2.44    |
+| Pinia         | 状态管理   | ^3.0     |
+| Vue Router    | 路由       | ^5.0     |
+| VueUse        | 组合式工具 | ^14.x    |
+| UnoCSS        | 原子 CSS   | ^66.x    |
+| @antv/g2      | 图表       | ^5.x     |
+| es-toolkit    | 工具函数   | ^1.x     |
+| dayjs         | 日期处理   | ^1.x     |
+| monaco-editor | 代码编辑器 | ^0.55    |
+
+### 后端核心依赖
+
+| 库             | 用途                    |
+| -------------- | ----------------------- |
+| Fiber v2       | Web 框架                |
+| GORM           | ORM(MySQL/PostgreSQL) |
+| mongo-driver   | MongoDB 驱动            |
+| go-redis/v9    | Redis 客户端            |
+| zap            | 结构化日志              |
+| viper          | 配置管理                |
+| asynq          | 异步任务队列            |
+| cron/v3        | 定时任务                |
+| minio-go       | 对象存储                |
+| paho.mqtt      | MQTT 客户端             |
+| casdoor-go-sdk | SSO 认证                |
+| excelize       | Excel 处理              |
+| swag           | Swagger 文档生成        |

+ 1691 - 0
service/DESIGN.md

@@ -0,0 +1,1691 @@
+# FranklinWH-ToolBox Service 设计文档
+
+> 本文档为项目技术设计说明,供新加入的开发者快速了解系统全貌。
+
+---
+
+## 目录
+
+1. [项目概述](#1-项目概述)
+2. [系统架构](#2-系统架构)
+3. [目录结构](#3-目录结构)
+4. [核心依赖](#4-核心依赖)
+5. [配置管理](#5-配置管理)
+6. [路由与API](#6-路由与api)
+7. [业务模块](#7-业务模块)
+8. [数据模型](#8-数据模型)
+9. [基础设施](#9-基础设施)
+10. [任务系统](#10-任务系统)
+11. [外部集成](#11-外部集成)
+12. [部署与运行](#12-部署与运行)
+
+---
+
+## 1. 项目概述
+
+### 1.1 项目定位
+
+FranklinWH-ToolBox Service 是 FranklinWH 家庭储能系统的**内部运维工具平台后端**,为运维团队提供站点监控、数据下载、设备管理、告警分析、OTA 升级配置等核心能力。
+
+### 1.2 核心功能
+
+- **站点信息管理** — 从 FWH Pro 平台同步全量站点数据,支持多维度筛选、标签管理和 Excel 导出
+- **批量数据下载** — 批量抓取站点运行数据(功率流、MQTT 日志、事件告警、EMS 日志、故障录波等),异步打包为 ZIP
+- **站点监控与分析** — 实时查看站点功率流、告警、模式信息;分析站点历史时间线
+- **自动升级配置** — 管理 aGate/aPower 设备 OTA 升级包和升级策略
+- **停机与告警统计** — 按区域统计每日停机数据,汇总告警码和受影响站点
+- **SIM 卡管理** — 定时采集 SmartHub/Acceleronix 平台 SIM 卡状态及流量
+- **故障录波** — 通过 MQTT 向设备下发故障录波参数设置指令
+- **设备日志(Xlog)** — 管理站点 Xlog 连接,通过 WebSocket 发送终端命令
+
+### 1.3 技术栈
+
+| 层级 | 技术选型 |
+|------|----------|
+| 语言 | Go 1.25 |
+| Web 框架 | Fiber v2 |
+| ORM | GORM + Gen(代码生成) |
+| 数据库 | PostgreSQL + MongoDB + MySQL(只读) |
+| 缓存 | Redis(go-redis v9) |
+| 对象存储 | MinIO |
+| 消息通信 | MQTT(Paho) |
+| 异步任务 | Asynq + 自研 Redis Stream 队列 |
+| 定时任务 | robfig/cron + Asynq Scheduler |
+| 认证 | Casdoor SSO + JWT |
+| 日志 | Zap(结构化日志) |
+| 配置 | Viper(.env 文件 + 环境变量) |
+| API 文档 | Swagger(swaggo 自动生成) |
+| 网络事件 | Evio(高性能事件驱动) |
+
+---
+
+## 2. 系统架构
+
+### 2.1 整体架构图
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│                           客户端(Web 前端)                              │
+└───────────────────────────────────┬─────────────────────────────────────┘
+                                    │ HTTP / WebSocket
+                                    ▼
+┌─────────────────────────────────────────────────────────────────────────┐
+│                         Fiber HTTP 服务 (:8301)                          │
+│  ┌──────────┐  ┌──────────────┐  ┌──────────┐  ┌────────────────────┐  │
+│  │ Swagger  │  │  Auth Route  │  │  /ping   │  │  /api/* (Protected)│  │
+│  └──────────┘  └──────────────┘  └──────────┘  └────────────────────┘  │
+└───────────────────────────────────┬─────────────────────────────────────┘
+                                    │
+                                    ▼
+┌─────────────────────────────────────────────────────────────────────────┐
+│                         Middleware 层                                     │
+│              Casdoor JWT 认证 │ 请求日志 │ Panic 恢复                     │
+└───────────────────────────────────┬─────────────────────────────────────┘
+                                    │
+                                    ▼
+┌─────────────────────────────────────────────────────────────────────────┐
+│                         Controller 层                                     │
+│   解析请求参数,调用 Service,返回响应                                      │
+└───────────────────────────────────┬─────────────────────────────────────┘
+                                    │
+                                    ▼
+┌─────────────────────────────────────────────────────────────────────────┐
+│                         Service 层(业务逻辑)                             │
+│                                   │                                      │
+│              ┌────────────────────┼────────────────────┐                 │
+│              ▼                    ▼                    ▼                  │
+│     直通 CRUD 模式       任务触发模式          缓存优先模式               │
+│                          (入队异步执行)        (Redis → DB)              │
+└──────────────┬────────────────────┬────────────────────┬─────────────────┘
+               │                    │                    │
+               │              ┌─────▼──────┐             │
+               │              │ Task Queue │             │
+               │              │ (FWH队列)  │             │
+               │              │ (SimCard队列)            │
+               │              └─────┬──────┘             │
+               │                    │                    │
+               │              ┌─────▼──────┐             │
+               │              │   Jobs 层   │            │
+               │              │ (后台作业)  │             │
+               │              └─────┬──────┘             │
+               │                    │                    │
+               ▼                    ▼                    ▼
+┌─────────────────────────────────────────────────────────────────────────┐
+│                    Repository 层(数据访问聚合)                           │
+│  ┌────────┐ ┌──────┐ ┌───────┐ ┌───────┐ ┌──────┐ ┌──────┐ ┌───────┐  │
+│  │MongoDB │ │PgSQL │ │ MySQL │ │ Redis │ │MinIO │ │ MQTT │ │FWH API│  │
+│  └────────┘ └──────┘ └───────┘ └───────┘ └──────┘ └──────┘ └───────┘  │
+└─────────────────────────────────────────────────────────────────────────┘
+```
+
+### 2.2 请求处理流
+
+```
+Client (HTTP Request)
+    │
+    ▼
+Route (Fiber 路由匹配)
+    │
+    ▼
+Middleware (JWT 认证 / 日志记录)
+    │
+    ▼
+Controller (参数解析、校验)
+    │
+    ▼
+Service (业务逻辑) ──────────────────────────────┐
+    │                                            ▼
+    │                              Task (实时任务) / Scheduler (定时任务)
+    │                                            │
+    │                                            ▼
+    │                                          Jobs (后台作业)
+    ▼                                            │
+Repository (数据访问聚合) ◀──────────────────────┘
+    │
+    ▼
+Database / Cache / 外部服务 (FWH API / MQTT / MinIO)
+```
+
+### 2.3 多数据源架构
+
+```
+┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
+│   MySQL (只读)    │     │   PostgreSQL     │     │    MongoDB       │
+│ FWH Pro 生产库    │     │ 本地业务数据      │     │ 文档型业务数据    │
+│ - iot_gateway    │     │ - 告警快照        │     │ - 站点信息       │
+│                  │     │ - 原始数据存储    │     │ - 批量下载配置    │
+│                  │     │                  │     │ - 升级配置       │
+│                  │     │                  │     │ - SIM卡数据      │
+└──────────────────┘     └──────────────────┘     └──────────────────┘
+
+┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
+│     Redis        │     │     MinIO        │     │     MQTT         │
+│ - 会话缓存       │     │ - 下载文件存储    │     │ - 设备通信       │
+│ - 任务队列       │     │ - 升级配置文件    │     │ - Xlog 使能      │
+│ - 分布式锁       │     │                  │     │ - 故障录波       │
+│ - Dashboard缓存  │     │                  │     │                  │
+└──────────────────┘     └──────────────────┘     └──────────────────┘
+```
+
+---
+
+## 3. 目录结构
+
+```bash
+service/
+├── main.go                 # 应用入口(初始化所有组件 + 启动服务)
+├── .env                    # 环境变量配置文件
+├── config.env              # 备选配置文件
+├── go.mod / go.sum         # Go 模块依赖
+├── DESIGN.md               # 本设计文档
+├── README.md               # 项目简介
+│
+├── config/                 # 配置管理
+│   └── config.go           #   Viper 加载配置,定义 Config/AppConfig/FwhApiConfig 等结构体
+│
+├── logger/                 # 日志系统
+│   └── logger.go           #   Zap 初始化,环境区分(dev/pro),全局 Logger
+│
+├── database/               # 数据库连接
+│   ├── database.go         #   统一初始化入口(PostgreSQL + MongoDB)
+│   ├── postgresql.go       #   GORM PostgreSQL 连接 + 自定义 Logger
+│   ├── mysql.go            #   GORM MySQL 连接(当前未启用)
+│   └── mongodb.go          #   MongoDB 连接 + GetCollection
+│
+├── cache/                  # 缓存封装
+│   ├── cache.go            #   统一 API 暴露(Set/Get/HSet/XAdd/TryLock 等)
+│   ├── cache_aside.go      #   Cache-Aside 模式(GetOrSet/GetOrSetMany)
+│   ├── stale.go            #   Stale 异步刷新模式
+│   ├── options.go          #   可选配置(TTL/锁/前缀等)
+│   └── redisx/             #   Redis 底层操作封装
+│       ├── stringx.go      #     String 操作
+│       ├── hashx.go        #     Hash 操作
+│       ├── listx.go        #     List 操作
+│       ├── setx.go         #     Set 操作
+│       ├── zsetx.go        #     ZSet 操作
+│       ├── streamx.go      #     Stream 操作(任务队列核心)
+│       ├── jsonx.go        #     泛型 JSON 序列化操作
+│       ├── lockx.go        #     分布式锁(SET NX + Lua)
+│       ├── pipelinex.go    #     Pipeline 管道
+│       ├── pubsubx.go      #     发布/订阅
+│       └── scriptx.go      #     Lua 脚本
+│
+├── routes/                 # 路由注册
+│   ├── routes.go           #   主路由入口 Setup()
+│   ├── authRoutes.go       #   认证路由(/auth)
+│   ├── batchDownloadRoutes.go
+│   ├── sitesMonitorRoutes.go
+│   ├── sitesAnalysisRoutes.go
+│   ├── autoUpdateConfigRoutes.go
+│   ├── siteInfoManageRoutes.go
+│   ├── shutdownRoutes.go
+│   ├── simCardManageRoutes.go
+│   └── ...                 #   各业务模块独立路由文件
+│
+├── middleware/             # 中间件
+│   └── auth.go             #   Casdoor JWT 认证 + 角色检查
+│
+├── controllers/            # 控制器层
+│   ├── batchDownloadControllers.go
+│   ├── autoUpdateConfigControllers.go
+│   ├── siteInfoManageControllers.go
+│   ├── sitesMonitorControllers.go
+│   ├── sitesAnalysisControllers.go
+│   ├── authControllers.go
+│   └── ...                 #   各业务模块控制器
+│
+├── services/               # 业务逻辑层
+│   ├── batchDownloadServices.go
+│   ├── siteInfoManageServices.go
+│   ├── sitesAnalysisServices.go
+│   ├── authServices.go
+│   ├── simCardManageServices.go
+│   └── ...                 #   各业务模块服务
+│
+├── repositories/           # 数据访问层(聚合模式)
+│   ├── repositories.go     #   统一入口,暴露所有子模块方法
+│   ├── mongo/              #   MongoDB 数据操作
+│   ├── redis/              #   Redis 数据操作(Session/缓存/队列)
+│   ├── pgsql/              #   PostgreSQL 数据操作(告警快照)
+│   ├── mysql/              #   MySQL 数据操作(FWH Pro 只读)
+│   ├── api/                #   外部 API 调用
+│   │   ├── fwh_pro/        #     FWH Pro 平台 API
+│   │   ├── smartHub/       #     SmartHub SIM 卡 API
+│   │   └── acceleronix/    #     Acceleronix SIM 卡 API
+│   ├── mqtt/               #   MQTT 通信
+│   │   └── fwh_mqtt/       #     FWH MQTT 客户端
+│   └── events/             #   Xlog 事件管理
+│
+├── domain/                 # 领域对象(业务核心模型)
+│   ├── siteInfo.go         #   站点信息 Domain
+│   ├── batchDownload.go    #   批量下载配置/数据
+│   ├── siteAnalysis.go     #   站点分析配置/数据
+│   ├── autoUpdate.go       #   自动升级配置
+│   ├── simCard.go          #   SIM 卡信息
+│   ├── shutdown.go         #   停机统计
+│   ├── auth.go             #   认证信息
+│   ├── mqttLogTypes.go     #   MQTT 日志类型定义(201/357 等)
+│   └── ...                 #   其他领域对象
+│
+├── models/                 # GORM 数据库模型(Gen 生成)
+│   ├── model_pgsql/        #   PostgreSQL 表模型
+│   ├── query_alarm_snapshot/ #  告警快照 Query 层
+│   ├── query_fwh_pro/      #   FWH Pro MySQL Query 层
+│   └── fwh_pro/            #   FWH Pro 模型定义
+│
+├── dto/                    # 数据传输对象(API 请求/响应)
+│   ├── siteInfo.go         #   站点信息 DTO
+│   ├── batchDownload.go    #   批量下载 DTO
+│   ├── simCard.go          #   SIM 卡 DTO
+│   ├── powerFlow.go        #   功率流 DTO
+│   ├── common.go           #   通用 DTO(ResPage 等)
+│   └── ...
+│
+├── task/                   # 异步任务系统
+│   ├── task.go             #   任务系统入口 InitTask()
+│   ├── core/               #   TaskCore 核心封装
+│   ├── plugin/             #   SchedulerManager + AggregatorManager
+│   └── queue/              #   业务队列
+│       ├── fwhQueue/       #     FWH 主任务队列(Redis Stream)
+│       └── simCardQueue/   #     SIM 卡采集队列(Asynq)
+│
+├── scheduler/              # 定时任务(robfig/cron)
+│   └── scheduler.go        #   Cron 初始化 + 任务注册
+│
+├── jobs/                   # 后台作业(任务具体执行逻辑)
+│   ├── callFwhFile.go      #   FWH 文件召唤(EMS日志/故障录波)
+│   ├── saveBatchDownloadData.go  # 批量下载数据保存(Excel/图表 → MinIO)
+│   ├── analysisSiteData.go #   站点数据分析(时间线/运行时)
+│   ├── statisticsShutdown.go #  停机统计
+│   ├── statisticsAlarm.go  #   告警统计
+│   ├── getAlarmSnapshot.go #   告警快照获取与解析
+│   └── createAutoUpdateConfigFile.go  # 升级配置文件生成
+│
+├── utils/                  # 工具函数
+│   ├── httpClient/         #   HTTP 客户端封装(自动 reLogin)
+│   ├── mqttClient/         #   MQTT 客户端封装(自动重连)
+│   ├── minio/              #   MinIO 对象存储客户端
+│   ├── snowflakeID/        #   分布式 ID 生成
+│   ├── casdoor/            #   Casdoor 认证集成
+│   ├── files/              #   文件操作(压缩/解压/下载)
+│   ├── mimetype/           #   MIME 类型检测
+│   ├── echart/             #   图表生成封装
+│   ├── emspara/            #   EMS 参数同步
+│   ├── xlog/               #   Xlog 协议解析
+│   ├── networkEvents/      #   Evio 网络事件监听
+│   └── global/             #   全局工具函数(Ternary/Deref)
+│
+├── pkg/                    # 自定义通用包
+├── docs/                   # Swagger 文档(自动生成)
+└── cmd/                    # 脚本与工具
+    ├── gen/                #   GORM Gen 代码生成
+    ├── deploy.sh           #   部署脚本
+    ├── only_build.sh       #   仅编译脚本
+    ├── showLog.sh          #   日志查看
+    ├── showTodayLog.sh     #   今日日志
+    └── showTodayErrorLog.sh #  今日错误日志
+```
+
+---
+
+## 4. 核心依赖
+
+### 4.1 主要依赖版本表
+
+| 依赖包 | 版本 | 用途 |
+|--------|------|------|
+| `go` | 1.25.0 | 编程语言 |
+| `gofiber/fiber/v2` | v2.52.9 | 高性能 Web 框架 |
+| `gorm.io/gorm` | v1.30.1 | ORM 框架 |
+| `gorm.io/driver/mysql` | v1.6.0 | MySQL 驱动 |
+| `gorm.io/driver/postgres` | v1.6.0 | PostgreSQL 驱动 |
+| `gorm.io/gen` | v0.3.27 | GORM 代码生成工具 |
+| `go.mongodb.org/mongo-driver` | v1.17.4 | MongoDB 官方驱动 |
+| `redis/go-redis/v9` | v9.21.0 | Redis 客户端 |
+| `spf13/viper` | v1.20.1 | 配置管理 |
+| `go.uber.org/zap` | v1.27.0 | 结构化日志 |
+| `robfig/cron/v3` | v3.0.1 | 定时任务 |
+| `hibiken/asynq` | v0.26.0 | 异步任务队列 |
+| `hibiken/asynqmon` | v0.7.2 | 任务队列监控 UI |
+| `eclipse/paho.mqtt.golang` | v1.5.1 | MQTT 客户端 |
+| `minio/minio-go/v7` | v7.0.95 | 对象存储客户端 |
+| `casdoor/casdoor-go-sdk` | v1.21.0 | SSO/鉴权 SDK |
+| `gofiber/swagger` | v1.1.1 | Swagger UI 集成 |
+| `swaggo/swag` | v1.16.4 | Swagger 文档生成 |
+| `xuri/excelize/v2` | v2.10.0 | Excel 文件读写 |
+| `tidwall/evio` | v1.0.8 | 高性能事件网络库 |
+| `go-echarts/go-echarts/v2` | v2.6.6 | 图表生成(功率流图) |
+| `bwmarrin/snowflake` | v0.3.0 | 分布式 ID 生成 |
+| `tiendc/go-deepcopy` | v1.7.1 | 结构体深拷贝 |
+
+### 4.2 依赖选型说明
+
+- **Fiber** 而非 Gin:Fiber 基于 fasthttp,性能更高,适合高并发场景
+- **GORM + Gen**:自动生成类型安全的 Query 代码,减少运行时错误
+- **Asynq**:成熟的 Redis 任务队列方案,自带监控面板和定时调度
+- **MongoDB + PostgreSQL 混合**:文档型数据用 MongoDB,结构化数据用 PostgreSQL
+- **Casdoor**:开源 SSO 平台,支持 OAuth2/OIDC,便于统一认证管理
+
+---
+
+## 5. 配置管理
+
+### 5.1 配置加载机制
+
+```
+配置来源优先级(高 → 低):
+
+环境变量 (os.Getenv)
+    ↑ AutomaticEnv() 自动覆盖
+./.env 文件
+    ↑ 未找到则尝试
+./config.env 文件
+    ↑ 未找到则使用
+缺省默认值
+```
+
+### 5.2 配置结构体
+
+```go
+type Config struct {
+    App      AppConfig      // 应用基础设施配置
+    FwhApi   FwhApiConfig   // FWH 平台 API 配置
+    FwhMqtt  FwhMqttConfig  // MQTT Broker 配置
+    FwhMqttTest FwhMqttConfig // MQTT 测试环境配置
+}
+```
+
+#### AppConfig — 基础设施
+
+| 字段 | 环境变量 | 说明 | 默认值 |
+|------|----------|------|--------|
+| AppEnv | `APP_ENV` | 运行环境 (dev/pro) | dev |
+| AppPort | `APP_PORT` | HTTP 监听端口 | 8301 |
+| RedisAddr | `REDIS_ADDR` | Redis 地址 | localhost:6379 |
+| MySQLDSN | `MYSQL_DSN` | MySQL 连接串 | — |
+| PostgresDSN | `POSTGRES_DSN` | PostgreSQL 连接串 | — |
+| MongoDSN | `MONGO_DSN` | MongoDB 连接串 | — |
+| MinIoEndpoint | `MINIO_ENDPOINT` | MinIO 端点 | localhost:9198 |
+| MinioAccessKey | `MINIO_ACCESS_KEY` | MinIO 访问密钥 | — |
+| MinioSecretKey | `MINIO_SECRET_KEY` | MinIO 密钥 | — |
+| MinioBucketName | `MINIO_BUCKET_NAME` | MinIO 存储桶 | — |
+| AsynqmonPort | `ASYNQMON_PORT` | Asynqmon 监控端口 | 8081 |
+
+#### FwhApiConfig — FWH 平台 API
+
+| 字段 | 说明 |
+|------|------|
+| FwhEnergy | FWH 能源管理 API 域名 |
+| FwhOss | FWH OSS 文件服务域名 |
+| FwhUser | 登录用户名 |
+| FwhPassword | 登录密码(MD5 加密存储) |
+
+#### FwhMqttConfig — MQTT 配置
+
+| 字段 | 说明 |
+|------|------|
+| Broker | MQTT Broker 地址 |
+| Ca | CA 证书路径 |
+| Cert | 客户端证书路径 |
+| Key | 客户端私钥路径 |
+
+### 5.3 环境区分
+
+| 特性 | dev(开发) | pro(生产) |
+|------|------------|------------|
+| 日志级别 | Debug | Warn |
+| 日志格式 | Console(可读) | JSON(结构化) |
+| 日志采样 | 无 | 100/100 |
+| 堆栈追踪 | Error 级别 | Error 级别 |
+
+---
+
+## 6. 路由与API
+
+### 6.1 路由架构
+
+```
+Fiber App
+├── /swagger/*                          → Swagger 文档 UI
+├── /ping                               → 健康检查
+├── /auth/*                             → 认证(公开,无需 Token)
+│   ├── POST /auth/login                → OAuth Code 登录
+│   ├── GET  /auth/session              → 获取会话信息
+│   ├── POST /auth/refreshToken         → 刷新 Token
+│   └── POST /auth/logout               → 登出
+│
+└── /api/*  [AuthMiddleware]            → 受保护路由(需 JWT)
+    ├── /api/userInfo/*                 → 用户配置
+    ├── /api/shutdownStatistics/*       → 停机统计
+    ├── /api/shutdownManage/*           → 停机管理
+    ├── /api/sitesMonitor/*             → 站点监控
+    ├── /api/sitesAnalysis/*            → 站点分析
+    ├── /api/batchDownload/*            → 批量下载
+    ├── /api/backgroundTask/*           → 后台任务
+    ├── /api/autoUpdateConfig/*         → 自动升级配置
+    ├── /api/xlog/*                     → 设备日志
+    ├── /api/siteInfoManage/*           → 站点信息管理
+    ├── /api/despatchConfig/*           → 调度配置
+    ├── /api/faultRecord/*              → 故障录波
+    ├── /api/simCardManage/*            → SIM 卡管理
+    └── WebSocket                       → 全局 WebSocket
+
+独立服务:
+└── Asynqmon (:8081)                    → 异步任务监控面板
+```
+
+### 6.2 完整 API 接口清单
+
+#### 认证模块 (`/auth`)
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | `/auth/login` | Casdoor OAuth Code 换 Token 登录 |
+| GET | `/auth/session` | 获取当前会话信息 |
+| POST | `/auth/refreshToken` | 刷新 Access Token |
+| POST | `/auth/logout` | 登出(清除 Session) |
+
+#### 批量下载 (`/api/batchDownload`)
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/batchDownload/manage/list` | 分页查询下载任务列表 |
+| POST | `/batchDownload/manage/config` | 创建批量下载任务 |
+| POST | `/batchDownload/manage/description/:id` | 更新任务描述 |
+| POST | `/batchDownload/manage/owner/:id` | 更新任务归属 |
+| DELETE | `/batchDownload/manage/config/:id` | 删除下载任务 |
+| GET | `/batchDownload/data/:id` | 流式 ZIP 下载任务数据 |
+| GET | `/batchDownload/manage/path` | 获取目录树结构 |
+| GET | `/batchDownload/manage/fileTree` | 获取 tar.gz 内部目录 |
+| GET | `/batchDownload/manage/fileDownload` | 单文件下载 |
+
+#### 自动升级配置 (`/api/autoUpdateConfig`)
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/autoUpdateConfig/option` | 获取配置选项 |
+| GET | `/autoUpdateConfig/packageInfo` | 获取升级包信息列表 |
+| POST | `/autoUpdateConfig/packageInfo/create/:type` | 创建升级包 |
+| POST | `/autoUpdateConfig/packageInfo/update/:id` | 更新升级包 |
+| DELETE | `/autoUpdateConfig/packageInfo/delete/:type/:id` | 删除升级包 |
+| GET | `/autoUpdateConfig/aPowerVersion` | 获取 aPower 版本列表 |
+| POST | `/autoUpdateConfig/aPowerVersion/:version` | 更新 aPower 版本 |
+| DELETE | `/autoUpdateConfig/aPowerVersion/:version` | 删除 aPower 版本 |
+| GET | `/autoUpdateConfig/config` | 获取升级配置 |
+| POST | `/autoUpdateConfig/config/:aGate` | 更新升级配置 |
+| DELETE | `/autoUpdateConfig/config/:aGate` | 删除升级配置 |
+| POST | `/autoUpdateConfig/config/special/:aGate` | 创建特殊设备配置 |
+| DELETE | `/autoUpdateConfig/config/special/:aGate/:id` | 删除特殊设备配置 |
+| GET | `/autoUpdateConfig/configFile` | 获取配置文件信息 |
+| POST | `/autoUpdateConfig/configFile/create` | 生成配置文件(Excel) |
+| GET | `/autoUpdateConfig/configFile/download/:fileName` | 下载配置文件 |
+
+#### 站点信息管理 (`/api/siteInfoManage`)
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/siteInfoManage/manage/update` | 获取站点信息更新状态 |
+| POST | `/siteInfoManage/manage/update` | 触发全量同步 |
+| POST | `/siteInfoManage/dataList` | 分页查询站点列表 |
+| POST | `/siteInfoManage/dataListExport` | 导出站点 Excel |
+| GET | `/siteInfoManage/siteInfo/query?sn=` | 按 SN 查询站点详情 |
+| POST | `/siteInfoManage/operate/enableXlog` | 使能站点 Xlog |
+
+#### 站点监控 (`/api/sitesMonitor`)
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/sitesMonitor/manage/nameList` | 获取监控配置名列表 |
+| GET | `/sitesMonitor/manage/config/:name` | 获取指定监控配置 |
+| POST | `/sitesMonitor/manage/config/:name` | 更新监控配置 |
+| POST | `/sitesMonitor/manage/pageConfig/:name` | 更新页面配置 |
+| DELETE | `/sitesMonitor/manage/config/:name` | 删除监控配置 |
+| GET | `/sitesMonitor/manage/config` | 获取所有监控配置 |
+| POST | `/sitesMonitor/manage/config` | 创建监控配置 |
+| POST | `/sitesMonitor/site/infoList` | 批量查询站点信息 |
+| GET | `/sitesMonitor/site/:site/info` | 单站点详细信息 |
+| GET | `/sitesMonitor/site/:site/modeInfo` | 站点工作模式 |
+| GET | `/sitesMonitor/site/:site/powerFlow/:day` | 站点功率流(按天) |
+| GET | `/sitesMonitor/site/:site/alarmList` | 站点告警列表 |
+
+#### 站点分析 (`/api/sitesAnalysis`)
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/sitesAnalysis/manage/analysisList` | 分析任务列表 |
+| POST | `/sitesAnalysis/manage/config` | 创建分析任务 |
+| GET | `/sitesAnalysis/manage/config/:id` | 获取分析配置 |
+| POST | `/sitesAnalysis/manage/config/:id` | 更新分析配置 |
+| POST | `/sitesAnalysis/manage/description/:id` | 更新描述 |
+| DELETE | `/sitesAnalysis/manage/config/:id` | 删除分析任务 |
+| GET | `/sitesAnalysis/data/:id` | 获取分析结果数据 |
+
+#### 其他模块
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/simCardManage/info` | SIM 卡批量信息 |
+| GET | `/simCardManage/monthly` | SIM 卡月度数据 |
+| GET | `/shutdownStatistics/date` | 停机统计日期 |
+| GET | `/shutdownStatistics` | 停机统计数据 |
+| GET | `/shutdownManage/date` | 停机管理日期 |
+| GET | `/shutdownManage/infoMap` | 停机详情映射 |
+| GET | `/alarmManage/infoMap` | 告警统计映射 |
+| POST | `/faultRecord/paramSetting` | 故障录波参数设置 |
+| GET | `/despatchConfig/touTemplatePro/list` | TOU 模板列表 |
+| GET | `/despatchConfig/touTemplatePro/info` | TOU 模板详情 |
+| GET | `/userInfo/config` | 获取用户配置 |
+| POST | `/userInfo/config` | 更新用户配置 |
+| GET | `/backgroundTask/info` | 后台任务列表 |
+| GET | `/xlog/manage/siteList` | Xlog 站点列表 |
+| POST | `/xlog/terminal/:uuid` | 发送终端命令 |
+
+---
+
+## 7. 业务模块
+
+### 7.1 模块总览
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│                    FWH ToolBox 业务模块                       │
+├─────────────────┬───────────────────┬───────────────────────┤
+│   数据采集类     │    运维管理类      │     监控分析类         │
+├─────────────────┼───────────────────┼───────────────────────┤
+│ • 批量数据下载   │ • 自动升级配置     │ • 站点监控            │
+│ • 站点信息同步   │ • 故障录波         │ • 站点分析            │
+│ • SIM卡信息采集  │ • 调度配置         │ • 停机统计            │
+│                 │ • Xlog 管理        │ • 告警统计            │
+│                 │ • 用户配置         │                       │
+└─────────────────┴───────────────────┴───────────────────────┘
+```
+
+### 7.2 批量数据下载 (BatchDownload)
+
+**功能描述**:批量抓取指定站点在指定时间段内的各类运行数据,异步处理后打包为 ZIP 文件供下载。
+
+**数据类型支持**:
+- 功率流图(PowerFlow)— 生成 Excel + ECharts 图表
+- MQTT 日志(类型 0/101/201/357)
+- 事件与告警日志(EventAlarm)
+- 告警快照(AlarmSnapshot)
+- EMS 日志(召唤式下载)
+- 故障录波数据(FaultRecord)
+- 系统升级记录(UpdateLog)
+
+**处理流程**:
+```
+用户创建任务 → Service 写入 MongoDB(状态: pending)
+    → 入队 FWH 任务队列(Redis Stream)
+    → Worker 消费任务(状态: running)
+    → 按 SN 遍历:
+        → 按数据类型调用 FWH API 获取数据
+        → Jobs 层处理(生成 Excel/图表)
+        → 存入 MinIO 对象存储
+    → 全部完成(状态: complete)/ 部分失败(状态: failed)
+
+用户下载 → 流式读取 MinIO 文件 → ZIP 打包 → HTTP Stream 响应
+```
+
+**关键技术**:
+- 流式 ZIP 打包(`SetBodyStreamWriter`),避免内存溢出
+- MinIO 对象存储,支持大文件
+- 文件召唤机制(EMS 日志/故障录波需要先召唤再下载)
+- 任务进度实时跟踪
+
+### 7.3 自动升级配置 (AutoUpdateConfig)
+
+**功能描述**:管理 aGate/aPower 设备的 OTA 固件升级策略。
+
+**核心概念**:
+- **升级包 (PackageInfo)**:固件文件元信息(版本号、硬件兼容性、文件路径)
+- **aPower 版本**:aPower 设备可用版本列表
+- **升级配置 (Config)**:按 aGate 硬件类型配置默认升级策略和特殊设备策略
+- **配置文件**:将配置导出为 Excel 并存储到 MinIO
+
+**数据存储**:MongoDB(配置数据) + MinIO(配置文件) + Redis(文件列表缓存)
+
+### 7.4 站点信息管理 (SiteInfoManage)
+
+**功能描述**:从 FWH Pro 平台全量同步站点信息到本地 MongoDB,提供多维度查询和 Excel 导出。
+
+**核心流程**:
+```
+触发全量同步 → 入队任务 → 分页调用 FWH API
+    → 逐条写入/更新 MongoDB(SiteInfoDomain)
+    → 完成后通知前端
+
+查询站点 → MongoDB 聚合查询(支持多种过滤器类型)
+    → Domain → DTO 转换(标签ID展开为详情)
+    → 分页返回
+
+导出 Excel → 查询全部匹配数据 → excelize 生成工作表 → 流式响应
+```
+
+**筛选器支持**:等于(整数/浮点/字符串)、包含、范围(整数/浮点/时间)、字段存在
+
+### 7.5 站点监控 (SitesMonitor)
+
+**功能描述**:配置监控面板,实时查询站点功率流、告警、工作模式。
+
+**特性**:
+- 多面板管理(每个面板配置独立站点列表和页面布局)
+- 实时数据通过 FWH Pro API 获取(功率流、告警列表、TOU 模式)
+- 站点信息展示(设备状态、固件版本、网络类型)
+
+### 7.6 站点分析 (SitesAnalysis)
+
+**功能描述**:分析站点历史 MQTT 日志,生成时间线事件和运行时数据。
+
+**分析数据源**:
+- MQTT 日志类型 0 — 离线事件
+- MQTT 日志类型 101 — 上线事件
+- MQTT 日志类型 201 — 运行数据(功率、SOC、模式等)
+- MQTT 日志类型 357 — 模式切换事件
+- 事件日志 / 告警日志
+
+**输出结果**:
+- **时间线** (`Timeline[]`):离线、上线、告警、模式切换等事件序列
+- **运行时数据** (`Runtime[]`):各时段运行状态数据
+
+### 7.7 停机统计 (ShutdownStatistics)
+
+**功能描述**:统计每日停机站点,生成按区域(ALL/US/AU)的周报和年报。
+
+**统计维度**:
+- 日统计:当日停机数 + 增长率
+- 周统计:最近 7 天趋势
+- 月统计:当月累计
+- 年统计:12 个月趋势
+
+**触发方式**:定时任务(每日执行)+ 手动触发
+
+### 7.8 SIM 卡管理 (SimCardManage)
+
+**功能描述**:定时从第三方平台采集 SIM 卡状态和流量数据。
+
+**数据源**:
+- SmartHub 平台 — 一类 SIM 卡
+- Acceleronix 平台 — 另一类 SIM 卡
+
+**采集模式**:
+```
+定时调度(每天 UTC 23:00)→ 创建首页任务
+    → 链式分页查询(Page 0 → Page 1 → ... → 最后一页)
+    → 聚合器收集多页数据
+    → 批量写入 MongoDB
+```
+
+### 7.9 故障录波 (FaultRecord)
+
+**功能描述**:通过 MQTT 协议向设备下发故障录波参数设置指令。
+
+**通信流程**:
+```
+API 请求 → Service → MQTT 发送 cmdType=377(参数设置)
+    → 等待设备回复 cmdType=378
+    → 返回结果
+```
+
+### 7.10 其他模块
+
+| 模块 | 说明 |
+|------|------|
+| **调度配置 (DespatchConfig)** | 代理查询 FWH Pro 的 TOU 调度模板列表和详情 |
+| **用户配置 (UserInfo)** | 管理用户个性化设置(表格列过滤器、自定义查询条件) |
+| **后台任务 (BackgroundTask)** | 查询 Redis Stream 中后台任务执行状态 |
+| **Xlog 管理** | 管理站点 Xlog 连接,通过 WebSocket 转发终端命令 |
+| **全局 WebSocket** | 维护 WebSocket 连接池,支持心跳检测和消息广播 |
+
+---
+
+## 8. 数据模型
+
+### 8.1 三层数据对象体系
+
+本项目采用三层数据对象设计,各层职责明确:
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│                      DTO 层 (dto/)                           │
+│  面向 API 接口,定义请求/响应结构                              │
+│  标签: json    ID类型: string (Hex)                          │
+└─────────────────────────────┬───────────────────────────────┘
+                              │ 转换函数
+┌─────────────────────────────▼───────────────────────────────┐
+│                    Domain 层 (domain/)                        │
+│  业务核心对象,内部逻辑处理和 MongoDB 存储                      │
+│  标签: bson    ID类型: primitive.ObjectID                     │
+└─────────────────────────────┬───────────────────────────────┘
+                              │
+┌─────────────────────────────▼───────────────────────────────┐
+│                    Model 层 (models/)                         │
+│  GORM 模型,映射 PostgreSQL/MySQL 表结构                      │
+│  标签: gorm    ID类型: int64/string                          │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 8.2 数据流转关系
+
+```
+外部数据源                    内部存储                    API 响应
+────────────                ────────                  ────────
+FWH Pro API                                           
+  (FwhSiteInfo)     →    domain.SiteInfoDomain    →   dto.SiteInfoDTO
+                          (MongoDB 存储)               (标签展开为详情)
+
+FWH Pro MySQL                                         
+  (IotGateway)      →    domain.FwhSiteInfo       →   dto.MonitorSiteInfo
+                          (API 响应映射)               (精简字段)
+
+SmartHub API                                          
+  (SimCardResponse) →    domain.SimCardInfo       →   dto.SimCardInfo
+                          (MongoDB 存储)               (增加关联SN)
+```
+
+### 8.3 核心 Domain 对象
+
+#### 任务状态枚举
+
+```go
+type Progress string  // pending | running | failed | error | complete
+type Trigger  string  // auto | manual
+```
+
+#### SiteInfoDomain — 站点信息(50+ 字段)
+
+```go
+type SiteInfoDomain struct {
+    Sn            string   `bson:"_id"`    // aGate 序列号(主键)
+    Name          *string                   // 站点名称
+    ActiveStatus  int                       // 0-未激活 1-已激活
+    Status        int                       // 0-离线 1-在线
+    RunStatus     int                       // 0-待机 1-充电 2-放电 3-故障
+    EmsModel      *int                      // EMS 调度模式
+    Version       string                    // 固件版本
+    FhpSn         string                    // aPower SN(逗号分割)
+    ConnType      int                       // 网络类型
+    TimeZone      float32                   // 时区
+    Country       string                    // 国家
+    ExtTags       *[]string                 // 扩展标签 ID 列表
+    // ... 更多字段
+}
+```
+
+#### BatchDownloadConfig — 批量下载任务
+
+```go
+type BatchDownloadConfig struct {
+    Id          primitive.ObjectID
+    Sn          []string                         // 目标 SN 列表
+    TimePeriod  [2]string                        // 时间范围 [开始, 结束]
+    Options     BatchDownloadOptions             // 下载选项
+    Progress    Progress                         // 任务进度
+    Data        map[string]BatchDownloadData     // SN → 数据映射
+    FailReasons []string                         // 失败原因
+}
+
+type BatchDownloadOptions struct {
+    PowerFlow     bool       // 功率流图
+    MqttLogs      []string   // MQTT 日志类型列表
+    EventAlarm    bool       // 事件告警
+    AlarmSnapshot bool       // 告警快照
+    EmsLogs       []string   // EMS 日志类型
+    FaultRecord   bool       // 故障录波
+    UpdateLog     bool       // 升级记录
+    Settings      bool       // 站点设置
+}
+```
+
+#### SiteAnalysisConfig — 站点分析
+
+```go
+type SiteAnalysisConfig struct {
+    Id         primitive.ObjectID
+    Sn         string
+    TimePeriod [2]string
+    Options    SiteAnalysisOptions
+    Progress   Progress
+    Trigger    Trigger              // auto(定时触发) / manual(手动)
+    Data       SiteAnalysisData
+}
+
+type SiteAnalysisData struct {
+    Timeline []TimelineItem       // 时间线事件
+    Runtime  []RuntimeItem        // 运行时数据
+}
+
+type TimelineType string  // offline | online | mode_change | event | alarm
+```
+
+#### ShutdownStatisticsItem — 停机统计
+
+```go
+type ShutdownStatisticsItem struct {
+    Date    string
+    Status  ShutdownStatisticsStatus
+    Count   ShutdownStatisticsArea[uint]    // 泛型按区域统计
+    SiteMap map[string]ShutdownStatisticsInfo
+}
+
+type ShutdownStatisticsArea[T any] struct {
+    ALL T
+    US  T
+    AU  T
+}
+```
+
+### 8.4 GORM 模型(PostgreSQL)
+
+#### 告警快照文件记录
+
+```go
+// 表: alarm_snapshot.site_alarm_snapshot_file_record
+type SiteAlarmSnapshotFileRecord struct {
+    ID           int64     `gorm:"primaryKey;autoIncrement"`
+    CodeType     int32
+    FileName     string    `gorm:"not null"`
+    FileURL      string    `gorm:"not null"`
+    Sn           string    `gorm:"not null"`
+    LogTime      time.Time
+    State        int16     // 0-分析中 1-完成 -1-失败
+}
+```
+
+#### 告警快照数据
+
+```go
+// 表: alarm_snapshot.site_alarm_snapshot_data
+type SiteAlarmSnapshotData struct {
+    ID           int64     `gorm:"primaryKey;autoIncrement"`
+    Sn           string    `gorm:"not null"`
+    FileRecordID int64     // 外键关联 FileRecord
+    LogTime      time.Time
+    JSONData     string    `gorm:"type:jsonb"`  // JSON 格式数据
+    ZoneInfo     string
+}
+```
+
+#### 原始数据存储
+
+```go
+// 表: original.site_original_data
+type SiteOriginalDataModel struct {
+    ID           int64          `gorm:"primaryKey"` // 雪花 ID
+    DataType     DataTypes      // "API" 或 "MQTT"
+    ChildType    string         // API 路径名 或 MQTT cmdType
+    SN           string
+    DataTime     time.Time
+    JsonData     datatypes.JSON `gorm:"type:jsonb"`
+}
+```
+
+### 8.5 GORM 模型(MySQL - 只读)
+
+```go
+// 表: iot_gateway (FWH Pro 生产库)
+type IotGateway struct {
+    ID            string  `gorm:"primaryKey"` // 网关 SN
+    Name          string
+    Version       string                      // 固件版本
+    ActiveStatus  int32                       // 激活状态
+    Status        int32                       // 在线状态
+    RunStatus     int32                       // 运行状态
+    EmsModel      int32                       // EMS 模式
+    FhpSn         string                      // aPower 编号
+    TimeZone      float32
+    Country       string
+    // ... 60+ 字段,完整映射生产数据库
+}
+```
+
+### 8.6 核心 DTO
+
+#### 通用分页响应
+
+```go
+type ResPage[T any] struct {
+    Data     []T  `json:"data"`
+    PageSize uint `json:"pageSize"`
+    PageNum  uint `json:"pageNum"`
+    Total    uint `json:"total"`
+}
+```
+
+#### 站点信息 DTO
+
+```go
+type SiteInfoDTO struct {
+    // 与 Domain 基本一致,区别:
+    // - json 标签(非 bson)
+    // - ExtTags 展开为 []SiteTagDTO(含名称、颜色等详情)
+    ExtTags []SiteTagDTO `json:"extTags,omitempty"`
+}
+
+type SiteInfoRequest struct {
+    Search   string                 `json:"search"`
+    Filter   *domain.SiteInfoFilter `json:"filter"`
+    PageSize uint                   `json:"pageSize"`
+    PageNum  uint                   `json:"pageNum"`
+}
+```
+
+#### 功率流 DTO
+
+```go
+type SitePowerFlow struct {
+    Index           uint
+    DeviceTimeArray []string    // 时间轴
+    PowerFhpArray   []float32  // aPower 功率
+    PowerGenArray   []float32  // 发电功率
+    PowerLoadArray  []float32  // 负载功率
+    PowerSunArray   []float32  // 光伏功率
+    PowerUtiArray   []float32  // 电网功率
+    SocArray        []float32  // SOC 曲线
+}
+```
+
+### 8.7 数据库分布
+
+| 数据库 | 访问模式 | 存储内容 |
+|--------|----------|----------|
+| **MongoDB** | 读写 | 站点信息、SIM卡、批量下载任务/配置、站点分析、升级配置、用户配置、监控配置、统计数据、标签 |
+| **PostgreSQL** | 读写 | 告警快照(文件记录+数据)、原始数据存储(API/MQTT 日志) |
+| **MySQL** | 只读 | FWH Pro 生产库 iot_gateway 表(站点基础信息) |
+| **Redis** | 读写 | 会话、Dashboard 缓存、任务队列、分布式锁、EMS 参数缓存 |
+| **MinIO** | 读写 | 批量下载文件、升级配置 Excel 文件 |
+
+---
+
+## 9. 基础设施
+
+### 9.1 数据库层 (database/)
+
+#### 初始化流程
+
+```go
+database.Init(cfg *config.AppConfig)
+    ├── InitPostgreSQL(cfg.PostgresDSN)  // GORM + postgres driver
+    ├── InitMongo(cfg.MongoDSN)          // mongo-driver
+    └── InitMySQL(cfg.MySQLDSN)          // 当前已注释
+```
+
+#### PostgreSQL 配置
+
+| 配置项 | 值 |
+|--------|------|
+| 驱动 | `gorm.io/driver/postgres` |
+| 日志 | 自定义 `GormZapLogger`(桥接 Zap) |
+| 慢查询阈值 | 200ms |
+| 代码生成 | GORM Gen(`query_alarm_snapshot`) |
+
+#### MongoDB 配置
+
+| 配置项 | 值 |
+|--------|------|
+| 驱动 | `go.mongodb.org/mongo-driver` |
+| 连接超时 | 10s |
+| 数据库名 | 从 URI 末段解析 |
+| 对外接口 | `GetCollection(collName)` |
+
+#### 自定义 GORM Logger
+
+将 GORM 的 SQL 日志、慢查询告警、错误信息全部桥接到 Zap 结构化日志:
+- Info 级别:普通 SQL
+- Warn 级别:慢查询(>200ms)
+- Error 级别:数据库错误
+
+### 9.2 缓存层 (cache/)
+
+#### 架构分层
+
+```
+上层 API (cache/)
+    │  统一暴露:Set/Get/HSet/LPush/ZAdd/XAdd/TryLock...
+    │  高级模式:GetOrSet/GetOrSetMany/GetOrSetStale
+    ▼
+底层封装 (cache/redisx/)
+    │  按数据结构分文件:stringx/hashx/listx/setx/zsetx/streamx
+    │  特殊能力:jsonx(泛型序列化)/lockx(分布式锁)/scriptx(Lua)
+    ▼
+Redis Client (redis.UniversalClient)
+    │  支持单机/哨兵/集群模式
+    ▼
+Redis Server (DB=12)
+```
+
+#### Cache-Aside 模式
+
+```go
+// 单键缓存(防击穿 + 防雪崩)
+result, err := cache.GetOrSet[T](ctx, key, loaderFunc, opts...)
+
+// 执行流程:
+// 1. GET key → 命中则返回
+// 2. 未命中 → 尝试获取分布式锁
+// 3. 获取锁成功 → double check → 调用 loader → 写缓存
+// 4. 获取锁失败 → 等待缓存更新 or fallback 直接调用 loader
+
+// 批量模式
+results, err := cache.GetOrSetMany[T](ctx, keys, batchLoaderFunc, opts...)
+```
+
+#### Stale 模式(异步刷新)
+
+```go
+// TTL 即将过期时返回旧值 + 后台刷新
+result, err := cache.GetOrSetStale[T](ctx, key, loaderFunc, opts...)
+// stale 阈值默认 30s,即 TTL < 30s 时触发后台刷新
+```
+
+#### 配置选项
+
+| 选项 | 默认值 | 说明 |
+|------|--------|------|
+| TTL | 5min | 缓存过期时间 |
+| AsyncWrite | true | 异步写入缓存 |
+| LockEnabled | true | 是否启用分布式锁 |
+| LockTTL | 5s | 锁过期时间 |
+| WaitCacheTimeout | 500ms | 等待缓存更新超时 |
+| StaleThreshold | 30s | Stale 刷新阈值 |
+
+#### 分布式锁
+
+基于 `SET key value NX PX timeout` + Lua 脚本安全释放:
+
+```go
+lock, ok, err := cache.TryLock(ctx, key, ttl)  // 尝试获取
+cache.LockRelease(ctx, key, token)              // 安全释放(Lua 校验 token)
+```
+
+### 9.3 中间件 (middleware/)
+
+#### JWT 认证中间件
+
+```go
+func AuthMiddleware(requiredRoles ...string) fiber.Handler
+```
+
+**处理流程**:
+```
+请求 → 提取 Authorization: Bearer <token>
+    → casdoorsdk.ParseJwtToken(token) 验证签名
+    → 提取用户信息 → 存入 c.Locals("user")
+    → (可选) 检查角色权限
+    → 通过 / 返回 401
+```
+
+**用户信息结构**:
+
+```go
+type AuthUserInfo struct {
+    Owner       string
+    Name        string         // 用户名
+    DisplayName string         // 显示名
+    Id          string
+    IsAdmin     bool
+    Roles       []RoleInfo     // 角色列表
+    Permissions []PermInfo     // 权限列表(含 Actions + Effect)
+    Groups      []string       // 用户组
+}
+```
+
+**辅助函数**:
+- `AuthGetUserInfo(c)` — 获取完整用户信息
+- `AuthGetUserName(c)` — 获取用户名
+- `AuthIsAdmin(c)` — 判断管理员
+- `AuthGetRoleNameList(c)` — 角色名列表
+- `AuthGetPermissionNameList(c)` — 权限名列表
+
+### 9.4 日志系统 (logger/)
+
+#### 初始化
+
+```go
+logger.Init(env string)
+// 全局变量:
+//   logger.Log   *zap.Logger         — 结构化日志
+//   logger.Sugar *zap.SugaredLogger  — Sugar 风格日志
+//   logger.Std   *log.Logger         — 标准库兼容
+```
+
+#### 环境配置对比
+
+| 配置 | dev | pro |
+|------|-----|-----|
+| 编码器 | Console | JSON |
+| 级别 | Debug | Warn |
+| 采样 | 无 | Initial=100, Thereafter=100 |
+| 堆栈 | Error+ | Error+ |
+| 输出 | stderr | stderr |
+
+#### 全局替换
+
+```go
+zap.ReplaceGlobals(Log)
+// 之后可通过 zap.L() / zap.S() 全局访问
+```
+
+---
+
+## 10. 任务系统
+
+### 10.1 架构概览
+
+系统包含**两套独立的任务队列**,各自服务不同业务场景:
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│                        任务系统架构                               │
+├────────────────────────────┬────────────────────────────────────┤
+│     FWH 队列(自研)        │        SimCard 队列(Asynq)       │
+│   基于 Redis Stream         │      基于 hibiken/asynq            │
+├────────────────────────────┼────────────────────────────────────┤
+│ • 批量数据下载              │ • SIM 卡信息采集                    │
+│ • 站点分析                  │ • 聚合写入                          │
+│ • 站点信息同步              │                                    │
+│ • 停机统计                  │                                    │
+│ • 告警统计                  │                                    │
+├────────────────────────────┼────────────────────────────────────┤
+│ 单 Worker 顺序执行          │ 单线程 + 分组聚合器                 │
+│ 分布式锁保证唯一性          │ 唯一性(5min 内不重复)             │
+│ 自动重试(最大3次)         │ 成功任务保留7天                     │
+└────────────────────────────┴────────────────────────────────────┘
+```
+
+### 10.2 FWH 队列 — Redis Stream 实现
+
+#### 技术方案
+
+基于 Redis Stream + Consumer Group 的自研任务队列:
+
+```
+生产端                          消费端
+──────                        ──────
+EnqueueTask(type, payload)     startTaskQueueWorker()
+    │                              │
+    ▼                              ▼
+Redis XADD  ──────────────→  XREADGROUP (阻塞读取)
+(stream: fwh_task_queue)           │
+                                   ▼
+                            handleTaskQueueMsg()
+                                   │ (按 type 路由)
+                                   ▼
+                            handleTaskBatchDownload()
+                            handleTaskSiteAnalysis()
+                            handleTaskSiteInfoUpdate()
+                            handleTaskShutdownStatistics()
+                            handleTaskAlarmStatistics()
+```
+
+#### 设计特点
+
+| 特性 | 说明 |
+|------|------|
+| 单 Worker | 顺序执行,避免任务冲突 |
+| 唯一任务锁 | `EnqueueUniqueTask` 通过 Redis 分布式锁保证同类任务不重复 |
+| 自动重试 | 最大 3 次,通过 `XAutoClaim` 检测滞留消息 |
+| 状态管理 | pending → running → complete/failed/error |
+| 进度追踪 | MongoDB 记录 Progress 字段 |
+
+#### 任务类型
+
+| 常量 | 处理函数 | 描述 |
+|------|----------|------|
+| `task_BatchDownload` | `handleTaskBatchDownload` | 批量数据下载 |
+| `task_SiteAnalysis` | `handleTaskSiteAnalysis` | 站点数据分析 |
+| `task_SiteInfoUpdate` | `handleTaskSiteInfoUpdate` | 全量同步站点信息 |
+| `task_ShutdownStatistics` | `handleTaskShutdownStatistics` | 停机统计 |
+| `task_AlarmStatistics` | `handleTaskAlarmStatistics` | 告警统计 |
+
+#### 任务状态机
+
+```
+┌─────────┐    入队     ┌─────────┐    Worker消费    ┌─────────┐
+│  创建   │ ─────────→ │ Pending │ ──────────────→ │ Running │
+└─────────┘            └─────────┘                 └────┬────┘
+                                                        │
+                              ┌──────────────────────────┼──────────────────┐
+                              ▼                          ▼                  ▼
+                       ┌──────────┐              ┌──────────┐       ┌──────────┐
+                       │ Complete │              │  Failed  │       │  Error   │
+                       │ (全部成功) │             │ (部分失败) │      │ (异常)   │
+                       └──────────┘              └──────────┘       └──────────┘
+```
+
+### 10.3 SimCard 队列 — Asynq 实现
+
+#### 技术方案
+
+基于 `hibiken/asynq` 的分布式任务队列:
+
+```
+定时调度 (每天 UTC 23:00)
+    │
+    ▼
+SyncOnePage(page=0) → 处理数据 → 创建聚合任务
+    │                              │
+    ▼                              ▼
+SyncOnePage(page=1) → 处理数据 → 创建聚合任务
+    │                              │
+    ...                            ▼ (聚合器收集)
+    │                         ┌──────────────────┐
+    ▼                         │ AggregatorManager│
+最后一页完成                   │ (30s宽限/5min最大)│
+                              └────────┬─────────┘
+                                       │
+                                       ▼
+                              Bulk Write → MongoDB
+```
+
+#### 配置参数
+
+| 参数 | 值 | 说明 |
+|------|------|------|
+| Concurrency | 1 | 单线程串行,控制 API 调用频率 |
+| GracePeriod | 30s | 聚合宽限期 |
+| MaxDelay | 5min | 最大等待时间 |
+| MaxSize | 100 | 单次聚合最大任务数 |
+| Uniqueness | 5min | 任务唯一性窗口 |
+| Retention | 7d | 成功任务保留时间 |
+
+#### 链式分页模式
+
+```go
+// 处理第 N 页后自动创建第 N+1 页任务
+func handleSyncOnePage(ctx, task) error {
+    data := fetchPage(page)          // 调用外部 API
+    createAggregateTask(data)        // 创建聚合任务
+    if hasNextPage {
+        enqueue(SyncOnePage{page+1}) // 链式创建下一页任务
+    }
+    return nil
+}
+```
+
+### 10.4 定时调度 (Scheduler)
+
+#### 多时区支持
+
+```go
+type SchedulerManager struct {
+    utc *asynq.Scheduler  // UTC
+    zh  *asynq.Scheduler  // Asia/Shanghai
+    sg  *asynq.Scheduler  // Asia/Singapore
+    la  *asynq.Scheduler  // America/Los_Angeles
+    ny  *asynq.Scheduler  // America/New_York
+}
+```
+
+#### 已注册定时任务
+
+| Cron 表达式 | 时区 | 任务 | 状态 |
+|-------------|------|------|------|
+| `0 23 * * *` | UTC | SmartHub SIM 卡同步 | 启用 |
+| `0 23 * * *` | UTC | Acceleronix SIM 卡同步 | 启用 |
+| `0 5 * * *` | — | 告警统计 | 启用 |
+| `35 15 * * *` | — | 停机统计 | 暂停 |
+
+### 10.5 Jobs 层 — 后台作业
+
+Jobs 层包含任务的具体执行逻辑:
+
+#### FWH 文件召唤 (`callFwhFile.go`)
+
+```
+按 SN 分组 → 每个 SN 独立 Redis Stream 队列
+    → 并行下载(EMS 日志需先"召唤"再下载)
+    → 最大召唤重试 3 次
+    → 下载完成写入 MinIO
+```
+
+#### 批量下载数据保存 (`saveBatchDownloadData.go`)
+
+| 数据类型 | 输出格式 | 存储路径 |
+|----------|----------|----------|
+| 功率流 | Excel + ECharts 图 | `BatchDownloadData/{SN}/PowerFlow/{Date}.*` |
+| MQTT 日志 | Excel | `BatchDownloadData/{SN}/MqttLog/{Type}/{Date}.xlsx` |
+| 事件告警 | Excel | `BatchDownloadData/{SN}/EventAlarm/{Date}.xlsx` |
+| 告警快照 | Excel | `BatchDownloadData/{SN}/AlarmSnapshot/{Date}.xlsx` |
+| 升级记录 | Excel | `BatchDownloadData/{SN}/UpdateLog/...` |
+
+#### 站点数据分析 (`analysisSiteData.go`)
+
+- `AnalysisSiteTimeline()` — 生成时间线(离线/上线/告警/模式切换事件)
+- `AnalysisSiteRuntime()` — 运行时数据分析(功率、SOC 趋势)
+
+#### 统计分析
+
+- `StatisticsShutdown()` — 停机统计(按 ALL/US/AU 区域分类)
+- `StatisticsAlarm()` — 告警码统计(触发站点分析回调)
+
+### 10.6 Asynqmon 监控面板
+
+- 独立 HTTP 服务,默认端口 `8081`
+- 提供 Web UI 查看任务队列状态
+- 支持任务重试、删除、归档操作
+- 查看定时任务调度情况
+
+---
+
+## 11. 外部集成
+
+### 11.1 FWH Pro API
+
+#### 概述
+
+FWH Pro 是 FranklinWH 核心业务平台,本系统通过 HTTP API 获取站点数据。
+
+#### 连接配置
+
+| 配置项 | 说明 |
+|--------|------|
+| 域名 | 通过代理 `energy.fwh-toolbox.top` 访问 |
+| OSS 域名 | `energy-oss.fwh-toolbox.top`(文件下载) |
+| 认证 | 用户名 + MD5 密码登录,获取 `Logintoken` |
+| 超时 | 3 分钟 |
+| 重试 | 401 时自动 reLogin 后重试 |
+
+#### HTTP 客户端特性
+
+```go
+// 泛型反序列化
+FwhHttpGetUnmarshal[T](url, params) (*T, error)
+FwhHttpPostUnmarshalPage[T](url, body) (*PageResult[T], error)
+
+// 自动 Token 管理
+// 请求头: Logintoken: <token>
+// 401 响应 → 自动调用 Login() → 重新发送请求
+```
+
+#### API 接口分类
+
+| 分类 | 接口示例 | 说明 |
+|------|----------|------|
+| **站点信息** | `selectIotFirmwareGatewayList` | 分页查询站点基础信息 |
+| | `selectIotGatewayList` | 站点完整信息列表 |
+| | `getGatewayTouListV2` | 站点 TOU 模式信息 |
+| **功率流** | 功率流查询接口 | 按天查询站点功率数据 |
+| **告警** | `QuerySiteAlarmList` | 站点告警列表 |
+| | `QueryAlarmStatistics` | 告警码统计 |
+| | `QueryAlarmSnapshotInfo` | 告警快照文件信息 |
+| **日志** | `QueryMqttLogListOnTimeRange` | MQTT 日志(类型 0/101/201/357) |
+| | `QueryEventOrAlarmListOnTimeRange` | 事件/告警日志 |
+| | `QueryCallEmsLog` | 召唤 EMS 日志 |
+| | `QueryDownloadEmsLogFile` | 下载 EMS 日志文件 |
+| **故障录波** | `QueryFaultRecordList` | 故障录波列表 |
+| | `QueryDownloadFaultRecordData` | 下载录波数据 |
+| **升级** | `QuerySystemUpdateLog` | 系统升级记录 |
+| | `QueryAHubUpdateLog` | aHub 升级记录 |
+| **TOU 模板** | `QueryTouTemplateInfoList` | 调度策略模板列表 |
+| | `QueryTouTemplateDetailInfo` | 模板详情 |
+| **Shutdown** | `QuerySiteShutdownList` | 停机站点列表 |
+
+### 11.2 SIM 卡平台 API
+
+#### SmartHub
+
+```go
+// 批量查询 SIM 卡信息
+smartHub.QueryBatchSimCardInfo(page, size) → []SmartHubSimCardInfo
+```
+
+| 字段 | 说明 |
+|------|------|
+| ICCID | SIM 卡标识 |
+| IMEI/IMSI | 设备标识 |
+| Status | 状态(TEST_READY/ACTIVATE_READY/ACTIVATED/DEACTIVATED/RETIRED) |
+| DataUsage | 流量使用量 |
+
+#### Acceleronix
+
+```go
+// SIM 卡列表
+acceleronix.QuerySimCardList(page, size) → []AcceleronixSimCard
+
+// 批量数据用量
+acceleronix.QueryBatchSimCardDataInfo(ids, month) → []DataUsageInfo
+```
+
+#### 数据统一存储
+
+外部平台数据经转换后统一存储为内部 `domain.SimCardInfo` 格式:
+
+```go
+// 转换函数
+SmartHubSimCardInfoToStorage(info) → domain.SimCardInfo
+AcceleronixSimCardToStorage(info)  → domain.SimCardInfo
+```
+
+### 11.3 MQTT 通信
+
+#### 连接配置
+
+- **Broker**: 通过 TLS 证书认证连接
+- **协议**: MQTT v3.1.1
+- **客户端特性**: 自动重连、断线恢复订阅
+
+#### 通信主题
+
+| 方向 | 主题模式 | 说明 |
+|------|----------|------|
+| 下行 | `fwh/c2s/mng/{sn}` | 服务端 → 设备(管理指令) |
+| 上行 | `fwh/s2c/mng/{sn}` | 设备 → 服务端(响应) |
+
+#### 支持的操作
+
+```go
+// Xlog 使能
+fwh_mqtt.EnableXlog(ctx, sn string) error
+// 主题: fwh/c2s/mng/{sn}
+// 作用: 使能设备 Xlog 功能
+
+// 故障录波参数设置
+fwh_mqtt.FaultRecordParamSetting(ctx, para FaultRecordParamInfo) error
+// 发送: cmdType=377 (参数设置指令)
+// 等待: cmdType=378 (设备确认响应)
+```
+
+### 11.4 MinIO 对象存储
+
+#### 用途
+
+| 场景 | 存储路径 | 文件类型 |
+|------|----------|----------|
+| 批量下载数据 | `BatchDownloadData/{SN}/{Type}/{Date}.*` | Excel、图表、tar.gz |
+| 升级配置文件 | 配置的 Bucket + Key | Excel |
+
+#### 操作接口
+
+```go
+minio.Upload(bucket, key, filePath) error        // 上传文件
+minio.UploadFromReader(bucket, key, reader) error // 流式上传
+minio.Download(bucket, key) (io.Reader, error)    // 下载文件
+minio.List(bucket, prefix) ([]ObjectInfo, error)  // 列出文件
+minio.Delete(bucket, key) error                   // 删除文件
+minio.ExistsObject(bucket, key) (bool, error)     // 检查存在
+minio.PresignedURL(bucket, key, expires) (string, error) // 预签名URL
+```
+
+### 11.5 Casdoor SSO
+
+#### 集成方式
+
+- **协议**: OAuth 2.0 Authorization Code Flow
+- **功能**: 统一登录、Token 刷新、JWT 解析
+- **会话管理**: Redis Hash 存储,TTL = 7 天
+
+#### 认证流程
+
+```
+前端 → Casdoor 登录页 → 获取 Authorization Code
+    → POST /auth/login {code, state}
+    → 后端调用 casdoor.GetOAuthToken(code, state)
+    → 获取 AccessToken + RefreshToken
+    → 存入 Redis Session
+    → 返回 SessionId + Tokens
+```
+
+---
+
+## 12. 部署与运行
+
+### 12.1 启动序列
+
+```
+main.go 启动流程:
+
+ 1. config.LoadConfig()           加载 .env 配置
+ 2. logger.Init(env)              初始化 Zap 日志
+ 3. database.Init(&cfg.App)       连接 PostgreSQL + MongoDB
+ 4. cache.Init(redisAddr)         连接 Redis (DB=12)
+ 5. minio.InitClient(&cfg.App)    连接 MinIO 对象存储
+ 6. repositories.FwhHttpInit()    初始化 FWH API 客户端
+ 7. scheduler.Init()              注册 Cron 定时任务
+ 8. task.InitTask(redisOpt)       启动 Asynq 异步任务系统
+ 9. casdoor.InitAuthConfig()      配置 SSO 认证
+10. repositories.FwhMqttInit()    连接 MQTT Broker
+11. routes.Setup(app)             注册 Fiber 路由
+12. networkEvents.InitEvio()      启动网络事件监听 (goroutine)
+13. Asynqmon HTTP 服务            启动任务监控面板 (:8081)
+14. fwh_pro.Login()               预登录 FWH 平台
+15. app.Listen(":8301")           启动 HTTP 服务
+16. Signal 监听                   优雅关闭 (SIGTERM/SIGINT)
+```
+
+### 12.2 环境依赖
+
+| 服务 | 用途 | 开发环境地址 |
+|------|------|-------------|
+| PostgreSQL | 本地业务数据 | 本地 |
+| MongoDB | 文档型数据存储 | `127.0.0.1:27017` |
+| Redis | 缓存 + 任务队列 | `localhost:6379` |
+| MinIO | 对象存储 | `localhost:9198` |
+| MySQL (FWH Pro) | 站点数据(只读) | AWS RDS |
+| Casdoor | SSO 认证 | 独立部署 |
+| MQTT Broker | 设备通信 | TLS 连接 |
+
+### 12.3 部署脚本
+
+```bash
+# 编译 + 部署
+cmd/deploy.sh
+
+# 仅编译
+cmd/only_build.sh
+
+# 查看日志
+cmd/showLog.sh              # 全部日志
+cmd/showTodayLog.sh         # 今日日志
+cmd/showTodayErrorLog.sh    # 今日错误日志
+```
+
+### 12.4 端口规划
+
+| 端口 | 服务 | 说明 |
+|------|------|------|
+| 8301 | Fiber HTTP | 主 API 服务 |
+| 8081 | Asynqmon | 异步任务监控面板 |
+| — | Evio | 网络事件监听(自定义端口) |
+
+### 12.5 优雅关闭
+
+```go
+// 监听系统信号
+quit := make(chan os.Signal, 1)
+signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
+<-quit
+
+// 关闭顺序:
+// 1. 停止接收新请求
+// 2. 等待进行中请求完成
+// 3. 关闭数据库连接
+// 4. 关闭 Redis 连接
+// 5. 关闭 MQTT 连接
+// 6. 停止定时任务
+// 7. 停止异步任务 Worker
+```
+
+### 12.6 开发指南
+
+#### 新增业务模块步骤
+
+1. **定义 Domain** — 在 `domain/` 中定义业务领域对象
+2. **定义 DTO** — 在 `dto/` 中定义 API 请求/响应结构
+3. **实现 Repository** — 在 `repositories/` 对应子目录中实现数据访问
+4. **注册到聚合层** — 在 `repositories/repositories.go` 中暴露新方法
+5. **实现 Service** — 在 `services/` 中编写业务逻辑
+6. **实现 Controller** — 在 `controllers/` 中处理 HTTP 请求
+7. **注册路由** — 在 `routes/` 中创建路由文件并在 `Setup()` 中注册
+8. **编写 Swagger 注释** — 在 Controller 函数上添加 swaggo 注释
+9. **生成文档** — 运行 `swag init` 更新 Swagger 文档
+
+#### 代码规范
+
+- **分层原则**:不允许跳层访问(Controller 不能直接调 Repository)
+- **聚合模式**:所有 Repository 通过聚合包暴露,Service 只导入 `repositories`
+- **错误处理**:统一通过 Fiber 响应错误码,Service 层返回 error
+- **日志规范**:使用 `logger.Log` 或 `zap.L()` 记录结构化日志
+- **命名规范**:
+  - 文件名:`{模块名}{层级}.go`(如 `batchDownloadServices.go`)
+  - 函数名:大驼峰,动词开头(如 `GetBatchDownloadList`)
+  - Domain 字段:bson tag 用 camelCase
+  - DTO 字段:json tag 用 camelCase
+  - Model 字段:gorm tag 用 snake_case
+
+---
+
+## 附录
+
+### A. 关键技术模式
+
+#### 流式文件传输
+
+```go
+c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
+    // ZIP 打包 / 文件转发 / Excel 写入
+    io.Copy(writer, source)
+    w.Flush()
+})
+```
+
+#### 分布式唯一任务
+
+```go
+lock, ok, err := cache.TryLock(ctx, "task:"+taskType+":"+id, 2*time.Hour)
+if !ok { return ErrTaskAlreadyRunning }
+defer cache.LockRelease(ctx, key, lock.Token)
+// 执行任务...
+```
+
+#### 泛型 Cache-Aside
+
+```go
+result, err := cache.GetOrSet[dto.SitePowerFlow](ctx, cacheKey,
+    func() (*dto.SitePowerFlow, error) {
+        return repositories.QuerySitePowerFlow(sn, date)
+    },
+    cache.WithTTL(24*time.Hour),
+)
+```
+
+### B. MQTT 命令类型速查
+
+| cmdType | 方向 | 说明 |
+|---------|------|------|
+| 0 | 上报 | 设备离线通知 |
+| 101 | 上报 | 设备上线通知 |
+| 201 | 上报 | 运行数据(功率/SOC/模式等 30+ 参数) |
+| 357 | 上报 | 模式切换事件 |
+| 377 | 下行 | 故障录波参数设置 |
+| 378 | 上行 | 故障录波参数设置响应 |
+
+### C. 数据库 Schema 速查
+
+| 数据库 | Schema/Database | 表/集合 |
+|--------|----------------|---------|
+| PostgreSQL | `alarm_snapshot` | `site_alarm_snapshot_file_record`、`site_alarm_snapshot_data` |
+| PostgreSQL | `original` | `site_original_data` |
+| MongoDB | `FwhToolboxDbDev` | site_info、batch_download、site_analysis、auto_update_config、sim_card_info、shutdown_statistics、user_info、sites_monitor、site_tags 等 |
+| MySQL | `home_energy_storage_server` | `iot_gateway`(只读) |
+| Redis | DB=12 | 会话(Hash)、缓存(String)、任务队列(Stream)、锁(String NX) |

+ 1 - 0
service/cmd/showTodayLog.sh

@@ -0,0 +1 @@
+journalctl -u fwh-toolbox-server --since today

+ 11 - 0
service/domain/sitesInfoManageDomain.go

@@ -166,6 +166,17 @@ type SiteInfoDomain struct {
 	// 忽略部分字段newHomeFlag、sleepStatus、isJoinJA12
 	SiteId  *uint   `bson:"siteId,omitempty" json:"siteId,omitempty"`   // 站点ID号
 	JobCode *string `bson:"jobCode,omitempty" json:"jobCode,omitempty"` // Job Code (Sunrun相关)
+
+	// 非原始数据
+	ExtTags *[]string `bson:"extTags,omitempty" json:"extTags,omitempty"` // (扩展)设备标签,保存内容为标签ID
+}
+
+type SiteTagDomain struct {
+	Id          primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` // 标签ID
+	Name        string             `bson:"name" json:"name"`                  // 标签名
+	Count       uint               `bson:"count" json:"count"`                // 标签关联设备数量
+	Color       string             `bson:"color" json:"color"`                // 标签颜色
+	Description string             `bson:"description" json:"description"`    // 标签描述
 }
 
 // Code generated. FwhSiteInfo -> SiteInfoDomain

+ 245 - 78
service/dto/sitesInfoManageDTO.go

@@ -2,6 +2,9 @@ package dto
 
 import (
 	"fwh-toolbox-server/domain"
+
+	"go.mongodb.org/mongo-driver/bson/primitive"
+	"go.uber.org/zap"
 )
 
 type SiteInfoRequest struct {
@@ -12,98 +15,119 @@ type SiteInfoRequest struct {
 }
 
 type SiteInfoDTO struct {
-	Sn                 string  `bson:"_id" json:"sn"` // aGate SN
-	Name               *string `bson:"name,omitempty" json:"name,omitempty"`
-	DistributorId      *int    `bson:"distributorId,omitempty" json:"distributorId,omitempty"`           // 经销商ID
-	DistributorName    *string `bson:"distributorName,omitempty" json:"distributorName,omitempty"`       // 经销商名称
-	InstallerId        *int    `bson:"installerId,omitempty" json:"installerId,omitempty"`               // 安装商ID
-	InstallerAccountId *int    `bson:"installerAccountId,omitempty" json:"installerAccountId,omitempty"` // 安装商账号ID
-	InstallerAccount   *string `bson:"installerAccount,omitempty" json:"installerAccount,omitempty"`     // 安装商账号
-	InstallerName      *string `bson:"installerName,omitempty" json:"installerName,omitempty"`           // 安装商名称
-	MaintId            *int    `bson:"maintId,omitempty" json:"maintId,omitempty"`                       // 维护商ID
-	MaintName          *string `bson:"maintName,omitempty" json:"maintName,omitempty"`                   // 维护商名称
-	Account            *string `bson:"account,omitempty" json:"account,omitempty"`                       // 绑定的用户账号
-	AccountName        *string `bson:"accountName,omitempty" json:"accountName,omitempty"`               // 绑定账号名
-	Longitude          string  `bson:"longitude" json:"longitude"`                                       // 经度
-	Latitude           string  `bson:"latitude" json:"latitude"`                                         // 纬度
-	Address            *string `bson:"address,omitempty" json:"address,omitempty"`
-	InstallTime        *string `bson:"installTime,omitempty" json:"installTime,omitempty"` // 安装时间
-	ActiveStatus       int     `bson:"activeStatus" json:"activeStatus"`                   // 激活状态 0-未激活 1-激活
-	ActiveTime         string  `bson:"activeTime" json:"activeTime"`                       // 激活时间(北京时间)
-	LocalActiveTime    string  `bson:"localActiveTime" json:"localActiveTime"`             // 激活时间(站点当地时间)
-	Status             int     `bson:"status" json:"status"`                               // 网络状态 0-离线 1-在线
-	OnLineTime         *string `bson:"onLineTime,omitempty" json:"onLineTime,omitempty"`   // 最后上线时间 YYYY-MM-DD HH:mm:SS
-	OffLineTime        *string `bson:"offLineTime,omitempty" json:"offLineTime,omitempty"` // 最后离线时间 YYYY-MM-DD HH:mm:SS
+	Sn                 string  `json:"sn"` // aGate SN
+	Name               *string `json:"name,omitempty"`
+	DistributorId      *int    `json:"distributorId,omitempty"`      // 经销商ID
+	DistributorName    *string `json:"distributorName,omitempty"`    // 经销商名称
+	InstallerId        *int    `json:"installerId,omitempty"`        // 安装商ID
+	InstallerAccountId *int    `json:"installerAccountId,omitempty"` // 安装商账号ID
+	InstallerAccount   *string `json:"installerAccount,omitempty"`   // 安装商账号
+	InstallerName      *string `json:"installerName,omitempty"`      // 安装商名称
+	MaintId            *int    `json:"maintId,omitempty"`            // 维护商ID
+	MaintName          *string `json:"maintName,omitempty"`          // 维护商名称
+	Account            *string `json:"account,omitempty"`            // 绑定的用户账号
+	AccountName        *string `json:"accountName,omitempty"`        // 绑定账号名
+	Longitude          string  `json:"longitude"`                    // 经度
+	Latitude           string  `json:"latitude"`                     // 纬度
+	Address            *string `json:"address,omitempty"`
+	InstallTime        *string `json:"installTime,omitempty"` // 安装时间
+	ActiveStatus       int     `json:"activeStatus"`          // 激活状态 0-未激活 1-激活
+	ActiveTime         string  `json:"activeTime"`            // 激活时间(北京时间)
+	LocalActiveTime    string  `json:"localActiveTime"`       // 激活时间(站点当地时间)
+	Status             int     `json:"status"`                // 网络状态 0-离线 1-在线
+	OnLineTime         *string `json:"onLineTime,omitempty"`  // 最后上线时间 YYYY-MM-DD HH:mm:SS
+	OffLineTime        *string `json:"offLineTime,omitempty"` // 最后离线时间 YYYY-MM-DD HH:mm:SS
 	// model * ??? 未知字段 (TODO:后续处理)
-	DeviceIp        *string `bson:"deviceIp,omitempty" json:"deviceIp,omitempty"`   // 设备联网IP
-	Subnet          *string `bson:"subnet,omitempty" json:"subnet,omitempty"`       // 子掩码
-	Mac             *string `bson:"mac,omitempty" json:"mac,omitempty"`             // MAC地址
-	GatewayIp       *string `bson:"gatewayIp,omitempty" json:"gatewayIp,omitempty"` // 网关IP
-	Signal          *string `bson:"signal,omitempty" json:"signal,omitempty"`       // 信号强度
-	ICCID           string  `bson:"iccId" json:"iccId"`
-	IMEI            string  `bson:"imei" json:"imei"`
-	FirmUpgradeTime *string `bson:"firmUpgradeTime,omitempty" json:"firmUpgradeTime,omitempty"` // 固件最后更新时间 YYYY-MM-DD HH:mm:SS
-	TurnoverTime    *string `bson:"turnoverTime,omitempty" json:"turnoverTime,omitempty"`       // 周转时间??? YYYY-MM-DD HH:mm:SS
-	CreateTime      *string `bson:"createTime,omitempty" json:"createTime,omitempty"`           // 设备首次登录时间 YYYY-MM-DD HH:mm:SS
+	DeviceIp        *string `json:"deviceIp,omitempty"`  // 设备联网IP
+	Subnet          *string `json:"subnet,omitempty"`    // 子掩码
+	Mac             *string `json:"mac,omitempty"`       // MAC地址
+	GatewayIp       *string `json:"gatewayIp,omitempty"` // 网关IP
+	Signal          *string `json:"signal,omitempty"`    // 信号强度
+	ICCID           string  `json:"iccId"`
+	IMEI            string  `json:"imei"`
+	FirmUpgradeTime *string `json:"firmUpgradeTime,omitempty"` // 固件最后更新时间 YYYY-MM-DD HH:mm:SS
+	TurnoverTime    *string `json:"turnoverTime,omitempty"`    // 周转时间??? YYYY-MM-DD HH:mm:SS
+	CreateTime      *string `json:"createTime,omitempty"`      // 设备首次登录时间 YYYY-MM-DD HH:mm:SS
 	// 忽略部分功率字段powerSun、powerGen、powerUti、powerFhp、powerLoad
-	BatterySoc      *float32 `bson:"batterySoc,omitempty" json:"batterySoc,omitempty"` // 系统电池SOC
-	RunStatus       int      `bson:"runStatus" json:"runStatus"`                       // 设备运行状态 0-待机 1-充电 2-放电 3-故障
-	EmsModel        *int     `bson:"emsModel,omitempty" json:"emsModel,omitempty"`     // EMS调度运行模式(策略) 参考调度ID
-	ElectricityType int      `bson:"electricityType" json:"electricityType"`           // 电网类型 0-离网运行 1-并网运行
+	BatterySoc      *float32 `json:"batterySoc,omitempty"` // 系统电池SOC
+	RunStatus       int      `json:"runStatus"`            // 设备运行状态 0-待机 1-充电 2-放电 3-故障
+	EmsModel        *int     `json:"emsModel,omitempty"`   // EMS调度运行模式(策略) 参考调度ID
+	ElectricityType int      `json:"electricityType"`      // 电网类型 0-离网运行 1-并网运行
 	// workMode * ??? 未知字段 (TODO:后续处理)
-	FhpSn            string  `bson:"fhpSn" json:"fhpSn"`                                           // aPower SN 多设备用,分割
-	TimeZone         float32 `bson:"timeZone" json:"timeZone"`                                     // 时区数值
-	Version          string  `bson:"version" json:"version"`                                       // 设备固件版本
-	SlVersion        string  `bson:"slVersion" json:"slVersion"`                                   // EMS-DSP版本
-	SlVersionJson    string  `bson:"slVersionJson" json:"slVersionJson"`                           // EMS-DSP版本详细信息JSON
-	MeterVersion     *string `bson:"meterVersion,omitempty" json:"meterVersion,omitempty"`         // 电表版本
-	MbVersion        *string `bson:"mbVersion,omitempty" json:"mbVersion,omitempty"`               // MODBUS版本
-	AwsVersion       *string `bson:"awsVersion,omitempty" json:"awsVersion,omitempty"`             // AWS版本
-	AppVersion       *string `bson:"appVersion,omitempty" json:"appVersion,omitempty"`             // APP版本(与APP通讯的模块软件)
-	ElecnetStatus    int     `bson:"elecnetStatus" json:"elecnetStatus"`                           // 电网状态 0-离网 1-并网
-	SlaverStat       int     `bson:"slaverStat" json:"slaverStat"`                                 // EMS-DSP状态
-	Dst              *int    `bson:"dst,omitempty" json:"dst,omitempty"`                           // 夏令时 0-否 1-是
-	ZoneInfo         string  `bson:"zoneInfo" json:"zoneInfo"`                                     // 时区信息 America/Los_Angeles
-	PostCode         string  `bson:"postCode" json:"postCode"`                                     // 邮编
-	DeviceTime       *string `bson:"deviceTime,omitempty" json:"deviceTime,omitempty"`             // 设备时间 YYYY-MM-DD HH:mm:SS
-	ProtocolVer      string  `bson:"protocolVer" json:"protocolVer"`                               // 协议版本
-	SysHdVersion     int     `bson:"sysHdVersion" json:"sysHdVersion"`                             // aGate硬件版本
-	RealSysHdVersion *string `bson:"realSysHdVersion,omitempty" json:"realSysHdVersion,omitempty"` // aGate系统版本名称
-	WifiSignal       string  `bson:"wifiSignal" json:"wifiSignal"`                                 // WIFI信号
-	ConnType         int     `bson:"connType" json:"connType"`                                     // 网络连接类型 0-未连接 1-eth0 2-eth1 3-WIFI 4-4G
-	UserName         *string `bson:"userName,omitempty" json:"userName,omitempty"`
-	ContactNumber    *string `bson:"contactNumber,omitempty" json:"contactNumber,omitempty"` // 联系方式(手机号)
+	FhpSn            string  `json:"fhpSn"`                      // aPower SN 多设备用,分割
+	TimeZone         float32 `json:"timeZone"`                   // 时区数值
+	Version          string  `json:"version"`                    // 设备固件版本
+	SlVersion        string  `json:"slVersion"`                  // EMS-DSP版本
+	SlVersionJson    string  `json:"slVersionJson"`              // EMS-DSP版本详细信息JSON
+	MeterVersion     *string `json:"meterVersion,omitempty"`     // 电表版本
+	MbVersion        *string `json:"mbVersion,omitempty"`        // MODBUS版本
+	AwsVersion       *string `json:"awsVersion,omitempty"`       // AWS版本
+	AppVersion       *string `json:"appVersion,omitempty"`       // APP版本(与APP通讯的模块软件)
+	ElecnetStatus    int     `json:"elecnetStatus"`              // 电网状态 0-离网 1-并网
+	SlaverStat       int     `json:"slaverStat"`                 // EMS-DSP状态
+	Dst              *int    `json:"dst,omitempty"`              // 夏令时 0-否 1-是
+	ZoneInfo         string  `json:"zoneInfo"`                   // 时区信息 America/Los_Angeles
+	PostCode         string  `json:"postCode"`                   // 邮编
+	DeviceTime       *string `json:"deviceTime,omitempty"`       // 设备时间 YYYY-MM-DD HH:mm:SS
+	ProtocolVer      string  `json:"protocolVer"`                // 协议版本
+	SysHdVersion     int     `json:"sysHdVersion"`               // aGate硬件版本
+	RealSysHdVersion *string `json:"realSysHdVersion,omitempty"` // aGate系统版本名称
+	WifiSignal       string  `json:"wifiSignal"`                 // WIFI信号
+	ConnType         int     `json:"connType"`                   // 网络连接类型 0-未连接 1-eth0 2-eth1 3-WIFI 4-4G
+	UserName         *string `json:"userName,omitempty"`
+	ContactNumber    *string `json:"contactNumber,omitempty"` // 联系方式(手机号)
 	// 忽略部分未知字段totalData、usedData、resourceList
-	FinancierName           *string `bson:"financierName,omitempty" json:"financierName,omitempty"`                     // 大客户名称
-	FinancierContractNumber *string `bson:"financierContractNumber,omitempty" json:"financierContractNumber,omitempty"` // 大客户联系方式(手机号)
-	CountryId               *int    `bson:"countryId,omitempty" json:"countryId,omitempty"`                             // 国家ID
-	ProvinceId              *int    `bson:"provinceId,omitempty" json:"provinceId,omitempty"`                           // 州/省ID
-	FinancierId             *int    `bson:"financierId,omitempty" json:"financierId,omitempty"`                         // 大客户ID
-	FirstName               *string `bson:"firstName,omitempty" json:"firstName,omitempty"`                             // 用户名
-	LastName                *string `bson:"lastName,omitempty" json:"lastName,omitempty"`                               // 用户姓
+	FinancierName           *string `json:"financierName,omitempty"`           // 大客户名称
+	FinancierContractNumber *string `json:"financierContractNumber,omitempty"` // 大客户联系方式(手机号)
+	CountryId               *int    `json:"countryId,omitempty"`               // 国家ID
+	ProvinceId              *int    `json:"provinceId,omitempty"`              // 州/省ID
+	FinancierId             *int    `json:"financierId,omitempty"`             // 大客户ID
+	FirstName               *string `json:"firstName,omitempty"`               // 用户名
+	LastName                *string `json:"lastName,omitempty"`                // 用户姓
 	// region * ??? 未知字段 (TODO:后续处理)
-	Remark            *string `bson:"remark,omitempty" json:"remark,omitempty"` // 备注信息
-	GenEn             int     `bson:"genEn" json:"genEn"`                       // 发电机是否使能 0-不使能 1-使能
-	UserId            *int    `bson:"userId,omitempty" json:"userId,omitempty"` // 用户ID
-	Country           string  `bson:"country" json:"country"`                   // 国家
-	Province          string  `bson:"province" json:"province"`                 // 州/省
-	City              string  `bson:"city" json:"city"`                         // 城市
-	AddressSecondLast *string `bson:"addressSecondLast,omitempty" json:"addressSecondLast,omitempty"`
-	DeviceAddress     *string `bson:"deviceAddress,omitempty" json:"deviceAddress,omitempty"` // 设备地址
+	Remark            *string `json:"remark,omitempty"` // 备注信息
+	GenEn             int     `json:"genEn"`            // 发电机是否使能 0-不使能 1-使能
+	UserId            *int    `json:"userId,omitempty"` // 用户ID
+	Country           string  `json:"country"`          // 国家
+	Province          string  `json:"province"`         // 州/省
+	City              string  `json:"city"`             // 城市
+	AddressSecondLast *string `json:"addressSecondLast,omitempty"`
+	DeviceAddress     *string `json:"deviceAddress,omitempty"` // 设备地址
 	// whetherEncrypt * ??? 未知字段 (TODO:后续处理)
-	EmsDeviceVer *int    `bson:"emsDeviceVer,omitempty" json:"emsDeviceVer,omitempty"` // EMS版本
-	Site         *string `bson:"site,omitempty" json:"site,omitempty"`                 // 站点ID名
+	EmsDeviceVer *int    `json:"emsDeviceVer,omitempty"` // EMS版本
+	Site         *string `json:"site,omitempty"`         // 站点ID名
 	// 忽略部分字段newHomeFlag、sleepStatus、isJoinJA12
-	SiteId  *uint   `bson:"siteId,omitempty" json:"siteId,omitempty"`   // 站点ID号
-	JobCode *string `bson:"jobCode,omitempty" json:"jobCode,omitempty"` // Job Code (Sunrun相关)
+	SiteId  *uint   `json:"siteId,omitempty"`  // 站点ID号
+	JobCode *string `json:"jobCode,omitempty"` // Job Code (Sunrun相关)
+
+	// 非原始数据
+	ExtTags []SiteTagDTO `json:"extTags,omitempty"` // (扩展)设备标签
+}
+
+type SiteTagDTO struct {
+	Id          string `json:"id,omitempty"` // 标签ID
+	Name        string `json:"name"`         // 标签名
+	Count       uint   `json:"count"`        // 标签关联设备数量
+	Color       string `json:"color"`        // 标签颜色
+	Description string `json:"description"`  // 标签描述
 }
 
 // Code generated. SiteInfoDomain -> SiteInfoDTO
-func SiteInfoDomainToSiteInfoDTO(src *domain.SiteInfoDomain) *SiteInfoDTO {
+func SiteInfoDomainToSiteInfoDTO(src *domain.SiteInfoDomain, tagMap *map[string]*SiteTagDTO) *SiteInfoDTO {
 	if src == nil {
 		return nil
 	}
 
+	tagList := []SiteTagDTO{}
+	if src.ExtTags != nil && tagMap != nil {
+		for _, tagId := range *src.ExtTags {
+			tagInfo := (*tagMap)[tagId]
+			if tagInfo != nil {
+				tagList = append(tagList, *tagInfo)
+			}
+		}
+	}
+
 	return &SiteInfoDTO{
 		Sn:                      src.Sn,
 		Name:                    src.Name,
@@ -180,5 +204,148 @@ func SiteInfoDomainToSiteInfoDTO(src *domain.SiteInfoDomain) *SiteInfoDTO {
 		EmsDeviceVer:            src.EmsDeviceVer,
 		Site:                    src.Site,
 		SiteId:                  src.SiteId,
+
+		// 非原始数据
+		ExtTags: tagList,
+	}
+}
+
+// Code generated. []SiteInfoDomain -> []SiteInfoDTO
+func SiteInfoDomainListToSiteInfoDTOList(src []domain.SiteInfoDomain, tagMap *map[string]*SiteTagDTO) []SiteInfoDTO {
+	if src == nil {
+		return nil
+	}
+
+	infoList := []SiteInfoDTO{}
+
+	for _, info := range src {
+		tagList := []SiteTagDTO{}
+		if info.ExtTags != nil && tagMap != nil {
+			for _, tagId := range *info.ExtTags {
+				tagInfo := (*tagMap)[tagId]
+				if tagInfo != nil {
+					tagList = append(tagList, *tagInfo)
+				}
+			}
+		}
+
+		infoList = append(infoList, SiteInfoDTO{
+			Sn:                      info.Sn,
+			Name:                    info.Name,
+			DistributorId:           info.DistributorId,
+			DistributorName:         info.DistributorName,
+			InstallerId:             info.InstallerId,
+			InstallerAccountId:      info.InstallerAccountId,
+			InstallerAccount:        info.InstallerAccount,
+			InstallerName:           info.InstallerName,
+			MaintId:                 info.MaintId,
+			MaintName:               info.MaintName,
+			Account:                 info.Account,
+			AccountName:             info.AccountName,
+			Longitude:               info.Longitude,
+			Latitude:                info.Latitude,
+			Address:                 info.Address,
+			InstallTime:             info.InstallTime,
+			ActiveStatus:            info.ActiveStatus,
+			ActiveTime:              info.ActiveTime,
+			Status:                  info.Status,
+			OnLineTime:              info.OnLineTime,
+			OffLineTime:             info.OffLineTime,
+			DeviceIp:                info.DeviceIp,
+			Subnet:                  info.Subnet,
+			Mac:                     info.Mac,
+			GatewayIp:               info.GatewayIp,
+			Signal:                  info.Signal,
+			ICCID:                   info.ICCID,
+			IMEI:                    info.IMEI,
+			FirmUpgradeTime:         info.FirmUpgradeTime,
+			TurnoverTime:            info.TurnoverTime,
+			CreateTime:              info.CreateTime,
+			BatterySoc:              info.BatterySoc,
+			RunStatus:               info.RunStatus,
+			EmsModel:                info.EmsModel,
+			ElectricityType:         info.ElectricityType,
+			FhpSn:                   info.FhpSn,
+			TimeZone:                info.TimeZone,
+			Version:                 info.Version,
+			SlVersion:               info.SlVersion,
+			SlVersionJson:           info.SlVersionJson,
+			MeterVersion:            info.MeterVersion,
+			MbVersion:               info.MbVersion,
+			AwsVersion:              info.AwsVersion,
+			AppVersion:              info.AppVersion,
+			ElecnetStatus:           info.ElecnetStatus,
+			SlaverStat:              info.SlaverStat,
+			Dst:                     info.Dst,
+			ZoneInfo:                info.ZoneInfo,
+			PostCode:                info.PostCode,
+			DeviceTime:              info.DeviceTime,
+			ProtocolVer:             info.ProtocolVer,
+			SysHdVersion:            info.SysHdVersion,
+			RealSysHdVersion:        info.RealSysHdVersion,
+			WifiSignal:              info.WifiSignal,
+			ConnType:                info.ConnType,
+			UserName:                info.UserName,
+			ContactNumber:           info.ContactNumber,
+			FinancierName:           info.FinancierName,
+			FinancierContractNumber: info.FinancierContractNumber,
+			CountryId:               info.CountryId,
+			ProvinceId:              info.ProvinceId,
+			FinancierId:             info.FinancierId,
+			FirstName:               info.FirstName,
+			LastName:                info.LastName,
+			Remark:                  info.Remark,
+			GenEn:                   info.GenEn,
+			UserId:                  info.UserId,
+			Country:                 info.Country,
+			Province:                info.Province,
+			City:                    info.City,
+			AddressSecondLast:       info.AddressSecondLast,
+			DeviceAddress:           info.DeviceAddress,
+			EmsDeviceVer:            info.EmsDeviceVer,
+			Site:                    info.Site,
+			SiteId:                  info.SiteId,
+
+			// 非原始数据
+			ExtTags: tagList,
+		})
+	}
+
+	return infoList
+}
+
+// Code generated. SiteTagDomain -> SiteTagDTO
+func SiteTagDomainToSiteTagDTO(src *domain.SiteTagDomain) *SiteTagDTO {
+	if src == nil {
+		return nil
+	}
+
+	return &SiteTagDTO{
+		Id:          src.Id.Hex(),
+		Name:        src.Name,
+		Count:       src.Count,
+		Color:       src.Color,
+		Description: src.Description,
+	}
+}
+
+// Code generated. SiteTagDTO -> SiteTagDomain
+func SiteSiteTagDTOToTagDomain(src *SiteTagDTO) *domain.SiteTagDomain {
+	if src == nil {
+		return nil
+	}
+
+	id, err := primitive.ObjectIDFromHex(src.Id)
+	if err != nil {
+		id = primitive.NilObjectID
+		zap.L().Warn("SiteSiteTagDTOToTagDomain illegal ID", zap.String("err", err.Error()), zap.String("id", src.Id))
+	}
+
+	return &domain.SiteTagDomain{
+		Id:          id,
+		Name:        src.Name,
+		Count:       src.Count,
+		Color:       src.Color,
+		Description: src.Description,
 	}
 }

+ 1 - 1
service/jobs/callFwhFile.go

@@ -499,7 +499,7 @@ func ackTask(streamInfo CallFileTaskStreamInfo, success bool, err error) {
 	}
 
 	if err != nil {
-		zap.L().Error("taskQueue ackTask error.", zap.String("err", err.Error()))
+		zap.L().Error("taskQueue ackTask error.", zap.String("err", err.Error()), zap.String("StreamName", streamInfo.StreamName), zap.String("GroupName", streamInfo.GroupName), zap.String("ID", streamInfo.ID))
 	}
 }
 

+ 10 - 9
service/repositories/api/fwh_pro/fwh_faultRecord.go

@@ -93,15 +93,16 @@ func queryCellFaultRecord(devType int, sn string, indexList []int) (*domain.FwhR
 		indexListStr[i] = strconv.Itoa(v)
 	}
 
-	// 要发送的表单数据
-	form := url.Values{}
-	form.Set("deviceId", sn)                           // aGate or aPower or aHub SN
-	form.Set("index", strings.Join(indexListStr, ",")) // 要召唤数据的idx
-	form.Set("callType", strconv.Itoa(devType))
-	form.Set("triggerType", strconv.Itoa(2)) // 触发类型,目前仅支持2-自动
-	form.Set("lang", "ZH_CN")
-
-	response, err := FwhHttpPostUnmarshal[interface{}](getFullEnergyUrl("/hes-gateway/manage/callFaultRecording"), form)
+	// 要发送的 JSON 数据
+	body := map[string]interface{}{
+		"deviceId":    sn,                              // aGate or aPower or aHub SN
+		"index":       strings.Join(indexListStr, ","), // 要召唤数据的idx
+		"callType":    devType,
+		"triggerType": 2,    // 触发类型,目前仅支持2-自动
+		"lang":        "ZH_CN",
+	}
+
+	response, err := FwhHttpPostUnmarshal[interface{}](getFullEnergyUrl("/hes-gateway/manage/callFaultRecording"), body)
 	if err != nil {
 		return nil, err
 	}

+ 141 - 5
service/repositories/mongo/siteInfoManageRepo.go

@@ -13,11 +13,13 @@ import (
 	"go.mongodb.org/mongo-driver/bson/primitive"
 	"go.mongodb.org/mongo-driver/mongo"
 	"go.mongodb.org/mongo-driver/mongo/options"
+	"go.uber.org/zap"
 )
 
 const (
 	CollectionName_SiteInfoManageRecord = "SiteInfoManageRecord"
 	CollectionName_SiteInfoManageData   = "SiteInfoManageData"
+	CollectionName_SiteInfoTags         = "SiteInfoTags"
 )
 
 // 更新站点信息获取结果
@@ -227,11 +229,9 @@ func GetSiteInfoManageList(page, size uint, filter *domain.SiteInfoFilter, searc
 	}
 
 	var findOptions *options.FindOptions
-	if size > 0 {
-		findOptions = options.Find().
-			SetSkip(int64((page - 1) * size)).
-			SetLimit(int64(size))
-	}
+	findOptions = options.Find().
+		SetSkip(int64((page - 1) * size)).
+		SetLimit(int64(size))
 
 	cursor, err := collection.Find(ctx, dbFilter, findOptions)
 	if err != nil {
@@ -318,3 +318,139 @@ func InsertSiteInfoData(data domain.SiteInfoDomain) error {
 
 	return nil
 }
+
+// 查询站点信息标签列表
+func GetSiteInfoTags(page, size uint, search string) (*dto.ResPage[domain.SiteTagDomain], error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_SiteInfoTags)
+
+	// 查询条件
+	filter := bson.M{}
+
+	if search != "" {
+		filter["name"] = bson.M{
+			"$regex": primitive.Regex{
+				Pattern: search,
+				Options: "i", // 忽略大小写
+			},
+		}
+	}
+
+	// 总数
+	total, err := collection.CountDocuments(ctx, filter)
+	if err != nil {
+		return nil, err
+	}
+
+	// 查询选项
+	opts := options.Find().
+		SetSkip(int64((page - 1) * size)).
+		SetLimit(int64(size)).
+		SetSort(bson.D{
+			{Key: "name", Value: 1}, // 按名称升序
+		})
+
+	cursor, err := collection.Find(ctx, filter, opts)
+	if err != nil {
+		return nil, err
+	}
+	defer cursor.Close(ctx)
+
+	list := make([]domain.SiteTagDomain, 0)
+
+	if err := cursor.All(ctx, &list); err != nil {
+		return nil, err
+	}
+
+	return &dto.ResPage[domain.SiteTagDomain]{
+		Data:     list,
+		PageSize: size,
+		PageNum:  page,
+		Total:    uint(total),
+	}, nil
+}
+
+// 查询站点信息标签列表(根据id列表)
+func GetSiteInfoTagsFromId(idList []string) (map[string]*dto.SiteTagDTO, error) {
+	tagMap := make(map[string]*dto.SiteTagDTO)
+	if len(idList) == 0 {
+		return tagMap, nil
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_SiteInfoTags)
+
+	// 转换为 ObjectID
+	objectIDs := make([]primitive.ObjectID, 0, len(idList))
+	for _, id := range idList {
+		objectID, err := primitive.ObjectIDFromHex(id)
+		if err != nil {
+			// 非法 ID
+			zap.L().Warn("GetSiteInfoTagsFromId illegal ID", zap.String("err", err.Error()), zap.String("id", id))
+			continue
+		}
+		objectIDs = append(objectIDs, objectID)
+	}
+
+	if len(objectIDs) == 0 {
+		return tagMap, nil
+	}
+
+	filter := bson.M{
+		"_id": bson.M{
+			"$in": objectIDs,
+		},
+	}
+
+	cursor, err := collection.Find(ctx, filter)
+	if err != nil {
+		return nil, err
+	}
+	defer cursor.Close(ctx)
+
+	for cursor.Next(ctx) {
+		var tag domain.SiteTagDomain
+		if err := cursor.Decode(&tag); err != nil {
+			return nil, err
+		}
+
+		tagMap[tag.Id.Hex()] = dto.SiteTagDomainToSiteTagDTO(&tag)
+	}
+
+	if err := cursor.Err(); err != nil {
+		return nil, err
+	}
+
+	return tagMap, nil
+}
+
+// 创建或更新站点信息标签
+func UpdateSiteInfoTag(tag domain.SiteTagDomain) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_SiteInfoTags)
+
+	filter := bson.M{"_id": tag.Id}
+
+	update := bson.M{
+		"$set": bson.M{
+			"name":        tag.Name,
+			"count":       tag.Count,
+			"color":       tag.Color,
+			"description": tag.Description,
+		},
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 6 - 1
service/repositories/pgsql/alarmSnapshot/siteAlarmSnapshotData.go

@@ -2,6 +2,7 @@ package pgsql_alarmSnapshot
 
 import (
 	"context"
+	"fmt"
 	"fwh-toolbox-server/models/model_pgsql/model_alarm_snapshot"
 	"fwh-toolbox-server/models/model_pgsql/query_alarm_snapshot"
 	"time"
@@ -35,6 +36,10 @@ func QuerySnapshotData(c context.Context, sn string, logTime string) ([]*Snapsho
 }
 
 func InsertSnapshotData(c context.Context, data *SnapshotData) error {
+	if data == nil {
+		return fmt.Errorf("InsertSnapshotData empty data.")
+	}
+
 	ctx, cancel := context.WithTimeout(c, 5*time.Second)
 	defer cancel()
 
@@ -43,7 +48,7 @@ func InsertSnapshotData(c context.Context, data *SnapshotData) error {
 		UpdateAll: true,
 	}).Create(data)
 	if err != nil {
-		zap.L().Error("文件解析记录数据库创建失败", zap.String("err", err.Error()))
+		zap.L().Error("文件解析记录数据库创建失败", zap.String("err", err.Error()), zap.Int64("ID", data.ID), zap.String("SN", data.Sn), zap.Int64("FileRecordID", data.FileRecordID), zap.String("LogTime", data.LogTime.String()))
 		return err
 	}
 

+ 3 - 0
service/repositories/repositories.go

@@ -164,6 +164,9 @@ var GetSiteInfoUpdateNewRecord = mongo.GetSiteInfoUpdateNewRecord
 var GetSiteInfoManageList = mongo.GetSiteInfoManageList
 var InsertSiteInfoData = mongo.InsertSiteInfoData
 var GetSiteInfoManageListFromGiccid = mongo.GetSiteInfoManageListFromGiccid
+var GetSiteInfoTags = mongo.GetSiteInfoTags
+var GetSiteInfoTagsFromId = mongo.GetSiteInfoTagsFromId
+var UpdateSiteInfoTag = mongo.UpdateSiteInfoTag
 
 // SimCardManage
 var GetSimCardBatchInfo = mongo.GetSimCardBatchInfo

+ 46 - 6
service/services/siteInfoManageServices.go

@@ -46,17 +46,23 @@ func QuerySiteInfoDataList(para dto.SiteInfoRequest) (*dto.ResPage[dto.SiteInfoD
 		return nil, err
 	}
 
+	idList := []string{}
+	for _, data := range res.Data {
+		idList = append(idList, *data.ExtTags...)
+	}
+	tagMap, err := repositories.GetSiteInfoTagsFromId(idList)
+	if err != nil {
+		zap.L().Error("QuerySiteInfoDataList GetSiteInfoTagsFromId failed.", zap.String("err", err.Error()), zap.Strings("idList", idList))
+		return nil, err
+	}
+
 	result := dto.ResPage[dto.SiteInfoDTO]{
-		Data:     []dto.SiteInfoDTO{},
+		Data:     dto.SiteInfoDomainListToSiteInfoDTOList(res.Data, &tagMap),
 		PageSize: res.PageSize,
 		PageNum:  res.PageNum,
 		Total:    res.Total,
 	}
 
-	for _, d := range res.Data {
-		result.Data = append(result.Data, *dto.SiteInfoDomainToSiteInfoDTO(&d))
-	}
-
 	return &result, nil
 }
 
@@ -283,7 +289,16 @@ func GetSiteInfoData(sn string) (*dto.SiteInfoDTO, error) {
 		zap.L().Error("保存站点信息失败", zap.String("err", err.Error()), zap.String("sn", sn))
 	}
 
-	dtoData := dto.SiteInfoDomainToSiteInfoDTO(domainData)
+	tagMap := make(map[string]*dto.SiteTagDTO)
+	if domainData.ExtTags != nil {
+		tagMap, err = repositories.GetSiteInfoTagsFromId(*domainData.ExtTags)
+		if err != nil {
+			zap.L().Error("GetSiteInfoData GetSiteInfoTagsFromId failed.", zap.String("err", err.Error()), zap.Strings("idList", *domainData.ExtTags))
+			return nil, err
+		}
+	}
+
+	dtoData := dto.SiteInfoDomainToSiteInfoDTO(domainData, &tagMap)
 
 	return dtoData, nil
 }
@@ -292,3 +307,28 @@ func GetSiteInfoData(sn string) (*dto.SiteInfoDTO, error) {
 func OperateSiteInfoEnableXlog(ctx context.Context, sn string) (string, error) {
 	return repositories.FwhMqttEnableXlog(ctx, sn)
 }
+
+// 获取站点信息标签列表
+func GetSiteInfoTags(page uint, size uint, search string) (*dto.ResPage[dto.SiteTagDTO], error) {
+	tags, err := repositories.GetSiteInfoTags(page, size, search)
+	if err != nil {
+		return nil, err
+	}
+
+	result := dto.ResPage[dto.SiteTagDTO]{
+		Data:     []dto.SiteTagDTO{},
+		PageSize: tags.PageSize,
+		PageNum:  tags.PageNum,
+		Total:    tags.Total,
+	}
+	for _, tag := range tags.Data {
+		result.Data = append(result.Data, *dto.SiteTagDomainToSiteTagDTO(&tag))
+	}
+
+	return &result, nil
+}
+
+// 创建或更新站点信息标签
+func UpdateSiteInfoTag(tag dto.SiteTagDTO) error {
+	return repositories.UpdateSiteInfoTag(*dto.SiteSiteTagDTOToTagDomain(&tag))
+}

+ 5 - 0
service/task/queue/fwhQueue/fwhTaskBatchDownload.go

@@ -509,6 +509,11 @@ func requestCallFaultRecord(downloadList *[]domain.BatchDownloadFileTaskInfo, de
 		return err
 	}
 
+	// 未召唤到记录,不添加任务
+	if len(callInfoList) == 0 {
+		return nil
+	}
+
 	// 提取符合日期范围的选项
 	var indexList string = ""
 	for _, info := range callInfoList {