24 KiB
24 KiB
污水厂智能控制系统 — 代码规范
本文档适用于本平台所有模块(数据管理、历史数据、实时监控、智能控制、系统配置等)的代码开发工作。 作为代码审查的依据标准,所有合入主分支的代码必须通过本规范的检查。
目录
- 1. 通用原则
- 2. 项目结构与工程规范
- 3. Go 后端规范
- 4. 前端规范
- 5. RESTful API 规范
- 6. 数据库规范
- 7. 日志与错误处理规范
- 8. 安全规范
- 9. 代码审核清单
- 附录 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 层规范
// ✅ 正确: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 层规范
// ✅ 正确: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 错误处理
// 使用自定义错误类型,而非 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 组件设计规范
<!-- ✅ 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 调用层规范
// 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 响应格式
所有 API 响应使用统一格式:
{
"code": 0,
"message": "success",
"data": { ... }
}
| 字段 | 必填 | 说明 |
|---|---|---|
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 // 固定子路径
查询参数命名(与数据库字段一致):
?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 或类似工具管理,禁止手动修改库结构 |
-- ✅ 正确示例
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 书写规范
-- ✅ 关键字大写,字段名/表名小写
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 日志格式规范
// ✅ 正确:采用结构化键值对
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 注入防护
// ✅ 正确:使用参数化查询,并显式声明返回字段
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 结构体字段映射规则:
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)代码
使用说明:
- 开发者开发新功能前阅读本文档,确保代码风格一致
- 提交 MR/PR 前依据 第 9 章 代码审核清单 逐条自查
- Code Review 时以本文档作为审核标准,不符合规范的要求修改后重新提审
- 本文档随项目推进持续更新,新增模块时补充对应章节