feat(api): 接入微信登录并原生实现咨询域(ECR-049/050)
小程序可在 Go 上完成微信手机号登录、测评、预约和下单,不再反代 Java。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,9 +14,11 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/avatar"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/javabridge"
|
||||
nickgen "github.com/yuxingu/digital-psychology/apps/api/internal/nickname"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/textsafe"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/wechat"
|
||||
)
|
||||
|
||||
// Service handles register/login sessions.
|
||||
@@ -24,6 +26,9 @@ import (
|
||||
type Service struct {
|
||||
Repo *repository.AuthRepo
|
||||
AvatarDir string
|
||||
WeChat wechat.Client
|
||||
WeChatApp string
|
||||
Java *javabridge.Client
|
||||
}
|
||||
|
||||
// Me is the public account payload.
|
||||
@@ -159,10 +164,106 @@ func (s *Service) UpdateAvatar(ctx context.Context, userID uuid.UUID, fh *multip
|
||||
return s.Me(ctx, userID)
|
||||
}
|
||||
|
||||
// WeChatLogin signs in with mini-program js_code + phone encryptedData.
|
||||
func (s *Service) WeChatLogin(ctx context.Context, deviceUserID uuid.UUID, deviceKey, code, encryptedData, iv string) (*SessionResult, error) {
|
||||
if s.WeChat == nil {
|
||||
return nil, errors.New("微信登录未配置")
|
||||
}
|
||||
sess, err := s.WeChat.Code2Session(ctx, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
phone, err := wechat.DecryptPhone(sess.SessionKey, encryptedData, iv, s.WeChatApp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acc, err := s.Repo.GetByWxOpenid(ctx, sess.OpenID)
|
||||
if err == nil {
|
||||
if acc.Status != "active" {
|
||||
return nil, ErrAccountRestricted
|
||||
}
|
||||
_ = s.Repo.BindWeChat(ctx, acc.ID, sess.OpenID, sess.UnionID, phone)
|
||||
if deviceKey != "" {
|
||||
_ = s.Repo.BindDevice(ctx, deviceKey, acc.ID)
|
||||
}
|
||||
return s.finishWeChat(ctx, acc.ID, phone, acc.Nickname, false, sess, deviceKey)
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
byPhone, err := s.Repo.GetByPhone(ctx, phone)
|
||||
if err == nil {
|
||||
if byPhone.Status != "active" {
|
||||
return nil, ErrAccountRestricted
|
||||
}
|
||||
_ = s.Repo.BindWeChat(ctx, byPhone.ID, sess.OpenID, sess.UnionID, phone)
|
||||
if deviceKey != "" {
|
||||
_ = s.Repo.BindDevice(ctx, deviceKey, byPhone.ID)
|
||||
}
|
||||
nick := byPhone.Nickname
|
||||
if nick == "" {
|
||||
nick = nickgen.Random()
|
||||
_ = s.Repo.UpdateNickname(ctx, byPhone.ID, nick)
|
||||
}
|
||||
return s.finishWeChat(ctx, byPhone.ID, phone, nick, false, sess, deviceKey)
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte("wx:"+sess.OpenID), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nick := nickgen.Random()
|
||||
uid := deviceUserID
|
||||
cur, curErr := s.Repo.GetAccount(ctx, deviceUserID)
|
||||
if curErr == nil && cur.Phone == "" && cur.WxOpenID == "" {
|
||||
if cur.Status != "active" {
|
||||
return nil, ErrAccountRestricted
|
||||
}
|
||||
if err := s.Repo.RegisterOnUser(ctx, deviceUserID, phone, string(hash), nick); err != nil {
|
||||
return nil, errors.New("登录失败,请重试")
|
||||
}
|
||||
_ = s.Repo.BindWeChat(ctx, deviceUserID, sess.OpenID, sess.UnionID, phone)
|
||||
} else {
|
||||
uid, err = s.Repo.CreateUserWithWeChat(ctx, phone, string(hash), nick, sess.OpenID, sess.UnionID)
|
||||
if err != nil {
|
||||
return nil, errors.New("登录失败,请重试")
|
||||
}
|
||||
}
|
||||
if deviceKey != "" {
|
||||
_ = s.Repo.BindDevice(ctx, deviceKey, uid)
|
||||
}
|
||||
return s.finishWeChat(ctx, uid, phone, nick, true, sess, deviceKey)
|
||||
}
|
||||
|
||||
func (s *Service) finishWeChat(ctx context.Context, userID uuid.UUID, phone, nickname string, isNew bool, sess wechat.Session, _ string) (*SessionResult, error) {
|
||||
res, err := s.issue(ctx, userID, phone, nickname, isNew)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.Java != nil && s.Java.Enabled() {
|
||||
if tok, jerr := s.Java.IssueMiniApp(ctx, sess.OpenID, sess.UnionID, phone); jerr == nil && tok != nil {
|
||||
_ = s.Repo.SetSessionJavaToken(ctx, res.Token, tok.AccessToken)
|
||||
if tok.MemberID > 0 {
|
||||
_ = s.Repo.SetJavaPlatformUserID(ctx, userID, tok.MemberID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Logout revokes the current bearer session and unbinds the device from the account
|
||||
// so a refresh no longer resolves as logged-in via X-Device-Key (Spec R5).
|
||||
func (s *Service) Logout(ctx context.Context, token, deviceKey string) error {
|
||||
if token != "" {
|
||||
if s.Java != nil && s.Java.Enabled() {
|
||||
if jt, err := s.Repo.JavaTokenByGoToken(ctx, token); err == nil && jt != "" {
|
||||
s.Java.Logout(ctx, jt)
|
||||
}
|
||||
}
|
||||
if err := s.Repo.RevokeSession(ctx, token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user