init project

This commit is contained in:
loser 2023-04-24 17:19:41 +08:00
parent 84729ebb66
commit cdb5a4f9cd
51 changed files with 3328 additions and 1 deletions

View File

@ -1,3 +1,3 @@
# Lee-WineList
# Lee-WineList/
酒单小程序-后端

109
api/app/login.go Normal file
View File

@ -0,0 +1,109 @@
package app
import (
"Lee-WineList/client"
"Lee-WineList/common/constant"
"Lee-WineList/core"
"Lee-WineList/model/param"
"Lee-WineList/oauth2"
"Lee-WineList/repository"
"context"
"encoding/json"
"fmt"
"git.echol.cn/loser/logger/log"
"github.com/gin-gonic/gin"
"net/http"
"net/url"
)
type loginApi struct{}
// LoginApi 暴露接口
func LoginApi() *loginApi {
return &loginApi{}
}
// Login 登录
func (l loginApi) Login(ctx *gin.Context) {
var p param.Login
if err := ctx.ShouldBind(&p); err != nil {
core.R(ctx).FailWithMessage(err.Error())
return
}
// 获取用户基础账号信息
userId, err := oauth2.OAuthServer.UserAuthorizationHandler(ctx.Writer, ctx.Request)
if err != nil {
log.Errorf("获取用户基础账号信息失败: %v", err)
core.R(ctx).FailWithMessage(err.Error())
return
}
// 重新组装登录参数
ctx.Request.Form = url.Values{
"username": {userId},
"password": {p.Password},
"scope": {"ALL"},
"grant_type": {"password"},
}
// 参数解析成功,进行登录
if err = oauth2.OAuthServer.HandleTokenRequest(ctx.Writer, ctx.Request); err != nil {
log.Errorf("登录失败:%s", err.Error())
core.R(ctx).FailWithMessage("系统错误,登录失败")
return
}
// 登录成功才更新登录时间
if ctx.Writer.Status() == http.StatusOK {
// 登录成功更新登录时间和IP
go repository.Login().UpdateLastLoginInfo(userId, ctx.ClientIP(), p.UserIdentity)
}
}
// Refresh 刷新登录Token
func (loginApi) Refresh(ctx *gin.Context) {
var p param.RefreshToken
if err := ctx.ShouldBind(&p); err != nil {
core.R(ctx).FailWithMessage("参数错误: " + err.Error())
return
}
// 刷新Token
if err := oauth2.OAuthServer.HandleTokenRequest(ctx.Writer, ctx.Request); err != nil {
log.Errorf("Token数据返回失败: %v", err.Error())
core.R(ctx).FailWithMessage("系统错误")
}
}
// Logout 退出登录
func (loginApi) Logout(ctx *gin.Context) {
log.Debug("退出登录啦")
r := core.R(ctx)
// Token字符串前缀
const bearerSchema string = "Bearer "
// 取出Token
tokenHeader := ctx.Request.Header.Get("Authorization")
tokenStr := tokenHeader[len(bearerSchema):]
// 取出原始RedisKey
baseDataId, err := client.Redis.Get(context.Background(), constant.OAuth2RedisKey+tokenStr).Result()
if err != nil {
r.FailWithMessage("Token信息获取失败")
return
}
baseDataStr, err := client.Redis.Get(context.Background(), constant.OAuth2RedisKey+baseDataId).Result()
if err != nil {
r.FailWithMessage("Token信息获取失败")
return
}
// 转换数据为Map
tokenData := make(map[string]any)
if err = json.Unmarshal([]byte(baseDataStr), &tokenData); err != nil {
r.FailWithMessage("系统错误: " + err.Error())
return
}
// 删除Redis缓存的数据
client.Redis.Del(context.Background(), constant.OAuth2RedisKey+baseDataId)
client.Redis.Del(context.Background(), fmt.Sprintf("%v%v", constant.OAuth2RedisKey, tokenData["Access"]))
client.Redis.Del(context.Background(), fmt.Sprintf("%v%v", constant.OAuth2RedisKey, tokenData["Refresh"]))
r.Ok()
}

104
api/app/user.go Normal file
View File

@ -0,0 +1,104 @@
package app
import (
"Lee-WineList/api"
"Lee-WineList/common/constant"
"Lee-WineList/core"
"Lee-WineList/model/entity"
"Lee-WineList/model/param"
"Lee-WineList/model/vo"
"Lee-WineList/repository"
"Lee-WineList/utils"
"git.echol.cn/loser/logger/log"
"github.com/gin-gonic/gin"
"strings"
)
type userApi struct {
}
// UserApi 暴露接口
func UserApi() *userApi {
return &userApi{}
}
// GetUser 获取当前登录用户信息
func (u userApi) GetUser(ctx *gin.Context) {
// 取出当前登录用户
var ue entity.User
if api.GetUser(ctx, &ue, false, true); ctx.IsAborted() {
return
}
// 转换为VO
var v vo.UserVO
v.ParseOrdinary(ue)
core.R(ctx).OkWithData(v)
}
// BindingWeChat 绑定微信
func (u userApi) BindingWeChat(ctx *gin.Context) {
var p param.BindingWeChat
if err := ctx.ShouldBind(&p); err != nil {
core.R(ctx).FailWithMessage("参数错误" + err.Error())
return
}
// 取出当前登录用户
var loginUser entity.User
if api.GetUser(ctx, &loginUser, true, true); ctx.IsAborted() {
return
}
// 解析出UnionId和OpenId
unionId, openId, _, err := utils.WeChatUtils().GetWechatUnionId(p.Code)
if err != nil {
log.Errorf("获取微信UnionId失败%s", err.Error())
core.R(ctx).FailWithMessage("系统错误,请稍后再试")
return
}
log.Debugf("用户[%v]的UnionId为[%v]OpenId为[%v]", loginUser.Id, unionId, openId)
if repository.User().CheckUnionIdIsExist(unionId, openId) {
core.R(ctx).FailWithMessage("该微信已绑定其他账号")
return
}
// 解析成功,修改用户信息
loginUser.WechatUnionId = &unionId
loginUser.WechatOpenId = &openId
if err = repository.User().UpdateUserInfo(&loginUser); err != nil {
log.Errorf("修改用户信息失败:%s", err.Error())
core.R(ctx).FailWithMessage("系统错误,请稍后再试")
return
}
core.R(ctx).Ok()
}
// UpdateUser 修改用户信息
func (u userApi) UpdateUser(ctx *gin.Context) {
var p param.ChangeUserInfo
if err := ctx.ShouldBind(&p); err != nil {
core.R(ctx).FailWithMessage("参数错误: " + err.Error())
return
}
// 获取当前登录用户
var loginUser entity.User
if api.GetUser(ctx, &loginUser, false, true); ctx.IsAborted() {
return
}
// 修改资料
if p.Sex != constant.UserSexNone {
loginUser.Sex = p.Sex
}
if p.Nickname != "" {
loginUser.Nickname = p.Nickname
}
if p.Avatar != "" && strings.HasPrefix(p.Avatar, "http") {
loginUser.Avatar = p.Avatar
}
// 修改数据
if err := repository.User().UpdateUserInfo(&loginUser); err != nil {
log.Errorf("修改用户信息失败:%s", err.Error())
core.R(ctx).FailWithMessage("修改用户信息失败: " + err.Error())
return
}
core.R(ctx).Ok()
}

