PAYATHON 2026

如何自定义 CS Cockpit 退款对话框并添加复选框?

支付小周

结论

CS Cockpit 的退款对话框通常无法通过简单配置直接添加业务字段。比较可靠的做法是在自定义扩展中覆盖退款对话框的视图和控制器,添加复选框或单选项,再将用户的选择传给独立的退款服务。

服务层需要明确区分两条退款路径:

  • 原路退款:调用现有的 payment gateway 退款流程。
  • 账户余额:创建 customer credit 记录,不调用支付网关。

不要直接修改 cscockpit 扩展的原始文件,这些修改在升级或重新部署时很容易被覆盖。

实现思路

具体的 Widget、Controller 和 ZUL 文件名取决于 SAP Commerce 版本及项目定制情况。下面的示例只用于说明推荐的代码结构。实际使用时,需要将类名和配置位置替换为当前项目退款对话框所使用的实现。

建议定义清晰的退款方式枚举,不要让底层业务逻辑直接依赖复选框的布尔值:

public enum RefundDestination
{
    PAYMENT_GATEWAY,
    CUSTOMER_CREDIT
}

以后如果要增加礼品卡、线下退款等方式,也不必继续添加布尔参数。

在对话框中增加选项

如果当前 CS Cockpit 的对话框使用 ZK/ZUL,可以在自定义 ZUL 文件中添加退款方式选择控件:

<radiogroup id="refundDestination">
    <radio
        id="paymentGatewayOption"
        label="原支付方式退款"
        value="PAYMENT_GATEWAY"
        checked="true"/>

    <radio
        id="customerCreditOption"
        label="存入客户账户余额"
        value="CUSTOMER_CREDIT"/>
</radiogroup>

需求虽然提到了复选框,但这两种处理方式互斥,使用单选按钮更合适。如果界面必须使用复选框,可以这样定义:

<checkbox
    id="storeAsCustomerCredit"
    label="将退款存入客户账户余额"/>

此时需要明确约定:

  • 未选中:通过 payment gateway 退款。
  • 已选中:存入 customer account credit。

界面上也应补充相应说明,以免客服人员误解默认行为。

从控制器读取选择结果

确认退款时,控制器读取控件状态,并将退款方式传入应用服务:

public void confirmRefund()
{
    validateRefund();

    final RefundDestination destination =
            storeAsCustomerCredit.isChecked()
                    ? RefundDestination.CUSTOMER_CREDIT
                    : RefundDestination.PAYMENT_GATEWAY;

    refundFacade.refund(
            order.getCode(),
            refundAmount,
            destination,
            currentUserService.getCurrentUser().getUid()
    );

    closeDialog();
}

如果使用单选按钮:

public void confirmRefund()
{
    validateRefund();

    final String selectedValue =
            refundDestination.getSelectedItem().getValue();

    final RefundDestination destination =
            RefundDestination.valueOf(selectedValue);

    refundFacade.refund(
            order.getCode(),
            refundAmount,
            destination,
            currentUserService.getCurrentUser().getUid()
    );

    closeDialog();
}

退款逻辑不能只在界面层切换。退款方式必须作为显式参数传入 Facade 或 Service,否则后台任务、重试流程和审计记录无法判断当时选择了哪种处理方式。

在服务层分流退款

可以为两种退款方式定义一个统一入口:

public interface RefundFacade
{
    void refund(
            String orderCode,
            BigDecimal amount,
            RefundDestination destination,
            String operatorUid);
}

示例实现如下:

public class DefaultRefundFacade implements RefundFacade
{
    private PaymentRefundService paymentRefundService;
    private CustomerCreditService customerCreditService;
    private RefundAuditService refundAuditService;

    @Override
    public void refund(
            final String orderCode,
            final BigDecimal amount,
            final RefundDestination destination,
            final String operatorUid)
    {
        validateAmount(amount);

        switch (destination)
        {
            case PAYMENT_GATEWAY:
                paymentRefundService.refund(orderCode, amount);
                break;

            case CUSTOMER_CREDIT:
                customerCreditService.credit(orderCode, amount);
                break;

            default:
                throw new IllegalArgumentException(
                        "Unsupported refund destination: " + destination);
        }

        refundAuditService.record(
                orderCode,
                amount,
                destination,
                operatorUid);
    }

