20260507213409
This commit is contained in:
1 parent
3070eac71b
commit
4c7f516a03
204 files changed
+3623
No files matched your search
@@ -0,0 +1,445 @@
|
||||
package handle
|
||||
|
||||
import (
|
||||
"allapp-go/internal/errors"
|
||||
"allapp-go/internal/httpx"
|
||||
"allapp-go/internal/middleware"
|
||||
"allapp-go/internal/types"
|
||||
"allapp-go/pkg/db"
|
||||
"allapp-go/pkg/jwtx"
|
||||
"allapp-go/pkg/requestx"
|
||||
"allapp-go/pkg/s3store"
|
||||
"allapp-go/pkg/uniqueid"
|
||||
"allapp-go/pkg/wechat"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// LoginQq ======================== QQ 登录 ========================
|
||||
func LoginQq(c fiber.Ctx) error {
|
||||
var req types.LoginQqReq
|
||||
vo := new(types.LoginResp)
|
||||
|
||||
if err := httpx.BindAndValidate(c, &req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return handleThirdLogin(c, req.Openid, req.Nickname, req.Avatar, 1, vo)
|
||||
}
|
||||
|
||||
// LoginWechat ======================== 微信登录 ========================
|
||||
func LoginWechat(c fiber.Ctx) error {
|
||||
var req types.LoginWechatReq
|
||||
vo := new(types.LoginResp)
|
||||
|
||||
if err := httpx.BindAndValidate(c, &req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
openid, token, err := wechat.GetWechatAccess(req.Code)
|
||||
if err != nil {
|
||||
log.Errorw("微信获取Token失败", "code", req.Code, "error", err)
|
||||
return httpx.Fail(c, "微信登录失败,请重试")
|
||||
}
|
||||
|
||||
nickname, avatar, err := wechat.GetWechatUserInfo(token, openid)
|
||||
if err != nil {
|
||||
log.Errorw("微信获取用户信息系失败", "openid", openid, "error", err)
|
||||
return httpx.Fail(c, "微信登录失败,请重试")
|
||||
}
|
||||
|
||||
return handleThirdLogin(c, openid, nickname, avatar, 0, vo)
|
||||
}
|
||||
|
||||
// ======================== 第三方登录主流程 ========================
|
||||
func handleThirdLogin(
|
||||
c fiber.Ctx,
|
||||
openid string,
|
||||
nickname string,
|
||||
avatar string,
|
||||
loginType int16,
|
||||
vo *types.LoginResp,
|
||||
) error {
|
||||
dbClient := db.New()
|
||||
|
||||
user, err := getUserByOpenID(dbClient, c, openid, loginType)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// 不存在 -> 注册
|
||||
if user == nil {
|
||||
return registerUser(dbClient, c, openid, nickname, avatar, loginType, vo)
|
||||
}
|
||||
|
||||
// ========= 校验状态 =========
|
||||
id := toInt64(user["id"])
|
||||
|
||||
// ========= 更新登录时间 =========
|
||||
if err := dbClient.Update(
|
||||
c.Context(),
|
||||
"b_user",
|
||||
"id",
|
||||
map[string]any{
|
||||
"id": id,
|
||||
"last_login_time": "NOW()",
|
||||
},
|
||||
); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// ========= token =========
|
||||
token, err := jwtx.CreateToken(c.Context(), jwtx.TokenData{
|
||||
UserID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
familyMembers, err := dbClient.LoadData(
|
||||
c.Context(),
|
||||
"b_family_member",
|
||||
"user_id = ?",
|
||||
"",
|
||||
[]string{"family_id"},
|
||||
[]any{id},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(familyMembers) == 0 {
|
||||
vo.FamilyId = 0
|
||||
} else {
|
||||
familyMember := familyMembers[0]
|
||||
|
||||
familyId := toInt64(familyMember["family_id"])
|
||||
|
||||
vo.FamilyId = familyId
|
||||
}
|
||||
|
||||
// ========= 返回 =========
|
||||
vo.Token = token
|
||||
vo.UserId = id
|
||||
vo.Nickname, _ = user["nickname"].(string)
|
||||
vo.Avatar, _ = user["avatar"].(string)
|
||||
|
||||
return httpx.OK(c, vo)
|
||||
}
|
||||
|
||||
// ======================== 注册 ========================
|
||||
func registerUser(
|
||||
dbClient *db.Client,
|
||||
c fiber.Ctx,
|
||||
openid, nickname, avatar string,
|
||||
loginType int16,
|
||||
vo *types.LoginResp,
|
||||
) error {
|
||||
userId := uniqueid.NextId()
|
||||
|
||||
defaultAvatar := "https://default-avatar-url.com/default.png"
|
||||
|
||||
if nickname == "" {
|
||||
nickname = fmt.Sprintf("用户_%d", userId)
|
||||
}
|
||||
|
||||
if avatar == "" {
|
||||
avatar = defaultAvatar
|
||||
} else {
|
||||
// 下载头像(流式)
|
||||
stream, size, contentType, err := requestx.GetFileStream(c.Context(), avatar)
|
||||
if err != nil {
|
||||
avatar = defaultAvatar
|
||||
} else {
|
||||
defer func() {
|
||||
_ = stream.Close()
|
||||
}()
|
||||
|
||||
// ✅ 只在这里转成可 seek
|
||||
data, err := io.ReadAll(stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(data)
|
||||
|
||||
// ✅ 从 avatar URL 获取扩展名(简化版)
|
||||
u, _ := url.Parse(avatar)
|
||||
ext := filepath.Ext(u.Path)
|
||||
if ext == "" {
|
||||
ext = ".jpg" // 兜底
|
||||
}
|
||||
|
||||
// 年月日 + uuid
|
||||
now := time.Now()
|
||||
key := fmt.Sprintf("%d/%02d/%02d/%s%s",
|
||||
now.Year(),
|
||||
now.Month(),
|
||||
now.Day(),
|
||||
uuid.NewString(),
|
||||
ext,
|
||||
)
|
||||
|
||||
// 上传到 S3
|
||||
if err := s3store.UploadToRustFS(
|
||||
c.Context(),
|
||||
reader,
|
||||
key,
|
||||
size,
|
||||
contentType,
|
||||
); err != nil {
|
||||
return errors.WithStack(err)
|
||||
} else {
|
||||
avatar = key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := dbClient.WithTx(c.Context(), func(tx *db.Client) error {
|
||||
|
||||
if err := tx.Insert(c.Context(), "b_user", "id", map[string]any{
|
||||
"id": userId,
|
||||
"nickname": nickname,
|
||||
"avatar": avatar,
|
||||
"create_time": "NOW()",
|
||||
"update_time": "NOW()",
|
||||
"last_login_time": "NOW()",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Insert(c.Context(), "b_user_oauth", "id", map[string]any{
|
||||
"id": uniqueid.NextId(),
|
||||
"user_id": userId,
|
||||
"type": loginType,
|
||||
"openid": openid,
|
||||
"create_time": "NOW()",
|
||||
"update_time": "NOW()",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
token, err := jwtx.CreateToken(c.Context(), jwtx.TokenData{
|
||||
UserID: userId,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
vo.Token = token
|
||||
vo.UserId = userId
|
||||
vo.Nickname = nickname
|
||||
vo.Avatar = avatar
|
||||
vo.FamilyId = 0
|
||||
|
||||
return httpx.OK(c, vo)
|
||||
}
|
||||
|
||||
// BindQq 绑定QQ
|
||||
func BindQq(c fiber.Ctx) error {
|
||||
var req types.LoginQqReq
|
||||
|
||||
if err := httpx.BindAndValidate(c, &req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
dbClient := db.New()
|
||||
|
||||
// 检查 QQ 是否已经被其他账号绑定
|
||||
var exist []map[string]any
|
||||
_, err := dbClient.LoadDataBySQL(
|
||||
c.Context(),
|
||||
"SELECT id FROM b_user_oauth WHERE openid = ? AND type = 1",
|
||||
[]any{req.Openid},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(exist) > 0 {
|
||||
return httpx.Fail(c, "该 QQ 已被其他账号绑定")
|
||||
}
|
||||
|
||||
userID, ok := middleware.GetUserID(c.Context())
|
||||
if !ok {
|
||||
return httpx.Unauthorized(c, "账号异常")
|
||||
}
|
||||
|
||||
var userBind []map[string]any
|
||||
_, err = dbClient.LoadDataBySQL(
|
||||
c.Context(),
|
||||
"SELECT id FROM b_user_oauth WHERE user_id = ? AND type = 1",
|
||||
[]any{userID},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(userBind) > 0 {
|
||||
return httpx.Fail(c, "该账号已绑定 QQ")
|
||||
}
|
||||
|
||||
err = dbClient.WithTx(c.Context(), func(tx *db.Client) error {
|
||||
err2 := tx.Insert(
|
||||
c.Context(),
|
||||
"b_user_oauth",
|
||||
"id",
|
||||
map[string]any{
|
||||
"id": uniqueid.NextId(),
|
||||
"user_id": userID,
|
||||
"type": 1, // QQ
|
||||
"openid": req.Openid,
|
||||
"create_time": time.Now(),
|
||||
"update_time": time.Now(),
|
||||
},
|
||||
)
|
||||
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return httpx.OK(c, "绑定成功")
|
||||
}
|
||||
|
||||
// BindWechat 绑定微信
|
||||
func BindWechat(c fiber.Ctx) error {
|
||||
var req types.LoginWechatReq
|
||||
|
||||
if err := httpx.BindAndValidate(c, &req); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
dbClient := db.New()
|
||||
|
||||
// 2️⃣ 检查 微信 是否已经被其他账号绑定
|
||||
var exist []map[string]any
|
||||
_, err := dbClient.LoadDataBySQL(
|
||||
c.Context(),
|
||||
"SELECT id FROM b_user_oauth WHERE openid = ? AND type = 0",
|
||||
[]any{req.Code},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(exist) > 0 {
|
||||
return httpx.Fail(c, "该 微信 已被其他账号绑定")
|
||||
}
|
||||
|
||||
userID, ok := middleware.GetUserID(c.Context())
|
||||
if !ok {
|
||||
return httpx.Unauthorized(c, "账号异常")
|
||||
}
|
||||
|
||||
var userBind []map[string]any
|
||||
_, err = dbClient.LoadDataBySQL(
|
||||
c.Context(),
|
||||
"SELECT id FROM b_user_oauth WHERE user_id = ? AND type = 0",
|
||||
[]any{userID},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(userBind) > 0 {
|
||||
return httpx.Fail(c, "该账号已绑定 微信")
|
||||
}
|
||||
|
||||
openid, _, err := wechat.GetWechatAccess(req.Code)
|
||||
if err != nil {
|
||||
log.Errorw("微信获取Token失败", "code", req.Code, "error", err)
|
||||
return httpx.Fail(c, "微信登录失败,请重试")
|
||||
}
|
||||
|
||||
err = dbClient.WithTx(c.Context(), func(tx *db.Client) error {
|
||||
err2 := tx.Insert(
|
||||
c.Context(),
|
||||
"b_user_oauth",
|
||||
"id",
|
||||
map[string]any{
|
||||
"id": uniqueid.NextId(),
|
||||
"user_id": userID,
|
||||
"type": 0, // 微信
|
||||
"openid": openid,
|
||||
"create_time": time.Now(),
|
||||
"update_time": time.Now(),
|
||||
},
|
||||
)
|
||||
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return httpx.OK(c, "绑定成功")
|
||||
}
|
||||
|
||||
// ======================== DB 查询封装(去重复 SQL) ========================
|
||||
func getUserByOpenID(dbClient *db.Client, c fiber.Ctx, openid string, loginType int16) (map[string]any, error) {
|
||||
users, err := dbClient.LoadDataBySQL(
|
||||
c.Context(),
|
||||
`SELECT u.*
|
||||
FROM b_user_oauth o
|
||||
JOIN b_user u ON u.id = o.user_id
|
||||
WHERE o.openid = $1 AND o.type = $2`,
|
||||
[]any{openid, loginType},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return users[0], nil
|
||||
}
|
||||
|
||||
// ======================== 类型工具(极简版) ========================
|
||||
func toInt64(v any) int64 {
|
||||
switch val := v.(type) {
|
||||
case int64:
|
||||
return val
|
||||
case int32:
|
||||
return int64(val)
|
||||
case int16:
|
||||
return int64(val)
|
||||
case int:
|
||||
return int64(val)
|
||||
case float64:
|
||||
return int64(val)
|
||||
default:
|
||||
fmt.Printf("unknown type: %T, value=%v\n", v, v)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user