diff --git a/admin/src/views/admin/Distribution.vue b/admin/src/views/admin/Distribution.vue index 4e75d55..4159d6d 100644 --- a/admin/src/views/admin/Distribution.vue +++ b/admin/src/views/admin/Distribution.vue @@ -542,6 +542,7 @@ const promoCenterTitle = ref('推广中心') const testTypeItems = [ { key: 'face', label: '人脸分析' }, { key: 'mbti', label: 'MBTI 测试' }, + { key: 'sbti', label: 'SBTI 测试' }, { key: 'disc', label: 'DISC 测试' }, { key: 'pdp', label: 'PDP 测试' }, ] @@ -550,6 +551,7 @@ const makeDefaultTs = (): TestSetting => ({ enabled: true, commissionType: 'rati const testSettings = reactive>({ face: makeDefaultTs(), mbti: makeDefaultTs(), + sbti: makeDefaultTs(), disc: makeDefaultTs(), pdp: makeDefaultTs(), }) diff --git a/admin/src/views/superadmin/Distribution.vue b/admin/src/views/superadmin/Distribution.vue index 17146e1..5aa3cd4 100644 --- a/admin/src/views/superadmin/Distribution.vue +++ b/admin/src/views/superadmin/Distribution.vue @@ -342,6 +342,7 @@ const withdrawFee = ref(0) const testTypeItems = [ { key: 'face', label: '人脸分析' }, { key: 'mbti', label: 'MBTI 测试' }, + { key: 'sbti', label: 'SBTI 测试' }, { key: 'disc', label: 'DISC 测试' }, { key: 'pdp', label: 'PDP 测试' }, ] @@ -350,6 +351,7 @@ const makeDefaultTs = (): TestSetting => ({ enabled: true, commissionType: 'rati const testSettings = reactive>({ face: makeDefaultTs(), mbti: makeDefaultTs(), + sbti: makeDefaultTs(), disc: makeDefaultTs(), pdp: makeDefaultTs(), }) diff --git a/api/app/controller/admin/Distribution.php b/api/app/controller/admin/Distribution.php index bf78f61..612b60c 100644 --- a/api/app/controller/admin/Distribution.php +++ b/api/app/controller/admin/Distribution.php @@ -1,889 +1,892 @@ -where('id', (int) ($user['userId'] ?? 0))->find(); - $e2 = $adminRow['enterpriseId'] ?? null; - $enterpriseId = ($e2 !== null && $e2 !== '') ? (int) $e2 : 0; - } - - return $enterpriseId > 0 ? $enterpriseId : 0; - } - - /** - * 本企业维度的「推荐人 id」集合:佣金表 + 企业绑定表 + 历史仅填 agentId 的佣金行 - * - * @return int[] - */ - private function distributorUserIdsForEnterprise(int $enterpriseId): array - { - if ($enterpriseId <= 0) { - return []; - } - - $a = Db::name('commission_records') - ->where('enterpriseId', $enterpriseId) - ->where('inviterId', '>', 0) - ->distinct(true) - ->column('inviterId'); - $b = Db::name('distribution_bindings') - ->where('enterpriseId', $enterpriseId) - ->where('inviterId', '>', 0) - ->distinct(true) - ->column('inviterId'); - $c = Db::name('commission_records') - ->where('enterpriseId', $enterpriseId) - ->where(function ($q) { - $q->whereNull('inviterId')->whereOr('inviterId', 0); - }) - ->where('agentId', '>', 0) - ->distinct(true) - ->column('agentId'); - - $merged = array_merge($a ?: [], $b ?: [], $c ?: []); - - return array_values(array_unique(array_filter(array_map('intval', $merged), static function ($v) { - return $v > 0; - }))); - } - - // ───────────────────────────────────────────────────────────── - // GET distribution/overview - // ───────────────────────────────────────────────────────────── - public function overview() - { - $user = $this->request->user ?? null; - if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { - return error('无权限', 403); - } - - $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); - if ($enterpriseId <= 0) { - return error('未绑定企业或企业无效', 400); - } - $todayStart = strtotime(date('Y-m-d 00:00:00')); - $days = 7; - $trendStart = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days'))); - - try { - $query = Db::name('commission_records') - ->where('enterpriseId', $enterpriseId); - - $totalCommission = (clone $query)->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0; - $paidCommission = (clone $query)->where('status', 'paid')->sum('commissionFen') ?: 0; - $frozenCommission = (clone $query)->where('status', 'frozen')->sum('commissionFen') ?: 0; - $totalOrders = (clone $query)->whereIn('status', ['paid', 'frozen'])->count(); - $todayCommission = (clone $query) - ->whereIn('status', ['paid', 'frozen']) - ->where('createdAt', '>=', $todayStart) - ->sum('commissionFen') ?: 0; - $pendingCount = (clone $query)->where('status', 'frozen')->count(); - - $bindingQuery = Db::name('distribution_bindings') - ->where('enterpriseId', $enterpriseId) - ->where('status', 'active') - ->where('expireAt', '>', time()); - $bindingCount = (clone $bindingQuery)->count(); - $totalAgents = (clone $bindingQuery)->distinct(true)->count('inviterId'); - $todayAgents = Db::name('distribution_bindings') - ->where('enterpriseId', $enterpriseId) - ->where('createdAt', '>=', $todayStart) - ->distinct(true) - ->count('inviterId'); - - $trendRows = Db::name('commission_records') - ->where('enterpriseId', $enterpriseId) - ->whereIn('status', ['paid', 'frozen']) - ->where('createdAt', '>=', $trendStart) - ->field("FROM_UNIXTIME(createdAt, '%Y-%m-%d') as d, SUM(commissionFen) as totalFen") - ->group('d') - ->order('d', 'asc') - ->select() - ->toArray(); - $trendMap = []; - foreach ($trendRows as $row) { - $trendMap[$row['d']] = (int) ($row['totalFen'] ?? 0); - } - $commissionTrend = []; - for ($i = 0; $i < $days; $i++) { - $date = date('Y-m-d', strtotime('-' . ($days - 1 - $i) . ' days')); - $commissionTrend[] = [ - 'date' => $date, - 'amount' => round(($trendMap[$date] ?? 0) / 100, 2), - ]; - } - - $productSeries = self::buildProductCommissionSeries($enterpriseId); - - return success([ - 'totalAgents' => (int) $totalAgents, - 'todayAgents' => (int) $todayAgents, - 'totalCommission' => number_format($totalCommission / 100, 2, '.', ''), - 'todayCommission' => number_format($todayCommission / 100, 2, '.', ''), - 'pendingCommission' => number_format($frozenCommission / 100, 2, '.', ''), - 'pendingCount' => (int) $pendingCount, - 'paidCommission' => number_format($paidCommission / 100, 2, '.', ''), - 'frozenCommission' => number_format($frozenCommission / 100, 2, '.', ''), - 'totalOrders' => (int) $totalOrders, - 'bindingCount' => (int) $bindingCount, - 'commissionTrend' => $commissionTrend, - 'productCommissionSeries' => $productSeries, - ]); - } catch (\Exception $e) { - return error('获取数据失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // GET distribution/distributors 分销商列表(有过邀请行为的用户) - // ───────────────────────────────────────────────────────────── - public function distributors() - { - $user = $this->request->user ?? null; - if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { - return error('无权限', 403); - } - - $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); - if ($enterpriseId <= 0) { - return error('未绑定企业或企业无效', 400); - } - - $search = trim((string) Request::param('search', '')); - $page = max(1, (int) Request::param('page', 1)); - $pageSize = min(100, (int) Request::param('pageSize', 20)); - - try { - $poolIds = $this->distributorUserIdsForEnterprise($enterpriseId); - if (empty($poolIds)) { - return success(['list' => [], 'total' => 0, 'page' => $page, 'pageSize' => $pageSize]); - } - - $inviterQuery = Db::name('wechat_users') - ->alias('u') - ->whereIn('u.id', $poolIds) - ->field('u.id, u.nickname, u.avatar, u.createdAt'); - - if ($search !== '') { - $inviterQuery->where(function ($q) use ($search) { - $q->where('u.nickname', 'like', "%{$search}%") - ->whereOr('u.id', '=', is_numeric($search) ? (int) $search : -1); - }); - } - - $inviterQuery->order('u.id', 'desc'); - - $total = (int) (clone $inviterQuery)->count(); - $inviters = $inviterQuery->page($page, $pageSize)->select()->toArray(); - - $inviterIds = array_values(array_filter(array_map('intval', array_column($inviters, 'id')))); - - $commStats = []; - $withdrawnMap = []; - $teamMap = []; - - if (!empty($inviterIds)) { - $inStr = implode(',', $inviterIds); - $rows = Db::name('commission_records') - ->where('enterpriseId', $enterpriseId) - ->whereRaw('COALESCE(inviterId, agentId) IN (' . $inStr . ')') - ->fieldRaw("COALESCE(inviterId, agentId) AS inviterKey, SUM(IF(status IN ('paid','frozen'), commissionFen, 0)) AS totalFen, SUM(IF(status = 'paid', commissionFen, 0)) AS paidFen") - ->group('inviterKey') - ->select() - ->toArray(); - foreach ($rows as $r) { - $kid = (int) ($r['inviterKey'] ?? 0); - if ($kid > 0) { - $commStats[$kid] = $r; - } - } - - $withdrawnRows = Db::name('distribution_withdrawals') - ->alias('w') - ->join('wechat_users u', 'w.userId = u.id') - ->whereIn('w.userId', $inviterIds) - ->where('u.enterpriseId', $enterpriseId) - ->whereIn('w.status', [0, 2, 3]) - ->field('w.userId, SUM(w.amountFen) AS withdrawnFen') - ->group('w.userId') - ->select() - ->toArray(); - foreach ($withdrawnRows as $r) { - $withdrawnMap[(int) ($r['userId'] ?? 0)] = (int) ($r['withdrawnFen'] ?? 0); - } - - $now = time(); - $teamRows = Db::name('distribution_bindings') - ->alias('b') - ->leftJoin('wechat_users w', 'w.id = b.inviteeId') - ->whereIn('b.inviterId', $inviterIds) - ->where(function ($q) use ($enterpriseId) { - $q->where('b.enterpriseId', $enterpriseId) - ->whereOr(function ($q2) use ($enterpriseId) { - $q2->whereNull('b.enterpriseId')->where('w.enterpriseId', $enterpriseId); - }); - }) - ->where('b.status', 'active') - ->where('b.expireAt', '>', $now) - ->fieldRaw('b.inviterId, COUNT(DISTINCT b.inviteeId) AS teamCount') - ->group('b.inviterId') - ->select() - ->toArray(); - foreach ($teamRows as $r) { - $teamMap[(int) ($r['inviterId'] ?? 0)] = (int) ($r['teamCount'] ?? 0); - } - } - - $list = []; - foreach ($inviters as $inv) { - $uid = (int) ($inv['id'] ?? 0); - $totalFen = (int) ($commStats[$uid]['totalFen'] ?? 0); - $paidFen = (int) ($commStats[$uid]['paidFen'] ?? 0); - $withdrawn = (int) ($withdrawnMap[$uid] ?? 0); - $avail = max(0, $paidFen - $withdrawn); - $list[] = [ - 'id' => $uid, - 'agentName' => $inv['nickname'] ?: ('用户' . $uid), - 'avatar' => $inv['avatar'] ?? '', - 'totalCommission' => number_format($totalFen / 100, 2, '.', ''), - 'availableCommission'=> number_format($avail / 100, 2, '.', ''), - 'teamCount' => (int) ($teamMap[$uid] ?? 0), - 'teamPerformance' => '-', - 'inviteCode' => '-', - 'level' => '-', - 'createdAt' => $inv['createdAt'], - ]; - } - - return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); - } catch (\Exception $e) { - return error('获取分销商列表失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // GET distribution/bindings 绑定记录列表 - // ───────────────────────────────────────────────────────────── - public function bindings() - { - $user = $this->request->user ?? null; - if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { - return error('无权限', 403); - } - - $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); - if ($enterpriseId <= 0) { - return error('未绑定企业或企业无效', 400); - } - - $page = max(1, (int) Request::param('page', 1)); - $pageSize = min(100, (int) Request::param('pageSize', 20)); - $status = Request::param('status', ''); - $inviterId = (int) Request::param('inviterId', 0); - - try { - $query = Db::name('distribution_bindings') - ->alias('b') - ->leftJoin('wechat_users inv', 'b.inviterId = inv.id') - ->leftJoin('wechat_users invt', 'b.inviteeId = invt.id') - ->field('b.*, inv.nickname as inviterName, inv.avatar as inviterAvatar, - invt.nickname as inviteeName, invt.avatar as inviteeAvatar') - ->where(function ($q) use ($enterpriseId) { - $q->where('b.enterpriseId', $enterpriseId) - ->whereOr(function ($q2) use ($enterpriseId) { - $q2->whereNull('b.enterpriseId') - ->where(function ($q3) use ($enterpriseId) { - $q3->where('inv.enterpriseId', $enterpriseId) - ->whereOr('invt.enterpriseId', $enterpriseId); - }); - }); - }); - - if ($inviterId > 0) { - $query->where('b.inviterId', $inviterId); - } - if ($status) { - $query->where('b.status', $status); - } - - $total = (clone $query)->count(); - $list = $query->order('b.updatedAt', 'desc') - ->page($page, $pageSize) - ->select() - ->toArray(); - - $now = time(); - foreach ($list as &$row) { - $row['remainDays'] = max(0, (int) ceil(($row['expireAt'] - $now) / 86400)); - $row['inviterName'] = $row['inviterName'] ?: '未知'; - $row['inviteeName'] = $row['inviteeName'] ?: '未知'; - } - - return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); - } catch (\Exception $e) { - return error('获取绑定记录失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // GET distribution/commissions 佣金记录列表 - // ───────────────────────────────────────────────────────────── - public function commissions() - { - $user = $this->request->user ?? null; - if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { - return error('无权限', 403); - } - - $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); - if ($enterpriseId <= 0) { - return error('未绑定企业或企业无效', 400); - } - - $page = max(1, (int) Request::param('page', 1)); - $pageSize = min(100, (int) Request::param('pageSize', 20)); - $status = Request::param('status', ''); - $inviterId = (int) Request::param('inviterId', 0); - - try { - $query = Db::name('commission_records') - ->alias('c') - ->leftJoin('wechat_users inv', 'c.inviterId = inv.id') - ->leftJoin('wechat_users invt', 'c.inviteeId = invt.id') - ->field('c.*, inv.nickname as inviterName, inv.avatar as inviterAvatar, invt.nickname as inviteeName, invt.avatar as inviteeAvatar') - ->where('c.enterpriseId', $enterpriseId); - - if ($inviterId > 0) { - $query->whereRaw('COALESCE(c.inviterId, c.agentId) = ?', [$inviterId]); - } - if ($status) { - $query->where('c.status', $status); - } - - $total = (clone $query)->count(); - $list = $query->order('c.createdAt', 'desc') - ->page($page, $pageSize) - ->select() - ->toArray(); - - $orderIds = []; - $testResultIds = []; - foreach ($list as $row) { - if (!empty($row['orderId'])) { - $orderIds[] = (int) $row['orderId']; - } - if (!empty($row['testResultId'])) { - $testResultIds[] = (int) $row['testResultId']; - } - } - - $orderTypeMap = []; - if (!empty($orderIds)) { - $rows = Db::name('test_results') - ->whereIn('orderId', array_values(array_unique($orderIds))) - ->field('orderId, testType') - ->select() - ->toArray(); - foreach ($rows as $item) { - $orderTypeMap[(int) $item['orderId']] = self::normalizeTestType($item['testType'] ?? ''); - } - } - - $resultTypeMap = []; - if (!empty($testResultIds)) { - $rows = Db::name('test_results') - ->whereIn('id', array_values(array_unique($testResultIds))) - ->field('id, testType') - ->select() - ->toArray(); - foreach ($rows as $item) { - $resultTypeMap[(int) $item['id']] = self::normalizeTestType($item['testType'] ?? ''); - } - } - - foreach ($list as &$row) { - $testType = 'other'; - if (($row['commissionSource'] ?? '') === 'test_completion' && !empty($row['testResultId'])) { - $testType = $resultTypeMap[(int) $row['testResultId']] ?? 'other'; - } elseif (!empty($row['orderId'])) { - $testType = $orderTypeMap[(int) $row['orderId']] ?? 'other'; - } - - $row['testType'] = $testType; - $row['testTypeLabel'] = self::getTestTypeLabel($testType); - $row['commissionYuan'] = number_format($row['commissionFen'] / 100, 2, '.', ''); - $row['orderYuan'] = number_format($row['orderAmount'] / 100, 2, '.', ''); - } - - return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); - } catch (\Exception $e) { - return error('获取佣金记录失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // GET distribution/withdrawals 提现申请列表 - // ───────────────────────────────────────────────────────────── - public function withdrawals() - { - $user = $this->request->user ?? null; - if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { - return error('无权限', 403); - } - - $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); - if ($enterpriseId <= 0) { - return error('未绑定企业或企业无效', 400); - } - - $page = max(1, (int) Request::param('page', 1)); - $pageSize = min(100, (int) Request::param('pageSize', 20)); - $status = Request::param('status', ''); - - try { - $query = Db::name('distribution_withdrawals') - ->alias('w') - ->join('wechat_users u', 'w.userId = u.id') - ->field('w.*, u.nickname, u.avatar') - ->where('u.enterpriseId', $enterpriseId); - - if ($status !== '') { - // 后台传入可以是字符串或数字,这里统一转 int - $query->where('w.status', (int)$status); - } - - $total = (clone $query)->count(); - $list = $query->order('w.createdAt', 'desc') - ->page($page, $pageSize) - ->select() - ->toArray(); - - foreach ($list as &$row) { - $row['amountYuan'] = number_format($row['amountFen'] / 100, 2, '.', ''); - $row['nickname'] = $row['nickname'] ?: '未知用户'; - - // 确保前端拿到的是数字 status(避免 '0' 和 0 比较异常) - $code = (int) ($row['status'] ?? 0); - $row['status'] = $code; - - // 统一后台状态文案:0审核中、1已驳回、2待收款、3已收款、4已过期 - switch ($code) { - case 0: - $row['statusLabel'] = '审核中'; - break; - case 1: - $row['statusLabel'] = '已驳回'; - break; - case 2: - $row['statusLabel'] = '待收款'; - break; - case 3: - $row['statusLabel'] = '已收款'; - break; - case 4: - $row['statusLabel'] = '已过期'; - break; - default: - $row['statusLabel'] = '未知'; - break; - } - } - - return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); - } catch (\Exception $e) { - return error('获取提现记录失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // POST distribution/withdrawals/:id/approve 审核通过 - // ───────────────────────────────────────────────────────────── - public function approveWithdrawal(int $id) - { - $user = $this->request->user ?? null; - if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { - return error('无权限', 403); - } - - $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); - if ($enterpriseId <= 0) { - return error('未绑定企业或企业无效', 400); - } - - $note = Request::param('note', ''); - $now = time(); - - $record = Db::name('distribution_withdrawals') - ->alias('w') - ->join('wechat_users u', 'w.userId = u.id') - ->where('w.id', $id) - ->where('u.enterpriseId', $enterpriseId) - ->field('w.*, u.openid') - ->find(); - // 仅允许处理审核中(status=0)的记录 - if (!$record || (int)$record['status'] !== 0) { - return error('提现申请不存在、已处理或无权限', 400); - } - - try { - // 生成商户明细单号:TX + 时间戳 + 随机数(示例:TX202603121526520005) - $outDetailNo = 'TX' . date('YmdHis') . mt_rand(1000, 9999); - - // 调用微信商家转账到零钱接口 - $service = new \app\common\service\WechatTransferService(); - $result = $service->createTransfer([ - 'out_detail_no' => $outDetailNo, - 'transfer_amount'=> (int) $record['amountFen'], - 'transfer_remark'=> '推广佣金提现', - 'openid' => $record['openid'], - 'batch_name' => '推广佣金提现', - 'batch_remark' => '用户提现', - ]); - - if ($result['success'] !== true) { - $err = $result['error'] ?? []; - $code = $err['code'] ?? 'UNKNOWN'; - $msg = $err['message'] ?? '微信转账接口调用失败'; - return error("微信转账发起失败({$code}):{$msg}", 500); - } - - $wechatData = $result['data'] ?? []; - Db::name('distribution_withdrawals') - ->where('id', $id) - ->update([ - // 2=待收款(已发起微信转账,等待用户确认) - 'status' => 2, - 'auditNote' => $note, - 'auditAt' => $now, - 'updatedAt' => $now, - 'pay_type' => 'wechat', - 'out_bill_no' => $outDetailNo, - 'transfer_bill_no' => $wechatData['batch_id'] ?? null, - 'wechat_pay_state' => $wechatData['batch_status'] ?? 'PROCESSING', - 'transfer_scene_id'=> $wechatData['transfer_scene_id'] ?? env('TRANSFER_SCENE_ID', '1005'), - 'mch_id' => env('MCH_ID', null), - ]); - - return success(null, '审核通过,已发起微信转账'); - } catch (\Exception $e) { - return error('操作失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // POST distribution/withdrawals/:id/reject 审核拒绝 - // ───────────────────────────────────────────────────────────── - public function rejectWithdrawal(int $id) - { - $user = $this->request->user ?? null; - if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { - return error('无权限', 403); - } - - $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); - if ($enterpriseId <= 0) { - return error('未绑定企业或企业无效', 400); - } - - $note = Request::param('note', ''); - $now = time(); - - $record = Db::name('distribution_withdrawals') - ->alias('w') - ->join('wechat_users u', 'w.userId = u.id') - ->where('w.id', $id) - ->where('u.enterpriseId', $enterpriseId) - ->field('w.*') - ->find(); - // 仅允许处理审核中(status=0)的记录 - if (!$record || (int)$record['status'] !== 0) { - return error('提现申请不存在、已处理或无权限', 400); - } - - Db::startTrans(); - try { - // 退回余额 - Db::name('wechat_users') - ->where('id', $record['userId']) - ->inc('walletBalance', $record['amountFen']) - ->update(['updatedAt' => $now]); - - Db::name('distribution_withdrawals') - ->where('id', $id) - ->update([ - // 1=已驳回 - 'status' => 1, - 'auditNote' => $note, - 'auditAt' => $now, - 'updatedAt' => $now, - ]); - - Db::commit(); - return success(null, '已拒绝,余额已退回'); - } catch (\Exception $e) { - Db::rollback(); - return error('操作失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // GET distribution/settings 获取企业分销配置 - // ───────────────────────────────────────────────────────────── - public function settings() - { - $user = $this->request->user ?? null; - if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { - return error('无权限', 403); - } - - $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); - if ($enterpriseId <= 0) { - return error('未绑定企业或企业无效', 400); - } - - try { - $config = Db::name('system_config') - ->where('key', 'distribution') - ->where('enterprise_id', $enterpriseId) - ->find(); - - $tsDefault = self::defaultTestSettings(); - $default = [ - 'enabled' => true, - 'promoCenterTitle' => '推广中心', - 'bindingDays' => 30, - 'testSettings' => $tsDefault, - ]; - - if ($config && $config['value']) { - $settings = is_string($config['value']) ? json_decode($config['value'], true) : $config['value']; - $settings = array_merge($default, $settings ?? []); - } else { - $settings = $default; - } - - // 附加前端可读的 commissionAmount(元) - $settings['testSettings'] = self::appendTestSettingsAmount( - $settings['testSettings'] ?? $tsDefault - ); - - return success($settings); - } catch (\Exception $e) { - return error('获取配置失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // PUT distribution/settings 更新企业分销配置 - // ───────────────────────────────────────────────────────────── - public function updateSettings() - { - $user = $this->request->user ?? null; - if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { - return error('无权限', 403); - } - - $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); - if ($enterpriseId <= 0) { - return error('未绑定企业或企业无效', 400); - } - - $settings = Request::only(['enabled', 'promoCenterTitle', 'bindingDays', 'testSettings']); - - $promoTitle = trim((string)($settings['promoCenterTitle'] ?? '')); - $toSave = [ - 'enabled' => (bool)($settings['enabled'] ?? true), - 'promoCenterTitle' => $promoTitle !== '' ? $promoTitle : '推广中心', - 'bindingDays' => (int)($settings['bindingDays'] ?? 30), - 'testSettings' => self::sanitizeTestSettings($settings['testSettings'] ?? null), - ]; - - try { - $now = time(); - $existing = Db::name('system_config') - ->where('key', 'distribution') - ->where('enterprise_id', $enterpriseId) - ->find(); - - if ($existing) { - Db::name('system_config') - ->where('key', 'distribution') - ->where('enterprise_id', $enterpriseId) - ->update(['value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), 'updatedAt' => $now]); - } else { - Db::name('system_config')->insert([ - 'key' => 'distribution', - 'enterprise_id' => $enterpriseId, - 'value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), - 'createdAt' => $now, - 'updatedAt' => $now, - ]); - } - - $toSave['testSettings'] = self::appendTestSettingsAmount($toSave['testSettings']); - return success($toSave, '配置已保存'); - } catch (\Exception $e) { - return error('保存配置失败:' . $e->getMessage(), 500); - } - } - - private static function defaultTestSettings(): array - { - $item = ['enabled' => true, 'commissionType' => 'ratio', 'commissionRate' => 90, 'commissionAmountFen' => 0, 'noPayment' => false]; - return ['face' => $item, 'mbti' => $item, 'disc' => $item, 'pdp' => $item]; - } - - private static function sanitizeTestSettings($raw): array - { - $default = self::defaultTestSettings(); - if (!is_array($raw)) return $default; - $result = []; - foreach ($default as $type => $def) { - $s = $raw[$type] ?? []; - $commissionType = in_array($s['commissionType'] ?? '', ['ratio', 'amount']) ? $s['commissionType'] : 'ratio'; - $amountFen = isset($s['commissionAmount']) - ? (int) round((float)$s['commissionAmount'] * 100) - : (int)($s['commissionAmountFen'] ?? 0); - $rate = max(0, min(100, (int)($s['commissionRate'] ?? 90))); - $result[$type] = [ - 'enabled' => ($s['enabled'] ?? true) !== false, - 'commissionType' => $commissionType, - 'commissionRate' => $commissionType === 'ratio' ? $rate : 0, - 'commissionAmountFen'=> $commissionType === 'amount' ? max(0, $amountFen) : 0, - 'noPayment' => !empty($s['noPayment']), - ]; - } - return $result; - } - - private static function appendTestSettingsAmount(array $ts): array - { - foreach ($ts as $k => $v) { - $ts[$k]['commissionAmount'] = round(($v['commissionAmountFen'] ?? 0) / 100, 2); - } - return $ts; - } - - private static function buildProductCommissionSeries(int $enterpriseId): array - { - $records = Db::name('commission_records') - ->where('enterpriseId', $enterpriseId) - ->whereIn('status', ['paid', 'frozen']) - ->field('orderId, testResultId, commissionSource, commissionFen') - ->select() - ->toArray(); - - $orderIds = []; - $testResultIds = []; - foreach ($records as $record) { - if (!empty($record['orderId'])) { - $orderIds[] = (int) $record['orderId']; - } - if (!empty($record['testResultId'])) { - $testResultIds[] = (int) $record['testResultId']; - } - } - - $orderTypeMap = []; - if (!empty($orderIds)) { - $rows = Db::name('test_results') - ->whereIn('orderId', array_values(array_unique($orderIds))) - ->field('orderId, testType') - ->select() - ->toArray(); - foreach ($rows as $row) { - $orderTypeMap[(int) $row['orderId']] = self::normalizeTestType($row['testType'] ?? ''); - } - } - - $resultTypeMap = []; - if (!empty($testResultIds)) { - $rows = Db::name('test_results') - ->whereIn('id', array_values(array_unique($testResultIds))) - ->field('id, testType') - ->select() - ->toArray(); - foreach ($rows as $row) { - $resultTypeMap[(int) $row['id']] = self::normalizeTestType($row['testType'] ?? ''); - } - } - - $totals = [ - 'face' => 0, - 'mbti' => 0, - 'disc' => 0, - 'pdp' => 0, - 'other' => 0, - ]; - - foreach ($records as $record) { - $type = 'other'; - if (($record['commissionSource'] ?? '') === 'test_completion' && !empty($record['testResultId'])) { - $type = $resultTypeMap[(int) $record['testResultId']] ?? 'other'; - } elseif (!empty($record['orderId'])) { - $type = $orderTypeMap[(int) $record['orderId']] ?? 'other'; - } - - if (!isset($totals[$type])) { - $type = 'other'; - } - $totals[$type] += (int) ($record['commissionFen'] ?? 0); - } - - return [ - ['label' => '人脸分析', 'value' => round($totals['face'] / 100, 2)], - ['label' => 'MBTI', 'value' => round($totals['mbti'] / 100, 2)], - ['label' => 'DISC', 'value' => round($totals['disc'] / 100, 2)], - ['label' => 'PDP', 'value' => round($totals['pdp'] / 100, 2)], - ['label' => '其他', 'value' => round($totals['other'] / 100, 2)], - ]; - } - - private static function normalizeTestType(string $testType): string - { - $normalized = strtolower(trim($testType)); - if ($normalized === 'ai') { - return 'face'; - } - if (in_array($normalized, ['face', 'mbti', 'disc', 'pdp'], true)) { - return $normalized; - } - return 'other'; - } - - private static function getTestTypeLabel(string $testType): string - { - $map = [ - 'face' => '人脸', - 'mbti' => 'MBTI', - 'disc' => 'DISC', - 'pdp' => 'PDP', - 'other' => '其他', - ]; - - return $map[$testType] ?? strtoupper($testType ?: '其他'); - } -} +where('id', (int) ($user['userId'] ?? 0))->find(); + $e2 = $adminRow['enterpriseId'] ?? null; + $enterpriseId = ($e2 !== null && $e2 !== '') ? (int) $e2 : 0; + } + + return $enterpriseId > 0 ? $enterpriseId : 0; + } + + /** + * 本企业维度的「推荐人 id」集合:佣金表 + 企业绑定表 + 历史仅填 agentId 的佣金行 + * + * @return int[] + */ + private function distributorUserIdsForEnterprise(int $enterpriseId): array + { + if ($enterpriseId <= 0) { + return []; + } + + $a = Db::name('commission_records') + ->where('enterpriseId', $enterpriseId) + ->where('inviterId', '>', 0) + ->distinct(true) + ->column('inviterId'); + $b = Db::name('distribution_bindings') + ->where('enterpriseId', $enterpriseId) + ->where('inviterId', '>', 0) + ->distinct(true) + ->column('inviterId'); + $c = Db::name('commission_records') + ->where('enterpriseId', $enterpriseId) + ->where(function ($q) { + $q->whereNull('inviterId')->whereOr('inviterId', 0); + }) + ->where('agentId', '>', 0) + ->distinct(true) + ->column('agentId'); + + $merged = array_merge($a ?: [], $b ?: [], $c ?: []); + + return array_values(array_unique(array_filter(array_map('intval', $merged), static function ($v) { + return $v > 0; + }))); + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/overview + // ───────────────────────────────────────────────────────────── + public function overview() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); + if ($enterpriseId <= 0) { + return error('未绑定企业或企业无效', 400); + } + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $days = 7; + $trendStart = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days'))); + + try { + $query = Db::name('commission_records') + ->where('enterpriseId', $enterpriseId); + + $totalCommission = (clone $query)->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0; + $paidCommission = (clone $query)->where('status', 'paid')->sum('commissionFen') ?: 0; + $frozenCommission = (clone $query)->where('status', 'frozen')->sum('commissionFen') ?: 0; + $totalOrders = (clone $query)->whereIn('status', ['paid', 'frozen'])->count(); + $todayCommission = (clone $query) + ->whereIn('status', ['paid', 'frozen']) + ->where('createdAt', '>=', $todayStart) + ->sum('commissionFen') ?: 0; + $pendingCount = (clone $query)->where('status', 'frozen')->count(); + + $bindingQuery = Db::name('distribution_bindings') + ->where('enterpriseId', $enterpriseId) + ->where('status', 'active') + ->where('expireAt', '>', time()); + $bindingCount = (clone $bindingQuery)->count(); + $totalAgents = (clone $bindingQuery)->distinct(true)->count('inviterId'); + $todayAgents = Db::name('distribution_bindings') + ->where('enterpriseId', $enterpriseId) + ->where('createdAt', '>=', $todayStart) + ->distinct(true) + ->count('inviterId'); + + $trendRows = Db::name('commission_records') + ->where('enterpriseId', $enterpriseId) + ->whereIn('status', ['paid', 'frozen']) + ->where('createdAt', '>=', $trendStart) + ->field("FROM_UNIXTIME(createdAt, '%Y-%m-%d') as d, SUM(commissionFen) as totalFen") + ->group('d') + ->order('d', 'asc') + ->select() + ->toArray(); + $trendMap = []; + foreach ($trendRows as $row) { + $trendMap[$row['d']] = (int) ($row['totalFen'] ?? 0); + } + $commissionTrend = []; + for ($i = 0; $i < $days; $i++) { + $date = date('Y-m-d', strtotime('-' . ($days - 1 - $i) . ' days')); + $commissionTrend[] = [ + 'date' => $date, + 'amount' => round(($trendMap[$date] ?? 0) / 100, 2), + ]; + } + + $productSeries = self::buildProductCommissionSeries($enterpriseId); + + return success([ + 'totalAgents' => (int) $totalAgents, + 'todayAgents' => (int) $todayAgents, + 'totalCommission' => number_format($totalCommission / 100, 2, '.', ''), + 'todayCommission' => number_format($todayCommission / 100, 2, '.', ''), + 'pendingCommission' => number_format($frozenCommission / 100, 2, '.', ''), + 'pendingCount' => (int) $pendingCount, + 'paidCommission' => number_format($paidCommission / 100, 2, '.', ''), + 'frozenCommission' => number_format($frozenCommission / 100, 2, '.', ''), + 'totalOrders' => (int) $totalOrders, + 'bindingCount' => (int) $bindingCount, + 'commissionTrend' => $commissionTrend, + 'productCommissionSeries' => $productSeries, + ]); + } catch (\Exception $e) { + return error('获取数据失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/distributors 分销商列表(有过邀请行为的用户) + // ───────────────────────────────────────────────────────────── + public function distributors() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); + if ($enterpriseId <= 0) { + return error('未绑定企业或企业无效', 400); + } + + $search = trim((string) Request::param('search', '')); + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + + try { + $poolIds = $this->distributorUserIdsForEnterprise($enterpriseId); + if (empty($poolIds)) { + return success(['list' => [], 'total' => 0, 'page' => $page, 'pageSize' => $pageSize]); + } + + $inviterQuery = Db::name('wechat_users') + ->alias('u') + ->whereIn('u.id', $poolIds) + ->field('u.id, u.nickname, u.avatar, u.createdAt'); + + if ($search !== '') { + $inviterQuery->where(function ($q) use ($search) { + $q->where('u.nickname', 'like', "%{$search}%") + ->whereOr('u.id', '=', is_numeric($search) ? (int) $search : -1); + }); + } + + $inviterQuery->order('u.id', 'desc'); + + $total = (int) (clone $inviterQuery)->count(); + $inviters = $inviterQuery->page($page, $pageSize)->select()->toArray(); + + $inviterIds = array_values(array_filter(array_map('intval', array_column($inviters, 'id')))); + + $commStats = []; + $withdrawnMap = []; + $teamMap = []; + + if (!empty($inviterIds)) { + $inStr = implode(',', $inviterIds); + $rows = Db::name('commission_records') + ->where('enterpriseId', $enterpriseId) + ->whereRaw('COALESCE(inviterId, agentId) IN (' . $inStr . ')') + ->fieldRaw("COALESCE(inviterId, agentId) AS inviterKey, SUM(IF(status IN ('paid','frozen'), commissionFen, 0)) AS totalFen, SUM(IF(status = 'paid', commissionFen, 0)) AS paidFen") + ->group('inviterKey') + ->select() + ->toArray(); + foreach ($rows as $r) { + $kid = (int) ($r['inviterKey'] ?? 0); + if ($kid > 0) { + $commStats[$kid] = $r; + } + } + + $withdrawnRows = Db::name('distribution_withdrawals') + ->alias('w') + ->join('wechat_users u', 'w.userId = u.id') + ->whereIn('w.userId', $inviterIds) + ->where('u.enterpriseId', $enterpriseId) + ->whereIn('w.status', [0, 2, 3]) + ->field('w.userId, SUM(w.amountFen) AS withdrawnFen') + ->group('w.userId') + ->select() + ->toArray(); + foreach ($withdrawnRows as $r) { + $withdrawnMap[(int) ($r['userId'] ?? 0)] = (int) ($r['withdrawnFen'] ?? 0); + } + + $now = time(); + $teamRows = Db::name('distribution_bindings') + ->alias('b') + ->leftJoin('wechat_users w', 'w.id = b.inviteeId') + ->whereIn('b.inviterId', $inviterIds) + ->where(function ($q) use ($enterpriseId) { + $q->where('b.enterpriseId', $enterpriseId) + ->whereOr(function ($q2) use ($enterpriseId) { + $q2->whereNull('b.enterpriseId')->where('w.enterpriseId', $enterpriseId); + }); + }) + ->where('b.status', 'active') + ->where('b.expireAt', '>', $now) + ->fieldRaw('b.inviterId, COUNT(DISTINCT b.inviteeId) AS teamCount') + ->group('b.inviterId') + ->select() + ->toArray(); + foreach ($teamRows as $r) { + $teamMap[(int) ($r['inviterId'] ?? 0)] = (int) ($r['teamCount'] ?? 0); + } + } + + $list = []; + foreach ($inviters as $inv) { + $uid = (int) ($inv['id'] ?? 0); + $totalFen = (int) ($commStats[$uid]['totalFen'] ?? 0); + $paidFen = (int) ($commStats[$uid]['paidFen'] ?? 0); + $withdrawn = (int) ($withdrawnMap[$uid] ?? 0); + $avail = max(0, $paidFen - $withdrawn); + $list[] = [ + 'id' => $uid, + 'agentName' => $inv['nickname'] ?: ('用户' . $uid), + 'avatar' => $inv['avatar'] ?? '', + 'totalCommission' => number_format($totalFen / 100, 2, '.', ''), + 'availableCommission'=> number_format($avail / 100, 2, '.', ''), + 'teamCount' => (int) ($teamMap[$uid] ?? 0), + 'teamPerformance' => '-', + 'inviteCode' => '-', + 'level' => '-', + 'createdAt' => $inv['createdAt'], + ]; + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取分销商列表失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/bindings 绑定记录列表 + // ───────────────────────────────────────────────────────────── + public function bindings() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); + if ($enterpriseId <= 0) { + return error('未绑定企业或企业无效', 400); + } + + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + $status = Request::param('status', ''); + $inviterId = (int) Request::param('inviterId', 0); + + try { + $query = Db::name('distribution_bindings') + ->alias('b') + ->leftJoin('wechat_users inv', 'b.inviterId = inv.id') + ->leftJoin('wechat_users invt', 'b.inviteeId = invt.id') + ->field('b.*, inv.nickname as inviterName, inv.avatar as inviterAvatar, + invt.nickname as inviteeName, invt.avatar as inviteeAvatar') + ->where(function ($q) use ($enterpriseId) { + $q->where('b.enterpriseId', $enterpriseId) + ->whereOr(function ($q2) use ($enterpriseId) { + $q2->whereNull('b.enterpriseId') + ->where(function ($q3) use ($enterpriseId) { + $q3->where('inv.enterpriseId', $enterpriseId) + ->whereOr('invt.enterpriseId', $enterpriseId); + }); + }); + }); + + if ($inviterId > 0) { + $query->where('b.inviterId', $inviterId); + } + if ($status) { + $query->where('b.status', $status); + } + + $total = (clone $query)->count(); + $list = $query->order('b.updatedAt', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + $now = time(); + foreach ($list as &$row) { + $row['remainDays'] = max(0, (int) ceil(($row['expireAt'] - $now) / 86400)); + $row['inviterName'] = $row['inviterName'] ?: '未知'; + $row['inviteeName'] = $row['inviteeName'] ?: '未知'; + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取绑定记录失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/commissions 佣金记录列表 + // ───────────────────────────────────────────────────────────── + public function commissions() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); + if ($enterpriseId <= 0) { + return error('未绑定企业或企业无效', 400); + } + + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + $status = Request::param('status', ''); + $inviterId = (int) Request::param('inviterId', 0); + + try { + $query = Db::name('commission_records') + ->alias('c') + ->leftJoin('wechat_users inv', 'c.inviterId = inv.id') + ->leftJoin('wechat_users invt', 'c.inviteeId = invt.id') + ->field('c.*, inv.nickname as inviterName, inv.avatar as inviterAvatar, invt.nickname as inviteeName, invt.avatar as inviteeAvatar') + ->where('c.enterpriseId', $enterpriseId); + + if ($inviterId > 0) { + $query->whereRaw('COALESCE(c.inviterId, c.agentId) = ?', [$inviterId]); + } + if ($status) { + $query->where('c.status', $status); + } + + $total = (clone $query)->count(); + $list = $query->order('c.createdAt', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + $orderIds = []; + $testResultIds = []; + foreach ($list as $row) { + if (!empty($row['orderId'])) { + $orderIds[] = (int) $row['orderId']; + } + if (!empty($row['testResultId'])) { + $testResultIds[] = (int) $row['testResultId']; + } + } + + $orderTypeMap = []; + if (!empty($orderIds)) { + $rows = Db::name('test_results') + ->whereIn('orderId', array_values(array_unique($orderIds))) + ->field('orderId, testType') + ->select() + ->toArray(); + foreach ($rows as $item) { + $orderTypeMap[(int) $item['orderId']] = self::normalizeTestType($item['testType'] ?? ''); + } + } + + $resultTypeMap = []; + if (!empty($testResultIds)) { + $rows = Db::name('test_results') + ->whereIn('id', array_values(array_unique($testResultIds))) + ->field('id, testType') + ->select() + ->toArray(); + foreach ($rows as $item) { + $resultTypeMap[(int) $item['id']] = self::normalizeTestType($item['testType'] ?? ''); + } + } + + foreach ($list as &$row) { + $testType = 'other'; + if (($row['commissionSource'] ?? '') === 'test_completion' && !empty($row['testResultId'])) { + $testType = $resultTypeMap[(int) $row['testResultId']] ?? 'other'; + } elseif (!empty($row['orderId'])) { + $testType = $orderTypeMap[(int) $row['orderId']] ?? 'other'; + } + + $row['testType'] = $testType; + $row['testTypeLabel'] = self::getTestTypeLabel($testType); + $row['commissionYuan'] = number_format($row['commissionFen'] / 100, 2, '.', ''); + $row['orderYuan'] = number_format($row['orderAmount'] / 100, 2, '.', ''); + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取佣金记录失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/withdrawals 提现申请列表 + // ───────────────────────────────────────────────────────────── + public function withdrawals() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); + if ($enterpriseId <= 0) { + return error('未绑定企业或企业无效', 400); + } + + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + $status = Request::param('status', ''); + + try { + $query = Db::name('distribution_withdrawals') + ->alias('w') + ->join('wechat_users u', 'w.userId = u.id') + ->field('w.*, u.nickname, u.avatar') + ->where('u.enterpriseId', $enterpriseId); + + if ($status !== '') { + // 后台传入可以是字符串或数字,这里统一转 int + $query->where('w.status', (int)$status); + } + + $total = (clone $query)->count(); + $list = $query->order('w.createdAt', 'desc') + ->page($page, $pageSize) + ->select() + ->toArray(); + + foreach ($list as &$row) { + $row['amountYuan'] = number_format($row['amountFen'] / 100, 2, '.', ''); + $row['nickname'] = $row['nickname'] ?: '未知用户'; + + // 确保前端拿到的是数字 status(避免 '0' 和 0 比较异常) + $code = (int) ($row['status'] ?? 0); + $row['status'] = $code; + + // 统一后台状态文案:0审核中、1已驳回、2待收款、3已收款、4已过期 + switch ($code) { + case 0: + $row['statusLabel'] = '审核中'; + break; + case 1: + $row['statusLabel'] = '已驳回'; + break; + case 2: + $row['statusLabel'] = '待收款'; + break; + case 3: + $row['statusLabel'] = '已收款'; + break; + case 4: + $row['statusLabel'] = '已过期'; + break; + default: + $row['statusLabel'] = '未知'; + break; + } + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取提现记录失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // POST distribution/withdrawals/:id/approve 审核通过 + // ───────────────────────────────────────────────────────────── + public function approveWithdrawal(int $id) + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); + if ($enterpriseId <= 0) { + return error('未绑定企业或企业无效', 400); + } + + $note = Request::param('note', ''); + $now = time(); + + $record = Db::name('distribution_withdrawals') + ->alias('w') + ->join('wechat_users u', 'w.userId = u.id') + ->where('w.id', $id) + ->where('u.enterpriseId', $enterpriseId) + ->field('w.*, u.openid') + ->find(); + // 仅允许处理审核中(status=0)的记录 + if (!$record || (int)$record['status'] !== 0) { + return error('提现申请不存在、已处理或无权限', 400); + } + + try { + // 生成商户明细单号:TX + 时间戳 + 随机数(示例:TX202603121526520005) + $outDetailNo = 'TX' . date('YmdHis') . mt_rand(1000, 9999); + + // 调用微信商家转账到零钱接口 + $service = new \app\common\service\WechatTransferService(); + $result = $service->createTransfer([ + 'out_detail_no' => $outDetailNo, + 'transfer_amount'=> (int) $record['amountFen'], + 'transfer_remark'=> '推广佣金提现', + 'openid' => $record['openid'], + 'batch_name' => '推广佣金提现', + 'batch_remark' => '用户提现', + ]); + + if ($result['success'] !== true) { + $err = $result['error'] ?? []; + $code = $err['code'] ?? 'UNKNOWN'; + $msg = $err['message'] ?? '微信转账接口调用失败'; + return error("微信转账发起失败({$code}):{$msg}", 500); + } + + $wechatData = $result['data'] ?? []; + Db::name('distribution_withdrawals') + ->where('id', $id) + ->update([ + // 2=待收款(已发起微信转账,等待用户确认) + 'status' => 2, + 'auditNote' => $note, + 'auditAt' => $now, + 'updatedAt' => $now, + 'pay_type' => 'wechat', + 'out_bill_no' => $outDetailNo, + 'transfer_bill_no' => $wechatData['batch_id'] ?? null, + 'wechat_pay_state' => $wechatData['batch_status'] ?? 'PROCESSING', + 'transfer_scene_id'=> $wechatData['transfer_scene_id'] ?? env('TRANSFER_SCENE_ID', '1005'), + 'mch_id' => env('MCH_ID', null), + ]); + + return success(null, '审核通过,已发起微信转账'); + } catch (\Exception $e) { + return error('操作失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // POST distribution/withdrawals/:id/reject 审核拒绝 + // ───────────────────────────────────────────────────────────── + public function rejectWithdrawal(int $id) + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); + if ($enterpriseId <= 0) { + return error('未绑定企业或企业无效', 400); + } + + $note = Request::param('note', ''); + $now = time(); + + $record = Db::name('distribution_withdrawals') + ->alias('w') + ->join('wechat_users u', 'w.userId = u.id') + ->where('w.id', $id) + ->where('u.enterpriseId', $enterpriseId) + ->field('w.*') + ->find(); + // 仅允许处理审核中(status=0)的记录 + if (!$record || (int)$record['status'] !== 0) { + return error('提现申请不存在、已处理或无权限', 400); + } + + Db::startTrans(); + try { + // 退回余额 + Db::name('wechat_users') + ->where('id', $record['userId']) + ->inc('walletBalance', $record['amountFen']) + ->update(['updatedAt' => $now]); + + Db::name('distribution_withdrawals') + ->where('id', $id) + ->update([ + // 1=已驳回 + 'status' => 1, + 'auditNote' => $note, + 'auditAt' => $now, + 'updatedAt' => $now, + ]); + + Db::commit(); + return success(null, '已拒绝,余额已退回'); + } catch (\Exception $e) { + Db::rollback(); + return error('操作失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/settings 获取企业分销配置 + // ───────────────────────────────────────────────────────────── + public function settings() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); + if ($enterpriseId <= 0) { + return error('未绑定企业或企业无效', 400); + } + + try { + $config = Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', $enterpriseId) + ->find(); + + $tsDefault = self::defaultTestSettings(); + $default = [ + 'enabled' => true, + 'promoCenterTitle' => '推广中心', + 'bindingDays' => 30, + 'testSettings' => $tsDefault, + ]; + + if ($config && $config['value']) { + $settings = is_string($config['value']) ? json_decode($config['value'], true) : $config['value']; + $settings = array_merge($default, $settings ?? []); + } else { + $settings = $default; + } + + // 附加前端可读的 commissionAmount(元);合并默认键,兼容旧库缺少 sbti 等字段 + $settings['testSettings'] = self::appendTestSettingsAmount( + array_merge($tsDefault, $settings['testSettings'] ?? []) + ); + + return success($settings); + } catch (\Exception $e) { + return error('获取配置失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // PUT distribution/settings 更新企业分销配置 + // ───────────────────────────────────────────────────────────── + public function updateSettings() + { + $user = $this->request->user ?? null; + if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) { + return error('无权限', 403); + } + + $enterpriseId = $this->resolveEnterpriseIdForAdmin($user); + if ($enterpriseId <= 0) { + return error('未绑定企业或企业无效', 400); + } + + $settings = Request::only(['enabled', 'promoCenterTitle', 'bindingDays', 'testSettings']); + + $promoTitle = trim((string)($settings['promoCenterTitle'] ?? '')); + $toSave = [ + 'enabled' => (bool)($settings['enabled'] ?? true), + 'promoCenterTitle' => $promoTitle !== '' ? $promoTitle : '推广中心', + 'bindingDays' => (int)($settings['bindingDays'] ?? 30), + 'testSettings' => self::sanitizeTestSettings($settings['testSettings'] ?? null), + ]; + + try { + $now = time(); + $existing = Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', $enterpriseId) + ->find(); + + if ($existing) { + Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', $enterpriseId) + ->update(['value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), 'updatedAt' => $now]); + } else { + Db::name('system_config')->insert([ + 'key' => 'distribution', + 'enterprise_id' => $enterpriseId, + 'value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + } + + $toSave['testSettings'] = self::appendTestSettingsAmount($toSave['testSettings']); + return success($toSave, '配置已保存'); + } catch (\Exception $e) { + return error('保存配置失败:' . $e->getMessage(), 500); + } + } + + private static function defaultTestSettings(): array + { + $item = ['enabled' => true, 'commissionType' => 'ratio', 'commissionRate' => 90, 'commissionAmountFen' => 0, 'noPayment' => false]; + return ['face' => $item, 'mbti' => $item, 'sbti' => $item, 'disc' => $item, 'pdp' => $item]; + } + + private static function sanitizeTestSettings($raw): array + { + $default = self::defaultTestSettings(); + if (!is_array($raw)) return $default; + $result = []; + foreach ($default as $type => $def) { + $s = $raw[$type] ?? []; + $commissionType = in_array($s['commissionType'] ?? '', ['ratio', 'amount']) ? $s['commissionType'] : 'ratio'; + $amountFen = isset($s['commissionAmount']) + ? (int) round((float)$s['commissionAmount'] * 100) + : (int)($s['commissionAmountFen'] ?? 0); + $rate = max(0, min(100, (int)($s['commissionRate'] ?? 90))); + $result[$type] = [ + 'enabled' => ($s['enabled'] ?? true) !== false, + 'commissionType' => $commissionType, + 'commissionRate' => $commissionType === 'ratio' ? $rate : 0, + 'commissionAmountFen'=> $commissionType === 'amount' ? max(0, $amountFen) : 0, + 'noPayment' => !empty($s['noPayment']), + ]; + } + return $result; + } + + private static function appendTestSettingsAmount(array $ts): array + { + foreach ($ts as $k => $v) { + $ts[$k]['commissionAmount'] = round(($v['commissionAmountFen'] ?? 0) / 100, 2); + } + return $ts; + } + + private static function buildProductCommissionSeries(int $enterpriseId): array + { + $records = Db::name('commission_records') + ->where('enterpriseId', $enterpriseId) + ->whereIn('status', ['paid', 'frozen']) + ->field('orderId, testResultId, commissionSource, commissionFen') + ->select() + ->toArray(); + + $orderIds = []; + $testResultIds = []; + foreach ($records as $record) { + if (!empty($record['orderId'])) { + $orderIds[] = (int) $record['orderId']; + } + if (!empty($record['testResultId'])) { + $testResultIds[] = (int) $record['testResultId']; + } + } + + $orderTypeMap = []; + if (!empty($orderIds)) { + $rows = Db::name('test_results') + ->whereIn('orderId', array_values(array_unique($orderIds))) + ->field('orderId, testType') + ->select() + ->toArray(); + foreach ($rows as $row) { + $orderTypeMap[(int) $row['orderId']] = self::normalizeTestType($row['testType'] ?? ''); + } + } + + $resultTypeMap = []; + if (!empty($testResultIds)) { + $rows = Db::name('test_results') + ->whereIn('id', array_values(array_unique($testResultIds))) + ->field('id, testType') + ->select() + ->toArray(); + foreach ($rows as $row) { + $resultTypeMap[(int) $row['id']] = self::normalizeTestType($row['testType'] ?? ''); + } + } + + $totals = [ + 'face' => 0, + 'mbti' => 0, + 'sbti' => 0, + 'disc' => 0, + 'pdp' => 0, + 'other' => 0, + ]; + + foreach ($records as $record) { + $type = 'other'; + if (($record['commissionSource'] ?? '') === 'test_completion' && !empty($record['testResultId'])) { + $type = $resultTypeMap[(int) $record['testResultId']] ?? 'other'; + } elseif (!empty($record['orderId'])) { + $type = $orderTypeMap[(int) $record['orderId']] ?? 'other'; + } + + if (!isset($totals[$type])) { + $type = 'other'; + } + $totals[$type] += (int) ($record['commissionFen'] ?? 0); + } + + return [ + ['label' => '人脸分析', 'value' => round($totals['face'] / 100, 2)], + ['label' => 'MBTI', 'value' => round($totals['mbti'] / 100, 2)], + ['label' => 'SBTI', 'value' => round($totals['sbti'] / 100, 2)], + ['label' => 'DISC', 'value' => round($totals['disc'] / 100, 2)], + ['label' => 'PDP', 'value' => round($totals['pdp'] / 100, 2)], + ['label' => '其他', 'value' => round($totals['other'] / 100, 2)], + ]; + } + + private static function normalizeTestType(string $testType): string + { + $normalized = strtolower(trim($testType)); + if ($normalized === 'ai') { + return 'face'; + } + if (in_array($normalized, ['face', 'mbti', 'sbti', 'disc', 'pdp'], true)) { + return $normalized; + } + return 'other'; + } + + private static function getTestTypeLabel(string $testType): string + { + $map = [ + 'face' => '人脸', + 'mbti' => 'MBTI', + 'sbti' => 'SBTI', + 'disc' => 'DISC', + 'pdp' => 'PDP', + 'other' => '其他', + ]; + + return $map[$testType] ?? strtoupper($testType ?: '其他'); + } +} diff --git a/api/app/controller/superadmin/Distribution.php b/api/app/controller/superadmin/Distribution.php index d566602..8493a6f 100644 --- a/api/app/controller/superadmin/Distribution.php +++ b/api/app/controller/superadmin/Distribution.php @@ -1,433 +1,435 @@ -whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0; - $paidCommission = Db::name('commission_records')->where('status', 'paid')->sum('commissionFen') ?: 0; - $frozenCommission = Db::name('commission_records')->where('status', 'frozen')->sum('commissionFen') ?: 0; - $totalOrders = Db::name('commission_records')->whereIn('status', ['paid', 'frozen'])->count(); - - $personalCommission = Db::name('commission_records')->where('scope', 'personal') - ->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0; - $enterpriseCommission = Db::name('commission_records')->where('scope', 'enterprise') - ->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0; - - $bindingCount = Db::name('distribution_bindings') - ->where('status', 'active') - ->where('expireAt', '>', $now) - ->count(); - - // 待处理提现:status=0 审核中 - $pendingWithdraw = Db::name('distribution_withdrawals') - ->where('status', 0) - ->sum('amountFen') ?: 0; - - $todayStart = strtotime(date('Y-m-d 00:00:00')); - $todayCommission = Db::name('commission_records') - ->where('status', 'paid') - ->where('paidAt', '>=', $todayStart) - ->sum('commissionFen') ?: 0; - - return success([ - 'totalCommission' => number_format($totalCommission / 100, 2, '.', ''), - 'paidCommission' => number_format($paidCommission / 100, 2, '.', ''), - 'frozenCommission' => number_format($frozenCommission / 100, 2, '.', ''), - 'personalCommission' => number_format($personalCommission / 100, 2, '.', ''), - 'enterpriseCommission'=> number_format($enterpriseCommission / 100, 2, '.', ''), - 'totalOrders' => $totalOrders, - 'bindingCount' => $bindingCount, - 'pendingWithdraw' => number_format($pendingWithdraw / 100, 2, '.', ''), - 'todayCommission' => number_format($todayCommission / 100, 2, '.', ''), - ]); - } catch (\Exception $e) { - return error('获取数据失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // GET distribution/bindings 全平台绑定记录 - // ───────────────────────────────────────────────────────────── - public function bindings() - { - $page = max(1, (int) Request::param('page', 1)); - $pageSize = min(100, (int) Request::param('pageSize', 20)); - $scope = Request::param('scope', ''); - $status = Request::param('status', ''); - $enterpriseId = (int) Request::param('enterpriseId', 0); - - try { - $query = Db::name('distribution_bindings') - ->alias('b') - ->leftJoin('wechat_users inv', 'b.inviterId = inv.id') - ->leftJoin('wechat_users invt', 'b.inviteeId = invt.id') - ->leftJoin('enterprises e', 'b.enterpriseId = e.id') - ->field('b.*, inv.nickname as inviterName, invt.nickname as inviteeName, e.name as enterpriseName'); - - if ($scope) $query->where('b.scope', $scope); - if ($status) $query->where('b.status', $status); - if ($enterpriseId) $query->where('b.enterpriseId', $enterpriseId); - - $total = (clone $query)->count(); - $list = $query->order('b.updatedAt', 'desc')->page($page, $pageSize)->select()->toArray(); - - $now = time(); - foreach ($list as &$row) { - $row['remainDays'] = max(0, (int) ceil(($row['expireAt'] - $now) / 86400)); - $row['inviterName'] = $row['inviterName'] ?: '未知'; - $row['inviteeName'] = $row['inviteeName'] ?: '未知'; - } - - return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); - } catch (\Exception $e) { - return error('获取绑定记录失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // GET distribution/commissions 全平台佣金记录 - // ───────────────────────────────────────────────────────────── - public function commissions() - { - $page = max(1, (int) Request::param('page', 1)); - $pageSize = min(100, (int) Request::param('pageSize', 20)); - $scope = Request::param('scope', ''); - $status = Request::param('status', ''); - - try { - $query = Db::name('commission_records') - ->alias('c') - ->leftJoin('wechat_users inv', 'c.inviterId = inv.id') - ->leftJoin('wechat_users invt', 'c.inviteeId = invt.id') - ->leftJoin('enterprises e', 'c.enterpriseId = e.id') - ->field('c.*, inv.nickname as inviterName, invt.nickname as inviteeName, e.name as enterpriseName'); - - if ($scope) $query->where('c.scope', $scope); - if ($status) $query->where('c.status', $status); - - $total = (clone $query)->count(); - $list = $query->order('c.createdAt', 'desc')->page($page, $pageSize)->select()->toArray(); - - foreach ($list as &$row) { - $row['commissionYuan'] = number_format($row['commissionFen'] / 100, 2, '.', ''); - $row['orderYuan'] = number_format($row['orderAmount'] / 100, 2, '.', ''); - } - - return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); - } catch (\Exception $e) { - return error('获取佣金记录失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // GET distribution/withdrawals 全平台提现申请 - // ───────────────────────────────────────────────────────────── - public function withdrawals() - { - $page = max(1, (int) Request::param('page', 1)); - $pageSize = min(100, (int) Request::param('pageSize', 20)); - $status = Request::param('status', ''); - - try { - $query = Db::name('distribution_withdrawals') - ->alias('w') - ->leftJoin('wechat_users u', 'w.userId = u.id') - ->field('w.*, u.nickname, u.avatar'); - - if ($status !== '') { - // 支持字符串或数字,统一转 int - $query->where('w.status', (int)$status); - } - - $total = (clone $query)->count(); - $list = $query->order('w.createdAt', 'desc')->page($page, $pageSize)->select()->toArray(); - - foreach ($list as &$row) { - $row['amountYuan'] = number_format($row['amountFen'] / 100, 2, '.', ''); - $row['nickname'] = $row['nickname'] ?: '未知用户'; - - // 确保前端拿到的是数字 status(避免 '0' 和 0 比较异常) - $code = (int) ($row['status'] ?? 0); - $row['status'] = $code; - - // 统一后台状态文案:0审核中、1已驳回、2待收款、3已收款、4已过期 - switch ($code) { - case 0: - $row['statusLabel'] = '审核中'; - break; - case 1: - $row['statusLabel'] = '已驳回'; - break; - case 2: - $row['statusLabel'] = '待收款'; - break; - case 3: - $row['statusLabel'] = '已收款'; - break; - case 4: - $row['statusLabel'] = '已过期'; - break; - default: - $row['statusLabel'] = '未知'; - break; - } - } - - return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); - } catch (\Exception $e) { - return error('获取提现记录失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // POST distribution/withdrawals/:id/approve 审核通过提现 - // ───────────────────────────────────────────────────────────── - public function approveWithdrawal(int $id) - { - $note = Request::param('note', ''); - $now = time(); - - $record = Db::name('distribution_withdrawals') - ->alias('w') - ->leftJoin('wechat_users u', 'w.userId = u.id') - ->field('w.*, u.openid') - ->where('w.id', $id) - ->find(); - // 仅允许处理审核中(status=0)的记录 - if (!$record || (int)$record['status'] !== 0) { - return error('提现申请不存在或已处理', 400); - } - - try { - // 生成商户单号:TX + 时间戳 + 随机数 + 提现ID(示例:TX202603121526520005123) - $outBillNo = 'TX' . date('YmdHis') . mt_rand(1000, 9999) . $record['id']; - - // 调用微信商家转账到零钱接口(参数对齐 ckb-admin Withdrawal::handleWechatPay) - $service = new \app\common\service\WechatTransferService(); - $result = $service->createTransfer([ - 'out_bill_no' => $outBillNo, - 'openid' => $record['openid'], - 'transfer_amount' => (int) $record['amountFen'], // 单位:分 - 'transfer_remark' => '推广佣金提现', - 'transfer_scene_id' => env('TRANSFER_SCENE_ID', '1005'), - 'transfer_scene_report_infos' => [ - [ - 'info_type' => '岗位类型', - 'info_content' => '推广人员', - ], - [ - 'info_type' => '报酬说明', - 'info_content' => '推广佣金提现', - ], - ], - 'notify_url' => env('WITHDRAW_NOTIFY_URL', ''), // 可选:提现专用回调 - ]); - - if ($result['success'] !== true) { - $err = $result['error'] ?? []; - $code = $err['code'] ?? 'UNKNOWN'; - $msg = $err['message'] ?? '微信转账接口调用失败'; - return error("微信转账发起失败({$code}):{$msg}", 500); - } - - $wechatData = $result['data'] ?? []; - Db::name('distribution_withdrawals')->where('id', $id)->update([ - // 2=待收款(已发起转账,等待用户确认) - 'status' => 2, - 'auditNote' => $note, - 'auditAt' => $now, - 'updatedAt' => $now, - 'pay_type' => 'wechat', - 'out_bill_no' => $outBillNo, - 'transfer_bill_no' => $wechatData['transfer_bill_no'] ?? null, - 'wechat_pay_state' => $wechatData['state'] ?? 'PROCESSING', - 'transfer_scene_id'=> $wechatData['transfer_scene_id'] ?? env('TRANSFER_SCENE_ID', '1005'), - 'package_info' => $wechatData['package_info'] ?? '', - 'mch_id' => env('MCH_ID', null), - ]); - return success(null, '审核通过,已发起微信转账'); - } catch (\Exception $e) { - return error('操作失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // POST distribution/withdrawals/:id/reject 拒绝提现 - // ───────────────────────────────────────────────────────────── - public function rejectWithdrawal(int $id) - { - $note = Request::param('note', ''); - $now = time(); - - $record = Db::name('distribution_withdrawals')->where('id', $id)->find(); - // 仅允许处理审核中(status=0)的记录 - if (!$record || (int)$record['status'] !== 0) { - return error('提现申请不存在或已处理', 400); - } - - Db::startTrans(); - try { - Db::name('wechat_users') - ->where('id', $record['userId']) - ->inc('walletBalance', $record['amountFen']) - ->update(['updatedAt' => $now]); - - Db::name('distribution_withdrawals')->where('id', $id)->update([ - // 1=已驳回 - 'status' => 1, - 'auditNote' => $note, - 'auditAt' => $now, - 'updatedAt' => $now, - ]); - - Db::commit(); - return success(null, '已拒绝,余额已退回'); - } catch (\Exception $e) { - Db::rollback(); - return error('操作失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // GET distribution/settings 个人版分销全局配置 - // ───────────────────────────────────────────────────────────── - public function settings() - { - try { - $config = Db::name('system_config')->where('key', 'distribution')->where('enterprise_id', 0)->find(); - $default = [ - 'enabled' => true, - 'promoCenterTitle' => '推广中心', - 'bindingDays' => 30, - 'minWithdrawFen' => 100, - 'maxWithdrawFen' => 0, - 'requireAudit' => true, - 'withdrawFee' => 0, - 'testSettings' => self::defaultTestSettings(), - ]; - if ($config && $config['value']) { - $settings = is_string($config['value']) ? json_decode($config['value'], true) : $config['value']; - $settings = array_merge($default, $settings ?? []); - } else { - $settings = $default; - } - $settings['minWithdraw'] = round((float)($settings['minWithdrawFen'] ?? 100) / 100, 2); - $settings['maxWithdraw'] = ($max = (int)($settings['maxWithdrawFen'] ?? 0)) > 0 ? round($max / 100, 2) : 0; - $settings['testSettings'] = self::appendTestSettingsAmount($settings['testSettings'] ?? self::defaultTestSettings()); - return success($settings); - } catch (\Exception $e) { - return error('获取配置失败:' . $e->getMessage(), 500); - } - } - - // ───────────────────────────────────────────────────────────── - // PUT distribution/settings 更新个人版分销全局配置 - // ───────────────────────────────────────────────────────────── - public function updateSettings() - { - $settings = Request::only([ - 'enabled', 'promoCenterTitle', 'bindingDays', - 'minWithdrawFen', 'minWithdraw', 'maxWithdrawFen', 'maxWithdraw', - 'requireAudit', 'withdrawFee', 'testSettings' - ]); - - $minWithdrawFen = isset($settings['minWithdraw']) - ? (int) round((float)$settings['minWithdraw'] * 100) - : (int)($settings['minWithdrawFen'] ?? 100); - $maxWithdrawFen = isset($settings['maxWithdraw']) - ? (int) round((float)$settings['maxWithdraw'] * 100) - : (int)($settings['maxWithdrawFen'] ?? 0); - $minWithdrawFen = max(100, min(20000, $minWithdrawFen)); - $maxWithdrawFen = $maxWithdrawFen > 0 ? min(20000, max(100, $maxWithdrawFen)) : 0; - - $promoTitle = trim((string)($settings['promoCenterTitle'] ?? '')); - $toSave = [ - 'enabled' => (bool)($settings['enabled'] ?? true), - 'promoCenterTitle' => $promoTitle !== '' ? $promoTitle : '推广中心', - 'bindingDays' => (int)($settings['bindingDays'] ?? 30), - 'minWithdrawFen' => $minWithdrawFen, - 'maxWithdrawFen' => $maxWithdrawFen, - 'requireAudit' => isset($settings['requireAudit']) ? (bool)$settings['requireAudit'] : true, - 'withdrawFee' => max(0, min(100, (float)($settings['withdrawFee'] ?? 0))), - 'testSettings' => self::sanitizeTestSettings($settings['testSettings'] ?? null), - ]; - - try { - $now = time(); - $existing = Db::name('system_config')->where('key', 'distribution')->where('enterprise_id', 0)->find(); - if ($existing) { - Db::name('system_config') - ->where('key', 'distribution') - ->where('enterprise_id', 0) - ->update(['value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), 'updatedAt' => $now]); - } else { - Db::name('system_config')->insert([ - 'key' => 'distribution', - 'enterprise_id' => 0, - 'value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), - 'createdAt' => $now, - 'updatedAt' => $now, - ]); - } - $toSave['minWithdraw'] = $toSave['minWithdrawFen'] / 100; - $toSave['maxWithdraw'] = $toSave['maxWithdrawFen'] > 0 ? $toSave['maxWithdrawFen'] / 100 : 0; - $toSave['testSettings'] = self::appendTestSettingsAmount($toSave['testSettings']); - return success($toSave, '配置已保存'); - } catch (\Exception $e) { - return error('保存配置失败:' . $e->getMessage(), 500); - } - } - - private static function defaultTestSettings(): array - { - $item = ['enabled' => true, 'commissionType' => 'ratio', 'commissionRate' => 90, 'commissionAmountFen' => 0, 'noPayment' => false]; - return ['face' => $item, 'mbti' => $item, 'disc' => $item, 'pdp' => $item]; - } - - private static function sanitizeTestSettings($raw): array - { - $default = self::defaultTestSettings(); - if (!is_array($raw)) return $default; - $result = []; - foreach ($default as $type => $def) { - $s = $raw[$type] ?? []; - $commissionType = in_array($s['commissionType'] ?? '', ['ratio', 'amount']) ? $s['commissionType'] : 'ratio'; - $amountFen = isset($s['commissionAmount']) - ? (int) round((float)$s['commissionAmount'] * 100) - : (int)($s['commissionAmountFen'] ?? 0); - $rate = max(0, min(100, (int)($s['commissionRate'] ?? 90))); - $result[$type] = [ - 'enabled' => ($s['enabled'] ?? true) !== false, - 'commissionType' => $commissionType, - 'commissionRate' => $commissionType === 'ratio' ? $rate : 0, - 'commissionAmountFen'=> $commissionType === 'amount' ? max(0, $amountFen) : 0, - 'noPayment' => !empty($s['noPayment']), - ]; - } - return $result; - } - - private static function appendTestSettingsAmount(array $ts): array - { - foreach ($ts as $k => $v) { - $ts[$k]['commissionAmount'] = round(($v['commissionAmountFen'] ?? 0) / 100, 2); - } - return $ts; - } -} +whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0; + $paidCommission = Db::name('commission_records')->where('status', 'paid')->sum('commissionFen') ?: 0; + $frozenCommission = Db::name('commission_records')->where('status', 'frozen')->sum('commissionFen') ?: 0; + $totalOrders = Db::name('commission_records')->whereIn('status', ['paid', 'frozen'])->count(); + + $personalCommission = Db::name('commission_records')->where('scope', 'personal') + ->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0; + $enterpriseCommission = Db::name('commission_records')->where('scope', 'enterprise') + ->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0; + + $bindingCount = Db::name('distribution_bindings') + ->where('status', 'active') + ->where('expireAt', '>', $now) + ->count(); + + // 待处理提现:status=0 审核中 + $pendingWithdraw = Db::name('distribution_withdrawals') + ->where('status', 0) + ->sum('amountFen') ?: 0; + + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayCommission = Db::name('commission_records') + ->where('status', 'paid') + ->where('paidAt', '>=', $todayStart) + ->sum('commissionFen') ?: 0; + + return success([ + 'totalCommission' => number_format($totalCommission / 100, 2, '.', ''), + 'paidCommission' => number_format($paidCommission / 100, 2, '.', ''), + 'frozenCommission' => number_format($frozenCommission / 100, 2, '.', ''), + 'personalCommission' => number_format($personalCommission / 100, 2, '.', ''), + 'enterpriseCommission'=> number_format($enterpriseCommission / 100, 2, '.', ''), + 'totalOrders' => $totalOrders, + 'bindingCount' => $bindingCount, + 'pendingWithdraw' => number_format($pendingWithdraw / 100, 2, '.', ''), + 'todayCommission' => number_format($todayCommission / 100, 2, '.', ''), + ]); + } catch (\Exception $e) { + return error('获取数据失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/bindings 全平台绑定记录 + // ───────────────────────────────────────────────────────────── + public function bindings() + { + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + $scope = Request::param('scope', ''); + $status = Request::param('status', ''); + $enterpriseId = (int) Request::param('enterpriseId', 0); + + try { + $query = Db::name('distribution_bindings') + ->alias('b') + ->leftJoin('wechat_users inv', 'b.inviterId = inv.id') + ->leftJoin('wechat_users invt', 'b.inviteeId = invt.id') + ->leftJoin('enterprises e', 'b.enterpriseId = e.id') + ->field('b.*, inv.nickname as inviterName, invt.nickname as inviteeName, e.name as enterpriseName'); + + if ($scope) $query->where('b.scope', $scope); + if ($status) $query->where('b.status', $status); + if ($enterpriseId) $query->where('b.enterpriseId', $enterpriseId); + + $total = (clone $query)->count(); + $list = $query->order('b.updatedAt', 'desc')->page($page, $pageSize)->select()->toArray(); + + $now = time(); + foreach ($list as &$row) { + $row['remainDays'] = max(0, (int) ceil(($row['expireAt'] - $now) / 86400)); + $row['inviterName'] = $row['inviterName'] ?: '未知'; + $row['inviteeName'] = $row['inviteeName'] ?: '未知'; + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取绑定记录失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/commissions 全平台佣金记录 + // ───────────────────────────────────────────────────────────── + public function commissions() + { + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + $scope = Request::param('scope', ''); + $status = Request::param('status', ''); + + try { + $query = Db::name('commission_records') + ->alias('c') + ->leftJoin('wechat_users inv', 'c.inviterId = inv.id') + ->leftJoin('wechat_users invt', 'c.inviteeId = invt.id') + ->leftJoin('enterprises e', 'c.enterpriseId = e.id') + ->field('c.*, inv.nickname as inviterName, invt.nickname as inviteeName, e.name as enterpriseName'); + + if ($scope) $query->where('c.scope', $scope); + if ($status) $query->where('c.status', $status); + + $total = (clone $query)->count(); + $list = $query->order('c.createdAt', 'desc')->page($page, $pageSize)->select()->toArray(); + + foreach ($list as &$row) { + $row['commissionYuan'] = number_format($row['commissionFen'] / 100, 2, '.', ''); + $row['orderYuan'] = number_format($row['orderAmount'] / 100, 2, '.', ''); + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取佣金记录失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/withdrawals 全平台提现申请 + // ───────────────────────────────────────────────────────────── + public function withdrawals() + { + $page = max(1, (int) Request::param('page', 1)); + $pageSize = min(100, (int) Request::param('pageSize', 20)); + $status = Request::param('status', ''); + + try { + $query = Db::name('distribution_withdrawals') + ->alias('w') + ->leftJoin('wechat_users u', 'w.userId = u.id') + ->field('w.*, u.nickname, u.avatar'); + + if ($status !== '') { + // 支持字符串或数字,统一转 int + $query->where('w.status', (int)$status); + } + + $total = (clone $query)->count(); + $list = $query->order('w.createdAt', 'desc')->page($page, $pageSize)->select()->toArray(); + + foreach ($list as &$row) { + $row['amountYuan'] = number_format($row['amountFen'] / 100, 2, '.', ''); + $row['nickname'] = $row['nickname'] ?: '未知用户'; + + // 确保前端拿到的是数字 status(避免 '0' 和 0 比较异常) + $code = (int) ($row['status'] ?? 0); + $row['status'] = $code; + + // 统一后台状态文案:0审核中、1已驳回、2待收款、3已收款、4已过期 + switch ($code) { + case 0: + $row['statusLabel'] = '审核中'; + break; + case 1: + $row['statusLabel'] = '已驳回'; + break; + case 2: + $row['statusLabel'] = '待收款'; + break; + case 3: + $row['statusLabel'] = '已收款'; + break; + case 4: + $row['statusLabel'] = '已过期'; + break; + default: + $row['statusLabel'] = '未知'; + break; + } + } + + return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]); + } catch (\Exception $e) { + return error('获取提现记录失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // POST distribution/withdrawals/:id/approve 审核通过提现 + // ───────────────────────────────────────────────────────────── + public function approveWithdrawal(int $id) + { + $note = Request::param('note', ''); + $now = time(); + + $record = Db::name('distribution_withdrawals') + ->alias('w') + ->leftJoin('wechat_users u', 'w.userId = u.id') + ->field('w.*, u.openid') + ->where('w.id', $id) + ->find(); + // 仅允许处理审核中(status=0)的记录 + if (!$record || (int)$record['status'] !== 0) { + return error('提现申请不存在或已处理', 400); + } + + try { + // 生成商户单号:TX + 时间戳 + 随机数 + 提现ID(示例:TX202603121526520005123) + $outBillNo = 'TX' . date('YmdHis') . mt_rand(1000, 9999) . $record['id']; + + // 调用微信商家转账到零钱接口(参数对齐 ckb-admin Withdrawal::handleWechatPay) + $service = new \app\common\service\WechatTransferService(); + $result = $service->createTransfer([ + 'out_bill_no' => $outBillNo, + 'openid' => $record['openid'], + 'transfer_amount' => (int) $record['amountFen'], // 单位:分 + 'transfer_remark' => '推广佣金提现', + 'transfer_scene_id' => env('TRANSFER_SCENE_ID', '1005'), + 'transfer_scene_report_infos' => [ + [ + 'info_type' => '岗位类型', + 'info_content' => '推广人员', + ], + [ + 'info_type' => '报酬说明', + 'info_content' => '推广佣金提现', + ], + ], + 'notify_url' => env('WITHDRAW_NOTIFY_URL', ''), // 可选:提现专用回调 + ]); + + if ($result['success'] !== true) { + $err = $result['error'] ?? []; + $code = $err['code'] ?? 'UNKNOWN'; + $msg = $err['message'] ?? '微信转账接口调用失败'; + return error("微信转账发起失败({$code}):{$msg}", 500); + } + + $wechatData = $result['data'] ?? []; + Db::name('distribution_withdrawals')->where('id', $id)->update([ + // 2=待收款(已发起转账,等待用户确认) + 'status' => 2, + 'auditNote' => $note, + 'auditAt' => $now, + 'updatedAt' => $now, + 'pay_type' => 'wechat', + 'out_bill_no' => $outBillNo, + 'transfer_bill_no' => $wechatData['transfer_bill_no'] ?? null, + 'wechat_pay_state' => $wechatData['state'] ?? 'PROCESSING', + 'transfer_scene_id'=> $wechatData['transfer_scene_id'] ?? env('TRANSFER_SCENE_ID', '1005'), + 'package_info' => $wechatData['package_info'] ?? '', + 'mch_id' => env('MCH_ID', null), + ]); + return success(null, '审核通过,已发起微信转账'); + } catch (\Exception $e) { + return error('操作失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // POST distribution/withdrawals/:id/reject 拒绝提现 + // ───────────────────────────────────────────────────────────── + public function rejectWithdrawal(int $id) + { + $note = Request::param('note', ''); + $now = time(); + + $record = Db::name('distribution_withdrawals')->where('id', $id)->find(); + // 仅允许处理审核中(status=0)的记录 + if (!$record || (int)$record['status'] !== 0) { + return error('提现申请不存在或已处理', 400); + } + + Db::startTrans(); + try { + Db::name('wechat_users') + ->where('id', $record['userId']) + ->inc('walletBalance', $record['amountFen']) + ->update(['updatedAt' => $now]); + + Db::name('distribution_withdrawals')->where('id', $id)->update([ + // 1=已驳回 + 'status' => 1, + 'auditNote' => $note, + 'auditAt' => $now, + 'updatedAt' => $now, + ]); + + Db::commit(); + return success(null, '已拒绝,余额已退回'); + } catch (\Exception $e) { + Db::rollback(); + return error('操作失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // GET distribution/settings 个人版分销全局配置 + // ───────────────────────────────────────────────────────────── + public function settings() + { + try { + $config = Db::name('system_config')->where('key', 'distribution')->where('enterprise_id', 0)->find(); + $default = [ + 'enabled' => true, + 'promoCenterTitle' => '推广中心', + 'bindingDays' => 30, + 'minWithdrawFen' => 100, + 'maxWithdrawFen' => 0, + 'requireAudit' => true, + 'withdrawFee' => 0, + 'testSettings' => self::defaultTestSettings(), + ]; + if ($config && $config['value']) { + $settings = is_string($config['value']) ? json_decode($config['value'], true) : $config['value']; + $settings = array_merge($default, $settings ?? []); + } else { + $settings = $default; + } + $settings['minWithdraw'] = round((float)($settings['minWithdrawFen'] ?? 100) / 100, 2); + $settings['maxWithdraw'] = ($max = (int)($settings['maxWithdrawFen'] ?? 0)) > 0 ? round($max / 100, 2) : 0; + $settings['testSettings'] = self::appendTestSettingsAmount( + array_merge(self::defaultTestSettings(), $settings['testSettings'] ?? []) + ); + return success($settings); + } catch (\Exception $e) { + return error('获取配置失败:' . $e->getMessage(), 500); + } + } + + // ───────────────────────────────────────────────────────────── + // PUT distribution/settings 更新个人版分销全局配置 + // ───────────────────────────────────────────────────────────── + public function updateSettings() + { + $settings = Request::only([ + 'enabled', 'promoCenterTitle', 'bindingDays', + 'minWithdrawFen', 'minWithdraw', 'maxWithdrawFen', 'maxWithdraw', + 'requireAudit', 'withdrawFee', 'testSettings' + ]); + + $minWithdrawFen = isset($settings['minWithdraw']) + ? (int) round((float)$settings['minWithdraw'] * 100) + : (int)($settings['minWithdrawFen'] ?? 100); + $maxWithdrawFen = isset($settings['maxWithdraw']) + ? (int) round((float)$settings['maxWithdraw'] * 100) + : (int)($settings['maxWithdrawFen'] ?? 0); + $minWithdrawFen = max(100, min(20000, $minWithdrawFen)); + $maxWithdrawFen = $maxWithdrawFen > 0 ? min(20000, max(100, $maxWithdrawFen)) : 0; + + $promoTitle = trim((string)($settings['promoCenterTitle'] ?? '')); + $toSave = [ + 'enabled' => (bool)($settings['enabled'] ?? true), + 'promoCenterTitle' => $promoTitle !== '' ? $promoTitle : '推广中心', + 'bindingDays' => (int)($settings['bindingDays'] ?? 30), + 'minWithdrawFen' => $minWithdrawFen, + 'maxWithdrawFen' => $maxWithdrawFen, + 'requireAudit' => isset($settings['requireAudit']) ? (bool)$settings['requireAudit'] : true, + 'withdrawFee' => max(0, min(100, (float)($settings['withdrawFee'] ?? 0))), + 'testSettings' => self::sanitizeTestSettings($settings['testSettings'] ?? null), + ]; + + try { + $now = time(); + $existing = Db::name('system_config')->where('key', 'distribution')->where('enterprise_id', 0)->find(); + if ($existing) { + Db::name('system_config') + ->where('key', 'distribution') + ->where('enterprise_id', 0) + ->update(['value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), 'updatedAt' => $now]); + } else { + Db::name('system_config')->insert([ + 'key' => 'distribution', + 'enterprise_id' => 0, + 'value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + } + $toSave['minWithdraw'] = $toSave['minWithdrawFen'] / 100; + $toSave['maxWithdraw'] = $toSave['maxWithdrawFen'] > 0 ? $toSave['maxWithdrawFen'] / 100 : 0; + $toSave['testSettings'] = self::appendTestSettingsAmount($toSave['testSettings']); + return success($toSave, '配置已保存'); + } catch (\Exception $e) { + return error('保存配置失败:' . $e->getMessage(), 500); + } + } + + private static function defaultTestSettings(): array + { + $item = ['enabled' => true, 'commissionType' => 'ratio', 'commissionRate' => 90, 'commissionAmountFen' => 0, 'noPayment' => false]; + return ['face' => $item, 'mbti' => $item, 'sbti' => $item, 'disc' => $item, 'pdp' => $item]; + } + + private static function sanitizeTestSettings($raw): array + { + $default = self::defaultTestSettings(); + if (!is_array($raw)) return $default; + $result = []; + foreach ($default as $type => $def) { + $s = $raw[$type] ?? []; + $commissionType = in_array($s['commissionType'] ?? '', ['ratio', 'amount']) ? $s['commissionType'] : 'ratio'; + $amountFen = isset($s['commissionAmount']) + ? (int) round((float)$s['commissionAmount'] * 100) + : (int)($s['commissionAmountFen'] ?? 0); + $rate = max(0, min(100, (int)($s['commissionRate'] ?? 90))); + $result[$type] = [ + 'enabled' => ($s['enabled'] ?? true) !== false, + 'commissionType' => $commissionType, + 'commissionRate' => $commissionType === 'ratio' ? $rate : 0, + 'commissionAmountFen'=> $commissionType === 'amount' ? max(0, $amountFen) : 0, + 'noPayment' => !empty($s['noPayment']), + ]; + } + return $result; + } + + private static function appendTestSettingsAmount(array $ts): array + { + foreach ($ts as $k => $v) { + $ts[$k]['commissionAmount'] = round(($v['commissionAmountFen'] ?? 0) / 100, 2); + } + return $ts; + } +}