chore: seed migration objects batch 5
This commit is contained in:
@@ -2,247 +2,512 @@ package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hotgo/internal/library/imadmin"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"io"
|
||||
"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 NewSysImInvite() *sSysImInvite { return &sSysImInvite{} }
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImInvite(NewSysImInvite())
|
||||
}
|
||||
func init() { service.RegisterSysImInvite(NewSysImInvite()) }
|
||||
|
||||
type imInviteListRequest struct {
|
||||
Page int `json:"page,omitempty"`
|
||||
PerPage int `json:"per_page,omitempty"`
|
||||
InviteCode string `json:"invite_code,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
BatchID int64 `json:"batch_id,omitempty"`
|
||||
CreatedBy int64 `json:"created_by,omitempty"`
|
||||
CreatedAtStart string `json:"created_at_start,omitempty"`
|
||||
CreatedAtEnd string `json:"created_at_end,omitempty"`
|
||||
ExpiresAtStart string `json:"expires_at_start,omitempty"`
|
||||
ExpiresAtEnd string `json:"expires_at_end,omitempty"`
|
||||
}
|
||||
|
||||
type imInviteCreateRequest struct {
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
}
|
||||
|
||||
type imInviteBatchCreateRequest struct {
|
||||
Quantity int `json:"quantity"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
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"`
|
||||
BatchID *int64 `json:"batch_id"`
|
||||
Code string `json:"code"`
|
||||
Status string `json:"status"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
UsedBy *int64 `json:"used_by"`
|
||||
UsedAt *time.Time `json:"used_at"`
|
||||
RequestID string `json:"request_id"`
|
||||
CreatedBy int64 `json:"created_by"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
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"`
|
||||
Quantity int `json:"quantity"`
|
||||
ActiveCount int `json:"active_count"`
|
||||
UsedCount int `json:"used_count"`
|
||||
RevokedCount int `json:"revoked_count"`
|
||||
ExpiredCount int `json:"expired_count"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
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, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
scope, db, err := inviteQueryDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, 0, err
|
||||
}
|
||||
req, err := buildInviteListRequest(in)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, 0, err
|
||||
}
|
||||
var page imadmin.Page[imInviteCodeResponse]
|
||||
if err = imadmin.DefaultClient(ctx).ListCodes(ctx, scope, req, &page); err != nil {
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_list", "invite_code", 0, "failed", g.Map{"hasInviteCode": in.InviteCode != ""})
|
||||
return
|
||||
if !validInviteStatus(req.Status) {
|
||||
return nil, 0, gerror.New("邀请码状态不正确")
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_list", "invite_code", 0, "success", g.Map{"hasInviteCode": in.InviteCode != ""})
|
||||
list = make([]*sysin.ImInviteCodeModel, 0, len(page.Items))
|
||||
for i := range page.Items {
|
||||
list = append(list, convertInviteCode(page.Items[i]))
|
||||
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
|
||||
}
|
||||
totalCount = page.Total
|
||||
return
|
||||
_ = 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, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
scope, db, err := inviteDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
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)
|
||||
}
|
||||
req := imInviteCreateRequest{ExpiresAt: formatRFC3339(in.ExpiresAt), Remark: in.Remark, IdempotencyKey: in.IdempotencyKey}
|
||||
var upstream imInviteCodeResponse
|
||||
err = imadmin.DefaultClient(ctx).CreateCode(ctx, scope, req, &upstream)
|
||||
res = convertInviteCode(upstream)
|
||||
result := "success"
|
||||
if err != nil {
|
||||
result = "failed"
|
||||
} else {
|
||||
res = convertInviteCode(item)
|
||||
}
|
||||
targetId, _ := strconv.ParseInt(res.Id, 10, 64)
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_create", "invite_code", targetId, result, g.Map{"expiresAt": in.ExpiresAt, "remark": in.Remark})
|
||||
return
|
||||
_ = 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, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
scope, db, err := inviteDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
req := imInviteBatchCreateRequest{Quantity: in.Quantity, ExpiresAt: formatRFC3339(in.ExpiresAt), Remark: in.Remark, IdempotencyKey: in.IdempotencyKey}
|
||||
var upstream imInviteBatchResponse
|
||||
err = imadmin.DefaultClient(ctx).CreateBatch(ctx, scope, req, &upstream)
|
||||
res = convertInviteBatch(upstream)
|
||||
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)
|
||||
}
|
||||
targetId, _ := strconv.ParseInt(res.BatchId, 10, 64)
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_create", "invite_batch", targetId, result, g.Map{"quantity": in.Quantity, "expiresAt": in.ExpiresAt, "remark": in.Remark})
|
||||
return
|
||||
_ = 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, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
scope, db, err := inviteDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
batchId, err := sysin.ParseOptionalInt64(in.BatchId)
|
||||
id, err := sysin.ParseOptionalInt64(in.BatchId)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
var upstream imInviteBatchResponse
|
||||
err = imadmin.DefaultClient(ctx).GetBatch(ctx, scope, batchId, &upstream)
|
||||
res = &sysin.ImInviteBatchDetailModel{ImInviteBatchModel: *convertInviteBatch(upstream)}
|
||||
return
|
||||
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, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
scope, db, err := inviteDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
id, err := sysin.ParseOptionalInt64(in.Id)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
err = imadmin.DefaultClient(ctx).RevokeCode(ctx, scope, id)
|
||||
result := "success"
|
||||
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 {
|
||||
result = "failed"
|
||||
auditResult = "failed"
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_revoke", "invite_code", id, result, nil)
|
||||
return
|
||||
_ = 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, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
scope, db, err := inviteDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
batchId, err := sysin.ParseOptionalInt64(in.BatchId)
|
||||
id, err := sysin.ParseOptionalInt64(in.BatchId)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
var upstream struct {
|
||||
Batch *imInviteBatchResponse `json:"batch"`
|
||||
RevokedCount int `json:"revoked_count"`
|
||||
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)
|
||||
}
|
||||
err = imadmin.DefaultClient(ctx).RevokeBatch(ctx, scope, batchId, &upstream)
|
||||
if upstream.Batch != nil {
|
||||
res = convertInviteBatch(*upstream.Batch)
|
||||
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 = &sysin.ImInviteBatchModel{BatchId: strconv.FormatInt(batchId, 10), RevokedCount: upstream.RevokedCount}
|
||||
res = convertInviteBatch(batch)
|
||||
}
|
||||
result := "success"
|
||||
if err != nil {
|
||||
result = "failed"
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_revoke", "invite_batch", batchId, result, nil)
|
||||
return
|
||||
_ = 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
|
||||
return err
|
||||
}
|
||||
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
scope, db, err := inviteQueryDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
req, err := buildInviteListRequest(in)
|
||||
if err != nil {
|
||||
return
|
||||
if err != nil || !validInviteStatus(req.Status) {
|
||||
return gerror.New("导出条件不正确")
|
||||
}
|
||||
resp, err := imadmin.DefaultClient(ctx).ExportCodes(ctx, scope, req)
|
||||
if err != nil {
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_export", "invite_code", 0, "failed", g.Map{"hasInviteCode": in.InviteCode != ""})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
req.TenantID = scope.TenantId
|
||||
request := ghttp.RequestFromCtx(ctx)
|
||||
if request == nil {
|
||||
return gerror.New("请求上下文不存在")
|
||||
}
|
||||
filename := resp.Header.Get("Content-Disposition")
|
||||
if filename == "" {
|
||||
filename = `attachment; filename="registration-invite-codes.csv"`
|
||||
}
|
||||
request.Response.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
|
||||
request.Response.Header().Set("Content-Disposition", filename)
|
||||
if _, err = io.Copy(request.Response.Writer, resp.Body); err != nil {
|
||||
return gerror.Wrap(err, "转发导出文件失败")
|
||||
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 != ""})
|
||||
fmt.Fprint(request.Response.Writer, "")
|
||||
return
|
||||
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)
|
||||
batchID, err := sysin.ParseOptionalInt64(in.BatchId)
|
||||
if err != nil {
|
||||
return imInviteListRequest{}, err
|
||||
}
|
||||
@@ -250,32 +515,120 @@ func buildInviteListRequest(in *sysin.ImInviteListInp) (imInviteListRequest, err
|
||||
if err != nil {
|
||||
return imInviteListRequest{}, err
|
||||
}
|
||||
return imInviteListRequest{
|
||||
Page: in.Page,
|
||||
PerPage: in.PerPage,
|
||||
InviteCode: in.InviteCode,
|
||||
Status: in.Status,
|
||||
BatchID: batchId,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAtStart: formatRFC3339(in.CreatedAtStart),
|
||||
CreatedAtEnd: formatRFC3339(in.CreatedAtEnd),
|
||||
ExpiresAtStart: formatRFC3339(in.ExpiresAtStart),
|
||||
ExpiresAtEnd: formatRFC3339(in.ExpiresAtEnd),
|
||||
}, nil
|
||||
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 formatRFC3339(t *gtime.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
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 t.Time.UTC().Format(time.RFC3339)
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
func convertTime(t *time.Time) *gtime.Time {
|
||||
if t == 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 gtime.New(t)
|
||||
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 {
|
||||
@@ -285,31 +638,27 @@ func formatOptionalInt64(value *int64) string {
|
||||
return strconv.FormatInt(*value, 10)
|
||||
}
|
||||
|
||||
func convertInviteCode(item imInviteCodeResponse) *sysin.ImInviteCodeModel {
|
||||
return &sysin.ImInviteCodeModel{
|
||||
Id: strconv.FormatInt(item.ID, 10),
|
||||
BatchId: formatOptionalInt64(item.BatchID),
|
||||
Code: item.Code,
|
||||
Status: item.Status,
|
||||
ExpiresAt: convertTime(item.ExpiresAt),
|
||||
UsedBy: formatOptionalInt64(item.UsedBy),
|
||||
UsedAt: convertTime(item.UsedAt),
|
||||
RequestId: item.RequestID,
|
||||
CreatedBy: strconv.FormatInt(item.CreatedBy, 10),
|
||||
Remark: item.Remark,
|
||||
CreatedAt: convertTime(item.CreatedAt),
|
||||
func formatTime(value *time.Time) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return value.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func convertInviteBatch(item imInviteBatchResponse) *sysin.ImInviteBatchModel {
|
||||
return &sysin.ImInviteBatchModel{
|
||||
BatchId: strconv.FormatInt(item.BatchID, 10),
|
||||
Quantity: item.Quantity,
|
||||
ActiveCount: item.ActiveCount,
|
||||
UsedCount: item.UsedCount,
|
||||
RevokedCount: item.RevokedCount,
|
||||
ExpiredCount: item.ExpiredCount,
|
||||
ExpiresAt: convertTime(item.ExpiresAt),
|
||||
CreatedAt: convertTime(item.CreatedAt),
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user