feat: add Z12/Z13 bind APIs, stock import, and sync docs
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.
This commit is contained in:
218
platform/internal/bindcodestore/store.go
Normal file
218
platform/internal/bindcodestore/store.go
Normal file
@@ -0,0 +1,218 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user