feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers

This commit is contained in:
2026-04-02 11:19:50 +08:00
parent e59a77bc49
commit dcc1f186f8
298 changed files with 62603 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
package domain
import (
"strings"
"time"
infraerrors "github.com/user-management-system/internal/pkg/errors"
)
const (
AnnouncementStatusDraft = "draft"
AnnouncementStatusActive = "active"
AnnouncementStatusArchived = "archived"
)
const (
AnnouncementNotifyModeSilent = "silent"
AnnouncementNotifyModePopup = "popup"
)
const (
AnnouncementConditionTypeSubscription = "subscription"
AnnouncementConditionTypeBalance = "balance"
)
const (
AnnouncementOperatorIn = "in"
AnnouncementOperatorGT = "gt"
AnnouncementOperatorGTE = "gte"
AnnouncementOperatorLT = "lt"
AnnouncementOperatorLTE = "lte"
AnnouncementOperatorEQ = "eq"
)
var (
ErrAnnouncementNotFound = infraerrors.NotFound("ANNOUNCEMENT_NOT_FOUND", "announcement not found")
ErrAnnouncementInvalidTarget = infraerrors.BadRequest("ANNOUNCEMENT_INVALID_TARGET", "invalid announcement targeting rules")
)
type AnnouncementTargeting struct {
// AnyOf 表示 OR任意一个条件组满足即可展示。
AnyOf []AnnouncementConditionGroup `json:"any_of,omitempty"`
}
type AnnouncementConditionGroup struct {
// AllOf 表示 AND组内所有条件都满足才算命中该组。
AllOf []AnnouncementCondition `json:"all_of,omitempty"`
}
type AnnouncementCondition struct {
// Type: subscription | balance
Type string `json:"type"`
// Operator:
// - subscription: in
// - balance: gt/gte/lt/lte/eq
Operator string `json:"operator"`
// subscription 条件匹配的订阅套餐group_id
GroupIDs []int64 `json:"group_ids,omitempty"`
// balance 条件:比较阈值
Value float64 `json:"value,omitempty"`
}
func (t AnnouncementTargeting) Matches(balance float64, activeSubscriptionGroupIDs map[int64]struct{}) bool {
// 空规则:展示给所有用户
if len(t.AnyOf) == 0 {
return true
}
for _, group := range t.AnyOf {
if len(group.AllOf) == 0 {
// 空条件组不命中(避免 OR 中出现无条件 “全命中”)
continue
}
allMatched := true
for _, cond := range group.AllOf {
if !cond.Matches(balance, activeSubscriptionGroupIDs) {
allMatched = false
break
}
}
if allMatched {
return true
}
}
return false
}
func (c AnnouncementCondition) Matches(balance float64, activeSubscriptionGroupIDs map[int64]struct{}) bool {
switch c.Type {
case AnnouncementConditionTypeSubscription:
if c.Operator != AnnouncementOperatorIn {
return false
}
if len(c.GroupIDs) == 0 {
return false
}
if len(activeSubscriptionGroupIDs) == 0 {
return false
}
for _, gid := range c.GroupIDs {
if _, ok := activeSubscriptionGroupIDs[gid]; ok {
return true
}
}
return false
case AnnouncementConditionTypeBalance:
switch c.Operator {
case AnnouncementOperatorGT:
return balance > c.Value
case AnnouncementOperatorGTE:
return balance >= c.Value
case AnnouncementOperatorLT:
return balance < c.Value
case AnnouncementOperatorLTE:
return balance <= c.Value
case AnnouncementOperatorEQ:
return balance == c.Value
default:
return false
}
default:
return false
}
}
func (t AnnouncementTargeting) NormalizeAndValidate() (AnnouncementTargeting, error) {
normalized := AnnouncementTargeting{AnyOf: make([]AnnouncementConditionGroup, 0, len(t.AnyOf))}
// 允许空 targeting展示给所有用户
if len(t.AnyOf) == 0 {
return normalized, nil
}
if len(t.AnyOf) > 50 {
return AnnouncementTargeting{}, ErrAnnouncementInvalidTarget
}
for _, g := range t.AnyOf {
if len(g.AllOf) == 0 {
return AnnouncementTargeting{}, ErrAnnouncementInvalidTarget
}
if len(g.AllOf) > 50 {
return AnnouncementTargeting{}, ErrAnnouncementInvalidTarget
}
group := AnnouncementConditionGroup{AllOf: make([]AnnouncementCondition, 0, len(g.AllOf))}
for _, c := range g.AllOf {
cond := AnnouncementCondition{
Type: strings.TrimSpace(c.Type),
Operator: strings.TrimSpace(c.Operator),
Value: c.Value,
}
for _, gid := range c.GroupIDs {
if gid <= 0 {
return AnnouncementTargeting{}, ErrAnnouncementInvalidTarget
}
cond.GroupIDs = append(cond.GroupIDs, gid)
}
if err := cond.validate(); err != nil {
return AnnouncementTargeting{}, err
}
group.AllOf = append(group.AllOf, cond)
}
normalized.AnyOf = append(normalized.AnyOf, group)
}
return normalized, nil
}
func (c AnnouncementCondition) validate() error {
switch c.Type {
case AnnouncementConditionTypeSubscription:
if c.Operator != AnnouncementOperatorIn {
return ErrAnnouncementInvalidTarget
}
if len(c.GroupIDs) == 0 {
return ErrAnnouncementInvalidTarget
}
return nil
case AnnouncementConditionTypeBalance:
switch c.Operator {
case AnnouncementOperatorGT, AnnouncementOperatorGTE, AnnouncementOperatorLT, AnnouncementOperatorLTE, AnnouncementOperatorEQ:
return nil
default:
return ErrAnnouncementInvalidTarget
}
default:
return ErrAnnouncementInvalidTarget
}
}
type Announcement struct {
ID int64
Title string
Content string
Status string
NotifyMode string
Targeting AnnouncementTargeting
StartsAt *time.Time
EndsAt *time.Time
CreatedBy *int64
UpdatedBy *int64
CreatedAt time.Time
UpdatedAt time.Time
}
func (a *Announcement) IsActiveAt(now time.Time) bool {
if a == nil {
return false
}
if a.Status != AnnouncementStatusActive {
return false
}
if a.StartsAt != nil && now.Before(*a.StartsAt) {
return false
}
if a.EndsAt != nil && !now.Before(*a.EndsAt) {
// ends_at 语义:到点即下线
return false
}
return true
}

