316 行
13 KiB
PHP
316 行
13 KiB
PHP
<?php
|
||
|
||
namespace addons\shopro\service\partner;
|
||
|
||
use think\Db;
|
||
use addons\shopro\service\Wallet;
|
||
use app\admin\model\shopro\partner\DailyReport;
|
||
use app\admin\model\shopro\partner\Reward as RewardModel;
|
||
use app\admin\model\shopro\commission\Agent as AgentModel;
|
||
|
||
/**
|
||
* 日结:马豆 / 馆县均分 / 券释放
|
||
*/
|
||
class DailySettleService
|
||
{
|
||
public function run($date = null, $rerun = false)
|
||
{
|
||
$settleDate = $date ?: date('Y-m-d', strtotime('-1 day'));
|
||
$lockDir = RUNTIME_PATH . 'lock';
|
||
if (!is_dir($lockDir)) {
|
||
@mkdir($lockDir, 0755, true);
|
||
}
|
||
$lockFile = $lockDir . '/partner_settle_' . date('Ymd', strtotime($settleDate)) . '.lock';
|
||
$fp = fopen($lockFile, 'c');
|
||
if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
|
||
throw new \Exception('日结正在执行,请稍后再试');
|
||
}
|
||
|
||
try {
|
||
$exist = DailyReport::where('settle_date', $settleDate)->where('status', 1)->find();
|
||
if ($exist && !$rerun) {
|
||
return $exist;
|
||
}
|
||
if ($rerun) {
|
||
$this->clearDaily($settleDate);
|
||
}
|
||
|
||
Db::transaction(function () use ($settleDate) {
|
||
$this->settle($settleDate);
|
||
});
|
||
|
||
return DailyReport::where('settle_date', $settleDate)->find();
|
||
} catch (\Exception $e) {
|
||
DailyReport::create([
|
||
'settle_date' => $settleDate,
|
||
'status' => 0,
|
||
'error_msg' => mb_substr($e->getMessage(), 0, 500),
|
||
'createtime' => time(),
|
||
'updatetime' => time(),
|
||
]);
|
||
throw $e;
|
||
} finally {
|
||
flock($fp, LOCK_UN);
|
||
fclose($fp);
|
||
}
|
||
}
|
||
|
||
protected function settle($settleDate)
|
||
{
|
||
$start = strtotime($settleDate . ' 00:00:00');
|
||
$end = strtotime($settleDate . ' 23:59:59');
|
||
$rateService = new RateService();
|
||
$active = new ActiveService();
|
||
|
||
$orders = $rateService->qualifiedOrderQuery($start, $end)->field('o.id,o.pay_fee')->select();
|
||
$orderCount = count($orders);
|
||
$gmv = 0;
|
||
foreach ($orders as $order) {
|
||
$gmv = bcadd((string) $gmv, (string) $order['pay_fee'], 2);
|
||
}
|
||
|
||
$windowStart = strtotime($settleDate . ' 00:00:00') - (PartnerConfig::activeDays() - 1) * 86400;
|
||
$tasterIds = $active->activeUserIds($windowStart, $end);
|
||
$venueIds = $active->activeUserIds($windowStart, $end, ['is_venue' => 1]);
|
||
$countyIds = $active->activeUserIds($windowStart, $end, ['is_county' => 1]);
|
||
|
||
$madow = (new MadowService())->share($settleDate, $gmv, $tasterIds);
|
||
|
||
$venuePer = (float) PartnerConfig::get('partner_venue_share_per_order', 100);
|
||
$countyPer = (float) PartnerConfig::get('partner_county_share_per_order', 50);
|
||
$venuePool = round($orderCount * $venuePer, 2);
|
||
$countyPool = round($orderCount * $countyPer, 2);
|
||
|
||
$this->cashShare($settleDate, $venueIds, $venuePool, 'venue_share', 'venue', '酒道馆');
|
||
$this->cashShare($settleDate, $countyIds, $countyPool, 'county_share', 'county', '区县');
|
||
|
||
$voucherTotal = (new VoucherService())->releaseByDate($settleDate);
|
||
|
||
$rateLog = $rateService->snapshot($settleDate, $gmv, [
|
||
'madow_pool' => $madow['pool'],
|
||
'share_user_count' => $madow['count'],
|
||
'venue_pool' => $venuePool,
|
||
'county_pool' => $countyPool,
|
||
]);
|
||
|
||
DailyReport::create([
|
||
'settle_date' => $settleDate,
|
||
'order_count' => $orderCount,
|
||
'order_count_n' => $rateLog->order_count_n ?? $rateService->countN(),
|
||
'gmv' => $gmv,
|
||
'rate' => $rateLog->rate ?? 0,
|
||
'madow_pool' => $madow['pool'],
|
||
'share_user_count' => $madow['count'],
|
||
'venue_pool' => $venuePool,
|
||
'venue_user_count' => count($venueIds),
|
||
'county_pool' => $countyPool,
|
||
'county_user_count' => count($countyIds),
|
||
'voucher_release_total' => $voucherTotal,
|
||
'status' => 1,
|
||
'error_msg' => '',
|
||
'createtime' => time(),
|
||
'updatetime' => time(),
|
||
]);
|
||
}
|
||
|
||
protected function cashShare($settleDate, $userIds, $pool, $type, $role, $label)
|
||
{
|
||
$count = count($userIds);
|
||
$orderId = PartnerRewardService::dailyOrderId($settleDate);
|
||
if ($pool <= 0) {
|
||
return;
|
||
}
|
||
if ($count <= 0) {
|
||
PartnerRewardService::write([
|
||
'order_id' => $orderId,
|
||
'user_id' => 0,
|
||
'role' => 'system',
|
||
'reward_type' => $type,
|
||
'amount' => $pool,
|
||
'base_amount' => $pool,
|
||
'settle_date' => $settleDate,
|
||
'ext' => ['formula' => "{$label}分母为0,奖池 {$pool} 进平台"],
|
||
'status' => 1,
|
||
]);
|
||
return;
|
||
}
|
||
$each = function_exists('bcdiv') ? bcdiv((string) $pool, (string) $count, 2) : number_format($pool / $count, 2, '.', '');
|
||
$distributed = 0;
|
||
foreach ($userIds as $userId) {
|
||
PartnerRewardService::write([
|
||
'order_id' => $orderId,
|
||
'user_id' => $userId,
|
||
'role' => $role,
|
||
'reward_type' => $type,
|
||
'amount' => $each,
|
||
'base_amount' => $pool,
|
||
'settle_date' => $settleDate,
|
||
'ext' => ['formula' => "{$label}奖池 {$pool} / {$count} = {$each}", 'share_user_count' => $count],
|
||
'status' => 1,
|
||
]);
|
||
if ((float) $each > 0) {
|
||
Wallet::change($userId, 'commission', $each, $type, [
|
||
'settle_date' => $settleDate,
|
||
]);
|
||
AgentModel::where('user_id', $userId)->setInc('total_income', $each);
|
||
}
|
||
$distributed = bcadd((string) $distributed, (string) $each, 2);
|
||
}
|
||
$tail = bcsub((string) $pool, (string) $distributed, 2);
|
||
if ((float) $tail > 0) {
|
||
PartnerRewardService::write([
|
||
'order_id' => $orderId,
|
||
'user_id' => 0,
|
||
'role' => 'system',
|
||
'reward_type' => $type,
|
||
'amount' => $tail,
|
||
'base_amount' => $pool,
|
||
'settle_date' => $settleDate,
|
||
'ext' => ['formula' => "{$label}尾差 {$tail} 进平台"],
|
||
'status' => 1,
|
||
]);
|
||
}
|
||
}
|
||
|
||
public function clearDaily($settleDate)
|
||
{
|
||
(new VoucherService())->rollbackRelease($settleDate);
|
||
(new MadowService())->rollback($settleDate);
|
||
|
||
$cashRows = RewardModel::where('settle_date', $settleDate)
|
||
->whereIn('reward_type', ['venue_share', 'county_share'])
|
||
->where('status', 1)
|
||
->where('user_id', '>', 0)
|
||
->select();
|
||
foreach ($cashRows as $row) {
|
||
if ((float) $row->amount > 0) {
|
||
Wallet::change($row->user_id, 'commission', -$row->amount, 'reward_back', [
|
||
'settle_date' => $settleDate,
|
||
'rerun' => 1,
|
||
], '日结重跑扣回');
|
||
AgentModel::where('user_id', $row->user_id)->setDec('total_income', $row->amount);
|
||
}
|
||
}
|
||
|
||
RewardModel::where('settle_date', $settleDate)
|
||
->whereIn('reward_type', PartnerConfig::dailyTypes())
|
||
->delete();
|
||
DailyReport::where('settle_date', $settleDate)->delete();
|
||
\app\admin\model\shopro\partner\RateLog::where('settle_date', $settleDate)->delete();
|
||
}
|
||
|
||
/**
|
||
* 日结试算(Dry Run,只读模拟计算)
|
||
* 纯计算昨日或指定日期的分润池、人均分配额、沉淀留存与综合现金拨比,不入账不写表
|
||
*/
|
||
public function preview($date = null)
|
||
{
|
||
$settleDate = $date ?: date('Y-m-d', strtotime('-1 day'));
|
||
$start = strtotime($settleDate . ' 00:00:00');
|
||
$end = strtotime($settleDate . ' 23:59:59');
|
||
|
||
$rateService = new RateService();
|
||
$active = new ActiveService();
|
||
|
||
$orders = $rateService->qualifiedOrderQuery($start, $end)->field('o.id,o.pay_fee')->select();
|
||
$orderCount = count($orders);
|
||
$gmv = 0;
|
||
foreach ($orders as $order) {
|
||
$gmv = bcadd((string) $gmv, (string) $order['pay_fee'], 2);
|
||
}
|
||
|
||
$windowStart = strtotime($settleDate . ' 00:00:00') - (PartnerConfig::activeDays() - 1) * 86400;
|
||
$tasterIds = $active->activeUserIds($windowStart, $end);
|
||
$venueIds = $active->activeUserIds($windowStart, $end, ['is_venue' => 1]);
|
||
$countyIds = $active->activeUserIds($windowStart, $end, ['is_county' => 1]);
|
||
|
||
$tasterCount = count($tasterIds);
|
||
$venueCount = count($venueIds);
|
||
$countyCount = count($countyIds);
|
||
|
||
// 汇率预测
|
||
$orderCountN = $rateService->countN();
|
||
$rate = $rateService->calcRate($orderCountN);
|
||
|
||
// 马豆池 (1:1 GMV)
|
||
$madowPool = (float) $gmv;
|
||
$madowEach = $tasterCount > 0 ? (function_exists('bcdiv') ? bcdiv((string) $madowPool, (string) $tasterCount, 8) : number_format($madowPool / $tasterCount, 8, '.', '')) : '0.00000000';
|
||
|
||
// 酒道馆与区县均分池
|
||
$venuePer = (float) PartnerConfig::get('partner_venue_share_per_order', 100);
|
||
$countyPer = (float) PartnerConfig::get('partner_county_share_per_order', 50);
|
||
$venuePool = round($orderCount * $venuePer, 2);
|
||
$countyPool = round($orderCount * $countyPer, 2);
|
||
|
||
$venueEach = $venueCount > 0 ? round($venuePool / $venueCount, 2) : 0;
|
||
$countyEach = $countyCount > 0 ? round($countyPool / $countyCount, 2) : 0;
|
||
|
||
// 沉淀金额(若无人合格分母为0,则全部沉淀至平台)
|
||
$venueSaved = $venueCount <= 0 ? $venuePool : 0;
|
||
$countySaved = $countyCount <= 0 ? $countyPool : 0;
|
||
$totalSaved = bcadd((string) $venueSaved, (string) $countySaved, 2);
|
||
|
||
// 当日即时奖产生总额(推三返一、创客极差、平级、酒道馆/区县团队奖等)
|
||
$instantCash = (float) RewardModel::whereBetween('createtime', [$start, $end])
|
||
->whereIn('reward_type', PartnerConfig::instantTypes())
|
||
->where('status', 1)
|
||
->sum('amount');
|
||
|
||
// 当日预计现金支出 = 即时奖 + 酒道馆实际发放 + 区县实际发放
|
||
$venueActualOut = $venueCount > 0 ? $venuePool : 0;
|
||
$countyActualOut = $countyCount > 0 ? $countyPool : 0;
|
||
$estimatedDailyCash = bcadd((string) $venueActualOut, (string) $countyActualOut, 2);
|
||
$totalCashPayout = bcadd((string) $instantCash, (string) $estimatedDailyCash, 2);
|
||
|
||
// 综合现金拨比
|
||
$payoutRatio = (float) $gmv > 0 ? round(($totalCashPayout / $gmv) * 100, 2) : 0;
|
||
|
||
// 兑换券释放预计
|
||
$voucherReleaseEstimate = (float) \app\admin\model\shopro\partner\VoucherPlan::where('status', 1)
|
||
->where('released_days', '<', 365)
|
||
->sum('daily_amount');
|
||
|
||
// 是否已经执行过正式日结
|
||
$existingReport = DailyReport::where('settle_date', $settleDate)->find();
|
||
|
||
return [
|
||
'settle_date' => $settleDate,
|
||
'is_settled' => ($existingReport && $existingReport->status == 1),
|
||
'existing_report' => $existingReport,
|
||
'order_count' => $orderCount,
|
||
'order_count_n' => $orderCountN,
|
||
'gmv' => number_format($gmv, 2, '.', ''),
|
||
'rate' => number_format($rate, 8, '.', ''),
|
||
'madow' => [
|
||
'pool' => IdentityService::formatMadow($madowPool),
|
||
'user_count' => $tasterCount,
|
||
'each' => IdentityService::formatMadow($madowEach),
|
||
],
|
||
'venue' => [
|
||
'pool' => number_format($venuePool, 2, '.', ''),
|
||
'user_count' => $venueCount,
|
||
'each' => number_format($venueEach, 2, '.', ''),
|
||
'saved' => number_format($venueSaved, 2, '.', ''),
|
||
],
|
||
'county' => [
|
||
'pool' => number_format($countyPool, 2, '.', ''),
|
||
'user_count' => $countyCount,
|
||
'each' => number_format($countyEach, 2, '.', ''),
|
||
'saved' => number_format($countySaved, 2, '.', ''),
|
||
],
|
||
'voucher' => [
|
||
'release_estimate' => number_format($voucherReleaseEstimate, 2, '.', ''),
|
||
],
|
||
'financial' => [
|
||
'instant_cash' => number_format($instantCash, 2, '.', ''),
|
||
'daily_cash' => number_format($estimatedDailyCash, 2, '.', ''),
|
||
'total_cash' => number_format($totalCashPayout, 2, '.', ''),
|
||
'saved_cash' => number_format($totalSaved, 2, '.', ''),
|
||
'payout_ratio' => $payoutRatio . '%',
|
||
'payout_ratio_val' => $payoutRatio,
|
||
'is_alert' => $payoutRatio > 70,
|
||
],
|
||
];
|
||
}
|
||
}
|