<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>足迹 on CHEN 的博客</title>
        <link>https://blog.chen-api.cloud/tags/%E8%B6%B3%E8%BF%B9/</link>
        <description>Recent content in 足迹 on CHEN 的博客</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Fri, 11 Sep 2026 12:05:00 +0800</lastBuildDate><atom:link href="https://blog.chen-api.cloud/tags/%E8%B6%B3%E8%BF%B9/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>足迹后台的实现：设备互联、地点解析与自动发布</title>
            <link>https://blog.chen-api.cloud/posts/footprints-admin-tailscale/</link>
            <pubDate>Fri, 11 Sep 2026 12:05:00 +0800</pubDate>
            <guid>https://blog.chen-api.cloud/posts/footprints-admin-tailscale/</guid>
            <description>&lt;p&gt;上一篇文章里，我写了足迹公开页是怎么做出来的：Hugo 读取 &lt;code&gt;data/footprints.yaml&lt;/code&gt;，把地点数据嵌进页面，再交给 TypeScript 初始化腾讯地图、地点列表和照片灯箱。&lt;/p&gt;&#xA;&lt;p&gt;这篇讲它后面的管理功能。整个功能的核心入口不是公网域名，而是 Tailscale：我的手机、电脑和服务器登录同一个 Tailscale 账户，设备之间先通过 tailnet 互联，然后手机才能进入服务器上的足迹管理页面。这样做的好处是，管理入口不需要暴露在博客公网域名上。&lt;/p&gt;&#xA;&lt;p&gt;公开页做完之后，真正麻烦的部分才开始出现：我不想每次拿手机拍完照片，都要先把文件传到电脑，再手动处理图片、填写 YAML、重新构建博客。于是我把上传、地点解析、数据更新和发布串成了一条只服务于自己生活记录的技术链路。&lt;/p&gt;&#xA;&lt;p&gt;它看起来是一个上传页面，实际上是一条小型发布流水线：手机通过 Tailscale 进入私有入口，输入或读取照片地点，服务处理图片，修改 Hugo 数据，构建并发布站点。中间任何一环出错，都应该明确告诉我，而不是给我一个“好像上传成功了”的按钮状态。&lt;/p&gt;&#xA;&lt;h2 id=&#34;我没有把上传接口放到公网&#34;&gt;我没有把上传接口放到公网&#xA;&lt;/h2&gt;&lt;p&gt;后台的第一个约束是：它不应该成为博客的公开功能。&lt;/p&gt;&#xA;&lt;p&gt;博客公开页面仍然由 Nginx 提供，足迹上传服务则是一个单独的 Flask 进程，默认只监听本机回环地址：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;127.0.0.1:8791&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;systemd 单元里明确写了运行用户和监听方式：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ini&#34; data-lang=&#34;ini&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;[Service]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;User&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;ubuntu&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Group&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;ubuntu&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;EnvironmentFile&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/etc/footprints-upload.env&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ExecStart&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/usr/bin/python3 /home/ubuntu/blog-src/scripts/footprints_upload.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里有两个好处。&lt;/p&gt;&#xA;&lt;p&gt;第一，公网 Nginx 根本无法把这个端口暴露出去。第二，上传服务不是 root 进程，即使代码出现问题，默认权限也被限制在博客用户和指定图片目录内。&lt;/p&gt;&#xA;&lt;p&gt;我还在公网 Nginx 里显式拒绝了这两个路径：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-nginx&#34; data-lang=&#34;nginx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;location&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;^~&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;/footprints-admin/&lt;/span&gt; { &lt;span style=&#34;color:#f92672&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;404&lt;/span&gt;; }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;location&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;^~&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;/api/footprints/&lt;/span&gt; { &lt;span style=&#34;color:#f92672&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;404&lt;/span&gt;; }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;之前 Nginx 对未知路径会回退到首页，访问后台路径虽然看不到后台内容，却会返回 &lt;code&gt;200&lt;/code&gt;。这不算真正的泄露，但会让排查和安全判断变得模糊，所以我最后让它明确返回 &lt;code&gt;404&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;h2 id=&#34;tailscale-解决的是谁能碰到后台&#34;&gt;Tailscale 解决的是“谁能碰到后台”&#xA;&lt;/h2&gt;&lt;p&gt;只监听 &lt;code&gt;127.0.0.1&lt;/code&gt; 之后，手机自然访问不到。这里我没有给后台再做一个公网域名，而是使用 Tailscale Serve：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;https://tokyo-exit-node.tailde6fbb.ts.net:8443/footprints-admin/&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tailscale 在这条链路里的价值，不只是“有一个 VPN IP”。它把后台入口限制在我的 tailnet 内：手机必须先登录同一个 Tailscale 网络，外部互联网用户即使知道 URL，也无法建立这条私有连接。&lt;/p&gt;&#xA;&lt;p&gt;但我很快遇到一个实际问题：博客的 Nginx 已经占用了公网 443 端口。最初我把 Serve 配到 443，配置状态看起来是成功的，服务器本机访问却仍然命中了 Nginx，手机端表现为页面卡住。&lt;/p&gt;&#xA;&lt;p&gt;最后我把 Serve 改到 8443：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;tailscale serve --bg --https&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;8443&lt;/span&gt; http://127.0.0.1:8791&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;改完以后，用完整端口访问，服务端返回才稳定命中 Flask 后台。这里的经验很直接：看到 &lt;code&gt;tailscale serve status&lt;/code&gt; 显示 proxy，并不等于你从实际客户端访问时一定经过了它，端口占用和本机网络路径都需要验证。&lt;/p&gt;&#xA;&lt;h2 id=&#34;tailscale-不是唯一一道门&#34;&gt;Tailscale 不是唯一一道门&#xA;&lt;/h2&gt;&lt;p&gt;Tailscale 解决的是网络范围，但我仍然给后台加了独立账号密码。&lt;/p&gt;&#xA;&lt;p&gt;Flask 在 &lt;code&gt;/footprints-admin/&lt;/code&gt; 和 &lt;code&gt;/api/footprints&lt;/code&gt; 前做 Basic Auth，密码只以 Werkzeug 哈希保存在：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/etc/footprints-upload.env&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个文件是 &lt;code&gt;root:root&lt;/code&gt;、&lt;code&gt;0600&lt;/code&gt; 权限，仓库里不保存明文密码。上传接口还要求：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-http&#34; data-lang=&#34;http&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;X-Requested-With: XMLHttpRequest&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这不是完整的复杂会话系统，但可以挡住普通跨站表单直接提交；后台页面通过同源 &lt;code&gt;fetch&lt;/code&gt; 自己带上这个请求头。&lt;/p&gt;&#xA;&lt;p&gt;所以现在的访问链路是两层：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;手机登录 Tailscale&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;进入 tailnet 私有 HTTPS&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Basic Auth 账号密码&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;后台页面和上传 API&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对我这种单用户、低频、只给自己用的后台来说，这比把上传接口直接挂在博客公网域名下更合适。它减少了公网攻击面，也不需要先为一个小工具搭完整的用户系统。&lt;/p&gt;&#xA;&lt;h2 id=&#34;每张图片都要有自己的地点&#34;&gt;每张图片都要有自己的地点&#xA;&lt;/h2&gt;&lt;p&gt;最初的上传页只有一个文件选择框。第一次实际用手机测试时，我发现“文件上传成功”不等于“足迹完成了”。图片目录里有了 WebP，但地图完全没有变化。&lt;/p&gt;&#xA;&lt;p&gt;原因很简单：地图读取的是 &lt;code&gt;data/footprints.yaml&lt;/code&gt;，它需要地点、坐标、日期和照片之间的关系。单独把图片放到 &lt;code&gt;static/images/footprints/&lt;/code&gt;，Hugo 不会凭空知道它应该落在哪个坐标。&lt;/p&gt;&#xA;&lt;p&gt;所以现在上传页会为每张图片生成一个地点输入框：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;照片 A   地点：泸沽湖&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;照片 B   地点：&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;照片 C   地点：香港大学梅堂&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;后台接收的地点数组和图片顺序一一对应：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;images    = [a.jpg, b.jpg, c.jpg]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;locations = [&amp;#34;泸沽湖&amp;#34;, &amp;#34;&amp;#34;, &amp;#34;香港大学梅堂&amp;#34;]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;手动填写地点时，服务端调用腾讯位置服务的地址解析接口，将文字转换为经纬度和省市信息。留空时，后台读取照片的 EXIF GPS；像最初那张香港大学梅堂的照片，就可以直接从原图拿到：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;date: 2017-01-16&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lat: 22.282116...&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lng: 114.139808...&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这两个分支的优先级很明确：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;手动地点&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ↓ 有&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;腾讯地理编码&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;手动地点为空&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ↓ 有&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;EXIF GPS&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;两者都没有&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;这一张失败，其他图片继续&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;“其他图片继续”很重要。批量上传 10 张照片时，不能因为其中一张没有 GPS，就把另外 9 张也全部回滚。接口现在会返回成功图片、失败文件名和失败原因，前端再用弹窗告诉我本次是全部成功还是部分成功。&lt;/p&gt;&#xA;&lt;h2 id=&#34;图片处理和地点数据现在是一件事&#34;&gt;图片处理和地点数据现在是一件事&#xA;&lt;/h2&gt;&lt;p&gt;每张图片成功后，后台会完成几步处理：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;检查扩展名、MIME 类型和真实文件签名；&lt;/li&gt;&#xA;&lt;li&gt;根据 EXIF 修正方向；&lt;/li&gt;&#xA;&lt;li&gt;生成最长边 2560 像素的展示图；&lt;/li&gt;&#xA;&lt;li&gt;生成最长边 640 像素的缩略图；&lt;/li&gt;&#xA;&lt;li&gt;删除公开版本中的 EXIF 和 GPS；&lt;/li&gt;&#xA;&lt;li&gt;生成随机图片 ID；&lt;/li&gt;&#xA;&lt;li&gt;组合地点、日期、坐标和照片 URL 的 YAML。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;现在返回的 YAML 已经不是单独的一段图片地址，而是可以直接进入 &lt;code&gt;locations&lt;/code&gt; 数组的完整地点：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- &lt;span style=&#34;color:#f92672&#34;&gt;id&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;fp-example&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;泸沽湖&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;date&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;lat&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;27.6894&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;lng&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;100.7798&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;country&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;中国&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;province&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;云南省&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;city&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;丽江市&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;category&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;category_name&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;description&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;cover&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/images/footprints/2026/09/fp-example-thumb.webp&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;photos&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#f92672&#34;&gt;src&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/images/footprints/2026/09/fp-example.webp&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;thumb&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/images/footprints/2026/09/fp-example-thumb.webp&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;alt&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;泸沽湖&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;caption&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;上传成功之后为什么还要自动发布&#34;&gt;上传成功之后，为什么还要自动发布&#xA;&lt;/h2&gt;&lt;p&gt;一开始我认为“把 YAML 片段返回给我，手动粘贴一下”也可以。但既然已经决定用手机管理图片，再让我回电脑改数据、跑 Hugo，就等于把最麻烦的步骤留在了后台之外。&lt;/p&gt;&#xA;&lt;p&gt;所以现在成功上传后，服务会自动把地点追加到：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;data/footprints.yaml&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后依次执行：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;检查文章是否有未来发布时间&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Hugo 构建 public/&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;root-only helper 同步 /var/www/blog/&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;线上足迹地图读取新的地点数据&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里没有给上传服务通用的 root 权限。它只能通过 sudoers 调用一个 root-owned helper，而这个 helper 只做一件事：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rsync -a --delete /home/ubuntu/blog-src/public/ /var/www/blog/&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上传服务本身不能执行任意 root 命令，也不会自动提交 Git。这样做的代价是发布链路更长，但权限边界更清楚。&lt;/p&gt;&#xA;&lt;h2 id=&#34;过程中踩到的坑&#34;&gt;过程中踩到的坑&#xA;&lt;/h2&gt;&lt;p&gt;第一个坑是上传按钮看起来没有反应。实际上服务已经返回了 &lt;code&gt;200&lt;/code&gt;，只是页面没有弹窗，也没有把结果滚动到视野内。我连续点了几次，结果同一批照片被上传了三遍。&lt;/p&gt;&#xA;&lt;p&gt;后来我按内容哈希清理了重复文件，并给前端加上了：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;上传中状态；&lt;/li&gt;&#xA;&lt;li&gt;按钮禁用；&lt;/li&gt;&#xA;&lt;li&gt;120 秒超时；&lt;/li&gt;&#xA;&lt;li&gt;成功、部分成功、失败弹窗；&lt;/li&gt;&#xA;&lt;li&gt;自动滚动到 URL 和 YAML 结果。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;第二个坑是腾讯位置服务的 WebService 配额。腾讯地图 JavaScript SDK 能正常加载，并不代表同一个 Key 的服务端地址解析额度一定够用。测试“泸沽湖”时，我手动输入地点后直接报错，排查才发现：在腾讯位置服务控制台创建 Key 还不够，还需要给这个 Key 对应的 WebService 能力分配调用额度。没有额度时，接口明确返回每日调用量已达到上限。&lt;/p&gt;&#xA;&lt;p&gt;这类错误不能被吞掉。现在它会作为某一张图片的地点解析失败返回，其他带 EXIF GPS 或已经解析成功的图片仍然可以继续。等配额恢复，或者调整 Key 的 WebService 配额，手动地点解析就可以继续使用。&lt;/p&gt;&#xA;&lt;p&gt;第三个坑是“图片上传成功”和“地图出现地点”其实是两个不同的成功标准。前者只说明文件被处理了，后者还要求地点数据成功写入、Hugo 构建成功、静态站点同步成功。现在后台把自动发布失败单独返回并弹窗提示，不再把“图片已写入磁盘”冒充成“地图已经上线”。&lt;/p&gt;&#xA;&lt;h2 id=&#34;最后&#34;&gt;最后&#xA;&lt;/h2&gt;&lt;p&gt;这个后台最后没有变成一个很大的系统：没有数据库，没有用户注册，也没有把公开足迹页改成动态服务。&lt;/p&gt;&#xA;&lt;p&gt;它只是把几个边界清楚的组件串了起来：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;手机照片&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Tailscale 私有入口&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Basic Auth 后台&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;EXIF / 腾讯地理编码&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;WebP 图片处理&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;footprints.yaml&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Hugo + root-only 发布 helper&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;公开足迹地图&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我现在对这类后台的判断也变得更具体了：真正难的不是加一个上传按钮，而是把“上传成功”“数据正确”“权限合理”“发布完成”这几件事连成一条可以验证的链路。&lt;/p&gt;&#xA;&lt;p&gt;如果其中任何一步只能靠我凭感觉确认，那它就还没有真正做完。&lt;/p&gt;&#xA;</description>
        </item><item>
            <title>腾讯地图接入实录：我如何给博客增加足迹功能</title>
            <link>https://blog.chen-api.cloud/posts/tencent-map-footprints/</link>
            <pubDate>Fri, 11 Sep 2026 11:40:00 +0800</pubDate>
            <guid>https://blog.chen-api.cloud/posts/tencent-map-footprints/</guid>
            <description>&lt;p&gt;最近我给博客增加了一个“足迹”板块。最初的想法很简单：把我拍过的照片放到地图上，让照片和地点对应起来。&lt;/p&gt;&#xA;&lt;p&gt;以前照片只是文章里的附件，放在某篇文章下面。这样虽然能保存照片，但过一段时间之后，很难快速回忆“这张照片是在什么地方拍的”。所以我想做一个单独的页面，用地图记录地点，再用照片补充记忆。&lt;/p&gt;&#xA;&lt;h2 id=&#34;先把一张照片放到地图上&#34;&gt;先把一张照片放到地图上&#xA;&lt;/h2&gt;&lt;p&gt;第一条样例足迹使用的是一张 2017 年在香港大学梅堂拍摄的照片。&lt;/p&gt;&#xA;&lt;p&gt;这张照片原本带有 EXIF 信息，里面包括拍摄日期、照片方向和 GPS 坐标。我先从原图中读取这些信息，再把它们整理成足迹数据：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;locations&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#f92672&#34;&gt;id&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;hku-may-hall&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;香港大学 · 梅堂&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;date&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;2017-01-16&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;lat&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;22.282117&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;lng&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;114.139808&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;country&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;中国&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;province&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;香港&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;city&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;香港&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;category&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;architecture&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;category_name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;建筑&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;description&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;在香港大学梅堂前留下的一张照片。&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;cover&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/images/footprints/hong-kong-2017-thumb.webp&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;photos&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#f92672&#34;&gt;src&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/images/footprints/hong-kong-2017.webp&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;thumb&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/images/footprints/hong-kong-2017-thumb.webp&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;alt&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;2017&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;年在香港大学梅堂前的留影&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;caption&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;香港大学 · 梅堂&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;目前地点数据放在 Hugo 的 &lt;code&gt;data/footprints.yaml&lt;/code&gt; 中。这样做的好处是结构清晰，新增地点时只需要增加一段数据，不需要修改页面模板。&lt;/p&gt;&#xA;&lt;h2 id=&#34;页面使用-hugo-和原生-typescript&#34;&gt;页面使用 Hugo 和原生 TypeScript&#xA;&lt;/h2&gt;&lt;p&gt;足迹页面没有引入数据库，也没有使用 React 或 Vue，而是继续沿用博客现有的 Hugo 结构。&lt;/p&gt;&#xA;&lt;p&gt;主要文件包括：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;data/footprints.yaml&lt;/code&gt;：地点和照片数据；&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;layouts/page/footprints.html&lt;/code&gt;：生成页面结构；&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;assets/ts/footprints.ts&lt;/code&gt;：处理地图和交互；&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;assets/scss/footprints.scss&lt;/code&gt;：处理样式和响应式布局；&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;config.toml&lt;/code&gt;：配置腾讯地图 Web Key。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Hugo 构建页面时，会把 YAML 数据写进 HTML：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;script&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data-footprints-data&lt;/span&gt;&amp;gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ...&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;script&lt;/span&gt;&amp;gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;浏览器加载页面后，TypeScript 再读取这段 JSON，完成搜索、筛选、地图标记和照片浏览。&lt;/p&gt;&#xA;&lt;p&gt;整体运行逻辑是：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;footprints.yaml&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Hugo 生成静态 HTML&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;页面内嵌地点 JSON&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;TypeScript 读取数据&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;腾讯地图显示坐标&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;用户进行筛选和照片浏览&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;腾讯位置服务怎么获取&#34;&gt;腾讯位置服务怎么获取&#xA;&lt;/h2&gt;&lt;p&gt;地图部分使用腾讯地图 JavaScript API GL。&lt;/p&gt;&#xA;&lt;p&gt;申请流程大致是：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;注册或登录&lt;a class=&#34;link&#34; href=&#34;https://lbs.qq.com/dev/console/application/mine&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;腾讯位置服务控制台&lt;/a&gt;；&lt;/li&gt;&#xA;&lt;li&gt;进入“应用管理”中的“我的应用”；&lt;/li&gt;&#xA;&lt;li&gt;创建一个应用；&lt;/li&gt;&#xA;&lt;li&gt;在应用中添加 Key；&lt;/li&gt;&#xA;&lt;li&gt;启用 JavaScript API GL；&lt;/li&gt;&#xA;&lt;li&gt;配置博客域名白名单；&lt;/li&gt;&#xA;&lt;li&gt;按控制台要求分配调用额度；&lt;/li&gt;&#xA;&lt;li&gt;将 Key 配置到博客中。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;腾讯官方文档说明，Key 用于识别开发者身份，不同产品能力可以单独启用。&lt;a class=&#34;link&#34; href=&#34;https://lbs.qq.com/service/webService/webServiceGuide/overview&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;WebService API 文档&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;我在 Hugo 页面中这样加载地图 SDK：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go-html-template&#34; data-lang=&#34;go-html-template&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;{{&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;.Site.Params.footprints.tencentMapKey&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;}}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;script&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;src&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://map.qq.com/api/gljs?v=1.exp&amp;amp;key=&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;{{&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;.&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;urlquery&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;}}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;script&lt;/span&gt;&amp;gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;{{&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;}}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后在 TypeScript 中初始化地图：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;map&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TMap&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Map&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;root&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;querySelector&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;[data-map]&amp;#39;&lt;/span&gt;), {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;center&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TMap&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;LatLng&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;first&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;lat&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;first&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;lng&lt;/span&gt;),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;zoom&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;12&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;minZoom&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;3&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;maxZoom&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;18&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里需要注意，前端地图使用的 Web Key 会出现在脚本地址里，因此重点不是完全隐藏它，而是配置正确的域名白名单和调用权限。真正的服务端密钥、签名密钥不能写入公开页面。&lt;/p&gt;&#xA;&lt;p&gt;之前参考的地图示例中还包含本地代理配置，例如：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;window.&lt;span style=&#34;color:#a6e22e&#34;&gt;_TMapSecurityConfig&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;serviceHost&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;http://127.0.0.1:...&amp;#39;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这种地址只适合本地环境。正式部署后，&lt;code&gt;127.0.0.1&lt;/code&gt; 指向的是访问者自己的设备，不能继续使用。线上必须使用真实域名和对应的 Key。&lt;/p&gt;&#xA;&lt;h2 id=&#34;地图上的标记使用照片缩略图&#34;&gt;地图上的标记使用照片缩略图&#xA;&lt;/h2&gt;&lt;p&gt;我没有使用普通图钉，而是让每个地点的封面缩略图直接作为地图标记。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;locations&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;forEach&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;location&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;styles&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;location&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TMap&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;MarkerStyle&lt;/span&gt;({&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;width&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;52&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;height&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;52&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;anchor&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;26&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;52&lt;/span&gt; },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;src&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;location.cover&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后通过 &lt;code&gt;TMap.MultiMarker&lt;/code&gt; 批量添加：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;markerLayer&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TMap&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;MultiMarker&lt;/span&gt;({&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;map&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;styles&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;geometries&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; []&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样用户看到地图时，可以直接通过图片判断这个地点对应的内容。&lt;/p&gt;&#xA;&lt;p&gt;但这也带来一个性能问题：如果标记直接使用原图，地图初始化时就会加载很多大文件。&lt;/p&gt;&#xA;&lt;p&gt;所以图片会处理成两种尺寸：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;展示图：最长边 2560 像素；&lt;/li&gt;&#xA;&lt;li&gt;缩略图：最长边 640 像素。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;地图标记和地点列表使用缩略图，用户点击照片后，灯箱才加载展示图。&lt;/p&gt;&#xA;&lt;h2 id=&#34;图片处理还解决了方向和隐私问题&#34;&gt;图片处理还解决了方向和隐私问题&#xA;&lt;/h2&gt;&lt;p&gt;手机照片不一定真的按照最终方向存储，有些照片只是依赖 EXIF 信息告诉浏览器如何旋转。&lt;/p&gt;&#xA;&lt;p&gt;如果直接转换成 WebP，可能出现图片横向或倒置的问题。因此处理图片时，先根据 EXIF 修正方向，再进行压缩。&lt;/p&gt;&#xA;&lt;p&gt;公开展示版本还会删除 EXIF 和 GPS 信息，避免把不必要的拍摄位置暴露出去。&lt;/p&gt;&#xA;&lt;p&gt;目前图片处理流程是：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;上传原图&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;检查扩展名、MIME 和真实文件签名&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;读取并修正 EXIF 方向&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;生成展示图和缩略图&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;删除公开版本中的 EXIF/GPS&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;写入静态图片目录&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;文件名也不会继续使用用户原始文件名，而是由服务端生成随机 ID，避免重名和覆盖已有文件。&lt;/p&gt;&#xA;&lt;h2 id=&#34;搜索筛选和地图联动&#34;&gt;搜索、筛选和地图联动&#xA;&lt;/h2&gt;&lt;p&gt;页面目前支持：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;地点名称搜索；&lt;/li&gt;&#xA;&lt;li&gt;城市筛选；&lt;/li&gt;&#xA;&lt;li&gt;年份筛选；&lt;/li&gt;&#xA;&lt;li&gt;场景类型筛选；&lt;/li&gt;&#xA;&lt;li&gt;地图和照片视图切换；&lt;/li&gt;&#xA;&lt;li&gt;点击地点卡片定位地图；&lt;/li&gt;&#xA;&lt;li&gt;点击照片打开大图；&lt;/li&gt;&#xA;&lt;li&gt;在灯箱中切换上一张和下一张照片。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;筛选时，TypeScript 会对本地地点数组进行过滤：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;filtered&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;locations&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;filter&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;location&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;matchesFilters&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;location&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;inMapBounds&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;location&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;过滤结果会同时更新地点列表和地图标记。&lt;/p&gt;&#xA;&lt;p&gt;点击地点卡片时，地图会移动到对应坐标：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;map&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;easeTo&lt;/span&gt;({&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;center&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TMap&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;LatLng&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;location&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;lat&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;location&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;lng&lt;/span&gt;),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;zoom&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;16&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;duration&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;600&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;照片浏览部分没有引入第三方轮播库，因为需求并不复杂。记录当前照片数组和索引，再根据索引更新灯箱中的图片即可。&lt;/p&gt;&#xA;&lt;h2 id=&#34;过程中遇到的问题&#34;&gt;过程中遇到的问题&#xA;&lt;/h2&gt;&lt;p&gt;第一个问题是图片不能直接使用原图。原图太大，不适合作为地图标记，也会影响首屏加载，所以必须生成缩略图。&lt;/p&gt;&#xA;&lt;p&gt;第二个问题是本地地图配置不能直接部署到线上。本地代理地址和正式域名的加载方式不同，必须重新申请 Key，并配置域名白名单。&lt;/p&gt;&#xA;&lt;p&gt;第三个问题是地点和照片之间的关系需要提前设计。如果只保存图片地址，后面还要补地点、日期、分类和说明。因此我最后把地点作为一级数据，照片作为地点下面的数组。&lt;/p&gt;&#xA;&lt;p&gt;第四个问题是部署后的资源缓存。足迹页面同时包含 Hugo 模板、SCSS、TypeScript、地图 SDK 和图片。不能只看本地构建成功，还要检查线上页面是否引用了最新的 CSS、JS 哈希，以及图片地址是否能够正常访问。&lt;/p&gt;&#xA;&lt;p&gt;目前照片先通过服务器后台上传一张 Demo 图片，生成展示图和缩略图，再把返回的 YAML 手动写入足迹数据。后面我准备做一个管理后台，让我可以直接用手机上传照片，具体实现放到下一篇。&lt;/p&gt;&#xA;&lt;h2 id=&#34;sqlite-会不会更好&#34;&gt;SQLite 会不会更好&#xA;&lt;/h2&gt;&lt;p&gt;这取决于足迹模块之后会不会变成一个需要频繁维护的内容系统。&lt;/p&gt;&#xA;&lt;p&gt;以目前的规模来看，YAML 反而更合适：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;地点数量少；&lt;/li&gt;&#xA;&lt;li&gt;数据结构简单；&lt;/li&gt;&#xA;&lt;li&gt;内容主要由自己维护；&lt;/li&gt;&#xA;&lt;li&gt;Hugo 可以直接生成静态页面；&lt;/li&gt;&#xA;&lt;li&gt;不需要运行数据库；&lt;/li&gt;&#xA;&lt;li&gt;Git 可以直接记录每次数据变化。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;如果现在直接引入 SQLite，就需要额外处理数据库备份、权限、接口、部署和数据同步，复杂度会增加。&lt;/p&gt;&#xA;&lt;p&gt;但如果下一步要做手机管理后台，SQLite 就会变得很有价值。它适合这种单用户、小规模、低并发的管理系统。&lt;/p&gt;&#xA;&lt;p&gt;比较合理的结构是：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;手机管理后台&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Python API&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SQLite 保存地点和照片元数据&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;图片文件保存到静态目录&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ↓&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;公开足迹页读取数据&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;SQLite 中只保存元数据，不建议把图片本身塞进数据库：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;locations&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- id&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- name&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- date&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- lat&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- lng&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- city&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- category&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- description&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;photos&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- id&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- location_id&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- src&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- thumb&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- caption&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- sort_order&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;图片依然保存为 WebP 文件，数据库只保存图片路径、所属地点和排序信息。&lt;/p&gt;&#xA;&lt;p&gt;我的建议是：当前阶段继续使用 YAML，把公开页先做稳定；管理后台第一版也可以继续生成 YAML。等以后需要手机新增地点、修改坐标、调整照片顺序、删除照片，再把 SQLite 作为后台的数据源。&lt;/p&gt;&#xA;&lt;p&gt;SQLite 不是现在一定更好，而是当管理需求增加后会更合适。对于目前这个只有少量地点的个人足迹页，YAML 更简单；对于下一阶段的手机管理后台，SQLite 会是一个不错的选择。&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