View File

@@ -0,0 +1,140 @@
package domain
// Status constants
const (
StatusActive = "active"
StatusDisabled = "disabled"
StatusError = "error"
StatusUnused = "unused"
StatusUsed = "used"
StatusExpired = "expired"
)
// Role constants
const (
RoleAdmin = "admin"
RoleUser = "user"
)
// Platform constants
const (
PlatformAnthropic = "anthropic"
PlatformOpenAI = "openai"
PlatformGemini = "gemini"
PlatformAntigravity = "antigravity"
PlatformSora = "sora"
)
// Account type constants
const (
AccountTypeOAuth = "oauth" // OAuth类型账号full scope: profile + inference
AccountTypeSetupToken = "setup-token" // Setup Token类型账号inference only scope
AccountTypeAPIKey = "apikey" // API Key类型账号
AccountTypeUpstream = "upstream" // 上游透传类型账号(通过 Base URL + API Key 连接上游)
AccountTypeBedrock = "bedrock" // AWS Bedrock 类型账号(通过 SigV4 签名或 API Key 连接 Bedrock由 credentials.auth_mode 区分)
)
// Redeem type constants
const (
RedeemTypeBalance = "balance"
RedeemTypeConcurrency = "concurrency"
RedeemTypeSubscription = "subscription"
RedeemTypeInvitation = "invitation"
)
// PromoCode status constants
const (
PromoCodeStatusActive = "active"
PromoCodeStatusDisabled = "disabled"
)
// Admin adjustment type constants
const (
AdjustmentTypeAdminBalance = "admin_balance" // 管理员调整余额
AdjustmentTypeAdminConcurrency = "admin_concurrency" // 管理员调整并发数
)
// Group subscription type constants
const (
SubscriptionTypeStandard = "standard" // 标准计费模式(按余额扣费)
SubscriptionTypeSubscription = "subscription" // 订阅模式(按限额控制)
)
// Subscription status constants
const (
SubscriptionStatusActive = "active"
SubscriptionStatusExpired = "expired"
SubscriptionStatusSuspended = "suspended"
)
// DefaultAntigravityModelMapping 是 Antigravity 平台的默认模型映射
// 当账号未配置 model_mapping 时使用此默认值
// 与前端 useModelWhitelist.ts 中的 antigravityDefaultMappings 保持一致
var DefaultAntigravityModelMapping = map[string]string{
// Claude 白名单
"claude-opus-4-6-thinking": "claude-opus-4-6-thinking", // 官方模型
"claude-opus-4-6": "claude-opus-4-6-thinking", // 简称映射
"claude-opus-4-5-thinking": "claude-opus-4-6-thinking", // 迁移旧模型
"claude-sonnet-4-6": "claude-sonnet-4-6",
"claude-sonnet-4-5": "claude-sonnet-4-5",
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
// Claude 详细版本 ID 映射
"claude-opus-4-5-20251101": "claude-opus-4-6-thinking", // 迁移旧模型
"claude-sonnet-4-5-20250929": "claude-sonnet-4-5",
// Claude Haiku → Sonnet无 Haiku 支持)
"claude-haiku-4-5": "claude-sonnet-4-6",
"claude-haiku-4-5-20251001": "claude-sonnet-4-6",
// Gemini 2.5 白名单
"gemini-2.5-flash": "gemini-2.5-flash",
"gemini-2.5-flash-image": "gemini-2.5-flash-image",
"gemini-2.5-flash-image-preview": "gemini-2.5-flash-image",
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
"gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
"gemini-2.5-pro": "gemini-2.5-pro",
// Gemini 3 白名单
"gemini-3-flash": "gemini-3-flash",
"gemini-3-pro-high": "gemini-3-pro-high",
"gemini-3-pro-low": "gemini-3-pro-low",
// Gemini 3 preview 映射
"gemini-3-flash-preview": "gemini-3-flash",
"gemini-3-pro-preview": "gemini-3-pro-high",
// Gemini 3.1 白名单
"gemini-3.1-pro-high": "gemini-3.1-pro-high",
"gemini-3.1-pro-low": "gemini-3.1-pro-low",
// Gemini 3.1 preview 映射
"gemini-3.1-pro-preview": "gemini-3.1-pro-high",
// Gemini 3.1 image 白名单
"gemini-3.1-flash-image": "gemini-3.1-flash-image",
// Gemini 3.1 image preview 映射
"gemini-3.1-flash-image-preview": "gemini-3.1-flash-image",
// Gemini 3 image 兼容映射(向 3.1 image 迁移)
"gemini-3-pro-image": "gemini-3.1-flash-image",
"gemini-3-pro-image-preview": "gemini-3.1-flash-image",
// 其他官方模型
"gpt-oss-120b-medium": "gpt-oss-120b-medium",
"tab_flash_lite_preview": "tab_flash_lite_preview",
}
// DefaultBedrockModelMapping 是 AWS Bedrock 平台的默认模型映射
// 将 Anthropic 标准模型名映射到 Bedrock 模型 ID
// 注意:此处的 "us." 前缀仅为默认值ResolveBedrockModelID 会根据账号配置的
// aws_region 自动调整为匹配的区域前缀(如 eu.、apac.、jp. 等)
var DefaultBedrockModelMapping = map[string]string{
// Claude Opus
"claude-opus-4-6-thinking": "us.anthropic.claude-opus-4-6-v1",
"claude-opus-4-6": "us.anthropic.claude-opus-4-6-v1",
"claude-opus-4-5-thinking": "us.anthropic.claude-opus-4-5-20251101-v1:0",
"claude-opus-4-5-20251101": "us.anthropic.claude-opus-4-5-20251101-v1:0",
"claude-opus-4-1": "us.anthropic.claude-opus-4-1-20250805-v1:0",
"claude-opus-4-20250514": "us.anthropic.claude-opus-4-20250514-v1:0",
// Claude Sonnet
"claude-sonnet-4-6-thinking": "us.anthropic.claude-sonnet-4-6",
"claude-sonnet-4-6": "us.anthropic.claude-sonnet-4-6",
"claude-sonnet-4-5": "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
"claude-sonnet-4-5-thinking": "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
"claude-sonnet-4-5-20250929": "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
"claude-sonnet-4-20250514": "us.anthropic.claude-sonnet-4-20250514-v1:0",
// Claude Haiku
"claude-haiku-4-5": "us.anthropic.claude-haiku-4-5-20251001-v1:0",
"claude-haiku-4-5-20251001": "us.anthropic.claude-haiku-4-5-20251001-v1:0",
}

