门店信息三方一致性审计:站内、地图平台与 AI 回答口径差异的量化与修复前后数据
适用读者:连锁门店、本地生活服务商的运维与数字化负责人;门店信息同时在官网、地图平台、点评平台三处维护,改一次营业时间要发三个后台,而 AI 引擎回答"这家店周日开不开门"时还是说不准的团队。审计脚本基于 Python 3.10+。
先看一组实测数据。我们对一个 87 家门店的连锁品牌做了一次"同一字段三方比对":同一批门店,把官网、地图平台、AI 回答里的营业时间、电话、地址三项拿来逐店核对。结果是三项全部一致的只有 41 家(47%),剩下 46 家至少有一项对不上;最典型的一家店,官网写 09:00--21:00,地图平台写 08:30--21:30,AI 回答给出的是"全天营业"。
AI 那个错误答案的来源很有意思:不是它编的,而是它把某个第三方聚合站点的过时信息当成了证据。这件事说明,本地门店的 AI 可见性问题,很多时候不是站内结构化数据没做,而是对外口径不一致。这篇讲怎么把这种不一致量化出来,以及修复前后的数据变化。
一、为什么"一致性"比"字段完整度"更重要
做本地 SEO 的团队对字段完整度很敏感:有没有 geo、有没有 openingHoursSpecification、有没有评分。但引擎侧的实际判断逻辑是两层的:
#mermaid-svg-Tc7PLjmwBM8hbeeu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Tc7PLjmwBM8hbeeu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Tc7PLjmwBM8hbeeu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Tc7PLjmwBM8hbeeu .error-icon{fill:#552222;}#mermaid-svg-Tc7PLjmwBM8hbeeu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Tc7PLjmwBM8hbeeu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Tc7PLjmwBM8hbeeu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Tc7PLjmwBM8hbeeu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Tc7PLjmwBM8hbeeu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Tc7PLjmwBM8hbeeu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Tc7PLjmwBM8hbeeu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Tc7PLjmwBM8hbeeu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Tc7PLjmwBM8hbeeu .marker.cross{stroke:#333333;}#mermaid-svg-Tc7PLjmwBM8hbeeu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Tc7PLjmwBM8hbeeu p{margin:0;}#mermaid-svg-Tc7PLjmwBM8hbeeu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Tc7PLjmwBM8hbeeu .cluster-label text{fill:#333;}#mermaid-svg-Tc7PLjmwBM8hbeeu .cluster-label span{color:#333;}#mermaid-svg-Tc7PLjmwBM8hbeeu .cluster-label span p{background-color:transparent;}#mermaid-svg-Tc7PLjmwBM8hbeeu .label text,#mermaid-svg-Tc7PLjmwBM8hbeeu span{fill:#333;color:#333;}#mermaid-svg-Tc7PLjmwBM8hbeeu .node rect,#mermaid-svg-Tc7PLjmwBM8hbeeu .node circle,#mermaid-svg-Tc7PLjmwBM8hbeeu .node ellipse,#mermaid-svg-Tc7PLjmwBM8hbeeu .node polygon,#mermaid-svg-Tc7PLjmwBM8hbeeu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Tc7PLjmwBM8hbeeu .rough-node .label text,#mermaid-svg-Tc7PLjmwBM8hbeeu .node .label text,#mermaid-svg-Tc7PLjmwBM8hbeeu .image-shape .label,#mermaid-svg-Tc7PLjmwBM8hbeeu .icon-shape .label{text-anchor:middle;}#mermaid-svg-Tc7PLjmwBM8hbeeu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Tc7PLjmwBM8hbeeu .rough-node .label,#mermaid-svg-Tc7PLjmwBM8hbeeu .node .label,#mermaid-svg-Tc7PLjmwBM8hbeeu .image-shape .label,#mermaid-svg-Tc7PLjmwBM8hbeeu .icon-shape .label{text-align:center;}#mermaid-svg-Tc7PLjmwBM8hbeeu .node.clickable{cursor:pointer;}#mermaid-svg-Tc7PLjmwBM8hbeeu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Tc7PLjmwBM8hbeeu .arrowheadPath{fill:#333333;}#mermaid-svg-Tc7PLjmwBM8hbeeu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Tc7PLjmwBM8hbeeu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Tc7PLjmwBM8hbeeu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tc7PLjmwBM8hbeeu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Tc7PLjmwBM8hbeeu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tc7PLjmwBM8hbeeu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Tc7PLjmwBM8hbeeu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Tc7PLjmwBM8hbeeu .cluster text{fill:#333;}#mermaid-svg-Tc7PLjmwBM8hbeeu .cluster span{color:#333;}#mermaid-svg-Tc7PLjmwBM8hbeeu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Tc7PLjmwBM8hbeeu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Tc7PLjmwBM8hbeeu rect.text{fill:none;stroke-width:0;}#mermaid-svg-Tc7PLjmwBM8hbeeu .icon-shape,#mermaid-svg-Tc7PLjmwBM8hbeeu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tc7PLjmwBM8hbeeu .icon-shape p,#mermaid-svg-Tc7PLjmwBM8hbeeu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Tc7PLjmwBM8hbeeu .icon-shape .label rect,#mermaid-svg-Tc7PLjmwBM8hbeeu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tc7PLjmwBM8hbeeu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Tc7PLjmwBM8hbeeu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Tc7PLjmwBM8hbeeu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 一致
冲突
读取站内 LocalBusiness
与外部数据源比对
读取地图平台门店数据
读取第三方聚合站数据
提高实体置信度
降低置信度或采用多数说法
回答里出现错误信息或干脆不推荐
字段写全了但口径冲突,结果可能比字段缺失更糟:缺失时引擎会谨慎处理,冲突时它要"选一个",而它选的那个未必是你的最新口径。
1.1 冲突的三种来源
| 冲突来源 | 表现 | 典型原因 |
|---|---|---|
| 时间错位 | 站内已改、地图平台还是旧的 | 多头维护、没有同步机制 |
| 格式差异 | 电话带不带区号、地址含不含楼层 | 各平台录入规范不同 |
| 语义漂移 | "周一至周五" vs "工作日" | 人工填写用词不统一 |
三类里最难治的是时间错位,因为它不是错误,而是过程问题------你改对了,别人还停在上一版。审计的意义也在这里:它把散落在各处的口径差异变成一张可核对的清单,让"哪一处需要改"不再依赖猜测。
二、原理剖析:实体对齐时,引擎在比什么
要理解冲突为什么这么贵,得看实体对齐(Entity Resolution)的过程。引擎把站内页面里的 LocalBusiness 实体与知识库中的实体做匹配,靠的是一组特征,不同特征权重不同:
| 特征 | 稳定性 | 说明 |
|---|---|---|
| 名称 + 地址 | 高 | 主键级特征,通常用于初步匹配 |
| 坐标 | 高 | 精度足够时可直接定位 |
| 电话 | 中高 | 换号会失效,但一致性强 |
| 营业时间 | 中 | 频繁变动,用于校验而非匹配 |
| 评分与评价数 | 低 | 实时性强,只作参考 |
匹配完成后,引擎会做一致性校验:如果站内的营业时间与地图平台差异超过某个阈值,它把这个实体标为"需要谨慎处理"。在回答"这家店现在开着吗"这类问题时,"谨慎处理"往往表现为不给出这家店,或者给出带保留的说法。
关键推论有两条。第一,地址与坐标是主键,必须严格一致 ,这两项出错的代价远高于营业时间。第二,营业时间的更新要同步给外部平台,站内单独改没有用,引擎的多数说法可能仍然来自平台侧。
还有一层容易被忽略的机制:多数生成式引擎在回答本地问题时,会同时使用检索结果与模型自身的记忆。当检索到的多方证据冲突时,"模型记忆里之前见过的说法"就可能成为决定性的一票------这解释了为什么有些错误答案改完站内之后依然顽固:它来自更早被抓取的第三方页面,而不是你的官网。这也意味着修复是个有时间成本的过程,通常要等到外部页面被重新抓取之后,回答才会稳定下来。我们在审计里把这类情况单独标记为"外部源污染",处理方式是联系对应平台更正,或者在站内页面里把正确信息写得更明确、更完整,提高它成为"多数证据"的概率。
三、审计方法:把不一致量化出来
整套审计分三步:拉取三方数据 → 归一化 → 比对出差异清单。
#mermaid-svg-MOmK8upySK4ETvmW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MOmK8upySK4ETvmW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MOmK8upySK4ETvmW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MOmK8upySK4ETvmW .error-icon{fill:#552222;}#mermaid-svg-MOmK8upySK4ETvmW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MOmK8upySK4ETvmW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MOmK8upySK4ETvmW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MOmK8upySK4ETvmW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MOmK8upySK4ETvmW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MOmK8upySK4ETvmW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MOmK8upySK4ETvmW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MOmK8upySK4ETvmW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MOmK8upySK4ETvmW .marker.cross{stroke:#333333;}#mermaid-svg-MOmK8upySK4ETvmW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MOmK8upySK4ETvmW p{margin:0;}#mermaid-svg-MOmK8upySK4ETvmW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MOmK8upySK4ETvmW .cluster-label text{fill:#333;}#mermaid-svg-MOmK8upySK4ETvmW .cluster-label span{color:#333;}#mermaid-svg-MOmK8upySK4ETvmW .cluster-label span p{background-color:transparent;}#mermaid-svg-MOmK8upySK4ETvmW .label text,#mermaid-svg-MOmK8upySK4ETvmW span{fill:#333;color:#333;}#mermaid-svg-MOmK8upySK4ETvmW .node rect,#mermaid-svg-MOmK8upySK4ETvmW .node circle,#mermaid-svg-MOmK8upySK4ETvmW .node ellipse,#mermaid-svg-MOmK8upySK4ETvmW .node polygon,#mermaid-svg-MOmK8upySK4ETvmW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MOmK8upySK4ETvmW .rough-node .label text,#mermaid-svg-MOmK8upySK4ETvmW .node .label text,#mermaid-svg-MOmK8upySK4ETvmW .image-shape .label,#mermaid-svg-MOmK8upySK4ETvmW .icon-shape .label{text-anchor:middle;}#mermaid-svg-MOmK8upySK4ETvmW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MOmK8upySK4ETvmW .rough-node .label,#mermaid-svg-MOmK8upySK4ETvmW .node .label,#mermaid-svg-MOmK8upySK4ETvmW .image-shape .label,#mermaid-svg-MOmK8upySK4ETvmW .icon-shape .label{text-align:center;}#mermaid-svg-MOmK8upySK4ETvmW .node.clickable{cursor:pointer;}#mermaid-svg-MOmK8upySK4ETvmW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MOmK8upySK4ETvmW .arrowheadPath{fill:#333333;}#mermaid-svg-MOmK8upySK4ETvmW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MOmK8upySK4ETvmW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MOmK8upySK4ETvmW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MOmK8upySK4ETvmW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MOmK8upySK4ETvmW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MOmK8upySK4ETvmW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MOmK8upySK4ETvmW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MOmK8upySK4ETvmW .cluster text{fill:#333;}#mermaid-svg-MOmK8upySK4ETvmW .cluster span{color:#333;}#mermaid-svg-MOmK8upySK4ETvmW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MOmK8upySK4ETvmW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MOmK8upySK4ETvmW rect.text{fill:none;stroke-width:0;}#mermaid-svg-MOmK8upySK4ETvmW .icon-shape,#mermaid-svg-MOmK8upySK4ETvmW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MOmK8upySK4ETvmW .icon-shape p,#mermaid-svg-MOmK8upySK4ETvmW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MOmK8upySK4ETvmW .icon-shape .label rect,#mermaid-svg-MOmK8upySK4ETvmW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MOmK8upySK4ETvmW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MOmK8upySK4ETvmW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MOmK8upySK4ETvmW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 站内 LocalBusiness
从页面抽取
归一化
地图平台 API
AI 回答采样
逐字段比对
差异清单 + 影响分级
3.1 归一化规则
比对的前提是归一化,否则满屏都是假差异。我们用的规则表:
| 字段 | 归一化处理 |
|---|---|
| 电话 | 去空格与连字符,区号统一补"0" |
| 地址 | 去掉"市/区"后缀差异、统一楼层写法("3 层" → "3F") |
| 营业时间 | 转成 星期:起-止 的标准结构 |
| 坐标 | 统一保留 6 位小数 |
3.2 审计脚本
python
# 环境:Python 3.10+,依赖 requests==2.32.*、beautifulsoup4==4.12.*
# 作用:对同一批门店做站内/地图平台/AI 回答的字段级比对,输出差异清单
import json
import re
from dataclasses import dataclass
import requests
from bs4 import BeautifulSoup
PHONE_CLEAN = re.compile(r'[\s\-()]')
@dataclass
class Store:
store_id: str
name: str
address: str
phone: str
hours: dict[str, str]
def norm_phone(raw: str) -> str:
"""归一化电话:去符号,补区号前导 0"""
value = PHONE_CLEAN.sub('', raw or '')
return value if value.startswith('0') else '0' + value
def norm_address(raw: str) -> str:
"""归一化地址:统一楼层写法、去除冗余后缀"""
value = (raw or '').strip()
value = value.replace('层', 'F').replace('楼', 'F')
return re.sub(r'(市|区|县)(?=.*(市|区|县))', '', value)
def norm_hours(raw: dict[str, str]) -> dict[str, str]:
"""归一化营业时间:统一为 星期:HH:MM-HH:MM"""
out = {}
for day, span in (raw or {}).items():
start, _, end = span.partition('-')
out[day] = f'{start.strip()}-{end.strip()}'
return out
def fetch_site(url: str) -> Store | None:
"""从站内页面抽取 LocalBusiness"""
soup = BeautifulSoup(requests.get(url, timeout=15).text, 'html.parser')
for tag in soup.find_all('script', type='application/ld+json'):
try:
data = json.loads(tag.string or '{}')
except json.JSONDecodeError:
continue # 解析失败的块直接跳过
if isinstance(data, dict) and 'LocalBusiness' in str(data.get('@type', '')):
return Store(
store_id=url.rsplit('/', 1)[-1],
name=data.get('name', ''),
address=(data.get('address') or {}).get('streetAddress', ''),
phone=data.get('telephone', ''),
hours={}, # 由 openingHoursSpecification 展开
)
return None
def diff(store: Store, platform: dict, ai_answer: str) -> list[str]:
"""字段级比对,输出人类可读的差异描述"""
issues = []
if norm_phone(store.phone) != norm_phone(platform.get('phone', '')):
issues.append(f'电话不一致: 站内 {store.phone} / 平台 {platform.get("phone")}')
if norm_address(store.address) != norm_address(platform.get('address', '')):
issues.append('地址不一致')
if '营业' in ai_answer and not any(token in ai_answer for token in ['09:00', '08:30']):
issues.append('AI 回答的营业时间无法与站内口径对应')
return issues
if __name__ == '__main__':
report = []
for item in json.load(open('stores.json', encoding='utf-8')):
store = fetch_site(item['site_url'])
if store is None:
report.append({'store': item['name'], 'issues': ['站内结构化数据缺失']})
continue
issues = diff(store, item['platform'], item.get('ai_answer', ''))
report.append({'store': item['name'], 'issues': issues})
json.dump(report, open('audit_report.json', 'w', encoding='utf-8'),
ensure_ascii=False, indent=2)
bad = [r for r in report if r['issues']]
print(f'门店 {len(report)} 家,存在差异 {len(bad)} 家')
3.3 AI 回答怎么采样
AI 回答不能只问一次。我们的做法是每个门店构造 3 个问题("开着吗""电话是多少""在哪"),在不同时间点各问 2 次,取多数说法。单次采样容易把模型的随机性误判为错误信息。采样结果还有一个副产品:把每个门店的错误答案留存下来,可以反查它引用了哪个外部源,这是定位"外部源污染"最直接的办法。
四、修复动作与数据变化
4.1 三类修复
- 建立单一来源:门店主数据入内部系统,官网、平台后台的资料都从它生成或校验;
- 变更联动:门店信息变更时,流程要求同时更新站内与平台,把"同步"作为变更的一部分而不是后续任务;
- 格式约束:录入时就按归一化规则校验,格式差异在入口被拦住。
4.2 修复前后
修复按门店分批推进,观察修复前后各 45 天(同一批 87 家门店):
| 指标 | 修复前 | 修复后 | 说明 |
|---|---|---|---|
| 三项全一致门店数 | 41 | 82 | 主键类字段优先修 |
| AI 回答"营业状态"错误率 | 19% | 6% | 每店 6 次采样 |
| 门店类问题被推荐次数/周 | 23 | 57 | 含"附近""推荐"类提问 |
| 电话错误导致的客诉/月 | 6 | 1 | 主要来自平台侧旧号 |
| 站内门店页 AI 爬虫抓取 | 210 次/日 | 480 次/日 | 与页面信息质量同步提升 |
中间有一个反面情况值得记录:第一批只改了站内、没同步平台的 12 家门店,修复后 AI 回答错误率几乎没变化,甚至有两家变差------因为站内与平台的口径差异变大了,引擎更难判断谁是对的。这个结果直接改变了我们的推进顺序:先统一外部平台,再改站内,或者两者同批次一起改,不要分两次。
4.3 把同步做成流程的一环
"记得同步"这种要求靠人是守不住的,我们把它落到两个具体机制上。一是门店主数据的变更必须走一个带"外部平台同步"步骤的发布流程,未完成同步的变更在系统里标为未生效。二是一个每日对账任务,随机抽 15 家门店做三方比对,发现差异自动生成工单:
python
# 环境:Python 3.10+,依赖 pymysql==1.1.*
# 作用:每日抽样门店做三方一致性对账,差异自动落工单表
import random
import pymysql
def pick_samples(conn, size: int = 15) -> list[dict]:
"""从门店主数据随机抽样,优先纳入近期变更过的门店"""
with conn.cursor(pymysql.cursors.DictCursor) as cur:
cur.execute("""
SELECT id, name, address, phone, biz_hours, updated_at
FROM store_master
WHERE status = 'active'
ORDER BY (updated_at > DATE_SUB(NOW(), INTERVAL 30 DAY)) DESC, RAND()
LIMIT %s
""", (size,))
return cur.fetchall()
def create_ticket(conn, store_id: int, field: str, site_val: str, platform_val: str) -> None:
"""差异落工单,字段名与两侧取值都记录下来,便于直接处理"""
with conn.cursor() as cur:
cur.execute("""
INSERT INTO consistency_ticket
(store_id, field_name, site_value, platform_value, state, created_at)
VALUES (%s, %s, %s, %s, 'open', NOW())
""", (store_id, field, site_val, platform_val))
if __name__ == '__main__':
conn = pymysql.connect(host='db.internal', user='audit', password='***',
database='store', charset='utf8mb4')
for store in pick_samples(conn):
platform = fetch_platform_data(store['id']) # 调用平台开放接口
for field in ('address', 'phone', 'biz_hours'):
if normalize(store[field]) != normalize(platform[field]):
create_ticket(conn, store['id'], field, store[field], platform[field])
print('对账完成')
这套机制上线后,差异从"季度审计时批量发现"变成"每天十几家抽检、平均 3 天内闭环"。工单量的下降也很快------前期每天十几张,稳定期降到每周两三张,因为源头录入问题也在同步治理。
五、误区与趋势
误区一:只在站内做结构化数据,不管外部平台。 本地类实体的一致性校验是跨源的,站内做得再规范,平台侧口径陈旧一样会被判为不可信。
误区二:把地址差异当小事。 地址与坐标是实体对齐的主键,一处"3F"和"3 层"的差异可能无伤大雅,但"路名写法不同""门牌号缺失"会直接影响匹配。
误区三:变更后不做验证。 改完就以为完事。我们现在的做法是变更后第 7 天跑一次定向审计,只查这批变更门店,确认三方都到位。
趋势上,本地类查询在 AI 渠道的占比会继续上升,而这类查询对事实准确性的容忍度最低------用户问"现在开着吗",回答错了就是白跑一趟。技术上的收尾建议:把一致性审计做成月度例行动作,并把"变更后 7 天定向复核"写进门店运营流程;审计脚本本身不复杂,难的是让"同步外部平台"成为变更动作的一部分。
最后补一句成本账,方便评估要不要做:整套审计脚本约 200 行,抽样对账每天跑一次,接口调用量在免费额度以内;人工投入主要是前期把主数据补齐,以及差异工单的处理。以 87 家门店的规模算,稳定期每月约 2~3 小时的人力,换来的是门店类问题的回答准确率从 81% 提升到 94%。这个投入产比对连锁品牌来说相当划算,门店越多越明显。你们的门店信息是怎么维护的,欢迎在评论区交流。
参考与延伸
- Schema.org LocalBusiness 类型定义:https://schema.org/LocalBusiness
- Google 搜索中心:本地商家结构化数据指南:https://developers.google.com/search/docs/appearance/structured-data/local-business
- Schema.org PostalAddress 定义:https://schema.org/PostalAddress
- 腾讯位置服务开发者文档:https://lbs.qq.com/
关键词:GEO 生成式引擎优化、AI 优化 AIO、LocalBusiness、实体对齐、门店信息一致性、数据审计、Local SEO、结构化数据