Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/api/internal/app/oneos_asset_page.go
T
lingniu d6b957dfc8 feat: sync vehicle monitoring and accumulated platform updates
Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates.

Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
2026-10-26 18:05:59 +08:00

162 lines
6.1 KiB
Go

package app
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
// oneOSAssetVehicle contains only asset catalog fields from the OneOS vehicle
// page. Authorization only consumes fully enumerated caller-scoped snapshots
// when ONEOS_VEHICLE_QUERY_SCOPE is explicitly enabled.
type oneOSAssetVehicle struct {
ID string `json:"id"`
VIN string `json:"vin"`
PlateNumber string `json:"plateNumber"`
VehicleType string `json:"vehicleType"`
Brand string `json:"brand"`
BrandName string `json:"brandName"`
Model string `json:"model"`
ModelName string `json:"modelName"`
ProvinceName string `json:"provinceName"`
CityName string `json:"cityName"`
CustomerName string `json:"customerName"`
ProjectName string `json:"projectName"`
BusinessDepartment string `json:"businessDepartment"`
BusinessResponsiblePerson string `json:"businessResponsiblePerson"`
OperationStatus string `json:"operationStatus"`
VehicleSource string `json:"vehicleSource"`
}
func (v *oneOSAssetVehicle) UnmarshalJSON(raw []byte) error {
type fields oneOSAssetVehicle
var payload struct {
fields
ID json.RawMessage `json:"id"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
return err
}
*v = oneOSAssetVehicle(payload.fields)
id, err := oneOSIDString(payload.ID)
if err != nil {
return err
}
v.ID = id
return nil
}
type oneOSAssetPage struct {
Records []oneOSAssetVehicle `json:"records"`
Total int64 `json:"total"`
Current int64 `json:"current"`
Size int64 `json:"size"`
}
var errOneOSAssetQuery = errors.New("invalid OneOS asset query")
var oneOSAssetPageFilters = map[string]bool{
"assetCategory": true, "vin": true, "plateNumber": true, "plateNumberList": true,
"province": true, "city": true, "provinces": true, "vehicleType": true,
"brand": true, "model": true, "customerName": true, "businessDept": true,
"registeredOwnership": true, "contractCode": true, "operationOrgId": true,
"vehicleSource": true, "rentalCompany": true, "operationStatus": true,
"vehicleStatus": true, "onlineStatus": true, "licenseStatus": true,
"packingLotId": true, "provinceName": true, "cityName": true,
"commercialInsuranceStatus": true, "compulsoryInsuranceStatus": true,
"projectName": true, "insuranceStatus": true,
}
const oneOSVehicleAssetCategory = "allOperating"
// VehicleAssetPage faithfully reads one bounded page of the OneOS vehicle
// catalog. It neither scans all pages nor turns records into permission grants.
func (c *oneOSProfileClient) VehicleAssetPage(ctx context.Context, token, clientID string, filters url.Values) (oneOSAssetPage, error) {
return c.vehicleAssetPage(ctx, token, clientID, filters, 50)
}
// Larger pages are reserved for the server-side permission snapshot.
func (c *oneOSProfileClient) vehicleAssetPage(ctx context.Context, token, clientID string, filters url.Values, maxPageSize int64) (oneOSAssetPage, error) {
if c == nil || !validOneOSLoginToken(token) || clientID == "" || clientID != c.passwordClientID {
return oneOSAssetPage{}, errors.New("invalid OneOS credential")
}
query := url.Values{}
pageNum, pageSize := int64(1), int64(10)
if filters != nil {
for key, values := range filters {
if key == "pageNum" || key == "pageSize" {
if len(values) != 1 || strings.TrimSpace(values[0]) == "" {
return oneOSAssetPage{}, errOneOSAssetQuery
}
continue
}
if !oneOSAssetPageFilters[key] || len(values) == 0 || len(values) > 20 {
return oneOSAssetPage{}, errOneOSAssetQuery
}
for _, value := range values {
if strings.TrimSpace(value) == "" || len(value) > 128 || strings.ContainsAny(value, "\r\n") {
return oneOSAssetPage{}, errOneOSAssetQuery
}
query.Add(key, value)
}
}
if raw := filters.Get("pageNum"); raw != "" {
parsed, err := strconv.ParseInt(raw, 10, 64)
if err != nil || parsed < 1 || parsed > 1000000 || len(filters["pageNum"]) != 1 {
return oneOSAssetPage{}, errOneOSAssetQuery
}
pageNum = parsed
}
if raw := filters.Get("pageSize"); raw != "" {
parsed, err := strconv.ParseInt(raw, 10, 64)
if err != nil || parsed < 1 || parsed > maxPageSize || len(filters["pageSize"]) != 1 {
return oneOSAssetPage{}, errOneOSAssetQuery
}
pageSize = parsed
}
}
if len(query["assetCategory"]) > 1 {
return oneOSAssetPage{}, errOneOSAssetQuery
}
// Fixed business boundary; caller filters cannot select a different category.
query.Set("assetCategory", oneOSVehicleAssetCategory)
query.Set("pageNum", strconv.FormatInt(pageNum, 10))
query.Set("pageSize", strconv.FormatInt(pageSize, 10))
ctx, cancel := context.WithTimeout(ctx, c.requestTimeout)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/asset/vehicle-info/page?"+query.Encode(), nil)
if err != nil {
return oneOSAssetPage{}, errors.New("invalid OneOS asset request")
}
request.Header.Set("Authorization", "Bearer "+token)
request.Header.Set("ClientID", clientID)
request.Header.Set("Accept", "application/json")
response, err := c.client.Do(request)
if err != nil {
return oneOSAssetPage{}, errors.New("OneOS asset page unavailable")
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return oneOSAssetPage{}, fmt.Errorf("OneOS asset page rejected: HTTP %d", response.StatusCode)
}
raw, err := io.ReadAll(io.LimitReader(response.Body, (1<<20)+1))
if err != nil || len(raw) > 1<<20 {
return oneOSAssetPage{}, errors.New("OneOS asset page too large")
}
var envelope oneOSProfileEnvelope[oneOSAssetPage]
if err := json.Unmarshal(raw, &envelope); err != nil || envelope.Code != 200 {
return oneOSAssetPage{}, errors.New("invalid OneOS asset page response")
}
page := envelope.Data
if page.Total < 0 || page.Current != pageNum || page.Size != pageSize || int64(len(page.Records)) > pageSize {
return oneOSAssetPage{}, errors.New("invalid OneOS asset page metadata")
}
return page, nil
}