diff --git a/开发文档/code-standards.md b/开发文档/code-standards.md index 54a88a6..0ee68f9 100644 --- a/开发文档/code-standards.md +++ b/开发文档/code-standards.md @@ -8,17 +8,15 @@ ## 目录 - [1. 通用原则](#1-通用原则) -- [1.4 优先复用,禁止重复造轮子](#14-优先复用禁止重复造轮子) - [2. 项目结构与工程规范](#2-项目结构与工程规范) - [3. Go 后端规范](#3-go-后端规范) - [4. 前端规范](#4-前端规范) - [5. RESTful API 规范](#5-restful-api-规范) - [6. 数据库规范](#6-数据库规范) -- [6.4 连接配置、初始化与重建](#64-连接配置初始化与重建) - [7. 日志与错误处理规范](#7-日志与错误处理规范) - [8. 安全规范](#8-安全规范) - [9. 代码审核清单](#9-代码审核清单) -- [附录 A:模块间关系与命名约定](#附录-a模块间关系与命名约定) +- [附录 A:命名约定](#附录-a命名约定) --- @@ -33,7 +31,6 @@ | **最小惊讶** | 函数的名称和签名应让调用者无需查看实现就能大致猜到行为 | | **防御式编程** | 不信任外部输入,对所有外部输入做校验,但内部调用尽量减少冗余校验 | | **模块自治** | 每个模块有清晰的边界,模块间通过 API 通信,不直接访问其他模块的内部数据 | -| **优先复用** | 先复用已有模块、成熟依赖和平台能力;仅在现有方案确实无法满足需求时才自行实现 | ### 1.2 命名风格对照表 @@ -57,14 +54,12 @@ - **FIXME 注释**:必须附带 issue 编号,如 `// FIXME(#123): 此处有并发安全问题` - **代码删除**:不保留被注释掉的旧代码,直接删除,有需要从 git 历史查找 -### 1.4 优先复用,禁止重复造轮子 +### 1.4 复用约束 -- **先查后写**:新增功能前,必须先检查项目现有代码、已引入依赖、组件库和已封装的公共能力;优先复用 `internal/pkg/`、`web/src/components/`、`web/src/utils/` 与同类模块的实现。 -- **优先级**:项目既有能力 → 官方 SDK / 框架内置能力 → 维护活跃、许可证兼容的成熟开源库 → 自研实现。不得因“实现简单”绕过这一顺序。 -- **禁止重复实现**:禁止自行实现已有成熟方案已覆盖的通用能力,例如鉴权、参数校验、密码哈希、UUID、数据库迁移、HTTP 客户端重试、日期处理、图表、虚拟列表、CSV 编解码和协议编解码。 -- **允许自研的条件**:现有方案无法满足性能、可靠性、许可、安全、离线部署或工业协议兼容性要求时,方可自研;提交中必须说明已调研方案、未采用原因、维护边界和测试方案。 -- **封装边界**:对第三方库的调用应集中在适配层或公共封装中,业务代码不得散落依赖某个供应商的私有 API;不得为薄封装无理由再造通用组件。 -- **审查要求**:PR 必须说明“复用了什么”或“为何不能复用”;无法说明的重复实现不得合入。 +- 新增功能前,必须优先复用 `internal/pkg/`、`web/src/components/`、`web/src/utils/` 中的现有实现及已引入的依赖 +- 复用优先级:项目既有能力 → 官方 SDK / 框架内置能力 → 维护活跃的开源库 → 自研实现 +- **禁止**:自行实现已有成熟方案已覆盖的通用能力,包括但不限于鉴权、参数校验、密码哈希、UUID、数据库迁移、HTTP 客户端重试、日期处理、图表、CSV 编解码和协议编解码 +- 封装边界:对第三方库的调用应集中在适配层或公共封装中,业务代码不得散落依赖某个供应商的私有 API --- @@ -138,24 +133,9 @@ water-plant-control/ └── Makefile # 构建脚本 ``` -### 2.2 新增模块的目录规范 +### 2.2 模块目录与命名约束 -当新增模块(如未来开发"实时监控""智能控制""系统配置")时,遵循以下规范: - -``` -# 后端新增模块,在以下目录各增加一个子包 -internal/ -├── api/monitoring/ # 实时监控 API -├── service/monitoring/ # 实时监控业务逻辑 -├── repository/... # 数据访问 -└── model/ # 数据模型 - -# 前端新增模块,在以下目录增加 -web/src/views/ -└── monitoring/ # 实时监控页面 -``` - -#### 模块级命名约束 +新增模块时,后端在 `internal/` 下各层同步添加子包,前端在 `web/src/views/` 下添加页面目录。 | 模块 | 后端 Go package | 前端目录 | API 路径前缀 | |------|----------------|----------|-------------| @@ -164,16 +144,13 @@ web/src/views/ | 数据管理-写入点 | `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 分支安装依赖。 -- **最小化引入**:不得为单个小功能引入体积过大或功能重叠的依赖;优先按需导入,并删除不再使用的依赖。 +- **版本锁定**:提交依赖变更时必须同时提交锁文件;禁止使用不受控的浮动版本 +- **最小化引入**:不得为单个小功能引入体积过大或功能重叠的依赖 --- @@ -207,14 +184,13 @@ Repository (数据访问、ORM 查询) #### 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 } - // Handler 只做参数校验和响应转换 device, err := h.svc.Create(c.Request.Context(), &req) if err != nil { response.Error(c, err) @@ -223,86 +199,32 @@ func (h *DeviceHandler) Create(c *gin.Context) { response.Success(c, device) } -// ✗ 禁止:Handler 中写业务逻辑 -func (h *DeviceHandler) Create(c *gin.Context) { - // ... 校验参数 - // ... 自己查数据库判断是否重名 ✗ 应该调用 Service - // ... 自己拼 SQL 插入 ✗ 应该调用 Service - // ... 自己写日志 ✗ 应该由 Service 层处理 -} +// ✗ 禁止:Handler 中写业务逻辑或直接操作数据库 ``` #### 3.2.2 Service 层规范 ```go -// ✅ 正确示例 +// ✅ 正确:Service 层专注业务逻辑 func (s *DeviceService) Create(ctx context.Context, req *CreateDeviceRequest) (*Device, error) { - // 1. 参数校验(复杂校验逻辑放在 Service 层) - if err := s.validateDevice(req); err != nil { + // 复杂校验放 Service 层 + if err := s.validate(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. 持久化 + // 调用 Repository if err := s.repo.Create(ctx, device); err != nil { return nil, fmt.Errorf("创建设备失败: %w", err) } - return device, nil } ``` #### 3.2.3 Repository 层规范 -```go -// ✅ 正确示例 -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,避免字符串拼接 -``` +- 使用参数化查询,禁止字符串拼接 SQL +- 显式指定查询字段,禁止 `SELECT *` +- 复杂查询使用 QueryBuilder,避免字符串拼接 +- 简单 CRUD 操作统一封装 BaseRepo ### 3.3 错误处理 @@ -311,7 +233,6 @@ func (r *DeviceRepo) FindByProtocolType(ctx context.Context, protocolType string var ( ErrDeviceNotFound = errors.New("设备不存在") ErrDeviceNameConflict = errors.New("设备名称已存在") - ErrProtocolNotSupport = errors.New("不支持的协议类型") ) // 错误包装:始终携带上下文 @@ -326,22 +247,10 @@ if err != nil { ### 3.4 并发安全 -- 采集引擎(运行时)涉及设备连接池、共享内存状态,必须使用 `sync.Mutex` 或 `sync.RWMutex` 保护 +- 采集引擎涉及设备连接池、共享内存状态,必须使用 `sync.Mutex` 或 `sync.RWMutex` 保护 - 避免裸 `sync.Map`,优先使用 `map + sync.RWMutex` - 协程启动必须可控:使用 `context.Context` + `sync.WaitGroup` 管理生命周期 -- **禁止无限制启动 goroutine**,必须通过带有缓冲 channel 或协程池限制并发数 - -```go -// ✅ 正确示例:采集引擎任务管理 -type CollectorManager struct { - mu sync.RWMutex - connPool map[string]*Connection // 设备连接池 - runners map[string]*PointRunner // 采集点执行器 - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup -} -``` +- **禁止无限制启动 goroutine**,必须通过缓冲 channel 或协程池限制并发数 ### 3.5 单元测试 @@ -354,13 +263,13 @@ type CollectorManager struct { - 测试文件与源码同目录,命名 `*_test.go` - Mock 统一使用 `github.com/stretchr/testify/mock` 或 `go.uber.org/mock` -- 测试数据使用 t.Helper() + t.Cleanup() 管理 +- 测试数据使用 `t.Helper()` + `t.Cleanup()` 管理 --- ## 4. 前端规范 -### 4.1 框架与技术选型(约定) +### 4.1 技术栈约定 | 领域 | 选型 | |------|------| @@ -376,9 +285,8 @@ type CollectorManager struct { ### 4.2 组件设计规范 ```vue - + @@ -407,30 +315,15 @@ const { data, fetchData } = useDeviceDetail(props.deviceId) ``` -### 4.3 API 调用层 +### 4.3 API 调用层规范 ```typescript // 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) { - return request.post('/api/v1/devices', data) +// 每个模块一个 API 文件,所有请求集中在 api/ 目录 +export function fetchDeviceList(params: Record) { + return request.get('/api/v1/devices', { params }) } // ✗ 禁止:在组件中直接调用 axios @@ -442,53 +335,12 @@ export function createDevice(data: Record) { - 图表组件统一封装在 `src/components/charts/` 目录下 - ECharts 的 option 构造使用纯函数,方便测试 - 图表容器使用 `ResizeObserver` 自动适应窗口变化 -- **曲线模式分段自适应 Y 轴**:实现为可复用的工具函数 `src/utils/axis-mapper.ts` -```typescript -// ✅ 工具函数封装示例 -// src/utils/axis-mapper.ts -interface Segment { - start: number - end: number - heightRatio: number // 占Y轴高度的比例 -} +### 4.5 状态管理规范 -export function buildSegments(dataValues: number[]): Segment[] { - // 根据数据分布自动分段 - // ... 实现分段自适应Y轴的映射逻辑 -} - -export function mapValueToPosition(value: number, segments: Segment[]): number { - // 将原始值映射为显示坐标 - // ... -} -``` - -### 4.5 状态与复选框勾选 - -- **勾选状态**:使用 Pinia store 统一管理,支持曲线/表格模式切换时保持状态 - -```typescript -// ✅ 正确示例 -// src/store/selected-points.ts -export const useSelectedPointsStore = defineStore('selected-points', () => { - const selectedIds = ref>(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 } -}) -``` +- 跨组件共享状态使用 Pinia store 统一管理 +- 每个 Store 文件职责单一,按业务模块拆分 +- 禁止在组件中直接修改非本组件的状态 --- @@ -501,7 +353,6 @@ export const useSelectedPointsStore = defineStore('selected-points', () => { 约定: - 资源名使用 kebab-case 复数形式(devices, collection-points, write-points) -- 子资源(如导出/导入)使用 POST 动词 + 路径 - 批量操作使用 POST,非 GET(避免 URL 过长) - 版本号 v1 写在路径中 ``` @@ -511,19 +362,11 @@ export const useSelectedPointsStore = defineStore('selected-points', () => { 所有 API 响应使用统一格式: ```json -// ✅ 成功响应 { "code": 0, "message": "success", "data": { ... } } - -// ✅ 错误响应 -{ - "code": 40001, - "message": "设备名称已存在", - "data": null -} ``` | 字段 | 必填 | 说明 | @@ -575,7 +418,6 @@ export const useSelectedPointsStore = defineStore('selected-points', () => { ``` ✅ /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 @@ -598,10 +440,6 @@ export const useSelectedPointsStore = defineStore('selected-points', () => { | 唯一索引 | 逻辑删除的表使用 `WHERE deleted = FALSE` 的条件唯一索引 | | 外键 | 显式声明 `REFERENCES`,但不启用级联(业务层处理级联逻辑) | | 迁移 | 使用 golang-migrate 或类似工具管理,禁止手动修改库结构 | -| 更新时间 | 通过迁移创建触发器或由仓储层统一维护 `updated_at`,不得依赖调用方遗漏更新 | - -- 每次结构变更必须新增可重复执行的迁移文件,并在空库和包含历史数据的库上验证。 -- 破坏性变更采用“先兼容、再迁移、后删除”的至少两个发布周期策略;确需一次性变更时,必须附带回滚与数据备份方案。 ```sql -- ✅ 正确示例 @@ -635,7 +473,6 @@ CREATE UNIQUE INDEX idx_devices_name ON devices(name) WHERE deleted = FALSE; | 质量戳 | `INT` 类型:`0=good`, `1=bad` | | 值 | 统一使用 `DOUBLE`,BOOL 类型存 0/1 | | 标签 | 元数据信息存为 TAGS,不做查询条件的有选择缓存 | -| KEEP 参数 | 通过系统配置动态调整,通过 `ALTER DATABASE` 执行 | ### 6.3 SQL 书写规范 @@ -649,47 +486,18 @@ LIMIT 20 OFFSET 0; -- ✗ 禁止:SELECT *,必须显式指定字段列表 --- ✅ INSERT 语句 +-- ✅ INSERT 语句:显式指定列名 INSERT INTO devices (name, protocol_type, host, port, protocol_config) VALUES ($1, $2, $3, $4, $5) RETURNING id; ``` -### 6.4 连接配置、初始化与重建 +### 6.4 数据库连接规范 -数据库连接必须通过环境变量或密钥管理服务注入,应用仅读取配置并在启动时执行连通性检查。数据库名称不是默认值,必须由部署环境显式指定;严禁在代码、示例配置或本文档中写入真实密码。 - -```dotenv -# .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 DSN**:6041 为 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_DB` 和 `TDENGINE_DB` 指定的目标库,禁止对系统库、未指定库或生产环境执行;生产环境重建须另行书面审批、备份和恢复演练。 -- 每次重建完成后必须执行迁移状态检查和最小连通性/读写冒烟测试,并记录执行人、时间、目标环境和结果。 +- 数据库连接参数通过环境变量注入,禁止硬编码在代码中 +- 连接 DSN 按 `postgres://USER:PASS@HOST:PORT/DB?sslmode=require` 格式组装 +- Go 后端使用连接池,并在应用启动时通过带超时的 `PingContext` 校验连接 +- 数据库连接配置仅由 `.env.example` 提供占位符模板,`.env` 文件必须加入 `.gitignore` --- @@ -697,27 +505,20 @@ TDENGINE_DB=aquacontrolai ### 7.1 日志级别 -| 级别 | 使用场景 | 示例 | -|------|---------|------| -| DEBUG | 调试信息,仅开发环境 | `DEBUG 采集点曝气池DO_01 采集完成,值=2.35` | -| INFO | 正常运行信息 | `INFO 设备一期曝气柜PLC 连接成功` | -| WARN | 可恢复的异常 | `WARN 设备一期曝气柜PLC 重连第3次失败,将在10秒后重试` | -| ERROR | 需要关注的错误 | `ERROR 写入点加药泵频率 写入失败: 回读值不匹配` | +| 级别 | 使用场景 | +|------|---------| +| DEBUG | 调试信息,仅开发环境 | +| INFO | 正常运行信息 | +| WARN | 可恢复的异常 | +| ERROR | 需要关注的错误 | -### 7.2 日志规范 +### 7.2 日志格式规范 ```go -// ✅ 正确示例 +// ✅ 正确:采用结构化键值对 logger.Info("设备连接成功", "device_id", device.ID, "device_name", device.Name, - "protocol", device.ProtocolType, -) - -logger.Error("写入操作失败", - "point_id", pointID, - "target_value", targetValue, - "error", err.Error(), ) // ✗ 禁止:没有上下文的日志 @@ -728,18 +529,15 @@ logger.Error("写入操作失败", ### 7.3 关键业务日志埋点 -以下业务操作必须记录日志(作为审计追溯依据): - -| 操作 | 日志级别 | 说明 | -|------|---------|------| -| 设备创建/修改/删除 | INFO | 记录操作人和变更内容 | -| 采集点创建/修改/删除 | INFO | 同上 | -| 写入点创建/修改/删除 | INFO | 同上 | -| 写入操作 | INFO | 记录写入值、来源(manual/auto)、操作人、结果 | -| 采集引擎启停 | INFO | 记录启动/停止原因 | -| 设备连接/断开 | WARN | 记录连接耗时或断开原因 | -| 重连失败 | WARN | 记录失败次数 | -| 协议驱动异常 | ERROR | 记录异常堆栈 | +| 操作 | 日志级别 | +|------|---------| +| 设备创建/修改/删除 | INFO | +| 采集点/写入点创建/修改/删除 | INFO | +| 写入操作 | INFO | +| 采集引擎启停 | INFO | +| 设备连接/断开 | WARN | +| 重连失败 | WARN | +| 协议驱动异常 | ERROR | --- @@ -752,7 +550,6 @@ logger.Error("写入操作失败", db.QueryContext(ctx, "SELECT id, name FROM devices WHERE name = $1", name) // ✗ 禁止:字符串拼接 SQL -// db.QueryContext(ctx, "SELECT * FROM devices WHERE name = '" + name + "'") ``` ### 8.2 输入校验 @@ -761,24 +558,22 @@ db.QueryContext(ctx, "SELECT id, name FROM devices WHERE name = $1", name) |------|---------| | IP 地址 | 使用 `net.ParseIP()` 或正则校验合法格式 | | 端口号 | 范围 1~65535 | -| 时间范围 | 结束时间必须晚于开始时间,最大跨度不超过 31 天(防止 TDengine 扫大量数据) | +| 时间范围 | 结束时间必须晚于开始时间,最大跨度不超过 31 天 | | 分页 | page >= 1, page_size <= 100 | | 文件名 | 导出的 CSV 文件名不含用户输入(防止路径穿越) | -| 布尔值 | 在 Go 中由 JSON 解析器自动校验,前端限制 true/false | ### 8.3 写入安全 -- **写入开关校验**:必须在 Service 层校验 `write_enabled=true`,Handler 层只做类型校验 -- **写入来源校验**:校验 `write_source` 与请求 `source` 的匹配关系(manual/auto/both) -- **回读验证**:写入后必须回读确认,实现位置在 Engine 层,Service 层调用 -- **写入权限**:人工写入(`source=manual`)需要登录鉴权;程序自动写入(`source=auto`)需要 API Token +- **写入开关校验**:在 Service 层校验 `write_enabled=true` +- **写入来源校验**:校验 `write_source` 与请求 `source` 的匹配关系 +- **回读验证**:写入后必须执行回读确认 +- **写入权限**:人工写入需登录鉴权;程序自动写入需 API Token ### 8.4 配置安全 -- 数据库密码、API Key 等敏感信息通过环境变量或 vault 注入,**禁止硬编码在代码中** -- `.env` 文件加入 `.gitignore`,仅提供 `.env.example` 模板 +- 数据库密码、API Key 等敏感信息通过环境变量注入,**禁止硬编码在代码中** +- `.env` 文件加入 `.gitignore`,仅提供 `.env.example` 占位符模板 - `protocol_config` 中不存储凭据类信息 -- 本规范、README、Issue、日志、截图和错误响应中均不得记录真实密码、完整 DSN 或 Token;如发生泄露,立即轮换凭据并按安全事件处理 --- @@ -793,9 +588,8 @@ db.QueryContext(ctx, "SELECT id, name FROM devices WHERE name = $1", name) □ 无被注释掉的旧代码 □ 无 console.log / fmt.Println 等调试输出 □ 无硬编码的敏感信息(密码、token、key) -□ 新增能力已复用既有实现或成熟依赖;如无法复用,已说明原因并完成评审 -□ 数据库变更已提供迁移,且未在应用启动流程中执行破坏性重建 -□ 新增 API 已添加至 API 文档 +□ 新增能力已复用既有实现或成熟依赖 +□ 数据库变更已提供迁移文件 □ 所有 TODO/FIXME 已确认或附带了责任人 ``` @@ -805,34 +599,27 @@ db.QueryContext(ctx, "SELECT id, name FROM devices WHERE name = $1", name) | 检查项 | 说明 | |--------|------| -| 模块边界是否清晰 | 是否有跨模块的内部依赖(如 Handler 直接调用其他模块的 Repo) | +| 模块边界是否清晰 | 是否有 Handler 直接调用其他模块的 Repo | | 分层是否遵守 | Handler → Service → Repository 单向依赖 | -| 是否引入循环依赖 | Package A 依赖 Package B,Package B 不应再依赖 Package A | -| 扩展点是否留好 | 协议驱动是否遵循 ProtocolDriver 接口而非直接调用具体实现 | -| 新增模块的目录是否符第 2 节规范 | 按照约定的目录结构添加 | -| 是否重复造轮子 | 是否已经检索并复用项目既有能力、官方 SDK 或成熟依赖;自研是否有充分理由 | +| 是否引入循环依赖 | 包间依赖不能成环 | +| 扩展点是否留好 | 协议驱动是否遵循 ProtocolDriver 接口 | #### 功能层面 | 检查项 | 说明 | |--------|------| -| 业务规则是否覆盖 | 对照 spec 中的业务规则表(R001~R013, H001~H010)逐条确认 | -| 边界条件是否处理 | 空列表、分页边界、超长输入、字段缺失、逻辑删除记录等 | +| 边界条件是否处理 | 空列表、分页边界、超长输入、逻辑删除记录等 | | 并发安全 | 共享状态的读写是否有锁保护 | | 事务管理 | 跨表操作的 Service 方法是否使用了数据库事务 | -| CSV 导入导出 | 编码是否为 UTF-8 with BOM,布尔值和 JSON 字段格式是否正确 | -| 写入流程完整性 | 是否执行了写入→回读验证→记录日志的完整流程 | #### 性能层面 | 检查项 | 说明 | |--------|------| -| N+1 查询 | 列表查询时是否 batch 加载关联数据,而非循环查询 | +| N+1 查询 | 列表查询时是否 batch 加载关联数据 | | 索引 | 新增查询条件是否添加了对应的数据库索引 | | 分页 | 列表接口是否都有分页,且 page_size 有上限约束 | -| 连接池 | 数据库和 PLC 连接是否使用了连接池而非每次新建 | -| 迁移安全 | 结构变更是否可追踪、可验证;破坏性变更是否具备备份、回滚和发布兼容方案 | -| 采集周期最小限制 | collect_interval 是否校验 >= 1 秒 | +| 连接池 | 数据库连接是否使用了连接池 | #### 安全层面 @@ -840,54 +627,23 @@ db.QueryContext(ctx, "SELECT id, name FROM devices WHERE name = $1", name) |--------|------| | SQL 注入 | 全库搜索字符串拼接的 SQL 语句 | | 参数校验 | 所有用户输入是否都通过了校验 | -| 写入权限校验 | `write_enabled` 和 `write_source` 是否在 Service 层校验 | -| 敏感信息泄露 | 错误信息是否直接返回给前端(如数据库密码、SQL 错误) | -| 路径穿越 | 文件操作的路径是否未使用用户输入构造 | +| 敏感信息泄露 | 错误信息是否直接返回给前端 | +| 路径穿越 | 文件操作的路径是否使用用户输入构造 | #### 前端层面 | 检查项 | 说明 | |--------|------| | API 路径 | 路径是否统一在 `src/api/` 中管理 | -| 组件拆分 | 是否合理拆分为可复用组件,而非单文件过长 | +| 组件拆分 | 是否合理拆分为可复用组件 | | 状态管理 | 跨组件共享状态是否使用 Pinia | -| 响应式 | 图表容器是否自适应窗口变化 | -| 错误处理 | API 调用是否有统一的错误处理(如失败提示、加载状态) | -| 勾选上限 | 历史数据点位勾选是否有 20 个上限 | +| 错误处理 | API 调用是否有统一的错误处理 | --- -## 附录 A:模块间关系与命名约定 +## 附录 A:命名约定 -### A.1 模块间依赖关系 - -``` -数据管理模块(基础层) -├── 提供:设备配置、采集点配置、写入点配置 -├── 提供:采集数据写入 TDengine -├── 依赖:PostgreSQL(配置)、TDengine(时序数据) -│ -├── 历史数据模块(消费层)依赖数据管理模块 -│ ├── 读取:采集点配置、设备配置 -│ ├── 读取:TDengine 历史数据 -│ └── 提供:历史数据查询、曲线/表格展示 -│ -├── 实时监控模块(待开发)依赖数据管理模块 -│ ├── 读取:采集点配置、设备配置 -│ ├── 读取:采集引擎内存中的最新值 -│ └── 订阅:MQTT 实时数据通道 -│ -├── 智能控制模块(待开发) -│ ├── 依赖:数据管理模块(设备信息、写入点) -│ ├── 依赖:历史数据模块(AI 训练数据来源) -│ └── 操作:通过写入点对 PLC 下发控制指令 -│ -└── 系统配置模块(待开发) - ├── 负责:全局参数管理,如历史保留天数、采集引擎参数 - └── 被所有模块引用 -``` - -### A.2 Go 模型命名与数据库字段映射 +### A.1 Go 模型命名与数据库字段映射 | 数据库表(snake_case) | Go 结构体(PascalCase) | 说明 | |----------------------|-----------------------|------| @@ -904,11 +660,9 @@ type Device struct { 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"` // 逻辑删除不返回前端 + Deleted bool `json:"-" db:"deleted"` // json:"-" 不返回前端 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"` @@ -920,29 +674,25 @@ type Device struct { 规则: - `json:"-"` 标记的字段不返回前端(如 `deleted`) - `omitempty` 用于可空字段 -- 请求体对应的结构体以 `Request` 结尾(如 `CreateDeviceRequest`) -- 响应体结构体在 Handler 层组装,不直接暴露模型 +- 请求体结构体以 `Request` 结尾(如 `CreateDeviceRequest`) -### A.3 前端目录与路由命名 +### A.2 前端目录与路由命名 -| 页面 | 路由路径 | 目录 | 说明 | -|------|---------|------|------| -| 设备管理 | `/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/` | 待开发 | +| 页面 | 路由路径 | 目录 | +|------|---------|------| +| 设备管理 | `/data/device` | `views/device/` | +| 数据采集 | `/data/collection` | `views/collection/` | +| 数据写入 | `/data/write-point` | `views/write-point/` | +| 历史数据 | `/history` | `views/history/` | --- -> 本文档版本:v1.0 -> 最后更新:2026-07-10 +> 本文档版本:v1.1 +> 最后更新:2026-07-11 > 适用范围:污水厂智能控制系统全部后端(Go)、前端(Vue 3)代码 > > **使用说明**: > 1. 开发者开发新功能前阅读本文档,确保代码风格一致 > 2. 提交 MR/PR 前依据 [第 9 章 代码审核清单](#9-代码审核清单) 逐条自查 > 3. Code Review 时以本文档作为审核标准,不符合规范的要求修改后重新提审 -> 4. 本文档随项目推进持续更新,新增模块时补充对应章节 +> 4. 本文档随项目推进持续更新,新增模块时补充对应章节 \ No newline at end of file