Series Article

Day06 · 物流管理系统(TMS)+ 跨境合规

配套面试准备:完成本篇后,可以继续阅读:Day06_TMS核心业务面试准备] Day06_TMS物流管理系统完整面试指南

课程目标:深度理解跨境国际物流的完整业务链路,实现从运单创建到买家签收的全流程数字化管理,掌握物流渠道智能分配引擎、物流轨迹追踪、费用核算等核心功能,并理解跨境合规的关键知识点。

今日交付物

  • 物流模块 8 张数据库表完整 SQL
  • 物流商与渠道体系完整设计
  • 物流渠道智能分配规则引擎
  • 运单创建与面单打印对接设计
  • 物流轨迹标准化与异常预警
  • 物流费用核算(首重/续重/材积重)
  • 逆向物流(退货)处理流程
  • 跨境合规:HS 编码 / VAT / 清关 / DDP
  • 两模块完整接口清单与错误码

第一节 跨境物流业务全景

1.1 跨境物流与国内物流的本质区别

在中国境内发快递,通常 1-3 天收到,整个过程简单直接。 跨境发货到美国,需要 7-30 天,中间经历十几个节点,每个节点都可能出问题。

graph LR
    subgraph 国内物流-简单
        A1["卖家发货"] --> B1["国内快递(顺丰/圆通)"] --> C1["买家收货"]
        D1["1-3天,1个清关节点(无)"]
    end

    subgraph 跨境物流-复杂
        A2["卖家打包"] --> B2["国内集货/揽收"]
        B2 --> C2["国内出口报关<br>(申报商品/价值/HS编码)"]
        C2 --> D2["国际干线运输<br>(航班/船运/陆运)"]
        D2 --> E2["目的国入境扫描"]
        E2 --> F2["进口清关<br>(缴纳关税/VAT)"]
        F2 --> G2["目的国分拣中心"]
        G2 --> H2["末端配送<br>(UPS/USPS/DHL末端)"]
        H2 --> I2["买家签收"]
        J2["7-30天,2个清关节点,9+个环节"]
    end

1.2 跨境物流主要类型与对比

物流类型时效成本适用场景重量限制特殊限制
国际小包(邮政)15-45天最低轻小件,价格敏感≤2kg含电限制
专线物流7-20天低-中中国直发,中等重量≤30kg按线路各异
商业快递(DHL/UPS/FedEx)3-7天高客单价,紧急≤70kg危险品严格
FBA头程(海运/空运)20-45天亚马逊FBA补货大批量必须符合FBA规范
海外仓配送1-5天(本地)低(本地段)已入海外仓,快速发货无限制需提前备货入仓
FBM(商家自发货)7-20天非FBA亚马逊卖家按渠道时效要求严

1.3 跨境物流的参与角色

graph TD
    subgraph 国内端
        A["🏭 卖家<br>打包+交货"]
        B["📦 头程服务商<br>国内揽收→出口清关<br>→国际运输安排"]
    end

    subgraph 国际干线
        C["✈️ 航空公司 / 🚢 船公司<br>跨洋运输"]
    end

    subgraph 目的国
        D["🏛️ 目的国海关<br>进口清关 + 征税"]
        E["🏢 末端物流商<br>UPS/USPS/DHL/本地配送"]
        F["👤 买家<br>最终收货"]
    end

    A --> B --> C --> D --> E --> F

    G["我们的TMS系统<br>统一管理全链路<br>物流商对接/运单/轨迹/费用"]
    G -.-> B & C & E

1.4 TMS 系统解决的核心问题

痛点TMS 解决方案
对接了十几家物流商,每家接口不同统一适配层,用同一套接口管理所有物流商
不知道选哪家发哪条线路智能规则引擎:自动匹配最优渠道
每次打面单要去各物流商官网操作系统内一键创建运单 + 批量打印面单
货发出去不知道到哪了自动拉取轨迹,异常自动预警
物流费用账单核对困难系统自动计算预估运费,与账单对比
退货逆向物流混乱统一退货单管理,追踪退货全程

第二节 物流数据库表设计

2.1 TMS 模块表结构总览