View File

@@ -0,0 +1,26 @@
package domain
import "testing"
func TestDefaultAntigravityModelMapping_ImageCompatibilityAliases(t *testing.T) {
t.Parallel()
cases := map[string]string{
"gemini-2.5-flash-image": "gemini-2.5-flash-image",
"gemini-2.5-flash-image-preview": "gemini-2.5-flash-image",
"gemini-3.1-flash-image": "gemini-3.1-flash-image",
"gemini-3.1-flash-image-preview": "gemini-3.1-flash-image",
"gemini-3-pro-image": "gemini-3.1-flash-image",
"gemini-3-pro-image-preview": "gemini-3.1-flash-image",
}
for from, want := range cases {
got, ok := DefaultAntigravityModelMapping[from]
if !ok {
t.Fatalf("expected mapping for %q to exist", from)
}
if got != want {
t.Fatalf("unexpected mapping for %q: got %q want %q", from, got, want)
}
}
}

View File

@@ -0,0 +1,127 @@
package domain
import "time"
// CustomFieldType 自定义字段类型
type CustomFieldType int
const (
CustomFieldTypeString CustomFieldType = iota // 字符串
CustomFieldTypeNumber // 数字
CustomFieldTypeBoolean // 布尔
CustomFieldTypeDate // 日期
)
// CustomField 自定义字段定义
type CustomField struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(50);not null" json:"name"` // 字段名称
FieldKey string `gorm:"type:varchar(50);uniqueIndex;not null" json:"field_key"` // 字段标识符
Type CustomFieldType `gorm:"type:int;not null" json:"type"` // 字段类型
Required bool `gorm:"default:false" json:"required"` // 是否必填
DefaultVal string `gorm:"type:varchar(255)" json:"default_val"` // 默认值
MinLen int `gorm:"default:0" json:"min_len"` // 最小长度(字符串)
MaxLen int `gorm:"default:255" json:"max_len"` // 最大长度(字符串)
MinVal float64 `gorm:"default:0" json:"min_val"` // 最小值(数字)
MaxVal float64 `gorm:"default:0" json:"max_val"` // 最大值(数字)
Options string `gorm:"type:varchar(500)" json:"options"` // 选项列表(逗号分隔)
Sort int `gorm:"default:0" json:"sort"` // 排序
Status int `gorm:"type:int;default:1" json:"status"` // 状态1启用 0禁用
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// TableName 指定表名
func (CustomField) TableName() string {
return "custom_fields"
}
// UserCustomFieldValue 用户自定义字段值
type UserCustomFieldValue struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"not null;index;uniqueIndex:idx_user_field" json:"user_id"`
FieldID int64 `gorm:"not null;index;uniqueIndex:idx_user_field" json:"field_id"`
FieldKey string `gorm:"type:varchar(50);not null" json:"field_key"` // 反规范化存储便于查询
Value string `gorm:"type:text" json:"value"` // 存储为字符串
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// TableName 指定表名
func (UserCustomFieldValue) TableName() string {
return "user_custom_field_values"
}
// CustomFieldValueResponse 自定义字段值响应
type CustomFieldValueResponse struct {
FieldKey string `json:"field_key"`
Value interface{} `json:"value"`
}
// GetValueAsInterface 根据字段类型返回解析后的值
func (v *UserCustomFieldValue) GetValueAsInterface(field *CustomField) interface{} {
switch field.Type {
case CustomFieldTypeString:
return v.Value
case CustomFieldTypeNumber:
var f float64
for _, c := range v.Value {
if c >= '0' && c <= '9' || c == '.' {
continue
}
return v.Value
}
if _, err := parseFloat(v.Value, &f); err == nil {
return f
}
return v.Value
case CustomFieldTypeBoolean:
return v.Value == "true" || v.Value == "1"
case CustomFieldTypeDate:
t, err := time.Parse("2006-01-02", v.Value)
if err == nil {
return t.Format("2006-01-02")
}
return v.Value
default:
return v.Value
}
}
func parseFloat(s string, f *float64) (int, error) {
var sign, decimals int
varMantissa := 0
*f = 0
i := 0
if i < len(s) && s[i] == '-' {
sign = 1
i++
}
for ; i < len(s); i++ {
c := s[i]
if c == '.' {
decimals = 1
continue
}
if c < '0' || c > '9' {
return i, nil
}
n := float64(c - '0')
*f = *f*10 + n
varMantissa++
}
if decimals > 0 {
for ; decimals > 0; decimals-- {
*f /= 10
}
}
if sign == 1 {
*f = -*f
}
return i, nil
}

45
internal/domain/device.go Normal file
View File

@@ -0,0 +1,45 @@
package domain
import "time"
// DeviceType 设备类型
type DeviceType int
const (
DeviceTypeUnknown DeviceType = iota
DeviceTypeWeb
DeviceTypeMobile
DeviceTypeDesktop
)
// DeviceStatus 设备状态
type DeviceStatus int
const (
DeviceStatusInactive DeviceStatus = 0
DeviceStatusActive DeviceStatus = 1
)
// Device 设备模型
type Device struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"not null;index" json:"user_id"`
DeviceID string `gorm:"type:varchar(100);uniqueIndex;not null" json:"device_id"`
DeviceName string `gorm:"type:varchar(100)" json:"device_name"`
DeviceType DeviceType `gorm:"type:int;default:0" json:"device_type"`
DeviceOS string `gorm:"type:varchar(50)" json:"device_os"`
DeviceBrowser string `gorm:"type:varchar(50)" json:"device_browser"`
IP string `gorm:"type:varchar(50)" json:"ip"`
Location string `gorm:"type:varchar(100)" json:"location"`
IsTrusted bool `gorm:"default:false" json:"is_trusted"` // 是否信任该设备
TrustExpiresAt *time.Time `gorm:"type:datetime" json:"trust_expires_at"` // 信任过期时间
Status DeviceStatus `gorm:"type:int;default:1" json:"status"`
LastActiveTime time.Time `json:"last_active_time"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// TableName 指定表名
func (Device) TableName() string {
return "devices"
}

View File

@@ -0,0 +1,21 @@
package domain
import (
"testing"
)
// TestUserStatusConstantsExtra 测试用户状态常量(额外验证)
func TestUserStatusConstantsExtra(t *testing.T) {
if UserStatusInactive != 0 {
t.Errorf("UserStatusInactive = %d, want 0", UserStatusInactive)
}
if UserStatusActive != 1 {
t.Errorf("UserStatusActive = %d, want 1", UserStatusActive)
}
if UserStatusLocked != 2 {
t.Errorf("UserStatusLocked = %d, want 2", UserStatusLocked)
}
if UserStatusDisabled != 3 {
t.Errorf("UserStatusDisabled = %d, want 3", UserStatusDisabled)
}
}

View File

@@ -0,0 +1,31 @@
package domain
import "time"
// LoginType 登录方式
type LoginType int
const (
LoginTypePassword LoginType = 1 // 用户名/邮箱/手机 + 密码
LoginTypeEmailCode LoginType = 2 // 邮箱验证码
LoginTypeSMSCode LoginType = 3 // 手机验证码
LoginTypeOAuth LoginType = 4 // 第三方 OAuth
)
// LoginLog 登录日志
type LoginLog struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID *int64 `gorm:"index" json:"user_id,omitempty"`
LoginType int `gorm:"not null" json:"login_type"` // 1-密码, 2-邮箱验证码, 3-手机验证码, 4-OAuth
DeviceID string `gorm:"type:varchar(100)" json:"device_id"`
IP string `gorm:"type:varchar(50)" json:"ip"`
Location string `gorm:"type:varchar(100)" json:"location"`
Status int `gorm:"not null" json:"status"` // 0-失败, 1-成功
FailReason string `gorm:"type:varchar(255)" json:"fail_reason,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
// TableName 指定表名
func (LoginLog) TableName() string {
return "login_logs"
}

