package onboarding import ( "context" "encoding/json" "errors" "time" "data-terminal-cloud/internal/outbox" ) // Wait in this HTTP request, not in the binding transaction. The outbox worker // uses the same durable registration fence and may finish the work concurrently. func (s *Service) finishBLEVoice(ctx context.Context, d Device, in BLEBindInput) (out map[string]any, err error) { ctx, cancel := context.WithTimeout(ctx, 35*time.Second) defer cancel() defer func() { if errors.Is(err, context.DeadlineExceeded) { err = fail(504, "VOICE_REGISTRATION_TIMEOUT", "获取火山配置超时,绑定已保存,请使用原绑定令牌重试") } }() started := false for { fields, event, e := s.bleVoiceSnapshot(ctx, d, in) if e != nil { return nil, e } switch fields["voice_status"] { case "ready": v, ok := fields["voice"].(DeviceVoiceCredential) if !ok || v.InstanceID == "" || v.ProductKey == "" || v.DeviceName == "" || !validVoiceSecret(v.DeviceSecret) || !validRTCAppID(v.RTCAppID) { return nil, fail(502, "VOICE_CONFIG_INVALID", "火山配置不完整,绑定已保存,请联系平台处理") } return map[string]any{"status": "success", "instance_id": v.InstanceID, "product_key": v.ProductKey, "device_name": v.DeviceName, "device_secret": v.DeviceSecret, "RTCAppID": v.RTCAppID}, nil case "paused": return nil, fail(503, "VOICE_REGISTRATION_PAUSED", "语音服务暂不可用,绑定已保存,请稍后使用原绑定令牌重试") case "migration_required": return nil, fail(409, "VOICE_MIGRATION_REQUIRED", "绑定已保存,旧设备需要补交原火山凭据,请联系平台处理") case "reconcile_required": return nil, fail(502, "VOICE_REGISTRATION_FAILED", "绑定已保存,火山注册结果未确认,请联系平台核对") } if !started { started = true if e = s.processServerActivation(ctx, event); e != nil { if ctx.Err() != nil { return nil, ctx.Err() } return nil, fail(502, "VOICE_REGISTRATION_FAILED", "绑定已保存,获取火山配置失败,请稍后重试或联系平台处理") } continue } timer := time.NewTimer(250 * time.Millisecond) select { case <-ctx.Done(): timer.Stop() return nil, ctx.Err() case <-timer.C: } } } func (s *Service) bleVoiceSnapshot(ctx context.Context, authenticated Device, in BLEBindInput) (map[string]any, outbox.Event, error) { var event outbox.Event tx, err := s.pool.Begin(ctx) if err != nil { return nil, event, err } defer tx.Rollback(ctx) d, err := readDevice(ctx, tx, authenticated.ID, true) if err != nil { return nil, event, err } c, err := readBLEClaim(ctx, tx, in.Token, d.ID) if err != nil { return nil, event, err } result, err := bleResult(ctx, tx, d, c, s.now()) if err != nil { return nil, event, err } if !deviceAllowed(d) || result["status"] != "success" { return nil, event, fail(409, "BINDING_CHANGED", "设备已解绑或归属发生变化,请重新绑定") } if err = bleClaimPermission(ctx, tx, c); err != nil { return nil, event, err } fields, err := s.voiceFields(ctx, tx, d) if err != nil { return nil, event, err } j, err := readJob(ctx, tx, d.ID) if err != nil { return nil, event, err } event.Payload, err = json.Marshal(map[string]any{"device_id": d.ID, "activation_id": j.ID, "binding_generation": d.Generation}) if err != nil { return nil, event, err } return fields, event, tx.Commit(ctx) }