This commit is contained in:
2026-08-13 21:51:20 +08:00
parent be5362913b
commit 21c264fcd7
60 changed files with 10174 additions and 69 deletions
+941
View File
@@ -0,0 +1,941 @@
package data
import (
"context"
"errors"
"strconv"
"strings"
"time"
"kra/internal/biz"
"github.com/casbin/casbin/v3"
casbinmodel "github.com/casbin/casbin/v3/model"
"gorm.io/gorm"
)
type apiPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Path string `gorm:"uniqueIndex:idx_api_path_method"`
Description string
APIGroup string `gorm:"column:api_group"`
Method string `gorm:"uniqueIndex:idx_api_path_method"`
}
func (apiPO) TableName() string { return "sys_apis" }
type ignoredAPIPO struct {
Path string `gorm:"primaryKey"`
Method string `gorm:"primaryKey"`
}
func (ignoredAPIPO) TableName() string { return "sys_ignore_apis" }
type authorityAPIPO struct {
AuthorityID uint `gorm:"primaryKey;column:authority_id"`
APIID uint `gorm:"primaryKey;column:api_id"`
}
func (authorityAPIPO) TableName() string { return "sys_authority_apis" }
type menuButtonPO struct {
ID uint `gorm:"primaryKey"`
Name string
Description string `gorm:"column:desc"`
MenuID uint `gorm:"column:sys_base_menu_id"`
}
func (menuButtonPO) TableName() string { return "sys_base_menu_btns" }
type authorityButtonPO struct {
AuthorityID uint `gorm:"primaryKey;column:authority_id"`
MenuID uint `gorm:"primaryKey;column:sys_menu_id"`
ButtonID uint `gorm:"primaryKey;column:sys_base_menu_btn_id"`
}
func (authorityButtonPO) TableName() string { return "sys_authority_btns" }
type departmentPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string `gorm:"index"`
ParentID uint
Ancestors string
Sort int
LeaderID uint
Status bool `gorm:"default:true"`
}
func (departmentPO) TableName() string { return "sys_departments" }
type positionPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string `gorm:"index"`
Code string
Sort int
Status bool `gorm:"default:true"`
Remark string
}
func (positionPO) TableName() string { return "sys_positions" }
type userDepartmentPO struct {
UserID uint `gorm:"primaryKey;column:sys_user_id"`
DepartmentID uint `gorm:"primaryKey;column:sys_department_id"`
}
func (userDepartmentPO) TableName() string { return "sys_user_departments" }
type userPositionPO struct {
UserID uint `gorm:"primaryKey;column:sys_user_id"`
PositionID uint `gorm:"primaryKey;column:sys_position_id"`
}
func (userPositionPO) TableName() string { return "sys_user_positions" }
type authorityDepartmentPO struct {
AuthorityID uint `gorm:"primaryKey;column:authority_id"`
DepartmentID uint `gorm:"primaryKey;column:department_id"`
}
func (authorityDepartmentPO) TableName() string { return "sys_authority_departments" }
type accessRepo struct{ data *Data }
func NewAccessRepo(data *Data) biz.AccessRepo { return &accessRepo{data: data} }
func (r *accessRepo) CreateAuthority(ctx context.Context, value *biz.Authority) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("存在相同角色id")
}
if value.DefaultRouter == "" {
value.DefaultRouter = "dashboard"
}
if err := tx.Create(&authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}).Error; err != nil {
return err
}
var dashboard menuPO
if err := tx.Where("name = ?", "dashboard").First(&dashboard).Error; err == nil {
if err = tx.Create(&authorityMenuPO{SysAuthorityAuthorityID: value.AuthorityID, SysBaseMenuID: dashboard.ID}).Error; err != nil {
return err
}
}
defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
for _, item := range defaults {
var api apiPO
if err := tx.Where("path = ? AND method = ?", item.path, item.method).First(&api).Error; err == nil {
if err = tx.Create(&authorityAPIPO{AuthorityID: value.AuthorityID, APIID: api.ID}).Error; err != nil {
return err
}
}
}
return nil
})
}
func (r *accessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *biz.Authority) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Count(&count).Error; err != nil {
return err
}
if count != 0 {
return errors.New("存在相同角色id")
}
if err := tx.Create(&authorityPO{AuthorityID: value.AuthorityID, AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}).Error; err != nil {
return err
}
copyLinks := func(table string, destination any, columns map[string]any) error {
return tx.Table(table).Where(columns).Find(destination).Error
}
var menus []authorityMenuPO
if err := copyLinks("sys_authority_menus", &menus, map[string]any{"sys_authority_authority_id": sourceID}); err != nil {
return err
}
for i := range menus {
menus[i].SysAuthorityAuthorityID = value.AuthorityID
}
if len(menus) > 0 {
if err := tx.Create(&menus).Error; err != nil {
return err
}
}
var apis []authorityAPIPO
if err := copyLinks("sys_authority_apis", &apis, map[string]any{"authority_id": sourceID}); err != nil {
return err
}
for i := range apis {
apis[i].AuthorityID = value.AuthorityID
}
if len(apis) > 0 {
if err := tx.Create(&apis).Error; err != nil {
return err
}
}
var buttons []authorityButtonPO
if err := copyLinks("sys_authority_btns", &buttons, map[string]any{"authority_id": sourceID}); err != nil {
return err
}
for i := range buttons {
buttons[i].AuthorityID = value.AuthorityID
}
if len(buttons) > 0 {
if err := tx.Create(&buttons).Error; err != nil {
return err
}
}
var departments []authorityDepartmentPO
if err := copyLinks("sys_authority_departments", &departments, map[string]any{"authority_id": sourceID}); err != nil {
return err
}
for i := range departments {
departments[i].AuthorityID = value.AuthorityID
}
if len(departments) > 0 {
return tx.Create(&departments).Error
}
return nil
})
}
func (r *accessRepo) UpdateAuthority(ctx context.Context, value *biz.Authority) error {
return r.data.gormDB.WithContext(ctx).Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Updates(map[string]any{"authority_name": value.AuthorityName, "parent_id": value.ParentID, "data_scope": value.DataScope, "default_router": value.DefaultRouter}).Error
}
func (r *accessRepo) DeleteAuthority(ctx context.Context, id uint) error {
if id == 888 {
return errors.New("不能删除超级管理员")
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var users, children int64
if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Count(&users).Error; err != nil {
return err
}
if users > 0 {
return errors.New("此角色有用户正在使用禁止删除")
}
if err := tx.Model(&userPO{}).Where("authority_id = ?", id).Count(&users).Error; err != nil {
return err
}
if users > 0 {
return errors.New("此角色有用户正在使用禁止删除")
}
if err := tx.Model(&authorityPO{}).Where("parent_id = ?", id).Count(&children).Error; err != nil {
return err
}
if children > 0 {
return errors.New("此角色存在子角色不允许删除")
}
if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
return err
}
if err := tx.Where("authority_id = ?", id).Delete(&authorityAPIPO{}).Error; err != nil {
return err
}
if err := tx.Where("authority_id = ?", id).Delete(&authorityButtonPO{}).Error; err != nil {
return err
}
if err := tx.Where("authority_id = ?", id).Delete(&authorityDepartmentPO{}).Error; err != nil {
return err
}
return tx.Delete(&authorityPO{}, "authority_id = ?", id).Error
})
}
func (r *accessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
var pos []authorityPO
if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.Authority, 0, len(pos))
for _, po := range pos {
v := toBizAuthority(po)
out = append(out, &v)
}
return out, nil
}
func (r *accessRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
return err
}
links := make([]authorityMenuPO, 0, len(ids))
for _, mid := range ids {
links = append(links, authorityMenuPO{SysAuthorityAuthorityID: id, SysBaseMenuID: mid})
}
if len(links) > 0 {
return tx.Create(&links).Error
}
return nil
})
}
func (r *accessRepo) AuthorityMenuIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_base_menu_id", &ids).Error
return ids, err
}
func (r *accessRepo) SetAuthorityUsers(ctx context.Context, id uint, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var oldIDs []uint
if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &oldIDs).Error; err != nil {
return err
}
if err := tx.Where("sys_authority_authority_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
return err
}
links := make([]userAuthorityPO, 0, len(ids))
for _, uid := range ids {
links = append(links, userAuthorityPO{SysUserID: uid, SysAuthorityAuthorityID: id})
}
if len(links) > 0 {
if err := tx.Create(&links).Error; err != nil {
return err
}
}
selected := map[uint]bool{}
for _, uid := range ids {
selected[uid] = true
}
for _, uid := range oldIDs {
if selected[uid] {
continue
}
var user userPO
if err := tx.First(&user, uid).Error; err == nil && user.AuthorityID == id {
if err = tx.Model(&user).Update("authority_id", 888).Error; err != nil {
return err
}
fallback := userAuthorityPO{SysUserID: uid, SysAuthorityAuthorityID: 888}
if err = tx.FirstOrCreate(&fallback, fallback).Error; err != nil {
return err
}
}
}
return nil
})
}
func (r *accessRepo) AuthorityUserIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &ids).Error
return ids, err
}
func (r *accessRepo) SetDataScope(ctx context.Context, id uint, scope int, deptIDs []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", id).Update("data_scope", scope).Error; err != nil {
return err
}
if err := tx.Where("authority_id = ?", id).Delete(&authorityDepartmentPO{}).Error; err != nil {
return err
}
links := make([]authorityDepartmentPO, 0, len(deptIDs))
for _, did := range deptIDs {
links = append(links, authorityDepartmentPO{AuthorityID: id, DepartmentID: did})
}
if len(links) > 0 {
return tx.Create(&links).Error
}
return nil
})
}
func (r *accessRepo) DataScopeDepartmentIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityDepartmentPO{}).Where("authority_id = ?", id).Pluck("department_id", &ids).Error
return ids, err
}
func (r *accessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
if authorityID == 888 {
return biz.DataScope{All: true}, nil
}
var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).First(&authority, "authority_id = ?", authorityID).Error; err != nil {
return biz.DataScope{}, err
}
if authority.DataScope == 1 {
return biz.DataScope{All: true}, nil
}
if authority.DataScope == 4 {
return biz.DataScope{OwnerUserID: userID}, nil
}
var ids []uint
if authority.DataScope == 5 {
var err error
ids, err = r.DataScopeDepartmentIDs(ctx, authorityID)
if err != nil {
return biz.DataScope{}, err
}
} else {
if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error; err != nil {
return biz.DataScope{}, err
}
if authority.DataScope == 2 && len(ids) > 0 {
var departments []departmentPO
if err := r.data.gormDB.WithContext(ctx).Find(&departments).Error; err != nil {
return biz.DataScope{}, err
}
selected := make(map[uint]bool, len(ids))
for _, id := range ids {
selected[id] = true
}
for _, department := range departments {
for _, part := range strings.Split(department.Ancestors, ",") {
value, _ := strconv.ParseUint(part, 10, 64)
if selected[uint(value)] {
selected[department.ID] = true
break
}
}
}
ids = ids[:0]
for id := range selected {
ids = append(ids, id)
}
}
}
return biz.DataScope{DepartmentIDs: ids}, nil
}
func menuFromPO(po menuPO) *biz.Menu {
return &biz.Menu{ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType}
}
func menuToPO(v *biz.Menu) menuPO {
return menuPO{ID: v.ID, ParentID: v.ParentID, Path: v.Path, Name: v.Name, Hidden: v.Hidden, Component: v.Component, Sort: v.Sort, ActiveName: v.ActiveName, KeepAlive: v.KeepAlive, DefaultMenu: v.DefaultMenu, Title: v.Title, Icon: v.Icon, CloseTab: v.CloseTab, TransitionType: v.TransitionType}
}
func (r *accessRepo) CreateMenu(ctx context.Context, v *biz.Menu) error {
po := menuToPO(v)
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
return nil
}
func (r *accessRepo) UpdateMenu(ctx context.Context, v *biz.Menu) error {
po := menuToPO(v)
return r.data.gormDB.WithContext(ctx).Model(&menuPO{}).Where("id = ?", v.ID).Updates(&po).Error
}
func (r *accessRepo) DeleteMenu(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64
if err := tx.Model(&menuPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("此菜单存在子菜单不可删除")
}
if err := tx.Where("sys_base_menu_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
return err
}
return tx.Delete(&menuPO{}, id).Error
})
}
func (r *accessRepo) FindMenu(ctx context.Context, id uint) (*biz.Menu, error) {
var po menuPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return menuFromPO(po), nil
}
func (r *accessRepo) ListMenus(ctx context.Context) ([]*biz.Menu, error) {
var pos []menuPO
if err := r.data.gormDB.WithContext(ctx).Order("sort,id").Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.Menu, 0, len(pos))
for _, po := range pos {
out = append(out, menuFromPO(po))
}
return out, nil
}
func (r *accessRepo) MenuRoleIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}).Where("sys_base_menu_id = ?", id).Pluck("sys_authority_authority_id", &ids).Error
return ids, err
}
func (r *accessRepo) SetMenuRoles(ctx context.Context, id uint, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_base_menu_id = ?", id).Delete(&authorityMenuPO{}).Error; err != nil {
return err
}
links := make([]authorityMenuPO, 0, len(ids))
for _, aid := range ids {
links = append(links, authorityMenuPO{SysAuthorityAuthorityID: aid, SysBaseMenuID: id})
}
if len(links) > 0 {
return tx.Create(&links).Error
}
return nil
})
}
func (r *accessRepo) ReplaceMenuButtons(ctx context.Context, menuID uint, buttons []*biz.MenuButton) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var oldIDs []uint
if err := tx.Model(&menuButtonPO{}).Where("sys_base_menu_id = ?", menuID).Pluck("id", &oldIDs).Error; err != nil {
return err
}
if len(oldIDs) > 0 {
if err := tx.Where("sys_base_menu_btn_id IN ?", oldIDs).Delete(&authorityButtonPO{}).Error; err != nil {
return err
}
}
if err := tx.Where("sys_base_menu_id = ?", menuID).Delete(&menuButtonPO{}).Error; err != nil {
return err
}
pos := make([]menuButtonPO, 0, len(buttons))
for _, button := range buttons {
pos = append(pos, menuButtonPO{Name: button.Name, Description: button.Description, MenuID: menuID})
}
if len(pos) > 0 {
return tx.Create(&pos).Error
}
return nil
})
}
func apiFromPO(po apiPO) *biz.API {
return &biz.API{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Path: po.Path, Description: po.Description, APIGroup: po.APIGroup, Method: po.Method}
}
func (r *accessRepo) CreateAPI(ctx context.Context, v *biz.API) error {
return r.data.gormDB.WithContext(ctx).Create(&apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: strings.ToUpper(v.Method)}).Error
}
func (r *accessRepo) UpdateAPI(ctx context.Context, v *biz.API) error {
return r.data.gormDB.WithContext(ctx).Model(&apiPO{}).Where("id = ?", v.ID).Updates(map[string]any{"path": v.Path, "description": v.Description, "api_group": v.APIGroup, "method": strings.ToUpper(v.Method)}).Error
}
func (r *accessRepo) DeleteAPIs(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil {
return err
}
return tx.Delete(&apiPO{}, ids).Error
})
}
func (r *accessRepo) FindAPI(ctx context.Context, id uint) (*biz.API, error) {
var po apiPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return apiFromPO(po), nil
}
func (r *accessRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*biz.API, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&apiPO{})
if q != nil {
if q.Path != "" {
db = db.Where("path LIKE ?", "%"+q.Path+"%")
}
if q.APIGroup != "" {
db = db.Where("api_group = ?", q.APIGroup)
}
if q.Method != "" {
db = db.Where("method = ?", strings.ToUpper(q.Method))
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []apiPO
if err := db.Order("api_group,path,method").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.API, 0, len(pos))
for _, po := range pos {
out = append(out, apiFromPO(po))
}
return out, total, nil
}
func (r *accessRepo) APIRoleIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityAPIPO{}).Where("api_id = ?", id).Pluck("authority_id", &ids).Error
return ids, err
}
func (r *accessRepo) SetAPIRoles(ctx context.Context, id uint, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("api_id = ?", id).Delete(&authorityAPIPO{}).Error; err != nil {
return err
}
links := make([]authorityAPIPO, 0, len(ids))
for _, aid := range ids {
links = append(links, authorityAPIPO{AuthorityID: aid, APIID: id})
}
if len(links) > 0 {
return tx.Create(&links).Error
}
return nil
})
}
func (r *accessRepo) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) {
policies, err := r.PolicyPaths(ctx, aid)
if err != nil {
return false, err
}
model, err := casbinmodel.NewModelFromString(`[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)`)
if err != nil {
return false, err
}
enforcer, err := casbin.NewEnforcer(model)
if err != nil {
return false, err
}
subject := strconv.FormatUint(uint64(aid), 10)
for _, policy := range policies {
if _, err := enforcer.AddPolicy(subject, policy.Path, strings.ToUpper(policy.Method)); err != nil {
return false, err
}
}
return enforcer.Enforce(subject, path, strings.ToUpper(method))
}
func (r *accessRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, error) {
var pos []apiPO
err := r.data.gormDB.WithContext(ctx).Table("sys_apis").Joins("JOIN sys_authority_apis ON sys_authority_apis.api_id = sys_apis.id").Where("sys_authority_apis.authority_id = ? AND sys_apis.deleted_at IS NULL", aid).Order("sys_apis.path,sys_apis.method").Find(&pos).Error
if err != nil {
return nil, err
}
out := make([]*biz.API, 0, len(pos))
for _, po := range pos {
out = append(out, apiFromPO(po))
}
return out, nil
}
func (r *accessRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("authority_id = ?", aid).Delete(&authorityAPIPO{}).Error; err != nil {
return err
}
links := make([]authorityAPIPO, 0, len(paths))
for _, path := range paths {
var po apiPO
err := tx.Where("path = ? AND method = ?", path.Path, strings.ToUpper(path.Method)).First(&po).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
po = apiPO{Path: path.Path, Method: strings.ToUpper(path.Method), Description: path.Description, APIGroup: path.APIGroup}
if err = tx.Create(&po).Error; err != nil {
return err
}
} else if err != nil {
return err
}
links = append(links, authorityAPIPO{AuthorityID: aid, APIID: po.ID})
}
if len(links) > 0 {
return tx.Create(&links).Error
}
return nil
})
}
func (r *accessRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) {
var pos []ignoredAPIPO
if err := r.data.gormDB.WithContext(ctx).Order("path,method").Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.API, 0, len(pos))
for _, po := range pos {
out = append(out, &biz.API{Path: po.Path, Method: po.Method})
}
return out, nil
}
func (r *accessRepo) SetAPIIgnored(ctx context.Context, path, method string, ignored bool) error {
po := ignoredAPIPO{Path: path, Method: strings.ToUpper(method)}
if ignored {
return r.data.gormDB.WithContext(ctx).FirstOrCreate(&po, po).Error
}
return r.data.gormDB.WithContext(ctx).Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error
}
func (r *accessRepo) ApplyAPISync(ctx context.Context, added, deleted []*biz.API) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for _, item := range added {
po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup}
if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
return err
}
}
for _, item := range deleted {
var ids []uint
if err := tx.Model(&apiPO{}).Where("path = ? AND method = ?", item.Path, strings.ToUpper(item.Method)).Pluck("id", &ids).Error; err != nil {
return err
}
if len(ids) > 0 {
if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil {
return err
}
if err := tx.Where("id IN ?", ids).Delete(&apiPO{}).Error; err != nil {
return err
}
}
}
return nil
})
}
func (r *accessRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuButton, error) {
var pos []menuButtonPO
if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", menuID).Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.MenuButton, 0, len(pos))
for _, po := range pos {
out = append(out, &biz.MenuButton{ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID})
}
return out, nil
}
func (r *accessRepo) SetAuthorityButtons(ctx context.Context, aid uint, buttons map[uint][]uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("authority_id = ?", aid).Delete(&authorityButtonPO{}).Error; err != nil {
return err
}
var links []authorityButtonPO
for menuID, ids := range buttons {
for _, id := range ids {
links = append(links, authorityButtonPO{AuthorityID: aid, MenuID: menuID, ButtonID: id})
}
}
if len(links) > 0 {
return tx.Create(&links).Error
}
return nil
})
}
func (r *accessRepo) SelectedButtons(ctx context.Context, aid, menuID uint) ([]uint, error) {
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Pluck("sys_base_menu_btn_id", &ids).Error
return ids, err
}
func (r *accessRepo) SetSelectedButtons(ctx context.Context, aid, menuID uint, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Delete(&authorityButtonPO{}).Error; err != nil {
return err
}
links := make([]authorityButtonPO, 0, len(ids))
for _, id := range ids {
links = append(links, authorityButtonPO{AuthorityID: aid, MenuID: menuID, ButtonID: id})
}
if len(links) > 0 {
return tx.Create(&links).Error
}
return nil
})
}
func (r *accessRepo) CanRemoveButton(ctx context.Context, id uint) (bool, error) {
var count int64
err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("sys_base_menu_btn_id = ?", id).Count(&count).Error
return count == 0, err
}
func (r *accessRepo) AuthorityButtonIDs(ctx context.Context, aid uint) ([]uint, error) {
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("authority_id = ?", aid).Pluck("sys_base_menu_btn_id", &ids).Error
return ids, err
}
func deptFromPO(po departmentPO) *biz.Department {
return &biz.Department{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID, Ancestors: po.Ancestors, Sort: po.Sort, LeaderID: po.LeaderID, Status: po.Status}
}
func (r *accessRepo) CreateDepartment(ctx context.Context, v *biz.Department) error {
v.Ancestors = "0"
if v.ParentID != 0 {
var parent departmentPO
if err := r.data.gormDB.WithContext(ctx).First(&parent, v.ParentID).Error; err != nil {
return err
}
v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",")
}
return r.data.gormDB.WithContext(ctx).Create(&departmentPO{Name: v.Name, ParentID: v.ParentID, Ancestors: v.Ancestors, Sort: v.Sort, LeaderID: v.LeaderID, Status: v.Status}).Error
}
func (r *accessRepo) UpdateDepartment(ctx context.Context, v *biz.Department) error {
v.Ancestors = "0"
if v.ParentID != 0 {
var parent departmentPO
if err := r.data.gormDB.WithContext(ctx).First(&parent, v.ParentID).Error; err != nil {
return err
}
v.Ancestors = strings.Trim(parent.Ancestors+","+strconv.FormatUint(uint64(parent.ID), 10), ",")
}
return r.data.gormDB.WithContext(ctx).Model(&departmentPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "parent_id": v.ParentID, "ancestors": v.Ancestors, "sort": v.Sort, "leader_id": v.LeaderID, "status": v.Status}).Error
}
func (r *accessRepo) DeleteDepartment(ctx context.Context, id uint) error {
var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&departmentPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("部门存在子部门")
}
return r.data.gormDB.WithContext(ctx).Delete(&departmentPO{}, id).Error
}
func (r *accessRepo) FindDepartment(ctx context.Context, id uint) (*biz.Department, error) {
var po departmentPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return deptFromPO(po), nil
}
func (r *accessRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) {
var pos []departmentPO
db := r.data.gormDB.WithContext(ctx).Order("sort,id")
if name != "" {
db = db.Where("name LIKE ?", "%"+name+"%")
}
if err := db.Find(&pos).Error; err != nil {
return nil, err
}
nodes := map[uint]*biz.Department{}
for _, po := range pos {
nodes[po.ID] = deptFromPO(po)
}
var roots []*biz.Department
for _, po := range pos {
n := nodes[po.ID]
if p := nodes[po.ParentID]; p != nil {
p.Children = append(p.Children, n)
} else {
roots = append(roots, n)
}
}
return roots, nil
}
func (r *accessRepo) DepartmentUserIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Pluck("sys_user_id", &ids).Error
return ids, err
}
func (r *accessRepo) SetDepartmentUsers(ctx context.Context, id uint, ids []uint) error {
return r.replaceUserDepartments(ctx, id, ids)
}
func (r *accessRepo) replaceUserDepartments(ctx context.Context, id uint, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_department_id = ?", id).Delete(&userDepartmentPO{}).Error; err != nil {
return err
}
links := make([]userDepartmentPO, 0, len(ids))
for _, uid := range ids {
links = append(links, userDepartmentPO{UserID: uid, DepartmentID: id})
}
if len(links) > 0 {
return tx.Create(&links).Error
}
return nil
})
}
func (r *accessRepo) SetUserDepartments(ctx context.Context, uid uint, ids []uint, primary uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_user_id = ?", uid).Delete(&userDepartmentPO{}).Error; err != nil {
return err
}
links := make([]userDepartmentPO, 0, len(ids))
for _, id := range ids {
links = append(links, userDepartmentPO{UserID: uid, DepartmentID: id})
}
if len(links) > 0 {
if err := tx.Create(&links).Error; err != nil {
return err
}
}
if primary == 0 && len(ids) > 0 {
primary = ids[0]
}
return tx.Model(&userPO{}).Where("id = ?", uid).Update("dept_id", primary).Error
})
}
func posFromPO(po positionPO) *biz.Position {
return &biz.Position{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Code: po.Code, Sort: po.Sort, Status: po.Status, Remark: po.Remark}
}
func (r *accessRepo) CreatePosition(ctx context.Context, v *biz.Position) error {
return r.data.gormDB.WithContext(ctx).Create(&positionPO{Name: v.Name, Code: v.Code, Sort: v.Sort, Status: v.Status, Remark: v.Remark}).Error
}
func (r *accessRepo) UpdatePosition(ctx context.Context, v *biz.Position) error {
return r.data.gormDB.WithContext(ctx).Model(&positionPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "code": v.Code, "sort": v.Sort, "status": v.Status, "remark": v.Remark}).Error
}
func (r *accessRepo) DeletePosition(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&positionPO{}, id).Error
}
func (r *accessRepo) FindPosition(ctx context.Context, id uint) (*biz.Position, error) {
var po positionPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return posFromPO(po), nil
}
func (r *accessRepo) ListPositions(ctx context.Context, page, size int, q *biz.Position) ([]*biz.Position, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&positionPO{})
if q != nil {
if q.Name != "" {
db = db.Where("name LIKE ?", "%"+q.Name+"%")
}
if q.Code != "" {
db = db.Where("code LIKE ?", "%"+q.Code+"%")
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []positionPO
if err := db.Order("sort,id").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.Position, 0, len(pos))
for _, po := range pos {
out = append(out, posFromPO(po))
}
return out, total, nil
}
func (r *accessRepo) PositionUserIDs(ctx context.Context, id uint) ([]uint, error) {
var ids []uint
err := r.data.gormDB.WithContext(ctx).Model(&userPositionPO{}).Where("sys_position_id = ?", id).Pluck("sys_user_id", &ids).Error
return ids, err
}
func (r *accessRepo) SetPositionUsers(ctx context.Context, id uint, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_position_id = ?", id).Delete(&userPositionPO{}).Error; err != nil {
return err
}
links := make([]userPositionPO, 0, len(ids))
for _, uid := range ids {
links = append(links, userPositionPO{UserID: uid, PositionID: id})
}
if len(links) > 0 {
return tx.Create(&links).Error
}
return nil
})
}
func (r *accessRepo) SetUserPositions(ctx context.Context, uid uint, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_user_id = ?", uid).Delete(&userPositionPO{}).Error; err != nil {
return err
}
links := make([]userPositionPO, 0, len(ids))
for _, id := range ids {
links = append(links, userPositionPO{UserID: uid, PositionID: id})
}
if len(links) > 0 {
return tx.Create(&links).Error
}
return nil
})
}
+430
View File
@@ -0,0 +1,430 @@
package data
import (
"bytes"
"context"
"errors"
"gorm.io/gorm"
"io/fs"
"kra/internal/biz"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
type operationPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
IP, Method, Path string
Status int
LatencyMS int64 `gorm:"column:latency_ms"`
Agent string `gorm:"type:text"`
ErrorMessage string
Body string `gorm:"type:text"`
Response string `gorm:"column:resp;type:text"`
UserID uint
RequestID string `gorm:"index"`
TraceID string `gorm:"index"`
DeviceID string
}
func (operationPO) TableName() string { return "sys_operation_records" }
type loginLogPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Username, IP string
Status bool
ErrorMessage, Agent string
UserID uint
}
func (loginLogPO) TableName() string { return "sys_login_logs" }
type dataAccessLogPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
EventType, TargetTable, Operation string
UserID, AuthorityID uint
Scope int
RequestID, Method, Path, Detail string
}
func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" }
type errorRecordPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Form string `gorm:"type:text"`
Info string `gorm:"type:text"`
Level string
RequestID string `gorm:"index"`
TraceID string `gorm:"index"`
Solution string `gorm:"type:text"`
Status string `gorm:"default:未处理"`
}
func (errorRecordPO) TableName() string { return "sys_error" }
type auditRepo struct{ data *Data }
func NewAuditRepo(data *Data) biz.AuditRepo { return &auditRepo{data: data} }
func (r *auditRepo) RecordOperation(ctx context.Context, v *biz.OperationRecord) error {
return r.data.gormDB.WithContext(ctx).Create(&operationPO{IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}).Error
}
func opFromPO(v operationPO) *biz.OperationRecord {
return &biz.OperationRecord{ID: v.ID, CreatedAt: v.CreatedAt, IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}
}
func (r *auditRepo) ListOperations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]*biz.OperationRecord, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&operationPO{})
if q != nil {
if q.Path != "" {
db = db.Where("path LIKE ?", "%"+q.Path+"%")
}
if q.Method != "" {
db = db.Where("method = ?", q.Method)
}
if q.Status != 0 {
db = db.Where("status = ?", q.Status)
}
if q.IP != "" {
db = db.Where("ip LIKE ?", "%"+q.IP+"%")
}
if q.UserID != 0 {
db = db.Where("user_id = ?", q.UserID)
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []operationPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.OperationRecord, 0, len(pos))
for _, po := range pos {
out = append(out, opFromPO(po))
}
return out, total, nil
}
func (r *auditRepo) FindOperation(ctx context.Context, id uint) (*biz.OperationRecord, error) {
var po operationPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return opFromPO(po), nil
}
func (r *auditRepo) DeleteOperations(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&operationPO{}, ids).Error
}
func (r *auditRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) error {
return r.data.gormDB.WithContext(ctx).Create(&loginLogPO{Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}).Error
}
func loginFromPO(v loginLogPO) *biz.LoginLog {
return &biz.LoginLog{ID: v.ID, CreatedAt: v.CreatedAt, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}
}
func (r *auditRepo) ListLogins(ctx context.Context, page, size int, q *biz.LoginLog) ([]*biz.LoginLog, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&loginLogPO{})
if q != nil {
if q.Username != "" {
db = db.Where("username LIKE ?", "%"+q.Username+"%")
}
if q.IP != "" {
db = db.Where("ip LIKE ?", "%"+q.IP+"%")
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []loginLogPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.LoginLog, 0, len(pos))
for _, po := range pos {
out = append(out, loginFromPO(po))
}
return out, total, nil
}
func (r *auditRepo) FindLogin(ctx context.Context, id uint) (*biz.LoginLog, error) {
var po loginLogPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return loginFromPO(po), nil
}
func (r *auditRepo) DeleteLogins(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&loginLogPO{}, ids).Error
}
func (r *auditRepo) RecordDataAccess(ctx context.Context, v *biz.DataAccessLog) error {
return r.data.gormDB.WithContext(ctx).Create(&dataAccessLogPO{EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}).Error
}
func dataAccessFromPO(v dataAccessLogPO) *biz.DataAccessLog {
return &biz.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}
}
func (r *auditRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.DataAccessLog) ([]*biz.DataAccessLog, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&dataAccessLogPO{})
if q != nil {
if q.EventType != "" {
db = db.Where("event_type = ?", q.EventType)
}
if q.TargetTable != "" {
db = db.Where("target_table LIKE ?", "%"+q.TargetTable+"%")
}
if q.UserID != 0 {
db = db.Where("user_id = ?", q.UserID)
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []dataAccessLogPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.DataAccessLog, 0, len(pos))
for _, po := range pos {
out = append(out, dataAccessFromPO(po))
}
return out, total, nil
}
func (r *auditRepo) DeleteDataAccess(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&dataAccessLogPO{}, ids).Error
}
func logRoot() (string, error) {
root, err := filepath.Abs("logs")
if err != nil {
return "", err
}
info, err := os.Stat(root)
if errors.Is(err, fs.ErrNotExist) {
return root, nil
}
if err != nil {
return "", err
}
if !info.IsDir() {
return "", errors.New("日志路径不是目录")
}
return root, nil
}
func (r *auditRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
if parsed, err := time.Parse("2006-01", month); err != nil || parsed.Format("2006-01") != month {
return nil, errors.New("日志月份格式不正确")
}
root, err := logRoot()
if err != nil {
return nil, err
}
entries, err := os.ReadDir(root)
if errors.Is(err, fs.ErrNotExist) {
return []biz.LogDate{}, nil
}
if err != nil {
return nil, err
}
out := []biz.LogDate{}
for _, entry := range entries {
if ctx.Err() != nil {
return nil, ctx.Err()
}
if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() || !strings.HasPrefix(entry.Name(), month+"-") {
continue
}
if parsed, parseErr := time.Parse("2006-01-02", entry.Name()); parseErr != nil || parsed.Format("2006-01-02") != entry.Name() {
continue
}
count := 0
_ = filepath.WalkDir(filepath.Join(root, entry.Name()), func(_ string, item fs.DirEntry, walkErr error) error {
if walkErr == nil && !item.IsDir() && strings.EqualFold(filepath.Ext(item.Name()), ".log") {
count++
}
return nil
})
if count > 0 {
out = append(out, biz.LogDate{Date: entry.Name(), FileCount: count})
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Date < out[j].Date })
return out, nil
}
func (r *auditRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) {
if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date {
return nil, errors.New("日志日期格式不正确")
}
root, err := logRoot()
if err != nil {
return nil, err
}
dateRoot := filepath.Join(root, date)
out := []biz.LogFile{}
err = filepath.WalkDir(dateRoot, func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
if errors.Is(walkErr, fs.ErrNotExist) {
return nil
}
return walkErr
}
if ctx.Err() != nil {
return ctx.Err()
}
if entry.Type()&os.ModeSymlink != 0 {
if entry.IsDir() {
return filepath.SkipDir
}
return nil
}
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), ".log") {
return nil
}
info, infoErr := entry.Info()
if infoErr != nil {
return infoErr
}
relative, _ := filepath.Rel(dateRoot, path)
out = append(out, biz.LogFile{Path: filepath.ToSlash(relative), Name: entry.Name(), Size: info.Size(), ModifiedAt: info.ModTime()})
return nil
})
sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
return out, err
}
func (r *auditRepo) LogContent(ctx context.Context, date, path string, cursor *int64) (*biz.LogContent, error) {
if _, err := time.Parse("2006-01-02", date); err != nil {
return nil, errors.New("日志日期格式不正确")
}
root, err := logRoot()
if err != nil {
return nil, err
}
dateRoot := filepath.Join(root, date)
target := filepath.Clean(filepath.Join(dateRoot, filepath.FromSlash(path)))
if target == dateRoot || !strings.HasPrefix(target, dateRoot+string(os.PathSeparator)) || !strings.EqualFold(filepath.Ext(target), ".log") {
return nil, errors.New("日志文件路径不合法")
}
info, err := os.Lstat(target)
if err != nil {
return nil, err
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return nil, errors.New("日志文件路径不合法")
}
end := info.Size()
if cursor != nil && *cursor >= 0 && *cursor < end {
end = *cursor
}
start := end - int64(2*1024*1024)
limited := start > 0
if start < 0 {
start = 0
}
file, err := os.Open(target)
if err != nil {
return nil, err
}
defer file.Close()
data := make([]byte, end-start)
n, err := file.ReadAt(data, start)
if err != nil && n == 0 {
return nil, err
}
data = data[:n]
lines := bytes.Split(data, []byte("\n"))
if len(lines) > 501 {
drop := len(lines) - 501
offset := 0
for _, line := range lines[:drop] {
offset += len(line) + 1
}
start += int64(offset)
data = data[offset:]
limited = true
}
return &biz.LogContent{Date: date, Path: path, Content: string(data), LineCount: bytes.Count(data, []byte("\n")), NextCursor: start, HasMore: start > 0, LimitedByBytes: limited, Size: info.Size(), ModifiedAt: info.ModTime()}, nil
}
func errorFromPO(v errorRecordPO) *biz.ErrorRecord {
return &biz.ErrorRecord{ID: v.ID, CreatedAt: v.CreatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
}
func (r *auditRepo) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
if v.Status == "" {
v.Status = "未处理"
}
return r.data.gormDB.WithContext(ctx).Create(&errorRecordPO{Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}).Error
}
func (r *auditRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
return r.data.gormDB.WithContext(ctx).Model(&errorRecordPO{}).Where("id = ?", v.ID).Updates(map[string]any{"form": v.Form, "info": v.Info, "level": v.Level, "solution": v.Solution, "status": v.Status}).Error
}
func (r *auditRepo) DeleteErrors(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&errorRecordPO{}, ids).Error
}
func (r *auditRepo) FindError(ctx context.Context, id uint) (*biz.ErrorRecord, error) {
var po errorRecordPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return errorFromPO(po), nil
}
func (r *auditRepo) ListErrors(ctx context.Context, page, size int, q *biz.ErrorRecord) ([]*biz.ErrorRecord, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&errorRecordPO{})
if q != nil {
if q.Form != "" {
db = db.Where("form LIKE ?", "%"+q.Form+"%")
}
if q.Level != "" {
db = db.Where("level = ?", q.Level)
}
if q.Status != "" {
db = db.Where("status = ?", q.Status)
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []errorRecordPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.ErrorRecord, 0, len(pos))
for _, po := range pos {
out = append(out, errorFromPO(po))
}
return out, total, nil
}
+107
View File
@@ -0,0 +1,107 @@
package data
import (
"context"
"strconv"
"sync"
"time"
"kra/internal/biz"
"github.com/redis/go-redis/v9"
)
type memoryCacheEntry struct {
value string
expiresAt time.Time
}
type cacheStore struct {
client *redis.Client
mu sync.Mutex
memory map[string]memoryCacheEntry
}
func NewCache(data *Data) biz.Cache {
return &cacheStore{client: data.redis, memory: make(map[string]memoryCacheEntry)}
}
func (s *cacheStore) Get(ctx context.Context, key string) (string, bool, error) {
if s.client != nil {
value, err := s.client.Get(ctx, key).Result()
if err == nil {
return value, true, nil
}
if err != redis.Nil {
return "", false, err
}
return "", false, nil
}
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.memory[key]
if !ok {
return "", false, nil
}
if !entry.expiresAt.IsZero() && time.Now().After(entry.expiresAt) {
delete(s.memory, key)
return "", false, nil
}
return entry.value, true, nil
}
func (s *cacheStore) Set(ctx context.Context, key, value string, expiration time.Duration) error {
if s.client != nil {
return s.client.Set(ctx, key, value, expiration).Err()
}
s.mu.Lock()
defer s.mu.Unlock()
entry := memoryCacheEntry{value: value}
if expiration > 0 {
entry.expiresAt = time.Now().Add(expiration)
}
s.memory[key] = entry
return nil
}
func (s *cacheStore) Delete(ctx context.Context, key string) error {
if s.client != nil {
return s.client.Del(ctx, key).Err()
}
s.mu.Lock()
delete(s.memory, key)
s.mu.Unlock()
return nil
}
func (s *cacheStore) Increment(ctx context.Context, key string, expiration time.Duration) (int64, error) {
if s.client != nil {
value, err := s.client.Incr(ctx, key).Result()
if err != nil {
return 0, err
}
if value == 1 && expiration > 0 {
if err := s.client.Expire(ctx, key, expiration).Err(); err != nil {
return 0, err
}
}
return value, nil
}
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.memory[key]
if ok && !entry.expiresAt.IsZero() && time.Now().After(entry.expiresAt) {
ok = false
}
value := int64(0)
if ok {
value, _ = strconv.ParseInt(entry.value, 10, 64)
}
value++
entry.value = strconv.FormatInt(value, 10)
if !ok && expiration > 0 {
entry.expiresAt = time.Now().Add(expiration)
}
s.memory[key] = entry
return value, nil
}
+49 -7
View File
@@ -2,29 +2,66 @@ package data
import (
"context"
"fmt"
"log"
"os"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/google/wire"
"github.com/redis/go-redis/v9"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"kra/internal/conf"
"kra/internal/data/ent"
"kra/internal/data/ent/migrate"
_ "github.com/go-sql-driver/mysql"
"github.com/google/wire"
)
// ProviderSet is data providers.
var ProviderSet = wire.NewSet(NewData, NewAdminRepo)
var ProviderSet = wire.NewSet(NewData, NewAdminRepo, NewSystemRepo, NewAccessRepo, NewSettingsRepo, NewVersionRepo, NewExportRepo, NewAuditRepo, NewTaskRepo, NewMediaRepo, NewCache, NewFileStorage)
// Data is a struct that contains the database client.
type Data struct {
db *ent.Client
db *ent.Client
gormDB *gorm.DB
redis *redis.Client
}
// NewData creates a new Data instance.
func NewData(c *conf.Data) (*Data, func(), error) {
if c == nil || c.Database == nil {
return nil, nil, fmt.Errorf("database configuration is required")
}
db, err := ent.Open(c.Database.Driver, c.Database.Source)
if err != nil {
log.Fatalf("failed opening connection to database: %v", err)
return nil, nil, fmt.Errorf("open ent database: %w", err)
}
gormDB, err := gorm.Open(mysql.Open(c.Database.Source), &gorm.Config{})
if err != nil {
_ = db.Close()
return nil, nil, err
}
var redisClient *redis.Client
if c.Redis != nil && c.Redis.Addr != "" {
options := &redis.Options{Addr: c.Redis.Addr}
if c.Redis.Network != "" {
options.Network = c.Redis.Network
}
if c.Redis.ReadTimeout != nil {
options.ReadTimeout = c.Redis.ReadTimeout.AsDuration()
}
if c.Redis.WriteTimeout != nil {
options.WriteTimeout = c.Redis.WriteTimeout.AsDuration()
}
candidate := redis.NewClient(options)
pingCtx, cancel := context.WithTimeout(context.Background(), 800*time.Millisecond)
if pingErr := candidate.Ping(pingCtx).Err(); pingErr != nil {
log.Printf("redis unavailable, using in-memory cache: %v", pingErr)
_ = candidate.Close()
} else {
redisClient = candidate
}
cancel()
}
if os.Getenv("DEPLOY_ENV") == "dev" {
// Enable debug mode for detailed logging.
@@ -35,9 +72,14 @@ func NewData(c *conf.Data) (*Data, func(), error) {
}
}
cleanup := func() {
db.Close()
_ = db.Close()
if redisClient != nil {
_ = redisClient.Close()
}
}
return &Data{
db: db,
db: db,
gormDB: gormDB,
redis: redisClient,
}, cleanup, nil
}
+308
View File
@@ -0,0 +1,308 @@
package data
import (
"context"
"errors"
"fmt"
"gorm.io/gorm"
"kra/internal/biz"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
type exportTemplatePO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
DBName string
Name string
DBTableName string `gorm:"column:table_name"`
TemplateID string `gorm:"uniqueIndex"`
TemplateInfo string `gorm:"type:text"`
SQL string `gorm:"type:text"`
ImportSQL string `gorm:"type:text"`
Limit *int
Order string
}
func (exportTemplatePO) TableName() string { return "sys_export_templates" }
type exportConditionPO struct {
ID uint `gorm:"primaryKey"`
TemplateID string `gorm:"index"`
From string
Column string
Operator string
}
func (exportConditionPO) TableName() string { return "sys_export_template_condition" }
type exportJoinPO struct {
ID uint `gorm:"primaryKey"`
TemplateID string `gorm:"index"`
Join string `gorm:"column:joins"`
Table string
On string `gorm:"column:on"`
}
func (exportJoinPO) TableName() string { return "sys_export_template_join" }
type exportRepo struct{ data *Data }
func NewExportRepo(data *Data) biz.ExportRepo { return &exportRepo{data: data} }
func exportFromPO(po exportTemplatePO, conditions []exportConditionPO, joins []exportJoinPO) *biz.ExportTemplate {
v := &biz.ExportTemplate{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DBName: po.DBName, Name: po.Name, TableName: po.DBTableName, TemplateID: po.TemplateID, TemplateInfo: po.TemplateInfo, SQL: po.SQL, ImportSQL: po.ImportSQL, Limit: po.Limit, Order: po.Order}
for _, x := range conditions {
v.Conditions = append(v.Conditions, biz.ExportCondition{From: x.From, Column: x.Column, Operator: x.Operator})
}
for _, x := range joins {
v.Joins = append(v.Joins, biz.ExportJoin{Join: x.Join, Table: x.Table, On: x.On})
}
return v
}
func exportToPO(v *biz.ExportTemplate) exportTemplatePO {
return exportTemplatePO{ID: v.ID, DBName: v.DBName, Name: v.Name, DBTableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order}
}
func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate) error {
if err := tx.Where("template_id = ?", v.TemplateID).Delete(&exportConditionPO{}).Error; err != nil {
return err
}
if err := tx.Where("template_id = ?", v.TemplateID).Delete(&exportJoinPO{}).Error; err != nil {
return err
}
conditions := make([]exportConditionPO, 0, len(v.Conditions))
for _, x := range v.Conditions {
conditions = append(conditions, exportConditionPO{TemplateID: v.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
}
if len(conditions) > 0 {
if err := tx.Create(&conditions).Error; err != nil {
return err
}
}
joins := make([]exportJoinPO, 0, len(v.Joins))
for _, x := range v.Joins {
joins = append(joins, exportJoinPO{TemplateID: v.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
}
if len(joins) > 0 {
return tx.Create(&joins).Error
}
return nil
}
func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po := exportToPO(v)
if err := tx.Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
return r.saveRelations(tx, v)
})
}
func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po := exportToPO(v)
if err := tx.Model(&exportTemplatePO{}).Where("id = ?", v.ID).Updates(&po).Error; err != nil {
return err
}
return r.saveRelations(tx, v)
})
}
func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var tids []string
if err := tx.Model(&exportTemplatePO{}).Where("id IN ?", ids).Pluck("template_id", &tids).Error; err != nil {
return err
}
if len(tids) > 0 {
if err := tx.Where("template_id IN ?", tids).Delete(&exportConditionPO{}).Error; err != nil {
return err
}
if err := tx.Where("template_id IN ?", tids).Delete(&exportJoinPO{}).Error; err != nil {
return err
}
}
return tx.Delete(&exportTemplatePO{}, ids).Error
})
}
func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string) (*biz.ExportTemplate, error) {
var po exportTemplatePO
db := r.data.gormDB.WithContext(ctx)
var err error
if id != 0 {
err = db.First(&po, id).Error
} else {
err = db.Where("template_id = ?", tid).First(&po).Error
}
if err != nil {
return nil, err
}
var conditions []exportConditionPO
var joins []exportJoinPO
if err = db.Where("template_id = ?", po.TemplateID).Find(&conditions).Error; err != nil {
return nil, err
}
if err = db.Where("template_id = ?", po.TemplateID).Find(&joins).Error; err != nil {
return nil, err
}
return exportFromPO(po, conditions, joins), nil
}
func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *biz.ExportTemplate) ([]*biz.ExportTemplate, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&exportTemplatePO{})
if q != nil {
if q.Name != "" {
db = db.Where("name LIKE ?", "%"+q.Name+"%")
}
if q.TableName != "" {
db = db.Where("table_name = ?", q.TableName)
}
if q.TemplateID != "" {
db = db.Where("template_id = ?", q.TemplateID)
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []exportTemplatePO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.ExportTemplate, 0, len(pos))
for _, po := range pos {
v, err := r.FindExportTemplate(ctx, po.ID, "")
if err != nil {
return nil, 0, err
}
out = append(out, v)
}
return out, total, nil
}
var safeIdentifier = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_.]*$`)
var allowedOperator = map[string]bool{"=": true, "!=": true, ">": true, ">=": true, "<": true, "<=": true, "LIKE": true, "IN": true, "NOT IN": true, "BETWEEN": true}
func validateSelectSQL(sql string) error {
normalized := strings.ToLower(strings.TrimSpace(sql))
if !strings.HasPrefix(normalized, "select ") || strings.Contains(normalized, ";") {
return errors.New("仅允许单条 SELECT 查询")
}
return nil
}
func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) ([]map[string]any, string, error) {
db := r.data.gormDB.WithContext(ctx)
var rows []map[string]any
if t.SQL != "" {
if err := validateSelectSQL(t.SQL); err != nil {
return nil, "", err
}
args := map[string]any{}
for k, v := range params {
args[k] = v
}
err := db.Raw(t.SQL, args).Scan(&rows).Error
return rows, t.SQL, err
}
if !safeIdentifier.MatchString(t.TableName) {
return nil, "", errors.New("表名不合法")
}
columns := make([]string, 0)
for key := range parseTemplateColumns(t.TemplateInfo) {
if !safeIdentifier.MatchString(key) {
return nil, "", errors.New("列名不合法")
}
columns = append(columns, key)
}
sort.Strings(columns)
if len(columns) == 0 {
return nil, "", errors.New("模板列为空")
}
query := db.Table(t.TableName).Select(strings.Join(columns, ","))
for _, j := range t.Joins {
joinType := strings.ToUpper(strings.TrimSpace(j.Join))
if joinType != "LEFT JOIN" && joinType != "RIGHT JOIN" && joinType != "INNER JOIN" && joinType != "JOIN" {
return nil, "", errors.New("关联类型不合法")
}
if !safeIdentifier.MatchString(j.Table) {
return nil, "", errors.New("关联表不合法")
}
query = query.Joins(joinType + " " + j.Table + " ON " + j.On)
}
for _, condition := range t.Conditions {
operator := strings.ToUpper(strings.TrimSpace(condition.Operator))
if !allowedOperator[operator] || !safeIdentifier.MatchString(condition.Column) {
return nil, "", errors.New("查询条件不合法")
}
value := params[condition.From]
if value == "" {
continue
}
switch operator {
case "LIKE":
query = query.Where(condition.Column+" LIKE ?", "%"+value+"%")
case "IN", "NOT IN":
query = query.Where(condition.Column+" "+operator+" ?", strings.Split(value, ","))
case "BETWEEN":
start, end := params["start"+condition.From], params["end"+condition.From]
if start != "" && end != "" {
query = query.Where(condition.Column+" BETWEEN ? AND ?", start, end)
}
default:
query = query.Where(condition.Column+" "+operator+" ?", value)
}
}
limit := 0
if raw := params["limit"]; raw != "" {
limit, _ = strconv.Atoi(raw)
} else if t.Limit != nil {
limit = *t.Limit
}
if limit > 0 {
query = query.Limit(limit)
}
if t.Order != "" {
parts := strings.Fields(t.Order)
if len(parts) < 1 || len(parts) > 2 || !safeIdentifier.MatchString(parts[0]) || (len(parts) == 2 && strings.ToUpper(parts[1]) != "ASC" && strings.ToUpper(parts[1]) != "DESC") {
return nil, "", errors.New("排序不合法")
}
query = query.Order(t.Order)
}
preview := query.ToSQL(func(tx *gorm.DB) *gorm.DB { return tx.Find(&[]map[string]any{}) })
err := query.Find(&rows).Error
return rows, preview, err
}
func parseTemplateColumns(raw string) map[string]string {
out := map[string]string{}
parts := strings.Split(strings.Trim(strings.TrimSpace(raw), "{}"), ",")
for _, part := range parts {
pair := strings.SplitN(part, ":", 2)
if len(pair) == 2 {
out[strings.Trim(strings.TrimSpace(pair[0]), `"`)] = strings.Trim(strings.TrimSpace(pair[1]), `"`)
}
}
return out
}
func (r *exportRepo) ImportExportRows(ctx context.Context, t *biz.ExportTemplate, rows []map[string]any) error {
sql := strings.TrimSpace(t.ImportSQL)
lower := strings.ToLower(sql)
if sql == "" || strings.Contains(sql, ";") || (!strings.HasPrefix(lower, "insert ") && !strings.HasPrefix(lower, "update ")) {
return errors.New("导入 SQL 仅允许单条 INSERT 或 UPDATE")
}
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for index, row := range rows {
if err := tx.Exec(sql, row).Error; err != nil {
return fmt.Errorf("第%d行导入失败: %w", index+2, err)
}
}
return nil
})
}
+188
View File
@@ -0,0 +1,188 @@
package data
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"kra/internal/biz"
"kra/internal/conf"
)
type fileStorage struct {
root string
urlPrefix string
}
func NewFileStorage(config *conf.AdminBackend) (biz.FileStorage, error) {
root, prefix := "uploads/file", "uploads/file"
if config != nil && config.Local != nil {
if config.Local.StorePath != "" {
root = config.Local.StorePath
}
if config.Local.PathPrefix != "" {
prefix = config.Local.PathPrefix
}
}
root = filepath.Clean(root)
if err := os.MkdirAll(root, 0o755); err != nil {
return nil, fmt.Errorf("create upload directory: %w", err)
}
return &fileStorage{root: root, urlPrefix: "/" + strings.Trim(prefix, "/")}, nil
}
func (s *fileStorage) resolve(name string) (string, error) {
clean := filepath.Clean(strings.TrimPrefix(name, "/"))
if clean == "." || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("invalid storage path")
}
return filepath.Join(s.root, clean), nil
}
func (s *fileStorage) Put(ctx context.Context, name string, reader io.Reader) (*biz.StoredFile, error) {
path, err := s.resolve(name)
if err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, err
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return nil, err
}
size, copyErr := io.Copy(file, reader)
closeErr := file.Close()
if copyErr != nil {
return nil, copyErr
}
if closeErr != nil {
return nil, closeErr
}
return &biz.StoredFile{Name: filepath.Base(name), Path: name, URL: s.urlPrefix + "/" + strings.TrimPrefix(filepath.ToSlash(name), "/"), Size: size}, nil
}
func (s *fileStorage) Open(ctx context.Context, name string) (io.ReadCloser, error) {
path, err := s.resolve(name)
if err != nil {
return nil, err
}
return os.Open(path)
}
func (s *fileStorage) Delete(ctx context.Context, name string) error {
path, err := s.resolve(name)
if err != nil {
return err
}
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
func (s *fileStorage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) {
reader, writer := io.Pipe()
hash := md5.New()
errCh := make(chan error, 1)
go func() {
defer writer.Close()
for _, name := range names {
if ctx.Err() != nil {
errCh <- ctx.Err()
return
}
file, err := s.Open(ctx, name)
if err != nil {
errCh <- err
return
}
_, err = io.Copy(io.MultiWriter(writer, hash), file)
_ = file.Close()
if err != nil {
errCh <- err
return
}
}
errCh <- nil
}()
stored, err := s.Put(ctx, destination, reader)
composeErr := <-errCh
if err != nil {
return nil, "", err
}
if composeErr != nil {
_ = s.Delete(ctx, destination)
return nil, "", composeErr
}
return stored, hex.EncodeToString(hash.Sum(nil)), nil
}
func (s *fileStorage) DeletePrefix(ctx context.Context, prefix string) error {
path, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder")
if err != nil {
return err
}
dir := filepath.Dir(path)
if !strings.HasPrefix(dir, s.root+string(filepath.Separator)) {
return fmt.Errorf("invalid storage path")
}
if err = os.RemoveAll(dir); err != nil {
return err
}
return nil
}
func (s *fileStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) {
if limit < 1 || limit > 1000 {
limit = 100
}
root, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder")
if err != nil {
return nil, "", false, err
}
walkRoot := filepath.Dir(root)
items := []*biz.StoredFile{}
err = filepath.WalkDir(walkRoot, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
if os.IsNotExist(walkErr) {
return nil
}
return walkErr
}
if ctx.Err() != nil {
return ctx.Err()
}
if entry.IsDir() || entry.Type()&os.ModeSymlink != 0 {
return nil
}
relative, _ := filepath.Rel(s.root, path)
relative = filepath.ToSlash(relative)
if relative <= cursor {
return nil
}
info, infoErr := entry.Info()
if infoErr != nil {
return infoErr
}
items = append(items, &biz.StoredFile{Name: entry.Name(), Path: relative, URL: s.urlPrefix + "/" + relative, Size: info.Size()})
return nil
})
if err != nil {
return nil, "", false, err
}
sort.Slice(items, func(i, j int) bool { return items[i].Path < items[j].Path })
hasMore := len(items) > limit
if hasMore {
items = items[:limit]
}
next := ""
if len(items) > 0 {
next = items[len(items)-1].Path
}
return items, next, hasMore, nil
}
+260
View File
@@ -0,0 +1,260 @@
package data
import (
"context"
"errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"kra/internal/biz"
"time"
)
type mediaPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
CategoryID int `gorm:"column:class_id"`
URL string
Tag string
Key string `gorm:"index"`
Size int64
Mime string
MD5 string `gorm:"index;column:md5"`
UserID uint `gorm:"index"`
}
func (mediaPO) TableName() string { return "media_file_upload_and_downloads" }
type categoryPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
ParentID uint `gorm:"column:pid"`
}
func (categoryPO) TableName() string { return "media_attachment_category" }
type uploadSessionPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
UserID uint `gorm:"index"`
FileName string
FileHash string `gorm:"index"`
FileSize int64
ChunkSize int64
ChunkTotal int
Status string
StorageKey string
MediaID uint
}
func (uploadSessionPO) TableName() string { return "media_uploads" }
type uploadChunkPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
UploadID uint `gorm:"uniqueIndex:idx_upload_chunk"`
ChunkIndex int `gorm:"uniqueIndex:idx_upload_chunk"`
ChunkHash string
Size int64
}
func (uploadChunkPO) TableName() string { return "media_upload_chunks" }
type mediaRepo struct{ data *Data }
func NewMediaRepo(data *Data) biz.MediaRepo { return &mediaRepo{data: data} }
func mediaFromPO(v mediaPO) *biz.MediaFile {
return &biz.MediaFile{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, CategoryID: v.CategoryID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: v.UserID}
}
func mediaToPO(v *biz.MediaFile) mediaPO {
return mediaPO{ID: v.ID, Name: v.Name, CategoryID: v.CategoryID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: v.UserID}
}
func (r *mediaRepo) CreateMedia(ctx context.Context, v *biz.MediaFile) error {
po := mediaToPO(v)
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
v.CreatedAt = po.CreatedAt
return nil
}
func (r *mediaRepo) FindMedia(ctx context.Context, id uint) (*biz.MediaFile, error) {
var po mediaPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return mediaFromPO(po), nil
}
func (r *mediaRepo) FindMediaByHash(ctx context.Context, userID uint, hash string) (*biz.MediaFile, error) {
var po mediaPO
if err := r.data.gormDB.WithContext(ctx).Where("user_id = ? AND md5 = ?", userID, hash).First(&po).Error; err != nil {
return nil, err
}
return mediaFromPO(po), nil
}
func (r *mediaRepo) ListMedia(ctx context.Context, page, size int, keyword string, categoryID int, tag string, userID uint) ([]*biz.MediaFile, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&mediaPO{})
if keyword != "" {
db = db.Where("name LIKE ?", "%"+keyword+"%")
}
if categoryID > 0 {
db = db.Where("class_id = ?", categoryID)
}
if tag != "" {
db = db.Where("tag = ?", tag)
}
if userID > 0 {
db = db.Where("user_id = ?", userID)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []mediaPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.MediaFile, 0, len(pos))
for _, po := range pos {
out = append(out, mediaFromPO(po))
}
return out, total, nil
}
func (r *mediaRepo) UpdateMediaName(ctx context.Context, id uint, name string) error {
return r.data.gormDB.WithContext(ctx).Model(&mediaPO{}).Where("id = ?", id).Update("name", name).Error
}
func (r *mediaRepo) DeleteMedia(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Unscoped().Delete(&mediaPO{}, id).Error
}
func (r *mediaRepo) MediaKeyReferences(ctx context.Context, key string) (int64, error) {
var count int64
err := r.data.gormDB.WithContext(ctx).Model(&mediaPO{}).Where("`key` = ?", key).Count(&count).Error
return count, err
}
func (r *mediaRepo) CreateMediaBatch(ctx context.Context, items []*biz.MediaFile) error {
pos := make([]mediaPO, 0, len(items))
for _, v := range items {
pos = append(pos, mediaToPO(v))
}
if len(pos) == 0 {
return nil
}
return r.data.gormDB.WithContext(ctx).Create(&pos).Error
}
func (r *mediaRepo) SaveCategory(ctx context.Context, v *biz.AttachmentCategory) error {
db := r.data.gormDB.WithContext(ctx)
var count int64
query := db.Model(&categoryPO{}).Where("name = ? AND pid = ?", v.Name, v.ParentID)
if v.ID != 0 {
query = query.Where("id <> ?", v.ID)
}
if err := query.Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("分类名称已存在")
}
if v.ID == 0 {
po := categoryPO{Name: v.Name, ParentID: v.ParentID}
if err := db.Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
return nil
}
return db.Model(&categoryPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "pid": v.ParentID}).Error
}
func (r *mediaRepo) DeleteCategory(ctx context.Context, id uint) error {
var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&categoryPO{}).Where("pid = ?", id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("请先删除子级")
}
return r.data.gormDB.WithContext(ctx).Unscoped().Delete(&categoryPO{}, id).Error
}
func (r *mediaRepo) ListCategories(ctx context.Context) ([]*biz.AttachmentCategory, error) {
var pos []categoryPO
if err := r.data.gormDB.WithContext(ctx).Order("id").Find(&pos).Error; err != nil {
return nil, err
}
byID := map[uint]*biz.AttachmentCategory{}
for _, po := range pos {
byID[po.ID] = &biz.AttachmentCategory{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID, Children: []*biz.AttachmentCategory{}}
}
roots := []*biz.AttachmentCategory{}
for _, po := range pos {
v := byID[po.ID]
if parent := byID[po.ParentID]; parent != nil {
parent.Children = append(parent.Children, v)
} else {
roots = append(roots, v)
}
}
return roots, nil
}
func uploadFromPO(v uploadSessionPO) *biz.UploadSession {
return &biz.UploadSession{ID: v.ID, UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status, StorageKey: v.StorageKey, MediaID: v.MediaID}
}
func (r *mediaRepo) FindUploadingSession(ctx context.Context, userID uint, hash string) (*biz.UploadSession, error) {
var po uploadSessionPO
if err := r.data.gormDB.WithContext(ctx).Where("user_id = ? AND file_hash = ? AND status = ?", userID, hash, "uploading").First(&po).Error; err != nil {
return nil, err
}
return uploadFromPO(po), nil
}
func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *biz.UploadSession) error {
po := uploadSessionPO{UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
return nil
}
func (r *mediaRepo) FindUploadSession(ctx context.Context, id uint) (*biz.UploadSession, error) {
var po uploadSessionPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return uploadFromPO(po), nil
}
func (r *mediaRepo) CompleteUploadSession(ctx context.Context, id uint, key string, mediaID uint) error {
return r.data.gormDB.WithContext(ctx).Model(&uploadSessionPO{}).Where("id = ?", id).Updates(map[string]any{"status": "completed", "storage_key": key, "media_id": mediaID}).Error
}
func (r *mediaRepo) DeleteUploadSession(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Unscoped().Delete(&uploadSessionPO{}, id).Error
}
func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *biz.UploadChunk) error {
po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size}
return r.data.gormDB.WithContext(ctx).Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "upload_id"}, {Name: "chunk_index"}}, DoUpdates: clause.AssignmentColumns([]string{"chunk_hash", "size", "updated_at"})}).Create(&po).Error
}
func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*biz.UploadChunk, error) {
var pos []uploadChunkPO
if err := r.data.gormDB.WithContext(ctx).Where("upload_id = ?", uploadID).Order("chunk_index").Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.UploadChunk, 0, len(pos))
for _, po := range pos {
out = append(out, &biz.UploadChunk{Index: po.ChunkIndex, Hash: po.ChunkHash, Size: po.Size})
}
return out, nil
}
func (r *mediaRepo) DeleteChunks(ctx context.Context, uploadID uint) error {
return r.data.gormDB.WithContext(ctx).Unscoped().Where("upload_id = ?", uploadID).Delete(&uploadChunkPO{}).Error
}
+485
View File
@@ -0,0 +1,485 @@
package data
import (
"context"
"errors"
"fmt"
"strings"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
type dictionaryPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
Type string `gorm:"uniqueIndex"`
Status bool
Desc string
ParentID *uint
}
func (dictionaryPO) TableName() string { return "sys_dictionaries" }
type dictionaryDetailPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Label string
Value string
Extend string
Status bool
Sort int
DictionaryID uint `gorm:"column:sys_dictionary_id;index"`
ParentID *uint
Level int
Path string
}
func (dictionaryDetailPO) TableName() string { return "sys_dictionary_details" }
type parameterPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
Key string `gorm:"uniqueIndex"`
Value string
Desc string
}
func (parameterPO) TableName() string { return "sys_params" }
type apiTokenPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
UserID uint
AuthorityID uint
Token string `gorm:"type:text;uniqueIndex"`
Status bool
ExpiresAt time.Time
Remark string
}
func (apiTokenPO) TableName() string { return "sys_api_tokens" }
type securityConfigPO struct {
ID uint `gorm:"primaryKey"`
CaptchaOpen int
CaptchaTimeout int
KeyLong int
ImgWidth int
ImgHeight int
PwdMinLength int
PwdRequireUpper bool
PwdRequireLower bool
PwdRequireDigit bool
PwdRequireSpecial bool
LimitEnable bool
LimitWindow int
LimitCount int
LockEnable bool
LockThreshold int
LockDuration int
PwdExpireEnable bool
PwdExpireDays int
ForceNewUserChangePassword bool
}
func (securityConfigPO) TableName() string { return "sys_security_config" }
type settingsRepo struct{ data *Data }
func NewSettingsRepo(data *Data) biz.SettingsRepo { return &settingsRepo{data: data} }
func dictionaryFromPO(po dictionaryPO) *biz.Dictionary {
return &biz.Dictionary{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Type: po.Type, Status: po.Status, Desc: po.Desc, ParentID: po.ParentID}
}
func detailFromPO(po dictionaryDetailPO) *biz.DictionaryDetail {
return &biz.DictionaryDetail{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Label: po.Label, Value: po.Value, Extend: po.Extend, Status: po.Status, Sort: po.Sort, DictionaryID: po.DictionaryID, ParentID: po.ParentID, Level: po.Level, Path: po.Path}
}
func parameterFromPO(po parameterPO) *biz.SystemParameter {
return &biz.SystemParameter{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Key: po.Key, Value: po.Value, Desc: po.Desc}
}
func (r *settingsRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) error {
po := dictionaryPO{Name: v.Name, Type: v.Type, Status: v.Status, Desc: v.Desc, ParentID: v.ParentID}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
return nil
}
func (r *settingsRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error {
return r.data.gormDB.WithContext(ctx).Model(&dictionaryPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "type": v.Type, "status": v.Status, "desc": v.Desc, "parent_id": v.ParentID}).Error
}
func (r *settingsRepo) DeleteDictionary(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64
if err := tx.Model(&dictionaryPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("存在子字典不可删除")
}
if err := tx.Where("sys_dictionary_id = ?", id).Delete(&dictionaryDetailPO{}).Error; err != nil {
return err
}
return tx.Delete(&dictionaryPO{}, id).Error
})
}
func (r *settingsRepo) FindDictionary(ctx context.Context, id uint, typ string, details bool) (*biz.Dictionary, error) {
var po dictionaryPO
db := r.data.gormDB.WithContext(ctx)
var err error
if id != 0 {
err = db.First(&po, id).Error
} else {
err = db.Where("type = ?", typ).First(&po).Error
}
if err != nil {
return nil, err
}
out := dictionaryFromPO(po)
if details {
out.Details, _, err = r.ListDictionaryDetails(ctx, 1, 10000, out.ID, "", "")
}
return out, err
}
func (r *settingsRepo) ListDictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]*biz.Dictionary, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&dictionaryPO{})
if name != "" {
db = db.Where("name LIKE ?", "%"+name+"%")
}
if typ != "" {
db = db.Where("type LIKE ?", "%"+typ+"%")
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []dictionaryPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.Dictionary, 0, len(pos))
for _, po := range pos {
v := dictionaryFromPO(po)
if details {
v.Details, _, _ = r.ListDictionaryDetails(ctx, 1, 10000, v.ID, "", "")
}
out = append(out, v)
}
return out, total, nil
}
func (r *settingsRepo) CreateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
po := dictionaryDetailPO{Label: v.Label, Value: v.Value, Extend: v.Extend, Status: v.Status, Sort: v.Sort, DictionaryID: v.DictionaryID, ParentID: v.ParentID}
po.Level = 0
po.Path = ""
if v.ParentID != nil && *v.ParentID != 0 {
var parent dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil {
return err
}
po.Level = parent.Level + 1
po.Path = strings.Trim(strings.Join([]string{parent.Path, fmt.Sprint(parent.ID)}, ","), ",")
}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
v.Level = po.Level
v.Path = po.Path
return nil
}
func (r *settingsRepo) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
var po dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).First(&po, v.ID).Error; err != nil {
return err
}
level, path := 0, ""
if v.ParentID != nil && *v.ParentID != 0 {
if *v.ParentID == v.ID {
return errors.New("不能将自身设为父级")
}
var parent dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).First(&parent, *v.ParentID).Error; err != nil {
return err
}
if strings.Contains(","+parent.Path+",", fmt.Sprintf(",%d,", v.ID)) {
return errors.New("不能移动到自己的子级")
}
level = parent.Level + 1
path = strings.Trim(strings.Join([]string{parent.Path, fmt.Sprint(parent.ID)}, ","), ",")
}
oldPrefix := strings.Trim(strings.Join([]string{po.Path, fmt.Sprint(po.ID)}, ","), ",")
newPrefix := strings.Trim(strings.Join([]string{path, fmt.Sprint(po.ID)}, ","), ",")
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&dictionaryDetailPO{}).Where("id = ?", v.ID).Updates(map[string]any{"label": v.Label, "value": v.Value, "extend": v.Extend, "status": v.Status, "sort": v.Sort, "sys_dictionary_id": v.DictionaryID, "parent_id": v.ParentID, "level": level, "path": path}).Error; err != nil {
return err
}
return tx.Model(&dictionaryDetailPO{}).Where("path = ? OR path LIKE ?", oldPrefix, oldPrefix+",%").Update("path", gorm.Expr("REPLACE(path, ?, ?)", oldPrefix, newPrefix)).Error
})
}
func (r *settingsRepo) DeleteDictionaryDetail(ctx context.Context, id uint) error {
var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("存在子级不可删除")
}
return r.data.gormDB.WithContext(ctx).Delete(&dictionaryDetailPO{}, id).Error
}
func (r *settingsRepo) FindDictionaryDetail(ctx context.Context, id uint) (*biz.DictionaryDetail, error) {
var po dictionaryDetailPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return detailFromPO(po), nil
}
func (r *settingsRepo) ListDictionaryDetails(ctx context.Context, page, size int, dictionaryID uint, label, value string) ([]*biz.DictionaryDetail, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{})
if dictionaryID != 0 {
db = db.Where("sys_dictionary_id = ?", dictionaryID)
}
if label != "" {
db = db.Where("label LIKE ?", "%"+label+"%")
}
if value != "" {
db = db.Where("value LIKE ?", "%"+value+"%")
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []dictionaryDetailPO
if err := db.Order("sort,id").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.DictionaryDetail, 0, len(pos))
for _, po := range pos {
out = append(out, detailFromPO(po))
}
return out, total, nil
}
func (r *settingsRepo) DictionaryDetailTree(ctx context.Context, dictionaryID uint, typ string) ([]*biz.DictionaryDetail, error) {
if dictionaryID == 0 {
dictionary, err := r.FindDictionary(ctx, 0, typ, false)
if err != nil {
return nil, err
}
dictionaryID = dictionary.ID
}
items, _, err := r.ListDictionaryDetails(ctx, 1, 100000, dictionaryID, "", "")
if err != nil {
return nil, err
}
byID := map[uint]*biz.DictionaryDetail{}
for _, item := range items {
item.Children = []*biz.DictionaryDetail{}
byID[item.ID] = item
}
roots := []*biz.DictionaryDetail{}
for _, item := range items {
if item.ParentID != nil && byID[*item.ParentID] != nil {
byID[*item.ParentID].Children = append(byID[*item.ParentID].Children, item)
} else {
roots = append(roots, item)
}
}
return roots, nil
}
func (r *settingsRepo) CreateParameter(ctx context.Context, v *biz.SystemParameter) error {
po := parameterPO{Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
return nil
}
func (r *settingsRepo) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error {
return r.data.gormDB.WithContext(ctx).Model(&parameterPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "key": v.Key, "value": v.Value, "desc": v.Desc}).Error
}
func (r *settingsRepo) DeleteParameters(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&parameterPO{}, ids).Error
}
func (r *settingsRepo) FindParameter(ctx context.Context, id uint, key string) (*biz.SystemParameter, error) {
var po parameterPO
db := r.data.gormDB.WithContext(ctx)
var err error
if id != 0 {
err = db.First(&po, id).Error
} else {
err = db.Where("`key` = ?", key).First(&po).Error
}
if err != nil {
return nil, err
}
return parameterFromPO(po), nil
}
func (r *settingsRepo) ListParameters(ctx context.Context, page, size int, q *biz.SystemParameter) ([]*biz.SystemParameter, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&parameterPO{})
if q != nil {
if q.Name != "" {
db = db.Where("name LIKE ?", "%"+q.Name+"%")
}
if q.Key != "" {
db = db.Where("`key` LIKE ?", "%"+q.Key+"%")
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []parameterPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.SystemParameter, 0, len(pos))
for _, po := range pos {
out = append(out, parameterFromPO(po))
}
return out, total, nil
}
func (r *settingsRepo) UserHasAuthority(ctx context.Context, userID, authorityID uint) (*biz.User, bool, error) {
var po userPO
if err := r.data.gormDB.WithContext(ctx).First(&po, userID).Error; err != nil {
return nil, false, err
}
var count int64
err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", userID, authorityID).Count(&count).Error
if err != nil {
return nil, false, err
}
user, err := (&systemRepo{data: r.data}).toBizUser(ctx, &po)
return user, count > 0 || po.AuthorityID == authorityID, err
}
func (r *settingsRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) error {
po := apiTokenPO{UserID: v.UserID, AuthorityID: v.AuthorityID, Token: v.Token, Status: v.Status, ExpiresAt: v.ExpiresAt, Remark: v.Remark}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
v.CreatedAt = po.CreatedAt
return nil
}
func (r *settingsRepo) ListAPITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]*biz.APIToken, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&apiTokenPO{})
if userID != 0 {
db = db.Where("user_id = ?", userID)
}
if status != nil {
db = db.Where("status = ?", *status)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []apiTokenPO
if err := db.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.APIToken, 0, len(pos))
for _, po := range pos {
v := &biz.APIToken{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UserID: po.UserID, AuthorityID: po.AuthorityID, Token: po.Token, Status: po.Status, ExpiresAt: po.ExpiresAt, Remark: po.Remark}
var userPO userPO
if r.data.gormDB.WithContext(ctx).First(&userPO, po.UserID).Error == nil {
v.User, _ = (&systemRepo{data: r.data}).toBizUser(ctx, &userPO)
}
out = append(out, v)
}
return out, total, nil
}
func (r *settingsRepo) DisableAPIToken(ctx context.Context, id uint) (string, error) {
var po apiTokenPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return "", err
}
return po.Token, r.data.gormDB.WithContext(ctx).Model(&po).Update("status", false).Error
}
func (r *settingsRepo) IsTokenDisabled(ctx context.Context, token string) (bool, error) {
var po apiTokenPO
err := r.data.gormDB.WithContext(ctx).Where("token = ?", token).First(&po).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
if err != nil {
return false, err
}
return !po.Status || time.Now().After(po.ExpiresAt), nil
}
func defaultSecurityConfig() securityConfigPO {
return securityConfigPO{ID: 1, CaptchaTimeout: 3600, KeyLong: 6, ImgWidth: 240, ImgHeight: 80, PwdMinLength: 8, LimitWindow: 60, LimitCount: 30, LockThreshold: 5, LockDuration: 30, PwdExpireDays: 90}
}
func securityFromPO(v securityConfigPO) *biz.SecurityConfig {
return &biz.SecurityConfig{ID: v.ID, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
}
func securityToPO(v *biz.SecurityConfig) securityConfigPO {
return securityConfigPO{ID: 1, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
}
func (r *settingsRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig, error) {
var po securityConfigPO
err := r.data.gormDB.WithContext(ctx).First(&po, 1).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
po = defaultSecurityConfig()
err = r.data.gormDB.WithContext(ctx).Create(&po).Error
}
if err != nil {
return nil, err
}
return securityFromPO(po), nil
}
func (r *settingsRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityConfig) error {
previous, err := r.SecurityConfig(ctx)
if err != nil {
return err
}
po := securityToPO(v)
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Save(&po).Error; err != nil {
return err
}
if v.PwdExpireEnable && !previous.PwdExpireEnable {
return tx.Model(&userPO{}).Where("password_updated_at IS NULL").Update("password_updated_at", time.Now()).Error
}
return nil
})
}
+430
View File
@@ -0,0 +1,430 @@
package data
import (
"context"
"encoding/json"
"errors"
"time"
"kra/internal/biz"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type userPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
UUID string `gorm:"type:char(36);uniqueIndex"`
Username string `gorm:"index;uniqueIndex"`
Password string
NickName string `gorm:"column:nick_name"`
HeaderImg string `gorm:"column:header_img"`
AuthorityID uint `gorm:"column:authority_id"`
DeptID uint `gorm:"column:dept_id"`
Phone string
Email string
Enable int `gorm:"default:1"`
OriginSetting string `gorm:"type:text"`
PasswordUpdatedAt *time.Time
MustChangePassword bool
}
func (userPO) TableName() string { return "sys_users" }
type authorityPO struct {
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
AuthorityID uint `gorm:"primaryKey;column:authority_id"`
AuthorityName string
ParentID *uint
DataScope int `gorm:"default:1"`
DefaultRouter string `gorm:"default:dashboard"`
}
func (authorityPO) TableName() string { return "sys_authorities" }
type menuPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
MenuLevel uint
ParentID uint `gorm:"column:parent_id"`
Path string
Name string
Hidden bool
Component string
Sort int
ActiveName string
KeepAlive bool
DefaultMenu bool
Title string
Icon string
CloseTab bool
TransitionType string
}
func (menuPO) TableName() string { return "sys_base_menus" }
type userAuthorityPO struct {
SysUserID uint `gorm:"primaryKey;column:sys_user_id"`
SysAuthorityAuthorityID uint `gorm:"primaryKey;column:sys_authority_authority_id"`
}
func (userAuthorityPO) TableName() string { return "sys_user_authority" }
type authorityMenuPO struct {
SysAuthorityAuthorityID uint `gorm:"primaryKey;column:sys_authority_authority_id"`
SysBaseMenuID uint `gorm:"primaryKey;column:sys_base_menu_id"`
}
func (authorityMenuPO) TableName() string { return "sys_authority_menus" }
type systemRepo struct{ data *Data }
func NewSystemRepo(data *Data) biz.SystemRepo { return &systemRepo{data: data} }
func (r *systemRepo) IsInitialized(ctx context.Context) (bool, error) {
return r.data.gormDB.WithContext(ctx).Migrator().HasTable(&userPO{}), nil
}
func (r *systemRepo) Initialize(ctx context.Context) error {
db := r.data.gormDB.WithContext(ctx)
if err := db.AutoMigrate(&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &menuButtonPO{}, &authorityButtonPO{}, &departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{}, &dictionaryPO{}, &dictionaryDetailPO{}, &parameterPO{}, &apiTokenPO{}, &securityConfigPO{}, &versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{}, &operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{}, &taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{}); err != nil {
return err
}
return db.Transaction(func(tx *gorm.DB) error {
authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", DataScope: 1, DefaultRouter: "dashboard"}
if err := tx.FirstOrCreate(&authority, authorityPO{AuthorityID: 888}).Error; err != nil {
return err
}
menus := defaultMenus()
for i := range menus {
if err := tx.Where("name = ?", menus[i].Name).FirstOrCreate(&menus[i]).Error; err != nil {
return err
}
}
var persisted []menuPO
if err := tx.Order("sort asc, id asc").Find(&persisted).Error; err != nil {
return err
}
nameID := make(map[string]uint, len(persisted))
for _, menu := range persisted {
nameID[menu.Name] = menu.ID
}
for i := range menus {
if menus[i].ParentID == 0 || menus[i].ActiveName == "" {
continue
}
_ = tx.Model(&menuPO{}).Where("name = ?", menus[i].Name).Updates(map[string]any{"parent_id": nameID[menus[i].ActiveName], "active_name": ""}).Error
}
if err := tx.Where("sys_authority_authority_id = ?", 888).Delete(&authorityMenuPO{}).Error; err != nil {
return err
}
links := make([]authorityMenuPO, 0, len(persisted))
for _, menu := range persisted {
links = append(links, authorityMenuPO{SysAuthorityAuthorityID: 888, SysBaseMenuID: menu.ID})
}
if len(links) > 0 {
if err := tx.Create(&links).Error; err != nil {
return err
}
}
var count int64
if err := tx.Model(&userPO{}).Where("username = ?", "admin").Count(&count).Error; err != nil {
return err
}
if count == 0 {
hash, err := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
if err != nil {
return err
}
now := time.Now()
user := userPO{UUID: uuid.NewString(), Username: "admin", Password: string(hash), NickName: "超级管理员", AuthorityID: 888, Enable: 1, PasswordUpdatedAt: &now}
if err := tx.Create(&user).Error; err != nil {
return err
}
if err := tx.Create(&userAuthorityPO{SysUserID: user.ID, SysAuthorityAuthorityID: 888}).Error; err != nil {
return err
}
}
return nil
})
}
func (r *systemRepo) FindUserByUsername(ctx context.Context, username string) (*biz.User, error) {
var po userPO
if err := r.data.gormDB.WithContext(ctx).Where("username = ?", username).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrAdminNotFound
}
return nil, err
}
return r.toBizUser(ctx, &po)
}
func (r *systemRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, error) {
var po userPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrAdminNotFound
}
return nil, err
}
return r.toBizUser(ctx, &po)
}
func (r *systemRepo) toBizUser(ctx context.Context, po *userPO) (*biz.User, error) {
var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).First(&authority, "authority_id = ?", po.AuthorityID).Error; err != nil {
return nil, err
}
var authorityPOs []authorityPO
if err := r.data.gormDB.WithContext(ctx).Table("sys_authorities").Joins("JOIN sys_user_authority ON sys_user_authority.sys_authority_authority_id = sys_authorities.authority_id").Where("sys_user_authority.sys_user_id = ? AND sys_authorities.deleted_at IS NULL", po.ID).Find(&authorityPOs).Error; err != nil {
return nil, err
}
authorities := make([]biz.Authority, 0, len(authorityPOs))
for _, item := range authorityPOs {
authorities = append(authorities, toBizAuthority(item))
}
setting := map[string]any(nil)
if po.OriginSetting != "" {
_ = json.Unmarshal([]byte(po.OriginSetting), &setting)
}
var departmentPOs []departmentPO
if err := r.data.gormDB.WithContext(ctx).Table("sys_departments").Joins("JOIN sys_user_departments ON sys_user_departments.sys_department_id = sys_departments.id").Where("sys_user_departments.sys_user_id = ? AND sys_departments.deleted_at IS NULL", po.ID).Find(&departmentPOs).Error; err != nil {
return nil, err
}
departments := make([]biz.Department, 0, len(departmentPOs))
var primary *biz.Department
for _, item := range departmentPOs {
value := deptFromPO(item)
departments = append(departments, *value)
if item.ID == po.DeptID {
copy := *value
primary = &copy
}
}
var positionPOs []positionPO
if err := r.data.gormDB.WithContext(ctx).Table("sys_positions").Joins("JOIN sys_user_positions ON sys_user_positions.sys_position_id = sys_positions.id").Where("sys_user_positions.sys_user_id = ? AND sys_positions.deleted_at IS NULL", po.ID).Find(&positionPOs).Error; err != nil {
return nil, err
}
positions := make([]biz.Position, 0, len(positionPOs))
for _, item := range positionPOs {
positions = append(positions, *posFromPO(item))
}
return &biz.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, Authority: toBizAuthority(authority), Authorities: authorities, DeptID: po.DeptID, Department: primary, Departments: departments, Positions: positions, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}, nil
}
func toBizAuthority(po authorityPO) biz.Authority {
return biz.Authority{AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter}
}
func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*biz.Menu, error) {
var pos []menuPO
err := r.data.gormDB.WithContext(ctx).Table("sys_base_menus").
Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id").
Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.deleted_at IS NULL", authorityID).
Order("sys_base_menus.sort asc, sys_base_menus.id asc").Scan(&pos).Error
if err != nil {
return nil, err
}
byID := make(map[uint]*biz.Menu, len(pos))
for _, po := range pos {
byID[po.ID] = &biz.Menu{ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType, Children: []*biz.Menu{}}
}
roots := make([]*biz.Menu, 0)
for _, po := range pos {
menu := byID[po.ID]
if parent := byID[po.ParentID]; parent != nil {
parent.Children = append(parent.Children, menu)
} else {
roots = append(roots, menu)
}
}
return roots, nil
}
func (r *systemRepo) ListUsers(ctx context.Context, page, pageSize int, filter *biz.UserListFilter) ([]*biz.User, int64, error) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&userPO{})
if scope, ok := biz.DataScopeFromContext(ctx); ok && !scope.All {
if scope.OwnerUserID != 0 {
db = db.Where("sys_users.id = ?", scope.OwnerUserID)
} else if len(scope.DepartmentIDs) > 0 {
db = db.Joins("JOIN sys_user_departments ON sys_user_departments.sys_user_id = sys_users.id").Where("sys_user_departments.sys_department_id IN ?", scope.DepartmentIDs).Distinct("sys_users.id")
} else {
db = db.Where("1 = 0")
}
}
order := "id desc"
if filter != nil {
if filter.Username != "" {
db = db.Where("username LIKE ?", "%"+filter.Username+"%")
}
if filter.NickName != "" {
db = db.Where("nick_name LIKE ?", "%"+filter.NickName+"%")
}
if filter.Phone != "" {
db = db.Where("phone LIKE ?", "%"+filter.Phone+"%")
}
if filter.Email != "" {
db = db.Where("email LIKE ?", "%"+filter.Email+"%")
}
allowed := map[string]bool{"id": true, "username": true, "nick_name": true, "phone": true, "email": true, "created_at": true}
if allowed[filter.OrderKey] {
order = filter.OrderKey
if filter.Desc {
order += " desc"
}
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []userPO
if err := db.Order(order).Offset((page - 1) * pageSize).Limit(pageSize).Find(&pos).Error; err != nil {
return nil, 0, err
}
users := make([]*biz.User, 0, len(pos))
for i := range pos {
user, err := r.toBizUser(ctx, &pos[i])
if err != nil {
return nil, 0, err
}
users = append(users, user)
}
return users, total, nil
}
func (r *systemRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User, error) {
if user.UUID == "" {
user.UUID = uuid.NewString()
}
if user.Enable == 0 {
user.Enable = 1
}
if user.AuthorityID == 0 {
user.AuthorityID = 888
}
if user.NickName == "" {
user.NickName = "系统用户"
}
now := time.Now()
po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword}
if err := r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&po).Error; err != nil {
return err
}
return tx.Create(&userAuthorityPO{SysUserID: po.ID, SysAuthorityAuthorityID: po.AuthorityID}).Error
}); err != nil {
return nil, err
}
return r.FindUserByID(ctx, po.ID)
}
func (r *systemRepo) UpdateUser(ctx context.Context, user *biz.User) error {
updates := map[string]any{"nick_name": user.NickName, "header_img": user.HeaderImg, "phone": user.Phone, "email": user.Email, "enable": user.Enable}
if user.AuthorityID != 0 {
updates["authority_id"] = user.AuthorityID
}
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error
}
func (r *systemRepo) DeleteUser(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
return err
}
return tx.Delete(&userPO{}, id).Error
})
}
func (r *systemRepo) UpdatePassword(ctx context.Context, id uint, password string) error {
now := time.Now()
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(map[string]any{"password": password, "password_updated_at": now, "must_change_password": false}).Error
}
func (r *systemRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
var pos []authorityPO
if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil {
return nil, err
}
result := make([]*biz.Authority, 0, len(pos))
for _, po := range pos {
a := toBizAuthority(po)
result = append(result, &a)
}
return result, nil
}
func (r *systemRepo) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
return err
}
links := make([]userAuthorityPO, 0, len(authorityIDs))
for _, authorityID := range authorityIDs {
var count int64
if err := tx.Model(&authorityPO{}).Where("authority_id = ?", authorityID).Count(&count).Error; err != nil || count == 0 {
if err != nil {
return err
}
return errors.New("角色不存在")
}
links = append(links, userAuthorityPO{SysUserID: id, SysAuthorityAuthorityID: authorityID})
}
if err := tx.Create(&links).Error; err != nil {
return err
}
return tx.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityIDs[0]).Error
})
}
func (r *systemRepo) SetUserAuthority(ctx context.Context, id, authorityID uint) error {
var count int64
if err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityID).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return errors.New("该用户无此角色")
}
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityID).Error
}
func (r *systemRepo) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error {
value, err := json.Marshal(setting)
if err != nil {
return err
}
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Update("origin_setting", string(value)).Error
}
// ActiveName temporarily carries the parent menu name during seeding. It is
// cleared from responses by the service for these records.
func defaultMenus() []menuPO {
root := func(path, name, title, icon string, sort int) menuPO {
return menuPO{Path: path, Name: name, Component: "view/routerHolder.vue", Title: title, Icon: icon, Sort: sort}
}
child := func(parent, path, name, component, title, icon string, sort int) menuPO {
return menuPO{MenuLevel: 1, Path: path, Name: name, Component: component, Title: title, Icon: icon, Sort: sort, ActiveName: parent}
}
return []menuPO{
{Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Title: "仪表盘", Icon: "odometer", Sort: 1},
root("permission", "permission", "权限管理", "perm-gva", 2), root("org", "org", "组织管理", "share", 3), root("systemConfig", "systemConfig", "系统设置", "config-gva", 4), root("monitor", "monitor", "运维监控", "monitor-gva", 5), root("media", "media", "媒体管理", "folder-opened", 6),
{Path: "person", Name: "person", Component: "view/person/person.vue", Title: "个人信息", Icon: "postcard", Hidden: true, Sort: 13},
child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-gva", 1), child("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2), child("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-gva", 3), child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4),
child("org", "user", "user", "view/superAdmin/user/user.vue", "用户管理", "user", 1), child("org", "department", "department", "view/superAdmin/department/department.vue", "部门管理", "office-building", 2), child("org", "position", "position", "view/superAdmin/position/position.vue", "岗位管理", "postcard", 3),
child("systemConfig", "system", "system", "view/systemTools/system/system.vue", "配置文件", "config-file-gva", 1), child("systemConfig", "dictionary", "dictionary", "view/superAdmin/dictionary/sysDictionary.vue", "字典管理", "notebook", 2), child("systemConfig", "sysParams", "sysParams", "view/superAdmin/params/sysParams.vue", "参数管理", "set-up", 3), child("systemConfig", "security", "security", "view/system/security/index.vue", "安全配置", "security-gva", 4),
child("monitor", "operation", "operation", "view/superAdmin/operation/sysOperationRecord.vue", "操作历史", "document", 1), child("monitor", "loginLog", "loginLog", "view/systemTools/loginLog/index.vue", "登录日志", "clock", 2), child("monitor", "sysError", "sysError", "view/systemTools/sysError/sysError.vue", "错误日志", "error-gva", 3), child("monitor", "sysVersion", "sysVersion", "view/systemTools/version/version.vue", "版本管理", "version-gva", 4), child("monitor", "state", "state", "view/system/state.vue", "服务器状态", "server", 5), child("monitor", "dataAccessLog", "dataAccessLog", "view/superAdmin/dataAccessLog/dataAccessLog.vue", "数据权限审计", "warning", 6), child("monitor", "timedTask", "timedTask", "view/systemTools/timedTask/index.vue", "定时任务", "timer", 7), child("monitor", "logViewer", "logViewer", "view/systemTools/logViewer/index.vue", "文件日志", "document", 8),
child("media", "upload", "upload", "view/media/upload.vue", "媒体库(上传下载)", "upload", 1), child("media", "chunkUpload", "chunkUpload", "view/media/chunkUpload.vue", "大文件上传", "folder-add", 2),
}
}
+155
View File
@@ -0,0 +1,155 @@
package data
import (
"context"
"gorm.io/gorm"
"kra/internal/biz"
"time"
)
type taskPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string `gorm:"index"`
Description, Spec string
WithSeconds bool
ExecutorType, MethodName string
Params []byte `gorm:"type:json"`
HTTPURL string
HTTPMethod string
HTTPHeader []byte `gorm:"type:json"`
HTTPBody string `gorm:"type:text"`
HTTPAllowPrivate, Enabled bool
}
func (taskPO) TableName() string { return "sys_timed_tasks" }
type taskLogPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
TaskID uint `gorm:"index"`
TaskName, TriggerType string
StartedAt, FinishedAt time.Time
DurationMS int64
Status string
ErrorMsg, Output string `gorm:"type:text"`
}
func (taskLogPO) TableName() string { return "sys_timed_task_logs" }
type taskRepo struct{ data *Data }
func NewTaskRepo(data *Data) biz.TaskRepo { return &taskRepo{data: data} }
func taskToPO(v *biz.TimedTask) taskPO {
return taskPO{ID: v.ID, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: v.Params, HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: v.HTTPHeader, HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
}
func taskFromPO(v taskPO) *biz.TimedTask {
return &biz.TimedTask{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: v.Params, HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: v.HTTPHeader, HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
}
func (r *taskRepo) CreateTask(ctx context.Context, v *biz.TimedTask) error {
po := taskToPO(v)
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
return nil
}
func (r *taskRepo) UpdateTask(ctx context.Context, v *biz.TimedTask) error {
po := taskToPO(v)
return r.data.gormDB.WithContext(ctx).Model(&taskPO{}).Where("id = ?", v.ID).Select("name", "description", "spec", "with_seconds", "executor_type", "method_name", "params", "http_url", "http_method", "http_header", "http_body", "http_allow_private", "enabled").Updates(&po).Error
}
func (r *taskRepo) DeleteTask(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&taskPO{}, id).Error
}
func (r *taskRepo) FindTask(ctx context.Context, id uint) (*biz.TimedTask, error) {
var po taskPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return taskFromPO(po), nil
}
func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTask) ([]*biz.TimedTask, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&taskPO{})
if q != nil {
if q.Name != "" {
db = db.Where("name LIKE ?", "%"+q.Name+"%")
}
if q.ExecutorType != "" {
db = db.Where("executor_type = ?", q.ExecutorType)
}
if q.EnabledFilter != nil {
db = db.Where("enabled = ?", *q.EnabledFilter)
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []taskPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.TimedTask, 0, len(pos))
for _, po := range pos {
out = append(out, taskFromPO(po))
}
return out, total, nil
}
func (r *taskRepo) ToggleTask(ctx context.Context, id uint, enabled bool) error {
return r.data.gormDB.WithContext(ctx).Model(&taskPO{}).Where("id = ?", id).Update("enabled", enabled).Error
}
func (r *taskRepo) RecordTaskLog(ctx context.Context, v *biz.TimedTaskLog) error {
return r.data.gormDB.WithContext(ctx).Create(&taskLogPO{TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}).Error
}
func taskLogFromPO(v taskLogPO) *biz.TimedTaskLog {
return &biz.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
}
func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint, status string) ([]*biz.TimedTaskLog, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&taskLogPO{})
if taskID != 0 {
db = db.Where("task_id = ?", taskID)
}
if status != "" {
db = db.Where("status = ?", status)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []taskLogPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.TimedTaskLog, 0, len(pos))
for _, po := range pos {
out = append(out, taskLogFromPO(po))
}
return out, total, nil
}
func (r *taskRepo) CleanupLogs(ctx context.Context, before time.Time) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Unscoped().Where("created_at < ?", before).Delete(&operationPO{}).Error; err != nil {
return err
}
if err := tx.Unscoped().Where("created_at < ?", before).Delete(&loginLogPO{}).Error; err != nil {
return err
}
return tx.Unscoped().Where("created_at < ?", before).Delete(&taskLogPO{}).Error
})
}
+201
View File
@@ -0,0 +1,201 @@
package data
import (
"context"
"errors"
"strings"
"time"
"gorm.io/gorm"
"kra/internal/biz"
)
type versionPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string `gorm:"column:version_name"`
Code string `gorm:"column:version_code"`
Description string
Data string `gorm:"column:version_data;type:text"`
}
func (versionPO) TableName() string { return "sys_versions" }
type versionRepo struct{ data *Data }
func NewVersionRepo(data *Data) biz.VersionRepo { return &versionRepo{data: data} }
func versionFromPO(v versionPO) *biz.Version {
return &biz.Version{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Code: v.Code, Description: v.Description, Data: v.Data}
}
func (r *versionRepo) CreateVersion(ctx context.Context, v *biz.Version) error {
po := versionPO{Name: v.Name, Code: v.Code, Description: v.Description, Data: v.Data}
if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil {
return err
}
v.ID = po.ID
return nil
}
func (r *versionRepo) DeleteVersions(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Delete(&versionPO{}, ids).Error
}
func (r *versionRepo) FindVersion(ctx context.Context, id uint) (*biz.Version, error) {
var po versionPO
if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return versionFromPO(po), nil
}
func (r *versionRepo) ListVersions(ctx context.Context, page, size int, name, code string) ([]*biz.Version, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 10
}
db := r.data.gormDB.WithContext(ctx).Model(&versionPO{})
if name != "" {
db = db.Where("version_name LIKE ?", "%"+name+"%")
}
if code != "" {
db = db.Where("version_code = ?", code)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var pos []versionPO
if err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.Version, 0, len(pos))
for _, po := range pos {
out = append(out, versionFromPO(po))
}
return out, total, nil
}
func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, dictIDs []uint) (*biz.VersionBundle, error) {
bundle := &biz.VersionBundle{}
var menus []menuPO
if len(menuIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", menuIDs).Order("sort,id").Find(&menus).Error; err != nil {
return nil, err
}
}
byID := map[uint]*biz.Menu{}
for _, po := range menus {
v := menuFromPO(po)
v.Children = []*biz.Menu{}
var btns []menuButtonPO
_ = r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", po.ID).Find(&btns).Error
for _, btn := range btns {
v.Buttons = append(v.Buttons, &biz.MenuButton{Name: btn.Name, Description: btn.Description})
}
byID[po.ID] = v
}
for _, po := range menus {
v := byID[po.ID]
if parent := byID[po.ParentID]; parent != nil {
parent.Children = append(parent.Children, v)
} else {
bundle.Menus = append(bundle.Menus, v)
}
}
var apis []apiPO
if len(apiIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", apiIDs).Find(&apis).Error; err != nil {
return nil, err
}
}
for _, po := range apis {
v := apiFromPO(po)
v.ID = 0
bundle.APIs = append(bundle.APIs, v)
}
var dictionaries []dictionaryPO
if len(dictIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("id IN ?", dictIDs).Find(&dictionaries).Error; err != nil {
return nil, err
}
}
settings := &settingsRepo{data: r.data}
for _, po := range dictionaries {
v := dictionaryFromPO(po)
v.ID = 0
details, _, err := settings.ListDictionaryDetails(ctx, 1, 100000, po.ID, "", "")
if err != nil {
return nil, err
}
for _, d := range details {
d.ID = 0
d.DictionaryID = 0
d.ParentID = nil
d.Path = ""
d.Level = 0
}
v.Details = details
bundle.Dictionaries = append(bundle.Dictionaries, v)
}
return bundle, nil
}
func (r *versionRepo) ImportVersionBundle(ctx context.Context, b *biz.VersionBundle) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var addMenus func([]*biz.Menu, uint) error
addMenus = func(items []*biz.Menu, parent uint) error {
for _, item := range items {
var existing menuPO
err := tx.Where("name = ? AND path = ?", item.Name, item.Path).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
po := menuToPO(item)
po.ID = 0
po.ParentID = parent
if err = tx.Create(&po).Error; err != nil {
return err
}
existing = po
for _, button := range item.Buttons {
if err = tx.Create(&menuButtonPO{Name: button.Name, Description: button.Description, MenuID: po.ID}).Error; err != nil {
return err
}
}
} else if err != nil {
return err
}
if err = addMenus(item.Children, existing.ID); err != nil {
return err
}
}
return nil
}
if err := addMenus(b.Menus, 0); err != nil {
return err
}
for _, item := range b.APIs {
po := apiPO{Path: item.Path, Description: item.Description, APIGroup: item.APIGroup, Method: strings.ToUpper(item.Method)}
if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
return err
}
}
for _, item := range b.Dictionaries {
var count int64
if err := tx.Model(&dictionaryPO{}).Where("type = ?", item.Type).Count(&count).Error; err != nil {
return err
}
if count > 0 {
continue
}
po := dictionaryPO{Name: item.Name, Type: item.Type, Status: item.Status, Desc: item.Desc}
if err := tx.Create(&po).Error; err != nil {
return err
}
for _, detail := range item.Details {
d := dictionaryDetailPO{Label: detail.Label, Value: detail.Value, Extend: detail.Extend, Status: detail.Status, Sort: detail.Sort, DictionaryID: po.ID}
if err := tx.Create(&d).Error; err != nil {
return err
}
}
}
return nil
})
}