View File

@@ -0,0 +1,23 @@
package domain
import "time"
// OperationLog 操作日志
type OperationLog struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID *int64 `gorm:"index" json:"user_id,omitempty"`
OperationType string `gorm:"type:varchar(50)" json:"operation_type"`
OperationName string `gorm:"type:varchar(100)" json:"operation_name"`
RequestMethod string `gorm:"type:varchar(10)" json:"request_method"`
RequestPath string `gorm:"type:varchar(200)" json:"request_path"`
RequestParams string `gorm:"type:text" json:"request_params"`
ResponseStatus int `json:"response_status"`
IP string `gorm:"type:varchar(50)" json:"ip"`
UserAgent string `gorm:"type:varchar(500)" json:"user_agent"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
// TableName 指定表名
func (OperationLog) TableName() string {
return "operation_logs"
}

View File

@@ -0,0 +1,16 @@
package domain
import "time"
// PasswordHistory 密码历史记录(防止重复使用旧密码)
type PasswordHistory struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"not null;index" json:"user_id"`
PasswordHash string `gorm:"type:varchar(255);not null" json:"-"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
// TableName 指定表名
func (PasswordHistory) TableName() string {
return "password_histories"
}

View File

@@ -0,0 +1,74 @@
package domain
import "time"
// PermissionType 权限类型
type PermissionType int
const (
PermissionTypeMenu PermissionType = iota // 菜单
PermissionTypeButton // 按钮
PermissionTypeAPI // 接口
)
// PermissionStatus 权限状态
type PermissionStatus int
const (
PermissionStatusDisabled PermissionStatus = 0 // 禁用
PermissionStatusEnabled PermissionStatus = 1 // 启用
)
// Permission 权限模型
type Permission struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(50);not null" json:"name"`
Code string `gorm:"type:varchar(100);uniqueIndex;not null" json:"code"`
Type PermissionType `gorm:"type:int;not null" json:"type"`
Description string `gorm:"type:varchar(200)" json:"description"`
ParentID *int64 `gorm:"index" json:"parent_id,omitempty"`
Level int `gorm:"default:1" json:"level"`
Path string `gorm:"type:varchar(200)" json:"path,omitempty"`
Method string `gorm:"type:varchar(10)" json:"method,omitempty"`
Sort int `gorm:"default:0" json:"sort"`
Icon string `gorm:"type:varchar(50)" json:"icon,omitempty"`
Status PermissionStatus `gorm:"type:int;default:1" json:"status"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Children []*Permission `gorm:"-" json:"children,omitempty"` // 子权限,不持久化
}
// TableName 指定表名
func (Permission) TableName() string {
return "permissions"
}
// DefaultPermissions 返回系统默认权限列表
func DefaultPermissions() []Permission {
return []Permission{
// 用户管理
{Name: "用户列表", Code: "user:list", Type: PermissionTypeAPI, Path: "/api/v1/users", Method: "GET", Sort: 10, Status: PermissionStatusEnabled, Description: "查看用户列表"},
{Name: "查看用户", Code: "user:view", Type: PermissionTypeAPI, Path: "/api/v1/users/:id", Method: "GET", Sort: 11, Status: PermissionStatusEnabled, Description: "查看用户详情"},
{Name: "编辑用户", Code: "user:edit", Type: PermissionTypeAPI, Path: "/api/v1/users/:id", Method: "PUT", Sort: 12, Status: PermissionStatusEnabled, Description: "编辑用户信息"},
{Name: "删除用户", Code: "user:delete", Type: PermissionTypeAPI, Path: "/api/v1/users/:id", Method: "DELETE", Sort: 13, Status: PermissionStatusEnabled, Description: "删除用户"},
{Name: "管理用户", Code: "user:manage", Type: PermissionTypeAPI, Path: "/api/v1/users/:id/status", Method: "PUT", Sort: 14, Status: PermissionStatusEnabled, Description: "管理用户状态和角色"},
// 个人资料
{Name: "查看资料", Code: "profile:view", Type: PermissionTypeAPI, Path: "/api/v1/auth/userinfo", Method: "GET", Sort: 20, Status: PermissionStatusEnabled, Description: "查看个人资料"},
{Name: "编辑资料", Code: "profile:edit", Type: PermissionTypeAPI, Path: "/api/v1/users/:id", Method: "PUT", Sort: 21, Status: PermissionStatusEnabled, Description: "编辑个人资料"},
{Name: "修改密码", Code: "profile:change_password", Type: PermissionTypeAPI, Path: "/api/v1/users/:id/password", Method: "PUT", Sort: 22, Status: PermissionStatusEnabled, Description: "修改密码"},
// 角色管理
{Name: "角色管理", Code: "role:manage", Type: PermissionTypeAPI, Path: "/api/v1/roles", Method: "GET", Sort: 30, Status: PermissionStatusEnabled, Description: "管理角色"},
{Name: "创建角色", Code: "role:create", Type: PermissionTypeAPI, Path: "/api/v1/roles", Method: "POST", Sort: 31, Status: PermissionStatusEnabled, Description: "创建角色"},
{Name: "编辑角色", Code: "role:edit", Type: PermissionTypeAPI, Path: "/api/v1/roles/:id", Method: "PUT", Sort: 32, Status: PermissionStatusEnabled, Description: "编辑角色"},
{Name: "删除角色", Code: "role:delete", Type: PermissionTypeAPI, Path: "/api/v1/roles/:id", Method: "DELETE", Sort: 33, Status: PermissionStatusEnabled, Description: "删除角色"},
// 权限管理
{Name: "权限管理", Code: "permission:manage", Type: PermissionTypeAPI, Path: "/api/v1/permissions", Method: "GET", Sort: 40, Status: PermissionStatusEnabled, Description: "管理权限"},
// 日志查看
{Name: "查看自己的日志", Code: "log:view_own", Type: PermissionTypeAPI, Path: "/api/v1/logs/login/me", Method: "GET", Sort: 50, Status: PermissionStatusEnabled, Description: "查看个人登录日志"},
{Name: "查看所有日志", Code: "log:view_all", Type: PermissionTypeAPI, Path: "/api/v1/logs/login", Method: "GET", Sort: 51, Status: PermissionStatusEnabled, Description: "查看全部日志(管理员)"},
// 系统统计
{Name: "仪表盘统计", Code: "stats:view", Type: PermissionTypeAPI, Path: "/api/v1/admin/stats/dashboard", Method: "GET", Sort: 60, Status: PermissionStatusEnabled, Description: "查看系统统计数据"},
// 设备管理
{Name: "设备管理", Code: "device:manage", Type: PermissionTypeAPI, Path: "/api/v1/devices", Method: "GET", Sort: 70, Status: PermissionStatusEnabled, Description: "管理设备"},
}
}

57
internal/domain/role.go Normal file
View File

@@ -0,0 +1,57 @@
package domain
import "time"
// RoleStatus 角色状态
type RoleStatus int
const (
RoleStatusDisabled RoleStatus = 0 // 禁用
RoleStatusEnabled RoleStatus = 1 // 启用
)
// Role 角色模型
type Role struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(50);uniqueIndex;not null" json:"name"`
Code string `gorm:"type:varchar(50);uniqueIndex;not null" json:"code"`
Description string `gorm:"type:varchar(200)" json:"description"`
ParentID *int64 `gorm:"index" json:"parent_id,omitempty"`
Level int `gorm:"default:1;index" json:"level"`
IsSystem bool `gorm:"default:false" json:"is_system"` // 是否系统角色
IsDefault bool `gorm:"default:false;index" json:"is_default"` // 是否默认角色
Status RoleStatus `gorm:"type:int;default:1" json:"status"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// TableName 指定表名
func (Role) TableName() string {
return "roles"
}
// PredefinedRoles 预定义角色
var PredefinedRoles = []Role{
{
ID: 1,
Name: "管理员",
Code: "admin",
Description: "系统管理员角色,拥有所有权限",
ParentID: nil,
Level: 1,
IsSystem: true,
IsDefault: false,
Status: RoleStatusEnabled,
},
{
ID: 2,
Name: "普通用户",
Code: "user",
Description: "普通用户角色,基本权限",
ParentID: nil,
Level: 1,
IsSystem: true,
IsDefault: true,
Status: RoleStatusEnabled,
},
}

View File

@@ -0,0 +1,16 @@
package domain
import "time"
// RolePermission 角色-权限关联
type RolePermission struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
RoleID int64 `gorm:"not null;index:idx_role_perm;index:idx_rp_role" json:"role_id"`
PermissionID int64 `gorm:"not null;index:idx_role_perm;index:idx_rp_perm" json:"permission_id"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
// TableName 指定表名
func (RolePermission) TableName() string {
return "role_permissions"
}

