Files
ai_site/platform/internal/bindcodestore/store.go
whm cb56e6847e 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.
2026-08-05 11:47:20 +08:00

219 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}