diff --git a/addons/shopro/console/PartnerSettle.php b/addons/shopro/console/PartnerSettle.php index 505951a..1395d2a 100644 --- a/addons/shopro/console/PartnerSettle.php +++ b/addons/shopro/console/PartnerSettle.php @@ -13,22 +13,40 @@ class PartnerSettle extends Command protected function configure() { $this->setName('shopro:partner:settle') - ->addOption('date', 'd', Option::VALUE_OPTIONAL, '结算日期 Y-m-d,默认昨天') + ->addOption('date', 'd', Option::VALUE_OPTIONAL, '结算日期 Y-m-d,指定日期时执行传统日结') ->addOption('rerun', 'r', Option::VALUE_NONE, '删除当天日结后重算') - ->setDescription('收益中心日结(马豆/馆县均分/兑换券释放)'); + ->addOption('daily', null, Option::VALUE_NONE, '强制执行昨天标准日结') + ->setDescription('收益中心结算(默认1分钟马豆增量结算与兑换券释放,亦支持指定日期日结)'); } protected function execute(Input $input, Output $output) { $date = $input->getOption('date'); $rerun = (bool) $input->getOption('rerun'); - $output->writeln('partner settle start ' . ($date ?: 'yesterday') . ($rerun ? ' rerun' : '')); + $isDaily = (bool) $input->getOption('daily'); + + // 指定日期或强制日结模式 + if ($date || $isDaily || $rerun) { + $settleDate = $date ?: date('Y-m-d', strtotime('-1 day')); + $output->writeln('partner daily settle start ' . $settleDate . ($rerun ? ' rerun' : '')); + try { + $report = (new DailySettleService())->run($date, $rerun); + $output->writeln('partner daily settle done: ' . json_encode($report ? $report->toArray() : [], JSON_UNESCAPED_UNICODE)); + return 0; + } catch (\Exception $e) { + $output->writeln('partner daily settle fail: ' . $e->getMessage()); + return 1; + } + } + + // 默认执行 1 分钟马豆增量结算与兑换券释放 + $output->writeln('[' . date('Y-m-d H:i:s') . '] partner minute settle start'); try { - $report = (new DailySettleService())->run($date, $rerun); - $output->writeln('partner settle done: ' . json_encode($report ? $report->toArray() : [], JSON_UNESCAPED_UNICODE)); + $result = (new DailySettleService())->runMinute(); + $output->writeln('[' . date('Y-m-d H:i:s') . '] partner minute settle done: ' . json_encode($result, JSON_UNESCAPED_UNICODE)); return 0; } catch (\Exception $e) { - $output->writeln('partner settle fail: ' . $e->getMessage()); + $output->writeln('[' . date('Y-m-d H:i:s') . '] partner minute settle fail: ' . $e->getMessage()); return 1; } } diff --git a/addons/shopro/service/partner/DailySettleService.php b/addons/shopro/service/partner/DailySettleService.php index 124c26b..18ae224 100644 --- a/addons/shopro/service/partner/DailySettleService.php +++ b/addons/shopro/service/partner/DailySettleService.php @@ -55,6 +55,148 @@ class DailySettleService } } + /** + * 临时分钟级调度:执行未结算订单的马豆分红,并释放一期兑换券 + */ + public function runMinute() + { + $lockDir = RUNTIME_PATH . 'lock'; + if (!is_dir($lockDir)) { + @mkdir($lockDir, 0755, true); + } + $lockFile = $lockDir . '/partner_settle_minute.lock'; + $fp = fopen($lockFile, 'c'); + if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) { + return ['status' => 'skipped', 'message' => '上一次分钟任务仍在执行中']; + } + + try { + $today = date('Y-m-d'); + $rateService = new RateService(); + $rewardService = new PartnerRewardService(); + $activeService = new ActiveService(); + + // 1. 兑换券 1 分钟释放一次 + $voucherTotal = (new VoucherService())->releaseMinute(); + + // 2. 马豆 1 分钟结算一次(提取已支付但尚未结算马豆的合格订单) + $allQualifiedOrders = $rateService->qualifiedOrderQuery()->field('o.id,o.pay_fee,o.user_id,o.ext,o.paid_time')->select(); + $unsettledOrders = []; + $madowGmv = 0; + + foreach ($allQualifiedOrders as $o) { + $ext = $o['ext'] ?? []; + if (is_string($ext)) { + $ext = json_decode($ext, true) ?: []; + } + if (empty($ext['madow_settled'])) { + $unsettledOrders[] = $o; + $qFee = $rewardService->getQualifiedPayFee($o); + $madowGmv = bcadd((string) $madowGmv, (string) $qFee, 2); + } + } + + $madowRes = ['pool' => 0, 'count' => 0, 'each' => 0]; + $settledOrderIds = []; + + if ((float) $madowGmv > 0 && !empty($unsettledOrders)) { + $settledOrderIds = array_column($unsettledOrders, 'id'); + $windowStart = time() - (PartnerConfig::activeDays() - 1) * 86400; + $tasterIds = $activeService->activeUserIds($windowStart, time()); + + $madowRes = (new MadowService())->shareMinute($madowGmv, $tasterIds, $settledOrderIds); + + // 标记订单已结算马豆 + foreach ($unsettledOrders as $o) { + $ext = $o['ext'] ?? []; + if (is_string($ext)) { + $ext = json_decode($ext, true) ?: []; + } + $ext['madow_settled'] = 1; + $ext['madow_settle_time'] = time(); + \think\Db::name('shopro_order')->where('id', $o['id'])->update([ + 'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE), + ]); + } + + // 清除汇率缓存,联动大盘最新动态汇率 + RateService::clearCache(); + } + + // 3. 统计今日最新大盘并同步/创建今日 DailyReport + $todayStart = strtotime($today . ' 00:00:00'); + $todayEnd = strtotime($today . ' 23:59:59'); + $todayGmv = $rateService->qualifiedGmv($todayStart, $todayEnd); + $todayOrderCount = $rateService->qualifiedOrderQuery($todayStart, $todayEnd)->count(); + $orderCountN = $rateService->countN(); + $currentRate = $rateService->calcRate($orderCountN); + + // 当日累计发放兑换券总额 + $todayVoucherReleased = (float) \think\Db::name('shopro_partner_reward') + ->whereBetween('createtime', [$todayStart, $todayEnd]) + ->where('reward_type', 'voucher_release') + ->where('status', 1) + ->sum('amount'); + + // 当日累计发放马豆总额 + $todayMadowPool = (float) \think\Db::name('shopro_partner_reward') + ->whereBetween('createtime', [$todayStart, $todayEnd]) + ->where('reward_type', 'madow_share') + ->where('status', 1) + ->sum('madow'); + + $existingTodayReport = DailyReport::where('settle_date', $today)->find(); + if ($existingTodayReport) { + $existingTodayReport->order_count = $todayOrderCount; + $existingTodayReport->order_count_n = $orderCountN; + $existingTodayReport->gmv = $todayGmv; + $existingTodayReport->rate = $currentRate; + $existingTodayReport->madow_pool = $todayMadowPool; + $existingTodayReport->voucher_release_total = $todayVoucherReleased; + $existingTodayReport->updatetime = time(); + $existingTodayReport->save(); + } else { + DailyReport::create([ + 'settle_date' => $today, + 'order_count' => $todayOrderCount, + 'order_count_n' => $orderCountN, + 'gmv' => $todayGmv, + 'rate' => $currentRate, + 'madow_pool' => $todayMadowPool, + 'share_user_count' => $madowRes['count'] ?? 0, + 'venue_pool' => 0, + 'venue_user_count' => 0, + 'county_pool' => 0, + 'county_user_count' => 0, + 'voucher_release_total' => $todayVoucherReleased, + 'status' => 1, + 'error_msg' => '', + 'createtime' => time(), + 'updatetime' => time(), + ]); + } + + // 更新汇率日志 + $rateService->snapshot($today, $todayGmv, [ + 'madow_pool' => $todayMadowPool, + 'share_user_count' => $madowRes['count'] ?? 0, + ]); + + return [ + 'status' => 'success', + 'time' => date('Y-m-d H:i:s'), + 'voucher_released' => $voucherTotal, + 'madow_settled_gmv' => (float) $madowGmv, + 'madow_pool' => $madowRes['pool'], + 'madow_each' => $madowRes['each'], + 'settled_orders' => $settledOrderIds, + ]; + } finally { + flock($fp, LOCK_UN); + fclose($fp); + } + } + protected function settle($settleDate) { $start = strtotime($settleDate . ' 00:00:00'); diff --git a/addons/shopro/service/partner/MadowService.php b/addons/shopro/service/partner/MadowService.php index b09f77c..b4842c2 100644 --- a/addons/shopro/service/partner/MadowService.php +++ b/addons/shopro/service/partner/MadowService.php @@ -82,6 +82,86 @@ class MadowService return ['pool' => $pool, 'count' => $count, 'each' => $each]; } + /** + * 临时分钟级马豆结算均分 + */ + public function shareMinute($gmv, $userIds, $orderIds = []) + { + $count = count($userIds); + $pool = (float) $gmv; + if ($pool <= 0) { + return ['pool' => 0, 'count' => $count, 'each' => 0]; + } + + $minId = (int) \think\Db::name('shopro_partner_reward')->where('order_id', '<', 0)->min('order_id'); + $virtualOrderId = min(-100000, $minId - 1); + + if ($count <= 0) { + PartnerRewardService::write([ + 'order_id' => $virtualOrderId, + 'buyer_id' => 0, + 'user_id' => 0, + 'role' => 'system', + 'reward_type' => 'madow_share', + 'amount' => 0, + 'madow' => $pool, + 'base_amount' => $pool, + 'settle_date' => null, + 'ext' => ['formula' => "分母为0,马豆池 {$pool} 进平台", 'orders' => $orderIds], + 'status' => 1, + ]); + return ['pool' => $pool, 'count' => 0, 'each' => 0]; + } + + $each = function_exists('bcdiv') ? bcdiv((string) $pool, (string) $count, 8) : number_format($pool / $count, 8, '.', ''); + $distributed = 0; + $now = time(); + $chunks = array_chunk($userIds, 500); + + foreach ($chunks as $chunk) { + AgentModel::whereIn('user_id', $chunk)->setInc('madow', $each); + $batchRows = []; + foreach ($chunk as $userId) { + $batchRows[] = [ + 'order_id' => $virtualOrderId, + 'buyer_id' => 0, + 'user_id' => $userId, + 'role' => 'taster', + 'reward_type' => 'madow_share', + 'amount' => 0, + 'madow' => $each, + 'base_amount' => $pool, + 'settle_date' => null, + 'ext' => json_encode(['formula' => "GMV {$pool} / {$count} = {$each}", 'share_user_count' => $count, 'gmv' => $pool, 'orders' => $orderIds], JSON_UNESCAPED_UNICODE), + 'status' => 1, + 'createtime' => $now, + 'updatetime' => $now, + ]; + $distributed = function_exists('bcadd') ? bcadd((string) $distributed, (string) $each, 8) : ($distributed + $each); + } + \think\Db::name('shopro_partner_reward')->insertAll($batchRows); + } + + $tail = function_exists('bcsub') ? bcsub((string) $pool, (string) $distributed, 8) : round($pool - $distributed, 8); + if ((float) $tail > 0) { + PartnerRewardService::write([ + 'order_id' => $virtualOrderId, + 'buyer_id' => 0, + 'user_id' => 0, + 'role' => 'system', + 'reward_type' => 'madow_share', + 'amount' => 0, + 'madow' => $tail, + 'base_amount' => $pool, + 'settle_date' => null, + 'ext' => ['formula' => "尾差 {$tail} 进平台", 'orders' => $orderIds], + 'status' => 1, + ]); + } + + return ['pool' => $pool, 'count' => $count, 'each' => $each]; + } + public function rollback($settleDate) { $rows = \app\admin\model\shopro\partner\Reward::where('settle_date', $settleDate) diff --git a/addons/shopro/service/partner/VoucherService.php b/addons/shopro/service/partner/VoucherService.php index 104ff7b..5d98fdb 100644 --- a/addons/shopro/service/partner/VoucherService.php +++ b/addons/shopro/service/partner/VoucherService.php @@ -144,7 +144,8 @@ class VoucherService return null; } $daily = round($total / $days, 2); - $startDate = date('Y-m-d', strtotime('+1 day', $this->orderTime($order))); + // 临时模式:当天立即生效起算,支持 1 分钟释放联调 + $startDate = date('Y-m-d', $this->orderTime($order)); $plan = VoucherPlan::create([ 'user_id' => $buyerId, @@ -183,6 +184,73 @@ class VoucherService 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) { $plans = VoucherPlan::where('status', 1) diff --git a/application/admin/controller/shopro/Dashboard.php b/application/admin/controller/shopro/Dashboard.php index 8ad2978..ff7234e 100644 --- a/application/admin/controller/shopro/Dashboard.php +++ b/application/admin/controller/shopro/Dashboard.php @@ -141,12 +141,18 @@ class Dashboard extends Common $currentRate = $rateService->currentRate(); $totalMadowCny = round($totalMadow * $currentRate, 2); $historyOrderN = $rateService->countN(); + $rawBase = PartnerConfig::get('partner_madow_base_rate', '0.00000075'); + $baseRate = IdentityService::formatRate($rawBase); + $stepOrders = intval(PartnerConfig::get('partner_madow_step_orders', 1)); + $stepOrders = $stepOrders > 0 ? $stepOrders : 1; $latestReport = DailyReport::order('settle_date', 'desc')->find(); $yesterdayMadowPool = $latestReport ? (float) $latestReport->madow_pool : 0.00; $madowData = [ 'total_madow' => IdentityService::formatMadow($totalMadow), + 'base_rate' => $baseRate, + 'step_orders' => $stepOrders, 'current_rate' => number_format($currentRate, 10, '.', ''), 'rate_per_wan' => number_format($currentRate * 10000, 4, '.', ''), 'total_madow_cny' => number_format($totalMadowCny, 2, '.', ''), diff --git a/application/admin/view/shopro/dashboard/index.html b/application/admin/view/shopro/dashboard/index.html index 504cca9..7b2477d 100644 --- a/application/admin/view/shopro/dashboard/index.html +++ b/application/admin/view/shopro/dashboard/index.html @@ -518,7 +518,14 @@