feat: 收益全额直入商城余额、存量结转与奖项名称全局规范化对齐
这个提交包含在:
@@ -36,7 +36,7 @@ class Pay extends Common
|
||||
$payment = $this->request->post('payment');
|
||||
$openid = $this->request->post('openid', '');
|
||||
$money = $this->request->post('money', 0);
|
||||
$money = $money > 0 ? $money : 0;
|
||||
$money = floatval($money) > 0 ? round(floatval($money), 2) : 0;
|
||||
$platform = $this->request->header('platform');
|
||||
|
||||
list($order, $order_type) = $this->getOrderInstance($order_sn);
|
||||
@@ -65,30 +65,62 @@ class Pay extends Common
|
||||
// pay 实例
|
||||
$payOper = new PayOper();
|
||||
|
||||
if ($money && $order_type == 'order') {
|
||||
// 余额混合支付
|
||||
$order = Db::transaction(function () use ($payOper, $order, $order_type, $money) {
|
||||
$mixedPayRecord = null;
|
||||
if ($money > 0 && $order_type == 'order') {
|
||||
// 余额混合支付(行锁串行化保障并发资金安全)
|
||||
$order = Db::transaction(function () use ($order, $order_type, $money, $user, &$mixedPayRecord) {
|
||||
// 加锁读订单
|
||||
$order = $order->lock(true)->find($order->id);
|
||||
if (in_array($order->status, [$order::STATUS_PAID, $order::STATUS_COMPLETED])) {
|
||||
$this->error('订单已支付');
|
||||
}
|
||||
|
||||
// 余额支付
|
||||
$order = $payOper->money($order, $money, $order_type);
|
||||
// 事务内行级排他锁查验用户最新可用余额,杜绝并发快照双花
|
||||
$lockedUser = \app\admin\model\shopro\user\User::where('id', $user->id)->lock(true)->find();
|
||||
if (bccomp((string)$money, (string)$lockedUser->money, 2) === 1) {
|
||||
$this->error('账户可用余额不足以进行本次抵扣');
|
||||
}
|
||||
|
||||
$deductMoney = min($money, floatval($order->remain_pay_fee));
|
||||
if ($deductMoney <= 0) {
|
||||
return $order;
|
||||
}
|
||||
|
||||
// 使用锁定的实时用户对象执行余额扣减
|
||||
$lockedPayOper = new PayOper($lockedUser);
|
||||
$order = $lockedPayOper->money($order, $deductMoney, $order_type);
|
||||
|
||||
return $order;
|
||||
});
|
||||
|
||||
// 获取刚才产生的余额支付记录,用于异常情况冲正
|
||||
$mixedPayRecord = PayModel::where('order_id', $order->id)
|
||||
->where('order_type', $order_type)
|
||||
->where('pay_type', 'money')
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
if (in_array($order->status, [$order::STATUS_PAID, $order::STATUS_COMPLETED])) {
|
||||
$this->success('订单支付成功', $order);
|
||||
}
|
||||
}
|
||||
|
||||
if ($payment == 'money' && $order_type == 'order') {
|
||||
// 余额支付
|
||||
$order = Db::transaction(function () use ($payOper, $order, $order_type) {
|
||||
// 纯余额支付
|
||||
$order = Db::transaction(function () use ($order, $order_type, $user) {
|
||||
// 加锁读订单
|
||||
$order = $order->lock(true)->find($order->id);
|
||||
if (in_array($order->status, [$order::STATUS_PAID, $order::STATUS_COMPLETED])) {
|
||||
$this->error('订单已支付');
|
||||
}
|
||||
|
||||
$order = $payOper->money($order, $order->remain_pay_fee, $order_type);
|
||||
$lockedUser = \app\admin\model\shopro\user\User::where('id', $user->id)->lock(true)->find();
|
||||
if (bccomp((string)$order->remain_pay_fee, (string)$lockedUser->money, 2) === 1) {
|
||||
$this->error('账户可用余额不足以付清该订单');
|
||||
}
|
||||
|
||||
$lockedPayOper = new PayOper($lockedUser);
|
||||
$order = $lockedPayOper->money($order, $order->remain_pay_fee, $order_type);
|
||||
|
||||
return $order;
|
||||
});
|
||||
@@ -146,7 +178,10 @@ class Pay extends Common
|
||||
}
|
||||
|
||||
if (empty($order_data['payer']['openid'])) {
|
||||
// 缺少 openid
|
||||
// 缺少 openid 时,若前面预扣了余额则冲正
|
||||
if ($mixedPayRecord) {
|
||||
$this->rollbackMixedMoney($order, $mixedPayRecord, $user, $order_type);
|
||||
}
|
||||
$this->error('miss_openid', -1);
|
||||
}
|
||||
}
|
||||
@@ -160,11 +195,18 @@ class Pay extends Common
|
||||
|
||||
try {
|
||||
$result = $payService->pay($order_data);
|
||||
} catch (\Yansongda\Pay\Exception\Exception $e) {
|
||||
$this->error('支付失败' . (config('app_debug') ? ":" . $e->getMessage() : ',请重试'));
|
||||
} catch (HttpResponseException $e) {
|
||||
$data = $e->getResponse()->getData();
|
||||
$message = $data ? ($data['msg'] ?? '') : $e->getMessage();
|
||||
} catch (\Exception $e) {
|
||||
// 第三方预支付异常,安全冲正本次混合支付预扣款
|
||||
if ($mixedPayRecord) {
|
||||
$this->rollbackMixedMoney($order, $mixedPayRecord, $user, $order_type);
|
||||
}
|
||||
|
||||
if ($e instanceof HttpResponseException) {
|
||||
$data = $e->getResponse()->getData();
|
||||
$message = $data ? ($data['msg'] ?? '') : $e->getMessage();
|
||||
} else {
|
||||
$message = $e->getMessage();
|
||||
}
|
||||
|
||||
$this->error('支付失败' . (config('app_debug') ? ":" . $message : ',请重试'));
|
||||
}
|
||||
@@ -183,6 +225,39 @@ class Pay extends Common
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 混合支付预下单异常时,冲正退回预扣余额
|
||||
*
|
||||
* @param \think\Model $order
|
||||
* @param \think\Model $mixedPayRecord
|
||||
* @param \think\Model $user
|
||||
* @param string $order_type
|
||||
* @return void
|
||||
*/
|
||||
protected function rollbackMixedMoney($order, $mixedPayRecord, $user, $order_type)
|
||||
{
|
||||
try {
|
||||
Db::transaction(function () use ($order, $mixedPayRecord, $user, $order_type) {
|
||||
\addons\shopro\service\Wallet::change($user, 'money', $mixedPayRecord->pay_fee, 'order_refund', [
|
||||
'order_id' => $order->id,
|
||||
'order_sn' => $order->order_sn,
|
||||
'order_type' => $order_type,
|
||||
'remark' => '混合支付预下单未完成,安全冲正退回'
|
||||
]);
|
||||
|
||||
$mixedPayRecord->status = PayModel::PAY_STATUS_REFUND;
|
||||
$mixedPayRecord->refund_fee = $mixedPayRecord->pay_fee;
|
||||
$mixedPayRecord->save();
|
||||
|
||||
$order = $order->lock(true)->find($order->id);
|
||||
$order->remain_pay_fee = bcadd((string)$order->remain_pay_fee, (string)$mixedPayRecord->pay_fee, 2);
|
||||
$order->save();
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
Log::write('混合支付预扣款冲正异常: ' . $e->getMessage(), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -303,16 +303,18 @@ class Agent extends Commission
|
||||
->column('id');
|
||||
|
||||
if (empty($teamUserIds)) {
|
||||
$selfAgent = AgentModel::where('user_id', $agentId)->find();
|
||||
$selfUser = UserModel::where('id', $agentId)->find();
|
||||
return [
|
||||
'total_count' => 0,
|
||||
'active_count' => 0,
|
||||
'expiring_count' => 0,
|
||||
'inactive_count' => 0,
|
||||
'partner_count' => 0,
|
||||
'team_total_consume' => '0.00',
|
||||
'team_total_consume' => \addons\shopro\service\partner\IdentityService::formatMoney($selfUser->total_consume ?? 0),
|
||||
'team_total_reward' => '0.00',
|
||||
'team_month_reward' => '0.00',
|
||||
'team_order_count' => 0,
|
||||
'team_order_count' => intval($selfAgent->child_order_count_0 ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -348,8 +350,17 @@ class Agent extends Commission
|
||||
$inactiveCount += $missingAgentCount;
|
||||
}
|
||||
|
||||
// 1. 团队累计自购总业绩
|
||||
$teamTotalConsume = UserModel::whereIn('id', $teamUserIds)->sum('total_consume');
|
||||
// 1. 团队累计自购总业绩与团队订单总数(统一使用包含本人自购的分销团队全口径)
|
||||
$selfAgent = AgentModel::where('user_id', $agentId)->find();
|
||||
$selfUser = UserModel::where('id', $agentId)->find();
|
||||
|
||||
$teamTotalConsume = isset($selfAgent->child_order_money_all) && floatval($selfAgent->child_order_money_all) > 0
|
||||
? floatval($selfAgent->child_order_money_all)
|
||||
: (UserModel::whereIn('id', $teamUserIds)->sum('total_consume') + floatval($selfUser->total_consume ?? 0));
|
||||
|
||||
$teamOrderCount = isset($selfAgent->child_order_count_all) && intval($selfAgent->child_order_count_all) > 0
|
||||
? intval($selfAgent->child_order_count_all)
|
||||
: ($teamOrderCount + intval($selfAgent->child_order_count_0 ?? 0));
|
||||
|
||||
// 2. 团队成员为当前用户贡献的累计收益与本月收益
|
||||
$monthStart = strtotime(date('Y-m-01 00:00:00'));
|
||||
|
||||
@@ -28,8 +28,10 @@ class Index extends Commission
|
||||
->where('createtime', '>=', $monthStart)
|
||||
->count();
|
||||
$agent = IdentityService::attachAgent($this->service->agent);
|
||||
$totalIncome = $agent['total_income'] ?? '0.00';
|
||||
|
||||
$this->success('ok', [
|
||||
'total_income' => $totalIncome,
|
||||
'month_income' => IdentityService::formatMoney($monthIncome),
|
||||
'skip_count' => intval($skipCount),
|
||||
'active_30d' => $agent['active_30d'] ?? true,
|
||||
@@ -92,10 +94,21 @@ class Index extends Commission
|
||||
}
|
||||
}
|
||||
|
||||
$monthStart = strtotime(date('Y-m-01 00:00:00'));
|
||||
$monthIncome = RewardModel::where('user_id', $userId)
|
||||
->where('status', 1)
|
||||
->where('createtime', '>=', $monthStart)
|
||||
->whereIn('reward_type', array_merge(PartnerConfig::instantTypes(), ['venue_share', 'county_share']))
|
||||
->sum('amount');
|
||||
$agent = IdentityService::attachAgent($this->service->agent);
|
||||
$totalIncome = $agent['total_income'] ?? '0.00';
|
||||
|
||||
$this->success('ok', [
|
||||
'total' => $list->total(),
|
||||
'per_page' => $list->listRows(),
|
||||
'current_page' => $list->currentPage(),
|
||||
'total_income' => $totalIncome,
|
||||
'month_income' => IdentityService::formatMoney($monthIncome),
|
||||
'data' => $rows,
|
||||
'buyer_info' => $buyerInfo,
|
||||
]);
|
||||
@@ -127,6 +140,7 @@ class Index extends Commission
|
||||
'madow' => $agent['madow'] ?? '0.00000000',
|
||||
'madow_rate' => $agent['madow_rate'] ?? IdentityService::formatRate(null),
|
||||
'madow_value' => $agent['madow_value'] ?? '0.00',
|
||||
'show_madow_rate' => PartnerConfig::showMadowRate(),
|
||||
'list' => $list,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,20 @@ class User extends Common
|
||||
|
||||
$agent = \addons\shopro\service\partner\IdentityService::findAgent($user->id);
|
||||
$partner = \addons\shopro\service\partner\IdentityService::attachProfile($agent);
|
||||
if (empty($partner['total_income']) || $partner['total_income'] === '0.00') {
|
||||
$userRewardCash = \app\admin\model\shopro\partner\Reward::where('user_id', $user->id)
|
||||
->where('status', 1)
|
||||
->where('amount', '>', 0)
|
||||
->sum('amount');
|
||||
if ($userRewardCash > 0) {
|
||||
$formatted = \addons\shopro\service\partner\IdentityService::formatMoney($userRewardCash);
|
||||
$partner['total_income'] = $formatted;
|
||||
$partner['income_total_cash'] = $formatted;
|
||||
}
|
||||
}
|
||||
$data = array_merge($user->toArray(), $partner);
|
||||
// 对齐用户余额字段:统一映射商城余额,避免前端不同字段读取出现口径偏差
|
||||
$data['balance'] = $user->money;
|
||||
|
||||
$this->success('个人详情', $data);
|
||||
}
|
||||
|
||||
@@ -19,20 +19,102 @@ class WalletLog extends Common
|
||||
$list_rows = $this->request->param('list_rows', 10);
|
||||
$date = $this->request->param('date/a');
|
||||
$user = auth_user();
|
||||
$where['user_id'] = $user->id;
|
||||
|
||||
$rewardEvents = [
|
||||
'push3', 'maker_diff', 'maker_peer', 'maker_overtaken',
|
||||
'team_venue', 'team_county', 'venue_peer', 'county_peer',
|
||||
'venue_share', 'county_share', 'reward_income', 'transfer_by_commission', 'transfer_to_money'
|
||||
];
|
||||
|
||||
$baseQuery = function () use ($user, $type, $rewardEvents) {
|
||||
$model = UserWalletLogModel::where('user_id', $user->id);
|
||||
if ($type === 'commission') {
|
||||
// 收益记录:既包含历史 commission 流水,也对齐直入 money 的收益分成流水
|
||||
$model->where(function ($query) use ($rewardEvents) {
|
||||
$query->where('type', 'commission')
|
||||
->whereOr(function ($sub) use ($rewardEvents) {
|
||||
$sub->where('type', 'money')->whereIn('event', $rewardEvents);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
$model->where('type', $type);
|
||||
}
|
||||
return $model;
|
||||
};
|
||||
|
||||
$filterQuery = $baseQuery();
|
||||
|
||||
if (!empty($date) && is_array($date) && count($date) === 2) {
|
||||
$filterQuery->whereTime('createtime', 'between', $date);
|
||||
}
|
||||
|
||||
switch ($tab) {
|
||||
case 'reward':
|
||||
$filterQuery->where('amount', '>', 0)->whereIn('event', $rewardEvents);
|
||||
break;
|
||||
case 'income':
|
||||
$where['amount'] = ['>', 0];
|
||||
$filterQuery->where('amount', '>', 0);
|
||||
break;
|
||||
case 'expense':
|
||||
$where['amount'] = ['<', 0];
|
||||
$filterQuery->where('amount', '<', 0);
|
||||
break;
|
||||
}
|
||||
|
||||
$income = UserWalletLogModel::where('user_id', $user->id)->{$type}()->where('amount', '>', 0)->whereTime('createtime', 'between', $date)->sum('amount');
|
||||
$expense = UserWalletLogModel::where('user_id', $user->id)->{$type}()->where('amount', '<', 0)->whereTime('createtime', 'between', $date)->sum('amount');
|
||||
$logs = UserWalletLogModel::where($where)->{$type}()->whereTime('createtime', 'between', $date)->order('createtime', 'desc')->paginate($list_rows);
|
||||
$this->success('获取成功', ['list' => $logs, 'income' => $income, 'expense' => $expense]);
|
||||
// 统计收入与支出(统一统计口径,避免内部互转重叠统计造成误导)
|
||||
$incomeQuery = $baseQuery();
|
||||
$expenseQuery = $baseQuery();
|
||||
$rewardQuery = $baseQuery();
|
||||
|
||||
if (!empty($date) && is_array($date) && count($date) === 2) {
|
||||
$incomeQuery->whereTime('createtime', 'between', $date);
|
||||
$expenseQuery->whereTime('createtime', 'between', $date);
|
||||
$rewardQuery->whereTime('createtime', 'between', $date);
|
||||
}
|
||||
|
||||
$actualRewardEvents = [
|
||||
'push3', 'maker_diff', 'maker_peer', 'maker_overtaken',
|
||||
'team_venue', 'team_county', 'venue_peer', 'county_peer',
|
||||
'venue_share', 'county_share', 'reward_income'
|
||||
];
|
||||
|
||||
if ($type === 'commission') {
|
||||
// 收益流水统计口径对齐:
|
||||
// 1. 真实收益(总收入):排除内部互转入账事件 transfer_by_commission,避免佣金转入余额时重复计入收益
|
||||
// 2. 真实扣减(总支出):排除内部结转转出事件 transfer_to_money 与 commission_transfer,避免误导用户认为产生了消费支出
|
||||
$income = (float) $incomeQuery->where('amount', '>', 0)
|
||||
->where(function ($q) use ($actualRewardEvents) {
|
||||
$q->where(function ($sub) use ($actualRewardEvents) {
|
||||
$sub->where('type', 'money')->whereIn('event', $actualRewardEvents);
|
||||
})->whereOr(function ($sub) {
|
||||
$sub->where('type', 'commission')->whereNotIn('event', ['transfer_by_commission']);
|
||||
});
|
||||
})->sum('amount');
|
||||
|
||||
$expense = (float) $expenseQuery->where('amount', '<', 0)
|
||||
->whereNotIn('event', ['transfer_to_money', 'commission_transfer'])
|
||||
->sum('amount');
|
||||
|
||||
$rewardIncome = (float) $rewardQuery->where('amount', '>', 0)
|
||||
->where(function ($q) use ($actualRewardEvents) {
|
||||
$q->where(function ($sub) use ($actualRewardEvents) {
|
||||
$sub->where('type', 'money')->whereIn('event', $actualRewardEvents);
|
||||
})->whereOr(function ($sub) {
|
||||
$sub->where('type', 'commission')->whereNotIn('event', ['transfer_by_commission']);
|
||||
});
|
||||
})->sum('amount');
|
||||
} else {
|
||||
$income = (float) $incomeQuery->where('amount', '>', 0)->sum('amount');
|
||||
$expense = (float) $expenseQuery->where('amount', '<', 0)->sum('amount');
|
||||
$rewardIncome = (float) $rewardQuery->where('amount', '>', 0)->whereIn('event', $actualRewardEvents)->sum('amount');
|
||||
}
|
||||
|
||||
$logs = $filterQuery->order('id', 'desc')->paginate($list_rows);
|
||||
|
||||
$this->success('获取成功', [
|
||||
'list' => $logs,
|
||||
'income' => $income,
|
||||
'expense' => $expense,
|
||||
'reward_income' => $rewardIncome,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户