erDiagram
    logistics_carrier {
        bigint id PK
        bigint tenant_id
        varchar carrier_code "物流商编码"
        varchar carrier_name "物流商名称"
        tinyint carrier_type "类型"
        tinyint status "状态"
    }

    logistics_channel {
        bigint id PK
        bigint carrier_id FK
        varchar channel_code "渠道编码"
        varchar channel_name "渠道名称"
        json country_codes "适用国家列表"
        decimal min_weight_g "最小重量"
        decimal max_weight_g "最大重量"
        tinyint allow_battery "是否允许带电"
        decimal min_days "最短时效天"
        decimal max_days "最长时效天"
        tinyint status "状态"
    }

    logistics_rate {
        bigint id PK
        bigint channel_id FK
        varchar country_code "适用国家"
        decimal first_weight_g "首重克"
        decimal first_weight_price "首重价格"
        decimal extra_weight_g "续重单位克"
        decimal extra_weight_price "续重价格"
        integer volume_factor "材积系数"
    }

    logistics_waybill {
        bigint id PK
        bigint tenant_id
        varchar waybill_no "内部运单号"
        varchar tracking_no "物流商单号"
        bigint order_id FK
        bigint channel_id FK
        tinyint status "状态"
        decimal declared_value "申报价值"
        decimal actual_weight_g "实际重量"
        decimal freight_fee "运费"
    }

    logistics_track {
        bigint id PK
        bigint waybill_id FK
        varchar track_code "轨迹节点码"
        varchar location "当前位置"
        datetime track_time "节点时间"
        tinyint is_exception "是否异常"
    }

    logistics_return {
        bigint id PK
        bigint tenant_id
        varchar return_no "退货单号"
        bigint original_waybill_id FK
        bigint order_id FK
        tinyint status "状态"
        varchar return_tracking_no "退货运单号"
    }

    logistics_fee_record {
        bigint id PK
        bigint waybill_id FK
        decimal estimated_fee "预估运费"
        decimal actual_fee "实际运费"
        decimal fuel_surcharge "燃油附加费"
        decimal peak_surcharge "旺季附加费"
        decimal total_fee "总费用"
    }

    logistics_carrier ||--o{ logistics_channel : "一个物流商有多条渠道"
    logistics_channel ||--o{ logistics_rate : "每条渠道有分国家费率"
    logistics_channel ||--o{ logistics_waybill : "运单使用某条渠道"
    logistics_waybill ||--o{ logistics_track : "运单有多个轨迹节点"
    logistics_waybill ||--o{ logistics_return : "可以产生退货运单"
    logistics_waybill ||--|| logistics_fee_record : "对应一条费用记录"

2.2 物流商主表

-- ============================================================
-- 物流商主表
-- 租户对接的所有物流服务商
-- 如:顺丰国际、EMS、DHL、四方达、云途物流等
-- ============================================================
CREATE TABLE `logistics_carrier`
(
    `id`                BIGINT UNSIGNED  NOT NULL           COMMENT '主键ID',
    `tenant_id`         BIGINT           NOT NULL           COMMENT '租户ID(0=平台预置,所有租户共享)',
    `carrier_code`      VARCHAR(32)      NOT NULL           COMMENT '物流商编码,如 SF_INT/EMS/DHL/USPS,接口调用时使用',
    `carrier_name`      VARCHAR(64)      NOT NULL           COMMENT '物流商名称,如 顺丰国际、DHL Express',
    `carrier_name_en`   VARCHAR(64)      NULL               COMMENT '物流商英文名称',
    `carrier_type`      TINYINT          NOT NULL
        COMMENT '物流商类型:1=国际快递(DHL/FedEx) 2=专线物流 3=邮政小包 4=海外仓配送 5=FBA头程',
    `logo_url`          VARCHAR(512)     NULL               COMMENT '物流商 Logo 图片URL',
    `api_base_url`      VARCHAR(256)     NULL               COMMENT '物流商开放 API 基础地址',
    `api_key`           VARCHAR(256)     NULL               COMMENT 'API Key(加密存储)',
    `api_secret`        VARCHAR(256)     NULL               COMMENT 'API Secret(加密存储)',
    `api_account`       VARCHAR(128)     NULL               COMMENT 'API 账号(部分物流商需要)',
    `api_version`       VARCHAR(16)      NULL               COMMENT 'API 版本号',
    `track_api_url`     VARCHAR(256)     NULL               COMMENT '专用轨迹查询接口地址(若与主接口不同)',
    `support_label`     TINYINT(1)       NOT NULL DEFAULT 1  COMMENT '是否支持在线获取面单:1=支持(系统自动打单) 0=不支持(手动上传)',
    `support_track`     TINYINT(1)       NOT NULL DEFAULT 1  COMMENT '是否支持轨迹查询',
    `status`            TINYINT          NOT NULL DEFAULT 1  COMMENT '状态:0=停用 1=正常',
    `remark`            VARCHAR(512)     NULL               COMMENT '备注(如:仅适用于含电产品/价格谈判联系人)',
    `create_time`       DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time`       DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `create_by`         BIGINT           NULL,
    `is_deleted`        TINYINT(1)       NOT NULL DEFAULT 0,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_tenant_carrier_code` (`tenant_id`, `carrier_code`),
    KEY `idx_carrier_type` (`carrier_type`),
    KEY `idx_status` (`status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
  COMMENT = '物流商主表';

2.3 物流渠道表

-- ============================================================
-- 物流渠道表
-- 同一物流商下可有多条渠道(产品线)
-- 如:顺丰国际 → 顺丰优选小包 / 顺丰空运标准 / 顺丰海运整柜
-- ============================================================
CREATE TABLE `logistics_channel`
(
    `id`                BIGINT UNSIGNED  NOT NULL           COMMENT '主键ID',
    `tenant_id`         BIGINT           NOT NULL           COMMENT '租户ID',
    `carrier_id`        BIGINT           NOT NULL           COMMENT '所属物流商ID',
    `channel_code`      VARCHAR(64)      NOT NULL           COMMENT '渠道编码,调用API时使用,如 SF_INT_EXPRESS',
    `channel_name`      VARCHAR(128)     NOT NULL           COMMENT '渠道名称,如 顺丰国际优选小包(美国线)',
    `channel_type`      TINYINT          NOT NULL
        COMMENT '渠道类型:1=空运快线 2=空运经济 3=海运整柜 4=海运散货 5=陆运 6=邮政小包 7=专线',
    `country_codes`     JSON             NOT NULL           COMMENT '适用目的国代码列表,如 ["US","CA","MX"],["ALL"]表示全球',
    `min_weight_g`      DECIMAL(10, 2)   NOT NULL DEFAULT 0  COMMENT '最小重量(克),低于此重量不适用此渠道',
    `max_weight_g`      DECIMAL(10, 2)   NOT NULL           COMMENT '最大重量(克),超过此重量不适用',
    `max_length_mm`     INT              NULL               COMMENT '最长边限制(毫米)',
    `max_girth_mm`      INT              NULL               COMMENT '围长限制(毫米):长+2×(宽+高),某些物流商有此限制',
    `allow_battery`     TINYINT(1)       NOT NULL DEFAULT 0  COMMENT '是否允许含锂电池:1=允许 0=不允许(航空安全限制)',
    `allow_liquid`      TINYINT(1)       NOT NULL DEFAULT 0  COMMENT '是否允许液体',
    `allow_powder`      TINYINT(1)       NOT NULL DEFAULT 0  COMMENT '是否允许粉末',
    `allow_food`        TINYINT(1)       NOT NULL DEFAULT 1  COMMENT '是否允许食品',
    `min_days`          DECIMAL(4, 1)    NOT NULL           COMMENT '正常时效最短天数',
    `max_days`          DECIMAL(4, 1)    NOT NULL           COMMENT '正常时效最长天数',
    `volume_factor`     INT              NOT NULL DEFAULT 5000 COMMENT '材积系数(除数),常见值5000或6000,计算材积重用',
    `declared_value_limit` DECIMAL(12, 2) NULL              COMMENT '申报价值上限(超过需购买附加险)',
    `status`            TINYINT          NOT NULL DEFAULT 1  COMMENT '状态:0=停用 1=正常 2=暂时关闭(物流商公告服务中断)',
    `sort_order`        INT              NOT NULL DEFAULT 0  COMMENT '推荐排序(数字小优先推荐)',
    `remark`            VARCHAR(512)     NULL               COMMENT '备注',
    `create_time`       DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time`       DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `create_by`         BIGINT           NULL,
    `is_deleted`        TINYINT(1)       NOT NULL DEFAULT 0,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_carrier_channel_code` (`carrier_id`, `channel_code`),
    KEY `idx_carrier_id`   (`carrier_id`),
    KEY `idx_status`       (`status`),
    KEY `idx_channel_type` (`channel_type`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
  COMMENT = '物流渠道表';

2.4 物流费率表

物流费率表不是由 SaaS 平台自行决定快递价格,而是用来保存物流商提供给卖家的合同价、报价表或接口同步价。最终结算金额仍以物流商账单为准,但系统在创建运单之前必须先能估算运费,否则无法自动选择渠道,也无法提前判断订单利润。

费率表主要服务四类业务:

使用场景说明
渠道推荐规则引擎需要先计算不同渠道的预估运费,才能比较成本、时效和可靠性
利润预估OMS/FMS 在订单履约前需要预估物流成本,判断订单是否可能亏损
预算控制物流主管可以按国家、渠道、物流商统计预计成本,提前发现费用异常
账单对账月底物流商账单到达后,用实际费用与系统预估费用对比,找出差异单

可以把它理解为“打车前的预估价”。预估价不是最终账单,但它决定了下单前是否选择这条线路,以及后续账单是否合理。

-- ============================================================
-- 物流费率表
-- 每条渠道按目的国配置收费标准
-- 支持首重+续重计费模型
-- ============================================================
CREATE TABLE `logistics_rate`
(
    `id`                  BIGINT UNSIGNED  NOT NULL         COMMENT '主键ID',
    `tenant_id`           BIGINT           NOT NULL         COMMENT '租户ID',
    `channel_id`          BIGINT           NOT NULL         COMMENT '关联渠道ID',
    `country_code`        CHAR(2)          NOT NULL         COMMENT '目的国代码,ALL表示通用费率',
    `zone`                VARCHAR(16)      NULL             COMMENT '目的区域(部分物流商按区分档,如ZONE1/ZONE2)',
    `currency`            CHAR(3)          NOT NULL DEFAULT 'CNY' COMMENT '计费货币',
    `first_weight_g`      DECIMAL(10, 2)   NOT NULL         COMMENT '首重重量(克),如500克以内',
    `first_weight_price`  DECIMAL(10, 4)   NOT NULL         COMMENT '首重价格',
    `extra_weight_g`      DECIMAL(10, 2)   NOT NULL DEFAULT 500 COMMENT '续重单位(克),如每增加500克',
    `extra_weight_price`  DECIMAL(10, 4)   NOT NULL         COMMENT '续重单价',
    `min_charge`          DECIMAL(10, 4)   NOT NULL DEFAULT 0 COMMENT '最低收费金额',
    `fuel_rate`           DECIMAL(6, 4)    NOT NULL DEFAULT 0 COMMENT '燃油附加费率(小数,如0.15=15%),每月更新',
    `peak_rate`           DECIMAL(6, 4)    NOT NULL DEFAULT 0 COMMENT '旺季附加费率(11月-1月通常会加收)',
    `remote_area_fee`     DECIMAL(10, 4)   NOT NULL DEFAULT 0 COMMENT '偏远地区附加费(如美国农村地区)',
    `effective_date`      DATE             NOT NULL         COMMENT '费率生效日期(费率会定期调整)',
    `expire_date`         DATE             NULL             COMMENT '费率失效日期,NULL表示长期有效',
    `create_time`         DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `create_by`           BIGINT           NULL,
    `is_deleted`          TINYINT(1)       NOT NULL DEFAULT 0,

    PRIMARY KEY (`id`),
    KEY `idx_channel_country` (`channel_id`, `country_code`),
    KEY `idx_effective_date`  (`effective_date`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
  COMMENT = '物流渠道费率表';

2.5 运单主表

-- ============================================================
-- 物流运单主表
-- 每一个发货包裹对应一张运单
-- 一个订单可能拆成多个包裹(多张运单)
-- ============================================================
CREATE TABLE `logistics_waybill`
(
    `id`                BIGINT UNSIGNED  NOT NULL           COMMENT '主键ID',
    `tenant_id`         BIGINT           NOT NULL           COMMENT '租户ID',
    `waybill_no`        VARCHAR(32)      NOT NULL           COMMENT '内部运单号,格式 WB-YYYYMMDD-XXXX',
    `tracking_no`       VARCHAR(128)     NULL               COMMENT '物流商运单号(物流商系统生成),买家追踪包裹用',
    `carrier_id`        BIGINT           NOT NULL           COMMENT '物流商ID',
    `channel_id`        BIGINT           NOT NULL           COMMENT '物流渠道ID',
    `order_id`          BIGINT           NOT NULL           COMMENT '关联销售订单ID',
    `order_no`          VARCHAR(32)      NOT NULL           COMMENT '关联销售订单号(冗余)',
    `warehouse_id`      BIGINT           NOT NULL           COMMENT '发货仓库ID',

    -- 收件人信息(从订单地址冗余,防止地址修改影响历史)
    `receiver_name`     VARCHAR(128)     NOT NULL           COMMENT '收件人姓名',
    `receiver_phone`    VARCHAR(32)      NULL               COMMENT '收件人电话',
    `country_code`      CHAR(2)          NOT NULL           COMMENT '目的国',
    `state`             VARCHAR(64)      NULL               COMMENT '州/省',
    `city`              VARCHAR(64)      NULL               COMMENT '城市',
    `address_line1`     VARCHAR(256)     NOT NULL           COMMENT '地址第一行',
    `address_line2`     VARCHAR(256)     NULL               COMMENT '地址第二行',
    `zip_code`          VARCHAR(16)      NOT NULL           COMMENT '邮政编码',

    -- 包裹信息
    `actual_weight_g`   DECIMAL(10, 2)   NOT NULL           COMMENT '实际重量(克)',
    `volume_weight_g`   DECIMAL(10, 2)   NULL               COMMENT '材积重(克)= 长×宽×高 ÷ 材积系数',
    `charge_weight_g`   DECIMAL(10, 2)   NOT NULL           COMMENT '计费重量 = MAX(实际重, 材积重)',
    `length_mm`         INT              NULL               COMMENT '包裹长度(毫米)',
    `width_mm`          INT              NULL               COMMENT '包裹宽度(毫米)',
    `height_mm`         INT              NULL               COMMENT '包裹高度(毫米)',
    `package_count`     INT              NOT NULL DEFAULT 1  COMMENT '包裹数量(一般1个)',

    -- 申报信息(清关用)
    `declared_value`    DECIMAL(12, 2)   NOT NULL           COMMENT '申报货值(USD),清关申报价值',
    `declared_currency` CHAR(3)          NOT NULL DEFAULT 'USD' COMMENT '申报货币',
    `declared_name_en`  VARCHAR(256)     NOT NULL           COMMENT '申报商品英文名称(清关用,要与HS编码对应)',
    `hs_code`           VARCHAR(16)      NULL               COMMENT 'HS商品编码(清关用)',
    `is_gift`           TINYINT(1)       NOT NULL DEFAULT 0  COMMENT '是否申报为礼品:1=是(部分国家礼品免税)',

    -- 运费信息
    `estimated_fee`     DECIMAL(12, 2)   NOT NULL DEFAULT 0  COMMENT '预估运费(创建运单时计算)',
    `actual_fee`        DECIMAL(12, 2)   NULL               COMMENT '实际运费(从物流商账单同步,可能与预估不同)',
    `fee_currency`      CHAR(3)          NOT NULL DEFAULT 'CNY' COMMENT '运费货币',

    -- 状态与时间
    `status`            TINYINT          NOT NULL DEFAULT 0
        COMMENT '状态:0=待揽收 1=已揽收 2=运输中 3=目的国到达 4=清关中 5=清关完成 6=派件中 7=已签收 8=异常 9=退回',
    `label_url`         VARCHAR(512)     NULL               COMMENT '面单文件URL(PDF或ZPL格式)',
    `label_format`      VARCHAR(8)       NULL               COMMENT '面单格式:PDF/ZPL/PNG',
    `create_waybill_time` DATETIME       NULL               COMMENT '向物流商创建运单时间',
    `pickup_time`       DATETIME         NULL               COMMENT '物流商揽收时间',
    `signed_time`       DATETIME         NULL               COMMENT '签收时间',
    `exception_desc`    VARCHAR(512)     NULL               COMMENT '异常描述',

    `create_time`       DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time`       DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `create_by`         BIGINT           NULL,
    `update_by`         BIGINT           NULL,
    `is_deleted`        TINYINT(1)       NOT NULL DEFAULT 0,
    `version`           INT              NOT NULL DEFAULT 0,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_tenant_waybill_no`  (`tenant_id`, `waybill_no`),
    UNIQUE KEY `uk_tracking_no`        (`tracking_no`)  COMMENT '物流商运单号全局唯一(方便通过快递单号反查)',
    KEY `idx_order_id`      (`order_id`),
    KEY `idx_channel_id`    (`channel_id`),
    KEY `idx_status`        (`status`),
    KEY `idx_create_time`   (`create_time`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
  COMMENT = '物流运单主表';

2.6 物流轨迹表

-- ============================================================
-- 物流轨迹表
-- 记录运单经过的每一个物流节点
-- 只增不改:每次从物流商拉取到新轨迹就插入新记录
-- ============================================================
CREATE TABLE `logistics_track`
(
    `id`              BIGINT UNSIGNED  NOT NULL           COMMENT '主键ID',
    `tenant_id`       BIGINT           NOT NULL           COMMENT '租户ID',
    `waybill_id`      BIGINT           NOT NULL           COMMENT '关联运单ID',
    `tracking_no`     VARCHAR(128)     NOT NULL           COMMENT '物流商运单号(冗余,方便查询)',
    `track_code`      VARCHAR(64)      NULL               COMMENT '轨迹节点标准化代码(内部定义)',
    `track_stage`     TINYINT          NOT NULL
        COMMENT '轨迹阶段:1=已揽收 2=已出库 3=途中 4=目的国到达 5=清关中 6=派件中 7=已签收 8=异常 9=退回',
    `raw_status`      VARCHAR(128)     NULL               COMMENT '物流商原始状态描述(英文)',
    `status_desc`     VARCHAR(256)     NOT NULL           COMMENT '中文状态描述(标准化后的)',
    `location`        VARCHAR(256)     NULL               COMMENT '当前位置,如:Los Angeles, CA, US',
    `location_country` CHAR(2)         NULL               COMMENT '当前位置的国家代码',
    `track_time`      DATETIME         NOT NULL           COMMENT '该轨迹节点发生的时间(物流商提供的时间)',
    `fetch_time`      DATETIME         NOT NULL           COMMENT '我们系统拉取到该轨迹的时间',
    `is_exception`    TINYINT(1)       NOT NULL DEFAULT 0  COMMENT '是否为异常轨迹:1=异常(如:海关扣留/无法投递)',
    `exception_type`  TINYINT          NULL
        COMMENT '异常类型:1=地址有误 2=收件人拒收 3=关税未付 4=海关扣押 5=包裹丢失 6=长时间停滞',
    `exception_desc`  VARCHAR(512)     NULL               COMMENT '异常详细描述',

    PRIMARY KEY (`id`),
    KEY `idx_waybill_id`   (`waybill_id`),
    KEY `idx_tracking_no`  (`tracking_no`),
    KEY `idx_track_time`   (`track_time`),
    KEY `idx_is_exception` (`is_exception`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
  COMMENT = '物流轨迹记录表(只增不改)';

2.7 运单费用记录与退货运单表

-- 运单费用明细记录表
CREATE TABLE `logistics_fee_record`
(
    `id`               BIGINT UNSIGNED  NOT NULL           COMMENT '主键ID',
    `tenant_id`        BIGINT           NOT NULL           COMMENT '租户ID',
    `waybill_id`       BIGINT           NOT NULL           COMMENT '关联运单ID',
    `waybill_no`       VARCHAR(32)      NOT NULL           COMMENT '运单编号(冗余)',
    -- 费用明细
    `base_fee`         DECIMAL(12, 4)   NOT NULL DEFAULT 0  COMMENT '基础运费(首重+续重)',
    `fuel_surcharge`   DECIMAL(12, 4)   NOT NULL DEFAULT 0  COMMENT '燃油附加费',
    `peak_surcharge`   DECIMAL(12, 4)   NOT NULL DEFAULT 0  COMMENT '旺季附加费',
    `remote_fee`       DECIMAL(12, 4)   NOT NULL DEFAULT 0  COMMENT '偏远地区附加费',
    `oversize_fee`     DECIMAL(12, 4)   NOT NULL DEFAULT 0  COMMENT '超尺寸附加费',
    `insurance_fee`    DECIMAL(12, 4)   NOT NULL DEFAULT 0  COMMENT '保价费(按申报货值比例)',
    `other_fee`        DECIMAL(12, 4)   NOT NULL DEFAULT 0  COMMENT '其他杂费',
    `estimated_total`  DECIMAL(12, 2)   NOT NULL DEFAULT 0  COMMENT '预估总费用(创建时计算)',
    `actual_total`     DECIMAL(12, 2)   NULL               COMMENT '实际总费用(从账单同步,为NULL表示待对账)',
    `currency`         CHAR(3)          NOT NULL DEFAULT 'CNY' COMMENT '费用货币',
    `fee_diff`         DECIMAL(12, 2)   GENERATED ALWAYS AS
                       (CASE WHEN `actual_total` IS NOT NULL
                             THEN `actual_total` - `estimated_total`
                             ELSE NULL END) STORED          COMMENT '费用差异 = 实际 - 预估(自动计算)',
    `billing_weight_g` DECIMAL(10, 2)   NOT NULL           COMMENT '计费重量(账单以此重量计费)',
    `rate_id`          BIGINT           NULL               COMMENT '使用的费率ID(方便追溯费率变更)',
    `create_time`      DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time`      DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_waybill_id` (`waybill_id`),
    KEY `idx_tenant_id` (`tenant_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
  COMMENT = '运单费用记录表';

-- 退货运单表(逆向物流)
CREATE TABLE `logistics_return`
(
    `id`                  BIGINT UNSIGNED  NOT NULL         COMMENT '主键ID',
    `tenant_id`           BIGINT           NOT NULL         COMMENT '租户ID',
    `return_no`           VARCHAR(32)      NOT NULL         COMMENT '退货运单号,格式 RWB-YYYYMMDD-XXXX',
    `original_waybill_id` BIGINT           NULL             COMMENT '原始发货运单ID',
    `order_id`            BIGINT           NOT NULL         COMMENT '关联销售订单ID',
    `refund_id`           BIGINT           NULL             COMMENT '关联退款单ID',
    `return_type`         TINYINT          NOT NULL
        COMMENT '退货类型:1=买家退货 2=物流退回(无法送达) 3=海关退回 4=拒收退回',
    `carrier_id`          BIGINT           NULL             COMMENT '退货物流商ID(买家使用的物流)',
    `return_tracking_no`  VARCHAR(128)     NULL             COMMENT '退货快递单号(买家填写)',
    `from_country`        CHAR(2)          NOT NULL         COMMENT '退货来源国(即目的国退回)',
    `to_warehouse_id`     BIGINT           NULL             COMMENT '退货目标仓库ID',
    `status`              TINYINT          NOT NULL DEFAULT 0
        COMMENT '状态:0=待发出 1=运输中 2=已到仓 3=已检验 4=已完成 5=异常',
    `expected_arrive_date` DATE            NULL             COMMENT '预计到仓日期',
    `actual_arrive_date`   DATE            NULL             COMMENT '实际到仓日期',
    `label_url`           VARCHAR(512)     NULL             COMMENT '退货面单URL(我们提供给买家的退货标签)',
    `remark`              VARCHAR(512)     NULL             COMMENT '备注',
    `create_time`         DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time`         DATETIME         NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `create_by`           BIGINT           NULL,
    `is_deleted`          TINYINT(1)       NOT NULL DEFAULT 0,

    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_tenant_return_no` (`tenant_id`, `return_no`),
    KEY `idx_order_id`           (`order_id`),
    KEY `idx_return_tracking_no` (`return_tracking_no`),
    KEY `idx_status`             (`status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
  COMMENT = '退货运单表(逆向物流)';

第三节 物流商与渠道管理

3.1 物流商层级关系

graph TD
    A["物流商:顺丰国际<br>carrier_code = SF_INT"] --> B["渠道1:顺丰优选小包(美线)<br>channel_code = SF_INT_US_ECO<br>适用国:US/CA<br>重量:0-2kg<br>时效:10-20天<br>费率:首重15元/续重8元/500g"]
    A --> C["渠道2:顺丰特惠专线(欧线)<br>channel_code = SF_INT_EU_STD<br>适用国:DE/FR/GB/NL<br>重量:0-30kg<br>时效:7-15天"]
    A --> D["渠道3:顺丰空运快件<br>channel_code = SF_INT_AIR<br>适用国:全球<br>重量:0-70kg<br>时效:3-7天<br>允许含电:是"]

    E["物流商:DHL Express<br>carrier_code = DHL"] --> F["渠道1:DHL Express Worldwide<br>全球覆盖,3-5天,价格高"]
    E --> G["渠道2:DHL eCommerce<br>经济小包,10-20天,价格低"]

3.2 渠道禁运规则配置

不同渠道对商品有不同限制,必须在系统中配置清楚,防止违规发货:

限制项说明违规后果
含锂电池航空对含锂电池有严格限制,超标会被没收货物被没收,卖家损失货物+运费
液体液体超过100ml不能走客机货舱货物被退回,高额罚款
粉末部分国家对粉末严格管控(疑似毒品)海关扣押,买家投诉
重量限制超重包裹被航司拒收退货,延误交付
价值限制高价值货物需要特殊险,超限被拒理赔困难

这些限制不是系统开发人员凭经验随便填写的,而是来自物流商渠道说明、合同报价表、开放 API 返回的渠道能力、物流商运营公告,以及各国海关和航空运输规则。实际项目中一般由物流运营人员维护,技术侧负责把规则固化成字段、校验逻辑和发布流程。

不同的租户 在进行签约物流供应商的时候 可以和物流供应商去谈 获取这些信息 然后录入到SaaS系统中

例如同一家物流商可能同时提供三条线路:

渠道能力说明
普货小包价格低,但不接含电池、液体、粉末
带电专线可接内置电池,如蓝牙耳机,但不接纯电池
敏感货专线可接部分化妆品、液体类商品,但价格更高,清关要求更严格

因此商品主数据中也要维护 is_batteryis_liquidhs_codedeclared_name_en 等字段,TMS 才能在创建运单时自动判断哪些渠道可用。

flowchart TD
    A["配置渠道禁运规则"] --> B["在 logistics_channel 表中设置:<br>allow_battery = 0(不允许含电)<br>allow_liquid = 0(不允许液体)<br>max_weight_g = 2000(最大2kg)"]
    B --> C["运单创建时系统自动校验:<br>查询商品的 is_battery / is_liquid 字段<br>查询包裹实际重量"]
    C --> D{"是否违反限制?"}
    D -- "违反" --> E["❌ 阻止选择该渠道<br>提示具体违规原因"]
    D -- "未违反" --> F["✅ 允许选择该渠道"]

3.3 渠道停用机制

物流商服务中断(如春节停运、台风停航)时,需要快速停用对应渠道:

配置渠道停用后 我们在给租户推荐物流商的时候 就要排除掉这些停用的物流商渠道

flowchart LR
    A["物流商发布公告:<br>2025-01-28至2025-02-05<br>春节停运"] --> B["运营人员在系统中<br>将渠道状态改为:<br>status = 2(暂时关闭)"]
    B --> C["系统自动停止推荐该渠道<br>已创建但未揽收的运单<br>发送预警通知"]
    C --> D["恢复运营后<br>将渠道改回 status = 1"]
    D --> E["重新推荐该渠道"]

渠道状态的变化通常有四个来源:

来源说明系统处理
物流商公告春节停运、台风停航、某国家线路暂停运营人员在后台手动停用渠道
物流商 API渠道状态接口返回不可用,或创建运单返回渠道关闭定时任务同步状态,必要时自动停用
失败率监控某渠道连续下单失败或超时比例过高自动告警,进入临时熔断状态
合规政策变化某类商品、某个国家临时禁运更新禁运规则并停止推荐

一般来说 如果租户是和物流商有签约的 那么物流商那边也会维护签约的用户信息 如果出现了某个渠道不可用的时候 物流商也会通过邮件的形式来通知签约用户

生产系统不能只依赖人工维护。比较稳妥的做法是:人工配置作为主入口,API 同步作为自动校验,失败率监控作为兜底保护。渠道被停用后,规则引擎必须立即排除该渠道,已经创建但尚未揽收的运单要提示物流专员切换备选渠道。

监控某个状态 如果超过了指定的时间 发出预警


第四节 运单智能分配规则引擎

4.1 为什么需要智能分配

跨境卖家通常对接 5-20 条物流渠道,每次发货时如果人工选渠道:

  • 需要了解每条渠道的限制(含电/重量/目的国)
  • 需要比较各渠道的价格和时效
  • 容易出错(选了不允许含电的渠道发含电产品)
  • 效率低(每单都要人工判断)

智能分配引擎:系统根据配置的规则,自动为每个包裹推荐最优渠道。 还可以实现一个在线的下单

4.2 规则引擎完整流程

flowchart TD
    A["输入参数:<br>目的国 = US<br>包裹重量 = 800g<br>材积:15×8×5cm<br>商品类型:含锂电池<br>申报价值 = $29.99<br>时效要求:10天内"] --> B["第一步:过滤层<br>排除不适用的渠道"]
    B --> C["过滤1:目的国覆盖<br>渠道的country_codes 不包含 US → 排除"]
    C --> D["过滤2:重量限制<br>800g > channel.max_weight_g → 排除"]
    D --> E["过滤3:含电检查<br>商品含锂电池 AND channel.allow_battery=0 → 排除"]
    E --> F["过滤4:状态检查<br>channel.status != 1(正常)→ 排除"]
    F --> G["过滤5:时效要求<br>channel.min_days > 10 → 排除(时效不满足)"]
    G --> H["第二步:候选渠道<br/>(通过所有过滤的渠道列表)"]
    H --> I["第三步:综合评分<br/>对每个候选渠道打分"]
    I --> J["成本评分(40%)<br>计算预估运费,越低分越高"]
    I --> K["时效评分(35%)<br>时效越短分越高"]
    I --> L["可靠性评分(25%)<br>近30天该渠道正常签收率"]
    J & K & L --> M["加权计算综合评分"]
    M --> N["按评分排序<br>返回 TOP3 推荐渠道"]
    N --> O["展示给物流专员<br>含:渠道名/预估运费/时效/综合评分"]
    O --> P{"物流专员决策"}
    P -- "接受推荐" --> Q["选择第一推荐渠道<br>自动创建运单"]
    P -- "手动选择" --> R["物流专员从候选中选择"]
    Q & R --> S["创建运单"]

4.3 渠道评分算法详解

成本评分(40%):

以候选渠道中最低运费为基准(100分)
其他渠道:评分 = (最低运费 / 本渠道运费) × 100

示例(发往美国 800g 含电产品):
估算运费是 有费率表 + 商品信息 计算得来的
  渠道A(专线含电):预估运费 35 元 → 成本评分 = 35/35 × 100 = 100分
  渠道B(DHL):     预估运费 85 元 → 成本评分 = 35/85 × 100 = 41分
  渠道C(EMS含电):预估运费 42 元 → 成本评分 = 35/42 × 100 = 83分

时效评分(35%):

以候选渠道中最短时效为基准(100分)
其他渠道:评分 = (最短时效 / 本渠道平均时效) × 100

示例:
  渠道A:时效 10-15天,平均12.5天 → 时效评分 = 3/12.5 × 100 = 24分
  渠道B(DHL):时效 3-5天,平均4天  → 时效评分 = 3/4 × 100 = 75分(最快)→100分
  渠道C(EMS):时效 7-12天,平均9.5天 → 时效评分 = 3/9.5 × 100 = 32分

可靠性评分(25%):

直接使用该渠道近30天的正常签收率(%)作为评分

示例(近30天数据):
需要查询数据库 来进行统计得来的
  渠道A:发出200票,正常签收190票,签收率95% → 可靠性评分 95分
  渠道B:发出50票,正常签收48票,签收率96%  → 可靠性评分 96分
  渠道C:发出80票,正常签收70票,签收率88%  → 可靠性评分 88分

综合评分计算示例:

渠道A:100×40% + 24×35% + 95×25% = 40 + 8.4 + 23.75 = 72.15分
渠道B:41×40% + 100×35% + 96×25% = 16.4 + 35 + 24 = 75.4分 ← 综合最优
渠道C:83×40% + 32×35% + 88×25% = 33.2 + 11.2 + 22 = 66.4分

推荐结果:
  🥇 首选:渠道B(DHL)     综合评分 75.4   运费¥85  时效3-5天
  🥈 次选:渠道A(专线含电) 综合评分 72.15  运费¥35  时效10-15天
  🥉 第三:渠道C(EMS含电) 综合评分 66.4   运费¥42  时效7-12天

系统备注:
  💡 渠道A价格最低,如果买家接受10天以上时效,可选渠道A节省50元
  💡 渠道B时效最快,适合高客单价或买家时效要求高的订单

4.4 规则引擎的 SQL 实现

这里的 SQL 只承担“过滤层”的职责,用来快速排除明显不符合条件的渠道,例如目的国不支持、重量超限、不允许含电、渠道停用等。真正的成本评分、时效评分、可靠性评分更适合放在 Java 代码中完成,因为评分逻辑会不断变化,全部写进 SQL 会导致维护困难。

低频变化的规则数据可以缓存起来。物流渠道、国家限制、含电规则、液体规则、异常关键词、费率模板通常不会每秒变化,因此可以采用“数据库为准,缓存加速”的设计:

关于常用的渠道 最好是缓存到Redis中 因为每次发货都需要进行物流的推荐 也就是每次发货都需要进行渠道表的查询 而渠道的信息 基本上也不太会发生变化 非常适合作为Redis的缓存进行存储

数据库:保存最终规则配置
Redis/本地缓存:保存热点渠道规则和费率
规则变更:后台保存后主动刷新缓存
运单创建:先读缓存,缓存未命中再查数据库

需要注意:缓存只能提升查询效率,不能成为唯一数据源。规则变更、渠道停用、费率调整后必须刷新缓存,否则系统可能继续推荐已经关闭或不合规的渠道。

只要涉及到渠道表的修改 都要先完成修改 然后再删除缓存

-- 查询适合某次发货的候选渠道(过滤层)
SELECT
    lc.id,
    lc.channel_code,
    lc.channel_name,
    lc.min_days,
    lc.max_days,
    (lc.min_days + lc.max_days) / 2.0   AS avg_days,
    lc.volume_factor,
    lb.carrier_name
FROM logistics_channel lc
JOIN logistics_carrier lb ON lc.carrier_id = lb.id
WHERE
    lc.tenant_id IN (#{tenantId}, 0)              -- 租户自定义渠道 + 平台预置渠道
    AND lc.status = 1                              -- 仅正常渠道
    AND lb.status = 1                              -- 仅正常物流商
    AND lc.is_deleted = 0
    -- 目的国过滤
    AND (
        JSON_CONTAINS(lc.country_codes, JSON_QUOTE('ALL'))
        OR JSON_CONTAINS(lc.country_codes, JSON_QUOTE(#{countryCode}))
    )
    -- 重量过滤
    AND lc.min_weight_g <= #{actualWeightG}
    AND lc.max_weight_g >= #{actualWeightG}
    -- 含电过滤
    AND (#{hasBattery} = 0 OR lc.allow_battery = 1)
    -- 液体过滤
    AND (#{hasLiquid} = 0 OR lc.allow_liquid = 1)
    -- 时效过滤(如有要求)
    AND (#{maxDaysRequired} IS NULL OR lc.min_days <= #{maxDaysRequired})
ORDER BY
    lc.sort_order ASC;

推荐的流程:

  1. 先从订单和SKU表中提取组合相关的物流信息(收货人的信息 商品的信息)
  2. 从物流商渠道表中进行过滤 找出满足收货人信息和商品信息的渠道
  3. 在筛选出来的这些渠道中 进行评分 (价格 时效性 签收率) 然后得出综合评分
  4. 按照综合评分 进行排序 列出 评分最高的 TOP3 返回显示给租户去看
  5. 租户可以配置是自动选择 还是 手动选择
    1. 如果租户配置的是自动选择 SaaS系统就可以自动选择 综合评分第一的渠道 然后实现自动在线下单 整个过程不需要人为参与
    2. 如果租户配置的是手动选择 SaaS系统只需要给列出来 TOP3 让租户的人员自己去选择即可

第五节 运单创建与面单打印

5.1 运单创建完整流程

flowchart TD
    A(["OMS 推送发货指令<br>订单已出库,需要创建运单"]) --> B["TMS 接收发货指令<br>获取:订单信息+商品信息+收货地址"]
    B --> C["调用规则引擎<br>获取推荐渠道 TOP3"]
    C --> D{"是否开启自动分配?<br>(租户配置)"}
    D -- "是(全自动模式)" --> E["自动选择第一推荐渠道<br>无需人工干预"]
    D -- "否(人工确认)" --> F["推送给物流专员<br>展示候选渠道对比"]
    F --> G["物流专员选择渠道<br>确认申报信息"]
    E & G --> H["创建内部运单记录<br>logistics_waybill<br/>status = 0(待揽收)"]
    H --> I["调用物流商 API<br>创建外部运单"]
    I --> J{"API 调用结果"}
    J -- "成功" --> K["获取物流商运单号 tracking_no<br>获取面单文件 URL(PDF/ZPL)"]
    K --> L["更新运单记录:<br>tracking_no 填入<br>label_url 填入<br/>status 保持 0(待揽收)"]
    L --> M["计算预估运费<br>写入 logistics_fee_record"]
    M --> N["回调 OMS:<br>更新订单的物流单号<br>订单状态 → 已发货"]
    J -- "失败" --> O["记录失败原因<br>发送告警通知<br/>进入人工处理队列"]
    O --> P["人工重试或切换渠道"]
    P --> I
    N --> Q(["运单创建完成"])

在线下单并不等于物流费用已经最终结算。创建运单 API 的主要作用是向物流商系统提交包裹信息,换取物流商运单号、跟踪号和面单文件。费用是否立即返回,取决于物流商和结算方式:

场景费用特点
预付渠道创建运单时可能立即扣费,API 会返回较明确的费用
月结渠道下单时只返回预估费用,月底物流商按实际揽收重量和材积重出账单
复称计费仓库申报重量只是初始重量,物流商揽收或入仓后复称,最终费用可能变化
多段物流国际段和末端派送可能产生不同单号和附加费

==因此 tracking_no 通常是买家可查询的物流跟踪号,但系统也要预留内部运单号、物流商运单号、末端派送单号、转单号等字段,避免跨境多段运输时只保存一个单号导致追踪断裂。==

5.2 面单打印设计

graph TD
    A["面单打印需求"] --> B{"面单格式"}
    B -- "PDF格式" --> C["适合:普通激光打印机<br>A4纸或快递面单纸<br>打印后粘贴到包裹"]
    B -- "ZPL格式" --> D["适合:热敏标签打印机(斑马打印机)<br>直接打出不干胶标签<br>贴到包裹上效率更高"]
    B -- "PNG图片" --> E["备用方案:<br>某些物流商只提供图片格式"]

    F["批量打印功能"] --> G["选择多张运单(≤200张)"]
    G --> H["系统将所有面单PDF合并<br>或按序排列ZPL指令"]
    H --> I["一次性发送给打印机<br>自动连续打印"]
    I --> J["打印完成后<br/>运单状态标记为:已打印<br/>记录打印时间和操作人"]

面单包含的关键信息:

┌─────────────────────────────────────────────┐
│  [物流商Logo]           [条形码: 运单号]    │
├─────────────────────────────────────────────┤
│  收件人:John Smith                          │
│  地址:123 Main St, Los Angeles, CA 90001   │
│  电话:+1-310-555-0001                      │
│  邮编:90001   国家:US                     │
├─────────────────────────────────────────────┤
│  发件人:FlexChain Warehouse                 │
│  地址:广东省广州市天河区XX路1号            │
│  中国 China                                  │
├─────────────────────────────────────────────┤
│  商品申报:Bluetooth Earphone               │
│  数量:1    申报价值:USD 15.00             │
│  HS Code:8518.30.00    重量:120g          │
├─────────────────────────────────────────────┤
│  内部订单号:ORD-20250117-0001              │
│  服务类型:Standard Shipping                 │
│  [二维码:扫码可追踪物流]                   │
└─────────────────────────────────────────────┘

生成面单和自动打印是两件事。TMS 从物流商 API 获取 PDF、PNG 或 ZPL 面单文件,只代表系统已经拿到了可打印内容;如果仓库要求“下单成功后自动打印”,还需要和本地打印环境联调,例如浏览器打印、热敏打印机、ZPL 指令、本地打印插件或局域网打印服务。

面单 有的物流商可以直接返回 有的物流商没有返回 有的物流商可能只会返回部分信息 不管是什么情况 我们都可以自己去组合成一个完整的面单信息

面单打印上线前至少要验证:

  • 标签纸尺寸是否正确,如 100mm × 150mm。
  • 条码和二维码是否清晰,扫描枪能否识别。
  • PDF、ZPL、PNG 三种格式是否按仓库打印机能力正确输出。
  • 批量打印时是否丢单、串单、顺序错乱。
  • 打印完成后是否记录打印时间、操作人和重打次数。

5.3 物流商 API 对接示例

以顺丰国际为例,展示创建运单 API 的调用流程:

sequenceDiagram
    participant TMS as TMS 服务
    participant SF as 顺丰国际 API

    TMS->>SF: POST /open/v1/waybills/create<br>Header: Authorization: Bearer {access_token}
    Note right of TMS: 请求体(JSON):<br>{"orderNo":"WB-20250117-0001",<br>"serviceCode":"SF_INT_EXPRESS",<br>"shipper":{...发件人信息},<br>"consignee":{...收件人信息},<br>"parcel":{"weight":120,"length":150,...},<br>"declared":{<br>"value":15.00,"currency":"USD",<br>"descEn":"Bluetooth Earphone",<br>"hsCode":"851830"<br>}}

    SF->>TMS: HTTP 200 响应
    Note left of SF: {"success":true,<br>"data":{<br>"waybillNo":"SF1234567890",<br>"labelUrl":"https://label.sf.com/xxx.pdf",<br>"labelFormat":"PDF",<br>"estimatedFee":38.50<br>}}

    TMS->>TMS: 更新运单记录<br>tracking_no = "SF1234567890"<br>label_url = "https://..."
    TMS->>OMS: 回调通知<br>订单已获取运单号

第六节 物流轨迹追踪系统

6.1 轨迹追踪的两种模式

graph TD
    A["物流轨迹获取方式"] --> B["主动拉取(定时轮询)<br>每2小时对所有运输中的运单<br>调用物流商轨迹查询API"]
    A --> C["被动接收(Webhook推送)<br>部分物流商支持轨迹更新时主动推送<br>如DHL/FedEx的事件通知"]

    B --> D["优点:实现简单<br>缺点:存在2小时延迟"]
    C --> E["优点:实时性高<br>缺点:并非所有物流商支持"]

    B & C --> F["统一处理层<br>轨迹标准化 + 写入数据库"]

6.2 轨迹标准化处理

不同物流商的轨迹描述千差万别,需要统一标准化:

flowchart TD
    A["物流商原始轨迹数据"] --> B{"来自哪家物流商?"}
    B -- "DHL" --> C["DHL原始状态:<br>Shipment picked up<br>In transit to destination<br>Customs cleared<br>Delivered"]
    B -- "EMS" --> D["EMS原始状态:<br>ACCEPTANCE<br>DESPATCH FROM OUTWARD OE<br>ARRIVAL AT INWARD OE<br>DELIVERY"]
    B -- "专线物流" --> E["专线原始状态:<br>已揽件<br>已到达中转中心<br>海关查验中<br>派件中<br>签收"]
    C & D & E --> F["标准化适配器<br>(每个物流商一个Adapter)"]
    F --> G["统一内部状态码(track_stage):<br>1=已揽收<br>2=已出库(离开中国)<br>3=途中(国际运输)<br>4=目的国到达<br>5=清关中<br>6=派件中<br>7=已签收<br>8=异常"]
    G --> H["统一中文描述:<br>包裹已由顺丰国际揽收<br>包裹已离开中国<br>包裹正在国际运输中<br>包裹已到达美国<br>..."]

6.3 轨迹拉取定时任务

一般每个物流商的API 都会有一个 限制 调用频率的限制 为了防止 攻击 还有一些无效的查询 每次的请求 对应物流商的服务器来说 都是压力的

我们可能是订单比较多 然后触发了限制 怎么做

“按物流商分组”不是为了绕过物流商限流,而是为了按不同物流商的限流规则分别控制调用节奏。比如云途允许每秒 50 次,DHL 允许每秒 20 次,燕文允许每分钟 1000 次,如果系统把所有运单混在一起无脑并发调用,很容易让某一家物流商 API 触发 429 或临时封禁。

分组后的处理方式是:每个物流商使用独立的限流器、线程池和重试策略。云途这一组只按云途的规则调用,DHL 这一组只按 DHL 的规则调用,某一家物流商接口变慢时不会拖垮其他物流商。

flowchart TD
    A(["定时任务:每2小时执行"]) --> B["查询所有处于运输中的运单:<br>status IN (0,1,2,3,4,5,6)<br>即非已签收、非异常、非退回"]
    B --> C["按物流商分组<br/>避免单个物流商API被限流"]
    C --> D["对每个物流商的运单<br>批量查询轨迹(最多50个/次)"]
    D --> E["调用物流商轨迹API<br>carrier_id对应不同的Adapter"]
    E --> F["解析返回的轨迹数据"]
    F --> G{"与已有轨迹对比<br/>是否有新节点?"}
    G -- "无新节点" --> H["跳过,无需更新"]
    G -- "有新轨迹节点" --> I["写入 logistics_track 表<br/>(追加新节点,不修改旧的)"]
    I --> J["更新运单状态<br/>logistics_waybill.status"]
    J --> K["检查是否有异常轨迹<br/>(is_exception = 1)"]
    K -- "有异常" --> L["触发异常预警<br/>发送告警通知"]
    K -- "无异常" --> M["检查是否最终签收"]
    M -- "已签收" --> N["更新运单状态为已签收(7)<br/>记录签收时间<br/>回调OMS更新订单状态"]
    L & N & H --> O["继续处理下一批运单"]
    O --> P(["任务结束"])

6.4 异常轨迹检测规则

异常轨迹检测可以用 SQL 做第一层筛选,但不能设计成“每个运单查一次轨迹”。真实系统应尽量批量处理,并在运单主表冗余关键字段,例如 last_track_timelast_track_statuslast_track_desclast_track_stage。这样检测“超过 N 天无轨迹更新”时,只需要扫描运输中的运单主表,而不是每次都聚合整张轨迹明细表。

推荐做法:

轨迹写入时:同步更新 logistics_waybill.last_track_time / last_track_stage
异常扫描时:批量查询运输中且 last_track_time 超时的运单
复杂关键词:从缓存读取异常关键词规则,在 Java 中匹配
异常结果:落库保存,不能只放 Redis

Redis 可以缓存异常关键词、不同阶段的停滞阈值、运输中运单集合等低频变化数据,但异常检测的最终结果仍然要写入数据库,方便查询、告警、人工跟进和后续复盘。

graph TD
    A["异常检测规则引擎"] --> B["规则1:长时间无更新(停滞)<br>最后一条轨迹时间超过N天(可配置)<br>默认:揽收后5天/途中7天/清关中3天"]
    A --> C["规则2:清关异常<br>出现'Customs hold'/'Cleared failed'<br>等关键词识别"]
    A --> D["规则3:地址问题<br>出现'Invalid address'/'Return to sender'<br/>等关键词"]
    A --> E["规则4:包裹损坏<br>出现'Damaged'/'Exception'<br/>等关键词"]
    A --> F["规则5:派件失败<br>连续3次出现'Delivery attempt failed'<br/>买家不在家未取件"]
    B & C & D & E & F --> G["生成异常预警通知<br/>is_exception = 1<br/>exception_type 对应类型"]
    G --> H["站内信+邮件通知物流专员<br/>附:运单号/订单号/当前位置/异常原因"]
    H --> I["物流专员跟进处理:<br/>联系物流商客服<br/>通知买家<br/>发起理赔等"]

异常停滞检测 SQL:

-- 查询所有超过7天无轨迹更新的运输中运单
SELECT
    w.waybill_no,
    w.tracking_no,
    w.order_no,
    w.country_code,
    lc.carrier_name,
    ch.channel_name,
    w.create_waybill_time,
    MAX(t.track_time)                    AS last_track_time,
    DATEDIFF(NOW(), MAX(t.track_time))   AS days_no_update,
    w.status
FROM logistics_waybill w
JOIN logistics_carrier lc ON w.carrier_id = lc.id
JOIN logistics_channel ch ON w.channel_id = ch.id
LEFT JOIN logistics_track t ON w.id = t.waybill_id
WHERE
    w.tenant_id = #{tenantId}
    AND w.status IN (1, 2, 3, 4, 5, 6)  -- 运输中的状态
    AND w.is_deleted = 0
GROUP BY
    w.id, w.waybill_no, w.tracking_no, w.order_no,
    w.country_code, lc.carrier_name, ch.channel_name,
    w.create_waybill_time, w.status
HAVING
    days_no_update > 7  -- 超过7天无更新
    OR MAX(t.track_time) IS NULL  -- 或者从未有轨迹(揽收后失联)
ORDER BY
    days_no_update DESC NULLS FIRST;

第七节 物流费用核算

物流的费用 最终是以物流公司的核算为准 SaaS平台的核算 仅仅只是一个估算 目的是为了帮租户筛选出来一个最优的物流商的渠道

TMS 计算物流费用并不是为了替代物流商账单,而是为了让卖家在发货前就知道大概成本,并在月底对物流商账单做核对。最终财务入账通常以物流商实际账单为准,但系统预估费用仍然非常重要:

使用方使用目的
物流专员下单前比较不同渠道成本,选择合适线路
订单系统判断某个订单发货后是否可能亏损
财务人员月底导入物流商账单,核对实际费用是否异常
运营/BI分析不同国家、渠道、SKU 的物流成本趋势

因此费用表中通常同时保留两套金额:estimated_fee 表示系统根据费率表计算的预估费用,actual_fee 表示物流商账单中的实际费用。两者的差异不是错误,而是对账分析的入口。

7.1 运费计算公式全解

graph TD
    A["运费计算步骤"] --> B["Step 1:确定计费重量"]
    B --> C["实际重量(克):称重获得"]
    B --> D["材积重(克)= 长(mm)×宽(mm)×高(mm) ÷ (材积系数×1000)<br>材积系数通常为5000或6000<br>单位统一:毫米÷(mm³/cm³×系数)=克"]
    C & D --> E["计费重量 = MAX(实际重量, 材积重量)"]
    E --> F["Step 2:计算基础运费"]
    F --> G["if 计费重量 ≤ 首重重量:<br>  基础运费 = 首重价格<br>else:<br>  续重段 = 向上取整((计费重量-首重重量) / 续重单位)<br>  基础运费 = 首重价格 + 续重段 × 续重价格"]
    G --> H["Step 3:叠加附加费"]
    H --> I["+&nbsp;燃油附加费 = 基础运费 × 燃油费率"]
    H --> J["+&nbsp;旺季附加费 = 基础运费 × 旺季费率(如适用)"]
    H --> K["+&nbsp;偏远地区附加费(如目的地是偏远区)"]
    H --> L["+&nbsp;保价费 = 申报货值 × 保价费率(如购买保价)"]
    I & J & K & L --> M["Step 4:取最低收费<br>总费用 = MAX(以上合计, 最低收费金额)"]

具体计算示例:

包裹信息:
  实际重量:800g
  尺寸:20cm × 15cm × 10cm
  目的国:美国
  申报价值:$29.99

Step 1 确定计费重量:
  材积重 = (200mm × 150mm × 100mm) ÷ (5000 × 1000mm³/cm³)
         = 3,000,000mm³ ÷ 5,000,000mm³/kg × 1000g/kg
         = 600g
  计费重量 = MAX(800g, 600g) = 800g(实际重量更大)

Step 2 计算基础运费(以顺丰国际优选小包为例):
  首重500g:¥18元
  续重每500g:¥9元
  超出首重:800g - 500g = 300g
  续重段数:向上取整(300/500) = 1段
  基础运费 = ¥18 + 1 × ¥9 = ¥27

Step 3 叠加附加费:
  燃油附加费率(当月):15%
  燃油费 = ¥27 × 15% = ¥4.05
  旺季:否(非11月-1月)
  偏远区:否(洛杉矶不是偏远区)

Step 4 总运费:
  总计 = ¥27 + ¥4.05 = ¥31.05
  最低收费:¥15(满足)
  最终运费:¥31.05

7.2 运费核算代码逻辑说明

flowchart TD
    A["创建运单时调用运费计算"] --> B["输入:渠道ID + 目的国 + 包裹重量+尺寸 + 申报价值"]
    B --> C["查询最新有效费率:<br>SELECT * FROM logistics_rate<br>WHERE channel_id=? AND country_code IN (?,  'ALL')<br>AND effective_date <= TODAY<br>AND (expire_date IS NULL OR expire_date > TODAY)<br/>ORDER BY country_code DESC LIMIT 1<br>(精确国家优先于ALL)"]
    C --> D["计算材积重:<br>volume_weight = L × W × H / (rate.volume_factor × 1000)"]
    D --> E["取计费重量:<br>charge_weight = MAX(actual_weight, volume_weight)"]
    E --> F["计算基础运费:<br>if charge <= first_weight:<br>  base = first_price<br>else:<br>  extra = CEIL((charge-first_weight)/extra_unit)<br>  base = first_price + extra × extra_price"]
    F --> G["叠加燃油/旺季/偏远附加费"]
    G --> H["写入 logistics_fee_record:<br>estimated_total = 计算结果<br>base_fee / fuel_surcharge / peak_surcharge 分别记录"]
    H --> I["更新 logistics_waybill.estimated_fee"]

7.3 运费账单对账

物流商每月发送账单,系统需要与预估运费进行对账:

预估费用和实际费用产生差异很常见,原因包括仓库称重与物流商复称不一致、材积重被重新计算、燃油附加费变化、偏远地区附加费、住宅地址附加费、清关附加费、旺季附加费等。系统允许小范围差异自动通过,是为了减少人工工作量;超过阈值的单据才需要人工复核。

通常可以设置:

差异比例 <= 5%:自动确认
差异比例 > 5%:进入差异单,财务或物流专员复核
多次出现同类差异:更新费率表或反馈物流商
flowchart TD
    A["物流商账单到达(Excel/CSV格式)"] --> B["财务专员上传账单到系统"]
    B --> C["系统解析账单数据:<br>逐行读取:运单号/计费重量/各项费用"]
    C --> D["根据运单号匹配内部运单"]
    D --> E["逐条对比:<br>实际费用 vs 预估费用"]
    E --> F{"差异是否在容忍范围内?<br>(通常允许±5%)"}
    F -- "在范围内" --> G["自动确认,标记为已对账"]
    F -- "超出范围" --> H["标记为差异单,人工复核"]
    H --> I{"差异原因"}
    I -- "物流商计费错误" --> J["向物流商申请更正"]
    I -- "我们重量申报有误" --> K["修正内部记录<br/>差额计入当期损益"]
    I -- "附加费变化未同步" --> L["更新系统费率配置"]
    G & J & K & L --> M["更新 logistics_fee_record.actual_total"]
    M --> N["生成对账报告:<br>本期总运费/差异金额/差异率/异常单列表"]

如果我们从物流商那边下载的Excel表格比较大 我们可以使用 EasyExcel 进行优化上传 避免OOM 对账:

  1. 先读取自己的表 然后查询 EasyExcel 解析后导入的那个表 再把结果 进行对比 可以通过 SQL 完全实现
  2. 把两个表的数据 都查询出来 然后通过Java代码进行循环对比

第八节 逆向物流(退货)

8.1 退货物流的三种场景

graph TD
    A["退货场景分类"] --> B["场景1:买家退货<br>买家不满意,主动退回<br>需要提供退货地址(海外仓/返回中国)"]
    A --> C["场景2:物流退回<br>无法投递:地址错误/收件人拒签/<br>超时未取/清关失败<br>物流商自动退回给卖家"]
    A --> D["场景3:召回处理<br>产品有质量问题,主动召回<br>发送退货标签给买家"]

    B --> E["处理:提供海外仓退货地址<br>或生成退货标签(Prepaid Label)"]
    C --> F["处理:等待货物退回仓库<br>分析退回原因,避免重复"]
    D --> G["处理:批量生成退货标签<br>发送给对应订单买家"]

退货标签 就是 退货的信息 买家在退货的时候 根据退货的信息 就知道要把这个快递 返还到哪个仓库

8.2 退货全流程

flowchart TD
    A(["买家申请退货<br>(OMS 退款单已审核通过)"]) --> B["根据退货政策决定:<br>是否需要退货?<br>还是直接退款不退货?"]
    B --> C{"需要退货"}
    C -- 需要 --> D["生成退货标签<br>(Prepaid Return Label)<br>买家无需付运费"]
    D --> E["将退货标签发给买家<br>站内消息 + 邮件"]
    E --> F["买家打印标签<br/>寄出退货包裹"]
    F --> G["创建退货运单记录<br>logistics_return"]
    G --> H["系统追踪退货物流轨迹<br>通过买家填写的退货单号"]
    H --> I{"货物状态"}
    I -- "退到海外仓" --> J["海外仓收货<br>二次质检<br/>(参见WMS退货入库流程)"]
    I -- "退回国内仓" --> K["国内仓收货<br/>二次质检"]
    I -- "退货在途超时" --> L["预警通知:<br>退货包裹可能丢失<br/>联系物流商跟进"]
    J & K --> M["质检完成<br/>通知OMS处理退款"]
    M --> N(["退货处理完成"])
    C -- "不需要(直接退款)" --> O(["无需退货,直接进入退款流程"])

8.3 退货面单(Prepaid Label)生成

退货标签(Return Label / Prepaid Label)可以理解为“逆向物流的面单”。正向发货时,仓库给包裹贴发货面单;买家退货时,也需要一张写明退货地址、退货运单号、条码和 RMA 编号的退货标签。

Prepaid Label 表示这张退货标签通常已经由卖家承担运费。买家只需要打印标签、贴到包裹上并交给指定物流商,不需要现场支付运费。系统生成退货标签后,可以通过邮件、站内消息或平台消息发送给买家,并在 TMS 中继续追踪退货包裹是否到达海外仓或退货仓。

flowchart LR
    A["系统决定提供退货标签"] --> B["选择退货物流渠道<br/>(通常用目的国当地物流商)"]
    B --> C["调用物流商API<br/>创建退货运单"]
    C --> D{"API返回退货标签"}
    D -- 成功 --> E["存储面单URL<br/>logistics_return.label_url"]
    E --> F["通过邮件发送给买家<br/>附:退货地址+打包要求+注意事项"]
    D -- "物流商不支持在线退货" --> G["人工生成退货地址<br/>发给买家"]

第九节 跨境合规专项

重要说明:跨境合规是一个专业的法律+海关领域知识,本节提供系统设计所需的核心概念,具体合规细节请咨询专业顾问。

9.1 HS 编码(海关商品编码)

HS 编码不是每个国家完全独立的一套编码。它的前 6 位基于世界海关组织 WCO 的 Harmonized System,属于国际通用基础编码;但第 7 位以后,各国可以根据自己的关税、监管、统计要求继续扩展。因此同一个商品在不同国家申报时,前 6 位通常一致,完整编码、税率和监管条件可能不同。

系统设计时不要只保存一个“全球唯一 HS Code”就结束,更合理的做法是:

商品基础资料:保存常用 6 位 HS 编码和英文申报名称
目的国配置:按国家维护完整申报码、关税税率、特殊监管要求
运单申报:根据目的国选择对应完整编码和申报信息
graph TD
    A["HS编码:8518.30.90"] --> B["85 = 大类<br>电机、发电机、变压器及<br>电气控制和配电设备"]
    A --> C["8518 = 小类<br>麦克风、扬声器、耳机等音频设备"]
    A --> D["8518.30 = 品目<br>耳机(无论是否有麦克风)"]
    A --> E["8518.30.90 = 子目<br>其他耳机(非助听器)"]

HS 编码在系统中的作用:

使用场景说明
出口报关中国海关要求申报 HS 编码,决定是否需要许可证
进口关税计算目的国根据 HS 编码确定关税税率
物流渠道限制某些 HS 编码的商品有运输限制
VAT 计算欧洲 VAT 税率按商品类别不同而不同

HS 编码校验规则:

-- product_sku 表中 hs_code 字段的校验逻辑(在应用层实现)
-- HS编码:6位数字(国际通用)或8-10位(各国细分)
-- 格式示例:851830(6位)或 8518309000(10位,中国版)

-- 验证规则:
-- 1. 只含数字
-- 2. 长度为6位或8-10位
-- 3. 前两位必须是有效的HS大类(01-99)
-- 4. 与商品品类要匹配(需要海关知识,系统可提供查询功能)

9.2 清关流程与申报要求

flowchart TD
    A(["包裹到达目的国海关"]) --> B["海关扫描包裹<br/>核查面单信息"]
    B --> C{"申报信息审核"}
    C --> D["核查:HS编码是否正确<br/>申报价值是否合理<br/>商品描述是否清晰"]
    D --> E{"是否超过免税门槛"}
    E -- "低于免税门槛" --> F["直接放行<br/>无需缴纳关税"]
    E -- "超过免税门槛" --> G["计算关税:<br/>关税 = 申报价值 × 关税税率(HS编码对应)<br/>VAT = (申报价值 + 关税) × VAT税率"]
    G --> H{"关税由谁支付?"}
    H -- "DDP条款(卖家承担)" --> I["卖家已预缴关税<br/>包裹直接放行"]
    H -- "DDU条款(买家承担)" --> J["买家需缴纳关税后<br/>包裹才能放行<br/>(买家体验差!)"]
    I & F --> K["清关通过<br/>进入末端派送"]
    J --> L{"买家是否愿意缴税"}
    L -- 愿意 --> K
    L -- "拒绝缴税" --> M["包裹退回给卖家<br/>卖家损失运费+货物"]
    C --> N{"申报有问题"}
    N --> O["海关查验(开箱检查)<br/>可能扣押/罚款/没收"]

各主要市场免税门槛(供系统配置参考):

目的国免税门槛备注
美国按最新政策配置美国小额免税政策会随商品来源国和政策变化调整,系统应做成可配置
欧盟€150超过此值需缴纳VAT,另外征收关税有额外门槛
英国£135超过需缴纳VAT(20%)
日本¥16,666(约$110)按CIF价值计算
澳大利亚AUD 1,000超过需缴纳GST(10%)
加拿大CAD 20门槛很低,几乎都要缴税

9.3 DDP vs DDU 对买家体验的影响

graph TD
    subgraph DDU-关税未预缴
        A1["买家下单支付$29.99"] --> B1["包裹到达海关"]
        B1 --> C1["海关通知买家:<br/>需要缴纳关税$8.50+VAT$4.50"]
        C1 --> D1["买家困惑或拒绝缴纳"]
        D1 --> E1["差评!包裹被退回!<br/>卖家亏损"]
    end

    subgraph DDP-关税已预缴
        A2["买家下单支付$29.99<br/>(含税价格)"] --> B2["包裹到达海关"]
        B2 --> C2["海关:关税已由卖家预缴<br/>直接放行"]
        C2 --> D2["买家无缝收货<br/>体验极佳"]
        D2 --> E2["好评!复购!"]
    end

系统设计要点:在 logistics_waybill 表中,is_gift 字段和申报价值 declared_value 直接影响清关。系统应该:

  1. 提醒用户合理申报货值(过低申报可能被海关查验)
  2. 高价值商品建议提示购买保价
  3. 对于 DDP 条款的渠道,提前计算关税成本,计入商品定价

9.4 欧洲 VAT 合规

VAT 是增值税。对跨境卖家来说,VAT 不只是税务部门的事情,也会直接影响订单利润、平台结算和商品定价。例如一笔德国订单售价包含 VAT,如果系统不把 VAT 单独拆出来,SKU 利润会被高估。

OSS 是欧盟的 One Stop Shop,一站式 VAT 申报机制。它的含义不是“只交一个国家的税”,而是卖家可以在一个欧盟成员国注册 OSS,通过一个入口统一申报其面向多个欧盟国家消费者销售产生的 VAT。比如卖家在德国注册 OSS,本季度同时卖给法国、意大利、西班牙消费者,就可以通过德国 OSS 入口汇总申报这些国家对应的 VAT。

OSS 只是简化了卖家的一个VAT的申报的流程 但是VAT的价格是没有少的

系统支持 VAT 功能,主要是为了帮助卖家完成三件事:

业务场景系统作用
利润核算从订单销售额中拆出 VAT,避免把税金误算成利润
财务对账对接平台结算报告时识别 VAT、退款、平台费等金额
申报准备按国家、税率、申报周期汇总销售额和应缴 VAT,导出给财务或税务代理
graph TD
    A["欧洲VAT合规要求"] --> B["什么是VAT?<br/>增值税(Value Added Tax)<br/>欧盟各国消费税(17%-27%不等)"]
    A --> C["跨境卖家需要VAT注册的条件:<br/>年销售额超过各国起征点<br/>或使用欧洲仓库存货<br/>(FBA必须注册VAT)"]
    B & C --> D["OSS制度(One Stop Shop)<br/>2021年推出,卖家只需在<br/>一个EU成员国注册<br/>统一申报所有EU国的VAT"]
    D --> E["系统需要支持的功能:<br/>1. 维护各国VAT税率配置表<br/>2. 按销售额自动计算VAT金额<br/>3. 生成VAT申报数据导出<br/>4. 记录VAT号和申报历史"]

VAT 相关数据库配置:

-- 增值税率配置表(全局配置)
CREATE TABLE `tax_vat_rate`
(
    `id`           BIGINT UNSIGNED  NOT NULL,
    `country_code` CHAR(2)          NOT NULL           COMMENT '国家代码,如DE/FR/GB',
    `country_name` VARCHAR(32)      NOT NULL           COMMENT '国家名称',
    `standard_rate` DECIMAL(6, 4)   NOT NULL           COMMENT '标准税率,如0.19=19%(德国)',
    `reduced_rate`  DECIMAL(6, 4)   NULL               COMMENT '优惠税率(食品/书籍等特殊品类)',
    `registration_threshold` DECIMAL(12, 2) NULL       COMMENT '注册起征点(当地货币)',
    `effective_date` DATE           NOT NULL           COMMENT '税率生效日期',
    `expire_date`   DATE            NULL               COMMENT '税率失效日期',
    PRIMARY KEY (`id`),
    KEY `idx_country_date` (`country_code`, `effective_date`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
  COMMENT = 'VAT税率配置表';

-- 插入主要欧洲国家VAT税率
INSERT INTO `tax_vat_rate` (`id`, `country_code`, `country_name`, `standard_rate`, `effective_date`) VALUES
(1, 'DE', '德国', 0.1900, '2024-01-01'),
(2, 'FR', '法国', 0.2000, '2024-01-01'),
(3, 'GB', '英国', 0.2000, '2024-01-01'),
(4, 'IT', '意大利', 0.2200, '2024-01-01'),
(5, 'ES', '西班牙', 0.2100, '2024-01-01'),
(6, 'NL', '荷兰', 0.2100, '2024-01-01'),
(7, 'PL', '波兰', 0.2300, '2024-01-01'),
(8, 'SE', '瑞典', 0.2500, '2024-01-01'),
(9, 'AT', '奥地利', 0.2000, '2024-01-01'),
(10, 'BE', '比利时', 0.2100, '2024-01-01');

9.5 数据合规(GDPR)

GDPR 是欧盟《通用数据保护条例》,关注的是个人数据保护。虽然我们的系统是给卖家使用的供应链 SaaS,但只要系统同步了欧盟消费者订单,就会处理买家姓名、电话、邮箱、收货地址、订单商品、物流轨迹等个人数据,因此不能把合规责任完全推给 Amazon、TikTok、Shopee 等电商平台。

在这个关系中,可以这样理解:

卖家:决定为什么处理数据,是数据控制者
SaaS 平台:按照卖家指令处理订单和物流数据,是数据处理者
电商平台:也会处理买家数据,但不代表 SaaS 可以忽略自身责任

虽然我们的SaaS平台 没有直接跟买家进行交互 但是只要SaaS平台拿到了买家的数据 就必须要遵守这个GDPR

GDPR 是否适用,不只看服务器部署在哪里,更看系统是否处理欧盟个人数据、是否服务面向欧盟消费者的业务。如果中国卖家把商品卖给德国、法国、意大利消费者,并把订单同步到国内部署的 SaaS 系统,系统仍然需要考虑数据最小化、访问控制、加密存储、保留期限、删除或匿名化等能力。

这些合规要求通常会通过平台规则、卖家合同、数据处理协议、消费者投诉、数据泄露事件和第三方审计体现出来。系统设计时不要等到出问题才补救,而应该在 OMS、TMS、FMS 和权限模块中预留基础能力。

graph TD
    A["GDPR 对系统的影响"] --> B["数据最小化原则<br/>只收集业务必需的个人信息<br/>不存储非必要数据"]
    A --> C["数据保留期限<br/>买家收货地址保留不超过6个月<br/>(超期自动脱敏处理)"]
    A --> D["数据访问权<br/>买家可申请导出自己的订单数据<br/>系统需要支持数据导出接口"]
    A --> E["数据删除权<br/>买家申请删除账户数据<br/>系统必须在30天内处理"]
    A --> F["数据加密传输<br/> HTTPS 全站加密<br/>数据库中电话/邮件等敏感字段加密存储"]
    B & C & D & E & F --> G["系统合规实现:<br/>1. 地址字段加密存储<br/>2. 定时任务自动脱敏过期数据<br/>3. 提供个人数据导出API<br/>4. 审计日志记录数据访问"]

第十节 物流数据统计报表

10.1 物流核心 KPI

指标名称计算公式业务意义预警阈值
平均签收时效签收时间 - 揽收时间(天)衡量物流整体效率> 渠道承诺最长时效 + 3天
正常签收率正常签收运单数 / 总运单数衡量物流服务质量< 95% 预警
异常率异常运单数 / 总运单数衡量问题频率> 3% 预警
运费准确率预估运费差异 ≤ 5% 的占比衡量费率配置准确性< 90% 预警
及时揽收率24h内揽收的运单占比衡量物流商响应速度< 95% 预警

10.2 物流渠道效率报表 SQL

-- 各渠道物流效率统计(近30天)
SELECT
    ch.channel_name,
    lc.carrier_name,
    COUNT(w.id)                                        AS total_waybills,  -- 总运单数
    SUM(CASE WHEN w.status = 7 THEN 1 ELSE 0 END)     AS signed_count,    -- 已签收数
    ROUND(
        SUM(CASE WHEN w.status = 7 THEN 1 ELSE 0 END) * 100.0 / COUNT(w.id)
    , 2)                                               AS sign_rate,       -- 签收率%
    SUM(CASE WHEN w.status = 8 THEN 1 ELSE 0 END)     AS exception_count, -- 异常数
    ROUND(
        SUM(CASE WHEN w.status = 8 THEN 1 ELSE 0 END) * 100.0 / COUNT(w.id)
    , 2)                                               AS exception_rate,  -- 异常率%
    ROUND(AVG(
        CASE WHEN w.status = 7 AND w.signed_time IS NOT NULL
             THEN DATEDIFF(w.signed_time, w.pickup_time)
             ELSE NULL END
    ), 1)                                              AS avg_days,        -- 平均时效(天)
    ROUND(AVG(w.estimated_fee), 2)                     AS avg_fee,         -- 平均运费(元)
    ROUND(SUM(w.estimated_fee), 2)                     AS total_fee        -- 总运费(元)
FROM logistics_waybill w
JOIN logistics_channel ch ON w.channel_id = ch.id
JOIN logistics_carrier lc ON w.carrier_id = lc.id
WHERE
    w.tenant_id = #{tenantId}
    AND w.create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
    AND w.is_deleted = 0
GROUP BY ch.id, ch.channel_name, lc.carrier_name
ORDER BY total_waybills DESC;

10.3 物流 Dashboard 看板设计

物流数据看板                          时间范围:[近30天]

┌──────────┬──────────┬──────────┬──────────┐
│ 本月运单量 │ 正常签收率 │ 异常运单  │ 平均时效  │
│  2,856票 │  97.3%   │  77票    │  12.3天  │
│ ↑8%      │ ↓0.5%    │ ↑3票     │ ↓1.2天   │
└──────────┴──────────┴──────────┴──────────┘

┌────────────────────────┬───────────────────────┐
│ 各渠道签收率对比(柱状图)│ 运单状态分布(饼图)   │
│                         │                       │
│ DHL:     99.1% ████████ │  ■ 运输中  45%        │
│ 顺丰专线: 97.8% ███████  │  ■ 已签收  38%        │
│ EMS:     94.2% ██████   │  ■ 清关中   9%        │
│ 专线含电: 91.5% █████    │  ■ 派件中   5%        │
│                         │  ■ 异常    3%         │
└────────────────────────┴───────────────────────┘

┌────────────────────────────────────────────────┐
│ 需要立即处理的异常运单                          │
│ 🔴 WB-20250110-0023  DHL  停滞12天  美国-加州  │
│    → [联系DHL客服] [通知买家] [发起理赔]        │
│ 🟡 WB-20250115-0156  EMS  清关5天   德国-柏林  │
│    → [查询清关状态] [提示买家缴税]              │
│ 🟡 WB-20250116-0089  SF   派件失败3次 英国     │
│    → [联系买家确认地址] [预约重新派件]          │
└────────────────────────────────────────────────┘

第十一节 接口设计规范

11.1 TMS 完整接口清单

基础路径前缀: /api/tms

模块接口名称HTTP方法路径权限标识
物流商物流商列表GET/carrierstms:carrier:list
物流商新增物流商POST/carrierstms:carrier:add
物流商编辑物流商PUT/carriers/{id}tms:carrier:edit
渠道管理渠道列表GET/channelstms:channel:list
渠道管理新增渠道POST/channelstms:channel:add
渠道管理停用渠道PUT/channels/{id}/disabletms:channel:edit
渠道管理费率配置PUT/channels/{id}/ratestms:channel:rate
运单管理运单列表GET/waybillstms:waybill:list
运单管理运单详情GET/waybills/{id}tms:waybill:list
运单管理创建运单POST/waybillstms:waybill:add
运单管理批量创建运单POST/waybills/batchtms:waybill:add
运单管理获取面单GET/waybills/{id}/labeltms:waybill:list
运单管理批量打印面单POST/waybills/labels/batchtms:waybill:list
运单管理手动更新状态PUT/waybills/{id}/statustms:waybill:edit
运单管理取消运单PUT/waybills/{id}/canceltms:waybill:cancel
渠道推荐智能推荐渠道POST/recommendtms:waybill:list
运费估算运费预估POST/fee/estimatetms:waybill:list
轨迹查询运单轨迹GET/waybills/{id}/trackstms:track:list
轨迹查询异常运单列表GET/waybills/exceptionstms:track:list
轨迹查询手动刷新轨迹POST/waybills/{id}/tracks/refreshtms:track:refresh
退货运单退货单列表GET/returnstms:return:list
退货运单创建退货单POST/returnstms:return:add
退货运单确认退货到仓PUT/returns/{id}/arrivetms:return:edit
费用核算费用记录列表GET/feestms:fee:list
费用核算账单导入POST/fees/import-billtms:fee:import
报表统计渠道效率报表GET/report/channel-efficiencytms:report:view
报表统计物流异常报表GET/report/exceptionstms:report:view
报表统计运费统计GET/report/fee-summarytms:report:view
Webhook轨迹推送(DHL)POST/webhook/dhl无需(签名验证)
Webhook轨迹推送(FedEx)POST/webhook/fedex无需(签名验证)

11.2 关键接口请求/响应示例

① 智能渠道推荐接口:

POST /api/tms/recommend
Content-Type: application/json

{
  "orderId": "7001",
  "countryCode": "US",
  "actualWeightG": 800,
  "lengthMm": 150,
  "widthMm": 80,
  "heightMm": 50,
  "hasBattery": true,
  "hasLiquid": false,
  "declaredValue": 29.99,
  "declaredCurrency": "USD",
  "maxDaysRequired": 15
}

响应:
{
  "code": 200,
  "data": {
    "chargeWeightG": 800,
    "volumeWeightG": 600,
    "recommendations": [
      {
        "rank": 1,
        "channelId": "200001",
        "channelCode": "SF_INT_BATTERY_US",
        "channelName": "顺丰国际含电专线(美国)",
        "carrierName": "顺丰国际",
        "minDays": 10,
        "maxDays": 18,
        "estimatedFee": 35.20,
        "currency": "CNY",
        "totalScore": 78.5,
        "scoreDetail": {
          "costScore": 100,
          "deliveryScore": 45,
          "reliabilityScore": 97
        },
        "allowBattery": true,
        "signRate30d": 97.3,
        "tips": "价格最优,时效10-18天,适合非紧急订单"
      },
      {
        "rank": 2,
        "channelId": "200002",
        "channelCode": "DHL_EXPRESS_US",
        "channelName": "DHL Express(美国)",
        "carrierName": "DHL",
        "minDays": 3,
        "maxDays": 5,
        "estimatedFee": 88.50,
        "currency": "CNY",
        "totalScore": 75.4,
        "scoreDetail": {
          "costScore": 40,
          "costBaseScore": 100,
          "reliabilityScore": 99
        },
        "allowBattery": true,
        "signRate30d": 99.1,
        "tips": "时效最快3-5天,价格较高,适合高客单价紧急订单"
      }
    ]
  }
}

② 运费估算接口:

POST /api/tms/fee/estimate

请求:
{
  "channelId": "200001",
  "countryCode": "US",
  "actualWeightG": 800,
  "lengthMm": 150,
  "widthMm": 80,
  "heightMm": 50,
  "declaredValue": 29.99
}

响应:
{
  "code": 200,
  "data": {
    "chargeWeightG": 800,
    "volumeWeightG": 600,
    "baseFee": 27.00,
    "fuelSurcharge": 4.05,
    "peakSurcharge": 0.00,
    "remoteFee": 0.00,
    "insuranceFee": 0.00,
    "totalEstimatedFee": 31.05,
    "currency": "CNY",
    "rateDetail": {
      "firstWeightG": 500,
      "firstWeightPrice": 18.00,
      "extraWeightG": 500,
      "extraWeightPrice": 9.00,
      "fuelRate": "15%",
      "effectiveDate": "2025-01-01"
    }
  }
}

11.3 TMS 模块专用错误码

错误码范围:16000 - 16099

错误码含义触发场景
16001物流商不存在根据ID查不到
16002渠道不存在或已停用创建运单时选择的渠道无效
16003商品含电池,该渠道不允许含电产品选择了限电渠道
16004包裹超重,该渠道不支持重量超过渠道最大限制
16005目的国不在该渠道服务范围目的国不在 country_codes 列表中
16006无可用渠道,请检查包裹信息所有渠道均被过滤掉
16007物流商 API 调用失败外部接口超时或返回错误
16008面单生成失败物流商拒绝创建运单
16009运单号不存在查询不到物流轨迹
16010运单已取消,不能操作操作已取消的运单
16011运单已揽收,不能取消货物已在物流商手中
16012费率配置不存在,无法计算运费缺少目的国费率
16013申报价值超过该渠道限额需要购买保价或换渠道

今日总结与作业

今日知识点回顾

TMS 物流管理:

  • 跨境物流 6 种类型对比(国际小包/专线/商业快递/FBA头程/海外仓配送/FBM)
  • 物流商→渠道两级体系设计,渠道禁运规则配置(含电/液体/重量)
  • 智能渠道推荐引擎:三层过滤(国家/重量/禁运)→三维评分(成本40%+时效35%+可靠性25%)
  • 运单创建完整流程(含 API 对接和失败降级处理)
  • 面单格式:PDF / ZPL(热敏标签)/ PNG,批量打印优化
  • 轨迹标准化:不同物流商原始描述 → 统一内部状态码(8个阶段)
  • 异常检测 5 条规则(停滞/清关/地址/损坏/派件失败)
  • 运费计算:材积重 = 长×宽×高 ÷ 材积系数;计费重 = MAX(实际重, 材积重)
  • 账单对账:预估 vs 实际,差异 > 5% 人工复核
  • 退货运单:3 种场景(买家退/物流退/召回),退货面单生成

跨境合规:

  • HS 编码:6-10 位商品编码,决定关税税率和运输限制
  • 清关流程:出口报关→国际运输→进口清关(缴税)→末端配送
  • DDP vs DDU:DDP 卖家预缴税,买家无感;DDU 买家缴税,体验差
  • 主要市场免税门槛(美国$800,欧盟€150,英国£135)
  • 欧洲 VAT:各国税率不同(17%-27%),OSS 统一申报制度
  • GDPR:数据最小化、保留期限、删除权、加密存储

数据库: 8 张表(物流商/渠道/费率/运单/轨迹/费用/退货/VAT税率)


今日作业

作业 1:数据库执行(必做)

  1. 执行本课所有建表 SQL(8 张表)
  2. 插入测试数据:
-- 插入物流商
INSERT INTO `logistics_carrier` (
    `id`, `tenant_id`, `carrier_code`, `carrier_name`, `carrier_type`,
    `support_label`, `support_track`, `status`
) VALUES
(6001, 0, 'SF_INT', '顺丰国际', 2, 1, 1, 1),
(6002, 0, 'DHL', 'DHL Express', 1, 1, 1, 1),
(6003, 0, 'EMS', 'EMS国际邮政', 3, 1, 1, 1);

-- 插入渠道
INSERT INTO `logistics_channel` (
    `id`, `tenant_id`, `carrier_id`, `channel_code`, `channel_name`,
    `channel_type`, `country_codes`,
    `min_weight_g`, `max_weight_g`, `allow_battery`, `allow_liquid`,
    `min_days`, `max_days`, `volume_factor`, `status`, `sort_order`
) VALUES
(7001, 0, 6001, 'SF_INT_ECO_US', '顺丰国际优选小包(美线)',
 2, '["US","CA","MX"]', 0, 2000, 0, 0, 10, 20, 5000, 1, 1),
(7002, 0, 6001, 'SF_INT_BATTERY_US', '顺丰国际含电专线(美线)',
 7, '["US","CA"]', 0, 5000, 1, 0, 10, 18, 5000, 1, 2),
(7003, 0, 6002, 'DHL_EXPRESS_WW', 'DHL Express Worldwide',
 1, '["ALL"]', 0, 70000, 1, 1, 3, 7, 5000, 1, 3);

-- 插入费率(顺丰含电专线 美国)
INSERT INTO `logistics_rate` (
    `id`, `tenant_id`, `channel_id`, `country_code`, `currency`,
    `first_weight_g`, `first_weight_price`, `extra_weight_g`, `extra_weight_price`,
    `min_charge`, `fuel_rate`, `effective_date`
) VALUES
(8001, 0, 7002, 'US', 'CNY', 500, 18.00, 500, 9.00, 15.00, 0.15, '2025-01-01'),
(8002, 0, 7002, 'CA', 'CNY', 500, 20.00, 500, 10.00, 18.00, 0.15, '2025-01-01');

-- 插入测试运单
INSERT INTO `logistics_waybill` (
    `id`, `tenant_id`, `waybill_no`, `tracking_no`,
    `carrier_id`, `channel_id`, `order_id`, `order_no`, `warehouse_id`,
    `receiver_name`, `country_code`, `state`, `city`,
    `address_line1`, `zip_code`,
    `actual_weight_g`, `charge_weight_g`,
    `declared_value`, `declared_currency`, `declared_name_en`, `hs_code`,
    `estimated_fee`, `fee_currency`,
    `status`, `create_waybill_time`
) VALUES (
    9001, 101, 'WB-20250117-0001', 'SF1234567890',
    6001, 7002, 7001, 'ORD-20250117-0001', 4001,
    'John Smith', 'US', 'California', 'Los Angeles',
    '123 Main Street Apt 4B', '90001',
    120, 120,
    15.00, 'USD', 'Bluetooth Earphone', '8518300090',
    31.05, 'CNY',
    1, NOW()
);

-- 插入运单轨迹
INSERT INTO `logistics_track` (
    `id`, `tenant_id`, `waybill_id`, `tracking_no`,
    `track_stage`, `raw_status`, `status_desc`,
    `location`, `location_country`,
    `track_time`, `fetch_time`, `is_exception`
) VALUES
(10001, 101, 9001, 'SF1234567890', 1, 'Picked up', '包裹已由顺丰国际揽收',
 '广州市天河区', 'CN', '2025-01-17 16:30:00', NOW(), 0),
(10002, 101, 9001, 'SF1234567890', 2, 'Departed from origin', '包裹已离开中国发往目的地',
 '广州白云国际机场', 'CN', '2025-01-17 23:45:00', NOW(), 0),
(10003, 101, 9001, 'SF1234567890', 3, 'In transit', '包裹在国际运输途中',
 'Los Angeles, CA', 'US', '2025-01-19 08:00:00', NOW(), 0);

-- 验证查询:查看运单当前状态和最新轨迹
SELECT
    w.waybill_no,
    w.tracking_no,
    lc.carrier_name,
    ch.channel_name,
    CASE w.status
        WHEN 0 THEN '待揽收'
        WHEN 1 THEN '已揽收'
        WHEN 2 THEN '运输中'
        WHEN 7 THEN '已签收'
        WHEN 8 THEN '异常'
        ELSE CONCAT('状态:', w.status)
    END AS status_name,
    w.estimated_fee,
    t.status_desc AS latest_track,
    t.location AS current_location,
    t.track_time AS latest_track_time
FROM logistics_waybill w
JOIN logistics_carrier lc ON w.carrier_id = lc.id
JOIN logistics_channel ch ON w.channel_id = ch.id
LEFT JOIN logistics_track t ON w.id = t.waybill_id
    AND t.track_time = (
        SELECT MAX(t2.track_time) FROM logistics_track t2 WHERE t2.waybill_id = w.id
    )
WHERE w.tenant_id = 101 AND w.is_deleted = 0;

作业 2:业务分析题(必做)

  1. 规则引擎优先级:同一包裹(800g 含锂电,发往美国)有 3 条可用渠道:A(专线含电,¥35,15天)、B(DHL,¥88,4天)、C(EMS,¥42,12天)。请用本课学习的评分算法(成本40%+时效35%+可靠性25%),假设三条渠道的历史签收率均为 97%,计算每条渠道的综合评分,并说明哪个应该排第一推荐。

  2. 材积重计算:一个包裹,实际重量 300g,尺寸为 30cm × 25cm × 20cm,物流商材积系数为 5000。请计算:材积重是多少克?计费重量是多少?(过程要完整写出)

  3. 轨迹停滞预警:运单 WB-20250110-0023 的最后一条轨迹时间是 2025-01-10 14:00,当前时间是 2025-01-17 10:00,共停滞了多少天?按本课设计的规则,应该触发什么级别的预警?应该怎么处理?

  4. DDP 和 DDU 的实际场景:假设你的耳机售价 $29.99,美国进口关税税率是 0%(由于中美贸易政策,假设该商品税率),但英国对该商品收 20% VAT,英国买家购买时售价是 £24.99。在 DDU 条款下,买家需要额外支付多少?这会造成什么影响?如果改成 DDP 条款,卖家需要在定价时如何调整?

参考答案:

  1. 可以先把成本和时效都转换成 0-100 分,成本越低分越高,时效越快分越高。假设成本分 = 最低费用 / 当前费用 × 100,时效分 = 最快时效 / 当前时效 × 100,签收率分 = 97。A:成本分 100,时效分 4/15×100=26.67,综合分 100×40% + 26.67×35% + 97×25% = 73.58。B:成本分 35/88×100=39.77,时效分 100,综合分 39.77×40% + 100×35% + 97×25% = 75.16。C:成本分 35/42×100=83.33,时效分 4/12×100=33.33,综合分 83.33×40% + 33.33×35% + 97×25% = 69.25。因此 B 排第一,但如果卖家选择“成本优先”策略,也可以把 A 或 C 提前。

  2. 材积重公式为:长 × 宽 × 高 / 材积系数。该包裹材积重为 30 × 25 × 20 / 5000 = 3kg = 3000g。实际重量是 300g,物流商按实际重量和材积重取较大值计费,所以计费重量是 3000g。

  3. 从 2025-01-10 14:00 到 2025-01-17 10:00,一共停滞 6 天 20 小时,接近 7 天。按常见规则,超过 3 天可黄色预警,超过 5 天可红色预警,所以应触发红色停滞预警。系统应生成物流异常记录,通知物流专员,自动调用物流商轨迹查询接口复查;如果仍无更新,需要联系物流商客服,并同步给订单系统,必要时给买家发送延迟说明。

  4. DDU 表示税费由买家到货时承担。美国进口关税假设为 0%,买家不需要额外支付关税。英国 VAT 为 20%,买家售价 £24.99,则买家可能额外支付 24.99 × 20% = £5.00 左右,还可能有清关服务费。这样会导致买家收货体验差,容易拒收或差评。如果改成 DDP,卖家需要提前承担 VAT 和可能的清关费用,定价时要把这部分成本计入售价或利润模型,例如把 £5.00 VAT 加入成本,再重新计算利润率,避免表面有利润、实际亏损。

作业 3:SQL 练习(必做)

-- 练习1:查询顺丰国际(carrier_code='SF_INT')
-- 所有渠道的基本信息
-- 要求显示:渠道名称、适用国家数量(从JSON中统计)、
-- 最大重量、是否允许含电、最短和最长时效
-- 你的答案:
SELECT
    ch.channel_name,
    CASE
        WHEN JSON_CONTAINS(ch.country_codes, JSON_QUOTE('ALL')) THEN '全部国家'
        ELSE CAST(JSON_LENGTH(ch.country_codes) AS CHAR)
    END AS country_count,
    ch.max_weight_g,
    CASE ch.allow_battery WHEN 1 THEN '允许' ELSE '不允许' END AS allow_battery,
    ch.min_days,
    ch.max_days
FROM logistics_channel ch
JOIN logistics_carrier lc ON ch.carrier_id = lc.id
WHERE lc.carrier_code = 'SF_INT'
  AND ch.status = 1
  AND ch.is_deleted = 0
ORDER BY ch.sort_order ASC;



-- 练习2:计算最近30天每个渠道的平均运费和运单量
-- 要求:按平均运费升序排列
-- 你的答案:
SELECT
    ch.channel_name,
    COUNT(w.id) AS waybill_count,
    ROUND(AVG(w.estimated_fee), 2) AS avg_estimated_fee
FROM logistics_waybill w
JOIN logistics_channel ch ON w.channel_id = ch.id
WHERE w.tenant_id = 101
  AND w.is_deleted = 0
  AND w.create_waybill_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY ch.id, ch.channel_name
ORDER BY avg_estimated_fee ASC;



-- 练习3:查询当前仍在运输中(status IN 1,2,3,4,5,6)
-- 且创建时间超过20天的运单(可能超期)
-- 要求显示:运单号、物流渠道、目的国、创建时间、已运输天数
-- 你的答案:
SELECT
    w.waybill_no,
    ch.channel_name,
    w.country_code,
    w.create_waybill_time,
    TIMESTAMPDIFF(DAY, w.create_waybill_time, NOW()) AS shipping_days
FROM logistics_waybill w
JOIN logistics_channel ch ON w.channel_id = ch.id
WHERE w.tenant_id = 101
  AND w.is_deleted = 0
  AND w.status IN (1, 2, 3, 4, 5, 6)
  AND w.create_waybill_time < DATE_SUB(NOW(), INTERVAL 20 DAY)
ORDER BY shipping_days DESC;



-- 练习4:统计今年各月的运费总支出
-- 按月份分组,显示月份、运单总量、预估总运费、已实际对账的实际总运费
-- 你的答案:
SELECT
    DATE_FORMAT(create_waybill_time, '%Y-%m') AS month,
    COUNT(*) AS waybill_count,
    SUM(estimated_fee) AS estimated_fee_total,
    SUM(CASE WHEN actual_fee IS NOT NULL THEN actual_fee ELSE 0 END) AS actual_fee_total
FROM logistics_waybill
WHERE tenant_id = 101
  AND is_deleted = 0
  AND create_waybill_time >= MAKEDATE(YEAR(CURDATE()), 1)
  AND create_waybill_time < DATE_ADD(MAKEDATE(YEAR(CURDATE()), 1), INTERVAL 1 YEAR)
GROUP BY DATE_FORMAT(create_waybill_time, '%Y-%m')
ORDER BY month ASC;

作业 4:设计题(选做)

新需求:支持”物流时效承诺”功能。卖家在商品详情页展示”预计送达日期”(如:预计2025年1月25日前送达),系统需要根据买家所在地、库存所在仓库、可用渠道的时效,动态计算最早送达日期。

请设计:

  1. 计算预计送达日期需要哪些输入参数?(至少考虑5个因素)
  2. 时效计算时需要考虑哪些非工作日的情况?(如春节/圣诞节/物流商公告停运)
  3. 设计一张节假日配置表(给出 SQL),用于在计算时效时跳过节假日
  4. 写出计算预计送达日期的伪代码逻辑(不需要真实代码,描述步骤即可)

参考答案:

  1. 预计送达日期至少需要:买家国家和邮编、发货仓库、SKU 是否含电/液体/敏感货、包裹重量和尺寸、可用物流渠道、渠道最短/最长时效、仓库处理时效、下单时间是否超过当日截单时间、节假日和物流商停运公告。

  2. 需要考虑中国春节、国庆、仓库周末休息、目的国圣诞节/黑五高峰、物流商临时停运、海关节假日、当地极端天气。跨境物流不是简单加 N 天,仓库处理、头程运输、清关、末端派送都可能受非工作日影响。

  3. 节假日配置表可以这样设计:

CREATE TABLE `logistics_calendar_holiday` (
  `id` BIGINT PRIMARY KEY COMMENT '主键ID',
  `tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID,0表示平台通用',
  `country_code` VARCHAR(8) NOT NULL COMMENT '国家编码',
  `carrier_id` BIGINT DEFAULT NULL COMMENT '物流商ID,NULL表示所有物流商',
  `holiday_date` DATE NOT NULL COMMENT '节假日日期',
  `holiday_name` VARCHAR(100) NOT NULL COMMENT '节假日名称',
  `holiday_type` TINYINT NOT NULL COMMENT '类型:1国家假日 2仓库休息 3物流商停运 4不可抗力',
  `is_workday` TINYINT NOT NULL DEFAULT 0 COMMENT '是否工作日',
  `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  UNIQUE KEY `uk_holiday` (`tenant_id`, `country_code`, `carrier_id`, `holiday_date`, `holiday_type`)
) COMMENT='物流节假日配置表';
  1. 伪代码逻辑:
1. 根据订单国家、仓库、重量、尺寸、商品属性筛选可用物流渠道
2. 按渠道规则计算预计费用和时效,得到 min_days、max_days
3. 取当前时间作为起点,如果超过仓库截单时间,则起运日顺延 1 天
4. 先加上仓库处理时效,再逐日累加物流运输时效
5. 累加过程中跳过仓库休息日、物流商停运日、目的国节假日
6. 得到最早送达日和最晚送达日
7. 返回给商品页或订单页展示:预计 X 月 X 日 - X 月 X 日送达

明日预告

第七天:财务结算系统(FMS)+ 数据分析与智能决策(BI)

明天将实现供应链最后的财务闭环和数据智能分析,包括:

  • 多平台收款对账(亚马逊 Settlement Report 解析)
  • SKU 级利润核算(扣除所有成本后的真实利润)
  • 多货币处理与汇兑损益
  • 应付账款管理(与第三天 PMS 联动)
  • 欧洲 VAT 申报数据生成
  • 供应链 KPI Dashboard(库存周转率/OTD/缺货率等)
  • 智能补货算法(移动平均预测)
  • AI 自然语言数据查询

预习建议:

  • 了解亚马逊 Settlement Report 的基本结构(含哪些费用项)
  • 理解什么是”毛利润”和”净利润”,以及计算公式
  • 思考:同一款商品,在亚马逊卖 $29.99,扣除哪些费用后才是真正赚到的钱?

学习提示:今天的内容包含大量的”行业知识”(跨境物流规则、清关、VAT),这些不是纯技术内容,但在跨境电商领域工作必须了解。

今天最重要的 3 个设计点:

  1. 渠道过滤规则:必须在渠道层面配置禁运规则,而不是在代码里硬编码。这样物流商规则变化时,只需改配置不需要改代码。
  2. 轨迹标准化:对接多个物流商时,用适配器模式(Adapter Pattern)将各物流商的原始格式统一转换,新增物流商只需新加一个 Adapter。
  3. 材积重计算:务必记住公式:材积重(g) = 长(mm)×宽(mm)×高(mm) ÷ (材积系数×1000),这在物流成本核算中每天都会用到。