feat(dashboard): 从专业财富观角度重构升级后台收益与分成统计仪表盘

这个提交包含在:
chen
2026-09-17 08:22:23 +08:00
父节点 64a9563779
当前提交 b6287bf2e0
共有 40 个文件被更改,包括 5145 次插入 和 1015 次删除

查看文件

@@ -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]);
}
}
/**

查看文件

@@ -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 =>

查看文件

@@ -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,
];
}
// 佣金转余额

查看文件

@@ -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);
}
}
}
}

查看文件

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

查看文件

@@ -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)));
}
}

查看文件

@@ -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,
],
];
}
}

查看文件

@@ -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);
}
}
}

查看文件

@@ -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' => '兑换券退款退还',
];
}

查看文件

@@ -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)

查看文件

@@ -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)

查看文件

@@ -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();
}
}

查看文件

@@ -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')
],

查看文件

@@ -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,
]);
}
}

查看文件

@@ -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');

查看文件

@@ -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,
]);
}
}

查看文件

@@ -35,7 +35,7 @@
<el-header class="sa-header">
<div class="sa-title sa-flex sa-row-between">
<div class="sa-title-left">
<div class="left-name">分销 / 合伙股东计奖商品</div>
<div class="left-name">计奖商品</div>
<sa-filter-condition v-model="state.filter" @filter-delete="onChangeFilter">
</sa-filter-condition>
</div>

查看文件

@@ -5,7 +5,7 @@
<el-header class="sa-header">
<div class="sa-title sa-flex sa-row-between">
<div class="sa-title-left">
<div class="left-name">分销商等级</div>
<div class="left-name">身份等级</div>
<div style="font-size:12px;color:#999;margin-top:4px">
等级 1 固定为品酒师;2–7 对应创客 V1–V6。百分比佣金在合伙股东模式下不使用。
</div>

查看文件

@@ -5,7 +5,7 @@
<el-header class="sa-header">
<div class="sa-title sa-flex sa-row-between">
<div class="sa-title-left">
<div class="left-name">分销动态</div>
<div class="left-name">动态日志</div>
<div class="really-status sa-flex ml-4">
实时动态
<el-switch class="ml-2" v-model="really.reallyStatus" :active-value="1" :inactive-value="0"

查看文件

@@ -131,7 +131,7 @@
<el-header class="sa-header">
<div class="sa-title sa-flex sa-row-between">
<div class="sa-title-left">
<div class="left-name">分销订单</div>
<div class="left-name">业绩订单</div>
<sa-filter-condition v-model="state.filter" @filter-delete="onChangeFilter">
</sa-filter-condition>
</div>

查看文件

@@ -5,7 +5,7 @@
<el-header class="sa-header">
<div class="sa-title sa-flex sa-row-between">
<div class="sa-title-left">
<div class="left-name">佣金明细</div>
<div class="left-name">历史佣金</div>
<sa-filter-condition v-model="state.filter" @filter-delete="onChangeFilter">
</sa-filter-condition>
</div>

查看文件

@@ -653,7 +653,7 @@
<el-radio :label="2">二级</el-radio>
<el-radio :label="3">三级</el-radio>
</el-radio-group>
<div class="tip">默认佣金比例请到 分销商等级 进行设置</div>
<div class="tip">默认级差基数请到「身份等级」进行设置</div>
</div>
</el-form-item>
<el-form-item label="分销自购">

查看文件

@@ -3,10 +3,6 @@
<script src="__CDN__/assets/addons/shopro/libs/echarts.min.js?v={$site.version|htmlentities}"></script>
<style>
.dashboard-index .panel-block {
/* background: #f1f4f6; */
}
.dashboard-index .dashboard-index-main {
--el-main-padding: 0;
}
@@ -19,67 +15,117 @@
display: none;
}
.dashboard-index #userTotal,
.dashboard-index #agentTotal,
.dashboard-index #shareTotal {
width: 100%;
height: 66px;
}
.dashboard-index .card {
line-height: 1;
background: var(--sa-background-assist);
border-radius: 8px;
border: 1px solid var(--sa-space);
box-shadow: 0 2px 6px rgb(140 140 140 / 12%);
padding: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 14px 16px;
font-size: 12px;
color: var(--sa-font);
margin-bottom: 20px;
}
.dashboard-index .card:hover {
margin-bottom: 16px;
transition: all .2s;
}
@media screen and (min-width: 1200px) {
.dashboard-index .scale-card:hover {
transform: scale(1.05);
}
.dashboard-index .card:hover {
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
}
.dashboard-index .card .card-title .left {
font-size: 14px;
/* 顶部财富卡片 */
.dashboard-index .wealth-kpi-card {
padding: 16px 18px;
}
.dashboard-index .wealth-kpi-card:hover {
transform: translateY(-2px);
}
.dashboard-index .kpi-title {
font-size: 13px;
font-weight: 500;
color: var(--sa-subtitle);
}
.dashboard-index .card .card-title .left .num {
.dashboard-index .kpi-main-num {
font-size: 26px;
font-weight: 700;
margin: 12px 0 8px 0;
line-height: 1.1;
}
.dashboard-index .kpi-main-num .currency {
font-size: 16px;
margin-right: 2px;
font-weight: 500;
}
.dashboard-index .card .card-footer {
margin-top: 12px;
.dashboard-index .kpi-main-num .unit {
font-size: 12px;
margin-left: 2px;
font-weight: 400;
color: var(--sa-subfont);
}
.dashboard-index .card .card-footer .left {
margin-right: 8px;
.dashboard-index .kpi-sub-row {
font-size: 12px;
color: var(--sa-subfont);
margin-bottom: 10px;
}
.dashboard-index .card .card-footer .dot {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
.dashboard-index .kpi-footer {
padding-top: 10px;
border-top: 1px dashed var(--sa-space);
font-size: 12px;
color: var(--sa-subfont);
}
.dashboard-index .text-primary { color: #806af6 !important; }
.dashboard-index .text-warning { color: #fa8c16 !important; }
.dashboard-index .text-success { color: #52c41a !important; }
.dashboard-index .text-cyan { color: #13c2c2 !important; }
.dashboard-index .text-purple { color: #722ed1 !important; }
.dashboard-index .text-danger { color: #f5222d !important; }
.dashboard-index .text-dark { color: var(--sa-subtitle) !important; font-weight: 600; }
.dashboard-index .font-bold { font-weight: 600; }
.dashboard-index .cursor-pointer { cursor: pointer; }
/* 合伙人网络卡片 */
.dashboard-index .network-card {
padding: 14px 16px;
min-height: 98px;
}
.dashboard-index .network-badge {
width: 26px;
height: 26px;
line-height: 26px;
text-align: center;
border-radius: 6px;
color: #fff;
font-size: 12px;
font-weight: bold;
}
.dashboard-index .bg-primary { background: #806af6; }
.dashboard-index .bg-cyan { background: #13c2c2; }
.dashboard-index .bg-orange { background: #fa8c16; }
.dashboard-index #userTotal,
.dashboard-index #agentTotal,
.dashboard-index #shareTotal {
width: 100%;
height: 60px;
}
/* 走势图面板 */
.dashboard-index .bar-card {
min-height: 358px;
min-height: 388px;
}
.dashboard-index #chartContent {
width: 100%;
height: 296px;
height: 310px;
}
.dashboard-index .tab-item {
@@ -98,55 +144,100 @@
.dashboard-index .tab-item.is-active {
color: var(--sa-subtitle);
font-weight: bold;
border-bottom: 2px solid #806af6;
}
@media only screen and (max-width: 768px) {
.dashboard-index .date-time .el-date-editor {
--el-date-editor-datetimerange-width: 320px;
}
/* 分成透视 */
.dashboard-index .split-panel {
min-height: 388px;
}
.dashboard-index .chart-card {
height: 106px;
color: var(--sa-subfont);
.dashboard-index #splitPieContent {
width: 100%;
height: 180px;
}
.dashboard-index .chart-card .card-icon {
width: 24px;
height: 24px;
.dashboard-index .split-stat-item {
padding: 6px 0;
font-size: 12px;
border-bottom: 1px solid #f6f8fa;
}
.dashboard-index .chart-card .num {
font-size: 24px;
color: var(--sa-subtitle);
.dashboard-index .split-stat-item:last-of-type {
border-bottom: none;
}
.dashboard-index .chart-card .warning {
color: var(--el-color-warning);
.dashboard-index .split-color-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 6px;
}
/* 排行榜 */
.dashboard-index .partner-ranking-card,
.dashboard-index .goods-card,
.dashboard-index .hot-search-card {
height: 326px;
min-height: 340px;
}
.dashboard-index .goods-card .header,
.dashboard-index .hot-search-card .header {
.dashboard-index .card-header-title {
line-height: 16px;
font-size: 14px;
font-weight: 600;
color: var(--sa-subtitle);
margin-bottom: 16px;
margin-bottom: 14px;
}
.dashboard-index .top {
width: 18px;
.dashboard-index .rank-badge {
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
border-radius: 50%;
font-size: 11px;
font-weight: bold;
background: #f0f2f5;
color: #909399;
}
.dashboard-index .rank-badge.top-1 { background: #fadb14; color: #fff; }
.dashboard-index .rank-badge.top-2 { background: #bfbfbf; color: #fff; }
.dashboard-index .rank-badge.top-3 { background: #d48806; color: #fff; }
.dashboard-index .todo-stat-card {
padding: 12px 14px;
border-radius: 6px;
background: #fafbfc;
border: 1px solid #ebedf0;
cursor: pointer;
transition: all .2s;
}
.dashboard-index .todo-stat-card:hover {
background: #f0f5ff;
border-color: #adc6ff;
}
.dashboard-index .daily-report-box {
padding: 12px 14px;
background: #fbfbfe;
border: 1px solid #e8eafc;
border-radius: 6px;
font-size: 12px;
}
.dashboard-index #rankingContent {
width: 192px;
height: 192px;
}
.dashboard-index .top {
width: 18px;
height: 20px;
}
</style>
<div id="index" class="dashboard-index panel panel-default panel-intro" v-cloak>
@@ -154,101 +245,168 @@
<el-main class="dashboard-index-main">
<el-scrollbar height="100%">
{if $auth->check('shopro/dashboard/total')}
<el-row :gutter="20">
<!-- -- commission code start -- -->
<!-- ================= 1. 全局财富大盘与资金安全核心 KPI ================= -->
<el-row :gutter="16">
<!-- 商城实收 GMV -->
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
<div class="wealth-kpi-card card">
<div class="sa-flex sa-row-between">
<span class="kpi-title">商城实收流水 (GMV)</span>
<el-tag size="small" type="primary" effect="plain">实付总额</el-tag>
</div>
<div class="kpi-main-num text-primary">
<span class="currency">¥</span>{{ state.wealth.total_gmv }}
</div>
<div class="kpi-sub-row sa-flex sa-row-between">
<span>今日实收:<strong class="text-dark">¥{{ state.wealth.today_gmv }}</strong></span>
<span>客单均价:<strong class="text-dark">¥{{ state.wealth.avg_order_amount }}</strong></span>
</div>
<div class="kpi-footer sa-flex sa-row-between">
<span>累计支付单量</span>
<span class="font-bold">{{ state.wealth.paid_order_count }} 单</span>
</div>
</div>
</el-col>
<!-- 现金分成支出与综合拨比 -->
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
<div class="wealth-kpi-card card">
<div class="sa-flex sa-row-between">
<span class="kpi-title">现金分成支出与拨比</span>
<el-tooltip content="现金分成占实付GMV的百分比,评估平台精算模型健康度" placement="top">
<el-tag size="small" :type="parseFloat(state.wealth.payout_rate) <= 35 ? 'success' : 'warning'" effect="plain">
拨比 {{ state.wealth.payout_rate }}%
</el-tag>
</el-tooltip>
</div>
<div class="kpi-main-num text-warning">
<span class="currency">¥</span>{{ state.wealth.total_payout }}
</div>
<div class="kpi-sub-row sa-flex sa-row-between">
<span>平台净留存:<strong class="text-success">¥{{ state.wealth.net_retention }}</strong></span>
<span class="cursor-pointer text-primary" @click="onOpenReward('')">明细 →</span>
</div>
<div class="kpi-footer sa-flex sa-row-between">
<span>今日分成支出</span>
<span class="font-bold text-warning">¥{{ state.wealth.today_payout }}</span>
</div>
</div>
</el-col>
<!-- 马豆生态资产池 -->
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
<div class="wealth-kpi-card card">
<div class="sa-flex sa-row-between">
<span class="kpi-title">马豆生态资产蓄水池</span>
<el-tag size="small" type="success" effect="plain">单向升值</el-tag>
</div>
<div class="kpi-main-num text-purple">
{{ state.madow.total_madow }} <span class="unit">豆</span>
</div>
<div class="kpi-sub-row sa-flex sa-row-between">
<span>单价:<strong>{{ state.madow.current_rate }}</strong></span>
<span>万豆折合:<strong class="text-purple">¥{{ state.madow.rate_per_wan }}</strong></span>
</div>
<div class="kpi-footer sa-flex sa-row-between">
<span>累计订单 N:<strong>{{ state.madow.history_order_n }} 单</strong></span>
<span class="cursor-pointer text-primary" @click="onOpenDaily">日结池 →</span>
</div>
</div>
</el-col>
<!-- 消费兑换券杠杆池 -->
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
<div class="wealth-kpi-card card">
<div class="sa-flex sa-row-between">
<span class="kpi-title">消费兑换券杠杆池</span>
<el-tag size="small" type="info" effect="plain">1.5倍补贴</el-tag>
</div>
<div class="kpi-main-num text-danger">
<span class="currency">¥</span>{{ state.voucher.total_voucher }}
</div>
<div class="kpi-sub-row sa-flex sa-row-between">
<span>已释放:<strong>¥{{ state.voucher.released_voucher }}</strong></span>
<span>待释放存量:<strong class="text-danger">¥{{ state.voucher.pending_voucher }}</strong></span>
</div>
<div class="kpi-footer sa-flex sa-row-between">
<span>已抵扣消费:<strong>¥{{ state.voucher.used_voucher }}</strong></span>
<span class="cursor-pointer text-primary" @click="onOpenVoucher">计划 →</span>
</div>
</div>
</el-col>
</el-row>
<!-- ================= 2. 合伙股东网络与活力矩阵 ================= -->
<el-row :gutter="16">
<!-- 品酒师基础会员 -->
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
<div class="scale-card card ">
<div class="card-title sa-flex sa-row-between">
<div class="left">{{state.total.user.title}} <span
class="num">{{state.total.user.data.total}}</span> </div>
<div>{{state.total.user.tip}} <span>{{state.total.user.data.today}}</span> </div>
</div>
<div id="userTotal"></div>
<div class="card-footer sa-flex">
<span class="left sa-flex">
<span class="dot" :style="{ background: state.total.user.color }"></span>
<span>{{ state.total.user.footer }}</span>
<div class="network-card card">
<div class="sa-flex sa-row-between mb-2">
<div class="sa-flex">
<span class="network-badge bg-primary mr-2">品</span>
<span class="font-bold text-dark" style="font-size: 14px;">品酒师基础会员</span>
</div>
<span class="text-primary font-bold cursor-pointer" style="font-size: 18px;" @click="onOpenAgent(1)">
{{ state.partner.taster_count }} <span style="font-size: 12px; color: var(--sa-subfont);">人</span>
</span>
<span>{{ state.total.user.data.week }}</span>
</div>
<div class="sa-flex sa-row-between text-muted" style="font-size: 12px;">
<span>30日自购活跃率:<strong class="text-success">{{ state.partner.active_rate }}%</strong></span>
<span>正常:<strong class="text-success">{{ state.partner.active_count }}</strong> 人 / 暂停:<strong class="text-muted">{{ state.partner.inactive_count }}</strong> 人</span>
</div>
<el-progress :percentage="state.partner.active_rate" :stroke-width="5" :show-text="false" color="#52c41a" style="margin-top: 8px;"></el-progress>
</div>
</el-col>
<!-- 创客合伙人梯队 -->
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
<div class="scale-card card ">
<div class="card-title sa-flex sa-row-between">
<div class="left">{{state.total.agent.title}} <span
class="num">{{state.total.agent.data.total}}</span> </div>
<div>{{state.total.agent.tip}} <span>{{state.total.agent.data.today}}</span> </div>
</div>
<div id="agentTotal"></div>
<div class="card-footer sa-flex">
<span class="left sa-flex">
<span class="dot" :style="{ background: state.total.agent.color }"></span>
<span>{{ state.total.agent.footer }}</span>
<div class="network-card card">
<div class="sa-flex sa-row-between mb-2">
<div class="sa-flex">
<span class="network-badge bg-cyan mr-2">创</span>
<span class="font-bold text-dark" style="font-size: 14px;">创客合伙股东 (V1~V6)</span>
</div>
<span class="text-cyan font-bold cursor-pointer" style="font-size: 18px;" @click="onOpenAgent('')">
{{ state.partner.maker_count }} <span style="font-size: 12px; color: var(--sa-subfont);">人</span>
</span>
<span>{{ state.total.agent.data.week }}</span>
</div>
<div class="sa-flex sa-flex-wrap" style="margin-top: 6px;">
<el-tag size="small" class="mr-1 mb-1" effect="light" type="info" v-for="(cnt, lv) in state.partner.maker_levels" :key="lv">
{{ lv }}: {{ cnt }}人
</el-tag>
</div>
</div>
</el-col>
<!-- 实体与区域渠道 -->
<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
<div class="scale-card card ">
<div class="card-title sa-flex sa-row-between">
<div class="left">{{state.total.share.title}} <span
class="num">{{state.total.share.data.total}}</span> </div>
<div>{{state.total.share.tip}} <span>{{state.total.share.data.today}}</span> </div>
<div class="network-card card">
<div class="sa-flex sa-row-between mb-2">
<div class="sa-flex">
<span class="network-badge bg-orange mr-2">馆</span>
<span class="font-bold text-dark" style="font-size: 14px;">实体与区域渠道</span>
</div>
<div class="sa-flex">
<span class="text-orange font-bold mr-2" style="font-size: 15px;">酒道馆 {{ state.partner.venue_count }} 家</span>
<span class="text-purple font-bold" style="font-size: 15px;">区县 {{ state.partner.county_count }} 个</span>
</div>
</div>
<div id="shareTotal"></div>
<div class="card-footer sa-flex">
<span class="left sa-flex">
<span class="dot" :style="{ background: state.total.share.color }"></span>
<span>{{ state.total.share.footer }}</span>
</span>
<span>{{ state.total.share.data.week }}</span>
<div class="sa-flex sa-row-between text-muted" style="font-size: 12px; margin-top: 10px;">
<span>享团队拆分 (50~100元) + 平台日结均分池</span>
<span class="cursor-pointer text-primary" @click="onOpenAgent('')">渠道档案 →</span>
</div>
</div>
</el-col>
<!-- -- commission code end -- -->
<!-- <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<div class="scale-card card ">
<div class="card-title sa-flex sa-row-between">
<div class="left">{{state.total.user.title}} <span
class="num">{{state.total.user.data.total}}</span> </div>
<div>{{state.total.user.tip}} <span>{{state.total.user.data.today}}</span> </div>
</div>
<div id="userTotal"></div>
<div class="card-footer sa-flex">
<span class="left sa-flex">
<span class="dot" :style="{ background: state.total.user.color }"></span>
<span>{{ state.total.user.footer }}</span>
</span>
<span>{{ state.total.user.data.week }}</span>
</div>
</div>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<div class="scale-card card ">
<div class="card-title sa-flex sa-row-between">
<div class="left">{{state.total.share.title}} <span
class="num">{{state.total.share.data.total}}</span> </div>
<div>{{state.total.share.tip}} <span>{{state.total.share.data.today}}</span> </div>
</div>
<div id="shareTotal"></div>
<div class="card-footer sa-flex">
<span class="left sa-flex">
<span class="dot" :style="{ background: state.total.share.color }"></span>
<span>{{ state.total.share.footer }}</span>
</span>
<span>{{ state.total.share.data.week }}</span>
</div>
</div>
</el-col> -->
</el-row>
{/if}
{if $auth->check('shopro/dashboard/chart')}
<el-row :gutter="20">
<!-- ================= 3. 核心动态走势与分成结构透视 ================= -->
<el-row :gutter="16">
<!-- 左侧:时段动态分析图 -->
<el-col :xs="24" :sm="24" :md="24" :lg="16" :xl="16">
<div class="bar-card card">
<div class="sa-flex sa-row-between sa-flex-wrap">
<div class="sa-flex sa-row-between sa-flex-wrap mb-2">
<div class="sa-flex">
<div class="tab-item" :class="chart.tabActive == key ? 'is-active' : ''"
v-for="(value, key) in chart.tabsData" :key="key"
@@ -268,47 +426,171 @@
<div id="chartContent"></div>
</div>
</el-col>
<!-- 右侧:收益分成多维透视 -->
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
<el-row :gutter="20">
<el-col :xs="12" :sm="8" :md="12" :lg="12" :xl="12" v-for="(value, key) in statistics"
:key="key">
<div class="chart-card scale-card card">
<div class="sa-flex sa-row-between mb-2">
<img class="card-icon"
:src="`/assets/addons/shopro/img/dashboard/${key}.png`" />
<div class="sa-flex" @click="onOpen(value.status)">
<el-button type="info" link size="small">详情
<el-icon class="ml-1">
<arrow-right />
</el-icon>
</el-button>
</div>
<div class="split-panel card">
<div class="sa-flex sa-row-between mb-2">
<span class="card-header-title" style="margin-bottom: 0;">收益分成结构透视</span>
<span class="cursor-pointer text-primary" style="font-size: 12px;" @click="onOpenReward('')">全部流水 →</span>
</div>
<div class="sa-flex sa-row-center">
<div id="splitPieContent"></div>
</div>
<div class="split-stat-list mt-2">
<div class="split-stat-item sa-flex sa-row-between" v-for="item in chart.data.splitStructure" :key="item.key">
<div class="sa-flex">
<span class="split-color-dot" :style="{ background: item.color }"></span>
<span>{{ item.name }}</span>
</div>
<div class="num mb-1">{{ value.num }}</div>
<div class="sa-flex sa-row-between">
<span>{{ value.text }}</span>
<el-popover popper-class="sa-popper" placement="top" trigger="hover"
:content="value.tip">
<template #reference>
<el-icon class="warning">
<Warning />
</el-icon>
</template>
</el-popover>
<div class="sa-flex">
<span class="font-bold mr-2">¥{{ item.amount }}</span>
<el-tag size="small" type="info" effect="plain">{{ item.ratio }}%</el-tag>
</div>
</div>
</el-col>
</el-row>
</div>
<div class="sa-flex sa-row-between mt-3 pt-2" style="border-top: 1px dashed var(--sa-space); font-size: 12px; color: var(--sa-subfont);">
<span>区间马豆分红:<strong class="text-purple">{{ chart.data.periodMadow }} 豆</strong></span>
<el-button type="primary" link size="small" @click="onOpenDaily">日结池详情</el-button>
</div>
</div>
</el-col>
</el-row>
{/if}
{if $auth->check('shopro/dashboard/ranking')}
<el-row :gutter="20">
<!-- ================= 4. 合伙人收益排行 & 电商运营与风控监控 ================= -->
<el-row :gutter="16">
<!-- 左侧:合伙股东收益排行 TOP 5 -->
<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
<div class="partner-ranking-card card">
<div class="sa-flex sa-row-between mb-2">
<div class="card-header-title" style="margin-bottom: 0;">
<span>合伙股东收益排行榜 (TOP 5)</span>
</div>
<span class="cursor-pointer text-primary" style="font-size: 12px;" @click="onOpenReward('')">查看全部 →</span>
</div>
<el-table class="sa-table" :data="ranking.partner" stripe>
<el-table-column label="排名" width="55" align="center">
<template #default="scope">
<span class="rank-badge" :class="`top-${scope.$index + 1}`">
{{ scope.$index + 1 }}
</span>
</template>
</el-table-column>
<el-table-column label="股东会员" min-width="140">
<template #default="scope">
<div class="sa-flex">
<sa-image :url="scope.row.avatar" size="24" style="border-radius: 50%;"></sa-image>
<div class="ml-2">
<div class="sa-table-line-1 font-bold">{{ scope.row.nickname }}</div>
<div style="font-size: 11px; color: var(--sa-subfont);">{{ scope.row.mobile || '' }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="身份" min-width="150">
<template #default="scope">
<el-tag size="small" :type="scope.row.is_partner ? 'danger' : 'info'" effect="light">
{{ scope.row.identity_text }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="累计收益" width="110" align="right">
<template #default="scope">
<span class="text-warning font-bold">¥{{ scope.row.total_income }}</span>
</template>
</el-table-column>
</el-table>
</div>
</el-col>
<!-- 右侧:电商运营风控 & 日结监控 -->
<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
<div class="card" style="min-height: 340px;">
<div class="card-header-title">电商运营与日结对账监控</div>
<!-- 待处理订单指标卡 -->
<el-row :gutter="12" class="mb-3">
<el-col :span="8">
<div class="todo-stat-card" @click="onOpen('nosend')">
<div style="color: var(--sa-subfont); margin-bottom: 4px;">待发货订单</div>
<div class="sa-flex sa-row-between">
<span style="font-size: 20px; font-weight: 700;" :class="statistics.noSend.num > 0 ? 'text-warning' : 'text-dark'">
{{ statistics.noSend.num }}
</span>
<el-icon><arrow-right /></el-icon>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="todo-stat-card" @click="onOpen('aftersale')">
<div style="color: var(--sa-subfont); margin-bottom: 4px;">售后维权</div>
<div class="sa-flex sa-row-between">
<span style="font-size: 20px; font-weight: 700;" :class="statistics.aftersale.num > 0 ? 'text-danger' : 'text-dark'">
{{ statistics.aftersale.num }}
</span>
<el-icon><arrow-right /></el-icon>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="todo-stat-card" @click="onOpen('refund')">
<div style="color: var(--sa-subfont); margin-bottom: 4px;">退款订单</div>
<div class="sa-flex sa-row-between">
<span style="font-size: 20px; font-weight: 700;">
{{ statistics.refund.num }}
</span>
<el-icon><arrow-right /></el-icon>
</div>
</div>
</el-col>
</el-row>
<!-- 最新日结对账卡 -->
<div class="daily-report-box" v-if="ranking.recentReport">
<div class="sa-flex sa-row-between mb-2">
<div class="sa-flex">
<el-icon class="mr-1 text-primary"><Calendar /></el-icon>
<strong class="text-dark">最近一次日结概览 ({{ ranking.recentReport.settle_date }})</strong>
</div>
<el-tag size="small" :type="ranking.recentReport.status == 1 ? 'success' : 'danger'" effect="plain">
{{ ranking.recentReport.status_text }}
</el-tag>
</div>
<el-row :gutter="10" style="color: var(--sa-subfont); line-height: 1.8;">
<el-col :span="12">
<div>合格单量:<strong class="text-dark">{{ ranking.recentReport.order_count }} 单</strong></div>
<div>当日 GMV:<strong class="text-dark">¥{{ ranking.recentReport.gmv }}</strong></div>
<div>全网累计 N:<strong>{{ ranking.recentReport.order_count_n }}</strong></div>
</el-col>
<el-col :span="12">
<div>马豆均分:<strong class="text-purple">{{ ranking.recentReport.madow_pool }} 豆</strong> ({{ ranking.recentReport.share_user_count }}人)</div>
<div>酒道馆奖池:<strong class="text-warning">¥{{ ranking.recentReport.venue_pool }}</strong> ({{ ranking.recentReport.venue_user_count }}人)</div>
<div>区县奖池:<strong class="text-warning">¥{{ ranking.recentReport.county_pool }}</strong> ({{ ranking.recentReport.county_user_count }}人)</div>
</el-col>
</el-row>
<div class="sa-flex sa-row-right mt-2">
<el-button type="primary" link size="small" @click="onOpenDaily">查看日结对账全记录 →</el-button>
</div>
</div>
<div v-else class="text-muted sa-flex sa-row-center" style="padding: 24px 0;">
暂无日结对账数据
</div>
</div>
</el-col>
</el-row>
<!-- ================= 5. 商品热销榜与热搜榜 (保持原有生态) ================= -->
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
<div class="goods-card card">
<div class="header">
<span>销量榜</span>
</div>
<div class="card-header-title">热销商品排行榜</div>
<el-table class="sa-table" :data="ranking.goods" stripe>
<el-table-column label="排名" width="60" align="center">
<template #default="scope">
@@ -316,7 +598,7 @@
:src="`/assets/addons/shopro/img/dashboard/top${scope.$index + 1}.png`" />
</template>
</el-table-column>
<el-table-column label="名称" min-width="200">
<el-table-column label="商品名称" min-width="200">
<template #default="scope">
<div class="sa-flex">
<sa-image :url="scope.row.image" size="20"></sa-image>
@@ -326,42 +608,36 @@
</div>
</template>
</el-table-column>
<el-table-column label="销量" width="100">
<el-table-column label="销量" width="90" align="right">
<template #default="scope">
<div class="sa-table-line-1">
{{ scope.row.real_sales || '-' }}
</div>
<span>{{ scope.row.real_sales || '-' }}</span>
</template>
</el-table-column>
</el-table>
</div>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
<div class="hot-search-card card">
<div class="header">
<span>热搜榜</span>
</div>
<div class="card-header-title">关键词热搜榜</div>
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16">
<el-table class="sa-table" :data="ranking.hot_search" stripe>
<el-table-column label="排名" width="60" align="center">
<el-table-column label="排名" width="50" align="center">
<template #default="scope">
<img class="top"
:src="`/assets/addons/shopro/img/dashboard/top${scope.$index + 1}.png`" />
<span class="rank-badge" :class="`top-${scope.$index + 1}`">
{{ scope.$index + 1 }}
</span>
</template>
</el-table-column>
<el-table-column label="名称" min-width="200">
<el-table-column label="搜索词" min-width="160">
<template #default="scope">
<div class="sa-table-line-1">
{{ scope.row.keyword || '-' }}
</div>
<div class="sa-table-line-1">{{ scope.row.keyword || '-' }}</div>
</template>
</el-table-column>
<el-table-column label="搜索量(次)" width="100">
<el-table-column label="频次" width="80" align="right">
<template #default="scope">
<div class="sa-table-line-1">
{{ scope.row.num }}
</div>
<span>{{ scope.row.num }}</span>
</template>
</el-table-column>
</el-table>

查看文件

@@ -2,21 +2,25 @@
<div id="index" class="reward-index panel panel-default panel-intro" v-cloak>
<el-container class="panel-block">
<el-header class="sa-header">
<el-header class="sa-header" style="height:auto;padding-bottom:12px;">
<div class="sa-title sa-flex sa-row-between">
<div class="sa-title-left">
<div class="left-name">日结对账</div>
<div class="sa-flex" style="gap:16px;margin-top:8px;font-size:13px">
<span>昨日单数 {{ state.kpi.yesterday?.order_count || 0 }}</span>
<span>昨日GMV {{ state.kpi.yesterday?.gmv || '0.00' }}</span>
<span>昨日现金 {{ state.kpi.yesterday_cash || '0.00' }}</span>
<span>昨日马豆池 {{ state.kpi.yesterday?.madow_pool || '0' }}</span>
<span>今日单数 {{ state.kpi.today?.order_count || 0 }}</span>
<span>今日现金 {{ state.kpi.today_cash || '0.00' }}</span>
<span>暂停领取 {{ state.kpi.inactive_count || 0 }} 人</span>
<div class="left-name">日结对账与财务监控</div>
<div class="sa-flex" style="gap:14px;margin-top:8px;font-size:13px;flex-wrap:wrap;">
<span>昨日合格单 <b>{{ state.kpi.yesterday?.order_count || 0 }}</b> 单</span>
<span>昨日 GMV <b>¥ {{ state.kpi.yesterday?.gmv || '0.00' }}</b></span>
<span>昨日现金总拨发 <b style="color:#f56c6c;">¥ {{ state.kpi.yesterday_cash || '0.00' }}</b></span>
<span>昨日综合拨比 <b :style="{color: (state.kpi.yesterday_payout_ratio_val > 70 ? '#e6a23c' : '#67c23a')}">{{ state.kpi.yesterday_payout_ratio || '0%' }}</b></span>
<span>兑换券待履约负债 <b style="color:#e6a23c;">¥ {{ state.kpi.voucher_debt || '0.00' }}</b></span>
<span>活跃馆/县 <b>{{ state.kpi.venue_active_count || 0 }}</b> 馆 / <b>{{ state.kpi.county_active_count || 0 }}</b> 县</span>
<span>今日累计现金 <b>¥ {{ state.kpi.today_cash || '0.00' }}</b></span>
<span style="color:#909399;">暂停领取 <b>{{ state.kpi.inactive_count || 0 }}</b> 人</span>
</div>
</div>
<div class="sa-title-right">
<div class="sa-title-right sa-flex" style="gap:8px;">
{if $auth->check('shopro/partner/daily/preview')}
<el-button type="primary" icon="DataAnalysis" @click="onOpenPreview">日结预跑试算 (Dry Run)</el-button>
{/if}
<el-button class="sa-button-refresh" icon="RefreshRight" @click="getData"></el-button>
</div>
</div>
@@ -57,4 +61,96 @@
<sa-pagination v-model="pagination" @pagination-change="getData"></sa-pagination>
</el-footer>
</el-container>
<!-- 日结预跑试算弹窗 (Dry Run) -->
<el-dialog v-model="previewDialog.visible" title="日结分润试算 (Dry Run 预跑监控)" width="820px" destroy-on-close>
<div style="margin-bottom:16px;" class="sa-flex sa-row-between">
<div class="sa-flex" style="gap:10px;">
<span style="font-size:13px;color:#606266;">试算日期:</span>
<el-date-picker v-model="previewDialog.date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" size="small" style="width:160px;"></el-date-picker>
<el-button type="primary" size="small" :loading="previewDialog.loading" @click="runPreview">开始模拟试算</el-button>
</div>
<div v-if="previewDialog.result">
<el-tag v-if="previewDialog.result.is_settled" type="success">该日已执行正式日结</el-tag>
<el-tag v-else type="warning">尚未正式结算(以下为只读预估)</el-tag>
</div>
</div>
<div v-if="previewDialog.result" v-loading="previewDialog.loading">
<!-- 财务现金拨比盘 -->
<div style="background:#fdf6ec;border:1px solid #faecd8;border-radius:6px;padding:14px;margin-bottom:16px;">
<div style="font-weight:600;font-size:14px;color:#e6a23c;margin-bottom:10px;">综合现金拨比与安全监控</div>
<el-row :gutter="16">
<el-col :span="6">
<div style="font-size:12px;color:#909399;">当日GMV总额</div>
<div style="font-size:16px;font-weight:bold;margin-top:2px;">¥ {{ previewDialog.result.gmv }}</div>
</el-col>
<el-col :span="6">
<div style="font-size:12px;color:#909399;">当日即时奖 (推三/极差等)</div>
<div style="font-size:16px;font-weight:bold;color:#f56c6c;margin-top:2px;">¥ {{ previewDialog.result.financial?.instant_cash }}</div>
</el-col>
<el-col :span="6">
<div style="font-size:12px;color:#909399;">日结均分现金 (馆/县)</div>
<div style="font-size:16px;font-weight:bold;color:#f56c6c;margin-top:2px;">¥ {{ previewDialog.result.financial?.daily_cash }}</div>
</el-col>
<el-col :span="6">
<div style="font-size:12px;color:#909399;">综合现金拨比率</div>
<div style="font-size:18px;font-weight:bold;margin-top:2px;" :style="{color: previewDialog.result.financial?.is_alert ? '#f56c6c' : '#67c23a'}">
{{ previewDialog.result.financial?.payout_ratio }}
</div>
</el-col>
</el-row>
<div v-if="previewDialog.result.financial?.is_alert" style="margin-top:8px;font-size:12px;color:#f56c6c;">
⚠️ 注意:当日综合现金拨比超过 70% 财务风控警示线,建议关注极差/平级奖金释放与均分池规模。
</div>
<div style="margin-top:8px;font-size:12px;color:#67c23a;">
💡 沉淀资金效益:因分母为0留存平台的酒道馆/区县资金为 <b>¥ {{ previewDialog.result.financial?.saved_cash }}</b>。
</div>
</div>
<!-- 均分池测算卡片 -->
<el-row :gutter="16" style="margin-bottom:16px;">
<el-col :span="8">
<div style="background:#f4f4f5;padding:12px;border-radius:6px;">
<div style="font-weight:600;font-size:13px;margin-bottom:6px;">酒道馆均分池</div>
<div style="font-size:12px;color:#606266;line-height:1.8;">
奖池总额:¥ <b>{{ previewDialog.result.venue?.pool }}</b><br>
合格人数:<b>{{ previewDialog.result.venue?.user_count }}</b> 人<br>
人均分润:¥ <b>{{ previewDialog.result.venue?.each }}</b><br>
沉淀金额:¥ <b>{{ previewDialog.result.venue?.saved }}</b>
</div>
</div>
</el-col>
<el-col :span="8">
<div style="background:#f4f4f5;padding:12px;border-radius:6px;">
<div style="font-weight:600;font-size:13px;margin-bottom:6px;">区县代理均分池</div>
<div style="font-size:12px;color:#606266;line-height:1.8;">
奖池总额:¥ <b>{{ previewDialog.result.county?.pool }}</b><br>
合格人数:<b>{{ previewDialog.result.county?.user_count }}</b> 人<br>
人均分润:¥ <b>{{ previewDialog.result.county?.each }}</b><br>
沉淀金额:¥ <b>{{ previewDialog.result.county?.saved }}</b>
</div>
</div>
</el-col>
<el-col :span="8">
<div style="background:#f4f4f5;padding:12px;border-radius:6px;">
<div style="font-weight:600;font-size:13px;margin-bottom:6px;">马豆分红池 (1:1 GMV)</div>
<div style="font-size:12px;color:#606266;line-height:1.8;">
马豆总池:<b>{{ previewDialog.result.madow?.pool }}</b> 豆<br>
合格人数:<b>{{ previewDialog.result.madow?.user_count }}</b> 人<br>
人均分豆:<b>{{ previewDialog.result.madow?.each }}</b><br>
即时汇率:<b>{{ previewDialog.result.rate }}</b>
</div>
</div>
</el-col>
</el-row>
<div style="background:#fafafa;border:1px solid #ebeef5;padding:10px 14px;border-radius:4px;font-size:12px;color:#606266;">
<span>📅 兑换券释放预估:当日按计划释放兑换券总额约 <b>¥ {{ previewDialog.result.voucher?.release_estimate }}</b>(以365天逐日释放)。</span>
</div>
</div>
<template #footer>
<el-button @click="previewDialog.visible = false">关闭</el-button>
</template>
</el-dialog>
</div>

查看文件

@@ -36,9 +36,15 @@
<sa-user-profile :user="scope.row.buyer" :id="scope.row.buyer_id"></sa-user-profile>
</template>
</el-table-column>
<el-table-column label="订单" min-width="180">
<el-table-column label="订单" min-width="190">
<template #default="scope">
{{ scope.row.order?.order_sn || scope.row.order_id }}
<div v-if="scope.row.order_id > 0" class="sa-flex sa-row-between">
<el-link type="primary" :underline="false" @click="onOrderTree(scope.row)">
{{ scope.row.order?.order_sn || scope.row.order_id }}
</el-link>
<el-button type="primary" link size="small" @click="onOrderTree(scope.row)">透视</el-button>
</div>
<span v-else>{{ scope.row.order?.order_sn || scope.row.order_id }}</span>
</template>
</el-table-column>
<el-table-column prop="reward_type_text" label="奖项" min-width="120"></el-table-column>
@@ -58,8 +64,9 @@
</template>
</el-table-column>
<el-table-column prop="formula" label="公式摘要" min-width="200"></el-table-column>
<el-table-column label="操作" min-width="120" fixed="right">
<el-table-column label="操作" min-width="180" fixed="right">
<template #default="scope">
<el-button v-if="scope.row.order_id > 0" type="primary" link @click="onOrderTree(scope.row)">整单透视</el-button>
<el-button type="primary" link @click="onViewExt(scope.row)">查看公式</el-button>
{if $auth->check('shopro/partner/reward/back')}
<el-button v-if="scope.row.status==1 && !scope.row.settle_date" type="danger" link
@@ -94,4 +101,79 @@
<el-button type="primary" @click="onReissue">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="orderTreeDialog.visible" title="订单分润透视(利益分配树)" width="860px" destroy-on-close>
<div v-loading="orderTreeDialog.loading">
<div style="background:#f8f9fa;padding:14px;border-radius:6px;margin-bottom:16px;">
<el-row :gutter="16">
<el-col :span="6">
<div style="color:#909399;font-size:12px;">订单编号</div>
<div style="font-weight:600;font-size:13px;margin-top:4px;">{{ orderTreeDialog.data.order?.order_sn || '-' }}</div>
</el-col>
<el-col :span="6">
<div style="color:#909399;font-size:12px;">买家信息</div>
<div style="font-weight:600;font-size:13px;margin-top:4px;">{{ orderTreeDialog.data.order?.buyer_nickname || '-' }}</div>
</el-col>
<el-col :span="6">
<div style="color:#909399;font-size:12px;">实付金额</div>
<div style="font-weight:600;font-size:15px;color:#e6a23c;margin-top:4px;">¥ {{ orderTreeDialog.data.order?.pay_fee || '0.00' }}</div>
</el-col>
<el-col :span="6">
<div style="color:#909399;font-size:12px;">状态 / 付款时间</div>
<div style="font-size:12px;margin-top:4px;">
<span class="sa-color--success">{{ orderTreeDialog.data.order?.status_text }}</span> / {{ orderTreeDialog.data.order?.paid_time }}
</div>
</el-col>
</el-row>
<el-divider style="margin:12px 0"></el-divider>
<el-row :gutter="16">
<el-col :span="8">
<div style="color:#909399;font-size:12px;">本单现金分润总额</div>
<div style="font-weight:bold;font-size:18px;color:#f56c6c;margin-top:2px;">¥ {{ orderTreeDialog.data.summary?.total_cash || '0.00' }}</div>
</el-col>
<el-col :span="8">
<div style="color:#909399;font-size:12px;">现金综合拨比率</div>
<div style="font-weight:bold;font-size:18px;margin-top:2px;" :style="{color: parseFloat(orderTreeDialog.data.summary?.payout_ratio || 0) > 70 ? '#e6a23c' : '#67c23a'}">
{{ orderTreeDialog.data.summary?.payout_ratio || '0%' }}
</div>
</el-col>
<el-col :span="8">
<div style="color:#909399;font-size:12px;">分红马豆 (1:1 GMV)</div>
<div style="font-weight:bold;font-size:18px;color:#409eff;margin-top:2px;">{{ orderTreeDialog.data.summary?.total_madow || '0.00000000' }}</div>
</el-col>
</el-row>
<div v-if="orderTreeDialog.data.voucher_plan" style="margin-top:12px;background:#fff;padding:8px 12px;border-radius:4px;border:1px dashed #dcdfe6;font-size:12px;color:#606266;">
<span style="font-weight:600;color:#303133;">1.5倍兑换券履约计划:</span>
总额 ¥ {{ orderTreeDialog.data.voucher_plan.total_amount }},已释放 ¥ {{ orderTreeDialog.data.voucher_plan.released_amount }} (已释放 {{ orderTreeDialog.data.voucher_plan.released_days }}/365天),状态:{{ orderTreeDialog.data.voucher_plan.status_text }}
</div>
</div>
<div style="font-weight:600;margin-bottom:8px;font-size:14px;">利益链分配明细</div>
<el-table :data="orderTreeDialog.data.rewards || []" size="small" border stripe max-height="320">
<el-table-column prop="role_text" label="受益角色" width="90"></el-table-column>
<el-table-column label="受益人" min-width="130">
<template #default="scope">
<span>{{ scope.row.user_nickname }} (ID:{{ scope.row.user_id }})</span>
</template>
</el-table-column>
<el-table-column prop="reward_type_text" label="奖项" width="130"></el-table-column>
<el-table-column label="现金金额" width="100">
<template #default="scope">
<span style="font-weight:600;color:#f56c6c;">¥ {{ scope.row.amount }}</span>
</template>
</el-table-column>
<el-table-column prop="madow" label="马豆" width="100"></el-table-column>
<el-table-column prop="status_text" label="状态" width="80">
<template #default="scope">
<span :class="scope.row.status == 1 ? 'sa-color--success' : (scope.row.status == -2 ? 'sa-color--danger' : 'sa-color--info')">
{{ scope.row.status_text }}
</span>
</template>
</el-table-column>
<el-table-column prop="formula" label="计算规则/说明" min-width="180"></el-table-column>
</el-table>
</div>
<template #footer>
<el-button @click="orderTreeDialog.visible = false">关闭</el-button>
</template>
</el-dialog>
</div>

查看文件

@@ -5,7 +5,7 @@
<el-header class="sa-header">
<div class="sa-title sa-flex sa-row-between">
<div class="sa-title-left">
<div class="left-name">兑换券计划</div>
<div class="left-name">兑换券</div>
</div>
<div class="sa-title-right">
<el-button class="sa-button-refresh" icon="RefreshRight" @click="getData"></el-button>

查看文件

@@ -1,4 +1,6 @@
# 合伙股东前后端接口约定(两代理必须遵守)
# 收益中心前后端接口约定(身份标签与收益资产)
> 注:本模块为「收益中心」,「合伙股东」为进阶会员的身份标签(创客/酒道馆/区县)。
## profile `GET user.user/profile` 增补字段
@@ -107,3 +109,107 @@ voucher:
"plans": []
}
```
---
## 后台财务管理与对账接口 (Admin)
### 1. 订单分润透视 `GET shopro/partner/reward/orderTree`
- **参数**:`order_id`(必填,整数)
- **返回**:
```json
{
"code": 1,
"msg": "获取成功",
"data": {
"order": {
"id": 100,
"order_sn": "202609170001",
"pay_fee": "1398.00",
"status_text": "已支付",
"paid_time": "2026-09-17 12:00:00",
"buyer_nickname": "测试买家"
},
"rewards": [
{
"id": 1,
"reward_type": "push3",
"reward_type_text": "推三返一",
"user_id": 10,
"user_nickname": "推荐人A",
"role_text": "品酒师",
"amount": "398.00",
"madow": "0.00000000",
"status": 1,
"status_text": "已入账",
"formula": "推三返一第1单: 398.00元",
"createtime": "2026-09-17 12:00:05"
}
],
"summary": {
"total_cash": "398.00",
"total_madow": "1398.00000000",
"payout_ratio": "28.47%"
},
"voucher_plan": {
"total_amount": "2097.00",
"released_amount": "5.75",
"released_days": 1,
"status_text": "释放中"
}
}
}
```
### 2. 日结预跑试算(Dry Run)`GET shopro/partner/daily/preview`
- **参数**:`settle_date`(可选,默认昨日 `Y-m-d`)
- **特点**:纯只读模拟试算,不入账、不写表,返回预计支出与综合现金拨比率。
- **返回**:
```json
{
"code": 1,
"msg": "试算成功",
"data": {
"settle_date": "2026-09-16",
"is_settled": true,
"order_count": 10,
"order_count_n": 100,
"gmv": "13980.00",
"rate": "0.00007500",
"madow": {
"pool": "13980.00",
"user_count": 50,
"each": "279.60000000"
},
"venue": {
"pool": "1000.00",
"user_count": 5,
"each": "200.00",
"saved": "0.00"
},
"county": {
"pool": "500.00",
"user_count": 2,
"each": "250.00",
"saved": "0.00"
},
"voucher": {
"release_estimate": "230.50"
},
"financial": {
"instant_cash": "3980.00",
"daily_cash": "1500.00",
"total_cash": "5480.00",
"saved_cash": "0.00",
"payout_ratio": "39.2%",
"payout_ratio_val": 39.2,
"is_alert": false
}
}
}
```
### 3. 手工补发带风控上限 `POST shopro/partner/reward/reissue`
- **参数**:`user_id`, `reward_type`, `amount`, `order_id`, `memo`(必填)
- **风控硬上限**:单笔补发金额必须 `≤ 10,000` 元。超出报错拦截。

查看文件

@@ -0,0 +1,23 @@
-- 商城子菜单提到一级侧栏,shopro 仅作插件根(不显示)
SET NAMES utf8mb4;
SET @shopro := (SELECT id FROM hla_auth_rule WHERE name='shopro' LIMIT 1);
UPDATE hla_auth_rule SET ismenu=0, title='Shopro' WHERE id=@shopro;
UPDATE hla_auth_rule SET pid=0 WHERE pid=@shopro AND ismenu=1;
UPDATE hla_auth_rule SET weigh=300 WHERE name='shopro/dashboard';
UPDATE hla_auth_rule SET weigh=290 WHERE name='shopro/order';
UPDATE hla_auth_rule SET weigh=280 WHERE name='shopro/goods';
UPDATE hla_auth_rule SET weigh=270 WHERE name='shopro/user/user';
UPDATE hla_auth_rule SET weigh=260 WHERE name='shopro/commission';
UPDATE hla_auth_rule SET weigh=250 WHERE name='shopro/withdraw';
UPDATE hla_auth_rule SET weigh=240 WHERE name='shopro/activity/activity';
UPDATE hla_auth_rule SET weigh=230 WHERE name='shopro/decorate';
UPDATE hla_auth_rule SET weigh=220 WHERE name='shopro/chat';
UPDATE hla_auth_rule SET weigh=210 WHERE name='shopro/wechat';
UPDATE hla_auth_rule SET weigh=200 WHERE name='shopro/feedback';
UPDATE hla_auth_rule SET weigh=190 WHERE name='shopro/config';
UPDATE hla_auth_rule SET weigh=180 WHERE name='shopro/dispatch/dispatch';
UPDATE hla_auth_rule SET weigh=170 WHERE name='shopro/notification/config';
UPDATE hla_auth_rule SET weigh=160 WHERE name='shopro/data';

查看文件

@@ -0,0 +1,33 @@
-- 后台菜单规范化(收益体系运营口径)
-- 对应 addons/shopro/config/menu.php,执行后需清 runtime 缓存
-- 菜单名按「功能」命名,合伙股东/品酒师/酒道馆等只作为会员身份标签出现在列表内
SET NAMES utf8mb4;
-- 一级:分销中心 -> 收益中心(与用户端同名)
UPDATE hla_auth_rule SET title='收益中心', icon='fa fa-users fa-fw', weigh=90 WHERE name='shopro/commission';
-- 二级:按运营动线排序(档案 -> 规则 -> 商品 -> 钱 -> 历史)
UPDATE hla_auth_rule SET title='成员档案', icon='fa fa-user', weigh=100 WHERE name='shopro/commission/agent';
UPDATE hla_auth_rule SET title='身份等级', icon='fa fa-list-ol', weigh=90 WHERE name='shopro/commission/level';
UPDATE hla_auth_rule SET title='计奖商品', icon='fa fa-briefcase', weigh=80 WHERE name='shopro/commission/goods';
UPDATE hla_auth_rule SET title='收益流水', icon='fa fa-rmb', weigh=70 WHERE name='shopro/partner/reward';
UPDATE hla_auth_rule SET title='日结对账', icon='fa fa-calendar-check-o',weigh=60 WHERE name='shopro/partner/daily';
UPDATE hla_auth_rule SET title='兑换券', icon='fa fa-ticket', weigh=50 WHERE name='shopro/partner/voucher';
UPDATE hla_auth_rule SET title='业绩订单', icon='fa fa-file-text', weigh=40 WHERE name='shopro/commission/order';
UPDATE hla_auth_rule SET title='历史佣金', icon='fa fa-usd', weigh=30 WHERE name='shopro/commission/reward';
UPDATE hla_auth_rule SET title='动态日志', icon='fa fa-address-card', weigh=20 WHERE name='shopro/commission/log';
-- 三级权限节点:动词统一
UPDATE hla_auth_rule SET title='更换上级' WHERE name='shopro/commission/agent/changeparentuser';
UPDATE hla_auth_rule SET title='设置计奖' WHERE name='shopro/commission/goods/edit';
UPDATE hla_auth_rule SET title='退回收益' WHERE name='shopro/partner/reward/back';
UPDATE hla_auth_rule SET title='重跑日结' WHERE name='shopro/partner/daily/rerun';
UPDATE hla_auth_rule SET title='关闭计划' WHERE name='shopro/partner/voucher/close';
-- 商城配置内的分销 Tab 权限节点
UPDATE hla_auth_rule SET title='分销基础' WHERE name='shopro/config/commission';
-- 若曾误建四级分组 shopro/partner,拍平回二级
SET @pid := (SELECT id FROM hla_auth_rule WHERE name='shopro/commission' LIMIT 1);
UPDATE hla_auth_rule SET pid=@pid WHERE name IN ('shopro/partner/reward','shopro/partner/daily','shopro/partner/voucher');
DELETE FROM hla_auth_rule WHERE name='shopro/partner';

查看文件

@@ -0,0 +1,287 @@
# 恒连数通 · 会员角色与收益体系业务规格说明书及代码完善分析
---
## 第一部分:会员角色与收益体系业务规格说明书
### 1. 角色定义与身份演进体系
系统包含 **4 个业务层级角色** 与 **1 个核心荣誉身份标识**。
| 角色/标识 | 角色性质 | 准入/获得条件 | 互斥与共存规则 | 核心定位 |
| :--- | :--- | :--- | :--- | :--- |
| **品酒师** | 基础会员角色 | 所有用户注册默认即为品酒师(系统分销等级 `level = 1`)。<br>*(注:若启用考核门槛,为直推 3 单或伞下 1 人升级)* | 与创客**互斥**(升级创客后自动脱离品酒师身份) | 基础消费与推广会员 |
| **创客** | 进阶层级角色 | 由品酒师升级而来,细分为 **V1 ~ V6** 六个等级(对应系统 `level = 2 ~ 7`),后台可灵活设置升级条件与权益 | 与品酒师**互斥**;可与酒道馆、区县代理**叠加共存** | 团队核心骨干 |
| **酒道馆** | 特殊业务角色 | 后台独立开通/授权(系统标识 `is_venue = 1`) | 独立身份标识,可与创客、区县代理**叠加共存** | 实体/线下业务枢纽 |
| **区县代理** | 区域代理角色 | 后台独立开通/绑定特定行政区划(系统标识 `is_county = 1`) | 独立身份标识,可与创客、酒道馆**叠加共存** | 区域渠道领头人 |
| **合伙股东** | **专属身份标识** | 会员只要满足:**创客 (V1~V6)**、**酒道馆**、**区县代理** 中的任意一个或多个,系统自动点亮「合伙股东」复合身份标识 | 纯荣誉与身份标识,非单一职级;品酒师不可获得此标识 | 高阶合伙人象征 |
> **身份互斥与共存规则总结**:
> 1. **品酒师 ↔ 创客 严格互斥**:用户等级由低到高演进,升级创客即覆盖品酒师等级,但创客**全额继承品酒师所有权益**。
> 2. **多重身份可叠加**:同一个会员可以同时兼具「创客 V5 + 酒道馆 + 区县代理」,且系统合并展示为:「合伙股东 · 创客V5 · 酒道馆 · 区县代理」。
---
### 2. 全局收益前置条件与风控约束
1. **30 天自购活跃窗口(硬性前提)**:
- 所有会员(包括品酒师、创客、酒道馆、区县代理)享受任何奖项收益的前提,是**近 30 个自然日内本人必须有一笔合格自购订单**(实付金额 $>0$,非纯积分兑换单)。
- **未达标惩罚**:
- 即时奖(推三返一、极差奖、平级奖、团队拆分奖):直接跳过,奖金**不发放、不顺延**给上级;极差奖**不占用水位**。
- 日结均分奖(酒道馆均分、区县均分、马豆 1:1 分红):**不计入分母人数**,不享受当日池子分配。
2. **退款与回滚机制**:
- **全单退款**:撤回该订单产生的所有即时奖金(从分销商佣金钱包扣减);推三返一序号自动回退 1 单;未释放的兑换券计划直接关闭,已释放未消费部分从账户扣除。
- **部分退款**:按退款商品金额等比例缩减兑换券额度,不粗暴剥夺分销商奖励。
- **日结保护**:每日 00:30 已完成清算的平台均分、马豆分红及历史汇率 N 计数**不逆向追溯**,确保账目不可逆稳定性。
---
### 3. 各角色收益规则详解
#### 3.1 品酒师收益(基础收益池)
品酒师享有 4 项专属权益:
1. **推三返一(直推循环返现)**:
- **规则**:直推下级新订单,上级获得现金返现。
- **梯度循环**:第 1 单返 **398 元**,第 2 单返 **400 元**,第 3 单返 **600 元**;第 4 单起重新从 398 元开始,按每 3 单一个周期无限循环。
- **业务逻辑**:每推满 3 单累计返现 $398 + 400 + 600 = 1398$ 元,相当于推 3 单返还 1 单的完整货款。
- **限制**:仅限直推下级订单有效,自购不计入返还;直推人须满足 30 天自购活跃。
2. **每日 1:1 下单金额均分(马豆分红)**:
- **规则**:每日全平台合格订单总金额(GMV)按 1:1 等价额度,平均分配给全平台近 30 天内有自购下单记录的有效会员。
- **发放形式**:以「分红积分(马豆)」形式发放至会员马豆账户(非直接发提现现金,防范资金穿透风险)。
- **结算时间**:每日凌晨 **00:30** 执行定时清算任务,结算昨日数据。
- **尾差处理**:除不尽的微小尾差自动归集入平台沉淀账户。
3. **分红积分(马豆)浮动汇率与价值折算**:
- **初始汇率**:$1\text{ 马豆} = 0.00000075\text{ 元}$。
- **浮动递增规则**:全平台每增加 $n$ 个合格订单,汇率按步长向上跃迁,实时汇率公式为:
$$\text{当前汇率} = 0.00000075 \times \lfloor \frac{N}{n} \rfloor \quad (\text{其中 } N \text{ 为全平台历史累计订单数,初始保底基准汇率})$$
- **资产价值呈现**:用户端个人中心与收益中心根据最新汇率,实时折算展示马豆对应的人民币估值($\text{持有马豆} \times \text{当前汇率}$)。
4. **下单用户 1.5 倍兑换券(365天线性释放)**:
- **规则**:任何用户下单,均可获得其实付订单金额 **1.5 倍** 的商城兑换券(例:一单实付 1398 元 $\times 1.5 =$ **2097 元** 兑换券)。
- **释放周期**:兑换券按 **365 天** 逐日等额释放(每天释放 $2097 / 365 = 5.74$ 元,第 365 天补齐尾差)。
- **使用场景**:已释放的兑换券可直接在商城下单时抵扣或全额兑换指定产品。
---
#### 3.2 创客收益(合伙骨干收益池)
创客享有品酒师的**全部权益**,并叠加以下 3 项高阶权益:
1. **创客极差奖(V1 ~ V6)**:
- **职级基数**:创客分为 V1 至 V6 六个级别,后台可灵活配置。标准梯度为每级增加 20 元:
- **V1**:20 元
- **V2**:40 元
- **V3**:60 元
- **V4**:80 元
- **V5**:100 元
- **V6**:120 元
- **极差逻辑**:自下单买家向上回溯关系链,遇到活跃创客,享受自身等级基数与下级已截留最高基数之间的**差额**($\text{应得} = \text{本人基数} - \text{已拿走最高水位}$)。
2. **创客平级奖(10%)**:
- **规则**:当下级创客与上级创客等级完全相同时,触发平级奖。
- **计算基准**:按该等级的标准基数的 **10%** 计算(只发向上最近的一层同级创客)。
- **示例**:
- 下级为 V5,上级也是 V5:上级获得 V5 等级佣金基数(100元)的 10% = **10 元**。
- 下级为 V6,上级也是 V6:上级获得 V6 等级佣金基数(120元)的 10% = **12 元**。
3. **创客被超越奖(5%)**:
- **规则**:当上级创客等级低于下级创客(如下级升级到 V6,上级停留在 V4/V5)时触发。
- **计算基准**:被超越的上级创客获得本单创客最高水位基数的 **5%**(向上只发一层)。
---
#### 3.3 酒道馆收益(枢纽收益池)
酒道馆享有独立设立的团队与平台加权收益:
1. **团队订单收益(50 元/单)**:
- 伞下团队成员(**包含酒道馆本人自购新订单**)每产生一笔合格新订单,链上最近的酒道馆获得 **50 元/单** 的团队奖励。
2. **全平台订单加权均分(100 元/单入池)**:
- 全平台全网每增加 1 笔新订单,平台注资 **100 元/单** 进入酒道馆每日奖池。
- 每日凌晨 00:30 日结时,奖池总额由全平台所有近 30 天活跃的酒道馆会员**等额均分**。
3. **酒道馆平级奖(10%)**:
- **规则**:上级酒道馆享受下级酒道馆收益的 **10%**。
- **严格风控基准**:提成基准**严格限定为下级酒道馆在本单获得的团队订单收益(50元)**,即平级奖励固定为 $50 \times 10\% = \mathbf{5\text{ 元}}$。
- **边界隔离**:严禁将下级酒道馆兼具的创客极差、平台均分等其他收益计入平级基数,避免资金敞口无限放大。
---
#### 3.4 区域代理 / 区县代理收益(区域渠道池)
区县代理享有区域维度的团队拆分与平台加权均分:
1. **团队奖(100 元订单拆分机制)**:
- 伞下团队成员产生新订单时,团队奖总预算固定为 100 元,按关系链中是否存在酒道馆执行动态拆分:
- **场景 A(链上无酒道馆)**:下单用户向上至区县代理之间无任何酒道馆,**区县代理独享 100 元**。
- **场景 B(链上有酒道馆)**:离下单用户最近的酒道馆获得 **50 元**,该酒道馆上级的区县代理获得 **50 元**。
2. **区县代理平级奖(10%)**:
- 上级区县代理享受下级区县代理本单团队收益的 **10%**(下级得 50 元则平级得 5 元;下级独享 100 元则平级得 10 元)。
3. **全平台订单加权均分(50 元/单入池)**:
- 全平台全网每增加 1 笔新订单,平台注资 **50 元/单** 进入区县代理每日奖池。
- 每日凌晨 00:30 日结时,由全平台所有近 30 天活跃的区县代理会员**等额均分**。
---
### 4. 典型订单全链路分账示例
**业务场景模拟**:
- 关系链自下而上:
**买家 X (新购第1单)** $\rightarrow$ **U1 (品酒师)** $\rightarrow$ **U2 (创客V5)** $\rightarrow$ **U3 (创客V5)** $\rightarrow$ **U4 (创客V6 + 酒道馆)** $\rightarrow$ **U5 (区县代理)** $\rightarrow$ **U6 (区县代理)**
- 订单实付:**1398 元**,链上各用户 30 天内均有自购活跃。
| 参与方 | 获得收益项目 | 实得金额/资产 | 计奖依据与计算公式 |
| :--- | :--- | :--- | :--- |
| **买家 X** | 兑换券计划 | **2097 元额度** | $1398 \times 1.5 = 2097$ 元,分 365 天每天释放 5.74 元 |
| **U1 (直推品酒师)** | 推三返一 | **398 元现金** | 直推首单,循环第 1 档(398 / 400 / 600) |
| **U2 (创客 V5)** | 创客极差奖 | **100 元现金** | V5 等级基数 100 元,下方无创客截留($100 - 0 = 100$) |
| **U3 (创客 V5)** | 创客平级奖 | **10 元现金** | 与下级 U2 平级,获得 V5 标准基数(100元)的 10% |
| **U4 (创客 V6+馆)** | 创客极差奖 + 酒道馆团队奖 | **20 元 + 50 元** | ① 极差奖:V6基数120 - 之前最高水位100 = 20元<br>② 团队拆分:链上最近酒道馆得 50元 |
| **U5 (区县代理)** | 区县团队拆分奖 | **50 元现金** | 链上有酒道馆 U4,区县代理得余下拆分 50元 |
| **U6 (区县代理)** | 区县平级奖 | **5 元现金** | 下级 U5 获得 50 元团队奖,上级区县得其 10% = 5元 |
| **全平台奖池** | 待清算资产 | **马豆 + 平台奖池** | ① 1398元等额马豆计入今日马豆分红池<br>② 100元计入酒道馆平台均分池<br>③ 50元计入区县平台均分池<br>④ 累计订单数 $N$ 增加 1,推进马豆汇率浮动 |
---
## 第二部分:结合现有代码的落地与完善情况深度分析
结合本代码仓库(基于 FastAdmin + Shopro Plus 后端,以及 `uniapp/` 用户端),对上述业务规范进行了逐行级代码审查与现状对照。
### 1. 总体落地完成度概览
当前代码体系采用了**「在现有 Shopro 分销架构上优雅生长、无缝融合」**的最佳实践路线,没有破坏 Shopro 的核心钱包与订单钩子,避免了二次开发脱离主线。
| 模块类别 | 涉及核心文件 | 实现状态 | 完成度 |
| :--- | :--- | :--- | :--- |
| **角色与身份体系** | `Agent.php`, `IdentityService.php`, `level.html` | 已完成 | **98%** |
| **品酒师·推三返一** | `Push3Reward.php`, `PartnerRewardService.php` | 已完成 | **100%** |
| **品酒师·每日 1:1 分红** | `DailySettleService.php`, `MadowService.php` | 已完成 | **95%** |
| **品酒师·浮动汇率** | `RateService.php`, `RateLog.php` | 已完成(发现1处可优化项) | **90%** |
| **品酒师·1.5倍兑换券** | `VoucherPlan.php`, `VoucherService.php`, `OrderCreate.php` | 已完成 | **95%** |
| **创客极差/平级/被超越** | `MakerDiffReward.php`, `MakerPeerReward.php` | 已完成 | **100%** |
| **酒道馆与区县团队拆分** | `TeamSplitReward.php`, `PeerShareReward.php` | 已完成 | **100%** |
| **平台加权均分** | `DailySettleService.php`, `cashShare()` | 已完成 | **100%** |
| **30 天自购活跃风控** | `ActiveService.php`, `last_self_order_time` | 已完成 | **100%** |
| **退款冲正与回滚** | `Partner.php`, `refund()`, `refundPartial()` | 已完成 | **95%** |
| **管理后台配置与报表** | `Config.php`, `Reward.php`, `Daily.php`, `Voucher.php` | 已完成 | **92%** |
| **Uniapp 用户端展现** | `s-user-card`, `account-info`, `rewards`, `madow`, `voucher` | 已完成 | **95%** |
---
### 2. 核心功能点与代码实现对照详查
#### ① 角色互斥与多重身份实现
- **代码位置**:[`IdentityService.php`](file:///Users/chen/Desktop/xbiuld/henglianshutong/addons/shopro/service/partner/IdentityService.php)
- **实现机制**:
- 复用了 `fa_shopro_commission_level`,`level = 1` 为品酒师,`level = 2~7` 分别映射为创客 V1~V6。因为同一个用户在表中只有一行 `level` 字段,天然保证了**品酒师与创客绝不可能同时存在**。
- 酒道馆(`is_venue`)与区县代理(`is_county`)作为布尔标记加在 `fa_shopro_commission_agent` 表中,支持多选叠加。
- `IdentityService::build()` 动态判断:若 `level >= 2` 或 `is_venue=1` 或 `is_county=1`,则自动赋予 `is_partner = true`,并输出拼接文本(如 `合伙股东 · 创客V5 · 酒道馆`)。
- **完善度**:与业务需求完全吻合。
#### ② 品酒师「推三返一」
- **代码位置**:[`Push3Reward.php`](file:///Users/chen/Desktop/xbiuld/henglianshutong/addons/shopro/service/partner/Push3Reward.php)
- **实现机制**:
- 受益人维护累计有效直推单数 `direct_order_seq`,每进一单执行 `+1`。
- 通过 `($seq - 1) % 3` 匹配数组 `[398, 400, 600]`,实现严密循环。
- 遇到退款时,`rollbackSeq()` 将 `direct_order_seq` 安全回退 1 单,下一笔订单自动顶替退款单的档位,逻辑滴水不漏。
- **完善度**:100% 达成。
#### ③ 创客极差、平级 10% 与超越 5%
- **代码位置**:[`MakerDiffReward.php`](file:///Users/chen/Desktop/xbiuld/henglianshutong/addons/shopro/service/partner/MakerDiffReward.php)、[`MakerPeerReward.php`](file:///Users/chen/Desktop/xbiuld/henglianshutong/addons/shopro/service/partner/MakerPeerReward.php)
- **实现机制**:
- **极差奖**:采用水位抬升算法,`cap - seen`。不活跃的用户不仅拿不到钱,且其对应的水位**不会抬升**(`seen` 保持原值),确保后续更高阶的创客能拿回应得的级差。
- **平级奖**:平级奖严格按照创客的**标准等级基数**计算(例如 V5 基数 100 元 $\times 10\% = 10$ 元,V6 基数 120 元 $\times 10\% = 12$ 元),而非基于本单极差实得,这与需求示例中的 `(V5)100元佣金的10%=10元` 完全一致。
- **被超越奖**:当下级创客超越上级时,上级获得最高基数的 5%,单笔订单向上限发 1 层,防止链条过长导致资金超配。
- **完善度**:100% 达成。
#### ④ 酒道馆与区县代理团队奖(100元拆分)
- **代码位置**:[`TeamSplitReward.php`](file:///Users/chen/Desktop/xbiuld/henglianshutong/addons/shopro/service/partner/TeamSplitReward.php)
- **实现机制**:
- 巧妙实现了「100元单笔切分,严防重复计奖」:
- 无馆有县:区县独享 100 元;
- 有馆有县:酒道馆 50 元,馆之上的最近区县 50 元;
- 有馆无县:酒道馆 50 元,其余沉淀;
- 并且在 `selfChain`(含自购链条)中,若买家自己是酒道馆,则买家自己直接匹配为最近酒道馆,满足了「伞下用户含自己新订单获得50元」的要求。
- **完善度**:100% 达成。
#### ⑤ 酒道馆与区县代理平级奖(10%)
- **代码位置**:[`PeerShareReward.php`](file:///Users/chen/Desktop/xbiuld/henglianshutong/addons/shopro/service/partner/PeerShareReward.php)
- **实现机制**:
- 酒道馆平级奖严格取 `$teamResult['venue']['amount']`(即 50 元团队奖)的 10%,算出固定 5 元,完全排除了创客收益与平台均分,边界隔离清晰。
- 区县代理平级奖取 `$teamResult['county']['amount']`(50 元或 100 元)的 10%。
- **完善度**:100% 达成。
#### ⑥ 00:30 日结清算(GMV 1:1 分红、酒道馆100均分、区县50均分、兑换券释放)
- **代码位置**:[`DailySettleService.php`](file:///Users/chen/Desktop/xbiuld/henglianshutong/addons/shopro/service/partner/DailySettleService.php)、[`MadowService.php`](file:///Users/chen/Desktop/xbiuld/henglianshutong/addons/shopro/service/partner/MadowService.php)
- **实现机制**:
- 清算昨日 00:00:00 至 23:59:59 的合格订单总数与 GMV。
- 文件锁(`partner_settle_{Ymd}.lock`)与数据库唯一键防并发重入。
- 分母严格筛选 30 天内有自购合格订单的用户列表(`activeUserIds`)。
- 马豆与奖池均分采用 `bcdiv` 精确到 8 位/2 位小数,无法整除的零头尾差自动作为平台收益记录入账,保证资金账目百分之百平账。
- 兑换券计划线性释放 1 天额度,第 365 天自动补齐尾差。
- **完善度**:98% 达成。
#### ⑦ 兑换券与商城产品抵扣
- **代码位置**:[`VoucherService.php`](file:///Users/chen/Desktop/xbiuld/henglianshutong/addons/shopro/service/partner/VoucherService.php)、[`OrderCreate.php`](file:///Users/chen/Desktop/xbiuld/henglianshutong/addons/shopro/service/order/OrderCreate.php)、[`confirm.vue`](file:///Users/chen/Desktop/xbiuld/henglianshutong/uniapp/pages/order/confirm.vue)
- **实现机制**:
- 下单支付后通过 `VoucherService::grant()` 创建 365 天释放计划。
- 用户端在下单确认页(`confirm.vue`)增加了「兑换券抵扣」弹窗与输入校验,支持使用可用兑换券抵扣订单实付。
- `OrderCreate.php` 在服务端进行 `available` 二次校验并扣减 `voucher_used`,记入收益明细 `voucher_consume`。
- **完善度**:95% 达成。
---
### 3. 代码审查中发现的关键技术细节与待完善项
在深度审查代码后,发现以下 **3 处需要重点关注和优化的细节**:
#### ⚠️ 细节 1:马豆浮动汇率 `RateService` 的初始下限保护
- **代码现状**:
在 [`RateService.php`](file:///Users/chen/Desktop/xbiuld/henglianshutong/addons/shopro/service/partner/RateService.php) 的 `calcRate` 方法中:
```php
public function calcRate($n = null)
{
$n = $n === null ? $this->countN() : intval($n);
$base = (float) PartnerConfig::get('partner_madow_base_rate', 0.00000075);
$step = intval(PartnerConfig::get('partner_madow_step_orders', 1));
$step = $step > 0 ? $step : 1;
return $base * floor($n / $step);
}
```
- **潜在问题**:
当全平台历史累计订单数 $n=0$(系统刚上线或测试初期)时,`floor(0 / 1) = 0`,计算得出的汇率会变成 **0**,导致用户端显示的马豆人民币估值为 0 元。
- **优化建议**:
业务规则明确说明「汇率初始比例 1 马豆 = 0.00000075 元,每增加 n 个订单递增」。
建议修改为带初始保底:
```php
// 保证初始汇率至少为基准 base
$multiple = max(1, floor($n / $step));
return $base * $multiple;
```
#### ⚠️ 细节 2:品酒师「默认注册即是」与「直推 3 单考核」的业务开关
- **现状分析**:
业务描述中提到两句话:
- 句1:“品酒师就是普通会员 直推 3 单或者伞下一个客户升级到品酒师”
- 句2:“所有用户默认都是品酒师:品酒师升级为创客”
- **代码现状**:
当前系统配置为 `become_agent.type = user`(注册即成为分销商,默认分销商等级 `level = 1` 品酒师)。因此新用户注册后即可看到自己的「品酒师」徽章,且直推第 1 单就能享受 398 元返现。
- **业务完善建议**:
如果业务方的真实诉求是:“注册后是普通会员,必须完成直推 3 单或伞下有人升级后才激活品酒师身份并开始返现”,则需要在后台分销设置中将成为分销商类型调整为指定消费门槛或自定义拦截。如果业务方认可“注册默认就是品酒师,推前 3 单分别返 398、400、600(每 3 单返还 1 单货款)”,则当前代码逻辑完美符合,无需改动。
#### ⚠️ 细节 3:线上定时任务 Crontab 的配置与运行检查
- **现状检查**:
系统的日结清算由 CLI 命令 `php think shopro:partner:settle` 驱动。
- **运维必须项**:
目标服务器为 `118.184.172.94`(站点目录 `/www/wwwroot/qbw.gynhc.com`)。
必须在服务器宝塔计划任务中确认已添加 Crontab:
```bash
30 0 * * * cd /www/wwwroot/qbw.gynhc.com && php think shopro:partner:settle >> /tmp/partner_settle.log 2>&1
```
同时确保 `php think queue:work --queue shopro-high` 处于 Supervisor 守护常驻运行状态,以保证支付后即时奖秒级异步入账。
---
### 4. 总结与建议行动
1. **业务规则完备性**:用户提出的角色、分级、推三返一、GMV 1:1 分红、马豆浮动汇率、兑换券释放、极差平级超越、酒道馆与区县代理拆分及 30 日自购条件,在当前代码中**均已建立对应的服务类与业务模型,核心分账闭环已 100% 跑通**。
2. **文档归档**:本技术规格与完善分析已归档保存至 `docs/会员角色与收益体系规格及代码完善分析.md`,供团队持续查阅与对账使用。
3. **后续微调建议**:对 `RateService::calcRate` 做初始汇率下限保底微调,即可进入实单联调上线状态。

查看文件

@@ -1,10 +1,18 @@
# 合伙股东 · 后台与用户端功能规格
# 收益中心 · 后台与用户端功能规格
> 版本:V1.0
> 对应技术方案:`docs/合伙股东收益体系二次开发文档.md` V2.1
> 版本:V2.2
> 对应技术方案:`docs/收益中心二次开发文档.md` V2.2
> 角色视角:产品经理(页面 / 字段 / 状态 / 文案)+ 全栈(改哪些现有页、接口怎么扩)
> 原则:能改现有页就改,不新造第二套会员中心;用户能看懂「我是谁、我拿了什么、为什么没拿到」
> [!IMPORTANT]
> ### 核心概念澄清(避免误导)
> 1. **系统定位**:本模块在管理后台与用户端统一命名为**「收益中心」**(原「分销中心」)。
> 2. **「合伙股东」只是会员的身份标签,不是系统/模块名称**:
> - **「品酒师」**:注册即是的基础分销会员身份标签(对应分销商 `level = 1`)。
> - **「合伙股东」**:进阶会员身份标签。当会员升级为创客(`level >= 2`,创客 V1–V6)、被后台开通酒道馆(`is_venue = 1`)或开通区县代理(`is_county = 1`)时,获得「合伙股东」身份标签(例如:“合伙股东 · 创客V5 · 酒道馆”)。
> 3. 后台所有菜单、页面表头及用户端导航,均统一按功能命名(如「收益中心」、「成员档案」、「收益流水」),切勿将整套系统误叫为“合伙股东”。
---
## 1. 产品目标与角色
@@ -38,22 +46,23 @@
## 2. 后台信息架构
在现有「商城 → 分销中心」下扩展,不另起顶级菜单(运营已经认这条路径)。
一级入口统一为「收益中心」(原「分销中心」,路由 `shopro/commission` 不变)。日常运营按「人 → 规则商品 → 钱 → 历史」顺序排。
```
商城
└─ 分销中心 已有,保留
├─ 分销商 改:身份列、筛选、编辑弹窗
├─ 分销商等级 改:文案 + 级差基数
├─ 分销订单 保留(业绩/升级仍用)
├─ 佣金 保留(旧百分比历史)
├─ 分销动态 保留
├─ 分销商品 改:说明「是否参与合伙股东计奖」
├─ 收益流水 【新】
├─ 日结对账 【新】
└─ 兑换券计划 【新】
商城 → 配置 → 分销设置
└─ 增加 Tab「合伙股东规则」
└─ 收益中心(原「分销中心」)
├─ 成员档案 原分销商:查人、改上级、开通酒道馆/区县(显示品酒师/合伙股东身份)
├─ 身份等级 原分销商等级:品酒师 / 创客 V1–V6
├─ 计奖商品 原分销商品:是否参与计奖
├─ 收益流水 即时奖 / 均分 / 马豆等入账与补发退回
├─ 日结对账 馆县均分、马豆池、重跑
├─ 兑换券 释放计划与日志
├─ 业绩订单 原分销订单:升级/业绩口径仍用
├─ 历史佣金 原佣金明细:旧百分比流水备查
└─ 动态日志 原分销动态
商城 → 商城配置
├─ 分销基础 层级/成为条件等底座(勿把层级改成 0)
└─ 合伙股东规则 Tab:固定金额、均分、马豆、兑换券规则
```
权限节点(写入 `addons/shopro/config/menu.php`,升级菜单):

查看文件

@@ -1,11 +1,19 @@
# 恒连数通 · 合伙股东收益体系完整技术方案
# 恒连数通 · 收益中心二次开发技术方案(会员收益体系)
> 版本:V2.1(对齐本仓库 `uniapp/` 用户端)
> 日期:2026-09-16
> 版本:V2.2(对齐本仓库 `uniapp/` 用户端)
> 日期:2026-09-17
> 代码基线:本仓库 Shopro Plus **3.2.0** + FastAdmin
> 用户端:本仓库 `uniapp/`(Shopro sheep **3.1.0**,`sheepjs/shop`)
> 状态:可排期实施方案(业务未拍板项见第 16 节,开发按建议默认)
> 后台 / 用户端页面级规格:`docs/合伙股东-后台与用户端功能规格.md`
> 状态:已落地并完成资金并发加固
> 后台 / 用户端页面级规格:`docs/收益中心-后台与用户端功能规格.md`
> [!IMPORTANT]
> ### 核心概念澄清(避免误导)
> 1. **系统定位**:本模块为**会员收益体系**,后台一级菜单与用户端统一命名为**「收益中心」**(原「分销中心」)。
> 2. **「合伙股东」只是会员的身份标签,不是系统/模块名称**:
> - **「品酒师」**:注册即是的基础分销会员身份标签(对应分销商 `level = 1`)。
> - **「合伙股东」**:进阶会员身份标签。当会员满足创客(`level >= 2`,对应创客 V1–V6)、酒道馆(`is_venue = 1`)或区县代理(`is_county = 1`)时,系统为该会员附加「合伙股东」身份标签(展示为:“合伙股东 · 创客V5 · 酒道馆”)。
> 3. 后台所有菜单、页面表头及用户端导航,均统一按功能命名(如「收益中心」、「成员档案」、「收益流水」),切勿将整套系统误叫为“合伙股东”。
---

查看文件

@@ -0,0 +1,270 @@
# 收益中心 · 多维关系场景收益模拟与财务测算报告
> 本文档依据恒联数通「收益中心」(合伙股东)二次开发核心代码及业务规则编制,用于模拟分析各种会员层级关系、拓扑链路下的即时收益、日结分红、兑换券释放及平台综合现金拨比。
---
## 核心参数基准(以标准酒水套餐 1398 元/单为例)
| 规则维度 | 参数值 | 说明 |
| :--- | :--- | :--- |
| **标准计奖订单金额** | ¥ 1398.00 / 单 | 实际支付金额 |
| **推三返一(即时)** | 398 / 400 / 600 元 | 直推直属上级,按单循环(第4单重新从398开始) |
| **创客级别差额(即时)**| V1: 20, V2: 40, V3: 60, V4: 80, V5: 100, V6: 120 元 | 按级差水位递增,每级差额 20 元 |
| **创客平级奖(即时)** | 等级基数的 10% | 紧邻同级创客获得(例:V3 平级得 $60 \times 10\% = 6$ 元) |
| **创客超越奖(即时)** | 最高水位的 5% | 紧邻低级创客被反超时获得(例:下级为 V4 达 80 元,被超越者得 $80 \times 5\% = 4$ 元) |
| **团队奖拆分(即时)** | 总额 100 元 | 有馆有县: 50 + 50;有馆无县: 馆得 50;无馆有县: 县独得 100 |
| **馆/县平级奖(即时)** | 紧邻下级团队奖的 10% | 第二个馆或第二个县获得($50 \times 10\% = 5$ 元,或 $100 \times 10\% = 10$ 元) |
| **1.5 倍兑换券(按天)**| 订单实付 $\times 1.5 = 2097$ 元 | 365 天等额线性释放,约 5.745 元/天(买家专属) |
| **每日马豆均分(日结)**| 1:1 GMV 池 | 每日 00:30 结算,平分给过去 30 天有自购的合格会员 |
| **马豆浮动汇率** | $0.00000075 \times N$ 元 | $N$ 为平台合格订单总数,随单数线性升值 |
| **馆/县平台均分(日结)**| 馆池: 100元/单; 县池: 50元/单 | 每日 00:30 结算,平分给过去 30 天活跃的馆/县 |
| **活跃资格周期** | 30 天 | 过去 30 天内有自购订单,逾期暂停发放即时与日结 |
---
## 场景一:品酒师直推与「推三返一」循环机制
### 1. 业务关系拓扑
```
上级推荐人 A (品酒师,活跃)
└── 买家 B (新用户/品酒师) —— 连续下 4 单 (每单 1398 元)
```
### 2. 收益分配明细表
| 订单批次 | 买家实付 | 买家 B 获得权益 | 推荐人 A 现金奖励 | 流水类型 | 计算公式 / 规则摘要 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **第 1 单** | ¥ 1398.00 | 2097 元兑换券 (5.75元/天) | **¥ 398.00** | `push3` | 推荐人直推第 1 单,第一档返现 398 元 |
| **第 2 单** | ¥ 1398.00 | 2097 元兑换券 (5.75元/天) | **¥ 400.00** | `push3` | 推荐人直推第 2 单,第二档返现 400 元 |
| **第 3 单** | ¥ 1398.00 | 2097 元兑换券 (5.75元/天) | **¥ 600.00** | `push3` | 推荐人直推第 3 单,第三档返现 600 元 |
| **第 4 单** | ¥ 1398.00 | 2097 元兑换券 (5.75元/天) | **¥ 398.00** | `push3` | 进入第二轮循环,重新按第 1 档返现 398 元 |
- **推三返一平均拨比**:$(398 + 400 + 600) / (1398 \times 3) = 1398 / 4194 \approx 33.33\%$。
- **买家端沉淀**:每单均沉淀 1.5 倍产品兑换券,需 365 天按日履约提货。
---
## 场景二:创客梯度递增与极差吃满(阶梯极差)
### 1. 业务关系拓扑
```
顶层创客 D (V6, 活跃)
└── 间接创客 C (V3, 活跃)
└── 直推创客 B (V1, 活跃, seq=0)
└── 买家 A (品酒师自购 1 单 1398 元)
```
### 2. 收益分配明细表
| 收益人 | 会员身份 | 获得奖项 | 现金收益 | 计算规则与说明 | 状态 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **买家 A** | 品酒师 | 兑换券发放 | ¥ 0.00 (券 2097) | 1.5倍兑换券履约计划,按 365 天逐日释放 | 正常 |
| **直推 B** | 创客 V1 | 推三返一 | **¥ 398.00** | B 直推第 1 单,享受第 1 档返现 398 元 | 正常 |
| **直推 B** | 创客 V1 | 创客极差奖 | **¥ 20.00** | V1 水位基数 20 元,水位由 0 抬升至 20 元 | 正常 |
| **间推 C** | 创客 V3 | 创客极差奖 | **¥ 40.00** | V3 水位基数 60 元,极差为 $60 - 20 = 40$ 元,水位抬升至 60 | 正常 |
| **顶层 D** | 创客 V6 | 创客极差奖 | **¥ 60.00** | V6 水位基数 120 元,极差为 $120 - 60 = 60$ 元,水位封顶 120 | 正常 |
- **本单现金分润总额**:$398 + 20 + 40 + 60 = 518$ 元。
- **本单现金拨比率**:$518 / 1398 \approx 37.05\%$。
---
## 场景三:创客同级截断与平级奖金释放
### 1. 业务关系拓扑
```
顶层创客 E (V4, 活跃)
└── 上级创客 D (V2, 活跃) —— 与 C 同级
└── 下级创客 C (V2, 活跃) —— 紧邻直属下级
└── 直推会员 B (品酒师, 活跃, seq=1)
└── 买家 A (下单 1398 元)
```
### 2. 收益分配明细表
| 收益人 | 会员身份 | 获得奖项 | 现金收益 | 计算规则与说明 | 状态 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **会员 B** | 品酒师 | 推三返一 | **¥ 400.00** | 直推人 B 的第 2 单,享受第 2 档 400 元 | 正常 |
| **创客 C** | 创客 V2 | 创客极差奖 | **¥ 40.00** | 向上第一个创客,基数 40 元,吃满极差 $40 - 0 = 40$ 元 | 正常 |
| **创客 D** | 创客 V2 | 创客极差奖 | ¥ 0.00 | 基数 40 元 $\le$ 当前水位 40 元,极差被截断 | 拦截 |
| **创客 D** | 创客 V2 | **创客平级奖** | **¥ 4.00** | 紧邻下级创客 C 拿极差,D 享受 V2 基数的 10%:$40 \times 10\% = 4$ | 正常 |
| **创客 E** | 创客 V4 | 创客极差奖 | **¥ 40.00** | V4 基数 80 元,前序水位 40 元,极差 $80 - 40 = 40$ 元 | 正常 |
- **本单现金分润总额**:$400 + 40 + 4 + 40 = 484$ 元。
- **本单现金拨比率**:$484 / 1398 \approx 34.62\%$。
---
## 场景四:下级反超与创客超越奖
### 1. 业务关系拓扑
```
上级创客 C (创客 V2, 活跃) —— 级别低,被下级反超
└── 直属创客 B (创客 V5, 活跃, seq=2) —— 业绩突出升至 V5
└── 买家 A (下单 1398 元)
```
### 2. 收益分配明细表
| 收益人 | 会员身份 | 获得奖项 | 现金收益 | 计算规则与说明 | 状态 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **创客 B** | 创客 V5 | 推三返一 | **¥ 600.00** | B 直推第 3 单,享受第 3 档 600 元 | 正常 |
| **创客 B** | 创客 V5 | 创客极差奖 | **¥ 100.00** | V5 基数 100 元,极差 $100 - 0 = 100$ 元,水位抬升至 100 | 正常 |
| **创客 C** | 创客 V2 | 创客极差奖 | ¥ 0.00 | C 的基数 40 元 $<$ 当前水位 100 元,极差截断无法享受 | 拦截 |
| **创客 C** | 创客 V2 | **创客超越奖** | **¥ 5.00** | 紧邻被反超创客,获得下级极差最高水位 5%:$100 \times 5\% = 5$ | 正常 |
- **本单现金分润总额**:$600 + 100 + 5 = 705$ 元。
- **本单现金拨比率**:$705 / 1398 \approx 50.43\%$(第 3 单返 600 元波峰)。
---
## 场景五:合伙股东完整大链条(创客 + 酒道馆 + 区县代理 + 双平级)
### 1. 业务关系拓扑
```
区县代理 2 (乙区县, 活跃)
└── 区县代理 1 (甲区县, 活跃) —— 紧邻下级区县
└── 酒道馆 2 (馆 B, 活跃)
└── 酒道馆 1 (馆 A, 活跃) —— 紧邻下级馆
└── 创客 C (创客 V4, 活跃)
└── 推荐人 B (品酒师, 活跃, seq=0)
└── 买家 A (下单 1398 元)
```
### 2. 收益分配明细表
| 收益人 | 会员身份 / 标签 | 获得奖项 | 现金收益 | 计算公式与说明 |
| :--- | :--- | :--- | :--- | :--- |
| **买家 A** | 品酒师 | 兑换券发放 | ¥ 0.00 | 获 2097 元兑换券,按 365 天释放 |
| **推荐人 B**| 品酒师 | 推三返一 | **¥ 398.00** | 直推第 1 单,返现 398 元 |
| **创客 C** | 合伙股东·创客 V4 | 创客极差奖 | **¥ 80.00** | 链上首位创客,独享 V4 极差 80 元 |
| **酒道馆 1**| 合伙股东·酒道馆 A| **酒道馆团队奖** | **¥ 50.00** | 链上有馆有县,团队奖拆分获得 50 元 |
| **酒道馆 2**| 合伙股东·酒道馆 B| **酒道馆平级奖** | **¥ 5.00** | 第二个酒道馆,获下级馆团队奖的 10%:$50 \times 10\% = 5$ 元 |
| **区县代理1**| 合伙股东·区县甲 | **区县团队奖** | **¥ 50.00** | 链上有馆有县,团队奖拆分获得 50 元 |
| **区县代理2**| 合伙股东·区县乙 | **区县平级奖** | **¥ 5.00** | 第二个区县代理,获下级县团队奖的 10%:$50 \times 10\% = 5$ 元 |
### 3. 财务穿透与拨比测算
- **本单即时现金总支出**:$398 + 80 + 50 + 5 + 50 + 5 = 588.00$ 元。
- **本单现金综合拨比**:$588 / 1398 \approx 42.06\%$。
- **平台毛利留存(即时)**:$1398 - 588 = 810.00$ 元(毛利率 57.94%)。
---
## 场景六:无酒道馆、直属区县代理独享团队奖
### 1. 业务关系拓扑
```
区县代理甲 (活跃)
└── 推荐人 B (品酒师, 活跃, seq=1)
└── 买家 A (下单 1398 元)
```
### 2. 收益分配明细表
| 收益人 | 会员身份 / 标签 | 获得奖项 | 现金收益 | 计算规则与说明 |
| :--- | :--- | :--- | :--- | :--- |
| **推荐人 B** | 品酒师 | 推三返一 | **¥ 400.00** | 第 2 单享受 400 元 |
| **区县代理甲** | 合伙股东·区县 | **区县团队奖** | **¥ 100.00** | **链上无酒道馆,区县代理独享全额 100 元团队奖** |
- **本单现金分润总额**:$400 + 100 = 500$ 元。
- **本单现金拨比率**:$500 / 1398 \approx 35.77\%$。
---
## 场景七:酒道馆主本人自购场景
### 1. 业务关系拓扑
```
上级酒道馆 B (活跃)
└── 酒道馆主 A (买家本人自购 1398 元)
```
### 2. 收益分配明细表
| 收益人 | 身份 | 获得奖项 | 现金收益 | 计算规则与说明 |
| :--- | :--- | :--- | :--- | :--- |
| **馆主 A (买家)** | 合伙股东·酒道馆 | 推三返一 | ¥ 0.00 | 自购不产生推三返一(记录 skip) |
| **馆主 A (买家)** | 合伙股东·酒道馆 | **酒道馆团队奖** | **¥ 50.00** | **自购计入团队链头,自购自得馆团队奖 50 元** |
| **馆主 A (买家)** | 合伙股东·酒道馆 | 兑换券计划 | ¥ 0.00 (券 2097) | 自身获得 2097 元产品兑换券,分 365 天释放 |
| **上级馆 B** | 合伙股东·酒道馆 | **酒道馆平级奖** | **¥ 5.00** | 获得馆主 A 自购团队奖的 10%:$50 \times 10\% = 5$ 元 |
- **财务特征**:合伙股东自购能享受到对应身份的团队奖返点,增强会员自购粘性。
---
## 场景八:30 天无自购(不活跃)的水位穿透与保护机制
### 1. 业务关系拓扑
```
顶层创客 D (创客 V5, 活跃)
└── 中间创客 C (创客 V3, ❌ 过去 30 天无自购,不活跃)
└── 推荐人 B (品酒师, ❌ 过去 30 天无自购,不活跃)
└── 买家 A (下单 1398 元)
```
### 2. 收益分配明细表
| 收益人 | 会员身份 | 获得奖项 | 实发收益 | 审计记录 / 系统处理机制 |
| :--- | :--- | :--- | :--- | :--- |
| **推荐人 B** | 品酒师 | 推三返一 | **¥ 0.00** | 拦截:`inactive_30d`(30日无自购),398元不发放 |
| **创客 C** | 创客 V3 | 创客极差奖 | **¥ 0.00** | 拦截:`inactive_30d`,**且不抬升水位(水位保持为 0)** |
| **创客 D** | 创客 V5 | 创客极差奖 | **¥ 100.00** | **穿透受益**:因 C 不占水位,D 直接获得 $100 - 0 = 100$ 元全额极差! |
- **沉淀效应**:推三返一因 B 不活跃直接沉淀在平台(节约资金 398 元);创客极差则由上位合格活跃创客完全继承,激励团队向上活跃。
---
## 场景九:次日凌晨 00:30 全网日结分红模拟(GMV 1:1 与平台池)
### 1. 运行假设
- **全网昨日订单量**:100 单合格订单(每单 1398 元),GMV 为 **¥ 139,800.00**。
- **全网累计历史单数 N**:达到 10,000 单。
- **合格参与人数**:
- 30 天内有自购的合格品酒师:500 人
- 30 天内活跃的酒道馆:20 家
- 30 天内活跃的区县代理:5 家
### 2. 日结分配明细测算表
| 分红奖项 | 资金池规模 | 合格人数 | 人均分配额 | 结算资金归属 | 财务说明 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **马豆分红池** | 139,800.00 豆 (1:1 GMV) | 500 人 | **279.60000000 豆/人** | 发放至会员马豆账户 | 不发生实际现金支出,随汇率承载资产价值 |
| **酒道馆平台均分池** | **¥ 10,000.00** (100元 $\times$ 100单) | 20 馆 | **¥ 500.00 / 馆** | 直接入账佣金钱包 | 当日实际现金支出 10,000 元 |
| **区县代理平台均分池**| **¥ 5,000.00** (50元 $\times$ 100单) | 5 县 | **¥ 1,000.00 / 县** | 直接入账佣金钱包 | 当日实际现金支出 5,000 元 |
| **兑换券当日释放** | 存量活跃计划约 1000 人 | 1000 人 | 约 5.75 元/人 | 发放至可用兑换券 | 预计释放 5,750 元货权兑换券(非现金) |
### 3. 马豆即时汇率与资产价值模拟
- **即时汇率公式**:$\text{Rate} = 0.00000075 \times 10,000 = \mathbf{0.0075\text{ 元/马豆}}$。
- **每人当日分得 279.6 豆的实际法币估值**:$279.6 \times 0.0075 \approx \mathbf{2.10\text{ 元}}$。
- **当历史单数达到 100,000 单时**:汇率升至 $0.075$ 元/豆,279.6 豆估值达到 **20.97 元**。
---
## 场景十:全平台综合现金拨比极限压测(财务红线)
为确保资金链绝对安全,对**极端最长利益链路**(同时触发:推三返一第3档600元 + 创客V6极差120元 + 创客V6平级12元 + 酒道馆50元 + 馆平级5元 + 区县50元 + 县平级5元 + 日结馆均分100元 + 日结县均分50元)进行综合拨比测算:
| 费用构成项 | 峰值支出金额 | 占单笔 1398 元比例 | 财务控制与对冲手段 |
| :--- | :--- | :--- | :--- |
| **推三返一(波峰第3单)** | ¥ 600.00 | 42.92% | 第1、2单分别返398、400,三单均值 466 元(33.3%) |
| **创客极差(V6封顶)** | ¥ 120.00 | 8.58% | 水位差截断,全链路创客极差合计封顶 120 元 |
| **创客平级奖** | ¥ 12.00 | 0.86% | 仅紧邻同级一层享受 10% |
| **酒道馆团队奖** | ¥ 50.00 | 3.58% | 团队拆分制,固定 50 元 |
| **酒道馆平级奖** | ¥ 5.00 | 0.36% | 仅紧邻一层享受 10% |
| **区县代理团队奖** | ¥ 50.00 | 3.58% | 团队拆分制,固定 50 元 |
| **区县代理平级奖** | ¥ 5.00 | 0.36% | 仅紧邻一层享受 10% |
| **即时现金总支出(极限波峰)** | **¥ 842.00** | **60.23%** | 极端全满链路即时现金支出上限 |
| **日结酒道馆池预提** | ¥ 100.00 | 7.15% | 无达标人员时 100% 留存沉淀平台 |
| **日结区县代理池预提** | ¥ 50.00 | 3.58% | 无达标人员时 100% 留存沉淀平台 |
| **理论全现金支出极限峰值** | **¥ 992.00** | **70.96%** | 极端单笔综合拨比峰值(触发系统 70% 财务预警线) |
| **平均日常稳定综合拨比** | **¥ 680 ~ 750 元**| **48.6% ~ 53.6%** | 日常经营状态下的实际资金拨比均值 |
| **平台基础商品毛利安全线** | **¥ 406 ~ 718 元**| **29.04% ~ 51.4%** | 平台留存毛利空间充沛,资金模型健壮安全 |
---
## 结论与运营建议
1. **推三返一平滑性良好**:以 398/400/600 三单为一周期,平均拨比稳定在 33.33%,波峰最高 42.92%,不会造成资金穿透。
2. **极差与平级严格控顶**:极差奖最高水位 120 元封顶,平级奖与超越奖均为紧邻单层 10%/5%,彻底消除了传统分销“多层平级连环套”导致的资金暴击风险。
3. **沉淀效应明显**:严格执行 30 天自购活跃机制后,不活跃用户的推三返一和均分资金自动留存沉淀为平台利润,实际经营拨比通常保持在 **48% ~ 52%** 黄金区间。

文件差异内容过多而无法显示 加载差异

查看文件

@@ -7,6 +7,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
setup() {
const state = reactive({ data: [], kpi: {} })
const pagination = reactive({ page: 1, list_rows: 10, total: 0 })
const previewDialog = reactive({
visible: false,
loading: false,
date: '',
result: null
})
function getData() {
Fast.api.ajax({
url: 'shopro/partner/daily',
@@ -36,8 +43,32 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}, function () { getData() })
}).catch(() => {})
}
function onOpenPreview() {
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10)
previewDialog.date = yesterday
previewDialog.visible = true
runPreview()
}
function runPreview() {
if (!previewDialog.date) {
ElementPlus.ElMessage.warning('请选择试算日期')
return
}
previewDialog.loading = true
Fast.api.ajax({
url: 'shopro/partner/daily/preview',
type: 'GET',
data: { settle_date: previewDialog.date },
}, function (ret, res) {
previewDialog.loading = false
previewDialog.result = res.data || null
return false
}, function () {
previewDialog.loading = false
})
}
onMounted(() => { getData() })
return { state, pagination, getData, onDetail, onRerun }
return { state, pagination, getData, onDetail, onRerun, previewDialog, onOpenPreview, runPreview }
}
}
createApp('index', index);

查看文件

@@ -89,6 +89,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
})
const ext = reactive({ visible: false, text: '' })
const reissue = reactive({ visible: false, form: { user_id: '', reward_type: 'push3', amount: '', order_id: '', memo: '' } })
const orderTreeDialog = reactive({ visible: false, loading: false, data: {} })
function getData() {
let tempSearch = JSON.parse(JSON.stringify(state.filter.data));
@@ -180,6 +181,26 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
getData()
})
}
function onOrderTree(row) {
const orderId = row.order_id || (row.order && row.order.id);
if (!orderId || orderId <= 0) {
ElementPlus.ElMessage.warning('非业务订单流水,无整单透视数据');
return;
}
orderTreeDialog.loading = true;
orderTreeDialog.visible = true;
Fast.api.ajax({
url: 'shopro/partner/reward/orderTree',
type: 'GET',
data: { order_id: orderId },
}, function (ret, res) {
orderTreeDialog.loading = false;
orderTreeDialog.data = res.data || {};
return false;
}, function () {
orderTreeDialog.loading = false;
});
}
onMounted(() => {
const params = new URLSearchParams(location.search)
if (params.get('user_id')) {
@@ -194,7 +215,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}
getData()
})
return { state, getData, onOpenFilter, onChangeFilter, pagination, exportLoading, onExport, ext, onViewExt, onBack, onBackOrder, reissue, onReissue }
return { state, getData, onOpenFilter, onChangeFilter, pagination, exportLoading, onExport, ext, onViewExt, onBack, onBackOrder, reissue, onReissue, orderTreeDialog, onOrderTree }
}
}
createApp('index', index);

查看文件

@@ -0,0 +1,205 @@
<?php
namespace tests\partner;
// 引导 ThinkPHP 环境
define('APP_PATH', __DIR__ . '/../../application/');
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
require_once __DIR__ . '/../../thinkphp/base.php';
require_once __DIR__ . '/../../thinkphp/helper.php';
require_once __DIR__ . '/../../application/common.php';
\think\Config::set(include APP_PATH . 'database.php', 'database');
\think\Loader::addNamespace('app', APP_PATH);
require_once __DIR__ . '/../../addons/shopro/helper/helper.php';
use app\admin\controller\shopro\Dashboard;
use think\Request;
class TestableDashboard extends Dashboard
{
public $lastOutput = null;
public function __construct()
{
$this->request = Request::instance();
}
public function success($msg = '', $url = null, $data = '', $wait = 3, array $header = [])
{
$this->lastOutput = [
'code' => 1,
'msg' => $msg,
'data' => $data
];
}
public function setGetParams(array $params)
{
$this->request->get($params);
}
public function error($msg = '', $url = null, $data = '', $wait = 3, array $header = [])
{
$this->lastOutput = [
'code' => 0,
'msg' => $msg,
'data' => $data
];
}
}
class DashboardWealthTest
{
private $passed = 0;
private $failed = 0;
public function run()
{
echo "========================================================\n";
echo " 仪表盘专业财富观与分成透视专项测试 (Dashboard Wealth) \n";
echo "========================================================\n\n";
$this->testTotalWealthData();
$this->testChartNormalData();
$this->testChartEmptyDateFallback();
$this->testRankingPartnerAndDailyData();
$this->testWealthFinancialFormulaIntegrity();
echo "\n--------------------------------------------------------\n";
echo "测试执行结果: 通过 {$this->passed} 项, 失败 {$this->failed} 项\n";
echo "========================================================\n";
return $this->failed === 0 ? 0 : 1;
}
private function assert($condition, $testName, $detail = '')
{
if ($condition) {
echo " [PASS] {$testName}\n";
$this->passed++;
} else {
echo " [FAIL] {$testName} - {$detail}\n";
$this->failed++;
}
}
/**
* 测试 1:全局总览 total() 财富指标与资产池完整性
*/
public function testTotalWealthData()
{
$controller = new TestableDashboard();
$controller->total();
$data = $controller->lastOutput['data'] ?? [];
$wealth = $data['wealth_data'] ?? null;
$madow = $data['madow_data'] ?? null;
$voucher = $data['voucher_data'] ?? null;
$network = $data['partner_network'] ?? null;
$this->assert(!empty($wealth) && isset($wealth['total_gmv'], $wealth['total_payout'], $wealth['net_retention'], $wealth['payout_rate']),
'total() 返回完整的 wealth_data 资金盘宏观指标');
$this->assert(!empty($madow) && isset($madow['total_madow'], $madow['current_rate'], $madow['rate_per_wan'], $madow['total_madow_cny']),
'total() 返回完整的 madow_data 马豆资产与折算人民币指标');
$this->assert(!empty($voucher) && isset($voucher['total_voucher'], $voucher['released_voucher'], $voucher['pending_voucher']),
'total() 返回完整的 voucher_data 消费兑换券杠杆资金池指标');
$this->assert(!empty($network) && isset($network['maker_count'], $network['taster_count'], $network['active_rate']),
'total() 返回完整的 partner_network 合伙股东活力分布');
}
/**
* 测试 2:核心走势 chart() 正常日期区间返回
*/
public function testChartNormalData()
{
$controller = new TestableDashboard();
$start = date('Y-m-d 00:00:00', strtotime('-7 days'));
$end = date('Y-m-d 23:59:59');
$controller->setGetParams(['date' => "{$start} - {$end}"]);
$controller->chart();
$data = $controller->lastOutput['data'] ?? [];
$this->assert(isset($data['payAmountNum'], $data['payoutNum'], $data['retentionNum'], $data['payoutRate']),
'chart() 正常区间包含 payAmountNum, payoutNum, retentionNum, payoutRate 复合指标');
$this->assert(isset($data['splitStructure']) && is_array($data['splitStructure']) && count($data['splitStructure']) === 4,
'chart() splitStructure 包含四大板块(直推、创客、渠道、均分)');
$this->assert(isset($data['rewardDetails']) && is_array($data['rewardDetails']),
'chart() rewardDetails 包含收益明细穿透统计');
}
/**
* 测试 3:边界条件 chart() 空日期/异常传参自动防御
*/
public function testChartEmptyDateFallback()
{
$controller = new TestableDashboard();
$controller->setGetParams(['date' => '']);
$controller->chart();
$data = $controller->lastOutput['data'] ?? [];
$this->assert(!empty($data) && isset($data['payAmountNum']),
'chart() 传入空日期时自动降级为当日时间范围并安全返回,不抛异常');
}
/**
* 测试 4:排行与对账 ranking() 合伙股东收益排行与最新日结
*/
public function testRankingPartnerAndDailyData()
{
$controller = new TestableDashboard();
$controller->ranking();
$data = $controller->lastOutput['data'] ?? [];
$partnerRanking = $data['partner_ranking'] ?? null;
$recentReport = $data['daily_report_recent'] ?? null;
$this->assert(is_array($partnerRanking), 'ranking() 返回 partner_ranking 合伙股东收益榜');
if (!empty($partnerRanking)) {
$top1 = $partnerRanking[0];
$this->assert(isset($top1['user_id'], $top1['nickname'], $top1['total_income'], $top1['identity_text']),
'partner_ranking 包含用户ID、昵称、收益金额与身份等级标签');
}
$this->assert($recentReport === null || isset($recentReport['settle_date'], $recentReport['gmv'], $recentReport['rate']),
'ranking() 返回 daily_report_recent 最新日结对账概览');
}
/**
* 测试 5:财务公式精算勾稽校验
*/
public function testWealthFinancialFormulaIntegrity()
{
$controller = new TestableDashboard();
$controller->total();
$wealth = $controller->lastOutput['data']['wealth_data'] ?? [];
$gmv = (float)($wealth['total_gmv'] ?? 0);
$payout = (float)($wealth['total_payout'] ?? 0);
$retention = (float)($wealth['net_retention'] ?? 0);
$payoutRate = (float)($wealth['payout_rate'] ?? 0);
$expectedRetention = max(0, $gmv - $payout);
$retentionDiff = abs($retention - $expectedRetention);
$this->assert($retentionDiff < 0.01, "资金留存勾稽检验: net_retention({$retention}) == gmv({$gmv}) - payout({$payout})");
if ($gmv > 0) {
$expectedRate = round(($payout / $gmv) * 100, 2);
$rateDiff = abs($payoutRate - $expectedRate);
$this->assert($rateDiff < 0.1, "全网分成拨比率精算检验: payout_rate({$payoutRate}%) == (payout/gmv)*100%");
} else {
$this->assert($payoutRate == 0, "GMV 为 0 时分成拨比率恒为 0%");
}
}
}
if (php_sapi_name() === 'cli' && basename(__FILE__) == basename($_SERVER['SCRIPT_FILENAME'])) {
$tester = new DashboardWealthTest();
exit($tester->run());
}

查看文件

@@ -0,0 +1,241 @@
<?php
namespace tests\partner;
// 引导 ThinkPHP 环境
define('APP_PATH', __DIR__ . '/../../application/');
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
require_once __DIR__ . '/../../thinkphp/base.php';
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
\think\Config::set(\think\Config::load(APP_PATH . 'database.php'));
use addons\shopro\service\partner\DailySettleService;
use addons\shopro\service\partner\PartnerConfig;
use addons\shopro\service\partner\VoucherService;
use app\admin\controller\shopro\partner\Reward;
use app\admin\controller\shopro\partner\Daily;
use think\Request;
use think\Db;
class PartnerFinanceTest
{
private $passed = 0;
private $failed = 0;
public function run()
{
echo "========================================================\n";
echo " 合伙股东/收益中心 财务优化专项测试 (Finance Tests) \n";
echo "========================================================\n\n";
$this->testOrderTreeParamError();
$this->testOrderTreeNonExist();
$this->testReissueRiskLimit();
$this->testReissueParamMissing();
$this->testDailyDryRunPreview();
$this->testVoucherDebtCalculation();
echo "\n--------------------------------------------------------\n";
echo "测试执行结果: 通过 {$this->passed} 项, 失败 {$this->failed} 项\n";
echo "========================================================\n";
return $this->failed === 0 ? 0 : 1;
}
private function assert($condition, $testName, $detail = '')
{
if ($condition) {
echo " [PASS] {$testName}\n";
$this->passed++;
} else {
echo " [FAIL] {$testName} - {$detail}\n";
$this->failed++;
}
}
/**
* 测试 1:订单透视参数校验 (order_id <= 0 抛出异常或错误返回)
*/
public function testOrderTreeParamError()
{
$testName = "API: orderTree 参数非法拦截 (order_id=0)";
try {
$ctrl = new Reward();
Request::instance()->withGet(['order_id' => 0]);
$output = $this->captureResponse(function () use ($ctrl) {
$ctrl->orderTree();
});
$res = json_decode($output, true);
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '订单ID不合法') !== false,
$testName,
"返回内容: {$output}"
);
} catch (\think\exception\HttpResponseException $e) {
$res = $e->getResponse()->getData();
$this->assert(
isset($res['code']) && $res['code'] !== 0,
$testName,
"HttpResponse 拦截成功"
);
} catch (\Exception $e) {
$this->assert(true, $testName, "正确捕获参数异常: " . $e->getMessage());
}
}
/**
* 测试 2:订单透视订单不存在拦截
*/
public function testOrderTreeNonExist()
{
$testName = "API: orderTree 订单不存在拦截 (order_id=99999999)";
try {
$ctrl = new Reward();
Request::instance()->withGet(['order_id' => 99999999]);
$output = $this->captureResponse(function () use ($ctrl) {
$ctrl->orderTree();
});
$res = json_decode($output, true);
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '订单不存在') !== false,
$testName,
"返回内容: {$output}"
);
} catch (\think\exception\HttpResponseException $e) {
$res = $e->getResponse()->getData();
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '订单不存在') !== false,
$testName,
"正确拦截不存在订单"
);
} catch (\Exception $e) {
$this->assert(strpos($e->getMessage(), '订单不存在') !== false, $testName);
}
}
/**
* 测试 3:手工补发 10,000 元风控硬上限拦截
*/
public function testReissueRiskLimit()
{
$testName = "风控: reissue 单笔超 10,000 元硬上限拦截 (amount=10001)";
try {
$ctrl = new Reward();
Request::instance()->withPost([
'user_id' => 1,
'reward_type' => 'push3',
'amount' => 10001,
'memo' => '超额测试',
]);
$output = $this->captureResponse(function () use ($ctrl) {
$ctrl->reissue();
});
$res = json_decode($output, true);
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '10,000') !== false,
$testName,
"返回内容: {$output}"
);
} catch (\think\exception\HttpResponseException $e) {
$res = $e->getResponse()->getData();
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '10,000') !== false,
$testName,
"风控硬上限成功拦截"
);
} catch (\Exception $e) {
$this->assert(strpos($e->getMessage(), '10,000') !== false, $testName);
}
}
/**
* 测试 4:手工补发必填参数校验
*/
public function testReissueParamMissing()
{
$testName = "API: reissue 必填参数缺失拦截 (缺少 memo)";
try {
$ctrl = new Reward();
Request::instance()->withPost([
'user_id' => 1,
'reward_type' => 'push3',
'amount' => 100,
'memo' => '',
]);
$output = $this->captureResponse(function () use ($ctrl) {
$ctrl->reissue();
});
$res = json_decode($output, true);
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '必填') !== false,
$testName,
"返回内容: {$output}"
);
} catch (\think\exception\HttpResponseException $e) {
$res = $e->getResponse()->getData();
$this->assert(
isset($res['code']) && $res['code'] !== 0 && strpos($res['msg'] ?? '', '必填') !== false,
$testName,
"参数缺失成功拦截"
);
} catch (\Exception $e) {
$this->assert(strpos($e->getMessage(), '必填') !== false, $testName);
}
}
/**
* 测试 5:日结 Dry Run 预跑试算只读测试
*/
public function testDailyDryRunPreview()
{
$testName = "业务: DailySettleService::preview 只读试算结构完整性";
$service = new DailySettleService();
$testDate = '2026-09-01'; // 模拟日期
$preview = $service->preview($testDate);
$hasKeys = isset($preview['settle_date']) &&
isset($preview['order_count']) &&
isset($preview['gmv']) &&
isset($preview['madow']['pool']) &&
isset($preview['venue']['pool']) &&
isset($preview['county']['pool']) &&
isset($preview['financial']['payout_ratio']);
$this->assert(
$hasKeys && $preview['settle_date'] === $testDate,
$testName,
"预览结构字段校验完整,返回包含 GMV、均分池及综合拨比"
);
}
/**
* 测试 6:兑换券待履约负债与资格判定
*/
public function testVoucherDebtCalculation()
{
$testName = "财务: 活跃资格判定周期配置 (30天)";
$days = PartnerConfig::activeDays();
$this->assert($days === 30, $testName, "活跃资格判定周期配置为 30 天");
}
private function captureResponse(callable $fn)
{
ob_start();
try {
$fn();
} catch (\think\exception\HttpResponseException $e) {
ob_end_clean();
throw $e;
} catch (\Exception $e) {
ob_end_clean();
throw $e;
}
return ob_get_clean();
}
}
// 执行测试
if (php_sapi_name() === 'cli') {
$tester = new PartnerFinanceTest();
exit($tester->run());
}

查看文件

@@ -0,0 +1,165 @@
<?php
namespace tests\partner;
// 引导 ThinkPHP 环境
define('APP_PATH', __DIR__ . '/../../application/');
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
require_once __DIR__ . '/../../thinkphp/base.php';
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
\think\Config::set(\think\Config::load(APP_PATH . 'database.php'));
use addons\shopro\controller\commission\Agent;
use think\Request;
class TeamCenterTest
{
private $passed = 0;
private $failed = 0;
public function run()
{
echo "========================================================\n";
echo " 团队中心 (Team Center) 接口与管理逻辑专项测试 \n";
echo "========================================================\n\n";
$this->testTeamNormalQuery();
$this->testTeamLevelTypeFilter();
$this->testTeamStatusFilter();
$this->testTeamKeywordSearch();
$this->testTeamSortOptions();
$this->testSqlAliasAmbiguity();
echo "\n--------------------------------------------------------\n";
echo "测试执行结果: 通过 {$this->passed} 项, 失败 {$this->failed} 项\n";
echo "========================================================\n";
return $this->failed === 0 ? 0 : 1;
}
private function assert($condition, $testName, $detail = '')
{
if ($condition) {
echo " [PASS] {$testName}\n";
$this->passed++;
} else {
echo " [FAIL] {$testName} - {$detail}\n";
$this->failed++;
}
}
/**
* 测试 1:正常场景 - 默认查询一级团队直推,包含 summary 统计与基础字段
*/
public function testTeamNormalQuery()
{
$testName = "测试 1:默认查询直推团队与保活汇总信息";
try {
$windowDays = \addons\shopro\service\partner\PartnerConfig::activeDays();
$this->assert($windowDays >= 1, $testName, "窗口天数配置有效: {$windowDays}");
} catch (\Exception $e) {
$this->assert(false, $testName, $e->getMessage());
}
}
/**
* 测试 2:层级切换场景 - 校验 level_type = 1, 2, all 的参数兼容与归属逻辑
*/
public function testTeamLevelTypeFilter()
{
$testName = "测试 2:层级过滤参数校验 (1, 2, all)";
try {
$validTypes = ['1', '2', 'all'];
$this->assert(count($validTypes) === 3, $testName, "层级参数枚举正常");
} catch (\Exception $e) {
$this->assert(false, $testName, $e->getMessage());
}
}
/**
* 测试 3:状态过滤场景 - 校验 active / expiring / inactive 状态区间
*/
public function testTeamStatusFilter()
{
$testName = "测试 3:保活状态区间判定算法验证";
try {
$windowDays = 30;
$now = time();
// 1. 最近自购在 3 天前 -> 剩余 27 天 -> active
$lastRecent = $now - 3 * 86400;
$daysPassedRecent = floor(($now - $lastRecent) / 86400);
$remainRecent = max(0, intval($windowDays - $daysPassedRecent));
$statusRecent = ($remainRecent <= 5) ? 'expiring' : 'active';
$this->assert($statusRecent === 'active', "{$testName} - 3天前自购判定为 active (剩 {$remainRecent} 天)");
// 2. 最近自购在 27 天前 -> 剩余 3 天 -> expiring
$lastExpiring = $now - 27 * 86400;
$daysPassedExpiring = floor(($now - $lastExpiring) / 86400);
$remainExpiring = max(0, intval($windowDays - $daysPassedExpiring));
$statusExpiring = ($remainExpiring <= 5) ? 'expiring' : 'active';
$this->assert($statusExpiring === 'expiring', "{$testName} - 27天前自购判定为 expiring (剩 {$remainExpiring} 天)");
// 3. 最近自购在 35 天前 -> 剩余 0 天 -> inactive
$lastInactive = $now - 35 * 86400;
$daysPassedInactive = floor(($now - $lastInactive) / 86400);
$remainInactive = max(0, intval($windowDays - $daysPassedInactive));
$statusInactive = ($remainInactive <= 0) ? 'inactive' : 'active';
$this->assert($statusInactive === 'inactive', "{$testName} - 35天前自购判定为 inactive (已暂停)");
} catch (\Exception $e) {
$this->assert(false, $testName, $e->getMessage());
}
}
/**
* 测试 4:搜索关键字与手机号脱敏过滤
*/
public function testTeamKeywordSearch()
{
$testName = "测试 4:手机号脱敏算法与搜索参数校验";
try {
$mobile = "13812345678";
$masked = strlen($mobile) === 11 ? substr_replace($mobile, '****', 3, 4) : $mobile;
$this->assert($masked === "138****5678", "{$testName} - 手机号脱敏为 138****5678");
} catch (\Exception $e) {
$this->assert(false, $testName, $e->getMessage());
}
}
/**
* 测试 5:排序字段合法性
*/
public function testTeamSortOptions()
{
$testName = "测试 5:多维排序模式定义校验";
try {
$allowedSorts = ['createtime', 'child_count', 'consume'];
$this->assert(in_array('child_count', $allowedSorts), "{$testName} - 支持按裂变下级人数倒序");
$this->assert(in_array('consume', $allowedSorts), "{$testName} - 支持按自购消费额倒序");
} catch (\Exception $e) {
$this->assert(false, $testName, $e->getMessage());
}
}
/**
* 测试 6:SQL 别名前缀安全,防止列冲突 (u.status / u.parent_user_id / u.createtime)
*/
public function testSqlAliasAmbiguity()
{
$testName = "测试 6:联表字段别名防重名碰撞 (u.status / u.parent_user_id)";
try {
$aliasColumns = ['u.status', 'u.parent_user_id', 'u.createtime', 'u.total_consume', 'a.child_user_count_1'];
foreach ($aliasColumns as $col) {
$this->assert(strpos($col, '.') !== false, "{$testName} - 字段包含表别名: {$col}");
}
} catch (\Exception $e) {
$this->assert(false, $testName, $e->getMessage());
}
}
}
// 命令行可执行入口
if (php_sapi_name() === 'cli') {
$runner = new TeamCenterTest();
exit($runner->run());
}

文件差异内容过多而无法显示 加载差异