prestashop 1.6 管理员部分退款时如何调用 Hook
结论
PrestaShop 1.6 已提供 actionOrderSlipAdd Hook。管理员执行标准退款或部分退款,并且系统成功创建 credit slip(退款单)后,就会调用这个 Hook。一般不需要修改核心文件,也不建议覆盖 AdminOrdersController。
安装模块时注册 Hook:
public function install()
{
return parent::install()
&& $this->registerHook('actionOrderSlipAdd');
}
然后在模块主类中实现:
public function hookActionOrderSlipAdd(array $params)
{
// 处理退款
}
注意,actionOrderSlipAdd 只表示退款单已经创建,不代表支付网关已经成功退款。支付模块仍需自行调用网关退款接口,并处理幂等、异常记录和重试。
Hook 能覆盖哪些情况
管理员在订单页面执行部分退款时,系统最终会创建 OrderSlip。以下操作通常都会触发 actionOrderSlipAdd:
- 退还部分商品数量;
- 退还某个商品的部分金额;
- 退还部分运费;
- 通过退款流程处理优惠金额。
这些操作不会分别触发不同的 Hook,而是统一进入 actionOrderSlipAdd。
PrestaShop 1.6 各小版本对 Hook 参数和退款单字段的处理有所不同。常见参数包括:
$params['order']
$params['productList']
$params['qtyList']
在部分退款流程中,productList 通常以 id_order_detail 为键,其中包含退款数量、退款金额等信息。普通退款流程则可能把数量放在 qtyList 中。因此,不能假设所有 1.6 版本的参数结构都相同,需要根据实际安装的版本做兼容处理。
模块实现示例
下面的代码先验证订单对象,再统一整理退款商品,最后将支付网关退款交给独立方法处理:
public function hookActionOrderSlipAdd(array $params)
{
if (
empty($params['order'])
|| !($params['order'] instanceof Order)
|| !Validate::isLoadedObject($params['order'])
) {
return;
}
/** @var Order $order */
$order = $params['order'];
$productList = isset($params['productList'])
&& is_array($params['productList'])
? $params['productList']
: array();
$qtyList = isset($params['qtyList'])
&& is_array($params['qtyList'])
? $params['qtyList']
: array();
$refundedProducts = array();
foreach ($productList as $idOrderDetail => $refundData) {
$idOrderDetail = (int) $idOrderDetail;
if (is_array($refundData)) {
// 部分退款常见结构
$quantity = isset($refundData['quantity'])
? (int) $refundData['quantity']
: 0;
$amount = isset($refundData['amount'])
? (float) $refundData['amount']
: 0.0;
} else {
// 某些退款路径只传商品列表,数量放在 qtyList
$quantity = isset($qtyList[$idOrderDetail])
? (int) $qtyList[$idOrderDetail]
: (int) $refundData;
$amount = null;
}
$orderDetail = new OrderDetail($idOrderDetail);
if (
!Validate::isLoadedObject($orderDetail)
|| (int) $orderDetail->id_order !== (int) $order->id
) {
continue;
}
$refundedProducts[] = array(
'id_order_detail' => $idOrderDetail,
'id_product' => (int) $orderDetail->product_id,
'quantity' => $quantity,
'amount' => $amount,
);
}
$this->processGatewayRefund($order, $refundedProducts, $params);
}
不要把所有退款逻辑都写在 Hook 方法中,可以将其单独封装:
protected function processGatewayRefund(
Order $order,
array $refundedProducts,
array $hookParams
) {
$transactionId = $this->findTransactionIdByOrder($order);
if (!$transactionId) {
PrestaShopLogger::addLog(
sprintf(
'Refund skipped: transaction not found for order %d.',
(int) $order->id
),
3
);
return;
}
$refundData = $this->buildRefundData(
$order,
$refundedProducts,
$hookParams
);
if ($refundData['amount'] <= 0) {
return;
}
$idempotencyKey = sprintf(
'order-%d-refund-%s',
(int) $order->id,
$refundData['reference']
);
if ($this->refundWasAlreadyProcessed($idempotencyKey)) {
return;
}
try {
$result = $this->refundPayment(
$transactionId,
$refundData['amount'],
$order->id_currency,
$idempotencyKey
);
$this->saveRefundResult(
$order,
$idempotencyKey,
$refundData,
$result
);
} catch (Exception $e) {
PrestaShopLogger::addLog(
sprintf(
'Payment refund failed for order %d: %s',
(int) $order->id,
$e->getMessage()
),
3
);
}
}
findTransactionIdByOrder()、buildRefundData() 和 refundPayment() 等方法,需要根据支付网关 API 及模块自身的交易表实现。
如何取得商品、运费和优惠退款金额
不要直接使用:
$order->total_paid
这个字段表示订单支付总额,不是本次退款金额。
本次退款数据可能来自两个地方:
productList中记录的商品退款数量或金额;- 新创建的
OrderSlip及其明细中保存的商品、税费和运费退款结果。
应优先使用退款单最终保存的金额。管理员填写的含税金额经过 PrestaShop 计算后,可能会出现舍入和税额差异。
不过,部分 PrestaShop 1.6 小版本不会通过 actionOrderSlipAdd 参数直接传入 OrderSlip 对象或退款单 ID。遇到这种情况,应先检查当前版本中 OrderSlip::createOrderSlip() 和 OrderSlip::createPartialOrderSlip() 调用 Hook 时传入的具体参数,再确定如何取得对应的退款单。
不要只查询该订单最新的一条退款单来关联记录。并发请求可能导致查询到错误的退款单。更稳妥的处理方式包括:
- 使用 Hook 已提供的退款单 ID 或对象;
- 如果当前版本没有提供,则在模块中保存能够唯一关联退款请求的记录;
- 或者使用范围尽可能小的
OrderSlip类覆盖,为 Hook 补充明确的退款单 ID。
处理优惠券退款时,还要区分两种情况:
- 退款金额实际计入 credit slip;
- 系统为客户重新生成 voucher/cart rule。
重新生成的优惠券不一定对应支付网关需要退回的现金金额,不能直接把新优惠券的面值作为网关退款金额。
幂等处理
后台管理员可能重复提交,页面也可能因超时而重试。如果网关请求已经成功,但页面响应失败,Hook 还可能再次触发。
建议为每次退款保存独立记录,例如:
id_order
id_order_slip
gateway_transaction_id
refund_amount
currency
idempotency_key
gateway_refund_id
status
request_payload
response_payload
date_add
date_upd
调用网关前先检查 idempotency_key。如果退款已经成功,就不应再次向网关发送请求。
还需要明确退款失败后的处理方式。actionOrderSlipAdd 执行时,PrestaShop 通常已经把退款单写入数据库。即使支付网关退款失败,仅从 Hook 返回 false 也不一定能撤销后台退款操作。因此,模块应记录失败状态,并提供后台重试或人工处理入口。
是否需要 override
只有在当前 PrestaShop 1.6 小版本没有提供足够的 Hook 参数,无法可靠取得本次 OrderSlip 时,才需要考虑 override。
应优先覆盖 OrderSlip,不要覆盖 AdminOrdersController::postProcess()。后者包含大量订单后台逻辑,复制整个方法难以维护,升级 PrestaShop 或安装其他模块后也容易发生冲突。
模块可以按照 PrestaShop 1.6 的 override 目录结构提供文件,例如:
modules/yourpayment/
└── override/
└── classes/
└── order/
└── OrderSlip.php
基本形式如下:
<?php
class OrderSlip extends OrderSlipCore
{
public static function createPartialOrderSlip(/* 使用当前版本的原始签名 */)
{
$result = parent::createPartialOrderSlip(/* 原样传递所有参数 */);
if ($result) {
// 仅补充原 Hook 缺少的信息,或调用模块服务。
// 不要再次创建退款单。
}
return $result;
}
}
这里无法给出适用于所有 PrestaShop 1.6 小版本的固定参数列表。不同版本的 createPartialOrderSlip() 方法签名和内部字段可能不同,必须复制当前安装版本的准确方法签名,并保持所有参数的默认值一致。
使用 override 时还要注意:
- PrestaShop override 是类级覆盖,并非只覆盖某个调用点;
- 多个模块覆盖同一个类时可能发生冲突;
- 安装或卸载 override 后,通常需要删除或重建
cache/class_index.php; - 升级 PrestaShop 后,必须重新核对父类方法签名;
- 不要在 override 中复制整段核心退款逻辑,应尽量调用
parent; - 卸载模块时应正确移除自身的 override,同时不能破坏其他模块的代码。
推荐的处理顺序是先使用 actionOrderSlipAdd,并兼容不同的参数结构。只有确实无法唯一确定本次退款单时,才增加一个尽可能简单的 OrderSlip override。
备注:内容仅供参考。
