package handler import ( "net/http" "time" "github.com/gin-gonic/gin" "github.com/sundynix/sundynix-gateway/internal/store" ) // 租户成员「二维码邀请」(可复用团队码)。owner/admin 生成一张微信带参二维码发给团队, // 成员扫码关注/识别即自动入组(见 wechat_login.go 的 WxMPEvent inv_ 分支)。 // // 挂在 tenant_self.go 旁,作用于当前活跃租户;建/撤销须 ≥admin(路由 RequireTenantRole 把守)。 const ( inviteScenePrefix = "inv_" // 二维码 scene 前缀,区分邀请扫码 vs 登录扫码 inviteMaxDays = 30 // 微信临时二维码有效期上限 inviteDefaultDays = 7 ) // CreateTenantInvite: POST /api/v1/tenants/current/invites {role?, expires_days?, max_uses?} // 建邀请码并调微信出二维码,返回含二维码图 URL 的邀请记录。 func (h *Handler) CreateTenantInvite(c *gin.Context) { var b struct { Role string `json:"role"` ExpiresDays int `json:"expires_days"` MaxUses int `json:"max_uses"` } _ = c.ShouldBindJSON(&b) role := b.Role if role == "" { role = store.RoleMember } if role == store.RoleOwner || !store.ValidRole(role) { c.JSON(http.StatusBadRequest, gin.H{"error": "非法角色(二维码不能邀请为 owner)"}) return } days := b.ExpiresDays if days <= 0 { days = inviteDefaultDays } if days > inviteMaxDays { days = inviteMaxDays } ctx := c.Request.Context() cfg := h.loadWechatMP(ctx) if !cfg.Enabled() { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "微信未配置,无法生成邀请二维码"}) return } token, err := h.accessToken(ctx, cfg) if err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": "微信暂不可用"}) return } inv, err := h.db.CreateInvite(ctx, tenantID(c), userID(c), role, time.Now().AddDate(0, 0, days), b.MaxUses) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } qr, err := cfg.CreateSceneQR(ctx, token, inviteScenePrefix+inv.Token, days*86400) if err != nil { // 二维码没建成 → 撤销这条,别在列表里留一条无图的死码。 _ = h.db.RevokeInvite(ctx, tenantID(c), inv.ID) c.JSON(http.StatusBadGateway, gin.H{"error": "生成二维码失败:" + err.Error()}) return } if err := h.db.SetInviteQR(ctx, inv.ID, qr); err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } inv.QRImage = qr c.JSON(http.StatusOK, gin.H{"invite": inv}) } // ListTenantInvites: GET /api/v1/tenants/current/invites —— 本租户的有效邀请码(含已用/上限/有效期)。 func (h *Handler) ListTenantInvites(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"invites": h.db.ListInvites(c.Request.Context(), tenantID(c), true)}) } // RevokeTenantInvite: DELETE /api/v1/tenants/current/invites/:id —— 撤销一张邀请码。 func (h *Handler) RevokeTenantInvite(c *gin.Context) { if err := h.db.RevokeInvite(c.Request.Context(), tenantID(c), c.Param("id")); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "ok"}) }