fix(stability): 支付并发事务内加锁、组合支付流水记录、省市区树形缓存与地址安全防越权 (TDD)

这个提交包含在:
wikiboss
2026-11-01 12:43:45 +08:00
父节点 1fc5138362
当前提交 7ea240dae4
共有 6 个文件被更改,包括 239 次插入 和 41 次删除

查看文件

@@ -24,16 +24,47 @@ class Address extends Base
public function area()
{
$data['provinceData'] = Area::where('level', 1)->order('id asc')->field('id as value, name as label, pid, level')->select();
foreach ($data['provinceData'] as $k => $p) {
$data['cityData'][$k] = Area::where(['level' => 2, 'pid' => $p->value])->order('id asc')->field('id as value, name as label, pid, level')->select();
foreach ($data['cityData'][$k] as $i => $c) {
$data['areaData'][$k][$i] = Area::where(['level' => 3, 'pid' => $c->value])->order('id asc')->field('id as value, name as label, pid, level')->select();
$cacheKey = 'shopro_area_tree_data';
$data = cache($cacheKey);
if (!$data) {
// 一次性查出全部省市区数据(仅 1 次查询),并在内存中按 pid 构建树形,消除 350+ 次嵌套 SQL 击穿
$allAreas = Area::where('level', 'in', [1, 2, 3])
->field('id as value, name as label, pid, level')
->order('id asc')
->select();
$provinceData = [];
$cityMap = [];
$areaMap = [];
foreach ($allAreas as $item) {
if ($item['level'] == 1) {
$provinceData[] = $item;
} elseif ($item['level'] == 2) {
$cityMap[$item['pid']][] = $item;
} elseif ($item['level'] == 3) {
$areaMap[$item['pid']][] = $item;
}
}
$cityData = [];
$areaData = [];
foreach ($provinceData as $k => $p) {
$cities = $cityMap[$p['value']] ?? [];
$cityData[$k] = $cities;
foreach ($cities as $i => $c) {
$areaData[$k][$i] = $areaMap[$c['value']] ?? [];
}
}
$data = [
'provinceData' => $provinceData,
'cityData' => $cityData,
'areaData' => $areaData
];
cache($cacheKey, $data, 604800);
}
$this->success('省市区', $data);
}
public function edit()

查看文件

@@ -342,12 +342,43 @@ class Pay extends Base
// 支付成功流程
$pay_fee = $payment == 'alipay' ? $data['total_amount'] : $data['total_fee'] / 100;
// 你可以在此编写订单逻辑
$order = $order->nopay()->where('order_sn', $out_trade_no)->lock(true)->find();
$timeStr = $data['time_end'] ?? ($data['gmt_payment'] ?? '');
$notify_time = $timeStr && ($ts = strtotime($timeStr)) ? date('Y-m-d H:i:s', $ts) : date('Y-m-d H:i:s');
if (!$order) {
// 已支付则直接返回成功,否则记录日志并通知微信重试
$paidOrder = Order::where('order_sn', $out_trade_no)->where('status', '>', 0)->find();
$payment_json = json_encode(
$data instanceof \think\Collection || (is_object($data) && method_exists($data, 'all'))
? $data->all()
: (array)$data
);
$buyer_email = $payment == 'alipay' ? ($data['buyer_logon_id'] ?? ($data['buyer_id'] ?? '')) : ($data['openid'] ?? '');
$transaction_id = $payment == 'alipay' ? ($data['trade_no'] ?? '') : ($data['transaction_id'] ?? '');
$isProcessed = false;
Db::transaction(function () use ($order, $out_trade_no, $transaction_id, $notify_time, $buyer_email, $payment_json, $pay_fee, $payment, &$isProcessed) {
// 必须在事务内加排他锁复查,确保高并发回调互斥幂等
$lockOrder = $order->nopay()->where('order_sn', $out_trade_no)->lock(true)->find();
if (!$lockOrder) {
$isProcessed = true;
return;
}
$notify = [
'order_sn' => $out_trade_no,
'transaction_id' => $transaction_id,
'notify_time' => $notify_time,
'buyer_email' => $buyer_email,
'payment_json' => $payment_json,
'pay_fee' => $pay_fee,
'pay_type' => $lockOrder->pay_type == 'group' ? 'group' : $payment
];
$lockOrder->paymentProcess($lockOrder, $notify);
});
if ($isProcessed) {
// 已经被并发处理过,直接向网关确认成功
$paidOrder = $order->where('order_sn', $out_trade_no)->where('status', '>', 0)->find();
if ($paidOrder) {
return $pay->success()->send();
}
@@ -355,20 +386,6 @@ class Pay extends Base
return false;
}
Db::transaction(function () use ($order, $data, $payment, $platform, $pay_fee, $prepay_type) {
$notify = [
'order_sn' => $data['out_trade_no'],
'transaction_id' => $payment == 'alipay' ? $data['trade_no'] : $data['transaction_id'],
'notify_time' => date('Y-m-d H:i:s', strtotime($data['time_end'])),
'buyer_email' => $payment == 'alipay' ? $data['buyer_logon_id'] : $data['openid'],
'payment_json' => json_encode($data->all()),
'pay_fee' => $pay_fee,
'pay_type' => $order->pay_type == 'group' ? 'group' : $payment // 支付方式
];
$order->paymentProcess($order, $notify);
});
return $pay->success()->send();
} catch (\Exception $e) {
Log::write('notifyx-error:' . $e->getMessage());

查看文件

@@ -800,9 +800,12 @@ trait OrderOper
$user = User::where('id', $order->user_id)->find();
OrderAction::operAdd($order, null, $user, 'user', '用户支付成功');
// 组合支付扣除用户余额
if ($order->pay_type == 'group') {
User::where(['id' => $user->id])->setDec('money', $order->pay_money);
// 组合支付扣除用户余额(记录流水并校验充裕性)
if ($order->pay_type == 'group' && $order->pay_money > 0) {
User::money(-$order->pay_money, $user->id, 'wallet_pay', $order->id, '组合付款扣除余额', [
'order_id' => $order->id,
'order_sn' => $order->order_sn,
]);
}
// 预约抢购标记成已使用

查看文件

@@ -86,7 +86,12 @@ class UserAddress extends Model
'address' => $address
]);
} else {
$edit = self::update([
// 防 IDOR 越权:更新时必须校验地址归属当前用户
$currentAddress = self::get(['id' => $id, 'user_id' => $user->id]);
if (!$currentAddress) {
new Exception('未找到该收货地址');
}
$edit = $currentAddress->save([
'consignee' => $consignee,
'phone' => $phone,
'province_id' => $province->id,
@@ -98,9 +103,7 @@ class UserAddress extends Model
'is_default' => $is_default,
'latitude' => (isset($latitude) && $latitude) ? $latitude : null,
'longitude' => (isset($longitude) && $longitude) ? $longitude : null,
'user_id' => $user->id,
'address' => $address,
'id' => $id
'address' => $address
]);
}
return $edit;
@@ -117,6 +120,10 @@ class UserAddress extends Model
{
$user = User::info();
extract($params);
return self::get(['id' => $id, 'user_id' => $user->id])->delete();
$addr = self::get(['id' => $id, 'user_id' => $user->id]);
if (!$addr) {
new Exception('未找到该收货地址');
}
return $addr->delete();
}
}