feat(dashboard): 从专业财富观角度重构升级后台收益与分成统计仪表盘
这个提交包含在:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户