Files
AquaControlAI/开发文档/code-standards.md
T
2026-07-11 13:04:16 +08:00

37 KiB
Raw Blame History

污水厂智能控制系统 — 代码规范

本文档适用于本平台所有模块(数据管理、历史数据、实时监控、智能控制、系统配置等)的代码开发工作。 作为代码审查的依据标准,所有合入主分支的代码必须通过本规范的检查。


目录


1. 通用原则

1.1 核心原则

原则 说明
可读性优先 代码是写给人类看的,其次才是给机器执行。优先清晰直白,避免晦涩技巧
一致性 同一概念在代码、数据库、API、前端中保持命名一致(如 device_id 在所有层统一)
最小惊讶 函数的名称和签名应让调用者无需查看实现就能大致猜到行为
防御式编程 不信任外部输入,对所有外部输入做校验,但内部调用尽量减少冗余校验
模块自治 每个模块有清晰的边界,模块间通过 API 通信,不直接访问其他模块的内部数据
优先复用 先复用已有模块、成熟依赖和平台能力;仅在现有方案确实无法满足需求时才自行实现

1.2 命名风格对照表

上下文 风格 示例
Go 源码 camelCase / CamelCase deviceService, CreateDevice()
数据库(PG/TDengine snake_case device_id, protocol_type
RESTful APIURL kebab-case /api/v1/collection-points
RESTful APIJSON Body snake_case "device_id": "uuid"
前端 JS/TS 源码 camelCase fetchDeviceList()
前端 CSS 类名 kebab-case .device-table-container
前端组件/目录名 kebab-case collection-points/
环境变量 UPPER_SNAKE_CASE DB_CONNECTION_STRING
Git 分支名 kebab-case feat/aeration-model

1.3 注释规范

  • Go 注释:遵循 // PackageName 包注释和 // FunctionName 功能描述 的 Go 标准注释风格
  • 不要注释显而易见的事// 递增 i 是废话注释
  • TODO 注释:必须附带责任人,如 // TODO(张三): 后续需要处理断线重连的边界情况
  • FIXME 注释:必须附带 issue 编号,如 // FIXME(#123): 此处有并发安全问题
  • 代码删除:不保留被注释掉的旧代码,直接删除,有需要从 git 历史查找

1.4 优先复用,禁止重复造轮子

  • 先查后写:新增功能前,必须先检查项目现有代码、已引入依赖、组件库和已封装的公共能力;优先复用 internal/pkg/web/src/components/web/src/utils/ 与同类模块的实现。
  • 优先级:项目既有能力 → 官方 SDK / 框架内置能力 → 维护活跃、许可证兼容的成熟开源库 → 自研实现。不得因“实现简单”绕过这一顺序。
  • 禁止重复实现:禁止自行实现已有成熟方案已覆盖的通用能力,例如鉴权、参数校验、密码哈希、UUID、数据库迁移、HTTP 客户端重试、日期处理、图表、虚拟列表、CSV 编解码和协议编解码。
  • 允许自研的条件:现有方案无法满足性能、可靠性、许可、安全、离线部署或工业协议兼容性要求时,方可自研;提交中必须说明已调研方案、未采用原因、维护边界和测试方案。
  • 封装边界:对第三方库的调用应集中在适配层或公共封装中,业务代码不得散落依赖某个供应商的私有 API;不得为薄封装无理由再造通用组件。
  • 审查要求:PR 必须说明“复用了什么”或“为何不能复用”;无法说明的重复实现不得合入。

2. 项目结构与工程规范

2.1 整体目录结构

water-plant-control/
├── cmd/                        # 可执行程序入口
│   ├── server/                 #   Web 服务端入口
│   │   └── main.go
│   └── collector/              #   采集引擎入口
│       └── main.go
├── internal/                   # 私有应用代码(不对外暴露)
│   ├── api/                    #   HTTP handler 层
│   │   ├── router.go           #   路由注册
│   │   ├── middleware/         #   中间件(鉴权、日志、CORS、恢复等)
│   │   ├── device/             #   设备管理 API
│   │   ├── collection/         #   采集点管理 API
│   │   ├── writepoint/         #   写入点管理 API
│   │   ├── history/            #   历史数据 API
│   │   └── system/             #   系统配置 API
│   ├── service/                #   业务逻辑层
│   │   ├── device/
│   │   ├── collection/
│   │   ├── writepoint/
│   │   ├── history/
│   │   └── collector/          #   采集引擎业务逻辑
│   ├── repository/             #   数据访问层
│   │   ├── postgres/           #   PostgreSQL 访问
│   │   └── tdengine/           #   TDengine 访问
│   ├── model/                  #   数据模型定义
│   │   ├── device.go
│   │   ├── collection_point.go
│   │   ├── write_point.go
│   │   └── history.go
│   ├── engine/                 #   运行时引擎
│   │   ├── collector/          #   采集引擎
│   │   │   ├── manager.go      #   设备连接管理器
│   │   │   ├── scheduler.go    #   采集任务调度器
│   │   │   └── runner.go       #   采集点执行单元
│   │   └── writer/             #   写入引擎
│   │       └── writer.go
│   ├── protocol/               #   协议插件化
│   │   ├── driver.go           #   ProtocolDriver 接口定义
│   │   ├── registry.go         #   注册表
│   │   ├── s7/                 #   S7 协议实现
│   │   └── modbus/             #   Modbus TCP 协议实现
│   └── pkg/                    #   内部共享工具包
│       ├── config/             #   配置管理
│       ├── logger/             #   日志工具
│       ├── response/           #   HTTP 响应统一封装
│       ├── validator/          #   自定义校验器
│       └── csvutil/            #   CSV 导入导出工具
├── web/                        # 前端代码
│   ├── src/
│   │   ├── api/                #   API 调用层
│   │   ├── components/         #   公共组件
│   │   ├── views/              #   页面级组件
│   │   │   ├── device/         #   设备管理
│   │   │   ├── collection/     #   数据采集
│   │   │   ├── write-point/    #   数据写入
│   │   │   └── history/        #   历史数据
│   │   ├── router/             #   前端路由
│   │   ├── store/              #   状态管理
│   │   └── utils/              #   工具函数
│   └── ...
├── deploy/                     # 部署相关
├── docs/                       # 文档
└── Makefile                    # 构建脚本

2.2 新增模块的目录规范

当新增模块(如未来开发"实时监控""智能控制""系统配置")时,遵循以下规范:

# 后端新增模块,在以下目录各增加一个子包
internal/
├── api/monitoring/      # 实时监控 API
├── service/monitoring/  # 实时监控业务逻辑
├── repository/...       # 数据访问
└── model/               # 数据模型

# 前端新增模块,在以下目录增加
web/src/views/
└── monitoring/          # 实时监控页面

模块级命名约束

模块 后端 Go package 前端目录 API 路径前缀
数据管理-设备 device device/ /api/v1/devices
数据管理-采集点 collection collection/ /api/v1/collection-points
数据管理-写入点 writepoint write-point/ /api/v1/write-points
历史数据 history history/ /api/v1/history

上表中未列出的模块(实时监控、智能控制、系统配置)在开发时补充至此表。

2.3 依赖管理

  • Go:使用 go.mod 管理依赖,定期执行 go mod tidy
  • 前端:使用 package.json + pnpm-lock.yaml(优先 pnpm,其次 npm
  • 禁止:直接拷贝第三方库源码到项目中(除非 fork 修改,且需在 README 中注明)
  • 新增依赖准入:新增依赖前必须检索现有依赖和代码,确认没有等价能力;选择有明确维护状态、兼容许可证、稳定版本与安全更新渠道的库。
  • 版本锁定:提交依赖变更时必须同时提交锁文件;禁止使用不受控的浮动版本或从未固定提交的 Git 分支安装依赖。
  • 最小化引入:不得为单个小功能引入体积过大或功能重叠的依赖;优先按需导入,并删除不再使用的依赖。

3. Go 后端规范

3.1 代码风格

  • 严格遵循 gofmt / goimports 格式,不允许任何格式化例外
  • 使用 go vetgolangci-lint 作为 CI 前置检查
  • 每行代码不超过 120 字符

3.2 分层架构规范

项目采用经典的三层架构(API Handler → Service → Repository),层间调用规则:

Handler (参数校验、请求/响应转换)
    │
    ▼
Service (业务逻辑、事务管理)
    │
    ▼
Repository (数据访问、ORM 查询)

禁止:
✗ Handler 直接调用 Repository
✗ Service 层处理 HTTP 请求/响应
✗ 循环依赖(A → B → A)

3.2.1 Handler 层规范

// ✅ 正确示例
func (h *DeviceHandler) Create(c *gin.Context) {
    var req CreateDeviceRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        response.BadRequest(c, "无效的请求参数", err.Error())
        return
    }
    // Handler 只做参数校验和响应转换
    device, err := h.svc.Create(c.Request.Context(), &req)
    if err != nil {
        response.Error(c, err)
        return
    }
    response.Success(c, device)
}

// ✗ 禁止:Handler 中写业务逻辑
func (h *DeviceHandler) Create(c *gin.Context) {
    // ... 校验参数
    // ... 自己查数据库判断是否重名           ✗ 应该调用 Service
    // ... 自己拼 SQL 插入                     ✗ 应该调用 Service
    // ... 自己写日志                          ✗ 应该由 Service 层处理
}

3.2.2 Service 层规范

// ✅ 正确示例
func (s *DeviceService) Create(ctx context.Context, req *CreateDeviceRequest) (*Device, error) {
    // 1. 参数校验(复杂校验逻辑放在 Service 层)
    if err := s.validateDevice(req); err != nil {
        return nil, err
    }
    
    // 2. 业务逻辑(如检查 name 唯一性)
    existing, err := s.repo.FindByName(ctx, req.Name)
    if err != nil {
        return nil, fmt.Errorf("查询设备名称失败: %w", err)
    }
    if existing != nil {
        return nil, ErrDeviceNameConflict
    }
    
    // 3. 转换为模型
    device := &model.Device{
        Name:       req.Name,
        ProtocolType: req.ProtocolType,
        Host:       req.Host,
        Port:       req.Port,
        // ...
    }
    
    // 4. 持久化
    if err := s.repo.Create(ctx, device); err != nil {
        return nil, fmt.Errorf("创建设备失败: %w", err)
    }
    
    return device, nil
}

3.2.3 Repository 层规范

// ✅ 正确示例
func (r *DeviceRepo) FindByProtocolType(ctx context.Context, protocolType string) ([]*model.Device, error) {
    query := `SELECT id, name, protocol_type, host, port, enabled, 
              connect_timeout, reconnect_interval, protocol_config,
              created_at, updated_at
              FROM devices 
              WHERE protocol_type = $1 AND deleted = FALSE
              ORDER BY name`
    
    rows, err := r.db.QueryContext(ctx, query, protocolType)
    if err != nil {
        return nil, fmt.Errorf("查询设备列表失败: %w", err)
    }
    defer rows.Close()
    
    var devices []*model.Device
    for rows.Next() {
        var d model.Device
        if err := rows.Scan(&d.ID, &d.Name, &d.ProtocolType, &d.Host, &d.Port,
            &d.Enabled, &d.ConnectTimeout, &d.ReconnectInterval, &d.ProtocolConfig,
            &d.CreatedAt, &d.UpdatedAt); err != nil {
            return nil, fmt.Errorf("扫描设备记录失败: %w", err)
        }
        devices = append(devices, &d)
    }
    return devices, rows.Err()
}

// ✅ 建议:对于简单 CRUD 操作,统一封装 BaseRepo
// ✅ 建议:复杂查询使用 QueryBuilder,避免字符串拼接

3.3 错误处理

// 使用自定义错误类型,而非 magic string
var (
    ErrDeviceNotFound     = errors.New("设备不存在")
    ErrDeviceNameConflict = errors.New("设备名称已存在")
    ErrProtocolNotSupport = errors.New("不支持的协议类型")
)

// 错误包装:始终携带上下文
if err != nil {
    return nil, fmt.Errorf("创建采集点失败: %w", err)
}

// 禁止:
// ✗ return errors.New("设备不存在")         // 没有上下文
// ✗ return nil, fmt.Errorf("err: %v", err)  // "err: " 无意义前缀

3.4 并发安全

  • 采集引擎(运行时)涉及设备连接池、共享内存状态,必须使用 sync.Mutexsync.RWMutex 保护
  • 避免裸 sync.Map,优先使用 map + sync.RWMutex
  • 协程启动必须可控:使用 context.Context + sync.WaitGroup 管理生命周期
  • 禁止无限制启动 goroutine,必须通过带有缓冲 channel 或协程池限制并发数
// ✅ 正确示例:采集引擎任务管理
type CollectorManager struct {
    mu       sync.RWMutex
    connPool map[string]*Connection    // 设备连接池
    runners  map[string]*PointRunner   // 采集点执行器
    ctx      context.Context
    cancel   context.CancelFunc
    wg       sync.WaitGroup
}

3.5 单元测试

层级 测试策略 覆盖率目标
Service 使用 mock repository 进行纯逻辑测试 ≥ 80%
Handler 使用 httptest 进行 API 测试 ≥ 60%
Repository 使用 testcontainers 或内嵌数据库 ≥ 50%
Engine 使用 mock protocol driver 测试采集逻辑 ≥ 70%
  • 测试文件与源码同目录,命名 *_test.go
  • Mock 统一使用 github.com/stretchr/testify/mockgo.uber.org/mock
  • 测试数据使用 t.Helper() + t.Cleanup() 管理

4. 前端规范

4.1 框架与技术选型(约定)

领域 选型
UI 框架 Vue 3 + Composition API
构建工具 Vite
状态管理 Pinia
路由 Vue Router
HTTP 请求 Axios
图表 ECharts
CSS Tailwind CSS(或 Less/Sass scoped
代码规范 ESLint + Prettier

4.2 组件设计规范

<!--  正确示例 -->
<script setup lang="ts">
// 1. props 和 emit 使用类型定义
interface Props {
  deviceId: string
  loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
  loading: false
})

const emit = defineEmits<{
  (e: 'update', id: string): void
  (e: 'delete', id: string): void
}>()

// 2. 组合式函数提取可复用逻辑
const { data, fetchData } = useDeviceDetail(props.deviceId)
</script>

<template>
  <div class="device-detail">
    <!-- 模板保持简洁复杂条件逻辑使用计算属性 -->
  </div>
</template>

<style scoped>
/* 使用 scoped style,避免全局污染 */
</style>

4.3 API 调用层

// src/api/device.ts
import request from '@/utils/request'

// ✅ 每个模块一个 API 文件,所有请求集中在 api/ 目录
export interface DeviceListParams {
  page?: number
  pageSize?: number
  keyword?: string
  protocolType?: string
  enabled?: boolean
}

export function fetchDeviceList(params: DeviceListParams) {
  const { pageSize, ...rest } = params
  return request.get('/api/v1/devices', {
    params: { ...rest, page_size: pageSize },
  })
}

export function createDevice(data: Record<string, unknown>) {
  return request.post('/api/v1/devices', data)
}

// ✗ 禁止:在组件中直接调用 axios
// ✗ 禁止:URL 路径写在组件内部

4.4 ECharts 使用规范

  • 图表组件统一封装在 src/components/charts/ 目录下
  • ECharts 的 option 构造使用纯函数,方便测试
  • 图表容器使用 ResizeObserver 自动适应窗口变化
  • 曲线模式分段自适应 Y 轴:实现为可复用的工具函数 src/utils/axis-mapper.ts
// ✅ 工具函数封装示例
// src/utils/axis-mapper.ts
interface Segment {
  start: number
  end: number
  heightRatio: number  // 占Y轴高度的比例
}

export function buildSegments(dataValues: number[]): Segment[] {
  // 根据数据分布自动分段
  // ... 实现分段自适应Y轴的映射逻辑
}

export function mapValueToPosition(value: number, segments: Segment[]): number {
  // 将原始值映射为显示坐标
  // ... 
}

4.5 状态与复选框勾选

  • 勾选状态:使用 Pinia store 统一管理,支持曲线/表格模式切换时保持状态
// ✅ 正确示例
// src/store/selected-points.ts
export const useSelectedPointsStore = defineStore('selected-points', () => {
  const selectedIds = ref<Set<string>>(new Set())
  
  // 最大勾选数 20
  const MAX_SELECTION = 20
  
  function toggle(id: string) {
    if (selectedIds.value.has(id)) {
      selectedIds.value.delete(id)
    } else if (selectedIds.value.size < MAX_SELECTION) {
      selectedIds.value.add(id)
    }
    // 超过上限不做操作
  }
  
  return { selectedIds, toggle }
})

5. RESTful API 规范

5.1 URL 设计

格式:/api/v1/{资源名}[/{资源ID}][/{子资源}][/{动作}]

约定:
- 资源名使用 kebab-case 复数形式(devices, collection-points, write-points
- 子资源(如导出/导入)使用 POST 动词 + 路径
- 批量操作使用 POST,非 GET(避免 URL 过长)
- 版本号 v1 写在路径中

5.2 响应格式

所有 API 响应使用统一格式:

// ✅ 成功响应
{
    "code": 0,
    "message": "success",
    "data": { ... }
}

// ✅ 错误响应
{
    "code": 40001,
    "message": "设备名称已存在",
    "data": null
}
字段 必填 说明
code 0 表示成功,非 0 表示业务错误码
message 人类可读的描述信息
data 响应数据,可为 null

5.3 分页响应格式

{
    "code": 0,
    "message": "success",
    "data": {
        "total": 200,
        "page": 1,
        "page_size": 20,
        "items": [ ... ]
    }
}

5.4 错误码规范

错误码范围 类别 说明
0 成功 请求正常处理
400xx 参数错误 请求参数校验失败
401xx 鉴权错误 未登录或 token 过期
403xx 权限错误 无操作权限
404xx 未找到 请求的资源不存在
409xx 冲突 唯一性冲突等
500xx 服务端错误 内部错误

各模块错误码分配:

模块 范围
设备管理 40001~40999
采集点管理 41001~41999
写入点管理 42001~42999
历史数据 43001~43999
采集引擎 50001~50999
写入引擎 51001~51999

5.5 路径参数命名规范

✅ /api/v1/devices/{id}               // 路径参数使用 {param} 风格
✅ /api/v1/collection-points/groups   // 固定子路径
✅ /api/v1/write-points/{id}/write    // 动作子路径

查询参数命名(与数据库字段一致):
  ?page=1&page_size=20&keyword=XXX&enabled=true

6. 数据库规范

6.1 PostgreSQL 规范

规则 说明
表名 全部小写 snake_case,复数形式:devices, collection_points, write_points
字段名 全部小写 snake_case
主键 统一使用 UUID 类型,默认 gen_random_uuid()
时间字段 created_at, updated_at 使用 TIMESTAMP WITH TIME ZONE
逻辑删除 统一使用 deleted BOOLEAN NOT NULL DEFAULT FALSE
索引命名 idx_{表名}_{字段名}:如 idx_devices_name
唯一索引 逻辑删除的表使用 WHERE deleted = FALSE 的条件唯一索引
外键 显式声明 REFERENCES,但不启用级联(业务层处理级联逻辑)
迁移 使用 golang-migrate 或类似工具管理,禁止手动修改库结构
更新时间 通过迁移创建触发器或由仓储层统一维护 updated_at,不得依赖调用方遗漏更新
  • 每次结构变更必须新增可重复执行的迁移文件,并在空库和包含历史数据的库上验证。
  • 破坏性变更采用“先兼容、再迁移、后删除”的至少两个发布周期策略;确需一次性变更时,必须附带回滚与数据备份方案。
-- ✅ 正确示例
CREATE TABLE devices (
    id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name              VARCHAR(128) NOT NULL,
    protocol_type     VARCHAR(32) NOT NULL,
    enabled           BOOLEAN NOT NULL DEFAULT TRUE,
    deleted           BOOLEAN NOT NULL DEFAULT FALSE,
    host              VARCHAR(256) NOT NULL,
    port              INTEGER NOT NULL,
    connect_timeout   INTEGER NOT NULL DEFAULT 5,
    reconnect_interval INTEGER NOT NULL DEFAULT 10,
    protocol_config   JSONB NOT NULL DEFAULT '{}',
    created_at        TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at        TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    created_by        VARCHAR(64),
    updated_by        VARCHAR(64)
);

CREATE UNIQUE INDEX idx_devices_name ON devices(name) WHERE deleted = FALSE;

6.2 TDengine 规范

规则 说明
表名 超级表用 snake_casecollection_data, computed_data
子表名 使用采集点 ID 去连字符后的 32 位小写字符串
时间戳 统一使用 TIMESTAMP,精度毫秒
质量戳 INT 类型:0=good, 1=bad
统一使用 DOUBLEBOOL 类型存 0/1
标签 元数据信息存为 TAGS,不做查询条件的有选择缓存
KEEP 参数 通过系统配置动态调整,通过 ALTER DATABASE 执行

6.3 SQL 书写规范

-- ✅ 关键字大写,字段名/表名小写
SELECT id, name, protocol_type
FROM devices
WHERE enabled = TRUE AND deleted = FALSE
ORDER BY name
LIMIT 20 OFFSET 0;

-- ✗ 禁止:SELECT *,必须显式指定字段列表

-- ✅ INSERT 语句
INSERT INTO devices (name, protocol_type, host, port, protocol_config)
VALUES ($1, $2, $3, $4, $5)
RETURNING id;

6.4 连接配置、初始化与重建

数据库连接必须通过环境变量或密钥管理服务注入,应用仅读取配置并在启动时执行连通性检查。数据库名称不是默认值,必须由部署环境显式指定;严禁在代码、示例配置或本文档中写入真实密码。

# .env.example:可提交,仅包含非敏感连接端点与占位符
POSTGRES_HOST=101.35.54.96
POSTGRES_PORT=5432
POSTGRES_USER=qsc20001102
POSTGRES_PASSWORD=qsc102341
POSTGRES_DB=aquacontrolai


TDENGINE_HOST=101.35.54.96
TDENGINE_PORT=6041
TDENGINE_USER=root
TDENGINE_PASSWORD=taosdata
TDENGINE_DB=aquacontrolai
  • PostgreSQL DSN:按 postgres://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}?sslmode={POSTGRES_SSLMODE} 组装。Go 后端使用连接池,并在应用启动时通过带超时的 PingContext 校验连接;不得记录完整 DSN 或密码。
  • TDengine DSN6041 为 taosAdapter 服务端口。Go 后端优先使用 github.com/taosdata/driver-go/v3 的 WebSocket 驱动,DSN 格式为 {TDENGINE_USER}:{TDENGINE_PASSWORD}@ws({TDENGINE_HOST}:{TDENGINE_PORT})/{TDENGINE_DB},驱动名为 taosWS。必须在 DSN 中指定数据库名,不依赖连接后的 USE 语句。
  • 网络与权限:对公网地址必须配置最小网络访问范围;生产环境 PostgreSQL 必须启用证书校验,两个数据库账号均应按环境和最小权限拆分,禁止使用共享管理员账号作为长期应用账号。
  • 配置文件:真实密码只允许位于部署平台密钥、CI Secret 或开发人员本地未提交的 .env 文件;.env.example 保留占位符,.env 必须在 .gitignore 中。

6.4.1 开发/测试环境数据库重建

开发或测试环境允许清空并重建 PostgreSQL 与 TDengine 数据库,以确保初始结构一致;此操作会永久删除目标库中的全部数据。

  • 仅允许由人工显式执行的初始化/重建脚本触发,禁止在应用启动、普通迁移或自动部署中隐式执行 DROP DATABASE
  • 脚本必须要求显式确认标记(例如 RESET_DATABASE=CONFIRM_DROP_AND_RECREATE),并在执行前打印目标主机、端口、数据库名和环境;任一项缺失则立即失败。
  • PostgreSQL 重建顺序:连接维护库 → 断开目标库现有连接 → 删除目标库 → 创建目标库及所需扩展 → 从零执行全部迁移。
  • TDengine 重建顺序:删除目标库 → 创建目标库及保留策略 → 从零执行全部时序表/超级表初始化脚本。
  • 重建脚本必须只操作 POSTGRES_DBTDENGINE_DB 指定的目标库,禁止对系统库、未指定库或生产环境执行;生产环境重建须另行书面审批、备份和恢复演练。
  • 每次重建完成后必须执行迁移状态检查和最小连通性/读写冒烟测试,并记录执行人、时间、目标环境和结果。

7. 日志与错误处理规范

7.1 日志级别

级别 使用场景 示例
DEBUG 调试信息,仅开发环境 DEBUG 采集点曝气池DO_01 采集完成,值=2.35
INFO 正常运行信息 INFO 设备一期曝气柜PLC 连接成功
WARN 可恢复的异常 WARN 设备一期曝气柜PLC 重连第3次失败,将在10秒后重试
ERROR 需要关注的错误 ERROR 写入点加药泵频率 写入失败: 回读值不匹配

7.2 日志规范

// ✅ 正确示例
logger.Info("设备连接成功",
    "device_id", device.ID,
    "device_name", device.Name,
    "protocol", device.ProtocolType,
)

logger.Error("写入操作失败",
    "point_id", pointID,
    "target_value", targetValue,
    "error", err.Error(),
)

// ✗ 禁止:没有上下文的日志
// ✗ logger.Info("连接成功")
// ✗ logger.Error("报错了: " + err.Error())
// ✗ fmt.Println() 或 fmt.Errorf() 替代日志库

7.3 关键业务日志埋点

以下业务操作必须记录日志(作为审计追溯依据):

操作 日志级别 说明
设备创建/修改/删除 INFO 记录操作人和变更内容
采集点创建/修改/删除 INFO 同上
写入点创建/修改/删除 INFO 同上
写入操作 INFO 记录写入值、来源(manual/auto)、操作人、结果
采集引擎启停 INFO 记录启动/停止原因
设备连接/断开 WARN 记录连接耗时或断开原因
重连失败 WARN 记录失败次数
协议驱动异常 ERROR 记录异常堆栈

8. 安全规范

8.1 SQL 注入防护

// ✅ 正确:使用参数化查询,并显式声明返回字段
db.QueryContext(ctx, "SELECT id, name FROM devices WHERE name = $1", name)

// ✗ 禁止:字符串拼接 SQL
// db.QueryContext(ctx, "SELECT * FROM devices WHERE name = '" + name + "'")

8.2 输入校验

场景 校验规则
IP 地址 使用 net.ParseIP() 或正则校验合法格式
端口号 范围 1~65535
时间范围 结束时间必须晚于开始时间,最大跨度不超过 31 天(防止 TDengine 扫大量数据)
分页 page >= 1, page_size <= 100
文件名 导出的 CSV 文件名不含用户输入(防止路径穿越)
布尔值 在 Go 中由 JSON 解析器自动校验,前端限制 true/false

8.3 写入安全

  • 写入开关校验:必须在 Service 层校验 write_enabled=trueHandler 层只做类型校验
  • 写入来源校验:校验 write_source 与请求 source 的匹配关系(manual/auto/both
  • 回读验证:写入后必须回读确认,实现位置在 Engine 层,Service 层调用
  • 写入权限:人工写入(source=manual)需要登录鉴权;程序自动写入(source=auto)需要 API Token

8.4 配置安全

  • 数据库密码、API Key 等敏感信息通过环境变量或 vault 注入,禁止硬编码在代码中
  • .env 文件加入 .gitignore,仅提供 .env.example 模板
  • protocol_config 中不存储凭据类信息
  • 本规范、README、Issue、日志、截图和错误响应中均不得记录真实密码、完整 DSN 或 Token;如发生泄露,立即轮换凭据并按安全事件处理

9. 代码审核清单

9.1 提交流程前置检查(开发者在提审前自行检查)

□ 代码通过 gofmt / goimports / eslint / prettier 格式化
□ 本地 go vet 和 ESLint 检查通过
□ 单元测试通过,且覆盖率满足要求
□ 无被注释掉的旧代码
□ 无 console.log / fmt.Println 等调试输出
□ 无硬编码的敏感信息(密码、token、key)
□ 新增能力已复用既有实现或成熟依赖;如无法复用,已说明原因并完成评审
□ 数据库变更已提供迁移,且未在应用启动流程中执行破坏性重建
□ 新增 API 已添加至 API 文档
□ 所有 TODO/FIXME 已确认或附带了责任人

9.2 代码审查 Checklist

架构层面

检查项 说明
模块边界是否清晰 是否有跨模块的内部依赖(如 Handler 直接调用其他模块的 Repo)
分层是否遵守 Handler → Service → Repository 单向依赖
是否引入循环依赖 Package A 依赖 Package BPackage B 不应再依赖 Package A
扩展点是否留好 协议驱动是否遵循 ProtocolDriver 接口而非直接调用具体实现
新增模块的目录是否符第 2 节规范 按照约定的目录结构添加
是否重复造轮子 是否已经检索并复用项目既有能力、官方 SDK 或成熟依赖;自研是否有充分理由

功能层面

检查项 说明
业务规则是否覆盖 对照 spec 中的业务规则表(R001R013, H001H010)逐条确认
边界条件是否处理 空列表、分页边界、超长输入、字段缺失、逻辑删除记录等
并发安全 共享状态的读写是否有锁保护
事务管理 跨表操作的 Service 方法是否使用了数据库事务
CSV 导入导出 编码是否为 UTF-8 with BOM,布尔值和 JSON 字段格式是否正确
写入流程完整性 是否执行了写入→回读验证→记录日志的完整流程

性能层面

检查项 说明
N+1 查询 列表查询时是否 batch 加载关联数据,而非循环查询
索引 新增查询条件是否添加了对应的数据库索引
分页 列表接口是否都有分页,且 page_size 有上限约束
连接池 数据库和 PLC 连接是否使用了连接池而非每次新建
迁移安全 结构变更是否可追踪、可验证;破坏性变更是否具备备份、回滚和发布兼容方案
采集周期最小限制 collect_interval 是否校验 >= 1 秒

安全层面

检查项 说明
SQL 注入 全库搜索字符串拼接的 SQL 语句
参数校验 所有用户输入是否都通过了校验
写入权限校验 write_enabledwrite_source 是否在 Service 层校验
敏感信息泄露 错误信息是否直接返回给前端(如数据库密码、SQL 错误)
路径穿越 文件操作的路径是否未使用用户输入构造

前端层面

检查项 说明
API 路径 路径是否统一在 src/api/ 中管理
组件拆分 是否合理拆分为可复用组件,而非单文件过长
状态管理 跨组件共享状态是否使用 Pinia
响应式 图表容器是否自适应窗口变化
错误处理 API 调用是否有统一的错误处理(如失败提示、加载状态)
勾选上限 历史数据点位勾选是否有 20 个上限

附录 A:模块间关系与命名约定

A.1 模块间依赖关系

数据管理模块(基础层)
├── 提供:设备配置、采集点配置、写入点配置
├── 提供:采集数据写入 TDengine
├── 依赖:PostgreSQL(配置)、TDengine(时序数据)
│
├── 历史数据模块(消费层)依赖数据管理模块
│   ├── 读取:采集点配置、设备配置
│   ├── 读取:TDengine 历史数据
│   └── 提供:历史数据查询、曲线/表格展示
│
├── 实时监控模块(待开发)依赖数据管理模块
│   ├── 读取:采集点配置、设备配置
│   ├── 读取:采集引擎内存中的最新值
│   └── 订阅:MQTT 实时数据通道
│
├── 智能控制模块(待开发)
│   ├── 依赖:数据管理模块(设备信息、写入点)
│   ├── 依赖:历史数据模块(AI 训练数据来源)
│   └── 操作:通过写入点对 PLC 下发控制指令
│
└── 系统配置模块(待开发)
    ├── 负责:全局参数管理,如历史保留天数、采集引擎参数
    └── 被所有模块引用

A.2 Go 模型命名与数据库字段映射

数据库表(snake_case Go 结构体(PascalCase 说明
devices Device 设备
collection_points CollectionPoint 采集点
write_points WritePoint 写入点
write_logs WriteLog 写入日志

Go 结构体字段映射规则:

type Device struct {
    ID                string          `json:"id" db:"id"`
    Name              string          `json:"name" db:"name"`
    ProtocolType      string          `json:"protocol_type" db:"protocol_type"`
    Enabled           bool            `json:"enabled" db:"enabled"`
    Deleted           bool            `json:"-" db:"deleted"`                // 逻辑删除不返回前端
    Host              string          `json:"host" db:"host"`
    Port              int             `json:"port" db:"port"`
    ConnectTimeout    int             `json:"connect_timeout" db:"connect_timeout"`
    ReconnectInterval int             `json:"reconnect_interval" db:"reconnect_interval"`
    ProtocolConfig    json.RawMessage `json:"protocol_config" db:"protocol_config"`
    CreatedAt         time.Time       `json:"created_at" db:"created_at"`
    UpdatedAt         time.Time       `json:"updated_at" db:"updated_at"`
    CreatedBy         *string         `json:"created_by,omitempty" db:"created_by"`
    UpdatedBy         *string         `json:"updated_by,omitempty" db:"updated_by"`
}

规则:

  • json:"-" 标记的字段不返回前端(如 deleted
  • omitempty 用于可空字段
  • 请求体对应的结构体以 Request 结尾(如 CreateDeviceRequest
  • 响应体结构体在 Handler 层组装,不直接暴露模型

A.3 前端目录与路由命名

页面 路由路径 目录 说明
设备管理 /data/device views/device/ 数据管理子模块
数据采集 /data/collection views/collection/ 数据管理子模块
数据写入 /data/write-point views/write-point/ 数据管理子模块
历史数据 /history views/history/ 独立模块
实时监控 /monitoring views/monitoring/ 待开发
智能控制 /control views/control/ 待开发
系统配置 /settings views/settings/ 待开发

本文档版本:v1.0 最后更新:2026-07-10 适用范围:污水厂智能控制系统全部后端(Go)、前端(Vue 3)代码

使用说明

  1. 开发者开发新功能前阅读本文档,确保代码风格一致
  2. 提交 MR/PR 前依据 第 9 章 代码审核清单 逐条自查
  3. Code Review 时以本文档作为审核标准,不符合规范的要求修改后重新提审
  4. 本文档随项目推进持续更新,新增模块时补充对应章节