69
api/base.go Normal file
View File

@ -0,0 +1,69 @@
package api
import (
"Lee-WineList/client"
"Lee-WineList/common/constant"
"Lee-WineList/core"
"Lee-WineList/model/entity"
"git.echol.cn/loser/logger/log"
"github.com/gin-gonic/gin"
"net/http"
)
// GetAdminUser 获取登录的管理员信息
func GetAdminUser(ctx *gin.Context, u *entity.AdminUser) {
userId := ctx.Request.Header.Get("userId")
if userId == "" {
ctx.Abort()
core.R(ctx).FailWithMessageAndCode("未授权操作", http.StatusUnauthorized)
return
}
u.Id = userId
// 查询用户信息
err := client.MySQL.Where("id = ?", u.Id).First(&u).Error
if err != nil {
log.Errorf("获取用户信息失败:%s", err.Error())
core.R(ctx).FailWithMessageAndCode("用户状态异常", http.StatusBadRequest)
ctx.Abort()
return
}
// 校验
if u.Status != constant.UserStatusActive {
core.R(ctx).FailWithMessageAndCode("用户已被禁用", http.StatusBadRequest)
ctx.Abort()
return
}
}
// GetUser 获取登录的普通用户信息dontResponse表示只获取用户信息不论对错, dontCheck表示不检查微信绑定
func GetUser(ctx *gin.Context, u *entity.User, dontResponse, dontCheck bool) {
userId := ctx.Request.Header.Get("userId")
if userId == "" {
if !dontResponse {
ctx.Abort()
core.R(ctx).FailWithMessageAndCode("未授权操作", http.StatusUnauthorized)
}
return
}
u.Id = userId
// 查询
err := client.MySQL.Take(&u).Error
if err != nil {
log.Errorf("获取用户信息失败:%s", err.Error())
ctx.Abort()
if !dontResponse {
core.R(ctx).FailWithMessageAndCode("用户状态异常", http.StatusBadRequest)
}
return
}
// 需要跳过微信绑定检验
if !dontCheck {
// 检查微信绑定
if u.WechatOpenId == nil || *u.WechatOpenId == "" {
log.Errorf("%v 未绑定微信", u.Nickname)
core.R(ctx).FailWithMessageAndCode("请先绑定微信", http.StatusForbidden)
ctx.Abort()
}
}
}

32
client/mysql.go Normal file
View File

