This commit is contained in:
yvan 2026-08-16 01:19:10 +08:00
parent c4b114762b
commit c554152f4f
43 changed files with 1315 additions and 322 deletions

View File

@ -72,9 +72,9 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
cleanup() cleanup()
return nil, nil, err return nil, nil, err
} }
mediaUsecase := biz.NewMediaUsecase(mediaRepo, fileStorage) mediaUsecase := biz.NewMediaUsecase(mediaRepo, fileStorage, runtimeSettings)
taskExecutor := worker.NewTaskExecutor(taskUsecase, mediaUsecase, runtime) taskExecutor := worker.NewTaskExecutor(taskUsecase, mediaUsecase, runtime)
taskScheduler := worker.NewTaskScheduler(taskUsecase, taskExecutor, logger) taskScheduler := worker.NewTaskScheduler(taskUsecase, authorityUsecase, taskExecutor, logger)
taskRuntime := worker.NewTaskRuntime(taskScheduler) taskRuntime := worker.NewTaskRuntime(taskScheduler)
taskApplicationUsecase := biz.NewTaskApplicationUsecase(taskUsecase, taskRuntime) taskApplicationUsecase := biz.NewTaskApplicationUsecase(taskUsecase, taskRuntime)
taskService := service.NewTaskService(taskApplicationUsecase) taskService := service.NewTaskService(taskApplicationUsecase)

View File

@ -104,12 +104,16 @@ admin:
# Upload chunks are stored below .chunks in the selected storage backend. # Upload chunks are stored below .chunks in the selected storage backend.
session_ttl: 24 session_ttl: 24
max_file_size: 0 max_file_size: 0
chunk_dir: .chunks
system: system:
use_redis: false use_redis: false
use_multipoint: false use_multipoint: false
use_strict_auth: false use_strict_auth: false
disable_auto_migrate: false disable_auto_migrate: false
use_mongo: false use_mongo: false
addr: 8000
iplimit_count: 0
iplimit_time: 0
zap: zap:
level: info level: info
prefix: "[kra] " prefix: "[kra] "

View File

@ -36,9 +36,20 @@ func (uc *AuthorityUsecase) AuthorityTree(ctx context.Context) ([]*Authority, er
if item.ParentID != nil && *item.ParentID != 0 && byID[*item.ParentID] != nil { if item.ParentID != nil && *item.ParentID != 0 && byID[*item.ParentID] != nil {
parent := byID[*item.ParentID] parent := byID[*item.ParentID]
parent.Children = append(parent.Children, item) parent.Children = append(parent.Children, item)
} else { } else if item.ParentID == nil || *item.ParentID == 0 {
roots = append(roots, item) roots = append(roots, item)
} }
} }
// In GVA strict-authority mode a non-root actor receives its direct
// children as the top-level result even though their parent is omitted.
if len(roots) == 0 {
if actor, ok := ActorFromContext(ctx); ok {
for _, item := range items {
if item.ParentID != nil && *item.ParentID == actor.AuthorityID {
roots = append(roots, item)
}
}
}
}
return roots, nil return roots, nil
} }

View File

