feat(api): 接入微信登录并原生实现咨询域(ECR-049/050)

小程序可在 Go 上完成微信手机号登录、测评、预约和下单,不再反代 Java。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-09-15 00:26:29 +08:00
co-authored by Cursor
parent db4d118f9a
commit 7155b8b53a
45 changed files with 3013 additions and 27 deletions
+101
View File
@@ -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
}