@ -0,0 +1,32 @@
package client
import (
"Lee-WineList/config"
"git.echol.cn/loser/logger"
"git.echol.cn/loser/logger/log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var MySQL *gorm.DB
func InitMySQLClient() {
// 创建连接对象
mysqlConfig := mysql.Config{
DSN: config.Scd.MySQL.GetDSN(),
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式
DontSupportRenameColumn: true, // 用 `change` 重命名列
}
conn, err := gorm.Open(mysql.New(mysqlConfig), &gorm.Config{Logger: logger.DefaultGormLogger()})
if err != nil {
log.Panic("初始化MySQL连接失败, 错误信息: %v", err)
} else {
log.Debug("MySQL连接成功")
}
MySQL = conn
//db, err := conn.DB()
//db.SetMaxIdleConns(10) // 用于设置连接池中空闲连接的最大数量
//db.SetMaxOpenConns(100) // 用于设置数据库连接的最大打开数量
//db.SetConnMaxLifetime(time.Hour) // 设置连接的最大存活时间
}

16
common/constant/login.go Normal file
View File

@ -0,0 +1,16 @@
package constant
// LoginType 自定义登录类型
type LoginType string
// 登录类型
const (
LoginTypeWeChatMiniApp LoginType = "wechat_mini_app" // 微信小程序登录
LoginTypeSms LoginType = "sms" // 短信验证码登录
LoginTypePassword LoginType = "password" // 密码登录
)
// String 转换为字符串
func (t LoginType) String() string {
return string(t)
}

View File

@ -0,0 +1,11 @@
package constant
const (
RdsCaptchaPrefix = "captcha:img:" // 验证码缓存前缀
RdsSmsCaptchaPrefix = "captcha:sms:" // 短信验证码缓存前缀
OAuth2RedisKey = "oauth:token:" // Token缓存前缀
OAuth2UserCacheKey = "oauth:user:" // 用户缓存前缀
ApiAntiShakeKey = "api:antishake:" // 防抖锁
ReportGenTaskKey = "report:gen:task" // 任务生成计划待办队列
WeChatSessionKey = "wechat:session:" // 小程序用户SessionKey前缀
)

111
common/types/date.go Normal file
View File

@ -0,0 +1,111 @@
package types
import (
"database/sql/driver"
"fmt"
"time"
)
// 默认时间格式
const dateFormat = "2006-01-02 15:04:05.000"
// DateTime 自定义时间类型
type DateTime time.Time
// Scan implements the Scanner interface.
func (dt *DateTime) Scan(value any) error {
// mysql 内部日期的格式可能是 2006-01-02 15:04:05 +0800 CST 格式,所以检出的时候还需要进行一次格式化
tTime, _ := time.ParseInLocation("2006-01-02 15:04:05 +0800 CST", value.(time.Time).String(), time.Local)
*dt = DateTime(tTime)
return nil
}
// Value implements the driver Valuer interface.
func (dt DateTime) Value() (driver.Value, error) {
// 0001-01-01 00:00:00 属于空值,遇到空值解析成 null 即可
if dt.String() == "0001-01-01 00:00:00.000" {
return nil, nil
}
return []byte(dt.Format(dateFormat)), nil
}
// 用于 fmt.Println 和后续验证场景
func (dt DateTime) String() string {
return dt.Format(dateFormat)
}
// Format 格式化
func (dt DateTime) Format(fm string) string {
return time.Time(dt).Format(fm)
}
// After 时间比较
func (dt *DateTime) After(now time.Time) bool {
return time.Time(*dt).After(now)
}
// Before 时间比较
func (dt *DateTime) Before(now time.Time) bool {
return time.Time(*dt).Before(now)
}
// IBefore 时间比较
func (dt *DateTime) IBefore(now DateTime) bool {
return dt.Before(time.Time(now))
}
// SubTime 对比
func (dt DateTime) SubTime(t time.Time) time.Duration {
return dt.ToTime().Sub(t)
}
// Sub 对比
func (dt DateTime) Sub(t DateTime) time.Duration {
return dt.ToTime().Sub(t.ToTime())
}
// ToTime 转换为golang的时间类型
func (dt DateTime) ToTime() time.Time {
return time.Time(dt).Local()
}
// IsNil 是否为空值
func (dt DateTime) IsNil() bool {
return dt.Format(dateFormat) == "0001-01-01 00:00:00.000"
}
// Unix 实现Unix函数
func (dt DateTime) Unix() int64 {
return dt.ToTime().Unix()
}
// EndOfCentury 获取本世纪最后时间
func (dt DateTime) EndOfCentury() DateTime {
yearEnd := time.Now().Local().Year()/100*100 + 99
return DateTime(time.Date(yearEnd, 12, 31, 23, 59, 59, 999999999, time.Local))
}
// ======== 序列化 ========
// MarshalJSON 时间到字符串
func (dt DateTime) MarshalJSON() ([]byte, error) {
// 过滤掉空数据
if dt.IsNil() {
return []byte("\"\""), nil
}
output := fmt.Sprintf(`"%s"`, dt.Format("2006-01-02 15:04:05"))
return []byte(output), nil
}
// UnmarshalJSON 字符串到时间
func (dt *DateTime) UnmarshalJSON(b []byte) error {
if len(b) == 2 {
*dt = DateTime{}
return nil
}
// 解析指定的格式
//now, err := time.ParseInLocation(`"`+dateFormat+`"`, string(b), time.Local)
now, err := time.ParseInLocation(dateFormat, string(b), time.Local)
*dt = DateTime(now)
return err
}

13
common/types/model.go Normal file
View File

@ -0,0 +1,13 @@
package types
import (
"gorm.io/gorm"
)
// BaseDbModel 数据库通用字段
type BaseDbModel struct {
Id int `json:"id" gorm:"primarykey"`
CreatedAt DateTime `json:"createdAt"`
UpdatedAt DateTime `json:"updatedAt"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index:deleted"`
}

35
config.yaml Normal file
View File

@ -0,0 +1,35 @@
api:
port: 8083
name: lee_wine_list
version: v1
prefix: /api/v1
mysql:
host: 127.0.0.1
port: 49154
user: root
password: mysqlpw
db: mini_app
#redis:
# host: 127.0.0.1
# port: 49153
# password: redispw
# db: 2
jwt:
AccessSecret: "leex"
AccessExpire: 604800
RefreshAfter: 86400
tencent:
mini-app:
app-id: xxxx
app-secret: xxxx
aliyun:
oss:
endpoint: oss-cn-shanghai.aliyuncs.com
bucket: xxxx
access-key: xxxx
access-key-secret: xxxx

14
config/aliyun.go Normal file
View File

@ -0,0 +1,14 @@
package config
// 阿里云配置
type aliyunConfig struct {
Oss aliOssConfig `mapstructure:"oss" yaml:"oss"` // oss配置
}
// OSSConfig 阿里云OSS配置
type aliOssConfig struct {
Endpoint string `mapstructure:"endpoint" yaml:"endpoint"`
Bucket string `mapstructure:"bucket" yaml:"bucket"`
AccessKeyId string `mapstructure:"access-key" yaml:"access-key"`
AccessKeySecret string `mapstructure:"access-key-secret" yaml:"access-key-secret"`
}

9
config/app.go Normal file
View File

@ -0,0 +1,9 @@
package config
type appInfo struct {
AppName string `mapstructure:"name" yaml:"name"` // 应用名称
Port uint64 `mapstructure:"port" yaml:"port"` // 端口号
Prefix string `mapstructure:"prefix" yaml:"prefix"` // 接口前缀
Version string `mapstructure:"version" yaml:"version"` // 版本号
Monster bool `mapstructure:"monster" yaml:"monster"` // 妖怪模式
}

14
config/config.go Normal file
View File

@ -0,0 +1,14 @@
package config
var Scd systemConfigData
var Nacos nacosConfig
// 配置信息
type systemConfigData struct {
Admin appInfo `mapstructure:"admin" yaml:"admin"` // 系统配置-Admin
Api appInfo `mapstructure:"api" yaml:"api"` // 系统配置-Api
MySQL mysqlConfig `mapstructure:"mysql" yaml:"mysql"` // MySQL配置
Aliyun aliyunConfig `mapstructure:"aliyun" yaml:"aliyun"` // 阿里云配置
Tencent tencentConfig `mapstructure:"tencent" yaml:"tencent"` // 腾讯相关配置
JWT JWT `mapstructure:"jwt" yaml:"jwt"`
}

8
config/jwt.go Normal file
View File

@ -0,0 +1,8 @@
package config
// JWT JWT配置
type JWT struct {
AccessSecret string `yaml:"AccessSecret"` // 加密签名
AccessExpire int64 `yaml:"AccessExpire"` // 签名有效时间
RefreshAfter int64 `yaml:"RefreshAfter"` // 签名刷新时间
}

20
config/mysql.go Normal file
View File

@ -0,0 +1,20 @@
package config
import (
"fmt"
)
// MySQL配置
type mysqlConfig struct {
Host string `mapstructure:"host" yaml:"host"` // 主机
Port int `mapstructure:"port" yaml:"port"` // 端口
User string `mapstructure:"user" yaml:"user"` // 用户名
Password string `mapstructure:"password" yaml:"password"` // 密码
Db string `mapstructure:"db" yaml:"db"` // 数据库名称
}
// GetDSN 返回 MySQL 连接字符串
func (c mysqlConfig) GetDSN() string {
return fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=utf8mb4&parseTime=True&loc=Local",
c.User, c.Password, c.Host, c.Port, c.Db)
}

9
config/nacos.go Normal file
View File

@ -0,0 +1,9 @@
package config
// Nacos配置
type nacosConfig struct {
Host string `env:"NACOS_HOST"` // 主机
Port uint64 `env:"NACOS_PORT" envDefault:"8848"` // 端口
NamespaceId string `env:"NACOS_NAMESPACE"` // 命名空间
CenterConfigName string `env:"NACOS_CONFIG_NAME" envDefault:"gtest.yml"` // 外部配置文件名,多个以逗号隔开
}

12
config/tencent.go Normal file
View File

@ -0,0 +1,12 @@
package config
// 腾讯相关配置
type tencentConfig struct {
MiniApp wechatMiniAppConfig `mapstructure:"mini-app" yaml:"mini-app"`
}
// 微信小程序配置
type wechatMiniAppConfig struct {
AppId string `mapstructure:"app-id" yaml:"app-id"`
AppSecret string `mapstructure:"app-secret" yaml:"app-secret"`
}

52
core/error_handle.go Normal file
View File

@ -0,0 +1,52 @@
package core
import (
"fmt"
"git.echol.cn/loser/logger/log"
"github.com/gin-gonic/gin"
"net"
"net/http"
"os"
"strings"
)
// NoMethodHandler 请求方式不对
func NoMethodHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
R(ctx).FailWithMessageAndCode(fmt.Sprintf("不支持%v请求", ctx.Request.Method), http.StatusMethodNotAllowed)
ctx.Abort()
}
}
// NoRouteHandler 404异常处理
func NoRouteHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
R(ctx).FailWithMessageAndCode("请求接口不存在", http.StatusNotFound)
ctx.Abort()
}
}
// Recovery Panic捕获
func Recovery() gin.HandlerFunc {
return func(ctx *gin.Context) {
defer func() {
if err := recover(); err != nil {
log.Errorf("系统错误: %v", err)
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
if brokenPipe {
log.Errorf("%s", err)
}
R(ctx).FailWithMessage("服务器异常,请联系管理员")
ctx.Abort()
}
}()
ctx.Next()
}
}

