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); } } /** * 临时分钟级调度:执行未结算订单的dmepa分红,并释放一期兑换券 */ 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. dmepa 1 分钟结算一次(提取已支付但尚未结算dmepa的合格订单) $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); // 标记订单已结算dmepa foreach ($unsettledOrders as $o) { $freshExt = \think\Db::name('shopro_order')->where('id', $o['id'])->value('ext'); $ext = is_array($freshExt) ? $freshExt : (json_decode($freshExt ?? '', true) ?: []); if (!is_array($ext)) { $ext = []; } $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. 酒道馆 1 分钟均分结算(提取未结算酒道馆的合格订单) $unsettledVenueOrders = []; $venuePool = 0; foreach ($allQualifiedOrders as $o) { $freshExt = \think\Db::name('shopro_order')->where('id', $o['id'])->value('ext'); $ext = is_array($freshExt) ? $freshExt : (json_decode($freshExt ?? '', true) ?: []); if (empty($ext['venue_settled'])) { $unsettledVenueOrders[] = $o; $rules = PartnerConfig::getOrderEffectiveRules($o); $vShare = isset($rules['venue_share']) && is_numeric($rules['venue_share']) ? (float) $rules['venue_share'] : (float) PartnerConfig::get('partner_venue_share_per_order', 100); $venuePool = bcadd((string) $venuePool, (string) $vShare, 2); } } $venueRes = ['pool' => 0, 'count' => 0, 'each' => '0.00']; $settledVenueOrderIds = []; if ((float) $venuePool > 0 && !empty($unsettledVenueOrders)) { $settledVenueOrderIds = array_column($unsettledVenueOrders, 'id'); $windowStart = time() - (PartnerConfig::activeDays() - 1) * 86400; $venueIds = $activeService->activeUserIds($windowStart, time(), ['is_venue' => 1]); $venueRes = $this->cashShareMinute($venueIds, $venuePool, 'venue_share', 'venue', '酒道馆', $settledVenueOrderIds); foreach ($unsettledVenueOrders as $o) { $freshExt = \think\Db::name('shopro_order')->where('id', $o['id'])->value('ext'); $ext = is_array($freshExt) ? $freshExt : (json_decode($freshExt ?? '', true) ?: []); if (!is_array($ext)) { $ext = []; } $ext['venue_settled'] = 1; $ext['venue_settle_time'] = time(); \think\Db::name('shopro_order')->where('id', $o['id'])->update([ 'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE), ]); } } // 4. 区县代理 1 分钟均分结算(提取未结算区县的合格订单) $unsettledCountyOrders = []; $countyPool = 0; foreach ($allQualifiedOrders as $o) { $freshExt = \think\Db::name('shopro_order')->where('id', $o['id'])->value('ext'); $ext = is_array($freshExt) ? $freshExt : (json_decode($freshExt ?? '', true) ?: []); if (!is_array($ext)) { $ext = []; } if (empty($ext['county_settled'])) { $unsettledCountyOrders[] = $o; $rules = PartnerConfig::getOrderEffectiveRules($o); $cShare = isset($rules['county_share']) && is_numeric($rules['county_share']) ? (float) $rules['county_share'] : (float) PartnerConfig::get('partner_county_share_per_order', 50); $countyPool = bcadd((string) $countyPool, (string) $cShare, 2); } } $countyRes = ['pool' => 0, 'count' => 0, 'each' => '0.00']; $settledCountyOrderIds = []; if ((float) $countyPool > 0 && !empty($unsettledCountyOrders)) { $settledCountyOrderIds = array_column($unsettledCountyOrders, 'id'); $windowStart = time() - (PartnerConfig::activeDays() - 1) * 86400; $countyIds = $activeService->activeUserIds($windowStart, time(), ['is_county' => 1]); $countyRes = $this->cashShareMinute($countyIds, $countyPool, 'county_share', 'county', '区县', $settledCountyOrderIds); foreach ($unsettledCountyOrders as $o) { $freshExt = \think\Db::name('shopro_order')->where('id', $o['id'])->value('ext'); $ext = is_array($freshExt) ? $freshExt : (json_decode($freshExt ?? '', true) ?: []); if (!is_array($ext)) { $ext = []; } $ext['county_settled'] = 1; $ext['county_settle_time'] = time(); \think\Db::name('shopro_order')->where('id', $o['id'])->update([ 'ext' => json_encode($ext, JSON_UNESCAPED_UNICODE), ]); } } // 5. 统计今日最新大盘并同步/创建今日 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'); // 当日累计发放dmepa总额 $todayMadowPool = (float) \think\Db::name('shopro_partner_reward') ->whereBetween('createtime', [$todayStart, $todayEnd]) ->where('reward_type', 'madow_share') ->where('status', 1) ->sum('madow'); // 当日累计发放酒道馆均分总额(含直接分给酒道馆与尾差/进平台金额) $todayVenuePool = (float) \think\Db::name('shopro_partner_reward') ->whereBetween('createtime', [$todayStart, $todayEnd]) ->where('reward_type', 'venue_share') ->where('status', 1) ->sum('amount'); // 当日累计发放区县均分总额(含直接分给区县与尾差/进平台金额) $todayCountyPool = (float) \think\Db::name('shopro_partner_reward') ->whereBetween('createtime', [$todayStart, $todayEnd]) ->where('reward_type', 'county_share') ->where('status', 1) ->sum('amount'); $windowStart = time() - (PartnerConfig::activeDays() - 1) * 86400; $venueUserCount = count($activeService->activeUserIds($windowStart, time(), ['is_venue' => 1])); $countyUserCount = count($activeService->activeUserIds($windowStart, time(), ['is_county' => 1])); $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->venue_pool = $todayVenuePool; $existingTodayReport->venue_user_count = $venueUserCount; $existingTodayReport->county_pool = $todayCountyPool; $existingTodayReport->county_user_count = $countyUserCount; $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' => $todayVenuePool, 'venue_user_count' => $venueUserCount, 'county_pool' => $todayCountyPool, 'county_user_count' => $countyUserCount, '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, 'venue_pool' => $todayVenuePool, 'county_pool' => $todayCountyPool, ]); 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'], 'venue_pool' => $venueRes['pool'], 'venue_each' => $venueRes['each'], 'county_pool' => $countyRes['pool'], 'county_each' => $countyRes['each'], 'settled_orders' => array_values(array_unique(array_merge($settledOrderIds, $settledVenueOrderIds, $settledCountyOrderIds))), ]; } 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 = $rateService->qualifiedGmv($start, $end); $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; $walletType = PartnerConfig::rewardWalletType(); foreach ($userIds as $userId) { $record = 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, 'wallet_type' => $walletType, ], 'status' => 1, ]); $isNew = ($record && !empty($record->_isNewCreated)) || PartnerRewardService::$lastWriteCreated; if ($isNew && (float) $each > 0) { Wallet::change($userId, $walletType, $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} 进平台", 'wallet_type' => 'system', ], 'status' => 1, ]); } } /** * 临时分钟级均分分润(酒道馆 / 区县代理) */ public function cashShareMinute($userIds, $pool, $type, $role, $label, $orderIds = []) { $count = count($userIds); $pool = round((float) $pool, 2); if ($pool <= 0) { return ['pool' => 0, 'count' => $count, 'each' => '0.00']; } $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' => $type, 'amount' => $pool, 'base_amount' => $pool, 'settle_date' => null, 'ext' => [ 'formula' => "{$label}分母为0,奖池 {$pool} 进平台", 'orders' => $orderIds, 'wallet_type' => 'system', ], 'status' => 1, ]); return ['pool' => $pool, 'count' => 0, 'each' => '0.00']; } $each = function_exists('bcdiv') ? bcdiv((string) $pool, (string) $count, 2) : number_format($pool / $count, 2, '.', ''); $distributed = 0; $walletType = PartnerConfig::rewardWalletType(); foreach ($userIds as $userId) { $record = PartnerRewardService::write([ 'order_id' => $virtualOrderId, 'buyer_id' => 0, 'user_id' => $userId, 'role' => $role, 'reward_type' => $type, 'amount' => $each, 'base_amount' => $pool, 'settle_date' => null, 'ext' => [ 'formula' => "{$label}奖池 {$pool} / {$count} = {$each}", 'share_user_count' => $count, 'pool' => $pool, 'orders' => $orderIds, 'wallet_type' => $walletType, ], 'status' => 1, ]); $isNew = ($record && !empty($record->_isNewCreated)) || PartnerRewardService::$lastWriteCreated; if ($isNew && (float) $each > 0) { Wallet::change($userId, $walletType, $each, $type, [ 'minute_settle' => 1, 'orders' => $orderIds, ]); 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' => $virtualOrderId, 'buyer_id' => 0, 'user_id' => 0, 'role' => 'system', 'reward_type' => $type, 'amount' => $tail, 'base_amount' => $pool, 'settle_date' => null, 'ext' => [ 'formula' => "{$label}尾差 {$tail} 进平台", 'orders' => $orderIds, 'wallet_type' => 'system', ], 'status' => 1, ]); } return ['pool' => $pool, 'count' => $count, 'each' => $each]; } 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) { $walletLog = \app\admin\model\shopro\user\WalletLog::where('user_id', $row->user_id) ->where('ext', 'like', '%"settle_date":"' . $settleDate . '"%') ->where('event', $row->reward_type) ->order('id', 'desc') ->find(); $targetWallet = ($walletLog && $walletLog->type === 'commission') ? 'commission' : PartnerConfig::rewardWalletType(); Wallet::change($row->user_id, $targetWallet, -$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 = $rateService->qualifiedGmv($start, $end); $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); // dmepa池 (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' => IdentityService::formatRate($rate), '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, ], ]; } }