    protected void validateAmount(final BigDecimal amount)
    {
        if (amount == null || amount.signum() <= 0)
        {
            throw new IllegalArgumentException(
                    "Refund amount must be greater than zero");
        }
    }

    public void setPaymentRefundService(
            final PaymentRefundService paymentRefundService)
    {
        this.paymentRefundService = paymentRefundService;
    }

    public void setCustomerCreditService(
            final CustomerCreditService customerCreditService)
    {
        this.customerCreditService = customerCreditService;
    }

    public void setRefundAuditService(
            final RefundAuditService refundAuditService)
    {
        this.refundAuditService = refundAuditService;
    }
}

PaymentRefundService、CustomerCreditService 和 RefundAuditService 都是示例接口,并非所有 SAP Commerce 版本都提供这些标准 API。项目实施时,应接入已有的支付事务服务、退款流程和账户余额模型。

注册自定义实现

在自定义扩展的 Spring 配置中注册控制器和服务。如果原退款对话框由 Spring Bean 创建,可以使用同名 Bean 覆盖原实现,或者替换相关依赖:

<bean id="customRefundDialogController"
      class="com.example.cscockpit.refund.CustomRefundDialogController"
      parent="defaultRefundDialogController">

    <property name="refundFacade" ref="refundFacade"/>
</bean>

<bean id="refundFacade"
      class="com.example.cscockpit.refund.impl.DefaultRefundFacade">

    <property name="paymentRefundService"
              ref="paymentRefundService"/>

    <property name="customerCreditService"
              ref="customerCreditService"/>

    <property name="refundAuditService"
              ref="refundAuditService"/>
</bean>

这里的 defaultRefundDialogController 和 paymentRefundService 只用于说明配置结构。实施前,需要在当前版本中确认以下内容:

  1. 打开退款对话框的是哪个 Widget 或 Action。
  2. 对话框对应哪个 ZUL 文件。
  3. 控制器的 Bean ID。
  4. 当前退款提交最终调用哪个 Facade 或 Service。
  5. Spring 配置是否允许通过加载顺序覆盖 Bean。

如果无法安全地覆盖原 Bean,可以注册新的退款 Action,再通过 CS Cockpit 配置用新 Action 替换原 Action。

客户账户余额的数据设计

“存入 customer account”不一定对应平台自带的通用余额功能。如果项目中没有现成的 store credit 模型,就需要自行设计,例如:

public interface CustomerCreditService
{
    void credit(String orderCode, BigDecimal amount);
}

余额流水至少需要保存:

  • 客户标识;
  • 原订单和退款记录;
  • 金额及币种;
  • 收支方向;
  • 创建时间;
  • 操作人;
  • 业务状态;
  • 唯一退款请求号;
  • 备注或原因。

不要只修改客户模型中的余额数字。每次余额变动都应生成不可变的流水记录,供审计、对账和并发更新处理使用。

注意事项

  • 原路退款与账户余额入账必须互斥,一次操作不能同时执行两种处理。
  • 服务端必须重新校验退款金额、币种、订单状态和可退款余额,不能只依赖界面校验。
  • 每个退款请求都应有唯一业务标识,防止客服重复点击或系统重试造成二次退款。
  • 记录退款方式、操作人、时间、金额和支付网关返回结果。
  • 账户余额入账与本地退款状态更新应处于同一事务边界。
  • 调用外部 payment gateway 时,不能简单依靠数据库事务回滚。还需要处理超时、未知状态、查询确认和补偿。
  • 只有支付网关明确返回成功后,才能将原路退款标记为成功。
  • 如果账户余额支持跨币种使用,需要事先确定汇率、精度和舍入规则。
  • 对话框的默认值应保持现有行为。如果系统原来默认通过 payment gateway 退款,新控件也应默认选择该方式。
  • 检查客服角色权限,避免所有 CS Cockpit 用户都能执行账户余额入账。
  • 修改完成后,至少要测试全额退款、部分退款、重复提交、网关失败、余额入账失败和并发退款等场景。

如果项目实际使用的是 Backoffice Customer Support Cockpit,而不是旧版 CS Cockpit,界面通常基于 Backoffice Widget 和 ZK MVVM,覆盖方式会有所不同。不过整体设计仍然一致:在视图中增加字段,由控制器传递退款方式,再由服务层完成分流。

备注:内容仅供参考。