770 lines
29 KiB
Markdown
770 lines
29 KiB
Markdown
# 污水厂智能控制系统 — 代码规范
|
||
|
||
> 本文档适用于本平台所有模块(数据管理、历史数据、实时监控、智能控制、系统配置等)的代码开发工作。
|
||
> 作为代码审查的依据标准,所有合入主分支的代码必须通过本规范的检查。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [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 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
|
||
├── 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. 本文档随项目推进持续更新,新增模块时补充对应章节
|