84
core/response.go Normal file
View File

@ -0,0 +1,84 @@
package core
import (
"Lee-WineList/utils"
"github.com/gin-gonic/gin"
"net/http"
)
// 返回数据包装
type response struct {
Code int `json:"code"`
Data any `json:"data"`
Msg string `json:"message"`
}
type rs struct {
ctx *gin.Context
}
// 定义状态码
const (
ERROR = http.StatusInternalServerError
SUCCESS = http.StatusOK
)
func R(ctx *gin.Context) *rs {
return &rs{ctx}
}
// Result 手动组装返回结果
func (r rs) Result(code int, data any, msg string) {
//if data == nil {
// data = map[string]any{}
//}
r.ctx.JSON(code, response{
code,
data,
msg,
})
}
// Ok 返回无数据的成功
func (r rs) Ok() {
r.Result(SUCCESS, nil, "操作成功")
}
// OkWithMessage 返回自定义成功的消息
func (r rs) OkWithMessage(message string) {
r.Result(SUCCESS, nil, message)
}
// OkWithData 自定义内容的成功返回
func (r rs) OkWithData(data any) {
r.Result(SUCCESS, data, "操作成功")
}
// OkWithPageData 返回分页数据
func (r rs) OkWithPageData(data any, total int64, current, size int) {
// 计算总页码
totalPage := utils.GenTotalPage(total, size)
// 返回结果
r.Result(SUCCESS, PageData{Current: current, Size: size, Total: total, TotalPage: totalPage, Records: data}, "操作成功")
}
// OkDetailed 自定义消息和内容的成功返回
func (r rs) OkDetailed(data any, message string) {
r.Result(SUCCESS, data, message)
}
// Fail 返回默认失败
func (r rs) Fail() {
r.Result(ERROR, nil, "操作失败")
}
// FailWithMessage 返回默认状态码自定义消息的失败
func (r rs) FailWithMessage(message string) {
r.Result(ERROR, nil, message)
}
// FailWithMessageAndCode 返回自定义消息和状态码的失败
func (r rs) FailWithMessageAndCode(message string, code int) {
r.Result(code, nil, message)
}

10
core/response_page.go Normal file
View File

@ -0,0 +1,10 @@
package core
// PageData 分页数据通用结构体
type PageData struct {
Current int `json:"current"` // 当前页码
Size int `json:"size"` // 每页数量
Total int64 `json:"total"` // 总数
TotalPage int `json:"total_page"` // 总页数
Records any `json:"records"` // 返回数据
}

107
go.mod Normal file
View File

@ -0,0 +1,107 @@
module Lee-WineList
go 1.18
require (
git.echol.cn/loser/logger v1.0.15
github.com/aliyun/alibaba-cloud-sdk-go v1.62.288
github.com/aliyun/aliyun-oss-go-sdk v2.2.7+incompatible
github.com/duke-git/lancet/v2 v2.1.19
github.com/fsnotify/fsnotify v1.6.0
github.com/gin-gonic/gin v1.9.0
github.com/go-oauth2/oauth2/v4 v4.5.2
github.com/go-oauth2/redis/v4 v4.1.1
github.com/go-redis/redis/v8 v8.11.5
github.com/google/uuid v1.3.0
github.com/medivhzhan/weapp/v3 v3.6.19
github.com/spf13/viper v1.15.0
golang.org/x/crypto v0.8.0
gorm.io/driver/mysql v1.5.0
gorm.io/driver/postgres v1.5.0
gorm.io/gorm v1.25.0
)
require (
git.echol.cn/loser/loki-client-go v1.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.8.0 // indirect
github.com/caarlos0/env/v6 v6.9.2 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/gin-contrib/sse v0.1.0 // 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-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
github.com/golang/protobuf v1.5.2 // 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.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lixh00/loki-client-go v1.0.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // 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/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.12.2 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.34.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
github.com/tidwall/buntdb v1.1.2 // indirect
github.com/tidwall/gjson v1.12.1 // indirect
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.1.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect
google.golang.org/grpc v1.52.0 // indirect
google.golang.org/protobuf v1.28.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
)

1480
go.sum Normal file

File diff suppressed because it is too large Load Diff

10
initialize/clients.go Normal file
View File

@ -0,0 +1,10 @@
package initialize
import "Lee-WineList/client"
// 初始化数据库客户端
func initClient() {
client.InitMySQLClient()
client.InitRedisClient()
//client.InitPostgreSQLClient()
}

40
initialize/config.go Normal file
View File

@ -0,0 +1,40 @@
package initialize
import (
"Lee-WineList/config"
"fmt"
"git.echol.cn/loser/logger/log"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
var localVp *viper.Viper
func initLocaConfig() {
localVp = viper.New()
localVp.SetConfigName("config") // 文件名-无需文件后缀
localVp.SetConfigType("yaml") // 文件扩展名
localVp.AddConfigPath(".")
// 处理读取配置文件的错误
if err := localVp.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
panic(fmt.Errorf("未找到配置文件,请检查路径: %s \n", err))
} else {
panic(fmt.Errorf("文件已找到,但读取文件时出错: %s \n", err))
}
}
// 解析配置文件为结构体
if err := localVp.Unmarshal(&config.Scd); err != nil {
log.Panic(err)
}
localVp.WatchConfig() // 监听配置文件变化
localVp.OnConfigChange(func(e fsnotify.Event) {
log.Info("配置文件发生变动:", e.Name)
fmt.Printf("配置文件发生变动:%s", e.Name)
if err := localVp.Unmarshal(&config.Scd); err != nil {
log.Panic(err)
}
// 配置文件发生变动,重新初始化一下连接信息
initLocaConfig()
})
}

21
initialize/db_table.go Normal file
View File

@ -0,0 +1,21 @@
package initialize
import (
"Lee-WineList/client"
"Lee-WineList/model/entity"
"git.echol.cn/loser/logger/log"
)
// 初始化数据库表
func databaseTable() {
dbs := []any{
entity.User{},
entity.AdminUser{},
entity.OAuth2Client{},
}
if err := client.MySQL.AutoMigrate(dbs...); err != nil {
log.Panicf("数据库表预初始化处理:%s", err.Error())
}
log.Debugf("数据库表预初始化处理完成")
}

8
initialize/init.go Normal file
View File

@ -0,0 +1,8 @@
package initialize
// InitSystem 初始化系统
func InitSystem() {
initLocaConfig() // 初始化本地配置
initClient() // 初始化连接池等信息
databaseTable() // 初始化数据库表信息
}

70
middleware/auth.go Normal file
View File

