459 行
16 KiB
PHP
459 行
16 KiB
PHP
<?php
|
||
|
||
namespace addons\shopro\service\partner;
|
||
|
||
use app\admin\model\shopro\partner\VoucherPlan;
|
||
use app\admin\model\shopro\partner\Reward as RewardModel;
|
||
use app\admin\model\shopro\commission\Agent as AgentModel;
|
||
|
||
/**
|
||
* 兑换券 1.5 倍 / 365 天释放
|
||
*/
|
||
class VoucherService
|
||
{
|
||
public function available($userId)
|
||
{
|
||
$agent = AgentModel::where('user_id', $userId)->find();
|
||
if (!$agent) {
|
||
return 0;
|
||
}
|
||
return max(0, round((float) $agent->voucher_released - (float) $agent->voucher_used, 2));
|
||
}
|
||
|
||
public function consume($userId, $amount, $orderId = 0)
|
||
{
|
||
$amount = round((float) $amount, 2);
|
||
if ($amount <= 0) {
|
||
return true;
|
||
}
|
||
$agent = AgentModel::where('user_id', $userId)->lock(true)->find();
|
||
if (!$agent) {
|
||
error_stop('兑换券账户不存在');
|
||
}
|
||
$available = max(0, round((float) $agent->voucher_released - (float) $agent->voucher_used, 2));
|
||
if (bccomp((string) $available, (string) $amount, 2) < 0) {
|
||
error_stop('兑换券可用额度不足');
|
||
}
|
||
$agent->setInc('voucher_used', $amount);
|
||
|
||
PartnerRewardService::write([
|
||
'order_id' => $orderId,
|
||
'buyer_id' => $userId,
|
||
'user_id' => $userId,
|
||
'role' => 'taster',
|
||
'reward_type' => 'voucher_consume',
|
||
'amount' => -$amount,
|
||
'base_amount' => $amount,
|
||
'ext' => ['formula' => "订单抵扣兑换券 {$amount}", 'order_id' => $orderId],
|
||
'status' => 1,
|
||
]);
|
||
return true;
|
||
}
|
||
|
||
public function refundConsume($order, $refundFee = null)
|
||
{
|
||
$ext = $order['ext'] ?? [];
|
||
if (is_string($ext)) {
|
||
$ext = json_decode($ext, true) ?: [];
|
||
}
|
||
$totalVoucher = round((float) ($ext['voucher_fee'] ?? 0), 2);
|
||
if ($totalVoucher <= 0 || empty($ext['voucher_consumed'])) {
|
||
return;
|
||
}
|
||
$userId = intval($order['user_id'] ?? 0);
|
||
$orderId = intval($order['id'] ?? 0);
|
||
|
||
// 幂等防重:检查是否已退回过兑换券,避免多次退款事件重复返还额度
|
||
$alreadyRefunded = (float) RewardModel::where('order_id', $orderId)
|
||
->where('reward_type', 'voucher_consume_refund')
|
||
->sum('amount');
|
||
$remainVoucher = max(0, round((float) $totalVoucher - $alreadyRefunded, 2));
|
||
if ($remainVoucher <= 0) {
|
||
return;
|
||
}
|
||
|
||
$fee = $refundFee !== null ? min($remainVoucher, round((float) $refundFee, 2)) : $remainVoucher;
|
||
if ($fee <= 0) {
|
||
return;
|
||
}
|
||
|
||
$agent = AgentModel::where('user_id', $userId)->lock(true)->find();
|
||
if ($agent) {
|
||
$agent->voucher_used = max(0, round((float) $agent->voucher_used - $fee, 2));
|
||
$agent->save();
|
||
|
||
PartnerRewardService::write([
|
||
'order_id' => $orderId,
|
||
'buyer_id' => $userId,
|
||
'user_id' => $userId,
|
||
'role' => 'taster',
|
||
'reward_type' => 'voucher_consume_refund',
|
||
'amount' => $fee,
|
||
'base_amount' => $fee,
|
||
'ext' => ['formula' => "订单退款退回兑换券 {$fee}", 'order_id' => $orderId],
|
||
'status' => 1,
|
||
]);
|
||
|
||
if (is_object($order) && method_exists($order, 'save')) {
|
||
$ext['voucher_refunded'] = round($alreadyRefunded + $fee, 2);
|
||
if (bccomp((string) $ext['voucher_refunded'], (string) $totalVoucher, 2) >= 0) {
|
||
$ext['voucher_consumed'] = 0;
|
||
}
|
||
$order->ext = $ext;
|
||
$order->save();
|
||
}
|
||
}
|
||
}
|
||
|
||
public function consumeByOrder($order)
|
||
{
|
||
$ext = $order['ext'] ?? [];
|
||
if (is_string($ext)) {
|
||
$ext = json_decode($ext, true) ?: [];
|
||
}
|
||
$fee = round((float) ($ext['voucher_fee'] ?? 0), 2);
|
||
if ($fee <= 0 || !empty($ext['voucher_consumed'])) {
|
||
return;
|
||
}
|
||
$this->consume(intval($order['user_id']), $fee, intval($order['id'] ?? 0));
|
||
if (is_object($order) && method_exists($order, 'save')) {
|
||
$ext['voucher_consumed'] = 1;
|
||
$order->ext = $ext;
|
||
$order->save();
|
||
}
|
||
}
|
||
|
||
public function grant($order, $buyerId, $rules = null)
|
||
{
|
||
$exists = VoucherPlan::where('order_id', $order['id'])->find();
|
||
if ($exists) {
|
||
return $exists;
|
||
}
|
||
|
||
$times = (isset($rules['voucher_times']) && is_numeric($rules['voucher_times']))
|
||
? (float) $rules['voucher_times']
|
||
: (float) PartnerConfig::get('partner_voucher_times', 1.5);
|
||
$days = (isset($rules['voucher_days']) && is_numeric($rules['voucher_days']))
|
||
? intval($rules['voucher_days'])
|
||
: intval(PartnerConfig::get('partner_voucher_days', 365));
|
||
$days = $days > 0 ? $days : 365;
|
||
$rewardService = new PartnerRewardService();
|
||
$qualifiedPayFee = $rewardService->getQualifiedPayFee($order);
|
||
$total = round((float) $qualifiedPayFee * $times, 2);
|
||
if ($total <= 0) {
|
||
return null;
|
||
}
|
||
$daily = round($total / $days, 2);
|
||
// 生产标准模式:下单次日生效起算,按 365 天线性释放
|
||
$startDate = date('Y-m-d', strtotime('+1 day', $this->orderTime($order)));
|
||
|
||
$plan = VoucherPlan::create([
|
||
'user_id' => $buyerId,
|
||
'order_id' => $order['id'],
|
||
'total_amount' => $total,
|
||
'daily_amount' => $daily,
|
||
'released_amount' => 0,
|
||
'released_days' => 0,
|
||
'total_days' => $days,
|
||
'start_date' => $startDate,
|
||
'status' => 1,
|
||
'createtime' => time(),
|
||
]);
|
||
|
||
$agent = AgentModel::where('user_id', $buyerId)->find();
|
||
if ($agent) {
|
||
$agent->setInc('voucher_total', $total);
|
||
}
|
||
|
||
PartnerRewardService::write([
|
||
'order_id' => $order['id'],
|
||
'buyer_id' => $buyerId,
|
||
'user_id' => $buyerId,
|
||
'role' => 'taster',
|
||
'reward_type' => 'voucher_grant',
|
||
'amount' => 0,
|
||
'base_amount' => $total,
|
||
'ext' => [
|
||
'formula' => "计奖实付{$qualifiedPayFee} × {$times} = {$total},{$days}天释放",
|
||
'qualified_pay_fee' => $qualifiedPayFee,
|
||
'order_pay_fee' => $order['pay_fee'] ?? 0
|
||
],
|
||
'status' => 1,
|
||
]);
|
||
|
||
return $plan;
|
||
}
|
||
|
||
/**
|
||
* 临时分钟级释放兑换券:每 1 分钟对所有释放中的计划释放一期
|
||
*
|
||
* @return float 本次释放总金额
|
||
*/
|
||
public function releaseMinute()
|
||
{
|
||
$plans = VoucherPlan::where('status', 1)->select();
|
||
$totalReleased = 0;
|
||
foreach ($plans as $plan) {
|
||
if ($plan->released_days >= $plan->total_days || bccomp((string) $plan->released_amount, (string) $plan->total_amount, 2) >= 0) {
|
||
$plan->status = 2;
|
||
$plan->save();
|
||
continue;
|
||
}
|
||
|
||
$leftDays = $plan->total_days - $plan->released_days;
|
||
if ($leftDays <= 1) {
|
||
$amount = round((float) $plan->total_amount - (float) $plan->released_amount, 2);
|
||
$plan->status = 2;
|
||
} else {
|
||
$amount = (float) $plan->daily_amount;
|
||
}
|
||
|
||
if ($amount <= 0) {
|
||
$plan->status = 2;
|
||
$plan->save();
|
||
continue;
|
||
}
|
||
|
||
$plan->released_amount = round((float) $plan->released_amount + $amount, 2);
|
||
$plan->released_days += 1;
|
||
if (bccomp((string) $plan->released_amount, (string) $plan->total_amount, 2) >= 0) {
|
||
$plan->status = 2;
|
||
}
|
||
$plan->save();
|
||
|
||
$agent = AgentModel::where('user_id', $plan->user_id)->find();
|
||
if ($agent) {
|
||
$agent->setInc('voucher_released', $amount);
|
||
}
|
||
|
||
// 唯一负数 order_id 规避与历史冲突;settle_date 设为 NULL 规避 uk_daily_user_type 唯一索引
|
||
$virtualOrderId = -(intval($plan->id) * 1000 + (intval($plan->released_days) % 1000));
|
||
PartnerRewardService::write([
|
||
'order_id' => $virtualOrderId,
|
||
'buyer_id' => $plan->user_id,
|
||
'user_id' => $plan->user_id,
|
||
'role' => 'taster',
|
||
'reward_type' => 'voucher_release',
|
||
'amount' => $amount,
|
||
'base_amount' => $amount,
|
||
'settle_date' => null,
|
||
'ext' => [
|
||
'formula' => "兑换券释放 {$amount}(第{$plan->released_days}/{$plan->total_days}期)",
|
||
'plan_id' => $plan->id,
|
||
'order_id' => $plan->order_id,
|
||
'released_days' => $plan->released_days,
|
||
],
|
||
'status' => 1,
|
||
]);
|
||
|
||
$totalReleased = bcadd((string) $totalReleased, (string) $amount, 2);
|
||
}
|
||
return (float) $totalReleased;
|
||
}
|
||
|
||
public function releaseByDate($settleDate)
|
||
{
|
||
// 幂等防重:检查该结算日期是否已释放过兑换券
|
||
$existingCount = RewardModel::where('settle_date', $settleDate)
|
||
->where('reward_type', 'voucher_release')
|
||
->count();
|
||
if ($existingCount > 0) {
|
||
return (float) RewardModel::where('settle_date', $settleDate)
|
||
->where('reward_type', 'voucher_release')
|
||
->sum('amount');
|
||
}
|
||
|
||
$plans = VoucherPlan::where('status', 1)
|
||
->where('start_date', '<=', $settleDate)
|
||
->select();
|
||
$byUser = [];
|
||
foreach ($plans as $plan) {
|
||
$amount = $this->releaseOne($plan, $settleDate);
|
||
if ($amount <= 0) {
|
||
continue;
|
||
}
|
||
$uid = intval($plan->user_id);
|
||
if (!isset($byUser[$uid])) {
|
||
$byUser[$uid] = ['amount' => 0, 'plans' => []];
|
||
}
|
||
$byUser[$uid]['amount'] = bcadd((string) $byUser[$uid]['amount'], (string) $amount, 2);
|
||
$byUser[$uid]['plans'][] = $plan->id;
|
||
}
|
||
$totalReleased = 0;
|
||
foreach ($byUser as $userId => $info) {
|
||
PartnerRewardService::write([
|
||
'order_id' => PartnerRewardService::dailyOrderId($settleDate),
|
||
'buyer_id' => $userId,
|
||
'user_id' => $userId,
|
||
'role' => 'taster',
|
||
'reward_type' => 'voucher_release',
|
||
'amount' => $info['amount'],
|
||
'base_amount' => $info['amount'],
|
||
'settle_date' => $settleDate,
|
||
'ext' => [
|
||
'formula' => "兑换券释放 {$info['amount']}",
|
||
'plan_ids' => $info['plans'],
|
||
],
|
||
'status' => 1,
|
||
]);
|
||
$totalReleased = bcadd((string) $totalReleased, (string) $info['amount'], 2);
|
||
}
|
||
return (float) $totalReleased;
|
||
}
|
||
|
||
public function releaseOne($plan, $settleDate)
|
||
{
|
||
if ($plan->status != 1) {
|
||
return 0;
|
||
}
|
||
if ($plan->released_days >= $plan->total_days) {
|
||
$plan->status = 2;
|
||
$plan->save();
|
||
return 0;
|
||
}
|
||
if ($plan->start_date > $settleDate) {
|
||
return 0;
|
||
}
|
||
|
||
$leftDays = $plan->total_days - $plan->released_days;
|
||
if ($leftDays <= 1) {
|
||
$amount = round((float) $plan->total_amount - (float) $plan->released_amount, 2);
|
||
$plan->status = 2;
|
||
} else {
|
||
$amount = (float) $plan->daily_amount;
|
||
}
|
||
if ($amount <= 0) {
|
||
$plan->status = 2;
|
||
$plan->save();
|
||
return 0;
|
||
}
|
||
|
||
$plan->released_amount = round((float) $plan->released_amount + $amount, 2);
|
||
$plan->released_days += 1;
|
||
$plan->save();
|
||
|
||
$agent = AgentModel::where('user_id', $plan->user_id)->find();
|
||
if ($agent) {
|
||
$agent->setInc('voucher_released', $amount);
|
||
}
|
||
|
||
return $amount;
|
||
}
|
||
|
||
public function closeByOrder($orderId)
|
||
{
|
||
$plan = VoucherPlan::where('order_id', $orderId)->find();
|
||
if (!$plan || $plan->status == 0) {
|
||
return;
|
||
}
|
||
$plan->status = 0;
|
||
$plan->save();
|
||
|
||
$agent = AgentModel::where('user_id', $plan->user_id)->lock(true)->find();
|
||
if ($agent) {
|
||
$releasedAmount = round((float) $plan->released_amount, 2);
|
||
$totalAmount = round((float) $plan->total_amount, 2);
|
||
$currentReleased = round((float) $agent->voucher_released, 2);
|
||
$currentUsed = round((float) $agent->voucher_used, 2);
|
||
|
||
$newTotal = max(0, round((float) $agent->voucher_total - $totalAmount, 2));
|
||
$newReleased = max(0, round($currentReleased - $releasedAmount, 2));
|
||
|
||
$overdraft = 0;
|
||
if ($newReleased < $currentUsed) {
|
||
$overdraft = round($currentUsed - $newReleased, 2);
|
||
$newReleased = $currentUsed;
|
||
}
|
||
|
||
$agent->voucher_released = $newReleased;
|
||
$agent->voucher_total = $newTotal;
|
||
$agent->save();
|
||
|
||
if ($overdraft > 0) {
|
||
\think\Log::record("[VoucherService] 订单 {$orderId} 退款关闭计划,用户 {$agent->user_id} 已透支兑换券额度: {$overdraft}", 'warning');
|
||
}
|
||
}
|
||
}
|
||
|
||
public function reduceByRefund($orderId, $refundItemFee)
|
||
{
|
||
$plan = VoucherPlan::where('order_id', $orderId)->find();
|
||
if (!$plan || $plan->status == 0) {
|
||
return;
|
||
}
|
||
$times = (float) PartnerConfig::get('partner_voucher_times', 1.5);
|
||
$reduceTotal = round((float) $refundItemFee * $times, 2);
|
||
if ($reduceTotal <= 0) {
|
||
return;
|
||
}
|
||
|
||
$reduceTotal = min($reduceTotal, (float) $plan->total_amount);
|
||
$plan->total_amount = max(0, round((float) $plan->total_amount - $reduceTotal, 2));
|
||
$leftDays = max(1, $plan->total_days - $plan->released_days);
|
||
$plan->daily_amount = round(($plan->total_amount - $plan->released_amount) / $leftDays, 2);
|
||
if ($plan->total_amount <= $plan->released_amount) {
|
||
$plan->status = 2;
|
||
}
|
||
$plan->save();
|
||
|
||
$agent = AgentModel::where('user_id', $plan->user_id)->lock(true)->find();
|
||
if ($agent) {
|
||
$agent->voucher_total = max(0, round((float) $agent->voucher_total - $reduceTotal, 2));
|
||
$agent->save();
|
||
}
|
||
}
|
||
|
||
public function rollbackRelease($settleDate)
|
||
{
|
||
$rows = RewardModel::where('settle_date', $settleDate)
|
||
->where('reward_type', 'voucher_release')
|
||
->where('status', 1)
|
||
->select();
|
||
foreach ($rows as $row) {
|
||
$planIds = $row->ext['plan_ids'] ?? [];
|
||
if (!$planIds && !empty($row->ext['plan_id'])) {
|
||
$planIds = [$row->ext['plan_id']];
|
||
}
|
||
foreach ($planIds as $planId) {
|
||
$plan = VoucherPlan::get($planId);
|
||
if (!$plan) {
|
||
continue;
|
||
}
|
||
$daily = (float) $plan->daily_amount;
|
||
$plan->released_amount = max(0, (float) $plan->released_amount - $daily);
|
||
$plan->released_days = max(0, $plan->released_days - 1);
|
||
if ($plan->status == 2 && $plan->released_days < $plan->total_days) {
|
||
$plan->status = 1;
|
||
}
|
||
$plan->save();
|
||
}
|
||
$agent = AgentModel::where('user_id', $row->user_id)->find();
|
||
if ($agent) {
|
||
$agent->voucher_released = max(0, (float) $agent->voucher_released - (float) $row->amount);
|
||
$agent->save();
|
||
}
|
||
}
|
||
}
|
||
|
||
protected function orderTime($order)
|
||
{
|
||
$paid = $order['paid_time'] ?? 0;
|
||
if (is_numeric($paid) && $paid > 0) {
|
||
return intval($paid);
|
||
}
|
||
if (is_string($paid) && $paid !== '') {
|
||
$ts = strtotime($paid);
|
||
if ($ts) {
|
||
return $ts;
|
||
}
|
||
}
|
||
return time();
|
||
}
|
||
|
||
protected function dailyOrderId($settleDate, $planId)
|
||
{
|
||
// 避免 uk_order_user_type 日结撞车:负日期 + 计划尾巴
|
||
return -intval(date('Ymd', strtotime($settleDate))) * 100 + intval($planId) % 100;
|
||
}
|
||
}
|