feat: 信用IEO系统 — 授信+申购+还款+逾期+后台
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\CreditLine;
|
||||
use App\CreditIeoOrder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CreditController extends Controller
|
||||
{
|
||||
public function index() { return view('manages.credit.index'); }
|
||||
public function orderIndex() { return view('manages.credit.orders'); }
|
||||
|
||||
public function list(Request $request)
|
||||
{
|
||||
$query = CreditLine::query()
|
||||
->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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Api\Controller;
|
||||
use App\CreditLine;
|
||||
use App\CreditIeoOrder;
|
||||
use App\IeoProject;
|
||||
use App\UsersWallet;
|
||||
use App\AccountLog;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Token;
|
||||
use App\Users;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CreditController extends Controller
|
||||
{
|
||||
public function dashboard(Request $request)
|
||||
{
|
||||
$user_id = Token::getUserIdByToken(Token::getToken());
|
||||
$user = \App\Users::find($user_id);
|
||||
if (!$user) return $this->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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user