本文目录导读:

在 PHP 项目中实现订单退款审核功能,通常涉及状态机设计、权限控制、金额计算以及与第三方支付网关的交互。
下面是一个典型的分步骤实现方案,涵盖数据库设计、后端逻辑、前端流程和第三方支付对接。
核心数据表设计
你需要扩展订单表或创建独立的退款表来管理审核流程。
订单表 orders (增加字段)
ALTER TABLE orders ADD COLUMN `refund_status` TINYINT DEFAULT 0 COMMENT '退款状态:0-无退款 1-申请中 2-审核通过 3-审核拒绝 -1-退款失败'; ALTER TABLE orders ADD COLUMN `refund_amount` DECIMAL(10,2) DEFAULT 0.00 COMMENT '已退款金额';
退款申请表 refund_requests (推荐)
这是审核流程的核心表。
CREATE TABLE `refund_requests` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `order_id` INT NOT NULL, `user_id` INT NOT NULL COMMENT '申请人(用户)ID', `admin_id` INT DEFAULT NULL COMMENT '审核人(管理员)ID', `amount` DECIMAL(10,2) NOT NULL COMMENT '申请退款金额', `reason` VARCHAR(500) NOT NULL COMMENT '退款原因', `refuse_reason` VARCHAR(500) DEFAULT NULL COMMENT '拒绝理由', `status` TINYINT DEFAULT 1 COMMENT '状态: 1-待审核 2-审核通过 3-审核拒绝', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX `idx_order_id` (`order_id`), INDEX `idx_status` (`status`), INDEX `idx_user_id` (`user_id`) );
后端流程与核心逻辑
用户发起退款申请
// UserRefundController.php
public function apply(Request $request)
{
$order = Order::findOrFail($request->order_id);
// 1. 校验订单状态:已支付、未申请过退款等
if ($order->status != 'paid') {
throw new \Exception('该订单状态不支持退款');
}
// 2. 校验退款金额不能超过实付金额
if ($request->amount > $order->pay_amount) {
throw new \Exception('退款金额不能超过实付金额');
}
// 3. 创建退款申请记录
$refund = new RefundRequest();
$refund->order_id = $order->id;
$refund->user_id = auth()->id();
$refund->amount = $request->amount;
$refund->reason = $request->reason;
$refund->status = 1; // 待审核
$refund->save();
// 4. 更新订单退款状态
$order->refund_status = 1; // 申请中
$order->save();
return response()->json(['message' => '退款申请已提交,请等待审核']);
}
管理员审核退款
这是流程的核心,审核通过后需要调用支付网关执行真正的退款,并处理成功/失败两种情况。
// AdminRefundController.php
public function audit(Request $request)
{
$refund = RefundRequest::with('order')->findOrFail($request->id);
if ($refund->status != 1) {
throw new \Exception('该申请已被处理');
}
if ($request->action == 'approve') {
// 1. 执行业务退款(调用第三方支付)
$result = $this->processPaymentRefund($refund);
if ($result['success']) {
// 2. 更新退款申请状态为通过
$refund->status = 2;
$refund->admin_id = auth()->id();
$refund->save();
// 3. 更新订单状态
$order = $refund->order;
$order->refund_status = 2; // 已退款
$order->refund_amount = $order->refund_amount + $refund->amount;
// 如果全额退款,可考虑将订单状态改为 refunded
if ($order->refund_amount >= $order->pay_amount) {
$order->status = 'refunded';
}
$order->save();
return response()->json(['message' => '退款成功']);
} else {
// 退款失败,标记异常
$refund->status = -1;
$refund->save();
$order = $refund->order;
$order->refund_status = -1;
$order->save();
return response()->json(['message' => '退款失败:'.$result['msg']], 500);
}
} else {
// 拒绝退款
$refund->status = 3;
$refund->refuse_reason = $request->refuse_reason;
$refund->admin_id = auth()->id();
$refund->save();
// 恢复订单退款状态
$order = $refund->order;
$order->refund_status = 3; // 拒绝
$order->save();
return response()->json(['message' => '已拒绝退款']);
}
}
// 调用第三方支付退款
private function processPaymentRefund($refund)
{
$order = $refund->order;
// 根据支付方式选择对应的处理类
if ($order->payment_method == 'alipay') {
// 调用支付宝退款API
// $response = AlipayTradeRefund::refund(...);
} elseif ($order->payment_method == 'wechat') {
// 调用微信退款API
// $response = WechatPay::refund(...);
}
// 返回成功或失败
return ['success' => true, 'msg' => ''];
}
处理异步通知(重要)
支付网关的退款通常是异步确认的,如果你的平台允许直接通过(即时到账),则上述同步处理即可;但更严谨的方式是:
- 审核通过时,先修改状态为“退款中”,标记为
status=4。 - 调用网关的退款接口。
- 等待支付网关的回调(Webhook/通知)确认退款成功后才最终更新状态。
// 异步回调处理
public function handleRefundNotify(Request $request)
{
// 解析支付网关的回调数据
$data = $request->all();
$orderId = $data['out_trade_no']; // 你的订单号
$refund = RefundRequest::where('order_id', $orderId)
->where('status', 4) // 退款中
->first();
if ($refund && $data['refund_status'] == 'SUCCESS') {
// 最终确认退款成功
$refund->status = 2;
$refund->save();
$order = $refund->order;
$order->refund_status = 2;
$order->save();
} elseif ($refund && $data['refund_status'] == 'FAIL') {
// 退款失败
$refund->status = -1;
$refund->save();
}
}
权限与角色控制
你必须确保只有管理员角色才能执行审核操作。
// 在控制器构造函数或路由中间件中
public function __construct()
{
$this->middleware('auth:admin'); // 管理员认证
$this->middleware('role:super_admin|finance'); // 财务角色权限
}
// 或者直接在方法中判断
public function audit(Request $request)
{
if (!auth()->user()->hasRole(['super_admin', 'finance'])) {
abort(403, '无权限审核退款');
}
// ... 后续逻辑
}
前端流程建议
- 用户端:提供“申请退款”按钮(仅在特定状态显示),展示退款进度(待审核 → 通过/拒绝)。
- 管理端:专门的退款审核列表,每条记录有“通过” / “拒绝”按钮,拒绝时需要填写原因。
需要考虑的关键点
| 要点 | 说明 |
|---|---|
| 幂等性 | 退款接口需要确保不会重复执行,最好在退款表中记录第三方退款流水号,或使用订单号+退款状态的唯一索引。 |
| 金额精度 | 使用 decimal 类型存储,计算时注意浮点误差,PHP中可使用 bccomp 函数比较金额。 |
| 事务处理 | 更新退款申请状态、订单状态、扣减库存(如有)应当放在数据库事务中,保证原子性。 |
| 日志记录 | 记录审核人、操作时间、审核结果等日志,方便追溯。 |
| 退款次数 | 支持部分退款时,需要累计已退金额,并确保累计不超过订单实付金额。 |
| 支付网关配置 | 微信/支付宝的退款证书、API密钥等敏感信息存储在配置文件中,不要硬编码。 |
简化版状态流转图
用户下单并支付
↓
用户申请退款 → 状态:订单 refund_status = 1
↓
管理员审核
├── 通过 → 调用支付网关退款
│ ├── 成功 → 状态:订单 refund_status = 2,退款单 status = 2
│ └── 失败 → 状态:订单 refund_status = -1,退款单 status = -1 (需人工处理)
└── 拒绝 → 状态:订单 refund_status = 3,退款单 status = 3
实现退款审核的核心步骤是:
- 数据层:张建退款申请表,记录申请、审核、执行全流程状态。
- 审核逻辑:管理员审核通过后,调用支付网关实际扣款(或等待异步回调)。
- 安全控制:严格权限校验,防止越权操作。
- 异常处理:处理好退款失败、部分退款、重复请求等边界情况。
如果你使用的框架(如 Laravel、ThinkPHP)有现成的状态机插件(如 spatie/laravel-model-states),可以进一步简化状态管理。