2026-07-14-mcp-export-pending-outbound-orders-design.md 12 KB

MCP 导出未排舱订单设计

背景

fmsoperate/app/admin/view/outbound/add.html 已提供“导出未排舱订单”功能。页面把当前筛选条件提交到 export/asyncExportData,固定使用 EXPORT_PEND_OUTBOUND_ORDER,并通过 is_sync=1 同步等待导出 任务完成后取得 OSS 下载链接。

本次在 MCP 中开放相同能力。MCP 不重新实现订单筛选或 Excel 生成逻辑,只复用 fmsoperate 已有的 员工会话、数据权限、未排舱订单查询和导出任务,最终向调用方返回下载链接。

目标

  • 新增 MCP 工具 export_pending_outbound_orders
  • 新增独立工具 list_pending_outbound_export_filter_options,让 AI 先把用户提供的筛选名称解析成当前员工有权使用的值。
  • 筛选参数名称、业务含义、类型和多选传参方式与 outbound/add.html 完全一致。
  • 复用 ExportPendOutboundOrderLogicOutboundModel::getPendOutboundOrder(),确保导出内容与页面一致。
  • 使用当前 MCP 员工身份、公司和数据权限,不接受调用方传入身份或权限字段。
  • 筛选项列表沿用 add 页面当前员工权限,禁止返回其他公司、其他客户或当前员工无权使用的筛选值。
  • 成功时返回可下载的文件 URL;失败时返回明确业务错误且不终止 MCP 会话。

参数契约

所有筛选参数均为可选。不传筛选条件时,行为与页面清空筛选后点击“导出未排舱订单”一致。

页面中文名称 MCP 参数名 类型与传参方式 业务说明
单号 number 字符串 订单号、参考号。可录入多个单号,以空格、英文逗号或回车分割。保持页面原始字符串格式,由现有导出逻辑调用 format_numbers()
产品分类 product_type_id 整数 单选产品分类 ID。
物流产品 product_id 整数数组 多选物流产品 ID。与页面 product_id 多选提交的数组一致。
集货仓库 order_warehouse_id 整数 单选集货仓库 ID。
目的国 receiver_country 字符串 单选目的国国家代码,如 US
是否装柜剔除 is_remove 整数 1 表示是,0 表示否;不传表示全部。必须保留 0 的有效含义。
派送地址 address 字符串 按页面现有规则模糊匹配派送地址。
入库时间 inbound_date 字符串 日期范围,格式为 YYYY-MM-DD - YYYY-MM-DD。由现有导出逻辑按员工时区转换开始和结束时间。
订单状态 status 整数数组 多选订单状态。页面可选值为 010203040,默认选中 3040;MCP 不传时不擅自补默认值,只有用户明确要求按页面默认筛选时才传 [30, 40]
商品属性 is_battery 字符串枚举 页面选择“带电”后提交 is_battery="Y"
商品属性 is_magnetic 字符串枚举 页面选择“带磁”后提交 is_magnetic="Y"
商品属性 is_wood 字符串枚举 页面选择“带木”后提交 is_wood="Y"
商品属性 is_other 字符串枚举 页面选择“其它”后提交 is_other="Y"
商品属性 is_fda 字符串枚举 页面选择“FDA产品”后提交 is_fda="Y"
商品属性 is_toy 字符串枚举 页面选择“玩具”后提交 is_toy="Y"
商品属性 is_ultra_limit 字符串枚举 页面选择“单箱尺寸超长超重”后提交 is_ultra_limit="Y"
商品属性 is_sensitive 字符串枚举 页面选择“敏感货”后提交 is_sensitive="Y"
商品属性 is_food 字符串枚举 页面选择“食品”后提交 is_food="Y"
商品属性 no_property 字符串枚举 页面选择“无属性”后提交 no_property="Y"
合并报关单号 merge_declare_number 字符串 精确匹配合并报关单号。
进口商 importer_id 整数 单选进口商 ID。
货物类型 packing_type 字符串 页面多选后以英文逗号拼接,例如 散货,整柜;不能改为数组。

多选规则