View File

@@ -0,0 +1,78 @@
package domain
import (
"database/sql/driver"
"encoding/json"
"time"
)
// SocialAccount models a persisted OAuth binding.
type SocialAccount struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"index;not null" json:"user_id"`
Provider string `gorm:"type:varchar(50);not null" json:"provider"`
OpenID string `gorm:"type:varchar(100);not null" json:"open_id"`
UnionID string `gorm:"type:varchar(100)" json:"union_id,omitempty"`
Nickname string `gorm:"type:varchar(100)" json:"nickname"`
Avatar string `gorm:"type:varchar(500)" json:"avatar"`
Gender string `gorm:"type:varchar(10)" json:"gender,omitempty"`
Email string `gorm:"type:varchar(100)" json:"email,omitempty"`
Phone string `gorm:"type:varchar(20)" json:"phone,omitempty"`
Extra ExtraData `gorm:"type:text" json:"extra,omitempty"`
Status SocialAccountStatus `gorm:"default:1" json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (SocialAccount) TableName() string {
return "user_social_accounts"
}
type SocialAccountStatus int
const (
SocialAccountStatusActive SocialAccountStatus = 1
SocialAccountStatusInactive SocialAccountStatus = 0
SocialAccountStatusDisabled SocialAccountStatus = 2
)
type ExtraData map[string]interface{}
func (e ExtraData) Value() (driver.Value, error) {
if e == nil {
return nil, nil
}
return json.Marshal(e)
}
func (e *ExtraData) Scan(value interface{}) error {
if value == nil {
*e = nil
return nil
}
bytes, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(bytes, e)
}
type SocialAccountInfo struct {
ID int64 `json:"id"`
Provider string `json:"provider"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Status SocialAccountStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
func (s *SocialAccount) ToInfo() *SocialAccountInfo {
return &SocialAccountInfo{
ID: s.ID,
Provider: s.Provider,
Nickname: s.Nickname,
Avatar: s.Avatar,
Status: s.Status,
CreatedAt: s.CreatedAt,
}
}

View File

@@ -0,0 +1,10 @@
package domain
import "testing"
func TestSocialAccountTableName(t *testing.T) {
var account SocialAccount
if account.TableName() != "user_social_accounts" {
t.Fatalf("unexpected table name: %s", account.TableName())
}
}

39
internal/domain/theme.go Normal file
View File

@@ -0,0 +1,39 @@
package domain
import "time"
// ThemeConfig 主题配置
type ThemeConfig struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(50);uniqueIndex;not null" json:"name"` // 主题名称
IsDefault bool `gorm:"default:false" json:"is_default"` // 是否默认主题
LogoURL string `gorm:"type:varchar(500)" json:"logo_url"` // Logo URL
FaviconURL string `gorm:"type:varchar(500)" json:"favicon_url"` // Favicon URL
PrimaryColor string `gorm:"type:varchar(20)" json:"primary_color"` // 主色调(如 #1890ff
SecondaryColor string `gorm:"type:varchar(20)" json:"secondary_color"` // 辅助色
BackgroundColor string `gorm:"type:varchar(20)" json:"background_color"` // 背景色
TextColor string `gorm:"type:varchar(20)" json:"text_color"` // 文字颜色
CustomCSS string `gorm:"type:text" json:"custom_css"` // 自定义CSS
CustomJS string `gorm:"type:text" json:"custom_js"` // 自定义JS
Enabled bool `gorm:"default:true" json:"enabled"` // 是否启用
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// TableName 指定表名
func (ThemeConfig) TableName() string {
return "theme_configs"
}
// DefaultThemeConfig 返回默认主题配置
func DefaultThemeConfig() *ThemeConfig {
return &ThemeConfig{
Name: "default",
IsDefault: true,
PrimaryColor: "#1890ff",
SecondaryColor: "#52c41a",
BackgroundColor: "#ffffff",
TextColor: "#333333",
Enabled: true,
}
}

70
internal/domain/user.go Normal file
View File

@@ -0,0 +1,70 @@
package domain
import "time"
// StrPtr 将 string 转为 *string空字符串返回 nil用于可选的 unique 字段)
func StrPtr(s string) *string {
if s == "" {
return nil
}
return &s
}
// DerefStr 安全解引用 *stringnil 返回空字符串
func DerefStr(s *string) string {
if s == nil {
return ""
}
return *s
}
// Gender 性别
type Gender int
const (
GenderUnknown Gender = iota // 未知
GenderMale // 男
GenderFemale // 女
)
// UserStatus 用户状态
type UserStatus int
const (
UserStatusInactive UserStatus = 0 // 未激活
UserStatusActive UserStatus = 1 // 已激活
UserStatusLocked UserStatus = 2 // 已锁定
UserStatusDisabled UserStatus = 3 // 已禁用
)
// User 用户模型
type User struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Username string `gorm:"type:varchar(50);uniqueIndex;not null" json:"username"`
// Email/Phone 使用指针类型nil 存储为 NULL允许多个用户没有邮箱/手机(唯一约束对 NULL 不生效)
Email *string `gorm:"type:varchar(100);uniqueIndex" json:"email"`
Phone *string `gorm:"type:varchar(20);uniqueIndex" json:"phone"`
Nickname string `gorm:"type:varchar(50)" json:"nickname"`
Avatar string `gorm:"type:varchar(255)" json:"avatar"`
Password string `gorm:"type:varchar(255)" json:"-"`
Gender Gender `gorm:"type:int;default:0" json:"gender"`
Birthday *time.Time `gorm:"type:date" json:"birthday,omitempty"`
Region string `gorm:"type:varchar(50)" json:"region"`
Bio string `gorm:"type:varchar(500)" json:"bio"`
Status UserStatus `gorm:"type:int;default:0;index" json:"status"`
LastLoginTime *time.Time `json:"last_login_time,omitempty"`
LastLoginIP string `gorm:"type:varchar(50)" json:"last_login_ip"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"`
// 2FA / TOTP 字段
TOTPEnabled bool `gorm:"default:false" json:"totp_enabled"`
TOTPSecret string `gorm:"type:varchar(64)" json:"-"` // Base32 密钥,不返回给前端
TOTPRecoveryCodes string `gorm:"type:text" json:"-"` // JSON 编码的恢复码列表
}
// TableName 指定表名
func (User) TableName() string {
return "users"
}

