feat(admin): 运营后台(登录/查看/审核编辑/补全)+ 写入API + 审计留痕
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
package adminstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// Quality weights mirror ingestion/opengoods/etl/quality.py.
|
||||
const (
|
||||
wCompleteness = 0.4
|
||||
wSourceTrust = 0.3
|
||||
wAgreement = 0.2
|
||||
wFreshness = 0.1
|
||||
)
|
||||
|
||||
// queryer is satisfied by both *pgxpool.Pool and pgx.Tx.
|
||||
type queryer interface {
|
||||
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
|
||||
}
|
||||
|
||||
func agreementFromSources(n int) float64 {
|
||||
switch {
|
||||
case n <= 1:
|
||||
return 0.5
|
||||
case n == 2:
|
||||
return 0.8
|
||||
default:
|
||||
return 1.0
|
||||
}
|
||||
}
|
||||
|
||||
func freshnessFromAge(ageDays *float64) float64 {
|
||||
if ageDays == nil {
|
||||
return 0.5
|
||||
}
|
||||
d := *ageDays
|
||||
switch {
|
||||
case d <= 30:
|
||||
return 1.0
|
||||
case d <= 180:
|
||||
return 0.8
|
||||
case d <= 365:
|
||||
return 0.6
|
||||
case d <= 730:
|
||||
return 0.4
|
||||
default:
|
||||
return 0.2
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) computeQuality(ctx context.Context, q queryer, productID string) (float64, error) {
|
||||
var name, country *string
|
||||
var gtin *string
|
||||
var brandID, categoryID *string
|
||||
var netCanonical *float64
|
||||
var ingredients *string
|
||||
var hasNutri, hasImage bool
|
||||
err := q.QueryRow(ctx, `
|
||||
SELECT p.name, p.gtin, p.brand_id, p.category_id, p.net_content_canonical,
|
||||
p.country_of_origin, f.ingredients_text,
|
||||
(f.nutriments IS NOT NULL AND f.nutriments::text <> '{}'),
|
||||
EXISTS (SELECT 1 FROM product_image pi WHERE pi.product_id = p.id)
|
||||
FROM product p LEFT JOIN food_detail f ON f.product_id = p.id
|
||||
WHERE p.id = $1`, productID).Scan(
|
||||
&name, >in, &brandID, &categoryID, &netCanonical, &country,
|
||||
&ingredients, &hasNutri, &hasImage)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
present := 0
|
||||
bump := func(ok bool) {
|
||||
if ok {
|
||||
present++
|
||||
}
|
||||
}
|
||||
bump(name != nil && *name != "")
|
||||
bump(gtin != nil && *gtin != "")
|
||||
bump(brandID != nil)
|
||||
bump(categoryID != nil)
|
||||
bump(netCanonical != nil)
|
||||
bump(country != nil && *country != "")
|
||||
bump(hasNutri)
|
||||
bump(ingredients != nil && *ingredients != "")
|
||||
bump(hasImage)
|
||||
completeness := float64(present) / float64(len(CompletenessFields))
|
||||
|
||||
var sourceCount int
|
||||
var sourceTrust *float64
|
||||
var lastFetched *time.Time
|
||||
err = q.QueryRow(ctx, `
|
||||
SELECT count(DISTINCT ps.source_id), COALESCE(max(s.trust_weight),0), max(ps.fetched_at)
|
||||
FROM product_source ps LEFT JOIN source s ON s.id = ps.source_id
|
||||
WHERE ps.product_id = $1`, productID).Scan(&sourceCount, &sourceTrust, &lastFetched)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
trust := 0.0
|
||||
if sourceTrust != nil {
|
||||
trust = *sourceTrust
|
||||
}
|
||||
|
||||
var ageDays *float64
|
||||
if lastFetched != nil {
|
||||
d := time.Since(*lastFetched).Hours() / 24.0
|
||||
if d < 0 {
|
||||
d = 0
|
||||
}
|
||||
ageDays = &d
|
||||
}
|
||||
|
||||
raw := wCompleteness*completeness + wSourceTrust*trust +
|
||||
wAgreement*agreementFromSources(sourceCount) + wFreshness*freshnessFromAge(ageDays)
|
||||
raw = math.Max(0, math.Min(1, raw))
|
||||
return math.Round(raw*1000) / 1000, nil
|
||||
}
|
||||
|
||||
func (s *Store) recomputeQualityTx(ctx context.Context, tx pgx.Tx, productID string) (float64, error) {
|
||||
v, err := s.computeQuality(ctx, tx, productID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_, err = tx.Exec(ctx, "UPDATE product SET quality_score=$1 WHERE id=$2", v, productID)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (s *Store) recomputeQuality(ctx context.Context, productID string) (float64, error) {
|
||||
v, err := s.computeQuality(ctx, s.pool, productID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_, err = s.pool.Exec(ctx, "UPDATE product SET quality_score=$1 WHERE id=$2", v, productID)
|
||||
return v, err
|
||||
}
|
||||
Reference in New Issue
Block a user