feat(M0): 工程骨架 — Go 后端 + Vue3 前端脚手架

- Go 工程:cmd/usernode CLI(serve/migrate/admin create/reset-password/user otp)
  + internal/{config,model,service,system,auth,cron,api,router,server,pkg,webui}
- 配置:TOML + 环境变量覆盖(USERNODE_<SEC>_<FIELD>),config.example.toml
- 数据:GORM 双驱动(SQLite/MySQL)8 表模型,migrate 子命令可跑通
- 系统层:system.Manager 接口(useradd/userdel/passwd 白名单,dev dry-run)
- HTTP:Gin 路由骨架(/api/v1 + 501 占位),healthz,slog 结构化日志,优雅启停
- 前端:Vite + Vue3 + TS + Element Plus 最小可运行(web/),go:embed 打通
- 构建:deploy/Containerfile 多阶段单二进制镜像,web/Containerfile.dev 前端 dev
  镜像,Makefile(build/test/dev/web-build/image);go.mod 锁定 go 1.22
- 验证:go build/vet/test 通过;podman 镜像构建运行 healthz+embed 通过
This commit is contained in:
2026-08-29 22:43:27 +08:00
parent f70b344c85
commit ae45aba607
46 changed files with 2878 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
// Package model 定义 GORM 模型与数据库接入。
//
// 兼容性约束(见 PLAN §6):
// - 避免平台特有类型,时间统一 UTC,字段尽量用通用类型;
// - detail 等 JSON 内容以 string 存储(入库前序列化),避免依赖
// datatypes.JSON 的平台差异;
// - 由本包 AutoMigrate 保证 SQLite / MySQL 均可平滑迁移。
package model
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/gorm"
gormlogger "gorm.io/gorm/logger"
)
// 外部用户账号状态。
const (
UserStatusActive = "active" // 正常可用
UserStatusDisabled = "disabled" // 管理员禁用
UserStatusExpired = "expired" // 已到期,等待回收或延期
)
// 密钥 / 申请 / 审计结果等通用状态。
const (
StatusActive = "active"
StatusRevoked = "revoked"
StatusPending = "pending"
StatusApproved = "approved"
StatusRejected = "rejected"
ResultSuccess = "success"
ResultFailed = "failed"
)
// AdminUser 管理端账号。
type AdminUser struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"`
PasswordHash string `gorm:"size:255;not null" json:"-"`
Email string `gorm:"size:255" json:"email"`
Role string `gorm:"size:32;not null;default:admin" json:"role"`
Status string `gorm:"size:16;not null;default:active" json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// User 外部用户(1:1 对应系统账号 ext_<name>)。
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"` // 含 ext_ 前缀
Email string `gorm:"size:255;not null" json:"email"`
Supervisor string `gorm:"size:128" json:"supervisor"` // 挂靠老师
Purpose string `gorm:"size:512" json:"purpose"` // 用途
Status string `gorm:"size:16;not null;default:active;index" json:"status"`
ExpireAt *time.Time `gorm:"index" json:"expire_at"`
Shell string `gorm:"size:64;not null" json:"shell"`
CreatedBy uint `json:"created_by"`
LastLoginAt *time.Time `json:"last_login_at"`
RecycledAt *time.Time `json:"recycled_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// SSHKey 用户上传的 SSH 公钥。仅用户自行上传(source=user_uploaded),管理员不代签。
type SSHKey struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;index" json:"user_id"`
Name string `gorm:"size:64;not null" json:"name"`
KeyType string `gorm:"size:32;not null" json:"key_type"` // ssh-ed25519 / ssh-rsa ...
PublicKey string `gorm:"type:text;not null" json:"public_key"`
Fingerprint string `gorm:"size:64;index" json:"fingerprint"`
Status string `gorm:"size:16;not null;default:active;index" json:"status"`
Source string `gorm:"size:32;not null;default:user_uploaded" json:"source"`
CreatedBy uint `json:"created_by"`
RevokedAt *time.Time `json:"revoked_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Approval 新账号申请单(仅新账号申请走审批流,延期由管理员直接操作)。
type Approval struct {
ID uint `gorm:"primaryKey" json:"id"`
UsernameRequested string `gorm:"size:64;index;not null" json:"username_requested"` // 不含 ext_ 前缀
Email string `gorm:"size:255;not null" json:"email"`
Supervisor string `gorm:"size:128" json:"supervisor"`
Purpose string `gorm:"size:512" json:"purpose"`
Status string `gorm:"size:16;not null;default:pending;index" json:"status"`
ReviewerID *uint `json:"reviewer_id"`
ReviewedAt *time.Time `json:"reviewed_at"`
Reason string `gorm:"size:512" json:"reason"` // 拒绝理由
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AuditLog 审计日志,append-only:业务代码只允许 Create,禁止 Update/Delete。
type AuditLog struct {
ID uint `gorm:"primaryKey" json:"id"`
ActorID uint `gorm:"index" json:"actor_id"`
ActorName string `gorm:"size:64" json:"actor_name"`
Action string `gorm:"size:64;index;not null" json:"action"`
ResourceType string `gorm:"size:32;index" json:"resource_type"`
ResourceID string `gorm:"size:64;index" json:"resource_id"`
Detail string `gorm:"type:text" json:"detail"` // JSON 序列化后的详情
IP string `gorm:"size:64" json:"ip"`
Result string `gorm:"size:16;not null" json:"result"`
CreatedAt time.Time `gorm:"index" json:"created_at"`
}
// Session 服务端会话(cookie 会话,DB 存储,兼容多实例)。
type Session struct {
ID string `gorm:"primaryKey;size:64" json:"session_id"`
UserType string `gorm:"size:16;not null" json:"user_type"` // admin / user
RefID uint `gorm:"index;not null" json:"ref_id"`
ExpireAt time.Time `gorm:"index;not null" json:"expire_at"`
IP string `gorm:"size:64" json:"ip"`
UserAgent string `gorm:"size:255" json:"user_agent"`
CreatedAt time.Time `json:"created_at"`
}
// Setting 系统设置(SMTP、默认有效期等,config 提供默认值,settings 表可覆盖)。
type Setting struct {
Key string `gorm:"primaryKey;size:128" json:"key"`
Value string `gorm:"type:text" json:"value"`
UpdatedAt time.Time `json:"updated_at"`
}
// MailLog 邮件发送记录(队列 + 重试 + 失败记录)。
type MailLog struct {
ID uint `gorm:"primaryKey" json:"id"`
To string `gorm:"size:255;index;not null" json:"to"`
Subject string `gorm:"size:255" json:"subject"`
Status string `gorm:"size:16;not null;default:pending;index" json:"status"`
Error string `gorm:"type:text" json:"error"`
RetryCount int `gorm:"not null;default:0" json:"retry_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AllModels 供 AutoMigrate 使用的全部模型。
func AllModels() []any {
return []any{
&AdminUser{}, &User{}, &SSHKey{}, &Approval{},
&AuditLog{}, &Session{}, &Setting{}, &MailLog{},
}
}
// Open 按 driver 打开 GORM 连接。
//
// driver: sqlite | mysql
// dsn: sqlite 为文件路径(自动创建父目录);mysql 为 DSN 字符串
func Open(driver, dsn string, debug bool) (*gorm.DB, error) {
logLevel := gormlogger.Warn
if debug {
logLevel = gormlogger.Info
}
cfg := &gorm.Config{
Logger: gormlogger.Default.LogMode(logLevel),
// 表名默认复数(users / ssh_keys / approvals / ...),与 PLAN §6 一致。
}
var dialector gorm.Dialector
switch driver {
case "sqlite":
if dsn != ":memory:" {
if err := os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil {
return nil, fmt.Errorf("model: create sqlite dir: %w", err)
}
}
dialector = sqlite.Open(dsn)
case "mysql":
dialector = mysql.Open(dsn)
default:
return nil, fmt.Errorf("model: unsupported driver %q", driver)
}
return gorm.Open(dialector, cfg)
}
// Migrate 执行 AutoMigrateCLI migrate 子命令入口)。
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(AllModels()...)
}