'车载充电器', 'k-battery' => 'K霸宝', 'bluetooth' => '蓝牙线充', 'power-bank' => '充电宝', 'password' => '密码线充', 'password-bluetooth' => '密码蓝牙线充', ]; /** * 旧项目 device_depend_type_deal 中与当前平台产品对应的设备类型。 */ private const DEVICE_TYPE_NAMES = [ 1 => '充电底座', 100 => 'K霸宝', 101 => '充电宝', 102 => 'K霸宝 Blue', 103 => '密码线充', 104 => '蓝牙线充', 105 => '密码蓝牙线充', 107 => '车载充电器', ]; private const WRITABLE_FIELDS = [ 'name', 'store_phone', 'storefront_image', 'industry', 'region', 'address', 'description', 'products', 'profit_method', 'profit_rate', 'contact', 'phone', 'permissions', 'distributed', 'status', 'agent_id', 'agent_type', 'parent_merchant_id', ]; public function __construct() { $this->model = new Merchant(); } /** * 商户分页列表。 */ public function indexList(array $where): array { $query = $this->buildListQuery($where); [$orderField, $orderType] = $this->resolveOrder($where); $page = max(1, (int) request()->input('page', 1)); $limit = min(100, max(1, (int) request()->input('limit', 10))); $result = $query->order($orderField, $orderType)->paginate([ 'page' => $page, 'list_rows' => $limit, ])->toArray(); $result['summary'] = $this->typeSummary($where); return $this->hydratePage($result); } /** * 按当前筛选条件导出全部有权访问的商户。 */ public function export(array $where): Response { $query = $this->buildListQuery($where); [$orderField, $orderType] = $this->resolveOrder($where); $result = $this->hydratePage([ 'data' => $query->order($orderField, $orderType)->select()->toArray(), ]); $data = []; foreach ($result['data'] as $row) { $data[] = [ 'id' => (int) $row['id'], 'name' => $this->excelSafe($row['name']), 'store_phone' => $this->excelSafe($row['store_phone']), 'phone' => $this->excelSafe($row['phone']), 'address' => $this->excelSafe($row['address']), 'total_amount' => (float) $row['total_amount'], 'total_income' => (float) $row['total_income'], 'withdrawable_amount' => (float) $row['withdrawable_amount'], 'order_count' => (int) $row['order_count'], 'device_count' => (int) $row['device_count'], 'yesterday_usage_rate' => (float) $row['yesterday_usage_rate'], 'three_day_usage_rate' => (float) $row['three_day_usage_rate'], 'seven_day_usage_rate' => (float) $row['seven_day_usage_rate'], 'agent_name' => $this->excelSafe($row['agent_name']), 'agent_phone' => $this->excelSafe($row['agent_phone']), 'profit_user_name' => $this->excelSafe($row['profit_user_name']), 'profit_user_phone' => $this->excelSafe($row['profit_user_phone']), 'profit_method' => $row['profit_method'], 'profit_rate' => (float) $row['profit_rate'], 'region' => $this->excelSafe($row['region']), 'industry' => $this->excelSafe($row['industry']), 'device_type' => $this->excelSafe($row['device_type']), 'distributed' => (int) $row['distributed'] === 1 ? '已铺货' : '未铺货', 'status' => (int) $row['status'] === 1 ? '启用' : '停用', 'created_at' => $row['created_at'], ]; } $fileName = '商户数据_' . date('Ymd_His') . '.xlsx'; $writer = new OpenSpoutWriter($fileName); $writer->setWidth([10, 24, 16, 16, 36, 15, 15, 15, 12, 12, 14, 14, 14, 18, 16, 18, 16, 14, 12, 24, 20, 24, 12, 10, 22]); $writer->setHeader([ '商户ID', '商户名称', '商户电话', '登录手机号', '商户地址', '总交易额', '总收益', '可提现金额', '订单量', '设备数', '昨日使用率(%)', '近三天使用率(%)', '近七天使用率(%)', '上级代理', '代理电话', '分润人', '分润人电话', '分成方式', '分润比例(%)', '城市区域', '行业分类', '设备类型', '铺货状态', '商户状态', '创建时间', ]); $writer->setData($data); return response()->download($writer->returnFile(), urlencode($fileName)); } /** * 读取一个当前用户有权访问的商户。 */ public function readAccessible(int $id): array { return $this->formatMerchant($this->findAccessible($id)->toArray(), $this->userMapForMerchant($id)); } /** * 新建直属商户。 */ public function addMerchant(array $data): int { $payload = $this->sanitize($data); $payload['agent_id'] = $this->resolveAgentId((int) ($data['agent_id'] ?? 0)); $payload['parent_merchant_id'] = $this->resolveParentMerchantId( (int) ($data['parent_merchant_id'] ?? 0) ); if ($payload['parent_merchant_id'] !== null) { $parent = $this->findAccessible((int) $payload['parent_merchant_id']); $payload['agent_id'] = (int) $parent->agent_id; } $this->assertPhoneUnique($payload['phone']); $payload['password'] = password_hash((string) $data['password'], PASSWORD_DEFAULT); return (int) $this->add($payload); } /** * 修改商户基础资料和分润资料。 */ public function editMerchant(int $id, array $data): bool { $model = $this->findAccessible($id); $payload = $this->sanitize($data); unset($payload['parent_merchant_id']); if ($this->currentAdminId() !== 1) { unset($payload['agent_id']); } elseif (isset($payload['agent_id'])) { $payload['agent_id'] = $this->resolveAgentId((int) $payload['agent_id']); } $this->assertPhoneUnique($payload['phone'], $id); return $model->save($payload) !== false; } /** * 重置商户登录密码。 */ public function resetPassword(int $id, string $password): bool { $model = $this->findAccessible($id); return $model->save(['password' => password_hash($password, PASSWORD_DEFAULT)]) !== false; } /** * 查询商户当前绑定的设备。 */ public function boundDevices(int $merchantId): array { $this->findAccessible($merchantId); $bindings = Db::table('sa_merchant_device_binding') ->where('merchant_id', $merchantId) ->order('bind_time', 'desc') ->select() ->toArray(); if ($bindings === []) { return []; } $deviceIds = array_map('intval', array_column($bindings, 'device_id')); $devices = MerchantDevice::whereIn('id', $deviceIds)->select()->toArray(); $deviceMap = []; foreach ($devices as $device) { $deviceMap[(int) $device['id']] = $device; } $result = []; foreach ($bindings as $binding) { $device = $deviceMap[(int) $binding['device_id']] ?? []; $type = (int) ($device['goods_type'] ?? $binding['depend_type'] ?? 0); $result[] = [ 'id' => (int) $binding['device_id'], 'name' => (string) ($device['goods_name'] ?? ''), 'sn' => (string) $binding['device_sn'], 'device_code' => (string) ($device['device_code'] ?? ''), 'model' => (string) ($device['model'] ?? ''), 'type' => $type, 'type_name' => self::DEVICE_TYPE_NAMES[$type] ?? '其他设备', 'status' => (int) ($device['status'] ?? 0), 'status_name' => $this->deviceStatusName((int) ($device['status'] ?? 0)), 'online_status' => (int) ($device['device_status'] ?? 0), 'bind_time' => (string) $binding['bind_time'], ]; } return $result; } /** * 将设备绑定到商户。 * * 迁移自旧系统 Store::bindZuo2Store:校验设备存在和管理权,阻止设备重复铺货, * 写入当前关系及铺货流水,并同步商户设备数与铺货状态。 */ public function bindDevices(int $merchantId, array $deviceSns): array { $merchant = $this->findAccessible($merchantId); $sns = $this->normalizeDeviceSns($deviceSns); $operatorId = $this->currentAdminId(); return $this->transaction(function () use ($merchant, $merchantId, $sns, $operatorId) { $deviceRows = Db::table('sa_device') ->whereIn('goods_sn', $sns) ->whereNull('delete_time') ->lock(true) ->select() ->toArray(); $deviceMap = []; foreach ($deviceRows as $device) { $deviceMap[(string) $device['goods_sn']] = $device; } $deviceIds = array_map('intval', array_column($deviceRows, 'id')); $bindingMap = []; if ($deviceIds !== []) { $bindingRows = Db::table('sa_merchant_device_binding') ->whereIn('device_id', $deviceIds) ->lock(true) ->select() ->toArray(); foreach ($bindingRows as $binding) { $bindingMap[(int) $binding['device_id']] = $binding; } } $now = date('Y-m-d H:i:s'); $bindings = []; $records = []; $success = []; $alreadyBound = []; $boundElsewhere = []; $notFound = []; $notOwned = []; foreach ($sns as $sn) { if (!isset($deviceMap[$sn])) { $notFound[] = $sn; continue; } $device = $deviceMap[$sn]; if ($operatorId !== 1 && (int) $device['manage_agent_id'] !== $operatorId) { $notOwned[] = $sn; continue; } $deviceId = (int) $device['id']; if (isset($bindingMap[$deviceId])) { if ((int) $bindingMap[$deviceId]['merchant_id'] === $merchantId) { $alreadyBound[] = $sn; } else { $boundElsewhere[] = $sn; } continue; } $type = (int) $device['goods_type']; $bindings[] = [ 'device_id' => $deviceId, 'device_sn' => $sn, 'depend_type' => $type, 'merchant_id' => $merchantId, 'status' => 1, 'operator_id' => $operatorId, 'bind_time' => $now, 'create_time' => $now, 'update_time' => $now, ]; $records[] = [ 'device_id' => $deviceId, 'device_sn' => $sn, 'depend_type' => $type, 'merchant_id' => $merchantId, 'action_type' => 1, 'operator_id' => $operatorId, 'action_time' => $now, 'created_by' => $operatorId, 'create_time' => $now, ]; $success[] = $sn; } if ($bindings !== []) { Db::table('sa_merchant_device_binding')->insertAll($bindings); Db::table('sa_merchant_device_binding_record')->insertAll($records); $this->syncMerchantDeviceSnapshot($merchant); } $message = $success === [] ? '没有可绑定的新设备' : '成功绑定 ' . count($success) . ' 台设备'; return [ 'message' => $message, 'bind_success_sn' => $success, 'has_binded_sn' => $alreadyBound, 'binded_other_store_sn' => $boundElsewhere, 'not_find_sn' => $notFound, 'not_owned_sn' => $notOwned, 'all_action_sn' => $sns, ]; }); } /** * 解除设备与指定商户的绑定,并保留解绑流水。 */ public function unbindDevice(int $merchantId, int $deviceId): bool { $merchant = $this->findAccessible($merchantId); $operatorId = $this->currentAdminId(); return (bool) $this->transaction(function () use ($merchant, $merchantId, $deviceId, $operatorId) { $binding = Db::table('sa_merchant_device_binding') ->where('device_id', $deviceId) ->where('merchant_id', $merchantId) ->lock(true) ->find(); if (empty($binding)) { throw new ApiException('设备未绑定到当前商户'); } $device = Db::table('sa_device')->where('id', $deviceId)->whereNull('delete_time')->find(); if (empty($device)) { throw new ApiException('设备不存在'); } if ($operatorId !== 1 && (int) $device['manage_agent_id'] !== $operatorId) { throw new ApiException('无权解绑该设备'); } $now = date('Y-m-d H:i:s'); Db::table('sa_merchant_device_binding') ->where('device_id', $deviceId) ->where('merchant_id', $merchantId) ->delete(); Db::table('sa_merchant_device_binding_record')->insert([ 'device_id' => $deviceId, 'device_sn' => (string) $binding['device_sn'], 'depend_type' => (int) $binding['depend_type'], 'merchant_id' => $merchantId, 'action_type' => 2, 'operator_id' => $operatorId, 'action_time' => $now, 'created_by' => $operatorId, 'create_time' => $now, ]); Db::table('sa_device')->where('id', $deviceId)->update([ 'is_active' => 0, 'active_time' => null, 'updated_by' => $operatorId, 'update_time' => $now, ]); $this->syncMerchantDeviceSnapshot($merchant); return true; }); } /** * 获取商户可提现金额更新方式。 * * update_mode 对应旧系统 crond_money_type:0 实时更新,1 定时更新。 * update_day 为 1 至 31;0 在定时模式下表示每月最后一天。 */ public function readWithdrawalSetting(int $merchantId): array { $merchant = $this->findAccessible($merchantId); return [ 'merchant_id' => (int) $merchant->id, 'update_mode' => (int) ($merchant->withdraw_update_mode ?? 0), 'update_day' => (int) ($merchant->withdraw_update_day ?? 0), ]; } /** * 保存商户可提现金额更新方式。 */ public function updateWithdrawalSetting(int $merchantId, int $mode, int $day): bool { $merchant = $this->findAccessible($merchantId); if (!in_array($mode, [0, 1], true)) { throw new ApiException('请选择正确的更新方式'); } if ($mode === 1 && ($day < 0 || $day > 31)) { throw new ApiException('请选择正确的更新日期'); } return $merchant->save([ 'withdraw_update_mode' => $mode, 'withdraw_update_day' => $mode === 0 ? 0 : $day, ]) !== false; } /** * 获取商户营业时间描述。 * * 旧系统将营业时间作为展示文本保存,默认营业时间为 09:00-22:00。 */ public function readBusinessTime(int $merchantId): array { $merchant = $this->findAccessible($merchantId); $businessTime = trim((string) ($merchant->business_time_des ?? '')); return [ 'merchant_id' => (int) $merchant->id, 'business_time_des' => $businessTime !== '' ? $businessTime : '09:00-22:00', ]; } /** * 保存商户营业时间描述。 */ public function updateBusinessTime(int $merchantId, string $businessTime): bool { $merchant = $this->findAccessible($merchantId); $businessTime = trim($businessTime); if ($businessTime === '') { throw new ApiException('请填写营业时间'); } if (mb_strlen($businessTime) > 64) { throw new ApiException('营业时间不能超过 64 个字符'); } return $merchant->save(['business_time_des' => $businessTime]) !== false; } /** * 获取商户按日统计。 * * 数据口径迁移自旧项目 statistics_shop_oneday:金额以分存储、按日汇总, * 同时保留微信、支付宝、余额三种支付方式的金额和订单量拆分。 */ public function statistics(int $merchantId, string $startDate = '', string $endDate = ''): array { $merchant = $this->findAccessible($merchantId); [$start, $end] = $this->normalizeStatisticsRange($startDate, $endDate); $rows = Db::table('sa_merchant_daily_statistics') ->where('merchant_id', $merchantId) ->where('stat_date', 'between', [$start, $end]) ->order('stat_date', 'asc') ->select() ->toArray(); $rowMap = []; foreach ($rows as $row) { $rowMap[(string) $row['stat_date']] = $row; } $daily = []; $summary = [ 'total_amount_cents' => 0, 'total_income_cents' => 0, 'total_orders' => 0, 'usage_rate_sum' => 0.0, ]; $payment = [ 'wechat' => ['amount_cents' => 0, 'orders' => 0], 'alipay' => ['amount_cents' => 0, 'orders' => 0], 'balance' => ['amount_cents' => 0, 'orders' => 0], ]; $cursor = new \DateTimeImmutable($start); $lastDay = new \DateTimeImmutable($end); while ($cursor <= $lastDay) { $date = $cursor->format('Y-m-d'); $row = $rowMap[$date] ?? []; $amountCents = (int) ($row['turnover_cents'] ?? 0); $incomeCents = (int) ($row['income_cents'] ?? 0); $orders = (int) ($row['order_count'] ?? 0); $usageRate = round((float) ($row['device_usage_rate'] ?? 0), 2); $wechatAmountCents = (int) ($row['wechat_turnover_cents'] ?? 0); $alipayAmountCents = (int) ($row['alipay_turnover_cents'] ?? 0); $balanceAmountCents = (int) ($row['balance_turnover_cents'] ?? 0); $wechatOrders = (int) ($row['wechat_order_count'] ?? 0); $alipayOrders = (int) ($row['alipay_order_count'] ?? 0); $balanceOrders = (int) ($row['balance_order_count'] ?? 0); $daily[] = [ 'date' => $date, 'amount' => $this->centsToMoney($amountCents), 'income' => $this->centsToMoney($incomeCents), 'orders' => $orders, 'usage_rate' => $usageRate, 'device_count' => (int) ($row['device_count'] ?? $merchant->device_count ?? 0), 'wechat_amount' => $this->centsToMoney($wechatAmountCents), 'alipay_amount' => $this->centsToMoney($alipayAmountCents), 'balance_amount' => $this->centsToMoney($balanceAmountCents), 'wechat_orders' => $wechatOrders, 'alipay_orders' => $alipayOrders, 'balance_orders' => $balanceOrders, ]; $summary['total_amount_cents'] += $amountCents; $summary['total_income_cents'] += $incomeCents; $summary['total_orders'] += $orders; $summary['usage_rate_sum'] += $usageRate; $payment['wechat']['amount_cents'] += $wechatAmountCents; $payment['wechat']['orders'] += $wechatOrders; $payment['alipay']['amount_cents'] += $alipayAmountCents; $payment['alipay']['orders'] += $alipayOrders; $payment['balance']['amount_cents'] += $balanceAmountCents; $payment['balance']['orders'] += $balanceOrders; $cursor = $cursor->modify('+1 day'); } $dayCount = count($daily); return [ 'merchant' => [ 'id' => (int) $merchant->id, 'name' => (string) $merchant->name, ], 'range' => ['start_date' => $start, 'end_date' => $end, 'day_count' => $dayCount], 'summary' => [ 'total_amount' => $this->centsToMoney($summary['total_amount_cents']), 'total_income' => $this->centsToMoney($summary['total_income_cents']), 'total_orders' => $summary['total_orders'], 'average_usage_rate' => $dayCount > 0 ? round($summary['usage_rate_sum'] / $dayCount, 2) : 0.0, ], 'payment' => [ 'wechat' => [ 'amount' => $this->centsToMoney($payment['wechat']['amount_cents']), 'orders' => $payment['wechat']['orders'], ], 'alipay' => [ 'amount' => $this->centsToMoney($payment['alipay']['amount_cents']), 'orders' => $payment['alipay']['orders'], ], 'balance' => [ 'amount' => $this->centsToMoney($payment['balance']['amount_cents']), 'orders' => $payment['balance']['orders'], ], ], 'daily' => $daily, ]; } /** * 导出所选时间范围内的商户每日统计明细。 */ public function statisticsExport(int $merchantId, string $startDate = '', string $endDate = ''): Response { $statistics = $this->statistics($merchantId, $startDate, $endDate); $data = []; foreach ($statistics['daily'] as $row) { $data[] = [ 'date' => $row['date'], 'amount' => $row['amount'], 'income' => $row['income'], 'orders' => $row['orders'], 'usage_rate' => $row['usage_rate'], 'wechat_amount' => $row['wechat_amount'], 'wechat_orders' => $row['wechat_orders'], 'alipay_amount' => $row['alipay_amount'], 'alipay_orders' => $row['alipay_orders'], 'balance_amount' => $row['balance_amount'], 'balance_orders' => $row['balance_orders'], 'device_count' => $row['device_count'], ]; } $safeMerchantName = preg_replace('/[\\\\\/:*?"<>|]+/u', '_', $statistics['merchant']['name']); $fileName = sprintf( '%s_数据统计_%s_%s.xlsx', $safeMerchantName ?: '商户', str_replace('-', '', $statistics['range']['start_date']), str_replace('-', '', $statistics['range']['end_date']) ); $writer = new OpenSpoutWriter($fileName); $writer->setWidth([14, 14, 14, 12, 14, 14, 12, 14, 12, 14, 12, 12]); $writer->setHeader([ '日期', '交易额(元)', '收益额(元)', '订单量', '使用率(%)', '微信金额(元)', '微信订单量', '支付宝金额(元)', '支付宝订单量', '余额金额(元)', '余额订单量', '设备数', ]); $writer->setData($data); return response()->download($writer->returnFile(), urlencode($fileName)); } /** * 软删除商户。禁止跨代理越权删除。 */ public function destroyAccessible(array|int|string $ids): bool { $idList = is_array($ids) ? $ids : explode(',', (string) $ids); $idList = array_values(array_unique(array_filter(array_map('intval', $idList)))); if ($idList === []) { throw new ApiException('请选择要删除的商户'); } return (bool) $this->transaction(function () use ($idList) { foreach ($idList as $id) { $this->findAccessible($id)->delete(); } return true; }); } private function normalizeDeviceSns(array $deviceSns): array { $sns = []; foreach ($deviceSns as $value) { $sn = trim((string) $value); if ($sn === '') { continue; } if (strlen($sn) > 60) { throw new ApiException('设备 SN 码不能超过 60 个字符'); } $sns[$sn] = $sn; } $sns = array_values($sns); if ($sns === []) { throw new ApiException('请至少输入一个设备 SN 码'); } if (count($sns) > 100) { throw new ApiException('单次最多绑定 100 台设备'); } return $sns; } private function syncMerchantDeviceSnapshot(Merchant $merchant): void { $count = Db::table('sa_merchant_device_binding') ->where('merchant_id', (int) $merchant->id) ->count(); $merchant->save([ 'device_count' => $count, 'distributed' => $count > 0 ? 1 : 0, ]); } private function deviceStatusName(int $status): string { return match ($status) { 1 => '运营中', 2 => '报修', default => '未启用', }; } private function buildListQuery(array $where) { $query = $this->model->where('id', '>', 0); $this->applyDataScope( $query, (string) ($where['scope'] ?? 'own'), (int) ($where['agent_id'] ?? 0) ); $name = trim((string) ($where['name'] ?? '')); if ($name !== '') { $query->whereLike('name', '%' . $name . '%'); } $phone = trim((string) ($where['phone'] ?? '')); if ($phone !== '') { $query->where(function ($subQuery) use ($phone) { $subQuery->whereLike('store_phone', '%' . $phone . '%') ->whereOr('phone', 'like', '%' . $phone . '%'); }); } if (($where['distributed'] ?? '') !== '') { $query->where('distributed', (int) $where['distributed']); } if (($where['status'] ?? '') !== '') { $query->where('status', (int) $where['status']); } $profitMethod = trim((string) ($where['profit_method'] ?? '')); if ($profitMethod !== '') { $query->where('profit_method', $profitMethod); } $agentType = trim((string) ($where['agent_type'] ?? '')); if ($agentType !== '') { $query->where('agent_type', $agentType); } $deviceCategory = trim((string) ($where['device_category'] ?? '')); if ($deviceCategory !== '' && $deviceCategory !== 'all' && isset(self::PRODUCT_CODES[$deviceCategory])) { $query->whereLike('products', '%"' . self::PRODUCT_CODES[$deviceCategory] . '"%'); } return $query; } /** * 返回当前数据范围和筛选条件下的代理类型数量。 * agent_type 本身不参与统计,避免切换分类后其它分类数量变成 0。 */ private function typeSummary(array $where): array { $countWhere = $where; $countWhere['agent_type'] = ''; $summary = [ 'all' => $this->buildListQuery($countWhere)->count(), 'city-partner' => 0, 'agent' => 0, 'salesperson' => 0, ]; foreach (['city-partner', 'agent', 'salesperson'] as $type) { $typedWhere = $countWhere; $typedWhere['agent_type'] = $type; $summary[$type] = $this->buildListQuery($typedWhere)->count(); } return array_map('intval', $summary); } private function resolveOrder(array $where): array { return match ((string) ($where['sort'] ?? 'default')) { 'amount_desc' => ['total_amount', 'DESC'], 'income_desc' => ['total_income', 'DESC'], 'created_asc' => ['id', 'ASC'], default => ['id', 'DESC'], }; } private function excelSafe(mixed $value): string { $value = (string) $value; return preg_match('/^[=+\-@]/u', $value) === 1 ? "'" . $value : $value; } private function normalizeStatisticsRange(string $startDate, string $endDate): array { if ($startDate === '' && $endDate === '') { $startDate = date('Y-m-01'); $endDate = date('Y-m-d'); } if (!$this->isValidDate($startDate) || !$this->isValidDate($endDate)) { throw new ApiException('请选择正确的统计日期'); } $start = new \DateTimeImmutable($startDate); $end = new \DateTimeImmutable($endDate); if ($start > $end) { throw new ApiException('开始日期不能晚于结束日期'); } $dayCount = (int) $start->diff($end)->days + 1; if ($dayCount > 366) { throw new ApiException('单次最多查询 366 天数据'); } return [$start->format('Y-m-d'), $end->format('Y-m-d')]; } private function isValidDate(string $value): bool { $date = \DateTimeImmutable::createFromFormat('!Y-m-d', $value); $errors = \DateTimeImmutable::getLastErrors(); return $date !== false && ($errors === false || ($errors['warning_count'] === 0 && $errors['error_count'] === 0)) && $date->format('Y-m-d') === $value; } private function centsToMoney(int $cents): string { return number_format($cents / 100, 2, '.', ''); } private function sanitize(array $data): array { $payload = []; foreach (self::WRITABLE_FIELDS as $field) { if (array_key_exists($field, $data)) { $payload[$field] = $data[$field]; } } foreach (['name', 'store_phone', 'storefront_image', 'industry', 'region', 'address', 'contact', 'phone', 'agent_type'] as $field) { if (array_key_exists($field, $payload)) { $payload[$field] = trim((string) $payload[$field]); } } $payload['description'] = (string) ($payload['description'] ?? ''); $payload['products'] = array_values(array_unique(array_filter(array_map('strval', $payload['products'] ?? [])))); $payload['permissions'] = array_values(array_unique(array_filter(array_map('strval', $payload['permissions'] ?? [])))); $payload['profit_method'] = trim((string) ($payload['profit_method'] ?? '')); if (!in_array($payload['profit_method'], ['比例分成', '不分成', '入驻费'], true)) { throw new ApiException('请选择正确的分成方式'); } $payload['profit_rate'] = (float) ($payload['profit_rate'] ?? 0); $payload['distributed'] = (int) ($payload['distributed'] ?? 0); $payload['status'] = (int) ($payload['status'] ?? 1); $payload['agent_type'] = $payload['agent_type'] !== '' ? $payload['agent_type'] : 'agent'; return $payload; } private function assertPhoneUnique(string $phone, int $exceptId = 0): void { $query = $this->model->where('phone', $phone); if ($exceptId > 0) { $query->where('id', '<>', $exceptId); } if ($query->count() > 0) { throw new ApiException('该手机号码已绑定其他商户'); } } private function resolveAgentId(int $requestedAgentId): int { $currentId = $this->currentAdminId(); if ($currentId !== 1) { return $currentId; } $agentId = $requestedAgentId > 0 ? $requestedAgentId : $currentId; if (SystemUser::where('id', $agentId)->count() === 0) { throw new ApiException('直属代理不存在'); } return $agentId; } private function resolveParentMerchantId(int $parentMerchantId): ?int { if ($parentMerchantId <= 0) { return null; } $parent = $this->findAccessible($parentMerchantId); if ((int) ($parent->parent_merchant_id ?? 0) > 0) { throw new ApiException('只能在主商户下添加分店'); } return (int) $parent->id; } private function findAccessible(int $id): Merchant { $query = $this->model->where('id', $id); if ($this->currentAdminId() !== 1) { $allowedAgentIds = array_merge([$this->currentAdminId()], $this->subordinateAgentIds()); $query->whereIn('agent_id', array_values(array_unique($allowedAgentIds))); } $model = $query->findOrEmpty(); if ($model->isEmpty()) { throw new ApiException('商户不存在或无权操作'); } return $model; } private function applyDataScope($query, string $scope, int $requestedAgentId): void { $currentId = $this->currentAdminId(); if ($currentId === 1) { if ($requestedAgentId > 0) { $query->where('agent_id', $requestedAgentId); } return; } if ($scope !== 'subordinate') { $query->where('agent_id', $currentId); return; } $agentIds = $this->subordinateAgentIds(); if ($requestedAgentId > 0) { $agentIds = in_array($requestedAgentId, $agentIds, true) ? [$requestedAgentId] : []; } if ($agentIds === []) { $query->where('agent_id', 0); return; } $query->whereIn('agent_id', $agentIds); } /** * SaiAdmin 使用部门层级表达代理上下级,等价替代旧表 admin_user_dependen.pid。 */ private function subordinateAgentIds(): array { $dept = $this->adminInfo['deptList'] ?? []; if (empty($dept['id']) || !isset($dept['level'])) { return []; } $levelPrefix = (string) $dept['level'] . (int) $dept['id'] . ','; $deptIds = SystemDept::whereLike('level', $levelPrefix . '%')->column('id'); if ($deptIds === []) { return []; } return array_map('intval', SystemUser::whereIn('dept_id', $deptIds)->column('id')); } private function currentAdminId(): int { return (int) ($this->adminInfo['id'] ?? 0); } private function hydratePage(array $result): array { $rows = $result['data'] ?? []; $userIds = []; foreach ($rows as $row) { $userIds[] = (int) ($row['agent_id'] ?? 0); $userIds[] = (int) ($row['profit_user_id'] ?? 0); } $userMap = $this->loadUserMap($userIds); foreach ($rows as $index => $row) { $rows[$index] = $this->formatMerchant($row, $userMap); } $result['data'] = $rows; return $result; } private function userMapForMerchant(int $merchantId): array { $merchant = $this->model->where('id', $merchantId)->field('agent_id,profit_user_id')->findOrEmpty(); if ($merchant->isEmpty()) { return []; } return $this->loadUserMap([(int) $merchant->agent_id, (int) $merchant->profit_user_id]); } private function loadUserMap(array $ids): array { $ids = array_values(array_unique(array_filter(array_map('intval', $ids)))); if ($ids === []) { return []; } $users = SystemUser::whereIn('id', $ids)->field('id,username,realname,phone')->select()->toArray(); $map = []; foreach ($users as $user) { $map[(int) $user['id']] = $user; } return $map; } private function formatMerchant(array $row, array $userMap): array { $agent = $userMap[(int) ($row['agent_id'] ?? 0)] ?? []; $profitUser = $userMap[(int) ($row['profit_user_id'] ?? 0)] ?? []; $products = $row['products'] ?? []; if (is_string($products)) { $products = json_decode($products, true) ?: []; } $permissions = $row['permissions'] ?? []; if (is_string($permissions)) { $permissions = json_decode($permissions, true) ?: []; } $row['products'] = $products; $row['permissions'] = $permissions; $row['device_type'] = implode('、', $products); $row['agent_name'] = (string) ($agent['realname'] ?? $agent['username'] ?? ''); $row['agent_phone'] = (string) ($agent['phone'] ?? ''); $row['profit_user_name'] = (string) ($profitUser['realname'] ?? $profitUser['username'] ?? $row['contact'] ?? ''); $row['profit_user_phone'] = (string) ($profitUser['phone'] ?? $row['phone'] ?? ''); $row['created_at'] = (string) ($row['create_time'] ?? ''); unset($row['password']); return $row; } }