@ -17,10 +17,12 @@ type Cache interface {
} }
type StoredFile struct { type StoredFile struct {
Name string Name string
Path string Path string
URL string URL string
Size int64 Size int64
LastModified time.Time
ContentType string
} }
// FileStorage owns the persistence boundary for uploaded files. // FileStorage owns the persistence boundary for uploaded files.
@ -50,6 +52,7 @@ type CaptchaSettings struct {
type MediaSettings struct { type MediaSettings struct {
SessionTTL int SessionTTL int
MaxFileSize int64 MaxFileSize int64
ChunkDir string
} }
// RuntimeSettings exposes only the active values needed by the application. // RuntimeSettings exposes only the active values needed by the application.
@ -75,6 +78,7 @@ type AuthClaims struct {
Username string Username string
NickName string NickName string
AuthorityID uint AuthorityID uint
UserType string
BufferTime time.Duration BufferTime time.Duration
MustChangePwd bool MustChangePwd bool
Issuer string Issuer string

View File

@ -22,11 +22,12 @@ type MediaRepo interface {
type MediaUsecase struct { type MediaUsecase struct {
MediaRepo MediaRepo
files FileStorage files FileStorage
settings RuntimeSettings
} }
func NewMediaUsecase(repo MediaRepo, files FileStorage) *MediaUsecase { func NewMediaUsecase(repo MediaRepo, files FileStorage, settings RuntimeSettings) *MediaUsecase {
return &MediaUsecase{MediaRepo: repo, files: files} return &MediaUsecase{MediaRepo: repo, files: files, settings: settings}
} }
var allowedMediaExtensions = map[string]bool{ var allowedMediaExtensions = map[string]bool{

View File

@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"path"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@ -15,6 +16,20 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
func (uc *MediaUsecase) chunkPrefix(uploadID uint) string {
directory := ".chunks"
if uc.settings != nil {
if configured := strings.Trim(uc.settings.MediaSettings().ChunkDir, "/\\ "); configured != "" {
directory = configured
}
}
return path.Join(directory, fmt.Sprintf("%d", uploadID))
}
func (uc *MediaUsecase) chunkKey(uploadID uint, index int) string {
return path.Join(uc.chunkPrefix(uploadID), fmt.Sprintf("%08d", index))
}
func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash string, size, chunkSize int64, total int) (*UploadSession, *MediaFile, []int, error) { func (uc *MediaUsecase) InitUpload(ctx context.Context, userID uint, name, hash string, size, chunkSize int64, total int) (*UploadSession, *MediaFile, []int, error) {
if err := validateMediaName(name); err != nil { if err := validateMediaName(name); err != nil {
return nil, nil, nil, err return nil, nil, nil, err
@ -57,7 +72,7 @@ func (uc *MediaUsecase) SaveChunk(ctx context.Context, userID, uploadID uint, in
return errors.New("上传会话状态不允许收片") return errors.New("上传会话状态不允许收片")
} }
hash := md5.New() hash := md5.New()
key := fmt.Sprintf(".chunks/%d/%08d", uploadID, index) key := uc.chunkKey(uploadID, index)
stored, err := uc.files.Put(ctx, key, io.TeeReader(reader, hash)) stored, err := uc.files.Put(ctx, key, io.TeeReader(reader, hash))
if err != nil { if err != nil {
return err return err
@ -95,7 +110,7 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin
if chunk.Index != index { if chunk.Index != index {
return fail(errors.New("分片序号不连续")) return fail(errors.New("分片序号不连续"))
} }
names = append(names, fmt.Sprintf(".chunks/%d/%08d", uploadID, index)) names = append(names, uc.chunkKey(uploadID, index))
} }
ext := strings.ToLower(filepath.Ext(session.FileName)) ext := strings.ToLower(filepath.Ext(session.FileName))
key := time.Now().Format("20060102") + "/" + uuid.NewString() + ext key := time.Now().Format("20060102") + "/" + uuid.NewString() + ext
@ -114,7 +129,7 @@ func (uc *MediaUsecase) CompleteUpload(ctx context.Context, userID, uploadID uin
} }
_ = uc.CompleteUploadSession(ctx, uploadID, key, media.ID) _ = uc.CompleteUploadSession(ctx, uploadID, key, media.ID)
_ = uc.DeleteChunks(ctx, uploadID) _ = uc.DeleteChunks(ctx, uploadID)
_ = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", uploadID)) _ = uc.files.DeletePrefix(ctx, uc.chunkPrefix(uploadID))
return media, nil return media, nil
} }
func (uc *MediaUsecase) CancelUpload(ctx context.Context, userID, uploadID uint) error { func (uc *MediaUsecase) CancelUpload(ctx context.Context, userID, uploadID uint) error {
@ -123,7 +138,7 @@ func (uc *MediaUsecase) CancelUpload(ctx context.Context, userID, uploadID uint)
return errors.New("上传会话不存在或无权操作") return errors.New("上传会话不存在或无权操作")
} }
_ = uc.DeleteChunks(ctx, uploadID) _ = uc.DeleteChunks(ctx, uploadID)
_ = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", uploadID)) _ = uc.files.DeletePrefix(ctx, uc.chunkPrefix(uploadID))
return uc.DeleteUploadSession(ctx, uploadID) return uc.DeleteUploadSession(ctx, uploadID)
} }
func (uc *MediaUsecase) CleanupStale(ctx context.Context, ttlHours int) error { func (uc *MediaUsecase) CleanupStale(ctx context.Context, ttlHours int) error {
@ -138,7 +153,7 @@ func (uc *MediaUsecase) CleanupStale(ctx context.Context, ttlHours int) error {
if err = uc.DeleteUploadData(ctx, id); err != nil { if err = uc.DeleteUploadData(ctx, id); err != nil {
return err return err
} }
if err = uc.files.DeletePrefix(ctx, fmt.Sprintf(".chunks/%d", id)); err != nil { if err = uc.files.DeletePrefix(ctx, uc.chunkPrefix(id)); err != nil {
return err return err
} }
} }

View File

@ -91,8 +91,8 @@ type TaskRuntime interface {
TriggerID(context.Context, uint) error TriggerID(context.Context, uint) error
NextRuns() map[uint]time.Time NextRuns() map[uint]time.Time
Reload(context.Context) error Reload(context.Context) error
Subscribe() chan []byte Subscribe(uint) chan []byte
Unsubscribe(chan []byte) Unsubscribe(uint, chan []byte)
} }
type TaskUsecase struct{ TaskRepo } type TaskUsecase struct{ TaskRepo }
@ -115,7 +115,7 @@ func (uc *TaskUsecase) Validate(value *TimedTask) error {
switch value.ExecutorType { switch value.ExecutorType {
case TaskExecutorMethod: case TaskExecutorMethod:
if !registeredTaskMethod(value.MethodName) { if !registeredTaskMethod(value.MethodName) {
return errors.New("方法未注册") return fmt.Errorf("方法 %s 未注册", value.MethodName)
} }
if len(value.Params) > 0 && !json.Valid(value.Params) { if len(value.Params) > 0 && !json.Valid(value.Params) {
return errors.New("params 必须是合法 JSON") return errors.New("params 必须是合法 JSON")
@ -128,7 +128,7 @@ func (uc *TaskUsecase) Validate(value *TimedTask) error {
if len(value.HTTPHeader) > 0 { if len(value.HTTPHeader) > 0 {
headers := map[string]string{} headers := map[string]string{}
if json.Unmarshal(value.HTTPHeader, &headers) != nil { if json.Unmarshal(value.HTTPHeader, &headers) != nil {
return errors.New("httpHeader 必须是 JSON 对象") return errors.New(`httpHeader 必须是 {"Key":"Value"} 形式的 JSON 对象`)
} }
} }
default: default:
@ -229,7 +229,9 @@ func (uc *TaskApplicationUsecase) Reload(ctx context.Context) error {
return uc.runtime.Reload(ctx) return uc.runtime.Reload(ctx)
} }
func (uc *TaskApplicationUsecase) Subscribe() chan []byte { return uc.runtime.Subscribe() } func (uc *TaskApplicationUsecase) Subscribe(userID uint) chan []byte {
func (uc *TaskApplicationUsecase) Unsubscribe(events chan []byte) { return uc.runtime.Subscribe(userID)
uc.runtime.Unsubscribe(events) }
func (uc *TaskApplicationUsecase) Unsubscribe(userID uint, events chan []byte) {
uc.runtime.Unsubscribe(userID, events)
} }

View File

@ -1168,6 +1168,7 @@ type AdminBackend_Media struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
SessionTtl int32 `protobuf:"varint,1,opt,name=session_ttl,json=sessionTtl,proto3" json:"session_ttl,omitempty"` SessionTtl int32 `protobuf:"varint,1,opt,name=session_ttl,json=sessionTtl,proto3" json:"session_ttl,omitempty"`
MaxFileSize int64 `protobuf:"varint,2,opt,name=max_file_size,json=maxFileSize,proto3" json:"max_file_size,omitempty"` MaxFileSize int64 `protobuf:"varint,2,opt,name=max_file_size,json=maxFileSize,proto3" json:"max_file_size,omitempty"`
ChunkDir string `protobuf:"bytes,3,opt,name=chunk_dir,json=chunkDir,proto3" json:"chunk_dir,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -1216,6 +1217,13 @@ func (x *AdminBackend_Media) GetMaxFileSize() int64 {
return 0 return 0
} }
func (x *AdminBackend_Media) GetChunkDir() string {
if x != nil {
return x.ChunkDir
}
return ""
}
type AdminBackend_Disk struct { type AdminBackend_Disk struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
MountPoint string `protobuf:"bytes,1,opt,name=mount_point,json=mountPoint,proto3" json:"mount_point,omitempty"` MountPoint string `protobuf:"bytes,1,opt,name=mount_point,json=mountPoint,proto3" json:"mount_point,omitempty"`
@ -1267,6 +1275,9 @@ type AdminBackend_System struct {
UseStrictAuth bool `protobuf:"varint,3,opt,name=use_strict_auth,json=useStrictAuth,proto3" json:"use_strict_auth,omitempty"` UseStrictAuth bool `protobuf:"varint,3,opt,name=use_strict_auth,json=useStrictAuth,proto3" json:"use_strict_auth,omitempty"`
DisableAutoMigrate bool `protobuf:"varint,4,opt,name=disable_auto_migrate,json=disableAutoMigrate,proto3" json:"disable_auto_migrate,omitempty"` DisableAutoMigrate bool `protobuf:"varint,4,opt,name=disable_auto_migrate,json=disableAutoMigrate,proto3" json:"disable_auto_migrate,omitempty"`
UseMongo bool `protobuf:"varint,5,opt,name=use_mongo,json=useMongo,proto3" json:"use_mongo,omitempty"` UseMongo bool `protobuf:"varint,5,opt,name=use_mongo,json=useMongo,proto3" json:"use_mongo,omitempty"`
Addr int32 `protobuf:"varint,6,opt,name=addr,proto3" json:"addr,omitempty"`
IplimitCount int32 `protobuf:"varint,7,opt,name=iplimit_count,json=iplimitCount,proto3" json:"iplimit_count,omitempty"`
IplimitTime int32 `protobuf:"varint,8,opt,name=iplimit_time,json=iplimitTime,proto3" json:"iplimit_time,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -1336,6 +1347,27 @@ func (x *AdminBackend_System) GetUseMongo() bool {
return false return false
} }
func (x *AdminBackend_System) GetAddr() int32 {
if x != nil {
return x.Addr
}
return 0
}
func (x *AdminBackend_System) GetIplimitCount() int32 {
if x != nil {
return x.IplimitCount
}
return 0
}
func (x *AdminBackend_System) GetIplimitTime() int32 {
if x != nil {
return x.IplimitTime
}
return 0
}
type AdminBackend_Zap struct { type AdminBackend_Zap struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Level string `protobuf:"bytes,1,opt,name=level,proto3" json:"level,omitempty"` Level string `protobuf:"bytes,1,opt,name=level,proto3" json:"level,omitempty"`
@ -2054,7 +2086,7 @@ const file_conf_conf_proto_rawDesc = "" +
"\x12connect_timeout_ms\x18\n" + "\x12connect_timeout_ms\x18\n" +
" \x01(\x03R\x10connectTimeoutMs\x12\x15\n" + " \x01(\x03R\x10connectTimeoutMs\x12\x15\n" +
"\x06is_zap\x18\v \x01(\bR\x05isZap\x120\n" + "\x06is_zap\x18\v \x01(\bR\x05isZap\x120\n" +
"\x05hosts\x18\f \x03(\v2\x1a.kratos.api.Data.MongoHostR\x05hosts\"\xb4\x1b\n" + "\x05hosts\x18\f \x03(\v2\x1a.kratos.api.Data.MongoHostR\x05hosts\"\xad\x1c\n" +
"\fAdminBackend\x12#\n" + "\fAdminBackend\x12#\n" +
"\rrouter_prefix\x18\x01 \x01(\tR\frouterPrefix\x12.\n" + "\rrouter_prefix\x18\x01 \x01(\tR\frouterPrefix\x12.\n" +
"\x03jwt\x18\x02 \x01(\v2\x1c.kratos.api.AdminBackend.JWTR\x03jwt\x12:\n" + "\x03jwt\x18\x02 \x01(\v2\x1c.kratos.api.AdminBackend.JWTR\x03jwt\x12:\n" +
@ -2097,20 +2129,24 @@ const file_conf_conf_proto_rawDesc = "" +
"\bnickname\x18\x05 \x01(\tR\bnickname\x12\x12\n" + "\bnickname\x18\x05 \x01(\tR\bnickname\x12\x12\n" +
"\x04port\x18\x06 \x01(\x05R\x04port\x12\x15\n" + "\x04port\x18\x06 \x01(\x05R\x04port\x12\x15\n" +
"\x06is_ssl\x18\a \x01(\bR\x05isSsl\x12\"\n" + "\x06is_ssl\x18\a \x01(\bR\x05isSsl\x12\"\n" +
"\ris_login_auth\x18\b \x01(\bR\visLoginAuth\x1aL\n" + "\ris_login_auth\x18\b \x01(\bR\visLoginAuth\x1ai\n" +
"\x05Media\x12\x1f\n" + "\x05Media\x12\x1f\n" +
"\vsession_ttl\x18\x01 \x01(\x05R\n" + "\vsession_ttl\x18\x01 \x01(\x05R\n" +
"sessionTtl\x12\"\n" + "sessionTtl\x12\"\n" +
"\rmax_file_size\x18\x02 \x01(\x03R\vmaxFileSize\x1a'\n" + "\rmax_file_size\x18\x02 \x01(\x03R\vmaxFileSize\x12\x1b\n" +
"\tchunk_dir\x18\x03 \x01(\tR\bchunkDir\x1a'\n" +
"\x04Disk\x12\x1f\n" + "\x04Disk\x12\x1f\n" +
"\vmount_point\x18\x01 \x01(\tR\n" + "\vmount_point\x18\x01 \x01(\tR\n" +
"mountPoint\x1a\xc3\x01\n" + "mountPoint\x1a\x9f\x02\n" +
"\x06System\x12\x1b\n" + "\x06System\x12\x1b\n" +
"\tuse_redis\x18\x01 \x01(\bR\buseRedis\x12%\n" + "\tuse_redis\x18\x01 \x01(\bR\buseRedis\x12%\n" +
"\x0euse_multipoint\x18\x02 \x01(\bR\ruseMultipoint\x12&\n" + "\x0euse_multipoint\x18\x02 \x01(\bR\ruseMultipoint\x12&\n" +
"\x0fuse_strict_auth\x18\x03 \x01(\bR\ruseStrictAuth\x120\n" + "\x0fuse_strict_auth\x18\x03 \x01(\bR\ruseStrictAuth\x120\n" +
"\x14disable_auto_migrate\x18\x04 \x01(\bR\x12disableAutoMigrate\x12\x1b\n" + "\x14disable_auto_migrate\x18\x04 \x01(\bR\x12disableAutoMigrate\x12\x1b\n" +
"\tuse_mongo\x18\x05 \x01(\bR\buseMongo\x1a\xf6\x03\n" + "\tuse_mongo\x18\x05 \x01(\bR\buseMongo\x12\x12\n" +
"\x04addr\x18\x06 \x01(\x05R\x04addr\x12#\n" +
"\riplimit_count\x18\a \x01(\x05R\fiplimitCount\x12!\n" +
"\fiplimit_time\x18\b \x01(\x05R\viplimitTime\x1a\xf6\x03\n" +
"\x03Zap\x12\x14\n" + "\x03Zap\x12\x14\n" +
"\x05level\x18\x01 \x01(\tR\x05level\x12\x16\n" + "\x05level\x18\x01 \x01(\tR\x05level\x12\x16\n" +
"\x06prefix\x18\x02 \x01(\tR\x06prefix\x12\x16\n" + "\x06prefix\x18\x02 \x01(\tR\x06prefix\x12\x16\n" +

View File

@ -127,6 +127,7 @@ message AdminBackend {
message Media { message Media {
int32 session_ttl = 1; int32 session_ttl = 1;
int64 max_file_size = 2; int64 max_file_size = 2;
string chunk_dir = 3;
} }
message Disk { message Disk {
@ -139,6 +140,9 @@ message AdminBackend {
bool use_strict_auth = 3; bool use_strict_auth = 3;
bool disable_auto_migrate = 4; bool disable_auto_migrate = 4;
bool use_mongo = 5; bool use_mongo = 5;
int32 addr = 6;
int32 iplimit_count = 7;
int32 iplimit_time = 8;
} }
message Zap { message Zap {

View File

@ -94,7 +94,7 @@ func (r *announcementRepo) UserOptions(ctx context.Context) ([]biz.UserOption, e
Label string Label string
Value uint Value uint
} }
err := r.data.gormDB.WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Where("deleted_at IS NULL").Scan(&rows).Error err := r.data.gormDB.WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Scan(&rows).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err return nil, err
} }

View File

@ -98,7 +98,7 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
return err 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"}} defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/base/login", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
for _, item := range defaults { for _, item := range defaults {
var api apiPO var api apiPO
if err := tx.Where("path = ? AND method = ?", item.path, item.method).First(&api).Error; err == nil { if err := tx.Where("path = ? AND method = ?", item.path, item.method).First(&api).Error; err == nil {
@ -186,13 +186,23 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
}) })
} }
func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *biz.Authority) error { func (r *authorityAccessRepo) 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 db := r.data.gormDB.WithContext(ctx)
var current authorityPO
if err := db.Where("authority_id = ?", value.AuthorityID).First(&current).Error; err != nil {
return errors.New("查询角色数据失败")
}
updates := &authorityPO{AuthorityName: value.AuthorityName, ParentID: value.ParentID, DataScope: value.DataScope, DefaultRouter: value.DefaultRouter}
return db.Model(&current).Updates(updates).Error
} }
func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) error { func (r *authorityAccessRepo) 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 { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var authority authorityPO
if err := tx.Where("authority_id = ?", id).First(&authority).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("该角色不存在")
}
return err
}
var users, children int64 var users, children int64
if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Count(&users).Error; err != nil { if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Count(&users).Error; err != nil {
return err return err
@ -221,15 +231,12 @@ func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) erro
if err := tx.Where("authority_id = ?", id).Delete(&authorityButtonPO{}).Error; err != nil { if err := tx.Where("authority_id = ?", id).Delete(&authorityButtonPO{}).Error; err != nil {
return err return err
} }
if err := tx.Where("authority_id = ?", id).Delete(&authorityDepartmentPO{}).Error; err != nil {
return err
}
return tx.Delete(&authorityPO{}, "authority_id = ?", id).Error return tx.Delete(&authorityPO{}, "authority_id = ?", id).Error
}) })
} }
func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) { func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
var pos []authorityPO var pos []authorityPO
if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil { if err := r.data.gormDB.WithContext(ctx).Find(&pos).Error; err != nil {
return nil, err return nil, err
} }
var allowed map[uint]bool var allowed map[uint]bool

View File

@ -66,6 +66,16 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
} }
} }
maskDataSecrets(dataConfig) maskDataSecrets(dataConfig)
safeAdmin := cloneAdminConfig(adminConfig)
if safeAdmin != nil {
if safeAdmin.Jwt != nil {
safeAdmin.Jwt.SigningKey = "******"
}
if safeAdmin.Email != nil {
safeAdmin.Email.Secret = "******"
}
maskStorageSecrets(safeAdmin.Storage)
}
dataMap := map[string]any{} dataMap := map[string]any{}
if dataConfig != nil { if dataConfig != nil {
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(dataConfig) raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(dataConfig)
@ -76,7 +86,9 @@ func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
return nil, err return nil, err
} }
} }
return json.Marshal(map[string]any{"config": map[string]any{"admin": admin, "email": email, "data": dataMap}}) config := gvaConfiguration(dataConfig, safeAdmin)
config["admin"], config["email"], config["data"] = admin, email, dataMap
return json.Marshal(map[string]any{"config": config})
} }
func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) error { func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) error {
@ -86,6 +98,9 @@ func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json
} }
nextData := cloneDataConfig(currentData) nextData := cloneDataConfig(currentData)
nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend) nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend)
if err := applyGVAConfiguration(raw, nextData, nextAdmin); err != nil {
return err
}
var value configurationEnvelope var value configurationEnvelope
if err := json.Unmarshal(raw, &value); err != nil { if err := json.Unmarshal(raw, &value); err != nil {
return err return err
@ -154,6 +169,13 @@ func cloneDataConfig(value *conf.Data) *conf.Data {
return proto.Clone(value).(*conf.Data) return proto.Clone(value).(*conf.Data)
} }
func cloneAdminConfig(value *conf.AdminBackend) *conf.AdminBackend {
if value == nil {
return &conf.AdminBackend{}
}
return proto.Clone(value).(*conf.AdminBackend)
}
func refreshDatabaseSources(value *conf.Data) error { func refreshDatabaseSources(value *conf.Data) error {
if value == nil { if value == nil {
return nil return nil
@ -222,6 +244,11 @@ func applyDataPatch(target, patch *conf.Data) {
func applyAdminPatch(target, patch *conf.AdminBackend) { func applyAdminPatch(target, patch *conf.AdminBackend) {
target.RouterPrefix = patch.RouterPrefix target.RouterPrefix = patch.RouterPrefix
if patch.System != nil { if patch.System != nil {
if target.System != nil {
patch.System.Addr = target.System.Addr
patch.System.IplimitCount = target.System.IplimitCount
patch.System.IplimitTime = target.System.IplimitTime
}
target.System = patch.System target.System = patch.System
} }
if patch.Jwt != nil { if patch.Jwt != nil {
@ -234,6 +261,9 @@ func applyAdminPatch(target, patch *conf.AdminBackend) {
target.Local = patch.Local target.Local = patch.Local
} }
if patch.Media != nil { if patch.Media != nil {
if patch.Media.ChunkDir == "" && target.Media != nil {
patch.Media.ChunkDir = target.Media.ChunkDir
}
target.Media = patch.Media target.Media = patch.Media
} }
if patch.Storage != nil { if patch.Storage != nil {

View File

@ -4,8 +4,10 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"kra/internal/conf" "kra/internal/conf"
@ -84,6 +86,45 @@ func mergeYAMLNode(dst, src *yaml.Node) {
dst.HeadComment, dst.LineComment, dst.FootComment = head, line, foot dst.HeadComment, dst.LineComment, dst.FootComment = head, line, foot
} }
func yamlMappingValue(node *yaml.Node, keys ...string) *yaml.Node {
if node == nil {
return nil
}
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
node = node.Content[0]
}
for _, key := range keys {
if node.Kind != yaml.MappingNode {
return nil
}
var next *yaml.Node
for i := 0; i+1 < len(node.Content); i += 2 {
if node.Content[i].Value == key {
next = node.Content[i+1]
break
}
}
if next == nil {
return nil
}
node = next
}
return node
}
func setServerHTTPPort(document *yaml.Node, port int32) error {
if port <= 0 {
return nil
}
host := "0.0.0.0"
if addr := yamlMappingValue(document, "server", "http", "addr"); addr != nil {
if currentHost, _, err := net.SplitHostPort(addr.Value); err == nil && currentHost != "" {
host = currentHost
}
}
return setYAMLMapping(document, "server", map[string]any{"http": map[string]any{"addr": net.JoinHostPort(host, strconv.Itoa(int(port)))}})
}
func (d *Data) persistConfig() error { func (d *Data) persistConfig() error {
dataConfig, adminConfig := d.runtime.Values() dataConfig, adminConfig := d.runtime.Values()
return d.persistConfigValues(dataConfig, adminConfig) return d.persistConfigValues(dataConfig, adminConfig)
@ -122,6 +163,11 @@ func (d *Data) persistConfigValuesLocked(dataConfig *conf.Data, adminConfig *con
if err = setYAMLMapping(&document, "admin", adminValue); err != nil { if err = setYAMLMapping(&document, "admin", adminValue); err != nil {
return err return err
} }
if adminConfig.System != nil {
if err = setServerHTTPPort(&document, adminConfig.System.Addr); err != nil {
return err
}
}
return writeConfigDocument(configPath, &document) return writeConfigDocument(configPath, &document)
} }

View File

@ -108,12 +108,6 @@ func (r *departmentRepo) DeleteDepartment(ctx context.Context, id uint) error {
if count > 0 { if count > 0 {
return errors.New("存在子部门,不允许删除") return errors.New("存在子部门,不允许删除")
} }
if err := r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("dept_id = ?", id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("该部门下存在用户,不允许删除")
}
if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Count(&count).Error; err != nil { if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Count(&count).Error; err != nil {
return err return err
} }
@ -135,7 +129,7 @@ func (r *departmentRepo) FindDepartment(ctx context.Context, id uint) (*biz.Depa
} }
func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) { func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) {
var pos []departmentPO var pos []departmentPO
db := r.data.gormDB.WithContext(ctx).Order("sort,id") db := r.data.gormDB.WithContext(ctx).Order("sort")
if name != "" { if name != "" {
db = db.Where("name LIKE ?", "%"+name+"%") db = db.Where("name LIKE ?", "%"+name+"%")
} }
@ -160,7 +154,7 @@ func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*b
n := nodes[po.ID] n := nodes[po.ID]
if p := nodes[po.ParentID]; p != nil { if p := nodes[po.ParentID]; p != nil {
p.Children = append(p.Children, n) p.Children = append(p.Children, n)
} else { } else if po.ParentID == 0 {
roots = append(roots, n) roots = append(roots, n)
} }
} }

View File

@ -111,19 +111,18 @@ func (r *dictionaryRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary
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 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 *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error { func (r *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { db := r.data.gormDB.WithContext(ctx)
var count int64 var dictionary dictionaryPO
if err := tx.Model(&dictionaryPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil { if err := db.First(&dictionary, id).Error; err != nil {
return err if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("请不要搞事")
} }
if count > 0 { return err
return errors.New("存在子字典不可删除") }
} if err := db.Delete(&dictionary).Error; err != nil {
if err := tx.Where("sys_dictionary_id = ?", id).Delete(&dictionaryDetailPO{}).Error; err != nil { return err
return err }
} return db.Where("sys_dictionary_id = ?", id).Delete(&dictionaryDetailPO{}).Error
return tx.Delete(&dictionaryPO{}, id).Error
})
} }
func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) { func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) {
var po dictionaryPO var po dictionaryPO

View File

@ -111,21 +111,7 @@ func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemp
}) })
} }
func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error { func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.gormDB.WithContext(ctx).Delete(&[]exportTemplatePO{}, "id IN ?", ids).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) { func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string) (*biz.ExportTemplate, error) {
var po exportTemplatePO var po exportTemplatePO
@ -173,31 +159,9 @@ func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q
if err := applyPagination(db, page, size, 0).Find(&pos).Error; err != nil { if err := applyPagination(db, page, size, 0).Find(&pos).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
templateIDs := make([]string, 0, len(pos))
for _, po := range pos {
templateIDs = append(templateIDs, po.TemplateID)
}
var conditions []exportConditionPO
var joins []exportJoinPO
if len(templateIDs) > 0 {
if err := r.data.gormDB.WithContext(ctx).Where("template_id IN ?", templateIDs).Find(&conditions).Error; err != nil {
return nil, 0, err
}
if err := r.data.gormDB.WithContext(ctx).Where("template_id IN ?", templateIDs).Find(&joins).Error; err != nil {
return nil, 0, err
}
}
conditionsByTemplate := make(map[string][]exportConditionPO, len(pos))
for _, condition := range conditions {
conditionsByTemplate[condition.TemplateID] = append(conditionsByTemplate[condition.TemplateID], condition)
}
joinsByTemplate := make(map[string][]exportJoinPO, len(pos))
for _, join := range joins {
joinsByTemplate[join.TemplateID] = append(joinsByTemplate[join.TemplateID], join)
}
out := make([]*biz.ExportTemplate, 0, len(pos)) out := make([]*biz.ExportTemplate, 0, len(pos))
for _, po := range pos { for _, po := range pos {
out = append(out, exportFromPO(po, conditionsByTemplate[po.TemplateID], joinsByTemplate[po.TemplateID])) out = append(out, exportFromPO(po, nil, nil))
} }
return out, total, nil return out, total, nil
} }

View File

@ -0,0 +1,545 @@
package data
import (
"encoding/json"
"strconv"
"strings"
"time"
"kra/internal/conf"
"google.golang.org/protobuf/types/known/durationpb"
)
func gvaDatabase(value *conf.Data_Database) map[string]any {
if value == nil {
return map[string]any{}
}
path := value.Host
if value.Driver == "sqlite" {
path = value.Path
}
return map[string]any{
"prefix": value.Prefix, "port": value.Port, "config": value.Config,
"db-name": value.Name, "username": value.User, "password": value.Password,
"path": path, "engine": value.Engine, "log-mode": value.LogMode,
"max-idle-conns": value.MaxIdleConns, "max-open-conns": value.MaxOpenConns,
"conn-max-lifetime": value.ConnMaxLifetime, "singular": value.Singular,
}
}
func gvaRedis(value *conf.Data_Redis) map[string]any {
if value == nil {
return map[string]any{}
}
return map[string]any{"name": value.Name, "addr": value.Addr, "password": value.Password, "db": value.Db, "useCluster": value.UseCluster, "clusterAddrs": value.ClusterAddrs}
}
func gvaObjectStore(value *conf.AdminBackend_ObjectStore) map[string]any {
if value == nil {
return map[string]any{}
}
return map[string]any{
"endpoint": value.Endpoint, "region": value.Region, "bucket": value.Bucket,
"access-key": value.AccessKey, "secret-key": value.SecretKey, "base-url": value.BaseUrl,
"path-prefix": value.PathPrefix, "use-ssl": value.UseSsl,
"s3-force-path-style": value.ForcePathStyle, "account-id": value.AccountId,
}
}
func gvaObjectStores(storage *conf.AdminBackend_Storage) map[string]map[string]any {
if storage == nil {
storage = &conf.AdminBackend_Storage{}
}
aliyun, huawei := storage.AliyunOss, storage.HuaweiObs
tencent, aws := storage.TencentCos, storage.AwsS3
cloudflare, minio := storage.CloudflareR2, storage.Minio
if aliyun == nil {
aliyun = &conf.AdminBackend_ObjectStore{}
}
if huawei == nil {
huawei = &conf.AdminBackend_ObjectStore{}
}
if tencent == nil {
tencent = &conf.AdminBackend_ObjectStore{}
}
if aws == nil {
aws = &conf.AdminBackend_ObjectStore{}
}
if cloudflare == nil {
cloudflare = &conf.AdminBackend_ObjectStore{}
}
if minio == nil {
minio = &conf.AdminBackend_ObjectStore{}
}
return map[string]map[string]any{
"aliyun-oss": {"endpoint": aliyun.Endpoint, "access-key-id": aliyun.AccessKey, "access-key-secret": aliyun.SecretKey, "bucket-name": aliyun.Bucket, "bucket-url": aliyun.BaseUrl, "base-path": aliyun.PathPrefix},
"hua-wei-obs": {"path": huawei.PathPrefix, "bucket": huawei.Bucket, "endpoint": huawei.Endpoint, "access-key": huawei.AccessKey, "secret-key": huawei.SecretKey},
"tencent-cos": {"bucket": tencent.Bucket, "region": tencent.Region, "secret-id": tencent.AccessKey, "secret-key": tencent.SecretKey, "base-url": tencent.BaseUrl, "path-prefix": tencent.PathPrefix},
"aws-s3": {"bucket": aws.Bucket, "region": aws.Region, "endpoint": aws.Endpoint, "secret-id": aws.AccessKey, "secret-key": aws.SecretKey, "base-url": aws.BaseUrl, "path-prefix": aws.PathPrefix, "s3-force-path-style": aws.ForcePathStyle, "disable-ssl": !aws.UseSsl},
"cloudflare-r2": {"bucket": cloudflare.Bucket, "base-url": cloudflare.BaseUrl, "path": cloudflare.PathPrefix, "account-id": cloudflare.AccountId, "access-key-id": cloudflare.AccessKey, "secret-access-key": cloudflare.SecretKey},
"minio": {"endpoint": minio.Endpoint, "access-key-id": minio.AccessKey, "access-key-secret": minio.SecretKey, "bucket-name": minio.Bucket, "use-ssl": minio.UseSsl, "base-path": minio.PathPrefix, "bucket-url": minio.BaseUrl},
}
}
func gvaConfiguration(data *conf.Data, admin *conf.AdminBackend) map[string]any {
result := map[string]any{}
if data == nil {
data = &conf.Data{}
}
if admin == nil {
admin = &conf.AdminBackend{}
}
jwt := admin.Jwt
if jwt == nil {
jwt = &conf.AdminBackend_JWT{}
}
result["jwt"] = map[string]any{"signing-key": jwt.SigningKey, "expires-time": durationString(jwt.ExpiresTime), "buffer-time": durationString(jwt.BufferTime), "issuer": jwt.Issuer}
zap := admin.Zap
if zap == nil {
zap = &conf.AdminBackend_Zap{}
}
result["zap"] = map[string]any{
"level": zap.Level, "prefix": zap.Prefix, "format": zap.Format, "director": zap.Director,
"encode-level": zap.EncodeLevel, "stacktrace-key": zap.StacktraceKey, "show-line": zap.ShowLine,
"log-in-console": zap.LogInConsole, "retention-day": zap.RetentionDay,
"access-req-body": zap.AccessReqBody, "access-resp-data": zap.AccessRespData,
"access-req-headers": zap.AccessReqHeaders, "access-log-max-bytes": zap.AccessLogMaxBytes,
"file-only-modules": zap.FileOnlyModules,
}
result["redis"] = gvaRedis(data.Redis)
redisList := make([]map[string]any, 0, len(data.RedisList))
for _, item := range data.RedisList {
redisList = append(redisList, gvaRedis(item))
}
result["redis-list"] = redisList
mongo := data.Mongo
if mongo == nil {
mongo = &conf.Data_Mongo{}
}
hosts := make([]map[string]any, 0, len(mongo.Hosts))
for _, host := range mongo.Hosts {
if host != nil {
hosts = append(hosts, map[string]any{"host": host.Host, "port": host.Port})
}
}
result["mongo"] = map[string]any{"coll": mongo.Coll, "options": mongo.Options, "database": mongo.Database, "username": mongo.Username, "password": mongo.Password, "auth-source": mongo.AuthSource, "min-pool-size": mongo.MinPoolSize, "max-pool-size": mongo.MaxPoolSize, "socket-timeout-ms": mongo.SocketTimeoutMs, "connect-timeout-ms": mongo.ConnectTimeoutMs, "is-zap": mongo.IsZap, "hosts": hosts}
email := admin.Email
if email == nil {
email = &conf.AdminBackend_Email{}
}
result["email"] = map[string]any{"to": email.To, "from": email.From, "host": email.Host, "secret": email.Secret, "nickname": email.Nickname, "port": email.Port, "is-ssl": email.IsSsl, "is-loginauth": email.IsLoginAuth}
system := admin.System
if system == nil {
system = &conf.AdminBackend_System{}
}
storageType := "local"
if admin.Storage != nil && admin.Storage.Type != "" {
storageType = admin.Storage.Type
}
driver := ""
if data.Database != nil {
driver = data.Database.Driver
}
result["system"] = map[string]any{"db-type": driver, "oss-type": storageType, "router-prefix": admin.RouterPrefix, "addr": system.Addr, "iplimit-count": system.IplimitCount, "iplimit-time": system.IplimitTime, "use-multipoint": system.UseMultipoint, "use-redis": system.UseRedis, "use-mongo": system.UseMongo, "use-strict-auth": system.UseStrictAuth, "disable-auto-migrate": system.DisableAutoMigrate}
for _, name := range []string{"mysql", "mssql", "pgsql", "oracle", "sqlite"} {
result[name] = map[string]any{}
}
if driver != "" {
result[driver] = gvaDatabase(data.Database)
}
dbList := make([]map[string]any, 0, len(data.DatabaseList))
for _, item := range data.DatabaseList {
row := gvaDatabase(item)
if item != nil {
row["type"], row["alias-name"], row["disable"] = item.Driver, item.AliasName, item.Disable
}
dbList = append(dbList, row)
}
result["db-list"] = dbList
local := admin.Local
if local == nil {
local = &conf.AdminBackend_Local{}
}
result["local"] = map[string]any{"path": local.PathPrefix, "store-path": local.StorePath}
media := admin.Media
if media == nil {
media = &conf.AdminBackend_Media{}
}
result["media"] = map[string]any{"session-ttl": media.SessionTtl, "max-file-size": media.MaxFileSize, "chunk-dir": media.ChunkDir}
if admin.Storage == nil {
admin.Storage = &conf.AdminBackend_Storage{}
}
qiniu := admin.Storage.Qiniu
if qiniu == nil {
qiniu = &conf.AdminBackend_Qiniu{}
}
result["qiniu"] = map[string]any{"zone": qiniu.Zone, "bucket": qiniu.Bucket, "img-path": qiniu.BaseUrl, "access-key": qiniu.AccessKey, "secret-key": qiniu.SecretKey, "use-https": qiniu.UseHttps, "use-cdn-domains": qiniu.UseCdnDomains}
for key, value := range gvaObjectStores(admin.Storage) {
result[key] = value
}
disks := make([]map[string]any, 0, len(admin.DiskList))
for _, disk := range admin.DiskList {
if disk != nil {
disks = append(disks, map[string]any{"mount-point": disk.MountPoint})
}
}
result["disk-list"] = disks
cors := admin.Cors
if cors == nil {
cors = &conf.AdminBackend_CORS{}
}
whitelist := make([]map[string]any, 0, len(cors.Whitelist))
for _, rule := range cors.Whitelist {
if rule != nil {
whitelist = append(whitelist, map[string]any{"allow-origin": rule.AllowOrigin, "allow-methods": rule.AllowMethods, "allow-headers": rule.AllowHeaders, "expose-headers": rule.ExposeHeaders, "allow-credentials": rule.AllowCredentials})
}
}
result["cors"] = map[string]any{"mode": cors.Mode, "whitelist": whitelist}
result["app"] = map[string]any{"node": admin.GetApp().GetNode(), "app-id": admin.GetApp().GetAppId(), "env": admin.GetApp().GetEnv()}
return result
}
func objectMap(value any) map[string]any {
result, _ := value.(map[string]any)
return result
}
func stringValue(values map[string]any, key string) string {
value, _ := values[key].(string)
return value
}
func boolValue(values map[string]any, key string, fallback bool) bool {
value, ok := values[key].(bool)
if !ok {
return fallback
}
return value
}
func int64Value(values map[string]any, key string, fallback int64) int64 {
value, ok := values[key].(float64)
if !ok {
return fallback
}
return int64(value)
}
func stringSliceValue(values map[string]any, key string, fallback []string) []string {
items, ok := values[key].([]any)
if !ok {
return fallback
}
result := make([]string, 0, len(items))
for _, item := range items {
if value, ok := item.(string); ok {
result = append(result, value)
}
}
return result
}
func applyGVARedis(target *conf.Data_Redis, values map[string]any) {
if target == nil || values == nil {
return
}
target.Name, target.Addr = stringValue(values, "name"), stringValue(values, "addr")
if password := stringValue(values, "password"); password != "" && password != "******" {
target.Password = password
}
target.Db = int32(int64Value(values, "db", int64(target.Db)))
target.UseCluster = boolValue(values, "useCluster", target.UseCluster)
target.ClusterAddrs = stringSliceValue(values, "clusterAddrs", target.ClusterAddrs)
}
func durationValue(raw string, fallback *durationpb.Duration) *durationpb.Duration {
if raw == "" {
return fallback
}
value, err := parseGVADuration(raw)
if err != nil {
return fallback
}
return durationpb.New(value)
}
func parseGVADuration(raw string) (time.Duration, error) {
raw = strings.TrimSpace(raw)
if value, err := time.ParseDuration(raw); err == nil {
return value, nil
}
if index := strings.Index(raw, "d"); index >= 0 {
days, _ := strconv.Atoi(raw[:index])
value := time.Duration(days) * 24 * time.Hour
remainder, err := time.ParseDuration(raw[index+1:])
if err != nil {
return value, nil
}
return value + remainder, nil
}
nanoseconds, err := strconv.ParseInt(raw, 10, 64)
return time.Duration(nanoseconds), err
}
func applyGVADatabase(target *conf.Data_Database, values map[string]any, driver string) {
if target == nil || values == nil {
return
}
target.Driver = driver
target.Prefix, target.Port, target.Config = stringValue(values, "prefix"), stringValue(values, "port"), stringValue(values, "config")
target.Name, target.User = stringValue(values, "db-name"), stringValue(values, "username")
if password := stringValue(values, "password"); password != "" && password != "******" {
target.Password = password
}
if driver == "sqlite" {
target.Path = stringValue(values, "path")
} else {
target.Host = stringValue(values, "path")
}
target.Engine, target.LogMode = stringValue(values, "engine"), stringValue(values, "log-mode")
target.MaxIdleConns = int32(int64Value(values, "max-idle-conns", int64(target.MaxIdleConns)))
target.MaxOpenConns = int32(int64Value(values, "max-open-conns", int64(target.MaxOpenConns)))
target.ConnMaxLifetime = int32(int64Value(values, "conn-max-lifetime", int64(target.ConnMaxLifetime)))
target.Singular = boolValue(values, "singular", target.Singular)
}
func applyGVAObjectStore(target *conf.AdminBackend_ObjectStore, values map[string]any) {
if target == nil || values == nil {
return
}
target.Endpoint, target.Region, target.Bucket = stringValue(values, "endpoint"), stringValue(values, "region"), stringValue(values, "bucket")
if target.Bucket == "" {
target.Bucket = stringValue(values, "bucket-name")
}
target.AccessKey = stringValue(values, "access-key")
if target.AccessKey == "" {
target.AccessKey = stringValue(values, "access-key-id")
}
if target.AccessKey == "" {
target.AccessKey = stringValue(values, "secret-id")
}
secret := stringValue(values, "secret-key")
if secret == "" {
secret = stringValue(values, "access-key-secret")
}
if secret == "" {
secret = stringValue(values, "secret-access-key")
}
if secret != "" && secret != "******" {
target.SecretKey = secret
}
target.BaseUrl = stringValue(values, "base-url")
if target.BaseUrl == "" {
target.BaseUrl = stringValue(values, "bucket-url")
}
target.PathPrefix = stringValue(values, "path-prefix")
if target.PathPrefix == "" {
target.PathPrefix = stringValue(values, "base-path")
}
if target.PathPrefix == "" {
target.PathPrefix = stringValue(values, "path")
}
target.UseSsl = boolValue(values, "use-ssl", !boolValue(values, "disable-ssl", !target.UseSsl))
target.ForcePathStyle = boolValue(values, "s3-force-path-style", target.ForcePathStyle)
target.AccountId = stringValue(values, "account-id")
}
func applyGVAConfiguration(raw json.RawMessage, data *conf.Data, admin *conf.AdminBackend) error {
var values map[string]any
if err := json.Unmarshal(raw, &values); err != nil {
return err
}
if _, isGVA := values["system"]; !isGVA {
return nil
}
if data.Database == nil {
data.Database = &conf.Data_Database{}
}
if admin.System == nil {
admin.System = &conf.AdminBackend_System{}
}
if admin.Storage == nil {
admin.Storage = &conf.AdminBackend_Storage{}
}
system := objectMap(values["system"])
driver := stringValue(system, "db-type")
if driver == "" {
driver = data.Database.Driver
}
applyGVADatabase(data.Database, objectMap(values[driver]), driver)
admin.Storage.Type = stringValue(system, "oss-type")
admin.RouterPrefix = stringValue(system, "router-prefix")
admin.System.UseMultipoint = boolValue(system, "use-multipoint", admin.System.UseMultipoint)
admin.System.UseRedis = boolValue(system, "use-redis", admin.System.UseRedis)
admin.System.UseMongo = boolValue(system, "use-mongo", admin.System.UseMongo)
admin.System.UseStrictAuth = boolValue(system, "use-strict-auth", admin.System.UseStrictAuth)
admin.System.DisableAutoMigrate = boolValue(system, "disable-auto-migrate", admin.System.DisableAutoMigrate)
admin.System.Addr = int32(int64Value(system, "addr", int64(admin.System.Addr)))
admin.System.IplimitCount = int32(int64Value(system, "iplimit-count", int64(admin.System.IplimitCount)))
admin.System.IplimitTime = int32(int64Value(system, "iplimit-time", int64(admin.System.IplimitTime)))
if admin.Jwt == nil {
admin.Jwt = &conf.AdminBackend_JWT{}
}
jwt := objectMap(values["jwt"])
if secret := stringValue(jwt, "signing-key"); secret != "" && secret != "******" {
admin.Jwt.SigningKey = secret
}
admin.Jwt.ExpiresTime = durationValue(stringValue(jwt, "expires-time"), admin.Jwt.ExpiresTime)
admin.Jwt.BufferTime = durationValue(stringValue(jwt, "buffer-time"), admin.Jwt.BufferTime)
admin.Jwt.Issuer = stringValue(jwt, "issuer")
if admin.Zap == nil {
admin.Zap = &conf.AdminBackend_Zap{}
}
zap := objectMap(values["zap"])
admin.Zap.Level, admin.Zap.Prefix, admin.Zap.Format, admin.Zap.Director = stringValue(zap, "level"), stringValue(zap, "prefix"), stringValue(zap, "format"), stringValue(zap, "director")
admin.Zap.EncodeLevel, admin.Zap.StacktraceKey = stringValue(zap, "encode-level"), stringValue(zap, "stacktrace-key")
admin.Zap.ShowLine, admin.Zap.LogInConsole = boolValue(zap, "show-line", admin.Zap.ShowLine), boolValue(zap, "log-in-console", admin.Zap.LogInConsole)
admin.Zap.RetentionDay = int32(int64Value(zap, "retention-day", int64(admin.Zap.RetentionDay)))
admin.Zap.AccessReqBody = boolValue(zap, "access-req-body", admin.Zap.AccessReqBody)
admin.Zap.AccessRespData = boolValue(zap, "access-resp-data", admin.Zap.AccessRespData)
admin.Zap.AccessReqHeaders = boolValue(zap, "access-req-headers", admin.Zap.AccessReqHeaders)
admin.Zap.AccessLogMaxBytes = int32(int64Value(zap, "access-log-max-bytes", int64(admin.Zap.AccessLogMaxBytes)))
if list, ok := zap["file-only-modules"].([]any); ok {
admin.Zap.FileOnlyModules = admin.Zap.FileOnlyModules[:0]
for _, item := range list {
if value, ok := item.(string); ok {
admin.Zap.FileOnlyModules = append(admin.Zap.FileOnlyModules, value)
}
}
}
if data.Redis == nil {
data.Redis = &conf.Data_Redis{}
}
applyGVARedis(data.Redis, objectMap(values["redis"]))
if items, ok := values["redis-list"].([]any); ok {
current := make(map[string]*conf.Data_Redis, len(data.RedisList))
for _, item := range data.RedisList {
if item != nil {
current[item.Name] = item
}
}
data.RedisList = make([]*conf.Data_Redis, 0, len(items))
for _, item := range items {
values := objectMap(item)
target := current[stringValue(values, "name")]
if target == nil {
target = &conf.Data_Redis{}
}
applyGVARedis(target, values)
data.RedisList = append(data.RedisList, target)
}
}
if mongo := objectMap(values["mongo"]); mongo != nil {
if data.Mongo == nil {
data.Mongo = &conf.Data_Mongo{}
}
data.Mongo.Coll, data.Mongo.Options, data.Mongo.Database = stringValue(mongo, "coll"), stringValue(mongo, "options"), stringValue(mongo, "database")
data.Mongo.Username, data.Mongo.AuthSource = stringValue(mongo, "username"), stringValue(mongo, "auth-source")
if password := stringValue(mongo, "password"); password != "" && password != "******" {
data.Mongo.Password = password
}
data.Mongo.MinPoolSize = uint64(int64Value(mongo, "min-pool-size", int64(data.Mongo.MinPoolSize)))
data.Mongo.MaxPoolSize = uint64(int64Value(mongo, "max-pool-size", int64(data.Mongo.MaxPoolSize)))
data.Mongo.SocketTimeoutMs = int64Value(mongo, "socket-timeout-ms", data.Mongo.SocketTimeoutMs)
data.Mongo.ConnectTimeoutMs = int64Value(mongo, "connect-timeout-ms", data.Mongo.ConnectTimeoutMs)
data.Mongo.IsZap = boolValue(mongo, "is-zap", data.Mongo.IsZap)
if hosts, ok := mongo["hosts"].([]any); ok {
data.Mongo.Hosts = make([]*conf.Data_MongoHost, 0, len(hosts))
for _, item := range hosts {
host := objectMap(item)
data.Mongo.Hosts = append(data.Mongo.Hosts, &conf.Data_MongoHost{Host: stringValue(host, "host"), Port: stringValue(host, "port")})
}
}
}
if items, ok := values["db-list"].([]any); ok {
current := make(map[string]*conf.Data_Database, len(data.DatabaseList))
for _, item := range data.DatabaseList {
if item != nil {
current[item.AliasName] = item
}
}
data.DatabaseList = make([]*conf.Data_Database, 0, len(items))
for _, item := range items {
row := objectMap(item)
alias := stringValue(row, "alias-name")
target := current[alias]
if target == nil {
target = &conf.Data_Database{}
}
applyGVADatabase(target, row, stringValue(row, "type"))
target.AliasName, target.Disable = alias, boolValue(row, "disable", target.Disable)
data.DatabaseList = append(data.DatabaseList, target)
}
}
if admin.Email == nil {
admin.Email = &conf.AdminBackend_Email{}
}
email := objectMap(values["email"])
admin.Email.To, admin.Email.From, admin.Email.Host = stringValue(email, "to"), stringValue(email, "from"), stringValue(email, "host")
admin.Email.Nickname = stringValue(email, "nickname")
admin.Email.Port = int32(int64Value(email, "port", int64(admin.Email.Port)))
admin.Email.IsSsl, admin.Email.IsLoginAuth = boolValue(email, "is-ssl", admin.Email.IsSsl), boolValue(email, "is-loginauth", admin.Email.IsLoginAuth)
if secret := stringValue(email, "secret"); secret != "" && secret != "******" {
admin.Email.Secret = secret
}
if admin.Local == nil {
admin.Local = &conf.AdminBackend_Local{}
}
local := objectMap(values["local"])
admin.Local.PathPrefix, admin.Local.StorePath = stringValue(local, "path"), stringValue(local, "store-path")
if admin.Media == nil {
admin.Media = &conf.AdminBackend_Media{}
}
media := objectMap(values["media"])
admin.Media.SessionTtl = int32(int64Value(media, "session-ttl", int64(admin.Media.SessionTtl)))
admin.Media.MaxFileSize = int64Value(media, "max-file-size", admin.Media.MaxFileSize)
admin.Media.ChunkDir = stringValue(media, "chunk-dir")
if admin.Storage.Qiniu == nil {
admin.Storage.Qiniu = &conf.AdminBackend_Qiniu{}
}
qiniu := objectMap(values["qiniu"])
admin.Storage.Qiniu.Zone, admin.Storage.Qiniu.Bucket, admin.Storage.Qiniu.BaseUrl = stringValue(qiniu, "zone"), stringValue(qiniu, "bucket"), stringValue(qiniu, "img-path")
admin.Storage.Qiniu.AccessKey = stringValue(qiniu, "access-key")
if secret := stringValue(qiniu, "secret-key"); secret != "" && secret != "******" {
admin.Storage.Qiniu.SecretKey = secret
}
admin.Storage.Qiniu.UseHttps, admin.Storage.Qiniu.UseCdnDomains = boolValue(qiniu, "use-https", admin.Storage.Qiniu.UseHttps), boolValue(qiniu, "use-cdn-domains", admin.Storage.Qiniu.UseCdnDomains)
stores := []struct {
key string
target **conf.AdminBackend_ObjectStore
}{{"aliyun-oss", &admin.Storage.AliyunOss}, {"hua-wei-obs", &admin.Storage.HuaweiObs}, {"tencent-cos", &admin.Storage.TencentCos}, {"aws-s3", &admin.Storage.AwsS3}, {"cloudflare-r2", &admin.Storage.CloudflareR2}, {"minio", &admin.Storage.Minio}}
for _, store := range stores {
if *store.target == nil {
*store.target = &conf.AdminBackend_ObjectStore{}
}
applyGVAObjectStore(*store.target, objectMap(values[store.key]))
}
if disks, ok := values["disk-list"].([]any); ok {
admin.DiskList = make([]*conf.AdminBackend_Disk, 0, len(disks))
for _, item := range disks {
disk := objectMap(item)
admin.DiskList = append(admin.DiskList, &conf.AdminBackend_Disk{MountPoint: stringValue(disk, "mount-point")})
}
}
if cors := objectMap(values["cors"]); cors != nil {
if admin.Cors == nil {
admin.Cors = &conf.AdminBackend_CORS{}
}
admin.Cors.Mode = stringValue(cors, "mode")
if rules, ok := cors["whitelist"].([]any); ok {
admin.Cors.Whitelist = make([]*conf.AdminBackend_CORSRule, 0, len(rules))
for _, item := range rules {
rule := objectMap(item)
admin.Cors.Whitelist = append(admin.Cors.Whitelist, &conf.AdminBackend_CORSRule{AllowOrigin: stringValue(rule, "allow-origin"), AllowMethods: stringValue(rule, "allow-methods"), AllowHeaders: stringValue(rule, "allow-headers"), ExposeHeaders: stringValue(rule, "expose-headers"), AllowCredentials: boolValue(rule, "allow-credentials", false)})
}
}
}
if app := objectMap(values["app"]); app != nil {
if admin.App == nil {
admin.App = &conf.AdminBackend_App{}
}
admin.App.Node, admin.App.AppId, admin.App.Env = stringValue(app, "node"), stringValue(app, "app-id"), stringValue(app, "env")
}
return nil
}

View File

@ -4,8 +4,11 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"fmt"
"io"
"io/fs" "io/fs"
"os" "os"
pathpkg "path"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@ -14,86 +17,320 @@ import (
"kra/internal/biz" "kra/internal/biz"
) )
func (r *logFileRepo) logRoot() (string, error) { const (
defaultLogChunkLines = 500
maxLogChunkBytes = 2 * 1024 * 1024
logReadBlockSize = 64 * 1024
)
func (r *logFileRepo) configuredLogRoot() (root string, exists bool, err error) {
admin := r.data.runtime.Admin() admin := r.data.runtime.Admin()
if admin == nil || admin.Zap == nil || strings.TrimSpace(admin.Zap.Director) == "" { if admin == nil || admin.Zap == nil || strings.TrimSpace(admin.Zap.Director) == "" {
return "", biz.ErrLogRootUnavailable return "", false, biz.ErrLogRootUnavailable
} }
root, err := filepath.Abs(admin.Zap.Director) root, err = filepath.Abs(admin.Zap.Director)
if err != nil { if err != nil {
return "", errors.Join(biz.ErrLogRootUnavailable, err) return "", false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
} }
info, err := os.Stat(root) info, err := os.Stat(root)
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
return root, nil return root, false, nil
} }
if err != nil { if err != nil {
return "", errors.Join(biz.ErrLogRootUnavailable, err) return "", false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
} }
if !info.IsDir() { if !info.IsDir() {
return "", biz.ErrLogRootUnavailable return "", false, biz.ErrLogRootUnavailable
} }
return root, nil return root, true, nil
} }
func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
if parsed, err := time.Parse("2006-01", month); err != nil || parsed.Format("2006-01") != month { func (r *logFileRepo) openConfiguredLogRoot() (root *os.Root, exists bool, err error) {
return nil, biz.ErrInvalidLogMonth rootPath, exists, err := r.configuredLogRoot()
if err != nil || !exists {
return nil, exists, err
} }
root, err := r.logRoot() root, err = os.OpenRoot(rootPath)
if err != nil {
return nil, false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
return root, true, nil
}
func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
if err := validateLogMonth(month); err != nil {
return nil, err
}
logRoot, exists, err := r.openConfiguredLogRoot()
if err != nil { if err != nil {
return nil, err return nil, err
} }
entries, err := os.ReadDir(root) if !exists {
if errors.Is(err, fs.ErrNotExist) {
return []biz.LogDate{}, nil return []biz.LogDate{}, nil
} }
defer logRoot.Close()
entries, err := fs.ReadDir(logRoot.FS(), ".")
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
} }
out := []biz.LogDate{} result := make([]biz.LogDate, 0)
for _, entry := range entries { for _, entry := range entries {
if ctx.Err() != nil { if err = ctx.Err(); err != nil {
return nil, ctx.Err() return nil, err
} }
if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() || !strings.HasPrefix(entry.Name(), month+"-") { if entry.Type()&os.ModeSymlink != 0 || !entry.IsDir() || !strings.HasPrefix(entry.Name(), month+"-") {
continue continue
} }
if parsed, parseErr := time.Parse("2006-01-02", entry.Name()); parseErr != nil || parsed.Format("2006-01-02") != entry.Name() { if validateLogDate(entry.Name()) != nil {
continue continue
} }
count := 0 count, countErr := countLogFiles(ctx, logRoot, entry.Name())
_ = filepath.WalkDir(filepath.Join(root, entry.Name()), func(_ string, item fs.DirEntry, walkErr error) error { if countErr != nil {
if walkErr == nil && !item.IsDir() && strings.EqualFold(filepath.Ext(item.Name()), ".log") { return nil, countErr
count++ }
}
return nil
})
if count > 0 { if count > 0 {
out = append(out, biz.LogDate{Date: entry.Name(), FileCount: count}) result = append(result, biz.LogDate{Date: entry.Name(), FileCount: count})
} }
} }
sort.Slice(out, func(i, j int) bool { return out[i].Date < out[j].Date }) sort.Slice(result, func(i, j int) bool { return result[i].Date < result[j].Date })
return out, nil return result, nil
} }
func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) { func (r *logFileRepo) 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 { if err := validateLogDate(date); err != nil {
return nil, biz.ErrInvalidLogDate return nil, err
} }
root, err := r.logRoot() logRoot, exists, err := r.openConfiguredLogRoot()
if err != nil { if err != nil {
return nil, err return nil, err
} }
dateRoot := filepath.Join(root, date) if !exists {
out := []biz.LogFile{} return []biz.LogFile{}, nil
err = filepath.WalkDir(dateRoot, func(path string, entry fs.DirEntry, walkErr error) error { }
defer logRoot.Close()
info, err := logRoot.Lstat(date)
if errors.Is(err, fs.ErrNotExist) {
return []biz.LogFile{}, nil
}
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return []biz.LogFile{}, nil
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
defer dateRoot.Close()
result := make([]biz.LogFile, 0)
err = fs.WalkDir(dateRoot.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil { if walkErr != nil {
if errors.Is(walkErr, fs.ErrNotExist) {
return nil
}
return walkErr return walkErr
} }
if ctx.Err() != nil { if err := ctx.Err(); err != nil {
return ctx.Err() return err
}
if path == "." {
return nil
}
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
}
fileInfo, infoErr := entry.Info()
if infoErr != nil {
return infoErr
}
if fileInfo.Mode().IsRegular() {
result = append(result, biz.LogFile{Path: path, Name: entry.Name(), Size: fileInfo.Size(), ModifiedAt: fileInfo.ModTime()})
}
return nil
})
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
sort.Slice(result, func(i, j int) bool { return result[i].Path < result[j].Path })
return result, nil
}
func (r *logFileRepo) LogContent(ctx context.Context, date, apiPath string, cursor *int64) (*biz.LogContent, error) {
result := &biz.LogContent{Date: date, Path: apiPath}
if err := ctx.Err(); err != nil {
return nil, err
}
if cursor != nil && *cursor < 0 {
return nil, biz.ErrInvalidLogPath
}
file, info, err := r.openValidatedLogFile(date, apiPath)
if err != nil {
return nil, err
}
defer file.Close()
end := info.Size()
if cursor != nil && *cursor <= end {
end = *cursor
}
start, limitedByBytes, err := findLogChunkStart(file, end)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
data, err := readLogRange(file, start, end)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
if int64(len(data)) < end-start {
currentInfo, statErr := file.Stat()
if statErr != nil || currentInfo.Size() < start {
if statErr != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, statErr)
}
return nil, biz.ErrLogFileUnreadable
}
info = currentInfo
}
result.Content = string(data)
result.LineCount = countLogicalLines(data)
result.NextCursor = start
result.HasMore = start > 0
result.LimitedByBytes = limitedByBytes
result.Size = info.Size()
result.ModifiedAt = info.ModTime()
return result, nil
}
func (r *logFileRepo) openValidatedLogFile(date, apiPath string) (file *os.File, info os.FileInfo, err error) {
if err = validateLogDate(date); err != nil {
return nil, nil, err
}
segments, err := validateLogAPIPath(apiPath)
if err != nil {
return nil, nil, err
}
logRoot, exists, err := r.openConfiguredLogRoot()
if err != nil {
return nil, nil, err
}
if !exists {
return nil, nil, biz.ErrLogFileNotFound
}
defer logRoot.Close()
dateInfo, err := logRoot.Lstat(date)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
}
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
if dateInfo.Mode()&os.ModeSymlink != 0 || !dateInfo.IsDir() {
return nil, nil, biz.ErrInvalidLogPath
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
defer dateRoot.Close()
relativePath := ""
var validatedInfo os.FileInfo
for index, segment := range segments {
relativePath = filepath.Join(relativePath, segment)
validatedInfo, err = dateRoot.Lstat(relativePath)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
}
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
if validatedInfo.Mode()&os.ModeSymlink != 0 {
return nil, nil, biz.ErrInvalidLogPath
}
if index < len(segments)-1 && !validatedInfo.IsDir() {
return nil, nil, biz.ErrInvalidLogPath
}
}
if validatedInfo == nil || !validatedInfo.Mode().IsRegular() {
return nil, nil, biz.ErrInvalidLogPath
}
file, err = dateRoot.Open(relativePath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
}
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
}
openedInfo, statErr := file.Stat()
if statErr != nil || !openedInfo.Mode().IsRegular() || !os.SameFile(validatedInfo, openedInfo) {
file.Close()
if statErr != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, statErr)
}
return nil, nil, biz.ErrInvalidLogPath
}
return file, openedInfo, nil
}
func validateLogMonth(month string) error {
parsed, err := time.Parse("2006-01", month)
if err != nil || parsed.Format("2006-01") != month {
return biz.ErrInvalidLogMonth
}
return nil
}
func validateLogDate(date string) error {
parsed, err := time.Parse("2006-01-02", date)
if err != nil || parsed.Format("2006-01-02") != date {
return biz.ErrInvalidLogDate
}
return nil
}
func validateLogAPIPath(apiPath string) ([]string, error) {
if apiPath == "" || strings.Contains(apiPath, "\\") || strings.Contains(apiPath, ":") || pathpkg.IsAbs(apiPath) {
return nil, biz.ErrInvalidLogPath
}
segments := strings.Split(apiPath, "/")
for _, segment := range segments {
if segment == "" || segment == "." || segment == ".." {
return nil, biz.ErrInvalidLogPath
}
}
if !strings.EqualFold(pathpkg.Ext(apiPath), ".log") {
return nil, biz.ErrInvalidLogPath
}
return segments, nil
}
func countLogFiles(ctx context.Context, logRoot *os.Root, date string) (count int, err error) {
dateInfo, err := logRoot.Lstat(date)
if err != nil || dateInfo.Mode()&os.ModeSymlink != 0 || !dateInfo.IsDir() {
return 0, err
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return 0, err
}
defer dateRoot.Close()
err = fs.WalkDir(dateRoot.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if err := ctx.Err(); err != nil {
return err
}
if path == "." {
return nil
} }
if entry.Type()&os.ModeSymlink != 0 { if entry.Type()&os.ModeSymlink != 0 {
if entry.IsDir() { if entry.IsDir() {
@ -108,70 +345,80 @@ func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile,
if infoErr != nil { if infoErr != nil {
return infoErr return infoErr
} }
relative, _ := filepath.Rel(dateRoot, path) if info.Mode().IsRegular() {
out = append(out, biz.LogFile{Path: filepath.ToSlash(relative), Name: entry.Name(), Size: info.Size(), ModifiedAt: info.ModTime()}) count++
}
return nil return nil
}) })
sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
return out, err
}
func (r *logFileRepo) LogContent(ctx context.Context, date, path string, cursor *int64) (*biz.LogContent, error) {
if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date {
return nil, biz.ErrInvalidLogDate
}
root, err := r.logRoot()
if err != nil { if err != nil {
return 0, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
}
return count, nil
}
func readLogRange(file *os.File, start, end int64) ([]byte, error) {
if end <= start {
return []byte{}, nil
}
data := make([]byte, int(end-start))
n, err := file.ReadAt(data, start)
if err != nil && !errors.Is(err, io.EOF) {
return nil, err return nil, err
} }
dateRoot := filepath.Join(root, date) return data[:n], nil
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, biz.ErrInvalidLogPath func findLogChunkStart(file *os.File, end int64) (start int64, limitedByBytes bool, err error) {
} if end <= 0 {
info, err := os.Lstat(target) return 0, false, nil
if errors.Is(err, fs.ErrNotExist) { }
return nil, biz.ErrLogFileNotFound lastByte := []byte{0}
} if _, err = file.ReadAt(lastByte, end-1); err != nil {
if err != nil { return 0, false, err
return nil, errors.Join(biz.ErrLogFileUnreadable, err) }
} targetNewlines := defaultLogChunkLines
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { if lastByte[0] == '\n' {
return nil, biz.ErrInvalidLogPath targetNewlines++
} }
end := info.Size() position, scanned, newlines := end, int64(0), 0
if cursor != nil && *cursor >= 0 && *cursor < end { for position > 0 && scanned < maxLogChunkBytes {
end = *cursor readSize := int64(logReadBlockSize)
} if readSize > position {
start := end - int64(2*1024*1024) readSize = position
limited := start > 0 }
if start < 0 { if remaining := int64(maxLogChunkBytes) - scanned; readSize > remaining {
start = 0 readSize = remaining
} }
file, err := os.Open(target) blockStart := position - readSize
if err != nil { block := make([]byte, int(readSize))
return nil, errors.Join(biz.ErrLogFileUnreadable, err) n, readErr := file.ReadAt(block, blockStart)
} if readErr != nil && !errors.Is(readErr, io.EOF) {
defer file.Close() return 0, false, readErr
data := make([]byte, end-start) }
n, err := file.ReadAt(data, start) for index := n - 1; index >= 0; index-- {
if err != nil && n == 0 { if block[index] == '\n' {
return nil, errors.Join(biz.ErrLogFileUnreadable, err) newlines++
} if newlines == targetNewlines {
data = data[:n] return blockStart + int64(index) + 1, false, nil
lines := bytes.Split(data, []byte("\n")) }
if len(lines) > 501 { }
drop := len(lines) - 501 }
offset := 0 scanned += int64(n)
for _, line := range lines[:drop] { position = blockStart
offset += len(line) + 1 }
} if position > 0 {
start += int64(offset) return end - int64(maxLogChunkBytes), true, nil
data = data[offset:] }
limited = true return 0, false, nil
} }
lineCount := bytes.Count(data, []byte("\n"))
if len(data) > 0 && data[len(data)-1] != '\n' { func countLogicalLines(data []byte) int {
lineCount++ if len(data) == 0 {
} return 0
return &biz.LogContent{Date: date, Path: path, Content: string(data), LineCount: lineCount, NextCursor: start, HasMore: start > 0, LimitedByBytes: limited, Size: info.Size(), ModifiedAt: info.ModTime()}, nil }
count := bytes.Count(data, []byte{'\n'})
if data[len(data)-1] != '\n' {
count++
}
return count
} }

