feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s

Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
+123
View File
@@ -0,0 +1,123 @@
package onboarding
import (
"context"
"errors"
"data-terminal-cloud/internal/identity"
"github.com/jackc/pgx/v5"
)
type BLEDeviceInput struct {
DeviceID string `json:"device_id"`
}
// BLEDeviceState does not authenticate the radio. It authorizes the signed-in
// account and returns the authoritative cloud binding state, never owner data.
func (s *Service) BLEDeviceState(ctx context.Context, p identity.Principal, in BLEDeviceInput) (map[string]any, error) {
a, err := readBLEDeviceAccess(ctx, s.pool, p, in.DeviceID, false)
if err != nil {
return nil, err
}
return map[string]any{"device_id": in.DeviceID, "id": a.Device.PublicID, "model": a.Device.Model, "bound": a.Bound}, nil
}
type bleDeviceAccess struct {
Device Device
UserID string
Bound bool
}
// Keep state checks and token issuance consistent. Errors describe the failed
// condition, never the identity or profile of another device owner.
func readBLEDeviceAccess(ctx context.Context, q queryer, p identity.Principal, factoryID string, lock bool) (bleDeviceAccess, error) {
var a bleDeviceAccess
if !factoryPattern.MatchString(factoryID) {
return a, fail(400, "DEVICE_ID_INVALID", "设备编号格式不正确,请重新搜索并选择设备")
}
d := &a.Device
var tenantStatus, userStatus, accountKind string
var knownAccount, familyBound, accountBound, accountCurrent, ownAccount, manageFamily bool
suffix := ""
if lock {
suffix = " FOR UPDATE OF d"
}
err := q.QueryRow(ctx, `SELECT d.id::text,d.public_id::text,d.tenant_id::text,d.factory_device_id,d.model,
d.status,d.activation_state,d.enrollment_mode,d.binding_generation,d.onboarding_released_at IS NOT NULL,t.status,
COALESCE(ua.id::text,''),COALESCE(ua.status,''),COALESCE(ua.account_kind,''),
EXISTS(SELECT 1 FROM user_account WHERE oidc_issuer=$2 AND oidc_subject=$3),
EXISTS(SELECT 1 FROM device_binding WHERE device_id=d.id AND unbound_at IS NULL),
EXISTS(SELECT 1 FROM device_account_binding WHERE device_id=d.id AND released_at IS NULL),
EXISTS(SELECT 1 FROM device_account_binding WHERE device_id=d.id AND released_at IS NULL AND generation=d.binding_generation),
EXISTS(SELECT 1 FROM device_account_binding WHERE device_id=d.id AND released_at IS NULL AND generation=d.binding_generation AND user_id=ua.id),
EXISTS(SELECT 1 FROM device_binding db JOIN household_member hm ON hm.household_id=db.household_id AND hm.tenant_id=db.tenant_id
JOIN household h ON h.id=hm.household_id AND h.status='active'
WHERE db.device_id=d.id AND db.unbound_at IS NULL AND hm.user_id=ua.id AND hm.status='active' AND hm.role IN ('owner','admin','editor'))
FROM device d JOIN tenant t ON t.id=d.tenant_id
LEFT JOIN user_account ua ON ua.tenant_id=d.tenant_id AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3
WHERE d.factory_device_id=$1`+suffix, factoryID, p.Issuer, p.Subject).Scan(
&d.ID, &d.PublicID, &d.TenantID, &d.FactoryID, &d.Model, &d.Status, &d.Lifecycle, &d.Mode, &d.Generation, &d.Released,
&tenantStatus, &a.UserID, &userStatus, &accountKind, &knownAccount, &familyBound, &accountBound, &accountCurrent, &ownAccount, &manageFamily)
if errors.Is(err, pgx.ErrNoRows) {
return a, fail(404, "DEVICE_NOT_REGISTERED", "平台未找到该设备的注册记录,请联系管理员登记设备")
}
if err != nil {
return a, err
}
if a.UserID == "" {
if knownAccount {
return a, fail(403, "DEVICE_TENANT_MISMATCH", "设备不属于当前账号所在组织,请切换到设备所属组织的账号")
}
return a, fail(403, "ACCOUNT_NOT_FOUND", "当前登录身份没有对应的平台账号,请重新登录或联系管理员")
}
if userStatus != "active" {
if userStatus == "deleted" {
return a, fail(403, "ACCOUNT_DELETED", "当前账号已删除,请使用其他有效账号登录")
}
return a, fail(403, "ACCOUNT_DISABLED", "当前账号已停用,请联系管理员恢复账号")
}
if accountKind != "customer" {
return a, fail(403, "CUSTOMER_REQUIRED", "当前登录的是管理账号,请切换到家庭账号添加设备")
}
if tenantStatus != "active" {
return a, fail(403, "TENANT_INACTIVE", "设备所属组织已停用,请联系平台管理员")
}
if d.Status == "revoked" || d.Lifecycle == "REVOKED" {
return a, fail(403, "DEVICE_REVOKED", "设备已被停用,无法添加或配网,请联系管理员")
}
if d.Lifecycle == "LOST" {
return a, fail(403, "DEVICE_LOST", "设备已被标记为丢失,请先联系管理员解除丢失状态")
}
if d.Status == "quarantined" {
return a, fail(403, "DEVICE_QUARANTINED", "设备已被隔离,暂时禁止添加或配网,请联系管理员检查设备")
}
if d.Mode != "physical_confirm" {
return a, fail(409, "DEVICE_ENROLLMENT_UNSUPPORTED", "该设备尚未启用蓝牙实体确认绑定,请联系管理员完成设备接入配置")
}
if !d.Released {
return a, fail(409, "DEVICE_NOT_RELEASED", "设备已登记但尚未放行,请联系管理员完成出厂放行")
}
if accountBound && !accountCurrent && !manageFamily {
return a, fail(409, "DEVICE_BINDING_STALE", "设备归属记录与当前设备状态不一致,请联系管理员处理")
}
if accountBound && !ownAccount && !manageFamily {
return a, fail(403, "DEVICE_OWNED_BY_OTHER_ACCOUNT", "设备已添加到其他账号,请使用原账号操作;如需更换账号,请先由原账号移除设备")
}
if familyBound && !manageFamily && !ownAccount {
return a, fail(403, "DEVICE_MANAGE_PERMISSION_REQUIRED", "设备已绑定家庭,当前账号没有该家庭的设备管理权限,请联系家庭管理员授权")
}
a.Bound = familyBound || accountCurrent
return a, nil
}
// DeviceBLEState is read-only: an empty BLE bind_token must never create a
// binding or bypass a cloud reset. Device authentication remains HMAC + HTTPS.
func (s *Service) DeviceBLEState(ctx context.Context, d Device) (map[string]any, error) {
if !deviceAllowed(d) {
return nil, errAccess
}
var bound bool
err := s.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_binding WHERE device_id=$1::uuid AND unbound_at IS NULL)
OR EXISTS(SELECT 1 FROM device_account_binding ab JOIN device d ON d.id=ab.device_id WHERE ab.device_id=$1::uuid AND ab.released_at IS NULL AND ab.generation=d.binding_generation)`, d.ID).Scan(&bound)
return map[string]any{"bound": bound}, err
}