feat: C2C商家交易系统后端 — API+后台管理

This commit is contained in:
testadmin
2026-04-28 14:35:00 +08:00
commit db5ed78ea9
5 changed files with 312 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class C2cMerchant extends Model
{
protected $table = 'c2c_merchant';
protected $fillable = [
'name','logo','available_qty','min_amount','max_amount','unit_price',
'crypto_currency','fiat_currency','payment_methods','rating',
'contact_type','contact_link','status','sort'
];
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class C2cMerchantOrder extends Model
{
protected $table = 'c2c_merchant_order';
protected $fillable = [
'order_no','user_id','merchant_id','direction','crypto_currency','fiat_currency',
'amount','unit_price','total_price','fee','fee_rate','status',
'contact_type','contact_link','expire_time'
];
public function merchant() { return $this->belongsTo(C2cMerchant::class, 'merchant_id'); }
public function user() { return $this->belongsTo(Users::class, 'user_id'); }
}
@@ -0,0 +1,115 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Api\Controller;
use App\C2cMerchant;
use App\C2cMerchantOrder;
use Illuminate\Http\Request;
use App\Token;
use App\Users;
class C2cMerchantController extends Controller
{
public function list(Request $request)
{
$query = C2cMerchant::where('status', 1);
$crypto = $request->input('crypto_currency');
if ($crypto) $query->where('crypto_currency', $crypto);
$fiat = $request->input('fiat_currency');
if ($fiat) $query->where('fiat_currency', $fiat);
$list = $query->orderBy('sort', 'desc')->orderBy('id', 'desc')->get();
return $this->success($list);
}
public function createOrder(Request $request)
{
$user_id = Token::getUserIdByToken(Token::getToken());
$user = \App\Users::find($user_id);
if (!$user) return $this->error('Please login');
$merchant_id = intval($request->input('merchant_id'));
$direction = $request->input('direction'); // buy / sell
$amount = floatval($request->input('amount'));
if (!in_array($direction, ['buy', 'sell'])) return $this->error('Invalid direction');
if ($amount <= 0) return $this->error('Invalid amount');
$merchant = C2cMerchant::find($merchant_id);
if (!$merchant || $merchant->status != 1) return $this->error('Merchant not available');
if ($amount < $merchant->min_amount) return $this->error('Min: ' . $merchant->min_amount);
if ($merchant->max_amount > 0 && $amount > $merchant->max_amount) return $this->error('Max: ' . $merchant->max_amount);
if ($merchant->available_qty > 0 && $amount > $merchant->available_qty) return $this->error('Insufficient merchant stock');
$unit_price = floatval($merchant->unit_price);
$total_price = round($amount * $unit_price, 6);
$fee_rate = 0.002; // 0.2%
$fee = round($total_price * $fee_rate, 6);
$order_no = date('YmdHis') . rand(1000, 9999);
$expire_time = date('Y-m-d H:i:s', time() + 1800);
$order = C2cMerchantOrder::create([
'order_no' => $order_no,
'user_id' => $user->id,
'merchant_id' => $merchant_id,
'direction' => $direction,
'crypto_currency' => $merchant->crypto_currency,
'fiat_currency' => $merchant->fiat_currency,
'amount' => $amount,
'unit_price' => $unit_price,
'total_price' => $total_price,
'fee' => $fee,
'fee_rate' => $fee_rate,
'status' => 0,
'contact_type' => $merchant->contact_type,
'contact_link' => $merchant->contact_link,
'expire_time' => $expire_time,
]);
$order->merchant_name = $merchant->name;
return $this->success($order);
}
public function myOrders(Request $request)
{
$user_id = Token::getUserIdByToken(Token::getToken());
$user = \App\Users::find($user_id);
if (!$user) return $this->error('Please login');
$query = C2cMerchantOrder::where('user_id', $user->id)
->with('merchant:id,name,logo');
$status = $request->input('status');
if ($status !== null && $status !== '' && $status !== 'all') {
$query->where('status', intval($status));
}
$data = $query->orderBy('id', 'desc')->paginate(intval($request->input('limit', 20)));
$statusMap = [0 => 'Pending', 1 => 'Completed', 2 => 'Cancelled', 3 => 'Expired'];
foreach ($data as $item) {
$item->status_text = $statusMap[$item->status] ?? '';
}
return $this->success($data);
}
public function orderDetail(Request $request)
{
$user_id = Token::getUserIdByToken(Token::getToken());
$user = \App\Users::find($user_id);
if (!$user) return $this->error('Please login');
$id = intval($request->input('id'));
$order = C2cMerchantOrder::with('merchant:id,name,logo,contact_type,contact_link')
->where('id', $id)->where('user_id', $user->id)->first();
if (!$order) return $this->error('Order not found');
$statusMap = [0 => 'Pending', 1 => 'Completed', 2 => 'Cancelled', 3 => 'Expired'];
$order->status_text = $statusMap[$order->status] ?? '';
return $this->success($order);
}
}
@@ -0,0 +1,104 @@
@extends('admin._layoutNew')
@section('title', 'C2C商家管理')
@section('page-content')
<div class="layui-card">
<div class="layui-card-header">C2C商家管理</div>
<div class="layui-card-body">
<button class="layui-btn layui-btn-sm" onclick="addMerchant()"><i class="layui-icon">&#xe654;</i> 添加商家</button>
<button class="layui-btn layui-btn-sm layui-btn-primary" onclick="loadData()"><i class="layui-icon">&#xe9aa;</i> 刷新</button>
<table id="merchantTable" lay-filter="merchantTable"></table>
</div>
</div>
<script type="text/html" id="statusTpl">
@{{# if(d.status==1){ }}<span class="layui-badge layui-bg-green">启用</span>@{{# } }}
@{{# if(d.status==0){ }}<span class="layui-badge layui-bg-gray">禁用</span>@{{# } }}
</script>
<script type="text/html" id="logoTpl">
@{{# if(d.logo){ }}<img src="@{{d.logo}}" style="height:28px;border-radius:4px;">@{{# } else { }}-@{{# } }}
</script>
<script type="text/html" id="actionTpl">
<button class="layui-btn layui-btn-xs" lay-event="edit">编辑</button>
<button class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</button>
</script>
@endsection
@section('scripts')
<script>
layui.use(['table','layer','form'], function(){
var table = layui.table;
window.loadData = function(){ table.reload('merchantTable'); };
table.render({
elem:'#merchantTable', url:'/admin/c2c_merchant/list', page:true,
cols:[[
{field:'id',title:'ID',width:60,sort:true},
{field:'logo',title:'Logo',width:70,templet:'#logoTpl'},
{field:'name',title:'商家名称',width:140},
{field:'crypto_currency',title:'加密货币',width:80},
{field:'fiat_currency',title:'法币',width:70},
{field:'unit_price',title:'单价',width:100},
{field:'available_qty',title:'库存',width:100},
{field:'min_amount',title:'最小限额',width:90},
{field:'max_amount',title:'最大限额',width:100},
{field:'payment_methods',title:'支付方式',width:160},
{field:'rating',title:'好评率',width:80},
{field:'contact_type',title:'联系方式',width:90},
{field:'contact_link',title:'联系链接',width:140},
{field:'sort',title:'排序',width:70},
{field:'status',title:'状态',width:90,templet:'#statusTpl'},
{fixed:'right',title:'操作',width:140,toolbar:'#actionTpl'}
]]
});
table.on('tool(merchantTable)', function(obj){
if(obj.event==='edit') editMerchant(obj.data);
if(obj.event==='del'){
layer.confirm('确定删除该商家?',function(i){
$.post('/admin/c2c_merchant/delete',{id:obj.data.id,_token:'{{csrf_token()}}'},function(r){
layer.close(i);
if(r.type=='ok'){layer.msg('删除成功');loadData();}else layer.msg(r.message,{icon:2});
});
});
}
});
window.addMerchant = function(){ openForm({}); };
window.editMerchant = function(d){ openForm(d); };
function openForm(d){
var html = '<form class="layui-form" style="padding:20px;">'
+'<div class="layui-form-item"><label class="layui-form-label">商家名称</label><div class="layui-input-block"><input name="name" class="layui-input" value="'+(d.name||'')+'"></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">Logo URL</label><div class="layui-input-block"><input name="logo" class="layui-input" value="'+(d.logo||'')+'"></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">加密货币</label><div class="layui-input-block"><input name="crypto_currency" class="layui-input" value="'+(d.crypto_currency||'USDT')+'"></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">法币</label><div class="layui-input-block"><input name="fiat_currency" class="layui-input" value="'+(d.fiat_currency||'USD')+'"></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">单价</label><div class="layui-input-block"><input name="unit_price" class="layui-input" value="'+(d.unit_price||'')+'"></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">库存数量</label><div class="layui-input-block"><input name="available_qty" class="layui-input" value="'+(d.available_qty||0)+'"></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">最小限额</label><div class="layui-input-block"><input name="min_amount" class="layui-input" value="'+(d.min_amount||100)+'"></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">最大限额</label><div class="layui-input-block"><input name="max_amount" class="layui-input" value="'+(d.max_amount||100000)+'"></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">支付方式</label><div class="layui-input-block"><input name="payment_methods" class="layui-input" value="'+(d.payment_methods||'银行卡')+'" placeholder="逗号分隔"></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">好评率(%)</label><div class="layui-input-block"><input name="rating" class="layui-input" value="'+(d.rating||99.99)+'" placeholder="0-100"></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">联系类型</label><div class="layui-input-block"><select name="contact_type"><option value="whatsapp" '+(d.contact_type=='whatsapp'?'selected':'')+'>WhatsApp</option><option value="telegram" '+(d.contact_type=='telegram'?'selected':'')+'>Telegram</option><option value="email" '+(d.contact_type=='email'?'selected':'')+'>Email</option></select></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">联系链接</label><div class="layui-input-block"><input name="contact_link" class="layui-input" value="'+(d.contact_link||'')+'" placeholder="WhatsApp号码或TG用户名"></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">排序</label><div class="layui-input-block"><input name="sort" class="layui-input" value="'+(d.sort||0)+'"></div></div>'
+'<div class="layui-form-item"><label class="layui-form-label">状态</label><div class="layui-input-block"><select name="status"><option value="1" '+(d.status==1?'selected':'')+'>启用</option><option value="0" '+(d.status==0?'selected':'')+'>禁用</option></select></div></div>'
+'</form>';
layer.open({
type:1, title:d.id?'编辑商家':'添加商家', area:['520px','680px'], content:html,
btn:['保存','取消'],
yes:function(index,layero){
var form = layero.find('form');
var postData = {};
form.find('input,select,textarea').each(function(){postData[this.name]=this.value;});
if(d.id) postData.id = d.id;
postData._token = '{{csrf_token()}}';
$.post('/admin/c2c_merchant/save', postData, function(r){
layer.close(index);
if(r.type=='ok'){layer.msg('保存成功');loadData();}else layer.msg(r.message,{icon:2});
});
}
});
}
});
</script>
@endsection
@@ -0,0 +1,64 @@
@extends('admin._layoutNew')
@section('title', 'C2C商家订单')
@section('page-content')
<div class="layui-card">
<div class="layui-card-header">C2C商家订单管理</div>
<div class="layui-card-body">
<div class="layui-btn-group" style="margin-bottom:15px;">
<button class="layui-btn layui-btn-sm" onclick="loadData()"><i class="layui-icon">&#xe9aa;</i> 刷新</button>
</div>
<table id="orderTable" lay-filter="orderTable"></table>
</div>
</div>
<script type="text/html" id="statusTpl">
@{{# if(d.status==0){ }}<span class="layui-badge layui-bg-orange">待处理</span>@{{# } }}
@{{# if(d.status==1){ }}<span class="layui-badge layui-bg-green">已完成</span>@{{# } }}
@{{# if(d.status==2){ }}<span class="layui-badge layui-bg-gray">已取消</span>@{{# } }}
@{{# if(d.status==3){ }}<span class="layui-badge">已过期</span>@{{# } }}
</script>
<script type="text/html" id="actionTpl">
@{{# if(d.status==0){ }}<button class="layui-btn layui-btn-xs layui-btn-normal" lay-event="complete">完成</button>@{{# } else { }}-@{{# } }}
</script>
@endsection
@section('scripts')
<script>
layui.use(['table','layer'], function(){
var table = layui.table;
window.loadData = function(){ table.reload('orderTable'); };
table.render({
elem:'#orderTable', url:'/admin/c2c_merchant/order/list', page:true,
cols:[[
{field:'id',title:'ID',width:60},
{field:'order_no',title:'订单号',width:170},
{field:'user_id',title:'用户ID',width:70},
{field:'user_email',title:'邮箱',width:180},
{field:'merchant_name',title:'商家',width:140},
{field:'direction_text',title:'方向',width:80},
{field:'crypto_currency',title:'加密货币',width:80},
{field:'amount',title:'数量',width:100},
{field:'unit_price',title:'单价',width:100},
{field:'total_price',title:'总价',width:110},
{field:'fee',title:'手续费',width:90},
{field:'status',title:'状态',width:100,templet:'#statusTpl'},
{field:'created_at',title:'创建时间',width:170},
{field:'expire_time',title:'过期时间',width:170},
{fixed:'right',title:'操作',width:110,toolbar:'#actionTpl'}
]]
});
table.on('tool(orderTable)', function(obj){
if(obj.event==='complete'){
layer.confirm('确定将此订单标记为已完成?',function(i){
$.post('/admin/c2c_merchant/order/complete',{id:obj.data.id,_token:'{{csrf_token()}}'},function(r){
layer.close(i);
if(r.type=='ok'){layer.msg('操作成功');loadData();}else layer.msg(r.message,{icon:2});
});
});
}
});
});
</script>
@endsection