专题显示区改为一组圆(后端与查询层)
把专题的「一个圆」换成「一组圆」(areas jsonb 数组):落在任一圆内即算在 区域内(SQL 里的 OR)。这一批只动后端与查询层,前台渲染与后台地图编辑器 分别跟进;后台表单暂时沿用原来的三个坐标输入框,保存时合成一个圆写进 areas,所以现有交互不受影响。 - topics.areas JSONB + 存量回填:老的单圆搬进去,之后 areas 是唯一权威。 废弃的 center_lat/center_lon/radius_km 保留(写入时镜像第一个圆), 一是回滚旧二进制时还能退化显示成第一个圆,二是回填的输入。 - Topic.AreaList() 归一:Areas 为空时退回老的三列,未回填的老行与直接构造 Topic 的调用方都继续工作。 - topicAreaCond 改成 N 个圆的 OR;每个圆占 3 个占位符,编号纪律照旧 (写错就是「专题内容全变 0」那种难查的事故)。 - fitParams 改用所有圆的并集外接矩形:多圆之后「圆心+半径」表达不了, 统一走 fitBBox;单圆时外接矩形就是这个圆的外接正方形,与原路径等价。 - 坐标系:编辑器提交的圆心若是 WGS-84 底图上点的,存前换算成 GCJ-02 (地点过滤要拿它跟 places 的 GCJ-02 坐标比距离)。 测试: - 表单解析(两个入口同一套校验)、wgs84 换算、半径为 0/空 = 不限区域、 坏 JSON/越界必须报错、上限 20 个。 - 占位符纪律:本段引用的 $n 必须恰好是 [prefix+1, len(args)] 连续区间, 最大值必须等于参数个数。 - 多圆并集语义(DB 门控):两圆专题的地点集合必须恰好等于两个单圆专题的 并集 —— 定义性不变量,变异验证过(只认第一个圆 ⇒ 两个用例同时红)。
This commit is contained in:
+48
-8
@@ -1572,6 +1572,15 @@ func dict(kv ...any) map[string]any {
|
||||
|
||||
// ---------- 专题 ----------
|
||||
|
||||
// TopicArea 是专题的一个圆形显示区(圆心 + 半径,半径单位公里)。
|
||||
// 坐标是 **GCJ-02**(与 places 同系):地点过滤是拿它和 p.lat/p.lon 直接比球面距离的,
|
||||
// 前台地图侧再由 rond-map.js 的 project() 换算到底图坐标系。存成底图坐标就会整体偏几百米。
|
||||
type TopicArea struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
RadiusKm float64 `json:"radiusKm"`
|
||||
}
|
||||
|
||||
// Topic 是一个行程专题:某段时间、某个区域内的足迹打包展示。
|
||||
type Topic struct {
|
||||
ID int64
|
||||
@@ -1585,12 +1594,16 @@ type Topic struct {
|
||||
EndDate string
|
||||
// RangeLabel 是展示用的时间范围文案
|
||||
RangeLabel string
|
||||
CenterLat float64
|
||||
CenterLon float64
|
||||
RadiusKm float64
|
||||
ShowFog bool
|
||||
Enabled bool
|
||||
SortOrder int
|
||||
// Areas 是显示区(一组圆),空表示不限区域。
|
||||
Areas []TopicArea
|
||||
// CenterLat / CenterLon / RadiusKm **已废弃**:早期只支持一个圆时的那三列。
|
||||
// 保留只为兼容「还没回填的老行」与「测试里直接构造 Topic 的调用方」——新代码一律走 Areas。
|
||||
CenterLat float64
|
||||
CenterLon float64
|
||||
RadiusKm float64
|
||||
ShowFog bool
|
||||
Enabled bool
|
||||
SortOrder int
|
||||
// PlaceCount / VisitCount / DayCount / CityCount / DwellMin 由内容统计得出
|
||||
PlaceCount int
|
||||
VisitCount int
|
||||
@@ -1599,8 +1612,35 @@ type Topic struct {
|
||||
DwellMin int64
|
||||
}
|
||||
|
||||
// HasArea 报告专题是否限定了区域(中心点 + 半径)。
|
||||
func (t Topic) HasArea() bool { return t.RadiusKm > 0 && (t.CenterLat != 0 || t.CenterLon != 0) }
|
||||
// AreaList 归一显示区:Areas 为空时退回老的单圆。
|
||||
// 这样「还没回填的老行」和「测试里直接构造的 Topic」都继续工作,
|
||||
// 不用要求每个调用方自己记着处理两套字段。
|
||||
func (t Topic) AreaList() []TopicArea {
|
||||
if len(t.Areas) > 0 {
|
||||
return t.Areas
|
||||
}
|
||||
if t.RadiusKm > 0 && (t.CenterLat != 0 || t.CenterLon != 0) {
|
||||
return []TopicArea{{Lat: t.CenterLat, Lon: t.CenterLon, RadiusKm: t.RadiusKm}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasArea 报告专题是否限定了显示区。
|
||||
func (t Topic) HasArea() bool { return len(t.AreaList()) > 0 }
|
||||
|
||||
// AreasJSON 把显示区序列化成内联给地图脚本的 JSON(不限区域时是 `[]`,前端按 length 判)。
|
||||
// 走 AreaList() 而不是裸 Areas:保证「HasArea 为真」与「脚本拿得到圆」永远同一份判据。
|
||||
func (t Topic) AreasJSON() template.JS {
|
||||
areas := t.AreaList()
|
||||
if len(areas) == 0 {
|
||||
return template.JS("[]")
|
||||
}
|
||||
b, err := json.Marshal(areas)
|
||||
if err != nil {
|
||||
return template.JS("[]")
|
||||
}
|
||||
return template.JS(b)
|
||||
}
|
||||
|
||||
// TopicAdminData 是后台专题管理页的数据。
|
||||
type TopicAdminData struct {
|
||||
|
||||
Reference in New Issue
Block a user