✨ init project
This commit is contained in:
parent
84729ebb66
commit
cdb5a4f9cd
109
api/app/login.go
Normal file
109
api/app/login.go
Normal 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
104
api/app/user.go
Normal 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
69
api/base.go
Normal 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
32
client/mysql.go
Normal 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
16
common/constant/login.go
Normal 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)
|
||||||
|
}
|
11
common/constant/rds_key.go
Normal file
11
common/constant/rds_key.go
Normal 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
111
common/types/date.go
Normal 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
13
common/types/model.go
Normal 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
35
config.yaml
Normal 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
14
config/aliyun.go
Normal 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
9
config/app.go
Normal 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
14
config/config.go
Normal 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
8
config/jwt.go
Normal 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
20
config/mysql.go
Normal 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
9
config/nacos.go
Normal 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
12
config/tencent.go
Normal 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
52
core/error_handle.go
Normal 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
84
core/response.go
Normal 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
10
core/response_page.go
Normal 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
107
go.mod
Normal 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
|
||||||
|
)
|
10
initialize/clients.go
Normal file
10
initialize/clients.go
Normal 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
40
initialize/config.go
Normal 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
21
initialize/db_table.go
Normal 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
8
initialize/init.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package initialize
|
||||||
|
|
||||||
|
// InitSystem 初始化系统
|
||||||
|
func InitSystem() {
|
||||||
|
initLocaConfig() // 初始化本地配置
|
||||||
|
initClient() // 初始化连接池等信息
|
||||||
|
databaseTable() // 初始化数据库表信息
|
||||||
|
}
|
70
middleware/auth.go
Normal file
70
middleware/auth.go
Normal 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
29
middleware/request.go
Normal 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
19
model/cache/user.go
vendored
Normal 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
|
||||||
|
}
|
18
model/entity/oauth2_client.go
Normal file
18
model/entity/oauth2_client.go
Normal 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
19
model/entity/user.go
Normal 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
7
model/param/base.go
Normal 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
7
model/param/general.go
Normal 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
23
model/param/login.go
Normal 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
36
model/param/user.go
Normal 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
22
model/resp/login.go
Normal 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
103
repository/base.go
Normal 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
99
repository/user.go
Normal 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
15
router/app/login.go
Normal 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
12
router/app/route.go
Normal 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
13
router/app/user.go
Normal 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
14
test/pwd_test.go
Normal 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
16
utils/desensitization.go
Normal 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
63
utils/file.go
Normal 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
79
utils/http.go
Normal 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
71
utils/jwt.go
Normal 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
62
utils/oss.go
Normal 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
14
utils/page.go
Normal 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
28
utils/time.go
Normal 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
20
utils/token.go
Normal 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
70
utils/wechat.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user