View File

@@ -0,0 +1,16 @@
package domain
import "time"
// UserRole 用户-角色关联
type UserRole struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"not null;index:idx_user_role;index:idx_user" json:"user_id"`
RoleID int64 `gorm:"not null;index:idx_user_role;index:idx_role" json:"role_id"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
// TableName 指定表名
func (UserRole) TableName() string {
return "user_roles"
}

View File

@@ -0,0 +1,81 @@
package domain
import (
"testing"
"time"
)
// TestUserModel 测试User模型基本属性
func TestUserModel(t *testing.T) {
u := &User{
Username: "testuser",
Email: StrPtr("test@example.com"),
Phone: StrPtr("13800138000"),
Password: "hashedpassword",
Status: UserStatusActive,
Gender: GenderMale,
CreatedAt: time.Now(),
}
if u.Username != "testuser" {
t.Errorf("Username = %v, want testuser", u.Username)
}
if u.Status != UserStatusActive {
t.Errorf("Status = %v, want %v", u.Status, UserStatusActive)
}
}
// TestUserTableName 测试User表名
func TestUserTableName(t *testing.T) {
u := User{}
if u.TableName() != "users" {
t.Errorf("TableName() = %v, want users", u.TableName())
}
}
// TestUserStatusConstants 测试用户状态常量值
func TestUserStatusConstants(t *testing.T) {
cases := []struct {
status UserStatus
value int
}{
{UserStatusInactive, 0},
{UserStatusActive, 1},
{UserStatusLocked, 2},
{UserStatusDisabled, 3},
}
for _, c := range cases {
if int(c.status) != c.value {
t.Errorf("UserStatus = %d, want %d", c.status, c.value)
}
}
}
// TestGenderConstants 测试性别常量
func TestGenderConstants(t *testing.T) {
if int(GenderUnknown) != 0 {
t.Errorf("GenderUnknown = %d, want 0", GenderUnknown)
}
if int(GenderMale) != 1 {
t.Errorf("GenderMale = %d, want 1", GenderMale)
}
if int(GenderFemale) != 2 {
t.Errorf("GenderFemale = %d, want 2", GenderFemale)
}
}
// TestUserActiveCheck 测试用户激活状态检查
func TestUserActiveCheck(t *testing.T) {
active := &User{Status: UserStatusActive}
inactive := &User{Status: UserStatusInactive}
locked := &User{Status: UserStatusLocked}
disabled := &User{Status: UserStatusDisabled}
if active.Status != UserStatusActive {
t.Error("active用户应为Active状态")
}
if inactive.Status == UserStatusActive {
t.Error("inactive用户不应为Active状态")
}
_ = locked
_ = disabled
}