View File

@ -68,7 +68,7 @@ func (s *runtimeSettings) MediaSettings() biz.MediaSettings {
if config == nil || config.Media == nil { if config == nil || config.Media == nil {
return biz.MediaSettings{} return biz.MediaSettings{}
} }
return biz.MediaSettings{SessionTTL: int(config.Media.SessionTtl), MaxFileSize: config.Media.MaxFileSize} return biz.MediaSettings{SessionTTL: int(config.Media.SessionTtl), MaxFileSize: config.Media.MaxFileSize, ChunkDir: config.Media.ChunkDir}
} }
func (s *runtimeSettings) UseMultipoint() bool { func (s *runtimeSettings) UseMultipoint() bool {
@ -110,5 +110,5 @@ func (i *tokenIssuer) ParseToken(token string) (*biz.AuthClaims, error) {
return nil, biz.ErrTokenInvalid return nil, biz.ErrTokenInvalid
} }
} }
return &biz.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, Issuer: claims.Issuer, ExpiresAt: claims.ExpiresAt.Time}, nil return &biz.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, Issuer: claims.Issuer, ExpiresAt: claims.ExpiresAt.Time}, nil
} }

View File

@ -95,7 +95,9 @@ func (s *aliyunStorage) List(_ context.Context, prefix, cursor string, limit int
} }
items := make([]*biz.StoredFile, 0, len(result.Objects)) items := make([]*biz.StoredFile, 0, len(result.Objects))
for _, object := range result.Objects { for _, object := range result.Objects {
items = append(items, s.file(object.Key, object.Size)) item := s.file(object.Key, object.Size)
item.LastModified = object.LastModified
items = append(items, item)
} }
next := "" next := ""
if result.IsTruncated { if result.IsTruncated {

View File

@ -141,7 +141,11 @@ func (s *awsStorage) List(ctx context.Context, prefix, cursor string, limit int)
if object.Size != nil { if object.Size != nil {
size = *object.Size size = *object.Size
} }
items = append(items, s.file(*object.Key, size)) item := s.file(*object.Key, size)
if object.LastModified != nil {
item.LastModified = *object.LastModified
}
items = append(items, item)
} }
next := "" next := ""
if result.NextContinuationToken != nil { if result.NextContinuationToken != nil {

View File

@ -94,7 +94,9 @@ func (s *huaweiStorage) List(_ context.Context, prefix, cursor string, limit int
} }
items := make([]*biz.StoredFile, 0, len(result.Contents)) items := make([]*biz.StoredFile, 0, len(result.Contents))
for _, object := range result.Contents { for _, object := range result.Contents {
items = append(items, s.file(object.Key, object.Size)) item := s.file(object.Key, object.Size)
item.LastModified = object.LastModified
items = append(items, item)
} }
next := result.NextMarker next := result.NextMarker
if result.IsTruncated && next == "" && len(result.Contents) > 0 { if result.IsTruncated && next == "" && len(result.Contents) > 0 {

View File

@ -6,6 +6,7 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io" "io"
"mime"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@ -200,7 +201,7 @@ func (s *fileStorage) List(ctx context.Context, prefix, cursor string, limit int
if infoErr != nil { if infoErr != nil {
return infoErr return infoErr
} }
items = append(items, &biz.StoredFile{Name: entry.Name(), Path: relative, URL: s.urlPrefix + "/" + relative, Size: info.Size()}) items = append(items, &biz.StoredFile{Name: entry.Name(), Path: relative, URL: s.urlPrefix + "/" + relative, Size: info.Size(), LastModified: info.ModTime(), ContentType: mime.TypeByExtension(filepath.Ext(entry.Name()))})
return nil return nil
}) })
if err != nil { if err != nil {

View File

@ -120,7 +120,10 @@ func (s *qiniuStorage) List(ctx context.Context, prefix, cursor string, limit in
} }
out := make([]*biz.StoredFile, 0, len(entries)) out := make([]*biz.StoredFile, 0, len(entries))
for _, entry := range entries { for _, entry := range entries {
out = append(out, s.file(entry.Key, entry.Fsize)) item := s.file(entry.Key, entry.Fsize)
item.LastModified = time.Unix(0, entry.PutTime*100)
item.ContentType = entry.MimeType
out = append(out, item)
} }
return out, marker, more, nil return out, marker, more, nil
} }

