Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions common/reconcile_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,28 @@ var ReconcileDriftWarnPct = 0.02
// granularity or shorten the time span — anything bigger overwhelms the JSON
// response and the frontend table.
var ReconcileMaxBuckets = 20000

// --- v3.1 difference-localisation (see docs §十三) ---

// ReconcileMaxAlignShiftHours: per-model the comparator probes integer-hour
// shifts in [-N, +N] to absorb the supplier's systematic hour-bucket offset
// before deciding what is a *real* difference. The parallel supplier drifts
// by ±1h, so 1 is enough — a wider window risks swallowing genuine adjacent
// differences as drift.
var ReconcileMaxAlignShiftHours = 1

// ReconcileSignificantAmountCNY: after alignment + ±1h residual netting, a
// (model, hour) bucket is shown in the detail table only if its residual Δ¥
// reaches this. Below it the bucket is pure drift / rounding and is hidden.
var ReconcileSignificantAmountCNY = 0.01

// ReconcileSignificantTokens / ReconcileSignificantPct gate whether a token
// dimension counts as a genuine *usage* mismatch (vs price-only): the delta
// must be at least this many tokens AND at least this share of the larger
// side. Both bounds avoid flagging rounding-scale token jitter.
var ReconcileSignificantTokens int64 = 1
var ReconcileSignificantPct = 0.005

// ReconcileDiffBreakdownTopN caps how many models the summary "差异构成"
// attribution lists explicitly; the rest collapse into a single "其他" item.
var ReconcileDiffBreakdownTopN = 5
33 changes: 31 additions & 2 deletions dto/reconcile_upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ type ReconcileSummary struct {
LocalTotal Totals `json:"local_total"`
Delta Totals `json:"delta"`
DeltaAmountPct float64 `json:"delta_amount_pct"`
// DiffBreakdown attributes the interval's total Δ¥ to its top contributing
// models (v3.1 §13.3). Sorted by |delta| desc; the tail collapses into a
// single {model:"其他"} item.
DiffBreakdown []DiffBreakdownItem `json:"diff_breakdown,omitempty"`
}

// DiffBreakdownItem is one row of the summary "差异构成" attribution.
type DiffBreakdownItem struct {
Model string `json:"model"`
DeltaAmountCNY float64 `json:"delta_amount_cny"`
DiffKind string `json:"diff_kind,omitempty"`
}

// DriftAnalysis classifies the cumulative Δ¥ pattern across the hour series.
Expand All @@ -58,7 +69,11 @@ type DiffSide struct {
RequestCount int `json:"request_count,omitempty"`
}

// DiffRow is one (model, hour_bucket) cell.
// DiffRow is one (model, hour_bucket) cell. In v3.1 the comparator only emits
// rows that carry a *genuine* residual difference after drift alignment —
// pure drift buckets are dropped, so HourBucket is the local anchor and the
// supplier data (if any) may have come from SupplierBucket = HourBucket +
// AlignShiftHours·3600.
type ReconcileDiffRow struct {
HourBucket int64 `json:"hour_bucket"`
Model string `json:"model"`
Expand All @@ -67,7 +82,17 @@ type ReconcileDiffRow struct {
Delta Totals `json:"delta"`
CumulativeDeltaAmountCNY float64 `json:"cumulative_delta_amount_cny"`
Status string `json:"status"` // matched / supplier_only / local_only
Regions []string `json:"regions,omitempty"`
// DiffKind localises *why* this row differs: price_only / usage /
// missing_local / missing_supplier (v3.1 §13.2).
DiffKind string `json:"diff_kind,omitempty"`
// AlignShiftHours is the integer-hour shift applied to this model's
// supplier series during alignment (supplier_bucket - hour_bucket, in
// hours). 0 when the supplier bucket already matched the local anchor.
AlignShiftHours int `json:"align_shift_hours,omitempty"`
// SupplierBucket is the supplier's original BucketEnd before alignment,
// for the "供方 19:00(对齐 −1h)" hint. 0 when there's no supplier side.
SupplierBucket int64 `json:"supplier_bucket,omitempty"`
Regions []string `json:"regions,omitempty"`
}

// ByModelKind is one token-kind row inside ByModel.
Expand All @@ -86,6 +111,10 @@ type ByModelStat struct {
SupplierAmountCNY float64 `json:"supplier_amount_cny"`
LocalAmountCNY float64 `json:"local_amount_cny"`
DeltaAmountCNY float64 `json:"delta_amount_cny"`
// DiffKind summarises this model's significant detail rows (v3.1 §13.3):
// price_only / usage / missing_local / missing_supplier / mixed, or empty
// when the model has no significant difference.
DiffKind string `json:"diff_kind,omitempty"`
}

// ParseError describes one bad row inside the uploaded xlsx.
Expand Down
Loading