开发文档


计算器-利润计算

1. 文件概述

文件路径:src\views\amazon\report\fba_fee

文件类型:Vue 3 单文件组件(SFC)

所属模块:ERP系统 - FBA费用报告页面

功能定位:本组件是亚马逊FBA费用报告页面,用于展示和对比自测FBA参数与亚马逊FBA参数之间的差异,帮助用户了解FBA费用的计算情况及可能的超收费用。

2. 功能说明

2.1 核心功能

2.2 页面结构

2.3 操作按钮

3. 数据展示说明

3.1 表格列说明

列名 说明 详细信息
图片 产品主图 显示产品的缩略图,便于识别产品
商品信息 产品基本信息 包含产品名称、SKU,以及是否为低价产品(低价产品会显示绿色标签)
参数类型 参数来源 分为两行:自测参数(用户录入)和FBA参数(亚马逊官方)
尺寸(长高cm) 产品尺寸 分为两行:自测尺寸(用户录入)和亚马逊尺寸(亚马逊官方测量)
重量(kg) 产品重量 分为两行:自测重量(用户录入)和亚马逊重量(亚马逊官方测量)
价格 产品售价 显示产品的销售价格,包含货币符号
尺寸分段 尺寸分类 分为两行:自测尺寸分段和亚马逊尺寸分段
FBA收费(每月预计超收费用) FBA费用对比 分为两行显示自测FBA费用和亚马逊FBA费用,若有超收费用会显示红色标签
更新时间 数据更新时间 显示数据的最后更新时间,若未更新则显示"未能正常更新"

3.2 数据来源

4. 使用方法

4.1 基本操作

  1. 进入FBA费用报告页面
  2. 使用页面头部的筛选器设置查询条件
  3. 点击查询按钮加载数据
  4. 查看表格中的FBA费用对比数据
  5. 点击列配置按钮可自定义表格显示的列

4.2 数据解读

5. 注意事项

  1. 重要提示:当本地录入的产品类型与亚马逊分类不一致时,可能造成FBA费用不一样。如:Apparel/Clothing类型的产品,在美国站点每笔加收$0.4。

  2. 低价产品:标有"低价产品"标签的商品属于Small & Light计划,FBA费用计算方式不同。

  3. 数据更新:请定期检查数据更新时间,确保使用最新的FBA费用数据。

  4. 费用差异:若发现FBA费用存在较大差异,建议核对产品参数是否准确,或联系亚马逊客服确认。

6. 技术说明

6.1 API接口

6.2 工具函数

6.3 依赖组件

7. 常见问题

7.1 为什么自测参数与FBA参数不一致?

7.2 为什么会产生超收费用?

7.3 如何减少FBA费用?

更新日志


如果您有任何问题或建议,请联系系统管理员。

财务报表模板配置

财务报表模板配置帮助手册

1. 系统概述

本系统提供灵活的财务报表模板配置功能,支持用户自定义报表结构、项目计算公式和数据源,实现个性化的财务报表生成。

1.1 核心功能

2. 模板基本信息配置

2.1 模板类型

系统支持以下类型的报表模板:

2.2 基本信息配置

字段名称 说明 示例值
模板名称 报表模板的显示名称 企业标准资产负债表
模板编码 报表模板的唯一标识 BALANCE_SHEET_STANDARD
模板类型 报表的类型分类 ASSET_LIABILITY
描述 模板的详细说明 符合企业会计准则的标准资产负债表
状态 模板的启用状态 1(启用)/0(禁用)

3. 报表项目配置

3.1 项目基本信息

字段名称 说明 示例值
项目编码 报表项目的唯一标识 ASSET_CURRENT
项目名称 报表项目的显示名称 流动资产合计
行次 项目在报表中的显示顺序 10
项目级别 项目的层级关系 1(一级)/2(二级)/3(三级)
父级编码 父级项目的编码 ASSET(资产总计)
是否末级 是否为末级项目 1(是)/0(否)
状态 项目的启用状态 1(启用)/0(禁用)
是否显示 是否在报表中显示 1(显示)/0(隐藏)

3.2 层级结构配置

示例层级结构

资产总计(ASSET)
├── 流动资产合计(ASSET_CURRENT)
│   ├── 货币资金(CASH)
│   ├── 应收票据(NOTES_RECEIVABLE)
│   └── 应收账款(ACCOUNTS_RECEIVABLE)
└── 非流动资产合计(ASSET_NON_CURRENT)
    ├── 固定资产(FIXED_ASSETS)
    └── 无形资产(INTANGIBLE_ASSETS)

4. 公式配置与规则

4.1 公式类型

系统支持四种公式类型:

公式类型 说明 适用场景
DIRECT 直接取值 从科目余额或常量直接获取数据
FORMULA 公式计算 使用数学公式计算项目金额
CUSTOM 自定义规则 使用系统预定义的自定义规则计算
CALCULATED 自动计算 自动汇总子项目或其他自动计算逻辑

4.2 DIRECT公式配置

直接从数据源获取数据,支持以下数据源:

数据源类型 说明 配置方式
SUBJECT 科目余额 配置科目代码和金额类型(期末余额/借方发生额/贷方发生额)
CONSTANT 常量值 直接输入数值
CUSTOM 自定义数据源 配置自定义数据源代码

示例配置

4.3 FORMULA公式配置

使用数学公式计算项目金额,支持标准数学运算符和函数。

4.3.1 公式语法

示例公式

4.3.2 公式预处理器

系统会自动对公式进行预处理:

  1. 清理公式中的空格
  2. 将科目代码转换为系统可识别的格式(如"1001" → "ACC_1001")
  3. 验证公式语法正确性

4.4 CUSTOM公式配置

使用系统预定义的自定义规则进行计算:

自定义规则 说明 适用报表类型
ASSET_CURRENT 流动资产合计 资产负债表
ASSET_NON_CURRENT 非流动资产合计 资产负债表
LIABILITY_CURRENT 流动负债合计 资产负债表
LIABILITY_NON_CURRENT 非流动负债合计 资产负债表
EQUITY_TOTAL 所有者权益合计 资产负债表
INCOME_OPERATING 营业收入 利润表
COST_OPERATING 营业成本 利润表

4.5 CALCULATED公式配置

自动计算规则,目前支持以下类型:

计算规则 说明
SUM(CHILDREN) 自动汇总所有显示的子项目金额

示例配置

5. 数据来源配置

5.1 科目余额数据源

从会计科目余额表获取数据,支持以下金额类型:

5.2 常量数据源

直接使用固定数值作为项目金额,适用于:

5.3 自定义数据源

从系统预定义的自定义数据源获取数据,支持:

6. 报表生成与验证

6.1 报表生成流程

  1. 选择报表模板
  2. 设置报告期间
  3. 选择对比期间(可选)
  4. 点击"生成报表"按钮
  5. 系统自动计算并生成报表

6.2 报表验证

系统会自动验证报表的完整性和准确性:

7. 常见问题与解决方案

7.1 公式计算错误

问题:报表生成时提示"公式计算错误" 解决方案

  1. 检查公式中的科目代码是否正确
  2. 检查公式语法是否符合规范
  3. 确认所有引用的项目或科目都已正确配置
  4. 检查数据源是否有可用数据

7.2 项目金额显示为0

问题:报表项目显示为0,但预期有数据 解决方案

  1. 检查项目的"是否显示"设置是否为1
  2. 检查数据源是否有实际数据
  3. 检查公式配置是否正确
  4. 确认父级项目是否包含该子项目

7.3 报表层级显示错误

问题:报表项目的层级关系显示不正确 解决方案

  1. 检查项目的"项目级别"设置是否正确
  2. 检查父级编码配置是否正确
  3. 确认项目的行次设置是否符合预期顺序

8. 最佳实践

8.1 模板设计原则

  1. 模块化设计:将报表拆分为多个逻辑模块,便于维护和扩展
  2. 层级清晰:保持项目层级关系清晰,避免过深的层级结构
  3. 命名规范:使用统一的命名规范,提高模板的可读性
  4. 复用性:设计可复用的报表模板,减少重复配置工作

8.2 公式配置技巧

  1. 优先使用系统函数:如SUM(CHILDREN),减少手动维护工作量
  2. 合理使用科目代码:直接引用科目代码比引用项目更灵活
  3. 避免复杂嵌套:复杂公式拆分为多个简单公式,提高可维护性
  4. 添加注释:对复杂公式添加说明,便于后续维护

8.3 性能优化建议

  1. 减少不必要的计算:隐藏不需要显示的项目,减少计算量
  2. 合理使用缓存:启用报表缓存功能,提高报表生成速度
  3. 优化公式复杂度:避免使用过于复杂的公式,影响计算性能
  4. 定期清理:定期清理不再使用的模板和项目,保持系统整洁

9. 附录

9.1 常用科目代码参考

科目名称 科目代码
库存现金 1001
银行存款 1002
应收账款 1122
存货 1405
固定资产 1601
短期借款 2001
应付账款 2202
实收资本 4001
营业收入 6001
营业成本 6401

9.2 系统函数列表

函数名称 说明 示例
SUM() 求和函数 SUM(1001,1002,1012)
AVG() 平均值函数 AVG(1122,1131)
MAX() 最大值函数 MAX(1405,1406)
MIN() 最小值函数 MIN(2001,2201)
IF() 条件函数 IF(1001>0,1001,0)

9.3 错误代码表

错误代码 错误信息 解决方案
E001 模板不存在 检查模板ID是否正确
E002 科目代码不存在 确认科目代码是否有效
E003 公式语法错误 检查公式语法是否正确
E004 数据源无数据 确认数据源是否有可用数据
E005 计算结果溢出 检查公式是否可能产生极大值

财务报表模板自定义计算规则详解

一、概述

本文档详细列出了财务报表模板中所有自定义(CUSTOM)计算规则及其对应的具体科目代码(subjectCode),帮助用户理解和配置报表模板。

二、自定义计算规则分类

1. 资产负债表相关规则

自定义规则代码 规则名称 计算公式 对应科目代码
ASSET_CURRENT 流动资产合计 货币资金 + 应收票据 + 应收账款 + 预付款项 + 存货 + 其他流动资产 - 货币资金:1001(库存现金) + 1002(银行存款) + 1012(其他货币资金)
- 应收票据:1121
- 应收账款:1122 - 1231(坏账准备)
- 预付款项:1123
- 存货:1403(原材料) + 1405(库存商品) + 5001(生产成本) - 1471(存货跌价准备)
- 其他流动资产:140101
ASSET_CURRENT_OTHER 其他流动资产 待摊费用 + 预交税金 + 其他 - 待摊费用:1301
- 预交税金:2225
- 其他:1401
ASSET_NON_CURRENT 非流动资产合计 长期股权投资 + 固定资产 + 在建工程 + 无形资产 + 长期待摊费用 + 其他非流动资产 - 长期股权投资:1511
- 固定资产:1601(原值) - 1602(累计折旧) - 1603(减值准备)
- 在建工程:1604
- 无形资产:1701(原值) - 1702(累计摊销) - 1703(减值准备)
- 长期待摊费用:1801
- 其他非流动资产:190101
ASSET_NON_CURRENT_OTHER 其他非流动资产 长期应收款 + 递延所得税资产 + 其他 - 长期应收款:1531
- 递延所得税资产:1811
- 其他:1901
LIABILITY_CURRENT 流动负债合计 短期借款 + 应付票据 + 应付账款 + 预收款项 + 应付职工薪酬 + 应交税费 + 其他流动负债 - 短期借款:2001
- 应付票据:2201
- 应付账款:2202
- 预收款项:2203
- 应付职工薪酬:2211
- 应交税费:2221
- 其他流动负债:224101
LIABILITY_CURRENT_OTHER 其他流动负债 应付利息 + 应付股利 + 其他应付款 - 应付利息:2231
- 应付股利:2232
- 其他应付款:2241
LIABILITY_NON_CURRENT 非流动负债合计 长期借款 + 应付债券 + 其他非流动负债 - 长期借款:2501
- 应付债券:2502
- 其他非流动负债:270201
LIABILITY_NON_CURRENT_OTHER 其他非流动负债 长期应付款 + 递延所得税负债 + 其他 - 长期应付款:2701
- 递延所得税负债:2901
- 其他:2702
EQUITY_OTHER 其他所有者权益 其他权益工具 + 专项储备 + 其他综合收益 - 其他权益工具:4003
- 专项储备:4102
- 其他综合收益:4103

2. 每股收益相关规则

自定义规则代码 规则名称 计算公式 对应科目代码
EPS_BASIC 基本每股收益 归属于普通股股东的净利润 / 发行在外普通股的加权平均数 - 归属于母公司股东的净利润:利润计算
- 加权平均股数:示例值
EPS_DILUTED 稀释每股收益 调整后归属于普通股股东的净利润 / 调整后发行在外普通股的加权平均数 - 调整后净利润:利润计算 + 稀释调整
- 调整后股数:基本股数 + 稀释股份

3. 经营活动现金流相关规则

自定义规则代码 规则名称 计算公式 对应科目代码
CASH_IN_SALES 销售商品、提供劳务收到的现金 营业收入 + 应交增值税(销项) + 应收账款的减少 + 预收账款的增加 - 营业收入:6001(主营业务) + 6051(其他业务)
- 销项税额:22210102
- 应收账款减少:前期-本期(1122)
- 预收账款增加:本期-前期(2203)
CASH_IN_TAX_REFUND 收到的税费返还 所得税返还 + 增值税返还 + 其他税费返还 - 所得税返还:6802
- 增值税返还:630101
CASH_IN_OTHER_OPERATING 收到其他与经营活动有关的现金 其他应收款的减少 + 其他应付款的增加 + 其他 - 其他应收款减少:前期-本期(1221)
- 其他应付款增加:本期-前期(2241)
CASH_OUT_PURCHASE 购买商品、接受劳务支付的现金 营业成本 + 应交增值税(进项) + 存货的增加 + 应付账款的减少 + 预付账款的增加 - 营业成本:6401(主营业务) + 6402(其他业务)
- 进项税额:22210101
- 存货增加:本期-前期(存货相关)
- 应付账款减少:前期-本期(2202)
- 预付账款增加:本期-前期(1123)
CASH_OUT_SALARY 支付给职工以及为职工支付的现金 应付职工薪酬的减少 + 本期计提的职工薪酬 - 应付职工薪酬减少:前期-本期(2211)
- 计提职工薪酬:221101
CASH_OUT_TAX 支付的各项税费 所得税费用 + 应交所得税的减少 + 应交增值税的减少 + 其他税费 - 所得税费用:6801
- 应交所得税减少:前期-本期
- 应交增值税减少:前期-本期
CASH_OUT_OTHER_OPERATING 支付其他与经营活动有关的现金 其他应付款的减少 + 管理费用 + 销售费用等 - 其他应付款减少:前期-本期(2241)
- 管理费用:6602
- 销售费用:6601

4. 投资活动现金流相关规则

自定义规则代码 规则名称 对应科目代码
CASH_IN_INVEST_RECOVER 收回投资收到的现金 - 长期股权投资减少:前期-本期(1511)
- 可供出售金融资产减少:前期-本期(1503)
CASH_IN_INVEST_INCOME 取得投资收益收到的现金 - 投资收益:6111
- 应收股利减少:前期-本期(1131)
- 应收利息减少:前期-本期(1132)
CASH_IN_ASSET_DISPOSE 处置固定资产、无形资产和其他长期资产收回的现金净额 - 处置资产收入:630102
CASH_IN_OTHER_INVESTING 收到其他与投资活动有关的现金 - 其他投资收入:611101
CASH_OUT_INVEST 投资支付的现金 - 长期股权投资增加:本期-前期(1511)
- 可供出售金融资产增加:本期-前期(1503)
CASH_OUT_INVEST_PAY 支付其他与投资活动有关的现金 - 其他投资支出:611102
CASH_OUT_OTHER_INVESTING 支付其他与投资活动有关的现金 - 其他投资支出:611102

5. 筹资活动现金流相关规则

自定义规则代码 规则名称 对应科目代码
CASH_IN_INVEST_ABSORB 吸收投资收到的现金 - 实收资本增加:本期-前期(4001)
- 资本公积增加:本期-前期(4002)
CASH_IN_LOAN 取得借款收到的现金 - 短期借款增加:本期-前期(2001)
- 长期借款增加:本期-前期(2501)
CASH_IN_OTHER_FINANCING 收到其他与筹资活动有关的现金 - 其他筹资收入:630103
CASH_OUT_LOAN_REPAY 偿还债务支付的现金 - 短期借款减少:前期-本期(2001)
- 长期借款减少:前期-本期(2501)
CASH_OUT_DIVIDEND 分配股利、利润或偿付利息支付的现金 - 应付股利减少:前期-本期(2232)
- 应付利息减少:前期-本期(2231)
CASH_OUT_INTEREST 支付的利息 - 利息费用:660301
CASH_OUT_OTHER_FINANCING 支付其他与筹资活动有关的现金 - 其他筹资支出:671101

6. 所有者权益变动相关规则

自定义规则代码 规则名称 对应科目代码
BALANCE_CAPITAL_PRIOR 实收资本期初余额 - 前期实收资本:4001
BALANCE_RESERVE_CAPITAL_PRIOR 资本公积期初余额 - 前期资本公积:4002
BALANCE_RESERVE_SURPLUS_PRIOR 盈余公积期初余额 - 前期盈余公积:4101
BALANCE_RETAINED_PRIOR 未分配利润期初余额 - 前期未分配利润:4104
CHANGE_FAIR_VALUE 公允价值变动净额 - 公允价值变动损益:6101
CHANGE_DIRECT_EQUITY_OTHER 直接计入所有者权益的利得和损失 - 其他综合收益:4103
CHANGE_CAPITAL_IN 实收资本增加 - 实收资本增加:4001增加额
CHANGE_CAPITAL_REDUCE 实收资本减少 - 实收资本减少:400101
CHANGE_SURPLUS_EXTRACT 提取盈余公积 - 提取盈余公积:410101
CHANGE_DIVIDEND 向股东分配利润 - 应付股利:2232
CHANGE_CAPITAL_SURPLUS 资本公积变动 - 资本公积:4002变动额
CHANGE_INTERNAL_TRANSFER_OTHER 其他内部结转 - 内部结转:410102

三、使用说明

  1. 规则选择:在配置报表项目时,可根据需要选择合适的自定义规则代码
  2. 科目映射:系统会自动根据上述表格中的科目代码进行计算
  3. 自定义扩展:如需添加新的自定义规则,需联系技术人员进行开发
  4. 规则优先级:自定义规则的计算优先级高于普通公式

四、示例应用

示例:配置资产负债表的"流动资产合计"项目

  1. 选择公式类型:CUSTOM
  2. 输入计算规则:ASSET_CURRENT
  3. 系统会自动计算:货币资金 + 应收票据 + 应收账款 + 预付款项 + 存货 + 其他流动资产

示例:配置现金流量表的"销售商品、提供劳务收到的现金"项目

  1. 选择公式类型:CUSTOM
  2. 输入计算规则:CASH_IN_SALES
  3. 系统会自动计算:营业收入 + 销项税额 + 应收账款减少 + 预收账款增加

五、注意事项

  1. 所有科目代码遵循企业会计准则的标准科目编码
  2. 多级科目使用"."或无分隔符表示,如"22210102"表示"应交税费-应交增值税-销项税额"
  3. 部分规则涉及前期与本期数据的比较,系统会自动处理期间逻辑
  4. 如发现规则计算结果异常,请检查相关科目是否有正确的余额数据

六、更新记录

日期 版本 更新内容
2025-11-04 V1.0 初始版本,包含所有自定义计算规则

本帮助手册详细介绍了财务报表模板的配置方法和使用技巧,希望能帮助您快速掌握报表模板的配置和应用。如有其他问题,请联系系统管理员或技术支持团队。

产品-产品管理

产品-品牌管理

1. 文件概述

文件路径/src/views/erp/baseinfo/brand/index.vue

文件类型:Vue 3 单文件组件(SFC)

所属模块:ERP系统 - 基础信息管理 - 品牌管理

功能定位:该组件实现了品牌信息的增、删、改、查等核心功能,是ERP系统基础信息管理的重要组成部分。

2. 技术架构

2.1 技术栈

2.2 核心组件

主要依赖组件

2.3 API依赖

引入的API模块

import brandApi from '@/api/erp/material/brandApi.js';

使用的API方法

3. 页面结构

3.1 整体布局

<div class="main-sty">
    <!-- 顶部操作栏 -->
    <div class="con-header">...</div>
    
    <!-- 数据表格 -->
    <el-row>
        <GlobalTable>...</GlobalTable>
    </el-row>
    
    <!-- 编辑对话框 -->
    <el-dialog>...</el-dialog>
</div>

3.2 关键区域详解

3.2.1 顶部操作栏

包含以下元素:

3.2.2 数据表格

使用自定义的GlobalTable组件展示品牌信息,包含以下列:

3.2.3 编辑对话框

用于新增和编辑品牌信息,包含以下字段:

4. 核心功能

4.1 数据展示

功能描述:以表格形式展示品牌信息,支持分页和排序。

技术实现

4.2 品牌搜索

功能描述:根据品牌名称进行搜索过滤。

使用方法

  1. 在搜索框中输入品牌名称
  2. 点击搜索按钮或按Enter键
  3. 系统会过滤显示匹配的品牌信息

技术实现

4.3 新增品牌

功能描述:添加新的品牌信息。

使用方法

  1. 点击"添加品牌"按钮
  2. 在弹出的对话框中填写品牌信息
  3. 点击"提交"按钮保存

技术实现

4.4 编辑品牌

功能描述:修改现有品牌信息。

使用方法

  1. 在表格中找到要修改的品牌
  2. 点击"编辑"按钮
  3. 在弹出的对话框中修改信息
  4. 点击"提交"按钮保存

技术实现

4.5 删除品牌

功能描述:删除不需要的品牌信息。

使用方法

  1. 在表格中找到要删除的品牌
  2. 点击"删除"按钮
  3. 在确认对话框中点击"确认"按钮

技术实现

5. 数据流程

5.1 数据加载流程

  1. 组件挂载时调用onMounted(loadData)
  2. loadData()构建搜索参数并调用globalTable.value.loadTable()
  3. GlobalTable组件内部调用loadTableData()方法
  4. loadTableData()调用brandApi.list()获取品牌列表数据
  5. 数据返回后更新tableData状态,表格自动刷新

5.2 数据保存流程

  1. 用户点击"添加"或"编辑"按钮,打开对话框
  2. 用户填写表单并点击"提交"
  3. 表单验证通过后调用brandApi.saveData()
  4. API返回成功后,关闭对话框并调用loadData()刷新列表
  5. 显示操作成功的消息提示

5.3 数据删除流程

  1. 用户点击"删除"按钮,弹出确认对话框
  2. 用户点击"确认"后调用brandApi.delBrand()
  3. API返回成功后,调用loadData()刷新列表
  4. 显示删除成功的消息提示

6. 状态管理

6.1 核心状态

let state = reactive({
    tableData: {records: [], total: 0}, // 表格数据
    selectRows: [], // 选中的行数据
    searchKeywords: "", // 搜索关键词
    dialogVisible: false, // 对话框显示状态
    formData: {name: ''}, // 表单数据
    rules: {name: [{ required: true, message: '请输入供应商名称', trigger: 'blur' }]} // 表单验证规则
})

6.2 状态转换

7. 表单验证

7.1 验证规则

rules: {
    name: [{ required: true, message: '请输入供应商名称', trigger: 'blur' }]
}

7.2 验证触发

8. 用户体验

8.1 操作反馈

8.2 界面优化

9. 代码优化建议

9.1 表单验证规则修正

问题:表单验证规则中错误地使用了"供应商名称"的提示信息,应改为"品牌名称"。

建议修改

rules: {
    name: [{ required: true, message: '请输入品牌名称', trigger: 'blur' }]
}

9.2 搜索逻辑简化

问题loadData()函数中的搜索逻辑可以简化。

建议修改

function loadData() {
    var data = {
        search: state.searchKeywords || ""
    };
    globalTable.value.loadTable(data);
}

9.3 变量命名统一

问题:API调用返回的处理函数中,变量命名不一致。

建议修改

brandApi.delBrand({"id": rows.id.toString()}).then((response) => {
    ElMessage.success('删除成功');
    loadData();
});

9.4 注释完善

建议:为关键函数和复杂逻辑添加详细注释,提升代码可维护性。

10. 使用说明

10.1 基本操作流程

  1. 查看品牌列表:进入页面后自动加载所有品牌信息
  2. 搜索品牌:在搜索框中输入名称,点击搜索按钮
  3. 新增品牌:点击"添加品牌"按钮,填写信息后提交
  4. 编辑品牌:找到要修改的品牌,点击"编辑"按钮,修改后提交
  5. 删除品牌:找到要删除的品牌,点击"删除"按钮,确认后删除

10.2 注意事项

11. 总结

品牌管理页面是ERP系统基础信息管理的重要模块,实现了品牌信息的完整生命周期管理。页面采用Vue 3 Composition API开发,使用Element Plus组件库构建用户界面,通过RESTful API与后端进行数据交互。

页面具有良好的用户体验,包括直观的操作界面、及时的操作反馈和完善的表单验证。代码结构清晰,功能模块化,便于维护和扩展。

通过合理的状态管理和数据流程设计,页面实现了高效的数据加载和更新,为用户提供了流畅的操作体验。

发货-询价-设置

物流报价设置页面详细帮助文档

1. 页面概述

setting.vue 是Wimoor ERP系统中物流模块下的报价设置页面,主要用于管理询价商和物流供应商信息。该页面实现了询价商令牌绑定、询价商信息管理以及物流供应商的增删改查功能,是物流报价流程的基础设置页面。

页面路径wimoor-ui/src/views/erp/shipv2/quote/setting.vue 技术栈:Vue 3 + Element Plus + Spring Boot + MyBatis Plus

2. 前端功能模块详解

2.1 页面布局

页面采用左右分栏布局:

2.2 询价商管理模块

2.2.1 状态显示与令牌管理

<div v-if="token" style="padding-bottom:20px">
  <el-descriptions :column="1" border >
    <el-descriptions-item label="状态"><el-tag type="success">已绑定</el-tag> </el-descriptions-item>
    <el-descriptions-item label="令牌">  <span v-if="tokenname">({{tokenname}})</span> {{token}} 
              <copy class="" @click.stop="CopyText(token)" title='复制SKU' theme="outline" size="14" fill="#666" :strokeWidth="3"/> </el-descriptions-item>
    <el-descriptions-item label="操作">  <el-button @click="unbindToken" link type="primary" >解绑</el-button></el-descriptions-item>
  </el-descriptions>
</div>
<div v-else style="padding-bottom:20px">
  <el-descriptions :column="1" border >
    <el-descriptions-item label="状态"> <el-tag type="danger">未绑定</el-tag> </el-descriptions-item>
    <el-descriptions-item label="令牌"> <el-input v-model="edittoken" placeholder="填写询价商Token"></el-input> </el-descriptions-item>
    <el-descriptions-item label="别名"> <el-input v-model="editname" placeholder="填写别名"></el-input> </el-descriptions-item>
    <el-descriptions-item label="操作"> <el-button @click="bindToken" type="primary" >绑定</el-button></el-descriptions-item>
  </el-descriptions>
</div>

功能说明

2.2.2 询价商信息设置

<el-collapse v-if="token" v-model="activeNames" @change="handleChange">
  <el-collapse-item title="高级" name="1">
    <el-form-item label="名称">
      <el-input v-model="buyer.name" :disabled="!buyer.edit" placeholder="填写供应商名称"></el-input>
    </el-form-item>
    <el-form-item label="地址">
      <el-input v-model="buyer.address" :disabled="!buyer.edit" placeholder="填写地址信息"></el-input>
    </el-form-item>
    <el-form-item label="手机号">
      <el-input v-model="buyer.mobile" :disabled="!buyer.edit" placeholder="填写手机号"></el-input>
    </el-form-item>
    <el-form-item label="联系人">
      <el-input v-model="buyer.contact" :disabled="!buyer.edit" placeholder="填写联系人"></el-input>
    </el-form-item>
    <div style=" margin-bottom:20px">
      <div v-if="token" >
        <el-button v-if="!buyer.edit" @click="buyer.edit=true" type="primary">修改</el-button>
        <el-button v-else @click="addBuyer" type="primary">保存</el-button>
      </div>
      <div v-else>
        <el-button @click="addBuyer" type="primary">新增</el-button>
      </div>
    </div>
  </el-collapse-item>
</el-collapse>

功能说明

2.3 物流供应商管理模块

2.3.1 供应商列表

<el-table :data="tableData" height="calc(100vh - 145px)" >
  <el-table-column prop="name" label="名称" >
    <template #default="scope">
      <div>{{scope.row.name}}</div>
      <div class="font-extraSmall">{{scope.row.address}}</div>
    </template>
  </el-table-column>
  <el-table-column prop="contact" label="联系人" width="230" >
    <template #default="scope">
      <div>{{scope.row.contact}}</div>
      <div>{{scope.row.mobile}}</div>
    </template>
  </el-table-column>
  <el-table-column prop="createtime" label="链接" width="240" v-if="isowner" v-hasPerm="'erp:pi:supplier:link'" >
    <template #default="scope">
      <el-link type="success" :href="urlFormat(scope.row)" target="_blank" > <el-icon><Link /></el-icon> 供应商页面</el-link>
      <copy style="padding-left:10px" @click.stop="CopyText(urlFormat(scope.row))" title='复制SKU' theme="outline" size="14" fill="#666" :strokeWidth="3"/>
    </template>
  </el-table-column>
  <el-table-column prop="createtime" label="创建时间" width="200" >
    <template #default="scope">
      {{dateTimesFormat(scope.row.createtime)}} 
    </template>
  </el-table-column>
  <el-table-column prop="createtime" label="操作" width="180" >
    <template #default="scope">
      <el-button @click="editSupplier(scope.row)">编辑</el-button>
      <el-button type="danger" @click="delSupplier(scope.row.id)">删除</el-button>
    </template>
  </el-table-column>
</el-table>

功能说明

2.3.2 供应商操作

<template #header>
  <div class="card-header flex-between">
    <el-button @click="handleShow">新增</el-button>
    <div>物流供应商管理</div>
  </div>
</template>

功能说明

3. 后端数据模型

3.1 询价商数据模型(UserBuyer)

表名t_user_buyer

@Data
@ApiModel(value="t_user_buyer对象", description="买家")
@EqualsAndHashCode(callSuper = true)
@TableName("t_user_buyer")
public class UserBuyer extends BaseEntity{
    @ApiModelProperty(value = "名称")
    @TableField(value = "name")
    private String name;

    @ApiModelProperty(value = "公司id")
    @TableField(value = "company")
    private String company;

    @ApiModelProperty(value = "地址")
    @TableField(value = "address")
    private String address;

    @ApiModelProperty(value = "联系人")
    @TableField(value = "contact")
    private String contact;

    @ApiModelProperty(value = "手机号")
    @TableField(value = "mobile")
    private String mobile;

    @ApiModelProperty(value = "token")
    @TableField(value = "token")
    private String token;

    @ApiModelProperty(value = "授权时间")
    @TableField(value = "tokentime")
    private Date tokentime;

    @ApiModelProperty(value = "创建时间")
    @TableField(value = "createtime")
    private Date createtime;
}

3.2 供应商数据模型(UserSupplier)

表名t_user_supplier

@Data
@ApiModel(value="UserSupplier对象", description="供应商")
@EqualsAndHashCode(callSuper = true)
@TableName("t_user_supplier")
public class UserSupplier extends BaseEntity{
    @ApiModelProperty(value = "名称")
    @TableField(value = "name")
    private String name;

    @ApiModelProperty(value = "buyerid")
    @TableField(value = "buyerid")
    private String buyerid;

    @ApiModelProperty(value = "地址")
    @TableField(value = "address")
    private String address;

    @ApiModelProperty(value = "联系人")
    @TableField(value = "contact")
    private String contact;

    @ApiModelProperty(value = "手机号")
    @TableField(value = "mobile")
    private String mobile;

    @ApiModelProperty(value = "token")
    @TableField(value = "token")
    private String token;

    @ApiModelProperty(value = "password")
    @TableField(value = "password")
    private String password;

    @ApiModelProperty(value = "授权时间")
    @TableField(value = "tokentime")
    private Date tokentime;

    @ApiModelProperty(value = "创建时间")
    @TableField(value = "createtime")
    private Date createtime;

    @ApiModelProperty(value = "停用")
    @TableField(value = "disabled")
    private Boolean disabled;
}

4. API接口分析

4.1 前端API调用

4.1.1 询价商相关API

功能 调用方法 API路径 所属文件
获取询价商信息 orderApi.getBuyer({"token":state.token}) /quote/api/v1/quote/getBuyer orderApi.js
添加/更新询价商 orderApi.addBuyer(state.buyer) /quote/api/v1/quote/addBuyer orderApi.js
获取报价令牌 thirdpartyApi.getQuoteToken() /erp/api/v1/thirdparty/getQuoteToken thirdpartyApi.js
保存报价令牌 thirdpartyApi.saveQuoteToken(data) /erp/api/v1/thirdparty/saveQuoteToken thirdpartyApi.js
解绑报价令牌 thirdpartyApi.unbindQuoteToken() /erp/api/v1/thirdparty/unbindQuoteToken thirdpartyApi.js

4.1.2 供应商相关API

功能 调用方法 API路径 所属文件
获取供应商列表 supplierApi.listsupplier(datas) /quote/api/v1/quote/supplier/listSupplier supplierApi.js
删除供应商 supplierApi.deletesupplier({"id":id}) /quote/api/v1/quote/supplier/deleteSupplier supplierApi.js
添加供应商 supplierApi.addsupplier(token,data) /quote/api/v1/quote/supplier/addSupplier/{token} supplierApi.js
更新供应商 supplierApi.updatesupplier(data) /quote/api/v1/quote/supplier/updateSupplier supplierApi.js

4.2 后端API实现

4.2.1 询价商API实现

// UserBuyerController.java (示例)
@RestController
@RequestMapping("/quote/api/v1/quote")
public class UserBuyerController {
    
    @Autowired
    private IUserBuyerService userBuyerService;
    
    @GetMapping("/getBuyer")
    public Result<UserBuyer> getBuyer(@RequestParam String token) {
        UserBuyer buyer = userBuyerService.getByToken(token);
        return Result.success(buyer);
    }
    
    @PostMapping("/addBuyer")
    public Result<String> addBuyer(@RequestBody UserBuyer buyer) {
        String token = userBuyerService.saveOrUpdateBuyer(buyer);
        return Result.success(token);
    }
}

4.2.2 第三方API实现

// ThirdPartyController.java (示例)
@RestController
@RequestMapping("/erp/api/v1/thirdparty")
public class ThirdPartyController {
    
    @GetMapping("/getQuoteToken")
    public Result<Map<String, Object>> getQuoteToken() {
        // 从当前用户获取绑定的令牌信息
        Map<String, Object> tokenInfo = new HashMap<>();
        // ... 实现逻辑
        return Result.success(tokenInfo);
    }
    
    @PostMapping("/saveQuoteToken")
    public Result<String> saveQuoteToken(@RequestBody Map<String, Object> data) {
        // 保存令牌信息到当前用户
        // ... 实现逻辑
        return Result.success();
    }
    
    @GetMapping("/unbindQuoteToken")
    public Result<String> unbindQuoteToken() {
        // 解绑当前用户的令牌
        // ... 实现逻辑
        return Result.success();
    }
}

4.2.3 供应商API实现

// SupplierManagerController.java (示例)
@RestController
@RequestMapping("/quote/api/v1/quote/supplier")
public class SupplierManagerController {
    
    @Autowired
    private IUserSupplierService userSupplierService;
    
    @PostMapping("/listSupplier")
    public Result<List<UserSupplier>> listSupplier(@RequestBody Map<String, Object> data) {
        String token = (String) data.get("token");
        List<UserSupplier> suppliers = userSupplierService.listByBuyerToken(token);
        return Result.success(suppliers);
    }
    
    @DeleteMapping("/deleteSupplier")
    public Result<String> deleteSupplier(@RequestParam String id) {
        userSupplierService.removeById(id);
        return Result.success();
    }
    
    @PostMapping("/addSupplier/{token}")
    public Result<String> addSupplier(@PathVariable String token, @RequestBody UserSupplier supplier) {
        supplier.setBuyerid(token);
        userSupplierService.save(supplier);
        return Result.success();
    }
    
    @PutMapping("/updateSupplier")
    public Result<String> updateSupplier(@RequestBody UserSupplier supplier) {
        userSupplierService.updateById(supplier);
        return Result.success();
    }
}

5. 业务流程解析

5.1 页面初始化流程

  1. 页面加载时调用 onMounted(() => loadToken())
  2. loadToken() 调用 thirdpartyApi.getQuoteToken() 获取当前用户绑定的令牌信息
  3. 如果令牌存在,调用 loadBuyer()loadSupplier() 分别加载询价商和供应商数据
  4. 渲染页面,显示已绑定或未绑定状态

5.2 令牌绑定流程

  1. 用户输入令牌和别名
  2. 点击"绑定"按钮,调用 bindToken() 方法
  3. bindToken() 先调用 orderApi.getBuyer() 验证令牌有效性
  4. 如果验证成功,调用 thirdpartyApi.saveQuoteToken() 保存令牌信息
  5. 保存成功后,加载询价商和供应商数据,更新页面状态

5.3 供应商管理流程

5.3.1 添加供应商

  1. 点击"新增"按钮,调用 handleShow() 方法
  2. 打开 CreateDialog 组件对话框
  3. 用户填写供应商信息并点击保存
  4. 调用 supplierApi.addsupplier() 保存供应商信息
  5. 保存成功后,刷新供应商列表

5.3.2 编辑供应商

  1. 点击供应商列表中的"编辑"按钮,调用 editSupplier() 方法
  2. 打开 CreateDialog 组件对话框并填充现有数据
  3. 用户修改信息并点击保存
  4. 调用 supplierApi.updatesupplier() 更新供应商信息
  5. 更新成功后,刷新供应商列表

5.3.3 删除供应商

  1. 点击供应商列表中的"删除"按钮,调用 delSupplier() 方法
  2. 弹出确认对话框
  3. 用户确认后,调用 supplierApi.deletesupplier() 删除供应商
  4. 删除成功后,刷新供应商列表

6. 操作指南

6.1 绑定询价商令牌

  1. 在左侧"询价商管理"区域,找到"令牌"输入框
  2. 输入从询价商获取的有效令牌
  3. 输入别名(可选)
  4. 点击"绑定"按钮
  5. 系统验证令牌有效性并完成绑定
  6. 绑定成功后,页面显示已绑定状态和令牌信息

6.2 设置询价商详细信息

  1. 确保已成功绑定询价商令牌
  2. 找到"高级"折叠面板并点击展开
  3. 点击"修改"按钮进入编辑模式
  4. 填写或修改以下信息:
    • 名称:询价商公司名称
    • 地址:询价商公司地址
    • 手机号:联系电话
    • 联系人:对接人姓名
  5. 点击"保存"按钮完成设置

6.3 新增物流供应商

  1. 确保已成功绑定询价商令牌
  2. 在右侧"物流供应商管理"区域,点击"新增"按钮
  3. 在弹出的对话框中填写供应商信息:
    • 名称:物流供应商名称
    • 地址:供应商地址
    • 联系人:对接人姓名
    • 手机号:联系电话
  4. 点击"保存"按钮完成新增
  5. 新增成功后,供应商列表会自动刷新

6.4 编辑物流供应商

  1. 在供应商列表中找到需要修改的供应商
  2. 点击该供应商右侧的"编辑"按钮
  3. 在弹出的对话框中修改供应商信息
  4. 点击"保存"按钮完成修改
  5. 修改成功后,供应商列表会自动刷新

6.5 删除物流供应商

  1. 在供应商列表中找到需要删除的供应商
  2. 点击该供应商右侧的"删除"按钮
  3. 在弹出的确认对话框中点击"确定"按钮
  4. 删除成功后,供应商列表会自动刷新

7. 常见问题解答

7.1 绑定令牌失败

问题现象:点击"绑定"按钮后,系统提示"绑定失败,未找到询价商"

可能原因

解决方案

  1. 检查令牌是否正确输入(注意大小写和空格)
  2. 联系询价商确认令牌有效性
  3. 检查网络连接
  4. 稍后重试

7.2 无法看到供应商列表

问题现象:绑定令牌后,右侧供应商列表为空

可能原因

解决方案

  1. 点击"新增"按钮添加物流供应商
  2. 检查网络连接
  3. 确认当前用户有查看供应商列表的权限

7.3 无法删除供应商

问题现象:点击"删除"按钮后,系统提示删除失败

可能原因

解决方案

  1. 确认该供应商没有关联的报价订单
  2. 检查当前用户是否有删除权限
  3. 检查网络连接

7.4 无法访问供应商页面

问题现象:点击供应商页面链接后无法访问

可能原因

解决方案

  1. 确认当前用户是询价商所有者
  2. 联系系统管理员获取访问权限
  3. 尝试手动复制URL并在新窗口打开

8. 代码优化建议

8.1 前端代码优化

8.1.1 状态管理优化

当前代码使用 reactive 创建状态对象,然后使用 toRefs 解构。建议使用 ref 直接创建响应式变量,使代码更简洁:

// 优化前
const state = reactive({
    edittoken:"",
    editname:"",
    supplier:{},
    tableData:[],
    token:"",
    tokenname:"",
    title:'新增',
    isowner:false,
    buyer:{'edit':true},
})
const{ token,buyer,edittoken,editname,supplier,tableData,title,isowner,tokenname }=toRefs(state);

// 优化后
const edittoken = ref("");
const editname = ref("");
const supplier = ref({});
const tableData = ref([]);
const token = ref("");
const tokenname = ref("");
const title = ref('新增');
const isowner = ref(false);
const buyer = ref({ edit: true });

8.1.2 错误处理优化

当前代码在API调用失败时只显示简单的错误信息,建议添加更详细的错误处理和用户提示:

// 优化前
orderApi.addBuyer(state.buyer).then(res=>{
    if(res.data){
        state.edittoken=res.data;
        bindToken("isowner");
    }else{
        ElMessage.error("操作失败");
        state.tableData=[];
    }
})

// 优化后
orderApi.addBuyer(state.buyer).then(res=>{
    if(res.data){
        state.edittoken=res.data;
        bindToken("isowner");
    }else{
        ElMessage.error(res.message || "操作失败");
        state.tableData=[];
    }
}).catch(error => {
    console.error("添加询价商失败:", error);
    ElMessage.error("网络错误,请稍后重试");
})

8.2 后端代码优化

8.2.1 API参数验证

建议在后端API中添加参数验证,确保数据完整性和安全性:

@PostMapping("/addBuyer")
public Result<String> addBuyer(@Valid @RequestBody UserBuyer buyer) {
    // 实现逻辑
}

// 在UserBuyer实体类中添加验证注解
@Data
public class UserBuyer {
    @NotBlank(message = "名称不能为空")
    private String name;
    
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误")
    private String mobile;
    
    // 其他字段...
}

8.2.2 事务管理

在涉及多个数据库操作的业务逻辑中添加事务管理,确保数据一致性:

@Service
public class UserSupplierServiceImpl implements IUserSupplierService {
    
    @Autowired
    private UserSupplierMapper userSupplierMapper;
    
    @Autowired
    private QuoteOrderSupplierMapper quoteOrderSupplierMapper;
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean removeSupplier(String id) {
        // 删除供应商
        boolean result = userSupplierMapper.deleteById(id) > 0;
        
        // 同时删除相关的报价订单关联
        quoteOrderSupplierMapper.deleteBySupplierId(id);
        
        return result;
    }
}

9. 总结

物流报价设置页面是Wimoor ERP系统中物流报价流程的基础设置页面,实现了询价商令牌绑定、询价商信息管理以及物流供应商的增删改查功能。通过该页面,用户可以建立与询价商的连接,并管理物流供应商信息,为后续的物流报价流程奠定基础。

该页面采用了清晰的左右分栏布局,使用Vue 3和Element Plus构建了友好的用户界面,后端使用Spring Boot和MyBatis Plus实现了高效的数据处理。页面的业务逻辑设计合理,流程清晰,能够满足用户的日常操作需求。

财务-报告

财务报表系统开发文档

1. 系统概述

财务报表系统是一个用于管理财务报表模板、配置报表项目、生成财务报表的完整解决方案。该系统支持多种报表类型(如资产负债表、利润表等),提供灵活的项目配置和计算公式功能,能够自动生成准确的财务报表数据。

2. 系统架构

2.1 前端架构

目录结构

wimoor-ui/src/views/finance/report/
├── items/                # 报表项目展示模块
│   ├── components/
│   │   └── report_sheet.vue  # 报表展示组件
│   └── index.vue              # 报表项目主页面
└── templates/            # 报表模板管理模块
    ├── components/
    │   ├── template_item_edit.vue  # 模板项目编辑组件
    │   └── template_items.vue      # 模板项目管理组件
    └── index.vue                   # 模板管理主页面

核心组件

2.2 后端架构

核心控制器

核心服务

数据传输对象

3. 功能模块

3.1 报表模板管理

功能说明

核心代码

<!-- 模板树结构 -->
<el-tree
  class="template-tree"
  :data="templateTree"
  :highlight-current="true"
  :props="treeProps"
  node-key="templateId"
  @node-click="handleTemplateSelect"
>
  <!-- 自定义节点内容 -->
</el-tree>

<!-- 模板新增/修改对话框 -->
<el-dialog :title="formTitle" v-model="openForm" width="600px" append-to-body>
  <el-form ref="templateFormRef" :model="form" :rules="rules" label-width="120px">
    <el-form-item label="模板名称" prop="templateName">
      <el-input v-model="form.templateName" placeholder="请输入模板名称"></el-input>
    </el-form-item>
    <!-- 其他表单字段 -->
  </el-form>
</el-dialog>

3.2 报表项目管理

功能说明

核心代码

<!-- 项目列表 -->
<el-table
    v-loading="loading"
    :data="itemsList"
    border
    size="small"
    :row-style="setRowStyle"
>
  <el-table-column prop="lineNumber" label="行次" width="60" align="center"></el-table-column>
  <el-table-column prop="itemCode" label="项目编码" width="300"></el-table-column>
  <el-table-column label="项目名称">
    <template #default="{ row }">
      <span :style="{ 'padding-left': (row.itemLevel - 1) * 20 + 'px' }">{{ row.itemName }}</span>
    </template>
  </el-table-column>
  <!-- 其他列 -->
</el-table>

3.3 报表生成

功能说明

核心代码

public ReportGenerateResponse generateReport(ReportGenerateRequest request) {
    ReportGenerateResponse response = new ReportGenerateResponse();

    try {
        String groupid = request.getGroupid();
        String templateCode = request.getTemplateCode();
        String period = request.getPeriod();
        
        // 1. 获取报表模板
        List<FinReportTemplates> templates = this.finReportTemplatesMapper.selectFinReportTemplatesList(finReportTemplates);
        
        // 2. 获取报表项目结构
        List<FinReportItems> reportItems = finReportItemsService.selectFinReportItemsList(itemsquery);
        
        // 3. 计算当前期间数据
        Map<String, BigDecimal> currentAmounts = calculateReportAmounts(groupid, period, reportItems, template);
        
        // 4. 计算比较期间数据(如果需要)
        Map<String, BigDecimal> compareAmounts = new HashMap<>();
        if (request.getIncludeComparison() && comparePeriod != null && !comparePeriod.isEmpty()) {
            compareAmounts = calculateReportAmounts(groupid, comparePeriod, reportItems, template);
        }
        
        // 5. 构建响应数据
        List<ReportItemValueDTO> itemDTOs = buildReportItems(reportItems, currentAmounts, compareAmounts, amountUnit);
        
        // 6. 计算财务指标、图表数据等
        
        // 7. 组装响应
        response.setItems(itemDTOs);
        response.setFinancialRatios(financialRatios);
        response.setChartData(chartData);
        response.setSummary(summary);
        
    } catch (Exception e) {
        response.setSuccess(false);
        response.setMessage("报表生成失败: " + e.getMessage());
    }

    return response;
}

4. 核心技术实现

4.1 报表项目金额计算

功能说明

核心代码

private Map<String, BigDecimal> calculateReportAmounts(String groupid, String period,
                                                       List<FinReportItems> reportItems,
                                                       FinReportTemplates template) {
    // 获取科目余额数据
    Map<String, BigDecimal> amounts = subjectBalanceService.getAllSubjectBalance(groupid, period);

    // 按层级从高到低排序计算,确保父级项目在子级之后计算
    List<FinReportItems> sortedItems = reportItems.stream()
            .sorted(Comparator.comparing(FinReportItems::getItemLevel).reversed())
            .collect(Collectors.toList());

    for (FinReportItems item : sortedItems) {
        if (!item.getIsShow()) {
            continue; // 跳过不显示的项目
        }

        BigDecimal amount = calculateItemAmount(groupid, period, item, amounts, reportItems);
        amounts.put(item.getItemCode(), amount);
    }

    return amounts;
}

private BigDecimal calculateItemAmount(String groupid, String period, FinReportItems item,
                                       Map<String, BigDecimal> amounts, List<FinReportItems> allItems) {
    String formulaType = item.getFormulaType();
    String calculationRule = item.getCalculationRule();
    String dataSource = item.getDataSource();

    try {
        switch (formulaType) {
            case "DIRECT":
                return calculateDirectAmount(groupid, period, item, dataSource);
            case "FORMULA":
                return calculateFormulaAmount(calculationRule, amounts, item.getFormulaContent());
            case "CUSTOM":
                return calculateCustomAmount(groupid, period, calculationRule, amounts, allItems);
            case "CALCULATED":
                return calculateAutoAmount(item, amounts, allItems);
            default:
                return BigDecimal.ZERO;
        }
    } catch (Exception e) {
        System.err.println("计算项目失败: " + item.getItemCode() + " - " + e.getMessage());
        return BigDecimal.ZERO;
    }
}

4.2 公式解析与计算

功能说明

核心代码

private BigDecimal evaluateFormula(String formula, Map<String, BigDecimal> amounts) {
    try {
        // 预处理公式
        String processedFormula = processAccountFormula(formula);
        
        // 准备环境变量
        Map<String, Object> env = prepareEnvironment(amounts);
        
        // 执行计算
        AviatorEvaluator.setOption(Options.ALWAYS_USE_DOUBLE_AS_DECIMAL, false);
        Object result = AviatorEvaluator.execute(processedFormula, env);
        
        return convertToBigDecimal(result);
    } catch (Exception e) {
        throw new RuntimeException("公式计算错误: " + formula, e);
    }
}

private String processAccountFormula(String formula) {
    // 清理空格
    String cleaned = formula.replaceAll("\\s+", "");
    
    // 使用正则匹配科目代码(4位数字),并添加前缀
    Pattern pattern = Pattern.compile("\\d{4,}"); // 匹配4位及以上数字
    Matcher matcher = pattern.matcher(cleaned);
    StringBuffer sb = new StringBuffer();
    
    while (matcher.find()) {
        String accountCode = matcher.group();
        matcher.appendReplacement(sb, "ACC_" + accountCode);
    }
    matcher.appendTail(sb);
    
    return sb.toString();
}

5. 接口说明

5.1 报表生成接口

接口地址/api/report/generate

请求方法:POST

请求参数

{
  "templateCode": "BALANCE_SHEET_STANDARD",
  "period": "202312",
  "comparePeriod": "202311",
  "groupid": "123456",
  "amountUnit": 1,
  "includeComparison": true,
  "includeChartData": true
}

响应参数

{
  "success": true,
  "message": "报表生成成功",
  "templateCode": "BALANCE_SHEET_STANDARD",
  "templateName": "标准资产负债表",
  "period": "202312",
  "reportDate": "2023-12-31",
  "items": [
    {
      "itemCode": "ASSET_TOTAL",
      "itemName": "资产总计",
      "itemLevel": 1,
      "lineNumber": 1,
      "amount": 1000000.00,
      "comparisonAmount": 950000.00,
      "changeAmount": 50000.00,
      "changeRate": 5.26,
      "isLeaf": false
    }
  ],
  "financialRatios": {},
  "chartData": {},
  "summary": {}
}

5.2 报表项目列表接口

接口地址/report/items/list

请求方法:GET

请求参数

响应参数

{
  "data": [
    {
      "itemId": 1,
      "templateId": 1,
      "itemCode": "ASSET_TOTAL",
      "itemName": "资产总计",
      "parentCode": "",
      "itemLevel": 1,
      "lineNumber": 1,
      "formulaType": "CALCULATED",
      "calculationRule": "SUM(CHILDREN)",
      "isShow": true,
      "isLeaf": false
    }
  ]
}

6. 技术栈

6.1 前端技术

6.2 后端技术

7. 系统流程

7.1 报表生成流程

  1. 用户在前端选择报表模板和期间
  2. 前端调用报表生成接口(/api/report/generate
  3. 后端获取报表模板和项目结构
  4. 后端计算当前期间和比较期间的项目金额
  5. 后端生成财务指标和图表数据
  6. 后端将结果返回给前端
  7. 前端展示报表数据和图表

7.2 模板项目配置流程

  1. 用户在模板管理页面选择一个模板
  2. 进入模板项目管理页面
  3. 点击"新增项目"按钮,填写项目信息
  4. 配置项目的计算公式和数据来源
  5. 保存项目配置
  6. 可以对项目进行验证,确保计算公式正确

8. 总结

财务报表系统是一个功能完整、灵活可扩展的财务报表解决方案。该系统通过前端提供友好的用户界面,后端提供强大的计算能力,能够满足企业对财务报表的各种需求。系统支持多种报表类型、灵活的项目配置和复杂的计算公式,是企业财务管理的重要工具。

报表模板算法解析说明

1. 核心计算流程算法

1.1 报表金额计算主流程 (calculateReportAmounts)

private Map<String, BigDecimal> calculateReportAmounts(String groupid, String period,
                                                       List<FinReportItems> reportItems,
                                                       FinReportTemplates template) {
    // 缓存机制(目前注释掉)
    String cacheKey = groupid + "_" + template.getTemplateCode() + "_" + period;
    
    // 1. 获取所有科目余额作为基础数据
    Map<String, BigDecimal> amounts = subjectBalanceService.getAllSubjectBalance(groupid, period);

    // 2. 关键排序:按层级从高到低排序,确保父级项目在子级之后计算
    List<FinReportItems> sortedItems = reportItems.stream()
            .sorted(Comparator.comparing(FinReportItems::getItemLevel).reversed())
            .collect(Collectors.toList());

    // 3. 逐个计算项目金额
    for (FinReportItems item : sortedItems) {
        if (!item.getIsShow()) continue;
        
        BigDecimal amount = calculateItemAmount(groupid, period, item, amounts, reportItems);
        amounts.put(item.getItemCode(), amount);
    }
    
    // 4. 结果缓存
    calculationCache.put(cacheKey, amounts);
    return amounts;
}

算法特点

1.2 项目金额计算策略 (calculateItemAmount)

private BigDecimal calculateItemAmount(String groupid, String period, FinReportItems item,
                                      Map<String, BigDecimal> amounts, List<FinReportItems> allItems) {
    String formulaType = item.getFormulaType();
    String calculationRule = item.getCalculationRule();
    String dataSource = item.getDataSource();

    try {
        switch (formulaType) {
            case "DIRECT":    // 直接取值
                return calculateDirectAmount(groupid, period, item, dataSource);
            case "FORMULA":   // 公式计算
                return calculateFormulaAmount(calculationRule, amounts, item.getFormulaContent());
            case "CUSTOM":    // 自定义规则
                return calculateCustomAmount(groupid, period, calculationRule, amounts, allItems);
            case "CALCULATED":// 自动计算
                return calculateAutoAmount(item, amounts, allItems);
            default:
                return BigDecimal.ZERO;
        }
    } catch (Exception e) {
        System.err.println("计算项目失败: " + item.getItemCode() + " - " + e.getMessage());
        return BigDecimal.ZERO;
    }
}

算法特点

2. 公式解析与计算算法

2.1 公式解析 (processAccountFormula)

private String processAccountFormula(String formula) {
    // 1. 清理空格
    String cleaned = formula.replaceAll("\\s+", "");

    // 2. 正则匹配科目代码(4位及以上数字)并添加前缀
    Pattern pattern = Pattern.compile("\\d{4,}");
    Matcher matcher = pattern.matcher(cleaned);
    StringBuffer sb = new StringBuffer();

    while (matcher.find()) {
        String accountCode = matcher.group();
        matcher.appendReplacement(sb, "ACC_" + accountCode);
    }
    matcher.appendTail(sb);

    return sb.toString();
}

算法特点

2.2 公式计算 (evaluateFormula)

private BigDecimal evaluateFormula(String formula, Map<String, BigDecimal> amounts) {
    try {
        // 1. 公式预处理
        String processedFormula = processAccountFormula(formula);
        
        // 2. 准备计算环境
        Map<String, Object> env = prepareEnvironment(amounts);
        
        // 3. 执行计算(禁用double作为小数类型,确保BigDecimal精度)
        AviatorEvaluator.setOption(Options.ALWAYS_USE_DOUBLE_AS_DECIMAL, false);
        Object result = AviatorEvaluator.execute(processedFormula, env);
        
        // 4. 结果转换
        return convertToBigDecimal(result);
    } catch (Exception e) {
        throw new RuntimeException("公式计算错误: " + formula, e);
    }
}

算法特点

2.3 类型转换 (convertToBigDecimal)

private BigDecimal convertToBigDecimal(Object value) {
    if (value instanceof BigDecimal) {
        return (BigDecimal) value;
    } else if (value instanceof Number) {
        return new BigDecimal(value.toString());
    } else {
        throw new RuntimeException("无法转换的类型: " + value.getClass());
    }
}

算法特点

3. 数据获取与汇总算法

3.1 直接取值计算 (calculateDirectAmount)

private BigDecimal calculateDirectAmount(String groupid, String period, FinReportItems item, String dataSource) {
    switch (dataSource) {
        case "SUBJECT":    // 科目余额
            if (item.getSubjectCodes() != null && !item.getSubjectCodes().isEmpty()) {
                return getSubjectBalance(groupid, period, item.getSubjectCodes(), item.getAmountType());
            }
            break;
            
        case "CONSTANT":   // 常量值
            if (item.getCalculationRule() != null) {
                try {
                    return new BigDecimal(item.getCalculationRule());
                } catch (NumberFormatException e) {
                    return BigDecimal.ZERO;
                }
            }
            break;
            
        case "CUSTOM":     // 自定义数据源
            return getCustomAmount(groupid, period, item.getCalculationRule());
    }
    return BigDecimal.ZERO;
}

算法特点

3.2 子项目汇总 (sumChildrenAmounts)

private BigDecimal sumChildrenAmounts(String parentCode, List<FinReportItems> allItems, Map<String, BigDecimal> amounts) {
    return allItems.stream()
            .filter(item -> parentCode.equals(item.getParentCode()) && item.getIsShow())
            .map(item -> amounts.getOrDefault(item.getItemCode(), BigDecimal.ZERO))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
}

算法特点

4. 算法优化与设计亮点

4.1 计算顺序优化

问题:父级项目通常依赖子级项目的计算结果
解决方案:按项目层级从高到低排序计算,确保父级项目在所有子级项目计算完成后执行

4.2 精度控制机制

问题:财务计算对精度要求极高,浮点数计算易产生误差
解决方案

4.3 缓存机制

问题:相同条件下重复生成报表会造成性能浪费
解决方案:实现基于groupid、templateCode和period的缓存机制,避免重复计算(目前代码中已实现但注释掉)

4.4 容错设计

问题:单个项目计算失败不应影响整个报表生成
解决方案

5. 应用场景与示例

5.1 资产负债表计算示例

1. 计算所有末级科目余额(如1001库存现金、1002银行存款等)
2. 按层级倒序计算:
   - 先计算"货币资金"(1001+1002)
   - 再计算"流动资产合计"(货币资金+应收票据+应收账款+...)
   - 最后计算"资产总计"(流动资产合计+非流动资产合计)
3. 负债和所有者权益项目同样按层级倒序计算
4. 最终验证"资产总计=负债合计+所有者权益合计"

5.2 利润表计算示例

1. 计算主营业务收入、主营业务成本等末级科目
2. 计算营业利润(营业收入-营业成本-税金及附加-...)
3. 计算利润总额(营业利润+营业外收入-营业外支出)
4. 计算净利润(利润总额-所得税费用)

6. 代码优化建议

6.1 启用缓存机制

当前代码中缓存功能已实现但被注释掉,建议在生产环境启用:

// 检查缓存
if (calculationCache.containsKey(cacheKey)) {
    return calculationCache.get(cacheKey);
}

6.2 公式预编译

对于频繁使用的公式,可以实现预编译机制提高性能:

// 在模板加载时预编译公式
Map<String, Expression> compiledFormulas = new HashMap<>();

// 计算时直接使用预编译表达式
Expression expr = compiledFormulas.computeIfAbsent(formula, AviatorEvaluator::compile);
Object result = expr.execute(env);

6.3 批量异常处理

当前实现中单个项目异常会打印日志,建议实现批量异常收集功能,生成报表时统一展示错误信息:

// 收集计算错误
List<String> calculationErrors = new ArrayList<>();

// 计算失败时记录错误
calculationErrors.add("项目 " + item.getItemCode() + " 计算失败: " + e.getMessage());

// 在报表响应中返回错误信息
response.setCalculationErrors(calculationErrors);

通过以上解析,可以看到报表模板算法设计注重了财务计算的精度、性能和可扩展性,采用了分层计算、策略模式、流式处理等现代Java编程技术,确保了报表生成的准确性和高效性。

发货-FBA发货规划

1. 页面概述

FBA发货规划页面是Wimoor系统中用于管理和规划亚马逊FBA发货的核心功能模块。该页面提供了全面的产品发货数据展示、库存分析、发货计划管理等功能,帮助用户制定合理的FBA发货策略,优化库存管理,提高运营效率。

主要功能亮点:

页面位置: wimoor666\wimoor-ui\src\views\erp\ship\ship_plan\index.vue

2. 功能模块

2.1 头部搜索和筛选模块

头部模块包含各种搜索和筛选条件,用于精确查询需要管理的产品数据。

核心功能:

2.2 产品列表模块

产品列表是页面的核心部分,展示了所有符合条件的产品信息,支持排序、筛选和展开查看详情。

核心功能:

2.3 展开详情模块

点击产品行展开按钮,可查看该产品的详细发货计划信息,支持编辑和管理发货计划。

核心功能:

2.4 报表和分析模块

提供产品销量和预计到货的可视化报表,帮助用户分析销售趋势和库存状况。

核心功能:

2.5 辅助功能模块

页面还包含多种辅助功能,增强用户操作体验和数据管理能力。

核心功能:

3. 核心功能解析

3.1 发货计划管理

功能描述: 允许用户为产品创建、编辑和删除发货计划,支持按国家/地区细分。

实现原理:

  1. 通过 planApi.getPlanList 获取产品列表数据
  2. 点击"编辑"按钮进入编辑模式
  3. 在展开的详情表格中设置发货数量和运输方式
  4. 点击"保存"按钮通过 planApi.save 保存发货计划
  5. 点击"移除"按钮通过 planApi.remove 删除发货计划

关键代码:

3.2 库存分析和预警

功能描述: 提供产品库存的详细分析,包括可用库存、预留库存、待入库库存等信息,并通过图表展示库存差异。

实现原理:

  1. 通过 inventoryApi.getInventory 获取库存数据
  2. 通过 inventoryRptApi.syncInventorySupply 同步FBA库存
  3. 点击库存数量查看详细库存信息
  4. 点击FBA库存差异图表查看库存差异分析

关键代码:

3.3 发货计划拆分

功能描述: 支持将一个发货计划拆分为多个子计划,适用于不同运输方式或不同批次的发货需求。

实现原理:

  1. 点击"拆分"按钮打开拆分对话框
  2. 设置拆分后的子计划数量和运输方式
  3. 保存拆分结果并更新主计划

关键代码:

3.4 销量和到货分析

功能描述: 通过图表展示产品的销量趋势和预计到货情况,帮助用户做出更合理的发货决策。

实现原理:

  1. 点击销量报表图标打开销量图表对话框
  2. 点击预计到货报表图标打开到货图表对话框
  3. 图表数据通过后端API获取并使用ECharts渲染

关键代码:

3.5 产品组装管理

功能描述: 对于组合产品,显示其组成部分和可组装数量,帮助用户了解组合产品的库存状况。

实现原理:

  1. 点击组合产品标签查看组装详情
  2. 通过 sublit API获取组装部件信息
  3. 显示可组装数量和部件库存状态

关键代码:

4. 前端API调用

4.1 核心API调用

API名称 调用函数 功能描述 参数说明 调用位置
获取计划列表 planApi.getPlanList 获取产品发货计划列表 {plantype: "ship", ...筛选条件} 800行
获取国家数据 planApi.getExpandCountryData 获取产品按国家细分的发货数据 {groupid, msku, warehouseid, plantype, ...} 709行, 1197行
保存发货计划 planApi.save 保存产品发货计划 [发货计划对象列表] 941行
删除发货计划 planApi.remove 删除产品发货计划 {warehouseid, msku, groupid} 931行
计划拆分 planApi.subsplit 获取计划拆分数据 {发货计划对象} 882行
同步库存 inventoryRptApi.syncInventorySupply 同步FBA库存数据 {skus, groupid, marketplaceid} 566行
获取库存 inventoryApi.getInventory 获取产品库存数据 {warehouseid, materialid} 638行
获取海外仓 warehouseApi.getOversaWarehouse 获取可用海外仓列表 {ftype: "oversea_usable", groupid, country} 620行
获取运输方式 transportationApi.getTransTypeAll 获取所有运输方式 1223行
隐藏产品 markApi.hide 隐藏产品 {materialid} 1065行
显示产品 markApi.show 显示产品 {materialid} 1079行
获取组装列表 sublit 获取产品组装部件列表 {materialid, warehouseid} 612行

4.2 API依赖文件

API文件 路径 功能描述
planApi.js @/api/erp/ship/planApi.js 发货计划相关API
inventoryApi.js @/api/erp/inventory/inventoryApi.js 库存相关API
inventoryRptApi.js @/api/amazon/inventory/inventoryRptApi.js 库存报表相关API
warehouseApi.js @/api/erp/warehouse/warehouseApi.js 仓库相关API
transportationApi.js @/api/erp/ship/transportationApi.js 运输方式相关API
markApi.js @/api/erp/material/markApi.js 产品标记相关API
assemblyApi.js @/api/erp/assembly/assemblyApi.js 产品组装相关API

5. 后端API实现

5.1 发货计划控制器

控制器路径: wimoor666\wimoor-amazon\amazon-boot\src\main\java\com\wimoor\amazon\product\controller\AmzProductSalesPlanController.java

核心接口:

API路径 方法 功能描述 请求参数 响应结果
/api/v1/product/salesplan/refreshPlanData GET 计划数据刷新 成功状态
/api/v1/product/salesplan/refreshDataByGroup GET 按店铺刷新计划 groupid 成功状态
/api/v1/product/salesplan/refreshDataBySKU GET 按SKU刷新计划 groupid, marketplaceid, sku 成功状态
/api/v1/product/salesplan/getExpandCountryData POST 获取国家细分数据 PlanDetailDTO 国家细分数据列表
/api/v1/product/salesplan/getPlanModel POST 获取计划模型数据 PlanDTO 计划数据分页列表

5.2 发货计划项目控制器

控制器路径: wimoor666\wimoor-amazon\amazon-boot\src\main\java\com\wimoor\amazon\product\controller\AmzProductSalesPlanShipItemController.java

核心接口:

API路径 方法 功能描述 请求参数 响应结果
/api/v1/product/salesplan/shipItem/save POST 保存发货计划项目 List<AmzProductSalesPlanShipItem> 保存数量
/api/v1/product/salesplan/shipItem/remove DELETE 删除发货计划项目 groupid, warehouseid, msku 成功状态
/api/v1/product/salesplan/shipItem/subsplit POST 计划拆分 AmzProductSalesPlanShipItem 拆分后的计划列表
/api/v1/product/salesplan/shipItem/clear GET 清除计划 groupid, warehouseid 成功状态
/api/v1/product/salesplan/shipItem/getSummary GET 获取计划汇总 groupid, warehouseid 汇总数据
/api/v1/product/salesplan/shipItem/list GET 获取计划列表 groupid, warehouseid 计划列表
/api/v1/product/salesplan/shipItem/batch POST 计划打包 List<AmzProductSalesPlanShipItem> 批次号
/api/v1/product/salesplan/shipItem/removeBatch POST 计划归档 batchnumber 成功状态

6. 数据模型

6.1 发货计划项目实体

实体类: AmzProductSalesPlanShipItem 表名: t_amz_product_sales_plan_ship_item

字段名 数据类型 描述
id BigInteger 主键ID
sku String 产品SKU
msku String 主SKU
shopid BigInteger 店铺ID
marketplaceid String 市场ID
groupid BigInteger 分组ID
amazonauthid BigInteger 亚马逊授权ID
warehouseid BigInteger 仓库ID
overseaid BigInteger 海外仓ID
transtype BigInteger 运输方式ID
batchnumber String 批次号
amount Integer 数量
aftersalesday Integer 售后天数
opttime LocalDateTime 操作时间
operator BigInteger 操作人ID
isdefault Boolean 是否默认
subnum Integer 子计划数量(非数据库字段)
subList List<AmzProductSalesPlanShipItem> 子计划列表(非数据库字段)

6.2 产品销售计划实体

实体类: AmzProductSalesPlan 表名: t_amz_product_sales_plan

字段名 数据类型 描述
id BigInteger 主键ID
sku String 产品SKU
msku String 主SKU
shopid BigInteger 店铺ID
marketplaceid String 市场ID
groupid BigInteger 分组ID
amazonauthid BigInteger 亚马逊授权ID
shipday Integer 发货天数
salesday Integer 销售天数
deliveryCycle Integer 交付周期
needship Integer 需要发货数量
shipMinCycleSale Integer 最小发货周期销量
needshipfba Integer FBA需要发货数量
avgsales Integer 平均销量
needpurchase Integer 需要采购数量
opttime Date 操作时间
shortTime LocalDate 短缺时间

7. 业务流程

7.1 发货计划创建流程

  1. 数据加载:用户设置筛选条件,系统通过 planApi.getPlanList 获取产品列表数据
  2. 选择产品:用户在产品列表中找到需要创建发货计划的产品
  3. 编辑计划:点击"编辑"按钮,进入编辑模式
  4. 设置参数:在展开的详情表格中,设置各国家/地区的发货数量、运输方式等参数
  5. 保存计划:点击"保存"按钮,系统通过 planApi.save 保存发货计划
  6. 数据更新:保存成功后,系统更新产品列表中的发货状态和数据

7.2 库存分析流程

  1. 数据获取:系统通过 inventoryApi.getInventory 获取产品库存数据
  2. 库存计算:计算可用库存、可组装库存等关键指标
  3. 状态判断:根据库存数据判断产品库存状态
  4. 预警提示:对库存不足的产品进行预警提示
  5. 数据展示:在产品列表中展示库存状态和相关数据

7.3 计划拆分流程

  1. 选择计划:在展开的详情表格中,找到需要拆分的发货计划
  2. 点击拆分:点击"拆分"按钮,打开拆分对话框
  3. 设置参数:在对话框中设置拆分后的子计划数量、运输方式等参数
  4. 确认拆分:点击"确认"按钮,系统通过 planApi.subsplit 处理拆分数据
  5. 更新计划:拆分完成后,系统更新发货计划数据

8. 操作指南

8.1 基本操作

8.1.1 搜索和筛选产品

  1. 在页面顶部的搜索栏中设置筛选条件
  2. 选择仓库、店铺、站点等筛选条件
  3. 点击"查询"按钮,系统会根据条件加载产品数据

8.1.2 查看产品详情

  1. 在产品列表中找到需要查看的产品
  2. 点击产品行左侧的展开按钮,或点击"展开全部"按钮查看所有产品详情
  3. 在展开的详情表格中查看产品的详细发货数据

8.1.3 创建发货计划

  1. 找到需要创建发货计划的产品,点击"编辑"按钮
  2. 在展开的详情表格中,为各国家/地区设置发货数量
  3. 选择合适的运输方式和海外仓(如需)
  4. 点击"保存"按钮,完成发货计划创建

8.1.4 修改发货计划

  1. 找到需要修改的发货计划,点击"编辑"按钮
  2. 修改发货数量、运输方式等参数
  3. 点击"保存"按钮,完成发货计划修改

8.1.5 删除发货计划

  1. 找到需要删除的发货计划,点击"移除"按钮
  2. 在弹出的确认对话框中点击"确定",完成发货计划删除

8.2 高级操作

8.2.1 查看销量报表

  1. 在产品列表中找到需要查看销量的产品
  2. 点击产品行中的销量报表图标(柱状图)
  3. 在弹出的销量报表对话框中查看详细的销量数据

8.2.2 查看预计到货报表

  1. 在产品列表中找到需要查看预计到货的产品
  2. 点击产品行中的预计到货报表图标(折线图)
  3. 在弹出的预计到货报表对话框中查看详细的到货数据

8.2.3 查看库存详情

  1. 在产品列表中找到需要查看库存的产品
  2. 点击产品行中的可用库存数量
  3. 在弹出的库存详情对话框中查看详细的库存数据

8.2.4 计划拆分

  1. 在展开的详情表格中,找到需要拆分的发货计划
  2. 点击"拆分"按钮,打开拆分对话框
  3. 设置拆分后的子计划数量、运输方式等参数
  4. 点击"确认"按钮,完成计划拆分

8.2.5 产品组装管理

  1. 在产品列表中找到需要查看组装信息的组合产品(带有"组合"标签)
  2. 点击"组合"标签,打开组装产品对话框
  3. 在对话框中查看产品的组装部件和可组装数量

9. 常见问题

9.1 库存数据不准确

问题描述:产品列表中显示的库存数据与实际库存不符

解决方法

  1. 点击库存数量旁边的刷新图标,手动同步FBA库存数据
  2. 检查库存数据来源是否正确
  3. 确认仓库选择是否正确

9.2 无法保存发货计划

问题描述:点击"保存"按钮后,发货计划无法保存

解决方法

  1. 检查发货数量是否超过可用库存
  2. 确认运输方式和海外仓选择是否正确
  3. 检查网络连接是否正常
  4. 刷新页面后重新尝试

9.3 计划拆分失败

问题描述:点击"拆分"按钮后,计划拆分失败

解决方法

  1. 检查发货计划数据是否完整
  2. 确认拆分参数设置是否合理
  3. 刷新页面后重新尝试

9.4 报表数据加载缓慢

问题描述:点击报表图标后,报表数据加载缓慢

解决方法

  1. 检查网络连接是否正常
  2. 确认产品数据量是否过大
  3. 尝试减少时间范围,查看数据量较小的报表

9.5 产品无法隐藏

问题描述:点击"隐藏产品"后,产品仍然显示在列表中

解决方法

  1. 检查产品是否已加入发货计划(已加入计划的产品无法隐藏)
  2. 确认操作权限是否足够
  3. 刷新页面后重新尝试

10. 技术实现细节

10.1 前端技术栈

10.2 后端技术栈

10.3 性能优化

  1. 数据加载优化

    • 使用分页查询减少数据传输量
    • 延迟加载非关键数据
    • 缓存常用数据
  2. 渲染优化

    • 使用虚拟滚动处理大量数据
    • 优化组件渲染性能
    • 减少不必要的DOM操作
  3. 交互优化

    • 异步处理耗时操作
    • 提供加载状态反馈
    • 优化用户操作流程

10.4 代码结构

前端代码结构

后端代码结构

11. 总结

FBA发货规划页面是Wimoor系统中一个功能强大、设计合理的核心模块,它通过直观的数据展示、智能的库存分析和灵活的计划管理,为用户提供了全面的FBA发货管理解决方案。

核心价值

该页面功能丰富,操作便捷,为用户提供了全面的FBA发货管理解决方案。同时,通过不断的优化和改进,可以进一步提高页面性能和用户体验,为用户创造更大的价值。

发货-创建发货单

FBA 发货单创建功能详细帮助文档

1. 功能概述

本文档详细介绍 FBA 发货单创建功能的实现,包括前端组件结构、API 调用、后端实现、数据模型和业务逻辑流程。该功能位于 wimoor666\wimoor-ui\src\views\erp\shipv2\shipment_add\create\ 目录下,是 FBA 发货流程的起点,用于创建新的发货计划。

2. 前端组件结构

2.1 组件层级

index.vue (容器组件)
└── Fromlist (核心表单组件,即 ./components/form.vue)
    ├── 基本信息表单
    │   ├── 计划编码
    │   ├── 发货店铺
    │   ├── 发货仓库
    │   ├── 收货国家
    │   ├── 发货地址
    │   ├── 备注
    │   ├── 运输方式
    │   ├── 物流方式
    │   └── 单据类型
    ├── 商品管理
    │   ├── 添加商品
    │   ├── 导入商品
    │   ├── 打印标签
    │   ├── 清空列表
    │   └── 商品列表
    └── 对话框
        ├── 地址编辑对话框
        ├── 商品选择对话框
        ├── 导入数据对话框
        └── 其它信息设置对话框

2.2 核心文件

文件路径 功能描述
wimoor666\wimoor-ui\src\views\erp\shipv2\shipment_add\create\index.vue 容器组件,加载核心表单
wimoor666\wimoor-ui\src\views\erp\shipv2\shipment_add\create\components\form.vue 核心表单组件,包含完整的发货单创建功能
wimoor666\wimoor-ui\src\views\erp\shipv2\shipment_add\create\components\mateiralDialog.vue 商品选择对话框
wimoor666\wimoor-ui\src\views\amazon\address\components\editdialog.vue 地址编辑对话框

3. API 调用分析

3.1 前端 API 调用

API 方法 用途 参数 来源文件
groupApi.getAmazonGroup() 获取店铺列表 groupApi.js
marketApi.getMarketByGroup({groupid}) 获取国家列表 {groupid} marketApi.js
warehouseApi.getWarehouseUseable() 获取仓库列表 [warehouseApi.js]
shipaddressApi.getAddressByid({addressid, groupid}) 获取地址列表 {addressid, groupid} [shipaddressApi.js]
serialnumberApi.getSerialNumber({ftype, isfind}) 获取序列号 {ftype: 'SF', isfind: 'true'} [serialnumberApi.js]
transportationApi.getTransTypeAll() 获取物流类型列表 [transportationApi.js]
shipmentplanApi.saveInboundPlan(formData) 保存入库计划 formData [shipmentplanApi.js]
profitParamApi.getInplaceList({country}) 获取入库地点列表 {country: 'US'} [profitParamApi.js]

3.2 后端控制器实现

控制器方法 用途 路径 来源文件
ShipInboundPlanV2Controller.saveInboundPlanAction() 保存入库计划 /api/v2/shipInboundPlan/saveInboundPlan ShipInboundPlanV2Controller.java
AmazonAuthorityController.getAmazonGroupAction() 获取店铺列表 /amazon/api/v1/amzauthority/getAmazonGroup AmazonAuthorityController.java
AmazonAuthorityController.getMarketByGroupAction() 获取国家列表 /amazon/api/v1/amzauthority/getMarketByGroup AmazonAuthorityController.java

4. 后端数据模型

4.1 核心实体类

实体类 描述 表名 来源文件
ShipInboundPlan 发货计划 t_erp_ship_v2_inboundplan ShipInboundPlan.java
ShipInboundItem 发货计划商品 t_erp_ship_v2_inbounditem ShipInboundItem.java
ShipInboundShipment 货件 t_erp_ship_v2_inboundshipment ShipInboundShipment.java
ShipInboundBox 箱子信息 t_erp_ship_v2_inboundbox ShipInboundBox.java

4.2 实体类关系

ShipInboundPlan (1) ──── (N) ShipInboundItem
ShipInboundPlan (1) ──── (N) ShipInboundShipment
ShipInboundShipment (1) ──── (N) ShipInboundBox

5. 业务逻辑流程

5.1 初始化流程

  1. 组件加载onMounted 钩子函数触发初始化
  2. 初始化查询数据initQueryData() 从路由参数中获取初始化数据
  3. 加载计划名称loadShipName() 生成计划名称
  4. 加载计划编码loadNumber() 获取序列号作为计划编码
  5. 加载店铺列表getGroupData("init") 获取店铺列表并设置默认值
  6. 加载仓库列表getWarehouseData() 获取仓库列表并设置默认值
  7. 加载物流方式getTranList() 获取物流方式列表
  8. 加载入库地点loadInplace() 获取入库地点列表

5.2 表单填写流程

  1. 基本信息填写

    • 选择发货店铺 → 触发 groupChange() → 加载对应市场列表和地址列表
    • 选择发货仓库 → 触发 warehouseChange() → 更新仓库信息
    • 选择收货国家 → 触发 marketChange() → 更新国家信息并验证 SKU
    • 选择发货地址 → 触发 radioChange() → 更新地址信息
    • 填写备注、运输方式、物流方式、单据类型
  2. 商品管理

    • 添加商品:点击"添加商品"按钮 → 打开商品选择对话框 → 选择商品 → 触发 getdata() → 添加商品到列表
    • 导入商品:点击"导入"按钮 → 打开导入数据对话框 → 上传文件 → 处理导入数据
    • 打印标签:点击"打印当前产品标签"按钮 → 打印商品标签
    • 清空列表:点击"清空列表"按钮 → 清空商品列表
    • 删除商品:点击商品行的"删除"按钮 → 从列表中移除商品
    • 其他维护:点击"其它维护"按钮 → 打开其它信息设置对话框 → 设置预备信息处理人、贴标人、保质期

5.3 提交流程

  1. 准备数据

    • 收集表单数据
    • 处理商品列表数据
    • 设置计划项目列表
  2. 提交计划

    • 点击"提交"按钮 → 触发 submitPlan()
    • 显示加载状态
    • 调用 shipmentplanApi.saveInboundPlan(state.formData) 提交数据
    • 处理响应:显示成功消息 → 跳转到新货件详情页面
  3. 取消操作

    • 点击"取消"按钮 → 触发 cancelPlan() → 清空商品列表并触发取消事件

6. 核心代码分析

6.1 前端核心代码

6.1.1 提交计划

function submitPlan() {
  var itemlist = [];
  state.formData.groupid = state.formData.groupid;
  state.formData.planmarketplaceid = state.queryData.marketplaceid;
  state.formData.planid = state.queryData.planid;
  state.formData.batchnumber = state.queryData.batchnumber;
  state.formData.issplit = state.queryData.issplit;
  state.productlist.forEach(function(item) {
    var row = item;
    row.QuantityShipped = item.quantity;
    if (state.formData.arecasesrequired) {
      row.boxnum = item.boxnum;
    }
    row.materialid = item.id;
    row.sellersku = item.sku;
    row.opttime = null;
    itemlist.push(row);
  });
  state.formData.planitemlist = itemlist;
  state.submitloading = true;
  if (props.isappend) {
    state.submitloading = false;
    emit("change", state);
  } else {
    if (!state.formData.sourceAddress) {
      ElMessage.error('地址信息未选中,请刷新重试!');
      return;
    }
    shipmentplanApi.saveInboundPlan(state.formData).then((res) => {
      ElMessage.success('已提交成功!');
      state.submitloading = false;
      router.push({
        path: '/newshipmentdetails',
        query: {
          id: res.data,
          title: "新货件详情",
          replace: true,
          path: '/newshipmentdetails',
        }
      })
    }).catch(error => {
      state.submitloading = false;
    })
  }
}

6.1.2 获取店铺列表

function getGroupData(type) {
  groupApi.getAmazonGroup().then((res) => {
    state.groupList = res.data;
    if (res.data && res.data.length > 0) {
      if (!state.formData.groupid || state.formData.groupid == "") {
        if (type == 'init' && state.queryData.groupid) {
          state.formData.groupid = state.queryData.groupid;
        } else {
          state.formData.groupid = res.data[0].id;
        }
      }
      getMarketData(state.formData.groupid, type);
      loadAddress();
      loadPlanData();
    }
  })
}

6.2 后端核心代码

6.2.1 保存入库计划

@ApiOperation(value = "提交发货计划")
@PostMapping("/saveInboundPlan")
@SystemControllerLog("新增")    
@Transactional
public Result<String> saveInboundPlanAction(@ApiParam("发货计划")@RequestBody ShipInboundPlan inplan){
    UserInfo user=UserInfoContext.get();
    inplan.setCreatetime(new Date());
    inplan.setCreator(user.getId());
    inplan.setOperator(user.getId());
    inplan.setOpttime(new Date());
    inplan.setShopid(user.getCompanyid());
    inplan.setAuditstatus(1);
    inplan.setInvstatus(0);
    try {
        inplan.setNumber(serialNumService.readSerialNumber(user.getCompanyid(), "SF"));
    } catch (Exception e) {
        e.printStackTrace();
        try {
            inplan.setNumber(serialNumService.readSerialNumber(user.getCompanyid(), "SF"));
        } catch (Exception e1) {
            e1.printStackTrace();
            throw new BizException("编码获取失败,请联系管理员");
        }
    }
    try {
        if(inplan.getInvtype()==null) {
            inplan.setInvtype(0);
        }
        shipInboundPlanV2Service.saveShipInboundPlan(inplan);
        shipInboundV2ShipmentRecordService.saveRecord(inplan);
        if(StrUtil.isNotBlank(inplan.getBatchnumber()) ) {
            iAmzProductSalesPlanShipItemService.moveBatch(user.getCompanyid(),inplan.getBatchnumber());
        }
        return Result.success(inplan.getId());
    }catch(FeignException e) {
        throw new BizException("提交失败" +e.getMessage());
    }catch(Exception e) {
        throw new BizException("提交失败" +e.getMessage());
    }
}

6.2.2 获取店铺列表

@ApiOperation(value = "获取店铺")
@GetMapping("/getAmazonGroup")
public Result<List<AmazonGroup>> getAmazonGroupAction() {
    UserInfo userinfo = UserInfoContext.get();
    List<AmazonGroup> result = iAmazonGroupService.getGroupByUser(userinfo);
    return Result.success(result);
}

7. 技术栈

类别 技术/框架 版本 用途
前端 Vue 3.x 前端框架
前端 Element Plus 最新版 UI 组件库
前端 Icon Park 最新版 图标库
前端 Axios 最新版 HTTP 客户端
后端 Spring Boot 最新版 后端框架
后端 MyBatis Plus 最新版 ORM 框架
后端 Swagger 最新版 API 文档
数据库 MySQL 最新版 数据库

8. 输入输出示例

8.1 输入示例

// 表单数据示例
const formData = {
  name: "PLN(1/22/2026 14:30)-1",
  number: "SF202601220001",
  groupid: "group1",
  warehouseid: "warehouse1",
  marketplaceid: "US",
  amazonauthid: "auth1",
  sourceAddress: "address1",
  remark: "测试发货计划",
  transtyle: "SP",
  transtype: "trans1",
  invtype: 0,
  planitemlist: [
    {
      sku: "SKU001",
      msku: "MSKU001",
      quantity: 10,
      labelOwner: "SELLER",
      prepOwner: "SELLER",
      materialid: "material1",
      sellersku: "SKU001"
    },
    {
      sku: "SKU002",
      msku: "MSKU002",
      quantity: 5,
      labelOwner: "SELLER",
      prepOwner: "SELLER",
      materialid: "material2",
      sellersku: "SKU002"
    }
  ]
};

// 调用API
const { data } = await shipmentplanApi.saveInboundPlan(formData);

8.2 输出示例

// 成功响应
{
  "code": 0,
  "msg": "",
  "data": "plan123456"
}

// 失败响应
{
  "code": 500,
  "msg": "提交失败:编码获取失败,请联系管理员",
  "data": null
}

9. 业务流程图

flowchart TD
    A[进入添加发货单页面] --> B[初始化数据]
    B --> C[填写基本信息]
    C --> D[选择发货店铺]
    D --> E[选择发货仓库]
    E --> F[选择收货国家]
    F --> G[选择发货地址]
    G --> H[填写其他信息]
    H --> I[添加商品]
    I --> J[提交发货计划]
    J --> K[后端处理]
    K --> L[返回计划ID]
    L --> M[跳转到新货件详情页面]
    
    subgraph 商品管理
        I1[添加商品]
        I2[导入商品]
        I3[打印标签]
        I4[清空列表]
        I5[删除商品]
        I6[其他维护]
    end
    
    I --> I1
    I --> I2
    I --> I3
    I --> I4
    I --> I5
    I --> I6

10. 代码优化建议

10.1 前端优化

  1. 错误处理增强:添加更详细的错误处理和用户提示,特别是在 API 调用失败时
  2. 性能优化:对于大量商品的列表,考虑使用虚拟滚动
  3. 代码组织:将复杂的业务逻辑拆分为更小的函数,提高代码可读性
  4. 表单验证:添加更严格的表单验证,确保数据完整性
  5. 用户体验:添加加载状态和进度指示,提升用户体验
  6. 代码复用:提取重复的代码为公共函数或组件

10.2 后端优化

  1. 事务管理:确保 saveInboundPlan 方法中的事务处理更加健壮
  2. 参数验证:增强请求参数的验证,确保数据完整性
  3. 错误处理:提供更详细的错误信息,便于前端处理
  4. 性能优化:对于频繁查询的数据,考虑添加缓存
  5. 日志记录:添加详细的日志记录,便于问题排查

11. 常见问题及解决方案

问题 原因 解决方案
地址信息未选中 地址列表加载失败或用户未选择 刷新页面重新加载地址列表,确保选择一个有效的发货地址
编码获取失败 序列号生成服务异常 检查序列号服务状态,或联系管理员
提交失败 后端处理异常 检查网络连接,查看控制台错误信息,或联系管理员
商品数量超过库存 发货数量大于可用库存 减少发货数量,确保不超过可用库存
SKU 验证失败 SKU 不存在或无效 检查 SKU 是否正确,确保 SKU 在系统中存在

12. 结论

FBA 发货单创建功能是一个功能完整、设计合理的业务组件,为用户提供了直观、高效的发货计划创建界面。通过前端与后端的紧密协作,实现了从基本信息填写到商品管理再到计划提交的完整流程。

该功能的实现展示了现代前端开发的最佳实践,包括:

同时,后端实现也体现了 Spring Boot 框架的优势,包括:

总体而言,FBA 发货单创建功能是一个设计精良、功能完善的业务组件,为 FBA 发货流程提供了重要的起点,帮助用户快速、准确地创建发货计划,提高了物流管理的效率。

发货-审核发货单

FBA 发货单审核功能详细帮助文档

1. 功能概述

本文档详细介绍 FBA 发货单审核功能的实现,包括前端组件结构、API 调用、后端实现、数据模型和业务逻辑流程。该功能位于 wimoor666\wimoor-ui\src\views\erp\shipv2\shipment_add\approve\ 目录下,是 FBA 发货流程的审核环节,用于审核和管理发货计划。

2. 前端组件结构

2.1 组件层级

index.vue (核心审核组件)
├── 头部信息区域
│   ├── 计划编码
│   ├── 复制新增按钮
│   ├── 预估配置费
│   └── 操作按钮组 (拆分、关闭、审核/驳回)
├── 主体内容区域
│   ├── 左侧商品列表 (Table 组件)
│   └── 右侧信息列表 (List 组件)
└── 对话框
    ├── 审核对话框 (approveVisible)
    │   ├── 货件列表
    │   ├── 审核选项 (通过/驳回)
    │   └── 全选驳回选项
    └── 拆分对话框 (SplitDialog)

2.2 核心文件

文件路径 功能描述
wimoor666\wimoor-ui\src\views\erp\shipv2\shipment_add\approve\index.vue 核心审核组件,包含完整的审核功能
wimoor666\wimoor-ui\src\views\erp\shipv2\shipment_add\approve\components\table.vue 商品列表组件
wimoor666\wimoor-ui\src\views\erp\shipv2\shipment_add\approve\components\list.vue 信息列表组件
wimoor666\wimoor-ui\src\views\erp\shipv2\shipment_add\approve\components\split_dialog.vue 拆分对话框组件

3. API 调用分析

3.1 前端 API 调用

API 方法 用途 参数 来源文件
shipmentplanApi.getPlanInfo({formid}) 获取发货计划信息 {formid: planid} [shipmentplanApi.js]
shipmentplanApi.approveInboundPlan({id}) 审核通过发货计划 {id: plandata.value.id} [shipmentplanApi.js]
shipmentplanApi.rejectInboundPlan({id}) 驳回发货计划 {id: plandata.value.id} [shipmentplanApi.js]
shipmentplanApi.createShipment({shipmentid}) 创建货件 {shipmentid: item.shipmentId} [shipmentplanApi.js]
shipmentplanApi.cancelShipment({shipmentid}) 取消货件 {shipmentid: item.shipmentId} [shipmentplanApi.js]
calculateApi.inplacefee(paramlist) 计算入库费用 paramlist (包含 SKU、入库地点、尺寸、重量等信息) [calculateApi.js]

3.2 后端控制器实现

控制器方法 用途 路径 来源文件
ShipInboundPlanV2Controller.getPlanInfoAction() 获取发货计划信息 /api/v2/shipInboundPlan/getPlanInfo ShipInboundPlanV2Controller.java
ShipInboundPlanV2Controller.approveInboundPlan() 审核通过发货计划 /api/v2/shipInboundPlan/approveInboundPlan ShipInboundPlanV2Controller.java
ShipInboundPlanV2Controller.rejectInboundPlan() 驳回发货计划 /api/v2/shipInboundPlan/rejectInboundPlan ShipInboundPlanV2Controller.java
ShipFormController.createShipmentAction() 创建货件 /api/v1/shipForm/create ShipFormController.java
ShipFormController.cancelShipmentAction() 取消货件 /api/v1/shipForm/cancelShipment ShipFormController.java

4. 后端数据模型

4.1 核心实体类

实体类 描述 表名 来源文件
ShipInboundPlan 发货计划 t_erp_ship_v2_inboundplan ShipInboundPlan.java
ShipInboundItem 发货计划商品 t_erp_ship_v2_inbounditem ShipInboundItem.java
ShipInboundShipment 货件 t_erp_ship_v2_inboundshipment ShipInboundShipment.java
ShipInboundOperation 操作记录 t_erp_ship_v2_inboundoperation ShipInboundOperation.java

4.2 实体类关系

ShipInboundPlan (1) ──── (N) ShipInboundItem
ShipInboundPlan (1) ──── (N) ShipInboundShipment
ShipInboundPlan (1) ──── (N) ShipInboundOperation

5. 业务逻辑流程

5.1 初始化流程

  1. 组件加载onMounted 钩子函数触发 loadData()
  2. 加载计划信息loadData() 调用 shipmentplanApi.getPlanInfo() 获取计划详情
  3. 初始化组件
    • 更新 plandata.value 存储计划信息
    • 设置 tableRef.value.planDatalistRef.value.planData
    • 调用 summaryTotalPrice(item) 计算入库费用
    • 调用 tableRef.value.initData(item) 初始化商品列表
    • 设置 listRef.value.remark 备注信息

5.2 审核流程

  1. 打开审核对话框:点击"审核"按钮 → 触发 approvePlan()
  2. 确认审核操作:弹出确认对话框,用户选择"通过"或"驳回"
  3. 执行审核操作
    • 选择"通过" → 调用 shipmentplanApi.approveInboundPlan()
    • 选择"驳回" → 调用 shipmentplanApi.rejectInboundPlan()
  4. 更新状态:审核操作完成后,调用 loadData() 重新加载计划信息,更新界面状态

5.3 货件审核流程

  1. 打开审核对话框:在审核对话框中,用户可以为每个货件选择"通过"或"驳回"
  2. 全选驳回:点击"全选驳回"复选框 → 触发 changeAllCancel() → 所有货件自动选择"驳回"
  3. 提交审核:点击"确认"按钮 → 触发 submitplan()
  4. 执行货件操作
    • 对于选择"通过"的货件 → 调用 shipmentplanApi.createShipment()
    • 对于选择"驳回"的货件 → 调用 shipmentplanApi.cancelShipment()
  5. 更新状态:操作完成后,关闭对话框并调用 loadData() 重新加载计划信息

5.4 其他操作流程

  1. 拆分计划:点击"拆分"按钮 → 触发 splitHandel() → 打开拆分对话框
  2. 关闭页面:点击"关闭"按钮 → 触发 closePage() → 跳转到新发货单页面
  3. 复制计划:点击"复制"按钮 → 触发 copyPlan() → 跳转到添加新版货件页面

6. 核心代码分析

6.1 前端核心代码

6.1.1 加载计划信息

function loadData() {
  if(planid) {
    //重新查询新的plan
    shipmentplanApi.getPlanInfo({"formid":planid}).then((res) => {
      if(res.data) {
        res.data.iserror=false;
        plandata.value=res.data;
        var item=res.data;
        var nowDate=new Date();
        item.nameVis=false;
        item.remarkVis=false;
        if(item.name=='' || item.name==undefined || item.name==null) {
          item.name="FBA"+"("+(nowDate.getMonth()+1)+"/"+(nowDate.getDate())+"/"+nowDate.getFullYear()+" "+nowDate.getHours()+":"+nowDate.getMinutes()+")-"+(index+1)
        }
        if(res.data.auditstatus==3 && item.status==1) {
          res.data.iserror=true;
        }
        
        tableRef.value.planData=res.data;
        summaryTotalPrice(item);
        tableRef.value.initData(item);
        listRef.value.planData=res.data;
        listRef.value.remark=res.data.remark;
        
      }
    });
  }
}

6.1.2 审核计划

function approvePlan() {
  ElMessageBox.confirm('确认审核该计划?', '警告', {
    distinguishCancelAndClose: true,
    confirmButtonText: '通过',
    cancelButtonText: '驳回',
    type: 'warning',
    center: true,
  })
  .then(() => {
    submitLoading.value=true;
    shipmentplanApi.approveInboundPlan({"id":plandata.value.id}).then(res => {
      ElMessage.success('通过成功');
      submitLoading.value=false;
      loadData();
    }).catch(error => {
      submitLoading.value=false;
      console.log(error);
    });
  })
  .catch((action) => {
    if(action=='cancel') {
      submitLoading.value=true;
      shipmentplanApi.rejectInboundPlan({"id":plandata.value.id}).then(res => {
        ElMessage.success('驳回成功');
        loadData();
        submitLoading.value=false;
      }).catch(error => {
        submitLoading.value=false;
      });
    }
  })
}

6.1.3 提交货件审核

async function submitplan() {
  confirmloading.value=true;
  for(var i=0;i<shipmentList.value.length;i++) {
    var item=shipmentList.value[i];
    if(item.approve==true) {
      await shipmentplanApi.createShipment({"shipmentid":item.shipmentId}).then(res => {
        ElMessage.success( item.shipmentId+'审核成功!');
      }).catch(error => {
      });
    } else {
      await shipmentplanApi.cancelShipment({"shipmentid":item.shipmentId}).then(res => {
        ElMessage.success(item.shipmentId+'驳回成功!');
      }).catch(error => {
      });
    }
  }
  nextTick(() => {
    confirmloading.value=false;
  })
  nextTick(() => {
    approveVisible.value=false;
  })
  var timer=setTimeout(function() {
    nextTick(() => {
      loadData();
    })
  }, 300);
}

6.2 后端核心代码

6.2.1 获取计划信息

@ApiOperation(value = "获取货件计划")
@GetMapping("/getPlanInfo")
public Result<ShipPlanVo> getPlanInfoAction(String formid) {
  UserInfo user=UserInfoContext.get();
  try {
    if(StrUtil.isEmptyIfStr(formid)) {
      throw new BizException("计划ID不能为空");
    }
    
    ShipPlanVo vo = shipInboundPlanV2Service.getPlanBaseInfo(formid,user);
    return Result.success(vo);
  } catch(Exception e) {
    e.printStackTrace();
  }
  return Result.success(null);
}

6.2.2 审核通过计划

@ApiOperation(value = "审核发货计划")
@GetMapping("/approveInboundPlan")
@SystemControllerLog("审核")    
@Transactional
public Result<String> approveInboundPlan(String id) {
  UserInfo user=UserInfoContext.get();
  ShipInboundPlan inplan=shipInboundPlanV2Service.getById(id);
  try {
    List<ShipInboundItemVo> itemlist = iShipInboundItemService.listByFormid(inplan.getId());
    ShipFormDTO dto=getFormDTO(inplan,itemlist);
    if(inplan.getInvtype()!=2) {
      erpClientOneFeign.outBoundShipInventory(dto);
      inplan.setInvstatus(1);
    }
    for(ShipInboundItemVo item:itemlist) {
      ShipInboundItem itemold = iShipInboundItemService.getById(item.getId());
      itemold.setMsku(item.getMsku());
      iShipInboundItemService.updateById(itemold);
    }
    inplan.setAuditstatus(2);
    inplan.setAuditor(user.getId());
    inplan.setAuditime(new Date());
    inplan.setOpttime(new Date());
    inplan.setOperator(user.getId());
    shipInboundPlanV2Service.updateById(inplan);
    shipInboundV2ShipmentRecordService.saveRecord(inplan);
    return Result.success(inplan.getId());
  } catch(FeignException e) {
    throw new BizException("提交失败" +e.getMessage());
  } catch(BizException e) {
    throw e;
  } catch(Exception e) {
    throw new BizException("提交失败" +e.getMessage());
  }
}

6.2.3 驳回计划

@ApiOperation(value = "驳回发货计划")
@GetMapping("/rejectInboundPlan")
@SystemControllerLog("驳回")    
@Transactional
public Result<String> rejectInboundPlan(String id) {
  UserInfo user=UserInfoContext.get();
  ShipInboundPlan inplan=shipInboundPlanV2Service.getById(id);
  inplan.setAuditstatus(11);
  inplan.setAuditime(new Date());
  inplan.setAuditor(user.getId());
  shipInboundPlanV2Service.updateById(inplan);
  shipInboundV2ShipmentRecordService.saveRecord(inplan);
  return Result.success(inplan.getId());
}

6.2.4 取消货件

@ApiOperation(value = "驳回虚拟货件")
@GetMapping("/cancelShipment")
public Result<Boolean> cancelShipmentAction(String shipmentid) {
  ShipInboundShipment shipment = shipInboundShipmentService.getById(shipmentid);
  if(shipment!=null) {
    shipment.setStatus(-1);
    ShipInboundPlan plan = shipInboundPlanService.getById(shipment.getInboundplanid());
    if(plan.getAuditstatus()==1) {
      plan.setAuditstatus(2);
      shipInboundPlanService.updateById(plan);
    }
    shipment.setShipmentstatus(ShipmentStatus.CANCELLED.getValue());
    boolean isupdate = shipInboundShipmentService.updateById(shipment);
    if(isupdate) {
      AmazonAuthority auth = amazonAuthorityService.selectByGroupAndMarket(plan.getAmazongroupid(), plan.getMarketplaceid());
      iFulfillmentInboundService.checkCancel(auth,plan,shipment);
    }
    return Result.judge(isupdate);
  } else {
    throw new BizException("找不到对应虚拟货件!");
  }
}

6. 技术栈

类别 技术/框架 版本 用途
前端 Vue 3.x 前端框架
前端 Element Plus 最新版 UI 组件库
前端 Icon Park 最新版 图标库
前端 Axios 最新版 HTTP 客户端
后端 Spring Boot 最新版 后端框架
后端 MyBatis Plus 最新版 ORM 框架
后端 Swagger 最新版 API 文档
数据库 MySQL 最新版 数据库

7. 输入输出示例

7.1 输入示例

// 获取计划信息请求
const { data } = await shipmentplanApi.getPlanInfo({ formid: "plan123" });

// 审核通过请求
const { data } = await shipmentplanApi.approveInboundPlan({ id: "plan123" });

// 驳回请求
const { data } = await shipmentplanApi.rejectInboundPlan({ id: "plan123" });

// 创建货件请求
const { data } = await shipmentplanApi.createShipment({ shipmentid: "shipment123" });

// 取消货件请求
const { data } = await shipmentplanApi.cancelShipment({ shipmentid: "shipment123" });

7.2 输出示例

// 获取计划信息响应
{
  "code": 0,
  "msg": "",
  "data": {
    "id": "plan123",
    "number": "SF202601220001",
    "name": "FBA(1/22/2026 14:30)-1",
    "groupid": "group1",
    "warehouseid": "warehouse1",
    "marketplaceid": "US",
    "amazonauthid": "auth1",
    "sourceAddress": "address1",
    "remark": "测试发货计划",
    "auditstatus": 1,
    "itemlist": [
      {
        "sku": "SKU001",
        "msku": "MSKU001",
        "quantity": 10,
        "labelOwner": "SELLER",
        "prepOwner": "SELLER"
      }
    ],
    "shipmentList": [
      {
        "shipmentId": "shipment123",
        "name": "货件1",
        "toquantity": 10,
        "weight": 5.5,
        "readweight": 6.0,
        "boxvolume": "0.1",
        "itemprice": 100,
        "addressTo": {
          "countrycode": "US"
        },
        "destinationFulfillmentCenterId": "ABE2"
      }
    ]
  }
}

// 审核通过响应
{
  "code": 0,
  "msg": "",
  "data": "plan123"
}

// 驳回响应
{
  "code": 0,
  "msg": "",
  "data": "plan123"
}

8. 业务流程图

flowchart TD
    A[进入发货单详情页面] --> B[加载计划信息]
    B --> C[显示计划详情]
    C --> D{审核状态}
    D -->|未审核| E[点击审核按钮]
    D -->|已审核| F[显示已审核状态]
    D -->|已驳回| G[显示已驳回状态]
    E --> H[弹出审核确认对话框]
    H --> I{用户选择}
    I -->|通过| J[调用approveInboundPlan接口]
    I -->|驳回| K[调用rejectInboundPlan接口]
    J --> L[更新计划状态为已审核]
    K --> M[更新计划状态为已驳回]
    L --> N[重新加载计划信息]
    M --> N
    N --> O[更新界面显示]
    
    subgraph 货件审核
        P[打开货件审核对话框]
        Q[选择货件审核状态]
        R[点击全选驳回]
        S[提交审核]
        T[执行货件操作]
    end
    
    E --> P
    P --> Q
    P --> R
    Q --> S
    R --> S
    S --> T
    T --> N

9. 代码优化建议

9.1 前端优化

  1. 错误处理增强:添加更详细的错误处理和用户提示,特别是在 API 调用失败时
  2. 性能优化:对于大量货件的列表,考虑使用虚拟滚动
  3. 代码组织:将复杂的业务逻辑拆分为更小的函数,提高代码可读性
  4. 表单验证:添加更严格的表单验证,确保数据完整性
  5. 用户体验:添加加载状态和进度指示,提升用户体验
  6. 代码复用:提取重复的代码为公共函数或组件
  7. 内存管理:清理定时器,避免内存泄漏

9.2 后端优化

  1. 事务管理:确保所有数据库操作都在事务中执行,保证数据一致性
  2. 参数验证:增强请求参数的验证,确保数据完整性
  3. 错误处理:提供更详细的错误信息,便于前端处理
  4. 性能优化:对于频繁查询的数据,考虑添加缓存
  5. 日志记录:添加详细的日志记录,便于问题排查
  6. API 设计:统一 API 接口设计,使用 POST 方法处理包含参数的请求

10. 常见问题及解决方案

问题 原因 解决方案
审核失败 后端 API 调用失败 检查网络连接,查看控制台错误信息,联系管理员
货件创建失败 货件信息不完整或格式错误 检查货件信息,确保所有必填字段都已填写
入库费用计算错误 商品尺寸或重量信息不正确 检查商品的尺寸和重量信息,确保数据准确
页面加载缓慢 计划信息过多或网络延迟 优化网络连接,考虑分页加载大量数据
拆分功能失败 商品列表为空或格式错误 确保商品列表不为空,检查商品数据格式

11. 结论

FBA 发货单审核功能是 FBA 发货流程中的重要环节,用于审核和管理发货计划。该功能通过前端组件与后端 API 的协作,实现了计划信息的加载、审核状态的管理、货件的创建和取消等核心功能。

该功能的实现展示了现代前端开发的最佳实践,包括:

同时,后端实现也体现了 Spring Boot 框架的优势,包括:

总体而言,FBA 发货单审核功能是一个设计精良、功能完善的业务组件,为 FBA 发货流程提供了重要的审核环节,帮助用户快速、准确地审核发货计划,提高了物流管理的效率。

发货-配货

配货页面 (one_pick.vue) 详细帮助文档

1. 页面概述

配货页面是 Wimoor ERP 系统中 FBA 发货流程的第一个步骤,主要负责展示和管理待发货的商品列表,允许用户进行配货操作、修改发货地址、查看商品详情等功能。

核心功能点

技术栈

2. 功能模块

2.1 页面布局

<template>
  <div class="box-margin">
    <div class="pag-radius-bor mar-bot">
      <div class="con-body"> 
        <Header ref="headerRef" ></Header>
        <!-- 地址信息 -->
        <!-- 商品列表 -->
        <el-table :data="planData.itemlist" border >
          <!-- 商品信息列 -->
        </el-table>
        <!-- 操作按钮 -->
      </div>
    </div>
  </div>
</template>

2.2 核心功能模块

模块名称 功能描述 实现方式
头部信息 展示货件计划的基本信息,如名称、状态等 引入 Header 组件
地址管理 展示和修改发货地址 弹窗选择地址,调用 addressApi.getAddress
商品列表 展示待配货的商品信息,支持编辑数量 el-table 组件,支持行内编辑
文档生成 生成配货单、标签等文档 调用 shipmentQuotaApi 相关方法
配货操作 完成配货,进入下一个步骤 调用 shipmentplanApi.doneInboundPlan
库存管理 查看商品库存信息 调用 inventoryApi.getInventory

3. 核心功能分析

3.1 数据加载

功能描述:页面加载时,通过 URL 参数获取计划 ID,然后调用 API 获取货件计划详情。

实现代码

onMounted(() => {
  let planid = route.query.formid;
  if (planid) {
    loadData(planid);
  }
});

function loadData(planid) {
  state.planData.id = planid;
  shipmentplanApi.getPlanInfo({"formid": planid}).then((res) => {
    if (res.code == 200) {
      state.planData = res.data;
      // 处理数据...
    }
  });
}

后端实现:对应 ShipInboundPlanV2Controller.getPlanInfoAction 方法,返回货件计划的详细信息。

3.2 配货操作

功能描述:用户确认配货完成后,调用 API 完成配货操作,更新货件计划状态。

实现代码

function donePlan() {
  ElMessageBox.confirm('确认完成配货吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    shipmentplanApi.doneInboundPlan({"formid": state.planData.id}).then(res => {
      if (res.code == 200) {
        ElMessage.success('操作成功');
        // 跳转到下一个步骤
        emit('nextStep');
      }
    });
  });
}

后端实现:对应 ShipInboundPlanV2Controller.doneInboundPlan 方法,更新货件计划状态为已完成配货。

3.3 地址修改

功能描述:用户可以修改发货地址,选择已有的地址或新增地址。

实现代码

function changeAddress() {
  // 打开地址选择弹窗
  const dialogRef = ElMessageBox.confirm(
    `<div style="height:450px;"><AddressSelect ref="addressSelectRef" @select="selectAddress" :companyid="userInfo.companyid" :addressid="state.planData.addressid" /></div>`,
    '选择地址',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
      dangerouslyUseHTMLString: true
    }
  );
}

function selectAddress(rows) {
  // 选择地址后调用 API 更新
  shipmentplanApi.changeAddress({"formid": state.planData.id, "addressid": rows[0].id}).then((res) => {
    if (res.code == 200) {
      ElMessage.success('操作成功');
      loadData(state.planData.id);
    }
  });
}

后端实现:对应 ShipInboundPlanV2Controller.changeAddressAction 方法,更新货件计划的发货地址。

3.4 商品编辑

功能描述:用户可以编辑商品的配货数量、贴标信息等。

实现代码

function handleEdit(row) {
  state.operatorRow = JSON.parse(JSON.stringify(row));
  state.dialogVisible = true;
}

function handleConfirm() {
  shipmentplanApi.updatePlanItem(state.operatorRow).then((res) => {
    if (res.code == 200) {
      ElMessage.success('操作成功');
      state.dialogVisible = false;
      loadData(state.planData.id);
    }
  });
}

后端实现:对应 ShipInboundPlanV2Controller.updatePlanItemAction 方法,更新商品的配货信息。

3.5 文档生成

功能描述:生成和打印配货单、标签等文档。

实现代码

function downLabel(ftype) {
  let formids = [];
  formids.push(state.planData.id);
  if (ftype == 'pdf') {
    shipmentQuotaApi.downPDFShipForm(ftype, formids, state.planData.number + ftypename + "-配货单");
  } else if (ftype == 'label') {
    shipmentQuotaApi.downPDFLabel({"formid": state.planData.id}, state.planData.number + "-");
  } else if (ftype == 'excel') {
    shipmentQuotaApi.downExcelLabel({"formid": state.planData.id}, state.planData.number + "-");
  }
}

后端实现:对应 ShipQuotaController 相关方法,生成和下载各种文档。

4. 前端 API 调用

4.1 API 模块引入

import shipmentBoxApi from '@/api/erp/shipv2/shipmentBoxApi.js';
import addressApi from '@/api/amazon/inbound/addressApi.js';
import shipmentplanApi from '@/api/erp/shipv2/shipmentplanApi.js';
import shipmentQuotaApi from '@/api/erp/shipv2/shipmentQuotaApi.js';

4.2 核心 API 调用

API 方法 功能描述 参数说明 后端对应方法
shipmentplanApi.getPlanInfo 获取货件计划详情 {formid: 计划ID} ShipInboundPlanV2Controller.getPlanInfoAction
shipmentplanApi.changeInboundPlan 提交配货信息 {货件计划对象} ShipInboundPlanV2Controller.changeInboundPlanAction
shipmentplanApi.changeAddress 修改发货地址 {formid: 计划ID, addressid: 地址ID} ShipInboundPlanV2Controller.changeAddressAction
shipmentplanApi.updatePlanItem 更新商品信息 {商品对象} ShipInboundPlanV2Controller.updatePlanItemAction
shipmentplanApi.doneInboundPlan 完成配货 {formid: 计划ID} ShipInboundPlanV2Controller.doneInboundPlan
shipmentplanApi.confirmInboundPlan 确认配货 {formid: 计划ID} ShipInboundPlanV2Controller.confirmInboundPlan
addressApi.getAddress 获取地址列表 {companyid: 公司ID} AddressController.getAddressList
shipmentQuotaApi.downPDFShipForm 下载配货单 {ftype: 类型, formids: 计划ID列表, filename: 文件名} ShipQuotaController.downPDFShipForm
shipmentQuotaApi.downPDFLabel 下载标签 {formid: 计划ID} ShipQuotaController.downPDFLabel

5. 后端 API 实现

5.1 核心控制器:ShipInboundPlanV2Controller

控制器路径com.wimoor.amazon.inboundV2.controller.ShipInboundPlanV2Controller

核心方法

方法名 URL 功能描述 请求方式
getPlanInfoAction /api/v2/shipInboundPlan/getPlanInfo 获取货件计划详情 GET
changeInboundPlanAction /api/v2/shipInboundPlan/changeInboundPlan 提交配货信息 POST
changeAddressAction /api/v2/shipInboundPlan/changeAddress 修改发货地址 GET
updatePlanItemAction /api/v2/shipInboundPlan/updatePlanItem 更新商品信息 POST
doneInboundPlan /api/v2/shipInboundPlan/doneInboundPlan 完成配货 GET
confirmInboundPlan /api/v2/shipInboundPlan/confirmInboundPlan 确认配货 GET

示例实现

@ApiOperation(value = "获取货件计划")
@GetMapping("/getPlanInfo")
public Result<ShipPlanVo> getPlanInfoAction(String formid) {
    UserInfo user = UserInfoContext.get();
    try {
        if (StrUtil.isEmptyIfStr(formid)) {
            throw new BizException("计划ID不能为空");
        }
        ShipPlanVo vo = shipInboundPlanV2Service.getPlanBaseInfo(formid, user);
        return Result.success(vo);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return Result.success(null);
}

6. 数据模型

6.1 核心实体类

ShipInboundPlan(货件计划)

表名t_erp_ship_v2_inboundplan

字段名 数据类型 描述
id String 计划ID
name String 计划名称
number String 计划编码
source_address String 发货地址ID
groupid String 店铺ID
marketplaceid String 站点ID
amazonauthid String 授权ID
warehouseid String 仓库ID
auditstatus Integer 审核状态
shopid String 公司ID
createtime Date 创建时间
creator String 创建人

ShipInboundItem(货件商品)

表名t_erp_ship_v2_inbounditem

字段名 数据类型 描述
id String 商品ID
formid String 计划ID
sku String 平台SKU
msku String ERP本地SKU
quantity Integer 订单数量
confirm_quantity Integer 发货量
label_owner String 贴标责任人
prep_owner String 预备信息处理人
expiration Date 过期日期

6.2 数据传输对象 (DTO)

ShipPlanListDTO

用于查询货件计划列表的参数对象,包含分页信息和查询条件。

ShipFormDTO

用于发货库存锁定和出库操作的参数对象,包含仓库ID、商品列表等信息。

7. 业务流程

7.1 配货流程

  1. 初始化:页面加载时,通过 URL 参数获取计划 ID,调用 shipmentplanApi.getPlanInfo 获取货件计划详情。

  2. 配货操作

    • 用户查看商品列表,确认配货数量
    • 可以修改商品的配货数量和状态
    • 可以修改发货地址
  3. 完成配货

    • 用户点击「完成配货」按钮
    • 调用 shipmentplanApi.doneInboundPlan 更新货件计划状态
    • 状态更新成功后,跳转到下一个发货步骤(装箱)

7.2 地址修改流程

  1. 打开地址选择弹窗:用户点击「修改地址」按钮,打开地址选择弹窗。

  2. 获取地址列表:调用 addressApi.getAddress 获取用户的地址列表。

  3. 选择地址:用户从列表中选择一个地址。

  4. 更新地址:调用 shipmentplanApi.changeAddress 更新货件计划的发货地址。

  5. 刷新数据:地址更新成功后,刷新页面数据,显示新的发货地址。

7.3 商品编辑流程

  1. 打开编辑弹窗:用户点击商品行的「编辑」按钮,打开编辑弹窗。

  2. 修改信息:用户修改商品的配货数量、贴标信息等。

  3. 保存修改:用户点击「保存」按钮,调用 shipmentplanApi.updatePlanItem 更新商品信息。

  4. 刷新数据:商品信息更新成功后,刷新页面数据,显示新的商品信息。

8. 技术要点

8.1 前端技术要点

  1. Composition API:使用 Vue 3 的 Composition API 组织代码,提高代码可读性和可维护性。

  2. 响应式状态管理:使用 reactiveref 管理页面状态,确保数据变化能够及时反映到 UI 上。

  3. 组件通信:使用 refemit 实现组件之间的通信,如头部组件和主页面之间的交互。

  4. 生命周期钩子:使用 onMounted 钩子在页面加载时初始化数据。

  5. 异步操作处理:使用 async/await 和 Promise 处理 API 调用等异步操作。

8.2 后端技术要点

  1. RESTful API 设计:遵循 RESTful 设计风格,使用合适的 HTTP 方法和状态码。

  2. 事务管理:使用 @Transactional 注解管理事务,确保数据操作的一致性。

  3. 用户认证:使用 UserInfoContext 获取当前用户信息,实现权限控制。

  4. 数据校验:使用 @Valid@NotNull 等注解进行数据校验,确保数据的合法性。

  5. 异常处理:使用 try-catch 捕获和处理异常,返回友好的错误信息。

9. 常见问题与解决方案

9.1 常见问题

问题描述 可能原因 解决方案
页面加载失败 计划 ID 不存在或无权限 检查 URL 参数是否正确,确认用户权限
配货数量修改失败 库存不足或参数错误 检查库存是否充足,确认参数格式正确
地址修改失败 地址 ID 不存在或无权限 检查地址是否存在,确认用户权限
文档下载失败 生成文档时出错 检查服务器状态,确认参数正确

9.2 性能优化建议

  1. 前端优化

    • 使用虚拟滚动处理大量商品数据
    • 合理使用缓存,减少重复 API 调用
    • 优化组件渲染,避免不必要的重渲染
  2. 后端优化

    • 使用索引优化数据库查询
    • 合理使用缓存,减少数据库访问
    • 优化 API 响应时间,提高并发处理能力

10. 代码优化建议

10.1 前端代码优化

  1. 代码结构优化

    • 将复杂的业务逻辑拆分为多个子函数,提高代码可读性
    • 使用自定义 Hook 封装重复的逻辑,如 API 调用、数据处理等
  2. 性能优化

    • 使用 computed 缓存计算结果,避免重复计算
    • 使用 watchEffect 替代 watch,减少不必要的依赖追踪
  3. 错误处理优化

    • 统一处理 API 错误,提供友好的错误提示
    • 使用 try-catch 捕获异步操作中的错误

10.2 后端代码优化

  1. 代码结构优化

    • 将业务逻辑从控制器中分离到服务层,提高代码可维护性
    • 使用 DTO 封装请求和响应数据,避免直接使用实体类
  2. 性能优化

    • 使用批量操作减少数据库访问次数
    • 合理使用索引,优化查询性能
  3. 安全性优化

    • 加强参数校验,防止 SQL 注入等攻击
    • 使用 HTTPS 加密传输数据,提高安全性

11. 总结

配货页面是 Wimoor ERP 系统中 FBA 发货流程的重要组成部分,主要负责商品的配货操作和管理。通过本文档的详细解析,我们可以了解到:

  1. 页面结构:采用模块化设计,包含头部信息、地址管理、商品列表等核心模块。

  2. 功能实现:通过前端 API 调用和后端控制器的配合,实现了货件计划详情获取、配货信息提交、地址修改、商品编辑等核心功能。

  3. 数据模型:使用 ShipInboundPlanShipInboundItem 等实体类,构建了完整的数据模型体系。

  4. 业务流程:清晰的配货流程,从初始化到完成配货,每一步都有明确的操作和状态管理。

  5. 技术要点:使用了 Vue 3 + Composition API + Element Plus 等前端技术,以及 Spring Boot + MyBatis Plus 等后端技术,构建了一个高效、可靠的配货管理系统。

通过不断优化和改进,配货页面将为用户提供更加便捷、高效的配货体验,助力企业更好地管理 FBA 发货流程。

发货-货件处理

发货-货件处理模块详细帮助文档

1. 功能概述

发货-货件处理模块是 Wimoor 系统中 FBA 发货流程的核心环节,用于处理和管理发货计划中的货件。该模块位于系统的 ERP 模块中,主要负责货件的信息展示、操作管理、状态更新等功能。

1.1 主要功能

2. 前端组件结构

2.1 组件层级

shipment_handing/
├── shipstep/
│   ├── components/
│   │   ├── shipment_operator.vue (货件操作组件)
│   │   ├── shipment_info.vue (货件信息组件)
│   │   └── destination.vue (箱标地址组件)

2.2 核心组件

组件名称 文件路径 功能描述
shipment_operator wimoor666\wimoor-ui\src\views\erp\shipv2\shipment_handing\shipstep\components\shipment_operator.vue 货件操作组件,提供货件的删除、复制等操作
shipment_info wimoor666\wimoor-ui\src\views\erp\shipv2\shipment_handing\shipstep\components\shipment_info.vue 货件信息组件,展示货件的详细信息
destination wimoor666\wimoor-ui\src\views\erp\shipv2\shipment_handing\shipstep\components\destination.vue 箱标地址组件,用于管理货件的箱标收货地址

2.3 组件功能详解

2.3.1 shipment_operator 组件

功能:提供货件的操作管理功能 核心功能

关键 API 调用

2.3.2 shipment_info 组件

功能:展示货件的详细信息 核心功能

关键 API 调用

3. 后端代码结构

3.1 核心控制器

控制器名称 文件路径 功能描述
ShipInboundPlanPlacementV2Controller wimoor666\wimoor666\wimoor-amazon\amazon-boot\src\main\java\com\wimoor\amazon\inboundV2\controller\ShipInboundPlanPlacementV2Controller.java 货件处理核心控制器,处理货件的各种操作
ShipFormController wimoor666\wimoor666\wimoor-amazon\amazon-boot\src\main\java\com\wimoor\amazon\inbound\controller\ShipFormController.java 货件表单控制器,处理货件的创建、更新等操作

3.2 核心服务

服务名称 文件路径 功能描述
IShipInboundShipmentService 货件服务接口,定义货件相关的业务逻辑
ShipInboundShipmentServiceImpl 货件服务实现,实现货件相关的业务逻辑
IFulfillmentInboundService 亚马逊入库服务接口,定义与亚马逊入库相关的操作
FulfillmentInboundServiceImpl 亚马逊入库服务实现,实现与亚马逊入库相关的操作

3.3 数据模型

实体类 文件路径 功能描述
ShipInboundShipment 货件实体,存储货件的基本信息
ShipInboundBox 箱子实体,存储箱子的信息
ShipInboundCase 箱子内容实体,存储箱子中的商品信息
ShipInboundDestinationAddress 目的地地址实体,存储货件的收货地址信息

4. API 调用关系

4.1 前端 API 调用

前端 API 方法 用途 后端对应接口
shipmenthandlingApi.requestInboundShipment() 获取亚马逊货件状态 ShipFormController.requestInboundShipmentAction()
shipmenthandlingApi.disableShipment() 删除货件 ShipFormController.cancelShipmentAction()
shipmenthandlingApi.localDoneShipment() 标记货件为本地已完成 对应后端服务方法
shipmentPlacementApi.getBaseInfo() 获取货件详细信息 ShipInboundPlanPlacementV2Controller.getBaseInfoAction()
shipmentPlacementApi.saveDestinationBox() 保存箱标收货地址 ShipInboundPlanPlacementV2Controller.saveDestinationBoxAction()

4.2 后端 API 接口

后端接口 路径 功能描述
getBaseInfoAction() /api/v2/shipInboundPlan/shipment/getBaseInfo 获取货件的详细信息
saveDestinationBoxAction() /api/v2/shipInboundPlan/shipment/saveDestinationBox 保存货件的箱标收货地址
requestInboundShipmentAction() /api/v1/shipForm/requestInboundShipment 获取亚马逊货件状态
cancelShipmentAction() /api/v1/shipForm/cancelShipment 取消货件
getPkgLabelUrlAction() /api/v2/shipInboundPlan/shipment/getPkgLabelUrl 获取箱子标签的下载 URL

5. 业务流程

5.1 货件信息加载流程

  1. 页面加载:用户进入货件处理页面
  2. 获取货件 ID:从 URL 参数中获取货件 ID
  3. 加载货件信息:调用 shipmentPlacementApi.getBaseInfo() 获取货件详细信息
  4. 渲染页面:使用获取到的信息渲染页面各个组件
  5. 计算费用:计算货件的总货值和物流费用

5.2 货件删除流程

  1. 用户点击删除按钮:触发删除货件操作
  2. 获取货件状态:调用 shipmenthandlingApi.requestInboundShipment() 获取亚马逊后台货件状态
  3. 显示确认对话框:根据货件状态显示确认删除对话框
  4. 用户确认删除:用户选择删除方式(仅删除本地或同步删除亚马逊货件)
  5. 执行删除操作:调用 shipmenthandlingApi.disableShipment() 执行删除操作
  6. 更新页面:删除完成后更新页面显示

5.3 箱标地址管理流程

  1. 用户点击添加箱标地址:触发添加箱标地址操作
  2. 打开地址选择对话框:显示地址选择对话框
  3. 用户选择地址:用户从地址列表中选择箱标收货地址
  4. 保存箱标地址:调用 shipmentPlacementApi.saveDestinationBox() 保存箱标地址
  5. 更新页面:保存完成后更新页面显示

5.4 货件复制流程

  1. 用户点击复制按钮:触发复制货件操作
  2. 系统跳转:系统跳转到添加货件页面
  3. 创建新货件:基于原货件信息创建新的货件

5.5 本地完成流程

  1. 用户点击本地完成按钮:触发本地完成操作
  2. 显示确认对话框:显示确认本地完成的对话框
  3. 用户确认操作:用户确认执行本地完成操作
  4. 执行本地完成:调用 shipmenthandlingApi.localDoneShipment() 执行本地完成操作
  5. 更新页面:操作完成后更新页面显示

6. 核心代码分析

6.1 前端核心代码

6.1.1 获取货件信息

function getBaseInfo(shipmentid) {
  shipmentPlacementApi.getBaseInfo({
    "shipmentid": shipmentid
  }).then(res => {
    if (res.data) {
      var data = res.data;
      shipmentInfo.value = data.shipmentAll;
      shipment.value = data.shipment;
      plan.value = data.plan;
      volume.value = data.totalBoxSize;
      shiptype.value = data.shipment.transtyle;
      // 设置箱子数量
      if (data.listbox && data.listbox.length > 0) {
        boxnum.value = data.listbox.length;
      } else {
        boxnum.value = data.shipment.boxnum;
      }
      // 设置总货值
      if (data.detail) {
        totalprice.value = "¥" + getValue(parseFloat(data.detail.itemprice));
      }
      // 设置地址信息
      if (data["fromAddress"]) {
        fromAddress.value = data["fromAddress"];
      }
      if (data["toAddress"]) {
        toAddress.value = data["toAddress"];
      }
      if (data["toAddressBox"]) {
        toAddressBox.value = data["toAddressBox"];
      }
      // 设置物流费用
      if (data.transinfo == null || data.transinfo == undefined) {
        totalfee.value = "0";
      } else {
        if (!data.transinfo.otherfee) data.transinfo.otherfee = 0;
        if (!data.transinfo.transweight) data.transinfo.transweight = 0;
        if (!data.transinfo.singleprice) data.transinfo.singleprice = 0;
        totalfee.value = ("¥" + formatFloat(parseFloat(parseFloat(data.transinfo.transweight) * parseFloat(data.transinfo.singleprice)) + parseFloat(data.transinfo.otherfee)));
      }
      // 设置重量信息
      boxweightvalue.value = data.totalweight;
      if (data.transinfo && data.transinfo.transweight) {
        if (data.transinfo.wunit) {
          weightvalue.value = (data.transinfo.transweight + "" + getValue(data.transinfo.wunit));
        } else {
          if (data.transchannel.priceunits == "weight") {
            weightvalue.value = (data.transinfo.transweight + "kg");
          } else {
            weightvalue.value = (data.transinfo.transweight + "cbm");
          }
        }
      }
      // 设置实际重量
      if (data.detail) {
        readyweightvalue.value = (getValue(data.detail.readweight));
      }
      emit("change", data);
    }
  })
}

6.1.2 删除货件

function deleteShipment() {
  // 先弹窗打开modal 获取最新的货件状态
  dialogVisible.value = true;
  statusLoading.value = true;
  var status = shipDatas.value.shipmentstatus;
  shipmenthandlingApi.requestInboundShipment({
    "shipmentid": shipmentid
  }).then(res => {
    if (res.data != "fail") {
      status = res.data;
      shipstatus.value = res.data;
      if (status != "CANCELLED") {
        visibleBtn.value = "";
        canceltitle.value = "亚马逊后台货件状态为" + status + ",请确认是否同步删除亚马逊货件?";
      } else {
        canceltitle.value = "亚马逊后台货件状态为" + status + ",请确认是否删除本地货件?";
      }
    } else {
      visibleBtn.value = "";
      canceltitle.value = "亚马逊后台货件状态无法判断,请选择仅删除本地货件。";
    }
    statusLoading.value = false;
    emit("change");
  })
}

function confirmDelete(ftype) {
  var nowstatus = "";
  if (ftype == "local") {
    nowstatus = "DELETED";
  } else {
    nowstatus = shipstatus.value;
  }
  confirmCancelLoading.value = true;
  shipmenthandlingApi.disableShipment({
    "shipmentid": shipmentid,
    "shipmentStatus": nowstatus,
    "disableShipment": "1"
  }).then(res => {
    ElMessage.success('操作成功!');
    confirmCancelLoading.value = false;
    dialogVisible.value = false;
    context.emit("change");
  }).catch(error => {
    confirmCancelLoading.value = false;
  })
}

6.2 后端核心代码

6.2.1 获取货件详细信息

@GetMapping("/getBaseInfo")
public Result<Map<String, Object>> getBaseInfoAction(@ApiParam("货件ID")@RequestParam String shipmentid) {
  ShipInboundShipment ship = null;
  if(shipmentid.contains("FBA")){
    ship=shipInboundShipmentV2Service.lambdaQuery().eq(ShipInboundShipment::getShipmentConfirmationId,shipmentid).one();
  }else{
    ship=shipInboundShipmentV2Service.lambdaQuery().eq(ShipInboundShipment::getShipmentid,shipmentid).one();
  }
  ShipInboundShipmenSummarytVo data = shipInboundShipmentV2Service.summaryShipmentItemWithoutItem(ship.getShipmentid());
  BeanUtil.copyProperties(ship, data,"itemList");
  ShipInboundPlan plan = shipInboundPlanV2Service.getById(ship.getFormid());
  if(plan!=null) {
    data.setMarketplaceid(plan.getMarketplaceid());
    data.setTranstyle(ship.getTranstyle());
    data.setCountryCode(marketplaceService.findMapByMarketplaceId().get(plan.getMarketplaceid()).getMarket());
  }
  List<ShipInboundShipment> shipments = shipInboundShipmentV2Service.lambdaQuery().eq(ShipInboundShipment::getFormid, ship.getFormid()).list();
  List<String> shipmentids=new LinkedList<String>();
  for(ShipInboundShipment shipment:shipments) {
    shipmentids.add(shipment.getShipmentid());
  }
  plan.setShipmentids(shipmentids);
  Map<String, Object> map = getItemPriceAction(ship.getShipmentid());
  SummaryShipmentVo detail = shipInboundShipmentV2Service.showPlanListByPlanid( ship.getShipmentid());
  map.put("detail", detail);
  map.put("plan", plan);
  ship.setShipmentstatus(shipInboundShipmentV2Service.getShipmentStatusName(ship.getShipmentstatus()));
  map.put("shipment", ship);
  map.put("shipmentid", ship.getShipmentid());
  ShipAddress fromAddress = shipAddressService.getById(plan.getSourceAddress());
  ShipInboundDestinationAddress toAddress = shipInboundShipmentV2Service.getToAddress(ship.getDestination());
  if(ship.getDestinationbox()!=null){
    ShipInboundDestinationAddress toAddressBox = shipInboundShipmentV2Service.getToAddress(ship.getDestinationbox());
    map.put("toAddressBox", toAddressBox);
  }
  map.put("toAddress", toAddress);
  map.put("fromAddress", fromAddress);
  map.put("shipmentAll",data);
  if(plan.getAreCasesRequired()!=null&&plan.getAreCasesRequired()==true){
    if((plan.getSubmitbox()==null||plan.getSubmitbox()==false)&&ship.getStatus()==3) {
      this.getEditBoxDetialCaseAction(map, ship, plan);
    }else {
      getBoxDetialCaseAction(map,ship,plan);
    }
  }else{
    if((plan.getSubmitbox()==null||plan.getSubmitbox()==false)&&ship.getStatus()==3) {
      this.getEditBoxDetialAction(map, ship, plan);
    }else {
      getBoxDetialAction(map,ship,plan);
    }
  }
  getShipAmazonInfoAction(map,ship);
  return Result.success(map);
}

6.2.2 保存箱标收货地址

@ApiOperation(value = "保存发货装箱箱标上的地址")
@SystemControllerLog("保存箱标地址")
@GetMapping("/saveDestinationBox")
public Result<String> saveDestinationBoxAction(String shipmentid,String destinationBox) {
  UserInfo user=UserInfoContext.get();
  ShipInboundShipment shipment = shipInboundShipmentV2Service.getById(shipmentid);
  if(destinationBox.equals("NA")){
    shipment.setDestinationbox(null);
  }else{
    shipment.setDestinationbox(destinationBox);
  }
  shipInboundShipmentV2Service.updateById(shipment);
  return Result.success();
}

7. 技术实现

7.1 前端技术

技术/框架 版本 用途
Vue 3.x 前端框架
Element Plus 最新版 UI 组件库
Icon Park 最新版 图标库
Axios 最新版 HTTP 客户端

7.2 后端技术

技术/框架 版本 用途
Spring Boot 最新版 后端框架
MyBatis Plus 最新版 ORM 框架
Swagger 最新版 API 文档
MySQL 最新版 数据库

8. 输入输出示例

8.1 获取货件信息

输入

const { data } = await shipmentPlacementApi.getBaseInfo({ shipmentid: "shipment123" });

输出

{
  "code": 0,
  "msg": "",
  "data": {
    "shipmentAll": {
      "number": "FBA202601230001",
      "name": "测试货件",
      "shipmentConfirmationId": "FBA1234567890",
      "referenceid": "REF123",
      "shipmentstatus": "WORKING",
      "groupname": "测试店铺",
      "country": "US",
      "warehouse": "深圳仓库",
      "remark": "测试货件",
      "skuamount": 2,
      "sumQuantity": 100
    },
    "shipment": {
      "shipmentid": "shipment123",
      "shipmentConfirmationId": "FBA1234567890",
      "destination": "destination1",
      "destinationbox": "destination2",
      "transtyle": "SP",
      "status": 3
    },
    "plan": {
      "id": "plan123",
      "sourceAddress": "address1",
      "marketplaceid": "US",
      "areCasesRequired": false
    },
    "detail": {
      "itemprice": 5000
    },
    "fromAddress": {
      "name": "发货地址",
      "addressline1": "深圳市南山区",
      "city": "深圳",
      "stateorprovincecode": "广东",
      "postalcode": "518000",
      "countrycode": "CN"
    },
    "toAddress": {
      "name": "收货地址",
      "addressLine1": "123 Main St",
      "city": "Los Angeles",
      "stateOrProvinceCode": "CA",
      "postalCode": "90001",
      "countryCode": "US"
    },
    "toAddressBox": {
      "name": "箱标收货地址",
      "addressLine1": "456 Oak St",
      "city": "New York",
      "stateOrProvinceCode": "NY",
      "postalCode": "10001",
      "countryCode": "US"
    },
    "totalweight": 25,
    "totalBoxSize": 0.5,
    "transinfo": {
      "transweight": 25,
      "singleprice": 10,
      "otherfee": 50
    }
  }
}

8.2 保存箱标收货地址

输入

const { data } = await shipmentPlacementApi.saveDestinationBox({ 
  shipmentid: "shipment123", 
  destinationBox: "destination2" 
});

输出

{
  "code": 0,
  "msg": "",
  "data": ""
}

8.3 删除货件

输入

const { data } = await shipmenthandlingApi.disableShipment({
  shipmentid: "shipment123",
  shipmentStatus: "CANCELLED",
  disableShipment: "1"
});

输出

{
  "code": 0,
  "msg": "",
  "data": true
}

9. 业务流程图

9.1 货件信息加载流程

flowchart TD
    A[用户进入货件处理页面] --> B[获取货件ID]
    B --> C[调用getBaseInfo API]
    C --> D[后端处理请求]
    D --> E[查询货件信息]
    E --> F[查询地址信息]
    F --> G[查询箱子信息]
    G --> H[计算费用信息]
    H --> I[返回货件详细信息]
    I --> J[前端渲染页面]
    J --> K[显示货件信息]

9.2 货件删除流程

flowchart TD
    A[用户点击删除按钮] --> B[调用requestInboundShipment API]
    B --> C[获取亚马逊货件状态]
    C --> D[显示确认删除对话框]
    D --> E{用户选择删除方式}
    E -->|仅删除本地| F[设置状态为DELETED]
    E -->|同步删除亚马逊货件| G[使用当前状态]
    F --> H[调用disableShipment API]
    G --> H
    H --> I[后端处理删除请求]
    I --> J[更新货件状态]
    J --> K[返回删除结果]
    K --> L[前端显示操作结果]
    L --> M[更新页面显示]

9.3 箱标地址管理流程

flowchart TD
    A[用户点击添加箱标地址] --> B[打开地址选择对话框]
    B --> C[用户选择箱标收货地址]
    C --> D[调用saveDestinationBox API]
    D --> E[后端处理保存请求]
    E --> F[更新货件的箱标地址]
    F --> G[返回保存结果]
    G --> H[前端显示操作结果]
    H --> I[更新页面显示]

10. 代码优化建议

10.1 前端优化

  1. 错误处理增强:添加更详细的错误处理和用户提示,特别是在 API 调用失败时
  2. 性能优化:对于大量数据的货件,考虑使用虚拟滚动技术,提高页面加载和滚动性能
  3. 代码组织:将复杂的业务逻辑拆分为更小的函数,提高代码可读性
  4. 用户体验:添加更多的加载状态和操作反馈,提升用户体验
  5. 代码复用:提取重复的代码为公共函数或组件
  6. 内存管理:清理定时器和事件监听器,避免内存泄漏

10.2 后端优化

  1. 性能优化:对于频繁查询的数据,考虑添加缓存机制,提高系统响应速度
  2. 错误处理:增强后端错误处理,提供更详细的错误信息,便于前端处理
  3. 事务管理:确保所有数据库操作都在事务中执行,保证数据一致性
  4. API 设计:统一 API 接口设计,使用 RESTful 风格的 API 设计
  5. 日志记录:添加详细的日志记录,便于问题排查
  6. 代码组织:优化代码结构,提高代码可读性和可维护性

11. 常见问题及解决方案

问题 原因 解决方案
货件状态同步失败 网络连接问题或亚马逊 API 限制 检查网络连接,稍后重试操作
箱标地址保存失败 地址信息不完整或格式错误 检查地址信息,确保所有必填字段都已填写
货件删除失败 亚马逊后台货件状态不允许删除 先确认亚马逊后台货件状态,再执行删除操作
页面加载缓慢 货件信息过多或网络延迟 优化网络连接,考虑分页加载大量数据
费用计算错误 物流信息不完整或汇率更新不及时 检查物流信息,确保汇率数据已更新

12. 功能亮点

12.1 完整的货件信息展示

系统提供了全面的货件信息展示,包括基本信息、运输信息、地址信息等,使用户能够一目了然地了解货件的详细情况。信息展示采用卡片式布局,结构清晰,视觉效果良好。

12.2 便捷的货件操作管理

系统提供了丰富的货件操作功能,包括删除、复制、本地完成等,操作流程简单直观,用户可以轻松完成各种货件管理任务。

12.3 智能的状态同步

系统能够与亚马逊后台同步货件状态,确保货件状态的准确性和实时性。在删除货件时,系统会先获取亚马逊后台的货件状态,然后根据状态提供不同的删除选项。

12.4 灵活的箱标地址管理

系统支持为货件添加箱标收货地址,用户可以根据需要灵活管理货件的收货地址,提高了系统的灵活性和适用性。

12.5 准确的费用计算

系统能够自动计算货件的物流费用和货值,帮助用户了解货件的成本情况。费用计算基于货件的重量、体积和物流方式等因素,计算结果准确可靠。

13. 结论

发货-货件处理模块是 Wimoor 系统中 FBA 发货流程的核心环节,为用户提供了全面、便捷的货件管理功能。该模块通过前端与后端的紧密协作,实现了货件信息的展示、操作管理、状态同步等功能,为用户的 FBA 发货流程提供了有力的支持。

该模块的实现展示了现代前端开发和后端开发的最佳实践,包括:

总体而言,发货-货件处理模块是一个设计精良、功能完善的业务组件,为 FBA 发货流程提供了重要的支持,帮助用户高效地管理和处理货件,提高了物流管理的效率和准确性。

货件-货件跟踪

货件跟踪模块功能解析文档

1. 模块架构概述

货件跟踪模块是 Wimoor 系统中用于管理和跟踪 FBA 货件整个生命周期的重要功能模块。该模块采用前后端分离架构,前端使用 Vue 3 + Element Plus 开发,后端采用 Java + MyBatis Plus 开发,支持多物流系统集成。

1.1 系统架构

flowchart TD
    A[前端界面] --> B[API调用]
    B --> C[后端服务]
    C --> D[物流系统API]
    D --> E[物流服务商]
    C --> F[数据库]
    E --> D

1.2 核心组件

组件 职责 技术实现
货件列表页面 展示和管理货件列表 Vue 3 + Element Plus
货件表格组件 展示货件详细信息 Vue 3 + Element Plus
物流信息弹窗 展示物流详情和轨迹 Vue 3 + Element Plus
后端服务 处理业务逻辑和数据 Java + Spring Boot
物流系统集成 与外部物流系统交互 RESTful API
数据库 存储货件和物流信息 MySQL

2. 前端代码分析

2.1 核心页面结构

2.1.1 货件处理列表页面 (index.vue)

// 状态标签切换逻辑
function handleClick(){
    if(activeName.value=="0"){
        obj.orderStatus = ""
        obj.hasexceptionnum=null;
    }else if(activeName.value=="1"){
        obj.orderStatus = 7
        obj.hasexceptionnum=null;
    }else if(activeName.value=="2"){
        obj.orderStatus = 5
        obj.hasexceptionnum=null;
    }else if(activeName.value=="3"){
        obj.orderStatus = 55
        obj.hasexceptionnum=null;
    }else if(activeName.value=="4"){
        obj.orderStatus = 6
        obj.hasexceptionnum=null;
    }else if(activeName.value=="5"){
        obj.orderStatus = 0
        obj.hasexceptionnum=null;
    }else if(activeName.value=="6"){
        obj.hasexceptionnum='ok';
        obj.orderStatus = 6
    }
    tableRef.value.getshipmentData(obj);
    headerRef.value.statusChange(obj);
}

2.1.2 货件表格组件 (table.vue)

// 加载货件数据
function loadtableData(params){
    params.groupid = parmashead.value.store;
    params.marketplaceid =parmashead.value.country;
    params.warehouseid =parmashead.value.warehouse;
    params.fbacenter=parmashead.value.fbacenter;
    if(parmashead.value.start!==undefined){
        params.fromdate = parmashead.value.start;
        params.enddate =parmashead.value.end;
    }else{
        const end = new Date()
        const start = new Date()
        start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
        params.fromdate =dateFormat(start);
        params.enddate =dateFormat(end);
    }
    params.auditstatus = parmashead.value.orderStatus;
    if(parmashead.value.seachtype!==undefined){
        params.searchtype =parmashead.value.seachtype;
    }else{
        params.searchtype ="sku";
    }
    params.search = parmashead.value.searchwords;
    params.company =parmashead.value.company;
    params.channel= parmashead.value.channel;
    params.transtype =parmashead.value.transtype;
    params.checkdown=parmashead.value.checkdown;
    params.checkinv=parmashead.value.checkinv;
    params.areCasesRequired=parmashead.value.areCasesRequired;
    params.hasexceptionnum=parmashead.value.hasexceptionnum;
    params.hasreferenceid=parmashead.value.hasreferenceid;
    var tagtypes=["primary","success","info","warning","danger"];
    shipmenthandlingApi.getshipList(params).then((res)=>{
        var indexv=0;
        res.data.records.forEach(itemv=>{
            if(oldcheckinv[itemv.checkinv]==undefined){
                indexv=(indexv+1)%5;
                itemv.tagtype=tagtypes[indexv];
                oldcheckinv[itemv.checkinv]=indexv;
            }else{
                itemv.tagtype=tagtypes[oldcheckinv[itemv.checkinv]];
            }
        });
        tableData.records = res.data.records;
        tableData.total =res.data.total;
    })
}

2.1.3 物流信息弹窗 (transinfo.vue)

// 加载物流详细信息
function loadTransDetialInfo(companyid,shipmentid,ordernum){
    var html="";
    loading.value=true;
    shipment.value="";
    zmData.value="";
    transportationApi.shipTransDetial({"companyid": companyid,"shipmentid":shipmentid,"ordernum":ordernum}).then(res=>{
        loading.value=false;
        if(res && res.data.ftype=="ZH"){
            systemType.value=res.data.ftype;
            loadZhApiDetail(res.data,companyid,shipmentid);
        }
        if(res && res.data.ftype=="ZM"){
            systemType.value=res.data.ftype;
            loadZmApiDetail(res.data);
        }
    })
    dialogTransVisible.value=true;
}

2.2 前端 API 调用

2.2.1 货件数据 API

2.2.2 物流信息 API

2.2.3 货件操作 API

2.3 前端状态管理

3. 后端代码分析

3.1 核心实体

3.1.1 货件实体 (Shipment.java)

3.2 核心服务

3.2.1 货件服务

3.2.2 物流服务

3.3 后端 API 接口

3.3.1 货件列表接口

3.3.2 物流详细信息接口

3.3.3 货件状态同步接口

4. 物流系统集成

4.1 支持的物流系统

4.2 物流系统 API 集成

4.2.1 API 调用流程

  1. 前端调用后端物流信息接口
  2. 后端根据物流系统类型调用相应的外部 API
  3. 外部物流系统返回物流信息
  4. 后端解析和处理物流信息
  5. 后端返回处理后的物流信息给前端
  6. 前端以统一的格式展示物流信息

4.2.2 数据格式处理

5. 业务流程分析

5.1 货件状态管理流程

flowchart TD
    A[用户选择状态标签] --> B[更新筛选条件]
    B --> C[调用货件列表API]
    C --> D[后端处理请求]
    D --> E[查询数据库]
    E --> F[返回货件列表数据]
    F --> G[前端渲染表格]

5.2 物流信息查看流程

flowchart TD
    A[用户点击查看物流按钮] --> B[获取货件信息]
    B --> C[调用物流详情API]
    C --> D[后端处理请求]
    D --> E[调用物流系统API]
    E --> F[获取物流信息]
    F --> G[解析物流信息]
    G --> H[返回处理后的物流信息]
    H --> I[前端渲染物流信息弹窗]

5.3 异常货件处理流程

flowchart TD
    A[用户点击异常状态图标] --> B[打开异常处理对话框]
    B --> C{用户选择处理方式}
    C -->|忽略异常| D[调用忽略异常API]
    C -->|重新同步| E[调用重新同步API]
    D --> F[后端处理请求]
    E --> F
    F --> G[更新货件状态]
    G --> H[返回处理结果]
    H --> I[前端更新页面]

6. 技术实现亮点

6.1 前端技术亮点

6.2 后端技术亮点

6.3 系统集成亮点

7. 代码优化建议

7.1 前端优化建议

  1. 性能优化

    • 使用虚拟滚动技术处理大量货件数据,提高页面加载和滚动性能
    • 实现数据缓存,减少重复 API 调用
    • 优化组件渲染,避免不必要的重渲染
  2. 代码组织

    • 将复杂的业务逻辑拆分为更小的函数,提高代码可读性
    • 使用 Pinia 或 Vuex 进行状态管理,简化组件间数据传递
    • 提取重复的代码为公共函数或组件
  3. 用户体验

    • 添加更多的加载状态和操作反馈,提升用户体验
    • 实现物流状态变更的实时通知
    • 优化表单验证和错误提示

7.2 后端优化建议

  1. 性能优化

    • 使用缓存机制缓存频繁查询的物流信息,提高系统响应速度
    • 优化数据库查询,使用索引提高查询效率
    • 实现异步处理,提高系统并发能力
  2. 代码组织

    • 优化代码结构,提高代码可读性和可维护性
    • 使用设计模式,如策略模式处理不同物流系统的 API 调用
    • 提取重复的代码为公共服务或工具类
  3. 系统稳定性

    • 增强错误处理,提供更详细的错误信息
    • 实现熔断机制,避免物流系统 API 故障影响整个系统
    • 添加日志记录,便于问题排查

7.3 架构优化建议

  1. 微服务架构

    • 考虑将物流服务拆分为独立的微服务,提高系统的可扩展性和可维护性
    • 使用服务发现和负载均衡,提高系统的可靠性
  2. API 网关

    • 引入 API 网关,统一管理和保护后端 API
    • 实现请求限流和熔断,提高系统的稳定性
  3. 数据存储

    • 考虑使用 NoSQL 数据库存储物流轨迹等半结构化数据
    • 实现数据分片,提高数据库的处理能力

8. 功能扩展建议

8.1 功能扩展

  1. 移动端支持

    • 开发移动端应用或响应式网页,支持在手机上查看物流信息
    • 实现物流状态变更的推送通知
  2. 更多物流系统集成

    • 集成更多的物流系统,如 FedEx、UPS、DHL 等
    • 提供物流系统 API 配置的可视化界面
  3. 数据分析功能

    • 实现物流数据的统计和分析,如运输时间、异常率等
    • 提供数据可视化图表,帮助用户分析物流性能
  4. 智能预警

    • 基于历史数据和规则,实现物流异常的智能预警
    • 提供预警通知和处理建议
  5. 多语言支持

    • 实现多语言界面,支持国际化业务需求
    • 支持不同国家和地区的物流规则和格式

8.2 技术扩展

  1. 使用 WebSocket

    • 实现实时物流状态更新,无需手动刷新页面
    • 提供更及时的物流轨迹变更通知
  2. 使用 AI 技术

    • 利用 AI 技术分析物流数据,预测可能的延误和异常
    • 提供智能的物流路径优化建议
  3. 区块链技术

    • 考虑使用区块链技术存储物流信息,提高数据的安全性和可追溯性
    • 实现物流过程的透明化和不可篡改

9. 总结

货件跟踪模块是 Wimoor 系统中一个功能完善、技术先进的模块,通过前后端的紧密配合,为用户提供了全面、实时的货件跟踪服务。该模块具有以下特点:

  1. 功能全面:支持货件状态管理、物流信息查看、物流轨迹跟踪、多物流系统集成等功能
  2. 技术先进:采用 Vue 3 + Element Plus 前端技术和 Java + Spring Boot 后端技术
  3. 用户友好:界面设计简洁直观,操作流程优化,物流信息展示清晰
  4. 系统稳定:完善的错误处理和异常管理机制,确保系统稳定运行
  5. 扩展性强:模块化设计,易于功能扩展和技术升级

通过不断优化和升级,货件跟踪模块将为用户提供更优质的 FBA 货件管理体验,帮助用户更高效地管理和跟踪货件的整个生命周期。

FBA每日库存

FBA每日库存模块功能解析文档

1. 系统架构

1.1 整体架构

FBA每日库存模块采用前后端分离架构,主要包含以下组件:

1.2 模块依赖关系

flowchart TD
    A[前端daily.vue] --> B[inventoryRptApi.js]
    B --> C[InventoryReportController]
    C --> D[FBAInventoryServiceImpl]
    D --> E[InventoryReportHisMapper]
    D --> F[ProductInfoService]
    E --> G[MySQL数据库]
    F --> H[产品信息数据库]

2. 前端实现

2.1 核心文件结构

└── src/
    └── views/
        └── amazon/
            └── inventory/
                └── fba/
                    └── daily.vue        # 每日库存主组件
    └── api/
        └── amazon/
            └── inventory/
                └── inventoryRptApi.js   # API接口定义
    └── components/
        └── header/
            ├── group.vue                # 产品组选择组件
            └── datepicker.vue           # 日期选择组件

2.2 前端核心代码分析

2.2.1 组件模板结构

<template>
    <div class="main-sty">
        <div class="con-header">
            <!-- 顶部操作栏 -->
            <el-row>
                <el-space>
                    <Group  @change="groupChange" defaultValue="" isproduct="ok"></Group>
                    <Datepicker longtime="ok" ref="datepickers" @changedate="changedate" />
                    <el-input  v-model="queryParams.sku" @input="handleQuery" clearable placeholder="请输入SKU" style="width: 250px;" class="input-with-select" >
                        <template #append>
                            <el-button @click="handleQuery" >
                                <el-icon class="ic-cen font-medium">
                                    <search/>
                                </el-icon>
                            </el-button>
                        </template>
                    </el-input>
                    <el-button type="primary" @click.stop="downloadExcel">导出</el-button>
                </el-space>
            </el-row>
        </div>
        <div class="con-body">
            <!-- 数据表格 -->
            <GlobalTable ref="globalTable"
                show-summary
                :summary-method="getSummaries"  
                :tableData="tableData"  height="calc(100vh - 210px)" @selectionChange='handleSelect' 
                :defaultSort="{ prop: 'sku', order: 'ascending' }"  @loadTable="loadTableData" :stripe="true"  
                style="width: 100%;margin-bottom:16px;">
                <template #field>
                    <!-- 产品信息列 -->
                    <el-table-column label="产品信息" prop="sku"   width="200" fixed='left' sortable="custom" show-overflow-tooltip>
                        <template #default="scope">
                            <div class="flex-center">
                                <el-image v-if="scope.row.image" :src="scope.row.image" class="img-40"  width="40" height="40"  ></el-image>
                                <el-image v-else :src="$require('empty/noimage40.png')"  class="img-40"  width="40" height="40"  ></el-image>
                                <div >
                                    <div>{{scope.row.pname}}</div>
                                    <p class="sku">{{scope.row.sku}} </p>
                                </div>
                            </div>
                        </template>
                    </el-table-column>
                    <!-- 仓库列 -->
                    <el-table-column label="仓库" prop="warehouse" fixed='left' width="120"  sortable="custom" />
                    <!-- 动态日期列 -->
                    <el-table-column :label="item.byday" :prop="item.field" v-for="item in fieldlist" min-width="120" sortable="custom"  />
                </template>
            </GlobalTable>
        </div>
    </div>
</template>

2.2.2 核心逻辑实现

// 数据初始化
let state = reactive({
    tableData: {records:[],total:0},
    queryParams:{
        sku:"",
    },
    isload:true,
    fieldlist:[],
    summary:{},
})

// 产品组变化处理
function groupChange(obj){
    state.queryParams.groupid=obj.groupid;
    if(obj.marketplaceid=="IEU"){
        state.queryParams.warehouse="EU";
    }else{
        state.queryParams.warehouse=obj.marketplaceid;
    }
    handleQuery();
}

// 日期变化处理
function changedate(dates){
    state.queryParams.fromdate=dates.start;
    state.queryParams.enddate=dates.end;
    if(state.isload==false){
        handleQuery();
    }
}

// 查询处理
function handleQuery(){
    state.isload=false;
    // 获取日期字段列表
    inventoryRptApi.getFBAInvDayDetailField(state.queryParams).then((res)=>{
        state.fieldlist=res.data;
        // 加载表格数据
        globalTable.value.loadTable(state.queryParams);
    });
}

// 加载表格数据
function loadTableData(params){
    inventoryRptApi.getFBAInvDayDetail(params).then(res=>{
        state.tableData.records=res.data.records;
        state.tableData.total=res.data.total;
        // 设置合计数据
        if(params.currentpage==1){
            if(res.data.total>0){
                state.summary=res.data.records[0].summary;
            }else{
                state.summary={};
            }
        }
    })
}

// 合计行计算
function getSummaries({columns,data}){
    var arr = ["合计"];
    columns.forEach((item,index)=>{
        if(index>=2){
            arr[index]=state.summary[item.label];
        }
    })
    return  arr
}

// 导出Excel
function downloadExcel(){
    inventoryRptApi.getFBAInvDayDetailExport(state.queryParams);
}

3. 后端实现

3.1 核心文件结构

└── src/
    └── main/
        └── java/
            └── com/
                └── wimoor/
                    └── amazon/
                        └── inventory/
                            ├── controller/
                            │   └── InventoryReportController.java   # 控制器
                            ├── service/
                            │   ├── IFBAInventoryService.java        # 服务接口
                            │   └── impl/
                            │       └── FBAInventoryServiceImpl.java # 服务实现
                            └── mapper/
                                └── InventoryReportHisMapper.java     # 数据访问层

3.2 后端核心代码分析

3.2.1 控制器层(InventoryReportController.java)

// 获取日期字段列表
@PostMapping(value = "getFBAInvDayDetailField")
public Result<List<Map<String, String>>> getFBAInvDayDetailFieldAction(@RequestBody InvDayDetailDTO query) {
    Map<String, Date> parameter = new HashMap<String, Date>();
    // 处理日期参数,默认最近7天
    // ...
    List<Map<String, String>> fieldlist = iFBAInventoryService.getInvDaySumField(parameter);
    return Result.success(fieldlist);
}

// 获取每日库存数据
@PostMapping(value = "getFBAInvDayDetail")
public Result<IPage<Map<String, Object>>> getFBAInvDayDetailAction(@RequestBody InvDayDetailDTO query) {
    Map<String, Object> parameter = new HashMap<String, Object>();
    // 设置查询参数
    // ...
    IPage<Map<String, Object>> list = iFBAInventoryService.getFBAInvDayDetail(query,parameter);
    // 添加合计数据
    if(query.getCurrentpage()==1) {
        Map<String, Object> summary = iFBAInventoryService.getFBAInvDayDetailTotal(parameter);
        if(list!=null&&list.getRecords().size()>0&&summary!=null) {
            list.getRecords().get(0).put("summary", summary);
        }
    }
    return Result.success(list);
}

// 导出Excel
@PostMapping("getFBAInvDayDetailExport")
public void getFBAInvDayDetailExport(@RequestBody InvDayDetailDTO query, HttpServletResponse response) {
    Map<String, Object> parameter = new HashMap<String, Object>();
    // 设置导出参数
    // ...
    try {
        SXSSFWorkbook workbook = new SXSSFWorkbook();
        response.setContentType("application/force-download");
        response.addHeader("Content-Disposition", "attachment;fileName=FBAInvDayDetail"+System.currentTimeMillis() + ".xlsx");
        ServletOutputStream fOut = response.getOutputStream();
        iFBAInventoryService.downloadFBAInvDayDetail(workbook, parameter);
        workbook.write(fOut);
        workbook.close();
        fOut.flush();
        fOut.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3.2.2 服务层(FBAInventoryServiceImpl.java)

// 生成日期字段列表
@Override
public List<Map<String, String>> getInvDaySumField(Map<String, Date> parameter) {
    List<Map<String, String>> list = new LinkedList<Map<String, String>>();
    Calendar calendar = Calendar.getInstance();
    Date endDate = parameter.get("endDate");
    Date beginDate = parameter.get("beginDate");
    calendar.setTime(endDate);
    // 遍历日期范围,生成字段列表
    for (Date step = calendar.getTime(); step.after(beginDate) || step.equals(beginDate); 
         calendar.add(Calendar.DATE, -1), step = calendar.getTime()) {
        String field = GeneralUtil.formatDate(step, GeneralUtil.FMT_YMD);
        Map<String, String> map = new HashMap<String, String>();
        map.put("byday", field);
        map.put("field", "v" + field);
        list.add(map);
    }
    return list;
}

// 获取每日库存数据
@Override
public IPage<Map<String, Object>> getFBAInvDayDetail(InvDayDetailDTO dto, Map<String, Object> parameter) {
    // 处理日期参数
    // ...
    List<Map<String, String>> fieldlist = getInvDaySumField(pmap);
    parameter.put("fieldlist", fieldlist);
    
    // 查询数据库
    List<Map<String, Object>> list = inventoryReportHisMapper.getFBAInvDayDetail(parameter);
    IPage<Map<String, Object>> pagelist = dto.getListPage(list);
    
    // 添加产品信息
    if (pagelist != null && pagelist.getTotal() > 0) {
        // ...
        for (Map<String, Object> pagemap : pagelist.getRecords()) {
            String sku_p = pagemap.get("sku").toString();
            Map<String, Object> product = iProductInfoService.findNameAndPicture(sku_p, marketplaceid, groupid);
            if (product != null) {
                pagemap.put("image", product.get("image"));
                pagemap.put("pname", product.get("name"));
            }
        }
    }
    return pagemap;
}

// 导出Excel实现
@Override
public void downloadFBAInvDayDetail(SXSSFWorkbook workbook, Map<String, Object> parameter) {
    // 处理日期参数
    // ...
    List<Map<String, String>> fieldlist = getInvDaySumField(pmap);
    parameter.put("fieldlist", fieldlist);
    
    // 查询数据
    List<Map<String, Object>> list = inventoryReportHisMapper.getFBAInvDayDetail(parameter);
    
    // 生成Excel
    Map<String, Object> titlemap = new LinkedHashMap<String, Object>();
    titlemap.put("sku", "SKU");
    titlemap.put("warehouse", "仓库");
    titlemap.put("pname", "名称");
    for(Map<String, String> itemfield:fieldlist) {
        titlemap.put(itemfield.get("field").toString(),itemfield.get("byday"));
    }
    
    // 创建Excel工作表和写入数据
    // ...
}

3.2.3 数据访问层(SQL实现)

动态生成的SQL示例:

SELECT 
    sku, 
    warehouse, 
    CASE WHEN byday = '2023-01-01' THEN quantity ELSE 0 END AS v20230101, 
    CASE WHEN byday = '2023-01-02' THEN quantity ELSE 0 END AS v20230102, 
    -- ... 更多日期列
    SUM(quantity) AS total
FROM 
    inventory_report_his
WHERE 
    byday BETWEEN #{beginDate} AND #{endDate}
    AND warehouse = #{warehouse}
    AND sku LIKE #{sku}
GROUP BY 
    sku, warehouse

4. 数据库设计

4.1 核心数据表

4.1.1 inventory_report_his(库存历史表)

字段名 数据类型 描述
id BIGINT 主键ID
sku VARCHAR(50) 产品SKU
warehouse VARCHAR(20) 仓库代码
byday DATE 统计日期
quantity INT 库存数量
shopid VARCHAR(32) 店铺ID
groupid VARCHAR(32) 产品组ID
created_at DATETIME 创建时间
updated_at DATETIME 更新时间

4.2 数据流程

  1. 数据同步:定时从亚马逊API获取FBA库存报告
  2. 数据处理:解析报告,生成每日库存快照
  3. 数据存储:将快照数据写入inventory_report_his表
  4. 数据查询:前端请求时,动态生成SQL查询数据
  5. 数据展示:前端根据返回结果动态渲染表格

5. API接口定义

5.1 接口列表

接口URL 请求方法 功能描述
/api/v1/inventoryRpt/getFBAInvDayDetailField POST 获取日期字段列表
/api/v1/inventoryRpt/getFBAInvDayDetail POST 获取每日库存数据
/api/v1/inventoryRpt/getFBAInvDayDetailExport POST 导出每日库存数据

5.2 请求参数(InvDayDetailDTO)

参数名 类型 描述
fromdate String 开始日期(YYYY-MM-DD)
enddate String 结束日期(YYYY-MM-DD)
groupid String 产品组ID
warehouse String 仓库代码
sku String SKU关键词
currentpage Integer 当前页码
pagesize Integer 每页条数

5.3 响应格式

{
  "code": 200,
  "msg": "success",
  "data": {
    "records": [
      {
        "sku": "ABC123",
        "warehouse": "US",
        "pname": "产品名称",
        "image": "产品图片URL",
        "v20230101": 100,
        "v20230102": 90,
        // ... 更多日期字段
        "summary": {
          "2023-01-01": 1000,
          "2023-01-02": 900,
          // ... 更多合计值
        }
      }
    ],
    "total": 100,
    "size": 20,
    "current": 1
  }
}

6. 关键技术点

6.1 动态日期列生成

6.2 高性能数据处理

6.3 日期范围处理

// 处理日期范围,生成连续日期列表
Calendar calendar = Calendar.getInstance();
calendar.setTime(endDate);
for (Date step = calendar.getTime(); step.after(beginDate) || step.equals(beginDate); 
     calendar.add(Calendar.DATE, -1), step = calendar.getTime()) {
    // 生成日期字段
    // ...
}

6.4 合计行实现

7. 性能优化

7.1 前端优化

  1. 虚拟滚动:避免一次性渲染大量数据,提高表格渲染速度
  2. 懒加载:按需加载数据,减少初始加载时间
  3. 防抖处理:搜索输入添加防抖,减少频繁请求

7.2 后端优化

  1. 索引优化:在inventory_report_his表的byday、sku、warehouse字段上建立联合索引
  2. 分页查询:使用MyBatis Plus的分页功能,避免全表扫描
  3. 动态SQL:根据查询条件动态生成SQL,减少不必要的字段查询
  4. 连接池优化:配置合适的数据库连接池参数,提高并发处理能力

7.3 数据库优化

  1. 分区表:对inventory_report_his表按日期进行分区,提高查询效率
  2. 定期归档:对历史数据进行归档,减少单表数据量
  3. 预计算:定时预计算常用日期范围的合计数据,提高查询速度

8. 最佳实践

8.1 代码规范

  1. 前端代码:遵循Vue 3 Composition API规范,组件化开发
  2. 后端代码:遵循Spring Boot最佳实践,分层架构清晰
  3. SQL代码:使用MyBatis动态SQL,避免硬编码

8.2 安全考虑

  1. 接口认证:所有API接口都需要进行身份认证和权限校验
  2. 参数校验:对所有输入参数进行严格校验,防止SQL注入
  3. 数据加密:敏感数据在传输和存储过程中进行加密处理

8.3 测试建议

  1. 单元测试:对核心业务逻辑进行单元测试
  2. 集成测试:测试前后端集成和API调用
  3. 性能测试:模拟大量数据场景,测试系统性能
  4. 兼容性测试:测试不同浏览器和设备的兼容性

9. 扩展建议

9.1 功能扩展

  1. 库存趋势图:添加库存变化趋势图表,直观展示库存变化
  2. 库存预警:根据历史数据设置库存预警阈值,及时提醒
  3. 多维度分析:支持按产品类别、品牌等维度进行库存分析
  4. 导出模板定制:支持自定义导出模板,满足不同需求

9.2 技术扩展

  1. 缓存机制:添加Redis缓存,提高查询速度
  2. 异步处理:使用消息队列处理大数据量导出请求
  3. 实时数据:添加WebSocket支持,实现实时库存更新
  4. 数据分析:集成数据分析工具,提供更深入的库存分析

10. 总结

FBA每日库存模块是Wimoor系统中重要的库存管理功能,采用了前后端分离架构,具有高性能、高扩展性的特点。通过动态日期列生成、多维度筛选和数据导出等功能,帮助卖家实时掌握库存动态,优化库存管理策略。

该模块的设计和实现遵循了现代软件 engineering 最佳实践,具有良好的可维护性和可扩展性。在未来的发展中,可以进一步扩展功能,提高系统性能,为卖家提供更全面、更深入的库存管理服务。


文档版本:v1.0 更新时间:2026-01-26 适用系统:Wimoor 6.0及以上版本

发货-发货详情(新)

发货详情报表模块(ShipV2)功能解析

1. 模块架构

1.1 前端架构

1.2 后端架构

1.3 数据流

前端用户操作 → 筛选条件设置 → API调用 → 后端控制器处理 → 服务层业务逻辑 → Mapper数据查询 → 数据库 → 数据返回 → 前端渲染

2. 前端实现

2.1 核心组件结构

<template>
  <div>
    <!-- 标签页切换 -->
    <el-tabs v-model="selecttype" @tab-change="handleQuery">
      <el-tab-pane label="按货件汇总" name="shipment"></el-tab-pane>
      <el-tab-pane label="按SKU汇总" name="sku"></el-tab-pane>
    </el-tabs>
    
    <!-- 筛选条件 -->
    <div class="filter-bar">
      <!-- 店铺选择 -->
      <el-select v-model="queryParam.groupid" @change="handleQuery">
        <!-- 选项 -->
      </el-select>
      
      <!-- 日期类型选择 -->
      <el-select v-model="queryParam.datetype" @change="handleQuery">
        <el-option label="创建日期" value="createdate"></el-option>
        <el-option label="发货日期" value="senddate"></el-option>
      </el-select>
      
      <!-- 日期范围选择 -->
      <el-date-picker v-model="dateRange" @change="handleQuery"></el-date-picker>
      
      <!-- 仓库选择 -->
      <el-select v-model="queryParam.warehouseid" @change="handleQuery">
        <el-option label="全部" value="all"></el-option>
        <!-- 选项 -->
      </el-select>
      
      <!-- 货件编码搜索 -->
      <el-input v-model="queryParam.search" @keyup.enter="handleQuery"></el-input>
      
      <!-- 高级筛选 -->
      <el-button @click="openFilter">筛选</el-button>
    </div>
    
    <!-- 操作按钮 -->
    <div class="operation-bar">
      <el-button @click="refresh">刷新</el-button>
      <el-button @click="handleExport">导出</el-button>
      <!-- 其他按钮 -->
    </div>
    
    <!-- 数据表格 -->
    <el-table v-loading="isLoading" :data="tableData.records">
      <!-- 列定义 -->
    </el-table>
    
    <!-- 分页 -->
    <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"></el-pagination>
  </div>
</template>

2.2 核心数据结构

2.2.1 响应式状态

const state = reactive({
  selecttype: 'shipment', // 当前选择的视图类型
  dateRange: [], // 日期范围
  queryParam: {
    groupid: undefined, // 店铺ID
    datetype: 'createdate', // 日期类型
    fromdate: '', // 开始日期
    enddate: '', // 结束日期
    warehouseid: 'all', // 仓库ID
    search: '', // 搜索关键词
    hasexceptionnum: '', // 接收异常
    carrierName: '' // 承运商
  },
  tableData: {
    records: [], // 表格数据
    total: 0 // 总记录数
  },
  isLoading: false, // 加载状态
  skutableData: {
    records: [], // SKU表格数据
    total: 0 // SKU总记录数
  },
  sumQty: 0, // 总发货数量
  sumReceivedQty: 0, // 总接收数量
  selectList: [] // 选中的记录
});

2.2.2 API接口

// reportV2Api.js
export default {
  getShipmentReportByLoistics, // 按物流商获取货件报表
  getShipmentReportByWarehouseLoistics, // 按仓库和物流商获取货件报表
  getShipmentDetailReport, // 获取货件详情报表
  getShipmentReport, // 获取货件报表
  downShipmentExcel, // 导出货件Excel
  downExcelShipmentReportByLoistics, // 导出物流商货件报表
  getShipmentReportByWarehouseLoistics
};

2.3 核心方法

2.3.1 数据加载方法

// 加载货件汇总数据
function loadTableData(params) {
  state.isLoading = true;
  reportApi.getShipmentReport(params).then((res) => {
    state.isLoading = false;
    state.tableData.records = res.data.records;
    state.tableData.total = res.data.total;
  });
}

// 加载SKU汇总数据
function skuloadTableData(params) {
  state.isLoading = true;
  reportApi.getShipmentDetailReport(params).then((res) => {
    state.isLoading = false;
    state.skutableData.records = res.data.records;
    state.skutableData.total = res.data.total;
    // 统计总发货和总接收数量
    state.sumQty = res.data.records.reduce((sum, item) => sum + (item.sendqty || 0), 0);
    state.sumReceivedQty = res.data.records.reduce((sum, item) => sum + (item.receivedqty || 0), 0);
  });
}

2.3.2 筛选条件处理

function handleQuery() {
  if (state.selecttype === 'shipment') {
    state.queryParam.pageNum = 1;
    loadTableData(state.queryParam);
  } else {
    state.queryParam.pageNum = 1;
    skuloadTableData(state.queryParam);
  }
}

function handleSizeChange(val) {
  state.queryParam.pageSize = val;
  handleQuery();
}

function handleCurrentChange(val) {
  state.queryParam.pageNum = val;
  handleQuery();
}

2.3.3 导出方法

function downloadList(ftype) {
  if (ftype == "shiptask") {
    // 导出发货处理任务量
    findProcessHandle({"fromdate":state.queryParam.fromdate,"enddate":state.queryParam.enddate});
  } else if (ftype == "shipqty") {
    // 导出发货数量简约版
    inventoryRptApi.downloadOutstockformOut({"fromdate":state.queryParam.fromdate,"enddate":state.queryParam.enddate});
  } else {
    // 其他导出类型
    state.queryParam.downloadType = ftype;
    reportApi.downShipmentExcel(state.queryParam, () => {
      state.downLoading = false;
    });
  }
}

2.4 前端交互逻辑

  1. 视图切换:通过selecttype状态和标签页切换,调用不同的数据加载方法
  2. 筛选条件:所有筛选条件变更都会触发handleQuery方法重新加载数据
  3. 分页:分页操作通过handleSizeChangehandleCurrentChange方法处理
  4. 导出:根据不同的导出类型调用不同的API接口
  5. 数据统计:SKU汇总视图下自动计算总发货和总接收数量

3. 后端实现

3.1 控制器层

3.1.1 核心API接口

API路径 方法 功能
/api/v2/ship/report/getShipmentReport POST 获取货件汇总报表
/api/v2/ship/report/getShipmentDetailReport POST 获取SKU汇总报表
/api/v2/ship/report/downShipmentExcel POST 导出货件报表Excel
/api/v2/ship/report/getShipmentReportByLoistics POST 按物流商获取货件报表
/api/v2/ship/report/getShipmentReportByWarehouseLoistics POST 按仓库和物流商获取货件报表
/api/v2/ship/report/downExcelShipmentReportByLoistics POST 导出物流商货件报表Excel

3.1.2 控制器方法实现

@PostMapping(value = "/getShipmentReport")
public Result<IPage<Map<String, Object>>> getShipmentReport(@RequestBody ShipInboundShipmenSummaryDTO dto) {
    Map<String, Object> param = new HashMap<>();
    // 参数处理
    UserInfo user = UserInfoContext.get();
    param.put("shopid", user.getCompanyid());
    param.put("marketplaceid", dto.getMarketplaceid());
    param.put("groupid", dto.getGroupid());
    param.put("search", dto.getSearch());
    param.put("datetype", dto.getDatetype());
    param.put("fromDate", dto.getFromdate());
    param.put("endDate", dto.getEnddate());
    param.put("warehouseid", dto.getWarehouseid());
    param.put("company", dto.getCompany());
    param.put("companyid", dto.getCompanyid());
    param.put("iserror", dto.getHasexceptionnum());
    
    // 调用服务层
    IPage<Map<String, Object>> pagelist = shipInboundPlanService.getShipmentReport(dto.getPage(), param);
    return Result.success(pagelist);
}

@PostMapping(value = "/downShipmentExcel")
public void downShipmentExcelAction(@RequestBody ShipInboundShipmenSummaryDTO dto, HttpServletResponse response) {
    try {
        // 创建Excel工作簿
        SXSSFWorkbook workbook = new SXSSFWorkbook();
        response.setContentType("application/force-download");
        response.addHeader("Content-Disposition", "attachment;fileName=Shipmenttemplate.xlsx");
        ServletOutputStream fOut = response.getOutputStream();
        
        // 参数处理
        UserInfo user = UserInfoContext.get();
        Map<String, Object> params = new HashMap<>();
        params.put("shopid", user.getCompanyid());
        params.put("datetype", dto.getDatetype());
        params.put("marketplaceid", dto.getMarketplaceid());
        params.put("groupid", dto.getGroupid());
        params.put("search", dto.getSearch());
        params.put("fromDate", dto.getFromdate());
        params.put("endDate", dto.getEnddate());
        params.put("ftype", dto.getDownloadType());
        
        // 调用服务层生成Excel
        shipInboundPlanService.setExcelBookByType(workbook, params);
        
        // 输出Excel
        workbook.write(fOut);
        workbook.close();
        fOut.flush();
        fOut.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3.2 服务层

3.2.1 服务接口定义

public interface IShipInboundPlanService extends IService<ShipInboundPlan>, IRunAmazonService {
    // 其他方法...
    
    /**
     * 获取货件详情报表
     */
    IPage<Map<String, Object>> getShipmentDetailReport(Page<Object> page, Map<String, Object> param);
    
    /**
     * 获取货件报表
     */
    IPage<Map<String, Object>> getShipmentReport(Page<Object> page, Map<String, Object> param);
    
    /**
     * 按类型设置Excel工作簿
     */
    void setExcelBookByType(SXSSFWorkbook workbook, Map<String, Object> params);
}

3.2.2 服务实现

@Override
public IPage<Map<String, Object>> getShipmentReport(Page<Object> page, Map<String, Object> param) {
    return this.baseMapper.getShipmentReport(page, param);
}

@Override
public IPage<Map<String, Object>> getShipmentDetailReport(Page<Object> page, Map<String, Object> param) {
    return this.baseMapper.getShipmentDetailReport(page, param);
}

@Override
public void setExcelBookByType(SXSSFWorkbook workbook, Map<String, Object> params) {
    String type = params.get("ftype").toString();
    if ("shipment".equals(type)) {
        // 货件汇总导出
        Map<String, Object> titlemap = new LinkedHashMap<>();
        titlemap.put("ShipmentId", "货件编码");
        titlemap.put("groupname", "发货店铺");
        titlemap.put("warehousename", "发货仓库");
        // 更多列定义...
        
        List<Map<String, Object>> list = this.baseMapper.getShipmentReport(params);
        Sheet sheet = workbook.createSheet("sheet1");
        // 写入表头和数据
    } else if ("sku".equals(type)) {
        // SKU汇总导出
        // 类似实现...
    }
}

3.3 数据访问层

3.3.1 Mapper接口

public interface ShipInboundPlanV2Mapper extends BaseMapper<ShipInboundPlan> {
    // 其他方法...
    
    /**
     * 获取货件报表
     */
    IPage<Map<String, Object>> getShipmentReport(Page<Object> page, @Param("param") Map<String, Object> param);
    
    /**
     * 获取货件详情报表
     */
    IPage<Map<String, Object>> getShipmentDetailReport(Page<Object> page, @Param("param") Map<String, Object> param);
    
    /**
     * 获取货件报表(导出用)
     */
    List<Map<String, Object>> getShipmentReport(@Param("param") Map<String, Object> param);
    
    /**
     * 获取货件详情报表(导出用)
     */
    List<Map<String, Object>> getShipmentDetailReport(@Param("param") Map<String, Object> param);
}

3.3.2 SQL实现

getShipmentReport(货件汇总)

<select id="getShipmentReport" parameterType="java.util.Map" resultType="java.util.Map">
    SELECT
        s.ShipmentId,
        ag.name AS groupname,
        wh.name AS warehousename,
        s.createdate,
        s.senddate,
        s.receivedate,
        s.completedate,
        s.destination,
        s.marketplace,
        s.warehouseid,
        s.estimateddeliverydate,
        s.carrierName,
        s.shippingchannel,
        SUM(i.receivedqty) AS receivedqty,
        SUM(i.quantity) AS sendqty,
        SUM(s.fee) AS feecount,
        s.weight,
        s.weightfee,
        s.otherfee,
        s.actualweight,
        s.estimateweight,
        s.boxactualweight,
        s.boxvolumeweight
    FROM
        t_erp_ship_v2_inboundplan s
    LEFT JOIN
        t_amazon_group ag ON ag.id = s.groupid
    LEFT JOIN
        t_erp_warehouse wh ON wh.id = s.warehouseid
    LEFT JOIN
        t_erp_ship_v2_inbounditem i ON i.formid = s.id
    WHERE
        s.shopid = #{param.shopid}
        <if test="param.marketplaceid != null">
            AND s.marketplaceid = #{param.marketplaceid}
        </if>
        <if test="param.groupid != null">
            AND s.groupid = #{param.groupid}
        </if>
        <if test="param.search != null">
            AND s.ShipmentId LIKE #{param.search}
        </if>
        <!-- 更多条件... -->
    GROUP BY
        s.ShipmentId
    ORDER BY
        s.createdate DESC
</select>

getShipmentDetailReport(SKU汇总)

<select id="getShipmentDetailReport" parameterType="java.util.Map" resultType="java.util.Map">
    SELECT
        i.sku,
        s.ShipmentId,
        s.number,
        ag.name AS groupname,
        s.destination,
        s.warehouseid,
        wh.name AS warehousename,
        s.senddate,
        s.receivedate,
        s.shippingchannel,
        s.estimateddeliverydate,
        i.quantity AS sendqty,
        i.receivedqty,
        s.status,
        s.createdate
    FROM
        t_erp_ship_v2_inboundplan s
    LEFT JOIN
        t_amazon_group ag ON ag.id = s.groupid
    LEFT JOIN
        t_erp_warehouse wh ON wh.id = s.warehouseid
    LEFT JOIN
        t_erp_ship_v2_inbounditem i ON i.formid = s.id
    WHERE
        s.shopid = #{param.shopid}
        <if test="param.marketplaceid != null">
            AND s.marketplaceid = #{param.marketplaceid}
        </if>
        <if test="param.groupid != null">
            AND s.groupid = #{param.groupid}
        </if>
        <if test="param.search != null">
            AND (s.ShipmentId LIKE #{param.search} OR i.sku LIKE #{param.search})
        </if>
        <!-- 更多条件... -->
    ORDER BY
        s.createdate DESC
</select>

4. 功能特性

4.1 双视图模式

4.2 多维度筛选

筛选条件 说明 数据类型
店铺 选择特定店铺 下拉选择
日期类型 创建日期/发货日期 下拉选择
日期范围 选择开始和结束日期 日期选择器
仓库 选择特定仓库 下拉选择
货件编码 搜索特定货件 文本输入
接收异常 筛选有/无接收异常的货件 下拉选择
承运商 选择特定承运商 下拉选择

4.3 数据导出

导出类型 说明 适用场景
普通导出 完整的货件或SKU数据 全面分析
含子SKU 包含子SKU的详细数据 变体产品分析
发货数量简约版 只包含发货数量的简化数据 快速统计
发货处理任务量 发货处理任务量统计 任务分配

4.4 数据统计

5. 技术亮点

5.1 前端技术亮点

  1. Vue 3 Composition API:使用reactive和ref进行状态管理,代码结构更清晰
  2. Element Plus组件:使用el-tabs、el-select、el-table等组件,界面美观且功能丰富
  3. 响应式设计:适配不同屏幕尺寸,操作流畅
  4. 异步数据加载:使用Axios进行异步请求,配合loading状态提升用户体验
  5. 条件渲染:根据不同视图类型和筛选条件动态渲染UI

5.2 后端技术亮点

  1. RESTful API设计:使用POST请求传递复杂参数,接口设计规范
  2. SXSSFWorkbook:使用SXSSFWorkbook处理大数据量Excel导出,避免内存溢出
  3. 参数校验:对输入参数进行非空检查和格式转换
  4. 多表关联查询:使用SQL多表关联查询,提高数据查询效率
  5. 动态SQL:使用MyBatis Plus的动态SQL,根据条件构建不同的查询语句

5.3 性能优化

  1. 分页查询:使用MyBatis Plus的分页功能,避免一次性加载过多数据
  2. 索引优化:数据库表建立适当索引,提高查询速度
  3. 缓存机制:使用Spring Cache缓存常用数据
  4. 批量操作:批量处理数据,减少数据库交互次数
  5. 异步处理:Excel导出等耗时操作使用异步处理

6. 代码优化建议

6.1 前端优化建议

  1. 代码模块化:将筛选条件、表格渲染等功能拆分为独立组件
  2. 状态管理:考虑使用Pinia或Vuex进行更复杂的状态管理
  3. 防抖处理:对搜索输入添加防抖处理,减少频繁请求
  4. 虚拟滚动:对大数据量表格使用虚拟滚动,提高渲染性能
  5. 错误处理:添加更完善的错误处理和用户提示

6.2 后端优化建议

  1. 参数验证:使用Spring Validation对请求参数进行更严格的验证
  2. 异常处理:统一异常处理,返回更友好的错误信息
  3. 日志记录:添加更详细的日志记录,便于问题排查
  4. 性能监控:集成Spring Boot Actuator进行性能监控
  5. SQL优化:进一步优化SQL查询,减少表关联次数

6.3 架构优化建议

  1. 微服务拆分:考虑将报表功能拆分为独立的微服务
  2. 缓存策略:使用Redis缓存热点数据,提高响应速度
  3. 消息队列:使用消息队列处理异步任务,提高系统可靠性
  4. API网关:使用API网关统一管理API接口
  5. 容器化部署:使用Docker容器化部署,提高部署效率

7. 业务价值

7.1 运营价值

  1. 数据可视化:通过报表直观展示发货数据,便于运营分析
  2. 异常监控:及时发现和处理接收异常,减少损失
  3. 趋势分析:分析发货趋势,优化库存管理
  4. 成本控制:监控物流费用,优化物流方案

7.2 管理价值

  1. 决策支持:基于数据做出更合理的发货决策
  2. 流程优化:发现发货流程中的瓶颈,优化流程
  3. 绩效考核:基于发货数据进行绩效考核
  4. 风险控制:及时发现和应对发货风险

7.3 技术价值

  1. 可扩展性:模块化设计,便于后续功能扩展
  2. 可维护性:代码结构清晰,易于维护
  3. 可复用性:封装通用功能,提高代码复用率
  4. 技术栈更新:使用Vue 3、Spring Boot等最新技术,保持技术先进性

8. 总结

发货详情报表模块(ShipV2)是一个功能完善、技术先进的FBA发货数据分析工具,通过前端Vue 3和后端Spring Boot的完美结合,实现了货件和SKU两个维度的数据分析和管理。该模块不仅提供了丰富的筛选和导出功能,还通过多表关联查询和数据统计,为用户提供了全面、准确的发货数据,帮助用户更好地管理FBA发货流程,优化物流方案,降低运营成本。

未来,该模块可以进一步扩展,例如添加更多的数据分析维度、集成更多的物流商数据、提供更丰富的图表展示等,以满足用户日益增长的需求。

发货-发货统计(新)

发货统计模块功能解析文档

1. 系统架构

1.1 整体架构

发货统计模块采用前后端分离架构,主要包含以下组件:

1.2 模块依赖关系

前端组件 (ship_summary/index.vue)
    ↓
API接口层 (reportApi.js / reportV2Api.js)
    ↓
控制器层 (ShipInboundReportController / ShipInboundReportV2Controller)
    ↓
服务层 (IShipInboundItemService)
    ↓
数据访问层 (ShipInboundItemMapper / ShipInboundItemV2Mapper)
    ↓
MySQL数据库

1.3 文件结构

wimoor-ui/src/views/amazon/report/
├── ship/
│   └── ship_summary/
│       ├── index.vue              # V1版本前端组件
│       └── component/
│           └── piechart.vue       # 饼图组件
└── shipv2/
    └── ship_summary/
        ├── index.vue              # V2版本前端组件
        └── component/
            └── piechart.vue       # 饼图组件

wimoor-amazon/amazon-boot/src/main/java/
└── com/wimoor/amazon/
    ├── inbound/
    │   ├── controller/
    │   │   └── ShipInboundReportController.java    # V1控制器
    │   ├── service/
    │   │   ├── IShipInboundItemService.java        # 服务接口
    │   │   └── impl/ShipInboundItemServiceImpl.java # 服务实现
    │   └── mapper/
    │       └── ShipInboundItemMapper.xml           # V1 Mapper XML
    └── inboundV2/
        ├── controller/
        │   └── ShipInboundReportV2Controller.java  # V2控制器
        ├── service/
        │   ├── IShipInboundItemService.java        # V2服务接口
        │   └── impl/ShipInboundItemServiceImpl.java # V2服务实现
        └── mapper/
            └── ShipInboundItemV2Mapper.xml         # V2 Mapper XML

2. 前端实现

2.1 核心组件分析

2.1.1 主页面组件(index.vue)

文件路径wimoor-ui/src/views/amazon/report/ship/ship_summary/index.vue

主要功能

核心代码结构

<template>
  <div class="main-sty">
    <!-- 分组条件区 -->
    <el-row>
      <el-checkbox-group v-model="queryParam.groupby" @change="handleQuery">
        <el-checkbox label="channeldetailid">物流承运商(汇总)</el-checkbox>
        <el-checkbox label="warehouse">FBA仓库(汇总)</el-checkbox>
        <el-checkbox label="warehouseid">本地仓库(汇总)</el-checkbox>
        <el-checkbox label="groupid">店铺(汇总)</el-checkbox>
        <el-checkbox label="marketplaceid">站点(汇总)</el-checkbox>
        <el-checkbox label="sku">SKU(汇总)</el-checkbox>
        <el-checkbox label="shipmentid">货件(汇总)</el-checkbox>
      </el-checkbox-group>
    </el-row>

    <!-- 筛选条件区 -->
    <el-row>
      <Group @change="getData" />
      <Warehouse @changeware="getWarehouse" />
      <el-select v-model="queryParam.datetype">
        <el-option value="createdate" label="创建日期"></el-option>
        <el-option value="deliverydate" label="发货日期"></el-option>
      </el-select>
      <Datepicker @changedate="changedate" />
      <el-select v-model="queryParam.companyid" @change="companyChange">
        <el-option v-for="item in companylist" :value="item.id" :label="item.name"></el-option>
      </el-select>
      <el-select v-model="queryParam.channelid">
        <el-option v-for="item in channellist" :value="item.id" :label="item.channame"></el-option>
      </el-select>
      <el-input v-model="queryParam.search" placeholder="请输入SKU" />
    </el-row>

    <!-- 饼图展示区 -->
    <el-row class="gary-bg pie-chart">
      <el-select v-model="fieldkey.value">
        <el-option v-for="item in fieldoptions" :value="item.key" :label="item.name"></el-option>
      </el-select>
      <div v-for="(value,key) in chartdata.value">
        <PieChart :name="key" :data="value" :keyvalue="fieldkey" :chartdata="chartdata" />
      </div>
    </el-row>

    <!-- 数据表格区 -->
    <GlobalTable :tableData="tableData" @loadTable="loadTableData" show-summary :summary-method="getSummaries">
      <template #field>
        <el-table-column v-if="queryParam.groupby.indexOf('groupid')>=0" label="店铺" />
        <el-table-column v-if="queryParam.groupby.indexOf('marketplaceid')>=0" label="站点" />
        <el-table-column v-if="queryParam.groupby.indexOf('warehouseid')>=0" label="本地仓" />
        <el-table-column v-if="queryParam.groupby.indexOf('channeldetailid')>=0" label="物流承运商" />
        <el-table-column v-if="queryParam.groupby.indexOf('channeldetailid')>=0" label="物流渠道" />
        <el-table-column v-if="queryParam.groupby.indexOf('warehouse')>=0" label="FBA仓库" />
        <el-table-column v-if="queryParam.groupby.indexOf('sku')>=0" label="SKU" />
        <el-table-column v-if="queryParam.groupby.indexOf('shipmentid')>=0" label="货件" />
        <!-- 发货信息列组 -->
        <el-table-column label="发货信息">
          <el-table-column prop="totalqty" label="计划发货" />
          <el-table-column prop="totalout" label="实际发货" />
          <el-table-column prop="totalrec" label="实际接收" />
          <el-table-column prop="lessrec" label="接收差值" />
          <el-table-column prop="needout" label="待发货" />
          <el-table-column prop="needrec" label="待接收" />
          <el-table-column prop="worth" label="实际发货货值" />
        </el-table-column>
        <!-- 运输信息列组 -->
        <el-table-column label="运输信息">
          <el-table-column prop="readweight" label="预估运输重量(KG)" />
          <el-table-column prop="transweight_kg" label="发货运输重量(KG)" />
          <el-table-column prop="transweight_cbm" label="发货运输重量(CBM)" />
          <el-table-column prop="totalbox" label="货件箱数" />
          <el-table-column prop="shipfee" label="运输费用" />
          <el-table-column prop="totalotherfee" label="关税/其他费用" />
          <el-table-column prop="avgtime" label="平均物流时效(天)" />
        </el-table-column>
        <!-- 货件信息列组 -->
        <el-table-column label="货件信息">
          <el-table-column prop="shipmentnum" label="货件票数" />
          <el-table-column prop="problem" label="异常货件票数" />
        </el-table-column>
      </template>
    </GlobalTable>
  </div>
</template>

状态管理

let state = reactive({
  downLoading: false,
  queryParam: {
    search: "",
    marketplaceid: "",
    searchtype: "company",
    datetype: "createdate",
    type: "logitics",
    groupby: ["channeldetailid"],
    companyid: "",
    channelid: ""
  },
  isload: true,
  tableData: { records: [], total: 0 },
  snapshotDate: '',
  summary: {},
  fieldkey: { value: "transweight_kg" },
  chartdata: { value: [] },
  companylist: [],
  channellist: [],
  fieldoptions: [
    { name: "预估运输重量", key: 'readweight' },
    { name: "计划发货", key: 'totalqty' },
    { name: "实际发货", key: 'totalout' },
    { name: "实际接收", key: 'totalrec' },
    { name: "接收差值", key: 'lessrec' },
    { name: "实际发货货值", key: 'worth' },
    { name: "待接收", key: 'needrec' },
    { name: "待发货", key: 'needout' },
    { name: "发货运输重量(KG)", key: 'transweight_kg' },
    { name: "发货运输重量(CBM)", key: 'transweight_cbm' },
    { name: "运输费用", key: 'shipfee' },
    { name: "货件箱数", key: 'totalbox' },
    { name: "关税/其他费用", key: 'totalotherfee' },
    { name: "货件票数", key: 'shipmentnum' },
    { name: "平均物流时效(天)", key: 'avgtime' }
  ]
});

核心方法

  1. handleQuery() - 处理查询请求
function handleQuery() {
  if (state.queryParam.groupby && state.queryParam.groupby.length > 0) {
    globalTable.value.loadTable(state.queryParam);
  }
}
  1. loadTableData() - 加载统计数据
function loadTableData(params) {
  reportApi.getShipmentReportByLoistics(params).then((res) => {
    state.isload = false;
    state.tableData.records = res.data.records;
    state.tableData.total = res.data.total;
    if (params.currentpage == 1 && res.data.total > 0) {
      state.summary = res.data.records[0].summary;
    }
  });
  reportApi.getShipmentReportByWarehouseLoistics(params).then((res) => {
    state.chartdata.value = res.data;
  });
}
  1. downloadList() - 导出数据
function downloadList() {
  state.downLoading = true;
  reportApi.downExcelShipmentReportByLoistics(state.queryParam, () => {
    state.downLoading = false;
  });
}
  1. getSummaries() - 计算合计行
function getSummaries({ columns, data }) {
  var arr = ["合计"];
  columns.forEach((item, index) => {
    if (index >= 2) {
      arr[index] = state.summary[item.property];
    }
  });
  return arr;
}
  1. companyChange() - 承运商变更处理
function companyChange(val) {
  getchannelList(val);
  handleQuery();
}

function getchannelList(val) {
  var companyid = val;
  state.queryParam.channelid = "";
  if (val != "") {
    transportationApi.getChannel({ "company": companyid, "marketplaceid": "", "transtype": "" }).then((res) => {
      res.data.push({ "id": "", "channame": "全部" });
      state.channellist = res.data;
    });
  } else {
    state.channellist = [];
  }
}

2.2 API接口层

文件路径wimoor-ui/src/api/amazon/inbound/reportApi.js

核心接口

// 获取货件报表(按物流)
function getShipmentReportByLoistics(data) {
  return request.post('/amazon/api/v1/ship/report/getShipmentReportByLoistics', data);
}

// 获取仓库物流报表
function getShipmentReportByWarehouseLoistics(data) {
  return request.post('/amazon/api/v1/ship/report/getShipmentReportByWarehouseLoistics', data);
}

// 导出物流报表Excel
function downExcelShipmentReportByLoistics(data, callback) {
  return request({
    url: "/amazon/api/v1/ship/report/downExcelShipmentReportByLoistics",
    responseType: "blob",
    data: data,
    method: 'post'
  }).then(res => {
    downloadhandler.downloadSuccess(res, "shipmentLogisticsReport.xlsx");
    if (callback) {
      callback();
    }
  }).catch(e => {
    downloadhandler.downloadFail(e);
    if (callback) {
      callback();
    }
  });
}

V2版本APIwimoor-ui/src/api/amazon/inbound/reportV2Api.js

// V2版本使用相同的接口路径(/api/v2/)
function getShipmentReportByLoistics(data) {
  return request.post('/amazon/api/v2/ship/report/getShipmentReportByLoistics', data);
}

function getShipmentReportByWarehouseLoistics(data) {
  return request.post('/amazon/api/v2/ship/report/getShipmentReportByWarehouseLoistics', data);
}

function downExcelShipmentReportByLoistics(data, callback) {
  return request({
    url: "/amazon/api/v2/ship/report/downExcelShipmentReportByLoistics",
    responseType: "blob",
    data: data,
    method: 'post'
  }).then(res => {
    downloadhandler.downloadSuccess(res, "shipmentLogisticsReport.xlsx");
    // ...
  });
}

3. 后端实现

3.1 控制器层

3.1.1 ShipInboundReportController

文件路径wimoor-amazon/amazon-boot/src/main/java/com/wimoor/amazon/inbound/controller/ShipInboundReportController.java

主要功能:提供发货统计相关的RESTful API接口

核心接口

  1. getShipmentReportByLoistics() - 获取货件统计报表
@PostMapping(value = "/getShipmentReportByLoistics")
public Result<IPage<Map<String, Object>>> getShipmentReportByLoistics(@RequestBody ShipmentReportByLogisticsDTO dto) {
  Map<String, Object> param = new HashMap<String, Object>();
  UserInfo user = UserInfoContext.get();
  String shopid = user.getCompanyid();
  param.put("shopid", shopid);
  
  // 处理承运商
  String companyid = dto.getCompanyid();
  if (StrUtil.isEmpty(companyid)) {
    companyid = null;
  }
  param.put("companyid", companyid);
  
  // 处理物流渠道
  String channelid = dto.getChannelid();
  if (StrUtil.isEmpty(channelid)) {
    channelid = null;
  }
  param.put("channelid", channelid);
  
  // 处理仓库
  String warehouseid = dto.getWarehouseid();
  if (StrUtil.isEmpty(warehouseid)) {
    warehouseid = null;
  }
  param.put("warehouseid", warehouseid);
  
  // 处理类型
  String type = dto.getType();
  param.put("type", type);
  
  // 处理搜索条件
  String search = dto.getSearch();
  if (StrUtil.isNotEmpty(search)) {
    param.put("search", search.trim() + "%");
  }
  
  // 处理日期类型
  String datetype = dto.getDatetype();
  param.put("datetype", datetype);
  
  // 处理分组
  String ftype = "nosku";
  Set<String> keySet = new TreeSet<String>();
  keySet.add("channeldetailid");
  keySet.add("warehouse");
  keySet.add("warehouseid");
  keySet.add("groupid");
  keySet.add("sku");
  keySet.add("marketplaceid");
  keySet.add("shipmentid");
  String groupby = null;
  for (String field : dto.getGroupby()) {
    if (keySet.contains(field)) {
      if (field.equals("sku")) {
        ftype = "sku";
      }
      if (groupby == null) {
        groupby = field;
      } else {
        groupby = groupby + "," + field;
      }
    }
  }
  param.put("groupby", groupby);
  param.put("type", ftype);
  
  // 处理日期范围
  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  String fromDate = dto.getFromDate();
  if (StrUtil.isNotEmpty(fromDate)) {
    param.put("fromDate", fromDate.trim());
  } else {
    Calendar cal = Calendar.getInstance();
    cal.add(Calendar.MONTH, -1);
    fromDate = GeneralUtil.formatDate(cal.getTime(), sdf);
    param.put("fromDate", fromDate);
  }
  
  String toDate = dto.getToDate();
  if (StrUtil.isNotEmpty(toDate)) {
    param.put("endDate", toDate.trim().substring(0, 10) + " 23:59:59");
  } else {
    toDate = GeneralUtil.formatDate(new Date(), sdf);
    param.put("endDate", toDate + " 23:59:59");
  }
  
  // 处理店铺和市场
  param.put("groupid", StrUtil.isBlank(dto.getGroupid()) ? null : dto.getGroupid());
  param.put("marketplaceid", StrUtil.isBlank(dto.getMarketplaceid()) ? null : dto.getMarketplaceid());
  
  // 调用服务层获取数据
  IPage<Map<String, Object>> list = iShipInboundItemService.shipmentReportByLoistics(dto.getPage(), param);
  
  // 添加汇总数据
  if (list != null && list.getRecords().size() > 0 && dto.getCurrentpage() == 1) {
    Map<String, Object> map = iShipInboundItemService.shipmentReportByLoisticsTotal(param);
    if (map != null) {
      list.getRecords().get(0).put("summary", map);
    }
  }
  
  return Result.success(list);
}
  1. getShipmentReportByWarehouseLoistics() - 获取仓库物流统计
@PostMapping(value = "/getShipmentReportByWarehouseLoistics")
public Result<Map<String, List<Map<String, Object>>>> shipmentReportByWarhouseCHType(@RequestBody ShipmentReportByLogisticsDTO dto) {
  Map<String, Object> param = new HashMap<String, Object>();
  UserInfo user = UserInfoContext.get();
  String shopid = user.getCompanyid();
  param.put("shopid", shopid);
  
  // 处理各种筛选条件...
  
  param.put("groupid", StrUtil.isBlank(dto.getGroupid()) ? null : dto.getGroupid());
  param.put("marketplaceid", StrUtil.isBlank(dto.getMarketplaceid()) ? null : dto.getMarketplaceid());
  
  Map<String, List<Map<String, Object>>> result = iShipInboundItemService.shipmentReportByWarhouseCHType(param);
  return Result.success(result);
}
  1. downExcelShipmentReportByLoisticsAction() - 导出Excel
@PostMapping(value = "/downExcelShipmentReportByLoistics")
public void downExcelShipmentReportByLoisticsAction(@RequestBody ShipmentReportByLogisticsDTO dto, HttpServletResponse response) {
  SXSSFWorkbook workbook = new SXSSFWorkbook();
  Map<String, Object> param = new HashMap<String, Object>();
  UserInfo user = UserInfoContext.get();
  String shopid = user.getCompanyid();
  param.put("shopid", shopid);
  
  // 处理各种参数...
  
  iShipInboundItemService.setShipmentReportByLoisticsExcelBook(workbook, param, dto.getGroupby());
  
  // 输出Excel文件
  response.setContentType("application/force-download");
  response.addHeader("Content-Disposition", "attachment;fileName=ShipmentReportByLoistics" + System.currentTimeMillis() + ".xlsx");
  ServletOutputStream fOut = response.getOutputStream();
  workbook.write(fOut);
  workbook.close();
  fOut.flush();
  fOut.close();
}

3.2 服务层

3.2.1 IShipInboundItemService

文件路径wimoor-amazon/amazon-boot/src/main/java/com/wimoor/amazon/inbound/service/IShipInboundItemService.java

核心方法

public interface IShipInboundItemService extends IService<ShipInboundItem> {
  // 其他方法...
  
  /**
   * 获取货件统计报表(按物流)
   */
  IPage<Map<String, Object>> shipmentReportByLoistics(Page<?> page, Map<String, Object> param);
  
  /**
   * 获取货件统计汇总
   */
  Map<String, Object> shipmentReportByLoisticsTotal(Map<String, Object> param);
  
  /**
   * 获取仓库物流统计
   */
  Map<String, List<Map<String, Object>>> shipmentReportByWarhouseCHType(Map<String, Object> param);
  
  /**
   * 生成货件统计报表Excel
   */
  void setShipmentReportByLoisticsExcelBook(SXSSFWorkbook workbook, Map<String, Object> param, List<String> groupby);
}

3.2.2 ShipInboundItemServiceImpl

核心实现逻辑

@Override
public IPage<Map<String, Object>> shipmentReportByLoistics(Page<?> page, Map<String, Object> param) {
  return this.baseMapper.shipmentReportByLoistics(page, param);
}

@Override
public Map<String, Object> shipmentReportByLoisticsTotal(Map<String, Object> param) {
  return this.baseMapper.shipmentReportByLoisticsTotal(param);
}

@Override
public Map<String, List<Map<String, Object>>> shipmentReportByWarhouseCHType(Map<String, Object> param) {
  List<Map<String, Object>> list = this.baseMapper.shipmentReportByWarhouseCHType(param);
  Map<String, List<Map<String, Object>>> result = new LinkedHashMap<String, List<Map<String, Object>>>();
  for (Map<String, Object> item : list) {
    String key = item.get("warehouseid") != null ? item.get("warehouseid").toString() : "";
    if (!result.containsKey(key)) {
      result.put(key, new ArrayList<Map<String, Object>>());
    }
    result.get(key).add(item);
  }
  return result;
}

@Override
public void setShipmentReportByLoisticsExcelBook(SXSSFWorkbook workbook, Map<String, Object> param, List<String> groupby) {
  Map<String, Object> titlemap = new LinkedHashMap<String, Object>();
  
  // 根据分组条件动态添加标题列
  if (groupby.contains("groupid")) {
    titlemap.put("gname", "店铺");
  }
  if (groupby.contains("marketplaceid")) {
    titlemap.put("market", "站点");
  }
  if (groupby.contains("warehouseid")) {
    titlemap.put("warehousename", "本地仓");
  }
  if (groupby.contains("channeldetailid")) {
    titlemap.put("logitics", "物流承运商");
    titlemap.put("channame", "物流渠道");
  }
  if (groupby.contains("warehouse")) {
    titlemap.put("warehouse", "FBA仓库");
  }
  if (groupby.contains("sku")) {
    titlemap.put("sku", "SKU");
  }
  if (groupby.contains("shipmentid")) {
    titlemap.put("shipmentid", "货件");
  }
  
  // 添加数据列
  titlemap.put("totalqty", "计划发货");
  titlemap.put("totalout", "实际发货");
  titlemap.put("totalrec", "实际接收");
  titlemap.put("lessrec", "接收差值");
  titlemap.put("needout", "待发货");
  titlemap.put("needrec", "待接收");
  titlemap.put("worth", "实际发货货值");
  titlemap.put("readweight", "预估运输重量(KG)");
  titlemap.put("transweight_kg", "发货运输重量(KG)");
  titlemap.put("transweight_cbm", "发货运输重量(CBM)");
  titlemap.put("totalbox", "货件箱数");
  titlemap.put("shipfee", "运输费用");
  titlemap.put("totalotherfee", "关税/其他费用");
  titlemap.put("avgtime", "平均物流时效(天)");
  titlemap.put("shipmentnum", "货件票数");
  titlemap.put("problem", "异常货件票数");
  
  List<Map<String, Object>> list = this.baseMapper.shipmentReportByLoistics(param);
  Sheet sheet = workbook.createSheet("sheet1");
  
  // 创建标题行
  Row trow = sheet.createRow(0);
  Object[] titlearray = titlemap.keySet().toArray();
  for (int i = 0; i < titlearray.length; i++) {
    Cell cell = trow.createCell(i);
    Object value = titlemap.get(titlearray[i].toString());
    cell.setCellValue(value.toString());
  }
  
  // 填充数据行
  for (int i = 0; i < list.size(); i++) {
    Row row = sheet.createRow(i + 1);
    Map<String, Object> map = list.get(i);
    for (int j = 0; j < titlearray.length; j++) {
      Cell cell = row.createCell(j);
      Object value = map.get(titlearray[j].toString());
      if (value != null) {
        cell.setCellValue(value.toString());
      }
    }
  }
}

3.3 数据访问层

3.3.1 ShipInboundItemMapper

文件路径wimoor-amazon/amazon-boot/src/main/java/com/wimoor/amazon/inbound/mapper/ShipInboundItemMapper.java

核心方法

public interface ShipInboundItemMapper extends BaseMapper<ShipInboundItem> {
  // 其他方法...
  
  IPage<Map<String, Object>> shipmentReportByLoistics(Page<?> page, @Param("param") Map<String, Object> param);
  
  Map<String, Object> shipmentReportByLoisticsTotal(@Param("param") Map<String, Object> param);
  
  List<Map<String, Object>> shipmentReportByWarhouseCHType(@Param("param") Map<String, Object> param);
}

3.3.2 ShipInboundItemMapper.xml

文件路径wimoor-amazon/amazon-boot/src/main/resources/mapper/inbound/ShipInboundItemMapper.xml

核心SQL查询

<select id="shipmentReportByLoistics" parameterType="java.util.Map" resultType="java.util.Map">
  SELECT * FROM (
    SELECT
      plan.marketplaceid warehouse,
      de.transtype,
      max(mkp.name) marketname,
      max(tt.name) name,
      sum(ifnull(dd.weight, 0) * item.Quantity) readweight,
      SUM(item.Quantity) totalqty,
      sum(CASE WHEN shipment.`status` = 5 OR shipment.`status` = 6 THEN item.QuantityShipped ELSE 0 END) totalout,
      sum(CASE WHEN shipment.`status` = 5 OR shipment.`status` = 6 THEN item.QuantityReceived ELSE 0 END) totalrec,
      sum(CASE WHEN shipment.`status` = 5 OR shipment.`status` = 6 THEN item.QuantityReceived - item.QuantityShipped ELSE 0 END) lessrec,
      sum(CASE WHEN shipment.`status` = 5 OR shipment.`status` = 6 THEN item.QuantityShipped * m.price ELSE 0 END) worth,
      sum(CASE WHEN shipment.`status` >= 2 AND shipment.`status` <= 5 THEN item.QuantityShipped ELSE 0 END) needrec,
      sum(CASE WHEN shipment.`status` >= 2 AND shipment.`status` <= 5 THEN item.Quantity - item.QuantityReceived ELSE 0 END) needout
    FROM t_erp_ship_inbounditem item
    LEFT JOIN t_erp_ship_inboundplan plan ON plan.id = item.inboundplanid
    LEFT JOIN t_marketplace mkp ON mkp.marketplaceId = plan.marketplaceid
    LEFT JOIN t_erp_warehouse w ON plan.warehouseid = w.id
    LEFT JOIN t_erp_material m ON m.sku = item.SellerSKU AND plan.shopid = m.shopid AND m.isDelete = 0
    LEFT JOIN t_dimensions dd ON dd.id = m.pkgDimensions
    LEFT JOIN t_erp_ship_inboundshipment shipment ON shipment.ShipmentId = item.ShipmentId
    LEFT JOIN t_erp_ship_inboundtrans trans ON trans.shipmentid = item.ShipmentId
    LEFT JOIN t_erp_ship_transdetail de ON de.id = trans.channel
    LEFT JOIN t_erp_ship_transchannel ch ON ch.id = de.channel
    LEFT JOIN t_erp_transtype tt ON tt.id = de.transtype
    LEFT JOIN t_erp_ship_transcompany com ON com.id = de.company
    WHERE plan.shopid = #{param.shopid, jdbcType = CHAR}
      AND shipment.status >= 5
      AND com.`name` IS NOT NULL
    <if test="param.datetype == 'createdate'">
      AND plan.createdate >= #{param.fromDate, jdbcType = DATE}
      AND plan.createdate <= #{param.endDate, jdbcType = DATE}
    </if>
    <if test="param.datetype == 'deliverydate'">
      AND shipment.shiped_date >= #{param.fromDate, jdbcType = DATE}
      AND shipment.shiped_date <= #{param.endDate, jdbcType = DATE}
    </if>
    <if test="param.warehouseid != null">
      AND w.id = #{param.warehouseid, jdbcType = CHAR}
    </if>
    <if test="param.companyid != null">
      AND de.company = #{param.companyid, jdbcType = CHAR}
    </if>
    <if test="param.channelid != null">
      AND trans.channel = #{param.channelid, jdbcType = CHAR}
    </if>
    <if test="param.search != null">
      AND item.SellerSKU LIKE #{param.search, jdbcType = CHAR}
    </if>
    <if test="param.groupid != null">
      AND plan.amazongroupid = #{param.groupid, jdbcType = CHAR}
    </if>
    <if test="param.marketplaceid != null">
      AND plan.marketplaceid = #{param.marketplaceid, jdbcType = CHAR}
    </if>
    GROUP BY plan.marketplaceid, de.transtype
  ) v
  LEFT JOIN (
    SELECT
      plan.marketplaceid warehouse,
      de.transtype,
      SUM(CASE WHEN trans.wunit = 'kg' THEN trans.transweight ELSE 0 END) transweight_kg,
      SUM(CASE WHEN trans.wunit = 'cbm' THEN trans.transweight ELSE 0 END) transweight_cbm,
      SUM(IFNULL(trans.singleprice, 0) * IFNULL(trans.transweight, 0) + IFNULL(trans.otherfee, 0)) shipfee,
      SUM(trans.otherfee) totalotherfee,
      COUNT(shipment.shipmentid) shipmentnum,
      SUM(shipment.boxnum) totalbox,
      ROUND(AVG(IFNULL(DATEDIFF(shipment.start_receive_date, shipment.shiped_date), 0)), 2) avgtime,
      COUNT(IF(shipment.status = '-1', TRUE, NULL)) problem
    FROM t_erp_ship_inboundshipment shipment
    LEFT JOIN t_erp_ship_inboundplan plan ON plan.id = shipment.inboundplanid
    LEFT JOIN t_marketplace mkp ON mkp.marketplaceid = plan.marketplaceid
    LEFT JOIN t_erp_warehouse w ON plan.warehouseid = w.id
    LEFT JOIN t_erp_ship_inboundtrans trans ON trans.shipmentid = shipment.ShipmentId
    LEFT JOIN t_erp_ship_transdetail de ON de.id = trans.channel
    LEFT JOIN t_erp_ship_transcompany com ON com.id = de.company
    WHERE plan.shopid = #{param.shopid, jdbcType = CHAR}
      AND (shipment.status >= 5 OR shipment.start_receive_date IS NOT NULL)
      AND com.`name` IS NOT NULL
    -- 相同的筛选条件...
    GROUP BY plan.marketplaceid, de.transtype
  ) w ON v.warehouse = w.warehouse AND v.transtype = w.transtype
</select>

4. 数据库设计

4.1 核心表结构

4.1.1 t_erp_ship_inbounditem(货件明细表)

字段名 类型 说明
id VARCHAR 主键ID
ShipmentId VARCHAR 亚马逊货件ID
SellerSKU VARCHAR 产品SKU
inboundplanid VARCHAR 发货计划ID
Quantity INT 计划数量
QuantityShipped INT 发货数量
QuantityReceived INT 接收数量

4.1.2 t_erp_ship_inboundplan(发货计划表)

字段名 类型 说明
id VARCHAR 主键ID
amazongroupid VARCHAR 店铺ID
marketplaceid VARCHAR 市场ID
warehouseid VARCHAR 仓库ID
shopid VARCHAR 公司ID
auditstatus INT 审核状态
createdate DATETIME 创建日期

4.1.3 t_erp_ship_inboundshipment(货件表)

字段名 类型 说明
ShipmentId VARCHAR 货件ID(主键)
inboundplanid VARCHAR 发货计划ID
status INT 状态码
shiped_date DATETIME 发货日期
start_receive_date DATETIME 开始接收日期
boxnum INT 箱数

4.1.4 t_erp_ship_inboundtrans(货件运输表)

字段名 类型 说明
shipmentid VARCHAR 货件ID
channel VARCHAR 渠道ID
transweight DECIMAL 运输重量
wunit VARCHAR 重量单位
singleprice DECIMAL 单价
otherfee DECIMAL 其他费用

4.1.5 t_erp_ship_transdetail(运输详情表)

字段名 类型 说明
id VARCHAR 主键ID
company VARCHAR 承运商ID
channel VARCHAR 渠道ID
channame VARCHAR 渠道名称
transtype VARCHAR 运输方式

4.1.6 t_erp_ship_transcompany(承运商表)

字段名 类型 说明
id VARCHAR 承运商ID(主键)
name VARCHAR 承运商名称

4.2 数据关系图

t_erp_ship_inboundplan (发货计划)
    ├── t_erp_ship_inbounditem (货件明细)
    │       └── t_erp_material (产品信息)
    └── t_erp_ship_inboundshipment (货件)
            ├── t_erp_ship_inboundtrans (运输信息)
            │       └── t_erp_ship_transdetail (渠道详情)
            │               └── t_erp_ship_transcompany (承运商)
            └── t_erp_warehouse (仓库)

5. API接口文档

5.1 获取货件统计报表

接口地址POST /api/v1/ship/report/getShipmentReportByLoistics

请求参数

{
  "currentpage": 1,
  "pagesize": 20,
  "groupby": ["channeldetailid", "warehouse", "warehouseid", "groupid", "marketplaceid", "sku", "shipmentid"],
  "companyid": "承运商ID",
  "channelid": "渠道ID",
  "warehouseid": "仓库ID",
  "search": "SKU搜索关键词",
  "datetype": "createdate",
  "fromDate": "2026-01-01",
  "toDate": "2026-01-31",
  "type": "logitics",
  "groupid": "店铺ID",
  "marketplaceid": "市场ID"
}

响应参数

{
  "code": 200,
  "msg": "success",
  "data": {
    "records": [
      {
        "market": "US",
        "warehousename": "深圳仓",
        "logitics": "DHL",
        "channame": "DHL快递",
        "subarea": "北美",
        "channelname": "快递",
        "transtype": "AIR",
        "totalqty": 1000,
        "totalout": 800,
        "totalrec": 750,
        "lessrec": -50,
        "needout": 200,
        "needrec": 250,
        "worth": 50000.00,
        "readweight": 150.5,
        "transweight_kg": 145.2,
        "transweight_cbm": 0.8,
        "totalbox": 20,
        "shipfee": 1500.00,
        "totalotherfee": 200.00,
        "avgtime": 5.5,
        "shipmentnum": 5,
        "problem": 0,
        "summary": {
          "totalqty": 5000,
          "totalout": 4000,
          "totalrec": 3800,
          "lessrec": -200,
          "worth": 250000.00,
          "shipfee": 7500.00
        }
      }
    ],
    "total": 100,
    "size": 20,
    "current": 1,
    "pages": 5
  }
}

5.2 获取仓库物流统计

接口地址POST /api/v1/ship/report/getShipmentReportByWarehouseLoistics

请求参数

{
  "groupby": ["warehouseid"],
  "companyid": "承运商ID",
  "warehouseid": "仓库ID",
  "datetype": "createdate",
  "fromDate": "2026-01-01",
  "toDate": "2026-01-31"
}

响应参数

{
  "code": 200,
  "msg": "success",
  "data": {
    "warehouse1": [
      {
        "warehouseid": "WH001",
        "warehouse": "深圳仓",
        "transweight_kg": 500.5,
        "shipfee": 3000.00
      }
    ],
    "warehouse2": [
      {
        "warehouseid": "WH002",
        "warehouse": "广州仓",
        "transweight_kg": 300.2,
        "shipfee": 1800.00
      }
    ]
  }
}

5.3 导出统计报表

接口地址POST /api/v1/ship/report/downExcelShipmentReportByLoistics

请求参数:同5.1

响应:Excel文件流

6. 业务流程

6.1 数据查询流程

用户操作
    ↓
前端构建查询参数(分组、筛选条件)
    ↓
调用 getShipmentReportByLoistics API
    ↓
控制器处理参数,构建查询条件
    ↓
服务层调用 Mapper
    ↓
Mapper 执行 SQL 查询
    ↓
数据库返回查询结果
    ↓
服务层处理结果,添加汇总数据
    ↓
控制器返回 JSON 响应
    ↓
前端渲染表格和图表

6.2 数据计算逻辑

发货指标计算

运输指标计算

7. 技术亮点

7.1 前端技术亮点

  1. 动态分组:通过el-checkbox-group动态控制分组维度,表格列根据分组条件动态显示
  2. 饼图可视化:使用PieChart组件展示数据分布,支持切换不同的汇总指标
  3. 合计行:使用el-table的show-summary和summary-method实现自动计算合计行
  4. 条件渲染:使用v-if指令根据分组条件动态渲染表格列
  5. 响应式设计:使用el-space布局组件,支持不同屏幕尺寸

7.2 后端技术亮点

  1. 动态SQL:使用MyBatis的动态SQL,根据分组条件动态构建查询语句
  2. 多表关联:使用LEFT JOIN关联多个表,实现复杂的数据统计
  3. 聚合计算:使用SUM、AVG等聚合函数进行数据统计
  4. SXSSFWorkbook:使用Apache POI的SXSSFWorkbook流式处理大数据量Excel导出
  5. 参数校验:对输入参数进行非空检查和格式转换

7.3 性能优化

  1. 索引优化:在关键字段上建立索引,提高查询速度
  2. 分页查询:使用分页功能避免一次性加载大量数据
  3. 缓存机制:缓存常用的查询结果
  4. 懒加载:饼图数据使用单独的请求加载

8. 扩展功能

8.1 分组维度扩展

当前支持7种分组维度,可扩展支持更多维度:

8.2 统计指标扩展

当前支持15种统计指标,可扩展支持更多指标:

8.3 图表类型扩展

当前使用饼图展示数据分布,可扩展支持:

9. 注意事项

9.1 数据同步

9.2 权限控制

9.3 性能考虑


文档版本:v1.0
最后更新:2026-01-26
适用系统:Wimoor FBA发货管理系统

发货-费用分摊(新)

发货费用分摊模块功能解析

1. 模块架构

发货费用分摊模块采用前后端分离的架构设计,主要包含以下组件:

1.1 前端组件

组件名称 文件路径 功能描述
主页面组件 wimoor-ui/src/views/erp/warehouse/fee/index.vue 核心页面,包含双标签页设计和数据展示
详情对话框组件 wimoor-ui/src/views/erp/warehouse/fee/detail_dialog.vue 展示SKU历史费用分摊情况和趋势图表
API接口文件 wimoor-ui/src/api/erp/ship/transportationApi.js 封装与后端通信的API接口

1.2 后端组件

组件名称 文件路径 功能描述
控制器 wimoor-erp/erp-boot/src/main/java/com/wimoor/erp/stock/controller/ErpDispatchOverseaTransController.java 处理前端请求,返回数据
服务接口 wimoor-erp/erp-boot/src/main/java/com/wimoor/erp/stock/service/IErpDispatchOverseaTransService.java 定义业务逻辑方法
服务实现 wimoor-erp/erp-boot/src/main/java/com/wimoor/erp/stock/service/impl/ErpDispatchOverseaTransServiceImpl.java 实现业务逻辑
Mapper接口 wimoor-erp/erp-boot/src/main/java/com/wimoor/erp/stock/mapper/ErpDispatchOverseaTransMapper.java 定义数据库操作方法

2. 前端实现分析

2.1 主页面组件 (index.vue)

2.1.1 核心结构

<template>
  <div class="main-sty">
    <el-tabs v-model="selectable" @tab-change="handleQuery">
      <el-tab-pane label="SKU头程" name="sku" key="sku"></el-tab-pane>
      <el-tab-pane label="SKU头程明细" name="detail" key="detail"></el-tab-pane>
    </el-tabs>
    <!-- 筛选条件区域 -->
    <!-- 操作按钮区域 -->
    <!-- 数据表格区域 -->
  </div>
  <Dialog ref="dialogRef"></Dialog>
</template>

2.1.2 核心逻辑

  1. 数据加载逻辑

    • 通过 loadTableData 函数根据当前选中的标签页加载对应的数据
    • 当标签页切换时,调用 handleQuery 函数重新加载数据
    • 当筛选条件变化时,调用 handleQuery 函数重新加载数据
  2. API调用

    • getShipFeeReport:获取SKU头程费用报告
    • getShipFeeDetailReport:获取SKU头程费用明细报告
    • downShipFeeReportExcel:导出SKU头程费用报告
    • downShipFeeDetailReportExcel:导出SKU头程费用明细报告
  3. 详情查看逻辑

    • 点击"历史详情"按钮时,调用 showDialog 函数打开详情对话框
    • 传递当前查询参数和SKU编码给详情对话框

2.2 详情对话框组件 (detail_dialog.vue)

2.2.1 核心结构

<template>
  <el-dialog title="SKU头程历史(每周费用分摊)" width="800px">
    <div id='mychart1' style='height:230px;width:100%'></div>
    <GlobalTable ref="globalTable" :tableData="tableData" @loadTable="loadTableData">
      <!-- 表格列定义 -->
    </GlobalTable>
  </el-dialog>
</template>

2.2.2 核心逻辑

  1. 数据加载逻辑

    • 通过 loadTableData 函数加载SKU历史费用数据
    • 调用 getShipSkuFeeReport API获取数据
  2. 图表渲染逻辑

    • 使用 ECharts 库渲染费用趋势图表
    • lineChart 函数负责图表的配置和渲染
    • 图表展示运费、发货数量和平均单价的趋势

2.3 API接口文件 (transportationApi.js)

2.3.1 核心API接口

API方法 URL 功能描述
getShipFeeReport /erp/api/v1/inventory/dispatch/overseaTrans/getShipFeeReport 获取SKU头程费用报告
getShipFeeDetailReport /erp/api/v1/inventory/dispatch/overseaTrans/getShipFeeDetailReport 获取SKU头程费用明细报告
getShipSkuFeeReport /erp/api/v1/inventory/dispatch/overseaTrans/getShipSkuFeeReport 获取SKU费用历史报告
downShipFeeReportExcel /erp/api/v1/inventory/dispatch/overseaTrans/downShipFeeReportExcel 导出SKU头程费用报告
downShipFeeDetailReportExcel /erp/api/v1/inventory/dispatch/overseaTrans/downShipFeeDetailReportExcel 导出SKU头程费用明细报告

3. 后端实现分析

3.1 控制器 (ErpDispatchOverseaTransController.java)

3.1.1 核心方法

方法名 HTTP方法 功能描述
getShipFeeReportAction POST 获取SKU头程费用报告
getShipFeeDetailReportAction POST 获取SKU头程费用明细报告
getShipSkuFeeReportAction POST 获取SKU费用历史报告
downShipFeeReportExcelAction POST 导出SKU头程费用报告
downShipFeeDetailReportExcelAction POST 导出SKU头程费用明细报告

3.1.2 核心逻辑

  1. 参数处理

    • 从前端请求中获取筛选参数
    • 处理参数默认值,如日期范围默认为最近7天
    • 将参数传递给服务层方法
  2. 响应处理

    • 将服务层返回的数据包装为 Result 对象返回
    • 对于Excel导出,设置响应头并写入Excel文件

3.2 服务实现 (ErpDispatchOverseaTransServiceImpl.java)

3.2.1 核心方法

方法名 功能描述
getShipFeeReport 获取SKU头程费用报告
getShipFeeDetailReport 获取SKU头程费用明细报告
transSKUFeeShared 获取SKU费用历史报告(包含图表数据)
setShipFeeReport 生成SKU头程费用报告Excel文件
setShipFeeDetailReport 生成SKU头程费用明细报告Excel文件

3.2.2 核心逻辑

  1. 费用分摊计算

    • getShipFeeDetailReport 方法中,计算每个SKU的单件费用:
      if(map.get("skufee")!=null && map.get("qty")!=null) {
          String skufeeStr=map.get("skufee").toString();
          BigDecimal qty=new BigDecimal(map.get("qty").toString());
          BigDecimal skufee=new BigDecimal(skufeeStr);
          if(skufee!=null&&qty!=null&&qty.intValue()>0) {
              Double rate = skufee.doubleValue()/qty.doubleValue();
              map.put("skufeeavg", rate);
          }else {
              map.put("skufeeavg", 0);
          }
      }else {
          map.put("skufeeavg", null);
      }
      
  2. 图表数据处理

    • transSKUFeeShared 方法中,处理每周费用数据并生成图表数据:
      // 处理每周数据
      for(Map<String, Object> item:pagelist.getRecords()) {
          if(item.get("opttime2")!=null){
              temp.put(item.get("opttime2").toString().substring(0,10), item);
          }
      }
      // 生成图表数据
      while (c.getTime().before(enddate)) {
          String key = sdf.format(c.getTime());
          if(temp.get(key)==null) {
              series.add("0");
              seriesfee.add("0");
              seriesavgfee.add("0");
          }else {
              // 处理有数据的周
          }
          labels.add(key);
          c.add(Calendar.DATE, 7);
      }
      
  3. Excel导出

    • 使用 SXSSFWorkbook 生成Excel文件
    • 设置表头和数据行
    • 计算并填充SKU单件费用

3.3 Mapper接口 (ErpDispatchOverseaTransMapper.java)

3.3.1 核心方法

方法名 功能描述
transFeeShared 查询SKU头程费用汇总数据
transFeeSharedDetail 查询SKU头程费用明细数据
transFeeSharedWeek 查询SKU每周费用分摊数据

4. 数据流分析

4.1 SKU头程费用报告数据流

  1. 前端请求:用户在"SKU头程"标签页设置筛选条件并点击搜索
  2. API调用:前端调用 getShipFeeReport API
  3. 后端处理
    • 控制器接收请求并处理参数
    • 调用服务层 getShipFeeReport 方法
    • 服务层调用Mapper transFeeShared 方法查询数据库
    • 数据库返回查询结果
    • 服务层将结果返回给控制器
    • 控制器将结果包装为 Result 对象返回
  4. 前端渲染:前端接收数据并渲染到表格中

4.2 SKU头程费用明细报告数据流

  1. 前端请求:用户在"SKU头程明细"标签页设置筛选条件并点击搜索
  2. API调用:前端调用 getShipFeeDetailReport API
  3. 后端处理
    • 控制器接收请求并处理参数
    • 调用服务层 getShipFeeDetailReport 方法
    • 服务层调用Mapper transFeeSharedDetail 方法查询数据库
    • 服务层计算每个SKU的单件费用
    • 服务层将结果返回给控制器
    • 控制器将结果包装为 Result 对象返回
  4. 前端渲染:前端接收数据并渲染到表格中

4.3 SKU历史费用报告数据流

  1. 前端请求:用户点击"历史详情"按钮
  2. API调用:前端调用 getShipSkuFeeReport API
  3. 后端处理
    • 控制器接收请求并处理参数
    • 调用服务层 transSKUFeeShared 方法
    • 服务层调用Mapper transFeeSharedWeek 方法查询数据库
    • 服务层处理数据并生成图表数据
    • 服务层将结果返回给控制器
    • 控制器将结果包装为 Result 对象返回
  4. 前端渲染
    • 前端接收数据并渲染到表格中
    • 前端使用ECharts渲染费用趋势图表

4.4 Excel导出数据流

  1. 前端请求:用户点击"导出"按钮
  2. API调用:前端调用对应的导出API
  3. 后端处理
    • 控制器接收请求并处理参数
    • 创建 SXSSFWorkbook 对象
    • 调用服务层对应的导出方法
    • 服务层调用Mapper查询数据
    • 服务层将数据写入Excel文件
    • 控制器设置响应头并将Excel文件写入响应流
  4. 前端处理:前端接收Excel文件并自动下载

5. 数据库操作分析

5.1 核心查询

  1. SKU头程费用汇总查询

    • 表:涉及发货单、SKU、费用等相关表
    • 关键字段:SKU编码、发货数量、费用金额
    • 聚合函数:SUM(计算总发货数量和总费用)
    • 分组:按SKU编码分组
  2. SKU头程费用明细查询

    • 表:涉及发货单、SKU、费用等相关表
    • 关键字段:发货单编码、SKU编码、发货数量、费用金额
    • 关联:多表关联查询
  3. SKU每周费用分摊查询

    • 表:涉及发货单、SKU、费用等相关表
    • 关键字段:SKU编码、发货日期、发货数量、费用金额
    • 分组:按SKU编码和周分组
    • 聚合函数:SUM(计算每周发货数量和费用)

6. 技术亮点

6.1 前端技术亮点

  1. 双标签页设计:使用Element Plus的 el-tabs 组件实现双标签页切换,提高用户体验
  2. 响应式布局:使用Element Plus的响应式组件构建界面,适配不同屏幕尺寸
  3. 数据可视化:集成ECharts实现费用趋势的图表展示,直观呈现数据变化
  4. 异步数据加载:使用Vue 3的Composition API实现异步数据加载,提高页面响应速度
  5. 模块化设计:将主页面和详情对话框分离为独立组件,提高代码可维护性

6.2 后端技术亮点

  1. 分层架构:采用控制器→服务→Mapper的分层架构,职责清晰
  2. Excel导出优化:使用 SXSSFWorkbook 生成Excel文件,支持大数据量导出
  3. 图表数据处理:在后端预处理图表数据,减轻前端负担
  4. 参数处理:统一处理参数默认值和边界情况,提高代码健壮性
  5. 费用计算逻辑:实现准确的费用分摊计算,确保数据准确性

7. 代码优化建议

7.1 前端优化建议

  1. 性能优化

    • 实现表格数据虚拟滚动,提高大数据量下的渲染性能
    • 对频繁调用的API实现缓存,减少重复请求
  2. 用户体验优化

    • 添加加载状态提示,提高用户体验
    • 实现筛选条件的记忆功能,下次进入页面时自动恢复上次的筛选条件
  3. 代码质量优化

    • 提取重复的代码为公共函数,减少代码冗余
    • 完善错误处理逻辑,提高代码健壮性

7.2 后端优化建议

  1. 性能优化

    • 优化数据库查询,添加适当的索引
    • 实现查询结果缓存,减少数据库压力
  2. 代码质量优化

    • 提取重复的参数处理逻辑为公共方法
    • 使用枚举或常量定义固定值,提高代码可维护性
    • 完善异常处理,提供更详细的错误信息
  3. 功能优化

    • 增加费用分摊规则的可配置性,支持不同场景的分摊需求
    • 增加费用分析报表,提供更丰富的数据洞察

8. 总结

发货费用分摊模块是一个功能完整、设计合理的企业级应用模块,主要实现了以下功能:

  1. 费用分摊计算:将发货总费用根据发货数量等因素分摊到各个SKU
  2. 多维度数据展示:支持SKU级别的费用汇总和发货单级别的费用明细
  3. 丰富的筛选条件:支持按仓库、日期、承运商、物流渠道等条件筛选
  4. 数据可视化:使用ECharts展示费用趋势图表,直观呈现数据变化
  5. Excel导出:支持将数据导出为Excel文件,方便离线分析

该模块通过前后端分离的架构设计,实现了从数据存储、业务逻辑到前端展示的完整流程,为用户提供了清晰、直观的费用分摊分析工具,帮助用户更好地了解和管理发货费用。

销售-商品分析

销售商品分析模块功能解析文档

1. 系统架构

1.1 整体架构

销售商品分析模块采用前后端分离架构,主要包含以下组件:

1.2 模块依赖关系

flowchart TD
    A[前端index.vue] --> B[productAnysApi.js]
    B --> C[ProductAnalysisController]
    C --> D[IProductInOrderService]
    D --> E[ProductInOrderMapper]
    D --> F[ProductRankMapper]
    D --> G[ProductInOptMapper]
    D --> H[IOrdersSumService]
    D --> I[IAmzProductPageviewsService]
    E --> J[MySQL数据库]
    F --> J
    G --> J
    H --> J
    I --> J

2. 前端实现

2.1 核心文件结构

wimoor-ui/src/views/amazon/listing/analysis/
├── index.vue                    # 主页面组件
└── components/
    ├── data_deatils.vue         # 数据详情组件
    └── dialog.vue               # 指标选择对话框组件

wimoor-ui/src/api/amazon/product/
└── productAnysApi.js            # 商品分析API接口

2.2 核心组件分析

2.2.1 主页面组件(index.vue)

文件路径wimoor-ui/src/views/amazon/listing/analysis/index.vue

主要功能

核心代码结构

<template>
  <div class="gird-line-head el-white-bg">
    <!-- 顶部筛选区域 -->
    <div class="flex-center">
      <el-space>
        <Group @change="groupChange" ref="groupRef" :init="true"/>
        <el-input v-model="queryParams.search" clearable @input="handleQuery" placeholder="请输入" class="input-with-select">
          <!-- 搜索类型选择 -->
          <template #prepend>
            <el-select v-model="queryParams.ftype" @change='handleQuery' style="width:100px;" placeholder="SKU">
              <!-- 搜索类型选项 -->
            </el-select>
          </template>
          <!-- 搜索按钮 -->
          <template #append>
            <el-button @click="handleQuery">
              <el-icon class="font-base ic-cen"><search /></el-icon>
            </el-button>
          </template>
        </el-input>
      </el-space>
    </div>
  </div>
  
  <!-- 左右分栏布局 -->
  <div class="grid-content">
    <!-- 左侧SKU列表 -->
    <div class="left-content el-white-bg ">
      <div class="con-header"><h4>SKU列表</h4></div>
      <el-scrollbar style="height:calc(100vh - 280px)">
        <div>
          <ul class="sku-list" v-infinite-scroll="load">
            <li class="pointer" v-for="item in tableData" @click="selectSku(item)" :class="{'active':item.active}">
              {{item.sku}}
              <div class="font-extraSmall">ASIN:{{item.asin}}</div>
              <div v-if="showmarket" class="font-extraSmall">{{item.groupname}}-{{item.marketname}}</div>
            </li>
          </ul>
        </div>
      </el-scrollbar>
      <!-- 分页控件 -->
      <pagination v-if="total > 0" :total="total" layout="total,prev,next" v-model:page="queryParams.currentpage" v-model:limit="queryParams.pagesize" @pagination="handleQuery" />
    </div>
    
    <!-- 右侧数据分析区域 -->
    <div class="right-content">
      <el-scrollbar class="screen-height gary-bg">
        <DataDeatils ref="dataDeatilsRef"/>
      </el-scrollbar>
    </div>
  </div>
</template>

<script setup>
import {ref,reactive,toRefs,onMounted}from"vue";
import DataDeatils from"./components/data_deatils.vue"
import Group from '@/components/header/group.vue';
import productAnysApi from '@/api/amazon/product/productAnysApi.js';

// 响应式状态
let state=reactive({
  tableData:[],
  total:10,
  showmarket:false,
  queryParams:{
    pagesize:10,
    currentpage:1,
    ftype:'sku',
  }
})

// 商品选择处理
function selectSku(row){
  state.tableData.forEach((item)=>{
    item.active = false;
  })
  row.active = true;
  dataDeatilsRef.value.show(row);
}

// 店铺分组变更处理
function groupChange(obj){
  state.queryParams.groupid=obj.groupid;
  state.queryParams.marketplaceid=obj.marketplaceid;
  if(state.queryParams.groupid&&state.queryParams.marketplaceid){
    state.showmarket=false;
  }else{
    state.showmarket=true;
  }
  handleQuery();
}

// 查询商品列表
function handleQuery(){
  productAnysApi.productAsinList(state.queryParams).then((res)=>{
    state.tableData=res.data.records;
    state.total=res.data.total;
    if(state.total>0){
      selectSku(res.data.records[0]);
    }
  });
}
</script>

2.2.2 数据详情组件(data_deatils.vue)

文件路径wimoor-ui/src/views/amazon/listing/analysis/components/data_deatils.vue

主要功能

核心代码结构

<template>
  <div class="gird-line-right">
    <!-- 商品基本信息卡片 -->
    <el-card>
      <el-row gutter="16">
        <el-col :span="16">
          <div class="p-b-h">
            <!-- 商品图片 -->
            <div>
              <el-image v-if="infoMap.image" :src="infoMap.image" class="img-size"></el-image>
              <el-image v-else :src="$require('empty/noimage40.png')" class="img-size"></el-image>
            </div>
            <!-- 商品信息 -->
            <div>
              <div class="name">{{infoMap.name}}</div>
              <div class="sku">{{infoMap.sku}}</div>
              <el-space class="font-extraSmall m-t-8">
                <span>ASIN:{{infoMap.asin}}</span>
                <el-divider direction="vertical"></el-divider>
                <span>首次上架日期:{{infoMap.opendate}}</span>
              </el-space>
              <div class="m-t-8" v-if="infoMap.anysisremark">
                <p>备注:{{infoMap.anysisremark}}</p>
              </div>
            </div>
          </div>
        </el-col>
        <!-- 操作按钮 -->
        <el-col :span="8" class="text-right">
          <el-space :size="16">
            <el-link @click="editRemarks" title="编辑备注" class="flex-center" :underline="false">
              <el-icon class="font-medium"><Edit /></el-icon>&nbsp;备注
            </el-link>
            <el-link title="跳转亚马逊" class="flex-center" target="_blank" :href="infoMap.link" :underline="false">
              <el-icon class="font-medium"><Link/></el-icon>&nbsp;跳转
            </el-link>
          </el-space>
        </el-col>
      </el-row>
    </el-card>
    
    <!-- 标签页导航 -->
    <el-tabs v-model="activeName" type="card" class="card-top-tabs m-t-16" @tab-change="loadChart">
      <el-tab-pane name="sales" label="销量"></el-tab-pane>
      <el-tab-pane name="hisrank" label="历史排名"></el-tab-pane>
      <el-tab-pane v-for="item in queryList" :label="item.name" :name="item.id" :class="item.name=='none'?'nopadding':''" :disabled="item.name=='none'?true:false">
        <template #label>
          <div @click.stop="showQueryDialog" class="custom-tabs-label pointer font-black" style="padding-left: 10px; padding-right: 10px;margin-left:-10px;margin-right:-10px" v-if="item.name=='none'">
            <el-icon><Plus /></el-icon>
          </div>
          <div class="custom-tabs-label" v-else>{{item.name}}</div>
        </template>
      </el-tab-pane>
    </el-tabs>
    
    <!-- 时间范围选择和图表展示 -->
    <el-card class="p-a-card">
      <template #header>
        <div class="flex-center-between">
          <el-space>
            <el-radio-group v-model="times" @change="changeTimes">
              <el-radio-button label="近7天" />
              <el-radio-button label="近30天" />
              <el-radio-button label="近90天" />
            </el-radio-group>
            <Datepicker ref="datepickersRef" :days="1" @changedate="changedate" />
          </el-space>
        </div>
      </template>
      <div class="p-a-body">
        <div class="p-a-right">
          <div id="anaysis-mycharts" class="my-chart"></div>
        </div>
      </div>
    </el-card>
    
    <!-- 数据表格 -->
    <el-card style="margin-top:10px;">
      <el-scrollbar style="width:calc(100vw - 350px);" always>
        <table class="sd-table">
          <tr>
            <td width="80px;">项目名称</td>
            <td width="80px;">汇总</td>
            <td v-for="label in labels" width="60px;">{{label}}</td>
          </tr>
          <tr v-for="(legend,index) in legends">
            <td width="80px;">{{legend}}</td>
            <td width="80px;">{{summary[index]}}</td>
            <td width="60px;" v-if="series && series[index] && series[index].data" v-for="item in series[index].data">
              {{item}}
            </td>
          </tr>
        </table>
      </el-scrollbar>
    </el-card>
    
    <!-- 备注编辑对话框 -->
    <el-dialog v-model="remarkVisable" title="备注">
      <el-input v-model="infoMap.remark2" type="textarea" :rows="3"></el-input>
      <template #footer>
        <el-button @click="remarkVisable=false">取消</el-button>
        <el-button type="primary" @click.stop="updateAnyRemark">确认</el-button>
      </template>
    </el-dialog>
    
    <!-- 指标选择对话框 -->
    <Dialog ref="dialogRef" @change="loadQueryList"></Dialog>
  </div>
</template>

<script setup>
import {ref,reactive,toRefs}from"vue"
import * as echarts from 'echarts';
import productAnysApi from '@/api/amazon/product/productAnysApi.js';
import queryFieldApi from '@/api/sys/tool/queryFieldApi.js';

// 响应式状态
let state=reactive({
  remarkVisable:false,
  activeName:"sales",
  times:"近7天",
  infoMap:{remark2:'',},
  isload:true,
  queryParams:{},
  labels:[],
  series:[],
  summary:[],
  legends:[],
  queryList:[{name:"none"}],
})

// 加载图表数据
function loadChart(){
  var ftype="";
  if(state.activeName=="hisrank"||state.activeName=="sales"){
    ftype=state.activeName;
  }else{
    state.queryList.forEach(item=>{
      if(state.activeName==item.id){
        ftype=item.queryfield;
      }
    })
  }
  if(ftype!="none"){
    setTimeout(function(){
      productAnysApi.getChartData({"sku":state.infoMap.sku,"marketplaceid":state.infoMap.marketplaceid,"groupid":state.infoMap.groupid,
      "ftype":ftype,"fromDate":state.queryParams.fromDate,"endDate":state.queryParams.endDate}).then((res)=>{
        if (res.data && res.data.length > 0) {
          var data=res.data;
          state.labels = data[0].labels;
          state.series = [];
          state.legends = [];
          var hasrightline=false;
          for (var i = 0; i < data.length; i++) {
            var name = data[i].name;
            if (name == '购物车比例' || name == '销售转化率' || name == '广告点击率' 
                    || name == 'Acos' || name == "AcoAs" || name == "广告转化率"
                        || name == "广告销量占比") {
              hasrightline=true;break;
            }
          }
          
          for (var i = 0; i < data.length; i++) {
            state.legends.push(data[i].name);
            var datas = {};
            state.summary[i]=0;
            data[i].data.forEach(item=>{
              if(item){
                state.summary[i]=state.summary[i]+parseFloat(item);
              }
            })
            datas.name = data[i].name;
            datas.type = "line";
            if (datas.name == '购物车比例' || datas.name == '销售转化率' || datas.name == '广告点击率' 
                    || datas.name == 'Acos' || datas.name == "AcoAs" || datas.name == "广告转化率"
                        || datas.name == "广告销量占比") {
              state.summary[i]=formatFloat(state.summary[i]/data[i].data.length)+" (avg)";
              datas.yAxisIndex = 1;
            } else if(hasrightline==true){
              datas.type = "bar";
              datas.barGap = "0%";
              datas.boundaryGap = "0%";
              datas.barMaxWidth = 32, datas.itemStyle = {
                normal : {
                  barBorderRadius : [ 4, 4, 0, 0 ]
                }
              };
            }
            if(hasrightline==false){
              datas.symbolSize = 0, datas.itemStyle = {
                normal : {
                  lineStyle : {
                    width : 2
                  }
                }
              };
            }
            datas.smooth = 0.5;
            datas.symbol = 'emptycircle';
            datas.data = data[i].data;
            datas.label={
              show:true,
            };
            datas.showAllSymbol=false;
            state.series.push(datas);
          }
          lineChart();
        }
        if(state.isload==true){
          state.isload=false;
        }
      });
    },500);
  }else{
    showQueryDialog();
  }
}

// 初始化图表
function lineChart() {
  if(myChart!=null){
    myChart.clear()
  }else{
    myChart =echarts.init(document.getElementById('anaysis-mycharts'));
  }
  var option = {
    tooltip : {
      trigger : 'axis',
      formatter : function(params) {
        var showHtm = "";
        for (var i = 0; i < params.length; i++) {
          var date = params[i].name;
          var name = params[i].seriesName;
          var value = params[i].value;
          if (name == '购物车比例' || name == '销售转化率' || name == '广告点击率'
                  || name == 'Acos' || name == "AcoAs" || name == "广告转化率"
                      || name == "广告销量占比") {
            showHtm += name + ": " + value + "%" + '<br>';
          } else {
            showHtm += name + ": " + value + '<br>';
          }
        }
        showHtm = date + '<br>' + showHtm;
        return showHtm;
      },
      axisPointer : {
        type : 'line',
        lineStyle : {
          color : '#ccc',
          width : 1,
          type : 'solid'
        },
      }
    },
    legend : {
      data : state.legends,
      y : 'top',
      x : 'center',
    },
    color : [ '#ffa400', '#75D6AA', '#EB6A79', '#7AA5DA', '#d69bf2',
            '#59f3e3', '#8875ff', '#e0e0e5', '#ff8559', '#00FF7F', '#00FF7F' ],
    grid : {
      x : 50,
      x2 : 50,
      y : 50,
      y2 :50,
      borderWidth : 0,
    },
    calculable : false,
    xAxis : [ {
      axisLabel : {
        show : true,
        textStyle : {
          color : '#999'
        }
      },
      splitLine : {
        lineStyle : {
          color : '#f1f1f1',
          width : 1,
        }
      },
      axisTick : {
        show : false,
        lineStyle : {
          color : '#f1f1f1'
        }
      },
      axisLine : {
        lineStyle : {
          color : '#f1f1f1',
          width : 1,
        }
      },
      type : 'category',
      boundaryGap : true,
      data : state.labels
    } ],
    yAxis : [ {
      axisLabel : {
        show : true,
        textStyle : {
          color : '#999'
        },
      },
      splitLine : {
        lineStyle : {
          color : '#f1f1f1',
          width : 1,
        }
      },
      axisLine : {
        lineStyle : {
          color : '#f1f1f1',
          width : 1,
        }
      },
    }, {
      axisLabel : {
        show : true,
        textStyle : {
          color : '#999'
        },
      },
      splitLine : {
        show:false,
      },
      axisLine : {
        lineStyle : {
          color : '#f1f1f1',
          width : 1,
        }
      },
    }, ],
    series : state.series
  };
  myChart.setOption(option);
  window.addEventListener('resize',()=>{
    myChart.resize();
  });
}

// 显示商品详情
function show(row){
  if(row.id){
    productAnysApi.productdetail({"pid":row.id}).then((res)=>{
      state.infoMap=res.data;
      state.infoMap.link="https://"+row.point_name+"/dp/"+res.data.asin;
      loadChart();
    });
  }
  loadQueryList();
}

// 导出组件方法
defineExpose({show})
</script>

2.3 API接口分析

文件路径wimoor-ui/src/api/amazon/product/productAnysApi.js

API接口列表

接口名称 URL 方法 功能描述
productAsinList /amazon/api/v1/report/product/analysis/productAsinList POST 获取商品ASIN列表
productdetail /amazon/api/v1/report/product/analysis/productdetail GET 获取商品详情
productdetailByInfo /amazon/api/v1/report/product/analysis/productdetailByInfo GET 通过SKU和市场获取商品详情
updateAnyRemark /amazon/api/v1/report/product/analysis/updateAnyRemark GET 更新商品备注
getChartData /amazon/api/v1/report/product/analysis/getChartData GET 获取图表数据

3. 后端实现

3.1 核心文件结构

wimoor-amazon/amazon-boot/src/main/java/com/wimoor/amazon/product/
├── controller/
│   └── ProductAnalysisController.java   # 商品分析控制器
├── service/
│   ├── IProductInOrderService.java       # 商品订单服务接口
│   └── impl/
│       └── ProductInOrderServiceImpl.java # 商品订单服务实现
└── mapper/
    ├── ProductInOrderMapper.java         # 商品订单数据访问
    ├── ProductRankMapper.java            # 商品排名数据访问
    └── ProductInOptMapper.java           # 商品操作数据访问

3.2 核心代码分析

3.2.1 控制器层(ProductAnalysisController.java)

文件路径wimoor-amazon/amazon-boot/src/main/java/com/wimoor/amazon/product/controller/ProductAnalysisController.java

主要功能

核心代码

@Api(tags = "商品分析")
@RestController
@SystemControllerLog("商品分析")
@RequestMapping("/api/v1/report/product/analysis")
@RequiredArgsConstructor
public class ProductAnalysisController {
    @Autowired
    IProductInfoService iProductInfoService;
    final IAmazonAuthorityService amazonAuthorityService;
    final IProductInOrderService iProductInOrderService;
    final IProductInOptService iProductInOptService;
    
    @PostMapping("/productAsinList")
    public Result<IPage<Map<String, Object>>> productListAction(@RequestBody ProductListDTO query) {
        String search = query.getSearch();
        String searchtype = query.getFtype();
        Map<String, Object> parameter = new HashMap<String, Object>();
        parameter.put("searchtype", searchtype);
        parameter.put("search", search != null && !search.isEmpty() ? "%" + search.trim() + "%" : null);
        String marketplaceid = query.getMarketplaceid();
        String groupid = query.getGroupid();
        parameter.put("marketplaceid", marketplaceid != null && !marketplaceid.isEmpty() && !"all".equals(marketplaceid) ? marketplaceid.trim() : null);
        UserInfo userinfo = UserInfoContext.get();
        if (userinfo.isLimit(UserLimitDataType.operations)) {
            parameter.put("myself", userinfo.getId());
        }
        
        parameter.put("shopid", userinfo.getCompanyid());
        if(!"all".equals(groupid)&&StrUtil.isNotEmpty(groupid)) {
            if(StrUtil.isNotEmpty(marketplaceid)) {
                AmazonAuthority auth = amazonAuthorityService.selectByGroupAndMarket(groupid, marketplaceid);
                if(auth!=null) {
                    parameter.put("amazonAuthId", auth.getId());
                }
            }
        }
        
        if (StrUtil.isBlankOrUndefined(groupid)||"all".equals(groupid)) {
            parameter.put("groupid", null);
            if(userinfo.getGroups()!=null&&userinfo.getGroups().size()>0) {
                parameter.put("groupList", userinfo.getGroups());
            }
        } else {
            parameter.put("groupid", groupid);
        }
        IPage<Map<String, Object>> list = iProductInfoService.getAsinList(query.getPage(), parameter);
        return Result.success(list);
    }
    
    @GetMapping("/productdetail")
    public Result<Map<String, Object>> productListAction(String pid) {
        UserInfo userinfo = UserInfoContext.get();
        return Result.success(iProductInOrderService.selectDetialById(pid, userinfo.getCompanyid()));
    }
    
    @GetMapping("/productdetailByInfo")
    public Result<Map<String, Object>> productdetailByInfoAction(String sku, String marketplaceid, String sellerid, String groupid) {
        UserInfo userinfo = UserInfoContext.get();
        AmazonAuthority auth = amazonAuthorityService.selectByGroupAndMarket(groupid, marketplaceid);
        if(auth!=null) {
            LambdaQueryWrapper<ProductInfo> queryWrapper = new LambdaQueryWrapper<ProductInfo>();
            queryWrapper.eq(ProductInfo::getAmazonAuthId, auth.getId());
            queryWrapper.eq(ProductInfo::getMarketplaceid, marketplaceid);
            queryWrapper.eq(ProductInfo::getSku, sku);
            ProductInfo info = iProductInfoService.getOne(queryWrapper);
            if(info!=null) {
                return Result.success(iProductInOrderService.selectDetialById(info.getId(), userinfo.getCompanyid()));
            } else {
                return Result.failed();
            }
        } else {
            return Result.failed();
        }
    }
    
    @SystemControllerLog("修改分析备注")
    @GetMapping("/updateAnyRemark")
    public Result<?> updateAnyRemarkAction(String pid, String remark) {
        ProductInOpt opt = iProductInOptService.getById(pid);
        if(opt!=null) {
            opt.setRemarkAnalysis(remark);
            return Result.success(iProductInOptService.updateById(opt));
        } else {
            opt = new ProductInOpt();
            opt.setPid(new BigInteger(pid));
            opt.setRemarkAnalysis(remark);
            return Result.success(iProductInOptService.save(opt));
        }
    }
    
    @GetMapping("/getChartData")
    public Result<List<Map<String, Object>>> getChartDataAction(String sku, String marketplaceid, String groupid, String ftype, String fromDate, String endDate) {
        List<Map<String, Object>> maps = null;
        UserInfo user = UserInfoContext.get();
        Map<String, Object> parameter = new HashMap<String, Object>();
        parameter.put("shopid", user.getCompanyid());
        parameter.put("marketplace", marketplaceid != null && !marketplaceid.isEmpty() ? marketplaceid.trim() : null);
        parameter.put("groupid", groupid != null && !groupid.isEmpty() ? groupid.trim() : null);

        if (StrUtil.isEmpty(groupid) || StrUtil.isEmpty(marketplaceid)) {
            throw new BizException("必须有店铺和站点");
        }
        AmazonAuthority amazonAuthority = amazonAuthorityService.selectByGroupAndMarket(groupid, marketplaceid);
        if (amazonAuthority != null) {
            parameter.put("amazonAuthId", amazonAuthority.getId());
        }
        parameter.put("userid", user.getId());
        String beginDate = fromDate;
        Map<String, Integer> ftypeset = new HashMap<String, Integer>();
        if ("sales".equals(ftype)) {
            ftypeset.put("uns", 0);
            ftypeset.put("ods", 1);
        } else if ("hisrank".equals(ftype)) {
            ftypeset.put("rnks", 0);
        } else {
            String[] ftypeStr = ftype.split(",");
            for (int i = 0; i < ftypeStr.length; i++) {
                if(StrUtil.isNotEmpty(ftypeStr[i])) {
                    ftypeset.put(ftypeStr[i], i);
                }
            }
        }
        if (StrUtil.isNotEmpty(ftype)) {
            maps = iProductInOrderService.getChartData(ftypeset, parameter, user);
        } else {
            maps = null;
        }
        return Result.success(maps);
    }
}

3.2.2 服务层(ProductInOrderServiceImpl.java)

文件路径wimoor-amazon/amazon-boot/src/main/java/com/wimoor/amazon/product/service/impl/ProductInOrderServiceImpl.java

主要功能

核心代码

@Service
@RequiredArgsConstructor
public class ProductInOrderServiceImpl extends ServiceImpl<ProductInOrderMapper, ProductInOrder> implements IProductInOrderService {
    
    final ProductRankMapper productRankMapper;
    final IAmzProductPageviewsService iAmzProductPageviewsService;
    final IAmazonAuthorityService iAmazonAuthorityService;
    final IOrdersSumService iOrdersSumService;
    final ProductInOptMapper productInOptMapper;
    
    @Override
    public Map<String, Object> selectDetialById(String pid, String shopid) {
        return this.baseMapper.selectDetialById(pid, shopid);
    }

    @Override
    public List<Map<String, Object>> getChartData(Map<String, Integer> typesMap, Map<String, Object> parameter, UserInfo user) {
        String sku = parameter.get("sku") == null ? null : (String) parameter.get("sku");
        String ftype = parameter.get("ftype") == null ? null : (String) parameter.get("ftype");
        String marketplace = parameter.get("marketplace") == null ? null : (String) parameter.get("marketplace");
        String beginDate = parameter.get("beginDate") == null ? null : (String) parameter.get("beginDate");
        String endDate = parameter.get("endDate") == null ? null : (String) parameter.get("endDate");
        String amazonAuthId = parameter.get("amazonAuthId") == null ? null : parameter.get("amazonAuthId").toString();
        
        // 销量汇总
        List<AmzProductPageviews> sessionlist = null;
        List<ProductRank> rnklist = null;
        List<Map<String, Object>> advlist = null;
        List<OrdersSummary> orderlist = null;
        List<Map<String, Object>> ftypeList = null;
        List<Map<String, Object>> rankList = null;
        
        if (typesMap.containsKey("rnk")) {// sales rank
            rnklist = this.productRank(sku, marketplace, beginDate, endDate, amazonAuthId, user, ftype);
        }
        if (typesMap.containsKey("rnks")) {// sales rank 提取多条分类排名
            ftypeList = this.getCountRankBySku(sku, marketplace, amazonAuthId, user);
            if (ftypeList != null && ftypeList.size() > 0) {
                rankList = this.getRankBySku(sku, marketplace, beginDate, endDate, amazonAuthId, user);
                for (int i = 0; i < ftypeList.size(); i++) {
                    typesMap.put(ftypeList.get(i).get("name").toString(), i + 1);
                }
            } else {
                rankList = new ArrayList<Map<String, Object>>();
                typesMap.put("销售排名", 1);
            }
        }
        if (typesMap.containsKey("uns") || typesMap.containsKey("ods") 
                || typesMap.containsKey("pts") || typesMap.containsKey("aups")) {
            // total order item 订单量 // units orders 销售数量, 商品总销售额, 广告销量占比
            orderlist = iOrdersSumService.orderSummaryBySkuDate(sku, marketplace, beginDate, endDate, amazonAuthId, user, ftype);
        }
        if (typesMap.containsKey("cks")// clicks 点击量
                || typesMap.containsKey("imp") // impressions 广告展示量
                || typesMap.containsKey("ctr") // 广告点击率ctr
                || typesMap.containsKey("spd") // adv spend 广告费用
                || typesMap.containsKey("cpc") // CPC
                || typesMap.containsKey("cr") // total order/click 转化率
                || typesMap.containsKey("acos") // total order/click 转化率
                || typesMap.containsKey("tos") // total sales广告销售额
                || typesMap.containsKey("acoas") 
                || typesMap.containsKey("aus") // 广告销量
                || typesMap.containsKey("aups")) {// 广告销量占比
            advlist = this.advInfo(sku, marketplace, beginDate, endDate, amazonAuthId, user, ftype);
            if (typesMap.containsKey("acoas") && orderlist == null) {
                orderlist = iOrdersSumService.orderSummaryBySkuDate(sku, marketplace, beginDate, endDate, amazonAuthId, user, ftype);
            }
        }
        if (typesMap.containsKey("ses")// session 页面访问量
                || typesMap.containsKey("pgv") // pageview 页面浏览量,
                || typesMap.containsKey("bbp") || typesMap.containsKey("osp")) {// Unit_Session_Percentage
            sessionlist = this.sessionPage(sku, marketplace, beginDate, endDate, amazonAuthId, user, ftype);
        }

        List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
        for (Entry<String, Integer> typeentry : typesMap.entrySet()) {
            ftype = typeentry.getKey();
            Map<String, Object> map = new HashMap<String, Object>();
            SimpleDateFormat sdf = new SimpleDateFormat("MM.dd");
            Map<String, Object> tempmap = new HashMap<String, Object>();
            
            if ("ses".equals(ftype)) {// session 页面访问量
                for (int i = 0; i < sessionlist.size(); i++) {
                    AmzProductPageviews item = sessionlist.get(i);
                    tempmap.put(sdf.format(GeneralUtil.getDate(item.getByday())), item.getSessions());
                }
                map.put("name", "访问人数");
            } else if ("pgv".equals(ftype)) {// pageview 页面浏览量
                for (int i = 0; i < sessionlist.size(); i++) {
                    AmzProductPageviews item = sessionlist.get(i);
                    tempmap.put(sdf.format(GeneralUtil.getDate(item.getByday())), item.getPageViews());
                }
                map.put("name", "页面浏览数量");
            } else if ("bbp".equals(ftype)) {// BuyBox percentage 购物车比例
                for (int i = 0; i < sessionlist.size(); i++) {
                    AmzProductPageviews item = sessionlist.get(i);
                    tempmap.put(sdf.format(GeneralUtil.getDate(item.getByday())), item.getBuyBoxPercentage());
                }
                map.put("name", "购物车比例");
            } else if ("osp".equals(ftype)) {// Unit_Session_Percentage 销售转化率
                for (int i = 0; i < sessionlist.size(); i++) {
                    AmzProductPageviews item = sessionlist.get(i);
                    tempmap.put(sdf.format(GeneralUtil.getDate(item.getByday())), item.getUnitSessionPercentage());
                }
                map.put("name", "销售转化率");
            } else if ("uns".equals(ftype)) {// units orders 销售数量
                for (int i = 0; i < orderlist.size(); i++) {
                    OrdersSummary item = orderlist.get(i);
                    tempmap.put(sdf.format(item.getPurchaseDate()), item.getQuantity());
                }
                map.put("name", "销量");
            } else if ("pts".equals(ftype)) {// 商品总销售额
                for (int i = 0; i < orderlist.size(); i++) {
                    OrdersSummary item = orderlist.get(i);
                    tempmap.put(sdf.format(item.getPurchaseDate()), item.getOrderprice());
                }
                map.put("name", "销售额");
            } else if ("ods".equals(ftype)) {// total order item 订单量
                for (int i = 0; i < orderlist.size(); i++) {
                    OrdersSummary item = orderlist.get(i);
                    tempmap.put(sdf.format(item.getPurchaseDate()), item.getOrdersum());
                }
                map.put("name", "销售订单");
            } else if (ftype.equals("rnk")) {
                for (int i = 0; i < rnklist.size(); i++) {
                    ProductRank item = rnklist.get(i);
                    tempmap.put(sdf.format(item.getByday()), item.getRank());
                }
                map.put("name", "销量排名");
            } else if (typesMap.containsKey("rnks")) {
                if (ftype.equals("rnks")) {
                    continue;
                }
                for (Map<String, Object> rankMap : rankList) {
                    String rankName = rankMap.get("name").toString();
                    if (ftype.equals(rankName)) {
                        tempmap.put(sdf.format(rankMap.get("byday")), rankMap.get("rank"));
                    }
                    continue;
                }
                map.put("name", ftype);
            } else {
                if ("acoas".equals(ftype)) {
                    long diff = GeneralUtil.getDatez(endDate).getTime() - GeneralUtil.getDatez(beginDate).getTime();
                    long daysize = diff / (1000 * 60 * 60 * 24);
                    Calendar c = Calendar.getInstance();
                    c.setTime(GeneralUtil.getDatez(beginDate));
                    Map<String, Object> costmap = new HashMap<String, Object>();
                    Map<String, Object> salesmap = new HashMap<String, Object>();
                    for (int i = 0; i < advlist.size(); i++) {
                        Map<String, Object> item = advlist.get(i);
                        costmap.put(sdf.format(item.get("bydate")), item.get("spd"));
                    }
                    for (int i = 0; i < orderlist.size(); i++) {
                        OrdersSummary item = orderlist.get(i);
                        salesmap.put(sdf.format(item.getPurchaseDate()), item.getOrderprice());
                    }
                    for (int i = 1; i <= daysize; i++, c.add(Calendar.DATE, 1)) {
                        String tempkey = sdf.format(c.getTime());
                        BigDecimal sales = new BigDecimal("0");
                        Object obj = salesmap.get(tempkey);
                        if (obj != null) {
                            sales = new BigDecimal(obj.toString());
                        }
                        BigDecimal cost = new BigDecimal("0");
                        Object obj2 = costmap.get(tempkey);
                        if (obj2 != null) {
                            cost = new BigDecimal(obj2.toString());
                        }
                        BigDecimal acoas = new BigDecimal("0");
                        if (sales.compareTo(new BigDecimal("0")) != 0) {
                            acoas = cost.multiply(new BigDecimal("100")).divide(sales, 2, RoundingMode.HALF_DOWN);
                        }
                        tempmap.put(tempkey, acoas);
                    }
                } else if ("aups".equals(ftype)) {//广告销量占比=广告订单销量/总销量
                    long diff = GeneralUtil.getDatez(endDate).getTime() - GeneralUtil.getDatez(beginDate).getTime();
                    long daysize = diff / (1000 * 60 * 60 * 24);
                    Calendar c = Calendar.getInstance();
                    c.setTime(GeneralUtil.getDatez(beginDate));
                    Map<String, Object> adUnitsmap = new HashMap<String, Object>();
                    Map<String, Object> totalUnitsmap = new HashMap<String, Object>();
                    for (int i = 0; i < advlist.size(); i++) {
                        Map<String, Object> item = advlist.get(i);
                        adUnitsmap.put(sdf.format(item.get("bydate")), item.get("aus"));
                    }
                    for (int i = 0; i < orderlist.size(); i++) {
                        OrdersSummary item = orderlist.get(i);
                        totalUnitsmap.put(sdf.format(item.getPurchaseDate()), item.getQuantity());
                    }
                    for (int i = 1; i <= daysize; i++, c.add(Calendar.DATE, 1)) {
                        String tempkey = sdf.format(c.getTime());
                        BigDecimal totalUnits = new BigDecimal("0");
                        Object obj = totalUnitsmap.get(tempkey);
                        if (obj != null) {
                            totalUnits = new BigDecimal(obj.toString());
                        }
                        BigDecimal adUnits = new BigDecimal("0");
                        Object obj2 = adUnitsmap.get(tempkey);
                        if (obj2 != null) {
                            adUnits = new BigDecimal(obj2.toString());
                        }
                        BigDecimal aups = new BigDecimal("0");
                        if (totalUnits.compareTo(new BigDecimal("0")) != 0) {
                            aups = adUnits.multiply(new BigDecimal("100")).divide(totalUnits, 2, RoundingMode.HALF_EVEN);
                        }
                        tempmap.put(tempkey, aups);
                    }
                    map.put("name", "广告销量占比");
                } else if(advlist!=null) {
                    for (int i = 0; i < advlist.size(); i++) {
                        Map<String, Object> item = advlist.get(i);
                        tempmap.put(sdf.format(item.get("bydate")), item.get(ftype));
                    }
                }
                if (ftype.equals("cks")) {
                    map.put("name", "广告点击量");
                } else if (ftype.equals("imp")) {
                    map.put("name", "广告展示量");
                } else if (ftype.equals("ctr")) {
                    map.put("name", "广告点击率");
                } else if (ftype.equals("spd")) {
                    map.put("name", "广告花费");
                } else if (ftype.equals("cpc")) {
                    map.put("name", "广告点击花费");
                } else if (ftype.equals("acos")) {
                    map.put("name", "Acos");
                } else if (ftype.equals("acoas")) {
                    map.put("name", "AcoAs");
                } else if (ftype.equals("cr")) {
                    map.put("name", "广告转化率");
                } else if (ftype.equals("tos")) {
                    map.put("name", "广告销售额");
                } else if (ftype.equals("aus")) {
                    map.put("name", "广告销量");
                }
            }
            
            // 处理日期范围和数据填充
            long diff = GeneralUtil.getDatez(endDate).getTime() - GeneralUtil.getDatez(beginDate).getTime();
            long daysize = diff / (1000 * 60 * 60 * 24);
            Calendar c = Calendar.getInstance();
            c.setTime(GeneralUtil.getDatez(beginDate));
            BigDecimal summary = new BigDecimal("0");
            List<String> listLabel = new ArrayList<String>();
            List<String> listData = new ArrayList<String>();
            for (int i = 1; i <= daysize+1; i++, c.add(Calendar.DATE, 1)) {
                String tempkey = sdf.format(c.getTime());
                String value = tempmap.get(tempkey) == null ? "0" : tempmap.get(tempkey).toString();
                listLabel.add(tempkey);
                listData.add(value);
                summary = summary.add(new BigDecimal(value));
            }
            map.put("summary", summary);
            map.put("labels", listLabel);
            map.put("data", listData);
            listMap.add(map);
        }
        return listMap;
    }
    
    // 获取商品排名数据
    private List<ProductRank> productRank(String sku, String marketplace, String beginDate, String endDate, String amazonAuthId, UserInfo user, String ftype) {
        Map<String, Object> param = new HashMap<String, Object>();
        param.put("sku", sku);
        param.put("marketplaceid", marketplace);
        param.put("begindate", beginDate);
        param.put("enddate", endDate);
        param.put("amazonAuthId", amazonAuthId);
        param.put("shopid", user.getCompanyid());
        List<ProductRank> list = productRankMapper.selectBySku(param);
        return list;
    }

    // 获取商品排名分类
    private List<Map<String, Object>> getCountRankBySku(String sku, String marketplace, String amazonAuthId, UserInfo user) {
        Map<String, Object> param = new HashMap<String, Object>();
        param.put("sku", sku);
        param.put("marketplaceid", marketplace);
        param.put("amazonAuthId", amazonAuthId);
        param.put("shopid", user.getCompanyid());
        List<Map<String, Object>> list = productRankMapper.selectCountRankBySku(param);
        return getProductRank(list);
    }

    // 获取商品排名数据
    private List<Map<String, Object>> getRankBySku(String sku, String marketplace, String beginDate, String endDate, String amazonAuthId, UserInfo user) {
        Map<String, Object> param = new HashMap<String, Object>();
        param.put("sku", sku);
        param.put("marketplaceid", marketplace);
        param.put("begindate", beginDate);
        param.put("enddate", endDate);
        param.put("amazonAuthId", amazonAuthId);
        param.put("shopid", user.getCompanyid());
        List<Map<String, Object>> list = productRankMapper.selectRankBySku(param);
        return getProductRank(list);
    }

    // 获取广告数据
    private List<Map<String, Object>> advInfo(String sku, String marketplace, String beginDate, String endDate, String amazonAuthId, UserInfo user, String ftype) {
        Map<String, Object> param = new HashMap<String, Object>();
        param.put("sku", sku);
        param.put("marketplaceid", marketplace);
        param.put("startdate", beginDate);
        param.put("enddate", endDate);
        param.put("amazonAuthId", amazonAuthId);
        param.put("shopid", user.getCompanyid());
        List<Map<String, Object>> list = null;
        AmazonAuthority auth = iAmazonAuthorityService.getById(amazonAuthId);
        param.put("sellerid", auth.getSellerid());
        list=productInOptMapper.findAdvert(param);
        return list;
    }
    
    // 处理商品排名数据
    public List<Map<String, Object>> getProductRank(List<Map<String, Object>> list) {
        if (list != null) {
            Iterator<Map<String, Object>> iterator = list.iterator();
            while (iterator.hasNext()) {
                Map<String, Object> map = iterator.next();
                Object name = map.get("name");
                if (name == null) {
                    Object categoryId = map.get("categoryId");
                    try {
                        new BigInteger(categoryId.toString());
                        iterator.remove();
                    } catch (Exception e) {
                        String strName = categoryId.toString().replace("_display_on_website", "").replace("_and_", "&").replace("_", " ");
                        String[] categoryName = strName.toString().split("&");
                        String str = "";
                        for (int i = 0; i < categoryName.length; i++) {
                            String str1 = categoryName[i].substring(0, 1).toUpperCase() + categoryName[i].substring(1);
                            if (i == categoryName.length - 1) {
                                str += str1;
                            } else {
                                str += str1 + " & ";
                            }
                        }
                        map.put("name", str);
                    }
                }
            }
        }
        return list;
    }
    
    // 获取页面访问数据
    public List<AmzProductPageviews> sessionPage(String sku, String marketplaceid, String startdate, String enddate, String amazonAuthId, UserInfo user, String ftype) {
        Map<String, Object> param = new HashMap<String, Object>();
        param.put("sku", sku);
        param.put("marketplaceid", marketplaceid);
        param.put("startDate", startdate);
        param.put("endDate", enddate);
        param.put("amazonAuthId", amazonAuthId);
        param.put("shopid", user.getCompanyid());
        List<AmzProductPageviews> list = iAmzProductPageviewsService.findPageviews(param);
        return list;
    }
}

4. 核心功能分析

4.1 商品列表查询

功能描述:根据店铺分组、市场和搜索条件查询商品列表

实现流程

  1. 前端调用 productAnysApi.productAsinList() 方法
  2. 后端 ProductAnalysisController.productListAction() 处理请求
  3. 构建查询参数,包括搜索类型、搜索关键词、店铺分组、市场等
  4. 调用 iProductInfoService.getAsinList() 获取商品列表
  5. 返回分页后的商品列表数据

4.2 商品详情查询

功能描述:根据商品ID查询商品详细信息

实现流程

  1. 前端调用 productAnysApi.productdetail() 方法
  2. 后端 ProductAnalysisController.productListAction() 处理请求
  3. 调用 iProductInOrderService.selectDetialById() 获取商品详情
  4. 返回商品的基本信息、图片、ASIN等数据

4.3 图表数据获取

功能描述:根据商品SKU、市场、时间范围和指标类型获取图表数据

实现流程

  1. 前端调用 productAnysApi.getChartData() 方法
  2. 后端 ProductAnalysisController.getChartDataAction() 处理请求
  3. 构建查询参数,解析指标类型
  4. 调用 iProductInOrderService.getChartData() 获取图表数据
  5. 根据指标类型获取不同的数据:
    • 销量数据:从订单汇总服务获取
    • 排名数据:从商品排名数据获取
    • 流量数据:从页面访问服务获取
    • 广告数据:从广告数据获取
  6. 处理数据格式,计算汇总值,填充日期范围
  7. 返回格式化的图表数据

4.4 商品备注管理

功能描述:更新商品的分析备注

实现流程

  1. 前端调用 productAnysApi.updateAnyRemark() 方法
  2. 后端 ProductAnalysisController.updateAnyRemarkAction() 处理请求
  3. 查询是否存在商品操作记录
  4. 存在则更新备注,不存在则创建新记录
  5. 返回更新结果

4.5 自定义指标分析

功能描述:支持用户自定义指标组合进行分析

实现流程

  1. 前端点击"+"标签页,打开指标选择对话框
  2. 用户选择需要分析的指标
  3. 前端创建自定义指标标签页
  4. 调用 productAnysApi.getChartData() 获取选定指标的数据
  5. 后端根据指标类型组合返回相应的数据
  6. 前端在图表中展示选定指标的趋势

5. 技术亮点

5.1 前端技术亮点

  1. 组件化设计:采用Vue 3 Composition API,将页面拆分为主组件和详情组件,提高代码复用性
  2. 响应式布局:使用Element Plus的栅格系统,实现自适应布局
  3. 数据可视化:集成ECharts图表库,支持多种图表类型和交互方式
  4. 无限滚动加载:实现商品列表的无限滚动加载,提升用户体验
  5. 实时数据更新:通过响应式状态管理,实现数据的实时更新和展示

5.2 后端技术亮点

  1. 服务分层:采用控制器→服务→数据访问的分层架构,职责清晰
  2. 数据整合:整合多个数据源的数据,包括订单、排名、流量和广告数据
  3. 动态指标计算:支持根据用户选择的指标动态计算和返回数据
  4. 日期范围处理:智能处理日期范围,填充缺失数据,确保数据连续性
  5. 性能优化:通过缓存和批量查询,优化数据查询性能

5.3 整体架构亮点

  1. 前后端分离:采用RESTful API设计,实现前后端解耦
  2. 数据一致性:确保前端展示的数据与后端存储的数据一致
  3. 可扩展性:模块化设计,支持轻松添加新的指标和分析维度
  4. 安全性:实现用户权限控制,确保数据安全
  5. 可维护性:代码结构清晰,文档完善,易于维护和扩展

6. 代码优化建议

6.1 前端优化建议

  1. 性能优化

    • 实现商品列表的虚拟滚动,减少DOM节点数量
    • 优化图表渲染,避免频繁重绘
    • 使用缓存机制,减少重复请求
  2. 代码质量

    • 提取重复的图表配置为可复用的函数
    • 优化响应式状态管理,避免不必要的状态更新
    • 添加TypeScript类型定义,提高代码可读性和可维护性
  3. 用户体验

    • 添加数据加载状态和错误处理
    • 实现图表的导出功能
    • 优化移动端适配

6.2 后端优化建议

  1. 性能优化

    • 实现数据缓存,减少数据库查询
    • 优化SQL查询,添加适当的索引
    • 使用异步处理,提高并发性能
  2. 代码质量

    • 提取重复的日期处理逻辑为工具类
    • 优化异常处理,提供更详细的错误信息
    • 添加单元测试,提高代码覆盖率
  3. 功能扩展

    • 支持更多的指标类型和分析维度
    • 实现数据导出功能
    • 添加数据预测和趋势分析功能

6.3 数据库优化建议

  1. 索引优化

    • 为常用查询字段添加索引
    • 优化复合索引的设计
  2. 表结构优化

    • 考虑分区表,提高大数据量查询性能
    • 优化字段类型和长度
  3. 数据清理

    • 实现历史数据的归档策略
    • 定期清理无效数据

7. 总结

销售商品分析模块是Wimoor系统中一个功能强大的亚马逊商品数据分析工具,通过前后端分离的架构,实现了多维度数据的整合和可视化展示。该模块支持销量、排名、流量、广告等多个维度的数据分析,为用户提供了全面的商品表现视图。

7.1 模块价值

7.2 技术实现

7.3 未来展望

  1. 功能扩展

    • 支持更多的数据分析维度和指标
    • 实现数据预测和趋势分析
    • 添加竞品对比分析功能
  2. 技术升级

    • 采用更先进的前端框架和状态管理方案
    • 引入大数据处理技术,提高数据处理能力
    • 实现实时数据更新和推送
  3. 用户体验优化

    • 提供更个性化的数据分析视图
    • 实现更丰富的图表交互功能
    • 优化移动端体验

销售商品分析模块通过技术创新和功能优化,为亚马逊卖家提供了强大的数据分析工具,帮助他们更好地理解商品表现,优化运营策略,实现销售增长。

广告-广告管理

广告管理模块功能解析文档

1. 系统架构

广告管理模块采用前后端分离架构,基于Vue 3前端框架和Spring Boot后端框架实现,集成亚马逊广告API,提供完整的广告管理功能。

1.1 技术栈

分类 技术 版本 用途
前端框架 Vue 3.x 构建用户界面,使用Composition API
前端UI库 Element Plus 最新版 提供组件库和样式
数据可视化 ECharts 最新版 展示广告数据图表
HTTP客户端 Axios 最新版 与后端API通信
状态管理 Vuex 4.x 管理全局状态
后端框架 Spring Boot 2.x 构建RESTful API
ORM框架 MyBatis Plus 最新版 数据库操作
亚马逊API Amazon Advertising API 最新版 与亚马逊广告平台交互
数据库 MySQL 5.7+ 存储广告数据和配置

1.2 架构分层

前端架构

后端架构

1.3 核心流程图

sequenceDiagram
    participant Frontend as 前端
    participant Backend as 后端API
    participant AmazonAPI as 亚马逊广告API
    participant DB as 数据库

    Frontend->>Backend: 请求广告活动列表
    Backend->>DB: 查询广告配置
    Backend->>AmazonAPI: 调用ListCampaigns接口
    AmazonAPI-->>Backend: 返回广告活动数据
    Backend->>DB: 存储/更新广告数据
    Backend-->>Frontend: 返回处理后的数据
    Frontend->>Frontend: 渲染广告活动列表

    Frontend->>Backend: 创建新广告活动
    Backend->>AmazonAPI: 调用CreateCampaigns接口
    AmazonAPI-->>Backend: 返回创建结果
    Backend->>DB: 存储新广告活动信息
    Backend-->>Frontend: 返回创建结果

2. 前端实现

2.1 主组件结构

广告管理模块的主组件是 index.vue,采用左右分栏布局:

2.2 组件分类

根据广告类型,前端组件分为三大类:

SP广告组件 (Sponsored Products)

SB广告组件 (Sponsored Brands)

SD广告组件 (Sponsored Display)

2.3 核心功能实现

2.3.1 广告树结构

广告树组件 ad_tree.vue 实现了广告账户、广告活动、广告组的层级展示:

// 核心逻辑:构建广告树数据结构
function buildAdTree(data) {
  // 构建账户节点
  // 构建广告活动节点
  // 构建广告组节点
  return treeData;
}

// 节点点击事件
function handleNodeClick(data) {
  // 发送数据到父组件
  emit('change', data);
}

2.3.2 标签页管理

根据广告类型和操作对象,动态生成标签页:

// 标签页数据
const tabsDataValue = [
  {name: '广告活动', value: 'adcams', count: ''},
  {name: '广告组', value: 'adgroups', count: ''},
  {name: '商品', value: 'ProductAds', count: ''},
  {name: '关键词', value: 'adkey', count: ''},
  // 其他标签页...
];

// 根据广告类型过滤标签页
function getTabs(filterTabs) {
  var list = [];
  state.tabsDataValue.forEach(item => {
    if(filterTabs.includes(item.value)) {
      // 特殊处理SB广告的"广告"标签
      if(item.value == "ProductAds" && state.queryParams.campaignType == "SB") {
        item.name = "广告";
      }
      list.push(item);
    }
  });
  return list;
}

2.3.3 数据加载与展示

根据当前选择的标签页和广告类型,加载对应数据:

function handleQuery() {
  state.queryParams.ftype = state.activeName;
  var activeName = state.activeName;
  if(state.queryParams.profileid) {
    nextTick(() => {
      if(state.queryParams.campaignType == "SP") {
        if(activeName == 'adcams') {
          spListCampaignsRef.value.show(state.queryParams);
        }
        // 其他标签页...
      }
      // SB和SD广告类型的处理...
    });
  }
}

2.4 API调用

前端通过封装的API模块与后端通信:

广告管理API

// advertApi.js
export default {
  loadProfile,          // 加载广告配置文件
  addSerchHistory,      // 添加搜索历史
  getSerchHistory,      // 获取搜索历史
  deleteSerchHistory,   // 删除搜索历史
  loadCampaignsNotArchived, // 加载未归档的广告活动
  findPortfoliosForProfileId, // 查找广告组合
  getallsumtype,        // 获取所有汇总类型
  saleorder,            // 销售订单数据
  cpcdata,              // CPC数据
};

广告活动API

// advCampaignApi.js
export default {
  getCampaignList,      // 获取广告活动列表
  getCampaignSummary,   // 获取广告活动汇总
  getCampaignChart,     // 获取广告活动图表数据
  // 其他方法...
};

3. 后端实现

3.1 控制器

广告活动控制器

AdvertCampaignManagerController.java 负责处理广告活动相关的API请求:

@Api(tags = "广告活动接口")
@RestController 
@RequestMapping("/api/v1/advCampaignManager") 
public class AdvertCampaignManagerController {
    
    @Resource
    IAmzAdvCampaignService amzAdvCampaignService;
    
    @Resource 
    IAmzAdvCampaignsSDService amzAdvCampaignsSDService;
    
    @Resource
    IAmzAdvCampaignHsaService amzAdvCampaignHsaService;
    
    // 获取广告活动列表
    @PostMapping("/getCampaignList")
    public Result<List<Map<String, Object>>> getCampaignListAction(@RequestBody QueryForList query) {
        // 实现逻辑
    }
    
    // 创建广告活动
    @PostMapping("/createCampaign")
    public Result<Map<String, Object>> createCampaignAction(@RequestBody JSONObject param) {
        // 实现逻辑
    }
    
    // 更新广告活动
    @PostMapping("/updateCampaign")
    public Result<Map<String, Object>> updateCampaignAction(@RequestBody JSONObject param) {
        // 实现逻辑
    }
    
    // 其他方法...
}

广告组控制器

AdvertAdgroupManagerController.java 负责处理广告组相关的API请求:

@Api(tags = "广告组接口")
@RestController 
@RequestMapping("/api/v1/advAdgroupManager") 
public class AdvertAdgroupManagerController {
    
    @Resource
    IAmzAdvAdGroupService amzAdvAdGroupService;
    
    @Resource 
    IAmzAdvAdgroupsSDService amzAdvAdgroupsSDService;
    
    @Resource
    IAmzAdvAdgroupsHsaService amzAdvAdgroupsHsaService;
    
    // 获取广告组列表
    @PostMapping("/getAdgroupList")
    public Result<List<Map<String, Object>>> getAdgroupListAction(@RequestBody QueryForList query) {
        // 实现逻辑
    }
    
    // 创建广告组
    @PostMapping("/createAdgroup")
    public Result<Map<String, Object>> createAdgroupAction(@RequestBody JSONObject param) {
        // 实现逻辑
    }
    
    // 其他方法...
}

3.2 服务层

广告活动服务

AmzAdvCampaignServiceImpl.java 实现了广告活动的核心业务逻辑:

@Service
public class AmzAdvCampaignServiceImpl implements IAmzAdvCampaignService {
    
    @Autowired
    AmzAdvCampaignsMapper amzAdvCampaignsMapper;
    
    @Autowired
    IAmzAdvAuthService amzAdvAuthService;
    
    // 创建广告活动
    @Override
    public Map<String, Object> amzCreateCampaigns(AmzAdvProfile profile, List<AmzAdvCampaigns> campaigns) {
        // 实现逻辑:调用亚马逊API创建广告活动
    }
    
    // 更新广告活动
    @Override
    public Map<String, Object> amzUpdateSpCampaigns(AmzAdvProfile profile, List<AmzAdvCampaigns> campaigns) {
        // 实现逻辑:调用亚马逊API更新广告活动
    }
    
    // 获取广告活动图表数据
    @Override
    public Map<String, Object> getCampaignChart(AmzAdvProfile profile, Map<String, Object> param) {
        // 实现逻辑:生成图表数据
    }
    
    // 其他方法...
}

广告组服务

AmzAdvAdGroupServiceImpl.java 实现了广告组的核心业务逻辑:

@Service
public class AmzAdvAdGroupServiceImpl implements IAmzAdvAdGroupService {
    
    @Autowired
    AmzAdvAdgroupsMapper amzAdvAdgroupsMapper;
    
    @Autowired
    IAmzAdvAuthService amzAdvAuthService;
    
    // 创建广告组
    @Override
    public Map<String, Object> amzCreateAdGroups(AmzAdvProfile profile, List<AmzAdvAdgroups> adgroups) {
        // 实现逻辑:调用亚马逊API创建广告组
    }
    
    // 更新广告组
    @Override
    public Map<String, Object> amzUpdateAdGroups(AmzAdvProfile profile, List<AmzAdvAdgroups> adgroups) {
        // 实现逻辑:调用亚马逊API更新广告组
    }
    
    // 其他方法...
}

3.3 数据模型

广告活动模型

public class AmzAdvCampaigns {
    private String campaignId;
    private String campaignName;
    private String campaignType;
    private String targetingType;
    private BigDecimal dailyBudget;
    private String state;
    private String startDate;
    private String endDate;
    private String biddingStrategy;
    // 其他字段...
    // getter和setter方法...
}

广告组模型

public class AmzAdvAdgroups {
    private String adGroupId;
    private String campaignId;
    private String name;
    private BigDecimal defaultBid;
    private String state;
    // 其他字段...
    // getter和setter方法...
}

3.4 亚马逊API集成

后端通过封装的客户端与亚马逊广告API交互:

public class AmazonAdvertisingAPIClient {
    
    private String accessToken;
    private String endpoint;
    private String profileId;
    
    // 调用ListCampaigns接口
    public List<Campaign> listCampaigns() {
        // 实现逻辑:构建请求,发送到亚马逊API,处理响应
    }
    
    // 调用CreateCampaigns接口
    public List<CampaignResponse> createCampaigns(List<Campaign> campaigns) {
        // 实现逻辑:构建请求,发送到亚马逊API,处理响应
    }
    
    // 其他API方法...
}

4. 核心功能分析

4.1 广告活动管理

功能描述

实现逻辑

  1. 前端发送创建广告活动请求
  2. 后端接收请求,验证参数
  3. 后端调用亚马逊API创建广告活动
  4. 后端存储广告活动信息到数据库
  5. 后端返回创建结果给前端
  6. 前端更新界面,显示新广告活动

关键代码

前端创建广告活动:

function createCampaign() {
  // 验证表单
  // 构建请求参数
  advCampaignApi.createCampaign(params).then(res => {
    if(res.code == 200) {
      ElMessage.success('创建成功');
      // 刷新广告活动列表
    } else {
      ElMessage.error('创建失败:' + res.msg);
    }
  });
}

后端处理创建请求:

@Override
public Map<String, Object> amzCreateCampaigns(AmzAdvProfile profile, List<AmzAdvCampaigns> campaigns) {
  // 构建亚马逊API请求
  // 调用API
  // 处理响应
  // 存储数据
  // 返回结果
}

4.2 广告组管理

功能描述

实现逻辑

  1. 前端选择广告活动,点击"创建广告组"
  2. 前端填写广告组信息,提交表单
  3. 后端接收请求,验证参数
  4. 后端调用亚马逊API创建广告组
  5. 后端存储广告组信息到数据库
  6. 后端返回创建结果给前端
  7. 前端更新界面,显示新广告组

4.3 商品广告管理

功能描述

实现逻辑

  1. 前端选择广告组,点击"添加商品"
  2. 前端选择要推广的商品,设置出价
  3. 后端接收请求,验证参数
  4. 后端调用亚马逊API创建商品广告
  5. 后端存储商品广告信息到数据库
  6. 后端返回创建结果给前端
  7. 前端更新界面,显示新商品广告

4.4 关键词管理

功能描述

实现逻辑

  1. 前端选择广告组,点击"添加关键词"
  2. 前端输入关键词,设置匹配类型和出价
  3. 后端接收请求,验证参数
  4. 后端调用亚马逊API创建关键词
  5. 后端存储关键词信息到数据库
  6. 后端返回创建结果给前端
  7. 前端更新界面,显示新关键词

4.5 定向管理

功能描述

实现逻辑

  1. 前端选择广告组,点击"添加定向"
  2. 前端选择定向类型,设置定向条件和出价
  3. 后端接收请求,验证参数
  4. 后端调用亚马逊API创建定向
  5. 后端存储定向信息到数据库
  6. 后端返回创建结果给前端
  7. 前端更新界面,显示新定向

4.6 数据报表与分析

功能描述

实现逻辑

  1. 前端选择时间范围和数据类型
  2. 前端发送数据请求
  3. 后端接收请求,查询数据库
  4. 后端处理数据,计算指标
  5. 后端返回处理后的数据
  6. 前端使用ECharts渲染图表
  7. 前端展示数据表格

5. 技术亮点

5.1 组件化设计

5.2 批量操作优化

5.3 数据可视化

5.4 智能优化

5.5 性能优化

6. 数据安全

6.1 权限控制

6.2 数据加密

6.3 防滥用措施

7. 扩展性分析

7.1 模块扩展

7.2 技术扩展

7.3 功能扩展

8. 代码优化建议

8.1 前端优化

  1. 组件拆分:将大型组件进一步拆分为更小的、可复用的组件
  2. 状态管理优化:使用Pinia替代Vuex,简化状态管理
  3. API请求优化:使用请求缓存和防抖节流,减少API调用
  4. 代码分割:使用动态导入实现代码分割,减少初始加载时间
  5. 类型定义:使用TypeScript,提高代码类型安全性

8.2 后端优化

  1. 缓存策略:优化缓存策略,减少数据库查询和API调用
  2. 异步处理:使用消息队列处理耗时操作,提高系统响应速度
  3. 数据库优化:优化数据库查询,添加适当的索引
  4. API设计:优化API设计,减少冗余接口
  5. 错误处理:完善错误处理机制,提高系统可靠性

8.3 性能优化

  1. 前端性能:优化前端渲染性能,减少重绘和回流
  2. 后端性能:优化后端代码,提高处理速度
  3. 数据库性能:优化数据库结构和查询,提高数据访问速度
  4. 网络性能:优化网络传输,减少数据传输量
  5. 系统架构:优化系统架构,提高整体性能

9. 总结

广告管理模块是Wimoor系统中一个功能强大、设计合理的核心模块,它通过集成亚马逊广告API,为用户提供了完整的广告管理功能。该模块采用前后端分离架构,使用Vue 3和Spring Boot等现代技术栈,实现了广告活动、广告组、商品广告、关键词、定向等的全生命周期管理。

9.1 核心价值

9.2 技术创新

9.3 未来发展

广告管理模块的设计和实现体现了现代软件架构的最佳实践,为用户提供了专业、高效、智能的广告管理解决方案。通过不断的技术创新和功能扩展,该模块将继续为用户创造更大的价值。

广告-广告统计

广告统计模块功能解析文档

1. 系统架构

1.1 技术栈

分类 技术 版本 说明
前端框架 Vue.js 3.x 采用Composition API开发模式
UI组件库 Element Plus 最新版 提供丰富的UI组件支持
数据可视化 ECharts 5.x 用于绘制各种数据图表
HTTP客户端 Axios 0.27.2 用于前端与后端API通信
状态管理 Vuex 4.x 用于前端状态管理
后端框架 Spring Boot 2.5.x 提供RESTful API服务
持久层框架 MyBatis Plus 3.5.x 简化数据库操作
数据库 MySQL 5.7+ 存储广告数据和统计信息
API集成 Amazon Advertising API 最新版 获取亚马逊广告数据

1.2 架构设计

广告统计模块采用前后端分离的架构设计,具体架构层次如下:

  1. 前端层

    • 视图层:Vue组件,负责数据展示和用户交互
    • 业务逻辑层:Vue组合式API,处理前端业务逻辑
    • API调用层:封装的API请求函数,与后端通信
  2. 后端层

    • 控制层:Spring MVC控制器,处理HTTP请求
    • 服务层:业务逻辑服务,处理核心业务逻辑
    • 数据访问层:MyBatis Plus Mapper,与数据库交互
    • 外部API层:与Amazon Advertising API交互,获取广告数据
  3. 数据层

    • 数据库:存储广告数据和统计信息
    • 缓存:可选,提高数据查询性能

1.3 核心流程图

sequenceDiagram
    participant User as 用户
    participant Frontend as 前端组件
    participant API as 前端API调用
    participant Backend as 后端控制器
    participant Service as 后端服务
    participant Mapper as 数据访问
    participant DB as 数据库
    participant AmazonAPI as 亚马逊广告API

    User->>Frontend: 访问广告统计模块
    Frontend->>API: 请求运行中活动数据
    API->>Backend: GET /api/v1/advSummary
    Backend->>Service: 调用getenablesumtype()
    Service->>Mapper: 查询广告活动数据
    Mapper->>DB: SELECT * FROM ad_campaigns WHERE status='enabled'
    DB-->>Mapper: 返回活动数据
    Mapper-->>Service: 处理数据
    Service-->>Backend: 返回汇总数据
    Backend-->>API: 200 OK { campaigns: 10, adGroups: 20, ... }
    API-->>Frontend: 更新运行中活动卡片
    
    Frontend->>API: 请求异常预警数据
    API->>Backend: GET /api/v1/advSummary/warning
    Backend->>Service: 调用getProductWarningIndicator()
    Service->>Mapper: 查询异常数据
    Mapper->>DB: SELECT * FROM ad_warning WHERE type='productads'
    DB-->>Mapper: 返回异常数据
    Mapper-->>Service: 处理数据
    Service-->>Backend: 返回预警数据
    Backend-->>API: 200 OK { impco: 5, clickco: 3, ... }
    API-->>Frontend: 更新异常预警卡片
    
    Frontend->>API: 请求广告数据分析
    API->>Backend: POST /api/v1/advReport/getsumproduct
    Backend->>Service: 调用getSumProduct()
    Service->>Mapper: 查询广告数据
    Mapper->>DB: SELECT * FROM ad_summary WHERE date BETWEEN ? AND ?
    DB-->>Mapper: 返回广告数据
    Mapper-->>Service: 处理数据
    Service-->>Backend: 返回分析数据
    Backend-->>API: 200 OK { summary: {...}, chartdata: {...} }
    API-->>Frontend: 更新数据分析面板和图表

2. 前端实现

2.1 组件结构

组件名称 文件路径 主要功能 核心方法
主组件 wimoor-ui/src/views/amazon/advertisement/overview/index.vue 广告统计模块主界面 loadWaringData(), loadWaringDataDetail()
广告统计组件 wimoor-ui/src/views/amazon/advertisement/overview/components/adStatistics.vue 广告数据分析和报表 loadSummaryChartData(), loadMonthSummaryData(), refreshChart()
漏斗分析组件 wimoor-ui/src/views/amazon/advertisement/overview/components/adFunnel.vue 广告转化漏斗分析 -
ROAS排名组件 wimoor-ui/src/views/amazon/advertisement/overview/components/roasRank.vue 广告投入产出比排名 -
指标详情组件 wimoor-ui/src/views/amazon/advertisement/overview/components/indicator_detail.vue 异常指标详情 -
指标设置组件 wimoor-ui/src/views/amazon/advertisement/overview/components/indicator.vue 预警指标设置 -

2.2 核心功能实现

2.2.1 运行中活动统计

实现原理

关键代码

// 加载运行中活动数据
onMounted(()=>{
    summaryApi.getenablesumtype().then(res=>{
        state.typedata=res.data;
    });
    loadWaringData();
})

2.2.2 异常数据预警

实现原理

关键代码

// 加载预警数据
function loadWaringData(){
    var param={ftype:state.waringType};
    if(state.wardatatype=="productads"){
        summaryApi.getProductWarningIndicator(param).then(res=>{
            state.waringData=res.data;
        });
    }else{
        summaryApi.getKeywordsWarningIndicator(param).then(res=>{
            state.waringData=res.data;
        });
    }
}

// 查看预警详情
function loadWaringDataDetail(ftype){
    indicatorDetailRef.value.show(ftype,state.waringType,state.wardatatype);
}

2.2.3 广告数据分析

实现原理

关键代码

// 加载图表数据
function loadSummaryChartData(){
    summaryApi.getsumproduct(state.queryParams).then(res=>{
        state.summaryData=res.data.summary;
        state.chartData=res.data.chartdata;
        var data=res.data.summary;
        data.ordersummary=res.data.ordersummary;
        state.summaryData.acosas = isNanPvalue(parseFloat(data.cost), data.ordersummary);
        state.summaryData.cpc = isNanvalue(parseFloat(data.cost),parseFloat( data.clicks));
        state.summaryData.roi = isNanvalue(parseFloat(data.attributedSales), parseFloat(data.cost));
        if(state.queryParams.currency=="USD"){
            state.adList.forEach(item=>{
                if(item.prefix=='¥'){
                    item.prefix='$';
                }
            })
        }else{
            state.adList.forEach(item=>{
                if(item.prefix=='$'){
                    item.prefix='¥';
                }
            })
        }
        refreshChart();
    })
}

// 加载月度报表数据
function loadMonthSummaryData() {
    summaryApi.getmonthsum(state.queryParams).then(res=>{ 
        state.monthData=res.data;
        if(state.queryParams.currency=="USD"){
            state.rows.forEach(item=>{
                if(item.prefix=='¥'){
                    item.prefix='$';
                }
            })
        }else{
            state.rows.forEach(item=>{
                if(item.prefix=='$'){
                    item.prefix='¥';
                }
            })
        }
    });
}

// 刷新图表
function refreshChart() {
    var chartdata=state.chartData;
    if (chartdata != null) {
        var labels = null;
        var color = [];
        var legends = [];
        var series = [];
        state.adList.forEach(row=>{
            var type = row.field;
            if (row.active&&chartdata[type]) {
                labels = chartdata[type]["listLabel"];
                legends.push(row.name);
                color.push(row.color);
                var datas = {};
                datas.name = row.name;
                if (type == "cr" || type == "ctr" || type == "acos") {
                    datas.type = "line";
                    datas.yAxisIndex = 1;
                    datas.symbol = 'emptycircle';
                    datas.smooth = true;
                    datas.symbolSize = 3;
                    datas.itemStyle = {
                        normal : {
                            lineStyle : {
                                width : 2
                            }
                        }
                    }
                } else {
                    datas.type = "bar";
                    datas.barGap = "0%";
                    datas.boundaryGap = "0%";
                    datas.barMaxWidth = 32, datas.itemStyle = {
                        normal : {
                            barBorderRadius : [ 4, 4, 0, 0 ]
                        }
                    }
                }
                datas.data = chartdata[type].listData;
                series.push(datas);
            }
        });
        if (labels != null) {
            lineChart(legends, labels, series, color);
        } else {
            document.getElementById("mychart").innerHTML="<div style='padding-top:10%' clas='font-extraSmall'>暂无数据</div>";
        }
    } else {
        document.getElementById("mychart").innerHTML="<div style='padding-top:10%' clas='font-extraSmall'>暂无数据</div>";
    }
}

2.2.4 漏斗分析

实现原理

2.2.5 ROAS排名

实现原理

2.3 API调用

API名称 方法 URL 功能描述 参数 返回值
getenablesumtype GET /api/v1/advSummary 获取运行中活动数据 { campaigns: 10, adGroups: 20, ads: 30, targets: 40 }
getProductWarningIndicator GET /api/v1/advSummary/warning 获取商品广告异常预警数据 ftype: "co"/"sequent"/"yesterday" { impco: 5, clickco: 3, crco: 2, acosco: 4 }
getKeywordsWarningIndicator GET /api/v1/advSummary/warning 获取关键词异常预警数据 ftype: "co"/"sequent"/"yesterday" { impco: 2, clickco: 1, crco: 0, acosco: 3 }
getsumproduct POST /api/v1/advReport/getsumproduct 获取广告数据分析数据 begin: "2023-01-01", end: "2023-01-31", groupid: "1", profileid: "2", currency: "USD" { summary: {...}, chartdata: {...}, ordersummary: 1000 }
getmonthsum POST /api/v1/advReport/getmonthsum 获取月度广告报表数据 begin: "2023-01", end: "2023-03", groupid: "1", profileid: "2", currency: "USD" { impressions: {...}, clicks: {...}, ... }

3. 后端实现

3.1 控制器

控制器名称 文件路径 主要功能 核心方法
AdvertReportController wimoor-amazon-adv/amazon-adv-boot/src/main/java/com/wimoor/amazon/adv/controller/AdvertReportController.java 广告报表控制 getSumProductAction(), getMonthSumAction()
AdvertManagerController wimoor-amazon-adv/amazon-adv-boot/src/main/java/com/wimoor/amazon/adv/controller/AdvertManagerController.java 广告管理控制 -

3.2 服务层

服务名称 文件路径 主要功能 核心方法
AmzAdvSumServiceImpl wimoor-amazon-adv/amazon-adv-boot/src/main/java/com/wimoor/amazon/adv/service/impl/AmzAdvSumServiceImpl.java 广告数据汇总服务 -
AmzAdvSumProductAdsService wimoor-amazon-adv/amazon-adv-boot/src/main/java/com/wimoor/amazon/adv/report/service/IAmzAdvSumProductAdsService.java 商品广告数据汇总服务 getSumProduct(), getMonthSumProduct(), getDaysSumProduct()
AmazonReportAdvSummaryService wimoor-amazon-adv/amazon-adv-boot/src/main/java/com/wimoor/amazon/adv/common/service/IAmazonReportAdvSummaryService.java 广告报表汇总服务 findAdvert()

3.3 数据模型

模型名称 文件路径 主要功能 核心字段
AmzAdvSumProductAds 数据汇总模型 商品广告数据汇总 campaignId, adGroupId, adId, impressions, clicks, cost, attributedSales, attributedUnitsOrdered
AmzAdvWarning 异常预警模型 广告异常预警数据 type, subtype, indicator, value, threshold, status
AmazonReportAdvSummary 报表汇总模型 广告报表汇总数据 date, profileId, campaignId, adGroupId, adId, impressions, clicks, cost, sales

3.4 数据访问

Mapper名称 文件路径 主要功能 核心方法
AmzAdvSumProductAdsMapper 数据访问映射 商品广告数据汇总CRUD selectSumProduct(), selectMonthSumProduct(), selectDaysSumProduct()
AmzAdvWarningMapper 数据访问映射 广告异常预警数据CRUD selectWarningIndicator()
AmazonReportAdvSummaryMapper 数据访问映射 广告报表汇总数据CRUD selectAdvert()

3.5 核心API实现

3.5.1 获取广告数据分析数据

实现原理

关键代码

@PostMapping("/getsumproduct")
public Result<Map<String, Object>> getSumProductAction(@RequestBody QueryForSumProductDTO dto){
    String begin =dto.getBegin();
    String end = dto.getEnd();
    String type = dto.getType();
    String groupid = dto.getGroupid();
    String profileid = dto.getProfileid();
    String currency = dto.getCurrency();
    UserInfo user = UserInfoContext.get();
    Map<String,Object> param=new HashMap<String,Object>();
    param.put("shopid", user.getCompanyid());
    param.put("type", type);
    param.put("currency",  currency);
    Map<String, Marketplace> allmarket = marketplaceService.findMapByMarketplaceId();
    if(StringUtil.isNotEmpty(profileid) && !"all".equals(profileid)) {
        if(StringUtil.isNotEmpty(groupid) && !"all".equals(groupid)) {
            AmzAdvProfile profile = amzAdvAuthService.getAmzAdvProfileByKey(new BigInteger(profileid));
            if(allmarket.get(profile.getMarketplaceid())!=null){
                param.put("pmarketplaceId", allmarket.get(profile.getMarketplaceid()).getPointName());
            }
            param.put("sellerid", profile.getSellerid());
            param.put("profileid", profileid);
            param.put("groupid", groupid);
        }else {
            param.put("marketplaceId", profileid);
            param.put("pmarketplaceId", allmarket.get(profileid).getPointName());
        }
    }else {
        if(StringUtil.isNotEmpty(groupid) && !"all".equals(groupid)) {
            List<Map<String, Object>> list = amzAdvAuthService.getSelleridBygroup(groupid);
            List<String> sellerList = new ArrayList<String>();
            for(Map<String,Object> map : list) {
                String seller = (String) map.get("sellerId");
                sellerList.add(seller);
            }
            param.put("sellerList", sellerList);
            param.put("groupid", groupid);
        } 
    }
    if(StringUtil.isNotEmpty(begin)) {
        param.put("begin", begin.replaceAll("/", "-").trim());
        param.put("beginDate", begin.replaceAll("/", "-").trim());
    }
    if(StringUtil.isNotEmpty(end)) {
        param.put("end",end.replaceAll("/", "-").trim());
        param.put("endDate", end.replaceAll("/", "-").trim());
    }
    Map<String, Object> result =new HashMap<String,Object>();
    Map<String, Object> map = amzAdvSumProductAdsService.getSumProduct(param);
    BigDecimal mapordersum = amzAdvSumProductAdsService.orderSummaryAll(param);
    Map<String, Object> chartdata = amzAdvSumProductAdsService.getDaysSumProduct(param);
    result.put("summary", map);
    result.put("ordersummary", mapordersum);
    result.put("chartdata", chartdata);
    return Result.success(result) ;
}

3.5.2 获取月度广告报表数据

实现原理

关键代码

@PostMapping("/getmonthsum")
public Result<Map<String, Object>> getMonthSumAction(@RequestBody QueryForSumProductDTO dto){
    String begin =dto.getBegin();
    String end = dto.getEnd();
    String groupid = dto.getGroupid();
    String profileid = dto.getProfileid();
    String currency = dto.getCurrency();
    UserInfo user = UserInfoContext.get();
    Map<String,Object> param=new HashMap<String,Object>();
    param.put("shopid", user.getCompanyid());
    param.put("groupid", groupid);
    param.put("profileid", profileid);
    param.put("currency",  currency);
    if(StringUtil.isNotEmpty(groupid) && !"all".equals(groupid)) {
        if(StringUtil.isNotEmpty(profileid) && !"all".equals(profileid)) {
            AmzAdvProfile profile = amzAdvAuthService.getAmzAdvProfileByKey(new BigInteger(profileid));
            Map<String, Marketplace> allmarket = marketplaceService.findMapByMarketplaceId();
            param.put("pmarketplaceId",allmarket.get(profile.getMarketplaceid()).getPointName());
            param.put("mmarketplaceId",profile.getMarketplaceid());
            param.put("sellerid", profile.getSellerid());
        }else {
            List<String> sellerList = new ArrayList<String>();
            List<Map<String, Object>> list = amzAdvAuthService.getSelleridBygroup(groupid);
            for(Map<String,Object> map : list) {
                String seller = (String) map.get("sellerId");
                sellerList.add(seller);
            }
            param.put("sellerList", sellerList);
            param.put("profileid", null);
        }
    }else {
        param.put("groupid", null);
        param.put("profileid", null);
        if(StringUtil.isNotEmpty(profileid) && !"all".equals(profileid)) {
            Map<String, Marketplace> allmarket = marketplaceService.findMapByMarketplaceId();
            param.put("pmarketplaceId", allmarket.get(profileid).getPointName());
            param.put("marketplaceId", profileid);
            param.put("mmarketplaceId",profileid);
        }
    }
    if(StringUtil.isNotEmpty(begin)) {
        param.put("begin", begin.replaceAll("/", "-").trim()+"-01");
        param.put("beginDate", begin.replaceAll("/", "-").trim()+"-01");
    }
    if(StringUtil.isNotEmpty(end)) {
        String[] endarray=end.split("-");
        if(endarray==null||endarray.length<2) {
            endarray=end.split("/");
        }
        if(endarray.length>=2) {
           String endate = GeneralUtil.getLastDayOfMonth(Integer.parseInt(endarray[0].trim()), Integer.parseInt(endarray[1].trim()));
            param.put("end",endate);
            param.put("endDate",endate);
        }
    }
    Map<String, Object> result = amzAdvSumProductAdsService.getMonthSumProduct(param);
    return Result.success(result) ;
}

4. 核心功能分析

4.1 数据统计功能

功能说明

技术实现

业务价值

4.2 异常预警功能

功能说明

技术实现

业务价值

4.3 数据导出功能

功能说明

技术实现

业务价值

4.4 漏斗分析功能

功能说明

技术实现

业务价值

4.5 ROAS排名功能

功能说明

技术实现

业务价值

5. 技术亮点

5.1 数据可视化

技术实现

优势

5.2 智能预警

技术实现

优势

5.3 高效数据处理

技术实现

优势

5.4 灵活数据筛选

技术实现

优势

5.5 全面的指标体系

技术实现

优势

6. 数据安全

6.1 权限控制

实现方案

安全措施

6.2 数据保护

实现方案

安全措施

6.3 合规性

实现方案

合规措施

7. 扩展性分析

7.1 功能扩展

潜在扩展点

扩展方案

7.2 技术扩展

潜在扩展点

扩展方案

7.3 集成扩展

潜在扩展点

扩展方案

8. 代码优化建议

8.1 前端优化

优化建议

  1. 组件拆分:将大型组件拆分为更小的、可复用的组件,提高代码可维护性
  2. 状态管理优化:合理使用Vuex管理全局状态,减少组件间的props传递
  3. 性能优化
    • 使用虚拟滚动处理大量数据列表
    • 优化图表渲染,减少不必要的重绘
    • 使用防抖和节流优化频繁触发的事件处理函数
  4. 代码规范
    • 统一代码风格,使用ESLint进行代码检查
    • 添加必要的注释,提高代码可读性
    • 遵循Vue最佳实践,如使用computed属性缓存计算结果

具体实现

// 优化前:频繁触发的事件处理函数
function handleQuery() {
    if(state.activeName=="chart"){
        loadSummaryChartData();
    }else{
        loadMonthSummaryData() ;
    }
}

// 优化后:使用防抖函数
import { debounce } from 'lodash-es';

const handleQuery = debounce(() => {
    if(state.activeName=="chart"){
        loadSummaryChartData();
    }else{
        loadMonthSummaryData() ;
    }
}, 300);

8.2 后端优化

优化建议

  1. 数据库优化
    • 为频繁查询的字段添加索引
    • 优化SQL查询,减少不必要的字段查询
    • 使用分页查询处理大量数据
  2. 缓存优化
    • 对频繁查询的数据使用Redis缓存
    • 合理设置缓存过期时间
    • 实现缓存预热机制
  3. 代码优化
    • 减少重复代码,提取公共方法
    • 使用Lambda表达式和Stream API简化代码
    • 添加必要的注释和文档
  4. 性能监控
    • 添加性能监控点,监控关键操作的执行时间
    • 定期分析性能瓶颈,进行针对性优化

具体实现

// 优化前:重复的参数处理代码
if(StringUtil.isNotEmpty(begin)) {
    param.put("begin", begin.replaceAll("/", "-").trim());
    param.put("beginDate", begin.replaceAll("/", "-").trim());
}
if(StringUtil.isNotEmpty(end)) {
    param.put("end",end.replaceAll("/", "-").trim());
    param.put("endDate", end.replaceAll("/", "-").trim());
}

// 优化后:提取公共方法
private void processDateParams(Map<String, Object> param, String begin, String end) {
    if(StringUtil.isNotEmpty(begin)) {
        String formattedBegin = begin.replaceAll("/", "-").trim();
        param.put("begin", formattedBegin);
        param.put("beginDate", formattedBegin);
    }
    if(StringUtil.isNotEmpty(end)) {
        String formattedEnd = end.replaceAll("/", "-").trim();
        param.put("end", formattedEnd);
        param.put("endDate", formattedEnd);
    }
}

8.3 数据处理优化

优化建议

  1. 批量处理:对大量数据的操作使用批量处理,减少数据库交互次数
  2. 异步处理:对耗时的数据计算任务采用异步处理,提高系统响应速度
  3. 数据压缩:对传输的数据进行压缩,减少网络传输量
  4. 预计算:对常用统计数据进行预计算,减少实时计算压力

具体实现

// 优化前:实时计算统计数据
@PostMapping("/getsumproduct")
public Result<Map<String, Object>> getSumProductAction(@RequestBody QueryForSumProductDTO dto) {
    // 实时计算统计数据
    Map<String, Object> map = amzAdvSumProductAdsService.getSumProduct(param);
    BigDecimal mapordersum = amzAdvSumProductAdsService.orderSummaryAll(param);
    Map<String, Object> chartdata = amzAdvSumProductAdsService.getDaysSumProduct(param);
    // ...
}

// 优化后:使用预计算数据
@PostMapping("/getsumproduct")
public Result<Map<String, Object>> getSumProductAction(@RequestBody QueryForSumProductDTO dto) {
    // 优先使用预计算数据
    Map<String, Object> precomputedData = amzAdvSumProductAdsService.getPrecomputedSumProduct(param);
    if (precomputedData != null) {
        return Result.success(precomputedData);
    }
    // 预计算数据不存在时,实时计算
    Map<String, Object> map = amzAdvSumProductAdsService.getSumProduct(param);
    BigDecimal mapordersum = amzAdvSumProductAdsService.orderSummaryAll(param);
    Map<String, Object> chartdata = amzAdvSumProductAdsService.getDaysSumProduct(param);
    // 缓存计算结果
    amzAdvSumProductAdsService.cacheSumProductResult(param, map, mapordersum, chartdata);
    // ...
}

9. 总结

9.1 核心价值

广告统计模块是Wimoor系统中功能强大、数据全面的广告分析工具,具有以下核心价值:

  1. 数据驱动决策:基于实时、全面的数据进行投放决策,提高决策的科学性和准确性
  2. 问题快速定位:通过异常预警快速定位投放问题,减少人工监控的工作量
  3. 效果可视化:通过图表和报表直观展示投放效果,提高数据可读性和分析效率
  4. 策略优化指导:基于数据分析结果指导投放策略优化,提高广告投放的整体效果
  5. 团队协作支持:方便团队成员共享数据和分析结果,促进团队协作

9.2 技术创新

广告统计模块在技术实现上具有以下创新点:

  1. 多维度数据统计:支持按时间、广告组、配置文件等多维度进行数据统计和分析
  2. 智能异常预警:采用多种预警维度和指标,实现广告异常的智能监测和预警
  3. 丰富的数据可视化:使用ECharts绘制多种类型的图表,直观展示数据趋势和变化
  4. 高效数据处理:通过缓存、异步处理等技术,提高数据处理效率和系统响应速度
  5. 灵活的扩展性:采用模块化、配置化的设计,便于功能扩展和技术升级

9.3 未来发展

广告统计模块具有广阔的发展前景,未来可以从以下几个方面进行发展:

  1. 功能增强:增加更多广告类型的支持,增加更多预警指标和分析维度
  2. 技术升级:引入机器学习、人工智能等先进技术,实现更智能的广告分析和优化
  3. 集成扩展:与更多第三方广告平台、电商平台集成,实现数据的统一管理和分析
  4. 移动化:开发移动应用,方便用户随时随地查看广告数据和接收异常预警
  5. 生态建设:构建广告数据分析生态,提供更多增值服务

9.4 结论

广告统计模块是Wimoor系统中不可或缺的核心功能模块,通过该模块,用户可以实时监控广告投放状态、分析投放效果、识别异常问题并优化投放策略。该模块采用先进的技术架构和实现方案,具有功能强大、数据全面、性能高效、扩展性强等特点,为用户提供了一站式的广告分析解决方案。

随着技术的不断发展和业务需求的不断变化,广告统计模块也将不断升级和完善,为用户提供更加智能、全面、高效的广告分析服务,帮助用户在激烈的市场竞争中获得更大的优势。

设置-1688绑定

1688绑定模块功能解析文档

1. 模块架构概述

1688绑定模块采用前后端分离架构,前端使用Vue 3 Composition API实现用户界面和交互逻辑,后端使用Spring Boot实现API接口和业务逻辑。模块通过OAuth2协议与1688开放平台进行交互,实现账号授权和数据获取。

1.1 系统架构图

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ 前端页面    │────>│ 后端API     │────>│ 聚石塔服务  │────>│ 1688开放平台│
│ (Vue 3)     │<────│ (Spring Boot)│<────│            │<────│             │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘

1.2 核心组件

2. 前端代码结构分析

2.1 主组件结构

前端主组件 index.vue 包含以下核心部分:

2.2 API调用服务

purchasealibabaApi.js 定义了与后端交互的API方法:

3. 后端代码结构分析

3.1 控制器层

AlibabaController.java 提供以下API端点:

3.2 服务实现层

PurchaseAlibabaAuthServiceImpl.java 实现了以下核心功能:

3.3 数据模型

PurchaseAlibabaAuth 实体包含以下核心字段:

4. 核心功能实现

4.1 授权流程实现

  1. 前端触发授权

    • 用户点击"绑定账号"按钮
    • 填写账号名称和开发者信息(可选)
    • 点击"授权"按钮,调用 bindAuth() 方法
    • bindAuth() 调用 getcode() 获取账号ID
    • getcode() 调用后端 /submitname 接口创建账号记录
    • 成功后调用 goUrl() 获取1688授权链接
  2. 1688平台授权

    • 前端打开1688授权页面
    • 用户登录1688账号并确认授权
    • 1688平台重定向回系统,并携带授权码
  3. 后端处理授权回调

    • 前端 GetRequest() 方法解析URL参数,获取授权码
    • 调用后端 /bindAuthData 接口,传入授权码和状态
    • 后端 bindAuth() 方法使用授权码获取访问令牌和刷新令牌
    • 保存令牌信息到数据库
  4. 前端更新状态

    • 授权成功后,前端显示成功提示
    • 刷新账号列表,显示新绑定的账号

4.2 令牌管理实现

  1. 令牌获取

    • bindAuth() 方法中,使用授权码调用1688开放平台的 /system.oauth2/getToken 接口
    • 获取 access_tokenrefresh_tokenexpires_in 等信息
    • 计算令牌过期时间并保存到数据库
  2. 令牌刷新

    • refreshAuthToken() 方法中,使用 refresh_token 调用1688开放平台的 /system.oauth2/getToken 接口
    • 获取新的 access_token 和过期时间
    • 更新数据库中的令牌信息
  3. 令牌检查

    • checkAuthorityToken() 方法中,检查访问令牌是否过期
    • 若过期,自动调用 refreshAuthToken() 刷新令牌

4.3 账号管理实现

  1. 账号列表

    • 前端调用 getauthList() 获取已绑定账号列表
    • 后端 getAuthData() 方法查询数据库,返回未删除的账号
    • 前端表格展示账号信息,包括名称、状态、到期时间等
  2. 账号编辑

    • 用户点击编辑图标,打开编辑弹窗
    • 修改账号名称或开发者信息
    • 点击"保存"按钮,调用后端 /submitname 接口更新账号信息
  3. 账号删除

    • 用户点击"删除"按钮
    • 前端调用 removeBind() 方法
    • 后端 updateAlibaba() 方法将账号标记为删除状态
  4. 延期授权

    • 用户点击"延期授权"按钮
    • 前端调用 goUrl() 方法重新获取授权链接
    • 重复授权流程,获取新的令牌和过期时间

5. API调用流程

5.1 授权链接获取流程

sequenceDiagram
    participant 前端
    participant 后端
    participant 1688平台
    
    前端->>后端: GET /get1688Url?redirectUrl=xxx&id=xxx
    后端->>后端: 构建授权URL
    后端-->>前端: 返回授权URL
    前端->>1688平台: 打开授权页面
    1688平台->>前端: 用户授权后重定向,携带code
    前端->>后端: GET /bindAuthData?code=xxx&state=xxx
    后端->>1688平台: 调用getToken接口获取令牌
    1688平台-->>后端: 返回令牌信息
    后端->>后端: 保存令牌信息到数据库
    后端-->>前端: 返回绑定成功信息

5.2 令牌刷新流程

sequenceDiagram
    participant 前端
    participant 后端
    participant 1688平台
    
    前端->>后端: GET /refreshAuthData?id=xxx
    后端->>后端: 检查refresh_token是否过期
    后端->>1688平台: 调用getToken接口,使用refresh_token
    1688平台-->>后端: 返回新的access_token
    后端->>后端: 更新令牌信息和过期时间
    后端-->>前端: 返回刷新成功信息

5.3 API调用流程

sequenceDiagram
    participant 前端
    participant 后端
    participant 聚石塔
    participant 1688平台
    
    前端->>后端: 调用业务API(如获取物流信息)
    后端->>后端: 检查令牌是否有效
    后端->>聚石塔: 转发API请求
    聚石塔->>1688平台: 调用1688开放平台API
    1688平台-->>聚石塔: 返回API响应
    聚石塔-->>后端: 转发响应数据
    后端-->>前端: 返回处理结果

6. 技术要点和难点

6.1 OAuth2授权实现

6.2 令牌生命周期管理

6.3 聚石塔服务集成

6.4 多账号管理

7. 代码优化建议

7.1 前端代码优化

  1. 错误处理优化

    • 当前代码在API调用失败时缺少统一的错误处理机制
    • 建议实现全局错误处理拦截器,统一处理API错误
  2. 状态管理优化

    • 当前使用本地响应式数据管理状态,对于复杂场景可能不够灵活
    • 建议使用Pinia或Vuex进行状态管理,提高代码可维护性
  3. 代码结构优化

    • 将授权流程相关的逻辑抽取为独立的composable函数
    • 提高代码的复用性和可读性

7.2 后端代码优化

  1. 安全性优化

    • 当前代码中AppKey和AppSecret直接存储在数据库中
    • 建议对敏感信息进行加密存储,提高安全性
  2. 异常处理优化

    • 当前代码中异常处理较为简单,缺少详细的错误信息
    • 建议实现统一的异常处理机制,提供更详细的错误信息
  3. 性能优化

    • 当前代码中存在重复的数据库查询操作
    • 建议使用缓存机制减少数据库查询,提高系统性能
  4. 代码结构优化

    • 将1688 API调用相关的代码抽取为独立的服务
    • 提高代码的模块化程度和可维护性

7.3 架构优化

  1. 微服务架构

    • 考虑将1688相关的功能抽取为独立的微服务
    • 提高系统的扩展性和可维护性
  2. 异步处理

    • 对于耗时的API调用,考虑使用异步处理方式
    • 提高系统的响应速度和并发处理能力
  3. 监控和告警

    • 实现对1688授权状态和API调用的监控
    • 当授权过期或API调用失败时及时告警

8. 总结

1688绑定模块是Wimoor系统中实现与1688平台对接的重要功能模块,通过OAuth2协议实现了账号授权和数据获取。模块采用前后端分离架构,前端使用Vue 3实现用户界面,后端使用Spring Boot实现业务逻辑,通过聚石塔服务与1688开放平台进行通信。

模块的核心功能包括账号绑定、授权管理、令牌管理、账号列表展示和账号管理等。通过这些功能,用户可以方便地绑定和管理1688账号,实现与1688平台的无缝对接,为后续的采购操作和物流信息查询提供了基础。

在技术实现上,模块解决了OAuth2授权流程、令牌生命周期管理、多账号管理等技术难点,为系统的稳定运行提供了保障。同时,通过代码优化建议的实施,可以进一步提高模块的性能、安全性和可维护性。

设置-店铺管理

店铺管理模块功能解析文档

1. 模块架构概述

店铺管理模块采用前后端分离架构,前端使用Vue 3 Composition API实现用户界面和交互逻辑,后端使用Spring Boot实现API接口和业务逻辑。模块主要负责管理亚马逊店铺的基本信息、财务设置和海关信息。

1.1 系统架构图

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ 前端页面    │────>│ 后端API     │────>│ 数据库      │
│ (Vue 3)     │<────│ (Spring Boot)│<────│ (MySQL)     │
└─────────────┘     └─────────────┘     └─────────────┘

1.2 核心组件

2. 前端代码结构分析

2.1 主组件结构

前端主组件 index.vue 包含以下核心部分:

2.2 API调用服务

groupApi.js 定义了与后端交互的API方法:

3. 后端代码结构分析

3.1 控制器层

AmazonGroupController.java 提供以下API端点:

3.2 服务实现层

AmazonGroupServiceImpl.java 实现了以下核心功能:

3.3 数据模型

AmazonGroup 实体包含以下核心字段:

4. 核心功能实现

4.1 店铺管理实现

  1. 添加店铺

    • 前端调用 addStorename() 方法打开添加弹窗
    • 用户填写店铺信息后点击保存
    • 前端调用 saveStore() 方法,通过 groupApi.AmazonGroupSave() 向后端发送请求
    • 后端 saveAmazonGroupAction() 方法处理请求,保存店铺信息到数据库
  2. 编辑店铺

    • 前端点击店铺右侧的编辑图标,调用 updataStorename() 方法
    • 前端通过 groupApi.getAmazonGroupById() 获取店铺详情
    • 用户修改信息后点击保存
    • 前端调用 saveStore() 方法保存修改
    • 后端 saveAmazonGroupAction() 方法处理请求,更新店铺信息
  3. 删除店铺

    • 前端点击店铺右侧的删除图标,调用 delectStore() 方法
    • 前端显示确认对话框,用户确认删除
    • 前端调用 groupApi.deleteAmazongroup() 向后端发送请求
    • 后端 delAmazonGroupByIdAction() 方法处理请求,检查店铺是否有关联的授权信息
    • 如果没有关联授权,将店铺标记为删除状态

4.2 店铺排序实现

  1. 打开排序弹窗

    • 前端点击"排序"按钮,打开排序弹窗
    • 前端显示店铺列表,支持拖拽排序
  2. 拖拽排序

    • 用户通过拖拽方式调整店铺顺序
    • 前端 dragEnd() 方法更新店铺的排序号
  3. 保存排序

    • 用户点击"确认"按钮保存排序
    • 前端调用 submitFormIndex() 方法,通过 groupApi.updateBatch() 向后端发送请求
    • 后端 updateAmazonGroupConfigAction() 方法处理请求,批量更新店铺排序

4.3 财务设置实现

  1. 设置财务账套

    • 前端编辑店铺信息,开启"是否财务账套"开关
    • 前端显示公司名称和税号输入框
    • 用户填写相关信息
  2. 初始化财务账套

    • 前端开启"初始化财务账套"开关
    • 前端调用 saveStore() 方法保存设置
    • 前端先调用 initFinAccountingSubjects() 初始化财务科目
    • 然后调用 groupApi.AmazonGroupSave() 保存店铺信息

4.4 权限控制实现

5. API调用流程

5.1 获取店铺列表流程

sequenceDiagram
    participant 前端
    participant 后端
    participant 数据库
    
    前端->>后端: GET /api/v1/amzgroup/list
    后端->>后端: 获取当前用户信息
    后端->>数据库: 查询用户所属公司的店铺列表
    数据库-->>后端: 返回店铺数据
    后端-->>前端: 返回店铺列表

5.2 保存店铺信息流程

sequenceDiagram
    participant 前端
    participant 后端
    participant 数据库
    
    前端->>后端: PUT /api/v1/amzgroup/save
    后端->>后端: 获取当前用户信息
    后端->>数据库: 检查店铺名称是否已存在
    数据库-->>后端: 返回检查结果
    后端->>数据库: 保存或更新店铺信息
    数据库-->>后端: 返回保存结果
    后端-->>前端: 返回操作结果

5.3 删除店铺流程

sequenceDiagram
    participant 前端
    participant 后端
    participant 数据库
    
    前端->>后端: DELETE /api/v1/amzgroup/delete/{id}
    后端->>后端: 获取当前用户信息
    后端->>数据库: 检查店铺是否有关联的授权信息
    数据库-->>后端: 返回检查结果
    后端->>数据库: 将店铺标记为删除状态
    数据库-->>后端: 返回更新结果
    后端-->>前端: 返回操作结果

5.4 批量更新店铺排序流程

sequenceDiagram
    participant 前端
    participant 后端
    participant 数据库
    
    前端->>后端: POST /api/v1/amzgroup/updateBatch
    后端->>后端: 获取当前用户信息
    后端->>数据库: 批量更新店铺排序号
    数据库-->>后端: 返回更新结果
    后端-->>前端: 返回操作结果

6. 技术要点和难点

6.1 前端技术要点

6.2 后端技术要点

6.3 技术难点

7. 代码优化建议

7.1 前端代码优化

  1. 错误处理优化

    • 当前代码在API调用失败时缺少统一的错误处理机制
    • 建议实现全局错误处理拦截器,统一处理API错误
  2. 表单验证优化

    • 当前表单验证逻辑较为简单,建议使用Element Plus的表单验证规则
    • 实现更全面的表单验证,确保数据的有效性
  3. 代码结构优化

    • 将店铺管理相关的逻辑抽取为独立的composable函数
    • 提高代码的复用性和可读性
  4. 性能优化

    • 实现店铺列表的虚拟滚动,提高大数据量下的渲染性能
    • 使用缓存机制减少重复的API调用

7.2 后端代码优化

  1. 安全性优化

    • 当前代码中缺少对输入参数的验证,建议实现请求参数验证
    • 使用@Valid注解和校验组实现更严格的参数验证
  2. 异常处理优化

    • 当前代码中异常处理较为简单,建议实现统一的异常处理机制
    • 提供更详细的错误信息和错误码
  3. 性能优化

    • 实现店铺列表的缓存机制,减少数据库查询
    • 使用批量操作减少数据库交互次数
  4. 代码结构优化

    • 将业务逻辑进一步分离,提高代码的可维护性
    • 实现更细粒度的服务层接口

7.3 架构优化

  1. 微服务架构

    • 考虑将店铺管理模块抽取为独立的微服务
    • 提高系统的扩展性和可维护性
  2. 缓存架构

    • 实现分布式缓存,提高系统性能
    • 缓存店铺列表和常用数据
  3. 监控架构

    • 实现店铺管理模块的监控和告警机制
    • 及时发现和处理系统异常

8. 总结

店铺管理模块是Wimoor系统中管理亚马逊店铺信息的核心模块,通过该模块用户可以方便地管理店铺的基本信息、财务设置和海关信息。模块采用前后端分离架构,前端使用Vue 3 Composition API实现用户界面,后端使用Spring Boot实现业务逻辑。

模块的核心功能包括店铺的添加、编辑、删除,店铺排序的调整,财务账套的设置和初始化,以及海关信息的配置。通过这些功能,用户可以有效地管理多个亚马逊店铺的信息,为后续的业务操作提供基础数据支持。

在技术实现上,模块解决了权限控制、数据一致性、拖拽排序等技术难点,为系统的稳定运行提供了保障。同时,通过代码优化建议的实施,可以进一步提高模块的性能、安全性和可维护性。