🎉初步完成用户

This commit is contained in:
coward 2024-10-18 17:19:19 +08:00
commit f375118c88
45 changed files with 3302 additions and 0 deletions

28
.gitignore vendored Normal file
View 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
View 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
View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
package constant
const (
Captcha = "captcha"
UserToken = "token"
)

108
go.mod Normal file
View 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
)

1348
go.sum Normal file

File diff suppressed because it is too large Load Diff

20
http/api/admin/base.go Normal file
View 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
View 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
View 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))))
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}