first commit

This commit is contained in:
likun 2025-04-18 17:17:23 +08:00
commit c882f04529
96 changed files with 9352 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea

26
admin/apps/game/boot.go Normal file
View File

@ -0,0 +1,26 @@
package game
import (
"admin/apps/game/server"
"admin/apps/game/service"
"admin/internal/global"
"admin/lib/node"
)
func initFun(app *node.Application) error {
svc := service.NewCmdServerSvc(global.GLOB_DB) // 初始化应用服务
srv := server.New(svc) // 初始化http服务
srv.Route(global.GLOB_API_ENGINE) // 初始化http服务路由
return nil
}
func New() *node.ApplicationDescInfo {
app := node.NewApplicationDescInfo("user", initFun)
return app
}
func must(err error) {
if err != nil {
panic(err)
}
}

View File

@ -0,0 +1,94 @@
package entity
import (
"admin/apps/game/model"
"admin/apps/game/model/dto"
"reflect"
)
var ProjectDtoFieldsDescInfo = DefaultProject().GetDtoFieldsDescInfo()
type Project struct {
Id int
po *model.Project
}
func DefaultProject() *Project {
return &Project{
po: &model.Project{},
}
}
func FromProjectPo(po *model.Project) *Project {
return &Project{
Id: po.ID,
po: po,
}
}
func FromProjectDto(dto *dto.CommonDtoValues) *Project {
et := DefaultProject()
po := et.po
//to := reflect.TypeOf(po)
vo := reflect.ValueOf(po)
for _, f := range dto.Values {
fo := vo.FieldByName(f.FieldName)
fo.Set(reflect.ValueOf(f.Value))
}
return et
}
func (et *Project) ToPo() *model.Project {
return et.po
}
func (et *Project) ToCommonDto() *dto.CommonDtoValues {
obj := &dto.CommonDtoValues{}
to := reflect.TypeOf(et.po).Elem()
vo := reflect.ValueOf(et.po).Elem()
for i := 0; i < vo.NumField(); i++ {
ft := to.Field(i)
fo := vo.Field(i)
f1 := &dto.CommonDtoValue{
FieldName: ft.Name,
Value: fo.Interface(),
}
obj.Values = append(obj.Values, f1)
}
return obj
}
func (et *Project) GetDtoFieldsDescInfo() []*dto.CommonDtoFieldDesc {
to := reflect.TypeOf(et.po).Elem()
vo := reflect.ValueOf(et.po).Elem()
obj := make([]*dto.CommonDtoFieldDesc, 0, to.NumField())
for i := 0; i < vo.NumField(); i++ {
ft := to.Field(i)
//fo := vo.Field(i)
f1 := &dto.CommonDtoFieldDesc{
Name: ft.Name,
Key: ft.Name,
Type: ft.Type.Name(),
HelpText: ft.Tag.Get("desc"),
Editable: true,
Require: true,
Choices: make([]*dto.CommonDtoFieldChoice, 0),
MultiChoice: false,
}
obj = append(obj, f1)
}
return obj
}

View File

@ -0,0 +1,94 @@
package entity
import (
"admin/apps/game/model"
"admin/apps/game/model/dto"
"reflect"
)
var ServerDtoFieldsDescInfo = DefaultServer().GetDtoFieldsDescInfo()
type Server struct {
Id int
po *model.Server
}
func DefaultServer() *Server {
return &Server{
po: &model.Server{},
}
}
func FromServerPo(po *model.Server) *Server {
return &Server{
Id: po.ID,
po: po,
}
}
func FromServerDto(dto *dto.CommonDtoValues) *Server {
et := DefaultServer()
po := et.po
//to := reflect.TypeOf(po)
vo := reflect.ValueOf(po)
for _, f := range dto.Values {
fo := vo.FieldByName(f.FieldName)
fo.Set(reflect.ValueOf(f.Value))
}
return et
}
func (et *Server) ToPo() *model.Server {
return et.po
}
func (et *Server) ToCommonDto() *dto.CommonDtoValues {
obj := &dto.CommonDtoValues{}
to := reflect.TypeOf(et.po).Elem()
vo := reflect.ValueOf(et.po).Elem()
for i := 0; i < vo.NumField(); i++ {
ft := to.Field(i)
fo := vo.Field(i)
f1 := &dto.CommonDtoValue{
FieldName: ft.Name,
Value: fo.Interface(),
}
obj.Values = append(obj.Values, f1)
}
return obj
}
func (et *Server) GetDtoFieldsDescInfo() []*dto.CommonDtoFieldDesc {
to := reflect.TypeOf(et.po).Elem()
vo := reflect.ValueOf(et.po).Elem()
obj := make([]*dto.CommonDtoFieldDesc, 0, to.NumField())
for i := 0; i < vo.NumField(); i++ {
ft := to.Field(i)
//fo := vo.Field(i)
f1 := &dto.CommonDtoFieldDesc{
Name: ft.Name,
Key: ft.Name,
Type: ft.Type.Name(),
HelpText: ft.Tag.Get("desc"),
Editable: true,
Require: true,
Choices: make([]*dto.CommonDtoFieldChoice, 0),
MultiChoice: false,
}
obj = append(obj, f1)
}
return obj
}

View File

@ -0,0 +1,31 @@
package domain
import (
"admin/apps/game/model/dto"
"admin/internal/errcode"
)
type IRestfulEntity interface {
ToCommonDto() *dto.CommonDtoValues
}
type IRestfulResourceSvc interface {
List(pageNo, pageLen int) ([]*dto.CommonDtoFieldDesc, []IRestfulEntity, error)
Post(obj *dto.CommonDtoValues) (IRestfulEntity, error)
Put(obj *dto.CommonDtoValues) (IRestfulEntity, error)
Delete(id int) error
}
var restfulResourceSvcMgr = make(map[string]IRestfulResourceSvc)
func registerRestfulSvc(name string, svc IRestfulResourceSvc) {
restfulResourceSvcMgr[name] = svc
}
func FindRestfulResourceSvc(name string) (IRestfulResourceSvc, error) {
svc, find := restfulResourceSvcMgr[name]
if !find {
return nil, errcode.New(errcode.ServerError, "not found %v restful svc", name)
}
return svc, nil
}

View File

@ -0,0 +1,48 @@
package domain
import (
"admin/apps/game/domain/entity"
"admin/apps/game/domain/repo"
"admin/apps/game/model/dto"
"gorm.io/gorm"
)
type ProjectSvc struct {
proRepo repo.IProjectRepo
}
func NewProjectSvc(db *gorm.DB) *ProjectSvc {
svc := &ProjectSvc{
proRepo: repo.NewProjectRepo(db),
}
registerRestfulSvc("project", svc)
return svc
}
func (svc *ProjectSvc) List(pageNo, pageLen int) ([]*dto.CommonDtoFieldDesc, []IRestfulEntity, error) {
entityList, err := svc.proRepo.List(pageNo, pageLen)
if err != nil {
return nil, nil, err
}
iList := make([]IRestfulEntity, 0, len(entityList))
for _, v := range entityList {
iList = append(iList, v)
}
return entity.ProjectDtoFieldsDescInfo, iList, nil
}
func (svc *ProjectSvc) Post(obj *dto.CommonDtoValues) (IRestfulEntity, error) {
et := entity.FromProjectDto(obj)
err := svc.proRepo.Create(et)
return et, err
}
func (svc *ProjectSvc) Put(obj *dto.CommonDtoValues) (IRestfulEntity, error) {
et := entity.FromProjectDto(obj)
err := svc.proRepo.Edit(et)
return et, err
}
func (svc *ProjectSvc) Delete(id int) error {
return svc.proRepo.Delete(id)
}

View File

@ -0,0 +1,77 @@
package repo
import (
"admin/apps/game/domain/entity"
"admin/apps/game/model"
"admin/internal/errcode"
"gorm.io/gorm"
)
type IProjectRepo interface {
List(pageNo, pageLen int) ([]*entity.Project, error)
Create(et *entity.Project) error
Edit(et *entity.Project) error
Delete(id int) error
}
func NewProjectRepo(db *gorm.DB) IProjectRepo {
return newProjectRepoImpl(db)
}
type projectRepoImpl struct {
db *gorm.DB
}
func newProjectRepoImpl(db *gorm.DB) *projectRepoImpl {
return &projectRepoImpl{db: db}
}
func (repo *projectRepoImpl) List(pageNo, pageLen int) ([]*entity.Project, error) {
list := make([]*model.Project, 0)
err := repo.db.Find(&list).Error
if err != nil {
return nil, errcode.New(errcode.DBError, "find project error:%v", err)
}
// debug
list = append(list, &model.Project{
ID: 123,
Name: "神魔大陆",
Desc: "神魔大陆服务器",
ApiAddr: "http://192.168.1.1:8081",
})
entityList := make([]*entity.Project, 0, len(list))
for _, Project := range list {
entityList = append(entityList, entity.FromProjectPo(Project))
}
return entityList, nil
}
func (repo *projectRepoImpl) Create(et *entity.Project) error {
po := et.ToPo()
err := repo.db.Create(po).Error
if err != nil {
return errcode.New(errcode.DBError, "create obj:%+v error:%v", et, err)
}
et.Id = po.ID
return nil
}
func (repo *projectRepoImpl) Edit(et *entity.Project) error {
po := et.ToPo()
err := repo.db.Where("id=?", et.Id).Updates(po).Error
if err != nil {
return errcode.New(errcode.DBError, "edit obj:%+v error:%v", et, err)
}
return nil
}
func (repo *projectRepoImpl) Delete(id int) error {
err := repo.db.Where("id=?", id).Unscoped().Delete(&model.Project{}).Error
if err != nil {
return errcode.New(errcode.DBError, "delete obj:%+v error:%v", id, err)
}
return nil
}

View File

@ -0,0 +1,69 @@
package repo
import (
"admin/apps/game/domain/entity"
"admin/apps/game/model"
"admin/internal/errcode"
"gorm.io/gorm"
)
type IServerRepo interface {
List(pageNo, pageLen int) ([]*entity.Server, error)
Create(et *entity.Server) error
Edit(et *entity.Server) error
Delete(id int) error
}
func NewServerRepo(db *gorm.DB) IServerRepo {
return newServerRepoImpl(db)
}
type ServerRepoImpl struct {
db *gorm.DB
}
func newServerRepoImpl(db *gorm.DB) *ServerRepoImpl {
return &ServerRepoImpl{db: db}
}
func (repo *ServerRepoImpl) List(pageNo, pageLen int) ([]*entity.Server, error) {
list := make([]*model.Server, 0)
err := repo.db.Find(&list).Error
if err != nil {
return nil, errcode.New(errcode.DBError, "find Server error:%v", err)
}
entityList := make([]*entity.Server, 0, len(list))
for _, Server := range list {
entityList = append(entityList, entity.FromServerPo(Server))
}
return entityList, nil
}
func (repo *ServerRepoImpl) Create(et *entity.Server) error {
po := et.ToPo()
err := repo.db.Create(po).Error
if err != nil {
return errcode.New(errcode.DBError, "create obj:%+v error:%v", et, err)
}
et.Id = po.ID
return nil
}
func (repo *ServerRepoImpl) Edit(et *entity.Server) error {
po := et.ToPo()
err := repo.db.Where("id=?", et.Id).Updates(po).Error
if err != nil {
return errcode.New(errcode.DBError, "edit obj:%+v error:%v", et, err)
}
return nil
}
func (repo *ServerRepoImpl) Delete(id int) error {
err := repo.db.Where("id=?", id).Unscoped().Delete(&model.Server{}).Error
if err != nil {
return errcode.New(errcode.DBError, "delete obj:%+v error:%v", id, err)
}
return nil
}

View File

@ -0,0 +1,48 @@
package domain
import (
"admin/apps/game/domain/entity"
"admin/apps/game/domain/repo"
"admin/apps/game/model/dto"
"gorm.io/gorm"
)
type ServerSvc struct {
serverRepo repo.IServerRepo
}
func NewServerSvc(db *gorm.DB) *ServerSvc {
svc := &ServerSvc{
serverRepo: repo.NewServerRepo(db),
}
registerRestfulSvc("server", svc)
return svc
}
func (svc *ServerSvc) List(pageNo, pageLen int) ([]*dto.CommonDtoFieldDesc, []IRestfulEntity, error) {
entityList, err := svc.serverRepo.List(pageNo, pageLen)
if err != nil {
return nil, nil, err
}
iList := make([]IRestfulEntity, 0, len(entityList))
for _, v := range entityList {
iList = append(iList, v)
}
return entity.ServerDtoFieldsDescInfo, iList, nil
}
func (svc *ServerSvc) Post(obj *dto.CommonDtoValues) (IRestfulEntity, error) {
et := entity.FromServerDto(obj)
err := svc.serverRepo.Create(et)
return et, err
}
func (svc *ServerSvc) Put(obj *dto.CommonDtoValues) (IRestfulEntity, error) {
et := entity.FromServerDto(obj)
err := svc.serverRepo.Edit(et)
return et, err
}
func (svc *ServerSvc) Delete(id int) error {
return svc.serverRepo.Delete(id)
}

View File

@ -0,0 +1,35 @@
package dto
type CommonDtoFieldChoice struct {
Desc string `json:"desc"`
Value any `json:"value"`
// 描述选项的类型,例如添加物品时,可以添加道具、翅膀、宠物等,他们可能不一定都设计为道具
Type int `json:"type"`
}
type CommonDtoFieldDesc struct {
Name string `json:"name"`
Key string `json:"key"`
// 字段类型基础类型支持int float string bool []<基础类行>
// 支持自定义类型和自定义类型的数组
Type string `json:"type"`
HelpText string `json:"help_text"`
Editable bool `json:"editable"` // 是否可编辑例如id就不可编辑新增时也不需要填写
Require bool `json:"require"` // 是否必填,不能为空
Choices []*CommonDtoFieldChoice `json:"choices"` // 可选项,用于字段做下拉框
MultiChoice bool `json:"multi_choice"` // 是否多选
}
type CommonDtoValue struct {
FieldName string `json:"field_name"`
Value any `json:"value"`
}
type CommonDtoValues struct {
Values []*CommonDtoValue `json:"values"`
}
type CommonDtoList struct {
FieldsDesc []*CommonDtoFieldDesc `json:"fields_desc"` // 数据字段描述信息
Rows []*CommonDtoValues `json:"rows"` // 数据行
}

View File

@ -0,0 +1,18 @@
package dto
type CommonListReq struct {
PageNo int `json:"page_no"`
PageLen int `json:"page_len"`
}
type CommonPostReq struct {
Dto *CommonDtoValues `json:"dto"`
}
type CommonPutReq struct {
Dto *CommonDtoValues `json:"dto"`
}
type CommonDeleteReq struct {
Id int `json:"id"`
}

View File

@ -0,0 +1,3 @@
package dto
type CommonListRsp = CommonDtoList

View File

@ -0,0 +1,28 @@
package model
import (
"admin/internal/db"
"gorm.io/gorm"
"time"
)
func init() {
db.RegisterTableModels(Project{})
}
// Project 游戏项目,例如神谕、神魔大陆
type Project struct {
ID int `gorm:"primarykey"`
Name string `gorm:"primarykey"`
Desc string
// command_list接口服务器地址为空代表由由项目下各个逻辑服提供command_list.
// 取决于每个项目改造难度:
// 不为空就代表项目要实现一个自己统一对外暴露的gm调用服务对内聚合、分发指令执行本后台执行指令只调用一次
// 为空就代表command_list实现在各个逻辑服由本后台系统在执行指令时聚合、分发指令
// 调用各个逻辑服执行,本后台执行指令需要根据逻辑服数量调用;
ApiAddr string //
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}

View File

@ -0,0 +1,28 @@
package model
import (
"admin/internal/db"
"gorm.io/gorm"
"time"
)
func init() {
db.RegisterTableModels(Server{})
}
// Server 逻辑服
type Server struct {
ID int `gorm:"primarykey"`
ServerConfID string `gorm:"primarykey"`
Desc string
// command_list接口服务器地址为空代表由由项目统一提供command_list.
// 取决于每个项目改造难度:
// 为空就代表项目要实现一个自己统一对外暴露的gm调用服务对内聚合、分发指令执行本后台执行指令只调用一次
// 不为空就代表command_list实现在各个逻辑服由本后台系统在执行指令时聚合、分发指令
// 调用各个逻辑服执行,本后台执行指令需要根据逻辑服数量调用;
ApiAddr string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}

View File

@ -0,0 +1,11 @@
package server
import "admin/apps/game/service"
type controller struct {
svc *service.Service
}
func newController(svc *service.Service) *controller {
return &controller{svc: svc}
}

View File

@ -0,0 +1,42 @@
package server
import (
"admin/apps/game/model/dto"
"admin/internal/context"
)
func (ctl *controller) CommonList(ctx *context.WebContext, restfulResourceName string, params *dto.CommonListReq) {
list, err := ctl.svc.CommonList(ctx, restfulResourceName, params)
if err != nil {
ctx.Fail(err)
return
}
ctx.Ok(list)
}
func (ctl *controller) CommonPost(ctx *context.WebContext, restfulResourceName string, params *dto.CommonDtoValues) {
newObj, err := ctl.svc.CommonPost(ctx, restfulResourceName, params)
if err != nil {
ctx.Fail(err)
return
}
ctx.Ok(newObj)
}
func (ctl *controller) CommonPut(ctx *context.WebContext, restfulResourceName string, params *dto.CommonDtoValues) {
newObj, err := ctl.svc.CommonPut(ctx, restfulResourceName, params)
if err != nil {
ctx.Fail(err)
return
}
ctx.Ok(newObj)
}
func (ctl *controller) CommonDelete(ctx *context.WebContext, restfulResourceName string, id int) {
err := ctl.svc.CommonDelete(ctx, restfulResourceName, id)
if err != nil {
ctx.Fail(err)
return
}
ctx.Ok(nil)
}