View File

@@ -0,0 +1,69 @@
package domain
import "time"
// WebhookEventType Webhook 事件类型
type WebhookEventType string
const (
EventUserRegistered WebhookEventType = "user.registered"
EventUserLogin WebhookEventType = "user.login"
EventUserLogout WebhookEventType = "user.logout"
EventUserUpdated WebhookEventType = "user.updated"
EventUserDeleted WebhookEventType = "user.deleted"
EventUserLocked WebhookEventType = "user.locked"
EventPasswordChanged WebhookEventType = "user.password_changed"
EventPasswordReset WebhookEventType = "user.password_reset"
EventTOTPEnabled WebhookEventType = "user.totp_enabled"
EventTOTPDisabled WebhookEventType = "user.totp_disabled"
EventLoginFailed WebhookEventType = "user.login_failed"
EventAnomalyDetected WebhookEventType = "security.anomaly_detected"
)
// WebhookStatus Webhook 状态
type WebhookStatus int
const (
WebhookStatusActive WebhookStatus = 1
WebhookStatusInactive WebhookStatus = 0
)
// Webhook Webhook 配置
type Webhook struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(100);not null" json:"name"`
URL string `gorm:"type:varchar(500);not null" json:"url"`
Secret string `gorm:"type:varchar(255)" json:"-"` // HMAC 签名密钥,不返回给前端
Events string `gorm:"type:text" json:"events"` // JSON 数组,订阅的事件类型
Status WebhookStatus `gorm:"default:1" json:"status"`
MaxRetries int `gorm:"default:3" json:"max_retries"`
TimeoutSec int `gorm:"default:10" json:"timeout_sec"`
CreatedBy int64 `gorm:"index" json:"created_by"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// TableName 指定表名
func (Webhook) TableName() string {
return "webhooks"
}
// WebhookDelivery Webhook 投递记录
type WebhookDelivery struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
WebhookID int64 `gorm:"index" json:"webhook_id"`
EventType WebhookEventType `gorm:"type:varchar(100)" json:"event_type"`
Payload string `gorm:"type:text" json:"payload"`
StatusCode int `json:"status_code"`
ResponseBody string `gorm:"type:text" json:"response_body"`
Attempt int `gorm:"default:1" json:"attempt"`
Success bool `gorm:"default:false" json:"success"`
Error string `gorm:"type:text" json:"error"`
DeliveredAt *time.Time `json:"delivered_at,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
// TableName 指定表名
func (WebhookDelivery) TableName() string {
return "webhook_deliveries"
}