功能:扩展开放平台氢耗溯源与合作站数据
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -137,20 +138,65 @@ func (r *MySQLRepository) TotalMileage(ctx context.Context, vin string, at time.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *MySQLRepository) StationaryLocationPoints(ctx context.Context, vins []string, start, end time.Time, longitude, latitude, radiusMeters, maxSpeedKmh float64) ([]StationaryLocationPoint, error) {
|
||||
if r.tdengine == nil || r.tdDatabase == "" {
|
||||
return nil, errors.New("TDengine is not configured for stationary vehicle query")
|
||||
}
|
||||
if len(vins) == 0 {
|
||||
return []StationaryLocationPoint{}, nil
|
||||
}
|
||||
// Use a small bounding box as TDengine's prefilter, then apply the exact
|
||||
// 5-metre great-circle calculation in the service before returning a match.
|
||||
latitudeDelta := radiusMeters / 111320.0
|
||||
longitudeDelta := radiusMeters / math.Max(1, 111320.0*math.Cos(latitude*math.Pi/180))
|
||||
quotedVINs := make([]string, 0, len(vins))
|
||||
for _, vin := range vins {
|
||||
quotedVINs = append(quotedVINs, "'"+strings.ReplaceAll(vin, "'", "''")+"'")
|
||||
}
|
||||
startLiteral := strings.ReplaceAll(start.Format(time.RFC3339), "'", "''")
|
||||
endLiteral := strings.ReplaceAll(end.Format(time.RFC3339), "'", "''")
|
||||
query := `SELECT CAST(ts AS BIGINT),vin,protocol,longitude,latitude,speed_kmh FROM ` + r.tdDatabase + `.vehicle_locations` +
|
||||
` WHERE ts>='` + startLiteral + `' AND ts<='` + endLiteral + `'` +
|
||||
` AND vin IN (` + strings.Join(quotedVINs, ",") + `)` +
|
||||
` AND longitude BETWEEN ` + strconv.FormatFloat(longitude-longitudeDelta, 'f', 8, 64) + ` AND ` + strconv.FormatFloat(longitude+longitudeDelta, 'f', 8, 64) +
|
||||
` AND latitude BETWEEN ` + strconv.FormatFloat(latitude-latitudeDelta, 'f', 8, 64) + ` AND ` + strconv.FormatFloat(latitude+latitudeDelta, 'f', 8, 64) +
|
||||
` AND speed_kmh BETWEEN 0 AND ` + strconv.FormatFloat(maxSpeedKmh, 'f', 3, 64) +
|
||||
` ORDER BY vin,ts ASC,protocol ASC`
|
||||
rows, err := r.tdengine.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
points := make([]StationaryLocationPoint, 0)
|
||||
for rows.Next() {
|
||||
var point StationaryLocationPoint
|
||||
var timestampMS int64
|
||||
if err := rows.Scan(×tampMS, &point.VIN, &point.Protocol, &point.Longitude, &point.Latitude, &point.SpeedKmh); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
point.ObservedAt = time.UnixMilli(timestampMS).In(time.FixedZone("Asia/Shanghai", 8*60*60))
|
||||
points = append(points, point)
|
||||
}
|
||||
return points, rows.Err()
|
||||
}
|
||||
|
||||
func (r *MySQLRepository) RealtimeVehicles(ctx context.Context, vins []string, now time.Time) (map[string]RealtimeVehiclePoint, error) {
|
||||
out := make(map[string]RealtimeVehiclePoint, len(vins))
|
||||
if len(vins) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
placeholders := strings.TrimRight(strings.Repeat("?,", len(vins)), ",")
|
||||
args := make([]any, 0, len(vins)+1)
|
||||
dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
args := make([]any, 0, len(vins)+2)
|
||||
args = append(args, dayStart)
|
||||
for _, vin := range vins {
|
||||
args = append(args, vin)
|
||||
}
|
||||
args = append(args, now.Add(-10*time.Minute))
|
||||
query := `
|
||||
SELECT l.vin,l.protocol,COALESCE(l.longitude,0),COALESCE(l.latitude,0),
|
||||
COALESCE(l.speed_kmh,0),COALESCE(l.total_mileage_km,0),l.updated_at
|
||||
COALESCE(l.speed_kmh,0),l.soc_percent,COALESCE(l.total_mileage_km,0),l.updated_at,
|
||||
MAX(CASE WHEN l.updated_at>=? THEN 1 ELSE 0 END) OVER (PARTITION BY l.vin) AS active_today
|
||||
FROM vehicle_realtime_location l
|
||||
WHERE BINARY l.vin IN (` + placeholders + `)
|
||||
ORDER BY l.vin,
|
||||
@@ -165,9 +211,14 @@ ORDER BY l.vin,
|
||||
onlineThreshold := now.Add(-time.Minute)
|
||||
for rows.Next() {
|
||||
var point RealtimeVehiclePoint
|
||||
if err := rows.Scan(&point.VIN, &point.Protocol, &point.Longitude, &point.Latitude, &point.SpeedKmh, &point.TotalMileageKm, &point.ObservedAt); err != nil {
|
||||
var soc sql.NullFloat64
|
||||
if err := rows.Scan(&point.VIN, &point.Protocol, &point.Longitude, &point.Latitude, &point.SpeedKmh, &soc, &point.TotalMileageKm, &point.ObservedAt, &point.ActiveToday); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if soc.Valid && soc.Float64 >= 0 && soc.Float64 <= 100 {
|
||||
value := round3(soc.Float64)
|
||||
point.SOCPercent = &value
|
||||
}
|
||||
point.Online = !point.ObservedAt.Before(onlineThreshold)
|
||||
if selected, exists := out[point.VIN]; !exists {
|
||||
out[point.VIN] = point
|
||||
@@ -183,35 +234,57 @@ ORDER BY l.vin,
|
||||
|
||||
func (r *MySQLRepository) HydrogenStations(ctx context.Context, request HydrogenStationRequest) ([]HydrogenStation, error) {
|
||||
where := []string{
|
||||
"s.longitude BETWEEN -180 AND 180",
|
||||
"s.latitude BETWEEN -90 AND 90",
|
||||
"NOT (s.longitude=0 AND s.latitude=0)",
|
||||
"n.del_flag='0'",
|
||||
"n.longitude BETWEEN -180 AND 180",
|
||||
"n.latitude BETWEEN -90 AND 90",
|
||||
"NOT (n.longitude=0 AND n.latitude=0)",
|
||||
}
|
||||
args := make([]any, 0, 3)
|
||||
args := make([]any, 0, 4)
|
||||
if request.Province != "" {
|
||||
where = append(where, "s.province=?")
|
||||
args = append(args, request.Province)
|
||||
where = append(where, "(n.province=? OR province_region.NAME=?)")
|
||||
args = append(args, request.Province, request.Province)
|
||||
}
|
||||
if request.City != "" {
|
||||
where = append(where, "s.city=?")
|
||||
args = append(args, request.City)
|
||||
}
|
||||
if request.CooperateOnly != nil {
|
||||
if *request.CooperateOnly {
|
||||
where = append(where, "s.inner_site_id IS NOT NULL")
|
||||
} else {
|
||||
where = append(where, "s.inner_site_id IS NULL")
|
||||
}
|
||||
where = append(where, "(n.city=? OR city_region.NAME=?)")
|
||||
args = append(args, request.City, request.City)
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT CAST(s.id AS CHAR),COALESCE(NULLIF(s.fixed_station_name,''),NULLIF(s.station_name,''),''),
|
||||
COALESCE(h.station_short_name,''),COALESCE(s.station_address,''),
|
||||
s.longitude,s.latitude,COALESCE(s.province,''),COALESCE(s.city,''),COALESCE(s.district,''),
|
||||
CASE WHEN s.inner_site_id IS NULL THEN 0 ELSE 1 END
|
||||
FROM ln_asset_management.tab_outside_hydrogen_site s
|
||||
LEFT JOIN ln_asset_management.hydrogen_station h ON h.id=s.inner_site_id AND h.del_flag='0'
|
||||
SELECT CAST(n.id AS CHAR),COALESCE(n.site_name,''),COALESCE(n.site_short_name,''),COALESCE(n.site_address,''),
|
||||
n.longitude,n.latitude,
|
||||
COALESCE(NULLIF(province_region.NAME,''),n.province,''),
|
||||
COALESCE(NULLIF(city_region.NAME,''),n.city,''),COALESCE(s.district,''),
|
||||
CASE WHEN s.inner_site_id IS NOT NULL OR (
|
||||
n.cooperation_start_date IS NOT NULL
|
||||
AND n.cooperation_start_date<=CURDATE()
|
||||
AND (n.cooperation_end_date IS NULL OR n.cooperation_end_date>=CURDATE())
|
||||
) THEN 1 ELSE 0 END,
|
||||
COALESCE(n.contact_person,''),COALESCE(NULLIF(n.mobile_phone,''),n.fixed_phone,''),COALESCE(n.cost_price,0),
|
||||
COALESCE(l.monthly_hydrogen_kg,0),COALESCE(l.total_hydrogen_kg,n.fill_weight,0)
|
||||
FROM ln_asset_management.new_hydrogen_site n
|
||||
LEFT JOIN ln_asset_management.common_district province_region
|
||||
ON province_region.CODE COLLATE utf8mb4_general_ci=n.province COLLATE utf8mb4_general_ci
|
||||
LEFT JOIN ln_asset_management.common_district city_region
|
||||
ON city_region.CODE COLLATE utf8mb4_general_ci=n.city COLLATE utf8mb4_general_ci
|
||||
LEFT JOIN ln_asset_management.tab_outside_hydrogen_site s ON s.id=(
|
||||
SELECT MIN(matched.id)
|
||||
FROM ln_asset_management.tab_outside_hydrogen_site matched
|
||||
WHERE matched.fixed_station_name=n.site_name COLLATE utf8mb4_general_ci
|
||||
OR matched.station_name=n.site_name COLLATE utf8mb4_general_ci
|
||||
OR matched.fixed_station_name=n.site_short_name COLLATE utf8mb4_general_ci
|
||||
OR matched.station_name=n.site_short_name COLLATE utf8mb4_general_ci
|
||||
)
|
||||
LEFT JOIN (
|
||||
SELECT station_id,
|
||||
SUM(amount_kg) AS total_hydrogen_kg,
|
||||
SUM(CASE WHEN refuel_date >= DATE_FORMAT(CURDATE(),'%Y-%m-01')
|
||||
AND refuel_date < DATE_ADD(DATE_FORMAT(CURDATE(),'%Y-%m-01'),INTERVAL 1 MONTH)
|
||||
THEN amount_kg ELSE 0 END) AS monthly_hydrogen_kg
|
||||
FROM ln_asset_management.hydrogen_fuel_ledger
|
||||
WHERE del_flag='0'
|
||||
GROUP BY station_id
|
||||
) l ON l.station_id=s.inner_site_id
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY s.province,s.city,s.fixed_station_name,s.id
|
||||
ORDER BY province_region.NAME,city_region.NAME,n.site_name,n.id
|
||||
LIMIT 2000`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -220,9 +293,15 @@ LIMIT 2000`, args...)
|
||||
stations := make([]HydrogenStation, 0, 512)
|
||||
for rows.Next() {
|
||||
var station HydrogenStation
|
||||
if err := rows.Scan(&station.ID, &station.Name, &station.ShortName, &station.Address, &station.Longitude, &station.Latitude, &station.Province, &station.City, &station.District, &station.Cooperative); err != nil {
|
||||
if err := rows.Scan(&station.ID, &station.Name, &station.ShortName, &station.Address, &station.Longitude, &station.Latitude, &station.Province, &station.City, &station.District, &station.Cooperative, &station.ContactPerson, &station.ContactPhone, &station.UnitPrice, &station.MonthlyHydrogenKg, &station.TotalHydrogenKg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isExcelSinopecStation(station.Name, station.ShortName) {
|
||||
station.Cooperative = true
|
||||
}
|
||||
if request.CooperateOnly != nil && station.Cooperative != *request.CooperateOnly {
|
||||
continue
|
||||
}
|
||||
stations = append(stations, station)
|
||||
}
|
||||
return stations, rows.Err()
|
||||
@@ -264,6 +343,48 @@ func (r *MySQLRepository) DailyMileage(ctx context.Context, vins []string, date
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// MileageRollbacks returns source days that were rejected because the odometer
|
||||
// moved backwards. They must not silently fall back to an earlier total mileage
|
||||
// and be exposed as normal data.
|
||||
func (r *MySQLRepository) MileageRollbacks(ctx context.Context, vins []string, startDate, endDate string, protocols []string) (map[string]bool, error) {
|
||||
out := make(map[string]bool)
|
||||
if len(vins) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
vinPlaceholders := strings.TrimRight(strings.Repeat("?,", len(vins)), ",")
|
||||
query := `
|
||||
SELECT DISTINCT vin,DATE_FORMAT(stat_date,'%Y-%m-%d')
|
||||
FROM vehicle_daily_mileage_source
|
||||
WHERE stat_date BETWEEN ? AND ?
|
||||
AND vin IN (` + vinPlaceholders + `)
|
||||
AND quality_reason='TOTAL_MILEAGE_ROLLBACK'`
|
||||
args := make([]any, 0, len(vins)+2+len(protocols))
|
||||
args = append(args, startDate, endDate)
|
||||
for _, vin := range vins {
|
||||
args = append(args, vin)
|
||||
}
|
||||
if len(protocols) > 0 {
|
||||
protocolPlaceholders := strings.TrimRight(strings.Repeat("?,", len(protocols)), ",")
|
||||
query += "\n AND protocol IN (" + protocolPlaceholders + ")"
|
||||
for _, protocol := range protocols {
|
||||
args = append(args, protocol)
|
||||
}
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var vin, date string
|
||||
if err := rows.Scan(&vin, &date); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[dailyMileageKey(vin, date)] = true
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (r *MySQLRepository) DailyMileageRange(ctx context.Context, vins []string, startDate, endDate string, protocols []string) (map[string]DailyMileage, error) {
|
||||
if len(vins) == 0 {
|
||||
return map[string]DailyMileage{}, nil
|
||||
@@ -275,7 +396,7 @@ SELECT
|
||||
DATE_FORMAT(m.stat_date,'%Y-%m-%d'),
|
||||
m.protocol,
|
||||
m.daily_mileage_km,
|
||||
m.latest_total_mileage_km,
|
||||
COALESCE(m.day_end_total_mileage_km,m.latest_total_mileage_km),
|
||||
COALESCE(DATE_FORMAT((
|
||||
SELECT MAX(selected.latest_event_time)
|
||||
FROM vehicle_daily_mileage_source selected
|
||||
@@ -289,8 +410,8 @@ SELECT
|
||||
FROM vehicle_daily_mileage m
|
||||
WHERE m.stat_date BETWEEN ? AND ?
|
||||
AND m.vin IN (` + placeholders + `)
|
||||
AND m.latest_total_mileage_km IS NOT NULL
|
||||
AND m.latest_total_mileage_km>=0
|
||||
AND COALESCE(m.day_end_total_mileage_km,m.latest_total_mileage_km) IS NOT NULL
|
||||
AND COALESCE(m.day_end_total_mileage_km,m.latest_total_mileage_km)>=0
|
||||
AND m.daily_mileage_km>=0`
|
||||
args := make([]any, 0, len(vins)+2+len(protocols)*2)
|
||||
args = append(args, startDate, endDate)
|
||||
@@ -345,7 +466,7 @@ SELECT
|
||||
DATE_FORMAT(m.stat_date,'%Y-%m-%d'),
|
||||
m.protocol,
|
||||
m.daily_mileage_km,
|
||||
m.latest_total_mileage_km,
|
||||
COALESCE(m.day_end_total_mileage_km,m.latest_total_mileage_km),
|
||||
COALESCE(DATE_FORMAT((
|
||||
SELECT MAX(selected.latest_event_time)
|
||||
FROM vehicle_daily_mileage_source selected
|
||||
@@ -362,8 +483,8 @@ JOIN (
|
||||
FROM vehicle_daily_mileage prior
|
||||
WHERE prior.stat_date<?
|
||||
AND prior.vin IN (` + vinPlaceholders + `)
|
||||
AND prior.latest_total_mileage_km IS NOT NULL
|
||||
AND prior.latest_total_mileage_km>=0
|
||||
AND COALESCE(prior.day_end_total_mileage_km,prior.latest_total_mileage_km) IS NOT NULL
|
||||
AND COALESCE(prior.day_end_total_mileage_km,prior.latest_total_mileage_km)>=0
|
||||
AND prior.daily_mileage_km>=0`
|
||||
args := make([]any, 0, len(vins)+1+len(protocols)*2)
|
||||
args = append(args, beforeDate)
|
||||
|
||||
Reference in New Issue
Block a user