228 lines
6.6 KiB
Go
228 lines
6.6 KiB
Go
package handler
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"soul-api/internal/database"
|
||
"soul-api/internal/model"
|
||
"soul-api/internal/wechat"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// WechatLogin POST /api/wechat/login
|
||
func WechatLogin(c *gin.Context) {
|
||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||
}
|
||
|
||
// WechatPhoneLoginReq 手机号登录请求:code 为 wx.login() 的 code,phoneCode 为 getPhoneNumber 返回的 code
|
||
type WechatPhoneLoginReq struct {
|
||
Code string `json:"code"` // wx.login() 得到,用于 code2session 拿 openId
|
||
PhoneCode string `json:"phoneCode"` // getPhoneNumber 得到,用于换手机号
|
||
}
|
||
|
||
// WechatPhoneLogin POST /api/wechat/phone-login
|
||
// 请求体:code(必填)+ phoneCode(必填)。先 code2session 得到 openId,再 getPhoneNumber 得到手机号,创建/更新用户并返回与 /api/miniprogram/login 一致的数据结构。
|
||
func WechatPhoneLogin(c *gin.Context) {
|
||
var req WechatPhoneLoginReq
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 code 或 phoneCode"})
|
||
return
|
||
}
|
||
if req.Code == "" || req.PhoneCode == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请提供 code 与 phoneCode"})
|
||
return
|
||
}
|
||
|
||
openID, sessionKey, _, err := wechat.Code2Session(req.Code)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": fmt.Sprintf("微信登录失败: %v", err)})
|
||
return
|
||
}
|
||
phoneNumber, countryCode, err := wechat.GetPhoneNumber(req.PhoneCode)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": fmt.Sprintf("获取手机号失败: %v", err)})
|
||
return
|
||
}
|
||
|
||
db := database.DB()
|
||
var user model.User
|
||
result := db.Where("open_id = ?", openID).First(&user)
|
||
isNewUser := errors.Is(result.Error, gorm.ErrRecordNotFound)
|
||
if result.Error != nil && !isNewUser {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "查询用户失败"})
|
||
return
|
||
}
|
||
|
||
if isNewUser {
|
||
// 软删除后再次登录:旧记录 id=openid 仍存在,需用新 id 避免主键冲突
|
||
// openID 可能为空/短值(微信异常回包),统一走尾部安全截断避免 panic
|
||
openIDLast6 := tail(openID, 6)
|
||
openIDLast4 := tail(openID, 4)
|
||
if openIDLast6 == "" {
|
||
openIDLast6 = strings.ToUpper(randomSuffix())
|
||
}
|
||
if openIDLast4 == "" {
|
||
openIDLast4 = strings.ToUpper(randomSuffix())
|
||
}
|
||
referralCode := "SOUL" + strings.ToUpper(openIDLast6)
|
||
nickname := "微信用户" + openIDLast4
|
||
avatar := ""
|
||
hasFullBook := false
|
||
earnings := 0.0
|
||
pendingEarnings := 0.0
|
||
referralCount := 0
|
||
purchasedSections := "[]"
|
||
phone := phoneNumber
|
||
if countryCode != "" && countryCode != "86" {
|
||
phone = "+" + countryCode + " " + phoneNumber
|
||
}
|
||
created := false
|
||
for i := 0; i < 5; i++ {
|
||
userID := "user_" + randomSuffix()
|
||
curReferralCode := referralCode
|
||
if i > 0 {
|
||
suffix := strings.ToUpper(tail(randomSuffix(), 3))
|
||
curReferralCode = "SOUL" + strings.ToUpper(tail(openIDLast6+suffix, 6))
|
||
}
|
||
user = model.User{
|
||
ID: userID,
|
||
OpenID: &openID,
|
||
SessionKey: &sessionKey,
|
||
Nickname: &nickname,
|
||
Avatar: &avatar,
|
||
Phone: &phone,
|
||
ReferralCode: &curReferralCode,
|
||
HasFullBook: &hasFullBook,
|
||
PurchasedSections: &purchasedSections,
|
||
Earnings: &earnings,
|
||
PendingEarnings: &pendingEarnings,
|
||
ReferralCount: &referralCount,
|
||
}
|
||
if err := db.Create(&user).Error; err != nil {
|
||
// 并发场景:若另一请求已创建同 open_id,直接回查复用,避免 500
|
||
if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
|
||
var existed model.User
|
||
if qErr := db.Where("open_id = ?", openID).First(&existed).Error; qErr == nil {
|
||
user = existed
|
||
isNewUser = false
|
||
created = true
|
||
break
|
||
}
|
||
continue
|
||
}
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "创建用户失败"})
|
||
return
|
||
}
|
||
created = true
|
||
break
|
||
}
|
||
if !created {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "创建用户失败,请稍后重试"})
|
||
return
|
||
}
|
||
} else {
|
||
phone := phoneNumber
|
||
if countryCode != "" && countryCode != "86" {
|
||
phone = "+" + countryCode + " " + phoneNumber
|
||
}
|
||
if err := db.Model(&user).Updates(map[string]interface{}{"session_key": sessionKey, "phone": phone}).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "更新用户失败"})
|
||
return
|
||
}
|
||
user.Phone = &phone
|
||
}
|
||
|
||
var orderRows []struct {
|
||
ProductID string `gorm:"column:product_id"`
|
||
}
|
||
db.Raw(`
|
||
SELECT DISTINCT product_id FROM orders WHERE user_id = ? AND status = 'paid' AND product_type = 'section'
|
||
`, user.ID).Scan(&orderRows)
|
||
purchasedSections := []string{}
|
||
for _, row := range orderRows {
|
||
if row.ProductID != "" {
|
||
purchasedSections = append(purchasedSections, row.ProductID)
|
||
}
|
||
}
|
||
|
||
responseUser := map[string]interface{}{
|
||
"id": user.ID,
|
||
"openId": strVal(user.OpenID),
|
||
"nickname": strVal(user.Nickname),
|
||
"avatar": resolveAvatarURLWithRequest(c, strVal(user.Avatar)),
|
||
"phone": strVal(user.Phone),
|
||
"wechatId": strVal(user.WechatID),
|
||
"referralCode": strVal(user.ReferralCode),
|
||
"hasFullBook": boolVal(user.HasFullBook),
|
||
"purchasedSections": purchasedSections,
|
||
"earnings": floatVal(user.Earnings),
|
||
"pendingEarnings": floatVal(user.PendingEarnings),
|
||
"referralCount": intVal(user.ReferralCount),
|
||
"createdAt": user.CreatedAt,
|
||
}
|
||
// 与 /api/miniprogram/login 一致,避免手机号登录后 VIP 引导、权益展示滞后
|
||
if user.IsVip != nil {
|
||
responseUser["isVip"] = *user.IsVip
|
||
}
|
||
if user.VipExpireDate != nil {
|
||
responseUser["vipExpireDate"] = user.VipExpireDate.Format("2006-01-02")
|
||
}
|
||
tokenSeed := tail(openID, 8)
|
||
if tokenSeed == "" {
|
||
tokenSeed = tail(randomSuffix(), 8)
|
||
}
|
||
token := fmt.Sprintf("tk_%s_%d", tokenSeed, time.Now().Unix())
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": map[string]interface{}{
|
||
"openId": openID,
|
||
"user": responseUser,
|
||
"token": token,
|
||
},
|
||
"isNewUser": isNewUser,
|
||
})
|
||
}
|
||
|
||
func tail(s string, n int) string {
|
||
if n <= 0 {
|
||
return ""
|
||
}
|
||
r := []rune(strings.TrimSpace(s))
|
||
if len(r) <= n {
|
||
return string(r)
|
||
}
|
||
return string(r[len(r)-n:])
|
||
}
|
||
|
||
func strVal(p *string) string {
|
||
if p == nil {
|
||
return ""
|
||
}
|
||
return *p
|
||
}
|
||
func boolVal(p *bool) bool {
|
||
if p == nil {
|
||
return false
|
||
}
|
||
return *p
|
||
}
|
||
func floatVal(p *float64) float64 {
|
||
if p == nil {
|
||
return 0
|
||
}
|
||
return *p
|
||
}
|
||
func intVal(p *int) int {
|
||
if p == nil {
|
||
return 0
|
||
}
|
||
return *p
|
||
}
|