diff --git a/addons/shopro/Shopro.php b/addons/shopro/Shopro.php index d580fce..7d26e68 100755 --- a/addons/shopro/Shopro.php +++ b/addons/shopro/Shopro.php @@ -100,7 +100,11 @@ class Shopro extends Addons if (request()->isCli()) { \think\Console::addDefaultCommands([ 'addons\shopro\console\ShoproChat', - 'addons\shopro\console\ShoproHelp' + 'addons\shopro\console\ShoproHelp', + 'addons\shopro\console\PartnerSettle', + 'addons\shopro\console\PartnerInstall', + 'addons\shopro\console\PartnerDemo', + 'addons\shopro\console\PartnerAccept', ]); } diff --git a/addons/shopro/config/menu.php b/addons/shopro/config/menu.php index 317c03c..289496a 100644 --- a/addons/shopro/config/menu.php +++ b/addons/shopro/config/menu.php @@ -1,8 +1,8 @@ - array ( @@ -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' => 'fxzx', - 'pinyin' => 'fenxiaozhongxin', + 'py' => 'hhgd', + 'pinyin' => 'hehuogudong', '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,9 +3049,9 @@ return array ( 'ismenu' => 1, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'fxs', - 'pinyin' => 'fenxiaoshang', - 'weigh' => 10, + 'py' => 'gdda', + 'pinyin' => 'gudongdangan', + 'weigh' => 100, 'sublist' => array ( 0 => @@ -3106,7 +3106,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/commission/agent/changeparentuser', - 'title' => '更换上级分销商', + 'title' => '更换上级', 'icon' => 'fa fa-circle-o', 'url' => '', 'condition' => '', @@ -3140,7 +3140,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/commission/level', - 'title' => '分销商等级', + 'title' => '身份等级', 'icon' => 'fa fa-list-ol', 'url' => '', 'condition' => '', @@ -3148,9 +3148,9 @@ return array ( 'ismenu' => 1, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'fxsdj', - 'pinyin' => 'fenxiaoshangdengji', - 'weigh' => 9, + 'py' => 'sfdj', + 'pinyin' => 'shenfendengji', + 'weigh' => 90, 'sublist' => array ( 0 => @@ -3239,7 +3239,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/commission/order', - 'title' => '分销订单', + 'title' => '业绩订单', 'icon' => 'fa fa-file-text', 'url' => '', 'condition' => '', @@ -3247,9 +3247,9 @@ return array ( 'ismenu' => 1, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'fxdd', - 'pinyin' => 'fenxiaodingdan', - 'weigh' => 7, + 'py' => 'yjdd', + 'pinyin' => 'yejidingdan', + 'weigh' => 40, 'sublist' => array ( 0 => @@ -3354,7 +3354,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/commission/log', - 'title' => '分销动态', + 'title' => '动态日志', 'icon' => 'fa fa-address-card', 'url' => '', 'condition' => '', @@ -3362,9 +3362,9 @@ return array ( 'ismenu' => 1, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'fxdt', - 'pinyin' => 'fenxiaodongtai', - 'weigh' => 6, + 'py' => 'dtrz', + 'pinyin' => 'dongtairizhi', + 'weigh' => 20, 'sublist' => array ( 0 => @@ -3389,7 +3389,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/commission/reward', - 'title' => '佣金明细', + 'title' => '历史佣金', 'icon' => 'fa fa-usd', 'url' => '', 'condition' => '', @@ -3397,9 +3397,9 @@ return array ( 'ismenu' => 1, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'yjmx', - 'pinyin' => 'yongjinmingxi', - 'weigh' => 5, + 'py' => 'lsyj', + 'pinyin' => 'lishiyongjin', + 'weigh' => 30, 'sublist' => array ( 0 => @@ -3440,7 +3440,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/commission/goods', - 'title' => '分销商品', + 'title' => '计奖商品', 'icon' => 'fa fa-briefcase', 'url' => '', 'condition' => '', @@ -3448,9 +3448,9 @@ return array ( 'ismenu' => 1, 'menutype' => 'addtabs', 'extend' => '', - 'py' => 'fxsp', - 'pinyin' => 'fenxiaoshangpin', - 'weigh' => 8, + 'py' => 'jjsp', + 'pinyin' => 'jijiangshangpin', + 'weigh' => 80, 'sublist' => array ( 0 => @@ -3489,7 +3489,7 @@ return array ( array ( 'type' => 'file', 'name' => 'shopro/commission/goods/edit', - 'title' => '设置佣金', + 'title' => '设置计奖', 'icon' => 'fa fa-circle-o', 'url' => '', 'condition' => '', @@ -3503,6 +3503,223 @@ return array ( ), ), ), + 6 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/reward', + 'title' => '收益流水', + 'icon' => 'fa fa-list-alt', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 1, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'syls', + 'pinyin' => 'shouyiliushui', + 'weigh' => 7, + 'sublist' => + array ( + 0 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/reward/index', + 'title' => '查看', + 'icon' => 'fa fa-circle-o', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 0, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'zk', + 'pinyin' => 'zhakan', + 'weigh' => 0, + ), + 1 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/reward/export', + 'title' => '导出', + 'icon' => 'fa fa-circle-o', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 0, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'dc', + 'pinyin' => 'daochu', + 'weigh' => 0, + ), + 2 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/reward/reissue', + 'title' => '手工补发', + 'icon' => 'fa fa-circle-o', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 0, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'sgbf', + 'pinyin' => 'shougongbufa', + 'weigh' => 0, + ), + 3 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/reward/back', + 'title' => '退回', + 'icon' => 'fa fa-circle-o', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 0, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'th', + 'pinyin' => 'tuihui', + 'weigh' => 0, + ), + 4 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/reward/backOrder', + 'title' => '整单退回', + 'icon' => 'fa fa-circle-o', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 0, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'zdth', + 'pinyin' => 'zhengdantuihui', + 'weigh' => 0, + ), + ), + ), + 7 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/daily', + 'title' => '日结对账', + 'icon' => 'fa fa-calendar', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 1, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'rjdiz', + 'pinyin' => 'rijieduizhang', + 'weigh' => 6, + 'sublist' => + array ( + 0 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/daily/index', + 'title' => '查看', + 'icon' => 'fa fa-circle-o', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 0, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'zk', + 'pinyin' => 'zhakan', + 'weigh' => 0, + ), + 1 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/daily/rerun', + 'title' => '重跑', + 'icon' => 'fa fa-circle-o', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 0, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'cp', + 'pinyin' => 'chongpao', + 'weigh' => 0, + ), + ), + ), + 8 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/voucher', + 'title' => '兑换券计划', + 'icon' => 'fa fa-ticket', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 1, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'dhqjh', + 'pinyin' => 'duihuanquanjihua', + 'weigh' => 5, + 'sublist' => + array ( + 0 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/voucher/index', + 'title' => '查看', + 'icon' => 'fa fa-circle-o', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 0, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'zk', + 'pinyin' => 'zhakan', + 'weigh' => 0, + ), + 1 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/voucher/close', + 'title' => '关闭', + 'icon' => 'fa fa-circle-o', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 0, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'gb', + 'pinyin' => 'guanbi', + 'weigh' => 0, + ), + 2 => + array ( + 'type' => 'file', + 'name' => 'shopro/partner/voucher/logs', + 'title' => '释放日志', + 'icon' => 'fa fa-circle-o', + 'url' => '', + 'condition' => '', + 'remark' => '', + 'ismenu' => 0, + 'menutype' => 'addtabs', + 'extend' => '', + 'py' => 'sfrz', + 'pinyin' => 'shifangrizhi', + 'weigh' => 0, + ), + ), + ), ), ), 7 => diff --git a/addons/shopro/console/PartnerAccept.php b/addons/shopro/console/PartnerAccept.php new file mode 100644 index 0000000..67ff122 --- /dev/null +++ b/addons/shopro/console/PartnerAccept.php @@ -0,0 +1,242 @@ +setName('shopro:partner:accept') + ->setDescription('跑通合伙股东验收剧本:V5极差/平级、馆县拆分、30日暂停、退款、日结'); + } + + protected function execute(Input $input, Output $output) + { + $goodsId = Db::name('shopro_commission_goods')->where('status', 1)->value('goods_id'); + if (!$goodsId) { + $output->writeln('no commission goods'); + return 1; + } + $goods = Db::name('shopro_goods')->where('id', $goodsId)->find(); + $service = new PartnerRewardService(); + $yesterday = date('Y-m-d', strtotime('-1 day')); + $yTime = strtotime($yesterday . ' 12:00:00'); + + $users = Db::transaction(function () use ($goods, $service, $yTime) { + $e = $this->ensureUser('13800000005', '验收区县E'); + $d = $this->ensureUser('13800000004', '验收酒道馆D', $e['id']); + $a = $this->ensureUser('13800000001', '测试品酒师A', $d['id']); + $b = $this->ensureUser('13800000002', '测试买家B', $a['id']); + $c = $this->ensureUser('13800000006', '验收创客C', $b['id']); + $x = $this->ensureUser('13800000007', '验收买家X', $c['id']); + + $this->ensureAgent($e['id'], 1, ['is_county' => 1, 'county_code' => '110101']); + $this->ensureAgent($d['id'], 7, ['is_venue' => 1]); // V6 + $this->ensureAgent($a['id'], 1); + $this->ensureAgent($b['id'], 6); // V5 + $this->ensureAgent($c['id'], 6); // V5 + $this->ensureAgent($x['id'], 1); + + // 活跃:各受益人先自购一笔(金额 1) + foreach ([$e, $d, $a, $b, $c] as $u) { + $o = $this->createPaidOrder($u['id'], $goods, 1); + $service->touchSelfOrder($u['id'], $o['paid_time']); + } + + // 步骤4:X 下 1398 → C 极差100,B 平级10,D 馆50,E 县50,C 推三398 + $orderX = $this->createPaidOrder($x['id'], $goods, 1398); + $service->touchSelfOrder($x['id'], $orderX['paid_time']); + $service->settleOrder($orderX['id']); + + // 步骤6:A 自购时间改到 31 天前,Y 挂在 A 下下一单 → A push3 skip + $aInactiveAt = time() - 31 * 86400; + Db::name('shopro_commission_agent')->where('user_id', $a['id'])->update([ + 'last_self_order_time' => $aInactiveAt, + ]); + $y = $this->ensureUser('13800000008', '验收暂停测Y', $a['id']); + $this->ensureAgent($y['id'], 1); + $orderY = $this->createPaidOrder($y['id'], $goods, 1398); + $service->touchSelfOrder($y['id'], $orderY['paid_time']); + $service->settleOrder($orderY['id']); + + // 步骤7:退 X 单 + $service->refund($orderX['id']); + + // 步骤8:补一笔「昨天」合格单供日结(挂在活跃的 B 下,买家 Z) + $z = $this->ensureUser('13800000009', '验收日结Z', $b['id']); + $this->ensureAgent($z['id'], 1); + // 恢复 B/D/E 活跃 + foreach ([$b, $d, $e] as $u) { + $service->touchSelfOrder($u['id'], $yTime); + } + $orderZ = $this->createPaidOrder($z['id'], $goods, 1398, $yTime); + $service->touchSelfOrder($z['id'], $yTime); + $service->settleOrder($orderZ['id']); + $yd = date('Y-m-d', $yTime); + Db::name('shopro_partner_voucher_plan')->where('order_id', $orderZ['id'])->update([ + 'start_date' => $yd, + 'released_days' => 0, + 'released_amount' => 0, + 'status' => 1, + ]); + + foreach ([$e, $d, $a, $b, $c, $x, $y, $z] as $u) { + (new AgentService($u['id']))->runAgentUpgradePlan(false); + } + + return compact('a', 'b', 'c', 'd', 'e', 'x', 'y', 'z', 'orderX', 'orderY', 'orderZ') + ['settle_date' => $yd]; + }); + + $settleDate = $users['settle_date'] ?? $yesterday; + $report1 = (new DailySettleService())->run($settleDate, true); + $report2 = (new DailySettleService())->run($settleDate, true); + + $output->writeln('=== 验收账号 ==='); + foreach (['e' => '区县E', 'd' => '馆D-V6', 'a' => '品酒师A', 'b' => '创客B-V5', 'c' => '创客C-V5', 'x' => '买家X'] as $k => $label) { + $u = $users[$k]; + $output->writeln("{$label} id={$u['id']} mobile={$u['mobile']}"); + } + + $this->dumpRewards($output, '步骤4 X单(已退)', $users['orderX']['id']); + $this->dumpRewards($output, '步骤6 Y单(A暂停)', $users['orderY']['id']); + $this->dumpRewards($output, '步骤8 昨日Z单', $users['orderZ']['id']); + + $output->writeln('=== 日结 ==='); + $output->writeln('settle1 gmv=' . ($report1['gmv'] ?? $report1->gmv ?? '') . ' madow=' . ($report1['madow_pool'] ?? $report1->madow_pool ?? '')); + $output->writeln('settle2(重跑) gmv=' . ($report2['gmv'] ?? $report2->gmv ?? '') . ' madow=' . ($report2['madow_pool'] ?? $report2->madow_pool ?? '')); + + $madowCount = Db::name('shopro_partner_reward')->where('settle_date', $settleDate)->where('reward_type', 'madow_share')->where('status', 1)->count(); + $venueShare = Db::name('shopro_partner_reward')->where('settle_date', $settleDate)->where('reward_type', 'venue_share')->where('status', 1)->sum('amount'); + $countyShare = Db::name('shopro_partner_reward')->where('settle_date', $settleDate)->where('reward_type', 'county_share')->where('status', 1)->sum('amount'); + $voucherRel = Db::name('shopro_partner_reward')->where('settle_date', $settleDate)->where('reward_type', 'voucher_release')->where('status', 1)->sum('amount'); + $output->writeln("日结发放 madow笔={$madowCount} venue_share={$venueShare} county_share={$countyShare} voucher_release={$voucherRel}"); + $output->writeln('shopro:partner:accept done'); + return 0; + } + + protected function dumpRewards(Output $output, $title, $orderId) + { + $output->writeln("=== {$title} order={$orderId} ==="); + $rows = Db::name('shopro_partner_reward')->where('order_id', $orderId)->select(); + foreach ($rows as $row) { + $ext = json_decode($row['ext'] ?: '{}', true) ?: []; + $output->writeln(sprintf( + ' %s user=%s amount=%s status=%s %s', + $row['reward_type'], + $row['user_id'], + $row['amount'], + $row['status'], + $ext['formula'] ?? ($ext['skip_reason'] ?? '') + )); + } + } + + protected function ensureUser($mobile, $nickname, $parentId = 0) + { + $user = Db::name('user')->where('mobile', $mobile)->find(); + if ($user) { + $update = ['nickname' => $nickname]; + if ($parentId) { + $update['parent_user_id'] = $parentId; + } + Db::name('user')->where('id', $user['id'])->update($update); + return Db::name('user')->where('id', $user['id'])->find(); + } + $salt = Random::alnum(); + $now = time(); + $id = Db::name('user')->insertGetId([ + 'group_id' => 1, + 'username' => $mobile, + 'nickname' => $nickname, + 'mobile' => $mobile, + 'password' => md5(md5('Demo123456') . $salt), + 'salt' => $salt, + 'status' => 'normal', + 'parent_user_id' => $parentId, + 'jointime' => $now, + 'createtime' => $now, + 'updatetime' => $now, + ]); + return Db::name('user')->where('id', $id)->find(); + } + + protected function ensureAgent($userId, $level, $extra = []) + { + $now = time(); + $data = array_merge([ + 'level' => $level, + 'status' => 'normal', + 'is_venue' => 0, + 'is_county' => 0, + 'county_code' => '', + 'updatetime' => $now, + ], $extra); + $agent = Db::name('shopro_commission_agent')->where('user_id', $userId)->find(); + if ($agent) { + Db::name('shopro_commission_agent')->where('user_id', $userId)->update($data); + return; + } + $data['user_id'] = $userId; + $data['apply_num'] = 1; + $data['become_time'] = $now; + $data['createtime'] = $now; + Db::name('shopro_commission_agent')->insert($data); + } + + protected function createPaidOrder($userId, $goods, $payFee, $paidTime = null) + { + $now = $paidTime ?: time(); + $orderId = Db::name('shopro_order')->insertGetId([ + 'type' => 'goods', + 'order_sn' => 'PA' . date('YmdHis', $now) . Random::numeric(4), + 'user_id' => $userId, + 'goods_original_amount' => $payFee, + 'goods_amount' => $payFee, + 'order_amount' => $payFee, + 'pay_fee' => $payFee, + 'remain_pay_fee' => 0, + 'status' => 'paid', + 'paid_time' => $now, + 'pay_mode' => 'online', + 'platform' => 'H5', + 'ext' => json_encode(['partner_accept' => 1], JSON_UNESCAPED_UNICODE), + 'createtime' => $now, + 'updatetime' => $now, + ]); + Db::name('shopro_order_item')->insert([ + 'order_id' => $orderId, + 'user_id' => $userId, + 'goods_id' => $goods['id'], + 'goods_type' => 'normal', + 'goods_title' => $goods['title'] ?? 'demo', + 'goods_image' => $goods['image'] ?? '', + 'goods_original_price' => $payFee, + 'goods_price' => $payFee, + 'goods_num' => 1, + 'pay_fee' => $payFee, + 'ext' => json_encode(['is_commission' => 1, 'partner_accept' => 1], JSON_UNESCAPED_UNICODE), + 'createtime' => $now, + 'updatetime' => $now, + ]); + return [ + 'id' => $orderId, + 'user_id' => $userId, + 'pay_fee' => $payFee, + 'paid_time' => $now, + 'status' => 'paid', + ]; + } +} diff --git a/addons/shopro/console/PartnerDemo.php b/addons/shopro/console/PartnerDemo.php new file mode 100644 index 0000000..3429f6d --- /dev/null +++ b/addons/shopro/console/PartnerDemo.php @@ -0,0 +1,246 @@ +setName('shopro:partner:demo') + ->addOption('amount', 'a', Option::VALUE_OPTIONAL, '买家订单实付', '1398') + ->addOption('refund', 'r', Option::VALUE_NONE, '结算后再整单退回') + ->addOption('venue', null, Option::VALUE_NONE, '为买家B开通酒道馆') + ->addOption('fix-team', null, Option::VALUE_NONE, '只重算团队人数并为B开通酒道馆,不新建订单') + ->setDescription('创建测试品酒师A/买家B并结算一单,验收推三返一'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->getOption('fix-team')) { + return $this->fixTeam($output); + } + + $amount = round((float) $input->getOption('amount'), 2); + if ($amount <= 0) { + $output->writeln('amount must > 0'); + return 1; + } + + $goodsId = Db::name('shopro_commission_goods')->where('status', 1)->value('goods_id'); + if (!$goodsId) { + $output->writeln('no commission goods, run shopro:partner:install first'); + return 1; + } + $goods = Db::name('shopro_goods')->where('id', $goodsId)->find(); + + $result = Db::transaction(function () use ($amount, $goods, $input) { + $a = $this->ensureUser('13800000001', '测试品酒师A'); + $b = $this->ensureUser('13800000002', '测试买家B', $a['id']); + $this->ensureAgent($a['id'], 1); + $this->ensureAgent($b['id'], 1); + if ($input->getOption('venue')) { + $this->openVenue($b['id']); + } + + $service = new PartnerRewardService(); + $selfOrder = $this->createPaidOrder($a['id'], $goods, 1); + $service->touchSelfOrder($a['id'], $selfOrder['paid_time']); + $service->settleOrder($selfOrder['id']); + + $buyOrder = $this->createPaidOrder($b['id'], $goods, $amount); + $service->touchSelfOrder($b['id'], $buyOrder['paid_time']); + $service->settleOrder($buyOrder['id']); + + if ($input->getOption('refund')) { + $service->refund($buyOrder['id']); + } + + return [ + 'a' => $a, + 'b' => $b, + 'self_order_id' => $selfOrder['id'], + 'buy_order_id' => $buyOrder['id'], + 'refunded' => (bool) $input->getOption('refund'), + ]; + }); + + $rewards = Db::name('shopro_partner_reward') + ->where('order_id', $result['buy_order_id']) + ->select(); + $aWallet = Db::name('user')->where('id', $result['a']['id'])->value('commission'); + $plan = Db::name('shopro_partner_voucher_plan')->where('order_id', $result['buy_order_id'])->find(); + $seq = Db::name('shopro_commission_agent')->where('user_id', $result['a']['id'])->value('direct_order_seq'); + + $output->writeln('A=' . $result['a']['id'] . ' ' . $result['a']['nickname'] . ' mobile=13800000001'); + $output->writeln('B=' . $result['b']['id'] . ' ' . $result['b']['nickname'] . ' mobile=13800000002 parent=' . $result['a']['id']); + $output->writeln('A自购单=' . $result['self_order_id'] . ' B下单=' . $result['buy_order_id']); + $output->writeln('A直推序号=' . $seq . ' A佣金钱包=' . $aWallet); + $output->writeln('B券计划=' . json_encode($plan ?: [], JSON_UNESCAPED_UNICODE)); + foreach ($rewards as $row) { + $output->writeln(sprintf( + 'reward id=%s type=%s user=%s amount=%s status=%s formula=%s', + $row['id'], + $row['reward_type'], + $row['user_id'], + $row['amount'], + $row['status'], + $this->formulaOf($row) + )); + } + $this->recountTeam($result['b']['id']); + $this->recountTeam($result['a']['id']); + $output->writeln($result['refunded'] ? 'shopro:partner:demo refunded' : 'shopro:partner:demo done'); + return 0; + } + + protected function fixTeam(Output $output) + { + $a = $this->ensureUser('13800000001', '测试品酒师A'); + $b = $this->ensureUser('13800000002', '测试买家B', $a['id']); + $this->ensureAgent($a['id'], 1); + $this->ensureAgent($b['id'], 1); + $this->openVenue($b['id']); + $this->recountTeam($b['id']); + $this->recountTeam($a['id']); + + $aAgent = Db::name('shopro_commission_agent')->where('user_id', $a['id'])->find(); + $bAgent = Db::name('shopro_commission_agent')->where('user_id', $b['id'])->find(); + $output->writeln('A=' . $a['id'] . ' users=' . intval($aAgent['child_user_count_1'] ?? 0) . '/' . intval($aAgent['child_user_count_all'] ?? 0) . ' agents=' . intval($aAgent['child_agent_count_1'] ?? 0) . '/' . intval($aAgent['child_agent_count_all'] ?? 0)); + $output->writeln('B=' . $b['id'] . ' is_venue=' . intval($bAgent['is_venue'] ?? 0) . ' level=' . intval($bAgent['level'] ?? 0)); + $output->writeln('shopro:partner:demo fix-team done'); + return 0; + } + + protected function openVenue($userId) + { + $now = time(); + $changed = Db::name('shopro_commission_agent')->where('user_id', $userId)->update([ + 'is_venue' => 1, + 'updatetime' => $now, + ]); + if ($changed) { + LogModel::add($userId, 'agent', ['type' => 'identity', 'field' => 'is_venue', 'value' => 1], null, '演示开通酒道馆'); + } + } + + protected function recountTeam($userId) + { + (new AgentService($userId))->runAgentUpgradePlan(false); + } + + protected function formulaOf($row) + { + $ext = $row['ext'] ?? ''; + if (is_string($ext)) { + $ext = json_decode($ext, true) ?: []; + } + return $ext['formula'] ?? ($ext['skip_reason'] ?? ''); + } + + protected function ensureUser($mobile, $nickname, $parentId = 0) + { + $user = Db::name('user')->where('mobile', $mobile)->find(); + if ($user) { + if ($parentId && intval($user['parent_user_id']) !== intval($parentId)) { + Db::name('user')->where('id', $user['id'])->update(['parent_user_id' => $parentId]); + $user['parent_user_id'] = $parentId; + } + return $user; + } + $salt = Random::alnum(); + $now = time(); + $id = Db::name('user')->insertGetId([ + 'group_id' => 1, + 'username' => $mobile, + 'nickname' => $nickname, + 'mobile' => $mobile, + 'password' => md5(md5('Demo123456') . $salt), + 'salt' => $salt, + 'status' => 'normal', + 'parent_user_id' => $parentId, + 'jointime' => $now, + 'createtime' => $now, + 'updatetime' => $now, + ]); + return Db::name('user')->where('id', $id)->find(); + } + + protected function ensureAgent($userId, $level) + { + $agent = Db::name('shopro_commission_agent')->where('user_id', $userId)->find(); + $now = time(); + if ($agent) { + Db::name('shopro_commission_agent')->where('user_id', $userId)->update([ + 'level' => $level, + 'status' => 'normal', + ]); + return; + } + Db::name('shopro_commission_agent')->insert([ + 'user_id' => $userId, + 'level' => $level, + 'status' => 'normal', + 'apply_num' => 1, + 'become_time' => $now, + 'createtime' => $now, + 'updatetime' => $now, + ]); + } + + protected function createPaidOrder($userId, $goods, $payFee) + { + $now = time(); + $orderId = Db::name('shopro_order')->insertGetId([ + 'type' => 'goods', + 'order_sn' => 'PD' . date('YmdHis') . Random::numeric(4), + 'user_id' => $userId, + 'goods_original_amount' => $payFee, + 'goods_amount' => $payFee, + 'order_amount' => $payFee, + 'pay_fee' => $payFee, + 'remain_pay_fee' => 0, + 'status' => 'paid', + 'paid_time' => $now, + 'pay_mode' => 'online', + 'platform' => 'H5', + 'ext' => json_encode(['partner_demo' => 1], JSON_UNESCAPED_UNICODE), + 'createtime' => $now, + 'updatetime' => $now, + ]); + Db::name('shopro_order_item')->insert([ + 'order_id' => $orderId, + 'user_id' => $userId, + 'goods_id' => $goods['id'], + 'goods_type' => 'normal', + 'goods_title' => $goods['title'] ?? 'demo', + 'goods_image' => $goods['image'] ?? '', + 'goods_original_price' => $payFee, + 'goods_price' => $payFee, + 'goods_num' => 1, + 'pay_fee' => $payFee, + 'ext' => json_encode(['is_commission' => 1, 'partner_demo' => 1], JSON_UNESCAPED_UNICODE), + 'createtime' => $now, + 'updatetime' => $now, + ]); + return [ + 'id' => $orderId, + 'user_id' => $userId, + 'pay_fee' => $payFee, + 'paid_time' => $now, + 'status' => 'paid', + ]; + } +} diff --git a/addons/shopro/console/PartnerInstall.php b/addons/shopro/console/PartnerInstall.php new file mode 100644 index 0000000..07f7062 --- /dev/null +++ b/addons/shopro/console/PartnerInstall.php @@ -0,0 +1,280 @@ +setName('shopro:partner:install') + ->setDescription('导入合伙股东表结构/配置,并升级后台菜单'); + } + + protected function execute(Input $input, Output $output) + { + $prefix = config('database.prefix') ?: 'fa_'; + $output->writeln('prefix=' . $prefix); + + $this->ensureAgentColumns($prefix, $output); + $this->runSqlFile($prefix, $output); + $this->applyBusinessConfig($output); + $this->enableCommissionGoods($output); + $this->backfillTasters($output); + $this->backfillLastSelfOrder($output); + $this->upgradeMenu($output); + $this->clearConfigCache($output); + $output->writeln('shopro:partner:install done'); + return 0; + } + + protected function ensureAgentColumns($prefix, Output $output) + { + $table = $prefix . 'shopro_commission_agent'; + $columns = [ + 'is_venue' => "tinyint(1) NOT NULL DEFAULT 0 COMMENT '酒道馆'", + 'is_county' => "tinyint(1) NOT NULL DEFAULT 0 COMMENT '区县代理'", + 'county_code' => "varchar(20) NOT NULL DEFAULT '' COMMENT '区县编码'", + 'direct_order_seq' => "int(11) NOT NULL DEFAULT 0 COMMENT '直推合格单累计'", + 'madow' => "decimal(20,8) NOT NULL DEFAULT 0.00000000 COMMENT '马豆'", + 'voucher_total' => "decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '兑换券总额'", + 'voucher_released' => "decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '兑换券已释放'", + 'voucher_used' => "decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '兑换券已使用'", + 'last_self_order_time' => "bigint(16) DEFAULT NULL COMMENT '本人最近合格订单时间'", + ]; + foreach ($columns as $name => $ddl) { + if ($this->columnExists($table, $name)) { + $output->writeln("skip column {$name}"); + continue; + } + Db::execute("ALTER TABLE `{$table}` ADD COLUMN `{$name}` {$ddl}"); + $output->writeln("add column {$name}"); + } + $indexes = [ + 'idx_venue' => 'is_venue', + 'idx_county' => 'is_county', + 'idx_last_self_order' => 'last_self_order_time', + ]; + foreach ($indexes as $idx => $col) { + if ($this->indexExists($table, $idx)) { + continue; + } + try { + Db::execute("ALTER TABLE `{$table}` ADD KEY `{$idx}` (`{$col}`)"); + } catch (\Exception $e) { + $output->writeln('index ' . $idx . ' ' . $e->getMessage()); + } + } + } + + protected function runSqlFile($prefix, Output $output) + { + $file = (defined('ADDON_PATH') ? ADDON_PATH : ROOT_PATH . 'addons/') . 'shopro/upgrade/partner_v2.sql'; + if (!is_file($file)) { + $output->writeln('sql file missing'); + return; + } + $sql = str_replace('__PREFIX__', $prefix, file_get_contents($file)); + $sql = preg_replace('/^--.*$/m', '', $sql); + $parts = array_filter(array_map('trim', explode(';', $sql))); + foreach ($parts as $part) { + if ($part === '' || stripos($part, 'ALTER TABLE') !== false && stripos($part, 'shopro_commission_agent') !== false) { + continue; + } + if (stripos($part, 'SET @') === 0) { + continue; + } + try { + Db::execute($part); + $output->writeln('ok: ' . mb_substr(preg_replace('/\s+/', ' ', $part), 0, 80)); + } catch (\Exception $e) { + $output->writeln('skip: ' . $e->getMessage()); + } + } + } + + protected function upgradeMenu(Output $output) + { + try { + $info = get_addon_info('shopro'); + $class = get_addon_class('shopro'); + $addon = new $class($info); + if (method_exists($addon, 'upgrade')) { + $addon->upgrade(); + $output->writeln('menu upgraded'); + } + } catch (\Exception $e) { + $output->writeln('menu upgrade fail: ' . $e->getMessage()); + try { + $shopro = new \ReflectionClass(\addons\shopro\Shopro::class); + $method = $shopro->getMethod('getMenu'); + $method->setAccessible(true); + $menu = $method->invoke(null); + Menu::upgrade('shopro', $menu['new']); + $output->writeln('menu upgraded via reflection'); + } catch (\Exception $ex) { + $output->writeln('menu fail: ' . $ex->getMessage()); + } + } + } + + protected function applyBusinessConfig(Output $output) + { + $force = [ + 'shop.commission.partner_mode' => '1', + 'shop.commission.partner_enable' => '1', + 'shop.commission.agent_check' => '0', + ]; + foreach ($force as $code => $value) { + $row = Db::name('shopro_config')->where('code', $code)->find(); + if ($row) { + Db::name('shopro_config')->where('code', $code)->update(['value' => $value]); + } else { + Db::name('shopro_config')->insert([ + 'code' => $code, + 'parent_code' => 'shop.commission', + 'name' => $code, + 'type' => 'boolean', + 'value' => $value, + 'weigh' => 80, + ]); + } + $output->writeln("config {$code}={$value}"); + } + + $become = Db::name('shopro_config')->where('code', 'shop.commission.become_agent')->find(); + if ($become) { + Db::name('shopro_config')->where('code', 'shop.commission.become_agent')->update([ + 'value' => json_encode(['type' => 'user', 'value' => ''], JSON_UNESCAPED_UNICODE), + ]); + $output->writeln('config become_agent=user'); + } + + $form = Db::name('shopro_config')->where('code', 'shop.commission.agent_form')->find(); + if ($form) { + $val = json_decode($form['value'], true); + if (!is_array($val)) { + $val = []; + } + $val['status'] = '0'; + Db::name('shopro_config')->where('code', 'shop.commission.agent_form')->update([ + 'value' => json_encode($val, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + ]); + $output->writeln('config agent_form.status=0'); + } + + $level = Db::name('shopro_config')->where('code', 'shop.commission.level')->find(); + if ($level && intval($level['value']) < 1) { + Db::name('shopro_config')->where('code', 'shop.commission.level')->update(['value' => '1']); + $output->writeln('config level=1'); + } + } + + protected function enableCommissionGoods(Output $output) + { + $goodsIds = Db::name('shopro_goods')->where('status', 'up')->column('id'); + $count = 0; + foreach ($goodsIds as $goodsId) { + $row = Db::name('shopro_commission_goods')->where('goods_id', $goodsId)->find(); + if ($row) { + if (intval($row['status']) !== 1) { + Db::name('shopro_commission_goods')->where('goods_id', $goodsId)->update(['status' => 1]); + $count++; + } + continue; + } + Db::name('shopro_commission_goods')->insert([ + 'goods_id' => $goodsId, + 'self_rules' => 0, + 'commission_rules' => '{}', + 'status' => 1, + 'commission_config' => null, + 'commission_order_status' => 1, + ]); + $count++; + } + $output->writeln("commission goods enabled: {$count}/" . count($goodsIds)); + } + + protected function backfillTasters(Output $output) + { + $userIds = Db::name('user')->column('id'); + $now = time(); + $count = 0; + foreach ($userIds as $userId) { + $exists = Db::name('shopro_commission_agent')->where('user_id', $userId)->find(); + if ($exists) { + continue; + } + Db::name('shopro_commission_agent')->insert([ + 'user_id' => $userId, + 'level' => 1, + 'status' => 'normal', + 'apply_num' => 1, + 'become_time' => $now, + 'createtime' => $now, + 'updatetime' => $now, + ]); + $count++; + } + $output->writeln("backfill tasters: {$count}"); + } + + protected function backfillLastSelfOrder(Output $output) + { + $rows = Db::name('shopro_order') + ->where('status', 'in', ['paid', 'completed']) + ->where('paid_time', '>', 0) + ->field('user_id, max(paid_time) as last_paid') + ->group('user_id') + ->select(); + $count = 0; + foreach ($rows as $row) { + $agent = Db::name('shopro_commission_agent')->where('user_id', $row['user_id'])->find(); + if (!$agent || !empty($agent['last_self_order_time'])) { + continue; + } + Db::name('shopro_commission_agent')->where('user_id', $row['user_id'])->update([ + 'last_self_order_time' => intval($row['last_paid']), + ]); + $count++; + } + $output->writeln("backfill last_self_order_time: {$count}"); + } + + protected function clearConfigCache(Output $output) + { + $codes = [ + 'shop.commission', + 'shop.commission.partner_mode', + 'shop.commission.partner_enable', + 'shop.commission.become_agent', + 'shop.commission.agent_form', + 'shop.commission.agent_check', + 'shop.commission.level', + ]; + foreach ($codes as $code) { + cache('config:' . $code, null); + } + $output->writeln('config cache cleared'); + } + + protected function columnExists($table, $column) + { + $database = config('database.database'); + $row = Db::query("SELECT COUNT(*) AS c FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?", [$database, $table, $column]); + return intval($row[0]['c'] ?? 0) > 0; + } + + protected function indexExists($table, $index) + { + $database = config('database.database'); + $row = Db::query("SELECT COUNT(*) AS c FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME = ?", [$database, $table, $index]); + return intval($row[0]['c'] ?? 0) > 0; + } +} diff --git a/addons/shopro/console/PartnerSettle.php b/addons/shopro/console/PartnerSettle.php new file mode 100644 index 0000000..411287e --- /dev/null +++ b/addons/shopro/console/PartnerSettle.php @@ -0,0 +1,35 @@ +setName('shopro:partner:settle') + ->addOption('date', 'd', Option::VALUE_OPTIONAL, '结算日期 Y-m-d,默认昨天') + ->addOption('rerun', 'r', Option::VALUE_NONE, '删除当天日结后重算') + ->setDescription('合伙股东日结(马豆/馆县均分/兑换券释放)'); + } + + protected function execute(Input $input, Output $output) + { + $date = $input->getOption('date'); + $rerun = (bool) $input->getOption('rerun'); + $output->writeln('partner settle start ' . ($date ?: 'yesterday') . ($rerun ? ' rerun' : '')); + try { + $report = (new DailySettleService())->run($date, $rerun); + $output->writeln('partner settle done: ' . json_encode($report ? $report->toArray() : [], JSON_UNESCAPED_UNICODE)); + return 0; + } catch (\Exception $e) { + $output->writeln('partner settle fail: ' . $e->getMessage()); + return 1; + } + } +} diff --git a/addons/shopro/controller/commission/Agent.php b/addons/shopro/controller/commission/Agent.php index 5def645..973a7cc 100644 --- a/addons/shopro/controller/commission/Agent.php +++ b/addons/shopro/controller/commission/Agent.php @@ -50,7 +50,7 @@ class Agent extends Commission $this->error('抱歉!您的账户已被冻结,如有疑问请联系客服', $condition, 107); break; } - $data = $this->service->agent; + $data = \addons\shopro\service\partner\IdentityService::attachAgent($this->service->agent); $this->success('分销商信息', $data); } @@ -134,6 +134,12 @@ class Agent extends Commission }]) ->paginate($this->request->param('list_rows', 8)); + $data->each(function ($user) { + if ($user->agent) { + $user->setRelation('agent', \addons\shopro\service\partner\IdentityService::attachAgent($user->agent)); + } + }); + $this->success("", $data); } diff --git a/addons/shopro/controller/commission/Order.php b/addons/shopro/controller/commission/Order.php index 032ffd4..3a80bdc 100644 --- a/addons/shopro/controller/commission/Order.php +++ b/addons/shopro/controller/commission/Order.php @@ -4,6 +4,8 @@ namespace addons\shopro\controller\commission; use think\Request; use app\admin\model\shopro\commission\Order as OrderModel; +use app\admin\model\shopro\partner\Reward as PartnerReward; +use addons\shopro\service\partner\PartnerConfig; class Order extends Commission { @@ -39,6 +41,38 @@ class Order extends Commission $query->$type(); } $data = $query->paginate($this->request->param('list_rows', 10)); + $this->attachPartnerAmount($data, $agentId); $this->success("", $data); } + + protected function attachPartnerAmount($data, $agentId) + { + $orderIds = []; + foreach ($data as $row) { + if (!empty($row->order_id)) { + $orderIds[] = intval($row->order_id); + } + } + $sums = []; + if ($orderIds) { + try { + $rows = PartnerReward::where('user_id', $agentId) + ->whereIn('order_id', $orderIds) + ->where('status', 1) + ->whereIn('reward_type', PartnerConfig::instantTypes()) + ->field('order_id, sum(amount) as partner_amount') + ->group('order_id') + ->select(); + foreach ($rows as $row) { + $sums[intval($row['order_id'])] = $row['partner_amount']; + } + } catch (\Exception $e) { + $sums = []; + } + } + foreach ($data as $row) { + $amount = $sums[intval($row->order_id)] ?? 0; + $row->partner_amount = $amount > 0 ? number_format($amount, 2, '.', '') : ''; + } + } } diff --git a/addons/shopro/controller/partner/Index.php b/addons/shopro/controller/partner/Index.php new file mode 100644 index 0000000..b2af48e --- /dev/null +++ b/addons/shopro/controller/partner/Index.php @@ -0,0 +1,144 @@ +service->user->id; + $monthStart = strtotime(date('Y-m-01 00:00:00')); + $monthIncome = RewardModel::where('user_id', $userId) + ->where('status', 1) + ->where('createtime', '>=', $monthStart) + ->whereIn('reward_type', array_merge(PartnerConfig::instantTypes(), ['venue_share', 'county_share'])) + ->sum('amount'); + $skipCount = RewardModel::where('user_id', $userId) + ->where('status', -1) + ->where('createtime', '>=', $monthStart) + ->count(); + $agent = IdentityService::attachAgent($this->service->agent); + + $this->success('ok', [ + 'month_income' => IdentityService::formatMoney($monthIncome), + 'skip_count' => intval($skipCount), + 'active_30d' => $agent['active_30d'] ?? true, + ]); + } + + public function rewards() + { + $userId = $this->service->user->id; + $tab = $this->request->param('tab', 'all'); + $query = RewardModel::where('user_id', $userId)->with(['buyer', 'order']); + + $tabMap = [ + 'push3' => ['push3'], + 'maker' => ['maker_diff', 'maker_peer', 'maker_overtaken'], + 'venue' => ['team_venue', 'venue_peer', 'venue_share'], + 'county' => ['team_county', 'county_peer', 'county_share'], + 'share' => ['venue_share', 'county_share', 'madow_share'], + ]; + if ($tab !== 'all' && isset($tabMap[$tab])) { + $query->whereIn('reward_type', $tabMap[$tab]); + } + + $list = $query->order('id', 'desc')->paginate($this->request->param('list_rows', 10)); + $rows = []; + foreach ($list as $item) { + $reason = $item->skip_reason; + $rows[] = [ + 'id' => intval($item->id), + 'reward_type' => $item->reward_type, + 'reward_type_text' => $item->reward_type_text, + 'amount' => IdentityService::formatMoney($item->amount), + 'madow' => IdentityService::formatMadow($item->madow), + 'status' => intval($item->status), + 'status_text' => $item->status_text, + 'skip_reason' => $reason, + 'skip_reason_text' => $reason ? PartnerConfig::skipReasonText($reason, true) : '', + 'buyer_nickname' => $item->buyer ? ($item->buyer->nickname ?? '') : '', + 'order_id' => intval($item->order_id) > 0 ? intval($item->order_id) : 0, + 'createtime' => $item->createtime, + 'formula' => $item->formula, + ]; + } + + $this->success('ok', [ + 'total' => $list->total(), + 'per_page' => $list->listRows(), + 'current_page' => $list->currentPage(), + 'data' => $rows, + ]); + } + + public function madow() + { + $userId = $this->service->user->id; + $agent = IdentityService::attachAgent($this->service->agent); + $logs = RewardModel::where('user_id', $userId) + ->where('reward_type', 'madow_share') + ->where('status', 1) + ->order('id', 'desc') + ->paginate($this->request->param('list_rows', 10)); + + $list = []; + foreach ($logs as $item) { + $list[] = [ + 'id' => $item->id, + 'settle_date' => $item->settle_date, + 'madow' => IdentityService::formatMadow($item->madow), + 'gmv' => $item->ext['gmv'] ?? '', + 'share_user_count' => $item->ext['share_user_count'] ?? 0, + 'createtime' => $item->createtime, + ]; + } + + $this->success('ok', [ + 'madow' => $agent['madow'] ?? '0.00000000', + 'madow_rate' => $agent['madow_rate'] ?? IdentityService::formatRate(null), + 'madow_value' => $agent['madow_value'] ?? '0.00', + 'list' => $list, + ]); + } + + public function voucher() + { + $userId = $this->service->user->id; + $agent = IdentityService::attachAgent($this->service->agent); + $plans = VoucherPlan::where('user_id', $userId)->order('id', 'desc')->select(); + $rows = []; + foreach ($plans as $plan) { + $rows[] = [ + 'id' => $plan->id, + 'order_id' => $plan->order_id, + 'total_amount' => IdentityService::formatMoney($plan->total_amount), + 'daily_amount' => IdentityService::formatMoney($plan->daily_amount), + 'released_amount' => IdentityService::formatMoney($plan->released_amount), + 'released_days' => intval($plan->released_days), + 'total_days' => intval($plan->total_days), + 'start_date' => $plan->start_date, + 'status' => intval($plan->status), + 'status_text' => $plan->status_text, + ]; + } + + $this->success('ok', [ + 'voucher_total' => $agent['voucher_total'] ?? '0.00', + 'voucher_released' => $agent['voucher_released'] ?? '0.00', + 'voucher_used' => $agent['voucher_used'] ?? '0.00', + 'voucher_available' => $agent['voucher_available'] ?? '0.00', + 'plans' => $rows, + ]); + } +} diff --git a/addons/shopro/controller/user/User.php b/addons/shopro/controller/user/User.php index b3960b7..ab299fc 100644 --- a/addons/shopro/controller/user/User.php +++ b/addons/shopro/controller/user/User.php @@ -82,7 +82,11 @@ class User extends Common $user->hidden(['password', 'salt', 'createtime', 'updatetime', 'deletetime', 'remember_token', 'login_fail', 'login_ip', 'login_time']); - $this->success('个人详情', $user); + $agent = \addons\shopro\service\partner\IdentityService::findAgent($user->id); + $partner = \addons\shopro\service\partner\IdentityService::attachProfile($agent); + $data = array_merge($user->toArray(), $partner); + + $this->success('个人详情', $data); } /** diff --git a/addons/shopro/filter/commission/AgentFilter.php b/addons/shopro/filter/commission/AgentFilter.php new file mode 100644 index 0000000..444b4df --- /dev/null +++ b/addons/shopro/filter/commission/AgentFilter.php @@ -0,0 +1,68 @@ +getTable(); + return $query->whereExists(function ($query) use ($current_name, $user) { + $table_name = (new User())->getQuery()->getTable(); + $query->table($table_name)->where($current_name . '.user_id=' . $table_name . '.id'); + foreach ($user as $field => $value) { + $query = $this->builderFilter($query, $field, $value); + } + return $query; + }); + } + + public function identity($query, $data) + { + $value = $this->plainValue($data); + if ($value === 'taster') { + return $query->where('level', 1); + } + if ($value === 'maker') { + return $query->where('level', '>=', 2); + } + if ($value === 'venue') { + return $query->where('is_venue', 1); + } + if ($value === 'county') { + return $query->where('is_county', 1); + } + return $query; + } + + public function active30d($query, $data) + { + $value = $this->plainValue($data); + if ($value === '' || $value === null) { + return $query; + } + $days = PartnerConfig::activeDays(); + $since = time() - $days * 86400; + if ($value === '1' || $value === 1 || $value === 'yes') { + return $query->where('last_self_order_time', '>=', $since); + } + return $query->where(function (Query $q) use ($since) { + $q->whereNull('last_self_order_time')->whereOr('last_self_order_time', '<', $since); + }); + } + + protected function plainValue($data) + { + if (is_array($data)) { + return $data[0] ?? ''; + } + return $data; + } +} diff --git a/addons/shopro/filter/partner/RewardFilter.php b/addons/shopro/filter/partner/RewardFilter.php new file mode 100644 index 0000000..b898fb9 --- /dev/null +++ b/addons/shopro/filter/partner/RewardFilter.php @@ -0,0 +1,52 @@ +getTable(); + return $query->whereExists(function ($query) use ($current_name, $user) { + $table_name = (new User())->getQuery()->getTable(); + $query->table($table_name)->where($current_name . '.buyer_id=' . $table_name . '.id'); + foreach ($user as $field => $value) { + $query = $this->builderFilter($query, $field, $value); + } + return $query; + }); + } + + public function user($query, $user) + { + $current_name = $query->getTable(); + return $query->whereExists(function ($query) use ($current_name, $user) { + $table_name = (new User())->getQuery()->getTable(); + $query->table($table_name)->where($current_name . '.user_id=' . $table_name . '.id'); + foreach ($user as $field => $value) { + $query = $this->builderFilter($query, $field, $value); + } + return $query; + }); + } + + public function order($query, $order) + { + $current_name = $query->getTable(); + return $query->whereExists(function ($query) use ($current_name, $order) { + $table_name = (new Order())->getQuery()->getTable(); + $query->table($table_name)->where($current_name . '.order_id=' . $table_name . '.id'); + foreach ($order as $field => $value) { + $query = $this->builderFilter($query, $field, $value); + } + return $query; + }); + } +} diff --git a/addons/shopro/hooks.php b/addons/shopro/hooks.php index e852274..2d6da9c 100755 --- a/addons/shopro/hooks.php +++ b/addons/shopro/hooks.php @@ -119,16 +119,19 @@ $commissionHooks = [ 'addons\\shopro\\listener\\Commission' ], 'order_paid_after' => [ // 订单支付成功 - 'addons\\shopro\\listener\\Commission' + 'addons\\shopro\\listener\\Commission', + 'addons\\shopro\\listener\\Partner' ], 'order_offline_paid_after' => [ // 货到付款支付成功 - 'addons\\shopro\\listener\\Commission' + 'addons\\shopro\\listener\\Commission', + 'addons\\shopro\\listener\\Partner' ], 'order_confirm_finish' => [ // 订单确认收货后 'addons\\shopro\\listener\\Commission' ], 'order_item_refund_after' => [ // 订单商品退款后 - 'addons\\shopro\\listener\\Commission' + 'addons\\shopro\\listener\\Commission', + 'addons\\shopro\\listener\\Partner' ], 'order_finish' => [ // 订单完成事件 'addons\\shopro\\listener\\Commission' diff --git a/addons/shopro/job/PartnerSettle.php b/addons/shopro/job/PartnerSettle.php new file mode 100644 index 0000000..4996f9f --- /dev/null +++ b/addons/shopro/job/PartnerSettle.php @@ -0,0 +1,30 @@ +settleOrder($orderId); + }); + } + $job->delete(); + } catch (HttpResponseException $e) { + $data = $e->getResponse()->getData(); + $message = $data ? ($data['msg'] ?? '') : $e->getMessage(); + format_log_error($e, 'PartnerSettle.HttpResponseException', $message); + } catch (\Exception $e) { + format_log_error($e, 'PartnerSettle'); + } + } +} diff --git a/addons/shopro/listener/Partner.php b/addons/shopro/listener/Partner.php new file mode 100644 index 0000000..d48a342 --- /dev/null +++ b/addons/shopro/listener/Partner.php @@ -0,0 +1,66 @@ +find(); + if ($orderModel) { + try { + (new VoucherService())->consumeByOrder($orderModel); + } catch (\Exception $e) { + // 表未升级时不阻断支付 + } + } + if (!PartnerConfig::isEnabled()) { + return; + } + + $service = new PartnerRewardService(); + if ($service->isQualified($order)) { + $userId = $order['user_id'] ?? ($params['user']['id'] ?? 0); + if ($userId) { + $agent = new AgentService($userId); + if (method_exists($agent, 'touchLastSelfOrderTime')) { + $agent->touchLastSelfOrderTime(); + } else { + $service->touchSelfOrder($userId); + } + } + Queue::push('\addons\shopro\job\PartnerSettle@order', [ + 'order_id' => $order['id'], + ], 'shopro-high'); + } + } + + public function orderOfflinePaidAfter($params) + { + $this->orderPaidAfter($params); + } + + public function orderItemRefundAfter($params) + { + $order = $params['order']; + if (!$order || empty($order['id'])) { + return; + } + try { + (new PartnerRewardService())->refund($order['id']); + } catch (\Exception $e) { + } + } +} diff --git a/addons/shopro/service/commission/Agent.php b/addons/shopro/service/commission/Agent.php index 9b9c8fd..469e642 100755 --- a/addons/shopro/service/commission/Agent.php +++ b/addons/shopro/service/commission/Agent.php @@ -134,6 +134,18 @@ class Agent return $this->parentUserId; } + /** + * 支付后更新本人最近合格订单时间 + */ + public function touchLastSelfOrderTime($time = null) + { + if (empty($this->agent)) { + return false; + } + $this->agent->last_self_order_time = $time ?: time(); + return $this->agent->save(); + } + /** * 创建分销商 */ diff --git a/addons/shopro/service/commission/Config.php b/addons/shopro/service/commission/Config.php index c12dec6..aa65840 100755 --- a/addons/shopro/service/commission/Config.php +++ b/addons/shopro/service/commission/Config.php @@ -23,7 +23,9 @@ class Config public function getInviteLockEvent() { $becomeAgentEvent = $this->getBecomeAgentEvent(); - if($becomeAgentEvent === 'user') { + $becomeType = is_array($becomeAgentEvent) ? ($becomeAgentEvent['type'] ?? '') : $becomeAgentEvent; + // 合伙股东模式下 become=user 仍尊重 invite_lock,避免注册即绑丢掉分享关系 + if ($becomeType === 'user' && empty($this->config['partner_mode'])) { return 'agent'; } return $this->config['invite_lock']; diff --git a/addons/shopro/service/commission/Order.php b/addons/shopro/service/commission/Order.php index 36b7d23..34bf86c 100755 --- a/addons/shopro/service/commission/Order.php +++ b/addons/shopro/service/commission/Order.php @@ -171,6 +171,9 @@ class Order */ public function runCommissionPlan($commissionOrder, $currentAgent = null, $currentCommissionLevel = 1) { + if (sheep_config('shop.commission.partner_mode')) { + return true; + } // 超出分佣层级 if ($this->commissionLevel < $currentCommissionLevel) { return true; diff --git a/addons/shopro/service/order/OrderCreate.php b/addons/shopro/service/order/OrderCreate.php index a52ae76..0c29197 100755 --- a/addons/shopro/service/order/OrderCreate.php +++ b/addons/shopro/service/order/OrderCreate.php @@ -63,6 +63,11 @@ class OrderCreate */ protected $coupon_id = 0; + /** + * 用户选择的兑换券抵扣金额 + */ + protected $voucher_fee = 0; + /** * 用户备注 */ @@ -102,6 +107,8 @@ class OrderCreate 'goods_old_amount' => '0', // 商品不参与活动时的总价 'goods_amount' => '0', // 商品总价 'coupon_discount_fee' => '0', // 优惠券优惠金额 + 'voucher_fee' => '0', // 兑换券抵扣 + 'voucher_available' => '0', // 当前可用兑换券 'promo_discount_fee' => 0, // 当前促销优惠总金额 (包含满包邮的邮费) 'total_discount_fee' => 0, // 当前订单,总优惠金额(优惠券 + 活动优惠) 'coupon' => null, // 所使用的优惠券 @@ -151,6 +158,7 @@ class OrderCreate $this->invoice_id = $params['invoice_id'] ?? 0; $this->coupon_id = $params['coupon_id'] ?? 0; + $this->voucher_fee = isset($params['voucher_fee']) ? floatval($params['voucher_fee']) : 0; $this->remark = $params['remark'] ?? ''; $this->money = (isset($params['money']) && $params['money'] > 0) ? $params['money'] : 0; @@ -593,6 +601,29 @@ class OrderCreate } } + /** + * 兑换券抵扣:先商城券,再兑换券;不超过可用额和应付 + */ + public function calcVoucher() + { + $available = 0; + if ($this->user && $this->user->id && $this->order_type !== 'score') { + $available = (new \addons\shopro\service\partner\VoucherService())->available($this->user->id); + } + $this->orderData['voucher_available'] = $available; + $want = $this->order_type === 'score' ? 0 : max(0, round((float) $this->voucher_fee, 2)); + $apply = min($want, (float) $available, (float) $this->orderData['pay_fee']); + $apply = $apply > 0 ? $apply : 0; + $this->orderData['voucher_fee'] = $apply; + if ($apply > 0) { + $this->orderData['pay_fee'] = bcsub((string) $this->orderData['pay_fee'], (string) $apply, 2); + $this->orderData['total_discount_fee'] = bcadd((string) $this->orderData['total_discount_fee'], (string) $apply, 2); + if ($this->orderData['pay_fee'] < 0) { + $this->orderData['pay_fee'] = 0; + } + } + } + /** * 获取用户的优惠券列表,可用和不可用的做区分 @@ -969,6 +1000,8 @@ class OrderCreate 'temp_remain_pay_fee' => $temp_remain_pay_fee >= 0 ? $temp_remain_pay_fee : 0, // 临时剩余应支付金额,订单确认页面显示 'total_discount_fee' => $this->orderData['total_discount_fee'], 'coupon_discount_fee' => $this->orderData['coupon_discount_fee'], + 'voucher_fee' => $this->orderData['voucher_fee'], + 'voucher_available' => $this->orderData['voucher_available'], 'promo_discount_fee' => $this->orderData['promo_discount_fee'], // 包含包邮的运费优惠 'money' => $this->money, // 余额支付部分 "invoice_amount" => $this->orderData[$this->invoiceConfig['amount_type']] // 可开票金额 @@ -1042,6 +1075,7 @@ class OrderCreate $this->orderData['total_discount_fee'] = bcadd($this->orderData['coupon_discount_fee'], $this->orderData['promo_discount_fee'], 2); $this->orderData['pay_fee'] = bcsub($this->orderData['order_amount'], $this->orderData['total_discount_fee'], 2); $this->orderData['pay_fee'] = $this->orderData['pay_fee'] < 0 ? 0 : $this->orderData['pay_fee']; + $this->calcVoucher(); return $this->calcReturn(); } @@ -1074,6 +1108,12 @@ class OrderCreate $order = Order::with('items')->where('id', $order->id)->find(); $pay_money = floatval($result['money']); + if ($order->status == Order::STATUS_UNPAID && bccomp((string) $order->pay_fee, '0', 2) === 0) { + $order = Db::transaction(function () use ($order) { + $payOper = new PayOper(); + return $payOper->checkAndPaid($order, 'order'); + }); + } if ($order->status == Order::STATUS_UNPAID && $pay_money > 0) { // 如果订单未支付,并且使用了余额混合支付 (支付失败,不影响订单状态) $order = Db::transaction(function () use ($order, $pay_money) { @@ -1129,6 +1169,7 @@ class OrderCreate } $ext['offline_status'] = $result['offline_status']; $ext['need_address'] = $result['need_address']; // 后台判断是否有 编辑收货地址按钮 + $ext['voucher_fee'] = $result['voucher_fee'] ?? 0; // 发票开具状态 $invoice_status = 0; // 没有申请 diff --git a/addons/shopro/service/partner/ActiveService.php b/addons/shopro/service/partner/ActiveService.php new file mode 100644 index 0000000..991f370 --- /dev/null +++ b/addons/shopro/service/partner/ActiveService.php @@ -0,0 +1,65 @@ +last_self_order_time ?? null); + $userId = is_array($agent) ? ($agent['user_id'] ?? 0) : ($agent->user_id ?? 0); + $at = $atTime ?: time(); + $start = $this->windowStart($at); + + if ($last) { + return intval($last) >= $start; + } + if (!$userId) { + return false; + } + return $this->hasQualifiedOrder($userId, $start, $at); + } + + public function hasQualifiedOrder($userId, $start, $end) + { + $order = OrderModel::where('user_id', $userId) + ->where('type', '<>', 'score') + ->where('pay_fee', '>', 0) + ->paid() + ->where('paid_time', '>=', $start) + ->where('paid_time', '<=', $end) + ->find(); + return (bool) $order; + } + + public function activeUserIds($start, $end, $extraWhere = []) + { + $query = \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); + }); + foreach ($extraWhere as $field => $value) { + $query->where($field, $value); + } + return $query->column('user_id'); + } +} diff --git a/addons/shopro/service/partner/ChainService.php b/addons/shopro/service/partner/ChainService.php new file mode 100644 index 0000000..6db3fa3 --- /dev/null +++ b/addons/shopro/service/partner/ChainService.php @@ -0,0 +1,74 @@ +node($buyerId); + if ($self) { + $nodes[] = $self; + $visited[$buyerId] = true; + } + } + + $currentId = $buyerId; + for ($i = 0; $i < $max; $i++) { + $agent = new AgentService($currentId); + $parentId = $agent->getParentUserId(); + if (!$parentId || isset($visited[$parentId])) { + break; + } + $node = $this->node($parentId); + if (!$node) { + break; + } + $nodes[] = $node; + $visited[$parentId] = true; + $currentId = $parentId; + } + + return $nodes; + } + + public function node($userId) + { + if (!$userId) { + return null; + } + $agent = AgentModel::where('user_id', $userId) + ->where('status', 'in', ['normal', 'freeze']) + ->find(); + if (!$agent) { + return null; + } + return [ + 'user_id' => intval($agent->user_id), + 'level' => intval($agent->level), + 'is_venue' => intval($agent->is_venue), + 'is_county' => intval($agent->is_county), + 'county_code' => (string) $agent->county_code, + 'last_self_order_time' => $agent->last_self_order_time ? intval($agent->last_self_order_time) : 0, + 'direct_order_seq' => intval($agent->direct_order_seq), + 'status' => $agent->status, + 'agent' => $agent, + ]; + } +} diff --git a/addons/shopro/service/partner/DailySettleService.php b/addons/shopro/service/partner/DailySettleService.php new file mode 100644 index 0000000..09e0470 --- /dev/null +++ b/addons/shopro/service/partner/DailySettleService.php @@ -0,0 +1,201 @@ +where('status', 1)->find(); + if ($exist && !$rerun) { + return $exist; + } + if ($rerun) { + $this->clearDaily($settleDate); + } + + Db::transaction(function () use ($settleDate) { + $this->settle($settleDate); + }); + + return DailyReport::where('settle_date', $settleDate)->find(); + } catch (\Exception $e) { + DailyReport::create([ + 'settle_date' => $settleDate, + 'status' => 0, + 'error_msg' => mb_substr($e->getMessage(), 0, 500), + 'createtime' => time(), + 'updatetime' => time(), + ]); + throw $e; + } finally { + flock($fp, LOCK_UN); + fclose($fp); + } + } + + protected function settle($settleDate) + { + $start = strtotime($settleDate . ' 00:00:00'); + $end = strtotime($settleDate . ' 23:59:59'); + $rateService = new RateService(); + $active = new ActiveService(); + + $orders = $rateService->qualifiedOrderQuery($start, $end)->field('o.id,o.pay_fee')->select(); + $orderCount = count($orders); + $gmv = 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]); + + $madow = (new MadowService())->share($settleDate, $gmv, $tasterIds); + + $venuePer = (float) PartnerConfig::get('partner_venue_share_per_order', 100); + $countyPer = (float) PartnerConfig::get('partner_county_share_per_order', 50); + $venuePool = round($orderCount * $venuePer, 2); + $countyPool = round($orderCount * $countyPer, 2); + + $this->cashShare($settleDate, $venueIds, $venuePool, 'venue_share', 'venue', '酒道馆'); + $this->cashShare($settleDate, $countyIds, $countyPool, 'county_share', 'county', '区县'); + + $voucherTotal = (new VoucherService())->releaseByDate($settleDate); + + $rateLog = $rateService->snapshot($settleDate, $gmv, [ + 'madow_pool' => $madow['pool'], + 'share_user_count' => $madow['count'], + 'venue_pool' => $venuePool, + 'county_pool' => $countyPool, + ]); + + DailyReport::create([ + 'settle_date' => $settleDate, + 'order_count' => $orderCount, + 'order_count_n' => $rateLog->order_count_n ?? $rateService->countN(), + 'gmv' => $gmv, + 'rate' => $rateLog->rate ?? 0, + 'madow_pool' => $madow['pool'], + 'share_user_count' => $madow['count'], + 'venue_pool' => $venuePool, + 'venue_user_count' => count($venueIds), + 'county_pool' => $countyPool, + 'county_user_count' => count($countyIds), + 'voucher_release_total' => $voucherTotal, + 'status' => 1, + 'error_msg' => '', + 'createtime' => time(), + 'updatetime' => time(), + ]); + } + + protected function cashShare($settleDate, $userIds, $pool, $type, $role, $label) + { + $count = count($userIds); + $orderId = PartnerRewardService::dailyOrderId($settleDate); + if ($pool <= 0) { + return; + } + if ($count <= 0) { + PartnerRewardService::write([ + 'order_id' => $orderId, + 'user_id' => 0, + 'role' => 'system', + 'reward_type' => $type, + 'amount' => $pool, + 'base_amount' => $pool, + 'settle_date' => $settleDate, + 'ext' => ['formula' => "{$label}分母为0,奖池 {$pool} 进平台"], + 'status' => 1, + ]); + return; + } + $each = function_exists('bcdiv') ? bcdiv((string) $pool, (string) $count, 2) : number_format($pool / $count, 2, '.', ''); + $distributed = 0; + foreach ($userIds as $userId) { + PartnerRewardService::write([ + 'order_id' => $orderId, + 'user_id' => $userId, + 'role' => $role, + 'reward_type' => $type, + 'amount' => $each, + 'base_amount' => $pool, + 'settle_date' => $settleDate, + 'ext' => ['formula' => "{$label}奖池 {$pool} / {$count} = {$each}", 'share_user_count' => $count], + 'status' => 1, + ]); + if ((float) $each > 0) { + Wallet::change($userId, 'commission', $each, $type, [ + 'settle_date' => $settleDate, + ]); + AgentModel::where('user_id', $userId)->setInc('total_income', $each); + } + $distributed = bcadd((string) $distributed, (string) $each, 2); + } + $tail = bcsub((string) $pool, (string) $distributed, 2); + if ((float) $tail > 0) { + PartnerRewardService::write([ + 'order_id' => $orderId, + 'user_id' => 0, + 'role' => 'system', + 'reward_type' => $type, + 'amount' => $tail, + 'base_amount' => $pool, + 'settle_date' => $settleDate, + 'ext' => ['formula' => "{$label}尾差 {$tail} 进平台"], + 'status' => 1, + ]); + } + } + + public function clearDaily($settleDate) + { + (new VoucherService())->rollbackRelease($settleDate); + (new MadowService())->rollback($settleDate); + + $cashRows = RewardModel::where('settle_date', $settleDate) + ->whereIn('reward_type', ['venue_share', 'county_share']) + ->where('status', 1) + ->where('user_id', '>', 0) + ->select(); + foreach ($cashRows as $row) { + if ((float) $row->amount > 0) { + Wallet::change($row->user_id, 'commission', -$row->amount, 'reward_back', [ + 'settle_date' => $settleDate, + 'rerun' => 1, + ], '日结重跑扣回'); + AgentModel::where('user_id', $row->user_id)->setDec('total_income', $row->amount); + } + } + + RewardModel::where('settle_date', $settleDate) + ->whereIn('reward_type', PartnerConfig::dailyTypes()) + ->delete(); + DailyReport::where('settle_date', $settleDate)->delete(); + \app\admin\model\shopro\partner\RateLog::where('settle_date', $settleDate)->delete(); + } +} diff --git a/addons/shopro/service/partner/IdentityService.php b/addons/shopro/service/partner/IdentityService.php new file mode 100644 index 0000000..045c113 --- /dev/null +++ b/addons/shopro/service/partner/IdentityService.php @@ -0,0 +1,171 @@ + [], + 'identity_text' => '', + 'is_partner' => false, + 'maker_level_name' => '', + ]; + if (!$agent) { + return $empty; + } + $level = intval(is_array($agent) ? ($agent['level'] ?? 0) : ($agent->level ?? 0)); + $isVenue = intval(is_array($agent) ? ($agent['is_venue'] ?? 0) : ($agent->is_venue ?? 0)); + $isCounty = intval(is_array($agent) ? ($agent['is_county'] ?? 0) : ($agent->is_county ?? 0)); + + $identities = []; + $parts = []; + $makerLevelName = ''; + + if ($level == 1) { + $identities[] = 'taster'; + } elseif ($level >= 2) { + $identities[] = 'maker'; + $makerLevelName = '创客V' . ($level - 1); + } + if ($isVenue) { + $identities[] = 'venue'; + } + if ($isCounty) { + $identities[] = 'county'; + } + + $isPartner = in_array('maker', $identities) || in_array('venue', $identities) || in_array('county', $identities); + + if ($isPartner) { + $parts[] = '合伙股东'; + if ($makerLevelName) { + $parts[] = $makerLevelName; + } + if ($isVenue) { + $parts[] = '酒道馆'; + } + if ($isCounty) { + $parts[] = '区县代理'; + } + } elseif ($level == 1) { + $parts[] = '品酒师'; + } + + return [ + 'identities' => $identities, + 'identity_text' => implode(' · ', $parts), + 'is_partner' => $isPartner, + 'maker_level_name' => $makerLevelName, + ]; + } + + public static function formatMoney($value, $scale = 2) + { + return number_format((float) $value, $scale, '.', ''); + } + + public static function formatMadow($value) + { + return number_format((float) $value, 8, '.', ''); + } + + public static function formatRate($value) + { + if ($value === null || $value === '') { + $value = PartnerConfig::get('partner_madow_base_rate', '0.00000075'); + } + return number_format((float) $value, 8, '.', ''); + } + + public static function attachAgent($agent, $rate = null) + { + if (!$agent) { + return null; + } + $identity = self::build($agent); + $madow = is_array($agent) ? ($agent['madow'] ?? 0) : ($agent->madow ?? 0); + $released = is_array($agent) ? ($agent['voucher_released'] ?? 0) : ($agent->voucher_released ?? 0); + $used = is_array($agent) ? ($agent['voucher_used'] ?? 0) : ($agent->voucher_used ?? 0); + $total = is_array($agent) ? ($agent['voucher_total'] ?? 0) : ($agent->voucher_total ?? 0); + $last = is_array($agent) ? ($agent['last_self_order_time'] ?? 0) : ($agent->last_self_order_time ?? 0); + if ($rate === null) { + $rate = (new RateService())->currentRate(); + } + $available = max(0, (float) $released - (float) $used); + + $data = is_object($agent) ? $agent->toArray() : $agent; + return array_merge($data, $identity, [ + 'is_venue' => intval($data['is_venue'] ?? 0), + 'is_county' => intval($data['is_county'] ?? 0), + 'county_code' => (string) ($data['county_code'] ?? ''), + 'direct_order_seq' => intval($data['direct_order_seq'] ?? 0), + 'madow' => self::formatMadow($madow), + 'madow_rate' => self::formatRate($rate), + 'madow_value' => self::formatMoney((float) $madow * (float) $rate), + 'voucher_total' => self::formatMoney($total), + 'voucher_released' => self::formatMoney($released), + 'voucher_used' => self::formatMoney($used), + 'voucher_available' => self::formatMoney($available), + 'active_30d' => (new ActiveService())->isActive($agent), + 'last_self_order_time' => $last ? intval($last) : 0, + ]); + } + + public static function attachProfile($agent, $rate = null) + { + if (!$agent) { + $baseRate = PartnerConfig::get('partner_madow_base_rate', '0.00000075'); + return [ + 'identity_text' => '', + 'identities' => [], + 'is_partner' => false, + 'maker_level_name' => '', + 'madow' => '0.00000000', + 'madow_rate' => self::formatRate($baseRate), + 'madow_value' => '0.00', + 'voucher_available' => '0.00', + 'active_30d' => true, + 'agent_form' => self::agentFormEnabled(), + 'partner_mode' => PartnerConfig::isPartnerMode(), + ]; + } + $full = self::attachAgent($agent, $rate); + return [ + 'identity_text' => $full['identity_text'], + 'identities' => $full['identities'], + 'is_partner' => $full['is_partner'], + 'maker_level_name' => $full['maker_level_name'], + 'madow' => $full['madow'], + 'madow_rate' => $full['madow_rate'], + 'madow_value' => $full['madow_value'], + 'voucher_available' => $full['voucher_available'], + 'active_30d' => $full['active_30d'], + 'agent_form' => self::agentFormEnabled(), + 'partner_mode' => PartnerConfig::isPartnerMode(), + ]; + } + + protected static function agentFormEnabled() + { + $form = sheep_config('shop.commission.agent_form'); + if (is_array($form)) { + return !empty($form['status']) && $form['status'] !== '0'; + } + return false; + } + + public static function findAgent($userId) + { + if (!$userId) { + return null; + } + return AgentModel::where('user_id', $userId)->find(); + } +} diff --git a/addons/shopro/service/partner/MadowService.php b/addons/shopro/service/partner/MadowService.php new file mode 100644 index 0000000..3ac7ce8 --- /dev/null +++ b/addons/shopro/service/partner/MadowService.php @@ -0,0 +1,90 @@ + 0, 'count' => $count, 'each' => 0]; + } + if ($count <= 0) { + PartnerRewardService::write([ + 'order_id' => PartnerRewardService::dailyOrderId($settleDate), + 'buyer_id' => 0, + 'user_id' => 0, + 'role' => 'system', + 'reward_type' => 'madow_share', + 'amount' => 0, + 'madow' => $pool, + 'base_amount' => $pool, + 'settle_date' => $settleDate, + 'ext' => ['formula' => "分母为0,马豆池 {$pool} 进平台", 'skip_reason' => ''], + 'status' => 1, + ]); + return ['pool' => $pool, 'count' => 0, 'each' => 0]; + } + + $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); + } + $tail = function_exists('bcsub') ? bcsub((string) $pool, (string) $distributed, 8) : round($pool - $distributed, 8); + if ((float) $tail > 0) { + PartnerRewardService::write([ + 'order_id' => PartnerRewardService::dailyOrderId($settleDate), + 'buyer_id' => 0, + 'user_id' => 0, + 'role' => 'system', + 'reward_type' => 'madow_share', + 'amount' => 0, + 'madow' => $tail, + 'base_amount' => $pool, + 'settle_date' => $settleDate, + 'ext' => ['formula' => "尾差 {$tail} 进平台"], + 'status' => 1, + ]); + } + + return ['pool' => $pool, 'count' => $count, 'each' => $each]; + } + + public function rollback($settleDate) + { + $rows = \app\admin\model\shopro\partner\Reward::where('settle_date', $settleDate) + ->where('reward_type', 'madow_share') + ->where('status', 1) + ->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(); + } + } + } +} diff --git a/addons/shopro/service/partner/MakerDiffReward.php b/addons/shopro/service/partner/MakerDiffReward.php new file mode 100644 index 0000000..0515589 --- /dev/null +++ b/addons/shopro/service/partner/MakerDiffReward.php @@ -0,0 +1,48 @@ +isActive($node['agent'] ?? $node)) { + PartnerRewardService::writeSkip($order, $buyerId, $node['user_id'], 'maker_diff', $role, 'inactive_30d', '创客 30 日无自购,不占水位'); + continue; + } + if ($cap <= $seen) { + PartnerRewardService::writeSkip($order, $buyerId, $node['user_id'], 'maker_diff', $role, 'cap_not_higher', "水位 {$seen},等级基数 {$cap}"); + continue; + } + $pay = round($cap - $seen, 2); + PartnerRewardService::account($order, $buyerId, $node['user_id'], 'maker_diff', $role, $pay, $cap, [ + 'formula' => "极差 {$cap} - {$seen} = {$pay}", + 'seen_before' => $seen, + 'cap' => $cap, + 'level' => $node['level'], + ]); + $seen = $cap; + $paid[] = [ + 'user_id' => $node['user_id'], + 'level' => $node['level'], + 'cap' => $cap, + 'amount' => $pay, + ]; + } + + return ['seen' => $seen, 'paid' => $paid]; + } +} diff --git a/addons/shopro/service/partner/MakerPeerReward.php b/addons/shopro/service/partner/MakerPeerReward.php new file mode 100644 index 0000000..43bdf8d --- /dev/null +++ b/addons/shopro/service/partner/MakerPeerReward.php @@ -0,0 +1,82 @@ + $node) { + $indexByUser[$node['user_id']] = $i; + } + + foreach ($paid as $item) { + $from = $indexByUser[$item['user_id']] ?? -1; + if ($from < 0) { + continue; + } + $peer = $this->findUp($chain, $from + 1, function ($node) use ($item) { + return intval($node['level']) === intval($item['level']); + }); + if (!$peer) { + continue; + } + $levelBonus = (float) ($bonus[$item['level']] ?? 0); + $amount = round($levelBonus * $peerRate, 2); + if (!$active->isActive($peer['agent'] ?? $peer)) { + PartnerRewardService::writeSkip($order, $buyerId, $peer['user_id'], 'maker_peer', 'maker', 'inactive_30d', '平级创客 30 日无自购'); + continue; + } + if ($amount <= 0) { + continue; + } + PartnerRewardService::account($order, $buyerId, $peer['user_id'], 'maker_peer', 'maker', $amount, $levelBonus, [ + 'formula' => "平级 {$levelBonus} × " . ($peerRate * 100) . "% = {$amount}", + 'from_user_id' => $item['user_id'], + 'level' => $item['level'], + ]); + } + + if (empty($paid)) { + return; + } + $highest = $paid[count($paid) - 1]; + $from = $indexByUser[$highest['user_id']] ?? -1; + $lower = $this->findUp($chain, $from + 1, function ($node) use ($highest) { + return intval($node['level']) >= 2 && intval($node['level']) < intval($highest['level']); + }); + if (!$lower) { + return; + } + $amount = round((float) $highest['cap'] * $overRate, 2); + if (!$active->isActive($lower['agent'] ?? $lower)) { + PartnerRewardService::writeSkip($order, $buyerId, $lower['user_id'], 'maker_overtaken', 'maker', 'inactive_30d', '被超越创客 30 日无自购'); + return; + } + if ($amount <= 0) { + return; + } + PartnerRewardService::account($order, $buyerId, $lower['user_id'], 'maker_overtaken', 'maker', $amount, $highest['cap'], [ + 'formula' => "超越奖 最高水位 {$highest['cap']} × " . ($overRate * 100) . "% = {$amount}", + 'from_user_id' => $highest['user_id'], + ]); + } + + protected function findUp($chain, $start, $matcher) + { + for ($i = $start; $i < count($chain); $i++) { + if ($matcher($chain[$i])) { + return $chain[$i]; + } + } + return null; + } +} diff --git a/addons/shopro/service/partner/PartnerConfig.php b/addons/shopro/service/partner/PartnerConfig.php new file mode 100644 index 0000000..6977569 --- /dev/null +++ b/addons/shopro/service/partner/PartnerConfig.php @@ -0,0 +1,145 @@ + 0 ? $days : 30; + } + + public static function chainMaxDepth() + { + $depth = intval(self::get('partner_chain_max_depth', 50)); + return $depth > 0 ? $depth : 50; + } + + public static function push3Amounts() + { + $amounts = self::get('partner_push3_amounts', [398, 400, 600]); + if (!is_array($amounts) || count($amounts) < 3) { + return [398, 400, 600]; + } + return array_map('floatval', array_values($amounts)); + } + + public static function makerBonus() + { + $bonus = self::get('partner_maker_bonus', [2 => 20, 3 => 40, 4 => 60, 5 => 80, 6 => 100, 7 => 120]); + if (!is_array($bonus)) { + return [2 => 20, 3 => 40, 4 => 60, 5 => 80, 6 => 100, 7 => 120]; + } + $map = []; + foreach ($bonus as $level => $amount) { + $map[intval($level)] = floatval($amount); + } + return $map; + } + + public static function rewardTypeText($type) + { + $map = self::rewardTypeMap(); + return $map[$type] ?? $type; + } + + public static function rewardTypeMap() + { + return [ + 'push3' => '推三返一', + 'maker_diff' => '创客极差奖', + 'maker_peer' => '创客平级奖', + 'maker_overtaken' => '创客超越奖', + 'team_venue' => '酒道馆团队奖', + 'team_county' => '区县团队奖', + 'venue_peer' => '酒道馆平级奖', + 'county_peer' => '区县平级奖', + 'venue_share' => '酒道馆平台均分', + 'county_share' => '区县平台均分', + 'madow_share' => '每日分红马豆', + 'voucher_grant' => '兑换券发放', + 'voucher_release' => '兑换券释放', + ]; + } + + public static function skipReasonText($code, $forUser = false) + { + $inner = [ + 'no_parent' => '无直推上级', + 'self_buy' => '自购', + 'inactive_30d' => '受益人 30 日无自购', + 'not_maker' => '不是创客', + 'not_venue' => '不是酒道馆', + 'not_county' => '不是区县代理', + 'cap_not_higher' => '等级未超过水位', + 'already_settled' => '幂等跳过', + 'partner_goods_off' => '商品未参与', + 'partner_off' => '结算未开启', + ]; + $user = [ + 'no_parent' => '暂无推荐人,不产生推三返一', + 'self_buy' => '自己下单不产生推三返一', + 'inactive_30d' => '需 30 日内有本人订单才能领取', + 'not_maker' => '', + 'not_venue' => '', + 'not_county' => '', + 'cap_not_higher' => '平级不发极差,走平级奖', + 'already_settled' => '', + 'partner_goods_off' => '', + 'partner_off' => '', + ]; + if ($forUser) { + return $user[$code] ?? ''; + } + return $inner[$code] ?? $code; + } + + public static function statusText($status) + { + $map = [ + 0 => '待入账', + 1 => '已入账', + -1 => '已取消', + -2 => '已退回', + ]; + return $map[intval($status)] ?? ''; + } + + public static function instantTypes() + { + return [ + 'push3', 'maker_diff', 'maker_peer', 'maker_overtaken', + 'team_venue', 'team_county', 'venue_peer', 'county_peer', + ]; + } + + public static function dailyTypes() + { + return ['venue_share', 'county_share', 'madow_share', 'voucher_release']; + } +} diff --git a/addons/shopro/service/partner/PartnerRewardService.php b/addons/shopro/service/partner/PartnerRewardService.php new file mode 100644 index 0000000..42a8ff2 --- /dev/null +++ b/addons/shopro/service/partner/PartnerRewardService.php @@ -0,0 +1,220 @@ +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); + + 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) { + (new VoucherService())->closeByOrder($orderId); + return; + } + + 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); + } + + public function isQualified($order) + { + if (!$order || ($order['type'] ?? '') === 'score') { + return false; + } + if ((float) ($order['pay_fee'] ?? 0) <= 0) { + return false; + } + $status = $order['status'] ?? ''; + if (!in_array($status, [OrderModel::STATUS_PAID, OrderModel::STATUS_COMPLETED])) { + return false; + } + $items = $order['items'] ?? []; + if (empty($items) && !empty($order['id'])) { + $fresh = OrderModel::with(['items'])->where('id', $order['id'])->find(); + if ($fresh) { + $order = $fresh; + $items = $fresh['items'] ?? []; + } + } + foreach ($items as $item) { + if (isset($item['ext']['is_commission']) && !$item['ext']['is_commission']) { + continue; + } + $goods = CommissionGoods::where('goods_id', $item['goods_id']) + ->where('status', CommissionGoods::GOODS_COMMISSION_STATUS_ON) + ->find(); + if ($goods) { + return true; + } + } + return false; + } + + public function touchSelfOrder($userId, $time = null) + { + $agent = AgentModel::where('user_id', $userId)->find(); + if ($agent) { + $agent->last_self_order_time = $time ?: time(); + $agent->save(); + } + } + + public static function dailyOrderId($settleDate) + { + return -intval(date('Ymd', strtotime($settleDate))); + } + + public static $lastWriteCreated = false; + + public static function write($data) + { + self::$lastWriteCreated = false; + $now = time(); + $payload = array_merge([ + 'order_id' => 0, + 'buyer_id' => 0, + 'user_id' => 0, + 'role' => '', + 'reward_type' => '', + 'amount' => 0, + 'madow' => 0, + 'base_amount' => 0, + 'rate' => 0, + 'ext' => [], + 'settle_date' => null, + 'status' => 0, + 'createtime' => $now, + 'updatetime' => $now, + ], $data); + + try { + $row = RewardModel::create($payload); + self::$lastWriteCreated = true; + return $row; + } catch (\Exception $e) { + $msg = $e->getMessage(); + if (stripos($msg, 'Duplicate') !== false || stripos($msg, '1062') !== false) { + return RewardModel::where('order_id', $payload['order_id']) + ->where('user_id', $payload['user_id']) + ->where('reward_type', $payload['reward_type']) + ->find(); + } + throw $e; + } + } + + public static function writeSkip($order, $buyerId, $userId, $type, $role, $reason, $formula = '') + { + $orderId = is_array($order) || $order instanceof \think\Model ? ($order['id'] ?? 0) : intval($order); + return self::write([ + 'order_id' => $orderId, + 'buyer_id' => $buyerId, + 'user_id' => $userId, + 'role' => $role, + 'reward_type' => $type, + 'amount' => 0, + 'base_amount' => 0, + 'ext' => [ + 'skip_reason' => $reason, + 'formula' => $formula ?: PartnerConfig::skipReasonText($reason), + ], + 'status' => -1, + ]); + } + + public static function account($order, $buyerId, $userId, $type, $role, $amount, $base, $ext = []) + { + $orderId = is_array($order) || $order instanceof \think\Model ? ($order['id'] ?? 0) : intval($order); + $row = self::write([ + 'order_id' => $orderId, + 'buyer_id' => $buyerId, + 'user_id' => $userId, + 'role' => $role, + 'reward_type' => $type, + 'amount' => $amount, + 'base_amount' => $base, + 'ext' => $ext, + 'status' => 1, + ]); + if ($row && self::$lastWriteCreated && (float) $amount > 0 && $userId > 0 && $row->status == 1 && in_array($type, PartnerConfig::instantTypes())) { + Wallet::change($userId, 'commission', $amount, $type, [ + 'partner_reward_id' => $row->id, + 'order_id' => $orderId, + ]); + AgentModel::where('user_id', $userId)->setInc('total_income', $amount); + } + return $row; + } +} diff --git a/addons/shopro/service/partner/PeerShareReward.php b/addons/shopro/service/partner/PeerShareReward.php new file mode 100644 index 0000000..a2deafa --- /dev/null +++ b/addons/shopro/service/partner/PeerShareReward.php @@ -0,0 +1,50 @@ +peer($order, $buyerId, $chain, $teamResult['venue'], 'is_venue', 'venue_peer', 'venue', $venueRate, $active, '酒道馆'); + } + if (!empty($teamResult['county'])) { + $this->peer($order, $buyerId, $chain, $teamResult['county'], 'is_county', 'county_peer', 'county', $countyRate, $active, '区县'); + } + } + + protected function peer($order, $buyerId, $chain, $from, $flag, $type, $role, $rate, ActiveService $active, $label) + { + $start = intval($from['index']) + 1; + $peer = null; + for ($i = $start; $i < count($chain); $i++) { + if (intval($chain[$i][$flag] ?? 0)) { + $peer = $chain[$i]; + break; + } + } + if (!$peer) { + return; + } + $base = (float) $from['amount']; + $amount = round($base * $rate, 2); + if (!$active->isActive($peer['agent'] ?? $peer)) { + PartnerRewardService::writeSkip($order, $buyerId, $peer['user_id'], $type, $role, 'inactive_30d', "{$label}平级 30 日无自购"); + return; + } + if ($amount <= 0) { + return; + } + PartnerRewardService::account($order, $buyerId, $peer['user_id'], $type, $role, $amount, $base, [ + 'formula' => "{$label}平级 {$base} × " . ($rate * 100) . "% = {$amount}", + 'from_user_id' => $from['user_id'], + ]); + } +} diff --git a/addons/shopro/service/partner/Push3Reward.php b/addons/shopro/service/partner/Push3Reward.php new file mode 100644 index 0000000..5365d72 --- /dev/null +++ b/addons/shopro/service/partner/Push3Reward.php @@ -0,0 +1,56 @@ +isActive($parent['agent'] ?? $parent)) { + PartnerRewardService::writeSkip($order, $buyerId, $parent['user_id'], 'push3', 'taster', 'inactive_30d', '直推上级 30 日无自购'); + return; + } + + $agent = AgentModel::where('user_id', $parent['user_id'])->lock(true)->find(); + if (!$agent) { + return; + } + $agent->direct_order_seq = intval($agent->direct_order_seq) + 1; + $agent->save(); + + $amounts = PartnerConfig::push3Amounts(); + $seq = intval($agent->direct_order_seq); + $amount = $amounts[($seq - 1) % 3]; + + PartnerRewardService::account($order, $buyerId, $agent->user_id, 'push3', 'taster', $amount, $amount, [ + 'formula' => "直推第{$seq}单,循环第" . ((($seq - 1) % 3) + 1) . "档 +{$amount}", + 'seq' => $seq, + ]); + } + + public function rollbackSeq($userId) + { + $agent = AgentModel::where('user_id', $userId)->lock(true)->find(); + if ($agent && $agent->direct_order_seq > 0) { + $agent->direct_order_seq = $agent->direct_order_seq - 1; + $agent->save(); + } + } +} diff --git a/addons/shopro/service/partner/RateService.php b/addons/shopro/service/partner/RateService.php new file mode 100644 index 0000000..c25343e --- /dev/null +++ b/addons/shopro/service/partner/RateService.php @@ -0,0 +1,81 @@ +find(); + if ($latest && (float) $latest->rate > 0) { + return (float) $latest->rate; + } + return $this->calcRate($this->countN()); + } + + 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); + } + + public function countN() + { + return $this->qualifiedOrderQuery()->count(); + } + + public function snapshot($settleDate, $gmv = 0, $extra = []) + { + $n = $this->countN(); + $rate = $this->calcRate($n); + $row = RateLog::where('settle_date', $settleDate)->find(); + $data = array_merge([ + 'settle_date' => $settleDate, + 'order_count_n' => $n, + 'rate' => $rate, + 'gmv' => $gmv, + 'createtime' => time(), + ], $extra); + if ($row) { + $row->save($data); + return $row; + } + return RateLog::create($data); + } + + public function qualifiedOrderQuery($start = null, $end = null) + { + $goodsTable = (new CommissionGoods())->getQuery()->getTable(); + $orderTable = (new OrderModel())->getQuery()->getTable(); + $itemTable = \think\Db::name('shopro_order_item')->getTable(); + + $query = OrderModel::alias('o') + ->where('o.type', '<>', 'score') + ->where('o.pay_fee', '>', 0) + ->whereIn('o.status', [OrderModel::STATUS_PAID, OrderModel::STATUS_COMPLETED]) + ->whereExists(function ($q) use ($orderTable, $itemTable, $goodsTable) { + $q->table($itemTable)->alias('oi') + ->join($goodsTable . ' cg', 'cg.goods_id = oi.goods_id') + ->whereRaw('oi.order_id = o.id') + ->where('cg.status', CommissionGoods::GOODS_COMMISSION_STATUS_ON); + }); + + if ($start !== null) { + $query->where('o.paid_time', '>=', $start); + } + if ($end !== null) { + $query->where('o.paid_time', '<=', $end); + } + return $query; + } +} diff --git a/addons/shopro/service/partner/TeamSplitReward.php b/addons/shopro/service/partner/TeamSplitReward.php new file mode 100644 index 0000000..4808daa --- /dev/null +++ b/addons/shopro/service/partner/TeamSplitReward.php @@ -0,0 +1,88 @@ + null, 'county' => null]; + + $venue = $this->nearest($chain, 'is_venue', $active); + $county = $this->nearestCounty($chain, $venue, $active); + + if (!$venue && !$county) { + PartnerRewardService::writeSkip($order, $buyerId, 0, 'team_venue', 'system', 'not_venue', '链上无酒道馆且无区县'); + return $result; + } + + if ($venue) { + if ($venue['inactive']) { + PartnerRewardService::writeSkip($order, $buyerId, $venue['user_id'], 'team_venue', 'venue', 'inactive_30d', '酒道馆 30 日无自购'); + } else { + PartnerRewardService::account($order, $buyerId, $venue['user_id'], 'team_venue', 'venue', $venueSplit, $venueSplit, [ + 'formula' => "酒道馆团队拆分 {$venueSplit}", + ]); + $result['venue'] = ['user_id' => $venue['user_id'], 'amount' => $venueSplit, 'index' => $venue['index']]; + } + } + + if ($county) { + $amount = $venue ? $venueSplit : $countyFull; + if ($county['inactive']) { + PartnerRewardService::writeSkip($order, $buyerId, $county['user_id'], 'team_county', 'county', 'inactive_30d', '区县 30 日无自购'); + } else { + PartnerRewardService::account($order, $buyerId, $county['user_id'], 'team_county', 'county', $amount, $amount, [ + 'formula' => $venue ? "区县团队拆分 {$amount}(馆上)" : "无馆区县全额 {$amount}", + ]); + $result['county'] = ['user_id' => $county['user_id'], 'amount' => $amount, 'index' => $county['index']]; + } + } elseif ($venue && !$county) { + PartnerRewardService::writeSkip($order, $buyerId, 0, 'team_county', 'system', 'not_county', '有馆无区县'); + } + + return $result; + } + + protected function nearest($chain, $flag, ActiveService $active) + { + foreach ($chain as $i => $node) { + if (!intval($node[$flag] ?? 0)) { + continue; + } + return [ + 'user_id' => $node['user_id'], + 'index' => $i, + 'inactive' => !$active->isActive($node['agent'] ?? $node), + 'node' => $node, + ]; + } + return null; + } + + protected function nearestCounty($chain, $venue, ActiveService $active) + { + $start = $venue ? ($venue['index'] + 1) : 0; + for ($i = $start; $i < count($chain); $i++) { + if (!intval($chain[$i]['is_county'] ?? 0)) { + continue; + } + return [ + 'user_id' => $chain[$i]['user_id'], + 'index' => $i, + 'inactive' => !$active->isActive($chain[$i]['agent'] ?? $chain[$i]), + 'node' => $chain[$i], + ]; + } + // 无馆时从链头找区县(含自购区县) + if (!$venue) { + return $this->nearest($chain, 'is_county', $active); + } + return null; + } +} diff --git a/addons/shopro/service/partner/VoucherService.php b/addons/shopro/service/partner/VoucherService.php new file mode 100644 index 0000000..60af37c --- /dev/null +++ b/addons/shopro/service/partner/VoucherService.php @@ -0,0 +1,281 @@ +find(); + if (!$agent) { + return 0; + } + return max(0, round((float) $agent->voucher_released - (float) $agent->voucher_used, 2)); + } + + public function consume($userId, $amount, $orderId = 0) + { + $amount = round((float) $amount, 2); + if ($amount <= 0) { + return true; + } + $agent = AgentModel::where('user_id', $userId)->lock(true)->find(); + if (!$agent) { + error_stop('兑换券账户不存在'); + } + $available = max(0, round((float) $agent->voucher_released - (float) $agent->voucher_used, 2)); + if (bccomp((string) $available, (string) $amount, 2) < 0) { + error_stop('兑换券可用额度不足'); + } + $agent->setInc('voucher_used', $amount); + return true; + } + + public function refundConsume($order) + { + $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'])) { + return; + } + $userId = intval($order['user_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(); + } + } + + public function consumeByOrder($order) + { + $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'])) { + return; + } + $this->consume(intval($order['user_id']), $fee, intval($order['id'] ?? 0)); + if (is_object($order) && method_exists($order, 'save')) { + $ext['voucher_consumed'] = 1; + $order->ext = $ext; + $order->save(); + } + } + + public function grant($order, $buyerId) + { + $exists = VoucherPlan::where('order_id', $order['id'])->find(); + if ($exists) { + return $exists; + } + + $times = (float) PartnerConfig::get('partner_voucher_times', 1.5); + $days = intval(PartnerConfig::get('partner_voucher_days', 365)); + $days = $days > 0 ? $days : 365; + $total = round((float) $order['pay_fee'] * $times, 2); + if ($total <= 0) { + return null; + } + $daily = round($total / $days, 2); + $startDate = date('Y-m-d', strtotime('+1 day', $this->orderTime($order))); + + $plan = VoucherPlan::create([ + 'user_id' => $buyerId, + 'order_id' => $order['id'], + 'total_amount' => $total, + 'daily_amount' => $daily, + 'released_amount' => 0, + 'released_days' => 0, + 'total_days' => $days, + 'start_date' => $startDate, + 'status' => 1, + 'createtime' => time(), + ]); + + $agent = AgentModel::where('user_id', $buyerId)->find(); + if ($agent) { + $agent->setInc('voucher_total', $total); + } + + PartnerRewardService::write([ + 'order_id' => $order['id'], + 'buyer_id' => $buyerId, + 'user_id' => $buyerId, + 'role' => 'taster', + 'reward_type' => 'voucher_grant', + 'amount' => 0, + 'base_amount' => $total, + 'ext' => ['formula' => "实付{$order['pay_fee']} × {$times} = {$total},{$days}天释放"], + 'status' => 1, + ]); + + return $plan; + } + + public function releaseByDate($settleDate) + { + $plans = VoucherPlan::where('status', 1) + ->where('start_date', '<=', $settleDate) + ->select(); + $byUser = []; + foreach ($plans as $plan) { + $amount = $this->releaseOne($plan, $settleDate); + if ($amount <= 0) { + continue; + } + $uid = intval($plan->user_id); + if (!isset($byUser[$uid])) { + $byUser[$uid] = ['amount' => 0, 'plans' => []]; + } + $byUser[$uid]['amount'] = bcadd((string) $byUser[$uid]['amount'], (string) $amount, 2); + $byUser[$uid]['plans'][] = $plan->id; + } + $totalReleased = 0; + foreach ($byUser as $userId => $info) { + PartnerRewardService::write([ + 'order_id' => PartnerRewardService::dailyOrderId($settleDate), + 'buyer_id' => $userId, + 'user_id' => $userId, + 'role' => 'taster', + 'reward_type' => 'voucher_release', + 'amount' => $info['amount'], + 'base_amount' => $info['amount'], + 'settle_date' => $settleDate, + 'ext' => [ + 'formula' => "兑换券释放 {$info['amount']}", + 'plan_ids' => $info['plans'], + ], + 'status' => 1, + ]); + $totalReleased = bcadd((string) $totalReleased, (string) $info['amount'], 2); + } + return (float) $totalReleased; + } + + public function releaseOne($plan, $settleDate) + { + if ($plan->status != 1) { + return 0; + } + if ($plan->released_days >= $plan->total_days) { + $plan->status = 2; + $plan->save(); + return 0; + } + if ($plan->start_date > $settleDate) { + return 0; + } + + $leftDays = $plan->total_days - $plan->released_days; + if ($leftDays <= 1) { + $amount = round((float) $plan->total_amount - (float) $plan->released_amount, 2); + $plan->status = 2; + } else { + $amount = (float) $plan->daily_amount; + } + if ($amount <= 0) { + $plan->status = 2; + $plan->save(); + return 0; + } + + $plan->released_amount = round((float) $plan->released_amount + $amount, 2); + $plan->released_days += 1; + $plan->save(); + + $agent = AgentModel::where('user_id', $plan->user_id)->find(); + if ($agent) { + $agent->setInc('voucher_released', $amount); + } + + return $amount; + } + + public function closeByOrder($orderId) + { + $plan = VoucherPlan::where('order_id', $orderId)->find(); + if (!$plan || $plan->status == 0) { + return; + } + $plan->status = 0; + $plan->save(); + + $agent = AgentModel::where('user_id', $plan->user_id)->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; + } + $agent->voucher_released = $newReleased; + $agent->voucher_total = $newTotal; + $agent->save(); + } + } + + public function rollbackRelease($settleDate) + { + $rows = RewardModel::where('settle_date', $settleDate) + ->where('reward_type', 'voucher_release') + ->where('status', 1) + ->select(); + foreach ($rows as $row) { + $planIds = $row->ext['plan_ids'] ?? []; + if (!$planIds && !empty($row->ext['plan_id'])) { + $planIds = [$row->ext['plan_id']]; + } + foreach ($planIds as $planId) { + $plan = VoucherPlan::get($planId); + if (!$plan) { + continue; + } + $daily = (float) $plan->daily_amount; + $plan->released_amount = max(0, (float) $plan->released_amount - $daily); + $plan->released_days = max(0, $plan->released_days - 1); + if ($plan->status == 2 && $plan->released_days < $plan->total_days) { + $plan->status = 1; + } + $plan->save(); + } + $agent = AgentModel::where('user_id', $row->user_id)->find(); + if ($agent) { + $agent->voucher_released = max(0, (float) $agent->voucher_released - (float) $row->amount); + $agent->save(); + } + } + } + + protected function orderTime($order) + { + $paid = $order['paid_time'] ?? 0; + if (is_numeric($paid) && $paid > 0) { + return intval($paid); + } + if (is_string($paid) && $paid !== '') { + $ts = strtotime($paid); + if ($ts) { + return $ts; + } + } + return time(); + } + + protected function dailyOrderId($settleDate, $planId) + { + // 避免 uk_order_user_type 日结撞车:负日期 + 计划尾巴 + return -intval(date('Ymd', strtotime($settleDate))) * 100 + intval($planId) % 100; + } +} diff --git a/addons/shopro/upgrade/partner_v2.sql b/addons/shopro/upgrade/partner_v2.sql new file mode 100644 index 0000000..8c9780f --- /dev/null +++ b/addons/shopro/upgrade/partner_v2.sql @@ -0,0 +1,143 @@ +-- 合伙股东收益体系 V2 +-- 表前缀以站点 database.prefix 为准(本仓库 .env 为 hla_) +-- 手工执行:将 __PREFIX__ 替换为实际前缀后导入 + +-- 1. 分销商扩展字段 +SET @dbname = DATABASE(); +SET @prefix = ( + SELECT TABLE_PREFIX FROM information_schema.TABLES + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME LIKE '%shopro_commission_agent' + ORDER BY CASE WHEN TABLE_NAME LIKE 'hla_%' THEN 0 ELSE 1 END + LIMIT 1 +); + +-- 直接按常见前缀改;执行脚本时请替换 __PREFIX__ + +ALTER TABLE `__PREFIX__shopro_commission_agent` + ADD COLUMN `is_venue` tinyint(1) NOT NULL DEFAULT 0 COMMENT '酒道馆' AFTER `level`, + ADD COLUMN `is_county` tinyint(1) NOT NULL DEFAULT 0 COMMENT '区县代理' AFTER `is_venue`, + ADD COLUMN `county_code` varchar(20) NOT NULL DEFAULT '' COMMENT '区县编码' AFTER `is_county`, + ADD COLUMN `direct_order_seq` int(11) NOT NULL DEFAULT 0 COMMENT '直推合格单累计(推三返一)' AFTER `county_code`, + ADD COLUMN `madow` decimal(20,8) NOT NULL DEFAULT 0.00000000 COMMENT '马豆' AFTER `direct_order_seq`, + ADD COLUMN `voucher_total` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '兑换券总额' AFTER `madow`, + ADD COLUMN `voucher_released` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '兑换券已释放' AFTER `voucher_total`, + ADD COLUMN `voucher_used` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '兑换券已使用' AFTER `voucher_released`, + ADD COLUMN `last_self_order_time` bigint(16) DEFAULT NULL COMMENT '本人最近合格订单时间' AFTER `voucher_used`, + ADD KEY `idx_venue` (`is_venue`), + ADD KEY `idx_county` (`is_county`), + ADD KEY `idx_last_self_order` (`last_self_order_time`); + +-- 2. 收益流水 +CREATE TABLE IF NOT EXISTS `__PREFIX__shopro_partner_reward` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `order_id` int(11) NOT NULL DEFAULT 0, + `buyer_id` int(11) NOT NULL DEFAULT 0, + `user_id` int(11) NOT NULL COMMENT '受益人', + `role` varchar(20) NOT NULL DEFAULT '' COMMENT 'taster/maker/venue/county/system', + `reward_type` varchar(32) NOT NULL, + `amount` decimal(12,2) NOT NULL DEFAULT 0.00, + `madow` decimal(20,8) NOT NULL DEFAULT 0.00000000, + `base_amount` decimal(12,2) NOT NULL DEFAULT 0.00, + `rate` decimal(16,10) NOT NULL DEFAULT 0.0000000000, + `ext` text, + `settle_date` date DEFAULT NULL, + `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0待入账 1已入账 -1取消 -2退回', + `createtime` bigint(16) DEFAULT NULL, + `updatetime` bigint(16) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_order_user_type` (`order_id`,`user_id`,`reward_type`), + UNIQUE KEY `uk_daily_user_type` (`settle_date`,`user_id`,`reward_type`), + KEY `idx_user` (`user_id`,`reward_type`,`status`), + KEY `idx_order` (`order_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合伙股东收益'; + +-- 3. 兑换券释放计划 +CREATE TABLE IF NOT EXISTS `__PREFIX__shopro_partner_voucher_plan` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `order_id` int(11) NOT NULL, + `total_amount` decimal(12,2) NOT NULL, + `daily_amount` decimal(12,2) NOT NULL, + `released_amount` decimal(12,2) NOT NULL DEFAULT 0.00, + `released_days` int(11) NOT NULL DEFAULT 0, + `total_days` int(11) NOT NULL DEFAULT 365, + `start_date` date NOT NULL, + `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '1释放中 2完成 0关闭', + `createtime` bigint(16) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_order` (`order_id`), + KEY `idx_user_status` (`user_id`,`status`,`start_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='兑换券释放计划'; + +-- 4. 汇率快照 +CREATE TABLE IF NOT EXISTS `__PREFIX__shopro_partner_rate_log` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `settle_date` date NOT NULL, + `order_count_n` int(11) NOT NULL DEFAULT 0 COMMENT '历史合格单累计N', + `rate` decimal(16,10) NOT NULL DEFAULT 0.0000000000, + `gmv` decimal(14,2) NOT NULL DEFAULT 0.00, + `madow_pool` decimal(20,8) NOT NULL DEFAULT 0.00000000, + `share_user_count` int(11) NOT NULL DEFAULT 0, + `venue_pool` decimal(12,2) NOT NULL DEFAULT 0.00, + `county_pool` decimal(12,2) NOT NULL DEFAULT 0.00, + `createtime` bigint(16) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_date` (`settle_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='马豆汇率日志'; + +-- 5. 日结对账 +CREATE TABLE IF NOT EXISTS `__PREFIX__shopro_partner_daily_report` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `settle_date` date NOT NULL, + `order_count` int(11) NOT NULL DEFAULT 0, + `order_count_n` int(11) NOT NULL DEFAULT 0, + `gmv` decimal(14,2) NOT NULL DEFAULT 0.00, + `rate` decimal(16,10) NOT NULL DEFAULT 0.0000000000, + `madow_pool` decimal(20,8) NOT NULL DEFAULT 0.00000000, + `share_user_count` int(11) NOT NULL DEFAULT 0, + `venue_pool` decimal(12,2) NOT NULL DEFAULT 0.00, + `venue_user_count` int(11) NOT NULL DEFAULT 0, + `county_pool` decimal(12,2) NOT NULL DEFAULT 0.00, + `county_user_count` int(11) NOT NULL DEFAULT 0, + `voucher_release_total` decimal(12,2) NOT NULL DEFAULT 0.00, + `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '1成功 0失败 2重跑中', + `error_msg` varchar(500) NOT NULL DEFAULT '', + `createtime` bigint(16) DEFAULT NULL, + `updatetime` bigint(16) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_date` (`settle_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合伙股东日结对账'; + +-- 6. 配置(挂在 shop.commission 下,sheep_config('shop.commission.partner_mode') 可读) +INSERT IGNORE INTO `__PREFIX__shopro_config` (`code`, `parent_code`, `name`, `description`, `type`, `value`, `store_range`, `rule`, `weigh`) VALUES +('shop.commission.partner_mode', 'shop.commission', '关闭旧百分比分佣', '1=跳过三级百分比', 'boolean', '1', NULL, NULL, 80), +('shop.commission.partner_enable', 'shop.commission', '启用合伙股东结算', '1=新单记即时奖', 'boolean', '1', NULL, NULL, 81), +('shop.commission.partner_count_type', 'shop.commission', '合格订单口径', 'order=订单头 item=商品件数', 'string', 'order', NULL, NULL, 82), +('shop.commission.partner_active_days', 'shop.commission', '领取窗口天数', NULL, 'int', '30', NULL, NULL, 83), +('shop.commission.partner_chain_max_depth', 'shop.commission', '关系链最大深度', NULL, 'int', '50', NULL, NULL, 84), +('shop.commission.partner_push3_amounts', 'shop.commission', '推三返一循环金额', NULL, 'array', '[398,400,600]', NULL, NULL, 85), +('shop.commission.partner_madow_mode', 'shop.commission', '马豆发放模式', 'madow=马豆 cash=现金', 'string', 'madow', NULL, NULL, 86), +('shop.commission.partner_madow_base_rate', 'shop.commission', '马豆基础汇率', NULL, 'string', '0.00000075', NULL, NULL, 87), +('shop.commission.partner_madow_step_orders', 'shop.commission', '马豆步进订单数', NULL, 'int', '1', NULL, NULL, 88), +('shop.commission.partner_voucher_times', 'shop.commission', '兑换券倍数', NULL, 'float', '1.5', NULL, NULL, 89), +('shop.commission.partner_voucher_days', 'shop.commission', '兑换券释放天数', NULL, 'int', '365', NULL, NULL, 90), +('shop.commission.partner_maker_bonus', 'shop.commission', '创客级差基数', NULL, 'array', '{\"2\":20,\"3\":40,\"4\":60,\"5\":80,\"6\":100,\"7\":120}', NULL, NULL, 91), +('shop.commission.partner_maker_peer_rate', 'shop.commission', '创客平级比例', NULL, 'float', '0.10', NULL, NULL, 92), +('shop.commission.partner_maker_overtaken_rate', 'shop.commission', '创客被超越比例', NULL, 'float', '0.05', NULL, NULL, 93), +('shop.commission.partner_team_total', 'shop.commission', '团队拆分总额', NULL, 'float', '100', NULL, NULL, 94), +('shop.commission.partner_venue_split', 'shop.commission', '酒道馆拆分份额', NULL, 'float', '50', NULL, NULL, 95), +('shop.commission.partner_county_full', 'shop.commission', '无馆区县全额', NULL, 'float', '100', NULL, NULL, 96), +('shop.commission.partner_venue_share_per_order', 'shop.commission', '酒道馆每单奖池', NULL, 'float', '100', NULL, NULL, 97), +('shop.commission.partner_county_share_per_order', 'shop.commission', '区县每单奖池', NULL, 'float', '50', NULL, NULL, 98), +('shop.commission.partner_venue_peer_rate', 'shop.commission', '酒道馆平级比例', NULL, 'float', '0.10', NULL, NULL, 99), +('shop.commission.partner_county_peer_rate', 'shop.commission', '区县平级比例', NULL, 'float', '0.10', NULL, NULL, 100); + +-- 7. 初始化 7 条等级(已存在则忽略) +INSERT IGNORE INTO `__PREFIX__shopro_commission_level` (`level`, `name`, `image`, `commission_rules`, `upgrade_type`, `upgrade_rules`) VALUES +(1, '品酒师', '', '{\"commission_1\":0,\"commission_2\":0,\"commission_3\":0}', 0, '{}'), +(2, '创客V1', '', '{\"commission_1\":0,\"commission_2\":0,\"commission_3\":0}', 0, '{}'), +(3, '创客V2', '', '{\"commission_1\":0,\"commission_2\":0,\"commission_3\":0}', 0, '{}'), +(4, '创客V3', '', '{\"commission_1\":0,\"commission_2\":0,\"commission_3\":0}', 0, '{}'), +(5, '创客V4', '', '{\"commission_1\":0,\"commission_2\":0,\"commission_3\":0}', 0, '{}'), +(6, '创客V5', '', '{\"commission_1\":0,\"commission_2\":0,\"commission_3\":0}', 0, '{}'), +(7, '创客V6', '', '{\"commission_1\":0,\"commission_2\":0,\"commission_3\":0}', 0, '{}'); diff --git a/application/admin/controller/shopro/Config.php b/application/admin/controller/shopro/Config.php index 9e6e280..e7404c6 100644 --- a/application/admin/controller/shopro/Config.php +++ b/application/admin/controller/shopro/Config.php @@ -51,6 +51,11 @@ class Config extends Common 'name' => 'shopro/config/commission', 'status' => $this->auth->check('shopro/config/commission') ], + [ + 'label' => '合伙股东', + 'name' => 'shopro/config/partner', + 'status' => $this->auth->check('shopro/config/commission') + ], [ 'label' => '支付配置', 'name' => 'shopro/pay_config', @@ -181,6 +186,92 @@ class Config extends Common $this->success('操作成功', null, $configs); } + /** + * 合伙股东规则 + */ + public function partner() + { + $keys = [ + 'partner_mode', 'partner_enable', 'partner_count_type', 'partner_active_days', + 'partner_chain_max_depth', 'partner_push3_amounts', 'partner_madow_mode', + 'partner_madow_base_rate', 'partner_madow_step_orders', 'partner_voucher_times', + 'partner_voucher_days', 'partner_maker_bonus', 'partner_maker_peer_rate', + 'partner_maker_overtaken_rate', 'partner_team_total', 'partner_venue_split', + 'partner_county_full', 'partner_venue_share_per_order', 'partner_county_share_per_order', + 'partner_venue_peer_rate', 'partner_county_peer_rate', + ]; + if ('GET' === $this->request->method()) { + $all = ShoproConfig::getConfigs('shop.commission', false) ?: []; + $configs = []; + foreach ($keys as $key) { + $configs[$key] = $all[$key] ?? null; + } + $defaults = [ + 'partner_mode' => 1, + 'partner_enable' => 1, + 'partner_count_type' => 'order', + 'partner_active_days' => 30, + 'partner_chain_max_depth' => 50, + 'partner_push3_amounts' => [398, 400, 600], + 'partner_madow_mode' => 'madow', + 'partner_madow_base_rate' => '0.00000075', + 'partner_madow_step_orders' => 1, + 'partner_voucher_times' => 1.5, + 'partner_voucher_days' => 365, + 'partner_maker_bonus' => ['2' => 20, '3' => 40, '4' => 60, '5' => 80, '6' => 100, '7' => 120], + 'partner_maker_peer_rate' => 0.10, + 'partner_maker_overtaken_rate' => 0.05, + 'partner_team_total' => 100, + 'partner_venue_split' => 50, + 'partner_county_full' => 100, + 'partner_venue_share_per_order' => 100, + 'partner_county_share_per_order' => 50, + 'partner_venue_peer_rate' => 0.10, + 'partner_county_peer_rate' => 0.10, + ]; + foreach ($defaults as $key => $value) { + if ($configs[$key] === null || $configs[$key] === '') { + $configs[$key] = $value; + } + } + } elseif ('POST' === $this->request->method()) { + check_env('commission'); + $params = $this->request->param(); + $save = []; + foreach ($keys as $key) { + if (array_key_exists($key, $params)) { + $save[$key] = $params[$key]; + } + } + if (isset($save['partner_active_days']) && intval($save['partner_active_days']) < 1) { + $this->error('领取窗口天数须 ≥ 1'); + } + if (!empty($save['partner_maker_bonus']) && is_array($save['partner_maker_bonus'])) { + $prev = null; + foreach ([2, 3, 4, 5, 6, 7] as $lv) { + $val = isset($save['partner_maker_bonus'][$lv]) + ? (float) $save['partner_maker_bonus'][$lv] + : (isset($save['partner_maker_bonus'][(string) $lv]) ? (float) $save['partner_maker_bonus'][(string) $lv] : null); + if ($val === null) { + continue; + } + if ($prev !== null && $val < $prev) { + $this->error('创客 V1–V6 单笔基数必须递增'); + } + $prev = $val; + } + } + if (isset($save['partner_team_total'], $save['partner_venue_split']) + && (float) $save['partner_venue_split'] > (float) $save['partner_team_total']) { + $this->error('酒道馆份额不能大于团队拆分总额'); + } + $configs = ShoproConfig::setConfigs('shop.commission', $save); + $this->success('规则已更新,仅对之后的新订单生效。', null, $configs); + return; + } + $this->success('操作成功', null, $configs); + } + /** * 订单配置 diff --git a/application/admin/controller/shopro/commission/Agent.php b/application/admin/controller/shopro/commission/Agent.php index 5ad9ef3..ebfcb38 100644 --- a/application/admin/controller/shopro/commission/Agent.php +++ b/application/admin/controller/shopro/commission/Agent.php @@ -12,7 +12,7 @@ use think\Db; class Agent extends Common { - protected $noNeedRight = ['select']; + protected $noNeedRight = ['select', 'identity']; protected $model = null; @@ -52,6 +52,28 @@ class Agent extends Common $this->error(__('No Results were found')); } + $amounts = \addons\shopro\service\partner\PartnerConfig::push3Amounts(); + $seq = intval($detail->direct_order_seq ?? 0); + $next = $amounts[$seq % count($amounts)] ?? 398; + $last = intval($detail->last_self_order_time ?? 0); + $days = \addons\shopro\service\partner\PartnerConfig::activeDays(); + $ago = $last ? intval(floor((time() - $last) / 86400)) : null; + $cash = \app\admin\model\shopro\partner\Reward::where('user_id', $id) + ->where('status', 1) + ->where('amount', '>', 0) + ->sum('amount'); + + $detail->next_push3_amount = number_format((float) $next, 2, '.', ''); + $detail->next_push3_text = '第 ' . ($seq + 1) . ' 单将返 ' . $detail->next_push3_amount; + $detail->last_self_order_text = $last ? date('Y-m-d H:i:s', $last) : '从未下单'; + $detail->active_status_text = !empty($detail->active_30d) + ? '正常领取' + : ('已暂停' . ($ago !== null ? '(距今 ' . $ago . ' 天)' : '')); + $detail->partner_cash_income = number_format((float) $cash, 2, '.', ''); + $detail->partner_mode = \addons\shopro\service\partner\PartnerConfig::isPartnerMode() ? 1 : 0; + $rate = (new \addons\shopro\service\partner\RateService())->currentRate(); + $detail->madow_value = \addons\shopro\service\partner\IdentityService::formatMoney((float) ($detail->madow ?? 0) * (float) $rate); + $this->success('分销商详情', null, $detail); } @@ -98,7 +120,7 @@ class Agent extends Common */ public function edit($id = null) { - $params = $this->request->only(['status', 'upgrade_lock', 'level_status', 'level', 'apply_info']); + $params = $this->request->only(['status', 'upgrade_lock', 'level_status', 'level', 'apply_info', 'is_venue', 'is_county', 'county_code']); $result = Db::transaction(function () use ($id, $params) { $row = $this->model->with(['user', 'level_info', 'level_status_info', 'upgrade_level'])->where('user_id', $id)->find(); @@ -117,6 +139,14 @@ class Agent extends Common case 'level': // 修改等级 return $this->changeLevel($row, $value); break; + case 'is_venue': + case 'is_county': + case 'county_code': + $result = $row->save([$field => $value]); + if ($result) { + LogModel::add($row->user_id, 'agent', ['type' => 'identity', 'field' => $field, 'value' => $value]); + } + return $result; default: return $row->save([$field => $value]); } @@ -129,6 +159,91 @@ class Agent extends Common } } + /** + * 开通 / 调整身份(弹窗一次提交) + */ + public function identity($id = null) + { + if (!$this->auth->check('shopro/commission/agent/edit')) { + $this->error(__('You have no permission')); + } + $params = $this->request->only(['level', 'is_venue', 'is_county', 'county_code', 'upgrade_lock', 'remark']); + if ($id === null || $id === '') { + $this->error('请选择用户'); + } + + $level = intval($params['level'] ?? 0); + if ($level < 1 || $level > 7) { + $this->error('请选择品酒师或创客等级'); + } + $isVenue = intval($params['is_venue'] ?? 0) ? 1 : 0; + $isCounty = intval($params['is_county'] ?? 0) ? 1 : 0; + $countyCode = trim((string) ($params['county_code'] ?? '')); + if ($isCounty && $countyCode === '') { + $this->error('开通区县代理时请选择区县'); + } + if (!$isCounty) { + $countyCode = ''; + } + $upgradeLock = array_key_exists('upgrade_lock', $params) ? intval($params['upgrade_lock']) : null; + $remark = trim((string) ($params['remark'] ?? '')); + + $result = Db::transaction(function () use ($id, $level, $isVenue, $isCounty, $countyCode, $upgradeLock, $remark) { + $service = new AgentService(intval($id)); + if (!$service->user) { + $this->error('未找到该用户'); + } + if (!$service->agent) { + $service->createNewAgent('user'); + $service = new AgentService(intval($id)); + } + $row = $service->agent; + if (!$row) { + $this->error('未能生成分销身份'); + } + + $before = \addons\shopro\service\partner\IdentityService::build($row); + $levelModel = LevelModel::find($level); + if (!$levelModel) { + $this->error('未找到该等级'); + } + + $save = [ + 'level' => $level, + 'is_venue' => $isVenue, + 'is_county' => $isCounty, + 'county_code' => $countyCode, + ]; + if ($upgradeLock !== null) { + $save['upgrade_lock'] = $upgradeLock ? 1 : 0; + } + $row->save($save); + + $after = \addons\shopro\service\partner\IdentityService::build($row); + $logRemark = $remark !== '' + ? $remark + : sprintf('将「%s」从 %s 调整为 %s。已入账收益不追溯。', $service->user->nickname ?: $id, $before['identity_text'] ?: '未开通', $after['identity_text'] ?: '-'); + LogModel::add($row->user_id, 'agent', [ + 'type' => 'identity', + 'field' => 'bundle', + 'value' => $after['identity_text'], + 'before' => $before['identity_text'], + ], null, $logRemark); + LogModel::add($row->user_id, 'agent', ['type' => 'level', 'level' => $levelModel]); + (new AgentService($row->user_id))->createAsyncAgentUpgrade(); + + return [ + 'identity_text' => $after['identity_text'], + 'level' => $level, + 'is_venue' => $isVenue, + 'is_county' => $isCounty, + 'county_code' => $countyCode, + ]; + }); + + $this->success('身份已更新', null, $result); + } + // 修改状态 private function changeStatus($row, $value) diff --git a/application/admin/controller/shopro/order/Order.php b/application/admin/controller/shopro/order/Order.php index e712b62..8f43364 100644 --- a/application/admin/controller/shopro/order/Order.php +++ b/application/admin/controller/shopro/order/Order.php @@ -180,9 +180,140 @@ class Order extends Common // 处理未支付订单 item status_code $order = $order->setOrderItemStatusByOrder($order); + $order->partner_split = $this->buildPartnerSplit($order); + $this->success('获取成功', null, $order); } + /** + * 合伙股东分成明细:即时奖列人;均分只汇总人数/人均 + */ + protected function buildPartnerSplit($order) + { + $orderId = intval($order->id ?? 0); + $shareTypes = ['venue_share', 'county_share', 'madow_share']; + $items = []; + $totalCash = 0; + + $rows = \app\admin\model\shopro\partner\Reward::with(['user']) + ->where('order_id', $orderId) + ->order('id', 'asc') + ->select(); + + foreach ($rows as $row) { + $type = (string) ($row->reward_type ?? ''); + if (in_array($type, $shareTypes, true)) { + continue; + } + $amount = round((float) $row->amount, 2); + $madow = (float) ($row->madow ?? 0); + if (intval($row->status) === 1 && $amount > 0) { + $totalCash = round($totalCash + $amount, 2); + } + $items[] = [ + 'display' => 'user', + 'id' => intval($row->id), + 'reward_type' => $type, + 'reward_type_text' => $row->reward_type_text, + 'role_text' => $row->role_text ?? '', + 'user_id' => intval($row->user_id), + 'user' => $row->user, + 'amount' => number_format($amount, 2, '.', ''), + 'madow' => \addons\shopro\service\partner\IdentityService::formatMadow($madow), + 'status' => intval($row->status), + 'status_text' => $row->status_text, + 'formula' => $row->formula, + 'skip_reason_text' => $row->skip_reason_text, + ]; + } + + $settleDate = null; + $paidRaw = $order->getData('paid_time') ?? ($order['paid_time'] ?? null); + if ($paidRaw) { + $paidTs = is_numeric($paidRaw) ? intval($paidRaw) : strtotime((string) $paidRaw); + if ($paidTs > 0) { + $settleDate = date('Y-m-d', $paidTs); + } + } + + if ($settleDate) { + foreach ($shareTypes as $type) { + $shareRows = \app\admin\model\shopro\partner\Reward::where('settle_date', $settleDate) + ->where('reward_type', $type) + ->where('status', 1) + ->where('user_id', '>', 0) + ->select(); + $userCount = count($shareRows); + if ($userCount <= 0) { + // 分母为 0 进平台的记录 + $platform = \app\admin\model\shopro\partner\Reward::where('settle_date', $settleDate) + ->where('reward_type', $type) + ->where('status', 1) + ->where('user_id', 0) + ->find(); + if (!$platform) { + continue; + } + $items[] = [ + 'display' => 'share', + 'reward_type' => $type, + 'reward_type_text' => \addons\shopro\service\partner\PartnerConfig::rewardTypeText($type), + 'user_count' => 0, + 'each_amount' => '0.00', + 'each_madow' => '0.00000000', + 'total_amount' => number_format((float) $platform->amount, 2, '.', ''), + 'total_madow' => \addons\shopro\service\partner\IdentityService::formatMadow($platform->madow ?? 0), + 'settle_date' => $settleDate, + 'status_text' => '已入账', + 'unit' => $type === 'madow_share' ? '马豆' : '元', + 'formula' => $platform->formula, + 'tip' => '付款日日结均分:当日无人参与,奖池进平台', + ]; + continue; + } + + $totalAmount = 0; + $totalMadow = 0; + foreach ($shareRows as $sr) { + $totalAmount = round($totalAmount + (float) $sr->amount, 2); + $totalMadow = function_exists('bcadd') + ? (float) bcadd((string) $totalMadow, (string) ($sr->madow ?? 0), 8) + : ($totalMadow + (float) ($sr->madow ?? 0)); + } + $first = $shareRows[0]; + $eachAmount = number_format((float) $first->amount, 2, '.', ''); + $eachMadow = \addons\shopro\service\partner\IdentityService::formatMadow($first->madow ?? 0); + $ext = is_array($first->ext) ? $first->ext : []; + if (!empty($ext['share_user_count'])) { + $userCount = intval($ext['share_user_count']); + } + + $items[] = [ + 'display' => 'share', + 'reward_type' => $type, + 'reward_type_text' => \addons\shopro\service\partner\PartnerConfig::rewardTypeText($type), + 'user_count' => $userCount, + 'each_amount' => $eachAmount, + 'each_madow' => $eachMadow, + 'total_amount' => number_format($totalAmount, 2, '.', ''), + 'total_madow' => \addons\shopro\service\partner\IdentityService::formatMadow($totalMadow), + 'settle_date' => $settleDate, + 'status_text' => '已入账', + 'unit' => $type === 'madow_share' ? '马豆' : '元', + 'formula' => $first->formula, + 'tip' => '付款日日结均分(不列出参与用户名单)', + ]; + } + } + + return [ + 'items' => $items, + 'total_cash' => number_format($totalCash, 2, '.', ''), + 'settle_date' => $settleDate, + 'has_data' => count($items) > 0, + ]; + } + /** * 批量发货渲染模板 diff --git a/application/admin/controller/shopro/partner/Daily.php b/application/admin/controller/shopro/partner/Daily.php new file mode 100644 index 0000000..365ac62 --- /dev/null +++ b/application/admin/controller/shopro/partner/Daily.php @@ -0,0 +1,75 @@ +model = new DailyReport(); + } + + public function index() + { + if (!$this->request->isAjax()) { + return $this->view->fetch(); + } + $list = $this->model->sheepFilter()->order('settle_date', 'desc')->paginate($this->request->param('list_rows', 10)); + $today = date('Y-m-d'); + $yesterday = date('Y-m-d', strtotime('-1 day')); + $days = \addons\shopro\service\partner\PartnerConfig::activeDays(); + $since = time() - $days * 86400; + $result = $list->toArray(); + $cashTypes = \addons\shopro\service\partner\PartnerConfig::instantTypes(); + $cashTypes = array_merge($cashTypes, ['venue_share', 'county_share']); + $sumCash = function ($date) use ($cashTypes) { + return (float) \app\admin\model\shopro\partner\Reward::where('settle_date', $date) + ->where('status', 1) + ->whereIn('reward_type', $cashTypes) + ->sum('amount'); + }; + $todayCash = $sumCash($today); + // 即时奖无 settle_date,按入账日合计 + $todayStart = strtotime($today . ' 00:00:00'); + $todayEnd = $todayStart + 86400 - 1; + $yStart = strtotime($yesterday . ' 00:00:00'); + $yEnd = $yStart + 86400 - 1; + $instantToday = (float) \app\admin\model\shopro\partner\Reward::where('status', 1) + ->whereNull('settle_date') + ->whereBetween('createtime', [$todayStart, $todayEnd]) + ->whereIn('reward_type', \addons\shopro\service\partner\PartnerConfig::instantTypes()) + ->sum('amount'); + $instantYesterday = (float) \app\admin\model\shopro\partner\Reward::where('status', 1) + ->whereNull('settle_date') + ->whereBetween('createtime', [$yStart, $yEnd]) + ->whereIn('reward_type', \addons\shopro\service\partner\PartnerConfig::instantTypes()) + ->sum('amount'); + $result['kpi'] = [ + 'today' => DailyReport::where('settle_date', $today)->find(), + 'yesterday' => DailyReport::where('settle_date', $yesterday)->find(), + 'today_cash' => number_format($todayCash + $instantToday, 2, '.', ''), + 'yesterday_cash' => number_format($sumCash($yesterday) + $instantYesterday, 2, '.', ''), + '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(), + ]; + $this->success('获取成功', null, $result); + } + + public function rerun() + { + $date = $this->request->param('settle_date'); + if (!$date) { + $this->error('请选择日期'); + } + $report = (new DailySettleService())->run($date, true); + $this->success('重跑完成', null, $report); + } +} diff --git a/application/admin/controller/shopro/partner/Reward.php b/application/admin/controller/shopro/partner/Reward.php new file mode 100644 index 0000000..c2e62d0 --- /dev/null +++ b/application/admin/controller/shopro/partner/Reward.php @@ -0,0 +1,170 @@ +model = new RewardModel(); + } + + public function index() + { + if (!$this->request->isAjax()) { + $exportConfig = (new \addons\shopro\library\Export())->getConfig(); + $this->assignconfig("save_type", $exportConfig['save_type'] ?? 'download'); + $this->assignconfig("rewardTypeMap", PartnerConfig::rewardTypeMap()); + return $this->view->fetch(); + } + + $showSkip = $this->request->param('show_skip', 0); + $query = $this->model->sheepFilter()->with(['buyer', 'user', 'order']); + if (!$showSkip) { + $query->where(function ($q) { + $q->where('amount', '>', 0)->whereOr('madow', '>', 0); + }); + } + $list = $query->paginate($this->request->param('list_rows', 10)); + $this->success('获取成功', null, $list); + } + + public function export() + { + $cellTitles = [ + 'id' => 'Id', + 'createtime' => '入账时间', + 'user_nickname' => '受益人', + 'buyer_nickname' => '买家', + 'order_id' => '订单', + 'reward_type_text' => '奖项', + 'role_text' => '角色', + 'amount' => '金额', + 'madow' => '马豆', + 'base_amount' => '基数', + 'status_text' => '状态', + 'formula' => '公式', + ]; + $total = $this->model->sheepFilter()->count(); + if ($total <= 0) { + $this->error('导出数据为空'); + } + $export = new \addons\shopro\library\Export(); + $params = [ + 'file_name' => '合伙股东收益流水', + 'cell_titles' => $cellTitles, + 'total' => $total, + 'is_sub_cell' => false, + ]; + $totalAmount = 0; + $totalMadow = 0; + $result = $export->export($params, function ($pages) use (&$totalAmount, &$totalMadow, $total) { + $datas = $this->model->sheepFilter()->with(['buyer', 'user', 'order']) + ->limit((($pages['page'] - 1) * $pages['list_rows']), $pages['list_rows']) + ->select(); + $newDatas = []; + foreach ($datas as $reward) { + $newDatas[] = [ + 'id' => $reward['id'], + 'createtime' => $reward['createtime'], + 'user_nickname' => $reward['user']['nickname'] ?? '-', + 'buyer_nickname' => $reward['buyer']['nickname'] ?? '-', + 'order_id' => $reward['order']['order_sn'] ?? $reward['order_id'], + 'reward_type_text' => $reward['reward_type_text'], + 'role_text' => $reward['role_text'] ?? $reward['role'], + 'amount' => $reward['amount'], + 'madow' => $reward['madow'], + 'base_amount' => $reward['base_amount'], + 'status_text' => $reward['status_text'], + 'formula' => $reward['formula'], + ]; + } + $totalAmount += array_sum(array_column($newDatas, 'amount')); + $totalMadow += array_sum(array_column($newDatas, 'madow')); + if ($pages['is_last_page']) { + $newDatas[] = ['id' => "总数:{$total};金额:{$totalAmount};马豆:{$totalMadow}"]; + } + return $newDatas; + }); + $this->success('导出成功', null, $result); + } + + public function reissue() + { + $params = $this->request->only(['user_id', 'reward_type', 'amount', 'order_id', 'memo']); + if (empty($params['user_id']) || empty($params['reward_type']) || empty($params['memo'])) { + $this->error('用户、奖项、备注必填'); + } + $amount = round((float) ($params['amount'] ?? 0), 2); + if ($amount == 0) { + $this->error('请输入金额'); + } + Db::transaction(function () use ($params, $amount) { + $row = PartnerRewardService::write([ + 'order_id' => intval($params['order_id'] ?? 0) ?: -time(), + 'user_id' => intval($params['user_id']), + 'buyer_id' => 0, + 'role' => 'system', + 'reward_type' => 'manual_' . $params['reward_type'], + 'amount' => $amount, + 'base_amount' => $amount, + 'ext' => ['formula' => '手工补发', 'memo' => $params['memo'], 'manual' => 1], + 'status' => 1, + ]); + Wallet::change($params['user_id'], 'commission', $amount, 'reward_income', [ + 'partner_reward_id' => $row->id ?? 0, + 'manual' => 1, + ], $params['memo']); + }); + $this->success('补发成功'); + } + + public function back() + { + $id = $this->request->param('id'); + $memo = trim((string) $this->request->param('memo', '')); + if ($memo === '') { + $this->error('请填写退回原因'); + } + $row = $this->model->where('id', $id)->find(); + if (!$row) { + $this->error('记录不存在'); + } + if ($row->status != 1 || $row->settle_date) { + $this->error('仅已入账的即时奖可退回'); + } + Db::transaction(function () use ($row, $memo) { + if ((float) $row->amount > 0 && $row->user_id > 0) { + Wallet::change($row->user_id, 'commission', -$row->amount, 'reward_back', [ + 'partner_reward_id' => $row->id, + ], $memo); + } + $ext = is_array($row->ext) ? $row->ext : (json_decode($row->ext ?: '{}', true) ?: []); + $ext['back_memo'] = $memo; + $row->ext = $ext; + $row->status = -2; + $row->save(); + }); + $this->success('已退回'); + } + + public function backOrder() + { + $orderId = intval($this->request->param('order_id')); + if ($orderId <= 0) { + $this->error('请选择订单'); + } + (new PartnerRewardService())->refund($orderId); + $this->success('整单已退回'); + } +} diff --git a/application/admin/controller/shopro/partner/Voucher.php b/application/admin/controller/shopro/partner/Voucher.php new file mode 100644 index 0000000..b26c72f --- /dev/null +++ b/application/admin/controller/shopro/partner/Voucher.php @@ -0,0 +1,98 @@ +model = new VoucherPlan(); + } + + public function index() + { + if (!$this->request->isAjax()) { + return $this->view->fetch(); + } + $list = $this->model->sheepFilter()->with(['user', 'order'])->order('id', 'desc')->paginate($this->request->param('list_rows', 10)); + $userIds = []; + foreach ($list as $row) { + $userIds[] = intval($row->user_id); + } + $agents = []; + if ($userIds) { + $rows = \app\admin\model\shopro\commission\Agent::whereIn('user_id', $userIds) + ->field('user_id,voucher_used,voucher_released') + ->select(); + foreach ($rows as $agent) { + $agents[$agent['user_id']] = $agent; + } + } + foreach ($list as $row) { + $agent = $agents[$row->user_id] ?? []; + $used = (float) ($agent['voucher_used'] ?? 0); + $released = (float) ($agent['voucher_released'] ?? 0); + $row->voucher_used = number_format($used, 2, '.', ''); + $row->voucher_available = number_format(max(0, $released - $used), 2, '.', ''); + } + $this->success('获取成功', null, $list); + } + + public function close() + { + $id = $this->request->param('id'); + $plan = $this->model->where('id', $id)->find(); + if (!$plan) { + $this->error('计划不存在'); + } + (new VoucherService())->closeByOrder($plan->order_id); + $this->success('已关闭'); + } + + /** + * 按日释放日志 + */ + public function logs() + { + $id = intval($this->request->param('id')); + $plan = $this->model->where('id', $id)->find(); + if (!$plan) { + $this->error('计划不存在'); + } + $list = \app\admin\model\shopro\partner\Reward::where('user_id', $plan->user_id) + ->where('reward_type', 'voucher_release') + ->order('settle_date', 'desc') + ->order('id', 'desc') + ->limit(200) + ->select(); + $rows = []; + foreach ($list as $item) { + $ext = is_array($item->ext) ? $item->ext : (json_decode($item->ext ?: '{}', true) ?: []); + $planIds = $ext['plan_ids'] ?? []; + if (!$planIds && !empty($ext['plan_id'])) { + $planIds = [$ext['plan_id']]; + } + $planIds = array_map('intval', (array) $planIds); + if ($planIds && !in_array(intval($plan->id), $planIds, true)) { + continue; + } + $rows[] = [ + 'id' => $item->id, + 'settle_date' => $item->settle_date, + 'amount' => $item->amount, + 'status_text' => $item->status_text, + 'formula' => $item->formula, + 'createtime' => $item->createtime, + ]; + } + $this->success('ok', null, ['plan' => $plan, 'logs' => $rows]); + } +} diff --git a/application/admin/controller/shopro/user/User.php b/application/admin/controller/shopro/user/User.php index d48ba6a..ab16d9a 100644 --- a/application/admin/controller/shopro/user/User.php +++ b/application/admin/controller/shopro/user/User.php @@ -49,8 +49,12 @@ class User extends Common if (!$user) { $this->error(__('No Results were found')); } + $agent = \addons\shopro\service\partner\IdentityService::findAgent($id); + $partner = \addons\shopro\service\partner\IdentityService::attachProfile($agent); + $data = array_merge($user->toArray(), $partner); + $data['has_agent'] = (bool) $agent; - $this->success('获取成功', null, $user); + $this->success('获取成功', null, $data); } /** diff --git a/application/admin/model/shopro/commission/Agent.php b/application/admin/model/shopro/commission/Agent.php index fb1b39b..73e1bc2 100644 --- a/application/admin/model/shopro/commission/Agent.php +++ b/application/admin/model/shopro/commission/Agent.php @@ -19,7 +19,14 @@ class Agent extends Common ]; protected $append = [ 'status_text', - 'pending_reward' + 'pending_reward', + 'identity_text', + 'identities', + 'is_partner', + 'maker_level_name', + 'voucher_available', + 'active_30d', + 'madow_value', ]; // 分销商状态 AGENT_STATUS @@ -71,6 +78,44 @@ class Agent extends Common return number_format($amount, 2, '.', ''); } + public function getIdentityTextAttr($value, $data) + { + return \addons\shopro\service\partner\IdentityService::build($data)['identity_text']; + } + + public function getIdentitiesAttr($value, $data) + { + return \addons\shopro\service\partner\IdentityService::build($data)['identities']; + } + + public function getIsPartnerAttr($value, $data) + { + return \addons\shopro\service\partner\IdentityService::build($data)['is_partner']; + } + + public function getMakerLevelNameAttr($value, $data) + { + return \addons\shopro\service\partner\IdentityService::build($data)['maker_level_name']; + } + + public function getVoucherAvailableAttr($value, $data) + { + $released = (float) ($data['voucher_released'] ?? 0); + $used = (float) ($data['voucher_used'] ?? 0); + return number_format(max(0, $released - $used), 2, '.', ''); + } + + public function getActive_30dAttr($value, $data) + { + return (new \addons\shopro\service\partner\ActiveService())->isActive($data); + } + + public function getMadowValueAttr($value, $data) + { + $rate = (new \addons\shopro\service\partner\RateService())->currentRate(); + return \addons\shopro\service\partner\IdentityService::formatMoney((float) ($data['madow'] ?? 0) * (float) $rate); + } + public function levelStatusInfo() { return $this->belongsTo(Level::class, 'level_status', 'level'); diff --git a/application/admin/model/shopro/commission/Log.php b/application/admin/model/shopro/commission/Log.php index c1d79ee..be63774 100644 --- a/application/admin/model/shopro/commission/Log.php +++ b/application/admin/model/shopro/commission/Log.php @@ -91,6 +91,11 @@ class Log extends Common case 'apply_info': $remark = '您的分销商资料信息已更新'; break; + case 'identity': + $fieldMap = ['is_venue' => '酒道馆', 'is_county' => '区县代理', 'county_code' => '区县编码']; + $name = $fieldMap[$ext['field'] ?? ''] ?? ($ext['field'] ?? '身份'); + $remark = "您的{$name}已更新"; + break; } return $remark ?? ""; } @@ -180,6 +185,7 @@ class Log extends Common 'reward' => '佣金', 'share' => '推荐', 'bind' => '绑定', + 'identity' => '身份', ]; } diff --git a/application/admin/model/shopro/partner/DailyReport.php b/application/admin/model/shopro/partner/DailyReport.php new file mode 100644 index 0000000..9ba6ead --- /dev/null +++ b/application/admin/model/shopro/partner/DailyReport.php @@ -0,0 +1,23 @@ + '失败', + 1 => '成功', + 2 => '重跑中', + ]; + } +} diff --git a/application/admin/model/shopro/partner/RateLog.php b/application/admin/model/shopro/partner/RateLog.php new file mode 100644 index 0000000..c303546 --- /dev/null +++ b/application/admin/model/shopro/partner/RateLog.php @@ -0,0 +1,12 @@ + 'json', + ]; + + protected $append = [ + 'status_text', + 'reward_type_text', + 'role_text', + 'skip_reason', + 'skip_reason_text', + 'formula', + ]; + + public function statusList() + { + return [ + -2 => '已退回', + -1 => '已取消', + 0 => '待入账', + 1 => '已入账', + ]; + } + + public function rewardTypeList() + { + return PartnerConfig::rewardTypeMap(); + } + + public function roleList() + { + return [ + 'taster' => '品酒师', + 'maker' => '创客', + 'venue' => '酒道馆', + 'county' => '区县', + 'system' => '平台', + ]; + } + + public function getRewardTypeTextAttr($value, $data) + { + return PartnerConfig::rewardTypeText($data['reward_type'] ?? ''); + } + + public function getRoleTextAttr($value, $data) + { + $role = $data['role'] ?? ''; + $list = $this->roleList(); + return $list[$role] ?? $role; + } + + public function getSkipReasonAttr($value, $data) + { + $ext = $data['ext'] ?? []; + if (is_string($ext)) { + $ext = json_decode($ext, true) ?: []; + } + return $ext['skip_reason'] ?? ''; + } + + public function getSkipReasonTextAttr($value, $data) + { + $reason = $this->getSkipReasonAttr(null, $data); + return $reason ? PartnerConfig::skipReasonText($reason) : ''; + } + + public function getFormulaAttr($value, $data) + { + $ext = $data['ext'] ?? []; + if (is_string($ext)) { + $ext = json_decode($ext, true) ?: []; + } + return $ext['formula'] ?? ''; + } + + public function buyer() + { + return $this->belongsTo(UserModel::class, 'buyer_id', 'id')->field('id, nickname, avatar, mobile'); + } + + public function user() + { + return $this->belongsTo(UserModel::class, 'user_id', 'id')->field('id, nickname, avatar, mobile'); + } + + public function order() + { + return $this->belongsTo(OrderModel::class, 'order_id', 'id'); + } +} diff --git a/application/admin/model/shopro/partner/VoucherPlan.php b/application/admin/model/shopro/partner/VoucherPlan.php new file mode 100644 index 0000000..1f059a8 --- /dev/null +++ b/application/admin/model/shopro/partner/VoucherPlan.php @@ -0,0 +1,44 @@ + '已关闭', + 1 => '释放中', + 2 => '已完成', + ]; + } + + public function getAvailableAttr($value, $data) + { + $released = (float) ($data['released_amount'] ?? 0); + return number_format($released, 2, '.', ''); + } + + public function user() + { + return $this->belongsTo(UserModel::class, 'user_id', 'id')->field('id, nickname, avatar, mobile'); + } + + public function order() + { + return $this->belongsTo(OrderModel::class, 'order_id', 'id'); + } +} diff --git a/application/admin/model/shopro/user/WalletLog.php b/application/admin/model/shopro/user/WalletLog.php index 1608baa..9575b4e 100644 --- a/application/admin/model/shopro/user/WalletLog.php +++ b/application/admin/model/shopro/user/WalletLog.php @@ -49,7 +49,17 @@ class WalletLog extends Common 'withdraw_error' => '提现失败-返还佣金', 'reward_income' => '佣金-收益', 'reward_back' => '佣金-退还', - 'transfer_to_money' => '佣金-转出到余额' + 'transfer_to_money' => '佣金-转出到余额', + 'push3' => '推三返一', + 'maker_diff' => '创客极差奖', + 'maker_peer' => '创客平级奖', + 'maker_overtaken' => '创客超越奖', + 'team_venue' => '酒道馆团队奖', + 'team_county' => '区县团队奖', + 'venue_peer' => '酒道馆平级奖', + 'county_peer' => '区县平级奖', + 'venue_share' => '酒道馆平台均分', + 'county_share' => '区县平台均分' ] ]; diff --git a/application/admin/view/shopro/commission/agent/detail.html b/application/admin/view/shopro/commission/agent/detail.html index ddb346a..7caf1a6 100644 --- a/application/admin/view/shopro/commission/agent/detail.html +++ b/application/admin/view/shopro/commission/agent/detail.html @@ -211,7 +211,10 @@ 更换 - + +
已切换为合伙股东固定金额,旧百分比不再生效。
+
+
一级(自购)
@@ -250,6 +253,51 @@ {/if} + + {{ state.data.is_venue ? '是' : '否' }} + + + {{ state.data.is_county ? '是' : '否' }}{{ state.data.county_code ? '(' + state.data.county_code + ')' : '' }} + + + {if $auth->check('shopro/commission/agent/edit')} + 开通 / 调整身份 + {/if} + + + {{ state.data.identity_text || '-' }} + + + {{ state.data.active_status_text || (state.data.active_30d ? '正常领取' : '已暂停') }} + + + 第 {{ state.data.direct_order_seq || 0 }} 单,{{ state.data.next_push3_text || '' }} + + + {{ state.data.last_self_order_text || '从未下单' }} + + + {{ state.data.madow || 0 }}(约合 ¥{{ state.data.madow_value || '0.00' }}) + + + 总额 {{ state.data.voucher_total || 0 }} / 已释放 {{ state.data.voucher_released || 0 }} / + 已使用 {{ state.data.voucher_used || 0 }} / 可用 {{ state.data.voucher_available || 0 }} + + + {{ state.data.partner_cash_income || '0.00' }} + + + + TA 的收益流水 + + {if $auth->check('shopro/partner/reward/reissue')} + + 手工补发 / 扣回 + + {/if} +
@@ -598,4 +646,37 @@ + + + {{ identity.form.nickname || state.data.user_id }} + {{ identity.form.before_text || '-' }} + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/application/admin/view/shopro/commission/agent/index.html b/application/admin/view/shopro/commission/agent/index.html index d870a7b..1028afc 100644 --- a/application/admin/view/shopro/commission/agent/index.html +++ b/application/admin/view/shopro/commission/agent/index.html @@ -4,6 +4,19 @@ .agent-index .status-text { margin-right: 12px; } + .agent-index .active-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 6px; + } + .agent-index .active-dot.is-on { + background: #52c41a; + } + .agent-index .active-dot.is-off { + background: #c0c4cc; + }
@@ -44,6 +57,32 @@ + + + + + + + + + + + + @@ -71,7 +110,7 @@ - + @@ -201,4 +243,46 @@ + + + + {{ identity.form.nickname || identity.form.user_id }} + + + {{ identity.form.before_text || '-' }} + + + + + + + + + + + + + + +
编码:{{ identity.form.county_code || '-' }}
+
+ + + + + + +
+ +
\ No newline at end of file diff --git a/application/admin/view/shopro/commission/goods/index.html b/application/admin/view/shopro/commission/goods/index.html index 0d080b2..b73a984 100644 --- a/application/admin/view/shopro/commission/goods/index.html +++ b/application/admin/view/shopro/commission/goods/index.html @@ -35,7 +35,7 @@
-
分销商品
+
分销 / 合伙股东计奖商品
@@ -91,7 +91,7 @@ {{ scope.row.commission_goods?.status_text || '未参与' }} {if $auth->check('shopro/commission/goods/edit')} - 设置佣金 + 设置计奖 {/if}
diff --git a/application/admin/view/shopro/commission/level/add.html b/application/admin/view/shopro/commission/level/add.html index 8cf5029..d0b568a 100644 --- a/application/admin/view/shopro/commission/level/add.html +++ b/application/admin/view/shopro/commission/level/add.html @@ -71,7 +71,7 @@ diff --git a/application/admin/view/shopro/commission/level/index.html b/application/admin/view/shopro/commission/level/index.html index b245efa..53b8d69 100644 --- a/application/admin/view/shopro/commission/level/index.html +++ b/application/admin/view/shopro/commission/level/index.html @@ -6,9 +6,13 @@
分销商等级
+
+ 等级 1 固定为品酒师;2–7 对应创客 V1–V6。百分比佣金在合伙股东模式下不使用。 +
+ 去规则里改基数 {if $auth->check('shopro/commission/level/add')} 添加 {/if} @@ -33,6 +37,12 @@ + + + diff --git a/application/admin/view/shopro/config/index.html b/application/admin/view/shopro/config/index.html index ef09238..2cded2b 100644 --- a/application/admin/view/shopro/config/index.html +++ b/application/admin/view/shopro/config/index.html @@ -864,6 +864,114 @@ + + +