diff --git a/app/Console/Commands/AutoCreditOverdue.php b/app/Console/Commands/AutoCreditOverdue.php new file mode 100644 index 0000000..7990258 --- /dev/null +++ b/app/Console/Commands/AutoCreditOverdue.php @@ -0,0 +1,63 @@ +get(); + $marked = 0; + + foreach ($orders as $order) { + $line = CreditLine::find($order->credit_line_id); + if (!$line) continue; + + $cycleDays = intval($line->cycle_days) ?: 15; + $createdTs = strtotime((string)$order->created_at); + if (!$createdTs) continue; + + $elapsed = (int)ceil((time() - $createdTs) / 86400); + if ($elapsed <= $cycleDays) continue; + + DB::beginTransaction(); + try { + $affected = CreditIeoOrder::where('id', $order->id) + ->where('status', 0) + ->update(['status' => 3]); + + if ($affected > 0) { + $maxInterestDays = min($elapsed, $cycleDays * 3); + $interest = sprintf("%.6f", $order->amount * $line->daily_rate * $maxInterestDays); + + CreditIeoOrder::where('id', $order->id) + ->update(['interest' => $interest]); + + AccountLog::insertLog( + ['user_id' => $order->user_id, 'value' => 0, 'info' => 'Credit IEO overdue #' . $order->id, 'type' => AccountLog::MICRO_TRADE_CLOSE_SETTLE, 'currency' => 3], + ['balance_type' => 1, 'wallet_id' => 0, 'lock_type' => 0, 'before' => 0, 'change' => 0, 'after' => 0] + ); + $marked++; + } + + DB::commit(); + } catch (\Exception $e) { + DB::rollBack(); + $this->error("Order #{$order->id} error: " . $e->getMessage()); + } + } + + if ($marked > 0) { + $this->info("Marked {$marked} orders as overdue"); + } + } +} diff --git a/app/CreditIeoOrder.php b/app/CreditIeoOrder.php new file mode 100644 index 0000000..58fe974 --- /dev/null +++ b/app/CreditIeoOrder.php @@ -0,0 +1,13 @@ +belongsTo(Users::class, 'user_id'); } + public function creditLine() { return $this->belongsTo(CreditLine::class, 'credit_line_id'); } + public function project() { return $this->belongsTo(IeoProject::class, 'project_id'); } +} diff --git a/app/CreditLine.php b/app/CreditLine.php new file mode 100644 index 0000000..7794518 --- /dev/null +++ b/app/CreditLine.php @@ -0,0 +1,11 @@ +belongsTo(Users::class, 'user_id'); } +} diff --git a/app/Http/Controllers/Admin/CreditController.php b/app/Http/Controllers/Admin/CreditController.php new file mode 100644 index 0000000..ab75f92 --- /dev/null +++ b/app/Http/Controllers/Admin/CreditController.php @@ -0,0 +1,113 @@ +leftJoin('users', 'credit_line.user_id', '=', 'users.id') + ->select( + 'credit_line.*', + 'users.phone as user_phone', + 'users.email as user_email' + ); + + $user_id = $request->input('user_id'); + if ($user_id) $query->where('credit_line.user_id', $user_id); + $keyword = $request->input('keyword'); + if ($keyword) { + $query->where(function ($q) use ($keyword) { + $q->where('users.phone', 'like', "%{$keyword}%") + ->orWhere('users.email', 'like', "%{$keyword}%"); + }); + } + + $data = $query->orderBy('credit_line.id', 'desc')->paginate($request->input('limit', 20)); + $statusMap = ['0' => 'Disabled', '1' => 'Active']; + foreach ($data as $item) { + $item->status_text = $statusMap[$item->status] ?? ''; + } + return $this->layuiData($data); + } + + public function grant(Request $request) + { + $user_id = intval($request->input('user_id')); + $total_amount = floatval($request->input('total_amount')); + $daily_rate = floatval($request->input('daily_rate')); + $cycle_days = intval($request->input('cycle_days')); + $credit_score = intval($request->input('credit_score', 0)); + $status = intval($request->input('status', 1)); + + if (!$user_id || $total_amount < 0 || $daily_rate < 0 || $cycle_days <= 0) { + return $this->error('Invalid params'); + } + + DB::beginTransaction(); + try { + $line = CreditLine::where('user_id', $user_id)->lockForUpdate()->first(); + if (!$line) { + CreditLine::create([ + 'user_id' => $user_id, + 'total_amount' => $total_amount, + 'used_amount' => 0, + 'available_amount' => $total_amount, + 'daily_rate' => $daily_rate, + 'cycle_days' => $cycle_days, + 'credit_score' => $credit_score, + 'status' => $status, + ]); + } else { + $used = floatval($line->used_amount); + if ($total_amount < $used) { DB::rollBack(); return $this->error('Total cannot be less than used'); } + $line->total_amount = $total_amount; + $line->available_amount = bc_sub($total_amount, $used, 6); + $line->daily_rate = $daily_rate; + $line->cycle_days = $cycle_days; + $line->credit_score = $credit_score; + $line->status = $status; + $line->save(); + } + DB::commit(); + return $this->success('OK'); + } catch (\Exception $e) { + DB::rollBack(); + return $this->error('System error'); + } + } + + public function orderList(Request $request) + { + $query = CreditIeoOrder::query() + ->leftJoin('users', 'credit_ieo_order.user_id', '=', 'users.id') + ->leftJoin('ieo_project', 'credit_ieo_order.project_id', '=', 'ieo_project.id') + ->select( + 'credit_ieo_order.*', + 'users.phone as user_phone', + 'users.email as user_email', + 'ieo_project.name as project_name', + 'ieo_project.symbol as symbol' + ); + + $status = $request->input('status'); + if ($status !== null && $status !== '') $query->where('credit_ieo_order.status', intval($status)); + $project_id = $request->input('project_id'); + if ($project_id) $query->where('credit_ieo_order.project_id', $project_id); + + $data = $query->orderBy('credit_ieo_order.id', 'desc')->paginate($request->input('limit', 20)); + $statusMap = ['0' => 'Pending Repayment', '1' => 'Repaid', '2' => 'Overdue']; + foreach ($data as $item) { + $item->status_text = $statusMap[$item->status] ?? ''; + } + return $this->layuiData($data); + } +} diff --git a/app/Http/Controllers/Api/CreditController.php b/app/Http/Controllers/Api/CreditController.php new file mode 100644 index 0000000..5118d5a --- /dev/null +++ b/app/Http/Controllers/Api/CreditController.php @@ -0,0 +1,250 @@ +error('Please login'); + $line = CreditLine::where('user_id', $user->id)->first(); + if (!$line) { + return $this->success([ + 'has_credit' => 0, + 'total_amount' => '0.00', + 'used_amount' => '0.00', + 'available_amount' => '0.00', + 'daily_rate' => '0.0000', + 'cycle_days' => 0, + 'credit_score' => 0, + 'status' => 0, + ]); + } + $line->has_credit = 1; + return $this->success($line); + } + + public function creditSubscribe(Request $request) + { + $user_id = Token::getUserIdByToken(Token::getToken()); + $user = \App\Users::find($user_id); + if (!$user) return $this->error('Please login'); + $project_id = intval($request->input('project_id')); + $amount = floatval($request->input('amount')); + if (!$project_id || $amount <= 0) return $this->error('Invalid params'); + + $project = IeoProject::find($project_id); + if (!$project || $project->status != 1) return $this->error('IEO not active'); + if ($amount < $project->min_buy) return $this->error('Min: ' . $project->min_buy . ' USDC'); + if ($project->max_buy > 0 && $amount > $project->max_buy) return $this->error('Max: ' . $project->max_buy . ' USDC'); + + $existingNormal = DB::table('ieo_order')->where('user_id', $user->id)->where('project_id', $project_id)->where('status', '!=', 2)->exists(); + if ($existingNormal) return $this->error('Already subscribed via normal IEO'); + + DB::beginTransaction(); + try { + $line = CreditLine::where('user_id', $user->id)->lockForUpdate()->first(); + if (!$line || $line->status != 1) { DB::rollBack(); return $this->error('No active credit line'); } + if ($line->available_amount < $amount) { DB::rollBack(); return $this->error('Insufficient credit'); } + + $existingCredit = CreditIeoOrder::where('user_id', $user->id)->where('project_id', $project_id)->whereIn('status', [0, 1])->exists(); + if ($existingCredit) { DB::rollBack(); return $this->error('Already subscribed via credit'); } + + $line->available_amount = bc_sub($line->available_amount, $amount, 6); + $line->used_amount = bc_add($line->used_amount, $amount, 6); + $line->save(); + + $token_amount = $project->token_price > 0 ? sprintf("%.6f", $amount / $project->token_price) : 0; + $order = CreditIeoOrder::create([ + 'user_id' => $user->id, + 'credit_line_id' => $line->id, + 'project_id' => $project_id, + 'amount' => $amount, + 'token_amount' => $token_amount, + 'status' => 0, + 'repaid_amount' => 0, + 'interest' => 0, + ]); + + AccountLog::insertLog( + ['user_id' => $user->id, 'value' => -$amount, 'info' => 'Credit IEO subscribe #' . $project->name, 'type' => AccountLog::MICRO_TRADE_SUBMIT, 'currency' => 3], + ['balance_type' => 1, 'wallet_id' => 0, 'lock_type' => 0, 'before' => bc_add($line->available_amount, $amount, 6), 'change' => -$amount, 'after' => $line->available_amount] + ); + + DB::commit(); + return $this->success(['order_id' => $order->id, 'msg' => 'Credit subscription successful']); + } catch (\Exception $e) { + DB::rollBack(); + return $this->error($e->getMessage()); + } + } + + public function myCreditOrders(Request $request) + { + $user_id = Token::getUserIdByToken(Token::getToken()); + $user = \App\Users::find($user_id); + if (!$user) return $this->error('Please login'); + $list = CreditIeoOrder::where('user_id', $user->id) + ->with('project:id,name,symbol,token_price,listing_time,unlock_time') + ->orderBy('id', 'desc') + ->get(); + $statusMap = ['0' => 'Pending Repayment', '1' => 'Repaid', '3' => 'Overdue']; + foreach ($list as $item) { + $item->status_text = $statusMap[$item->status] ?? ''; + $line = $item->creditLine ?? CreditLine::find($item->credit_line_id); + $dailyRate = $line ? floatval($line->daily_rate) : 0; + $createdTs = strtotime((string)$item->created_at); + if (!$createdTs) $createdTs = time(); + $days = max(1, (int)ceil((time() - $createdTs) / 86400)); + $item->accrued_interest = sprintf('%.6f', $item->amount * $dailyRate * $days); + $item->repay_total = sprintf('%.6f', $item->amount + ($item->amount * $dailyRate * $days)); + $item->days_elapsed = $days; + } + return $this->success($list); + } + + public function repay(Request $request) + { + $user_id = Token::getUserIdByToken(Token::getToken()); + $user = \App\Users::find($user_id); + if (!$user) return $this->error('Please login'); + $order_id = intval($request->input('order_id')); + if (!$order_id) return $this->error('Invalid order'); + + DB::beginTransaction(); + try { + $order = CreditIeoOrder::where('id', $order_id)->where('user_id', $user->id)->lockForUpdate()->first(); + if (!$order) { DB::rollBack(); return $this->error('Order not found'); } + if ($order->status == 1) { DB::rollBack(); return $this->error('Already repaid'); } + + $line = CreditLine::where('id', $order->credit_line_id)->lockForUpdate()->first(); + if (!$line) { DB::rollBack(); return $this->error('Credit line missing'); } + + $dailyRate = floatval($line->daily_rate); + $createdTs = strtotime((string)$order->created_at); + if (!$createdTs) $createdTs = time(); + $days = max(1, (int)ceil((time() - $createdTs) / 86400)); + $interest = sprintf('%.6f', $order->amount * $dailyRate * $days); + $total = bc_add($order->amount, $interest, 6); + + $wallet = UsersWallet::where('user_id', $user->id)->where('currency', 3)->lockForUpdate()->first(); + if (!$wallet || $wallet->legal_balance < $total) { DB::rollBack(); return $this->error('Insufficient balance to repay'); } + + $before = $wallet->legal_balance; + $wallet->legal_balance = bc_sub($wallet->legal_balance, $total, 6); + $wallet->save(); + AccountLog::insertLog( + ['user_id' => $user->id, 'value' => -$total, 'info' => 'Credit IEO Repay #' . $order->id, 'type' => AccountLog::IEO_OPERATION, 'currency' => 3], + ['balance_type' => 1, 'wallet_id' => $wallet->id, 'lock_type' => 0, 'before' => $before, 'change' => -$total, 'after' => $wallet->legal_balance] + ); + + $line->used_amount = max(0, bc_sub($line->used_amount, $order->amount, 6)); + $line->available_amount = bc_add($line->available_amount, $order->amount, 6); + if ($line->available_amount > $line->total_amount) { + $line->available_amount = $line->total_amount; + } + $line->save(); + + $order->interest = $interest; + $order->repaid_amount = $total; + $order->status = 1; + $order->save(); + + if ($order->token_amount > 0) { + $project = \App\IeoProject::find($order->project_id); + if ($project) { + $tokenCurrency = \App\Currency::where('name', $project->symbol)->first(); + if ($tokenCurrency) { + $tokenWallet = UsersWallet::where('user_id', $user->id) + ->where('currency', $tokenCurrency->id)->lockForUpdate()->first(); + if ($tokenWallet) { + $beforeToken = $tokenWallet->change_balance; + $tokenWallet->change_balance = bc_add($tokenWallet->change_balance, $order->token_amount, 6); + $tokenWallet->save(); + AccountLog::insertLog( + ['user_id' => $user->id, 'value' => $order->token_amount, 'info' => 'Credit IEO Token Unlock - ' . $project->name, 'type' => AccountLog::IEO_OPERATION, 'currency' => $tokenCurrency->id], + ['balance_type' => 2, 'wallet_id' => $tokenWallet->id, 'lock_type' => 0, 'before' => $beforeToken, 'change' => $order->token_amount, 'after' => $tokenWallet->change_balance] + ); + } + } + } + } + + DB::commit(); + return $this->success(['msg' => 'Repaid', 'total' => $total, 'interest' => $interest]); + } catch (\Exception $e) { + DB::rollBack(); + return $this->error($e->getMessage()); + } + } + + public function repayAll(Request $request) + { + $user_id = Token::getUserIdByToken(Token::getToken()); + $user = \App\Users::find($user_id); + if (!$user) return $this->error('Please login'); + + $credit = CreditLine::where('user_id', $user->id)->lockForUpdate()->first(); + if (!$credit) return $this->error('No credit line'); + + $orders = CreditIeoOrder::where('user_id', $user->id) + ->whereIn('status', [0, 3]) + ->get(); + + if ($orders->isEmpty()) return $this->error('No pending orders'); + + $totalRepay = 0; + foreach ($orders as $order) { + $days = max(1, ceil((time() - strtotime($order->created_at)) / 86400)); + $interest = sprintf('%.6f', $order->amount * floatval($credit->daily_rate) * $days); + $totalRepay = bc_add($totalRepay, bc_add($order->amount, $interest, 6), 6); + } + + $wallet = UsersWallet::where('user_id', $user->id)->where('currency', 3)->lockForUpdate()->first(); + if (!$wallet || bc_comp($wallet->legal_balance, $totalRepay) < 0) { + return $this->error('Insufficient balance, need ' . $totalRepay . ' USDC'); + } + + DB::beginTransaction(); + try { + $before = $wallet->legal_balance; + foreach ($orders as $order) { + $days = max(1, ceil((time() - strtotime($order->created_at)) / 86400)); + $interest = sprintf('%.6f', $order->amount * floatval($credit->daily_rate) * $days); + $repayAmount = bc_add($order->amount, $interest, 6); + + $wallet->legal_balance = bc_sub($wallet->legal_balance, $repayAmount, 6); + $credit->used_amount = bc_sub($credit->used_amount, $order->amount, 6); + $credit->available_amount = bc_add($credit->available_amount, $order->amount, 6); + + $order->update(['status' => 1, 'repaid_amount' => $repayAmount, 'interest' => $interest]); + } + $wallet->save(); + AccountLog::insertLog( + ['user_id' => $user->id, 'value' => -$totalRepay, 'info' => 'Credit IEO Repay All', 'type' => AccountLog::IEO_OPERATION, 'currency' => 3], + ['balance_type' => 1, 'wallet_id' => $wallet->id, 'lock_type' => 0, 'before' => $before, 'change' => -$totalRepay, 'after' => $wallet->legal_balance] + ); + if (bc_comp($credit->available_amount, $credit->total_amount) > 0) { + $credit->available_amount = $credit->total_amount; + } + $credit->save(); + DB::commit(); + return $this->success('All orders repaid'); + } catch (\Throwable $e) { + DB::rollBack(); + return $this->error($e->getMessage()); + } + } +} diff --git a/resources/views/manages/credit/index.blade.php b/resources/views/manages/credit/index.blade.php new file mode 100644 index 0000000..4bbd55d --- /dev/null +++ b/resources/views/manages/credit/index.blade.php @@ -0,0 +1,139 @@ +@extends('admin._layoutNew') +@section('title', '信用额度管理') +@section('page-content') +
+
信用额度管理
+
+
+
+ +
+
+ +
+
+ + + +
+
+
+
+
+ + + + +@endsection +@section('scripts') + + +@endsection diff --git a/resources/views/manages/credit/orders.blade.php b/resources/views/manages/credit/orders.blade.php new file mode 100644 index 0000000..81d4006 --- /dev/null +++ b/resources/views/manages/credit/orders.blade.php @@ -0,0 +1,61 @@ +@extends('admin._layoutNew') +@section('title', '信用IEO订单') +@section('page-content') +
+
信用IEO订单管理
+
+
+
+ +
+
+ +
+
+ + +
+
+
+
+
+ + + +@endsection +@section('scripts') + +@endsection