文件
fenleixinxi/application/common/library/OrderClose.php

168 行
6.1 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?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;
}
}