refactor(go): make daily metric totals opt in
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
- 原因:日统计是正式业务指标,应只统计已经定位到 VIN 的车辆。
|
- 原因:日统计是正式业务指标,应只统计已经定位到 VIN 的车辆。
|
||||||
- 无 VIN 的 JT808 等数据保留在 raw/Redis/session 中用于排查和待绑定,不进入正式车辆指标。
|
- 无 VIN 的 JT808 等数据保留在 raw/Redis/session 中用于排查和待绑定,不进入正式车辆指标。
|
||||||
- 状态:schema 使用 `PRIMARY KEY (vin, stat_date, protocol)`,不保留自增 `id` 和 `created_at`;首末总里程和样本数保留为日里程计算状态。
|
- 状态:schema 使用 `PRIMARY KEY (vin, stat_date, protocol)`,不保留自增 `id` 和 `created_at`;首末总里程和样本数保留为日里程计算状态。
|
||||||
|
- 查询:`/api/stats/daily-metrics` 默认不执行 `COUNT(*)`,`total` 表示本页返回条数;只有报表总页数等场景传 `includeTotal=true`。
|
||||||
- 生产:RDS 已在上线前删除泛化 `vehicle_daily_metric`,改为专用 `vehicle_daily_mileage`。
|
- 生产:RDS 已在上线前删除泛化 `vehicle_daily_metric`,改为专用 `vehicle_daily_mileage`。
|
||||||
|
|
||||||
4. `vehicle_locations` 不再接收临时身份兜底字段。
|
4. `vehicle_locations` 不再接收临时身份兜底字段。
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ type Queryer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MetricQuery struct {
|
type MetricQuery struct {
|
||||||
VIN string
|
VIN string
|
||||||
Protocol string
|
Protocol string
|
||||||
DateFrom string
|
IncludeTotal bool
|
||||||
DateTo string
|
DateFrom string
|
||||||
Limit int
|
DateTo string
|
||||||
Offset int
|
Limit int
|
||||||
|
Offset int
|
||||||
}
|
}
|
||||||
|
|
||||||
type MetricRow struct {
|
type MetricRow struct {
|
||||||
@@ -184,16 +185,22 @@ func (h *MetricHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeMetricError(w, http.StatusBadRequest, err.Error())
|
writeMetricError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, err := h.repository.Count(r.Context(), query)
|
var total int64
|
||||||
if err != nil {
|
if query.IncludeTotal {
|
||||||
writeMetricError(w, http.StatusInternalServerError, err.Error())
|
total, err = h.repository.Count(r.Context(), query)
|
||||||
return
|
if err != nil {
|
||||||
|
writeMetricError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
rows, err := h.repository.Query(r.Context(), query)
|
rows, err := h.repository.Query(r.Context(), query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeMetricError(w, http.StatusInternalServerError, err.Error())
|
writeMetricError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !query.IncludeTotal {
|
||||||
|
total = int64(len(rows))
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"items": rows,
|
"items": rows,
|
||||||
@@ -214,12 +221,13 @@ func parseMetricQuery(r *http.Request) (MetricQuery, error) {
|
|||||||
return MetricQuery{}, err
|
return MetricQuery{}, err
|
||||||
}
|
}
|
||||||
query := MetricQuery{
|
query := MetricQuery{
|
||||||
VIN: values.Get("vin"),
|
VIN: values.Get("vin"),
|
||||||
Protocol: values.Get("protocol"),
|
Protocol: values.Get("protocol"),
|
||||||
DateFrom: values.Get("dateFrom"),
|
IncludeTotal: strings.EqualFold(strings.TrimSpace(values.Get("includeTotal")), "true"),
|
||||||
DateTo: values.Get("dateTo"),
|
DateFrom: values.Get("dateFrom"),
|
||||||
Limit: limit,
|
DateTo: values.Get("dateTo"),
|
||||||
Offset: offset,
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
}
|
}
|
||||||
if !validDate(query.DateFrom) || !validDate(query.DateTo) {
|
if !validDate(query.DateFrom) || !validDate(query.DateTo) {
|
||||||
return MetricQuery{}, errors.New("dateFrom/dateTo must use YYYY-MM-DD")
|
return MetricQuery{}, errors.New("dateFrom/dateTo must use YYYY-MM-DD")
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func TestMetricHandlerReturnsDailyMetrics(t *testing.T) {
|
|||||||
))
|
))
|
||||||
|
|
||||||
handler := NewMetricHandler(NewMetricRepository(db))
|
handler := NewMetricHandler(NewMetricRepository(db))
|
||||||
request := httptest.NewRequest(http.MethodGet, "/api/stats/daily-metrics?vin=LB9A32A21R0LS1707&protocol=GB32960&dateFrom=2020-07-01&dateTo=2020-07-01", nil)
|
request := httptest.NewRequest(http.MethodGet, "/api/stats/daily-metrics?vin=LB9A32A21R0LS1707&protocol=GB32960&dateFrom=2020-07-01&dateTo=2020-07-01&includeTotal=true", nil)
|
||||||
response := httptest.NewRecorder()
|
response := httptest.NewRecorder()
|
||||||
|
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
@@ -103,9 +103,6 @@ func TestMetricHandlerReturnsEmptyItemsArrayWhenNoRows(t *testing.T) {
|
|||||||
t.Fatalf("sqlmock.New() error = %v", err)
|
t.Fatalf("sqlmock.New() error = %v", err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM vehicle_daily_mileage").
|
|
||||||
WithArgs("YUTONG_MQTT").
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(0))
|
|
||||||
mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage").
|
mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage").
|
||||||
WithArgs("YUTONG_MQTT", 50, 0).
|
WithArgs("YUTONG_MQTT", 50, 0).
|
||||||
WillReturnRows(sqlmock.NewRows([]string{
|
WillReturnRows(sqlmock.NewRows([]string{
|
||||||
@@ -132,6 +129,41 @@ func TestMetricHandlerReturnsEmptyItemsArrayWhenNoRows(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMetricHandlerSkipsTotalCountByDefault(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sqlmock.New() error = %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage").
|
||||||
|
WithArgs("JT808", 1, 0).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{
|
||||||
|
"vin", "stat_date", "protocol", "daily_mileage_km",
|
||||||
|
"first_total_mileage_km", "latest_total_mileage_km", "sample_count",
|
||||||
|
"updated_at",
|
||||||
|
}).AddRow(
|
||||||
|
"LKLG7C4E3NA774736", "2026-07-02", "JT808", 12.3,
|
||||||
|
8792.8, 8805.1, 30, "2026-07-02 23:59:59",
|
||||||
|
))
|
||||||
|
|
||||||
|
handler := NewMetricHandler(NewMetricRepository(db))
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/stats/daily-metrics?protocol=JT808&limit=1", nil)
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(response, request)
|
||||||
|
|
||||||
|
if response.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
|
||||||
|
}
|
||||||
|
body := response.Body.String()
|
||||||
|
if !strings.Contains(body, `"total":1`) {
|
||||||
|
t.Fatalf("response should use page size as total when total count is not requested: %s", body)
|
||||||
|
}
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("sql expectations: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMetricHandlerRejectsInvalidPagination(t *testing.T) {
|
func TestMetricHandlerRejectsInvalidPagination(t *testing.T) {
|
||||||
handler := NewMetricHandler(NewMetricRepository(&sql.DB{}))
|
handler := NewMetricHandler(NewMetricRepository(&sql.DB{}))
|
||||||
request := httptest.NewRequest(http.MethodGet, "/api/stats/daily-metrics?limit=2001", nil)
|
request := httptest.NewRequest(http.MethodGet, "/api/stats/daily-metrics?limit=2001", nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user