feat(platform): consolidate production vehicle data workflows
This commit is contained in:
200
vehicle-data-platform/apps/api/internal/businessscope/source.go
Normal file
200
vehicle-data-platform/apps/api/internal/businessscope/source.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package businessscope
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// candidateQuery deliberately derives the active lease lifecycle from delivery and
|
||||
// return task facts. vehicle_lease_order_record remains an independent customer
|
||||
// ownership check, but its last_return_time is not authoritative: saving a return
|
||||
// draft currently updates that aggregate field before the return is completed.
|
||||
const candidateQuery = `SELECT
|
||||
dv.vehicle_id,
|
||||
v.id,
|
||||
COALESCE(v.vin, ''),
|
||||
COALESCE(v.plate_number, dv.plate_number, r.plate_number, ''),
|
||||
r.customer_id,
|
||||
ci.id,
|
||||
dv.contract_id,
|
||||
co.id,
|
||||
COALESCE(co.other_customer_id, co.customer_id),
|
||||
COALESCE(co.contract_code, r.contract_code, ''),
|
||||
COALESCE(co.project_name, r.project_name, ''),
|
||||
COALESCE(vs.operation_status, ''),
|
||||
dv.delivery_time,
|
||||
dv.update_time
|
||||
FROM delivery_vehicle dv
|
||||
LEFT JOIN vehicle_info v ON v.id = dv.vehicle_id AND v.del_flag = '0'
|
||||
LEFT JOIN vehicle_lease_order_record r ON r.vehicle_id = dv.vehicle_id AND r.del_flag = '0'
|
||||
LEFT JOIN customer_info ci ON ci.id = r.customer_id AND ci.del_flag = '0'
|
||||
LEFT JOIN vehicle_lease_contract_info co ON co.id = dv.contract_id AND co.del_flag = '0'
|
||||
LEFT JOIN vehicle_status vs ON vs.vehicle_id = dv.vehicle_id AND vs.del_flag = '0'
|
||||
WHERE dv.del_flag = '0'
|
||||
AND dv.delivery_status IN (2, 3)
|
||||
AND dv.vehicle_id IS NOT NULL
|
||||
AND dv.delivery_time IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM delivery_vehicle newer
|
||||
WHERE newer.del_flag = '0'
|
||||
AND newer.vehicle_id = dv.vehicle_id
|
||||
AND newer.vehicle_id IS NOT NULL
|
||||
AND (
|
||||
COALESCE(newer.delivery_time, '1000-01-01') > COALESCE(dv.delivery_time, '1000-01-01')
|
||||
OR (
|
||||
COALESCE(newer.delivery_time, '1000-01-01') = COALESCE(dv.delivery_time, '1000-01-01')
|
||||
AND newer.id > dv.id
|
||||
)
|
||||
)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM return_vehicle_task rt
|
||||
WHERE rt.delivery_vehicle_id = dv.id
|
||||
AND rt.del_flag = '0'
|
||||
AND rt.status IN (2, 3, 5)
|
||||
)
|
||||
ORDER BY dv.vehicle_id, dv.id, r.id`
|
||||
|
||||
func ReadCandidates(ctx context.Context, db *sql.DB) ([]Candidate, error) {
|
||||
connection, err := db.Conn(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acquire OneOS connection: %w", err)
|
||||
}
|
||||
defer connection.Close()
|
||||
if err := VerifyReadOnlyGrants(ctx, connection); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := connection.ExecContext(ctx, `SET SESSION TRANSACTION READ ONLY`); err != nil {
|
||||
return nil, fmt.Errorf("force OneOS session read only: %w", err)
|
||||
}
|
||||
if _, err := connection.ExecContext(ctx, `SET SESSION MAX_EXECUTION_TIME = 10000`); err != nil {
|
||||
return nil, fmt.Errorf("set OneOS query timeout: %w", err)
|
||||
}
|
||||
tx, err := connection.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead, ReadOnly: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("begin OneOS read-only snapshot: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
rows, err := tx.QueryContext(ctx, candidateQuery)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query OneOS customer vehicle candidates: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
candidates := make([]Candidate, 0, 1024)
|
||||
for rows.Next() {
|
||||
var vehicleID, vehicleProfileID, customerID, customerProfileID sql.NullInt64
|
||||
var contractID, contractProfileID, effectiveCustomerID sql.NullInt64
|
||||
var vin, plate, contractCode, projectName, operationStatus string
|
||||
var scopeStart time.Time
|
||||
var updatedAt sql.NullTime
|
||||
if err := rows.Scan(
|
||||
&vehicleID, &vehicleProfileID, &vin, &plate, &customerID, &customerProfileID,
|
||||
&contractID, &contractProfileID, &effectiveCustomerID, &contractCode, &projectName,
|
||||
&operationStatus, &scopeStart, &updatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan OneOS scope candidate: %w", err)
|
||||
}
|
||||
candidate := Candidate{
|
||||
RowNumber: len(candidates) + 1,
|
||||
VehicleID: vehicleID.Int64, VehiclePresent: vehicleID.Valid && vehicleProfileID.Valid,
|
||||
VIN: vin, PlateNumber: plate,
|
||||
CustomerID: customerID.Int64, CustomerPresent: customerID.Valid,
|
||||
CustomerProfileExists: customerProfileID.Valid,
|
||||
ContractID: contractID.Int64, ContractPresent: contractID.Valid,
|
||||
ContractProfileExists: contractProfileID.Valid,
|
||||
EffectiveCustomerID: effectiveCustomerID.Int64, EffectiveCustomerSet: effectiveCustomerID.Valid,
|
||||
ContractCode: contractCode, ProjectName: projectName, OperationStatus: operationStatus,
|
||||
ScopeStartAt: scopeStart,
|
||||
}
|
||||
if updatedAt.Valid {
|
||||
value := updatedAt.Time
|
||||
candidate.SourceUpdatedAt = &value
|
||||
}
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate OneOS scope candidates: %w", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("finish OneOS read-only snapshot: %w", err)
|
||||
}
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
func VerifyReadOnlyGrants(ctx context.Context, connection *sql.Conn) error {
|
||||
rows, err := connection.QueryContext(ctx, `SHOW GRANTS FOR CURRENT_USER`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect OneOS database grants: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
grants := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var grant string
|
||||
if err := rows.Scan(&grant); err != nil {
|
||||
return fmt.Errorf("scan OneOS database grant: %w", err)
|
||||
}
|
||||
grants = append(grants, grant)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return fmt.Errorf("iterate OneOS database grants: %w", err)
|
||||
}
|
||||
if err := ValidateReadOnlyGrants(grants); err != nil {
|
||||
return fmt.Errorf("OneOS database account is not read-only: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateReadOnlyGrants(grants []string) error {
|
||||
if len(grants) == 0 {
|
||||
return fmt.Errorf("SHOW GRANTS returned no rows")
|
||||
}
|
||||
allowedSelectScopes := map[string]bool{
|
||||
"LN_ASSET_MANAGEMENT.VEHICLE_LEASE_ORDER_RECORD": true,
|
||||
"LN_ASSET_MANAGEMENT.DELIVERY_VEHICLE": true,
|
||||
"LN_ASSET_MANAGEMENT.RETURN_VEHICLE_TASK": true,
|
||||
"LN_ASSET_MANAGEMENT.VEHICLE_INFO": true,
|
||||
"LN_ASSET_MANAGEMENT.CUSTOMER_INFO": true,
|
||||
"LN_ASSET_MANAGEMENT.VEHICLE_LEASE_CONTRACT_INFO": true,
|
||||
"LN_ASSET_MANAGEMENT.VEHICLE_STATUS": true,
|
||||
}
|
||||
for _, raw := range grants {
|
||||
grant := strings.ToUpper(strings.TrimSpace(raw))
|
||||
if strings.HasPrefix(grant, "SET DEFAULT ROLE ") {
|
||||
return fmt.Errorf("role-based grants are not accepted; grant SELECT directly to the sync account")
|
||||
}
|
||||
if !strings.HasPrefix(grant, "GRANT ") {
|
||||
return fmt.Errorf("unsupported grant statement")
|
||||
}
|
||||
onIndex := strings.Index(grant, " ON ")
|
||||
if onIndex < 0 {
|
||||
return fmt.Errorf("unsupported role or dynamic grant")
|
||||
}
|
||||
privileges := strings.TrimSpace(strings.TrimPrefix(grant[:onIndex], "GRANT "))
|
||||
toIndex := strings.Index(grant[onIndex+4:], " TO ")
|
||||
if toIndex < 0 {
|
||||
return fmt.Errorf("unsupported grant scope")
|
||||
}
|
||||
scope := strings.ReplaceAll(strings.TrimSpace(grant[onIndex+4:onIndex+4+toIndex]), "`", "")
|
||||
for _, privilege := range strings.Split(privileges, ",") {
|
||||
privilege = strings.TrimSpace(privilege)
|
||||
switch privilege {
|
||||
case "USAGE":
|
||||
if scope != "*.*" {
|
||||
return fmt.Errorf("USAGE has unsupported scope %q", scope)
|
||||
}
|
||||
case "SELECT":
|
||||
if !allowedSelectScopes[scope] {
|
||||
return fmt.Errorf("SELECT scope %q is not required by the sync query", scope)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("disallowed privilege %q", privilege)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user