多选参数必须严格保持页面的三种不同传法:

  1. product_idstatus 使用数组。
  2. 商品属性不传 property_2;每个选中属性转换为对应字段并传字符串 Y
  3. packing_type 使用英文逗号拼接的字符串。

Gateway 只接受白名单中的参数。property_2export_typeis_synccompany_idoperator_idis_supertimezone 等页面内部字段或身份字段不放入 MCP Schema。

名称解析规则

用户提供产品、仓库、国家、进口商等业务名称时,AI 必须先调用 list_pending_outbound_export_filter_options 查询当前员工有权使用的选项,再把返回的 value 原样传给 导出工具。不得根据名称、历史调用或示例猜测 ID。

  • product_idproduct_type_idorder_warehouse_idimporter_id 使用筛选项返回的 ID。
  • receiver_country 使用国家选项返回的国家代码。
  • packing_type 使用货物类型选项返回的名称值;多选时以英文逗号拼接。
  • 商品属性使用选项返回的字段名,并把对应字段设置为 Y
  • status 使用订单状态选项返回的整数值。

架构与数据流

Python Gateway

新增 ExportPendingOutboundOrdersTool

  • 工具名为 export_pending_outbound_orders
  • 路由为 /mcp/tools/exportPendingOutboundOrders
  • Schema 使用上表原始参数名和中文业务备注。
  • 调用前仅做类型、枚举、长度、数组数量及日期范围格式校验。
  • 保留页面要求的原始传参结构,不在 Gateway 改写多选格式。
  • 通过现有 ApiClient / ScopedApiClient 转发,并携带工具代码和 request ID。

本地 stdio 与公网 Gateway 都注册该工具;是否展示和允许调用继续由 fmsoperate 的动态工具注册表控制。

筛选项工具

新增 ListPendingOutboundExportFilterOptionsTool

  • 工具名为 list_pending_outbound_export_filter_options
  • 路由为 /mcp/tools/listPendingOutboundExportFilterOptions
  • 必填 filter_type,可选 keywordpagelimit;查询物流产品时可选传 product_type_id,实现与 add 页面相同的产品分类联动。
  • filter_type 支持 product_typeproductwarehousecountryimporterpacking_typeorder_statusgoods_attributeis_remove
  • 返回统一的 records,每项包含可直接用于导出参数的 value、显示名称 label 和辅助识别字段 code;分页信息放在 meta
  • 工具说明明确要求返回值只能用于对应导出字段,不得跨字段使用或猜测未返回的 ID。

fmsoperate

新增 MCP Controller 路由、Validate 规则和专用 Logic。Controller 仅接收请求、调用 Logic 并返回统一响应。

Logic 执行以下步骤:

  1. 从当前 MCP token 恢复的员工会话读取身份、公司、时区和权限上下文。
  2. 只提取参数白名单,拒绝未知参数和调用方提供的身份字段。
  3. 服务端固定补充 export_type=EXPORT_PEND_OUTBOUND_ORDERis_sync=1
  4. 复用现有导出任务创建及 ExportPendOutboundOrderLogic,不复制 SQL、Excel 表头或 OSS 上传逻辑。
  5. 将现有导出结果中的 url 标准化为 MCP 返回字段 file_url

为了保证与后台页面权限一致,MCP 工具必须复用后台“新增排舱/导出未排舱订单”对应菜单权限,并继续经过 动态工具白名单校验。不得仅依赖工具注册表绕过后台菜单权限。

筛选项权限边界

筛选项工具必须先通过动态工具白名单和后台“新增排舱/导出未排舱订单”菜单权限,再查询选项。所有查询只 使用 MCP token 恢复出的服务端会话,禁止接收 company_idadmin_idis_superaccount_typecountry_authplatform_company_idplatform_customer_id 等外部身份或权限参数。

各类筛选项必须复用 add 页面的数据来源和权限口径:

