feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
This commit is contained in:
232
internal/domain/announcement.go
Normal file
232
internal/domain/announcement.go
Normal 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
|
||||
}
|
||||
140
internal/domain/constants.go
Normal file
140
internal/domain/constants.go
Normal 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",
|
||||
}
|
||||
26
internal/domain/constants_test.go
Normal file
26
internal/domain/constants_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
127
internal/domain/custom_field.go
Normal file
127
internal/domain/custom_field.go
Normal 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
45
internal/domain/device.go
Normal 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"
|
||||
}
|
||||
21
internal/domain/jwt_test.go
Normal file
21
internal/domain/jwt_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
31
internal/domain/login_log.go
Normal file
31
internal/domain/login_log.go
Normal 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"
|
||||
}
|
||||
23
internal/domain/operation_log.go
Normal file
23
internal/domain/operation_log.go
Normal 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"
|
||||
}
|
||||
16
internal/domain/password_history.go
Normal file
16
internal/domain/password_history.go
Normal 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"
|
||||
}
|
||||
74
internal/domain/permission.go
Normal file
74
internal/domain/permission.go
Normal 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
57
internal/domain/role.go
Normal 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,
|
||||
},
|
||||
}
|
||||
16
internal/domain/role_permission.go
Normal file
16
internal/domain/role_permission.go
Normal 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"
|
||||
}
|
||||
78
internal/domain/social_account.go
Normal file
78
internal/domain/social_account.go
Normal 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,
|
||||
}
|
||||
}
|
||||
10
internal/domain/social_account_test.go
Normal file
10
internal/domain/social_account_test.go
Normal 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
39
internal/domain/theme.go
Normal 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
70
internal/domain/user.go
Normal 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 安全解引用 *string,nil 返回空字符串
|
||||
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"
|
||||
}
|
||||
16
internal/domain/user_role.go
Normal file
16
internal/domain/user_role.go
Normal 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"
|
||||
}
|
||||
81
internal/domain/user_test.go
Normal file
81
internal/domain/user_test.go
Normal 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
|
||||
}
|
||||
69
internal/domain/webhook.go
Normal file
69
internal/domain/webhook.go
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user