View File

@ -0,0 +1,57 @@
package server
import (
"admin/apps/game/model/dto"
"admin/internal/context"
"admin/lib/web"
)
func (srv *Server) Route(engine *web.Engine) {
apiGroup := engine.Group("/api")
srv.proRoute(apiGroup)
srv.serverRoute(apiGroup)
}
func (srv *Server) proRoute(engine *web.Group) {
resourceName := "project"
proGroup := engine.Group("/" + resourceName)
proGroup.Get("", "获取项目列表", web.AccessMode_Read, dto.CommonListReq{}, commonHandlerList(srv.ctl, resourceName))
proGroup.Post("", "新增项目", web.AccessMode_Read, dto.CommonPostReq{}, commonHandlerList(srv.ctl, resourceName))
proGroup.Put("", "修改项目", web.AccessMode_Read, dto.CommonPutReq{}, commonHandlerList(srv.ctl, resourceName))
proGroup.Delete("", "删除项目", web.AccessMode_Read, dto.CommonDeleteReq{}, commonHandlerList(srv.ctl, resourceName))
}
func (srv *Server) serverRoute(engine *web.Group) {
resourceName := "server"
proGroup := engine.Group("/" + resourceName)
proGroup.Get("", "获取服务器列表", web.AccessMode_Read, dto.CommonListReq{}, commonHandlerList(srv.ctl, resourceName))
proGroup.Post("", "新增服务器", web.AccessMode_Read, dto.CommonPostReq{}, commonHandlerList(srv.ctl, resourceName))
proGroup.Put("", "修改服务器", web.AccessMode_Read, dto.CommonPutReq{}, commonHandlerList(srv.ctl, resourceName))
proGroup.Delete("", "删除服务器", web.AccessMode_Read, dto.CommonDeleteReq{}, commonHandlerList(srv.ctl, resourceName))
}
func commonHandlerList(ctl *controller, resourceName string) func(ctx *context.WebContext, params *dto.CommonListReq) {
return func(ctx *context.WebContext, params *dto.CommonListReq) {
ctl.CommonList(ctx, resourceName, params)
}
}
func commonHandlerPost(ctl *controller, resourceName string) func(ctx *context.WebContext, params *dto.CommonPostReq) {
return func(ctx *context.WebContext, params *dto.CommonPostReq) {
ctl.CommonPost(ctx, resourceName, params.Dto)
}
}
func commonHandlerPut(ctl *controller, resourceName string) func(ctx *context.WebContext, params *dto.CommonPutReq) {
return func(ctx *context.WebContext, params *dto.CommonPutReq) {
ctl.CommonPut(ctx, resourceName, params.Dto)
}
}
func commonHandlerDelete(ctl *controller, resourceName string) func(ctx *context.WebContext, params *dto.CommonDeleteReq) {
return func(ctx *context.WebContext, params *dto.CommonDeleteReq) {
ctl.CommonDelete(ctx, resourceName, params.Id)
}
}

View File

@ -0,0 +1,15 @@
package server
import "admin/apps/game/service"
type Server struct {
svc *service.Service
ctl *controller
}
func New(svc *service.Service) *Server {
return &Server{
svc: svc,
ctl: newController(svc),
}
}

View File

@ -0,0 +1,68 @@
package service
import (
"admin/apps/game/domain"
"admin/apps/game/model/dto"
"context"
"gorm.io/gorm"
)
type Service struct {
db *gorm.DB
projectSvc *domain.ProjectSvc
serverSvc *domain.ServerSvc
}
func NewCmdServerSvc(db *gorm.DB) *Service {
return &Service{
db: db,
projectSvc: domain.NewProjectSvc(db),
serverSvc: domain.NewServerSvc(db),
}
}
func (svc *Service) CommonList(ctx context.Context, resourceName string, params *dto.CommonListReq) (*dto.CommonDtoList, error) {
restfulDomainSvc, err := domain.FindRestfulResourceSvc(resourceName)
if err != nil {
return nil, err
}
dtoFieldsDescInfo, list, err := restfulDomainSvc.List(params.PageNo, params.PageLen)
if err != nil {
return nil, err
}
retList := make([]*dto.CommonDtoValues, 0, len(list))
for _, v := range list {
retList = append(retList, v.ToCommonDto())
}
return &dto.CommonDtoList{FieldsDesc: dtoFieldsDescInfo, Rows: retList}, nil
}
func (svc *Service) CommonPost(ctx context.Context, resourceName string, params *dto.CommonDtoValues) (*dto.CommonDtoValues, error) {
restfulDomainSvc, err := domain.FindRestfulResourceSvc(resourceName)
if err != nil {
return nil, err
}
et, err := restfulDomainSvc.Post(params)
if err != nil {
return nil, err
}
return et.ToCommonDto(), nil
}
func (svc *Service) CommonPut(ctx context.Context, resourceName string, params *dto.CommonDtoValues) (*dto.CommonDtoValues, error) {
restfulDomainSvc, err := domain.FindRestfulResourceSvc(resourceName)
if err != nil {
return nil, err
}
et, err := restfulDomainSvc.Put(params)
if err != nil {
return nil, err
}
return et.ToCommonDto(), nil
}
func (svc *Service) CommonDelete(ctx context.Context, resourceName string, id int) error {
restfulDomainSvc, err := domain.FindRestfulResourceSvc(resourceName)
if err != nil {
return err
}
return restfulDomainSvc.Delete(id)
}

18
admin/apps/user/boot.go Normal file
View File

@ -0,0 +1,18 @@
package user
import "admin/lib/node"
func initFun(app *node.Application) error {
return nil
}
func New() *node.ApplicationDescInfo {
app := node.NewApplicationDescInfo("user", initFun)
return app
}
func must(err error) {
if err != nil {
panic(err)
}
}

View File

@ -0,0 +1,26 @@
package main
import (
"admin/apps/game"
"admin/internal/mynode"
"admin/lib/node"
)
var appList []*node.ApplicationDescInfo
func init() {
appList = []*node.ApplicationDescInfo{
game.New(),
}
}
func main() {
nd := mynode.New()
for _, app := range appList {
nd.WithApp(app)
}
err := nd.Run()
if err != nil {
panic(err)
}
}

53
admin/go.mod Normal file
View File

@ -0,0 +1,53 @@
module admin
go 1.24.2
require (
github.com/gin-contrib/pprof v1.5.3
github.com/gin-gonic/gin v1.10.0
github.com/go-sql-driver/mysql v1.7.0
github.com/prometheus/client_golang v1.22.0
github.com/rs/zerolog v1.34.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)

139
admin/go.sum Normal file
View File

@ -0,0 +1,139 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/pprof v1.5.3 h1:Bj5SxJ3kQDVez/s/+f9+meedJIqLS+xlkIVDe/lcvgM=
github.com/gin-contrib/pprof v1.5.3/go.mod h1:0+LQSZ4SLO0B6+2n6JBzaEygpTBxe/nI+YEYpfQQ6xY=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@ -0,0 +1,10 @@
package config
type CommonBootFlags struct {
ApiPort string `env:"api_port" default:"8080" desc:"api端口客户端请求的地址端口"`
DBType string `env:"db_type" default:"sqlite3" desc:"数据库类型默认sqlite可选sqlite|mysql|pg"`
DBAddr string `env:"db_addr" default:"localhost" desc:"数据库地址"`
DBName string `env:"db_name" default:"uniugm" desc:"数据库名字"`
DBUser string `env:"db_user" default:"root" desc:"数据库用户名"`
DBPass string `env:"db_pass" default:"" desc:"数据库密码"`
}

View File

@ -0,0 +1 @@
package config

View File

@ -0,0 +1,43 @@
package context
import (
"admin/internal/errcode"
"admin/lib/web"
"context"
)
type WebContext struct {
context.Context
rawCtx web.RawContext
}
func NewWebContext() web.Context {
return &WebContext{}
}
func (ctx *WebContext) Ok(data any) {
ctx.rawCtx.Json(200, map[string]any{
"code": 200,
"msg": "",
"data": data,
})
}
func (ctx *WebContext) Fail(err error) {
code, stack, msg := errcode.ParseError(err)
ctx.rawCtx.Json(200, map[string]any{
"code": code,
"stack": stack,
"msg": msg,
"data": "",
})
}
func (ctx *WebContext) SetRawContext(rawCtx web.RawContext) {
ctx.Context = context.Background()
ctx.rawCtx = rawCtx
}
func (ctx *WebContext) GetRawContext() web.RawContext {
return ctx.rawCtx
}

142
admin/internal/db/db.go Normal file
View File

@ -0,0 +1,142 @@
package db
import (
"admin/internal/errcode"
"admin/internal/global"
"admin/lib/xlog"
"fmt"
mysqlDriver "github.com/go-sql-driver/mysql"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"sync"
"time"
)
var (
globalTables []any
locker sync.Mutex
)
func RegisterTableModels(models ...any) {
locker.Lock()
defer locker.Unlock()
globalTables = append(globalTables, models...)
}
func NewDB(dbType, dbAddr, dbName, dbUser, dbPass string) (db *gorm.DB, err error) {
switch dbType {
case "sqlite":
db, err = gorm.Open(sqlite.Open(dbName+".db"), &gorm.Config{})
if err != nil {
return nil, err
}
case "mysql":
dsn := fmt.Sprintf("%v:%v@tcp(%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", dbUser, dbPass, dbAddr, dbName)
dsnWithoutDB := fmt.Sprintf("%v:%v@tcp(%v)/?charset=utf8mb4&parseTime=True&loc=Local", dbUser, dbPass, dbAddr)
db, err = createDBAndGuaranteeMigrate(dsnWithoutDB, dsn, globalTables)
}
global.GLOB_DB = db
return db, nil
}
func createDBAndGuaranteeMigrate(dsnWithoutDb, dsn string, tables []any) (*gorm.DB, error) {
mysqlDriverConf, err := mysqlDriver.ParseDSN(dsn)
if err != nil {
return nil, fmt.Errorf("parse dsn:%v error:%v", dsn, err)
}
dbName := mysqlDriverConf.DBName
_, err = tryCreateDB(dbName, dsnWithoutDb)
if err != nil {
xlog.Fatalf(err)
return nil, err
}
driverConf := mysql.Config{
DSN: dsn,
DontSupportRenameColumn: true,
//SkipInitializeWithVersion: false, // 根据数据库版本自动配置
}
dialector := mysql.New(driverConf)
//slowLogger := logger.New(
// syslog.New(xlog.GetGlobalWriter(), "\n", syslog.LstdFlags),
// logger.Config{
// // 设定慢查询时间阈值为 默认值200 * time.Millisecond
// SlowThreshold: 200 * time.Millisecond,
// // 设置日志级别
// LogLevel: logger.Warn,
// Colorful: true,
// },
//)
db, err := gorm.Open(dialector, &gorm.Config{
Logger: &gormLogger{},
PrepareStmt: false, // 关闭缓存sql语句功能因为后续use db会报错这个缓存会无限存储可能导致内存泄露
//SkipDefaultTransaction: true, // 跳过默认事务
})
if err != nil {
return nil, fmt.Errorf("failed to connect to mysql:%v", err)
}
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(50)
sqlDB.SetConnMaxIdleTime(time.Minute * 5)
sqlDB.SetConnMaxLifetime(time.Minute * 10)
if len(tables) > 0 {
err = autoMigrate(db, tables...)
if err != nil {
xlog.Fatalf(err)
return nil, fmt.Errorf("automigrate error:%v", err)
}
}
//addMetricsCollection(db, strconv.Itoa(int(serverId)), dbName)
return db, nil
}
func tryCreateDB(dbName, dsn string) (string, error) {
driverConf := mysql.Config{
DSN: dsn,
DontSupportRenameColumn: true,
//SkipInitializeWithVersion: false, // 根据数据库版本自动配置
}
dialector := mysql.New(driverConf)
db, err := gorm.Open(dialector, &gorm.Config{
PrepareStmt: false, // 关闭缓存sql语句功能因为后续use db会报错这个缓存会无限存储可能导致内存泄露
//SkipDefaultTransaction: true, // 跳过默认事务
})
if err != nil {
return "", fmt.Errorf("failed to connect to mysql:%v", err)
}
// 检查数据库是否存在
var count int
db.Raw("SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = ?", dbName).Scan(&count)
if count == 0 {
// 数据库不存在,创建它
sql := fmt.Sprintf(`create database if not exists %s default charset utf8mb4 collate utf8mb4_unicode_ci`,
dbName)
if e := db.Exec(sql).Error; e != nil {
return "", fmt.Errorf("failed to create database:%v", e)
}
}
sqlDb, _ := db.DB()
sqlDb.Close()
return dbName, nil
}
func autoMigrate(db *gorm.DB, tables ...interface{}) error {
// 这个函数是在InitConn之后调用的
// 初始化表
if err := db.AutoMigrate(tables...); err != nil {
return errcode.New(errcode.DBError, "failed to init tables", err)
}
return nil
}

View File

@ -0,0 +1,99 @@
package db
import (
"admin/lib/xlog"
"context"
"fmt"
"gorm.io/gorm/logger"
"strings"
"time"
)
type gormLogger struct {
}
func (l *gormLogger) LogMode(logger.LogLevel) logger.Interface {
return l
}
func (l *gormLogger) Info(ctx context.Context, format string, args ...interface{}) {
xlog.Infof(fmt.Sprintf("[GORM LOGGER] "+format, args...))
}
func (l *gormLogger) Warn(ctx context.Context, format string, args ...interface{}) {
xlog.Warnf(fmt.Sprintf("[GORM LOGGER] "+format, args...))
}
func (l *gormLogger) Error(ctx context.Context, format string, args ...interface{}) {
xlog.Errorf(fmt.Sprintf("[GORM LOGGER] "+format, args...))
}
func (l *gormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
sqlStr, aff := fc()
if strings.Contains(sqlStr, "SHOW STATUS") {
return
}
if err != nil {
xlog.Errorf("[GORM LOGGER] "+"sql:%v affected:%v error:%v", sqlStr, aff, err)
cmd, table := getSqlCmdAndTable(sqlStr)
if cmd == "" || table == "" {
return
}
//metricsMysqlErrorCount.LabelValues(cmd, table).Add(1)
} else {
dura := time.Now().Sub(begin)
if dura.Milliseconds() > 2000 {
xlog.Warnf("[GORM LOGGER] [SLOW] "+"sql:%v affected:%v use %vms", sqlStr, aff, dura.Milliseconds())
} else {
xlog.Tracef("[GORM LOGGER] "+"sql:%v affected:%v", sqlStr, aff)
}
}
}
func getSqlCmdAndTable(sql string) (string, string) {
cmd := ""
table := ""
idx := strings.IndexAny(sql, " ")
if idx <= 0 {
cmd = sql
} else {
cmd = sql[:idx]
}
switch cmd {
case "SELECT":
if idx := strings.Index(sql, "FROM"); idx >= 0 {
for i := idx + 5; i < len(sql); i++ {
if sql[i] == ' ' {
break
}
table += string(sql[i])
}
}
case "UPDATE":
if idx := strings.Index(sql, "UPDATE"); idx >= 0 {
for i := idx + 7; i < len(sql); i++ {
if sql[i] == ' ' {
break
}
table += string(sql[i])
}
}
case "INSERT":
if idx := strings.Index(sql, "INTO"); idx >= 0 {
for i := idx + 5; i < len(sql); i++ {
if sql[i] == ' ' || sql[i] == '(' {
break
}
table += string(sql[i])
}
}
case "DELETE":
if idx := strings.Index(sql, "FROM"); idx >= 0 {
for i := idx + 5; i < len(sql); i++ {
if sql[i] == ' ' {
break
}
table += string(sql[i])
}
}
default:
}
return cmd, table
}

View File

@ -0,0 +1,7 @@
package errcode
const (
Ok = 0
ServerError = 1 // 服务器错误
DBError = 2 // 数据库错误
)

View File

@ -0,0 +1,53 @@
package errcode
import (
"fmt"
"runtime"
"strconv"
"strings"
)
type errorWithCode struct {
code int
stack string
msg string
}
func newErrorWithCode(code int, stack, msg string) *errorWithCode {
return &errorWithCode{
code: code,
stack: stack,
msg: msg,
}
}
func (e *errorWithCode) Error() string {
return fmt.Sprintf("[stack ==> %v] code:%v msg:%v", e.stack, e.code, e.msg)
}
func New(code int, format string, args ...interface{}) error {
return newError(code, 2, format, args...)
}
func ParseError(err error) (int, string, string) {
if specErr, ok := err.(*errorWithCode); ok {
return specErr.code, specErr.stack, specErr.msg
}
return ParseError(newError(ServerError, 3, err.Error()))
}
func newError(code int, callDeep int, format string, args ...interface{}) error {
_, caller, line, ok := runtime.Caller(callDeep)
if !ok {
panic(ok)
}
tokens := strings.Split(caller, "/")
if len(tokens) > 5 {
tokens = tokens[len(tokens)-5:]
}
fileInfo := strings.Join(tokens, "/")
err := newErrorWithCode(code, fileInfo+":"+strconv.Itoa(int(line)), fmt.Sprintf(format, args...))
return err
}

