后端与全仓代码质量清理(golangci 扩展集 · 测试质量 · 并发安全 · 文档同步)

代码质量全量清理,零行为变化:golangci 扩展集 13 类 linter(gosec/modernize/perfsprint/canonicalheader/usestdlibvars/wastedassign/intrange/errorlint/forcetypeassert/recvcheck/exhaustive/unparam)全量修复,测试代码质量(testifylint/thelper/usetesting)25→0,frpc 进程生命周期真 bug(进程组击杀)、全仓 go test -race 6 类数据竞争(含 1 个生产竞争)、SPDX license 头补齐 131 文件、前端测试套件 next-intl 迁移后 44 失败→全绿、过期 swagger 文档重新生成、pnpm-workspace 构建审批。

Experiments: #2-#17, #18, #20, #21, #23
Metric: total_issues 108 → 8 (-92.6%)
This commit is contained in:
ryan
2026-08-16 21:23:37 +08:00
parent 5a8722ff07
commit 2f60329886
292 changed files with 1362 additions and 793 deletions
+26 -41
View File
@@ -50,9 +50,9 @@ type GetTableDataRequest struct {
// TableDataResponse 动态数据表响应结构体
type TableDataResponse struct {
Columns []string `json:"columns"`
Total int64 `json:"total"`
Results []map[string]interface{} `json:"results"`
Columns []string `json:"columns"`
Total int64 `json:"total"`
Results []map[string]any `json:"results"`
}
// ExecuteSQLRequest 执行自定义 SQL 请求结构体
@@ -62,11 +62,11 @@ type ExecuteSQLRequest struct {
// ExecuteSQLResponse 执行自定义 SQL 响应结构体
type ExecuteSQLResponse struct {
Type string `json:"type"` // "select" 或 "exec"
Columns []string `json:"columns,omitempty"`
Results []map[string]interface{} `json:"results,omitempty"`
AffectedRows int64 `json:"affected_rows"`
ExecutionTimeMs int64 `json:"execution_time_ms"`
Type string `json:"type"` // "select" 或 "exec"
Columns []string `json:"columns,omitempty"`
Results []map[string]any `json:"results,omitempty"`
AffectedRows int64 `json:"affected_rows"`
ExecutionTimeMs int64 `json:"execution_time_ms"`
}
// formatBytes 格式化字节大小为可读字符串
@@ -103,7 +103,7 @@ func formatBytes(bytes uint64) string {
}
// getSQLiteOverview 获取 SQLite 数据库概览信息
func getSQLiteOverview(gormDB *gorm.DB) (DBOverviewResponse, error) {
func getSQLiteOverview(gormDB *gorm.DB) DBOverviewResponse {
name := config.Config.Database.SQLitePath
if name == "" {
name = "./data/openflare.db"
@@ -119,10 +119,7 @@ func getSQLiteOverview(gormDB *gorm.DB) (DBOverviewResponse, error) {
var sizeStr string
if fi, err := os.Stat(name); err == nil {
size := fi.Size()
if size < 0 {
size = 0
}
size := max(fi.Size(), 0)
sizeStr = formatBytes(uint64(size))
} else {
sizeStr = "0 B"
@@ -147,11 +144,11 @@ func getSQLiteOverview(gormDB *gorm.DB) (DBOverviewResponse, error) {
Size: sizeStr,
TableCount: tableCount,
Connections: connCount,
}, nil
}
}
// getPostgresOverview 获取 PostgreSQL 数据库概览信息
func getPostgresOverview(gormDB *gorm.DB) (DBOverviewResponse, error) {
func getPostgresOverview(gormDB *gorm.DB) DBOverviewResponse {
name := config.Config.Database.Database
var version string
@@ -165,10 +162,7 @@ func getPostgresOverview(gormDB *gorm.DB) (DBOverviewResponse, error) {
var sizeStr string
var sizeBytes sql.NullInt64
if err := gormDB.Raw("SELECT pg_database_size(current_database())").Scan(&sizeBytes).Error; err == nil && sizeBytes.Valid {
size := sizeBytes.Int64
if size < 0 {
size = 0
}
size := max(sizeBytes.Int64, 0)
sizeStr = formatBytes(uint64(size))
} else {
sizeStr = "0 B"
@@ -198,7 +192,7 @@ func getPostgresOverview(gormDB *gorm.DB) (DBOverviewResponse, error) {
Size: sizeStr,
TableCount: tableCount,
Connections: connCount,
}, nil
}
}
// GetDBOverview 获取数据库运行概览
@@ -220,17 +214,11 @@ func GetDBOverview(c *gin.Context) {
}
var overview DBOverviewResponse
var err error
if !config.Config.Database.Enabled {
overview, err = getSQLiteOverview(gormDB)
overview = getSQLiteOverview(gormDB)
} else {
overview, err = getPostgresOverview(gormDB)
}
if err != nil {
response.AbortInternal(c, err.Error())
return
overview = getPostgresOverview(gormDB)
}
c.JSON(http.StatusOK, response.OK(overview))
@@ -294,10 +282,7 @@ func GetDBTableData(c *gin.Context) {
return
}
offset := (req.Page - 1) * req.PageSize
if offset < 0 {
offset = 0
}
offset := max((req.Page-1)*req.PageSize, 0)
limit := req.PageSize
if limit <= 0 {
limit = 10
@@ -332,11 +317,11 @@ func GetDBTableData(c *gin.Context) {
}
// scanTableRows 扫描并提取数据表行数据,做截断处理
func scanTableRows(rows *sql.Rows, cols []string) ([]map[string]interface{}, error) {
results := make([]map[string]interface{}, 0)
func scanTableRows(rows *sql.Rows, cols []string) ([]map[string]any, error) {
results := make([]map[string]any, 0)
for rows.Next() {
columns := make([]interface{}, len(cols))
columnPointers := make([]interface{}, len(cols))
columns := make([]any, len(cols))
columnPointers := make([]any, len(cols))
for i := range columns {
columnPointers[i] = &columns[i]
}
@@ -345,7 +330,7 @@ func scanTableRows(rows *sql.Rows, cols []string) ([]map[string]interface{}, err
return nil, err
}
rowMap := make(map[string]interface{})
rowMap := make(map[string]any)
for i, colName := range cols {
val := columns[i]
if b, ok := val.([]byte); ok {
@@ -385,10 +370,10 @@ func executeSQLQuery(gormDB *gorm.DB, sqlStr string, startTime time.Time) (Execu
return ExecuteSQLResponse{}, err
}
results := make([]map[string]interface{}, 0)
results := make([]map[string]any, 0)
for rows.Next() {
columns := make([]interface{}, len(cols))
columnPointers := make([]interface{}, len(cols))
columns := make([]any, len(cols))
columnPointers := make([]any, len(cols))
for i := range columns {
columnPointers[i] = &columns[i]
}
@@ -397,7 +382,7 @@ func executeSQLQuery(gormDB *gorm.DB, sqlStr string, startTime time.Time) (Execu
return ExecuteSQLResponse{}, err
}
rowMap := make(map[string]interface{})
rowMap := make(map[string]any)
for i, colName := range cols {
val := columns[i]
if b, ok := val.([]byte); ok {
+2 -2
View File
@@ -56,11 +56,11 @@ func GetLogs(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "200")
var cursor, limit int
if _, err := parsePositiveInt(cursorStr, &cursor); err != nil {
if err := parsePositiveInt(cursorStr, &cursor); err != nil {
response.AbortWithError(c, http.StatusBadRequest, admin.InvalidCursorParam)
return
}
if _, err := parsePositiveInt(limitStr, &limit); err != nil || limit <= 0 {
if err := parsePositiveInt(limitStr, &limit); err != nil || limit <= 0 {
limit = defaultLimit
}
if limit > maxLimit {
+6 -6
View File
@@ -35,8 +35,8 @@ func getUpgrader() *websocket.Upgrader {
ctx := r.Context()
if sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress); err == nil && sc.Value != "" {
originToCheck := strings.TrimRight(strings.TrimSpace(origin), "/")
allowedOrigins := strings.Split(sc.Value, ",")
for _, allowed := range allowedOrigins {
allowedOrigins := strings.SplitSeq(sc.Value, ",")
for allowed := range allowedOrigins {
allowed = strings.TrimRight(strings.TrimSpace(allowed), "/")
if allowed != "" && strings.EqualFold(allowed, originToCheck) {
return true
@@ -49,15 +49,15 @@ func getUpgrader() *websocket.Upgrader {
}
// parsePositiveInt 解析非负整数字符串
func parsePositiveInt(s string, result *int) (bool, error) {
func parsePositiveInt(s string, result *int) error {
if s == "" {
*result = 0
return true, nil
return nil
}
n, err := strconv.Atoi(s)
if err != nil || n < 0 {
return false, err
return err
}
*result = n
return true, nil
return nil
}
+1
View File
@@ -16,6 +16,7 @@ import (
)
func setupTestDB(t *testing.T) *gorm.DB {
t.Helper()
dbConn, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("failed to open sqlite in memory: %v", err)
@@ -3,6 +3,8 @@
package push
import "slices"
import "sync"
const (
@@ -66,13 +68,7 @@ func ListDefinitions() []Definition {
}
// Add any others
for t, d := range definitions {
found := false
for _, o := range order {
if o == t {
found = true
break
}
}
found := slices.Contains(order, t)
if !found {
res = append(res, d)
}
+2 -3
View File
@@ -9,6 +9,7 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"strings"
"github.com/Rain-kl/Wavelet/internal/infra/task"
@@ -34,9 +35,7 @@ func (m NotificationMessage) Flatten() map[string]any {
keyContent: m.Content,
keyLevel: m.Level,
}
for k, v := range m.Ext {
res[k] = v
}
maps.Copy(res, m.Ext)
return res
}
+13 -12
View File
@@ -65,6 +65,7 @@ func (m *mockPusher) ValidateConfig(cfg pkgpush.Config) error {
}
func setupPushTest(t *testing.T) (*gorm.DB, *miniredis.Miniredis, func()) {
t.Helper()
dbConn, mr, cleanup := testhelper.SetupTestEnvironment(t)
// AutoMigrate push tables in SQLite test environment
@@ -319,7 +320,7 @@ func TestPushHandler(t *testing.T) {
assert.Equal(t, "Structured Alert", mPusher.sentBody["title"])
assert.Equal(t, "Hello World", mPusher.sentBody["content"])
assert.Equal(t, "WARNING", mPusher.sentBody["level"])
assert.Equal(t, float64(42), mPusher.sentBody["extra_val"]) // unmarshaled json numbers are float64 by default
assert.InDelta(t, float64(42), mPusher.sentBody["extra_val"], 1e-9) // unmarshaled json numbers are float64 by default
mPusher.mu.Unlock()
// Verify PushHistory recorded
@@ -436,7 +437,7 @@ func TestPushRouters(t *testing.T) {
dataMap, ok := resp.Data.(map[string]any)
assert.True(t, ok)
assert.Equal(t, float64(1), dataMap["total"])
assert.InDelta(t, float64(1), dataMap["total"], 1e-9)
})
t.Run("test push endpoint", func(t *testing.T) {
@@ -614,37 +615,37 @@ func TestPushChannelAPI(t *testing.T) {
t.Run("validate push channel model constraints", func(t *testing.T) {
// 校验名称合法性
c1 := &model.PushChannel{Name: "invalid-name!", URL: "https://hook.com", Other: "{}"}
assert.Error(t, c1.Validate())
require.Error(t, c1.Validate())
// 校验 URL 安全前缀 HTTPS
c2 := &model.PushChannel{Name: "custom_channel", URL: "http://insecure-hook.com", Other: "{}"}
assert.Error(t, c2.Validate())
require.Error(t, c2.Validate())
// 校验 JSON 格式
c3 := &model.PushChannel{Name: "custom_channel", URL: "https://hook.com", Other: "{invalid-json}"}
assert.Error(t, c3.Validate())
require.Error(t, c3.Validate())
// 正确配置
c4 := &model.PushChannel{Name: "custom_channel", URL: "https://hook.com", Other: "{\"content\":\"$content\"}"}
assert.NoError(t, c4.Validate())
require.NoError(t, c4.Validate())
// 飞书渠道校验:非 HTTPS 地址报错
c5 := &model.PushChannel{Name: "lark_channel", Type: "lark", URL: "http://open.feishu.cn", Other: ""}
assert.Error(t, c5.Validate())
require.Error(t, c5.Validate())
// 飞书正确配置
c6 := &model.PushChannel{Name: "lark_channel", Type: "lark", URL: "https://open.feishu.cn", Other: ""}
assert.NoError(t, c6.Validate())
require.NoError(t, c6.Validate())
// Telegram 渠道校验
cTelegramErr := &model.PushChannel{Name: "tg_channel", Type: "telegram", URL: "https://api.telegram.org", Token: "", Other: ""}
assert.Error(t, cTelegramErr.Validate())
require.Error(t, cTelegramErr.Validate())
cTelegramErr2 := &model.PushChannel{Name: "tg_channel", Type: "telegram", URL: "http://api.telegram.org", Token: "123:abc", Other: ""}
assert.Error(t, cTelegramErr2.Validate())
require.Error(t, cTelegramErr2.Validate())
cTelegramOk := &model.PushChannel{Name: "tg_channel", Type: "telegram", URL: "", Token: "123:abc", Other: "-100123"}
assert.NoError(t, cTelegramOk.Validate())
require.NoError(t, cTelegramOk.Validate())
assert.Equal(t, "https://api.telegram.org", cTelegramOk.URL)
// 邮件配置校验:允许空配置以复用系统全局设置
@@ -733,7 +734,7 @@ func TestPushChannelAPI(t *testing.T) {
dbConn.First(&updated, createdID)
assert.Equal(t, "Updated remark", updated.Description)
assert.Equal(t, "new_chan_token", updated.Token)
assert.Equal(t, `{"text": "$content"}`, updated.Other)
assert.JSONEq(t, `{"text": "$content"}`, updated.Other)
})
t.Run("admin test channel endpoint", func(t *testing.T) {
+3 -2
View File
@@ -14,6 +14,7 @@ import (
"os"
"os/exec"
"runtime"
"strconv"
"time"
"github.com/Rain-kl/Wavelet/internal/infra/config"
@@ -309,7 +310,7 @@ func exportSQLite(c *gin.Context) {
c.Header("Content-Disposition", `attachment; filename="openflare.db"`)
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Length", fmt.Sprintf("%d", fi.Size()))
c.Header("Content-Length", strconv.FormatInt(fi.Size(), 10))
c.Status(http.StatusOK)
http.ServeContent(c.Writer, c.Request, "openflare.db", fi.ModTime(), f)
}
@@ -328,7 +329,7 @@ func exportPostgres(c *gin.Context) {
args := []string{
"--no-password",
"-h", dbCfg.Host,
"-p", fmt.Sprintf("%d", dbCfg.Port),
"-p", strconv.Itoa(dbCfg.Port),
"-U", dbCfg.Username,
dbCfg.Database,
}
+6 -5
View File
@@ -46,6 +46,7 @@ func registerInternalOnlyTaskMeta() {
}
func setupTaskTestEnvironment(t *testing.T) func() {
t.Helper()
_, mr, cleanup := testhelper.SetupTestEnvironment(t)
bootstrap.RegisterTasks()
task.AsynqClient = asynq.NewClient(asynq.RedisClientOpt{
@@ -490,7 +491,7 @@ func TestListTaskExecutions(t *testing.T) {
var data map[string]interface{}
json.Unmarshal(dataBytes, &data)
assert.Equal(t, float64(3), data["total"])
assert.InDelta(t, float64(3), data["total"], 1e-9)
})
t.Run("filter by status", func(t *testing.T) {
@@ -507,7 +508,7 @@ func TestListTaskExecutions(t *testing.T) {
var data map[string]interface{}
json.Unmarshal(dataBytes, &data)
assert.Equal(t, float64(1), data["total"])
assert.InDelta(t, float64(1), data["total"], 1e-9)
})
t.Run("filter by task_type (asynq task name)", func(t *testing.T) {
@@ -524,7 +525,7 @@ func TestListTaskExecutions(t *testing.T) {
var data map[string]interface{}
json.Unmarshal(dataBytes, &data)
assert.Equal(t, float64(3), data["total"])
assert.InDelta(t, float64(3), data["total"], 1e-9)
})
t.Run("filter by task_type (management task type)", func(t *testing.T) {
@@ -541,7 +542,7 @@ func TestListTaskExecutions(t *testing.T) {
var data map[string]interface{}
json.Unmarshal(dataBytes, &data)
assert.Equal(t, float64(3), data["total"])
assert.InDelta(t, float64(3), data["total"], 1e-9)
})
t.Run("pagination", func(t *testing.T) {
@@ -558,7 +559,7 @@ func TestListTaskExecutions(t *testing.T) {
var data map[string]interface{}
json.Unmarshal(dataBytes, &data)
assert.Equal(t, float64(3), data["total"])
assert.InDelta(t, float64(3), data["total"], 1e-9)
})
}
+5 -12
View File
@@ -17,6 +17,7 @@ import (
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"sync"
"time"
@@ -163,13 +164,7 @@ func selectLatestRelease(repository string, releases []githubRelease) (githubRel
}
expectedNames := expectedAssetNames(repository, release.TagName)
for _, asset := range release.Assets {
matched := false
for _, name := range expectedNames {
if asset.Name == name {
matched = true
break
}
}
matched := slices.Contains(expectedNames, asset.Name)
if !matched || asset.BrowserDownloadURL == "" || asset.State != "uploaded" {
continue
}
@@ -199,7 +194,7 @@ func (m *manager) fetchRelease(ctx context.Context, repository string) (githubRe
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("User-Agent", "OpenFlare-Updater")
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
req.Header.Set("X-Github-Api-Version", "2022-11-28")
resp, err := m.client.Do(req)
if err != nil {
@@ -348,10 +343,8 @@ func getCandidateBinaryNames(executable string, repository string) []string {
if runtime.GOOS == windowsOS && !strings.HasSuffix(strings.ToLower(name), ".exe") {
name += ".exe"
}
for _, existing := range names {
if existing == name {
return
}
if slices.Contains(names, name) {
return
}
names = append(names, name)
}
+7 -10
View File
@@ -7,6 +7,7 @@ package user
import (
"errors"
"net/http"
"slices"
"strconv"
"time"
@@ -93,17 +94,13 @@ func abortUserLogicError(c *gin.Context, err error, notFoundMsg string, forbidde
return true
}
msg := err.Error()
for _, m := range badRequestMsgs {
if msg == m {
response.AbortBadRequest(c, msg)
return true
}
if slices.Contains(badRequestMsgs, msg) {
response.AbortBadRequest(c, msg)
return true
}
for _, m := range forbiddenMsgs {
if msg == m {
response.AbortForbidden(c, msg)
return true
}
if slices.Contains(forbiddenMsgs, msg) {
response.AbortForbidden(c, msg)
return true
}
logger.ErrorF(c.Request.Context(), "Admin user error: %v", err)
response.AbortInternal(c, "内部服务器错误")