filter_type add 页面数据来源与权限要求 返回 value
product_type OutboundTypeModel::getAllOutboundType(),按当前公司和模型现有条件返回可用排舱分类 分类 ID
product ProductModel::getProductList(),保留当前公司或平台客户产品配置限制,以及页面对产品状态的展示条件;传 product_type_id 时只能在已授权产品内继续过滤 产品 ID
warehouse OrderModel::getSortWarehouse(),保留当前公司、账号和仓库权限排序/过滤规则 仓库 ID
country getTcAddCountryCode() 可用国家范围;非超级管理员且账号类型包含 3 时,继续按当前员工 country_auth 取交集;无授权返回空数组 国家代码
importer fms_importer.company_id=session('company_id')status=1,并保留页面的“由EAC提供”选项 进口商 ID,或 -1
packing_type dictionary('fms_packing_type') 当前有效字典值 页面提交的货物类型名称
order_status OrderModel::getOperateOrderStatus() 中 add 页面实际展示的 0/10/20/30/40 整数状态值
goods_attribute GoodsModel::getGoodsAttr() 及页面附加的“无属性” 对应属性字段名,如 is_batteryno_property
is_remove 页面固定选项“是/否” 整数 1/0

不得为了分页或关键字搜索改成绕过上述业务方法的无权限全表查询。关键字过滤必须在授权结果集内执行;先按 关键字查全表再补权限过滤也不允许。对于静态字典项,仍需先通过工具和菜单权限后才返回。

筛选项查询只负责减少错误输入,不授予数据权限。导出接口收到筛选值后,仍由 OutboundModel::getPendOutboundOrder() 和现有导出链路再次按当前员工上下文限制实际订单数据。

返回与错误处理

成功响应示例:

{
  "code": "MCP_0000",
  "msg": "导出成功",
  "data": {
    "file_url": "https://example.com/fms_export/order-20260714.xlsx"
  },
  "meta": {
    "request_id": "rq_xxx"
  }
}

以下情况返回业务错误:

  • 参数类型、枚举、数量或日期格式不合法;
  • 当前员工没有对应后台菜单权限或工具已停用;
  • 没有可导出的未排舱订单;
  • 导出任务、Excel 生成或 OSS 上传失败;
  • fmsoperate 返回成功但缺少有效下载链接。

业务错误继续由现有 MCP 协议适配器转换为 isError=true,保留 codemsgrequest_id,不产生 JSON-RPC 协议错误,不中断会话。

测试

Python

  • 工具元数据逐项校验参数名、中文备注、类型和枚举。
  • 验证 product_idstatus 保持数组。
  • 验证商品属性转换后的独立 Y 字段,不暴露 property_2
  • 验证 packing_type 保持英文逗号字符串。
  • 验证 is_remove=0 不会被当作空值删除。
  • 验证 stdio、公网工具列表、动态启用和调用转发。
  • 验证成功 URL、业务错误及缺失 URL 的处理。
  • 验证筛选项工具的全部 filter_type、产品分类联动、分页结构和名称解析引导。
  • 验证筛选项工具不接受任何身份或权限覆盖参数。

PHP

  • Validate 覆盖全部白名单参数及边界。
  • Controller/Logic 测试断言传给导出链路的参数与 add.html 实际提交结构一致。
  • 断言服务端固定导出类型、同步标记和会话身份字段,外部无法覆盖。
  • 断言员工菜单权限、动态工具状态和公司数据权限继续生效。
  • 断言成功响应返回 file_url,无数据和导出失败返回明确错误。
  • 使用 A/B 两个公司或客户上下文验证选项隔离,A 员工不得看到 B 公司的产品、仓库、进口商和排舱分类。
  • 验证平台客户只能看到其产品配置范围内的物流产品。
  • 验证受限头程账号只看到 country_auth 允许的目的国,无国家授权时返回空列表。
  • 验证 product_type_id 只能缩小已授权产品范围,不能通过分类联动扩大权限。
  • 验证无菜单权限、工具被停用和伪造身份字段均被拒绝并记录访问日志。

非目标

  • 不修改 outbound/add.html 或其现有导出行为。
  • 不在 Python 中生成、保存或代理传输 Excel 文件。
  • 不新增导出模板、Excel 字段或筛选项。
  • 不改变 OutboundModel::getPendOutboundOrder() 的查询口径。
  • 不把同步导出改造成新的任务轮询协议。