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:
whm
2026-08-05 11:47:20 +08:00
parent b04b180d30
commit cb56e6847e
31 changed files with 1882 additions and 91 deletions

View 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")
}