LikeShop 单商户 Java 版商城售后退换货流程的自定义二开落地

简介: 本文针对LikeShop单商户Java版售后模块的扩展需求,提出“独立售后单+状态机驱动+事件联动”的轻量二开方案。覆盖仅退款、退货退款、换货三类场景,支持部分退换、多级审核、物流跟踪与运费规则,全程不侵入原订单逻辑,保障资金、库存一致性,兼顾平滑升级与高可维护性。

电商系统里,售后退换货是最容易出纠纷、也最容易埋坑的模块。

LikeShop 单商户 Java 版原生已经提供了基础的退款能力,但在实际业务落地时,很多客户会有更细化的诉求:区分「仅退款」与「退货退款」、支持换货、多级审核、用户寄回物流跟踪、仓库收货核验、部分商品退换、运费承担规则等。如果直接在原有订单逻辑里硬改,很容易打乱原有的订单状态机,导致资金、库存不同步。

这类需求属于典型的业务扩展型二开,最佳实践是独立售后单体系 + 状态机驱动 + 事件联动原有订单能力,不推翻原生订单逻辑,以最小侵入成本实现完整的退换货流程。

本文基于 LikeShop 单商户 Java 版的技术底座,从架构设计、表结构改造、核心流程实现、上下游联动、平滑升级五个维度,整理一套可落地的售后二开方案。

一、二开背景与核心目标

1.1 原生售后能力的局限

原生版本以订单维度的退款为主,能够满足基础的全额退款场景,但在复杂退换货场景下存在短板:

  • 流程颗粒度粗:未区分仅退款、退货退款、换货三种核心售后类型;
  • 状态节点少:缺少商家审核、用户寄回、仓库收货、退款中这些中间状态;
  • 部分退换支持弱:一个订单多件商品时,无法针对单个商品发起售后;
  • 物流与收货缺失:退货物流跟踪、仓库收货核验环节能力不足;
  • 审核规则单一:缺少超时自动同意、商家驳回可申诉等灵活配置。

1.2 二开核心目标

  1. 售后类型全覆盖:支持仅退款、退货退款、换货三种主流售后类型;
  2. 完整状态机:从申请、审核、退货、收货、退款/换货到完结,全链路状态可控,非法跳转拦截;
  3. 部分退换支持:支持按商品维度发起售后,一个订单可多次发起售后;
  4. 最小侵入原则:独立售后单体系,复用原生订单、库存、支付退款能力,不破坏原有正向流程;
  5. 可配置化:审核超时时间、运费承担规则、驳回是否可重提等可配置,适配不同行业客户。

二、核心设计思路

2.1 三种售后类型

类型 流程特点 适用场景
仅退款 不用退货,审核通过直接退款 商品破损、发错货、差价补偿
退货退款 用户寄回商品,仓库收货后退款 7天无理由、质量问题退货
换货 用户寄回旧品,仓库重新发新品 尺码不合适、发错型号、质量问题换货

2.2 售后状态机

状态机是售后模块的灵魂,所有操作前置校验状态,杜绝非法跳转。以退货退款为例,完整流转如下:

用户提交申请 → 待商家审核
    ↓
┌─ 商家驳回 → 已驳回 → 用户可重新提交
│
└─ 商家同意 → 待用户退货
        ↓
    用户填写物流 → 待商家收货
        ↓
    仓库核验收货 → 待退款
        ↓
    发起原路退款 → 退款中
        ↓
    退款成功 → 已完成

补充异常分支:

  • 审核超时未处理 → 系统自动同意;
  • 用户超时未退货 → 自动关闭售后;
  • 收货核验不通过 → 驳回退款,状态回退待用户确认。

2.3 订单与售后的关系

  • 一对多关系:一个正向订单可发起多笔售后单,支持部分商品、部分数量退换;
  • 金额独立计算:每笔售后单单独计算退款金额、运费、优惠分摊;
  • 状态解耦:售后单有独立状态机,不直接修改原订单状态,仅在终态时同步订单售后标记。

三、数据库设计

采用「新增表为主、扩展字段为辅」的策略,尽量不修改原生表结构,降低升级冲突。

3.1 售后主表 ls_after_sale

