'wallet_balance', 'card_gift' => 'card_gift_balance', ]; private const ORDER_STATUSES = [ 0 => '待支付', 1 => '使用中', 2 => '已完成', 3 => '超时完成', 4 => '租借失败', 5 => '扣款失败', 6 => '申请退款', ]; private const MEMBER_CARD_TYPE_NAMES = [ 0 => '月卡', 1 => '季卡', 2 => '半年卡', 3 => '年卡', ]; public function __construct() { $this->model = new RentalUser(); } public function indexList(array $filters): array { $page = max(1, (int) request()->input('page', 1)); $limit = min(100, max(1, (int) request()->input('limit', 20))); $managerIds = $this->accessibleManagerIds(); $query = Db::table('sa_rental_user')->alias('u')->whereNull('u.delete_time'); if (is_array($managerIds)) { $userIds = $managerIds === [] ? [] : Db::table('sa_rental_order') ->whereNull('delete_time') ->whereIn('manage_agent_id', $managerIds) ->where('user_id', '>', 0) ->distinct(true) ->column('user_id'); if ($userIds === []) { $query->where('u.id', 0); } else { $query->whereIn('u.id', array_map('intval', $userIds)); } } $source = trim((string) ($filters['source'] ?? '')); if (in_array($source, ['1', '2'], true)) { $query->where('u.source', (int) $source); } $userId = (int) ($filters['user_id'] ?? 0); if ($userId > 0) { $query->where('u.id', $userId); } $this->like($query, 'u.nickname', $filters['nickname'] ?? ''); $this->like($query, 'u.phone', $filters['phone'] ?? ''); $result = $query->field([ 'u.id', 'u.agent_id', 'u.source', 'u.nickname', 'u.phone', 'u.avatar', 'u.deposit_amount', 'u.wallet_balance', 'u.card_balance', 'u.card_gift_balance', 'u.member_expire_at', 'u.member_card_type', 'u.status', 'u.register_at', 'u.create_time', ])->order('u.register_at', 'desc')->order('u.id', 'desc') ->paginate(['page' => $page, 'list_rows' => $limit])->toArray(); $rows = $result['data'] ?? []; $stats = $this->orderStats(array_map('intval', array_column($rows, 'id')), $managerIds); $result['data'] = array_map(function (array $row) use ($stats): array { $userId = (int) $row['id']; $stat = $stats[$userId] ?? ['rent_count' => 0, 'amount' => 0.0]; $cardType = (int) ($row['member_card_type'] ?? 0); $expireAt = (string) ($row['member_expire_at'] ?? ''); $memberActive = $expireAt !== '' && strtotime($expireAt) > time(); return [ 'id' => $userId, 'agent_id' => (int) ($row['agent_id'] ?? 0), 'source' => (int) ($row['source'] ?? 1), 'source_name' => (int) ($row['source'] ?? 1) === 2 ? '支付宝' : '微信', 'nickname' => (string) (($row['nickname'] ?? '') ?: '未设置昵称'), 'phone' => (string) ($row['phone'] ?? ''), 'avatar' => (string) ($row['avatar'] ?? ''), 'deposit_amount' => (float) ($row['deposit_amount'] ?? 0), 'rent_count' => (int) $stat['rent_count'], 'amount' => (float) $stat['amount'], 'wallet_balance' => (float) ($row['wallet_balance'] ?? 0), 'card_balance' => (float) ($row['card_balance'] ?? 0), 'card_gift_balance' => (float) ($row['card_gift_balance'] ?? 0), 'member_expire_at' => $expireAt, 'member_card_type' => $cardType, 'member_card_type_name' => self::MEMBER_CARD_TYPE_NAMES[$cardType] ?? '未开通', 'member_active' => $memberActive, 'status' => (int) ($row['status'] ?? 1), 'register_at' => (string) (($row['register_at'] ?? '') ?: ($row['create_time'] ?? '')), ]; }, $rows); return $result; } /** * 管理员对钱包余额或储值卡赠额加款/扣款并写流水。 * * 沿用 list/orders 的可见范围:超级管理员可对任意用户充值; * 普通账号只能对本人及递归下级管理订单涉及的用户充值。事务内行锁防并发, * 扣款时禁止减为负数。 * * 旧系统的平台“储值卡充值”只调整赠额;实付本金必须来自支付回调, * 因此管理端不开放 card_balance 手工调整。 * * @param array $data user_id, balance_type(wallet/card_gift), amount, remark */ public function recharge(array $data, int $operatorId, string $operatorName): array { $userId = (int) ($data['user_id'] ?? 0); if ($userId <= 0) { throw new ApiException('用户参数错误'); } $balanceType = trim((string) ($data['balance_type'] ?? '')); if ($balanceType === '') { $balanceType = 'wallet'; } if (!isset(self::RECHARGE_BALANCE_COLUMNS[$balanceType])) { throw new ApiException('账户类型不支持'); } $amountInput = trim((string) ($data['amount'] ?? '')); if (!preg_match('/^-?\d{1,10}(?:\.\d{1,2})?$/D', $amountInput)) { throw new ApiException('调整金额最多 10 位整数、2 位小数'); } $amount = (float) $amountInput; if ($amount === 0.0) { throw new ApiException('调整金额不能为 0'); } $remark = trim((string) ($data['remark'] ?? '')); $managerIds = $this->accessibleManagerIds(); $this->assertUserAccessible($userId, $managerIds); return $this->transaction(function () use ($userId, $balanceType, $amount, $remark, $operatorId, $operatorName) { $user = Db::table('sa_rental_user') ->where('id', $userId) ->whereNull('delete_time') ->lock(true) ->find(); if (!$user) { throw new ApiException('用户不存在'); } $column = self::RECHARGE_BALANCE_COLUMNS[$balanceType]; $before = round((float) ($user[$column] ?? 0), 2); $after = round($before + $amount, 2); if ($after < 0) { throw new ApiException('余额不足,无法扣减'); } if ($after > 9999999999.99) { throw new ApiException('调整后余额超出系统上限'); } $direction = $amount > 0 ? 1 : -1; $now = date('Y-m-d H:i:s'); Db::table('sa_rental_user')->where('id', $userId)->update([ $column => $after, 'update_time' => $now, ]); Db::table('sa_rental_user_wallet_log')->insert([ 'user_id' => $userId, 'balance_type' => $balanceType, 'direction' => $direction, 'amount' => $amount, 'before_balance' => $before, 'after_balance' => $after, 'source' => 'admin_recharge', 'remark' => $remark, 'operator_id' => $operatorId, 'operator_name' => $operatorName, 'created_at' => $now, ]); return [ 'user_id' => $userId, 'balance_type' => $balanceType, 'amount' => $amount, 'before_balance' => $before, 'after_balance' => $after, ]; }); } /** * 管理员对租借用户开通或续费会员。 * * 沿用 list/orders 的可见范围(超级管理员可对任意用户, * 普通账号只能对本人及递归下级管理订单涉及的用户)。事务内: * 1. 写 sa_member_activation_record(source=3 后台开通) * 2. 同步 sa_rental_user.member_expire_at / member_card_type 快照 * * 退订/撤销流程不在本期范围。开通金额允许为 0(管理员手工赠送), * 但不能为负。日期超长到期(>9999-12-31)由 validate 拒绝。 * * @param array $data user_id, card_type(0-3), pay_amount, expire_time, activation_time?, remark? */ public function activateMember(array $data, int $operatorId, string $operatorName): array { $userId = (int) ($data['user_id'] ?? 0); if ($userId <= 0) { throw new ApiException('用户参数错误'); } $cardType = (int) ($data['card_type'] ?? -1); if (!isset(self::MEMBER_CARD_TYPE_NAMES[$cardType])) { throw new ApiException('会员卡类型不支持'); } $payAmountInput = trim((string) ($data['pay_amount'] ?? '')); if (!preg_match('/^\d{1,10}(?:\.\d{1,2})?$/D', $payAmountInput)) { throw new ApiException('支付金额最多 10 位整数、2 位小数'); } $payAmount = (float) $payAmountInput; $expireTime = trim((string) ($data['expire_time'] ?? '')); if ($expireTime === '' || !strtotime($expireTime)) { throw new ApiException('到期时间不合法'); } if (strtotime($expireTime) <= time()) { throw new ApiException('到期时间必须晚于当前时间'); } $activationTime = trim((string) ($data['activation_time'] ?? '')); if ($activationTime === '') { $activationTime = date('Y-m-d H:i:s'); } elseif (!strtotime($activationTime)) { throw new ApiException('开通时间格式不正确'); } $remark = trim((string) ($data['remark'] ?? '')); if (mb_strlen($remark) > 500) { throw new ApiException('备注不能超过 500 个字符'); } $managerIds = $this->accessibleManagerIds(); $this->assertUserAccessible($userId, $managerIds); return $this->transaction(function () use ($userId, $cardType, $payAmount, $expireTime, $activationTime, $remark, $operatorId, $operatorName) { $user = Db::table('sa_rental_user') ->where('id', $userId) ->whereNull('delete_time') ->lock(true) ->find(); if (!$user) { throw new ApiException('用户不存在'); } $now = date('Y-m-d H:i:s'); $orderSn = 'ADMIN-MEMBER-' . $userId . '-' . date('YmdHis') . sprintf('%04d', mt_rand(0, 9999)); Db::table('sa_member_activation_record')->insert([ 'merchant_order_sn' => $orderSn, 'transaction_sn' => '', 'user_id' => $userId, 'manage_agent_id' => $operatorId, 'source' => 3, 'device_type' => 0, 'merchant_id' => 0, 'merchant_name' => '', 'pay_amount' => $payAmount, 'card_type' => $cardType, 'activation_time' => $activationTime, 'expire_time' => $expireTime, 'status' => 1, 'remark' => $remark, 'created_by' => $operatorId, 'updated_by' => $operatorId, 'create_time' => $now, 'update_time' => $now, ]); Db::table('sa_rental_user')->where('id', $userId)->update([ 'member_expire_at' => $expireTime, 'member_card_type' => $cardType, 'update_time' => $now, ]); return [ 'user_id' => $userId, 'card_type' => $cardType, 'card_type_name' => self::MEMBER_CARD_TYPE_NAMES[$cardType], 'pay_amount' => round($payAmount, 2), 'activation_time' => $activationTime, 'expire_time' => $expireTime, 'merchant_order_sn' => $orderSn, ]; }); } public function orderList(int $userId): array { if ($userId <= 0) { throw new ApiException('用户参数错误'); } $managerIds = $this->accessibleManagerIds(); $this->assertUserAccessible($userId, $managerIds); $page = max(1, (int) request()->input('page', 1)); $limit = min(100, max(1, (int) request()->input('limit', 20))); $query = Db::table('sa_rental_order')->alias('o') ->leftJoin('sa_device d', 'd.id = o.device_id AND d.delete_time IS NULL') ->leftJoin('sa_merchant m', 'm.id = o.rent_merchant_id AND m.delete_time IS NULL') ->whereNull('o.delete_time')->where('o.user_id', $userId); if (is_array($managerIds)) { $query->whereIn('o.manage_agent_id', $managerIds); } $result = $query->field([ 'o.id', 'o.order_sn', 'o.device_type', 'o.rent_at', 'o.return_at', 'o.status', 'o.paid_amount', 'o.refund_amount', 'o.manage_agent_id', 'd.goods_name AS device_name', 'd.goods_sn AS device_sn', 'm.name AS merchant_name', ])->order('o.rent_at', 'desc')->order('o.id', 'desc') ->paginate(['page' => $page, 'list_rows' => $limit])->toArray(); $result['data'] = array_map(fn (array $row): array => [ 'id' => (int) $row['id'], 'order_sn' => (string) $row['order_sn'], 'device_type' => (int) ($row['device_type'] ?? 0), 'device_name' => (string) ($row['device_name'] ?? ''), 'device_sn' => (string) ($row['device_sn'] ?? ''), 'merchant_name' => (string) ($row['merchant_name'] ?? ''), 'rent_at' => (string) ($row['rent_at'] ?? ''), 'return_at' => (string) ($row['return_at'] ?? ''), 'status' => (int) ($row['status'] ?? 0), 'status_name' => self::ORDER_STATUSES[(int) ($row['status'] ?? 0)] ?? '未知状态', 'paid_amount' => (float) ($row['paid_amount'] ?? 0), 'refund_amount' => (float) ($row['refund_amount'] ?? 0), 'manage_agent_id' => (int) ($row['manage_agent_id'] ?? 0), ], $result['data'] ?? []); return $result; } /** @param int[]|null $managerIds */ private function orderStats(array $userIds, ?array $managerIds): array { if ($userIds === []) { return []; } $query = Db::table('sa_rental_order')->whereNull('delete_time')->whereIn('user_id', $userIds); if (is_array($managerIds)) { $query->whereIn('manage_agent_id', $managerIds); } $rows = $query->field('user_id,COUNT(*) AS rent_count,COALESCE(SUM(paid_amount),0) AS amount') ->group('user_id')->select()->toArray(); $result = []; foreach ($rows as $row) { $result[(int) $row['user_id']] = [ 'rent_count' => (int) $row['rent_count'], 'amount' => (float) $row['amount'], ]; } return $result; } /** @param int[]|null $managerIds */ private function assertUserAccessible(int $userId, ?array $managerIds): void { $exists = Db::table('sa_rental_user')->where('id', $userId)->whereNull('delete_time')->count(); if ($exists === 0) { throw new ApiException('用户不存在'); } if (is_array($managerIds)) { if ($managerIds === []) { throw new ApiException('用户不存在或无权查看'); } $orderExists = Db::table('sa_rental_order')->where('user_id', $userId) ->whereIn('manage_agent_id', $managerIds)->whereNull('delete_time')->count(); if ($orderExists === 0) { throw new ApiException('用户不存在或无权查看'); } } } /** * null 表示超级管理员可查看全部;数组表示本人及递归下级管理人范围。 * @return int[]|null */ private function accessibleManagerIds(): ?array { $currentId = (int) ($this->adminInfo['id'] ?? 0); if ($currentId === 1) { return null; } if ($currentId <= 0) { return []; } $managerIds = [$currentId]; $dept = $this->adminInfo['deptList'] ?? []; if (empty($dept['id']) || !isset($dept['level'])) { return $managerIds; } $prefix = (string) $dept['level'] . (int) $dept['id'] . ','; $deptIds = Db::table('sa_system_dept')->whereNull('delete_time') ->whereLike('level', $prefix . '%')->column('id'); if ($deptIds !== []) { $managerIds = array_merge($managerIds, array_map('intval', SystemUser::whereIn('dept_id', $deptIds) ->whereNull('delete_time')->column('id'))); } return array_values(array_unique($managerIds)); } private function like($query, string $field, mixed $value): void { $value = trim((string) $value); if ($value !== '') { $query->whereLike($field, '%' . $value . '%'); } } }