|
@@ -24,6 +24,7 @@ import (
|
|
|
"yuansiwei/unified-backend/internal/identity/wechat"
|
|
"yuansiwei/unified-backend/internal/identity/wechat"
|
|
|
apperrors "yuansiwei/unified-backend/internal/infra/errors"
|
|
apperrors "yuansiwei/unified-backend/internal/infra/errors"
|
|
|
appredis "yuansiwei/unified-backend/internal/infra/redis"
|
|
appredis "yuansiwei/unified-backend/internal/infra/redis"
|
|
|
|
|
+ orgmodel "yuansiwei/unified-backend/internal/org/model"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
// WeChat 扫码状态机取值(state.Status 字段;前端据状态决定是否继续轮询)。
|
|
// WeChat 扫码状态机取值(state.Status 字段;前端据状态决定是否继续轮询)。
|
|
@@ -56,6 +57,7 @@ type WechatService struct {
|
|
|
states *appredis.WechatQrcodeStore
|
|
states *appredis.WechatQrcodeStore
|
|
|
tickets *appredis.LoginTicketStore
|
|
tickets *appredis.LoginTicketStore
|
|
|
identities repository.IdentityRepository
|
|
identities repository.IdentityRepository
|
|
|
|
|
+ memberships repository.MembershipRepository
|
|
|
campuses []WechatCampus
|
|
campuses []WechatCampus
|
|
|
callbackURL string
|
|
callbackURL string
|
|
|
stateTTL time.Duration
|
|
stateTTL time.Duration
|
|
@@ -75,6 +77,7 @@ type WechatConfig struct {
|
|
|
States *appredis.WechatQrcodeStore
|
|
States *appredis.WechatQrcodeStore
|
|
|
Tickets *appredis.LoginTicketStore
|
|
Tickets *appredis.LoginTicketStore
|
|
|
Identities repository.IdentityRepository
|
|
Identities repository.IdentityRepository
|
|
|
|
|
+ Memberships repository.MembershipRepository
|
|
|
Campuses []WechatCampus
|
|
Campuses []WechatCampus
|
|
|
CallbackURL string
|
|
CallbackURL string
|
|
|
// StateTTL / TicketTTL 可选:0/缺省用 5min。
|
|
// StateTTL / TicketTTL 可选:0/缺省用 5min。
|
|
@@ -101,6 +104,9 @@ func NewWechatService(cfg WechatConfig) (*WechatService, error) {
|
|
|
if cfg.Identities == nil {
|
|
if cfg.Identities == nil {
|
|
|
return nil, apperrors.New(apperrors.CodeInternal, "wechat: 登录身份仓储未注入")
|
|
return nil, apperrors.New(apperrors.CodeInternal, "wechat: 登录身份仓储未注入")
|
|
|
}
|
|
}
|
|
|
|
|
+ if cfg.Memberships == nil {
|
|
|
|
|
+ return nil, apperrors.New(apperrors.CodeInternal, "wechat: 成员关系仓储未注入")
|
|
|
|
|
+ }
|
|
|
if len(cfg.Campuses) == 0 {
|
|
if len(cfg.Campuses) == 0 {
|
|
|
return nil, apperrors.New(apperrors.CodeInternal, "wechat: 无 per-campus 微信应用配置(Campuses 至少 1 条)")
|
|
return nil, apperrors.New(apperrors.CodeInternal, "wechat: 无 per-campus 微信应用配置(Campuses 至少 1 条)")
|
|
|
}
|
|
}
|
|
@@ -120,6 +126,7 @@ func NewWechatService(cfg WechatConfig) (*WechatService, error) {
|
|
|
states: cfg.States,
|
|
states: cfg.States,
|
|
|
tickets: cfg.Tickets,
|
|
tickets: cfg.Tickets,
|
|
|
identities: cfg.Identities,
|
|
identities: cfg.Identities,
|
|
|
|
|
+ memberships: cfg.Memberships,
|
|
|
campuses: cfg.Campuses,
|
|
campuses: cfg.Campuses,
|
|
|
callbackURL: cfg.CallbackURL,
|
|
callbackURL: cfg.CallbackURL,
|
|
|
stateTTL: stTTL,
|
|
stateTTL: stTTL,
|
|
@@ -174,6 +181,9 @@ type StatusResult struct {
|
|
|
LoginTicket string `json:"loginTicket,omitempty"`
|
|
LoginTicket string `json:"loginTicket,omitempty"`
|
|
|
// UserID success 时绑定的用户 ULID(前端可据此展示"已登录 X")。
|
|
// UserID success 时绑定的用户 ULID(前端可据此展示"已登录 X")。
|
|
|
UserID string `json:"userId,omitempty"`
|
|
UserID string `json:"userId,omitempty"`
|
|
|
|
|
+ // Memberships success 时绑定用户的可选作用域列表(机构/校区/角色;前端据此走与账密登录
|
|
|
|
|
+ // 同一套 ② issue 分流:单条自动②、多条进作用域选择页;空 = 该用户暂无机构归属)。
|
|
|
|
|
+ Memberships []MembershipView `json:"memberships,omitempty"`
|
|
|
// Expired true = state 不存在/已过期(前端停止轮询,提示"二维码已失效,请刷新")。
|
|
// Expired true = state 不存在/已过期(前端停止轮询,提示"二维码已失效,请刷新")。
|
|
|
// 与 Status 互斥:Expired=true 时 Status 为空串(调用方按 Expired 优先判断)。
|
|
// 与 Status 互斥:Expired=true 时 Status 为空串(调用方按 Expired 优先判断)。
|
|
|
Expired bool `json:"expired,omitempty"`
|
|
Expired bool `json:"expired,omitempty"`
|
|
@@ -197,6 +207,7 @@ func (s *WechatService) Status(ctx context.Context, state string) (*StatusResult
|
|
|
if st.Status == WechatQrcodeSuccess {
|
|
if st.Status == WechatQrcodeSuccess {
|
|
|
res.LoginTicket = st.Ticket
|
|
res.LoginTicket = st.Ticket
|
|
|
res.UserID = st.UserID
|
|
res.UserID = st.UserID
|
|
|
|
|
+ res.Memberships = toMembershipViewsFromState(st.Memberships)
|
|
|
}
|
|
}
|
|
|
return res, nil
|
|
return res, nil
|
|
|
}
|
|
}
|
|
@@ -255,7 +266,13 @@ func (s *WechatService) Callback(ctx context.Context, code, state string) (*Stat
|
|
|
return nil, err // *AppError(ErrDBError)
|
|
return nil, err // *AppError(ErrDBError)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 已绑定:发 loginTicket(复用 ② 机制)写回 state=success
|
|
|
|
|
|
|
+ // 取作用域列表(供前端 ② issue 分流:单条自动②、多条进作用域选择页;与账密登录同源)
|
|
|
|
|
+ mergs, merr := s.memberships.ListByCanonicalUserID(ctx, it.CanonicalUserID)
|
|
|
|
|
+ if merr != nil {
|
|
|
|
|
+ return nil, merr // *AppError(ErrDBError)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 已绑定:发 loginTicket(复用 ② 机制)写回 state=success(载荷含 memberships,两条读法一致)
|
|
|
ticket, terr := s.newTicket()
|
|
ticket, terr := s.newTicket()
|
|
|
if terr != nil {
|
|
if terr != nil {
|
|
|
return nil, apperrors.WithCause(apperrors.ErrInternal, terr)
|
|
return nil, apperrors.WithCause(apperrors.ErrInternal, terr)
|
|
@@ -264,15 +281,20 @@ func (s *WechatService) Callback(ctx context.Context, code, state string) (*Stat
|
|
|
return nil, setErr // *AppError(ErrDBError)——票据写失败阻断(不放行半套)
|
|
return nil, setErr // *AppError(ErrDBError)——票据写失败阻断(不放行半套)
|
|
|
}
|
|
}
|
|
|
final := appredis.WechatState{
|
|
final := appredis.WechatState{
|
|
|
- Status: WechatQrcodeSuccess,
|
|
|
|
|
- CampusID: st.CampusID,
|
|
|
|
|
- Ticket: ticket,
|
|
|
|
|
- UserID: it.CanonicalUserID,
|
|
|
|
|
|
|
+ Status: WechatQrcodeSuccess,
|
|
|
|
|
+ CampusID: st.CampusID,
|
|
|
|
|
+ Ticket: ticket,
|
|
|
|
|
+ UserID: it.CanonicalUserID,
|
|
|
|
|
+ Memberships: toMembershipInfos(mergs),
|
|
|
}
|
|
}
|
|
|
if setErr := s.states.SetState(ctx, state, final, s.stateTTL); setErr != nil {
|
|
if setErr := s.states.SetState(ctx, state, final, s.stateTTL); setErr != nil {
|
|
|
return nil, setErr
|
|
return nil, setErr
|
|
|
}
|
|
}
|
|
|
- return &StatusResult{Status: WechatQrcodeSuccess, CampusID: st.CampusID, LoginTicket: ticket, UserID: it.CanonicalUserID}, nil
|
|
|
|
|
|
|
+ return &StatusResult{
|
|
|
|
|
+ Status: WechatQrcodeSuccess, CampusID: st.CampusID,
|
|
|
|
|
+ LoginTicket: ticket, UserID: it.CanonicalUserID,
|
|
|
|
|
+ Memberships: toMembershipViews(mergs), // org 域来源(auth_service.go 的 toMembershipViews)
|
|
|
|
|
+ }, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ---- 私有工具 ----
|
|
// ---- 私有工具 ----
|
|
@@ -304,3 +326,36 @@ func (s *WechatService) newTicket() (string, error) {
|
|
|
}
|
|
}
|
|
|
return hex.EncodeToString(buf), nil
|
|
return hex.EncodeToString(buf), nil
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// toMembershipInfos org 域成员关系切片 → redis 状态机载荷切片(callback 落终态时写入 state)。
|
|
|
|
|
+// 空切片保持非 nil(JSON 序列化为 [] 而非 null;无机构归属时前端据此提示"联系管理员")。
|
|
|
|
|
+func toMembershipInfos(list []orgmodel.Membership) []appredis.MembershipInfo {
|
|
|
|
|
+ out := make([]appredis.MembershipInfo, 0, len(list))
|
|
|
|
|
+ for _, m := range list {
|
|
|
|
|
+ out = append(out, appredis.MembershipInfo{
|
|
|
|
|
+ MembershipID: m.MembershipID,
|
|
|
|
|
+ InstitutionID: m.InstitutionID,
|
|
|
|
|
+ CampusID: m.CampusID,
|
|
|
|
|
+ Role: m.Role,
|
|
|
|
|
+ Status: m.Status,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ return out
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// toMembershipViewsFromState redis 状态机载荷切片 → 对外 MembershipView(Status 轮询/callback 回显用)。
|
|
|
|
|
+// 与 AuthService.toMembershipViews(org 域来源,同名异签名)区分:本函数源类型为 redis 载荷,
|
|
|
|
|
+// 避免同包内重名冲突(auth_service.go 已有 toMembershipViews(orgmodel.Membership))。
|
|
|
|
|
+func toMembershipViewsFromState(list []appredis.MembershipInfo) []MembershipView {
|
|
|
|
|
+ out := make([]MembershipView, 0, len(list))
|
|
|
|
|
+ for _, m := range list {
|
|
|
|
|
+ out = append(out, MembershipView{
|
|
|
|
|
+ MembershipID: m.MembershipID,
|
|
|
|
|
+ InstitutionID: m.InstitutionID,
|
|
|
|
|
+ CampusID: m.CampusID,
|
|
|
|
|
+ Role: m.Role,
|
|
|
|
|
+ Status: m.Status,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ return out
|
|
|
|
|
+}
|