View File

@ -118,7 +118,10 @@ func (s *s3Storage) List(ctx context.Context, prefix, cursor string, limit int)
if item.Err != nil { if item.Err != nil {
return nil, "", false, item.Err return nil, "", false, item.Err
} }
out = append(out, s.file(item.Key, item.Size)) file := s.file(item.Key, item.Size)
file.LastModified = item.LastModified
file.ContentType = item.ContentType
out = append(out, file)
if len(out) > limit { if len(out) > limit {
break break
} }

View File

@ -8,6 +8,7 @@ import (
"net/url" "net/url"
"path" "path"
"strings" "strings"
"time"
cos "github.com/tencentyun/cos-go-sdk-v5" cos "github.com/tencentyun/cos-go-sdk-v5"
"kra/internal/biz" "kra/internal/biz"
@ -107,7 +108,14 @@ func (s *tencentStorage) List(ctx context.Context, prefix, cursor string, limit
} }
items := make([]*biz.StoredFile, 0, len(result.Contents)) items := make([]*biz.StoredFile, 0, len(result.Contents))
for _, object := range result.Contents { for _, object := range result.Contents {
items = append(items, s.file(object.Key, object.Size)) item := s.file(object.Key, object.Size)
for _, layout := range []string{time.RFC3339, "2006-01-02T15:04:05.000Z"} {
if parsed, parseErr := time.Parse(layout, object.LastModified); parseErr == nil {
item.LastModified = parsed
break
}
}
items = append(items, item)
} }
next := result.NextMarker next := result.NextMarker
if result.IsTruncated && next == "" && len(result.Contents) > 0 { if result.IsTruncated && next == "" && len(result.Contents) > 0 {

View File

@ -429,6 +429,9 @@ func (r *userRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User
} }
func (r *userRepo) DeleteUser(ctx context.Context, id uint) error { func (r *userRepo) DeleteUser(ctx context.Context, id uint) error {
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("id = ?", id).Delete(&userPO{}).Error; err != nil {
return err
}
if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil { if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil {
return err return err
} }
@ -438,7 +441,7 @@ func (r *userRepo) DeleteUser(ctx context.Context, id uint) error {
if err := tx.Where("sys_user_id = ?", id).Delete(&userPositionPO{}).Error; err != nil { if err := tx.Where("sys_user_id = ?", id).Delete(&userPositionPO{}).Error; err != nil {
return err return err
} }
return tx.Delete(&userPO{}, id).Error return nil
}) })
} }
func (r *userRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error { func (r *userRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error {

View File

@ -242,10 +242,10 @@ func (h *Dictionary) Path(c *gin.Context) {
httpx.Fail(c, "字典详情ID格式错误") httpx.Fail(c, "字典详情ID格式错误")
return return
} }
item, err := h.service.DictionaryDetail(c.Request.Context(), uint(id)) path, err := h.service.DictionaryPath(c.Request.Context(), uint(id))
if err != nil { if err != nil {
httpx.Fail(c, "获取失败") httpx.Fail(c, "获取失败")
return return
} }
httpx.Write(c, httpx.CodeSuccess, gin.H{"path": item.Path}, "获取成功") httpx.Write(c, httpx.CodeSuccess, gin.H{"path": path}, "获取成功")
} }

View File

@ -30,11 +30,6 @@ func exportParams(values url.Values) map[string]string {
out[key] = items[0] out[key] = items[0]
} }
} }
for key, items := range values {
if key != "params" && len(items) > 0 {
out[key] = items[0]
}
}
return out return out
} }

