audit-2026-09-21.md 里的后端条目落地。每条都配了回归守卫,并逐条做过变异验证 (注入原 bug → 确认按正确理由变红 → 还原 → 变绿)。 安全 - CSRF 只加同源 Origin/Referer 守卫(csrf.go),不动模板 - 会话 cookie 的 Secure 按请求判断;两步验证两闸(loginGuard + 验证码一次性) - 跳转收口到 safeInternalPath;clientIP 只认 XFF 最后一段 - GPX 按属主圈定且查 RowsAffected;下载名走 downloadDisposition - 上传口统一 MaxBytesReader;落盘后的失败路径清掉半成品 数据一致性 - 区域判定两端各过一遍再 AND(apiTrack 与 movementsWhere 对齐) - Filter.Verdict 全域一致(下推到 visitsWhere);聚合刷新必须返回 error - 附属数据改挂坐标键,Go 与 SQL 两套表达式逐位一致 - 脏备份的伪时间/越界坐标不再进领域模型;rows 循环统一 drainRows - 合并与导出:writeRawRow 上抛错误、悬空引用清理、原生列补全 性能 - placeCols 相关子查询带 dataset_id 前导列;findSourceArchive 先按大小筛 - 批量勾选改 ANY($3::bigint[]);fillupCache 加上限;同一查询不再查两遍 另:新增「全新库」冒烟测试 —— 空库跑完 Migrate 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
160 lines
5.7 KiB
Go
160 lines
5.7 KiB
Go
package ingest
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
"rond-with-you/internal/geo"
|
|
"rond-with-you/internal/rond"
|
|
)
|
|
|
|
// querier 让回填既能跑在导入事务里,也能跑在启动时的连接池上。
|
|
type querier interface {
|
|
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
|
|
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
|
|
}
|
|
|
|
// BackfillCoordSource 给「有 ZLOCATION 留档」的地点补坐标来源,并自愈两类历史错误。
|
|
//
|
|
// 判据不能是「有留档行」——站点导出的备份会给新建地点合成 ZLOCATION 行,把
|
|
// ZRAWLATITUDE 写成 GCJ02ToWGS84(导出那一刻的显示坐标)。这份包被回导之后,合成行
|
|
// 会被误当成真机的原始 GPS,于是照片导入的点被标成「rond 导入」。区分办法就是这条
|
|
// 往返关系本身:
|
|
//
|
|
// ZRAWLATITUDE == GCJ02ToWGS84(ZLATITUDE) (位级相等,因为值就是我们自己算的)
|
|
//
|
|
// 真机行的 ZRAW 是独立采样值,从不满足(实测基线 321 行零命中)。判据必须拿留档行
|
|
// **自己的** ZLATITUDE 去比,而不是 places.lat——后者会被后来的坐标编辑改掉。
|
|
//
|
|
// 顺带修一处历史笔误:早期回填把 src_lon 写成了显示经度 ZLONGITUDE,应为原始经度
|
|
// ZRAWLONGITUDE。按「src_lon 恰好等于显示经度」精确识别后改写,不碰用户手工订正过的值。
|
|
func BackfillCoordSource(ctx context.Context, q querier, datasetID int64) (int, error) {
|
|
rows, err := q.QueryContext(ctx, `SELECT p.src_pk, COALESCE(p.coord_source,''), p.src_lat, p.src_lon,
|
|
`+rond.RawFloat8(`r.raw->>'ZLATITUDE'`)+`, `+rond.RawFloat8(`r.raw->>'ZLONGITUDE'`)+`,
|
|
`+rond.RawFloat8(`r.raw->>'ZRAWLATITUDE'`)+`, `+rond.RawFloat8(`r.raw->>'ZRAWLONGITUDE'`)+`
|
|
FROM places p JOIN entity_raw r
|
|
ON r.dataset_id=p.dataset_id AND r.entity='ZLOCATION' AND r.src_pk=p.src_pk
|
|
WHERE p.dataset_id=$1
|
|
AND (r.raw->>'ZLATITUDE') ~ '`+rond.RawFloatPattern+`'
|
|
AND (r.raw->>'ZLONGITUDE') ~ '`+rond.RawFloatPattern+`'
|
|
AND (r.raw->>'ZRAWLATITUDE') ~ '`+rond.RawFloatPattern+`'
|
|
AND (r.raw->>'ZRAWLONGITUDE') ~ '`+rond.RawFloatPattern+`'`, datasetID)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("读留档行: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var fillPK []int64
|
|
var fillLat, fillLon []float64
|
|
var clearPK, fixPK []int64
|
|
var fixLat, fixLon []float64
|
|
|
|
for rows.Next() {
|
|
var pk int64
|
|
var source string
|
|
var srcLat, srcLon sql.NullFloat64
|
|
var zlat, zlon, rawLat, rawLon float64
|
|
if err := rows.Scan(&pk, &source, &srcLat, &srcLon, &zlat, &zlon, &rawLat, &rawLon); err != nil {
|
|
return 0, err
|
|
}
|
|
gLat, gLon := geo.GCJ02ToWGS84(zlat, zlon)
|
|
if gLat == zlat && gLon == zlon {
|
|
// 境外点不做 GCJ 偏移,ZRAW 与显示坐标本来就该相等。可合成行写的是
|
|
// GCJ02ToWGS84(显示坐标),在境外同样是恒等——两边长得一模一样,判不出来,
|
|
// 干脆不碰:宁可留空,也别把真机的境外点清掉。
|
|
continue
|
|
}
|
|
if gLat == rawLat && gLon == rawLon {
|
|
// 合成行:早期回填按「有留档」误判成了 rond,撤回它写下的那份
|
|
if source == "rond" && srcLat.Valid && srcLat.Float64 == rawLat {
|
|
clearPK = append(clearPK, pk)
|
|
}
|
|
continue
|
|
}
|
|
switch {
|
|
case source == "":
|
|
fillPK = append(fillPK, pk)
|
|
fillLat = append(fillLat, rawLat)
|
|
fillLon = append(fillLon, rawLon)
|
|
case source == "rond" && srcLon.Valid && srcLon.Float64 == zlon && srcLon.Float64 != rawLon:
|
|
fixPK = append(fixPK, pk)
|
|
fixLat = append(fixLat, rawLat)
|
|
fixLon = append(fixLon, rawLon)
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
changed := 0
|
|
if len(fillPK) > 0 {
|
|
res, err := q.ExecContext(ctx, `UPDATE places p SET coord_source='rond', coord_sys='wgs84',
|
|
src_lat=v.rlat, src_lon=v.rlon
|
|
FROM (SELECT unnest($2::bigint[]) AS pk, unnest($3::float8[]) AS rlat,
|
|
unnest($4::float8[]) AS rlon) v
|
|
WHERE p.dataset_id=$1 AND p.src_pk=v.pk AND COALESCE(p.coord_source,'')=''`,
|
|
datasetID, fillPK, fillLat, fillLon)
|
|
if err != nil {
|
|
return changed, fmt.Errorf("补坐标来源: %w", err)
|
|
}
|
|
if n, _ := res.RowsAffected(); n > 0 {
|
|
changed += int(n)
|
|
}
|
|
}
|
|
if len(fixPK) > 0 {
|
|
res, err := q.ExecContext(ctx, `UPDATE places p SET src_lat=v.rlat, src_lon=v.rlon
|
|
FROM (SELECT unnest($2::bigint[]) AS pk, unnest($3::float8[]) AS rlat,
|
|
unnest($4::float8[]) AS rlon) v
|
|
WHERE p.dataset_id=$1 AND p.src_pk=v.pk`,
|
|
datasetID, fixPK, fixLat, fixLon)
|
|
if err != nil {
|
|
return changed, fmt.Errorf("修正原始坐标: %w", err)
|
|
}
|
|
if n, _ := res.RowsAffected(); n > 0 {
|
|
changed += int(n)
|
|
}
|
|
}
|
|
if len(clearPK) > 0 {
|
|
res, err := q.ExecContext(ctx, `UPDATE places SET coord_source=NULL, coord_sys=NULL,
|
|
src_lat=NULL, src_lon=NULL
|
|
WHERE dataset_id=$1 AND src_pk=ANY($2::bigint[]) AND coord_source='rond'`,
|
|
datasetID, clearPK)
|
|
if err != nil {
|
|
return changed, fmt.Errorf("撤回误判的来源: %w", err)
|
|
}
|
|
if n, _ := res.RowsAffected(); n > 0 {
|
|
changed += int(n)
|
|
}
|
|
}
|
|
return changed, nil
|
|
}
|
|
|
|
// BackfillAllCoordSource 对所有数据集跑一遍回填,供启动时自愈历史数据用。
|
|
// 单个数据集失败不阻断启动——留档是增强,不该挡住服务。
|
|
func BackfillAllCoordSource(ctx context.Context, d *sql.DB) error {
|
|
rows, err := d.QueryContext(ctx, `SELECT id FROM datasets ORDER BY id`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var ids []int64
|
|
for rows.Next() {
|
|
var id int64
|
|
if err := rows.Scan(&id); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
rows.Close()
|
|
if err := rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
for _, id := range ids {
|
|
if _, err := BackfillCoordSource(ctx, d, id); err != nil {
|
|
return fmt.Errorf("数据集 %d: %w", id, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|