package user import ( "bytes" "context" "encoding/base64" "errors" "image/png" "bindbox-game/internal/pkg/douyin" "bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/repository/mysql/model" randomname "github.com/DanPlayer/randomname" identicon "github.com/issue9/identicon/v2" "go.uber.org/zap" "gorm.io/gorm" "gorm.io/gorm/clause" ) // LoginDouyinInput 抖音登录输入参数 type LoginDouyinInput struct { Code string // tt.login 获取的 code AnonymousCode string // 匿名登录 code Nickname string AvatarURL string InviteCode string ChannelCode string } // LoginDouyinOutput 抖音登录输出结果 type LoginDouyinOutput struct { User *model.Users IsNewUser bool InviterID int64 } // LoginDouyin 抖音小程序登录 func (s *service) LoginDouyin(ctx context.Context, in LoginDouyinInput) (*LoginDouyinOutput, error) { // 1. 调用抖音 code2session 获取 openid if in.Code == "" && in.AnonymousCode == "" { return nil, errors.New("code 或 anonymous_code 不能为空") } resp, err := douyin.Code2Session(ctx, in.Code, in.AnonymousCode) if err != nil { s.logger.Error("抖音 code2session 失败", zap.Error(err)) return nil, err } openID := resp.Data.OpenID if openID == "" { openID = resp.Data.AnonymousOpenID } if openID == "" { return nil, errors.New("获取抖音 openid 失败") } unionID := resp.Data.UnionID var u *model.Users // 事务处理:创建/更新用户 + 处理邀请 var isNewUser bool var inviterID int64 err = s.writeDB.Transaction(func(tx *dao.Query) error { var err error // 2. 先通过 douyin_id 查找用户 u, err = tx.Users.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}). Where(tx.Users.DouyinID.Eq(openID)).First() if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } // 3. 如果有 unionid,尝试通过 unionid 关联到已有的微信用户 if u == nil && unionID != "" { u, err = tx.Users.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}). Where(tx.Users.Unionid.Eq(unionID)).First() if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } // 找到微信用户,绑定抖音ID if u != nil && u.DouyinID == "" { _, _ = tx.Users.WithContext(ctx).Where(tx.Users.ID.Eq(u.ID)). Updates(map[string]any{"douyin_id": openID}) u.DouyinID = openID } } // 查找渠道ID var channelID int64 if in.ChannelCode != "" { ch, _ := s.readDB.Channels.WithContext(ctx).Where(s.readDB.Channels.Code.Eq(in.ChannelCode)).First() if ch != nil { channelID = ch.ID } } // 4. 如果用户不存在,创建新用户 isNewUser = false if u == nil { isNewUser = true code := s.generateInviteCode(ctx) nickname := in.Nickname if nickname == "" { nickname = randomname.GenerateName() } avatar := in.AvatarURL if avatar == "" { seed := openID if seed == "" { seed = nickname } img := identicon.S2(128).Make([]byte(seed)) var buf bytes.Buffer _ = png.Encode(&buf, img) avatar = "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()) } u = &model.Users{ Nickname: nickname, Avatar: avatar, DouyinID: openID, Unionid: unionID, InviteCode: code, Status: 1, ChannelID: channelID, } if err := tx.Users.WithContext(ctx).Create(u); err != nil { return err } } else { // 更新用户信息 set := map[string]any{} if in.Nickname != "" && u.Nickname == "" { set["nickname"] = in.Nickname } if in.AvatarURL != "" && u.Avatar == "" { set["avatar"] = in.AvatarURL } if unionID != "" && u.Unionid == "" { set["unionid"] = unionID } if channelID > 0 && u.ChannelID == 0 { set["channel_id"] = channelID } if len(set) > 0 { if _, err := tx.Users.WithContext(ctx).Where(tx.Users.ID.Eq(u.ID)).Updates(set); err != nil { return err } u, _ = tx.Users.WithContext(ctx).Where(tx.Users.ID.Eq(u.ID)).First() } } // 5. 处理邀请逻辑 // 只有在真正创建新用户记录时才发放邀请奖励,防止多账号切换重复刷奖励 if in.InviteCode != "" && isNewUser { // 查询邀请人 var inviter model.Users // First() 返回 (result, error) inviterResult, err := tx.Users.WithContext(ctx).Where(tx.Users.InviteCode.Eq(in.InviteCode)).First() if err == nil && inviterResult != nil { inviter = *inviterResult // 创建邀请记录 invite := &model.UserInvites{ InviteeID: u.ID, // UserID -> InviteeID InviterID: inviter.ID, InviteCode: in.InviteCode, // Status: 1, // Removed } if err := tx.UserInvites.WithContext(ctx).Create(invite); err != nil { return err } // 更新被邀请人的邀请人ID // UpdateColumn for single column update if _, err := tx.Users.WithContext(ctx).Where(tx.Users.ID.Eq(u.ID)). UpdateColumn(tx.Users.InviterID, inviter.ID); err != nil { return err } // 返回邀请人ID,以便外层触发任务中心逻辑 inviterID = inviter.ID s.logger.Info("抖音登录邀请关系建立成功", zap.Int64("user_id", u.ID), zap.Int64("inviter_id", inviter.ID)) } } return nil }) if err != nil { return nil, err } return &LoginDouyinOutput{ User: u, IsNewUser: isNewUser, InviterID: inviterID, }, nil }