View File

@ -0,0 +1,13 @@
package global
import (
"admin/internal/config"
"admin/lib/web"
"gorm.io/gorm"
)
var (
GLOB_BOOT_FLAGS = &config.CommonBootFlags{} // 启动命令行参数
GLOB_DB *gorm.DB // 数据库
GLOB_API_ENGINE *web.Engine // 全局api服务器
)

View File

@ -0,0 +1,38 @@
package mynode
import (
"admin/internal/context"
"admin/internal/db"
"admin/internal/global"
"admin/lib/node"
"admin/lib/web"
"admin/lib/xlog"
)
func New() *node.Node {
nd := node.NewNode()
nd.ApplyOptions(node.WithNodeExBootFlags(global.GLOB_BOOT_FLAGS))
nd.AddInitTask("初始化全局api监听服务", func() error {
global.GLOB_API_ENGINE = web.NewEngine("gin", context.NewWebContext)
return nil
})
nd.AddInitTask("初始化数据库", func() error {
flags := global.GLOB_BOOT_FLAGS
_, err := db.NewDB(flags.DBType, flags.DBAddr, flags.DBName, flags.DBUser, flags.DBPass)
return err
})
nd.AddPostTask("启动全局api监听服务", func() error {
go func() {
err := global.GLOB_API_ENGINE.Run(":" + global.GLOB_BOOT_FLAGS.ApiPort)
if err != nil {
xlog.Errorf("start api server on %v error: %v", global.GLOB_BOOT_FLAGS, err)
}
}()
return nil
})
return nd
}

76
admin/lib/flags/flags.go Normal file
View File

@ -0,0 +1,76 @@
package flags
import (
"flag"
"fmt"
"os"
"reflect"
"strconv"
"unsafe"
)
// ParseWithStructPointers 启动参数解析如果启动参数没有指定会去env里查找同名参数
// flagStructPointers为结构体指针数组
func ParseWithStructPointers(flagStructPointers ...interface{}) {
for _, st := range flagStructPointers {
flagParseStruct2Flags(st)
}
flag.Parse()
}
func flagParseStruct2Flags(st interface{}) {
if st == nil {
return
}
var stTo = reflect.TypeOf(st)
var stVo = reflect.ValueOf(st)
switch stTo.Kind() {
case reflect.Ptr:
stTo = stTo.Elem()
stVo = stVo.Elem()
// case reflect.Struct:
// break
default:
panic(fmt.Errorf("invalid flags parse struct(%+v), must be pointer or struct", st))
}
for i := 0; i < stTo.NumField(); i++ {
field := stTo.Field(i)
key, find := field.Tag.Lookup("env")
if !find {
continue
}
desc := field.Tag.Get("desc")
defaultValue, find := os.LookupEnv(key)
if !find {
defaultValue, find = field.Tag.Lookup("default")
if !find {
defaultValue = ""
}
}
var fieldValuePointer = unsafe.Pointer(stVo.Field(i).Addr().Pointer())
switch field.Type.Kind() {
case reflect.String:
flag.StringVar((*string)(fieldValuePointer), key, defaultValue, desc)
case reflect.Int:
defaultValue1, _ := strconv.Atoi(defaultValue)
flag.IntVar((*int)(fieldValuePointer), key, defaultValue1, desc)
case reflect.Int64:
defaultValue1, _ := strconv.ParseInt(defaultValue, 10, 64)
flag.Int64Var((*int64)(fieldValuePointer), key, defaultValue1, desc)
case reflect.Bool:
flag.BoolVar((*bool)(fieldValuePointer), key, defaultValue == "true", desc)
default:
panic(fmt.Errorf("parse flag kind invalid,must be string/int/int64/bool, not %+v", field.Type.Kind()))
}
}
return
}

216
admin/lib/node/app.go Normal file
View File

@ -0,0 +1,216 @@
package node
import (
"admin/lib/xlog"
"fmt"
"github.com/gin-gonic/gin"
)
// Task 不会永久执行的任务串行用于启动前初始化或者启动后初始化工作返回error就停止application
type Task func() error
// Worker 永久执行的工作协程一旦停止就停止application
type Worker func() error
// Job 不会永久执行的任务,且不关心执行结果,不关心执行顺序,例如内存预热等
type Job func()
type pair struct {
key any
value any
}
// Application 受scheduler调度的最小逻辑单元有独立的启动参数、各种串行、并行任务
type Application struct {
Name string
bootFlag interface{}
initializeTasks []pair // 启动服务前串行执行初始化任务的job
services []pair // rpc服务
servers []pair // web服务
postRunTasks []pair // 启动后串行执行的job
postRunWorkers []pair // 启动后后台永久执行的工作协程一旦推出就停止application
parallelJobs []pair // 启动services、servers后并行执行的任务不关心结果例如内存数据的预热等
stopTasks []pair
}
// newApp
func newApp(name string, options ...AppOption) *Application {
app := new(Application)
app.Name = name
app.applyOptions(options...)
return app
}
// WithInitializeTask app完成init之后run之前执行的任务可以用来初始化某些业务或者检查配置等
func (app *Application) WithInitializeTask(desc string, task Task) *Application {
if task == nil {
return app
}
app.initializeTasks = append(app.initializeTasks, pair{desc, task})
return app
}
// WithServer 添加web服务器
func (app *Application) WithServer(desc string, addr string) *gin.Engine {
server := gin.Default()
app.servers = append(app.servers, pair{desc, pair{addr, server}})
return server
}
// WithService 添加rpc服务
//func (app *Application) WithService(desc string, service *joyservice.ServicesManager) *Application {
// if service == nil {
// return app
// }
// app.services = append(app.services, pair{desc, service})
// return app
//}
// WithPostTask app run之后执行的任务一般做临时检查任务可以用来服务启动后加载数据检查等
func (app *Application) WithPostTask(desc string, task Task) *Application {
if task == nil {
return nil
}
app.postRunTasks = append(app.postRunTasks, pair{desc, task})
return app
}
// WithPostWorker 完成post task之后执行的后台任务报错退出等app也会退出一般做永久的关键后台逻辑
func (app *Application) WithPostWorker(desc string, worker Worker) *Application {
if worker == nil {
return app
}
app.postRunWorkers = append(app.postRunWorkers, pair{desc, worker})
return app
}
// WithParallelJob 完成post task之后执行的并行后台任务一般做永久的不关键后台逻辑例如内存预热等
func (app *Application) WithParallelJob(desc string, job Job) *Application {
if job == nil {
return app
}
app.parallelJobs = append(app.parallelJobs, pair{desc, job})
return app
}
// WithStopTask 注册停服逻辑只能处理正常停服异常例如panic、oom会监听不到
func (app *Application) WithStopTask(desc string, task Task) *Application {
if task == nil {
return app
}
app.stopTasks = append(app.stopTasks, pair{desc, task})
return app
}
func (app *Application) GetBootConfig() any {
return app.bootFlag
}
func (app *Application) applyOptions(options ...AppOption) *Application {
for _, option := range options {
option.Apply(app)
}
return app
}
func (app *Application) run() (err error) {
waitChan := make(chan error, 1)
// 启动前的初始化任务
for _, j := range app.initializeTasks {
curErr := j.value.(Task)()
if curErr != nil {
err = fmt.Errorf("run initialize task(%s) return error:%v", j.key, curErr)
return
}
}
// 启动rpc服务
//for _, pair := range app.services {
// go func(desc string, s *joyservice.ServicesManager) {
// jlog.Noticef("app %v service %v will listen on %v", app.Name, desc, s.Addr)
// curErr := s.Run()
// if curErr != nil {
// waitChan <- fmt.Errorf("service %s run on %v error:%v", desc, s.Addr, curErr)
// } else {
//
// }
// }(pair.desc, pair.item.(*joyservice.ServicesManager))
//}
//defer app.stopServices()
// 启动web服务
for _, server := range app.servers {
go func(desc string, info pair) {
addr := info.key.(string)
engine := info.value.(*gin.Engine)
xlog.Noticef("app %v server %v will listen on %v", app.Name, desc, addr)
err := engine.Run(addr)
if err != nil {
waitChan <- fmt.Errorf("server %s error:%v", desc, err)
} else {
}
}(server.key.(string), server.value.(pair))
}
//defer app.stopServers()
// 启动后串行执行的job
for _, j := range app.postRunTasks {
curErr := j.value.(Task)()
if curErr != nil {
err = fmt.Errorf("run post task %s return error:%v", j.key, curErr)
return
}
}
// 启动后串行执行的工作协程
for _, worker := range app.postRunWorkers {
go func(desc string, g Worker) {
curErr := g()
if curErr != nil {
waitChan <- fmt.Errorf("run post worker %s return error:%v", desc, curErr)
}
}(worker.key.(string), worker.value.(Worker))
}
// 启动后的并行job
for _, j := range app.parallelJobs {
go j.value.(Job)()
}
xlog.Noticef("application[%v] run ok.", app.Name)
select {
case anyErr := <-waitChan:
xlog.Critif("scheduler stop with execute error:%v", anyErr)
return anyErr
}
}
func (app *Application) stop() {
for _, task := range app.stopTasks {
err := task.value.(Task)()
if err != nil {
xlog.Errorf("app stop, execute %v error:%v", task.key, err)
} else {
xlog.Infof("app %v stop, execute task:%v", app.Name, task.key)
}
}
//app.stopServices()
//app.stopServers()
}
//func (app *Application) stopServers() {
// for _, s := range app.servers {
// s.item.(*web.Engine).Stop()
// }
//}
//func (app *Application) stopServices() {
// for _, s := range app.services {
// s.item.(*joyservice.ServicesManager).Stop()
// }
//}

View File

@ -0,0 +1,22 @@
package node
type AppInitFunc func(app *Application) error
// ApplicationDescInfo 调度器创建app时注入的app描述信息
type ApplicationDescInfo struct {
name string
initFunc AppInitFunc
options []AppOption
}
func NewApplicationDescInfo(name string, initFunc AppInitFunc) *ApplicationDescInfo {
adi := new(ApplicationDescInfo)
adi.name = name
adi.initFunc = initFunc
return adi
}
func (adi *ApplicationDescInfo) WithOptions(options ...AppOption) *ApplicationDescInfo {
adi.options = append(adi.options, options...)
return adi
}

View File

@ -0,0 +1,31 @@
package node
// WithAppBootFlag 设置app的起服参数flags必须为结构体指针
// 只支持string/int/int64/bool四种字段类型例如
//
// type Flags struct {
// F1 string `env:"id" desc:"boot id" default:"default value"`
// F2 int `env:"num" desc:"number" default:"3"`
// }
// WithAppBootFlag(&Flags{})
func WithAppBootFlag(flag interface{}) AppOption {
return appOptionFunction(func(app *Application) {
app.bootFlag = flag
})
}
func WithAppStopTask(desc string, task Task) AppOption {
return appOptionFunction(func(app *Application) {
app.stopTasks = append(app.stopTasks, pair{desc, task})
})
}
type AppOption interface {
Apply(scd *Application)
}
type appOptionFunction func(app *Application)
func (of appOptionFunction) Apply(app *Application) {
of(app)
}

209
admin/lib/node/node.go Normal file
View File

@ -0,0 +1,209 @@
package node
import (
"admin/lib/xlog"
"admin/lib/xos"
"fmt"
"github.com/gin-gonic/gin"
"sync"
"time"
)
type IBootConfigContent interface {
GetLogConfig() *LogBootConfig
OnReload(first bool, oldContent string)
}
// Node 调度器调度多个app
type Node struct {
// app全局启动参数
bootFlags struct {
globalBootFlag *CommonBootFlags
exBootFlag interface{}
}
// app全局配置文件
bootConfigFile struct {
globalBootConfigFileContent IBootConfigContent // app全局配置文件内容结构体指针为空没有配置文件解析
globalBootConfigParser func(in []byte, out interface{}) error // 配置文件解析函数默认yaml
}
preInitFuncs []pair // 节点node全局的调用方自定义初始化方法早于节点初始化初始化起服配置、日志后
initFuncs []pair // 节点node全局的调用方自定义初始化方法晚于节点初始化初始化起服配置、日志后、早于各个app初始化
postFuncs []pair // 节点node全局的调用方自定义启动所有app之后执行的方法
adis []*ApplicationDescInfo // app的描述信息列表用来生成apps
apps []*Application // 可绑定多个app
tracer *gin.Engine // app全局监控服务为prometheus、pprof共用
}
// NewNode
func NewNode(appOptions ...NodeOption) *Node {
node := new(Node)
node.bootFlags.globalBootFlag = GlobalBootFlags
node.applyOptions(appOptions...)
return node
}
// AddPreInitTask Node自身初始化任务在启动APP逻辑之前执行
func (node *Node) AddPreInitTask(desc string, task Task) *Node {
node.preInitFuncs = append(node.preInitFuncs, pair{desc, task})
return node
}
// AddInitTask Node自身初始化任务在启动APP逻辑之前执行
func (node *Node) AddInitTask(desc string, task Task) *Node {
node.initFuncs = append(node.initFuncs, pair{desc, task})
return node
}
// AddInitTask Node自身初始化任务在启动APP逻辑之前执行
func (node *Node) AddPostTask(desc string, task Task) *Node {
node.postFuncs = append(node.postFuncs, pair{desc, task})
return node
}
// GetBootFlags 获取节点全局的启动参数信息,即./node -node_id 123 -service_name game的值
func (node *Node) GetBootFlags() *CommonBootFlags {
return node.bootFlags.globalBootFlag
}
// GetExBootFlags 获取调用方自定义的全局启动参数
func (node *Node) GetExBootFlags() interface{} {
return node.bootFlags.exBootFlag
}
// GetBootConfigContent 获取注册启动配置文件app.yaml内容
func (node *Node) GetBootConfigContent() IBootConfigContent {
return node.bootConfigFile.globalBootConfigFileContent
}
func (node *Node) applyOptions(options ...NodeOption) *Node {
for _, option := range options {
option.Apply(node)
}
return node
}
// ApplyOptions 应用一些节点配置项
func (node *Node) ApplyOptions(options ...NodeOption) *Node {
node.applyOptions(options...)
return node
}
// WithApp 节点注入app
func (node *Node) WithApp(appDescInfoList ...*ApplicationDescInfo) *Node {
for _, adi := range appDescInfoList {
node.withAppDescInfo(adi)
}
return node
}
func (node *Node) Run() error {
// 初始化node
node.initialize()
defer xlog.CatchWithInfo("run node panic")
// 启动调度器
type waitInfo struct {
desc string
app *Application
err error
}
waitChan := make(chan waitInfo, 1)
// 运行trace server
go func() {
tracerPort := node.bootFlags.globalBootFlag.TracePort
xlog.Noticef("trace server listen on %v", tracerPort)
err := node.tracer.Run(":" + tracerPort)
if err != nil {
waitChan <- waitInfo{"trace server", nil, err}
}
}()
// 初始化各个app
wg := &sync.WaitGroup{}
wg.Add(len(node.apps))
for i, app := range node.apps {
go func(app *Application, initFunc AppInitFunc) {
if initFunc != nil {
err := initFunc(app)
if err != nil {
errInfo := fmt.Errorf("application[%v] init return error[%v]", app.Name, err)
waitChan <- waitInfo{app.Name, app, errInfo}
} else {
xlog.Noticef("application[%v] initialize ok", app.Name)
}
}
wg.Done()
err := app.run()
if err != nil {
// 返回调度器的报错
waitChan <- waitInfo{app.Name, app, err}
}
}(app, node.adis[i].initFunc)
}
// for _, app := range node.apps {
// go func(app *Application) {
// err := app.run()
// if err != nil {
// // 返回调度器的报错
// waitChan <- waitInfo{app.Name, app, err}
// }
// }(app)
// }
wg.Wait()
// 初始化逻辑
for _, j := range node.postFuncs {
curErr := j.value.(Task)()
if curErr != nil {
err := fmt.Errorf("node run post task(%s) return error:%v", j.key, curErr)
panic(err)
}
}
watchSignChan := xos.WatchSignal1()
defer node.Stop()
select {
case signal := <-watchSignChan:
// 优雅停服,监听某些信号
xlog.Noticef("Application receive signal(%v), will graceful stop", signal)
return nil
case errInfo := <-waitChan:
err := fmt.Errorf("Application receive Node(%v) stop with error:%v", errInfo.desc, errInfo.err)
xlog.Errorf(err.Error())
return err
}
}
func (node *Node) Stop() {
for _, app := range node.apps {
app.stop()
}
time.Sleep(3 * time.Second)
}
// WithNode 添加调度器
func (node *Node) withAppDescInfo(adi *ApplicationDescInfo) *Node {
node.adis = append(node.adis, adi)
return node
}
func (node *Node) initApps() error {
for i, app := range node.apps {
if node.adis[i].initFunc != nil {
err := node.adis[i].initFunc(app)
if err != nil {
return fmt.Errorf("application[%v] init return error[%v]", app.Name, err)
} else {
xlog.Noticef("application[%v] initialize ok", app.Name)
}
}
}
return nil
}

View File

@ -0,0 +1,10 @@
package node
var GlobalBootFlags = &CommonBootFlags{}
type CommonBootFlags struct {
NodeID string `env:"node_id" desc:"【必填】节点id进程id分布式环境下唯一标识没指定就是随机、k8s环境下用pod名字作为id"`
ServiceName string `env:"service_name" desc:"【可选】节点服务名标识节点服务种类例如game、login用于rpc服务名调用、日志服务种类输出等"`
TracePort string `env:"trace_port" default:"7788" desc:"监控服务端口暴露prometheus指标、pprof采集等"`
BootConfig string `env:"boot_config" default:"config/boot.yaml" desc:"启动配置文件路径,默认\"config/boot.yaml\""`
}