承载售后单核心信息,每笔申请一条记录。

CREATE TABLE ls_after_sale (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  after_sale_sn VARCHAR(32) NOT NULL COMMENT '售后单号',
  order_id BIGINT NOT NULL COMMENT '原订单ID',
  order_sn VARCHAR(32) NOT NULL COMMENT '原订单号',
  user_id BIGINT NOT NULL,

  type TINYINT NOT NULL COMMENT '售后类型:1仅退款 2退货退款 3换货',
  reason VARCHAR(128) NOT NULL COMMENT '售后原因',
  description VARCHAR(500) COMMENT '问题描述',
  images VARCHAR(500) COMMENT '凭证图片,逗号分隔',

  refund_amount DECIMAL(10,2) NOT NULL COMMENT '退款金额',
  refund_freight DECIMAL(10,2) DEFAULT 0 COMMENT '退运费金额',
  freight_bearer TINYINT COMMENT '运费承担方:1买家 2商家',

  status TINYINT NOT NULL COMMENT '售后状态',
  reject_reason VARCHAR(255) COMMENT '驳回原因',

  apply_time DATETIME NOT NULL,
  audit_time DATETIME COMMENT '审核时间',
  finish_time DATETIME COMMENT '完成时间',

  create_time DATETIME NOT NULL,
  update_time DATETIME NOT NULL,

  UNIQUE KEY uk_after_sale_sn(after_sale_sn),
  INDEX idx_order_id(order_id),
  INDEX idx_user_id(user_id),
  INDEX idx_status(status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='售后主表';

3.2 售后商品明细表 ls_after_sale_item

记录本次售后涉及的具体商品与数量,支持部分退换。

CREATE TABLE ls_after_sale_item (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  after_sale_id BIGINT NOT NULL,
  order_item_id BIGINT NOT NULL COMMENT '原订单商品明细ID',
  goods_id BIGINT NOT NULL,
  sku_id BIGINT NOT NULL,
  goods_name VARCHAR(255) NOT NULL,
  sku_specs VARCHAR(128) COMMENT '规格信息',
  goods_image VARCHAR(255),

  quantity INT NOT NULL COMMENT '退换数量',
  unit_price DECIMAL(10,2) NOT NULL COMMENT '单价',
  total_amount DECIMAL(10,2) NOT NULL COMMENT '该商品退款金额',

  INDEX idx_after_sale_id(after_sale_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='售后商品明细表';

3.3 售后操作日志表 ls_after_sale_log

全流程留痕,每一次状态变更都记录,纠纷时可追溯。

CREATE TABLE ls_after_sale_log (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  after_sale_id BIGINT NOT NULL,
  from_status TINYINT NOT NULL,
  to_status TINYINT NOT NULL,
  action VARCHAR(32) NOT NULL COMMENT '操作动作:apply/approve/reject/deliver/receive/refund',
  operator_type TINYINT NOT NULL COMMENT '1用户 2商家 3系统',
  operator_id BIGINT,
  remark VARCHAR(255),
  create_time DATETIME NOT NULL,
  INDEX idx_after_sale_id(after_sale_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='售后操作日志表';

3.4 退货物流表 ls_after_sale_express

退货退款与换货场景下,记录用户寄回的物流信息。

CREATE TABLE ls_after_sale_express (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  after_sale_id BIGINT NOT NULL UNIQUE,
  express_company VARCHAR(32),
  express_no VARCHAR(32),
  deliver_time DATETIME COMMENT '用户寄出时间',
  receive_time DATETIME COMMENT '商家收货时间',
  receive_remark VARCHAR(255) COMMENT '收货备注',
  INDEX idx_after_sale_id(after_sale_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='退货物流表';

3.5 原订单表扩展

原生订单表仅增加一个标记字段,不破坏原有结构。

ALTER TABLE ls_order 
ADD COLUMN after_sale_status TINYINT DEFAULT 0 COMMENT '售后状态:0无售后 1售后中 2部分售后 3全部售后'
AFTER order_status;

四、核心业务流程与代码实现

所有核心操作封装在独立的 AfterSaleService 中,通过事件驱动联动订单、库存、支付模块,不侵入原生业务代码。

4.1 用户提交售后申请

校验订单状态、商品可退数量、金额计算,生成售后单与明细。

@Service
public class AfterSaleServiceImpl implements AfterSaleService {

    @Autowired
    private AfterSaleMapper afterSaleMapper;
    @Autowired
    private AfterSaleItemMapper itemMapper;
    @Autowired
    private OrderService orderService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public AfterSaleVO apply(AfterSaleApplyDTO dto, Long userId) {
        // 1. 校验原订单状态:已付款、未完成或已完成且在售后期内
        Order order = orderService.getById(dto.getOrderId());
        if (!order.isAfterSaleAllowed()) {
            throw new BizException("当前订单状态不可申请售后");
        }

        // 2. 校验商品可退数量,防止超额申请
        List<AfterSaleItemDTO> items = dto.getItems();
        checkReturnQuantity(order, items);

        // 3. 计算退款金额(含优惠分摊、运费计算)
        BigDecimal refundAmount = calcRefundAmount(order, items);
        BigDecimal refundFreight = calcRefundFreight(order, dto);

        // 4. 生成售后单号
        String afterSaleSn = SnGenerator.generate("AS");

        // 5. 写入售后主表
        AfterSale afterSale = new AfterSale();
        afterSale.setAfterSaleSn(afterSaleSn);
        afterSale.setOrderId(order.getId());
        afterSale.setOrderSn(order.getOrderSn());
        afterSale.setUserId(userId);
        afterSale.setType(dto.getType());
        afterSale.setReason(dto.getReason());
        afterSale.setDescription(dto.getDescription());
        afterSale.setImages(dto.getImages());
        afterSale.setRefundAmount(refundAmount);
        afterSale.setRefundFreight(refundFreight);
        afterSale.setStatus(AfterSaleStatus.PENDING_AUDIT.getCode());
        afterSale.setApplyTime(new Date());
        afterSaleMapper.insert(afterSale);

        // 6. 写入售后商品明细
        batchInsertItems(afterSale.getId(), items, order);

        // 7. 记录操作日志
        saveLog(afterSale.getId(), null, AfterSaleStatus.PENDING_AUDIT,
                "apply", OperatorType.USER, userId, "提交售后申请");

        // 8. 标记订单进入售后状态
        orderService.markAfterSaleStatus(order.getId(), AfterSaleOrderStatus.PROCESSING);

        // 9. 发布售后申请事件,通知商家
        applicationEventPublisher.publishEvent(new AfterSaleAppliedEvent(afterSale));

        return BeanCopyUtils.copy(afterSale, AfterSaleVO.class);
    }
}

4.2 商家审核

同意或驳回,校验当前状态,同步触发后续流程。

@Override
@Transactional(rollbackFor = Exception.class)
public void audit(Long afterSaleId, boolean pass, String reason, Long adminId) {
    AfterSale afterSale = afterSaleMapper.selectById(afterSaleId);
    if (!afterSale.getStatus().equals(AfterSaleStatus.PENDING_AUDIT.getCode())) {
        throw new BizException("当前状态不可审核");
    }

    Integer fromStatus = afterSale.getStatus();

    if (pass) {
        // 审核通过
        if (afterSale.getType() == AfterSaleType.REFUND_ONLY) {
            // 仅退款:直接进入待退款
            afterSale.setStatus(AfterSaleStatus.PENDING_REFUND.getCode());
            // 异步发起退款
            asyncRefund(afterSale);
        } else {
            // 退货/换货:待用户退货
            afterSale.setStatus(AfterSaleStatus.WAIT_DELIVER.getCode());
        }
        afterSale.setAuditTime(new Date());
    } else {
        // 驳回
        afterSale.setStatus(AfterSaleStatus.REJECTED.getCode());
        afterSale.setRejectReason(reason);
    }

    afterSaleMapper.updateById(afterSale);

    // 记录日志
    saveLog(afterSaleId, fromStatus, afterSale.getStatus(),
            pass ? "approve" : "reject", OperatorType.MERCHANT, adminId, reason);

    // 通知用户审核结果
    notifyUserAuditResult(afterSale);
}

4.3 仓库收货确认与退款执行

用户寄回商品,仓库核验无误后,确认收货并触发退款。

@Override
@Transactional(rollbackFor = Exception.class)
public void confirmReceive(Long afterSaleId, String remark, Long adminId) {
    AfterSale afterSale = afterSaleMapper.selectById(afterSaleId);
    if (!afterSale.getStatus().equals(AfterSaleStatus.WAIT_RECEIVE.getCode())) {
        throw new BizException("当前状态不可确认收货");
    }

    Integer fromStatus = afterSale.getStatus();

    if (afterSale.getType() == AfterSaleType.RETURN_REFUND) {
        // 退货退款:进入待退款
        afterSale.setStatus(AfterSaleStatus.PENDING_REFUND.getCode());
        afterSaleMapper.updateById(afterSale);
        // 发起原路退款
        asyncRefund(afterSale);

    } else if (afterSale.getType() == AfterSaleType.EXCHANGE) {
        // 换货:进入待发货,商家重新发新品
        afterSale.setStatus(AfterSaleStatus.WAIT_SHIP.getCode());
        afterSaleMapper.updateById(afterSale);
        // 触发换货发货通知
        applicationEventPublisher.publishEvent(new ExchangeShipEvent(afterSale));
    }

    // 回补库存(退货入库)
    restockAfterReturn(afterSale);

    saveLog(afterSaleId, fromStatus, afterSale.getStatus(),
            "receive", OperatorType.MERCHANT, adminId, remark);
}

4.4 退款成功回调

支付通道退款成功后,更新售后单状态,同步订单售后标记。

@Override
@Transactional(rollbackFor = Exception.class)
public void onRefundSuccess(String afterSaleSn, String refundFlowNo) {
    AfterSale afterSale = afterSaleMapper.selectBySn(afterSaleSn);
    if (afterSale.getStatus().equals(AfterSaleStatus.REFUNDED.getCode())) {
        return; // 幂等
    }

    Integer fromStatus = afterSale.getStatus();
    afterSale.setStatus(AfterSaleStatus.REFUNDED.getCode());
    afterSale.setFinishTime(new Date());
    afterSale.setRefundFlowNo(refundFlowNo);
    afterSaleMapper.updateById(afterSale);

    // 更新订单售后状态
    updateOrderAfterSaleStatus(afterSale.getOrderId());

    // 记录日志
    saveLog(afterSale.getId(), fromStatus, AfterSaleStatus.REFUNDED,
            "refund_success", OperatorType.SYSTEM, null, "退款成功");

    // 通知用户
    notifyUserRefundSuccess(afterSale);
}

五、上下游模块联动

售后不是孤立模块,必须和订单、库存、支付、积分等模块联动,才能保证数据一致。

5.1 订单模块

  • 售后申请成功,订单标记「售后中」;
  • 所有售后单完结后,更新订单最终售后状态(部分售后/全部售后);
  • 不直接修改原订单金额与支付状态,资金变动以售后单为准,避免打乱原生订单统计。

5.2 库存模块

  • 仅退款不涉及库存变动;
  • 退货退款、换货收货后,商品无损坏则回补可售库存;
  • 损坏商品计入残次品库存,不回补可售库存;
  • 换货发货时扣减新商品库存,走正常出库逻辑。

5.3 支付退款模块

复用 LikeShop 原生的支付退款能力,售后模块只发起退款请求,不重复对接支付通道。退款异步回调通过事件通知售后模块更新状态,解耦两个模块。

5.4 积分与优惠券

  • 退款时按比例回退使用的积分与优惠券;
  • 积分退回用户账户,优惠券退回至「已使用」状态不可二次使用,防止薅羊毛;
  • 金额计算与优惠分摊逻辑与下单时保持一致,避免精度误差。

5.5 定时任务兜底

  • 商家审核超时:超过配置时长未处理,系统自动同意售后申请;
  • 用户退货超时:审核通过后超时未填写物流,自动关闭售后单;
  • 退款失败重试:退款异常的订单,定时重试,超过次数转人工处理。

六、平滑升级与兼容方案

6.1 升级步骤

  1. 执行新增表 SQL 与订单表扩展字段 SQL,不修改原生核心字段;
  2. 部署售后模块独立代码包,原有订单、支付代码保持不动;
  3. 后台先开启「售后开关」灰度,初期仅部分商品类目开放;
  4. 验证退款、库存、金额计算全链路正确后,全量放开。

6.2 历史数据兼容

  • 历史订单默认售后状态为 0,不影响原有数据;
  • 在售后期内的历史订单,允许发起售后,逻辑与新订单一致;
  • 原有原生退款订单,不迁移到售后表,保留在原订单体系中,统计时合并口径即可。

6.3 官方版本升级兼容

  • 核心改造均为新增表与新增 Service,极少修改原生文件;
  • 官方版本迭代时,重点核对订单状态、支付退款接口是否有变动,同步适配即可;
  • 状态机与事件驱动的设计,最大程度降低了版本升级的冲突概率。

七、常见坑与避坑建议

1. 状态跳转混乱

现象:售后状态随便改,后台直接更新状态字段,导致流程错乱。
避坑:所有状态变更统一走 AfterSaleService 的状态机方法,前置校验当前状态,禁止直接 updateById 改状态。

2. 退款金额计算错误

现象:优惠、运费分摊计算不对,退款金额和实付金额对不上。
避坑:复用订单下单时的分摊逻辑,按商品实付金额占比分摊优惠,保证正向逆向计算口径一致。

3. 退款幂等失效

现象:支付回调重复通知,导致多次退款或状态错乱。
避坑:按售后单号做幂等校验,退款成功后重复回调直接返回成功,不重复执行业务。

4. 库存回退重复

现象:退货入库和退款回调都触发库存回补,导致库存多增加。
避坑:明确唯一触发点,退货收货时统一回补库存,退款成功不再重复操作。

5. 超额售后

现象:一个商品多次发起售后,累计退换数量超过购买数量。
避坑:申请时校验历史已成功售后的商品数量,超额直接拦截。

结语

售后退换货的二开,看起来是加几个状态的事,本质是一套独立的逆向交易体系。

LikeShop 单商户 Java 版原生的订单、支付、库存能力已经很扎实,二开时不用推翻重构,通过新增独立售后单、状态机驱动、事件联动原生能力的方式,可以用最小的侵入成本实现完整的退换货流程,同时保留后续官方版本的可升级性。

对二开团队来说,核心原则始终是:能扩展就不修改,能解耦就不耦合。架构上分层做扎实了,后续再加运费险、上门取件、极速退款等玩法,也都是在这套骨架上做增量。

目录
相关文章
|
2天前
|
人工智能 监控 安全
个人智能体真能管事了:用了三天灵光 2.0,我把盯新闻、盯货、翻账单都交给了它
作者分享使用蚂蚁灵光2.0个人智能体三天的体验:通过“帮我管”功能创建长期任务,让智能体自动盯AI新闻并整理选题写入飞书、监控iPhone Duo库存并在到货时推送、以及翻查账单。文章重点描述了资讯值守任务如何帮助作者第一时间捕捉Kimi K3.1 Pro发布消息,体现了个人智能体从聊天工具向主动事务管理工具的转变。
|
2天前
|
存储 人工智能 弹性计算
阿里云服务器2核2G3M99元一年与2核4G5M199元一年活动详解
阿里云推出99元/年(经济型e实例2核2G、3M带宽、40G ESSD)与199元/年(通用算力型u1实例2核4G、5M带宽、80G ESSD)两款低价长效云服务器。99元适合个人学习、小型网站等轻负载场景,199元限企业认证用户。活动截止2029年3月31日,每服务年可低价续费1次,权益与配置绑定,变配或释放可能丧失资格,购买需实名认证且通常不可叠加优惠券。
|
2天前
|
安全 网络协议 网络安全
Palo Alto PAN-OS 12.1.7 VM-Series for ESXi – 基于机器学习的下一代防火墙操作系统
PaloAltoNetworks的PAN-OS是其下一代防火墙(NGFW)的操作系统,通过App-ID、Content-ID、设备ID和用户ID等技术,提供全面的网络流量控制和管理,确保安全策略的有效执行‌。PAN-OS的核心设计理念是通过深度集成多种安全功能,提供更高效、更智能的威胁防护。PaloAltoPAN-OS功能特点-了解应用程序的真实身份PAN-OS包括App-ID™—我们的专利流量分类技术,可自动发现和控制新应用程序,即使是那些试图通过伪装成合法流量、跳跃端口,或通过加密潜入防火墙来规避检测的应用程序。
29 2
|
2天前
|
SQL 人工智能 自然语言处理
夯爆了!分享几个我在用的,基于业务场景接口串联测试Skill !
本文介绍AI驱动的业务场景接口串联测试新范式,填补单接口测试无法覆盖的“数据传递、状态流转、一致性、规则联动”四大缺陷。通过`api-scenario-modeler`将自然语言业务流程(如注册→登录→下单→支付)自动建模为标准化YAML编排文件,实现端到端链路验证,真正保障用户核心业务流程通畅。
83 2
夯爆了!分享几个我在用的,基于业务场景接口串联测试Skill !
|
2天前
|
存储 人工智能 弹性计算
阿里云双十一大促2026年活动总结:企业云产品特惠有哪些,三大入口与四类权益一文讲透
2026年阿里云双十一是面向企业上云、开发者实践与AI应用落地的一次集中权益释放,主题为“智惠就在11.11,AI就用阿里云”,活动周期持续至11月30日。它不是单纯的价格促销,而是围绕企业数字化建设与云计算成本优化展开的综合活动,覆盖 阿里云轻量应用服务器、阿里云ECS云服务器、阿里云GPU云服务器、阿里云大模型服务平台 、AI大模型、应用开发、企业上云、出海服务等关键场景。
47 1
|
2天前
|
人工智能 缓存 安全
手机 AI Agent 的端云分工:为什么画面留在本地、决策放上云
从工程视角拆解手机AI Agent端云分工:端侧负责感知执行与脱敏,云端负责意图规划与状态管理,含通信容错与敏感数据保护设计
|
2天前
|
弹性计算 缓存 监控
一次误发之后的十分钟:把撤回做成可回退的日常操作
本文复盘一次误发消息后的10分钟应急操作,将“撤回”转化为可回退的标准化流程:从判断回合闭合、评估撤回代价,到精准修改、版本比对与上下文验证,并适配ECS部署要点。适合需留痕、多轮迭代的排障与客户协作场景。(239字)
31 1
|
2天前
|
人工智能 前端开发 JavaScript
解决 UI 自动化定位痛点 ! 我封装了这套Skill
做UI自动化测试的不少同学可能都有遇到过这种问题:脚本运行极其脆弱,前端稍微调整页面样式、DOM 结构、文案,整套定位代码直接大面积报错,后续维护成本翻倍。 目前稳定的元素定位优先级里, data-testid 是大家比较认可的,它和样式、页面结构解耦,只要标识不变,无论前端怎么重构页面,自动化脚本都无需改动,是保障用例长期稳定运行的最优解。 元素定位稳定的优先级如下: 理论上我们只需要让前端在交互元素上统一补充 data-testid 就能解决问题,但实际情况总是不理想: 1. 前端开发日常业务排期紧张,额外增加埋点属性会占用开发时间,普遍抵触; 2. 团队缺少统一规范,手动新增容易出现命名
38 0
|
2天前
查询sqlserver大表语句
查询sqlserver大表语句
39 0
|
2天前
|
人工智能 自然语言处理 搜索推荐
什么是跨语种语义蒸馏:一份可落地的工程口径(以盖立克思公开体系为样本)
同一个品牌,中文页面写"年产能 12 万吨",英文页面写 "120,000 tons per year",再经一次转译可能又变成另一个数。这类口径漂移在多语种站点里很普遍,而生成式引擎是按语种分别检索语料的,口径对不上的品牌可能被当成两个主体,也可能被挑一个"看起来更权威"的说法,反而失真。本文给"跨语种语义蒸馏"下一个可操作的工程定义,拆解从事实底稿、派生生成到一致性校验的实现路径,并附可运行的校验代码。文中样本取自杭州盖立克思(New Galaxy AI)公开披露的技术体系,仅作方法拆解,不构成推荐或效果承诺。
40 0

热门文章

最新文章