# life-circle **Repository Path**: yelangcode/life-circle ## Basic Information - **Project Name**: life-circle - **Description**: 15 分钟生活圈时空智能体检与规划助手,技术栈:SpringBoot 3 + MyBatis-Plus + PostGIS + Thymeleaf + 原生 JS 前端(百度 JS API GL) 开源许可:Apache-2.0。以百度地图开放能力为数据底座:输入社区/街道中心点坐标,系统基于真实步行路网逐点测时 (百度「步行路线规划(轻量)」服务), 自动计算「15 分钟步行等时圈」。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-15 - **Last Updated**: 2026-10-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 经纬上海 · 时空引擎 > 15 分钟生活圈时空智能体检与规划助手 > 技术栈:SpringBoot 3 + MyBatis-Plus + PostGIS + Thymeleaf + 原生 JS 前端(百度 JS API GL) > 开源许可:Apache-2.0 以**百度地图开放能力**为数据底座:输入社区/街道中心点坐标,系统基于**真实步行路网**逐点测时 (百度「步行路线规划(轻量)」服务), 自动计算「15 分钟步行等时圈」,统计圈内 8 类民生设施覆盖情况,识别服务盲区与灰色区域, 输出可视化“体检报告”与自动诊疗建议,并支持体检历史**分页**归档回看与**报告导出** (完整长图 PNG / 多页 PDF / 单文件 HTML / 原始 JSON)。 --- ## 一、项目简介 - **输入**:地址(地理编码)或地图选点(逆地理编码回填真实地名)→ 得到中心点(BD-09); - **计算**:中心点 → 真实路网步行测时(步行路线规划(轻量)· `directionlite/v1/walking`)→ 15 分钟等时圈 → 圈内设施覆盖; - **输出**:综合评分 + 四维评价 + 设施覆盖明细 + 灰色区域 + 盲区告警 + 规划建议 + 可导出报告; - **AI**:接入大模型(默认**硅基流动**,OpenAI 兼容协议)——生成报告时润色规划建议; **AI 规划助手**支持自然语言提问(如"预算 5 处,补在哪里最划算?"):大模型解析意图 → 服务端执行网格诊断与选址计算 → 返回结论 + 结构化事实 + 建议设施点(可一键在地图高亮)。 未配置 `AI_API_KEY` 时自动回退**服务端规则引擎**,功能不降级(响应体 `engine` 字段如实标注); **全部问答模板与提示词工程集中在服务端**(`RulePlanner`:意图提示词、回答提示词、快捷问题、欢迎语、 追问建议、引擎展示名、分人群结论模板),前端只做渲染、不持有任何模板; - **POI 归类以百度 type 为准**:检索结果按 `detail_info.tag`(「一级;二级」分类)精确匹配到设施类别, 不再只凭名称——"公园门口的餐厅"不会算作公园、"医疗;药店"不会混入医疗;并提供清空 POI 池的运维接口; **交通类含公交站与地铁站**(白名单 `交通设施;公交车站` / `交通设施;地铁站`, 关键词 `公交站 / 地铁站 / 地铁`——补「地铁」是因为「地铁站」的检索单页会被 「××地铁站-3口」这类 `出入口;其他` POI 挤占,实测徐家汇 1200m:关键词「地铁站」单页 20 条里 只有 4 条是车站、「地铁」的 4 条全部是车站;地铁出入口本身**不计入**设施数,避免一站重复计数); **同一地铁站只算一处**:百度对同一个站会返回多行(不同线路各一条,实测徐家汇 2 行相距 78m), 故地铁站按**站名**去重、公交站仍按"名称+坐标"(同名站台可能是马路两侧两个真实上车点); **覆盖统计与设施清单同口径**:`selectCoverage` 用与去重键一致的 `COUNT(DISTINCT …)`, 不会再出现"覆盖明细 117 处 / 清单 108 处"这种一份报告两套数(详见设计文档 §6.4.1aq); - **扩展**:需求人群(全龄/亲子/适老)切换实时重算(**等时圈按该人群的实测耗时阈值重插值**—— 与基准圈同一批步行测时样本,不做几何缩放;灰色缺失区域随速度同步缩放,两者口径在页面上分别标注)、 **关注度权重按人群调整**:适老上调医疗/药店/菜市场、**下调学校至 0.4×**,且非刚需类不参与"保基本"封顶)、 **分数可追溯解读**(逐维给出加权贡献与失分依据)、**地图标记就地解释**(图例 + 图层悬浮说明 + 说明卡)、 **设施清单悬浮详情卡**(名称/地址列会被省略号截断,悬浮任意一行弹出全称 + 完整地址 + 类别 + 圈内可达/圈外 + BD-09 坐标; 卡片挂 body 做 fixed 定位以免被清单滚动容器裁掉,贴光标并自动避让视口边缘)、 **步行网络页内含通行障碍清单**(路口 / 天桥 / 围挡逐点列出,徽标配色与地图方块标注一致; 口径与该页一致——障碍点是成因定位与图上标注,不逐点参与测时,其影响由实测非直线系数整体体现)、 **通行障碍点双通道识别**(① POI 通道:以「路口」POI 代理信号交叉点,给"附近有哪些"; ② **路径实测通道**:对「中心 → 15 分钟圈边缘」的真实步行路线(步行路线规划(轻量)) 读取导航指引文本,提取过马路 / 人行横道 / 过天桥 / 地下通道并落到该步终点坐标, 给"这一趟必须过什么"——POI 库对天桥基本无收录,这条通道才有数据。 两条通道都只用**步行**侧能力;信号灯"个数"只有驾车接口有,与算路口径冲突,故默认关闭、 代码路径保留(`LC_OBSTACLE_SIGNAL_CORRIDORS=4` 可启用))、 **规划选址点专属金色标记**(地图原生 Marker + SVG 图标:金点 + 白描边 + 光晕 + 圈内序号, 与现状设施点的类别色在颜色和形状上双重区分,点位可点击查看补点口径)、 **缺失区域随类别聚焦展示**(与缺口网格、选址点同一聚焦口径,可逐类查看,避免多类叠加盖满整圈)、 **缺失区域重点突出**(深灰斜纹面 + 红色加粗虚线 + 呼吸脉冲 + 红底白字标签,让"设施覆盖不到的面"第一眼可见)、 **图例即开关**(缺失区域 / 缺口网格 / 选址点 / 盲区环四项可在图例上直接点击开关,与图层开关双向同步)、 **AI 总结带分人群视角**(三档人群评分 + 各人群关注设施的达标率与优先补点方向,随当前人群联动)、 **AI 助手标注接入的具体大模型**(大模型在线时显示「服务商 · 模型」,如「硅基流动 · Qwen/Qwen2.5-7B-Instruct」, 规则引擎回答则不显示,不会把模板回答伪装成某模型产出)、 **图例面板精简**(细节下沉到悬浮提示与说明卡,面板高度压至地图的 45% 以内且设上限兜底)、 体检历史分页归档回看(支持**按社区名称检索**与二次确认删除单条记录)、 **同点位查重提示**(同一经纬度 25m 内已有体检记录时,提交前提示并可直接打开那份报告, 不重复建任务、不重复消耗配额;也可在弹窗里选择「重新体检」)、 **一键重新体检当前点位**(报告是快照,重跑即可得到可比的新结果并生成新历史)、通行障碍点标注、 **报告分主题页展示**(地图常驻左侧,报告按「总览 / 设施覆盖 / 均等性 / 步行网络 / 短板与规划 / 设施清单」 六个主题分页,页面基本无需纵向滚动;导出长图时自动展开全部主题页,交付内容依旧完整)、 **按 POI 类别查看**(单击图例=筛选/取消该类(可多选)、双击=只看该类、地图顶部筛选提示条与 "全部显示"一键复原;地图点位、覆盖明细表、设施清单同步联动)、 报告导出(**服务端生成**完整长图 PNG / 多页 PDF;HTML 报告内嵌矢量地图;另有原始 JSON)、 **公文 / 建议书导出(可编辑 Word)**(`brief` 领导专报 / `report` 体检评估报告 / `proposal` 补短板项目建议书: 公文体 GB/T 9704 版式、Word 原生表格、项目清单含坐标与预估投资、待回填整改台账、可核验附则; 文号与编制单位留待填占位,默认标注草案——见 §2.9)、 **AI 质量门禁(规划问答评测集)**(8 条用例覆盖 7 类意图与两类边界,判定含**数字一致性**校验: 回答里带单位的数字必须能在事实卡/建议点里找到——"大模型不产生数字"从此有自动验证, 未配置 Key 时整套在规则引擎上运行、可进 CI;见 §2.10)。 > 图例交互实现要点(避免"点击无反应"):图例采用**容器级事件委托**(内容重渲染后依然有效); > 单击延迟 180ms 以区分双击;筛选状态由地图侧独立维护,报告模块缺失或异常时自动降级, > **任何情况下点击都会改变地图点位并弹出提示**;地图浮层 z-index 显式抬高,确保不被地图 SDK > 内部图层遮挡;浏览器 Console 会打印 `[life-circle] 图例点击 → ...` 便于定位环境问题。 ### 解决的问题 道路并非直线,红绿灯、过街天桥、施工围挡等都会显著改变真实步行可达范围;传统人工普查与静态数据成本高、更新慢,难以发现“服务盲区”。本系统将「地图 API 测时 + 空间插值 + POI 大数据」组合成可重复、低成本、可量化的自动体检工具。 --- ## 二、核心方案与技术设计 ### 2.1 总体架构与任务数据流 ``` [前端] 地址/选点 → 体检任务提交 → 后台轮询真实进度(不占页面) → 报告/图表/地图渲染 [接入] Geo / Analysis REST 接口(统一 {code,msg,data} 响应包) [编排] AnalysisService:任务状态机 + 分阶段进度,独立异步线程池执行 [能力] BaiduAdapter(百度开放能力统一出口):地理编码 / 逆地理编码 / POI 周边检索 / 批量步行算路(route matrix) / 坐标转换,统一走:AK 校验 → 缓存命中 → 配额计量 → 限速 → 重试 [存储] PostGIS:analysis_task / poi(全局池) / isochrone / gray_area / baidu_cache / analysis_record [计算] Isochrone 等时圈 · Poi 清洗复用 · Coverage 覆盖与灰色区 · BlindSpot 盲区 · Obstacle 通行障碍 · Equity 供需匹配 · Evaluation 规范权重四维 · Ai/规则 自动诊疗 ``` 一次体检的执行链: 逆地理编码解析社区 → POI 检索(库内池只作补充)→ 36 方向扇形采样 → 分轮步行测时(二分收缩) → 等时线提取并落库 → 覆盖率与灰色区(PostGIS ST_Contains / JTS 差集)→ 盲区检测 → 规范权重四维评价 + 基尼 + 区位熵 → 自动诊疗建议 → 报告落库 + 结构化历史记录。 ### 2.2 地图开放能力调用策略 | 能力 | 接口 | 用途 | | --- | --- | --- | | 地理编码 | `/geocoding/v3/` | 地址 → 坐标候选 | | 逆地理编码 | `/reverse_geocoding/v3/` | 地图选点后回填真实地名 | | POI 周边检索 | `/place/v2/search` | 8 类民生设施采集 | | 步行路线规划(轻量) | `/directionlite/v1/walking` | 等时圈核心测时 + 障碍点路径实测(**一次调用 = 一条 OD**) | | 坐标转换 | `/geoconv/v1/` | 外部数据/坐标系适配(主流程已统一 BD-09,不再调用) | | **批量算路** | `/routematrix/v2/walking` | **在用(等时圈测时)**:一轮的候选点合成 **1 次请求**,36 方向 × 5 轮 = 180 个 OD 只发 **5 次**(逐点要 180 次);每次最多 50 个 OD。**配额不受影响**:百度按 **OD 计费**,5 次与 180 次都算 180 OD,差别只在请求数与墙钟时间(实测步行测时 27.9s → **0.4s**)。批量失败/返回不完整会自动逐点回退,并在报告里标明"存在回退轮次"(详见设计文档 §6.4.1ay) | | **驾车路线规划** | `/direction/v2/driving` | **默认关闭**:唯一能给出"一条走廊有几个信号灯"的接口,因与步行算路口径冲突默认不启用(`LC_OBSTACLE_SIGNAL_CORRIDORS=0` 可显式开启) | - **坐标约定**:前端为百度地图,采集点即百度 **BD-09 原始坐标**;库内与接口全程沿用同一基准、 **不做 WGS84 换算**(数据同源同基准,社区级局部相对距离/包含判定可直接使用;如需对接外部 WGS 数据 请先用 `Bd09Transform` 换算后再入库)。 - **科学性提示(论文方法学)**:几何列类型虽以 4326 承载数值,但语义为“百度 BD-09 平面参考”而非真实 WGS84;ST_DWithin / ST_Contains 仅在全部数据同基准、社区级尺度下近似成立;`CoverageService` 的 设施服务缓冲在按 cos(纬度) 校正的局部米制等距平面中计算,避免经纬度直接 buffer 的经向拉伸。 - **统一出口**:全部调用收敛到 `BaiduAdapter.callCached`,统一处理鉴权、缓存、配额、限速与重试,避免散落调用导致失控。 - **响应缓存**:`baidu_cache` 持久化按 URL 哈希缓存响应(POI **7 天**、算路与地理编码 **3 天**, 天数可配 `BAIDU_CACHE_PLACE_DAYS` / `BAIDU_CACHE_ROUTE_DAYS`),**命中不发请求、不消耗配额**; 同一对 OD 的 URL 完全一致,故等时圈测时与障碍点必经性共享同一份缓存。每次体检结束会把 "缓存命中 / 实际调用"两个数写进日志与报告 `meta.cacheHits / meta.cacheMisses`——缓存省了多少额度可核验。 ### 2.3 等时圈算法:不获取底层路网的空间推导 核心思想:**不下载/解析路网数据**,仅把“步行 15 分钟能否到达某点”交给真实步行算路 API 逐点实测, 再用采样与插值推导近似连通区域——这正是路径规划接口 + 空间算法组合的典型用法。 1. **扇形采样**:以中心为原点按 36 个方向生成候选采样点(0~1800m); 2. **分轮二分测时(批量算路)**:步行耗时沿方向近似单调,故每方向用二分法收缩边界—— 每轮 36 个方向各取 1 个候选点,这 36 个 OD **合成一次「批量算路」请求** (`routematrix/v2/walking`,一次最多 50 个 OD),5 轮共 **5 次请求**即可把边界定位到 约 maxRadius/2⁵ ≈ 56m 精度;相比同精度逐点线性扫描(离线基准实测 599 次调用)调用量下降 99%。 配额按 **OD 计**,故 180 OD 用 5 次还是 180 次请求都是 180 OD 额度,换服务的收益是**墙钟时间** (实测步行测时 27.9s → **0.4s**)。批量失败或返回不完整会自动逐点回退到 「步行路线规划(轻量)」并如实在报告里标注;开关 `LC_ISO_ROUTE_MODE=single` 可回到逐点口径。 **算路不做缓存**(`BAIDU_CACHE_ROUTE_DAYS=0`):每次都真实调用,避免"重跑看到的是旧耗时"; 3. **等时线提取**:沿每条采样射线对实测“(半径,耗时)”做**一维线性插值**,定位耗时=900s 的位置组成闭合环; 4. **分级圈(5 / 10 / 15 分钟)**:对**同一批实测射线**分别插值出耗时 = 300 / 600 / 900s 的半径并连成环—— 三个圈同源、不额外消耗算路配额,**不是把 15 分钟圈按比例缩放的缓冲近似** (实测各方向 r5/r15 散布在 0.25~0.51,若缩放应恒为 0.3333); 5. **IDW 成本热力面**:以全部采样点(耗时)做反距离加权(幂=2,含奇异保护)生成连续成本网格; 注意 IDW 为平滑算子、会被中心 0 秒样本拉低,因此**只用于热力面,不用于边界**; 6. **人群速度**:1.2/1.0/0.8 m/s。亲子/适老档的等时圈**同样出自那一次步行测时的实测样本**—— 步行算路返回的是时间,慢速人群走同一条路的耗时按速度比放大,故其"15 分钟可达"对应更小的实测耗时阈值 (亲子 1.0 m/s → 750s、适老 0.8 m/s → 600s),后端按该阈值插值出环(**零额外算路调用、不做几何缩放**), 前端切人群时直接使用;仅旧报告(缺 `speedRings` 字段)才回退等比缩放,页面口径按 `meta.isoSource` 如实区分 (实测环 vs 缩放近似)。覆盖与评分随人群重算,服务端权威分仍以基准档为准。 7. **缺口与规划也随人群联动(后端一次算好三档)**:切「亲子/适老」时,不只是等时圈和覆盖在变—— **服务缺口网格、补点选址、灰色缺失面、规划建议**同样按该档等时圈重算(服务圈口径是 "等时圈形状平移到设施位置",环换了服务圈就跟着换)。三档在体检时一次算完写进报告的 `crowdTiers`(同一份设施坐标 + 同一套 250m 网格,**只换环**),前端切换即整段换数据,不做裁剪、 不做等比缩放;预算滑块的实时重算接口也一次返回三档。**不随人群变的量**:配置标准(每千人指标)、 补点预算、实测非直线系数、1km 盲区(正圆刚性口径)。详见设计文档 §6.4.1av。 打开**旧报告**(`crowdTiers` 字段引入之前落库的)时,前端会自动调用 `GET /api/analysis/{id}/crowd-tiers` 让服务端按报告里已有的速度档环**纯几何补算**一次 (零地图配额、不写回库),使历史记录同样"区域跟随人群";更早、连速度档环都没有的报告会保留 裁剪/缩放回退并在页面上如实标注为近似口径(详见设计文档 §6.4.1ax)。 8. **评分口径(避免跨档误读)**:三档综合分**不是同一个尺度**——维度分按该人群关注度加权 (适老档医疗/药店加权、学校降权 0.4),且"达标 1.25 倍"处有软饱和,故**不同人群间的分数 不宜直接比高低**,跨档/跨社区比较请以基准档(全龄)的后端权威分为准。另:**"步行网络"维度 三档恒为同一个值是正确的**——它只由实测非直线系数决定(路网距离÷直线距离,速度被约掉)。 页面在分数解读区已写明这两点,并在"本档与基准档恰好同分"时显式标注"四维构成不同,不是未联动" (实测某社区亲子档与全龄档同为 69 分,但四维 85/64/87/90 与 86/64/87/85 不同)。 ### 2.4 POI 检索、清洗与池数据策略 - **全局 POI 池**:`poi` 表跨任务去重复用,去重键 `(category, name, round(lng,5), round(lat,5))`, 写入 `ON CONFLICT DO NOTHING`,同名同址 1m 级坐标天然只入一次; - **检索为主、池数据只作补充**:每个社区每一类设施都按关键词**真实检索**百度 place 接口 (单次最多 20 条 → 检测到截断时用 8 方向偏移中心补强,出参再按检索半径回收), 库内池命中只**并入结果**、不作为"跳过检索"的理由。 早期"池内数量达到配置标准即跳过检索"的做法会把残缺的旧数据永久沿用:实测曹杨新村交通 只统计到 12 处(百度同一中心 2200m 内有公交站 90 处、地铁站 85 处),据此判出**假盲区** 并触发保基本封顶,总分 40;古北新区 163 条复用 / 仅 63 条新检索,五类基础保障全部判成盲区 (总分 19)。根因是 POI 检索发生在等时圈计算**之前**(进度 15% vs 45%),此刻既无法判断 池数据是否完整,也无法判断它能否服务到本社区——故**任何基于池内数量的门槛都不可靠**; - **省配额靠响应缓存**:`baidu_cache` 按 URL(含中心点与半径)缓存百度原始响应(place 缓存 7 天), 同一社区重跑近乎零配额、零耗时;换中心点则本就应当重新检索; - **清洗规则**:坐标合法性校验 → 名称规范化 → 类型白名单(按百度 `detail_info.tag` 的 **二级类型**匹配,一级归类不锁死——图书馆可能被标成 `教育培训;图书馆`、社区医院被标成 `医疗;其他`,硬匹配一级会把真实设施整类剔除)→ 关键词噪声剔除(停车场/收费站等)→ 入库。 ### 2.5 服务盲区与灰色区域识别 - **1km 服务盲区**:检索半径 1000m 内是否缺失菜市场/药店/教育(学校)/养老(基础保障刚性需求), 缺失即在报告置顶告警并给出补点建议坐标; - **灰色区域**:用 JTS 做「等时圈面 − 同类设施 600m 服务缓冲并集」差集, 得到真正“走得到但服务不到”的空白区域,取最大块输出质心建议点; - **覆盖统计下推数据库**:`ST_Contains(isochrone.geom, poi.geom)` + GIST 索引在 PostGIS 内完成, 避免全量 POI 回传 JVM。 ### 2.6 通行障碍点(阻抗可视化) **两条互补通道,缺一不可**(赛题核心痛点:红绿灯 / 过街天桥 / 施工围挡影响步行可达范围): | 通道 | 数据源 | 回答的问题 | 地图标注 | | --- | --- | --- | --- | | ① POI 检索 | 百度地点检索(`/place/v2/search`) | 附近**有哪些**:道路交叉口(如“梅岭北路/棠浦路(路口)”)、过街天桥、施工围挡 | 施 / 灯 / 桥 | | ② **路径实测** | 百度**步行路线规划(轻量)**(`/directionlite/v1/walking`)的**逐步指引文本** | 这一趟**必须过什么**:过马路 / 人行横道 / 过天桥 / 地下通道 | 行 / 道 | > 两条通道都只用**步行**侧能力(POI 检索 + 步行路线规划(轻量)),与等时圈测时同一个服务、 > 同一份 URL 缓存。 > > **关于"红绿灯"**:百度开放平台**没有**信号灯数据给步行侧用——POI 关键词「红绿灯」「信号灯」 > `total` 均为 0(2026-09 实测),步行路线响应里也没有信号灯字段;能给出"一条走廊上有几个信号灯" > 的只有**驾车**导航的路线级 `traffic_light`。由于本项目的口径是"后台路线规划一律使用 > **步行路线规划(轻量)**",这条驾车通道**默认关闭**(`LC_OBSTACLE_SIGNAL_CORRIDORS=0`), > 红绿灯信息由步行侧承担:**「路口」POI 代理道路交叉口**(顺便标注"是否设信号灯以现场为准",因为 > 该 POI 不含此字段)+ **步行路线指引文本里的必经过街点**。 > 若需要"信号灯个数 / 走廊密度(个/km)"这项增量数据,设 `LC_OBSTACLE_SIGNAL_CORRIDORS=4` > 即可启用(代码路径完整保留,报告会自动多出红绿灯卡与走廊密度文案)。 为什么不能只靠 POI:实测「过街天桥」在武汉 / 成都 / 广州 10km、上海徐家汇 3km、上海曹杨 1.2km 半径内均为 **0 条**(POI 库基本没收录),而**路径指引文本只在路线真的走了天桥/地下通道时才写出来**; 「路口」POI 也只能回答"附近有多少交叉口",回答不了"这一趟必须过几次街"。 实现要点:走廊端点取等时圈边界均匀 8 点;只匹配**动作短语**(`过天桥/走天桥/地下通道/人行横道/过马路/过街`) 而非路名(否则"沿天桥路步行"会被误判);坐标取该步**终点**(过街发生在段末);命名=`枫桥路 × 梅岭北路(距中心 20m)`; 按"类型 + 5 位坐标"去重;每社区 8 次步行算路,走 URL 缓存(算路类 3 天)。 口径:障碍点均为**图上标注与成因定位,不逐点参与测时**——它们的影响已由 36 方向实测的 **非直线系数**整体体现(步行路线规划(轻量)与等时圈用的是**同一个服务、同一份 URL 缓存**)。 **缺失区域(灰色面)的三个口径要点**(详见设计文档 §6.4.1az): - **每次只画一个类别**:8 类叠加时并集几乎盖满整圈、会被误读成"整个 15 分钟圈都缺失", 故网页与导出件都只画聚焦的那一类(网页可点图例切换;导出件按同一缺省规则=缺口格最多的那一类), 并在图例提示行/图注里写明**当前画的是哪一类**,其余类别见逐类清单; - **两个面积口径不要混用**:缺失面=**精确几何面积**(`grayZones[].areaHa`); 缺口网格=**250m 抽样口径**(±1 格 ≈ ±6.25 ha)。同一类别的两个数允许差一格量级; - **不全就说清**:每块的 `desc` 写明"该类合计几块、多少公顷",以及"另有 X 公顷更小空白面未在图上单列"、 "含 N 处已覆盖空腔(图上按外轮廓填充)"——不静默少画。 - **准确度上限由"设施检索"决定**(不只是几何):缺失面=等时圈减各设施服务圈,**少检索到一处设施 就会凭空多出一块"缺口"**。2026-09 实测过一个真实案例:养老类旧词表只有 6 个词, 漏掉了「为老服务中心 / 长者照护之家 / 社区食堂 / 护理院 / 颐养院 / 老年助餐中心」等真实命名, 该社区只找到 8 处(圈内 1 处)→ 灰面 61.1 ha;补 6 个词并加**名称词元白名单**与 **附属点位去重**后为 28 处(圈内 7 处)→ 灰面 **0**、覆盖率 25%→100%。 **口径调整后需清空 POI 池**(`DELETE /api/analysis/pois`)再重跑,否则旧池数据会继续沿用。 详见设计文档 §6.4.1ba。 - **缺失区域与选址按规划口径**(2026-09 重做):① 需求侧不再用"距中心衰减"这种编造的人口分布, 改为检索**真实住宅小区**(百度官方分类 `房地产;住宅区`)做核密度需求权重场; ② 缺失判定改用**论文口径**(武汉大学学报 2022《15min生活圈医疗服务可达性》Huff 2SFCA、 北大 2023《公共服务设施可达性度量方法研究综述》):对每个真实住宅小区,**实测**其到同类设施的 步行时间(不是直线距离,也不是"平移等时圈"的几何近似),标准步行时间内可达即覆盖、否则缺失; 缺失区域=未覆盖小区影响区(外扩 300m)并集 ∩ 15 分钟圈——河道/工业/待建地不再算缺失; ③ 服务半径**按各类设施的规划标准步行时间** (幼儿园 5′、小学/菜市场/药店/社区卫生/养老 10′、公交 8′/地铁 15′、公园与金融文体 15′, 教育按学段、交通按方式分档),不再一律 15 分钟;④ 选址候选点**必须落在居住影响区内**, 目标函数改为"覆盖居住核密度最大",因此建议点天然贴近居民、并避开河湖水面/绿地/工业用地。 **注意**:口径收紧后部分类别的灰面数字会变大(如药店按 10 分钟标准判定), 这是"标准更严"而非"算错"——每块缺失面的 desc 都写明它用的标准分钟与"仅统计居住影响区"。 详见设计文档 §6.4.1bf。 - **类别识别以百度官方 POI 行业分类(tag)为准,不靠名称文字**:设施归类用 `detail_info.tag`=「一级;二级」,白名单一律**精确到二级**并取自官方表 《百度地图POI行业分类(tag)》(19 个一级行业,已摘录进 `CategoryRegistry`)。只写一级前缀会 误收同级非目标设施(`购物;商铺` 冻品批发、`教育培训;培训机构` 公考班、`休闲娱乐;ktv`…), 写错一级名会静默漏检(官方是 `文化传媒;文化宫`,写成「文化场馆」则真实文化宫整条丢弃)。 官方一级「**出入口**」(门/站口)一律不计——它不是设施本体。 **唯一例外是养老类**:官方表没有"养老"分类,真实设施散落在 医疗/政府机构/生活服务/美食 四个一级, 且噪声餐厅与真实社区食堂同处「美食」一级,故该类用"官方 tag **或** 名称词元"(页面口径已如实标注)。 各条白名单与实测扩展、以及每条规则踩过的坑,见设计文档 §6.4.1bb。 未配置 AK 或检索失败时如实为空(不编造),失败会写入报告的降级说明。 **红绿灯:默认不给"个数",只给"路口"代理**(口径优先于数据量): - 关闭态(默认):报告与地图上的红绿灯信息=`道路交叉口·××(路口)` 这类 POI 代理,影响描述写明 "是否信号控制以现场为准"——因为「路口」POI 的 `tag` 只有 `道路;路口`,**不含"是否设信号灯"**; - 启用态(`LC_OBSTACLE_SIGNAL_CORRIDORS=4`):额外给出走廊级信号灯**个数**与密度(实测徐家汇 18 个 / 4 条走廊 = **3.3 个/km**,曹杨新村 2.6 个/km)+车流当量 (=个数 × **假定**双向日均 1 万车次,报告里与"非实测"同时出现); 它**只给个数、不给点位**(开放接口没有逐点坐标),也**不参与任何测时**。 - 曾经做过、已移除的"信号灯密度网格图":把走廊总数按 `exp(-d/400m)` 分摊到 25m 网格画成橙色方格 + 3D 立柱。它的形状完全由这条假设决定、峰值必然落在社区中心,既不比"走廊有多密"多出信息, 又容易被读成实测点位分布,故连字段一起删除(设计文档 §6.4.1aq)。 POI 通道的三个如实口径(避免"数据里没有的信息被写进报告"): 1. **不谎称信号控制**:百度「路口」POI 的 tag 只有 `道路;路口`,**不含"是否设信号灯"**这一字段, 因此清单与图例一律写「道路交叉口」,影响描述注明"是否信号控制以现场为准"—— 仅以路口 POI **代理**信号交叉点,不替数据下结论; 2. **入库前校验 tag**:只接受 `tag` 含「路口」或名称含「路口」的结果,滤掉"××路口便利店 / 小区门" 这类同名词条(它们会因为名字里带"路口"而混进阻抗成因清单); 3. **清单条数 ≠ 实际数量**:每类只列**最近的 4 处**,而实测「路口」在 1.2km 内可有 70 处, 故报告口径说明里逐类写明"命中总数 N 处 / 清单列最近 4 处",统计以总数口径为准。 同时障碍点检索只取**单页**(不做覆盖率场景的 8 次偏移补强),每社区 3~6 次 place 调用。 详见 `docs/技术设计文档.md` §6.4.1ah。 ### 2.7 综合评价模型(规范权重 + 基尼 + 区位熵) 以 6×6 个 500m 网格单元为样本,对四维指标按**规范权重**加权 (设施覆盖 45% / 供需匹配 20% / 品质特色 20% / 步行网络 15%,可配置): | 维度 | 含义 | 主要依据 | | --- | --- | --- | | A 设施覆盖 | **分类达标率**加权平均(基础保障类权重加倍,软饱和;标准按单元人口折算) | POI + 等时圈 ST_Contains | | B 步行网络 | 仅取实测非直线系数 | 步行测时推算真实绕行系数 | | C 供需匹配 | 服务完整性:单元可达设施**类别数**占比 | 网格单元可达设施类别统计(不随人群切换) | | D 品质特色 | 品质提升类(公园/文体)达标率 | 设施保障层级分层 | **保基本短板约束**(只作用于**基础保障类**,且按人群判定刚需:适老档小学权重 0.4、不参与封顶): | 约束 | 触发条件 | 总分封顶 | 语义 | | --- | --- | --- | --- | | ① 刚性盲区 | 某基础保障类**15 分钟步行圈内可达数 = 0** | 59 | 不得评为"基本达标"(60)及以上 | | ② 多类不足 | 基础保障类 ≥2 类配置达标率 < 60% | 69 | 不得评为"良好"(70)及以上 | | ③ 单类严重不足 | 任一基础保障类配置达标率 < 40% | 74 | 不得评为"优质达标"(85)及以上 | 多条同时命中取**最小值**;触发时报告同时给出加权原始分、封顶后总分与**逐条原因** (含是哪几类、各多少达标率)。注意①的口径是**等时圈内可达数**,不是"市中心 1km 圆内计数" (后者是 `blindSpots` 的另一套口径,两者不可混读)。 > 熵权法(客观赋权)在本项目中**仅作对照展示**:单社区网格样本存在强空间自相关, > 且饱和维度的差异系数趋近 0(实测设施覆盖维度仅得 **0.89%** 权重),不满足熵权法的独立样本前提。 - **基尼系数 + 洛伦兹曲线**:衡量设施服务在网格单元间的空间均等性; - **区位熵 LQ**:社区某类人均可达数 / 全市该类人均水平,识别相对短板; - **自动诊疗**:基于覆盖率/区位熵/灰色区/盲区规则生成分级建议;可选接入大模型润色为自然语言。 #### 覆盖统计的三个口径(勿混用) | 口径 | 计算 | 回答的问题 | 出现位置 | | --- | --- | --- | --- | | **配置达标率**(数量口径) | 15 分钟可达数 ÷ **配置标准数** | 设施**数量够不够**(等级与保基本封顶依据) | 覆盖明细表、KPI、最薄弱设施、短板清单 | | **空间覆盖率**(面积口径) | 被服务网格数 ÷ 圈内网格总数 | 设施**分布到不到位**(灰区与补点选址依据) | 覆盖明细表、规划优化(250m 网格诊断) | | **圈内占比** | 可达数 ÷ **周边设施数** | 周边设施有多少**落在步行圈内** | 覆盖明细表 | > 三者分母完全不同、**不可互相换算**,且**前两者可以相差很大却同时成立**:实测某社区药店 > 配置达标率 **25%**(2/8 处)、空间覆盖率 **83.3%**——前者说"药店太少",后者说"分布尚可"。 > 为此每类都按 **2×2 矩阵给出处方**(数量 × 空间): > **选址补点**(量少且分布有缺口)/**扩容提质**(量少但分布到位)/**优化布局**(量够但分布不均)/ > **达标维持**(两者皆达标)。详见设计文档 §6.4.1at。 > > 其中「周边设施数」是中心点 **2200m 检索范围**内的该类设施总量 > (检索半径刻意大于体检半径,用于覆盖等时圈向外凸出的部分:服务判定是"格心落在设施服务圈内", > 故 |设施| ≤ 2 × R_iso,取 2200m 覆盖 R_iso ≤ 1080m 的硬上界),因此它**不等于**报告设施清单里的 > POI 总数(该清单含检索范围内、但不在等时圈内的设施)。 > 顶部 KPI「15 分钟可达设施 a / b 处」的 a、b 取自**覆盖明细的逐类汇总**,与明细表逐项对应。 > > 还需注意**两个统计的 POI 集合范围不同**:覆盖明细统计的是**全局 POI 池**中落在中心 2200m 内的设施 > (含其他任务采集、坐标恰好落在此范围内的设施,池按坐标去重复用),而**设施清单**展示的是 > **本次体检采集与复用**的 POI(复用半径同口径 2200m,两者差别只在"本次采集"与"全局池"两个集合)。 > 因此清单里的"可达"个数与覆盖明细的可达数**允许存在少量差异**,评分与结论一律以覆盖明细为准。 #### 可达算子与不确定度(如实披露) - **全站统一算子**:单元可达判定=「等时圈平移」——居民点与设施的相对位移落在本社区等时圈内即视为可达, 与灰色缺失面、250m 网格诊断、预算选址**完全同源**(评分、灰区、缺口不再各用一套口径; 等时圈缺失时回退正圆阈值并在报告 `reachModel` 标注)。 - **实测精度**:抽样 60 组 OD 与本社区真实步行测时对比(2026-09),**一致率 90%**(高估、低估各 5%), 误差集中在边界附近——单个网格的"可达/不可达"不宜当定论,聚合口径请按约 **±5 个百分点**理解。 - **网格量化**:按"格心落在圈内"计整格,实测「格数 × 单格」比等时圈实际面积高约 **3%**; 250m 网格下每格翻转 ≈ 服务率跳变 **4 个百分点**。 - **POI 检出率未建模**:某类计数为 0 既可能是真实缺失,也可能是检索未覆盖(故 1km 盲区与覆盖明细需并看)。 ### 2.8 历史分析归档与回看 `analysis_record` 表以任务为键保存结构化指标快照:核心列(总分/等级/OD/绕行/POI 统计) + JSONB(8 类覆盖明细、四维得分、均衡指标、灰色区、建议、该次 POI 快照)。 前端「体检历史」可列表回看任意历史任务的综合分与结论,一键重新打开完整报告与地图; 支持**按社区名称检索**(模糊匹配、忽略大小写,命中条数实时提示,清除即回到全量); 提交体检前还会做**同点位查重**:同一经纬度(默认 25m 容差)已有记录时提示并可直接打开, 避免同一社区反复体检堆出一串重复历史。 ### 2.9 公文 / 建议书导出(可编辑 Word) 报告长图与 PDF 是**不可编辑的快照**,适合归档与转发;而街道办真正要往下走流程的是**公文**—— 要改数字、回填责任单位与工期、在 Word 里套红头与盖章。因此 `OfficialDocService` 把**同一份报告 JSON** 按文种组装为 DOCX,数据与页面同源,**不重新计算、不外推**: | 文种(`?type=`) | 篇幅 | 内容侧重 | | --- | --- | --- | | `brief` 领导专报 | 一页 | 总体评价 + 主要问题(盲区优先)+ 建议点位表 + 数据来源 | | `report` 体检评估报告 | 完整 | 基本情况 / 结论 / 四维评价 / 设施覆盖 / 供需匹配与均等性 / 步行网络 / 盲区与缺失区域 / 主要短板 / 附图 / **数据说明与局限性** | | `proposal` 补短板项目建议书 | 3~6 页 | 编制依据 / 基本情况 / 问题 / **项目清单表(含 BD-09 坐标、新增服务面积、覆盖率变化、预估投资、建议时序)** / 分期实施安排 / 预期效益 / **项目推进台账(待回填)** / 附图 / 附则 | **版式**(`DocStyles`,公文体按 GB/T 9704-2012):A4 纵向、页边距上 37/下 35/左 28/右 26 mm、 正文三号仿宋、一级标题黑体、二级标题楷体、文件标题二号小标宋、固定行距 29pt(对应每页约 22 行)、 页脚居中页码域。字体缺失时 Word 会自动回退(观感略变,不影响打开),也可用 `LC_DOC_TITLE_FONT` / `LC_DOC_BODY_FONT` 指定本机已装字体。 **零新增依赖**(`DocxWriter`):docx 本质是「zip + WordprocessingML」,用 JDK 自带的 `java.util.zip` 即可写出,故**不引入 Apache POI**(连带 xmlbeans 数 MB),保持 `mvn -o` 离线可构建—— 与 `PdfImageWriter` 手写 PDF 是同一取舍。写出的表格是 **Word 原生表格**(`w:tbl`,固定列宽、 表头跨页重复),**不是贴图**:拿到文件就能直接改数字、回填台账。 **三条不越界**(与全项目"不编数"的基线一致): 1. **数字与坐标全部来自报告 JSON**,本服务不做任何计算或推断;结论里的短板封顶(加权原始分 ≠ 落库分) 也如实写出原因; 2. **标准依据只从白名单取**(`DocCatalog`):只登记本项目实际用到的标准与行动计划,且标准号需人工核对, 不允许模型或代码"写条文"——这是大模型幻觉代价最高的一处; 3. **编制单位、文号、投资估算一律留占位**:未配置时分别输出"(编制单位:____)"、"(文号待编)"、 "待测算",并默认标注草案(`LC_DOC_DRAFT`)。文号、落款、金额属"谁发文谁负责", 系统不自动编造,避免导出件被误当正式公文。 附则章("数据说明与局限性")不是套话,而是**把可核验信息落纸**:坐标基准 BD-09、等时圈采样参数 (方向数 × 二分轮数取自当前配置)、本次实际调用与缓存命中次数、数据局限(`meta.caveats`)、 降级原因(`meta.degradedReasons`),以及"指标与坐标均可由体检报告复算"的声明—— 一份敢被审计的公文比好看的公文值钱。 --- ### 2.10 AI 质量门禁(规划问答评测集) 接上大模型不等于"AI 能力可交付"。项目在真实使用中踩过的坑(模型把"补点后预测值"当现状、 把内部判读标注念出来、明明给了建议点却写"无需新增设施")**换模型、改提示词都可能复发**, 所以把每类坑固化成用例,随时可重跑: | 用例 | 问题 | 期望意图 | 关键判定 | | --- | --- | --- | --- | | E01 | 请总述本社区的主要短板 | summary | 事实含"综合评分" | | E02 | 菜市场的覆盖情况如何? | coverage | 事实含"菜市场" | | E03 | 1km 内有没有服务盲区? | blindspot | 回答非空且不超长 | | E04 | 如果预算 3 处,补在哪里最划算? | optimize | **必须给出建议点** + 坐标齐全 | | E05 | 菜市场和公园哪个更弱? | compare | 回答非空 | | E06 | 这些指标是怎么算出来的? | method | 事实含"等时圈算法" | | E07 | 预算 0 处时还要补点吗? | optimize | 无点位也要诚实回答 | | E08 | 哪个社区得分最高? | rank | 服务端直出,不走模型改写 | 判定项(全部确定性):意图是否正确、事实是否给全、该给建议点时是否给了、 **数字一致性**、禁用措辞(内部标注/与建议点矛盾的"无需新增")、答案长度、引擎标识如实。 > **数字一致性是这套门禁的核心**:从回答里抽出带单位的数字(%/分/处/公顷/米/m/s), > 逐个核对是否出现在事实卡、建议点或计算口径中——于是"大模型不产生数字"这条核心设计 > **第一次有了自动验证**,而不再依赖人工试读。为避免误报,规则只查"带单位的数字" > ("15 分钟""8 类"这类口径常量不查),并把口径数字(半径/步行速度)纳入已知集合。 **门禁首跑就抓出 4 个真实缺陷**(这正是它存在的意义): 1. 回答引用了事实卡里没有的「配置标准 19/28/12 处」→ 补进盲区事实卡; 2. 回答引用了事实卡里没有的「距达标还差 N 处」→ 补进覆盖事实卡; 3. 口径说明把 900 秒写成"30 分钟阈值"→ 改为按配置换算(15 分钟); 4. 对真实大模型跑时发现:模型仍会把 `【短板项】` 等内部判读标注写进正文 → 除提示词新增约束外,再加一道**确定性清洗**(`stripInternalLabels`,与既有的 `guardAgainstPoints` 同一思路)。 **出口**:`POST /api/meta/ai-eval`(请求体为报告 JSON,返回 `{passed,total,passRate,violationCounts,avgLatencyMs,cases[]}`), 以及 JUnit `AiEvalTest` —— 未配置 `AI_API_KEY` 时整套在**规则引擎**上运行并可进 CI; 连上真实模型后再跑,就是一份现成的"模型质量体检报告"(注意大模型路径本身有随机性, 同一套用例两次结果可能不同——这恰恰说明门禁必须常跑)。 --- ## 系统运行截图 按一次完整体检的典型路径,系统运行界面如下: **① 首页与定位选点**:地址搜索(地理编码)或地图选点(逆地理编码回填真实地名,BD-09)。 ![首页与定位选点](images/首页与定位选点.jpg) **② 体检计算链路**:任务分阶段进度——逆地理编码 → POI 检索 → 等时圈测时 → 评价与报告。 ![体检计算链路](images/体检计算链路.jpg) **③ 报告总览**:综合评分 + 四维评价 + 15 分钟等时圈(总览主题页)。 ![报告总览](images/报告总览.jpg) **④ 设施覆盖**:8 类民生设施覆盖明细(数量 / 空间 / 圈内占比三口径)。 ![设施覆盖](images/设施覆盖.jpg) **⑤ 均等性**:基尼系数 + 区位熵 + 供需匹配(均等性主题页)。 ![均等性](images/均等性.jpg) **⑥ 步行网络**:非直线系数 + 通行障碍清单(路口 / 天桥 / 围挡)。 ![步行网络](images/步行网络.jpg) **⑦ 短板与规划**:1km 刚性盲区 + 灰色缺失面 + 250m 缺口网格 + 预算内选址。 ![短板与规划](images/短板与规划.jpg) **⑧ AI 规划助手**:自然语言提问 → 结论 + 事实卡 + 建议点坐标 + 追问建议。 ![AI规划助手回答结构](images/AI规划助手回答结构.jpg) --- ## 三、快速开始 > **本项目只有本地进程一种运行形态**:`mvn spring-boot:run` 或 `java -jar target/life-circle.jar`, > 配置全部来自 `application.yml` + `.env`(可叠加操作系统环境变量 / 命令行参数), > 没有容器编排等额外依赖;编译一次即可运行,改代码能立刻重启验证,日志与调试最直接。 ### 3.1 环境要求 | 项 | 要求 | 说明 | | --- | --- | --- | | JDK | **17+**(CI 同时验证 17 / 21) | `java -version` | | Maven | 3.8+(或用 IDE 内置的 Maven) | `mvn -v` | | PostGIS | PostGIS 15(**跑真实数据时必需**) | 见 3.4;本地或局域网实例均可,离线演示模式不需要 | | 百度 AK | 可选 | 未配置 → 离线演示模式(报告会**显著标注**,见第四章) | 两种运行形态,按需要选: | 形态 | 需要数据库 | 需要 AK | 产出 | | --- | --- | --- | --- | | **真实体检**(推荐) | ✅ PostGIS | ✅ 服务端 AK | 真实 POI / 路网测时 / 等时圈 / 完整报告 | | 离线演示 | ❌ | ❌ | 内置样例数据平移后的报告,页面顶部标注"不代表本社区" | > 为什么真实模式必须有数据库:POI 池、等时圈面、灰色区、历史记录都在 PostGIS 里, > 覆盖统计(`ST_DWithin` / `ST_Contains`)也由数据库算——**没有库,真实模式的体检会在统计阶段失败**。 > 离线演示模式则在加载样例数据后即返回,完全不碰数据库,所以能零依赖跑通。 ### 3.2 编译与启动(本地,推荐路径) 在项目根目录(`life-circle/`,即含 `pom.xml` 的那一层)执行: ```bash # ① 打包(跳过测试执行)→ 产物 target/life-circle.jar mvn clean package -DskipTests # ② 首次配置本地密钥:复制模板后填入 BAIDU_AK / BAIDU_JS_AK / AI_API_KEY cp .env.example .env # Windows PowerShell: Copy-Item .env.example .env # ③ 启动(会自动读取上一步的 .env,见 3.3) java -jar target/life-circle.jar # 或开发模式(改代码不用重新打包): mvn spring-boot:run ``` 启动成功后打开 。 启动日志里会明确告诉你当前是哪种形态,**以这行为准**: ``` INFO ... BaiduAdapter : 百度开放能力:AK 已配置,服务端调用正常 ← 真实模式 WARN ... BaiduAdapter : 百度开放能力:未配置 BAIDU_AK(或值仍为占位符), 进入离线演示模式——体检报告使用内置样例数据… ← 离线演示 ``` 其他常用命令: ```bash mvn test # 单元测试 mvn -o clean package -DskipTests # 离线构建(依赖已在本地仓库时) mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8081 # 换端口 ``` > **Windows PowerShell 注意**:Maven 参数名中若含点(如 `-Dmaven.test.skip=true`), > PowerShell 会在第一个点处把参数拆成两个,报 `Unknown lifecycle phase ".test.skip=true"`。 > 请三选一:① 加引号 `mvn clean package "-Dmaven.test.skip=true"`; > ② 用停止解析符 `mvn clean package --% -Dmaven.test.skip=true`; > ③ 改用不带点的 `mvn clean package -DskipTests`(推荐,等价于跳过测试执行)。 > 另:`compile` 阶段本就不编译/运行测试,该参数加在 `compile` 上是多余的。 > > 两者区别:`-DskipTests` 仍会**编译**测试类、只是不执行;`-Dmaven.test.skip=true` 连测试类一起**不编译**(构建更快)。 ### 3.3 本地启动时如何带上 `.env`(环境设置) 仓库里 `application.yml` 只放**占位符**(见第四章),真实密钥走 `.env` 或环境变量。 本地启动有四种方式,**推荐方式一(零操作)**: #### 方式一(推荐):`.env` 自动加载 —— 不需要任何额外步骤 `application.yml` 中已声明: ```yaml spring: config: import: optional:file:./.env[.properties] ``` 于是只要**在项目根目录**用下面任一方式启动,`.env` 就会被自动当作 properties 文件读入: ```bash mvn spring-boot:run java -jar target/life-circle.jar ``` | 细节 | 行为 | | --- | --- | | 没有 `.env` | 不报错(`optional:`),回落到占位符 → 离线演示模式(日志/报告三处标注) | | `.env` 写法 | 必须是 `KEY=value`:**不要**写 `export KEY=…`、**不要**在值后面写行内注释、**不要**用引号包整行 | | 注释怎么写 | 单独占一行(以 `#` 开头)。例如 `BAIDU_MAX_CONCURRENT=3` 后面不能跟 `# 并发上限` | | 优先级 | 命令行参数 > 操作系统环境变量 > `.env` > `application.yml` 默认值 | | 工作目录 | 路径是 `./.env`,请**在项目根目录启动**(换目录启动时读不到) | | 配置来源 | 只有三处:命令行参数 / 操作系统环境变量 / `.env`(+ `application.yml` 默认值),无任何外部注入 | > `.env.example` 已按上述写法整理好(说明一律单独成行),照它的格式填即可。 #### 方式二:命令行参数(临时覆盖,优先级最高) ```bash java -jar target/life-circle.jar --baidu.ak=你的服务端AK --lifecircle.ai.api-key=sk-xxx ``` #### 方式三:环境变量 ```powershell # Windows PowerShell(仅当前窗口有效) $env:BAIDU_AK='你的服务端AK' $env:BAIDU_JS_AK='你的浏览器端AK' $env:AI_API_KEY='sk-xxx' java -jar target\life-circle.jar ``` ```bash # Linux / macOS / Git Bash export BAIDU_AK=你的服务端AK export BAIDU_JS_AK=你的浏览器端AK export AI_API_KEY=sk-xxx java -jar target/life-circle.jar ``` > 想让它长期有效:Windows 用 `setx BAIDU_AK "…"`(新开终端生效);Linux 写进 `~/.bashrc`。 > 但注意这会覆盖 `.env`(环境变量优先级更高),排障时容易被"旧变量"误导——推荐还是用方式一。 #### 方式四:一键脚本(`scripts/demo.sh` / `demo.ps1`) 脚本会先载入 `.env` 再启动,但**两种模式的语义不同,别混用**: ```bash ./scripts/demo.sh # lite(默认):强制 LC_MOCK=true 的**离线演示**(零依赖,评审体验用) ./scripts/demo.sh local # 本地真实数据:载入 .env + 本地 PostGIS + 真实百度开放能力 ``` > ⚠️ `lite` 会设置 `LC_MOCK=true`,**即使 `.env` 里配了 AK 也不会调百度**(它就是"零依赖演示"档)。 > 要跑**真实数据**,用 `local` 模式,或按方式一/二/三直接起 `mvn spring-boot:run` / `java -jar`(会自动读 `.env`)。 --- ### 3.3.1 完整示例:从 `.env` 配置文件启动服务(真实数据模式) 下面给出一条最常用、最不容易出错的完整命令流(以 PowerShell 为例,Bash 只需把路径分隔符 `/` 换成 `\`)。 ```powershell # 步骤 1:进入项目根目录(含 pom.xml 和 .env 的那一层) cd c:\Users\dbrg\WorkBuddy\2026-08-18-22-03-16\life-circle # 步骤 2:首次配置:复制模板后编辑 .env Copy-Item .env.example .env notepad .env # 填入 BAIDU_AK / BAIDU_JS_AK / AI_API_KEY(如不需要 AI 可留空) # 步骤 3:打包(跳过测试执行) mvn clean package -DskipTests # 步骤 4:用 .env 启动真实模式服务 java -jar target/life-circle.jar ``` 启动后控制台会打印: ``` INFO ... BaiduAdapter : 百度开放能力:AK 已配置,服务端调用正常 INFO ... LifeCircleApplication : Started LifeCircleApplication in ... seconds ``` 此时打开浏览器访问 ,页面顶部不会显示"离线演示"提示条,体检结果来自真实百度 POI / 路网测时。 **如果 `.env` 填错了或没填**,日志会出现: ``` WARN ... BaiduAdapter : 百度开放能力:未配置 BAIDU_AK(或值仍为占位符), 进入离线演示模式——体检报告使用内置样例数据… ``` 这表示服务仍在运行,但分析走的是内置样例数据。修改 `.env` 后**重启**即可切换到真实模式。 **验证是否读到 `.env` 的三种方法**: 1. 看启动日志里的 `BaiduAdapter` 行(上文的 INFO / WARN); 2. 访问 ,响应中出现 `"baidu":{...}` 即说明后端持有 AK; 3. 在页面发起一次体检,报告顶部「数据来源」写"百度地图开放平台实时检索"而非"离线演示"。 ### 3.4 准备数据库(PostGIS) 真实模式需要一个 PostGIS 库(**本地或局域网实例均可**): **A. 本机已装 PostgreSQL + PostGIS** ```bash # 先在 .env 里写好连接信息(DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD) ./scripts/init-db.sh # 建表;Windows:powershell -File scripts\init-db.ps1 java -jar target/life-circle.jar ``` **B. 本机还没有 PostgreSQL** 按官方安装包/包管理器安装 PostgreSQL,再执行 `CREATE EXTENSION postgis;`(或安装 `postgis` 包)即可: ```bash # Debian/Ubuntu 例:apt install postgresql postgis # Windows/macOS:用官方安装器(EDB / Postgres.app),勾选 PostGIS 组件 psql -U postgres -c "CREATE DATABASE lifecircle;" ./scripts/init-db.sh java -jar target/life-circle.jar ``` **C. 用远程/共享的 PostGIS** 在 `.env` 里把 `DB_HOST` / `DB_PORT` 指向该实例(默认 `localhost:5432`)后同样执行 `init-db.sh`。 > 建表说明:`db/schema.sql` 全量幂等(`CREATE EXTENSION/TABLE/INDEX IF NOT EXISTS`), > 因此 `init-db.sh`(Windows:`init-db.ps1`)**任何时刻都能重跑**,用于首次建表或升级表结构,不影响既有数据。 > 脚本用的是本机 `psql` 客户端;若本机没有,可直接把 `src/main/resources/db/schema.sql` 粘进任意 SQL 客户端执行—— > 内容完全一致(该文件也是应用随包携带的唯一建表脚本)。 > 完全没有数据库也能体验全部功能:`LC_MOCK=true` 的离线演示模式不碰数据库(见 3.1)。 ### 3.5 常用命令速查 | 场景 | 命令 | | --- | --- | | **本地打包(推荐)** | `mvn clean package -DskipTests` | | **本地启动(读 `.env`)** | `java -jar target/life-circle.jar` | | **本地开发模式启动** | `mvn spring-boot:run` | | 换端口启动 | `mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8081` | | 初始化/升级表结构 | `./scripts/init-db.sh`(Windows:`powershell -File scripts/init-db.ps1`) | | 一键离线演示(零依赖) | `./scripts/demo.sh`(Windows:`scripts\demo.ps1`) | | 一键本地真实数据 | `./scripts/demo.sh local`(Windows:`scripts\demo.ps1 local`) | | 单元测试 | `mvn test` | | 访问首页 | | --- ## 四、AK 配置 > ⚠️ 百度 AK 按「应用类型」区分,服务端与浏览器端是两个不同的 AK, > 需在 [百度地图控制台](https://lbs.baidu.com/apiconsole/center) 分别创建应用后获取: | AK | 用途 | 应用类型 | 校验方式 | 是否暴露 | | --- | --- | --- | --- | --- | | `baidu.ak` | 后端调用 Web 服务 API(地理编码 / POI 检索 / 步行路线规划(轻量) / 坐标转换) | 服务端 | IP 白名单(本地可填 `0.0.0.0/0`) | ❌ 仅服务端 | | `baidu.js-ak` | 前端加载百度 JS API GL 真实底图 | 浏览器端 | Referer 白名单(如 `http://localhost:8081/*`) | ✅ 随页面下发(正常做法) | **配置方式**:复制 `.env.example` 为 `.env` 并填入,然后在**项目根目录**启动即可 (`mvn spring-boot:run` / `java -jar target/life-circle.jar` 都会自动读取 `.env`)。 ```bash cp .env.example .env # Windows PowerShell: Copy-Item .env.example .env # 编辑 .env,填入 BAIDU_AK / BAIDU_JS_AK / AI_API_KEY java -jar target/life-circle.jar ``` `.env` 的写法要求、优先级、以及另外三种给值方式(命令行参数 / 环境变量 / 页面临时注入), 统一见 **3.3 本地启动时如何带上 `.env`**。 #### 仓库里只放占位符(开源合规) `src/main/resources/application.yml` 与 `.env.example` 中**只有占位符**,没有任何真实密钥: | 配置项 | 默认值(占位符) | 说明 | | --- | --- | --- | | `baidu.ak` / `BAIDU_AK` | `your_server_ak_here` | 服务端 AK,后端调用 Web 服务 API | | `baidu.js-ak` / `BAIDU_JS_AK` | `your_browser_ak_here` | 浏览器端 AK,前端底图(留空自动回退用 `ak`) | | `lifecircle.ai.api-key` / `AI_API_KEY` | `your_ai_api_key_here` | 大模型 Key,留空自动回退本地规则引擎 | **没填 Key 会怎样(重要)**:应用照样启动,但进入**离线演示模式**(内置样例数据)。 为避免"样例报告被当成真实结论",三处同时标注: 1. **启动日志**:`WARN ... 未配置 BAIDU_AK(或值仍为占位符),进入离线演示模式`; 2. **报告头部「数据来源」**:显示 `离线演示模式:内置样例数据(未调用百度地图开放平台,不可作为体检结论)`; 3. **报告顶部降级提示条**:列出降级原因("本报告不代表该社区真实情况")。 判定口径收敛在 `com.lifecircle.util.ApiKeyUtil`:空值、长度过短、以及含 `your_` / `_here` / `placeholder` / `请填入` 等占位词的"密钥"一律视为**未配置**——避免拿占位符去发请求、把 "鉴权失败"误报成"这个社区没有设施"。填入真实 Key 后重新体检即为真实结论。 > 安全提示:`.env` 已加入 `.gitignore`,请勿把真实 Key 写回仓库内的任何配置文件; > 仓库内自检脚本:`sh scripts/check-secrets.sh`(CI 亦会执行)。 ### 关键调优参数(`application.yml` 默认值 / `.env` 或环境变量覆盖) | 变量 | 默认 | 说明 | | --- | --- | --- | | `BAIDU_QPS` | 12 | 令牌桶速率(全站唯一闸门;被 401 拒绝时 AIMD 自动降 20%、10 秒后逐步恢复) | | `BAIDU_MAX_CONCURRENT` | 30 | **未识别接口**的并发回退值(2026-09-20 平台配额调整后,各族已分别配置,见下) | | `BAIDU_REVERSE_CONCURRENT` | 30 | 逆地理编码并发(平台配额) | | `BAIDU_GEOCODE_CONCURRENT` | 30 | 地理编码 / 坐标转换并发(平台未单独给出,与逆地理同档) | | `BAIDU_PLACE_CONCURRENT` | 30 | 地点检索并发(平台配额) | | `BAIDU_WALKING_CONCURRENT` | 30 | 步行路线规划(轻量)并发(平台配额;本项目最主要的调用) | | `BAIDU_MATRIX_CONCURRENT` | 200 | 批量算路并发(平台配额;后台当前不调用,闸门备用) | | `BAIDU_DRIVING_CONCURRENT` | 8 | 驾车路线规划并发(配额未明确,保守取 8;信号灯通道默认关闭) | | `BAIDU_POOL_CORE` / `BAIDU_POOL_MAX` | 8 / 128 | 线程池常驻 / 弹性上限(真正把关的是上面的并发闸门) | | `BAIDU_CACHE_PLACE_DAYS` | 7 | POI 响应缓存天数(URL 级;命中不发请求、不消耗配额) | | `BAIDU_CACHE_ROUTE_DAYS` | 0 | 算路(批量算路/步行耗时/必经指引)与地理编码缓存天数。**0 = 不缓存、每次都真实调用**;调成 3(天)可省配额,代价是同一 OD 沿用可能已变化的耗时 | | `LC_ISO_ROUTE_MODE` | batch | 等时圈测时的算路方式:`batch`=批量算路(一轮 1 次请求 × 5 轮)/ `single`=逐点步行轻量(一轮 36 次)。**换这个开关会带来耗时可观测位移**(批量比轻量短 4%~12%,见设计文档 §6.4.1ay),跨版本报告不可混着比 | | `BAIDU_MIN_INTERVAL_MS` | 0 | **每线程**最小间隔(只防单线程突发;全站速率由令牌桶负责) | | `BAIDU_JITTER_MS` | 0 | 最小间隔之上的随机抖动 | | `BAIDU_ROUND_INTERVAL_MS` | 0 | 等时圈分轮测时的轮间隔(令牌桶已限速,无需额外等待) | | `LC_ISO_EARLY_STOP` | 0 | 等时圈自适应提前停止阈值(米),0 = 关闭(实测只省 4%,且无离线精度证据) | | `BAIDU_PLACE_DAILY` | 3000 | 地点检索日配额熔断阈值 | | `BAIDU_ROUTE_DAILY` | 10000 | 步行路线规划(轻量)日配额熔断阈值(按 OD 计,一次体检 36×5=180 OD) | | `BAIDU_GEOCODE_DAILY` | 10000 | 地理编码日配额熔断阈值 | | `LC_MOCK` | false | 强制离线样例模式 | | `LC_OBSTACLE_PATH` | true | 通行障碍点「路径实测」通道:从真实步行路线指引文本提取过街设施 | | `LC_OBSTACLE_CORRIDORS` | 8 | 路径实测走廊条数(中心 → 等时圈边缘均匀取点,每条 1 次步行算路,走缓存) | | `LC_OBSTACLE_SIGNAL_CORRIDORS` | 0 | 红绿灯"个数"实测的驾车走廊条数。**默认 0(关闭)**:该通道要用驾车算路,与"后台一律步行轻量"的口径冲突;>0 时每条 1 次驾车算路(走缓存) | | `LC_POI_REUSE_LIMIT` | 500 | 库内 POI 每类取用条数上限(给足,才与覆盖统计的全池口径一致) | | `AI_ENABLED` | true | 大模型总开关(关闭后一律走本地规则引擎,不产生外部调用) | | `AI_BASE_URL` | `https://api.siliconflow.cn/v1` | 大模型接口(OpenAI 兼容协议) | | `AI_API_KEY` | 空 | 硅基流动 API Key;留空时自动回退规则引擎,功能不降级 | | `AI_MODEL` | `Qwen/Qwen2.5-7B-Instruct` | 对话模型(可换 `deepseek-ai/DeepSeek-V3` 等) | | `AI_ASK_PER_MINUTE` | 20 | 自然语言问答限流(次/任务/分钟) | | `LC_DOC_STYLE` | official | 公文版式:`official`(公文体,GB/T 9704-2012 版式)/ `report`(技术报告体) | | `LC_DOC_ISSUER` | 空 | 编制单位(落款)。**留空时文件上留待填占位**——文号与落款属"谁发文谁负责",系统不自动编造 | | `LC_DOC_NO` | 空 | 文号;留空时输出"〔 〕  号(文号待编)" | | `LC_DOC_DRAFT` | true | 是否标注草案(默认标注;须经复核、签发后方为正式文件) | | `LC_DOC_SHOW_MAP` | true | 是否附现状图(复用报告长图的同一套服务端地图绘制器) | | `LC_DOC_LEDGER` | true | 是否附「项目推进台账(待回填)」空表 | | `LC_DOC_TITLE_FONT` / `LC_DOC_BODY_FONT` | 空 | 公文标题 / 正文字体(留空用国标字体名:方正小标宋简体 / 仿宋_GB2312;缺失时 Word 自动回退) | | `lifecircle.doc.cost.<类别>` | 空 | 单处投资估算区间文案(如 `market: 200~500 万元/处`)。**默认空 → 表格显示"待测算"**,不产生任何未经确认的金额 | ### 大模型接入(AI 诊疗建议 + 自然语言规划问答) 默认对接 **硅基流动 SiliconFlow**(接口与 OpenAI 完全兼容),三步启用: ```bash # ① 在 https://cloud.siliconflow.cn 注册并创建 API Key # ② 写入 .env(也可用操作系统环境变量 / 命令行参数) # 注意:KEY=value 形式,说明单独占一行,别写成行内注释 echo "AI_API_KEY=sk-你的Key" >> .env # ③ 重启应用即可生效——本地 java -jar / mvn spring-boot:run 会自动读取 .env(见 3.3) ``` > **未配置 Key 也能用**:规划建议与自然语言问答会自动回退「服务端规则引擎」, > 仍返回结构化事实 + 中文结论,响应体 `engine` 字段如实标注 `llm` / `rule`,不会把模板回答伪装成大模型输出。 > 换服务商(DeepSeek / 通义 / 本地 Ollama)只需改 `AI_BASE_URL` 与 `AI_MODEL`,**代码零改动**。 > > **模板统一在服务端**:问答的快捷问题、欢迎语、自动提问、追问建议、引擎展示名、分人群结论段落, > 全部由服务端下发(`GET /api/analysis/ask-meta` + `POST /api/analysis/{id}/ask`), > 前端 `ask.js` 只保留渲染与请求。此前前端曾内置一份与后端重复的本地规则引擎与模板, > 两处独立维护必然漂移(同一份报告可能出现两套数字),已移除;离线时如实提示需要连接后端。 --- ## 五、目录结构 ``` src/main/java/com/lifecircle/ LifeCircleApplication.java config/ BaiduProperties / LifeCircleProperties / Async / Cache / MybatisPlus / Throttle adapter/baidu/ BaiduAdapter(统一出口)/ BaiduQuotaMeter / BaiduException service/ Isochrone 等时圈 / Poi 检索清洗 / Coverage 覆盖与灰色区 / BlindSpot 盲区 / Obstacle 通行障碍 / Equity 供需匹配 / Evaluation 规范权重四维 / Network 步行网络 / HistoryRecord 历史归档 / PlanOptimize 片区规划优化(网格诊断 + 贪心选址)/ Ai 自动诊疗 / Sample 样例 / Analysis 编排 service/ai/ LlmClient 大模型客户端(OpenAI 兼容,默认硅基流动)/ PlanningAskService 规划问答编排(意图 → 计算 → 表达;含内部标注清洗与建议点兜底)/ RulePlanner 规则降级(意图关键词解析 + 中文回答模板)/ AiEvalService AI 质量门禁(8 条用例 + 数字一致性自动校验,未配 Key 时走规则引擎) service/export/ ReportCanvas 报告长图渲染 / MapSnapshotPainter 地图快照 / PdfImageWriter 零依赖多页 PDF 封装 / ExportFontResolver 中文字体解析 / DocxWriter 零依赖 DOCX 写出(段落/原生表格/内嵌图片/页码页脚)/ DocStyles 版式常量(公文体 GB/T 9704 + 报告体)/ ReportRenderService 导出编排 service/doc/ OfficialDocService 公文组装(三文种:专报/评估报告/项目建议书)/ DocCatalog 编制依据白名单(只登记项目实际用到的标准) controller/ Index(Thymeleaf) / Analysis(任务+历史+记录) / Export(图片、PDF 与公文 DOCX 导出) / Geo / Meta / 全局异常处理 mapper/ Task / Poi(PostGIS 空间 SQL)/ Isochrone / GrayArea / BaiduCache / AnalysisRecord model/ entity(PostGIS 表)+ dto(报告 VO / 统一响应) util/ Bd09Transform / GeoUtil / IdwInterpolator / EntropyWeight / GiniUtil / RateLimiter / ApiThrottle / JsonGeo src/main/resources/ templates/index.html 页面(Thymeleaf,AK 由服务端注入) static/{css,js,data} 前端资源(map/sample/api/app/heat/charts/report/ask/export)与样例数据 db/schema.sql PostGIS 建表(含生成列与 GIST 索引) docs/技术设计文档.md 算法与调用策略的详细设计文档 ``` --- ## 六、接口一览 | 方法 | 路径 | 说明 | | --- | --- | --- | | POST | `/api/analysis/run` | 提交体检任务,返回任务 `id`(字符串,雪花 ID 防 JS 精度丢失) | | GET | `/api/analysis/{id}` | 任务状态:`status/percent/stage/result/errorMsg` | | GET | `/api/analysis/{id}/report` | 完整报告 JSON | | GET | `/api/analysis/{id}/isochrone?minutes=15` | 等时圈 GeoJSON(BD-09) | | GET | `/api/analysis/{id}/poi` | POI 列表(含是否在圈内) | | GET | `/api/analysis/history?page=&size=&keyword=` | 体检历史**分页**(按创建倒序):返回 `{records,total,page,size,pages,hasPrev,hasNext}`,含综合分/等级/结论摘要;`size` 默认 6(上限 100),兼容旧参数 `limit`(等价 size,仅第 1 页);`keyword` 按**社区名称模糊检索**(忽略大小写,后端筛选后再分页,total/pages 为筛选后口径) | | GET | `/api/analysis/find-by-center?lng=&lat=&tolerance=` | **同点位查重**:该经纬度是否已有体检记录(默认容差 25m,排除失败任务)→ `{found:false}` 或 `{found:true, task:{…与历史列表同构…}}`;前端在**提交体检之前**调用,命中即提示并可直接打开已有报告,不新建任务 | | GET | `/api/analysis/{id}/record` | 单次体检结构化历史记录(核心指标列 + 覆盖/均衡/建议/POI 快照 JSONB) | | GET | `/api/analysis/{id}/plan?budget=3` | **片区规划优化**:250m 网格服务诊断 + 预算约束贪心选址(纯几何计算,零额外地图 API 调用) | | POST | `/api/analysis/{id}/ask` | **自然语言规划问答(AI 规划助手)**:请求 `{question}` → 大模型解析意图 → 服务端确定性计算 → 返回 `answer` + `facts`(结构化事实)+ `points`(建议设施点,含 BD-09 坐标,可直接落图)+ `followUps`;支持 8 类意图(补点方案 / 覆盖诊断 / 盲区排查 / 类别对比 / **跨社区排名**(如"哪个社区得分最高?"——读历史记录表,同名社区只取最新一条,本社区位次按 taskId 精确匹配)/ 整体解读 / 口径说明 / **助手自我介绍与能力说明**,其中自我介绍与跨社区排名由服务端如实直出、不交给模型改写);未配置 `AI_API_KEY` 时自动走规则引擎(`engine=rule`) | | DELETE | `/api/analysis/{id}` | **删除一次体检记录**:级联清理等时圈 / 灰色区域 / 结构化历史快照 / 任务本身;**刻意保留全局 POI 池与百度响应缓存**(跨任务复用,删某社区不影响其他社区的数据与配额缓存);运行中的任务不可删除 | | GET | `/api/geo/geocode?q=&city=` | 地址 → 坐标候选(真实地理编码,失败降级候选库) | | GET | `/api/geo/reverse?lng=&lat=` | 坐标 → 地址(真实逆地理编码) | | GET | `/api/meta/categories` | 8 类设施体系(菜市场 / 药店 / 教育 / 医疗 / 公园 / 交通 / 金融文体 / 养老) | | GET | `/api/meta/quota` | 百度各 API 配额使用情况 | | POST | `/api/analysis/export/png` | **服务端导出报告长图**:请求体为页面报告 JSON → 返回 `image/png`(内容与页面一致且含全部明细) | | POST | `/api/analysis/export/pdf` | **服务端导出 PDF**:A4 多页完整输出(长图按内容区比例切片,逐页内嵌,带页码) | | GET | `/api/analysis/{id}/export/png` | 按任务 id 导出 PNG(使用服务端落库报告,适合分享/归档) | | GET | `/api/analysis/{id}/export/pdf` | 按任务 id 导出 PDF | | POST | `/api/analysis/export/docx?type=` | **服务端导出公文 / 建议书(可编辑 Word)**:`type=brief`(领导专报)\| `report`(体检评估报告)\| `proposal`(补短板项目建议书,含项目清单表、分期安排与待回填台账)→ 返回 `...wordprocessingml.document` | | GET | `/api/analysis/{id}/export/docx?type=` | 按任务 id 导出公文 DOCX(方便街道办直接下载归档件) | | GET | `/api/analysis/{id}/export/info` | 导出信息预览(社区/城市/综合分/POI 数与灰色区、障碍数量) | | POST | `/api/meta/ai-eval` | **AI 质量门禁**:请求体为报告 JSON → 跑 8 条规划问答用例,返回 `{provider,model,llmOnline,total,passed,passRate,violationCounts,avgLatencyMs,cases[]}`;判定含**数字一致性**(回答里带单位的数字必须能在事实卡/建议点/口径里找到);未配置 `AI_API_KEY` 时整套在规则引擎上运行,可进 CI | > 导出文件由服务端 **Java2D 渲染**(无浏览器、无 CDN 依赖):先渲染整份报告长图,再按页封装为 PDF; > PDF 为自研零依赖封装(`/Filter /DCTDecode` 内嵌 JPEG),中文由位图承载因此不会乱码。 > 中文字体解析顺序:`lifecircle.export.font` → classpath:`fonts/*` → 系统字体(Windows 黑体/雅黑、 > Linux Noto Sans CJK、macOS PingFang)→ 常见字体目录扫描。Windows/macOS 通常开箱可用; > 纯 Linux 服务器需自行安装中文字体(如 `fonts-noto-cjk`),否则导出中文会呈方框。 > > **PDF 分页规则**:每页内容严格裁剪在「页高 − 页脚带」以内(页脚不被内容占用), > 切片点自动吸附到长图中的空白行(表格行/段落之间),不会把一行文字或一条记录切成两半; > 页码在页数确定后统一绘制。前端导出的 **HTML 报告内嵌矢量 SVG 地图**(等时圈环带 / 灰色区 / > 设施点 / 障碍点 / 图例 / 比例尺),单文件自包含,离线打开或打印时地图始终可见。 > > **DOCX 与 PNG / PDF 的分工**:PNG / PDF 是**不可编辑的快照**,适合归档与转发; > DOCX 是**可编辑的公文**(版式见 §2.9),街道办拿到后要改数字、回填责任单位与工期、套红头盖章, > 因此表格一律写成 Word 原生表格而不是贴图。DOCX 由自研零依赖写出器生成(`DocxWriter`,zip + WordprocessingML), > 与 `PdfImageWriter` 同一理由:**不引入 Apache POI,保持 `mvn -o` 离线可构建**。 统一响应包:`{code, msg, data}`,`code=0` 为成功。 --- ## 七、性能与稳定性设计 - **计算下推**:覆盖统计、圈内判定、复用检索全部下推 PostGIS(`ST_Contains`/`ST_DWithin` + GIST),避免大 POI 集回传 JVM; - **请求优化**:扇形二分(36 方向 × 5 轮 = 180 个 OD)改用**批量算路**,一轮 1 次请求、5 轮共 **5 次** (逐点要 180 次,同精度下比逐点线性扫描少 99%),另加 8 次步行走廊(障碍点必经性,需逐步指引文本故走轻量接口), **算路不做缓存(每次都真实调用)**;POI 仍按 URL 缓存 7 天(驾车"信号灯个数"通道默认关闭,启用后再加 4 次驾车算路); - **限流与容错**:**按接口族独立的并发闸门**(逆地理 30 / 地点检索 30 / 步行轻量 30 / 批量算路 200, 与平台配额一致;各族互不挤占)+ 最小间隔(抖动) + **全站 QPS 令牌桶**(速率的唯一闸门), 并叠加 **AIMD 自适应限速**(收到 302/401/4 时下调 20% 并重试,未被拒则逐步回收—— 系统自动停在"刚好不被拒"的速率);指数退避重试(仅限流/网络类可恢复错误,参数错误直接失败); 按 API 日配额熔断并降级; - **实测性能(2026-09,同一 AK,每次都是全新社区)**:单次步行测时接口延迟 **≈92ms**。 瓶颈演进有两步:① 早期"全局串行放行 + 500ms 间隔"把吞吐压到 ≈1.7 req/s(总耗时 185s); ② 改为**并发发起 + 令牌桶限速**后降到 76.4s;③ 平台把并发配额调到 30/30/30/200 后 (本项目同步改为按接口族独立闸门,见设计文档 §6.4.1au):**冷启动总耗时 37.6s(墙钟 40s)**, 其中 POI 检索 12.9s、步行测时 **15.1s**(180 个 OD)、组装 8.0s,**被限流 0 次**。 注意:步行测时 180 OD ÷ 15.1s ≈ **11.9 req/s,已贴着 QPS 闸门(12 req/s)**—— 即**并发不再是瓶颈、速率成了瓶颈**;若平台同时提高了每秒配额,把 `BAIDU_QPS` 调到 20~30 即可继续提速 (AIMD 仍会在被拒时自动退让)。 ④ 等时圈改用**批量算路**后(§6.4.1ay):步行测时阶段 **15.1s → 0.4s**(180 OD / 5 次请求), 瓶颈转移到 POI 检索;全新城市冷启动的算路部分实测 **5 次请求**、一天内多次重跑也**每次都真实调用**(算路不缓存)。 **⚠️ 口径位移提示**:两个算路服务的耗时口径不同(同一批 OD 实测:批量比轻量短 **4%~12%**,距离几乎一致), 所以同一中心的 15 分钟环均半径 732m → **807m**、综合分 89 → **93**——**跨版本报告的面积与得分不可直接比** (报告 `isochrone.properties.model` 写明本次用的服务;非直线系数为距离口径,不受影响)。 同一社区重复体检(POI 缓存 7 天)**约 13s**,日志里可直接看到"缓存命中 N / 实调 0"。每阶段耗时都写进日志 (`阶段耗时:POI 检索 / 步行测时 / 盲区与障碍点 / 评估与报告组装`); 服务端 SQL 日志默认关闭(此前一次体检 7.2MB 输出 → 现 12KB); - **缓存与复用**:`baidu_cache` 按 URL 持久缓存百度响应(place 7 天,同中心点重复分析零额外调用) + POI 全局池库内补充(只并入结果,不替代检索); - **异步任务**:独立线程池执行长任务,前端真实进度轮询(可最小化后台、可并发发起多次,仅渲染最新结果),页面不被长任务阻塞; - **降级兜底**:POI 检索失败返回库内已有、测时失败方向保守内缩、数据库不可用切内存任务、无 AK 自动离线样例,保证流程不中断; - **导出不依赖浏览器**:报告图片与 PDF 由服务端渲染,长报告按内容自适应高度、PDF 自动分页,避免 前端画布在超长内容/WebGL 地图/跨域画布下的截断与空白;前端渲染仅作为纯静态部署时的降级路径。 --- ## 八、测试 ```bash mvn test # 坐标变换 / IDW 插值与等时线 / 熵权 / 基尼 / JSON 坐标变换 / 限流节流 # + 报告导出:长图渲染、PNG 有效性、PDF 结构与 xref 偏移校验、导出接口 HTTP 层 ``` --- ## 九、开源规范 - 许可证:Apache-2.0(见 `LICENSE`); - 密钥脱敏:`.env.example` 占位,`.gitignore` 排除 `.env`; - CI:GitHub Actions(JDK 17/21 矩阵 → 编译 → 测试 → Spotless 格式检查 → 打包); - 一键运行:`mvn spring-boot:run` / `java -jar target/life-circle.jar`(只有本地进程一种形态,无容器依赖); - 时区:报告「数据日期」取 JVM 本地日期,跨时区机器建议显式加 `-Duser.timezone=Asia/Shanghai`(或在系统层面设为东八区),否则日期可能差一天。