201
admin/lib/node/node_init.go Normal file
View File

@ -0,0 +1,201 @@
/**
* @Author: likun
* @Title: todo
* @Description: todo
* @File: node_init
* @Date: 2024-11-08 14:02:42
*/
package node
import (
"admin/lib/flags"
"admin/lib/prom"
"admin/lib/xlog"
"admin/lib/xlog/handler"
"encoding/json"
"fmt"
"github.com/rs/zerolog"
"gopkg.in/yaml.v3"
"io"
"os"
"time"
)
// initialize 初始化app
func (node *Node) initialize() {
// 初始化节点启动参数,例如 ./app -a a -b b -c 123
must(node.initBootFlags())
// 初始化节点启动配置文件例如config/app.yaml
must(node.initBootConfig())
// 初始化逻辑
for _, j := range node.preInitFuncs {
curErr := j.value.(Task)()
if curErr != nil {
err := fmt.Errorf("node run pre_initialize task(%s) return error:%v", j.key, curErr)
panic(err)
}
}
// 初始化程序日志系统
must(node.initLog())
// 初始化prometheus metrics、go pprof
must(node.initTracer())
// 初始化逻辑
for _, j := range node.initFuncs {
curErr := j.value.(Task)()
if curErr != nil {
err := fmt.Errorf("node run initialize task(%s) return error:%v", j.key, curErr)
panic(err)
}
}
return
}
func (node *Node) initBootFlags() error {
var appBootFlags []interface{}
for _, v := range node.adis {
curApp := newApp(v.name, v.options...)
node.apps = append(node.apps, curApp)
if curApp.bootFlag == nil {
continue
}
appBootFlags = append(appBootFlags, curApp.bootFlag)
}
// 解析启动参数
flags.ParseWithStructPointers(append([]interface{}{node.bootFlags.globalBootFlag, node.bootFlags.exBootFlag}, appBootFlags...)...)
flagBin, _ := json.Marshal(node.bootFlags.globalBootFlag)
xlog.Noticef("os args:%+v, parsed flags:%v", os.Args, string(flagBin))
// check
if node.bootFlags.globalBootFlag.ServiceName == "" {
if len(node.adis) > 1 {
node.bootFlags.globalBootFlag.ServiceName = "all_in_one"
} else {
node.bootFlags.globalBootFlag.ServiceName = node.adis[0].name
}
}
return nil
}
func (node *Node) initBootConfig() error {
// 解析配置文件
if node.bootConfigFile.globalBootConfigFileContent != nil && node.bootFlags.globalBootFlag.BootConfig != "" {
err := node.parseWithConfigFileContent(true)
if err != nil {
newErr := fmt.Errorf("parse With Config File Content is error:%v", err)
return newErr
}
// 定时解析配置文件
ticker := time.NewTicker(time.Second * 3)
go func() {
for range ticker.C {
// 解析配置文件
err = node.parseWithConfigFileContent(false)
if err != nil {
xlog.Errorf("parse With Config File Content is error:%v", err)
continue
}
}
}()
}
return nil
}
func (node *Node) initLog() error {
logConfig := &LogBootConfig{}
if node.bootConfigFile.globalBootConfigFileContent != nil {
logConfig = node.bootConfigFile.globalBootConfigFileContent.GetLogConfig().Check()
}
var (
enableLogFile = logConfig.EnableFile
enableLogStdout = logConfig.EnableStdout
logDir = logConfig.LogDir
logLevel = logConfig.LogLevel
nodeId = node.bootFlags.globalBootFlag.NodeID
serviceName = node.bootFlags.globalBootFlag.ServiceName
maxLogFileBytesSize = logConfig.LogFileSize * 1024 * 1024 * 1024
maxLogRotateBackupCount = logConfig.LogFileRotateCount
)
// 初始化日志系统
var logHandlers []io.Writer
if !enableLogFile {
// 没有指定日志输出目录,默认输出到控制台
logHandlers = append(logHandlers, os.Stdout)
} else {
// 指定日志输出目录
logHandler, err := handler.NewRotatingDayMaxFileHandler(logDir, serviceName, maxLogFileBytesSize, maxLogRotateBackupCount)
if err != nil {
newErr := fmt.Errorf("new xlog file handler with path [%v] name[%v] error:%v", logDir, serviceName, err)
return newErr
}
logHandlers = append(logHandlers, logHandler)
// 也指定输出到控制台
if enableLogStdout {
logHandlers = append(logHandlers, os.Stdout)
}
}
// 创建logger
logLevelEnum := xlog.ParseLogLevelString(logLevel)
xlog.NewGlobalLogger(logHandlers, logLevelEnum, func(l zerolog.Logger) zerolog.Logger {
return l.With().Str("service", serviceName).Str("node_id", nodeId).Logger()
})
return nil
}
func (node *Node) initTracer() error {
node.tracer = prom.NewEngine(true)
return nil
}
// 解析配置文件
func (node *Node) parseWithConfigFileContent(first bool) error {
content, err := os.ReadFile(node.bootFlags.globalBootFlag.BootConfig)
if err != nil {
newErr := fmt.Errorf("load boot config file %v error:%v", node.bootFlags.globalBootFlag.BootConfig, err)
return newErr
}
if node.bootConfigFile.globalBootConfigParser == nil {
node.bootConfigFile.globalBootConfigParser = yaml.Unmarshal
}
oldContent, _ := json.Marshal(node.bootConfigFile.globalBootConfigFileContent)
err = node.bootConfigFile.globalBootConfigParser(content, node.bootConfigFile.globalBootConfigFileContent)
if err != nil {
newErr := fmt.Errorf("load boot config file %v content %v ok, but parse content error:%v",
node.bootFlags.globalBootFlag.BootConfig, string(content), err)
return newErr
}
// 读取配置后设置一次日志等级
if !first {
xlog.SetLogLevel(xlog.ParseLogLevelString(node.bootConfigFile.globalBootConfigFileContent.GetLogConfig().LogLevel))
}
node.bootConfigFile.globalBootConfigFileContent.OnReload(first, string(oldContent))
return nil
}
func must(err error) {
if err != nil {
panic(err)
}
}

View File

@ -0,0 +1,37 @@
/**
* @Author: likun
* @Title: todo
* @Description: todo
* @File: node_log
* @Date: 2024-11-08 14:39:33
*/
package node
type LogBootConfig struct {
EnableStdout bool `yaml:"enable_stdout"`
EnableFile bool `yaml:"enable_file"`
LogDir string `yaml:"log_dir"`
LogFileSize int `yaml:"log_file_size"` // 单个日志文件最大容量单位G
LogFileRotateCount int `yaml:"log_file_rotate_count"` // 日志文件最大滚动次数
LogLevel string `yaml:"log_level"` // 日志等级: trace/debug/info/notice/warn/error/fatal
}
func (lc *LogBootConfig) Check() *LogBootConfig {
if lc.LogDir == "" {
lc.LogDir = "./logs"
}
if lc.LogFileSize == 0 {
lc.LogFileSize = 1024 * 1024 * 1024 * 5
}
if lc.LogFileRotateCount == 0 {
lc.LogFileRotateCount = 10
}
if lc.LogLevel == "" {
lc.LogLevel = "debug"
}
return lc
}

View File

@ -0,0 +1,46 @@
package node
// WithNodeBootFlags 设置启动参数解析结构
//func WithNodeBootFlags(f *CommonBootFlags) NodeOption {
// return nodeOptionFun(func(node *Node) {
// node.globalBootFlag = f
// })
//}
// WithNodeBootConfigFileContent 设置启动配置文件的解析结构不设置默认无起服配置默认以yaml解析
func WithNodeBootConfigFileContent(content IBootConfigContent) NodeOption {
return nodeOptionFun(func(node *Node) {
node.bootConfigFile.globalBootConfigFileContent = content
})
}
// WithNodeExBootFlags 设置额外的启动参数解析结构
func WithNodeExBootFlags(content any) NodeOption {
return nodeOptionFun(func(node *Node) {
node.bootFlags.exBootFlag = content
})
}
// WithNodeBootConfigFileParser 设置起服文件解析函数默认yaml格式
func WithNodeBootConfigFileParser(f func(content []byte, out interface{}) error) NodeOption {
return nodeOptionFun(func(node *Node) {
node.bootConfigFile.globalBootConfigParser = f
})
}
// WithNodeLogFileTimestampFormat 设置日志文件默认时间戳格式,默认"20060102"
//func WithNodeLogFileTimestampFormat(format string) NodeOption {
// return appOptionFun(func(node *Node) {
// node.xlog.logFileTsFormat = format
// })
//}
type NodeOption interface {
Apply(app *Node)
}
type nodeOptionFun func(node *Node)
func (of nodeOptionFun) Apply(node *Node) {
of(node)
}

View File

@ -0,0 +1,50 @@
package prom
import (
"math/rand"
"testing"
"time"
)
func TestProm(t *testing.T) {
go func() {
NewEngine(true).Run(":9008")
}()
// 模拟gate进程1
ccuOnlineCounter := NewGauge("ccu").InitDefaultLabels(map[string]string{
"app_id": "1",
"app": "gate",
}, []string{"nation"})
// 模拟gate进程2
ccuOnlineCounter1 := NewGauge("ccu1").InitDefaultLabels(map[string]string{
"app_id": "2",
"app": "gate",
}, []string{"nation"})
// 模拟比赛进程1
ccuBattleCounter := NewGauge("ccu_battle").InitDefaultLabels(map[string]string{
"app_id": "1",
"app": "battle",
}, []string{"nation"})
go func() {
for {
// 模拟统计gate1间隔收集在线人数
num := float64(rand.Int31n(100))
ccuOnlineCounter.LabelValues("cn").Add(num)
// 模拟统计gate2间隔收集在线人数
ccuOnlineCounter1.LabelValues("cn").Add(num * 2)
// 模拟统计比赛1间隔收集比赛人数
num = float64(rand.Int31n(100))
ccuBattleCounter.LabelValues("uk").Add(num)
time.Sleep(time.Second * 5)
}
}()
select {}
}

View File

@ -0,0 +1,65 @@
package pprof
import (
"expvar"
_ "expvar"
"net/http"
_ "net/http/pprof"
"sync"
)
var _expvars_ints = make(map[string]*expvar.Int)
var _expvars_floats = make(map[string]*expvar.Float)
var _expvars_strings = make(map[string]*expvar.String)
var _expvars_ints_lock = &sync.RWMutex{}
var _expvars_floats_lock = &sync.RWMutex{}
var _expvars_strings_lock = &sync.RWMutex{}
// StartCommonProfileMonitor 启动公共性能分析http服务器
// 接口1http://ip:port/debug/vars返回内存监控的json数据
// 接口2http://ip:port/debug/pprof/xxx
func StartCommonProfileMonitor(accessHttpAddr string) {
go func() {
http.ListenAndServe(accessHttpAddr, nil)
}()
}
// AddCommonProfileExpVarInt 添加/debug/vars返回的json变量保证全局名字唯一
func AddCommonProfileExpVarInt(name string, delta int64) {
_expvars_ints_lock.Lock()
defer _expvars_ints_lock.Unlock()
if data, find := _expvars_ints[name]; find {
data.Add(delta)
} else {
v := expvar.NewInt(name)
v.Add(delta)
_expvars_ints[name] = v
}
}
// AddCommonProfileExpVarFloat 添加/debug/vars返回的json变量保证全局名字唯一
func AddCommonProfileExpVarFloat(name string, delta float64) {
_expvars_floats_lock.Lock()
defer _expvars_floats_lock.Unlock()
if data, find := _expvars_floats[name]; find {
data.Add(delta)
} else {
v := expvar.NewFloat(name)
v.Add(delta)
_expvars_floats[name] = v
}
}
// AddCommonProfileExpVarInt 添加/debug/vars返回的json变量保证全局名字唯一
func AddCommonProfileExpVarString(name string, cur string) {
_expvars_strings_lock.Lock()
defer _expvars_strings_lock.Unlock()
if data, find := _expvars_strings[name]; find {
data.Set(cur)
} else {
v := expvar.NewString(name)
v.Set(cur)
_expvars_strings[name] = v
}
}

View File

@ -0,0 +1,37 @@
package prom
import (
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"math/rand"
"testing"
"time"
)
func TestRawProm(t *testing.T) {
vec := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "test",
Help: "test help",
Buckets: []float64{1, 50, 100},
},
[]string{"app"},
)
prometheus.MustRegister(vec)
go func() {
for i := 0; i < 1000; i++ {
vec.WithLabelValues("battle").Observe(float64(rand.Intn(100)))
time.Sleep(time.Second)
}
}()
gin.SetMode(gin.ReleaseMode)
engine := gin.Default()
engine.GET("/metrics", gin.WrapH(promhttp.Handler()))
err := engine.Run(":12345")
if err != nil {
panic(err)
}
}

211
admin/lib/prom/promethus.go Normal file
View File

@ -0,0 +1,211 @@
package prom
import (
"fmt"
"sync/atomic"
ginPprof "github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func NewCounter(name string) *PromeCounterStatMgr {
mgr := new(PromeCounterStatMgr)
mgr.promeStatMgr = newPromeStatMgr(name, func(defaultLabels, labels []string) prometheus.Collector {
return newCounterVec(name, defaultLabels, labels)
})
return mgr
}
func NewGauge(name string) *PromeGaugeStatMgr {
mgr := new(PromeGaugeStatMgr)
mgr.promeStatMgr = newPromeStatMgr(name, func(defaultLabels, labels []string) prometheus.Collector {
return newGagueVec(name, defaultLabels, labels)
})
return mgr
}
func NewHistogram(name string, buckets []float64) *PromeHistogramStatMgr {
mgr := new(PromeHistogramStatMgr)
mgr.promeStatMgr = newPromeStatMgr(name, func(defaultLabels, labels []string) prometheus.Collector {
return newHistogramVec(name, defaultLabels, labels, buckets)
})
return mgr
}
type PromeCounterStatMgr struct {
*promeStatMgr
}
// InitLabels 初始化指标的标签说明,以后每次收集指标都给上标签值
func (c *PromeCounterStatMgr) InitLabels(labels []string) *PromeCounterStatMgr {
return c.InitDefaultLabels(nil, labels)
}
// InitDefaultLabels 带上默认标签例如某些服务启动就能知道标签值比如app_nameapp_id等
// 又不想每次收集指标都传递标签值,可以用此方法一开始就设置好
func (c *PromeCounterStatMgr) InitDefaultLabels(defaultLabels map[string]string, labels []string) *PromeCounterStatMgr {
c.promeStatMgr.withDefaultLabels(defaultLabels, labels)
return c
}
// LabelValues 准备收集指标,调用这个传递标签值,后续再给上指标值
// 即c.LabelValues("xxx").Add(234)
func (c *PromeCounterStatMgr) LabelValues(labels ...string) prometheus.Counter {
return c.promeStatMgr.getCounterWithLabels(labels...)
}
type PromeGaugeStatMgr struct {
*promeStatMgr
}
// InitLabels 初始化指标的标签说明,以后每次收集指标都给上标签值
func (c *PromeGaugeStatMgr) InitLabels(labels []string) *PromeGaugeStatMgr {
return c.InitDefaultLabels(nil, labels)
}
// InitDefaultLabels 带上默认标签例如某些服务启动就能知道标签值比如app_nameapp_id等
// 又不想每次收集指标都传递标签值,可以用此方法一开始就设置好
func (c *PromeGaugeStatMgr) InitDefaultLabels(defaultLabels map[string]string, labels []string) *PromeGaugeStatMgr {
c.promeStatMgr.withDefaultLabels(defaultLabels, labels)
return c
}
// LabelValues 准备收集指标,调用这个传递标签值,后续再给上指标值
// 即c.LabelValues("xxx").Add(234)
func (c *PromeGaugeStatMgr) LabelValues(labels ...string) prometheus.Gauge {
return c.promeStatMgr.getGaugeWithLabels(labels...)
}
type PromeHistogramStatMgr struct {
*promeStatMgr
}
// InitLabels 初始化指标的标签说明,以后每次收集指标都给上标签值
func (c *PromeHistogramStatMgr) InitLabels(labels []string) *PromeHistogramStatMgr {
return c.InitDefaultLabels(nil, labels)
}
// InitDefaultLabels 带上默认标签例如某些服务启动就能知道标签值比如app_nameapp_id等
// 又不想每次收集指标都传递标签值,可以用此方法一开始就设置好
func (c *PromeHistogramStatMgr) InitDefaultLabels(defaultLabels map[string]string, labels []string) *PromeHistogramStatMgr {
c.promeStatMgr.withDefaultLabels(defaultLabels, labels)
return c
}
// LabelValues 准备收集指标,调用这个传递标签值,后续再给上指标值
// 即c.LabelValues("xxx").Add(234)
func (c *PromeHistogramStatMgr) LabelValues(labels ...string) prometheus.Observer {
return c.promeStatMgr.getHistogramWithLabels(labels...)
}
type promeStatMgr struct {
collector prometheus.Collector
name string
newCollectorFun func(defaultLabels, labels []string) prometheus.Collector
defaultLabelsValue []string
initCounter int32
}
func (mgr *promeStatMgr) withDefaultLabels(defaultLabels map[string]string, labels []string) {
if atomic.AddInt32(&mgr.initCounter, 1) > 1 {
panic(fmt.Errorf("promethus vec labels must init once"))
}
defaultLabelKeys := make([]string, 0)
defaultLabelValues := make([]string, 0)
for k, v := range defaultLabels {
defaultLabelKeys = append(defaultLabelKeys, k)
defaultLabelValues = append(defaultLabelValues, v)
}
mgr.defaultLabelsValue = defaultLabelValues
mgr.collector = mgr.newCollectorFun(defaultLabelKeys, labels)
return
}
func (mgr *promeStatMgr) joinLabels(labels ...string) []string {
newLabels := make([]string, 0, len(mgr.defaultLabelsValue)+len(labels))
newLabels = append(newLabels, mgr.defaultLabelsValue...)
newLabels = append(newLabels, labels...)
return newLabels
}
func (mgr *promeStatMgr) getCounterWithLabels(labels ...string) prometheus.Counter {
newLabels := mgr.joinLabels(labels...)
return mgr.collector.(*prometheus.CounterVec).WithLabelValues(newLabels...)
}
func (mgr *promeStatMgr) getGaugeWithLabels(labels ...string) prometheus.Gauge {
newLabels := mgr.joinLabels(labels...)
return mgr.collector.(*prometheus.GaugeVec).WithLabelValues(newLabels...)
}
func (mgr *promeStatMgr) getHistogramWithLabels(labels ...string) prometheus.Observer {
newLabels := mgr.joinLabels(labels...)
return mgr.collector.(*prometheus.HistogramVec).WithLabelValues(newLabels...)
}
func newPromeStatMgr(name string,
newCollectorFun func(defaultLabelsKey []string, labels []string) prometheus.Collector) *promeStatMgr {
mgr := new(promeStatMgr)
mgr.name = name
mgr.newCollectorFun = newCollectorFun
return mgr
}
func newCounterVec(name string, defaultLabels []string, dynamicLabels []string) prometheus.Collector {
vec := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: name,
Help: name,
},
append(defaultLabels, dynamicLabels...),
)
prometheus.MustRegister(vec)
return vec
}
func newGagueVec(name string, defaultLabels []string, dynamicLabels []string) prometheus.Collector {
vec := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: name,
Help: name,
},
append(defaultLabels, dynamicLabels...),
)
prometheus.MustRegister(vec)
return vec
}
func newHistogramVec(name string, defaultLabels []string, dynamicLabels []string, buckets []float64) prometheus.Collector {
vec := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: name,
Help: name,
Buckets: buckets,
},
append(defaultLabels, dynamicLabels...),
)
prometheus.MustRegister(vec)
return vec
}
func NewEngine(enablePprof bool) *gin.Engine {
//gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.Use(gin.Recovery())
RouteEngine(engine, enablePprof)
return engine
}
func RouteEngine(engine *gin.Engine, enablePprof bool) {
if enablePprof {
ginPprof.Register(engine)
}
engine.Use(gin.LoggerWithConfig(gin.LoggerConfig{
SkipPaths: []string{"/metrics"},
}))
engine.GET("/metrics", gin.WrapH(promhttp.Handler()))
}

