package sys import ( "context" cryptorand "crypto/rand" "encoding/csv" "errors" "fmt" "hotgo/internal/library/imtenant" "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/internal/service" "net/url" "strconv" "strings" "time" mysqldriver "github.com/go-sql-driver/mysql" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gtime" ) const inviteGenerateRetries = 10 var errInviteBatchNotFound = errors.New("invite batch not found") type sSysImInvite struct{} func NewSysImInvite() *sSysImInvite { return &sSysImInvite{} } func init() { service.RegisterSysImInvite(NewSysImInvite()) } type imInviteListRequest struct { TenantID int64 InviteCode string Status string BatchID int64 CreatedBy int64 CreatedAtStart *time.Time CreatedAtEnd *time.Time ExpiresAtStart *time.Time ExpiresAtEnd *time.Time } type imInviteCodeResponse struct { ID int64 `json:"id"` TenantID int64 `json:"tenant_id"` TenantName string `json:"tenant_name"` BatchID *int64 `json:"batch_id"` Code string `json:"code"` Status string `json:"status"` ExpiresAt time.Time `json:"expires_at"` MaxUses int `json:"max_uses"` UsageCount int `json:"usage_count"` BoundUserID *int64 `json:"bound_user_id"` BoundUserLabel string `json:"bound_user_label"` LastUsedAt *time.Time `json:"last_used_at"` CreatedBy int64 `json:"created_by"` Remark string `json:"remark"` CreatedAt time.Time `json:"created_at"` } type imInviteBatchResponse struct { BatchID int64 `json:"batch_id"` TenantID int64 `json:"tenant_id"` Quantity int `json:"quantity"` IdempotencyKey string `json:"idempotency_key"` Remark string `json:"remark"` ActiveCount int `json:"active_count"` UsageCount int `json:"usage_count"` RevokedCount int `json:"revoked_count"` ExpiredCount int `json:"expired_count"` ExpiresAt time.Time `json:"expires_at"` CreatedAt time.Time `json:"created_at"` } func (s *sSysImInvite) List(ctx context.Context, in *sysin.ImInviteListInp) (list []*sysin.ImInviteCodeModel, totalCount int, err error) { if err = in.Filter(ctx); err != nil { return } scope, db, err := inviteQueryDB(ctx, in.TenantId) if err != nil { return nil, 0, err } req, err := buildInviteListRequest(in) if err != nil { return nil, 0, err } if !validInviteStatus(req.Status) { return nil, 0, gerror.New("邀请码状态不正确") } req.TenantID = scope.TenantId items, total, err := queryInviteCodes(ctx, db, req, in.PerPage, (in.Page-1)*in.PerPage) result := "success" if err != nil { result = "failed" } else { list = convertInviteCodes(items) totalCount = total } _ = writeImAdminAudit(ctx, scope.TenantId, "invite_list", "invite_code", 0, result, g.Map{"hasInviteCode": in.InviteCode != ""}) return list, totalCount, err } func (s *sSysImInvite) Create(ctx context.Context, in *sysin.ImInviteCreateInp) (res *sysin.ImInviteCodeModel, err error) { if err = in.Filter(ctx); err != nil { return } scope, db, err := inviteDB(ctx, in.TenantId) if err != nil { return nil, err } expiresAt := in.ExpiresAt.Time.UTC().Round(time.Millisecond) var item imInviteCodeResponse if key := strings.TrimSpace(in.IdempotencyKey); key != "" { batch, codes, createErr := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, 1, expiresAt, in.Remark, key) if createErr != nil { err = createErr } else if len(codes) > 0 { item = codes[0] } else { codes, _, err = queryInviteCodes(ctx, db, imInviteListRequest{TenantID: scope.TenantId, BatchID: batch.BatchID}, 1, 0) if err == nil && len(codes) > 0 { item = codes[0] } else if err == nil { err = gerror.New("邀请码生成结果不存在") } } } else { item, err = createSingleInvite(ctx, db, scope.TenantId, scope.OperatorId, expiresAt, in.Remark) } result := "success" if err != nil { result = "failed" } else { res = convertInviteCode(item) } _ = writeImAdminAudit(ctx, scope.TenantId, "invite_create", "invite_code", item.ID, result, g.Map{"expiresAt": in.ExpiresAt, "remark": in.Remark}) return res, err } func (s *sSysImInvite) BatchCreate(ctx context.Context, in *sysin.ImInviteBatchCreateInp) (res *sysin.ImInviteBatchModel, err error) { if err = in.Filter(ctx); err != nil { return } scope, db, err := inviteDB(ctx, in.TenantId) if err != nil { return nil, err } key := strings.TrimSpace(in.IdempotencyKey) batch, _, err := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, in.Quantity, in.ExpiresAt.Time.UTC().Round(time.Millisecond), in.Remark, key) result := "success" if err != nil { result = "failed" } else { res = convertInviteBatch(batch) } _ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_create", "invite_batch", batchID(batch), result, g.Map{"quantity": in.Quantity, "expiresAt": in.ExpiresAt, "remark": in.Remark}) return res, err } func (s *sSysImInvite) BatchView(ctx context.Context, in *sysin.ImInviteBatchViewInp) (res *sysin.ImInviteBatchDetailModel, err error) { scope, db, err := inviteDB(ctx, in.TenantId) if err != nil { return nil, err } id, err := sysin.ParseOptionalInt64(in.BatchId) if err != nil { return nil, err } batch, err := getInviteBatch(ctx, db, scope.TenantId, "b.id=?", id) if err != nil { return nil, err } codes, _, err := queryInviteCodes(ctx, db, imInviteListRequest{TenantID: scope.TenantId, BatchID: id}, batch.Quantity, 0) if err != nil { return nil, err } return &sysin.ImInviteBatchDetailModel{ImInviteBatchModel: *convertInviteBatch(batch), List: convertInviteCodes(codes)}, nil } func (s *sSysImInvite) Revoke(ctx context.Context, in *sysin.ImInviteRevokeInp) (err error) { scope, db, err := inviteDB(ctx, in.TenantId) if err != nil { return err } id, err := sysin.ParseOptionalInt64(in.Id) if err != nil { return err } now := time.Now().UTC().Round(time.Millisecond) result, err := db.Exec(ctx, `UPDATE im_registration_invite_codes SET status='revoked',updated_at=? WHERE tenant_id=? AND id=? AND status='active' AND expires_at>?`, now, scope.TenantId, id, now) if err == nil { if affected, _ := result.RowsAffected(); affected == 0 { err = gerror.New("有效邀请码不存在") } } auditResult := "success" if err != nil { auditResult = "failed" } _ = writeImAdminAudit(ctx, scope.TenantId, "invite_revoke", "invite_code", id, auditResult, nil) return err } func (s *sSysImInvite) BindUser(ctx context.Context, in *sysin.ImInviteBindUserInp) (err error) { if err = in.Filter(ctx); err != nil { return err } scope, db, err := inviteDB(ctx, in.TenantId) if err != nil { return err } id, err := sysin.ParseOptionalInt64(in.Id) if err != nil || id <= 0 { return gerror.New("邀请码ID不能为空") } tenant, err := resolveImAdminTenant(ctx, scope.TenantId) if err != nil { return err } label, err := inviteBoundUserLabel(ctx, tenant, in.UserId) if err != nil { return err } now := time.Now().UTC().Round(time.Millisecond) result, err := db.Exec(ctx, `UPDATE im_registration_invite_codes SET bound_user_id=?,bound_user_label=?,updated_at=? WHERE tenant_id=? AND id=? AND status='active' AND usage_count=0 AND (bound_user_id IS NULL OR bound_user_id=0)`, in.UserId, label, now, scope.TenantId, id) if err == nil { if affected, _ := result.RowsAffected(); affected == 0 { err = gerror.New("邀请码不存在、已使用、已绑定或不可绑定") } } auditResult := "success" if err != nil { auditResult = "failed" } _ = writeImAdminAudit(ctx, scope.TenantId, "invite_bind_user", "invite_code", id, auditResult, g.Map{"user_id": in.UserId}) if err != nil { return err } return nil } func (s *sSysImInvite) BatchRevoke(ctx context.Context, in *sysin.ImInviteBatchRevokeInp) (res *sysin.ImInviteBatchModel, err error) { scope, db, err := inviteDB(ctx, in.TenantId) if err != nil { return nil, err } id, err := sysin.ParseOptionalInt64(in.BatchId) if err != nil { return nil, err } if _, err = getInviteBatch(ctx, db, scope.TenantId, "b.id=?", id); err == nil { now := time.Now().UTC().Round(time.Millisecond) _, err = db.Exec(ctx, `UPDATE im_registration_invite_codes SET status='revoked',updated_at=? WHERE tenant_id=? AND batch_id=? AND status='active' AND expires_at>?`, now, scope.TenantId, id, now) } var batch *imInviteBatchResponse if err == nil { batch, err = getInviteBatch(ctx, db, scope.TenantId, "b.id=?", id) } auditResult := "success" if err != nil { auditResult = "failed" } else { res = convertInviteBatch(batch) } _ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_revoke", "invite_batch", id, auditResult, nil) return res, err } func (s *sSysImInvite) Export(ctx context.Context, in *sysin.ImInviteListInp) (err error) { if err = in.Filter(ctx); err != nil { return err } scope, db, err := inviteQueryDB(ctx, in.TenantId) if err != nil { return err } req, err := buildInviteListRequest(in) if err != nil || !validInviteStatus(req.Status) { return gerror.New("导出条件不正确") } req.TenantID = scope.TenantId request := ghttp.RequestFromCtx(ctx) if request == nil { return gerror.New("请求上下文不存在") } request.Response.Header().Set("Content-Type", "text/csv; charset=utf-8") request.Response.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=registration-invite-codes-%s.csv", time.Now().Format("20060102-150405"))) _, _ = request.Response.Writer.Write([]byte{0xef, 0xbb, 0xbf}) writer := csv.NewWriter(request.Response.Writer) _ = writer.Write([]string{"tenant_id", "tenant_name", "code", "status", "expires_at", "usage_count", "available_uses", "last_used_at", "created_by", "created_at", "remark"}) for offset := 0; ; offset += 100 { items, total, queryErr := queryInviteCodes(ctx, db, req, 100, offset) if queryErr != nil { return gerror.Wrap(queryErr, "导出邀请码失败") } for _, item := range items { _ = writer.Write([]string{strconv.FormatInt(item.TenantID, 10), item.TenantName, item.Code, item.Status, item.ExpiresAt.Format(time.RFC3339), strconv.Itoa(item.UsageCount), formatInviteAvailableUses(item.MaxUses, item.UsageCount), formatTime(item.LastUsedAt), strconv.FormatInt(item.CreatedBy, 10), item.CreatedAt.Format(time.RFC3339), item.Remark}) } writer.Flush() if writer.Error() != nil { return writer.Error() } if offset+len(items) >= total || len(items) == 0 { break } } _ = writeImAdminAudit(ctx, scope.TenantId, "invite_export", "invite_code", 0, "success", g.Map{"hasInviteCode": in.InviteCode != ""}) return nil } func inviteDB(ctx context.Context, selectedTenantID int64) (imAdminScope, gdb.DB, error) { scope, err := resolveImAdminScope(ctx, selectedTenantID) if err != nil { return scope, nil, err } return scope, imtenant.RegistryDB(), nil } func inviteQueryDB(ctx context.Context, selectedTenantID int64) (imAdminScope, gdb.DB, error) { scope, err := resolveImAdminScopeWithAll(ctx, selectedTenantID, true) if err != nil { return scope, nil, err } return scope, imtenant.RegistryDB(), nil } func createSingleInvite(ctx context.Context, db gdb.DB, tenantID, operatorID int64, expiresAt time.Time, remark string) (imInviteCodeResponse, error) { for attempt := 0; attempt < inviteGenerateRetries; attempt++ { id, err := imtenant.NextSnowflakeID(ctx) if err != nil { return imInviteCodeResponse{}, err } code, err := randomInviteCode() if err != nil { return imInviteCodeResponse{}, err } now := time.Now().UTC().Round(time.Millisecond) _, err = db.Exec(ctx, `INSERT INTO im_registration_invite_codes (id,tenant_id,batch_id,code,status,expires_at,max_uses,usage_count,last_used_at,created_by,remark,created_at,updated_at) VALUES(?,?,NULL,?,'active',?,1,0,NULL,?,?,?,?)`, id, tenantID, code, expiresAt, operatorID, remark, now, now) if err == nil { return imInviteCodeResponse{ID: id, TenantID: tenantID, Code: code, Status: "active", ExpiresAt: expiresAt, MaxUses: 1, CreatedBy: operatorID, Remark: remark, CreatedAt: now}, nil } if !isDuplicateError(err) { return imInviteCodeResponse{}, err } } return imInviteCodeResponse{}, gerror.New("无法生成不重复的邀请码,请重试") } func createInviteBatch(ctx context.Context, db gdb.DB, tenantID, operatorID int64, quantity int, expiresAt time.Time, remark, key string) (*imInviteBatchResponse, []imInviteCodeResponse, error) { if existing, err := getInviteBatch(ctx, db, tenantID, "b.idempotency_key=?", key); err == nil { if !sameInviteBatch(existing, quantity, expiresAt, remark) { return nil, nil, gerror.New("幂等键已被其他批量生成请求使用") } return existing, nil, nil } else if !errors.Is(err, errInviteBatchNotFound) { return nil, nil, err } for attempt := 0; attempt < inviteGenerateRetries; attempt++ { batchID, err := imtenant.NextSnowflakeID(ctx) if err != nil { return nil, nil, err } codes := make([]imInviteCodeResponse, quantity) seen := make(map[string]struct{}, quantity) for i := range codes { for { code, randomErr := randomInviteCode() if randomErr != nil { return nil, nil, randomErr } if _, ok := seen[code]; ok { continue } seen[code] = struct{}{} codes[i].Code = code break } codes[i].ID, err = imtenant.NextSnowflakeID(ctx) if err != nil { return nil, nil, err } codes[i].BatchID = &batchID } now := time.Now().UTC().Round(time.Millisecond) err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { if _, txErr := tx.Exec(`INSERT INTO im_registration_invite_batches (id,tenant_id,quantity,idempotency_key,expires_at,created_by,remark,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)`, batchID, tenantID, quantity, key, expiresAt, operatorID, remark, now, now); txErr != nil { return txErr } for i := range codes { if _, txErr := tx.Exec(`INSERT INTO im_registration_invite_codes (id,tenant_id,batch_id,code,status,expires_at,max_uses,usage_count,last_used_at,created_by,remark,created_at,updated_at) VALUES(?,?,?,?,'active',?,0,0,NULL,?,?,?,?)`, codes[i].ID, tenantID, batchID, codes[i].Code, expiresAt, operatorID, remark, now, now); txErr != nil { return txErr } codes[i].TenantID, codes[i].Status, codes[i].ExpiresAt, codes[i].CreatedBy, codes[i].Remark, codes[i].CreatedAt = tenantID, "active", expiresAt, operatorID, remark, now } return nil }) if err == nil { return &imInviteBatchResponse{BatchID: batchID, TenantID: tenantID, Quantity: quantity, IdempotencyKey: key, Remark: remark, ActiveCount: quantity, ExpiresAt: expiresAt, CreatedAt: now}, codes, nil } if !isDuplicateError(err) { return nil, nil, err } if existing, lookupErr := getInviteBatch(ctx, db, tenantID, "b.idempotency_key=?", key); lookupErr == nil { if !sameInviteBatch(existing, quantity, expiresAt, remark) { return nil, nil, gerror.New("幂等键已被其他批量生成请求使用") } return existing, nil, nil } else if !errors.Is(lookupErr, errInviteBatchNotFound) { return nil, nil, lookupErr } } return nil, nil, gerror.New("无法生成不重复的邀请码,请重试") } func queryInviteCodes(ctx context.Context, db gdb.DB, req imInviteListRequest, limit, offset int) ([]imInviteCodeResponse, int, error) { now := time.Now().UTC().Round(time.Millisecond) expireWhere := "status='active' AND expires_at<=?" expireArgs := []any{now, now} if req.TenantID > 0 { expireWhere = "tenant_id=? AND " + expireWhere expireArgs = []any{now, req.TenantID, now} } if _, err := db.Exec(ctx, `UPDATE im_registration_invite_codes SET status='expired',updated_at=? WHERE `+expireWhere, expireArgs...); err != nil { return nil, 0, err } where, args := inviteWhere(req) totalValue, err := db.GetValue(ctx, "SELECT COUNT(*) FROM im_registration_invite_codes c WHERE "+where, args...) if err != nil { return nil, 0, err } queryArgs := append(append([]any{}, args...), limit, offset) items := make([]imInviteCodeResponse, 0) err = db.Raw(`SELECT c.id,c.tenant_id,t.name tenant_name,c.batch_id,c.code,c.status,c.expires_at,c.max_uses,c.bound_user_id,c.bound_user_label,c.created_by,c.remark,c.created_at, c.usage_count,c.last_used_at FROM im_registration_invite_codes c LEFT JOIN im_tenant t ON t.id=c.tenant_id WHERE `+where+` ORDER BY c.created_at DESC,c.id DESC LIMIT ? OFFSET ?`, queryArgs...).Ctx(ctx).Scan(&items) return items, totalValue.Int(), err } func inviteWhere(req imInviteListRequest) (string, []any) { where := []string{"1=1"} args := make([]any, 0, 8) if req.TenantID > 0 { where, args = append(where, "c.tenant_id=?"), append(args, req.TenantID) } if req.InviteCode != "" { where, args = append(where, "c.code=?"), append(args, req.InviteCode) } if req.Status != "" { where, args = append(where, "c.status=?"), append(args, req.Status) } if req.BatchID > 0 { where, args = append(where, "c.batch_id=?"), append(args, req.BatchID) } if req.CreatedBy > 0 { where, args = append(where, "c.created_by=?"), append(args, req.CreatedBy) } if req.CreatedAtStart != nil { where, args = append(where, "c.created_at>=?"), append(args, *req.CreatedAtStart) } if req.CreatedAtEnd != nil { where, args = append(where, "c.created_at<=?"), append(args, *req.CreatedAtEnd) } if req.ExpiresAtStart != nil { where, args = append(where, "c.expires_at>=?"), append(args, *req.ExpiresAtStart) } if req.ExpiresAtEnd != nil { where, args = append(where, "c.expires_at<=?"), append(args, *req.ExpiresAtEnd) } return strings.Join(where, " AND "), args } func getInviteBatch(ctx context.Context, db gdb.DB, tenantID int64, where string, arg any) (*imInviteBatchResponse, error) { items := make([]imInviteBatchResponse, 0, 1) now := time.Now().UTC().Round(time.Millisecond) err := db.Raw(`SELECT b.id batch_id,b.tenant_id,b.quantity,b.idempotency_key,b.expires_at,b.remark,b.created_at, COALESCE(SUM(CASE WHEN c.status='active' AND c.expires_at>? THEN 1 ELSE 0 END),0) active_count, COALESCE(SUM(c.usage_count),0) usage_count, COALESCE(SUM(CASE WHEN (c.status='active' AND c.expires_at<=?) OR c.status='expired' THEN 1 ELSE 0 END),0) expired_count, COALESCE(SUM(CASE WHEN c.status='revoked' THEN 1 ELSE 0 END),0) revoked_count FROM im_registration_invite_batches b LEFT JOIN im_registration_invite_codes c ON c.tenant_id=b.tenant_id AND c.batch_id=b.id WHERE b.tenant_id=? AND `+where+` GROUP BY b.id LIMIT 1`, now, now, tenantID, arg).Ctx(ctx).Scan(&items) if err != nil { return nil, err } if len(items) == 0 { return nil, gerror.Wrap(errInviteBatchNotFound, "邀请码批次不存在") } return &items[0], nil } func buildInviteListRequest(in *sysin.ImInviteListInp) (imInviteListRequest, error) { batchID, err := sysin.ParseOptionalInt64(in.BatchId) if err != nil { return imInviteListRequest{}, err } createdBy, err := sysin.ParseOptionalInt64(in.CreatedBy) if err != nil { return imInviteListRequest{}, err } return imInviteListRequest{InviteCode: in.InviteCode, Status: in.Status, BatchID: batchID, CreatedBy: createdBy, CreatedAtStart: nativeTime(in.CreatedAtStart), CreatedAtEnd: nativeTime(in.CreatedAtEnd), ExpiresAtStart: nativeTime(in.ExpiresAtStart), ExpiresAtEnd: nativeTime(in.ExpiresAtEnd)}, nil } func randomInviteCode() (string, error) { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" result := make([]byte, 5) buffer := make([]byte, 10) for index := 0; index < len(result); { if _, err := cryptorand.Read(buffer); err != nil { return "", err } for _, value := range buffer { if value >= 208 { continue } result[index] = alphabet[int(value)%len(alphabet)] index++ if index == len(result) { break } } } return string(result), nil } func isDuplicateError(err error) bool { var mysqlErr *mysqldriver.MySQLError return errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 } func validInviteStatus(status string) bool { return status == "" || status == "active" || status == "expired" || status == "revoked" } func sameInviteBatch(batch *imInviteBatchResponse, quantity int, expiresAt time.Time, remark string) bool { return batch != nil && batch.Quantity == quantity && batch.ExpiresAt.UTC().Round(time.Millisecond).Equal(expiresAt.UTC().Round(time.Millisecond)) && batch.Remark == remark } func convertInviteCodes(items []imInviteCodeResponse) []*sysin.ImInviteCodeModel { list := make([]*sysin.ImInviteCodeModel, 0, len(items)) for _, item := range items { list = append(list, convertInviteCode(item)) } return list } func convertInviteCode(item imInviteCodeResponse) *sysin.ImInviteCodeModel { return &sysin.ImInviteCodeModel{Id: strconv.FormatInt(item.ID, 10), BatchId: formatOptionalInt64(item.BatchID), Code: item.Code, TenantId: strconv.FormatInt(item.TenantID, 10), TenantName: item.TenantName, Status: item.Status, ExpiresAt: gtime.New(item.ExpiresAt), MaxUses: item.MaxUses, UsageCount: item.UsageCount, BoundUserId: formatOptionalInt64(item.BoundUserID), BoundUserLabel: item.BoundUserLabel, LastUsedAt: convertTime(item.LastUsedAt), CreatedBy: strconv.FormatInt(item.CreatedBy, 10), Remark: item.Remark, CreatedAt: gtime.New(item.CreatedAt)} } func inviteBoundUserLabel(ctx context.Context, tenant *entity.ImTenant, userID int64) (string, error) { if tenant == nil || tenant.Code == "" || userID <= 0 { return "", gerror.New("绑定用户不合法") } values := addPageQuery(url.Values{}, 1, 1) values.Set("user_ids", strconv.FormatInt(userID, 10)) users, _, err := imAdminQueryList[sysin.ImUserListModel](ctx, tenant, "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/users", values, map[string]string{ "public": "profileJson", "trusted": "trustedJson", "access": "accessJson", }, ) if err != nil { return "", err } if len(users) == 0 { return "", gerror.New("绑定用户不存在") } user := users[0] if user.Nickname != "" { return fmt.Sprintf("%s(%s)", user.Nickname, user.UserId), nil } if user.Username != "" { return fmt.Sprintf("%s(%s)", user.Username, user.UserId), nil } if user.Phone != "" { return fmt.Sprintf("%s(%s)", user.Phone, user.UserId), nil } return user.UserId, nil } func convertInviteBatch(item *imInviteBatchResponse) *sysin.ImInviteBatchModel { if item == nil { return nil } return &sysin.ImInviteBatchModel{BatchId: strconv.FormatInt(item.BatchID, 10), Quantity: item.Quantity, ActiveCount: item.ActiveCount, TenantId: strconv.FormatInt(item.TenantID, 10), UsageCount: item.UsageCount, RevokedCount: item.RevokedCount, ExpiredCount: item.ExpiredCount, ExpiresAt: gtime.New(item.ExpiresAt), CreatedAt: gtime.New(item.CreatedAt)} } func nativeTime(value *gtime.Time) *time.Time { if value == nil { return nil } t := value.Time.UTC() return &t } func convertTime(value *time.Time) *gtime.Time { if value == nil { return nil } return gtime.New(value) } func formatOptionalInt64(value *int64) string { if value == nil || *value == 0 { return "" } return strconv.FormatInt(*value, 10) } func formatTime(value *time.Time) string { if value == nil { return "" } return value.Format(time.RFC3339) } func formatInviteAvailableUses(maxUses, usageCount int) string { if maxUses <= 0 { return "不限制" } available := maxUses - usageCount if available < 0 { available = 0 } return strconv.Itoa(available) } func batchID(batch *imInviteBatchResponse) int64 { if batch == nil { return 0 } return batch.BatchID }