feat(operations): support canonical source providers
This commit is contained in:
@@ -93,9 +93,9 @@ LEFT JOIN vehicle_realtime_snapshot s ON s.vin = v.vin
|
||||
LEFT JOIN vehicle_realtime_location l ON l.vin = s.vin AND l.protocol = s.protocol
|
||||
LEFT JOIN vehicle_realtime_location_source ls ON ls.vin = l.vin AND ls.protocol = l.protocol AND ls.source_key = l.source_key
|
||||
LEFT JOIN platform_vehicle_source_provider provider
|
||||
ON BINARY provider.vin = BINARY l.vin
|
||||
AND BINARY provider.protocol = BINARY l.protocol
|
||||
AND BINARY provider.source_key = BINARY l.source_key
|
||||
ON BINARY provider.vin = BINARY s.vin
|
||||
AND BINARY provider.protocol = BINARY s.protocol
|
||||
AND BINARY provider.source_key = BINARY COALESCE(NULLIF(l.source_key, ''), CONCAT(s.protocol, ':canonical'))
|
||||
LEFT JOIN (
|
||||
SELECT vin, protocol, MAX(daily_mileage_km) AS daily_mileage_km
|
||||
FROM vehicle_daily_mileage
|
||||
|
||||
@@ -229,6 +229,12 @@ func (m *MockStore) VehicleLocationSourceHistory(_ context.Context, vin string)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockStore) VehicleSourceProvider(_ context.Context, vin string, protocol string, sourceKey string) (string, error) {
|
||||
m.sourcePolicyMu.RLock()
|
||||
defer m.sourcePolicyMu.RUnlock()
|
||||
return m.sourceProviders[vin+"\x00"+protocol+"\x00"+sourceKey], nil
|
||||
}
|
||||
|
||||
func (m *MockStore) VehicleSourcePolicy(_ context.Context, vin string) (VehicleSourcePolicyConfig, error) {
|
||||
m.sourcePolicyMu.RLock()
|
||||
defer m.sourcePolicyMu.RUnlock()
|
||||
@@ -252,7 +258,7 @@ func (m *MockStore) SaveVehicleSourcePolicy(_ context.Context, update vehicleSou
|
||||
}
|
||||
providerKey := update.VIN + "\x00" + update.Protocol + "\x00" + update.SourceKey
|
||||
currentProvider := m.sourceProviders[providerKey]
|
||||
policyChanged := update.CurrentEnabled != update.Enabled || update.CurrentPriority != update.Priority || update.CurrentRemark != update.Remark
|
||||
policyChanged := update.PolicyConfigurable && (update.CurrentEnabled != update.Enabled || update.CurrentPriority != update.Priority || update.CurrentRemark != update.Remark)
|
||||
providerChanged := currentProvider != update.ProviderName
|
||||
if !policyChanged && !providerChanged {
|
||||
return current, nil
|
||||
|
||||
@@ -1134,14 +1134,15 @@ type VehicleSourcePolicyUpdate struct {
|
||||
|
||||
type vehicleSourcePolicyStoreUpdate struct {
|
||||
VehicleSourcePolicyUpdate
|
||||
VIN string
|
||||
Protocol string
|
||||
SourceKey string
|
||||
SourceLabel string
|
||||
CurrentEnabled bool
|
||||
CurrentPriority int
|
||||
CurrentRemark string
|
||||
CurrentProvider string
|
||||
VIN string
|
||||
Protocol string
|
||||
SourceKey string
|
||||
SourceLabel string
|
||||
CurrentEnabled bool
|
||||
CurrentPriority int
|
||||
CurrentRemark string
|
||||
CurrentProvider string
|
||||
PolicyConfigurable bool
|
||||
}
|
||||
|
||||
type vehicleLocationSourceHistory struct {
|
||||
|
||||
@@ -69,14 +69,20 @@ func (s *ProductionStore) VehicleSourceEvidence(ctx context.Context, vin string,
|
||||
}
|
||||
|
||||
func (s *ProductionStore) canonicalLocationEvidence(ctx context.Context, vin string) ([]canonicalLocationEvidence, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT protocol, COALESCE(NULLIF(source_key, ''), CONCAT(protocol, ':canonical')),
|
||||
COALESCE(NULLIF(plate, ''), ''), DATE_FORMAT(event_time, '%Y-%m-%d %H:%i:%s'),
|
||||
latitude, longitude, speed_kmh, total_mileage_km, soc_percent,
|
||||
DATE_FORMAT(received_at, '%Y-%m-%d %H:%i:%s'),
|
||||
CASE WHEN received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE) THEN 1 ELSE 0 END
|
||||
FROM vehicle_realtime_location
|
||||
WHERE vin = ?
|
||||
ORDER BY protocol`, vin)
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT l.protocol,
|
||||
COALESCE(NULLIF(l.source_key, ''), CONCAT(l.protocol, ':canonical')),
|
||||
COALESCE(NULLIF(l.plate, ''), ''), DATE_FORMAT(l.event_time, '%Y-%m-%d %H:%i:%s'),
|
||||
l.latitude, l.longitude, l.speed_kmh, l.total_mileage_km, l.soc_percent,
|
||||
DATE_FORMAT(l.received_at, '%Y-%m-%d %H:%i:%s'),
|
||||
CASE WHEN l.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE) THEN 1 ELSE 0 END,
|
||||
COALESCE(provider.provider_name, '')
|
||||
FROM vehicle_realtime_location l
|
||||
LEFT JOIN platform_vehicle_source_provider provider
|
||||
ON BINARY provider.vin = BINARY l.vin
|
||||
AND BINARY provider.protocol = BINARY l.protocol
|
||||
AND BINARY provider.source_key = BINARY COALESCE(NULLIF(l.source_key, ''), CONCAT(l.protocol, ':canonical'))
|
||||
WHERE l.vin = ?
|
||||
ORDER BY l.protocol`, vin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -88,16 +94,19 @@ ORDER BY protocol`, vin)
|
||||
var eventTime, receivedAt sql.NullString
|
||||
var latitude, longitude, speed, mileage, soc sql.NullFloat64
|
||||
var online int
|
||||
var provider string
|
||||
if err := rows.Scan(
|
||||
&row.Protocol, &row.sourceKey, &plate, &eventTime,
|
||||
&latitude, &longitude, &speed, &mileage, &soc, &receivedAt, &online,
|
||||
&latitude, &longitude, &speed, &mileage, &soc, &receivedAt, &online, &provider,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row.SourceLabel = row.Protocol
|
||||
row.ProviderOverride = strings.TrimSpace(provider)
|
||||
row.SourceLabel = firstNonEmpty(row.ProviderOverride, row.Protocol)
|
||||
row.SourceKind = "CANONICAL"
|
||||
row.SelectedWithinProtocol = true
|
||||
row.Enabled = true
|
||||
row.Priority = 100
|
||||
row.Online = online == 1
|
||||
row.QualityStatus = "OK"
|
||||
row.Longitude = nullFloatPointer(longitude)
|
||||
|
||||
@@ -13,6 +13,7 @@ type vehicleSourcePolicyStore interface {
|
||||
VehicleSourcePolicy(context.Context, string) (VehicleSourcePolicyConfig, error)
|
||||
SaveVehicleSourcePolicy(context.Context, vehicleSourcePolicyStoreUpdate) (VehicleSourcePolicyConfig, error)
|
||||
VehicleLocationSourceHistory(context.Context, string) ([]vehicleLocationSourceHistory, error)
|
||||
VehicleSourceProvider(context.Context, string, string, string) (string, error)
|
||||
}
|
||||
|
||||
func sourceReference(vin, protocol, sourceKey string) string {
|
||||
@@ -57,9 +58,48 @@ func (s *Service) VehicleSourceDiagnostic(ctx context.Context, vin string) (Vehi
|
||||
protocolStatus[status.Protocol] = status
|
||||
}
|
||||
}
|
||||
representedProtocols := make(map[string]bool, len(evidence.LocationSources))
|
||||
for _, source := range evidence.LocationSources {
|
||||
representedProtocols[source.Protocol] = true
|
||||
}
|
||||
if access != nil {
|
||||
for _, status := range access.ProtocolStatuses {
|
||||
if !status.Connected || representedProtocols[status.Protocol] {
|
||||
continue
|
||||
}
|
||||
sourceKey := status.Protocol + ":canonical"
|
||||
providerOverride, providerErr := store.VehicleSourceProvider(ctx, evidence.VIN, status.Protocol, sourceKey)
|
||||
if providerErr != nil {
|
||||
return VehicleSourceDiagnostic{}, providerErr
|
||||
}
|
||||
source := VehicleLocationSourceEvidence{
|
||||
Protocol: status.Protocol,
|
||||
SourceLabel: firstNonEmpty(providerOverride, status.Provider, status.Protocol),
|
||||
ProviderOverride: providerOverride,
|
||||
SourceKind: "CANONICAL",
|
||||
SelectedWithinProtocol: true,
|
||||
Recommended: evidence.RecommendedLocationProtocol == status.Protocol,
|
||||
Enabled: true,
|
||||
Priority: 100,
|
||||
Online: status.OnlineState == "online",
|
||||
QualityStatus: "NO_LOCATION",
|
||||
QualityReason: "协议已形成接入快照,但当前没有可展开的独立位置来源",
|
||||
EventTime: status.LatestEventAt,
|
||||
ReceivedAt: status.LatestReceivedAt,
|
||||
FirstSeenAt: status.FirstSeenAt,
|
||||
ReportIntervalSec: status.ReportIntervalSec,
|
||||
sourceKey: sourceKey,
|
||||
}
|
||||
evidence.LocationSources = append(evidence.LocationSources, source)
|
||||
representedProtocols[status.Protocol] = true
|
||||
if source.Recommended {
|
||||
evidence.RecommendedLocationLabel = source.SourceLabel
|
||||
}
|
||||
}
|
||||
}
|
||||
for index := range evidence.LocationSources {
|
||||
source := &evidence.LocationSources[index]
|
||||
if !strings.EqualFold(source.SourceKind, "CANONICAL") {
|
||||
if source.sourceKey != "" {
|
||||
source.SourceRef = sourceReference(evidence.VIN, source.Protocol, source.sourceKey)
|
||||
}
|
||||
if item, exists := historyBySource[source.Protocol+"\x00"+source.sourceKey]; exists {
|
||||
@@ -135,7 +175,7 @@ func (s *Service) UpdateVehicleSourcePolicy(ctx context.Context, vin string, upd
|
||||
var selected *VehicleLocationSourceEvidence
|
||||
for index := range evidence.LocationSources {
|
||||
source := &evidence.LocationSources[index]
|
||||
if !strings.EqualFold(source.SourceKind, "CANONICAL") && sourceReference(vin, source.Protocol, source.sourceKey) == update.SourceRef {
|
||||
if source.sourceKey != "" && sourceReference(vin, source.Protocol, source.sourceKey) == update.SourceRef {
|
||||
selected = source
|
||||
break
|
||||
}
|
||||
@@ -143,6 +183,15 @@ func (s *Service) UpdateVehicleSourcePolicy(ctx context.Context, vin string, upd
|
||||
if selected == nil || selected.sourceKey == "" {
|
||||
return VehicleSourceDiagnostic{}, clientError{Code: "SOURCE_NOT_FOUND", Message: "当前车辆不存在该来源,请刷新后重试"}
|
||||
}
|
||||
policyConfigurable := !strings.EqualFold(selected.SourceKind, "CANONICAL")
|
||||
if !policyConfigurable && (selected.Enabled != update.Enabled ||
|
||||
selected.Priority != update.Priority ||
|
||||
strings.TrimSpace(selected.PolicyRemark) != update.Remark) {
|
||||
return VehicleSourceDiagnostic{}, clientError{
|
||||
Code: "SOURCE_POLICY_CANONICAL_READ_ONLY",
|
||||
Message: "协议融合快照只能维护提供方,不能调整启停、优先级或策略备注",
|
||||
}
|
||||
}
|
||||
providerChanged := strings.TrimSpace(selected.ProviderOverride) != update.ProviderName
|
||||
if providerChanged && update.ProviderEvidence == "" {
|
||||
return VehicleSourceDiagnostic{}, clientError{Code: "SOURCE_PROVIDER_REASON_REQUIRED", Message: "维护提供方时必须填写权威资料来源或核验说明"}
|
||||
@@ -161,6 +210,7 @@ func (s *Service) UpdateVehicleSourcePolicy(ctx context.Context, vin string, upd
|
||||
CurrentPriority: selected.Priority,
|
||||
CurrentRemark: selected.PolicyRemark,
|
||||
CurrentProvider: selected.ProviderOverride,
|
||||
PolicyConfigurable: policyConfigurable,
|
||||
}); err != nil {
|
||||
return VehicleSourceDiagnostic{}, err
|
||||
}
|
||||
@@ -183,7 +233,7 @@ func authorizeInternalOperations(ctx context.Context, adminOnly bool) error {
|
||||
|
||||
func sourceSelectionReason(source VehicleLocationSourceEvidence) string {
|
||||
if strings.EqualFold(source.SourceKind, "CANONICAL") {
|
||||
return "这是协议融合结果快照,不是独立终端候选;需要展开实际来源后调整策略。"
|
||||
return "这是协议融合结果快照,不是独立终端候选;可维护经权威资料确认的提供方,但不能从该快照调整终端启停或优先级。"
|
||||
}
|
||||
if !source.Enabled {
|
||||
return "已由运维策略停用,不参与协议内选举。"
|
||||
@@ -207,7 +257,7 @@ func recommendationReason(evidence VehicleSourceEvidence) string {
|
||||
for _, source := range evidence.LocationSources {
|
||||
if source.Recommended {
|
||||
if strings.EqualFold(source.SourceKind, "CANONICAL") {
|
||||
return fmt.Sprintf("当前推荐 %s 的协议融合结果;本次证据未取得可配置的独立终端候选,不能从该快照反推人工优先级。", source.Protocol)
|
||||
return fmt.Sprintf("当前推荐 %s 的协议融合结果;本次证据未取得独立终端候选,只能维护权威提供方,不能从该快照反推人工优先级。", source.Protocol)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"当前推荐 %s / %s:来源已启用、质量为 %s、协议内选中,策略优先级为 %d。选举还会综合两分钟新鲜度、漂移冲突保护和连续有效样本。",
|
||||
|
||||
@@ -31,6 +31,17 @@ ORDER BY protocol, source_key`, vin)
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *ProductionStore) VehicleSourceProvider(ctx context.Context, vin string, protocol string, sourceKey string) (string, error) {
|
||||
var provider string
|
||||
err := s.db.QueryRowContext(ctx, `SELECT provider_name
|
||||
FROM platform_vehicle_source_provider
|
||||
WHERE vin = ? AND protocol = ? AND source_key = ?`, vin, protocol, sourceKey).Scan(&provider)
|
||||
if err == sql.ErrNoRows {
|
||||
return "", nil
|
||||
}
|
||||
return strings.TrimSpace(provider), err
|
||||
}
|
||||
|
||||
func (s *ProductionStore) VehicleSourcePolicy(ctx context.Context, vin string) (VehicleSourcePolicyConfig, error) {
|
||||
config := VehicleSourcePolicyConfig{VIN: vin, Version: 1, UpdatedBy: "system", Audit: []VehicleSourcePolicyAudit{}}
|
||||
var updatedAt sql.NullString
|
||||
@@ -95,16 +106,18 @@ WHERE vin = ? FOR UPDATE`, update.VIN).Scan(¤tVersion); err != nil {
|
||||
currentPriority := update.CurrentPriority
|
||||
currentRemark := update.CurrentRemark
|
||||
currentProvider := strings.TrimSpace(update.CurrentProvider)
|
||||
var enabled int
|
||||
err = tx.QueryRowContext(ctx, `SELECT enabled, priority, remark
|
||||
if update.PolicyConfigurable {
|
||||
var enabled int
|
||||
err = tx.QueryRowContext(ctx, `SELECT enabled, priority, remark
|
||||
FROM vehicle_location_source_policy
|
||||
WHERE vin = ? AND protocol = ? AND source_key = ?`,
|
||||
update.VIN, update.Protocol, update.SourceKey,
|
||||
).Scan(&enabled, ¤tPriority, ¤tRemark)
|
||||
if err == nil {
|
||||
currentEnabled = enabled == 1
|
||||
} else if err != sql.ErrNoRows {
|
||||
return VehicleSourcePolicyConfig{}, err
|
||||
update.VIN, update.Protocol, update.SourceKey,
|
||||
).Scan(&enabled, ¤tPriority, ¤tRemark)
|
||||
if err == nil {
|
||||
currentEnabled = enabled == 1
|
||||
} else if err != sql.ErrNoRows {
|
||||
return VehicleSourcePolicyConfig{}, err
|
||||
}
|
||||
}
|
||||
err = tx.QueryRowContext(ctx, `SELECT provider_name
|
||||
FROM platform_vehicle_source_provider
|
||||
@@ -116,9 +129,9 @@ WHERE vin = ? AND protocol = ? AND source_key = ?`,
|
||||
} else if err != nil {
|
||||
return VehicleSourcePolicyConfig{}, err
|
||||
}
|
||||
policyChanged := currentEnabled != update.Enabled ||
|
||||
policyChanged := update.PolicyConfigurable && (currentEnabled != update.Enabled ||
|
||||
currentPriority != update.Priority ||
|
||||
strings.TrimSpace(currentRemark) != update.Remark
|
||||
strings.TrimSpace(currentRemark) != update.Remark)
|
||||
providerChanged := strings.TrimSpace(currentProvider) != update.ProviderName
|
||||
if !policyChanged && !providerChanged {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
|
||||
@@ -61,13 +61,14 @@ func TestProductionProviderUpdateKeepsPolicyRemarkSeparate(t *testing.T) {
|
||||
Remark: "保持当前优先级",
|
||||
Actor: "平台管理员",
|
||||
},
|
||||
VIN: "VIN001",
|
||||
Protocol: "JT808",
|
||||
SourceKey: "opaque-source-key",
|
||||
SourceLabel: "JT808 终端",
|
||||
CurrentEnabled: true,
|
||||
CurrentPriority: 20,
|
||||
CurrentRemark: "保持当前优先级",
|
||||
VIN: "VIN001",
|
||||
Protocol: "JT808",
|
||||
SourceKey: "opaque-source-key",
|
||||
SourceLabel: "JT808 终端",
|
||||
CurrentEnabled: true,
|
||||
CurrentPriority: 20,
|
||||
CurrentRemark: "保持当前优先级",
|
||||
PolicyConfigurable: true,
|
||||
}
|
||||
|
||||
mock.ExpectBegin()
|
||||
@@ -120,3 +121,156 @@ WHERE vin = ? AND protocol = ? AND source_key = ?`)).
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type canonicalProviderStore struct {
|
||||
*MockStore
|
||||
}
|
||||
|
||||
func newCanonicalProviderStore() *canonicalProviderStore {
|
||||
return &canonicalProviderStore{MockStore: NewMockStore()}
|
||||
}
|
||||
|
||||
func (s *canonicalProviderStore) VehicleSourceEvidence(_ context.Context, vin string, date string) (VehicleSourceEvidence, error) {
|
||||
sourceKey := "JT808:canonical"
|
||||
providerKey := vin + "\x00JT808\x00" + sourceKey
|
||||
s.sourcePolicyMu.RLock()
|
||||
provider := s.sourceProviders[providerKey]
|
||||
s.sourcePolicyMu.RUnlock()
|
||||
return VehicleSourceEvidence{
|
||||
VIN: vin, Plate: "沪A00001", MileageDate: date,
|
||||
LocationSources: []VehicleLocationSourceEvidence{{
|
||||
Protocol: "JT808", SourceLabel: firstNonEmpty(provider, "JT808"), ProviderOverride: provider,
|
||||
SourceKind: "CANONICAL", SelectedWithinProtocol: true, Enabled: true, Priority: 100,
|
||||
Online: true, QualityStatus: "OK", sourceKey: sourceKey,
|
||||
}},
|
||||
MileageSources: []VehicleMileageSourceEvidence{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestCanonicalSourceAllowsProviderButRejectsPolicyMutation(t *testing.T) {
|
||||
store := newCanonicalProviderStore()
|
||||
service := NewService(store)
|
||||
admin := WithPrincipal(context.Background(), Principal{Name: "平台管理员", Role: "admin", UserType: "admin"})
|
||||
diagnostic, err := service.VehicleSourceDiagnostic(admin, "VIN-CANONICAL-01")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(diagnostic.Evidence.LocationSources) != 1 || len(diagnostic.Evidence.LocationSources[0].SourceRef) != 64 {
|
||||
t.Fatalf("canonical source must expose an opaque provider reference: %+v", diagnostic.Evidence.LocationSources)
|
||||
}
|
||||
source := diagnostic.Evidence.LocationSources[0]
|
||||
updated, err := service.UpdateVehicleSourcePolicy(admin, diagnostic.Evidence.VIN, VehicleSourcePolicyUpdate{
|
||||
Version: diagnostic.Policy.Version, SourceRef: source.SourceRef,
|
||||
ProviderName: "东方北斗", ProviderEvidence: "GPS 运维终端清单 2026-07-16",
|
||||
Enabled: true, Priority: 100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("canonical provider update failed: %v", err)
|
||||
}
|
||||
if updated.Policy.Version != 2 || updated.Evidence.LocationSources[0].ProviderOverride != "东方北斗" {
|
||||
t.Fatalf("canonical provider update was not persisted: %+v", updated)
|
||||
}
|
||||
_, err = service.UpdateVehicleSourcePolicy(admin, diagnostic.Evidence.VIN, VehicleSourcePolicyUpdate{
|
||||
Version: 2, SourceRef: source.SourceRef,
|
||||
ProviderName: "东方北斗", Enabled: false, Priority: 100,
|
||||
})
|
||||
clientErr, ok := asClientError(err)
|
||||
if !ok || clientErr.Code != "SOURCE_POLICY_CANONICAL_READ_ONLY" {
|
||||
t.Fatalf("canonical policy mutation should fail closed, err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type accessOnlyProviderStore struct {
|
||||
*MockStore
|
||||
}
|
||||
|
||||
func (s *accessOnlyProviderStore) VehicleSourceEvidence(_ context.Context, vin string, date string) (VehicleSourceEvidence, error) {
|
||||
return VehicleSourceEvidence{
|
||||
VIN: vin, MileageDate: date,
|
||||
LocationSources: []VehicleLocationSourceEvidence{},
|
||||
MileageSources: []VehicleMileageSourceEvidence{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestDiagnosticSynthesizesProviderTargetForConnectedProtocolWithoutLocation(t *testing.T) {
|
||||
service := NewService(&accessOnlyProviderStore{MockStore: NewMockStore()})
|
||||
operator := WithPrincipal(context.Background(), Principal{Name: "运维员", Role: "operator", UserType: "operator"})
|
||||
diagnostic, err := service.VehicleSourceDiagnostic(operator, "LB9A32A24R0LS1426")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var jt808 *VehicleLocationSourceEvidence
|
||||
for index := range diagnostic.Evidence.LocationSources {
|
||||
if diagnostic.Evidence.LocationSources[index].Protocol == "JT808" {
|
||||
jt808 = &diagnostic.Evidence.LocationSources[index]
|
||||
break
|
||||
}
|
||||
}
|
||||
if jt808 == nil || jt808.SourceKind != "CANONICAL" || len(jt808.SourceRef) != 64 || jt808.Priority != 100 {
|
||||
t.Fatalf("connected protocol without location must remain provider-maintainable: %+v", diagnostic.Evidence.LocationSources)
|
||||
}
|
||||
if jt808.QualityStatus != "NO_LOCATION" || jt808.QualityReason == "" {
|
||||
t.Fatalf("synthetic provider target must explain missing location evidence: %+v", jt808)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionCanonicalProviderUpdateSkipsTerminalPolicyTable(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
store := NewProductionStore(db, nil, "")
|
||||
update := vehicleSourcePolicyStoreUpdate{
|
||||
VehicleSourcePolicyUpdate: VehicleSourcePolicyUpdate{
|
||||
Version: 1, SourceRef: "canonical-ref", ProviderName: "东方北斗",
|
||||
ProviderEvidence: "GPS 运维终端清单 2026-07-16",
|
||||
Enabled: true, Priority: 100, Actor: "平台管理员",
|
||||
},
|
||||
VIN: "VIN-CANONICAL-01", Protocol: "JT808", SourceKey: "JT808:canonical",
|
||||
SourceLabel: "JT808", CurrentEnabled: true, CurrentPriority: 100,
|
||||
PolicyConfigurable: false,
|
||||
}
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec(regexp.QuoteMeta(`INSERT IGNORE INTO platform_vehicle_source_policy_version
|
||||
(vin, version, updated_by) VALUES (?, 1, 'system')`)).
|
||||
WithArgs("VIN-CANONICAL-01").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT version FROM platform_vehicle_source_policy_version
|
||||
WHERE vin = ? FOR UPDATE`)).
|
||||
WithArgs("VIN-CANONICAL-01").WillReturnRows(sqlmock.NewRows([]string{"version"}).AddRow(1))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT provider_name
|
||||
FROM platform_vehicle_source_provider
|
||||
WHERE vin = ? AND protocol = ? AND source_key = ?`)).
|
||||
WithArgs("VIN-CANONICAL-01", "JT808", "JT808:canonical").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"provider_name"}))
|
||||
mock.ExpectExec(`INSERT INTO platform_vehicle_source_provider`).
|
||||
WithArgs("VIN-CANONICAL-01", "JT808", "JT808:canonical", "canonical-ref", "东方北斗", "平台管理员").
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectExec(`UPDATE platform_vehicle_source_policy_version`).
|
||||
WithArgs(2, "平台管理员", "VIN-CANONICAL-01", 1).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec(`INSERT INTO platform_vehicle_source_provider_audit`).
|
||||
WithArgs(
|
||||
"VIN-CANONICAL-01", 2, "JT808", "canonical-ref", "JT808:canonical",
|
||||
"", "东方北斗", "平台管理员", "GPS 运维终端清单 2026-07-16",
|
||||
"JT808:提供方 未维护→东方北斗;核验依据:GPS 运维终端清单 2026-07-16",
|
||||
).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
mock.ExpectQuery(`SELECT version, updated_by`).WithArgs("VIN-CANONICAL-01").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"version", "updated_by", "updated_at"}).AddRow(2, "平台管理员", "2026-07-16 20:10:00"))
|
||||
mock.ExpectQuery(`SELECT version, change_type, protocol, source_ref`).WithArgs("VIN-CANONICAL-01", "VIN-CANONICAL-01").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"version", "change_type", "protocol", "source_ref", "source_label", "actor", "changed_at", "summary"}).
|
||||
AddRow(2, "provider", "JT808", "canonical-ref", "东方北斗", "平台管理员", "2026-07-16 20:10:00", "JT808:提供方 未维护→东方北斗;核验依据:GPS 运维终端清单 2026-07-16"))
|
||||
|
||||
config, err := store.SaveVehicleSourcePolicy(context.Background(), update)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if config.Version != 2 {
|
||||
t.Fatalf("unexpected canonical provider config: %+v", config)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user