Enable auto default sync channels on agent activate, bind-code/phone confirm flows, publish ALTER, and align admin/yuheng docs with the production bind path.
219 lines
5.0 KiB
Go
219 lines
5.0 KiB
Go
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")
|
||
}
|