168 行
6.1 KiB
PHP
168 行
6.1 KiB
PHP
<?php
|
||
|
||
namespace app\common\library;
|
||
|
||
use think\Db;
|
||
use think\Log;
|
||
|
||
/**
|
||
* 订单取消与超时关闭服务
|
||
*
|
||
* 统一处理主动取消与超时自动关闭:
|
||
* 1. 状态由 pending 原子更新为 closed
|
||
* 2. 自动释放被锁定的优惠券
|
||
* 3. 关联业务的回收处理(如未支付的发布贴置为关闭,防止未支付生效)
|
||
* 4. 触发站内信通知
|
||
*/
|
||
class OrderClose
|
||
{
|
||
/** 默认订单超时时间(秒):30分钟 */
|
||
const DEFAULT_TIMEOUT_SECONDS = 1800;
|
||
|
||
/**
|
||
* 获取系统待支付订单超时时长(秒)
|
||
* 优先读取配置 trade.order_timeout 或 order.timeout_minutes(单位:分钟),默认 30 分钟
|
||
*
|
||
* @return int
|
||
*/
|
||
public static function getTimeoutSeconds()
|
||
{
|
||
$minutes = Db::name('fc_config')->where('key', 'order.timeout_minutes')->value('value');
|
||
if ($minutes === null || $minutes === '') {
|
||
$minutes = Db::name('fc_config')->where('key', 'trade.order_timeout')->value('value');
|
||
}
|
||
$val = (int)$minutes;
|
||
if ($val > 0) {
|
||
return $val * 60;
|
||
}
|
||
return self::DEFAULT_TIMEOUT_SECONDS;
|
||
}
|
||
|
||
/**
|
||
* 判断订单是否超时
|
||
*
|
||
* @param array $order fc_order 行
|
||
* @param int|null $now 当前时间戳
|
||
* @return bool
|
||
*/
|
||
public static function isExpired(array $order, $now = null)
|
||
{
|
||
if (empty($order) || (string)$order['status'] !== 'pending') {
|
||
return false;
|
||
}
|
||
$now = $now ?: time();
|
||
$createtime = isset($order['createtime']) ? (int)$order['createtime'] : 0;
|
||
if ($createtime <= 0) {
|
||
return false;
|
||
}
|
||
$timeout = self::getTimeoutSeconds();
|
||
return ($now - $createtime) >= $timeout;
|
||
}
|
||
|
||
/**
|
||
* 关闭订单(原子操作,支持主动取消或超时自动关闭)
|
||
*
|
||
* @param array $order fc_order 行(至少含 id、order_no、user_id、type、status、related_id)
|
||
* @param string $reason 关闭原因:user_cancel(用户主动取消)、timeout_auto(超时自动关闭)、admin_cancel(后台取消)
|
||
* @return bool 是否成功关闭
|
||
*/
|
||
public static function close(array $order, $reason = 'user_cancel')
|
||
{
|
||
$orderId = isset($order['id']) ? (int)$order['id'] : 0;
|
||
$orderNo = isset($order['order_no']) ? (string)$order['order_no'] : '';
|
||
if ($orderId <= 0 || $orderNo === '') {
|
||
return false;
|
||
}
|
||
|
||
$now = time();
|
||
$closed = false;
|
||
|
||
try {
|
||
Db::transaction(function () use ($orderId, $orderNo, $order, $reason, $now, &$closed) {
|
||
// 条件更新,保证只有 pending 状态的订单才能转为 closed,天然防并发与重复关闭
|
||
$affected = Db::name('fc_order')
|
||
->where('id', $orderId)
|
||
->where('status', 'pending')
|
||
->update([
|
||
'status' => 'closed',
|
||
'refund_reason' => ($reason === 'user_cancel' ? '用户主动取消' : ($reason === 'timeout_auto' ? '超时未支付自动关闭' : '管理员关闭')),
|
||
'updatetime' => $now,
|
||
]);
|
||
|
||
if (!$affected) {
|
||
$closed = false;
|
||
return;
|
||
}
|
||
|
||
$closed = true;
|
||
|
||
// 1. 释放优惠券(若有)
|
||
CouponService::release($orderNo);
|
||
|
||
// 2. 关联业务的回收处理
|
||
$type = isset($order['type']) ? (string)$order['type'] : '';
|
||
$relatedId = isset($order['related_id']) ? (int)$order['related_id'] : 0;
|
||
|
||
// 若是未付款的发布信息,将关联的草稿或帖子置为下架/关闭,避免未付款直接审核显示
|
||
if ($type === 'post' && $relatedId > 0) {
|
||
Db::name('fc_post')
|
||
->where('id', $relatedId)
|
||
->where('user_id', (int)$order['user_id'])
|
||
->where('audit_status', 0) // 仅当尚未审核通过时
|
||
->update([
|
||
'status' => 0, // 设为下架/关闭
|
||
'updatetime' => $now,
|
||
]);
|
||
}
|
||
|
||
// 3. 发送站内消息通知
|
||
$userId = isset($order['user_id']) ? (int)$order['user_id'] : 0;
|
||
if ($userId > 0) {
|
||
$title = $reason === 'user_cancel' ? '订单已取消' : '订单已超时关闭';
|
||
$orderTitle = isset($order['title']) ? (string)$order['title'] : '同城订单';
|
||
$content = $reason === 'user_cancel'
|
||
? "您的订单【{$orderTitle}】(订单号:{$orderNo})已主动取消。"
|
||
: "您的订单【{$orderTitle}】(订单号:{$orderNo})因超过支付时效已自动关闭,锁定的优惠券已退回。";
|
||
|
||
Db::name('fc_message')->insert([
|
||
'user_id' => $userId,
|
||
'type' => 'system',
|
||
'title' => $title,
|
||
'content' => $content,
|
||
'isread' => 0,
|
||
'readtime' => null,
|
||
'createtime' => $now,
|
||
]);
|
||
}
|
||
});
|
||
} catch (\Throwable $e) {
|
||
Log::record("订单关闭失败:订单 {$orderNo},原因:{$e->getMessage()}", 'error');
|
||
return false;
|
||
}
|
||
|
||
return $closed;
|
||
}
|
||
|
||
/**
|
||
* 惰性检查并在超时时代为关闭
|
||
*
|
||
* @param array $order fc_order 行
|
||
* @return array 最新订单数据
|
||
*/
|
||
public static function checkAndCloseIfExpired(array $order)
|
||
{
|
||
if (empty($order) || (string)$order['status'] !== 'pending') {
|
||
return $order;
|
||
}
|
||
|
||
if (self::isExpired($order)) {
|
||
if (self::close($order, 'timeout_auto')) {
|
||
$order['status'] = 'closed';
|
||
$order['updatetime'] = time();
|
||
}
|
||
}
|
||
|
||
return $order;
|
||
}
|
||
}
|