feat(report): close v2 headline and coverage gaps
This commit is contained in:
295
scripts/report_event_coverage.go
Normal file
295
scripts/report_event_coverage.go
Normal file
@@ -0,0 +1,295 @@
|
||||
//go:build llm_script
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type CoverageDay struct {
|
||||
Date string
|
||||
OfficialRelease bool
|
||||
PromoCampaign bool
|
||||
NewModel bool
|
||||
PriceChange bool
|
||||
}
|
||||
|
||||
func (d CoverageDay) HasTrueEvent() bool {
|
||||
return d.OfficialRelease || d.PromoCampaign || d.NewModel || d.PriceChange
|
||||
}
|
||||
|
||||
func (d CoverageDay) EventLabels() []string {
|
||||
var labels []string
|
||||
if d.OfficialRelease {
|
||||
labels = append(labels, "official_release")
|
||||
}
|
||||
if d.PromoCampaign {
|
||||
labels = append(labels, "promo_campaign")
|
||||
}
|
||||
if d.NewModel {
|
||||
labels = append(labels, "new_model")
|
||||
}
|
||||
if d.PriceChange {
|
||||
labels = append(labels, "price_change")
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
type CoverageSummary struct {
|
||||
TotalDays int
|
||||
CoveredDays int
|
||||
CoveragePct float64
|
||||
}
|
||||
|
||||
type CoveragePromoDefinition struct {
|
||||
Date string `json:"date"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 3 {
|
||||
fmt.Fprintln(os.Stderr, "用法: go run -tags llm_script scripts/report_event_coverage.go YYYY-MM-DD YYYY-MM-DD")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
startDate, endDate, err := parseCoverageArgs(os.Args[1], os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "参数错误:", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
dsn := os.Getenv("DATABASE_URL")
|
||||
if dsn == "" {
|
||||
dsn = "postgres://long@/llm_intelligence?host=/var/run/postgresql"
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "连接数据库失败:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
days, err := collectCoverageDays(db, startDate, endDate)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "统计覆盖率失败:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(days) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "指定范围内没有已生成的日报记录")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
summary := summarizeCoverage(days)
|
||||
fmt.Printf("V2 事件覆盖率报告 [%s, %s]\n", startDate, endDate)
|
||||
for _, day := range days {
|
||||
status := "MISS"
|
||||
if day.HasTrueEvent() {
|
||||
status = "PASS"
|
||||
}
|
||||
fmt.Printf("%s\t%s\t%s\n", day.Date, status, strings.Join(day.EventLabels(), ", "))
|
||||
}
|
||||
fmt.Printf("\n总日报天数: %d\n", summary.TotalDays)
|
||||
fmt.Printf("命中真实变化事件天数: %d\n", summary.CoveredDays)
|
||||
fmt.Printf("覆盖率: %.2f%%\n", summary.CoveragePct)
|
||||
|
||||
if summary.CoveragePct < 80 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func parseCoverageArgs(start, end string) (string, string, error) {
|
||||
startDate, err := time.Parse("2006-01-02", start)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("开始日期格式不正确")
|
||||
}
|
||||
endDate, err := time.Parse("2006-01-02", end)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("结束日期格式不正确")
|
||||
}
|
||||
if endDate.Before(startDate) {
|
||||
return "", "", fmt.Errorf("结束日期不能早于开始日期")
|
||||
}
|
||||
return startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nil
|
||||
}
|
||||
|
||||
func collectCoverageDays(db *sql.DB, startDate, endDate string) ([]CoverageDay, error) {
|
||||
reportDates, err := loadGeneratedReportDates(db, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
promoDates, err := loadCoveragePromoDates()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var days []CoverageDay
|
||||
for _, date := range reportDates {
|
||||
officialRelease, err := hasOfficialRelease(db, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newModel, err := hasNewModel(db, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
priceChange, err := hasPriceChange(db, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
days = append(days, CoverageDay{
|
||||
Date: date,
|
||||
OfficialRelease: officialRelease,
|
||||
PromoCampaign: promoDates[date],
|
||||
NewModel: newModel,
|
||||
PriceChange: priceChange,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(days, func(i, j int) bool {
|
||||
return days[i].Date < days[j].Date
|
||||
})
|
||||
return days, nil
|
||||
}
|
||||
|
||||
func loadGeneratedReportDates(db *sql.DB, startDate, endDate string) ([]string, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT DISTINCT report_date::text
|
||||
FROM daily_report
|
||||
WHERE report_date BETWEEN $1::date AND $2::date
|
||||
AND status = 'generated'
|
||||
ORDER BY report_date
|
||||
`, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var dates []string
|
||||
for rows.Next() {
|
||||
var date string
|
||||
if err := rows.Scan(&date); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dates = append(dates, date)
|
||||
}
|
||||
return dates, rows.Err()
|
||||
}
|
||||
|
||||
func hasOfficialRelease(db *sql.DB, date string) (bool, error) {
|
||||
var exists bool
|
||||
err := db.QueryRow(`
|
||||
WITH latest_prices AS (
|
||||
SELECT
|
||||
rp.model_id,
|
||||
COALESCE(o.type, 'reseller') AS operator_type,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY rp.model_id
|
||||
ORDER BY rp.effective_date DESC NULLS LAST, rp.id DESC
|
||||
) AS rn
|
||||
FROM region_pricing rp
|
||||
LEFT JOIN operator o ON rp.operator_id = o.id
|
||||
)
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM models m
|
||||
LEFT JOIN latest_prices lp ON lp.model_id = m.id AND lp.rn = 1
|
||||
WHERE m.deleted_at IS NULL
|
||||
AND m.release_date = $1::date
|
||||
AND COALESCE(m.source_url, '') <> ''
|
||||
AND COALESCE(lp.operator_type, 'reseller') IN ('official', 'cloud')
|
||||
)
|
||||
`, date).Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func hasNewModel(db *sql.DB, date string) (bool, error) {
|
||||
var exists bool
|
||||
err := db.QueryRow(`
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM models
|
||||
WHERE deleted_at IS NULL
|
||||
AND DATE(created_at) = $1::date
|
||||
)
|
||||
`, date).Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func hasPriceChange(db *sql.DB, date string) (bool, error) {
|
||||
var exists bool
|
||||
err := db.QueryRow(`
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM pricing_history
|
||||
WHERE DATE(changed_at) = $1::date
|
||||
AND (
|
||||
COALESCE(old_input_price, 0) <> COALESCE(new_input_price, 0)
|
||||
OR COALESCE(old_output_price, 0) <> COALESCE(new_output_price, 0)
|
||||
)
|
||||
)
|
||||
`, date).Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func loadCoveragePromoDates() (map[string]bool, error) {
|
||||
path, err := resolveCoveragePromoDataPath()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return map[string]bool{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var definitions []CoveragePromoDefinition
|
||||
if err := json.Unmarshal(body, &definitions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dates := make(map[string]bool, len(definitions))
|
||||
for _, definition := range definitions {
|
||||
if strings.TrimSpace(definition.Date) != "" {
|
||||
dates[definition.Date] = true
|
||||
}
|
||||
}
|
||||
return dates, nil
|
||||
}
|
||||
|
||||
func resolveCoveragePromoDataPath() (string, error) {
|
||||
candidates := []string{
|
||||
filepath.Join("scripts", "testdata", "report_promo_campaigns.json"),
|
||||
filepath.Join("testdata", "report_promo_campaigns.json"),
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
|
||||
func summarizeCoverage(days []CoverageDay) CoverageSummary {
|
||||
summary := CoverageSummary{TotalDays: len(days)}
|
||||
for _, day := range days {
|
||||
if day.HasTrueEvent() {
|
||||
summary.CoveredDays++
|
||||
}
|
||||
}
|
||||
if summary.TotalDays > 0 {
|
||||
summary.CoveragePct = float64(summary.CoveredDays) * 100 / float64(summary.TotalDays)
|
||||
}
|
||||
return summary
|
||||
}
|
||||
Reference in New Issue
Block a user