# 污水厂智能控制系统 — 代码规范 > 本文档适用于本平台所有模块(数据管理、历史数据、实时监控、智能控制、系统配置等)的代码开发工作。 > 作为代码审查的依据标准,所有合入主分支的代码必须通过本规范的检查。 --- ## 目录 - [1. 通用原则](#1-通用原则) - [2. 项目结构与工程规范](#2-项目结构与工程规范) - [3. Go 后端规范](#3-go-后端规范) - [4. 前端规范](#4-前端规范) - [5. RESTful API 规范](#5-restful-api-规范) - [6. 数据库规范](#6-数据库规范) - [7. 日志与错误处理规范](#7-日志与错误处理规范) - [8. 安全规范](#8-安全规范) - [9. 代码审核清单](#9-代码审核清单) - [附录 A:命名约定](#附录-a命名约定) --- ## 1. 通用原则 ### 1.1 核心原则 | 原则 | 说明 | |------|------| | **可读性优先** | 代码是写给人类看的,其次才是给机器执行。优先清晰直白,避免晦涩技巧 | | **一致性** | 同一概念在代码、数据库、API、前端中保持命名一致(如 `device_id` 在所有层统一) | | **最小惊讶** | 函数的名称和签名应让调用者无需查看实现就能大致猜到行为 | | **防御式编程** | 不信任外部输入,对所有外部输入做校验,但内部调用尽量减少冗余校验 | | **模块自治** | 每个模块有清晰的边界,模块间通过 API 通信,不直接访问其他模块的内部数据 | ### 1.2 命名风格对照表 | 上下文 | 风格 | 示例 | |--------|------|------| | Go 源码 | `camelCase` / `CamelCase` | `deviceService`, `CreateDevice()` | | 数据库(PG/TDengine) | `snake_case` | `device_id`, `protocol_type` | | RESTful API(URL) | `kebab-case` | `/api/v1/collection-points` | | RESTful API(JSON 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 --- ## 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/` 下各层同步添加子包,前端在 `web/src/views/` 下添加页面目录。 | 模块 | 后端 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 中注明) - **版本锁定**:提交依赖变更时必须同时提交锁文件;禁止使用不受控的浮动版本 - **最小化引入**:不得为单个小功能引入体积过大或功能重叠的依赖 --- ## 3. Go 后端规范 ### 3.1 代码风格 - 严格遵循 `gofmt` / `goimports` 格式,不允许任何格式化例外 - 使用 `go vet` 和 `golangci-lint` 作为 CI 前置检查 - 每行代码不超过 120 字符 ### 3.2 分层架构规范 项目采用经典的三层架构(API Handler → Service → Repository),层间调用规则: ``` Handler (参数校验、请求/响应转换) │ ▼ Service (业务逻辑、事务管理) │ ▼ Repository (数据访问、ORM 查询) 禁止: ✗ Handler 直接调用 Repository ✗ Service 层处理 HTTP 请求/响应 ✗ 循环依赖(A → B → A) ``` #### 3.2.1 Handler 层规范 ```go // ✅ 正确:Handler 只做参数校验和响应转换 func (h *DeviceHandler) Create(c *gin.Context) { var req CreateDeviceRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "无效的请求参数", err.Error()) return } device, err := h.svc.Create(c.Request.Context(), &req) if err != nil { response.Error(c, err) return } response.Success(c, device) } // ✗ 禁止:Handler 中写业务逻辑或直接操作数据库 ``` #### 3.2.2 Service 层规范 ```go // ✅ 正确:Service 层专注业务逻辑 func (s *DeviceService) Create(ctx context.Context, req *CreateDeviceRequest) (*Device, error) { // 复杂校验放 Service 层 if err := s.validate(req); err != nil { return nil, err } // 调用 Repository if err := s.repo.Create(ctx, device); err != nil { return nil, fmt.Errorf("创建设备失败: %w", err) } return device, nil } ``` #### 3.2.3 Repository 层规范 - 使用参数化查询,禁止字符串拼接 SQL - 显式指定查询字段,禁止 `SELECT *` - 复杂查询使用 QueryBuilder,避免字符串拼接 - 简单 CRUD 操作统一封装 BaseRepo ### 3.3 错误处理 ```go // 使用自定义错误类型,而非 magic string var ( ErrDeviceNotFound = errors.New("设备不存在") ErrDeviceNameConflict = 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.Mutex` 或 `sync.RWMutex` 保护 - 避免裸 `sync.Map`,优先使用 `map + sync.RWMutex` - 协程启动必须可控:使用 `context.Context` + `sync.WaitGroup` 管理生命周期 - **禁止无限制启动 goroutine**,必须通过缓冲 channel 或协程池限制并发数 ### 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/mock` 或 `go.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 组件设计规范 ```vue ``` ### 4.3 API 调用层规范 ```typescript // src/api/device.ts import request from '@/utils/request' // 每个模块一个 API 文件,所有请求集中在 api/ 目录 export function fetchDeviceList(params: Record) { return request.get('/api/v1/devices', { params }) } // ✗ 禁止:在组件中直接调用 axios // ✗ 禁止:URL 路径写在组件内部 ``` ### 4.4 ECharts 使用规范 - 图表组件统一封装在 `src/components/charts/` 目录下 - ECharts 的 option 构造使用纯函数,方便测试 - 图表容器使用 `ResizeObserver` 自动适应窗口变化 ### 4.5 状态管理规范 - 跨组件共享状态使用 Pinia store 统一管理 - 每个 Store 文件职责单一,按业务模块拆分 - 禁止在组件中直接修改非本组件的状态 --- ## 5. RESTful API 规范 ### 5.1 URL 设计 ``` 格式:/api/v1/{资源名}[/{资源ID}][/{子资源}][/{动作}] 约定: - 资源名使用 kebab-case 复数形式(devices, collection-points, write-points) - 批量操作使用 POST,非 GET(避免 URL 过长) - 版本号 v1 写在路径中 ``` ### 5.2 响应格式 所有 API 响应使用统一格式: ```json { "code": 0, "message": "success", "data": { ... } } ``` | 字段 | 必填 | 说明 | |------|------|------| | `code` | 是 | 0 表示成功,非 0 表示业务错误码 | | `message` | 是 | 人类可读的描述信息 | | `data` | 否 | 响应数据,可为 null | ### 5.3 分页响应格式 ```json { "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 // 固定子路径 查询参数命名(与数据库字段一致): ?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 或类似工具管理,禁止手动修改库结构 | ```sql -- ✅ 正确示例 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_case`:`collection_data`, `computed_data` | | 子表名 | 使用采集点 ID 去连字符后的 32 位小写字符串 | | 时间戳 | 统一使用 `TIMESTAMP`,精度毫秒 | | 质量戳 | `INT` 类型:`0=good`, `1=bad` | | 值 | 统一使用 `DOUBLE`,BOOL 类型存 0/1 | | 标签 | 元数据信息存为 TAGS,不做查询条件的有选择缓存 | ### 6.3 SQL 书写规范 ```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 数据库连接规范 - 数据库连接参数通过环境变量注入,禁止硬编码在代码中 - 连接 DSN 按 `postgres://USER:PASS@HOST:PORT/DB?sslmode=require` 格式组装 - Go 后端使用连接池,并在应用启动时通过带超时的 `PingContext` 校验连接 - 数据库连接配置仅由 `.env.example` 提供占位符模板,`.env` 文件必须加入 `.gitignore` --- ## 7. 日志与错误处理规范 ### 7.1 日志级别 | 级别 | 使用场景 | |------|---------| | DEBUG | 调试信息,仅开发环境 | | INFO | 正常运行信息 | | WARN | 可恢复的异常 | | ERROR | 需要关注的错误 | ### 7.2 日志格式规范 ```go // ✅ 正确:采用结构化键值对 logger.Info("设备连接成功", "device_id", device.ID, "device_name", device.Name, ) // ✗ 禁止:没有上下文的日志 // ✗ logger.Info("连接成功") // ✗ logger.Error("报错了: " + err.Error()) // ✗ fmt.Println() 或 fmt.Errorf() 替代日志库 ``` ### 7.3 关键业务日志埋点 | 操作 | 日志级别 | |------|---------| | 设备创建/修改/删除 | INFO | | 采集点/写入点创建/修改/删除 | INFO | | 写入操作 | INFO | | 采集引擎启停 | INFO | | 设备连接/断开 | WARN | | 重连失败 | WARN | | 协议驱动异常 | ERROR | --- ## 8. 安全规范 ### 8.1 SQL 注入防护 ```go // ✅ 正确:使用参数化查询,并显式声明返回字段 db.QueryContext(ctx, "SELECT id, name FROM devices WHERE name = $1", name) // ✗ 禁止:字符串拼接 SQL ``` ### 8.2 输入校验 | 场景 | 校验规则 | |------|---------| | IP 地址 | 使用 `net.ParseIP()` 或正则校验合法格式 | | 端口号 | 范围 1~65535 | | 时间范围 | 结束时间必须晚于开始时间,最大跨度不超过 31 天 | | 分页 | page >= 1, page_size <= 100 | | 文件名 | 导出的 CSV 文件名不含用户输入(防止路径穿越) | ### 8.3 写入安全 - **写入开关校验**:在 Service 层校验 `write_enabled=true` - **写入来源校验**:校验 `write_source` 与请求 `source` 的匹配关系 - **回读验证**:写入后必须执行回读确认 - **写入权限**:人工写入需登录鉴权;程序自动写入需 API Token ### 8.4 配置安全 - 数据库密码、API Key 等敏感信息通过环境变量注入,**禁止硬编码在代码中** - `.env` 文件加入 `.gitignore`,仅提供 `.env.example` 占位符模板 - `protocol_config` 中不存储凭据类信息 --- ## 9. 代码审核清单 ### 9.1 提交流程前置检查(开发者在提审前自行检查) ``` □ 代码通过 gofmt / goimports / eslint / prettier 格式化 □ 本地 go vet 和 ESLint 检查通过 □ 单元测试通过,且覆盖率满足要求 □ 无被注释掉的旧代码 □ 无 console.log / fmt.Println 等调试输出 □ 无硬编码的敏感信息(密码、token、key) □ 新增能力已复用既有实现或成熟依赖 □ 数据库变更已提供迁移文件 □ 所有 TODO/FIXME 已确认或附带了责任人 ``` ### 9.2 代码审查 Checklist #### 架构层面 | 检查项 | 说明 | |--------|------| | 模块边界是否清晰 | 是否有 Handler 直接调用其他模块的 Repo | | 分层是否遵守 | Handler → Service → Repository 单向依赖 | | 是否引入循环依赖 | 包间依赖不能成环 | | 扩展点是否留好 | 协议驱动是否遵循 ProtocolDriver 接口 | #### 功能层面 | 检查项 | 说明 | |--------|------| | 边界条件是否处理 | 空列表、分页边界、超长输入、逻辑删除记录等 | | 并发安全 | 共享状态的读写是否有锁保护 | | 事务管理 | 跨表操作的 Service 方法是否使用了数据库事务 | #### 性能层面 | 检查项 | 说明 | |--------|------| | N+1 查询 | 列表查询时是否 batch 加载关联数据 | | 索引 | 新增查询条件是否添加了对应的数据库索引 | | 分页 | 列表接口是否都有分页,且 page_size 有上限约束 | | 连接池 | 数据库连接是否使用了连接池 | #### 安全层面 | 检查项 | 说明 | |--------|------| | SQL 注入 | 全库搜索字符串拼接的 SQL 语句 | | 参数校验 | 所有用户输入是否都通过了校验 | | 敏感信息泄露 | 错误信息是否直接返回给前端 | | 路径穿越 | 文件操作的路径是否使用用户输入构造 | #### 前端层面 | 检查项 | 说明 | |--------|------| | API 路径 | 路径是否统一在 `src/api/` 中管理 | | 组件拆分 | 是否合理拆分为可复用组件 | | 状态管理 | 跨组件共享状态是否使用 Pinia | | 错误处理 | API 调用是否有统一的错误处理 | --- ## 附录 A:命名约定 ### A.1 Go 模型命名与数据库字段映射 | 数据库表(snake_case) | Go 结构体(PascalCase) | 说明 | |----------------------|-----------------------|------| | `devices` | `Device` | 设备 | | `collection_points` | `CollectionPoint` | 采集点 | | `write_points` | `WritePoint` | 写入点 | | `write_logs` | `WriteLog` | 写入日志 | Go 结构体字段映射规则: ```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"` // json:"-" 不返回前端 Host string `json:"host" db:"host"` Port int `json:"port" db:"port"` 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`) ### A.2 前端目录与路由命名 | 页面 | 路由路径 | 目录 | |------|---------|------| | 设备管理 | `/data/device` | `views/device/` | | 数据采集 | `/data/collection` | `views/collection/` | | 数据写入 | `/data/write-point` | `views/write-point/` | | 历史数据 | `/history` | `views/history/` | --- > 本文档版本:v1.1 > 最后更新:2026-07-11 > 适用范围:污水厂智能控制系统全部后端(Go)、前端(Vue 3)代码 > > **使用说明**: > 1. 开发者开发新功能前阅读本文档,确保代码风格一致 > 2. 提交 MR/PR 前依据 [第 9 章 代码审核清单](#9-代码审核清单) 逐条自查 > 3. Code Review 时以本文档作为审核标准,不符合规范的要求修改后重新提审 > 4. 本文档随项目推进持续更新,新增模块时补充对应章节