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