| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- <?php
- namespace plugin\saiadmin\app\logic\user;
- use plugin\saiadmin\app\model\system\SystemUser;
- use plugin\saiadmin\app\model\user\RentalUser;
- use plugin\saiadmin\basic\think\BaseLogic;
- use plugin\saiadmin\exception\ApiException;
- use support\think\Db;
- /**
- * 租借用户列表与订单记录。
- *
- * 参照旧项目 Search::query_users():用户资料独立存储,租借次数和消费金额
- * 从订单实时汇总。非超级管理员仅能看到本人及递归下级管理订单涉及的用户。
- */
- class RentalUserLogic extends BaseLogic
- {
- private const RECHARGE_BALANCE_COLUMNS = [
- 'wallet' => '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 . '%');
- }
- }
- }
|