文件
Henglianshutong/addons/shopro/service/partner/VoucherService.php

459 行
16 KiB
PHP

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

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

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