diff --git a/addons/shopro/Shopro.php b/addons/shopro/Shopro.php index 7d26e68..1936ffe 100755 --- a/addons/shopro/Shopro.php +++ b/addons/shopro/Shopro.php @@ -22,6 +22,7 @@ class Shopro extends Addons // 创建菜单 $menu = self::getMenu(); Menu::create($menu['new']); + self::flattenShoproMenus(); return true; } @@ -33,7 +34,10 @@ class Shopro extends Addons public function uninstall() { // 删除菜单 - Menu::delete('shopro'); + $ids = AuthRule::where('name', 'like', 'shopro%')->column('id'); + if ($ids) { + AuthRule::destroy($ids); + } return true; } @@ -44,7 +48,8 @@ class Shopro extends Addons public function enable() { // 启用菜单 - Menu::enable('shopro'); + AuthRule::where('name', 'like', 'shopro%')->update(['status' => 'normal']); + self::flattenShoproMenus(); return true; } @@ -57,6 +62,7 @@ class Shopro extends Addons // 更新菜单 $menu = self::getMenu(); Menu::upgrade('shopro', $menu['new']); + self::flattenShoproMenus(); return true; } @@ -67,7 +73,7 @@ class Shopro extends Addons public function disable() { // 禁用菜单 - Menu::disable('shopro'); + AuthRule::where('name', 'like', 'shopro%')->update(['status' => 'hidden']); return true; } @@ -133,6 +139,45 @@ class Shopro extends Addons return ['new' => $newMenu, 'old' => $oldMenu]; } + /** + * 业务菜单提到一级侧栏,shopro 只作插件根节点(不显示) + */ + public static function flattenShoproMenus() + { + $root = AuthRule::getByName('shopro'); + if (!$root) { + return; + } + + AuthRule::where('id', $root['id'])->update([ + 'ismenu' => 0, + 'title' => 'Shopro', + ]); + + AuthRule::where('pid', $root['id'])->where('ismenu', 1)->update(['pid' => 0]); + + $weighs = [ + 'shopro/dashboard' => 300, + 'shopro/order' => 290, + 'shopro/goods' => 280, + 'shopro/user/user' => 270, + 'shopro/commission' => 260, + 'shopro/withdraw' => 250, + 'shopro/activity/activity' => 240, + 'shopro/decorate' => 230, + 'shopro/chat' => 220, + 'shopro/wechat' => 210, + 'shopro/feedback' => 200, + 'shopro/config' => 190, + 'shopro/dispatch/dispatch' => 180, + 'shopro/notification/config' => 170, + 'shopro/data' => 160, + ]; + foreach ($weighs as $name => $weigh) { + AuthRule::where('name', $name)->update(['weigh' => $weigh]); + } + } + /** diff --git a/addons/shopro/config/menu.php b/addons/shopro/config/menu.php index 289496a..5595f4f 100644 --- a/addons/shopro/config/menu.php +++ b/addons/shopro/config/menu.php @@ -8,12 +8,12 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro', - 'title' => 'Shopro商城', + 'title' => 'Shopro', 'icon' => 'fa fa-dropbox', 'url' => '', 'condition' => '', - 'remark' => '', - 'ismenu' => 1, + 'remark' => '插件根节点,不在侧栏展示', + 'ismenu' => 0, 'menutype' => 'addtabs', 'extend' => '', 'py' => 'Ssc', @@ -2744,7 +2744,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/config/commission', - 'title' => '合伙股东设置', + 'title' => '分销基础', 'icon' => 'fa fa-circle-o', 'url' => '', 'condition' => '', @@ -3024,7 +3024,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/commission', - 'title' => '合伙股东', + 'title' => '收益中心', 'icon' => 'fa fa-users', 'url' => '', 'condition' => '', @@ -3032,8 +3032,8 @@ return array ( 'ismenu' => 1, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'hhgd', - 'pinyin' => 'hehuogudong', + 'py' => 'syzx', + 'pinyin' => 'shouyizhongxin', 'weigh' => 90, 'sublist' => array ( @@ -3041,7 +3041,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/commission/agent', - 'title' => '股东档案', + 'title' => '成员档案', 'icon' => 'fa fa-user', 'url' => '', 'condition' => '', @@ -3049,8 +3049,8 @@ return array ( 'ismenu' => 1, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'gdda', - 'pinyin' => 'gudongdangan', + 'py' => 'cyda', + 'pinyin' => 'chengyuandangan', 'weigh' => 100, 'sublist' => array ( @@ -3508,16 +3508,16 @@ return array ( 'type' => 'file', 'name' => 'shopro/partner/reward', 'title' => '收益流水', - 'icon' => 'fa fa-list-alt', + 'icon' => 'fa fa-rmb', 'url' => '', 'condition' => '', - 'remark' => '', + 'remark' => '即时奖与日结收益明细', 'ismenu' => 1, 'menutype' => 'addtabs', 'extend' => '', 'py' => 'syls', 'pinyin' => 'shouyiliushui', - 'weigh' => 7, + 'weigh' => 70, 'sublist' => array ( 0 => @@ -3572,7 +3572,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/partner/reward/back', - 'title' => '退回', + 'title' => '退回收益', 'icon' => 'fa fa-circle-o', 'url' => '', 'condition' => '', @@ -3580,8 +3580,8 @@ return array ( 'ismenu' => 0, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'th', - 'pinyin' => 'tuihui', + 'py' => 'thsy', + 'pinyin' => 'tuihuishouyi', 'weigh' => 0, ), 4 => @@ -3600,6 +3600,22 @@ return array ( 'pinyin' => 'zhengdantuihui', 'weigh' => 0, ), + 5 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/reward/orderTree', + 'title' => '订单利益透视', + 'icon' => 'fa fa-circle-o', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 0, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'ddlyts', + 'pinyin' => 'dingdanliyitoushi', + 'weigh' => 0, + ), ), ), 7 => @@ -3607,16 +3623,16 @@ return array ( 'type' => 'file', 'name' => 'shopro/partner/daily', 'title' => '日结对账', - 'icon' => 'fa fa-calendar', + 'icon' => 'fa fa-calendar-check-o', 'url' => '', 'condition' => '', - 'remark' => '', + 'remark' => '按日查看均分池与重跑', 'ismenu' => 1, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'rjdiz', + 'py' => 'rjdz', 'pinyin' => 'rijieduizhang', - 'weigh' => 6, + 'weigh' => 60, 'sublist' => array ( 0 => @@ -3639,7 +3655,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/partner/daily/rerun', - 'title' => '重跑', + 'title' => '重跑日结', 'icon' => 'fa fa-circle-o', 'url' => '', 'condition' => '', @@ -3647,8 +3663,24 @@ return array ( 'ismenu' => 0, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'cp', - 'pinyin' => 'chongpao', + 'py' => 'cprj', + 'pinyin' => 'chongpaorijie', + 'weigh' => 0, + ), + 2 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/daily/preview', + 'title' => '日结试算', + 'icon' => 'fa fa-circle-o', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 0, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'rjss', + 'pinyin' => 'rijieshisuan', 'weigh' => 0, ), ), @@ -3657,17 +3689,17 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/partner/voucher', - 'title' => '兑换券计划', + 'title' => '兑换券', 'icon' => 'fa fa-ticket', 'url' => '', 'condition' => '', - 'remark' => '', + 'remark' => '兑换券计划与释放记录', 'ismenu' => 1, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'dhqjh', - 'pinyin' => 'duihuanquanjihua', - 'weigh' => 5, + 'py' => 'dhq', + 'pinyin' => 'duihuanquan', + 'weigh' => 50, 'sublist' => array ( 0 => @@ -3690,7 +3722,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/partner/voucher/close', - 'title' => '关闭', + 'title' => '关闭计划', 'icon' => 'fa fa-circle-o', 'url' => '', 'condition' => '', @@ -3698,8 +3730,8 @@ return array ( 'ismenu' => 0, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'gb', - 'pinyin' => 'guanbi', + 'py' => 'gbjh', + 'pinyin' => 'guanbijihua', 'weigh' => 0, ), 2 => diff --git a/addons/shopro/controller/commission/Agent.php b/addons/shopro/controller/commission/Agent.php index 973a7cc..bc5e31a 100644 --- a/addons/shopro/controller/commission/Agent.php +++ b/addons/shopro/controller/commission/Agent.php @@ -126,21 +126,217 @@ class Agent extends Commission public function team() { $agentId = $this->service->user->id; + $levelType = $this->request->param('level_type', '1'); // 1=直推, 2=间推, all=全部 + $filterType = $this->request->param('filter_type', 'all'); // all, partner, active, expiring, inactive + $keyword = trim((string) $this->request->param('keyword', '')); + $sort = $this->request->param('sort', 'createtime'); // createtime, child_count, consume + $listRows = (int) $this->request->param('list_rows', 10); - $data = UserModel::where('parent_user_id', $agentId) + // 1. 获取直推一级成员 ID 列表 + $level1Ids = UserModel::where('parent_user_id', $agentId) ->where('status', 'normal') - ->with(['agent' => function ($query) { - return $query->with('level_info'); - }]) - ->paginate($this->request->param('list_rows', 8)); + ->column('id'); - $data->each(function ($user) { - if ($user->agent) { - $user->setRelation('agent', \addons\shopro\service\partner\IdentityService::attachAgent($user->agent)); + // 2. 根据 level_type 确定父级范围 + $query = UserModel::alias('u')->where('u.status', 'normal'); + if ($levelType == '2') { + if (empty($level1Ids)) { + $query->whereRaw('1=0'); + } else { + $query->whereIn('u.parent_user_id', $level1Ids); } + } elseif ($levelType == 'all') { + $allParentIds = array_merge([$agentId], $level1Ids); + $query->whereIn('u.parent_user_id', $allParentIds); + } else { + $query->where('u.parent_user_id', $agentId); + } + + // 3. 关键字搜索 (昵称/手机号/ID) + if ($keyword !== '') { + $query->where(function ($q) use ($keyword) { + if (is_numeric($keyword)) { + $q->where('u.id', intval($keyword)) + ->whereOr('u.mobile', 'like', "%{$keyword}%") + ->whereOr('u.nickname', 'like', "%{$keyword}%"); + } else { + $q->where('u.nickname', 'like', "%{$keyword}%"); + } + }); + } + + // 4. 计算活跃度时间窗口 (默认30天保活,剩余<=5天为临期) + $windowDays = \addons\shopro\service\partner\PartnerConfig::activeDays(); + $windowStart = (new \addons\shopro\service\partner\ActiveService())->windowStart(); + $expiringStart = time() - ($windowDays - 5) * 86400; + + // 5. 身份与活跃状态过滤 + if ($filterType === 'partner') { + $partnerUserIds = AgentModel::where('status', 'in', ['normal', 'freeze']) + ->where(function ($q) { + $q->where('level', '>=', 2) + ->whereOr('is_venue', 1) + ->whereOr('is_county', 1); + })->column('user_id'); + if (empty($partnerUserIds)) { + $query->whereRaw('1=0'); + } else { + $query->whereIn('u.id', $partnerUserIds); + } + } elseif ($filterType === 'active') { + $activeUserIds = AgentModel::where('last_self_order_time', '>=', $expiringStart)->column('user_id'); + if (empty($activeUserIds)) { + $query->whereRaw('1=0'); + } else { + $query->whereIn('u.id', $activeUserIds); + } + } elseif ($filterType === 'expiring') { + $expiringUserIds = AgentModel::where('last_self_order_time', '>=', $windowStart) + ->where('last_self_order_time', '<', $expiringStart) + ->column('user_id'); + if (empty($expiringUserIds)) { + $query->whereRaw('1=0'); + } else { + $query->whereIn('u.id', $expiringUserIds); + } + } elseif ($filterType === 'inactive') { + $validUserIds = AgentModel::where('last_self_order_time', '>=', $windowStart)->column('user_id'); + if (!empty($validUserIds)) { + $query->whereNotIn('u.id', $validUserIds); + } + } + + // 6. 排序处理 + if ($sort === 'child_count') { + $agentTable = (new AgentModel())->getTable(); + $query->join($agentTable . ' a', 'u.id = a.user_id', 'LEFT') + ->order('a.child_user_count_1', 'desc') + ->order('u.id', 'desc'); + } elseif ($sort === 'consume') { + $query->order('u.total_consume', 'desc')->order('u.id', 'desc'); + } else { + $query->order('u.createtime', 'desc')->order('u.id', 'desc'); + } + $query->field('u.*'); + + // 7. 分页并加载推荐人与分销商信息 + $data = $query->with([ + 'parentUser' => function ($q) { + $q->field('id, nickname, avatar, mobile'); + }, + 'agent' => function ($q) { + $q->with('level_info'); + } + ])->paginate($listRows); + + // 8. 增强成员卡片数据 + $now = time(); + $data->each(function ($user) use ($agentId, $windowDays, $now) { + $userAgent = $user->agent; + if ($userAgent) { + $attached = \addons\shopro\service\partner\IdentityService::attachAgent($userAgent); + $user->setRelation('agent', $attached); + + $last = intval($attached['last_self_order_time'] ?? 0); + $isActive = !empty($attached['active_30d']); + + if ($isActive) { + if ($last > 0) { + $daysPassed = floor(($now - $last) / 86400); + $remaining = max(0, intval($windowDays - $daysPassed)); + $user->active_status = ($remaining <= 5) ? 'expiring' : 'active'; + $user->days_remaining = $remaining; + } else { + $user->active_status = 'active'; + $user->days_remaining = $windowDays; + } + } else { + $user->active_status = 'inactive'; + $user->days_remaining = 0; + } + + $user->order_count = intval($userAgent['child_order_count_0'] ?? 0); + $user->consume_amount = \addons\shopro\service\partner\IdentityService::formatMoney($userAgent['child_order_money_0'] ?? ($user->total_consume ?? 0)); + $user->child_user_count_1 = intval($userAgent['child_user_count_1'] ?? 0); + } else { + $user->active_status = 'inactive'; + $user->days_remaining = 0; + $user->order_count = 0; + $user->consume_amount = \addons\shopro\service\partner\IdentityService::formatMoney($user->total_consume ?? 0); + $user->child_user_count_1 = 0; + } + + $user->team_level = ($user->parent_user_id == $agentId) ? 1 : 2; + $rawMobile = $user->mobile ? (string) $user->mobile : ''; + $user->mobile_raw = $rawMobile; + $user->mobile_hide = $rawMobile ? (strlen($rawMobile) == 11 ? substr_replace($rawMobile, '****', 3, 4) : $rawMobile) : ''; }); - $this->success("", $data); + // 9. 计算整体团队健康度统计 + $summary = $this->getTeamHealthSummary($agentId, $level1Ids, $windowStart, $expiringStart); + + $result = $data->toArray(); + $result['summary'] = $summary; + + $this->success("", $result); + } + + /** + * 计算团队保活健康度统计 + */ + protected function getTeamHealthSummary($agentId, $level1Ids, $windowStart, $expiringStart) + { + $allMembers = array_merge([$agentId], $level1Ids); + $teamUserIds = UserModel::whereIn('parent_user_id', $allMembers) + ->where('status', 'normal') + ->column('id'); + + if (empty($teamUserIds)) { + return [ + 'total_count' => 0, + 'active_count' => 0, + 'expiring_count' => 0, + 'inactive_count' => 0, + 'partner_count' => 0, + ]; + } + + $agents = AgentModel::whereIn('user_id', $teamUserIds)->select(); + $activeCount = 0; + $expiringCount = 0; + $inactiveCount = 0; + $partnerCount = 0; + + foreach ($agents as $ag) { + $level = intval($ag->level ?? 0); + $isVenue = intval($ag->is_venue ?? 0); + $isCounty = intval($ag->is_county ?? 0); + if ($level >= 2 || $isVenue == 1 || $isCounty == 1) { + $partnerCount++; + } + + $last = intval($ag->last_self_order_time ?? 0); + if ($last >= $expiringStart) { + $activeCount++; + } elseif ($last >= $windowStart) { + $expiringCount++; + } else { + $inactiveCount++; + } + } + + $missingAgentCount = count($teamUserIds) - count($agents); + if ($missingAgentCount > 0) { + $inactiveCount += $missingAgentCount; + } + + return [ + 'total_count' => count($teamUserIds), + 'active_count' => $activeCount, + 'expiring_count' => $expiringCount, + 'inactive_count' => $inactiveCount, + 'partner_count' => $partnerCount, + ]; } // 佣金转余额 diff --git a/addons/shopro/job/PartnerSettle.php b/addons/shopro/job/PartnerSettle.php index 4996f9f..edafa65 100644 --- a/addons/shopro/job/PartnerSettle.php +++ b/addons/shopro/job/PartnerSettle.php @@ -23,8 +23,18 @@ class PartnerSettle extends BaseJob $data = $e->getResponse()->getData(); $message = $data ? ($data['msg'] ?? '') : $e->getMessage(); format_log_error($e, 'PartnerSettle.HttpResponseException', $message); + if ($job->attempts() >= 3) { + $job->delete(); + } else { + $job->release(5); + } } catch (\Exception $e) { format_log_error($e, 'PartnerSettle'); + if ($job->attempts() >= 3) { + $job->delete(); + } else { + $job->release(5); + } } } } diff --git a/addons/shopro/listener/Partner.php b/addons/shopro/listener/Partner.php index d48a342..d449146 100644 --- a/addons/shopro/listener/Partner.php +++ b/addons/shopro/listener/Partner.php @@ -23,7 +23,7 @@ class Partner try { (new VoucherService())->consumeByOrder($orderModel); } catch (\Exception $e) { - // 表未升级时不阻断支付 + format_log_error($e, 'Partner.consumeByOrder', '兑换券核销失败: ' . $e->getMessage()); } } if (!PartnerConfig::isEnabled()) { @@ -55,12 +55,45 @@ class Partner public function orderItemRefundAfter($params) { $order = $params['order']; + $item = $params['item'] ?? null; if (!$order || empty($order['id'])) { return; } + try { - (new PartnerRewardService())->refund($order['id']); + $service = new PartnerRewardService(); + $orderId = intval($order['id']); + + if ($this->checkIsAllRefund($orderId)) { + // 全单退款:全额回滚即时奖与兑换券计划 + $service->refund($orderId); + } elseif ($item) { + // 部分退款:仅调整兑换券额度,不全额剥夺分销商奖励 + $service->refundPartial($order, $item); + } } catch (\Exception $e) { + format_log_error($e, 'Partner.orderItemRefundAfter', '合伙股东退款处理异常: ' . $e->getMessage()); } } + + /** + * 检查订单是否已全部退款或取消关闭 + */ + protected function checkIsAllRefund($orderId) + { + $order = OrderModel::where('id', $orderId)->find(); + if (!$order) { + return true; + } + if (in_array($order->status, [OrderModel::STATUS_CLOSED, OrderModel::STATUS_CANCEL])) { + return true; + } + + $unrefundedCount = \think\Db::name('shopro_order_item') + ->where('order_id', $orderId) + ->where('refund_status', '<>', \app\admin\model\shopro\order\OrderItem::REFUND_STATUS_COMPLETED) + ->count(); + + return $unrefundedCount === 0; + } } diff --git a/addons/shopro/service/partner/ActiveService.php b/addons/shopro/service/partner/ActiveService.php index 991f370..231e53a 100644 --- a/addons/shopro/service/partner/ActiveService.php +++ b/addons/shopro/service/partner/ActiveService.php @@ -50,16 +50,41 @@ class ActiveService public function activeUserIds($start, $end, $extraWhere = []) { - $query = \think\Db::name('shopro_commission_agent') + $baseQuery = \think\Db::name('shopro_commission_agent') ->where('status', 'in', ['normal', 'freeze']) - ->where('level', '>=', 1) - ->where(function ($q) use ($start, $end) { - $q->where('last_self_order_time', '>=', $start) - ->where('last_self_order_time', '<=', $end); - }); + ->where('level', '>=', 1); foreach ($extraWhere as $field => $value) { - $query->where($field, $value); + $baseQuery->where($field, $value); } - return $query->column('user_id'); + + // 1. 优先通过 last_self_order_time 索引快速命中 + $hitUserIds = (clone $baseQuery)->where(function ($q) use ($start, $end) { + $q->where('last_self_order_time', '>=', $start) + ->where('last_self_order_time', '<=', $end); + })->column('user_id'); + + // 2. 容错:查找 last_self_order_time 为空或为 0 的代理商 + $nullUserIds = (clone $baseQuery)->where(function ($q) { + $q->whereNull('last_self_order_time')->whereOr('last_self_order_time', 0); + })->column('user_id'); + + if (!empty($nullUserIds)) { + $orderHits = \think\Db::name('shopro_order') + ->whereIn('user_id', $nullUserIds) + ->where('type', '<>', 'score') + ->where('pay_fee', '>', 0) + ->whereIn('status', ['paid', 'completed']) + ->where('paid_time', '>=', $start) + ->where('paid_time', '<=', $end) + ->group('user_id') + ->column('max(paid_time)', 'user_id'); + + foreach ($orderHits as $uId => $lastTime) { + \think\Db::name('shopro_commission_agent')->where('user_id', $uId)->update(['last_self_order_time' => $lastTime]); + $hitUserIds[] = intval($uId); + } + } + + return array_values(array_unique(array_map('intval', $hitUserIds))); } } diff --git a/addons/shopro/service/partner/DailySettleService.php b/addons/shopro/service/partner/DailySettleService.php index 09e0470..78737ed 100644 --- a/addons/shopro/service/partner/DailySettleService.php +++ b/addons/shopro/service/partner/DailySettleService.php @@ -198,4 +198,118 @@ class DailySettleService 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' => number_format($madowPool, 2, '.', ''), + 'user_count' => $tasterCount, + 'each' => $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, + ], + ]; + } } diff --git a/addons/shopro/service/partner/MadowService.php b/addons/shopro/service/partner/MadowService.php index 3ac7ce8..b09f77c 100644 --- a/addons/shopro/service/partner/MadowService.php +++ b/addons/shopro/service/partner/MadowService.php @@ -35,22 +35,32 @@ class MadowService $each = function_exists('bcdiv') ? bcdiv((string) $pool, (string) $count, 8) : number_format($pool / $count, 8, '.', ''); $distributed = 0; - foreach ($userIds as $userId) { - AgentModel::where('user_id', $userId)->setInc('madow', $each); - PartnerRewardService::write([ - 'order_id' => PartnerRewardService::dailyOrderId($settleDate), - 'buyer_id' => 0, - 'user_id' => $userId, - 'role' => 'taster', - 'reward_type' => 'madow_share', - 'amount' => 0, - 'madow' => $each, - 'base_amount' => $pool, - 'settle_date' => $settleDate, - 'ext' => ['formula' => "GMV {$pool} / {$count} = {$each}", 'share_user_count' => $count, 'gmv' => $pool], - 'status' => 1, - ]); - $distributed = function_exists('bcadd') ? bcadd((string) $distributed, (string) $each, 8) : ($distributed + $each); + $dailyOrderId = PartnerRewardService::dailyOrderId($settleDate); + $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' => $dailyOrderId, + 'buyer_id' => 0, + 'user_id' => $userId, + 'role' => 'taster', + 'reward_type' => 'madow_share', + 'amount' => 0, + 'madow' => $each, + 'base_amount' => $pool, + 'settle_date' => $settleDate, + 'ext' => json_encode(['formula' => "GMV {$pool} / {$count} = {$each}", 'share_user_count' => $count, 'gmv' => $pool], 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) { @@ -80,10 +90,8 @@ class MadowService ->where('user_id', '>', 0) ->select(); foreach ($rows as $row) { - $agent = AgentModel::where('user_id', $row->user_id)->find(); - if ($agent && (float) $row->madow > 0) { - $agent->madow = max(0, (float) $agent->madow - (float) $row->madow); - $agent->save(); + if ((float) $row->madow > 0) { + AgentModel::where('user_id', $row->user_id)->setDec('madow', $row->madow); } } } diff --git a/addons/shopro/service/partner/PartnerConfig.php b/addons/shopro/service/partner/PartnerConfig.php index 6977569..88b178d 100644 --- a/addons/shopro/service/partner/PartnerConfig.php +++ b/addons/shopro/service/partner/PartnerConfig.php @@ -10,7 +10,13 @@ class PartnerConfig public static function get($key, $default = null) { $code = strpos($key, 'shop.commission.') === 0 ? $key : 'shop.commission.' . $key; - $value = sheep_config($code); + if (!function_exists('sheep_config')) { + $helper = defined('ROOT_PATH') ? (ROOT_PATH . 'addons/shopro/helper/helper.php') : (__DIR__ . '/../../helper/helper.php'); + if (file_exists($helper)) { + require_once $helper; + } + } + $value = function_exists('sheep_config') ? sheep_config($code) : null; if ($value === null || $value === '') { return $default; } @@ -84,6 +90,8 @@ class PartnerConfig 'madow_share' => '每日分红马豆', 'voucher_grant' => '兑换券发放', 'voucher_release' => '兑换券释放', + 'voucher_consume' => '兑换券消费抵扣', + 'voucher_consume_refund' => '兑换券退款退还', ]; } diff --git a/addons/shopro/service/partner/PartnerRewardService.php b/addons/shopro/service/partner/PartnerRewardService.php index 42a8ff2..49cd0dd 100644 --- a/addons/shopro/service/partner/PartnerRewardService.php +++ b/addons/shopro/service/partner/PartnerRewardService.php @@ -20,72 +20,111 @@ class PartnerRewardService return false; } - $order = OrderModel::with(['items'])->where('id', $orderId)->lock(true)->find(); - if (!$order) { - return false; - } - if (!$this->isQualified($order)) { - return false; - } + return Db::transaction(function () use ($orderId) { + $order = OrderModel::with(['items'])->where('id', $orderId)->lock(true)->find(); + if (!$order) { + return false; + } + if (!$this->isQualified($order)) { + return false; + } + + $exists = RewardModel::where('order_id', $orderId)->where('order_id', '>', 0)->find(); + if ($exists) { + return true; + } + + $buyerId = intval($order['user_id']); + $active = new ActiveService(); + $chainService = new ChainService(); + $parentChain = $chainService->collect($buyerId, false); + $selfChain = $chainService->collect($buyerId, true); + + $parent = $parentChain[0] ?? null; + (new Push3Reward())->run($order, $buyerId, $parent, $active); + + $diff = (new MakerDiffReward())->run($order, $buyerId, $parentChain, $active); + (new MakerPeerReward())->run($order, $buyerId, $parentChain, $diff, $active); + + $team = (new TeamSplitReward())->run($order, $buyerId, $selfChain, $active); + (new PeerShareReward())->run($order, $buyerId, $selfChain, $team, $active); + + (new VoucherService())->grant($order, $buyerId); - $exists = RewardModel::where('order_id', $orderId)->where('order_id', '>', 0)->find(); - if ($exists) { return true; - } - - $buyerId = intval($order['user_id']); - $active = new ActiveService(); - $chainService = new ChainService(); - $parentChain = $chainService->collect($buyerId, false); - $selfChain = $chainService->collect($buyerId, true); - - $parent = $parentChain[0] ?? null; - (new Push3Reward())->run($order, $buyerId, $parent, $active); - - $diff = (new MakerDiffReward())->run($order, $buyerId, $parentChain, $active); - (new MakerPeerReward())->run($order, $buyerId, $parentChain, $diff, $active); - - $team = (new TeamSplitReward())->run($order, $buyerId, $selfChain, $active); - (new PeerShareReward())->run($order, $buyerId, $selfChain, $team, $active); - - (new VoucherService())->grant($order, $buyerId); - - return true; + }); } public function refund($orderId) { - $rewards = RewardModel::where('order_id', $orderId) - ->where('order_id', '>', 0) - ->where('status', 1) - ->whereIn('reward_type', array_merge(PartnerConfig::instantTypes(), ['voucher_grant'])) - ->lock(true) - ->select(); - $order = OrderModel::where('id', $orderId)->find(); - if ($order) { - (new VoucherService())->refundConsume($order); - } - if (!$rewards || count($rewards) == 0) { + return Db::transaction(function () use ($orderId) { + $rewards = RewardModel::where('order_id', $orderId) + ->where('order_id', '>', 0) + ->where('status', 1) + ->whereIn('reward_type', array_merge(PartnerConfig::instantTypes(), ['voucher_grant'])) + ->lock(true) + ->select(); + $order = OrderModel::where('id', $orderId)->find(); + if ($order) { + (new VoucherService())->refundConsume($order); + } + if (!$rewards || count($rewards) == 0) { + (new VoucherService())->closeByOrder($orderId); + return true; + } + + foreach ($rewards as $reward) { + if (in_array($reward->reward_type, PartnerConfig::instantTypes()) && (float) $reward->amount > 0 && $reward->user_id > 0) { + Wallet::change($reward->user_id, 'commission', -$reward->amount, 'reward_back', [ + 'partner_reward_id' => $reward->id, + 'order_id' => $orderId, + ], '合伙股东收益退回'); + AgentModel::where('user_id', $reward->user_id)->setDec('total_income', $reward->amount); + } + if ($reward->reward_type === 'push3' && $reward->user_id > 0) { + (new Push3Reward())->rollbackSeq($reward->user_id); + } + $reward->status = -2; + $reward->save(); + } + (new VoucherService())->closeByOrder($orderId); - return; - } + return true; + }); + } - foreach ($rewards as $reward) { - if (in_array($reward->reward_type, PartnerConfig::instantTypes()) && (float) $reward->amount > 0 && $reward->user_id > 0) { - Wallet::change($reward->user_id, 'commission', -$reward->amount, 'reward_back', [ - 'partner_reward_id' => $reward->id, - 'order_id' => $orderId, - ], '合伙股东收益退回'); - AgentModel::where('user_id', $reward->user_id)->setDec('total_income', $reward->amount); - } - if ($reward->reward_type === 'push3' && $reward->user_id > 0) { - (new Push3Reward())->rollbackSeq($reward->user_id); - } - $reward->status = -2; - $reward->save(); - } + /** + * 部分退款处理:仅等比缩减兑换券额度,不全额撤销分销商奖励 + */ + public function refundPartial($order, $item) + { + return Db::transaction(function () use ($order, $item) { + $orderId = intval(is_array($order) ? ($order['id'] ?? 0) : ($order->id ?? 0)); + $itemPayFee = floatval(is_array($item) ? ($item['pay_fee'] ?? 0) : ($item->pay_fee ?? 0)); + $voucherService = new VoucherService(); - (new VoucherService())->closeByOrder($orderId); + if ($itemPayFee > 0) { + $voucherService->reduceByRefund($orderId, $itemPayFee); + } + + PartnerRewardService::write([ + 'order_id' => $orderId, + 'buyer_id' => intval($order['user_id'] ?? 0), + 'user_id' => intval($order['user_id'] ?? 0), + 'role' => 'taster', + 'reward_type' => 'voucher_grant', + 'amount' => 0, + 'base_amount' => $itemPayFee, + 'ext' => [ + 'formula' => "子商品退款 {$itemPayFee},缩减兑换券总额", + 'item_id' => $item['id'] ?? 0, + 'partial_refund' => 1 + ], + 'status' => -2, + ]); + + return true; + }); } public function isQualified($order) diff --git a/addons/shopro/service/partner/RateService.php b/addons/shopro/service/partner/RateService.php index c25343e..aa01b9c 100644 --- a/addons/shopro/service/partner/RateService.php +++ b/addons/shopro/service/partner/RateService.php @@ -14,10 +14,16 @@ class RateService public function currentRate() { $latest = RateLog::order('id', 'desc')->find(); - if ($latest && (float) $latest->rate > 0) { + if ($latest) { return (float) $latest->rate; } - return $this->calcRate($this->countN()); + $cached = cache('partner_madow_current_rate'); + if ($cached !== null && $cached !== false) { + return (float) $cached; + } + $rate = $this->calcRate($this->countN()); + cache('partner_madow_current_rate', $rate, 300); + return $rate; } public function calcRate($n = null) @@ -48,9 +54,12 @@ class RateService ], $extra); if ($row) { $row->save($data); + cache('partner_madow_current_rate', (float) $rate, 300); return $row; } - return RateLog::create($data); + $created = RateLog::create($data); + cache('partner_madow_current_rate', (float) $rate, 300); + return $created; } public function qualifiedOrderQuery($start = null, $end = null) diff --git a/addons/shopro/service/partner/VoucherService.php b/addons/shopro/service/partner/VoucherService.php index 60af37c..4904a6c 100644 --- a/addons/shopro/service/partner/VoucherService.php +++ b/addons/shopro/service/partner/VoucherService.php @@ -35,24 +35,53 @@ class VoucherService 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) + public function refundConsume($order, $refundFee = null) { $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'])) { + $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, + ]); } } @@ -212,17 +241,57 @@ class VoucherService $plan->status = 0; $plan->save(); - $agent = AgentModel::where('user_id', $plan->user_id)->find(); + $agent = AgentModel::where('user_id', $plan->user_id)->lock(true)->find(); if ($agent) { - $unusedReleased = max(0, (float) $plan->released_amount); - $newReleased = max(0, (float) $agent->voucher_released - $unusedReleased); - $newTotal = max(0, (float) $agent->voucher_total - (float) $plan->total_amount); - if ($newReleased < (float) $agent->voucher_used) { - $newReleased = (float) $agent->voucher_used; + $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(); } } diff --git a/application/admin/controller/shopro/Config.php b/application/admin/controller/shopro/Config.php index e7404c6..892042f 100644 --- a/application/admin/controller/shopro/Config.php +++ b/application/admin/controller/shopro/Config.php @@ -47,12 +47,12 @@ class Config extends Common 'status' => $this->auth->check('shopro/config/rechargewithdraw') ], [ - 'label' => '分销配置', + 'label' => '分销基础', 'name' => 'shopro/config/commission', 'status' => $this->auth->check('shopro/config/commission') ], [ - 'label' => '合伙股东', + 'label' => '合伙股东规则', 'name' => 'shopro/config/partner', 'status' => $this->auth->check('shopro/config/commission') ], diff --git a/application/admin/controller/shopro/Dashboard.php b/application/admin/controller/shopro/Dashboard.php index 920b5df..541f48a 100644 --- a/application/admin/controller/shopro/Dashboard.php +++ b/application/admin/controller/shopro/Dashboard.php @@ -7,7 +7,15 @@ use app\admin\model\shopro\order\OrderItem; use app\admin\model\shopro\goods\Goods; use app\admin\model\shopro\user\User; use app\admin\model\shopro\Share; +use app\admin\model\shopro\commission\Agent; +use app\admin\model\shopro\partner\Reward; +use app\admin\model\shopro\partner\DailyReport; +use app\admin\model\shopro\partner\RateLog; +use app\admin\model\shopro\partner\VoucherPlan; use addons\shopro\service\SearchHistory; +use addons\shopro\service\partner\RateService; +use addons\shopro\service\partner\IdentityService; +use addons\shopro\service\partner\PartnerConfig; class Dashboard extends Common { @@ -38,7 +46,7 @@ class Dashboard extends Common public function total() { - // 用户数据 + // 1. 基础用户数据 (保持向下兼容) $userData = [ 'total' => User::count(), 'today' => User::whereTime('createtime', 'today')->count(), @@ -49,19 +57,18 @@ class Dashboard extends Common }) ]; - // -- commission code start -- + // 2. 分销商/合伙人基础数据 (保持向下兼容) $agentData = [ - 'total' => \app\admin\model\shopro\commission\Agent::count(), - 'today' => \app\admin\model\shopro\commission\Agent::whereTime('createtime', 'today')->count(), - 'week' => \app\admin\model\shopro\commission\Agent::whereTime('createtime', 'week')->count(), - 'array' => collection(\app\admin\model\shopro\commission\Agent::field('user_id,createtime')->whereTime('createtime', 'today')->select())->each(function ($agent) { + 'total' => Agent::count(), + 'today' => Agent::whereTime('createtime', 'today')->count(), + 'week' => Agent::whereTime('createtime', 'week')->count(), + 'array' => collection(Agent::field('user_id,createtime')->whereTime('createtime', 'today')->select())->each(function ($agent) { $agent->counter = 1; $agent->createtime_unix = $agent->getData('createtime') * 1000; }) ]; - // -- commission code end -- - // 分享数据 + // 3. 分享数据 (保持向下兼容) $shareData = [ 'total' => Share::count(), 'today' => Share::whereTime('createtime', 'today')->count(), @@ -72,10 +79,102 @@ class Dashboard extends Common }) ]; + // 4. 全局财富大盘与资金流 (专业财富指标) + $paidStatuses = [Order::STATUS_PAID, Order::STATUS_COMPLETED]; + $totalGmv = (float) Order::whereIn('status', $paidStatuses)->sum('pay_fee'); + $todayGmv = (float) Order::whereIn('status', $paidStatuses)->whereTime('createtime', 'today')->sum('pay_fee'); + $paidOrderCount = (int) Order::whereIn('status', $paidStatuses)->count(); + $todayPaidOrderCount = (int) Order::whereIn('status', $paidStatuses)->whereTime('createtime', 'today')->count(); + $avgOrderAmount = $paidOrderCount > 0 ? round($totalGmv / $paidOrderCount, 2) : 0.00; + + // 现金分成总支出 (status = 1 真实入账) + $totalPayout = (float) Reward::where('status', 1)->sum('amount'); + $todayPayout = (float) Reward::where('status', 1)->whereTime('createtime', 'today')->sum('amount'); + $netRetention = round($totalGmv - $totalPayout, 2); + $payoutRate = $totalGmv > 0 ? round(($totalPayout / $totalGmv) * 100, 2) : 0.00; + + $wealthData = [ + 'total_gmv' => number_format($totalGmv, 2, '.', ''), + 'today_gmv' => number_format($todayGmv, 2, '.', ''), + 'total_payout' => number_format($totalPayout, 2, '.', ''), + 'today_payout' => number_format($todayPayout, 2, '.', ''), + 'net_retention' => number_format($netRetention, 2, '.', ''), + 'payout_rate' => number_format($payoutRate, 2, '.', ''), + 'paid_order_count' => $paidOrderCount, + 'today_paid_order_count' => $todayPaidOrderCount, + 'avg_order_amount' => number_format($avgOrderAmount, 2, '.', ''), + ]; + + // 5. 马豆生态资产蓄水池 + $totalMadow = (float) Agent::sum('madow'); + $rateService = new RateService(); + $currentRate = $rateService->currentRate(); + $totalMadowCny = round($totalMadow * $currentRate, 2); + $historyOrderN = $rateService->countN(); + + $latestReport = DailyReport::order('settle_date', 'desc')->find(); + $yesterdayMadowPool = $latestReport ? (float) $latestReport->madow_pool : 0.00; + + $madowData = [ + 'total_madow' => IdentityService::formatMadow($totalMadow), + 'current_rate' => number_format($currentRate, 10, '.', ''), + 'rate_per_wan' => number_format($currentRate * 10000, 4, '.', ''), // 每万豆价值折算 + 'total_madow_cny' => number_format($totalMadowCny, 2, '.', ''), + 'history_order_n' => $historyOrderN, + 'yesterday_madow_pool' => IdentityService::formatMadow($yesterdayMadowPool), + ]; + + // 6. 消费兑换券杠杆池 + $totalVoucher = (float) VoucherPlan::sum('total_amount'); + $releasedVoucher = (float) VoucherPlan::sum('released_amount'); + $usedVoucher = (float) Agent::sum('voucher_used'); + $pendingVoucher = max(0, round($totalVoucher - $releasedVoucher, 2)); + $availableVoucher = max(0, round($releasedVoucher - $usedVoucher, 2)); + + $voucherData = [ + 'total_voucher' => number_format($totalVoucher, 2, '.', ''), + 'released_voucher' => number_format($releasedVoucher, 2, '.', ''), + 'used_voucher' => number_format($usedVoucher, 2, '.', ''), + 'pending_voucher' => number_format($pendingVoucher, 2, '.', ''), + 'available_voucher' => number_format($availableVoucher, 2, '.', ''), + ]; + + // 7. 合伙股东网络与资本活力 + $totalAgents = (int) Agent::count(); + $tasterCount = (int) Agent::where('level', 1)->count(); + $makerCount = (int) Agent::where('level', '>=', 2)->count(); + $makerLevels = []; + for ($lv = 2; $lv <= 7; $lv++) { + $makerLevels['V' . ($lv - 1)] = (int) Agent::where('level', $lv)->count(); + } + $venueCount = (int) Agent::where('is_venue', 1)->count(); + $countyCount = (int) Agent::where('is_county', 1)->count(); + + $activeWindow = time() - (int) PartnerConfig::get('partner_active_days', 30) * 86400; + $activeCount = (int) Agent::where('last_self_order_time', '>=', $activeWindow)->count(); + $inactiveCount = max(0, $totalAgents - $activeCount); + $activeRate = $totalAgents > 0 ? round(($activeCount / $totalAgents) * 100, 1) : 0.0; + + $partnerNetwork = [ + 'total_agents' => $totalAgents, + 'taster_count' => $tasterCount, + 'maker_count' => $makerCount, + 'maker_levels' => $makerLevels, + 'venue_count' => $venueCount, + 'county_count' => $countyCount, + 'active_count' => $activeCount, + 'inactive_count' => $inactiveCount, + 'active_rate' => $activeRate, + ]; + $this->success('获取成功', null, [ 'user_data' => $userData, 'agent_data' => $agentData ?? null, - 'share_data' => $shareData + 'share_data' => $shareData, + 'wealth_data' => $wealthData, + 'madow_data' => $madowData, + 'voucher_data' => $voucherData, + 'partner_network' => $partnerNetwork, ]); } @@ -85,6 +184,9 @@ class Dashboard extends Common { $date = $this->request->param('date', ''); $date = array_values(array_filter(explode(' - ', $date))); + if (empty($date) || count($date) < 2) { + $date = [date('Y-m-d 00:00:00'), date('Y-m-d 23:59:59')]; + } $orders = Order::with(['items'])->whereTime('createtime', 'between', $date) ->order('id', 'asc')->select(); @@ -96,22 +198,22 @@ class Dashboard extends Common // 支付订单(包含退款的订单, 不包含货到付款还未收货(未支付)订单) $data['payOrderNum'] = 0; $data['payOrderArr'] = []; - //支付金额(包含退款的订单, 不包含货到付款还未收货(未支付)订单) + // 支付金额(包含退款的订单, 不包含货到付款还未收货(未支付)订单) $data['payAmountNum'] = 0; $data['payAmountArr'] = []; - // 支付用户(一个人不管下多少单,都算一个,包含退款的订单, 不包含货到付款还未收货(未支付)订单) + // 支付用户 $userIds = []; $data['payUserNum'] = 0; $data['payUserArr'] = []; - // 代发货(包含货到付款) + // 待发货(包含货到付款) $data['noSendNum'] = 0; $data['noSendArr'] = []; - //售后维权 + // 售后维权 $data['aftersaleNum'] = 0; $data['aftersaleArr'] = []; - //退款订单 + // 退款订单 $data['refundNum'] = 0; $data['refundArr'] = []; @@ -122,9 +224,8 @@ class Dashboard extends Common 'user_id' => $order->user_id ]; - // 已支付的,不包含,货到付款未支付的 + // 已支付的,不包含货到付款未支付的 if (in_array($order->status, [Order::STATUS_PAID, Order::STATUS_COMPLETED])) { - // 支付订单数 $data['payOrderNum']++; $data['payOrderArr'][] = [ @@ -137,7 +238,7 @@ class Dashboard extends Common $data['payAmountNum'] = bcadd((string)$data['payAmountNum'], $order->pay_fee, 2); $data['payAmountArr'][] = [ - 'counter' => $order->pay_fee, + 'counter' => (float) $order->pay_fee, 'createtime' => $order->getData('createtime') * 1000, ]; @@ -152,7 +253,7 @@ class Dashboard extends Common } } - // 已支付的,和 货到付款未支付的 + // 已支付的和货到付款未支付的 if (in_array($order->status, [Order::STATUS_PAID, Order::STATUS_COMPLETED]) || $order->isOffline($order)) { $flagnoSend = false; $flagaftersale = false; @@ -181,34 +282,141 @@ class Dashboard extends Common if (!$flagaftersale && $item->aftersale_status != OrderItem::AFTERSALE_STATUS_NOAFTER) { $data['aftersaleNum']++; - // $data['aftersaleArr'][] = [ - // 'counter' => 1, - // 'createtime' => $order->getData('createtime') * 1000, - // ]; $flagaftersale = true; } if (!$flagrefund && $item->refund_status > OrderItem::REFUND_STATUS_NOREFUND) { $data['refundNum']++; - // $data['refundArr'][] = [ - // 'counter' => 1, - // 'createtime' => $order->getData('createtime') * 1000, - // ]; $flagrefund = true; } } if (!$aftersaleIng && $flagnoSend) { - // 存在正在售后中的订单,不算待发货(和订单列表保持一直) $data['noSendNum']++; - // $data['noSendArr'][] = [ - // 'counter' => 1, - // 'createtime' => $order->getData('createtime') * 1000, - // ]; } } } + // ================= 财富与收益分成时段穿透 (新增) ================= + $rewards = Reward::where('status', 1) + ->whereTime('createtime', 'between', $date) + ->order('id', 'asc') + ->select(); + + $payoutTotal = 0.00; + $payoutArr = []; + $periodMadow = 0.00000000; + + // 四大分成板块结构 + $structureMap = [ + 'push3' => [ + 'key' => 'push3', + 'name' => '直推裂变(推三返一)', + 'types' => ['push3'], + 'amount' => 0.00, + 'count' => 0, + 'color' => '#806AF6', + ], + 'maker' => [ + 'key' => 'maker', + 'name' => '创客团队(极差/平级/超越)', + 'types' => ['maker_diff', 'maker_peer', 'maker_overtaken'], + 'amount' => 0.00, + 'count' => 0, + 'color' => '#13c2c2', + ], + 'channel' => [ + 'key' => 'channel', + 'name' => '实体渠道(馆县拆分/平级)', + 'types' => ['team_venue', 'team_county', 'venue_peer', 'county_peer'], + 'amount' => 0.00, + 'count' => 0, + 'color' => '#fa8c16', + ], + 'share' => [ + 'key' => 'share', + 'name' => '全网共享(平台日结均分)', + 'types' => ['venue_share', 'county_share'], + 'amount' => 0.00, + 'count' => 0, + 'color' => '#eb2f96', + ], + ]; + + // 详细奖项映射 + $detailList = []; + foreach (PartnerConfig::rewardTypeMap() as $typeKey => $typeName) { + if (in_array($typeKey, ['madow_share', 'voucher_grant', 'voucher_release'])) { + continue; + } + $detailList[$typeKey] = [ + 'type' => $typeKey, + 'name' => $typeName, + 'amount' => 0.00, + 'count' => 0, + ]; + } + + foreach ($rewards as $reward) { + $amt = (float) $reward->amount; + $rwType = $reward->reward_type; + + if ($rwType === 'madow_share') { + $periodMadow = function_exists('bcadd') + ? (float) bcadd((string)$periodMadow, (string)($reward->madow ?? 0), 8) + : ($periodMadow + (float)($reward->madow ?? 0)); + continue; + } + + if ($amt > 0) { + $payoutTotal = round($payoutTotal + $amt, 2); + $payoutArr[] = [ + 'counter' => $amt, + 'createtime' => $reward->getData('createtime') * 1000, + 'type' => $rwType, + ]; + + // 归类到板块 + foreach ($structureMap as $catKey => &$cat) { + if (in_array($rwType, $cat['types'])) { + $cat['amount'] = round($cat['amount'] + $amt, 2); + $cat['count']++; + break; + } + } + unset($cat); + + if (isset($detailList[$rwType])) { + $detailList[$rwType]['amount'] = round($detailList[$rwType]['amount'] + $amt, 2); + $detailList[$rwType]['count']++; + } + } + } + + // 格式化分成板块占比 + $splitStructure = []; + foreach ($structureMap as $catKey => $cat) { + $catAmount = (float) $cat['amount']; + $ratio = $payoutTotal > 0 ? round(($catAmount / $payoutTotal) * 100, 1) : 0.0; + $splitStructure[] = [ + 'key' => $catKey, + 'name' => $cat['name'], + 'amount' => number_format($catAmount, 2, '.', ''), + 'count' => $cat['count'], + 'ratio' => $ratio, + 'color' => $cat['color'], + ]; + } + + $payAmountNum = (float) $data['payAmountNum']; + $data['payoutNum'] = number_format($payoutTotal, 2, '.', ''); + $data['payoutArr'] = $payoutArr; + $data['retentionNum'] = number_format(max(0, round($payAmountNum - $payoutTotal, 2)), 2, '.', ''); + $data['payoutRate'] = $payAmountNum > 0 ? round(($payoutTotal / $payAmountNum) * 100, 2) : 0.00; + $data['splitStructure'] = $splitStructure; + $data['rewardDetails'] = array_values($detailList); + $data['periodMadow'] = IdentityService::formatMadow($periodMadow); + $this->success('获取成功', null, $data); } @@ -216,6 +424,7 @@ class Dashboard extends Common public function ranking() { + // 1. 商品销量排行 (原有) $goods = Goods::limit(5)->order('sales', 'desc')->select(); foreach ($goods as $key => $gd) { $gd->append(['real_sales']); @@ -231,11 +440,65 @@ class Dashboard extends Common $gd['sale_total_money'] = $result['sale_total_money'] ?: 0; } + // 2. 热搜排行 (原有) $searchHistory = new SearchHistory(); + $hotSearch = $searchHistory->hotSearch(); + + // 3. 合伙股东收益排行 TOP 5 (新增) + $partnerRanking = []; + $rankingRows = Reward::field('user_id, sum(amount) as total_income, count(*) as reward_count') + ->where('status', 1) + ->where('user_id', '>', 0) + ->group('user_id') + ->order('total_income', 'desc') + ->limit(5) + ->select(); + + foreach ($rankingRows as $row) { + $user = User::field('id, nickname, avatar, mobile')->where('id', $row->user_id)->find(); + $agent = Agent::where('user_id', $row->user_id)->find(); + $identity = IdentityService::build($agent); + + $partnerRanking[] = [ + 'user_id' => $row->user_id, + 'nickname' => $user ? ($user->nickname ?: '用户#' . $row->user_id) : '用户#' . $row->user_id, + 'avatar' => $user ? $user->avatar : '', + 'mobile' => $user ? $user->mobile : '', + 'total_income' => number_format((float)$row->total_income, 2, '.', ''), + 'reward_count' => (int) $row->reward_count, + 'identity_text' => $identity['identity_text'] ?: '普通会员', + 'is_partner' => $identity['is_partner'], + 'direct_order_seq' => $agent ? (int) $agent->direct_order_seq : 0, + ]; + } + + // 4. 最新日结概览 (新增) + $dailyReport = DailyReport::order('settle_date', 'desc')->find(); + $recentReport = null; + if ($dailyReport) { + $recentReport = [ + 'settle_date' => $dailyReport->settle_date, + 'gmv' => number_format((float)$dailyReport->gmv, 2, '.', ''), + 'order_count' => (int)$dailyReport->order_count, + 'order_count_n' => (int)$dailyReport->order_count_n, + 'rate' => number_format((float)$dailyReport->rate, 10, '.', ''), + 'madow_pool' => IdentityService::formatMadow($dailyReport->madow_pool), + 'share_user_count' => (int)$dailyReport->share_user_count, + 'venue_pool' => number_format((float)$dailyReport->venue_pool, 2, '.', ''), + 'venue_user_count' => (int)$dailyReport->venue_user_count, + 'county_pool' => number_format((float)$dailyReport->county_pool, 2, '.', ''), + 'county_user_count' => (int)$dailyReport->county_user_count, + 'voucher_release_total' => number_format((float)$dailyReport->voucher_release_total, 2, '.', ''), + 'status' => (int)$dailyReport->status, + 'status_text' => $dailyReport->status_text, + ]; + } $this->success('获取成功', null, [ 'goods' => $goods, - 'hot_search' => $searchHistory->hotSearch() + 'hot_search' => $hotSearch, + 'partner_ranking' => $partnerRanking, + 'daily_report_recent' => $recentReport, ]); } } diff --git a/application/admin/controller/shopro/partner/Daily.php b/application/admin/controller/shopro/partner/Daily.php index 365ac62..e317c5d 100644 --- a/application/admin/controller/shopro/partner/Daily.php +++ b/application/admin/controller/shopro/partner/Daily.php @@ -51,11 +51,30 @@ class Daily extends Common ->whereBetween('createtime', [$yStart, $yEnd]) ->whereIn('reward_type', \addons\shopro\service\partner\PartnerConfig::instantTypes()) ->sum('amount'); + + $yesterdayReport = DailyReport::where('settle_date', $yesterday)->find(); + $yesterdayGmv = $yesterdayReport ? (float) $yesterdayReport->gmv : 0; + $yesterdayTotalCash = $sumCash($yesterday) + $instantYesterday; + $yesterdayPayoutRatio = $yesterdayGmv > 0 ? round(($yesterdayTotalCash / $yesterdayGmv) * 100, 2) : 0; + + $voucherTotal = (float) \app\admin\model\shopro\partner\VoucherPlan::where('status', 1)->sum('total_amount'); + $voucherReleased = (float) \app\admin\model\shopro\partner\VoucherPlan::where('status', 1)->sum('released_amount'); + $voucherDebt = max(0, $voucherTotal - $voucherReleased); + + $activeService = new \addons\shopro\service\partner\ActiveService(); + $venueActiveCount = count($activeService->activeUserIds($since, time(), ['is_venue' => 1])); + $countyActiveCount = count($activeService->activeUserIds($since, time(), ['is_county' => 1])); + $result['kpi'] = [ 'today' => DailyReport::where('settle_date', $today)->find(), - 'yesterday' => DailyReport::where('settle_date', $yesterday)->find(), + 'yesterday' => $yesterdayReport, 'today_cash' => number_format($todayCash + $instantToday, 2, '.', ''), - 'yesterday_cash' => number_format($sumCash($yesterday) + $instantYesterday, 2, '.', ''), + 'yesterday_cash' => number_format($yesterdayTotalCash, 2, '.', ''), + 'yesterday_payout_ratio' => $yesterdayPayoutRatio . '%', + 'yesterday_payout_ratio_val' => $yesterdayPayoutRatio, + 'voucher_debt' => number_format($voucherDebt, 2, '.', ''), + 'venue_active_count' => $venueActiveCount, + 'county_active_count' => $countyActiveCount, 'inactive_count' => \app\admin\model\shopro\commission\Agent::where(function ($q) use ($since) { $q->whereNull('last_self_order_time')->whereOr('last_self_order_time', '<', $since); })->count(), @@ -63,6 +82,13 @@ class Daily extends Common $this->success('获取成功', null, $result); } + public function preview() + { + $date = $this->request->param('settle_date', date('Y-m-d', strtotime('-1 day'))); + $data = (new DailySettleService())->preview($date); + $this->success('试算成功', null, $data); + } + public function rerun() { $date = $this->request->param('settle_date'); diff --git a/application/admin/controller/shopro/partner/Reward.php b/application/admin/controller/shopro/partner/Reward.php index c2e62d0..2361964 100644 --- a/application/admin/controller/shopro/partner/Reward.php +++ b/application/admin/controller/shopro/partner/Reward.php @@ -92,7 +92,20 @@ class Reward extends Common $totalAmount += array_sum(array_column($newDatas, 'amount')); $totalMadow += array_sum(array_column($newDatas, 'madow')); if ($pages['is_last_page']) { - $newDatas[] = ['id' => "总数:{$total};金额:{$totalAmount};马豆:{$totalMadow}"]; + $newDatas[] = [ + 'id' => '财务合计', + 'createtime' => '-', + 'user_nickname' => "共 {$total} 笔流水", + 'buyer_nickname' => '-', + 'order_id' => '-', + 'reward_type_text' => '-', + 'role_text' => '-', + 'amount' => number_format($totalAmount, 2, '.', ''), + 'madow' => number_format($totalMadow, 8, '.', ''), + 'base_amount' => '-', + 'status_text' => '已汇总', + 'formula' => "总现金: {$totalAmount}, 总马豆: {$totalMadow}", + ]; } return $newDatas; }); @@ -109,6 +122,9 @@ class Reward extends Common if ($amount == 0) { $this->error('请输入金额'); } + if ($amount > 10000) { + $this->error('单笔补发金额不能超过 10,000 元(财务风控限制)'); + } Db::transaction(function () use ($params, $amount) { $row = PartnerRewardService::write([ 'order_id' => intval($params['order_id'] ?? 0) ?: -time(), @@ -167,4 +183,79 @@ class Reward extends Common (new PartnerRewardService())->refund($orderId); $this->success('整单已退回'); } + + /** + * 订单整单利益分配透视(财务与客服一键对账) + */ + public function orderTree() + { + $orderId = intval($this->request->param('order_id')); + if ($orderId <= 0) { + $this->error('订单ID不合法'); + } + + $order = \app\admin\model\shopro\order\Order::with(['user'])->where('id', $orderId)->find(); + if (!$order) { + $this->error('订单不存在'); + } + + $rewards = $this->model->with(['user', 'buyer']) + ->where('order_id', $orderId) + ->order('id', 'asc') + ->select(); + + $rows = []; + $totalCash = 0; + $totalMadow = 0; + foreach ($rewards as $r) { + $rows[] = [ + 'id' => $r->id, + 'reward_type' => $r->reward_type, + 'reward_type_text' => $r->reward_type_text, + 'user_id' => $r->user_id, + 'user_nickname' => $r->user ? ($r->user->nickname ?: $r->user->mobile) : '平台/买家', + 'role_text' => $r->role_text ?: $r->role, + 'amount' => $r->amount, + 'madow' => $r->madow, + 'base_amount' => $r->base_amount, + 'status' => $r->status, + 'status_text' => $r->status_text, + 'formula' => $r->formula, + 'createtime' => $r->createtime, + ]; + if ($r->status == 1) { + if (in_array($r->reward_type, PartnerConfig::instantTypes())) { + $totalCash = bcadd((string)$totalCash, (string)$r->amount, 2); + } + $totalMadow = bcadd((string)$totalMadow, (string)$r->madow, 8); + } + } + + $voucherPlan = \app\admin\model\shopro\partner\VoucherPlan::where('order_id', $orderId)->find(); + $payFee = floatval($order->pay_fee); + $payoutRatio = $payFee > 0 ? round(($totalCash / $payFee) * 100, 2) : 0; + + $this->success('获取成功', null, [ + 'order' => [ + 'id' => $order->id, + 'order_sn' => $order->order_sn, + 'pay_fee' => $order->pay_fee, + 'status_text' => $order->status_text, + 'paid_time' => $order->paid_time ? date('Y-m-d H:i:s', $order->paid_time) : '-', + 'buyer_nickname' => $order->user ? ($order->user->nickname ?: $order->user->mobile) : '', + ], + 'rewards' => $rows, + 'summary' => [ + 'total_cash' => number_format($totalCash, 2, '.', ''), + 'total_madow' => number_format($totalMadow, 8, '.', ''), + 'payout_ratio' => $payoutRatio . '%', + ], + 'voucher_plan' => $voucherPlan ? [ + 'total_amount' => $voucherPlan->total_amount, + 'released_amount' => $voucherPlan->released_amount, + 'released_days' => $voucherPlan->released_days, + 'status_text' => $voucherPlan->status_text, + ] : null, + ]); + } } diff --git a/application/admin/view/shopro/commission/goods/index.html b/application/admin/view/shopro/commission/goods/index.html index b73a984..7fdbc52 100644 --- a/application/admin/view/shopro/commission/goods/index.html +++ b/application/admin/view/shopro/commission/goods/index.html @@ -35,7 +35,7 @@
-
分销 / 合伙股东计奖商品
+
计奖商品
diff --git a/application/admin/view/shopro/commission/level/index.html b/application/admin/view/shopro/commission/level/index.html index 53b8d69..d7890bd 100644 --- a/application/admin/view/shopro/commission/level/index.html +++ b/application/admin/view/shopro/commission/level/index.html @@ -5,7 +5,7 @@
-
分销商等级
+
身份等级
等级 1 固定为品酒师;2–7 对应创客 V1–V6。百分比佣金在合伙股东模式下不使用。
diff --git a/application/admin/view/shopro/commission/log/index.html b/application/admin/view/shopro/commission/log/index.html index 47e530c..227c581 100644 --- a/application/admin/view/shopro/commission/log/index.html +++ b/application/admin/view/shopro/commission/log/index.html @@ -5,7 +5,7 @@
-
分销动态
+
动态日志
实时动态
-
分销订单
+
业绩订单
diff --git a/application/admin/view/shopro/commission/reward/index.html b/application/admin/view/shopro/commission/reward/index.html index 14fc29a..e5199f4 100644 --- a/application/admin/view/shopro/commission/reward/index.html +++ b/application/admin/view/shopro/commission/reward/index.html @@ -5,7 +5,7 @@
-
佣金明细
+
历史佣金
diff --git a/application/admin/view/shopro/config/index.html b/application/admin/view/shopro/config/index.html index 2cded2b..456ba2f 100644 --- a/application/admin/view/shopro/config/index.html +++ b/application/admin/view/shopro/config/index.html @@ -653,7 +653,7 @@ 二级 三级 -
默认佣金比例请到 分销商等级 进行设置
+
默认级差基数请到「身份等级」进行设置
diff --git a/application/admin/view/shopro/dashboard/index.html b/application/admin/view/shopro/dashboard/index.html index 5d4e73f..7f2b59a 100644 --- a/application/admin/view/shopro/dashboard/index.html +++ b/application/admin/view/shopro/dashboard/index.html @@ -3,10 +3,6 @@
@@ -154,101 +245,168 @@ {if $auth->check('shopro/dashboard/total')} - - + + + + +
+
+ 商城实收流水 (GMV) + 实付总额 +
+
+ ¥{{ state.wealth.total_gmv }} +
+
+ 今日实收:¥{{ state.wealth.today_gmv }} + 客单均价:¥{{ state.wealth.avg_order_amount }} +
+ +
+
+ + + +
+
+ 现金分成支出与拨比 + + + 拨比 {{ state.wealth.payout_rate }}% + + +
+
+ ¥{{ state.wealth.total_payout }} +
+
+ 平台净留存:¥{{ state.wealth.net_retention }} + 明细 → +
+ +
+
+ + + +
+
+ 马豆生态资产蓄水池 + 单向升值 +
+
+ {{ state.madow.total_madow }} 豆 +
+
+ 单价:{{ state.madow.current_rate }} + 万豆折合:¥{{ state.madow.rate_per_wan }} +
+ +
+
+ + + +
+
+ 消费兑换券杠杆池 + 1.5倍补贴 +
+
+ ¥{{ state.voucher.total_voucher }} +
+
+ 已释放:¥{{ state.voucher.released_voucher }} + 待释放存量:¥{{ state.voucher.pending_voucher }} +
+ +
+
+
+ + + + -
-
-
{{state.total.user.title}} {{state.total.user.data.total}}
-
{{state.total.user.tip}} {{state.total.user.data.today}}
-
-
-