package bindcodestore import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "os" "path/filepath" "strings" "sync" "time" ) // BindCode 公司绑定码(Z13):兑换后挂默认同步落点。 type BindCode struct { Code string `json:"code"` TenantID int64 `json:"tenant_id"` ChannelID string `json:"channel_id,omitempty"` OnlineDBID string `json:"online_db_id,omitempty"` DatabaseName string `json:"database_name,omitempty"` MaxUses int `json:"max_uses"` UsedCount int `json:"used_count"` Revoked bool `json:"revoked"` ExpiresAt *time.Time `json:"expires_at,omitempty"` CreatedBy int64 `json:"created_by"` CreatedAt time.Time `json:"created_at"` Note string `json:"note,omitempty"` } type CreateInput struct { ChannelID string OnlineDBID string DatabaseName string MaxUses int // 默认 1 ExpiresIn time.Duration // 0=7天 Note string } type Store interface { Create(ctx context.Context, tenantID, createdBy int64, in CreateInput) (*BindCode, error) List(ctx context.Context, tenantID int64) ([]BindCode, error) Revoke(ctx context.Context, tenantID int64, code string) error Redeem(ctx context.Context, code string) (*BindCode, error) // 校验并 +1 used Get(ctx context.Context, code string) (*BindCode, error) } type FileStore struct { mu sync.Mutex path string } func NewFileStore(dir string) (*FileStore, error) { if err := os.MkdirAll(dir, 0o755); err != nil { return nil, err } return &FileStore{path: filepath.Join(dir, "bind_codes.json")}, nil } func (s *FileStore) read() ([]BindCode, error) { b, err := os.ReadFile(s.path) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } if len(b) == 0 { return nil, nil } var list []BindCode if err := json.Unmarshal(b, &list); err != nil { return nil, err } return list, nil } func (s *FileStore) write(list []BindCode) error { b, err := json.MarshalIndent(list, "", " ") if err != nil { return err } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, b, 0o644); err != nil { return err } return os.Rename(tmp, s.path) } func genCode() (string, error) { var buf [8]byte if _, err := rand.Read(buf[:]); err != nil { return "", err } return strings.ToUpper(hex.EncodeToString(buf[:])), nil } func (s *FileStore) Create(_ context.Context, tenantID, createdBy int64, in CreateInput) (*BindCode, error) { s.mu.Lock() defer s.mu.Unlock() list, err := s.read() if err != nil { return nil, err } code, err := genCode() if err != nil { return nil, err } maxUses := in.MaxUses if maxUses <= 0 { maxUses = 1 } expIn := in.ExpiresIn if expIn <= 0 { expIn = 7 * 24 * time.Hour } exp := time.Now().UTC().Add(expIn) bc := BindCode{ Code: code, TenantID: tenantID, ChannelID: strings.TrimSpace(in.ChannelID), OnlineDBID: strings.TrimSpace(in.OnlineDBID), DatabaseName: strings.TrimSpace(in.DatabaseName), MaxUses: maxUses, CreatedBy: createdBy, CreatedAt: time.Now().UTC(), ExpiresAt: &exp, Note: in.Note, } list = append(list, bc) if err := s.write(list); err != nil { return nil, err } return &bc, nil } func (s *FileStore) List(_ context.Context, tenantID int64) ([]BindCode, error) { s.mu.Lock() defer s.mu.Unlock() list, err := s.read() if err != nil { return nil, err } out := make([]BindCode, 0) for _, bc := range list { if bc.TenantID == tenantID { out = append(out, bc) } } return out, nil } func (s *FileStore) Revoke(_ context.Context, tenantID int64, code string) error { s.mu.Lock() defer s.mu.Unlock() code = strings.ToUpper(strings.TrimSpace(code)) list, err := s.read() if err != nil { return err } for i := range list { if list[i].Code == code && list[i].TenantID == tenantID { list[i].Revoked = true return s.write(list) } } return fmt.Errorf("bind code not found") } func (s *FileStore) Get(_ context.Context, code string) (*BindCode, error) { s.mu.Lock() defer s.mu.Unlock() code = strings.ToUpper(strings.TrimSpace(code)) list, err := s.read() if err != nil { return nil, err } for i := range list { if list[i].Code == code { cp := list[i] return &cp, nil } } return nil, fmt.Errorf("bind code not found") } func (s *FileStore) Redeem(_ context.Context, code string) (*BindCode, error) { s.mu.Lock() defer s.mu.Unlock() code = strings.ToUpper(strings.TrimSpace(code)) list, err := s.read() if err != nil { return nil, err } for i := range list { bc := &list[i] if bc.Code != code { continue } if bc.Revoked { return nil, fmt.Errorf("bind code revoked") } if bc.ExpiresAt != nil && time.Now().UTC().After(*bc.ExpiresAt) { return nil, fmt.Errorf("bind code expired") } if bc.UsedCount >= bc.MaxUses { return nil, fmt.Errorf("bind code exhausted") } bc.UsedCount++ if err := s.write(list); err != nil { return nil, err } cp := *bc return &cp, nil } return nil, fmt.Errorf("bind code not found") }