diff --git a/app/Console/Commands/AutoOptionSettle.php b/app/Console/Commands/AutoOptionSettle.php new file mode 100644 index 0000000..4aa0b3d --- /dev/null +++ b/app/Console/Commands/AutoOptionSettle.php @@ -0,0 +1,86 @@ +where('delivery_time', '<=', now()) + ->pluck('id'); + + if ($orderIds->isEmpty()) return; + + foreach ($orderIds as $orderId) { + DB::beginTransaction(); + try { + $affected = OptionOrder::where('id', $orderId)->where('status', 0) + ->update(['status' => 99]); + if ($affected === 0) { + DB::rollBack(); + continue; + } + + $order = OptionOrder::lockForUpdate()->find($orderId); + $pair = OptionPair::find($order->pair_id); + $match = $pair ? CurrencyMatch::find($pair->currency_match_id) : null; + $currency = $match ? Currency::find($match->currency_id) : null; + $close_price = $currency ? sprintf("%.6f", $currency->price) : '0'; + + if (bccomp($close_price, '0', 6) <= 0) { + $order->update(['status' => 0]); + DB::commit(); + $this->warn("Order #{$orderId}: close_price=0, skip"); + continue; + } + + $cmp = bccomp($close_price, sprintf("%.6f", $order->open_price), 6); + $win = $order->direction == 'up' ? $cmp > 0 : $cmp < 0; + + $income = $win + ? bcmul(sprintf("%.6f", $order->amount), bcdiv(sprintf("%.6f", $order->profit_rate), '100', 8), 6) + : bcmul(sprintf("%.6f", $order->amount), '-1', 6); + + $order->update([ + 'close_price' => $close_price, + 'income' => $income, + 'status' => $win ? 1 : 2, + ]); + + if ($win) { + $wallet = UsersWallet::where('user_id', $order->user_id) + ->where('currency', 3)->lockForUpdate()->first(); + if ($wallet) { + $return_amount = bcadd(sprintf("%.6f", $order->amount), $income, 6); + $before = $wallet->legal_balance; + $wallet->legal_balance = bcadd($wallet->legal_balance, $return_amount, 6); + $wallet->save(); + AccountLog::insertLog( + ['user_id' => $order->user_id, 'value' => $return_amount, 'info' => 'Option Win - ' . $order->symbol, 'type' => AccountLog::MICRO_TRADE_CLOSE_SETTLE, 'currency' => 3], + ['balance_type' => 1, 'wallet_id' => $wallet->id, 'lock_type' => 0, 'before' => $before, 'change' => $return_amount, 'after' => $wallet->legal_balance] + ); + } + } + + DB::commit(); + $this->info("Order #{$orderId} settled: " . ($win ? 'WIN' : 'LOSE')); + } catch (\Exception $e) { + DB::rollBack(); + $this->error("Order #{$orderId} error: " . $e->getMessage()); + } + } + } +} diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php new file mode 100644 index 0000000..b3dea33 --- /dev/null +++ b/app/Http/Controllers/Admin/OptionController.php @@ -0,0 +1,92 @@ +leftJoin('users', 'option_order.user_id', '=', 'users.id') + ->leftJoin('option_pair', 'option_order.pair_id', '=', 'option_pair.id') + ->select('option_order.*', 'users.phone as user_phone', 'users.email as user_email', 'option_pair.coinname'); + + $keyword = $request->input('keyword'); + if ($keyword) { + $query->where(function ($q) use ($keyword) { + $q->where('option_order.user_id', $keyword) + ->orWhere('users.email', 'like', "%{$keyword}%") + ->orWhere('users.phone', 'like', "%{$keyword}%"); + }); + } + $status = $request->input('status'); + if ($status !== null && $status !== '') { + $query->where('option_order.status', intval($status)); + } + + $data = $query->orderBy('option_order.id', 'desc')->paginate($request->input('limit', 20)); + $statusMap = ['0'=>'待交割','1'=>'盈利','2'=>'亏损','3'=>'已取消']; + foreach ($data as $item) { + $item->status_text = $statusMap[$item->status] ?? ''; + $item->direction_text = $item->direction == 'up' ? '买涨' : '买跌'; + } + return $this->layuiData($data); + } + + public function settle(Request $request) + { + $ids = $request->input('ids'); + $force = $request->input('force', ''); + if (!$ids) return $this->error('请先选择订单'); + $orders = OptionOrder::whereIn('id', explode(',', $ids))->where('status', 0)->get(); + if ($orders->isEmpty()) return $this->error('没有待结算订单'); + + $settled = 0; + DB::beginTransaction(); + try { + foreach ($orders as $order) { + $pair = OptionPair::find($order->pair_id); + $match = $pair ? CurrencyMatch::find($pair->currency_match_id) : null; + $currency = $match ? Currency::find($match->currency_id) : null; + $close_price = $currency ? floatval($currency->price) : 0; + + if ($force === 'win') $win = true; + elseif ($force === 'lose') $win = false; + else $win = $order->direction == 'up' ? $close_price > $order->open_price : $close_price < $order->open_price; + + $income = $win ? sprintf("%.6f", $order->amount * $order->profit_rate / 100) : -$order->amount; + $order->update(['close_price' => $close_price, 'income' => $income, 'status' => $win ? 1 : 2]); + + $wallet = UsersWallet::where('user_id', $order->user_id)->where('currency', 3)->first(); + if ($wallet) { + $return_amount = $win ? $order->amount + abs($income) : 0; + if ($return_amount > 0) { + $before = $wallet->legal_balance; + $wallet->legal_balance = bc_add($wallet->legal_balance, $return_amount, 6); + $wallet->save(); + AccountLog::insertLog( + ['user_id'=>$order->user_id,'value'=>$return_amount,'info'=>'Option '.($win?'Win':'Refund').' - '.$order->symbol,'type'=>AccountLog::MICRO_TRADE_CLOSE_SETTLE,'currency'=>3], + ['balance_type'=>1,'wallet_id'=>$wallet->id,'lock_type'=>0,'before'=>$before,'change'=>$return_amount,'after'=>$wallet->legal_balance] + ); + } + } + $settled++; + } + DB::commit(); + return $this->success("已结算 {$settled} 个订单"); + } catch (\Exception $e) { + DB::rollBack(); + return $this->error($e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Api/OptionController.php b/app/Http/Controllers/Api/OptionController.php new file mode 100644 index 0000000..89aabf6 --- /dev/null +++ b/app/Http/Controllers/Api/OptionController.php @@ -0,0 +1,111 @@ +orderBy('weigh', 'desc')->get(); + foreach ($pairs as $p) { + $match = CurrencyMatch::find($p->currency_match_id); + if ($match) { + $currency = Currency::find($match->currency_id); + $p->close = $currency ? $currency->price : 0; + $p->price_change = $currency ? ($currency->change ?? 0) : 0; + $p->high = $currency ? ($currency->high ?? $p->close) : 0; + $p->low = $currency ? ($currency->low ?? $p->close) : 0; + $p->match_legal_id = $match->legal_id; + $p->match_currency_id = $match->currency_id; + $p->volume_24h = $currency ? ($currency->vol ?? 0) : 0; + } + } + return $this->success($pairs); + } + + public function placeOrder(Request $request) + { + $user_id = Token::getUserIdByToken(Token::getToken()); + $user = \App\Users::find($user_id); + if (!$user) return $this->error('Please login'); + + $pair_id = $request->input('pair_id'); + $direction = $request->input('direction'); + $amount = floatval($request->input('amount')); + $delivery_time = $request->input('delivery_time'); + + if (!in_array($direction, ['up', 'down'])) return $this->error('Invalid direction'); + if ($amount <= 0) return $this->error('Invalid amount'); + if (!$delivery_time) return $this->error('Select delivery time'); + + $pair = OptionPair::where('id', $pair_id)->where('status', 1)->first(); + if (!$pair) return $this->error('Pair not available'); + if ($amount < $pair->min_amount) return $this->error('Min: ' . $pair->min_amount . ' USDC'); + if ($amount > $pair->max_amount) return $this->error('Max: ' . $pair->max_amount . ' USDC'); + + $delivery_ts = is_numeric($delivery_time) ? date('Y-m-d H:i:s', $delivery_time) : $delivery_time; + if (strtotime($delivery_ts) <= time() + 10) return $this->error('Delivery time must be in the future'); + + $match = CurrencyMatch::find($pair->currency_match_id); + $currency = $match ? Currency::find($match->currency_id) : null; + $open_price = $currency ? floatval($currency->price) : 0; + $fee = sprintf("%.6f", $amount * $pair->fee_rate / 100); + + DB::beginTransaction(); + try { + $wallet = UsersWallet::where('user_id', $user->id)->where('currency', 3)->lockForUpdate()->first(); + if (!$wallet || $wallet->legal_balance < $amount) { + DB::rollBack(); + return $this->error('Insufficient balance'); + } + $before = $wallet->legal_balance; + $wallet->legal_balance = bc_sub($wallet->legal_balance, $amount, 6); + $wallet->save(); + AccountLog::insertLog( + ['user_id' => $user->id, 'value' => -$amount, 'info' => 'Option ' . strtoupper($direction) . ' - ' . $pair->coinname, 'type' => AccountLog::MICRO_TRADE_SUBMIT, 'currency' => 3], + ['balance_type' => 1, 'wallet_id' => $wallet->id, 'lock_type' => 0, 'before' => $before, 'change' => -$amount, 'after' => $wallet->legal_balance] + ); + OptionOrder::create([ + 'user_id' => $user->id, 'pair_id' => $pair_id, + 'symbol' => $pair->symbol, 'direction' => $direction, + 'amount' => $amount, 'fee' => $fee, + 'open_price' => $open_price, 'profit_rate' => $pair->profit_rate, + 'delivery_time' => $delivery_ts, 'status' => 0, + ]); + DB::commit(); + return $this->success('Order placed'); + } catch (\Exception $e) { + DB::rollBack(); + return $this->error($e->getMessage()); + } + } + + public function myOrders(Request $request) + { + $user_id = Token::getUserIdByToken(Token::getToken()); + $user = \App\Users::find($user_id); + if (!$user) return $this->error('Please login'); + $status = $request->input('status', 'all'); + $query = OptionOrder::where('user_id', $user->id)->with('pair:id,coinname'); + if ($status !== 'all') $query->where('status', $status); + $list = $query->orderBy('id', 'desc')->paginate(20); + $statusMap = ['0'=>'Pending','1'=>'Win','2'=>'Lose','3'=>'Canceled']; + foreach ($list as $item) { + $item->status_text = $statusMap[$item->status] ?? ''; + $item->direction_text = $item->direction == 'up' ? 'Buy Up' : 'Buy Down'; + $item->coinname = $item->pair ? $item->pair->coinname : $item->symbol; + } + return $this->success($list); + } +} diff --git a/app/OptionOrder.php b/app/OptionOrder.php new file mode 100644 index 0000000..056a45d --- /dev/null +++ b/app/OptionOrder.php @@ -0,0 +1,10 @@ +belongsTo(OptionPair::class, 'pair_id'); } +} diff --git a/app/OptionPair.php b/app/OptionPair.php new file mode 100644 index 0000000..e64da8c --- /dev/null +++ b/app/OptionPair.php @@ -0,0 +1,9 @@ + +
期权订单管理
+
+
+
+ +
+
+ +
+
+ +
+
+
+ + + + +
+
+
+ +@endsection +@section('scripts') + +@endsection