🎉初步完成用户
This commit is contained in:
commit
f375118c88
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
### Go template
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
*.db
|
||||||
|
app.yaml
|
||||||
|
|
||||||
|
logs/*
|
||||||
|
logs
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
6
.idea/git_toolbox_blame.xml
generated
Normal file
6
.idea/git_toolbox_blame.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GitToolBoxBlameSettings">
|
||||||
|
<option name="version" value="2" />
|
||||||
|
</component>
|
||||||
|
</project>
|
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="GoDfaErrorMayBeNotNil" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<functions>
|
||||||
|
<function importPath="github.com/golang-jwt/jwt/v5" name="ParseWithClaims" />
|
||||||
|
</functions>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/website-nav.iml" filepath="$PROJECT_DIR$/.idea/website-nav.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
8
.idea/watcherTasks.xml
generated
Normal file
8
.idea/watcherTasks.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectTasksOptions">
|
||||||
|
<enabled-global>
|
||||||
|
<option value="go fmt" />
|
||||||
|
</enabled-global>
|
||||||
|
</component>
|
||||||
|
</project>
|
9
.idea/website-nav.iml
generated
Normal file
9
.idea/website-nav.iml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
55
component/captcha.go
Normal file
55
component/captcha.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package component
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"website-nav/global/client"
|
||||||
|
"website-nav/global/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Captcha struct{}
|
||||||
|
|
||||||
|
// Set
|
||||||
|
// @description: 验证码放入指定存储
|
||||||
|
// @receiver Captcha
|
||||||
|
// @param id
|
||||||
|
// @param value
|
||||||
|
// @return error
|
||||||
|
func (Captcha) Set(id string, value string) error {
|
||||||
|
return client.Cache.Set([]byte(fmt.Sprintf("%s:%s", constant.Captcha, id)), []byte(value), 2*60)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get
|
||||||
|
// @description: 获取验证码信息
|
||||||
|
// @receiver Captcha
|
||||||
|
// @param id
|
||||||
|
// @param clear
|
||||||
|
// @return string
|
||||||
|
func (Captcha) Get(id string, clear bool) string {
|
||||||
|
val, err := client.Cache.Get([]byte(fmt.Sprintf("%s:%s", constant.Captcha, id)))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if clear {
|
||||||
|
client.Cache.Del([]byte(fmt.Sprintf("%s:%s", constant.Captcha, id)))
|
||||||
|
return string(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
// @description: 校验
|
||||||
|
// @receiver Captcha
|
||||||
|
// @param id
|
||||||
|
// @param answer
|
||||||
|
// @param clear
|
||||||
|
// @return bool
|
||||||
|
func (c Captcha) Verify(id, answer string, clear bool) bool {
|
||||||
|
if os.Getenv("GIN_MODE") != "release" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.ToUpper(answer) == strings.ToUpper(c.Get(id, clear))
|
||||||
|
}
|
135
component/jwt.go
Normal file
135
component/jwt.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package component
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"website-nav/config"
|
||||||
|
"website-nav/global/client"
|
||||||
|
"website-nav/global/constant"
|
||||||
|
"website-nav/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JwtComponent struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT
|
||||||
|
// @description: 初始化JWT组件
|
||||||
|
// @return JwtComponent
|
||||||
|
func JWT() JwtComponent {
|
||||||
|
return JwtComponent{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken
|
||||||
|
// @description: 生成token
|
||||||
|
// @receiver JwtComponent
|
||||||
|
// @param userId
|
||||||
|
// @param password
|
||||||
|
// @return token
|
||||||
|
// @return expireTime
|
||||||
|
// @return err
|
||||||
|
func (JwtComponent) GenerateToken(userId, secret string, times ...time.Time) (token string, expireTime *jwt.NumericDate, err error) {
|
||||||
|
var notBefore, issuedAt *jwt.NumericDate
|
||||||
|
if len(times) != 0 {
|
||||||
|
expireTime = jwt.NewNumericDate(times[0])
|
||||||
|
notBefore = jwt.NewNumericDate(times[1])
|
||||||
|
issuedAt = jwt.NewNumericDate(times[1])
|
||||||
|
} else {
|
||||||
|
timeNow := time.Now().Local()
|
||||||
|
expireTime = jwt.NewNumericDate(timeNow.Add(7 * time.Hour))
|
||||||
|
notBefore = jwt.NewNumericDate(timeNow)
|
||||||
|
issuedAt = jwt.NewNumericDate(timeNow)
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := JwtComponent{
|
||||||
|
ID: userId,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
Issuer: config.GlobalConfig.Http.Endpoint, // 颁发站点
|
||||||
|
Subject: "you can you up,no can no bb", // 发布主题
|
||||||
|
ExpiresAt: expireTime, // 过期时间
|
||||||
|
NotBefore: notBefore, // token不得早于该时间
|
||||||
|
IssuedAt: issuedAt, // token颁发时间
|
||||||
|
ID: strings.ReplaceAll(uuid.NewString(), "-", ""), // 该token的id
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
||||||
|
token, err = t.SignedString([]byte(secret))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("生成token失败: %v", err.Error())
|
||||||
|
return "", nil, errors.New("生成token失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = client.Cache.Set([]byte(fmt.Sprintf("%s:%s", constant.UserToken, userId)),
|
||||||
|
[]byte(token),
|
||||||
|
int(expireTime.Sub(time.Now()).Abs().Seconds()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseToken
|
||||||
|
// @description: 解析token
|
||||||
|
// @receiver JwtComponent
|
||||||
|
// @param token
|
||||||
|
// @return *JwtComponent
|
||||||
|
// @return error
|
||||||
|
func (JwtComponent) ParseToken(token, secret string) (*JwtComponent, error) {
|
||||||
|
tokenStr := strings.Split(token, "Bearer ")[1]
|
||||||
|
|
||||||
|
t, err := jwt.ParseWithClaims(tokenStr, &JwtComponent{}, func(token *jwt.Token) (any, error) {
|
||||||
|
return []byte(secret), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if claims, ok := t.Claims.(*JwtComponent); ok && t.Valid {
|
||||||
|
userToken, err := client.Cache.Get([]byte(fmt.Sprintf("%s:%s", constant.UserToken, claims.ID)))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error())
|
||||||
|
return nil, errors.New("token不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(userToken) != tokenStr {
|
||||||
|
log.Errorf("token不一致")
|
||||||
|
return nil, errors.New("token错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSecret
|
||||||
|
// @description: 生成token解析密钥【每个用户的secret不一样,提高安全性】
|
||||||
|
// @receiver JwtComponent
|
||||||
|
// @param secret
|
||||||
|
// @return string
|
||||||
|
func (JwtComponent) GenerateSecret(secret ...string) string {
|
||||||
|
// 添加10个元素,增加随机性
|
||||||
|
for i := 0; i <= 10; i++ {
|
||||||
|
secret = append(secret, uuid.NewString())
|
||||||
|
}
|
||||||
|
// 混淆一下明文secret的顺序
|
||||||
|
n := len(secret)
|
||||||
|
for i := n - 1; i > 0; i-- {
|
||||||
|
j := rand.Intn(i + 1)
|
||||||
|
secret[i], secret[j] = secret[j], secret[i]
|
||||||
|
}
|
||||||
|
secretStr := strings.Join(secret, ".")
|
||||||
|
return utils.Hash().MD5(utils.Hash().SHA256(utils.Hash().SHA512(secretStr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
// @description: 退出登陆
|
||||||
|
// @receiver JwtComponent
|
||||||
|
// @param userId
|
||||||
|
// @return error
|
||||||
|
func (JwtComponent) Logout(userId string) error {
|
||||||
|
_ = client.Cache.Del([]byte(fmt.Sprintf("%s:%s", constant.UserToken, userId)))
|
||||||
|
return nil
|
||||||
|
}
|
97
component/validator.go
Normal file
97
component/validator.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package component
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
"github.com/go-playground/locales/en"
|
||||||
|
"github.com/go-playground/locales/zh"
|
||||||
|
ut "github.com/go-playground/universal-translator"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
zhTranslations "github.com/go-playground/validator/v10/translations/zh"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Validator struct{}
|
||||||
|
|
||||||
|
var validatorTrans ut.Translator
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initValidatorTranslator()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Validate() Validator {
|
||||||
|
return Validator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Validator) Error(err error) string {
|
||||||
|
var errs validator.ValidationErrors
|
||||||
|
if ok := errors.As(err, &errs); !ok {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
errMap := errs.Translate(validatorTrans)
|
||||||
|
|
||||||
|
var errMsg string
|
||||||
|
for _, v := range errMap {
|
||||||
|
errMsg = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return errMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// initValidatorTranslator
|
||||||
|
// @description: 初始化翻译机
|
||||||
|
// @receiver vli
|
||||||
|
func initValidatorTranslator() {
|
||||||
|
//修改gin框架中的Validator属性,实现自定制
|
||||||
|
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
||||||
|
// 注册一个获取json tag的自定义方法
|
||||||
|
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
|
||||||
|
name := strings.SplitN(fld.Tag.Get("label"), ",", 2)[0]
|
||||||
|
if name == "-" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
})
|
||||||
|
|
||||||
|
zhT := zh.New() //中文翻译器
|
||||||
|
enT := en.New() //英文翻译器
|
||||||
|
|
||||||
|
// 第一个参数是备用(fallback)的语言环境
|
||||||
|
// 后面的参数是应该支持的语言环境(支持多个)
|
||||||
|
// uni := ut.New(zhT, zhT) 也是可以的
|
||||||
|
uni := ut.New(enT, zhT, enT)
|
||||||
|
|
||||||
|
// locale 通常取决于 http 请求头的 'Accept-Language'
|
||||||
|
var ok bool
|
||||||
|
// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
|
||||||
|
validatorTrans, ok = uni.GetTranslator("zh")
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("获取翻译机失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := overrideTranslator(v, validatorTrans)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("覆盖原有翻译失败: %v", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideTranslator
|
||||||
|
// @description: 覆盖原有翻译
|
||||||
|
// @param v
|
||||||
|
// @param translator
|
||||||
|
// @return error
|
||||||
|
func overrideTranslator(v *validator.Validate, translator ut.Translator) error {
|
||||||
|
err := zhTranslations.RegisterDefaultTranslations(v, translator)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
9
config/cache.go
Normal file
9
config/cache.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type cache struct {
|
||||||
|
Driver string `yaml:"driver"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Db int `yaml:"db"`
|
||||||
|
}
|
10
config/config.go
Normal file
10
config/config.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
var GlobalConfig *config
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Http *http `yaml:"http"`
|
||||||
|
Database *database `yaml:"database"`
|
||||||
|
Cache *cache `yaml:"cache"`
|
||||||
|
File *file `yaml:"file"`
|
||||||
|
}
|
30
config/database.go
Normal file
30
config/database.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type database struct {
|
||||||
|
Driver string `yaml:"driver"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
User string `yaml:"user"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Database string `yaml:"database"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDSN
|
||||||
|
// @description: 获取链接字符串
|
||||||
|
// @receiver d
|
||||||
|
// @return string
|
||||||
|
func (d *database) GetDSN() string {
|
||||||
|
var dsn string
|
||||||
|
switch d.Driver {
|
||||||
|
case "mysql":
|
||||||
|
dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", d.User, d.Password, d.Host, d.Port, d.Database)
|
||||||
|
case "pgsql":
|
||||||
|
dsn = fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai", d.Host, d.User, d.Password, d.Database, d.Port)
|
||||||
|
case "sqlite":
|
||||||
|
dsn = fmt.Sprintf("%s.db", d.Database)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dsn
|
||||||
|
}
|
10
config/file.go
Normal file
10
config/file.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type file struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Endpoint string `yaml:"endpoint"`
|
||||||
|
AccessId string `yaml:"accessId"`
|
||||||
|
AccessSecret string `yaml:"accessSecret"`
|
||||||
|
BucketName string `yaml:"bucketName"`
|
||||||
|
}
|
6
config/http.go
Normal file
6
config/http.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type http struct {
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Endpoint string `yaml:"endpoint"`
|
||||||
|
}
|
15
global/client/client.go
Normal file
15
global/client/client.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/coocood/freecache"
|
||||||
|
"github.com/cowardmrx/go_aliyun_oss"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DB *gorm.DB
|
||||||
|
Cache *freecache.Cache
|
||||||
|
OSS *go_aliyun_oss.AliOssClient // oss客户端
|
||||||
|
HttpClient *resty.Client // http客户端
|
||||||
|
)
|
6
global/constant/cache.go
Normal file
6
global/constant/cache.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const (
|
||||||
|
Captcha = "captcha"
|
||||||
|
UserToken = "token"
|
||||||
|
)
|
108
go.mod
Normal file
108
go.mod
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
module website-nav
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
gitee.ltd/lxh/logger v1.0.15
|
||||||
|
github.com/coocood/freecache v1.2.4
|
||||||
|
github.com/cowardmrx/go_aliyun_oss v1.0.7
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/glebarez/sqlite v1.11.0
|
||||||
|
github.com/go-playground/locales v0.14.1
|
||||||
|
github.com/go-playground/universal-translator v0.18.1
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0
|
||||||
|
github.com/go-resty/resty/v2 v2.15.3
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/mojocn/base64Captcha v1.3.6
|
||||||
|
github.com/spf13/viper v1.19.0
|
||||||
|
golang.org/x/crypto v0.25.0
|
||||||
|
gorm.io/driver/mysql v1.5.7
|
||||||
|
gorm.io/driver/postgres v1.5.9
|
||||||
|
gorm.io/gorm v1.25.11
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/caarlos0/env/v6 v6.10.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
|
github.com/go-kit/kit v0.12.0 // indirect
|
||||||
|
github.com/go-kit/log v0.2.1 // indirect
|
||||||
|
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/jpillora/backoff v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/lixh00/loki-client-go v1.0.1 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||||
|
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.13.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
|
github.com/prometheus/common v0.37.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.8.0 // indirect
|
||||||
|
github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
|
go.uber.org/zap v1.23.0 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
|
golang.org/x/image v0.13.0 // indirect
|
||||||
|
golang.org/x/net v0.27.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.18.0 // indirect
|
||||||
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
|
golang.org/x/text v0.16.0 // indirect
|
||||||
|
golang.org/x/time v0.6.0 // indirect
|
||||||
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
|
||||||
|
google.golang.org/grpc v1.62.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.22.5 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.5.0 // indirect
|
||||||
|
modernc.org/sqlite v1.23.1 // indirect
|
||||||
|
)
|
20
http/api/admin/base.go
Normal file
20
http/api/admin/base.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"website-nav/http/response"
|
||||||
|
"website-nav/http/vo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCurrentLoginUser
|
||||||
|
// @description: 获取当前登陆用户
|
||||||
|
// @param c
|
||||||
|
// @return *vo.User
|
||||||
|
func GetCurrentLoginUser(c *gin.Context) *vo.User {
|
||||||
|
if user, ok := c.Get("user"); ok {
|
||||||
|
return user.(*vo.User)
|
||||||
|
}
|
||||||
|
response.R(c).AuthorizationFailed("暂未登陆")
|
||||||
|
c.Abort()
|
||||||
|
return nil
|
||||||
|
}
|
111
http/api/admin/login.go
Normal file
111
http/api/admin/login.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/mojocn/base64Captcha"
|
||||||
|
"time"
|
||||||
|
"website-nav/component"
|
||||||
|
"website-nav/http/param"
|
||||||
|
"website-nav/http/response"
|
||||||
|
"website-nav/http/vo"
|
||||||
|
"website-nav/service"
|
||||||
|
"website-nav/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Login struct{}
|
||||||
|
|
||||||
|
func LoginAPI() Login {
|
||||||
|
return Login{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Captcha
|
||||||
|
// @description: 获取验证码
|
||||||
|
// @receiver login
|
||||||
|
// @param c
|
||||||
|
func (Login) Captcha(c *gin.Context) {
|
||||||
|
math := base64Captcha.DriverMath{Height: 120, Width: 480, Fonts: []string{"ApothecaryFont.ttf", "3Dumb.ttf"}}
|
||||||
|
mathDriver := math.ConvertFonts()
|
||||||
|
|
||||||
|
capt := base64Captcha.NewCaptcha(mathDriver, component.Captcha{})
|
||||||
|
|
||||||
|
id, base64Str, _, err := capt.Generate()
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError(fmt.Errorf("生成验证码失败: %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OkWithData(map[string]any{
|
||||||
|
"id": id,
|
||||||
|
"captcha": base64Str,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login
|
||||||
|
// @description: 登陆
|
||||||
|
// @receiver login
|
||||||
|
// @param c
|
||||||
|
func (Login) Login(c *gin.Context) {
|
||||||
|
var p param.Login
|
||||||
|
if err := c.ShouldBind(&p); err != nil {
|
||||||
|
response.R(c).Validator(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证验证码是否正确
|
||||||
|
ok := component.Captcha{}.Verify(p.CaptchaId, p.CaptchaCode, true)
|
||||||
|
if !ok {
|
||||||
|
response.R(c).FailedWithError("验证码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证码正确,查询用户信息
|
||||||
|
user, err := service.User().GetUserByAccount(p.Account)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("获取用户信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对比密码
|
||||||
|
if !utils.Password().ComparePassword(user.Password, p.Password) {
|
||||||
|
response.R(c).FailedWithError("密码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := component.JWT().GenerateSecret(p.Password, uuid.NewString(), time.Now().Local().String())
|
||||||
|
// 生成token
|
||||||
|
token, expireAt, err := component.JWT().GenerateToken(user.Id, secret)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("用户[%s]生成token失败: %v", user.Account, err.Error())
|
||||||
|
response.R(c).FailedWithError("登陆失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Writer.Header().Set("X-TOKEN", secret)
|
||||||
|
response.R(c).OkWithData(map[string]any{
|
||||||
|
"token": token,
|
||||||
|
"type": "Bearer",
|
||||||
|
"expireAt": expireAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
// @description: 退出登陆
|
||||||
|
// @receiver LoginApi
|
||||||
|
// @param c
|
||||||
|
func (Login) Logout(c *gin.Context) {
|
||||||
|
loginUser, ok := c.Get("user")
|
||||||
|
if !ok {
|
||||||
|
response.R(c).AuthorizationFailed("未登陆")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := component.JWT().Logout(loginUser.(*vo.User).Id); err != nil {
|
||||||
|
response.R(c).FailedWithError("退出登陆失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
185
http/api/admin/user.go
Normal file
185
http/api/admin/user.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"website-nav/http/param"
|
||||||
|
"website-nav/http/response"
|
||||||
|
"website-nav/http/vo"
|
||||||
|
"website-nav/model"
|
||||||
|
"website-nav/service"
|
||||||
|
"website-nav/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct{}
|
||||||
|
|
||||||
|
func UserAPI() User {
|
||||||
|
return User{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLoginUser
|
||||||
|
// @description: 获取当前登陆用户信息
|
||||||
|
// @receiver u
|
||||||
|
// @param c
|
||||||
|
func (u User) GetLoginUser(c *gin.Context) {
|
||||||
|
var loginUser *vo.User
|
||||||
|
if loginUser = GetCurrentLoginUser(c); c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.R(c).OkWithData(loginUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List
|
||||||
|
// @description: 用户列表
|
||||||
|
// @receiver u
|
||||||
|
// @param c
|
||||||
|
func (u User) List(c *gin.Context) {
|
||||||
|
var p param.UserList
|
||||||
|
if err := c.ShouldBind(&p); err != nil {
|
||||||
|
response.R(c).Validator(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, total, err := service.User().List(p)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).Paginate(data, total, p.Current, p.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
// @description: 新增/编辑用户
|
||||||
|
// @receiver u
|
||||||
|
// @param c
|
||||||
|
func (u User) Save(c *gin.Context) {
|
||||||
|
var p param.SaveUser
|
||||||
|
if err := c.ShouldBind(&p); err != nil {
|
||||||
|
response.R(c).Validator(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ent := &model.User{
|
||||||
|
Base: model.Base{
|
||||||
|
Id: p.Id,
|
||||||
|
},
|
||||||
|
Account: p.Account,
|
||||||
|
Password: p.Password,
|
||||||
|
Avatar: p.Avatar,
|
||||||
|
Nickname: p.Nickname,
|
||||||
|
Contact: p.Contact,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := service.User().CreateUser(ent); err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
// @description: 删除用户
|
||||||
|
// @receiver u
|
||||||
|
// @param c
|
||||||
|
func (u User) Delete(c *gin.Context) {
|
||||||
|
var id = c.Param("id")
|
||||||
|
if id == "" || id == "undefined" {
|
||||||
|
response.R(c).Validator(errors.New("id不能为空"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := service.User().Delete(id); err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePassword
|
||||||
|
// @description: 修改密码
|
||||||
|
// @receiver u
|
||||||
|
// @param c
|
||||||
|
func (u User) ChangePassword(c *gin.Context) {
|
||||||
|
var p param.ChangePassword
|
||||||
|
if err := c.ShouldBind(&p); err != nil {
|
||||||
|
response.R(c).Validator(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := GetCurrentLoginUser(c)
|
||||||
|
if user == nil {
|
||||||
|
response.R(c).FailedWithError("用户信息错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断原密码是否对
|
||||||
|
if !utils.Password().ComparePassword(user.Password, p.OriginalPassword) {
|
||||||
|
response.R(c).FailedWithError("原密码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
if err := service.User().ChangePassword(user.Id, p.NewPassword); err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPassword
|
||||||
|
// @description: 重置密码
|
||||||
|
// @receiver UserApi
|
||||||
|
// @param c
|
||||||
|
func (u User) ResetPassword(c *gin.Context) {
|
||||||
|
var id = c.Param("id")
|
||||||
|
if id == "" || id == "undefined" {
|
||||||
|
response.R(c).FailedWithError("id不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loginUser := GetCurrentLoginUser(c)
|
||||||
|
if loginUser == nil {
|
||||||
|
response.R(c).FailedWithError("用户信息错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先查询一下
|
||||||
|
user, err := service.User().GetUserById(id)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError("获取用户信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Account == loginUser.Account {
|
||||||
|
response.R(c).FailedWithError("当前用户不可重置密码")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
if err := service.User().ChangePassword(user.Id, "admin123"); err != nil {
|
||||||
|
response.R(c).FailedWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OK()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAvatar
|
||||||
|
// @description: 生成头像
|
||||||
|
// @receiver UserApi
|
||||||
|
// @param c
|
||||||
|
func (u User) GenerateAvatar(c *gin.Context) {
|
||||||
|
avatar, err := utils.Avatar().GenerateAvatar(false)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).FailedWithError(fmt.Errorf("生成头像失败: %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.R(c).OkWithData(fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString([]byte(avatar))))
|
||||||
|
}
|
77
http/middleware/authorization.go
Normal file
77
http/middleware/authorization.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"website-nav/component"
|
||||||
|
"website-nav/http/response"
|
||||||
|
"website-nav/service"
|
||||||
|
"website-nav/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
// @description: 授权中间件
|
||||||
|
// @return gin.HandlerFunc
|
||||||
|
func Authorization() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
token := c.GetHeader("Authorization")
|
||||||
|
if token == "" || !strings.HasPrefix(token, "Bearer ") {
|
||||||
|
response.R(c).AuthorizationFailed("未登陆")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hashPassword := c.Request.Header.Get("X-TOKEN")
|
||||||
|
if hashPassword == "" {
|
||||||
|
response.R(c).AuthorizationFailed("未登陆")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userClaims, err := component.JWT().ParseToken(token, hashPassword)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).AuthorizationFailed("未登陆")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果token的颁发者与请求的站点不一致那么就给它抬出去
|
||||||
|
if !slices.Contains(strings.Split(userClaims.Issuer, ","), utils.WebSite().GetHost(c.Request.Header.Get("Referer"))) {
|
||||||
|
response.R(c).AuthorizationFailed("未登陆")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户
|
||||||
|
user, err := service.User().GetUserById(userClaims.ID)
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).AuthorizationFailed("用户不存在")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将用户信息放入上下文
|
||||||
|
c.Set("user", &user)
|
||||||
|
|
||||||
|
if c.Request.RequestURI == "/api/user/logout" {
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成一个新token
|
||||||
|
secret := component.JWT().GenerateSecret(user.Password, uuid.NewString(), time.Now().Local().String())
|
||||||
|
tokenStr, _, err := component.JWT().GenerateToken(user.Id, secret, userClaims.ExpiresAt.Time, time.Now().Local())
|
||||||
|
if err != nil {
|
||||||
|
response.R(c).AuthorizationFailed("校验失败")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Writer.Header().Set("Authorization", fmt.Sprintf("Bearer %s", tokenStr))
|
||||||
|
c.Writer.Header().Set("X-TOKEN", secret)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
10
http/param/login.go
Normal file
10
http/param/login.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package param
|
||||||
|
|
||||||
|
// Login
|
||||||
|
// @description: 登陆
|
||||||
|
type Login struct {
|
||||||
|
Account string `json:"account" form:"account" label:"账号" binding:"required,min=2,max=20"`
|
||||||
|
Password string `json:"password" form:"password" label:"密码" binding:"required,min=8,max=32"`
|
||||||
|
CaptchaId string `json:"captchaId" form:"captchaId" label:"验证码ID" binding:"required"`
|
||||||
|
CaptchaCode string `json:"captchaCode" form:"captchaCode" label:"验证码" binding:"required,max=4" `
|
||||||
|
}
|
6
http/param/request.go
Normal file
6
http/param/request.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package param
|
||||||
|
|
||||||
|
type Page struct {
|
||||||
|
Current int64 `json:"current" form:"current" label:"页码数" binding:"required"`
|
||||||
|
Size int64 `json:"size" form:"size" label:"每页数量" binging:"required"`
|
||||||
|
}
|
26
http/param/user.go
Normal file
26
http/param/user.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package param
|
||||||
|
|
||||||
|
// UserList
|
||||||
|
// @description: 用户列表
|
||||||
|
type UserList struct {
|
||||||
|
Page
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveUser
|
||||||
|
// @description: 新增/编辑用户
|
||||||
|
type SaveUser struct {
|
||||||
|
Id string `json:"id" form:"id" label:"id" binding:"omitempty"` // id
|
||||||
|
Account string `json:"account" form:"account" label:"账户号" binding:"required_without=Id"` // 账户号
|
||||||
|
Password string `json:"password" form:"password" label:"密码" binding:"omitempty"` // 密码
|
||||||
|
Nickname string `json:"nickname" form:"nickname" label:"昵称" binding:"required,min=2"` // 昵称
|
||||||
|
Avatar string `json:"avatar" form:"avatar" label:"头像" binding:"omitempty"` // 头像
|
||||||
|
Contact string `json:"contact" form:"contact" label:"联系方式" binding:"omitempty"` // 联系方式
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePassword
|
||||||
|
// @description: 修改密码
|
||||||
|
type ChangePassword struct {
|
||||||
|
OriginalPassword string `json:"originalPassword" form:"originalPassword" label:"原密码" binding:"required,min=8,max=32"` // 原密码
|
||||||
|
NewPassword string `json:"newPassword" form:"newPassword" label:"新密码" binding:"required,min=8,max=32"` // 新密码
|
||||||
|
ConfirmPassword string `json:"confirmPassword" form:"confirmPassword" label:"确认密码" binding:"eqfield=NewPassword"` // 确认密码
|
||||||
|
}
|
131
http/response/response.go
Normal file
131
http/response/response.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"website-nav/component"
|
||||||
|
"website-nav/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PageData[T any] struct {
|
||||||
|
Current int `json:"current"` // 当前页码
|
||||||
|
Size int `json:"size"` // 每页数量
|
||||||
|
Total int64 `json:"total"` // 总数
|
||||||
|
TotalPage int `json:"totalPage"` // 总页数
|
||||||
|
Records T `json:"records"` // 返回数据
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpResponse struct{ *gin.Context }
|
||||||
|
|
||||||
|
func R(c *gin.Context) HttpResponse {
|
||||||
|
return HttpResponse{c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK
|
||||||
|
// @description: success
|
||||||
|
// @receiver h
|
||||||
|
func (h HttpResponse) OK() {
|
||||||
|
h.Context.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": http.StatusOK,
|
||||||
|
"message": "success",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// OkWithData
|
||||||
|
// @description: success with data
|
||||||
|
// @receiver h
|
||||||
|
// @param data
|
||||||
|
func (h HttpResponse) OkWithData(data any) {
|
||||||
|
h.Context.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": http.StatusOK,
|
||||||
|
"message": "success",
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paginate
|
||||||
|
// @description: success page data
|
||||||
|
// @receiver h
|
||||||
|
// @param data
|
||||||
|
// @param total
|
||||||
|
// @param current
|
||||||
|
// @param size
|
||||||
|
func (h HttpResponse) Paginate(data any, total int64, current, size int64) {
|
||||||
|
// 处理一下页码、页数量
|
||||||
|
if current == -1 {
|
||||||
|
current = 1
|
||||||
|
size = total
|
||||||
|
}
|
||||||
|
// 计算总页码
|
||||||
|
totalPage := utils.Paginate().Generate(total, int(size))
|
||||||
|
// 返回结果
|
||||||
|
h.Context.JSON(http.StatusOK, map[string]any{
|
||||||
|
"code": http.StatusOK,
|
||||||
|
"data": &PageData[any]{Current: int(current), Size: int(size), Total: total, TotalPage: totalPage, Records: data},
|
||||||
|
"message": "success",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizationFailed
|
||||||
|
// @description: authorization failed
|
||||||
|
// @receiver h
|
||||||
|
// @param msg
|
||||||
|
func (h HttpResponse) AuthorizationFailed(msg string) {
|
||||||
|
if msg == "" {
|
||||||
|
msg = "authorized failed"
|
||||||
|
}
|
||||||
|
h.Context.JSON(http.StatusUnauthorized, gin.H{
|
||||||
|
"code": http.StatusUnauthorized,
|
||||||
|
"message": msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failed
|
||||||
|
// @description: request bad
|
||||||
|
// @receiver h
|
||||||
|
func (h HttpResponse) Failed() {
|
||||||
|
h.Context.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": http.StatusBadRequest,
|
||||||
|
"message": "failed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailedWithError
|
||||||
|
// @description: custom error
|
||||||
|
// @receiver h
|
||||||
|
// @param err
|
||||||
|
func (h HttpResponse) FailedWithError(err any) {
|
||||||
|
var errStr string
|
||||||
|
switch err.(type) {
|
||||||
|
case error:
|
||||||
|
errStr = err.(error).Error()
|
||||||
|
case string:
|
||||||
|
errStr = err.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Context.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": http.StatusBadRequest,
|
||||||
|
"message": errStr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validator
|
||||||
|
// @description: request param error
|
||||||
|
// @receiver h
|
||||||
|
// @param err
|
||||||
|
func (h HttpResponse) Validator(err error) {
|
||||||
|
h.Context.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": http.StatusBadRequest,
|
||||||
|
"message": component.Validate().Error(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
// @description: server error
|
||||||
|
// @receiver h
|
||||||
|
func (h HttpResponse) Internal() {
|
||||||
|
h.Context.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"code": http.StatusInternalServerError,
|
||||||
|
"message": "server error",
|
||||||
|
})
|
||||||
|
}
|
51
http/router/admin.go
Normal file
51
http/router/admin.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"website-nav/http/api/admin"
|
||||||
|
"website-nav/http/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func adminAPI(r *gin.RouterGroup) {
|
||||||
|
adminApi := r.Group("admin")
|
||||||
|
{
|
||||||
|
loginAPI(adminApi)
|
||||||
|
userAPI(adminApi)
|
||||||
|
navTypeAPI(adminApi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loginAPI
|
||||||
|
// @description: 登陆相关API
|
||||||
|
// @param r
|
||||||
|
func loginAPI(r *gin.RouterGroup) {
|
||||||
|
login := r.Group("/login")
|
||||||
|
{
|
||||||
|
login.GET("/captcha", admin.LoginAPI().Captcha) // 获取验证码
|
||||||
|
login.POST("/token", admin.LoginAPI().Login) // 登陆
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// userAPI
|
||||||
|
// @description: 用户相关API
|
||||||
|
// @param r
|
||||||
|
func userAPI(r *gin.RouterGroup) {
|
||||||
|
user := r.Group("/user")
|
||||||
|
user.Use(middleware.Authorization())
|
||||||
|
{
|
||||||
|
user.GET("/info", admin.UserAPI().GetLoginUser) // 获取当前登陆用户信息
|
||||||
|
user.GET("/list", admin.UserAPI().List) // 用户列表
|
||||||
|
user.POST("/save", admin.UserAPI().Save) // 新增/编辑用户
|
||||||
|
user.DELETE("/:id", admin.UserAPI().Delete) // 删除用户
|
||||||
|
user.PUT("/change-password", admin.UserAPI().ChangePassword) // 修改密码【当前用户更改自己的】
|
||||||
|
user.PUT("/reset-password/:id", admin.UserAPI().ResetPassword) // 重置密码 【重置其他管理员的】
|
||||||
|
user.GET("/generate-avatar", admin.UserAPI().GenerateAvatar) // 生成头像
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// navTypeAPI
|
||||||
|
// @description: 导航分类API
|
||||||
|
// @param r
|
||||||
|
func navTypeAPI(r *gin.RouterGroup) {
|
||||||
|
|
||||||
|
}
|
33
http/router/root.go
Normal file
33
http/router/root.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option func(engine *gin.RouterGroup)
|
||||||
|
|
||||||
|
var options []Option
|
||||||
|
|
||||||
|
func includeRouters(opts ...Option) {
|
||||||
|
options = append(options, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitRouter() *gin.Engine {
|
||||||
|
r := gin.New()
|
||||||
|
// 开启IP 追踪
|
||||||
|
r.ForwardedByClientIP = true
|
||||||
|
// 将请求打印至控制台
|
||||||
|
r.Use(gin.Logger())
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(r.Group("api"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rooters() {
|
||||||
|
includeRouters(
|
||||||
|
adminAPI,
|
||||||
|
)
|
||||||
|
}
|
18
http/vo/navigation.go
Normal file
18
http/vo/navigation.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
// TypeItem
|
||||||
|
// @description: 导航分类
|
||||||
|
type TypeItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NavigationItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
25
http/vo/user.go
Normal file
25
http/vo/user.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
import "website-nav/model"
|
||||||
|
|
||||||
|
// UserItem
|
||||||
|
// @description: 用户列表的数据
|
||||||
|
type UserItem struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Account string `json:"account"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Contact string `json:"contact"`
|
||||||
|
CreatedAt model.JsonTime `json:"createdAt"`
|
||||||
|
UpdatedAt model.JsonTime `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// User
|
||||||
|
// @description: 用户信息
|
||||||
|
type User struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Account string `json:"account"`
|
||||||
|
Password string `json:"-"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Contact string `json:"contact"`
|
||||||
|
}
|
126
initialize/init.go
Normal file
126
initialize/init.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package initialize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitee.ltd/lxh/logger"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/coocood/freecache"
|
||||||
|
"github.com/cowardmrx/go_aliyun_oss"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
gl "gorm.io/gorm/logger"
|
||||||
|
"time"
|
||||||
|
"website-nav/config"
|
||||||
|
"website-nav/global/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
initLogger()
|
||||||
|
initHttpClient()
|
||||||
|
initConfig()
|
||||||
|
initDatabase()
|
||||||
|
initCache()
|
||||||
|
initOSS()
|
||||||
|
}
|
||||||
|
|
||||||
|
// initLogger
|
||||||
|
// @description: 初始化日志
|
||||||
|
func initLogger() {
|
||||||
|
logger.InitLogger(logger.LogConfig{
|
||||||
|
Mode: logger.Dev,
|
||||||
|
FileEnable: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// initHttpClient
|
||||||
|
// @description: 初始化http客户端
|
||||||
|
func initHttpClient() {
|
||||||
|
client.HttpClient = resty.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
// initConfig
|
||||||
|
// @description: 加载配置文件
|
||||||
|
func initConfig() {
|
||||||
|
vp := viper.New()
|
||||||
|
vp.SetConfigFile("app.yaml")
|
||||||
|
if err := vp.ReadInConfig(); err != nil {
|
||||||
|
log.Panicf("读取配置文件失败: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := vp.Unmarshal(&config.GlobalConfig); err != nil {
|
||||||
|
log.Panicf("解析配置文件失败: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
vp.OnConfigChange(func(in fsnotify.Event) {
|
||||||
|
if err := vp.Unmarshal(&config.GlobalConfig); err != nil {
|
||||||
|
log.Errorf("配置文件变动,读取失败: %v", err.Error())
|
||||||
|
} else {
|
||||||
|
initDatabase()
|
||||||
|
initCache()
|
||||||
|
initOSS()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vp.WatchConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// initDatabase
|
||||||
|
// @description: 初始化持久化数据库链接
|
||||||
|
func initDatabase() {
|
||||||
|
// 不同驱动提供
|
||||||
|
var dbDialector gorm.Dialector
|
||||||
|
switch config.GlobalConfig.Database.Driver {
|
||||||
|
case "mysql":
|
||||||
|
dbDialector = mysql.Open(config.GlobalConfig.Database.GetDSN())
|
||||||
|
case "pgsql":
|
||||||
|
dbDialector = postgres.Open(config.GlobalConfig.Database.GetDSN())
|
||||||
|
case "sqlite":
|
||||||
|
dbDialector = sqlite.Open(config.GlobalConfig.Database.GetDSN())
|
||||||
|
}
|
||||||
|
|
||||||
|
logLevel := gl.Info
|
||||||
|
|
||||||
|
db, err := gorm.Open(dbDialector, &gorm.Config{
|
||||||
|
Logger: logger.NewGormLoggerWithConfig(gl.Config{
|
||||||
|
SlowThreshold: time.Second, // Slow SQL threshold
|
||||||
|
IgnoreRecordNotFoundError: false, // 忽略没找到结果的错误
|
||||||
|
LogLevel: logLevel, // Log level
|
||||||
|
Colorful: false, // Disable color
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("链接数据库[%s]失败:%v", config.GlobalConfig.Database.Driver, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
client.DB = db
|
||||||
|
}
|
||||||
|
|
||||||
|
// initCache
|
||||||
|
// @description: 初始化缓存
|
||||||
|
func initCache() {
|
||||||
|
cache := freecache.NewCache(1024 * 1024 * 10)
|
||||||
|
client.Cache = cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// initOSS
|
||||||
|
// @description: 初始化oss客户端
|
||||||
|
func initOSS() {
|
||||||
|
if config.GlobalConfig.File.Type != "oss" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ossConfig := &go_aliyun_oss.AliOssConfig{
|
||||||
|
EndPoint: config.GlobalConfig.File.Endpoint,
|
||||||
|
AccessKeyId: config.GlobalConfig.File.AccessId,
|
||||||
|
AccessKeySecret: config.GlobalConfig.File.AccessSecret,
|
||||||
|
BucketName: config.GlobalConfig.File.BucketName,
|
||||||
|
OriginalFileName: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
ossClient := ossConfig.CreateOssConnect()
|
||||||
|
client.OSS = ossClient
|
||||||
|
}
|
37
main.go
Normal file
37
main.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
"website-nav/config"
|
||||||
|
"website-nav/http/router"
|
||||||
|
"website-nav/initialize"
|
||||||
|
"website-nav/script"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initialize.Init()
|
||||||
|
if err := script.New().Do(); err != nil {
|
||||||
|
log.Errorf("执行脚本失败: %v", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 随机数种子
|
||||||
|
rand.New(rand.NewSource(time.Now().Local().UnixNano()))
|
||||||
|
|
||||||
|
// 启动服务
|
||||||
|
router.Rooters()
|
||||||
|
handler := router.InitRouter()
|
||||||
|
httpServe := http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", config.GlobalConfig.Http.Port),
|
||||||
|
Handler: handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := httpServe.ListenAndServe(); err != nil {
|
||||||
|
log.Panicf("启动http服务端失败: %v", err.Error())
|
||||||
|
}
|
||||||
|
}
|
67
model/base.go
Normal file
67
model/base.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Base
|
||||||
|
// @description: 数据模型基类
|
||||||
|
type Base struct {
|
||||||
|
Id string `json:"id" gorm:"primaryKey;type:varchar(36);not null;comment:'主键'"`
|
||||||
|
Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Base) BeforeCreate(*gorm.DB) (err error) {
|
||||||
|
if b.Id == "" {
|
||||||
|
b.Id = strings.ReplaceAll(uuid.NewString(), "-", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonTime struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Timestamp struct {
|
||||||
|
CreatedAt JsonTime
|
||||||
|
UpdatedAt JsonTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jt JsonTime) MarshalJSON() ([]byte, error) {
|
||||||
|
if jt.IsZero() {
|
||||||
|
return []byte(`""`), nil
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("\"%s\"", jt.Format("2006-01-02 15:04:05"))
|
||||||
|
return []byte(output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jt JsonTime) Value() (driver.Value, error) {
|
||||||
|
var zeroTime time.Time
|
||||||
|
if jt.Time.UnixNano() == zeroTime.UnixNano() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return jt.Time.Format("2006-01-02 15:04:05"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jt *JsonTime) Scan(v any) error {
|
||||||
|
value, ok := v.(time.Time)
|
||||||
|
if ok {
|
||||||
|
*jt = JsonTime{Time: value}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("can not convert %v to timestamp", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jt JsonTime) String() string {
|
||||||
|
if jt.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("%s", jt.Format("2006-01-02 15:04:05"))
|
||||||
|
return output
|
||||||
|
}
|
29
model/navigation.go
Normal file
29
model/navigation.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// NavigationType
|
||||||
|
// @description: 分类
|
||||||
|
type NavigationType struct {
|
||||||
|
Base
|
||||||
|
Name string `gorm:"column:name;type:varchar(255);not null;comment:'名称'"`
|
||||||
|
Icon string `gorm:"column:icon;type:varchar(255);not null;comment:'图标'"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (NavigationType) TableName() string {
|
||||||
|
return "t_navigation_type"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
// @description: 导航
|
||||||
|
type Navigation struct {
|
||||||
|
Base
|
||||||
|
TypeID string `gorm:"column:type_id;type:varchar(255);not null;comment:'分类ID'"`
|
||||||
|
Title string `gorm:"column:title;type:varchar(255);not null;comment:'标题'"`
|
||||||
|
Url string `gorm:"column:url;type:varchar(255);not null;comment:'链接'"`
|
||||||
|
Description string `gorm:"column:description;type:varchar(255);not null;comment:'描述'"`
|
||||||
|
Icon string `gorm:"column:icon;type:varchar(255);not null;comment:'图标'"`
|
||||||
|
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;comment:'是否启用'"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Navigation) TableName() string {
|
||||||
|
return "t_navigation"
|
||||||
|
}
|
16
model/user.go
Normal file
16
model/user.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// User
|
||||||
|
// @description: 后台用户表
|
||||||
|
type User struct {
|
||||||
|
Base
|
||||||
|
Account string `json:"account" gorm:"type:varchar(50);not null;index:idx_account;comment: '登陆账号'"`
|
||||||
|
Password string `json:"password" gorm:"type:varchar(255);not null;comment: '密码'"`
|
||||||
|
Avatar string `json:"avatar" gorm:"type:varchar(255);default null;comment: '头像'"`
|
||||||
|
Nickname string `json:"nickname" gorm:"type:varchar(50);not null;comment: '昵称'"`
|
||||||
|
Contact string `json:"contact" gorm:"type:varchar(255);default null;comment: '联系方式(邮箱|电话)'"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (User) TableName() string {
|
||||||
|
return "t_user"
|
||||||
|
}
|
69
script/script.go
Normal file
69
script/script.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package script
|
||||||
|
|
||||||
|
import (
|
||||||
|
"website-nav/global/client"
|
||||||
|
"website-nav/model"
|
||||||
|
"website-nav/service"
|
||||||
|
"website-nav/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type script struct{}
|
||||||
|
|
||||||
|
func New() script {
|
||||||
|
return script{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s script) Do() error {
|
||||||
|
if err := s.migrate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.createSuperAdmin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate
|
||||||
|
// @description: 生成数据库
|
||||||
|
// @receiver s
|
||||||
|
// @return error
|
||||||
|
func (s script) migrate() error {
|
||||||
|
var ent = []any{
|
||||||
|
new(model.User),
|
||||||
|
new(model.NavigationType),
|
||||||
|
new(model.Navigation),
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.DB.AutoMigrate(ent...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createSuperAdmin
|
||||||
|
// @description: 创建超级管理员
|
||||||
|
// @receiver s
|
||||||
|
// @return error
|
||||||
|
func (s script) createSuperAdmin() error {
|
||||||
|
var count int64
|
||||||
|
if err := client.DB.Model(&model.User{}).
|
||||||
|
Where("account = ?", "admin").Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有超管就创建一个
|
||||||
|
if count > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
avatar, err := utils.Avatar().GenerateAvatar(true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.User().CreateUser(&model.User{
|
||||||
|
Avatar: avatar,
|
||||||
|
Nickname: "超级管理员",
|
||||||
|
Account: "admin",
|
||||||
|
Contact: "",
|
||||||
|
Password: "admin123",
|
||||||
|
})
|
||||||
|
}
|
25
service/base.go
Normal file
25
service/base.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
func Paginate(current, size int64) func(db *gorm.DB) *gorm.DB {
|
||||||
|
// 如果页码是-1,就不分页
|
||||||
|
if current == -1 {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 分页
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
if current == 0 {
|
||||||
|
current = 1
|
||||||
|
}
|
||||||
|
if size < 1 {
|
||||||
|
size = 10
|
||||||
|
}
|
||||||
|
// 计算偏移量
|
||||||
|
offset := (current - 1) * size
|
||||||
|
// 返回组装结果
|
||||||
|
return db.Offset(int(offset)).Limit(int(size))
|
||||||
|
}
|
||||||
|
}
|
112
service/user.go
Normal file
112
service/user.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"website-nav/global/client"
|
||||||
|
"website-nav/http/param"
|
||||||
|
"website-nav/http/vo"
|
||||||
|
"website-nav/model"
|
||||||
|
"website-nav/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type user struct {
|
||||||
|
*gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func User() user {
|
||||||
|
return user{client.DB}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List
|
||||||
|
// @description: 用户列表
|
||||||
|
// @receiver s
|
||||||
|
// @param p
|
||||||
|
// @return data
|
||||||
|
// @return err
|
||||||
|
func (s user) List(p param.UserList) (data []vo.UserItem, count int64, err error) {
|
||||||
|
err = s.Model(&model.User{}).Scopes(Paginate(p.Current, p.Size)).Find(&data).Limit(-1).Offset(-1).Count(&count).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser
|
||||||
|
// @description: 创建/更新用户
|
||||||
|
// @receiver s
|
||||||
|
// @param user
|
||||||
|
// @return err
|
||||||
|
func (s user) CreateUser(user *model.User) (err error) {
|
||||||
|
// 更新
|
||||||
|
if user.Id != "" {
|
||||||
|
updates := map[string]any{
|
||||||
|
"nickname": user.Nickname,
|
||||||
|
"contact": user.Contact,
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Model(&model.User{}).Where("id = ?", user.Id).Updates(&updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建前判断账户是否已经存在
|
||||||
|
if _, err = s.GetUserByAccount(user.Account); err == nil {
|
||||||
|
return errors.New("账号已存在,请勿重复创建")
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPassword := utils.Password().GenerateHashPassword("admin123")
|
||||||
|
if user.Password == "" { // 没有密码给一个默认密码
|
||||||
|
user.Password = defaultPassword
|
||||||
|
} else {
|
||||||
|
user.Password = utils.Password().GenerateHashPassword(user.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成一个默认头像
|
||||||
|
if user.Avatar == "" {
|
||||||
|
avatar, err := utils.Avatar().GenerateAvatar(true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.Avatar = avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建
|
||||||
|
return s.Create(&user).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
// @description: 删除指定用户
|
||||||
|
// @receiver s
|
||||||
|
// @param id
|
||||||
|
// @return error
|
||||||
|
func (s user) Delete(id string) error {
|
||||||
|
return s.Model(&model.User{}).Where("id = ?", id).Delete(&model.User{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByAccount
|
||||||
|
// @description: 通过账号获取用户信息
|
||||||
|
// @receiver user
|
||||||
|
// @param account
|
||||||
|
// @return data
|
||||||
|
// @return err
|
||||||
|
func (s user) GetUserByAccount(account string) (data vo.User, err error) {
|
||||||
|
err = s.Model(&model.User{}).First(&data, "account = ?", account).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserById
|
||||||
|
// @description: 根据id获取用户信息
|
||||||
|
// @receiver s
|
||||||
|
// @param id
|
||||||
|
// @return data
|
||||||
|
// @return err
|
||||||
|
func (s user) GetUserById(id string) (data vo.User, err error) {
|
||||||
|
err = s.Model(&model.User{}).Where("id = ?", id).Take(&data).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePassword
|
||||||
|
// @description: 修改密码
|
||||||
|
// @receiver s
|
||||||
|
// @param id
|
||||||
|
// @param password
|
||||||
|
// @return error
|
||||||
|
func (s user) ChangePassword(id, password string) error {
|
||||||
|
return s.Model(&model.User{}).Where("id = ?", id).Update("password", utils.Password().GenerateHashPassword(password)).Error
|
||||||
|
}
|
42
utils/avatar.go
Normal file
42
utils/avatar.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
"website-nav/global/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type avatar struct{}
|
||||||
|
|
||||||
|
func Avatar() avatar {
|
||||||
|
return avatar{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAvatar
|
||||||
|
// @description: 生成随机头像 - 默认头像
|
||||||
|
// @receiver avatar
|
||||||
|
// @return path
|
||||||
|
// @return err
|
||||||
|
func (avatar) GenerateAvatar(isUpload bool) (path string, err error) {
|
||||||
|
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
r := client.HttpClient.R()
|
||||||
|
result, err := r.Get(fmt.Sprintf("https://api.dicebear.com/7.x/croodles/png?seed=%d&scale=120&size=200&clip=true&randomizeIds=true&beard=variant01,variant02,variant03&"+
|
||||||
|
"eyes=variant01,variant02,variant03,variant04,variant05,variant06,variant07,variant08,variant09,variant10,variant11,variant12&mustache=variant01,variant02,variant03&"+
|
||||||
|
"topColor=000000,0fa958,699bf7", rand.Uint32()))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("请求头像API失败: %v", err.Error())
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isUpload {
|
||||||
|
path, err = FileSystem().UploadFile(result.Body(), ".png")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(result.Body()), nil
|
||||||
|
}
|
50
utils/file_system.go
Normal file
50
utils/file_system.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"website-nav/config"
|
||||||
|
"website-nav/global/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileSystem struct{}
|
||||||
|
|
||||||
|
func FileSystem() fileSystem {
|
||||||
|
return fileSystem{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadFile
|
||||||
|
// @description: 上传文件
|
||||||
|
// @receiver fileSystem
|
||||||
|
// @param file
|
||||||
|
// @return filePath
|
||||||
|
// @return err
|
||||||
|
func (fileSystem) UploadFile(file []byte, suffix string) (filePath string, err error) {
|
||||||
|
switch config.GlobalConfig.File.Type {
|
||||||
|
case "oss":
|
||||||
|
ossObj, err := client.OSS.Put(config.GlobalConfig.File.Path, file, suffix)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = ossObj.LongPath
|
||||||
|
case "local":
|
||||||
|
basePath := fmt.Sprintf("%s/%d/avatar", config.GlobalConfig.File.Path, time.Now().Unix())
|
||||||
|
filePath = fmt.Sprintf("%s/%s%s", basePath, strings.ReplaceAll(uuid.NewString(), "-", ""), suffix)
|
||||||
|
// 创建目录
|
||||||
|
if err = os.MkdirAll(basePath, os.FileMode(0777)); err != nil {
|
||||||
|
log.Errorf("本地存储目录创建失败: %v", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err = os.WriteFile(filePath, file, os.FileMode(0777)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
subPath := strings.ReplaceAll(filePath, fmt.Sprintf("%s/", config.GlobalConfig.File.Path), "")
|
||||||
|
filePath = fmt.Sprintf("http://%s/assets/%s", config.GlobalConfig.Http.Endpoint, subPath)
|
||||||
|
}
|
||||||
|
return filePath, err
|
||||||
|
}
|
44
utils/hash.go
Normal file
44
utils/hash.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hash struct{}
|
||||||
|
|
||||||
|
func Hash() hash {
|
||||||
|
return hash{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MD5
|
||||||
|
// @description: MD5摘要
|
||||||
|
// @param str
|
||||||
|
// @return string
|
||||||
|
func (hash) MD5(str string) string {
|
||||||
|
hs := md5.New()
|
||||||
|
hs.Write([]byte(str))
|
||||||
|
return hex.EncodeToString(hs.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SHA256
|
||||||
|
// @description: SHA256
|
||||||
|
// @param str
|
||||||
|
// @return string
|
||||||
|
func (hash) SHA256(str string) string {
|
||||||
|
hasher := sha256.New()
|
||||||
|
hasher.Write([]byte(str))
|
||||||
|
return hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SHA512
|
||||||
|
// @description: SHA512
|
||||||
|
// @param str
|
||||||
|
// @return string
|
||||||
|
func (hash) SHA512(str string) string {
|
||||||
|
hasher := sha512.New()
|
||||||
|
hasher.Write([]byte(str))
|
||||||
|
return hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
}
|
25
utils/paginate.go
Normal file
25
utils/paginate.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
type paginate struct{}
|
||||||
|
|
||||||
|
func Paginate() paginate {
|
||||||
|
return paginate{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate
|
||||||
|
// @description: 生成页码
|
||||||
|
// @receiver paginate
|
||||||
|
// @param count
|
||||||
|
// @param size
|
||||||
|
// @return int
|
||||||
|
func (paginate) Generate(count int64, size int) int {
|
||||||
|
totalPage := 0
|
||||||
|
if count > 0 {
|
||||||
|
upPage := 0
|
||||||
|
if int(count)%size > 0 {
|
||||||
|
upPage = 1
|
||||||
|
}
|
||||||
|
totalPage = (int(count) / size) + upPage
|
||||||
|
}
|
||||||
|
return totalPage
|
||||||
|
}
|
38
utils/password.go
Normal file
38
utils/password.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type password struct{}
|
||||||
|
|
||||||
|
func Password() password {
|
||||||
|
return password{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateHashPassword
|
||||||
|
// @description: 生成hash密码
|
||||||
|
// @receiver password
|
||||||
|
// @param pass
|
||||||
|
// @return string
|
||||||
|
func (password) GenerateHashPassword(pass string) string {
|
||||||
|
bytePass := []byte(pass)
|
||||||
|
hPass, _ := bcrypt.GenerateFromPassword(bytePass, bcrypt.DefaultCost)
|
||||||
|
return string(hPass)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComparePassword
|
||||||
|
// @description: 密码比对
|
||||||
|
// @receiver password
|
||||||
|
// @param dbPass
|
||||||
|
// @param pass
|
||||||
|
// @return bool
|
||||||
|
func (password) ComparePassword(dbPass, pass string) bool {
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(dbPass), []byte(pass)); err != nil {
|
||||||
|
log.Errorf("密码错误: %v", err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
23
utils/website.go
Normal file
23
utils/website.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "net/url"
|
||||||
|
|
||||||
|
type website struct{}
|
||||||
|
|
||||||
|
func WebSite() website {
|
||||||
|
return website{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHost
|
||||||
|
// @description: 获取主机host
|
||||||
|
// @receiver website
|
||||||
|
// @param addr
|
||||||
|
// @return string
|
||||||
|
func (website) GetHost(addr string) string {
|
||||||
|
uu, err := url.Parse(addr)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return uu.Host
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user