# luan-takeaway-v2 **Repository Path**: daluan/luan-takeaway-v2 ## Basic Information - **Project Name**: luan-takeaway-v2 - **Description**: v2 for luan-takeaway - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-05 - **Last Updated**: 2026-08-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Luan Takeaway V2 概要设计 基于 Java/Go 多语言服务协作的外卖智能点餐系统。系统由三个独立服务构成:Java 业务编排服务、Java 数据能力服务、Go 智能 Agent。业务服务负责业务规则、流程编排与调用 Agent;数据服务负责 MySQL 持久化、Qdrant 向量索引、单库事务、原子条件更新和库存一致性;Agent 按需调用数据服务获取信息并提供全局菜品推荐与知识生成能力。异步流程通过 RabbitMQ 完成,并使用可靠事件与业务幂等保证最终一致性。 ## 1. 系统架构 ``` ┌──────────────────────────────────────────────────────────┐ │ 前端 (luan-ui) │ │ React + TypeScript │ └───────────────────────┬──────────────────────────────────┘ │ HTTP (REST) ┌───────────────────────▼──────────────────────────────────┐ │ Spring Boot 业务编排服务 │ │ │ │ 订单管理 │ 支付管理 │ Agent 编排 │ │ RabbitMQ 异步消费 │ 业务状态流转 │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ RabbitMQ ──→ 幂等 MQ Consumer │ │ │ │ (超时取消 / 知识生成;事件由数据服务 Outbox 发布) │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ ⚠ 不直接操作数据库,查询与事务型数据操作均经由数据服务 │ └───────────────────────┬──────────────────────────────────┘ │ HTTP (REST) │ 编排 Agent 执行推荐/知识生成 │ ┌───────────────────────▼──────────────────────────────────┐ │ Go Agent 服务 │ │ │ │ LLM 语义解析 │ 意图 Embedding │ 全局向量召回 │ │ 融合排序 │ LLM 重排+解释生成 │ │ LLM 生成知识文档 │ Embedding 向量化 │ │ │ │ ✓ 可按需调用数据服务 │ └───────────────────────┬──────────────────────────────────┘ │ HTTP (REST) │ 查询菜品/知识文档 / 回写知识文档 │ ┌───────────────────────▼──────────────────────────────────┐ │ Spring Boot 数据能力服务 │ │ │ │ 数据查询/维护 │ 事务型数据操作 │ 条件更新 │ Outbox │ │ MySQL 持久化 │ Qdrant 向量索引 │ Redis 库存 + Lua 原子操作 │ │ │ │ ⚠ 保证数据一致性,不负责跨角色业务流程决策 │ └───────────────────────────────────────────────────────────┘ ``` ### Agent 设计原则 Agent 是纯粹的 AI 计算单元,遵循"接收请求 → 按需查数据 → 调用 LLM/Embedding → 返回结果"模式: - 不直接访问 MySQL - 不处理业务状态流转(订单、支付等) - 可按需调用数据服务获取菜品、知识文档等信息 - 只做语义理解、检索计算、LLM 调用、Embedding 向量化与结果编排 ### 服务间调用关系 ``` 前端 → 业务服务 → Go Agent(推荐请求 / 知识生成请求) Go Agent → 数据服务(全局向量召回、查菜品/知识文档、回写知识文档) 业务服务 → 数据服务(查询、维护、订单/支付/状态变更等事务型数据操作) 数据服务 Outbox → RabbitMQ → 业务服务消费者(订单超时取消、知识生成等) ``` ### 职责边界 | 服务 | 能做 | 不能做 | |------|------|--------| | **业务服务** | 业务规则校验、订单状态机决策、支付流程编排、Agent 编排 | 直接操作数据库、绕过数据服务修改库存 | | **数据服务** | 数据查询与维护、Qdrant 向量检索、单库事务、原子条件更新、Redis 库存原子操作、Outbox | 决定角色权限、订单下一状态等业务流程 | | **Go Agent** | 意图解析、推荐排序、LLM 重排、知识生成、Embedding 向量化 | 业务逻辑、直接访问 DB | ## 2. 服务职责 ### 2.1 业务编排服务(Spring Boot) | 模块 | 核心能力 | |------|---------| | **订单管理** | 下单、接单、状态流转、配送、查询 | | **支付管理** | 模拟支付 | | **Agent 编排** | 接收前端 AI 请求 → 调用 Agent → 处理并返回结果;需要持久化时调用数据服务 | | **异步队列** | 消费 RabbitMQ 事件,处理订单超时取消、菜品知识生成等异步流程;消费者按 `eventId` 幂等 | 业务服务是所有前端请求的身份边界。当前用户的 `userId` 及其 `customerId`、`merchantId`、`riderId` 必须由 Access Token 和服务端用户资料解析,不能采用请求体中由前端提交的主体 ID。内部 `RequestContext` 由业务服务生成,前端不得直接填写。MVP 阶段内部接口使用仅服务间持有的固定 API Key,并限制在容器私网访问。 #### 异步任务(Outbox 可靠投递 + MQ 幂等消费) ``` 菜品录入事务 → Outbox → MQ(knowledge.generate)→ 消费者 → 调用 Go Agent 生成知识文档 订单创建事务 → Outbox → MQ(order.timeout) → 消费者:延时消息 → 条件取消仍处于待支付的订单 ``` ### 2.2 数据能力服务(Spring Boot) | 模块 | 核心能力 | |------|---------| | **基础数据** | 用户、商家、骑手、地址、菜品和知识文档的查询与维护 | | **事务型数据操作** | 在一个本地事务中完成订单+明细+状态日志、支付流水+订单状态、状态更新+日志等关联写入 | | **条件更新** | 接收业务服务给出的目标状态与期望旧状态,通过乐观锁/CAS 防止重复支付、重复接单和并发状态覆盖 | | **库存一致性(MVP)** | MySQL 是最终库存事实源;Redis Lua 原子校验并预扣,随后订单事务扣减 MySQL,事务失败时执行一次反向 Lua 释放。MVP 不实现定时对账和复杂补偿编排 | | **向量索引** | 在 Qdrant 中维护全部有效菜品知识向量;向量写入、失效和全局候选召回均由数据服务封装 | | **可靠事件** | 事务内写入 Outbox,异步投递 RabbitMQ;允许至少一次投递,消费方必须幂等 | > 数据服务不只是 CRUD 层。它负责数据级不变量、事务边界、并发控制与一致性;业务服务仍负责“当前角色是否有权操作、业务上能否流转、下一状态是什么”等规则。MVP 只实现 Redis Lua 预扣、MySQL 事务扣减及失败时的直接释放;缓存对账、自动修复等工程化能力后续扩展。 > 菜品创建或商家修改库存时必须经数据服务同步刷新 Redis 可售库存;缓存缺失时从 MySQL 重建。待支付订单取消时,同一取消流程回补 MySQL 库存并通过 Lua 释放 Redis 库存。 ### 2.3 Go Agent — 智能 Agent 服务 #### 智能推荐 | 能力 | 说明 | |------|------| | **Query Understanding** | 调用 LLM 将自然语言解析为请求级 `RecommendationIntent`,提取可选价格区间、辣度、餐段等检索条件和语义推荐意图;检索范围是平台全部菜品,不预先限定商家 | | **意图 Embedding** | 将语义意图拼接为 embedding text,调用独立配置的 Embedding API 生成向量;未配置时使用本地确定性向量 | | **全局向量召回** | 将意图向量和结构化条件交给数据服务;数据服务从 Qdrant 全局召回 Top-N,并以 MySQL/Redis 中的店铺营业状态、菜品上下架、可售库存和真实价格过滤,再返回菜品事实、商家信息、AI 标签和语义分 | | **候选补充** | 全局向量召回不足时,由数据服务补充热门在售且有库存的菜品。默认向量召回 N=300,事实过滤后保留 M=100 | | **融合排序** | 对候选集计算融合分(权重可配置,默认值):`FusionScore = StructuredScore × 0.5 + SemanticScore × 0.5`;价格等真实字段为硬条件,AI 标签默认参与匹配评分 | | **LLM 重排** | 仅取融合排序后的较小候选池(默认 Top-30)交给 LLM 二次排序并生成推荐理由 | | **兜底策略** | 候选为空或融合排序为空时回退热门在售菜;LLM 重排失败回退规则排序结果 | #### RecommendationIntent(请求级模型,不持久化) `RecommendationIntent` 是 Go Agent 对单次自然语言请求的结构化解析结果,只在当前推荐请求中使用,不属于数据库实体,也不保存原始用户表达。 ```text priceMin — 可选最低价格;仅在用户明确给出下限时设置 priceMax — 可选最高价格;仅在用户明确给出预算上限时设置 spicyLevels — 可选辣度集合,例如 [0] 表示不辣 mealTimes — 可选餐段集合,例如 [dinner] keywords — 菜品类别、食材等显式关键词 semanticText — 去除硬条件后的口味、场景、人群等语义意图 ``` > 价格条件只与 `wm_dish.price` 比较,不写入知识表。缺失条件保持 `null`/空集合,Agent 不得自行补造上下限。例如“预算 30 元以内”只产生 `priceMax=30`,不能推断 `priceMin=15`。 #### 知识库生成 | 能力 | 说明 | |------|------| | **知识文档生成** | 调用 LLM 基于菜品基础信息生成 `DishKnowledgeDoc`:辣度、餐段、菜品类型、烹饪方式、风味、主要食材、场景、建议人数和食用温度等结构化标签,以及 semanticText;知识文档不重复保存菜品价格 | | **Embedding 向量化** | 将 embeddingText 送入 OpenAI Embedding API 生成向量 | | **结果回写** | 将生成的知识文档和 Embedding 向量通过数据服务写回 | #### DishKnowledgeDoc 结构 ``` AI 推断标签(用于匹配和排序): spicy — 辣度:0=不辣, 1=微辣, 2=中辣, 3=特辣 mealTimes — 适用餐段数组,可包含 breakfast/lunch/dinner/snack dishTypeTags — 菜品类型标签数组 cookingMethods — 烹饪方式数组 flavorTags — 风味标签数组 mainIngredients — 主要食材数组 sceneTags — 用餐场景数组 estimatedServings — 建议食用人数 servingTemperature — 建议食用温度:hot/cold/room_temperature semanticText — LLM 生成的一段语义描述(融合口味、场景、人群等) embeddingText — AI 推断标签 + semanticText 拼接后的文本 (送入 Embedding API 生成向量,不再拆成多个字段) ``` > 菜品名称、价格、上下架和库存属于业务事实,以 `wm_dish` 为准;知识表只保存 AI 生成内容及生成过程元数据。AI 推断标签可能不完整,默认用于召回和评分,不能覆盖菜品事实字段。 ## 3. 关键业务流程 ### 3.1 智能推荐流程 ``` 用户: "晚饭想吃不辣的面,预算30以内" │ ▼ 业务服务 接收请求 → 转发 Go Agent {query} │ ▼ Go Agent: LLM 解析意图 ├─ 事实约束:priceMin=null, priceMax=30 ├─ AI 标签条件:spicyLevels=[0], mealTimes=[dinner] └─ 语义意图:想吃面食,适合晚餐 │ ▼ Go Agent: semanticText → Embedding API(或本地向量)→ 意图向量 │ ▼ Go Agent → 数据服务 SearchRecommendationCandidates ├─ Qdrant 从平台全部有效菜品知识向量中召回 Top-N=300 ├─ MySQL 过滤店铺营业、菜品在售和真实价格 ├─ Redis 过滤当前可售库存 └─ 返回菜品事实 + 商家信息 + AI 标签 + SemanticScore,最多 M=100 │ ▼ Go Agent 计算 StructuredScore 与 FusionScore │ ▼ Top-30 → LLM 重排 → 最终 Top-5(每项携带 merchantId/storeName) │ ▼ 返回推荐结果给业务服务 → 返回前端 ``` ### 3.2 知识库生成流程(MQ 异步触发) ``` 商家录入菜品 → 业务服务 → 数据服务(创建菜品记录 + 同事务写 Outbox) │ ▼ Outbox 发布器 → MQ(knowledge.generate) │ ▼ MQ 消费者 → HTTP POST → Go Agent {dishId} │ ▼ Go Agent → HTTP GET → 数据服务 /api/dishes/{id} → 菜品基础信息 │ ▼ Go Agent: LLM 生成 DishKnowledgeDoc ├─ spicy、mealTimes、dishTypeTags、cookingMethods、flavorTags ├─ mainIngredients、sceneTags、estimatedServings、servingTemperature └─ semanticText(LLM 生成的一段语义描述) │ ▼ Go Agent: embeddingText = AI 推断标签 + semanticText 拼接 → Embedding API(未配置时使用本地确定性向量)→ 1024 维向量 │ ▼ Go Agent → HTTP PUT → 数据服务 /api/dishes/{id}/knowledge ├─ 仅当消息 sourceHash 等于菜品当前 sourceHash 时写回;旧任务直接忽略 ├─ 写入 MySQL 知识文档 └─ 幂等 Upsert Qdrant 向量;成功后 generation_status=SUCCEEDED ``` > 知识文档不暴露给前端,仅供 Go Agent 推荐时内部使用。 ### 3.3 下单异步流程 ``` 用户下单 │ ▼ 业务服务完成业务校验 → 数据服务按 clientRequestNo 幂等执行订单创建一致性单元 ├─ 校验全部菜品属于同一 merchantId、店铺营业、菜品在售,并以数据库价格计算金额 ├─ Redis Lua 原子校验并预扣库存,防止超卖 ├─ MySQL 事务扣减最终库存并写入订单、明细、初始状态日志和 Outbox └─ MySQL 事务失败时立即执行反向 Lua 释放本次预扣;MVP 不实现定时对账 │ ▼ Outbox → MQ(order.timeout.15m)→ 延时 15 分钟 → 仅当订单仍为待支付时取消并释放库存 │ ▼ 返回订单创建成功 ``` ### 3.4 订单状态流转(核心) 订单从创建到完成经历 7 个状态,涉及客户、商家、骑手三方协作: ``` ┌─────────────────────────────────────────┐ │ 1. 待支付 │ │ 客户已下单,等待支付 │ └──────┬────────────────────┬─────────────┘ │ 用户支付 │ 超时/用户取消 ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ 2. 已支付 │ │ │ │ 等待商家接单 │ │ │ └──────┬───────┘ │ │ │ 商家接单 │ │ ▼ │ │ ┌──────────────┐ │ │ │ 3. 制作中 │ │ 7. 已取消 │ │ 商家制作菜品 │ │ │ └──────┬───────┘ │ │ │ 制作完成 │ │ ▼ │ │ ┌──────────────┐ │ │ │ 4. 待配送 │ │ │ │ 等待骑手接单 │ │ │ └──────┬───────┘ │ │ │ 骑手接单 │ │ ▼ │ │ ┌──────────────┐ │ │ │ 5. 配送中 │ │ │ │ 骑手配送中 │ │ │ └──────┬───────┘ │ │ │ 骑手送达 │ │ ▼ │ │ ┌──────────────┐ │ │ │ 6. 已完成 │ │ │ │ 订单已完成 │ │ │ └──────────────┘ └──────────────┘ ``` | 状态 | 触发动作 | 执行角色 | 说明 | |------|---------|---------|------| | **1. 待支付** | 创建订单 | 客户 | 订单创建后 15 分钟内需完成支付,超时自动取消 | | **2. 已支付** | 支付成功 | 客户 | 等待商家确认接单 | | **3. 制作中** | 商家接单 | 商家 | 商家开始制作菜品 | | **4. 待配送** | 制作完成 | 商家 | 等待骑手接单配送 | | **5. 配送中** | 骑手接单 | 骑手 | 骑手取餐后开始配送 | | **6. 已完成** | 骑手送达 | 骑手 | 订单完成 | | **7. 已取消** | 取消操作 | 客户/系统 | MVP 仅允许待支付订单取消 | #### 取消规则 - MVP 仅允许状态 1(待支付)取消;状态 2-6 暂不提供取消能力 - 待支付状态超时(15 分钟)由 MQ 死信队列自动取消 - 客户可在 15 分钟内主动取消自己的待支付订单 - MVP 不实现退款流程;支付成功后订单必须继续完成,后续版本再扩展售后/退款 #### 状态变更涉及的 API | 状态变更 | API | |---------|-----| | 1→2(支付) | `POST /api/pay` | | 2→3(接单) | `PUT /api/orders/{id}/accept` | | 3→4(制作完成) | `PUT /api/orders/{id}/ready` | | 4→5(骑手接单) | `PUT /api/orders/{id}/deliver` | | 5→6(配送完成) | `PUT /api/orders/{id}/complete` | | →7(取消) | `PUT /api/orders/{id}/cancel` | > 每次状态变更均同步写入 `wm_order_status_log` 表,记录前后状态、操作人与时间,作为流转审计依据。 > 骑手一次只能配送一单。4→5 时数据服务在同一 MySQL 事务中以条件更新将骑手从 `IDLE` 改为 `DELIVERING` 并绑定订单;条件不满足则抢单失败。5→6 时校验订单归属该骑手,并在同一事务中将骑手恢复为 `IDLE`。 ## 4. 数据存储 ### MySQL 核心表 | 表 | 说明 | |----|------| | `wm_user` | 用户主表(商家/客户/骑手通用信息) | | `wm_customer` | 客户扩展表 | | `wm_merchant` | 商家扩展表 | | `wm_rider` | 骑手扩展表 | | `wm_address` | 地址表 | | `wm_dish` | 菜品基础信息 | | `wm_dish_knowledge` | 菜品知识文档 | | `wm_order` | 订单 | | `wm_order_item` | 订单明细 | | `wm_order_status_log` | 订单状态流转记录 | | `wm_pay_record` | 支付交易流水(结构预留后续退款,带幂等请求号) | | `wm_outbox_event` | 与业务数据同事务写入的可靠事件 | > ID 语义统一:订单和菜品中的 `customerId`、`merchantId`、`riderId` 分别引用角色扩展表主键;只有登录主体、支付人和审计操作人使用 `userId`,避免把角色 ID 与用户 ID 混用。 ### Qdrant 向量集合 数据服务维护 `dish_knowledge` Collection,主键使用 `dishId`,向量维度为 1024。Payload 至少保存 `dishId`、`sourceHash` 和 `contentVersion`,只索引生成成功且未过期的知识文档。MySQL 是知识文档事实源,Qdrant 是可重建的检索索引;写入必须校验当前 `sourceHash`,菜品下架或知识失效时从索引删除或标记不可检索。 ### RabbitMQ 队列(数据服务 Outbox 发布,业务服务消费) | 队列 | 说明 | |------|------| | `knowledge.generate` | 菜品录入后触发知识库生成 | | `order.timeout` | 订单超时(死信队列,延时 15min) | > MQ 采用“至少一次”投递语义。每条事件携带唯一 `eventId`;消费者必须幂等。超时消费者通过期望状态条件更新避免已支付订单被取消,知识生成消费者通过 `dishId + sourceHash` 避免重复生成。 ## 5. API 设计 `idl/*.thrift` 是跨语言请求、响应、枚举和服务方法的唯一契约来源。MVP 实际传输仍采用 HTTP/JSON,由各服务 Controller/Handler 将 REST 路由映射到对应 Thrift 结构和方法语义,不额外部署原生 Thrift RPC Server。 ### 5.1 业务编排服务 → 前端 | 接口 | 方法 | 说明 | |------|------|------| | `/api/auth/register` | POST | 用户注册(客户/商家/骑手) | | `/api/auth/login` | POST | 登录 | | `/api/admin/import/users` | POST | 管理员逐行事务批量导入客户、商家和骑手资料 | | `/api/admin/import/dishes` | POST | 管理员批量导入菜品并逐条触发知识生成 MQ | | `/api/users/profile` | GET | 获取当前用户信息 | | `/api/users/profile` | PUT | 更新当前用户信息 | | `/api/merchants/{id}` | GET | 获取商家店铺信息 | | `/api/merchants/profile` | PUT | 当前商家更新自己的店铺信息(名称/地址/营业状态) | | `/api/riders/profile` | GET | 获取当前骑手资料和忙闲状态 | | `/api/riders/status` | PUT | 当前骑手上线/下线;配送中状态不能人工修改 | | `/api/riders/location` | PUT | 当前骑手上报位置 | | `/api/dishes` | POST | 商家创建菜品(触发知识库生成 MQ) | | `/api/dishes` | GET | 查询菜品列表(按商家、上下架状态) | | `/api/dishes/{id}` | GET | 获取菜品详情 | | `/api/dishes/{id}` | PUT | 更新菜品(价格/描述/库存等) | | `/api/dishes/{id}/status` | PUT | 菜品上下架(1=上架/0=下架) | | `/api/addresses` | GET | 查询当前用户地址列表 | | `/api/addresses` | POST | 新增地址 | | `/api/addresses/{id}` | PUT | 更新地址 | | `/api/addresses/{id}` | DELETE | 删除地址 | | `/api/orders` | POST | 创建订单 | | `/api/orders` | GET | 查询订单列表(按角色/状态筛选) | | `/api/orders/{id}` | GET | 查询订单详情 | | `/api/orders/{id}/accept` | PUT | 商家接单(2→3) | | `/api/orders/{id}/ready` | PUT | 商家标记制作完成(3→4) | | `/api/orders/{id}/deliver` | PUT | 骑手接单(4→5) | | `/api/orders/{id}/complete` | PUT | 配送完成(5→6) | | `/api/orders/{id}/cancel` | PUT | 客户取消自己的待支付订单(1→7) | | `/api/pay` | POST | 模拟支付(按请求号幂等执行支付交易并推动 1→2) | | `/api/ai/recommend` | POST | 智能推荐入口(编排 Go Agent) | > 知识库生成不由前端触发,菜品录入后通过 MQ 异步执行。 > 地址新增或更新时,设置默认地址与地址写入由数据服务在同一事务完成。若地址正被 `default_address_id`、`store_address_id` 或 `station_address_id` 引用,删除请求返回“地址使用中”,不会自动清空引用。 > 一个订单只能包含同一商家的菜品。支付接口不接受客户端金额和支付人 ID,支付人由 Access Token 派生,金额以订单 `pay_amount` 为准。 ### 5.2 数据能力服务 → 调用方(业务服务、Go Agent) | 接口 | 方法 | 说明 | |------|------|------| | `/api/dishes` | GET | 查询菜品列表(按商家、上下架状态) | | `/api/dishes` | POST | 创建菜品记录,并在同事务写入 knowledge.generate Outbox | | `/api/dishes/{id}` | GET | 获取菜品详情 | | `/api/dishes/{id}` | PUT | 更新菜品记录 | | `/internal/admin/import/users` | POST | 批量创建用户及其角色、地址扩展资料 [内部接口] | | `/internal/admin/import/dishes` | POST | 按商家用户名批量创建菜品 [内部接口] | | `/api/dishes/knowledge` | GET | 批量获取知识文档(?dishIds=1,2,3)[内部接口] | | `/api/dishes/{id}/knowledge` | PUT | 更新菜品知识文档 [内部接口] | | `/api/dishes/{id}/knowledge` | GET | 获取指定菜品知识文档 [内部接口] | | `/api/recommendation/candidates/search` | POST | 通过 Qdrant 从全部菜品中向量召回,并结合 MySQL/Redis 事实过滤 [内部接口] | | `/api/users` | GET | 查询用户/商家/骑手列表 | | `/api/users` | POST | 创建用户记录 | | `/api/users/{id}` | GET | 获取用户详情 | | `/api/users/{id}` | PUT | 更新用户记录 | | `/api/addresses` | GET | 查询地址列表(按 user_id) | | `/api/addresses` | POST | 创建地址记录 | | `/api/addresses/{id}` | GET | 获取地址详情 | | `/api/addresses/{id}` | PUT | 更新地址记录 | | `/api/addresses/{id}` | DELETE | 删除地址记录 | | `/api/riders` | GET | 查询骑手列表(按状态筛选) [内部接口] | | `/api/riders/{id}/location` | PUT | 更新骑手位置 [内部接口] | | `/api/orders` | GET | 查询订单记录(按状态/骑手筛选) | | `/api/orders/{id}` | GET | 获取订单详情 | 事务型内部接口由业务服务调用,接口内部保证关联表写入的本地事务、条件更新和 Outbox 写入: | 接口 | 方法 | 原子操作 | |------|------|---------| | `/internal/users/register` | POST | 原子创建用户主表和对应的客户/商家/骑手扩展记录 | | `/internal/orders` | POST | 按 `clientRequestNo` 幂等执行 Redis 预扣 + MySQL 库存扣减 + 订单/明细/初始日志/Outbox;失败直接释放预扣 | | `/internal/orders/{id}/pay` | POST | 按幂等请求号创建支付流水 + 条件更新订单 + 状态日志 | | `/internal/orders/{id}/transition` | POST | 按 `expectedStatus + version` 更新订单和日志;骑手抢单/完成时原子更新骑手状态 | | `/internal/orders/{id}/cancel` | POST | 仅按条件取消待支付订单并释放库存 | > 普通 CRUD 接口用于基础资料维护和查询;涉及多个实体或并发竞争的写操作必须走事务型内部接口,不能由业务服务拼装多个 CRUD 请求。 > `[内部接口]` 表示不暴露给前端,仅供 Agent 和业务服务内部调用。 ### 5.3 Go Agent | 接口 | 方法 | 说明 | |------|------|------| | `/api/recommend` | POST | 从平台全部有效菜品中推荐(接收 query,结果携带商家信息) | | `/api/knowledge/generate` | POST | 生成菜品知识文档(由 MQ 消费者调用) | | `/health` | GET | 健康检查 | ## 6. 技术栈 | 层 | 技术 | |----|------| | **业务编排服务** | Spring Boot 3 + RabbitMQ | | **数据能力服务** | Spring Boot 3 + MyBatis-Plus + MySQL 8 + Redis + Qdrant | | **Go Agent** | Go + Gin + DeepSeek OpenAI-compatible API | | **前端** | React + TypeScript | | **LLM** | DeepSeek `deepseek-v4-flash` | | **Embedding** | 独立 OpenAI-compatible Embedding API / 本地确定性降级 | | **部署** | Docker + docker-compose | ## 7. 项目结构 三个服务为三个独立项目,各自有独立的构建和部署单元。 ``` luan-takeaway-biz/ # Spring Boot 业务编排服务 ├── src/main/java/ │ ├── controller/ # 对外 API + Agent 编排 │ ├── service/ # 业务逻辑(订单状态机、支付流程) │ └── mq/ # RabbitMQ 幂等 Consumer ├── src/main/resources/application.yml └── pom.xml luan-takeaway-data/ # Spring Boot 数据能力服务 ├── src/main/java/ │ ├── controller/ # 查询/维护 API + 事务型内部 API │ ├── service/ # 数据查询与事务操作 │ ├── transaction/ # 订单/支付/状态变更事务边界 │ ├── inventory/ # Redis Lua 预扣与 MySQL 库存落账 │ ├── vector/ # Qdrant 索引维护与全局候选召回 │ ├── outbox/ # 可靠事件写入与投递 │ ├── mapper/ # MyBatis-Plus Mapper │ └── entity/ # 数据实体 ├── src/main/resources/ │ ├── application.yml │ └── db/migration/ # SQL 初始化脚本 └── pom.xml luan-takeaway-agent-go/ # Go 智能 Agent ├── cmd/server/main.go ├── internal/ │ ├── handler/ # HTTP 处理器 │ ├── llm/ # LLM 调用封装 │ ├── embedding/ # Embedding 调用封装 │ ├── ranking/ # 融合排序 │ ├── knowledge/ # 知识生成 │ └── client/ # 调用数据服务的 HTTP 客户端 ├── go.mod └── go.sum luan-ui/ # 前端工程 docker-compose.yml # 基础设施编排 ``` ## 8. 本地启动 ### 管理员与批量数据 在 `.env` 中同时配置 `ADMIN_USERNAME` 和 `ADMIN_PASSWORD`(密码至少 8 位),数据服务会在启动时幂等创建管理员;可通过 `ADMIN_NICKNAME` 设置显示名称。管理员登录后访问 `/admin/import`,可上传、预览并编辑用户或菜品 CSV。菜品成功导入后会逐条写入 Outbox,并通过 RabbitMQ 在后台生成知识文档。 根目录的 `mock_data.py` 可生成与导入页面完全一致的 UTF-8 BOM CSV: ```bash python mock_data.py --merchants 10 --riders 20 --customers 50 --dishes-per-merchant 10 ``` 脚本输出 `mock_users.csv` 和 `mock_dishes.csv`。先导入用户,再导入菜品;菜品通过唯一的 `merchantUsername` 关联商家。可使用 `--seed` 固定结果、`--output-dir` 修改输出目录、用 `--password` 设置统一初始密码,或通过 `--catalog` 读取另一份兼容的本地目录快照。CSV 包含明文初始密码,只应用于受控的初始化流程并在使用后妥善删除。 菜品来自仓库内的 `mock/catalog/dishes.json` 固定快照,生成 CSV 时不会访问网络。目录以店型组织菜单,并根据菜品分类、主要食材和份量生成常规外卖标价与原创描述。需要主动更新开放数据时执行: ```bash python tools/update_dish_catalog.py ``` 更新工具固定读取 HowToCook 的已审核提交,并使用 Wikidata 补充可匹配的中文别名与来源信息。来源版本、响应哈希和许可证记录在 `mock/catalog/sources.lock.json`,归属说明见 `mock/catalog/NOTICE.md`。更新失败不会覆盖当前可用快照;普通开发、测试和部署不依赖这两个站点。 实现目录采用已创建的 `luan-takeaway-v2-biz`、`luan-takeaway-v2-data`、`luan-takeaway-v2-agent` 和 `luan-takeaway-v2-web`。在本目录执行: ```bash cp .env.example .env docker compose up --build ``` 启动后访问: - Web:`http://localhost:5173` - 业务服务 Swagger:`http://localhost:8080/swagger-ui.html` - 数据服务 Swagger:`http://localhost:8081/swagger-ui.html` - Agent 健康检查:`http://localhost:8082/health` - RabbitMQ 管理台:`http://localhost:15672` 在 `.env` 设置 `DEEPSEEK_API_KEY` 后,Agent 使用 `deepseek-v4-flash` 完成推荐意图解析和菜品知识生成。Embedding 使用独立的 `EMBEDDING_*` 配置;未配置 `EMBEDDING_API_KEY` 时使用本地确定性向量。原有 `OPENAI_*` 配置仍作为兼容回退保留。