Files
ai_site/platform/internal/logic/applogic/bind_flow.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

358 lines
11 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 applogic
import (
"encoding/json"
"fmt"
"strings"
"time"
"aijianzhan/platform/internal/agentstore"
"aijianzhan/platform/internal/authx"
"aijianzhan/platform/internal/bindcodestore"
"aijianzhan/platform/internal/dbsync"
"aijianzhan/platform/internal/userstore"
)
// AgentMe Z12b智能体自查绑定无需「管理智能体」
func (l *AuthLogic) AgentMe() (*agentstore.Account, error) {
if !authx.IsAgent(authx.Role(l.ctx)) {
return nil, fmt.Errorf("仅智能体可访问")
}
if l.svcCtx.Agents == nil {
return nil, fmt.Errorf("agent store unavailable")
}
tid := authx.TenantID(l.ctx)
aid := authx.UserID(l.ctx)
acc, err := l.svcCtx.Agents.Get(l.ctx, tid, aid)
if err != nil {
return nil, err
}
return acc, nil
}
type BindCodeCreateReq struct {
ChannelID string `json:"channel_id"`
OnlineDBID string `json:"online_db_id"`
DatabaseName string `json:"database_name"`
MaxUses int `json:"max_uses"`
ExpiresHours int `json:"expires_hours"` // 0=168h
Note string `json:"note"`
}
func (l *AuthLogic) CreateBindCode(req BindCodeCreateReq) (*bindcodestore.BindCode, error) {
if l.svcCtx.BindCodes == nil {
return nil, fmt.Errorf("bind code store unavailable")
}
tid := authx.TenantID(l.ctx)
if tid <= 0 {
return nil, fmt.Errorf("未加入公司")
}
channelID := strings.TrimSpace(req.ChannelID)
online := strings.TrimSpace(req.OnlineDBID)
dbName := strings.TrimSpace(req.DatabaseName)
if channelID == "" && l.svcCtx.DBSync != nil {
if ch, err := l.svcCtx.DBSync.Store().FindSystemDefaultChannel(tid); err == nil && ch != nil {
channelID = ch.ID
if online == "" {
online = dbsync.ResolveOnlineDBID("", ch.ID)
}
} else {
// 尝试创建默认同步通道
cfg := l.svcCtx.Config.DBSync
driver := dbsync.Driver(strings.TrimSpace(cfg.DefaultRemoteDriver))
if driver == "" {
driver = dbsync.DriverPostgres
}
saved, err := l.svcCtx.DBSync.Store().EnsureSystemDefaultChannel(dbsync.DefaultChannelOpts{
TenantID: tid,
RemoteDriver: driver,
RemoteDSN: strings.TrimSpace(cfg.DefaultRemoteDSN),
})
if err != nil {
return nil, fmt.Errorf("无默认同步通道:%w", err)
}
channelID = saved.ID
if online == "" {
online = dbsync.ResolveOnlineDBID("", saved.ID)
}
}
}
if channelID == "" {
return nil, fmt.Errorf("请先启用智能体以生成默认同步通道,或指定 channel_id")
}
if online == "" {
online = dbsync.ResolveOnlineDBID("", channelID)
}
exp := time.Duration(req.ExpiresHours) * time.Hour
return l.svcCtx.BindCodes.Create(l.ctx, tid, authx.UserID(l.ctx), bindcodestore.CreateInput{
ChannelID: channelID,
OnlineDBID: online,
DatabaseName: dbName,
MaxUses: req.MaxUses,
ExpiresIn: exp,
Note: req.Note,
})
}
func (l *AuthLogic) ListBindCodes() ([]bindcodestore.BindCode, error) {
if l.svcCtx.BindCodes == nil {
return nil, fmt.Errorf("bind code store unavailable")
}
tid := authx.TenantID(l.ctx)
return l.svcCtx.BindCodes.List(l.ctx, tid)
}
func (l *AuthLogic) RevokeBindCode(code string) error {
if l.svcCtx.BindCodes == nil {
return fmt.Errorf("bind code store unavailable")
}
return l.svcCtx.BindCodes.Revoke(l.ctx, authx.TenantID(l.ctx), code)
}
type BindCodeRedeemReq struct {
Code string `json:"code"`
HostKey string `json:"host_key"`
Name string `json:"name"` // 可选:无 agent 时注册用
}
type BindCodeRedeemResp struct {
OK bool `json:"ok"`
TenantID int64 `json:"tenant_id"`
AgentID int64 `json:"agent_id"`
ClientID string `json:"client_id,omitempty"`
ChannelID string `json:"channel_id"`
OnlineDBID string `json:"online_db_id"`
DatabaseName string `json:"database_name,omitempty"`
SyncBound bool `json:"sync_bound"`
Message string `json:"message,omitempty"`
}
func (l *AuthLogic) RedeemBindCode(req BindCodeRedeemReq) (*BindCodeRedeemResp, error) {
if l.svcCtx.BindCodes == nil || l.svcCtx.Agents == nil {
return nil, fmt.Errorf("bind service unavailable")
}
code := strings.TrimSpace(req.Code)
hostKey := strings.TrimSpace(req.HostKey)
if code == "" || hostKey == "" {
return nil, fmt.Errorf("code and host_key required")
}
bc, err := l.svcCtx.BindCodes.Redeem(l.ctx, code)
if err != nil {
return nil, err
}
acc, err := l.svcCtx.Agents.FindByHostKey(l.ctx, hostKey)
if err != nil {
name := strings.TrimSpace(req.Name)
if name == "" {
name = "离线终端"
}
created, secret, _, regErr := l.svcCtx.Agents.Register(l.ctx, bc.TenantID, name, hostKey)
if regErr != nil {
return nil, fmt.Errorf("register agent: %w", regErr)
}
_ = secret
acc = created
}
online := strings.TrimSpace(bc.OnlineDBID)
if online == "" {
online = dbsync.ResolveOnlineDBID("", bc.ChannelID)
}
dbName := strings.TrimSpace(bc.DatabaseName)
if dbName == "" {
dbName = fmt.Sprintf("agent_%d", acc.AgentID)
}
updated, err := l.svcCtx.Agents.AttachSyncBind(l.ctx, acc.AgentID, bc.TenantID, bc.ChannelID, online, dbName, true)
if err != nil {
return nil, err
}
_ = l.writeBindAudit("bind_code_redeem", updated.TenantID, updated.AgentID, map[string]any{
"code": bc.Code, "channel_id": bc.ChannelID, "online_db_id": online,
})
return &BindCodeRedeemResp{
OK: true,
TenantID: updated.TenantID,
AgentID: updated.AgentID,
ClientID: updated.ClientID,
ChannelID: updated.ChannelID,
OnlineDBID: updated.OnlineDBID,
DatabaseName: updated.DatabaseName,
SyncBound: true,
Message: "绑定成功",
}, nil
}
type PhoneLookupReq struct {
Phone string `json:"phone"`
}
type PhoneLookupResp struct {
Exists bool `json:"exists"`
TenantID int64 `json:"tenant_id,omitempty"`
TenantName string `json:"tenant_name,omitempty"`
MaskedName string `json:"masked_name,omitempty"`
NeedConfirm bool `json:"need_confirm"`
Message string `json:"message,omitempty"`
}
func maskDisplayName(name string) string {
name = strings.TrimSpace(name)
if name == "" {
return "***"
}
r := []rune(name)
if len(r) == 1 {
return string(r[0]) + "*"
}
if len(r) == 2 {
return string(r[0]) + "*"
}
return string(r[0]) + strings.Repeat("*", len(r)-2) + string(r[len(r)-1])
}
func (l *AuthLogic) PhoneLookup(req PhoneLookupReq) (*PhoneLookupResp, error) {
if l.svcCtx.Users == nil {
return nil, fmt.Errorf("user store unavailable")
}
phone, err := userstore.NormalizePhone(req.Phone)
if err != nil {
return &PhoneLookupResp{Exists: false, Message: "手机号格式不正确"}, nil
}
u, err := l.svcCtx.Users.GetByPhone(l.ctx, phone)
if err != nil || u == nil {
return &PhoneLookupResp{Exists: false, NeedConfirm: false, Message: "无此成员;请使用绑定码或联系管理员"}, nil
}
if u.TenantID <= 0 {
return &PhoneLookupResp{Exists: true, NeedConfirm: false, Message: "该手机号账号尚未加入公司"}, nil
}
tenantName := ""
if t, err := l.svcCtx.Users.GetTenant(l.ctx, u.TenantID); err == nil && t != nil {
tenantName = t.Name
}
return &PhoneLookupResp{
Exists: true,
TenantID: u.TenantID,
TenantName: tenantName,
MaskedName: maskDisplayName(u.DisplayName),
NeedConfirm: true,
Message: fmt.Sprintf("已找到账号「%s」所属「%s」是否绑定到本机", maskDisplayName(u.DisplayName), tenantName),
}, nil
}
type PhoneConfirmReq struct {
Phone string `json:"phone"`
HostKey string `json:"host_key"`
Name string `json:"name"`
Confirm bool `json:"confirm"` // 必须 true
LocalDBID string `json:"local_database_id"`
}
type PhoneConfirmResp struct {
OK bool `json:"ok"`
TenantID int64 `json:"tenant_id"`
AgentID int64 `json:"agent_id"`
ChannelID string `json:"channel_id"`
OnlineDBID string `json:"online_db_id"`
DatabaseName string `json:"database_name,omitempty"`
SyncBound bool `json:"sync_bound"`
Message string `json:"message,omitempty"`
}
func (l *AuthLogic) PhoneConfirm(req PhoneConfirmReq) (*PhoneConfirmResp, error) {
if !req.Confirm {
return nil, fmt.Errorf("须明确确认绑定confirm=true")
}
if l.svcCtx.Users == nil || l.svcCtx.Agents == nil || l.svcCtx.DBSync == nil {
return nil, fmt.Errorf("bind service unavailable")
}
phone, err := userstore.NormalizePhone(req.Phone)
if err != nil {
return nil, fmt.Errorf("手机号格式不正确")
}
hostKey := strings.TrimSpace(req.HostKey)
if hostKey == "" {
return nil, fmt.Errorf("host_key required")
}
u, err := l.svcCtx.Users.GetByPhone(l.ctx, phone)
if err != nil || u == nil {
return nil, fmt.Errorf("无此成员;请使用绑定码")
}
if u.TenantID <= 0 {
return nil, fmt.Errorf("该账号尚未加入公司")
}
cfg := l.svcCtx.Config.DBSync
driver := dbsync.Driver(strings.TrimSpace(cfg.DefaultRemoteDriver))
if driver == "" {
driver = dbsync.DriverPostgres
}
ch, err := l.svcCtx.DBSync.Store().EnsureSystemDefaultChannel(dbsync.DefaultChannelOpts{
TenantID: u.TenantID,
RemoteDriver: driver,
RemoteDSN: strings.TrimSpace(cfg.DefaultRemoteDSN),
})
if err != nil {
return nil, fmt.Errorf("ensure channel: %w", err)
}
acc, err := l.svcCtx.Agents.FindByHostKey(l.ctx, hostKey)
if err != nil {
name := strings.TrimSpace(req.Name)
if name == "" {
name = "离线终端 · " + maskDisplayName(u.DisplayName)
}
created, _, _, regErr := l.svcCtx.Agents.Register(l.ctx, u.TenantID, name, hostKey)
if regErr != nil {
return nil, regErr
}
acc = created
}
online := dbsync.ResolveOnlineDBID("", ch.ID)
// 个人落点:按用户隔离 online_db_id
online = fmt.Sprintf("%s_u%d", online, u.UserID)
dbName := fmt.Sprintf("%s", strings.TrimSpace(u.DisplayName))
if dbName == "" {
dbName = fmt.Sprintf("user_%d", u.UserID)
}
updated, err := l.svcCtx.Agents.AttachSyncBind(l.ctx, acc.AgentID, u.TenantID, ch.ID, online, dbName, true)
if err != nil {
return nil, err
}
localID := strings.TrimSpace(req.LocalDBID)
if localID == "" {
localID = "host:" + hostKey
}
_, _ = l.svcCtx.DBSync.Store().EnsureBinding(dbsync.Binding{
TenantID: u.TenantID,
UserID: u.UserID,
LocalDatabaseID: localID,
OnlineDBID: online,
ChannelID: ch.ID,
DatabaseName: dbName,
DisplayName: dbName,
Note: "phone-confirm",
})
_ = l.writeBindAudit("phone_confirm_bind", u.TenantID, updated.AgentID, map[string]any{
"phone": phone, "user_id": u.UserID, "channel_id": ch.ID, "online_db_id": online,
})
return &PhoneConfirmResp{
OK: true,
TenantID: updated.TenantID,
AgentID: updated.AgentID,
ChannelID: updated.ChannelID,
OnlineDBID: updated.OnlineDBID,
DatabaseName: updated.DatabaseName,
SyncBound: true,
Message: "绑定成功",
}, nil
}
func (l *AuthLogic) writeBindAudit(action string, tenantID, agentID int64, detail map[string]any) error {
if l.svcCtx.Audit == nil {
return nil
}
if detail == nil {
detail = map[string]any{}
}
detail["agent_id"] = agentID
b, _ := json.Marshal(detail)
return l.svcCtx.Audit.Log(l.ctx, tenantID, authx.UserID(l.ctx), action, string(b))
}