[优化] 代码优化

This commit is contained in:
ryan
2026-05-31 13:37:40 +08:00
parent c2fcd2eddf
commit 8894620b92
12 changed files with 125 additions and 57 deletions
+9 -4
View File
@@ -14,10 +14,13 @@
4. [docs/design/development.md](./docs/design/development.md)
作用:理解当前开发规范、阶段原则、分层约束、数据模型边界、API 约定、Agent 约束、前端规范与测试要求。
5. [docs/guide/deployment.md](./docs/guide/deployment.md)
5. [docs/guildline/](./docs/guildline/) 下的所有开发准则文件
作用:通用代码开发准则与特定项目开发准则(包含通用 Go 设计模式、并发安全、数据库事务约束、参数解析解析响应规范、Utils 与 GORM 彻底隔离、Slice 去重及 JSON 序列化避坑细则)。
6. [docs/guide/deployment.md](./docs/guide/deployment.md)
作用:理解当前部署方式、Agent 接入、升级、卸载和联调步骤。
6. [docs/reference/configuration.md](./docs/reference/configuration.md)
7. [docs/reference/configuration.md](./docs/reference/configuration.md)
作用:理解系统启动时支持的环境变量、命令行参数、运行时配置项和 Agent 配置字段。
如任务涉及用户文档、贡献者入口或排障体验,还应阅读:
@@ -33,17 +36,19 @@
* 如果实现内容超出 [产品边界](./docs/design/index.md),先修改设计文档,再继续编码。
* 如果实现方式违反 [开发约束](./docs/design/development.md),应优先调整方案,而不是绕过规范。
* 如果实现方式涉及后端代码逻辑,必须严格遵循 [docs/guildline/](./docs/guildline/) 下的所有开发准则。
* 如果需求与当前阶段原则冲突,优先遵守 [开发约束](./docs/design/development.md) 中的变更准入与验收标准。
* 如果任务涉及前端改造或管理端 UI,必须同时遵守 [开发约束](./docs/design/development.md) 中的前端规范。
## 文档维护要求
当以下内容发生变化时,应同步更新对应 VitePress 页面:
当以下内容发生变化时,应同步更新对应中文文档, 不要同步英文文档:
* 产品范围或系统边界变化:更新 `docs/design/index.md`
* 系统结构、模块职责变化:更新 `docs/design/architecture.md`
* 发布、同步、回滚模型变化:更新 `docs/design/release-model.md`
* 开发约束、代码规范、接口约定、阶段原则、测试基线变化:更新 `docs/design/development.md`
* 业务分层、数据模型边界、接口约定、阶段原则、测试基线变化:更新 `docs/design/development.md`
* 后端开发规范、代码质量要求、重构模式、去重逻辑与避坑指南变化:更新 `docs/guildline/` 下的对应开发准则文件
* 产品启动、部署、升级、联调方式变化:更新 `docs/guide/quick-start.md`、`docs/guide/deployment.md` 和 `README.md`
* 用户操作路径、常见场景变化:更新 `docs/guide/usage.md`
* 本地开发、测试、构建方式变化:更新 `docs/guide/development.md`
+57
View File
@@ -0,0 +1,57 @@
# OpenFlare 特定项目开发准则 (Project Guidelines)
本文档定义了针对 **OpenFlare** 项目特定的后端开发约束、架构设计模式、GORM 数据库交互规范以及关键的 JSON 序列化避坑指南。所有参与项目后端开发的代码必须严格遵守。
---
## 1. 统一接口输入与响应处理(Controller 约束)
为了保证 API 的一致性,并消除控制器层中大量的样板代码,所有 Gin Controller 必须遵守以下规范:
### 1.1 参数解析与绑定
- **URL ID 参数解析**:必须调用统一的 `parseIDParam(c)` 辅助函数。严禁手写 `strconv.ParseUint(c.Param("id"), ...)`。
- **JSON 请求体绑定**:必须调用统一的 `bindJSON(c, &input)` 辅助函数。严禁手动调用 `c.ShouldBindJSON` 或 `json.NewDecoder` 并重复编写错误返回逻辑。
### 1.2 标准 API 响应
- 所有控制器方法的返回必须统一使用 `respondSuccess`、`respondFailure`、`respondBadRequest` 等标准方法。
- **严禁手写** `c.JSON(http.StatusOK, gin.H{...})`,以确保全局 API 响应字段结构(`success`/`message`/`data`)的百分之百一致。
> [!IMPORTANT]
> 接口的入参解析与响应统一规范定义在 [openflare_server/controller/response.go](file:///Users/ryan/DEV/Go/OpenFlare/openflare_server/controller/response.go) 中。
---
## 2. 纯净工具类与数据库逻辑完全隔离(Utils 约束)
为了确保代码的可测试性、高内聚和低耦合,`utils/` 目录下的工具包必须保持纯净性:
### 2.1 无副作用与解耦原则
- 所有底层客户端与外部服务对接包(如 `utils/acme` 证书操作、邮件发送、DNS 供应商对接等)**必须完全剥离数据库或 GORM 依赖**。
- 工具包中严禁导入 `openflare/model` 包或直接访问数据库连接。它们应当只接受基础数据类型(如 `string`、`[]byte` 等)或本地无依赖结构体作为输入,并返回纯粹的计算或请求结果。
### 2.2 业务服务层(Service)职责
- 业务服务层 `service/` 负责数据库实体的加载、组装、事务持久化,并将底层的具体网络或加密操作委托给 `utils/` 工具包。
- 这样不仅保证了底层工具类的百分之百可单元测试性,也维护了清晰的系统分层。
---
## 3. Go 泛型切片去重与 JSON 序列化陷阱(Slice 约束)
在进行切片操作和去重时,必须使用泛型辅助函数,并注意 Go Slice 的空/零值在 JSON 序列化中的表现。
### 3.1 避免重复编写 map-seen 逻辑
- 禁止在 `service/` 或 `model/` 中手写临时的 map-seen 去重样板代码。
- 必须统一调用基于 Go 泛型实现的 [openflare_server/utils/slice.go](file:///Users/ryan/DEV/Go/OpenFlare/openflare_server/utils/slice.go) 中的 `utils.Unique()` 辅助函数。
### 3.2 关键的 JSON 序列化规则(Nil vs. Empty Slice)
在 Go 中,未初始化的 `nil` 切片和已初始化的空切片 `[]T{}` 在内存中不同,它们在序列化为 JSON 时也有着决定性的区别:
- **`nil` 切片**:序列化为 JSON `null`。
- **空切片 (`make([]T, 0)`)**:序列化为 JSON `[]`。
> [!CAUTION]
> **开发避坑准则**:
> 1. GORM 数据库的很多 JSON/Array 字段(例如 `domain_cert_ids`、`upstreams` 等)或配置版本变更检测机制(如 `checksum` 计算和 `diff` 检测),要求空数组在 JSON 中必须表示为 `[]` 而非 `null`,否则会触发重复发布或解析失败的 bug。
> 2. `utils.Unique` 必须具备 **Nil-Preservation(空值保留)** 特性:
> - 如果传入的 Slice 是 `nil`,它必须返回 `nil`,以支持 `omitempty` 或在需要表示“缺失”的场景中输出 `null`。
> - 如果传入的 Slice 不是 `nil`(即使长度为 0 或去重后长度为 0),它必须返回非 nil 的空切片 `make([]T, 0)`,以确保序列化为 `[]`。
> 3. 所有类似的切片加工辅助函数都必须遵循此行为。
+8
View File
@@ -11,6 +11,7 @@ import (
"openflare/utils/security"
"os"
"reflect"
"strings"
"sync"
)
@@ -313,3 +314,10 @@ func CloseDB() error {
err = sqlDB.Close()
return err
}
func IsUniqueConstraintError(err error) bool {
if err == nil {
return false
}
return strings.Contains(strings.ToLower(err.Error()), "unique")
}
+10 -1
View File
@@ -412,7 +412,7 @@ func PublishConfigVersion(createdBy string, force bool) (*ReleaseResult, error)
return nil
})
if err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("版本号生成冲突,请重试")
}
return nil, err
@@ -1095,6 +1095,9 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot,
builder.WriteString(renderNamedUpstreamBlock(upstreamConfig))
}
powEnabled, _ := getPoWConfigForRoute(route.ID, wafSnapshot)
if route.PoWEnabled {
powEnabled = true
}
if !route.EnableHTTPS {
builder.WriteString(renderHTTPProxyServer(serverNames, displayName, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, powEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg))
continue
@@ -1849,6 +1852,12 @@ func renderPowConfigBundle(routes []*model.ProxyRoute, wafSnapshot snapshotWAFDo
hasPow := false
for _, route := range routes {
powEnabled, powConfig := getPoWConfigForRoute(route.ID, wafSnapshot)
if route.PoWEnabled {
powEnabled = true
if decoded, err := decodeStoredPoWConfig(route.PoWEnabled, route.PoWConfig); err == nil {
powConfig = decoded
}
}
if !powEnabled {
continue
}
+2 -2
View File
@@ -46,7 +46,7 @@ func CreateManagedDomain(input ManagedDomainInput) (*model.ManagedDomain, error)
return nil, err
}
if err = domain.Insert(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("域名已存在")
}
return nil, err
@@ -64,7 +64,7 @@ func UpdateManagedDomain(id uint, input ManagedDomainInput) (*model.ManagedDomai
return nil, err
}
if err = domain.Update(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("域名已存在")
}
return nil, err
+2 -2
View File
@@ -83,7 +83,7 @@ func CreateNode(input NodeInput) (*NodeView, error) {
applyGeoInfoFromIP(node, node.IP)
}
if err := node.Insert(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("节点标识生成冲突,请重试")
}
return nil, err
@@ -454,7 +454,7 @@ func RegisterNodeWithDiscovery(payload AgentNodePayload) (*AgentRegistrationResp
}
applyNodeRuntime(node, payload, false)
if err = node.Insert(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("节点标识生成冲突,请重试")
}
return nil, err
+3 -3
View File
@@ -88,7 +88,7 @@ func CreateOrigin(input OriginInput) (*model.Origin, error) {
return nil, err
}
if err = origin.Insert(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("源站地址已存在")
}
return nil, err
@@ -108,7 +108,7 @@ func UpdateOrigin(id uint, input OriginInput) (*model.Origin, error) {
}
err = model.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Save(nextOrigin).Error; err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return errors.New("源站地址已存在")
}
return err
@@ -171,7 +171,7 @@ func getOrCreateOriginByAddress(address string) (*model.Origin, error) {
Remark: "",
}
if err := origin.Insert(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return model.GetOriginByAddress(normalizedAddress)
}
return nil, err
+5 -20
View File
@@ -7,6 +7,7 @@ import (
"net"
"net/url"
"openflare/model"
"openflare/utils"
"regexp"
"strings"
"time"
@@ -121,7 +122,7 @@ func CreateProxyRoute(input ProxyRouteInput) (*ProxyRouteView, error) {
return nil, err
}
if err = route.Insert(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("proxy route identity already exists")
}
return nil, err
@@ -139,7 +140,7 @@ func UpdateProxyRoute(id uint, input ProxyRouteInput) (*ProxyRouteView, error) {
return nil, err
}
if err = route.Update(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("proxy route identity already exists")
}
return nil, err
@@ -438,7 +439,6 @@ func normalizeProxyRouteDomainsInput(route *model.ProxyRoute, rawDomain string,
func normalizeProxyRouteDomains(rawDomains []string) ([]string, error) {
normalized := make([]string, 0, len(rawDomains))
seen := make(map[string]struct{}, len(rawDomains))
for _, rawDomain := range rawDomains {
domain := normalizeProxyRouteDomainValue(rawDomain)
if domain == "" {
@@ -447,12 +447,9 @@ func normalizeProxyRouteDomains(rawDomains []string) ([]string, error) {
if strings.Contains(domain, "://") || strings.Contains(domain, "/") {
return nil, errors.New("domain format is invalid")
}
if _, ok := seen[domain]; ok {
continue
}
seen[domain] = struct{}{}
normalized = append(normalized, domain)
}
normalized = utils.Unique(normalized)
if len(normalized) == 0 {
return nil, errors.New("at least one domain is required")
}
@@ -850,15 +847,7 @@ func normalizeUpstreams(originURL string, upstreams []string) ([]string, error)
}
trimmed = append(trimmed, item)
}
unique := make([]string, 0, len(trimmed))
seen := make(map[string]struct{}, len(trimmed))
for _, item := range trimmed {
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
unique = append(unique, item)
}
unique := utils.Unique(trimmed)
normalized := make([]string, 0, len(unique))
var scheme string
multiUpstream := len(unique) > 1
@@ -1091,10 +1080,6 @@ func validateOriginHost(raw string) error {
return nil
}
func isUniqueConstraintError(err error) bool {
return err != nil && strings.Contains(strings.ToLower(err.Error()), "unique")
}
// PoW configuration types and validation
type ProxyRoutePoWListConfig struct {
+5 -5
View File
@@ -105,7 +105,7 @@ func CreateTLSCertificate(input TLSCertificateInput) (*model.TLSCertificate, err
return nil, err
}
if err = certificate.Insert(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("certificate name already exists")
}
return nil, err
@@ -144,7 +144,7 @@ func UpdateTLSCertificate(id uint, input TLSCertificateInput) (*model.TLSCertifi
return nil, err
}
if err = certificate.Update(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("certificate name already exists")
}
return nil, err
@@ -219,7 +219,7 @@ func ApplyTLSCertificate(input TLSApplyInput) (*model.TLSCertificate, error) {
}
if err := cert.Insert(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("certificate name already exists")
}
return nil, err
@@ -261,7 +261,7 @@ func UpdateAcmeCertificate(id uint, input TLSApplyInput) (*model.TLSCertificate,
cert.ApplyStatus = "applying"
if err := cert.Update(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("certificate name already exists")
}
return nil, err
@@ -308,7 +308,7 @@ func ConvertTLSCertificateToAcme(id uint, input TLSApplyInput) (*model.TLSCertif
cert.ApplyMessage = ""
if err := cert.Update(); err != nil {
if isUniqueConstraintError(err) {
if model.IsUniqueConstraintError(err) {
return nil, errors.New("certificate name already exists")
}
return nil, err
+5 -20
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"net/netip"
"openflare/model"
"openflare/utils"
"sort"
"strings"
"time"
@@ -413,7 +414,6 @@ func loadWAFBindings() (map[uint][]uint, error) {
func normalizeWAFIPList(items []string) ([]string, error) {
normalized := make([]string, 0, len(items))
seen := make(map[string]struct{}, len(items))
for _, raw := range items {
item := strings.TrimSpace(raw)
if item == "" {
@@ -432,19 +432,15 @@ func normalizeWAFIPList(items []string) ([]string, error) {
}
item = addr.String()
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
normalized = append(normalized, item)
}
normalized = utils.Unique(normalized)
sort.Strings(normalized)
return normalized, nil
}
func normalizeWAFCountryList(items []string) ([]string, error) {
normalized := make([]string, 0, len(items))
seen := make(map[string]struct{}, len(items))
for _, raw := range items {
item := strings.ToUpper(strings.TrimSpace(raw))
if item == "" {
@@ -453,30 +449,23 @@ func normalizeWAFCountryList(items []string) ([]string, error) {
if len(item) != 2 || !unicode.IsLetter(rune(item[0])) || !unicode.IsLetter(rune(item[1])) {
return nil, fmt.Errorf("%s 不是合法国家代码", item)
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
normalized = append(normalized, item)
}
normalized = utils.Unique(normalized)
sort.Strings(normalized)
return normalized, nil
}
func normalizeStringList(items []string) []string {
normalized := make([]string, 0, len(items))
seen := make(map[string]struct{}, len(items))
for _, raw := range items {
item := strings.TrimSpace(raw)
if item == "" {
continue
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
normalized = append(normalized, item)
}
normalized = utils.Unique(normalized)
sort.Strings(normalized)
return normalized
}
@@ -518,18 +507,14 @@ func normalizeWAFRuleGroupIDs(groupIDs []uint) ([]uint, error) {
}
func uniqueUintIDs(ids []uint) []uint {
seen := make(map[uint]struct{}, len(ids))
normalized := make([]uint, 0, len(ids))
for _, id := range ids {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
normalized = append(normalized, id)
}
normalized = utils.Unique(normalized)
sort.Slice(normalized, func(i, j int) bool { return normalized[i] < normalized[j] })
return normalized
}
+19
View File
@@ -0,0 +1,19 @@
package utils
// Unique returns a new slice containing only the unique elements of the input slice,
// preserving their original order.
func Unique[T comparable](slice []T) []T {
if slice == nil {
return nil
}
seen := make(map[T]struct{})
result := make([]T, 0)
for _, item := range slice {
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
result = append(result, item)
}
return result
}