如何自定义 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 只用于说明配置结构。实施前,需要在当前版本中确认以下内容:
- 打开退款对话框的是哪个 Widget 或 Action。
- 对话框对应哪个 ZUL 文件。
- 控制器的 Bean ID。
- 当前退款提交最终调用哪个 Facade 或 Service。
- 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,覆盖方式会有所不同。不过整体设计仍然一致:在视图中增加字段,由控制器传递退款方式,再由服务层完成分流。
备注:内容仅供参考。
