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

770 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 污水厂智能控制系统 — 代码规范
> 本文档适用于本平台所有模块(数据管理、历史数据、实时监控、智能控制、系统配置等)的代码开发工作。
> 作为代码审查的依据标准,所有合入主分支的代码必须通过本规范的检查。
---
## 目录
- [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. 通用原则
> 跨文档契约发生冲突时,必须遵循《[一致性决策基线](一致性决策基线.md)》;该文件的已采纳决策优先于本文旧表述。
### 1.1 核心原则
| 原则 | 说明 |
|------|------|
| **可读性优先** | 代码是写给人类看的,其次才是给机器执行。优先清晰直白,避免晦涩技巧 |
| **一致性** | 同一概念在代码、数据库、API、前端中保持命名一致(如 `device_id` 在所有层统一) |
| **最小惊讶** | 函数的名称和签名应让调用者无需查看实现就能大致猜到行为 |
| **防御式编程** | 不信任外部输入,对所有外部输入做校验,但内部调用尽量减少冗余校验 |
| **模块自治** | 每个模块有清晰的边界。模块间通过公开的 Service/领域接口通信;同一进程内不得直接访问其他模块的 Repository、ORM 模型或数据库表 |
### 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
---
## 2. 项目结构与工程规范
### 2.1 整体目录结构
```
water-plant-control/
├── cmd/ # 可执行程序入口
│ └── server/ # Web 服务端入口(嵌入采集引擎)
│ └── 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 # ProtocolDriverFactory / ProtocolConnection 接口定义
│ │ ├── 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 # 构建脚本
```
运行模型采用**单进程嵌入式**`cmd/server/main.go` 是唯一的生产可执行入口,负责初始化依赖、创建并启动采集引擎、启动 Web API,并在服务关闭时按序停止采集引擎。`internal/engine/collector` 是进程内运行时组件,不提供独立的 `cmd/collector` 进程。
### 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 {
logger.Warn("请求参数绑定失败", "error", err)
response.BadRequest(c, "无效的请求参数", nil)
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 层规范
- 所有用户值使用参数化查询,禁止拼接用户输入
- 显式指定查询字段,禁止 `SELECT *`
- 复杂查询使用 QueryBuilder
- 简单 CRUD 操作统一封装 BaseRepo
- 动态表名仅允许用于 TDengine 子表:必须由已验证 UUID 派生为 `p_<uuid32>`、通过 `^p_[0-9a-f]{32}$` 白名单并使用安全标识符引用;请求不得直接传入表名
#### 3.2.4 模块间调用规范
本项目采用模块化单体:模块间调用使用进程内公开的 Service/领域接口,不通过 HTTP 调用本应用自身。
- 数据访问权归属数据所有模块;只有该模块的 Service 可以调用其 Repository。
- 调用模块依赖数据所有模块公开的只读接口,不得导入、调用或复用其 Repository、ORM 模型及表名。
- 公开接口返回稳定的领域 DTO,不暴露数据库实体;接口的实现与依赖装配由 `cmd/server/main.go` 负责。
- 需要未来拆分为独立服务时,保持接口语义不变,再以 HTTP/gRPC 适配器替换进程内实现。
### 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**,采集调度必须使用有界 worker pool;每个点位是调度任务,不默认占用一个长期 goroutine
- 协议注册表只保存无状态工厂;每个设备持有独立连接实例
- 同一设备连接默认串行读写,采集与人工写入通过设备级互斥或单线程命令队列协调
### 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
<!-- props emit 使用类型定义 -->
<script setup lang="ts">
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
}>()
// 组合式函数提取可复用逻辑
const { data, fetchData } = useDeviceDetail(props.deviceId)
</script>
<template>
<div class="device-detail">
<!-- 模板保持简洁复杂条件逻辑使用计算属性 -->
</div>
</template>
<style scoped>
/* 使用 scoped style,避免全局污染 */
</style>
```
### 4.3 API 调用层规范
```typescript
// src/api/device.ts
import request from '@/utils/request'
// 每个模块一个 API 文件,所有请求集中在 api/ 目录
export function fetchDeviceList(params: Record<string, unknown>) {
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 响应格式
JSON API 使用统一格式:
```json
{
"code": 0,
"message": "success",
"data": { ... }
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `code` | 是 | 0 表示成功,非 0 表示模块业务错误码 |
| `message` | 是 | 稳定、可展示的人类可读描述;不得暴露原始驱动或数据库错误 |
| `data` | 否 | 响应数据,可为 null |
**流式响应例外**:CSV/其他文件下载、SSE 和 WebSocket 成功时直接返回对应的文件或数据流,不使用 JSON 包装;发生错误时仍返回统一 JSON 错误。
同步执行设备操作时,只有实际执行与回读均成功才可返回 HTTP 200 和 `code=0`。设备执行失败或超时必须返回非 2xx HTTP 状态和非零业务码;如已生成操作日志,`data` 返回 `write_log_id`
### 5.3 分页响应格式
```json
{
"code": 0,
"message": "success",
"data": {
"total": 200,
"page": 1,
"page_size": 20,
"items": [ ... ]
}
}
```
### 5.4 错误码规范
HTTP 状态码表达错误类别;响应体 `code` 表达模块内具体原因。不得根据业务码前缀推断 HTTP 状态。
| HTTP 状态 | 类别 | 典型场景 |
|---|---|---|
| 400 | 请求参数错误 | 格式、范围、必填项或时间范围不合法 |
| 401 | 未认证 | token 缺失或失效 |
| 403 | 无权限 | 已认证但无权执行操作 |
| 404 | 资源不存在 | 设备、点位或历史元数据不存在 |
| 409 | 业务冲突 | 名称冲突、不可变字段修改、状态冲突 |
| 413 | 请求或导出结果过大 | 超出行数或文件限制 |
| 422 | 业务校验失败 | 值类型或设备状态不满足执行条件 |
| 502 | 下游设备/数据库执行失败 | PLC 写入失败、TDengine 查询失败 |
| 504 | 下游超时 | PLC 或数据库超时 |
| 500 | 未预期内部错误 | 未分类异常 |
业务码按模块分配:
| 模块 | 范围 |
|------|------|
| 通用、认证与权限 | 10001~10999 |
| 设备管理 | 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` |
| 子表名 | 固定为 `p_<uuid32>`,必须匹配 `^p_[0-9a-f]{32}$` |
| UUID 字段/TAG | 使用带连字符的 36 位标准 UUID |
| 时间戳 | 统一使用 `TIMESTAMP`,精度毫秒 |
| 质量戳 | `INT``0=good`, `1=bad``none` 不落库 |
| 值 | 使用可空 `DOUBLE`,BOOL 存 0/1;读取失败可存 NULL |
| 质量原因 | 可空 `VARCHAR(32)` 稳定代码,例如 `timeout`, `parse_error`, `out_of_range` |
| 标签 | `device_name``data_type` 等为创建子表时的快照,不作为当前配置的权威来源 |
> API 质量枚举统一为 `good | bad | none`。无匹配记录返回固定对象 `{value:null, quality:"none", quality_reason:null, matched_ts:null}`。
### 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 数据库连接规范
- PostgreSQL 和 TDengine 的主机、端口、用户、密码和数据库名均通过环境变量注入,禁止硬编码
- PostgreSQL DSN 按 `postgres://USER:PASS@HOST:PORT/DB?sslmode=require` 格式组装
- TDengine 数据库名使用 `TDENGINE_DATABASE`,默认值可为 `aquacontrolai`
- Go 后端使用连接池,并在应用启动时通过带超时的健康检查校验连接
- `.env.example` 只提供占位符;`.env` 必须加入 `.gitignore`
### 6.5 CSV 导入导出格式规范
CSV 分为两类:
#### 6.5.1 配置型 CSV
用于设备、采集点和写入点导入导出,必须可再次导入。
| 规则 | 说明 |
|------|------|
| 编码 | `UTF-8 with BOM``charset=utf-8-sig` |
| 标题 | 固定字段,使用 `snake_case` |
| 布尔值 | `TRUE` / `FALSE` |
| 数字 | 整数无小数位,浮点数按实际精度输出 |
| JSON 字段 | RFC 4180 转义后的紧凑 JSON 字符串 |
| 空值 | 空字段;导入时按字段规则解释 |
| 日期时间 | `YYYY-MM-DD HH:mm:ss` |
| 导入校验 | 与对应 API 一致,失败行跳过并返回行号与稳定原因 |
#### 6.5.2 报表型 CSV
用于历史数据等面向用户的下载,不要求再次导入。
- 允许本地化标题和动态点位列。
- 重复点位名称追加 UUID 前 8 位,保证列名唯一。
- 缺失或不展示的值使用 `—`,质量列使用 `good | bad | —`
- 文件名由服务端使用已解析参数重新格式化,只允许 ASCII 字母、数字、下划线、短横线和点。
- 文件扩展名统一为 `.csv`
---
## 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 |
| 设备连接成功 | INFO |
| 设备断开或连接失败 | WARN |
| 重连失败 | WARN |
| 协议驱动异常 | ERROR |
---
## 8. 安全规范
### 8.1 SQL 注入防护
```go
// ✅ 用户值使用参数化查询
db.QueryContext(ctx, "SELECT id, name FROM devices WHERE name = $1", name)
```
TDengine 动态子表名按以下流程处理:
1. 解析并验证标准 UUID
2. 转为小写、移除连字符并加 `p_`
3. 校验 `^p_[0-9a-f]{32}$`
4. 使用驱动的安全标识符引用能力构造 SQL;
5. 时间和值条件继续使用参数绑定。
禁止把请求参数、点位名称、分组名称或任意未验证字符串拼入 SQL。
### 8.2 输入校验
| 场景 | 校验规则 |
|------|---------|
| IP 地址 | 使用 `net.ParseIP()` 或正则校验合法格式 |
| 端口号 | 范围 1~65535 |
| 时间范围 | 结束时间必须晚于开始时间,最大跨度不超过 31 天 |
| 曲线结果 | `max_samples` 范围 100~10000,默认 2000 |
| 分页 | page >= 1, page_size <= 100 |
| 文件名 | 不直接拼接原始用户输入;解析后由服务端重新格式化为安全文件名 |
### 8.3 写入安全
- **写入开关校验**:在 Service 层校验 `write_enabled=true`
- **回读验证**:写入后必须执行回读确认
- **人工写入记录**:当前阶段写入接口仅支持人工写入;Service 固定记录 `source=manual``operator=null`,请求体不得传入这两个字段
- **访问边界**:未实施身份认证前,写入 API 只允许部署在受信任的内部网络;接入身份认证后再补充权限与操作者归属
### 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
#### 架构层面
| 检查项 | 说明 |
|--------|------|
| 模块边界是否清晰 | 是否有跨模块直接调用 Repository、ORM 模型或访问其他模块的数据表 |
| 分层是否遵守 | Handler → Service → Repository 单向依赖 |
| 是否引入循环依赖 | 包间依赖不能成环 |
| 扩展点是否留好 | 协议驱动是否使用 Factory/Connection 模型,且每设备连接隔离 |
#### 功能层面
| 检查项 | 说明 |
|--------|------|
| 边界条件是否处理 | 空列表、分页边界、超长输入、逻辑删除记录等 |
| 并发安全 | 共享状态的读写是否有锁保护 |
| 事务管理 | 跨表操作的 Service 方法是否使用了数据库事务 |
#### 性能层面
| 检查项 | 说明 |
|--------|------|
| N+1 查询 | 列表查询时是否 batch 加载关联数据 |
| 索引 | 新增查询条件是否添加了对应的数据库索引 |
| 分页/采样 | 列表是否分页;历史曲线是否有 max_samples 和降采样 |
| 连接池 | 数据库连接是否使用了连接池 |
#### 安全层面
| 检查项 | 说明 |
|--------|------|
| SQL 注入 | 用户值是否参数化;动态子表名是否只由 UUID 派生并通过白名单 |
| 参数校验 | 所有用户输入是否都通过了校验 |
| 敏感信息泄露 | 错误信息是否直接返回给前端 |
| 路径穿越 | 文件操作的路径是否使用用户输入构造 |
#### 前端层面
| 检查项 | 说明 |
|--------|------|
| 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.2
> 最后更新:2026-07-11
> 适用范围:污水厂智能控制系统全部后端(Go)、前端(Vue 3)代码
>
> **使用说明**
> 1. 开发者开发新功能前阅读本文档,确保代码风格一致
> 2. 提交 MR/PR 前依据 [第 9 章 代码审核清单](#9-代码审核清单) 逐条自查
> 3. Code Review 时以本文档作为审核标准,不符合规范的要求修改后重新提审
> 4. 本文档随项目推进持续更新,新增模块时补充对应章节