351 行
12 KiB
PHP
351 行
12 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;
|
||
}
|
||
$fee = $refundFee !== null ? min($totalVoucher, round((float) $refundFee, 2)) : $totalVoucher;
|
||
if ($fee <= 0) {
|
||
return;
|
||
}
|
||
$userId = intval($order['user_id'] ?? 0);
|
||
$orderId = intval($order['id'] ?? 0);
|
||
$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,
|
||
]);
|
||
}
|
||
}
|
||
|
||
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)
|
||
{
|
||
$exists = VoucherPlan::where('order_id', $order['id'])->find();
|
||
if ($exists) {
|
||
return $exists;
|
||
}
|
||
|
||
$times = (float) PartnerConfig::get('partner_voucher_times', 1.5);
|
||
$days = intval(PartnerConfig::get('partner_voucher_days', 365));
|
||
$days = $days > 0 ? $days : 365;
|
||
$total = round((float) $order['pay_fee'] * $times, 2);
|
||
if ($total <= 0) {
|
||
return null;
|
||
}
|
||
$daily = round($total / $days, 2);
|
||
$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' => "实付{$order['pay_fee']} × {$times} = {$total},{$days}天释放"],
|
||
'status' => 1,
|
||
]);
|
||
|
||
return $plan;
|
||
}
|
||
|
||
public function releaseByDate($settleDate)
|
||
{
|
||
$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;
|
||
}
|
||
}
|