update
This commit is contained in:
@ -10,6 +10,7 @@ type StreetSnapQuery struct {
|
|||||||
Page int
|
Page int
|
||||||
Size int // 服务端固定为 StreetSnapFixedSize,不从 query 读取
|
Size int // 服务端固定为 StreetSnapFixedSize,不从 query 读取
|
||||||
Year int // 单值年份,0 表示不过滤
|
Year int // 单值年份,0 表示不过滤
|
||||||
|
City string // 地区/城市(精确匹配 city 列),'' 表示不过滤
|
||||||
Sort string // image_count(默认)/ year_desc / year_asc
|
Sort string // image_count(默认)/ year_desc / year_asc
|
||||||
Images int // 服务端固定为 StreetSnapFixedImages,不从 query 读取
|
Images int // 服务端固定为 StreetSnapFixedImages,不从 query 读取
|
||||||
Locale string
|
Locale string
|
||||||
|
|||||||
@ -30,6 +30,7 @@ func NewStreetSnapHandler(svc service.StreetSnapService) *StreetSnapHandler {
|
|||||||
// 查询参数:
|
// 查询参数:
|
||||||
// - page 页码,默认 1(服务端固定每页 StreetSnapFixedSize=24 条)
|
// - page 页码,默认 1(服务端固定每页 StreetSnapFixedSize=24 条)
|
||||||
// - year 年份(整数),0 表示不过滤
|
// - year 年份(整数),0 表示不过滤
|
||||||
|
// - city 地区/城市(精确匹配 city 列),'' 表示不过滤
|
||||||
// - sort image_count(默认,按图片数降序)/ year_desc / year_asc
|
// - sort image_count(默认,按图片数降序)/ year_desc / year_asc
|
||||||
// - locale cn / en
|
// - locale cn / en
|
||||||
//
|
//
|
||||||
@ -51,6 +52,7 @@ func (h *StreetSnapHandler) parseListQuery(c *gin.Context) dto.StreetSnapQuery {
|
|||||||
Page: queryInt(c, "page", 1),
|
Page: queryInt(c, "page", 1),
|
||||||
Size: dto.StreetSnapFixedSize, // 服务端固定,前端不传
|
Size: dto.StreetSnapFixedSize, // 服务端固定,前端不传
|
||||||
Year: queryInt(c, "year", 0),
|
Year: queryInt(c, "year", 0),
|
||||||
|
City: c.DefaultQuery("city", ""),
|
||||||
Sort: c.DefaultQuery("sort", "image_count"),
|
Sort: c.DefaultQuery("sort", "image_count"),
|
||||||
Images: dto.StreetSnapFixedImages, // 服务端固定,前端不传
|
Images: dto.StreetSnapFixedImages, // 服务端固定,前端不传
|
||||||
Locale: parseLocale(c),
|
Locale: parseLocale(c),
|
||||||
|
|||||||
@ -8,6 +8,7 @@ type StreetSnap struct {
|
|||||||
ID uint32 `gorm:"primaryKey;column:id" json:"id"`
|
ID uint32 `gorm:"primaryKey;column:id" json:"id"`
|
||||||
Title string `gorm:"column:title" json:"title"`
|
Title string `gorm:"column:title" json:"title"`
|
||||||
Year uint16 `gorm:"column:year" json:"year"`
|
Year uint16 `gorm:"column:year" json:"year"`
|
||||||
|
City string `gorm:"column:city" json:"city"` // 地区/城市(从 title 第一段 \t 解析回填),'' 表示未归类
|
||||||
Cover string `gorm:"column:cover" json:"cover"`
|
Cover string `gorm:"column:cover" json:"cover"`
|
||||||
SourceURL string `gorm:"column:source_url" json:"source_url"`
|
SourceURL string `gorm:"column:source_url" json:"source_url"`
|
||||||
ImageCount uint16 `gorm:"column:image_count" json:"image_count"`
|
ImageCount uint16 `gorm:"column:image_count" json:"image_count"`
|
||||||
|
|||||||
@ -40,6 +40,9 @@ func snapFilterScope(q dto.StreetSnapQuery) func(*gorm.DB) *gorm.DB {
|
|||||||
if q.Year != 0 {
|
if q.Year != 0 {
|
||||||
db = db.Where("year = ?", q.Year)
|
db = db.Where("year = ?", q.Year)
|
||||||
}
|
}
|
||||||
|
if q.City != "" {
|
||||||
|
db = db.Where("city = ?", q.City)
|
||||||
|
}
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
206
scripts/backfill_city/main.go
Normal file
206
scripts/backfill_city/main.go
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
// Command backfill_city 给 street_snap 加 city 列并从 title(首选)/ source_url(兜底)回填。
|
||||||
|
//
|
||||||
|
// 项目已移除 AutoMigrate,表结构由外部 SQL 托管(见 scripts/sql/004_add_streetsnap_city.sql)。
|
||||||
|
// 本程序是「没有 mysql 客户端时」的执行器:
|
||||||
|
// - ALTER 走 ensureColumn:先查 information_schema,缺才加,可安全重跑(幂等)。
|
||||||
|
// - 回填:优先从 title 第一段(location,\t 分隔)按城市关键词匹配;
|
||||||
|
// 命中不到再退到 source_url / cover 的 URL slug 解析。幂等(已填过的不会覆盖)。
|
||||||
|
//
|
||||||
|
// 存储的 city 值即「规范英文名首字母大写」(Paris / New York …),
|
||||||
|
// 与前端街拍列表 City 筛选项 value、header 下拉 ?city= 参数完全一致(精确匹配)。
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dsn = "root:root@tcp(127.0.0.1:3306)/db_dev?charset=utf8mb4&parseTime=True&loc=Local"
|
||||||
|
|
||||||
|
// cityKeywords 城市关键词 → 规范存储值(首字母大写)。
|
||||||
|
// 顺序无关;extractCityFromTitle 用「包含」匹配,可容忍 "Paris Fashion Week" / "New York FW25" 等形式。
|
||||||
|
var cityKeywords = []struct {
|
||||||
|
kw string
|
||||||
|
city string
|
||||||
|
}{
|
||||||
|
{"paris", "Paris"},
|
||||||
|
{"milan", "Milan"},
|
||||||
|
{"london", "London"},
|
||||||
|
{"new york", "New York"},
|
||||||
|
{"nyfw", "New York"},
|
||||||
|
{"tokyo", "Tokyo"},
|
||||||
|
{"seoul", "Seoul"},
|
||||||
|
{"florence", "Florence"},
|
||||||
|
{"shanghai", "Shanghai"},
|
||||||
|
{"copenhagen", "Copenhagen"},
|
||||||
|
{"berlin", "Berlin"},
|
||||||
|
{"rome", "Rome"},
|
||||||
|
{"madrid", "Madrid"},
|
||||||
|
{"amsterdam", "Amsterdam"},
|
||||||
|
{"antwerp", "Antwerp"},
|
||||||
|
{"los angeles", "Los Angeles"},
|
||||||
|
{"moscow", "Moscow"},
|
||||||
|
{"mumbai", "Mumbai"},
|
||||||
|
{"beijing", "Beijing"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// citySlug(URL 兜底用):slug → 规范存储值。多词城市(new-york)必须排在单词组之前被 HasPrefix 命中。
|
||||||
|
var citySlug = map[string]string{
|
||||||
|
"paris": "Paris",
|
||||||
|
"milan": "Milan",
|
||||||
|
"london": "London",
|
||||||
|
"new-york": "New York",
|
||||||
|
"tokyo": "Tokyo",
|
||||||
|
"seoul": "Seoul",
|
||||||
|
"copenhagen": "Copenhagen",
|
||||||
|
"florence": "Florence",
|
||||||
|
"berlin": "Berlin",
|
||||||
|
"shanghai": "Shanghai",
|
||||||
|
"beijing": "Beijing",
|
||||||
|
"moscow": "Moscow",
|
||||||
|
"los-angeles": "Los Angeles",
|
||||||
|
"rome": "Rome",
|
||||||
|
"madrid": "Madrid",
|
||||||
|
"amsterdam": "Amsterdam",
|
||||||
|
"antwerp": "Antwerp",
|
||||||
|
"mumbai": "Mumbai",
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractCityFromTitle 从 title 第一段(location,\t 分隔)按关键词匹配规范城市名。
|
||||||
|
func extractCityFromTitle(title string) string {
|
||||||
|
seg := title
|
||||||
|
if i := strings.Index(title, "\t"); i >= 0 {
|
||||||
|
seg = title[:i]
|
||||||
|
}
|
||||||
|
segL := strings.ToLower(strings.TrimSpace(seg))
|
||||||
|
if segL == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, c := range cityKeywords {
|
||||||
|
if strings.Contains(segL, c.kw) {
|
||||||
|
return c.city
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractCityFromURL 兜底:从 source_url / cover 的 URL 第一段 slug 解析城市。
|
||||||
|
func extractCityFromURL(raws ...string) string {
|
||||||
|
for _, raw := range raws {
|
||||||
|
u := strings.TrimSpace(raw)
|
||||||
|
if u == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i := strings.Index(u, "://"); i >= 0 {
|
||||||
|
u = u[i+3:]
|
||||||
|
}
|
||||||
|
if i := strings.Index(u, "/"); i >= 0 {
|
||||||
|
u = u[i+1:]
|
||||||
|
}
|
||||||
|
u = strings.Trim(u, "/")
|
||||||
|
if u == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seg := strings.SplitN(u, "/", 2)[0]
|
||||||
|
segL := strings.ToLower(seg)
|
||||||
|
for slug, city := range citySlug {
|
||||||
|
if segL == slug || strings.HasPrefix(segL, slug+"-") {
|
||||||
|
return city
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("open:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) 幂等加列(与 004_add_streetsnap_city.sql 对齐)
|
||||||
|
ensureColumn(db, "street_snap", "city", "VARCHAR(128) NOT NULL DEFAULT '' AFTER year")
|
||||||
|
|
||||||
|
// 2) 逐行回填:优先 title 第一段,兜底 URL slug
|
||||||
|
type row struct {
|
||||||
|
ID uint32
|
||||||
|
Title string
|
||||||
|
SourceURL string
|
||||||
|
Cover string
|
||||||
|
}
|
||||||
|
var rows []row
|
||||||
|
if r := db.Raw("SELECT id, title, source_url, cover FROM street_snap WHERE is_deleted=0 AND (city IS NULL OR city='')").Scan(&rows); r.Error != nil {
|
||||||
|
log.Fatal("select:", r.Error)
|
||||||
|
}
|
||||||
|
var updated int64
|
||||||
|
for _, r := range rows {
|
||||||
|
city := extractCityFromTitle(r.Title)
|
||||||
|
if city == "" {
|
||||||
|
city = extractCityFromURL(r.SourceURL, r.Cover)
|
||||||
|
}
|
||||||
|
if city == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r2 := db.Exec("UPDATE street_snap SET city=? WHERE id=?", city, r.ID); r2.Error != nil {
|
||||||
|
log.Printf("update id=%d failed: %v", r.ID, r2.Error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updated++
|
||||||
|
}
|
||||||
|
fmt.Printf(" updated city for %d rows\n", updated)
|
||||||
|
|
||||||
|
// 3) 校验:输出 distinct city 分布 + 未归类样本
|
||||||
|
printCities(db)
|
||||||
|
printUnclassifiedSamples(db)
|
||||||
|
fmt.Println("backfill city done.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureColumn(db *gorm.DB, table, col, def string) {
|
||||||
|
var n int64
|
||||||
|
db.Raw("SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='db_dev' AND TABLE_NAME=? AND COLUMN_NAME=?", table, col).Scan(&n)
|
||||||
|
if n > 0 {
|
||||||
|
fmt.Printf(" skip %s.%s (exists)\n", table, col)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stmt := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, col, def)
|
||||||
|
fmt.Println(" +", stmt)
|
||||||
|
if r := db.Exec(stmt); r.Error != nil {
|
||||||
|
log.Fatalf("alter %s.%s failed: %v", table, col, r.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCities(db *gorm.DB) {
|
||||||
|
type row struct {
|
||||||
|
City string
|
||||||
|
Cnt int64
|
||||||
|
}
|
||||||
|
var rows []row
|
||||||
|
db.Raw(`SELECT city, COUNT(*) cnt FROM street_snap WHERE is_deleted=0 AND city<>'' GROUP BY city ORDER BY cnt DESC`).Scan(&rows)
|
||||||
|
fmt.Printf(" distinct cities: %d\n", len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
fmt.Printf(" %-14q %d\n", r.City, r.Cnt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printUnclassifiedSamples 输出仍未归类行的 title 第一段样本,便于后续补充城市词典。
|
||||||
|
func printUnclassifiedSamples(db *gorm.DB) {
|
||||||
|
type row struct {
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
var rows []row
|
||||||
|
db.Raw(`SELECT title FROM street_snap WHERE is_deleted=0 AND (city IS NULL OR city='') LIMIT 25`).Scan(&rows)
|
||||||
|
var total int64
|
||||||
|
db.Raw(`SELECT COUNT(*) FROM street_snap WHERE is_deleted=0 AND (city IS NULL OR city='')`).Scan(&total)
|
||||||
|
fmt.Printf(" unclassified rows: %d (sample locations below)\n", total)
|
||||||
|
for _, r := range rows {
|
||||||
|
loc := r.Title
|
||||||
|
if i := strings.Index(loc, "\t"); i >= 0 {
|
||||||
|
loc = loc[:i]
|
||||||
|
}
|
||||||
|
fmt.Printf(" loc=%-30q title=%-50q\n", loc, r.Title)
|
||||||
|
}
|
||||||
|
}
|
||||||
4
scripts/sql/004_add_streetsnap_city.sql
Normal file
4
scripts/sql/004_add_streetsnap_city.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
-- 街拍地区筛选:street_snap 新增 city 列(从 title 第一段 \t 解析回填)
|
||||||
|
-- 对应执行器 scripts/backfill_city/main.go(幂等,可重跑)
|
||||||
|
-- city 存「地点」原文(如 Paris / 巴黎 / New York),'' 表示未归类,不参与地区筛选
|
||||||
|
ALTER TABLE street_snap ADD COLUMN city VARCHAR(128) NOT NULL DEFAULT '' AFTER year;
|
||||||
Reference in New Issue
Block a user