View File

@ -0,0 +1,212 @@
package main
import (
"admin/lib/web"
"fmt"
"html/template"
"sort"
)
type MyContext struct {
rawCtx web.RawContext
}
func (ctx *MyContext) SetRawContext(rawCtx web.RawContext) {
ctx.rawCtx = rawCtx
}
func (ctx *MyContext) GetRawContext() web.RawContext {
return ctx.rawCtx
}
func main() {
coreRouter := "iris" // gin|iris
engine := web.NewEngine(":8888", coreRouter, func() web.Context {
return new(MyContext)
})
groupV1 := engine.Group("/v1")
{
type Test1 struct {
RoleId string `json:"role_id" desc:"角色id字段" required:"true"`
F1 int `json:"f1" desc:"这是字段1" default:"324"`
F2 string `json:"f2" desc:"这是字段2" default:"abcd"`
F3 bool `json:"f3" desc:"这是字段3" default:"false"`
}
groupV1.Get("/test1", "设置玩家背包数据", web.AccessMode_Write, Test1{}, func(ctx *MyContext, params *Test1) {
fmt.Printf("receive test1 path params:%+v\n", params)
ctx.GetRawContext().Json(200, map[string]any{
"msg": "ok",
"code": 200,
})
})
groupV1.Post("/test1", "获取玩家数据", web.AccessMode_Read, Test1{}, func(ctx *MyContext, params *Test1) {
fmt.Printf("receive test1 path params:%+v\n", params)
ctx.GetRawContext().Json(200, map[string]any{
"msg": "ok",
"code": 200,
})
})
// URI --> :8888/v1/test1?f1=123&f2=sdfsd&f3=true
// BODY --> :8888/v1/test1 {"f1":123,"f2":"sdfds","f3":"true"}
}
engine.Get("/test2", "获取玩家比赛", web.AccessMode_Read, nil, func(ctx *MyContext) {
fmt.Printf("receive test2 request\n")
ctx.GetRawContext().Json(200, map[string]any{
"msg": "ok",
"code": 200,
})
})
engine.Post("/test2", "测试gm指令名字", web.AccessMode_Read, nil, func(ctx *MyContext) {
fmt.Printf("receive test2 request\n")
ctx.GetRawContext().Json(200, map[string]any{
"msg": "ok",
"code": 200,
})
})
groupV2 := engine.Group("v2")
{
type Test2 struct {
F1 int `json:"f1" desc:"这是字段1"`
F2 string `json:"f2" desc:"这是字段2"`
F3 bool `json:"f3" desc:"这是字段3"`
}
groupV2.Post("test3", "测试gm指令名字123", web.AccessMode_Write, Test2{}, func(ctx *MyContext, params *Test2) {
fmt.Printf("receive test3\n")
ctx.GetRawContext().Json(200, map[string]any{
"msg": "ok",
"code": 200,
})
})
}
engine.Get("path_tree", "测试gm指令名字3424", web.AccessMode_Write, nil, func(ctx *MyContext) {
tree := engine.TravelPathTree()
ctx.GetRawContext().Json(200, tree)
})
engine.Get("/grid", "获取指令描述表", web.AccessMode_Read, nil, func(c *MyContext) {
tree := &Tree{tree: engine.TravelPathTree()}
tmpl, err := template.New("html_test").Funcs(template.FuncMap(map[string]any{
"incr": incr,
})).Parse(tplText)
if err != nil {
panic(err)
}
err = tmpl.Execute(c.GetRawContext().Writer(), tree)
if err != nil {
panic(err)
}
})
err := engine.Run()
if err != nil {
panic(err)
}
}
type Path struct {
Path string
Route *web.RouteDescInfo
}
type Tree struct {
tree map[string]*web.RouteDescInfo
}
func (t *Tree) Paths() [][]*Path {
splitCol := 4
list := make([]*Path, 0)
for k, v := range t.tree {
list = append(list, &Path{Path: k, Route: v})
}
sort.SliceStable(list, func(i, j int) bool {
return list[i].Path < list[j].Path
})
if len(list) <= splitCol {
return [][]*Path{list}
}
section := len(list) / splitCol
paths := make([][]*Path, splitCol)
for i := 0; i < splitCol; i++ {
paths[i] = make([]*Path, 0)
start := i * section
for j := 0; j < section; j++ {
start += j
paths[i] = append(paths[i], list[start])
}
}
if len(list)%splitCol > 0 {
idx := 0
for i := len(list) - len(list)%splitCol; i < len(list); i++ {
paths[idx] = append(paths[idx], list[i])
idx++
}
}
return paths
}
func incr(src int) int {
return src + 1
}
var tplText = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GM MASTER</title>
<div style="padding-left: 50px;">
{{ $pathsList := .Paths }}
{{ range $pidex, $paths := $pathsList }}
<div style="float:left">
<h1 style="text-align: center">指令列表{{ incr $pidex }}</h1>
<div style="padding-left: 20px;padding-right: 20px">
<table border = "1" align="center">
{{ range $idx, $path := $paths }}
<tr>
<th colspan="5" style="background-color: green; text-align: left">{{ $path.Route.Desc }}</th>
</tr>
<tr>
<td><b>请求路径</b></td>
<th colspan="4" style="text-align: left">&nbsp;&nbsp;{{ $path.Path }}</th>
</tr>
<tr>
<td><b>方法</b></td>
<th colspan="4" style="text-align: left">&nbsp;&nbsp;{{ $path.Route.MethodDesc "或" }}</th>
</tr>
{{ if $path.Route.HasRequest }}
{{ range $idx1, $field := $path.Route.Fields }}
<tr>
<td><b>{{ printf "参数%d" $idx1 }}</b></td>
<td style="text-align: center">{{printf "%s" $field.Name }}</td>
<td style="text-align: center">{{printf "%s" $field.Type }}</td>
<td>{{printf "%s" $field.Desc }}</td>
{{ if eq $field.Default "" }}
<td>{{printf "默认:无" }}</td>
{{ else }}
<td>{{printf "默认:%s" $field.Default }}</td>
{{ end }}
</tr>
{{ end }}
{{ else }}
<tr><th colspan="5">无需参数</th>
{{ end }}
<!--<tr><th colspan="5"><hr></th></tr>-->
{{ end }}
</table>
</div>
</div>
{{ end }}
</div>
</head>
<body>
</body>
</html>
`

183
admin/lib/web/group.go Normal file
View File

@ -0,0 +1,183 @@
package web
type routeGroupInterface interface {
Use(middlewares ...HandlerFunc)
Group(path string, handlers ...HandlerFunc) routeGroupInterface
Get(path string, desc string, req any, handlers ...HandlerFunc) routeGroupInterface
Post(path string, desc string, req any, handlers ...HandlerFunc) routeGroupInterface
Put(path string, desc string, req any, handlers ...HandlerFunc) routeGroupInterface
Delete(path string, desc string, req any, handlers ...HandlerFunc) routeGroupInterface
}
type Group struct {
coreRouter routeGroupInterface
DirectRoutes map[string]*RouteDescInfo
GroupRoutes map[string]*Group
}
func newGroup(coreRouter routeGroupInterface) *Group {
g := &Group{
coreRouter: coreRouter,
DirectRoutes: make(map[string]*RouteDescInfo),
GroupRoutes: make(map[string]*Group),
}
return g
}
func (e *Group) Use(middlewares ...HandlerFunc) {
e.coreRouter.Use(middlewares...)
}
func (e *Group) Group(path string, handlers ...HandlerFunc) *Group {
path = pathBeTheSame(path)
routeGroup := e.coreRouter.Group(path, handlers...)
group := newGroup(routeGroup)
e.GroupRoutes[path] = group
return group
}
// Get 注册get方法路由根据request请求体优先从body中以json格式解析参数如果没有body则从uri参数中解析出请求参数
//
// path:路径
// desc:路由的一个简短描述
// request:请求结构体
// 格式:
// type struct {
// F1 int `json:"f1" desc:"字段描述" default:"234" required:"true"`
// }
// tag描述
// json:字段名
// desc:字段简短描述,没有可以不写
// default:默认值,没有可以不写
// required:是否必填字段,没有要求可以不写
// 注意get、post注册的request结构字段如果是uri参数方式类型只支持golang基础类型以及基础类型的切片不能是结构体类型
// 例如:
// type Field struct {
// A int
// B bool
// }
// type Request struct {
// F1 *Field
// }
// F1字段就是非法的无法解析会报错
// handlers:路由处理函数如果没有请求体就是func(ctx)否则就是func(ctx, request)
func (e *Group) Get(path string, desc string, access AccessMode, request any, handler HandlerFunc) {
path = pathBeTheSame(path)
old, find := e.DirectRoutes[path]
if !find {
e.DirectRoutes[path] = newRouteDescInfo(path, desc, "GET", access, request)
} else {
old.Method = append(old.Method, "GET")
e.DirectRoutes[path] = old
}
e.coreRouter.Get(path, desc, request, handler)
}
// Post 注册post方法路由根据request请求体优先从body中以json格式解析参数如果没有body则从uri参数中解析出请求参数
//
// path:路径
// desc:路由的一个简短描述
// request:请求结构体
// 格式:
// type struct {
// F1 int `json:"f1" desc:"字段描述" default:"234" required:"true"`
// }
// tag描述
// json:字段名
// desc:字段简短描述,没有可以不写
// default:默认值,没有可以不写
// required:是否必填字段,没有要求可以不写
// 注意get、post注册的request结构字段如果是uri参数方式类型只支持golang基础类型以及基础类型的切片不能是结构体类型
// 例如:
// type Field struct {
// A int
// B bool
// }
// type Request struct {
// F1 *Field
// }
// F1字段就是非法的无法解析会报错
// handlers:路由处理函数如果没有请求体就是func(ctx)否则就是func(ctx, request)
func (e *Group) Post(path, desc string, access AccessMode, request any, handler HandlerFunc) {
path = pathBeTheSame(path)
old, find := e.DirectRoutes[path]
if !find {
e.DirectRoutes[path] = newRouteDescInfo(path, desc, "POST", access, request)
} else {
old.Method = append(old.Method, "POST")
e.DirectRoutes[path] = old
}
e.coreRouter.Post(path, desc, request, handler)
}
func (e *Group) Put(path, desc string, access AccessMode, request any, handler HandlerFunc) {
path = pathBeTheSame(path)
old, find := e.DirectRoutes[path]
if !find {
e.DirectRoutes[path] = newRouteDescInfo(path, desc, "PUT", access, request)
} else {
old.Method = append(old.Method, "PUT")
e.DirectRoutes[path] = old
}
e.coreRouter.Put(path, desc, request, handler)
}
func (e *Group) Delete(path, desc string, access AccessMode, request any, handler HandlerFunc) {
path = pathBeTheSame(path)
old, find := e.DirectRoutes[path]
if !find {
e.DirectRoutes[path] = newRouteDescInfo(path, desc, "DELETE", access, request)
} else {
old.Method = append(old.Method, "DELETE")
e.DirectRoutes[path] = old
}
e.coreRouter.Delete(path, desc, request, handler)
}
// GetAndPost 注册get&post方法路由根据request请求体优先从body中以json格式解析参数如果没有body则从uri参数中解析出请求参数
//
// path:路径
// desc:路由的一个简短描述
// request:请求结构体
// 格式:
// type struct {
// F1 int `json:"f1" desc:"字段描述" default:"234" required:"true"`
// }
// tag描述
// json:字段名
// desc:字段简短描述,没有可以不写
// default:默认值,没有可以不写
// required:是否必填字段,没有要求可以不写
// 注意get、post注册的request结构字段如果是uri参数方式类型只支持golang基础类型以及基础类型的切片不能是结构体类型
// 例如:
// type Field struct {
// A int
// B bool
// }
// type Request struct {
// F1 *Field
// }
// F1字段就是非法的无法解析会报错
// handlers:路由处理函数如果没有请求体就是func(ctx)否则就是func(ctx, request)
func (e *Group) GetAndPost(path, desc string, access AccessMode, request any, handler HandlerFunc) {
path = pathBeTheSame(path)
record := newRouteDescInfo(path, desc, "GET", access, request)
record.Method = append(record.Method, "POST")
e.DirectRoutes[path] = record
e.coreRouter.Get(path, desc, request, handler)
e.coreRouter.Post(path, desc, request, handler)
}
func (e *Group) TravelPathTree() map[string]*RouteDescInfo {
m := make(map[string]*RouteDescInfo)
for k, route := range e.DirectRoutes {
m[k] = route
}
for k, subG := range e.GroupRoutes {
gm := subG.TravelPathTree()
for k1, v1 := range gm {
m[k+k1] = v1
}
}
return m
}

24
admin/lib/web/model.go Normal file
View File

@ -0,0 +1,24 @@
package web
import (
"fmt"
"io"
)
type Context interface {
SetRawContext(ctx RawContext)
GetRawContext() RawContext
}
type HandlerFunc any
type RawContext interface {
Json(code int, v any)
Writer() io.Writer
}
var ParseParamsErrorHandler = func(ctx Context, path string, err error) {
ctx.GetRawContext().Json(501, map[string]interface{}{
"MSG": fmt.Sprintf("parse params error:%v", err),
})
}

12
admin/lib/web/readme.txt Normal file
View File

@ -0,0 +1,12 @@
注意get、post注册的request结构字段如果是uri参数方式类型只支持golang基础类型以及基础类型的切片不能是结构体类型
例如:
type Field struct {
A int
B bool
}
type Request struct {
F1 *Field
}
F1字段就是非法的无法解析会报错

173
admin/lib/web/router.go Normal file
View File

@ -0,0 +1,173 @@
package web
import "fmt"
type routerInterface interface {
routeGroupInterface
Run(addr string) error
}
type Engine struct {
Addr string
newContextFun func() Context
DirectRoutes map[string]*RouteDescInfo
GroupRoutes map[string]*Group
coreRouter routerInterface
}
// NewEngine 使用gin或者iris创建web引擎newContextFun为调用方需要实现的context初始化函数
// coreRouter指定不同web引擎目前只支持gin
func NewEngine(coreRouter string, newContextFun func() Context) *Engine {
e := &Engine{
newContextFun: newContextFun,
DirectRoutes: make(map[string]*RouteDescInfo),
GroupRoutes: make(map[string]*Group),
}
switch coreRouter {
//case "iris":
// e.coreRouter = newRouterIris(newContextFun)
case "gin":
e.coreRouter = newRouterGin(newContextFun)
default:
panic(fmt.Errorf("NewEngine only support irir or gin, invalid type:%v", coreRouter))
}
return e
}
func (e *Engine) Use(middlewares ...HandlerFunc) {
e.coreRouter.Use(middlewares...)
}
func (e *Engine) Group(path string, handlers ...HandlerFunc) *Group {
path = pathBeTheSame(path)
routeGroup := e.coreRouter.Group(path, handlers...)
group := newGroup(routeGroup)
e.GroupRoutes[path] = group
return group
}
// Get 注册get方法路由根据request请求体优先从body中以json格式解析参数如果没有body则从uri参数中解析出请求参数
//
// path:路径
// desc:路由的一个简短描述
// request:请求结构体
// 格式:
// type struct {
// F1 int `json:"f1" desc:"字段描述" default:"234" required:"true"`
// }
// tag描述
// json:字段名
// desc:字段简短描述,没有可以不写
// default:默认值,没有可以不写
// required:是否必填字段,没有要求可以不写
// 注意get、post注册的request结构字段如果是uri参数方式类型只支持golang基础类型以及基础类型的切片不能是结构体类型
// 例如:
// type Field struct {
// A int
// B bool
// }
// type Request struct {
// F1 *Field
// }
// F1字段就是非法的无法解析会报错
// handlers:路由处理函数如果没有请求体就是func(ctx)否则就是func(ctx, request)
func (e *Engine) Get(path string, desc string, access AccessMode, request any, handler HandlerFunc) {
path = pathBeTheSame(path)
old, find := e.DirectRoutes[path]
if !find {
e.DirectRoutes[path] = newRouteDescInfo(path, desc, "GET", access, request)
} else {
old.Method = append(old.Method, "GET")
e.DirectRoutes[path] = old
}
e.coreRouter.Get(path, desc, request, handler)
}
// Post 注册post方法路由根据request请求体优先从body中以json格式解析参数如果没有body则从uri参数中解析出请求参数
//
// path:路径
// desc:路由的一个简短描述
// request:请求结构体
// 格式:
// type struct {
// F1 int `json:"f1" desc:"字段描述" default:"234" required:"true"`
// }
// tag描述
// json:字段名
// desc:字段简短描述,没有可以不写
// default:默认值,没有可以不写
// required:是否必填字段,没有要求可以不写
// 注意get、post注册的request结构字段如果是uri参数方式类型只支持golang基础类型以及基础类型的切片不能是结构体类型
// 例如:
// type Field struct {
// A int
// B bool
// }
// type Request struct {
// F1 *Field
// }
// F1字段就是非法的无法解析会报错
// handlers:路由处理函数如果没有请求体就是func(ctx)否则就是func(ctx, request)
func (e *Engine) Post(path, desc string, access AccessMode, request any, handler HandlerFunc) {
path = pathBeTheSame(path)
old, find := e.DirectRoutes[path]
if !find {
e.DirectRoutes[path] = newRouteDescInfo(path, desc, "POST", access, request)
} else {
old.Method = append(old.Method, "POST")
e.DirectRoutes[path] = old
}
e.coreRouter.Post(path, desc, request, handler)
}
func (e *Engine) Run(addr string) error {
e.Addr = addr
return e.coreRouter.Run(addr)
}
// TravelPathTree 获取所有路径的描述表
func (e *Engine) TravelPathTree() map[string]*RouteDescInfo {
m := make(map[string]*RouteDescInfo)
for k, route := range e.DirectRoutes {
m[k] = route
}
for k, subG := range e.GroupRoutes {
gm := subG.TravelPathTree()
for k1, v1 := range gm {
m[k+k1] = v1
}
}
return m
}
func pathBeTheSame(path string) string {
if path == "" {
return ""
}
if path == "/" {
return path
}
if path[0] != '/' {
path = "/" + path
}
if path[len(path)-1] == '/' {
path = path[:len(path)-1]
}
return path
}
func pathBeTheSame1(path string) string {
if path == "" {
return ""
}
if path == "/" {
return path
}
if path[0] != '/' {
path = "/" + path
}
if path[len(path)-1] == '/' {
path = path[:len(path)-1]
}
return path
}

View File

@ -0,0 +1,85 @@
package web
import (
"reflect"
"strings"
)
type AccessMode int
const (
AccessMode_Read = 1
AccessMode_Write = 2
)
type RouteDescInfo struct {
Path string
Desc string
Method []string
AccessMode
rawRequest any
Fields []*FieldDescInfo
}
func newRouteDescInfo(path, desc, method string, access AccessMode, request any) *RouteDescInfo {
info := &RouteDescInfo{
Path: path,
Desc: desc,
Method: []string{method},
AccessMode: access,
rawRequest: request,
}
info.RequestFieldsDesc()
return info
}
func (info *RouteDescInfo) MethodDesc(sep string) string {
return strings.Join(info.Method, sep)
}
func (info *RouteDescInfo) RequestFieldsDesc() []*FieldDescInfo {
if !info.HasRequest() {
return []*FieldDescInfo{}
}
list := make([]*FieldDescInfo, 0)
to := reflect.TypeOf(info.rawRequest)
if to.Kind() == reflect.Ptr {
to = to.Elem()
}
for i := 0; i < to.NumField(); i++ {
field := to.Field(i)
list = append(list, newFieldDescInfo(field))
}
info.Fields = list
return list
}
func (info *RouteDescInfo) HasRequest() bool {
return info.rawRequest != nil
}
type FieldDescInfo struct {
Name string
FieldName string
Type string
Desc string
Required bool
Default string
rawTypeOfField reflect.StructField
}
func newFieldDescInfo(field reflect.StructField) *FieldDescInfo {
desc := &FieldDescInfo{
Name: field.Tag.Get("json"),
FieldName: field.Name,
Type: field.Type.String(),
Desc: field.Tag.Get("desc"),
rawTypeOfField: field,
}
if field.Tag.Get("required") == "true" {
desc.Required = true
}
desc.Default = desc.rawTypeOfField.Tag.Get("default")
return desc
}

303
admin/lib/web/router_gin.go Normal file
View File

@ -0,0 +1,303 @@
package web
import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"io"
"reflect"
"strconv"
"strings"
)
type routerGroupGin struct {
group *gin.RouterGroup
newContextFun func() Context
}
func (group *routerGroupGin) Use(middlewares ...HandlerFunc) {
group.group.Use(getGinHandlerFun(group.newContextFun, middlewares...)...)
}
func (group *routerGroupGin) Group(path string, handlers ...HandlerFunc) routeGroupInterface {
ginGroup := group.group.Group(path, getGinHandlerFun(group.newContextFun, handlers...)...)
group1 := &routerGroupGin{group: ginGroup, newContextFun: group.newContextFun}
return group1
}
func (group *routerGroupGin) Get(path string, desc string, req any, handlers ...HandlerFunc) routeGroupInterface {
if req == nil {
group.group.GET(path, getGinHandlerFun(group.newContextFun, handlers...)...)
} else {
group.group.GET(path, getGinHandlerFunWithRequest(group.newContextFun, req, handlers...)...)
}
return group
}
func (group *routerGroupGin) Post(path string, desc string, req any, handlers ...HandlerFunc) routeGroupInterface {
if req == nil {
group.group.POST(path, getGinHandlerFun(group.newContextFun, handlers...)...)
} else {
group.group.POST(path, getGinHandlerFunWithRequest(group.newContextFun, req, handlers...)...)
}
return group
}
func (group *routerGroupGin) Put(path string, desc string, req any, handlers ...HandlerFunc) routeGroupInterface {
if req == nil {
group.group.PUT(path, getGinHandlerFun(group.newContextFun, handlers...)...)
} else {
group.group.PUT(path, getGinHandlerFunWithRequest(group.newContextFun, req, handlers...)...)
}
return group
}
func (group *routerGroupGin) Delete(path string, desc string, req any, handlers ...HandlerFunc) routeGroupInterface {
if req == nil {
group.group.DELETE(path, getGinHandlerFun(group.newContextFun, handlers...)...)
} else {
group.group.DELETE(path, getGinHandlerFunWithRequest(group.newContextFun, req, handlers...)...)
}
return group
}
type routerGin struct {
engine *gin.Engine
newContextFun func() Context
}
func newRouterGin(newContextFun func() Context) routerInterface {
engine := gin.Default()
router := &routerGin{
engine: engine,
newContextFun: newContextFun,
}
return router
}
func (router *routerGin) Use(middlewares ...HandlerFunc) {
router.engine.Use(getGinHandlerFun(router.newContextFun, middlewares...)...)
}
func (router *routerGin) Group(path string, handlers ...HandlerFunc) routeGroupInterface {
ginGroup := router.engine.Group(path, getGinHandlerFun(router.newContextFun, handlers...)...)
group := &routerGroupGin{
group: ginGroup,
newContextFun: router.newContextFun,
}
return group
}
func (router *routerGin) Get(path string, desc string, req any, handlers ...HandlerFunc) routeGroupInterface {
if req == nil {
router.engine.GET(path, getGinHandlerFun(router.newContextFun, handlers...)...)
} else {
router.engine.GET(path, getGinHandlerFunWithRequest(router.newContextFun, req, handlers...)...)
}
return router
}
func (router *routerGin) Post(path string, desc string, req any, handlers ...HandlerFunc) routeGroupInterface {
if req == nil {
router.engine.POST(path, getGinHandlerFun(router.newContextFun, handlers...)...)
} else {
router.engine.POST(path, getGinHandlerFunWithRequest(router.newContextFun, req, handlers...)...)
}
return router
}
func (router *routerGin) Put(path string, desc string, req any, handlers ...HandlerFunc) routeGroupInterface {
if req == nil {
router.engine.PUT(path, getGinHandlerFun(router.newContextFun, handlers...)...)
} else {
router.engine.PUT(path, getGinHandlerFunWithRequest(router.newContextFun, req, handlers...)...)
}
return router
}
func (router *routerGin) Delete(path string, desc string, req any, handlers ...HandlerFunc) routeGroupInterface {
if req == nil {
router.engine.DELETE(path, getGinHandlerFun(router.newContextFun, handlers...)...)
} else {
router.engine.DELETE(path, getGinHandlerFunWithRequest(router.newContextFun, req, handlers...)...)
}
return router
}
func getGinHandlerFun(newContextFun func() Context, handlers ...HandlerFunc) []gin.HandlerFunc {
list := make([]gin.HandlerFunc, 0, len(handlers))
for _, handler := range handlers {
list = append(list, func(rawCtx *gin.Context) {
rawCtx1 := &ginCtx{ctx: rawCtx}
ctx := newContextFun()
ctx.SetRawContext(rawCtx1)
reflect.ValueOf(handler).Call([]reflect.Value{reflect.ValueOf(ctx)})
if rawCtx.IsAborted() {
return
}
})
}
return list
}
func getGinHandlerFunWithRequest(newContextFun func() Context, requestTemplate any, handlers ...HandlerFunc) []gin.HandlerFunc {
list := make([]gin.HandlerFunc, 0, len(handlers))
for _, handler := range handlers {
list = append(list, func(rawCtx *gin.Context) {
rawCtx1 := &ginCtx{ctx: rawCtx}
ctx := newContextFun()
ctx.SetRawContext(rawCtx1)
request, err := rawCtx1.parseRequest(requestTemplate)
if err != nil {
ParseParamsErrorHandler(ctx, rawCtx.Request.RequestURI, err)
return
}
reflect.ValueOf(handler).Call([]reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(request)})
})
}
return list
}
func (router *routerGin) Run(addr string) error {
return router.engine.Run(addr)
}
type ginCtx struct {
ctx *gin.Context
}
func (ctx *ginCtx) Json(code int, v any) {
ctx.ctx.JSON(code, v)
}
func (ctx *ginCtx) Writer() io.Writer {
return ctx.ctx.Writer
}
func (ctx *ginCtx) body() ([]byte, error) {
buf, err := io.ReadAll(ctx.ctx.Request.Body)
if err != nil {
return nil, fmt.Errorf("read body error:%v", err)
}
return buf, nil
}
func (ctx *ginCtx) parseRequest(request any) (any, error) {
if request == nil {
return nil, nil
}
requestType := reflect.TypeOf(request)
newRequest := reflect.New(requestType).Interface()
body, err := ctx.body()
if err != nil {
return nil, err
}
if len(body) == 0 {
newRequestValue := reflect.ValueOf(newRequest).Elem()
newRequestValueType := newRequestValue.Type()
for i := 0; i < newRequestValue.NumField(); i++ {
f := newRequestValueType.Field(i)
fieldTagName := f.Tag.Get("json")
if fieldTagName == "" {
fieldTagName = f.Name
}
field := newRequestValue.Field(i)
if !field.CanSet() {
continue
}
fieldStr := ctx.ctx.Query(fieldTagName)
err := setValue(field, fieldStr, f.Tag.Get("default"), f.Tag.Get("required"))
if err != nil {
return nil, fmt.Errorf("parse uri params field(%v) set value(%v) error:%v", f.Name, fieldStr, err)
}
}
} else {
err = json.Unmarshal(body, newRequest)
if err != nil {
return nil, fmt.Errorf("json unmarshal body error:%v", err)
}
}
return newRequest, nil
}
// setValue 设置结构体一个字段的值
func setValue(field reflect.Value, value string, defaultValue string, required string) error {
if value == "" {
value = defaultValue
}
if value == "" && required == "true" {
return fmt.Errorf("field is required, please give a valid value")
}
if field.Kind() == reflect.Ptr {
if value == "" {
return nil
}
if field.IsNil() {
field.Set(reflect.New(field.Type().Elem()))
}
field = field.Elem()
}
switch field.Kind() {
case reflect.String:
field.SetString(value)
case reflect.Bool:
if value == "" {
field.SetBool(false)
} else {
b, err := strconv.ParseBool(value)
if err != nil {
return err
}
field.SetBool(b)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if value == "" {
field.SetInt(0)
} else {
i, err := strconv.ParseInt(value, 0, field.Type().Bits())
if err != nil {
return err
}
field.SetInt(i)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if value == "" {
field.SetUint(0)
break
}
ui, err := strconv.ParseUint(value, 0, field.Type().Bits())
if err != nil {
return err
}
field.SetUint(ui)
case reflect.Float32, reflect.Float64:
if value == "" {
field.SetFloat(0)
break
}
f, err := strconv.ParseFloat(value, field.Type().Bits())
if err != nil {
return err
}
field.SetFloat(f)
case reflect.Struct:
return fmt.Errorf("unsupport struct field:%v", field.Type())
case reflect.Slice:
values := strings.Split(value, ",")
if len(values) == 1 && values[0] == "" {
values = []string{}
}
field.Set(reflect.MakeSlice(field.Type(), len(values), len(values)))
for i := 0; i < len(values); i++ {
err := setValue(field.Index(i), values[i], "", "")
if err != nil {
return err
}
}
default:
return fmt.Errorf("no support type %s", field.Type())
}
return nil
}

56
admin/lib/xlog/catch.go Normal file
View File

@ -0,0 +1,56 @@
package xlog
import (
"runtime"
"time"
)
func Catch() {
if v := recover(); v != nil {
backtrace(v)
}
}
func CatchWithInfo(info string) {
if v := recover(); v != nil {
Critif("panic info: [%v]", info)
backtrace(v)
}
}
func CatchWithInfoFun(info string, f func()) {
if v := recover(); v != nil {
Critif("panic info: [%v]", info)
backtrace(v)
f()
}
}
func PrintCallerStack(info string) {
for i := 1; ; i++ {
pc, file, line, ok := runtime.Caller(i + 1)
if !ok {
break
}
//fmt.Fprintf(os.Stderr, "% 3d. %s() %s:%d\n", i, runtime.FuncForPC(pc).Name(), file, line)
Debugf("[%v] % 3d. %s() %s:%d\n", info, i, runtime.FuncForPC(pc).Name(), file, line)
}
}
func backtrace(message interface{}) {
//fmt.Fprintf(os.Stderr, "Traceback[%s] (most recent call last):\n", time.Now())
Critif("Traceback[%s] (most recent call last):\n", time.Now())
for i := 0; ; i++ {
pc, file, line, ok := runtime.Caller(i + 1)
if !ok {
break
}
//fmt.Fprintf(os.Stderr, "% 3d. %s() %s:%d\n", i, runtime.FuncForPC(pc).Name(), file, line)
Critif("% 3d. %s() %s:%d\n", i, runtime.FuncForPC(pc).Name(), file, line)
}
//fmt.Fprintf(os.Stderr, "%v\n", message)
Critif("%v\n", message)
// 等待写日志
//time.Sleep(time.Second * 5)
}

View File

@ -0,0 +1,31 @@
//go:build darwin
// +build darwin
package crosssyslog
import "log/syslog"
type Syslog struct {
*syslog.Writer
}
type Priority = syslog.Priority
var (
LOG_INFO = syslog.LOG_INFO
LOG_LOCAL0 = syslog.LOG_LOCAL0
)
func New(priority Priority, tag string) (*Syslog, error) {
SyslogLocal, syslogErr := syslog.New(syslog.LOG_NOTICE, tag)
if syslogErr != nil {
return nil, syslogErr
}
return &Syslog{
Writer: SyslogLocal,
}, nil
}
func (s *Syslog) Write(data []byte) (int, error) {
return s.Writer.Write(data)
}

View File

@ -0,0 +1,31 @@
//go:build linux
// +build linux
package crosssyslog
import "log/syslog"
type Syslog struct {
*syslog.Writer
}
type Priority = syslog.Priority
var (
LOG_INFO = syslog.LOG_INFO
LOG_LOCAL0 = syslog.LOG_LOCAL0
)
func New(priority Priority, tag string) (*Syslog, error) {
SyslogLocal, syslogErr := syslog.New(priority, tag)
if syslogErr != nil {
return nil, syslogErr
}
return &Syslog{
Writer: SyslogLocal,
}, nil
}
func (s *Syslog) Write(data []byte) (int, error) {
return s.Writer.Write(data)
}

View File

@ -0,0 +1,21 @@
// +build windows
package crosssyslog
type Syslog struct {
}
type Priority int
var (
LOG_INFO Priority = 1
LOG_LOCAL0 Priority = 2
)
func New(priority Priority, tag string) (*Syslog, error) {
return &Syslog{}, nil
}
func (s *Syslog) Write(data []byte) (int, error) {
return len(data), nil
}

View File

@ -0,0 +1,8 @@
package xlog
// Handler writes logs to somewhere
type Handler interface {
//Rotate()
Write(p []byte) (n int, err error)
Close() error
}

View File

@ -0,0 +1,58 @@
package handler
import (
"os"
"path"
"sync"
)
// FileHandler writes xlog to a file.
type FileHandler struct {
fileName string
flag int
fd *os.File
fdLock *sync.RWMutex
}
func NewFileHandler(fileName string, flag int) (*FileHandler, error) {
dir := path.Dir(fileName)
os.Mkdir(dir, 0777)
f, err := os.OpenFile(fileName, flag, 0666)
if err != nil {
return nil, err
}
h := new(FileHandler)
h.fileName = fileName
h.flag = flag
h.fd = f
h.fdLock = &sync.RWMutex{}
return h, nil
}
//func (h *FileHandler) Rotate() {
// fd, err := os.OpenFile(h.fileName, h.flag, 0666)
// if err != nil {
// h.fd.Write([]byte(fmt.Sprintf("interval check xlog file %v open error:%v", h.fileName, err)))
// return
// }
//
// h.fdLock.Lock()
// oldFd := h.fd
// h.fd = fd
// oldFd.Close()
// h.fdLock.Unlock()
//}
func (h *FileHandler) Write(b []byte) (n int, err error) {
h.fdLock.RLock()
defer h.fdLock.RUnlock()
return h.fd.Write(b)
}
func (h *FileHandler) Close() error {
return h.fd.Close()
}

View File

@ -0,0 +1,83 @@
package handler
import (
"fmt"
"os"
"path"
)
// RotatingFileHandler writes xlog a file, if file size exceeds maxBytes,
// it will backup current file and open a new one.
//
// max backup file number is set by backupCount, it will delete oldest if backups too many.
type RotatingFileHandler struct {
fd *os.File
fileName string
maxBytes int
backupCount int
}
func NewRotatingFileHandler(fileName string, maxBytes int, backupCount int) (*RotatingFileHandler, error) {
dir := path.Dir(fileName)
os.Mkdir(dir, 0777)
h := new(RotatingFileHandler)
if maxBytes <= 0 {
return nil, fmt.Errorf("invalid max bytes")
}
h.fileName = fileName
h.maxBytes = maxBytes
h.backupCount = backupCount
var err error
h.fd, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return nil, err
}
return h, nil
}
func (h *RotatingFileHandler) Write(p []byte) (n int, err error) {
h.doRollover()
return h.fd.Write(p)
}
func (h *RotatingFileHandler) Close() error {
if h.fd != nil {
return h.fd.Close()
}
return nil
}
func (h *RotatingFileHandler) doRollover() {
f, err := h.fd.Stat()
if err != nil {
return
}
if h.maxBytes <= 0 {
return
} else if f.Size() < int64(h.maxBytes) {
return
}
if h.backupCount > 0 {
h.fd.Close()
for i := h.backupCount - 1; i > 0; i-- {
sfn := fmt.Sprintf("%s.%d", h.fileName, i)
dfn := fmt.Sprintf("%s.%d", h.fileName, i+1)
os.Rename(sfn, dfn)
}
dfn := fmt.Sprintf("%s.1", h.fileName)
os.Rename(h.fileName, dfn)
h.fd, _ = os.OpenFile(h.fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
}
}

View File

@ -0,0 +1,207 @@
package handler
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"time"
)
const (
kRollPerSeconds = 60 * 60 * 24 // one day
)
var (
pid = os.Getpid()
host = "unknowhost"
)
func init() {
h, err := os.Hostname()
if err == nil {
host = shortHostname(h)
}
}
func shortHostname(hostname string) string {
if i := strings.Index(hostname, "."); i >= 0 {
return hostname[:i]
}
return hostname
}
func logFileName(basename string) string {
now := time.Now()
name := fmt.Sprintf("%s.%04d%02d%02d-%02d%02d%02d.%s.%d.xlog",
basename,
now.Year(),
now.Month(),
now.Day(),
now.Hour(),
now.Minute(),
now.Second(),
host,
pid,
)
return name
}
type RotatingFileAtDayHandler struct {
baseName string
rollSize int
flushInterval int64
checkEveryN int
syncFlush bool
count int
startofPeriod int64
lastRoll int64
lastFlush int64
file *appendFile
}
// baseName 日志文件的基本名包含全路径 如 "/tmp/test"
// rollSize 每写入rollSize字节日志滚动文件
// flushInterval 刷新文件写入缓存的间隔
// checkEveryN 每写入checkEveryN次 检查文件回滚和缓存刷新
// syncFlush == true flushInterval和checkEveryN 失效
func NewRotatingFileAtDayHandler(baseName string, rollSize int,
flushInterval int64, checkEveryN int, syncFlush bool) *RotatingFileAtDayHandler {
hander := &RotatingFileAtDayHandler{
baseName: baseName,
rollSize: rollSize,
flushInterval: flushInterval,
checkEveryN: checkEveryN,
syncFlush: syncFlush,
count: 0,
startofPeriod: 0,
lastRoll: 0,
lastFlush: 0,
}
hander.rollFile()
return hander
}
func NewDefaultRotatingFileAtDayHandler(baseName string,
rollSize int) *RotatingFileAtDayHandler {
//checkEveryN 开发期间默认设置为1 上线后调高已提高处理性能
return NewRotatingFileAtDayHandler(baseName, rollSize, 3, 1, true)
}
func (self *RotatingFileAtDayHandler) Write(b []byte) (int, error) {
n, err := self.file.append(b)
if err != nil {
return n, err
}
if self.file.writtenBytes() > self.rollSize {
self.rollFile()
} else {
self.count++
if self.count >= self.checkEveryN || self.syncFlush {
self.count = 0
now := time.Now().Unix()
thisPeriod := now / kRollPerSeconds * kRollPerSeconds
if thisPeriod != self.startofPeriod {
self.rollFile()
} else if now-self.lastFlush > self.flushInterval || self.syncFlush {
self.lastFlush = now
err = self.file.flush()
}
}
}
return n, err
}
func (self *RotatingFileAtDayHandler) Rotate() {
}
func (self *RotatingFileAtDayHandler) flush() error {
return self.file.flush()
}
func (self *RotatingFileAtDayHandler) rollFile() bool {
fileName := logFileName(self.baseName)
t := time.Now().Unix()
start := t / kRollPerSeconds * kRollPerSeconds
//滚动时间间隔最小为1秒
if t > self.lastRoll {
self.lastRoll = t
self.lastFlush = t
self.startofPeriod = start
if self.file != nil {
self.file.close()
}
tmpFile, _ := newAppendFile(fileName)
self.file = tmpFile
return true
}
return false
}
func (self *RotatingFileAtDayHandler) Close() error {
return self.file.close()
}
type appendFile struct {
file *os.File
writer *bufio.Writer
writtenBytes_ int
}
func newAppendFile(filename string) (*appendFile, error) {
file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
if err != nil {
return nil, err
}
return &appendFile{
file: file,
writer: bufio.NewWriterSize(file, 64*1024),
writtenBytes_: 0,
}, nil
}
func (self *appendFile) append(b []byte) (int, error) {
length := len(b)
remain := length
offset := 0
var err error
for remain > 0 {
x, err := self.writer.Write(b[offset:])
if err != nil {
if err != io.ErrShortWrite {
break
}
}
offset = offset + x
remain = length - offset
}
self.writtenBytes_ = self.writtenBytes_ + length
return offset, err
}
func (self *appendFile) writtenBytes() int {
return self.writtenBytes_
}
func (self *appendFile) flush() error {
return self.writer.Flush()
}
func (self *appendFile) close() error {
err := self.writer.Flush()
for err != nil {
if err == io.ErrShortWrite {
err = self.writer.Flush()
} else {
break
}
}
if err != nil {
return err
}
return self.file.Close()
}

View File

@ -0,0 +1,202 @@
package handler
import (
"fmt"
"os"
"time"
"admin/lib/xlog/handler/logsyscall"
)
type RotatingDayMaxFileHandler struct {
baseName string
outPath string
fd *os.File
rotateInfo struct {
maxBytes int // 单个日志文件最大长度
maxBackupCount int // 最大归档文件数量
day time.Time // 记录跨天
}
}
// NewRotatingDayMaxFileHandler 每天00点滚动当超过大小也滚动
func NewRotatingDayMaxFileHandler(outPath, baseName string, maxBytes int, backupCount int) (*RotatingDayMaxFileHandler, error) {
if outPath == "" {
outPath = "log/"
}
h := new(RotatingDayMaxFileHandler)
h.baseName = baseName
h.outPath = outPath
h.rotateInfo.day = time.Now()
h.rotateInfo.maxBytes = maxBytes
h.rotateInfo.maxBackupCount = backupCount
fd, err := openFile(h.outPath, h.baseName, false)
if err != nil {
panic(err)
} else {
h.fd = fd
}
go h.rotateHandler()
return h, nil
}
func (h *RotatingDayMaxFileHandler) Write(p []byte) (n int, err error) {
return h.fd.Write(p)
}
func (h *RotatingDayMaxFileHandler) Close() error {
if h.fd != nil {
return h.fd.Close()
}
return nil
}
func (h *RotatingDayMaxFileHandler) rotateHandler() {
for {
time.Sleep(time.Second)
h.tryRotate()
}
}
func (h *RotatingDayMaxFileHandler) tryRotate() {
// 校验时间是否触发归档
now := time.Now()
if now.Day() != h.rotateInfo.day.Day() {
h.rotateDay()
h.rotateInfo.day = now
return
}
// 校验文件大小是否触发归档
size, err := calcFileSize(h.fd)
if err != nil {
outErrorLog("stat xlog file(%v) error:%v/home/likun/work/idlerpg-server/internal/utils/xlog", h.baseName, err)
return
}
if h.rotateInfo.maxBytes > 0 && h.rotateInfo.maxBytes <= size {
h.rotateSize()
return
}
}
// rotateAt 到点触发归档
func (h *RotatingDayMaxFileHandler) rotateDay() {
// 创建新的一天的日志文件
newFd, err := openFile(h.outPath, h.baseName, false)
if err != nil {
outErrorLog("new day rotate file, but open new file(%v) error:%v", h.baseName, err)
return
}
// 用新的一天的日志文件描述符接管当前使用的
oldFd := h.fd
h.fd = newFd
oldFd.Close()
}
// rotateSize 文件过大触发归档
func (h *RotatingDayMaxFileHandler) rotateSize() {
// 锁定文件,使触发归档的别的进程也锁住
lockFile(h.fd)
// 重新打开文件判断大小,防止文件被别的归档进程改名
curSize := calcFileNameSize(h.fd.Name())
if curSize < h.rotateInfo.maxBytes {
// 别的进程归档过了
unlockFile(h.fd)
return
}
// 滚动copy归档的文件那么归档的1号文件空出来了
baseFileName := h.fd.Name()
for i := h.rotateInfo.maxBackupCount; i > 0; i-- {
sfn := fmt.Sprintf("%s.%d", baseFileName, i)
dfn := fmt.Sprintf("%s.%d", baseFileName, i+1)
os.Rename(sfn, dfn)
}
// 将当前文件内容拷贝到归档1号文件
dfn := fmt.Sprintf("%s.1", baseFileName)
os.Rename(baseFileName, dfn)
// 重新创建当前日志文件得到新的描述符
newFd, err := os.OpenFile(baseFileName, os.O_CREATE|os.O_APPEND|os.O_RDWR|os.O_TRUNC, 0777)
if err != nil {
outErrorLog("rotate xlog file size, open file(%v) error:%v", baseFileName, err)
unlockFile(h.fd)
} else {
oldFd := h.fd
h.fd = newFd
unlockFile(h.fd)
oldFd.Close()
}
}
func calcFileSize(fd *os.File) (int, error) {
st, err := fd.Stat()
return int(st.Size()), err
}
func calcFileNameSize(fileName string) int {
fd, err := os.OpenFile(fileName, os.O_RDWR|os.O_APPEND, 0777)
if err != nil {
return 0
}
defer fd.Close()
st, err := fd.Stat()
if err != nil {
return 0
}
return int(st.Size())
}
var fileTimeStampFormat = "20060102"
func openFile(path, baseName string, isTrunc bool) (*os.File, error) {
os.MkdirAll(path, 0777)
timeNow := time.Now()
t := timeNow.Format(fileTimeStampFormat)
fullFileName := baseName + "-" + t + ".log"
if path != "" {
if path[len(path)-1] != '/' {
path += "/"
}
}
fullPathFileName := path + fullFileName
var fd *os.File
var err error
flag := os.O_CREATE | os.O_APPEND | os.O_RDWR
if isTrunc {
flag |= os.O_TRUNC
}
fd, err = os.OpenFile(fullPathFileName, flag, 0777)
if err != nil {
return fd, fmt.Errorf("open xlog file(%v) error:%v", fullPathFileName, err)
}
return fd, nil
}
func lockFile(fd *os.File) {
err := logsyscall.Flock(int(fd.Fd()), logsyscall.LOCK_EX)
if err != nil {
outErrorLog("lock file(%v) error:%v", fd.Name(), err)
}
}
func unlockFile(fd *os.File) {
err := logsyscall.Flock(int(fd.Fd()), logsyscall.LOCK_UN)
if err != nil {
outErrorLog("unlock file(%v) error:%v", fd.Name(), err)
}
}
func outErrorLog(format string, values ...interface{}) {
fmt.Fprintf(os.Stderr, format+"\n", values...)
}

View File

@ -0,0 +1,96 @@
package handler
import (
"fmt"
"os"
"path"
"time"
)
// TimeRotatingFileHandler writes xlog to a file,
// it will backup current and open a new one, with a period time you sepecified.
//
// refer: http://docs.python.org/2/library/logging.handlers.html.
// same like python TimedRotatingFileHandler.
type TimeRotatingFileHandler struct {
fd *os.File
baseName string
interval int64
suffix string
rolloverAt int64
}
const (
WhenSecond = iota
WhenMinute
WhenHour
WhenDay
)
func NewTimeRotatingFileHandler(baseName string, when int8, interval int) (*TimeRotatingFileHandler, error) {
dir := path.Dir(baseName)
os.Mkdir(dir, 0777)
h := new(TimeRotatingFileHandler)
h.baseName = baseName
switch when {
case WhenSecond:
h.interval = 1
h.suffix = "2006-01-02_15-04-05"
case WhenMinute:
h.interval = 60
h.suffix = "2006-01-02_15-04"
case WhenHour:
h.interval = 3600
h.suffix = "2006-01-02_15"
case WhenDay:
h.interval = 3600 * 24
h.suffix = "2006-01-02"
default:
return nil, fmt.Errorf("invalid when_rotate: %d", when)
}
h.interval = h.interval * int64(interval)
var err error
h.fd, err = os.OpenFile(h.baseName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return nil, err
}
fInfo, _ := h.fd.Stat()
h.rolloverAt = fInfo.ModTime().Unix() + h.interval
return h, nil
}
func (h *TimeRotatingFileHandler) doRollover() {
//refer http://hg.python.org/cpython/file/2.7/Lib/logging/handlers.py
now := time.Now()
t := now.Unix()
if h.rolloverAt <= t {
fName := h.baseName + now.Format(h.suffix)
h.fd.Close()
e := os.Rename(h.baseName, fName)
if e != nil {
panic(e)
}
h.fd, _ = os.OpenFile(h.baseName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
h.rolloverAt = t + h.interval
}
}
func (h *TimeRotatingFileHandler) Write(b []byte) (n int, err error) {
h.doRollover()
return h.fd.Write(b)
}
func (h *TimeRotatingFileHandler) Close() error {
return h.fd.Close()
}

View File

@ -0,0 +1,17 @@
package handler
// NullHandler does nothing, it discards anything.
type NullHandler struct {
}
func NewNullHandler() (*NullHandler, error) {
return new(NullHandler), nil
}
func (h *NullHandler) Write(b []byte) (n int, err error) {
return len(b), nil
}
func (h *NullHandler) Close() {
}

View File

@ -0,0 +1,65 @@
package handler
import (
"encoding/binary"
"net"
"time"
)
// SocketHandler writes xlog to a connectionl.
// Network protocol is simple: xlog length + xlog | xlog length + xlog. xlog length is uint32, bigendian.
// you must implement your own xlog server, maybe you can use logd instead simply.
type SocketHandler struct {
c net.Conn
protocol string
addr string
}
func NewSocketHandler(protocol string, addr string) (*SocketHandler, error) {
s := new(SocketHandler)
s.protocol = protocol
s.addr = addr
return s, nil
}
func (h *SocketHandler) Write(p []byte) (n int, err error) {
if err = h.connect(); err != nil {
return
}
buf := make([]byte, len(p)+4)
binary.BigEndian.PutUint32(buf, uint32(len(p)))
copy(buf[4:], p)
n, err = h.c.Write(buf)
if err != nil {
h.c.Close()
h.c = nil
}
return
}
func (h *SocketHandler) Close() error {
if h.c != nil {
h.c.Close()
}
return nil
}
func (h *SocketHandler) connect() error {
if h.c != nil {
return nil
}
var err error
h.c, err = net.DialTimeout(h.protocol, h.addr, 20*time.Second)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,28 @@
package handler
import "io"
// StreamHandler writes logs to a specified io Writer, maybe stdout, stderr, etc...
type StreamHandler struct {
w io.Writer
}
func NewStreamHandler(w io.Writer) (*StreamHandler, error) {
h := new(StreamHandler)
h.w = w
return h, nil
}
func (h *StreamHandler) Rotate() {
}
func (h *StreamHandler) Write(b []byte) (n int, err error) {
return h.w.Write(b)
}
func (h *StreamHandler) Close() error {
return nil
}

View File

@ -0,0 +1,62 @@
package handler
import (
"strconv"
"sync"
"testing"
"time"
)
func testHandler(goroutineNo int) {
h, err := NewRotatingDayMaxFileHandler("test_log", "shop", 40960, 1000)
if err != nil {
panic(err)
}
wg := new(sync.WaitGroup)
wg.Add(3)
go func(no int) {
for i := 0; i < 10000; i++ {
h.Write([]byte("test content:" + strconv.Itoa(goroutineNo) + ":" + strconv.Itoa(no) + ":" + strconv.Itoa(i) + "\n"))
time.Sleep(time.Millisecond * 2)
}
wg.Done()
}(1)
go func(no int) {
for i := 0; i < 10000; i++ {
h.Write([]byte("test content:" + strconv.Itoa(goroutineNo) + ":" + strconv.Itoa(no) + ":" + strconv.Itoa(i) + "\n"))
time.Sleep(time.Millisecond * 2)
}
wg.Done()
}(2)
go func(no int) {
for i := 0; i < 10000; i++ {
h.Write([]byte("test content:" + strconv.Itoa(goroutineNo) + ":" + strconv.Itoa(no) + ":" + strconv.Itoa(i) + "\n"))
time.Sleep(time.Millisecond * 2)
}
wg.Done()
}(3)
wg.Wait()
h.Write([]byte("tail\n"))
h.Close()
}
func TestHandler(t *testing.T) {
wg := new(sync.WaitGroup)
num := 5
wg.Add(5)
// 并发创建几个协程,模拟多进程并发写归档
// grep "test content" test_log/*|sed 's/^.*test content:\(.*\):\(.*\):\(.*\)$/\3/g'|sort|uniq -c|awk -F' ' '{print $1}'|uniq -ct pull
for i := 0; i < num; i++ {
go func(no int) {
testHandler(no)
wg.Done()
}(i)
}
wg.Wait()
}

View File

@ -0,0 +1,14 @@
//go:build darwin
// +build darwin
package logsyscall
const (
LOCK_EX int = 2
LOCK_UN int = 8
)
// Flock
func Flock(fd int, how int) (err error) {
return nil
}

View File

@ -0,0 +1,16 @@
//go:build linux
// +build linux
package logsyscall
import "syscall"
const (
LOCK_EX int = 2
LOCK_UN int = 8
)
// Flock
func Flock(fd int, how int) (err error) {
return syscall.Flock(fd, how)
}

View File

@ -0,0 +1,14 @@
//go:build windows
// +build windows
package logsyscall
const (
LOCK_EX int = 2
LOCK_UN int = 8
)
// Flock
func Flock(fd int, how int) (err error) {
return nil
}

255
admin/lib/xlog/log.go Normal file
View File

@ -0,0 +1,255 @@
package xlog
import (
"fmt"
"io"
"os"
"strconv"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
type LogLevel = zerolog.Level
type LogEvent = zerolog.Event
var (
LogLevelTrace = zerolog.TraceLevel // 追踪日志,调试完毕需要删除
LogLevelDebug = zerolog.DebugLevel // 调试日志,可以放在代码中,线上出问题调成这个级别
LogLevelInfo = zerolog.InfoLevel // 正常关键逻辑记录信息,线上日常设置为这个级别
LogLevelNotice = zerolog.Level(99) // 系统关键节点时输出的留意日志
LogLevelWarn = zerolog.WarnLevel // 警告某些逻辑出现意向不到的情况输出告警例如配置表错误、rpc错误
LogLevelError = zerolog.ErrorLevel // 错误服务器重要组件出现意向不到的情况输出错误例如数据库、redis错误
LogLevelCriti = zerolog.Level(100) // 危急用于需要开发注意的信息例如崩溃但不影响服务器运行的栈日志一般接上sms、im告警
LogLevelFatal = zerolog.FatalLevel // 致命核心组建出问题无法运行输出告警并以1的错误码退出
LogLevelPanic = zerolog.PanicLevel // 崩溃,核心组建出问题,无法运行,崩溃退出
)
var LogLevelStr2Enum = map[string]LogLevel{
"trace": LogLevelTrace,
"debug": LogLevelDebug,
"info": LogLevelInfo,
"notice": LogLevelNotice,
"warn": LogLevelWarn,
"error": LogLevelError,
"criti": LogLevelCriti,
"fatal": LogLevelFatal,
"panic": LogLevelPanic,
}
func init() {
// 设置时间格式
zerolog.TimeFieldFormat = "06/01/02 15:04:05.000"
// 修改level字段key防止跟调用方的key一样
zerolog.LevelFieldName = "log_level"
// 修改时间key
zerolog.TimestampFieldName = "log_time"
// 设置调用文件名路径深度
zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
depth := 1
for i := len(file) - 1; i > 0; i-- {
if file[i] == '/' {
if depth == FileReversedDepth {
file = file[i+1:]
break
}
depth++
}
}
return file + ":" + strconv.Itoa(line)
}
// 设置全局日志等级
zerolog.SetGlobalLevel(LogLevelTrace)
}
// 文件路径保留深度
var FileReversedDepth = 3
func NewGlobalLogger(writers []io.Writer, level LogLevel, initFun func(logger zerolog.Logger) zerolog.Logger) {
// 设置全局日志等级
zerolog.SetGlobalLevel(level)
var parentLogger zerolog.Logger
if len(writers) == 0 {
fmt.Fprintf(os.Stderr, "NewGlobalLogger but write is nil, default give os.Stdout\n")
writers = append(writers, os.Stdout)
}
multi := zerolog.MultiLevelWriter(writers...)
// 创建全局日志
parentLogger = zerolog.New(multi).With().Logger()
if initFun != nil {
log.Logger = initFun(parentLogger)
} else {
log.Logger = parentLogger
}
log.Logger = log.Hook(new(PrefixHook))
globalWriter = multi
}
func NewCustomLogger(writer Handler, initFun func(logger *zerolog.Logger) *zerolog.Logger) *zerolog.Logger {
if writer == nil {
fmt.Fprintf(os.Stderr, "NewGlobalLogger but write is nil, default give os.Stdout\n")
writer = os.Stdout
}
parentLogger := zerolog.New(writer).With().Logger()
initFun(&parentLogger)
return &parentLogger
}
var globalWriter io.Writer
func GetGlobalWriter() io.Writer {
if globalWriter == nil {
return os.Stdout
}
return globalWriter
}
// GetSubLogger 获取全局logger的子logger可以设置子logger的输出格式
func GetSubLogger() zerolog.Context {
return log.Logger.With()
}
func GetLogLevel() LogLevel {
return zerolog.GlobalLevel()
}
func SetLogLevel(level LogLevel) {
zerolog.SetGlobalLevel(level)
}
func ParseLogLevelString(level string) LogLevel {
lvEnum, find := LogLevelStr2Enum[level]
if !find {
return LogLevelDebug
}
return lvEnum
}
// transFormat
func transFormat(v ...interface{}) (string, []interface{}) {
if len(v) == 0 {
return "empty content", v
} else {
formatStr, ok := v[0].(string)
if ok {
return formatStr, v[1:]
}
formatStr = fmt.Sprint(v...)
return formatStr, []interface{}{}
}
}
func Tracef(v ...interface{}) {
format, v := transFormat(v...)
output(LogLevelTrace, format, v...)
}
func Debugf(v ...interface{}) {
format, v := transFormat(v...)
output(LogLevelDebug, format, v...)
}
func Infof(v ...interface{}) {
format, v := transFormat(v...)
output(LogLevelInfo, format, v...)
}
func Noticef(v ...interface{}) {
format, v := transFormat(v...)
output(LogLevelNotice, format, v...)
}
func Warnf(v ...interface{}) {
format, v := transFormat(v...)
output(LogLevelWarn, format, v...)
}
func Errorf(v ...interface{}) {
format, v := transFormat(v...)
output(LogLevelError, format, v...)
}
func Critif(v ...interface{}) {
format, v := transFormat(v...)
output(LogLevelCriti, format, v...)
}
func Fatalf(v ...interface{}) {
format, v := transFormat(v...)
output(LogLevelFatal, format, v...)
}
func WithLevelEvent(level LogLevel) *LogEvent {
e := log.WithLevel(level)
return e
}
func WithEventMsgF(e *LogEvent, v ...interface{}) {
format, v := transFormat(v...)
e.Timestamp().Caller(2).Msgf(format, v...)
}
func output(level LogLevel, format string, v ...interface{}) {
var e *zerolog.Event
switch level {
case LogLevelTrace:
e = log.Trace()
case LogLevelDebug:
e = log.Debug()
case LogLevelInfo:
e = log.Info()
case LogLevelNotice:
e = log.WithLevel(zerolog.NoLevel)
e.Str("log_level", "notice")
case LogLevelWarn:
e = log.Warn()
case LogLevelError:
e = log.Error()
case LogLevelCriti:
e = log.WithLevel(zerolog.NoLevel)
e.Str("log_level", "criti")
case LogLevelFatal:
e = log.Fatal()
case LogLevelPanic:
e = log.Panic()
default:
return
}
e.Timestamp().Caller(2).Msgf(format, v...)
}
func Output(level LogLevel) *zerolog.Event {
var e *zerolog.Event
switch level {
case LogLevelTrace:
e = log.Trace()
case LogLevelDebug:
e = log.Debug()
case LogLevelInfo:
e = log.Info()
case LogLevelNotice:
e = log.WithLevel(zerolog.NoLevel)
e.Str("log_level", "notice")
case LogLevelWarn:
e = log.Warn()
case LogLevelError:
e = log.Error()
case LogLevelCriti:
e = log.WithLevel(zerolog.NoLevel)
e.Str("log_level", "criti")
case LogLevelFatal:
e = log.Fatal()
case LogLevelPanic:
e = log.Panic()
default:
return nil
}
return e
}

View File

@ -0,0 +1,15 @@
package xlog
import (
"github.com/rs/zerolog"
)
type PrefixHook struct{}
func (h *PrefixHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
//e.Str("log_level", level.String())
return
if level != zerolog.NoLevel {
e.Str("severity", level.String())
}
}

30
admin/lib/xos/signal.go Normal file
View File

@ -0,0 +1,30 @@
package xos
import (
"os"
"os/signal"
"syscall"
)
func WatchSignal(notify func(signal2 os.Signal)) {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL)
for {
s := <-c
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL:
notify(s)
return
case syscall.SIGHUP:
// TODO reload
default:
return
}
}
}
func WatchSignal1() chan os.Signal {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL)
return c
}

2
ui/.env.development Normal file
View File

@ -0,0 +1,2 @@
VITE_APP_BASE_API = '/api'
VITE_APP_BASE_URL = 'http://192.168.78.128:8080/api'

30
ui/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

3
ui/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

29
ui/README.md Normal file
View File

@ -0,0 +1,29 @@
# ui
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

13
ui/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
ui/jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

3862
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
ui/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "ui",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.8.4",
"element-plus": "^2.9.7",
"pinia": "^3.0.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"unplugin-auto-import": "^19.1.2",
"unplugin-vue-components": "^28.5.0",
"vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2"
}
}

BIN
ui/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

11
ui/src/App.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<router-view />
</template>
<style scoped>
</style>

View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getProjectList(params) {
return request({
url: '/project',
method: 'get',
params: params
})
}

86
ui/src/assets/base.css Normal file
View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

1
ui/src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

35
ui/src/assets/main.css Normal file
View File

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@ -0,0 +1,26 @@
<script setup>
const props = defineProps({
rows: {}
})
console.log("rows:", props.rows)
</script>
<template>
<div>
<el-container>
<el-header>
<el-button size="large" type="primary">添加</el-button>
</el-header>
<el-main>
<el-table v-for="rows"
</el-main>
</el-container>
</div>
</template>
<style scoped>
</style>

16
ui/src/main.js Normal file
View File

@ -0,0 +1,16 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(ElementPlus)
app.use(createPinia())
app.use(router)
app.mount('#app')

27
ui/src/router/index.js Normal file
View File

@ -0,0 +1,27 @@
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: Home,
children: [
{
path: '/user',
name: 'user',
component: import('@/views/user/user.vue')
},
{
path: '/project',
name: 'project',
component: import('@/views/project/project.vue')
}
]
},
],
})
export default router

12
ui/src/stores/counter.js Normal file
View File

@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

68
ui/src/utils/request.js Normal file
View File

@ -0,0 +1,68 @@
import axios from 'axios'
import {ElMessageBox} from "element-plus";
// import LocalCache from "@/stores/localCache";
import {useRouter} from "vue-router";
// import ExpireCache from "@/stores/expireCache";
// 创建axios实例
const service=axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 10000,
headers: {
"Content-type": "application/json;charset=utf-8",
"Cache-Control": 'no-cache',
}
})
// axios.post('192.168.1.60:8888/login', {username: "likun", password: "likun1"}).then((res)=>{
// console.log(res)
// }).catch((err)=>{
// console.log('login error')
// console.log(err)
// })
const reqInterceptor = (config)=>{
config.headers=config.headers || {}
// const token = ExpireCache.getCache('token')
// if(token){
// config.headers["x-token"]=token || ""
// } else {
// console.log("not found local storage token")
// }
return config
}
const resInterceptor = (res)=>{
console.log("res:", res.data)
const code=res.data.code
if(code!=200) {
console.log("interceptor err code", res)
ElMessageBox.alert("请求服务器成功,但是逻辑错误:" + res.data.message, "服务器错误码" + code, {
type: "warning",
confirmButtonText: '知道了',
})
return Promise.reject(res.data)
}
return res.data
}
const resErrorInterceptor = (err)=>{
console.log(err)
const code = err.response && err.response.status || -1
const message = err.response && err.response.data.message || err
ElMessageBox.alert(message, "请求服务器返回http错误码-" + code, {
type: "error",
confirmButtonText: '知道了',
})
return Promise.reject(err)
}
// 请求拦截
service.interceptors.request.use(reqInterceptor)
// 响应拦截
service.interceptors.response.use(resInterceptor, resErrorInterceptor)
export default service

43
ui/src/views/Home.vue Normal file
View File

@ -0,0 +1,43 @@
<script setup>
import { useRouter, useRoute } from 'vue-router'
import { computed } from 'vue'
import avatarUrl from '@/assets/icon/header.png'
const router = useRouter()
const route = useRoute()
//
const activeMenu = computed(() => route.path)
//
const handleMenuSelect = (index) => {
router.push(index)
}
</script>
<template>
<main>
<div>
<el-container>
<el-aside width="200px">
<el-avatar shape="square" :size="100" :src="avatarUrl"></el-avatar>
<el-menu
:default-active="activeMenu"
@select="handleMenuSelect"
>
<el-menu-item index="/user">
<span>用户管理</span>
</el-menu-item>
<el-menu-item index="/project">
<span>项目管理</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header>Header</el-header>
<el-header><router-view/></el-header>
</el-container>
</el-container>
</div>
</main>
</template>

View File

@ -0,0 +1,30 @@
<script setup>
import {ref} from "vue";
import {getProjectList} from '@/api/project/project.js'
import tableComp from "@/components/restful/table.vue"
const rows = ref([])
const getProjectListOk = ref(false)
getProjectList({page_no: 0, page_len: 100}).then((response) => {
console.log(response)
rows.value = response.data
getProjectListOk.value = true
}, (err) => {
console.log(err)
})
</script>
<template>
项目
<div>
<template v-if="getProjectListOk">
<component :is="tableComp" :rows="rows"></component>
</template>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,11 @@
<script setup>
</script>
<template>
用户管理
</template>
<style scoped>
</style>

58
ui/vite.config.js Normal file
View File

@ -0,0 +1,58 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { loadEnv } from 'vite'
// https://vite.dev/config/
export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, process.cwd())
const { VITE_APP_ENV, VITE_APP_BASE_URL,VITE_APP_BASE_URL_WS} = env
return {
plugins: [
vue(),
vueDevTools(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
// vite 相关配置
server: {
port: 5173,
host: true,
open: false,
proxy: {
// https://cn.vitejs.dev/config/#server-proxy
'/api': {
target: VITE_APP_BASE_URL,
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
},
// '/dev-ws': {
// target: VITE_APP_BASE_URL_WS,
// changeOrigin: true,
// rewrite: (p) => p.replace(/^\/dev-ws/, ''),
// ws: true
// }
}
},
}
})