PHP项目怎样实现订单退款审核?

wen PHP项目 26

本文目录导读:

PHP项目怎样实现订单退款审核?

  1. 核心数据表设计
  2. 后端流程与核心逻辑
  3. 权限与角色控制
  4. 前端流程建议
  5. 需要考虑的关键点
  6. 简化版状态流转图

在 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' => ''];
}

处理异步通知(重要)

支付网关的退款通常是异步确认的,如果你的平台允许直接通过(即时到账),则上述同步处理即可;但更严谨的方式是:

  1. 审核通过时,先修改状态为“退款中”,标记为status=4
  2. 调用网关的退款接口。
  3. 等待支付网关的回调(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, '无权限审核退款');
    }
    // ... 后续逻辑
}

前端流程建议

  1. 用户端:提供“申请退款”按钮(仅在特定状态显示),展示退款进度(待审核 → 通过/拒绝)。
  2. 管理端:专门的退款审核列表,每条记录有“通过” / “拒绝”按钮,拒绝时需要填写原因。

需要考虑的关键点

要点 说明
幂等性 退款接口需要确保不会重复执行,最好在退款表中记录第三方退款流水号,或使用订单号+退款状态的唯一索引。
金额精度 使用 decimal 类型存储,计算时注意浮点误差,PHP中可使用 bccomp 函数比较金额。
事务处理 更新退款申请状态、订单状态、扣减库存(如有)应当放在数据库事务中,保证原子性。
日志记录 记录审核人、操作时间、审核结果等日志,方便追溯。
退款次数 支持部分退款时,需要累计已退金额,并确保累计不超过订单实付金额。
支付网关配置 微信/支付宝的退款证书、API密钥等敏感信息存储在配置文件中,不要硬编码。

简化版状态流转图

用户下单并支付
     ↓
用户申请退款  → 状态:订单 refund_status = 1
     ↓
管理员审核
 ├── 通过 → 调用支付网关退款
 │          ├── 成功 → 状态:订单 refund_status = 2,退款单 status = 2
 │          └── 失败 → 状态:订单 refund_status = -1,退款单 status = -1 (需人工处理)
 └── 拒绝 → 状态:订单 refund_status = 3,退款单 status = 3

实现退款审核的核心步骤是:

  1. 数据层:张建退款申请表,记录申请、审核、执行全流程状态。
  2. 审核逻辑:管理员审核通过后,调用支付网关实际扣款(或等待异步回调)。
  3. 安全控制:严格权限校验,防止越权操作。
  4. 异常处理:处理好退款失败、部分退款、重复请求等边界情况。

如果你使用的框架(如 Laravel、ThinkPHP)有现成的状态机插件(如 spatie/laravel-model-states),可以进一步简化状态管理。

抱歉,评论功能暂时关闭!