View File

@ -1,8 +1,6 @@
package handler package handler
import ( import (
"strconv"
"kra/internal/server/httpx" "kra/internal/server/httpx"
"kra/internal/service" "kra/internal/service"
"kra/internal/service/dto" "kra/internal/service/dto"
@ -24,7 +22,7 @@ func (h *Organization) ListDepartments(c *gin.Context) {
_ = c.ShouldBindJSON(&req) _ = c.ShouldBindJSON(&req)
items, err := h.departments.Departments(c.Request.Context(), req.Name) items, err := h.departments.Departments(c.Request.Context(), req.Name)
if err != nil { if err != nil {
httpx.Fail(c, "获取失败") httpx.Fail(c, "获取失败:"+err.Error())
return return
} }
httpx.Write(c, httpx.CodeSuccess, items, "获取成功") httpx.Write(c, httpx.CodeSuccess, items, "获取成功")
@ -63,19 +61,23 @@ func (h *Organization) UpdateDepartment(c *gin.Context) {
} }
func (h *Organization) DeleteDepartment(c *gin.Context) { func (h *Organization) DeleteDepartment(c *gin.Context) {
var req dto.DeleteDepartmentRequest var req dto.DeleteDepartmentRequest
if c.ShouldBindJSON(&req) != nil { if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, "参数错误") httpx.Fail(c, err.Error())
return return
} }
if err := h.departments.Delete(c.Request.Context(), req.ID); err != nil { if err := h.departments.Delete(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, err.Error()) httpx.Fail(c, "删除失败:"+err.Error())
return return
} }
httpx.Write(c, httpx.CodeSuccess, nil, "删除成功") httpx.Write(c, httpx.CodeSuccess, nil, "删除成功")
} }
func (h *Organization) FindDepartment(c *gin.Context) { func (h *Organization) FindDepartment(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("id"), 10, 64) var req dto.DeleteDepartmentRequest
item, err := h.departments.Department(c.Request.Context(), uint(id)) if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
item, err := h.departments.Department(c.Request.Context(), req.ID)
if err != nil { if err != nil {
httpx.Fail(c, "获取失败:"+err.Error()) httpx.Fail(c, "获取失败:"+err.Error())
return return
@ -83,8 +85,12 @@ func (h *Organization) FindDepartment(c *gin.Context) {
httpx.Write(c, httpx.CodeSuccess, item, "获取成功") httpx.Write(c, httpx.CodeSuccess, item, "获取成功")
} }
func (h *Organization) DepartmentUsers(c *gin.Context) { func (h *Organization) DepartmentUsers(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("deptId"), 10, 64) var req dto.SetDepartmentUsersRequest
ids, err := h.departments.UserIDs(c.Request.Context(), uint(id)) if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
ids, err := h.departments.UserIDs(c.Request.Context(), req.DepartmentID)
if err != nil { if err != nil {
httpx.Fail(c, "获取失败:"+err.Error()) httpx.Fail(c, "获取失败:"+err.Error())
return return
@ -96,8 +102,12 @@ func (h *Organization) DepartmentUsers(c *gin.Context) {
} }
func (h *Organization) SetDepartmentUsers(c *gin.Context) { func (h *Organization) SetDepartmentUsers(c *gin.Context) {
var req dto.SetDepartmentUsersRequest var req dto.SetDepartmentUsersRequest
if c.ShouldBindJSON(&req) != nil { if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, "参数错误") httpx.Fail(c, err.Error())
return
}
if req.DepartmentID == 0 {
httpx.Fail(c, "部门ID不能为空")
return return
} }
if err := h.departments.SetUsers(c.Request.Context(), &req); err != nil { if err := h.departments.SetUsers(c.Request.Context(), &req); err != nil {
@ -108,8 +118,8 @@ func (h *Organization) SetDepartmentUsers(c *gin.Context) {
} }
func (h *Organization) SetUserDepartments(c *gin.Context) { func (h *Organization) SetUserDepartments(c *gin.Context) {
var req dto.SetUserDepartmentsRequest var req dto.SetUserDepartmentsRequest
if c.ShouldBindJSON(&req) != nil { if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, "参数错误") httpx.Fail(c, err.Error())
return return
} }
if err := h.departments.SetUserDepartments(c.Request.Context(), &req); err != nil { if err := h.departments.SetUserDepartments(c.Request.Context(), &req); err != nil {
@ -121,13 +131,13 @@ func (h *Organization) SetUserDepartments(c *gin.Context) {
func (h *Organization) ListPositions(c *gin.Context) { func (h *Organization) ListPositions(c *gin.Context) {
var req dto.PositionListRequest var req dto.PositionListRequest
if c.ShouldBindJSON(&req) != nil { if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, "参数错误") httpx.Fail(c, err.Error())
return return
} }
items, total, err := h.positions.Positions(c.Request.Context(), &req) items, total, err := h.positions.Positions(c.Request.Context(), &req)
if err != nil { if err != nil {
httpx.Fail(c, "获取失败") httpx.Fail(c, "获取失败:"+err.Error())
return return
} }
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功") httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
@ -166,8 +176,8 @@ func (h *Organization) UpdatePosition(c *gin.Context) {
} }
func (h *Organization) DeletePosition(c *gin.Context) { func (h *Organization) DeletePosition(c *gin.Context) {
var req dto.DeletePositionRequest var req dto.DeletePositionRequest
if c.ShouldBindJSON(&req) != nil { if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, "参数错误") httpx.Fail(c, err.Error())
return return
} }
if err := h.positions.Delete(c.Request.Context(), req.ID); err != nil { if err := h.positions.Delete(c.Request.Context(), req.ID); err != nil {
@ -177,8 +187,12 @@ func (h *Organization) DeletePosition(c *gin.Context) {
httpx.Write(c, httpx.CodeSuccess, nil, "删除成功") httpx.Write(c, httpx.CodeSuccess, nil, "删除成功")
} }
func (h *Organization) FindPosition(c *gin.Context) { func (h *Organization) FindPosition(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("id"), 10, 64) var req dto.DeletePositionRequest
item, err := h.positions.Position(c.Request.Context(), uint(id)) if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
item, err := h.positions.Position(c.Request.Context(), req.ID)
if err != nil { if err != nil {
httpx.Fail(c, "获取失败:"+err.Error()) httpx.Fail(c, "获取失败:"+err.Error())
return return
@ -186,8 +200,12 @@ func (h *Organization) FindPosition(c *gin.Context) {
httpx.Write(c, httpx.CodeSuccess, item, "获取成功") httpx.Write(c, httpx.CodeSuccess, item, "获取成功")
} }
func (h *Organization) PositionUsers(c *gin.Context) { func (h *Organization) PositionUsers(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("positionId"), 10, 64) var req dto.SetPositionUsersRequest
ids, err := h.positions.UserIDs(c.Request.Context(), uint(id)) if err := c.ShouldBindQuery(&req); err != nil {
httpx.Fail(c, err.Error())
return
}
ids, err := h.positions.UserIDs(c.Request.Context(), req.PositionID)
if err != nil { if err != nil {
httpx.Fail(c, "获取失败:"+err.Error()) httpx.Fail(c, "获取失败:"+err.Error())
return return
@ -199,8 +217,12 @@ func (h *Organization) PositionUsers(c *gin.Context) {
} }
func (h *Organization) SetPositionUsers(c *gin.Context) { func (h *Organization) SetPositionUsers(c *gin.Context) {
var req dto.SetPositionUsersRequest var req dto.SetPositionUsersRequest
if c.ShouldBindJSON(&req) != nil { if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, "参数错误") httpx.Fail(c, err.Error())
return
}
if req.PositionID == 0 {
httpx.Fail(c, "岗位ID不能为空")
return return
} }
if err := h.positions.SetUsers(c.Request.Context(), &req); err != nil { if err := h.positions.SetUsers(c.Request.Context(), &req); err != nil {
@ -211,8 +233,8 @@ func (h *Organization) SetPositionUsers(c *gin.Context) {
} }
func (h *Organization) SetUserPositions(c *gin.Context) { func (h *Organization) SetUserPositions(c *gin.Context) {
var req dto.SetUserPositionsRequest var req dto.SetUserPositionsRequest
if c.ShouldBindJSON(&req) != nil { if err := c.ShouldBindJSON(&req); err != nil {
httpx.Fail(c, "参数错误") httpx.Fail(c, err.Error())
return return
} }
if err := h.positions.SetUserPositions(c.Request.Context(), &req); err != nil { if err := h.positions.SetUserPositions(c.Request.Context(), &req); err != nil {

View File

@ -123,11 +123,7 @@ func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc {
values = append(values, dto.Route{Path: route.Path, Method: route.Method}) values = append(values, dto.Route{Path: route.Path, Method: route.Method})
} }
if err := h.system.InitializeRoutes(c.Request.Context(), &input, values); err != nil { if err := h.system.InitializeRoutes(c.Request.Context(), &input, values); err != nil {
if errors.Is(err, biz.ErrTaskRuntimeReload) { httpx.Fail(c, "自动创建数据库失败,请查看后台日志,检查后在进行初始化")
httpx.Fail(c, "数据库已初始化,但定时任务加载失败:"+err.Error())
return
}
httpx.Fail(c, "自动创建数据库失败: "+err.Error())
return return
} }
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "自动创建数据库成功") httpx.Write(c, httpx.CodeSuccess, gin.H{}, "自动创建数据库成功")

View File

@ -1,9 +1,6 @@
package handler package handler
import ( import (
"errors"
"kra/internal/biz"
"kra/internal/server/httpx" "kra/internal/server/httpx"
"kra/internal/service" "kra/internal/service"
"kra/internal/service/dto" "kra/internal/service/dto"
@ -67,10 +64,6 @@ func (h *SystemConfig) Set(c *gin.Context) {
func (h *SystemConfig) Reload(c *gin.Context) { func (h *SystemConfig) Reload(c *gin.Context) {
if err := h.system.ReloadConfig(c.Request.Context()); err != nil { if err := h.system.ReloadConfig(c.Request.Context()); err != nil {
if errors.Is(err, biz.ErrTaskRuntimeReload) {
httpx.Fail(c, "系统配置已重载,但定时任务重载失败:"+err.Error())
return
}
httpx.Fail(c, "重载系统失败:"+err.Error()) httpx.Fail(c, "重载系统失败:"+err.Error())
return return
} }

View File

@ -1,14 +1,13 @@
package handler package handler
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
"kra/internal/biz"
"kra/internal/server/httpx" "kra/internal/server/httpx"
"kra/internal/server/middleware"
"kra/internal/service" "kra/internal/service"
"kra/internal/service/dto" "kra/internal/service/dto"
@ -31,13 +30,7 @@ func (h *Task) Create(c *gin.Context) {
} }
_, err := h.service.CreateRequest(c.Request.Context(), &req) _, err := h.service.CreateRequest(c.Request.Context(), &req)
if err != nil { if err != nil {
if errors.Is(err, biz.ErrTaskSchedule) { httpx.Fail(c, "创建失败: "+err.Error())
var scheduleErr *biz.TaskScheduleError
errors.As(err, &scheduleErr)
httpx.Fail(c, "调度失败:"+scheduleErr.Error())
return
}
httpx.Fail(c, "创建失败:"+err.Error())
return return
} }
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功") httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
@ -50,13 +43,7 @@ func (h *Task) Update(c *gin.Context) {
return return
} }
if err := h.service.UpdateRequest(c.Request.Context(), &req); err != nil { if err := h.service.UpdateRequest(c.Request.Context(), &req); err != nil {
if errors.Is(err, biz.ErrTaskSchedule) { httpx.Fail(c, "更新失败: "+err.Error())
var scheduleErr *biz.TaskScheduleError
errors.As(err, &scheduleErr)
httpx.Fail(c, "调度失败:"+scheduleErr.Error())
return
}
httpx.Fail(c, "更新失败:"+err.Error())
return return
} }
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功") httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
@ -134,8 +121,13 @@ func (h *Task) Methods(c *gin.Context) {
} }
func (h *Task) AlertStream(c *gin.Context) { func (h *Task) AlertStream(c *gin.Context) {
claims := middleware.Claims(c)
if claims == nil || claims.ID == 0 {
httpx.Fail(c, "未获取到用户身份")
return
}
c.Header("Content-Type", "text/event-stream") c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache") c.Header("Cache-Control", "no-cache, no-transform")
c.Header("Connection", "keep-alive") c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no") c.Header("X-Accel-Buffering", "no")
flusher, ok := c.Writer.(http.Flusher) flusher, ok := c.Writer.(http.Flusher)
@ -145,11 +137,11 @@ func (h *Task) AlertStream(c *gin.Context) {
} }
// Remove the normal server deadline for long-lived SSE connections. // Remove the normal server deadline for long-lived SSE connections.
_ = http.NewResponseController(c.Writer).SetWriteDeadline(time.Time{}) _ = http.NewResponseController(c.Writer).SetWriteDeadline(time.Time{})
events := h.service.Subscribe() events := h.service.Subscribe(claims.ID)
defer h.service.Unsubscribe(events) defer h.service.Unsubscribe(claims.ID, events)
ticker := time.NewTicker(20 * time.Second) ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop() defer ticker.Stop()
_, _ = fmt.Fprint(c.Writer, "event: connected\ndata: {}\n\n") _, _ = fmt.Fprint(c.Writer, ": connected\n\n")
flusher.Flush() flusher.Flush()
for { for {
select { select {
@ -157,10 +149,10 @@ func (h *Task) AlertStream(c *gin.Context) {
if !open { if !open {
return return
} }
_, _ = fmt.Fprintf(c.Writer, "event: alert\ndata: %s\n\n", event) _, _ = fmt.Fprintf(c.Writer, "event: timedTask:alert\ndata: %s\n\n", event)
flusher.Flush() flusher.Flush()
case <-ticker.C: case <-ticker.C:
_, _ = fmt.Fprint(c.Writer, ": keepalive\n\n") _, _ = fmt.Fprint(c.Writer, ": ping\n\n")
flusher.Flush() flusher.Flush()
case <-c.Request.Context().Done(): case <-c.Request.Context().Done():
return return

View File

@ -124,6 +124,21 @@ func (s *DictionaryService) DictionaryDetail(ctx context.Context, id uint) (*dto
} }
return detailDTO(v), nil return detailDTO(v), nil
} }
func (s *DictionaryService) DictionaryPath(ctx context.Context, id uint) ([]*dto.DictionaryDetailResponse, error) {
path := make([]*dto.DictionaryDetailResponse, 0)
for id != 0 {
value, err := s.uc.FindDictionaryDetail(ctx, id)
if err != nil {
return nil, err
}
path = append([]*dto.DictionaryDetailResponse{detailDTO(value)}, path...)
if value.ParentID == nil {
break
}
id = *value.ParentID
}
return path, nil
}
func (s *DictionaryService) DictionaryTree(ctx context.Context, id uint, typ string) ([]*dto.DictionaryDetailResponse, error) { func (s *DictionaryService) DictionaryTree(ctx context.Context, id uint, typ string) ([]*dto.DictionaryDetailResponse, error) {
items, err := s.uc.DictionaryDetailTree(ctx, id, typ) items, err := s.uc.DictionaryDetailTree(ctx, id, typ)
if err != nil { if err != nil {

View File

@ -29,10 +29,10 @@ type MediaCategoryResponse struct {
} }
type StorageObjectResponse struct { type StorageObjectResponse struct {
Name string `json:"name"` Key string `json:"Key"`
Key string `json:"key"` Size int64 `json:"Size"`
URL string `json:"url"` LastModified time.Time `json:"LastModified"`
Size int64 `json:"size"` ContentType string `json:"ContentType"`
} }
type InitUploadResponse struct { type InitUploadResponse struct {

View File

@ -15,10 +15,10 @@ type DepartmentListRequest struct {
Name string `json:"name"` Name string `json:"name"`
} }
type DeleteDepartmentRequest struct { type DeleteDepartmentRequest struct {
ID uint `json:"ID"` ID uint `json:"ID" form:"id"`
} }
type SetDepartmentUsersRequest struct { type SetDepartmentUsersRequest struct {
DepartmentID uint `json:"deptId"` DepartmentID uint `json:"deptId" form:"deptId"`
UserIDs []uint `json:"userIds"` UserIDs []uint `json:"userIds"`
} }
type SetUserDepartmentsRequest struct { type SetUserDepartmentsRequest struct {
@ -60,10 +60,10 @@ type PositionListRequest struct {
Status *bool `json:"status"` Status *bool `json:"status"`
} }
type DeletePositionRequest struct { type DeletePositionRequest struct {
ID uint `json:"ID"` ID uint `json:"ID" form:"id"`
} }
type SetPositionUsersRequest struct { type SetPositionUsersRequest struct {
PositionID uint `json:"positionId"` PositionID uint `json:"positionId" form:"positionId"`
UserIDs []uint `json:"userIds"` UserIDs []uint `json:"userIds"`
} }
type SetUserPositionsRequest struct { type SetUserPositionsRequest struct {

View File

@ -87,13 +87,19 @@ func (s *ExportService) TemplatesFilter(ctx context.Context, page, size int, nam
return s.Templates(ctx, page, size, &biz.ExportTemplate{Name: name, TableName: tableName, TemplateID: templateID, StartCreatedAt: start, EndCreatedAt: end}) return s.Templates(ctx, page, size, &biz.ExportTemplate{Name: name, TableName: tableName, TemplateID: templateID, StartCreatedAt: start, EndCreatedAt: end})
} }
func exportDTO(v *biz.ExportTemplate) *dto.ExportTemplateResponse { func exportDTO(v *biz.ExportTemplate) *dto.ExportTemplateResponse {
conditions := make([]dto.ExportConditionResponse, 0, len(v.Conditions)) var conditions []dto.ExportConditionResponse
for _, x := range v.Conditions { if v.Conditions != nil {
conditions = append(conditions, dto.ExportConditionResponse{From: x.From, Column: x.Column, Operator: x.Operator}) conditions = make([]dto.ExportConditionResponse, 0, len(v.Conditions))
for _, x := range v.Conditions {
conditions = append(conditions, dto.ExportConditionResponse{From: x.From, Column: x.Column, Operator: x.Operator})
}
} }
joins := make([]dto.ExportJoinResponse, 0, len(v.Joins)) var joins []dto.ExportJoinResponse
for _, x := range v.Joins { if v.Joins != nil {
joins = append(joins, dto.ExportJoinResponse{Join: x.Join, Table: x.Table, On: x.On}) joins = make([]dto.ExportJoinResponse, 0, len(v.Joins))
for _, x := range v.Joins {
joins = append(joins, dto.ExportJoinResponse{Join: x.Join, Table: x.Table, On: x.On})
}
} }
return &dto.ExportTemplateResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, DBName: v.DBName, Name: v.Name, TableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order, Conditions: conditions, Joins: joins} return &dto.ExportTemplateResponse{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, DeletedAt: nil, DBName: v.DBName, Name: v.Name, TableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order, Conditions: conditions, Joins: joins}
} }

View File

@ -6,7 +6,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"strconv"
"strings" "strings"
"time"
"github.com/xuri/excelize/v2" "github.com/xuri/excelize/v2"
) )
@ -59,7 +61,23 @@ func (s *ExportService) Export(ctx context.Context, tid string, params map[strin
for rowIndex, row := range rows { for rowIndex, row := range rows {
for column, key := range keys { for column, key := range keys {
cell, _ := excelize.CoordinatesToCellName(column+1, rowIndex+2) cell, _ := excelize.CoordinatesToCellName(column+1, rowIndex+2)
_ = file.SetCellValue(sheet, cell, row[key]) lookup := strings.ReplaceAll(strings.ReplaceAll(key, `"`, ""), "`", "")
if len(template.Joins) > 0 {
if parts := strings.Split(lookup, " as "); len(parts) > 1 {
lookup = strings.TrimSpace(parts[1])
} else if parts = strings.Split(lookup, "."); len(parts) > 1 {
lookup = parts[1]
}
}
value := fmt.Sprintf("%v", row[lookup])
if timestamp, ok := row[lookup].(time.Time); ok {
value = timestamp.Format("2006-01-02 15:04:05")
}
if number, parseErr := strconv.ParseFloat(value, 64); parseErr == nil {
_ = file.SetCellValue(sheet, cell, number)
} else {
_ = file.SetCellValue(sheet, cell, value)
}
} }
} }
buffer, err := file.WriteToBuffer() buffer, err := file.WriteToBuffer()
@ -108,11 +126,7 @@ func (s *ExportService) Import(ctx context.Context, tid string, reader io.Reader
return err return err
} }
defer file.Close() defer file.Close()
sheets := file.GetSheetList() rows, err := file.GetRows("Sheet1")
if len(sheets) == 0 {
return errors.New("工作簿没有工作表")
}
rows, err := file.GetRows(sheets[0])
if err != nil { if err != nil {
return err return err
} }
@ -128,6 +142,9 @@ func (s *ExportService) Import(ctx context.Context, tid string, reader io.Reader
reverse[labels[key]] = key reverse[labels[key]] = key
} }
headers := rows[0] headers := rows[0]
for index := range headers {
headers[index] = strings.TrimSpace(headers[index])
}
items := make([]map[string]any, 0, len(rows)-1) items := make([]map[string]any, 0, len(rows)-1)
for _, row := range rows[1:] { for _, row := range rows[1:] {
item := map[string]any{} item := map[string]any{}
@ -142,8 +159,5 @@ func (s *ExportService) Import(ctx context.Context, tid string, reader io.Reader
} }
items = append(items, item) items = append(items, item)
} }
if err = s.uc.ImportExportRows(ctx, template, items); err != nil { return s.uc.ImportExportRows(ctx, template, items)
return fmt.Errorf("导入失败: %w", err)
}
return nil
} }

View File

@ -75,7 +75,7 @@ func (s *MediaService) Storage(ctx context.Context, prefix, cursor string, limit
} }
out := make([]*dto.StorageObjectResponse, 0, len(items)) out := make([]*dto.StorageObjectResponse, 0, len(items))
for _, v := range items { for _, v := range items {
out = append(out, &dto.StorageObjectResponse{Name: v.Name, Key: v.Path, URL: v.URL, Size: v.Size}) out = append(out, &dto.StorageObjectResponse{Key: v.Path, Size: v.Size, LastModified: v.LastModified, ContentType: v.ContentType})
} }
return out, next, more, nil return out, next, more, nil
} }

View File

@ -76,10 +76,10 @@ func (s *TaskService) Logs(ctx context.Context, page, size int, taskID uint, sta
return out, total, nil return out, total, nil
} }
func (s *TaskService) Trigger(ctx context.Context, id uint) error { return s.uc.Trigger(ctx, id) } func (s *TaskService) Trigger(ctx context.Context, id uint) error { return s.uc.Trigger(ctx, id) }
func (s *TaskService) Reload(ctx context.Context) error { return s.uc.Reload(ctx) } func (s *TaskService) Reload(ctx context.Context) error { return s.uc.Reload(ctx) }
func (s *TaskService) Subscribe() chan []byte { return s.uc.Subscribe() } func (s *TaskService) Subscribe(userID uint) chan []byte { return s.uc.Subscribe(userID) }
func (s *TaskService) Unsubscribe(events chan []byte) { s.uc.Unsubscribe(events) } func (s *TaskService) Unsubscribe(userID uint, events chan []byte) { s.uc.Unsubscribe(userID, events) }
func (s *TaskService) RegisteredMethods() []*dto.TaskMethodResponse { func (s *TaskService) RegisteredMethods() []*dto.TaskMethodResponse {
methods := biz.RegisteredTaskMethods() methods := biz.RegisteredTaskMethods()

View File

@ -56,7 +56,7 @@ func taskHTTPClient(allowPrivate bool) *http.Client {
func (e *TaskExecutor) runHTTP(ctx context.Context, task *biz.TimedTask) (string, error) { func (e *TaskExecutor) runHTTP(ctx context.Context, task *biz.TimedTask) (string, error) {
parsed, err := url.Parse(task.HTTPURL) parsed, err := url.Parse(task.HTTPURL)
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("URL 非法: %w", err)
} }
if parsed.Scheme != "http" && parsed.Scheme != "https" { if parsed.Scheme != "http" && parsed.Scheme != "https" {
return "", fmt.Errorf("仅允许 http/https, 实际为 %q", parsed.Scheme) return "", fmt.Errorf("仅允许 http/https, 实际为 %q", parsed.Scheme)
@ -70,7 +70,11 @@ func (e *TaskExecutor) runHTTP(ctx context.Context, task *biz.TimedTask) (string
return "", err return "", err
} }
headers := map[string]string{} headers := map[string]string{}
_ = json.Unmarshal(task.HTTPHeader, &headers) if len(task.HTTPHeader) > 0 {
if err = json.Unmarshal(task.HTTPHeader, &headers); err != nil {
return "", fmt.Errorf("http_header 必须是 JSON 对象: %w", err)
}
}
for key, value := range headers { for key, value := range headers {
request.Header.Set(key, value) request.Header.Set(key, value)
} }
@ -118,6 +122,8 @@ func (e *TaskExecutor) runMethod(task *biz.TimedTask) error {
ttl = int(config.Media.SessionTtl) ttl = int(config.Media.SessionTtl)
} }
err = e.media.CleanupStale(ctx, ttl) err = e.media.CleanupStale(ctx, ttl)
default:
err = fmt.Errorf("方法 %s 未注册(需在 initialize/timer.go 经 task.Register 注册)", task.MethodName)
} }
done <- err done <- err
}() }()

View File

@ -14,6 +14,7 @@ import (
type TaskScheduler struct { type TaskScheduler struct {
tasks *biz.TaskUsecase tasks *biz.TaskUsecase
authorities *biz.AuthorityUsecase
executor *TaskExecutor executor *TaskExecutor
logger *slog.Logger logger *slog.Logger
standard *cron.Cron standard *cron.Cron
@ -24,7 +25,7 @@ type TaskScheduler struct {
runContext context.Context runContext context.Context
cancel context.CancelFunc cancel context.CancelFunc
subMu sync.RWMutex subMu sync.RWMutex
subscribers map[chan []byte]struct{} subscribers map[uint]map[chan []byte]struct{}
} }
type scheduledEntry struct { type scheduledEntry struct {
@ -32,8 +33,8 @@ type scheduledEntry struct {
entry cron.EntryID entry cron.EntryID
} }
func NewTaskScheduler(tasks *biz.TaskUsecase, executor *TaskExecutor, logger *slog.Logger) *TaskScheduler { func NewTaskScheduler(tasks *biz.TaskUsecase, authorities *biz.AuthorityUsecase, executor *TaskExecutor, logger *slog.Logger) *TaskScheduler {
return &TaskScheduler{tasks: tasks, executor: executor, logger: logger, standard: cron.New(), seconds: cron.New(cron.WithSeconds()), entries: map[uint]scheduledEntry{}, subscribers: map[chan []byte]struct{}{}} return &TaskScheduler{tasks: tasks, authorities: authorities, executor: executor, logger: logger, standard: cron.New(), seconds: cron.New(cron.WithSeconds()), entries: map[uint]scheduledEntry{}, subscribers: map[uint]map[chan []byte]struct{}{}}
} }
func NewTaskRuntime(scheduler *TaskScheduler) biz.TaskRuntime { return scheduler } func NewTaskRuntime(scheduler *TaskScheduler) biz.TaskRuntime { return scheduler }
@ -131,7 +132,12 @@ func (s *TaskScheduler) executionContext() context.Context {
func (s *TaskScheduler) run(task *biz.TimedTask, trigger string) { func (s *TaskScheduler) run(task *biz.TimedTask, trigger string) {
log := s.executor.Run(s.executionContext(), task, trigger) log := s.executor.Run(s.executionContext(), task, trigger)
if log.Status != "success" { if log.Status != "success" {
s.Broadcast(map[string]any{"taskId": task.ID, "taskName": task.Name, "status": log.Status, "errorMsg": log.ErrorMsg, "time": time.Now()}) ids, err := s.authorities.AuthorityUserIDs(context.Background(), 888)
if err != nil {
s.logger.Error("query timed task alert recipients failed", "error", err)
return
}
s.PublishToUsers(ids, map[string]any{"taskId": task.ID, "name": task.Name, "error": log.ErrorMsg, "time": time.Now().Format(time.RFC3339)})
} }
} }
@ -196,32 +202,44 @@ func (s *TaskScheduler) Trigger(task *biz.TimedTask) {
go s.run(&copy, "manual") go s.run(&copy, "manual")
} }
func (s *TaskScheduler) Subscribe() chan []byte { func (s *TaskScheduler) Subscribe(userID uint) chan []byte {
ch := make(chan []byte, 16) ch := make(chan []byte, 16)
s.subMu.Lock() s.subMu.Lock()
s.subscribers[ch] = struct{}{} if s.subscribers[userID] == nil {
s.subscribers[userID] = map[chan []byte]struct{}{}
}
s.subscribers[userID][ch] = struct{}{}
s.subMu.Unlock() s.subMu.Unlock()
return ch return ch
} }
func (s *TaskScheduler) Unsubscribe(ch chan []byte) { func (s *TaskScheduler) Unsubscribe(userID uint, ch chan []byte) {
s.subMu.Lock() s.subMu.Lock()
if _, ok := s.subscribers[ch]; ok { if subscribers := s.subscribers[userID]; subscribers != nil {
delete(s.subscribers, ch) if _, ok := subscribers[ch]; !ok {
s.subMu.Unlock()
return
}
delete(subscribers, ch)
if len(subscribers) == 0 {
delete(s.subscribers, userID)
}
close(ch) close(ch)
} }
s.subMu.Unlock() s.subMu.Unlock()
} }
func (s *TaskScheduler) Broadcast(value any) { func (s *TaskScheduler) PublishToUsers(userIDs []uint, value any) {
raw, err := json.Marshal(value) raw, err := json.Marshal(value)
if err != nil { if err != nil {
raw = []byte(fmt.Sprint(value)) raw = []byte(fmt.Sprint(value))
} }
s.subMu.RLock() s.subMu.RLock()
defer s.subMu.RUnlock() defer s.subMu.RUnlock()
for ch := range s.subscribers { for _, userID := range userIDs {
select { for ch := range s.subscribers[userID] {
case ch <- raw: select {
default: case ch <- raw:
default:
}
} }
} }
} }

View File

@ -22,6 +22,7 @@ type Claims struct {
NickName string NickName string
AuthorityID uint `json:"AuthorityId"` AuthorityID uint `json:"AuthorityId"`
BufferTime int64 BufferTime int64
UserType string
MustChangePwd bool `json:"mustChangePwd"` MustChangePwd bool `json:"mustChangePwd"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@ -31,7 +32,7 @@ func Generate(secret, issuer string, expires, buffer time.Duration, userID, auth
return "", nil, errors.New("empty JWT signing key") return "", nil, errors.New("empty JWT signing key")
} }
now := time.Now() now := time.Now()
claims := &Claims{UUID: uuid, ID: userID, Username: username, NickName: nickname, AuthorityID: authorityID, BufferTime: int64(buffer / time.Second), MustChangePwd: mustChange, RegisteredClaims: jwt.RegisteredClaims{Issuer: issuer, IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now.Add(-time.Second)), ExpiresAt: jwt.NewNumericDate(now.Add(expires))}} claims := &Claims{UUID: uuid, ID: userID, Username: username, NickName: nickname, AuthorityID: authorityID, BufferTime: int64(buffer / time.Second), UserType: "admin", MustChangePwd: mustChange, RegisteredClaims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"GVA"}, Issuer: issuer, NotBefore: jwt.NewNumericDate(now.Add(-1000)), ExpiresAt: jwt.NewNumericDate(now.Add(expires))}}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret)) token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
return token, claims, err return token, claims, err
} }