本文档基于当前仓库代码、脚本、迁移文件和已存在文档整理,适用范围仅限当前仓库 /Users/williamwang/Downloads/项目/Fluxa Map。工作区中的 Payments-Maps、payments-fluxa-worktree.CqAAfJ 等目录不在本文档覆盖范围内。
Fluxa Map 是一个“支付地点数据工作台”。它把以下能力放进同一个系统:
- 地图 / 列表 / 品牌目录浏览
- 支付地点录入与详情查看
- 支付尝试记录维护
- 品牌目录维护
- 个人卡册管理
- 浏览历史和个人资料
- MCP 授权,让外部 AI 客户端在受控范围内读取和写入 Fluxa 数据
- AI 辅助地点录入、品牌匹配、商户名称推断
- 管理员批量从地图 POI 导入品牌门店
- 用户通过 Supabase OAuth 登录。
- 前端根据当前路由进入地图、列表、品牌、个人资料或历史页面。
- 前端通过
src/services/*从 Supabase 读取地点、品牌、卡册、用户资料。 - 新增地点有三条主路径:
- 手工新增真实地点:写入
pos_machines,同时创建首条pos_attempts - 新增壳地点:写入
fluxa_locations,不写支付尝试 - 管理员批量导入:先做地图搜索和去重,再批量写入壳地点或真实地点
- 手工新增真实地点:写入
- 用户可在详情页继续补充尝试记录、删除地点、查看评论/评价。
- 如果启用 MCP,用户可创建会话,把授权后的连接地址交给 Codex、Claude Desktop、Cherry Studio 等 AI 客户端。
- 内部运营 / 数据维护人员:维护品牌、地点、覆盖情况
- 现场贡献者 / 志愿者:录入真实支付地点和尝试记录
- 管理员:批量导入品牌门店、删除地点、执行高权限校验
- AI agent:通过 MCP 在授权范围内检索和写入业务数据
属于本系统:
- React Web 应用
- Vercel Serverless API
- 独立 MCP Server
- Supabase 数据库访问、迁移和 Edge Functions
不属于本系统:
- AMap / Nominatim / OpenRouter 等外部服务本体
- Supabase 控制台中的 OAuth Provider 配置和远程历史数据
- 工作区中的其他项目副本或旧版本
- 设计稿
.pen、图片和 PDF 资料文件
属于本系统的代码和运行单元:
- 当前仓库根目录:前端 + Vercel API + Supabase 资产
mcp-server/:Fluxa 的 MCP 服务实现- 远程 Supabase 项目:由脚本
supabase link --project-ref ytzmqzxspcuclffegazk指向 - Vercel 部署:由
vercel.json定义构建与重写规则
明确不属于本系统的同工作区目录:
Payments-Mapspayments-fluxa-worktree.CqAAfJ
当前机器上已验证可用的版本:
- Node.js
v25.1.0 - npm
11.6.2 - Supabase CLI
2.78.1
建议至少满足:
- Node 20+
- npm 10+
- Supabase CLI 2.x
额外依赖:
- 可访问的 Supabase 项目
- 可用的 AMap Key
- 如需 AI 辅助录入,需要 OpenRouter Key
- 如需本地 MCP 独立运行,需要
mcp-server/的依赖也安装完成
根项目和 MCP 子项目是两套 package.json,都要安装:
cd /Users/williamwang/Downloads/项目/Fluxa\ Map
npm install
npm --prefix ./mcp-server install前端 .env:
cp .env.example .env字段说明:
VITE_SUPABASE_URL: 前端使用的 Supabase URLVITE_SUPABASE_ANON_KEY: 前端匿名 KeyVITE_MCP_SERVER_URL: 前端调用 MCP 会话管理接口的基地址,默认可指向http://localhost:3030VITE_AMAP_KEY: AMap JS KeyVITE_AMAP_SECURITY_JS_CODE: AMap 安全码
Supabase Functions:
cp supabase/functions/.env.example supabase/functions/.env至少需要:
OPENROUTER_API_KEYOPENROUTER_MODEL或OPENROUTER_MODEL_PRIMARY
MCP 服务端环境变量不是 .env.example 形式显式提供,但代码要求以下变量:
SUPABASE_URLSUPABASE_SERVICE_ROLE_KEYPORT,默认3030HOST,默认127.0.0.1MCP_PUBLIC_BASE_URLMCP_CORS_ORIGINSMCP_MAP_SEARCH_USER_AGENTMCP_MAP_SEARCH_CHROME_PATHVITE_AMAP_KEYVITE_AMAP_SECURITY_JS_CODE或VITE_AMAP_SECURITY_KEY
最小前端启动:
npm run dev前端默认是 Vite 开发服务器,通常在 http://localhost:5173。
本地 MCP 服务:
npm run mcp:dev或构建后直接启动:
npm run mcp:build
cd mcp-server
./run-local-server.shSupabase 本地容器:
npm run supabase:start
npm run supabase:status本地运行 Edge Functions:
npm run supabase:functions:serve
npm run supabase:functions:serve:smart-add说明:
- 前端可以连远程 Supabase,也可以连本地 Supabase
- MCP 服务单独运行时需要服务端密钥,不能只靠前端
.env - Vercel 本地开发未在仓库中提供统一脚本,当前主要以 Vite + 独立 MCP + Supabase CLI 方式开发
当前仓库没有自动化测试脚本,也没有发现独立测试目录。现阶段的最小检查是:
npm run typecheck
npm run mcp:typecheck
npm run build2026-03-22 已在当前仓库执行并通过:
npm run typechecknpm run mcp:typechecknpm run build
前端构建:
npm run buildMCP 服务构建:
npm run mcp:buildSupabase 数据库迁移:
npm run supabase:db:pushSupabase Edge Functions 部署:
npm run supabase:functions:deploy:merchant
npm run supabase:functions:deploy:brand
npm run supabase:functions:deploy:smart-addVercel 发布:
- 仓库通过
vercel.json声明了 Vite 构建与 API 重写 - 仓库没有内置一键发布脚本
- 实际发布通常通过 Vercel 控制台、Git 集成,或手动执行
vercel deploy
-
页面能打开,但业务全是只读或空白
- 检查
.env里的VITE_SUPABASE_URL和VITE_SUPABASE_ANON_KEY src/lib/supabase.ts在变量缺失时会退回 placeholder,并打印只读警告
- 检查
-
登录按钮报 “provider is not enabled”
- 这是 Supabase OAuth Provider 没开启,不是前端问题
- 需要去 Supabase 控制台启用 GitHub / Google / Microsoft 等 Provider
-
AMap 地图脚本加载失败
- 检查
VITE_AMAP_KEY和VITE_AMAP_SECURITY_JS_CODE - 本地走
/api/amap/js代理,生产也通过 Vercel Function 转发
- 检查
-
MCP 设置页无法创建会话
- 前端看
VITE_MCP_SERVER_URL是否指向正确服务 - 服务端看
SUPABASE_URL和SUPABASE_SERVICE_ROLE_KEY是否存在 /health可用于先确认 MCP 服务状态
- 前端看
-
AI 辅助录入报 500
- 检查
supabase/functions/.env是否设置了OPENROUTER_API_KEY - 检查 Edge Function 是否已
serve或已部署
- 检查
-
supabase db push后本地库仍缺表- 当前迁移只管理本仓库新增或补充的部分表
brands、pos_machines、pos_attempts、users、reviews、comments、user_history等历史表不完全由本仓库定义- 新人若只起本地空库,很多读写能力会缺失
-
npm run build通过但提示 chunk 很大- 这是当前已知状态,不阻塞发布
- 说明客户端 bundle 较大,后续应考虑拆分路由或重做分包策略
.
├── api/ Vercel Serverless API 入口
├── docs/ 文档
├── mcp-server/ 独立 MCP 服务
├── src/ Web 前端源码
├── supabase/ 迁移和 Edge Functions
├── public/ 静态资源
├── assets/ images/ 品牌与设计资源
└── vercel.json Vercel 构建与路由规则
src/main.tsx
- 只负责挂载 React 根节点和
I18nProvider - 不承载业务逻辑
src/root-app.tsx
- 负责登录态恢复、OAuth 回调处理、登出
- 决定用户是进登录页还是主应用
- 不负责具体业务页面渲染细节
src/App.tsx
- 负责单页应用路由状态、页面级状态、Overlay 流程编排
- 是地图 / 列表 / 品牌 / 设置 / MCP / 新增地点等视图的协调层
- 不直接写数据库
src/components/
- 页面组件和复合业务组件
- 例如
add-location-wizard.tsx、place-detail-web.tsx、web-mcp-settings.tsx - 负责界面交互和视图拼装
- 不应该堆积原始 Supabase 访问逻辑
src/components/ui/
- 底层 UI 原子组件,来自 shadcn 风格体系
- 负责通用展示和交互基础件
- 不应写业务条件分支
src/hooks/
- 面向业务场景的读取 / 写入 hook
- 负责加载态、错误态、本地缓存、分页/滚动模式等
- 当前项目的“状态管理”主要就在这里
- 不负责数据库字段映射细节
src/services/
- API / 数据访问层
- 负责 Supabase 查询、表结构兼容、行对象到领域对象的映射
- 是前端写库读库的主要边界
- 不负责页面状态
src/lib/
- 基础能力和跨模块工具
- 包括 Supabase 客户端、地图工具、搜索索引、MCP 请求客户端、AI 辅助工具
- 这里适合放纯函数、集成封装和协议工具
src/types/
- 领域类型定义
- 对前端消费的业务对象做统一建模
- 修改这里通常意味着多个 service / component 都会受影响
src/data/
- mock 或静态样例数据
- 不是生产真相源
api/ 不是完整后端,而是 Vercel Serverless 入口层:
/api/health: 健康检查/api/amap/js: AMap JS 代理/api/mcp/*: MCP 会话管理和远程 MCP 接入
这些文件大多只是把请求转给 mcp-server/src/app.ts,真正逻辑不在 api/ 本身。
mcp-server/ 是独立服务实现:
- Express 应用入口:
src/app.ts - 会话和权限:
src/session-service.ts - 业务读写封装:
src/fluxa-service.ts - 地图搜索:
src/map-search-service.ts - 审计日志:
src/audit-log-service.ts
它的职责是对外提供:
- REST 会话管理接口
- Streamable HTTP MCP 连接端点
- 管理员品牌地图搜索 / 批量导入接口
supabase/migrations/
- 管理本仓库新增和扩展的数据库对象
- 当前主要覆盖
fluxa_locations、card_album_cards、mcp_sessions、mcp_tool_logs和若干列扩展
supabase/functions/
- 管理 AI 相关 Edge Functions
infer-merchant-nameinfer-brand-matchassist-add-location
注意:
- 远程库里还有历史业务表,但并不都在当前迁移目录里
- 因此
supabase db reset得到的本地库不等于线上完整业务库
flowchart LR
U["用户 / AI 客户端"] --> W["Web App (Vite + React)"]
U --> M["Remote MCP Client"]
W --> S["Supabase (Auth + DB)"]
W --> VF["Vercel Functions /api/*"]
W --> EF["Supabase Edge Functions"]
VF --> MCP["mcp-server/app.ts"]
MCP --> S
MCP --> MAP["AMap / Nominatim"]
EF --> OR["OpenRouter"]
M --> VF
这个系统没有传统“单独后端仓库”。
实际是三层组合:
- 前端:React SPA
- 服务端接入层:Vercel Functions + 独立 MCP Express
- 数据与 AI:Supabase DB/Auth/Functions
前端直连 Supabase 做大部分读写;只有以下场景必须经过服务端:
- MCP 会话创建、撤销、远程连接
- 服务端管理员校验
- 服务角色密钥访问
- AMap JS 代理
浏览器调用:
src/services/*-> Supabase JS Clientsrc/lib/mcp-session-service.ts->/api/mcp/*src/lib/amap.ts->/api/amap/jssrc/services/ai-service.ts-> Supabase Edge Functions
Vercel / MCP 调用:
api/*->mcp-server/src/app.tsmcp-server/src/app.ts->SessionService+FluxaServiceFluxaService-> Supabase + 外部地图服务
新增真实地点:
- 前端收集表单
useFluxaLocations->locationService.createLocation- 写入
pos_machines - 在同一创建流程里再写首条
pos_attempts - 失效缓存并刷新地图、列表、搜索目录
新增壳地点:
- 前端或 MCP 调用
createShellLocation - 只写
fluxa_locations - 不创建尝试记录
浏览地图:
- 先拉轻量 map index
- 依据 viewport 在前端分区
- 按分区补拉真实地点详情
- sessionStorage 缓存 map index,内存缓存已拉分区
MCP 会话创建:
- 前端带 Supabase Access Token 调
/api/mcp/sessions - 服务端校验 bearer token
- 生成 session token,数据库仅保存 hash
- 返回连接 URL 给外部 AI 客户端
当前项目没有 Redux / Zustand / React Query。
状态管理方式是:
- 页面级状态:
App.tsx中的useState - 数据加载与写入:
src/hooks/* - 模块级共享缓存:hook 文件内部的
shared*Cache - 本地持久化:
localStorage存语言、地图主题、Beta 开关sessionStorage存 map index 缓存
这意味着:
- 改 hook 缓存策略时,容易影响多个视图的一致性
- 没有统一失效中心,主要靠
invalidate*Cache()手工维护
当前仓库没有消息队列、任务编排器或 cron 定义。
现有异步主要是:
- 浏览器内异步加载和后台预取
- Supabase Edge Functions 的即时调用
- MCP 工具调用
- 管理员批量导入时的 on-demand 批处理
前端缓存:
- 品牌目录、地点目录、map index 用模块级缓存
- 地图 index 额外持久化到
sessionStorage,TTL 15 分钟 - 视口分区结果保存在内存 Map 中
服务端 / HTTP 缓存:
/api/amap/js透传上游cache-control,默认 5 分钟
注意:
- 任何写操作后都需要显式
invalidate*Cache() - 若只改服务层不改失效逻辑,地图/列表/品牌页面会出现旧数据
浏览器侧:
- 使用 Supabase Auth
root-app.tsx恢复 session,支持 OAuth 和手动 callback 调试
管理员识别:
- 通过
user_metadata或app_metadata中的is_admin/admin/role/roles - 前端和 MCP 服务都各自实现了 admin 判定逻辑
MCP:
- 会话管理接口用 Supabase Access Token 鉴权
- 真正的 MCP 连接用
sessionToken - 数据库里保存的是
session_token_hash,不是明文 token
服务端数据库访问:
mcp-server/src/supabase.ts使用SUPABASE_SERVICE_ROLE_KEY- 这是高权限客户端,只能在服务端运行
Location
- 前端统一领域对象
- 可能来自
fluxa_locations或pos_machines source字段是来源真相
Shell Location
- 对应
fluxa_locations - 表示“存在该门店,但还没有真实支付尝试”
- 适合批量铺点
Verified POS Location
- 对应
pos_machines - 表示真实录入、可挂支付尝试的地点
Location Attempt
- 对应
pos_attempts - 记录某次支付尝试和设备状态
Brand
- 对应
brands - 维护品牌目录和品牌级统计
Card Album Card
- 对应
card_album_cards - 支持
public和personal
Browsing History
- 对应
user_history - 保存用户访问过的地点
MCP Session
- 对应
mcp_sessions - 表示对某个外部 AI 客户端的授权会话
MCP Tool Log
- 对应
mcp_tool_logs - 记录 MCP 调用审计摘要
brands1 -> Npos_machines,通过brand_idfluxa_locations通过brand文本值挂品牌展示名,不是外键pos_machines1 -> Npos_attemptspos_machines1 -> Nreviewspos_machines1 -> Ncommentsusers1 -> Nuser_historyusers1 -> Ncard_album_cards,仅个人卡有user_idusers1 -> Nmcp_sessionsmcp_sessions1 -> Nmcp_tool_logs
fluxa_locations.brand
- 文本字段
- 当前品牌详情页对壳地点归属依赖这个展示名
- 不是品牌外键
pos_machines.brand_id
- 真实地点对品牌的结构化归属
- 是品牌统计和筛选的重要来源
Location.source
- 前端统一视图里的来源标记
- 真相值,不是派生装饰
status
- 地点层面只允许
active/inactive - 设备状态与地点可用性会相互影响展示
session_token_hash
- MCP 会话明文 token 的哈希
- 属于安全真相源
token_hint
- 只是提示展示,不是鉴权真相
地点状态机:
active->inactiveinactive->active
当前项目没有更细的地点状态枚举,更多业务语义由尝试记录和备注表达。
MCP 会话状态机:
- 创建后为活跃
- 到期前可使用
- 撤销后不可再用
- 数据库存储通过
revoked_at判断是否失效
卡册状态机:
publicpersonal
不支持更多生命周期状态。
真相源:
fluxa_locations/pos_machinespos_attemptsbrandscard_album_cardsuser_historymcp_sessions
派生值:
successRatestoreCountactiveStoreCountinactiveStoreCountprimaryCitylastSyncAt- 列表统计数、地图可见数
修改派生逻辑通常不改表结构,但会影响多个页面和 MCP 输出。
本仓库迁移里能看到的表并不完整。当前可确认:
- 仓库显式管理:
fluxa_locations、card_album_cards、mcp_sessions、mcp_tool_logs - 仓库显式扩展:
brands、fluxa_locations - 仓库依赖但未完整定义:
brands、pos_machines、pos_attempts、reviews、comments、users、user_history
这意味着本仓库不是“从零重建线上库”的完整 schema 源。
当前没有 OpenAPI / Swagger。权威源是:
mcp-server/src/app.tssrc/lib/mcp-session-service.tssrc/services/*
| 方法 | 路径 | 鉴权 | 作用 |
|---|---|---|---|
GET |
/health / /api/health |
无 | 返回 MCP 服务健康状态 |
GET |
/api/mcp/templates |
无 | 返回会话模板列表 |
GET |
/api/mcp/sessions |
Authorization: Bearer <supabase access token> |
列出当前用户的 MCP 会话 |
POST |
/api/mcp/sessions |
Bearer | 创建 MCP 会话 |
POST |
/api/mcp/sessions/:id/revoke |
Bearer | 撤销指定会话 |
POST |
/api/mcp/sessions/revoke |
Bearer | 通过 body 里的 id 撤销会话 |
POST |
/api/admin/brand-map-search |
Bearer + Admin | 地图搜索品牌门店 |
POST |
/api/admin/brand-map-import |
Bearer | 批量导入品牌门店,内部再做管理员校验 |
ALL |
/mcp/:sessionToken / /api/mcp/:sessionToken / /api/mcp/connect |
MCP Session Token | Remote MCP 连接入口 |
GET |
/api/amap/js |
无 | AMap JS 代理 |
创建 MCP 会话请求:
{
"sessionLabel": "Fluxa 桌面会话",
"clientType": "codex",
"scopeTemplate": "standard_user"
}返回:
{
"session": {
"id": "uuid",
"sessionLabel": "Fluxa 桌面会话",
"clientType": "codex",
"scopeTemplate": "standard_user",
"scopes": ["analytics.read", "locations.read"],
"tokenHint": "xxxx...abcd",
"lastUsedAt": null,
"expiresAt": "2026-04-21T00:00:00.000Z",
"revokedAt": null,
"createdAt": "2026-03-22T00:00:00.000Z"
},
"sessionToken": "plain-token",
"connectionUrl": "https://.../mcp/<sessionToken>",
"publicBaseUrl": "https://..."
}品牌地图搜索请求:
{
"brand": "星巴克 (Starbucks)",
"area": "上海",
"brandAliases": ["Starbucks", "星巴克"],
"countryCode": "CN",
"limit": 200
}- MCP 会话管理:必须带 Supabase Access Token
- 管理员地图搜索 / 导入:必须是管理员用户
- Remote MCP:使用 session token,不用 Supabase Access Token
- 前端直连 Supabase 的读写由 Supabase RLS 和当前登录态共同控制
REST 接口统一风格:
{
"error": "Unable to create MCP session."
}常见状态码:
400: 参数错误、业务校验失败401: Bearer token 缺失或无效405: 方法不允许500: 上游或服务内部异常503: 服务端缺少 Supabase 服务角色配置
当前没有通用 idempotency key 设计。
可以近似视为安全重试的操作:
POST /api/mcp/sessions/:id/revokePOST /api/mcp/sessions/revoke
需要业务去重保护的操作:
- 创建地点
- 创建卡片
- 批量品牌导入
其中批量导入只有在 skipExisting=true 且去重命中时,才接近幂等。
REST 层本身分页有限,更多分页逻辑在前端 hooks 内处理。
当前常见规则:
- 地点和品牌目录先全量或轻量拉取,再由前端分页 / 滚动分页
- 排序常用
updated_at desc或名称排序 - MCP 工具里的
limit普遍上限 25 / 100 / 500,具体看工具 schema
MCPScopeTemplate当前只有standard_userMCPClientType当前只支持generic、claude_desktop、cherry_studio、codex- 品牌匹配函数只能返回品牌候选列表里的精确字符串
LocationStatus目前只有active/inactive- 前端和 MCP 服务各自定义了一份部分重复类型,改字段时要双边同步
MCP 主要工具按领域分组如下:
分析类:
get_site_statisticsrank_location_contributorscollect_locations_dataset
地点类:
search_locationsget_location_detailcreate_locationcreate_shell_locationbulk_create_locationscreate_location_attemptsearch_brand_locations_on_mapbulk_import_brand_locations_from_map
品牌类:
list_brands
卡册类:
list_card_album_cardscreate_card_album_cardcreate_personal_cardadd_card_to_albumadd_public_card_to_personal
个人类:
list_browsing_historyclear_browsing_historyget_my_profileupdate_my_profile
源代码级权威位置:mcp-server/src/app.ts
它做什么:
- 统一读取
fluxa_locations和pos_machines - 提供地图点位、列表、详情、创建、删除、尝试追加
入口:
- 前端:
src/services/location-service.ts - MCP:
mcp-server/src/fluxa-service.ts
依赖:
- Supabase
- 品牌数据
- 用户数据
- 评论 / 评价 / 尝试表
修改注意:
- 这是当前最复杂的模块,涉及双来源合并、缓存、权限、详情页和地图页
- 改字段要同时看前端
types/location.ts和 MCPmcp-server/src/types.ts
高风险点:
- 壳地点和真实地点合并逻辑
- 删除地点时同时删
pos_machines和fluxa_locations - 城市、品牌、支持网络、成功率等派生字段
它做什么:
- 维护品牌目录、品牌详情、品牌统计和品牌下地点列表
入口:
src/services/brand-service.tssrc/hooks/use-fluxa-brand-catalog.tssrc/hooks/use-fluxa-brand-locations.ts
依赖:
brandspos_machines- 壳地点里的品牌文本
修改注意:
- 壳地点品牌归属靠文本展示名,不是外键
- 品牌改名可能导致
fluxa_locations.brand不再匹配
高风险点:
- 品牌名变更
- 品牌统计字段的派生逻辑
- 品牌详情页对地点聚合的展示结果
它做什么:
- 创建 / 撤销授权会话
- 为外部 AI 客户端暴露 Remote MCP
- 记录工具调用审计
入口:
src/components/web-mcp-settings.tsxsrc/lib/mcp-session-service.tsmcp-server/src/app.tsmcp-server/src/session-service.ts
依赖:
- Supabase Auth
mcp_sessionsmcp_tool_logs
修改注意:
- 明文 session token 只会在创建时返回一次
- 存库的是 hash,不可逆恢复
- 一旦改 scope 或模板,需要同时改前端和服务端常量
高风险点:
- 鉴权错误会直接导致外部 AI 完全无法接入
- 服务角色密钥绝不能泄漏到前端
它做什么:
- 根据地图逆地理结果推断商户名
- 从品牌目录匹配品牌
- 用对话式方式补全新增地点表单
入口:
- 前端:
src/services/ai-service.ts - Edge Functions:
supabase/functions/*
依赖:
- OpenRouter
- 品牌候选列表
- AMap 逆地理结果
修改注意:
- 前端对函数返回做了强制归一化,改返回字段要同步双端
- 大模型输出必须是 JSON,可解析失败
高风险点:
- 提示词变更会影响录入稳定性
- 模型切换会影响字段填充质量
- 必须容忍空结果和低置信度结果
它做什么:
- 解析区域
- 搜索品牌地图 POI
- 结合去重逻辑批量导入地点
入口:
mcp-server/src/map-search-service.tsmcp-server/src/fluxa-service.ts- 文档:
docs/bulk-brand-import.md
依赖:
- Nominatim
- AMap JS
- 现有地点表
- 管理员权限
修改注意:
- 先读
docs/bulk-brand-import.md - 这是高风险数据写入路径
高风险点:
- 误导入闭店门店
- 壳地点覆盖真实地点
- 品牌名不一致导致品牌详情聚合失真
它做什么:
- 显示用户资料、贡献统计、浏览历史
- 支持更新 profile metadata
入口:
src/services/viewer-profile-service.tssrc/services/browsing-history-service.ts- 对应 MCP 工具在
mcp-server/src/fluxa-service.ts
高风险点:
- 依赖的
users、user_historyRPC 和历史表结构不完全由本仓库管理 - 本地空库很容易出现“功能看起来存在但表不存在”
本节分为两类:
- 已在仓库中明确体现的约定
- 仓库未强制、但为了维持一致性应遵循的最小实践
命名与文件:
- React 组件文件多用 kebab-case 文件名,导出 PascalCase 组件
- hooks 统一使用
use-*.ts - services 统一使用
*-service.ts - 类型集中在
src/types/*
模块边界:
- 组件不直接写复杂 Supabase 查询
- 查询与映射主要放
src/services/* - 页面数据读取优先通过 hooks,不直接在组件里拼 SQL 风格查询
路径别名:
- 统一使用
@/指向src
错误处理:
- service 层抛
Error - hook 层把异常转成用户可读 message
- 组件层负责显示,不吞异常细节
日志:
- 当前主要用
console.error/console.warn - 没有统一日志 SDK
状态管理:
- 不使用 Redux
- 用 hook + 模块缓存 + storage
组件 / 服务 / hook 拆分:
- 纯展示或局部交互放组件
- 复用数据流程放 hook
- 任何 Supabase 表读写或外部接口封装放 service / lib
目录约定:
- 新的前端领域对象先定义在
src/types - 新的数据库读写优先进入对应
src/services/* - 不要把大段业务逻辑塞回
App.tsx
错误处理规范:
- service 层始终抛可定位错误
- hook 层统一转人类可读消息
- UI 不直接暴露底层对象原始结构
日志规范:
- 浏览器只记可诊断问题
- 服务端禁止打印密钥、token、完整授权头
- MCP 审计日志已经做了 sanitize,新增字段时保持同样策略
测试规范:
- 当前没有自动测试,改动至少跑
typecheck+build - 任何涉及地图、MCP、AI 辅助录入的变更,都应补手工验证步骤
提交信息规范:
- 仓库未配置 commitlint / husky
- 当前没有代码层强制格式
- 建议 commit message 至少带清晰主题和作用域,例如
feat(mcp): add map import endpoint
PR 规范:
- 仓库未发现模板
- 建议 PR 描述至少写清:改动背景、影响范围、手工验证、环境变量变化、是否涉及迁移
代码审查标准:
- 优先看数据真相源是否被破坏
- 优先看缓存失效是否遗漏
- 优先看 admin / session / service-role 边界是否被放松
src/types/location.tssrc/services/location-service.ts- 相关 hooks
- 详情页 / 列表页 / 地图卡片
mcp-server/src/types.tsmcp-server/src/fluxa-service.ts- 若落库,还要补 migration
src/types/brand.tssrc/services/brand-service.ts- 品牌详情页 / 品牌目录页
mcp-server/src/types.tsmcp-server/src/fluxa-service.tsbrands表结构或兼容逻辑
src/services/viewer-access-service.tssrc/hooks/use-viewer-access.tssrc/services/location-service.ts里的 admin 校验mcp-server/src/fluxa-service.ts的isAdminUser/requireAdminUsermcp-server/src/session-service.ts
MCP 会话相关:
src/lib/mcp-session-service.tssrc/components/web-mcp-settings.tsxmcp-server/src/app.tsmcp-server/src/session-service.tssrc/types/mcp.ts
AI 辅助录入相关:
src/services/ai-service.tssrc/types/add-location-assistant.tssupabase/functions/*src/components/add-location-smart-add-dialog.tsx
vercel.jsonvite.config.ts- 根
.env supabase/functions/.env- MCP 服务端环境变量
- Preview / Production 的 AMap、Supabase、OpenRouter 配置是否一致
- 所有
invalidate*Cache()调用点 - 地图页和列表页是否出现旧数据
- sessionStorage 版本号是否需要升级
- 写操作后品牌 / 地点计数是否同步更新
docs/bulk-brand-import.md- 去重是否仍覆盖真实记录
- 闭店过滤是否仍有效
- 品牌文本写入是否与品牌展示名一致
从仓库可确认的环境层级是:
- Development
- Preview
- Production
依据:
- OpenRouter 模型池按
development/preview/production分环境切换 - Vercel 默认支持 Preview / Production
未看到明确单独建模的环境:
- 独立 Test
- 独立 Staging
因此当前更合理的理解是:
- 本地开发 = Development
- Vercel Preview = 预发布
- Vercel Production = 生产
前端与 API:
- 由 Vercel 构建
vercel.json指定:framework: vitebuildCommand: npm run buildoutputDirectory: dist/health、/mcp/:sessionToken等路由重写
MCP 服务:
- 可嵌入 Vercel API 使用
- 也支持本地独立运行
mcp-server/dist/index.js - 仓库附带了本地
launchdplist 和run-local-server.sh
Supabase:
- 数据库迁移通过 CLI 推送
- Edge Functions 独立部署
仓库内没有显式 CI 配置文件,因此可确认的只有“构建契约”,没有完整流水线定义。
当前可推断的发布最小流程:
- 本地
npm run typecheck - 本地
npm run mcp:typecheck - 本地
npm run build - 推送代码,由 Vercel 构建前端和
api/* - 单独执行 Supabase migration / function deploy
也就是说:
- Web/API 发布和 Supabase 发布不是一个原子流水线
- 迁移与函数部署需要手工或外部流水线补齐
来源分三类:
- 本地文件:
.env、supabase/functions/.env - Vercel 项目环境变量
- Supabase Functions Secrets
涉及的关键密钥:
- 前端只应拿匿名 Key
- MCP 服务端拿 Service Role Key
- Edge Functions 拿 OpenRouter Key
建议流程:
- 新增 migration 到
supabase/migrations/ - 本地或目标项目执行
npm run supabase:db:push - 如 Edge Function 依赖新字段,再部署 Functions
- 手工验证前端和 MCP 读写链路
注意:
- 当前 schema 不是完整业务库快照
- 对历史表做修改前,先确认远程真实结构
仓库内没有自动回滚机制,建议区分三类:
前端 / Vercel API:
- 回滚到上一个 Vercel 部署版本
Supabase Functions:
- 重新部署上一版函数
数据库:
- 不建议直接回滚迁移
- 更安全的方式是新建 forward-fix migration
仓库未定义统一监控 / 告警平台接入。
当前可用入口:
/health健康检查- Vercel Runtime / Function Logs
- Supabase Logs
- 本地 MCP
launchd日志:/tmp/fluxa-map-mcp-launchd.out.log/tmp/fluxa-map-mcp-launchd.err.log
缺失项:
- 自动告警规则
- 统一 dashboard
- Sentry / Datadog / Grafana 等接入
看 .env 是否已创建,以及 VITE_SUPABASE_URL、VITE_SUPABASE_ANON_KEY 是否有效。
当前登录依赖 Supabase 控制台中的 OAuth Provider 配置。前端代码不会替你启用 Provider。
常见原因:
VITE_MCP_SERVER_URL指错- MCP 服务端未启动
- 服务端缺少
SUPABASE_SERVICE_ROLE_KEY - 前端当前没有有效登录 session
因为当前仓库里没有 cron 或 webhook 调度实现。若你期待自动任务,说明需求还没落到仓库。
因为当前项目大量依赖手工缓存失效。检查写操作后是否调用了相应的 invalidate*Cache()。
因为当前 migration 目录不是完整业务库定义。它只覆盖本仓库最近管理的部分对象。
可能原因:
- Edge Function 没运行
OPENROUTER_API_KEY缺失- 模型返回非 JSON
- 前端拿到的是合法的“无法判断”结果
因为本地和生产都通过代理路径 /api/amap/js,但最终是否成功仍取决于 AMap Key、安全码和域名限制。
壳地点品牌归属依赖 fluxa_locations.brand 的展示名文本匹配,不是外键。如果品牌名写错或改名未同步,会直接漏数据。
因为删除逻辑会同时尝试删除 pos_machines 和 fluxa_locations 对应记录,且会影响尝试、评论、历史记录等关联展示。
以下 ADR 是根据现有代码可确认的已落地决策,不是未来建议。
为什么选:
- 当前页面虽然复杂,但状态大多围绕单页工作台
- hooks 已能承载缓存、分页、异步读取
为什么没选:
- 仓库里没有 Redux / Zustand / React Query 依赖和接入
适用前提:
- 数据一致性主要靠手工失效控制
什么情况下要重构:
- 如果出现跨页面实时同步、离线缓存、复杂并发更新,现有方案会越来越脆弱
为什么选:
- 需要同时支持“壳地点铺设”和“真实支付验证记录”
为什么没选单表:
- 单表无法清晰区分“只是存在”与“已经验证”
适用前提:
- 前端和 MCP 都有合并视图逻辑
什么情况下要重构:
- 如果双表导致大量重复去重和一致性问题,可考虑抽象成统一地点主表 + 证据表
为什么选:
- 减少凭证泄漏风险
为什么没选明文持久化:
- 明文 token 被库泄露时风险过高
适用前提:
- 用户能接受 token 丢失后重新创建
什么情况下要重构:
- 如果未来要支持 token 轮换、细粒度撤销或设备管理,可引入更完整的凭证模型
为什么选:
- 本地和生产走同一入口
- 方便控制缓存和错误返回
为什么没选前端直连:
- 本地和 Vercel 的入口不一致,调试和部署更分裂
适用前提:
- 代理层足够轻,不承担复杂业务
什么情况下要重构:
- 若需要更强的地图网关能力,可把地图代理从 Vercel Function 独立出去
为什么选:
- 离前端近,便于直接被 Supabase JS 调用
- 模型切换、环境区分和回退池可以集中控制
为什么没选直接浏览器调模型:
- 密钥不能放前端
适用前提:
- AI 输出是小型结构化 JSON 任务
什么情况下要重构:
- 如果提示词管理、审计、成本控制变复杂,应拆到独立 AI 后端层
为什么选:
- 真实记录带有人类核验和支付轨迹,可信度更高
为什么没选“官方源全覆盖”:
- 官方地址更整齐不代表更适合替换真实支付轨迹
适用前提:
- 导入前做合并视图查重
什么情况下要重构:
- 如果未来要做正式的门店主数据同步,应引入更明确的“主记录 + 外部来源映射”模型
发生冲突时,建议按以下优先级理解系统:
- 运行中的代码
mcp-server/src/app.ts与src/services/*supabase/migrations/*- 本文档
- 其他说明文档
批量品牌导入是例外,请优先同时参考:
docs/bulk-brand-import.mdmcp-server/src/map-search-service.tsmcp-server/src/fluxa-service.ts