@ -0,0 +1,70 @@
package middleware
import (
"Lee-WineList/core"
"Lee-WineList/oauth2"
"git.echol.cn/loser/logger/log"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
// AuthorizeToken 验证OAuth2生成的Token
func AuthorizeToken() gin.HandlerFunc {
return func(ctx *gin.Context) {
// 判断有无token
tokenStr := ctx.GetHeader("Authorization")
if tokenStr == "" || !strings.HasPrefix(tokenStr, "Bearer ") {
core.R(ctx).FailWithMessageAndCode("请先登录", http.StatusUnauthorized)
ctx.Abort()
return
}
// 先取出用户Token
token, err := oauth2.OAuthServer.ValidationBearerToken(ctx.Request)
if err != nil {
log.Errorf("获取Token失败错误%s", err.Error())
core.R(ctx).FailWithMessageAndCode("登录已失效或已在其他地方登录", http.StatusUnauthorized)
ctx.Abort()
return
}
// 把UserId字段反序列化成map
//info := make(map[string]string)
//if err = json.Unmarshal([]byte(token.GetUserID()), &info); err != nil {
// core.R(ctx).FailWithMessageAndCode("Token数据解析失败", http.StatusUnauthorized)
// ctx.Abort()
// return
//}
//go func() {
// // 异步记录用户在线情况,十分钟没操作就是不在线了
// rdsKey := "oauth:online:" + info["userId"]
// global.RedisConn.Set(context.Background(), rdsKey, "1", 10*time.Minute)
//}()
// 判断通过,允许放行
ctx.Request.Header.Add("userId", token.GetUserID())
ctx.Set("userId", token.GetUserID())
ctx.Next()
}
}
// DealLoginUserId 处理登录用户Id
func DealLoginUserId() gin.HandlerFunc {
return func(ctx *gin.Context) {
// 判断有无token
tokenStr := ctx.GetHeader("Authorization")
if tokenStr == "" || !strings.HasPrefix(tokenStr, "Bearer ") {
//ctx.Next()
return
}
// 先取出用户Token
token, err := oauth2.OAuthServer.ValidationBearerToken(ctx.Request)
if err != nil {
//ctx.Next()
return
}
//log.Debugf("本次请求存在正常Token: %v", tokenStr)
// 判断通过,允许放行
ctx.Request.Header.Add("userId", token.GetUserID())
ctx.Set("userId", token.GetUserID())
//ctx.Next()
}
}

29
middleware/request.go Normal file
View File

@ -0,0 +1,29 @@
package middleware
import (
"Lee-WineList/core"
"github.com/gin-gonic/gin"
"net/http"
)
type request struct{}
// Request Open
func Request() *request {
return &request{}
}
// NotInternalRequest 检查是否是内部调用
func (request) NotInternalRequest() gin.HandlerFunc {
return func(ctx *gin.Context) {
// 取出来源
from := ctx.Request.Header.Get("X-Request-From")
if from != "internal" {
// 如果请求不是内部请求,直接返回请求不合法
core.R(ctx).FailWithMessageAndCode("请求不合法", http.StatusBadRequest)
ctx.Abort()
return
}
ctx.Next()
}
}

19
model/cache/user.go vendored Normal file
View File

@ -0,0 +1,19 @@
package cache
import "encoding/json"
// UserInfo 登录用的用户信息结构体
type UserInfo struct {
UserType string `json:"userType"` // 用户类型
RoleCodes string `json:"roleCodes"` // 角色代码
UserId string `json:"userId"` // 用户Id
}
// String 实现Stringer接口
func (i UserInfo) String() (string, error) {
b, err := json.Marshal(i)
if err != nil {
return "", err
}
return string(b), nil
}

View File

@ -0,0 +1,18 @@
package entity
import (
"Lee-WineList/common/types"
)
// OAuth2Client OAuth2客户端信息
type OAuth2Client struct {
types.BaseDbModel
ClientId string `gorm:"type:varchar(255) not null"`
ClientSecret string `gorm:"type:varchar(255) not null"`
Grant string `gorm:"type:varchar(255) not null"` // 允许的授权范围,支持类型: oauth2.GrantType
Domain string `gorm:"type:varchar(255)"`
}
func (OAuth2Client) TableName() string {
return "t_oauth2_client"
}

19
model/entity/user.go Normal file
View File

@ -0,0 +1,19 @@
package entity
import (
"Lee-WineList/common/types"
)
// User 普通用户表
type User struct {
types.BaseDbModel
Phone string `json:"phone" gorm:"index:deleted,unique;type:varchar(255) not null comment '手机号'"`
UnionId string `json:"union_id" gorm:"type:varchar(255) comment '微信UnionId'"`
OpenId string `json:"open_id" gorm:"type:varchar(255) comment '微信OpenId'"`
Nickname string `json:"nickname" gorm:"type:varchar(255) comment '昵称'"`
Avatar string `json:"avatar" gorm:"type:varchar(255) comment '头像'"`
}
func (User) TableName() string {
return "t_user"
}

7
model/param/base.go Normal file
View File

@ -0,0 +1,7 @@
package param
// 分页通用参数
type page struct {
Current int `json:"current" form:"current" binding:"required"` // 页码
Size int `json:"size" form:"size" binding:"required"` // 每页数量
}

7
model/param/general.go Normal file
View File

@ -0,0 +1,7 @@
package param
// SendSmsCode 获取短信验证码入参
type SendSmsCode struct {
Phone string `json:"phone" form:"phone" binding:"required"` // 手机号
Key string `json:"key" form:"key" binding:"required"` // 验证码key
}

23
model/param/login.go Normal file
View File

@ -0,0 +1,23 @@
package param
// Login 用户登录入参
type Login struct {
VerifyId string `json:"verifyId" form:"verifyId"` // 验证Id
VerifyCode string `json:"verifyCode" form:"verifyCode"` // 验证码
Username string `json:"username" form:"username" binding:"required"` // 邮箱或手机号
Password string `json:"password" form:"password"` // 密码
Code string `json:"code" form:"code"` // 微信小程序登录code
}
// RefreshToken 刷新Token入参
type RefreshToken struct {
RefreshToken string `json:"refresh_token" form:"refresh_token" binding:"required"` // 刷新Token
GrantType string `json:"grant_type" form:"grant_type" binding:"required"` // 授权类型,写refresh_token
}
// DecryptMobile 解密用户手机号入参
type DecryptMobile struct {
SessionKey string `json:"sessionKey" form:"sessionKey"` // sessionKey
EncryptedData string `json:"encryptedData" form:"encryptedData" binding:"required"` // 加密数据
Iv string `json:"iv" form:"iv" binding:"required"` // 加密算法的初始向量
}

36
model/param/user.go Normal file
View File

@ -0,0 +1,36 @@
package param
// BindingWeChat 绑定微信
type BindingWeChat struct {
Code string `json:"code" form:"code" binding:"required"` // 微信code
}
type GetUserList struct {
page
Phone string `json:"phone" form:"phone"` // 手机号
Status string `json:"status" form:"status" binding:"oneof='' NORMAL DISABLE"` // 用户状态
StartAt string `json:"startAt" form:"startAt"` // 开始时间
EndAt string `json:"endAt" form:"endAt"` // 结束时间
}
// ChangeUserInfo 修改普通用户信息
type ChangeUserInfo struct {
Nickname string `json:"nickname" form:"nickname"` // 昵称
Avatar string `json:"avatar" form:"avatar"` // 头像
Birthday string `json:"birthday" form:"birthday"` // 生日
}
// ChangePassword 修改密码
type ChangePassword struct {
OldPassword string `json:"oldPassword" form:"oldPassword" binding:"required"` // 旧密码
NewPassword string `json:"newPassword" form:"oldPassword" binding:"required"` // 新密码
ConfirmPassword string `json:"confirmPassword" form:"oldPassword" binding:"required,eqcsfield=NewPassword"` // 确认新密码
}
// SaveUser 保存用户信息
type SaveUser struct {
Id int `json:"id" form:"id"` // 用户ID
Username string `json:"username" form:"username"` // 用户名
Nickname string `json:"nickname" form:"nickname"` // 昵称
Password string `json:"password" form:"password"` // 密码
}

22
model/resp/login.go Normal file
View File

@ -0,0 +1,22 @@
package resp
// Code2Session 用户登录凭证校验模型
type Code2Session struct {
Code string
AppId string
AppSecret string
}
// Code2SessionResult 凭证校验后返回的JSON数据包模型
type Code2SessionResult struct {
OpenId string `json:"openid"`
SessionKey string `json:"session_key"`
UnionId string `json:"unionid"`
ErrCode uint `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
// UserInfo 用户信息,OpenID用户唯一标识
type UserInfo struct {
OpenId string `json:"openId"`
}

103
repository/base.go Normal file
View File

@ -0,0 +1,103 @@
package repository
import (
"fmt"
"git.echol.cn/loser/logger/log"
"gorm.io/gorm"
)
// 分页组件
func page(current, size int) func(db *gorm.DB) *gorm.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(offset).Limit(size)
}
}
// @title updateSortBefore
// @description 更新之前处理序号
// @param tx *gorm.DB "已开启的事务对象"
// @param model any "模型对象"
// @return error "错误信息"
func updateSortBefore(tx *gorm.DB, tableName, id string, sort int, param string) (err error) {
// 查出原来的排序号
var oldSort int
err = tx.Table(tableName).Select("sort").Where("id = ?", id).Scan(&oldSort).Error
if err != nil {
log.Errorf("查询老数据失败: %v", err.Error())
return
}
// 如果相等,啥都不干
if oldSort == sort {
return nil
}
// 处理排序
// 如果老的排序号小于新的,(老, 新]之间的排序号都要-1
// 如果老的大于新的,[老, 新)排序号-1
if oldSort < sort {
// 老的小于新的,[老, 新) + 1
sel := tx.Table(tableName).
Where("sort <= ? AND sort > ?", sort, oldSort).
Where("deleted_at IS NULL")
if param != "" {
sel.Where(param) // 自定义条件
}
err = sel.Update("sort", gorm.Expr("sort - 1")).Error
} else {
// 老的大于新的,[新, 老) + 1
sel := tx.Table(tableName).
Where("sort >= ? AND sort < ?", sort, oldSort).
Where("deleted_at IS NULL")
if param != "" {
sel.Where(param) // 自定义条件
}
err = sel.Update("sort", gorm.Expr("sort + 1")).Error
}
return
}
// @title createSortBefore
// @description 新建之前处理序号
// @param tx *gorm.DB "已开启的事务对象"
// @param model any "模型对象"
// @return error "错误信息"
func createSortBefore(tx *gorm.DB, tableName string, sort int, param string) (err error) {
// 处理排序,如果没有传,就会是在最前面
sel := tx.Table(tableName).Where("sort >= ?", sort).
Where("deleted_at IS NULL")
if param != "" {
sel.Where(param)
}
err = sel.Update("sort", gorm.Expr("sort + 1")).Error
if err != nil {
log.Errorf("处理前置排序失败:%v", err)
}
return
}
// @title dealSortAfter
// @description 处理序号之后
// @param tx *gorm.DB "已开启的事务对象"
// @param modelName string "表名"
// @return error "错误信息"
func dealSortAfter(tx *gorm.DB, modelName, param string) (err error) {
// 保存成功,刷新排序
if param != "" {
param += " AND "
}
sql := fmt.Sprintf("UPDATE %s a, (SELECT (@i := @i + 1) i, id FROM %s WHERE %s deleted_at IS NULL order by sort ASC) i, "+
"(SELECT @i := 0) ir SET a.sort = i.i, updated_at=now() WHERE a.id = i.id", modelName, modelName, param)
err = tx.Exec(sql).Error
if err != nil {
log.Errorf("刷新排序失败: %v", err.Error())
}
return
}

99
repository/user.go Normal file
View File

@ -0,0 +1,99 @@
package repository
import (
"Lee-WineList/client"
"Lee-WineList/config"
"Lee-WineList/model/entity"
"Lee-WineList/model/resp"
"encoding/json"
"errors"
"fmt"
"git.echol.cn/loser/logger/log"
"gorm.io/gorm"
"net/http"
)
type user struct {
}
// User ...
func User() *user {
return &user{}
}
// GetUser 查询单个用户信息
func (user) GetUser(user *entity.User) (err error) {
return client.MySQL.Take(&user, user).Error
}
// Login Code登录
func (u *user) Login(code string) *entity.User {
var acsJson resp.Code2SessionResult
acs := resp.Code2Session{
Code: code,
AppId: config.Scd.Tencent.MiniApp.AppId,
AppSecret: config.Scd.Tencent.MiniApp.AppSecret,
}
api := "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
res, err := http.DefaultClient.Get(fmt.Sprintf(api, acs.AppId, acs.AppSecret, acs.Code))
if err != nil {
fmt.Println("微信登录凭证校验接口请求错误")
return nil
}
if err := json.NewDecoder(res.Body).Decode(&acsJson); err != nil {
fmt.Println("decoder error...")
return nil
}
// 查看用户是否已经存在
rows := client.MySQL.Where("open_id = ?", acsJson.OpenId).First(&entity.User{}).RowsAffected
if rows == 0 {
// 不存在,添加用户
fmt.Println(acsJson.OpenId)
user := entity.User{
OpenId: acsJson.OpenId,
}
row := client.MySQL.Create(&user).RowsAffected
if row == 0 {
fmt.Println("add app user error...")
return nil
}
}
return &entity.User{OpenId: acsJson.OpenId}
}
// GetOrCreate 查询或创建用户
func (user) GetOrCreate(user *entity.User) (err error) {
err = client.MySQL.Take(&user, user).Error
if err == nil {
return
}
// 如果是没查询到记录,则创建用户
if err == gorm.ErrRecordNotFound {
// 用户不存在,创建用户
user.Nickname = "用户"
//user.Avatar = "https://hyxc-mini.oss-cn-beijing.aliyuncs.com/application/resource/miniapp/index/index-more-love.png"
user.Avatar = "https://hyxc-mini.oss-cn-beijing.aliyuncs.com/avatar/mona-loading-dark.gif"
if err = client.MySQL.Create(&user).Error; err != nil {
log.Errorf("账号创建失败: %v", err)
err = errors.New("登录失败")
return
}
}
return
}
// CheckUnionIdIsExist 检查UnionId和OpenId是否存在
func (user) CheckUnionIdIsExist(unionId, openId string) bool {
var count int64
err := client.MySQL.Model(&entity.User{}).Where("union_id = ? and open_id = ?", unionId, openId).Count(&count).Error
if err != nil {
return false
}
return count > 0
}
// UpdateUserInfo 更新普通用户信息
func (user) UpdateUserInfo(e *entity.User) (err error) {
return client.MySQL.Updates(&e).Error
}

15
router/app/login.go Normal file
View File

@ -0,0 +1,15 @@
package app
import (
"Lee-WineList/api/app"
"Lee-WineList/middleware"
"github.com/gin-gonic/gin"
)
// 登录相关
func login(g *gin.RouterGroup) {
// 登录相关接口
g.POST("/token", app.LoginApi().Login)
g.POST("/token/refresh", app.LoginApi().Refresh)
g.POST("/token/logout", middleware.AuthorizeToken(), app.LoginApi().Logout)
}

12
router/app/route.go Normal file
View File

@ -0,0 +1,12 @@
package app
import (
"Lee-WineList/middleware"
"github.com/gin-gonic/gin"
)
// InitRoute 初始化路由
func InitRoute(g *gin.RouterGroup) {
login(g) // 登录相关路由
user(g.Group("/user", middleware.AuthorizeToken())) // 用户相关路由
}

13
router/app/user.go Normal file
View File

@ -0,0 +1,13 @@
package app
import (
"Lee-WineList/api/app"
"github.com/gin-gonic/gin"
)
// 用户相关路由
func user(g *gin.RouterGroup) {
g.GET("", app.UserApi().GetUser) // 获取当前登录用户信息
g.POST("/binding/wechat", app.UserApi().BindingWeChat) // 绑定微信
g.POST("/update", app.UserApi().UpdateUser) // 修改用户信息
}

14
test/pwd_test.go Normal file
View File

@ -0,0 +1,14 @@
package test
import (
"fmt"
"golang.org/x/crypto/bcrypt"
"testing"
)
func TestPwd(t *testing.T) {
bytePass := []byte("123123")
hPass, _ := bcrypt.GenerateFromPassword(bytePass, bcrypt.DefaultCost)
s := string(hPass)
fmt.Println(s)
}

16
utils/desensitization.go Normal file
View File

@ -0,0 +1,16 @@
package utils
type desensitization struct{}
// Desensitization 暴露接口 - 脱敏相关
func Desensitization() *desensitization {
return &desensitization{}
}
// Phone 脱敏手机号
func (desensitization) Phone(phone string) string {
if len(phone) == 11 {
return phone[:3] + "****" + phone[7:]
}
return phone
}

63
utils/file.go Normal file
View File

@ -0,0 +1,63 @@
package utils
import (
"fmt"
"github.com/duke-git/lancet/v2/fileutil"
"os"
)
type fileUtils struct{}
func FileUtils() *fileUtils {
return &fileUtils{}
}
func (f fileUtils) CheckIsExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
// CheckAndCreate 判断文件名是否存在,不存在就创建
func (f fileUtils) CheckAndCreate(path string) error {
exists, err := f.CheckIsExists(path)
if err != nil {
return err
}
if !exists {
err = os.MkdirAll(path, os.ModePerm)
if err != nil {
return err
}
}
return nil
}
// DelPath 删除目录下的所有文件和目录本身
func (f fileUtils) DelPath(path string) error {
files, err := fileutil.ListFileNames(path)
if err != nil {
return err
}
for _, file := range files {
// 如果是目录,则递归删除
if fileutil.IsDir(file) {
err = f.DelPath(file)
if err != nil {
return err
}
} else {
err = fileutil.RemoveFile(fmt.Sprintf("%s/%s", path, file))
if err != nil {
return err
}
}
}
_ = fileutil.RemoveFile(path)
return nil
}

79
utils/http.go Normal file
View File

@ -0,0 +1,79 @@
package utils
import (
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
type httpUtil struct {
}
// Http 暴露接口
func Http() *httpUtil {
return &httpUtil{}
}
// NewProxy 重写创建代理函数,加入 Host 信息
func (hu httpUtil) NewProxy(target *url.URL) *httputil.ReverseProxy {
targetQuery := target.RawQuery
director := func(req *http.Request) {
req.Host = target.Host
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path, req.URL.RawPath = hu.joinURLPath(target, req.URL)
// 过滤掉参数
if uri, err := url.Parse(req.URL.Path); err == nil {
req.URL.Path = uri.Path
}
var rawQuery string
if targetQuery == "" || req.URL.RawQuery == "" {
rawQuery = targetQuery + req.URL.RawQuery
} else {
rawQuery = targetQuery + "&" + req.URL.RawQuery
}
req.URL.RawQuery = rawQuery
if _, ok := req.Header["User-Agent"]; !ok {
// explicitly disable User-Agent so it's not set to default value
req.Header.Set("User-Agent", "")
}
// 补充内部调用Header
req.Header.Set("X-Request-From", "internal")
}
return &httputil.ReverseProxy{Director: director}
}
func (hu httpUtil) joinURLPath(a, b *url.URL) (path, rawpath string) {
if a.RawPath == "" && b.RawPath == "" {
return hu.singleJoiningSlash(a.Path, b.Path), ""
}
// Same as singleJoiningSlash, but uses EscapedPath to determine
// whether a slash should be added
apath := a.EscapedPath()
bpath := b.EscapedPath()
aslash := strings.HasSuffix(apath, "/")
bslash := strings.HasPrefix(bpath, "/")
switch {
case aslash && bslash:
return a.Path + b.Path[1:], apath + bpath[1:]
case !aslash && !bslash:
return a.Path + "/" + b.Path, apath + "/" + bpath
}
return a.Path + b.Path, apath + bpath
}
func (hu httpUtil) singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}

71
utils/jwt.go Normal file
View File

@ -0,0 +1,71 @@
package utils
import (
"Lee-WineList/config"
"time"
"github.com/golang-jwt/jwt"
)
type JWT struct {
claims jwt.MapClaims
}
// GenerateToken 根据UserId生成并返回Token
func GenerateToken(userId uint64) string {
jwtConfig := config.Scd.JWT
now := time.Now().Unix()
claims := make(jwt.MapClaims)
claims["exp"] = now + jwtConfig.AccessExpire
claims["iat"] = now
claims["rft"] = now + jwtConfig.RefreshAfter
claims["userId"] = userId
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
tokenStr, _ := token.SignedString([]byte(jwtConfig.AccessSecret))
return tokenStr
}
// ParseToken 解析Token
func ParseToken(tokenStr string) (*JWT, error) {
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
return []byte(config.Scd.JWT.AccessSecret), nil
})
if err != nil {
return nil, err
}
return &JWT{claims: token.Claims.(jwt.MapClaims)}, nil
}
// Valid 验证token是否有效
func (jwt *JWT) Valid() bool {
if err := jwt.claims.Valid(); err != nil {
return false
}
return true
}
// CheckRefresh 检查是否可以刷新
func (jwt *JWT) CheckRefresh() bool {
if time.Now().Unix() > jwt.GetRefreshTime() {
return true
}
return false
}
func (jwt *JWT) RefreshToken() string {
return GenerateToken(jwt.GetUserId())
}
// GetUserId 从Token中解析userId
func (jwt *JWT) GetUserId() uint64 {
return uint64(jwt.claims["userId"].(float64))
}
// GetRefreshTime 从Token中解析refreshTime
func (jwt *JWT) GetRefreshTime() int64 {
return int64(jwt.claims["rft"].(float64))
}

62
utils/oss.go Normal file
View File

@ -0,0 +1,62 @@
package utils
import (
"Lee-WineList/config"
"fmt"
"git.echol.cn/loser/logger/log"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"io"
)
type ossUtils struct{}
func OssUtils() *ossUtils {
return &ossUtils{}
}
// UploadReader 上传文件 - io.Reader
func (o ossUtils) UploadReader(fileName string, data io.Reader) (string, error) {
// 创建OSSClient实例。
client, err := oss.New(config.Scd.Aliyun.Oss.Endpoint, config.Scd.Aliyun.Oss.AccessKeyId, config.Scd.Aliyun.Oss.AccessKeySecret)
if err != nil {
log.Errorf("创建OSSClient实例失败: %s", err)
return "", err
}
// 获取存储空间。
bucket, err := client.Bucket(config.Scd.Aliyun.Oss.Bucket)
if err != nil {
log.Errorf("获取存储空间失败: %s", err)
return "", err
}
// 指定存储类型为标准存储,默认为标准存储。
storageType := oss.ObjectStorageClass(oss.StorageStandard)
// 指定访问权限为公共读默认为继承bucket的权限。
objectAcl := oss.ObjectACL(oss.ACLPublicRead)
// 上传文件
err = bucket.PutObject(fileName, data, storageType, objectAcl)
if err != nil {
log.Errorf("上传文件失败: %s", err)
return "", err
}
fileUrl := fmt.Sprintf("https://%s.%s/%s", config.Scd.Aliyun.Oss.Bucket, config.Scd.Aliyun.Oss.Endpoint, fileName)
return fileUrl, err
}
func (o ossUtils) GetFile(fileName, localFilePath string) error {
client, err := oss.New(config.Scd.Aliyun.Oss.Endpoint, config.Scd.Aliyun.Oss.AccessKeyId, config.Scd.Aliyun.Oss.AccessKeySecret)
if err != nil {
log.Errorf("创建OSSClient实例失败: %s", err)
return err
}
// 获取存储空间。
bucket, err := client.Bucket(config.Scd.Aliyun.Oss.Bucket)
if err != nil {
log.Errorf("获取存储空间失败: %s", err)
return err
}
err = bucket.GetObjectToFile(fileName, localFilePath)
if err != nil {
return err
}
return nil
}

14
utils/page.go Normal file
View File

@ -0,0 +1,14 @@
package utils
// GenTotalPage 计算总页数
func GenTotalPage(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
}

28
utils/time.go Normal file
View File

@ -0,0 +1,28 @@
package utils
import (
"fmt"
"strings"
"time"
)
type timeUtils struct{}
func TimeUtils() *timeUtils {
return &timeUtils{}
}
// DurationString Duration转自定义String
func (tu timeUtils) DurationString(d time.Duration) string {
if d.Seconds() == 0 {
return "00:00:00"
}
s := d.String()
s = strings.ReplaceAll(s, "h", "小时")
s = strings.ReplaceAll(s, "m", "分")
idx := strings.Index(s, ".")
if idx == -1 {
return fmt.Sprintf("%s秒", s)
}
return fmt.Sprintf("%s秒", s[:idx])
}

20
utils/token.go Normal file
View File

@ -0,0 +1,20 @@
package utils
import (
"golang.org/x/crypto/bcrypt"
)
// 生成Token的密钥写死防止乱改
//var jwtSecret = "qSxw4fCBBBecPsws"
// HashPassword 加密密码
func HashPassword(pass *string) {
bytePass := []byte(*pass)
hPass, _ := bcrypt.GenerateFromPassword(bytePass, bcrypt.DefaultCost)
*pass = string(hPass)
}
// ComparePassword 校验密码
func ComparePassword(dbPass, pass string) bool {
return bcrypt.CompareHashAndPassword([]byte(dbPass), []byte(pass)) == nil
}

70
utils/wechat.go Normal file
View File

@ -0,0 +1,70 @@
package utils
import (
"Lee-WineList/config"
"Lee-WineList/model/param"
"git.echol.cn/loser/logger/log"
"github.com/medivhzhan/weapp/v3"
"github.com/medivhzhan/weapp/v3/auth"
"github.com/medivhzhan/weapp/v3/phonenumber"
)
type wechat struct{}
func WeChatUtils() *wechat {
return &wechat{}
}
// GetWechatUnionId 获取微信用户基础信息
func (w wechat) GetWechatUnionId(code string) (unionId, openId, sessionKey string, err error) {
sdk := weapp.NewClient(config.Scd.Tencent.MiniApp.AppId, config.Scd.Tencent.MiniApp.AppSecret, weapp.WithLogger(nil))
cli := sdk.NewAuth()
p := auth.Code2SessionRequest{
Appid: config.Scd.Tencent.MiniApp.AppId,
Secret: config.Scd.Tencent.MiniApp.AppSecret,
JsCode: code,
GrantType: "authorization_code",
}
session, err := cli.Code2Session(&p)
if err != nil {
return
}
if session.GetResponseError() != nil {
log.Errorf("Code解析失败: %v", session.GetResponseError())
err = session.GetResponseError()
return
}
// 设置UnionId值
unionId = session.Unionid
openId = session.Openid
sessionKey = session.SessionKey
return
}
// GetWechatPhone 根据Code获取小程序用户手机号
// return 不带区号的手机号
func (w wechat) GetWechatPhone(param param.DecryptMobile) (string, error) {
sdk := weapp.NewClient(config.Scd.Tencent.MiniApp.AppId, config.Scd.Tencent.MiniApp.AppSecret)
mobile, err := sdk.DecryptMobile(param.SessionKey, param.EncryptedData, param.Iv)
if err != nil {
log.Errorf("解密手机号失败: %v", err)
return "", err
}
log.Debugf("解密后的手机号: %+v", mobile)
return mobile.PurePhoneNumber, nil
}
// GetPhoneNumber 获取手机号
func (w wechat) GetPhoneNumber(code string) (phone string, err error) {
sdk := weapp.NewClient(config.Scd.Tencent.MiniApp.AppId, config.Scd.Tencent.MiniApp.AppSecret)
resp, err := sdk.NewPhonenumber().GetPhoneNumber(&phonenumber.GetPhoneNumberRequest{Code: code})
if err != nil {
log.Errorf("获取手机号失败: %v", err)
return
}
// 获取手机号
phone = resp.Data.PurePhoneNumber
return
}