mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	feat: 优化后台UI
This commit is contained in:
		@@ -146,6 +146,7 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
 | 
			
		||||
		if c.Request.URL.Path == "/api/user/login" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/user/resetPass" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/admin/login" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/admin/login/captcha" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/user/register" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/chat/history" ||
 | 
			
		||||
			c.Request.URL.Path == "/api/chat/detail" ||
 | 
			
		||||
 
 | 
			
		||||
@@ -124,8 +124,10 @@ func (c RedisConfig) Url() string {
 | 
			
		||||
 | 
			
		||||
// Manager 管理员
 | 
			
		||||
type Manager struct {
 | 
			
		||||
	Username string `json:"username"`
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
	Username  string `json:"username"`
 | 
			
		||||
	Password  string `json:"password"`
 | 
			
		||||
	Captcha   string `json:"captcha"`    // 验证码
 | 
			
		||||
	CaptchaId string `json:"captcha_id"` // 验证码id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ChatConfig 系统默认的聊天配置
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								api/go.mod
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								api/go.mod
									
									
									
									
									
								
							@@ -27,7 +27,15 @@ require github.com/xxl-job/xxl-job-executor-go v1.2.0
 | 
			
		||||
 | 
			
		||||
require github.com/bg5t/mydiscordgo v0.28.1
 | 
			
		||||
 | 
			
		||||
require github.com/shopspring/decimal v1.3.1 // indirect
 | 
			
		||||
require (
 | 
			
		||||
	github.com/mojocn/base64Captcha v1.3.1
 | 
			
		||||
	github.com/shopspring/decimal v1.3.1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
 | 
			
		||||
	golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/andybalholm/brotli v1.0.4 // indirect
 | 
			
		||||
 
 | 
			
		||||
@@ -65,6 +65,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
 | 
			
		||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
 | 
			
		||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
 | 
			
		||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 | 
			
		||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
 | 
			
		||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
 | 
			
		||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
 | 
			
		||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 | 
			
		||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 | 
			
		||||
@@ -129,6 +131,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
 | 
			
		||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 | 
			
		||||
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/mojocn/base64Captcha v1.3.1 h1:2Wbkt8Oc8qjmNJ5GyOfSo4tgVQPsbKMftqASnq8GlT0=
 | 
			
		||||
github.com/mojocn/base64Captcha v1.3.1/go.mod h1:wAQCKEc5bDujxKRmbT6/vTnTt5CjStQ8bRfPWUuz/iY=
 | 
			
		||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
 | 
			
		||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
 | 
			
		||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 | 
			
		||||
@@ -229,6 +233,8 @@ golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
 | 
			
		||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
 | 
			
		||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 | 
			
		||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 | 
			
		||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 h1:TbGuee8sSq15Iguxu4deQ7+Bqq/d2rsQejGcEtADAMQ=
 | 
			
		||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 | 
			
		||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
			
		||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 | 
			
		||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"github.com/mojocn/base64Captcha"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
@@ -36,6 +37,13 @@ func (h *ManagerHandler) Login(c *gin.Context) {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// add captcha
 | 
			
		||||
	if !base64Captcha.DefaultMemStore.Verify(data.CaptchaId, data.Captcha, true) {
 | 
			
		||||
		resp.ERROR(c, "验证码错误,请重新输入!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	manager := h.App.Config.Manager
 | 
			
		||||
	if data.Username == manager.Username && data.Password == manager.Password {
 | 
			
		||||
		// 创建 token
 | 
			
		||||
 
 | 
			
		||||
@@ -64,11 +64,10 @@ func (h *SysUserHandler) List(c *gin.Context) {
 | 
			
		||||
// Save 更新或者新增
 | 
			
		||||
func (h *SysUserHandler) Save(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Id          uint   `json:"id"`
 | 
			
		||||
		Password    string `json:"password"`
 | 
			
		||||
		Username    string `json:"username"`
 | 
			
		||||
		ExpiredTime string `json:"expired_time"`
 | 
			
		||||
		Status      bool   `json:"status"`
 | 
			
		||||
		Id       uint   `json:"id"`
 | 
			
		||||
		Password string `json:"password"`
 | 
			
		||||
		Username string `json:"username"`
 | 
			
		||||
		Status   bool   `json:"status"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
@@ -81,18 +80,16 @@ func (h *SysUserHandler) Save(c *gin.Context) {
 | 
			
		||||
		user.Id = data.Id
 | 
			
		||||
		// 此处需要用 map 更新,用结构体无法更新 0 值
 | 
			
		||||
		res = h.db.Model(&user).Updates(map[string]interface{}{
 | 
			
		||||
			"username":     data.Username,
 | 
			
		||||
			"status":       data.Status,
 | 
			
		||||
			"expired_time": utils.Str2stamp(data.ExpiredTime),
 | 
			
		||||
			"username": data.Username,
 | 
			
		||||
			"status":   data.Status,
 | 
			
		||||
		})
 | 
			
		||||
	} else {
 | 
			
		||||
		salt := utils.RandString(8)
 | 
			
		||||
		u := model.AdminUser{
 | 
			
		||||
			Username:    data.Username,
 | 
			
		||||
			Password:    utils.GenPassword(data.Password, salt),
 | 
			
		||||
			Salt:        salt,
 | 
			
		||||
			Status:      true,
 | 
			
		||||
			ExpiredTime: utils.Str2stamp(data.ExpiredTime),
 | 
			
		||||
			Username: data.Username,
 | 
			
		||||
			Password: utils.GenPassword(data.Password, salt),
 | 
			
		||||
			Salt:     salt,
 | 
			
		||||
			Status:   true,
 | 
			
		||||
		}
 | 
			
		||||
		res = h.db.Create(&u)
 | 
			
		||||
		_ = utils.CopyObject(u, &userVo)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								api/handler/admin/captcha_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								api/handler/admin/captcha_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/mojocn/base64Captcha"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type CaptchaHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewCaptchaHandler(app *core.AppServer) *CaptchaHandler {
 | 
			
		||||
	h := CaptchaHandler{}
 | 
			
		||||
	h.App = app
 | 
			
		||||
	return &h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CaptchaVo struct {
 | 
			
		||||
	CaptchaId string `json:"captcha_id"`
 | 
			
		||||
	PicPath   string `json:"pic_path"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCaptcha 获取验证码
 | 
			
		||||
func (h *CaptchaHandler) GetCaptcha(c *gin.Context) {
 | 
			
		||||
	var captchaVo CaptchaVo
 | 
			
		||||
	driver := base64Captcha.NewDriverDigit(48, 130, 4, 0.4, 10)
 | 
			
		||||
	cp := base64Captcha.NewCaptcha(driver, base64Captcha.DefaultMemStore)
 | 
			
		||||
	// b64s是图片的base64编码
 | 
			
		||||
	id, b64s, err := cp.Generate()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "生成验证码错误!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	captchaVo.CaptchaId = id
 | 
			
		||||
	captchaVo.PicPath = b64s
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, captchaVo)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										97
									
								
								api/handler/admin/upload_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								api/handler/admin/upload_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
package admin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/handler"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
	"chatplus/store/vo"
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UploadHandler struct {
 | 
			
		||||
	handler.BaseHandler
 | 
			
		||||
	db              *gorm.DB
 | 
			
		||||
	uploaderManager *oss.UploaderManager
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler {
 | 
			
		||||
	adminHandler := &UploadHandler{db: db, uploaderManager: manager}
 | 
			
		||||
	adminHandler.App = app
 | 
			
		||||
	return adminHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UploadHandler) Upload(c *gin.Context) {
 | 
			
		||||
	file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	userId := 0
 | 
			
		||||
	res := h.db.Create(&model.File{
 | 
			
		||||
		UserId:    userId,
 | 
			
		||||
		Name:      file.Name,
 | 
			
		||||
		ObjKey:    file.ObjKey,
 | 
			
		||||
		URL:       file.URL,
 | 
			
		||||
		Ext:       file.Ext,
 | 
			
		||||
		Size:      file.Size,
 | 
			
		||||
		CreatedAt: time.Time{},
 | 
			
		||||
	})
 | 
			
		||||
	if res.Error != nil || res.RowsAffected == 0 {
 | 
			
		||||
		resp.ERROR(c, "error with update database: "+res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, file)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UploadHandler) List(c *gin.Context) {
 | 
			
		||||
	userId := 0
 | 
			
		||||
	var items []model.File
 | 
			
		||||
	var files = make([]vo.File, 0)
 | 
			
		||||
	h.db.Where("user_id = ?", userId).Find(&items)
 | 
			
		||||
	if len(items) > 0 {
 | 
			
		||||
		for _, v := range items {
 | 
			
		||||
			var file vo.File
 | 
			
		||||
			err := utils.CopyObject(v, &file)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			file.CreatedAt = v.CreatedAt.Unix()
 | 
			
		||||
			files = append(files, file)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, files)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Remove remove files
 | 
			
		||||
func (h *UploadHandler) Remove(c *gin.Context) {
 | 
			
		||||
	userId := 0
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
	var file model.File
 | 
			
		||||
	tx := h.db.Where("user_id = ? AND id = ?", userId, id).First(&file)
 | 
			
		||||
	if tx.Error != nil || file.Id == 0 {
 | 
			
		||||
		resp.ERROR(c, "file not existed")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// remove database
 | 
			
		||||
	tx = h.db.Model(&model.File{}).Delete("id = ?", id)
 | 
			
		||||
	if tx.Error != nil || tx.RowsAffected == 0 {
 | 
			
		||||
		resp.ERROR(c, "failed to update database")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// remove files
 | 
			
		||||
	objectKey := file.ObjKey
 | 
			
		||||
	if objectKey == "" {
 | 
			
		||||
		objectKey = file.URL
 | 
			
		||||
	}
 | 
			
		||||
	_ = h.uploaderManager.GetUploadHandler().Delete(objectKey)
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
@@ -33,7 +33,7 @@ func (h *UploadHandler) Upload(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	res := h.db.Create(&model.File{
 | 
			
		||||
		UserId:    userId,
 | 
			
		||||
		UserId:    int(userId),
 | 
			
		||||
		Name:      file.Name,
 | 
			
		||||
		ObjKey:    file.ObjKey,
 | 
			
		||||
		URL:       file.URL,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								api/main.go
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								api/main.go
									
									
									
									
									
								
							@@ -369,16 +369,30 @@ func main() {
 | 
			
		||||
			group.GET("token", h.GenToken)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		// 系统管理员
 | 
			
		||||
		fx.Provide(admin.NewSysUserHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.SysUserHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/sysUser/")
 | 
			
		||||
			group.POST("save", h.Save)
 | 
			
		||||
			group.GET("list", h.List)
 | 
			
		||||
			group.POST("remove", h.Remove)
 | 
			
		||||
			group.POST("resetPass", h.ResetPass)
 | 
			
		||||
		// 验证码
 | 
			
		||||
		fx.Provide(admin.NewCaptchaHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.CaptchaHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/login/")
 | 
			
		||||
			group.GET("captcha", h.GetCaptcha)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(admin.NewUploadHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.UploadHandler) {
 | 
			
		||||
			s.Engine.POST("/api/admin/upload", h.Upload)
 | 
			
		||||
			s.Engine.GET("/api/admin/upload/list", h.List)
 | 
			
		||||
			s.Engine.GET("/api/admin/upload/remove", h.Remove)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		//// 系统管理员
 | 
			
		||||
		//fx.Provide(admin.NewSysUserHandler),
 | 
			
		||||
		//fx.Invoke(func(s *core.AppServer, h *admin.SysUserHandler) {
 | 
			
		||||
		//	group := s.Engine.Group("/api/admin/sysUser/")
 | 
			
		||||
		//	group.POST("save", h.Save)
 | 
			
		||||
		//	group.GET("list", h.List)
 | 
			
		||||
		//	group.POST("remove", h.Remove)
 | 
			
		||||
		//	group.POST("resetPass", h.ResetPass)
 | 
			
		||||
		//}),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(handler.NewFunctionHandler),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/function/")
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ type AdminUser struct {
 | 
			
		||||
	Username    string
 | 
			
		||||
	Password    string
 | 
			
		||||
	Salt        string // 密码盐
 | 
			
		||||
	ExpiredTime int64  // 账户到期时间
 | 
			
		||||
	Status      bool   `gorm:"default:true"` // 当前状态
 | 
			
		||||
	LastLoginAt int64  // 最后登录时间
 | 
			
		||||
	LastLoginIp string // 最后登录 IP
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import "time"
 | 
			
		||||
 | 
			
		||||
type File struct {
 | 
			
		||||
	Id        uint `gorm:"primarykey;column:id"`
 | 
			
		||||
	UserId    uint
 | 
			
		||||
	UserId    int
 | 
			
		||||
	Name      string
 | 
			
		||||
	ObjKey    string
 | 
			
		||||
	URL       string
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ type AdminUser struct {
 | 
			
		||||
	BaseVo
 | 
			
		||||
	Username    string `json:"username"`
 | 
			
		||||
	Salt        string `json:"salt"`          // 密码盐
 | 
			
		||||
	ExpiredTime int64  `json:"expired_time"`  // 账户到期时间
 | 
			
		||||
	Status      bool   `json:"status"`        // 当前状态
 | 
			
		||||
	LastLoginAt int64  `json:"last_login_at"` // 最后登录时间
 | 
			
		||||
	LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user