开发文档
- 计算器-利润计算
- 财务报表模板配置
- 产品-产品管理
- 产品-品牌管理
- 发货-询价-设置
- 财务-报告
- 发货-FBA发货规划
- 发货-创建发货单
- 发货-审核发货单
- 发货-配货
- 发货-货件处理
- 货件-货件跟踪
- FBA每日库存
- 发货-发货详情(新)
- 发货-发货统计(新)
- 发货-费用分摊(新)
- 销售-商品分析
- 广告-广告管理
- 广告-广告统计
- 设置-1688绑定
- 设置-店铺管理
计算器-利润计算
1. 文件概述
文件路径:src\views\amazon\report\fba_fee
文件类型:Vue 3 单文件组件(SFC)
所属模块:ERP系统 - FBA费用报告页面
功能定位:本组件是亚马逊FBA费用报告页面,用于展示和对比自测FBA参数与亚马逊FBA参数之间的差异,帮助用户了解FBA费用的计算情况及可能的超收费用。
2. 功能说明
2.1 核心功能
- 展示产品的FBA费用相关数据
- 对比自测参数与亚马逊官方参数
- 计算并显示每月预计超收费用
- 提供数据筛选和导出功能
2.2 页面结构
- 页面头部:包含数据筛选器和操作按钮
- 提示信息:重要提示说明,提醒用户产品类型对FBA费用的影响
- 表格展示:详细展示各产品的FBA费用相关数据
2.3 操作按钮
- 列配置:设置表格显示的列
- 帮助文档:查看本帮助文档
3. 数据展示说明
3.1 表格列说明
| 列名 | 说明 | 详细信息 |
|---|---|---|
| 图片 | 产品主图 | 显示产品的缩略图,便于识别产品 |
| 商品信息 | 产品基本信息 | 包含产品名称、SKU,以及是否为低价产品(低价产品会显示绿色标签) |
| 参数类型 | 参数来源 | 分为两行:自测参数(用户录入)和FBA参数(亚马逊官方) |
| 尺寸(长宽高cm) | 产品尺寸 | 分为两行:自测尺寸(用户录入)和亚马逊尺寸(亚马逊官方测量) |
| 重量(kg) | 产品重量 | 分为两行:自测重量(用户录入)和亚马逊重量(亚马逊官方测量) |
| 价格 | 产品售价 | 显示产品的销售价格,包含货币符号 |
| 尺寸分段 | 尺寸分类 | 分为两行:自测尺寸分段和亚马逊尺寸分段 |
| FBA收费(每月预计超收费用) | FBA费用对比 | 分为两行显示自测FBA费用和亚马逊FBA费用,若有超收费用会显示红色标签 |
| 更新时间 | 数据更新时间 | 显示数据的最后更新时间,若未更新则显示"未能正常更新" |
3.2 数据来源
- 自测参数:用户在系统中录入的产品参数
- FBA参数:亚马逊官方测量的产品参数
- FBA费用:根据对应参数计算得出的FBA费用
4. 使用方法
4.1 基本操作
- 进入FBA费用报告页面
- 使用页面头部的筛选器设置查询条件
- 点击查询按钮加载数据
- 查看表格中的FBA费用对比数据
- 点击列配置按钮可自定义表格显示的列
4.2 数据解读
- 参数对比:通过对比自测参数与FBA参数,了解参数差异
- 费用对比:通过对比自测FBA费用与亚马逊FBA费用,了解费用差异
- 超收费用:若亚马逊FBA费用高于自测FBA费用,会显示红色标签的超收费用
5. 注意事项
-
重要提示:当本地录入的产品类型与亚马逊分类不一致时,可能造成FBA费用不一样。如:Apparel/Clothing类型的产品,在美国站点每笔加收$0.4。
-
低价产品:标有"低价产品"标签的商品属于Small & Light计划,FBA费用计算方式不同。
-
数据更新:请定期检查数据更新时间,确保使用最新的FBA费用数据。
-
费用差异:若发现FBA费用存在较大差异,建议核对产品参数是否准确,或联系亚马逊客服确认。
6. 技术说明
6.1 API接口
- 使用
productFbaFeeApi.getSizePro获取FBA费用数据
6.2 工具函数
dateTimesFormat:格式化日期时间formatFloat:格式化浮点数
6.3 依赖组件
MyHeader:页面头部组件GlobalTable:全局表格组件ElMessage:消息提示组件Element Plus:UI组件库
7. 常见问题
7.1 为什么自测参数与FBA参数不一致?
- 可能是用户录入的参数不准确
- 可能是亚马逊测量方式与用户测量方式不同
- 建议重新测量产品参数并更新系统数据
7.2 为什么会产生超收费用?
- 通常是由于参数差异导致的费用计算差异
- 也可能是产品分类不正确导致的额外费用
7.3 如何减少FBA费用?
- 确保产品参数录入准确
- 选择合适的产品分类
- 考虑使用Small & Light计划(针对低价产品)
更新日志
- 2026-01-08:初始版本发布,包含FBA费用对比功能
- 支持自测参数与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 | 自定义数据源 | 配置自定义数据源代码 |
示例配置:
- 科目余额:选择SUBJECT,配置科目代码"1001,1002",金额类型"期末余额"
- 常量值:选择CONSTANT,配置计算规则"1000000"
4.3 FORMULA公式配置
使用数学公式计算项目金额,支持标准数学运算符和函数。
4.3.1 公式语法
- 基本运算符:+、-、*、/、()
- 函数支持:SUM()、AVG()、MAX()、MIN()等
- 科目引用:直接使用科目代码,系统自动转换
示例公式:
- 货币资金 = 库存现金 + 银行存款 →
1001 + 1002 - 利润总额 = 营业利润 + 营业外收入 - 营业外支出 →
PROFIT_OPERATING + 6301 - 6711 - 净利润 = 利润总额 × (1 - 所得税税率) →
PROFIT_TOTAL * (1 - 0.25)
4.3.2 公式预处理器
系统会自动对公式进行预处理:
- 清理公式中的空格
- 将科目代码转换为系统可识别的格式(如"1001" → "ACC_1001")
- 验证公式语法正确性
4.4 CUSTOM公式配置
使用系统预定义的自定义规则进行计算:
| 自定义规则 | 说明 | 适用报表类型 |
|---|---|---|
| ASSET_CURRENT | 流动资产合计 | 资产负债表 |
| ASSET_NON_CURRENT | 非流动资产合计 | 资产负债表 |
| LIABILITY_CURRENT | 流动负债合计 | 资产负债表 |
| LIABILITY_NON_CURRENT | 非流动负债合计 | 资产负债表 |
| EQUITY_TOTAL | 所有者权益合计 | 资产负债表 |
| INCOME_OPERATING | 营业收入 | 利润表 |
| COST_OPERATING | 营业成本 | 利润表 |
4.5 CALCULATED公式配置
自动计算规则,目前支持以下类型:
| 计算规则 | 说明 |
|---|---|
| SUM(CHILDREN) | 自动汇总所有显示的子项目金额 |
示例配置:
- 流动资产合计 = SUM(CHILDREN) → 自动汇总所有流动资产项目的金额
5. 数据来源配置
5.1 科目余额数据源
从会计科目余额表获取数据,支持以下金额类型:
- 期末余额:会计科目在报告期末的余额
- 借方发生额:会计科目在报告期内的借方发生额合计
- 贷方发生额:会计科目在报告期内的贷方发生额合计
- 年初余额:会计科目在报告期年初的余额
5.2 常量数据源
直接使用固定数值作为项目金额,适用于:
- 固定的财务指标
- 调整项或特殊项目
- 预算目标值
5.3 自定义数据源
从系统预定义的自定义数据源获取数据,支持:
- 预算数据
- 外部系统数据
- 自定义统计数据
6. 报表生成与验证
6.1 报表生成流程
- 选择报表模板
- 设置报告期间
- 选择对比期间(可选)
- 点击"生成报表"按钮
- 系统自动计算并生成报表
6.2 报表验证
系统会自动验证报表的完整性和准确性:
- 资产负债表验证:资产总计 = 负债合计 + 所有者权益合计
- 利润表验证:营业收入 > 0(根据实际业务规则调整)
- 项目计算验证:检查公式计算是否存在错误
7. 常见问题与解决方案
7.1 公式计算错误
问题:报表生成时提示"公式计算错误" 解决方案:
- 检查公式中的科目代码是否正确
- 检查公式语法是否符合规范
- 确认所有引用的项目或科目都已正确配置
- 检查数据源是否有可用数据
7.2 项目金额显示为0
问题:报表项目显示为0,但预期有数据 解决方案:
- 检查项目的"是否显示"设置是否为1
- 检查数据源是否有实际数据
- 检查公式配置是否正确
- 确认父级项目是否包含该子项目
7.3 报表层级显示错误
问题:报表项目的层级关系显示不正确 解决方案:
- 检查项目的"项目级别"设置是否正确
- 检查父级编码配置是否正确
- 确认项目的行次设置是否符合预期顺序
8. 最佳实践
8.1 模板设计原则
- 模块化设计:将报表拆分为多个逻辑模块,便于维护和扩展
- 层级清晰:保持项目层级关系清晰,避免过深的层级结构
- 命名规范:使用统一的命名规范,提高模板的可读性
- 复用性:设计可复用的报表模板,减少重复配置工作
8.2 公式配置技巧
- 优先使用系统函数:如SUM(CHILDREN),减少手动维护工作量
- 合理使用科目代码:直接引用科目代码比引用项目更灵活
- 避免复杂嵌套:复杂公式拆分为多个简单公式,提高可维护性
- 添加注释:对复杂公式添加说明,便于后续维护
8.3 性能优化建议
- 减少不必要的计算:隐藏不需要显示的项目,减少计算量
- 合理使用缓存:启用报表缓存功能,提高报表生成速度
- 优化公式复杂度:避免使用过于复杂的公式,影响计算性能
- 定期清理:定期清理不再使用的模板和项目,保持系统整洁
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 |
三、使用说明
- 规则选择:在配置报表项目时,可根据需要选择合适的自定义规则代码
- 科目映射:系统会自动根据上述表格中的科目代码进行计算
- 自定义扩展:如需添加新的自定义规则,需联系技术人员进行开发
- 规则优先级:自定义规则的计算优先级高于普通公式
四、示例应用
示例:配置资产负债表的"流动资产合计"项目
- 选择公式类型:CUSTOM
- 输入计算规则:ASSET_CURRENT
- 系统会自动计算:货币资金 + 应收票据 + 应收账款 + 预付款项 + 存货 + 其他流动资产
示例:配置现金流量表的"销售商品、提供劳务收到的现金"项目
- 选择公式类型:CUSTOM
- 输入计算规则:CASH_IN_SALES
- 系统会自动计算:营业收入 + 销项税额 + 应收账款减少 + 预收账款增加
五、注意事项
- 所有科目代码遵循企业会计准则的标准科目编码
- 多级科目使用"."或无分隔符表示,如"22210102"表示"应交税费-应交增值税-销项税额"
- 部分规则涉及前期与本期数据的比较,系统会自动处理期间逻辑
- 如发现规则计算结果异常,请检查相关科目是否有正确的余额数据
六、更新记录
| 日期 | 版本 | 更新内容 |
|---|---|---|
| 2025-11-04 | V1.0 | 初始版本,包含所有自定义计算规则 |
本帮助手册详细介绍了财务报表模板的配置方法和使用技巧,希望能帮助您快速掌握报表模板的配置和应用。如有其他问题,请联系系统管理员或技术支持团队。
产品-产品管理
产品-品牌管理
1. 文件概述
文件路径:/src/views/erp/baseinfo/brand/index.vue
文件类型:Vue 3 单文件组件(SFC)
所属模块:ERP系统 - 基础信息管理 - 品牌管理
功能定位:该组件实现了品牌信息的增、删、改、查等核心功能,是ERP系统基础信息管理的重要组成部分。
2. 技术架构
2.1 技术栈
- 前端框架:Vue 3
- 开发模式:Composition API
- UI组件库:Element Plus
- 图标库:IconPark
- HTTP客户端:Axios(通过brandApi间接使用)
2.2 核心组件
主要依赖组件:
<GlobalTable>:自定义表格组件,用于数据展示和分页<el-dialog>:Element Plus对话框组件,用于新增/编辑品牌信息<el-form>:Element Plus表单组件,用于品牌信息的输入和验证<el-button>、<el-input>、<el-table>等基础UI组件
2.3 API依赖
引入的API模块:
import brandApi from '@/api/erp/material/brandApi.js';
使用的API方法:
brandApi.list():获取品牌列表数据brandApi.saveData():保存或更新品牌信息brandApi.delBrand():删除品牌信息
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 数据展示
功能描述:以表格形式展示品牌信息,支持分页和排序。
技术实现:
- 使用
GlobalTable组件实现数据展示 - 默认按操作时间降序排序
- 表格高度自适应,避免页面滚动
4.2 品牌搜索
功能描述:根据品牌名称进行搜索过滤。
使用方法:
- 在搜索框中输入品牌名称
- 点击搜索按钮或按Enter键
- 系统会过滤显示匹配的品牌信息
技术实现:
loadData()函数处理搜索逻辑- 将搜索关键词传递给API进行过滤
- 支持空搜索(显示所有品牌)
4.3 新增品牌
功能描述:添加新的品牌信息。
使用方法:
- 点击"添加品牌"按钮
- 在弹出的对话框中填写品牌信息
- 点击"提交"按钮保存
技术实现:
Add()函数清空表单并打开对话框- 表单验证确保品牌名称必填
- 调用
brandApi.saveData()保存数据
4.4 编辑品牌
功能描述:修改现有品牌信息。
使用方法:
- 在表格中找到要修改的品牌
- 点击"编辑"按钮
- 在弹出的对话框中修改信息
- 点击"提交"按钮保存
技术实现:
Edit(rows)函数将选中的品牌数据填充到表单- 调用
brandApi.saveData()更新数据 - 自动关闭对话框并刷新列表
4.5 删除品牌
功能描述:删除不需要的品牌信息。
使用方法:
- 在表格中找到要删除的品牌
- 点击"删除"按钮
- 在确认对话框中点击"确认"按钮
技术实现:
Remove(rows)函数触发删除确认对话框- 调用
brandApi.delBrand()删除数据 - 使用Element Plus的
ElMessageBox和ElMessage提供操作反馈
5. 数据流程
5.1 数据加载流程
- 组件挂载时调用
onMounted(loadData) loadData()构建搜索参数并调用globalTable.value.loadTable()GlobalTable组件内部调用loadTableData()方法loadTableData()调用brandApi.list()获取品牌列表数据- 数据返回后更新
tableData状态,表格自动刷新
5.2 数据保存流程
- 用户点击"添加"或"编辑"按钮,打开对话框
- 用户填写表单并点击"提交"
- 表单验证通过后调用
brandApi.saveData() - API返回成功后,关闭对话框并调用
loadData()刷新列表 - 显示操作成功的消息提示
5.3 数据删除流程
- 用户点击"删除"按钮,弹出确认对话框
- 用户点击"确认"后调用
brandApi.delBrand() - API返回成功后,调用
loadData()刷新列表 - 显示删除成功的消息提示
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 状态转换
- 表格数据更新:
loadTableData()函数更新tableData - 搜索状态:
searchKeywords实时响应输入变化 - 对话框状态:
dialogVisible控制对话框的显示和隐藏 - 表单数据:新增时清空,编辑时填充选中行数据
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 基本操作流程
- 查看品牌列表:进入页面后自动加载所有品牌信息
- 搜索品牌:在搜索框中输入名称,点击搜索按钮
- 新增品牌:点击"添加品牌"按钮,填写信息后提交
- 编辑品牌:找到要修改的品牌,点击"编辑"按钮,修改后提交
- 删除品牌:找到要删除的品牌,点击"删除"按钮,确认后删除
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 页面布局
页面采用左右分栏布局:
- 左侧(25%宽度):询价商管理模块
- 右侧(75%宽度):物流供应商管理模块
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>
功能说明:
- 根据是否绑定令牌显示不同的UI界面
- 已绑定状态:显示令牌信息、别名和解绑按钮
- 未绑定状态:提供输入框和绑定按钮
- 支持复制令牌功能
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>
功能说明:
- 表格展示供应商列表,包含名称、地址、联系人、联系电话等信息
- 根据用户权限(
isowner和v-hasPerm)决定是否显示供应商页面链接 - 提供编辑和删除操作按钮
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 页面初始化流程
- 页面加载时调用
onMounted(() => loadToken()) loadToken()调用thirdpartyApi.getQuoteToken()获取当前用户绑定的令牌信息- 如果令牌存在,调用
loadBuyer()和loadSupplier()分别加载询价商和供应商数据 - 渲染页面,显示已绑定或未绑定状态
5.2 令牌绑定流程
- 用户输入令牌和别名
- 点击"绑定"按钮,调用
bindToken()方法 bindToken()先调用orderApi.getBuyer()验证令牌有效性- 如果验证成功,调用
thirdpartyApi.saveQuoteToken()保存令牌信息 - 保存成功后,加载询价商和供应商数据,更新页面状态
5.3 供应商管理流程
5.3.1 添加供应商
- 点击"新增"按钮,调用
handleShow()方法 - 打开
CreateDialog组件对话框 - 用户填写供应商信息并点击保存
- 调用
supplierApi.addsupplier()保存供应商信息 - 保存成功后,刷新供应商列表
5.3.2 编辑供应商
- 点击供应商列表中的"编辑"按钮,调用
editSupplier()方法 - 打开
CreateDialog组件对话框并填充现有数据 - 用户修改信息并点击保存
- 调用
supplierApi.updatesupplier()更新供应商信息 - 更新成功后,刷新供应商列表
5.3.3 删除供应商
- 点击供应商列表中的"删除"按钮,调用
delSupplier()方法 - 弹出确认对话框
- 用户确认后,调用
supplierApi.deletesupplier()删除供应商 - 删除成功后,刷新供应商列表
6. 操作指南
6.1 绑定询价商令牌
- 在左侧"询价商管理"区域,找到"令牌"输入框
- 输入从询价商获取的有效令牌
- 输入别名(可选)
- 点击"绑定"按钮
- 系统验证令牌有效性并完成绑定
- 绑定成功后,页面显示已绑定状态和令牌信息
6.2 设置询价商详细信息
- 确保已成功绑定询价商令牌
- 找到"高级"折叠面板并点击展开
- 点击"修改"按钮进入编辑模式
- 填写或修改以下信息:
- 名称:询价商公司名称
- 地址:询价商公司地址
- 手机号:联系电话
- 联系人:对接人姓名
- 点击"保存"按钮完成设置
6.3 新增物流供应商
- 确保已成功绑定询价商令牌
- 在右侧"物流供应商管理"区域,点击"新增"按钮
- 在弹出的对话框中填写供应商信息:
- 名称:物流供应商名称
- 地址:供应商地址
- 联系人:对接人姓名
- 手机号:联系电话
- 点击"保存"按钮完成新增
- 新增成功后,供应商列表会自动刷新
6.4 编辑物流供应商
- 在供应商列表中找到需要修改的供应商
- 点击该供应商右侧的"编辑"按钮
- 在弹出的对话框中修改供应商信息
- 点击"保存"按钮完成修改
- 修改成功后,供应商列表会自动刷新
6.5 删除物流供应商
- 在供应商列表中找到需要删除的供应商
- 点击该供应商右侧的"删除"按钮
- 在弹出的确认对话框中点击"确定"按钮
- 删除成功后,供应商列表会自动刷新
7. 常见问题解答
7.1 绑定令牌失败
问题现象:点击"绑定"按钮后,系统提示"绑定失败,未找到询价商"
可能原因:
- 令牌输入错误
- 令牌已过期
- 网络连接问题
- 询价商系统故障
解决方案:
- 检查令牌是否正确输入(注意大小写和空格)
- 联系询价商确认令牌有效性
- 检查网络连接
- 稍后重试
7.2 无法看到供应商列表
问题现象:绑定令牌后,右侧供应商列表为空
可能原因:
- 该询价商下尚未添加任何物流供应商
- 网络连接问题
- 权限不足
解决方案:
- 点击"新增"按钮添加物流供应商
- 检查网络连接
- 确认当前用户有查看供应商列表的权限
7.3 无法删除供应商
问题现象:点击"删除"按钮后,系统提示删除失败
可能原因:
- 供应商已被其他业务引用
- 权限不足
- 网络连接问题
解决方案:
- 确认该供应商没有关联的报价订单
- 检查当前用户是否有删除权限
- 检查网络连接
7.4 无法访问供应商页面
问题现象:点击供应商页面链接后无法访问
可能原因:
- 不是询价商所有者(
isowner为false) - 没有访问权限(
v-hasPerm='erp:pi:supplier:link'不满足) - 供应商页面URL格式错误
解决方案:
- 确认当前用户是询价商所有者
- 联系系统管理员获取访问权限
- 尝试手动复制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 # 模板管理主页面
核心组件:
items/index.vue: 报表项目展示页面,通过标签页切换不同报表模板report_sheet.vue: 报表展示组件,负责展示生成的报表数据和图表templates/index.vue: 报表模板管理页面,提供模板的增删改查功能template_items.vue: 模板项目管理组件,负责管理模板下的项目template_item_edit.vue: 模板项目编辑组件,用于配置项目的属性和计算公式
2.2 后端架构
核心控制器:
ReportController.java: 报表生成控制器,处理报表生成请求FinReportItemsController.java: 报表项目控制器,处理报表项目的CRUD操作
核心服务:
IFinReportTemplatesService.java: 报表模板服务接口FinReportTemplatesServiceImpl.java: 报表模板服务实现,包含报表生成核心逻辑
数据传输对象:
ReportGenerateRequest.java: 报表生成请求DTOReportGenerateResponse.java: 报表生成响应DTOReportItemValueDTO.java: 报表项目值DTO
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 报表项目金额计算
功能说明:
- 支持多种公式类型:DIRECT(直接科目)、FORMULA(自定义公式)、CUSTOM(自定义规则)、CALCULATED(自动计算)
- 按层级从高到低排序计算,确保父级项目在子级之后计算
核心代码:
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 公式解析与计算
功能说明:
- 使用AviatorEvaluator解析和执行复杂公式
- 支持科目代码引用、数学运算、函数调用等
- 自动处理科目代码到变量名的转换
核心代码:
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
请求参数:
templateId: 模板ID
响应参数:
{
"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 前端技术
- Vue 3
- Element Plus
- ECharts(图表展示)
- Axios(HTTP请求)
6.2 后端技术
- Spring Boot
- MyBatis-Plus
- AviatorEvaluator(公式计算)
- Redis(可选,用于缓存)
7. 系统流程
7.1 报表生成流程
- 用户在前端选择报表模板和期间
- 前端调用报表生成接口(
/api/report/generate) - 后端获取报表模板和项目结构
- 后端计算当前期间和比较期间的项目金额
- 后端生成财务指标和图表数据
- 后端将结果返回给前端
- 前端展示报表数据和图表
7.2 模板项目配置流程
- 用户在模板管理页面选择一个模板
- 进入模板项目管理页面
- 点击"新增项目"按钮,填写项目信息
- 配置项目的计算公式和数据来源
- 保存项目配置
- 可以对项目进行验证,确保计算公式正确
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();
}
算法特点:
- 标准化处理:统一公式格式,移除空格干扰
- 科目代码识别:使用正则表达式精准匹配会计科目代码
- 变量转换:将科目代码转换为Aviator引擎可识别的变量名(如:1001 → ACC_1001)
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);
}
}
算法特点:
- 精度控制:使用BigDecimal确保财务计算精度
- 表达式引擎:集成Aviator引擎支持复杂数学运算
- 异常封装:提供清晰的错误信息便于调试
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());
}
}
算法特点:
- 类型安全:支持多种数值类型转换为BigDecimal
- 异常明确:对不支持的类型给出明确错误信息
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;
}
算法特点:
- 多数据源支持:支持科目余额、常量、自定义三种数据源
- 容错处理:常量值解析失败返回0而非抛出异常
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);
}
算法特点:
- 流式计算:使用Java 8 Stream API实现高效汇总
- 条件过滤:仅汇总显示的子项目
- 默认值处理:未计算的项目使用0作为默认值
4. 算法优化与设计亮点
4.1 计算顺序优化
问题:父级项目通常依赖子级项目的计算结果
解决方案:按项目层级从高到低排序计算,确保父级项目在所有子级项目计算完成后执行
4.2 精度控制机制
问题:财务计算对精度要求极高,浮点数计算易产生误差
解决方案:
- 统一使用BigDecimal进行所有计算
- 禁用Aviator引擎的double类型转换
- 所有中间结果和最终结果保持BigDecimal类型
4.3 缓存机制
问题:相同条件下重复生成报表会造成性能浪费
解决方案:实现基于groupid、templateCode和period的缓存机制,避免重复计算(目前代码中已实现但注释掉)
4.4 容错设计
问题:单个项目计算失败不应影响整个报表生成
解决方案:
- 每个项目计算独立捕获异常
- 记录错误日志但继续计算其他项目
- 失败项目返回0值确保报表结构完整
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 产品列表模块
产品列表是页面的核心部分,展示了所有符合条件的产品信息,支持排序、筛选和展开查看详情。
核心功能:
- 产品基本信息展示(SKU、名称、标签等)
- 发货需求量和实际发货总量显示
- 可用库存和可组装库存信息
- 批量展开/折叠操作
- 行点击事件处理
2.3 展开详情模块
点击产品行展开按钮,可查看该产品的详细发货计划信息,支持编辑和管理发货计划。
核心功能:
- 按国家/地区展示发货计划
- 发货数量编辑
- 运输方式选择
- 海外仓选择
- 发货计划拆分
2.4 报表和分析模块
提供产品销量和预计到货的可视化报表,帮助用户分析销售趋势和库存状况。
核心功能:
- 销量报表(柱状图)
- 预计到货报表(折线图)
- 库存差异分析(FBA库存差异图表)
2.5 辅助功能模块
页面还包含多种辅助功能,增强用户操作体验和数据管理能力。
核心功能:
- 备货周期管理
- 库存详情查询
- 库存库龄分析
- 产品备注管理
- 产品组装管理
3. 核心功能解析
3.1 发货计划管理
功能描述: 允许用户为产品创建、编辑和删除发货计划,支持按国家/地区细分。
实现原理:
- 通过
planApi.getPlanList获取产品列表数据 - 点击"编辑"按钮进入编辑模式
- 在展开的详情表格中设置发货数量和运输方式
- 点击"保存"按钮通过
planApi.save保存发货计划 - 点击"移除"按钮通过
planApi.remove删除发货计划
关键代码:
- 加载数据:
loadTableData函数(800行) - 保存计划:
savePlanItem函数(940行) - 提交表单:
submitForm函数(952行) - 删除计划:
handleDelete函数(929行)
3.2 库存分析和预警
功能描述: 提供产品库存的详细分析,包括可用库存、预留库存、待入库库存等信息,并通过图表展示库存差异。
实现原理:
- 通过
inventoryApi.getInventory获取库存数据 - 通过
inventoryRptApi.syncInventorySupply同步FBA库存 - 点击库存数量查看详细库存信息
- 点击FBA库存差异图表查看库存差异分析
关键代码:
- 刷新库存:
refreshInventory函数(563行) - 库存图表:
FBAinventoryChart函数(490行) - 查看库存详情:
handleShowInventory函数(1245行)
3.3 发货计划拆分
功能描述: 支持将一个发货计划拆分为多个子计划,适用于不同运输方式或不同批次的发货需求。
实现原理:
- 点击"拆分"按钮打开拆分对话框
- 设置拆分后的子计划数量和运输方式
- 保存拆分结果并更新主计划
关键代码:
- 显示拆分对话框:
showSplitRow函数(38行) - 处理拆分结果:
handleSplitRow函数(764行) - 计划拆分API:
planApi.subsplit调用(882行)
3.4 销量和到货分析
功能描述: 通过图表展示产品的销量趋势和预计到货情况,帮助用户做出更合理的发货决策。
实现原理:
- 点击销量报表图标打开销量图表对话框
- 点击预计到货报表图标打开到货图表对话框
- 图表数据通过后端API获取并使用ECharts渲染
关键代码:
- 销量报表:
handlesaleChart函数(684行) - 到货报表:
handlarrivalChart函数(784行)
3.5 产品组装管理
功能描述: 对于组合产品,显示其组成部分和可组装数量,帮助用户了解组合产品的库存状况。
实现原理:
- 点击组合产品标签查看组装详情
- 通过
sublitAPI获取组装部件信息 - 显示可组装数量和部件库存状态
关键代码:
- 显示组装对话框:
handleAssemblyShow函数(549行) - 获取组装列表:
getAssembliyList函数(609行)
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 发货计划创建流程
- 数据加载:用户设置筛选条件,系统通过
planApi.getPlanList获取产品列表数据 - 选择产品:用户在产品列表中找到需要创建发货计划的产品
- 编辑计划:点击"编辑"按钮,进入编辑模式
- 设置参数:在展开的详情表格中,设置各国家/地区的发货数量、运输方式等参数
- 保存计划:点击"保存"按钮,系统通过
planApi.save保存发货计划 - 数据更新:保存成功后,系统更新产品列表中的发货状态和数据
7.2 库存分析流程
- 数据获取:系统通过
inventoryApi.getInventory获取产品库存数据 - 库存计算:计算可用库存、可组装库存等关键指标
- 状态判断:根据库存数据判断产品库存状态
- 预警提示:对库存不足的产品进行预警提示
- 数据展示:在产品列表中展示库存状态和相关数据
7.3 计划拆分流程
- 选择计划:在展开的详情表格中,找到需要拆分的发货计划
- 点击拆分:点击"拆分"按钮,打开拆分对话框
- 设置参数:在对话框中设置拆分后的子计划数量、运输方式等参数
- 确认拆分:点击"确认"按钮,系统通过
planApi.subsplit处理拆分数据 - 更新计划:拆分完成后,系统更新发货计划数据
8. 操作指南
8.1 基本操作
8.1.1 搜索和筛选产品
- 在页面顶部的搜索栏中设置筛选条件
- 选择仓库、店铺、站点等筛选条件
- 点击"查询"按钮,系统会根据条件加载产品数据
8.1.2 查看产品详情
- 在产品列表中找到需要查看的产品
- 点击产品行左侧的展开按钮,或点击"展开全部"按钮查看所有产品详情
- 在展开的详情表格中查看产品的详细发货数据
8.1.3 创建发货计划
- 找到需要创建发货计划的产品,点击"编辑"按钮
- 在展开的详情表格中,为各国家/地区设置发货数量
- 选择合适的运输方式和海外仓(如需)
- 点击"保存"按钮,完成发货计划创建
8.1.4 修改发货计划
- 找到需要修改的发货计划,点击"编辑"按钮
- 修改发货数量、运输方式等参数
- 点击"保存"按钮,完成发货计划修改
8.1.5 删除发货计划
- 找到需要删除的发货计划,点击"移除"按钮
- 在弹出的确认对话框中点击"确定",完成发货计划删除
8.2 高级操作
8.2.1 查看销量报表
- 在产品列表中找到需要查看销量的产品
- 点击产品行中的销量报表图标(柱状图)
- 在弹出的销量报表对话框中查看详细的销量数据
8.2.2 查看预计到货报表
- 在产品列表中找到需要查看预计到货的产品
- 点击产品行中的预计到货报表图标(折线图)
- 在弹出的预计到货报表对话框中查看详细的到货数据
8.2.3 查看库存详情
- 在产品列表中找到需要查看库存的产品
- 点击产品行中的可用库存数量
- 在弹出的库存详情对话框中查看详细的库存数据
8.2.4 计划拆分
- 在展开的详情表格中,找到需要拆分的发货计划
- 点击"拆分"按钮,打开拆分对话框
- 设置拆分后的子计划数量、运输方式等参数
- 点击"确认"按钮,完成计划拆分
8.2.5 产品组装管理
- 在产品列表中找到需要查看组装信息的组合产品(带有"组合"标签)
- 点击"组合"标签,打开组装产品对话框
- 在对话框中查看产品的组装部件和可组装数量
9. 常见问题
9.1 库存数据不准确
问题描述:产品列表中显示的库存数据与实际库存不符
解决方法:
- 点击库存数量旁边的刷新图标,手动同步FBA库存数据
- 检查库存数据来源是否正确
- 确认仓库选择是否正确
9.2 无法保存发货计划
问题描述:点击"保存"按钮后,发货计划无法保存
解决方法:
- 检查发货数量是否超过可用库存
- 确认运输方式和海外仓选择是否正确
- 检查网络连接是否正常
- 刷新页面后重新尝试
9.3 计划拆分失败
问题描述:点击"拆分"按钮后,计划拆分失败
解决方法:
- 检查发货计划数据是否完整
- 确认拆分参数设置是否合理
- 刷新页面后重新尝试
9.4 报表数据加载缓慢
问题描述:点击报表图标后,报表数据加载缓慢
解决方法:
- 检查网络连接是否正常
- 确认产品数据量是否过大
- 尝试减少时间范围,查看数据量较小的报表
9.5 产品无法隐藏
问题描述:点击"隐藏产品"后,产品仍然显示在列表中
解决方法:
- 检查产品是否已加入发货计划(已加入计划的产品无法隐藏)
- 确认操作权限是否足够
- 刷新页面后重新尝试
10. 技术实现细节
10.1 前端技术栈
- 框架:Vue 3 + Composition API
- UI组件:Element Plus
- 图表库:ECharts
- 状态管理:reactive + ref
- 生命周期:onMounted, nextTick
10.2 后端技术栈
- 框架:Spring Boot
- ORM:MyBatis Plus
- API风格:RESTful
- 事务管理:@Transactional
- 认证授权:UserInfoContext
10.3 性能优化
-
数据加载优化:
- 使用分页查询减少数据传输量
- 延迟加载非关键数据
- 缓存常用数据
-
渲染优化:
- 使用虚拟滚动处理大量数据
- 优化组件渲染性能
- 减少不必要的DOM操作
-
交互优化:
- 异步处理耗时操作
- 提供加载状态反馈
- 优化用户操作流程
10.4 代码结构
前端代码结构:
index.vue:主页面组件components/expand_table.vue:展开详情表格组件components/ship_record.vue:发货记录组件components/remarks_dialog.vue:备注对话框组件components/split_plan_dialog.vue:计划拆分对话框组件
后端代码结构:
AmzProductSalesPlanController.java:发货计划控制器AmzProductSalesPlanShipItemController.java:发货计划项目控制器AmzProductSalesPlanShipItem.java:发货计划项目实体AmzProductSalesPlan.java:产品销售计划实体
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 初始化流程
- 组件加载:
onMounted钩子函数触发初始化 - 初始化查询数据:
initQueryData()从路由参数中获取初始化数据 - 加载计划名称:
loadShipName()生成计划名称 - 加载计划编码:
loadNumber()获取序列号作为计划编码 - 加载店铺列表:
getGroupData("init")获取店铺列表并设置默认值 - 加载仓库列表:
getWarehouseData()获取仓库列表并设置默认值 - 加载物流方式:
getTranList()获取物流方式列表 - 加载入库地点:
loadInplace()获取入库地点列表
5.2 表单填写流程
-
基本信息填写:
- 选择发货店铺 → 触发
groupChange()→ 加载对应市场列表和地址列表 - 选择发货仓库 → 触发
warehouseChange()→ 更新仓库信息 - 选择收货国家 → 触发
marketChange()→ 更新国家信息并验证 SKU - 选择发货地址 → 触发
radioChange()→ 更新地址信息 - 填写备注、运输方式、物流方式、单据类型
- 选择发货店铺 → 触发
-
商品管理:
- 添加商品:点击"添加商品"按钮 → 打开商品选择对话框 → 选择商品 → 触发
getdata()→ 添加商品到列表 - 导入商品:点击"导入"按钮 → 打开导入数据对话框 → 上传文件 → 处理导入数据
- 打印标签:点击"打印当前产品标签"按钮 → 打印商品标签
- 清空列表:点击"清空列表"按钮 → 清空商品列表
- 删除商品:点击商品行的"删除"按钮 → 从列表中移除商品
- 其他维护:点击"其它维护"按钮 → 打开其它信息设置对话框 → 设置预备信息处理人、贴标人、保质期
- 添加商品:点击"添加商品"按钮 → 打开商品选择对话框 → 选择商品 → 触发
5.3 提交流程
-
准备数据:
- 收集表单数据
- 处理商品列表数据
- 设置计划项目列表
-
提交计划:
- 点击"提交"按钮 → 触发
submitPlan() - 显示加载状态
- 调用
shipmentplanApi.saveInboundPlan(state.formData)提交数据 - 处理响应:显示成功消息 → 跳转到新货件详情页面
- 点击"提交"按钮 → 触发
-
取消操作:
- 点击"取消"按钮 → 触发
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 前端优化
- 错误处理增强:添加更详细的错误处理和用户提示,特别是在 API 调用失败时
- 性能优化:对于大量商品的列表,考虑使用虚拟滚动
- 代码组织:将复杂的业务逻辑拆分为更小的函数,提高代码可读性
- 表单验证:添加更严格的表单验证,确保数据完整性
- 用户体验:添加加载状态和进度指示,提升用户体验
- 代码复用:提取重复的代码为公共函数或组件
10.2 后端优化
- 事务管理:确保
saveInboundPlan方法中的事务处理更加健壮 - 参数验证:增强请求参数的验证,确保数据完整性
- 错误处理:提供更详细的错误信息,便于前端处理
- 性能优化:对于频繁查询的数据,考虑添加缓存
- 日志记录:添加详细的日志记录,便于问题排查
11. 常见问题及解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 地址信息未选中 | 地址列表加载失败或用户未选择 | 刷新页面重新加载地址列表,确保选择一个有效的发货地址 |
| 编码获取失败 | 序列号生成服务异常 | 检查序列号服务状态,或联系管理员 |
| 提交失败 | 后端处理异常 | 检查网络连接,查看控制台错误信息,或联系管理员 |
| 商品数量超过库存 | 发货数量大于可用库存 | 减少发货数量,确保不超过可用库存 |
| SKU 验证失败 | SKU 不存在或无效 | 检查 SKU 是否正确,确保 SKU 在系统中存在 |
12. 结论
FBA 发货单创建功能是一个功能完整、设计合理的业务组件,为用户提供了直观、高效的发货计划创建界面。通过前端与后端的紧密协作,实现了从基本信息填写到商品管理再到计划提交的完整流程。
该功能的实现展示了现代前端开发的最佳实践,包括:
- 使用 Vue 3 Composition API 进行状态管理
- 与后端 API 的高效交互
- 响应式 UI 设计
- 良好的用户体验
- 模块化的代码组织
同时,后端实现也体现了 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 初始化流程
- 组件加载:
onMounted钩子函数触发loadData() - 加载计划信息:
loadData()调用shipmentplanApi.getPlanInfo()获取计划详情 - 初始化组件:
- 更新
plandata.value存储计划信息 - 设置
tableRef.value.planData和listRef.value.planData - 调用
summaryTotalPrice(item)计算入库费用 - 调用
tableRef.value.initData(item)初始化商品列表 - 设置
listRef.value.remark备注信息
- 更新
5.2 审核流程
- 打开审核对话框:点击"审核"按钮 → 触发
approvePlan() - 确认审核操作:弹出确认对话框,用户选择"通过"或"驳回"
- 执行审核操作:
- 选择"通过" → 调用
shipmentplanApi.approveInboundPlan() - 选择"驳回" → 调用
shipmentplanApi.rejectInboundPlan()
- 选择"通过" → 调用
- 更新状态:审核操作完成后,调用
loadData()重新加载计划信息,更新界面状态
5.3 货件审核流程
- 打开审核对话框:在审核对话框中,用户可以为每个货件选择"通过"或"驳回"
- 全选驳回:点击"全选驳回"复选框 → 触发
changeAllCancel()→ 所有货件自动选择"驳回" - 提交审核:点击"确认"按钮 → 触发
submitplan() - 执行货件操作:
- 对于选择"通过"的货件 → 调用
shipmentplanApi.createShipment() - 对于选择"驳回"的货件 → 调用
shipmentplanApi.cancelShipment()
- 对于选择"通过"的货件 → 调用
- 更新状态:操作完成后,关闭对话框并调用
loadData()重新加载计划信息
5.4 其他操作流程
- 拆分计划:点击"拆分"按钮 → 触发
splitHandel()→ 打开拆分对话框 - 关闭页面:点击"关闭"按钮 → 触发
closePage()→ 跳转到新发货单页面 - 复制计划:点击"复制"按钮 → 触发
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 前端优化
- 错误处理增强:添加更详细的错误处理和用户提示,特别是在 API 调用失败时
- 性能优化:对于大量货件的列表,考虑使用虚拟滚动
- 代码组织:将复杂的业务逻辑拆分为更小的函数,提高代码可读性
- 表单验证:添加更严格的表单验证,确保数据完整性
- 用户体验:添加加载状态和进度指示,提升用户体验
- 代码复用:提取重复的代码为公共函数或组件
- 内存管理:清理定时器,避免内存泄漏
9.2 后端优化
- 事务管理:确保所有数据库操作都在事务中执行,保证数据一致性
- 参数验证:增强请求参数的验证,确保数据完整性
- 错误处理:提供更详细的错误信息,便于前端处理
- 性能优化:对于频繁查询的数据,考虑添加缓存
- 日志记录:添加详细的日志记录,便于问题排查
- API 设计:统一 API 接口设计,使用 POST 方法处理包含参数的请求
10. 常见问题及解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 审核失败 | 后端 API 调用失败 | 检查网络连接,查看控制台错误信息,联系管理员 |
| 货件创建失败 | 货件信息不完整或格式错误 | 检查货件信息,确保所有必填字段都已填写 |
| 入库费用计算错误 | 商品尺寸或重量信息不正确 | 检查商品的尺寸和重量信息,确保数据准确 |
| 页面加载缓慢 | 计划信息过多或网络延迟 | 优化网络连接,考虑分页加载大量数据 |
| 拆分功能失败 | 商品列表为空或格式错误 | 确保商品列表不为空,检查商品数据格式 |
11. 结论
FBA 发货单审核功能是 FBA 发货流程中的重要环节,用于审核和管理发货计划。该功能通过前端组件与后端 API 的协作,实现了计划信息的加载、审核状态的管理、货件的创建和取消等核心功能。
该功能的实现展示了现代前端开发的最佳实践,包括:
- 使用 Vue 3 Composition API 进行状态管理
- 与后端 API 的高效交互
- 响应式 UI 设计
- 良好的用户体验
- 模块化的代码组织
同时,后端实现也体现了 Spring Boot 框架的优势,包括:
- 清晰的控制器设计
- 灵活的服务层架构
- 高效的数据处理
- 完善的事务管理
总体而言,FBA 发货单审核功能是一个设计精良、功能完善的业务组件,为 FBA 发货流程提供了重要的审核环节,帮助用户快速、准确地审核发货计划,提高了物流管理的效率。
发货-配货
配货页面 (one_pick.vue) 详细帮助文档
1. 页面概述
配货页面是 Wimoor ERP 系统中 FBA 发货流程的第一个步骤,主要负责展示和管理待发货的商品列表,允许用户进行配货操作、修改发货地址、查看商品详情等功能。
核心功能点:
- 展示货件计划的基本信息和商品列表
- 支持修改商品的配货数量和状态
- 支持修改发货地址
- 支持生成和打印配货单、标签等文档
- 支持完成配货操作,进入下一个发货步骤
技术栈:
- 前端:Vue 3 + Composition API + Element Plus
- 后端:Spring Boot + MyBatis Plus
- 数据可视化:ECharts
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 配货流程
-
初始化:页面加载时,通过 URL 参数获取计划 ID,调用
shipmentplanApi.getPlanInfo获取货件计划详情。 -
配货操作:
- 用户查看商品列表,确认配货数量
- 可以修改商品的配货数量和状态
- 可以修改发货地址
-
完成配货:
- 用户点击「完成配货」按钮
- 调用
shipmentplanApi.doneInboundPlan更新货件计划状态 - 状态更新成功后,跳转到下一个发货步骤(装箱)
7.2 地址修改流程
-
打开地址选择弹窗:用户点击「修改地址」按钮,打开地址选择弹窗。
-
获取地址列表:调用
addressApi.getAddress获取用户的地址列表。 -
选择地址:用户从列表中选择一个地址。
-
更新地址:调用
shipmentplanApi.changeAddress更新货件计划的发货地址。 -
刷新数据:地址更新成功后,刷新页面数据,显示新的发货地址。
7.3 商品编辑流程
-
打开编辑弹窗:用户点击商品行的「编辑」按钮,打开编辑弹窗。
-
修改信息:用户修改商品的配货数量、贴标信息等。
-
保存修改:用户点击「保存」按钮,调用
shipmentplanApi.updatePlanItem更新商品信息。 -
刷新数据:商品信息更新成功后,刷新页面数据,显示新的商品信息。
8. 技术要点
8.1 前端技术要点
-
Composition API:使用 Vue 3 的 Composition API 组织代码,提高代码可读性和可维护性。
-
响应式状态管理:使用
reactive和ref管理页面状态,确保数据变化能够及时反映到 UI 上。 -
组件通信:使用
ref和emit实现组件之间的通信,如头部组件和主页面之间的交互。 -
生命周期钩子:使用
onMounted钩子在页面加载时初始化数据。 -
异步操作处理:使用
async/await和 Promise 处理 API 调用等异步操作。
8.2 后端技术要点
-
RESTful API 设计:遵循 RESTful 设计风格,使用合适的 HTTP 方法和状态码。
-
事务管理:使用
@Transactional注解管理事务,确保数据操作的一致性。 -
用户认证:使用
UserInfoContext获取当前用户信息,实现权限控制。 -
数据校验:使用
@Valid和@NotNull等注解进行数据校验,确保数据的合法性。 -
异常处理:使用
try-catch捕获和处理异常,返回友好的错误信息。
9. 常见问题与解决方案
9.1 常见问题
| 问题描述 | 可能原因 | 解决方案 |
|---|---|---|
| 页面加载失败 | 计划 ID 不存在或无权限 | 检查 URL 参数是否正确,确认用户权限 |
| 配货数量修改失败 | 库存不足或参数错误 | 检查库存是否充足,确认参数格式正确 |
| 地址修改失败 | 地址 ID 不存在或无权限 | 检查地址是否存在,确认用户权限 |
| 文档下载失败 | 生成文档时出错 | 检查服务器状态,确认参数正确 |
9.2 性能优化建议
-
前端优化:
- 使用虚拟滚动处理大量商品数据
- 合理使用缓存,减少重复 API 调用
- 优化组件渲染,避免不必要的重渲染
-
后端优化:
- 使用索引优化数据库查询
- 合理使用缓存,减少数据库访问
- 优化 API 响应时间,提高并发处理能力
10. 代码优化建议
10.1 前端代码优化
-
代码结构优化:
- 将复杂的业务逻辑拆分为多个子函数,提高代码可读性
- 使用自定义 Hook 封装重复的逻辑,如 API 调用、数据处理等
-
性能优化:
- 使用
computed缓存计算结果,避免重复计算 - 使用
watchEffect替代watch,减少不必要的依赖追踪
- 使用
-
错误处理优化:
- 统一处理 API 错误,提供友好的错误提示
- 使用
try-catch捕获异步操作中的错误
10.2 后端代码优化
-
代码结构优化:
- 将业务逻辑从控制器中分离到服务层,提高代码可维护性
- 使用 DTO 封装请求和响应数据,避免直接使用实体类
-
性能优化:
- 使用批量操作减少数据库访问次数
- 合理使用索引,优化查询性能
-
安全性优化:
- 加强参数校验,防止 SQL 注入等攻击
- 使用 HTTPS 加密传输数据,提高安全性
11. 总结
配货页面是 Wimoor ERP 系统中 FBA 发货流程的重要组成部分,主要负责商品的配货操作和管理。通过本文档的详细解析,我们可以了解到:
-
页面结构:采用模块化设计,包含头部信息、地址管理、商品列表等核心模块。
-
功能实现:通过前端 API 调用和后端控制器的配合,实现了货件计划详情获取、配货信息提交、地址修改、商品编辑等核心功能。
-
数据模型:使用
ShipInboundPlan和ShipInboundItem等实体类,构建了完整的数据模型体系。 -
业务流程:清晰的配货流程,从初始化到完成配货,每一步都有明确的操作和状态管理。
-
技术要点:使用了 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 调用:
shipmenthandlingApi.requestInboundShipment():获取亚马逊货件状态shipmenthandlingApi.disableShipment():删除货件shipmenthandlingApi.localDoneShipment():标记货件为本地已完成
2.3.2 shipment_info 组件
功能:展示货件的详细信息 核心功能:
- 单据信息:展示货件的基本信息,如订单编码、货件名称、货件编码等
- 运输信息:展示货件的运输相关信息,如总货值、物流总费用、SKU个数等
- 地址信息:展示货件的发货地址和收货地址
- 箱标地址管理:添加和管理货件的箱标收货地址
关键 API 调用:
shipmentPlacementApi.getBaseInfo():获取货件的详细信息shipmentPlacementApi.saveDestinationBox():保存箱标收货地址
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 货件信息加载流程
- 页面加载:用户进入货件处理页面
- 获取货件 ID:从 URL 参数中获取货件 ID
- 加载货件信息:调用
shipmentPlacementApi.getBaseInfo()获取货件详细信息 - 渲染页面:使用获取到的信息渲染页面各个组件
- 计算费用:计算货件的总货值和物流费用
5.2 货件删除流程
- 用户点击删除按钮:触发删除货件操作
- 获取货件状态:调用
shipmenthandlingApi.requestInboundShipment()获取亚马逊后台货件状态 - 显示确认对话框:根据货件状态显示确认删除对话框
- 用户确认删除:用户选择删除方式(仅删除本地或同步删除亚马逊货件)
- 执行删除操作:调用
shipmenthandlingApi.disableShipment()执行删除操作 - 更新页面:删除完成后更新页面显示
5.3 箱标地址管理流程
- 用户点击添加箱标地址:触发添加箱标地址操作
- 打开地址选择对话框:显示地址选择对话框
- 用户选择地址:用户从地址列表中选择箱标收货地址
- 保存箱标地址:调用
shipmentPlacementApi.saveDestinationBox()保存箱标地址 - 更新页面:保存完成后更新页面显示
5.4 货件复制流程
- 用户点击复制按钮:触发复制货件操作
- 系统跳转:系统跳转到添加货件页面
- 创建新货件:基于原货件信息创建新的货件
5.5 本地完成流程
- 用户点击本地完成按钮:触发本地完成操作
- 显示确认对话框:显示确认本地完成的对话框
- 用户确认操作:用户确认执行本地完成操作
- 执行本地完成:调用
shipmenthandlingApi.localDoneShipment()执行本地完成操作 - 更新页面:操作完成后更新页面显示
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 前端优化
- 错误处理增强:添加更详细的错误处理和用户提示,特别是在 API 调用失败时
- 性能优化:对于大量数据的货件,考虑使用虚拟滚动技术,提高页面加载和滚动性能
- 代码组织:将复杂的业务逻辑拆分为更小的函数,提高代码可读性
- 用户体验:添加更多的加载状态和操作反馈,提升用户体验
- 代码复用:提取重复的代码为公共函数或组件
- 内存管理:清理定时器和事件监听器,避免内存泄漏
10.2 后端优化
- 性能优化:对于频繁查询的数据,考虑添加缓存机制,提高系统响应速度
- 错误处理:增强后端错误处理,提供更详细的错误信息,便于前端处理
- 事务管理:确保所有数据库操作都在事务中执行,保证数据一致性
- API 设计:统一 API 接口设计,使用 RESTful 风格的 API 设计
- 日志记录:添加详细的日志记录,便于问题排查
- 代码组织:优化代码结构,提高代码可读性和可维护性
11. 常见问题及解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 货件状态同步失败 | 网络连接问题或亚马逊 API 限制 | 检查网络连接,稍后重试操作 |
| 箱标地址保存失败 | 地址信息不完整或格式错误 | 检查地址信息,确保所有必填字段都已填写 |
| 货件删除失败 | 亚马逊后台货件状态不允许删除 | 先确认亚马逊后台货件状态,再执行删除操作 |
| 页面加载缓慢 | 货件信息过多或网络延迟 | 优化网络连接,考虑分页加载大量数据 |
| 费用计算错误 | 物流信息不完整或汇率更新不及时 | 检查物流信息,确保汇率数据已更新 |
12. 功能亮点
12.1 完整的货件信息展示
系统提供了全面的货件信息展示,包括基本信息、运输信息、地址信息等,使用户能够一目了然地了解货件的详细情况。信息展示采用卡片式布局,结构清晰,视觉效果良好。
12.2 便捷的货件操作管理
系统提供了丰富的货件操作功能,包括删除、复制、本地完成等,操作流程简单直观,用户可以轻松完成各种货件管理任务。
12.3 智能的状态同步
系统能够与亚马逊后台同步货件状态,确保货件状态的准确性和实时性。在删除货件时,系统会先获取亚马逊后台的货件状态,然后根据状态提供不同的删除选项。
12.4 灵活的箱标地址管理
系统支持为货件添加箱标收货地址,用户可以根据需要灵活管理货件的收货地址,提高了系统的灵活性和适用性。
12.5 准确的费用计算
系统能够自动计算货件的物流费用和货值,帮助用户了解货件的成本情况。费用计算基于货件的重量、体积和物流方式等因素,计算结果准确可靠。
13. 结论
发货-货件处理模块是 Wimoor 系统中 FBA 发货流程的核心环节,为用户提供了全面、便捷的货件管理功能。该模块通过前端与后端的紧密协作,实现了货件信息的展示、操作管理、状态同步等功能,为用户的 FBA 发货流程提供了有力的支持。
该模块的实现展示了现代前端开发和后端开发的最佳实践,包括:
- 使用 Vue 3 Composition API 进行状态管理
- 与后端 API 的高效交互
- 响应式 UI 设计
- 良好的用户体验
- 模块化的代码组织
- 清晰的控制器设计
- 灵活的服务层架构
- 高效的数据处理
总体而言,发货-货件处理模块是一个设计精良、功能完善的业务组件,为 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)
-
主要功能:
- 提供货件状态标签栏,支持按状态筛选货件
- 集成筛选和搜索功能
- 管理表格组件和头部组件的交互
-
关键代码分析:
- 状态标签切换逻辑:通过
activeName变量控制当前选中的状态标签,根据不同标签设置不同的orderStatus值 - 数据传递:通过
getdata方法接收头部组件的筛选条件,通过tableRef.value.getshipmentData(obj)传递给表格组件 - 标签点击事件:通过
handleClick方法处理标签切换事件,更新筛选条件并重新加载数据
- 状态标签切换逻辑:通过
// 状态标签切换逻辑
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)
-
主要功能:
- 展示货件详细信息,包括货件编码、店铺、配送中心、创建日期等
- 提供物流信息查看、跟踪发货、异常处理等操作
- 支持表格排序和筛选
-
关键代码分析:
- 数据加载:通过
loadtableData方法加载货件数据,构建请求参数并调用后端 API - 物流信息查看:通过
showTransInfoDailog方法打开物流信息弹窗,传递货件相关参数 - 跟踪发货:通过
shipmentfollow方法跳转到货件跟踪页面 - 异常处理:通过
refreshShipmentDialog和ignoreShipmentWarn方法处理异常货件
- 数据加载:通过
// 加载货件数据
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)
-
主要功能:
- 支持多种物流系统(ZH和ZM)
- 展示货件基本信息、收件人和发件人地址
- 以时间线形式展示物流轨迹
- 展示货箱详情表格
-
关键代码分析:
- 物流数据加载:通过
loadTransDetialInfo方法加载物流详细信息,根据物流系统类型调用不同的处理方法 - ZH物流系统处理:通过
loadZhApiDetail方法处理ZH物流系统的数据 - ZM物流系统处理:通过
loadZmApiDetail方法处理ZM物流系统的数据
- 物流数据加载:通过
// 加载物流详细信息
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
shipmenthandlingApi.getshipList(params):获取货件列表数据- 参数:筛选条件,包括店铺、国家、仓库、日期、状态等
- 返回值:货件列表数据,包括货件编码、状态、物流信息等
2.2.2 物流信息 API
transportationApi.shipTransDetial(params):获取物流详细信息- 参数:公司ID、货件ID、订单号
- 返回值:物流详细信息,包括服务类型、运费、轨迹等
2.2.3 货件操作 API
-
shipmentApi.refreshShipmentRec(params):刷新货件接收状态- 参数:货件ID
- 返回值:同步结果
-
shipmentApi.ignoreShipment(params):忽略货件异常- 参数:货件ID
- 返回值:忽略结果
2.3 前端状态管理
- 组件状态:使用 Vue 3 的
ref和reactive管理组件内部状态 - 数据传递:通过 props 和 emit 实现组件间数据传递
- 路由跳转:使用 Vue Router 实现页面跳转
3. 后端代码分析
3.1 核心实体
3.1.1 货件实体 (Shipment.java)
-
主要属性:
- 买家ID、货件名称、目的地
- 重量、体积、状态
- 货箱列表、商品列表
-
关联关系:
- 一对多:一个货件包含多个货箱
- 一对多:一个货件包含多个商品
3.2 核心服务
3.2.1 货件服务
- 主要功能:
- 货件数据的增删改查
- 货件状态的管理和更新
- 货件与物流系统的交互
3.2.2 物流服务
- 主要功能:
- 与外部物流系统的API交互
- 物流信息的获取和解析
- 物流轨迹的处理和存储
3.3 后端 API 接口
3.3.1 货件列表接口
- 路径:
/api/erp/ship/shipmenthandling/getshipList - 方法:GET
- 参数:筛选条件,包括店铺、国家、仓库、日期、状态等
- 返回值:货件列表数据,包括货件编码、状态、物流信息等
3.3.2 物流详细信息接口
- 路径:
/api/erp/ship/transportation/shipTransDetial - 方法:GET
- 参数:公司ID、货件ID、订单号
- 返回值:物流详细信息,包括服务类型、运费、轨迹等
3.3.3 货件状态同步接口
- 路径:
/api/erp/ship/shipment/refreshShipmentRec - 方法:GET
- 参数:货件ID
- 返回值:同步结果
4. 物流系统集成
4.1 支持的物流系统
-
ZH物流系统:
- 提供详细的物流信息,包括服务类型、运费、状态等
- 支持货箱详情的查看
- 以时间线形式展示完整的物流轨迹
-
ZM物流系统:
- 提供运单号码和物流轨迹
- 以时间线形式展示物流状态变更
4.2 物流系统 API 集成
4.2.1 API 调用流程
- 前端调用后端物流信息接口
- 后端根据物流系统类型调用相应的外部 API
- 外部物流系统返回物流信息
- 后端解析和处理物流信息
- 后端返回处理后的物流信息给前端
- 前端以统一的格式展示物流信息
4.2.2 数据格式处理
-
ZH物流系统数据格式:
{ "service_name": "快递服务", "shipment_id": "123456", "client_reference": "REF789", "charge_amount": "100.00", "status": "运输中", "parcel_count": "2", "sumqty": "10", "sumprice": "500.00", "to_address": { "name": "收件人", "address_1": "地址1", "city_1": "城市", "postcode": "100000", "country": "CN" }, "from_address": { "name": "发件人", "address_1": "地址1", "city_1": "城市", "postcode": "200000", "country": "CN" }, "traces": [ { "time": 1674567890, "info": "货件已发出" }, { "time": 1674567900, "info": "货件在运输中" } ], "parcels": [ { "number": "P123", "ext_number": "EP123", "client_weight": "5.0", "client_length": "30", "client_width": "20", "client_height": "10", "carrier_code": "SF", "tracking_number": "SF1234567890", "chargeable_weight": "5.0", "actual_weight": "4.8", "chargeable_length": "30", "chargeable_width": "20", "chargeable_height": "10", "picking_time": 1674567890, "declarations": [ { "sku": "SKU123", "name_zh": "商品名称", "name_en": "Product Name", "unit_value": "50.00", "qty": "2", "material": "材质", "usage": "用途", "brand": "品牌", "size": "型号", "sale_url": "https://example.com", "weight": "1.0", "hscode": "12345678" } ] } ] } -
ZM物流系统数据格式:
{ "jobno": "ZM1234567890", "podInfoDTOList": [ { "scanTime": "2026-01-23 10:00:00", "remark": "货件已发出" }, { "scanTime": "2026-01-23 14:00:00", "remark": "货件在运输中" } ] }
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 前端技术亮点
- Vue 3 Composition API:使用 Vue 3 的 Composition API 进行状态管理和逻辑组织,代码结构清晰
- Element Plus:使用 Element Plus 组件库构建界面,样式统一美观
- 响应式设计:页面布局响应式,适配不同屏幕尺寸
- 组件化开发:将页面拆分为多个组件,提高代码复用性和可维护性
- 时间线展示:使用 Element Plus 的时间线组件展示物流轨迹,清晰直观
- 表格功能:使用自定义表格组件,支持排序、筛选、分页等功能
6.2 后端技术亮点
- Spring Boot:使用 Spring Boot 框架,简化后端开发
- MyBatis Plus:使用 MyBatis Plus 进行数据库操作,提高开发效率
- RESTful API:设计 RESTful API 接口,规范后端接口设计
- 多物流系统集成:支持多种物流系统的 API 集成,统一数据格式
- 数据缓存:使用缓存技术提高系统响应速度
- 异常处理:完善的异常处理机制,提高系统稳定性
6.3 系统集成亮点
- 多物流系统支持:集成了 ZH 和 ZM 等多种物流系统,统一展示物流信息
- 无缝切换:在不同物流系统间无缝切换查看物流信息
- 适配不同 API:自动适配不同物流系统的 API 格式和数据结构
- 实时数据:实时获取和展示物流信息,确保数据准确性
- 统一界面:不同物流系统的信息以统一的界面展示,提高用户体验
7. 代码优化建议
7.1 前端优化建议
-
性能优化:
- 使用虚拟滚动技术处理大量货件数据,提高页面加载和滚动性能
- 实现数据缓存,减少重复 API 调用
- 优化组件渲染,避免不必要的重渲染
-
代码组织:
- 将复杂的业务逻辑拆分为更小的函数,提高代码可读性
- 使用 Pinia 或 Vuex 进行状态管理,简化组件间数据传递
- 提取重复的代码为公共函数或组件
-
用户体验:
- 添加更多的加载状态和操作反馈,提升用户体验
- 实现物流状态变更的实时通知
- 优化表单验证和错误提示
7.2 后端优化建议
-
性能优化:
- 使用缓存机制缓存频繁查询的物流信息,提高系统响应速度
- 优化数据库查询,使用索引提高查询效率
- 实现异步处理,提高系统并发能力
-
代码组织:
- 优化代码结构,提高代码可读性和可维护性
- 使用设计模式,如策略模式处理不同物流系统的 API 调用
- 提取重复的代码为公共服务或工具类
-
系统稳定性:
- 增强错误处理,提供更详细的错误信息
- 实现熔断机制,避免物流系统 API 故障影响整个系统
- 添加日志记录,便于问题排查
7.3 架构优化建议
-
微服务架构:
- 考虑将物流服务拆分为独立的微服务,提高系统的可扩展性和可维护性
- 使用服务发现和负载均衡,提高系统的可靠性
-
API 网关:
- 引入 API 网关,统一管理和保护后端 API
- 实现请求限流和熔断,提高系统的稳定性
-
数据存储:
- 考虑使用 NoSQL 数据库存储物流轨迹等半结构化数据
- 实现数据分片,提高数据库的处理能力
8. 功能扩展建议
8.1 功能扩展
-
移动端支持:
- 开发移动端应用或响应式网页,支持在手机上查看物流信息
- 实现物流状态变更的推送通知
-
更多物流系统集成:
- 集成更多的物流系统,如 FedEx、UPS、DHL 等
- 提供物流系统 API 配置的可视化界面
-
数据分析功能:
- 实现物流数据的统计和分析,如运输时间、异常率等
- 提供数据可视化图表,帮助用户分析物流性能
-
智能预警:
- 基于历史数据和规则,实现物流异常的智能预警
- 提供预警通知和处理建议
-
多语言支持:
- 实现多语言界面,支持国际化业务需求
- 支持不同国家和地区的物流规则和格式
8.2 技术扩展
-
使用 WebSocket:
- 实现实时物流状态更新,无需手动刷新页面
- 提供更及时的物流轨迹变更通知
-
使用 AI 技术:
- 利用 AI 技术分析物流数据,预测可能的延误和异常
- 提供智能的物流路径优化建议
-
区块链技术:
- 考虑使用区块链技术存储物流信息,提高数据的安全性和可追溯性
- 实现物流过程的透明化和不可篡改
9. 总结
货件跟踪模块是 Wimoor 系统中一个功能完善、技术先进的模块,通过前后端的紧密配合,为用户提供了全面、实时的货件跟踪服务。该模块具有以下特点:
- 功能全面:支持货件状态管理、物流信息查看、物流轨迹跟踪、多物流系统集成等功能
- 技术先进:采用 Vue 3 + Element Plus 前端技术和 Java + Spring Boot 后端技术
- 用户友好:界面设计简洁直观,操作流程优化,物流信息展示清晰
- 系统稳定:完善的错误处理和异常管理机制,确保系统稳定运行
- 扩展性强:模块化设计,易于功能扩展和技术升级
通过不断优化和升级,货件跟踪模块将为用户提供更优质的 FBA 货件管理体验,帮助用户更高效地管理和跟踪货件的整个生命周期。
FBA每日库存
FBA每日库存模块功能解析文档
1. 系统架构
1.1 整体架构
FBA每日库存模块采用前后端分离架构,主要包含以下组件:
- 前端组件:Vue 3 + Element Plus 构建的单页应用
- 后端服务:Spring Boot 微服务,提供 RESTful API
- 数据库:MySQL 数据库存储库存数据
- 数据来源:亚马逊API同步的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 数据流程
- 数据同步:定时从亚马逊API获取FBA库存报告
- 数据处理:解析报告,生成每日库存快照
- 数据存储:将快照数据写入inventory_report_his表
- 数据查询:前端请求时,动态生成SQL查询数据
- 数据展示:前端根据返回结果动态渲染表格
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 动态日期列生成
- 前端实现:通过v-for指令动态渲染日期列
- 后端实现:根据日期范围生成字段列表,动态构建SQL查询
- 技术优势:灵活适应不同日期范围查询,减少前端代码冗余
6.2 高性能数据处理
- 虚拟滚动:前端使用GlobalTable组件的虚拟滚动功能,支持处理大量数据
- 动态SQL优化:后端使用CASE WHEN语句动态生成列,减少数据库压力
- SXSSFWorkbook:使用SXSSFWorkbook处理大数据量Excel导出,避免内存溢出
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 合计行实现
- 前端实现:使用Element Plus的summary-method属性自定义合计逻辑
- 后端实现:单独查询合计数据,添加到第一条记录的summary字段中
- 技术优势:减少前端计算压力,提高响应速度
7. 性能优化
7.1 前端优化
- 虚拟滚动:避免一次性渲染大量数据,提高表格渲染速度
- 懒加载:按需加载数据,减少初始加载时间
- 防抖处理:搜索输入添加防抖,减少频繁请求
7.2 后端优化
- 索引优化:在inventory_report_his表的byday、sku、warehouse字段上建立联合索引
- 分页查询:使用MyBatis Plus的分页功能,避免全表扫描
- 动态SQL:根据查询条件动态生成SQL,减少不必要的字段查询
- 连接池优化:配置合适的数据库连接池参数,提高并发处理能力
7.3 数据库优化
- 分区表:对inventory_report_his表按日期进行分区,提高查询效率
- 定期归档:对历史数据进行归档,减少单表数据量
- 预计算:定时预计算常用日期范围的合计数据,提高查询速度
8. 最佳实践
8.1 代码规范
- 前端代码:遵循Vue 3 Composition API规范,组件化开发
- 后端代码:遵循Spring Boot最佳实践,分层架构清晰
- SQL代码:使用MyBatis动态SQL,避免硬编码
8.2 安全考虑
- 接口认证:所有API接口都需要进行身份认证和权限校验
- 参数校验:对所有输入参数进行严格校验,防止SQL注入
- 数据加密:敏感数据在传输和存储过程中进行加密处理
8.3 测试建议
- 单元测试:对核心业务逻辑进行单元测试
- 集成测试:测试前后端集成和API调用
- 性能测试:模拟大量数据场景,测试系统性能
- 兼容性测试:测试不同浏览器和设备的兼容性
9. 扩展建议
9.1 功能扩展
- 库存趋势图:添加库存变化趋势图表,直观展示库存变化
- 库存预警:根据历史数据设置库存预警阈值,及时提醒
- 多维度分析:支持按产品类别、品牌等维度进行库存分析
- 导出模板定制:支持自定义导出模板,满足不同需求
9.2 技术扩展
- 缓存机制:添加Redis缓存,提高查询速度
- 异步处理:使用消息队列处理大数据量导出请求
- 实时数据:添加WebSocket支持,实现实时库存更新
- 数据分析:集成数据分析工具,提供更深入的库存分析
10. 总结
FBA每日库存模块是Wimoor系统中重要的库存管理功能,采用了前后端分离架构,具有高性能、高扩展性的特点。通过动态日期列生成、多维度筛选和数据导出等功能,帮助卖家实时掌握库存动态,优化库存管理策略。
该模块的设计和实现遵循了现代软件 engineering 最佳实践,具有良好的可维护性和可扩展性。在未来的发展中,可以进一步扩展功能,提高系统性能,为卖家提供更全面、更深入的库存管理服务。
文档版本:v1.0 更新时间:2026-01-26 适用系统:Wimoor 6.0及以上版本
发货-发货详情(新)
发货详情报表模块(ShipV2)功能解析
1. 模块架构
1.1 前端架构
- 核心文件:
wimoor-ui/src/views/amazon/report/shipv2/detail/index.vue - 技术栈:Vue 3、Element Plus、Vite、Axios
- 状态管理:Vue 3 Composition API(reactive、ref)
- API接口:
wimoor-ui/src/api/amazon/inbound/reportV2Api.js
1.2 后端架构
- 控制器:
ShipInboundReportV2Controller.java - 服务层:
IShipInboundPlanService.java、ShipInboundPlanServiceImpl.java - 数据访问:
ShipInboundPlanV2Mapper.java、ShipInboundPlanV2Mapper.xml - 技术栈:Spring Boot、MyBatis Plus、MySQL
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 前端交互逻辑
- 视图切换:通过
selecttype状态和标签页切换,调用不同的数据加载方法 - 筛选条件:所有筛选条件变更都会触发
handleQuery方法重新加载数据 - 分页:分页操作通过
handleSizeChange和handleCurrentChange方法处理 - 导出:根据不同的导出类型调用不同的API接口
- 数据统计: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 双视图模式
- 货件汇总视图:以货件为单位,展示货件的整体情况
- SKU汇总视图:以SKU为单位,展示每个SKU的发货和到货情况
4.2 多维度筛选
| 筛选条件 | 说明 | 数据类型 |
|---|---|---|
| 店铺 | 选择特定店铺 | 下拉选择 |
| 日期类型 | 创建日期/发货日期 | 下拉选择 |
| 日期范围 | 选择开始和结束日期 | 日期选择器 |
| 仓库 | 选择特定仓库 | 下拉选择 |
| 货件编码 | 搜索特定货件 | 文本输入 |
| 接收异常 | 筛选有/无接收异常的货件 | 下拉选择 |
| 承运商 | 选择特定承运商 | 下拉选择 |
4.3 数据导出
| 导出类型 | 说明 | 适用场景 |
|---|---|---|
| 普通导出 | 完整的货件或SKU数据 | 全面分析 |
| 含子SKU | 包含子SKU的详细数据 | 变体产品分析 |
| 发货数量简约版 | 只包含发货数量的简化数据 | 快速统计 |
| 发货处理任务量 | 发货处理任务量统计 | 任务分配 |
4.4 数据统计
- SKU汇总视图:自动计算总发货数量和总接收数量
- 货件汇总视图:自动汇总物流费用、运输重量等
5. 技术亮点
5.1 前端技术亮点
- Vue 3 Composition API:使用reactive和ref进行状态管理,代码结构更清晰
- Element Plus组件:使用el-tabs、el-select、el-table等组件,界面美观且功能丰富
- 响应式设计:适配不同屏幕尺寸,操作流畅
- 异步数据加载:使用Axios进行异步请求,配合loading状态提升用户体验
- 条件渲染:根据不同视图类型和筛选条件动态渲染UI
5.2 后端技术亮点
- RESTful API设计:使用POST请求传递复杂参数,接口设计规范
- SXSSFWorkbook:使用SXSSFWorkbook处理大数据量Excel导出,避免内存溢出
- 参数校验:对输入参数进行非空检查和格式转换
- 多表关联查询:使用SQL多表关联查询,提高数据查询效率
- 动态SQL:使用MyBatis Plus的动态SQL,根据条件构建不同的查询语句
5.3 性能优化
- 分页查询:使用MyBatis Plus的分页功能,避免一次性加载过多数据
- 索引优化:数据库表建立适当索引,提高查询速度
- 缓存机制:使用Spring Cache缓存常用数据
- 批量操作:批量处理数据,减少数据库交互次数
- 异步处理:Excel导出等耗时操作使用异步处理
6. 代码优化建议
6.1 前端优化建议
- 代码模块化:将筛选条件、表格渲染等功能拆分为独立组件
- 状态管理:考虑使用Pinia或Vuex进行更复杂的状态管理
- 防抖处理:对搜索输入添加防抖处理,减少频繁请求
- 虚拟滚动:对大数据量表格使用虚拟滚动,提高渲染性能
- 错误处理:添加更完善的错误处理和用户提示
6.2 后端优化建议
- 参数验证:使用Spring Validation对请求参数进行更严格的验证
- 异常处理:统一异常处理,返回更友好的错误信息
- 日志记录:添加更详细的日志记录,便于问题排查
- 性能监控:集成Spring Boot Actuator进行性能监控
- SQL优化:进一步优化SQL查询,减少表关联次数
6.3 架构优化建议
- 微服务拆分:考虑将报表功能拆分为独立的微服务
- 缓存策略:使用Redis缓存热点数据,提高响应速度
- 消息队列:使用消息队列处理异步任务,提高系统可靠性
- API网关:使用API网关统一管理API接口
- 容器化部署:使用Docker容器化部署,提高部署效率
7. 业务价值
7.1 运营价值
- 数据可视化:通过报表直观展示发货数据,便于运营分析
- 异常监控:及时发现和处理接收异常,减少损失
- 趋势分析:分析发货趋势,优化库存管理
- 成本控制:监控物流费用,优化物流方案
7.2 管理价值
- 决策支持:基于数据做出更合理的发货决策
- 流程优化:发现发货流程中的瓶颈,优化流程
- 绩效考核:基于发货数据进行绩效考核
- 风险控制:及时发现和应对发货风险
7.3 技术价值
- 可扩展性:模块化设计,便于后续功能扩展
- 可维护性:代码结构清晰,易于维护
- 可复用性:封装通用功能,提高代码复用率
- 技术栈更新:使用Vue 3、Spring Boot等最新技术,保持技术先进性
8. 总结
发货详情报表模块(ShipV2)是一个功能完善、技术先进的FBA发货数据分析工具,通过前端Vue 3和后端Spring Boot的完美结合,实现了货件和SKU两个维度的数据分析和管理。该模块不仅提供了丰富的筛选和导出功能,还通过多表关联查询和数据统计,为用户提供了全面、准确的发货数据,帮助用户更好地管理FBA发货流程,优化物流方案,降低运营成本。
未来,该模块可以进一步扩展,例如添加更多的数据分析维度、集成更多的物流商数据、提供更丰富的图表展示等,以满足用户日益增长的需求。
发货-发货统计(新)
发货统计模块功能解析文档
1. 系统架构
1.1 整体架构
发货统计模块采用前后端分离架构,主要包含以下组件:
- 前端组件:Vue 3 + Element Plus构建的单页应用
- 后端服务:Spring Boot微服务,提供RESTful API
- 数据库:MySQL数据库存储发货相关数据
- 外部依赖:亚马逊API用于获取和同步FBA货件信息
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' }
]
});
核心方法:
- handleQuery() - 处理查询请求
function handleQuery() {
if (state.queryParam.groupby && state.queryParam.groupby.length > 0) {
globalTable.value.loadTable(state.queryParam);
}
}
- 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;
});
}
- downloadList() - 导出数据
function downloadList() {
state.downLoading = true;
reportApi.downExcelShipmentReportByLoistics(state.queryParam, () => {
state.downLoading = false;
});
}
- getSummaries() - 计算合计行
function getSummaries({ columns, data }) {
var arr = ["合计"];
columns.forEach((item, index) => {
if (index >= 2) {
arr[index] = state.summary[item.property];
}
});
return arr;
}
- 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版本API:wimoor-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接口
核心接口:
- 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);
}
- 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);
}
- 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 数据计算逻辑
发货指标计算:
- 计划发货(totalqty):SUM(产品数量)
- 实际发货(totalout):SUM(状态为已发货的货件的发货数量)
- 实际接收(totalrec):SUM(状态为已发货的货件的接收数量)
- 接收差值(lessrec):SUM(实际发货 - 实际接收)
- 待发货(needout):SUM(计划发货 - 实际发货)
- 待接收(needrec):SUM(实际发货 - 实际接收)
运输指标计算:
- 预估运输重量(readweight):SUM(产品重量 × 发货数量)
- 发货运输重量(transweight_kg):SUM(计费重量,单位kg)
- 发货运输体积(transweight_cbm):SUM(计费体积,单位cbm)
- 运输费用(shipfee):SUM(单价 × 重量 + 其他费用)
- 平均时效(avgtime):AVG(接收日期 - 发货日期)
7. 技术亮点
7.1 前端技术亮点
- 动态分组:通过el-checkbox-group动态控制分组维度,表格列根据分组条件动态显示
- 饼图可视化:使用PieChart组件展示数据分布,支持切换不同的汇总指标
- 合计行:使用el-table的show-summary和summary-method实现自动计算合计行
- 条件渲染:使用v-if指令根据分组条件动态渲染表格列
- 响应式设计:使用el-space布局组件,支持不同屏幕尺寸
7.2 后端技术亮点
- 动态SQL:使用MyBatis的动态SQL,根据分组条件动态构建查询语句
- 多表关联:使用LEFT JOIN关联多个表,实现复杂的数据统计
- 聚合计算:使用SUM、AVG等聚合函数进行数据统计
- SXSSFWorkbook:使用Apache POI的SXSSFWorkbook流式处理大数据量Excel导出
- 参数校验:对输入参数进行非空检查和格式转换
7.3 性能优化
- 索引优化:在关键字段上建立索引,提高查询速度
- 分页查询:使用分页功能避免一次性加载大量数据
- 缓存机制:缓存常用的查询结果
- 懒加载:饼图数据使用单独的请求加载
8. 扩展功能
8.1 分组维度扩展
当前支持7种分组维度,可扩展支持更多维度:
- 按月/周分组
- 按产品类别分组
- 按运输方式分组
8.2 统计指标扩展
当前支持15种统计指标,可扩展支持更多指标:
- 利润率
- 退货率
- 库存周转率
8.3 图表类型扩展
当前使用饼图展示数据分布,可扩展支持:
- 折线图(趋势分析)
- 柱状图(对比分析)
- 地图(地理分布)
9. 注意事项
9.1 数据同步
- 货件数据通过亚马逊API同步,可能存在延迟
- 建议在发货后24小时再查看统计数据
- 接收数据可能需要更长时间才能同步完成
9.2 权限控制
- 用户只能查看有权限的店铺和仓库数据
- 权限由系统管理员设置
9.3 性能考虑
- 避免选择过大的日期范围
- SKU分组会显著增加数据量
- 大量数据导出可能需要较长时间
文档版本: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 核心逻辑
-
数据加载逻辑:
- 通过
loadTableData函数根据当前选中的标签页加载对应的数据 - 当标签页切换时,调用
handleQuery函数重新加载数据 - 当筛选条件变化时,调用
handleQuery函数重新加载数据
- 通过
-
API调用:
getShipFeeReport:获取SKU头程费用报告getShipFeeDetailReport:获取SKU头程费用明细报告downShipFeeReportExcel:导出SKU头程费用报告downShipFeeDetailReportExcel:导出SKU头程费用明细报告
-
详情查看逻辑:
- 点击"历史详情"按钮时,调用
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 核心逻辑
-
数据加载逻辑:
- 通过
loadTableData函数加载SKU历史费用数据 - 调用
getShipSkuFeeReportAPI获取数据
- 通过
-
图表渲染逻辑:
- 使用 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 核心逻辑
-
参数处理:
- 从前端请求中获取筛选参数
- 处理参数默认值,如日期范围默认为最近7天
- 将参数传递给服务层方法
-
响应处理:
- 将服务层返回的数据包装为
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 核心逻辑
-
费用分摊计算:
- 在
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); }
- 在
-
图表数据处理:
- 在
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); }
- 在
-
Excel导出:
- 使用
SXSSFWorkbook生成Excel文件 - 设置表头和数据行
- 计算并填充SKU单件费用
- 使用
3.3 Mapper接口 (ErpDispatchOverseaTransMapper.java)
3.3.1 核心方法
| 方法名 | 功能描述 |
|---|---|
transFeeShared |
查询SKU头程费用汇总数据 |
transFeeSharedDetail |
查询SKU头程费用明细数据 |
transFeeSharedWeek |
查询SKU每周费用分摊数据 |
4. 数据流分析
4.1 SKU头程费用报告数据流
- 前端请求:用户在"SKU头程"标签页设置筛选条件并点击搜索
- API调用:前端调用
getShipFeeReportAPI - 后端处理:
- 控制器接收请求并处理参数
- 调用服务层
getShipFeeReport方法 - 服务层调用Mapper
transFeeShared方法查询数据库 - 数据库返回查询结果
- 服务层将结果返回给控制器
- 控制器将结果包装为
Result对象返回
- 前端渲染:前端接收数据并渲染到表格中
4.2 SKU头程费用明细报告数据流
- 前端请求:用户在"SKU头程明细"标签页设置筛选条件并点击搜索
- API调用:前端调用
getShipFeeDetailReportAPI - 后端处理:
- 控制器接收请求并处理参数
- 调用服务层
getShipFeeDetailReport方法 - 服务层调用Mapper
transFeeSharedDetail方法查询数据库 - 服务层计算每个SKU的单件费用
- 服务层将结果返回给控制器
- 控制器将结果包装为
Result对象返回
- 前端渲染:前端接收数据并渲染到表格中
4.3 SKU历史费用报告数据流
- 前端请求:用户点击"历史详情"按钮
- API调用:前端调用
getShipSkuFeeReportAPI - 后端处理:
- 控制器接收请求并处理参数
- 调用服务层
transSKUFeeShared方法 - 服务层调用Mapper
transFeeSharedWeek方法查询数据库 - 服务层处理数据并生成图表数据
- 服务层将结果返回给控制器
- 控制器将结果包装为
Result对象返回
- 前端渲染:
- 前端接收数据并渲染到表格中
- 前端使用ECharts渲染费用趋势图表
4.4 Excel导出数据流
- 前端请求:用户点击"导出"按钮
- API调用:前端调用对应的导出API
- 后端处理:
- 控制器接收请求并处理参数
- 创建
SXSSFWorkbook对象 - 调用服务层对应的导出方法
- 服务层调用Mapper查询数据
- 服务层将数据写入Excel文件
- 控制器设置响应头并将Excel文件写入响应流
- 前端处理:前端接收Excel文件并自动下载
5. 数据库操作分析
5.1 核心查询
-
SKU头程费用汇总查询:
- 表:涉及发货单、SKU、费用等相关表
- 关键字段:SKU编码、发货数量、费用金额
- 聚合函数:SUM(计算总发货数量和总费用)
- 分组:按SKU编码分组
-
SKU头程费用明细查询:
- 表:涉及发货单、SKU、费用等相关表
- 关键字段:发货单编码、SKU编码、发货数量、费用金额
- 关联:多表关联查询
-
SKU每周费用分摊查询:
- 表:涉及发货单、SKU、费用等相关表
- 关键字段:SKU编码、发货日期、发货数量、费用金额
- 分组:按SKU编码和周分组
- 聚合函数:SUM(计算每周发货数量和费用)
6. 技术亮点
6.1 前端技术亮点
- 双标签页设计:使用Element Plus的
el-tabs组件实现双标签页切换,提高用户体验 - 响应式布局:使用Element Plus的响应式组件构建界面,适配不同屏幕尺寸
- 数据可视化:集成ECharts实现费用趋势的图表展示,直观呈现数据变化
- 异步数据加载:使用Vue 3的Composition API实现异步数据加载,提高页面响应速度
- 模块化设计:将主页面和详情对话框分离为独立组件,提高代码可维护性
6.2 后端技术亮点
- 分层架构:采用控制器→服务→Mapper的分层架构,职责清晰
- Excel导出优化:使用
SXSSFWorkbook生成Excel文件,支持大数据量导出 - 图表数据处理:在后端预处理图表数据,减轻前端负担
- 参数处理:统一处理参数默认值和边界情况,提高代码健壮性
- 费用计算逻辑:实现准确的费用分摊计算,确保数据准确性
7. 代码优化建议
7.1 前端优化建议
-
性能优化:
- 实现表格数据虚拟滚动,提高大数据量下的渲染性能
- 对频繁调用的API实现缓存,减少重复请求
-
用户体验优化:
- 添加加载状态提示,提高用户体验
- 实现筛选条件的记忆功能,下次进入页面时自动恢复上次的筛选条件
-
代码质量优化:
- 提取重复的代码为公共函数,减少代码冗余
- 完善错误处理逻辑,提高代码健壮性
7.2 后端优化建议
-
性能优化:
- 优化数据库查询,添加适当的索引
- 实现查询结果缓存,减少数据库压力
-
代码质量优化:
- 提取重复的参数处理逻辑为公共方法
- 使用枚举或常量定义固定值,提高代码可维护性
- 完善异常处理,提供更详细的错误信息
-
功能优化:
- 增加费用分摊规则的可配置性,支持不同场景的分摊需求
- 增加费用分析报表,提供更丰富的数据洞察
8. 总结
发货费用分摊模块是一个功能完整、设计合理的企业级应用模块,主要实现了以下功能:
- 费用分摊计算:将发货总费用根据发货数量等因素分摊到各个SKU
- 多维度数据展示:支持SKU级别的费用汇总和发货单级别的费用明细
- 丰富的筛选条件:支持按仓库、日期、承运商、物流渠道等条件筛选
- 数据可视化:使用ECharts展示费用趋势图表,直观呈现数据变化
- Excel导出:支持将数据导出为Excel文件,方便离线分析
该模块通过前后端分离的架构设计,实现了从数据存储、业务逻辑到前端展示的完整流程,为用户提供了清晰、直观的费用分摊分析工具,帮助用户更好地了解和管理发货费用。
销售-商品分析
销售商品分析模块功能解析文档
1. 系统架构
1.1 整体架构
销售商品分析模块采用前后端分离架构,主要包含以下组件:
- 前端组件:Vue 3 + Element Plus 构建的单页应用
- 后端服务:Spring Boot 微服务,提供 RESTful API
- 数据库:MySQL 数据库存储商品相关数据
- 外部依赖:亚马逊API用于获取和同步商品销售、排名、流量和广告数据
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
主要功能:
- 提供店铺分组选择和商品搜索功能
- 展示SKU列表,支持分页和无限滚动加载
- 管理商品选择状态,传递选中商品信息给详情组件
核心代码结构:
<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> 备注
</el-link>
<el-link title="跳转亚马逊" class="flex-center" target="_blank" :href="infoMap.link" :underline="false">
<el-icon class="font-medium"><Link/></el-icon> 跳转
</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
主要功能:
- 处理商品分析相关的HTTP请求
- 调用相应的服务方法处理业务逻辑
- 返回统一的响应格式
核心代码:
@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 商品列表查询
功能描述:根据店铺分组、市场和搜索条件查询商品列表
实现流程:
- 前端调用
productAnysApi.productAsinList()方法 - 后端
ProductAnalysisController.productListAction()处理请求 - 构建查询参数,包括搜索类型、搜索关键词、店铺分组、市场等
- 调用
iProductInfoService.getAsinList()获取商品列表 - 返回分页后的商品列表数据
4.2 商品详情查询
功能描述:根据商品ID查询商品详细信息
实现流程:
- 前端调用
productAnysApi.productdetail()方法 - 后端
ProductAnalysisController.productListAction()处理请求 - 调用
iProductInOrderService.selectDetialById()获取商品详情 - 返回商品的基本信息、图片、ASIN等数据
4.3 图表数据获取
功能描述:根据商品SKU、市场、时间范围和指标类型获取图表数据
实现流程:
- 前端调用
productAnysApi.getChartData()方法 - 后端
ProductAnalysisController.getChartDataAction()处理请求 - 构建查询参数,解析指标类型
- 调用
iProductInOrderService.getChartData()获取图表数据 - 根据指标类型获取不同的数据:
- 销量数据:从订单汇总服务获取
- 排名数据:从商品排名数据获取
- 流量数据:从页面访问服务获取
- 广告数据:从广告数据获取
- 处理数据格式,计算汇总值,填充日期范围
- 返回格式化的图表数据
4.4 商品备注管理
功能描述:更新商品的分析备注
实现流程:
- 前端调用
productAnysApi.updateAnyRemark()方法 - 后端
ProductAnalysisController.updateAnyRemarkAction()处理请求 - 查询是否存在商品操作记录
- 存在则更新备注,不存在则创建新记录
- 返回更新结果
4.5 自定义指标分析
功能描述:支持用户自定义指标组合进行分析
实现流程:
- 前端点击"+"标签页,打开指标选择对话框
- 用户选择需要分析的指标
- 前端创建自定义指标标签页
- 调用
productAnysApi.getChartData()获取选定指标的数据 - 后端根据指标类型组合返回相应的数据
- 前端在图表中展示选定指标的趋势
5. 技术亮点
5.1 前端技术亮点
- 组件化设计:采用Vue 3 Composition API,将页面拆分为主组件和详情组件,提高代码复用性
- 响应式布局:使用Element Plus的栅格系统,实现自适应布局
- 数据可视化:集成ECharts图表库,支持多种图表类型和交互方式
- 无限滚动加载:实现商品列表的无限滚动加载,提升用户体验
- 实时数据更新:通过响应式状态管理,实现数据的实时更新和展示
5.2 后端技术亮点
- 服务分层:采用控制器→服务→数据访问的分层架构,职责清晰
- 数据整合:整合多个数据源的数据,包括订单、排名、流量和广告数据
- 动态指标计算:支持根据用户选择的指标动态计算和返回数据
- 日期范围处理:智能处理日期范围,填充缺失数据,确保数据连续性
- 性能优化:通过缓存和批量查询,优化数据查询性能
5.3 整体架构亮点
- 前后端分离:采用RESTful API设计,实现前后端解耦
- 数据一致性:确保前端展示的数据与后端存储的数据一致
- 可扩展性:模块化设计,支持轻松添加新的指标和分析维度
- 安全性:实现用户权限控制,确保数据安全
- 可维护性:代码结构清晰,文档完善,易于维护和扩展
6. 代码优化建议
6.1 前端优化建议
-
性能优化:
- 实现商品列表的虚拟滚动,减少DOM节点数量
- 优化图表渲染,避免频繁重绘
- 使用缓存机制,减少重复请求
-
代码质量:
- 提取重复的图表配置为可复用的函数
- 优化响应式状态管理,避免不必要的状态更新
- 添加TypeScript类型定义,提高代码可读性和可维护性
-
用户体验:
- 添加数据加载状态和错误处理
- 实现图表的导出功能
- 优化移动端适配
6.2 后端优化建议
-
性能优化:
- 实现数据缓存,减少数据库查询
- 优化SQL查询,添加适当的索引
- 使用异步处理,提高并发性能
-
代码质量:
- 提取重复的日期处理逻辑为工具类
- 优化异常处理,提供更详细的错误信息
- 添加单元测试,提高代码覆盖率
-
功能扩展:
- 支持更多的指标类型和分析维度
- 实现数据导出功能
- 添加数据预测和趋势分析功能
6.3 数据库优化建议
-
索引优化:
- 为常用查询字段添加索引
- 优化复合索引的设计
-
表结构优化:
- 考虑分区表,提高大数据量查询性能
- 优化字段类型和长度
-
数据清理:
- 实现历史数据的归档策略
- 定期清理无效数据
7. 总结
销售商品分析模块是Wimoor系统中一个功能强大的亚马逊商品数据分析工具,通过前后端分离的架构,实现了多维度数据的整合和可视化展示。该模块支持销量、排名、流量、广告等多个维度的数据分析,为用户提供了全面的商品表现视图。
7.1 模块价值
- 数据驱动决策:通过多维度数据分析,帮助用户做出更明智的运营决策
- 运营效率提升:自动化数据收集和分析,减少人工操作,提高运营效率
- 销售业绩增长:通过深入的数据分析,发现销售机会,优化运营策略,提升销售业绩
- 竞争优势建立:通过对商品表现的持续监控和分析,建立竞争优势
7.2 技术实现
- 前端:Vue 3 + Element Plus + ECharts,实现了响应式布局和丰富的数据可视化
- 后端:Spring Boot + MyBatis Plus,实现了高效的数据处理和业务逻辑
- 数据:整合亚马逊多个数据源的数据,提供全面的分析视角
- 架构:前后端分离,模块化设计,确保系统的可扩展性和可维护性
7.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 架构分层
前端架构
- 表现层:Vue组件,负责用户界面渲染和交互
- 业务逻辑层:Vue组合式API,处理业务逻辑
- 数据通信层:Axios,与后端API通信
- 状态管理:Vuex,管理全局状态和共享数据
后端架构
- 控制层:Controller,处理HTTP请求和响应
- 服务层:Service,实现业务逻辑
- 数据访问层:Mapper,与数据库交互
- 亚马逊API层:集成亚马逊广告API,处理广告操作
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)
listCampaigns.vue:广告活动管理listAdgroups.vue:广告组管理listProductAds.vue:商品广告管理listKeywords.vue:关键词管理listNegativaKeywords.vue:否定关键词管理listTarget.vue:商品定向管理listNegativaTarget.vue:否定定向管理listPurchaseProductAds.vue:商品购买数据
SB广告组件 (Sponsored Brands)
listCampaigns.vue:广告活动管理listAdgroups.vue:广告组管理listProductAds.vue:品牌广告管理listKeywords.vue:关键词管理listNegativaKeywords.vue:否定关键词管理listTarget.vue:定向管理listPurchaseProductAds.vue:商品购买数据
SD广告组件 (Sponsored Display)
listCampaigns.vue:广告活动管理listAdgroups.vue:广告组管理listProductAds.vue:商品广告管理listTarget.vue:定向管理listNegativaTarget.vue:否定定向管理listPurchaseProductAds.vue:商品购买数据
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 广告活动管理
功能描述
- 创建、编辑、暂停、启用、删除广告活动
- 设置广告活动预算、开始/结束日期、出价策略
- 查看广告活动性能数据
实现逻辑
- 前端发送创建广告活动请求
- 后端接收请求,验证参数
- 后端调用亚马逊API创建广告活动
- 后端存储广告活动信息到数据库
- 后端返回创建结果给前端
- 前端更新界面,显示新广告活动
关键代码
前端创建广告活动:
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 广告组管理
功能描述
- 创建、编辑、暂停、启用、删除广告组
- 设置广告组名称、默认出价
- 查看广告组性能数据
实现逻辑
- 前端选择广告活动,点击"创建广告组"
- 前端填写广告组信息,提交表单
- 后端接收请求,验证参数
- 后端调用亚马逊API创建广告组
- 后端存储广告组信息到数据库
- 后端返回创建结果给前端
- 前端更新界面,显示新广告组
4.3 商品广告管理
功能描述
- 添加、编辑、暂停、启用商品广告
- 设置商品广告出价
- 查看商品广告性能数据
- 批量操作商品广告
实现逻辑
- 前端选择广告组,点击"添加商品"
- 前端选择要推广的商品,设置出价
- 后端接收请求,验证参数
- 后端调用亚马逊API创建商品广告
- 后端存储商品广告信息到数据库
- 后端返回创建结果给前端
- 前端更新界面,显示新商品广告
4.4 关键词管理
功能描述
- 添加、编辑、删除关键词
- 设置关键词匹配类型和出价
- 查看关键词性能数据
- 批量操作关键词
实现逻辑
- 前端选择广告组,点击"添加关键词"
- 前端输入关键词,设置匹配类型和出价
- 后端接收请求,验证参数
- 后端调用亚马逊API创建关键词
- 后端存储关键词信息到数据库
- 后端返回创建结果给前端
- 前端更新界面,显示新关键词
4.5 定向管理
功能描述
- 添加、编辑、删除商品定向
- 设置定向出价
- 查看定向性能数据
- 管理否定定向
实现逻辑
- 前端选择广告组,点击"添加定向"
- 前端选择定向类型,设置定向条件和出价
- 后端接收请求,验证参数
- 后端调用亚马逊API创建定向
- 后端存储定向信息到数据库
- 后端返回创建结果给前端
- 前端更新界面,显示新定向
4.6 数据报表与分析
功能描述
- 查看广告活动、广告组、商品广告、关键词的性能数据
- 生成趋势图表
- 分析ACOS、ROAS、CTR等关键指标
- 导出报表数据
实现逻辑
- 前端选择时间范围和数据类型
- 前端发送数据请求
- 后端接收请求,查询数据库
- 后端处理数据,计算指标
- 后端返回处理后的数据
- 前端使用ECharts渲染图表
- 前端展示数据表格
5. 技术亮点
5.1 组件化设计
- 模块化结构:按广告类型和功能模块拆分组件,提高代码复用性和可维护性
- 动态组件加载:根据广告类型和标签页动态加载对应组件
- 统一数据接口:不同广告类型的组件使用统一的数据接口,简化代码
5.2 批量操作优化
- 批量创建:支持批量创建广告活动、广告组、关键词等
- 批量修改:支持批量修改出价、预算、状态等
- 异步处理:批量操作采用异步处理,避免阻塞界面
- 错误处理:完善的错误处理机制,确保批量操作的可靠性
5.3 数据可视化
- 多维度图表:支持趋势图、对比图、分布图等多种图表类型
- 实时数据:展示实时广告数据,帮助用户快速决策
- 数据钻取:支持从广告活动到广告组到商品广告的数据钻取
- 自定义报表:支持自定义报表维度和指标
5.4 智能优化
- 出价建议:基于历史数据和竞争情况,提供智能出价建议
- 预算优化:根据广告表现,推荐预算分配方案
- 关键词推荐:基于搜索词数据,推荐高潜力关键词
- 定向优化:分析定向效果,推荐优化方案
5.5 性能优化
- 缓存机制:缓存频繁访问的数据,减少API调用
- 懒加载:采用懒加载方式加载组件和数据
- 分页处理:大数据集采用分页处理,提高加载速度
- 请求合并:合并多个相关请求,减少网络开销
6. 数据安全
6.1 权限控制
- 基于角色的权限控制:不同角色拥有不同的操作权限
- 店铺级权限:用户只能操作有权限的店铺的广告
- 操作日志:记录所有广告操作,便于审计
6.2 数据加密
- API密钥加密:亚马逊API密钥加密存储
- 传输加密:使用HTTPS加密传输数据
- 敏感数据保护:敏感数据加密存储
6.3 防滥用措施
- 请求频率限制:限制API请求频率,防止滥用
- 批量操作限制:限制批量操作的数量,避免系统过载
- 异常检测:检测异常操作,及时预警
7. 扩展性分析
7.1 模块扩展
- 新广告类型支持:系统设计支持轻松添加新的广告类型
- 新功能模块:模块化设计便于添加新的功能模块
- 第三方集成:预留接口,支持集成第三方广告工具
7.2 技术扩展
- 微服务架构:可扩展为微服务架构,提高系统可靠性和可扩展性
- 容器化部署:支持Docker容器化部署,便于水平扩展
- 云服务集成:可集成AWS、阿里云等云服务
7.3 功能扩展
- AI智能优化:集成AI算法,提供更智能的广告优化建议
- 多平台支持:扩展支持其他电商平台的广告管理
- 自动化规则:支持设置自动化规则,实现广告的自动管理
8. 代码优化建议
8.1 前端优化
- 组件拆分:将大型组件进一步拆分为更小的、可复用的组件
- 状态管理优化:使用Pinia替代Vuex,简化状态管理
- API请求优化:使用请求缓存和防抖节流,减少API调用
- 代码分割:使用动态导入实现代码分割,减少初始加载时间
- 类型定义:使用TypeScript,提高代码类型安全性
8.2 后端优化
- 缓存策略:优化缓存策略,减少数据库查询和API调用
- 异步处理:使用消息队列处理耗时操作,提高系统响应速度
- 数据库优化:优化数据库查询,添加适当的索引
- API设计:优化API设计,减少冗余接口
- 错误处理:完善错误处理机制,提高系统可靠性
8.3 性能优化
- 前端性能:优化前端渲染性能,减少重绘和回流
- 后端性能:优化后端代码,提高处理速度
- 数据库性能:优化数据库结构和查询,提高数据访问速度
- 网络性能:优化网络传输,减少数据传输量
- 系统架构:优化系统架构,提高整体性能
9. 总结
广告管理模块是Wimoor系统中一个功能强大、设计合理的核心模块,它通过集成亚马逊广告API,为用户提供了完整的广告管理功能。该模块采用前后端分离架构,使用Vue 3和Spring Boot等现代技术栈,实现了广告活动、广告组、商品广告、关键词、定向等的全生命周期管理。
9.1 核心价值
- 提高效率:批量操作和自动化功能,大大提高广告管理效率
- 数据驱动:丰富的数据报表和分析工具,支持数据驱动决策
- 智能优化:智能出价建议和预算优化,提升广告效果
- 全类型支持:支持SP、SB、SD三种广告类型,满足不同推广需求
- 安全可靠:完善的权限控制和数据安全措施,确保广告操作安全
9.2 技术创新
- 组件化设计:模块化、组件化的前端架构,提高代码可维护性
- 动态组件加载:根据广告类型和功能动态加载组件,灵活适应不同需求
- 实时数据可视化:实时展示广告数据,帮助用户快速决策
- 智能算法集成:集成智能算法,提供优化建议
- 高性能架构:优化的系统架构,确保在大量广告数据下的性能
9.3 未来发展
- AI深度集成:进一步集成AI技术,提供更智能的广告优化
- 多平台支持:扩展支持其他电商平台的广告管理
- 自动化运营:实现更高级的自动化规则,减少人工操作
- 预测分析:基于历史数据,预测广告效果,提供前瞻性建议
- 生态系统:构建广告管理生态系统,集成更多第三方工具
广告管理模块的设计和实现体现了现代软件架构的最佳实践,为用户提供了专业、高效、智能的广告管理解决方案。通过不断的技术创新和功能扩展,该模块将继续为用户创造更大的价值。
广告-广告统计
广告统计模块功能解析文档
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 架构设计
广告统计模块采用前后端分离的架构设计,具体架构层次如下:
-
前端层:
- 视图层:Vue组件,负责数据展示和用户交互
- 业务逻辑层:Vue组合式API,处理前端业务逻辑
- API调用层:封装的API请求函数,与后端通信
-
后端层:
- 控制层:Spring MVC控制器,处理HTTP请求
- 服务层:业务逻辑服务,处理核心业务逻辑
- 数据访问层:MyBatis Plus Mapper,与数据库交互
- 外部API层:与Amazon Advertising API交互,获取广告数据
-
数据层:
- 数据库:存储广告数据和统计信息
- 缓存:可选,提高数据查询性能
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 运行中活动统计
实现原理:
- 通过调用
summaryApi.getenablesumtype()获取当前运行中的广告活动、广告组、商品广告和关键词数量 - 数据返回后更新到
typedata对象中,通过模板渲染到对应的数据卡片
关键代码:
// 加载运行中活动数据
onMounted(()=>{
summaryApi.getenablesumtype().then(res=>{
state.typedata=res.data;
});
loadWaringData();
})
2.2.2 异常数据预警
实现原理:
- 支持切换「商品广告」和「关键词」两种预警类型
- 支持选择「昨日变动」、「连续变动」、「同期变动」三种预警维度
- 通过调用
summaryApi.getProductWarningIndicator()或summaryApi.getKeywordsWarningIndicator()获取异常预警数据 - 点击异常指标可查看详细信息,调用
loadWaringDataDetail()方法
关键代码:
// 加载预警数据
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 广告数据分析
实现原理:
- 支持选择时间范围、广告组和币种
- 提供「广告数据分析」和「广告报表统计」两个标签页
- 「广告数据分析」标签页通过ECharts绘制趋势图表,支持多指标叠加显示
- 「广告报表统计」标签页以表格形式展示月度数据统计
- 通过调用
summaryApi.getsumproduct()获取图表数据,调用summaryApi.getmonthsum()获取月度报表数据
关键代码:
// 加载图表数据
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 漏斗分析
实现原理:
- 通过调用相关API获取广告转化漏斗数据
- 使用ECharts绘制漏斗图,展示从曝光到转化的完整转化路径
- 分析各阶段的转化率,识别转化瓶颈
2.2.5 ROAS排名
实现原理:
- 通过调用相关API获取广告ROAS数据
- 使用表格或图表展示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 获取广告数据分析数据
实现原理:
- 接收前端传入的查询参数,包括时间范围、广告组、配置文件ID和币种
- 构建查询参数,调用
amzAdvSumProductAdsService.getSumProduct()获取广告数据汇总 - 调用
amzAdvSumProductAdsService.orderSummaryAll()获取总销售额 - 调用
amzAdvSumProductAdsService.getDaysSumProduct()获取按日汇总的数据(用于图表展示) - 将数据封装返回给前端
关键代码:
@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 获取月度广告报表数据
实现原理:
- 接收前端传入的查询参数,包括时间范围、广告组、配置文件ID和币种
- 构建查询参数,调用
amzAdvSumProductAdsService.getMonthSumProduct()获取月度广告报表数据 - 将数据封装返回给前端
关键代码:
@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 数据统计功能
功能说明:
- 统计运行中广告活动、广告组、商品广告和关键词数量
- 统计广告投放的核心指标,如曝光量、点击量、花费、销售额等
- 支持按日、按月的时间维度进行统计
- 支持按广告组、配置文件等维度进行筛选
技术实现:
- 前端通过API调用获取统计数据
- 后端通过SQL查询和数据计算生成统计结果
- 使用ECharts绘制趋势图表,直观展示数据变化
业务价值:
- 帮助用户快速了解广告投放规模和基本状态
- 为投放策略调整提供数据支持
- 便于团队成员了解整体投放情况
4.2 异常预警功能
功能说明:
- 监测商品广告和关键词的异常表现
- 支持「昨日变动」、「连续变动」、「同期变动」三种预警维度
- 预警指标包括曝光量突降、点击量突降、转化率突降、ACOS突增
- 点击异常指标可查看详细的异常信息
技术实现:
- 后端通过数据计算和比对生成异常预警数据
- 前端展示异常预警信息,并提供详细查看功能
- 支持自定义预警阈值和监测维度
业务价值:
- 帮助用户及时发现广告投放中的异常问题
- 减少人工监控的工作量
- 提高问题响应速度,降低损失
4.3 数据导出功能
功能说明:
- 支持导出广告数据报表
- 导出格式为Excel,方便用户进行进一步分析
- 支持按时间范围、广告组等维度筛选导出数据
技术实现:
- 后端使用Apache POI生成Excel文件
- 通过HTTP响应将Excel文件下载到客户端
业务价值:
- 方便用户在离线环境下分析数据
- 便于与团队成员共享数据
- 支持与其他系统的数据集成
4.4 漏斗分析功能
功能说明:
- 展示广告转化漏斗,分析用户从曝光到转化的全过程
- 计算各阶段的转化率,识别转化瓶颈
- 支持按时间范围、广告组等维度进行分析
技术实现:
- 后端计算转化漏斗各阶段的数据
- 前端使用ECharts绘制漏斗图
业务价值:
- 帮助用户了解广告转化的完整路径
- 识别转化过程中的瓶颈环节
- 为优化投放策略提供针对性建议
4.5 ROAS排名功能
功能说明:
- 展示广告投入产出比排名
- 识别高效广告和低效广告
- 支持按时间范围、广告组等维度进行排序
技术实现:
- 后端计算各广告的ROAS值并排序
- 前端展示ROAS排名列表
业务价值:
- 帮助用户快速识别高效广告和低效广告
- 为预算分配和投放优化提供依据
- 提高广告投放的整体效率
5. 技术亮点
5.1 数据可视化
技术实现:
- 使用ECharts绘制多种类型的图表,包括折线图、柱状图、漏斗图等
- 支持多指标叠加显示,便于数据对比分析
- 图表支持响应式布局,适配不同屏幕尺寸
- 图表交互功能丰富,支持 tooltip、图例切换等
优势:
- 直观展示数据趋势和变化
- 提高数据可读性和分析效率
- 增强用户体验
5.2 智能预警
技术实现:
- 多种预警维度:支持昨日变动、连续变动、同期变动三种预警维度
- 多指标监测:监测曝光量、点击量、转化率、ACOS等核心指标
- 自定义阈值:支持用户自定义预警阈值
- 详细异常信息:点击异常指标可查看详细的异常信息
优势:
- 及时发现广告投放中的异常问题
- 减少人工监控的工作量
- 提高问题响应速度
5.3 高效数据处理
技术实现:
- 后端使用MyBatis Plus进行数据库操作,提高查询效率
- 数据缓存机制:对频繁查询的数据进行缓存
- 异步处理:对耗时的数据计算任务采用异步处理
- 数据预计算:对常用统计数据进行预计算,减少实时计算压力
优势:
- 提高系统响应速度
- 减少服务器负载
- 支持处理大量数据
5.4 灵活数据筛选
技术实现:
- 多维度筛选:支持按时间范围、广告组、配置文件等维度进行筛选
- 多币种支持:支持USD和CNY币种切换
- 自定义指标:支持用户选择需要查看的指标
优势:
- 满足不同用户的个性化需求
- 提高数据分析的针对性
- 增强系统的灵活性
5.5 全面的指标体系
技术实现:
- 核心指标:曝光量、点击量、花费、销售额、订单数等
- 效率指标:CTR、转化率、ACOS、ROAS、CPC等
- 衍生指标:ACOAS(广告花费/总销售额比率)等
优势:
- 提供全面的广告投放效果评估
- 满足不同业务场景的分析需求
- 帮助用户制定更精准的优化策略
6. 数据安全
6.1 权限控制
实现方案:
- 基于用户角色的权限控制:不同角色的用户拥有不同的操作权限
- 数据访问控制:用户只能访问自己有权限的广告数据
- 操作日志记录:记录用户的关键操作,便于审计和追溯
安全措施:
- 使用Spring Security进行权限管理
- 实现细粒度的数据权限控制
- 定期审计操作日志
6.2 数据保护
实现方案:
- 数据加密:对敏感数据进行加密存储
- 数据脱敏:在前端展示时对敏感数据进行脱敏处理
- 数据备份:定期对广告数据进行备份,防止数据丢失
安全措施:
- 使用HTTPS协议传输数据
- 数据库密码加密存储
- 定期数据备份和恢复演练
6.3 合规性
实现方案:
- 遵守亚马逊广告API使用规范
- 符合数据隐私保护法规
- 确保数据采集和使用的合法性
合规措施:
- 定期更新亚马逊广告API集成
- 制定数据使用政策
- 获得用户数据使用授权
7. 扩展性分析
7.1 功能扩展
潜在扩展点:
- 增加更多广告类型的支持:如Sponsored Brands、Sponsored Display等
- 增加更多预警指标:如CPC突增、ROAS突降等
- 增加更多数据分析维度:如按产品类别、关键词类型等
- 增加预测分析功能:基于历史数据预测未来广告表现
扩展方案:
- 模块化设计:采用模块化架构,便于功能扩展
- 插件机制:支持通过插件方式增加新功能
- 配置化:核心功能参数可配置,便于适应不同业务需求
7.2 技术扩展
潜在扩展点:
- 增加实时数据处理能力:使用流处理技术处理实时广告数据
- 增加机器学习能力:使用机器学习算法进行异常检测和预测分析
- 增加数据可视化功能:增加更多图表类型和交互方式
- 增加API集成能力:集成更多第三方广告平台的API
扩展方案:
- 微服务架构:将核心功能拆分为微服务,便于独立扩展
- 容器化部署:使用Docker容器化部署,提高部署灵活性
- 云服务集成:利用云服务的弹性扩展能力
7.3 集成扩展
潜在扩展点:
- 与其他模块的集成:如与产品管理、库存管理等模块的集成
- 与第三方工具的集成:如与广告优化工具、数据分析工具的集成
- 与电商平台的集成:除亚马逊外,集成其他电商平台的广告数据
扩展方案:
- 统一API接口:提供标准化的API接口,便于与其他系统集成
- 消息队列:使用消息队列实现系统间的异步通信
- Webhooks:支持通过Webhooks与第三方系统集成
8. 代码优化建议
8.1 前端优化
优化建议:
- 组件拆分:将大型组件拆分为更小的、可复用的组件,提高代码可维护性
- 状态管理优化:合理使用Vuex管理全局状态,减少组件间的props传递
- 性能优化:
- 使用虚拟滚动处理大量数据列表
- 优化图表渲染,减少不必要的重绘
- 使用防抖和节流优化频繁触发的事件处理函数
- 代码规范:
- 统一代码风格,使用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 后端优化
优化建议:
- 数据库优化:
- 为频繁查询的字段添加索引
- 优化SQL查询,减少不必要的字段查询
- 使用分页查询处理大量数据
- 缓存优化:
- 对频繁查询的数据使用Redis缓存
- 合理设置缓存过期时间
- 实现缓存预热机制
- 代码优化:
- 减少重复代码,提取公共方法
- 使用Lambda表达式和Stream API简化代码
- 添加必要的注释和文档
- 性能监控:
- 添加性能监控点,监控关键操作的执行时间
- 定期分析性能瓶颈,进行针对性优化
具体实现:
// 优化前:重复的参数处理代码
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 数据处理优化
优化建议:
- 批量处理:对大量数据的操作使用批量处理,减少数据库交互次数
- 异步处理:对耗时的数据计算任务采用异步处理,提高系统响应速度
- 数据压缩:对传输的数据进行压缩,减少网络传输量
- 预计算:对常用统计数据进行预计算,减少实时计算压力
具体实现:
// 优化前:实时计算统计数据
@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系统中功能强大、数据全面的广告分析工具,具有以下核心价值:
- 数据驱动决策:基于实时、全面的数据进行投放决策,提高决策的科学性和准确性
- 问题快速定位:通过异常预警快速定位投放问题,减少人工监控的工作量
- 效果可视化:通过图表和报表直观展示投放效果,提高数据可读性和分析效率
- 策略优化指导:基于数据分析结果指导投放策略优化,提高广告投放的整体效果
- 团队协作支持:方便团队成员共享数据和分析结果,促进团队协作
9.2 技术创新
广告统计模块在技术实现上具有以下创新点:
- 多维度数据统计:支持按时间、广告组、配置文件等多维度进行数据统计和分析
- 智能异常预警:采用多种预警维度和指标,实现广告异常的智能监测和预警
- 丰富的数据可视化:使用ECharts绘制多种类型的图表,直观展示数据趋势和变化
- 高效数据处理:通过缓存、异步处理等技术,提高数据处理效率和系统响应速度
- 灵活的扩展性:采用模块化、配置化的设计,便于功能扩展和技术升级
9.3 未来发展
广告统计模块具有广阔的发展前景,未来可以从以下几个方面进行发展:
- 功能增强:增加更多广告类型的支持,增加更多预警指标和分析维度
- 技术升级:引入机器学习、人工智能等先进技术,实现更智能的广告分析和优化
- 集成扩展:与更多第三方广告平台、电商平台集成,实现数据的统一管理和分析
- 移动化:开发移动应用,方便用户随时随地查看广告数据和接收异常预警
- 生态建设:构建广告数据分析生态,提供更多增值服务
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 核心组件
- 前端组件:
wimoor-ui/src/views/erp/purchase/open1688/bind/index.vue- 1688绑定主组件 - API服务:
wimoor-ui/src/api/erp/purchase/open1688/purchasealibabaApi.js- 前端API调用服务 - 后端控制器:
AlibabaController.java- 处理1688相关的HTTP请求 - 服务实现:
PurchaseAlibabaAuthServiceImpl.java- 实现1688授权和业务逻辑 - 数据模型:
PurchaseAlibabaAuth.java- 1688授权数据实体
2. 前端代码结构分析
2.1 主组件结构
前端主组件 index.vue 包含以下核心部分:
-
模板部分:
- 账号列表表格,展示已绑定的1688账号信息
- 绑定账号弹窗,用于添加或编辑账号信息
- 操作按钮,包括绑定、延期授权和删除
-
脚本部分:
- 响应式数据:账号信息、弹窗状态、表单数据等
- 生命周期钩子:组件挂载时获取授权列表并处理授权回调
- 核心方法:
getauthList():获取已绑定账号列表showDialog():打开绑定/编辑弹窗bindAuth():触发授权流程goUrl():获取1688授权链接并跳转removeBind():删除已绑定账号GetRequest():处理1688授权回调
2.2 API调用服务
purchasealibabaApi.js 定义了与后端交互的API方法:
getAuthData():获取授权账号列表get1688Url():获取1688授权链接logicDelete():删除授权账号refreshAuthData():刷新授权数据bindAuthData():绑定授权数据submitname():提交账号信息updateName():更新账号名称
3. 后端代码结构分析
3.1 控制器层
AlibabaController.java 提供以下API端点:
POST /submitname:提交或更新1688账号信息GET /bindAuthData:绑定1688授权数据GET /refreshAuthData:刷新1688授权数据GET /getAuthData:获取已绑定的1688账号列表GET /delete:删除并解绑1688账号GET /get1688Url:获取1688授权链接GET /message:接收1688平台的消息通知
3.2 服务实现层
PurchaseAlibabaAuthServiceImpl.java 实现了以下核心功能:
saveAction():保存或更新1688账号信息bindAuth():处理1688授权流程,获取访问令牌refreshAuthToken():使用refresh_token刷新访问令牌refreshAuthRefreshToken():刷新refresh_token本身getAuthData():获取已绑定的账号列表updateAlibaba():更新账号状态(删除)callApi():调用1688开放平台APIcheckAuthorityToken():检查并刷新授权令牌
3.3 数据模型
PurchaseAlibabaAuth 实体包含以下核心字段:
id:主键IDshopid:店铺IDname:账号名称appkey:1688应用密钥appsecret:1688应用密钥accessToken:访问令牌refreshToken:刷新令牌accessTokenTimeout:访问令牌过期时间refreshTokenTimeout:刷新令牌过期时间aliId:1688用户IDresourceOwner:1688账号名称memberId:1688会员IDisDelete:删除状态
4. 核心功能实现
4.1 授权流程实现
-
前端触发授权:
- 用户点击"绑定账号"按钮
- 填写账号名称和开发者信息(可选)
- 点击"授权"按钮,调用
bindAuth()方法 bindAuth()调用getcode()获取账号IDgetcode()调用后端/submitname接口创建账号记录- 成功后调用
goUrl()获取1688授权链接
-
1688平台授权:
- 前端打开1688授权页面
- 用户登录1688账号并确认授权
- 1688平台重定向回系统,并携带授权码
-
后端处理授权回调:
- 前端
GetRequest()方法解析URL参数,获取授权码 - 调用后端
/bindAuthData接口,传入授权码和状态 - 后端
bindAuth()方法使用授权码获取访问令牌和刷新令牌 - 保存令牌信息到数据库
- 前端
-
前端更新状态:
- 授权成功后,前端显示成功提示
- 刷新账号列表,显示新绑定的账号
4.2 令牌管理实现
-
令牌获取:
- 在
bindAuth()方法中,使用授权码调用1688开放平台的/system.oauth2/getToken接口 - 获取
access_token、refresh_token、expires_in等信息 - 计算令牌过期时间并保存到数据库
- 在
-
令牌刷新:
- 在
refreshAuthToken()方法中,使用refresh_token调用1688开放平台的/system.oauth2/getToken接口 - 获取新的
access_token和过期时间 - 更新数据库中的令牌信息
- 在
-
令牌检查:
- 在
checkAuthorityToken()方法中,检查访问令牌是否过期 - 若过期,自动调用
refreshAuthToken()刷新令牌
- 在
4.3 账号管理实现
-
账号列表:
- 前端调用
getauthList()获取已绑定账号列表 - 后端
getAuthData()方法查询数据库,返回未删除的账号 - 前端表格展示账号信息,包括名称、状态、到期时间等
- 前端调用
-
账号编辑:
- 用户点击编辑图标,打开编辑弹窗
- 修改账号名称或开发者信息
- 点击"保存"按钮,调用后端
/submitname接口更新账号信息
-
账号删除:
- 用户点击"删除"按钮
- 前端调用
removeBind()方法 - 后端
updateAlibaba()方法将账号标记为删除状态
-
延期授权:
- 用户点击"延期授权"按钮
- 前端调用
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授权实现
- 技术要点:实现完整的OAuth2授权流程,包括授权码获取、令牌交换、令牌刷新
- 实现方案:使用1688开放平台的OAuth2.0协议,通过授权码模式获取令牌
- 难点:处理授权回调和令牌管理,确保授权流程的安全性和可靠性
6.2 令牌生命周期管理
- 技术要点:管理访问令牌和刷新令牌的生命周期,确保API调用的连续性
- 实现方案:
- 记录令牌的过期时间
- 定期检查并自动刷新令牌
- 处理令牌过期的异常情况
- 难点:平衡令牌刷新的频率和系统性能,避免频繁刷新令牌导致的API调用限制
6.3 聚石塔服务集成
- 技术要点:通过聚石塔服务与1688开放平台进行通信
- 实现方案:使用HTTP客户端调用聚石塔的转发接口,处理API请求和响应
- 难点:处理聚石塔服务的异常情况,确保API调用的稳定性
6.4 多账号管理
- 技术要点:支持绑定和管理多个1688账号
- 实现方案:为每个账号创建独立的授权记录,分别管理令牌和状态
- 难点:确保多账号场景下的令牌管理和API调用的正确性
7. 代码优化建议
7.1 前端代码优化
-
错误处理优化:
- 当前代码在API调用失败时缺少统一的错误处理机制
- 建议实现全局错误处理拦截器,统一处理API错误
-
状态管理优化:
- 当前使用本地响应式数据管理状态,对于复杂场景可能不够灵活
- 建议使用Pinia或Vuex进行状态管理,提高代码可维护性
-
代码结构优化:
- 将授权流程相关的逻辑抽取为独立的composable函数
- 提高代码的复用性和可读性
7.2 后端代码优化
-
安全性优化:
- 当前代码中AppKey和AppSecret直接存储在数据库中
- 建议对敏感信息进行加密存储,提高安全性
-
异常处理优化:
- 当前代码中异常处理较为简单,缺少详细的错误信息
- 建议实现统一的异常处理机制,提供更详细的错误信息
-
性能优化:
- 当前代码中存在重复的数据库查询操作
- 建议使用缓存机制减少数据库查询,提高系统性能
-
代码结构优化:
- 将1688 API调用相关的代码抽取为独立的服务
- 提高代码的模块化程度和可维护性
7.3 架构优化
-
微服务架构:
- 考虑将1688相关的功能抽取为独立的微服务
- 提高系统的扩展性和可维护性
-
异步处理:
- 对于耗时的API调用,考虑使用异步处理方式
- 提高系统的响应速度和并发处理能力
-
监控和告警:
- 实现对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 核心组件
- 前端组件:
wimoor-ui/src/views/amazon/storeAuth/index.vue- 店铺管理主组件 - API服务:
wimoor-ui/src/api/amazon/group/groupApi.js- 前端API调用服务 - 后端控制器:
AmazonGroupController.java- 处理店铺相关的HTTP请求 - 服务实现:
AmazonGroupServiceImpl.java- 实现店铺管理的业务逻辑 - 数据模型:
AmazonGroup.java- 店铺数据实体
2. 前端代码结构分析
2.1 主组件结构
前端主组件 index.vue 包含以下核心部分:
-
模板部分:
- 左侧店铺列表区域,展示已创建的店铺信息
- 右侧店铺详情和授权区域
- 添加/编辑店铺弹窗
- 店铺排序弹窗
-
脚本部分:
- 响应式数据:店铺列表、表单数据、弹窗状态等
- 生命周期钩子:组件挂载时加载店铺列表
- 核心方法:
addStorename():打开添加店铺弹窗saveStore():保存店铺信息loadStoreList():加载店铺列表updataStorename():编辑店铺信息delectStore():删除店铺submitFormIndex():提交店铺排序
2.2 API调用服务
groupApi.js 定义了与后端交互的API方法:
getAmazongroupList():获取店铺列表AmazonGroupSave():保存店铺信息getAmazonGroupById():根据ID获取店铺详情deleteAmazongroup():删除店铺updateBatch():批量更新店铺排序
3. 后端代码结构分析
3.1 控制器层
AmazonGroupController.java 提供以下API端点:
GET /list:获取店铺列表GET /listByAdmin:管理员根据公司ID获取店铺列表GET /id/{id}:根据ID获取店铺详情DELETE /delete/{id}:删除店铺POST /updateBatch:批量更新店铺排序PUT /save:保存店铺信息
3.2 服务实现层
AmazonGroupServiceImpl.java 实现了以下核心功能:
getGroupByUser():根据用户信息获取店铺列表selectByShopId():根据公司ID获取店铺列表findAmazonGroupByName():根据店铺名称和公司ID查找店铺selectTaskInfoList():获取任务信息列表
3.3 数据模型
AmazonGroup 实体包含以下核心字段:
id:主键IDname:店铺名称shopid:公司IDprofitcfgid:匹配的利润计算方案IDcompany:公司名称isfinance:是否财务账套taxNumber:税务编码customNumber:海关注册编码dxpid:海关Dxpidfindex:排序号operator:操作人IDopttime:操作时间creator:创建人IDcreatetime:创建时间isdelete:逻辑删除标记
4. 核心功能实现
4.1 店铺管理实现
-
添加店铺:
- 前端调用
addStorename()方法打开添加弹窗 - 用户填写店铺信息后点击保存
- 前端调用
saveStore()方法,通过groupApi.AmazonGroupSave()向后端发送请求 - 后端
saveAmazonGroupAction()方法处理请求,保存店铺信息到数据库
- 前端调用
-
编辑店铺:
- 前端点击店铺右侧的编辑图标,调用
updataStorename()方法 - 前端通过
groupApi.getAmazonGroupById()获取店铺详情 - 用户修改信息后点击保存
- 前端调用
saveStore()方法保存修改 - 后端
saveAmazonGroupAction()方法处理请求,更新店铺信息
- 前端点击店铺右侧的编辑图标,调用
-
删除店铺:
- 前端点击店铺右侧的删除图标,调用
delectStore()方法 - 前端显示确认对话框,用户确认删除
- 前端调用
groupApi.deleteAmazongroup()向后端发送请求 - 后端
delAmazonGroupByIdAction()方法处理请求,检查店铺是否有关联的授权信息 - 如果没有关联授权,将店铺标记为删除状态
- 前端点击店铺右侧的删除图标,调用
4.2 店铺排序实现
-
打开排序弹窗:
- 前端点击"排序"按钮,打开排序弹窗
- 前端显示店铺列表,支持拖拽排序
-
拖拽排序:
- 用户通过拖拽方式调整店铺顺序
- 前端
dragEnd()方法更新店铺的排序号
-
保存排序:
- 用户点击"确认"按钮保存排序
- 前端调用
submitFormIndex()方法,通过groupApi.updateBatch()向后端发送请求 - 后端
updateAmazonGroupConfigAction()方法处理请求,批量更新店铺排序
4.3 财务设置实现
-
设置财务账套:
- 前端编辑店铺信息,开启"是否财务账套"开关
- 前端显示公司名称和税号输入框
- 用户填写相关信息
-
初始化财务账套:
- 前端开启"初始化财务账套"开关
- 前端调用
saveStore()方法保存设置 - 前端先调用
initFinAccountingSubjects()初始化财务科目 - 然后调用
groupApi.AmazonGroupSave()保存店铺信息
4.4 权限控制实现
- 用户权限:后端通过
UserInfoContext.get()获取当前用户信息 - 公司权限:根据用户的公司ID过滤店铺列表
- 管理员权限:管理员用户可以查看和管理所有公司的店铺
- 操作权限:删除店铺时检查店铺是否有关联的授权信息
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 前端技术要点
- Vue 3 Composition API:使用Vue 3的Composition API实现组件逻辑,提高代码可维护性
- 拖拽排序:使用vuedraggable库实现店铺的拖拽排序功能
- 响应式数据:使用ref和reactive实现响应式数据管理
- 表单验证:实现基本的表单验证逻辑,确保数据的完整性
6.2 后端技术要点
- 权限控制:基于用户角色和公司ID实现权限控制
- 事务管理:确保数据操作的原子性和一致性
- 数据校验:检查店铺名称的唯一性,防止重复创建
- 逻辑删除:使用逻辑删除标记,保留数据历史
6.3 技术难点
- 拖拽排序实现:确保拖拽过程的流畅性和排序结果的准确性
- 财务账套初始化:协调财务系统和店铺管理系统的数据同步
- 权限控制:实现细粒度的权限控制,确保数据安全
- 数据一致性:确保店铺信息与其他模块数据的一致性
7. 代码优化建议
7.1 前端代码优化
-
错误处理优化:
- 当前代码在API调用失败时缺少统一的错误处理机制
- 建议实现全局错误处理拦截器,统一处理API错误
-
表单验证优化:
- 当前表单验证逻辑较为简单,建议使用Element Plus的表单验证规则
- 实现更全面的表单验证,确保数据的有效性
-
代码结构优化:
- 将店铺管理相关的逻辑抽取为独立的composable函数
- 提高代码的复用性和可读性
-
性能优化:
- 实现店铺列表的虚拟滚动,提高大数据量下的渲染性能
- 使用缓存机制减少重复的API调用
7.2 后端代码优化
-
安全性优化:
- 当前代码中缺少对输入参数的验证,建议实现请求参数验证
- 使用@Valid注解和校验组实现更严格的参数验证
-
异常处理优化:
- 当前代码中异常处理较为简单,建议实现统一的异常处理机制
- 提供更详细的错误信息和错误码
-
性能优化:
- 实现店铺列表的缓存机制,减少数据库查询
- 使用批量操作减少数据库交互次数
-
代码结构优化:
- 将业务逻辑进一步分离,提高代码的可维护性
- 实现更细粒度的服务层接口
7.3 架构优化
-
微服务架构:
- 考虑将店铺管理模块抽取为独立的微服务
- 提高系统的扩展性和可维护性
-
缓存架构:
- 实现分布式缓存,提高系统性能
- 缓存店铺列表和常用数据
-
监控架构:
- 实现店铺管理模块的监控和告警机制
- 及时发现和处理系统异常
8. 总结
店铺管理模块是Wimoor系统中管理亚马逊店铺信息的核心模块,通过该模块用户可以方便地管理店铺的基本信息、财务设置和海关信息。模块采用前后端分离架构,前端使用Vue 3 Composition API实现用户界面,后端使用Spring Boot实现业务逻辑。
模块的核心功能包括店铺的添加、编辑、删除,店铺排序的调整,财务账套的设置和初始化,以及海关信息的配置。通过这些功能,用户可以有效地管理多个亚马逊店铺的信息,为后续的业务操作提供基础数据支持。
在技术实现上,模块解决了权限控制、数据一致性、拖拽排序等技术难点,为系统的稳定运行提供了保障。同时,通过代码优化建议的实施,可以进一步提高模块的性能、安全性和可维护性。