From 48211fa587dc5da0ca8b496efbc44563bac0b48a Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 28 Aug 2026 13:42:49 +0800 Subject: [PATCH] chore: code-check --- Makefile | 29 +-- backend/plugins/domain/admin/plugin.go | 11 + backend/plugins/domain/auth/plugin.go | 9 + backend/plugins/domain/cap/plugin.go | 11 + .../message_gateway/channels/qq/adapter.go | 6 +- .../channels/telegram/adapter.go | 9 +- .../plugins/domain/message_gateway/plugin.go | 13 +- backend/plugins/domain/risk_control/plugin.go | 10 + backend/plugins/domain/system/plugin.go | 14 +- backend/plugins/domain/upload/plugin.go | 10 + backend/plugins/drivers/driver_http/engine.go | 13 +- scripts/check_cordis_architecture.sh | 195 ++++++++++++++++++ 12 files changed, 289 insertions(+), 41 deletions(-) create mode 100755 scripts/check_cordis_architecture.sh diff --git a/Makefile b/Makefile index 9a0b24ba..947d44e8 100644 --- a/Makefile +++ b/Makefile @@ -34,34 +34,7 @@ build-embedded: main.go code-check: - @echo "==> Architecture guards..." - @command -v rg >/dev/null 2>&1 || { echo 'error: rg (ripgrep) is required for architecture guards' >&2; exit 1; } - @echo " → core/ must not import gin, gorm, asynq..." - @if rg -n '"github.com/gin-gonic/gin|"gorm.io/gorm|"github.com/hibiken/asynq' backend/core/ --glob '*.go' -g '!*contracts*' -g '!*_test.go' 2>/dev/null; then \ - echo 'error: backend/core/ must not import gin, gorm, or asynq' >&2; \ - exit 1; \ - fi - @echo " → core/contracts/ must not import plugins/..." - @if rg -n 'plugins/' backend/core/contracts --glob '*.go' 2>/dev/null; then \ - echo 'error: backend/core/contracts/ must not import plugins/' >&2; \ - exit 1; \ - fi - @echo " → pkg/ must not import plugins/..." - @if rg -n 'plugins/' backend/pkg --glob '*.go' -g '!*testhelper*' -g '!*_test.go' 2>/dev/null; then \ - echo 'error: backend/pkg/ must not import plugins/' >&2; \ - exit 1; \ - fi - @echo " → plugins/domain/ must not import other plugins/domain/..." - @for d in backend/plugins/domain/*/; do \ - name=$$(basename $$d); \ - imports=$$(rg -n '"$(MODULE)/plugins/domain/' backend/plugins/domain/"$$name" -g '*.go' 2>/dev/null | rg -v "backend/plugins/domain/$$name/" | rg -v '_test.go' || true); \ - if [ -n "$$imports" ]; then \ - echo "error: backend/plugins/domain/$$name must not import other domain plugins" >&2; \ - echo "$$imports" >&2; \ - exit 1; \ - fi; \ - done - @echo " → Architecture guards PASS" + @scripts/check_cordis_architecture.sh cd backend && golangci-lint run cd frontend && pnpm tsc --noEmit --jsx preserve && npx eslint . --max-warnings 0 diff --git a/backend/plugins/domain/admin/plugin.go b/backend/plugins/domain/admin/plugin.go index 7b493dbd..992b08ed 100644 --- a/backend/plugins/domain/admin/plugin.go +++ b/backend/plugins/domain/admin/plugin.go @@ -7,6 +7,7 @@ package admin import ( "context" "embed" + "reflect" "Wavelet/core" "Wavelet/core/contracts" @@ -40,6 +41,16 @@ func (p *Plugin) Name() string { return "admin" } +// Inject declares required dependencies for the admin domain plugin. +func (p *Plugin) Inject() []reflect.Type { + return []reflect.Type{ + reflect.TypeFor[contracts.DBService](), + reflect.TypeFor[contracts.CacheService](), + reflect.TypeFor[contracts.UserService](), + reflect.TypeFor[contracts.AuthService](), + } +} + // Manifest returns the plugin metadata. func (p *Plugin) Manifest() core.Manifest { return core.Manifest{ diff --git a/backend/plugins/domain/auth/plugin.go b/backend/plugins/domain/auth/plugin.go index a949eb25..84941962 100644 --- a/backend/plugins/domain/auth/plugin.go +++ b/backend/plugins/domain/auth/plugin.go @@ -7,6 +7,7 @@ package auth import ( "context" "embed" + "reflect" "Wavelet/core" "Wavelet/core/contracts" @@ -55,6 +56,14 @@ func (p *Plugin) Name() string { return "auth" } +// Inject declares required dependencies for the auth domain plugin. +func (p *Plugin) Inject() []reflect.Type { + return []reflect.Type{ + reflect.TypeFor[contracts.DBService](), + reflect.TypeFor[contracts.CacheService](), + } +} + // Manifest returns the plugin metadata. func (p *Plugin) Manifest() core.Manifest { return core.Manifest{ diff --git a/backend/plugins/domain/cap/plugin.go b/backend/plugins/domain/cap/plugin.go index 3c113a27..cdaa31de 100644 --- a/backend/plugins/domain/cap/plugin.go +++ b/backend/plugins/domain/cap/plugin.go @@ -5,7 +5,10 @@ package cap import ( + "reflect" + "Wavelet/core" + "Wavelet/core/contracts" "Wavelet/core/extpoints" ) @@ -22,6 +25,14 @@ func (p *Plugin) Name() string { return "cap" } +// Inject declares required dependencies for the cap domain plugin. +func (p *Plugin) Inject() []reflect.Type { + return []reflect.Type{ + reflect.TypeFor[contracts.DBService](), + reflect.TypeFor[contracts.CacheService](), + } +} + // Manifest returns the plugin metadata. func (p *Plugin) Manifest() core.Manifest { return core.Manifest{ diff --git a/backend/plugins/domain/message_gateway/channels/qq/adapter.go b/backend/plugins/domain/message_gateway/channels/qq/adapter.go index 8ddbf96b..f50cee22 100644 --- a/backend/plugins/domain/message_gateway/channels/qq/adapter.go +++ b/backend/plugins/domain/message_gateway/channels/qq/adapter.go @@ -5,6 +5,7 @@ package qq import ( + "Wavelet/pkg/util" "context" "fmt" "strings" @@ -13,6 +14,7 @@ import ( "Wavelet/pkg/logger" "Wavelet/plugins/domain/message_gateway" + "github.com/tencent-connect/botgo" "github.com/tencent-connect/botgo/dto" "github.com/tencent-connect/botgo/event" @@ -105,11 +107,11 @@ func (a *Adapter) Connect(ctx context.Context) error { a.disconnected = false a.mu.Unlock() - go func() { + util.Go(func() { if err := botgo.NewSessionManager().Start(wsAP, tokSrc, &intent); err != nil { logger.ErrorF(runCtx, "qq session stopped: %v", err) } - }() + }) return nil } diff --git a/backend/plugins/domain/message_gateway/channels/telegram/adapter.go b/backend/plugins/domain/message_gateway/channels/telegram/adapter.go index f0db07c3..7371211b 100644 --- a/backend/plugins/domain/message_gateway/channels/telegram/adapter.go +++ b/backend/plugins/domain/message_gateway/channels/telegram/adapter.go @@ -12,6 +12,7 @@ import ( "strconv" "strings" + "Wavelet/pkg/util" "Wavelet/plugins/domain/message_gateway" tele "gopkg.in/telebot.v4" ) @@ -65,11 +66,13 @@ func (a *Adapter) Connect(ctx context.Context) error { a.handleTeleMessage(ctx, c.Message()) return nil }) - go bot.Start() - go func() { + util.Go(func() { + bot.Start() + }) + util.Go(func() { <-ctx.Done() bot.Stop() - }() + }) return nil } diff --git a/backend/plugins/domain/message_gateway/plugin.go b/backend/plugins/domain/message_gateway/plugin.go index 5f8bad15..682fa5b0 100644 --- a/backend/plugins/domain/message_gateway/plugin.go +++ b/backend/plugins/domain/message_gateway/plugin.go @@ -7,10 +7,12 @@ package message_gateway import ( "context" "embed" + "reflect" "Wavelet/core" "Wavelet/core/contracts" "Wavelet/core/extpoints" + "Wavelet/pkg/util" "github.com/gin-gonic/gin" "github.com/hibiken/asynq" ) @@ -50,6 +52,13 @@ func (p *Plugin) Name() string { return "message_gateway" } +// Inject declares required dependencies for the message_gateway domain plugin. +func (p *Plugin) Inject() []reflect.Type { + return []reflect.Type{ + reflect.TypeFor[contracts.DBService](), + } +} + // Manifest returns the plugin metadata. func (p *Plugin) Manifest() core.Manifest { return core.Manifest{ @@ -199,9 +208,9 @@ func (p *Plugin) Apply(ctx *core.Context) error { if p.autoStartRunner { runnerCtx, cancel := context.WithCancel(ctx.GoContext()) p.cancelRunner = cancel - go func() { + util.Go(func() { _ = Start(runnerCtx) - }() + }) } ctx.OnDispose(func() error { diff --git a/backend/plugins/domain/risk_control/plugin.go b/backend/plugins/domain/risk_control/plugin.go index ced0147e..34ac10da 100644 --- a/backend/plugins/domain/risk_control/plugin.go +++ b/backend/plugins/domain/risk_control/plugin.go @@ -7,8 +7,10 @@ package risk_control import ( "context" "embed" + "reflect" "Wavelet/core" + "Wavelet/core/contracts" "Wavelet/core/extpoints" "github.com/gin-gonic/gin" ) @@ -47,6 +49,14 @@ func (p *Plugin) Name() string { return "risk_control" } +// Inject declares required dependencies for the risk_control domain plugin. +func (p *Plugin) Inject() []reflect.Type { + return []reflect.Type{ + reflect.TypeFor[contracts.DBService](), + reflect.TypeFor[contracts.CacheService](), + } +} + // Manifest returns the plugin metadata. func (p *Plugin) Manifest() core.Manifest { return core.Manifest{ diff --git a/backend/plugins/domain/system/plugin.go b/backend/plugins/domain/system/plugin.go index 71d0f20f..c5afd188 100644 --- a/backend/plugins/domain/system/plugin.go +++ b/backend/plugins/domain/system/plugin.go @@ -6,11 +6,12 @@ package system import ( "net/http" + "reflect" "Wavelet/core" + "Wavelet/core/contracts" "Wavelet/pkg/config" "Wavelet/pkg/response" - database "Wavelet/plugins/infra/database" "github.com/gin-gonic/gin" ) @@ -27,6 +28,13 @@ func (p *Plugin) Name() string { return "system" } +// Inject declares required dependencies for the system domain plugin. +func (p *Plugin) Inject() []reflect.Type { + return []reflect.Type{ + reflect.TypeFor[contracts.DBService](), + } +} + // Manifest returns the plugin metadata. func (p *Plugin) Manifest() core.Manifest { return core.Manifest{ @@ -54,7 +62,9 @@ func (p *Plugin) Apply(ctx *core.Context) error { Value string `json:"value"` } var configs []configItem - _ = database.DB(c.Request.Context()).Table("w_system_configs").Where("visibility = ?", "visible").Find(&configs).Error + if dbSvc := ctx.DB(); dbSvc != nil { + _ = dbSvc.DB(c.Request.Context()).Table("w_system_configs").Where("visibility = ?", "visible").Find(&configs).Error + } c.JSON(http.StatusOK, response.OK(gin.H{ "configs": configs, "app": gin.H{ diff --git a/backend/plugins/domain/upload/plugin.go b/backend/plugins/domain/upload/plugin.go index 7dffc849..d8c2c581 100644 --- a/backend/plugins/domain/upload/plugin.go +++ b/backend/plugins/domain/upload/plugin.go @@ -7,6 +7,7 @@ package upload import ( "context" "embed" + "reflect" "Wavelet/core" "Wavelet/core/contracts" @@ -34,6 +35,15 @@ func (p *Plugin) Name() string { return "upload" } +// Inject declares required dependencies for the upload domain plugin. +func (p *Plugin) Inject() []reflect.Type { + return []reflect.Type{ + reflect.TypeFor[contracts.DBService](), + reflect.TypeFor[contracts.StorageService](), + reflect.TypeFor[contracts.AuthService](), + } +} + // Manifest returns the plugin metadata. func (p *Plugin) Manifest() core.Manifest { return core.Manifest{ diff --git a/backend/plugins/drivers/driver_http/engine.go b/backend/plugins/drivers/driver_http/engine.go index 559bffa7..46f0384e 100644 --- a/backend/plugins/drivers/driver_http/engine.go +++ b/backend/plugins/drivers/driver_http/engine.go @@ -18,8 +18,6 @@ import ( "Wavelet/pkg/config" "Wavelet/pkg/trace" "Wavelet/pkg/util" - "Wavelet/plugins/domain/auth" - "Wavelet/plugins/domain/risk_control" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" @@ -65,12 +63,19 @@ func BuildEngine() (*gin.Engine, error) { } } - sessionStore.Options(auth.GetSessionOptions(config.Config.App.SessionAge)) + sessionStore.Options(sessions.Options{ + Path: "/", + Domain: config.Config.App.SessionDomain, + MaxAge: config.Config.App.SessionAge, + HttpOnly: config.Config.App.SessionHTTPOnly, + Secure: config.Config.App.SessionSecure, + SameSite: http.SameSiteLaxMode, + }) r.Use(sessions.Sessions(config.Config.App.SessionCookieName, sessionStore)) // 补充中间件 - r.Use(otelgin.Middleware(config.Config.App.AppName), errorHandlerMiddleware(), loggerMiddleware(), risk_control.Middleware()) + r.Use(otelgin.Middleware(config.Config.App.AppName), errorHandlerMiddleware(), loggerMiddleware()) return r, nil } diff --git a/scripts/check_cordis_architecture.sh b/scripts/check_cordis_architecture.sh new file mode 100755 index 00000000..531bf881 --- /dev/null +++ b/scripts/check_cordis_architecture.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +# Copyright 2026 Arctel.net +# SPDX-License-Identifier: Apache-2.0 +# +# check_cordis_architecture.sh +# 验证代码库是否严格遵循 Cordis 插件化架构规约与设计规范。 + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BACKEND_DIR="${ROOT_DIR}/backend" + +MODULE=$(cd "${BACKEND_DIR}" && go list -m 2>/dev/null || echo "Wavelet") + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +ERRORS=0 + +log_check() { + echo -e "${BLUE}==>${NC} ${BOLD}$1${NC}" +} + +log_pass() { + echo -e " ${GREEN}✓${NC} $1" +} + +log_fail() { + echo -e " ${RED}✗ [FAIL]${NC} $1" >&2 + ERRORS=$((ERRORS + 1)) +} + +log_warn() { + echo -e " ${YELLOW}! [WARN]${NC} $1" +} + +# 确保 ripgrep 可用 +if ! command -v rg >/dev/null 2>&1; then + echo -e "${RED}error: rg (ripgrep) is required to run architecture checks.${NC}" >&2 + exit 1 +fi + +echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" +echo -e "${BOLD} Cordis Architecture & Spatiotemporal Composability Linter ${NC}" +echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" + +# ============================================================================== +# 1. 微内核绝对隔离 (Core Micro-Kernel Isolation) +# ============================================================================== +log_check "1. 检查微内核 (backend/core/) 纯洁度..." + +# 1.1 禁止直接依赖重型 Web/ORM/Worker 框架 +CORE_FRAMEWORK_IMPORTS=$(rg -n '"github.com/gin-gonic/gin"|"gorm.io/gorm"|"github.com/hibiken/asynq"|"github.com/robfig/cron' \ + "${BACKEND_DIR}/core/" --glob '*.go' -g '!*contracts*' -g '!*_test.go' || true) + +if [ -n "${CORE_FRAMEWORK_IMPORTS}" ]; then + log_fail "backend/core/ 严禁导入具体 Web/ORM/Worker 运行时框架 (gin, gorm, asynq, cron):" + echo "${CORE_FRAMEWORK_IMPORTS}" >&2 +else + log_pass "backend/core/ 无重型框架依赖" +fi + +# 1.2 core/ 禁止导入任何插件 +CORE_PLUGIN_IMPORTS=$(rg -n "\"${MODULE}/plugins/|\"${MODULE}/downstream/" \ + "${BACKEND_DIR}/core/" --glob '*.go' -g '!*_test.go' || true) + +if [ -n "${CORE_PLUGIN_IMPORTS}" ]; then + log_fail "backend/core/ 严禁直接依赖具体插件 (plugins/ 或 downstream/):" + echo "${CORE_PLUGIN_IMPORTS}" >&2 +else + log_pass "backend/core/ 零插件反向依赖" +fi + +# ============================================================================== +# 2. 服务契约纯洁度 (Contracts Cleanliness) +# ============================================================================== +log_check "2. 检查契约层 (backend/core/contracts/) 抽象纯洁度..." + +CONTRACTS_PLUGIN_IMPORTS=$(rg -n "\"${MODULE}/plugins/|\"${MODULE}/downstream/|\"github.com/gin-gonic/gin\"|\"github.com/hibiken/asynq\"" \ + "${BACKEND_DIR}/core/contracts/" --glob '*.go' || true) + +if [ -n "${CONTRACTS_PLUGIN_IMPORTS}" ]; then + log_fail "backend/core/contracts/ 必须保持纯 Interface/DTO,严禁导入插件或 Web/Worker 框架依赖:" + echo "${CONTRACTS_PLUGIN_IMPORTS}" >&2 +else + log_pass "backend/core/contracts/ 纯抽象无侵入" +fi + +# ============================================================================== +# 3. 基础包纯洁度 (backend/pkg/ Purity) +# ============================================================================== +log_check "3. 检查基础库 (backend/pkg/) 纯洁度..." + +# 3.1 pkg/ 严禁导入 plugins/ +PKG_PLUGIN_IMPORTS=$(rg -n "\"${MODULE}/plugins/" \ + "${BACKEND_DIR}/pkg/" --glob '*.go' -g '!*testhelper*' -g '!*_test.go' || true) + +if [ -n "${PKG_PLUGIN_IMPORTS}" ]; then + log_fail "backend/pkg/ 严禁导入任何上层 plugins/:" + echo "${PKG_PLUGIN_IMPORTS}" >&2 +else + log_pass "backend/pkg/ 零插件依赖" +fi + +# 3.2 pkg/util/ 严禁导入 ORM / Session 框架 +UTIL_FRAMEWORK_IMPORTS=$(rg -n '"gorm.io/gorm"|"github.com/gorilla/sessions"' \ + "${BACKEND_DIR}/pkg/util/" --glob '*.go' -g '!*_test.go' || true) + +if [ -n "${UTIL_FRAMEWORK_IMPORTS}" ]; then + log_fail "backend/pkg/util/ 必须保持纯粹,禁止导入 gorm、sessions 等数据库/会话框架包:" + echo "${UTIL_FRAMEWORK_IMPORTS}" >&2 +else + log_pass "backend/pkg/util/ 保持纯净无状态" +fi + +# ============================================================================== +# 4. 插件间隔离与单一所有者防线 (Plugin-to-Plugin Isolation & Single Owner Principle) +# ============================================================================== +log_check "4. 检查插件间隔离性 (禁止跨域直接 import)..." + +# 4.1 Domain 插件之间严禁相互 import +DOMAIN_CROSS_IMPORTS="" +for d in "${BACKEND_DIR}"/plugins/domain/*/; do + [ -d "$d" ] || continue + name=$(basename "$d") + imports=$(rg -n "\"${MODULE}/plugins/domain/" "${BACKEND_DIR}/plugins/domain/${name}" \ + -g '*.go' -g '!*_test.go' 2>/dev/null | rg -v "backend/plugins/domain/${name}/" || true) + if [ -n "$imports" ]; then + DOMAIN_CROSS_IMPORTS="${DOMAIN_CROSS_IMPORTS}\n[domain/${name} -> other domain]:\n${imports}\n" + fi +done + +if [ -n "${DOMAIN_CROSS_IMPORTS}" ]; then + log_fail "发现跨 Domain 插件直接依赖(必须通过 core/contracts 接口或 EventBus 解耦):" + echo -e "${DOMAIN_CROSS_IMPORTS}" >&2 +else + log_pass "Domain 插件间 100% 解耦,无跨域直连 import" +fi + +# 4.2 Driver 插件严禁导入 Domain 插件 +DRIVER_DOMAIN_IMPORTS=$(rg -n "\"${MODULE}/plugins/domain/" \ + "${BACKEND_DIR}/plugins/drivers/" --glob '*.go' -g '!*_test.go' || true) + +if [ -n "${DRIVER_DOMAIN_IMPORTS}" ]; then + log_fail "Driver 驱动插件严禁直接依赖具体业务 domain 插件:" + echo "${DRIVER_DOMAIN_IMPORTS}" >&2 +else + log_pass "Driver 驱动插件独立无业务污染" +fi + +# ============================================================================== +# 5. 数据库规范与 GORM AutoMigrate 禁令 (Database Migration & ORM Rules) +# ============================================================================== +log_check "5. 检查数据库操作与 AutoMigrate 禁令..." + +AUTOMIGRATE_CALLS=$(rg -n '\.AutoMigrate\(' "${BACKEND_DIR}" \ + --glob '*.go' -g '!*_test.go' -g '!*testhelper*' || true) + +if [ -n "${AUTOMIGRATE_CALLS}" ]; then + log_fail "严禁在生产代码中使用 GORM AutoMigrate(必须使用插件自包含 Goose SQL 迁移):" + echo "${AUTOMIGRATE_CALLS}" >&2 +else + log_pass "零 GORM AutoMigrate,100% Goose SQL 迁移管理" +fi + +# ============================================================================== +# 6. 并发安全规范 (Goroutine Concurrency Safety) +# ============================================================================== +log_check "6. 检查并发安全规范 (禁止生产代码中使用裸 go func())..." + +BARE_GO_ROUTINES=$(rg -n '\bgo func\(' "${BACKEND_DIR}" \ + --glob '*.go' -g '!*_test.go' -g '!goroutine.go' -g '!events.go' || true) + +if [ -n "${BARE_GO_ROUTINES}" ]; then + log_fail "生产代码严禁使用裸 'go func()',必须使用 'util.Go' 确保 panic 恢复与调用栈追踪:" + echo "${BARE_GO_ROUTINES}" >&2 +else + log_pass "并发调用统一使用 util.Go 具备 panic 恢复能力" +fi + +# ============================================================================== +# 总结与判定 +# ============================================================================== +echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" +if [ ${ERRORS} -eq 0 ]; then + echo -e "${GREEN}${BOLD}✓ 所有 Cordis 架构合规性检查全部通过 (0 Violations)!${NC}" + exit 0 +else + echo -e "${RED}${BOLD}✗ 发现 ${ERRORS} 项 Cordis 架构规约违背,请根据上述提示修复!${NC}" >&2 + exit 1 +fi