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