You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
432 lines
15 KiB
432 lines
15 KiB
// Code scaffolded by goctl. Safe to edit.
|
|
// goctl 1.9.2
|
|
|
|
package svc
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/casbin/casbin/v2"
|
|
casbinmodel "github.com/casbin/casbin/v2/model"
|
|
gormadapter "github.com/casbin/gorm-adapter/v3"
|
|
|
|
"github.com/youruser/base/internal/config"
|
|
"github.com/youruser/base/internal/middleware"
|
|
"github.com/youruser/base/internal/storage"
|
|
"github.com/youruser/base/model"
|
|
|
|
"gorm.io/driver/mysql"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/zeromicro/go-zero/rest"
|
|
)
|
|
|
|
const casbinModelText = `
|
|
[request_definition]
|
|
r = sub, obj, act
|
|
|
|
[policy_definition]
|
|
p = sub, obj, act
|
|
|
|
[role_definition]
|
|
g = _, _
|
|
|
|
[policy_effect]
|
|
e = some(where (p.eft == allow))
|
|
|
|
[matchers]
|
|
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && r.act == p.act
|
|
`
|
|
|
|
type ServiceContext struct {
|
|
Config config.Config
|
|
Cors rest.Middleware
|
|
Log rest.Middleware
|
|
Auth rest.Middleware
|
|
Authz rest.Middleware
|
|
// 数据库连接
|
|
DB *gorm.DB
|
|
// Casbin enforcer
|
|
Enforcer *casbin.Enforcer
|
|
// 文件存储
|
|
Storage storage.Storage
|
|
}
|
|
|
|
func NewServiceContext(c config.Config) *ServiceContext {
|
|
// 创建数据库连接
|
|
dsn := c.MySQL.DSN
|
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
|
if err != nil {
|
|
panic("Failed to connect database: " + err.Error())
|
|
}
|
|
|
|
// 自动迁移表
|
|
err = db.AutoMigrate(
|
|
&model.User{}, &model.Profile{}, &model.File{},
|
|
&model.Menu{}, &model.Role{}, &model.RoleMenu{},
|
|
&model.Organization{}, &model.UserOrganization{},
|
|
// AI models
|
|
&model.AIProvider{}, &model.AIModel{}, &model.AIApiKey{},
|
|
&model.AIConversation{}, &model.AIChatMessage{},
|
|
&model.AIUsageRecord{}, &model.AIUserQuota{},
|
|
)
|
|
if err != nil {
|
|
panic("Failed to migrate database: " + err.Error())
|
|
}
|
|
|
|
// 初始化 Casbin
|
|
enforcer := initCasbin(db)
|
|
|
|
// 种子超级管理员
|
|
seedSuperAdmin(db)
|
|
|
|
// 种子 Casbin 策略
|
|
seedCasbinPolicies(enforcer)
|
|
|
|
// 种子角色、菜单、角色-菜单关联
|
|
seedRoles(db)
|
|
seedMenus(db)
|
|
seedRoleMenus(db)
|
|
|
|
// 种子 AI 供应商和模型
|
|
seedAIProviders(db)
|
|
seedAIModels(db)
|
|
|
|
// 初始化存储
|
|
store, err := storage.NewStorage(c.Storage)
|
|
if err != nil {
|
|
panic("Failed to initialize storage: " + err.Error())
|
|
}
|
|
log.Printf("[Storage] Initialized with type: %s", c.Storage.Type)
|
|
|
|
return &ServiceContext{
|
|
Config: c,
|
|
Cors: middleware.NewCorsMiddleware().Handle,
|
|
Log: middleware.NewLogMiddleware().Handle,
|
|
Auth: middleware.NewAuthMiddleware().Handle,
|
|
Authz: middleware.NewAuthzMiddleware(enforcer).Handle,
|
|
DB: db,
|
|
Enforcer: enforcer,
|
|
Storage: store,
|
|
}
|
|
}
|
|
|
|
// Close 关闭资源
|
|
func (s *ServiceContext) Close() error {
|
|
if s.DB != nil {
|
|
sqlDB, err := s.DB.DB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return sqlDB.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// initCasbin 初始化 Casbin enforcer
|
|
func initCasbin(db *gorm.DB) *casbin.Enforcer {
|
|
// 使用 GORM adapter(自动创建 casbin_rule 表)
|
|
adapter, err := gormadapter.NewAdapterByDB(db)
|
|
if err != nil {
|
|
panic("Failed to create Casbin adapter: " + err.Error())
|
|
}
|
|
|
|
// 从字符串加载 model
|
|
m, err := casbinmodel.NewModelFromString(casbinModelText)
|
|
if err != nil {
|
|
panic("Failed to create Casbin model: " + err.Error())
|
|
}
|
|
|
|
enforcer, err := casbin.NewEnforcer(m, adapter)
|
|
if err != nil {
|
|
panic("Failed to create Casbin enforcer: " + err.Error())
|
|
}
|
|
|
|
// 加载策略
|
|
if err := enforcer.LoadPolicy(); err != nil {
|
|
panic("Failed to load Casbin policy: " + err.Error())
|
|
}
|
|
|
|
log.Println("[Casbin] Enforcer initialized successfully")
|
|
return enforcer
|
|
}
|
|
|
|
// seedSuperAdmin 首次启动创建超级管理员
|
|
func seedSuperAdmin(db *gorm.DB) {
|
|
ctx := context.Background()
|
|
|
|
existing, err := model.FindOneByUsername(ctx, db, "admin")
|
|
if err == nil {
|
|
if existing.Role != model.RoleSuperAdmin {
|
|
existing.Role = model.RoleSuperAdmin
|
|
existing.Source = model.SourceSystem
|
|
model.Update(ctx, db, existing)
|
|
log.Println("[Seed] Updated admin to super_admin role")
|
|
}
|
|
return
|
|
}
|
|
|
|
password := fmt.Sprintf("%x", md5.Sum([]byte("admin123")))
|
|
admin := &model.User{
|
|
Username: "admin",
|
|
Phone: "13800000000",
|
|
Email: "",
|
|
Password: password,
|
|
Role: model.RoleSuperAdmin,
|
|
Source: model.SourceSystem,
|
|
Remark: "系统自动创建的超级管理员",
|
|
Status: 1,
|
|
}
|
|
|
|
_, err = model.Insert(ctx, db, admin)
|
|
if err != nil {
|
|
log.Printf("[Seed] Failed to create super admin: %v", err)
|
|
return
|
|
}
|
|
log.Println("[Seed] Super admin created: admin / admin123")
|
|
}
|
|
|
|
// seedCasbinPolicies 种子 Casbin 策略(幂等)
|
|
func seedCasbinPolicies(enforcer *casbin.Enforcer) {
|
|
// 角色层级: super_admin > admin > user > guest
|
|
roleHierarchy := [][]string{
|
|
{"super_admin", "admin"},
|
|
{"admin", "user"},
|
|
{"user", "guest"},
|
|
}
|
|
for _, g := range roleHierarchy {
|
|
if has, _ := enforcer.HasGroupingPolicy(g[0], g[1]); !has {
|
|
enforcer.AddGroupingPolicy(g[0], g[1])
|
|
}
|
|
}
|
|
|
|
// 默认策略
|
|
policies := [][]string{
|
|
// guest: 仪表盘只读
|
|
{"guest", "/api/v1/dashboard/*", "GET"},
|
|
|
|
// user: 个人中心
|
|
{"user", "/api/v1/profile/*", "GET"},
|
|
{"user", "/api/v1/profile/*", "PUT"},
|
|
{"user", "/api/v1/profile/*", "POST"},
|
|
|
|
// admin: 用户管理(增查改)
|
|
{"admin", "/api/v1/users", "GET"},
|
|
{"admin", "/api/v1/user", "POST"},
|
|
{"admin", "/api/v1/user/:id", "GET"},
|
|
{"admin", "/api/v1/user/:id", "PUT"},
|
|
|
|
// super_admin: 用户删除
|
|
{"super_admin", "/api/v1/user/:id", "DELETE"},
|
|
|
|
// user: 文件管理
|
|
{"user", "/api/v1/file/upload", "POST"},
|
|
{"user", "/api/v1/files", "GET"},
|
|
{"user", "/api/v1/file/:id", "GET"},
|
|
{"user", "/api/v1/file/:id/url", "GET"},
|
|
{"user", "/api/v1/file/:id", "PUT"},
|
|
|
|
// super_admin: 文件删除
|
|
{"super_admin", "/api/v1/file/:id", "DELETE"},
|
|
|
|
// user: 个人机构相关
|
|
{"user", "/api/v1/profile/orgs", "GET"},
|
|
{"user", "/api/v1/profile/current-org", "PUT"},
|
|
|
|
// admin: 菜单管理(读取)
|
|
{"admin", "/api/v1/menus", "GET"},
|
|
|
|
// super_admin: 菜单管理(增删改)
|
|
{"super_admin", "/api/v1/menu", "POST"},
|
|
{"super_admin", "/api/v1/menu/:id", "PUT"},
|
|
{"super_admin", "/api/v1/menu/:id", "DELETE"},
|
|
|
|
// admin: 角色管理(读取)
|
|
{"admin", "/api/v1/roles", "GET"},
|
|
{"admin", "/api/v1/role/:id/menus", "GET"},
|
|
|
|
// super_admin: 角色管理(增删改)
|
|
{"super_admin", "/api/v1/role", "POST"},
|
|
{"super_admin", "/api/v1/role/:id", "PUT"},
|
|
{"super_admin", "/api/v1/role/:id", "DELETE"},
|
|
{"super_admin", "/api/v1/role/:id/menus", "PUT"},
|
|
|
|
// admin: 机构管理
|
|
{"admin", "/api/v1/organizations", "GET"},
|
|
{"admin", "/api/v1/organization", "POST"},
|
|
{"admin", "/api/v1/organization/:id", "PUT"},
|
|
{"admin", "/api/v1/organization/:id/members", "GET"},
|
|
{"admin", "/api/v1/organization/:id/member", "POST"},
|
|
{"admin", "/api/v1/organization/:id/member/:userId", "PUT"},
|
|
{"admin", "/api/v1/organization/:id/member/:userId", "DELETE"},
|
|
|
|
// super_admin: 机构删除
|
|
{"super_admin", "/api/v1/organization/:id", "DELETE"},
|
|
|
|
// AI: all authenticated users
|
|
{"user", "/api/v1/ai/chat/completions", "POST"},
|
|
{"user", "/api/v1/ai/conversations", "GET"},
|
|
{"user", "/api/v1/ai/conversation", "POST"},
|
|
{"user", "/api/v1/ai/conversation/:id", "GET"},
|
|
{"user", "/api/v1/ai/conversation/:id", "PUT"},
|
|
{"user", "/api/v1/ai/conversation/:id", "DELETE"},
|
|
{"user", "/api/v1/ai/models", "GET"},
|
|
{"user", "/api/v1/ai/quota/me", "GET"},
|
|
}
|
|
|
|
for _, p := range policies {
|
|
if has, _ := enforcer.HasPolicy(p[0], p[1], p[2]); !has {
|
|
enforcer.AddPolicy(p[0], p[1], p[2])
|
|
}
|
|
}
|
|
|
|
enforcer.SavePolicy()
|
|
log.Println("[Casbin] Policies seeded successfully")
|
|
}
|
|
|
|
// seedRoles 种子系统角色(幂等)
|
|
func seedRoles(db *gorm.DB) {
|
|
roles := []model.Role{
|
|
{Name: "超级管理员", Code: model.RoleSuperAdmin, Description: "系统超级管理员", IsSystem: true, SortOrder: 1, Status: 1},
|
|
{Name: "管理员", Code: model.RoleAdmin, Description: "系统管理员", IsSystem: true, SortOrder: 2, Status: 1},
|
|
{Name: "普通用户", Code: model.RoleUser, Description: "普通用户", IsSystem: true, SortOrder: 3, Status: 1},
|
|
{Name: "访客", Code: model.RoleGuest, Description: "访客", IsSystem: true, SortOrder: 4, Status: 1},
|
|
}
|
|
|
|
for _, r := range roles {
|
|
var existing model.Role
|
|
if err := db.Where("code = ?", r.Code).First(&existing).Error; err != nil {
|
|
db.Create(&r)
|
|
}
|
|
}
|
|
log.Println("[Seed] Roles seeded successfully")
|
|
}
|
|
|
|
// seedMenus 种子默认菜单(幂等)
|
|
func seedMenus(db *gorm.DB) {
|
|
menus := []model.Menu{
|
|
{Name: "我的", Path: "/my", Icon: "User", Type: "default", SortOrder: 1, Visible: true, Status: 1},
|
|
{Name: "仪表盘", Path: "/dashboard", Icon: "LayoutDashboard", Type: "config", SortOrder: 2, Visible: true, Status: 1},
|
|
{Name: "用户管理", Path: "/users", Icon: "Users", Type: "config", SortOrder: 3, Visible: true, Status: 1},
|
|
{Name: "文件管理", Path: "/files", Icon: "FolderOpen", Type: "config", SortOrder: 4, Visible: true, Status: 1},
|
|
{Name: "AI 对话", Path: "/ai/chat", Icon: "Bot", Type: "config", SortOrder: 5, Visible: true, Status: 1},
|
|
{Name: "角色管理", Path: "/roles", Icon: "Shield", Type: "config", SortOrder: 6, Visible: true, Status: 1},
|
|
{Name: "菜单管理", Path: "/menus", Icon: "Menu", Type: "config", SortOrder: 7, Visible: true, Status: 1},
|
|
{Name: "机构管理", Path: "/organizations", Icon: "Building2", Type: "config", SortOrder: 8, Visible: true, Status: 1},
|
|
{Name: "设置", Path: "/settings", Icon: "Settings", Type: "default", SortOrder: 9, Visible: true, Status: 1},
|
|
}
|
|
|
|
for _, m := range menus {
|
|
var existing model.Menu
|
|
if err := db.Where("path = ?", m.Path).First(&existing).Error; err != nil {
|
|
db.Create(&m)
|
|
}
|
|
}
|
|
log.Println("[Seed] Menus seeded successfully")
|
|
}
|
|
|
|
// seedRoleMenus 种子角色-菜单关联(幂等)
|
|
func seedRoleMenus(db *gorm.DB) {
|
|
// 获取所有角色
|
|
var roles []model.Role
|
|
db.Find(&roles)
|
|
|
|
// 获取所有菜单
|
|
var menus []model.Menu
|
|
db.Find(&menus)
|
|
|
|
if len(roles) == 0 || len(menus) == 0 {
|
|
return
|
|
}
|
|
|
|
// 构建菜单分类
|
|
var allMenuIds []int64
|
|
var defaultMenuIds []int64
|
|
for _, m := range menus {
|
|
allMenuIds = append(allMenuIds, m.Id)
|
|
if m.Type == "default" {
|
|
defaultMenuIds = append(defaultMenuIds, m.Id)
|
|
}
|
|
}
|
|
|
|
for _, r := range roles {
|
|
var menuIds []int64
|
|
switch r.Code {
|
|
case model.RoleSuperAdmin, model.RoleAdmin:
|
|
menuIds = allMenuIds
|
|
case model.RoleUser, model.RoleGuest:
|
|
menuIds = defaultMenuIds
|
|
}
|
|
|
|
// 获取已有的菜单关联
|
|
var existingMenuIds []int64
|
|
db.Model(&model.RoleMenu{}).Where("role_id = ?", r.Id).Pluck("menu_id", &existingMenuIds)
|
|
existingSet := make(map[int64]bool)
|
|
for _, id := range existingMenuIds {
|
|
existingSet[id] = true
|
|
}
|
|
|
|
// 添加缺失的菜单关联
|
|
var newRecords []model.RoleMenu
|
|
for _, menuId := range menuIds {
|
|
if !existingSet[menuId] {
|
|
newRecords = append(newRecords, model.RoleMenu{RoleId: r.Id, MenuId: menuId})
|
|
}
|
|
}
|
|
if len(newRecords) > 0 {
|
|
db.Create(&newRecords)
|
|
}
|
|
}
|
|
log.Println("[Seed] RoleMenus seeded successfully")
|
|
}
|
|
|
|
// seedAIProviders 种子 AI 供应商(幂等)
|
|
func seedAIProviders(db *gorm.DB) {
|
|
providers := []model.AIProvider{
|
|
{Name: "openai", DisplayName: "OpenAI", BaseUrl: "https://api.openai.com/v1", SdkType: "openai_compat", Protocol: "openai", IsActive: true, SortOrder: 1},
|
|
{Name: "claude", DisplayName: "Anthropic Claude", BaseUrl: "https://api.anthropic.com", SdkType: "anthropic", Protocol: "anthropic", IsActive: true, SortOrder: 2},
|
|
{Name: "qwen", DisplayName: "阿里千问", BaseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", SdkType: "openai_compat", Protocol: "openai", IsActive: true, SortOrder: 3},
|
|
{Name: "zhipu", DisplayName: "智谱 GLM", BaseUrl: "https://open.bigmodel.cn/api/paas/v4", SdkType: "openai_compat", Protocol: "openai", IsActive: true, SortOrder: 4},
|
|
{Name: "deepseek", DisplayName: "DeepSeek", BaseUrl: "https://api.deepseek.com/v1", SdkType: "openai_compat", Protocol: "openai", IsActive: true, SortOrder: 5},
|
|
}
|
|
for _, p := range providers {
|
|
var existing model.AIProvider
|
|
if err := db.Where("name = ?", p.Name).First(&existing).Error; err != nil {
|
|
db.Create(&p)
|
|
}
|
|
}
|
|
log.Println("[Seed] AI providers seeded successfully")
|
|
}
|
|
|
|
// seedAIModels 种子 AI 模型(幂等)
|
|
func seedAIModels(db *gorm.DB) {
|
|
// First, fetch providers by name to get their IDs
|
|
providerIds := map[string]int64{}
|
|
var providers []model.AIProvider
|
|
db.Find(&providers)
|
|
for _, p := range providers {
|
|
providerIds[p.Name] = p.Id
|
|
}
|
|
|
|
models := []model.AIModel{
|
|
{ProviderId: providerIds["openai"], ModelId: "gpt-4o", DisplayName: "GPT-4o", InputPrice: 0.0025, OutputPrice: 0.01, MaxTokens: 16384, ContextWindow: 128000, SupportsStream: true, SupportsVision: true, IsActive: true, SortOrder: 1},
|
|
{ProviderId: providerIds["openai"], ModelId: "gpt-4o-mini", DisplayName: "GPT-4o Mini", InputPrice: 0.00015, OutputPrice: 0.0006, MaxTokens: 16384, ContextWindow: 128000, SupportsStream: true, SupportsVision: true, IsActive: true, SortOrder: 2},
|
|
{ProviderId: providerIds["claude"], ModelId: "claude-sonnet-4-5-20250514", DisplayName: "Claude Sonnet 4.5", InputPrice: 0.003, OutputPrice: 0.015, MaxTokens: 8192, ContextWindow: 200000, SupportsStream: true, SupportsVision: true, IsActive: true, SortOrder: 3},
|
|
{ProviderId: providerIds["claude"], ModelId: "claude-haiku-3-5-20241022", DisplayName: "Claude Haiku 3.5", InputPrice: 0.0008, OutputPrice: 0.004, MaxTokens: 8192, ContextWindow: 200000, SupportsStream: true, SupportsVision: true, IsActive: true, SortOrder: 4},
|
|
{ProviderId: providerIds["qwen"], ModelId: "qwen-plus", DisplayName: "通义千问 Plus", InputPrice: 0.0008, OutputPrice: 0.002, MaxTokens: 8192, ContextWindow: 131072, SupportsStream: true, SupportsVision: false, IsActive: true, SortOrder: 5},
|
|
{ProviderId: providerIds["qwen"], ModelId: "qwen-turbo", DisplayName: "通义千问 Turbo", InputPrice: 0.0003, OutputPrice: 0.0006, MaxTokens: 8192, ContextWindow: 131072, SupportsStream: true, SupportsVision: false, IsActive: true, SortOrder: 6},
|
|
{ProviderId: providerIds["zhipu"], ModelId: "glm-4-flash", DisplayName: "GLM-4 Flash", InputPrice: 0.0001, OutputPrice: 0.0001, MaxTokens: 4096, ContextWindow: 128000, SupportsStream: true, SupportsVision: false, IsActive: true, SortOrder: 7},
|
|
{ProviderId: providerIds["deepseek"], ModelId: "deepseek-chat", DisplayName: "DeepSeek Chat", InputPrice: 0.00014, OutputPrice: 0.00028, MaxTokens: 8192, ContextWindow: 64000, SupportsStream: true, SupportsVision: false, IsActive: true, SortOrder: 8},
|
|
{ProviderId: providerIds["deepseek"], ModelId: "deepseek-reasoner", DisplayName: "DeepSeek Reasoner", InputPrice: 0.00055, OutputPrice: 0.00219, MaxTokens: 8192, ContextWindow: 64000, SupportsStream: true, SupportsVision: false, IsActive: true, SortOrder: 9},
|
|
}
|
|
for _, m := range models {
|
|
var existing model.AIModel
|
|
if err := db.Where("model_id = ?", m.ModelId).First(&existing).Error; err != nil {
|
|
db.Create(&m)
|
|
}
|
|
}
|
|
log.Println("[Seed] AI models seeded successfully")
|
|
}
|
|
|